diff --git a/.github/actions/setup-project-bun/action.yml b/.github/actions/setup-project-bun/action.yml index 1213761b085..f758ad64ff8 100644 --- a/.github/actions/setup-project-bun/action.yml +++ b/.github/actions/setup-project-bun/action.yml @@ -1,8 +1,18 @@ name: Setup project Bun description: >- - Install the Bun runtime for a job. Installs the version declared in - package.json (dependencies.bun), keeping the runtime SOT in one place so - version bumps only touch package.json and bun.lock. + Install the package.json Bun pin for a job's role: dependencies.bun for + runtime (default), or testRunnerBun for test-runner. + +# Runtime 1.4.2 fixes #6684's Windows Bun.fetch stream truncation. The test runner +# stays on 1.4.0: #4821 reverted #4064 after 1.4.2's multi-file --isolate preload +# segfault; #6713 run 37631558263 reproduces it on Linux 1/4, macOS 2/2, Windows 9/9. +# Rejoin only after the upstream runner crash is fixed and a green lane=all +# dispatch proves the unified version; then delete testRunnerBun and this role input. +inputs: + role: + description: Bun role to install (runtime or test-runner). + required: false + default: runtime outputs: version: @@ -15,14 +25,28 @@ runs: - name: Resolve project Bun version id: resolve shell: bash + env: + BUN_ROLE: ${{ inputs.role }} run: | - version="$(node -p "require('./package.json').dependencies.bun")" - if [ -z "$version" ]; then - echo "::error::Could not resolve Bun version from package.json" - exit 1 - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "Using Bun $version from package.json" + node <<'NODE' + const pkg = require('./package.json'); + const role = process.env.BUN_ROLE; + let key; + let version; + switch (role) { + case 'runtime': key = 'dependencies.bun'; version = pkg.dependencies?.bun; break; + case 'test-runner': key = 'testRunnerBun'; version = pkg.testRunnerBun; break; + default: + console.error(`::error::Unknown Bun role: ${role}`); + process.exit(1); + } + if (typeof version !== 'string' || version !== version.trim() || !/^\d+\.\d+\.\d+$/.test(version)) { + console.error(`::error::Missing or invalid package.json ${key} for Bun role ${role}`); + process.exit(1); + } + require('node:fs').appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`); + console.log(`Using Bun ${version} from package.json ${key} (${role})`); + NODE - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab43fa93915..e5624b4b2c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -549,6 +549,11 @@ jobs: printf 'Scheduled run: workflow from %s, testing dev at %s\n' "$GITHUB_SHA" "$sha" >> "$GITHUB_STEP_SUMMARY" printf 'scheduled run tests dev at %s (workflow %s)\n' "$sha" "$GITHUB_SHA" + # Bun roles (#6713): runtime 1.4.2 fixes #6684's Windows Bun.fetch streaming; + # test-runner 1.4.0 avoids 1.4.2's multi-file --isolate preload segfault (#4821 + # reverted #4064; run 37631558263: Linux 1/4, macOS 2/2, Windows 9/9). + # Rejoin after the upstream runner crash is fixed and a green lane=all dispatch + # proves the unified version, then delete testRunnerBun and the action's role input. # The suite, split by file across four Linux runners. # # `scripts/ci/run-bun-test-batches.sh` assigns files to shards by the per-file @@ -603,6 +608,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. # The GUI install is NOT optional here, however unrelated it looks to a # test shard. Several files under tests/ import JSX-bearing modules from @@ -649,6 +656,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -689,6 +698,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -725,6 +736,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -824,6 +837,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -893,6 +908,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -1035,6 +1052,8 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner # See the Bun roles contract above test. - name: Install dependencies run: | @@ -1240,8 +1259,8 @@ jobs: run: bun run structure:check # The composite Bun setup, run on each runner family it serves, when a change touches only the - # action (see the setup_action filter). One step past the action proves it installed the runtime - # package.json declares; nothing else runs, so this never grows into a suite. + # action (see the setup_action filter). Each role proves its resolved and installed + # version matches package.json; nothing else runs, so this never grows into a suite. setup-action: name: setup action ${{ matrix.os }} needs: changes @@ -1277,6 +1296,26 @@ jobs: exit 1 fi + - name: Setup project Bun test runner + id: bun-test-runner + uses: ./.github/actions/setup-project-bun + with: + role: test-runner + + - name: Require the test runner package.json declares + shell: bash + env: + RESOLVED: ${{ steps.bun-test-runner.outputs.version }} + run: | + set -euo pipefail + declared="$(node -p "require('./package.json').testRunnerBun")" + installed="$(bun --version)" + echo "declared=$declared resolved=$RESOLVED installed=$installed" + if [ "$RESOLVED" != "$declared" ] || [ "$installed" != "$declared" ]; then + echo "::error::setup-project-bun resolved '$RESOLVED' and installed '$installed', but package.json testRunnerBun declares '$declared'" + exit 1 + fi + # The Rust remote-workspace helper, when a change touches it (see the remote_helper filter). # Formatting once, then clippy and the crate's tests on each platform: the sandbox and the live # confinement tests compile only on macOS and Windows, and Linux covers the protocol and stub. @@ -1432,10 +1471,8 @@ jobs: persist-credentials: false ref: ${{ needs.changes.outputs.checkout_ref }} - - name: Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: 1.3.14 + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun - name: Install dependencies run: | diff --git a/.github/workflows/dev-version-bump.yml b/.github/workflows/dev-version-bump.yml index b54a6d76d31..99a8946c3e4 100644 --- a/.github/workflows/dev-version-bump.yml +++ b/.github/workflows/dev-version-bump.yml @@ -78,10 +78,13 @@ jobs: persist-credentials: false # The repository-owned composite action, not a hand-pinned setup-bun SHA: it - # resolves the Bun version from package.json so the runtime SOT stays in one - # place. An independently pinned action here would drift from every other job. + # selects package.json's test-runner pin even for this single-file test so + # validation follows the CI runner contract (see setup-project-bun/action.yml). + # This job edits version metadata; it does not compile a shipped runtime. - name: Setup project Bun uses: ./.github/actions/setup-project-bun + with: + role: test-runner - name: Install dependencies run: bun install --frozen-lockfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e28dac23c2..4c74e70a2d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,8 +33,8 @@ For local development commands, architecture notes, and release workflow details contributing guide above instead of duplicating instructions here. Source development requires the `bun` CLI on your `PATH`. The published npm package bundles its own -Bun runtime for end users, but contributor commands such as `bun install`, `bun run test`, and -`bun run prepush` run from your local Bun installation. +Bun runtime for end users. Package scripts such as `bun run test` and `bun run prepush` may resolve +Bun through that bundled dependency. ## Pull request contract @@ -61,6 +61,10 @@ A ready-for-review PR is the author's claim that the change is complete, underst ## Local validation and hooks +The suite uses Bun 1.4.0 (`testRunnerBun`) while the shipped runtime is 1.4.2. +`scripts/test.ts` finds a 1.4.0 binary on PATH or in `~/.bun/bin`, or uses +`OCX_TEST_RUNNER_BUN`; install that version locally if it is missing. + Run `bun run test` before review readiness. If the full local suite is too costly for the task or available resources, run at least focused regression tests for the changed behavior. Document the reason, commands, results, and remaining diff --git a/Dockerfile b/Dockerfile index f8cfc4002b8..c8e587fe934 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,12 @@ # syntax=docker/dockerfile:1 # Keep the runtime aligned with package.json and pin the multi-platform image index. -ARG BUN_IMAGE=oven/bun:1.4.0@sha256:5ff609364c049b54eb0ff560ec96319729a972078ef2c755d758f0c6ef89c2d6 +# Runtime 1.4.2 fixes #6684's Windows Bun.fetch streaming; CI tests stay on 1.4.0 +# because 1.4.2 segfaults reloading preloads in multi-file --isolate (#4821; +# #6713 run 37631558263: Linux 1/4, macOS 2/2, Windows 9/9). Rejoin after the +# upstream crash fix passes a unified-version lane=all dispatch, then delete +# package.json's testRunnerBun pin and setup-project-bun's role input. +ARG BUN_IMAGE=oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895 FROM ${BUN_IMAGE} AS manifest WORKDIR /home/bun/app diff --git a/README.md b/README.md index ec815156577..96f95ba64a3 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ OpenCodex tracks process-retained state in the categories below. Each has a docu store, which is pinned and never evicted. - **4 observed buffers** (translator accumulators, image/OAuth/Grok tails) are monitored for in-flight byte pressure without eviction. -- **28 state-store registrations** handle expiry sweeps (60 s interval) and +- **31 state-store registrations** handle expiry sweeps (60 s interval) and config-generation reconciliation so stale provider/account keys are removed. - **Path and fingerprint memos** (workspace metadata, hardened identities, installation salts, mode-hint capabilities) use insertion-order LRU caps (8–128 entries). @@ -458,8 +458,8 @@ Acknowledging a report is not the same as triaging it, and no first-response tar ## Development -Source development requires the `bun` CLI on your `PATH`. This is separate from the published npm -package's bundled Bun runtime, which is used only by installed `ocx` commands. +Source development requires the `bun` CLI on your `PATH`. The published npm package bundles its own +Bun runtime for installed `ocx` commands; package scripts may also resolve Bun through that bundled dependency. ```bash git clone https://github.com/lidge-jun/opencodex.git diff --git a/bun.lock b/bun.lock index 46893143a37..063207baa8b 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@bufbuild/protobuf": "^2.14.0", "@modelcontextprotocol/sdk": "^1.30.0", "@napi-rs/keyring": "1.3.0", - "bun": "1.4.0", + "bun": "1.4.2", "zod": "4.4.3", }, "devDependencies": { @@ -63,29 +63,29 @@ "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg=="], - "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GCpf8QuFLsyioVawP5HrMxA1ZRBlu6Hq9RNnSc3UTUWAzIxBso9trjoZczw1HdgpqSssFkszfIV2zmOzFTjhkw=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXdZkP1featqxZ+/VTXWG1BVjM4OGBehVY2Q88EeUj/7L0UMeCGItmyPYTN+wxvlGJ6F66JEtzsw+GvQWewnag=="], - "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cIrhwOr0SPEraewznhC+c/k6TG8bwFn5uZ4EJuXwjiKJLcAF36q7/bGjWkeXSe48JwMcPRUR054JXF7+cRwSSA=="], + "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gZTxZuLjkUhAWjTETu3tw0WhsEdNkJ64daj60ybhPf835a2yollV3yTkK9JozvzKPx4TRFzLSl8C+U525pxVbw=="], - "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.4.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-09x7wnjMR6M5KGBDBhVl2CpfoCIQOkVDbPX2KfIhpXv4N6grbWE7dfLPw/Ydi9gaUMGhU7UKhoz444Nu6RCycA=="], + "@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.4.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-SMNItMw1Z8QeeQVKnw8jA7xQNkeXdP+OPgin4Wi/QTx/B8RHHLnuZfqmFy7NtVeT2NF0kKYppW4WWd2CCYZjhQ=="], - "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.4.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dRwzti/qJqV1HWplU27iUWUqp+f2DtFSf2yqQKSb+HH2dDOC//Uqd9u/A5h1DMsLszfP5OGP9UwQIKxVwFODaA=="], + "@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.4.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-THbPKXhO54N0DpFRKZNDZpQ7dpbX0bWASuARckAUS9wRtFIHsiY+uULXJvxJGo2YD1YewvXQ4G8Fj7XT5oBCiw=="], - "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y5yAtCbHK6JjprXEtkdklDQFPADgs+CkfcliyY5g4JJ8baGHyQSrfpSkX3XVJ2C+aBLsdwNDdW+oczMsAwx6uA=="], + "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3BBP9ovJ2RGHFH6Ae1CAtxNtG1+YY6GD6rmYbsUosoAk9+OEl6zeDQ/k4fBkc6dYOJCtWnx8hUxzNzQATSmvYQ=="], - "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.4.0", "", { "os": "android", "cpu": "arm64" }, "sha512-HpPIxJfDNPBPhiBNMyZoo/dOLijARfsx5j72vNuLtaTvl0Hh7HUculxjsOQ2WSyGoCgqXMEr1Qqjab1im9u1RA=="], + "@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.4.2", "", { "os": "android", "cpu": "arm64" }, "sha512-3mZKO2rhsNgbAUtAHC1UKUlF2zTxFraDZT/Elv8wzyH0fJL9h+Iv3TgB9lO63w89PRn3eFe+NRA1bhVgikKNPQ=="], - "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RUjAAkJ/CdNV++zVxyANWshPc73CECYsfhk0fWAkoJjtywxJ2BwXzI6nopBBDMfs0HS+fhRGn6zGwU8ccxLeJg=="], + "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+Sm6y+lSiSFBOtXmnekp5Q6n1tUKlyv71FCPWBc61Cgb14T5eBs8SN/nh4MUCOKzONkI3O+as3MGUgikS4aCBQ=="], - "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Du44zebtPXJujvMLmtIxEQ6ykOhYt7L/Q+YIGVm+Yy+Pj/fpOnq60ggwIpKp/pGAFbYHNiTrA3JTjuZ9MTbZIg=="], + "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9/E/UXOTpSo3YsV5g+FhtTd/qTpiWoKuxS12cqtuYA1ssu9fRAoPQnipFgGyck3tWO63iUdxBiygq+kELFawng=="], - "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.4.0", "", { "os": "android", "cpu": "x64" }, "sha512-u++KyLlfMn36yWz+AgJs+fZtS46UFDNpSSZhrcitkytONtNwq0X6Q9BDVEFXxYl/+Eec0xme1rb6MgW+U35WeA=="], + "@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.4.2", "", { "os": "android", "cpu": "x64" }, "sha512-6HC5tzcC79113n2IHCTJMWv+HsQImv4ZFEK2XpYLxY6HbT8tM4cUM2Zv1bHZBQsS3jv/zYBamDJ1UX7If0d5tw=="], - "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-C1Dv+ISL8YKEKM9jAHzNifOcRUoziy6UMxh+yVXjUCP6QnbRhENDHLaIWWkQZJyBLTn0I3xozflorAlHiGzGqA=="], + "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-vVTKUg1bnPhRP/Hp73jIVoFh2vPFNYEqYX0ERKfZBOQEEHitNAeukZzzuUDZS0SoDCIpuWUGSpd/CDMbjdR+Uw=="], - "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-FBAYaQpJBP0asgqzL6NFUfjdQqsV+kvTpJ/eWxPKj+RcDgIfPSuE8kvQuPYu5pa8u8JTujYMjmuyvHxVuQsInA=="], + "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-8EJ1ST7339WJE3poPW5nBgVW/lWf9HBz4W27ZUNhburKmcBLOByPyE6DP9fHD8FQGm5c+ilUN2hX1mrW0jxq9Q=="], - "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jRKv1NPLznMSZY5BEWciMF7zv0Tiyo2pQSxAJ3w+YWJ6y3VWNJQQQdLlV5Jx8lbOFDrJdrc9dD3GV17k3BP41A=="], + "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+bN6OuVld/9diT/RLSXSW7JE6CvNE3gL9XsAEjULi1nUsXd6DNO6GuA9jNdNb3r8PdJFnYHr5aypNV1Oj3Rd9g=="], "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], @@ -139,7 +139,7 @@ "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - "bun": ["bun@1.4.0", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.0", "@oven/bun-darwin-x64": "1.4.0", "@oven/bun-freebsd-aarch64": "1.4.0", "@oven/bun-freebsd-x64": "1.4.0", "@oven/bun-linux-aarch64": "1.4.0", "@oven/bun-linux-aarch64-android": "1.4.0", "@oven/bun-linux-aarch64-musl": "1.4.0", "@oven/bun-linux-x64": "1.4.0", "@oven/bun-linux-x64-android": "1.4.0", "@oven/bun-linux-x64-musl": "1.4.0", "@oven/bun-windows-aarch64": "1.4.0", "@oven/bun-windows-x64": "1.4.0" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-iRiFkc2W7UVpCyZXO9tod45TP9QCyN19fWqbpeN/jaM/K7uzeHYx/OSPsahMJazGKBgPsnxRt+4Jc43d8BcHZw=="], + "bun": ["bun@1.4.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.2", "@oven/bun-darwin-x64": "1.4.2", "@oven/bun-freebsd-aarch64": "1.4.2", "@oven/bun-freebsd-x64": "1.4.2", "@oven/bun-linux-aarch64": "1.4.2", "@oven/bun-linux-aarch64-android": "1.4.2", "@oven/bun-linux-aarch64-musl": "1.4.2", "@oven/bun-linux-x64": "1.4.2", "@oven/bun-linux-x64-android": "1.4.2", "@oven/bun-linux-x64-musl": "1.4.2", "@oven/bun-windows-aarch64": "1.4.2", "@oven/bun-windows-x64": "1.4.2" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-TrSXo6HJfIEaczpb3kjX82I2pL47vK1QUNmHRCUdz9IzaOwa9lzOXSWwu2l18YHE3sNfGRapVLd4nNm+22vVVA=="], "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index ba075268d55..180ea4c168e 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.80.0" +version = "2.81.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 9886719215e..95e4134d16b 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.80.0" +version = "2.81.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/src/provider_icons.rs b/desktop/src-tauri/src/provider_icons.rs index 045d9e8f16f..2799453c795 100644 --- a/desktop/src-tauri/src/provider_icons.rs +++ b/desktop/src-tauri/src/provider_icons.rs @@ -24,6 +24,7 @@ macro_rules! svg { const ALIASES: &[(&str, &str)] = &[ ("anthropic", "claude-color.svg"), + ("anthropic2", "claude-green.svg"), ("anthropic-apikey", "claude-color.svg"), ("claude-cli", "claude-color.svg"), ("azure-openai", "openai.svg"), @@ -152,6 +153,7 @@ fn svg(file: &str) -> Option<&'static str> { "bizrouter.svg" => svg!("bizrouter.svg"), "cerebras.svg" => svg!("cerebras.svg"), "claude-color.svg" => svg!("claude-color.svg"), + "claude-green.svg" => svg!("claude-green.svg"), "cline-color.svg" => svg!("cline-color.svg"), "cloudflare-ai-gateway-color.svg" => svg!("cloudflare-ai-gateway-color.svg"), "commandcode-color.svg" => svg!("commandcode-color.svg"), diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index d39a1725311..b8f069d1d01 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.80.0", + "version": "2.81.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/devlog/_fin/261008_anthropic2_account_pool/000_plan.md b/devlog/_fin/261008_anthropic2_account_pool/000_plan.md new file mode 100644 index 00000000000..c3f08922bbe --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/000_plan.md @@ -0,0 +1,122 @@ +# anthropic2: a second, independent Anthropic account pool + +Status: open. Opened 2026-10-08 against `dev` at `bf9ecf3d79` (v2.81.0). + +## Outcome + +Add a builtin OAuth provider instance `anthropic2` ("Anthropic · Pool 2") next to the +existing `anthropic`. Both run the single Anthropic implementation (adapter `anthropic`, +the same login/refresh engine, native Messages and the Responses bridge, the same model +metadata). Only the account drawer is separate: credentials, pool configuration, pool runtime +state, quota/usage/reset attribution and helper credential resolution belong to the instance +the request selected. A user picks `anthropic/` or `anthropic2/` in the existing +picker. + +Terms used in every document of this unit: + +- **family** — shared Anthropic behaviour (login, wire format, model capability, error classes). +- **instance** — `anthropic` (A) or `anthropic2` (B), the provider a user selects. +- **account** — one stored credential inside an instance; **pool** — the accounts and policy of one instance. + +## Decisions + +| ID | Decision | +|---|---| +| D-01 | Exactly two fixed IDs. `anthropic3` or prefix matching is never accepted. B is a dormant registry preset until the user adds it. | +| D-02 | One engine, separated identifiers. Instance membership is declared by registry metadata (`oauthFamily: "anthropic"` on OAuth rows) and checked by `isAnthropicOAuthInstance(id)`; never by `adapter === "anthropic"` or `startsWith`. `anthropic-apikey` and Anthropic-compatible gateways are not instances. | +| D-03 | One protected auth file, separate keys (`anthropic`, `anthropic2`). Existing lock/atomic-write/generation/refresh-intent contracts are reused. | +| D-04 | A keeps `config.anthropicAccountPool`; B uses `config.providers.anthropic2.anthropicAccountPool`. Same schema, same product defaults, no inheritance from A. `providers.anthropic.anthropicAccountPool` and the field on any other provider are rejected by validated writes and reported by diagnostics. | +| D-05 | Pool state (selection, revision, round-robin cursor, affinity, quorum, manual preference) is per instance; account state (cooldown, admission pause, family quota, usage history) is per (instance, account). Route `fallback: true` widens only within the same instance. | +| D-06 | B starts empty and supports browser OAuth only (OPEN-01, owner-confirmed 2026-10-08). No Claude Code CLI import, adoption or write-back for B; A keeps its import/continuity behaviour. Proven duplicate access/refresh token or verified account UUID across instances is rejected inside the store write lock. | +| D-07 | A direct `anthropic2/` request never recovers onto A. Explicit combos/helpers naming A remain a user-declared cross-provider choice. Helper `backend: "anthropic"` names the family; the new optional `anthropicInstance` names an instance; unset means "inherit the request's instance". | +| D-08 | Pool separation is an opencodex routing boundary only. It does not claim provider-side account separation, terms compliance or OS-level isolation. | +| D-09 | Explicit builtin ownership: newly created B config carries `anthropicOAuthInstance: "anthropic2"`. Every pre-existing unmarked `anthropic2` row remains custom, including one at the canonical endpoint. Login refuses the collision without rewriting config/auth. Marker plus Anthropic OAuth shape identifies B; endpoint equality does not. Both instances retain the existing baseUrl override behavior. The marker records operator configuration intent, not an OS or cryptographic boundary. | +| D-10 | GUI: B uses the Claude mark recoloured green (`claude-green.svg`, design-system green) so the two pools are distinguishable at a glance. | + +## Code contract (shared by every slice) + +- `src/providers/anthropic-instance-id.ts` (leaf, no imports): `AnthropicInstanceId = "anthropic" | "anthropic2"`, + `ANTHROPIC_INSTANCE_IDS`, `isAnthropicInstanceId(id)` (pure string check). The registry imports only this leaf. +- `src/providers/anthropic-instance.ts`: `isAnthropicOAuthInstance(id)` (exact ID + registry entry with + `oauthFamily`), `isBuiltinAnthropicInstanceRow(name, providerConfig)` (D-09 shape check) and + `configuredAnthropicInstance(config, name)` (for `anthropic` it returns the instance as a compatibility + identity and callers keep their existing checks; for `anthropic2` only when the row exists, is enabled and + passes the shape check). Runtime selection and physical send use `configuredAnthropicInstance`, so an + orphan `anthropic2` auth row without an enabled builtin-shaped config row never activates B. +- `src/oauth/anthropic-pool-config.ts`: `resolveAnthropicAccountPoolConfig(config, instance)` + reads the instance's own location only; the existing `anthropicAccountPoolConfig(config)` stays + as the A wrapper. +- Instance-scoped state uses a factory per module: `anthropicRoutingFor(instance)`, + `anthropicModelQuotaFor(instance)`, `anthropicRatePolicyFor(instance)`, cooldown-generation and + reset-ledger owners likewise. State is created lazily on first write, never on read. Existing named + exports remain and mean A, so existing callers and tests keep their meaning. Required-instance variants + get distinct names (`...ForInstance(instance, ...)`, e.g. `resolveAnthropicModelRouteForInstance`, + `setAnthropicAccountThresholdForInstance`, `effectiveAnthropicAccountThresholdForInstance`, + `resolveNativeOAuthBindingForInstance`); the old names stay as A wrappers. Any B-reachable call site + passes the instance explicitly; an omitted instance never silently means A on a B request. +- Facade objects may be allocated lazily on lookup; mutable state (health, affinity, pauses, family + evidence) is created only by a write. Removing a B account or the B row bumps the same generation fences A + uses, and sweeps reconcile every existing bucket against its own roster. +- The request path derives the instance once from the settled route (`route.providerName` narrowed by + `isAnthropicOAuthInstance`) and carries it through binding, commit, dispatch, refusal recovery, + quota recording and log labels. +- Bare `claude-*` inference keeps its A-only provider list; qualified `anthropic2/...` routes never + take the Claude Code caller-forward path. + +## Work-phases + +| WP | Doc | Scope | +|---|---|---| +| wp1 | this unit | roadmap only | +| wp2 | [010](010_identity_config_store.md) | registry preset, identity helper, config types/schema/diagnostics, auth store, OAuth login/refresh, duplicate guard, collision guard | +| wp3 | [020](020_pool_runtime_dataplane.md) | routing/quota/rate/kernel state per instance; native Messages; Responses transport, continuation, sidecar execution, combos; protocol settings | +| wp4 | [030](030_helpers_surfaces_gui.md) | sidecar auth, vision, web search, images, quota probes, reset grants, usage labels, management API, CLI, catalog, GUI (green mark), docs, structure | +| wp5 | [040](040_delivery.md) | one PR to `dev`, hosted CI repair loop, independent review, merge | + +[050](050_acceptance_map.md) maps every acceptance scenario and release blocker to a phase and test file. +Implementation workers are `gpt-6.1-sol` subagents (owner request); main integrates, commits and owns git. +During wp4 the ChatGPT account began refusing every Sol model name for subagents +(`gpt-6.1-sol`, `gpt-6-sol`, `gpt-5.6-sol`: "not supported when using Codex with a ChatGPT +account"), so wp4's workers ran on the session default model; Sol was retried for each new lane. + +All implementation lands on one branch, `codex/anthropic2-account-pool`, and ships as one PR so B is +never exposed half-built. Work inside a phase is split across workers with disjoint file ownership. +The main session alone owns shared fixtures, `scripts/test-layout/layout.json`, +`tests/fixtures/test-layout-expected.json`, `structure/` and `docs-site/`; workers return the entries they +need and main applies them. Files near their cap at the start: `src/oauth/index.ts` 1996/1999, +`src/server/management/provider-routes.ts` 1994/1999, `src/providers/registry.ts` 232/232 (baseline), +`src/providers/quota.ts` 557/558 (baseline), `scripts/test-layout/layout.json` 1955/1999. Growth in these +goes to sibling modules; layout registrations stay compact. + +## Verification policy for this unit + +The owner forbade every local test, typecheck, lint and build for this unit. Execution evidence is +exact-head hosted CI on the PR. Local checks are limited to static inspection (`git diff --check`, +reading diffs). Each phase adds focused regression tests next to the subsystem it changes; they run +in CI. New test files are registered in `scripts/test-layout/layout.json` and +`tests/fixtures/test-layout-expected.json`. Files at their size cap get sibling modules, never a raised cap. + +## Outcome (2026-10-09) + +Landed as one PR, [#6743](https://github.com/lidge-jun/opencodex/pull/6743), squash-merged into `dev` +as `4222989ff4` at head `9445c48caf`. Every check passed at that head +([Cross-platform CI 37812949507](https://github.com/lidge-jun/opencodex/actions/runs/37812949507): 41 pass, +four conditional skips, aggregate `ci` pass). No local suite, typecheck, lint or build ran, by owner +instruction; hosted CI is the only execution evidence. + +`anthropic2` ("Anthropic · Pool 2") ships as a dormant builtin preset that shares the Anthropic +implementation and keeps its own credentials (browser OAuth only, OPEN-01), pool settings, runtime +state, quota, usage labels and reset journal. Helpers bind to an explicit or inherited pool and skip, +never fall back, when that pool is unavailable. Bare `claude-*`, the default provider and Claude Code +forwarding stay on `anthropic`. The dashboard shows Pool 2 with a green Claude mark. + +Reviews folded before merge: a wp4 correctness review (helper refusal no longer fails the main +request; inheritance ignores a custom unmarked row while a pool removed mid-request still refuses; +availability counts any usable account; reset currency uses the live config), a security review +(PASS; Lab live probes now refuse a Pool 2 bearer for an unmarked row), and CodeRabbit (marked Pool 2 +excluded from bare-model fallback; an invalid sidecar `anthropicInstance` degrades at load instead of +resetting the config; native OAuth target and send-time eligibility judged on the routed provider). + +Subagent note: from wp4 on, every Sol model name was refused for this ChatGPT account, so wp4 and +wp5 workers ran on the session default model. Not verified: live Anthropic accounts, real OAuth +login, provider-side account separation. diff --git a/devlog/_fin/261008_anthropic2_account_pool/010_identity_config_store.md b/devlog/_fin/261008_anthropic2_account_pool/010_identity_config_store.md new file mode 100644 index 00000000000..70b78b20b79 --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/010_identity_config_store.md @@ -0,0 +1,130 @@ +# 010 — Identity, configuration, auth store, OAuth (wp2) + +## Registry and identity + +- `src/providers/registry/types.ts`: add `oauthFamily?: "anthropic"` to the registry entry type; + classify it `NONE` in `src/providers/registry/model-ids.ts` (exhaustive field map). +- `src/providers/anthropic-instance.ts` (new): the contract from 000. The predicate reads the registry + entry by exact ID and requires `authKind: "oauth"`, `oauthId === id`, `oauthFamily === "anthropic"`. +- `src/providers/registry/entries-core.ts`: replace the inline `anthropic` row with + `anthropicOAuthEntry(id, label)`; add `anthropic2` with label "Anthropic · Pool 2", note + "Independent Claude account pool", `featured`/preset exposure as for A, fresh copies of every array + and map. `jawcodeBundle` stays `anthropic` so generated metadata aliases B to the family. +- `src/generated/model-metadata.ts`: regenerated alias only through `scripts/generate-model-metadata.ts` + semantics (`anthropic2 -> anthropic`), not a hand-written bundle. + +## Configuration + +- `src/types/anthropic-account-pool.ts` (new leaf): `AnthropicAccountPoolConfig` extracted from + `OcxConfig.anthropicAccountPool`; `OcxProviderConfig.anthropicAccountPool?` added for B. +- `src/oauth/anthropic-pool-config.ts` (new): `resolveAnthropicAccountPoolConfig(config, instance)` + returns the raw object of that instance (or `{}`); `isAnthropicPoolEnabledFor(config, instance)`. +- `src/config/schema/*`: one shared pool schema for both locations; B's nested field validated; + validated writes reject the field on `providers.anthropic` and on non-B providers. +- `src/config/diagnostics.ts`: pool diagnostics run per location; a misplaced field is a reported + error, never silently ignored. B is not added to defaults or created by migrations. +- `src/types/config.ts`: optional `anthropicInstance?: AnthropicInstanceId` on `webSearchSidecar`, + `visionSidecar` and the Claude-origin sidecar overrides; validation rejects it with a + non-Anthropic backend. Absence is preserved on save. + +## Auth store and OAuth + +- `src/oauth/index.ts` is at 1996/1999 lines: the OAuth provider definition for both instances moves to + a sibling (`src/oauth/anthropic-oauth-definitions.ts`) built by one factory. B's definition passes + `importLocal: "off"` always; refresh dispatch uses `isAnthropicOAuthInstance(provider)` and keeps the + real provider argument. +- `src/oauth/anthropic-continuity.ts`: `captureAnthropicCredentialOwner(instance, ...)` reads + `store[instance]`; `newerClaudeCredential` returns nothing for B before touching disk/Keychain. +- `src/oauth/store.ts`: `setAnthropicAccountThresholdForInstance(instance, ...)` (old name kept as A wrapper); + `assertNoCrossAnthropicRegistration(store, instance, credential)` runs inside + `saveCredentialWithReceipt`, `saveAccountCredential` and `upsertCredentialByIdentity` before any row + changes. It compares SHA-256 fingerprints of non-empty access/refresh tokens against every row of the + other instance (paused included) and verified UUIDs only when both proofs validate against their own + bearer. Email, alias and unverified account IDs are ignored. It throws a credential-free typed error. + B `local-cli` provenance is rejected. Refresh-time merges are not blocked (rotations of an already + admitted row must not strand A). +- Collision (D-09): every config-aware decision uses `isBuiltinAnthropicInstanceRow` (adapter `anthropic`, + `authMode: oauth`, explicit `anthropicOAuthInstance: "anthropic2"` for B). Login, OAuth upsert and config + publication for `anthropic2` refuse a row that fails it; startup catalog reconciliation and registry + enrichment (`src/providers/registry.ts` lookups for a configured row) leave such a row custom. +- `src/oauth/token-guardian.ts`: B participates only when configured and enabled. + +## Tests (new files, registered in both layout inventories) + +- `tests/providers/provider-anthropic-instance.test.ts` — exact IDs, `anthropic-apikey`/compatible adapters excluded, B seed is a deep copy, B absent from default config. +- `tests/config/config-anthropic-instance-pool.test.ts` — A/B locations, no inheritance, misplaced field rejected, `anthropicInstance` validation. +- `tests/oauth/oauth-anthropic-instance-registration.test.ts` — duplicate token / verified UUID rejected across instances, distinct accounts with equal IDs accepted, B local-cli refused, collision guard. + +## wp2 execution + +Shared leaf code (main writes it first, so both workers import a fixed contract): + +```ts +// src/providers/anthropic-instance-id.ts — no imports (registry.ts may import this leaf; it must never import anthropic-instance.ts) +export const ANTHROPIC_INSTANCE_IDS = ["anthropic", "anthropic2"] as const; +export type AnthropicInstanceId = typeof ANTHROPIC_INSTANCE_IDS[number]; +export function isAnthropicInstanceId(value: unknown): value is AnthropicInstanceId; +export function anthropicInstanceRowShapeMatches(name: string, row: { adapter?: string; authMode?: string; baseUrl?: string; anthropicOAuthInstance?: unknown } | undefined): boolean; +// pure; "anthropic": true; "anthropic2": row present, adapter "anthropic", authMode "oauth", +// own anthropicOAuthInstance marker equals "anthropic2"; endpoint is not ownership; other names: false + +// src/providers/anthropic-instance.ts +export function isAnthropicOAuthInstance(id: string): id is AnthropicInstanceId; // exact ID + registry oauthFamily/oauthId/authKind +export function isBuiltinAnthropicInstanceRow(name: string, row?: Partial>): boolean; +// isAnthropicOAuthInstance(name) && anthropicInstanceRowShapeMatches(name, row); re-exports the leaf +export function configuredAnthropicInstance(config: Pick, name: string | undefined): AnthropicInstanceId | undefined; +// anthropic: "anthropic" whenever name === "anthropic" — a compatibility identity result only; callers keep +// every existing auth-mode, enabled-provider and eligibility check they apply to A today; +// anthropic2: only when the row exists, is not disabled and passes isBuiltinAnthropicInstanceRow + +// src/oauth/anthropic-pool-config.ts +export function rawAnthropicAccountPool(config, instance): unknown; // A: config.anthropicAccountPool; B: providers.anthropic2.anthropicAccountPool +export function resolveAnthropicAccountPoolConfig(config, instance): AnthropicAccountPoolConfig; // object or {} +export function isAnthropicPoolEnabledFor(config, instance): boolean; +``` + +P3 fidelity refinement supersedes the initial endpoint-shaped rule: both registry rows keep +`allowBaseUrlOverride: true`. Explicit B creation seeds its ownership marker; config load, enrichment, +reconciliation and migrations never stamp it onto an existing custom row. Browser-only local credential +intake remains the owner-approved onboarding exception. + +Registry ownership guard: `providerMatchesRegistryTransport` in `src/providers/registry.ts` today returns +`true` for every non-key entry. For `anthropic2` it must first return +`anthropicInstanceRowShapeMatches("anthropic2", provider)`, so a colliding custom row is never pinned or +enriched as the builtin. `registry.ts` stays within its 232-line cap (move code to a sibling if needed). + +The pool config type leaf `src/types/anthropic-account-pool.ts` carries `AnthropicAccountPoolConfig` +together with the rotation-strategy, quota-window and model-route types it references; `config.ts` imports +and re-exports them, never the reverse. + +W2-oauth keeps the old continuity and threshold signatures as A wrappers and adds distinct +`...ForInstance` variants; A's existing refresh and import behaviour is pinned by fixtures in its new tests. +Routing/native callers stay with wp3; wp2 proves the foundation contracts, not runtime B readiness. + +| Worker | Owns (exclusive writes) | New tests | +|---|---|---| +| W2-config | `src/providers/registry/types.ts`, `registry/model-ids.ts`, `registry/entries-core.ts`, `src/providers/registry.ts` (232-line baseline cap: net growth 0), `src/providers/derive.ts`, `src/types/anthropic-account-pool.ts` (new), `src/types/config.ts`, `src/types/provider.ts`, `src/config/schema/*`, `src/config/diagnostics.ts`, `src/generated/model-metadata.ts` (generator output only) | `tests/providers/provider-anthropic-instance.test.ts`, `tests/config/config-anthropic-instance-pool.test.ts` | +| W2-oauth | `src/oauth/index.ts` (1996/1999: extract first), `src/oauth/anthropic-oauth-definitions.ts` (new), `src/oauth/store.ts`, `src/oauth/store-anthropic-instance.ts` (new, duplicate guard), `src/oauth/anthropic-continuity.ts`, `src/oauth/anthropic.ts`, `src/oauth/token-guardian.ts`, `src/oauth/login-cli.ts` | `tests/oauth/oauth-anthropic-instance-registration.test.ts`, `tests/oauth/oauth-anthropic-instance-refresh.test.ts` | + +Main owns the leaf files above, both layout inventories and every `structure/` edit. Workers do not run tests, +typecheck, lint or builds; the only local command allowed besides read/search is the deterministic +model-metadata generator. Workers report the layout entries their new tests need. + +### wp2 audit fold (reviewer near-pass) + +- Ownership: main writes, before dispatch, exactly `src/providers/anthropic-instance-id.ts`, + `src/providers/anthropic-instance.ts`, `src/oauth/anthropic-pool-config.ts` and + `src/types/anthropic-account-pool.ts`. W2-config edits `src/types/config.ts` to import/re-export the type + leaf and owns any sibling extracted from `src/providers/registry.ts` (e.g. `src/providers/registry-transport.ts`). +- Publication atomicity: B login publishes its provider row through `mutatePersistedConfig`, rechecking + `anthropicInstanceRowShapeMatches` against the latest persisted row inside the callback. The shape is also + checked before the browser opens and before credentials persist. If a competing writer claims the name in + between, publication refuses with a typed error, the custom row is left untouched, and the B credential just + written stays as an orphan auth row that no request can use (orphan rows are never execution candidates); + the error tells the user to remove it or rename the custom provider. A deterministic competing-write case goes + in `oauth-anthropic-instance-registration.test.ts`. +- Load versus write: the shared pool schema keeps A's tolerant load semantics exactly (malformed + `nativeMessages` loads as `false`, a malformed pool container loads as absent, unrelated providers survive) + and applies the same tolerance to B's nested field; strict rejection applies only to validated writes and + diagnostics. W2-config adds A and B cases for both paths next to `tests/config/config-load-degrade.test.ts` + behaviour in `tests/config/config-anthropic-instance-pool.test.ts`. diff --git a/devlog/_fin/261008_anthropic2_account_pool/020_pool_runtime_dataplane.md b/devlog/_fin/261008_anthropic2_account_pool/020_pool_runtime_dataplane.md new file mode 100644 index 00000000000..3aed52b3807 --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/020_pool_runtime_dataplane.md @@ -0,0 +1,209 @@ +# 020 — Pool runtime state and data plane (wp3) + +## Pool runtime + +- `src/oauth/anthropic-routing.ts`: module body becomes `createAnthropicRouting(instance)`; module-level + `PROVIDER`, `upstreamHealth`, `sessionAffinity`, `manualPreference`, `manualSelectionGeneration`, + `quorumCache` move into the instance closure. `anthropicRoutingFor(instance)` caches one facade per + instance; existing exports are bound to A. Event subscriptions are registered once and dispatch to the + facade of `event.provider` only when it exists. Config reads use `resolveAnthropicAccountPoolConfig`. + Errors such as `OAuthLoginRequiredError` carry the real instance. If the file would pass 1999 lines, + move the factory into `anthropic-routing-instance.ts`. +- `src/oauth/anthropic-model-routes.ts`: `resolveAnthropicModelRouteForInstance(instance, config, modelId)` + reads the instance's routes; the old name stays as the A wrapper; `routeCandidates` stays pure. +- `src/oauth/anthropic-model-quota.ts`, `anthropic-rate-limit-policy.ts`: per-instance maps behind + `anthropicModelQuotaFor(instance)` / `anthropicRatePolicyFor(instance)`; classifiers stay shared; + legacy exports mean A; `clearAll...` helpers for shutdown/tests. +- `src/oauth/anthropic-account-refusal.ts`: response ownership keeps `snapshot.provider`; recovery + requires the matching instance. +- `src/oauth/anthropic-account-threshold.ts`: `effectiveAnthropicAccountThresholdForInstance(instance, config, row)`; + the old name stays as the A wrapper. +- `src/oauth/health.ts`: `projectStoredOAuthAccountHealth` reads cooldown evidence through the routing facade + of the row's instance (today it does so only for `anthropic`). +- `src/oauth/pool-kernel.ts`: `anthropicPoolKey(instance)` (A keeps `"anthropic"`); reconcile builds one + roster per instance. `src/oauth/generic-account-failover.ts` excludes both instances. +- `src/oauth/pool-settings-capability.ts`: both instances get the Anthropic capability; DTO reads the + instance's config. +- `src/lib/state-store-registrations.ts`: sweeps traverse every existing instance bucket. +- `src/providers/quota/anthropic-cooldown-recovery.ts`: generations keyed by (instance, account). + +## Data plane + +- `src/protocols/settings.ts`: pooled native preference resolved for the selected instance; + `protocolPolicyRevision` includes B's inputs. +- `src/server/messages-native-eligibility.ts`: OAuth native gate uses `configuredAnthropicInstance(config, + route.providerName)` (ID predicate plus the D-09 row-shape guard); the wire `adapter` check is unchanged, + so a custom `anthropic2` gateway row keeps its current non-instance behaviour. +- Every request-path instance derivation in this phase uses `configuredAnthropicInstance`, never the bare ID + predicate. +- `src/server/messages-native-oauth.ts`, `messages-native.ts`: bindings carry an immutable instance used + for currentness, credential/UUID reads, family leases, quota recording and refusal recovery. +- `src/server/claude-messages.ts`: the caller-forward candidate predicate excludes provider-qualified + routes to an instance other than the bare A path; quorum reads use the settled instance. +- `src/server/responses/request-transport.ts`: every `route.providerName === "anthropic"` pool/OAuth + gate becomes instance-aware, with the instance captured once; log bases use it. +- `adapter-dispatch.ts`, `adapter-continuation.ts`, `sidecar-execution.ts`, `core-combo.ts`, + `src/routing/quota.ts`: recovery, snapshots, labels and cache keys use the captured instance. +- `src/router.ts` bare Claude inference: unchanged (A only). + +## Tests + +- `tests/adapters/anthropic/anthropic-instance-isolation.test.ts` — equal account and session IDs in A + and B with distinct tokens: affinity, manual selection, cooldown, admission pause, family quota and + round-robin state stay separate; B with no accounts fails closed while A has accounts. +- `tests/claude-integration/anthropic2-native-routing.test.ts` — native eligibility for B, qualified B + route does not take caller-forward, bare claude still resolves to A. + +## Execution plan revalidated at 56a6a7fd17 + +Previous D: the identity/config/credential foundation passed its hosted contract and baseline regressions, Typecheck and structure gates. Full runtime parity remains this phase and the helper/product-surface phase; full CI remains required before merge. No change of direction. + +### Main dispositions + +- Accept captured-instance facades, A compatibility wrappers, shared pure helpers + and module-level subscriptions dispatched only to existing matching instances. +- Accept distinct ForInstance APIs for model routes, thresholds, response recovery, + native bindings and quota-header recording. Existing signatures remain A. +- Accept scope expansion to account-cache and quota barrel: physical data-plane + assertions cannot pass while header attribution remains canonical. +- Require configured admission at B selection and immediately before physical send, + preserving existing A auth/disabled checks. Same instance through every await, + body rebuild, recovery and continuation. Snapshot.provider must match the settled + provider; errors must not cause sibling-pool fallback. +- A route named `anthropic2` with OAuth auth and failed configured admission must + refuse locally before the generic OAuth resolver can run. An undefined instance + is not permission to resolve the same B auth namespace through generic fallback. + Custom key rows keep their existing key-auth path. Native-ineligible B OAuth + follows the scoped Responses bridge, never generic orphan credential resolution. +- Claims/leases carry instance and generation. Removal/re-add must invalidate old + claims; sweeps walk existing buckets without allocating dormant B state. +- Keep shared error classes/pure wire transformations outside factories. If factory + extraction is necessary, put shared contract types and pure helpers in a leaf; + avoid facade/implementation runtime cycles. + +## Fixed interfaces + +### Ownership refinement prerequisite + +Before integrating runtime workers, add explicit B configuration provenance: +`OcxProviderConfig.anthropicOAuthInstance?: "anthropic2"`. B's pure row predicate +requires an own marker, Anthropic adapter and OAuth mode. Endpoint equality, +existing auth rows and registry membership do not imply ownership. A remains +legacy-compatible without a marker. Both registry instances retain baseUrl override +support; first-party native eligibility stays the existing shared policy. + +Creation: `src/providers/derive.ts` seeds the marker only for explicit builtin B +creation; `src/oauth/index.ts` preserves it on owned updates and refuses every +existing unmarked collision before login/persist/publication. No auto-migration. +Serialization: provider config and editor round trips retain it. Deserialization: +`src/config/schema/leaf-validators.ts` validates the literal; raw diagnostics reject +misplaced or incompatible markers without taking over an unmarked custom row. +Consumers: pure/config-aware identity helpers, registry transport ownership, +OAuth publication/guardian, discovery/probe guards and all runtime admission. +Update exhaustive field maps in `src/providers/model-rename-fields.ts` and +`src/server/auth-cors.ts`; this field carries no model IDs or credentials. + +Discovery captures the authorized target derived from the marked configuration +with the existing transport/model-discovery URL rules. Guard against that target, +not a hardcoded first-party origin or request.url used as its own authority. +Live probe checks must match ownership and the captured target after awaits. +Marked overrides preserve A/B bridge parity; unmarked custom key rows retain their +own key. Unmarked OAuth rows never obtain the builtin B credential. + +Ownership worker (exclusive): `src/providers/anthropic-instance-id.ts`, +`src/providers/anthropic-instance.ts`, `src/types/provider.ts`, +`src/providers/registry.ts`, `src/providers/registry/types.ts` if needed, +`src/providers/registry/entries-core.ts`, `src/providers/derive.ts`, +`src/providers/model-rename-fields.ts`, `src/config/schema/leaf-validators.ts`, +`src/config/schema/anthropic-account-pool.ts`, `src/config/diagnostics.ts`, +`src/oauth/index.ts`, `src/oauth/store-anthropic-instance.ts`, +`src/oauth/model-discovery-auth.ts`, `src/codex/catalog/provider-models.ts`, +`src/codex/catalog/gather-capture.ts`, `src/server/auth-cors.ts`, +`src/server/management/provider-routes.ts`, `src/cli/provider.ts`. +It may extract sibling modules for existing caps. Update the six existing instance +foundation tests/fixtures for marked B and preserve all previous cases; add unmarked +canonical collisions, marked overrides, marker round-trip/removal and live target +replacement. State's new shared fixture seeds marked B directly. + +The four runtime packets below retain disjoint files from this prerequisite. +Publish marker semantics first; all packets use the same fixed helper names. +This refinement requires fresh source security review and hosted evidence. + +Every method on anthropicRoutingFor(instance), anthropicModelQuotaFor(instance), +anthropicRatePolicyFor(instance), anthropicCooldownRecoveryFor(instance) retains +its current argument order and return contract, as enumerated by the architect. +The instance is required by the factory and immutable on returned facades. +Legacy named exports bind only to A. + +New required-instance entrypoints: + +- resolveAnthropicModelRouteForInstance(instance, config, modelId) +- effectiveAnthropicAccountThresholdForInstance(instance, config, row?) +- rotateAnthropicAccountOnResponseForInstance(instance, response, options) +- getAnthropicSidecarAccessTokenForInstance(instance, model, config) +- resolveNativeOAuthBindingForInstance(instance, config, options?) +- recordAnthropicAccountQuotaFromHeadersForInstance(instance, accountId, headers, + writerGeneration, status?, model?) + +The header recorder signature is locked to the argument order above: headers is +`Headers`; writerGeneration is the numeric config/roster publication generation. +It is distinct from the credential-generation string in the sent snapshot. +Preserve `mayCommitAccountQuotaKey`'s live-key exception for an older config +generation. Physical response consumers synchronously check sent provider, bearer +and credential generation after awaits and before recording; native also preserves +UUID ownership. Old recorder remains an A wrapper. + +## Disjoint workers + +State: oauth/anthropic-routing* implementation, anthropic-model-routes, +anthropic-account-threshold, pool-kernel, generic-account-failover, health, +pool-settings-capability; lib/state-store-registrations; +routing/quota, routing/compatibility/assemble and +server/management/routing-profile-routes.ts (including both dry-run consumers and +their parseCandidateEvidence config plumbing). +Tests: anthropic-instance-isolation, anthropic-instance-pool-parity, +oauth-anthropic-instance-health; owns reusable tests/helpers/anthropic-instance-fixture.ts. + +Quota: oauth/anthropic-model-quota, anthropic-rate-limit-policy, +anthropic-account-refusal, providers/quota/account-cache, +providers/quota/anthropic-cooldown-recovery, providers/quota.ts export only. +Tests: adapters/anthropic/anthropic-instance-quota.test.ts (separate file), with +equal IDs, independent family claims/admission pauses and physical header attribution. +This splits the architect's large State packet along the existing quota-owner boundary. + +Responses: server/responses/request-transport, adapter-dispatch, +adapter-continuation, sidecar-execution, core-combo and context types required in +those files. Ask main before modifying another owner. +Tests: anthropic-instance-recovery, anthropic2-fast-parity, +responses/anthropic2-responses-parity. + +Native: server/messages-native*.ts, server/claude-messages.ts, +protocols/settings.ts and protocols/plan-snapshot.ts. The snapshot's +messagesPassthroughPossible predicate must exclude qualified B selectors just as +ingress does; preview stays credential-free. +Tests: claude-integration/anthropic2-native-routing. + +Main: task docs, structure owners, both test inventories and git/CI. Fixture is +assigned to State (amend architect's parent ownership) so contract implementations +and fixtures arrive together; consumers may write tests against documented fixture +or inline minimal setup until State publishes it, then consolidate. + +## Dependency and verification + +State and Quota publish their real facade APIs together before integration. +Responses and Native use the fixed method-name contract; no placeholder production +functions. Quota publishes explicit all-bucket reconciliation hooks; State owns +sweeper registration and the routing claim's readonly instance field. Removal and +clear advance incarnation fences before pruning; retain monotonic counters or +tombstones so an old zero-generation claim cannot become current after re-add. +Workers cannot change branches, commit, push, run local tests/build/typecheck/lint, +start services, spawn children or mutate goal/FSM. +All use gpt-6.1-sol. No source file exceeds its baseline or 1999 lines. + +H-01/02/03 require isolated homes, equal stored IDs/session IDs but distinct tokens +and verified UUIDs, a send ledger, deterministic await barriers, and an intentional +misbinding negative control caught by the ledger. New cases cover pool-off recovery, +strict/fallback routes, pause/cooldown/family behavior, native and translated paths, +prefix stripping, caller credential exclusion, and no post-output replay. +Actual execution is hosted CI at the phase commit; no local test execution. +Full merge readiness and all applicable jobs remain wp5 criteria. diff --git a/devlog/_fin/261008_anthropic2_account_pool/030_helpers_surfaces_gui.md b/devlog/_fin/261008_anthropic2_account_pool/030_helpers_surfaces_gui.md new file mode 100644 index 00000000000..d1ec8f7ade2 --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/030_helpers_surfaces_gui.md @@ -0,0 +1,215 @@ +# 030 — Helpers, management, CLI, catalog, GUI, docs (wp4) + +## Execution contract + +Prior D: wp3 has instance-bound routing/quota/native/Responses and send-time +incarnation ownership. Hosted focused and baseline contracts passed at +`6ba14b70f1`; full-suite source-inventory follow-ups are retained in the same PR. +This phase consumes those APIs and completes their helper/product consumers. +No change of objective or acceptance reduction. All execution evidence comes from +hosted CI; no local tests, typecheck, lint, build or live-account calls. + +### Cross-worker APIs + +The helper owner defines these in `src/sidecar/auth.ts`: + +- `resolveAnthropicSidecarAuth(config, instance)` returns the selected provider + name/config or undefined, never another pool. +- `resolveSidecarAuth(config, instance?)` retains legacy discovery when no target + is supplied. Legacy discovery excludes B as a new automatic candidate. +- `AnthropicHelperContext = { backendFamily: string; anthropicInstance?: AnthropicInstanceId; parentProviderName?: string }`. +- `resolveAnthropicHelperInstance(config, context)` selects explicit target, + then configured builtin parent, then legacy discovery only AFTER backend-family + selection. An invalid/unavailable explicit or inherited target is a typed + refusal, never undefined that would trigger legacy fallback. + +The typed refusal is `AnthropicHelperUnavailableError`, exported by +`src/sidecar/auth.ts` with fixed code `anthropic_helper_unavailable` and selected +instance. Executors preserve refusal; options DTOs may expose unavailable state +but never turn it into permission to discover another pool. + +The helper owner adds `src/sidecar/anthropic-binding.ts`: +`resolveAnthropicHelperSnapshot(config, instance, model): Promise`. +It reuses model-route admission and the selected routing facade. Actual builtin +helper sends capture physical ownership before fetch and retain target/config, +account/generation and send-incarnation checks after awaits. Existing custom +helper providers retain their own credential path. + +The quota owner adds `fetchAnthropicUsageQuotaForInstance(instance, token, fresh?)`; +the old function remains A. `fetchAnthropicQuota(provider)` retains the actual +instance. After refresh, use credential-scoped flight keys and +`captureProviderAccountQuotaEpoch(instance)`; B cache clear cannot invalidate A. + +Reset uses existing ledger options with +`anthropicResetJournalPathForInstance(instance, customDir?)` and +`anthropicResetLedgerOptionsForInstance(instance, customDir?)`. +A journal stays byte-compatible; B uses `anthropic2-reset-grant-ledger.json`. +GET reset-grants takes provider query; consume takes provider in its JSON body. +Both echo provider. Omission means A only at legacy ingress. New B callers always +send provider. Account/grant/operation IDs and unknown-outcome retry ownership remain +bound to the instance; no automatic spend and no relaxation of session consent. + +The account-management owner extracts `src/server/management/anthropic-pool-settings.ts` +when useful. `writeAnthropicPoolSettings(config, instance, value)` writes exactly +one supported location; `persistAnthropicPoolPatch` accepts required instance and +uses that location for durable mutation, uncertain-save comparison and publication. +DTO kind stays `anthropic`, provider remains A/B. Existing A wrappers keep A meaning. + +Helper settings PATCH: missing instance preserves, null deletes, A/B sets explicit. +Validate the effective merged backend/model/instance before writing; omit unset +values on GET/save rather than materializing A. Optional options DTO: +`anthropicPool: { selected?, resolved?, mixed: boolean, available: AnthropicInstanceId[] }`. +Mixed is computed only with a known parent instance. Backend changes away from +Anthropic clear the UI draft and submit deletion. Non-Anthropic defaults stay intact. + +### Disjoint write ownership + +W1 helpers: `src/sidecar/auth.ts`, new binding helper and `candidates.ts`; +`src/vision/plan.ts`, `backends.ts`, `eligibility.ts`, `index.ts`, +`anthropic-describe.ts`; `src/web-search/index.ts`, `sidecar-providers.ts`, +`backends.ts`, `passthrough-bridge.ts`, `alpha-search.ts`, +`anthropic-executor.ts`, `run-turn-loop.ts`, `loop.ts`; `src/images/loop.ts`. +Tests: `tests/vision/vision-anthropic-instance-sidecar.test.ts`, +`tests/web-search/web-search-anthropic-instance.test.ts`. + +W2 quota/reset: `src/providers/quota.ts` (cap 558; extract a cohesive sibling); +new `src/providers/quota/anthropic-account-quota.ts`; `quota/account-cache.ts`, +`vendor-probes-oauth.ts`, `anthropic-cooldown-recovery.ts`; +`src/providers/anthropic-reset-grant-ledger.ts`; +`src/server/management/anthropic-reset-grant-routes.ts`. +Tests: new `tests/providers/provider-anthropic-instance-quota.test.ts`, +existing reset route/cooldown tests. + +W3 account surfaces: `src/server/management/oauth-account-routes.ts`, +`anthropic-account-threshold.ts`, new pool-settings helper; +`src/oauth/pool-settings-capability.ts`; `src/cli/account.ts`, +`account-extended.ts`, `account-api.ts`, `account-policy.ts`, +`account-policy-dto.ts`, `account-anthropic-threshold.ts`, +`capabilities-accounts.ts`; `src/codex/catalog/provider-models.ts`; +`src/providers/label.ts`; `src/usage/cost.ts`, `expected-prices.ts`, +`summary.ts`, `timeline.ts`. +Reset CLI retains old syntax with optional `--provider anthropic|anthropic2`. +Tests: server/anthropic2-management, cli/cli-anthropic2-account, +codex-integration/anthropic2-catalog, usage/anthropic2-usage-attribution. + +W4 account GUI/icons: `gui/src/components/provider-workspace/ProviderAuthPanel.tsx`, +`AnthropicAccountPoolSettings.tsx`, `AnthropicResetGrants.tsx`; +`gui/src/hooks/useAnthropicResetGrants.ts`, `useProviderAccountPools.ts`; +`gui/src/pool-settings.ts`, `oauth-tos-risk.ts`, `provider-icons.ts`, +`provider-payload.ts`, `models-groups.ts`, `protocol-deep-links.ts`; +`gui/src/pages/providers-shared.ts`, `Providers.tsx` for provider-scoped mutation +state consumers; green SVG and desktop Rust alias table. +W4 alone owns ALL locale catalogs, including W5 keys: +`sidecar.pool`, `sidecar.poolCurrent`, `sidecar.poolA`, `sidecar.poolB`, +`sidecar.poolMixed`. Every existing locale receives the keys. +Tests: GUI anthropic2 provider mark/surfaces and reset-grants suites. + +W5 helper settings/UI: `src/server/management/config-routes.ts`, +`agent-settings-routes.ts`, `web-search-sidecar-options.ts`, +`vision-sidecar-options.ts`; `src/types/config.ts`; +`src/config/schema/anthropic-account-pool.ts`; +`gui/src/pages/dashboard-shared.ts`, `use-dashboard-data.ts`, +`dashboard-overview-sections.tsx`, `claude-manual-env.ts`, +`claude-code-types.ts`, `claude-code-save.ts`, `claude-code-sidecar.ts`, +`claude-code-sections.tsx`. +Tests: existing Claude sidecar override/serializer tests; new +`tests/server/management-sidecar-anthropic-instance.test.ts` and +`gui/tests/anthropic-instance-helper-controls.test.tsx`. +W5 sends translation key requirements to main/W4 and never edits locale files. + +Main owns both inventories, structure/manifest/index, docs-site translations, +generated capability surface, screenshot evidence and all Git/CI work. Main also +owns the first-login repair (`src/oauth/index.ts` plus an isolated publication +helper/test if needed) and pinned old-version hosted fixture; no worker edits these. +Additional files require main assignment. Workers use Sol and do not spawn. + +### Required final acceptance work + +- B first login with initially absent config uses existing + `initializePersistedConfigIfMissing` and must not overwrite a concurrent winner. + Distinguish initially missing from deleted/invalid during login; preserve defaults, + A and any concurrent custom B row. Initial creation happens only during explicit + onboarding. Add isolated no-config and competing-create cases. +- STATE-11 runs an actual pre-feature pinned checkout at + `6a7632db2a85c359da9feac976180450ebb42c60` in hosted CI against synthetic auth/config + data. Observe B orphan-key preservation under old auth read/write and config + salvage separately; publish a receipt/artifact. No in-place downgrade claim. +- GUI screenshot/interaction evidence comes from hosted rendering at the final + feature head, not running a local suite. Capture A/B cards, account lists, + model groups, B settings and empty B refusal; read images before claiming visual + verification. Screenshots never enter the feature branch. +- Final matrix includes explicit/inherited/no-parent helpers, unavailable B, + strict routes, post-output no-replay, generated-cache separation, equal-ID + management/reset operations, stale-response UI isolation, default preservation, + all locale keys and native icon alias parity. Passing a narrower matrix does + not close the original acceptance map. + +## Helpers + +- `src/sidecar/auth.ts`: `resolveAnthropicSidecarAuth(config, instance)` — exact lookup, no fallback. + Legacy discovery keeps its order but never adds B automatically. +- Helper resolution order: the backend family is chosen first by the existing rules (web search still + defaults to OpenAI; vision keeps its automatic order). Only when that yields Anthropic: explicit + `anthropicInstance` > the parent request's instance > legacy discovery, which never adds B automatically + (no parent, or a non-Anthropic parent). `anthropicInstance` combined with a non-Anthropic backend, or + with a provider-qualified routed helper model naming the other instance, is a validation error. A helper + whose explicit instance differs from the main request is reported as mixed in the settings options DTO. + Applied in `src/vision/plan.ts`, `backends.ts`, `eligibility.ts`, `src/web-search/index.ts`, + `sidecar-providers.ts`, `backends.ts`, `passthrough-bridge.ts`, `alpha-search.ts`. +- Physical helper executors `src/vision/anthropic-describe.ts` and `src/web-search/anthropic-executor.ts` + obtain their token through the selected instance's routing facade (model-route admission and snapshot + included); `getAnthropicSidecarAccessToken` takes the instance and has no first-match fallback. +- Generated image-description cache keys (`src/vision/index.ts`) include instance, model and the effective + helper policy; deterministic resize caches stay shared. +- `src/web-search/loop.ts`, `src/images/loop.ts`: account-403 recovery recognised for both instances, + within the sending instance. + +## Quota, reset, usage + +- `src/providers/quota.ts` (557/558: extract helpers to a sibling), `quota/account-cache.ts`, + `quota/vendor-probes-oauth.ts`: B is a supported per-account quota provider; probes, header recording + and publication use the instance. Retention and family-window handling in `account-cache.ts` recognise + both instance key prefixes. +- `src/providers/anthropic-reset-grant-ledger.ts`: B uses its own journal file; A's file and records are + untouched. `src/server/management/anthropic-reset-grant-routes.ts` accepts an optional `provider` + (omitted means A) and echoes it. +- `src/providers/label.ts`, `src/usage/*`: B keeps its own label and grouping; pricing comes from the + family metadata alias. + +## Management API and CLI + +- `src/server/management/oauth-account-routes.ts`, `anthropic-account-threshold.ts`: pool settings, + per-account threshold, pause and cleanup accept either instance and write that instance's location. +- `src/server/management/config-routes.ts`, `agent-settings-routes.ts`: persist and validate + `anthropicInstance`. +- `src/cli/account*.ts`, `capabilities-accounts.ts`: `ocx account pool|auto-switch|use anthropic2 ...` + behave as for A; regenerate the skill surface map if a capability string changes. +- `src/codex/catalog/provider-models.ts`: B discovery and selection capture like A. + +## GUI + +- `gui/public/provider-icons/claude-green.svg` (new): Claude mark path, fill `#0a7d5c`, dark-scheme fill + `#4ecb9d`. `gui/src/provider-icons.ts` maps `anthropic2` to it; the desktop alias table + (`desktop/src-tauri/src/provider_icons.rs`) mirrors it. +- `ProviderAuthPanel.tsx`, `AnthropicAccountPoolSettings.tsx`, `AnthropicResetGrants.tsx`, + `useAnthropicResetGrants.ts`, `gui/src/pool-settings.ts`, `oauth-tos-risk.ts`, + `providers-shared.ts`: instance-predicate instead of `=== "anthropic"`; provider passed explicitly; + React keys/query keys include the provider. +- i18n: new keys in all locale catalogs. +- Helper instance controls (PRD D-07): the global web-search and vision sidecar settings and the Claude-origin + sidecar overrides get a "Pool" select with "Current request's pool" (unset), "Anthropic" and + "Anthropic · Pool 2", shown only when the backend is Anthropic. `gui/src/pages/claude-manual-env.ts` and + `gui/src/pages/claude-code-sidecar.ts` types/serialisers carry `anthropicInstance`; unset is never saved + as `anthropic`; a mixed explicit choice shows a short note. Server DTOs in `config-routes.ts` and + `agent-settings-routes.ts` round-trip it. + +## Docs and structure + +- `docs-site/.../reference/configuration/providers.md` (+ existing translations): B location, defaults, + isolation, browser-only onboarding, helper `anthropicInstance`. +- `structure/providers/anthropic-account-pool.md` (+ manifest ownership for new files). + +## Tests + +- `tests/vision/vision-anthropic-instance-sidecar.test.ts`, `tests/server/anthropic2-management.test.ts`, + `gui/tests/anthropic2-provider-mark.test.ts` and focused additions next to existing suites. diff --git a/devlog/_fin/261008_anthropic2_account_pool/040_delivery.md b/devlog/_fin/261008_anthropic2_account_pool/040_delivery.md new file mode 100644 index 00000000000..9f0e5ed7b9b --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/040_delivery.md @@ -0,0 +1,22 @@ +# 040 — Delivery (wp5) + +1. Push `codex/anthropic2-account-pool`, open one PR against `dev` using + `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification, Checklist). Verification states that no + local suite ran by owner instruction and names the hosted CI run for the exact head. +2. GUI change requires a rendered screenshot in the description (uploaded via the `pr-assets` branch and + linked by commit SHA, never committed to the feature branch). +3. CI repair loop: read failing job logs for the exact head, fix the root cause, push, re-read. Skipped, + queued, cancelled or older-head results are not passes. +4. Independent security-focused review of the final diff — credential store, refresh, duplicate guard, + collision guard, isolation, fallback, no replay after output — findings folded or rebutted with reasons. + This change touches credential handling, so the review is mandatory before merge (AGENTS.md security + boundary). +5. Merge with `--match-head-commit` after every required check is green on that head. + +Acceptance coverage expected in CI (spread across the phase test files): shared A/B fixtures with equal +account and session IDs and distinct tokens; concurrent isolation; physical-send attribution; asynchronous +fencing after awaits; pool-off reactive recovery inside B; no replay after output; onboarding leaves the +default provider and bare-model resolution unchanged; reset-grant idempotency per instance; custom +`anthropic2` rows (key provider, gateway OAuth, orphan auth) preserved. + +Not verified by this unit: live Anthropic accounts, real OAuth login, provider-side account separation. diff --git a/devlog/_fin/261008_anthropic2_account_pool/050_acceptance_map.md b/devlog/_fin/261008_anthropic2_account_pool/050_acceptance_map.md new file mode 100644 index 00000000000..bbeca16e5e5 --- /dev/null +++ b/devlog/_fin/261008_anthropic2_account_pool/050_acceptance_map.md @@ -0,0 +1,104 @@ +# 050 — Acceptance map + +Every acceptance scenario of this unit maps to a work-phase, a test file and the hosted CI job that runs +it. "Shared fixture" means the scenario is executed for both instances by the parameterised harness in +`tests/helpers/anthropic-instance-fixture.ts` (two instances, four accounts, A1 and B1 deliberately sharing +the stored account ID while tokens and verified UUIDs differ, a fake upstream that records which token and +UUID each physical send carried). No suite is copied with a string replace. + +## Harness + +| ID | Contract | WP | Test file | +|---|---|---|---| +| H-01 | two instances, four accounts, equal-ID collision, send ledger, isolated home | wp3 | `tests/helpers/anthropic-instance-fixture.ts` | +| H-02 | the same case body runs for A and B; only instance/account mapping is normalised | wp3 | every `*instance*` file below | +| H-03 | negative control: a deliberately misbound test resolver (B bound to A's facade) must make the no-cross-send assertion fail; production code is untouched | wp3 | `tests/adapters/anthropic/anthropic-instance-isolation.test.ts` | + +## Regression, UX, config + +| IDs | WP | Test file | +|---|---|---| +| REG-01, REG-03, CFG-01, STATE-05, STATE-12 | wp2 | `tests/providers/provider-anthropic-instance.test.ts` | +| REG-02, REG-06, WIRE-05, WIRE-15, UX-02, UX-07 | wp3 | `tests/claude-integration/anthropic2-native-routing.test.ts` | +| REG-04, STATE-13, AUTH-01 | wp3 | `tests/adapters/anthropic/anthropic-instance-isolation.test.ts` | +| REG-05 | wp2 | `tests/providers/provider-anthropic-instance.test.ts` (apikey, compatible adapter, claude-cli excluded) | +| REG-07, AUX-07 | wp3 | `tests/adapters/anthropic/anthropic-instance-isolation.test.ts` (explicit combo keeps targets) | +| UX-01, UX-03 | wp4 | `tests/codex-integration/anthropic2-catalog.test.ts` | +| UX-04, UX-05 | wp4 | `gui/tests/anthropic2-provider-surfaces.test.tsx` | +| UX-06 | wp4 | `tests/cli/cli-anthropic2-account.test.ts` | +| CFG-02, CFG-03, CFG-04, STATE-11 (salvage half) | wp2 | `tests/config/config-anthropic-instance-pool.test.ts` | +| CFG-05 | wp5 | single PR: B is advertised only in the PR that carries every slice | + +## Credentials + +| IDs | WP | Test file | +|---|---|---| +| AUTH-02, AUTH-12, AUTH-15 | wp2 | `tests/oauth/oauth-anthropic-instance-registration.test.ts` (CLI fixture hash unchanged, no Keychain stub call) | +| AUTH-03, AUTH-14, UX-04 (server half) | wp2 | same file (login writes only B; failure leaves A and defaults untouched) | +| AUTH-04, AUTH-05, AUTH-08 | wp2 | `tests/oauth/oauth-anthropic-instance-refresh.test.ts` | +| AUTH-06, AUTH-07 | wp2 | same file, run for both instances against the existing single-flight/intent contract | +| AUTH-09, AUTH-10, AUTH-11, AUTH-13, AUTH-16 | wp2 | `tests/oauth/oauth-anthropic-instance-registration.test.ts` | +| STATE-10 | wp2/wp3 refinement | same file (custom key provider, gateway OAuth row, unmarked canonical OAuth row and orphan auth row preserved); marked override parity and marker round-trip in provider/config/discovery tests | + +## Pool policy and recovery + +| IDs | WP | Test file | +|---|---|---| +| POOL-01 … POOL-09, POOL-16, POOL-17 | wp3 | `tests/adapters/anthropic/anthropic-instance-pool-parity.test.ts` (shared fixture) | +| POOL-10 … POOL-15 | wp3 | `tests/adapters/anthropic/anthropic-instance-recovery.test.ts` | +| POOL-18 … POOL-20 | wp3 | same file (await barriers, output-started no-replay, physical budget) | +| STATE-01, STATE-02, STATE-03, STATE-09 | wp3 | `tests/adapters/anthropic/anthropic-instance-isolation.test.ts` | + +## Wire and model features + +| IDs | WP | Test file | +|---|---|---| +| WIRE-01, WIRE-02, WIRE-14, WIRE-16 | wp3 | `tests/claude-integration/anthropic2-native-routing.test.ts` | +| WIRE-03, WIRE-04, WIRE-12, WIRE-13 | wp3 | `tests/responses/anthropic2-responses-parity.test.ts` | +| WIRE-06, WIRE-08, WIRE-09 | wp3 | `tests/responses/anthropic2-responses-parity.test.ts` and `tests/claude-integration/anthropic2-native-routing.test.ts`: separate cases per feature (thinking/redacted thinking/signatures; tools, tool results, parallel tools; cache_control/TTL/images), each run native and translated for A and B, upstream bodies compared after normalising only token/UUID | +| WIRE-07 | wp3 | `tests/claude-integration/anthropic2-native-routing.test.ts`: first-party destination and beta/identity conditions identical; a custom `anthropic2` gateway row stays non-native | +| WIRE-10 | wp3 | `tests/adapters/anthropic/anthropic2-fast-parity.test.ts`: fast off, opt-in on, unsupported model, for A and B (mirrors `anthropic-fast-opt-in.test.ts` cases) | +| WIRE-11 | wp4 | `tests/codex-integration/anthropic2-catalog.test.ts`: catalog/client context and output markers versus effective limits are equal for A and B | + +## Helpers, observability + +| IDs | WP | Test file | +|---|---|---| +| AUX-01 … AUX-06, AUX-08, AUX-11 … AUX-14 | wp4 | `tests/vision/vision-anthropic-instance-sidecar.test.ts`, `tests/web-search/web-search-anthropic-instance.test.ts` | +| AUX-09, AUX-10 | wp4 | `tests/server/anthropic2-management.test.ts` | +| STATE-04 | wp4 | same file (unknown save outcome on B pool settings, A untouched) | +| STATE-06, STATE-07, STATE-08 | wp4 | `tests/usage/anthropic2-usage-attribution.test.ts` | +| account health DTO (A/B same ID, different cooldown) | wp3 | `tests/oauth/oauth-anthropic-instance-health.test.ts` | + +## Compatibility evidence and exclusions + +- STATE-11 pinned pre-feature source at `6a7632db2a85c359da9feac976180450ebb42c60` + executed under Bun 1.4.0 in [hosted run 37752878337](https://github.com/lidge-jun/opencodex/actions/runs/37752878337). + Its receipt confirms orphan B read preservation, preservation after A and B + writes, and selective config salvage without auth mutation. The uploaded artifact + is named `anthropic2-old-version-`; only synthetic data was used. + This establishes the narrow old-store compatibility check, not supported in-place + downgrade with active B. Downgrade instructions retain stop/backup/remove-B-config + requirements and never move or delete A credentials. +- The diagnostic workflow (`.github/workflows/anthropic2-contract-diagnostic.yml`), its old-version + fixture (`tests/fixtures/anthropic-instance-old-version.ts`) and the dashboard capture scripts + (`scripts/ci/anthropic2-gui-capture/`) targeted only this branch and were removed before merge; + they remain at `6921271276`. The wp4 helper/surface suites and GUI suites passed there in hosted run + 37797569391, which also produced the PR screenshots (published on `pr-assets` at `50b1da0832`). +- Section 9 live-account smoke: needs separate owner approval and real accounts; not run. + +## Release blockers (PRD §11) → evidence + +| Blocker | Evidence | +|---|---| +| B succeeds with no B account via A/caller/CLI token | AUTH-01, AUTH-02, WIRE-05 | +| B 403/429/usage changes A state | POOL-10, POOL-11, POOL-13, H-03 | +| B is bridge-only | WIRE-01 | +| account/UI calls an A-only endpoint | UX-05, UX-06, AUX-09 | +| helpers use A for a B request | AUX-01, AUX-02, AUX-03 | +| A config change changes B or the reverse | CFG-02, STATE-04 | +| provider prefix reaches upstream | WIRE-15 | +| migration overwrites A config/auth | REG-01, STATE-10 | +| B changes bare-model/default/Claude Code routing | REG-02, REG-06 | +| tests cannot tell A from B | H-03 negative control | +| unrun CI reported as passing | wp5 exact-head check-run evidence | diff --git a/devlog/_plan/261003_codex_local_messaging/000_scope_and_boundary.md b/devlog/_plan/261003_codex_local_messaging/000_scope_and_boundary.md new file mode 100644 index 00000000000..52341d2a6d3 --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/000_scope_and_boundary.md @@ -0,0 +1,119 @@ +# 000 — local Codex messaging: proposed first contribution + +Status: preparation, not an accepted architecture or review-ready implementation. +Foundation implementation progress is recorded in [030](030_foundation_implementation.md). +Prepared 2026-10-03 against upstream `dev` at +`2e3acab46e20b9bc7eace0c9301bf4330be83ac1`. + +## Direction and scope + +[Proposal #6478](https://github.com/lidge-jun/opencodex/issues/6478), +[maintainer direction](https://github.com/lidge-jun/opencodex/issues/6478#issuecomment-5965174821) +and [contributor reply](https://github.com/lidge-jun/opencodex/issues/6478#issuecomment-5965461973) +establish the proposed first slice: exact local-session discovery and +caller-owned Codex queued delivery/correlation, with uncertain outcomes, +bounded lifecycle and disabled-path regressions. Boundary acceptance is pending. + +The existing broader implementation remains a separate local experiment. +Do not cherry-pick its complete feature commits into this contribution. + +Included: + +- An explicitly invoked CLI command that discovers loaded Codex sessions in the + caller's effective Codex home through its existing app-server control socket. +- Exact loaded thread ID or exact unique session name resolution. No fuzzy match. +- One native `codex queue` submission to that resolved thread ID. +- A compact peer envelope with wrapper-generated message ID, kind, optional + correlation ID and honest sender/reply context. +- Machine-readable submission receipts and bounded command-owned resources. + +Excluded: + +- Remote hosts, enrollment, SSH, bearer-token management, topology and dashboard. +- Claude transport, permissions/delegation machinery and idle triggers. +- Persistent directories/caches, isolation policy, managed skills and daemon startup. +- Resuming or creating sessions, steering/interruption, processing acknowledgements, + delivery monitoring, retries and message-body logging. + +## Proposed boundary and assumptions + +The command is the opt-in boundary. Ordinary proxy startup must not import or +activate the messaging implementation; use command-local dynamic dispatch. +No server composition hook, listener, persisted enable flag or background timer +is necessary. This is the working interpretation of *caller-owned*, not a claim +that the maintainer has approved this exact design. + +Provisional command surface: + +```text +ocx message sessions --json +ocx message send --thread --stdin --json +ocx message send --name --stdin --json +ocx message send --thread --kind response --in-reply-to --stdin --json +``` + +Send accepts exactly one selector. Reject remote, Claude and unsupported options before +transport or child-process work. Initial transport scope is Linux/macOS Unix +sockets; Windows reports unsupported explicitly. macOS parity remains untested. +No guessed home, arbitrary remote URL or socket from message-body text. + +Discovery uses `thread/loaded/list`, follows bounded pagination and reads metadata +with `thread/read` and `includeTurns: false`. Do not scan rollout files or return +turns/previews. Resolve names only after complete discovery. A failed metadata +read or exhausted/repeated cursor makes name resolution incomplete, not unique. +Require a selected ID to be loaded; a race in which it unloads is not permission +to resume it. Snapshot membership cannot guarantee that it remains loaded at send. + +Use the existing Codex home/runtime selection and executable-invocation helpers, +without persisting selection, installing a runtime or starting an app-server. +Capability checks are read-only and command-scoped. Selection/probe time must +count toward the whole-operation deadline, not just the queue child deadline. + +`CODEX_THREAD_ID` is contextual sender attribution, not proof of authority. Read +its name metadata when available; otherwise return an explicit missing/unknown +identity. Generate a reply command only from a validated sender ID, never from +peer prose or a display name. No managed skill is installed in this slice, so do +not advertise `responseSkill` as though one were available. + +Request/response/notification intent is explicit. Responses carry `inReplyTo`; +responses and notifications do not solicit courtesy acknowledgements. A short +request hint can say that normal final output does not reply to the peer. Peer +messages are not user approval or escalation. No new delegation policy is added. + +## Submission outcomes + +| Boundary | Receipt meaning | Retry | +| --- | --- | --- | +| Validation/discovery/capability failure before submission | `not_sent`; no queue submission attempted | Caller may fix input; no automatic retry | +| Native queue exits successfully | `queued`; submission acknowledged, not processed | None | +| Queue invocation may have submitted, but result is lost/failed/timed out | `unknown`; delivery cannot be inferred | Never replay automatically | + +Retain the generated envelope ID in uncertain receipts for correlation. Do not +call it a native queue submission ID, deduplication guarantee or delivery receipt. +The native CLI owns its `clientUserMessageId`; the envelope's `messageId` is a +separate application-level correlation identifier. + +Provisional limits: one 30-second overall deadline, at most 10 seconds per RPC, +at most 20 seconds for the queue child, 20 discovery pages of 50 IDs, four +concurrent metadata reads, 1 MiB RPC frames, 64 KiB per child output stream and +256 KiB complete envelope. These are proposed bounds, not native protocol limits. +Any exhausted discovery budget is incomplete. Close all command-owned sockets, +pending requests and timers; join terminated child processes. Never stop the +recipient session, its daemon or a proxy service. + +## Next decision and safe implementation order + +Proceed provisionally with standalone local types, bounded RPC and strict offline +fixtures first; then exact discovery, envelope/send, and CLI registration/docs. +No acceptance of the broader architecture is inferred from starting these pieces. +Publish a draft only after boundary agreement, native interoperability evidence +and required independent review, including explicit security review of the +transport/identity changes under `MAINTAINERS.md`. + +Implementation must add an owning structure document/manifest claim and an ADR +using the then-current available number. Update CLI registry/help, generated skill +surface and public documentation together. No GUI screenshot is needed for this +CLI-only slice. Follow the PR template and target current `dev`, not `main`. + +See [extraction map](010_extraction_map.md) and +[contract/test ledger](020_contract_and_test_matrix.md). diff --git a/devlog/_plan/261003_codex_local_messaging/010_extraction_map.md b/devlog/_plan/261003_codex_local_messaging/010_extraction_map.md new file mode 100644 index 00000000000..f7d7d722fc3 --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/010_extraction_map.md @@ -0,0 +1,72 @@ +# 010 — source extraction map + +Source experiment: `feat/claude-messaging` at +`9875e0c363579230fc53c0bea3114c868ef5ec94`. +Destination base: upstream `dev` at +`2e3acab46e20b9bc7eace0c9301bf4330be83ac1`. +Paths below are relative to the respective repository, not links claiming these +modules already exist on the destination branch. + +## Reuse by responsibility, not whole feature commits + +| Experiment source | Local responsibility | Disposition | +| --- | --- | --- | +| `src/messaging/socket.ts` | Bun Unix WebSocket adapter | Extract with unsupported-platform and socket-URL tests | +| `src/messaging/rpc.ts` | Initialize/initialized, metadata reads, pagination, pending requests | Extract; add whole-operation deadline, bounded metadata concurrency and strict response validation | +| `src/messaging/process.ts` | No-shell child runner, output bounds, termination/join | Extract; retain abort/timeout cleanup and sanitized failures | +| `src/messaging/send.ts` | Exact resolution, native queue invocation, uncertain receipt | Rewrite local-only; remove store, bridge, enrollment, Claude, isolation and remote authentication dependencies | +| `src/messaging/sender.ts` | `CODEX_THREAD_ID` plus metadata-only name | Rewrite Codex-only; no machine store, permission attribution or cross-harness process inference | +| `src/messaging/envelope.ts` | Message kinds, IDs, reply/correlation | Rewrite compact local routing; no machine identity or managed response skill | +| `src/messaging/types.ts` | UUID validation, local receipt/thread/error types | Extract only local types, not mesh/peer/policy state | +| `src/cli/message.ts`, `src/cli/message-args.ts` | Command dispatch and argument parsing | Rewrite minimal local commands; do not retain unavailable feature flags | +| `tests/helpers/messaging.ts` | Isolated daemon fixture | Replace store-dependent fixture with standalone Unix socket fixture | + +Exclude `src/messaging/` store/files/runtime/slot/bridge/policy/supervisor/SSH, +enrollment, mesh, directory/cache, isolation, idle, Claude, sender-permissions, +sender-process and managed-skill modules. Do not carry messaging assets or +server management routes. Existing broad lifecycle/authentication tests do not +prove the new CLI-only boundary. + +## Reuse destination facilities + +| Current upstream file | Purpose / integration caution | +| --- | --- | +| `src/codex/home.ts` | Effective home resolution; do not hard-code a user's home | +| `src/codex/runtime.ts` | Runtime selection; use non-persisting resolution, not `resolveAndPersistCodexRuntime` | +| `src/codex/exec-invocation.ts` | Preserve launcher invocation semantics instead of assuming the selected command is a native executable | +| `src/cli/dispatch.ts` | Command-local dynamic import; no startup activation | +| `src/cli/registry.ts`, `src/cli/capabilities.ts`, `src/cli/help.ts` | Register the minimal surface and derive/regenerate documentation counts | +| `scripts/test-layout/layout.json`, `tests/fixtures/test-layout-expected.json` | Register each new domain test in both files | +| `structure/manifest.json`, `structure/INDEX.md` | Add ownership and regenerate index when source is introduced | + +Observed runtime resolution ranks environment/configured/discovered candidates +and performs version probes in temporary Codex homes. It does not itself install +a runtime. Preserve bounded command execution by injecting a bounded async probe +and checking the operation deadline; do not allow a large candidate search to +escape the messaging command's budget. Avoid modifying the shared runtime +resolver solely for this feature unless evidence shows a required fix. + +## Existing regression seeds + +- `tests/codex-integration/messaging-transport.test.ts`: metadata-only sender, + Unix transport translation, single submission, ambiguous/incomplete names. +- `tests/codex-integration/messaging-envelope.test.ts`: explicit kinds, IDs, + body-independent reply routing and no courtesy-ack obligation. +- `tests/codex-integration/messaging-lifecycle.test.ts`: broad default-off checks; + replace with command-local import/resource assertions rather than copying the + broad runtime's lifecycle. +- `tests/codex-integration/messaging-native-queue.test.ts`: opt-in native queue + fixture through an authenticated bridge; replace with direct Unix fixture. + Its existence is not evidence that the native test ran. + +## Evidence limits + +This is a direct source inventory, not a certified import graph or exhaustive +blast-radius analysis. No complete dependency graph was materialized for this +preparation pass. + +A text search of destination `src/` and `tests/` found no existing +`app-server-control`, `ws+unix`, `thread/loaded/list` or `thread/queue/add` matches. +That bounded observation is not a claim that no related functionality exists. +Current app-server process/restart management and remote workspace transport are +different responsibilities; no reuse of their session-creation behavior is assumed. diff --git a/devlog/_plan/261003_codex_local_messaging/020_contract_and_test_matrix.md b/devlog/_plan/261003_codex_local_messaging/020_contract_and_test_matrix.md new file mode 100644 index 00000000000..597fd5aaee0 --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/020_contract_and_test_matrix.md @@ -0,0 +1,109 @@ +# 020 — native contract and acceptance ledger + +Preparation snapshot, 2026-10-03, before implementation or source tests. +Subsequent foundation implementation and verification are recorded in +[030](030_foundation_implementation.md); the remaining matrix is not all complete. + +## Native evidence actually obtained + +Native binary: `codex-cli 0.160.0`, Linux x86_64 musl distribution. +Binary SHA-256: +`12eb3e81114588aca3b7998f4f19e8997b056aca08e57a7ca7c8a3ec8c652aad`. + +Executed `codex --version`, `codex queue --help`, and offline schema export: + +```bash +codex app-server generate-json-schema --experimental --out /schemas +``` + +Schemas are ignored scratch artifacts, not vendored upstream source. Regenerate +from the pinned binary for interoperability verification. No live message, daemon +startup, session resume, API inference or queue write was performed in this pass. + +| Contract | Evidence and limit | +| --- | --- | +| Initialization | Official app-server documentation describes one initialize request followed by initialized per connection | +| Loaded discovery | Generated `ThreadLoadedListParams` has optional limit/cursor; response requires data IDs with optional nextCursor | +| Metadata read | Generated `ThreadReadParams` requires threadId and permits includeTurns; use false | +| Queue CLI | Help exposes thread/message/remote; remote accepts Unix sockets. Resolve the target ourselves, then supply its ID | +| Queue RPC | Generated `ThreadQueueAddParams` requires threadId, input and clientUserMessageId; response requires queuedSubmission with id/input/clientUserMessageId | +| Control socket path | Existing experiment uses `/app-server-control/app-server-control.sock`; deployment convention still needs native runtime verification, not schema inference | +| Unix transport spelling | Bun `ws+unix://:/` versus native CLI `unix://`; prove translation against isolated native queue fixture | +| Queue completion | Schema acknowledgement is not evidence of recipient processing, steering or end-to-end delivery | + +[Official Codex app-server documentation](https://learn.chatgpt.com/docs/app-server) +was consulted after an official-domain search. Its overview did not document +`thread/queue/add`; the queue-specific evidence above comes from this binary's +generated schemas/help, not an invented public API guarantee. + +Generated schema SHA-256 receipts: + +| File under `v2/` | SHA-256 | +| --- | --- | +| `ThreadLoadedListParams.json` | `42c2d124ab2b2998d84f0126daae2d32b0bf9c5a7ea7266e3a0814e5e37cf00d` | +| `ThreadLoadedListResponse.json` | `1ea02c4710083e552b2a3a89b931b9049d5bf05b7db46278b8352cf9fd80cd52` | +| `ThreadQueueAddParams.json` | `60f25b7d3e3357c215bef9d2fe7047200f545e030dca9e00527ac0620cd0c88f` | +| `ThreadQueueAddResponse.json` | `c11b9772788427b19c9f9decf0765e2b41cbdead1aa9c3d39f42fdd70b339314` | + +Only 0.160.0 was inspected here. Minimum supported version, cross-version +compatibility, macOS operation and actual native queue interoperability remain +unverified. Do not claim a supported version range from presence of help flags. + +## Named offline acceptance cases (proposed, not yet implemented) + +All files below belong in `tests/codex-integration/`; use a standalone +`tests/helpers/messaging-local.ts` with fresh scratch home/socket, no actual +sessions, no global config, no listeners surviving teardown and strict RPC +allowlisting. An unexpected thread/start, thread/resume or turn/start must fail. + +| Proposed file | Required cases | +| --- | --- | +| `messaging-local-discovery.test.ts` | metadata-only loaded discovery; exact ID/name; duplicate or absent names; incomplete metadata; repeated/exhausted cursors; malformed/oversized pages; unloaded ID; empty valid directory; bounded concurrency | +| `messaging-local-send.test.ts` | Unix argv translation; effective home and launcher selection; unsupported queue fails before send; one submission on success; lost receipt/nonzero/timeout yields unknown and no replay; preflight failure yields not_sent; cancellation before/after invocation; no daemon start/resume | +| `messaging-local-envelope.test.ts` | generated IDs; response correlation; explicit kind; no ack obligation for responses/FYIs; missing sender cannot invent reply; names/body cannot supply routing; UUID/control-character validation; bounded stdin/full envelope; no body in receipts/logs | +| `messaging-local-lifecycle.test.ts` | disabled startup imports/activates no messaging; no store/listener/timer/skill/probe on ordinary startup; RPC abort/close clears requests/timers/listeners; oversized frame/output ends safely; terminated own child is joined; whole-operation deadline covers discovery/probes; Windows unsupported | +| `messaging-local-native-queue.test.ts` | pinned actual native queue against isolated Unix fixture: initialize handshake, exact ID, exactly one thread/queue/add and correlated queued submission; no lifecycle mutation; failed/lost acknowledgement cannot trigger replay | + +Native interoperability is opt-in locally using an explicit binary path/version +receipt; the deterministic fixture regressions must run offline by default. +Pin the native binary in CI only through accepted repository installation policy; +do not silently download a moving `latest` or count an opt-in skip as passing +interoperability. Investigate capability mismatch before declaring support. + +## Verification and readiness sequence + +1. Implement fixture/RPC tests, then discovery, envelope/send and CLI integration; + run focused regressions at each stage. Register new files in both test maps. +2. Run the native Unix fixture test with exact binary/hash recorded. It must not + connect to the user's daemon or require an API key. +3. Before review readiness: `bun run typecheck`, focused source-oracle/CLI/layout + regressions, `bun run test:changed` and the default `bun run test`; document any + resource exception and exact coverage left to CI rather than claiming a pass. +4. Run `bun run privacy:scan`, `bun run structure:check`, and + `bun run skill:surface:check` after registry/docs integration. Check file-size + ratchets; extract siblings instead of raising caps. +5. Recheck current `dev`, resolve correct automated review findings, obtain + independent/security review and inspect required CI for the exact PR head. + Follow the contributor draft/readiness checklist; missing or skipped CI is not + passing evidence. Publication/merge/deployment are separate authorizations. + +Preparation-only validation should cover whitespace, referenced source paths, +document links and privacy/structure guards. Do not rerun the source suite merely +to validate unchanged code or label this document packet review-ready software. + +## Preparation verification receipt + +On 2026-10-03, using the existing repository's Bun executable against the +isolated destination worktree: + +- `bun scripts/privacy-scan.ts`: passed for the tracked repository. +- `bun scripts/structure-ssot.ts`: passed. +- Direct `scanText` on all three new, then-untracked preparation files: passed. +- Direct whitespace/relative-link checks and existence checks for 25 source + references across the source and destination trees: passed. +- `git diff --check`: passed; new files also checked directly because an + unstaged untracked file is not included in this command's diff. + +No runtime source changed. Typecheck, the source test suite and native queue +interoperability were not run; these preparation checks do not substitute for +implementation acceptance evidence. No publish, install or restart occurred. diff --git a/devlog/_plan/261003_codex_local_messaging/030_foundation_implementation.md b/devlog/_plan/261003_codex_local_messaging/030_foundation_implementation.md new file mode 100644 index 00000000000..e6764ada511 --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/030_foundation_implementation.md @@ -0,0 +1,87 @@ +# 030 — local foundation implementation and handoff + +2026-10-03. Branch `feat/codex-local-messaging`, based on upstream `dev` at +`2e3acab46e20b9bc7eace0c9301bf4330be83ac1`. + +The user authorized proceeding with reasonable assumptions after no further +maintainer reply. **No PR may be opened without the user's explicit go-ahead.** +This is implementation progress, not upstream acceptance or review readiness. + +The subsequent command-local implementation and current validation are recorded +in [040](040_command_implementation.md); remaining work below is historical to +this foundation increment. + +## Implemented boundary + +- Standalone local types and conservative Unix socket addressing; no network URL, + token, home discovery, persistent state or daemon startup. +- Initialize/initialized handshake, metadata-only reads, strict bounded response + projection, four pending requests and close/cancellation cleanup. +- Complete loaded-only pagination and exact selector resolution, rejecting + duplicate/ambiguous/incomplete data and unload races. +- A whole-operation deadline and bounded no-shell helper runner. Unix helpers + have their own process group so launcher descendants cannot hold inherited + pipes open indefinitely after timeout; the daemon/agent group is not signalled. +- Offline strict Unix fixtures and native queue interoperability, including a + lost acknowledgement with exactly one submission and no replay. +- Structure ownership/ADR and both test-layout maps. The foundation has no + incoming source imports or public CLI registration yet. + +## Validation obtained + +Using Bun 1.4.0 and the explicit native Codex 0.160.0 binary/hash recorded in +[020](020_contract_and_test_matrix.md): + +| Command / scope | Result | +| --- | --- | +| `bun scripts/test.ts --changed=dev`, with explicit native binary opt-in | 25 passed, 0 failed across the three messaging regression files; compared with upstream/dev merge base above | +| Focused messaging plus structure and file-size regressions before the final additional bound cases | 83 passed, 0 failed; included both native queue cases | +| Test-layout and tooling regressions | 18 passed; the same combined run initially exposed a parser guard defect later corrected | +| `node_modules/.bin/tsc --noEmit` | Passed with the isolated worktree's pinned TypeScript | +| `bun scripts/privacy-scan.ts` | Passed during implementation; repeated on staged final packet before commit | +| `bun scripts/structure-ssot.ts --fix` and structure regressions | Passed; generated index includes the new owner | +| `git diff --check` / staged whitespace check | Passed | + +The changed wrapper was invoked directly with the existing Bun executable on +PATH. Locked runtime and GUI dependencies were installed separately with +`--frozen-lockfile --ignore-scripts`; no Bun dependency postinstall was enabled. +The npm `bun run test:changed` alias initially reached that intentionally +uninitialized wrapper, so the identical `scripts/test.ts --changed=dev` entry +was used instead. + +Initial socket tests failed under the socket-restricted sandbox (`EPERM`) and +were run with scoped escalation against temporary fixtures only. The literal +import guard also initially tried to parse shebangs and erased declaration +files; both harness defects were corrected. + +The first native-enabled changed-runner attempt found a real fixture mismatch: +nested temporary homes made the socket path 126 bytes, and native queue failed +before any RPC call. Fixtures now use a short, uniquely owned Unix temporary +root with the repository stale-root marker. Transport addressing enforces a +conservative 103-byte UTF-8 path policy. The corrected native-enabled changed +run passed, including lost-acknowledgement behavior. + +No full-suite run, macOS/Windows native interoperability, arbitrary version +range or live recipient processing is claimed. This is an implementation +increment, not a review-ready PR; the nested source instructions defer the full +suite until review readiness. No account inference/API call, live message, +publish, install of OpenCodex, deployment or service restart occurred. + +Validation used direct source inspection and the named regression tests above. +No complete dependency graph was materialized; certified dependency completeness +is not claimed. + +## Remaining bounded slice + +Implement the local envelope, honest Codex sender context and receipt layer +(`not_sent`, `queued`, `unknown`), then the minimal command-local CLI, existing +home/runtime selection, capability preflight and public docs/generated surface. +Include uncertain-send/no-replay, no acknowledgement loops and no guessed reply +routes in the focused regressions. Replace the foundation's zero-incoming-import +guard with a command-local activation guard when CLI wiring actually exists. + +Remote, Claude, skill management, permissions/delegation, dashboard, +isolation and idle notices remain outside this contribution. Retain the deployed +`feat/claude-messaging` branch/head unchanged. Independent/security review and +the contributor readiness gates remain outstanding; PR authorization remains +explicitly withheld. diff --git a/devlog/_plan/261003_codex_local_messaging/040_command_implementation.md b/devlog/_plan/261003_codex_local_messaging/040_command_implementation.md new file mode 100644 index 00000000000..55fae4ee55e --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/040_command_implementation.md @@ -0,0 +1,97 @@ +# 040 — local command implementation and validation + +2026-10-03. User authorized completing the local-only contribution. The isolated +branch `feat/codex-local-messaging` was rebased cleanly onto upstream `dev` +`e601cefcebcc20b2e04a04821ab45df6538d98f9`. The deployed checkout and its +`feat/claude-messaging` head remain unchanged; no service, installation, live +queue, remote host, issue comment or PR was changed. + +## Implemented + +- `ocx message sessions [--json]` returns a complete loaded-only metadata snapshot. +- `ocx message send (--thread | --name ) --stdin` supports + request, correlated response and notification kinds with bounded UTF-8 input. +- Sender context comes from loaded CODEX_THREAD_ID plus metadata, explicitly not + authenticated authority. No body/name-derived route or invented missing route. +- Command-owned receipts distinguish not_sent, queued and unknown; queued is not + processed, and uncertain sends are never replayed or probed by another send. +- Native runtime selection reuses the existing non-persisting resolver and + invocation builder. Admission requires Codex 0.160.0 and tested queue/Unix help. +- Native probes and one queue invocation use an owned temporary credential-free + home, shim bypass/probe flags and the explicit existing local Unix address. +- The exact destination ID is rechecked after preflight; unloading blocks sending + without resume. One operation budget includes stdin and helper work. +- CLI registration/help/capability data, generated operating surface, owning + structure documentation, provisional ADR and public CLI reference are updated. + +Remote auth/topology/SSH, dashboard, Claude, permissions/delegation, +isolation, managed skill installation and idle notices remain absent. + +## Validation receipts + +Commands used the existing Bun 1.4.0 executable, not the uninitialized npm wrapper. +Socket-capable runs used scoped escalation and temporary fixtures only. Locked +documentation dependencies were installed with frozen lockfile and ignored +lifecycle scripts. No lockfiles changed. + +| Check | Result | +| --- | --- | +| New send/envelope/CLI plus lifecycle regressions | 29 passed, 0 failed | +| Native Codex 0.160.0 queue/workflow, discovery, registry/capabilities/help and layout regressions | 70 passed, 0 failed | +| `OCX_MESSAGE_CODEX_BINARY= bun scripts/test.ts --changed=dev` | 1039 passed, 1 Windows-specific skip, 0 failed; 51 files; 81.6 seconds; merge base above | +| Structure, file-size ratchet and generated operating-surface regressions | 76 passed, 0 failed | +| `node_modules/.bin/tsc --noEmit` | Passed | +| `bun scripts/structure-ssot.ts` | Passed; generated index updated | +| `bun scripts/generate-ocx-skill-surface.ts --check` | Passed | +| `bun scripts/privacy-scan.ts` | Passed | +| `cd docs-site && bun run build` | Passed; 569 pages; 78880 internal links; build/search index completed | +| `git diff --check` | Passed | + +Native version/hash receipts remain in [020](020_contract_and_test_matrix.md). +The complete send fixture proves one native queue add and envelope correlation; +lost acknowledgement proves unknown with one submission, not replay. No live +recipient processing, macOS native interoperability, Windows transport or arbitrary +version range is claimed. + +The initial owning-runtime doc insertion hit its 600-line limit. It was replaced +with a concise update to the existing entrypoint contract, without increasing the +budget or introducing a grace exemption. Corrected structure/ratchet checks passed. + +The command-local parser inventory and named tests are source/runtime evidence, +not certified graph completeness. No complete dependency graph was materialized +on this worktree. + +## Remaining readiness gates + +### Final upstream refresh + +Upstream advanced during the first validation pass. The completed contribution +was preserved and rebased cleanly onto +`358b8ffd4c7f95237dadee1e9a5b4fedb671f4f4` (the newly integrated Claude OAuth +identity change). No messaging source conflict or runtime seam change occurred; +both test-layout maps retain the upstream identity regression registration. + +On this refreshed source tree: + +- `bun scripts/test.ts --changed=dev`, with the same pinned native binary: + **1039 passed, 1 Windows-specific skip, 0 failed**, 51 files, 83.4 seconds. +- Explicit layout/tooling, structure, file-size and generated-surface regressions: + **94 passed, 0 failed**. +- `node_modules/.bin/tsc --noEmit`: passed. +- Documentation build: passed again, including search indexing and 78880 internal + links across 569 pages (51.1 seconds). + +Ignored logs remain in the worktree's `.tmp/` as +`local-messaging-changed-final.log`, `local-messaging-source-guards-final.log` and +`local-messaging-docs-build-final.log`. These are local validation receipts, not +hosted CI or live-delivery evidence. No additional source edits followed this pass. + +### Outstanding + +The bounded implementation slice is complete, but no independent/security review +or hosted CI is claimed. The full default suite is deferred until review readiness, +as required by the nested source instructions; the changed suite and named +source-oracle/golden regressions above cover this implementation increment. +Recheck latest upstream dev, run the default suite, resolve correct independent +review findings and fill the contributor readiness checklist before claiming +review readiness. The user still explicitly withholds permission to open a PR. diff --git a/devlog/_plan/261003_codex_local_messaging/050_cleanup_and_rebase_validation.md b/devlog/_plan/261003_codex_local_messaging/050_cleanup_and_rebase_validation.md new file mode 100644 index 00000000000..d69baaa96b7 --- /dev/null +++ b/devlog/_plan/261003_codex_local_messaging/050_cleanup_and_rebase_validation.md @@ -0,0 +1,66 @@ +# 050 - helper cleanup and current-dev refresh + +2026-10-03. User authorized the helper cleanup correction and a refresh onto +current upstream dev. The contribution was rebased cleanly onto +`3541261ac776679448664e98164add5fba203edd`; all five contribution commits replayed +without conflict. The deployed checkout remains unchanged. + +## Changes and provenance + +- Forced helper cleanup cancels pending output readers without waiting for EOF + or asynchronous cancellation hooks. Owned group termination remains intact; + no detached group outside that ownership is signalled. +- Regression coverage exercises timeout, cancellation and output overflow with + a self-expiring detached fixture, while retaining the existing same-group + descendant cleanup and uncertain-submission/no-replay tests. +- Upstream-facing documents stay confined to the contribution's intended scope. +- The source extraction follows [010](010_extraction_map.md), not a whole-feature + cherry-pick. It retains the existing local messaging approach while tightening + loaded-only discovery, lifecycle budgets, runtime admission and helper homes. + It is not byte-identical to the deployed experiment with other features removed. + +## Validation on the rebased implementation + +Commands used the installed Bun 1.4.0 and corrected worktree dependency PATH. +Socket/native tests used isolated fixtures and explicit Codex CLI 0.160.0 from +the locally installed, previously hash-verified standalone release. + +| Command | Result | +| --- | --- | +| `bun scripts/test.ts --changed=dev` with `OCX_MESSAGE_CODEX_BINARY` set | 1040 passed, one Windows-only skip, zero failures; 51 files; 93.5 seconds; comparison merge base is the dev commit above. | +| `bun scripts/test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/structure-ssot.test.ts tests/ci-workflows/file-size-ratchet.test.ts tests/ci-workflows/skill-ocx.test.ts` | 94 passed, zero failures; 1.7 seconds. | +| `node_modules/.bin/tsc --noEmit` | Passed. | +| `bun scripts/structure-ssot.ts` | Passed. | +| `bun scripts/generate-ocx-skill-surface.ts --check` | Passed. | +| `bun scripts/privacy-scan.ts` | Passed. | +| `cd docs-site && bun install --frozen-lockfile --ignore-scripts && bun run build` | Passed; 569 pages, 78892 checked internal links; build 50.9 seconds. | +| Repository hygiene/sponsorship-path classifiers on the complete contribution diff | No failures or restricted-path flags; not independent/security review. | +| `git diff --check upstream/dev...HEAD` | Passed. | + +Before rebase, focused lifecycle/send/native-queue validation passed all 24 tests. +The original isolated cleanup probe returned `process_incomplete` in 1163 ms +with a 150 ms helper timeout and 1000 ms termination grace. +Ignored final logs are `.tmp/local-messaging-rebased-changed.log`, +`.tmp/local-messaging-rebased-source-guards.log` and +`.tmp/local-messaging-rebased-docs.log`. + +## Remaining review gates and full-suite exception + +This is implementation validation, not a review-ready or merge-approved claim. +The preceding full-suite audit reached its 900-second lane limit, with 612 files +unstarted and four interrupted. Corrected focused reruns reproduced four timeout +cases and 13 service-runtime failures on an untouched upstream base. That audit +does not establish full-suite success, and its results are not claimed as passing +evidence for this rebased implementation. + +Another full run is deferred for this bounded implementation step under the +nested source instructions and documented resource exception. Connected tests +and explicit source-oracle/native regressions passed above; broader full-suite, +cross-platform and hosted validation remain for review readiness/CI. Independent +and explicit security review remain outstanding. No unrelated failure was fixed, +test timeout increased, runtime installed or compatibility range invented. + +No remote force-push, PR, deployment or service restart was performed. The fork +still points at the prior published history; publishing this rebased history +requires a separately authorized, lease-guarded update. User permission to open a +PR remains withheld. diff --git a/devlog/_plan/261008_haiku_5_5_catalog/010_plan.md b/devlog/_plan/261008_haiku_5_5_catalog/010_plan.md new file mode 100644 index 00000000000..eaddc125d59 --- /dev/null +++ b/devlog/_plan/261008_haiku_5_5_catalog/010_plan.md @@ -0,0 +1,93 @@ +# 261008 Claude Haiku 5.5 catalog, pricing and request contract + +## Loop spec + +- Class: C3 (catalog + pricing + adapter contract, cross-provider; no persistence or auth change). +- Goal: session 01a11911-4ca5-7252-a072-a46c7de5e759, goalplan `add-claude-haiku-5-5-claude-haiku-5-5-released-2`, work-phase wp1. +- Source worktree: `.tmp/lanes/h-haiku-5-5`, branch `codex/haiku-5-5-catalog` from origin/dev `bf9ecf3d79`. +- Write scope: files listed in the diff map below plus this unit. No GUI, no release. +- Tool/credential scope: public HTTP (models.dev, openrouter, vercel, cursor docs), Aside exec read-only docs, the local Anthropic OAuth credential read in memory for live contract probes (never printed or written). +- Budget: one PABCD cycle; wall clock bound 6h. +- SoT sync target: `structure/providers-and-adapters.md`, `structure/providers/chat-compat.md`, `structure/dashboard-and-usage.md` (line budgets 600; replace, do not append). + +## Problem + +Anthropic released Claude Haiku 5.5 (`claude-haiku-5-5`) on 2026-10-07. Nothing in the static catalog knows it, so live discovery shows no price, the native Anthropic seed lacks it, and the adapter would send Haiku 4.5's request shape: `thinking: enabled` with a budget (400 on Haiku 5.5) and non-default temperature/top_p (400). Its price is the first Claude price with a prompt-length band (5x above 100K prompt tokens). The same launch cut Claude Sonnet 5.5's cache-read price from 0.20 to 0.10. + +## Evidence (2026-10-08) + +| Source | Surface | Facts | +|---|---|---| +| platform.claude.com pricing, models overview, haiku-5-5 overview / whats-new / migration guide, effort, thinking, thinking-troubleshooting, fast-mode; anthropic.com/claude-haiku-5-5 | Aside exec (`.tmp/haiku55/aside-docs.md`) | id `claude-haiku-5-5` (fixed, no date); Bedrock `anthropic.claude-haiku-5-5`; Vertex/Foundry `claude-haiku-5-5`; 1M / 128K (300K batch beta); adaptive thinking on by default; effort low..max, default medium; `enabled` 400; `between_tools` 400; `disabled` OK at effort <= high, 400 at xhigh/max; forced tool_choice accepted; non-default temperature/top_p/top_k 400; assistant prefill 400; no fast mode; no Priority Tier; price <=100K 0.10 in / 0.50 out / 0.125 5m write / 0.20 1h write / 0.01 hit, >100K 0.50 / 2.50 / 0.625 / 1.00 / 0.05; Sonnet 5.5 cache hit now 0.05x (0.10) | +| models.dev/api.json | curl | anthropic, bedrock (anthropic., global. base; us./eu./jp./au. 1.1x), openrouter, vercel, kilo (no tier), opencode, opencode-go, venice (1.25x), vertex, azure: 1M/128K, base 0.1/0.5/0.01/0.125 with context tier size 100000 at 5x; Sonnet 5.5 cache_read 0.1 on anthropic, openrouter, vercel, kilo, github-copilot, edenai; 0.2 on bedrock, vertex, azure, cloudflare, opencode; venice Sonnet 5.5 2.5/12.5/0.125/3.125 | +| openrouter.ai/api/v1/models | curl | `anthropic/claude-haiku-5.5` 1M/128K, override min_prompt_tokens 100000 at 5x; Sonnet 5.5 cache read 1e-7 | +| ai-gateway.vercel.sh/v1/models | curl | `anthropic/claude-haiku-5.5` tiers min 100001; regional eu/us 1.1x | + +Raw captures: `.tmp/haiku55/` (scratch, not committed). + +## Architect consultation + +Architect: gpt-6.1-sol subagent 01a1199b-4ba7-7193-b5d0-b4e0f68092fb, read-only, proposal D1-D10 (kept in the session transcript). Dispositions: + +| ID | Proposal | Disposition | +|---|---|---| +| D1 | Keep existing owners; per-family contract, no shared "5.5" contract | ACCEPT | +| D2 | Edit-site map from c34c4d20db plus native max output 128K | ACCEPT | +| D3 | Exact CONTEXT_TIERS rows, threshold 100_000, inclusive false, multiplier 5x on all four, omit confirmedPriorityRelation | ACCEPT with scope: anthropic, anthropic-apikey, six Bedrock ids, openrouter, vercel-ai-gateway, opencode-go, opencode-zen, venice; reference tiers on every preemptive surface that gets a Haiku 5.5 row or overlay (cursor regular ids, devin, devin-cli, kiro, claude-cli if it resolves a price, zenmux, cloudflare, github-copilot). Kilo untiered (its listing has no tier). REJECT google-vertex `@default` overlay: the preset uses the Google bundle and has no Claude transport; pricing an unsupported surface is out of scope | +| D4 | Add opencode-go row; no wire override without gateway evidence | ACCEPT; residual recorded | +| D5 | Contract: adaptive minimum haiku 5.5; explicit disable for haiku >= 5.5; sampling rejection haiku >= 5.5; between_tools stays Sonnet-only and forced-choice rejection stays Opus 5.5 / Sonnet >= 5.5 / Fable >= 5.1 (`rejectsForcedToolChoice` in `anthropic-model-contract.ts`) | ACCEPT; no change to those two predicates | +| D6 | Keep all defaults; built-in `haiku` alias becomes ambiguous and is suppressed | ACCEPT: same rule already suppressed `sonnet` and `opus` when Sonnet 5.5 / Opus 5.5 landed; default aliases are opt-in; add regression | +| D7 | Sonnet 5.5 cache-read 0.2 -> 0.1 by provenance | ACCEPT as amended by 020: anthropic snapshot + `CLAUDE_SONNET_55` (anthropic, anthropic-apikey, cursor verified from cursor.com, derived devin/devin-cli/antigravity), openrouter, vercel, kilo, github-copilot snapshots to 0.1; venice exact overlay 2.5/12.5/0.125/3.125. Bedrock unchanged (partner pricing still 0.2/0.22). opencode-zen, cloudflare, zenmux have no bundle and follow the Anthropic fallback to 0.1 | +| D8 | Sibling tests within ratchet headroom | ACCEPT | +| D9 | structure docs at budget: replace prose | ACCEPT. docs-site scope exception: docs-site has no per-model Claude roster or Claude price table (only illustrative Haiku 4.5 examples; `reference/configuration/providers.md` documents OpenAI/xAI bands only) and #6210 changed no docs-site page. The roster reaches users through the live catalog and Claude CLI picker; the price band is documented in `structure/dashboard-and-usage.md` | +| D10 | Verification set | ACCEPT plus live probes and the broader-validation exception recorded under Verification | + +## Diff-level plan + +1. Contract (`src/adapters/anthropic-model-contract.ts`): add `haiku: [5, 5]` to the adaptive-thinking minimums and the sampling-rejection minimums; extend `supportsExplicitThinkingDisable` to Haiku >= 5.5 (alongside the bounded Sonnet range). `usesBetweenToolsFloor`, `rejectsForcedToolChoice`, `hasNoThinkingOffSwitch` unchanged. Comments in `src/adapters/anthropic.ts` updated where they name families. Resulting wire: reasoning none -> `thinking: disabled`; low..max -> adaptive + `output_config.effort`; omitted reasoning -> no thinking/effort (API default medium); temperature/top_p dropped on every path; forced tool choice kept. +2. Pricing (`src/usage/expected-prices.ts`): `CLAUDE_HAIKU_55` = 0.1 / 0.5 / 0.01 / 0.125 with provenance; overlays anthropic, anthropic-apikey (verified, official pricing page opened directly), venice exact 0.125 / 0.625 / 0.0125 / 0.15625 (`verified-derived`: models.dev listing, not a Venice page), cursor exact overlays for the 18 ids Cursor can emit, generated in code as `CURSOR_HAIKU_55_IDS` = three base spellings (`claude-haiku-5-5`, `claude-haiku-5.5`, `claude-5.5-haiku`) x (bare + `-low/-medium/-high/-xhigh/-max`) per `src/adapters/cursor/catalog.ts` effort-id composition (`verified`: cursor.com/docs/models/claude-haiku-5-5), devin/devin-cli (verified-derived, preemptive); Venice exact Sonnet 5.5 overlay 2.5 / 12.5 / 0.125 / 3.125 (verified-derived listing; its snapshot row is inert because Venice has no generated bundle); `HAIKU_55_LONG_CONTEXT` 5x multiplier and CONTEXT_TIERS rows per D3 (exact ids; cursor ids derived from the capability's regular level list). `CLAUDE_SONNET_55.cacheRead` 0.1 and source/date refresh per D7 (fans out to anthropic, anthropic-apikey, cursor, devin, devin-cli and the four Antigravity derived overlays). Tier rows: exact provider + id for anthropic, anthropic-apikey, six Bedrock ids, openrouter, vercel-ai-gateway, opencode-go, opencode-zen, venice, github-copilot, cloudflare-ai-gateway, zenmux, kiro `claude-haiku-5.5`, devin, devin-cli, claude-cli (if it resolves a price), and all 18 `CURSOR_HAIKU_55_IDS`. Kilo untiered. +3. Snapshot (`scripts/model-metadata.source.json`), regenerate `src/generated/model-metadata.ts` with `bun run generate:model-metadata`: Haiku rows anthropic, Bedrock x6 (base / 1.1x), openrouter, vercel, kilo, venice, opencode-zen, opencode-go (published); github-copilot, cloudflare, zenmux (preemptive, repository id conventions). All 1M / 128K, reasoning, text+image. Sonnet 5.5 corrections per D7. Runtime consumers: only bundled providers (anthropic, amazon-bedrock, openrouter, opencode-go) read these rows for catalog metadata; rows for venice, vercel, kilo, github-copilot, cloudflare, zenmux and opencode-zen are inert snapshot parity with #6210, and those providers price through the Anthropic vendor fallback (so opencode-zen, cloudflare and zenmux Sonnet 5.5 cache read follows the Anthropic correction to 0.1). +4. Seeds (`src/providers/registry/model-seeds.ts`): `claude-haiku-5-5` before `claude-haiku-4-5`, context 1_000_000, max output 128_000, provenance comment. +5. Devin (`entries-core.ts` seed list, `devin/live-models.ts` context), Kiro (`kiro-models.ts` `claude-haiku-5.5` + 1M), Cursor (`cursor/catalog.ts` regular-only FULL ladder, `cursor/effort-map.ts` low..max), Claude Desktop suggestion (`claude/intercept/model-bindings.ts`). All preemptive. +6. Structure docs per D9 (replace prose within budgets). +7. Tests: new `tests/adapters/anthropic/anthropic-haiku-5-5-contract.test.ts`, `tests/usage/usage-haiku-5-5-pricing.test.ts`, `tests/providers/haiku-5-5-catalog.test.ts` (registered in both layout maps, layout.json kept <= 1999 lines); updates to `usage-cost.test.ts` (Sonnet tuples, overlay count), `tests/usage/usage-antigravity-55.test.ts` (Sonnet cache read 0.1 plus a cache-read-token case), `anthropic-output-maxima.test.ts`, `cursor-catalog.test.ts`, `devin-adapter.test.ts`, `kiro-adapter.test.ts` (inline only, 3 lines headroom). + +## Acceptance (activation scenarios) + +- Tier boundary: `estimateRequestCost` / `estimateAttemptCost` for anthropic `claude-haiku-5-5` at 100,000 raw input tokens uses base; at 100,001 uses 5x on input, output, cache read and cache write. The same 100,001 case is asserted for every tier row: account-labelled anthropic provider, six Bedrock ids (regional at 1.1x base), openrouter/vercel slash ids, opencode-go, opencode-zen, venice, copilot, cloudflare, zenmux, kiro dotted id, devin, devin-cli, and every one of the 18 Cursor ids (three spellings x bare and five regular effort suffixes, including `claude-haiku-5.5-medium` and `claude-5.5-haiku-medium`), each with a non-null price; plus one selection-to-estimation case that resolves a Cursor effort selection through the catalog and estimates it at 100,001 tokens. Kilo stays base above 100K. +- Contract: adapter-built body for Haiku 5.5 with reasoning none has `thinking.type = disabled` and no effort; with medium/max has adaptive + effort; temperature/top_p absent; `tool_choice` required -> `any` retained. Controls: Haiku 4.5 still enabled+budget; Sonnet 5.5 still between_tools and forced->auto. +- Alias: two Haiku ids suppress the built-in `haiku` alias; an explicit user alias still wins. +- Sonnet correction: anthropic/openrouter/vercel/kilo/copilot Sonnet 5.5 resolve cache read 0.1; bedrock stays 0.2. + +## Consumer chains (PLAN-FIELD-CHAIN-01) + +| New value | Creation | Serialization | Deserialization | Consumers | +|---|---|---|---|---| +| `haiku: [5, 5]` in adaptive and sampling minimum tables, Haiku clause in `supportsExplicitThinkingDisable` | `anthropic-model-contract.ts` | N/A (in-memory predicate) | N/A | `anthropic.ts` thinking/effort/sampling builders, `sidecarThinkingOff` (vision, web-search), `rejectsCombinedSampling` guard | +| `CLAUDE_HAIKU_55`, `HAIKU_55_LONG_CONTEXT`, tier rows | `expected-prices.ts` | N/A | N/A | `findExpectedPriceOverlay`, `findContextTier` -> `cost.ts` `applyContextTier` -> `estimateRequestCost`/`estimateAttemptCost` -> usage records and dashboard | +| `CLAUDE_SONNET_55.cacheRead` 0.1 | `expected-prices.ts` | N/A | N/A | anthropic, anthropic-apikey, cursor, devin, devin-cli, Antigravity overlays | +| Snapshot rows | `model-metadata.source.json` | `generate:model-metadata` -> `src/generated/model-metadata.ts` | `resolveMetadataProvider` (bundled providers only) | catalog parsing, model hints, vendor price fallback | +| Seed id, context, max output | `model-seeds.ts` | N/A | N/A | Anthropic OAuth/key entries, Claude CLI roster, native context, effort/modality maps, default-alias claims | + +## Verification + +- `bun run generate:model-metadata` then `tests/codex-integration/model-metadata-sync.test.ts` (exists; reads the generated file). +- Focused: the three new tests plus updated files listed in item 7; `tests/adapters/anthropic/anthropic-sonnet-5-5-contract.test.ts`, `tests/providers/provider-registry-parity.test.ts`, `tests/usage/usage-antigravity-55.test.ts`. +- `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts`, `bun run typecheck`, `bun run structure:check`, `bun run privacy:scan`. +- Live: adapter-built requests to api.anthropic.com with the local Anthropic OAuth credential (token in memory only) for Haiku 5.5 reasoning none, medium, max, required tool choice, temperature 0.2 set by the caller; expect 200 after the change and record the pre-change 400s. Control: Haiku 4.5 reasoning medium. +- Broader validation: `bun run test:changed` (import graph against the resolved dev merge base) in addition to the focused files. Full-suite exception (AGENTS.md resource clause): 15+ concurrent lane worktrees share this machine, so a full local `bun run test` would contend with them; the PR's exact-head CI runs all four test shards plus macOS/Windows legs and is required before merge. `tests/providers/provider-registry-parity.test.ts` imports the generated metadata and seeds directly, so `test:changed` reaches it; it is also run explicitly in the focused set. `tests/codex-integration/model-metadata-sync.test.ts` reads the generated file as data and is run explicitly. + +## Residuals (recorded, not fixed here) + +- Dotted Bedrock ids do not parse as a Claude family (all families). +- Messages-native passthrough forwards caller `thinking`/sampling verbatim. +- One-hour cache writes (0.20 / 1.00) are estimated at the 5-minute price; the estimator has one cache-write bucket. +- Whether cached tokens count toward Anthropic's 100K threshold is unstated; the existing tier mechanism uses inclusive raw input. +- OpenRouter publishes `min_prompt_tokens: 100000`; rule uses `> 100000` like Anthropic and Vercel. +- opencode-go Haiku request wire is unverified; Cursor roster and Bedrock/reseller propagation of the Sonnet cut are unverified. +- Claude CLI output default stays 64K. + +## Bounds + +PR to `dev`, exact-head CI, squash merge. No release. + diff --git a/devlog/_plan/261008_haiku_5_5_catalog/020_audit.md b/devlog/_plan/261008_haiku_5_5_catalog/020_audit.md new file mode 100644 index 00000000000..3d2d71b245f --- /dev/null +++ b/devlog/_plan/261008_haiku_5_5_catalog/020_audit.md @@ -0,0 +1,28 @@ +# 020 Audit (wp1): Haiku 5.5 plan + +Auditor: gpt-6.1-sol subagent 01a119a8-3953-79f1-b689-052895c6470d, read-only, against origin/dev bf9ecf3d79. + +## Round 1: FAIL (4 High blockers) + +Root causes (REVIEW-SYNTHESIS-01): + +- **Identity mismatch between price and tier lookup.** `findExpectedPriceOverlay` canonicalizes Cursor spellings, `findContextTier` matches the raw provider/model id. Blockers 1 and 2 are the two faces of this: regular effort ids (`claude-haiku-5-5-low` ... `-max`) do not canonicalize at all (`src/adapters/cursor/claude-id.ts` has no plain regular-effort suffix parser), so no price resolves; alternate spellings (`claude-haiku-5.5`, `claude-5.5-haiku`) resolve the base price but miss the tier. +- **Snapshot rows are not runtime consumers for unbundled providers, and shared constants fan out.** Venice, Vercel, Kilo, Copilot, Cloudflare, ZenMux and opencode-zen have no generated bundle; their snapshot rows are inert and pricing comes from the Anthropic vendor fallback. Blockers 3 and 4: the plan promised provider-specific Sonnet outcomes that the fallback cannot deliver, and `CLAUDE_SONNET_55` also feeds the Antigravity derived overlays pinned by `tests/usage/usage-antigravity-55.test.ts`. + +| # | Finding | Disposition | +|---|---|---| +| 1 | Cursor regular effort ids price to null | FOLDED: explicit exact overlays for `claude-haiku-5-5` and each emitted regular id `-low/-medium/-high/-xhigh/-max`, plus matching tier rows. No change to the shared Cursor id parser (pre-existing gap for other families recorded as residual after checking whether Sonnet/Opus 5.5 effort ids share it) | +| 2 | Cursor alternate spellings miss the tier | FOLDED: tier rows also for `claude-haiku-5.5` and `claude-5.5-haiku`; acceptance estimates every Cursor spelling at 100,001 tokens | +| 3 | Inert snapshot rows for unbundled providers | FOLDED: snapshot rows kept for parity with #6210 and labelled inert; runtime behaviour per provider is now stated as what actually resolves. Venice gets exact overlays (Haiku 5.5 1.25x tuple and corrected Sonnet 5.5 tuple 2.5/12.5/0.125/3.125, verified-derived from the models.dev listing). opencode-zen, Cloudflare and ZenMux follow the Anthropic vendor fallback, so their Sonnet 5.5 cache read becomes 0.1 with the Anthropic correction (they resell at list); D7 text corrected. Tier rows name the serving provider and its exact id for every fallback provider (vercel, kilo excluded, github-copilot, cloudflare, zenmux, opencode-zen) | +| 4 | Antigravity test pins 0.2 | FOLDED: `tests/usage/usage-antigravity-55.test.ts` in write and focused scope; fixture moves to 0.1 and gains a case with cache-read tokens | +| n1 | `estimateSingleCost` does not exist | FOLDED: acceptance uses `estimateRequestCost` / `estimateAttemptCost` | +| n2 | PLAN-FIELD-CHAIN-01 consumer chains | FOLDED: chain table added to 010 | +| n3 | Cursor publishes Haiku tier and Sonnet 0.10 cache read directly | FOLDED: Cursor overlays `verified` from cursor.com docs; shared `CLAUDE_SONNET_55` change applies to Cursor (no separate constant) | +| n4 | Cursor docs advertise a thinking variant | RESIDUAL: regular-only until the live GetUsableModels roster lists the id, as #6210 did | +| r1 | Architect reflection: alternate spellings also carry effort suffixes (`claude-haiku-5.5-medium`, `claude-5.5-haiku-medium`) | FOLDED: 18 generated Cursor ids (3 spellings x bare + 5 rungs) get overlays and tiers; selection-to-estimation test | +| n5 | `test:changed` selected 0 tests at the base | NOTED: it is meaningful only after edits; focused set remains authoritative | + + +## Round 2: PASS + +Same reviewer re-audited plan hash 7294db4146d2 against bf9ecf3d79: all four blockers closed (108 Cursor request/attempt/cache estimates across both boundary values, 15 selection-to-estimation cases, Venice overlay precedence over fallback, 8 Antigravity cache-read estimates, all in memory). No new High/Critical blockers. Non-blocking: overlays grow from 160 to 184. Architect reflection ALIGNED after the D2/D3/D7 amendments. diff --git a/devlog/_plan/261008_haiku_5_5_catalog/030_done.md b/devlog/_plan/261008_haiku_5_5_catalog/030_done.md new file mode 100644 index 00000000000..31ea706b70e --- /dev/null +++ b/devlog/_plan/261008_haiku_5_5_catalog/030_done.md @@ -0,0 +1,33 @@ +# 030 Done (wp1): Haiku 5.5 catalog, pricing and request contract + +## Outcome + +- Contract (`src/adapters/anthropic-model-contract.ts`): Haiku >= 5.5 uses adaptive thinking, supports an explicit disable, and rejects sampling parameters. Between-tools and forced-choice rules are unchanged, so Haiku 5.5 keeps forced `tool_choice`. +- Pricing (`src/usage/expected-prices.ts`): `CLAUDE_HAIKU_55` 0.1 / 0.5 / 0.01 / 0.125 and a 5x `CONTEXT_TIERS` band above 100,000 raw input tokens (`inclusive: false`) for every provider/id in the plan, including the 18 generated Cursor ids. Kilo stays untiered. `CLAUDE_SONNET_55` cache read 0.2 -> 0.1 (pricing page: 0.05x on Sonnet 5.5). +- Snapshot rows and regenerated metadata, Anthropic seed (1M / 128K), Devin, Kiro, Cursor capability and effort tier, Claude Desktop suggestion. Defaults unchanged. + +## Live evidence (2026-10-08, api.anthropic.com, OAuth, adapter-built bodies, streams read to message_stop) + +| Request | Before (bf9ecf3d79) | After (ae66276dd7) | +|---|---|---| +| Haiku 5.5, reasoning none | 200, thinking omitted (adaptive on by default) | 200, `thinking: disabled`, no effort | +| Haiku 5.5, medium / max | 200 with `enabled` + budget, no thinking block | 200, `adaptive` + effort medium / xhigh / max | +| Haiku 5.5, caller temperature 0.2 | 400 (`temperature` is deprecated for this model) | 200, temperature and top_p stripped | +| Haiku 5.5, tool choice required | 200, `any` | 200, `any` kept, tool_use returned | +| Haiku 5.5, no reasoning field | 200 | 200, no thinking/effort (API default medium) | +| Direct `disabled` + effort max | 400 (effort max not supported when thinking is disabled) | adapter never builds it | +| Haiku 4.5 medium, Sonnet 5.5 none (controls) | 200 | 200, unchanged shapes (`enabled`+budget, `between_tools`) | + +The documented 400 for `enabled` did not reproduce live: the API accepted it but returned no thinking block, so the old shape silently lost reasoning. Raw tables live in scratch (`.tmp/haiku55-probe/`), not committed. + +## Verification + +`bun run generate:model-metadata` output identical to the committed file; 15 focused files 616 pass / 0 fail (three new tests, Sonnet 5.5 contract, output maxima, usage cost, Antigravity 5.5, Cursor, Devin, Kiro, registry parity, metadata sync, layout and file-size ratchet); typecheck, structure:check, privacy:scan exit 0. Full suite delegated to PR CI (resource exception recorded in 010). + +## Residuals + +- Dotted Bedrock ids do not parse as a Claude family (all families); Messages-native passthrough forwards caller `thinking`/sampling verbatim. +- One-hour cache writes are estimated at the 5-minute price; whether cached tokens count toward the 100K threshold is unstated by Anthropic. +- Preemptive rows (Devin, Kiro, Cursor, Copilot, Cloudflare, ZenMux) are inert until those providers list the model; the Cursor row stays regular-only until the live roster shows a thinking variant. +- opencode-go Haiku request wire and Bedrock/reseller propagation of the Sonnet cache-read cut are unverified. + diff --git a/devlog/_plan/261008_haiku_5_5_catalog/030_implementation.md b/devlog/_plan/261008_haiku_5_5_catalog/030_implementation.md new file mode 100644 index 00000000000..395ad4b4304 --- /dev/null +++ b/devlog/_plan/261008_haiku_5_5_catalog/030_implementation.md @@ -0,0 +1,93 @@ +# 030 Implementation and verification + +Implemented the audited 010/020 scope on `codex/haiku-5-5-catalog`, based on `bf9ecf3d79`. +The implementation reuses the existing family predicates, exact price overlays, context-tier +estimator, provider seeds and metadata generator. No pricing mechanism or default changes. + +## Implementation commits + +- `56f777ddfd872ea1fc5636577f0d5ba106049f6c`: Haiku 5.5 request contract and regression coverage. +- `a624064711dbce22ca8b332d777f951d3a1dceb4`: snapshot/generation, 18 generated Cursor exact IDs, + 38 exact context-tier rows, reseller provenance and Sonnet 5.5 cache-read correction. +- `fb36e34e54dedb98c526145f9f3dced10000a319`: Anthropic/Claude CLI, Devin, Kiro and Desktop seeds, + catalog/output/alias coverage. All existing defaults retained. + +The three new tests are registered in both layout inventories. Overlay membership is checked +for unique keys against the actual overlay array rather than a restated count (currently 184). +Structure docs remain 600/600/599 lines; layout.json remains 1,955; Kiro's test stays 2,047 +(cap 2,050). Snapshot rows on unbundled providers remain inert; runtime tiers/overlays name +the serving provider and exact ID, as required by the audit. + +## Verification (2026-10-08) + +Logs are in scratch `.tmp/haiku55/`; none contain OAuth credentials or account identifiers. + +| Command | Result | Log | +| --- | --- | --- | +| `bun run generate:model-metadata` | exit 0; generated from source, never hand-edited | generator stdout | +| focused `bun test` command below | 588 pass / 0 fail, 12 files | focused-final.log | +| `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts` | 28 pass / 0 fail, 3 files | layout-ratchet.log | +| `rg -l 'claude-haiku\|CONTEXT_TIERS\|EXPECTED_PRICE_OVERLAYS\|ANTHROPIC_MODELS' tests -g '*.test.ts'`, then `bun test --isolate ` | 1,820 pass / 0 fail, 46 files | mentioned-test-files.txt, mentioned-isolated-tests.log | +| `bun test tests/routing/fastwire-observability.test.ts tests/usage/usage-anthropic-fast-pricing.test.ts` | 36 pass / 0 fail, 2 additional expected-price consumers | additional-price-tests.log | +| `bun run test:changed` | running; import graph against origin/dev merge base bf9ecf3d79 | test-changed.log | +| `bun run typecheck` | exit 0 | typecheck.log | +| `bun run structure:check` | exit 0, SSOT checks passed | structure.log | +| `bun run privacy:scan` | exit 0, Privacy scan passed | privacy.log | +| `git diff --check` | exit 0 | no output | + +Focused command: + +```sh +bun test tests/adapters/anthropic/anthropic-haiku-5-5-contract.test.ts \ + tests/usage/usage-haiku-5-5-pricing.test.ts tests/providers/haiku-5-5-catalog.test.ts \ + tests/adapters/anthropic/anthropic-sonnet-5-5-contract.test.ts \ + tests/adapters/anthropic/anthropic-output-maxima.test.ts tests/usage/usage-cost.test.ts \ + tests/usage/usage-antigravity-55.test.ts tests/providers/cursor/cursor-catalog.test.ts \ + tests/providers/devin-adapter.test.ts tests/providers/kiro/kiro-adapter.test.ts \ + tests/providers/provider-registry-parity.test.ts tests/codex-integration/model-metadata-sync.test.ts +``` + +Earlier diagnostic runs: + +- Haiku contract before implementation: 11 pass / 11 fail (contract-red.log). + After implementation, the Haiku+Sonnet contract set passed 49/0; an extra direct parsed + `__omit__` test was removed because that sentinel is normalized by the caller and was + outside the audited adapter contract. Omitted reasoning remains covered. +- Initial pricing/catalog: 153 pass / 1 fail: the new catalog test assumed devin-cli was a + registry entry; it is a pricing surface. Corrected to test the canonical Devin registry. +- Initial focused set: 587 pass / 1 fail: a scripted fixture replacement changed Bedrock's + expected 0.22 to 0.12. Restored 0.22; usage-cost passed 102/0, final focused 588/0. +- A direct multi-file Bun invocation without `--isolate` gave 1,750 pass / 70 fail. + The shared `mock.module` OAuth store in vision-backend-union remained active for later + account-pool/Kiro files. Isolated file globals, matching scripts/test.ts, passed 1,820/0. + No production or existing test harness changes were needed. + +## Live API evidence and discrepancy + +Read-only local Anthropic OAuth credentials stayed in memory; no refresh or credential-store +write occurred. `.tmp/haiku55/live-probe.ts` builds requests with createAnthropicAdapter. +No credential, account ID, request text or upstream response text is logged. + +- Haiku 5.5 reasoning none, medium and max: HTTP 200, end_turn. +- Haiku 5.5 required tool choice with medium: HTTP 200, tool_use. +- All four caller inputs requested temperature 0.2 and top_p 0.9; the adapter stripped them. +- Haiku 4.5 medium control: HTTP 200, end_turn. +- Reconstructed legacy non-default sampling shape: HTTP 400, sampling-contract rejection. +- Reconstructed legacy enabled/budget shape: HTTP 200, not the documented/audited 400. + A second probe with budget_tokens 8,192 and max_tokens 16,384 also returned HTTP 200, + model `claude-haiku-5-5`, end_turn (live-enabled-control.log). + +These legacy shapes are reconstructed rejected-field probes using current adapter-built OAuth +framing, not a claim that the old checkout was executed live. The live 200 for enabled is a +current OAuth observation that contradicts the supplied docs. The binding audited adaptive +implementation is unchanged; no supported wire emitted by this patch failed a live probe. +The planned assertion that legacy enabled returns 400 could not be established. + +## Scope and residuals + +No implementation scope expansion. The only extra checks are additional expected-price +consumers and diagnostic scratch probes. A final comment-only change keeps each Cursor model's +provenance directly above its own capability row. The supplied 010/020 planning inputs remain +under parent ownership. No push, PR, release, full local suite, or hosted CI was performed. +The 010 residuals (one-hour cache bucket, raw-input band accounting, preemptive provider wire +availability, regular Cursor variant evidence and dotted Bedrock family parsing) remain. diff --git a/devlog/_plan/261008_train_281_recovery/000_plan.md b/devlog/_plan/261008_train_281_recovery/000_plan.md new file mode 100644 index 00000000000..ca3598b48cb --- /dev/null +++ b/devlog/_plan/261008_train_281_recovery/000_plan.md @@ -0,0 +1,25 @@ +# Recover the 2.81.0 release train + +The previous coordinator stopped before integration and publication. Continue its four remaining deliveries, verify their combined behavior, then publish 2.81.0 and the improved homepage. Existing lane owners retain their branches. + +Loop: satisfy-spec, C4 release; trigger: owner request to finish the previous train and deploy. Scope: #6370 carry, #6743 independent Anthropic pool, #6748 picker fixture cleanup, homepage mobile/GitHub stars. Exclude unrelated PRs, provider policy, live service restarts, destructive cleanup and credential changes. Verification: exact-head PR checks and independent reviews, frozen candidate cross-platform CI, release workflow and registry/asset/Pages readback. Stop only when all delivery receipts pass; unresolved policy or external access is NEEDS_HUMAN, not completion. Evidence lives in this unit and ignored `.tmp/train281-recovery/`. No user token/time budget was imposed; use existing repository GitHub access, bounded polling and no new paid services. Reassess actual failures; do not invent resource exhaustion. + +| Phase | Document | Dependency | Exit | +| --- | --- | --- | --- | +| wp1 | this plan | none | recovered roadmap reflected and independently audited | +| wp2 | 010_integration.md | wp1 | four qualified merges, superseded table, frozen C full CI | +| wp3 | 020_publication.md | wp2 | version pre-move, promotion, npm/GitHub/assets and homepage readback | + +Main owns GitHub mutations and this task checkout. Spend and homepage lanes were resumed in their assigned checkouts. The active Anthropic owner retains its no-local-test instruction, including composition checks; use hosted proof for that lane. No cancellations or rebases; no changes in peer working trees. Per-PR CI remains mandatory. + +Architect proposal: Planck, handle `01a11a91-1357-7af0-a98b-c819216afe72`. Accept REC-281-01 through REC-281-09: preserve owners, separate exact-head gates, keep lane execution restrictions, attribute failures, serialize merges, freeze C, pre-move dev without changing C, promote M with release-line policy, and independently verify published artifacts and Pages. The homepage dependency and deployment receipt repair omissions in the original roadmap. Reflection and audit are recorded below when received. + +The pre-existing roadmap is `devlog/_plan/261008_train_281/` in the previous coordinator checkout. Its implementation contracts remain authoritative for lane work. This recovery unit changes execution ownership and explicitly adds the later homepage requirement; it does not redefine product contracts. Architecture/source-of-truth updates stay in their owning implementation PRs. No new runtime field or enforcement mechanism is added here. + +Baseline observed: dev `aeebf11e5a2c49b5f2edb2e0d8ecafd95e872119`, public release v2.80.0 at `250f17afd8ff44c93c620d87c1f346ef56f64fb4`. Refresh all volatile facts before mutation. + +Architect reflection: ALIGNED after narrowing the release CI event requirement; only Service lifecycle may use a documented exact-M manual equivalent. Main accepted that correction. Homepage recovery moved to fresh thread 01a11a97-e166-7973-a13c-8bac65626d95 after two confirmed provider failures; it must reconcile existing writers before takeover. + +Independent roadmap audit: Socrates `01a11a98-8c20-7a90-85ad-cc29c6f08d81`, VERDICT PASS, no blockers. Read-only policy/workflow review and whitespace check passed; product execution remains pending. + +Recovery update: fresh homepage task also failed with the same provider error. Main reclaims the existing four-file change into its own checkout after confirming terminal task state, no running homepage process and reconciling child writers. Preserve original partial files. This changes execution ownership only, not review/QA/CI scope. diff --git a/devlog/_plan/261008_train_281_recovery/010_integration.md b/devlog/_plan/261008_train_281_recovery/010_integration.md new file mode 100644 index 00000000000..860f7730712 --- /dev/null +++ b/devlog/_plan/261008_train_281_recovery/010_integration.md @@ -0,0 +1,22 @@ +# wp2: reviewed integration and candidate verification + +Depends on wp1. Before: four outstanding deliveries, no final combined candidate. After: all four on dev and one immutable release candidate C passes the full matrix. + +Changes: merge reviewed lane PRs into dev; record receipts here. #6748 changes only `tests/claude-integration/claude-picker-startup.test.ts`. Other lane source/test/doc maps remain in their existing plans and PR diffs. This coordinator does not expand implementation scope. + +1. For each PR, capture head H, base, native stack membership, review threads and objections, independent APPROVE bound to H, security review where applicable, required CI and actual tested merge SHA/event/run/attempt/jobs. Confirm current maintainer identity and policy with `scripts/ci/assert-mergeable-review.sh --maintainer-integration `. Recheck immediately before `gh pr merge --squash --admin --match-head-commit H`. Record maintainer integration separately from independent technical approval. +2. Qualify #6748 first if ready. Independent review PASS at `86c3bb8b84c0109c200fab392c4e304598b90cd9`; changed fixture and all ten tests passed Windows 3/9. Unchanged auth teardown failed Windows 8/9 in run 37744580908 attempt 1, job 113203022254, with EPERM. One failed-job rerun is authorized and requested. A repeat requires investigation; green retry remains unattributed and cannot prove the handle owner. Preserve the blocked-ACL regression proof and both attempts. +3. Homepage must show en/ko at 360x800, 390x844, 768x1024 and 1280x800 (and lane-required sizes), no horizontal overflow, visible GitHub link while scrolled, star count, failure fallback, actual inspected screenshots and independent final committed-head approval. Screenshots belong outside the PR branch; use pr-assets immutable links. Main obtains the owner-requested inherited subagent review before merge. +4. Serialize #6370 and #6743 by readiness. Check their combined tree and run relevant spend/OAuth/routing regressions in hosted CI. Preserve #6743 no-local-test restriction. Require #6370's E1-E7/T1-T9 contract and recovery compatibility proof from its carry; require #6743's complete product/UI scope, not an intermediate foundation approval. +5. After all four land, re-evaluate #6370 and bounded candidates #6532/#3375. Close only with concrete superseding merge and behavior evidence; otherwise KEEP. Record each decision and source SHA. The original sweep had zero confident close candidates, so do not close by title resemblance. +6. Freeze final integrated dev C and tree, verify package.json/desktop Cargo.toml/Cargo.lock/tauri.conf.json version 2.81.0. Include all release-content fixes before this freeze. Run `gh workflow run ci.yml --ref dev -f lane=all`; verify the dispatched run actually resolves C. Wait for all requested jobs, including Linux four shards, Windows nine shards, macOS control and applicable smoke/build/gates. Conditional skips must match workflow conditions. Missing/cancelled/older-head jobs do not pass. No cancellation; no repeated successful checks. A new product fix invalidates C and requires a documented new candidate and necessary validation. + +The GitHub read commands were exercised during recovery: `gh pr view 6748`, `gh api` PR and reviewThreads, `gh release view`, and workflow source inspection. They read the named PR/head and publication. Hosted tests remain pending; no unrun gate is called passing. Docs-only recovery uses semantic audit and `git diff --check`, not a product suite. + +## wp2 recovery entry + +Previous D locked the reviewed roadmap (c63ff6afd9); integration follows that plan. Main takes over only the failed homepage lane: copy `docs-site/src/components/Header.astro`, `Landing.astro`, `styles/custom.css`, and new `data/github-stars.ts` into this checkout. Earlier source children were all terminal; no homepage/astro/agbrowse processes remained. Preserve the original files and baseline/QA artifacts. Existing changes add static build-time memoized stars with timeout/offline fallback, persistent mobile header link and lower-page CTA, mobile text/targets and media dimensions. Retain established design and all locale labels. Main runs real build and viewport/state QA, fixes observed defects, obtains a fresh inherited final-head review, then opens the normal dev PR with immutable screenshot evidence. This is a concrete ownership amendment, not new product scope. + +Recovery QA: main-built production fallback has 569 pages and 79,090 internal links; en/ko home 360x740, 360x800, 390x844, 430x932, 768x1024, 1280x800 plus installation docs at 360/1280 produced no horizontal overflow and persistent visible header GitHub link. Inspected mobile hero, Korean mid-page picker, desktop hero and mobile docs screenshots. Centered the enlarged native docs menu glyph after visual review. A separate build preloads a synthetic 17,092-star response solely for count-state QA; no fixture or generated output ships. + +Count-state QA also passed the same 16 route/viewport combinations, with 17k rendered and no overflow or hidden header link. The synthetic count is labeled in evidence; the normal network-failure build remains link-only. Focused docs checks: 573 pass / 0 fail across 24 files; typecheck, structure:check and privacy:scan exit 0. Full suite deferred to per-PR hosted CI and final coordinator all-lane run because parallel train lanes share this host. No product source outside docs-site changed. diff --git a/devlog/_plan/261008_train_281_recovery/020_publication.md b/devlog/_plan/261008_train_281_recovery/020_publication.md new file mode 100644 index 00000000000..45cfecc204c --- /dev/null +++ b/devlog/_plan/261008_train_281_recovery/020_publication.md @@ -0,0 +1,12 @@ +# wp3: publish the fixed 2.81.0 candidate and homepage + +Depends on wp2. Before: green immutable C on dev, version 2.81.0; public channel 2.80.0. After: published 2.81.0 identified by main SHA M, assets and homepage independently read back. + +1. Capture current tags/npm channels and ensure 2.81.0 is unused and advances stable. Preserve C before any bump. Dispatch `dev-version-bump.yml --ref main -f intended-version=2.81.0 -f mode=pre-move`. Review the generated version-only PR, wait for exact-head applicable CI and merge by policy. Verify dev outranks 2.81.0 (normally 2.82.0). Accept changed=false only with proven precedence. Do not release the bumped dev tree. +2. Audit `origin/main` changes absent from C before the documented promotion method. Create promotion branch from C and merge current main with `git merge -s ours` only after proving no main-only fix is lost. Prove tree equality with C. Open a template-complete main promotion PR, release summary since v2.80.0 and source audit included. Require independent final-head review, applicable security review, and successful checks except the documented enforce-target promotion exception. Main review rules remain distinct from dev's maintainer integration; use the owner's express release authorization and live branch policy, never fabricate approval. Merge with `--merge --match-head-commit` (admin only within that authorized promotion). Record M and prove M tree equals C. +3. Require completed successful push-event Cross-platform CI and Service lifecycle at M, inspecting actual jobs. The C manual all-lane run and M push run serve separate gates. Cross-platform CI requires a successful main push run at M; no other event qualifies. If Service lifecycle is absent, investigate its trigger and explicitly record any accepted exact-M manual-dispatch equivalent, including the deviation from the original push-only train requirement. Do not infer missing jobs passed. +4. Dispatch release.yml from main, `version=2.81.0`, `tag=latest`, `expected-sha=M`, first dry-run=true then dry-run=false after dry-run success. Serialize runs. Record workflow builder/runtime, audit, packaging and registry smoke evidence. If publication was acknowledged but later steps fail, preserve acknowledgment and resume with resume-after-npm-publish=true at the same M; do not blindly republish. +5. Verify npm version/latest/gitHead=M, tag v2.81.0 -> M, non-draft/non-prerelease GitHub release, expected standalone/desktop assets, digests, checksums/signatures and latest.json version/platform URLs. Fetch published package into isolated scratch for version/content/safe CLI smoke where permitted, never install over or restart the user's proxy. Record release provenance and artifact identity. Prior stable 2.80.0 remains the rollback artifact; an actual rollback/channel mutation requires a separate decision if needed. +6. Observe automatic deploy-docs.yml at main M. If absent, dispatch from main after confirming no duplicate active run. Record deployed source SHA, completed build/deploy and environment URL. Inspect public en/ko homepage mobile/desktop, scrolled GitHub link, star display/fallback and absence of overflow. Persist screenshots outside tracked source. Publication is incomplete until Pages and artifact readbacks pass. + +Record failures as observed, with attempt identities and unresolved cause. No runtime upgrade, credential migration, personal setting mutation or destructive cleanup is part of this release. Signing gates and known artifact limitations cannot be waived by a green headline. Final report includes release URL/version, M, CI/release/Pages runs, completed scope and material caveats. diff --git a/docs-site/src/components/Header.astro b/docs-site/src/components/Header.astro index 5bc9fc74350..1e6c9e2d582 100644 --- a/docs-site/src/components/Header.astro +++ b/docs-site/src/components/Header.astro @@ -6,7 +6,7 @@ import { Icon } from '@astrojs/starlight/components'; import Search from '@astrojs/starlight/components/Search.astro'; import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; -import SocialIcons from '@astrojs/starlight/components/SocialIcons.astro'; +import { GITHUB_URL, getStarCount, formatStarCount } from '../data/github-stars'; const { locale } = Astro.locals.starlightRoute; // Splash pages have no Starlight sidebar/hamburger, so the pill carries its own @@ -51,11 +51,23 @@ const fr = { 'Main': 'Navigation principale', 'Select language': 'Choisir la langue', 'Menu': 'Menu', + 'GitHub': 'GitHub', + 'GitHub, {count} stars': 'GitHub, {count} étoiles', } as const; const t = (en: K, ko: string, zh: string, ru: string, ja: string, zhTW: string, tr: string): string => locale === 'fr' ? fr[en] : locale === 'ko' ? ko : locale === 'zh-cn' ? zh : locale === 'zh-tw' ? zhTW : locale === 'ru' ? ru : locale === 'ja' ? ja : locale === 'tr' ? tr : en; +const starCount = await getStarCount(); +const starLocale = locale ?? 'en'; +const githubLabel = t('GitHub', 'GitHub', 'GitHub', 'GitHub', 'GitHub', 'GitHub', 'GitHub'); +const starLabel = t( + 'GitHub, {count} stars', 'GitHub, 별 {count}개', 'GitHub,{count} 个星标', + 'GitHub, {count} звёзд', 'GitHub、{count} スター', 'GitHub,{count} 個星標', 'GitHub, {count} yıldız', +); +const githubAccessibleName = starCount === null ? githubLabel + : starLabel.replace('{count}', starCount.toLocaleString(starLocale)); + type LanguageOption = { code: 'root' | 'fr' | 'ko' | 'zh-cn' | 'zh-tw' | 'ru' | 'ja' | 'tr'; label: string }; const languages: LanguageOption[] = [ { code: 'root', label: 'English' }, @@ -174,10 +186,16 @@ const contributing: NavLink = {
+ + + {starCount !== null && {formatStarCount(starCount)}} +
-
{/* [Decision Log] @@ -292,16 +310,69 @@ const contributing: NavLink = { min-width: 0; } - /* below the sidebar breakpoint the fixed hamburger overlays the pill's right - end — reserve room so search and the menu button never collide */ + .title-wrapper :global(.site-title) { min-height: 44px; } + :global(.sl-skip-link:focus) { min-height: 44px; } + + .search-wrapper, + .github-link, + .right-group, + .lp-menu { + flex-shrink: 0; + } + .github-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + min-width: 44px; + min-height: 44px; + padding-inline: 0.375rem; + border-radius: var(--ocx-radius-pill, 999px); + color: var(--sl-color-text-accent); + font-size: 0.75rem; + text-decoration: none; + white-space: nowrap; + } + .github-link :global(svg) { width: 1rem; height: 1rem; flex-shrink: 0; } + .github-link [data-gh-stars] { + font-variant-numeric: tabular-nums; + min-width: 5ch; + width: 5ch; + text-align: center; + } + .github-link:hover { color: var(--sl-color-white); } + .github-link:focus-visible, + .lp-menu-btn:focus-visible { + outline: 2px solid var(--sl-color-accent); + outline-offset: 2px; + } + + /* Starlight's own docs hamburger is fixed outside this component. Reserve + its enlarged hit area while keeping every header action visible. */ @media (max-width: 49.99rem) { + .header { gap: 0.25rem; padding-inline: 0.5rem; } + .title-wrapper { flex: 1; } + .title-wrapper :global(.site-title) { gap: 0.375rem; font-size: 1rem; } + .title-wrapper :global(.site-title img) { width: 28px; height: 28px; flex-shrink: 0; } + .search-wrapper :global(button[data-open-modal]) { + min-width: 44px; + min-height: 44px; + justify-content: center; + } + .lp-menu-btn { width: 44px; height: 44px; } :global([data-has-sidebar]) .header { - padding-inline-end: calc(var(--sl-menu-button-size) + 0.75rem); + padding-inline-end: calc(44px + 0.5rem); + } + :global(starlight-menu-button button) { + width: 44px; + height: 44px; + align-items: center; + justify-content: center; + top: calc((var(--sl-nav-height) - 44px) / 2); } } - .right-group, - .social-icons { + .right-group { gap: 1rem; align-items: center; } @@ -309,7 +380,7 @@ const contributing: NavLink = { gap: 0.125rem; align-items: center; } - .social-icons::after { + .right-group::before { content: ''; height: 2rem; border-inline-end: 1px solid var(--sl-color-gray-5); @@ -324,7 +395,7 @@ const contributing: NavLink = { align-items: center; justify-content: center; gap: 0.375rem; - min-height: 2.25rem; + min-height: 44px; padding: 0.375rem 0.75rem; border: 0; border-radius: var(--ocx-radius-pill, 999px); @@ -532,6 +603,8 @@ const contributing: NavLink = { justify-content: center; width: 2.25rem; height: 2.25rem; + min-width: 44px; + min-height: 44px; border: 0; border-radius: var(--ocx-radius-pill, 999px); background: transparent; @@ -580,7 +653,9 @@ const contributing: NavLink = { .lp-menu-group ul { list-style: none; margin: 0; padding: 0; } .lp-menu-group a, .lp-menu-contrib { - display: block; + min-height: 44px; + display: flex; + align-items: center; padding: 0.4rem 0.5rem; border-radius: var(--ocx-radius-sm, 8px); color: var(--sl-color-gray-2); diff --git a/docs-site/src/components/Landing.astro b/docs-site/src/components/Landing.astro index e917f7cd0d0..4048ae628be 100644 --- a/docs-site/src/components/Landing.astro +++ b/docs-site/src/components/Landing.astro @@ -9,6 +9,7 @@ import { siApple, siDeepseek, siGoogle, + siGithub, siLinux, siMoonshotai, siOllama, @@ -20,6 +21,7 @@ import picker from '../assets/codex-app-picker.png'; import heroFieldLight from '../assets/hero-route-light.png'; import heroFieldDark from '../assets/hero-route-dark.png'; import SiteJsonLd from './SiteJsonLd.astro'; +import { GITHUB_URL, getStarCount, formatStarCount } from '../data/github-stars'; interface Props { locale?: 'fr' | 'ko' | 'zh-cn' | 'zh-tw' | 'ru' | 'ja' | 'tr'; @@ -111,11 +113,20 @@ const fr = { 'Desktop app guide': 'Guide de l’app de bureau', 'Desktop App': 'App de bureau', 'Latest release': 'Dernière version', + 'Star on GitHub': 'Ajouter une étoile sur GitHub', + 'Star on GitHub, {count} stars': 'Ajouter une étoile sur GitHub, {count} étoiles', } as const; const t = (en: K, ko: string, zh: string, ru: string, ja: string, zhTW: string, tr: string): string => locale === 'fr' ? fr[en] : locale === 'ko' ? ko : locale === 'zh-cn' ? zh : locale === 'zh-tw' ? zhTW : locale === 'ru' ? ru : locale === 'ja' ? ja : locale === 'tr' ? tr : en; +const starCount = await getStarCount(); +const starLocale = locale || 'en'; +const githubCta = t('Star on GitHub', 'GitHub에서 별 주기', '在 GitHub 上加星', 'Поставить звезду на GitHub', 'GitHub でスターを付ける', '在 GitHub 上加星', 'GitHub’da yıldız verin'); +const starLabel = t('Star on GitHub, {count} stars', 'GitHub에서 별 주기, 별 {count}개', '在 GitHub 上加星,{count} 个星标', 'Поставить звезду на GitHub, {count} звёзд', 'GitHub でスターを付ける、{count} スター', '在 GitHub 上加星,{count} 個星標', 'GitHub’da yıldız verin, {count} yıldız'); +const githubAccessibleName = starCount === null ? githubCta + : starLabel.replace('{count}', new Intl.NumberFormat(starLocale).format(starCount)); + const icons: Record = { // Lucide outline paths (24x24, stroke) plug: 'M12 22v-5M9 8V2M15 8V2M6 8h12l-1 6a5 5 0 0 1-10 0Z', @@ -358,7 +369,7 @@ const docsMap = [

{t('Watch Codex run on a routed model.', '라우팅된 모델 위의 Codex를 그대로.', '看 Codex 跑在路由模型上。', 'Смотрите, как Codex работает на маршрутизированной модели.', 'ルーティングされたモデルで動く Codex を見る。', '看 Codex 跑在路由模型上。', 'Codex’in yönlendirilen bir model üzerinde çalışmasını izleyin.')}

- + {t('Codex

{t('One task, streamed end to end through opencodex.', '작업 하나가 opencodex를 관통해 끝까지 스트리밍됩니다.', '一个任务,经 opencodex 全程流式完成。', 'Одна задача — стриминг через opencodex от начала до конца.', '1 つのタスクが opencodex を経てエンドツーエンドでストリーミングされます。', '一個任務,經 opencodex 全程流式完成。', 'Tek bir görev, opencodex üzerinden uçtan uca canlı akışla tamamlanır.')}

@@ -396,7 +407,7 @@ const docsMap = [

Claude Code

{t('ocx claude launches Claude Code against the same port — every routed model in the /model picker, auto-context up to 1M, roster sub-agents, and your claude.ai login stays active.', 'ocx claude가 같은 포트로 Claude Code를 띄웁니다 — 라우팅된 모든 모델이 /model 선택기에 뜨고, 최대 1M auto-context, 로스터 서브에이전트, claude.ai 로그인은 그대로 유지됩니다.', 'ocx claude 让 Claude Code 连接同一端口 —— 所有路由模型出现在 /model 选择器中,auto-context 最高 1M,roster 子代理,claude.ai 登录保持不变。', 'ocx claude запускает Claude Code на том же порту — все маршрутизированные модели в селекторе /model, auto-context до 1M, подагенты из ростера, а ваш вход в claude.ai остаётся активным.', 'ocx claude が同じポートで Claude Code を起動します — ルーティングされたすべてのモデルが /model ピッカーに表示され、最大 1M の auto-context、ロスターのサブエージェント、claude.ai ログインはそのまま維持されます。', 'ocx claude 讓 Claude Code 連線同一連接埠 —— 所有路由模型出現在 /model 選擇器中,auto-context 最高 1M,roster 子代理,claude.ai 登入保持不變。', 'ocx claude, Claude Code’u aynı port üzerinden başlatır — /model seçicisindeki tüm yönlendirilmiş modeller, 1M’a kadar otomatik bağlam, alt ajanlar ve claude.ai oturumunuz aktif kalır.')}

- {t('Claude + {t('Claude

{t('Search & vision sidecars', '검색 & 비전 사이드카', '搜索与视觉边车', 'Сайдкары поиска и зрения', '検索 & ビジョンサイドカー', '搜尋與視覺邊車', 'Arama & görme sidecar’ları')}

@@ -413,6 +424,15 @@ const docsMap = [
+ +

{t('Explore the docs', '문서 살펴보기', '浏览文档', 'Изучите документацию', 'ドキュメントを探る', '瀏覽文件', 'Dokümantasyonu Keşfedin')}

diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index a7c1dd547d0..888b80644a2 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -6,7 +6,7 @@ description: Develop opencodex — setup, layout, conventions, and how to add a ## Setup Source development requires the `bun` CLI on your `PATH`. The published npm package bundles its own -Bun runtime for users, but this checkout's scripts run through your local Bun installation. +Bun runtime for users; package scripts may resolve Bun through that bundled dependency. ```bash git clone https://github.com/lidge-jun/opencodex.git @@ -45,6 +45,10 @@ bun run privacy:scan # credential/privacy scan used by CI bun run prepare:package # refresh package launchers/assets ``` +The suite uses Bun 1.4.0 (`package.json`'s `testRunnerBun`) while the shipped runtime is 1.4.2. +`scripts/test.ts` finds a 1.4.0 binary on PATH or in `~/.bun/bin`, or uses `OCX_TEST_RUNNER_BUN`. +Install that version locally if it is missing; the resolver never downloads it. + Run `bun run test` by default. If a full run is disproportionately expensive for the task size, available machine resources, or concurrent worktrees, you must at least run focused regression tests that exercise the changed behavior, such as `bun test tests//.test.ts`. Explain the diff --git a/docs-site/src/content/docs/fr/contributing.md b/docs-site/src/content/docs/fr/contributing.md index b27a50689f2..279b582c754 100644 --- a/docs-site/src/content/docs/fr/contributing.md +++ b/docs-site/src/content/docs/fr/contributing.md @@ -6,7 +6,7 @@ description: Développez opencodex — installation, structure, conventions et a ## Configuration Le développement depuis les sources exige la CLI `bun` dans votre `PATH`. Le paquet npm publié fournit son propre -runtime Bun aux utilisateurs, mais les scripts de ce dépôt utilisent votre installation locale de Bun. +runtime Bun aux utilisateurs ; les scripts du paquet peuvent résoudre Bun via cette dépendance incluse. ```bash git clone https://github.com/lidge-jun/opencodex.git diff --git a/docs-site/src/content/docs/fr/getting-started/quickstart.md b/docs-site/src/content/docs/fr/getting-started/quickstart.md index 8d39e2502ab..cad99b993ba 100644 --- a/docs-site/src/content/docs/fr/getting-started/quickstart.md +++ b/docs-site/src/content/docs/fr/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` vous accompagne dans les étapes suivantes : -1. **Choix d’un fournisseur** — sélectionnez l’un des 102 préréglages intégrés au registre, ou `custom` pour saisir une +1. **Choix d’un fournisseur** — sélectionnez l’un des 103 préréglages intégrés au registre, ou `custom` pour saisir une URL de base et un adaptateur. 2. **Clé API** — collez une clé ou référencez une variable d’environnement telle que `${ANTHROPIC_API_KEY}`. 3. **Modèle par défaut** — pour les fournisseurs clés, locaux et personnalisés, acceptez le préréglage ou saisissez un identifiant de modèle. diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index 0de2c391c4f..58e92f8e0ad 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -752,7 +752,7 @@ par défaut par un contenu minimal (`blockedSkills: ["claude-api"]`). ``, et non l'argument `model` de l'outil Agent. Vérifiez que la directive désigne la route voulue. Utilisez `"haiku"` comme valeur de remplacement pour le modèle. -Dans `config.json`, `claudeCode.stabilizePromptCache: true` déplace les notices Claude reconnues en fin des instructions système vers un dernier message utilisateur sur les routes traduites. La valeur par défaut est `false`. Activez cette option seulement si ce changement de rôle convient à vos clients. Les exemples dans des blocs de code et le texte non reconnu sont conservés ; le transfert Anthropic natif reste inchangé. Sans métadonnées, la clé de cache suit les instructions stabilisées. Cette option ne crée pas une identité de conversation et ne garantit aucun succès du cache amont. +Dans `config.json`, `claudeCode.stabilizePromptCache: true` retire les notices Claude reconnues à la fin des instructions système sur les routes traduites. Les pieds de page reconnus de la forme `N tokens left` sont supprimés, même lorsqu’ils sont répétés. Les rappels TaskCreate sont toujours déplacés vers un dernier message utilisateur ; si seuls des pieds de page de tokens ont été retirés, aucun message d’entrée n’est ajouté. La valeur par défaut est `false`. Activez cette option seulement si ce changement de rôle convient à vos clients. Les exemples dans des blocs de code et le texte non reconnu sont conservés ; le transfert Anthropic natif reste inchangé. Sans métadonnées, la clé de cache suit les instructions stabilisées. Cette option ne crée pas une identité de conversation et ne garantit aucun succès du cache amont. Sur toutes les routes Chat traduites, les rappels de l’historique conservent leur position dans la conversation, après les résultats d’outils encore attendus. L’ajout d’un rappel ne réécrit donc pas le prompt système initial, et une instruction placée au milieu de la conversation n’arrive plus avant les tours qu’elle était censée suivre. Le rôle porté par cet emplacement se décide séparément : un rappel part en `system`, sauf si le fournisseur enregistre `foldDeveloperRoleToSystem: false`, ce qui indique que le service en amont accepte le rôle `developer` et le transmet à la même position. Un service qui ne l’accepte pas répond `400 role 'developer' is not allowed` et le tour ne démarre pas, d’où le repli d’une destination non enregistrée. Ce comportement s’applique avec ou sans `stabilizePromptCache` ; le transfert Anthropic natif reste inchangé. La réutilisation du cache exige toujours une identité de session stable et un cache disponible en amont. Les changements des instructions ou outils antérieurs et la compaction de la conversation peuvent aussi affecter les succès du cache ; préserver l’ordre des rappels ne suffit pas à garantir sa réutilisation. diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index 6be2373dfdf..cd51255add8 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Intégrations -description: Connectez opencodex à OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo et Factory Droid depuis le tableau de bord — un commutateur par client, avec une sauvegarde avant chaque écriture. +description: Connectez opencodex à OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code et Factory Droid depuis le tableau de bord — un commutateur par client, avec une sauvegarde avant chaque écriture. --- L'onglet **Connexion** écrit le bloc fournisseur d'opencodex dans le fichier de configuration du client, -puis peut le retirer. Dix-sept clients fonctionnent ainsi, chacun avec son propre commutateur : +puis peut le retirer. Dix-huit clients fonctionnent ainsi, chacun avec son propre commutateur : | Client | Fichier de configuration | Format | Prise d'effet de la modification | Identifiant | |---|---|---|---|---| @@ -24,6 +24,7 @@ puis peut le retirer. Dix-sept clients fonctionnent ainsi, chacun avec son propr | omo | `~/.omo/agent/models.json` | JSON | nouvelles sessions | espace réservé de bouclage | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | après arrêt et redémarrage | bouclage uniquement | | Kilo | premier fichier existant parmi `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` ou `config.json` sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire ; `kilo.jsonc` est créé si aucun n'existe) | JSONC | nouvelles sessions | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (sous Windows avec le répertoire personnel par défaut : utiliser `HOME ?? USERPROFILE` uniquement si la valeur n’est ni vide ni composée uniquement d’espaces et désigne un chemin absolu Windows, sinon `homedir()` ; un répertoire personnel fourni séparément est conservé ; ajouter `.commandcode\providers.json`) | JSON | au prochain lancement de Command Code | aucun — boucle locale sans clé (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` sous Windows) | JSON | dès la détection du fichier | boucle locale sans clé | Les modèles GJC dotés d'une échelle d'effort de raisonnement prise en charge exportent `reasoning: true`, `thinking.levels` et `compat.supportsReasoningEffort`, afin que GJC propose le choix de l'effort. Les modèles Codex natifs reçoivent leur échelle standard même si le catalogue l'omet. Ces champs sont absents sans échelle connue ; `none` n'envoie pas d'effort et `ultra` devient `max` sur le réseau. Actualisez l'intégration pour mettre à jour ces options. diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 7d168123544..8a928f1b299 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -295,7 +295,7 @@ existante n'est pas concernée. ## 3. Catalogue des clés API -opencodex fournit 102 préréglages intégrés : 84 à clé, 14 OAuth, trois locaux et un préréglage par défaut de +opencodex fournit 103 préréglages intégrés : 84 à clé, 15 OAuth, trois locaux et un préréglage par défaut de transfert ChatGPT. Dans le tableau de bord, le sélecteur **Ajouter un fournisseur** ouvre le tableau de bord du fournisseur à clé, valide la clé et l'enregistre ; la validation dépend du fournisseur. Parmi les entrées notables : diff --git a/docs-site/src/content/docs/fr/guides/web-dashboard.md b/docs-site/src/content/docs/fr/guides/web-dashboard.md index 5aa846f364e..56fbf660e94 100644 --- a/docs-site/src/content/docs/fr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/fr/guides/web-dashboard.md @@ -157,6 +157,28 @@ OpenCodex demandent à Codex de transmettre les remplacements à `spawn_agent` ; Le plan de gestion du tableau de bord est séparé du trafic modèle direct client→hub. **Connexion → Clés API** affiche les rotations en attente, montre le secret de remplacement une seule fois et exige une validation ou une annulation explicite. La déconnexion du navigateur n'invalide que la session courante. L'usage connecté vient du hub filtré par `apiKeyId`; l'usage déconnecté est local, sans réplication. +### Afficher une clé API enregistrée + +L'affichage de la valeur complète d'une clé enregistrée exige une session du tableau de bord +créée par un appairage explicite ou une identité Tailscale de confiance. Une session locale +créée automatiquement peut lire la liste masquée, mais ne peut pas révéler les valeurs enregistrées. +Le jeton administrateur ne peut pas non plus appeler cette opération réservée aux sessions. +Utilisez le processus d'appairage Remote Hub existant ou une connexion Tailscale de confiance, si disponible. + +Le formulaire d'appairage local est proposé uniquement sur un tableau de bord autonome de même origine, +ouvert en HTTP à l'adresse IP littérale `127.0.0.1` ou `[::1]`. L'origine doit correspondre à l'adresse +IP de bouclage configurée pour l'écoute du serveur. Depuis `localhost` ou un alias, vérifiez cette adresse : +rouvrez `http://127.0.0.1:` pour une écoute sur `127.0.0.1`, ou `http://[::1]:` pour +une écoute sur `::1`, puis effectuez l'appairage. Un serveur dont le hostname est `localhost` n'a +aucune origine admissible pour l'appairage local. Le tableau de bord ne peut pas déduire l'adresse +d'écoute d'une URL avec un alias : il affiche donc le refus général sans suggérer une autre URL. + +Après l'appairage, cliquez à nouveau sur la clé pour demander sa valeur. L'appairage seul ne révèle aucune clé. +Les valeurs affichées et l'état de copie sont effacés lorsque la session partagée est supprimée ou remplacée, +au début d'un appairage, en quittant ce panneau, en masquant l'onglet ou la fenêtre de l'application, +ou en changeant de serveur. Cela inclut les valeurs des clés créées ou remplacées, affichées une seule fois. +Si la session autorisée expire ou est révoquée, effectuez à nouveau l'appairage. + La garantie de remplacement lors d'une création de sous-agent s'applique au texte de consignes v2 **intégré**. Un `injectionPrompt` personnalisé remplace entièrement ce texte et doit contenir les espaces réservés `{{model}}` et `{{effort}}` — et facultativement `{{roster}}` — sans quoi ces valeurs n'apparaîtront pas dans diff --git a/docs-site/src/content/docs/fr/reference/cli/agents.md b/docs-site/src/content/docs/fr/reference/cli/agents.md index c7fb629c9a5..0fdec30be67 100644 --- a/docs-site/src/content/docs/fr/reference/cli/agents.md +++ b/docs-site/src/content/docs/fr/reference/cli/agents.md @@ -179,7 +179,7 @@ Gérez et appliquez la clôture du modèle Grok Build. ## Exportation de la configuration client -### `ocx export --client ` +### `ocx export --client ` Imprimez une configuration client connectée au proxy en cours d'exécution. La commande sérialise le bloc fournisseur `opencodex` — URL de base, liste de modèles et référence d’identifiant du client @@ -190,7 +190,7 @@ les modèles Codex peuvent actuellement voir. | Option | Actions | | --- | --- | -| `--client ` | Requis. Sélectionne le dialecte de configuration client. | +| `--client ` | Requis. Sélectionne le dialecte de configuration client. | | `--json` | Imprimez le document généré en tant que JSON sur la sortie standard pour les scripts. Il s'agit de JSON même lorsque le format natif du client sélectionné est YAML, TOML ou JSON5. | | `--out ` | Écrivez le format de configuration natif du client dans ``. Refuse de remplacer un fichier existant. | | `--force` | Autoriser `--out` à remplacer un fichier existant. | @@ -224,6 +224,7 @@ propres valeurs par défaut à ces lignes. | `raycast` | `~/.config/raycast/ai/providers.yaml`, sur macOS comme sur Windows (Raycast n'honore pas `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | aucun — bouclage uniquement, aucune entrée `api_keys` n'est écrite | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, puis `SENPI_CODING_AGENT_DIR`, puis `PI_CODING_AGENT_DIR` l'emportent dans cet ordre une fois définis ; une valeur relative est refusée) | `omo-models.json` | aucun — espace réservé de bouclage | | `kilo` | premier fichier existant parmi `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` ou `config.json` sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire) ; utilise `kilo.jsonc` si aucun n'existe | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (sous Windows avec le répertoire personnel par défaut : utiliser `HOME ?? USERPROFILE` uniquement si la valeur n’est ni vide ni composée uniquement d’espaces et désigne un chemin absolu Windows, sinon `homedir()` ; un répertoire personnel fourni séparément est conservé ; ajouter `.commandcode\providers.json`) | `providers.json` | boucle locale uniquement ; `apiKey: false` ; aucune variable d’environnement | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | boucle locale uniquement ; aucune variable d’environnement | L'exportation Raycast est un document `providers.yaml` autonome contenant un seul élément `id: opencodex` diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index 32adbfb434b..af0b2a50512 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -253,6 +253,65 @@ Laissez cette option désactivée, sauf si vous comprenez les risques liés aux préférez le changement manuel avec `ocx account use anthropic `. ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2` (« Anthropic · Pool 2 ») est un second fournisseur OAuth Anthropic intégré, doté de son propre pool de comptes. Il utilise la même implémentation Anthropic que `anthropic` : même flux de connexion, même format de requête, mêmes Messages natifs et pont Responses, mêmes métadonnées de modèles. Seuls les comptes et le pool qui les entoure sont séparés. Le préfixe du modèle choisit le pool : `anthropic/claude-sonnet-5` utilise le pool principal et `anthropic2/claude-sonnet-5` le Pool 2. + +Le Pool 2 reste inactif tant que vous ne l'ajoutez pas. Connectez-vous avec `ocx login anthropic2` ou ajoutez **Anthropic · Pool 2** dans la page Fournisseurs du tableau de bord. Une première connexion réussie crée `providers.anthropic2` avec le marqueur `"anthropicOAuthInstance": "anthropic2"`. Le Pool 2 ne devient jamais le fournisseur par défaut : les noms de modèles `claude-*` sans préfixe, le modèle par défaut et le transfert d'appelant de Claude Code continuent de se résoudre vers `anthropic`. Une entrée `anthropic2` non marquée que vous avez créée vous-même garde sa signification personnalisée ; la connexion refuse la collision de noms et ne réécrit ni l'entrée ni vos identifiants. + +Le Pool 2 lit ses réglages de pool dans sa propre entrée de fournisseur. Les clés, valeurs par défaut et comportements sont ceux de `anthropicAccountPool` au niveau racine, décrit ci-dessus, et rien n'est hérité du pool principal. `providers.anthropic.anthropicAccountPool`, ou ce champ sur tout autre fournisseur, est rejeté. + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +Les deux pools sont isolés dans opencodex : + +- **Identifiants :** les comptes du Pool 2 sont stockés sous leur propre clé `anthropic2` dans le magasin d'identifiants protégé. Une connexion dont le jeton ou le compte Anthropic vérifié est déjà stocké dans l'autre pool est rejetée. +- **Réglages et état d'exécution :** sélection, affinité de session, temporisations, pauses, routes de modèles et seuils par compte appartiennent à un seul pool. `fallback: true` sur une route n'élargit qu'au sein du même pool. +- **Quota et utilisation :** sondes d'utilisation, caches de quota et attribution de l'utilisation sont enregistrés par pool ; un même ID de compte présent dans les deux pools a donc deux enregistrements distincts. +- **Octrois de réinitialisation :** le Pool 2 tient son propre journal (`anthropic2-reset-grant-ledger.json`) à côté du journal inchangé du pool principal. +- **Reprise :** une requête directe `anthropic2/` ne bascule jamais vers le pool principal, et une limite de débit ou un refus du Pool 2 ne met jamais en temporisation un compte du pool principal. Un combo explicite qui nomme les deux pools conserve les cibles déclarées. + +Cette séparation est une frontière de routage interne à opencodex. Elle ne change ni la façon dont Anthropic traite vos comptes ni les risques liés aux règles de comptes décrits ci-dessus. + +Les comptes du Pool 2 s'ajoutent uniquement par OAuth dans le navigateur. Contrairement à `anthropic`, le Pool 2 n'importe, n'adopte et ne réécrit jamais de jeton de la CLI Claude Code ; cette différence est voulue. Le Pool 2 démarre vide, et une requête Pool 2 sans compte Pool 2 utilisable échoue avec une erreur d'authentification au lieu d'emprunter un identifiant du pool principal ou de Claude Code. + +Les commandes de compte et les API de gestion désignent le pool par son nom : `ocx account pool anthropic2 …`, `ocx account auto-switch anthropic2 …`, `ocx account routes anthropic2 …` et `ocx account anthropic-reset-grants --provider anthropic2`. Les points de terminaison des réglages de pool et des octrois de réinitialisation acceptent `provider: "anthropic2"` ; sans ce champ, le pool principal reste utilisé. + +#### Choix du pool des assistants (`anthropicInstance`) + +Les assistants de recherche web et de vision acceptent un champ facultatif `anthropicInstance`, dans les réglages globaux `webSearchSidecar` et `visionSidecar` ainsi que dans les surcharges Claude Code `claudeCode.webSearchSidecar` et `claudeCode.visionSidecar`. Le tableau de bord l'affiche sous forme de menu **Pool de comptes** lorsque le backend de l'assistant est Anthropic. + +| Valeur | Comportement | +| --- | --- | +| non défini (par défaut) | Suit le pool de la requête en cours : une requête `anthropic2/` utilise le Pool 2 et une requête `anthropic/` le pool principal. Une requête d'un autre fournisseur conserve la découverte existante des assistants, qui ne choisit jamais le Pool 2. | +| `"anthropic"` | Toujours le pool principal. | +| `"anthropic2"` | Toujours le Pool 2. | + +Le champ ne s'applique que si le backend de l'assistant se résout en Anthropic. Le définir avec un autre backend est une erreur de validation ; comme la recherche web utilise OpenAI par défaut, définissez aussi `"backend": "anthropic"`. Un modèle d'assistant qualifié par l'autre pool, par exemple `anthropic/claude-sonnet-5` avec `"anthropicInstance": "anthropic2"`, est également rejeté. Si le pool choisi n'a aucun compte utilisable, l'assistant échoue avant tout envoi ; il ne passe pas à l'autre pool. La requête principale se poursuit alors sans cet assistant. Un choix non défini est enregistré comme absent, jamais comme `"anthropic"`. + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[Retour à une version antérieure] +Les versions sans Pool 2 ne comprennent pas l'entrée `anthropic2`. Avant d'installer une version plus ancienne, arrêtez le proxy, sauvegardez `config.json` et `auth.json`, puis retirez `providers.anthropic2` de `config.json`. Le Pool 2 ne déplace ni ne réécrit jamais les identifiants du pool principal. Un retour en arrière sur place avec un Pool 2 actif n'est pas pris en charge. +::: + ### Formes d'enregistrement gérées Les entrées `apiKeys[]` contiennent les chaînes `id`, `name`, la valeur `key` générée et la date ISO `createdAt`. diff --git a/docs-site/src/content/docs/fr/reference/configuration/server.md b/docs-site/src/content/docs/fr/reference/configuration/server.md index d82a29a23ca..14bbc651b04 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/server.md +++ b/docs-site/src/content/docs/fr/reference/configuration/server.md @@ -286,3 +286,35 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available. The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. + +## Continuité de l’historique des pools + +Le plafond d’un pool s’applique au fournisseur canonique choisi par le routage, indépendamment du libellé de compte dans les journaux. Pour chaque fournisseur `P` actuellement configuré, OpenCodex associe automatiquement `h(pool, P)`, son alias de pool salé propre à cette installation, à `P` lors de la lecture des soldes. Cela inclut les soldes historiques conservés sous ce même alias. Les plafonds indépendants ne nécessitent ni association manuelle du fournisseur à lui-même ni calcul avec le sel. + +Les autres libellés historiques, y compris ceux contenant un numéro de compte, restent sans propriétaire confirmé. Chaque solde d’origine est compté une seule fois dans son groupe associé ; tout solde positif encore non associé est ajouté, par prudence, une fois à chaque pool candidat. Les montants réglés, réservés et non résolus comptent tous. Un historique inconnu peut donc limiter un pool inutilisé. Les noms actuels et l’ordre des comptes ne prouvent pas à quel fournisseur cet historique appartient. Les plafonds de racine et d’identité restent indépendants. + +Pour un historique dont vous avez vérifié le propriétaire, ajoutez une entrée dans `spendPoolAliases`, au premier niveau de `config.json`, en dehors de `spend`. La clé est l’alias salé exact du pool dans le journal de cette installation, composé de 32 caractères hexadécimaux minuscules ; la valeur est l’ID exact d’un fournisseur vérifié et actuellement configuré, sans espaces au début ni à la fin. Gardez le journal, le sel et les éléments justificatifs privés. L’alias propre d’un fournisseur configuré ne peut pas être attribué à un autre fournisseur. La validation est répétée lorsque les fournisseurs changent et lit le sel existant sans en créer. + +Les associations automatiques et explicites s’appliquent uniquement à la lecture : elles ne déplacent pas les soldes et n’inscrivent aucun lien d’identité dans le journal. Supprimer une association explicite rend l’historique inconnu, sauf si son alias est celui d’un fournisseur actuellement configuré. Une association invalide est rejetée à l’écriture ; une modification manuelle invalide conserve les plafonds mais bloque l’admission du pool. + +Un historique inconnu et inactif expire seulement quand sa dernière activité est strictement antérieure à la limite de `spend.retentionDays`. Un manque de capacité ne raccourcit pas ce délai pour un solde positif. Les réservations actives et leurs cibles restent protégées, même à zéro jeton. La suppression doit être enregistrée durablement avant de réduire le total utilisé pour l’admission. + +### Réservations et limite d’envoi + +Lorsqu’un plafond de racine, d’identité ou de pool s’applique, le premier envoi pour chaque cible ou clé nécessite une réservation dans la capacité normale de suivi. Sans réservation, aucun envoi n’atteint le fournisseur. Les nouvelles tentatives stables réutilisent les mêmes périmètres ; une autre cible ou clé nécessite une nouvelle réservation. `L` est la limite des envois physiques pour l’ensemble de la requête, figée au démarrage de la requête : quatre par défaut, ou jusqu’à dix-huit avec le profil de requête OAuth existant. + +L’activation des limites, les plafonds de jetons applicables à la racine, à l’identité et au pool, ainsi que `L`, sont déterminés au démarrage de chaque requête. Les changements de configuration s’appliquent uniquement aux requêtes démarrées après le changement. Une requête déjà en cours garde sa politique initiale pour toutes les nouvelles tentatives et continuations : activer ou abaisser un plafond ne la restreint pas davantage ; le relever ou le supprimer ne l’assouplit pas. Une requête commencée en observation seule reste dans ce mode jusqu’à sa fin. + +Le règlement final attend les rapports des envois démarrés. Un ID d’envoi n’est oublié qu’après enregistrement durable de sa comptabilité ; son solde demeure. La limite borne les envois, pas la facture : toute utilisation réelle supérieure à l’estimation reste comptabilisée. Sans plafond applicable, le comportement reste en observation seule, y compris l’omission des écritures lorsque la capacité de suivi est pleine. Aucun point de contrôle d’identité n’est ajouté. + +Claude CLI, CodeBuddy et Qoder comptent chaque invocation CLI comme un envoi et exigent une réservation normale avant le lancement. Les nouvelles tentatives et les tours d’outils internes au CLI ne consomment pas séparément la limite d’envois de la requête. Leur coût est réglé selon l’utilisation réelle rapportée, y compris la part dépassant l’estimation initiale. + +### Retour à 2.80.0 : contrat C + +Les nouveaux enregistrements utilisent le format v1 ordinaire et le domaine d’alias `pool`. La version 2.80.0 non modifiée les lit, compacte le journal et applique la rétention selon ses propres règles par libellé. Conservez le journal actuel et son sel : restaurer une ancienne copie perd les dépenses enregistrées depuis. Aucun correctif rétroporté, blocage du lanceur ou commande de rapprochement n’est nécessaire. + +Le retour à 2.80.0 ne garantit ni l’agrégation canonique ni le même solde disponible que si un trafic identique avait toujours été traité par 2.80.0. Lors d’une nouvelle mise à niveau, les associations automatiques des fournisseurs configurés et les correspondances explicites actuelles s’appliquent aux soldes d’origine encore conservés. Aucun nouveau point de contrôle ne dépend de métadonnées de liaison d’identité à préserver par l’ancien lecteur. + +Les journaux de versions expérimentales non publiées contenant `pool-current` ou `poolContinuity` sont exclus du contrat C, même après compactage. Leurs soldes d’origine restent un historique inconnu jusqu’à l’expiration normale, sans conversion ni suppression immédiate. + +Un enregistrement complet mais invalide, y compris un `null` final, bloque l’admission sous plafond. Le compactage peut conserver les données valides dans un point de contrôle propre, mais le refus reste actif jusqu’au redémarrage avec ce journal propre. Une dernière ligne JSON incomplète suit les règles de récupération existantes. Voir les limites et codes d’erreur dans la [référence anglaise](/reference/configuration/server/#historical-pool-continuity). diff --git a/docs-site/src/content/docs/fr/reference/management-api.md b/docs-site/src/content/docs/fr/reference/management-api.md index 9e7a6ec106f..1ee13d85e11 100644 --- a/docs-site/src/content/docs/fr/reference/management-api.md +++ b/docs-site/src/content/docs/fr/reference/management-api.md @@ -345,6 +345,8 @@ lui-même s'il souhaite ajouter une étoile au dépôt. ### Cycle de vie du système +`POST /api/system/restart` conserve par défaut un délai de 60 secondes lorsque le corps est absent ou vaut `{}`. Une session de gestion ou un jeton administrateur peut choisir explicitement un délai plus court avec `{"drainGraceMs":2000}` : la valeur doit être un entier compris entre 1 et 60000 millisecondes. Un corps ou une valeur invalide renvoie 400 sans lancer le redémarrage ; la capacité locale liée à la cible ne permet pas cette option (403). `drainTimeoutMs` indique le délai accepté, fixé par le premier appel et inchangé lors des appels suivants. Il inclut le temps de transmission de la réponse ; les budgets de nettoyage et de préparation du processus de remplacement restent respectivement de 60 et 70 secondes. Une requête interrompue peut déjà avoir été exécutée : vérifiez son résultat avant de la renvoyer, sans répétition automatique. + | Méthode et chemin | Objectif | Erreurs notables | | --- | --- | --- | | `GET /api/system/memory` | Renvoyer les mesures scalaires du processus, du tas, des flux, de l'état des réponses, du mécanisme de surveillance et des tours actifs | — | diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 05913d24b03..41e4485ce6e 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` walks you through: -1. **Pick a provider** — choose one of the 102 built-in registry presets or `custom` to type a base +1. **Pick a provider** — choose one of the 103 built-in registry presets or `custom` to type a base URL and adapter. 2. **API key** — paste a key, or reference an environment variable like `${ANTHROPIC_API_KEY}`. 3. **Default model** — for key, local, and custom providers, accept the preset or enter a model id. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 945a0c56834..613c85fc0c9 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -1016,7 +1016,7 @@ and `"force"`. A value you export yourself always wins over the injected one. directives, not the Agent tool's `model` argument. Make sure the directive matches the intended route. Pass `"haiku"` as the model placeholder. -Set `claudeCode.stabilizePromptCache` to `true` in `config.json` to relocate supported trailing Claude harness notices from system instructions to a trailing user message on translated routes. The default is `false`. Enable it only when this role change is appropriate for your clients. It preserves fenced examples and unmatched text; native Anthropic passthrough is unchanged. The metadata-less prompt-cache key then follows stabilized instructions. This does not create conversation identity or guarantee upstream cache hits. +Set `claudeCode.stabilizePromptCache` to `true` in `config.json` to peel supported trailing Claude harness notices from system instructions on translated routes. Recognized `N tokens left` footers are dropped, including repeated footers. TaskCreate reminders still move to a trailing user message; if only token footers were peeled, no input message is added. The default is `false`. Enable it only when this role change is appropriate for your clients. It preserves fenced examples and unmatched text; native Anthropic passthrough is unchanged. The metadata-less prompt-cache key then follows stabilized instructions. This does not create conversation identity or guarantee upstream cache hits. On every translated Chat route, timeline reminders keep their position in the conversation, after any pending tool results. This prevents a newly appended diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 888a131f6fb..7d756a212d7 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -1047,6 +1047,8 @@ In **Codex Set → Multi-auth**, enable the **Codex credits** switch in the **Co When an account reaches 100% on a usage window and still holds credits, upstream keeps serving it and draws the balance. OpenCodex does not let that happen by default: an account at 100% is switched out while its weekly or monthly window (only monthly on 30-day plans) or its 5-hour window is full, and used again once that window resets. The order of the other accounts does not change, and when no other account is available, selection finds none rather than spending credits. A request for the main account is refused like a hard-lock refusal until the reset. A full 5-hour window without a reset time holds an account only while that reading is fresh. Allowing the main account does not lift its hard lock (on by default at 98%), which still stops it first; turn the lock off if the main account should spend credits (a lock at 100% still stops it at 100%). To let accounts keep working from their credits, turn on **Use credits** next to the **Codex credits** switch in the Codex Auth header. The switch allows every account. To choose accounts one by one, open an account card's **⋯** menu and use **Use credits after limit** there; the header switch shows a middle position when only some accounts are on. An account allowed to spend carries a **Uses credits** badge on its card. New accounts start off. The choice is stored in `creditCodexAccountIds` and never redeems reset credits; the **Codex credits** display switch only shows balances and never changes routing. +For an opted-in main account with a full usage window, the background recovery cycle renews previously spendable credit observations from three minutes of age, even while the dashboard is closed. It prepares a valid token before the authenticated usage lookup so the balance can be observed again before its five-minute freshness limit. Missing, zero, restricted or retracted credits do not trigger this renewal. A failed or incomplete lookup does not refresh old balance evidence, and requests are still refused once that evidence expires. The separate main-account hard lock and upstream retry delays still apply. This metadata lookup does not send a model validation request or set or clear the account's needs-reauth state. + Background revalidation is separate and off by default. It requires Token Guardian, the `openai` provider's `proactive` refresh policy, and `tokenGuardian.codexWarmupEnabled`. It skips accounts awaiting deferred registration validation. ### Cancelling main-account device reauthentication diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 6c983ad6b8d..deff7eb095c 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -316,7 +316,9 @@ Combo's `decisionProvider`: } ``` -- The row's `baseUrl` must be the full decision endpoint and its path must end in `/systemone`. +- The row's `baseUrl` must be the full decision endpoint. HTTPS services may use any path, + such as `/v1/decisions`; plain HTTP endpoints must still end in `/systemone`. + Userinfo, query strings, and fragments are not accepted in decision URLs. The decision model is `defaultModel`, else the first `models` entry; a row with neither is treated as unusable and fails open without a request (`jev-latest` is TypeSafe's model and is never sent to a self-hosted host). The row is a decision service only: it is never published as a @@ -344,9 +346,31 @@ Combo's `decisionProvider`: The provider's **Test connection** sends a bounded probe decision to its endpoint. **Create JEV Auto** on a System One provider (keyless rows included) prefills that row as the -decision provider. Disabled rows, endpoints not ending in `/systemone`, and rows without a model +decision provider. Disabled rows, invalid decision URLs, and rows without a model are shown with a reason and cannot be picked. +For an alternative hosted service with the same decision request/response contract, add a separate +provider row and select it with the Combo's `decisionProvider`: + +```json +{ + "providers": { + "alternative-decider": { + "adapter": "jev-decision", + "baseUrl": "https://decisions.example/v1/decisions", + "apiKey": "${ALTERNATIVE_DECISION_KEY}", + "defaultModel": "decision-model-v1", + "liveModels": false + } + } +} +``` + +Set `decisionProvider: "alternative-decider"` on the existing JEV Combo. Its own credential and +model accompany the bounded decision state; TypeSafe environment credentials are never inherited. +The `jev` id remains canonical. HTTPS/TLS, outbound destination restrictions, redirect refusal, +request/response bounds, timeout, cancellation, and the eligible-choice allowlist still apply. + [Laya MLX](https://github.com/mizorewww/laya-mlx) runs typed decisions on Apple Silicon. To use it with this method, expose it through a System One-compatible HTTP wrapper, then configure a row like `ollama-tev1` above with the wrapper's full `/systemone` URL and accepted model id as `defaultModel`. @@ -803,7 +827,7 @@ Combos are stored in the top-level `combos` object, keyed by combo id: | `alias` | No | none | Optional trimmed public model id; use the alias rules above. An empty value is stored as no alias. | | `nativeAlias` | No | `false` | Explicitly permit a currently supported bare native `alias` to take routing and catalog precedence. Never inferred from the alias. | | `displayName` | No | none | Bounded display-only catalog label. Required and non-empty when `nativeAlias` is true. | -| `decisionProvider` | No | `"jev"` | JEV only. Provider id of the decision service: `"jev"` (TypeSafe, valid without a provider row; the same as omission) or a configured `adapter: "jev-decision"` row with a `/systemone` `baseUrl`, such as a self-hosted Ollama `tev1`. | +| `decisionProvider` | No | `"jev"` | JEV only. Provider id of the decision service: `"jev"` (TypeSafe, valid without a provider row; the same as omission) or a configured `adapter: "jev-decision"` row with a full HTTPS decision URL or a local HTTP `/systemone` URL, such as a self-hosted Ollama `tev1`. | | `decisionTimeoutMs` | No | `4000` | JEV only. Integer from 1000 to 120000: the decision deadline before failing open to the first eligible target. | ## Troubleshooting diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index 2ad08330021..bac4472e959 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Integrations -description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo and Factory Droid from the dashboard — one switch per client, with a backup taken before every write. +description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code and Factory Droid from the dashboard — one switch per client, with a backup taken before every write. --- The **Connect** page writes opencodex's provider block into a client's own config -file, and removes it again. Seventeen clients work this way, each with a switch: +file, and removes it again. Eighteen clients work this way, each with a switch: | Client | Config file | Format | When the change takes effect | Credential | |---|---|---|---|---| @@ -24,6 +24,7 @@ file, and removes it again. Seventeen clients work this way, each with a switch: | omo (Pi / senpi) | `~/.omo/agent/models.json` | JSON | new sessions | loopback placeholder | | Cline CLI | `~/.cline/data/settings/providers.json` and sibling `models.json` | JSON pair | after stopping and restarting Cline | loopback placeholder | | Kilo | first existing `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, or `config.json` under `~/.config/kilo` | JSONC | new sessions | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (on Windows with the default home: use `HOME ?? USERPROFILE` only if nonblank and Windows-absolute, otherwise `homedir()`; a separately supplied home is preserved; append `.commandcode\providers.json`) | JSON | next launch of Command Code | none — keyless loopback (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | JSON | immediately via file watching | none — keyless loopback | "omo" names three products that share the `~/.omo` folder. The **omo** tab manages Pi-based omo @@ -741,6 +742,48 @@ ocx integration client enable --client kilo ocx export --client kilo --out ./kilo.jsonc ``` +## Command Code + +Command Code keeps its custom providers in `~/.commandcode/providers.json`. This +integration writes `provider.opencodex` into that file and is loopback-only. + +**Which root it writes.** Command Code resolves its provider map as +`document.provider ?? document.providers`, so the singular root wins whenever it +exists. The integration therefore reads the target file first and writes into the +root that file already uses: a document that already carries a `providers` map keeps +it, and your other providers stay readable. A fresh file gets the singular `provider` +root. Both roots are declared as managed paths, so Disable removes the block from +whichever one it was written into. + +**The credential form.** The provider is written with `apiKey: false`, the documented +form for an endpoint that needs no key. A raw string is refused by Command Code, and +no service-token file is read, referenced, or exported by this integration. + +**Home resolution.** There is no `COMMANDCODE_HOME` override here. The published +client resolves `HOME ?? USERPROFILE` and then appends `/.commandcode/providers.json`; +it does not consult that variable. An override this integration honoured but the +client ignored would make Apply report success at a path Command Code never opens, so +the path is always `~/.commandcode/providers.json`. +On Windows with the default home, the integration uses `HOME ?? USERPROFILE` +only when the selected value is nonblank and Windows-absolute; otherwise it uses +`homedir()`. A separately supplied home is preserved. A `HOME` set by Git for +Windows or MSYS2 is therefore honoured only when it passes those checks. + +Paste the key yourself if you later move the provider off loopback: Command Code +stores keys in `~/.commandcode/auth.json` (via `/connect`), not in `providers.json`. + +```bash +ocx integration client enable --client commandcode +ocx export --client commandcode --out ./providers.json +``` + +`ocx commandcode restore --op ` accepts only Command Code operations, including +`--preview` and `--plan-fingerprint` requests. It rejects client/profile overrides. +The running proxy must support client-scoped restore; older proxies return an error +without falling back to the generic restore endpoint. +Use `ocx integration client restore` for the generic journal and the explicit Aside +profile commands above for Aside operations. + ## Factory Droid Run Droid once to create `~/.factory`, then explicitly enable this integration with diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 1d1384eecc5..4eedaf971c3 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -448,6 +448,16 @@ token counts may still be estimated, and the credit value does not replace USD c Completion fallback requests add their reported credits. An absent value means Kiro did not report credit usage; an explicit zero means it reported no spend. +### Kiro refresh attention + +If an AWS SSO refresh returns `400 invalid_request` and matching Kiro CLI recovery does not +succeed, the dashboard shows the affected account as needing re-authentication once its access +token expires. The active provider then reports `loggedIn: false` from `/api/oauth/status`. +A still-valid access token remains logged in. You can sign in again through the dashboard; +a later successful automatic refresh also clears the attention state. This status does not +classify the refresh grant as revoked or disable future refresh attempts. Network and server +errors do not trigger this status. + ### Kiro credential import The dashboard offers native Builder ID, Google, and GitHub device login without Kiro CLI. @@ -492,7 +502,7 @@ The account list marks Kiro accounts excluded from automatic selection with a re ## 3. API-key catalog -opencodex ships 102 built-in presets: 84 key-based, 14 OAuth, three local, and one default +opencodex ships 103 built-in presets: 84 key-based, 15 OAuth, three local, and one default ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard, validates the key, and stores it; validation is provider-specific. Notable entries: @@ -1052,7 +1062,7 @@ OpenCodex provides official adapter support for Qoder through the `qoder` (Globa ([Global](https://qoder.com/account/integrations), [CN](https://qoder.cn/account/integrations)) and paste it as the provider's API key. The stored key reaches the CLI only as `QODER_PERSONAL_ACCESS_TOKEN` (Global) or `QODERCN_PERSONAL_ACCESS_TOKEN` (CN) in a scoped child environment. - **Region Isolation:** Each preset accepts only its canonical destination (`https://qoder.com` or `https://qoder.cn`) and resolves its own executable. Credentials, model cache, usage, and health are independent; neither region falls back to the other. An older custom provider named `qoder` with a different destination keeps its existing adapter and URL. - **Model Discovery:** `qoder --list-models` is the authoritative entitlement roster for the current PAT. The cache is bound to an irreversible fingerprint of the token, so switching accounts never reuses another account's roster. If discovery fails, the provider degrades to a stale cache and then the documented static seed. -- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. v1 is text and reasoning only; image input fails explicitly. +- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. Client-declared tools are exposed through an isolated request-scoped MCP server; image input fails explicitly. - **Quota:** No public quota API is used, so totals and reset times are unavailable. Insufficient-credit errors (vendor code 118) surface as HTTP 429 `insufficient_quota`. - **Operators:** Qoder Global is operated by BRIGHT ZENITH PRIVATE LIMITED under the [product service terms](https://qoder.com/product-service); Qoder CN by 通义云启(杭州)信息技术有限公司 with Alibaba Cloud. Verify `ocx provider test qoder` (or `qoder-cn`) after configuring. diff --git a/docs-site/src/content/docs/guides/remote-hub.md b/docs-site/src/content/docs/guides/remote-hub.md index b82d731aeb7..a363084187c 100644 --- a/docs-site/src/content/docs/guides/remote-hub.md +++ b/docs-site/src/content/docs/guides/remote-hub.md @@ -557,7 +557,7 @@ input checks. opencodex does not publish an official container image. The repository does maintain a source-build [`Dockerfile`](https://github.com/lidge-jun/opencodex/blob/main/Dockerfile), [`compose.yaml`](https://github.com/lidge-jun/opencodex/blob/main/compose.yaml), and a narrow -`.dockerignore`. The build pins the multi-platform Bun 1.4.0 image index by digest, runs the proxy as +`.dockerignore`. The build pins the multi-platform Bun 1.4.2 image index by digest, runs the proxy as the non-root `bun` user, keeps the root filesystem read-only, drops Linux capabilities, and publishes only the data listener on the host's `127.0.0.1:10100` by default. The foreground process uses `OCX_SERVICE=1`, so stopping or recreating the container preserves routed Codex state instead diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 9d137750c3f..d25c15a07ef 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -268,7 +268,33 @@ they have been synchronized. See ## Remote Hub sessions, keys, and usage -In **Connect → API Keys**, every row of the key table has its own delete button, which asks for confirmation in place, and clicking a key shows its full value with a **Copy** button. Revealing a key needs a signed-in dashboard session; the admin token cannot read key values. +In **Connect → API Keys**, every row of the key table has its own delete button, which asks for confirmation in place. Clicking a key can show its full value with a **Copy** button only from an independently authorized session. + +### Reading an existing API key + +Showing a stored key's full value requires a dashboard session established by explicit pairing or +trusted Tailscale identity. An automatic local dashboard session can still show the masked key list, +but cannot reveal existing values; a raw admin token cannot call this session-only action either. +Use the existing [dashboard pairing flow](/guides/remote-hub/#pairing-this-browser-with-a-hub) to establish an +operator-authorized session before requesting a stored value; a refused reveal states the requirement +on the page. The local pairing form is offered only by a same-origin standalone dashboard opened +over HTTP at the literal `127.0.0.1` or `[::1]` address. The origin must match the server's +configured literal loopback bind. On `http://localhost:` (or another localhost alias), +check the server's bind: reopen `http://127.0.0.1:` for a `127.0.0.1` bind or +`http://[::1]:` for a `::1` bind, then pair there. A server configured with hostname +`localhost` has no eligible local pairing origin. The dashboard cannot infer the configured +bind from an alias URL, so it shows the generic denial without suggesting a replacement URL. +Use the existing remote-hub pairing flow or trusted Tailscale sign-in where available. +Pair again if the authorized session expires or is revoked. Ordinary dashboard sign-in, +key creation, rotation, and deletion are unchanged. + +After pairing, click the key again to request its value. Pairing itself never reveals a key. +Displayed key values and copy feedback are cleared when the shared session is cleared or replaced, +pairing starts, you leave this panel, hide the browser tab or desktop window, or switch servers. +This also clears newly created and replacement values, so copy a one-time value before leaving +the panel. If creation or rotation succeeds after the session or view changes, its one-time +value stays hidden. After switching servers, returning to the +original server revalidates its key list. Pending rotations remain available for explicit commit or abort. The dashboard's management plane is separate from direct client→hub model traffic. **Connect → API Keys** shows pending rotations, displays a replacement secret only once, and requires explicit commit or abort. Browser logout invalidates only the current remote session. Connected usage is the hub store filtered by the client's `apiKeyId`; disconnected usage is local, with no mirroring. diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md index 370d2cddbc5..500c89a2f65 100644 --- a/docs-site/src/content/docs/ja/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` では次の手順を説明します。 -1. **プロバイダーを選択してください** — 102 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 +1. **プロバイダーを選択してください** — 103 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。 2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。 3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。 4. **プロキシ ポート** — デフォルトは `10100` です。 diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index d208366d07f..a859acaa62d 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -611,7 +611,7 @@ Anthropic バックエンドを明示すると意図的に失敗後停止しま 引数ではなく `` ディレクティブを使います。ディレクティブが希望ルートと一致するか確認し、 モデルプレースホルダとして `"haiku"` を渡してください。 -`config.json` の `claudeCode.stabilizePromptCache` を `true` にすると、変換ルートのシステム指示末尾にある対応済み Claude 通知を最後のユーザーメッセージへ移します。既定値は `false` です。このロール変更が適切なクライアントでのみ有効にしてください。コードフェンス内の例と一致しない本文は保持され、Anthropic のネイティブ転送は変わりません。メタデータがない場合のキャッシュキーは安定化した指示から計算されます。会話 ID の生成やキャッシュヒットの保証は行いません。 +`config.json` の `claudeCode.stabilizePromptCache` を `true` にすると、変換ルートのシステム指示末尾にある対応済み Claude 通知を切り離します。認識された `N tokens left` 形式のトークンフッターは、繰り返されている場合もすべて削除します。TaskCreate のリマインダーは従来どおり末尾のユーザーメッセージへ移します。トークンフッターだけを切り離した場合、入力メッセージは追加しません。既定値は `false` です。このロール変更が適切なクライアントでのみ有効にしてください。コードフェンス内の例と一致しない本文は保持され、Anthropic のネイティブ転送は変わりません。メタデータがない場合のキャッシュキーは安定化した指示から計算されます。会話 ID の生成やキャッシュヒットの保証は行いません。 変換されたすべての Chat ルートで、タイムライン上のリマインダーは保留中のツール結果の後、会話内の元の位置を保ちます。これにより、新しいリマインダーを追加しても先頭のシステムプロンプトが書き換わらず、会話の途中に置かれた指示がそれより前のターンの前に移動することもありません。そのスロットが運ぶロールは別に決まります。プロバイダーが `foldDeveloperRoleToSystem: false` を記録していないかぎり、リマインダーは `system` として送られます。この記録は上流が `developer` ロールを受け付けることを表し、その場合は同じ位置のまま転送します。受け付けない上流は `400 role 'developer' is not allowed` を返してターンが始まらないため、記録のない宛先は畳む側になります。`stabilizePromptCache` の設定にかかわらず適用され、Anthropic のネイティブ転送は変わりません。キャッシュの再利用には、安定したセッション ID と上流キャッシュの利用可能性が引き続き必要です。過去の指示やツールの変更、会話の圧縮もキャッシュヒットに影響します。リマインダーの順序を保つだけで再利用が保証されるわけではありません。 diff --git a/docs-site/src/content/docs/ja/guides/integrations.md b/docs-site/src/content/docs/ja/guides/integrations.md index adc738aac7b..cdc1d244bee 100644 --- a/docs-site/src/content/docs/ja/guides/integrations.md +++ b/docs-site/src/content/docs/ja/guides/integrations.md @@ -1,9 +1,9 @@ --- title: クライアント統合 -description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Factory Droid に接続します。クライアントごとにスイッチがあり、書き込み前には必ずバックアップを取ります。 +description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code、Factory Droid に接続します。クライアントごとにスイッチがあり、書き込み前には必ずバックアップを取ります。 --- -**接続** タブは、各クライアントの設定ファイルに opencodex のプロバイダーブロックを書き込み、必要に応じて削除します。次の 17 クライアントは、それぞれのスイッチで管理できます。 +**接続** タブは、各クライアントの設定ファイルに opencodex のプロバイダーブロックを書き込み、必要に応じて削除します。次の 18 クライアントは、それぞれのスイッチで管理できます。 | クライアント | 設定ファイル | 形式 | 変更が反映される時点 | 認証情報 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Her | omo | `~/.omo/agent/models.json` | JSON | 新しいセッション | ループバック用プレースホルダー | | Cline CLI | `~/.cline/data/settings/providers.json` と同階層の `models.json` | JSON のペア | Cline の停止と再起動後 | ループバック用プレースホルダー | | Kilo | `~/.config/kilo` 内で最初に存在する `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json`、`config.json`(`XDG_CONFIG_HOME` でディレクトリを変更可能。どれもなければ `kilo.jsonc` を作成) | JSONC | 新しいセッション | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows の既定ホームを使用する場合: `HOME ?? USERPROFILE` が空文字列や空白のみではなく Windows の絶対パスの場合に限り使用し、それ以外は `homedir()`。別途指定されたホームは保持。その配下の `.commandcode\providers.json`) | JSON | Command Code の次回起動時 | なし — キー不要のループバック (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows の場合) | JSON | ファイル変更を即時反映 | キー不要のループバック | 生成されるカタログには、各プロバイダーの選択で有効なモデルのみが含まれます。これはダウンロードと管理対象の統合の両方に適用され、Pi と Aside も対象です。管理画面のモデル一覧にはすべてのモデルが表示されるため、追加のモデルを有効にできます。 diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index 0d8ec20f6ce..a499d77d404 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -193,7 +193,7 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht ## 3. API キーカタログ -opencodex には組み込みプリセットが 102 個含まれています。キー方式 84、OAuth 14、ローカル 3、 +opencodex には組み込みプリセットが 103 個含まれています。キー方式 84、OAuth 15、ローカル 3、 デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、 入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです: diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md index 385964bfd97..3bc41575906 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -123,6 +123,27 @@ Codex タスクだけに適用され、このオプション自体が委任を ダッシュボードの管理プレーンと client→hub のモデル通信は別経路です。**接続 → API キー** は保留中の更新を表示し、新しい秘密値を一度だけ示し、明示的な確定または中止を要求します。ブラウザーのログアウトは現在のセッションだけを無効にします。接続中の使用量は hub で `apiKeyId` に絞り、切断後はローカル記録を使い、ミラーリングしません。 +### 保存済み API キーの全体表示 + +保存済みキーの全体を表示するには、明示的なペアリングまたは信頼された Tailscale 認証で +発行されたダッシュボードセッションが必要です。自動発行されたローカルセッションでは +マスク済みの一覧を確認できますが、保存済みキーの値は表示できません。管理トークンも +このセッション専用操作には使えません。既存の Remote Hub ペアリング手順、または利用可能な +信頼された Tailscale ログインを使用してください。 + +ローカルペアリングフォームを表示するのは、同一オリジンの単独ダッシュボードを HTTP の +`127.0.0.1` または `[::1]` で開いた場合だけです。オリジンはサーバーに設定された +ループバック IP のバインドと一致する必要があります。`localhost` やその別名で開いた場合は +バインドを確認し、`127.0.0.1` なら `http://127.0.0.1:`、`::1` なら +`http://[::1]:` で開き直してペアリングしてください。hostname が `localhost` に +設定されたサーバーには、ローカルペアリングを許可するオリジンがありません。ダッシュボードは +別名 URL からバインドを判定できないため、代替 URL を提案せず一般的な拒否理由を表示します。 + +ペアリング後はキーをもう一度クリックして表示を要求してください。ペアリングだけで値は表示されません。 +共有セッションの削除・切り替え、ペアリング開始、パネルからの移動、ブラウザータブやデスクトップ +ウィンドウの非表示、サーバーの切り替えで、表示中の値とコピー状態を消去します。新規作成・交換した +一度だけ表示される値も対象です。承認済みセッションが失効した場合は再度ペアリングしてください。 + セレクターには有効化されたネイティブおよびルーティングモデルと Codex グローバル推論段階が表示されます。API は 選んだ強度がグローバル段階にあるか検査し、Codex は再び対象カタログ項目がその強度をサポートするか 検査します。 diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md index 562f64aaaf5..72b192b7369 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -140,7 +140,7 @@ Grok Build モデル フェンスを管理および適用します。 ## クライアント設定のエクスポート -### `ocx export --client ` +### `ocx export --client ` 実行中のプロキシに接続するクライアント設定を出力します。このコマンドは、ベース URL、モデル一覧、およびクライアントに応じた認証情報参照または `opencodex-loopback` プレースホルダーを含む `opencodex` プロバイダーブロックを、選択したクライアントのネイティブ形式でシリアル化します。 @@ -148,7 +148,7 @@ Grok Build モデル フェンスを管理および適用します。 |旗 |アクション | | --- | --- | -| `--client ` |必須。クライアントの設定形式を選択します。 | +| `--client ` |必須。クライアントの設定形式を選択します。 | | `--json` |構成 JSON のみを標準出力に出力するため、リダイレクトはバイト正確な出力をキャプチャします。 `--out` 書き込みメモを含むすべての診断は stderr に送られます。 | | `--out ` |設定を `` に書き込みます。既存のファイルの置き換えを拒否します。 | | `--force` | `--out` が既存のファイルを置き換えることを許可します。 | @@ -179,6 +179,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS と Windows で同じ。Raycast は `XDG_CONFIG_HOME` を尊重しません) | `raycast-providers.yaml` | なし — loopback のみ。`api_keys` エントリは書き込まれません | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`、次に `SENPI_CODING_AGENT_DIR`、次に `PI_CODING_AGENT_DIR` の順で設定時に優先。相対値は拒否されます) | `omo-models.json` | なし — loopback placeholder | | `kilo` | `~/.config/kilo` 配下で最初に存在する `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json`、`config.json`(`XDG_CONFIG_HOME` が設定されていればその配下)。候補がなければ `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows の既定ホームを使用する場合: `HOME ?? USERPROFILE` が空文字列や空白のみではなく Windows の絶対パスの場合に限り使用し、それ以外は `homedir()`。別途指定されたホームは保持。その配下の `.commandcode\providers.json`) | `providers.json` | ループバックのみ・`apiKey: false`・環境変数不要 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | ループバックのみ・環境変数不要 | Raycast のエクスポートは、`providers` シーケンスに `id: opencodex` 要素を 1 つだけ持つ独立した `providers.yaml` 文書です。内容は `name: OpenCodex`、プロキシの `/v1` ベース URL、および `abilities` 付きのルーティング済み全モデルです (`tools` と `system_message` は常にサポート、`vision` はカタログの入力モダリティから、`reasoning_effort` はモデルに effort ラダーがある場合、`temperature` は推論モデルではオフ)。Custom Providers は Raycast Pro の機能で、Raycast はこのファイルを監視しているため、保存した変更は再起動なしで反映されます。形式は [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers) に記載されています。`api_keys` エントリは書き込まれないため、このエクスポートは loopback 専用で、loopback 以外のバインドは拒否されます。 diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index 4ae7448950a..fe4649c729b 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -228,6 +228,65 @@ affinity を維持します。これらの戦略は provider enforcement を回 Anthropic アカウント ポリシーのリスクを理解していない限り、これは無効のままにしてください。不明な場合は、`ocx account use anthropic ` を手動で切り替えることをお勧めします。 ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2`(「Anthropic · Pool 2」)は、独自のアカウントプールを持つ 2 つ目の組み込み Anthropic OAuth プロバイダーです。`anthropic` と同じ Anthropic 実装(同じログインフロー、ワイヤ形式、ネイティブ Messages と Responses ブリッジ、モデルメタデータ)で動作し、分かれているのはアカウントとそれを囲むプールだけです。プールはモデルの接頭辞で選びます。`anthropic/claude-sonnet-5` はプライマリプール、`anthropic2/claude-sonnet-5` は Pool 2 を使います。 + +Pool 2 は追加するまで休止状態です。`ocx login anthropic2` でログインするか、ダッシュボードのプロバイダーページで **Anthropic · プール2** を追加します。最初のログインが成功すると、マーカー `"anthropicOAuthInstance": "anthropic2"` 付きの `providers.anthropic2` が作成されます。Pool 2 が既定のプロバイダーになることはありません。接頭辞のない `claude-*` モデル名、既定モデル、Claude Code の呼び出し元転送は引き続き `anthropic` に解決されます。以前に自分で作成したマーカーのない `anthropic2` エントリは独自の意味を保ちます。ログインは名前の衝突を拒否し、エントリも資格情報も書き換えません。 + +Pool 2 はプール設定を自身のプロバイダーエントリから読み取ります。キー、既定値、動作は上記のトップレベル `anthropicAccountPool` と同じで、プライマリプールからは何も継承しません。`providers.anthropic.anthropicAccountPool` や、他のプロバイダー上の同じフィールドは拒否されます。 + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +2 つのプールは opencodex 内で分離されています。 + +- **資格情報:** Pool 2 のアカウントは、保護された資格情報ストアの独自キー `anthropic2` に保存されます。トークンまたは検証済み Anthropic アカウントがもう一方のプールに既に保存されているログインは拒否されます。 +- **プール設定と実行時状態:** 選択、セッションアフィニティ、クールダウン、一時停止、モデルルート、アカウント別しきい値は 1 つのプールに属します。ルートの `fallback: true` は同じプール内でのみ範囲を広げます。 +- **クォータと使用量:** 使用量プローブ、クォータキャッシュ、使用量の帰属はプールごとに記録されるため、両方のプールに同じアカウント ID があっても記録は別々です。 +- **リセット付与:** Pool 2 は、変更されないプライマリプールのジャーナルの隣に独自のジャーナル(`anthropic2-reset-grant-ledger.json`)を持ちます。 +- **復旧:** `anthropic2/` への直接リクエストがプライマリプールにフォールバックすることはなく、Pool 2 のレート制限や拒否がプライマリプールのアカウントをクールダウンさせることもありません。両方のプールを指定した明示的なコンボは、宣言したターゲットを維持します。 + +この分離は opencodex 内のルーティング境界です。Anthropic によるアカウントの扱いや、上記のアカウントポリシーのリスクは変わりません。 + +Pool 2 のアカウントはブラウザー OAuth でのみ追加できます。`anthropic` と異なり、Pool 2 は Claude Code CLI のトークンをインポート、採用、書き戻しすることはありません。この違いは意図的です。Pool 2 は空の状態で始まり、使用可能な Pool 2 アカウントがない Pool 2 リクエストは、プライマリプールや Claude Code の資格情報を借りずに認証エラーで失敗します。 + +アカウントコマンドと管理 API はプールを名前で指定します: `ocx account pool anthropic2 …`、`ocx account auto-switch anthropic2 …`、`ocx account routes anthropic2 …`、`ocx account anthropic-reset-grants --provider anthropic2`。プール設定とリセット付与のエンドポイントは `provider: "anthropic2"` を受け付け、省略するとプライマリプールのままです。 + +#### ヘルパーのプール選択 (`anthropicInstance`) + +Web 検索とビジョンのヘルパーは、グローバルの `webSearchSidecar` と `visionSidecar`、および Claude Code オーバーライドの `claudeCode.webSearchSidecar` と `claudeCode.visionSidecar` で、任意の `anthropicInstance` を受け付けます。ヘルパーのバックエンドが Anthropic のとき、ダッシュボードに **アカウントプール** の選択肢として表示されます。 + +| 値 | 動作 | +| --- | --- | +| 未設定(既定) | 現在のリクエストのプールに従います。`anthropic2/` リクエストは Pool 2、`anthropic/` リクエストはプライマリプールを使います。他のプロバイダーからのリクエストは既存のヘルパー検出を使い、Pool 2 を選ぶことはありません。 | +| `"anthropic"` | 常にプライマリプールを使います。 | +| `"anthropic2"` | 常に Pool 2 を使います。 | + +このフィールドは、ヘルパーのバックエンドが Anthropic に解決される場合にのみ適用されます。他のバックエンドと組み合わせると検証エラーになります。Web 検索の既定は OpenAI なので、`"backend": "anthropic"` も設定してください。`anthropic/claude-sonnet-5` と `"anthropicInstance": "anthropic2"` のように、もう一方のプールで修飾されたヘルパーモデルも拒否されます。選んだプールに使用可能なアカウントがない場合、ヘルパーは何も送信せずに失敗し、もう一方のプールには切り替えません。その場合も本来のリクエストは、そのヘルパーなしでそのまま処理されます。未設定の選択は `"anthropic"` ではなく、値なしとして保存されます。 + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[ダウングレード] +Pool 2 のないバージョンは `anthropic2` エントリを理解しません。古いバージョンをインストールする前に、プロキシを停止し、`config.json` と `auth.json` をバックアップしてから、`config.json` から `providers.anthropic2` を削除してください。Pool 2 がプライマリプールの資格情報を移動したり書き換えたりすることはありません。Pool 2 が有効なままのインプレースダウングレードはサポートされていません。 +::: + ### 管理されたレコードの形状 `apiKeys[]` エントリには、`id`、`name`、生成された `key`、および ISO `createdAt` 文字列が含まれます。 `codexAccounts[]` エントリには `id`、`email`、および `isMain` が必要で、オプションの `plan`、`chatgptAccountId`、およびプライバシー セーフな `logLabel` が必要です。これらのレコードは通常、ダッシュボードで管理されます。 diff --git a/docs-site/src/content/docs/ja/reference/configuration/server.md b/docs-site/src/content/docs/ja/reference/configuration/server.md index 227ded775dd..ba9807287fb 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/server.md +++ b/docs-site/src/content/docs/ja/reference/configuration/server.md @@ -200,6 +200,47 @@ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agen The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. -## トークン予約と上限 - -`spend.root.maxTokens`、`spend.identity.maxTokens`、`spend.pool.maxTokens` のいずれかがリクエストに適用される場合、追跡容量の不足などでトークン予約を記録できなければ送信を拒否します。適用される上限がないリクエストは観測のみを続けます。 +## 過去のプール使用量の継承 + +プロバイダープールの上限は正規のルーティング先に適用し、ログのアカウント表示名とは分けて扱います。 +現在設定されているプロバイダー `P` では、`h(pool, P)`(このインストールのソルト付きプール別名)を、 +残高の読み取り時に自動で `P` に対応付けます。同じ別名の過去残高も対象です。プロバイダー別の上限を使うために、 +自己対応の手動登録やソルトからの計算をする必要はありません。 + +異なる過去の表示名やアカウント番号付きラベルは、この自動対応の対象になりません。 +所有先を確認した履歴だけを `config.json` のトップレベルの `spendPoolAliases` に登録してください。 +キーは同じインストールの過去のプール別名(32 文字の小文字 16 進数)、値は現在設定されている、確認済みの正規プロバイダー ID です。 +ID の前後に空白を付けないでください。 +現在の名前やアカウントの並び順から所有先を推測しません。ジャーナルとソルトは非公開で保管してください。 + +自動対応と明示的な対応関係は読み取り時だけ適用し、元の残高を移動したり、ジャーナルに識別情報を保存したりしません。 +明示的な対応を削除した履歴は、現在設定されたプロバイダー自身の別名でない限り、再び所有先不明になります。 +各元残高は対応先のグループに一度だけ計上し、所有先不明の正の残高は全候補プールに一度ずつ保守的に加算します。 +設定済みプロバイダー自身のソルト付き別名を別のプロバイダーに割り当てる設定は拒否します。 +プロバイダー追加時にも再検証し、検証だけでソルトを作成することはありません。 + +休眠中の所有先不明の履歴は、最終活動時刻が `spend.retentionDays` の期限より古くなってから失効します。 +容量不足を理由に正の履歴を早く削除しません。送信中の予約と seed はゼロトークンでも保持し、削除記録の保存後に残高を減らします。 +適用される上限がある送信は、各送信先・キーの初回に通常容量の予約を必要とします。 +再試行は同じ範囲を使います。`L` は要求全体の物理送信回数上限で、要求開始時に固定します。 +通常は 4 回で、既存の OAuth 要求プロファイルでは最大 18 回です。すべての物理送信がこの有限の上限を共有します。 + +制限を適用するかどうか、適用対象のルート・識別子・プールのトークン上限、および `L` は要求開始時に確定します。 +設定変更は、変更後に開始する要求だけに適用します。実行中の要求は再試行・継続も含め、開始時のポリシーを維持します。 +上限の有効化・引き下げで実行中の要求を厳しくしたり、引き上げ・削除で緩和したりしません。 +監視のみで始まった要求は、完了まで監視のみのままです。 +最終精算は報告完了を待ち、送信 ID の削除後も残高を保持します。上限未設定では従来の監視のみの動作を保ち、容量満杯時の記録省略も変わりません。 + +Claude CLI、CodeBuddy、Qoder は CLI 呼び出し 1 回を送信 1 回と数え、起動前に通常の予約を必要とします。 +CLI 内部の再試行・ツールターンは要求全体の送信回数上限に個別計上しませんが、初期見積もりを超えた分も実際に報告された使用量で精算します。 + +### 2.80.0 へのロールバック(契約 C) + +新しい記録は通常の v1 形式と `pool` 別名を使います。未改変の 2.80.0 は自身のラベル別規則で読み取り、圧縮・保持処理を行えます。 +最新のジャーナルとソルトを維持してください。ダウングレード後の正規プール合算や、同じ通信を最初から 2.80.0 で処理した場合の残り枠は保証しません。 +再アップグレード時には、残存する元残高に、現在設定されたプロバイダーの自動対応と明示的な対応関係を適用します。起動フェンスや追加の照合コマンドは不要です。 +未公開の実験版の `pool-current` / `poolContinuity` 履歴は契約 C の対象外です。残高は所有先不明の履歴として保持し、通常の期限でのみ失効させます。 + +完全だが不正な記録(最終行の `null` も含む)は上限付きの受け入れを拒否します。圧縮は有効な残高を保持して続行し、 +実行中の拒否はクリーンなジャーナルを再起動で読み直すまで残ります。途中で切れた最終 JSON 行は従来の復旧規則に従います。 +詳しい境界とエラーは[英語の正本](/reference/configuration/server/#historical-pool-continuity)を参照してください。 diff --git a/docs-site/src/content/docs/ja/reference/management-api.md b/docs-site/src/content/docs/ja/reference/management-api.md index 25eb7807eab..733c910157c 100644 --- a/docs-site/src/content/docs/ja/reference/management-api.md +++ b/docs-site/src/content/docs/ja/reference/management-api.md @@ -301,6 +301,8 @@ CLI コマンドは Anthropic OAuth アカウントを id または一意の別 ### システムのライフサイクル +`POST /api/system/restart` は、本文を省略した場合や `{}` の場合、既定の 60 秒のドレイン猶予を維持します。管理セッションまたは管理者トークンを使う呼び出し元は、`{"drainGraceMs":2000}` で短い猶予を明示的に選択できます。値は 1~60000 ミリ秒の整数です。不正な本文や値は再起動を開始せずに 400 を返します。対象プロセスに限定されたローカル再起動 capability では、このオプションを設定できません(403)。応答の `drainTimeoutMs` は最初に受け入れた猶予を示し、繰り返しの呼び出しでも期限は変わりません。猶予には応答送信までの遅延を含み、クリーンアップと置換プロセスの準備には別々の 60 秒・70 秒の予算があります。中断された処理が既に実行された可能性があるため、再送前に結果を確認してください。自動再送は追加されません。 + |メソッドとパス |目的 |注目すべきエラー | | --- | --- | --- | | `GET /api/system/memory` |スカラー プロセス、ヒープ、ストリーム、応答状態、ウォッチドッグ、およびアクティブ ターン メトリックを返します。 — | diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md index 5c5ae7d9fb1..22653fc9b91 100644 --- a/docs-site/src/content/docs/ko/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init`은 다음 과정을 안내합니다: -1. **프로바이더 선택** — 내장 레지스트리 프리셋 102개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. +1. **프로바이더 선택** — 내장 레지스트리 프리셋 103개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다. 2. **API 키** — 키를 붙여넣거나 `${ANTHROPIC_API_KEY}` 같은 환경 변수를 참조합니다. 3. **기본 모델** — 키, 로컬, custom 프로바이더에서는 프리셋을 그대로 쓰거나 모델 ID를 직접 입력합니다. 4. **프록시 포트** — 기본값은 `10100`입니다. diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index b545d4ca0e0..1b9947eebb9 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -674,7 +674,7 @@ Anthropic 백엔드를 명시하면 의도적으로 실패 후 중단해요. 인자가 아니라 `` 지시문을 사용해요. 지시문이 원하는 라우트와 일치하는지 확인하고, 모델 자리 표시자로 `"haiku"`를 전달하세요. -`config.json`에서 `claudeCode.stabilizePromptCache`를 `true`로 설정하면 번역 경로의 시스템 지시 끝에 붙은 지원 대상 Claude 알림을 마지막 사용자 메시지로 옮깁니다. 기본값은 `false`입니다. 사용하는 클라이언트에서 이 역할 변경을 허용할 때만 켜세요. 코드 펜스 안의 예제와 일치하지 않는 원문은 보존하며, Anthropic 원본 전달 경로는 바꾸지 않습니다. 메타데이터가 없는 요청의 캐시 키는 정리된 지시문을 기준으로 계산합니다. 대화 식별자를 만들거나 상위 서비스의 캐시 적중을 보장하는 기능은 아닙니다. +`config.json`에서 `claudeCode.stabilizePromptCache`를 `true`로 설정하면 변환 경로의 시스템 지시 끝에 붙은 지원 대상 Claude 알림을 떼어냅니다. 인식된 `N tokens left` 토큰 푸터는 반복된 경우에도 모두 제거합니다. TaskCreate 알림은 기존처럼 마지막 사용자 메시지로 옮기며, 토큰 푸터만 떼어낸 경우에는 입력 메시지를 추가하지 않습니다. 기본값은 `false`입니다. 사용하는 클라이언트에서 이 역할 변경을 허용할 때만 켜세요. 코드 펜스 안의 예제와 일치하지 않는 원문은 보존하며, Anthropic 원본 전달 경로는 바꾸지 않습니다. 메타데이터가 없는 요청의 캐시 키는 정리된 지시문을 기준으로 계산합니다. 대화 식별자를 만들거나 상위 서비스의 캐시 적중을 보장하는 기능은 아닙니다. 변환된 모든 Chat 경로에서 타임라인 알림은 대기 중인 도구 결과 뒤, 대화 안의 원래 위치를 그대로 유지합니다. 덕분에 새 알림을 추가해도 맨 앞의 시스템 프롬프트를 다시 쓰지 않고, 대화 중간의 지시가 그 지시보다 앞선 턴으로 끌려가지도 않습니다. 그 자리가 어떤 역할을 싣는지는 따로 정합니다. 공급자가 `foldDeveloperRoleToSystem: false`를 기록하지 않는 한 알림은 `system`으로 보내며, 이 기록은 상위 서비스가 `developer` 역할을 받아들인다는 뜻이라 같은 위치에서 그대로 전달합니다. 받아들이지 않는 상위 서비스는 `400 role 'developer' is not allowed`로 응답해 턴이 시작조차 못 하므로, 기록이 없는 목적지는 접는 쪽을 씁니다. `stabilizePromptCache` 설정과 관계없이 적용되며 Anthropic 네이티브 전달은 기존 동작을 유지합니다. 캐시 재사용에는 안정적인 세션 식별자와 사용 가능한 상위 서비스 캐시가 여전히 필요합니다. 이전 지시나 도구의 변경, 대화 압축도 캐시 적중에 영향을 줄 수 있으며, 알림 순서를 유지하는 것만으로 재사용을 보장하지는 않습니다. diff --git a/docs-site/src/content/docs/ko/guides/integrations.md b/docs-site/src/content/docs/ko/guides/integrations.md index 9e09c8167eb..189689b5e2c 100644 --- a/docs-site/src/content/docs/ko/guides/integrations.md +++ b/docs-site/src/content/docs/ko/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 연동 -description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo와 Factory Droid를 opencodex에 연결합니다. 클라이언트마다 스위치가 하나씩 있으며 기록 전마다 백업합니다. +description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code와 Factory Droid를 opencodex에 연결합니다. 클라이언트마다 스위치가 하나씩 있으며 기록 전마다 백업합니다. --- -**연결** 페이지는 클라이언트의 설정 파일에 opencodex 프로바이더 블록을 쓰고 다시 제거합니다. 다음 17개 클라이언트는 각각 스위치로 관리합니다. +**연결** 페이지는 클라이언트의 설정 파일에 opencodex 프로바이더 블록을 쓰고 다시 제거합니다. 다음 18개 클라이언트는 각각 스위치로 관리합니다. | 클라이언트 | 설정 파일 | 형식 | 변경 적용 시점 | 자격 증명 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, | omo | `~/.omo/agent/models.json` | JSON | 새 세션에서 | 루프백 자리표시자 | | Cline CLI | `~/.cline/data/settings/providers.json` 및 같은 위치의 `models.json` | JSON 파일 쌍 | Cline을 중지하고 다시 시작한 뒤 | 루프백 자리표시자 | | Kilo | `~/.config/kilo`에서 먼저 존재하는 `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, `config.json` (`XDG_CONFIG_HOME`로 디렉터리 변경 가능, 모두 없으면 `kilo.jsonc` 생성) | JSONC | 새 세션에서 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows 기본 홈 사용 시: `HOME ?? USERPROFILE` 값이 비어 있거나 공백만으로 구성되지 않은 Windows 절대 경로일 때만 사용하고, 그 외에는 `homedir()` 사용. 별도로 전달된 홈은 유지. 해당 홈 아래의 `.commandcode\providers.json`) | JSON | 다음 Command Code 실행 시 | 없음 — 키 없는 루프백 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows에서) | JSON | 파일 변경 시 즉시 | 키 없는 루프백 | 생성된 카탈로그에는 각 프로바이더 선택에서 활성화된 모델만 들어갑니다. Pi와 Aside를 포함한 다운로드와 관리형 연동 모두에 적용됩니다. 관리 모델 목록에는 전체 모델이 계속 표시되어 추가 모델을 활성화할 수 있습니다. @@ -129,6 +130,12 @@ ocx integration client history --client hermes ocx integration client restore --op [--confirm-drift] ``` +`ocx commandcode restore --op `는 Command Code 작업만 복원합니다. +`--preview`와 `--plan-fingerprint`에도 같은 조건이 적용되며, 클라이언트나 +프로필을 바꾸는 옵션은 받지 않습니다. 실행 중인 프록시가 이 전용 복원 경로를 +지원하지 않으면 일반 복원으로 재시도하지 않고 오류를 반환합니다. 일반 작업 기록은 위의 +`ocx integration client restore`를, Aside 작업은 아래의 프로필 전용 명령을 사용하세요. + `--overwrite-conflict`는 **Replace**의 터미널 명령입니다. ```bash diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index e36b1876e29..3fda5134b55 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -190,7 +190,7 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http ## 3. API 키 카탈로그 -opencodex에는 빌트인 프리셋이 102개 들어 있습니다. 키 방식 84개, OAuth 14개, 로컬 3개, +opencodex에는 빌트인 프리셋이 103개 들어 있습니다. 키 방식 84개, OAuth 15개, 로컬 3개, 기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고, 입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다: diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index f6e78c66223..560e2bc61c0 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -139,6 +139,31 @@ Dashboard의 **Sub-agent delegation** 선택기는 `injectionModel`과 선택적 대시보드 관리 API와 클라이언트에서 허브로 가는 모델 요청은 서로 다른 경로입니다. **연결 → API 키**에서는 진행 중인 키 교체를 확인하고, 새 키를 한 번만 표시하며, 확정 또는 취소를 직접 눌러야 합니다. 브라우저 로그아웃은 현재 원격 세션만 끝냅니다. 연결 중 사용량은 허브에서 해당 `apiKeyId`만 보고, 연결 해제 후에는 로컬 기록을 보며 서로 복제하지 않습니다. +### 기존 API 키의 전체 값 조회 + +저장된 키의 전체 값을 보려면 명시적인 페어링이나 신뢰된 Tailscale 인증으로 발급된 +대시보드 세션이 필요합니다. 자동 발급된 로컬 세션은 마스킹된 목록을 계속 볼 수 있지만, +기존 키의 전체 값은 조회할 수 없습니다. 관리자 토큰도 이 세션 전용 동작을 호출하지 못합니다. +기존 대시보드 페어링 절차로 세션을 승인한 뒤 조회하세요. 거부된 조회는 페이지에서 +요구 사항을 안내합니다. 로컬 페어링 폼은 같은 출처의 단독 대시보드를 HTTP의 +`127.0.0.1` 또는 `[::1]` IP 주소에서 열었을 때만 제공합니다. 오리진은 서버에 설정된 +루프백 IP 바인드와 일치해야 합니다. `http://localhost:`나 다른 localhost 별칭으로 +접속했다면 서버의 바인드를 먼저 확인하세요. `127.0.0.1` 바인드이면 `http://127.0.0.1:`, +`::1` 바인드이면 `http://[::1]:`로 다시 열어 페어링하세요. hostname을 `localhost`로 +설정한 서버에는 로컬 페어링을 허용하는 오리진이 없습니다. 대시보드는 별칭 URL만으로 실제 +바인드를 알 수 없으므로 대체 URL을 제안하지 않고 일반 거절 안내만 표시합니다. +사용 가능한 경우 기존 Remote Hub 페어링 절차나 신뢰된 Tailscale 로그인을 이용하세요. +승인된 세션이 만료되거나 해제되면 다시 페어링해야 합니다. 일반 대시보드 접속과 +키 생성·교체·삭제는 변경하지 않습니다. + +페어링을 마친 뒤 키를 다시 클릭해야 전체 값을 조회합니다. 페어링만으로 키를 +자동 조회하지 않습니다. 공유 세션이 해제되거나 교체될 때, 페어링을 시작할 때, +이 패널을 벗어나거나 브라우저 탭 또는 데스크톱 창을 숨길 때, 서버를 바꿀 때 표시된 +키와 복사 상태를 지웁니다. 새로 생성하거나 교체한 값도 지우므로 일회성 값은 패널을 +벗어나기 전에 복사하세요. 생성·교체 요청 중 세션이나 화면이 바뀌면 작업이 성공하더라도 +일회성 값은 숨깁니다. 서버를 바꾼 뒤 원래 서버로 돌아오면 키 목록을 다시 검증해 +불러옵니다. 진행 중인 교체는 명시적으로 확정하거나 취소할 수 있습니다. + 선택기에는 활성화된 네이티브 및 라우팅 모델과 Codex 전역 reasoning 단계가 표시됩니다. API는 선택한 강도가 전역 단계에 있는지 검사하고, Codex는 다시 대상 카탈로그 항목이 그 강도를 지원하는지 검사합니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md index 53f508c4f98..ed386c85efd 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -170,7 +170,7 @@ Grok Build model fence를 관리하고 적용합니다. ## 클라이언트 설정 내보내기 -### `ocx export --client ` +### `ocx export --client ` 실행 중인 프록시에 연결할 client config를 출력합니다. 이 명령은 base URL, model list, 그리고 client에 따라 credential reference 또는 `opencodex-loopback` placeholder를 포함한 `opencodex` provider block을 선택한 client의 네이티브 형식으로 직렬화합니다. @@ -178,7 +178,7 @@ Grok Build model fence를 관리하고 적용합니다. | 플래그 | 동작 | | --- | --- | -| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | +| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | | `--json` | config JSON만 stdout에 출력하므로, redirect가 byte-exact 출력을 캡처합니다. `--out` write note를 포함한 모든 진단 메시지는 stderr로 갑니다. | | `--out ` | config를 ``에 씁니다. 기존 파일이 있으면 덮어쓰지 않습니다. | | `--force` | `--out`이 기존 파일을 덮어쓰도록 허용합니다. | @@ -209,6 +209,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS와 Windows 모두 동일. Raycast는 `XDG_CONFIG_HOME`을 따르지 않습니다) | `raycast-providers.yaml` | 없음 — loopback 전용. `api_keys` 항목은 쓰지 않습니다 | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, `SENPI_CODING_AGENT_DIR`, `PI_CODING_AGENT_DIR` 순서로 설정된 값이 우선. 상대 경로는 거부됩니다) | `omo-models.json` | 없음 — loopback placeholder | | `kilo` | `~/.config/kilo`에서 먼저 존재하는 `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` 또는 `config.json` (`XDG_CONFIG_HOME`이 설정되면 해당 디렉터리 사용); 후보가 없으면 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows 기본 홈 사용 시: `HOME ?? USERPROFILE` 값이 비어 있거나 공백만으로 구성되지 않은 Windows 절대 경로일 때만 사용하고, 그 외에는 `homedir()` 사용. 별도로 전달된 홈은 유지. 해당 홈 아래의 `.commandcode\providers.json`) | `providers.json` | 루프백 전용; `apiKey: false`; 환경 변수 불필요 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 루프백 전용, 환경 변수 불필요 | Raycast 내보내기는 `providers` 시퀀스에 `id: opencodex` 요소 하나만 담은 독립 `providers.yaml` 문서입니다. 내용은 `name: OpenCodex`, proxy의 `/v1` base URL, 그리고 `abilities`가 붙은 라우팅된 모든 모델입니다(`tools`와 `system_message`는 항상 지원, `vision`은 카탈로그의 입력 모달리티를 따름, `reasoning_effort`는 모델에 effort 사다리가 있을 때, `temperature`는 추론 모델에서 꺼짐). Custom Providers는 Raycast Pro 기능이며, Raycast가 이 파일을 감시하므로 저장한 변경은 재시작 없이 적용됩니다. 형식은 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)에 문서화되어 있습니다. `api_keys` 항목은 쓰지 않으므로 이 내보내기는 loopback 전용이며, loopback이 아닌 bind는 거부됩니다. diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 59e26108466..493df5501b2 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -355,6 +355,8 @@ Windows에서 Task Scheduler 항목을 만들려면 권한 상승이 필요합 시작할 때 `another process owns the runtime mutation lease`가 보고되거나 `ocx service status`에 `Runtime mutation lease busy`가 표시되면, 프록시가 실행 중이 아니더라도 이 리스가 시작이나 서비스 변경을 막고 있는 것입니다. 메시지에는 잠금 경로, 기록된 PID, 그 PID가 현재 살아 있는지 여부, 확인할 수 있는 경우 실행 파일 이름, 리스 경과 시간이 들어 있습니다. 프로세스의 신원은 검증되지 않습니다. PID가 재사용되었을 수 있으므로, 생존 여부와 실행 파일 이름은 지금 그 PID를 쓰고 있는 프로세스에 대한 정보입니다. 작업이 끝날 때까지 기다린 뒤 다시 시도하세요. 이 PID만 보고 잠금을 삭제하거나 프로세스를 중지하지 마세요. 리스 경과 시간이 30초를 넘고 기록된 PID가 더 이상 살아 있지 않으면 이후의 변경 시도가 오래된 리스를 회수할 수 있습니다. `ocx service status`는 리스를 확인만 합니다. +Windows에서는 이 공용 Node/Bun 진단 경로가 실행 파일 이름 조회용 프로그램을 실행하지 않습니다. 기록된 PID, 생존 여부, 리스 경과 시간과 복구 안내는 계속 표시됩니다. + ### `ocx codex-shim ` macOS와 Linux에서 `ocx codex-shim install`은 확정된 OpenCodex 홈에 전용 wrapper `/bin/codex`와 셸에서 읽을 `/codex-shell-env.sh`를 설치합니다. 원래 런처는 brew, npm, fnm이 설치한 위치에 그대로 있어 패키지 관리자 업데이트와 버전 롤백 뒤에도 다시 감쌀 필요가 없습니다. Windows는 기존처럼 스크립트 런처를 제자리에서 감싸며 실제 `codex.exe`는 건드리지 않습니다. Windows에서 `codex.exe`만 제공된다면 `ocx service install`을 사용하세요. diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index 0c3494ff3ac..b7f21a0ecb2 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -230,6 +230,65 @@ affinity 초기화 뒤의 기존 작업도 포함될 수 있습니다. 출력 Anthropic 계정 정책 위험을 이해하지 못한다면 이 기능은 꺼두십시오. 확신이 없으면 수동 `ocx account use anthropic ` 전환을 우선하십시오. ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2`("Anthropic · Pool 2")는 자체 계정 풀을 가진 두 번째 내장 Anthropic OAuth 공급자입니다. `anthropic`과 같은 Anthropic 구현, 즉 같은 로그인 흐름, 와이어 형식, 네이티브 Messages와 Responses 브리지, 모델 메타데이터를 사용하며, 계정과 그 계정을 묶는 풀만 분리됩니다. 풀은 모델 접두사로 고릅니다. `anthropic/claude-sonnet-5`는 기본 풀을, `anthropic2/claude-sonnet-5`는 Pool 2를 사용합니다. + +Pool 2는 추가하기 전까지 비활성 상태입니다. `ocx login anthropic2`로 로그인하거나 대시보드 공급자 페이지에서 **Anthropic · 풀 2**를 추가하십시오. 첫 로그인이 성공하면 `"anthropicOAuthInstance": "anthropic2"` 표식이 붙은 `providers.anthropic2`가 만들어집니다. Pool 2는 기본 공급자가 되지 않습니다. 접두사 없는 `claude-*` 모델 이름, 기본 모델, Claude Code 호출자 전달은 계속 `anthropic`으로 해석됩니다. 예전에 직접 만든, 표식 없는 `anthropic2` 항목은 사용자 정의 의미를 유지하며, 로그인은 이름 충돌을 거부하고 그 항목과 자격 증명을 다시 쓰지 않습니다. + +Pool 2는 풀 설정을 자체 공급자 항목에서 읽습니다. 키, 기본값, 동작은 위의 최상위 `anthropicAccountPool`과 같고, 기본 풀에서 아무것도 상속하지 않습니다. `providers.anthropic.anthropicAccountPool`이나 다른 공급자에 둔 같은 필드는 거부됩니다. + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +두 풀은 opencodex 안에서 분리됩니다. + +- **자격 증명:** Pool 2 계정은 보호된 자격 증명 저장소의 별도 키 `anthropic2` 아래에 저장됩니다. 토큰이나 검증된 Anthropic 계정이 이미 다른 풀에 저장된 로그인은 거부됩니다. +- **풀 설정과 실행 상태:** 선택, 세션 결속, 쿨다운, 일시 중지, 모델 경로, 계정별 임계값은 한 풀에 속합니다. 경로의 `fallback: true`도 같은 풀 안에서만 넓어집니다. +- **할당량과 사용량:** 사용량 조회, 할당량 캐시, 사용량 귀속은 풀별로 기록되므로, 두 풀에 같은 계정 ID가 있어도 기록은 따로 남습니다. +- **리셋 부여:** Pool 2는 그대로 유지되는 기본 풀 저널 옆에 자체 저널(`anthropic2-reset-grant-ledger.json`)을 둡니다. +- **복구:** `anthropic2/` 직접 요청은 기본 풀로 넘어가지 않으며, Pool 2의 속도 제한이나 거부가 기본 풀 계정을 쿨다운시키지 않습니다. 두 풀을 모두 지정한 명시적 콤보는 선언한 대상을 그대로 유지합니다. + +이 분리는 opencodex 내부의 라우팅 경계입니다. Anthropic이 계정을 다루는 방식이나 위에서 설명한 계정 정책 위험은 바뀌지 않습니다. + +Pool 2 계정은 브라우저 OAuth로만 추가합니다. `anthropic`과 달리 Pool 2는 Claude Code CLI 토큰을 가져오거나, 채택하거나, 다시 쓰지 않으며, 이 차이는 의도된 것입니다. Pool 2는 빈 상태로 시작하고, 쓸 수 있는 Pool 2 계정이 없는 Pool 2 요청은 기본 풀이나 Claude Code 자격 증명을 빌리지 않고 인증 오류로 실패합니다. + +계정 명령과 관리 API는 풀을 이름으로 지정합니다: `ocx account pool anthropic2 …`, `ocx account auto-switch anthropic2 …`, `ocx account routes anthropic2 …`, `ocx account anthropic-reset-grants --provider anthropic2`. 풀 설정과 리셋 부여 엔드포인트는 `provider: "anthropic2"`를 받으며, 생략하면 기본 풀을 그대로 사용합니다. + +#### 보조 기능의 풀 선택 (`anthropicInstance`) + +웹 검색과 비전 보조 기능은 전역 `webSearchSidecar`, `visionSidecar` 설정과 Claude Code 재정의 `claudeCode.webSearchSidecar`, `claudeCode.visionSidecar`에서 선택적 `anthropicInstance`를 받습니다. 보조 기능의 백엔드가 Anthropic이면 대시보드에 **계정 풀** 선택 항목으로 나타납니다. + +| 값 | 동작 | +| --- | --- | +| 설정 안 함(기본값) | 현재 요청의 풀을 따릅니다. `anthropic2/` 요청은 Pool 2를, `anthropic/` 요청은 기본 풀을 사용합니다. 다른 공급자의 요청은 기존 보조 기능 탐색을 그대로 쓰며, 이 탐색은 Pool 2를 고르지 않습니다. | +| `"anthropic"` | 항상 기본 풀을 사용합니다. | +| `"anthropic2"` | 항상 Pool 2를 사용합니다. | + +이 필드는 보조 기능의 백엔드가 Anthropic으로 결정될 때만 적용됩니다. 다른 백엔드와 함께 설정하면 검증 오류입니다. 웹 검색의 기본 백엔드는 OpenAI이므로 `"backend": "anthropic"`도 함께 설정하십시오. `anthropic/claude-sonnet-5`에 `"anthropicInstance": "anthropic2"`를 지정하는 것처럼 다른 풀로 한정된 보조 모델도 거부됩니다. 고른 풀에 쓸 수 있는 계정이 없으면 보조 기능은 아무것도 보내기 전에 실패하며, 다른 풀로 바꾸지 않습니다. 이때 본 요청은 그 보조 기능 없이 그대로 진행됩니다. 설정하지 않은 선택은 `"anthropic"`이 아니라 값 없음으로 저장됩니다. + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[다운그레이드] +Pool 2가 없는 버전은 `anthropic2` 항목을 이해하지 못합니다. 이전 버전을 설치하기 전에 프록시를 멈추고, `config.json`과 `auth.json`을 백업한 뒤, `config.json`에서 `providers.anthropic2`를 제거하십시오. Pool 2는 기본 풀 자격 증명을 옮기거나 다시 쓰지 않습니다. Pool 2가 활성 상태인 채로 제자리 다운그레이드하는 것은 지원하지 않습니다. +::: + ### 관리되는 레코드 구조 `apiKeys[]` 항목에는 `id`, `name`, 생성된 `key`, ISO 형식 `createdAt` 문자열이 들어갑니다. `codexAccounts[]` 항목에는 `id`, `email`, `isMain`이 필요하고, 선택적으로 `plan`, `chatgptAccountId`, 개인정보를 해치지 않는 `logLabel`을 둘 수 있습니다. 이런 레코드는 보통 대시보드가 관리합니다. diff --git a/docs-site/src/content/docs/ko/reference/configuration/server.md b/docs-site/src/content/docs/ko/reference/configuration/server.md index 14c89eaa329..eeabdb41dab 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/server.md +++ b/docs-site/src/content/docs/ko/reference/configuration/server.md @@ -257,6 +257,53 @@ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agen The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. -## 토큰 예약과 한도 - -요청에 `spend.root.maxTokens`, `spend.identity.maxTokens`, `spend.pool.maxTokens` 중 하나가 적용되면 추적 용량 부족 등으로 토큰 예약을 기록할 수 없을 때 전송을 거부합니다. 적용되는 한도가 없는 요청은 계속 관측만 합니다. +## 과거 풀 사용량의 연속성 + +제공자 풀 한도는 실제로 선택된 제공자에 적용합니다. 요청 로그의 계정 표시 이름과는 구분합니다. +현재 설정된 제공자 `P`는 잔액을 읽을 때 자신의 솔트 풀 별칭인 `h(pool, P)`와 자동으로 연결됩니다. +같은 별칭에 남아 있는 과거 잔액도 여기에 속합니다. 제공자마다 독립적인 한도를 쓰려고 자기 매핑을 직접 넣거나 +솔트로 해시를 계산할 필요는 없습니다. + +다른 과거 표시 이름이나 계정 순번이 붙은 라벨은 자동으로 연결하지 않습니다. +소유자를 확인한 과거 이력은 `config.json` 최상위의 `spendPoolAliases`에 매핑을 넣으세요. +키는 같은 설치의 과거 풀 별칭인 32자리 소문자 16진수이고, 값은 현재 설정에 있는 확인된 제공자의 정확한 ID입니다. +ID 앞뒤에 공백을 넣지 마세요. +현재 이름이나 계정 순서만 보고 소유자를 추측하지 마세요. 저널과 솔트는 비공개로 보관하세요. + +자동 연결과 명시적 매핑은 잔액을 읽을 때만 적용합니다. 원래 잔액을 옮기거나 저널에 식별 연결 정보를 저장하지 않습니다. +명시적 매핑을 삭제하면 그 이력은 다시 소유자 미확인 상태가 됩니다. 단, 현재 설정된 제공자 자신의 별칭이면 자동 연결이 남습니다. +각 잔액은 연결된 제공자 그룹에 한 번만 계산합니다. 연결되지 않은 양수 잔액은 모든 후보 풀에 한 번씩 보수적으로 더합니다. +설정된 제공자 자신의 솔트 별칭을 다른 제공자에 연결하면 거부합니다. 제공자를 추가할 때도 검사하며, +검증 과정에서 새 솔트를 만들지는 않습니다. 매핑이 잘못돼도 기존 한도는 유지합니다. + +휴면 상태인 미확인 이력은 마지막 활동이 `spend.retentionDays`보다 오래됐을 때만 만료됩니다. +용량이 부족해도 양수 이력을 먼저 지우지 않습니다. 진행 중인 예약과 seed는 0토큰이어도 보존하며, +삭제 기록이 저장된 뒤에만 허용 여부를 판단하는 잔액을 줄입니다. +한도가 적용되는 요청은 대상·키별 첫 전송 전에 일반 추적 용량으로 예약해야 합니다. 재시도는 같은 범위를 씁니다. +`L`은 요청 전체가 공유하는 물리 전송 횟수 한도이며 요청이 시작할 때 고정됩니다. 기본은 4회이고, +기존 OAuth 요청 프로필은 최대 18회까지 허용합니다. 모든 물리 전송이 이 유한한 한도를 함께 씁니다. + +한도 적용 여부, 적용할 루트·계정·제공자 풀의 토큰 한도, `L`은 요청이 시작할 때 정합니다. +설정을 바꾸면 변경 후 시작하는 요청에만 적용됩니다. 이미 실행 중인 요청은 재시도와 후속 전송에서도 시작할 때의 정책을 유지합니다. +한도를 켜거나 낮춰도 실행 중인 요청을 더 제한하지 않고, 높이거나 없애도 더 허용하지 않습니다. +관측 모드로 시작한 요청은 끝날 때까지 관측 모드를 유지합니다. +최종 정산은 보고가 끝날 때까지 기다립니다. 전송 ID를 지워도 잔액은 남습니다. +적용되는 한도가 없으면 기존 관측 동작을 유지하며, 추적 용량이 찼을 때 기록을 생략하는 동작도 그대로입니다. + +Claude CLI·CodeBuddy·Qoder는 CLI 호출 한 번을 전송 한 번으로 셉니다. 실행 전 일반 예약이 필요하며, 예약하지 못하면 실행을 거부합니다. +CLI 내부 재시도·도구 턴은 요청 전체 전송 횟수 한도에 따로 세지 않지만 비용은 보고된 실제 사용량으로 정산하며, 최초 추정량을 넘은 비용도 기록합니다. + +### 2.80.0으로 롤백: 계약 C + +새 기록은 기존 v1 형식과 `pool` 별칭을 씁니다. 수정하지 않은 2.80.0도 자신의 라벨별 규칙대로 읽고 압축·보존할 수 있습니다. +최신 저널과 솔트를 유지하세요. 롤백 후 제공자별 통합 집계가 유지되거나, 같은 요청을 처음부터 2.80.0에서 처리했을 때와 +남은 한도가 같다는 보장은 없습니다. 다시 업그레이드하면 남아 있는 원래 잔액에 +현재 설정된 제공자의 자동 연결과 명시적 매핑을 적용합니다. +별도의 실행 차단 장치나 조정 명령은 필요하지 않습니다. +미공개 실험 버전의 `pool-current` / `poolContinuity` 이력은 계약 C에서 제외합니다. +원래 잔액을 미확인 이력으로 보존하고 일반 보존 기간이 지난 뒤 만료시키며, 즉시 변환하거나 삭제하지 않습니다. + +완전하지만 잘못된 기록(마지막 줄의 `null` 포함)이 있으면 한도가 적용되는 요청을 거부합니다. +압축은 유효한 잔액을 보존하며 계속할 수 있습니다. 실행 중 거부 상태는 재시작 뒤 깨끗한 저널을 읽을 때까지 남습니다. +쓰다 끊긴 마지막 JSON 줄은 기존 복구 규칙을 따릅니다. 세부 조건과 오류 코드는 +[영문 원문](/reference/configuration/server/#historical-pool-continuity)을 참조하세요. diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index ed7d0292fa8..f9dd67d9f89 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -313,6 +313,8 @@ OpenAI도 같은 규칙을 따르며, 스위치를 켠다고 별도의 922k 모 ### 시스템 수명 주기 +`POST /api/system/restart`는 본문이 없거나 `{}`이면 기본 60초 drain 유예 시간을 유지합니다. 관리 세션 또는 관리자 토큰을 사용하는 호출자는 `{"drainGraceMs":2000}`으로 짧은 유예 시간을 명시적으로 선택할 수 있습니다. 값은 1~60000밀리초의 정수여야 합니다. 잘못된 본문이나 값은 재시작을 시작하지 않고 400을 반환하며, 대상 프로세스에 한정된 로컬 재시작 capability는 이 옵션을 설정할 수 없습니다(403). 응답의 `drainTimeoutMs`는 처음 수락한 유예 시간을 나타내며 반복 호출은 기존 기한을 바꾸지 않습니다. 유예 시간에는 응답 전송 지연이 포함됩니다. 정리와 대체 프로세스 준비에는 각각 별도의 60초와 70초 예산이 유지됩니다. 중단된 요청이 이미 실행되었을 수 있으므로 재전송 전에 결과를 확인하세요. 자동 재전송은 추가되지 않습니다. + | HTTP 메서드와 경로 | 목적 | 주요 오류 | | --- | --- | --- | | `GET /api/system/memory` | 프로세스, heap, stream, response-state, watchdog, active-turn의 스칼라 메트릭을 반환합니다 | — | diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 1a788461e3b..892b9ccd878 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -56,10 +56,13 @@ transport; it does not infer subscription attribution from the inbound protocol. - **Rewrites Codex's GPT-5 identity prompt** to a model-agnostic intro so routed models don't claim to be OpenAI. - For translated `Qwen3.8-27B` requests (including gateway-namespaced ids such as - `openai/Qwen3.8-27B`), a text-only developer reminder after the leading system - message stays in its conversation slot but is sent as `user`. The model's - [chat template](https://huggingface.co/Qwen/Qwen3.8-27B/blob/1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/chat_template.jinja) - rejects later `system` messages and does not accept `developer`, while later `user` messages + `openai/Qwen3.8-27B`) and `OrcaSAQ-2-Cyber-27B` GGUF requests, a text-only developer reminder + after the leading system message stays in its conversation slot but is sent as `user`. + The Orca exception requires `GGUF` in the final model segment before any optional colon + quant tag; non-GGUF Orca variants keep their configured developer-role behavior. + These pinned chat templates — for example + [Qwen3.8-27B's](https://huggingface.co/Qwen/Qwen3.8-27B/blob/1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/chat_template.jinja) — + reject later `system` messages and do not accept `developer`, while later `user` messages are valid. This preserves order but cannot preserve developer-role precedence. Other models keep their configured developer-role behavior; native Chat passthrough is unchanged. - **Clamps `reasoning_effort`** to the model's advertised subset when an exact tier is unavailable; diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 79624e60ea5..6fde5049a97 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -81,6 +81,14 @@ fallback, capability JSON, and provider-specific error handling retain their exi ## Command families +### `ocx message` + +`ocx message sessions [--json]` discovers loaded local Codex sessions. +`ocx message send (--thread | --name ) --stdin [--json]` +submits one correlated peer message, without starting a daemon or resuming a thread. +See [Local Codex Messaging](/reference/cli/messaging/) for sender context, kinds, +the tested runtime and `not_sent`/`queued`/`unknown` receipts. No remote transport is included. + ### `ocx provider` `ocx provider`, `ocx help provider`, `ocx provider help`, `ocx provider --help`, and diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 76dbf9947bb..733e8cc6bd5 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -903,7 +903,7 @@ Manage and apply the Grok Build model fence. ## Client config export -### `ocx export --client ` +### `ocx export --client ` Print a client config wired to the running proxy. The command serializes the `opencodex` provider block — base URL, model list, and the client's credential @@ -919,7 +919,7 @@ See [client integrations](/guides/integrations/) for managed refresh and upgrade | Flag | Action | | --- | --- | -| `--client ` | Required. Selects the client config dialect. | +| `--client ` | Required. Selects the client config dialect. | | `--json` | Print the generated document as JSON on stdout for scripts. This is JSON even when the selected client's native format is YAML, TOML, or JSON5. | | `--out ` | Write the client's native config format to ``. Refuses to replace an existing file. | | `--force` | Allow `--out` to replace an existing file. | @@ -952,6 +952,7 @@ client applies its own defaults for those). | `raycast` | `~/.config/raycast/ai/providers.yaml` on macOS and Windows alike (Raycast does not honor `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | none — loopback only, no `api_keys` entry is written | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, then `SENPI_CODING_AGENT_DIR`, then `PI_CODING_AGENT_DIR` win in that order when set; a relative value is refused) | `omo-models.json` | none — loopback placeholder | | `kilo` | first existing `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, or `config.json` under `~/.config/kilo` (`XDG_CONFIG_HOME` relocates that directory); uses `kilo.jsonc` when none exists | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (on Windows with the default home: use `HOME ?? USERPROFILE` only if nonblank and Windows-absolute, otherwise `homedir()`; a separately supplied home is preserved; append `.commandcode\providers.json`) | `providers.json` | loopback only; `apiKey: false`; no environment variable | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | loopback only; no environment variable | The managed DSH export requires DSH 0.1.0-rc.6 or newer and owns only diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 7baa048e2ab..17f424b3317 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -827,6 +827,9 @@ that PID now. Wait for the operation to finish and retry; do not delete the lock process based only on this PID. A later mutation attempt can reclaim a stale lease once its age exceeds 30 seconds and the recorded PID is no longer alive; status only inspects it. +On Windows, this shared Node/Bun diagnostic does not launch an executable to look up the +image name; the recorded PID, liveness, lease age and recovery guidance remain available. + ### `ocx codex-shim ` On macOS and Linux, install a private autostart wrapper at `/bin/codex` and a diff --git a/docs-site/src/content/docs/reference/cli/messaging.md b/docs-site/src/content/docs/reference/cli/messaging.md new file mode 100644 index 00000000000..65df17bd87f --- /dev/null +++ b/docs-site/src/content/docs/reference/cli/messaging.md @@ -0,0 +1,89 @@ +--- +title: Local Codex Messaging +description: Discover loaded local Codex sessions and submit one correlated queued message. +--- + +`ocx message` is an opt-in, command-local surface for existing Codex sessions. +It starts no proxy or app-server, resumes no thread and installs no skill. +Linux/macOS Unix sockets are supported; Windows fails explicitly. The existing +Codex daemon must support experimental `thread/queue/add`. No CLI version pin or +`CODEX_CLI_PATH` selection is required. + +## Discover and send + +```bash +ocx message sessions --json +printf '%s\n' 'Please review the current change and send your findings back.' | + ocx message send --name 'reviewer' --stdin --json +``` + +Discovery returns only loaded thread IDs, names and runtime statuses, not history, +preview or working directories. Use one exact UUID (`--thread`) or a unique exact +name (`--name`). Missing, duplicate, incomplete or unloaded destinations fail; +there is no fuzzy matching or fallback to stored sessions. + +The existing control socket is resolved under effective `CODEX_HOME`, using the +same home policy as other Codex integration commands. Before connecting, the +resolved socket must be owned by your uid and be a socket. Every real parent +through root must be owned by your uid or root and must not be group/other +writable, except root-owned sticky directories such as `/tmp`. Symlinked homes +are allowed when their resolved paths pass. Untrusted paths fail with +`untrusted_socket` and no resolved-path details. + +Discovery, the final loaded-target check and queueing use one connection to that +socket. No native CLI helper, installation, repair or reconnection occurs. A +daemon without local queue support returns `unsupported_queue`. + +Only UTF-8 stdin is accepted, at most 16 KiB of nonempty text without NUL. One +30-second deadline covers input, discovery, revalidation and submission. Each RPC +is capped at 10 seconds. The complete envelope is limited to 32 KiB. Message text +is sent over the Unix RPC connection and never placed in spawned process arguments. + +## Sender and replies + +The wrapper generates a message UUID and attaches sender ID/name from a loaded +`CODEX_THREAD_ID` and daemon metadata. This context is **not authenticated peer +authority** and never grants user approval or escalation. An invalid/unloaded +claimed sender fails; absent context stays unknown, without guessing a reply route. +Peer messages are queued as text, not permission approvals or configuration +overrides. The receiving agent's harness enforces its permissions; envelope +guidance is not a technical authorization boundary or a guarantee of model behavior. + +The default kind is `request`. Answer by using the generated `replyCommand` in +the envelope, or explicitly correlate a response: + +```bash +printf '%s\n' 'Review findings: ...' | + ocx message send --thread --kind response \ + --in-reply-to --stdin --json +``` + +`response` requires `--in-reply-to`; other kinds reject it. Use `--kind notification` +for an FYI. Responses and notifications do not request acknowledgements. A normal +agent final answer is not sent back to its peer; missing routes must not be guessed. +No global prompt changes or acknowledgement loops are introduced. + +## Receipts and exit codes + +`--json` emits one versioned receipt (`ocx-message/1`) containing message UUID, +kind, response correlation, sender/target metadata and submission status. It never +includes body text or raw daemon output. Input/setup failures before a receipt +exists use `ocx-message-error/1` with `not_sent`; discovery failures use the same +error schema without a send status. Invalid usage exits 64 on stderr before I/O. + +| Status | Exit | Meaning | +| --- | --- | --- | +| `not_sent` | 1 | Validation/discovery/revalidation failed, no queue frame was written, or the daemon explicitly rejected it. | +| `queued` | 0 | A correlated daemon response acknowledged submission, **not processing** by the recipient. | +| `unknown` | 3 | A submission may have occurred, but acknowledgement was lost, malformed, mismatched or cancelled. **Do not replay.** | + +The application message UUID is also the queue request's `clientUserMessageId`. +The daemon's submission ID is separate; neither proves recipient processing. +Queueing is not steering and can wait for a busy turn to end. OpenCodex performs +no automatic replay, follow-up probing, persistence or delivery monitoring. +The final loaded-target check and queue submission are not atomic: if the target +unloads afterwards, an explicit rejection is `not_sent` with `queue_rejected`; +a lost or invalid reply is `unknown`. OpenCodex does not resume it or retry. + +No remote hosts, bearer management, SSH, Claude messaging, +dashboard, isolation or idle-notification controls are part of this surface. diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md index 1c9cac81800..3f40362c515 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -246,6 +246,12 @@ when account failover is enabled, the request can retry on another eligible acco Google's account verification, then run `ocx login google-antigravity`. Silent token refresh does not clear this verification requirement. If OpenCodex cannot save the quarantine, this adapter exchange preserves the original `403` without another recovery send; the account has not been durably quarantined. If a sibling request cannot be built or admitted for sending, the exchange delivers the original `403` through normal error formatting. An enclosing combo or policy route can still apply its existing fallback rules. +For fetch-based web-search requests, automatic sibling rotation accepts only a complete, bounded +Google error envelope containing `error.details[].reason === "VALIDATION_REQUIRED"`. Verification +wording alone does not rotate. This request-scoped recovery uses the sibling's token and project +without persisting a `needs-reauth` mark; cancellation, committed output, budget exhaustion or an +unavailable sibling preserves the failure. + A proxy that is already running picks up the new credential without a restart: the CLI asks it to reload that one provider from disk, and the request carries no credential of its own. If the running proxy cannot accept that request — most often because it started from a build that predates diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md index 909edc30e26..06cf68b87c6 100644 --- a/docs-site/src/content/docs/reference/configuration.md +++ b/docs-site/src/content/docs/reference/configuration.md @@ -61,7 +61,7 @@ a known configured model id. Cursor may require a model-list refresh or restart `fastRows` is an optional boolean and defaults to `true`. The raw OpenAI-style `/v1/models` list, Claude Code discovery, and client config exports (including pi, OpenCode, -OMP, Hermes, OpenClaw, Kimi, gjc, DSH, MCode, ZCode, Prime, Aside, Raycast, omo, Cline, and Kilo) add a `--fast` selector for every model whose +OMP, Hermes, OpenClaw, Kimi, gjc, DSH, MCode, ZCode, Prime, Aside, Raycast, omo, Cline, Kilo, and Command Code) add a `--fast` selector for every model whose resolved Fast policy is eligible. Selecting one routes the base model and requests the `priority` service tier — the same Fast the Codex app exposes through its picker toggle. The base row stays listed, so the row is an addition rather than a replacement. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index ce28f9ffaa3..82594b33438 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -478,10 +478,19 @@ optional pin in a hand-edited file is ignored on load without discarding the res Codex reads `auto_review_model_override` from the catalog row of the current turn's model to choose the model that reviews approval requests. The root `auto_review_model` setting in -`$CODEX_HOME/config.toml` applies one reviewer to every catalog row; the provider-scoped fields +`$CODEX_HOME/config.toml` applies one reviewer to task catalog rows; the provider-scoped fields below override it per provider. The [provider guide](/guides/providers/#approval-reviewer-per-provider) has the operator workflow and a worked example. +When native OpenAI rows are included and the final catalog has an ordinary bare native row +other than Reserve (hidden rows count), OpenCodex keeps the hidden `codex-auto-review` row so +Codex can select its preferred approval reviewer when no override is configured. It stays out +of model pickers, subagent choices, Desktop lists and public `/v1/models` lists. Provider and +root reviewer overrides retain their precedence and do not stamp this internal row. Catalogs +without an ordinary bare native row, including Reserve-only catalogs, omit it and preserve +Codex's fallback to the task model. The reviewer receives the same multi-agent mode projection +as ordinary native rows. + `autoReviewModel` is the provider-wide reviewer target. A value can be a bare model id of that same provider (the catalog row is normalized to the `provider/model` slug) or a full public catalog slug such as `opencode-go/deepseek-v4-flash`. A bare value resolves against that provider's rows @@ -935,6 +944,104 @@ Leave this disabled unless you understand Anthropic account policy risk. Prefer `ocx account use anthropic ` switching when unsure. ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2` ("Anthropic · Pool 2") is a second builtin Anthropic OAuth provider with its own +account pool. It runs the same Anthropic implementation as `anthropic`: the same login flow, wire +format, native Messages and Responses bridge, and model metadata. Only the accounts and the pool +around them are separate. Choose a pool with the model prefix: `anthropic/claude-sonnet-5` uses the +primary pool and `anthropic2/claude-sonnet-5` uses Pool 2. + +Pool 2 stays dormant until you add it. Log in with `ocx login anthropic2` or add **Anthropic · Pool 2** +on the dashboard Providers page. A successful first login creates `providers.anthropic2` with the +marker `"anthropicOAuthInstance": "anthropic2"`. Pool 2 never becomes the default provider: bare +`claude-*` model names, the default model, and Claude Code caller forwarding keep resolving to +`anthropic`. An unmarked `anthropic2` entry you created yourself earlier keeps its custom meaning; +login refuses the name collision and rewrites neither the entry nor your credentials. + +Pool 2 reads its pool settings from its own provider entry. The keys, defaults and behavior match +the top-level [`anthropicAccountPool`](#anthropicaccountpool-experimental), and nothing is inherited +from the primary pool. `providers.anthropic.anthropicAccountPool`, or the field on any other provider, +is rejected. + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +The two pools are isolated inside opencodex: + +- **Credentials:** Pool 2 accounts are stored under their own `anthropic2` key in the protected + credential store. A login whose token or verified Anthropic account is already stored in the other + pool is rejected. +- **Pool settings and runtime state:** selection, session affinity, cooldowns, pauses, model routes + and per-account thresholds belong to one pool. Route `fallback: true` widens only within the same + pool. +- **Quota and usage:** usage probes, quota caches and usage attribution are recorded per pool, so an + account ID that exists in both pools still has two separate records. +- **Reset grants:** Pool 2 keeps its own journal (`anthropic2-reset-grant-ledger.json`) next to the + primary pool's unchanged journal. +- **Recovery:** a direct `anthropic2/` request never falls back to the primary pool, and a + Pool 2 rate limit or refusal never cools a primary-pool account. An explicit combo that names both + pools keeps the targets you declared. + +This separation is a routing boundary inside opencodex. It does not change how Anthropic treats your +accounts or the [account policy risk](#anthropicaccountpool-experimental) described above. + +Pool 2 accounts are added only through browser OAuth. Unlike `anthropic`, Pool 2 never imports, +adopts or writes back a Claude Code CLI token; this difference is intentional. Pool 2 starts empty, +and a Pool 2 request with no usable Pool 2 account fails with an authentication error instead of +borrowing a primary-pool or Claude Code credential. + +Account commands and management APIs take the pool by name: `ocx account pool anthropic2 …`, +`ocx account auto-switch anthropic2 …`, `ocx account routes anthropic2 …`, and +`ocx account anthropic-reset-grants --provider anthropic2`. The pool settings and reset-grant +endpoints accept `provider: "anthropic2"`; omitting it keeps the primary pool. + +#### Helper pool selection (`anthropicInstance`) + +The web-search and vision helpers accept an optional `anthropicInstance`, in the global +`webSearchSidecar` and `visionSidecar` settings and in the Claude Code overrides +`claudeCode.webSearchSidecar` and `claudeCode.visionSidecar`. The dashboard shows it as a **Pool** +select when the helper backend is Anthropic. + +| Value | Behavior | +| --- | --- | +| unset (default) | Follow the pool of the current request: an `anthropic2/` request uses Pool 2 and an `anthropic/` request uses the primary pool. A request from another provider keeps the existing helper discovery, which never selects Pool 2. | +| `"anthropic"` | Always use the primary pool. | +| `"anthropic2"` | Always use Pool 2. | + +The field only applies when the helper's backend resolves to Anthropic. Setting it with another +backend is a validation error; because web search defaults to OpenAI, set +`"backend": "anthropic"` as well. A helper model qualified with the other pool, such as +`anthropic/claude-sonnet-5` with `"anthropicInstance": "anthropic2"`, is also rejected. If the chosen +pool has no usable account, the helper fails before sending anything; it does not switch to the other +pool. The main request then continues without that helper. An unset choice is saved as absent, never +as `"anthropic"`. + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[Downgrading] +Versions without Pool 2 do not understand the `anthropic2` entry. Before installing an older +version, stop the proxy, back up `config.json` and `auth.json`, and remove +`providers.anthropic2` from `config.json`. Primary-pool credentials are never moved or rewritten by +Pool 2. An in-place downgrade with an active Pool 2 is not supported. +::: + ### `oauthAccountFailover` Rotates to another logged-in account of the same provider when one is rate-limited, for OAuth @@ -1019,7 +1126,15 @@ The same main dispatch may also switch once on a 403 when Google's bounded error finds a complete structured `VALIDATION_REQUIRED` reason. The 401 and 403 paths share one sibling attempt per request; an unrelated or incomplete 403 keeps its original error. A rejected sibling, cancellation or exhausted send budget does not cause another upstream send. -Continuations, native Responses passthrough, image and web-search sidecars, and output already sent +The fetch-based web-search loop also rotates once on this structured 403 reason, using the sibling's +OAuth token and matching Cloud Code Assist project. It accepts only complete, bounded Google error +envelopes with `error.details[].reason === "VALIDATION_REQUIRED"`; verification wording alone does +not authorize rotation, and this path does not persist a reauthentication mark. Each physical send, +including a 429 retry, consumes the same request budget exactly once. A disabled pool, cancellation, +exhausted budget or unavailable sibling preserves the failure. If sibling request construction or +dispatch fails, the original bounded 403 remains the error; a replay rejected before physical +dispatch returns its unused send allowance. +Continuations, native Responses passthrough, image sidecars, and output already sent to the client do not use this rotation. Current scope is the ordinary Responses request paths. Cursor reports rate limits as adapter diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 1b7c6d1de8f..e0d0535b850 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -151,7 +151,7 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`, | `alias?` | `string` | — | Optional public model id in place of the canonical picker slug. | | `nativeAlias?` | `boolean` | `false` | Let a currently supported bare native id take precedence only for that unqualified id. Bare `gpt-5.6-*` ids use Codex Pool/Direct credentials. Account-qualified routes remain distinct. Provider-qualified routes such as `openai-apikey/gpt-5.6-*` use their configured API-key route and never fall through to the native alias. | | `displayName?` | `string` | — | Display-only catalog label, required and non-empty for a native alias. | -| `decisionProvider?` | `string` | `"jev"` | `strategy: "jev"` only. `"jev"` (the same as omitting it, and stored as omission) is the TypeSafe decision service, valid without a provider row; any other value must name a configured provider with `adapter: "jev-decision"` whose `baseUrl` ends in `/systemone`. | +| `decisionProvider?` | `string` | `"jev"` | `strategy: "jev"` only. `"jev"` (the same as omitting it, and stored as omission) is the TypeSafe decision service, valid without a provider row; any other value must name a configured provider with `adapter: "jev-decision"` and a full HTTPS decision `baseUrl` (any path) or a local HTTP `/systemone` endpoint. Userinfo, query strings, and fragments are refused. | | `decisionModel?` | `string` | unset | `strategy: "jev"` only, mutually exclusive with `decisionProvider`. An ordinary opencodex route (for example `ollama/qwen3:4b`) asked to pick one offered option as JSON. It runs with the selected provider's stored credentials, never the caller's, and cannot resolve to this combo, any JEV combo, or a `jev-decision` row. | | `decisionTimeoutMs?` | `number` | `4000` | `strategy: "jev"` only. Decision deadline before failing open, 1000–120000 ms. | diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 50864587924..b9d367a2a32 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -29,7 +29,7 @@ runs helper features around provider requests. | `usageLedgerMaxBytes?` | `number` | unset | Opt-in ceiling in bytes for `usage.jsonl`. Absent means the request history grows without limit, which stays the default. See [usage history size](#usage-history-size). | | `appOwnedMemoryBudgetMb?` | `number` | `256` | Cap in MiB for evictable app-owned logs, caches, blobs, and continuation payloads. Range 64–4096; not an RSS cap. | | `metricsExport.enabled?` | `boolean` | `false` | Enable process-local aggregate request metrics at authenticated `GET /api/metrics`. Restart required; disabled mode returns 404 and starts no exporter activity. | -| `spend?` | `{ root?: { maxTokens?: number }; identity?: { maxTokens?: number }; pool?: { maxTokens?: number }; retentionDays?: number }` | unset | Durable token ceilings, off unless you write one. Each scope bounds settled spend plus in-flight reservations plus unresolved spend: `root` is one task including its whole fan-out, `identity` is one account across every task it serves, and `pool` is one provider pool. They intersect, so a request is admitted only when all three have room — which is what holds a ceiling against a client that mints a new task id per request. A reservation is the request's whole input plus its enforceable output ceiling, counted as if every cached prefix misses. Observe-only mode still journals, so every server owns the state directory's single-writer lease; an explicit sibling must use a separate `OPENCODEX_HOME`. Spend survives an ordinary process restart when its writes reached the filesystem, but the journal does not promise survival across host power loss because each append is not fsynced. Raising or removing the value is what grants more. `maxTokens` must be a positive integer (0 would refuse everything), `retentionDays` is 1–365 and defaults to 7, and an unknown key in this section is rejected rather than ignored. A refusal is a local HTTP 429 carrying `x-opencodex-local-refusal: workflow_spend_exhausted`, and its message names the scope and the ceiling; no provider is contacted. With an applicable ceiling, dispatch is also refused if its token reservation cannot be booked, including full tracking capacity. Requests without an applicable ceiling remain observe-only. | +| `spend?` | `{ root?: { maxTokens?: number }; identity?: { maxTokens?: number }; pool?: { maxTokens?: number }; retentionDays?: number }` | unset | Durable token ceilings, off unless you write one. Each scope bounds settled spend plus in-flight reservations plus unresolved spend: `root` is one task including its whole fan-out, `identity` is one account across every task it serves, and `pool` is one provider pool. They intersect, so a request is admitted only when all three have room — which is what holds a ceiling against a client that mints a new task id per request. A reservation is the request's whole input plus its enforceable output ceiling, counted as if every cached prefix misses. Observe-only mode still journals, so every server owns the state directory's single-writer lease; an explicit sibling must use a separate `OPENCODEX_HOME`. Spend survives an ordinary process restart when its writes reached the filesystem, but the journal does not promise survival across host power loss because each append is not fsynced. Raising or removing a ceiling changes the allowance for requests started afterward; requests already running keep their starting policy. `maxTokens` must be a positive integer (0 would refuse everything), `retentionDays` is 1–365 and defaults to 7, and an unknown key in this section is rejected rather than ignored. A refusal is a local HTTP 429 carrying `x-opencodex-local-refusal: workflow_spend_exhausted`, and its message names the scope and the ceiling; no provider is contacted. With an applicable ceiling, dispatch is also refused if its token reservation cannot be booked, including full tracking capacity. Requests without an applicable ceiling remain observe-only. | | `codexAutoStart?` | `boolean` | `true` | Let the Codex shim run `ocx ensure` before launching Codex. False makes ensure a no-op. | | `codexShimAutoRestore?` | `boolean` | `true` | Restore an installed shim after a completed external Codex update replaces it. Environment opt-out: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. | @@ -42,6 +42,7 @@ runs helper features around provider requests. | `codexMainAccountHardLockThresholds?` | `{ short?: number; long?: number }` | `{ short: 90, long: 98 }` | Independent 5h and weekly/monthly thresholds. Integers 80–100, with `short <= long`. Invalid persisted fields fall back to defaults; the settings API rejects invalid writes before mutation. | | `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another's request record. Logs carry a hashed account key only. | | `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility. Original metadata is backed up and restored by `ocx stop` / `ocx restore`. | +| `spendPoolAliases?` | `Record` | unset | Explicit historical salted pool-alias to canonical provider mappings. Keep this key at the top level, outside `spend`. See [Historical pool continuity](#historical-pool-continuity). | | `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` | off | Redirect recognized Codex helper/shadow calls to a chosen model while preserving the request's configured reasoning effort. The default source prefixes are `gpt-6-luna` and `gpt-5.6-luna`; older clients through 0.144.x used `gpt-5.4-mini`, which `sourceModels` can restore. | | `memoryModels?` | `{ extract?: { model: string; reasoningEffort?: string }; consolidation?: { model: string; reasoningEffort?: string } }` | off | Route Codex's two memory phases to a chosen model, with an optional reasoning effort per phase. See [Memory routing](#memory-routing). | | `webSearchSidecar?` | `OcxWebSearchSidecarConfig` | on when usable | Web-search sidecar options. | @@ -940,3 +941,90 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available. The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. + +## Historical pool continuity + +Provider-pool ceilings use the canonical routed provider. For every currently configured provider +`P`, OpenCodex automatically binds its salted pool alias, `h(pool, P)`, to `P` when reading +balances. No manual self-mapping or salt calculation is needed for independent +provider ceilings. This rule also applies to retained historical balances with that exact alias. + +Request logs still use account-specific display labels. A different historical label, including an +account ordinal, does not establish provider ownership. With `spend.pool.maxTokens`, each original +balance contributes once to its bound provider group or, when still unbound and positive, to every +candidate pool. Settled, reserved and unresolved usage all count; unknown usage is not a refund. +Unbound history may therefore restrict an otherwise unused pool. Current account order and display +names do not resolve that history. Root and identity ceilings remain independent. + +For distinct historical labels whose ownership you have verified, add mappings to the top-level +`spendPoolAliases` object in `config.json`. Each key is an exact 32-character lowercase hexadecimal +pool alias from this installation's journal; each value is the exact ID of a verified, currently +configured canonical provider, with no surrounding whitespace. +Keep evidence for the mapping and keep the journal and salt private. + +Automatic self-bindings and explicit mappings apply only when balances are read. They do not move +original balances or persist identity links in the journal. Removing an explicit historical mapping +makes that history unbound again unless its alias is a currently configured provider's own alias. +Keep `spendPoolAliases` outside `spend`, because older versions reject unknown fields inside that +strict section. Invalid mappings are rejected on writes; invalid hand edits retain ceilings and +refuse pool admission. A configured provider's own salted pool alias cannot map to another provider. +This is checked again when the provider set changes. Validation reads an existing salt without +creating one. + +Dormant unbound history expires only after its last activity is strictly older than +`spend.retentionDays`. Capacity pressure cannot shorten that interval for positive unknown history. +Live reservations and seed targets remain protected, even at zero tokens. Cleanup persists a normal +journal deletion record before admission uses the reduced total. Known groups keep the ordinary +active/exhausted protections. No automatic tool reconstructs unverifiable mappings. + +### Token reservations and reporting + +When a root, identity or pool ceiling applies, the first physical send for each selected target/key +requires a normal-capacity reservation. If that cannot be recorded, the send is refused before it +reaches upstream. Stable retries reuse that reservation's scopes; a different target/key needs a +new one. `L` is the request-wide physical-send limit, frozen when the request starts +(default four; the existing bounded OAuth request profile allows up to eighteen). +Every additional physical send shares this finite limit. + +The enforcement state, applicable root/identity/pool token ceilings, and `L` are captured when each +request starts. Configuration changes apply only to requests started after the change. Requests +already running keep their starting policy for all retries and continuations: enabling or lowering +a ceiling does not tighten them, and raising or removing a ceiling does not relax them. A request +that started in observe-only mode remains observe-only until it finishes. + +Delayed reports retain already-started usage. Terminal settlement waits for reporters, and send IDs +are forgotten only after their accounting is durable; forgetting an ID does not remove its balance. +The limit bounds retries, not the final bill: actual usage or already-started retries can exceed a +token estimate, and their full liability remains counted. With no applicable ceiling, behavior stays +observe-only, including omitted bookings when tracking capacity is full. No identity checkpoints are +added for unconfigured traffic. + +Claude CLI, CodeBuddy and Qoder count each CLI invocation as one send. Their first invocation +still needs a normal reservation and is refused if that reservation cannot be obtained. Internal +CLI retries and tool turns do not separately consume this request-wide send limit; their cost +settles from actual reported usage, including amounts above the initial estimate. This exception preserves CLI use with configured +spend ceilings while limiting the number of child invocations. + +### Rollback to 2.80.0: contract C + +New records use the existing v1 format and `pool` alias domain. **Unmodified 2.80.0 reads them using +its own per-label rules**, including ordinary compaction and retention. There is no required +backport, launcher fence or reconciliation command. Keep the current journal and salt: restoring +an older copy loses the spend recorded since that copy. + +This does not preserve canonical aggregation after downgrade, nor promise the same remaining +allowance that 2.80.0 would have calculated for identical traffic without the upgrade. It may book +new traffic under account labels. On re-upgrade, current configured-provider self-bindings and explicit +alias mappings apply to the original balances still retained by 2.80.0. New checkpoints contain no identity-link metadata that +an older reader must preserve. + +Journals written by unpublished experimental builds with `pool-current` aliases or `poolContinuity` +metadata are excluded from this compatibility contract, including later checkpoints carrying those +aliases. Their original balances and send targets remain conservative unbound history until normal +retention expires them; they are not converted or immediately deleted. + +A complete invalid journal record (including a final `null`) causes configured admission to refuse. +Compaction can still preserve valid accounting in a clean checkpoint; the live refusal remains until +a clean restart replays it. A torn, unparseable final line keeps the existing recovery behavior. +Storage/corruption denials use `workflow_spend_undurable`; unsafe files and ownership failures remain +storage errors. Alias validation failures use `workflow_pool_history_unresolved`. diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 3bd3700766c..5b7e28afe56 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -662,6 +662,23 @@ whether to star the repository. ### System lifecycle +`POST /api/system/restart` keeps the 60-second drain by default, including requests +with no body or `{}`. An authenticated caller can explicitly send a JSON body such +as `{"drainGraceMs":2000}` to choose a shorter active-request/scoped-drain grace. +The value must be an integer from 1 to 60000 milliseconds. Invalid bodies or values +return 400 before starting a drain; the standard management-body limit still applies. +The target-bound local restart capability retains the default grace; setting this +body option requires a management session or admin token (otherwise 403). +The response's `drainTimeoutMs` reports the accepted grace. Repeated calls retain +the first accepted restart's grace and do not change its deadline. + +Grace is measured from acceptance, including the response-flush delay. The cleanup +watchdog remains 60 seconds and replacement readiness retains its separate 70-second +budget. A short grace can interrupt a turn that already executed upstream. Check its +outcome before resubmitting; OpenCodex adds no automatic replay of ambiguous work, +and a healthy replacement does not prove resending is safe. The dashboard, CLI, tray +and automatic restarts continue using the 60-second default. + During a restart drain, new data-plane requests receive HTTP 503 with JSON `error.type: "server_error"`, `error.code: "server_restarting"`, and the message "OpenCodex is restarting; retry this request." Responses retain `Retry-After: 5` @@ -671,7 +688,7 @@ the 503 without reporting model capacity; provider overload errors retain their | Method and path | Purpose | Notable errors | | --- | --- | --- | | `GET /api/system/memory` | Return scalar process, heap, stream, response-state, watchdog, and active-turn metrics. Response-state diagnostics include spill-write status, consecutive failures, fixed privacy-safe failure class, and last failure/success timestamps. `spillLastWriteFailureOrigin` is `retry_returned_timeout`, `timeout_memo_refusal`, or null; cumulative `spillAclRetryReturnedTimeouts` and `spillAclTimeoutMemoRefusals` count terminal failed publications. See [Windows spill diagnostics](/troubleshooting/windows-memory/) for process-local semantics. Raw errors and paths are never returned. | — | -| `POST /api/system/restart` | Begin a drain-aware process restart without removing client injection | Returns 202; repeated calls report the existing drain | +| `POST /api/system/restart` | Begin a drain-aware process restart without removing client injection; optional JSON `drainGraceMs` explicitly shortens the default 60s grace | Returns 202; repeated calls report the existing grace; 400 invalid body or grace | | `POST /api/stop` | Stop the service, restore native Codex, remove managed Grok injection, and drain the proxy | 409 service ownership conflict; 409 `respawnable_service` when a Windows Task Scheduler wrapper could respawn the proxy and the caller is not `ocx stop` (nothing is changed); 409 `self_unload_service` when this proxy is running as the installed launchd/systemd service, because stopping the manager from inside it would end the process before native Codex is restored — run `ocx stop` instead (nothing is changed); 409 when the installed manager refuses to stop; 409 `service_state_unknown` when the Task Scheduler state cannot be read (nothing is changed; repair the query and retry) | | `GET /api/system/codex-app-server` | Report whether running Codex app-servers predate the current model catalog | — | | `POST /api/system/codex-restart` | Refresh the catalog, then restart stale Codex app-servers and fully quit and relaunch the Codex desktop app so the model picker reloads. When the proxy itself is running inside the Codex app, the desktop restart is refused rather than handed off. | Returns 200 with `code: partially_stopped` when a target survives | diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index 2fb63737b72..e16a7823988 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -355,6 +355,11 @@ When available, `input_tokens_details` can also include `cache_write_tokens`. Th detail objects are a compatibility guarantee for strict Responses clients; zero can mean "not reported," not necessarily "the provider performed no such work." +For Anthropic upstreams, a reported `usage.output_tokens_details.thinking_tokens` is preserved as +Responses `output_tokens_details.reasoning_tokens` and Chat `completion_tokens_details.reasoning_tokens`, +for JSON and streaming replies. It is already included in the output-token total, not added again; +missing or invalid detail is not estimated from visible thinking text. See [Anthropic's usage contract](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost). + ### Correlating a response with its request log Every admitted HTTP Responses reply carries an `x-opencodex-request-id` header holding a diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index 3662e395830..16108d8d67a 100644 --- a/docs-site/src/content/docs/ru/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` проведёт вас по следующим шагам: -1. **Выбор провайдера** — выберите один из 102 встроенных пресетов реестра или `custom`, чтобы +1. **Выбор провайдера** — выберите один из 103 встроенных пресетов реестра или `custom`, чтобы ввести базовый URL и адаптер вручную. 2. **API-ключ** — вставьте ключ или сошлитесь на переменную окружения вида `${ANTHROPIC_API_KEY}`. 3. **Модель по умолчанию** — для провайдеров с ключом, локальных и `custom` примите значение из diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index d96d99cd490..2248e9647fa 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -646,7 +646,7 @@ Responses `web_search_call` в парные блоки Anthropic `server_tool_us ``, а не аргумент `model` инструмента Agent. Убедитесь, что директива соответствует нужному маршруту. В качестве плейсхолдера модели передавайте `"haiku"`. -Параметр `claudeCode.stabilizePromptCache: true` в `config.json` переносит поддерживаемые уведомления Claude в конце системных инструкций в последнее пользовательское сообщение на маршрутах с преобразованием. По умолчанию он выключен (`false`). Включайте его только когда такое изменение роли допустимо для ваших клиентов. Примеры в блоках кода и нераспознанный текст сохраняются; нативная передача Anthropic не меняется. Без метаданных ключ кэша рассчитывается по стабилизированным инструкциям. Идентификатор разговора не создаётся, попадания в кэш не гарантируются. +Параметр `claudeCode.stabilizePromptCache: true` в `config.json` отделяет поддерживаемые уведомления Claude от конца системных инструкций на маршрутах с преобразованием. Распознанные блоки вида `N tokens left` удаляются, в том числе при повторении. Напоминания TaskCreate по-прежнему переносятся в пользовательское сообщение в конце; если отделены только блоки с остатком токенов, входное сообщение не добавляется. По умолчанию он выключен (`false`). Включайте его только когда такое изменение роли допустимо для ваших клиентов. Примеры в блоках кода и нераспознанный текст сохраняются; нативная передача Anthropic не меняется. Без метаданных ключ кэша рассчитывается по стабилизированным инструкциям. Идентификатор разговора не создаётся, попадания в кэш не гарантируются. На всех преобразованных Chat-маршрутах напоминания в истории сохраняют свою позицию в разговоре — после ожидаемых результатов инструментов. Поэтому добавление нового напоминания не переписывает начальный системный промпт, а инструкция из середины разговора не оказывается раньше тех ходов, после которых она была написана. Роль этой позиции определяется отдельно: напоминание отправляется как `system`, если провайдер не записал `foldDeveloperRoleToSystem: false` — эта запись означает, что вышестоящий сервис принимает роль `developer`, и тогда она передаётся в той же позиции. Сервис, который её не принимает, отвечает `400 role 'developer' is not allowed`, и ход не начинается, поэтому незаписанное назначение сворачивается. Это работает независимо от `stabilizePromptCache`; нативная передача Anthropic остаётся прежней. Для повторного использования кэша по-прежнему нужны стабильный идентификатор сессии и доступный кэш провайдера. Изменения прежних инструкций или инструментов и сжатие разговора также могут влиять на попадания в кэш; само сохранение порядка напоминаний не гарантирует повторного использования. diff --git a/docs-site/src/content/docs/ru/guides/integrations.md b/docs-site/src/content/docs/ru/guides/integrations.md index 6f46dcd8282..dc1c432498c 100644 --- a/docs-site/src/content/docs/ru/guides/integrations.md +++ b/docs-site/src/content/docs/ru/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Интеграции -description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo и Factory Droid из дашборда — отдельный переключатель для каждого клиента и резервная копия перед каждой записью. +description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code и Factory Droid из дашборда — отдельный переключатель для каждого клиента и резервная копия перед каждой записью. --- Вкладка **Подключение** записывает блок провайдера opencodex в собственный файл -конфигурации клиента и при необходимости удаляет его. Так работают семнадцать +конфигурации клиента и при необходимости удаляет его. Так работают восемнадцать клиентов, у каждого свой переключатель: | Клиент | Файл конфигурации | Формат | Когда изменение начинает действовать | Учётные данные | @@ -25,6 +25,7 @@ description: Подключайте opencodex к OpenCode, Pi, OMP, Hermes, Open | omo | `~/.omo/agent/models.json` | JSON | в новых сессиях | заглушка для loopback | | Cline CLI | `~/.cline/data/settings/providers.json` и соседний `models.json` | пара JSON | после остановки и повторного запуска Cline | заглушка для loopback | | Kilo | первый существующий файл среди `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` и `config.json` в `~/.config/kilo` (`XDG_CONFIG_HOME` переносит каталог; если файлов нет, создаётся `kilo.jsonc`) | JSONC | в новых сессиях | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (в Windows при использовании домашнего каталога по умолчанию: использовать `HOME ?? USERPROFILE`, только если значение не пустое, не состоит из пробелов и является абсолютным путём Windows, иначе `homedir()`; отдельно переданный домашний каталог сохраняется; добавить `.commandcode\providers.json`) | JSON | при следующем запуске Command Code | нет — loopback без ключа (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` в Windows) | JSON | сразу при изменении файла | loopback без ключа | Создаваемые каталоги включают только модели, включённые в настройках каждого diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 606d4739e0b..7317ddafbc4 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -206,7 +206,7 @@ Inline JSON и лишние позиционные аргументы откло ## 3. Каталог API-ключей -opencodex поставляется с 102 встроенными пресетами: 84 на основе ключей, 14 OAuth, три локальных и +opencodex поставляется с 103 встроенными пресетами: 84 на основе ключей, 15 OAuth, три локальных и один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера. Наиболее заметные записи: diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index e5d83921b22..40b8b0afc3d 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -127,6 +127,28 @@ bun run dev:gui Контур управления панели отделён от прямого трафика client→hub. **Подключение → Ключи API** показывает ожидающую ротацию, отображает новый секрет один раз и требует явного подтверждения или отмены. Logout браузера отзывает только текущую сессию. При подключении статистика hub фильтруется по `apiKeyId`; после отключения используется локальная, без зеркалирования. +### Просмотр сохранённого API-ключа + +Для просмотра полного значения сохранённого ключа нужна сессия панели, выданная после явного +сопряжения или аутентификации через доверенную учётную запись Tailscale. Автоматическая локальная +сессия позволяет просматривать маскированный список, но не раскрывать сохранённые значения. +Административный токен также не подходит для этой операции, доступной только сессиям. +Используйте существующий процесс сопряжения с Remote Hub или доверенный вход Tailscale, если он доступен. + +Локальная форма сопряжения предлагается только в автономной панели с тем же origin, открытой +по HTTP на буквальном адресе `127.0.0.1` или `[::1]`. Origin должен соответствовать настроенному +loopback IP-адресу привязки сервера. Если панель открыта через `localhost` или его псевдоним, +проверьте привязку: для `127.0.0.1` откройте `http://127.0.0.1:`, а для `::1` — +`http://[::1]:`, затем выполните сопряжение. При hostname `localhost` подходящего origin для +локального сопряжения нет. Панель не может определить привязку по URL с псевдонимом, поэтому +показывает общий отказ без предложения другого URL. + +После сопряжения снова нажмите на ключ, чтобы запросить его значение. Само сопряжение ключ не раскрывает. +При очистке или замене общей сессии, начале сопряжения, выходе из панели, скрытии вкладки браузера +или окна приложения либо смене сервера показанные значения и состояние копирования очищаются. +Это относится и к значениям новых и заменённых ключей, отображаемым один раз. При истечении срока +или отзыве авторизованной сессии выполните сопряжение заново. + Селектор предлагает включённые нативные и маршрутизируемые модели, а также глобальную шкалу уровней рассуждений Codex. API валидирует выбранный уровень глобально; Codex дополнительно валидирует уровень порождения по целевой записи каталога. diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md index 545181350e4..119187c09ef 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -166,7 +166,7 @@ override, но файлы на диске никогда не меняются. ## Экспорт client config -### `ocx export --client ` +### `ocx export --client ` Печатает client config, направленный на работающий прокси. Команда сериализует блок провайдера `opencodex` в нативном формате выбранного клиента: base URL, список моделей и, @@ -177,7 +177,7 @@ override, но файлы на диске никогда не меняются. | Флаг | Действие | | --- | --- | -| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | +| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | | `--json` | Печатать только JSON-конфиг в stdout, чтобы redirect сохранял побайтно точный вывод. Вся диагностика, включая заметку о записи через `--out`, идёт в stderr. | | `--out ` | Записать конфиг в ``. Перезаписывать существующий файл не позволит. | | `--force` | Разрешить `--out` заменить существующий файл. | @@ -211,6 +211,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml` одинаково на macOS и Windows (Raycast не учитывает `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | нет — только loopback, запись `api_keys` не создаётся | | `omo` | `~/.omo/agent/models.json` (`OMO_CODING_AGENT_DIR`, затем `SENPI_CODING_AGENT_DIR`, затем `PI_CODING_AGENT_DIR` имеют приоритет в этом порядке, если заданы; относительное значение отклоняется) | `omo-models.json` | нет — loopback placeholder | | `kilo` | первый существующий файл среди `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` или `config.json` в `~/.config/kilo` (`XDG_CONFIG_HOME` переносит каталог); если ни одного нет, используется `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (в Windows при использовании домашнего каталога по умолчанию: использовать `HOME ?? USERPROFILE`, только если значение не пустое, не состоит из пробелов и является абсолютным путём Windows, иначе `homedir()`; отдельно переданный домашний каталог сохраняется; добавить `.commandcode\providers.json`) | `providers.json` | только loopback; `apiKey: false`; переменная окружения не нужна | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | только loopback; переменная окружения не нужна | Экспорт для Raycast — это отдельный документ `providers.yaml` с одним элементом `id: opencodex` в diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index 35f25ae4f52..83501986f90 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -269,6 +269,65 @@ reauth или эффективного порога исчерпания это `ocx account use anthropic `. ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2` («Anthropic · Pool 2») — второй встроенный OAuth-провайдер Anthropic со своим пулом аккаунтов. Он работает на той же реализации Anthropic, что и `anthropic`: тот же вход, формат запросов, нативные Messages и мост Responses, те же метаданные моделей. Отдельны только аккаунты и пул вокруг них. Пул выбирается префиксом модели: `anthropic/claude-sonnet-5` использует основной пул, а `anthropic2/claude-sonnet-5` — Pool 2. + +Pool 2 неактивен, пока вы его не добавите. Войдите командой `ocx login anthropic2` или добавьте **Anthropic · Пул 2** на странице Providers в dashboard. Первый успешный вход создаёт `providers.anthropic2` с маркером `"anthropicOAuthInstance": "anthropic2"`. Pool 2 никогда не становится провайдером по умолчанию: имена моделей `claude-*` без префикса, модель по умолчанию и пересылка вызывающего Claude Code по-прежнему разрешаются в `anthropic`. Созданная вами ранее запись `anthropic2` без маркера сохраняет свой пользовательский смысл; вход отклоняет конфликт имён и не переписывает ни запись, ни ваши учётные данные. + +Pool 2 читает настройки пула из собственной записи провайдера. Ключи, значения по умолчанию и поведение совпадают с описанным выше `anthropicAccountPool` верхнего уровня, и ничего не наследуется от основного пула. `providers.anthropic.anthropicAccountPool`, как и это поле у любого другого провайдера, отклоняется. + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +Внутри opencodex два пула изолированы: + +- **Учётные данные:** аккаунты Pool 2 хранятся под собственным ключом `anthropic2` в защищённом хранилище учётных данных. Вход, чей токен или подтверждённый аккаунт Anthropic уже сохранён в другом пуле, отклоняется. +- **Настройки и состояние выполнения:** выбор, закрепление сессий, cooldown, паузы, маршруты моделей и пороги отдельных аккаунтов принадлежат одному пулу. `fallback: true` в маршруте расширяет выбор только внутри того же пула. +- **Квота и использование:** проверки использования, кеши квот и учёт использования ведутся по пулам, поэтому ID аккаунта, существующий в обоих пулах, имеет две отдельные записи. +- **Гранты сброса:** Pool 2 ведёт собственный журнал (`anthropic2-reset-grant-ledger.json`) рядом с неизменным журналом основного пула. +- **Восстановление:** прямой запрос `anthropic2/` никогда не переходит на основной пул, а rate limit или отказ в Pool 2 никогда не отправляет аккаунт основного пула в cooldown. Явный combo, указывающий оба пула, сохраняет объявленные цели. + +Это разделение — граница маршрутизации внутри opencodex. Оно не меняет того, как Anthropic относится к вашим аккаунтам, и не снимает описанных выше рисков политики аккаунтов. + +Аккаунты Pool 2 добавляются только через OAuth в браузере. В отличие от `anthropic`, Pool 2 никогда не импортирует, не перенимает и не записывает обратно токен Claude Code CLI; это различие намеренное. Pool 2 начинается пустым, и запрос к Pool 2 без пригодного аккаунта Pool 2 завершается ошибкой аутентификации, а не занимает учётные данные основного пула или Claude Code. + +Команды аккаунтов и management API указывают пул по имени: `ocx account pool anthropic2 …`, `ocx account auto-switch anthropic2 …`, `ocx account routes anthropic2 …` и `ocx account anthropic-reset-grants --provider anthropic2`. Endpoint'ы настроек пула и грантов сброса принимают `provider: "anthropic2"`; без него используется основной пул. + +#### Выбор пула для помощников (`anthropicInstance`) + +Помощники веб-поиска и vision принимают необязательное поле `anthropicInstance` в глобальных настройках `webSearchSidecar` и `visionSidecar` и в переопределениях Claude Code `claudeCode.webSearchSidecar` и `claudeCode.visionSidecar`. В dashboard оно показывается как список **Пул аккаунтов**, когда backend помощника — Anthropic. + +| Значение | Поведение | +| --- | --- | +| не задано (по умолчанию) | Следовать пулу текущего запроса: запрос `anthropic2/` использует Pool 2, а запрос `anthropic/` — основной пул. Запрос от другого провайдера сохраняет существующий поиск помощника, который никогда не выбирает Pool 2. | +| `"anthropic"` | Всегда основной пул. | +| `"anthropic2"` | Всегда Pool 2. | + +Поле действует, только когда backend помощника разрешается в Anthropic. Указать его с другим backend — ошибка валидации; поскольку веб-поиск по умолчанию использует OpenAI, задайте также `"backend": "anthropic"`. Модель помощника с префиксом другого пула, например `anthropic/claude-sonnet-5` вместе с `"anthropicInstance": "anthropic2"`, тоже отклоняется. Если в выбранном пуле нет пригодного аккаунта, помощник завершается ошибкой до какой-либо отправки и не переключается на другой пул. Основной запрос при этом выполняется без этого помощника. Незаданный выбор сохраняется как отсутствующий, а не как `"anthropic"`. + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[Откат версии] +Версии без Pool 2 не понимают запись `anthropic2`. Перед установкой более старой версии остановите прокси, сделайте резервную копию `config.json` и `auth.json` и удалите `providers.anthropic2` из `config.json`. Pool 2 никогда не перемещает и не переписывает учётные данные основного пула. Откат на месте при активном Pool 2 не поддерживается. +::: + ### Формы управляемых записей В `apiKeys[]` записи содержат `id`, `name`, сгенерированный `key` и ISO-строки `createdAt`. diff --git a/docs-site/src/content/docs/ru/reference/configuration/server.md b/docs-site/src/content/docs/ru/reference/configuration/server.md index 6ca105b9dc9..9795c728847 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/server.md +++ b/docs-site/src/content/docs/ru/reference/configuration/server.md @@ -248,6 +248,58 @@ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agen The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. -## Резервирование токенов и лимиты - -Если к запросу применяется `spend.root.maxTokens`, `spend.identity.maxTokens` или `spend.pool.maxTokens`, отправка отклоняется, когда резерв токенов нельзя учесть, в том числе из-за заполнения хранилища отслеживаемых записей. Запросы без применимого лимита остаются в режиме наблюдения. +## Непрерывный учёт истории пулов + +Лимиты пула относятся к каноническому провайдеру маршрута, а не к отображаемой метке аккаунта. +Для каждого настроенного сейчас провайдера `P` его солёный псевдоним пула `h(pool, P)` автоматически +связывается с `P` при чтении балансов. Это касается и сохранённой истории с точно таким же псевдонимом. +Независимые лимиты провайдеров не требуют ручного назначения собственных псевдонимов или вычислений с солью. + +Другие исторические метки, включая метки с порядковым номером аккаунта, автоматически не связываются. +Для истории с подтверждённым владельцем верхнеуровневый `spendPoolAliases` в `config.json` задаёт связь +точного псевдонима пула (32 строчных шестнадцатеричных символа из журнала этой установки) с точным ID +проверенного и настроенного сейчас провайдера, без пробелов по краям. +Текущие имена и порядок аккаунтов не доказывают владельца истории. Храните журнал и соль в секрете. + +Автоматические и явные связи применяются только при чтении: исходные балансы не перемещаются, +идентификационные связи в журнал не записываются. Удаление явной связи снова делает историю неопознанной, +если её псевдоним не принадлежит самому настроенному сейчас провайдеру. Каждый исходный баланс +учитывается один раз в связанной группе; неопознанный положительный баланс консервативно добавляется +один раз к каждому возможному пулу. Псевдоним самого настроенного провайдера нельзя назначить другому +провайдеру. Проверка повторяется при добавлении провайдеров и не создаёт соль. +Некорректное соответствие сохраняет лимиты и блокирует допуск к пулу. + +Неактивная неопознанная история истекает только когда её последняя активность строго старше +`spend.retentionDays`. Нехватка места не сокращает этот срок для положительных балансов. +Активные резервирования и seed сохраняются даже при нулевой оценке; уменьшение баланса требует +сначала записать удаление в журнал. При применимом лимите первая отправка для каждой цели/ключа +требует обычного резервирования. Повторы используют те же области. `L` — общий лимит физических отправок +одного запроса, фиксируемый при запуске запроса. Обычно он равен 4; существующий профиль запроса OAuth +допускает до 18. Все физические отправки делят этот конечный лимит. + +Режим применения ограничений, действующие токенные лимиты корня, идентичности и пула, а также `L` +фиксируются при запуске запроса. Изменение конфигурации применяется только к запросам, начатым после него. +Уже выполняющийся запрос сохраняет исходную политику для всех повторов и продолжений: включение или +снижение лимита не ужесточает её, а повышение или удаление не ослабляет. Запрос, начатый в режиме +наблюдения, остаётся в этом режиме до завершения. +Окончательный расчёт ждёт отчётов; удаление ID отправки не удаляет баланс. Без применимого лимита +сохраняется режим наблюдения, включая пропуск записей при заполненной ёмкости отслеживания. + +Для Claude CLI, CodeBuddy и Qoder один вызов CLI считается одной отправкой и требует обычного резерва до запуска. +Внутренние повторы и инструментальные ходы CLI отдельно не расходуют общий лимит отправок запроса, но стоимость учитывается по фактически сообщённому использованию, включая превышение первоначальной оценки. + +### Откат к 2.80.0: контракт C + +Новые записи используют обычный формат v1 и псевдонимы `pool`. Неизменённая 2.80.0 читает их, +уплотняет журнал и применяет срок хранения по своим правилам отдельных меток. Сохраните текущие +журнал и соль. После отката не гарантируется каноническая агрегация или тот же остаток лимита, +который 2.80.0 вычислила бы для того же трафика без обновления. При повторном обновлении автоматические +связи настроенных сейчас провайдеров и явные соответствия применяются к сохранившимся исходным балансам. Барьер запуска и команда сверки не требуются. +История неопубликованных экспериментальных сборок с `pool-current` / `poolContinuity` исключена +из контракта C. Исходные балансы сохраняются как неопознанная история до обычного истечения срока; +немедленного преобразования или удаления нет. + +Полная, но некорректная запись (включая последний `null`) блокирует запросы с лимитом. +Уплотнение продолжает сохранять корректные балансы; отказ живого процесса сохраняется до перезапуска +с чистым журналом. Оборванная последняя строка JSON обрабатывается по прежним правилам восстановления. +Подробности и коды ошибок: [английский оригинал](/reference/configuration/server/#historical-pool-continuity). diff --git a/docs-site/src/content/docs/ru/reference/management-api.md b/docs-site/src/content/docs/ru/reference/management-api.md index 77b456ab70c..209c7ffdf05 100644 --- a/docs-site/src/content/docs/ru/reference/management-api.md +++ b/docs-site/src/content/docs/ru/reference/management-api.md @@ -338,6 +338,8 @@ Management-аутентификация доказывает доступ к п ### Жизненный цикл системы +`POST /api/system/restart` сохраняет стандартный период drain в 60 секунд, если тело отсутствует или равно `{}`. Сессия управления или токен администратора позволяют явно выбрать более короткий период через `{"drainGraceMs":2000}`. Значение должно быть целым числом от 1 до 60000 миллисекунд. Некорректное тело или значение возвращает 400 без запуска перезапуска; локальная capability, привязанная к целевому процессу, не разрешает этот параметр (403). Поле ответа `drainTimeoutMs` сообщает первый принятый период, который повторные вызовы не меняют. Отсчёт включает задержку отправки ответа; отдельные бюджеты очистки и готовности замены остаются равными 60 и 70 секундам. Прерванный запрос мог уже выполниться: проверьте результат перед повторной отправкой. Автоматическая повторная отправка не добавляется. + | Метод и путь | Назначение | Особые ошибки | | --- | --- | --- | | `GET /api/system/memory` | Вернуть скалярные метрики процесса, heap, stream, response-state, watchdog и active-turn | — | diff --git a/docs-site/src/content/docs/tr/contributing.md b/docs-site/src/content/docs/tr/contributing.md index 4189a7ad5b6..ff5c86f2cb3 100644 --- a/docs-site/src/content/docs/tr/contributing.md +++ b/docs-site/src/content/docs/tr/contributing.md @@ -7,8 +7,8 @@ description: opencodex geliştirme — kurulum, düzen, kurallar ve yeni bir sa Kaynak kod üzerinde geliştirme yapmak için `PATH` ortam değişkeninizde `bun` CLI aracının bulunması gerekir. Yayınlanan npm paketi kullanıcılar için kendi Bun -çalışma zamanını paketler, ancak bu depodaki betikler yerel Bun kurulumunuz -üzerinden çalışır. +çalışma zamanını paketler; paket betikleri Bun'u bu paketlenmiş bağımlılıktan +bulabilir. ```bash git clone https://github.com/lidge-jun/opencodex.git diff --git a/docs-site/src/content/docs/tr/getting-started/quickstart.md b/docs-site/src/content/docs/tr/getting-started/quickstart.md index 955ff8c0f68..0d707f5e0ee 100644 --- a/docs-site/src/content/docs/tr/getting-started/quickstart.md +++ b/docs-site/src/content/docs/tr/getting-started/quickstart.md @@ -14,7 +14,7 @@ ocx init `ocx init` adım adım size rehberlik eder: -1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 102 önayardan birini +1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 103 önayardan birini veya bir temel URL ile adaptör yazmak için `custom` seçeneğini belirleyin. 2. **API anahtarı** — bir anahtar yapıştırın veya `${ANTHROPIC_API_KEY}` gibi bir ortam değişkenine başvurun. diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 04e4d331fb2..607937289a1 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -864,7 +864,7 @@ aracının `model` argümanını değil, `` yönergelerin kullanır. Yönergenin hedeflenen rotayla eşleştiğinden emin olun. Model yer tutucusu olarak `"haiku"` iletin. -`config.json` içindeki `claudeCode.stabilizePromptCache: true`, dönüştürülen rotalarda sistem talimatlarının sonundaki desteklenen Claude bildirimlerini son kullanıcı mesajına taşır. Varsayılan değer `false` olur. Yalnızca bu rol değişikliği istemcileriniz için uygunsa etkinleştirin. Kod bloklarındaki örnekler ve eşleşmeyen metin korunur; yerel Anthropic aktarımı değişmez. Meta veri yoksa önbellek anahtarı kararlı talimatlardan hesaplanır. Bu seçenek konuşma kimliği oluşturmaz veya üst hizmette önbellek isabeti garanti etmez. +`config.json` içindeki `claudeCode.stabilizePromptCache: true`, dönüştürülen rotalarda sistem talimatlarının sonundaki desteklenen Claude bildirimlerini ayırır. Tanınan `N tokens left` biçimindeki token altbilgileri, yinelenenler de dahil olmak üzere silinir. TaskCreate hatırlatmaları eskisi gibi sona eklenen bir kullanıcı mesajına taşınır; yalnızca token altbilgileri ayrılmışsa girişe yeni bir mesaj eklenmez. Varsayılan değer `false` olur. Yalnızca bu rol değişikliği istemcileriniz için uygunsa etkinleştirin. Kod bloklarındaki örnekler ve eşleşmeyen metin korunur; yerel Anthropic aktarımı değişmez. Meta veri yoksa önbellek anahtarı kararlı talimatlardan hesaplanır. Bu seçenek konuşma kimliği oluşturmaz veya üst hizmette önbellek isabeti garanti etmez. Dönüştürülen tüm Chat rotalarında zaman çizelgesi hatırlatmaları, bekleyen araç sonuçlarından sonra konuşmadaki konumlarını korur. Böylece yeni bir hatırlatma eklenmesi baştaki sistem istemini yeniden yazmaz ve konuşmanın ortasındaki bir yönerge, izlemesi gereken turların önüne geçmez. O konumun hangi rolü taşıdığı ayrı bir karardır: sağlayıcı `foldDeveloperRoleToSystem: false` kaydetmedikçe hatırlatma `system` olarak gönderilir; bu kayıt, üst hizmetin `developer` rolünü kabul ettiğini belirtir ve rol aynı konumda iletilir. Kabul etmeyen bir üst hizmet `400 role 'developer' is not allowed` yanıtı verir ve tur hiç başlamaz; kaydı olmayan hedefin katlanmasının nedeni budur. Bu davranış `stabilizePromptCache` açık veya kapalıyken geçerlidir; yerel Anthropic aktarımı değişmez. Önbelleğin yeniden kullanımı için kararlı bir oturum kimliği ve kullanılabilir üst hizmet önbelleği hâlâ gereklidir. Önceki talimatların veya araçların değişmesi ve konuşmanın sıkıştırılması da önbellek isabetini etkileyebilir; hatırlatma sırasını korumak tek başına yeniden kullanımı garanti etmez. diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index 77b3def8080..7256fda4458 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -1,10 +1,10 @@ --- title: Entegrasyonlar -description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo ve Factory Droid'u opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek. +description: Kontrol panelinden OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code ve Factory Droid'u opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek. --- **Bağlantı** sekmesi, opencodex'in sağlayıcı bloğunu istemcinin kendi -yapılandırma dosyasına yazar ve tekrar kaldırır. On yedi istemci bu şekilde +yapılandırma dosyasına yazar ve tekrar kaldırır. On sekiz istemci bu şekilde çalışır, her biri bir anahtarla: | İstemci | Yapılandırma dosyası | Format | Değişiklik ne zaman geçerli olur? | Kimlik bilgisi | @@ -25,6 +25,7 @@ yapılandırma dosyasına yazar ve tekrar kaldırır. On yedi istemci bu şekild | omo | `~/.omo/agent/models.json` | JSON | yeni oturumlarda | geri döngü yer tutucusu | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | kapatıp yeniden başlattıktan sonra | yalnızca loopback | | Kilo | `~/.config/kilo` altında ilk bulunan `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` veya `config.json` (`XDG_CONFIG_HOME` bu dizini taşır; hiçbiri yoksa `kilo.jsonc` oluşturulur) | JSONC | yeni oturumlarda | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows’ta varsayılan ana dizin kullanılırken: `HOME ?? USERPROFILE` değerini yalnızca boş veya sırf boşluklardan oluşmayan mutlak bir Windows yoluysa kullanır, aksi halde `homedir()` kullanılır; ayrıca verilen ana dizin korunur; sonuna `.commandcode\providers.json` eklenir) | JSON | Command Code'un sonraki başlatılmasında | yok — anahtarsız geri döngü (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows'ta) | JSON | dosya değişince hemen | anahtarsız geri döngü | Desteklenen akıl yürütme düzeylerine sahip GJC modelleri, GJC'nin düzey seçimi sunabilmesi için `reasoning: true`, `thinking.levels` ve `compat.supportsReasoningEffort` alanlarını dışa aktarır. Yerel Codex modelleri, katalogda belirtilmese bile standart düzeylerini alır. Bilinen düzeyi olmayan modellerde bu alanlar bulunmaz. `none` düzey göndermez ve `ultra` gönderimde `max` düzeyine dönüşür; bu yüzden seçeneklerde yer almazlar. Model seçeneklerini güncellemek için entegrasyonu yenileyin. diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index 142c3948a87..75bb1318860 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -327,7 +327,7 @@ olmayan bir makineden oturum açmak bundan etkilenmez. ## 3. API anahtarı kataloğu -opencodex 102 yerleşik önayar ile birlikte gelir: 84 anahtar tabanlı, 14 +opencodex 103 yerleşik önayar ile birlikte gelir: 84 anahtar tabanlı, 15 OAuth, üç yerel ve bir varsayılan ChatGPT iletme önayarı. Kontrol panelinin **Sağlayıcı ekle** seçicisi bir anahtar sağlayıcısının kontrol panelini açar, anahtarı doğrular ve saklar; doğrulama sağlayıcıya özgüdür. Dikkate değer diff --git a/docs-site/src/content/docs/tr/guides/web-dashboard.md b/docs-site/src/content/docs/tr/guides/web-dashboard.md index d133ef6f62c..ba55ef9788d 100644 --- a/docs-site/src/content/docs/tr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/tr/guides/web-dashboard.md @@ -164,6 +164,26 @@ için [Alt Ajan Arayüzü](/tr/guides/sub-agent-surface/) sayfasına bakın. Pano yönetim düzlemi doğrudan client→hub model trafiğinden ayrıdır. **Bağlantı → API Anahtarları** bekleyen döndürmeyi gösterir, yeni sırrı bir kez görüntüler ve açık onay veya iptal ister. Tarayıcı logout yalnızca mevcut oturumu geçersiz kılar. Bağlı kullanım hub üzerinde `apiKeyId` ile filtrelenir; bağlantı kesilince yerel kayıt kullanılır ve yansıtma yapılmaz. +### Kaydedilmiş bir API anahtarını görüntüleme + +Kaydedilmiş bir anahtarın tam değerini göstermek için açık eşleştirme veya güvenilir Tailscale +kimlik doğrulamasıyla oluşturulmuş bir pano oturumu gerekir. Otomatik yerel oturum maskelenmiş +listeyi okuyabilir, ancak kaydedilmiş değerleri gösteremez. Yönetici belirteci de yalnızca oturumlara +izin verilen bu işlemi çağıramaz. Mevcut Remote Hub eşleştirme akışını veya varsa güvenilir Tailscale girişini kullanın. + +Yerel eşleştirme formu yalnızca HTTP üzerinden doğrudan `127.0.0.1` veya `[::1]` IP adresinde açılmış, +aynı origin'e sahip bağımsız panoda sunulur. Origin, sunucunun yapılandırılmış geri döngü IP bağlama +adresine uymalıdır. `localhost` veya bir takma adres kullanıyorsanız sunucunun bağlama adresini kontrol edin: +`127.0.0.1` için `http://127.0.0.1:`, `::1` için `http://[::1]:` adresinde yeniden açıp +eşleştirin. hostname değeri `localhost` olan sunucuda uygun bir yerel eşleştirme origin'i yoktur. +Pano, takma adresli URL'den gerçek bağlama adresini belirleyemediği için başka bir URL önermeden genel ret açıklamasını gösterir. + +Eşleştirmeden sonra değeri istemek için anahtara yeniden tıklayın. Eşleştirme tek başına anahtarı göstermez. +Paylaşılan oturum temizlendiğinde veya değiştirildiğinde, eşleştirme başladığında, panelden ayrıldığınızda, +tarayıcı sekmesi ya da masaüstü penceresi gizlendiğinde veya sunucu değiştiğinde gösterilen değerler ve +kopyalama durumu temizlenir. Bir kez gösterilen yeni ve yedek anahtar değerleri de temizlenir. +Yetkili oturumun süresi dolarsa veya iptal edilirse yeniden eşleştirin. + Spawn geçersiz kılma garantisi **yerleşik** v2 rehberlik metni için geçerlidir. Özel bir `injectionPrompt` bu metnin yerini tamamen alır ve `{{model}}` ve `{{effort}}` yer tutucularını (ve isteğe bağlı olarak `{{roster}}`) içermelidir, diff --git a/docs-site/src/content/docs/tr/reference/cli/agents.md b/docs-site/src/content/docs/tr/reference/cli/agents.md index 7f77c3e01ba..97c0b8ae3c2 100644 --- a/docs-site/src/content/docs/tr/reference/cli/agents.md +++ b/docs-site/src/content/docs/tr/reference/cli/agents.md @@ -204,7 +204,7 @@ Grok Build model çitini yönetin ve uygulayın. ## İstemci yapılandırma dışa aktarma -### `ocx export --client ` +### `ocx export --client ` Çalışan proxy'ye bağlı bir istemci yapılandırmasını yazdırın. Komut, `opencodex` sağlayıcı bloğunu — temel URL, model listesi ve istemcinin kimlik bilgisi @@ -216,7 +216,7 @@ yalnızca Codex'in şu anda görebildiği modelleri yayınlar. | Bayrak | Eylem | | --- | --- | -| `--client ` | Gerekli. İstemci yapılandırma lehçesini seçer. | +| `--client ` | Gerekli. İstemci yapılandırma lehçesini seçer. | | `--json` | Betikler için stdout üzerinde oluşturulan belgeyi JSON olarak yazdırın. Bu, seçilen istemcinin yerel formatı YAML, TOML veya JSON5 olsa bile JSON'dur. | | `--out ` | İstemcinin yerel yapılandırma formatını `` konumuna yazın. Mevcut bir dosyanın üzerine yazmayı reddeder. | | `--force` | `--out`'un mevcut bir dosyanın üzerine yazmasına izin verin. | @@ -250,6 +250,7 @@ için kendi varsayılanlarını uygular) gelir. | `raycast` | `~/.config/raycast/ai/providers.yaml`, macOS ve Windows'ta aynı (Raycast `XDG_CONFIG_HOME` değerini dikkate almaz) | `raycast-providers.yaml` | yok — yalnızca geri döngü, `api_keys` girdisi yazılmaz | | `omo` | `~/.omo/agent/models.json` (ayarlandığında sırasıyla `OMO_CODING_AGENT_DIR`, `SENPI_CODING_AGENT_DIR`, `PI_CODING_AGENT_DIR` öncelikli; göreli değer reddedilir) | `omo-models.json` | yok — geri döngü yer tutucusu | | `kilo` | `~/.config/kilo` altında ilk bulunan `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json` veya `config.json` (`XDG_CONFIG_HOME` bu dizini taşır); hiçbiri yoksa `kilo.jsonc` kullanılır | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows’ta varsayılan ana dizin kullanılırken: `HOME ?? USERPROFILE` değerini yalnızca boş veya sırf boşluklardan oluşmayan mutlak bir Windows yoluysa kullanır, aksi halde `homedir()` kullanılır; ayrıca verilen ana dizin korunur; sonuna `.commandcode\providers.json` eklenir) | `providers.json` | yalnızca geri döngü; `apiKey: false`; ortam değişkeni gerekmez | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | yalnızca loopback; ortam değişkeni gerekmez | Raycast dışa aktarımı, `providers` dizisinde tek bir `id: opencodex` öğesi içeren bağımsız diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index e351ffe2c3d..156cd41eac5 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -284,6 +284,65 @@ Anthropic hesap politikası riskini anlamadığınız sürece bunu devre dışı Emin olmadığınızda manuel `ocx account use anthropic ` geçişini tercih edin. ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2` ("Anthropic · Pool 2"), kendi hesap havuzuna sahip ikinci yerleşik Anthropic OAuth sağlayıcısıdır. `anthropic` ile aynı Anthropic uygulamasını çalıştırır: aynı giriş akışı, istek biçimi, yerel Messages ve Responses köprüsü ile aynı model meta verileri. Yalnızca hesaplar ve onları çevreleyen havuz ayrıdır. Havuzu model önekiyle seçersiniz: `anthropic/claude-sonnet-5` birincil havuzu, `anthropic2/claude-sonnet-5` ise Pool 2'yi kullanır. + +Pool 2 siz ekleyene kadar etkin değildir. `ocx login anthropic2` ile giriş yapın veya dashboard Sağlayıcılar sayfasında **Anthropic · Havuz 2** ekleyin. İlk başarılı giriş, `"anthropicOAuthInstance": "anthropic2"` işaretiyle `providers.anthropic2` oluşturur. Pool 2 hiçbir zaman varsayılan sağlayıcı olmaz: öneksiz `claude-*` model adları, varsayılan model ve Claude Code çağıran yönlendirmesi `anthropic` olarak çözümlenmeye devam eder. Daha önce kendiniz oluşturduğunuz işaretsiz bir `anthropic2` girdisi özel anlamını korur; giriş ad çakışmasını reddeder ve ne girdiyi ne de kimlik bilgilerinizi yeniden yazar. + +Pool 2 havuz ayarlarını kendi sağlayıcı girdisinden okur. Anahtarlar, varsayılanlar ve davranış yukarıdaki üst düzey `anthropicAccountPool` ile aynıdır ve birincil havuzdan hiçbir şey devralınmaz. `providers.anthropic.anthropicAccountPool` veya bu alanın başka herhangi bir sağlayıcıdaki karşılığı reddedilir. + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +İki havuz opencodex içinde yalıtılmıştır: + +- **Kimlik bilgileri:** Pool 2 hesapları korumalı kimlik bilgisi deposunda kendi `anthropic2` anahtarı altında saklanır. Belirteci veya doğrulanmış Anthropic hesabı diğer havuzda zaten saklı olan bir giriş reddedilir. +- **Havuz ayarları ve çalışma durumu:** seçim, oturum bağlılığı, soğuma süreleri, duraklatmalar, model rotaları ve hesap başına eşikler tek bir havuza aittir. Rotadaki `fallback: true` yalnızca aynı havuz içinde genişletir. +- **Kota ve kullanım:** kullanım yoklamaları, kota önbellekleri ve kullanım atfı havuz başına kaydedilir; bu yüzden iki havuzda da bulunan bir hesap kimliğinin iki ayrı kaydı olur. +- **Sıfırlama hakları:** Pool 2, birincil havuzun değişmeyen günlüğünün yanında kendi günlüğünü (`anthropic2-reset-grant-ledger.json`) tutar. +- **Kurtarma:** doğrudan bir `anthropic2/` isteği hiçbir zaman birincil havuza düşmez ve Pool 2'deki bir hız sınırı veya ret birincil havuzdaki bir hesabı soğumaya almaz. İki havuzu da adlandıran açık bir combo, bildirdiğiniz hedefleri korur. + +Bu ayrım opencodex içindeki bir yönlendirme sınırıdır. Anthropic'in hesaplarınıza nasıl davrandığını ve yukarıda açıklanan hesap politikası riskini değiştirmez. + +Pool 2 hesapları yalnızca tarayıcı OAuth ile eklenir. `anthropic` sağlayıcısından farklı olarak Pool 2, Claude Code CLI belirtecini hiçbir zaman içe aktarmaz, benimsemez veya geri yazmaz; bu fark kasıtlıdır. Pool 2 boş başlar ve kullanılabilir Pool 2 hesabı olmayan bir Pool 2 isteği, birincil havuzdan veya Claude Code'dan kimlik bilgisi ödünç almak yerine kimlik doğrulama hatasıyla başarısız olur. + +Hesap komutları ve yönetim API'leri havuzu adıyla belirtir: `ocx account pool anthropic2 …`, `ocx account auto-switch anthropic2 …`, `ocx account routes anthropic2 …` ve `ocx account anthropic-reset-grants --provider anthropic2`. Havuz ayarları ve sıfırlama hakları uç noktaları `provider: "anthropic2"` kabul eder; belirtilmezse birincil havuz kullanılmaya devam eder. + +#### Yardımcı havuz seçimi (`anthropicInstance`) + +Web arama ve görsel yardımcıları, genel `webSearchSidecar` ve `visionSidecar` ayarlarında ve Claude Code geçersiz kılmaları `claudeCode.webSearchSidecar` ile `claudeCode.visionSidecar` içinde isteğe bağlı bir `anthropicInstance` alanı kabul eder. Yardımcının arka ucu Anthropic olduğunda dashboard bunu **Hesap havuzu** seçimi olarak gösterir. + +| Değer | Davranış | +| --- | --- | +| ayarlanmamış (varsayılan) | Geçerli isteğin havuzunu izler: bir `anthropic2/` isteği Pool 2'yi, bir `anthropic/` isteği birincil havuzu kullanır. Başka bir sağlayıcıdan gelen istek mevcut yardımcı keşfini korur ve bu keşif Pool 2'yi hiçbir zaman seçmez. | +| `"anthropic"` | Her zaman birincil havuzu kullanır. | +| `"anthropic2"` | Her zaman Pool 2'yi kullanır. | + +Alan yalnızca yardımcının arka ucu Anthropic olarak çözümlendiğinde uygulanır. Başka bir arka uçla ayarlamak doğrulama hatasıdır; web arama varsayılan olarak OpenAI kullandığından `"backend": "anthropic"` da ayarlayın. Diğer havuzla nitelenmiş bir yardımcı model, örneğin `"anthropicInstance": "anthropic2"` ile birlikte `anthropic/claude-sonnet-5`, de reddedilir. Seçilen havuzda kullanılabilir hesap yoksa yardımcı hiçbir şey göndermeden başarısız olur; diğer havuza geçmez. Ana istek bu durumda o yardımcı olmadan devam eder. Ayarlanmamış bir seçim `"anthropic"` olarak değil, yok olarak kaydedilir. + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[Sürüm düşürme] +Pool 2 içermeyen sürümler `anthropic2` girdisini anlamaz. Daha eski bir sürümü kurmadan önce proxy'yi durdurun, `config.json` ve `auth.json` dosyalarını yedekleyin ve `providers.anthropic2` girdisini `config.json` dosyasından kaldırın. Pool 2, birincil havuzun kimlik bilgilerini hiçbir zaman taşımaz veya yeniden yazmaz. Pool 2 etkinken yerinde sürüm düşürme desteklenmez. +::: + ### Yönetilen kayıt biçimleri `apiKeys[]` girdileri `id`, `name`, oluşturulan `key` ve ISO `createdAt` diff --git a/docs-site/src/content/docs/tr/reference/configuration/server.md b/docs-site/src/content/docs/tr/reference/configuration/server.md index 2fedcb91188..29b7666b5c7 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/server.md +++ b/docs-site/src/content/docs/tr/reference/configuration/server.md @@ -317,3 +317,35 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available. The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. + +## Havuz harcama geçmişinin sürekliliği + +Havuz sınırı, istek günlüğündeki hesap etiketinden bağımsız olarak yönlendirmenin seçtiği kanonik sağlayıcıya uygulanır. Şu anda yapılandırılmış her `P` sağlayıcısı için bu kuruluma ait tuzlanmış havuz takma adı `h(pool, P)`, bakiyeler okunurken otomatik olarak `P` ile eşleştirilir. Aynı takma ad altında tutulan eski bakiyeler de buna dahildir. Bağımsız sağlayıcı sınırları için elle öz eşleme eklemek veya tuzu kullanarak karma hesaplamak gerekmez. + +Hesap sıra numarası içeren etiketler dahil diğer geçmiş etiketlerin sahibi otomatik olarak belirlenmez. Her özgün bakiye, eşleştiği sağlayıcı grubunda bir kez sayılır. Hâlâ eşleşmemiş pozitif bakiyeler, ihtiyatlı olmak için her aday havuza birer kez eklenir. Kesinleşmiş, rezerve edilmiş ve çözümlenmemiş kullanımın tümü hesaba katılır. Bu yüzden belirsiz geçmiş, kullanılmamış bir havuzu da sınırlayabilir. Güncel adlar ve hesap sırası geçmişin sahibini kanıtlamaz. Kök ve kimlik sınırları bağımsız kalır. + +Sahibini doğruladığınız geçmiş için `config.json` dosyasının en üst düzeyindeki `spendPoolAliases` alanına eşleme ekleyin; bu alan `spend` içinde olmamalıdır. Anahtar, bu kurulumun günlüğündeki tam 32 küçük harfli onaltılık karakterden oluşan tuzlanmış havuz takma adıdır. Değer, doğrulanmış ve şu anda yapılandırılmış sağlayıcının tam kimliğidir; başında veya sonunda boşluk bulunamaz. Günlüğü, tuzu ve eşleme kanıtını gizli tutun. Yapılandırılmış bir sağlayıcının kendi takma adı başka bir sağlayıcıya atanamaz. Sağlayıcı kümesi değiştiğinde doğrulama tekrarlanır; doğrulama mevcut tuzu okur, yenisini oluşturmaz. + +Otomatik ve açık eşlemeler yalnızca okuma sırasında uygulanır; özgün bakiyeleri taşımaz veya günlüğe kimlik bağlantısı yazmaz. Açık bir eşleme kaldırılınca geçmiş yeniden belirsiz olur; ancak takma ad halen yapılandırılmış bir sağlayıcının kendi takma adıysa otomatik eşleme sürer. Geçersiz eşlemeler yazılırken reddedilir. Elle yapılmış geçersiz bir değişiklik mevcut sınırları korur ve havuza yeni istek kabulünü engeller. + +Etkin olmayan belirsiz geçmiş, yalnızca son etkinliği `spend.retentionDays` eşiğinden kesin olarak daha eski olduğunda sona erer. Kapasite baskısı pozitif bakiyeler için bu süreyi kısaltmaz. Etkin rezervasyonlar ve hedefleri sıfır belirteçte bile korunur. Kabul kararında daha düşük toplam kullanılmadan önce silme kaydı kalıcı olarak yazılır. + +### Rezervasyonlar ve gönderim sınırı + +Kök, kimlik veya havuz sınırı geçerliyse her hedef ya da anahtarın ilk gönderimi normal izleme kapasitesinden bir rezervasyon gerektirir. Rezervasyon alınamazsa sağlayıcıya gönderim yapılmaz. Aynı hedefteki yeniden denemeler aynı kapsamları kullanır; farklı hedef veya anahtar yeni rezervasyon gerektirir. `L`, istek başladığında sabitlenen, isteğin tamamı için fiziksel gönderim sınırıdır. Varsayılan değer dörttür; mevcut OAuth istek profili en fazla on sekiz gönderime izin verir. + +Sınırların uygulanıp uygulanmayacağı, geçerli kök, kimlik ve havuz belirteç sınırları ile `L`, her isteğin başlangıcında belirlenir. Yapılandırma değişiklikleri yalnızca değişiklikten sonra başlayan istekleri etkiler. Devam eden istekler, tüm yeniden denemeler ve devam çağrıları için başlangıç politikalarını korur: bir sınırı etkinleştirmek veya düşürmek onları daha fazla kısıtlamaz; yükseltmek veya kaldırmak da daha fazla izin vermez. Yalnızca gözlem modunda başlayan bir istek, tamamlanana kadar bu modda kalır. + +Son hesaplaşma, başlamış gönderimlerin raporlarını bekler. Bir gönderim kimliği ancak muhasebesi kalıcılaştıktan sonra unutulur; bakiyesi silinmez. Sınır faturayı değil gönderim sayısını sınırlar: ilk tahmini aşan gerçek kullanımın tamamı kaydedilir. Geçerli bir harcama sınırı yoksa yalnızca gözlem davranışı sürer; izleme kapasitesi dolduğunda kayıtların atlanması da değişmez. Kimlik bağlantısı içeren yeni kontrol noktaları eklenmez. + +Claude CLI, CodeBuddy ve Qoder için her CLI çağrısı bir gönderim sayılır. Başlatmadan önce normal rezervasyon gerekir; alınamazsa çağrı reddedilir. CLI içindeki yeniden denemeler ve araç turları istek genelindeki gönderim sınırını ayrıca tüketmez. Maliyetleri, ilk tahmini aşan bölüm dahil, raporlanan gerçek kullanıma göre hesaplanır. + +### 2.80.0 sürümüne dönüş: C sözleşmesi + +Yeni kayıtlar olağan v1 biçimini ve `pool` takma ad alanını kullanır. Değiştirilmemiş 2.80.0 bunları kendi etiket bazlı kurallarıyla okur, günlüğü sıkıştırır ve saklama süresini uygular. Güncel günlüğü ve tuzu koruyun; eski bir kopyayı geri yüklemek, o kopyadan sonra kaydedilen harcamayı kaybettirir. Geriye dönük yama, başlatıcı engeli veya mutabakat komutu gerekmez. + +Sürüm düşürme, kanonik toplamların korunacağını veya aynı trafik baştan beri 2.80.0 üzerinde işlenseydi kalacak kotayla eşitliği garanti etmez. Yeniden yükseltmede, yapılandırılmış sağlayıcıların otomatik öz eşlemeleri ve güncel açık eşlemeler, 2.80.0 tarafından hâlâ tutulan özgün bakiyelere uygulanır. Yeni kontrol noktaları eski okuyucunun koruması gereken kimlik bağlantısı metaverisi içermez. + +Yayımlanmamış deneysel sürümlerin `pool-current` veya `poolContinuity` içeren günlükleri, daha sonra sıkıştırılsalar bile C sözleşmesinin dışındadır. Özgün bakiyeleri normal saklama süresi dolana kadar belirsiz geçmiş olarak tutulur; hemen dönüştürülmez veya silinmez. + +Tam ama geçersiz bir kayıt, son satırdaki `null` dahil, sınır uygulanan isteklerin kabulünü engeller. Sıkıştırma geçerli muhasebeyi temiz bir kontrol noktasında koruyabilir; çalışan süreç temiz günlükle yeniden başlatılana kadar ret durumu sürer. Yarım kalmış son JSON satırı mevcut kurtarma kurallarına tabidir. Ayrıntılar ve hata kodları için [İngilizce kaynağa](/reference/configuration/server/#historical-pool-continuity) bakın. diff --git a/docs-site/src/content/docs/tr/reference/management-api.md b/docs-site/src/content/docs/tr/reference/management-api.md index 2199bce119f..5233d4ce458 100644 --- a/docs-site/src/content/docs/tr/reference/management-api.md +++ b/docs-site/src/content/docs/tr/reference/management-api.md @@ -363,6 +363,8 @@ dolaşmamalıdır. Depoya yıldız verip vermeyeceğini kullanıcı seçmelidir. ### Sistem yaşam döngüsü +`POST /api/system/restart`, gövde yoksa veya `{}` ise varsayılan 60 saniyelik boşaltma süresini korur. Bir yönetim oturumu veya yönetici token'ı, `{"drainGraceMs":2000}` ile daha kısa süreyi açıkça seçebilir. Değer 1 ile 60000 milisaniye arasında bir tam sayı olmalıdır. Geçersiz gövde veya değer, yeniden başlatmayı başlatmadan 400 döndürür; hedef sürece bağlı yerel yeniden başlatma capability'si bu seçeneğe izin vermez (403). Yanıttaki `drainTimeoutMs`, ilk kabul edilen süreyi bildirir; tekrarlanan çağrılar mevcut süreyi veya son tarihi değiştirmez. Yanıt gönderme gecikmesi bu süreye dahildir; temizleme ve yeni sürecin hazır olması için ayrı 60 ve 70 saniyelik bütçeler korunur. Kesilen bir istek zaten yürütülmüş olabilir: yeniden göndermeden önce sonucunu kontrol edin. Otomatik yeniden gönderme eklenmez. + | Yöntem ve yol | Amaç | Dikkate değer hatalar | | --- | --- | --- | | `GET /api/system/memory` | Skaler süreç, yığın (heap), akış, yanıt durumu, denetleyici ve aktif tur metriklerini döndürün | — | diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md index 892e33377e0..c73b77d255f 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md @@ -18,7 +18,7 @@ ocx init `ocx init` 会引导你完成: -1. **选择 provider** — 从内置 registry 的 102 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 +1. **选择 provider** — 从内置 registry 的 103 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。 2. **API key** — 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。 3. **默认模型** — 对于 key、本地和 custom provider,接受预设值或输入模型 id。 4. **代理端口** — 默认为 `10100`。 diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 5fbf851d5d2..1f0ed34b9ce 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -573,7 +573,7 @@ Claude 模型时自动加载。对于原生透传,这是正常现象;对于 **子代理派发到错误模型**——名册代理(`ocx-*`)使用 `` 指令, 而不是 Agent 工具的 `model` 参数。请确保指令与预期路由一致。传入 `"haiku"` 作为模型占位符。 -在 `config.json` 中设置 `claudeCode.stabilizePromptCache: true`,可在转换路由上将系统指令末尾受支持的 Claude 提示移到最后一条用户消息。默认值为 `false`。仅在客户端允许这种角色变化时启用。代码围栏内的示例和不匹配的文本会保留,Anthropic 原生透传不变。没有元数据时,缓存键按稳定后的指令计算。该选项不会生成会话标识,也不保证上游缓存命中。 +在 `config.json` 中设置 `claudeCode.stabilizePromptCache: true`,可在转换路由上剥离系统指令末尾受支持的 Claude 提示。识别出的 `N tokens left` 形式的 token 页脚会被删除,包括重复出现的页脚。TaskCreate 提醒仍按原有方式移到末尾的用户消息;如果剥离的只有 token 页脚,则不会添加输入消息。默认值为 `false`。仅在客户端允许这种角色变化时启用。代码围栏内的示例和不匹配的文本会保留,Anthropic 原生透传不变。没有元数据时,缓存键按稳定后的指令计算。该选项不会生成会话标识,也不保证上游缓存命中。 在所有转换后的 Chat 路由上,时间线提醒都会保留在对话中的原有位置(排在尚待返回的工具结果之后)。因此,追加提醒不会重写开头的系统提示,对话中途的指令也不会被挪到它本应跟随的轮次之前。该位置携带哪个角色是单独决定的:除非提供方记录了 `foldDeveloperRoleToSystem: false`,否则提醒以 `system` 发送;该记录表示上游接受 `developer` 角色,此时提醒在同一位置按原样转发。不接受该角色的上游会返回 `400 role 'developer' is not allowed`,这一轮根本无法开始,所以未记录的目的地采用折叠。无论 `stabilizePromptCache` 是否启用,该行为都会生效;Anthropic 原生透传保持不变。缓存复用仍需要稳定的会话标识和可用的上游缓存。修改较早的指令或工具、压缩对话也可能影响缓存命中;仅保留提醒顺序并不保证缓存复用。 diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 9b19850626b..925a226790f 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -327,6 +327,8 @@ fallback 行为,参见 [Sub-agent Surface](/guides/sub-agent-surface/)。 在 **Codex Set → Multi-auth** 中,打开 **Codex Auth** 标题栏中的 **Codex 额度** 开关,即可在 Week 下方显示主账号和各池账号最近查询到的积分。默认关闭,并保存为 `showCodexCredits`。余额按地区格式显示;上游报告时会显示无限额或超额使用上限警告。由于没有积分总上限,条形表示可用状态而非百分比。此开关仅控制显示,新登录需等待其自身的查询结果。 +允许主账号在额度耗尽后使用积分且用量窗口已满时,后台恢复周期会从观察满三分钟起更新此前可用的积分余额,即使仪表盘已关闭。查询前先准备有效令牌,为五分钟有效期到期前重新观察余额留出时间。缺失、零余额、受限或已撤回的积分不会触发更新。查询失败或信息不完整不会刷新旧余额证据;证据过期后请求仍会被拒绝。独立的主账号硬锁和上游重试等待期限仍然适用。这是已认证的用量查询,不发送模型验证请求,也不会设置或清除账号的 needs-reauth 状态。 + 后台重新验证是独立功能,默认关闭。它要求 Token Guardian、`openai` 的 `proactive` 刷新策略及 `tokenGuardian.codexWarmupEnabled`,并跳过等待注册验证的账号。 ### 账号停止处理请求的原因 diff --git a/docs-site/src/content/docs/zh-cn/guides/integrations.md b/docs-site/src/content/docs/zh-cn/guides/integrations.md index 17f43b6a644..debbfb1dad1 100644 --- a/docs-site/src/content/docs/zh-cn/guides/integrations.md +++ b/docs-site/src/content/docs/zh-cn/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 集成 -description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo 和 Factory Droid;每个客户端都有独立开关,且每次写入前都会备份。 +description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code 和 Factory Droid;每个客户端都有独立开关,且每次写入前都会备份。 --- -**连接** 标签页可将 opencodex 的提供商配置块写入客户端自己的配置文件,也可再次移除。以下 17 个客户端都采用这种方式,各有独立开关: +**连接** 标签页可将 opencodex 的提供商配置块写入客户端自己的配置文件,也可再次移除。以下 18 个客户端都采用这种方式,各有独立开关: | 客户端 | 配置文件 | 格式 | 变更生效时间 | 凭据 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 从仪表盘将 opencodex 连接到 OpenCode、Pi、OMP、Hermes、 | omo | `~/.omo/agent/models.json` | JSON | 新会话 | 回环占位符 | | Cline CLI | `~/.cline/data/settings/providers.json` 及同目录下的 `models.json` | JSON 文件对 | 停止并重启 Cline 后 | 回环占位符 | | Kilo | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可迁移目录;均不存在时创建 `kilo.jsonc`) | JSONC | 新会话 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows 使用默认主目录时:仅当 `HOME ?? USERPROFILE` 的值非空、非纯空白且为 Windows 绝对路径时使用,否则使用 `homedir()`;单独传入的主目录保持不变;文件位于其下的 `.commandcode\providers.json`) | JSON | 下次启动 Command Code 时 | 无 — 无密钥回环 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows 上) | JSON | 文件变更时立即生效 | 无密钥回环 | 生成的目录只包含各提供商选择中已启用的模型。下载文件和托管集成都遵循这一规则,Pi 和 Aside 也不例外。管理模型列表仍显示完整阵容,以便启用更多模型。 diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 351261f6a74..ba8c5cff244 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -181,7 +181,7 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal ## 3. API 密钥目录 -opencodex 内置 102 个预设:84 个密钥预设、14 个 OAuth 预设、3 个本地预设,以及 1 个默认的 +opencodex 内置 103 个预设:84 个密钥预设、15 个 OAuth 预设、3 个本地预设,以及 1 个默认的 ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。 验证因提供商而异。主要条目包括: diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 598de46bcdd..0d8f285caa4 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -117,6 +117,22 @@ Dashboard 的 **Sub-agent delegation** 选择器会保存 `injectionModel`,以 控制台管理平面与 client→hub 的模型流量相互独立。**连接 → API 密钥** 显示待处理轮换,只显示一次替换密钥,并要求显式提交或中止。浏览器 logout 只使当前会话失效。连接时从 hub 按 `apiKeyId` 过滤用量;断开后使用本地记录,两者不会镜像。 +### 查看已保存的 API 密钥 + +查看已保存密钥的完整值,需要通过显式配对或可信 Tailscale 身份认证签发的控制台会话。 +自动签发的本地会话仍可查看掩码列表,但不能显示已保存的密钥值;管理员令牌也不能调用 +这项仅限会话的操作。请使用现有的 Remote Hub 配对流程,或在可用时通过可信 Tailscale 身份登录。 + +本地配对表单仅在通过 HTTP、以字面 IP 地址 `127.0.0.1` 或 `[::1]` 打开的同源独立控制台中提供。 +来源必须与服务器配置的回环 IP 绑定一致。如果通过 `localhost` 或其别名打开,请先检查服务器绑定: +绑定为 `127.0.0.1` 时重新打开 `http://127.0.0.1:`,绑定为 `::1` 时重新打开 +`http://[::1]:`,然后配对。hostname 配置为 `localhost` 的服务器没有符合条件的本地配对来源。 +控制台无法从别名 URL 判断实际绑定,因此只显示通用拒绝说明,不建议替代 URL。 + +配对后请再次点击密钥以请求其值;配对本身不会显示密钥。共享会话被清除或替换、开始配对、 +离开面板、隐藏浏览器标签页或桌面窗口、切换服务器时,已显示的值和复制状态都会清除。 +新建或替换密钥的一次性值也会清除。授权会话过期或被撤销后,请重新配对。 + 选择器会列出已启用的原生与路由模型,以及全局 Codex reasoning 阶梯。API 会先验证所选强度是否 属于全局阶梯;Codex 仍会根据目标目录条目再次校验该 spawn 强度。 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md index 7032e860d07..6a14c40d344 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md @@ -144,7 +144,7 @@ ocx claude desktop import [--apply] Validate and import JSON ## Client config export -### `ocx export --client ` +### `ocx export --client ` 输出连接到正在运行代理的客户端配置。此命令会以所选客户端的原生格式序列化 `opencodex` provider 块,其中包含基础 URL、模型列表,以及该客户端适用的凭据引用或 `opencodex-loopback` 占位值。 @@ -152,7 +152,7 @@ ocx claude desktop import [--apply] Validate and import JSON | 标志 | 动作 | | --- | --- | -| `--client ` | 必需。选择客户端配置格式。 | +| `--client ` | 必需。选择客户端配置格式。 | | `--json` | 仅在 stdout 打印配置 JSON,这样重定向即可捕获字节级精确输出。包括 `--out` 写入提示在内的所有诊断信息都会输出到 stderr。 | | `--out ` | 将配置写入 ``。拒绝替换已存在的文件。 | | `--force` | 允许 `--out` 替换已存在的文件。 | @@ -183,6 +183,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 与 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 无 — 仅限回环,不会写入 `api_keys` 条目 | | `omo` | `~/.omo/agent/models.json`(设置后依次由 `OMO_CODING_AGENT_DIR`、`SENPI_CODING_AGENT_DIR`、`PI_CODING_AGENT_DIR` 优先;相对路径会被拒绝) | `omo-models.json` | 无 — loopback placeholder | | `kilo` | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可更改该目录);均不存在时使用 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows 使用默认主目录时:仅当 `HOME ?? USERPROFILE` 的值非空、非纯空白且为 Windows 绝对路径时使用,否则使用 `homedir()`;单独传入的主目录保持不变;文件位于其下的 `.commandcode\providers.json`) | `providers.json` | 仅限回环;`apiKey: false`;无需环境变量 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 仅限回环;无需环境变量 | Raycast 导出是一份独立的 `providers.yaml` 文档,在 `providers` 序列中只有一个 `id: opencodex` 元素:`name: OpenCodex`、代理的 `/v1` 基础 URL,以及每个已路由模型及其 `abilities`(`tools` 与 `system_message` 始终支持,`vision` 取自目录的输入模态,`reasoning_effort` 在模型有 effort 阶梯时设置,`temperature` 对推理模型关闭)。Custom Providers 是 Raycast Pro 功能,且 Raycast 会监视该文件,因此保存后的更改无需重启即可生效。格式见 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)。不会写入任何 `api_keys` 条目,所以该导出仅限回环,非回环绑定会被拒绝。 diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 39e62f9de1a..9dfedddeb7c 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -254,6 +254,65 @@ affinity。这些策略不能规避 provider enforcement。 除非你理解 Anthropic 账户策略风险,否则请保持关闭。若不确定,优先手动使用 `ocx account use anthropic ` 切换。 ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2`(“Anthropic · Pool 2”)是第二个内置的 Anthropic OAuth 提供者,拥有自己的账户池。它与 `anthropic` 使用同一套 Anthropic 实现:相同的登录流程、线路格式、原生 Messages 与 Responses 桥接以及模型元数据。只有账户及其所在的账户池是分开的。通过模型前缀选择账户池:`anthropic/claude-sonnet-5` 使用主账户池,`anthropic2/claude-sonnet-5` 使用 Pool 2。 + +在你添加之前,Pool 2 处于休眠状态。使用 `ocx login anthropic2` 登录,或在控制面板的提供者页面添加 **Anthropic · 账户池 2**。首次登录成功后会创建带有标记 `"anthropicOAuthInstance": "anthropic2"` 的 `providers.anthropic2`。Pool 2 永远不会成为默认提供者:不带前缀的 `claude-*` 模型名、默认模型以及 Claude Code 调用方转发仍解析到 `anthropic`。你此前自行创建、没有标记的 `anthropic2` 条目保持其自定义含义;登录会拒绝该名称冲突,既不改写该条目,也不改写你的凭据。 + +Pool 2 从自己的提供者条目读取账户池设置。键、默认值和行为与上文的顶层 `anthropicAccountPool` 相同,且不从主账户池继承任何内容。`providers.anthropic.anthropicAccountPool`,以及任何其他提供者上的该字段,都会被拒绝。 + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +两个账户池在 opencodex 内部相互隔离: + +- **凭据:** Pool 2 账户以独立的 `anthropic2` 键保存在受保护的凭据存储中。若登录的令牌或已验证的 Anthropic 账户已保存在另一个账户池中,该登录会被拒绝。 +- **账户池设置与运行时状态:** 选择、会话亲和性、冷却、暂停、模型路由和按账户阈值都只属于一个账户池。路由的 `fallback: true` 只在同一账户池内放宽。 +- **配额与用量:** 用量探测、配额缓存和用量归属按账户池分别记录,因此同一个账户 ID 同时出现在两个账户池中时,会有两条独立记录。 +- **重置额度:** Pool 2 使用自己的日志(`anthropic2-reset-grant-ledger.json`),与保持不变的主账户池日志并存。 +- **恢复:** 直接的 `anthropic2/` 请求绝不会回退到主账户池,Pool 2 的速率限制或拒绝也绝不会让主账户池的账户进入冷却。显式指定两个账户池的组合(combo)保留你声明的目标。 + +这种分离是 opencodex 内部的路由边界。它不会改变 Anthropic 对待你账户的方式,也不会消除上文所述的账户策略风险。 + +Pool 2 账户只能通过浏览器 OAuth 添加。与 `anthropic` 不同,Pool 2 从不导入、接管或回写 Claude Code CLI 令牌;这一差异是有意为之。Pool 2 初始为空;没有可用 Pool 2 账户的 Pool 2 请求会以认证错误失败,而不会借用主账户池或 Claude Code 的凭据。 + +账户命令和管理 API 按名称指定账户池:`ocx account pool anthropic2 …`、`ocx account auto-switch anthropic2 …`、`ocx account routes anthropic2 …` 以及 `ocx account anthropic-reset-grants --provider anthropic2`。账户池设置和重置额度端点接受 `provider: "anthropic2"`;省略时仍使用主账户池。 + +#### 辅助功能的账户池选择(`anthropicInstance`) + +网页搜索和视觉辅助功能在全局 `webSearchSidecar`、`visionSidecar` 设置以及 Claude Code 覆盖项 `claudeCode.webSearchSidecar`、`claudeCode.visionSidecar` 中接受可选的 `anthropicInstance`。当辅助功能的后端为 Anthropic 时,控制面板将其显示为 **账户池** 选择项。 + +| 值 | 行为 | +| --- | --- | +| 未设置(默认) | 跟随当前请求的账户池:`anthropic2/` 请求使用 Pool 2,`anthropic/` 请求使用主账户池。来自其他提供者的请求保留现有的辅助功能发现逻辑,该逻辑从不选择 Pool 2。 | +| `"anthropic"` | 始终使用主账户池。 | +| `"anthropic2"` | 始终使用 Pool 2。 | + +该字段仅在辅助功能的后端解析为 Anthropic 时生效。与其他后端一起设置会导致校验错误;由于网页搜索默认使用 OpenAI,请同时设置 `"backend": "anthropic"`。以另一个账户池限定的辅助模型(例如 `anthropic/claude-sonnet-5` 搭配 `"anthropicInstance": "anthropic2"`)同样会被拒绝。如果所选账户池没有可用账户,辅助功能会在发送任何内容之前失败,不会切换到另一个账户池。此时主请求会在没有该辅助功能的情况下照常进行。未设置的选择会保存为缺省,而不是 `"anthropic"`。 + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[降级] +不包含 Pool 2 的版本无法识别 `anthropic2` 条目。安装旧版本之前,请停止代理,备份 `config.json` 和 `auth.json`,并从 `config.json` 中删除 `providers.anthropic2`。Pool 2 从不移动或改写主账户池的凭据。不支持在 Pool 2 处于启用状态时原地降级。 +::: + ### 托管记录形状 `apiKeys[]` 条目包含 `id`、`name`、生成的 `key` 以及 ISO 格式的 `createdAt` 字符串。`codexAccounts[]` 条目要求有 `id`、`email` 和 `isMain`,并可选 `plan`、`chatgptAccountId` 和具备隐私安全性的 `logLabel`。这些记录通常由仪表板管理。 diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md index 9a2d8ceba78..1027288da9f 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md @@ -214,6 +214,45 @@ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agen The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. -## 令牌预留与额度限制 - -如果请求受 `spend.root.maxTokens`、`spend.identity.maxTokens` 或 `spend.pool.maxTokens` 限制,无法记录令牌预留时会拒绝发送,包括跟踪容量已满的情况。没有适用额度限制的请求仍保持仅观察模式。 +## 历史池用量的连续性 + +提供方池额度按路由选定的规范提供方计算,请求日志仍保留账户显示标签。 +对于当前配置的每个提供方 `P`,读取余额时会自动将本安装的加盐池别名 `h(pool, P)` 绑定到 `P`, +相同别名下保留的历史余额也适用。独立的提供方额度不需要手动添加自映射,也不需要用盐值自行计算哈希。 + +其他历史标签,包括带账户序号的标签,不会自动绑定。对于已核实归属的历史,在 `config.json` 顶层配置 +`spendPoolAliases`:键是本安装日志中的历史池加盐别名(32 位小写十六进制),值必须是已核实且当前已配置的提供方准确 ID,不得有首尾空格。 +当前名称或账户排列顺序不能证明历史归属。请妥善保密保存日志和盐值。 + +自动绑定和显式映射都仅在读取时应用,不移动原始余额,也不把身份关联写入日志。 +删除显式映射会使该历史重新成为归属未知;但如果别名属于当前配置的提供方自身,自动绑定仍然生效。 +每份原始余额只计入所属组一次;仍未绑定的正余额则保守地计入每个候选池一次。 +已配置提供方自己的池别名不能映射到另一个提供方;新增提供方时会重新验证。验证只读取现有盐值,不创建盐值。 +无效映射保留原有额度并拒绝池准入。 + +休眠的未知历史只有在最后活动时间严格早于 `spend.retentionDays` 的截止时间后才过期。 +容量压力不会提前删除正余额的未知历史。活动预留和 seed 即使为零令牌也会保留; +只有删除记录持久化后,准入计算才使用减少后的余额。 +有适用额度的请求必须在每个目标/密钥首次发送前取得普通容量的预留。 +重试沿用相同范围。`L` 是整个请求共享的物理发送次数上限,在请求开始时固定。 +默认是 4 次,现有 OAuth 请求配置最多允许 18 次。所有物理发送共享这一有限上限。 + +是否执行额度限制、适用的根请求/身份/池令牌额度,以及 `L`,都在请求开始时确定。 +配置变更仅影响变更后开始的请求。已经运行的请求在所有重试和后续发送中保留开始时的策略: +启用或降低额度不会收紧它,提高或删除额度也不会放宽它。以仅观察模式开始的请求会保持该模式直到结束。 +最终结算等待报告完成;遗忘发送 ID 不会删除余额。无适用额度时保留原有仅观察行为,包括容量已满时省略记账。 + +Claude CLI、CodeBuddy 和 Qoder 将一次 CLI 调用计为一次发送,启动前仍必须取得普通预留;无法预留就拒绝启动。 +CLI 内部重试和工具轮次不单独计入请求级发送次数上限,但费用按报告的实际使用量结算,包括超出初始估算的部分。 + +### 回滚至 2.80.0:契约 C + +新记录使用普通 v1 格式和 `pool` 别名。未经修改的 2.80.0 可以按自身的逐标签规则读取、压缩并执行保留策略。 +请保留最新日志和盐值。降级后不保证规范提供方聚合,也不保证与从未升级、始终运行 2.80.0 的相同流量具有相同剩余额度。 +再次升级时,当前配置提供方的自动绑定及显式映射应用于仍然保留的原始余额。不需要启动屏障或对账命令。 +未发布实验版本的 `pool-current` / `poolContinuity` 历史不属于契约 C;原始余额作为未知历史保留到正常过期, +不会立即转换或删除。 + +完整但无效的记录(包括末行 `null`)会拒绝有额度限制的请求。压缩仍可保留有效余额, +当前进程的拒绝状态持续到重启并读取干净日志。写入中断的最后一行 JSON 沿用原有恢复规则。 +详细边界及错误码见[英文原文](/reference/configuration/server/#historical-pool-continuity)。 diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md index abcdb26b4f9..a590806cd01 100644 --- a/docs-site/src/content/docs/zh-cn/reference/management-api.md +++ b/docs-site/src/content/docs/zh-cn/reference/management-api.md @@ -292,6 +292,8 @@ OpenAI 也遵循此规则:开关不会选择特殊的 922k 模式。有效上 ### 系统生命周期 +`POST /api/system/restart` 在没有请求体或请求体为 `{}` 时保留默认的 60 秒排空等待。使用管理会话或管理员令牌的调用方可通过 `{"drainGraceMs":2000}` 显式选择短等待,取值须为 1–60000 毫秒的整数。无效 JSON 或参数返回 400,且不会开始重启;绑定目标进程的本机 restart capability 不允许设置该参数(403)。响应中的 `drainTimeoutMs` 表示首次接受的等待时间,重复调用不会改变已接受的时间或截止点。等待时间包含发送接受响应前的延迟,清理和替代进程就绪仍分别保留独立的 60 秒、70 秒预算。被中断的请求可能已经执行,重发前应核对结果;此选项不会增加自动重发。 + | 方法和路径 | 用途 | 典型错误 | | --- | --- | --- | | `GET /api/system/memory` | 返回标量级的进程、堆、流、响应状态、看门狗和活跃回合指标 | — | diff --git a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md index 42b37e373c7..0583cbe4473 100644 --- a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md @@ -13,7 +13,7 @@ ocx init `ocx init` 會引導你完成: -1. **選擇 provider** —— 從內建 registry 的 102 個預設中選擇一個,或選擇 `custom` 手動輸入 +1. **選擇 provider** —— 從內建 registry 的 103 個預設中選擇一個,或選擇 `custom` 手動輸入 base URL 和 adapter。 2. **API key** —— 貼上一個 key,或引用一個環境變數,例如 `${ANTHROPIC_API_KEY}`。 3. **預設模型** —— 對於 API key、本機和 custom provider,可接受預設值或輸入模型 id。 diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index ceac4790726..3d6bc538a02 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -649,7 +649,7 @@ Claude 模型時自動載入。對於原生透傳,這是正常現象;對於 **子代理派發到錯誤模型**——名冊代理(`ocx-*`)使用 `` 指令, 而不是 Agent 工具的 `model` 引數。請確保指令與預期路由一致。傳入 `"haiku"` 作為模型佔位符。 -在 `config.json` 中設定 `claudeCode.stabilizePromptCache: true`,可在轉換路由上將系統指令末尾支援的 Claude 提示移到最後一則使用者訊息。預設值為 `false`。僅在用戶端允許這種角色變更時啟用。程式碼圍欄中的範例和不符合的文字會保留,Anthropic 原生轉送不變。沒有中繼資料時,快取鍵依穩定後的指令計算。此選項不會產生對話識別碼,也不保證上游快取命中。 +在 `config.json` 中設定 `claudeCode.stabilizePromptCache: true`,可在轉換路由上移除系統指令末尾支援的 Claude 提示。辨識出的 `N tokens left` 格式的 token 頁尾會被刪除,包括重複出現的頁尾。TaskCreate 提醒仍依原有方式移到末尾的使用者訊息;如果移除的只有 token 頁尾,就不會新增輸入訊息。預設值為 `false`。僅在用戶端允許這種角色變更時啟用。程式碼圍欄中的範例和不符合的文字會保留,Anthropic 原生轉送不變。沒有中繼資料時,快取鍵依穩定後的指令計算。此選項不會產生對話識別碼,也不保證上游快取命中。 在所有轉換後的 Chat 路由上,時間線提醒都會保留在對話中的原有位置(排在尚待傳回的工具結果之後)。因此,新增提醒不會重寫開頭的系統提示,對話中途的指令也不會被移到它原本應跟隨的輪次之前。該位置攜帶哪個角色是另外決定的:除非提供者記錄了 `foldDeveloperRoleToSystem: false`,否則提醒以 `system` 傳送;該記錄表示上游接受 `developer` 角色,此時提醒在相同位置照原樣轉送。不接受該角色的上游會回應 `400 role 'developer' is not allowed`,該回合根本無法開始,所以未記錄的目的地採用摺疊。無論 `stabilizePromptCache` 是否啟用,此行為都會生效;Anthropic 原生轉送維持不變。快取重用仍需要穩定的工作階段識別碼和可用的上游快取。修改較早的指令或工具、壓縮對話也可能影響快取命中;僅保留提醒順序並不保證快取重用。 diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md index 38c9170b76f..2444ccefe84 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -1,9 +1,9 @@ --- title: 整合 -description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo 與 Factory Droid——每個客戶端一個開關,每次寫入前都會先備份。 +description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、gjc、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside、Raycast、omo、Cline CLI、Kilo、Command Code 與 Factory Droid——每個客戶端一個開關,每次寫入前都會先備份。 --- -**連線** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有十七個客戶端以這種方式運作,每個都有一個開關: +**連線** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有十八個客戶端以這種方式運作,每個都有一個開關: | 客戶端 | 設定檔 | 格式 | 變更生效時機 | 憑證 | |---|---|---|---|---| @@ -23,6 +23,7 @@ description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、 | omo | `~/.omo/agent/models.json` | JSON | 新工作階段 | loopback 佔位符 | | Cline CLI | `~/.cline/data/settings/providers.json` + `models.json` | JSON | 結束並重新啟動後 | 僅限 loopback | | Kilo | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 會移動該目錄;若都不存在則建立 `kilo.jsonc`) | JSONC | 新工作階段 | `OPENCODEX_KILO_API_KEY` | +| Command Code | `~/.commandcode/providers.json` (Windows 使用預設家目錄時:僅當 `HOME ?? USERPROFILE` 的值非空、非純空白且為 Windows 絕對路徑時使用,否則使用 `homedir()`;另行傳入的家目錄保持不變;檔案位於其下的 `.commandcode\providers.json`) | JSON | 下次啟動 Command Code 時 | 無 — 無金鑰迴環 (`apiKey: false`) | | Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` Windows 上) | JSON | 檔案變更時立即生效 | 無金鑰迴環 | 具有受支援推理強度階梯的 GJC 模型會匯出 `reasoning: true`、`thinking.levels` 與 `compat.supportsReasoningEffort`,讓 GJC 提供強度選擇。原生 Codex 模型即使未在目錄中列出階梯,也會取得標準階梯。沒有已知階梯的模型會省略這些欄位;`none` 不傳送強度,`ultra` 在傳輸時會折疊成 `max`,因此不會列為選項。重新整理整合即可更新模型選項。 diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index c0ea41bc752..2deea9cb86d 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -249,7 +249,7 @@ database 並移除目前的 WAL、SHM 與 journal sidecar,再發布先前的 s ## 3. API 金鑰目錄 -opencodex 內建 102 個 preset:84 個 key-based、14 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward +opencodex 內建 103 個 preset:84 個 key-based、15 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward preset。儀表板的 **Add provider** picker 會開啟 key provider 的 dashboard、驗證金鑰並儲存;驗證方式 依 provider 而異。主要條目如下。 diff --git a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md index c91f953ff4b..5b52203ae71 100644 --- a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md @@ -120,6 +120,22 @@ Dashboard 的 **Sub-agent delegation** 選擇器會儲存 `injectionModel`,以 儀表板管理平面與 client→hub 模型流量彼此獨立。**連線 → API 金鑰** 顯示待處理輪替,只顯示一次替代金鑰,並要求明確提交或中止。瀏覽器 logout 只會使目前工作階段失效。連線時從 hub 依 `apiKeyId` 篩選用量;中斷後使用本機記錄,兩者不會鏡像。 +### 查看已儲存的 API 金鑰 + +查看已儲存金鑰的完整值,需要透過明確配對或受信任的 Tailscale 身分驗證簽發的儀表板工作階段。 +自動簽發的本機工作階段仍可查看遮罩清單,但無法顯示已儲存的金鑰值;管理員權杖也無法呼叫 +這項僅限工作階段的操作。請使用既有的 Remote Hub 配對流程,或在可用時透過受信任的 Tailscale 身分登入。 + +本機配對表單僅提供於透過 HTTP、以明確的 IP 位址 `127.0.0.1` 或 `[::1]` 開啟的同源獨立儀表板。 +來源必須與伺服器設定的回環 IP 綁定位址一致。如果透過 `localhost` 或其別名開啟,請先檢查伺服器綁定: +綁定為 `127.0.0.1` 時重新開啟 `http://127.0.0.1:`,綁定為 `::1` 時重新開啟 +`http://[::1]:`,然後配對。hostname 設為 `localhost` 的伺服器沒有符合條件的本機配對來源。 +儀表板無法從別名 URL 判斷實際綁定,因此只顯示一般拒絕說明,不建議替代 URL。 + +配對後請再次點選金鑰以要求其值;配對本身不會顯示金鑰。共用工作階段遭清除或取代、開始配對、 +離開面板、隱藏瀏覽器分頁或桌面視窗、切換伺服器時,已顯示的值與複製狀態都會清除。 +新建或替代金鑰的一次性值也會清除。已授權的工作階段到期或遭撤銷後,請重新配對。 + 選擇器會列出已啟用的原生與路由模型,以及全域 Codex reasoning 階梯。API 會先驗證所選強度是否 屬於全域階梯;Codex 仍會根據目標目錄條目再次校驗該 spawn 強度。 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md index 1d2476344d2..0adaf030a29 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md @@ -142,7 +142,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON ## 客戶端設定匯出 -### `ocx export --client ` +### `ocx export --client ` 印出連接到執行中代理的客戶端設定。此指令會用所選客戶端的原生格式,序列化含有 base URL、模型清單,以及適用的環境變數參考或 loopback 佔位符的 `opencodex` provider 區塊。 @@ -150,7 +150,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON | 旗標 | 動作 | | --- | --- | -| `--client ` | 必填。選擇客戶端設定格式。 | +| `--client ` | 必填。選擇客戶端設定格式。 | | `--json` | 僅在 stdout 印出設定 JSON,使重導向能擷取逐位元組輸出。所有診斷訊息(含 `--out` 寫入提示)皆送至 stderr。 | | `--out ` | 將設定寫入 ``。拒絕覆寫既有檔案。 | | `--force` | 允許 `--out` 覆寫既有檔案。 | @@ -181,6 +181,7 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 與 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 無——僅限 loopback,不會寫入 `api_keys` 項目 | | `omo` | `~/.omo/agent/models.json`(設定後依序由 `OMO_CODING_AGENT_DIR`、`SENPI_CODING_AGENT_DIR`、`PI_CODING_AGENT_DIR` 優先;相對路徑會被拒絕) | `omo-models.json` | 無——loopback 佔位符 | | `kilo` | `~/.config/kilo` 下最先存在的 `kilo.jsonc`、`kilo.json`、`opencode.jsonc`、`opencode.json` 或 `config.json`(`XDG_CONFIG_HOME` 可變更該目錄);皆不存在時使用 `kilo.jsonc` | `kilo.jsonc` | `OPENCODEX_KILO_API_KEY` | +| `commandcode` | `~/.commandcode/providers.json` (Windows 使用預設家目錄時:僅當 `HOME ?? USERPROFILE` 的值非空、非純空白且為 Windows 絕對路徑時使用,否則使用 `homedir()`;另行傳入的家目錄保持不變;檔案位於其下的 `.commandcode\providers.json`) | `providers.json` | 僅限迴環;`apiKey: false`;不需環境變數 | | `droid` | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | `factory-settings.json` | 僅限迴環;不需環境變數 | Raycast 匯出是一份獨立的 `providers.yaml` 文件,在 `providers` 序列中只有一個 `id: opencodex` 元素:`name: OpenCodex`、proxy 的 `/v1` base URL,以及每個路由模型及其 `abilities`(`tools` 與 `system_message` 一律支援,`vision` 依目錄的輸入模態而定,`reasoning_effort` 在模型有 effort 階梯時設定,`temperature` 對推理模型關閉)。Custom Providers 是 Raycast Pro 功能,且 Raycast 會監看該檔案,因此儲存後的變更不需重新啟動即可生效。格式說明見 [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)。不會寫入任何 `api_keys` 項目,所以此匯出僅限 loopback,非 loopback 的 bind 會被拒絕。 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 78d0118dd20..29f1287ebc7 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -188,6 +188,65 @@ API-key 供應商可持有字面值金鑰或環境參考。OAuth 供應商使用 除非你了解 Anthropic 帳號政策風險,否則保持停用。不確定時偏好手動 `ocx account use anthropic ` 切換。 ::: +### Anthropic · Pool 2 (`anthropic2`) + +`anthropic2`(「Anthropic · Pool 2」)是第二個內建的 Anthropic OAuth 供應商,擁有自己的帳戶池。它與 `anthropic` 使用同一套 Anthropic 實作:相同的登入流程、線路格式、原生 Messages 與 Responses 橋接以及模型中繼資料。只有帳號及其所在的帳戶池是分開的。以模型前綴選擇帳戶池:`anthropic/claude-sonnet-5` 使用主要帳戶池,`anthropic2/claude-sonnet-5` 使用 Pool 2。 + +在你新增之前,Pool 2 處於休眠狀態。使用 `ocx login anthropic2` 登入,或在儀表板的供應商頁面新增 **Anthropic · 帳戶池 2**。首次登入成功後會建立帶有標記 `"anthropicOAuthInstance": "anthropic2"` 的 `providers.anthropic2`。Pool 2 永遠不會成為預設供應商:不帶前綴的 `claude-*` 模型名稱、預設模型以及 Claude Code 呼叫端轉送仍解析至 `anthropic`。你先前自行建立、沒有標記的 `anthropic2` 項目保留其自訂意義;登入會拒絕該名稱衝突,既不改寫該項目,也不改寫你的憑證。 + +Pool 2 從自己的供應商項目讀取帳戶池設定。鍵、預設值與行為與上文的頂層 `anthropicAccountPool` 相同,且不從主要帳戶池繼承任何內容。`providers.anthropic.anthropicAccountPool`,以及任何其他供應商上的該欄位,都會被拒絕。 + +```json +{ + "anthropicAccountPool": { "enabled": true, "strategy": "quota" }, + "providers": { + "anthropic2": { + "adapter": "anthropic", + "authMode": "oauth", + "baseUrl": "https://api.anthropic.com", + "anthropicOAuthInstance": "anthropic2", + "anthropicAccountPool": { "enabled": true, "strategy": "round-robin" } + } + } +} +``` + +兩個帳戶池在 opencodex 內部彼此隔離: + +- **憑證:** Pool 2 帳號以獨立的 `anthropic2` 鍵儲存在受保護的憑證存放區中。若登入的權杖或已驗證的 Anthropic 帳號已儲存在另一個帳戶池,該登入會被拒絕。 +- **帳戶池設定與執行階段狀態:** 選擇、工作階段親和性、冷卻、暫停、模型路由與各帳號閾值都只屬於一個帳戶池。路由的 `fallback: true` 只在同一帳戶池內放寬。 +- **配額與用量:** 用量探測、配額快取與用量歸屬依帳戶池分別記錄,因此同一個帳號 ID 同時存在於兩個帳戶池時,會有兩筆獨立記錄。 +- **重設額度:** Pool 2 使用自己的日誌(`anthropic2-reset-grant-ledger.json`),與維持不變的主要帳戶池日誌並存。 +- **復原:** 直接的 `anthropic2/` 請求絕不會退回主要帳戶池,Pool 2 的速率限制或拒絕也絕不會讓主要帳戶池的帳號進入冷卻。明確指定兩個帳戶池的組合(combo)保留你宣告的目標。 + +這種分離是 opencodex 內部的路由邊界。它不會改變 Anthropic 對待你帳號的方式,也不會消除上文所述的帳號政策風險。 + +Pool 2 帳號只能透過瀏覽器 OAuth 新增。與 `anthropic` 不同,Pool 2 從不匯入、接管或回寫 Claude Code CLI 權杖;這項差異是刻意的。Pool 2 一開始是空的;沒有可用 Pool 2 帳號的 Pool 2 請求會以驗證錯誤失敗,而不會借用主要帳戶池或 Claude Code 的憑證。 + +帳號命令與管理 API 以名稱指定帳戶池:`ocx account pool anthropic2 …`、`ocx account auto-switch anthropic2 …`、`ocx account routes anthropic2 …` 以及 `ocx account anthropic-reset-grants --provider anthropic2`。帳戶池設定與重設額度端點接受 `provider: "anthropic2"`;省略時仍使用主要帳戶池。 + +#### 輔助功能的帳戶池選擇(`anthropicInstance`) + +網頁搜尋與視覺輔助功能在全域 `webSearchSidecar`、`visionSidecar` 設定以及 Claude Code 覆寫項目 `claudeCode.webSearchSidecar`、`claudeCode.visionSidecar` 中接受選用的 `anthropicInstance`。當輔助功能的後端為 Anthropic 時,儀表板會將其顯示為 **帳戶池** 選項。 + +| 值 | 行為 | +| --- | --- | +| 未設定(預設) | 跟隨目前請求的帳戶池:`anthropic2/` 請求使用 Pool 2,`anthropic/` 請求使用主要帳戶池。來自其他供應商的請求保留現有的輔助功能探索邏輯,該邏輯從不選擇 Pool 2。 | +| `"anthropic"` | 一律使用主要帳戶池。 | +| `"anthropic2"` | 一律使用 Pool 2。 | + +此欄位僅在輔助功能的後端解析為 Anthropic 時生效。與其他後端一起設定會造成驗證錯誤;由於網頁搜尋預設使用 OpenAI,請同時設定 `"backend": "anthropic"`。以另一個帳戶池限定的輔助模型(例如 `anthropic/claude-sonnet-5` 搭配 `"anthropicInstance": "anthropic2"`)同樣會被拒絕。如果所選帳戶池沒有可用帳號,輔助功能會在送出任何內容之前失敗,不會切換到另一個帳戶池。此時主要請求會在沒有該輔助功能的情況下照常進行。未設定的選擇會儲存為缺省,而不是 `"anthropic"`。 + +```json +{ + "webSearchSidecar": { "backend": "anthropic", "anthropicInstance": "anthropic2" } +} +``` + +:::caution[降級] +不含 Pool 2 的版本無法辨識 `anthropic2` 項目。安裝舊版之前,請停止代理、備份 `config.json` 與 `auth.json`,並從 `config.json` 中移除 `providers.anthropic2`。Pool 2 從不移動或改寫主要帳戶池的憑證。不支援在 Pool 2 啟用時就地降級。 +::: + ### 受管記錄結構 `apiKeys[]` 項目包含 `id`、`name`、生成的 `key` 與 ISO `createdAt` 字串。 diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/server.md b/docs-site/src/content/docs/zh-tw/reference/configuration/server.md index 00fd7020f53..59b3555f3ed 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/server.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/server.md @@ -230,3 +230,35 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_ Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available. The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support. + +## 歷史集區用量的連續性 + +提供者集區額度依路由選定的標準提供者計算,請求日誌仍保留帳戶顯示標籤。對於目前設定的每個提供者 `P`,讀取餘額時會自動將本安裝環境的加鹽集區別名 `h(pool, P)` 綁定至 `P`,相同別名下保留的歷史餘額也適用。各提供者的獨立額度不需要手動加入自身對應,也不需要用鹽值自行計算雜湊。 + +其他歷史標籤,包括帶有帳戶序號的標籤,不會自動綁定。每份原始餘額只計入所屬群組一次;仍未綁定的正餘額則保守地計入每個候選集區一次。已結算、預留及尚未確認的用量都會計入,因此未知歷史可能限制尚未使用的集區。目前名稱或帳戶排列順序不能證明歷史歸屬。根請求及身分額度仍各自獨立。 + +對於已確認歸屬的歷史,請在 `config.json` 最上層設定 `spendPoolAliases`,不要放在 `spend` 內。索引鍵是本安裝環境日誌中的完整歷史集區加鹽別名,由 32 個小寫十六進位字元組成;值必須是已確認且目前已設定的提供者完整 ID,前後不得有空白。請保密保存日誌、鹽值及確認依據。已設定提供者自己的集區別名不能對應到另一個提供者;提供者集合變更時會重新驗證。驗證只讀取現有鹽值,不會建立新鹽值。 + +自動綁定及明確對應都只在讀取時套用,不搬移原始餘額,也不將身分關聯寫入日誌。刪除明確對應會讓該歷史重新成為歸屬未知;但如果別名屬於目前設定的提供者自身,自動綁定仍然有效。無效對應在寫入時會遭拒;手動編輯留下無效對應時,既有額度仍保留,但拒絕新的集區請求。 + +閒置的未知歷史只有在最後活動時間嚴格早於 `spend.retentionDays` 的期限後才會到期。容量不足不會提早刪除正餘額的未知歷史。活動中的預留及其目標即使為零權杖也會保留;只有刪除紀錄已持久化後,請求准入判斷才會使用減少後的餘額。 + +### 預留及傳送次數上限 + +若根請求、身分或集區額度適用,每個目標或金鑰的首次傳送都必須先取得一般追蹤容量的預留。無法預留時,不會傳送至提供者。同一目標的重試沿用原有範圍;不同目標或金鑰需要新的預留。`L` 是整個請求共用的實體傳送次數上限,在請求開始時固定。預設為四次;既有 OAuth 請求設定最多允許十八次。 + +是否執行額度限制、適用的根請求/身分/集區權杖額度,以及 `L`,都在請求開始時確定。設定變更只影響變更後開始的請求。已經執行中的請求在所有重試及後續傳送中保留開始時的策略:啟用或降低額度不會收緊它,提高或刪除額度也不會放寬它。以僅觀察模式開始的請求會維持該模式直到結束。 + +最終結算會等待已開始傳送的回報完成。只有帳務已持久化後才會遺忘傳送 ID,餘額仍會保留。上限限制的是傳送次數而非帳單,超過初始估算的實際用量仍全數計入。沒有適用額度時,維持原有的僅觀察行為,包括追蹤容量已滿時省略記帳;不會新增身分關聯檢查點。 + +Claude CLI、CodeBuddy 及 Qoder 將每次 CLI 呼叫計為一次傳送,啟動前仍須取得一般預留,否則拒絕啟動。CLI 內部重試及工具回合不會另外消耗整個請求的傳送次數上限。費用依回報的實際用量結算,包括超出初始估算的部分。 + +### 回復至 2.80.0:契約 C + +新紀錄使用一般 v1 格式及 `pool` 別名範圍。未修改的 2.80.0 可依其逐標籤規則讀取、壓縮日誌及套用保留期限。請保留最新日誌及鹽值;還原較舊副本會遺失該副本之後記錄的支出。不需要回補修正、啟動阻擋機制或對帳指令。 + +降版後不保證維持標準提供者的彙總,也不保證與相同流量從未升級、始終由 2.80.0 處理時具有相同的剩餘額度。再次升級時,目前設定提供者的自動綁定及明確對應會套用至 2.80.0 仍保留的原始餘額。新檢查點不含需要舊版讀取器保留的身分關聯中繼資料。 + +未發布實驗版本的日誌若含 `pool-current` 或 `poolContinuity`,即使後來經過壓縮,仍不屬於契約 C。其原始餘額會作為未知歷史保留至正常到期,不會立即轉換或刪除。 + +完整但無效的紀錄,包括最後一行的 `null`,會使有額度限制的請求遭拒。壓縮仍可將有效帳務保留在乾淨的檢查點;目前程序的拒絕狀態會持續到重新啟動並讀取乾淨日誌。寫入中斷的最後一行 JSON 沿用原有復原規則。詳細範圍及錯誤碼請見[英文原文](/reference/configuration/server/#historical-pool-continuity)。 diff --git a/docs-site/src/content/docs/zh-tw/reference/management-api.md b/docs-site/src/content/docs/zh-tw/reference/management-api.md index 8bbfae3db3c..a67def0e48a 100644 --- a/docs-site/src/content/docs/zh-tw/reference/management-api.md +++ b/docs-site/src/content/docs/zh-tw/reference/management-api.md @@ -289,6 +289,8 @@ OpenAI 也遵循此規則:開關不會選擇特殊的 922k 模式。生效中 ### 系統生命週期 +`POST /api/system/restart` 在沒有請求本文或本文為 `{}` 時,保留預設的 60 秒排空等待。使用管理工作階段或管理員權杖的呼叫端可透過 `{"drainGraceMs":2000}` 明確選擇短等待;值必須是 1–60000 毫秒的整數。無效 JSON 或參數回傳 400,且不會開始重新啟動;綁定目標行程的本機 restart capability 不允許設定此參數(403)。回應中的 `drainTimeoutMs` 表示第一次接受的等待時間,重複呼叫不會變更已接受的時間或截止點。等待時間包含傳送接受回應前的延遲;清理與替代行程就緒仍分別保留獨立的 60 秒及 70 秒預算。被中斷的請求可能已經執行,重新傳送前應核對結果;此選項不會增加自動重新傳送。 + | 方法與路徑 | 用途 | 主要錯誤 | | --- | --- | --- | | `GET /api/system/memory` | 回傳純量行程、heap、串流、回應狀態、看門狗與活躍回合指標 | — | diff --git a/docs-site/src/data/github-stars.ts b/docs-site/src/data/github-stars.ts new file mode 100644 index 00000000000..2eedfe9a059 --- /dev/null +++ b/docs-site/src/data/github-stars.ts @@ -0,0 +1,51 @@ +// Build-time GitHub star count for the repository links in the header and on +// the landing page. One unauthenticated request per build, shared by every +// page. Any failure (offline build, rate limit, timeout, unexpected payload) +// yields null and the UI renders the link without a number, so the build never +// depends on the network. OPENCODEX_DOCS_OFFLINE=1 skips the request entirely. +// No token is read on purpose: the count is public and the build must not +// handle credentials for it. + +export const GITHUB_REPO = 'lidge-jun/opencodex'; +export const GITHUB_URL = `https://github.com/${GITHUB_REPO}`; +export const GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_REPO}`; + +const TIMEOUT_MS = 5000; + +let pending: Promise | undefined; + +/** Stargazer count, fetched once per build; null when unavailable. */ +export function getStarCount(): Promise { + pending ??= load(); + return pending; +} + +async function load(): Promise { + const env = (globalThis as { process?: { env?: Record } }).process?.env; + if (env?.OPENCODEX_DOCS_OFFLINE === '1') return null; + try { + const res = await fetch(GITHUB_API_URL, { + headers: { accept: 'application/vnd.github+json', 'user-agent': 'opencodex-docs-build' }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (!res.ok) return null; + const data: unknown = await res.json(); + const count = (data as { stargazers_count?: unknown } | null)?.stargazers_count; + return typeof count === 'number' && Number.isInteger(count) && count >= 0 ? count : null; + } catch { + return null; + } +} + +/** + * Compact star label: 999, 1.2k, 23.4k, 123k, 1.2m. Rounds down so the label + * never overstates the count. Labels remain static until the next build. + */ +export function formatStarCount(count: number): string { + if (count < 1000) return String(count); + if (count < 100_000) return `${trim(Math.floor(count / 100) / 10)}k`; + if (count < 1_000_000) return `${Math.floor(count / 1000)}k`; + return `${trim(Math.floor(count / 100_000) / 10)}m`; +} + +const trim = (n: number) => String(n).replace(/\.0$/, ''); diff --git a/docs-site/src/styles/custom.css b/docs-site/src/styles/custom.css index 45757eb5fe1..b20fb2a3985 100644 --- a/docs-site/src/styles/custom.css +++ b/docs-site/src/styles/custom.css @@ -639,9 +639,13 @@ main:has(.lp) .content-panel:first-child { padding-block: 0; } justify-content: center; min-height: 0; width: 100%; + aspect-ratio: 900 / 802; + max-width: 58rem; + flex: none; } .lp-scrub-canvas { display: none; + aspect-ratio: 900 / 802; max-width: min(58rem, 100%); width: 100%; height: auto; @@ -650,6 +654,7 @@ main:has(.lp) .content-panel:first-child { padding-block: 0; } } .lp-scrub-poster { display: block; + aspect-ratio: 900 / 802; max-width: min(58rem, 100%); width: 100%; height: auto; @@ -803,6 +808,63 @@ main:has(.lp) .content-panel:first-child { padding-block: 0; } } .lp-next-col a:hover { text-decoration: underline; text-underline-offset: 3px; } +.lp-github-row { display: flex; justify-content: center; padding-bottom: 2rem; } +.lp-github-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.625rem; + min-height: 44px; + padding: 0.625rem 1rem; + max-width: 100%; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--ocx-radius-pill); + background: var(--ocx-surface); + color: var(--sl-color-white); + font-size: 1rem; + text-decoration: none; +} +.lp-github-cta svg { flex: none; } +.lp-github-cta > span:first-of-type { min-width: 0; } +.lp-github-cta [data-gh-stars] { min-width: 6ch; text-align: end; font-variant-numeric: tabular-nums; } +.lp-github-cta:hover { border-color: var(--sl-color-gray-3); } +.lp-github-cta:focus-visible { outline: 2px solid var(--sl-color-accent); outline-offset: 3px; } + +/* Mobile text and links stay readable within each sheet. Desktop keeps its + existing composition; commands wrap instead of hiding overflow. */ +@media (max-width: 48rem) { + .lp-hero-copy h1 { font-size: clamp(1.875rem, 5.5vw, 2.5rem); overflow-wrap: anywhere; } + .lp-kline { font-size: clamp(2rem, 5vw, 2.5rem); } + .lp .lp-kline { animation: none; clip-path: none; } + .lp-sub { font-size: 1rem; } + .lp .lp-cell p, .lp .lp-scrub-caption, .lp .lp-disclaimer, + .lp .lp-dl-meta, .lp .lp-dl-note, .lp .lp-dl-foot, .lp .lp-dl-reco, + .lp .lp-next-col h3, .lp .lp-next-col a, .lp .lp-dl-alt, .lp .lp-dl-btn { font-size: 1rem; } + .lp-announce, .lp-chip-more a, .lp-inline-link, .lp-dl-alt, .lp-dl-foot a { + display: inline-flex; + align-items: center; + min-height: 44px; + min-width: 44px; + } + .lp-btn-primary, .lp-btn-ghost, .lp-dl-btn { min-height: 44px; } + .lp-terminal pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 0.875rem; line-height: 1.6; } + .lp-terminal code { white-space: inherit; } + .lp-note { display: block; white-space: normal; padding-left: 2ch; } + .lp-terminal code + code { display: inline-block; margin-top: 0.5rem; } + .lp-dl-foot code { white-space: normal; overflow-wrap: anywhere; max-width: 100%; } + .lp-next-col a { display: flex; align-items: center; min-height: 44px; padding-block: 0.25rem; } + .lp-next-col li + li { margin-top: 0.25rem; } + /* Remove the clipped heading embedded above the picker menus. */ + .lp-cell-picker { --picker-crop: 3rem; } + .lp-cell-picker img { + flex: none; + height: auto; + max-height: none; + margin-top: calc(-1 * var(--picker-crop)); + clip-path: inset(var(--picker-crop) 0 0 round var(--ocx-radius-sm)); + } +} + .lp-disclaimer { margin: 0; padding-block: 0 clamp(2.5rem, 6vh, 4rem); @@ -1118,6 +1180,9 @@ footer.sl-flex .meta { color: var(--sl-color-gray-4); } } @keyframes ocx-field-push { to { transform: scale(1.08); } } /* scrub canvas fills the stage height without overflowing the sheet */ + .lp-scene-inner.lp-scrub .lp-scrub-stage { + width: min(100%, calc((100dvh - var(--sl-nav-height) - 12rem) * 900 / 802)); + } .lp-scene-inner.lp-scrub .lp-scrub-canvas, .lp-scene-inner.lp-scrub .lp-scrub-poster { max-height: calc(100dvh - var(--sl-nav-height) - 12rem); diff --git a/gui/package.json b/gui/package.json index 33b37040cfb..c547ce4d0c7 100644 --- a/gui/package.json +++ b/gui/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint .", - "test": "bun test tests", + "test": "bun ../scripts/test-with-pinned-bun.ts tests", "lint:i18n": "oxlint src/pages src/components src/App.tsx src/main.tsx src/ui.tsx src/provider-workspace-data.ts", "doctor": "npx --yes react-doctor@0.9.11 --verbose --scope changed --base origin/main --no-telemetry", "doctor:full": "npx --yes react-doctor@0.9.11 --verbose --scope full --no-telemetry", diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index c54807fc8d3..0e595d7b4f7 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -14,6 +14,9 @@ Export-client marks (used by the API tab's connect rows, not the provider list): - `cline-color.svg` — reuses the existing provider mark already tracked in this directory for Cline CLI; no new image was imported for the file integration. +- `commandcode-color.svg` — reuses the Command Code provider mark already tracked in this + directory (the ⌘ mark in brand purple `#8C4EDD`, added in `b143984642`) for the Command + Code client integration; no new image was imported. - `pi.svg` — fetched 2026-08-02 from `https://pi.dev/favicon.svg`, the Pi project's own favicon, unmodified. Pi is `earendil-works/pi` diff --git a/gui/public/provider-icons/claude-green.svg b/gui/public/provider-icons/claude-green.svg new file mode 100644 index 00000000000..e2d31a1a7df --- /dev/null +++ b/gui/public/provider-icons/claude-green.svg @@ -0,0 +1 @@ +Claude Pool 2 \ No newline at end of file diff --git a/gui/src/api.ts b/gui/src/api.ts index 48d47eac70c..503a69e8bd5 100644 --- a/gui/src/api.ts +++ b/gui/src/api.ts @@ -7,6 +7,8 @@ import { adminTokenPromptAllowed, standaloneApiTargets, type ApiPlane, type ApiT * its existing readiness state; cancelled callers and newer valid sessions emit no notice. */ export const SESSION_UNAVAILABLE_EVENT = "opencodex:session-unavailable"; +/** Token identity changed; contains only the plane, never credential material. */ +export const SESSION_CHANGED_EVENT = "opencodex:session-changed"; const LEGACY_TOKEN_KEY = "opencodex-api-token"; // Any guarded route answers 401 for a bad token; this one is a cheap config read. /api/settings @@ -43,7 +45,7 @@ let rebootstrapTimeoutMs = SESSION_REBOOTSTRAP_TIMEOUT_MS; let resolutionWatchdogMs = RESOLUTION_WATCHDOG_MS; const runtimes = new Map(); -function reportSessionUnavailable(plane: ApiPlane): void { +function reportSessionEvent(plane: ApiPlane, event: string): void { if (typeof window === "undefined") return; // Take the constructor off the same window we dispatch on: a test harness (and a // sandboxed embed) can supply a document without installing CustomEvent globally. @@ -51,10 +53,16 @@ function reportSessionUnavailable(plane: ApiPlane): void { ?? (typeof CustomEvent === "function" ? CustomEvent : null); if (!Ctor) return; try { - window.dispatchEvent(new Ctor(SESSION_UNAVAILABLE_EVENT, { detail: { plane } })); + window.dispatchEvent(new Ctor(event, { detail: { plane } })); } catch { /* a shell that cannot receive the notice must not break the fetch path */ } } +function updateSession(state: TargetRuntime, session: ApiSessionState): void { + const changed = state.session.token !== session.token; + state.session = session; + if (changed) reportSessionEvent(state.target.id, SESSION_CHANGED_EVENT); +} + function blankSession(): ApiSessionState { return { token: null, csrfToken: null, browserOrigin: null, serverOrigin: null }; } @@ -72,6 +80,7 @@ export function configureApiTargets(targets: ApiTargets): void { configuredTargets = targets; for (const plane of ["machine", "shared"] as const) { const current = runtimes.get(plane); + if (current && !sameTarget(current.target, targets[plane])) updateSession(current, blankSession()); runtimes.set(plane, current && sameTarget(current.target, targets[plane]) ? { ...current, target: targets[plane] } : { target: targets[plane], session: blankSession(), resolutionInFlight: null, promptCancelled: false }); @@ -85,7 +94,7 @@ function runtime(plane: ApiPlane): TargetRuntime { function clearSessionIfCurrent(plane: ApiPlane, expected: string | null): void { const state = runtime(plane); - if (expected !== null && state.session.token === expected) state.session = blankSession(); + if (expected !== null && state.session.token === expected) updateSession(state, blankSession()); } function storeSession( @@ -98,10 +107,10 @@ function storeSession( const state = runtime(plane); if (!token?.startsWith("ocx_session_") || !csrfToken || browserOrigin !== window.location.origin || serverOrigin !== state.target.serverOrigin) { - state.session = blankSession(); + updateSession(state, blankSession()); return false; } - state.session = { token, csrfToken, browserOrigin, serverOrigin }; + updateSession(state, { token, csrfToken, browserOrigin, serverOrigin }); state.promptCancelled = false; return true; } @@ -120,7 +129,7 @@ export async function logoutApiSession(plane: ApiPlane): Promise { signal: bounded.signal, }); if (!response.ok) return false; - state.session = blankSession(); + updateSession(state, blankSession()); state.promptCancelled = false; return true; } catch { @@ -287,7 +296,7 @@ async function resolveTokenAfter401(plane: ApiPlane, failedToken: string | null, } const prompted = await requestAdminToken(token => verifyAdminToken(plane, token)); if (prompted) { - state.session = { token: prompted, csrfToken: null, browserOrigin: null, serverOrigin: state.target.serverOrigin }; + updateSession(state, { token: prompted, csrfToken: null, browserOrigin: null, serverOrigin: state.target.serverOrigin }); return prompted; } state.promptCancelled = true; @@ -337,14 +346,14 @@ export function installApiAuthFetch(): void { const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : undefined); const nextToken = await resolveTokenAfter401(classified.plane, token, callerSignal ?? undefined); if (!nextToken) { - if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionUnavailable(classified.plane); + if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionEvent(classified.plane, SESSION_UNAVAILABLE_EVENT); return response; } const [retryInput, retryInit] = withAuth(classified.plane, input, init, nextToken); const retry = await originalFetch(retryInput, retryInit); if (retry.status === 401) { clearSessionIfCurrent(classified.plane, nextToken); - if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionUnavailable(classified.plane); + if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionEvent(classified.plane, SESSION_UNAVAILABLE_EVENT); } return retry; }; diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index b286b95ee5d..be3c3c8efe0 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -137,6 +137,7 @@ export const INTEGRATION_TAB_HASHES = [ "integrations/dsh", "integrations/mcode", "integrations/zcode", + "integrations/commandcode", "integrations/prime", "integrations/aside", "integrations/raycast", diff --git a/gui/src/client-resource.ts b/gui/src/client-resource.ts index bed565430dc..3b2554d92af 100644 --- a/gui/src/client-resource.ts +++ b/gui/src/client-resource.ts @@ -18,6 +18,7 @@ export type ResourceSnapshot = { }; type Store = { + key: string; snapshot: ResourceSnapshot; listeners: Set<() => void>; /** listener → requested poll interval (undefined = no poll from that subscriber) */ @@ -58,6 +59,8 @@ type Store = { * different resource types (no runtime check — keys are an API contract). */ const stores = new Map>(); +// Explicit invalidations survive store eviction and fresh session-cache seeds. +const invalidatedKeys = new Set(); /** * Per-attempt deadline for every store fetch. Endpoints documented as slow finish in @@ -81,6 +84,7 @@ function getStore(key: string): Store { let store = stores.get(key) as Store | undefined; if (!store) { store = { + key, snapshot: { data: undefined, error: undefined, @@ -360,6 +364,7 @@ async function runFetch( if (gen !== store.generation || controller.signal.aborted) return; // Cleared on settle (not at subscribe) so StrictMode's aborted first mount still revalidates. store.seedNeedsRevalidate = false; + invalidatedKeys.delete(store.key); store.lastSettledAt = Date.now(); store.snapshot = { data, @@ -458,7 +463,7 @@ function subscribeResource( const stale = typeof staleAfterMs === "number" && store.lastSettledAt !== undefined && Date.now() - store.lastSettledAt > staleAfterMs; - if (store.snapshot.data === undefined || store.seedNeedsRevalidate || stale) { + if (store.snapshot.data === undefined || store.seedNeedsRevalidate || invalidatedKeys.has(key) || stale) { void runFetch(store, fetcher, { replaceInflight: true, owner: onStoreChange, deadlineMs }); } } @@ -530,7 +535,7 @@ function seedClientResourceIfEmpty(key: string, data: T, cachedAt?: number | setClientResourceData(key, data); // A seed fresher than the staleness window skips the mount revalidation entirely: // that is the whole request saved on a tab revisit. Unknown age counts as stale. - if (typeof staleAfterMs === "number" && typeof cachedAt === "number" && Date.now() - cachedAt < staleAfterMs) { + if (!invalidatedKeys.has(key) && typeof staleAfterMs === "number" && typeof cachedAt === "number" && Date.now() - cachedAt < staleAfterMs) { store.seedNeedsRevalidate = false; store.lastSettledAt = cachedAt; } @@ -638,6 +643,18 @@ export function useKeyedClientResource( return resource; } +/** Mark a resource stale without publishing a snapshot. Active subscribers refresh + * through runFetch; an inactive/evicted key revalidates on its next subscription. */ +export function invalidateClientResource(key: string): void { + invalidatedKeys.add(key); + const store = stores.get(key); + if (!store) return; + const entry = pickFetcherEntry(store); + if (entry) { + void runFetch(store, entry.fetcher, { replaceInflight: true, owner: entry.owner, deadlineMs: entry.deadlineMs }); + } +} + /** Publish data for a key and invalidate any in-flight fetch so it cannot stomp this write. */ export function setClientResourceData(key: string, data: T) { const store = getStore(key); @@ -670,6 +687,7 @@ export function clearClientResourceStoresForTests(): void { store.inflightOwner = null; } stores.clear(); + invalidatedKeys.clear(); for (const [intervalMs, bucket] of [...pollBuckets]) { if (bucket.timer !== null) clearInterval(bucket.timer); pollBuckets.delete(intervalMs); diff --git a/gui/src/components/apikeys-workspace/ApiKeysListPanel.tsx b/gui/src/components/apikeys-workspace/ApiKeysListPanel.tsx index eef15854759..25dbc41ef1d 100644 --- a/gui/src/components/apikeys-workspace/ApiKeysListPanel.tsx +++ b/gui/src/components/apikeys-workspace/ApiKeysListPanel.tsx @@ -13,12 +13,16 @@ * concluded a key could not be removed. A hover-only control would repeat that on * touch screens, so it stays on screen. */ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { IconTrash } from "../../icons"; import { useT } from "../../i18n/shared"; -import { formatCreatedDate, type ApiKeyEntry } from "../../pages/api-keys-utils"; +import { formatCreatedDate, type ApiKeyEntry, type RevealKeyResult } from "../../pages/api-keys-utils"; import type { UsageReadMetadata } from "../../usage-summary-resource"; import { UsageIncompleteNotice } from "../usage-incomplete-notice"; +import { Notice } from "../../ui"; +import { isStandaloneRuntime, standaloneApiTargets } from "../../api-targets"; +import { ConnectPairingForm } from "../../connect-pairing"; +import { useKeyDisclosure, type DisclosureResetReason } from "../../use-key-disclosure"; export default function ApiKeysListPanel({ keys, @@ -27,6 +31,9 @@ export default function ApiKeysListPanel({ attributionSince, usageMetadata, localeTag, + apiBase, + active = true, + onPairingStart, busy, onSelect, onDelete, @@ -39,18 +46,24 @@ export default function ApiKeysListPanel({ attributionSince?: string; usageMetadata?: UsageReadMetadata; localeTag?: string; + /** Management API origin a reveal denial pairs this dashboard against. */ + apiBase: string; + active?: boolean; + onPairingStart?: () => void; /** A mutation is in flight; its result is bound to one key, so navigation waits. */ busy: boolean; onSelect: (id: string) => void; /** Resolves true only when the key is really gone. */ onDelete?: (id: string) => Promise; - /** The full key, or null when the server would not hand it over. */ - onReveal?: (id: string) => Promise; + /** The reveal outcome: the full key, a standing refusal, or a transient failure. */ + onReveal?: (id: string) => Promise; }) { const t = useT(); const [revealed, setRevealed] = useState>({}); const [revealPendingId, setRevealPendingId] = useState(null); const [revealFailedId, setRevealFailedId] = useState(null); + /** A refused row offers pairing; success still needs a fresh reveal click. */ + const [revealDeniedId, setRevealDeniedId] = useState(null); const [copiedId, setCopiedId] = useState(null); const [copyFailedId, setCopyFailedId] = useState(null); const copiedTimer = useRef(null); @@ -58,6 +71,20 @@ export default function ApiKeysListPanel({ const [confirmArmed, setConfirmArmed] = useState(false); const [deletingId, setDeletingId] = useState(null); const [deleteFailedId, setDeleteFailedId] = useState(null); + const pendingReveal = useRef(null); + const clearDisclosure = useCallback((reason: DisclosureResetReason) => { + const interrupted = pendingReveal.current; + pendingReveal.current = null; + setRevealed({}); + setRevealPendingId(null); + setRevealFailedId(null); + setRevealDeniedId(current => reason === "session" ? interrupted ?? current : reason === "pairing" ? current : null); + setCopiedId(null); + setCopyFailedId(null); + if (copiedTimer.current !== null) window.clearTimeout(copiedTimer.current); + copiedTimer.current = null; + }, []); + const disclosure = useKeyDisclosure(apiBase, active, clearDisclosure); // Same 300ms fuse as the detail pane: the click that opened the confirmation // must not be able to land on the button that replaced it. @@ -79,22 +106,46 @@ export default function ApiKeysListPanel({ const rowLocked = busy || deletingId !== null; + // The same gate RemoteLink applies to its local-pairing offer: only the + // literal same-origin loopback transport the standalone grant mint accepts. + // An alias URL does not reveal the configured bind host, so never invent + // a recovery URL from it; keep the generic denial guidance. + const localPairingTarget = standaloneApiTargets(apiBase).shared; + const canPairLocally = isStandaloneRuntime() + && window.location.protocol === "http:" + && ["127.0.0.1", "[::1]"].includes(window.location.hostname) + && localPairingTarget.serverOrigin === window.location.origin; + const toggleReveal = async (k: ApiKeyEntry) => { + const generation = disclosure.generation.current; + if (!disclosure.current(generation)) return; const id = k.id; setRevealFailedId(null); + setRevealDeniedId(null); if (currentReveal(k) !== undefined) { setRevealed(({ [id]: _hidden, ...rest }) => rest); return; } if (!onReveal || revealPendingId) return; + pendingReveal.current = id; setRevealPendingId(id); try { - const full = await onReveal(id); - if (deletedIds.current.has(id)) return; - if (full) setRevealed(prev => ({ ...prev, [id]: full })); + const result = await onReveal(id); + if (!disclosure.current(generation) || deletedIds.current.has(id)) return; + if (result.ok) setRevealed(prev => ({ ...prev, [id]: result.key })); + else if (result.kind === "denied") { + disclosure.invalidate(); + onPairingStart?.(); + setRevealDeniedId(id); + } else setRevealFailedId(id); + } catch { + if (disclosure.current(generation)) setRevealFailedId(id); } finally { - setRevealPendingId(null); + if (disclosure.current(generation)) { + pendingReveal.current = null; + setRevealPendingId(null); + } } }; @@ -104,6 +155,8 @@ export default function ApiKeysListPanel({ }; const copyKey = (id: string, value: string) => { + const generation = disclosure.generation.current; + if (!disclosure.current(generation)) return; setCopyFailedId(null); let write: Promise | undefined; try { @@ -116,13 +169,14 @@ export default function ApiKeysListPanel({ return; } write.then(() => { + if (!disclosure.current(generation) || deletedIds.current.has(id)) return; setCopiedId(id); if (copiedTimer.current !== null) window.clearTimeout(copiedTimer.current); copiedTimer.current = window.setTimeout(() => { setCopiedId(current => current === id ? null : current); copiedTimer.current = null; }, 2000); - }, () => copyFailed(id)); + }, () => { if (disclosure.current(generation)) copyFailed(id); }); }; useEffect(() => () => { @@ -156,6 +210,27 @@ export default function ApiKeysListPanel({
+ {revealDeniedId !== null && ( + <> + {t("api.key.revealDenied")} + {/* Pairing authorizes the browser, but never discloses a value itself. */} + {canPairLocally && ( + { + disclosure.invalidate(); + onPairingStart?.(); + }} + onConnected={() => { + disclosure.invalidate(); + onPairingStart?.(); + setRevealDeniedId(null); + }} + /> + )} + + )} {keysLoading ? (
) : keys.length === 0 ? ( diff --git a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx index 745edc84523..0432c0db6bd 100644 --- a/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx +++ b/gui/src/components/apikeys-workspace/ApiKeysWorkspace.tsx @@ -16,6 +16,7 @@ import { type ApiKeyEntry, type ApiSurfacesInfo, type ModelTests, + type RevealKeyResult, } from "../../pages/api-keys-utils"; import { ApiKeysEndpointsPanel, @@ -35,6 +36,7 @@ export interface ApiKeysWorkspaceProps { /** Management API origin the client-config panel fetches from. */ apiBase: string; active?: boolean; + onPairingStart?: () => void; /** Dataset-level. Absent means nothing is attributable yet — a different * statement from a key whose counters read zero. */ attributionSince?: string; @@ -74,8 +76,10 @@ export interface ApiKeysWorkspaceProps { onDismissNewKey: () => void; onCopyKey: () => void; onDelete: (id: string) => Promise; - /** Full key for a click-to-reveal in the key table; absent hides the control. */ - onRevealKey?: (id: string) => Promise; + /** Full key for a click-to-reveal in the key table; absent hides the control. + * The result discriminates a standing 403 refusal — the list answers it with + * the pairing surface, not a retry hint — from a transient failure. */ + onRevealKey?: (id: string) => Promise; onRename: (id: string, name: string) => Promise; onRotationStart?: (id: string) => Promise; onRotationCommit?: (id: string, rotationId: string) => Promise; @@ -94,6 +98,7 @@ export default function ApiKeysWorkspace({ keys, apiBase, active = true, + onPairingStart, attributionSince, historyTruncated, usageMetadata, @@ -533,6 +538,9 @@ export default function ApiKeysWorkspace({ attributionSince={attributionSince} usageMetadata={usageMetadata} localeTag={localeTag} + apiBase={apiBase} + active={active} + onPairingStart={onPairingStart} busy={mutationPending} onDelete={onDelete} onReveal={onRevealKey} diff --git a/gui/src/components/apikeys-workspace/client-config-clients.ts b/gui/src/components/apikeys-workspace/client-config-clients.ts index e4ac4a62e1a..8689f3b0a46 100644 --- a/gui/src/components/apikeys-workspace/client-config-clients.ts +++ b/gui/src/components/apikeys-workspace/client-config-clients.ts @@ -8,7 +8,7 @@ * with EXPORT_CLIENT_IDS by hand; adding a client server-side renders no row * until this tuple changes. */ -export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"] as const; +export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "commandcode", "prime", "aside", "raycast", "omo", "cline", "kilo", "droid"] as const; export type ExportClientId = (typeof CLIENTS)[number]; export const CLIENT_LABEL_KEYS = { @@ -22,6 +22,7 @@ export const CLIENT_LABEL_KEYS = { dsh: "api.clientConfig.clientDsh", mcode: "api.clientConfig.clientMcode", zcode: "api.clientConfig.clientZcode", + commandcode: "api.clientConfig.clientCommandCode", prime: "api.clientConfig.clientPrime", aside: "api.clientConfig.clientAside", raycast: "api.clientConfig.clientRaycast", @@ -81,6 +82,7 @@ export const CLIENT_MARKS: Partial> = { // so it would paint the plate and throw the face away — see the README. omo: "/provider-icons/omo.svg", cline: "/provider-icons/cline-color.svg", + commandcode: "/provider-icons/commandcode-color.svg", kilo: "/provider-icons/kilo.svg", droid: "/provider-icons/factory-droid.svg", }; diff --git a/gui/src/components/integration-marks.ts b/gui/src/components/integration-marks.ts index 669cc2ba114..a7db3023ba9 100644 --- a/gui/src/components/integration-marks.ts +++ b/gui/src/components/integration-marks.ts @@ -60,6 +60,7 @@ export const INTEGRATION_MARKS: Record = { raycast: CLIENT_MARKS.raycast ?? null, omo: CLIENT_MARKS.omo ?? null, cline: CLIENT_MARKS.cline ?? null, + commandcode: CLIENT_MARKS.commandcode ?? null, kilo: CLIENT_MARKS.kilo ?? null, droid: CLIENT_MARKS.droid ?? null, }; diff --git a/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx b/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx index 612d93efa54..cf89714bf8c 100644 --- a/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx +++ b/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx @@ -1,3 +1,4 @@ +import type { AnthropicInstanceId } from "../../../../src/providers/anthropic-instance-id"; /** * Opt-in Anthropic OAuth account pool controls (#294). * Experimental. The conditions it is meant for are static helper text next to the toggle, @@ -69,10 +70,12 @@ function enabledStatus( export default function AnthropicAccountPoolSettings({ apiBase, + provider = "anthropic", accountCount, onThresholdChange, }: { apiBase: string; + provider?: AnthropicInstanceId; accountCount: number; onThresholdChange?: (threshold: number) => void; }) { @@ -90,6 +93,7 @@ export default function AnthropicAccountPoolSettings({ const onThresholdChangeRef = useRef(onThresholdChange); const mountedRef = useRef(true); const apiBaseRef = useRef(apiBase); + const providerRef = useRef(provider); const saveAbortRef = useRef(null); useLayoutEffect(() => { @@ -104,7 +108,8 @@ export default function AnthropicAccountPoolSettings({ useLayoutEffect(() => { onThresholdChangeRef.current = onThresholdChange; apiBaseRef.current = apiBase; - }, [apiBase, onThresholdChange]); + providerRef.current = provider; + }, [apiBase, provider, onThresholdChange]); useEffect(() => { let cancelled = false; @@ -119,7 +124,7 @@ export default function AnthropicAccountPoolSettings({ // in-flight cancellation, which is the part that actually needs to be cancellable. void Promise.resolve() // Through the shared pool client, which speaks the one contract every kind answers on. - .then(() => getPoolSettings(apiBase, "anthropic", (input, init) => fetch(input, init), { signal: ac.signal })) + .then(() => getPoolSettings(apiBase, provider, (input, init) => fetch(input, init), { signal: ac.signal })) .then(settings => { if (!settings) throw new Error("load"); return settings; @@ -154,7 +159,7 @@ export default function AnthropicAccountPoolSettings({ cancelled = true; ac.abort(); }; - }, [apiBase, reloadVersion]); + }, [apiBase, provider, reloadVersion]); const save = useCallback(async (next: { enabled: boolean; @@ -165,10 +170,11 @@ export default function AnthropicAccountPoolSettings({ nativeMessages: boolean; }) => { const requestApiBase = apiBase; + const requestProvider = provider; saveAbortRef.current?.abort(); const controller = new AbortController(); saveAbortRef.current = controller; - const currentRequest = () => mountedRef.current && apiBaseRef.current === requestApiBase + const currentRequest = () => mountedRef.current && apiBaseRef.current === requestApiBase && providerRef.current === requestProvider && saveAbortRef.current === controller && !controller.signal.aborted; const previousState = state; setSaving(true); @@ -177,7 +183,7 @@ export default function AnthropicAccountPoolSettings({ try { // The client owns the field mapping: `threshold` becomes `autoSwitchThreshold` and the // provider is always sent, so no call site can forget either. - const json = await putPoolSettings(requestApiBase, "anthropic", { + const json = await putPoolSettings(requestApiBase, requestProvider, { enabled: next.enabled, threshold: next.threshold, strategy: next.strategy, @@ -221,9 +227,9 @@ export default function AnthropicAccountPoolSettings({ } finally { const ownsSave = saveAbortRef.current === controller; if (ownsSave) saveAbortRef.current = null; - if (ownsSave && mountedRef.current && apiBaseRef.current === requestApiBase) setSaving(false); + if (ownsSave && mountedRef.current && apiBaseRef.current === requestApiBase && providerRef.current === requestProvider) setSaving(false); } - }, [apiBase, state, t]); + }, [apiBase, provider, state, t]); const enabled = state?.enabled === true; const threshold = state?.threshold ?? 80; diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index 6c3c6cc9a23..da5419c975c 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -1,3 +1,4 @@ +import { anthropicInstanceRowShapeMatches, isAnthropicInstanceId } from "../../../../src/providers/anthropic-instance-id"; /** * ProviderAuthPanel — OAuth accounts, API-key pool, and forward-auth * embedding for the workspace Settings tab (WP091). Consumes WP040+WP060 @@ -248,13 +249,21 @@ export default function ProviderAuthPanel({ const grokCoupons = useGrokResetCoupons({ apiBase, accountIds: grokAccountIds, enabled: grokCouponsEnabled }); const [couponAccount, setCouponAccount] = useState(null); // Claude usage resets ride a separate usage read, like the Grok coupons above. - const claudeGrantsEnabled = isOauth && item.name === "anthropic" && accounts.length > 0; + const isAnthropicPool = isAnthropicInstanceId(item.name) && anthropicInstanceRowShapeMatches(item.name, item); + const claudeGrantsEnabled = isOauth && isAnthropicPool && accounts.length > 0; const claudeAccountIds = useMemo( () => (claudeGrantsEnabled ? accounts.filter(account => !accountShowsReauth(account)).map(account => account.id) : []), [claudeGrantsEnabled, accounts], ); - const claudeGrants = useAnthropicResetGrants({ apiBase, accountIds: claudeAccountIds, enabled: claudeGrantsEnabled }); - const [grantAccount, setGrantAccount] = useState(null); + const claudeGrants = useAnthropicResetGrants({ apiBase, provider: isAnthropicInstanceId(item.name) ? item.name : "anthropic", accountIds: claudeAccountIds, enabled: claudeGrantsEnabled }); + // The open reset dialog belongs to the pool it was opened in. Switching server or pool + // hides it during render rather than resetting it from an effect. + const grantScope = `${apiBase}\0${item.name}`; + const [grantSelection, setGrantSelection] = useState<{ scope: string; account: OAuthAccountRow } | null>(null); + const grantAccount = grantSelection?.scope === grantScope ? grantSelection.account : null; + const setGrantAccount = (account: OAuthAccountRow | null) => { + setGrantSelection(account ? { scope: grantScope, account } : null); + }; const refreshQuota = async () => { if (!onRefreshQuota || refreshingQuota) return; const generation = ++quotaRefreshGeneration.current; @@ -428,10 +437,11 @@ export default function ProviderAuthPanel({ )} {isOauth && ( <> - {item.name === "anthropic" && ( + {isAnthropicPool && ( { void authHandlers?.onAccountPoolThreshold?.(item.name, threshold); }} /> @@ -628,7 +638,7 @@ export default function ProviderAuthPanel({
- {item.name === "anthropic" && account.autoSwitchThresholdOverride !== undefined + {isAnthropicPool && account.autoSwitchThresholdOverride !== undefined && account.autoSwitchThreshold !== undefined && authHandlers.onAccountThreshold && ( void; + /** Let a secret-bearing surface expire its previous disclosure lifetime. */ + onPairingStart?: () => void; /** Explicit local pairing for a standalone link join; never an automatic bootstrap. */ local?: boolean; }) { @@ -27,6 +30,7 @@ export function ConnectPairingForm({ const submit = async (event: FormEvent) => { event.preventDefault(); if (busy) return; + onPairingStart?.(); setBusy(true); setError(null); const controller = new AbortController(); diff --git a/gui/src/hooks/useAnthropicResetGrants.ts b/gui/src/hooks/useAnthropicResetGrants.ts index 177b57c0f00..7152e6cbcb8 100644 --- a/gui/src/hooks/useAnthropicResetGrants.ts +++ b/gui/src/hooks/useAnthropicResetGrants.ts @@ -1,3 +1,4 @@ +import type { AnthropicInstanceId } from "../../../src/providers/anthropic-instance-id"; /** * useAnthropicResetGrants — reads and spends Claude usage-limit reset grants for * Anthropic OAuth accounts through `GET /api/anthropic/reset-grants` and @@ -12,7 +13,7 @@ * `unknown_outcome`, 500 `journal_write_failed`, 409 `in_flight`) is reported as * `unknown` so the dialog keeps the operation id and can only retry that same id. */ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { createBoundedFetch } from "../bounded-fetch"; export type AnthropicResetWindow = "five_hour" | "seven_day" | "seven_day_overage_included"; @@ -65,6 +66,7 @@ const READ_TIMEOUT_MS = 20_000; /** Upstream claim bound (25 s) plus the profile and gate reads in front of it. */ const SPEND_TIMEOUT_MS = 60_000; const MAX_READS_IN_FLIGHT = 3; +const NO_ENTRIES: Record = {}; const WINDOWS: readonly AnthropicResetWindow[] = ["five_hour", "seven_day", "seven_day_overage_included"]; const UNKNOWN_CODES = new Set(["unknown_outcome", "journal_write_failed", "in_flight"]); @@ -143,18 +145,43 @@ function errorCode(value: unknown): string { return "failed"; } -export function useAnthropicResetGrants({ apiBase, accountIds, enabled }: { +export function useAnthropicResetGrants({ apiBase, provider = "anthropic", accountIds, enabled }: { apiBase: string; + provider?: AnthropicInstanceId; accountIds: string[]; enabled: boolean; }): AnthropicResetGrantController { - const [entries, setEntries] = useState>({}); + const namespace = `${apiBase}\0${provider}`; + // Entries are stored with the pool namespace that produced them. A render for another + // namespace treats them as absent, so equal account IDs in A and B never share a row. + const [store, setStore] = useState<{ namespace: string; entries: Record }>( + () => ({ namespace, entries: {} }), + ); const epoch = useRef(0); const tokens = useRef(new Map()); const gate = useRef<{ active: number; waiting: Array<() => void> }>({ active: 0, waiting: [] }); const identity = accountIds.join("\u0000"); + useLayoutEffect(() => { + epoch.current += 1; + tokens.current.clear(); + // A stalled read from the previous pool must not occupy this pool's read slots. Reads + // already running settle against the queue they captured; queued ones are released and + // return at their epoch check without sending. + const previous = gate.current; + gate.current = { active: 0, waiting: [] }; + for (const release of previous.waiting.splice(0)) release(); + return () => { epoch.current += 1; }; + }, [namespace]); + + const setEntry = useCallback((accountId: string, entry: AnthropicGrantEntry) => { + setStore(current => ({ + namespace, + entries: { ...(current.namespace === namespace ? current.entries : {}), [accountId]: entry }, + })); + }, [namespace]); const read = useCallback(async (accountId: string, rosterEpoch: number) => { + if (epoch.current !== rosterEpoch) return; const token = (tokens.current.get(accountId) ?? 0) + 1; tokens.current.set(accountId, token); const queue = gate.current; @@ -165,27 +192,27 @@ export function useAnthropicResetGrants({ apiBase, accountIds, enabled }: { const current = () => epoch.current === rosterEpoch && tokens.current.get(accountId) === token; const bounded = createBoundedFetch(READ_TIMEOUT_MS); try { + if (!current()) return; const response = await fetch( - `${apiBase}/api/anthropic/reset-grants?accountId=${encodeURIComponent(accountId)}`, + `${apiBase}/api/anthropic/reset-grants?accountId=${encodeURIComponent(accountId)}${provider === "anthropic" ? "" : `&provider=${encodeURIComponent(provider)}`}`, { signal: bounded.signal }, ); - const snapshot = response.ok ? parseAnthropicGrantSnapshot(await response.json().catch(() => null)) : null; + const raw = response.ok ? await response.json().catch(() => null) : null; + const ownsResponse = isRecord(raw) && (raw.provider === provider || (provider === "anthropic" && raw.provider === undefined)); + const snapshot = ownsResponse ? parseAnthropicGrantSnapshot(raw) : null; if (!current()) return; - setEntries(existing => ({ - ...existing, - [accountId]: snapshot - ? { status: "ready", snapshot } - : { status: "error", reason: response.status === 401 ? "auth" : "upstream" }, - })); + setEntry(accountId, snapshot + ? { status: "ready", snapshot } + : { status: "error", reason: response.status === 401 ? "auth" : "upstream" }); } catch { if (!current()) return; - setEntries(existing => ({ ...existing, [accountId]: { status: "error", reason: "upstream" } })); + setEntry(accountId, { status: "error", reason: "upstream" }); } finally { bounded.clear(); queue.active -= 1; queue.waiting.shift()?.(); } - }, [apiBase]); + }, [apiBase, provider, setEntry]); useEffect(() => { if (!enabled || identity === "") return; @@ -197,20 +224,21 @@ export function useAnthropicResetGrants({ apiBase, accountIds, enabled }: { }, [enabled, identity, read]); const refresh = useCallback(async (accountId: string) => { - setEntries(current => ({ ...current, [accountId]: { status: "loading" } })); + setEntry(accountId, { status: "loading" }); await read(accountId, epoch.current); - }, [read]); + }, [read, setEntry]); const spend = useCallback(async ( accountId: string, request: { grantId: string; operationId: string }, ): Promise => { + const spendEpoch = epoch.current; const bounded = createBoundedFetch(SPEND_TIMEOUT_MS); try { const response = await fetch(`${apiBase}/api/anthropic/reset-grants/consume`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ accountId, grantId: request.grantId, operationId: request.operationId }), + body: JSON.stringify({ ...(provider === "anthropic" ? {} : { provider }), accountId, grantId: request.grantId, operationId: request.operationId }), signal: bounded.signal, }); if (!response.ok) { @@ -218,8 +246,9 @@ export function useAnthropicResetGrants({ apiBase, accountIds, enabled }: { return UNKNOWN_CODES.has(code) ? { kind: "unknown", code } : { kind: "refused", code }; } const data = await response.json().catch(() => null) as unknown; - if (!isRecord(data) || typeof data.code !== "string") return { kind: "unknown", code: "unknown_outcome" }; - void read(accountId, epoch.current); + if (!isRecord(data) || typeof data.code !== "string" + || (data.provider !== provider && !(provider === "anthropic" && data.provider === undefined))) return { kind: "unknown", code: "unknown_outcome" }; + if (spendEpoch === epoch.current) void read(accountId, spendEpoch); return { kind: "settled", code: data.code, @@ -232,7 +261,7 @@ export function useAnthropicResetGrants({ apiBase, accountIds, enabled }: { } finally { bounded.clear(); } - }, [apiBase, read]); + }, [apiBase, provider, read]); - return { entries, refresh, spend }; + return { entries: store.namespace === namespace ? store.entries : NO_ENTRIES, refresh, spend }; } diff --git a/gui/src/hooks/useProviderAccountPools.ts b/gui/src/hooks/useProviderAccountPools.ts index d9a04b3e453..b934bd94f60 100644 --- a/gui/src/hooks/useProviderAccountPools.ts +++ b/gui/src/hooks/useProviderAccountPools.ts @@ -125,8 +125,11 @@ export function useProviderAccountPools(deps: { } = deps; const [accountSets, setAccountSets] = useState>({}); const [accountLoadStates, setAccountLoadStates] = useState>({}); - const [switchingAccount, setSwitchingAccount] = useState<{ provider: string; accountId: string } | null>(null); - const [pausingAccount, setPausingAccount] = useState<{ provider: string; accountId: string; paused: boolean } | null>(null); + const [switchingAccounts, setSwitchingAccounts] = useState>({}); + const switchingAccount = Object.values(switchingAccounts)[0] ?? null; + const setSwitchingAccount = (target: { provider: string; accountId: string } | null) => setSwitchingAccounts(target ? { [target.provider]: target } : {}); + const [pausingAccounts, setPausingAccounts] = useState>({}); + const pausingAccount = Object.values(pausingAccounts)[0] ?? null; const [openAccounts, setOpenAccounts] = useState>({}); const [keyPools, setKeyPools] = useState>({}); const [addingKeyFor, setAddingKeyFor] = useState(null); @@ -136,7 +139,8 @@ export function useProviderAccountPools(deps: { const quotaGenerationRef = useRef>({}); const selectionMutationsRef = useRef(new Map()); const requestsRef = useRef(new Set()); - const pausingAccountRef = useRef<{ provider: string; accountId: string } | null>(null); + const pausingAccountRef = useRef(new Map()); + const switchingAccountRef = useRef(new Map()); const mountedRef = useRef(true); const serverRef = useRef(apiBase); useEffect(() => { @@ -145,13 +149,16 @@ export function useProviderAccountPools(deps: { const rosterGenerations = rosterGenerationRef.current; const quotaGenerations = quotaGenerationRef.current; const mutations = selectionMutationsRef.current; + const pausingOwners = pausingAccountRef.current; + const switchingOwners = switchingAccountRef.current; mountedRef.current = true; const serverChanged = serverRef.current !== apiBase; serverRef.current = apiBase; if (serverChanged) void Promise.resolve().then(() => { if (!mountedRef.current || serverRef.current !== apiBase) return; setAccountSets({}); - setPausingAccount(null); + setPausingAccounts({}); + setSwitchingAccounts({}); setKeyPools({}); setAccountLoadStates({}); }); @@ -161,7 +168,8 @@ export function useProviderAccountPools(deps: { for (const key of Object.keys(rosterGenerations)) rosterGenerations[key] += 1; for (const key of Object.keys(quotaGenerations)) quotaGenerations[key] += 1; mutations.clear(); - pausingAccountRef.current = null; + pausingOwners.clear(); + switchingOwners.clear(); for (const controller of requests) controller.abort(); requests.clear(); }; @@ -170,7 +178,6 @@ export function useProviderAccountPools(deps: { // uncancellable, and StrictMode double-invokes their effects, so dedupe by list identity here. const accountSetsKeyRef = useRef(null); const keyPoolsKeyRef = useRef(null); - const switchingAccountRef = useRef<{ provider: string; accountId: string } | null>(null); const readRoster = useCallback(async (url: string, signal?: AbortSignal): Promise => { const bounded = createBoundedFetch(20_000); @@ -386,10 +393,10 @@ export function useProviderAccountPools(deps: { }; const switchAccount = async (provider: string, account: OAuthAccount) => { - if (account.active || account.needsReauth || account.paused || switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(`oauth:${provider}`)) return; + if (account.active || account.needsReauth || account.paused || switchingAccountRef.current.has(provider) || pausingAccountRef.current.has(provider) || selectionMutationsRef.current.has(`oauth:${provider}`)) return; const target = { provider, accountId: account.id }; - switchingAccountRef.current = target; - setSwitchingAccount(target); + switchingAccountRef.current.set(provider, target); + setSwitchingAccounts(current => ({ ...current, [provider]: target })); const key = invalidateSelectionReads(provider, "oauth"); const mutation = Symbol(); selectionMutationsRef.current.set(key, mutation); @@ -419,16 +426,16 @@ export function useProviderAccountPools(deps: { selectionMutationsRef.current.delete(key); void refreshAccountRosters({ provider, kind: "oauth" }); } - if (switchingAccountRef.current?.provider === target.provider && switchingAccountRef.current.accountId === target.accountId) { - switchingAccountRef.current = null; - if (aliveRef.current) setSwitchingAccount(null); + if (switchingAccountRef.current.get(provider) === target) { + switchingAccountRef.current.delete(provider); + if (aliveRef.current) setSwitchingAccounts(current => { const next = { ...current }; delete next[provider]; return next; }); } } }; const setAccountThreshold = async (provider: string, account: OAuthAccount, threshold: number | null): Promise => { const key = `oauth:${provider}`; - if (switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(key)) return false; + if (switchingAccountRef.current.has(provider) || pausingAccountRef.current.has(provider) || selectionMutationsRef.current.has(key)) return false; const mutationKey = invalidateSelectionReads(provider, "oauth"); const mutation = Symbol(); selectionMutationsRef.current.set(mutationKey, mutation); @@ -477,10 +484,10 @@ export function useProviderAccountPools(deps: { }; const pauseAccount = async (provider: string, account: OAuthAccount, paused: boolean) => { - if (switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(`oauth:${provider}`)) return; + if (switchingAccountRef.current.has(provider) || pausingAccountRef.current.has(provider) || selectionMutationsRef.current.has(`oauth:${provider}`)) return; const target = { provider, accountId: account.id }; - pausingAccountRef.current = target; - setPausingAccount({ ...target, paused }); + pausingAccountRef.current.set(provider, target); + setPausingAccounts(current => ({ ...current, [provider]: { ...target, paused } })); const key = invalidateSelectionReads(provider, "oauth"); const mutation = Symbol(); selectionMutationsRef.current.set(key, mutation); @@ -522,9 +529,9 @@ export function useProviderAccountPools(deps: { selectionMutationsRef.current.delete(key); void refreshAccountRosters({ provider, kind: "oauth" }); } - if (pausingAccountRef.current?.provider === target.provider && pausingAccountRef.current.accountId === target.accountId) { - pausingAccountRef.current = null; - if (aliveRef.current) setPausingAccount(null); + if (pausingAccountRef.current.get(provider) === target) { + pausingAccountRef.current.delete(provider); + if (aliveRef.current) setPausingAccounts(current => { const next = { ...current }; delete next[provider]; return next; }); } } // The server persisted the change. A failed follow-up read is not a failed pause: keep @@ -694,7 +701,7 @@ export function useProviderAccountPools(deps: { ); return { - accountSets, accountLoadStates, switchingAccount, pausingAccount, openAccounts, keyPools, addingKeyFor, newKeyValue, + accountSets, accountLoadStates, switchingAccount, pausingAccount, switchingAccounts, pausingAccounts, openAccounts, keyPools, addingKeyFor, newKeyValue, setAccountSets, setAccountLoadStates, setSwitchingAccount, setOpenAccounts, setKeyPools, setAddingKeyFor, setNewKeyValue, fetchAccountSets, fetchKeyPools, refreshAccountRosters, switchAccount, pauseAccount, setAccountPoolThreshold, setAccountThreshold, switchApiKey, removeApiKey, addApiKeyValue, addApiKey, editCredentialAlias, removeAccount, oauthCardProviders, keyCardProviders, activeAccountNeedsReauth, diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index e3be8ba278d..225e03b12b1 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -5,6 +5,12 @@ import type { TKey } from "./en"; * German i18n catalog, generated from en.ts. Must match the `TKey` set (compile-checked). */ export const de: Record = { + "sidecar.pool": "Kontopool", + "sidecar.poolCurrent": "Pool der aktuellen Anfrage", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Pool 2", + "sidecar.poolMixed": "Diese explizite Auswahl kann einen anderen Pool als die Hauptanfrage verwenden.", + "provider.name.anthropic2": "Anthropic · Pool 2", "nav.claude": "Claude", "claude.pageSub": "OpenCodex-Routing und Einstellungen für Claude Code.", @@ -1500,6 +1506,7 @@ export const de: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1723,6 +1730,7 @@ export const de: Record = { "integrations.semantics.dsh": "OpenCodex verwaltet nur den Anbieter opencodex in der llm-pi-ai-Zeile von $DSH_HOME/profiles/desktop/cordis.patch.yml, dem Desktop-Profil, das DSH ab 0.1.7 liest ($DSH_HOME/settings.yaml, solange dieses Profil fehlt). 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.commandcode": "Verwaltet nur den OpenCodex-Provider-Eintrag in ~/.commandcode/providers.json. Ihre anderen Provider und Einstellungen bleiben unverändert.", "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 ~/.aside/u//models.json dieses Profils. Andere Provider bleiben unverändert. Beende Aside nach dem Anwenden vollständig und öffne es erneut.", "integrations.semantics.raycast": "Fügt einen OpenCodex-Provider-Eintrag in die providers.yaml von Raycast ein, damit jedes geroutete Modell in der Modellauswahl von Raycast AI erscheint. Raycast Pro erforderlich.", @@ -2283,6 +2291,8 @@ export const de: Record = { "api.key.revealHint": "Klicken, um den vollständigen Schlüssel anzuzeigen", "api.key.hideHint": "Klicken, um den vollständigen Schlüssel auszublenden", "api.key.revealFailed": "Der vollständige Schlüssel konnte nicht geladen werden.", + "api.key.revealDenied": "Das Anzeigen eines gespeicherten Schlüssels erfordert eine vom Betreiber autorisierte Sitzung. Koppeln Sie diesen Browser, um fortzufahren, oder melden Sie sich über eine vertrauenswürdige Tailscale-Identität an.", + "api.key.mutationHidden": "Der Schlüssel wurde erstellt oder seine Rotation gestartet, aber der einmalige Wert wurde ausgeblendet, weil sich die Sitzung oder Ansicht geändert hat.", "api.key.copyFailedShort": "Kopieren fehlgeschlagen. Markiere den Schlüssel und kopiere ihn manuell.", "api.key.deleteRowAria": "{name} löschen", "api.key.deleteShort": "Löschen", @@ -2309,6 +2319,7 @@ export const de: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 1dce105005e..07e3c3b1e06 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -6,6 +6,12 @@ * `{var}` are plain interpolations. */ export const en = { + "sidecar.pool": "Pool", + "sidecar.poolCurrent": "Current request's pool", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Pool 2", + "sidecar.poolMixed": "This explicit selection may use a different pool from the main request.", + "provider.name.anthropic2": "Anthropic · Pool 2", "nav.claude": "Claude", "claude.pageSub": "OpenCodex routing and settings for Claude Code.", @@ -2066,6 +2072,7 @@ export const en = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2334,6 +2341,7 @@ export const en = { "integrations.semantics.dsh": "OpenCodex manages only the opencodex provider of the llm-pi-ai row in $DSH_HOME/profiles/desktop/cordis.patch.yml, the Desktop profile DSH 0.1.7+ reads ($DSH_HOME/settings.yaml until that profile exists). 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.commandcode": "Manages only the OpenCodex provider entry in ~/.commandcode/providers.json. Your other providers and settings stay unchanged.", "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 this profile’s ~/.aside/u//models.json. Your other providers stay unchanged. Fully quit and reopen Aside after applying.", "integrations.semantics.raycast": "Adds an OpenCodex provider entry to Raycast's providers.yaml so every routed model appears in the Raycast AI model picker. Raycast Pro required.", @@ -2879,6 +2887,8 @@ export const en = { "api.key.revealHint": "Click to show the full key", "api.key.hideHint": "Click to hide the full key", "api.key.revealFailed": "Could not load the full key.", + "api.key.revealDenied": "Showing a stored key requires an operator-authorized session. Pair this browser to continue, or sign in through a trusted Tailscale identity.", + "api.key.mutationHidden": "The key was created or its rotation started, but the one-time value was hidden because the session or view changed.", "api.key.copyFailedShort": "Could not copy. Select the key and copy it manually.", "api.key.deleteRowAria": "Delete {name}", "api.key.deleteShort": "Delete", @@ -2931,6 +2941,7 @@ export const en = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 1295c428bb3..1e92d08a249 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -4,6 +4,12 @@ import type { TKey } from "./en"; * French i18n catalog. Must match the `TKey` set. */ export const fr: Record = { + "sidecar.pool": "Pool de comptes", + "sidecar.poolCurrent": "Pool de la requête actuelle", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Pool 2", + "sidecar.poolMixed": "Ce choix explicite peut utiliser un autre pool que la requête principale.", + "provider.name.anthropic2": "Anthropic · Pool 2", "nav.claude": "Claude", "claude.pageSub": "Routage et paramètres OpenCodex pour Claude Code.", @@ -2038,6 +2044,7 @@ export const fr: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2261,6 +2268,7 @@ export const fr: Record = { "integrations.semantics.dsh": "OpenCodex gère uniquement le fournisseur opencodex de la ligne llm-pi-ai dans $DSH_HOME/profiles/desktop/cordis.patch.yml, le profil Desktop que lit DSH 0.1.7+ ($DSH_HOME/settings.yaml tant que ce profil n’existe pas). 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.commandcode": "Gère uniquement l’entrée du fournisseur OpenCodex dans ~/.commandcode/providers.json. Vos autres fournisseurs et paramètres restent inchangés.", "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 fichier ~/.aside/u//models.json de ce profil. Vos autres fournisseurs restent inchangés. Quittez complètement Aside et relancez-le après application.", "integrations.semantics.raycast": "Ajoute une entrée de fournisseur OpenCodex dans le providers.yaml de Raycast afin que chaque modèle routé apparaisse dans le sélecteur de modèles de Raycast AI. Raycast Pro requis.", @@ -2795,6 +2803,8 @@ export const fr: Record = { "api.key.revealHint": "Cliquez pour afficher la clé complète", "api.key.hideHint": "Cliquez pour masquer la clé complète", "api.key.revealFailed": "Impossible de charger la clé complète.", + "api.key.revealDenied": "Afficher une clé enregistrée nécessite une session autorisée par l'opérateur. Associez ce navigateur pour continuer, ou connectez-vous via une identité Tailscale de confiance.", + "api.key.mutationHidden": "La clé a été créée ou sa rotation a démarré, mais la valeur à usage unique a été masquée car la session ou la vue a changé.", "api.key.copyFailedShort": "Copie impossible. Sélectionnez la clé et copiez-la manuellement.", "api.key.deleteRowAria": "Supprimer {name}", "api.key.deleteShort": "Supprimer", @@ -2847,6 +2857,7 @@ export const fr: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index f134f328e33..e8122729569 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -4,6 +4,12 @@ import type { TKey } from "./en"; * Japanese i18n catalog; must match the `TKey` set (compile-checked). */ export const ja: Record = { + "sidecar.pool": "アカウントプール", + "sidecar.poolCurrent": "現在のリクエストのプール", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · プール2", + "sidecar.poolMixed": "この明示的な選択では、メインのリクエストと異なるプールを使用する場合があります。", + "provider.name.anthropic2": "Anthropic · プール2", "nav.claude": "Claude", "claude.pageSub": "Claude Code の OpenCodex ルーティングと設定を管理します。", @@ -1933,6 +1939,7 @@ export const ja: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2156,6 +2163,7 @@ export const ja: Record = { "integrations.semantics.dsh": "OpenCodex が管理するのは $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 以降が読む Desktop プロファイル。存在しない間は $DSH_HOME/settings.yaml)の llm-pi-ai 行にある 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.commandcode": "~/.commandcode/providers.json の OpenCodex プロバイダー項目のみを管理します。他のプロバイダーや設定は変更しません。", "integrations.semantics.prime": "Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。", "integrations.semantics.aside": "このプロファイルの ~/.aside/u//models.json 内の providers.opencodex のみを管理します。他のプロバイダーは変更しません。適用後は Aside を完全に終了してから開き直してください。", "integrations.semantics.raycast": "Raycast の providers.yaml に OpenCodex のプロバイダーエントリを追加し、ルーティングされたすべてのモデルを Raycast AI のモデル選択に表示します。Raycast Pro が必要です。", @@ -2722,6 +2730,8 @@ export const ja: Record = { "api.key.revealHint": "クリックでキー全体を表示", "api.key.hideHint": "クリックでキー全体を隠す", "api.key.revealFailed": "キー全体を読み込めませんでした。", + "api.key.revealDenied": "保存済みキーの表示には、オペレーターが認可したセッションが必要です。続行するにはこのブラウザーをペアリングするか、信頼された Tailscale ID でサインインしてください。", + "api.key.mutationHidden": "キーの作成またはローテーションの開始は完了しましたが、セッションまたは画面が変わったため、一度だけ表示される値を非表示にしました。", "api.key.copyFailedShort": "コピーできませんでした。キーを選択して手動でコピーしてください。", "api.key.deleteRowAria": "{name} を削除", "api.key.deleteShort": "削除", @@ -2748,6 +2758,7 @@ export const ja: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index fc2ecd9da3a..031b5a465fd 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -4,6 +4,12 @@ import type { TKey } from "./en"; * Korean i18n catalog; must match the `TKey` set (compile-checked). */ export const ko: Record = { + "sidecar.pool": "계정 풀", + "sidecar.poolCurrent": "현재 요청의 풀 사용", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · 풀 2", + "sidecar.poolMixed": "이 선택은 메인 요청과 다른 풀을 사용할 수 있습니다.", + "provider.name.anthropic2": "Anthropic · 풀 2", "nav.claude": "Claude", "claude.pageSub": "Claude Code의 OpenCodex 라우팅과 설정을 관리합니다.", @@ -1544,6 +1550,7 @@ export const ko: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1767,6 +1774,7 @@ export const ko: Record = { "integrations.semantics.dsh": "OpenCodex는 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7+가 읽는 Desktop profile, 이 profile이 없으면 $DSH_HOME/settings.yaml)의 llm-pi-ai 행에 있는 opencodex provider만 관리합니다. 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.commandcode": "~/.commandcode/providers.json의 OpenCodex 프로바이더 항목만 관리합니다. 다른 프로바이더와 설정은 변경되지 않습니다.", "integrations.semantics.prime": "Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.", "integrations.semantics.aside": "이 프로필의 ~/.aside/u//models.json에서 providers.opencodex만 관리합니다. 다른 프로바이더는 그대로 유지됩니다. 적용 후 Aside를 완전히 종료하고 다시 여세요.", "integrations.semantics.raycast": "Raycast의 providers.yaml에 OpenCodex 프로바이더 항목을 추가해 라우팅된 모든 모델이 Raycast AI 모델 선택기에 표시되도록 합니다. Raycast Pro가 필요합니다.", @@ -2330,6 +2338,8 @@ export const ko: Record = { "api.key.revealHint": "클릭하면 전체 키가 보입니다", "api.key.hideHint": "클릭하면 전체 키를 숨깁니다", "api.key.revealFailed": "전체 키를 불러오지 못했습니다.", + "api.key.revealDenied": "저장된 키를 표시하려면 운영자가 승인한 세션이 필요합니다. 이 브라우저를 페어링해 계속하거나, 신뢰된 Tailscale 신원으로 로그인하세요.", + "api.key.mutationHidden": "키가 생성되었거나 교체가 시작되었지만, 세션이나 화면이 바뀌어 일회성 값을 숨겼습니다.", "api.key.copyFailedShort": "복사하지 못했습니다. 키를 선택해 직접 복사하세요.", "api.key.deleteRowAria": "{name} 삭제", "api.key.deleteShort": "삭제", @@ -2356,6 +2366,7 @@ export const ko: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts index ed9e0e4d5b2..afe11765d7a 100644 --- a/gui/src/i18n/pt.ts +++ b/gui/src/i18n/pt.ts @@ -6,6 +6,12 @@ import type { TKey } from "./en"; * Technical terms and model identifiers intentionally remain English. */ export const pt: Record = { + "sidecar.pool": "Pool de contas", + "sidecar.poolCurrent": "Pool da solicitação atual", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Pool 2", + "sidecar.poolMixed": "Esta seleção explícita pode usar um pool diferente da solicitação principal.", + "provider.name.anthropic2": "Anthropic · Pool 2", "compactionRouting.sources": "Origens", "compactionRouting.sourcesAll": "Todos os modelos de conversa", "compactionRouting.sourcesSelected": "Somente origens selecionadas", @@ -2055,6 +2061,7 @@ export const pt: Record = { "integrations.tab.raycast": "Raycast", "integrations.tab.omo": "omo", "integrations.tab.cline": "Cline CLI", + "integrations.tab.commandcode": "Command Code", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", "integrations.aside.profilesTitle": "Perfis do Aside", @@ -2357,6 +2364,7 @@ export const pt: Record = { "integrations.lazycodexRoles.auto.moveUp": "Suba de nível se: {text}", "integrations.lazycodexRoles.auto.moveDown": "Desça de nível se: {text}", "integrations.semantics.cline": "Gerencia o OpenCodex no providers.json e no models.json do Cline CLI. Pare o Cline antes de alterar ou sincronizar esses arquivos e reinicie depois. Desfazer restaura os dois originais. Seu provedor padrão permanece inalterado; selecione o OpenCodex no Cline.", + "integrations.semantics.commandcode": "Gerencia apenas a entrada do provedor OpenCodex em ~/.commandcode/providers.json. Seus outros provedores e configurações permanecem inalterados.", "integrations.semantics.kilo": "Gerencia apenas provider.opencodex na configuração global do Kilo — o primeiro arquivo existente entre kilo.jsonc, kilo.json, opencode.jsonc, opencode.json ou config.json em ~/.config/kilo (XDG_CONFIG_HOME realoca esse diretório; kilo.jsonc é criado quando nenhum existe). As demais chaves permanecem inalteradas. Aplicar reescreve o arquivo inteiro, então comentários e vírgulas finais não são preservados. Selecione opencodex/ no Kilo.", "integrations.semantics.droid": "Adiciona modelos personalizados do OpenCodex ao settings.json do Factory Droid. Desativar remove apenas as linhas gerenciadas; desfazer restaura o arquivo salvo.", "integrations.raycast.proRequired": "Custom Providers é um recurso do Raycast Pro. O arquivo será gravado, mas o Raycast o ignora até que uma assinatura Pro esteja ativa.", @@ -2838,6 +2846,8 @@ export const pt: Record = { "api.key.revealHint": "Clique para mostrar a chave completa", "api.key.hideHint": "Clique para ocultar a chave completa", "api.key.revealFailed": "Não foi possível carregar a chave completa.", + "api.key.revealDenied": "Mostrar uma chave armazenada exige uma sessão autorizada pelo operador. Emparelhe este navegador para continuar ou entre com uma identidade Tailscale confiável.", + "api.key.mutationHidden": "A chave foi criada ou sua rotação foi iniciada, mas o valor de exibição única foi ocultado porque a sessão ou a tela mudou.", "api.key.copyFailedShort": "Não foi possível copiar. Selecione a chave e copie-a manualmente.", "api.key.deleteRowAria": "Excluir {name}", "api.key.deleteShort": "Excluir", @@ -2895,6 +2905,7 @@ export const pt: Record = { "api.clientConfig.clientRaycast": "Raycast", "api.clientConfig.clientOmo": "omo", "api.clientConfig.clientCline": "Cline CLI", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientKilo": "Kilo", "api.clientConfig.clientDroid": "Factory Droid", "api.clientConfig.clineBundle": "Este é um pacote de dois arquivos, não um arquivo de configurações do Cline. Mescle settings em providers.json e catalog no models.json vizinho. Use o interruptor de integração para uma gravação com backup.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 301e460bf8b..20abe9ccb4c 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -4,6 +4,12 @@ import type { TKey } from "./en"; * Russian i18n catalog; must match the `TKey` set (compile-checked). */ export const ru: Record = { + "sidecar.pool": "Пул аккаунтов", + "sidecar.poolCurrent": "Пул текущего запроса", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Пул 2", + "sidecar.poolMixed": "При явном выборе может использоваться другой пул, чем у основного запроса.", + "provider.name.anthropic2": "Anthropic · Пул 2", "nav.claude": "Claude", "claude.pageSub": "Маршрутизация и настройки OpenCodex для Claude Code.", @@ -2024,6 +2030,7 @@ export const ru: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2247,6 +2254,7 @@ export const ru: Record = { "integrations.semantics.dsh": "OpenCodex управляет только провайдером opencodex в строке llm-pi-ai файла $DSH_HOME/profiles/desktop/cordis.patch.yml — профиля Desktop, который читает DSH 0.1.7+ ($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.commandcode": "Управляет только записью провайдера OpenCodex в ~/.commandcode/providers.json. Другие провайдеры и настройки остаются без изменений.", "integrations.semantics.prime": "Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.", "integrations.semantics.aside": "Управляет только providers.opencodex в файле ~/.aside/u//models.json этого профиля. Другие провайдеры остаются без изменений. После применения полностью закройте и снова откройте Aside.", "integrations.semantics.raycast": "Добавляет запись провайдера OpenCodex в providers.yaml Raycast, чтобы каждая маршрутизируемая модель появилась в выборе моделей Raycast AI. Требуется Raycast Pro.", @@ -2812,6 +2820,8 @@ export const ru: Record = { "api.key.revealHint": "Нажмите, чтобы показать ключ полностью", "api.key.hideHint": "Нажмите, чтобы скрыть полный ключ", "api.key.revealFailed": "Не удалось загрузить полный ключ.", + "api.key.revealDenied": "Чтобы показать сохранённый ключ, требуется сеанс, авторизованный оператором. Создайте пару для этого браузера, чтобы продолжить, или войдите с доверенной личностью Tailscale.", + "api.key.mutationHidden": "Ключ создан или его ротация начата, но одноразовое значение скрыто, поскольку сеанс или экран изменился.", "api.key.copyFailedShort": "Не удалось скопировать. Выделите ключ и скопируйте его вручную.", "api.key.deleteRowAria": "Удалить {name}", "api.key.deleteShort": "Удалить", @@ -2838,6 +2848,7 @@ export const ru: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 3a73605cf8b..0f7a9f6de52 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -5,6 +5,12 @@ import type { TKey } from "./en"; * Turkish i18n catalog. Must match the `TKey` set (compile-checked). */ export const tr: Record = { + "sidecar.pool": "Hesap havuzu", + "sidecar.poolCurrent": "Geçerli isteğin havuzu", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Havuz 2", + "sidecar.poolMixed": "Bu açık seçim, ana istekten farklı bir havuz kullanabilir.", + "provider.name.anthropic2": "Anthropic · Havuz 2", "nav.claude": "Claude", "claude.pageSub": "Claude Code için OpenCodex yönlendirmesi ve ayarları.", @@ -2051,6 +2057,7 @@ export const tr: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2273,6 +2280,7 @@ export const tr: Record = { "integrations.semantics.dsh": "OpenCodex yalnızca $DSH_HOME/profiles/desktop/cordis.patch.yml içindeki llm-pi-ai satırının opencodex sağlayıcısını yönetir; bu, DSH 0.1.7+ sürümünün okuduğu Desktop profilidir (profil yokken $DSH_HOME/settings.yaml). 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.commandcode": "Yalnızca ~/.commandcode/providers.json içindeki OpenCodex sağlayıcı girdisini yönetir. Diğer sağlayıcılarınız ve ayarlarınız değişmez.", "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 bu profilin ~/.aside/u//models.json dosyasındaki providers.opencodex bölümünü yönetir. Diğer sağlayıcılarınız değişmez. Uyguladıktan sonra Aside’ı tamamen kapatıp yeniden açın.", "integrations.semantics.raycast": "Raycast'in providers.yaml dosyasına bir OpenCodex sağlayıcı girdisi ekler; böylece yönlendirilen her model Raycast AI model seçicisinde görünür. Raycast Pro gerekir.", @@ -2814,6 +2822,8 @@ export const tr: Record = { "api.key.revealHint": "Tam anahtarı göstermek için tıklayın", "api.key.hideHint": "Tam anahtarı gizlemek için tıklayın", "api.key.revealFailed": "Tam anahtar yüklenemedi.", + "api.key.revealDenied": "Kaydedilmiş bir anahtarı göstermek, operatörün yetkilendirdiği bir oturum gerektirir. Devam etmek için bu tarayıcıyı eşleştirin veya güvenilir bir Tailscale kimliğiyle oturum açın.", + "api.key.mutationHidden": "Anahtar oluşturuldu veya anahtar rotasyonu başlatıldı, ancak oturum ya da görünüm değiştiği için tek seferlik değer gizlendi.", "api.key.copyFailedShort": "Kopyalanamadı. Anahtarı seçip elle kopyalayın.", "api.key.deleteRowAria": "{name} sil", "api.key.deleteShort": "Sil", @@ -2865,6 +2875,7 @@ export const tr: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 77677c884fb..dded499109d 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -6,6 +6,12 @@ import type { TKey } from "./en"; * Technical terms and model identifiers intentionally remain English. */ export const vi: Record = { + "sidecar.pool": "Nhóm tài khoản", + "sidecar.poolCurrent": "Nhóm của yêu cầu hiện tại", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · Nhóm 2", + "sidecar.poolMixed": "Lựa chọn rõ ràng này có thể dùng nhóm khác với yêu cầu chính.", + "provider.name.anthropic2": "Anthropic · Nhóm 2", "nav.claude": "Claude", "claude.pageSub": "Định tuyến và cài đặt OpenCodex cho Claude Code.", @@ -2021,6 +2027,7 @@ export const vi: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -2289,6 +2296,7 @@ export const vi: Record = { "integrations.semantics.dsh": "OpenCodex chỉ quản lý provider opencodex trong dòng llm-pi-ai của $DSH_HOME/profiles/desktop/cordis.patch.yml, profile Desktop mà DSH 0.1.7+ đọc ($DSH_HOME/settings.yaml khi profile đó chưa tồn tại). DSH sẽ hot reload provider này; model mặc định của bạn và deepseek-official không thay đổi. Hiện chỉ hỗ trợ loopback; không ghi credential thật nào.", "integrations.semantics.mcode": "Chỉ quản lý custom_provider.opencodex. Model mặc định của bạn và đăng nhập MiniMax không thay đổi.", "integrations.semantics.zcode": "Chỉ quản lý provider.opencodex trong ~/.zcode/v2/config.json. Đăng nhập Z.ai của bạn và các provider khác không thay đổi. Khởi động lại ZCode sau khi thay đổi.", + "integrations.semantics.commandcode": "Chỉ quản lý mục nhà cung cấp OpenCodex trong ~/.commandcode/providers.json. Các provider và cài đặt khác của bạn vẫn không thay đổi.", "integrations.semantics.prime": "Chỉ quản lý providers.opencodex trong models.json của Prime Agent — ~/.prime/agent trừ khi PRIME_AGENT_CODING_AGENT_DIR điều hướng nó. Các provider khác và ngoại lệ model của bạn không thay đổi. Áp dụng cho các session mới.", "integrations.semantics.aside": "Chỉ quản lý providers.opencodex trong ~/.aside/u//models.json của profile này. Các provider khác của bạn không thay đổi. Hãy thoát hoàn toàn và mở lại Aside sau khi áp dụng.", "integrations.semantics.raycast": "Thêm một provider OpenCodex vào providers.yaml của Raycast để mọi model được định tuyến xuất hiện trong bộ chọn model AI của Raycast. Yêu cầu Raycast Pro.", @@ -2813,6 +2821,8 @@ export const vi: Record = { "api.key.revealHint": "Nhấp để hiện toàn bộ khóa", "api.key.hideHint": "Nhấp để ẩn toàn bộ khóa", "api.key.revealFailed": "Không tải được toàn bộ khóa.", + "api.key.revealDenied": "Hiển thị khóa đã lưu yêu cầu phiên được người vận hành cấp quyền. Ghép nối trình duyệt này để tiếp tục, hoặc đăng nhập qua danh tính Tailscale tin cậy.", + "api.key.mutationHidden": "Khóa đã được tạo hoặc quá trình xoay khóa đã bắt đầu, nhưng giá trị chỉ hiển thị một lần đã bị ẩn vì phiên hoặc màn hình đã thay đổi.", "api.key.copyFailedShort": "Không sao chép được. Hãy chọn key và sao chép thủ công.", "api.key.deleteRowAria": "Xóa {name}", "api.key.deleteShort": "Xóa", @@ -2865,6 +2875,7 @@ export const vi: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index dbbd6bb1cd3..02fdccf33a9 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2,6 +2,12 @@ import type { TKey } from "./en"; /** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */ export const zhTW: Record = { + "sidecar.pool": "帳戶池", + "sidecar.poolCurrent": "使用目前請求的帳戶池", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · 帳戶池 2", + "sidecar.poolMixed": "此明確選擇可能使用與主要請求不同的帳戶池。", + "provider.name.anthropic2": "Anthropic · 帳戶池 2", "nav.claude": "Claude", "claude.pageSub": "管理 Claude Code 的 OpenCodex 路由與設定。", @@ -2068,6 +2074,8 @@ export const zhTW: Record = { "api.key.revealHint": "點擊顯示完整金鑰", "api.key.hideHint": "點擊隱藏完整金鑰", "api.key.revealFailed": "無法載入完整金鑰。", + "api.key.revealDenied": "顯示已儲存的金鑰需要經操作員授權的工作階段。請配對此瀏覽器以繼續,或透過受信任的 Tailscale 身分登入。", + "api.key.mutationHidden": "金鑰已建立或已開始輪替,但因工作階段或畫面已變更,一次性值已隱藏。", "api.key.copyFailedShort": "無法複製。請選取金鑰後手動複製。", "api.key.deleteRowAria": "刪除 {name}", "api.key.deleteShort": "刪除", @@ -2944,6 +2952,7 @@ export const zhTW: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -3167,6 +3176,7 @@ export const zhTW: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 起讀取的 Desktop profile;該 profile 不存在時為 $DSH_HOME/settings.yaml)中 llm-pi-ai 這一列的 opencodex provider。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.commandcode": "僅管理 ~/.commandcode/providers.json 中的 OpenCodex 供應商項目。其他供應商和設定保持不變。", "integrations.semantics.prime": "僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。", "integrations.semantics.aside": "僅管理此設定檔的 ~/.aside/u//models.json 中的 providers.opencodex。其他供應商維持不變。套用後請完全結束並重新開啟 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中新增一個 OpenCodex 供應商項目,讓所有已路由的模型出現在 Raycast AI 模型選擇器中。需要 Raycast Pro。", @@ -3266,6 +3276,7 @@ export const zhTW: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 50a7c291ad1..c8d2ecd0276 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -4,6 +4,12 @@ import type { TKey } from "./en"; * Chinese i18n catalog; must match the `TKey` set (compile-checked). */ export const zh: Record = { + "sidecar.pool": "账户池", + "sidecar.poolCurrent": "使用当前请求的账户池", + "sidecar.poolA": "Anthropic", + "sidecar.poolB": "Anthropic · 账户池 2", + "sidecar.poolMixed": "此显式选择可能使用与主请求不同的账户池。", + "provider.name.anthropic2": "Anthropic · 账户池 2", "nav.claude": "Claude", "claude.pageSub": "管理 Claude Code 的 OpenCodex 路由和设置。", @@ -1517,6 +1523,7 @@ export const zh: Record = { "integrations.tab.dsh": "DSH", "integrations.tab.mcode": "MiniMax Code", "integrations.tab.zcode": "ZCode", + "integrations.tab.commandcode": "Command Code", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", "integrations.tab.raycast": "Raycast", @@ -1740,6 +1747,7 @@ export const zh: Record = { "integrations.semantics.dsh": "OpenCodex 只管理 $DSH_HOME/profiles/desktop/cordis.patch.yml(DSH 0.1.7 起读取的 Desktop profile;该 profile 不存在时为 $DSH_HOME/settings.yaml)中 llm-pi-ai 这一行的 opencodex provider。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.commandcode": "仅管理 ~/.commandcode/providers.json 中的 OpenCodex 提供商条目。其他提供商和设置保持不变。", "integrations.semantics.prime": "仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。", "integrations.semantics.aside": "仅管理此配置文件的 ~/.aside/u//models.json 中的 providers.opencodex。其他提供商保持不变。应用后请完全退出并重新打开 Aside。", "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中添加一个 OpenCodex 提供商条目,让所有已路由的模型出现在 Raycast AI 模型选择器中。需要 Raycast Pro。", @@ -2303,6 +2311,8 @@ export const zh: Record = { "api.key.revealHint": "点击显示完整密钥", "api.key.hideHint": "点击隐藏完整密钥", "api.key.revealFailed": "无法加载完整密钥。", + "api.key.revealDenied": "显示已存储的密钥需要经操作员授权的会话。请配对此浏览器以继续,或通过受信任的 Tailscale 身份登录。", + "api.key.mutationHidden": "密钥已创建或已开始轮换,但由于会话或视图发生变化,一次性值已被隐藏。", "api.key.copyFailedShort": "无法复制。请选中密钥后手动复制。", "api.key.deleteRowAria": "删除 {name}", "api.key.deleteShort": "删除", @@ -2329,6 +2339,7 @@ export const zh: Record = { "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", "api.clientConfig.clientMcode": "MiniMax Code", "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientCommandCode": "Command Code", "api.clientConfig.clientPrime": "Prime Agent", "api.clientConfig.clientAside": "Aside", "api.clientConfig.clientRaycast": "Raycast", diff --git a/gui/src/oauth-tos-risk.ts b/gui/src/oauth-tos-risk.ts index e5143bd774c..4b92e407ea6 100644 --- a/gui/src/oauth-tos-risk.ts +++ b/gui/src/oauth-tos-risk.ts @@ -7,7 +7,7 @@ */ export type OAuthTosRiskLevel = "high" | "elevated"; -const HIGH_RISK = new Set(["anthropic", "google-antigravity", "meta-muse"]); +const HIGH_RISK = new Set(["anthropic", "anthropic2", "google-antigravity", "meta-muse"]); const ELEVATED_RISK = new Set(["github-copilot", "cursor", "zed"]); export function oauthTosRisk(providerId: string): OAuthTosRiskLevel | null { @@ -62,7 +62,7 @@ export type OAuthTosCopyKeys = { export function oauthTosCopyKeys(providerId: string, level: OAuthTosRiskLevel): OAuthTosCopyKeys { const id = providerId.trim().toLowerCase(); - if (id === "anthropic") { + if (id === "anthropic" || id === "anthropic2") { return { title: "oauthTos.anthropicTitle", body: "oauthTos.anthropicBody", diff --git a/gui/src/pages/ApiKeys.tsx b/gui/src/pages/ApiKeys.tsx index affe4d2684f..d7496433ebd 100644 --- a/gui/src/pages/ApiKeys.tsx +++ b/gui/src/pages/ApiKeys.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Notice } from "../ui"; import { readUsageMetadata, type UsageReadMetadata } from "../usage-summary-resource"; import { useI18n, LOCALES } from "../i18n/shared"; @@ -13,8 +13,10 @@ import { } from "../api-access-models"; import { readSessionListCacheEntry, writeSessionListCacheEntry } from "../session-list-cache"; import { createBoundedFetch } from "../bounded-fetch"; +import { invalidateClientResource } from "../client-resource"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; +import { useKeyDisclosure } from "../use-key-disclosure"; import ApiKeysWorkspace from "../components/apikeys-workspace/ApiKeysWorkspace"; import { DEFAULT_ENDPOINTS, @@ -29,6 +31,7 @@ import { type ApiKeyEntry, type ModelTestResult, type ModelTests, + type RevealKeyResult, } from "./api-keys-utils"; interface KeysResponse extends UsageReadMetadata { @@ -140,6 +143,9 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a const cachedModelsEntry = readSessionListCacheEntry(modelsCacheKey); const cachedKeys = validCachedKeys(cachedKeysEntry?.data ?? null); const cachedModels = cachedModelsEntry?.data ?? null; + const currentApiBase = useRef(apiBase); + useLayoutEffect(() => { currentApiBase.current = apiBase; }, [apiBase]); + const [hiddenMutationBase, setHiddenMutationBase] = useState(null); const [actionError, setActionError] = useState(null); const [modelQuery, setModelQuery] = useState(""); const [copiedModelId, setCopiedModelId] = useState(null); @@ -151,6 +157,13 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a const [rotationSecret, setRotationSecret] = useState<{ id: string; key: string; rotationId: string } | null>(null); const [rotationCopied, setRotationCopied] = useState(false); const creatingRef = useRef(false); + const clearDisclosure = useCallback(() => { + setNewKey(null); + setCopied(false); + setRotationSecret(null); + setRotationCopied(false); + }, []); + const disclosure = useKeyDisclosure(apiBase, active, clearDisclosure); const fetchKeys = useCallback(async (signal: AbortSignal): Promise => { const res = await fetch(`${apiBase}/api/keys`, { signal }); @@ -185,7 +198,7 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a authMatrix: data.authMatrix, }; // Prefixes only — never the secret key material. - writeSessionListCacheEntry(keysCacheKey, next); + if (!signal.aborted) writeSessionListCacheEntry(keysCacheKey, next); return next; }, [apiBase, keysCacheKey, t]); @@ -266,10 +279,12 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a }, [modelQuery, models]); const handleCreate = async (name?: string): Promise => { + const generation = disclosure.generation.current; if (creatingRef.current) return false; creatingRef.current = true; setCreating(true); setActionError(null); + setHiddenMutationBase(null); try { const effectiveName = name ?? newName; const res = await fetch(`${apiBase}/api/keys`, { @@ -277,14 +292,21 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: effectiveName || "default" }), }); + if (res.ok) { + if (currentApiBase.current === apiBase) refreshKeys(); + else invalidateClientResource(keysResourceKey); + } const data = await readJsonOrThrow(res, t("api.createFailed")); + if (!disclosure.current(generation)) { + setHiddenMutationBase(apiBase); + return true; + } if (typeof data?.key !== "string" || data.key.length === 0) { setActionError(t("api.createFailed")); return false; } setNewKey(data.key); setNewName(""); - refreshKeys(); return true; } catch { setActionError(t("api.createFailed")); @@ -322,8 +344,10 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a }; /** The full key for one row. Read-only, but bounded like the mutations so a - * stalled connection releases the cell's pending state. */ - const handleReveal = async (id: string): Promise => { + * stalled connection releases the cell's pending state. The 401/403 standing + * refusal is reported apart from transient failures so the list can offer + * the remedy — pairing — instead of a bare "try again". */ + const handleReveal = async (id: string): Promise => { const bounded = createBoundedFetch(MUTATION_TIMEOUT_MS); try { const res = await fetch(`${apiBase}/api/keys/reveal`, { @@ -333,11 +357,14 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a signal: bounded.signal, cache: "no-store", }); - if (!res.ok) return null; + if (res.status === 401 || res.status === 403) return { ok: false, kind: "denied" }; + if (!res.ok) return { ok: false, kind: "failed" }; const body = await res.json() as { key?: unknown }; - return typeof body.key === "string" && body.key ? body.key : null; + return typeof body.key === "string" && body.key + ? { ok: true, key: body.key } + : { ok: false, kind: "failed" }; } catch { - return null; + return { ok: false, kind: "failed" }; } finally { bounded.clear(); } @@ -369,7 +396,9 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a }; const handleRotationStart = async (id: string): Promise => { + const generation = disclosure.generation.current; setActionError(null); + setHiddenMutationBase(null); const bounded = createBoundedFetch(MUTATION_TIMEOUT_MS); try { const res = await fetch(`${apiBase}/api/keys/rotate`, { @@ -378,10 +407,17 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a body: JSON.stringify({ id }), signal: bounded.signal, }); + if (res.ok) { + if (currentApiBase.current === apiBase) refreshKeys(); + else invalidateClientResource(keysResourceKey); + } const data = await readJsonOrThrow(res, t("api.rotation.startFailed")); + if (!disclosure.current(generation)) { + setHiddenMutationBase(apiBase); + return true; + } if (!data || typeof data.key !== "string" || !data.key || typeof data.rotationId !== "string" || !data.rotationId) return false; setRotationSecret({ id, key: data.key, rotationId: data.rotationId }); - refreshKeys(); return true; } catch { return false; @@ -413,23 +449,28 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a const copyRotationSecret = async () => { if (!rotationSecret) return; + const generation = disclosure.generation.current; try { await navigator.clipboard.writeText(rotationSecret.key); + if (!disclosure.current(generation)) return; setRotationCopied(true); - window.setTimeout(() => setRotationCopied(false), 2000); + window.setTimeout(() => { if (disclosure.current(generation)) setRotationCopied(false); }, 2000); } catch { - setActionError(t("api.key.copyFailed")); + if (disclosure.current(generation)) setActionError(t("api.key.copyFailed")); } }; const copyKey = async () => { if (!newKey) return; + const generation = disclosure.generation.current; setActionError(null); try { await navigator.clipboard.writeText(newKey); + if (!disclosure.current(generation)) return; setCopied(true); - window.setTimeout(() => setCopied(false), 2000); + window.setTimeout(() => { if (disclosure.current(generation)) setCopied(false); }, 2000); } catch { + if (!disclosure.current(generation)) return; // The one-time key is the only string in the product with no second // chance, so a silent failure here is the worst kind. Keep it on screen. setCopied(false); @@ -526,6 +567,7 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a {subtitleParts[1]}

+ {hiddenMutationBase === apiBase && {t("api.key.mutationHidden")}} {actionError && {actionError}} {keysState.showError && keysData && {t("api.keysLoadFailed")}} {/* The model failure is reported inside the models panel, beside the @@ -542,7 +584,9 @@ export default function ApiKeys({ apiBase, active = true }: { apiBase: string; a ) : ( <> disclosure.invalidate()} keys={keys} apiBase={apiBase} attributionSince={attributionSince} diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx index 718c13d61fb..9c0883a4f91 100644 --- a/gui/src/pages/Providers.tsx +++ b/gui/src/pages/Providers.tsx @@ -387,7 +387,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { fetchConfig, fetchOauth, fetchProviderQuotas, codexActiveNeedsReauth, }); const { - accountSets, setAccountSets, accountLoadStates, switchingAccount, pausingAccount, keyPools, fetchAccountSets, fetchKeyPools, + accountSets, setAccountSets, accountLoadStates, switchingAccounts, pausingAccounts, keyPools, fetchAccountSets, fetchKeyPools, refreshAccountRosters, oauthCardProviders, keyCardProviders, switchAccount, pauseAccount, setAccountPoolThreshold, setAccountThreshold, switchApiKey, removeApiKey, addApiKeyValue, editCredentialAlias, removeAccount, activeAccountNeedsReauth, @@ -669,8 +669,8 @@ export default function Providers({ apiBase }: { apiBase: string }) { accountsFocusProvider={accountsFocus.provider} settingsFocusToken={settingsFocus.token} settingsFocusProvider={settingsFocus.provider} - switchingAccountId={switchingAccount?.provider === item.name ? switchingAccount.accountId : null} - pausingAccountId={pausingAccount?.provider === item.name ? pausingAccount.accountId : null} + switchingAccountId={switchingAccounts[item.name]?.accountId ?? null} + pausingAccountId={pausingAccounts[item.name]?.accountId ?? null} busyProvider={busy} loginHint={loginInfo} authHandlers={authHandlers} diff --git a/gui/src/pages/api-keys-utils.ts b/gui/src/pages/api-keys-utils.ts index 806541b0e78..3a5fe54f044 100644 --- a/gui/src/pages/api-keys-utils.ts +++ b/gui/src/pages/api-keys-utils.ts @@ -20,6 +20,17 @@ export interface ApiKeyEntry { usage: ApiKeyUsage; } +/** + * Outcome of a stored-key reveal. `denied` is the server's standing answer to a + * dashboard session without stored-secret authority — a loopback session may + * list masked keys but cannot disclose one until the operator pairs this + * browser or signs in through a trusted identity. `failed` is the transient + * remainder: timeout, unreachable server, malformed answer. + */ +export type RevealKeyResult = + | { ok: true; key: string } + | { ok: false; kind: "denied" | "failed" }; + /** * A usage object the GUI can actually render. Coercing a malformed one to zeroes * would state "used zero times" about data we could not read — the exact false diff --git a/gui/src/pages/claude-code-sections.tsx b/gui/src/pages/claude-code-sections.tsx index 64e95cb2172..d8efea5b53e 100644 --- a/gui/src/pages/claude-code-sections.tsx +++ b/gui/src/pages/claude-code-sections.tsx @@ -5,6 +5,7 @@ import { Select } from "../ui"; import { applySidecarBackendChange, applySidecarModelChange, + applySidecarPoolChange, sidecarSelectValue, type SidecarSelectValue, } from "./claude-code-sidecar"; @@ -156,6 +157,7 @@ export function ClaudeCodeSettingsCard({ const titleKey = key === "webSearchSidecar" ? "claude.webSearchSidecar" : "claude.visionSidecar"; const hintKey = key === "webSearchSidecar" ? "claude.webSearchSidecarHint" : "claude.visionSidecarHint"; const listId = `claude-sidecar-models-${key}`; + const backend = override?.backend ?? state.sidecarPools?.[key]?.backend; return (
@@ -204,6 +206,21 @@ export function ClaudeCodeSettingsCard({ ))} )} + {backend === "anthropic" && ( + { void saveSidecar({ webSearch: { + anthropicInstance: value === "anthropic" || value === "anthropic2" ? value : null, + } }); }} /> + {sidecar.webSearch.anthropicPool?.mixed && {t("sidecar.poolMixed")}} +
+ )}
{t("dash.webSearchStream")}
+ {sidecar && visionBackend === "anthropic" && ( +
+