diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c449060c9d..00abe5ec24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,32 +21,115 @@ on: # because this workflow is `pull_request` (not `pull_request_target`), # declares `contents: read`, and reads no secrets. # - # Every integration push needs an exact-head aggregate check for promotion - # and release evidence. The `changes` job below keeps expensive work scoped. - # Merge queue candidates are synthetic commits. They must produce the same - # stable `ci` check as pull requests, but their changed-file range comes from - # the payload's immutable base/head SHAs rather than the moving queue ref. - merge_group: - types: [checks_requested] + # `push:` stays pinned to the integration lines: it gates the release path, + # and this trigger already covers review. push: branches: [main, preview, dev] + paths: + - "Dockerfile" + - "compose.yaml" + - ".dockerignore" + - "docker/**" + - "src/**" + - "bin/**" + - "tests/**" + - "scripts/**" + - "gui/**" + - "assets/**" + - ".gitattributes" + - ".npmignore" + - "package.json" + - "bun.lock" + - "tsconfig.json" + - "README.md" + - "LICENSE" + - ".github/workflows/ci.yml" + - ".github/workflows/release.yml" + - ".github/workflows/enforce-pr-target.yml" + - ".github/workflows/stale-needs-info.yml" workflow_dispatch: + inputs: + lane: + description: "all (default) or macos-control" + type: choice + default: all + options: [all, macos-control] permissions: contents: read # Retrigger CI after dir-fsync / oauth deadline follow-ups (tip 34a1ac46). concurrency: - # Keep identities distinct by event. Merge-queue candidates use their - # immutable synthetic SHA; pull requests share a number so a new commit - # supersedes the obsolete run; pushes share a branch ref; manual dispatches - # get an independent run identity. - group: cross-platform-ci-${{ github.event_name }}-${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || github.event_name == 'pull_request' && github.event.pull_request.number || github.event_name == 'push' && github.ref || github.event_name == 'workflow_dispatch' && github.run_id || github.run_id }} - # Candidate evidence must finish. New PR commits and pushes supersede stale - # runs, while merge-queue and manually dispatched runs are never cancelled. - cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} + group: cross-platform-ci-${{ github.ref }} + cancel-in-progress: true jobs: + # Which Windows runner this run is allowed to use. + # + # READ THIS BEFORE TREATING IT AS A SECURITY BOUNDARY: it is not one. + # + # On `pull_request` this workflow is loaded from the PR head, so the `case` + # below is owned by the proposed patch exactly like an `if:` guard would be. + # A hostile PR can delete the branch and hardcode the self-hosted labels into + # `$GITHUB_OUTPUT`, and `runs-on` will honour it. That this job runs on + # `ubuntu-latest` changes nothing — the untrusted part is its OUTPUT, not its + # host. `.github/workflows/ci.yml` is in the `changes` job's `ci` filter, so + # such an edit triggers every expensive verification job. + # + # What actually keeps untrusted code off a self-hosted runner lives OUTSIDE + # this file, where a PR cannot reach it: the fork-PR approval policy + # (`all_external_contributors`) and the judgement of whoever clicks approve. + # Runner groups would be the other lever, but they are an organisation + # feature and this repository is user-owned, so the approval policy is the + # only one available here. GitHub's own guidance is to avoid self-hosted + # runners on public repositories for this reason. + # + # So read the routing below as a STABILITY/OPERATIONS control that keeps + # honest pull requests on GitHub-hosted runners and lets trusted branch runs + # avoid the hosted-Windows Bun crashes. It is not the security boundary. + # + # `push` on dev/main/preview requires the push permission, and + # `workflow_dispatch` requires write access, so both carry a trusted author. + # A trusted author is not audited code: merging a contributor PR into `dev` + # fires `push`, and its dependencies and postinstall hooks then run here. + select-windows-runner: + name: select windows runner + runs-on: ubuntu-latest + timeout-minutes: 2 + outputs: + runner: ${{ steps.pick.outputs.runner }} + label: ${{ steps.pick.outputs.label }} + steps: + - name: Pick runner + id: pick + env: + # Read through env rather than interpolating directly into the script: + # `github.event_name` is a fixed vocabulary, but keeping the habit means + # no future edit here can grow a script-injection sink. + EVENT_NAME: ${{ github.event_name }} + USE_SELF_HOSTED: ${{ vars.OCX_SELF_HOSTED_WINDOWS }} + shell: bash + run: | + set -euo pipefail + trusted=no + case "$EVENT_NAME" in + push|workflow_dispatch) trusted=yes ;; + esac + + # Repository variable OCX_SELF_HOSTED_WINDOWS is an OPERATIONAL switch, + # not a security control: a PR that rewrites this script ignores it for + # the same reason it ignores the event check above. Its job is to keep CI + # working when the box is off or busy. Anything other than `1` — + # including unset, the state before a runner exists — falls back to + # windows-latest. + if [ "$trusted" = "yes" ] && [ "${USE_SELF_HOSTED:-}" = "1" ]; then + echo 'runner=["self-hosted","Windows","X64","ocx-home"]' >> "$GITHUB_OUTPUT" + echo 'label=self-hosted (ocx-home)' >> "$GITHUB_OUTPUT" + else + echo 'runner="windows-latest"' >> "$GITHUB_OUTPUT" + echo 'label=windows-latest' >> "$GITHUB_OUTPUT" + fi + # Which areas this push actually touches. # # Deliberately a job-level filter rather than a wider workflow-level `paths:` @@ -64,7 +147,6 @@ jobs: # a failed filter produces empty outputs, which every `== 'true'` condition # below would read as "nothing changed, skip". permissions: - actions: read contents: read pull-requests: read outputs: @@ -72,26 +154,20 @@ jobs: # step. A missing or malformed filter output must fail this job instead # of silently making every expensive job skip. ci: ${{ steps.scope.outputs.ci }} - dependencies: ${{ steps.scope.outputs.dependencies }} - reuse_dependency_audit: ${{ steps.promotion-audit.outputs.reuse }} - gui: ${{ steps.scope.outputs.gui }} - packaging: ${{ steps.scope.outputs.packaging }} - macos: ${{ steps.scope.outputs.macos }} - swift: ${{ steps.scope.outputs.swift }} + gui: ${{ steps.filter.outputs.gui }} + packaging: ${{ steps.filter.outputs.packaging }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false - # The promotion audit verifier needs both parents of the main merge. - fetch-depth: 2 - name: Detect changed areas id: filter - if: github.event_name != 'merge_group' - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: # Without this the action compares against the repository's DEFAULT # branch, which is `main`. A push to `dev` would then be diffed against @@ -99,66 +175,87 @@ jobs: # as "changed" — the scoped jobs would run on nearly every dev push and # the saving would silently not happen while CI stayed green. # - # paths-filter ignores `base` and uses the API file list for a pull - # request. On pushes this preserves the previous ref comparison. + # On `pull_request` the action ignores this and uses the PR's own file + # list. On a branch push it means "compare against the previous commit + # on this branch", which is the intent. base: ${{ github.ref }} - filters: .github/policies/ci-paths.yml - - - name: Detect merge queue changed areas - id: merge-group-filter - if: github.event_name == 'merge_group' - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 - with: - # Never infer this range from the moving gh-readonly-queue ref. The - # merge_group webhook supplies the immutable synthetic head and the - # exact base commit against which this candidate was constructed. - base: ${{ github.event.merge_group.base_sha }} - ref: ${{ github.event.merge_group.head_sha }} - filters: .github/policies/ci-paths.yml + filters: | + # Mirrors the push trigger's path allowlist. Pull requests always + # start the workflow so the aggregate check exists, while these + # paths decide whether the expensive test jobs need to run. + ci: + - 'Dockerfile' + - 'compose.yaml' + - '.dockerignore' + - 'docker/**' + - 'src/**' + - 'bin/**' + - 'tests/**' + - 'scripts/**' + - 'gui/**' + - 'assets/**' + - '.gitattributes' + - '.npmignore' + - 'package.json' + - 'bun.lock' + - 'tsconfig.json' + - 'README.md' + - 'LICENSE' + - '.github/workflows/ci.yml' + - '.github/workflows/release.yml' + - '.github/workflows/enforce-pr-target.yml' + - '.github/workflows/stale-needs-info.yml' + gui: + - 'gui/**' + # Everything that ends up inside `npm pack`, or that decides what + # does. `src/**` belongs here because package.json ships `src` and + # bin/ocx.mjs executes it: without that entry an ordinary source PR + # would get no Windows verification at all, since the Windows suite + # now runs only at the shipping boundary. + packaging: + - 'package.json' + - 'bun.lock' + - 'src/**' + - 'bin/**' + - 'gui/**' + - 'assets/**' + - '.npmignore' + # `.gitattributes` decides how tracked package inputs are + # materialized on each runner, so an attribute change can put CRLF + # shebangs into the tarball without any source file moving. + - '.gitattributes' + - 'README.md' + - 'LICENSE' + - 'scripts/prepare-package.ts' - name: Assert the scope output is usable id: scope shell: bash env: - EVENT_NAME: ${{ github.event_name }} - NORMAL_CI_SCOPE: ${{ steps.filter.outputs.ci }} - NORMAL_DEPENDENCIES_SCOPE: ${{ steps.filter.outputs.dependencies }} - NORMAL_GUI_SCOPE: ${{ steps.filter.outputs.gui }} - NORMAL_PACKAGING_SCOPE: ${{ steps.filter.outputs.packaging }} - NORMAL_MACOS_SCOPE: ${{ steps.filter.outputs.macos }} - NORMAL_SWIFT_SCOPE: ${{ steps.filter.outputs.swift }} - MERGE_GROUP_CI_SCOPE: ${{ steps.merge-group-filter.outputs.ci }} - MERGE_GROUP_DEPENDENCIES_SCOPE: ${{ steps.merge-group-filter.outputs.dependencies }} - MERGE_GROUP_GUI_SCOPE: ${{ steps.merge-group-filter.outputs.gui }} - MERGE_GROUP_PACKAGING_SCOPE: ${{ steps.merge-group-filter.outputs.packaging }} - MERGE_GROUP_MACOS_SCOPE: ${{ steps.merge-group-filter.outputs.macos }} - MERGE_GROUP_SWIFT_SCOPE: ${{ steps.merge-group-filter.outputs.swift }} + CI_SCOPE: ${{ steps.filter.outputs.ci }} run: | set -euo pipefail - prefix=NORMAL - [ "$EVENT_NAME" = merge_group ] && prefix=MERGE_GROUP - for scope in ci dependencies gui packaging macos swift; do - name="${prefix}_${scope^^}_SCOPE" - value="${!name-}" - case "$value" in - true|false) printf '%s=%s\n' "$scope" "$value" >> "$GITHUB_OUTPUT" ;; - *) printf '::error::%s was %q, expected true or false\n' "$name" "$value"; exit 1 ;; - esac - done - - - name: Verify reusable promotion audit evidence - id: promotion-audit - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - env: - BEFORE_SHA: ${{ github.event.before }} - DEPENDENCIES_CHANGED: ${{ steps.scope.outputs.dependencies }} - GITHUB_TOKEN: ${{ github.token }} - run: node .github/scripts/promotion-audit-reuse.cjs - - # The general suite retains fresh-process containment after direct native - # sharding failed qualification. The lane manifest is authoritative for - # membership; validated Bun 1.4 timings only drive longest-processing-time - # allocation. The batching helper remains the process boundary. + case "$CI_SCOPE" in + true|false) + printf 'ci=%s\n' "$CI_SCOPE" >> "$GITHUB_OUTPUT" + ;; + *) + printf '::error::changes.outputs.ci was %q, expected true or false\n' "$CI_SCOPE" + exit 1 + ;; + esac + + # The suite, split by file across four Linux runners. + # + # `scripts/ci/run-bun-test-batches.sh` mirrors Bun's sorted round-robin shard + # assignment, then runs each shard in small batches so every batch gets a fresh + # Bun process. The helper prints the exact files before each batch and retries + # only a Bun runtime crash once; ordinary test failures are never retried. + # Storage-policy API tests and api-usage are deliberately excluded here and run + # in dedicated jobs below. Bun 1.3.14 can corrupt the Linux isolate/epoll state + # around those Worker-heavy harnesses; keeping them out of the general shards + # prevents one runtime failure from wedging ~150 unrelated files while preserving + # the same coverage in fresh Bun processes. # # Only the suite lives here. Typecheck, lint, build, and the scans run once in # `gates` rather than four times — they are fixed cost, and paying it per shard @@ -166,9 +263,7 @@ jobs: test: name: test ${{ matrix.shard }}/4 needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ubuntu-latest # A quarter of the suite. A shard that needs longer than this is wedged, not # slow — the old 30-minute ceiling was margin for the Windows leg, which no @@ -182,8 +277,9 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false # tests/ci-workflows/release-version-line.test.ts compares package.json against the # newest release tag. actions/checkout fetches no tags by default, so @@ -222,35 +318,10 @@ jobs: cd gui bun run build - - name: Restore canonical test timings - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: .bun-timings.json - key: ocx-test-timings-dev-${{ github.sha }} - restore-keys: | - ocx-test-timings-dev- - - - name: Validate restored timing data - if: hashFiles('.bun-timings.json') != '' - run: bun scripts/ci/validate-timings.ts .bun-timings.json --discard-invalid - - - name: Test in fresh-process timing-aware batches - shell: bash + - name: Test in fresh-process batches env: TEST_SHARD: ${{ matrix.shard }}/4 - run: | - bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD" - - - name: Upload trusted-dev shard timing report - if: >- - success() && github.event_name == 'push' && github.ref == 'refs/heads/dev' && - hashFiles('.bun-timings.json') != '' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ci-timings-general-${{ matrix.shard }} - path: .bun-timings.json - include-hidden-files: true - if-no-files-found: error + run: bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD" # Bun 1.3.14 has shown a Linux isolate/epoll race around the storage-policy # harness. Keep the entire six-file family in one fresh process so a runtime @@ -258,9 +329,7 @@ jobs: storage-policy: name: storage policy needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -285,8 +354,13 @@ jobs: - name: Test storage policy API run: | - mapfile -t dedicated_files < <(bun scripts/ci/test-lanes.ts --lane dedicated-storage) - bun scripts/test.ts --isolate "${dedicated_files[@]}" + bun test --isolate \ + ./tests/storage/api-storage-policy-already-running.test.ts \ + ./tests/storage/api-storage-policy-mutation-busy.test.ts \ + ./tests/storage/api-storage-policy-put-race.test.ts \ + ./tests/storage/api-storage-policy-run.test.ts \ + ./tests/storage/api-storage-policy.test.ts \ + ./tests/storage/api-storage.test.ts # Bun 1.3.14 has shown a Linux isolate wedge around startServer() plus the user # cost overlay reconciler. Keep api-usage in one fresh process so a runtime @@ -294,9 +368,7 @@ jobs: api-usage: name: api usage needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -320,86 +392,7 @@ jobs: bun run build - name: Test api usage API - run: | - mapfile -t dedicated_files < <(bun scripts/ci/test-lanes.ts --lane dedicated-api) - bun scripts/test.ts --isolate "${dedicated_files[@]}" - - serial-load-sensitive: - name: serial load-sensitive tests - needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - persist-credentials: false - fetch-tags: true - - - name: Setup project Bun - uses: ./.github/actions/setup-project-bun - - - name: Install dependencies - run: | - bun install --frozen-lockfile - cd gui - bun install --frozen-lockfile - - - name: Build GUI - run: | - cd gui - bun run build - - - name: Test serial lane - shell: bash - run: | - set -euo pipefail - mapfile -t serial_files < <(bun scripts/ci/test-lanes.ts --lane serial) - bun scripts/test.ts --isolate --parallel=1 "${serial_files[@]}" - - # Only trusted dev pushes may turn the four partial reports into the one - # canonical timing cache. PRs and release branches can restore that cache, - # but never publish data under a key they could later restore. - publish-test-timings: - name: publish test timings - needs: test - if: >- - needs.test.result == 'success' && github.event_name == 'push' && - github.ref == 'refs/heads/dev' - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - with: - persist-credentials: false - - - name: Setup project Bun - uses: ./.github/actions/setup-project-bun - - - name: Download trusted-dev shard timing reports - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: ci-timings-general-* - path: .tmp/ci-timings - - - name: Merge and validate timing reports - shell: bash - run: | - set -euo pipefail - mapfile -t timing_files < <(find .tmp/ci-timings -type f -name .bun-timings.json -print) - test "${#timing_files[@]}" -eq 4 - bun scripts/ci/merge-timings.ts "${timing_files[@]}" - bun scripts/ci/validate-timings.ts .bun-timings.json - - - name: Save canonical dev timing cache - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: .bun-timings.json - key: ocx-test-timings-dev-${{ github.sha }} + run: bun test --isolate ./tests/server/api-usage.test.ts # Everything that is not the suite: type safety, privacy, lint, build, smoke. # One runner, once per push. Splitting these across the shards would repeat a @@ -407,17 +400,16 @@ jobs: gates: name: gates needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false - name: Setup project Bun @@ -429,49 +421,6 @@ jobs: cd gui bun install --frozen-lockfile - - name: Dependency audit (high severity) - id: dependency-audit - if: needs.changes.outputs.dependencies == 'true' && needs.changes.outputs.reuse_dependency_audit != 'true' - run: bun run audit:high - - - name: Create dependency audit proof - id: dependency-audit-proof - if: github.event_name == 'pull_request' && steps.dependency-audit.outcome == 'success' - shell: bash - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - set -euo pipefail - [[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] - [[ "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] - [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] - audited_tree="$(git rev-parse 'HEAD^{tree}')" - proof_name="dependency-audit-pr-${PR_NUMBER}-base-${BASE_SHA}-head-${HEAD_SHA}-tree-${audited_tree}" - mkdir -p .tmp/dependency-audit-proof - printf '%s\n' "$proof_name" > .tmp/dependency-audit-proof/evidence.txt - printf 'name=%s\n' "$proof_name" >> "$GITHUB_OUTPUT" - - - name: Publish dependency audit proof - if: steps.dependency-audit-proof.outcome == 'success' - # Artifact availability affects only later reuse. The audit itself has - # already passed, so an artifact outage must withhold reuse, not turn - # this PR red; main then falls back to its own live fail-closed audit. - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ steps.dependency-audit-proof.outputs.name }} - path: .tmp/dependency-audit-proof/evidence.txt - if-no-files-found: error - retention-days: 1 - - - name: Validate GitHub Actions workflows - run: bun run lint:workflows - - - name: Validate repository workflow policy - run: bun scripts/ci/check-workflow-policy.ts - - name: GUI lint if: needs.changes.outputs.gui == 'true' run: | @@ -482,15 +431,7 @@ jobs: run: | bun x tsc --noEmit bun x tsc --noEmit -p tests/tsconfig.doctor-service-memory-contract.json - - - name: Install replit-gateway companion - run: cd integrations/replit-gateway && bun install --frozen-lockfile - - - name: Replit gateway typecheck - run: cd integrations/replit-gateway && bun run typecheck - - - name: Replit gateway tests - run: cd integrations/replit-gateway && bun run test + bun x tsc --ignoreConfig --noEmit --strict --target ESNext --module ESNext --moduleResolution bundler --types bun-types --skipLibCheck scripts/ci/docker-smoke.ts - name: GUI tests run: cd gui && bun test --isolate tests @@ -531,25 +472,28 @@ jobs: - name: CLI help smoke run: bun run src/cli/index.ts help - # Dev pushes and relevant pull requests keep a focused Darwin/process lane. - # Release refs and main-targeting changes use the timing-balanced full-suite - # matrix below; nightly-macos retains the unsharded scheduler/load control. platform-macos: - name: macos + name: macos ${{ matrix.shard }}/2 needs: changes - if: >- - (github.event_name == 'push' && github.ref == 'refs/heads/dev') || - (github.event_name == 'pull_request' && github.base_ref == 'main') || - (github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/main') || - ((github.event_name == 'pull_request' || github.event_name == 'merge_group') && needs.changes.outputs.macos == 'true') + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: macos-latest - timeout-minutes: 15 + # Two shards. Unsharded, this job was the critical path on every green dev + # push (mean 14.9 min against a 4.7 min Linux maximum; devlog + # 260905_test_modularization_and_windows/003). Two halves finish in ~7.7 and + # cost 0.6 extra macOS minutes of setup per run. The whole-pool control that + # the single job used to provide lives in macos-control below, on dispatch. + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + shard: [1, 2] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false # tests/ci-workflows/release-version-line.test.ts compares package.json against the # newest release tag. actions/checkout fetches no tags by default, so @@ -578,71 +522,139 @@ jobs: cd gui bun run build - - name: Restore canonical test timings - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: .bun-timings.json - key: ocx-test-timings-dev-${{ github.sha }} - restore-keys: | - ocx-test-timings-dev- - - - name: Validate restored timing data - if: hashFiles('.bun-timings.json') != '' - run: bun scripts/ci/validate-timings.ts .bun-timings.json --discard-invalid - - - name: Swift/WebKit integration - if: >- - needs.changes.outputs.swift == 'true' && - (github.event_name == 'pull_request' || github.event_name == 'merge_group' || - (github.event_name == 'push' && github.ref == 'refs/heads/dev')) - run: bash scripts/ci/run-bun-with-crash-retry.sh -- bun scripts/test.ts --isolate --timeout 60000 tests/adapters/google/aistudio-native-webkit.test.ts - - - name: Focused Darwin/process lifecycle tests - if: >- - github.event_name == 'pull_request' || github.event_name == 'merge_group' || - (github.event_name == 'push' && github.ref == 'refs/heads/dev') + # Bun 1.3.14 segfaults while reclaiming a Worker at an `--isolate` file + # boundary: the header shows BALANCED `workers_spawned(N) + # workers_terminated(N)` and the process dies with exit 133 after the last + # assertion in the file already passed. It landed on + # `storage-worker-lifecycle` and `server-background-lifecycle` — the two + # files that tear down a still-busy policy Worker — and accounted for most + # of this leg's red runs on `dev` while every failing SHA passed on rerun. + # + # This is a runtime crash, not a test result, and the Linux shards already + # retry exactly this class through `scripts/ci/run-bun-test-batches.sh` + # (`is_bun_runtime_crash`). The unsharded macOS control had no equivalent, + # so the same crash that Linux absorbs failed the whole promotion here. + # + # Keep the signature list in sync with `is_bun_runtime_crash` in + # scripts/ci/run-bun-test-batches.sh. An assertion failure still fails on + # the first attempt — only the crash signature is retried, exactly once. + - name: Test + env: + MACOS_TEST_SHARD: ${{ matrix.shard }} run: | - bash scripts/ci/run-bun-with-crash-retry.sh -- bun scripts/test.ts --isolate --timeout 60000 \ - --timings .bun-timings.json --update-timings \ - tests/codex-integration/codex-app-server-processes.test.ts \ - tests/codex-integration/codex-prompt-text-probe.test.ts \ - tests/providers/cursor/cursor-native-exec-shell.test.ts \ - tests/lib/process-control.test.ts \ - tests/service/process-state.test.ts \ - tests/service/service.test.ts \ - tests/storage/storage-worker-lifecycle.test.ts \ - tests/storage/storage-worker-os-join-settle.test.ts \ - tests/storage/storage-worker-teardown-isolate.test.ts \ - tests/ci-workflows/test-runner.test.ts + # GitHub Actions starts bash `run:` blocks with `-e`. Disable + # errexit so a Bun crash reaches PIPESTATUS and the bounded retry. + set +e + set -uo pipefail + + run_macos_suite() { + local suite_log suite_status attempt + suite_log="$(mktemp -t ocx-macos-suite.XXXXXX)" || return $? + for attempt in 1 2; do + # Preserve the existing per-test ceiling and crash-only retry for + # every invocation, including each isolated serial file. + bun test --isolate --timeout 60000 "$@" 2>&1 | tee "$suite_log" + suite_status="${PIPESTATUS[0]}" + if [ "$suite_status" -eq 0 ]; then + rm -f "$suite_log" + return 0 + fi + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + rm -f "$suite_log" + return "$suite_status" + fi + echo "::warning::Bun runtime crash in the macOS suite (exit ${suite_status}, attempt ${attempt})." + done + echo "::error::Bun runtime crash repeated on the macOS suite; failing after one retry." + rm -f "$suite_log" + return "$suite_status" + } + + case "$MACOS_TEST_SHARD" in + 1|2) ;; + *) echo "::error::Invalid macOS test shard"; exit 64 ;; + esac + serial_manifest="$(bun -e 'import { SERIAL_FULL_SUITE_FILES } from "./scripts/test.ts"; console.log(SERIAL_FULL_SUITE_FILES.join("\n"));')" + manifest_status=$? + if [ "$manifest_status" -ne 0 ]; then + exit "$manifest_status" + fi + serial_files=() + ignore_args=() + serial_count=0 + while IFS= read -r file; do + if [[ ! "$file" =~ ^[[:alnum:]_./-]+$ || "$file" == /* || "/$file/" == *"/../"* || "/$file/" == *"/./"* ]]; then + echo "::error::Invalid serial test path" + exit 1 + fi + for ((index=0; index- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.base_ref == 'main') || - (github.event_name == 'merge_group' && - github.event.merge_group.base_ref == 'refs/heads/main') || - (github.event_name == 'push' && - (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/preview')) + if: github.event_name == 'workflow_dispatch' runs-on: macos-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - shard: [1, 2] + # The unsharded control for the sharded Linux lane: the only place the whole + # suite runs in one pool, so it is the place that catches what sharding + # hides. The flakes it keeps surfacing are timing, not logic, and the fix + # is the tests, not a fourth lane. + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false + # tests/ci-workflows/release-version-line.test.ts compares package.json against the + # newest release tag. actions/checkout fetches no tags by default, so + # without this the check reads an empty tag set and passes on anything - + # the exact regression it exists to catch would ride through CI green. + # + # Tags only, not full history: `fetch-depth: 0` would clone every commit to + # answer a question about refs. A shallow fetch still brings each tag and its + # target commit, which is all the check reads - the tag list, and whether the + # newest tag names HEAD. That second read only happens on a release commit, + # where the tag points at HEAD and the commit is present by definition. fetch-tags: true - name: Setup project Bun @@ -654,52 +666,61 @@ jobs: cd gui bun install --frozen-lockfile + # Same reason as the shards: the suite serves gui/dist and reads it back. - name: Build GUI run: | cd gui bun run build - - name: Restore canonical test timings - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: .bun-timings.json - key: ocx-test-timings-dev-${{ github.sha }} - restore-keys: | - ocx-test-timings-dev- - - - name: Validate restored timing data - if: hashFiles('.bun-timings.json') != '' - run: bun scripts/ci/validate-timings.ts .bun-timings.json --discard-invalid - - - name: Full macOS shard with isolated load-sensitive files - shell: bash - env: - TEST_SHARD: ${{ matrix.shard }}/2 + # Bun 1.3.14 segfaults while reclaiming a Worker at an `--isolate` file + # boundary: the header shows BALANCED `workers_spawned(N) + # workers_terminated(N)` and the process dies with exit 133 after the last + # assertion in the file already passed. It landed on + # `storage-worker-lifecycle` and `server-background-lifecycle` — the two + # files that tear down a still-busy policy Worker — and accounted for most + # of this leg's red runs on `dev` while every failing SHA passed on rerun. + # + # This is a runtime crash, not a test result, and the Linux shards already + # retry exactly this class through `scripts/ci/run-bun-test-batches.sh` + # (`is_bun_runtime_crash`). The unsharded macOS control had no equivalent, + # so the same crash that Linux absorbs failed the whole promotion here. + # + # Keep the signature list in sync with `is_bun_runtime_crash` in + # scripts/ci/run-bun-test-batches.sh. An assertion failure still fails on + # the first attempt — only the crash signature is retried, exactly once. + - name: Test run: | - set -euo pipefail - main_list="$(mktemp -t ocx-macos-main.XXXXXX)" - serial_list="$(mktemp -t ocx-macos-serial.XXXXXX)" - cleanup() { rm -f -- "$main_list" "$serial_list"; } - trap cleanup EXIT - bun scripts/ci/test-lanes.ts --lane platform-main --timings .bun-timings.json --shard "$TEST_SHARD" > "$main_list" - bun scripts/ci/test-lanes.ts --lane platform-serial --timings .bun-timings.json --shard "$TEST_SHARD" > "$serial_list" - main_files=() - while IFS= read -r file; do main_files+=("$file"); done < "$main_list" - test "${#main_files[@]}" -gt 0 - bash scripts/ci/run-bun-with-crash-retry.sh -- bun scripts/test.ts --isolate --timeout 60000 "${main_files[@]}" - serial_files=() - while IFS= read -r file; do serial_files+=("$file"); done < "$serial_list" - test "${#serial_files[@]}" -gt 0 - for file in "${serial_files[@]}"; do - bash scripts/ci/run-bun-with-crash-retry.sh -- bun scripts/test.ts --isolate --parallel=1 --timeout 60000 "$file" + # GitHub Actions starts bash `run:` blocks with `-e`. Disable + # errexit so a Bun crash reaches PIPESTATUS and the bounded retry. + set +e + set -uo pipefail + suite_log="$(mktemp -t ocx-macos-suite.XXXXXX)" + for attempt in 1 2; do + # --timeout: Bun's default 5s per-test ceiling is the recurring flake + # class on this loaded shared runner (real retry windows + server + # round-trips exceed 5s under contention; a 10s-floor in-test + # watchdog fired at 10.16s there). 60s keeps hangs bounded (the 30m + # job timeout is the outer backstop) while removing the timing + # flakes — assertions are untouched. Pairs with the 30s CI floor in + # tests/helpers/ci-watchdog.ts. + bun test --isolate --timeout 60000 tests 2>&1 | tee "$suite_log" + suite_status="${PIPESTATUS[0]}" + if [ "$suite_status" -eq 0 ]; then + exit 0 + fi + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + exit "$suite_status" + fi + echo "::warning::Bun runtime crash in the macOS suite (exit ${suite_status}, attempt ${attempt})." done + echo "::error::Bun runtime crash repeated on the macOS suite; failing after one retry." + exit 1 - name: CLI help smoke run: bun run src/cli/index.ts help - # Windows is a required shipping-platform gate on integration pushes and on - # pull requests that touch CI-relevant code. Documentation-only pull requests - # may skip it through the same `changes` decision used by the Linux shards. + # Windows runs only when a maintainer asks for it by hand. # # It left the PR lane first (16m23s against a 6-minute Linux critical path; # the ceiling was raised twice rather than the gap closed — #711 vs #653, @@ -710,20 +731,17 @@ jobs: # version — gating the release on them blocks shipping fixes to the platforms # that pass, for a platform that has never shipped green. # - # The tracked portability failures are fixed and the four hosted shards are - # green again. Keep the leg sharded and bounded: removing it from ordinary - # pushes is what allowed the Windows-only backlog to accumulate unnoticed. + # The leg stays in the workflow, sharded and dispatchable, so the failure + # list can be burned down without losing the ability to measure progress. + # release.yml gates on a successful push-event run of this workflow, which + # now means Linux + macOS + the gates; Windows re-enters the gate when the + # tracked failures are fixed, not before. platform-windows: name: windows ${{ matrix.shard }}/6 - needs: [changes] + needs: select-windows-runner if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' - # Public pull-request code must never reach a persistent self-hosted host. - # Keep the complete Windows lane on an ephemeral GitHub-hosted runner for - # every event, including trusted pushes, so event-routing drift cannot turn - # a repository workflow edit into host access. - runs-on: windows-latest + github.event_name == 'workflow_dispatch' && (github.event.inputs.lane == '' || github.event.inputs.lane == 'all') + runs-on: ${{ fromJSON(needs.select-windows-runner.outputs.runner) }} # Sharded like the Linux legs. The single-leg run reached 30 minutes on a # green suite and was killed in cleanup; four shards put each leg inside the # same budget the Linux shards already hold. @@ -736,23 +754,50 @@ jobs: # margin. 25 leaves the outer bound in place — a wedged shard still dies — # while making a completed shard the normal outcome. The crash-retry below can # double a shard's work, and this ceiling has to cover that second attempt too. - # Four shards later grew back into that ceiling as the suite expanded. Six - # shards reduce each hosted-Windows process's filesystem, Worker, and child - # process pressure without raising the ceiling or weakening assertions. - # Shard 1 of upstream run 34036848646 then reached that wall with 2736 - # passing tests and no test failures. Keep all six hosted shards and every - # test deadline, but leave the whole batch and cleanup a 30-minute bound. + # + # Four shards then grew into the ceiling: across five runs of one branch, completed + # shards took 17-25 minutes and run 33934756997 cancelled a green 3/4 at 25m12s — + # the same truncation as above. The bound is kept; the work per shard is cut instead. + # Six shards put each leg at roughly two-thirds of the four-shard wall time, back + # inside the margin 25 was chosen to provide. + # Shard 1 of run 34036848646 then reached that wall with 2736 passing tests + # and no test failures. The matched tests were 25% slower than the prior + # complete run; about one minute of tests remained. Keep every test deadline + # and all six shards, but leave the whole batch and cleanup a 30-minute bound. timeout-minutes: 30 strategy: fail-fast: false matrix: shard: [1, 2, 3, 4, 5, 6] steps: + - name: Show selected runner + shell: bash + run: echo "windows leg on ${{ needs.select-windows-runner.outputs.label }}" + + # A self-hosted runner keeps its working directory between jobs. Without an + # explicit wipe, a file deleted in the commit under test survives on disk + # and the suite passes against a tree that no longer exists in git. + # `--ephemeral` registration de-registers the runner after each job but does + # not clean the workspace, so this step is what makes the checkout honest. + - name: Clean workspace (self-hosted only) + if: runner.environment == 'self-hosted' + shell: bash + # `|| true` used to swallow this, which defeats the point: a clean that + # fails on permissions leaves the deleted files in place and the checkout + # below then validates a tree that no longer exists in git. Only the + # not-a-repository case is tolerated — that is the first run on a fresh + # box, where there is nothing to clean. + run: | + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git clean -xffd . + fi + - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false # Same reason as the Linux shards and the macOS control: this leg runs the # whole suite, and tests/ci-workflows/release-version-line.test.ts reads release tags. @@ -774,24 +819,6 @@ jobs: cd gui bun run build - - name: Restore canonical test timings - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 - with: - path: .bun-timings.json - key: ocx-test-timings-dev-${{ github.sha }} - restore-keys: | - ocx-test-timings-dev- - - - name: Validate restored timing data - if: hashFiles('.bun-timings.json') != '' - run: bun scripts/ci/validate-timings.ts .bun-timings.json --discard-invalid - - # Validate a fresh Windows Bun/CLI process before the process-heavy shard. - # A hosted VM can otherwise finish every test and then fail the loader with - # STATUS_DLL_INIT_FAILED before the CLI executes at all. - - name: CLI help smoke - run: bun run src/cli/index.ts help - - name: Test # --timeout: the Linux batches and the macOS control both pass 60000; this leg was # the only one left on Bun's 5s default, and it is the slowest hardware on the board. @@ -803,68 +830,14 @@ jobs: # on it reports a defect this repository does not have (#2152). An ordinary assertion # failure returns its status immediately — only the crash signatures below are retried, # and only once, so a genuinely broken build cannot be retried into green. - # - # Every shard owns one ephemeral GitHub-hosted VM and this step invokes its - # test runners sequentially. There is no peer process to queue behind, so - # resolving and ACL-hardening the desktop-grade per-user lock adds no safety. - # More importantly, that resolution starts PowerShell before any test can - # run; under hosted contention the bounded SID lookup has timed out and - # discarded an otherwise usable shard. Keep the local/multi-worktree lock - # fail-closed, while explicitly using its documented no-queue mode inside - # this isolated single-owner CI boundary. - env: - OCX_TEST_NO_QUEUE: "1" shell: bash run: | set +e set -uo pipefail suite_log="$(mktemp -t ocx-windows-suite.XXXXXX)" - timing_args=() - if [ -f .bun-timings.json ]; then - timing_args=(--timings .bun-timings.json) - fi - general_list="$(mktemp -t ocx-windows-general.XXXXXX)" - serial_list="$(mktemp -t ocx-windows-serial.XXXXXX)" - if ! bun scripts/ci/test-lanes.ts --lane general "${timing_args[@]}" --shard ${{ matrix.shard }}/6 > "$general_list"; then - echo "::error::Windows general lane selection failed." - exit 1 - fi - if ! bun scripts/ci/test-lanes.ts --lane serial "${timing_args[@]}" --shard ${{ matrix.shard }}/6 > "$serial_list"; then - echo "::error::Windows serial lane selection failed." - exit 1 - fi - mapfile -t general_files < "$general_list" - mapfile -t serial_files < "$serial_list" - rm -f "$general_list" "$serial_list" - if [ "${#general_files[@]}" -eq 0 ] || [ "${#serial_files[@]}" -eq 0 ]; then - echo "::error::Windows lane selection returned an empty shard." - exit 1 - fi for attempt in 1 2; do - : > "$suite_log" - bun scripts/test.ts --isolate --timeout 60000 "${general_files[@]}" 2>&1 | tee "$suite_log" + bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/6 2>&1 | tee "$suite_log" suite_status="${PIPESTATUS[0]}" - if [ "$suite_status" -eq 0 ]; then - # Each process-heavy serial file gets a fresh Bun runner. `--isolate` - # resets test globals, but one multi-file Bun process can still retain - # runtime/handle pressure before the next file spawns its own children. - for serial_file in "${serial_files[@]}"; do - serial_timeout=60000 - if [[ "$serial_file" == "tests/codex-integration/codex-composed-acceptance.test.ts" ]]; then - # Case E starts two real lock children. Each resolves its - # effective-token namespace through bounded PowerShell calls, - # so the file's 240s case budget must not be preempted by the - # general 60s per-test ceiling. The extra minute lets the test - # report its own deadline instead of racing Bun's outer kill. - serial_timeout=300000 - fi - bun scripts/test.ts --isolate --parallel=1 --timeout "$serial_timeout" "$serial_file" 2>&1 | tee -a "$suite_log" - suite_status="${PIPESTATUS[0]}" - if [ "$suite_status" -ne 0 ]; then - break - fi - done - fi if [ "$suite_status" -eq 0 ]; then exit 0 fi @@ -877,15 +850,16 @@ jobs: echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/6; failing after one retry." exit 1 + - name: CLI help smoke + run: bun run src/cli/index.ts help + # Keep every OS credential-store check on a disposable GitHub-hosted machine. # A force-cancelled process cannot run its in-process finally cleanup, so no - # keyring matrix leg may use a persistent runner. + # keyring matrix leg may use the persistent self-hosted Windows runner. keyring-smoke: name: keyring ${{ matrix.name }} needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - needs.changes.outputs.ci == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: ${{ matrix.runner }} timeout-minutes: 8 strategy: @@ -943,24 +917,53 @@ jobs: bun run scripts/keyring-smoke.ts ' + # Exercise the source-build Compose contract, including real volume reuse. + # Host fixtures cannot prove image construction or container recreation. + docker-smoke: + name: docker smoke + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Build, start, and recreate the container + run: bun scripts/ci/docker-smoke.ts + npm-global-smoke: name: npm-global ${{ matrix.os }} needs: changes if: needs.changes.outputs.packaging == 'true' runs-on: ${{ matrix.os }} + # 8 minutes was too tight for the Windows leg: dependency installation alone takes + # about 7 there, leaving under a minute for pack, verify, global install, and the + # help smoke. The job was cancelled at the wall rather than failing, so whichever + # step happened to be running was reported `cancelled` — observed on step 8 four + # times and on step 7 once, which is what made it read as a flaky global install + # instead of a budget that one OS cannot meet (#3441). timeout-minutes: 20 strategy: fail-fast: false matrix: - # Keep global-install smoke on disposable hosted machines: it writes - # into the machine's global prefix and must not leave state behind. + # Deliberately NOT routed to the self-hosted box. This job runs + # `npm install -g`, which writes into the machine's global prefix and + # would leave an `ocx` on a maintainer's personal PATH. It is an + # short job on Linux and macOS, so there is nothing to win by moving it. os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - # No job here pushes, so leaving a usable token in .git/config is - # avoidable residue. Matches the other workflows. + # No job here pushes, and the self-hosted box keeps its checkout + # between jobs, so leaving a usable token in .git/config is avoidable + # residue. Matches the convention already used by the other workflows. persist-credentials: false # Deliberately NO setup-bun: prove `npm install -g` works without a @@ -968,7 +971,7 @@ jobs: - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22 + node-version: 20 - name: Install package dependencies run: npm install @@ -985,7 +988,7 @@ jobs: - name: Install globally (downloads bundled bun) shell: bash - run: npm install -g "./$(node -p "require('./pack.json')[0].filename")" + run: npm install -g ./bitkyc08-opencodex-*.tgz - name: ocx help via bundled bun run: ocx help @@ -1008,8 +1011,11 @@ jobs: ci: name: ci if: always() - # Every producer, including the ones that only feed other jobs. - needs: [changes, test, storage-policy, api-usage, serial-load-sensitive, publish-test-timings, gates, platform-macos, platform-macos-full, platform-windows, keyring-smoke, npm-global-smoke] + # Every producer, including the ones that only feed other jobs. `needs` holds + # direct dependencies only, so a failing `select-windows-runner` would + # otherwise reach this gate as nothing at all while its dependents report + # `skipped` — which the gate is required to read as a deliberate skip. + needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, npm-global-smoke] runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -1017,8 +1023,6 @@ jobs: shell: bash env: RESULTS: ${{ toJSON(needs) }} - WINDOWS_REQUIRED: ${{ (github.event_name != 'pull_request' && github.event_name != 'merge_group') || needs.changes.outputs.ci == 'true' }} - WINDOWS_RESULT: ${{ needs.platform-windows.result }} run: | set -euo pipefail echo "$RESULTS" | jq . @@ -1036,7 +1040,9 @@ jobs: echo "::error::needed job(s) did not pass: $bad" exit 1 fi - if [ "$WINDOWS_REQUIRED" = "true" ] && [ "$WINDOWS_RESULT" != "success" ]; then - echo "::error::Windows was required but reported: $WINDOWS_RESULT" - exit 1 - fi + + # Windows is dispatch-only, so there is no event where a skipped Windows + # leg is a gate violation: on push events it is always skipped, and on + # dispatch a failed Windows leg already fails the allowlist above. The + # old "windows must have run on main/preview" assertion left with the + # condition it policed. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c9ec66505..7b565b6800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ name: Release -# Publish opencodex to npm — triggered from the Actions tab with explicit inputs, or by the -# fork's audited stable-release dispatcher. Bump package.json on main BEFORE dispatching (or -# use `bun run release `, which does the bump+commit+push+dispatch for you); the workflow -# verifies the version and exact commit before publishing. +# Publish opencodex to npm — jawcode-style: triggered from the Actions tab with an explicit +# version, dist-tag, and a dry-run-first default. Bump package.json on main BEFORE dispatching +# (or use `bun run release `, which does the bump+commit+push+dispatch for you); the +# workflow verifies the version matches before publishing. on: workflow_dispatch: inputs: @@ -18,7 +18,6 @@ on: options: - latest - preview - - dev default: latest dry-run: description: "Dry run (build + pack, no actual publish)" @@ -29,27 +28,9 @@ on: description: "Immutable release commit this dispatch must publish (fail if the branch moved)" required: true type: string - candidate-run-id: - description: "Successful Build release candidate workflow run ID" - required: false - type: string - candidate-artifact-id: - description: "Immutable release candidate artifact ID from that run" - required: false - type: string - repository_dispatch: - types: [fork-auto-release] permissions: {} -env: - DISPATCH_VERSION: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version || inputs.version }} - DISPATCH_TAG: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.tag || inputs.tag }} - DISPATCH_EXPECTED_SHA: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.expected_sha || inputs.expected-sha }} - DISPATCH_DRY_RUN: ${{ github.event_name == 'repository_dispatch' && 'false' || inputs.dry-run }} - DISPATCH_CANDIDATE_RUN_ID: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.candidate_run_id || inputs.candidate-run-id }} - DISPATCH_CANDIDATE_ARTIFACT_ID: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.candidate_artifact_id || inputs.candidate-artifact-id }} - concurrency: group: release cancel-in-progress: false @@ -63,37 +44,24 @@ jobs: - name: Checkout trusted dispatch guard uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - ref: ${{ github.sha }} + ref: ${{ github.event.repository.default_branch }} persist-credentials: false path: trusted-dispatch - name: Validate release dispatch env: - EXPECTED_SHA: ${{ env.DISPATCH_EXPECTED_SHA }} - DISPATCH_TAG: ${{ env.DISPATCH_TAG }} - DISPATCH_DRY_RUN: ${{ env.DISPATCH_DRY_RUN }} - CANDIDATE_RUN_ID: ${{ env.DISPATCH_CANDIDATE_RUN_ID }} - CANDIDATE_ARTIFACT_ID: ${{ env.DISPATCH_CANDIDATE_ARTIFACT_ID }} - EVENT_ACTION: ${{ github.event.action || '' }} + EXPECTED_SHA: ${{ inputs.expected-sha }} run: | node - <<'NODE' - const fs = require("node:fs"); const { validateReleaseDispatch } = require( "./trusted-dispatch/.github/scripts/release-dispatch-guard.cjs", ); - const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); const failure = validateReleaseDispatch({ eventName: process.env.GITHUB_EVENT_NAME, - eventAction: process.env.EVENT_ACTION, ref: process.env.GITHUB_REF, expectedSha: process.env.EXPECTED_SHA, actualSha: process.env.GITHUB_SHA, - tag: process.env.DISPATCH_TAG, - dryRun: process.env.DISPATCH_DRY_RUN, - candidateRunId: process.env.CANDIDATE_RUN_ID, - candidateArtifactId: process.env.CANDIDATE_ARTIFACT_ID, - clientPayload: event.client_payload, }); if (failure) { @@ -114,48 +82,11 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: - ref: ${{ env.DISPATCH_EXPECTED_SHA }} fetch-depth: 0 - persist-credentials: false - - - name: Verify and download immutable release candidate - if: ${{ env.DISPATCH_CANDIDATE_RUN_ID != '' }} - env: - GH_TOKEN: ${{ github.token }} - EXPECTED_SHA: ${{ env.DISPATCH_EXPECTED_SHA }} - DISPATCH_TAG: ${{ env.DISPATCH_TAG }} - CANDIDATE_RUN_ID: ${{ env.DISPATCH_CANDIDATE_RUN_ID }} - CANDIDATE_ARTIFACT_ID: ${{ env.DISPATCH_CANDIDATE_ARTIFACT_ID }} - shell: bash - run: | - set -euo pipefail - run_json="$(gh api "/repos/${GITHUB_REPOSITORY}/actions/runs/${CANDIDATE_RUN_ID}")" - jq -e --arg repo "$GITHUB_REPOSITORY" --arg sha "$EXPECTED_SHA" --arg release_event "$GITHUB_EVENT_NAME" --arg release_tag "$DISPATCH_TAG" \ - '(.repository.full_name == $repo) and (.head_sha == $sha) and (.status == "completed") and (.conclusion == "success") and (.name == "Build release candidate") and (.path == ".github/workflows/release-candidate.yml") and ((.event == "workflow_run") or (.event == "workflow_dispatch")) and (($release_event != "repository_dispatch") or ((.event == "workflow_run") and (.head_branch == "main"))) and (($release_tag != "latest") or (.head_branch == "main"))' \ - <<<"$run_json" >/dev/null || { echo "::error::candidate run is not a successful Build release candidate for the expected repository/SHA"; exit 1; } - repo_id="$(jq -r '.repository.id' <<<"$run_json")" - run_attempt="$(jq -r '.run_attempt' <<<"$run_json")" - test "$run_attempt" -ge 1 || { echo "::error::candidate run attempt is invalid"; exit 1; } - echo "CANDIDATE_RUN_ATTEMPT=$run_attempt" >> "$GITHUB_ENV" - artifact_json="$(gh api "/repos/${GITHUB_REPOSITORY}/actions/artifacts/${CANDIDATE_ARTIFACT_ID}")" - expected_name="release-candidate-${EXPECTED_SHA}" - jq -e --arg repo_id "$repo_id" --arg sha "$EXPECTED_SHA" --arg name "$expected_name" --arg run "$CANDIDATE_RUN_ID" \ - '(.name == $name) and (.expired == false) and ((.workflow_run.id|tostring) == $run) and ((.workflow_run.repository_id|tostring) == $repo_id) and (.workflow_run.head_sha == $sha)' \ - <<<"$artifact_json" >/dev/null || { echo "::error::candidate artifact is not the unexpired expected artifact belonging to the verified run"; exit 1; } - - - name: Download immutable release candidate - if: ${{ env.DISPATCH_CANDIDATE_RUN_ID != '' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ env.DISPATCH_CANDIDATE_ARTIFACT_ID }} - github-token: ${{ github.token }} - repository: ${{ github.repository }} - run-id: ${{ env.DISPATCH_CANDIDATE_RUN_ID }} - path: release-candidate - name: Verify dispatched SHA env: - EXPECTED_SHA: ${{ env.DISPATCH_EXPECTED_SHA }} + EXPECTED_SHA: ${{ inputs.expected-sha }} run: | if [ -z "$EXPECTED_SHA" ]; then echo "::error::expected-sha is required; refusing to publish without an audited commit" @@ -169,33 +100,6 @@ jobs: - name: Setup project Bun uses: ./.github/actions/setup-project-bun - - name: Verify candidate manifest, package bytes, and inputs - id: candidate-package - if: ${{ env.DISPATCH_CANDIDATE_RUN_ID != '' }} - env: - EXPECTED_SHA: ${{ env.DISPATCH_EXPECTED_SHA }} - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} - CANDIDATE_RUN_ID: ${{ env.DISPATCH_CANDIDATE_RUN_ID }} - CANDIDATE_RUN_ATTEMPT: ${{ env.CANDIDATE_RUN_ATTEMPT }} - shell: bash - run: | - set -euo pipefail - manifest="release-candidate/release-candidate.json" - package_file="$(find release-candidate -maxdepth 1 -type f -name '*.tgz' -print)" - test -f "$manifest" && test "$(printf '%s\n' "$package_file" | sed '/^$/d' | wc -l | tr -d ' ')" = 1 || { echo "::error::candidate must contain one manifest and one tarball"; exit 1; } - tree_sha="$(git rev-parse 'HEAD^{tree}')" - bun scripts/release-candidate.ts verify "$manifest" "$package_file" \ - --repository "$GITHUB_REPOSITORY" --sha "$EXPECTED_SHA" --tree "$tree_sha" --input-root "$GITHUB_WORKSPACE" - node -e ' - const m=require("./release-candidate/release-candidate.json"); - const [name,version]=[m.package.name,m.package.version]; - if (version !== process.env.RELEASE_VERSION) throw new Error(`candidate version ${version} != ${process.env.RELEASE_VERSION}`); - if (m.builder.workflow !== "Build release candidate" || m.builder.runId !== process.env.CANDIDATE_RUN_ID || String(m.builder.runAttempt) !== process.env.CANDIDATE_RUN_ATTEMPT) throw new Error("candidate builder provenance mismatch"); - if (!/^@[a-z0-9._~-]+\/[a-z0-9._~-]+$/.test(name)) throw new Error(`unexpected package name ${name}`); - if (require("./package.json").name !== name) throw new Error("candidate package name differs from checked-out package.json"); - ' - echo "path=$package_file" >> "$GITHUB_OUTPUT" - # node + npm perform the actual publish. registry-url points npm at the public registry. - name: Setup Node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -227,32 +131,22 @@ jobs: - name: Verify version matches package.json env: - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} + RELEASE_VERSION: ${{ inputs.version }} run: | PKG=$(node -p "require('./package.json').version") echo "package.json=$PKG input=${RELEASE_VERSION}" - if [ -n "$DISPATCH_CANDIDATE_RUN_ID" ]; then - test "$PKG" = "$RELEASE_VERSION" || { - echo "::error::package.json ($PKG) != requested (${RELEASE_VERSION}) — build a candidate from a commit carrying the exact version"; - exit 1; - } - elif [ "$GITHUB_REF" = "refs/heads/dev" ] && [ "$PKG" != "$RELEASE_VERSION" ]; then - echo "Setting package.json version to $RELEASE_VERSION for transitional dev publish" - npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version - else - test "$PKG" = "$RELEASE_VERSION" || { - echo "::error::package.json ($PKG) != requested (${RELEASE_VERSION}) — bump package.json first"; - exit 1; - } - fi + test "$PKG" = "$RELEASE_VERSION" || { + echo "::error::package.json ($PKG) != requested (${RELEASE_VERSION}) — bump package.json on main first"; + exit 1; + } # The exact-SHA CI gate includes the hosted Linux, Windows, and macOS # keyring smoke matrix. Do not duplicate its Linux bootstrap here. - name: Require successful Cross-platform CI for this commit env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} - NPM_DIST_TAG: ${{ env.DISPATCH_TAG }} + RELEASE_VERSION: ${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} run: | set -euo pipefail @@ -271,15 +165,8 @@ jobs: exit 1 fi ;; - refs/heads/dev) - expected_tag="dev" - if [[ "$RELEASE_VERSION" != *-dev.* ]]; then - echo "::error::dev releases must use a dev prerelease version; got ${RELEASE_VERSION}" - exit 1 - fi - ;; *) - echo "::error::Release must run from main, preview, or dev; got ${GITHUB_REF}" + echo "::error::Release must run from main or preview; got ${GITHUB_REF}" exit 1 ;; esac @@ -352,16 +239,26 @@ jobs: echo "Service lifecycle passed for ${GITHUB_SHA}: ${service_url}" fi - # Tokenless publish via Trusted Publishing (OIDC) — NO NPM_TOKEN secret. Publish the verified - # candidate tarball directly; npm must not repack or execute lifecycle scripts here. + - name: Require dev to be ready for this release + env: + RELEASE_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + git fetch --force --tags origin +refs/heads/dev:refs/remotes/origin/dev + dev_version="$(git show origin/dev:package.json | bun -e 'console.log(JSON.parse(await Bun.stdin.text()).version)')" + bun scripts/version-line.ts assert-ahead "$dev_version" "$RELEASE_VERSION" + + # Tokenless publish via Trusted Publishing (OIDC) — NO NPM_TOKEN secret. npm auto-detects the + # OIDC environment (`id-token: write` above) and generates provenance automatically, so neither a + # token nor `--provenance` is needed. `npm publish` runs prepublishOnly first (typecheck + build + # the GUI into gui/dist), so even a dry-run fully verifies the build. # PREREQUISITE: configure the Trusted Publisher for this repo + workflow on npmjs.com — possible # only AFTER the package's first version exists (do the first publish locally, see the runbook). - name: Preflight release metadata - id: release-metadata env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} - DRY_RUN: ${{ env.DISPATCH_DRY_RUN }} + RELEASE_VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry-run }} run: | set -euo pipefail @@ -372,62 +269,56 @@ jobs: git fetch --force --tags origin existing_tag_sha="$(git rev-parse -q --verify "refs/tags/${release_tag}^{commit}" || true)" - release_exists=false - gh release view "$release_tag" >/dev/null 2>&1 && release_exists=true - - npm_exists=false - npm_git_head="" - if npm_metadata="$(npm view "${pkg_name}@${RELEASE_VERSION}" version gitHead --json 2>/dev/null)"; then - npm_exists=true - npm_git_head="$(jq -r '.gitHead // empty' <<<"$npm_metadata")" + if [ -n "$existing_tag_sha" ] && [ "$existing_tag_sha" != "$GITHUB_SHA" ]; then + echo "::error::${release_tag} already points at ${existing_tag_sha}, not ${GITHUB_SHA}" + exit 1 fi - decision="$( - EXPECTED_SHA="$GITHUB_SHA" \ - NPM_EXISTS="$npm_exists" \ - NPM_GIT_HEAD="$npm_git_head" \ - TAG_SHA="$existing_tag_sha" \ - RELEASE_EXISTS="$release_exists" \ - RELEASE_DRY_RUN="$dry_run" \ - node - <<'NODE' - const { decideReleasePostpublish } = require("./.github/scripts/release-postpublish.cjs"); - const result = decideReleasePostpublish({ - expectedSha: process.env.EXPECTED_SHA, - npmExists: process.env.NPM_EXISTS === "true", - npmGitHead: process.env.NPM_GIT_HEAD, - tagSha: process.env.TAG_SHA, - releaseExists: process.env.RELEASE_EXISTS === "true", - dryRun: process.env.RELEASE_DRY_RUN === "true", - }); - process.stdout.write(JSON.stringify(result)); - NODE - )" - action="$(jq -r .action <<<"$decision")" - echo "publish-needed=$(jq -r .publish <<<"$decision")" >> "$GITHUB_OUTPUT" - echo "tag-needed=$(jq -r .createTag <<<"$decision")" >> "$GITHUB_OUTPUT" - echo "release-needed=$(jq -r .createRelease <<<"$decision")" >> "$GITHUB_OUTPUT" - echo "::notice::Release metadata disposition: ${action}" + if [ -n "$existing_tag_sha" ]; then + if [ "$dry_run" = "true" ]; then + echo "::notice::${release_tag} already exists at this commit; dry-run only" + else + echo "::error::${release_tag} already exists. Refusing to publish a version with pre-existing Git metadata." + exit 1 + fi + fi + + if gh release view "$release_tag" >/dev/null 2>&1; then + if [ "$dry_run" = "true" ]; then + echo "::notice::GitHub Release ${release_tag} already exists; dry-run only" + else + echo "::error::GitHub Release ${release_tag} already exists. Choose the next unused patch version." + exit 1 + fi + fi + + if npm view "${pkg_name}@${RELEASE_VERSION}" version >/dev/null 2>&1; then + if [ "$dry_run" = "true" ]; then + echo "::notice::${pkg_name}@${RELEASE_VERSION} already exists on npm; dry-run only" + else + echo "::error::${pkg_name}@${RELEASE_VERSION} already exists on npm. Choose the next unused patch version." + exit 1 + fi + fi - name: Refuse a release the current tag set already outranks env: - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} + RELEASE_VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry-run }} run: | set -euo pipefail - allow=() + allow="" existing_tag_sha="$(git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}^{commit}" || true)" - # release-postpublish.cjs already proved that any existing public state - # belongs to this exact source commit. Permit an exact-tag resume while - # still rejecting a candidate outranked by any other release tag. - if [ -n "$existing_tag_sha" ] && [ "$existing_tag_sha" = "$GITHUB_SHA" ]; then - allow=(--allow-existing-tag "v${RELEASE_VERSION}") + if [ "$DRY_RUN" = "true" ] && [ -n "$existing_tag_sha" ] && [ "$existing_tag_sha" = "$GITHUB_SHA" ]; then + allow="--allow-existing-tag-at-head" fi - git tag --list 'v*' | bun scripts/version-line.ts assert-releasable "$RELEASE_VERSION" "${allow[@]}" + git tag --list 'v*' | bun scripts/version-line.ts assert-releasable "$RELEASE_VERSION" $allow - name: Build and validate release changelog env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} - NPM_DIST_TAG: ${{ env.DISPATCH_TAG }} + RELEASE_VERSION: ${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} run: | set -euo pipefail notes_file="$GITHUB_WORKSPACE/.release-notes.md" @@ -443,62 +334,60 @@ jobs: } - name: Publish (or dry-run) + id: publication env: - DRY_RUN: ${{ env.DISPATCH_DRY_RUN }} - NPM_DIST_TAG: ${{ env.DISPATCH_TAG }} - CANDIDATE_PACKAGE_PATH: ${{ steps.candidate-package.outputs.path }} - PUBLISH_NEEDED: ${{ steps.release-metadata.outputs.publish-needed }} + DRY_RUN: ${{ inputs.dry-run }} + NPM_DIST_TAG: ${{ inputs.tag }} run: | set -euo pipefail - package_file="" - if [ -n "$DISPATCH_CANDIDATE_RUN_ID" ]; then - package_file="./${CANDIDATE_PACKAGE_PATH#./}" - test -f "$package_file" || { echo "::error::verified candidate package path is unavailable"; exit 1; } - fi - if [ -z "$DISPATCH_CANDIDATE_RUN_ID" ] && [ "$DRY_RUN" = "true" ]; then - echo "::notice::TRANSITIONAL DRY RUN — building locally; automatic main releases always consume an immutable candidate" + if [ "$DRY_RUN" = "true" ]; then + echo "::notice::DRY RUN — building + packing, not publishing" npm run prepublishOnly npm pack --dry-run - elif [ "$PUBLISH_NEEDED" != "true" ]; then - echo "::notice::Exact npm version is already published from ${GITHUB_SHA}; resuming post-publish metadata only" - elif [ -z "$DISPATCH_CANDIDATE_RUN_ID" ]; then - echo "::warning::transitional manual release is building locally; migrate this caller to candidate IDs" - npm publish --tag "$NPM_DIST_TAG" --access public - elif [ "$DRY_RUN" = "true" ]; then - echo "::notice::DRY RUN — validating exact candidate tarball, not publishing" - npm publish "$package_file" --tag "$NPM_DIST_TAG" --access public --ignore-scripts --dry-run else - npm publish "$package_file" --tag "$NPM_DIST_TAG" --access public --ignore-scripts + npm publish --tag "$NPM_DIST_TAG" --access public + echo "published=true" >> "$GITHUB_OUTPUT" fi - # Confirm the registry actually has the new version (real publishes only). + # Publication is acknowledged before registry reads, which can lag or fail. + # Recover only observation failures in this run; never retry npm publish. - name: Post-publish registry smoke - if: ${{ env.DISPATCH_DRY_RUN != 'true' }} + id: registry-smoke + if: ${{ inputs.dry-run != true && steps.publication.outputs.published == 'true' }} env: - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} + RELEASE_VERSION: ${{ inputs.version }} + PUBLISHED: ${{ steps.publication.outputs.published }} run: | + set -euo pipefail + test "$PUBLISHED" = "true" || { + echo "::error::No successful publication receipt; refusing registry recovery" + exit 1 + } pkg_name="$(node -p "require('./package.json').name")" - for attempt in $(seq 1 30); do - if VERSION=$(npm view "${pkg_name}@${RELEASE_VERSION}" version 2>/dev/null); then + for attempt in $(seq 1 6); do + if VERSION=$(timeout --kill-after=2s 10s npm view "${pkg_name}@${RELEASE_VERSION}" version --fetch-retries=0 --fetch-timeout=8000 2>/dev/null); then + if [ "$VERSION" != "$RELEASE_VERSION" ]; then + echo "::error::Registry returned an unexpected version; refusing to create a release" + exit 1 + fi echo "registry version=$VERSION" - test "$VERSION" = "$RELEASE_VERSION" - npm dist-tag ls "$pkg_name" + echo "verification=verified" >> "$GITHUB_OUTPUT" + echo "Registry verified ${pkg_name}@${RELEASE_VERSION}." >> "$GITHUB_STEP_SUMMARY" + timeout --kill-after=2s 10s npm dist-tag ls "$pkg_name" --fetch-retries=0 --fetch-timeout=8000 || echo "::warning::Could not read npm dist-tags; exact version was verified" exit 0 fi - echo "::notice::${pkg_name}@${RELEASE_VERSION} not visible in npm registry yet (attempt $attempt/30)" - sleep 10 + echo "::notice::Registry lookup not confirmed (attempt $attempt/6)" + if [ "$attempt" -lt 6 ]; then sleep 5; fi done - echo "::error::npm registry smoke failed after 30 attempts" - npm view "$pkg_name" versions dist-tags --json || true - exit 1 + echo "verification=pending" >> "$GITHUB_OUTPUT" + echo "::warning::npm publish succeeded, but registry verification remains pending; continuing GitHub release creation without republishing" + echo "Publication acknowledged for ${pkg_name}@${RELEASE_VERSION}; registry verification pending after bounded reads. Inspect the registry before announcing availability. Do not republish this version." >> "$GITHUB_STEP_SUMMARY" - name: Create GitHub release - if: ${{ env.DISPATCH_DRY_RUN != 'true' }} + if: ${{ inputs.dry-run != true && steps.publication.outputs.published == 'true' }} env: GH_TOKEN: ${{ github.token }} - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} - TAG_NEEDED: ${{ steps.release-metadata.outputs.tag-needed }} - RELEASE_NEEDED: ${{ steps.release-metadata.outputs.release-needed }} + RELEASE_VERSION: ${{ inputs.version }} run: | set -euo pipefail @@ -518,33 +407,14 @@ jobs: fi prerelease_flag="" - if [[ "$RELEASE_VERSION" == *-preview.* || "$RELEASE_VERSION" == *-dev.* ]]; then + if [[ "$RELEASE_VERSION" == *-preview.* ]]; then prerelease_flag="--prerelease" fi - if [ "$TAG_NEEDED" = "true" ]; then - # Checkout deliberately leaves no credential in .git/config. Create - # the lightweight tag through the authenticated API instead of an - # unauthenticated git push, then verify the public ref before release. - if ! gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ - -f ref="refs/tags/${release_tag}" -f sha="$GITHUB_SHA" >/dev/null; then - echo "::notice::Tag creation raced another writer; verifying the resulting ref" - fi + if [ -z "$existing_tag_sha" ]; then + git tag "$release_tag" "$GITHUB_SHA" + git push origin "refs/tags/${release_tag}" fi - git fetch --force --tags origin - published_tag_sha="$(git rev-parse -q --verify "refs/tags/${release_tag}^{commit}" || true)" - if [ "$published_tag_sha" != "$GITHUB_SHA" ]; then - echo "::error::${release_tag} resolved to ${published_tag_sha:-nothing}, not ${GITHUB_SHA}" - exit 1 - fi - - if [ "$RELEASE_NEEDED" = "true" ]; then - if ! gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \ - --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}; then - echo "::notice::Release creation raced another writer; verifying the resulting release" - gh release view "$release_tag" --json tagName,targetCommitish >/dev/null - fi - else - echo "::notice::GitHub Release ${release_tag} already exists at the verified tag" - fi + gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \ + --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag} diff --git a/CREDITS.md b/CREDITS.md index 14bd5c84d5..16fa2553d8 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -23,8 +23,8 @@ invalidating every tag and clone — direction: authorship credit in git history is not rewritten. This file is the forward repair. -Every entry cites the maintainer's own words from the closing comment or the -landing commit. Nothing here is inferred from a diff. +Every entry cites the maintainer's own words from the closing comment, pull-request +description, or landing commit. Nothing here is inferred from a diff. This file is **not** a contributor list. Most contributions merged normally, with authorship intact, and need no entry. Absence from this page means the @@ -57,6 +57,104 @@ Code, design, or tests from these pull requests shipped. | [#3300](https://github.com/lidge-jun/opencodex/pull/3300) | [@S0RYUASUKA](https://github.com/S0RYUASUKA) | `15b43e51c` | the same two test files made hermetic | | [#3284](https://github.com/lidge-jun/opencodex/pull/3284) | [@mdwsk88](https://github.com/mdwsk88) | `3d3c4fe26` | "Core implementation is already on `dev` via #3286 (`3d3c4fe26`), including the suffix wire ladder, picker collapse, Google adapter coverage" | +### 2026-09-07 follow-up: missing or malformed trailers + +These additional landings are present in the audited 3,000-commit window. +The linked landing descriptions or commit messages identify what was taken. + +| Pull request | Author | Landed as | What landed | +| --- | --- | --- | --- | +| [#1748](https://github.com/lidge-jun/opencodex/pull/1748) | [@Blushyes](https://github.com/Blushyes) | [`e3bbf5321`](https://github.com/lidge-jun/opencodex/commit/e3bbf5321c6c0483e9662466e044545bb0e086ba) | [Scoped reimplementation of the outbound Fake-IP discovery fix; the source PR author is @Blushyes, correcting the name in the landing message.](https://github.com/lidge-jun/opencodex/commit/e3bbf5321c6c0483e9662466e044545bb0e086ba) | +| [#1842](https://github.com/lidge-jun/opencodex/pull/1842) | [@luvs01](https://github.com/luvs01) | [`e1e431332`](https://github.com/lidge-jun/opencodex/commit/e1e43133281cba5f952dfa3226a4d55d505365b5) | [Public OAuth error projection and preservation of typed authentication failures.](https://github.com/lidge-jun/opencodex/pull/2043) | +| [#1889](https://github.com/lidge-jun/opencodex/pull/1889) | [@dbc-hbin](https://github.com/dbc-hbin) | [`ea16f8613`](https://github.com/lidge-jun/opencodex/commit/ea16f86130291042486ba3c10640e73b63772d27) | [The remaining x-goog-api-client removal and the contributor's header assertions.](https://github.com/lidge-jun/opencodex/pull/2018) | +| [#1896](https://github.com/lidge-jun/opencodex/pull/1896) | [@luvyoun0224-beep](https://github.com/luvyoun0224-beep) | [`5f2b93979`](https://github.com/lidge-jun/opencodex/commit/5f2b93979e4eae78e1a8c66d1f4a324c4f394084) | [The functions-namespace parser flattening; the hardcoded-name guidance was not taken.](https://github.com/lidge-jun/opencodex/pull/2020) | +| [#1899](https://github.com/lidge-jun/opencodex/pull/1899) | [@ntdatt812](https://github.com/ntdatt812) | [`fb5ceee35`](https://github.com/lidge-jun/opencodex/commit/fb5ceee35f18925a118615b3eb69dc0093f27730) | [The catalog-writer temp-path binding, extended with ordered write/harden/publish assertions.](https://github.com/lidge-jun/opencodex/pull/1923) | +| [#1920](https://github.com/lidge-jun/opencodex/pull/1920) | [@Yuxin-Qiao](https://github.com/Yuxin-Qiao) | [`34b167367`](https://github.com/lidge-jun/opencodex/commit/34b167367c09a3a2445261b845ed10a3c0664693) | [Scoped native and replay Computer Use tool-result normalization.](https://github.com/lidge-jun/opencodex/pull/2038) | +| [#1932](https://github.com/lidge-jun/opencodex/pull/1932) | [@harryzhou2000](https://github.com/harryzhou2000) | [`f2b507f83`](https://github.com/lidge-jun/opencodex/commit/f2b507f831e8065d4fac2fd9fe44bf8106b3fa0e) | [The transient bare-401 concept and test scaffolding; the literal trailer "PR #1932" identifies no account.](https://github.com/lidge-jun/opencodex/pull/2021) | +| [#2027](https://github.com/lidge-jun/opencodex/pull/2027) | [@yzxcj797](https://github.com/yzxcj797) | [`5445ce3e6`](https://github.com/lidge-jun/opencodex/commit/5445ce3e626849855521e5ff54571a1c3babc7e2), [`293494cfa`](https://github.com/lidge-jun/opencodex/commit/293494cfa0c89542320a14bb3af6e4a3f31935a2) | ["Absorbed from #2027" — OpenCode Go identity follows its destination.](https://github.com/lidge-jun/opencodex/commit/5445ce3e626849855521e5ff54571a1c3babc7e2) | +| [#2040](https://github.com/lidge-jun/opencodex/pull/2040) | [@Ingwannu](https://github.com/Ingwannu) | [`16345ab8b`](https://github.com/lidge-jun/opencodex/commit/16345ab8b0a9e5d6fdc8a237786339b674fc374c) | ["implementation and tests, with two corrections".](https://github.com/lidge-jun/opencodex/commit/16345ab8b0a9e5d6fdc8a237786339b674fc374c) | +| [#2053](https://github.com/lidge-jun/opencodex/pull/2053) | [@Ingwannu](https://github.com/Ingwannu) | [`f4ad13922`](https://github.com/lidge-jun/opencodex/commit/f4ad1392271370e1c7c08b0b11ddea346d065138) | ["Carries @Ingwannu's #2053 unchanged."](https://github.com/lidge-jun/opencodex/commit/f4ad1392271370e1c7c08b0b11ddea346d065138) | +| [#2056](https://github.com/lidge-jun/opencodex/pull/2056) | [@Ingwannu](https://github.com/Ingwannu) | [`9b0c5a02d`](https://github.com/lidge-jun/opencodex/commit/9b0c5a02d95220161fc18c72aba5756fdcbf68e3) | [The shortPercent known-quota and scoring changes.](https://github.com/lidge-jun/opencodex/commit/9b0c5a02d95220161fc18c72aba5756fdcbf68e3) | +| [#2075](https://github.com/lidge-jun/opencodex/pull/2075) | [@olddonkey](https://github.com/olddonkey) | [`647b98eb8`](https://github.com/lidge-jun/opencodex/commit/647b98eb8ae4d4ca3c16e1d515dc17a97e5993e4) | [The native Chat fast-capability gate, reconciled with the exact-ID contract.](https://github.com/lidge-jun/opencodex/commit/647b98eb8ae4d4ca3c16e1d515dc17a97e5993e4) | +| [#2077](https://github.com/lidge-jun/opencodex/pull/2077) | [@ntdatt812](https://github.com/ntdatt812) | [`f9c224b70`](https://github.com/lidge-jun/opencodex/commit/f9c224b70abcb4237d166d93fd10677791719b51) | ["Both patches are @ntdatt812's work from #2100 and #2077, applied unchanged."](https://github.com/lidge-jun/opencodex/commit/f9c224b70abcb4237d166d93fd10677791719b51) | +| [#2082](https://github.com/lidge-jun/opencodex/pull/2082) | [@yzxcj797](https://github.com/yzxcj797) | [`06cdbc109`](https://github.com/lidge-jun/opencodex/commit/06cdbc109a631203618a2d1bcf2f395f121af8fa), [`057e8575c`](https://github.com/lidge-jun/opencodex/commit/057e8575c3138f4e90d31e23feccbad60511a2fe) | [AgentRouter opening-turn framing, with exact hostname and separate-block corrections.](https://github.com/lidge-jun/opencodex/commit/06cdbc109a631203618a2d1bcf2f395f121af8fa) | +| [#2099](https://github.com/lidge-jun/opencodex/pull/2099) | [@yzxcj797](https://github.com/yzxcj797) | [`81492fd10`](https://github.com/lidge-jun/opencodex/commit/81492fd10f428d7d638550de4ba2ebcdf13d7715) | [The repro-shaped prompt-cache-retention test fixture; the runtime contract came from #2102.](https://github.com/lidge-jun/opencodex/pull/2138) | +| [#2100](https://github.com/lidge-jun/opencodex/pull/2100) | [@ntdatt812](https://github.com/ntdatt812) | [`f9c224b70`](https://github.com/lidge-jun/opencodex/commit/f9c224b70abcb4237d166d93fd10677791719b51) | ["Both patches are @ntdatt812's work from #2100 and #2077, applied unchanged."](https://github.com/lidge-jun/opencodex/commit/f9c224b70abcb4237d166d93fd10677791719b51) | +| [#2101](https://github.com/lidge-jun/opencodex/pull/2101) | [@Ingwannu](https://github.com/Ingwannu) | [`0bce9516d`](https://github.com/lidge-jun/opencodex/commit/0bce9516d8d987d5b209d1c92cce24d278941b55) | [The entitlement module and full test suite, with maintainer corrections.](https://github.com/lidge-jun/opencodex/pull/2146) | +| [#2102](https://github.com/lidge-jun/opencodex/pull/2102) | [@lilinxiong](https://github.com/lilinxiong) | [`5904178c3`](https://github.com/lidge-jun/opencodex/commit/5904178c349c555704b5f461ef38a47a47324074) | [The prompt-cache-retention implementation, narrowed to the exact native model family.](https://github.com/lidge-jun/opencodex/pull/2138) | +| [#2104](https://github.com/lidge-jun/opencodex/pull/2104) | [@olddonkey](https://github.com/olddonkey) | [`7cd270dc2`](https://github.com/lidge-jun/opencodex/commit/7cd270dc2cd4f5a4cb07b2f23b4eb0dfe869b0da) | ["Carries @olddonkey's #2104 unchanged."](https://github.com/lidge-jun/opencodex/commit/7cd270dc2cd4f5a4cb07b2f23b4eb0dfe869b0da) | +| [#2105](https://github.com/lidge-jun/opencodex/pull/2105) | [@lilinxiong](https://github.com/lilinxiong) | [`a0635eaa2`](https://github.com/lidge-jun/opencodex/commit/a0635eaa2ec16344e25f77f41916637f9cddc4c3) | ["Carries @lilinxiong's #2105 implementation and tests."](https://github.com/lidge-jun/opencodex/commit/a0635eaa2ec16344e25f77f41916637f9cddc4c3) | +| [#2109](https://github.com/lidge-jun/opencodex/pull/2109) | [@drakonkat](https://github.com/drakonkat) | [`d2493a147`](https://github.com/lidge-jun/opencodex/commit/d2493a147d5286f54be34735a13f5d13f8f19597) | [The shared base-URL override implementation from #2109 and #2110.](https://github.com/lidge-jun/opencodex/pull/2148) | +| [#2110](https://github.com/lidge-jun/opencodex/pull/2110) | [@drakonkat](https://github.com/drakonkat) | [`d2493a147`](https://github.com/lidge-jun/opencodex/commit/d2493a147d5286f54be34735a13f5d13f8f19597) | [The shared base-URL override implementation from #2109 and #2110.](https://github.com/lidge-jun/opencodex/pull/2148) | +| [#2122](https://github.com/lidge-jun/opencodex/pull/2122) | [@chilung-cgu](https://github.com/chilung-cgu) | [`6a6efa928`](https://github.com/lidge-jun/opencodex/commit/6a6efa928a165726c0fd17d893d11c21176dc812) | [Configuration union, schema and migration design for retainModels; #2860 supplied the retention predicate.](https://github.com/lidge-jun/opencodex/pull/3206) | +| [#2127](https://github.com/lidge-jun/opencodex/pull/2127) | [@agentHits](https://github.com/agentHits) | [`1adcfde0c`](https://github.com/lidge-jun/opencodex/commit/1adcfde0cfff10589eefdaa33aae077ffe404bcc) | ["Carries @agentHits's #2127 unchanged."](https://github.com/lidge-jun/opencodex/commit/1adcfde0cfff10589eefdaa33aae077ffe404bcc) | +| [#2131](https://github.com/lidge-jun/opencodex/pull/2131) | [@bet4it](https://github.com/bet4it) | [`8ff77e11e`](https://github.com/lidge-jun/opencodex/commit/8ff77e11ebe7bc6472164d29c89c779986b9469a) | ["Carries @bet4it's #2131 implementation and tests."](https://github.com/lidge-jun/opencodex/commit/8ff77e11ebe7bc6472164d29c89c779986b9469a) | +| [#2155](https://github.com/lidge-jun/opencodex/pull/2155) | [@waw4303](https://github.com/waw4303) | [`64ba54edb`](https://github.com/lidge-jun/opencodex/commit/64ba54edbe5ac90b1127f51d5890e33da183c3f0), [`772d375fe`](https://github.com/lidge-jun/opencodex/commit/772d375fed252f838c41b9315cf88046f64d192a) | [Resolved tool-call padding handling, with per-field provenance and diagnostic corrections.](https://github.com/lidge-jun/opencodex/commit/64ba54edbe5ac90b1127f51d5890e33da183c3f0) | +| [#2227](https://github.com/lidge-jun/opencodex/pull/2227) | [@olddonkey](https://github.com/olddonkey) | [`63d387cae`](https://github.com/lidge-jun/opencodex/commit/63d387cae369e3e72d4d0a7702db9d98e607d785) | [The Grok OAuth registry flip, structure documentation and test conversions.](https://github.com/lidge-jun/opencodex/pull/2255) | +| [#2432](https://github.com/lidge-jun/opencodex/pull/2432) | [@mdwsk88](https://github.com/mdwsk88) | [`850afb2e9`](https://github.com/lidge-jun/opencodex/commit/850afb2e9f84979c87e914b248de482f44b34cd6) | [The omit-sentinel documentation and provider wire comments.](https://github.com/lidge-jun/opencodex/pull/3603) | +| [#2639](https://github.com/lidge-jun/opencodex/pull/2639) | [@bet4it](https://github.com/bet4it) | [`fefeb0501`](https://github.com/lidge-jun/opencodex/commit/fefeb05016d21dc9a3b8afe1b52427e4e1d8a0ed) | [The Responses item status backfill; queued/in_progress mapping was corrected separately.](https://github.com/lidge-jun/opencodex/pull/2721) | +| [#2647](https://github.com/lidge-jun/opencodex/pull/2647) | [@darwintree](https://github.com/darwintree) | [`e1e6ec04f`](https://github.com/lidge-jun/opencodex/commit/e1e6ec04f43a287b4cfb5149893d2c6c0a520588) | [Command Code profile metadata, reconciled with the intervening catalog update.](https://github.com/lidge-jun/opencodex/pull/2721) | +| [#2663](https://github.com/lidge-jun/opencodex/pull/2663) | [@Eleven-is-cool](https://github.com/Eleven-is-cool) | [`cb9bb9b76`](https://github.com/lidge-jun/opencodex/commit/cb9bb9b7634640f18568207322d386a059f6c9ac) | [Bare code-mode helper calls through exec: "the implementation is yours, unchanged".](https://github.com/lidge-jun/opencodex/pull/2724) | +| [#2938](https://github.com/lidge-jun/opencodex/pull/2938) | [@luvs01](https://github.com/luvs01) | [`8427efe6e`](https://github.com/lidge-jun/opencodex/commit/8427efe6e80a5ce9488eab7b80b2b1663ab20579) | [The failed-wrapper diagnosis and linear-scan design, with six behavioral divergences corrected.](https://github.com/lidge-jun/opencodex/pull/2945) | +| [#3069](https://github.com/lidge-jun/opencodex/pull/3069) | [@justin-mc-lai](https://github.com/justin-mc-lai) | [`a0d386b49`](https://github.com/lidge-jun/opencodex/commit/a0d386b49074ec81df5646fcc20a5e4979c67878) | [The two query/queries parity commits, followed by malformed-history boundary fixes.](https://github.com/lidge-jun/opencodex/pull/3089) | +| [#3329](https://github.com/lidge-jun/opencodex/pull/3329) | [@Veritas-7](https://github.com/Veritas-7) | [`3ac310782`](https://github.com/lidge-jun/opencodex/commit/3ac31078244ea04c9abce0e50275ffaccf25455a) | [Combo cooldown/wait design with corrected reset metadata, clocks and Retry-After handling.](https://github.com/lidge-jun/opencodex/pull/3606) | +| [#3407](https://github.com/lidge-jun/opencodex/pull/3407) | [@turin-dev](https://github.com/turin-dev) | [`3b3fe21d4`](https://github.com/lidge-jun/opencodex/commit/3b3fe21d45e57761e9769020da4b37de5cd95726) | [The desired-state Codex switch, observed-state badge and effective-home path.](https://github.com/lidge-jun/opencodex/pull/3617) | +| [#3421](https://github.com/lidge-jun/opencodex/pull/3421) | [@Skyline-23](https://github.com/Skyline-23) | [`89c0a64fe`](https://github.com/lidge-jun/opencodex/commit/89c0a64fe2c59af1814230b0c85d61cd08672bd5) | [The Compose/container foundation, with loopback binding and generated compatibility identity.](https://github.com/lidge-jun/opencodex/pull/3604) | +| [#3469](https://github.com/lidge-jun/opencodex/pull/3469) | [@agentHits](https://github.com/agentHits) | [`c44e187ee`](https://github.com/lidge-jun/opencodex/commit/c44e187ee901275f977f5a2be32c782f4e1f1794) | [Google location-error classification, carried through #3547 and corrected for error precedence.](https://github.com/lidge-jun/opencodex/pull/3608) | +| [#3487](https://github.com/lidge-jun/opencodex/pull/3487) | [@Ingwannu](https://github.com/Ingwannu) | [`f8ba644f3`](https://github.com/lidge-jun/opencodex/commit/f8ba644f3ad650b14af9cc420d4d42782939bfef) | [The bounded Kiro fallback-execution assertion at the migrated test path.](https://github.com/lidge-jun/opencodex/pull/3602) | +| [#3489](https://github.com/lidge-jun/opencodex/pull/3489) | [@Flowershangfromthebranches](https://github.com/Flowershangfromthebranches) | [`55395a9dc`](https://github.com/lidge-jun/opencodex/commit/55395a9dc8a252a01f606b7b65859579e4f2e53d) | [Canonical-final-URL discovery injection alongside independently proxy-bound IPv6 handling.](https://github.com/lidge-jun/opencodex/pull/3618) | +| [#3528](https://github.com/lidge-jun/opencodex/pull/3528) | [@benedictusrey](https://github.com/benedictusrey) | [`bef04efbc`](https://github.com/lidge-jun/opencodex/commit/bef04efbcf506ac26ebd3eeba8ac397a5d8a8d0d) | [The effort CLI command, exact selectors and distinct live/offline failures.](https://github.com/lidge-jun/opencodex/pull/3612) | +| [#3531](https://github.com/lidge-jun/opencodex/pull/3531) | [@benedictusrey](https://github.com/benedictusrey) | [`45045623b`](https://github.com/lidge-jun/opencodex/commit/45045623bfc9c1ec7f8c55e47493da343b98a968) | [The agy alias, with captured discovery authority retained.](https://github.com/lidge-jun/opencodex/pull/3601) | + +### 2026-09-07 follow-up: unlinked trailers + +These commits contain a contributor name, but GitHub's commit-author mapping +does not resolve that trailer to the source PR author. No personal addresses +are reproduced here. The forward correction uses account-linked noreply identities. + +| Pull request | Author | Landed as | What landed | +| --- | --- | --- | --- | +| [#2817](https://github.com/lidge-jun/opencodex/pull/2817) | [@gulup](https://github.com/gulup) | [`6fe46312c`](https://github.com/lidge-jun/opencodex/commit/6fe46312cd509bbef0e79025181e7ab6fc285681) | [The opt-in upstream Responses WebSocket transport and six carried commits.](https://github.com/lidge-jun/opencodex/pull/3216) | +| [#3148](https://github.com/lidge-jun/opencodex/pull/3148) | [@Veritas-7](https://github.com/Veritas-7) | [`865a36ef0`](https://github.com/lidge-jun/opencodex/commit/865a36ef04eb6395e617f94ed87aaa474a903444) | [The two subscription-launch admission-key fixes, plus connected-launch reconciliation.](https://github.com/lidge-jun/opencodex/pull/3182) | +| [#3293](https://github.com/lidge-jun/opencodex/pull/3293) | [@Veritas-7](https://github.com/Veritas-7) | [`3a9c4d297`](https://github.com/lidge-jun/opencodex/commit/3a9c4d297451bc40abb24cf13d5f50648450fc2e) | [The missing claude-fable-5-1 model metadata and accompanying usage-cost test.](https://github.com/lidge-jun/opencodex/pull/3478) | + +The correction commit records these contributors and the earlier **Carried work** +authors as co-authors. This is forward attribution: the old commit objects, +their original dates and release tags are unchanged. + +### 2026-09-07 follow-up: four-track source-to-landing attribution + +At the owner's request, this audit makes the original PR titles, authors and +delivered slices explicit for the four follow-up tracks after #3771. All linked +landing commits are ancestors of `5759d9ea2f1e7281cdc01eb9628f2e0a123fb59c`. +The human contributors already resolve through reachable source commits or merge +trailers; a merge commit with no repeated trailer does not erase its parents' +authorship. This forward record strengthens discoverability without claiming that +every earlier landing omitted credit or rewriting existing commits and tags. + +| Original pull request | Original author | Landed through | Delivered scope | +| --- | --- | --- | --- | +| [#3769: fix(responses): fallback to routed compaction on 404 and enable quota failover on incomplete terminal](https://github.com/lidge-jun/opencodex/pull/3769) | [@ideabib](https://github.com/ideabib) | [#3791](https://github.com/lidge-jun/opencodex/pull/3791) (`fcf07446aa`) | Quota/incomplete attribution only; native compact 404 fallback remains outside this landing. | +| [#3736: fix: preserve compaction progress and use a 600s stall budget](https://github.com/lidge-jun/opencodex/pull/3736) | [@Hylouis233](https://github.com/Hylouis233) | [#3792](https://github.com/lidge-jun/opencodex/pull/3792) (`823ffeb771`) | Content-free buffered-compaction progress; the proposed global 600-second default was not adopted. | +| [#3744: fix(server): opt the compact route out of the request idle timeout](https://github.com/lidge-jun/opencodex/pull/3744) | [@mashfromband](https://github.com/mashfromband) | [#3792](https://github.com/lidge-jun/opencodex/pull/3792) (`823ffeb771`) | Accepted compact-request lifetime, with complete-body admission and bounded response-body inactivity. | +| [#3740: fix(responses): answer a wrapped WebSocket rejection with its HTTP status](https://github.com/lidge-jun/opencodex/pull/3740) | [@FredAmartey](https://github.com/FredAmartey) | [#3793](https://github.com/lidge-jun/opencodex/pull/3793) (`110623ecfc`) | Precommit wrapped WebSocket rejection status; mid-turn failures retain their separate boundary. | +| [#3779: fix(chat): preserve completion semantics in JSON-to-SSE fallback](https://github.com/lidge-jun/opencodex/pull/3779) | [@Ingwannu](https://github.com/Ingwannu) | [#3803](https://github.com/lidge-jun/opencodex/pull/3803) (`ac4a7659fd`) | JSON-to-stream tools, reasoning, usage and finish-reason preservation, extended across both fallback paths. | +| [#3730: feat(claude): gate routed protocol compatibility](https://github.com/lidge-jun/opencodex/pull/3730) | [@yansigit](https://github.com/yansigit) | [#3806](https://github.com/lidge-jun/opencodex/pull/3806) (`4255bfac61`), [#3808](https://github.com/lidge-jun/opencodex/pull/3808) (`5759d9ea2f`) | Opt-in translated Messages compatibility and bounded diagnostics; integrated with the final fixture layer. | +| [#3747: fix(container): persist Codex home separately from OCX state](https://github.com/lidge-jun/opencodex/pull/3747) | [@Ingwannu](https://github.com/Ingwannu) | [#3788](https://github.com/lidge-jun/opencodex/pull/3788) (`ad5285e415`) | Separate persisted Codex home under the read-only container root, with serializer and documentation corrections. | +| [#3324: docs(skill): keep access-key secrets out of agent sessions](https://github.com/lidge-jun/opencodex/pull/3324) | [@luvs01](https://github.com/luvs01) | [#3789](https://github.com/lidge-jun/opencodex/pull/3789) (`26fa36424a`) | Agent-facing secret-bearing command and rotation-recipe restrictions, including aliases and management API spellings. | +| [#3632: feat(config): add exclusive initialize-if-missing primitive](https://github.com/lidge-jun/opencodex/pull/3632) | [@yansigit](https://github.com/yansigit) | [#3796](https://github.com/lidge-jun/opencodex/pull/3796) (`443310e5dc`), [#3802](https://github.com/lidge-jun/opencodex/pull/3802) (`f89b815090`) | Exclusive initial configuration publication and its real setup consumer, with filesystem and cancellation corrections. | +| [#3728: feat(quota): show subscription credits in capacity bars](https://github.com/lidge-jun/opencodex/pull/3728) | [@yansigit](https://github.com/yansigit) | [#3798](https://github.com/lidge-jun/opencodex/pull/3798) (`b72b8ea6c8`) | Subscription-credit quota rows, including duplicate-label and displayed-row urgency handling. | +| [#3250: perf(logs): poll request history incrementally](https://github.com/lidge-jun/opencodex/pull/3250) | [@chilung-cgu](https://github.com/chilung-cgu) | [#3800](https://github.com/lidge-jun/opencodex/pull/3800) (`57211f43d4`) | Incremental request-history polling, extended to preserve changed requests and reset behavior. | +| [#3383: feat(models): add main picker ordering controls](https://github.com/lidge-jun/opencodex/pull/3383) | [@x3M3x](https://github.com/x3M3x) | [#3801](https://github.com/lidge-jun/opencodex/pull/3801) (`8615f1a1c9`) | Picker-order controls and isolated saves; unrelated source-PR Windows changes are not credited as part of this layer. | + +The source commits for @yansigit also credit Yumi. That original automation +attribution remains in the reachable history; this audit does not invent a GitHub +account mapping for its unlinked automation identity. The forward human trailers +use the source authors' verified numeric GitHub account identities. + +A delivered slice is not a statement that every requirement in its original PR +or umbrella issue is complete. The table deliberately retains the unadopted scope. + ## Report and diagnosis These fixes exist because of the report. The branch's own approach was not the @@ -73,6 +171,25 @@ code would misstate what happened in the other direction. | [#3143](https://github.com/lidge-jun/opencodex/pull/3143) | [@Ingwannu](https://github.com/Ingwannu) | `408652698` | "The diagnosis here was yours and it was right" | | [#3223](https://github.com/lidge-jun/opencodex/pull/3223) | [@alex-jordan547](https://github.com/alex-jordan547) | `d23eab43a` | "The report itself was what made the fix quick; the wire capture pointed straight at the cause" | +### Four-track reports and diagnostic evidence + +These issue authors supplied the reports or observations used by the follow-up +work. They are acknowledged as reporters, separately from the carried PR authors. + +| Report | Reporter | Follow-up | Contribution | +| --- | --- | --- | --- | +| [#3778](https://github.com/lidge-jun/opencodex/issues/3778) | [@turin-dev](https://github.com/turin-dev) | [#3786](https://github.com/lidge-jun/opencodex/pull/3786) | Non-atomic cleanup-manifest failure report. | +| [#3746](https://github.com/lidge-jun/opencodex/issues/3746) | [@juzijia](https://github.com/juzijia) | [#3788](https://github.com/lidge-jun/opencodex/pull/3788) | Read-only container Codex-home persistence failure. | +| [#3770](https://github.com/lidge-jun/opencodex/issues/3770) | [@turin-dev](https://github.com/turin-dev) | [#3803](https://github.com/lidge-jun/opencodex/pull/3803) | JSON-to-SSE completion-semantics loss. | +| [#3767](https://github.com/lidge-jun/opencodex/issues/3767) | [@turin-dev](https://github.com/turin-dev) | [#3805](https://github.com/lidge-jun/opencodex/pull/3805) | Refusal loss across Chat projections. | +| [#3775](https://github.com/lidge-jun/opencodex/issues/3775) | [@leonclab](https://github.com/leonclab) | [#3804](https://github.com/lidge-jun/opencodex/pull/3804) | Unsupported effort report; the delivered fix is limited to proven native capability aliases. | +| [#3661](https://github.com/lidge-jun/opencodex/issues/3661) | [@Hu9956](https://github.com/Hu9956) | [#3794](https://github.com/lidge-jun/opencodex/pull/3794) | Encrypted-task recovery failure classes; the landed change exposes bounded reasons. | +| [#3522](https://github.com/lidge-jun/opencodex/issues/3522) | [@stephen-drew](https://github.com/stephen-drew) | [#3790](https://github.com/lidge-jun/opencodex/pull/3790) | Same-process Windows spill failure evidence; the landed change separates timeout origins. | +| [#3781](https://github.com/lidge-jun/opencodex/issues/3781) | [@jaychou0642-create](https://github.com/jaychou0642-create) | [#3799](https://github.com/lidge-jun/opencodex/pull/3799) | Canonical-destination/Fake-IP quota-path investigation; field acceptance remains separate. | + +Diagnostic-only delivery does not establish that the reported runtime failure +has been resolved. + ## Closed as landed, carry not stated Two more were closed with a landing commit and nothing further. The landing is @@ -84,6 +201,19 @@ inaccuracy this file exists to correct. - [#2675](https://github.com/lidge-jun/opencodex/pull/2675) by [@Ingwannu](https://github.com/Ingwannu) — closed "Landed via #2677 at `8412fe156`". +Two further source PRs were closed with a landing reference but without an +explicit statement of what was carried. Their authors are acknowledged here; +they are not counted as carried code solely from that closure: + +- [#2360](https://github.com/lidge-jun/opencodex/pull/2360) by + [@chilung-cgu](https://github.com/chilung-cgu) — + [closed as landed via #2371](https://github.com/lidge-jun/opencodex/pull/2360#issuecomment-5379638729) + at `ae05672e3`. +- [#3621](https://github.com/lidge-jun/opencodex/pull/3621) by + [@yansigit](https://github.com/yansigit) — + [closed as landed via #3622](https://github.com/lidge-jun/opencodex/pull/3621#issuecomment-5549370301) + at `1505cb196`. + ## How this stays accurate This page is a repair, not a process. The process is @@ -127,3 +257,18 @@ The lesson generalizes: when carrying work, take the trailer address from the author's GitHub account (the numeric-id `users.noreply.github.com` form is always safe), not from the commit metadata on their branch. A contributor who commits under a work email is the normal case, not an edge case. + +### Verify the landing, not just the proposal + +The 2026-09-07 audit read exactly 3,000 commits reachable from +`7d8523eed75a67f7a4a15b533744fcd0e6059aa8`, ending at +`53130de4e540fbfcf2629079effb851af54e989e`, and followed source-PR descriptions, +closure comments and GitHub commit-author mappings. Normally merged work, +credited cherry-picks, independent fixes and report-only acknowledgements were +kept distinct from the carried-work tables. + +A PR description or an intermediate branch commit can contain the right trailer +and still lose it when a custom squash message replaces that text. Before +calling a carry credited, inspect the **actual landing commit**: its final +`Co-authored-by` trailer must remain present and resolve to the source author's +GitHub account. The existing presence gate alone does not establish either fact. diff --git a/Dockerfile b/Dockerfile index 382e2db380..5987bfa991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,18 +18,23 @@ RUN cd gui && bun install --frozen-lockfile COPY --chown=bun:bun src ./src COPY --chown=bun:bun scripts/model-metadata.source.json ./scripts/model-metadata.source.json -COPY --chown=bun:bun docker/bootstrap-tls.ts docker/bootstrap-token.ts docker/config.json docker/healthcheck.ts docker/verify-compatibility.ts ./docker/ +COPY --chown=bun:bun docker ./docker COPY --chown=bun:bun gui ./gui RUN cd gui && bun run build FROM ${BUN_IMAGE} AS runtime WORKDIR /home/bun/app +# Docker supervises this foreground process; retain routed state on stop/recreate. +# This uses the existing service lifecycle mode and does not install a service manager. ENV NODE_ENV=production \ + OCX_SERVICE=1 \ OPENCODEX_HOME=/home/bun/.opencodex \ + CODEX_HOME=/home/bun/.codex \ OCX_API_TOKEN_FILE=/home/bun/.opencodex/service-api-token -RUN install -d -m 0700 -o bun -g bun /home/bun/.opencodex +# These homes have incompatible auth.json formats; persist them without combining them. +RUN install -d -m 0700 -o bun -g bun /home/bun/.opencodex /home/bun/.codex COPY --chown=bun:bun --chmod=0600 docker/config.json /home/bun/.opencodex/config.json COPY --from=build --chown=bun:bun /home/bun/app/package.json ./package.json @@ -44,13 +49,12 @@ COPY --from=build --chown=bun:bun /home/bun/app/docker ./docker COPY --from=build --chown=bun:bun /home/bun/app/gui/dist ./gui/dist USER bun -RUN ["bun", "docker/verify-compatibility.ts", "--runtime"] +RUN ["bun", "docker/verify-compatibility.ts"] RUN ["bun", "-e", "import { readOpenCodexCompatibilityVersion } from './src/routing/compatibility/version.ts'; if (!/^[0-9a-f]{64}$/.test(readOpenCodexCompatibilityVersion() ?? '')) throw new Error('Missing or invalid generated compatibility manifest');"] -RUN ["/usr/bin/openssl", "version"] -VOLUME ["/home/bun/.opencodex"] +VOLUME ["/home/bun/.opencodex", "/home/bun/.codex"] EXPOSE 10100 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD ["bun", "docker/healthcheck.ts"] + CMD ["bun", "-e", "const r=await fetch('http://127.0.0.1:10100/healthz');if(!r.ok)process.exit(1)"] -CMD ["sh", "-c", "bun run docker/bootstrap-tls.ts && exec bun run src/cli/index.ts start --port 10100"] +CMD ["bun", "run", "src/cli/index.ts", "start", "--port", "10100"] diff --git a/README.md b/README.md index ff5c72bbaa..b2ab7be63e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,26 @@ account exclusion, affinity expiry, or 401/403 and 429 recovery can rebind them. selection order when one of them — usually your Codex Desktop login — should only be reached for once the others are drained. +### Sponsors + +Sponsors keep opencodex maintained across every upstream protocol change. Interested? +See [SPONSORS.md](./SPONSORS.md). + + + + + +--- +
Docker Compose @@ -226,6 +246,7 @@ see the [installation docs](https://opencodex.me/getting-started/installation/). - **Sub-agents on any model** — feature routed models in Codex's sub-agent picker, with v1/v2 surface control and fallback chains. See the [sub-agent guide](https://opencodex.me/guides/sub-agent-surface/). + - **Log in once, skip the API key** — OAuth for xAI, Anthropic, and Kimi; or forward `codex login`, paste a key, or use `${ENV_VAR}` references. - **Web search & vision sidecars** — non-OpenAI models get real web search and image understanding @@ -278,6 +299,7 @@ full-slash form keeps working too. Details: [model routing docs](https://opencod ## Providers & adapters + OpenAI (ChatGPT login or API key), Anthropic, Google Gemini, xAI, Kimi, Azure OpenAI, Ollama (local + Cloud), Cursor (experimental), and every OpenAI-compatible endpoint — plus DeepSeek, Groq, OpenRouter, Together, Fireworks, Cerebras, Mistral, Hugging Face, NVIDIA NIM, MiniMax, diff --git a/SPONSORS.md b/SPONSORS.md new file mode 100644 index 0000000000..a0a2ee33b1 --- /dev/null +++ b/SPONSORS.md @@ -0,0 +1,105 @@ +# Sponsors + +opencodex is an independent, MIT-licensed project maintained without company backing. Provider +sponsorships fund maintenance and keep the proxy current with every upstream protocol change. +This page is the public rule set: what a sponsor gets, who qualifies for which tier, and how to +ask. It is written so that a sponsor, a contributor, and a user reading the README all see the +same terms. + +"Sponsor" here means a paying provider sponsor. It is unrelated to the `maintainer-sponsored` +label in [`MAINTAINERS.md`](./MAINTAINERS.md), which is about a maintainer vouching for a +contributor's change to a restricted surface. + +Sponsorship buys placement and maintenance attention. It never buys a change in routing behavior, +a default model, a weaker security default, or an exception to the review policy in +[`MAINTAINERS.md`](./MAINTAINERS.md). A sponsored preset goes through the same registry +pattern, typecheck, tests, and review as any other provider. + +## Tiers + +Two tiers, split by what the sponsor is. + +### Main — model developers + +Reserved for organizations that train or host their own foundation models (the OpenAI, +Anthropic, Google, Moonshot, MiniMax class). API relays and gateways are never sold Main +regardless of budget. + +Every model developer is supported as a first-class provider whether or not it sponsors; that +part does not change. A Main sponsor additionally receives: + +- The single banner slot above the sponsor table in the README (one at a time; see + [Placement](#placement)). +- First mention in the README login and provider lines (the "Log in once" OAuth paragraph and + the Providers & adapters summary, both marked with a `sponsors:main-first-mention` comment) + and priority ordering in the built-in provider picker. +- Everything in the Standard tier below. + +### Standard — relays, gateways, and API resellers + +For OpenAI-compatible relays, routers, gateways, and other resellers of model access. A Standard +sponsor receives: + +- One row in the sponsor table: logo (about 150px wide, linking to the sponsor URL), a + "Thanks to X for sponsoring this project!" line, and a blurb of up to about 80 English words + supplied by the sponsor and published verbatim. The maintainer may decline or require edits to + text that is false, misleading, disparages third parties, or breaches applicable law or GitHub + policy. A second-language blurb (for example Chinese) may run alongside the English one. +- A built-in provider preset (`ocx provider select `) shipped in a public npm release, + listed near the top of the provider picker in the dashboard and CLI and marked as a sponsor + there. (The registry field and picker ordering that back this land with the first sponsor + preset; today the picker follows registry order.) +- A detailed entry on the [providers page](https://opencodex.me/guides/providers/) of the docs + site. +- Maintenance: if a release breaks the preset or its adapter, the maintainer fixes it; issues + filed against that provider are triaged first. There is no response-time SLA. + +## Placement + +The README sponsor section sits directly under **Quick start**, before the Docker Compose +details, so it is on screen before a first-time visitor scrolls. It carries one line of context +and the placements themselves: + +1. One Main banner (empty until a Main sponsor signs). +2. The Standard table, one row per sponsor, in order of signing date. + +The README says nothing else about sponsorship; tiers, pricing, and contact channels live only on +this page. + +The translated READMEs under [`readme/`](./readme) carry one linking line right after their +own quick-start block instead of duplicating the section, so a sponsor change is one edit in +English. + +## Pricing + +Pricing is by inquiry; there is no public rate card. Sponsors who sign before the repository +reaches 20,000 GitHub stars lock in their rate for the length of their agreement. Rates rise +once that mark is passed. + +Agreements are integration-scoped: they name the deliverables above, anchor the term to the npm +release that ships them, and carry no marketing obligations on either side. Both sides can walk +away with a pro-rated refund of unused months if the integration cannot be delivered. + +## How to ask + +- X: DM [@claudeebum](https://x.com/claudeebum) +- Discord: [discord.gg/JEaPEtkHwh](https://discord.gg/JEaPEtkHwh), channel `#sponsors` +- Email: jun@lidgeai.com + +Send what you are (model developer or relay), the base URL and model list of your +OpenAI-compatible endpoint, and the tier you want. The maintainer replies with terms and a +draft agreement. + +## What sponsors do not get + +- No influence on routing defaults, failover order, quota policy, or which provider a user's + request reaches. +- No relaxation of the [security review](./MAINTAINERS.md) that applies to authentication, + credentials, or workflow changes. +- No access to user data, request logs, or telemetry; opencodex does not collect any. +- No say over unrelated issues, pull requests, or the release schedule. + +## Current sponsors + +Listed in the README sponsor section. This page carries the rules; the README carries the +names. diff --git a/compose.yaml b/compose.yaml index cea1818568..7692d1691c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,9 @@ services: target: runtime init: true read_only: true + environment: + # A custom CODEX_HOME also requires a matching writable volume target below. + CODEX_HOME: /home/bun/.codex ports: - "${OPENCODEX_BIND_ADDRESS:-127.0.0.1}:${OPENCODEX_PORT:-10100}:10100" environment: @@ -16,6 +19,7 @@ services: OCX_CONTAINER_PUBLIC_ORIGIN: "${OPENCODEX_PUBLIC_ORIGIN:-}" volumes: - ocx-state:/home/bun/.opencodex + - codex-state:/home/bun/.codex tmpfs: - /tmp:size=64m,mode=1777 security_opt: @@ -27,3 +31,4 @@ services: volumes: ocx-state: + codex-state: diff --git a/devlog/_fin/260905_always_on_429_failover/090_outcome.md b/devlog/_fin/260905_always_on_429_failover/090_outcome.md index 3de38c8936..07e4399881 100644 --- a/devlog/_fin/260905_always_on_429_failover/090_outcome.md +++ b/devlog/_fin/260905_always_on_429_failover/090_outcome.md @@ -18,7 +18,7 @@ the tree rather than against the plan — the plan's own criteria were satisfied Two were defects the fix itself created (#3499, #3503), three were surfaces still describing the old contract (#3517, #3520, #3523), one closed the structural gap that let this unit ship two subset-rotator loops (#3512), and one cleaned up after a collision with concurrent maintainer -work (#3526). All are recorded in `091`. +work (#3526). The runtime post-merge findings and CI lessons are recorded in `091`. ## What changed diff --git a/devlog/_fin/260905_always_on_429_failover/091_post_merge_audit.md b/devlog/_fin/260905_always_on_429_failover/091_post_merge_audit.md index 490040325d..297213b2a3 100644 --- a/devlog/_fin/260905_always_on_429_failover/091_post_merge_audit.md +++ b/devlog/_fin/260905_always_on_429_failover/091_post_merge_audit.md @@ -67,14 +67,37 @@ The post-merge run on `dev` then showed `ci failure`, which was a genuinely alar out. It turned out to be cancellation by the maintainer's next merge two minutes later, not a real failure — every job read `cancelled`, not `failure`. -**Rule:** verify with the check-runs API and require zero `null` conclusions, not a pass count: +**Rule:** use the exact head SHA, require every expected aggregate or policy gate by name, and +also require zero non-terminal check runs. A missing check is not success. Paginate before treating +the returned set as complete: ```bash -gh api repos///commits//check-runs \ - --jq '[.check_runs[] | .conclusion] | group_by(.) | map({(.[0]//"null"): length}) | add' +set -o pipefail +gh api --paginate repos///commits//check-runs \ + | jq -se ' + [.[].check_runs[]] as $runs + | ["ci", "enforce-target", "hygiene", "react-doctor"] as $expected + | ($expected - [ + $runs[] + | select(.status == "completed" and .conclusion == "success") + | .name + ]) as $missing + | [ + $runs[] + | select(.status != "completed" or .conclusion == null) + | .name + ] as $pending + | if ($missing | length) == 0 and ($pending | length) == 0 + then {ready: true, expected: $expected} + else error("missing=\($missing) pending=\($pending)") + end' ``` -A clean result looks like `{"skipped":3,"success":24}` — no `null` key at all. +A clean result is `{"ready":true,...}` with exit status 0. This does not replace review-policy +checks such as confirming the approval belongs to the same head. Every `$expected` value is an +exact Checks API `.check_runs[].name`, not a workflow title or workflow-run name. If those required +check-run names change, update this list with the policy; silently accepting an absent name +recreates the original bug. The near-miss paid for itself: sweeping `dev` afterwards found a real defect. #3511 and #3513 landed concurrently, one moving `anthropic-quorum-cache.test.ts` into `tests/routing/` and the diff --git a/devlog/_fin/260907_axis1_bugfixes/000_plan.md b/devlog/_fin/260907_axis1_bugfixes/000_plan.md new file mode 100644 index 0000000000..ad0da69270 --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/000_plan.md @@ -0,0 +1,24 @@ +# Axis 1: measured bug fixes and failure diagnostics + +Completed: see [031_delivery_record.md](031_delivery_record.md) for merged commits, final CI, attribution and deferrals. + +Archetype: satisfy existing contracts. Trigger: owner assigned axis 1 (#3809, #3464, #3661). Goal: deliver reviewable fixes through a manual PR chain and merge the verified scope. Non-goals: new account/retry policy, auth defaults, multipart recovery, releases, native stacks, sibling edits. Stop: merged feasible scope plus explicit unresolved dispositions. Escalation: defer a policy-dependent or unreproducible slice; reclaim a worker slice after two failed packets. Evidence: this unit plus ignored `.tmp/axis1/` and `.codexclaw` receipts. Resources: task-owned worktree/branches and GitHub repository access; Astra high leaves within host capacity; no caller-specified token or wall-clock budget. + +Baseline: origin/dev 137d6a727; source PR #3809 at 4a1012359a522ddd6d7ff77203c9e5f3632d605c. Assigned 5cc8 checkout has pre-existing changes and remains untouched. Code lives in /tmp/ocx-axis1-20260907. + +## Cycle map +1. wp0: docs-only scope, source audit and dependency roadmap; no runtime changes. +2. wp1: bounded quota, version-guidance and recovery-diagnostic changes; independent source/security review and structural checks. Runtime verification deferred explicitly to wp2. +3. wp2: publish ordinary PR chain, run final cumulative hosted CI, resolve findings, admin merge bottom-up and verify dev ancestry. Lower CI only if final CI fails. + +## Delivery contract +The owner explicitly requests a manual delivery chain even where units are independent: quota -> CLI guidance -> recovery reasons, with each layer carrying its own tests and credit. This order is an integration order, not a fabricated runtime dependency. No native registration. Lower commits carry [skip ci] to defer duplicate workflow runs; final head does not. Skipped lower runs are never called passing. No local tests/typecheck/build suites and no hook-triggered suites; task pushes use --no-verify. Hosted ci.yml on the final head must cover all changed runtime/tests; lower-level runs are diagnostic only after final failure. Merge with --admin under the explicit owner exception; preserve original commits/trailers with merge commits, retarget each child to dev, and check integration trees against final evidence. Concurrent dev changes require fresh combined verification. + +## Work boundaries +- Quota: src/providers/quota.ts, src/oauth/anthropic-routing.ts, src/oauth/health.ts, src/server/responses/core.ts, src/images/loop.ts, src/web-search/loop.ts, focused quota tests/layout, provider documentation. +- CLI: src/cli/version-skew.ts and relevant status/doctor consumers, tests/cli/cli-version-skew.test.ts, troubleshooting documentation. No service restart or repair behavior changes. +- Recovery: src/server/responses/agent-task-recovery.ts, agent-task-recovery-cache.ts, src/lib/bounded-body.ts and existing focused tests, Responses error projection if needed, recovery documentation. No expanded admission/retry. +- Main owns shared core.ts integration and test-layout files. Workers must not touch each other's paths or git index. + +## Verification and acceptance +No local suite commands are executed. Source mapping, git diff --check and documentation structural checks are local evidence only. Hosted Cross-platform CI at final head provides runtime/typecheck/privacy and affected platform proof; inspect jobs for skipped coverage. Build completion is provisional until that run and independent audit succeed. Original PR author(s) must be named in commit Co-authored-by trailers, sourced from original commits/API; report authors may also be acknowledged accurately. Source-of-truth sync uses relevant existing structure and docs-site pages. diff --git a/devlog/_fin/260907_axis1_bugfixes/010_roadmap.md b/devlog/_fin/260907_axis1_bugfixes/010_roadmap.md new file mode 100644 index 0000000000..75e07478c0 --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/010_roadmap.md @@ -0,0 +1,3 @@ +# wp0: scope roadmap + +Read current source, prior issue disposition and PR #3809 before choosing changes. Independent Astra high reviewers map each bounded issue. Confirm existing launcher behavior and bounded recovery reasons are already in dev; plan only residual fixes. Record exact file boundaries and acceptance scenarios in 020. Success: all three slices have verifiable requirements, main-owned shared files, original author anchors and explicit policy exclusions. Local evidence is documentation and source inspection; no runtime claim. diff --git a/devlog/_fin/260907_axis1_bugfixes/011_audit.md b/devlog/_fin/260907_axis1_bugfixes/011_audit.md new file mode 100644 index 0000000000..cc1cb1d51e --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/011_audit.md @@ -0,0 +1,5 @@ +# wp0 audit disposition + +Independent Astra high reviewer Hooke: VERDICT: GO-WITH-FIXES (blockers=1). Shared-flight failure propagation was the blocker. Accepted: 000/020 now assign cache and bounded-body ownership and define shared typed outcomes, success-only cache, caller-local cancellation and capacity semantics. Source scouts independently identified and confirmed these requirements. Fixed stale CLI test path. Windows runtime proof requires final workflow_dispatch, now explicit in 030. + +No runtime code changed. Documentation source/ownership inspection and git diff --check are the wp0 evidence. Runtime verification remains wp2. diff --git a/devlog/_fin/260907_axis1_bugfixes/012_roadmap_lock.md b/devlog/_fin/260907_axis1_bugfixes/012_roadmap_lock.md new file mode 100644 index 0000000000..cf2bf0afce --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/012_roadmap_lock.md @@ -0,0 +1,5 @@ +# Roadmap lock + +The second independent audit returned VERDICT: PASS with no remaining blockers. The three accepted slices are ready for scoped implementation. Original quota author: Éverton Toffanetto (everton-dgn), commit identity from 4f3779c04753 and 3ef0ade296c3. Issue reporters: garysassano (10464497) and Hu9956 (282876394). Reporter acknowledgement is separate from code authorship. + +Preserve raw unequal version diagnostics. Detailed recovery outcomes must travel in the shared flight, not caller-local closures. Quota observations use immutable dispatch identity. Final verification is hosted workflow_dispatch for full Windows coverage; local suites remain prohibited. diff --git a/devlog/_fin/260907_axis1_bugfixes/020_bounded_fixes.md b/devlog/_fin/260907_axis1_bugfixes/020_bounded_fixes.md new file mode 100644 index 0000000000..836f743a73 --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/020_bounded_fixes.md @@ -0,0 +1,20 @@ +# wp1: implement bounded bug fixes + +## Quota +Carry only the source PR diff onto current dev, with original-author trailer. Header utilization fraction -> percentage; reset epoch -> timestamp. Creation: parser; serialization: account quota cache; deserialization: existing hydration; consumers: account ranking/health and management reading. Account-bound writer generation is captured with serving credentials, including retry/sidecar/continuation rebinds. Header observations merge model-specific windows and cannot indefinitely postpone probes. Existing 429 eligibility and retry count stay unchanged. Explicit reset evidence must not be truncated by an invented six-hour policy; any unresolved policy piece is deferred. +Scenarios: 200 and 429 on main/sidecar/continuation attribute only the serving account; generation invalidation discards writes; partial/malformed headers preserve known fields; no prior probe means model-window probe is still due; weekly rejected reset outlasts five-hour reset; absent evidence retains existing fallback. Verify with focused tests included in final hosted CI. + +## Version guidance +Compare CLI and running proxy using existing semantic-version utilities if present. CLI newer points to service restart; proxy newer points to upgrading/PATH resolution of CLI; equal/unknown retain suppression; incomparable differing builds use neutral wording. status and doctor share advice. Preserve whether requests are allowed and do not perform repair. Test both directions, prereleases, placeholders, malformed versions and consumer projection. + +## Recovery reasons +Keep existing public wrapper returning boolean and typed detailed result. Classify actual upstream HTTP refusal, transport error, timeout/caller cancellation, response-body/decode failures with a bounded vocabulary. Creation: request/collector; propagation: detailed recovery result; consumers: existing response reason projection/tests/docs. No raw upstream body/errors/tokens/ciphertext in output. Strict admission, one attempt, same credential and unchanged request mutation guarantees. Exercise each failure branch, cancellation races, malformed terminal output and successful recovery in final hosted CI. + +Main owns src/server/responses/core.ts and layout metadata. Source/security review must check public boundaries and negative cases, not only implementation-mirroring tests. Source-only C evidence does not claim runtime correctness; wp2 is mandatory. + +## Source-map clarification from independent #3464 research +Use src/lib/strict-semver.ts unchanged. Raw unequal versions remain skewed; equal precedence with different build metadata and invalid/whitespace/v-prefixed values get neutral wording, not normalization or a guessed direction. Placeholder suppression is unchanged. src/cli/doctor.ts must not call suppressed placeholders a confirmed match. Focused files: tests/cli/cli-version-skew.test.ts, tests/cli/cli-status-json.test.ts, tests/codex-integration/doctor.test.ts. Documentation: reference/cli/lifecycle.md and directly affected Korean/Russian pages. Existing launcher landed via #3616 (4e2246c32); no service runtime changes. + +## Audit refinements +Quota: observe physical responses at the existing oauthDispatch boundary before any main/continuation replacement or return. Use immutable request binding to pair response with selected account; skip when final authorization headers do not prove that bearer or credentialGeneration has changed. An active-account switch alone does not invalidate another account's in-flight observation. Native Claude passthrough and single-account expansion remain outside #3809 carry. Preserve Retry-After precedence; only reject nonfinite/unrepresentable deadlines rather than invent an anomaly ceiling. Header-only rows are probe-due; hydrated Anthropic observations must be probe-due unless probe time is proven. Failed probes settle with the most recent committed observation for all joiners. +Recovery: worker owns agent-task-recovery-cache.ts and bounded-body.ts narrow decode discriminator alongside focused tests. Shared flight carries typed outcome, cache retains only success plaintext, cancelled waiters remain local. Recognized caller cancellation precedes owned timeout, which precedes decode/transport classification. Fatal UTF-8 discriminator must identify actual decoder exceptions without reclassifying fetch/body-reader TypeErrors. Rejected-response cancellation is nonblocking best effort. Keep current public wrappers and combo error projection. Update documented reason lists in structure/04_transports-and-sidecars.md and docs-site/reference/architecture.md. diff --git a/devlog/_fin/260907_axis1_bugfixes/021_source_review.md b/devlog/_fin/260907_axis1_bugfixes/021_source_review.md new file mode 100644 index 0000000000..516a760813 --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/021_source_review.md @@ -0,0 +1,7 @@ +# wp1 source review + +Three bounded patches implemented with regression coverage. Hooke independently passed the physical-response quota observer wiring; Tesla independently passed quota/recovery security and source review with zero blockers. Version comparator and status/doctor projections inspected by main. All source workers report no local suite/typecheck/build execution. + +Quota source: #3809, Éverton Toffanetto; Co-authored-by included in f215f79b4. Version report: garysassano; Reported-by included in f91e3953a. Recovery report: Hu9956; Reported-by included in recovery commit. + +Source-only checks: git diff --check and documentation fence/whitespace inspection. These do not prove runtime correctness. wp2 final cumulative hosted CI is still mandatory. Final CI dispatch includes Windows because ordinary PR workflow omits it. No release/deploy workflow will be dispatched. diff --git a/devlog/_fin/260907_axis1_bugfixes/030_delivery.md b/devlog/_fin/260907_axis1_bugfixes/030_delivery.md new file mode 100644 index 0000000000..be851f530d --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/030_delivery.md @@ -0,0 +1,7 @@ +# wp2: hosted proof and manual-stack landing + +Publish task-owned branches with --no-verify. Standard PR template, source links, truthful skipped-local/lower-CI disclosure and contributor trailers. Lower layers use [skip ci], final cumulative head runs existing Cross-platform CI; never modify shared workflow filters or fabricate checks. On final failure inspect failing jobs, fix owned defects, and only then use lower CI to localize ambiguity. Leave unrelated/unresolvable slices unmerged with evidence. + +Before admin merge: source/security review findings resolved, final CI SHA/run pinned, current PR head and manual membership inspected. Record owner-authorized admin review/lower-CI exception. Merge bottom-up with original commits preserved; do not delete parent branches while children depend on them. Retarget child to dev after parent landing. Reconcile concurrent dev before claiming final integrated proof. Verify every merge SHA is ancestor of refreshed origin/dev. Close #3809 only after its accepted replacement scope lands; keep #3661 open for multipart/retry and #3464 open if broader original acceptance remains unresolved. No release/deploy. + +Final full platform evidence uses workflow_dispatch ci.yml on the final cumulative branch, because ordinary PR CI excludes the Windows runtime job. Cancel only duplicate task-owned PR CI runs; skipped/cancelled runs are not passing evidence. diff --git a/devlog/_fin/260907_axis1_bugfixes/031_delivery_record.md b/devlog/_fin/260907_axis1_bugfixes/031_delivery_record.md new file mode 100644 index 0000000000..439c395f8b --- /dev/null +++ b/devlog/_fin/260907_axis1_bugfixes/031_delivery_record.md @@ -0,0 +1,53 @@ +# Axis 1 delivery record + +Terminal outcome: DONE for the authorized bounded bug/diagnostic scope, with the explicitly listed broader work deferred. Completed 2026-09-07. + +## Delivered + +- #3825 carries #3809 with serving-credential quota attribution, upstream deadline handling, probe-clock preservation and known-reset expiration. Invalid reset metadata does not erase otherwise valid usage; no new unknown-window TTL or synthetic zero was introduced. +- #3826 corrects CLI-versus-proxy version guidance in both directions and prevents false doctor match claims. +- #3827 exposes bounded recovery refusal/timeout/transport/invalid-output reasons through shared flights while preserving admission, success-only caching and caller-local cancellation. +- #3842 is supporting validation work: exact private BigInt file identities preserve existing Aside profile boundaries, including high-ID distinction and directory replacement detection. Public IO/serialization and link refusals remain unchanged. + +## Landing proof + +All four ordinary PRs were merged bottom-up with owner-authorized admin authority. No native stack was registered. Children were retargeted to dev before their parent branches could be automatically deleted. + +| PR | Reviewed layer head | Merge commit | +| --- | --- | --- | +| [#3825](https://github.com/lidge-jun/opencodex/pull/3825) | `d3c70f9d8c8cc6fced7a93577b93e8b141473ea3` | `85fbdb59621046da3db1839a5cce4c7260f99385` | +| [#3826](https://github.com/lidge-jun/opencodex/pull/3826) | `872f0e5aa714f6a2e757510195d1c038ac70e26d` | `860baaf9032fa7ea3030c78ab555608e3325a338` | +| [#3827](https://github.com/lidge-jun/opencodex/pull/3827) | `2e8ef03428f8e619dc92b250fbbc5d5dd7ad53cb` | `5a97db9b20f03a65e714ddc88d2523bea9aeacae` | +| [#3842](https://github.com/lidge-jun/opencodex/pull/3842) | `b29bbb440aaf70b283445a9c37e194a4a4e6859a` | `5fdf9bbdd9ff7657f0b6d7101697317d708af0e7` | + +The runtime integration commit is `5fdf9bbdd9ff7657f0b6d7101697317d708af0e7`. Its full tree `90a75118402d2f310393bef9ac3e4668cfcbdcfa` exactly matches the final combined validation candidate `9470fdb1bc9a02715a3760c36301d3d030a4e4fa`. A fresh fetch and ancestor check confirmed every merge on dev. The candidate included dev `bf85e675484a2391b94b2135bbebe739813a9621` plus all four layers. + +## Verification + +- [Cross-platform CI 34074350604](https://github.com/lidge-jun/opencodex/actions/runs/34074350604): all 26 jobs succeeded at the combined candidate, including Linux, macOS, Windows, Docker smoke, typecheck, privacy, build and operational checks. +- [Service lifecycle 34074351720](https://github.com/lidge-jun/opencodex/actions/runs/34074351720): Linux, macOS and Windows succeeded at the same candidate. +- Independent Astra high source/security audits covered the scoped implementations, merge interactions and exact-identity support. +- All current review threads on the four delivered PRs were resolved after runtime evidence was available. +- No local application test suite or local typecheck ran. Pushes used --no-verify; per-layer CI was deferred by explicit owner instruction. Cancelled and skipped checks were never represented as passing tests. +- Privacy scanning passed. Documentation static build produced 425 pages in 8.23 seconds at 2522264d5; its documentation subtree remained unchanged by the supporting identity fix. Dependencies were installed from the frozen lockfile with install scripts disabled. The build changed no tracked files. +- The assigned pre-existing working-tree changes were preserved; delivery used an isolated worktree. + +## Corrections and remaining limits + +Initial verification exposed incomplete test homes/default configuration and old calendar reset dates in current-measurement fixtures. Those fixtures were corrected without removing behavioral assertions. Known-expiry tests use explicit simulated time. Later review added expired-window handling, field normalization and a global test network guard. + +Imported axis-five closeout contact addresses blocked privacy scanning. [#3836](https://github.com/lidge-jun/opencodex/pull/3836) removed the addresses while retaining author names and all commit attribution; no scanner rule or allowlist was weakened. + +Earlier Windows Aside incidents reported an apparent shared catalog target. Their actual file IDs were not captured. The independently demonstrable Number-precision defect was corrected by #3842, and semantic/native regressions plus the previously failing route case passed in final CI. This does not retroactively prove every earlier incident's raw IDs or cause. + +An earlier Windows outbound-proxy test timed out at its existing 15-second bound. Its scoped test/transport files were unchanged and the stalled phase was not measured. No timeout increase or unrelated proxy repair was made; later passing execution is not a claim that the timing root cause was fixed. + +## Attribution and issue disposition + +Éverton Toffanetto's Co-authored-by trailer is retained in reachable commit `f215f79b4562735029ad5672a68bc6104e534b98`. The issue reporters garysassano and Hu9956 are acknowledged in the corresponding diagnostic commits. Merge commits preserve those commits and trailers. + +The original #3809 was confirmed closed with a landed-via-#3825 marker at final recheck. The initial carry source was 4a1012359; the original author subsequently updated the source PR, so this record does not claim a verbatim merge of its later head. + +#3464 remains open for its broader automatic-repair/request-policy requests. #3661 remains open for multipart reconstruction and recovery retry policy. Those choices were outside this delivery. No release, deployment, new account-selection strategy or authentication-default change was performed. + +The preceding numbered documents are historical plans and audits; their original _plan paths refer to the planning stage. diff --git a/devlog/_fin/260907_axis3_protocol/000_plan.md b/devlog/_fin/260907_axis3_protocol/000_plan.md new file mode 100644 index 0000000000..d0511f46ec --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/000_plan.md @@ -0,0 +1,17 @@ +# Axis 3 protocol fidelity roadmap + +Mode: satisfy-spec HOTL, requested by the maintainer on 2026-09-07. Deliver source-grounded dispositions for #3815, #3816, #3807, #3719 and land accepted fixes with original authors credited in commits. No local suites or typecheck; verification is remote exact final-head CI, with lower-layer CI only on final failure. Ordinary manual PR chain only; admin merge authorized. No explicit token or wall-time limit was requested; agents use bounded tasks and waits. Do not invoke private provider accounts or spend inference credits. Tools: local Git/files, GitHub gh, Astra high leaf agents. Writes confined to task worktrees and this axis's GitHub branches/PRs. Preserve unrelated dirty work. + +Scope: ordered Claude thinking/redacted/tool-result envelope fidelity; Grok strict-client control frame projection; valid task-seed diagnosis. Exclude new auth/routing/default policies, fabricated provider signatures or tool pairing IDs (new Responses reasoning item IDs are permitted transport identities), cache savings claims, unrelated axes, deployment/release. Unknown field/runtime reports receive explicit deferred dispositions per user direction. + +Work phases: wp0 roadmap audit and lock; wp1 prepare two independently reviewable source layers and any justified contract regressions, then remote final combined verification; wp2 publish/merge ordinary PRs bottom-up and record final ancestry/dispositions. The two source fixes are independent; the manual chain is the user's requested integration/CI grouping, not a runtime dependency. + +Success: roadmap verified, accepted changes reviewed and remotely validated, commits credit SB Yoon (yansigit) and Yumi for #3815 and Danh Thanh (dt418) for #3816, landed SHA proven ancestor of refreshed dev; uncertain #3807/#3719 runtime or cache claims remain open. Stop only after accepted delivery and explicit dispositions. Escalate only an unavoidable owner-policy choice; defer that portion and continue the rest. + +Acceptance: (1) thinking then text/tool then result retains order and genuine signatures; opaque blocks remain bounded and malformed/nested signatures fail closed. (2) Grok user agent receives ordinary Responses data without codex.rate_limits/codex.response.metadata, while proxy inspection and normal clients retain metadata. (3) valid external task seeds preserve text/order; absent metadata invalid tool outputs still reject. (4) no credential, admission, cache-retention default, provider/routing policy mutation. (5) final CI must really run relevant tests/typecheck, not skip/cancel or fabricate success. No local suite was run. Final failure permits lower-layer CI for localization; unrelated failures may defer delivery, never count as success. + +Sources: PRs https://github.com/lidge-jun/opencodex/pull/3815 and /pull/3816; issues /issues/3807 and /issues/3719. Current dev 137d6a727. Evidence snapshots under .tmp/axis3. Public notes contain no unreleased vulnerability detail; any new security investigation stays in scratch. + +## Terminal outcome + +Runtime scope delivered in3830–3832 with the evidence and explicit diagnostic remainders in021_delivery_record.md. Documentation-only completion retains the late source-author rows and archives this unit. Initial planning statements are historical; the delivery record is the outcome authority. diff --git a/devlog/_fin/260907_axis3_protocol/001_roadmap_lock.md b/devlog/_fin/260907_axis3_protocol/001_roadmap_lock.md new file mode 100644 index 0000000000..115f8c3dfa --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/001_roadmap_lock.md @@ -0,0 +1,3 @@ +# Roadmap lock + +Independent Astra high reviewer Pauli passed the amended wp0 roadmap. Transport reasoning IDs are permitted; fabricated tool pairing IDs remain prohibited. Claude fallback retention must be bounded or removed and checked remotely. Grok parser must follow SSE last-field/reset semantics. No runtime was changed in wp0. Next: wp1 carries source layers, adds justified regression coverage and verifies the final combined head remotely. diff --git a/devlog/_fin/260907_axis3_protocol/010_prepare_and_verify.md b/devlog/_fin/260907_axis3_protocol/010_prepare_and_verify.md new file mode 100644 index 0000000000..c278f094bf --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/010_prepare_and_verify.md @@ -0,0 +1,42 @@ +# Prepare and verify combined protocol candidate + +Reverify base/source heads before build. Carry exact source deltas from the scratch diff snapshots, fold independently confirmed review fixes only. Each commit contains verified contributor trailers. Do not include upstream planning notes or unrelated changes. + +Layer 1 MODIFY: +scripts/test-layout/layout.json +src/claude/inbound.ts +src/claude/outbound.ts +src/responses/reasoning-envelope.ts +tests/claude-integration/claude-code-thought-signature-scope.test.ts +tests/claude-integration/claude-inbound.test.ts +tests/claude-integration/claude-outbound.test.ts +tests/claude-integration/claude-source-envelope.test.ts +tests/fixtures/test-layout-expected.json +tests/responses/reasoning-envelope.test.ts + +Preserve genuine signatures; encode bounded unsigned/redacted fallback; keep structured tool results. Layer 2 NEW src/server/grok-responses-control-frame.ts and MODIFY: +src/server/grok-responses-control-frame.ts +src/server/responses/core.ts +tests/responses/responses-snapshot-repair-server.test.ts + +Separate strict-client filtering from internal inspection. On a Grok metadata frame, forward no incompatible client frame; on ordinary delta, preserve unchanged; ordinary clients remain unchanged. No shared account/routing changes. + +Potential follow-up tests belong only in existing responses/Claude test files after diagnosis, with independent expected values. If no valid unhandled #3807 input is established, leave production guards unchanged. #3719 cache-hit and true Anthropic signed replay cannot be certified by codec fixtures. + +SoT: update docs-site/src/content/docs/guides/claude-code.md and existing translated counterparts only if #3815 makes their drop-policy statements stale. Read docs-site/AGENTS.md first. No global retention change. + +Verification: user prohibits local suites/typecheck (NOT RUN). Inspect source and diff-check locally. Push task branches with --no-verify. Dispatch existing Cross-platform CI workflow on final combined head, lane all. Confirm workflow head SHA, jobs, conclusion, test/typecheck execution from logs. Final CI failure permits lower-layer CI. Keep workflow/protection configuration unchanged; suppress only task-owned redundant automatic runs when needed for requested top-first scheduling, reporting cancelled runs honestly. No real accounts are used. + +## Audit amendments + +New rs_ reasoning IDs are normal transport identity, not fabricated tool call pairing. Do not synthesize tool-call IDs to bypass #3807 validation. + +Before acceptance, remove unbounded thinkingBuf retention introduced by #3815 or charge it to the existing TranslatorBudget retained bytes with normal fail-closed overflow. Use the established budget and error event; no silent truncation or new policy default. Cover multi-part text exactness, empty continuity fallback, and overflow with a small injected existing budget in remote regression tests. Decoder/consumer traces must prove any compact continuity marker still replays the original summary. + +#3816 must use SSE last-event-field-wins semantics, including colonless/empty resets and removal of only one optional leading space. Test event-only, data-only, repeated event fields in both orders, and preservation of ordinary completion data. Keep downstream Grok WebSocket support deferred because the existing surface marker is absent there; do not claim this HTTP/SSE patch solves it. + +## WP1 source refresh and scoped hardening + +Previous D: roadmap locked; execute reviewed source preparation. PR #3815 advanced to 76e07d181c48dca8c80167878381e1edb5642395 during investigation, including budget fixes and translated guide changes; carry fresh source, not old snapshots. Add a third dependent hardening layer only for source-proven preservation faults. MODIFY src/responses/parser.ts: retain recognized redacted-only and empty signed envelopes even when text is empty, preserving real boundary grouping. MODIFY src/bridge.ts: preserve signed block boundaries and redacted block positions identically in streaming/buffered output; signature fragments must be assembled at owning adapter boundary. MODIFY src/claude/outbound.ts only for exact block order/text restoration where current contract permits; do not invent a new signed continuity carrier or change hide-thinking policy. If hidden signed replay needs a new policy/carrier, explicitly defer that part rather than widening scope. Existing budget/guard contracts remain. + +Tests: existing tests/responses/anthropic-thinking-signature.test.ts or matching current domain file and Claude envelope tests get exact block-array roundtrip oracles; no fixture claims a live genuine signature. tests/responses/responses-compaction-routing.test.ts gets an established-history complete send_message_to_thread envelope across normal response, stored-ID continuation, v2 compaction_trigger and v1 compact endpoint, preserving real pairing and task content. If current fixture support makes a case impractical, record exact gap; no runtime seed repair. diff --git a/devlog/_fin/260907_axis3_protocol/011_candidate.md b/devlog/_fin/260907_axis3_protocol/011_candidate.md new file mode 100644 index 0000000000..7edc9aa1ce --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/011_candidate.md @@ -0,0 +1,9 @@ +# Combined candidate + +Source baseline: dev 137d6a727. Foundation carries #3815 through 76e07d181 with SB Yoon/Yumi commit trailers. Grok carries #3816 d5e0a9a2 and corrects SSE event overwrite/reset semantics, with Danh Thanh trailers. Added established-history external-task HTTP/continuation/compact fixtures without changing the missing-ID guard. Replay hardening preserves signed/opaque-only inputs and block ordering; signature updates replace previous values according to the SDK accumulator contract, and block closure waits for the next semantic event. + +Independent source reviews: Pauli scoped foundation PASS (18/18 files); Faraday Grok/seed PASS. Final Claude combined source audit and remote CI pending. Local suites/typecheck/build not run under user instruction. No live accounts invoked. + +Deferred: #3807 lacks raw failing current-version input; #3719 still needs live intended-Anthropic acceptance and controlled cache comparisons. Locally hidden text through Claude and legacy combined-envelope streaming order recovery are not claimed supported. Existing compatibility enforcement, hidden presentation, credential/admission and retention policies remain. + +Ordinary PR chain is an integration grouping requested by owner, with final combined CI first. Lower-layer runs only if it fails. Admin merge is authorized after accepted evidence. No GitHub native stack or fabricated check status. diff --git a/devlog/_fin/260907_axis3_protocol/020_delivery.md b/devlog/_fin/260907_axis3_protocol/020_delivery.md new file mode 100644 index 0000000000..714a9c0b8e --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/020_delivery.md @@ -0,0 +1,15 @@ +# Publish and deliver verified manual chain + +Prerequisite: wp1 accepted-source review and successful final-head remote validation, or source-grounded defer outcome. Publish ordinary PRs targeting dev then the parent branch, using every repository template section. Bodies name source PRs, own layer-only diff, exact final combined CI evidence and explicit lower-layer CI deferral per owner instruction. Do not attest local CI. Preserve original contributor trailers in commits; admin merge with merge commits preserves their identity. + +Read live native-stack membership and head/base identity before merge. Never register a native stack. Parent merges to dev first; retain its branch, retarget child to dev, verify current head and ancestry. If integration tree changes materially, refresh final combined CI before landing. Use --admin and --match-head-commit exact guard. Do not merge into the parent branch by mistake. Refresh origin/dev and prove each merge SHA ancestor. Close superseded source PRs only after equivalent fix is actually landed, with credit and replacement link. Keep #3807 and #3719 open if real reproduction/cache acceptance remains unmet. No release or deployment. + +Record final PR URLs, source-to-delivery mapping, commit authors/trailers, CI run and exact SHA, review verdicts, remaining limitations and preserved dirty-work evidence. No fabricated status checks. Completion: every candidate has an honest disposition, accepted work is landed, unresolved diagnostics explicitly deferred under user direction. + +## Delivery revalidation + +Previous D: all 24 real GitHub runtime producer jobs succeeded at final9b5b670db; same-head remote Bun1.4 full suite20897pass18skip0fail, focused405pass, docs build pass. Aggregate ci is still queued; do not claim the workflow complete or manufacture a status. Its only operation is combining those passed producer results. Maintainer explicitly authorized admin merge, and live dev rules expose no required_status_checks rule. Delivery may use the actual completed producer evidence with aggregation status explicitly disclosed; never waive an unrun or failed runtime producer. + +Carry late source docs #3815 through221353662: eight outbound redacted-reasoning table rows in the same eight locales. Prepared docs-only68d90aa37 has runtime/test trees identical to9b and remote docs build passed. After three runtime PRs land bottom-up, bring this docs-only tail and a final delivery record into a fourth ordinary PR. MOVE the completed owning unit from devlog/_plan/260907_axis3_protocol to devlog/_fin/260907_axis3_protocol and NEW021_delivery.md with actual merge/CI/source-credit evidence and deferred issues. No new runtime tests: exact code-tree equality plus docs build/hygiene are the applicable checks. + +Refresh dev and membership before each guarded admin merge; compare merged runtime tree with tested9b. Any unrelated concurrent dev change requires integration review and appropriate renewed evidence. Original #3815 and #3816 close only after their full carried changes (including docs tail) are landed. #3807 and #3719 remain open for the already recorded limits. diff --git a/devlog/_fin/260907_axis3_protocol/021_delivery_record.md b/devlog/_fin/260907_axis3_protocol/021_delivery_record.md new file mode 100644 index 0000000000..e27fd8b41b --- /dev/null +++ b/devlog/_fin/260907_axis3_protocol/021_delivery_record.md @@ -0,0 +1,31 @@ +# Axis 3 delivery record + +## Delivered runtime + +Ordinary PR chain, merged bottom-up with explicit owner admin authorization: + +| PR | Scope | Merge commit | +| --- | --- | --- | +| [3830](https://github.com/lidge-jun/opencodex/pull/3830) | Claude envelope foundation from #3815 | 2269e076d4222ada6ea3694fb8eed04f91a201d2 | +| [3831](https://github.com/lidge-jun/opencodex/pull/3831) | Grok strict-client projection from #3816 and SSE field correction | 07f8d70a75f088b19f4c9dd849e34034a88ab5f3 | +| [3832](https://github.com/lidge-jun/opencodex/pull/3832) | Replay boundaries, terminal overflow, established-history fixtures | 4349cf3cefdb5ed04575f49023ae34ffe6462e1c | + +Original contributors are retained in commits: SB Yoon and Yumi for #3815, Danh Thanh for #3816. Merge commits preserve the carried commits. The documentation-only tail of #3815 through221353662 is carried with both original contributor trailers in68d90aa37. + +## Verification + +- [Final candidate CI](https://github.com/lidge-jun/opencodex/actions/runs/34065721438) completed SUCCESS: all25 jobs at9b5b670db3e24ae5522c5d61e74c071c71257a26, including Linux, macOS shards/control and all six Windows shards. +- Same-head remote Linux Bun1.4.0 full suite:20897pass18skip0fail with `bun run test -- --parallel=1`; typecheck, privacy scan and documentation build passed. Focused protocol coverage:405pass1skip0fail. +- While CI ran, dev advanced to b65b9d8f2 with BigModel/Raycast changes. Conflict-free integration cc6afe2c97fb423363e99682b906bcb529478688 passed remote typecheck and633tests1skip0fail across15 relevant files, including shared passthrough/registry/layout guards. +- Actual runtime landing tree at4349cf3ce equals the integration tree ccaf0a0383cb3e8808e24576271c861625b506fb exactly. This is integration proof, not a claim that the earlier full CI ran on4349cf3ce. +- Independent Astra high source/security/integration reviews passed. The terminal-closure overflow finding was fixed before acceptance. New-test oracle mistakes found remotely were corrected without weakening exact assistant-array or pairing assertions. +- The earlier parallel remote run had21 catalog timeouts; isolated and final sequential runs passed, and final hosted CI passed. No separate root-cause fix is claimed. +- No local test suite or typecheck ran. All pushes used `--no-verify`. Native stacks and fabricated check statuses were not used. Automatic lower/intermediate CI was deferred or cancelled under the owner's combined-first direction. + +## Explicit remainders + +#3807 remains open: the demonstrated complete external-task envelope is already supported, and current-version raw reporter reproduction is unavailable. New ordinary, stored-ID continuation, v2-trigger and v1-compact fixtures preserve established history without relaxing missing-call-ID validation. + +#3719 remains open: live intended-Anthropic acceptance and controlled cache measurements are unverified. Locally hidden text through the Claude boundary and legacy combined-envelope streaming ordering remain outside the preservation claim. Existing compatibility enforcement, hidden display, authentication, routing and cache-retention defaults remain intact. + +The supplied dirty worktree and existing remote main checkout were preserved; execution used separate task worktrees. No release, deployment or account configuration change was made. diff --git a/devlog/_fin/260907_axis5_display_cli/000_plan.md b/devlog/_fin/260907_axis5_display_cli/000_plan.md new file mode 100644 index 0000000000..eb6ed46d27 --- /dev/null +++ b/devlog/_fin/260907_axis5_display_cli/000_plan.md @@ -0,0 +1,37 @@ +# Axis 5: display names and provider automation + +Date: 2026-09-07. Class C3, scoped satisfy-spec HOTL loop requested by owner. +Goal: deliver feasible unique changes from #3627, #2716, #3780 on dev with original author trailers. +Scope: display-only catalog metadata, discovered-model editor, optional JSONL CLI output, regression coverage and their docs. No auth/default/routing changes, native stacks, releases, or edits to existing dirty work. +Resources: existing repository/GitHub credentials and Astra high leaf agents; no user-set time/token cap. Isolated checkout /tmp/ocx-axis5-01a078d6. Owner authorizes no-verify push and admin merge. All local suites are prohibited; typecheck/build/test evidence will come from final combined GitHub CI. No invented lower-layer CI successes. +Terminal: merged, proven already delivered, or evidence-backed deferred when infeasible; finish after verifying all dispositions. New product decisions are isolated and deferred rather than guessed. +Records: this unit, .tmp/axis5-evidence, and session-bound .codexclaw goalplan. + +## Roadmap + +WP0: documentation-only source-delta and delivery plan, independent plan audit and document validation. +WP1: implement three scoped source carries with individual credited commits, publish ordinary manual PR chain, audit final tree, validate final combined head, merge verified layers bottom-up, and record dev ancestry. +The three layers are a user-requested review/integration sequence, not a claimed runtime dependency: native catalog -> JSONL CLI -> discovered editor. Source PR branches are never rewritten. +Read 010_delivery.md for diff-level scope and activation scenarios. + +## Sources + +- https://github.com/lidge-jun/opencodex/pull/3627 +- https://github.com/lidge-jun/opencodex/pull/2716 +- https://github.com/lidge-jun/opencodex/pull/3780 +- Base dev: 137d6a7270e7ecfb1c791993800a17c0e30022d9 +- Existing API display-name contract from #3212 is already on dev; only missing UI is carried. + +## CI and merge + +.github/workflows/ci.yml has pull_request triggers on all bases and workflow_dispatch lane=all for complete coverage. Pushes to feature branches do not independently trigger it. Defer/cancel only this task's lower-layer expensive runs as authorized, recording cancellation as cancellation. Dispatch all on final head; only if final CI fails use lower-layer runs to isolate. Do not edit shared workflow policy or fabricate check statuses. +Use merge commits and retain parent branches so commit identity and author trailers survive bottom-up merges. Retarget a child only after its parent lands. If dev moves concurrently, integrate the new dev into the top and refresh exact combined CI before shipping the resulting changed tree. +Review-ready requirements remain visible; local suite prohibition is explicitly documented instead of ticking a false local attestation. Admin waiver applies to the requested merge, not to truthful evidence. + +CI scope refinement: the discovered editor is the final layer so the final commit and PR diff include gui/**, activating GUI lint/build/artifact jobs. ci.yml gates always run GUI tests; docs deployment is NOT dispatched because it publishes. Public docs receive static source consistency inspection here, with docs build explicitly unverified unless an existing build-only remote path is available. + +CI scheduling refinement: lower-layer head commits may use GitHub documented [skip ci] to avoid push/pull_request suite launches; this yields missing/pending evidence, NOT green. Final head has no skip marker and receives lane=all workflow_dispatch. Source: https://docs.github.com/en/actions/how-tos/manage-workflow-runs/skip-workflow-runs (opened 2026-09-07). Admin merge records this explicit owner-requested lower-layer waiver. Do not propagate skip markers into integration merge messages. + +## Terminal status + +DONE: all three feature layers landed; see 020_delivery.md for exact commits, verification boundaries and deferred Mac test-runner investigation. diff --git a/devlog/_fin/260907_axis5_display_cli/001_roadmap_audit.md b/devlog/_fin/260907_axis5_display_cli/001_roadmap_audit.md new file mode 100644 index 0000000000..2e4b740b4d --- /dev/null +++ b/devlog/_fin/260907_axis5_display_cli/001_roadmap_audit.md @@ -0,0 +1,11 @@ +# Roadmap audit closure + +WP0 documentation implementation, 2026-09-07. +Independent Astra high reviewers: Carver (#3627), Godel (#2716), Anscombe (#3780 and integrated roadmap). + +The integrated verdict was GO-WITH-FIXES (four blockers). The plan now distinguishes lower-layer waived CI from final combined passing evidence; removes the unreachable empty-provider CLI acceptance; requires confirmed-persistence reconciliation after GUI refresh failure; and requires timeout reachability analysis against the installed bounded-fetch wrapper before adding any timeout logic. + +Source heads: native f699ec7f998d56bf205db96762b821cd8c228a35; editor 93ed44053b68a9707f8271981d5f7e4bc25e9b70; JSONL 9b873e6f7519a022dd4658db4d1cb92689bb4663. +The physical manual chain is native -> JSONL -> GUI, enabling final GUI CI jobs. It is owner-requested integration ordering, not a claimed runtime dependency. +Native external-name preservation is qualified by existing pinned Astra normalization; existing policy remains intact. +No product tests, typecheck or build ran. WP0 checks only roadmap structure, source paths, explicit acceptance and credit records. Product verification remains WP1 remote CI. diff --git a/devlog/_fin/260907_axis5_display_cli/010_delivery.md b/devlog/_fin/260907_axis5_display_cli/010_delivery.md new file mode 100644 index 0000000000..b9cc7e2c4d --- /dev/null +++ b/devlog/_fin/260907_axis5_display_cli/010_delivery.md @@ -0,0 +1,37 @@ +# WP1: reconcile, deliver, verify and merge axis 5 + +Depends on WP0 roadmap audit. Source baseline dev 137d6a727. Previous D must confirm roadmap-only completion before production patches. + +## Layer 1 — #3627 native display names + +MODIFY src/codex/catalog/sync.ts: introduce reversible native label overlay at observed-state merge, restore original label before metadata normalization, strip marker from template clones, apply configured label to supported bare native rows only. MODIFY src/codex/convergence.ts: supply the same modelDisplayNames map as retained sync. MODIFY tests/codex-integration/codex-catalog.test.ts and provider configuration docs (English, Japanese, Korean, Simplified Chinese). +Field chain: existing providers.openai.modelDisplayNames config -> both merge call sites -> nativeDisplayNames argument -> display_name plus catalog-only opencodex_native_display_name {slug,original,applied} -> JSON catalog serialization -> restoration before next normalization. Clone consumers must remove overlay markers; source inputs remain immutable. +Activation: configured label replaces native name; removing/blanking restores owned original; external Sol rename is preserved; Astra remains subject to existing pinned-metadata normalization and docs/tests state that exception; newer native metadata upgrades after reset; repeated serialized cycles stable; account-qualified/combo/pro/custom rows unchanged. Exact model IDs and capabilities unchanged. +Credit: Co-authored-by: Éverton Toffanetto . + +## Layer 2 — #2716 discovered name editor + +NEW gui/src/components/ModelDisplayNameDialog.tsx and gui/tests/models-display-name-editor.test.tsx from source PR after current API contract comparison. MODIFY gui/src/pages/Models.tsx, models-shared.ts, gui/src/styles.css, all nine locale modules, English provider configuration docs. +Field chain: existing /api/models displayNameOverride/displayNameSource -> ModelRow optional fields -> Name action/dialog -> existing display-name save/reset endpoint -> persisted provider modelDisplayNames -> reload /api/models. No new persisted field or endpoint is needed. +Activation: save/reset/unchanged cancel; blank/too long/slash/control input; one submit under double click; save failure retains dialog; reload failure remains recoverable; focus returns after close; original selector always visible and alias action remains separate. +Credit: Co-authored-by: Zig Zag . +Browser smoke: render real isolated app, open Name dialog and observe screenshot; use mocked management responses or isolated disposable home, never mutate personal config. GUI tests/build/i18n/lint and docs build are remote CI obligations; not run locally. + +## Layer 3 — #3780 provider JSONL + +MODIFY src/cli/provider.ts and src/cli/capabilities.ts to accept --jsonl, emit existing configured-array objects one per line, reject combined --json/--jsonl before reading config. MODIFY tests/cli/cli-provider.test.ts, public CLI docs and skills/ocx/references/01_management_surface.md, 02_json_shapes.md, 03_recipes.md. Regenerate or reconcile derived surface with generator source; no unrelated output. +Field chain: argv -> consumeFlag -> output choice; no config serialization changes. JSONL entries use exactly existing JSON configured fields; no credentials added. The real config loader seeds providers; a zero-provider CLI scenario is not a reachable acceptance claim. Preserve existing loader behavior. Extend source tests to compare every emitted object with --json.configured for multiple registry/custom providers, ensure empty stdout on both conflicting flag orders, and verify escaping. Update all seven translated CLI provider tables and describe consumer-side line processing without claiming producer streaming. +Activation: multiple providers including custom names -> one parseable record each; default human and --json unchanged; both flags rejected; unknown args still rejected; conflicting flags -> empty stdout before config loading. +Credit: Co-authored-by: 투린 . + +## Verification and disposition + +Static git diff --check and independent source audits throughout. Existing focused test paths are reviewed for target coverage, but ALL LOCAL SUITES NOT RUN by owner instruction. Final ci.yml workflow_dispatch lane=all on published final SHA supplies typecheck, full tests and platform results; inspect actual job conclusions and head SHA. Add missing coverage within source scope if audit identifies a contract gap. Inspect GUI workflow coverage and obtain remote GUI/build evidence if not present in final dispatch. +Source-of-truth: provider configuration and CLI docs above; update structure/03_catalog-and-subagents.md only for native overlay contract. No new enforcement layer; tests/CI are evidence, admin bypass is owner-authorized and recorded. +Before merging: fresh heads and native membership, independent review dispositions, final CI proof, original author trailers, screenshot for GUI PR. If infeasible, record concrete cause and leave only that layer unmerged. After each merge: verify mergeCommit SHA and inclusion on fetched dev. Close superseded original PR only once its delivery is on dev and preserve attribution. + +Audit amendment: native label restoration preserves an external edit only subject to existing metadata normalization, notably pinned Astra replacement. Do not change native normalization policy. Add the Astra external-edit regression and qualify the promise consistently in all four affected docs. The native feature must preserve metadata including capabilities; English/Japanese wording is explicit. Final physical branch order is native -> JSONL -> GUI to activate final GUI gates; numeric sections above identify features, not alternate dependency claims. + +GUI audit amendment: confirmed persisted save/reset must reconcile editor snapshot and draft even when reload fails. A saved:true error is distinct from an unpersisted error. Stalled requests must not lock every dialog exit indefinitely: use existing UI request cancellation/deadline conventions, and represent uncertain write outcome without claiming rollback. Add focused source tests for first-save/reset plus reload failure, saved:true errors, duplicate protection and stalled cancellation. + +Plan audit synthesis (Astra high Anscombe): GO-WITH-FIXES, four blockers folded. (1) Lower layer CI is explicitly waived/deferred, never labeled passing; fresh head/base checks plus resulting tree equivalence tie admin merges to final combined evidence. (2) Removed unreachable empty-provider CLI scenario; loader behavior preserved. (3) Confirmed-persistence vs refresh state and tests required. (4) First rederive stalled-request reachability through installed global createBoundedFetch; reuse existing bound if it already applies, add no duplicate budget. Any remaining timeout scenario must be production-reachable. diff --git a/devlog/_fin/260907_axis5_display_cli/020_delivery.md b/devlog/_fin/260907_axis5_display_cli/020_delivery.md new file mode 100644 index 0000000000..7a2b76d0f8 --- /dev/null +++ b/devlog/_fin/260907_axis5_display_cli/020_delivery.md @@ -0,0 +1,42 @@ +# Axis five delivery record + +Outcome: DONE on 2026-09-07. The three feature layers landed in dev through owner-authorized admin integration. Original contribution credit is present in both carried commits and merge commits. + +| Source | Delivery | Merge commit | +| --- | --- | --- | +| #3627 native OpenAI display names | #3820 | 1e16fe4c077ecf353d79c46873d8039d9176704d | +| #3780 provider list JSONL | #3821 | be24986e5ff8474ca6699895855f0ad9352e9d86 | +| #2716 discovered-model name editor | #3824 | 44c69fdd619b272066113388edd80f6c59b0682a | + +The source pull requests were closed after landing. The late #3627 head 81f150e4 added metadata wording already covered by the delivery; its runtime files were checked byte-for-byte against dev before closure. + +## Delivered behavior + +Native labels are reversible overlays on supported bare native rows. IDs, capabilities and routing remain intact; restoring a label still respects existing pinned Astra normalization. Both retained synchronization and convergence pass the same configuration map. + +JSONL emits one configured-provider object per line using the existing JSON fields. Both conflicting flag orders fail without stdout. Multi-provider parity and escaping are covered, and all translated CLI tables and generated capability documentation were updated. + +The editor preserves exact selectors, validates labels, supports reset, and recovers confirmed saves separately from failed refreshes and unknown transport outcomes. Stalled operations use the existing bounded-fetch mechanism. Draft reconciliation preserves the mounted dialog and focus behavior. Desktop/mobile Korean rendering and save/reset/validation/focus were driven against the compiled CI artifact with disposable fixtures. + +## Verification boundaries + +- Feature head f51ec2421c49df0fd4eac8a9a56a6283b426387d: [Cross-platform CI attempt 2](https://github.com/lidge-jun/opencodex/actions/runs/34068041704/attempts/2), 25 successful jobs. Dashboard tests: 1,737 passed, zero failed. Typecheck, lint, scans and build passed. +- Late platform base changes had zero overlap with the 39-file feature delta and passed [their 26-job CI](https://github.com/lidge-jun/opencodex/actions/runs/34068218011). Independent compatibility review checked the decompression diagnostics and container lifecycle interaction. +- Prospective merge tree 85c9b25818a93859a6d6fc824e2ed0678da46c8f passed 370 focused tests on isolated Linux with project Bun 1.4.0: 344 catalog/CLI tests and 26 editor tests, zero failures. The transmitted source archive SHA-256 was ae191e1f0a809e75c9c198bc92233964880541f6747e4603a47b2c180f773d49. +- The actual final runtime merge 44c69fdd619b272066113388edd80f6c59b0682a has exactly that tested tree. This is focused merged-tree evidence plus full feature-head CI, not a claim of full CI on the final merge commit. +- No local test suite, typecheck or build ran. Pushes used --no-verify. Lower-layer CI was deferred until a final-head failure, and no cancelled or missing check was presented as passing. +- Public documentation was source-reviewed. This axis did not run a documentation build. + +## Diagnostic disposition + +One Mac shard reached its 20-minute limit after an unchanged history-lock test. The full Mac control had two Cursor decoded-frame-silence assertion failures; the separately annotated server-auth stream reset was intentional and its test passed. Only the unsuccessful Mac jobs were replayed, with unchanged source and limits, and they passed. The [baseline control comparison](https://github.com/lidge-jun/opencodex/actions/runs/34069848260) also passed. These observations do not establish the stall or timing root cause. No threshold increase, assertion suppression, or unrelated harness fix was included; deeper investigation remains deferred. + +## Attribution + +- Éverton Toffanetto +- 투린 +- Zig Zag + +Original author identities remain in the landed commits; this note lists names without contact addresses. + +The preceding numbered files are the historical roadmap and audits; their original _plan locations refer to the planning phase before this closeout. diff --git a/devlog/_plan/260904_raycast_integration/000_plan.md b/devlog/_plan/260904_raycast_integration/000_plan.md new file mode 100644 index 0000000000..c98701a2cd --- /dev/null +++ b/devlog/_plan/260904_raycast_integration/000_plan.md @@ -0,0 +1,121 @@ +# Raycast Custom Providers integration — plan + +Raycast (Pro-only) reads `~/.config/raycast/ai/providers.yaml` and watches it, so a +file-toggle client is the right shape. Spec: https://manual.raycast.com/ai/custom-providers. + +Decisions taken with the maintainer: + +1. Install signal is `~/.config/raycast/ai` (the directory Raycast creates on + "Reveal Providers Config"), not `Raycast.app`. +2. A non-Pro plan is a warning in status/GUI, never a refusal. +3. Every exported model declares `tools: supported: true` (same stance as Hermes: + every routed model is tool-capable). +4. Array ownership goes into the shared merge/classifier layer as a path-segment + selector rather than a Raycast-only patcher. `structure/09_client-integrations.md` + forbids a special case that lives only in the writer or only in status; a + selector segment that `readPath`/`setPath`/`deletePath` all understand is the + one way both keep agreeing. + +## Raycast file shape + +```yaml +providers: + - id: opencodex # <- our one owned sequence item + name: OpenCodex + base_url: http://127.0.0.1:10100/v1 + models: + - id: anthropic/claude-opus-5 + name: Claude Opus 5 + context: 200000 + abilities: + temperature: { supported: true } + vision: { supported: true } + system_message: { supported: true } + tools: { supported: true } + reasoning_effort: { supported: false } +``` + +No `api_keys`: loopback is unauthenticated and the file has no env interpolation, +so the client is `loopbackOnly: true`. + +## Pro signal (macOS) + +`defaults read com.raycast.macos.v1 subscriptions_active` → `1` / `0`. Read via +`Bun.spawnSync`, not by parsing the binary plist (cfprefsd caches). Windows: `unknown`. + +## Work packages (disjoint files, run in parallel) + +| WP | Files | +|---|---| +| 1 merge selector | `src/integrations/merge.ts`, `src/integrations/state.ts`, `tests/integrations-merge.test.ts` | +| 2 client | `src/clients/config-export.ts`, `src/integrations/registry.ts`, `src/cli/registry.ts`, `src/cli/help.ts`, `tests/raycast-client.test.ts`, list-assertion tests | +| 3 sync fan-out | `src/integrations/owned-refresh.ts`, `src/cli/dispatch.ts`, `src/server/management/config-routes.ts`, `src/cli/index.ts`, `tests/sync-client-integrations.test.ts` | +| 4 detect + API + GUI | `src/integrations/raycast-detect.ts`, `src/server/management/integration-routes.ts`, `src/cli/integrations.ts`, `gui/**`, i18n | +| 5 docs | `docs-site/**` | + +### WP1 — `[field=value]` path segment + +```ts +// merge.ts +const ARRAY_SELECTOR = /^\[([A-Za-z_][A-Za-z0-9_]*)=([^\]]+)\]$/u; +export type PathSegment = { kind: "key"; key: string } | { kind: "select"; field: string; value: string }; +export function parseSegment(raw: string): PathSegment; +export class AmbiguousSelectorError extends Error {} +``` + +- `setPath`: a `select` segment addresses the element of an array whose + `item[field] === value`. Missing parent → `[]` is created (recorded by + `createdContainerPaths`). Match found → replace in place; none → push; ≥2 → + throw `AmbiguousSelectorError` (writer maps it to `unsafe` alongside + `UnserializableValueError`). +- `deletePath`: splice the match; an emptied array we created is pruned by the + existing `createdContainers` walk. +- `state.ts readPath`: `select` → `Array.prototype.find`. Because the classifier + and the writer share this one function, status and mutation cannot disagree. +- `blockedContainerPath`: a non-array, non-undefined value where a `select` + segment expects an array is blocked (`providers: {}` written by the user). +- `createdContainerPaths`: unchanged join rule; a `select` segment is never a + container prefix on its own. +- A key-only path is byte-for-byte the old behaviour; the twelve existing clients + do not change. + +### WP2 — client registration + +`config-export.ts`: `"raycast"` in `ExportClientId`; `raycastAiDir(env, home)` = +`join(home, ".config", "raycast", "ai")` (Raycast ignores XDG; same path on Windows); +`raycastConfigPath` = `…/providers.yaml`; types `RaycastAbility`, +`RaycastModelEntry`, `RaycastProviderEntry`, `RaycastGeneratedConfig`; +`buildRaycastClientConfig(ctx)` over `normalizeExportModels(ctx.models)` with +`exportModelLabel(model)` as `name`, `contextWindow` → `context`, abilities: +`temperature: !(reasoningEfforts?.length)`, `vision: inputModalities?.includes("image") ?? false`, +`system_message: true`, `tools: true`, `reasoning_effort: (reasoningEfforts?.length ?? 0) > 0`. +`buildRaycastContribution` = `singleFragment("raycast", ["providers", "[id=opencodex]"], providers[0])`. +`summarizeRaycast` finds the `opencodex` item. `EXPORT_CLIENTS.raycast`: +`filename: "raycast-providers.yaml"`, `format: "yaml"`, `apiKeyEnv: ""`, `loopbackOnly: true`. + +`registry.ts`: `configPath: raycastConfigPath`, `detectDir: raycastAiDir`, no +`sourcePreservingYaml` (that patcher handles block-map leaves only), no `writerLock`. + +### WP3 — sync fan-out + +Raycast joins the shared `refreshOwnedCatalogIntegrations` coordinator. Model +selection changes use its default `["pi", "aside", "raycast"]` set; +`POST /api/sync` uses `["mcode", "pi", "aside", "raycast"]`; direct CLI sync +updates `["mcode", "pi", "raycast"]` locally and keeps Aside behind its +server-owned multi-profile route. Startup and ensure refresh the owned Raycast +catalog after the Codex catalog publishes, using the live port. + +### WP4 — detection, API, GUI + +`raycast-detect.ts` mirrors `cursor-detect.ts` (injectable deps, read-only): +`RaycastPlan = "pro" | "free" | "unknown"`, `detectRaycast(deps)` → +`{ appPath, aiDirPresent, plan }`. `GET /api/client-integrations/raycast` +adds `raycast: { plan, appPath, aiDirPresent }` to the envelope (only for this +client). `ocx integration client status --client raycast` prints `plan`. GUI: +every surface in `devlog/_fin/260831_aside_client_and_integrations_ux/002_registration_checklist.md` +plus one `RaycastPlanNotice` shown when `plan !== "pro"` or `!aiDirPresent`. + +### WP5 — docs + +`guides/integrations.md` row + paragraph (Pro, reveal-first), `reference/cli/agents.md`, +translated locales, `bun run build` in `docs-site`. diff --git a/devlog/_plan/260907_code_mode_host_contract/000_plan.md b/devlog/_plan/260907_code_mode_host_contract/000_plan.md new file mode 100644 index 0000000000..0f9a8057fb --- /dev/null +++ b/devlog/_plan/260907_code_mode_host_contract/000_plan.md @@ -0,0 +1,197 @@ +# 000 — Code-mode host contract for routed models: plan + +Revision 2 after audit round 1 (gpt-6-astra explorer, VERDICT: FAIL, 8 blockers). Synthesis and +dispositions are in the "Audit round 1" section at the end; the body below is the amended plan. + +## Loop-spec + +- Loop archetype: satisfy-spec repair (verifier-defined). No optimization loop. +- Trigger: xai/grok-4.6 retrospective (2026-09-07) on a routed native-Responses Codex session. The + model hit Codex host contracts that OpenCodex neither states before the first call nor explains + after the failure, then abandoned the right tools for shell heredocs and sleep loops. +- Goal: a routed non-OpenAI model in Codex code mode learns the host's argument shape and waiting + protocol up front, and when it still trips, the exec result names the rule it broke. +- Non-goals: rewriting model JavaScript; new payload repair (`apply-patch-envelope.ts`, + `code-mode-helper-compat.ts`, `bridge.ts`, `parser.ts` untouched); OpenAI/ChatGPT destinations + or compaction requests; Lab; GUI; version bumps; annotation on Anthropic/Google/OpenAI-chat/ + command-code result paths (they have no exec-result seam today). No local test suite, typecheck, + build, or install in this worktree (user instruction). Merge/release out of scope. +- Verifier: hosted `.github/workflows/ci.yml` on the exact head of each pushed work-phase (PR + `pull_request` trigger; test shards 1-4 + `gates` typecheck/privacy). Local: NOT RUN. +- Stop condition: PR ready-for-review against `dev` with exact-head CI green and receipt bound. +- Memory artifact: this unit, the bound goalplan + `.codexclaw/goalplans/code-mode-host-contract-for-routed-models-shared/`, and the PR body. +- Expected terminal outcomes: DONE (PR open, CI green); NOOP ruled out below; BLOCKED if + GitHub/CI fails after retries; UNSAFE if a change would rewrite JavaScript or widen a fail-open + write; NEEDS_HUMAN for merge. +- Escalation: main reclaims a slice after two distinct agents fail its packet; pushing a slice to + a worker requires a P-phase amendment. Every dispatch uses `gpt-6-astra`, reasoning `high`. +- HOTL bounds: write scope is this worktree on `codex/code-mode-host-contract`; push of that branch + and PR creation are authorized by the user; no token or wall-clock budget was set. + +## Incident and live host evidence + +Host strings were read from the installed Codex 0.153.2 binaries (`strings -n 8` on `codex` and +`codex-code-mode-host`) and then re-probed live from this session's own code-mode isolate: + +| Probe (`tools.apply_patch` argument) | Host result | +|---|---| +| `{ input: "*** Begin Patch…" }` (object) | throws `tool \`apply_patch\` expects a string input` | +| `"*** Begin Patch ***\n…\n*** End Patch ***"` (decorated) | throws `apply_patch verification failed: invalid patch: The first line of the patch must be '*** Begin Patch'` | +| `"\n\n*** Begin Patch\n…"` (leading newlines) | ACCEPTED, file written | +| `" *** Begin Patch\n…"` (indented) | ACCEPTED, file written | +| `"…*** End Patch\n\n"` (trailing newlines) | ACCEPTED | +| `import x from "y"` in the isolate | `Unsupported import in exec: ` (host string; capital U) | + +So the Grok report's "blank line before the template literal" was not itself the rejection; the +"first line" error fires for a decorated marker, a code fence, prose, or any non-marker first +token. The pre-call sentence and recovery hints below describe exactly that and do not claim the +host rejects surrounding whitespace. + +The fourth contract has no error string: a shell `for i in 1..20; sleep 1` inside one +`exec_command` simply blocks the call, while the host's own protocol is to let the call return a +`session_id` and poll with `tools.write_stdin({session_id, chars: ""})`. + +After the two apply_patch rejections the model stopped using apply_patch entirely — the pattern +`devlog/_fin/260813_260813-nudge-apply-patch-forbid/` recorded: once the tool feels broken, routed +models fall back to `python3`/`cat <` | +| two `*** End Patch` lines (envelope pasted twice) | throws `The last line of the patch must be '*** End Patch'` | + +Conclusion carried into the wording: the host strips surrounding whitespace before checking the +marker lines, so "no leading newline" is not a rule. The rule is that the first non-blank line is +exactly `*** Begin Patch` and the last is exactly `*** End Patch`, undecorated. + +## Long-running command protocol + +The `exec_command` schema in this session: `yield_time_ms` "Wait before yielding output. Defaults to +10000 ms; effective range is 250-30000 ms"; `session_id` "Session identifier to pass to write_stdin +when the process is still running". `write_stdin`: `chars` "Defaults to empty, which polls without +writing"; empty polls wait 5000-300000 ms. A shell `for i in 1..20; sleep 1` inside one call +produces no error string; it simply spends the call's yield budget blocked. + +## Isolate globals + +The `exec` description in this session lists `exit`, `text`, `image`, `audio`, `generatedImage`, +`store`/`load`, `notify`, `setTimeout`/`clearTimeout`, `ALL_TOOLS`, `yield_control`, plus `tools.*`. +The list varies by client version, which is why the pre-call sentence names a few examples and +defers to the description rather than enumerating. + diff --git a/devlog/_plan/260907_code_mode_host_contract/010_pre_call_contract.md b/devlog/_plan/260907_code_mode_host_contract/010_pre_call_contract.md new file mode 100644 index 0000000000..c6d7e6b9aa --- /dev/null +++ b/devlog/_plan/260907_code_mode_host_contract/010_pre_call_contract.md @@ -0,0 +1,186 @@ +# 010 — wp1: pre-call host contract sentence and its three injection sites + +Depends on 000_plan.md (rev 2). Class C2. Anchors verified against ec799db26. Ends with an +authorized push and a draft PR so exact-head CI exists for this and later heads. + +## MODIFY `src/adapters/exec-tool-result-normalize.ts` + +Insert after the `CODE_MODE_RESULT_ECHO_SENTENCE` declaration (its closing `;` is at line 116): + +```ts + +/** + * Host rules a routed model most often breaks on its first code-mode edit or wait, stated BEFORE + * the call. Wording tracks the Codex host (0.153.2), probed live on 2026-09-07: a non-string + * argument to `apply_patch` throws "expects a string input"; a body whose first line is not the + * bare marker (decorated `*** Begin Patch ***`, a code fence, prose) throws "The first line of the + * patch must be '*** Begin Patch'" — surrounding newlines are tolerated; ES imports throw + * "Unsupported import in exec"; a command that outlives `yield_time_ms` returns `session_id` for + * `write_stdin` polling. xai/grok-4.6 hit the first two, abandoned apply_patch for heredoc writes, + * blocked a turn in a shell sleep loop, and died once on an import. None of that is repairable in + * the proxy (devlog/_plan/260905_apply_patch_envelope_gap/010 MODE B); it is a contract the proxy + * had not stated. + */ +export const CODE_MODE_HOST_CONTRACT_SENTENCE = + "Host contract for the nested helpers: `tools.apply_patch(patch)` takes exactly one string, never an object such as `{input: ...}`; the patch text opens with the bare marker line `*** Begin Patch` and closes with the bare marker line `*** End Patch`, written without a code fence, prose, or extra asterisks on those lines (blank lines or indentation around the markers are tolerated; a decorated or missing marker is rejected). The isolate has no `import`, `require`, or module loader; use the globals the exec tool description lists (for example `tools`, `text`, `notify`, `store`/`load`, `ALL_TOOLS`). For a command that may outlive `yield_time_ms`, let `tools.exec_command` return a `session_id` and poll it on later calls with `tools.write_stdin({session_id, chars: \"\"})` instead of blocking a shell in a sleep loop."; +``` + +## MODIFY `src/adapters/tool-catalog-nudge.ts` + +Line 8 BEFORE: +```ts +import { CODE_MODE_RESULT_ECHO_SENTENCE } from "./exec-tool-result-normalize"; +``` +AFTER: +```ts +import { CODE_MODE_HOST_CONTRACT_SENTENCE, CODE_MODE_RESULT_ECHO_SENTENCE } from "./exec-tool-result-normalize"; +``` + +Line 124 is one 1035-byte string ending in `rejected by Codex before the file is touched."`. +BEFORE (tail): +```ts +OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched." +``` +AFTER (tail): +```ts +OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched. " + CODE_MODE_HOST_CONTRACT_SENTENCE +``` +The flat-catalog branch (`"If a listed tool exposes nested helpers such as a tools.* API…"`) is unchanged. + +## MODIFY `src/adapters/cursor/tool-guidance.ts` + +Line 2 BEFORE: +```ts +import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; +``` +AFTER: +```ts +import { CODE_MODE_HOST_CONTRACT_SENTENCE, CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; +``` + +Lines 189-191 BEFORE (4-space indent as in source): +```ts + codeMode + ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers." + : undefined, +``` +AFTER: +```ts + codeMode + ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE + : undefined, +``` + +## MODIFY `src/adapters/responses-code-mode.ts` + +Line 3 BEFORE: +```ts +import { CODE_MODE_RESULT_ECHO_SENTENCE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize"; +``` +AFTER: +```ts +import { CODE_MODE_HOST_CONTRACT_SENTENCE, CODE_MODE_RESULT_ECHO_SENTENCE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize"; +``` + +Insert before `/** Native routed Responses needs the same first-call/output contract… */` (line 32): +```ts +/** Append each sentence a replayed instructions string does not already carry, in order. */ +function appendMissing(instructions: string, sentences: readonly string[]): string { + return sentences.reduce( + (acc, sentence) => acc.includes(sentence) ? acc : [acc, sentence].filter(Boolean).join("\n\n"), + instructions, + ); +} +``` + +Lines 45-46 BEFORE (4-space indent): +```ts + instructions: instructions.includes(CODE_MODE_RESULT_ECHO_SENTENCE) + ? instructions : [instructions, CODE_MODE_RESULT_ECHO_SENTENCE].filter(Boolean).join("\n\n"), +``` +AFTER: +```ts + instructions: appendMissing(instructions, [CODE_MODE_RESULT_ECHO_SENTENCE, CODE_MODE_HOST_CONTRACT_SENTENCE]), +``` + +The exec `input` parameter description (line 27) keeps only the echo sentence; the contract belongs in +`instructions`, which the existing test asserts byte-exactly. + +Activation: routed native Responses request whose visible catalog has a bare freeform `exec` and no +bare shell bridge, non-OpenAI destination, not a compaction request (gate at lines 35-37). +Observable: `wire.instructions` ends with the contract sentence. + +## TESTS (in place; no new file in wp1) + +`tests/adapters/tool-catalog-nudge.test.ts` +- Line 7 import becomes `import { CODE_MODE_HOST_CONTRACT_SENTENCE, CODE_MODE_RESULT_ECHO_SENTENCE, EMPTY_EXEC_OUTPUT_MESSAGE } from "../../src/adapters/exec-tool-result-normalize";` +- In `"defines nested helper names as non-callable unless separately listed"` append: +```ts + // The host contract rides the same code-mode branch as the echo rule (Grok 2026-09-07). + expect(note).toContain(CODE_MODE_HOST_CONTRACT_SENTENCE); + expect(note).toContain("takes exactly one string"); + expect(note).toContain("write_stdin({session_id, chars: \"\"})"); +``` +- In `"keeps the generic nested-helper parent-tool rule when exec is not listed"` append: +```ts + expect(note).not.toContain("Host contract for the nested helpers"); +``` + +`tests/providers/cursor/cursor-tool-definitions.test.ts` +- In `"teaches the nested-helper contract instead of a top-level shell bridge"` (starts line 754) append + after the `"OpenCodex does not rewrite JavaScript inside exec"` assertion: +```ts + expect(note).toContain("Host contract for the nested helpers"); + expect(note).toContain("takes exactly one string"); + expect(note).toContain("write_stdin"); +``` +- In `"keeps flat-catalog shell-bridge guidance when a bare bridge is advertised"` append: +```ts + expect(note).not.toContain("Host contract for the nested helpers"); +``` + +`tests/responses/openai-responses-passthrough.test.ts` +- Line 6 import adds `CODE_MODE_HOST_CONTRACT_SENTENCE`. +- Line 54 BEFORE: +```ts + expect(wire.instructions).toBe(`Keep this instruction.\n\n${CODE_MODE_RESULT_ECHO_SENTENCE}`); +``` + AFTER: +```ts + expect(wire.instructions).toBe(`Keep this instruction.\n\n${CODE_MODE_RESULT_ECHO_SENTENCE}\n\n${CODE_MODE_HOST_CONTRACT_SENTENCE}`); +``` +- New test after `"does not duplicate instructions or explain an unpaired or unrelated result"`: +```ts + test("a replayed body that already carries the echo rule gains only the missing contract sentence", () => { + const body = { ...raw(), instructions: `Keep this instruction.\n\n${CODE_MODE_RESULT_ECHO_SENTENCE}` }; + const parsed = parseRequest(body); + const first = normalizeResponsesCodeMode(body, parsed, routed) as typeof body; + expect(first.instructions).toBe(`${body.instructions}\n\n${CODE_MODE_HOST_CONTRACT_SENTENCE}`); + expect(first.instructions.split(CODE_MODE_RESULT_ECHO_SENTENCE).length).toBe(2); + const second = normalizeResponsesCodeMode(first, parsed, routed) as typeof body; + expect(second.instructions).toBe(first.instructions); + }); +``` +- In `"official OpenAI and non-code-mode catalogs remain untouched"`, inside the `for (const native…)` loop + append `expect(JSON.stringify(wire)).not.toContain("Host contract for the nested helpers");`. + +`tests/providers/kiro/kiro-adapter.test.ts` +- In `"names ALL_TOOLS when a freeform exec is advertised without a bare shell bridge"` (line 1817) append: +```ts + // Survives Kiro's 16 384-char injected-instruction bound on the real wire prompt. + expect(content).toContain("Host contract for the nested helpers"); +``` + +## Delivery for this phase + +`git add` only the files above; `git diff --cached --stat` first; commit `--no-verify`; then +`git push --no-verify -u origin codex/code-mode-host-contract` and +`gh pr create --draft --base dev --title "fix(code-mode): state the host contract for nested helpers and annotate host failures" --body-file .tmp/pr-body.md` +(body per template; Verification section says local checks NOT RUN, hosted CI is the verifier; +wp2/wp3 will extend it). + +## Verification (C, hosted only) + +NOT RUN locally by instruction. Poll `gh run list --branch codex/code-mode-host-contract --json databaseId,headSha,status,conclusion,name` +in short `exec_command` calls; when the Cross-platform CI run for `git rev-parse HEAD` completes, +`cxc receipt test --session --cwd -- gh run view --exit-status`. diff --git a/devlog/_plan/260907_code_mode_host_contract/020_post_hoc_annotation.md b/devlog/_plan/260907_code_mode_host_contract/020_post_hoc_annotation.md new file mode 100644 index 0000000000..86430340c0 --- /dev/null +++ b/devlog/_plan/260907_code_mode_host_contract/020_post_hoc_annotation.md @@ -0,0 +1,388 @@ +# 020 — wp2: post-hoc annotation of host failures on exec results + +Depends on 010 (same module, same wording). Class C2. Anchors verified against ec799db26 plus +the wp1 delta. Ends with an authorized push; the draft PR from wp1 picks up the new head. + +## MODIFY `src/adapters/exec-tool-result-normalize.ts` + +Insert after `CODE_MODE_HOST_CONTRACT_SENTENCE` (added in wp1): + +```ts + +/** + * Post-hoc half of the host contract: the four host strings a routed model reads inside a + * non-error exec result, each paired with the rule it broke. Matched case-insensitively because + * the host writes "Unsupported import in exec: " while Cursor's earlier marker was + * lowercase; one table, one owner, so this text and the pre-call sentence cannot drift. + */ +export const CODE_MODE_HOST_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [ + { + marker: "expects a string input", + guidance: "tools.apply_patch takes exactly one string argument; pass the patch text itself, not an object such as {input: ...}.", + }, + { + marker: "the first line of the patch must be", + guidance: "The patch text must open with the bare marker line `*** Begin Patch`: no code fence, prose, or extra asterisks on that line (blank lines or indentation before it are tolerated).", + }, + { + marker: "the last line of the patch must be", + guidance: "The patch text must close with the bare marker line `*** End Patch`: no trailing text or extra asterisks on that line (blank lines after it are tolerated).", + }, + { + marker: "unsupported import in exec", + guidance: "Imports are not available in this exec context; use the injected globals (tools, text, notify, store, load, ALL_TOOLS) instead.", + }, +]; + +/** Prefix of every recovery line this module appends; callers use it to recognise replayed annotations. */ +export const CODE_MODE_HOST_RECOVERY_PREFIX = "[recovery: "; + +/** Namespaces under which Cursor displays Codex's own Responses tools (see cursor/tool-naming.ts). */ +const CODEX_RESPONSES_DISPLAY_NAMESPACES: ReadonlySet = new Set(["opencodex-responses", "mcp__opencodex-responses"]); +/** Flattened spellings of the same code-mode exec when a client folds the namespace into the name. */ +const CODEX_CODE_MODE_EXEC_ALIASES: ReadonlySet = new Set(["exec", "mcp__opencodex-responses__exec", "mcp_opencodex-responses_exec"]); + +/** + * The code-mode `exec` tool by NAME — bare, or under Codex's own `opencodex-responses` display + * namespace, matched exactly. The four host strings above originate only in that isolate, so flat + * shell bridges (`exec_command`, `shell`, …) and every other namespace (`mcp__docker`, + * `mcp__foreign-opencodex-responses`) are excluded: an unrelated server's output that quotes the + * phrase must not receive Codex guidance. Narrower than `isCodexExecBridgeTool` on purpose; the + * empty-output repair keeps the wider gate. Callers that KNOW the catalog shape (Kiro's + * `codeModeExecName`, the Responses body gate) add that check on top; this predicate alone cannot + * tell a structured tool named `exec` from the freeform one. + */ +export function isCodexCodeModeExecResult(toolName?: string, toolNamespace?: string): boolean { + if (!toolName) return false; + const lower = toolName.toLowerCase(); + if (toolNamespace !== undefined) return CODEX_RESPONSES_DISPLAY_NAMESPACES.has(toolNamespace) && lower === "exec"; + return CODEX_CODE_MODE_EXEC_ALIASES.has(lower); +} + +/** + * Append a one-line recovery hint when a code-mode exec result carries a known host failure string. + * Returns undefined when the tool is not the code-mode exec, no marker matches, or a recovery line is + * already present (a replayed annotated result must not grow a second one). Never touches error + * status: the host already decided whether the call failed. + */ +export function annotateCodeModeHostFailure( + text: string, + options: { toolName?: string; toolNamespace?: string } = {}, +): string | undefined { + if (!isCodexCodeModeExecResult(options.toolName, options.toolNamespace)) return undefined; + if (text.includes(CODE_MODE_HOST_RECOVERY_PREFIX)) return undefined; + const lower = text.toLowerCase(); + const hit = CODE_MODE_HOST_FAILURE_GUIDANCE.find(({ marker }) => lower.includes(marker)); + return hit ? `${text}\n${CODE_MODE_HOST_RECOVERY_PREFIX}${hit.guidance}]` : undefined; +} +``` + +Flat shell tools are deliberately not annotated: the strings come from the code-mode host, and the +"flat catalogs untouched" statement in the docs is therefore literally true. + +## MODIFY `src/adapters/responses-code-mode.ts` + +Line 3 import gains `annotateCodeModeHostFailure`. + +Line 55 BEFORE (6-space indent): +```ts + const normalized = text === undefined ? undefined : normalizeEmptyExecToolResultText(text, { toolName: "exec" }); +``` +AFTER: +```ts + const normalized = text === undefined + ? undefined + : normalizeEmptyExecToolResultText(text, { toolName: "exec" }) + ?? annotateCodeModeHostFailure(text, { toolName: "exec" }); +``` +Activation: paired `custom_tool_call_output` whose text contains `\`apply_patch\` expects a string input`; +observable: output ends with the recovery line, `input[0]` is the same object reference. + +## MODIFY `src/adapters/kiro.ts` + +Line 47 BEFORE: +```ts +import { EMPTY_EXEC_OUTPUT_MESSAGE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize"; +``` +AFTER: +```ts +import { EMPTY_EXEC_OUTPUT_MESSAGE, annotateCodeModeHostFailure, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize"; +``` + +Lines 758-771 BEFORE (6-space indent): +```ts + const normalizedExecText = normalizeEmptyExecToolResultText(text, { + toolName: tr.toolName, + toolNamespace: tr.toolNamespace, + }); + const resultText = normalizedExecText ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE); + const images = extractKiroImages(tr.content); + const toolUseId = normalizeToolId(tr.toolCallId); + const call = priorCalls.get(toolUseId); + if (!call || call.rawId !== tr.toolCallId) { + throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`); + } + // Keep real whitespace and failed wrappers, but no empty-success wrapper boilerplate. + const rawGroupText = text.length > 0 && (!text.trim() || normalizedExecText !== EMPTY_EXEC_OUTPUT_MESSAGE) + ? text : undefined; +``` +AFTER: +```ts + const execOptions = { toolName: tr.toolName, toolNamespace: tr.toolNamespace }; + const normalizedExecText = normalizeEmptyExecToolResultText(text, execOptions); + // A host failure string inside a non-empty exec result gets the rule it broke appended, but + // only when this request's emitted catalog is genuinely code mode (`codeModeExecName` above): + // a structured tool named exec, or exec beside a shell bridge, never ran the isolate. This is + // the only substitution the grouping path below also carries: whitespace and empty/failed + // wrappers keep their existing raw policy. + const annotatedExecText = normalizedExecText === undefined && codeModeExecName !== undefined + ? annotateCodeModeHostFailure(text, execOptions) + : undefined; + const resultText = normalizedExecText ?? annotatedExecText ?? (text.trim() ? text : KIRO_EMPTY_TOOL_RESULT_MESSAGE); + const images = extractKiroImages(tr.content); + const toolUseId = normalizeToolId(tr.toolCallId); + const call = priorCalls.get(toolUseId); + if (!call || call.rawId !== tr.toolCallId) { + throw new Error(`Kiro history contains an orphaned tool result for call ${JSON.stringify(tr.toolCallId)}`); + } + // Keep real whitespace and failed wrappers, but no empty-success wrapper boilerplate. + const rawGroupText = text.length > 0 && (!text.trim() || normalizedExecText !== EMPTY_EXEC_OUTPUT_MESSAGE) + ? (annotatedExecText ?? text) : undefined; +``` +`annotatedExecText` is defined only when `normalizedExecText` is undefined, i.e. the text is neither an +empty-success nor a failed-empty wrapper, so every existing grouping expectation +(`kiro-adapter.test.ts:1209` whitespace, `1252` raw failed wrapper) is unchanged by construction. + +## MODIFY `src/adapters/cursor/tool-result-normalize.ts` + +Imports (lines 12-18) gain `CODE_MODE_HOST_RECOVERY_PREFIX`, `annotateCodeModeHostFailure` and +`isCodexCodeModeExecResult`. `RUNTIME_FAILURE_GUIDANCE` (lines 50-67) and its +loop (lines 107-113) stay byte-identical: Cursor's marker semantics, case sensitivity and +`isError:true` policy are its own. + +Lines 97-106 BEFORE (2-space indent): +```ts + if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && isEmptyOrFailedExecWrapper(text.trim())) { + return { + // A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success + // would erase the only failure signal. Text classification stays separate from Cursor's + // isError policy, which the Computer Use branch above owns. + text: isFailedEmptyExecWrapper(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE, + isError: false, + changed: true, + }; + } +``` +AFTER (append one branch directly after that block): +```ts + if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && isEmptyOrFailedExecWrapper(text.trim())) { + return { + // A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success + // would erase the only failure signal. Text classification stays separate from Cursor's + // isError policy, which the Computer Use branch above owns. + text: isFailedEmptyExecWrapper(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE, + isError: false, + changed: true, + }; + } + // A host failure string inside a code-mode exec result gets the rule it broke appended, with + // Cursor's isError decision left exactly as the caller passed it. A replayed result that already + // carries a recovery line returns here unchanged: falling through would let the legacy loop + // below match the lowercase import marker a second time and flip isError. + if (isCodexCodeModeExecResult(options.toolName, options.toolNamespace)) { + if (text.includes(CODE_MODE_HOST_RECOVERY_PREFIX)) return { text, isError, changed: false }; + const hostFailure = annotateCodeModeHostFailure(text, options); + if (hostFailure !== undefined) return { text: hostFailure, isError, changed: true }; + } +``` +The existing `unsupported import in exec` row in `RUNTIME_FAILURE_GUIDANCE` still serves node_repl / +Computer Use tools; for the code-mode exec the new branch runs first, carries the shared hint, and +terminates replay before the legacy loop can see it. + +## NEW `tests/adapters/exec-tool-result-normalize.test.ts` + +```ts +import { describe, expect, test } from "bun:test"; +import { + CODE_MODE_HOST_CONTRACT_SENTENCE, + CODE_MODE_HOST_FAILURE_GUIDANCE, + annotateCodeModeHostFailure, +} from "../../src/adapters/exec-tool-result-normalize"; + +// Live host strings (Codex 0.153.2, probed 2026-09-07) and the rule each one names. The pre-call +// sentence and these rows are one contract in one module; a model must never be told one thing +// before the call and another after. +describe("code-mode host failure annotation", () => { + test.each(CODE_MODE_HOST_FAILURE_GUIDANCE.map(row => [row.marker, row.guidance] as const))( + "annotates an exec result carrying %p regardless of case", + (marker, guidance) => { + const text = `Script failed\nWall time 0.1 seconds\nOutput:\nError: ${marker.toUpperCase()}`; + expect(annotateCodeModeHostFailure(text, { toolName: "exec" })).toBe(`${text}\n[recovery: ${guidance}]`); + }, + ); + + test("matches the host's real capitalisation and argument text", () => { + expect(annotateCodeModeHostFailure("Unsupported import in exec: node:fs", { toolName: "exec" })).toContain("injected globals"); + expect(annotateCodeModeHostFailure("Script error:\ntool `apply_patch` expects a string input", { toolName: "exec" })).toContain("exactly one string"); + expect(annotateCodeModeHostFailure( + "apply_patch verification failed: invalid patch: The first line of the patch must be '*** Begin Patch'", + { toolName: "exec" }, + )).toContain("bare marker line `*** Begin Patch`"); + }); + + test("leaves non-exec tools, shell bridges, foreign namespaces, non-matching text and already-annotated text alone", () => { + expect(annotateCodeModeHostFailure("expects a string input", { toolName: "read_file" })).toBeUndefined(); + // Flat shell bridges never run the isolate, so the four strings cannot be theirs. + expect(annotateCodeModeHostFailure("expects a string input", { toolName: "exec_command" })).toBeUndefined(); + // A foreign MCP server's own exec is not Codex's, even when its output quotes the phrase, and a + // namespace that merely CONTAINS the provider name is still foreign. + expect(annotateCodeModeHostFailure("expects a string input", { toolName: "exec", toolNamespace: "mcp__docker" })).toBeUndefined(); + expect(annotateCodeModeHostFailure("expects a string input", { toolName: "exec", toolNamespace: "mcp__foreign-opencodex-responses" })).toBeUndefined(); + // Codex's own display namespaces and flattened aliases for the same code-mode tool still count. + for (const options of [ + { toolName: "exec", toolNamespace: "opencodex-responses" }, + { toolName: "exec", toolNamespace: "mcp__opencodex-responses" }, + { toolName: "mcp__opencodex-responses__exec" }, + { toolName: "mcp_opencodex-responses_exec" }, + ]) { + expect(annotateCodeModeHostFailure("expects a string input", options)).toContain("[recovery:"); + } + expect(annotateCodeModeHostFailure("all good", { toolName: "exec" })).toBeUndefined(); + const once = annotateCodeModeHostFailure("expects a string input", { toolName: "exec" }); + if (!once) throw new Error("expected one annotation"); + expect(annotateCodeModeHostFailure(once, { toolName: "exec" })).toBeUndefined(); + }); + + test("every failure row is a rule the pre-call sentence already states", () => { + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).toContain("takes exactly one string"); + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).toContain("`*** Begin Patch`"); + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).toContain("`*** End Patch`"); + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).toContain("no `import`"); + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).toContain("write_stdin"); + // Never shows the decorated marker as a copyable literal (same rule as the nudge tests). + expect(CODE_MODE_HOST_CONTRACT_SENTENCE).not.toContain("*** Begin Patch ***"); + }); +}); +``` + +Register in `scripts/test-layout/layout.json` `explicit` between +`"empty-tool-output-annotation.test.ts": "adapters",` (line 620) and its successor: +`"exec-tool-result-normalize.test.ts": "adapters",`; same key/value in +`tests/fixtures/test-layout-expected.json` in alphabetical position. The name matches no regex seed +(`"adapters"` seed is `^(?:bridge\.test\.ts|buffered|identity|run|tool|translator)-`), so the explicit +entry is required and `tests/test-layout-tooling.test.ts` names it if missing. + +## Updated tests + +`tests/responses/openai-responses-passthrough.test.ts` — add inside the code-mode describe: +```ts + test("annotates a paired exec result that carries a host failure string without touching the program", () => { + const failure = "Script failed\nWall time 0.1 seconds\nOutput:\nScript error:\ntool `apply_patch` expects a string input"; + const body = raw(failure); + const wire = JSON.parse(createResponsesPassthroughAdapter(routed).buildRequest(parseRequest(body)).body); + expect(wire.input[1].output).toBe(`${failure}\n[recovery: tools.apply_patch takes exactly one string argument; pass the patch text itself, not an object such as {input: ...}.]`); + expect(JSON.parse(wire.input[0].arguments).input).toBe(body.input[0].input); + // Replayed history already carrying the hint is not annotated twice: the output item and the + // program keep their identity, and a second pass over the normalized body is a deep no-op. + const replayed = raw(wire.input[1].output); + const once = normalizeResponsesCodeMode(replayed, parseRequest(replayed), routed) as typeof replayed; + expect(once.input[1]).toBe(replayed.input[1]); + expect(once.input[0]).toBe(replayed.input[0]); + expect(normalizeResponsesCodeMode(once, parseRequest(once), routed)).toEqual(once); + }); +``` + +`tests/providers/kiro/kiro-adapter.test.ts` +- After `"an empty code-mode exec result carries the actionable reason…"` (line 323) add: +```ts + test("a code-mode exec result carrying a host failure string names the broken rule", async () => { + // freeform: the Kiro seam annotates only when the emitted catalog is genuinely code mode. + const execTool = { name: "exec", description: "Run JavaScript", freeform: true, parameters: { type: "object" } }; + const failure = "apply_patch verification failed: invalid patch: The first line of the patch must be '*** Begin Patch'"; + const messages = [ + { role: "user", content: "run it" }, + { role: "assistant", content: [{ type: "toolCall", id: "call-x", name: "exec", arguments: {} }] }, + { role: "toolResult", toolCallId: "call-x", toolName: "exec", content: failure, isError: false }, + ]; + const { body } = await createKiroAdapter(provider).buildRequest(parsedWith(messages, [execTool])); + const resultText = JSON.parse(body).conversationState.currentMessage.userInputMessage + .userInputMessageContext.toolResults[0].content[0].text; + expect(resultText).toBe(`${failure}\n[recovery: The patch text must open with the bare marker line \`*** Begin Patch\`: no code fence, prose, or extra asterisks on that line (blank lines or indentation before it are tolerated).]`); + }); + + test("a host failure string on a non-code-mode catalog stays raw", async () => { + const failure = "tool `apply_patch` expects a string input"; + const messages = [ + { role: "user", content: "run it" }, + { role: "assistant", content: [{ type: "toolCall", id: "call-x", name: "exec", arguments: {} }] }, + { role: "toolResult", toolCallId: "call-x", toolName: "exec", content: failure, isError: false }, + ]; + for (const tools of [ + // A structured tool that merely shares the name exec. + [{ name: "exec", description: "Run a shell string", parameters: { type: "object" } }], + // Freeform exec beside a bare shell bridge is the flat-catalog shape, not code mode. + [ + { name: "exec", description: "Run JavaScript", freeform: true, parameters: { type: "object" } }, + { name: "exec_command", description: "Run", parameters: { type: "object" } }, + ], + ]) { + const { body } = await createKiroAdapter(provider).buildRequest(parsedWith(messages, tools)); + const resultText = JSON.parse(body).conversationState.currentMessage.userInputMessage + .userInputMessageContext.toolResults[0].content[0].text; + expect(resultText).toBe(failure); + } + }); +``` +- In the grouped-result table (the `execResult` cases around lines 1195-1262) add one case: +```ts + { + name: "host failure chunk in a multi group carries its recovery line beside raw siblings", + id: "call-host-failure-multi", + results: [execResult("call-host-failure-multi", " "), execResult("call-host-failure-multi", "tool `apply_patch` expects a string input"), execResult("call-host-failure-multi", failedExecWrapper)], + content: [{ text: " " }, { text: "tool `apply_patch` expects a string input\n[recovery: tools.apply_patch takes exactly one string argument; pass the patch text itself, not an object such as {input: ...}.]" }, { text: failedExecWrapper }], + status: "success", + forbidden: [EMPTY_EXEC_OUTPUT_MESSAGE, FAILED_EXEC_OUTPUT_MESSAGE, KIRO_EMPTY_TOOL_RESULT_MESSAGE], + }, +``` + This drives the grouping path with whitespace, an annotated chunk and a raw failed wrapper in one + group — the exact combination blocker 1 said the single-result test could not exercise. + +`tests/providers/cursor/cursor-toolresult-normalize.test.ts` — add after the `test.each` runtime-failure table: +```ts + test.each(["Unsupported import in exec: node:fs", "unsupported import in exec: node:fs"])( + "a code-mode exec result carrying %p gains the shared hint, keeps its isError, and is not re-annotated on replay", + (payload) => { + const out = normalizeCursorToolResultText(payload, { toolName: "exec" }); + expect(out.changed).toBe(true); + expect(out.isError).toBe(false); + expect(out.text).toBe(`${payload}\n[recovery: Imports are not available in this exec context; use the injected globals (tools, text, notify, store, load, ALL_TOOLS) instead.]`); + // Replay through Responses history arrives with isError=false; the legacy lowercase marker + // row must not get a second look at it. + const replay = normalizeCursorToolResultText(out.text, { toolName: "exec", isError: false }); + expect(replay).toEqual({ text: out.text, isError: false, changed: false }); + }, + ); + + test("the legacy node_repl import row keeps its own isError policy", () => { + const out = normalizeCursorToolResultText("unsupported import in exec", { toolName: "js", toolNamespace: "mcp__node_repl" }); + expect(out.isError).toBe(true); + expect(out.text).toContain("injected globals"); + }); + + test("a non-exec tool whose successful output merely mentions a host phrase stays byte-identical", () => { + const doc = "The docs say apply_patch expects a string input."; + const out = normalizeCursorToolResultText(doc, { toolName: "read_file" }); + expect(out.changed).toBe(false); + expect(out.isError).toBe(false); + expect(out.text).toBe(doc); + }); +``` + +## Delivery for this phase + +Stage only the files above (`git diff --cached --stat` first); commit `--no-verify`; push `--no-verify`. + +## Verification (C, hosted only) + +NOT RUN locally. Exact-head Cross-platform CI on the wp2 head; receipt via +`cxc receipt test --session --cwd -- gh run view --exit-status`. diff --git a/devlog/_plan/260907_code_mode_host_contract/030_docs_and_delivery.md b/devlog/_plan/260907_code_mode_host_contract/030_docs_and_delivery.md new file mode 100644 index 0000000000..8bd40668e0 --- /dev/null +++ b/devlog/_plan/260907_code_mode_host_contract/030_docs_and_delivery.md @@ -0,0 +1,83 @@ +# 030 — wp3: SoT sync, ready-for-review, exact-head CI receipt + +Depends on 020. Class C2 for the docs. Push and PR creation are authorized by the user for this +branch ("no verify로 푸시", "pr올려봐"); the draft PR already exists from wp1. Merge is not authorized. + +## MODIFY `structure/04_transports-and-sidecars.md` + +Insert after the paragraph that ends "…or reconstruct output that the code-mode host never +emitted." (line 331), before the `[Decision Log]` that begins "목적과 의도: Keep Codex hosted web +search usable on xAI's public Responses endpoint…": + +``` +Routed code-mode turns also carry the host contract for the nested helpers, stated in the same three +injection sites as the result-emission rule (shared catalog nudge, Cursor code-mode guidance, native +routed Responses instructions): `tools.apply_patch` takes one string that opens and closes with the +bare patch marker lines (blank lines or indentation around them are tolerated; a decorated or missing +marker is rejected), the isolate has no `import`/`require`, and a command that outlives +`yield_time_ms` is polled through `write_stdin` with empty `chars` rather than a shell sleep loop. +When a code-mode exec result still carries one of the host's failure strings ("expects a string +input", "The first line of the patch must be", "The last line of the patch must be", "Unsupported +import in exec"), the native routed Responses, Kiro, and Cursor result paths append a one-line +recovery hint naming the broken rule; flat shell bridges and foreign MCP namespaces are never +annotated, Responses and Kiro additionally require the request's verified code-mode catalog, Cursor +matches the exact `exec` name under its `opencodex-responses` provider without catalog context, and +Cursor's error classification and Kiro's whitespace and failed-wrapper grouping are unchanged. Both +halves live in `src/adapters/exec-tool-result-normalize.ts` +so the pre-call and post-hoc wording cannot drift. This guidance and annotation change rewrites +neither the model's JavaScript nor its patch payload; the existing name-alias delimiter +normalization in `src/responses/code-mode-helper-compat.ts` is unchanged, and the host still rejects a +malformed call exactly as before. Anthropic, Google, OpenAI-chat and command-code result paths +have no exec-result seam today and are not annotated. + +[Decision Log] +- 목적과 의도: Stop routed models from abandoning `apply_patch` after the Codex host rejects an object argument or a decorated marker, and from blocking a turn in a shell sleep loop when the host offers `session_id` polling. +- 기존 구현 및 제약 조건: The shared nudge, Cursor guidance and native Responses instructions already carry the result-emission rule from `exec-tool-result-normalize.ts`, but none stated the helper's argument type, the marker rule, the import ban, or the polling protocol; `260905_apply_patch_envelope_gap` refused to rewrite JavaScript bodies (MODE B), so payload repair is off the table. +- 검토한 주요 대안: Repair the argument shape inside the proxy (rejected: same body ambiguity as MODE B and it turns a rejected write into a performed one); Cursor-only guidance (rejected: the incident was native routed Responses on xAI); annotate every adapter's tool results (rejected: Anthropic/Google/OpenAI-chat/command-code have no exec-result seam and would need a new one). +- 선택한 방식: One pre-call sentence and one marker→recovery table in the module that already owns the echo pair; inject the sentence at the three existing code-mode sites; annotate at the three existing exec-result seams with an exec-gated, idempotent helper that never changes error status. +- 다른 대안 대신 이 방식을 선택한 이유: The safe repair for a host contract the model broke is to state it before the call and name it after the failure; keeping both halves in one file is what keeps them consistent. +- 장점, 단점 및 영향: Code-mode system prompts grow by roughly 600 characters on routed turns; OpenAI destinations, flat catalogs and compaction requests are untouched. An exec result that legitimately prints one of the four phrases gains a recovery line, which is additive text and never an error flip. The effect on the live Grok defect rate is unmeasured until a re-probe. +``` + +## MODIFY `docs-site/src/content/docs/guides/codex-integration.md` + +Insert after the paragraph ending "…and unrelated native custom payloads stay unchanged." (line 331): + +``` +Routed code-mode turns are also told the host's rules for the nested helpers before the first +call: `tools.apply_patch` takes one string that opens and closes with the bare patch marker lines, +the isolate has no `import`, and long-running commands are polled through `write_stdin`. When a +code-mode exec result on the native routed Responses, Kiro, or Cursor path still carries one of the host's +failure messages, opencodex appends a one-line hint naming the rule. This change does not rewrite +the model's code or its patch text. +``` + +Translated locales (7 files) are not edited; the English source gains a paragraph they do not +contradict. + +## Delivery steps (t3b) + +1. Stage only `structure/04_transports-and-sidecars.md`, `docs-site/.../codex-integration.md` and this unit's + devlog; inspect `git diff --cached --stat`; commit `--no-verify`; `git push --no-verify`. +2. Rewrite the PR body (`gh pr edit --body-file .tmp/pr-body.md`) to the final template: Summary + (problem, before/after, the four host strings), Verification (hosted CI run ids per head; local + suite/typecheck/build NOT RUN by instruction), Checklist ticked truthfully. No `gui` mention. +3. Poll `gh run list --branch codex/code-mode-host-contract --json databaseId,headSha,status,conclusion,name` + in short `exec_command` calls (each < 30 s) until the Cross-platform CI run whose `headSha` equals + `git rev-parse HEAD` completes; `gh run watch` is not used inside one call. +4. Receipt at phase C: `cxc receipt test --session --cwd -- gh run view --exit-status`. +5. `gh pr ready ` only after that receipt exists. If the head moves later, a fresh run and fresh + receipt are required before any further ready claim. + +## Verification (C) + +- `gh run view --exit-status` exit 0 on the exact head; `gh pr view --json headRefOid` equals HEAD. +- `gh pr checks ` lists test 1/4..4/4, gates, storage policy, api usage as pass. +- Local suite / typecheck / build: NOT RUN (instruction). + +## D record + +Append `040_delivery_record.md` with PR number, head SHA, CI run id, per-job results, what did not +improve (LOOP-PESSIMIST-01: prose cannot force compliance; effect on real Grok defect rate is +unmeasured until a live re-probe), and the residual: Anthropic/Google/OpenAI-chat/command-code +tool-result paths do not annotate host failures because they have no exec-result seam today. diff --git a/devlog/_plan/260907_code_mode_host_contract/040_delivery_record.md b/devlog/_plan/260907_code_mode_host_contract/040_delivery_record.md new file mode 100644 index 0000000000..3c2b28e23d --- /dev/null +++ b/devlog/_plan/260907_code_mode_host_contract/040_delivery_record.md @@ -0,0 +1,100 @@ +# 040 — Delivery record: code-mode host contract + +Recorded 2026-09-07 from GitHub PR and Actions API responses. This records the delivery requested +by [030_docs_and_delivery.md](030_docs_and_delivery.md#d-record). + +## Delivered revision and CI identity + +- [PR #3854](https://github.com/lidge-jun/opencodex/pull/3854) is merged into `dev`; + GitHub records `merged_at: 2026-09-07T06:41:03Z`. +- Final PR head: `6bdcba5bff4196debf3cd159c7af3d34e35a24e0`. +- Merge commit: `ece556a6ed32dc811bd660ddd8ef9e829512457a`. +- [Pre-merge CI run 34090946313](https://github.com/lidge-jun/opencodex/actions/runs/34090946313), + attempt 1: `event: pull_request`, `head_sha: 6bdcba5bff4196debf3cd159c7af3d34e35a24e0`, + `status: completed`, `conclusion: success`; updated `2026-09-07T06:39:04Z`. +- [Merge-head CI run 34091933836](https://github.com/lidge-jun/opencodex/actions/runs/34091933836), attempt 1: + `event: push`, `head_sha: ece556a6ed32dc811bd660ddd8ef9e829512457a`, + `status: completed`, `conclusion: success`; updated `2026-09-07T06:50:18Z`. + +The pre-merge run matches the final PR head; the later push run matches the merge commit. +These are distinct CI records. This API check does not attest that the separate local receipt +required by 030 was recorded. + +## Per-job results + +Each run has 21 completed jobs: 19 success, 2 skipped. Every job has the same conclusion in both +runs. Names below are the literal Actions job names; each evidence link identifies its own run. + +| Job | Conclusion in both runs | Pre-merge evidence | Merge-head evidence | +|---|---|---|---| +| `select windows runner` | success | [job 101644191502](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644191502) | [job 101647069433](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647069433) | +| `changes` | success | [job 101644191303](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644191303) | [job 101647069779](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647069779) | +| `windows ${{ matrix.shard }}/6` | skipped | [job 101644212182](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644212182) | [job 101647096144](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647096144) | +| `macos 1/2` | success | [job 101644233998](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644233998) | [job 101647111898](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111898) | +| `api usage` | success | [job 101644234038](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234038) | [job 101647111914](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111914) | +| `storage policy` | success | [job 101644234034](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234034) | [job 101647111922](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111922) | +| `docker smoke` | success | [job 101644234277](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234277) | [job 101647111928](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111928) | +| `keyring ubuntu` | success | [job 101644234063](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234063) | [job 101647111929](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111929) | +| `test 3/4` | success | [job 101644234103](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234103) | [job 101647111932](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111932) | +| `test 4/4` | success | [job 101644234047](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234047) | [job 101647111936](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111936) | +| `keyring macos` | success | [job 101644233982](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644233982) | [job 101647111942](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111942) | +| `test 1/4` | success | [job 101644234139](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234139) | [job 101647111951](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111951) | +| `gates` | success | [job 101644233985](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644233985) | [job 101647111970](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111970) | +| `keyring windows` | success | [job 101644234037](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234037) | [job 101647111972](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111972) | +| `macos 2/2` | success | [job 101644234066](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234066) | [job 101647111974](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111974) | +| `npm-global ubuntu-latest` | success | [job 101644234059](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234059) | [job 101647111980](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111980) | +| `npm-global windows-latest` | success | [job 101644234098](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234098) | [job 101647111990](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647111990) | +| `test 2/4` | success | [job 101644234167](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234167) | [job 101647112003](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647112003) | +| `npm-global macos-latest` | success | [job 101644234033](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644234033) | [job 101647112012](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647112012) | +| `macos control` | skipped | [job 101644235362](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101644235362) | [job 101647112696](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101647112696) | +| `ci` | success | [job 101646588871](https://github.com/lidge-jun/opencodex/actions/runs/34090946313/job/101646588871) | [job 101649082610](https://github.com/lidge-jun/opencodex/actions/runs/34091933836/job/101649082610) | + +The Windows full-suite matrix was **SKIPPED in both runs**. Windows keyring create/read/delete smoke and +npm-global packaging/install/help smoke passed; those focused passes do not establish Windows +full-suite coverage. The `ci` aggregate accepts successful or skipped prerequisites, so its green +result does not turn skipped jobs into passes. On the merge-head run, `gates` includes successful Typecheck, GUI tests, +Privacy scan, skill-surface check, release-helper syntax check, and CLI help smoke; its GUI lint, +GUI build, and dashboard-preview steps were skipped. + +Evidence retrieval (read-only): + +```sh +gh api repos/lidge-jun/opencodex/pulls/3854 +gh api repos/lidge-jun/opencodex/actions/runs/34090946313 +gh api 'repos/lidge-jun/opencodex/actions/runs/34090946313/jobs?per_page=100' +gh api repos/lidge-jun/opencodex/actions/runs/34091933836 +gh api 'repos/lidge-jun/opencodex/actions/runs/34091933836/jobs?per_page=100' +``` + +## Limits and residuals + +The delivered scope is the pre-call guidance and post-hoc recovery annotations described in +[030](030_docs_and_delivery.md). Guidance cannot force model compliance, repair the model's +JavaScript or patch payload, or replace the host's validation. The effect on the live Grok defect +rate remains **unmeasured** until a live re-probe; CI success is not a defect-rate measurement. + +Anthropic, Google, OpenAI-chat, and command-code tool-result paths still lack exec-result +annotation seams and do not annotate these host failures. Existing coverage is limited to native +routed Responses, Kiro, and Cursor. + +Two public review threads were **OPEN / UNRESOLVED in the recorded 2026-09-07 audit snapshot**: GitHub's review-thread API returned +`isResolved: false` for both on 2026-09-07. The merge and green CI do not resolve these findings. +Source inspected for that snapshot was read at worktree HEAD `0fd3408b99994f74bd509975df7ee89823ddfecd`: + +- [discussion_r3947178410](https://github.com/lidge-jun/opencodex/pull/3854#discussion_r3947178410): + `src/adapters/exec-tool-result-normalize.ts:196` searches arbitrary output for a marker substring. + Successful output from a command such as `rg` or `cat` can therefore receive a misleading + recovery hint when it quotes that phrase, even though the command did not fail. The requested + host-error status/envelope or exact diagnostic check remains unimplemented at this anchor. +- [discussion_r3947178418](https://github.com/lidge-jun/opencodex/pull/3854#discussion_r3947178418): + `src/adapters/cursor/tool-result-normalize.ts:114` gates annotation on tool name/namespace + without request-catalog or freeform provenance. A structured tool named `exec` can receive + unrelated host guidance. The requested code-mode provenance check remains unimplemented at + this anchor. + +These limitations were also recorded in [000](000_plan.md). Recording them here is not a fix, +review resolution, or claim that successful output is left byte-identical. + +Local runtime, tests, typecheck, build, and install: **NOT RUN** by instruction. No live model +re-probe was performed for this record. The remote results above belong to the recorded PR head +and merge commit and do not validate later candidate documentation or test patches. diff --git a/devlog/_plan/260907_lane_c/000_plan.md b/devlog/_plan/260907_lane_c/000_plan.md new file mode 100644 index 0000000000..d26120ae29 --- /dev/null +++ b/devlog/_plan/260907_lane_c/000_plan.md @@ -0,0 +1,7 @@ +# Lane C release train roadmap + +Satisfy-spec HOTL, explicitly delegated by release-train main task. Goal: prepare five manual dependent PRs for main-session landing. No merge/release/publish/main/preview changes; no local tests, typecheck, build or install. All such checks NOT RUN. Remote Cross-platform CI dispatch lane=all at top head is the verifier. Stop after exact-head green CI, Astra review verdicts, screenshots, credit and SHA handoff; unresolved material blockers are reported with evidence. No user-specified token/cost/time bound. Tools: local scoped git/files, gh read/PR/push/CI, Astra explorer audits and browser inspection. New security findings stay in .tmp/lane-c. Main owns config-routes.ts; no edits there. Escalate cross-owner collisions; reclaim delegated slices after two distinct worker failures. + +Dependency order: roadmap → 3839 → 3841 → 3863 → 3860 → 3252/1533 → top CI and handoff. Lower-layer commit subjects include [skip ci]; stack:null. Every carry uses cherry-pick -x and source PR author Co-authored-by. Existing configuration field contracts are reused. Rollback is revert of a layer with descendant cascade, within main-authorized integration. Current source and read-only git/gh are evidence; no claimed local execution of product verifiers. Public original diffs are recorded in decade documents; private audit notes stay in scratch. + +Main steering: all gui/src/i18n/*.ts are append-only multiwriter; C adds namespaced keys at feature-section ends, never edits/deletes existing keys. Final cascade resolves append collisions. diff --git a/devlog/_plan/260907_lane_c/010_web_search.md b/devlog/_plan/260907_lane_c/010_web_search.md new file mode 100644 index 0000000000..ccb7217ea3 --- /dev/null +++ b/devlog/_plan/260907_lane_c/010_web_search.md @@ -0,0 +1,155 @@ +# 3839 implementation contract + +Carry public source patch with -x. Add deterministic 64KiB SSE and HTTP error-body regressions including cancel that never settles. Preserve complete prefix frames and discard incomplete tail. Tests use public run/parse APIs and controlled byte streams. + +Validation: local tests/typecheck/build/install NOT RUN by instruction. Read diff and source; top remote CI exercises changed test paths. Each conditional branch listed above is exercised by controlled fixtures; screenshot inspects GUI state. No new enforcement layer; existing API guards remain authoritative. + +## Public source diff (MODIFY/NEW paths) + +```diff +diff --git a/src/web-search/anthropic-executor.ts b/src/web-search/anthropic-executor.ts +index 1eb206afa..cd3893900 100644 +--- a/src/web-search/anthropic-executor.ts ++++ b/src/web-search/anthropic-executor.ts +@@ -5,7 +5,11 @@ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "../adapters/client-fin + import { signalWithTimeout, cancelBodyOnAbort } from "../lib/abort"; + import { sidecarEnter } from "../lib/sidecar-tracker"; + import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-retry"; +-import type { WebSearchSource } from "./parse"; ++import { ++ MAX_SIDECAR_RESPONSE_BYTES, ++ cancelReaderWithoutWaiting, ++ type WebSearchSource, ++} from "./parse"; + import { BASE_INSTRUCTION, IMAGE_INSTRUCTION, type SidecarOutcome, type SidecarSettings } from "./executor"; + + /** Hardcoded per-turn search bound handed to the server tool (mirrors the loop's maxSearches intent). */ +@@ -17,6 +21,33 @@ function isRec(v: unknown): v is Record { + return !!v && typeof v === "object" && !Array.isArray(v); + } + ++/** Read at most `MAX_SIDECAR_RESPONSE_BYTES` of an untrusted upstream body, then stop reading. */ ++async function readBoundedText(res: Response): Promise { ++ if (!res.body) return ""; ++ const reader = res.body.getReader(); ++ const decoder = new TextDecoder(); ++ let out = ""; ++ let seen = 0; ++ try { ++ for (;;) { ++ const { done, value } = await reader.read(); ++ if (done) break; ++ const remaining = MAX_SIDECAR_RESPONSE_BYTES - seen; ++ const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining); ++ seen += accepted.byteLength; ++ out += decoder.decode(accepted, { stream: true }); ++ if (seen >= MAX_SIDECAR_RESPONSE_BYTES) { ++ cancelReaderWithoutWaiting(reader, "sidecar error body byte limit reached"); ++ break; ++ } ++ } ++ out += decoder.decode(); ++ } catch { ++ /* a failed error-body read must not mask the HTTP status we are about to report */ ++ } ++ return out; ++} ++ + /** + * Fold an Anthropic Messages SSE stream (a web_search_20250305 turn) into a WebSearchResult. + * +@@ -41,6 +72,7 @@ export async function parseAnthropicSidecarSSE(res: Response): Promise): void => { + const type = typeof data.type === "string" ? data.type : ""; +@@ -82,15 +114,27 @@ export async function parseAnthropicSidecarSSE(res: Response): Promise= MAX_SIDECAR_RESPONSE_BYTES) { ++ // Keep the frames already folded above, drop the unterminated tail, and do not wait on ++ // upstream teardown. ++ cancelReaderWithoutWaiting(reader, "sidecar response byte limit reached"); ++ buffer = ""; ++ break; ++ } + } + // Flush the decoder and process any final unterminated frame (a stream that ends without \n\n). + buffer = (buffer + decoder.decode()).replace(/\r\n/g, "\n"); +@@ -177,7 +221,9 @@ export async function runAnthropicWebSearch( + // (found investigating #1419). + const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal); + if (!res.ok) { +- const t = await res.text().catch(() => ""); ++ // Untrusted upstream error bodies are only used for an auth-failure message, so read a ++ // bounded prefix instead of buffering an arbitrarily large response. ++ const t = await readBoundedText(res); + detachBodyGuard(); + console.warn(`[web-search] anthropic sidecar HTTP ${res.status} for query "${query.slice(0, 80)}" (${Date.now() - t0}ms)`); + if (res.status === 401) { +diff --git a/src/web-search/parse.ts b/src/web-search/parse.ts +index 757c309f3..7ba5d2607 100644 +--- a/src/web-search/parse.ts ++++ b/src/web-search/parse.ts +@@ -193,7 +193,7 @@ function fromOutputArray(output: OutputItem[], seen: Set): WebSearchResu + return { text, sources }; + } + +-function cancelReaderWithoutWaiting( ++export function cancelReaderWithoutWaiting( + reader: ReadableStreamDefaultReader, + reason: string, + ): void { +diff --git a/tests/web-search/web-search-anthropic.test.ts b/tests/web-search/web-search-anthropic.test.ts +index f5b7f1df2..33f2616cc 100644 +--- a/tests/web-search/web-search-anthropic.test.ts ++++ b/tests/web-search/web-search-anthropic.test.ts +@@ -130,6 +130,27 @@ describe("parseAnthropicSidecarSSE", () => { + expect(out.error).toBeDefined(); + }); + ++ test("an unterminated frame cannot buffer the stream without bound", async () => { ++ // A sidecar that never emits a frame separator: without a cap the parser would accumulate ++ // the whole stream in memory before it could fold anything. ++ let produced = 0; ++ let cancelled = false; ++ const chunk = new TextEncoder().encode(`data: {"filler":"${"x".repeat(64 * 1024)}"}`); ++ const body = new ReadableStream({ ++ pull(c) { ++ if (produced > 8 * 1024 * 1024) { c.close(); return; } ++ produced += chunk.byteLength; ++ c.enqueue(chunk); ++ }, ++ cancel() { cancelled = true; }, ++ }); ++ const out = await parseAnthropicSidecarSSE(new Response(body, { status: 200 })); ++ expect(cancelled).toBe(true); ++ // The cap stops the read long before the producer would have finished on its own. ++ expect(produced).toBeLessThan(1024 * 1024); ++ expect(out.text).toBe(""); ++ }); ++ + test("empty results (content:[]) with answer text is a success, not an error", async () => { + const res = sseResponse([ + { type: "content_block_start", index: 0, content_block: { type: "web_search_tool_result", tool_use_id: "srvtoolu_3", content: [] } }, + +``` diff --git a/devlog/_plan/260907_lane_c/020_vision.md b/devlog/_plan/260907_lane_c/020_vision.md new file mode 100644 index 0000000000..9dab5d18e2 --- /dev/null +++ b/devlog/_plan/260907_lane_c/020_vision.md @@ -0,0 +1,135 @@ +# 3841 implementation contract + +Carry public source patch with -x. Add 64KiB HTTP error-body and non-settling cancel regressions. Preserve complete description frames before cap; discard unfinished frame even at exact cap; retain downstream clamp. No credential-policy changes. + +Validation: local tests/typecheck/build/install NOT RUN by instruction. Read diff and source; top remote CI exercises changed test paths. Each conditional branch listed above is exercised by controlled fixtures; screenshot inspects GUI state. No new enforcement layer; existing API guards remain authoritative. + +## Public source diff (MODIFY/NEW paths) + +```diff +diff --git a/src/vision/anthropic-describe.ts b/src/vision/anthropic-describe.ts +index 4f41017ef..280096f03 100644 +--- a/src/vision/anthropic-describe.ts ++++ b/src/vision/anthropic-describe.ts +@@ -10,6 +10,8 @@ import type { DescribeOutcome, VisionSettings } from "./describe"; + const ANTHROPIC_VISION_MAX_TOKENS = 1024; + const ALLOWED_IMAGE_MIME = new Set(["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"]); + const MAX_IMAGE_BYTES = 20 * 1024 * 1024; ++/** Bound the sidecar SSE stream and its untrusted error body; the description is clamped downstream. */ ++const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024; + const DESCRIBE_INSTRUCTION = + "You are a vision describer for a text-only model that cannot see the image. Describe the image " + + "thoroughly and factually so that model can fully reason about it: transcribe any visible text " + +@@ -43,6 +45,34 @@ function buildImageBlock(imageUrl: string): { block?: AnthropicImageBlock; error + return { error: "unsupported image URL scheme (expected data: or https:)" }; + } + ++/** Read at most `MAX_SIDECAR_RESPONSE_BYTES` of an untrusted upstream body, then stop reading. */ ++async function readBoundedText(res: Response): Promise { ++ if (!res.body) return ""; ++ const reader = res.body.getReader(); ++ const decoder = new TextDecoder(); ++ let out = ""; ++ let seen = 0; ++ try { ++ for (;;) { ++ const { done, value } = await reader.read(); ++ if (done) break; ++ const remaining = MAX_SIDECAR_RESPONSE_BYTES - seen; ++ const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining); ++ seen += accepted.byteLength; ++ out += decoder.decode(accepted, { stream: true }); ++ if (seen >= MAX_SIDECAR_RESPONSE_BYTES) { ++ try { void reader.cancel("vision sidecar error body byte limit reached").catch(() => undefined); } ++ catch { /* best-effort body teardown */ } ++ break; ++ } ++ } ++ out += decoder.decode(); ++ } catch { ++ /* a failed error-body read must not mask the HTTP status we are about to report */ ++ } ++ return out; ++} ++ + /** Fold Anthropic Messages text deltas into one description. Malformed frames are ignored. */ + export async function parseAnthropicVisionSSE(res: Response): Promise { + if (!res.body) return { text: "", error: "anthropic vision sidecar returned no response body" }; +@@ -52,6 +82,7 @@ export async function parseAnthropicVisionSSE(res: Response): Promise { + let dataLine = ""; +@@ -76,12 +107,24 @@ export async function parseAnthropicVisionSSE(res: Response): Promise= MAX_SIDECAR_RESPONSE_BYTES) { ++ // Keep the frames folded above, drop the unterminated tail, and do not wait on teardown. ++ try { void reader.cancel("vision sidecar response byte limit reached").catch(() => undefined); } ++ catch { /* best-effort body teardown */ } ++ buffer = ""; ++ break; ++ } + } + buffer = (buffer + decoder.decode()).replace(/\r\n/g, "\n"); + if (buffer.trim()) processFrame(buffer); +@@ -164,7 +207,8 @@ export async function describeImageAnthropic( + { abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" }, + ); + if (!res.ok) { +- const responseText = await res.text().catch(() => ""); ++ // The body is untrusted and only feeds one auth-failure message, so read a bounded prefix. ++ const responseText = await readBoundedText(res); + console.warn(`[vision] anthropic sidecar HTTP ${res.status} (${Date.now() - startedAt}ms)`); + if (res.status === 401) { + return { text: "", error: `anthropic vision sidecar auth failed: ${publicOAuthAuthenticationErrorMessage(new Error(responseText))}` }; +diff --git a/tests/vision/vision-anthropic.test.ts b/tests/vision/vision-anthropic.test.ts +index ee4b01b42..30ed17af9 100644 +--- a/tests/vision/vision-anthropic.test.ts ++++ b/tests/vision/vision-anthropic.test.ts +@@ -225,6 +225,27 @@ describe("Anthropic vision executor", () => { + expect(result).toEqual({ text: "first second" }); + }); + ++ test("an unterminated frame cannot buffer the stream without bound", async () => { ++ // A sidecar that never emits a frame separator: without a cap the parser accumulates the ++ // whole response in memory before it can fold anything. ++ let produced = 0; ++ let cancelled = false; ++ const chunk = new TextEncoder().encode(`data: {"filler":"${"x".repeat(64 * 1024)}"}`); ++ const body = new ReadableStream({ ++ pull(c) { ++ if (produced > 8 * 1024 * 1024) { c.close(); return; } ++ produced += chunk.byteLength; ++ c.enqueue(chunk); ++ }, ++ cancel() { cancelled = true; }, ++ }); ++ const out = await parseAnthropicVisionSSE(new Response(body, { status: 200 })); ++ expect(cancelled).toBe(true); ++ // The cap stops the read long before the producer would have finished on its own. ++ expect(produced).toBeLessThan(1024 * 1024); ++ expect(out.text).toBe(""); ++ }); ++ + test("malformed and terminal-error streams degrade to explicit errors", async () => { + const malformed = await parseAnthropicVisionSSE(sseResponse(["{not-json", { type: "message_stop" }])); + expect(malformed.text).toBe(""); + +``` diff --git a/devlog/_plan/260907_lane_c/030_health.md b/devlog/_plan/260907_lane_c/030_health.md new file mode 100644 index 0000000000..eaeb4bbcd6 --- /dev/null +++ b/devlog/_plan/260907_lane_c/030_health.md @@ -0,0 +1,123 @@ +# 3863 implementation contract + +Carry with -x excluding config-routes.ts. getStartupHealthSnapshot returns fresh cached value unchanged; stale/empty read schedules refresh and returns immediately. Catch rejected or synchronously thrown detached probe and retain stale conservative health; invalidation generation cannot overwrite newer reading. Replace 100ms production settings assertion with controlled probe fixtures. Exact route wiring remains main responsibility. + +Validation: local tests/typecheck/build/install NOT RUN by instruction. Read diff and source; top remote CI exercises changed test paths. Each conditional branch listed above is exercised by controlled fixtures; screenshot inspects GUI state. No new enforcement layer; existing API guards remain authoritative. + +## Public source diff (MODIFY/NEW paths) + +```diff +diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts +index 4d551a886..9ddd02300 100644 +--- a/src/server/management/config-routes.ts ++++ b/src/server/management/config-routes.ts +@@ -107,7 +107,7 @@ import type { PersistedUsageAttempt } from "../../usage/log"; + import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; + import { withProviderServiceTierDTO } from "./provider-capability-config"; + import { applySystemEnvToggle } from "../system-env"; +-import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; ++import { getCachedStartupHealth, getStartupHealthSnapshot, invalidateStartupHealthCache } from "../startup-health-cache"; + import { runWindowsTrayAction } from "../windows-tray-control"; + import { runStartupInstallAction, type StartupInstallAction } from "../startup-action-control"; + import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../../codex/runtime"; +@@ -329,7 +329,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise Promise; + } + ++/** ++ * Return the last completed probe immediately and refresh it in the background. ++ * ++ * Settings are consumed by several dashboard controls. They must not block on a ++ * Windows service-manager probe; the dedicated /api/startup-health route owns ++ * the fresh, bounded diagnostic read. ++ */ ++export function getStartupHealthSnapshot( ++ config: Pick, ++ deps: StartupHealthCacheDeps = {}, ++): StartupHealth { ++ const now = deps.now ?? Date.now; ++ if (!cached || now() - cached.timestamp >= CACHE_TTL_MS) refreshInBackground(config, deps); ++ return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config); ++} ++ + export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth { + if (!value.localRoutingDependency) return { ...value, diagnosticStale: true }; + return { +diff --git a/tests/service/autostart-health.test.ts b/tests/service/autostart-health.test.ts +index 639f1b34c..48bb7b539 100644 +--- a/tests/service/autostart-health.test.ts ++++ b/tests/service/autostart-health.test.ts +@@ -3,7 +3,7 @@ import { deriveStartupHealth, formatStartupRoutingDetail, startupHealthSummary } + import { unusedProxyWarningLines } from "../../src/cli/status"; + import { classifyCodexRouting, hasInjectedCodexRouting } from "../../src/codex/inject"; + import { handleManagementAPI } from "../../src/server/management-api"; +-import { getCachedStartupHealth, invalidateStartupHealthCache, markStartupHealthDiagnosticStale } from "../../src/server/startup-health-cache"; ++import { getCachedStartupHealth, getStartupHealthSnapshot, invalidateStartupHealthCache, markStartupHealthDiagnosticStale } from "../../src/server/startup-health-cache"; + import type { OcxConfig } from "../../src/types"; + + const base = { +@@ -277,6 +277,43 @@ describe("Codex startup health", () => { + await pendingProbe; + invalidateStartupHealthCache(); + }); ++ ++ test("settings snapshot starts a probe without waiting for it", async () => { ++ invalidateStartupHealthCache(); ++ let releaseProbe!: (value: ReturnType) => void; ++ const pendingProbe = new Promise>(resolve => { ++ releaseProbe = resolve; ++ }); ++ ++ const health = getStartupHealthSnapshot( ++ { codexAutoStart: true }, ++ { probe: async () => pendingProbe }, ++ ); ++ ++ expect(health.diagnosticStale).toBe(true); ++ releaseProbe(deriveStartupHealth({ ...base, routingKind: "native" })); ++ await pendingProbe; ++ invalidateStartupHealthCache(); ++ }); ++ ++ test("settings GET uses the non-blocking startup-health snapshot in production", async () => { ++ invalidateStartupHealthCache(); ++ const url = new URL("http://localhost/api/settings"); ++ ++ const response = await Promise.race([ ++ handleManagementAPI( ++ new Request(url), ++ url, ++ { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, ++ ), ++ new Promise(resolve => setTimeout(() => resolve(null), 100)), ++ ]); ++ ++ expect(response?.status).toBe(200); ++ const body = await response!.json() as { startupHealth?: { diagnosticStale?: boolean } }; ++ expect(body.startupHealth?.diagnosticStale).toBe(true); ++ invalidateStartupHealthCache(); ++ }); + }); + import { ManagementRequest as Request } from "../helpers/management-auth"; + + +``` + +## Main-owned route handoff + +At current dev, settings GET uses `startupHealth: await readStartupHealth(config)` at `src/server/management/config-routes.ts:332`. M changes only this settings read to the exported immediate snapshot and retains the dedicated `/api/startup-health` bounded read. Settings PUT at line 625 is separately present; it must remain reviewed explicitly rather than blindly replaced. C does not modify either call site. diff --git a/devlog/_plan/260907_lane_c/040_desktop.md b/devlog/_plan/260907_lane_c/040_desktop.md new file mode 100644 index 0000000000..4eb4ebffd5 --- /dev/null +++ b/devlog/_plan/260907_lane_c/040_desktop.md @@ -0,0 +1,408 @@ +# 3860 implementation contract + +Carry source patch plus skipped-sync correction with -x. Default false/absent OFF, true remains true; persist preference before sync and surface sync failures. All nine locales and existing screenshot. Independent auth boundary review confirms remote admission/upstream credentials unchanged. + +Validation: local tests/typecheck/build/install NOT RUN by instruction. Read diff and source; top remote CI exercises changed test paths. Each conditional branch listed above is exercised by controlled fixtures; screenshot inspects GUI state. No new enforcement layer; existing API guards remain authoritative. + +## Public source diff (MODIFY/NEW paths) + +```diff +diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md +index 7d66e72c3..ff2df04fd 100644 +--- a/docs-site/src/content/docs/guides/codex-integration.md ++++ b/docs-site/src/content/docs/guides/codex-integration.md +@@ -215,6 +215,15 @@ HTTP/SSE. + + ### Authless Codex Desktop (opt-in) + ++In **Dashboard → Overview**, **Open Codex without signing in** controls this existing ++opt-in preference. The switch defaults to **off** when the setting is absent or false; ++an existing explicit `codexDesktopAuthless: true` stays enabled. The dashboard saves ++the preference and runs a full sync. Restart Codex Desktop after changing it. ++If synchronization fails, the saved preference remains and the dashboard shows the error; ++retry **Sync** before restarting. Account-gated Desktop features may be unavailable ++when enabled. Upstream credentials, local eligibility, remote admission authentication ++and user-owned gateway settings retain their existing requirements. ++ + Codex Desktop shows its ChatGPT login screen whenever the active provider requires OpenAI auth. If + your OpenCodex setup never uses ChatGPT credentials (routed providers only, or a blocked + `chatgpt.com`), you can opt out of that gate: +diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts +index 5faab4b35..495104bd4 100644 +--- a/gui/src/i18n/de.ts ++++ b/gui/src/i18n/de.ts +@@ -299,6 +299,8 @@ export const de: Record = { + "models.staleBanner": "Codex zeigt eine ältere Modellliste als dieser Katalog. Starte Codex neu, um sie neu zu laden.", + "dash.codexAutoStart": "opencodex mit Codex starten", + "dash.codexAutoStartHint": "Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.", ++ "dash.codexDesktopAuthless": "Codex ohne Anmeldung öffnen", ++ "dash.codexDesktopAuthlessHint": "Standardmäßig aus. Überspringt die separate Desktop-Anmeldung bei geeigneten lokalen Verbindungen. Zugangsdaten für den Anbieter bleiben erforderlich. Codex nach einer Änderung neu starten. Kontogebundene Desktop-Funktionen können fehlen.", + "dash.searchModel": "Such-Sidecar-Modell", + "dash.searchModelHint": "Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.", + "dash.searchReasoning": "Such-Reasoning-Aufwand", +diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts +index c71208942..22a380785 100644 +--- a/gui/src/i18n/en.ts ++++ b/gui/src/i18n/en.ts +@@ -311,6 +311,8 @@ export const en = { + "models.staleBanner": "Codex is showing an older model list than this catalog. Restart Codex to reload it.", + "dash.codexAutoStart": "Start opencodex with Codex", + "dash.codexAutoStartHint": "Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.", ++ "dash.codexDesktopAuthless": "Open Codex without signing in", ++ "dash.codexDesktopAuthlessHint": "Off by default. Skip the separate Desktop sign-in for eligible local connections. Upstream credentials are still required. Restart Codex after changing this setting. Account-gated Desktop features may be unavailable.", + "dash.searchModel": "Search sidecar model", + "dash.searchModelHint": "Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.", + "dash.searchReasoning": "Search reasoning effort", +diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts +index e1b3519ef..9f0f26517 100644 +--- a/gui/src/i18n/fr.ts ++++ b/gui/src/i18n/fr.ts +@@ -301,6 +301,8 @@ export const fr: Record = { + "models.staleBanner": "Codex affiche une liste de modèles plus ancienne que ce catalogue. Redémarrez Codex pour la recharger.", + "dash.codexAutoStart": "Démarrer opencodex avec Codex", + "dash.codexAutoStartHint": "Permet à un mécanisme de lancement installé d’exécuter ocx ensure. Ce réglage n’installe pas de protection au redémarrage ; consultez Sécurité du démarrage pour connaître l’état effectif.", ++ "dash.codexDesktopAuthless": "Ouvrir Codex sans se connecter", ++ "dash.codexDesktopAuthlessHint": "Désactivé par défaut. Ignore la connexion Desktop séparée pour les connexions locales admissibles. Les identifiants du fournisseur restent nécessaires. Redémarrez Codex après toute modification. Certaines fonctions Desktop liées au compte peuvent être indisponibles.", + "dash.searchModel": "Modèle auxiliaire de recherche", + "dash.searchModelHint": "Modèle utilisé pour web_search sur les modèles routés autres qu’OpenAI. Nécessite une connexion à ChatGPT.", + "dash.searchReasoning": "Effort de raisonnement pour la recherche", +diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts +index cf9483158..55a6fe249 100644 +--- a/gui/src/i18n/ja.ts ++++ b/gui/src/i18n/ja.ts +@@ -308,6 +308,8 @@ export const ja: Record = { + "models.staleBanner": "Codex はこのカタログより古いモデル一覧を表示しています。Codex を再起動すると読み直されます。", + "dash.codexAutoStart": "Codex と一緒に opencodex を起動", + "dash.codexAutoStartHint": "インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。", ++ "dash.codexDesktopAuthless": "ログインせずに Codex を開く", ++ "dash.codexDesktopAuthlessHint": "既定ではオフです。対象のローカル接続で Desktop の個別ログインを省略します。上流プロバイダーの認証情報は引き続き必要です。変更後は Codex を再起動してください。アカウントに依存する Desktop 機能が利用できない場合があります。", + "dash.searchModel": "検索サイドカーモデル", + "dash.searchModelHint": "非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。", + "dash.searchReasoning": "検索の推論負荷", +diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts +index c1959482b..19285b150 100644 +--- a/gui/src/i18n/ko.ts ++++ b/gui/src/i18n/ko.ts +@@ -303,6 +303,8 @@ export const ko: Record = { + "models.staleBanner": "Codex가 이 카탈로그보다 오래된 모델 목록을 보여주고 있습니다. Codex를 재시작하면 새로 읽습니다.", + "dash.codexAutoStart": "Codex 실행 시 opencodex 시작", + "dash.codexAutoStartHint": "설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.", ++ "dash.codexDesktopAuthless": "로그인 없이 Codex 열기", ++ "dash.codexDesktopAuthlessHint": "기본값은 꺼짐입니다. 지원되는 로컬 연결에서 별도의 Desktop 로그인을 건너뜁니다. 업스트림 인증 정보는 여전히 필요합니다. 변경 후 Codex를 다시 시작하세요. 계정에 연결된 Desktop 기능을 사용하지 못할 수 있습니다.", + "dash.searchModel": "서치 사이드카 모델", + "dash.searchModelHint": "비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.", + "dash.searchReasoning": "서치 추론 강도", +diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts +index 0109f5ebd..87704912a 100644 +--- a/gui/src/i18n/ru.ts ++++ b/gui/src/i18n/ru.ts +@@ -308,6 +308,8 @@ export const ru: Record = { + "models.staleBanner": "Codex показывает список моделей старее этого каталога. Перезапустите Codex, чтобы перечитать его.", + "dash.codexAutoStart": "Запускать opencodex вместе с Codex", + "dash.codexAutoStartHint": "Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.", ++ "dash.codexDesktopAuthless": "Открывать Codex без входа", ++ "dash.codexDesktopAuthlessHint": "По умолчанию выключено. Пропускает отдельный вход в Desktop для допустимых локальных подключений. Учётные данные провайдера по-прежнему нужны. После изменения перезапустите Codex. Функции Desktop, связанные с аккаунтом, могут быть недоступны.", + "dash.searchModel": "Модель сайдкара поиска", + "dash.searchModelHint": "Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.", + "dash.searchReasoning": "Уровень рассуждений для поиска", +diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts +index fa8b8e9c2..807eeae32 100644 +--- a/gui/src/i18n/tr.ts ++++ b/gui/src/i18n/tr.ts +@@ -309,6 +309,8 @@ export const tr: Record = { + "models.staleBanner": "Codex, bu katalogdan daha eski bir model listesi gösteriyor. Yeniden okumak için Codex'i yeniden başlatın.", + "dash.codexAutoStart": "opencodex'i Codex ile başlat", + "dash.codexAutoStartHint": "Yüklü bir shim'in ocx ensure çalıştırmasına izin verir. Arka plan servisi veya yeniden başlatma koruması kurmaz; sistem durumu için Başlatma Güvenliği'ne bakın.", ++ "dash.codexDesktopAuthless": "Codex’i oturum açmadan başlat", ++ "dash.codexDesktopAuthlessHint": "Varsayılan olarak kapalıdır. Uygun yerel bağlantılarda ayrı Desktop oturum açma adımını atlar. Sağlayıcı kimlik bilgileri yine gereklidir. Değişiklikten sonra Codex’i yeniden başlatın. Hesaba bağlı Desktop özellikleri kullanılamayabilir.", + "dash.searchModel": "Arama yan araç modeli", + "dash.searchModelHint": "OpenAI dışı yönlendirilen modellerde web_search için kullanılan model. ChatGPT girişi gerektirir.", + "dash.searchReasoning": "Arama akıl yürütme çabası", +diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts +index 3bc246543..62e1f0711 100644 +--- a/gui/src/i18n/zh-TW.ts ++++ b/gui/src/i18n/zh-TW.ts +@@ -200,6 +200,8 @@ export const zhTW: Record = { + "models.staleBanner": "Codex 顯示的模型清單比目前的目錄舊。重新啟動 Codex 即可重新讀取。", + "dash.codexAutoStart": "隨 Codex 啟動 opencodex", + "dash.codexAutoStartHint": "允許已安裝的 launcher shim 執行 ocx ensure。此設定不會安裝重新啟動保護;請在啟動安全中檢查實際狀態。", ++ "dash.codexDesktopAuthless": "無需登入即可開啟 Codex", ++ "dash.codexDesktopAuthlessHint": "預設關閉。為符合條件的本機連線略過獨立的 Desktop 登入。仍需上游供應商憑證。變更後請重新啟動 Codex。依賴帳戶的 Desktop 功能可能無法使用。", + "dash.searchModel": "搜尋附屬模型", + "dash.searchModelHint": "用於非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登入。", + "dash.searchReasoning": "搜尋推理強度", +diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts +index b10c48688..994691442 100644 +--- a/gui/src/i18n/zh.ts ++++ b/gui/src/i18n/zh.ts +@@ -303,6 +303,8 @@ export const zh: Record = { + "models.staleBanner": "Codex 显示的模型列表比当前目录旧。重启 Codex 即可重新读取。", + "dash.codexAutoStart": "随 Codex 启动 opencodex", + "dash.codexAutoStartHint": "允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。", ++ "dash.codexDesktopAuthless": "无需登录即可打开 Codex", ++ "dash.codexDesktopAuthlessHint": "默认关闭。为符合条件的本地连接跳过单独的 Desktop 登录。仍需上游提供商凭据。更改后请重启 Codex。依赖账户的 Desktop 功能可能不可用。", + "dash.searchModel": "搜索附属模型", + "dash.searchModelHint": "用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。", + "dash.searchReasoning": "搜索推理强度", +diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx +index 8da531f97..6606c4f56 100644 +--- a/gui/src/pages/dashboard-overview-sections.tsx ++++ b/gui/src/pages/dashboard-overview-sections.tsx +@@ -163,7 +163,7 @@ export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) { + + export function DashboardMaintenancePanel({ d }: { d: Dash }) { + const { +- t, runSync, syncing, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, ++ t, runSync, syncing, settingsSaving, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, + syncResult, syncError, updateJob, reconnecting, clearSyncFeedback, + } = d; + const syncHoldsWarning = !!syncResult && ( +@@ -211,7 +211,7 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) { +
{t("dash.syncModelsHint")}
+ +
+- +
+ + ++
++
++
++
{t("dash.codexDesktopAuthless")}
++
{t("dash.codexDesktopAuthlessHint")}
++ {settings?.catalogRefreshPending &&
{t("codexAuth.catalogRefreshPending")}
} ++
++ ++
++
++ +
+ {/* Both sidecar cards wear the DashboardInjectionPanel shell: the PANEL is + the flex row, copy left, controls right. */} +diff --git a/gui/src/pages/dashboard-shared.ts b/gui/src/pages/dashboard-shared.ts +index 0793a7def..d24051028 100644 +--- a/gui/src/pages/dashboard-shared.ts ++++ b/gui/src/pages/dashboard-shared.ts +@@ -48,6 +48,8 @@ export interface ProviderInfo { name: string; adapter: string; baseUrl: string; + export interface ModelInfo { id: string; provider: string; namespaced: string; owned_by?: string; reasoningEfforts?: string[] } + export interface SettingsData { + codexAutoStart: boolean; ++ codexDesktopAuthless?: boolean; ++ catalogRefreshPending?: boolean; + /** Whether a login may open a browser on the machine running the proxy. */ + oauthOpenBrowser?: boolean; + port: number; +diff --git a/gui/src/pages/use-dashboard-data.ts b/gui/src/pages/use-dashboard-data.ts +index 6f84950ce..6da776ea1 100644 +--- a/gui/src/pages/use-dashboard-data.ts ++++ b/gui/src/pages/use-dashboard-data.ts +@@ -607,23 +607,24 @@ export function useDashboardData(apiBase: string) { + finally { setInjectionSaving(false); } + }; + +- const toggleCodexAutoStart = async () => { +- if (!settings || settingsSaving) return; +- const next = !settings.codexAutoStart; ++ const toggleCodexSetting = async (key: "codexAutoStart" | "codexDesktopAuthless") => { ++ if (!settings || settingsSaving || syncing) return; ++ const next = !(settings[key] ?? (key === "codexAutoStart")); + setSettingsSaving(true); + settingsMutationInFlightRef.current = true; +- setSettings({ ...settings, codexAutoStart: next }); ++ setSettings({ ...settings, [key]: next }); + try { + const res = await fetch(`${apiBase}/api/settings`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, +- body: JSON.stringify({ codexAutoStart: next }), ++ body: JSON.stringify({ [key]: next }), + }); +- const data = await requireJson<{ codexAutoStart: boolean; startupHealth?: SettingsData["startupHealth"] }>(res, "save failed"); ++ const data = await requireJson(res, "save failed"); + settingsMutationEpochRef.current += 1; +- setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); ++ setSettings(prev => prev ? { ...prev, [key]: data[key], catalogRefreshPending: key === "codexDesktopAuthless" ? data.catalogRefreshPending : prev.catalogRefreshPending, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); ++ if (key === "codexDesktopAuthless") await runSync(); + } catch { +- setSettings(prev => prev ? { ...prev, codexAutoStart: !next } : prev); ++ setSettings(prev => prev ? { ...prev, [key]: !next } : prev); + setError(true); + } finally { + settingsMutationInFlightRef.current = false; +@@ -631,6 +632,9 @@ export function useDashboardData(apiBase: string) { + } + }; + ++ const toggleCodexAutoStart = () => toggleCodexSetting("codexAutoStart"); ++ const toggleCodexDesktopAuthless = () => toggleCodexSetting("codexDesktopAuthless"); ++ + // Clears the sync result/error in this hook. The dashboard toast owns its own dismissal + // timer but must publish the dismissal here: syncResult/syncError live above the dashboard + // tabs, so a component-local flag alone would let a stale result remount as a fresh toast +@@ -649,6 +653,7 @@ export function useDashboardData(apiBase: string) { + const res = await fetch(`${apiBase}/api/sync`, { method: "POST" }); + const data = await requireJson(res, "sync failed"); + setSyncResult(data); ++ setSettings(prev => prev ? { ...prev, catalogRefreshPending: false } : prev); + if (data.projectConfigGrouped) setProjectConfigWarnings(data.projectConfigGrouped); + } catch (err) { + setSyncError(err instanceof Error ? err.message : String(err)); +@@ -789,7 +794,7 @@ export function useDashboardData(apiBase: string) { + effortCapHelpTriggerRef, updateTriggerRef, maHelpTriggerRef, shadowCallHelpTriggerRef, + effortCapHelpDialogRef, updateDialogRef, maHelpDialogRef, shadowCallHelpDialogRef, + filteredGroups, sidecarModels, visionModels, +- saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, runSync, clearSyncFeedback, ++ saveSidecar, saveShadowCall, switchMaMode, toggleCodexAutoStart, toggleCodexDesktopAuthless, runSync, clearSyncFeedback, + fetchUpdateCheck, closeUpdateDialog, openUpdateDialog, changeUpdateChannel, runUpdate, + }; + } +diff --git a/gui/tests/vision-sidecar-dashboard.test.tsx b/gui/tests/vision-sidecar-dashboard.test.tsx +index dc762de58..994a40912 100644 +--- a/gui/tests/vision-sidecar-dashboard.test.tsx ++++ b/gui/tests/vision-sidecar-dashboard.test.tsx +@@ -12,7 +12,7 @@ import { LanguageProvider } from "../src/i18n/provider"; + import { DashboardSidecarPanels } from "../src/pages/dashboard-overview-sections"; + import type { SidecarData, SidecarPatch } from "../src/pages/dashboard-shared"; + import { mergeSidecarSetting } from "../src/pages/dashboard-shared"; +-import type { useDashboardData } from "../src/pages/use-dashboard-data"; ++import { useDashboardData } from "../src/pages/use-dashboard-data"; + + const globals = ["document", "window", "navigator", "IS_REACT_ACT_ENVIRONMENT"] as const; + let previousGlobals: Record<(typeof globals)[number], PropertyDescriptor | undefined>; +@@ -382,4 +382,79 @@ test("model and reasoning saves still omit enabled, limit, and timeout", async ( + expect(patches).toHaveLength(2); + expect(patches[1]).toEqual({ vision: { reasoning: "high" } }); + assertVisionControlFieldsOmitted(patches[1]!); +-}); +\ No newline at end of file ++}); ++ ++test("Desktop login switch defaults off, preserves explicit opt-in, and disables while saving", async () => { ++ const { d } = harness(); ++ let clicks = 0; ++ d.toggleCodexDesktopAuthless = async () => { clicks += 1; }; ++ d.settings = { codexAutoStart: true, port: 10100, hostname: "127.0.0.1" }; ++ await mount(d); ++ const toggle = () => host.querySelector(`button[aria-label="${en["dash.codexDesktopAuthless"]}"]`)!; ++ expect(toggle().getAttribute("aria-pressed")).toBe("false"); ++ d.settings.codexDesktopAuthless = true; ++ await mount(d); ++ expect(toggle().getAttribute("aria-pressed")).toBe("true"); ++ await act(async () => { toggle().click(); }); ++ expect(clicks).toBe(1); ++ d.settings.codexDesktopAuthless = false; ++ d.settings.catalogRefreshPending = true; ++ d.settingsSaving = true; ++ await mount(d); ++ expect(toggle().getAttribute("aria-pressed")).toBe("false"); ++ expect(toggle().disabled).toBe(true); ++ expect(host.textContent).toContain(en["codexAuth.catalogRefreshPending"]); ++}); ++ ++ ++test.each([undefined, false, true])("Desktop login preference %s persists before full sync; sync failure keeps the saved preference", async (initial) => { ++ const originalFetch = globalThis.fetch; ++ const writes: Array<{ path: string; body: unknown }> = []; ++ let latest: Dash | undefined; ++ let saved = initial; ++ const apiBase = `/authless-test-${String(initial)}`; ++ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { ++ const path = String(input); ++ if (init?.method === "PUT") { ++ const body = JSON.parse(String(init.body)); ++ writes.push({ path, body }); ++ if (body.codexDesktopAuthless !== undefined) { ++ saved = body.codexDesktopAuthless; ++ return Response.json({ codexDesktopAuthless: saved, catalogRefreshPending: true }); ++ } ++ return Response.json({ codexAutoStart: body.codexAutoStart, catalogRefreshPending: false }); ++ } ++ if (path.endsWith("/api/sync")) { ++ writes.push({ path, body: null }); ++ return Response.json({ error: "sync unavailable" }, { status: 503 }); ++ } ++ if (path.endsWith("/api/settings")) { ++ return Response.json({ codexAutoStart: true, codexDesktopAuthless: saved, port: 10100, hostname: "127.0.0.1" }); ++ } ++ return Response.json({}, { status: 503 }); ++ }) as typeof fetch; ++ function Harness() { latest = useDashboardData(apiBase); return null; } ++ try { ++ const { createRoot } = await import("react-dom/client"); ++ await act(async () => { ++ root = createRoot(host); ++ root.render(); ++ }); ++ expect(latest?.settings?.codexDesktopAuthless).toBe(initial); ++ await act(async () => { await latest!.toggleCodexDesktopAuthless(); }); ++ expect(writes).toEqual([ ++ { path: `${apiBase}/api/settings`, body: { codexDesktopAuthless: !initial } }, ++ { path: `${apiBase}/api/sync`, body: null }, ++ ]); ++ expect(latest?.settings?.codexDesktopAuthless).toBe(!initial); ++ expect(latest?.syncError).toBe("sync unavailable"); ++ expect(latest?.settings?.catalogRefreshPending).toBe(true); ++ await act(async () => { await latest!.toggleCodexAutoStart(); }); ++ expect(latest?.settings?.codexAutoStart).toBe(false); ++ expect(latest?.settings?.catalogRefreshPending).toBe(true); ++ } finally { ++ await act(async () => { root?.unmount(); }); ++ root = null; ++ globalThis.fetch = originalFetch; ++ } ++}); +diff --git a/tests/codex-integration/codex-inject.test.ts b/tests/codex-integration/codex-inject.test.ts +index 84ac5f67b..b6be3c2f6 100644 +--- a/tests/codex-integration/codex-inject.test.ts ++++ b/tests/codex-integration/codex-inject.test.ts +@@ -31,8 +31,8 @@ describe("Codex config injection", () => { + }); + + describe("authless Codex Desktop opt-in (#1107)", () => { +- test("default target on loopback stays Design B and byte-identical", () => { +- const target = standaloneCodexRoutingTarget(10100, {}); ++ test.each([undefined, false])("disabled preference %s on loopback stays Design B and byte-identical", (codexDesktopAuthless) => { ++ const target = standaloneCodexRoutingTarget(10100, { codexDesktopAuthless }); + expect(target.desktopAuthless).toBeUndefined(); + expect(buildProfileFile(target, null)).toBe(buildProfileFile(10100, null)); + expect(buildProviderTableBlock(target)).toContain("requires_openai_auth = true"); + +``` + +Audit amendment: clear catalogRefreshPending only if sync status is affirmative success, not HTTP 200 skipped. Add skipped/no-write regression. + +## Lane E documentation handoff + +After run 34106956362 reported a GUI lint failure, include the separately prepared code-mode host-rule translations in the seven fr/ja/ko/ru/tr/zh-cn/zh-tw Codex integration guides. The patch adds 51 documentation lines matching the existing English paragraph; it does not modify runtime code or provider guides. Apply on the Desktop layer, record `docs handoff from lane E` and `[skip ci]` in its own commit, then cascade the fallback layer and dispatch the top CI again. Local documentation install/build remains NOT RUN. diff --git a/devlog/_plan/260907_lane_c/050_fallback.md b/devlog/_plan/260907_lane_c/050_fallback.md new file mode 100644 index 0000000000..6bf165a96c --- /dev/null +++ b/devlog/_plan/260907_lane_c/050_fallback.md @@ -0,0 +1,368 @@ +# 3252 implementation contract + +Carry source commits with -x. Preserve configured fallback models absent from availability. Add focused GUI tests for add/remove/reorder/save and unavailable model round-trip. Reuse existing /api/v2 (enabled, multiAgentMode, keepNativeChatGptOnV1) and report recovery enabled/eligibility as unknown when the server does not expose it, never fabricate recovery settings state for contextual native-parent/routed-child V2 guidance. Never infer all workflows are native; warn conditionally, show disabled/eligible/experimental/unknown state truthfully, link issue 92. No roster-reuse switch. Update all locales and codex-integration docs; actual UI screenshot. New PR body is valid Markdown, removes unsupported roster-switch claims. + +Validation: local tests/typecheck/build/install NOT RUN by instruction. Read diff and source; top remote CI exercises changed test paths. Each conditional branch listed above is exercised by controlled fixtures; screenshot inspects GUI state. No new enforcement layer; existing API guards remain authoritative. + +## Public source diff (MODIFY/NEW paths) + +```diff +diff --git a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +index 46c0447a7..7c3b0e942 100644 +--- a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx ++++ b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +@@ -28,6 +28,13 @@ export interface SubagentDelegationSectionProps { + onUltraModeSave: (patch: UltraModePatch) => void; + ultraLoadFailed: boolean; + onUltraModeRetry: () => void; ++ fallback: string[]; ++ fallbackPollMs: number; ++ fallbackBusy: boolean; ++ availableModels: string[]; ++ onFallbackChange: (models: string[]) => void; ++ onFallbackPollMsChange: (pollMs: number) => void; ++ onFallbackSave: () => void; + } + + export default function SubagentDelegationSection({ +@@ -44,6 +51,7 @@ export default function SubagentDelegationSection({ + onUltraModeSave, + ultraLoadFailed, + onUltraModeRetry, ++ fallback, fallbackPollMs, fallbackBusy, availableModels, onFallbackChange, onFallbackPollMsChange, onFallbackSave, + }: SubagentDelegationSectionProps) { + const t = useT(); + // A present empty/whitespace hint is an upstream override that suppresses the +@@ -97,6 +105,31 @@ export default function SubagentDelegationSection({ +
+ + ++
++
++
{t("sub.fallbackLabel")}
++
{t("sub.fallbackHint")}
++
++
++ {fallback.map((modelName, index) => ( ++
++ {index + 1}. {modelName} ++ ++ ++ ++
++ ))} ++ ++ ++ ++
++
++ +
+
+
{t("dash.syncCodexSubagentDefaults")}
+diff --git a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx +index a22bd2a30..30b722b2b 100644 +--- a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx ++++ b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx +@@ -37,6 +37,12 @@ export interface SubagentsWorkspaceProps { + onToggle: (m: string) => void; + onMove: (i: number, dir: -1 | 1) => void; + onSave: () => void; ++ fallback: string[]; ++ fallbackPollMs: number; ++ fallbackBusy: boolean; ++ onFallbackChange: (models: string[]) => void; ++ onFallbackPollMsChange: (pollMs: number) => void; ++ onFallbackSave: () => void; + delegation: { + model: string; + effort: string; +@@ -63,6 +69,7 @@ export default function SubagentsWorkspace({ + onToggle, + onMove, + onSave, ++ fallback, fallbackPollMs, fallbackBusy, onFallbackChange, onFallbackPollMsChange, onFallbackSave, + delegation, + }: SubagentsWorkspaceProps) { + const t = useT(); +@@ -237,6 +244,13 @@ export default function SubagentsWorkspace({ + onUltraModeSave={delegation.onUltraModeSave} + ultraLoadFailed={delegation.ultraLoadFailed} + onUltraModeRetry={delegation.onUltraModeRetry} ++ fallback={fallback} ++ fallbackPollMs={fallbackPollMs} ++ fallbackBusy={fallbackBusy} ++ availableModels={available} ++ onFallbackChange={onFallbackChange} ++ onFallbackPollMsChange={onFallbackPollMsChange} ++ onFallbackSave={onFallbackSave} + /> + +
+diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts +index 429379396..2bb0b10c1 100644 +--- a/gui/src/i18n/de.ts ++++ b/gui/src/i18n/de.ts +@@ -672,6 +672,12 @@ export const de: Record = { + "sub.ultraModeLoadFail": "Ultra-Modus-Einstellungen konnten nicht geladen werden — läuft der Proxy?", + "sub.ultraModeSaveFail": "Ultra-Modus-Einstellungen konnten nicht gespeichert werden", + "sub.ultraModeSaved": "Ultra-Modus gespeichert. Gilt für neue Codex-Sitzungen.", ++ "sub.fallbackLabel": "Fallback-Kette für Sub-Agenten", ++ "sub.fallbackHint": "Geordnete Modelle, die versucht werden, wenn ein Sub-Agent-Modell nicht verfügbar ist oder fehlschlägt.", ++ "sub.fallbackAdd": "Fallback-Modell hinzufügen…", ++ "sub.fallbackPoll": "Intervall der Verfügbarkeitsprüfung", ++ "sub.fallbackSaved": "Fallback-Einstellungen für Sub-Agenten gespeichert.", ++ "sub.fallbackSaveFailed": "Fallback-Einstellungen konnten nicht gespeichert werden", + "logs.title": "Anfrage-Protokolle", + "logs.tabLogs": "Protokolle", + "logs.tabDebug": "Diagnose", +diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts +index 9cbf8699f..e1346616a 100644 +--- a/gui/src/i18n/en.ts ++++ b/gui/src/i18n/en.ts +@@ -315,6 +315,12 @@ export const en = { + "dash.visionTimeout": "Timeout", + "dash.visionTimeoutInvalid": "Enter an integer from {min} to {max} milliseconds.", + "dash.visionAdvancedPopover": "Advanced vision settings", ++ "sub.fallbackLabel": "Sub-agent fallback chain", ++ "sub.fallbackHint": "Ordered models tried when a sub-agent model is unavailable or fails.", ++ "sub.fallbackAdd": "Add fallback model…", ++ "sub.fallbackPoll": "Availability check interval", ++ "sub.fallbackSaved": "Sub-agent fallback settings saved.", ++ "sub.fallbackSaveFailed": "Failed to save fallback settings", + "dash.shadowCallIntercept": "Shadow Call Intercept", + "dash.shadowCallInterceptHint": "Intercepts Codex App's background helper calls ({models}) for title generation and commit messages and redirects them to your chosen model.", + "dash.shadowCallWarning": "⚠ When enabled, ALL requests for {models} will be replaced with the selected model.", +diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts +index ec171e627..d5d29b0be 100644 +--- a/gui/src/i18n/fr.ts ++++ b/gui/src/i18n/fr.ts +@@ -305,6 +305,12 @@ export const fr: Record = { + "dash.visionTimeout": "Délai d’expiration", + "dash.visionTimeoutInvalid": "Saisissez un entier compris entre {min} et {max} millisecondes.", + "dash.visionAdvancedPopover": "Paramètres de vision avancés", ++ "sub.fallbackLabel": "Chaîne de secours des sous-agents", ++ "sub.fallbackHint": "Modèles essayés dans l’ordre lorsqu’un modèle de sous-agent est indisponible ou échoue.", ++ "sub.fallbackAdd": "Ajouter un modèle de secours…", ++ "sub.fallbackPoll": "Intervalle de vérification de disponibilité", ++ "sub.fallbackSaved": "Paramètres de secours des sous-agents enregistrés.", ++ "sub.fallbackSaveFailed": "Échec de l’enregistrement des paramètres de secours", + "dash.shadowCallIntercept": "Interception des appels fantômes", + "dash.shadowCallInterceptHint": "Intercepte les appels auxiliaires en arrière-plan de l’application Codex ({models}) pour générer les titres et les messages de commit, puis les redirige vers le modèle choisi.", + "dash.shadowCallWarning": "⚠ Lorsque cette option est activée, TOUTES les requêtes destinées à {models} sont remplacées par le modèle sélectionné.", +diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts +index c71bd7a04..747438bdc 100644 +--- a/gui/src/i18n/ja.ts ++++ b/gui/src/i18n/ja.ts +@@ -632,6 +632,12 @@ export const ja: Record = { + "sub.ultraModeLoadFail": "ウルトラモード設定を読み込めませんでした — プロキシは実行中ですか?", + "sub.ultraModeSaveFail": "ウルトラモード設定の保存に失敗しました", + "sub.ultraModeSaved": "ウルトラモードを保存しました。新しい Codex セッションから適用されます。", ++ "sub.fallbackLabel": "サブエージェントのフォールバックチェーン", ++ "sub.fallbackHint": "サブエージェントモデルが利用できないか失敗した場合に順番に試すモデルです。", ++ "sub.fallbackAdd": "フォールバックモデルを追加…", ++ "sub.fallbackPoll": "利用可能性チェック間隔", ++ "sub.fallbackSaved": "サブエージェントのフォールバック設定を保存しました。", ++ "sub.fallbackSaveFailed": "フォールバック設定の保存に失敗しました", + + // logs + "logs.title": "リクエストログ", +diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts +index 63ac30442..ecc0e4560 100644 +--- a/gui/src/i18n/ko.ts ++++ b/gui/src/i18n/ko.ts +@@ -689,6 +689,12 @@ export const ko: Record = { + "sub.ultraModeLoadFail": "울트라 모드 설정을 불러오지 못했습니다 — 프록시가 실행 중인가요?", + "sub.ultraModeSaveFail": "울트라 모드 설정 저장에 실패했습니다", + "sub.ultraModeSaved": "울트라 모드가 저장되었습니다. 새 Codex 세션부터 적용됩니다.", ++ "sub.fallbackLabel": "서브에이전트 폴백 체인", ++ "sub.fallbackHint": "서브에이전트 모델을 사용할 수 없거나 실패할 때 순서대로 시도할 모델입니다.", ++ "sub.fallbackAdd": "폴백 모델 추가…", ++ "sub.fallbackPoll": "가용성 확인 간격", ++ "sub.fallbackSaved": "서브에이전트 폴백 설정을 저장했습니다.", ++ "sub.fallbackSaveFailed": "폴백 설정을 저장하지 못했습니다", + + // logs + "logs.title": "요청 로그", +diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts +index 9f220ba2b..852eb4467 100644 +--- a/gui/src/i18n/ru.ts ++++ b/gui/src/i18n/ru.ts +@@ -687,6 +687,12 @@ export const ru: Record = { + "sub.ultraModeLoadFail": "Не удалось загрузить настройки ультра-режима — работает ли прокси?", + "sub.ultraModeSaveFail": "Не удалось сохранить настройки ультра-режима", + "sub.ultraModeSaved": "Ультра-режим сохранён. Применяется к новым сеансам Codex.", ++ "sub.fallbackLabel": "Цепочка резервных моделей субагента", ++ "sub.fallbackHint": "Модели, которые последовательно пробуются, если модель субагента недоступна или завершается ошибкой.", ++ "sub.fallbackAdd": "Добавить резервную модель…", ++ "sub.fallbackPoll": "Интервал проверки доступности", ++ "sub.fallbackSaved": "Настройки резервных моделей субагента сохранены.", ++ "sub.fallbackSaveFailed": "Не удалось сохранить настройки резервных моделей", + + // logs + "logs.title": "Журнал запросов", +diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts +index aee152cd3..71d9e7313 100644 +--- a/gui/src/i18n/tr.ts ++++ b/gui/src/i18n/tr.ts +@@ -694,6 +694,12 @@ export const tr: Record = { + "sub.ultraModeLoadFail": "Ultra modu ayarları yüklenemedi — proxy çalışıyor mu?", + "sub.ultraModeSaveFail": "Ultra modu ayarları kaydedilemedi", + "sub.ultraModeSaved": "Ultra modu kaydedildi. Yeni Codex oturumlarına uygulanır.", ++ "sub.fallbackLabel": "Alt ajan yedek zinciri", ++ "sub.fallbackHint": "Alt ajan modeli kullanılamadığında veya başarısız olduğunda sırayla denenecek modeller.", ++ "sub.fallbackAdd": "Yedek model ekle…", ++ "sub.fallbackPoll": "Kullanılabilirlik kontrol aralığı", ++ "sub.fallbackSaved": "Alt ajan yedek ayarları kaydedildi.", ++ "sub.fallbackSaveFailed": "Yedek ayarlar kaydedilemedi", + + // logs + "logs.title": "İstek Günlükleri", +diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts +index 39c9e2f0b..50659c2e6 100644 +--- a/gui/src/i18n/zh-TW.ts ++++ b/gui/src/i18n/zh-TW.ts +@@ -541,6 +541,12 @@ export const zhTW: Record = { + "sub.ultraModeLoadFail": "無法載入超級模式設定 — 代理是否在執行?", + "sub.ultraModeSaveFail": "儲存超級模式設定失敗", + "sub.ultraModeSaved": "超級模式已儲存。適用於新的 Codex 會話。", ++ "sub.fallbackLabel": "子代理備援鏈", ++ "sub.fallbackHint": "子代理模型無法使用或失敗時,依序嘗試的模型。", ++ "sub.fallbackAdd": "新增備援模型…", ++ "sub.fallbackPoll": "可用性檢查間隔", ++ "sub.fallbackSaved": "子代理備援設定已儲存。", ++ "sub.fallbackSaveFailed": "備援設定儲存失敗", + "logs.title": "請求日誌", + "logs.tabLogs": "日誌", + "logs.tabDebug": "除錯", +diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts +index 1ba4cabfa..ded94d699 100644 +--- a/gui/src/i18n/zh.ts ++++ b/gui/src/i18n/zh.ts +@@ -682,6 +682,12 @@ export const zh: Record = { + "sub.ultraModeLoadFail": "无法加载超级模式设置 — 代理是否在运行?", + "sub.ultraModeSaveFail": "保存超级模式设置失败", + "sub.ultraModeSaved": "超级模式已保存。适用于新的 Codex 会话。", ++ "sub.fallbackLabel": "子代理回退链", ++ "sub.fallbackHint": "子代理模型不可用或失败时按顺序尝试的模型。", ++ "sub.fallbackAdd": "添加回退模型…", ++ "sub.fallbackPoll": "可用性检查间隔", ++ "sub.fallbackSaved": "子代理回退设置已保存。", ++ "sub.fallbackSaveFailed": "保存回退设置失败", + + // logs + "logs.title": "请求日志", +diff --git a/gui/src/pages/Subagents.tsx b/gui/src/pages/Subagents.tsx +index 6b54d39ff..299c9306f 100644 +--- a/gui/src/pages/Subagents.tsx ++++ b/gui/src/pages/Subagents.tsx +@@ -8,7 +8,7 @@ import { useDataSurface } from "../data-surface"; + import { DataSurfaceSkeleton } from "../components/data-surface"; + import { useSubagentDelegation, type UltraModePatch, type UltraModeState } from "./use-subagent-delegation"; + +-type CachedSubagents = { available: string[]; chosen: string[] }; ++type CachedSubagents = { available: string[]; chosen: string[]; fallback: string[]; pollMs: number }; + + function seedSubagents(cacheKey: string): CachedSubagents | null { + return readSessionListCache(cacheKey); +@@ -19,6 +19,9 @@ export default function Subagents({ apiBase }: { apiBase: string }) { + const cacheKey = `ocx.subagents.v1:${apiBase}`; + const cached = seedSubagents(cacheKey); + const [chosen, setChosen] = useState(() => cached?.chosen ?? []); ++ const [fallback, setFallback] = useState(() => cached?.fallback ?? []); ++ const [fallbackPollMs, setFallbackPollMs] = useState(() => cached?.pollMs ?? 60000); ++ const [fallbackBusy, setFallbackBusy] = useState(false); + const [status, setStatus] = useState(""); + const [ok, setOk] = useState(false); + const [busy, setBusy] = useState(false); +@@ -117,16 +120,24 @@ export default function Subagents({ apiBase }: { apiBase: string }) { + const loadSubagents = useCallback(async (signal?: AbortSignal): Promise => { + // The resource layer's deadline abort must reach the wire — a signal dropped + // here is a store that can only settle by race timeout. +- const res = await fetch(`${apiBase}/api/subagent-models`, { signal }); +- const response = await readJsonOrThrow<{ available?: string[]; chosen?: string[] }>(res, t("sub.loadFail")); +- if (!response) throw new Error(t("sub.loadFail")); +- const available = response.available ?? []; ++ const [rosterRes, fallbackRes] = await Promise.all([ ++ fetch(`${apiBase}/api/subagent-models`, { signal }), ++ fetch(`${apiBase}/api/subagent-model-fallback`, { signal }), ++ ]); ++ const response = await readJsonOrThrow<{ available?: string[]; chosen?: string[] }>(rosterRes, t("sub.loadFail")); ++ const fallbackResponse = await readJsonOrThrow<{ available?: string[]; models?: string[]; pollMs?: number }>(fallbackRes, t("sub.loadFail")); ++ if (!response || !fallbackResponse) throw new Error(t("sub.loadFail")); ++ const available = response.available ?? fallbackResponse.available ?? []; + const availableSet = new Set(available); + const next = { + available, + chosen: (response.chosen ?? []).filter(model => availableSet.has(model)), ++ fallback: (fallbackResponse.models ?? []).filter(model => availableSet.has(model)), ++ pollMs: fallbackResponse.pollMs ?? 60000, + }; + setChosen(next.chosen); ++ setFallback(next.fallback); ++ setFallbackPollMs(next.pollMs); + writeSessionListCache(cacheKey, next); + return next; + }, [apiBase, cacheKey, t]); +@@ -174,7 +185,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { + const d = await readJsonOrThrow<{ applied?: string[] }>(r, t("sub.saveFailed")); + const applied = d?.applied ?? chosen; + if (d?.applied) setChosen(d.applied); +- writeSessionListCache(cacheKey, { available, chosen: applied }); ++ writeSessionListCache(cacheKey, { available, chosen: applied, fallback, pollMs: fallbackPollMs }); + setOk(true); + setStatus(t("sub.saved", { n: applied.length, cmd: "ocx sync" })); + } catch (error) { +@@ -186,6 +197,28 @@ export default function Subagents({ apiBase }: { apiBase: string }) { + } + }; + ++ const saveFallback = async () => { ++ if (fallbackBusy) return; ++ setFallbackBusy(true); ++ try { ++ const r = await fetch(`${apiBase}/api/subagent-model-fallback`, { ++ method: "PUT", ++ headers: { "Content-Type": "application/json" }, ++ body: JSON.stringify({ models: fallback, pollMs: fallbackPollMs }), ++ }); ++ const d = await readJsonOrThrow<{ models?: string[]; pollMs?: number }>(r, t("sub.fallbackSaveFailed")); ++ if (d?.models) setFallback(d.models); ++ if (d?.pollMs) setFallbackPollMs(d.pollMs); ++ setOk(true); ++ setStatus(t("sub.fallbackSaved")); ++ } catch (error) { ++ setOk(false); ++ setStatus(error instanceof Error && error.message ? error.message : t("sub.networkError")); ++ } finally { ++ setFallbackBusy(false); ++ } ++ }; ++ + // The skeleton owns the live region while this resource has no content yet. + if (state.showSkeleton && !snapshot) { + return ; +@@ -214,7 +247,13 @@ export default function Subagents({ apiBase }: { apiBase: string }) { + busy={busy} + onToggle={toggle} + onMove={move} +- onSave={() => { void save(); }} ++ onSave={() => { void save(); }} ++ fallback={fallback} ++ fallbackPollMs={fallbackPollMs} ++ fallbackBusy={fallbackBusy} ++ onFallbackChange={setFallback} ++ onFallbackPollMsChange={setFallbackPollMs} ++ onFallbackSave={() => { void saveFallback(); }} + delegation={{ + model: delegation.model, + effort: delegation.effort, + +``` + +Audit amendment: cache server-confirmed fallback values after fallback Save; roster Save preserves committed fallback snapshot, never draft. Add independent-save and remount regressions. Existing dashboard density, CSS tokens, Select and icon library retained; no concept art needed for utility editor. diff --git a/devlog/_plan/260907_lane_d/000_plan.md b/devlog/_plan/260907_lane_d/000_plan.md new file mode 100644 index 0000000000..97f6761463 --- /dev/null +++ b/devlog/_plan/260907_lane_d/000_plan.md @@ -0,0 +1,32 @@ +# Lane D release-train roadmap + +Satisfy-spec HOTL for delegated recommendations #16 → #17 → #15 → #21 → #25. +Goal: independently audited manual dependent PRs ready for main-session integration. +Scope: Claude outbound, display-name dialog, usage costs/overlays/summary, usage GUI, +plus directly required CLI/API/tests/docs. i18n files are append-only shared per main's +2026-09-07 correction. No other lane-owned files; no merge/release/main/preview. +No local test/typecheck/build/install. Remote ci.yml lane=all at final top SHA is +sole product verifier. Local source and diff checks are not execution evidence. +No user token or wall-clock bound supplied. Use existing repo/GitHub authorization. +Stop: top-head green with reviewer verdicts and layer PR/SHA evidence; otherwise +record exact DEFER/BLOCKED reasons without claiming implementation passes. +Memory/evidence: this unit plus .tmp/lane-d for review drafts. Unpublished security +material stays in scratch. Reclaim failed delegated work after two distinct agents; +other-lane file collision requires main coordination. + +## Dependency and publication map + +| Phase | Item | Outcome | Branch | +|---|---|---|---| +| 0 | Roadmap | Lock all diff plans before code | first layer docs | +| 1 | #3719 slice | Legacy redacted-before-signed SSE/JSON parity | codex/260907-d1-thinking | +| 2 | receipt guard | Prevent new intent while recovery is pending | codex/260907-d2-receipt | +| 3 | #3817 | Exact account identity resolves provider overlays | codex/260907-d3-account-prices | +| 4 | #3667 | Price editor + CLI + authoritative explicit zero | codex/260907-d4-price-editor | +| 5 | #3379 slice / #2956 | Inclusive custom usage bounds + GUI | codex/260907-d5-usage-ranges | +| 6 | readiness | Fresh top CI, screenshots and implementation audits | top branch | + +All lower subjects include [skip ci]; every push uses --no-verify. Native stack null. +Only phase 6 dispatches ci.yml lane=all; failures get Astra-high exact-log diagnosis, +fixes on their owning layer and rebase --update-refs cascade. Main alone merges. +#3719 and #3379 stay open. #2956 credit uses verified GitHub author identity. diff --git a/devlog/_plan/260907_lane_d/001_roadmap_audit.md b/devlog/_plan/260907_lane_d/001_roadmap_audit.md new file mode 100644 index 0000000000..c14a08d2da --- /dev/null +++ b/devlog/_plan/260907_lane_d/001_roadmap_audit.md @@ -0,0 +1,16 @@ +# Roadmap audit resolution + +Astra Herschel (01a07b2b-5148-73c0-a067-a13485ab32c9) returned +GO-WITH-FIXES with four bounded roadmap corrections. All are incorporated in +040_price_editor.md and 050_usage_ranges.md: register management routes; persist +manual-price display state; filter individual ledger entries before daily aggregation; +preserve apiKeyId and scan consistency; define milliseconds and explicit window bounds. + +Astra Dirac identified two thinking design blockers, recorded in 010 for re-audit: +item ownership and simultaneous reasoning/frame retention. Astra Ohm limits the account +mapping to evidenced Codex identities and requires consistent tier-namespace resolution. +The first implementation phase must finish those fold-backs before code changes. + +Only documentation has changed. Source references were inspected; product tests, +typecheck, builds and installs are NOT RUN by delegation instruction. Product acceptance +remains open until top-head Cross-platform CI executes lane=all. diff --git a/devlog/_plan/260907_lane_d/010_thinking.md b/devlog/_plan/260907_lane_d/010_thinking.md new file mode 100644 index 0000000000..f2bb7e18bf --- /dev/null +++ b/devlog/_plan/260907_lane_d/010_thinking.md @@ -0,0 +1,26 @@ +# 010 Thinking ordering +MODIFY src/claude/outbound.ts ensureBlock/closeOpenBlock and reasoning done. +Before: thinking start/deltas are emitted immediately; done closes thinking then red. +After: retain already-budgeted thinking text, defer its start/index/delta until close; +reasoning done emits red blocks before flushing pending signed thinking. Preserve text +and tool order, hidden env.txt non-disclosure, genuine signature and budget release. +MODIFY tests/claude-integration/claude-outbound.test.ts: compare collected SSE against +literal expected content and JSON for combined envelopes with preceding deltas, +multiple summary parts/red blocks, text prefix, signed-only, red-only. Check sequential +non-overlapping block indices and cancellation/overflow existing assertions. +Independent Astra audit must resolve streaming latency and allocation implications. + +Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI. + +## A audit fold-back +Astra Dirac found two blockers: unmatched-item reordering and closure memory overlap. +Track bounded reasoningItemKey separately from part identity; flush on changed explicit +item identity, and close unrelated pending thinking before another item's red blocks. +Only same identity (including both omitted) reorders red before pending thinking. +Retain thinkingBuf through signature emission as before; +queued frame budget stays authoritative, never weakened. Add near-limit valid control, +shared-budget collector control, overflow/cancel regressions. Deferred thinking is an +accepted visible-latency tradeoff; text/tool frames remain live with incremental-reader +coverage. Late done after a different emitted block cannot reorder earlier content. + +Re-audit Dirac: VERDICT PASS, blockers=0. Accept tight artificial budget capacity reduction; retain original overflow assertions and production limits. diff --git a/devlog/_plan/260907_lane_d/020_receipt.md b/devlog/_plan/260907_lane_d/020_receipt.md new file mode 100644 index 0000000000..97568c39a8 --- /dev/null +++ b/devlog/_plan/260907_lane_d/020_receipt.md @@ -0,0 +1,17 @@ +# 020 Display-name receipt recovery +MODIFY gui/src/components/ModelDisplayNameDialog.tsx. +Before: input/reset enabled whenever saving=false; input onEdit clears recovery. +After: new mutationOutcomeUnknown prop from Models.tsx recovery.confirmed===false +disables draft editing and reset, submit retains +read/retry action. Handler guards prevent synthetic events bypassing disabled controls. +Close/cancel stays available. This is bounded UI recovery, not server request ordering. +MODIFY gui/tests/models-display-name-editor.test.tsx: unknown receipt cannot replace intent; retry recovers; confirmed saved:true +and ordinary validation error remain +editable. Screenshot changed disabled input/reset with retry available. + +Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI. + +Implementation: unknown outcome guards input/reset handlers and submit, and focuses Retry +when saving fails without a receipt. Saved:true remains editable. Transport/body failure +matrix attempts a replacement intent and asserts no second PUT before read-only retry. +Astra Herschel plan verdict PASS. Screenshots and product execution await top CI artifact. diff --git a/devlog/_plan/260907_lane_d/030_account_prices.md b/devlog/_plan/260907_lane_d/030_account_prices.md new file mode 100644 index 0000000000..2ac6e9ccf4 --- /dev/null +++ b/devlog/_plan/260907_lane_d/030_account_prices.md @@ -0,0 +1,27 @@ +# 030 Account price identity +MODIFY src/usage/user-cost-overlays.ts registry refresh and signature/version. +Before: configured provider set and overlay rows only. +After: exact account identifiers/log labels from config mapped to established provider +identity. Include mapping in signature for memo and aggregate cache invalidation. +MODIFY src/usage/cost.ts resolveMatchedPrice: exact configured namespace and exact +user overlay precede account identity; unresolved suffix is never guessed/stripped. +MODIFY tests/usage/usage-cost.test.ts or existing provider-overlay tests: custom account +id, qualified id, stable log label, configured collision, unrelated hyphenated provider, +account rename/removal invalidation. Account aliases never become identity authority. +Audit determines precise supported historical labels from actual producer evidence. + +Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI. + +Astra Ohm audit corrections: config-only identity mapping supports selectable Codex +accounts, effective codexAccountLogLabel, exact ID compatibility aliases, and built-in +main/__main__. Generic OAuth stores are separate and excluded; no free-form inference. +Use exact configured provider before canonical account identity, exact override first. +Apply same namespace for context/priority/lower-bound modifiers, preserving attribution. +Include sorted mapping in version signature, but aliases/plan/reordering stay no-ops. + +Implementation: exact selectable account IDs, effective labels and main forms are resolved +from config at overlay refresh. Only identity changes bump cache versions. Exact configured +providers and explicit user rows remain isolated; context/Fast/lower-bound use the selected +price namespace while request attribution is unchanged. Existing memo fast path is retained. +Regression fixtures cover mappings, collisions, ignored aliases/invalid rows, add/remove/ +label invalidation, presentation no-ops, estimate/attempt/combo and tier parity. diff --git a/devlog/_plan/260907_lane_d/040_price_editor.md b/devlog/_plan/260907_lane_d/040_price_editor.md new file mode 100644 index 0000000000..8d79b2160e --- /dev/null +++ b/devlog/_plan/260907_lane_d/040_price_editor.md @@ -0,0 +1,44 @@ +# 040 Manual price editor +MODIFY src/usage/cost.ts userOverlayMatch: valid operator all-zero row returns user +price, while generated catalog zeros keep unknown/fallback semantics. +MODIFY src/server/management/model-routes.ts: exact-provider model-costs GET/PUT, +validate four finite nonnegative bounded rates or null reset, preserve siblings, +rollback on persist failure, no routing/catalog mutation required for price-only edits. +MODIFY src/cli/models-runtime.ts, models-runtime-subcommands.ts and capabilities.ts: +models set-price provider/model --input N --output N [--cache-read N --cache-write N] +or --auto. GET for show and PUT for set/reset through existing management client. +ADD gui/src/components/ModelPriceDialog.tsx; MODIFY Models.tsx and models-shared.ts +only as needed: edit action, load exact saved override, inputs 4 rates USD/1M, +save/reset and manual indicator. Reuse dialog/fetch/i18n patterns. All locale keys +append-only pricing.override.*. Add endpoint, CLI, estimator and GUI regressions; +register new test files in both append-only layout manifests. Public docs and generated +CLI surface map mirror actual capability entries; source-generation commands NOT RUN +locally so map is updated by its source contract without claiming verification. + +Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI. + +A fold-back: add GET/PUT entries in src/server/management/route-registry.ts. +Reuse providerModelCostsConfigError. GET returns sanitized per-provider modelCosts map; +Models owns a typed map loaded with catalog or dedicated GET, so manual badges survive +reload. CLI omitted cache-read/cache-write rates default to zero, explicitly documented. + +P revalidation/API contract: GET /api/providers/{provider}/model-costs returns +{provider,modelCosts}; PUT accepts {modelId,cost:Cost4|null}, returns +{ok:true,provider,modelId,cost}. Null deletes only that model key. Models API adds +manualPricing boolean on applicable rows so badges survive reload, while the dialog +GET owns editable rates. CLI models price reads; models set-price writes/resets. +Same C2 phase splits disjoint workers: backend API/CLI/model-row/tests; frontend dialog/ +Models/types/i18n/tests; main owns explicit-zero cost semantics, docs and manifests. +No worker commits/pushes/runs local checks. Main integrates once both return. +Main granted D exactly the zero sentence in all seven translated providers config +reference pages; leave all other sections to E/M. New i18n keys are append-only. + +Implementation checkpoint: GET/PUT editor and two CLI verbs share the four-rate store; +manualPricing is emitted only for exact stored overrides. All-zero user prices are +known-zero estimates while catalog zero fallbacks remain unchanged. API/CLI and dialog +regressions cover persistence, reset, sibling isolation, invalid input and unknown receipts. +All 9 locale catalogs gained matching append-only keys. Seven existing configuration +rows (English plus six translations) had only the zero sentence updated; zh-tw has no +modelCosts row on this baseline and was left untouched. CLI surface regenerated by its +own generator, not a build or test. New backend test names appended to both manifests. +Local suites/typecheck/build/install NOT RUN; final top CI and screenshot remain open. diff --git a/devlog/_plan/260907_lane_d/050_usage_ranges.md b/devlog/_plan/260907_lane_d/050_usage_ranges.md new file mode 100644 index 0000000000..e08d758e5b --- /dev/null +++ b/devlog/_plan/260907_lane_d/050_usage_ranges.md @@ -0,0 +1,49 @@ +# 050 Custom usage windows +REIMPLEMENT range slice from PR #2956 with Manson2438 credit; do not carry offline reports. +ADD src/usage/time-range.ts strict timestamp parser and inclusive since/until bounds; +MODIFY summary.ts accumulator interface to support bounded windows without poisoning +preset daily aggregates. Use stream ledger filtering for partial days if compact daily +partitions cannot answer exact boundaries. Reject malformed/reversed bounds at API/CLI. +MODIFY src/server/management/logs-usage-routes.ts custom-window path before preset cache, +stream/filter into isolated accumulator preserving surface/provider/model and truncation +metadata. Do not persist normalized ledger rows. Include bounds in response. +MODIFY CLI observe/capabilities usage flags and GUI Usage.tsx custom datetime inputs, +independent draft/applied bounds, cache key includes bounds, grid anchored to effective +window, clear returns to preset. All locale keys append-only usage.range.*. +Tests: inclusive boundaries, partial same-day, reversed/invalid, empty ledger, existing +provider/model/surface filters, preset cache after custom query; GUI apply/clear/errors. +Public API/CLI docs describe epoch/ISO contract and local datetime conversion. + +Verification: NOT RUN locally by user instruction; focused tests execute in final top-head Cross-platform CI. + +A fold-back: immutable window option on createUsageSummaryAccumulator; add() checks +inclusive bounds AFTER recording whole-scan snapshot timestamps but BEFORE partitioning. +clone preserves window. summarize uses window endpoint for grid, actual now for generatedAt; +retain 366-day grid cap. Custom queries use isolated row-unique accumulator via existing +getFilteredUsageAggregate with window in key. Reuse overlay/timezone revision restart +and scanner identity controls. Preserve apiKeyId and current filter echo alongside all +other filters. USAGE_RANGES remains preset-only; response range stays selected preset +with customWindow:true, since/until explicit bounds (bounds override preset). API accepts +integer epoch milliseconds or full ISO-8601 with timezone only; require both bounds; +reject negative/unsafe/date-invalid/reversed, never normalize overflow dates. +MODIFY src/cli/usage-report.ts heading prints since/until for customWindow responses. +GUI datetime values become epoch ms locally; end selected minute includes 59.999s. + +P revalidation: custom windows always filter rows before aggregation. Introduce exported +UsageTimeWindow {since:number,until:number} and immutable optional accumulator window; +snapshot timestamps update first, clone retains the window, summary returns customWindow:true +and exact since/until while actual generatedAt stays now. Partition/day filtering must not +drop the partial first day. Grid uses local calendar day boundaries and caps at 366 days. +getFilteredUsageAggregate accepts window, keys both bounds, passes window to factory and +reuses existing revision/timezone/overlay guards. Only-window queries preserve account rows. +GUI skips held/session report caching for custom windows (arbitrary keys must not grow the +preset cache); useDataSurface key still includes bounds and unsubscribed stores already evict. +Workers split backend/API/CLI/tests and GUI/i18n/tests; main owns docs/manifests/generated map. + +Implementation checkpoint: shared strict ISO/epoch-ms parser, immutable per-entry window, +window-keyed filtered cache, API and CLI inclusive bounds, exact interval heading, and +localized Usage date/time controls are implemented. Custom GUI reports bypass held caches; +calendar grid stays within the server's bounded days. Tests cover partial/inclusive bounds, +filters/accounts, cache invalidation, clone/snapshot behavior, empty/error responses and UI +apply/clear/stale-response paths. New parser test registered in both manifests. ISO fractions +beyond millisecond precision reject instead of truncating. Product execution NOT RUN locally. diff --git a/devlog/_plan/260907_lane_d/060_delivery.md b/devlog/_plan/260907_lane_d/060_delivery.md new file mode 100644 index 0000000000..38bcb1c80a --- /dev/null +++ b/devlog/_plan/260907_lane_d/060_delivery.md @@ -0,0 +1,24 @@ +# 060 Remote verification and delivery + +Consume D5's implementation checkpoint. Resolve remaining independent review feedback on +its owning layer, cascade all dependent refs, and preserve contributor trailers. Detailed +unpublished security-review notes stay in scratch. Reconcile A's added reasoning-envelope +budget arguments with D1 ordering when A reaches dev; preserve both changes. + +Fetch fresh dev before final dispatch. Run only the top branch's ci.yml workflow with +lane=all; require successful actual platform jobs including Windows on the exact head. +Download its dashboard-preview artifact and verify build-commit/build-gui-tree markers. +Capture the changed dialogs and custom Usage range with synthetic data through the existing +browser capability; publish proof images separately so evidence does not change tested code. + +Create D2-D5 PRs with the required template, screenshot, manual chain table and native +stack:null proof. Attach independent implementation/security verdicts and top CI URL to +each PR. Leave all merges and original issue/PR closure actions to the main task. +Local product tests/typecheck/build/install remain NOT RUN. D closes only when exact-head +remote evidence and the requested handoff table are complete. + +Calendar audit fold-back: custom heatmaps iterate the server's returned civil dates, +using UTC only for weekday/month layout; they do not step a local midnight cursor. +The server's backward calendar walk resets midnight after decrement and explicitly +advances to the prior existing local day if a whole-day timezone jump prevented progress. +Regressions pin America/Santiago (2026-09-05..07) and Pacific/Apia (2011-12-29..31). diff --git a/devlog/_plan/260907_next_release_recommendations/000_plan.md b/devlog/_plan/260907_next_release_recommendations/000_plan.md new file mode 100644 index 0000000000..3febda57ea --- /dev/null +++ b/devlog/_plan/260907_next_release_recommendations/000_plan.md @@ -0,0 +1,28 @@ +# 000 — Plan: next-release recommendation report (wp1) + +Unit: devlog/_plan/260907_next_release_recommendations +Class: C2 (docs-only deliverable; research via read-only explorer lanes) +Goal: rank 10–30 items to land on `dev` before the release after v2.46.0 (dev open at 2.47.0). + +## Diff-level plan +- Write scope: this directory only (000_plan.md, 010_recommendations.md). No src/gui/docs-site edits. +- Branch: codex/260907-next-release-recommendations (local commit only; no push/merge). +- Lanes (each an independent astra-high explorer, read-only): + - L1 non-draft (`review-ready` label) PRs: #3858 #3845 #3843 #3840 #3839 #3837 (+ #3748 #3742 enhancement review-ready; #2805 maintainer-sponsored) + - L2 draft bug PRs + small feature: #3863 #3862 #3860 (open, feature) #3856 #3849 #3848 #3841 #3838 #3769 (+ hygiene-blocked flags) + - L3 open bug issues without PR: #3807 #3782 #3781 #3775 #3765 #3761 #3719 #3675 #3661 #3657 #3644 #3522 #3506 #3464 #3433 + - L4 open enhancement issues + older draft feature PRs worth carrying: #3859 #3817 #3729 #3630 #3573 #3266 #3336 #3389 #2280/#2279 #3652 #3635 #2805 + - L5 devlog/_plan residual work (units dated 260905–260907, plus older units with open TODOs) + - L6 post-2.46.0 regressions: dev CI status, main..dev delta, release follow-up notes in devlog/_fin/260907_release_246 + - L7 catch-all PRs (audit round 1 blocker 1): #3833 #3810 #3741 #3738 #3709 #3663 #3648 #3639 #3532 #3463 #3458 #3451 #3350 #3349 #3340 #3283 #3282 #3252 #3080 #3025 #3010 #2956 #2921 #2881 #2562 #2527 #2462 #2366 #2362 #2355 #2351 #2244 #2230 #2213 #2033 #1645 + - L8 catch-all issues (audit round 1 blocker 2): #3777 #3774 #3705 #3667 #3666 #3494 #3459 #3417 #3379 #3377 #3376 #3375 #3320 #3255 #3245 #3191 #2894 #2834 #2811 #2730 #2511 #2495 #2455 #2358 #1811 #1782 #1711 #1533 #1416 #1213 #95 (L3 already covers #3861 #3857 #3855 #3846) + - Inventory reconciliation: 010 must carry a dated appendix listing every open PR (59 at audit time) and open issue (57) with lane + disposition, so coverage is checkable by diffing against `gh pr list`/`gh issue list`. +- Each lane returns: per item -> disposition, risk class, evidence anchors (path:line / URL), overlap notes, effort. +- Main session merges lane returns, dedups, ranks, writes 010_recommendations.md. + +## Acceptance (from goalplan c-1..c-3; tightened after audit round 1) +- 10–30 ranked items. Each item has: source id, disposition, risk class, effort, ≥1 evidence anchor gathered this session (GitHub URL or path:line), and a one-line ranking rationale under the stated criteria (user impact × risk × effort × contributor-credit cost). +- Appendix reconciles the full open PR/issue inventory (every number appears once with lane + disposition); overlaps between PRs and issues are recorded as explicit pairs. +- `bun run privacy:scan` exit 0 on the report commit; commit contains only the two files in this directory (`git show --stat` as proof). +- ≥5 anchors spot-checked live by the main session, with the anchor, command, and result recorded in 010's verification section. +- Security: only already-public evidence (existing issues/PRs/diffs) may be cited; no new weakness is written here (AGENTS.md security working notes). diff --git a/devlog/_plan/260907_next_release_recommendations/010_recommendations.md b/devlog/_plan/260907_next_release_recommendations/010_recommendations.md new file mode 100644 index 0000000000..696c974057 --- /dev/null +++ b/devlog/_plan/260907_next_release_recommendations/010_recommendations.md @@ -0,0 +1,227 @@ +# 010 — Next-release landing recommendations (dev after v2.46.0) + +Snapshot: 2026-09-07, `origin/dev@ece556a6e` (package 2.47.0). Latest exact-head Cross-platform CI on dev: success +(run 34091933836; Windows full suite dispatch-only by policy). No open 2.46 regression issue found; #3782 is the only +open 2.45-tagged report and predates 2.45. + +Method: eight read-only astra-high explorer lanes (L1–L8 in 000_plan.md) over every open PR (59) and issue (57), +plus devlog/_plan 2609xx residuals and devlog/_fin/260907_release_246. All evidence below was gathered this session +from live GitHub/git; behind-dev counts are exact-SHA comparisons against `ece556a6e`. Dispositions are +maintainer-facing judgments, not merge approvals. Carrying any contributor PR requires `cherry-pick -x` plus a +surviving `Co-authored-by` trailer (AGENTS.md "Landing another author's work"). + +Ranking criteria: user impact × inverse risk class × inverse effort × contributor-credit cost of waiting. +Effort: S ≤ half day, M ≤ 2 days, L > 2 days. + +## Ranked list (27 items) + +| # | Source | What | Cat. | Disposition | Risk / Effort | Rationale | Evidence | +|---|---|---|---|---|---|---|---| +| 1 | PR #3862 → #3861 (Ingwannu) | Admit reasoning-envelope allocations before materialization | bug | LAND_WITH_FIX (security sign-off + Windows-shard evidence) | C4 / M | Availability hardening, maintainer-authored, exact-head `ci: SUCCESS` (run 34098616286); 9 behind; draft. Highest-priority review. | `src/responses/reasoning-envelope.ts:70` at head `9bcb7748f`: `activeBudget.reserveTransient(8 * encryptedContent.length, …)`; #3861 "The unchanged base fails nine admission regressions" | +| 2 | PR #3858 → #3857 (makesomethingshit) | Pi/OpenCode Go session affinity through native Chat and bridges | bug | LAND_WITH_FIX (reconcile readiness contradiction, classify residual failures) | C3 / M | 0 behind, review-ready, strong header-capture tests. Body still says "Readiness remains blocked" while boxes are 4/4 — verify before merge. | `src/server/chat-completions.ts:174` (dev) `return handleNativeChatCompletions({`; PR diff `compat: { sendSessionAffinityHeaders: true }` | +| 3 | PR #3840 (chilung-cgu) | Route Responses-only Copilot GPT/Grok/MAI models correctly | bug | LAND_AS_IS (after ancestry refresh + head CI) | C2 / S | 13 behind, all 4 threads resolved, endpoint-capture tests 5 models × 3 inbound formats. | `src/providers/registry.ts:3084` at head: `"gpt-6-astra": "openai-responses",` | +| 4 | PR #3863 (x3M3x) | Dashboard settings load no longer blocks on Windows health probe | bug | LAND_WITH_FIX (keep fresh cache non-stale; handle probe rejection; controlled timing test) | C2 / S | 0 behind; mechanism substantiated; one CodeRabbit finding open. Windows user pain. | head `startup-health-cache.ts:66`: `return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config);`; discussion_r3948034138 | +| 5 | PR #3837 (luvs01) | Gate Kiro request diagnostics behind debug check | bug | LAND_WITH_FIX (isolate `OCX_DEBUG` in test) | C1 / S | 25 behind; CHANGES_REQUESTED by Ingwannu with one concrete test fix. | pullrequestreview-5127337985 "One test correction is needed before approval."; discussion_r3945935220 | +| 6 | PR #3843 (luvs01) | Bound streaming citation-marker span | bug | LAND_WITH_FIX (same-delta malformed text must be emitted verbatim + regression) | C2 / S | 25 behind; one unresolved major finding contradicts findings-resolved box. | head `src/responses/citation-markers.ts:78`: `MAX_STREAMING_MARKER_SPAN_LENGTH = 4_096`; discussion_r3946034145 | +| 7 | PR #3845 (luvs01) | Refuse keychain restore across provider ownership | bug | LAND_AS_IS (explicit credential-security review) | C4 / S | 25 behind; small, tests cover foreign-ref rejection and own-account restore. | head `src/providers/key-store.ts:202`: `const foreign = refs.filter(ref => !keychainReferenceBelongsToProvider(ref, name));` | +| 8 | PR #3839 + #3841 (luvs01) | Bound Anthropic web-search and vision sidecar SSE/error bodies (pair) | bug | LAND_WITH_FIX (error-body cap + cancellation tests; pin partial-description behavior) | C4 / S each | Same 64 KiB policy, disjoint files; land as a pair. #3841 is draft 0/4, #3839 review-ready. | `src/web-search/anthropic-executor.ts:226` `readBoundedText(res)`; `src/vision/anthropic-describe.ts:14` `MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024` | +| 9 | PR #3860 (RobinBially) | Opt-in Codex Desktop sign-in toggle in GUI | feature | LAND_AS_IS (security review; default OFF) | C4 / S | Became review-ready 4/4 during this session, 0 behind, screenshot present, replaces #3689. | PR body "an explicit opt-in, default **OFF**"; issuecomment-5567316521 | +| 10 | PR #3849 → #3781 (hualiny) | Admit Mihomo IPv6 fake-IP under TUN transparency exception | bug | LAND_WITH_FIX (IPv6-only path + `NO_PROXY` negative tests; SSRF boundary review) | C4 / S | 11 behind (over 10-commit readiness tolerance), 0/4 boxes; narrow patch; complements landed #3799. | head `src/lib/provider-outbound.ts:147`: `const allowMihomoIpv6FakeIp = (effectiveProxy !== null && !noProxyMatches(parsed))` | +| 11 | PR #3856 → #3855 (terrytan95) | Sustain quota window activation after reset | bug | LAND_WITH_FIX (maintainer sponsorship clears `unsponsored_surface`; serial with #3848) | C4 / M | 0 behind, 3/4 boxes, hygiene-blocked only by sponsorship gate; overlaps #3848 in `auth-api.ts`/`quota-auto-refresh.ts`. | dev `src/codex/quota-auto-refresh.ts:103`: `await warmCodexAccount(await getValidCodexToken(accountId));`; issuecomment-5565953215 "hygiene: unsponsored_surface" | +| 12 | PR #3838 (jpierrevd) | Lower Codex-private input items Console Go rejects | bug | LAND_WITH_FIX (parent-namespace child identity; keep nameless built-ins; two regressions) | C3 / M | 25 behind, 0/4; author reports 400→200 on 70-item replay. Complements #3858. Commit author identity generic — resolve before carry. | head `src/adapters/opencode-go.ts:83`: `const kept = (tool.tools as unknown[]).filter(child => claim(child));`; issuecomment-5564388455 | +| 13 | PR #2033 (louis-tepe) | Expose web-search sidecar enabled status in GET/PUT | bug | REIMPLEMENT (two serialization lines + regression; Co-authored-by) | C1 / S | 1364 behind but omission confirmed on dev; cheapest credit-preserving carry in the backlog. | PR #2033 (draft, gates PASS); L7 confirmed omission on `origin/dev` management routes | +| 14 | PR #3532 (Ingwannu) | Make CI completion audit fail closed (devlog docs) | hygiene | LAND_WITH_FIX (refresh onto dev; verify current gate names) | C0 / S | Non-draft, two doc files, 829 behind but docs-only. | PR #3532 head CI SUCCESS (runtime jobs skipped) | +| 15 | Issue #3817 (rrmlima) | Apply base-provider price overlays to all account log labels | bug | LAND_WITH_FIX (implement: use account→provider identity, no suffix stripping) | C2 / M | Cost-reporting correctness for pool users; bounded in `src/usage/cost.ts`. | dev `src/usage/cost.ts:193` comment on suffix/base-provider pricing boundary | +| 16 | Issue #3719 residual (lidge-jun) | Streaming reverses signed/redacted thinking order vs JSON | bug | REIMPLEMENT (ordering parity + tests incl. preceding deltas) | C4 / M | Concrete, explicitly deferred in release-246 review; separate from the larger replay/cache acceptance work (DEFER). | `devlog/_fin/260907_release_246/020_progress.md:9` "explicit deferral, not a fix"; dev `src/claude/outbound.ts:569` `closeOpenBlock();` before red loop at 575; JSON emits red first at 823 | +| 17 | release-246 follow-up | Display-name editor unknown-receipt recovery guard | bug | REIMPLEMENT (bounded recovery guard) | C2 / M | P2 label-only follow-up recorded at release; reversible. | `devlog/_fin/260907_release_246/090_delivery.md:29`; `ModelDisplayNameDialog.tsx:140` `disabled={saving}`; discussion_r3946496126 | +| 18 | release-246 follow-up | Publication-aware registry-smoke recovery in release.yml | hygiene | REIMPLEMENT (no republish; treat accepted publish + smoke timeout as recoverable) | C4 / M | Both 2.45/2.46 release runs hit the 5-minute smoke timeout; manual recovery each time. Release-surface → security review. | `.github/workflows/release.yml:355` `for attempt in $(seq 1 30); do`, `:363 sleep 10`; 090_delivery.md:27 | +| 19 | release-246 follow-ups (bundle) | Raycast unsupported-platform copy + CLI text assertions + 7 provider-locale editor sections + French integrations prose | hygiene | REIMPLEMENT (one docs/CLI PR) | C1 / S–M | All named at release close; zero runtime risk. | 090_delivery.md:29; discussion_r3946497677, r3946496225, r3946496426, r3946496024; `raycast-detect.ts:108` | +| 20 | 260907_code_mode_host_contract + #3782 docs | Append `040_delivery_record.md` for #3854; qualify Claude Desktop `/model` workaround; translate new code-mode paragraph (7 locales) | hygiene | LAND_WITH_FIX (docs only) | C0 / S | Closes the open unit and answers #3782 honestly (client-owned failure). | `devlog/_plan/260907_code_mode_host_contract/030_docs_and_delivery.md:80` and `:55`; `docs-site/src/content/docs/guides/claude-code.md:312`; #3782 issuecomment-5565317534 | +| 21 | Issue #3667 (nordz0r) | Manual price override editor/CLI over existing `modelCosts` | feature | REIMPLEMENT (expose existing store; resolve explicit-zero semantics) | C2 / M | Backend already exists; UI/CLI gap only. Pairs with #15. | dev `src/usage/user-cost-overlays.ts:240-250` `const costs = provider?.modelCosts;` | +| 22 | Issue #1533 (Zbyy0311) | Explain native-parent/routed-child V2 compatibility state in GUI | feature | REIMPLEMENT (state-aware guidance near preferred worker; no routing change) | C2 / S | Long-open UX ask, small, reads existing agent-settings API. | dev `src/server/management/agent-settings-routes.ts:248` | +| 23 | PR #3252 (x3M3x) | GUI editor for existing sub-agent fallback API | feature | LAND_WITH_FIX (repair JSON-encoded body; drop roster-switch claims; keep unavailable configured models; focused GUI tests) | C2 / M | 175 behind, hygiene-blocked by body format; overlaps #22's surface — land #22 guidance inside this panel. | PR #3252 gates FAIL (body) | +| 24 | Issue #3774 (leonclab) | Drag-and-drop `modelPickerOrder` | feature | REIMPLEMENT (on top of landed presets #3801) | C3 / M | Presets landed; DnD residual; define native/featured row behavior first. | dev `gui/src/model-picker-order.ts:56`; `gui/src/pages/Models.tsx:1823` | +| 25 | Issue #3379 usage-range slice ← PR #2956 (Manson2438) | Custom usage time ranges (slice only; not offline reports/picker) | feature | REIMPLEMENT slice with Co-authored-by | C2 / M | #2956 is 1304 behind/DIRTY; the range slice is small on current code. | dev `src/usage/summary.ts:15` `USAGE_RANGES = ["today", "7d", "30d", "all"]`; `gui/src/pages/Usage.tsx:14` | +| 26 | PR #3769 residual (ideabib) | Native compact 404 → routed compaction fallback (quota half already landed via #3791) | bug | REIMPLEMENT residual only (canonical-forward streaming test) | C4 / M | 180 behind, DIRTY, 3 unresolved threads; do not re-land the quota classifier. | discussion_r3943911361 "Add a canonical-forward streaming fallback test."; #3795 closed against v2.46.0 | +| 27 | PR #3336 (Liang-Psych) | Per-model pinned reasoning-effort overrides | feature | LAND_WITH_FIX (carry; adapt to current tests/docs) | C3 / M | 980 behind, 3/4 boxes, earlier cap/key findings fixed. Strongest older contributor carry; last in this batch because of drift. | head `src/server/chat-native.ts:165` `applyChatEffortCap(...)` | + +Suggested batching: items 1–8 first (bug fixes, all S/M, mostly review-ready), then 9–13 (C4 small + carries), then +14–20 (docs/release hygiene, can run in parallel), then 21–27 (feature slices as capacity allows). Serialize #11 → #3848 +(item in DEFER) on `src/codex/auth-api.ts`; serialize #2 → #12 on OpenCode Go adapter; land #22 inside #23's panel. + +## Overlap pairs recorded + +#3861↔#3862; #3857↔#3858; #3855↔#3856; #3846↔#3848 (both touch `auth-api.ts`, `quota-auto-refresh.ts`); +#3781↔#3849; #3459↔#3463; #2894↔#2921↔#3741; #3376↔#2881↔#3856; #3375↔#2562↔#3283↔#3738; #3377↔#3282; +#3379↔#2956; #1533↔#3252; #3667↔#3817↔#3666; #3630↔#3729; #2279↔#2280↔#3336; #3839↔#3841 (pair); +#3858↔#3838 (OpenCode Go); #3840↔#2805↔#3838 (registry); #3765↔#3433↔#3719 (cache/replay). + +## DEFER (needs evidence, sponsorship, or a dedicated train — not for this release) + +Issues awaiting reporter/field evidence: #3807 (raw synthetic repro), #3782 (client-owned; docs only in #20), #3775 +(gateway capability), #3765/#3433 (matched cache identity evidence), #3657 (transport boundary), #3644 (categorized +TUN/system-proxy A/B), #3522 (same-process ACL evidence), #3661 (encrypted multipart contract), #3320/#3245 (needs-info). +PRs needing security review or coordination: #3848 (61 files, LAND_WITH_FIX after #3856 and sponsorship), #3742 (Cursor +pool kernel, stale verification SHA), #3748 (telemetry ledger, 221 behind), #3833 (Command Code credential refs), +#3463, #3389, #3652, #3635 (REIMPLEMENT later), #2921, #2280, #2366, #2362, #2355, #2213, #2230, #1645, #3741, #3738, +#3709, #3663, #3639, #3451, #3350/#3349/#3340 (provider train), #3282, #3080, #2562, #2956 (beyond the #25 slice). +Issues DEFER: #3666, #3630, #2279, #1711, #3777, #3859, #3573, #3266, #3729, #3417, #3459, #2894, #3761, #3506. +devlog residuals DEFER: #3719 replay/cache acceptance, #3348-B cooldown persistence, #3383 Windows temp proposal, +split-train 840/850 evidence, image roundtrip remote/OCR, macOS client-connect stall instrumentation. + +## NOT_NOW (explicit) + +#3810 (Go runtime line; AGENTS.md "New work does not go here"), #2805 (1488 behind, CONFLICTING → REIMPLEMENT as scoped +carries later), #3458, #3025, #3010, #2881, #2527, #2462, #2351, #2244, #3283, #3648; issues #3705, #3494, #3377, +#3376, #3375, #3255, #3191, #2834, #2811, #2730, #2511, #2495, #2455, #2358, #1811, #1782, #1416, #1213, #95, #3464, +#3675, #3506; devlog: #3348-C/quota cooldown/raw-key signature, split-train modularization debt, apply-patch envelope +quotation tradeoff, Windows full-suite gate restoration (#1059 closed policy). + +## Verification (main session, live) + +Anchor spot-check on `origin/dev@ece556a6e` via `git show origin/dev: | sed -n p`: + +| Anchor | Result | +|---|---| +| `src/server/chat-completions.ts:174` | match: `return handleNativeChatCompletions({` | +| `src/codex/quota-auto-refresh.ts:103` | match: `await warmCodexAccount(await getValidCodexToken(accountId));` | +| `src/usage/summary.ts:15` | match: `USAGE_RANGES = ["today", "7d", "30d", "all"]` | +| `src/web-search/index.ts:223` | match: `if (!parsed._webSearch || isPassthrough) return undefined;` | +| `.github/workflows/release.yml:355` | match: `for attempt in $(seq 1 30); do` | +| `src/claude/outbound.ts:569` | match: `closeOpenBlock();` | +| `src/server/request-decompress.ts:22` | match: `MAX_DECOMPRESSED_BODY_BYTES = 256 * 1024 * 1024` | +| `src/usage/cost.ts:193` | near: line is the comment block the lane paraphrased | +| `src/responses/citation-markers.ts:78`, `src/server/relay.ts:462` | PR-head anchors (#3843, #3652), not dev; dev line differs as expected | + +GitHub state re-read: #3860 draft=false labels enhancement,review-ready head 0f21769f3; #3837 reviewDecision +CHANGES_REQUESTED head d5d711a7b; #3858 draft=false review-ready head 23d869350; #3862 draft=true head 9bcb7748f; +#3856 labels bug, intake: hygiene-blocked; #2033 draft, title "Expose web search sidecar enabled status". + +`bun run privacy:scan` on the report commit: see 000_plan.md acceptance; result recorded in the D attest. + +## Appendix A — open PR inventory (59) with lane and disposition + +| PR | Lane | Disposition | +|---|---|---| +| 3863 | L2 | LAND_WITH_FIX (#4) | +| 3862 | L2 | LAND_WITH_FIX (#1) | +| 3860 | L2 | LAND_AS_IS (#9) | +| 3858 | L1 | LAND_WITH_FIX (#2) | +| 3856 | L2 | LAND_WITH_FIX (#11) | +| 3849 | L2 | LAND_WITH_FIX (#10) | +| 3848 | L2 | DEFER (after #3856; sponsorship) | +| 3845 | L1 | LAND_AS_IS (#7) | +| 3843 | L1 | LAND_WITH_FIX (#6) | +| 3841 | L2 | LAND_WITH_FIX (#8) | +| 3840 | L1 | LAND_AS_IS (#3) | +| 3839 | L1 | LAND_WITH_FIX (#8) | +| 3838 | L2 | LAND_WITH_FIX (#12) | +| 3837 | L1 | LAND_WITH_FIX (#5) | +| 3833 | L4/L7 | DEFER (credential refs review) | +| 3810 | L4/L7 | NOT_NOW | +| 3769 | L2 | REIMPLEMENT residual (#26) | +| 3748 | L1 | DEFER | +| 3742 | L1 | DEFER | +| 3741 | L7 | DEFER | +| 3738 | L7 | DEFER | +| 3709 | L7 | DEFER | +| 3663 | L7 | DEFER | +| 3652 | L4 | DEFER | +| 3648 | L7 | NOT_NOW | +| 3639 | L7 | DEFER | +| 3635 | L4 | REIMPLEMENT later (DEFER) | +| 3532 | L7 | LAND_WITH_FIX (#14) | +| 3463 | L4/L7 | DEFER | +| 3458 | L7 | NOT_NOW | +| 3451 | L7 | DEFER | +| 3389 | L4 | DEFER | +| 3350 | L7 | DEFER | +| 3349 | L7 | DEFER | +| 3340 | L7 | DEFER | +| 3336 | L4 | LAND_WITH_FIX (#27) | +| 3283 | L7 | NOT_NOW | +| 3282 | L7 | DEFER | +| 3252 | L7 | LAND_WITH_FIX (#23) | +| 3080 | L7 | DEFER | +| 3025 | L7 | NOT_NOW | +| 3010 | L7 | NOT_NOW | +| 2956 | L5/L7 | DEFER (slice via #25) | +| 2921 | L4/L7 | DEFER | +| 2881 | L7 | NOT_NOW | +| 2805 | L1 | NOT_NOW (REIMPLEMENT as carries later) | +| 2562 | L7 | DEFER | +| 2527 | L7 | NOT_NOW | +| 2462 | L7 | NOT_NOW | +| 2366 | L7 | DEFER | +| 2362 | L7 | DEFER | +| 2355 | L7 | DEFER | +| 2351 | L7 | NOT_NOW | +| 2280 | L4 | DEFER | +| 2244 | L7 | NOT_NOW | +| 2230 | L7 | DEFER | +| 2213 | L7 | DEFER | +| 2033 | L7 | REIMPLEMENT (#13) | +| 1645 | L7 | DEFER | + +## Appendix B — open issue inventory (57) with lane and disposition + +| Issue | Lane | Disposition | +|---|---|---| +| 3861 | L3 | via PR #3862 (#1) | +| 3859 | L4 | DEFER | +| 3857 | L3 | via PR #3858 (#2) | +| 3855 | L3 | via PR #3856 (#11) | +| 3846 | L3 | via PR #3848 (DEFER) | +| 3817 | L4 | LAND_WITH_FIX (#15) | +| 3807 | L3/L5 | DEFER (repro) | +| 3782 | L3/L6 | DEFER; docs in #20 | +| 3781 | L3 | via PR #3849 (#10) | +| 3777 | L4/L8 | DEFER | +| 3775 | L3/L5 | DEFER | +| 3774 | L4/L8 | REIMPLEMENT (#24) | +| 3765 | L3 | DEFER | +| 3761 | L3/L5 | DEFER | +| 3729 | L4 | DEFER | +| 3719 | L3/L5 | REIMPLEMENT ordering (#16); rest DEFER | +| 3705 | L8 | NOT_NOW | +| 3675 | L3 | NOT_NOW | +| 3667 | L4/L8 | REIMPLEMENT (#21) | +| 3666 | L4/L8 | DEFER | +| 3661 | L3 | DEFER | +| 3657 | L3 | DEFER | +| 3644 | L3/L5 | DEFER | +| 3630 | L4 | DEFER | +| 3573 | L4 | DEFER | +| 3522 | L3/L5 | DEFER | +| 3506 | L3/L5 | DEFER | +| 3494 | L8 | NOT_NOW | +| 3464 | L3 | NOT_NOW | +| 3459 | L4/L8 | DEFER (via #3463) | +| 3433 | L3/L5 | DEFER | +| 3417 | L4/L8 | NOT_NOW | +| 3379 | L8 | REIMPLEMENT slice (#25) | +| 3377 | L8 | NOT_NOW | +| 3376 | L8 | NOT_NOW | +| 3375 | L8 | NOT_NOW | +| 3320 | L5/L8 | NOT_NOW (needs-info) | +| 3266 | L4 | DEFER | +| 3255 | L8 | NOT_NOW (needs-info) | +| 3245 | L5/L8 | NOT_NOW (needs-info) | +| 3191 | L8 | NOT_NOW | +| 2894 | L4/L8 | DEFER | +| 2834 | L8 | NOT_NOW | +| 2811 | L8 | NOT_NOW | +| 2730 | L8 | NOT_NOW | +| 2511 | L8 | NOT_NOW | +| 2495 | L8 | NOT_NOW | +| 2455 | L8 | NOT_NOW | +| 2358 | L8 | NOT_NOW | +| 2279 | L4 | DEFER | +| 1811 | L8 | NOT_NOW (needs-info) | +| 1782 | L8 | NOT_NOW (needs-info) | +| 1711 | L4/L8 | DEFER | +| 1533 | L8 | REIMPLEMENT (#22) | +| 1416 | L8 | NOT_NOW | +| 1213 | L8 | NOT_NOW | +| 95 | L8 | NOT_NOW (roadmap) | + diff --git a/devlog/_plan/260907_platform_validation/000_plan.md b/devlog/_plan/260907_platform_validation/000_plan.md new file mode 100644 index 0000000000..d1b3cb632f --- /dev/null +++ b/devlog/_plan/260907_platform_validation/000_plan.md @@ -0,0 +1,31 @@ +# Platform verification follow-up + +Baseline: dev `137d6a7270e7ecfb1c791993800a17c0e30022d9` (2026-09-07). + +## Objective and authority + +Satisfy the existing platform contracts for #3383, #3449, #3522 and #3573. The owner requested ordinary manual PRs, top-of-stack CI first, lower-layer CI only to diagnose a failed final run, no local test suites, push with --no-verify, admin merge after verification, and original contributor credit in commit trailers. No native GitHub stack registration. No publish, release, global settings changes, admission-limit increases, ACL relaxation, or speculative recovery policy. + +The initial assigned checkout contains unrelated dirty work and is preserved. Work lives in an isolated worktree. No SessionStart FSM binding is available in the supplied context; this record documents the work without claiming automatic loop continuation is armed. + +## Evidence and scope + +Dockerfile, compose.yaml, docker/bootstrap-token.ts and the source-build guide already exist. Cross-platform CI has no real image build/start/recreate check. #3522 requires same-process Windows recovery evidence; #3573 requires actual rejected compact-byte evidence. Existing diagnostics must be checked before adding anything. PR #3383 is a mixed historical source: only Windows temp/teardown residuals are in scope, not picker controls. + +Original Docker contributor: Buseong Kim , verified from original #3421 commit metadata. Carry this identity in commit trailers. + +## Dependency map + +1. `010_oauth_teardown.md`: drain the asynchronous ACL fixture before deletion. +2. `020_container_smoke.md`: executable isolated container acceptance probe. +3. `030_container_ci.md`: CI consumes that probe and gates its result. +4. `035_body_diagnostics.md`: distinguish declared size, observed lower bound, and decoded size without changing admission. +5. `040_residual_evidence.md`: settle the Windows/spill/compact residuals; implement only a proven narrow gap through a plan amendment, otherwise preserve open status. + +The manual review chain contains the independent OAuth fixture carry, bounded body diagnostics, the container probe, then its dependent CI integration. Independent code is prepared in disjoint files; the top CI validates their combined tree. Existing workflow triggers remain honest: final branch workflow_dispatch supplies the complete integration result; lower PR runs are not represented as passed if skipped/cancelled. Every implemented layer is reviewed, and final head is pinned before CI. After successful final CI, merge bottom-up using merge commits so reviewed commit ancestry survives. Revalidate the resulting integration and distinguish unrelated concurrent dev changes. + +## Verification and completion + +Local suites and typecheck are NOT RUN by owner instruction. Syntax and read-only diff checks are allowed. The real verifier is GitHub Cross-platform CI on the final branch, including the new Docker job. A failed final run is diagnosed on the smallest affected scope; do not repeatedly run passing gates. Independent Astra high review covers functionality and workflow/security boundaries. Security working notes remain in scratch, not this public unit. + +Completion means verified deliverable PRs merged with commit attribution, plus explicit no-op/blocked disposition for unavailable field evidence. It does not mean every original issue is fixed. New product/security policy choices remain outside scope. Evidence and final outcome are appended to this unit; workflow run URLs and SHAs are preserved. diff --git a/devlog/_plan/260907_platform_validation/001_plan_audit.md b/devlog/_plan/260907_platform_validation/001_plan_audit.md new file mode 100644 index 0000000000..96ac86c266 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/001_plan_audit.md @@ -0,0 +1,5 @@ +# Plan audit disposition + +Independent Astra high reviewer: NEAR-PASS. OAuth teardown and bounded body diagnostics passed within scope. Three Docker/CI conditions were incorporated before implementation: explicit final lane=all executed-job inventory; isolated project/image/port and bounded cleanup; concrete readiness/admission/catalog/persistence checks before and after actual replacement. + +Main judgment: pass with those amendments. Scope remains unchanged: existing Docker contract verification, test-fixture teardown, bounded diagnostics. Live spill recovery and exact historical compact-body proof remain deferred. No local suites or typecheck were run. diff --git a/devlog/_plan/260907_platform_validation/010_oauth_teardown.md b/devlog/_plan/260907_platform_validation/010_oauth_teardown.md new file mode 100644 index 0000000000..d4ee405020 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/010_oauth_teardown.md @@ -0,0 +1,14 @@ +# OAuth fixture teardown carry + +Original source: #3383 commit 51726d2c7c58146defdd6088aefa2b95a1e58553. +Original contributor: x3M3x (Git commit metadata). + +## Concrete delta + +MODIFY `tests/oauth/oauth-store-multi.test.ts` only: import flushConfigDirHardeningForTests and the async ICACLS test runner; stub synchronous and asynchronous runners consistently in setup. Change teardown to await the tracked hardening work before resetting runners/caches, restoring OPENCODEX_HOME, or removing the fixture. Preserve removeTreeWithRetry and all production semantics. Add a deterministic held-async-runner regression against the actual cleanup routine if the existing fixture seams allow it without a new production test API. + +Production path proof: store reads call hardenConfigDir; config/paths tracks asynchronous directory hardening; resetHardenedStateForTests clears caches but does not drain those jobs. Deletion retries alone do not ensure ordering. The prior carry #3258 only replaced the removal function. + +## Acceptance + +No real asynchronous ICACLS escapes the fixture runner. Cleanup waits while a controlled ACL flight is unresolved and only deletes/restores environment after completion. The same OAuth test file passes in final Linux/macOS/Windows CI. Local tests/typecheck are NOT RUN by owner instruction. No numeric-open-flags change is included without current Bun reproduction. No new API/auth policy, credentials, or production runtime change. diff --git a/devlog/_plan/260907_platform_validation/020_container_smoke.md b/devlog/_plan/260907_platform_validation/020_container_smoke.md new file mode 100644 index 0000000000..17d7139d35 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/020_container_smoke.md @@ -0,0 +1,25 @@ +# Container smoke executable + +## File delta + +NEW `scripts/ci/docker-smoke.ts`: bounded Bun-native TypeScript probe for the existing source-build Compose contract. Reuse the canonical compatibility generator and docker/bootstrap-token.ts; do not add an alternative token writer or deployment configuration. The probe creates a unique temporary Compose project and image, builds the actual Dockerfile, bootstraps a freshly generated throwaway token through stdin, starts the hub, verifies health and data-plane admission, recreates the container on the same named volumes, and verifies persistent state again. Cleanup is limited to the unique test project and its generated artifacts. Never use an operator project, host home, provider credentials, global docker prune, or real upstream inference. + +MODIFY owning documentation only as needed to explain the CI acceptance scope and its limits; no claim of upstream-provider validation. + +## Acceptance + +- Real image builds from the checkout with a generated compatibility manifest. +- Read-only/non-root Compose service becomes healthy; requests without a token are refused. +- A synthetic catalog in the separate Codex volume is served with the throwaway token, proving admission and persistence without provider access. +- /readyz succeeds separately from liveness, token reinitialization fails without replacement, and effective container restrictions are verified. +- Token/config/catalog persist across an actual container replacement (different container id, same volumes). +- Failures and cleanup are bounded; token/body contents never appear in logs. +- Existing Docker settings and defaults remain unchanged. + +Run only in final remote CI. Locally perform source/static inspection, not the smoke or a test suite. Read the current lifecycle/API contracts before implementing assertions. + +## Audit amendments + +Use explicit unique project on every Compose command, unique image tag via a temporary override, controlled Compose environment, and loopback ephemeral host port. Preserve pre-existing generated files; cleanup must fail the probe if it cannot remove its own project resources. Bound every child, output capture and cleanup; terminate/reap timed-out children. Never print raw runtime logs or complete inspect output. + +Before/after replacement: require readyz 200 with status ready; authenticated catalog 200 with exact synthetic fixture; missing/wrong token 401 for catalog, Responses and compact. Second bootstrap must fail and preserve the original token while rejecting the proposed replacement. Verify different container IDs, identical named-volume identities and persistent config/catalog evidence without reseeding; check effective non-root UID and read-only root. diff --git a/devlog/_plan/260907_platform_validation/025_container_lifecycle_mode.md b/devlog/_plan/260907_platform_validation/025_container_lifecycle_mode.md new file mode 100644 index 0000000000..c86f4fc765 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/025_container_lifecycle_mode.md @@ -0,0 +1,10 @@ +# Container lifecycle mode + +Amendment after real Docker recreation verification. Docker supervises the foreground hub and must retain persisted routed state across replacement. + +MODIFY Dockerfile runtime ENV: set existing OCX_SERVICE=1, with no service manager installation or privilege change. Preserve image digest, foreground CMD, listener authentication, separate writable homes and read-only root. +MODIFY scripts/ci/docker-smoke.ts: assert the actual container process receives service lifecycle mode. Retain the routed synthetic slug and exact token/catalog/config hashes across graceful recreation. +MODIFY tests/service/container-bootstrap.test.ts: include the runtime ENV declaration in the existing packaging contract. +MODIFY docs-site/src/content/docs/guides/remote-hub.md: document service-mode foreground lifecycle, Compose restart/recreation, and the limit on other dashboard restart paths. + +Independent Astra high lifecycle/security review accepted the bounded packaging change. Actual remote CLI comparison confirmed preservation with service mode. Final image CI must prove the same real container lifecycle; no local tests or Docker execution. This does not change shared CLI cleanup, restart policy, or authentication code. diff --git a/devlog/_plan/260907_platform_validation/030_container_ci.md b/devlog/_plan/260907_platform_validation/030_container_ci.md new file mode 100644 index 0000000000..28f3e08fc9 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/030_container_ci.md @@ -0,0 +1,21 @@ +# Container CI integration + +Depends on the committed probe from phase 1. + +## File delta + +MODIFY `.github/workflows/ci.yml`: include Dockerfile, compose.yaml, .dockerignore and docker/** in relevant scope detection; add an ubuntu-latest Docker smoke job using the existing pinned checkout and setup-project-bun action; invoke the script after installing required project dependencies if the generator needs them. Preserve read-only workflow permissions and persist-credentials false. Add the job to aggregate ci needs so failures cannot silently pass. No registry publishing, credentials, native stack integration or changes to existing suite retry/concurrency policy. + +MODIFY `tests/ci-workflows/ci-workflows.test.ts`: extend the existing source-oracle checks for scope paths, direct aggregate dependency, pinned actions, and actual probe invocation. Keep existing domain/layout registration unchanged by using the owning test file. + +MODIFY `docs-site/src/content/docs/guides/remote-hub.md`: describe image lifecycle validation and separate readiness/provider-auth limitations. + +## Acceptance and verifier + +Final-branch Cross-platform CI workflow_dispatch must run the smoke and the existing platform gates. The Docker job's failures must reach ci. Local suite/typecheck NOT RUN per owner. Independent review checks full workflow event, permission, input, credential, and cleanup boundaries before publishing. Existing source-oracle tests execute remotely in CI. + +Publish branches with --no-verify; do not claim lower-layer CI if only the final tree was tested. Final failure permits narrower runs. User authorized admin merge of verified layers; original author names/emails come from source commit metadata and are included as Co-authored-by trailers. + +## Final execution inventory + +Dispatch existing Cross-platform CI with lane=all on the immutable final head. Record each expected job and actual conclusion: Docker, four Linux shards, storage-policy, api-usage, gates, two macOS shards, macos-control, six Windows shards, keyring jobs, any selected npm packaging jobs, and ci. Aggregate green alone does not prove Windows or Docker ran. Explain legitimate scope skips instead of counting them as tests. diff --git a/devlog/_plan/260907_platform_validation/035_body_diagnostics.md b/devlog/_plan/260907_platform_validation/035_body_diagnostics.md new file mode 100644 index 0000000000..5165526583 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/035_body_diagnostics.md @@ -0,0 +1,17 @@ +# Bounded inbound-body diagnostic semantics + +Issue #3573 requests usable size evidence. The existing error stores a byte value but returns only the admission limit; the byte value currently mixes declared length, observed wire bytes, an artificial limit+1 lower bound, and exact decoded length. + +## File delta + +MODIFY `src/server/request-decompress.ts`: extend DecompressedBodyTooLargeError with a closed measurement category and retained limit, preserving existing constructor call compatibility. Annotate existing throw sites: declared_wire, observed_wire_lower_bound, decoded_exact, decoded_lower_bound. Append a bounded numeric/category suffix to the current message so existing core.ts error mapping carries it. No request body, path, headers, item counts, further inflate/read, admission-limit changes, or new retry semantics. + +MODIFY `tests/usage/request-decompress.test.ts`: extend small-cap fixtures to verify identity/gzip/zstd/deflate and declared/fragmented input semantics. In particular, limit+1 remains a lower bound, never exact size. Verify HTTP 413 and existing error code/type through existing handler mapping. Preserve stream cancellation. + +MODIFY `docs-site/src/content/docs/reference/proxy-formats.md`: explain wire declared length vs measured/lower-bound diagnostics, separately from compact-response limits. State that Bun listener rejection may happen before application diagnostics and that this does not measure the exact historical compact payload. + +## Acceptance + +Unchanged 256 MiB listener/decoder limit and rejection classification. No context-window wording that causes errors.ts to reclassify the failure. Message remains bounded, only fixed categories and finite numeric values. Negative tests run in final remote CI; no local test/typecheck. Keep #3573 open pending exact real compact evidence. + +This is a new diagnostic refinement of an issue, not a carry of a new contributor PR. Credit reporter @nowhere1975 in commit prose without inventing name/email. Any borrowed existing PR patches must additionally retain their actual git author trailers. diff --git a/devlog/_plan/260907_platform_validation/040_residual_evidence.md b/devlog/_plan/260907_platform_validation/040_residual_evidence.md new file mode 100644 index 0000000000..f5466a4d38 --- /dev/null +++ b/devlog/_plan/260907_platform_validation/040_residual_evidence.md @@ -0,0 +1,15 @@ +# Windows and request diagnostic residuals + +## Read-only targets + +- #3383: inspect current PR and merged descendants for Windows temp creation and OAuth teardown. Confirm current source behavior and test coverage before proposing a residual patch. No picker UI changes. +- #3522: inspect response spill telemetry and fresh-versus-memoized timeout handling. The acceptance is recovery within the same affected Windows process; generic synthetic success does not prove the reported process recovered. +- #3573: inspect decompression rejection diagnostics and exact latest issue measurements. Serialized journal size and normal requests after raising a cap do not prove the rejected compact payload size or compact success. + +## Conditional delta + +No production edit is pre-approved by this document without a source-grounded residual. If the existing code covers the measurement, record the missing field evidence and leave the issue open. If a specific content-free diagnostic is missing, amend with exact files, field flow and negative assertions before implementation. Never change admission caps, parse a rejected body to count items, relax ACLs, clear memo state, or choose a new recovery/retry policy. + +## Completion + +Record source/commit evidence, original contributor attribution where code is carried, and a separate status per candidate: already implemented, proven patch delivered, or blocked on field evidence. Do not close an original feature PR or issue merely because one residual probe passes. diff --git a/devlog/_plan/260907_release_train/000_plan.md b/devlog/_plan/260907_release_train/000_plan.md new file mode 100644 index 0000000000..fc5510ad37 --- /dev/null +++ b/devlog/_plan/260907_release_train/000_plan.md @@ -0,0 +1,76 @@ +# 000 — Release train 260907: land ranked recommendations on dev (loop-in-loop) + +Source of items: `devlog/_plan/260907_next_release_recommendations/010_recommendations.md` (27 ranked items). +Base: `origin/dev@ece556a6e` (2.47.0). Goalplan: `release-train-260907-land-ranked-recommendations`. +Class: C4 (release train; admin merges; contributor credit). Full PABCD per work-phase; delegated threads run their own cxc-loop. + +## Common rules (verbatim for every lane, main and delegated) + +1. No local test suite, typecheck, build, or install. Label them NOT RUN. Remote CI is the only verifier. +2. `git push --no-verify` always. +3. Manual dependent PR chains only (`stack: null`; never GitHub native stacks). Every commit on lower layers carries `[skip ci]` in its + subject (GitHub suppresses `pull_request` runs only when the PR HEAD commit carries it); the chain's top head runs Cross-platform CI via + `gh workflow run ci.yml --ref -f lane=all` so the Windows shards are included (ordinary PR runs skip them). +4. If top-head CI is red: dispatch astra-high explorer subagents to diagnose the exact job log, fix sequentially on the owning layer, + cascade (`git rebase --update-refs`), rerun top-head CI. Never weaken a production assertion; controlled baseline + failing mutant for timing changes. +5. Integration = the Track 2/3 procedure (rollout 01a0778a-b74a / 01a0778a-c620): the chain is verified once at its top head; lower PRs are + merged bottom-up into `dev` as history-only steps whose cumulative tree at the top equals the CI-tested tree (`git rev-parse ^{tree}` + vs tested `^{tree}` after the last merge; if dev advanced, cascade + rerun top CI first). Preconditions per merge: fresh `git fetch origin dev`, + PR head/base/repo refreshed, no unresolved non-outdated threads, no outstanding maintainer CHANGES_REQUESTED, required gates (enforce-target, + hygiene, label) green on the head, actor = lidge-jun (admin). The PR body records the MAINTAINERS.md integration decision and the exact top-head + CI run id ("maintainer integration, not self-approval"). `delete_branch_on_merge=true` → retarget the immediate child to `dev` before merging its parent. + Authorization for rule 1 and admin merge: the user's instruction in this thread ("로컬 스위트 금지 … no verify로 푸시 … 하위는 ci돌리지 않고 가장 상위만"). + Pre-merge (prospective) check, before the first merge of a chain: pin every layer head SHA; compute the expected cumulative tree by + `git merge-tree --write-tree origin/dev ` (or a scratch merge in a temp worktree) and require it to equal the tested `^{tree}` + (i.e. dev has not advanced under the chain; if it has, cascade and rerun top CI). Intermediate layers become real `dev` states, so each + layer must be standalone-correct (own thesis, builds in isolation by construction of the chain). Post-merge: compare the final merge's + tree to the tested tree; expected advancement from the chain's own merges is the only allowed delta. +6. Immediately after each landing: comment on the original PR and issue with the landing SHA. Close the original PR always (superseded/carried). + Close the issue only when the item fully resolves it; for slices (#3719 ordering, #3379 ranges, #3782 docs, #3774 DnD, #3769 residual) comment + with the landed slice and the explicit residual, keep the issue open. Use `Closes #n` in PR bodies only for full resolutions. +7. Contributor credit: `git cherry-pick -x` for carried commits; every carried/reimplemented change carries a `Co-authored-by: ` trailer resolved from the PR author (not the generic commit author). CREDITS.md must not grow. +8. PR body follows `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Verification with NOT RUN labels, Checklist) plus the manual chain table. GUI-touching PRs include a screenshot. +9. Ancestry proof after merge: `git fetch origin dev && git merge-base --is-ancestor FETCH_HEAD` → 0. +10. Security surfaces (auth, credentials, workflows, release.yml) get an independent astra explorer review before merge; the review verdict is pasted into the PR. + +## Lane split (disjoint write sets; conflicting items share a lane) + +| Lane | Owner | Items (rank) | Primary files | Chain shape | +|---|---|---|---|---| +| M (main, this thread) | main session | #3 #3840 Copilot routing · #12 #3838 OpenCode Go input items (moved from A: shares `registry.ts`) · #5 #3837 Kiro debug gate · #6 #3843 citation span · #7 #3845 keychain restore · #14 #3532 docs (bottom, docs-only) · #13 #2033 web-search enabled (top) | `src/providers/registry.ts`, `src/adapters/opencode-go.ts`, `src/adapters/kiro*`, `src/responses/citation-markers.ts`, `src/providers/key-store.ts`, `src/server/management/config-routes.ts` (M owns; C's #3863 must not touch it — its settings change lives in `startup-health-cache.ts`/settings route only), `docs-site/.../guides/providers.md` (M owns; A's #3858 provider-guide hunk is re-applied by M after A lands), devlog | 7-layer chain: #3532 → #3840 → #3838 → #3837 → #3843 → #3845 → #2033 (top) | +| A (delegated) | thread A | #1 #3862 reasoning envelope admission · #2 #3858 Pi session affinity · #26 #3769 residual compact fallback | `src/responses/reasoning-envelope.ts`, translator budget, `src/server/chat-completions.ts`, `src/server/chat-native.ts`, `src/clients/config-export.ts`, `src/server/responses/core.ts` (A owns), compaction fallback | 3-layer chain: #3862 → #3858 → #3769. Windows shards required for #3862 (dispatch lane=all). Do NOT edit `docs-site/.../guides/providers.md` — hand the hunk to M in the final report. | +| B (delegated) | thread B | #11 #3856 quota activation · #10 #3849 Mihomo IPv6 · #3848 (#3846) DEFER by default; attempt only after #3856 lands and only the runtime slice without GUI i18n/docs (i18n + `codex-integration.md` belong to C) | `src/codex/quota-auto-refresh.ts`, `src/codex/auth-api.ts`, `src/lib/provider-outbound.ts`, `src/types/config.ts` (B owns; E's #3336 config field is added by E after B lands) | chain #3856 → #3849 | +| C (delegated) | thread C | #8 #3839+#3841 sidecar bounds · #4 #3863 Windows health probe · #9 #3860 Desktop sign-in toggle · #23 #3252 subagent fallback GUI (+ #22 #1533 guidance inside it) | `src/web-search/anthropic-executor.ts`, `src/vision/anthropic-describe.ts`, `src/server/startup-health-cache.ts`, settings route (not `config-routes.ts` — if #3863 needs it, coordinate through main), `gui/src/i18n/*.ts` (C owns all i18n edits), `docs-site/.../guides/codex-integration.md` (C owns), agent-settings GUI | chain #3839 → #3841 → #3863 → #3860 → #3252 | +| D (delegated) | thread D | #16 #3719 thinking order parity (slice; issue stays open) · #17 display-name receipt guard · #15 #3817 price overlay · #21 #3667 price editor · #25 #3379 usage ranges slice (←#2956; issue stays open) | `src/claude/outbound.ts`, `gui/.../ModelDisplayNameDialog.tsx`, `src/usage/cost.ts`, `src/usage/user-cost-overlays.ts`, `src/usage/summary.ts`, `gui/src/pages/Usage.tsx` | chain in that order | +| E (delegated) | thread E | #18 release.yml smoke recovery · #19 Raycast/locale docs bundle · #20 code-mode delivery record + Desktop /model docs + translations · #24 #3774 picker DnD (slice; issue stays open) · #27 #3336 pinned effort (waits for A on `core.ts` and B on `config.ts`; rebase onto dev after both land) | `.github/workflows/release.yml`, docs-site locales (not the two guide files owned by M/C), devlog, `gui/src/model-picker-order.ts`, `src/server/chat-native.ts` (after A) | release.yml as its own PR (security review); docs chain; #3774 separate PR; #3336 last | + +Single-owner files (audit round 1): `registry.ts`, `config-routes.ts`, `guides/providers.md` → M; `responses/core.ts` → A; `types/config.ts` → B; +`gui/src/i18n/*`, `guides/codex-integration.md` → C. Ownership transfers: `core.ts` and `config.ts` transfer to E once A's and B's chains are +ancestors of `dev` (E verifies with `git merge-base --is-ancestor` before editing). Cross-lane prerequisites (executable handoffs): +- A#3858 lands before M#3838 (both touch OpenCode Go); M rebases its chain onto dev after A reports landing and re-applies A's `providers.md` hunk. +- #3863's `config-routes.ts` wiring (replace the blocking startup-health read) is implemented by M as a layer in M's chain after C reports its + `startup-health-cache.ts` layer landed; C ships the cache/probe change with the existing route call unchanged and names the exact call site in its report. +- E#3336 after A and B; E#3774/#18/#19/#20 have no prerequisites. +Shared manifests `tests/fixtures/test-layout-expected.json` + `scripts/test-layout/layout.json` are explicitly multi-writer (append-only); the lane +that cascades last resolves. Amendment (wp1, lane D report): `gui/src/i18n/*.ts` are also multi-writer append-only — each lane adds its own +feature-namespaced keys at the end of the relevant section in every locale (gui/AGENTS.md), never edits or removes existing keys; C's exclusive +ownership is withdrawn. Lane B additionally owns `docs-site/**/getting-started/how-it-works.mdx` (en, ja, ko, ru, zh-cn) for the #3856 carry only. +Write sets are otherwise disjoint. Any lane that must touch another lane's owned file stops and reports to main instead of editing. + +## Delegated thread packet (sent verbatim with lane-specific rows) + +TASK: run cxc-loop (HOTL) in your own worktree to land lane items on dev. SCOPE: the files above plus their tests/docs. MUST DO: common rules 1–10; +PABCD per layer with an independent astra explorer audit; report landing SHAs, CI run ids, closed PR/issue links. MUST NOT: touch other lanes' files, release, +publish, native stacks, local suites, force-push without lease. PROOF: ancestry command output, CI run URL, closure comment URLs. RETURN: a final message +with a table item → disposition → SHA → CI → closures, and DEFER reasons. + +## Merge serialization + +Main session is the only actor that admin-merges. Delegated threads bring a chain to "top-head CI green + review pasted" and report; the main session +refreshes dev, re-checks tree equality, merges bottom-up, retargets children, closes originals. If dev advanced under a chain, the owning thread cascades and reruns top CI before merge. + +## Acceptance (goalplan c-1..c-4) + +Every attempted item landed with ancestry proof or DEFER/BLOCKED with reason; each merged chain has an exact-head CI run id; original PRs closed with SHA +comments and credit trailers, issues closed only on full resolution (slices commented and kept open per rule 6); final readiness doc `090_readiness.md` +committed with privacy:scan exit 0. diff --git a/devlog/_plan/260907_release_train/010_wp1_execution.md b/devlog/_plan/260907_release_train/010_wp1_execution.md new file mode 100644 index 0000000000..064568391d --- /dev/null +++ b/devlog/_plan/260907_release_train/010_wp1_execution.md @@ -0,0 +1,62 @@ +# 010 — wp1 execution log (main lane M + dispatch) + +## Dispatch (2026-09-07 ~09:20Z) +Threads created (gpt-6-astra, high): A 01a07b28-06e1-77e3-a323-1e400fd777ca, B 01a07b28-06f3-7cd0-ba8a-c646d4dc5c11, +C 01a07b28-0793-7ff1-abb6-adbbb31d1c72, D 01a07b28-072f-7b93-b840-0f0f16b0ec33, E 01a07b28-06f3-7cd0-ba8a-c62b1f7d1d94. +Ownership amendments accepted during wp1: B owns how-it-works.mdx (en+4) for #3856; i18n is append-only multi-writer; +E owns reference/configuration/providers.md locales for #19; D owns the single modelCosts zero sentence in those files for #3667. + +## Main lane M chain (PRs #3865 → #3870) +| Layer | PR | Branch | Head | Source | Notes | +|---|---|---|---|---|---| +| 1 | #3865 | codex/rt-m1-3532 | f1604c6b2 | #3532 Ingwannu | cherry-pick -x, [skip ci] | +| 2 | #3866 | codex/rt-m2-3840 | 98564bdbf | #3840 chilung-cgu | 5 commits squashed (merge commit in source), [skip ci] | +| 3 | #3867 | codex/rt-m3-3837 | 6061dcce0 | #3837 luvs01 | + test isolation fix for discussion_r3945935220 | +| 4 | #3868 | codex/rt-m4-3843 | 00b74c720 | #3843 luvs01 | + same-delta fix for discussion_r3946034145 | +| 5 | #3869 | codex/rt-m5-3845 | 924b65799 | #3845 luvs01 | security review PASS pasted in PR body | +| 6 | #3870 | codex/rt-m6-2033 | 6eadb1658 | #2033 louis-tepe (reimplemented) | top; amended after first top CI | + +Independent chain review (astra explorer): PASS, no blockers; security review of #3845 PASS. + +Top CI history: +- run 34105730157 @911047281: test 2/4 FAIL — `tests/vision/vision-anthropic.test.ts:342` exact-equality on webSearch body lacked the new `enabled` key (two assertions). Fixed in 6eadb1658 (amend of layer 6). Run cancelled. +- run 34106345180 @6eadb1658 (workflow_dispatch lane=all): queued behind a 60+ run backlog (all lanes dispatching simultaneously). Duplicate pull_request run 34106351272 cancelled. + +## Lane status (from wait_threads snapshots) +- A: chain #3879 → #3880 → #3881 published, three-layer source/security audits PASS, top fa9c1ee68 CI queued. +- B: chain #3871 (#3856) → #3872 (#3849); top CI: Linux test 3/4 failure under analysis by lane B. +- C: chain c1…c5 (#3839, #3841, #3863, #3860, #3252) with GUI re-audit PASS; top 8f8ac0d82 CI requested. +- D: #3877 (#3719 ordering) + name-guard layer + price overlay in progress; audits PASS on first two. +- E: #3864 (#18 release.yml) CI in progress with security audit; #19/#20 handoff patches prepared against ece556a6e. + + +## Landing (wp1 D, 2026-09-07 ~10:40Z) +| Layer | PR | Merge SHA | Original closed | +|---|---|---|---| +| 1 | #3865 | 7f2fb922c | #3532 | +| 2 | #3866 | dcec71715 | #3840 | +| 3 | #3867 | 0ef7d2906 | #3837 | +| 4 | #3868 | 99451df82 | #3843 | +| 5 | #3869 | 0719457d1 | #3845 | +| 6 | #3870 | d00615d56 | #2033 | + +Chain-top CI: run 34106345180 @6eadb1658 (lane=all) success, aggregate `ci` success. Prospective merge tree `git merge-tree --write-tree origin/dev codex/rt-m6-2033` = 7621cac89 = tested tree; post-merge `origin/dev^{tree}` = 7621cac89. Every layer head and d00615d56 are ancestors of fetched dev. Stale CodeRabbit trailer findings on #3869/#3870 replied (heads carry trailers). Lanes notified of the new dev head; A told that M#3838 follows A#3858. + + +## wp2 amendments (user instruction, 2026-09-07 ~10:50Z) +- CI runner saturation: all queued Cross-platform runs cancelled; one chain at a time. Order: B → A → M7 (#3882) → C → E #3864 → D → E rest. +- Per-chain gate excludes Windows shards and macos control; they run once on the final release-train head (wp3). +- Lane B landed: #3871 (62fe747af) → #3872 (ddee5e8b4); tree 58536270a == tested; run 34111578200 (Linux 1/2/4, macOS 1/2, gates, policy, api, keyring, npm, docker green; test 3/4 = prompt-text-probe timing flake, untouched by B; Windows/control cancelled by policy). Closed #3856, #3849, issue #3855; #3781 slice comment. +- Lane A landed: #3879 (b0bcb4b10) → #3880 (dac7e28c4) → #3881 (76436a3ee); tree d4f095822 == tested; run 34113638182 (all non-Windows/control jobs green). Closed #3862/#3858/#3769, issues #3861/#3857. +- M7 #3882 (citation whole-string/streaming parity, found by lane A composition audit) merged 6389787dc; M8 #3888 (providers.md hunk from A) merged 522ce5f8c; run 34114667385 green on non-Windows/control jobs. +- Slot order now: C → E #3864 → D → E docs/#3774/#3336. +- Lane C landed: #3873 (f46a7f49c) → #3874 (3f07e09bc) → #3875 (686cb127c) → #3876 (2eec04fe1) → #3878 (d0fca4a9b); tree e0b0e5886 == tested; run 34116228181 aggregate ci success (attempt 2 after a macos 1/2 20-min hang in codex-inject-write-lock; cause unproven, no code change). Closed #3839/#3841/#3860/#3252, issue #1533. #3863 reopened: contributor widened it mid-train (retitled, +2 commits) — only the original health-cache commit landed via #3875. +- Lane E #3864 (release.yml registry-smoke recovery, security review PASS) merged f4a4b468f; run 34119094967 green on non-Windows/control jobs. +- Slot order now: D → E docs (#3883/#3884) → #3887 → #3892 → final Windows/control run on the train head. +- Lane D landed: #3877 (4fe4ad8df) → #3902 (d05250de5) → #3903 (cb1113f6d) → #3904 (29405d314) → #3905 (da707ccb6); tree ded24302f == tested; run 34120761219 (non-Windows/control jobs green; two CI-found repairs: react-compiler EffectSetState in ModelPriceDialog, GUI test alert selectors). Closed issues #3817/#3667, PR #2956 (slice); #3719/#3379 slice comments, kept open. +- Remaining: E docs (#3883/#3884, run 34121907231) → #3887 (#3774 DnD) → #3892 (#3336) → final Windows/control run on train head. +- Lane E docs landed: #3883 (1649247c1) → #3884 (74089fdc3); tree c415b6abd == prospective merge tree (differs from tested 986ae11d only by D's landed files; shared locale reference files auto-merged in disjoint sections). run 34121907231. #3782 commented (docs caveat, stays open). +- Lane E #3887 (#3774 DnD slice) merged 1e188b787; tree 139cade3f == tested; run 34124333662 (two CI-found repairs: EffectSetState lint in ModelPickerOrderEditor, stale-GET fixtures). #3774 slice comment, stays open. +- Remaining: #3892 (#3336) → final Windows/control run on train head → wp3 readiness doc. +- Lane E #3892 (#3336 carry + pricing-PUT race fix) merged f802f7112; tree 402b8e750 == tested; run 34126879673. Closed #3336. +- All chains landed. Final train head dev f802f7112; full lane=all (Windows 6 + macos control) dispatched: run 34127950924. diff --git a/devlog/_plan/260907_release_train/090_readiness.md b/devlog/_plan/260907_release_train/090_readiness.md new file mode 100644 index 0000000000..adf2363842 --- /dev/null +++ b/devlog/_plan/260907_release_train/090_readiness.md @@ -0,0 +1,69 @@ +# 090 — Release-train readiness (dev after v2.46.0) + +Train head: `origin/dev@f802f7112` (2.47.0). Base: `ece556a6e`. Delta: 28 PR merges, 207 files, +11,450 / −491. +Source plan: `010_recommendations.md` (27 ranked items). Execution log: `010_wp1_execution.md`. + +Policy (user instruction, this train): no local suites/typecheck/build/install (NOT RUN); `--no-verify` pushes; manual dependent chains (`stack: null`); +one chain's top head on Cross-platform CI at a time; per-chain gate = Linux 4 + macOS 2 + gates/storage/api/keyring ×3/npm ×3/docker; +Windows 6 shards + macos control once on the final train head; admin merges recorded in each PR body with exact-head evidence; +originals closed with landing SHA and `Co-authored-by` trailers on every carried/reimplemented commit. + +## Landed (ranked item → merge) + +| # | Item | Landed via | Merge SHA | Chain-top CI | Original disposition | +|---|---|---|---|---|---| +| 1 | #3862 reasoning-envelope admission (Ingwannu) | #3879 | b0bcb4b10 | 34113638182 | PR closed; #3861 closed | +| 2 | #3858 Pi/OpenCode Go affinity (makesomethingshit) | #3880 (+ docs #3888 522ce5f8c) | dac7e28c4 | 34113638182 / 34114667385 | PR closed; #3857 closed | +| 3 | #3840 Copilot Responses-only routing (chilung-cgu) | #3866 | dcec71715 | 34106345180 | PR closed | +| 4 | #3863 Windows health probe (x3M3x) — original commit only | #3875 | 686cb127c | 34116228181 | PR reopened: contributor widened scope mid-train (+2 commits) | +| 5 | #3837 Kiro debug gate (luvs01) + test isolation | #3867 | 0ef7d2906 | 34106345180 | PR closed | +| 6 | #3843 citation span bound (luvs01) + same-delta fix; parity follow-up | #3868, #3882 | 99451df82, 6389787dc | 34106345180 / 34114667385 | PR closed | +| 7 | #3845 keychain restore ownership (luvs01), security review PASS | #3869 | 0719457d1 | 34106345180 | PR closed | +| 8 | #3839 + #3841 Anthropic sidecar bounds (luvs01) | #3873, #3874 | f46a7f49c, 3f07e09bc | 34116228181 | PRs closed | +| 9 | #3860 Desktop sign-in opt-in, default OFF (RobinBially) | #3876 | 2eec04fe1 | 34116228181 | PR closed | +| 10 | #3849 Mihomo IPv6 fake-IP TUN (hualiny) | #3872 | ddee5e8b4 | 34111578200 | PR closed; #3781 slice comment, open | +| 11 | #3856 quota window activation (terrytan95) | #3871 | 62fe747af | 34111578200 | PR closed; #3855 closed | +| 12 | #3838 OpenCode Go input items (jpierrevd) | — | — | — | **DEFER**: planned as M layer after A#3858; not started (see Remaining) | +| 13 | #2033 web-search enabled state (louis-tepe) reimplemented | #3870 | d00615d56 | 34106345180 | PR closed | +| 14 | #3532 CI audit docs (Ingwannu) | #3865 | 7f2fb922c | 34106345180 | PR closed | +| 15 | #3817 price overlay identity (rrmlima) | #3903 | cb1113f6d | 34120761219 | issue closed | +| 16 | #3719 thinking order parity (slice) | #3877 | 4fe4ad8df | 34120761219 | issue slice comment, open | +| 17 | display-name receipt guard | #3902 | d05250de5 | 34120761219 | — | +| 18 | release.yml smoke recovery, security review PASS | #3864 | f4a4b468f | 34119094967 | — | +| 19 | Raycast/CLI/locale docs bundle | #3883 | 1649247c1 | 34121907231 | — | +| 20 | code-mode record + Desktop /model caveat + translations | #3884 | 74089fdc3 | 34121907231 | #3782 commented, open | +| 21 | #3667 manual price editor (nordz0r) | #3904 | 29405d314 | 34120761219 | issue closed | +| 22 | #1533 V2 compatibility guidance (Zbyy0311) | #3878 | d0fca4a9b | 34116228181 | issue closed | +| 23 | #3252 sub-agent fallback GUI (x3M3x) | #3878 | d0fca4a9b | 34116228181 | PR closed | +| 24 | #3774 picker drag-and-drop (leonclab, slice) | #3887 | 1e188b787 | 34124333662 | issue slice comment, open | +| 25 | #3379 usage ranges slice (from #2956, Manson2438) | #3905 | da707ccb6 | 34120761219 | #2956 closed; #3379 slice comment, open | +| 26 | #3769 residual compact fallback (ideabib) | #3881 | 76436a3ee | 34113638182 | PR closed | +| 27 | #3336 pinned reasoning effort (Liang-Psych) + pricing-PUT race fix | #3892 | f802f7112 | 34126879673 | PR closed | + +26 of 27 items landed (item 12 deferred). Every merge SHA above is an ancestor of `origin/dev@f802f7112`; every chain's post-merge dev tree +equalled its CI-tested tree (or, for the E docs chain, the prospective `git merge-tree` result after D landed). + +## Final train-head CI (Windows + macos control) + +Run 34127950924 @f802f7112 (workflow_dispatch, lane=all): Linux 4/4, macOS 1/2 + 2/2, Windows 6/6, gates, storage policy, api usage, +keyring ×3, npm-global ×3, docker smoke = success. `macos control` attempt 1 failed on one test +(`tests/responses/responses-state.test.ts:1552` "shutdown fallback prices the job-owned superseded generation before publishing": +ETIMEDOUT from an 80 ms wall-clock fallback reserve that the test does not freeze; 21,404 pass / 1 fail). Independent diagnosis: FLAKE — +the train did not touch `src/responses/state.ts`, the test, spill/ACL helpers or translator-budget; the same job passed on ece556a6e and on the +C chain head. The failed job alone was rerun (attempt 2) but was cancelled by ref concurrency when an unrelated docs PR (#3910, 8bc9e4ee2, SPONSORS.md + README) pushed to dev at 14:06Z. A fresh lane=all dispatch on dev@8bc9e4ee2 (f802f7112 + that docs-only commit) — run 34131381795 — passed every job: Linux 4/4, macOS 1/2 + 2/2, **macos control**, **Windows 6/6**, gates, storage policy, api usage, keyring ×3, npm-global ×3, docker smoke, aggregate ci = success. That run is the final train-head evidence. + +## Remaining / deferred + +- Item 12 #3838 (OpenCode Go input-item normalization, jpierrevd): not carried — DEFER to the next train; needs the parent-namespace child identity and + nameless built-in fixes from the review, on top of #3880. +- #3863 (x3M3x): reopened; the contributor widened it (combo capabilities, archive retention, health-refresh rejection guard). Only the original + startup-health-cache commit landed (#3875). Contributor to rebase onto dev for the rest. +- #3848 (#3846, shaun0927): DEFER by plan (sponsorship + 61-file scope); untouched. +- Slices kept open: #3719 (live replay/cache acceptance), #3379 (selector rename), #3774 (native/featured rows), #3781 (authenticated TUN acceptance), #3782 (client-owned). +- Known pre-existing flake to fix separately: `responses-state.test.ts` shutdown-fallback test should freeze `Date.now()` like its neighbour at :1494. + +## Release readiness + +dev@8bc9e4ee2 (train head f802f7112 + docs #3910) is release-candidate ready: full matrix green on run 34131381795. Version line is already 2.47.0 (opened in #3850). +Promotion to preview/main and npm publish are outside this train's scope. + diff --git a/devlog/_plan/260907_release_train_b/000_plan.md b/devlog/_plan/260907_release_train_b/000_plan.md new file mode 100644 index 0000000000..be31f8e242 --- /dev/null +++ b/devlog/_plan/260907_release_train_b/000_plan.md @@ -0,0 +1,11 @@ +# Lane B delivery roadmap + +Satisfy-spec HOTL, triggered by delegated release-train packet. Goal: prepare a manual #3856 -> #3849 carry chain for main-session integration. No merges, releases, installs, local tests/typechecks/builds, native stacks, or edits to other lane files. Resources: existing git/gh and astra reviewers; user set no token/cost/time limit. Stop after exact top-head remote CI success, independent security verdicts, and handoff evidence. BLOCKED means a concrete unresolved owner/security/CI condition; #3848 is DEFER until #3856 lands. Main reclaims after two distinct failed leaf packets; new worker scope requires plan amendment. + +Memory/evidence: this neutral roadmap, `.tmp/lane-b/` for all security work notes, `.codexclaw/` for FSM/goalplan. Escalate cross-lane conflicts to main. How-it-works English/ja/ko/ru/zh-cn ownership was explicitly assigned to B by main. No automatic peer writes beyond collision coordination. + +1. Docs-only roadmap audit and lock. +2. Carry quota activation original commits with cherry-pick -x and contributor trailers; inspect default-off, identity and pending-state contracts. Lower layer code verification is deferred to top CI by explicit user instruction; its D certifies carry preparation, not runtime success. +3. Carry Mihomo transport commit plus IPv6-only and canonical NO_PROXY/unsafe companion regressions. Publish manual chain, independently review final implementation, dispatch ci.yml lane=all only on top. Repair lower layers sequentially and cascade with rebase --update-refs. + +Verifier: gh workflow run ci.yml --ref codex/260907-b-mihomo-ipv6 -f lane=all; read exact head SHA and every job including Windows shards. Local product commands NOT RUN by user instruction. Inspect workflow definitions instead of executing local verifiers. No claims of live TUN validation; deterministic resolver/pinned transport tests are remote CI proof. diff --git a/devlog/_plan/260907_release_train_b/010_carry.md b/devlog/_plan/260907_release_train_b/010_carry.md new file mode 100644 index 0000000000..cde8f79785 --- /dev/null +++ b/devlog/_plan/260907_release_train_b/010_carry.md @@ -0,0 +1,3 @@ +# Quota activation carry + +Detailed working plan: `.tmp/lane-b/010_carry.md` (gitignored security work space). Public source and contributor provenance are recorded in the roadmap. Only published outcomes will be added here. diff --git a/devlog/_plan/260907_release_train_b/020_carry.md b/devlog/_plan/260907_release_train_b/020_carry.md new file mode 100644 index 0000000000..10d853e99e --- /dev/null +++ b/devlog/_plan/260907_release_train_b/020_carry.md @@ -0,0 +1,3 @@ +# Mihomo IPv6 carry + +Detailed working plan: `.tmp/lane-b/020_carry.md` (gitignored security work space). Public source and contributor provenance are recorded in the roadmap. Only published outcomes will be added here. diff --git a/devlog/_plan/260907_sponsor_branches/000_plan.md b/devlog/_plan/260907_sponsor_branches/000_plan.md new file mode 100644 index 0000000000..cea5e924af --- /dev/null +++ b/devlog/_plan/260907_sponsor_branches/000_plan.md @@ -0,0 +1,41 @@ +# 260907 sponsor branches + +Goal: two sponsor branches from `origin/dev` (`8bc9e4ee2`, which carries SPONSORS.md and the README +Sponsors section), each ending in an open PR against `dev`. Neither merges here. + +## Shared mechanism (010, applied identically on both branches) + +- `ProviderRegistryEntry.sponsor?: { tier: "main" | "standard"; url: string }`. +- `DerivedProviderPreset.sponsor?: "main" | "standard"` via `entryToPreset`. +- `deriveProviderPresets()` keeps registry order; sorting is the picker's job. +- GUI catalog (`provider-presets.ts` + `ProviderCatalog.tsx`): sponsors first, alphabetical by label + among sponsors (Main before Standard), then the existing usage/label order. Sponsor rows get a + `Sponsor` chip (`badge-accent`) before the auth badge. i18n key `modal.badge.sponsor` in all + nine locales. +- CLI `ocx provider presets` prints `(sponsor)` after the label for sponsor rows. +- Tests: derive test for the field, catalog ordering test for pinning + chip. + +## OrcaRouter (020) + +- Registry: `sponsor: { tier: "standard", url: "https://www.orcarouter.ai/?utm_source=opencodex" }` + on the existing `orcarouter` entry. PKCE lands separately via #3908 (author akf66), untouched. +- README: first Standard row, uncomment the table. Logo `assets/sponsors/orcarouter.png` (from + `gui/public/provider-icons/orcarouter.svg` rendered to PNG), blurb from the sponsor if delivered, + else a neutral maintainer-written 60-word blurb marked for replacement. +- docs-site providers guide: OrcaRouter paragraph in section 3. +- Screenshots: dashboard Providers tab picker with OrcaRouter pinned, README section render. + +## PackyCode (030) + +- Registry: new `packycode` entry, `openai-chat`, baseUrl `https://cf.api.fan/v1` (from + docs.packyapi.com Codex/Kimi guides; `/v1/models` answers 401 without a key so the host is live), + `dashboardUrl https://www.packyapi.com/register?aff=k5KT`, sponsor standard. Model list from + the docs token groups: Codex group (gpt-5.5, gpt-5.1-codex), CC group (claude), seeded conservatively. +- Icon: `gui/public/provider-icons/packycode.svg` from packyapi.com favicon. +- README: Standard row with the sponsor's EN blurb and the ZH blurb beneath. +- docs-site providers guide paragraph; screenshots as above. + +## Order + +010 on `sponsors/orcarouter`, cherry-picked to `sponsors/packycode`, then 020 and 030 in +parallel. Each branch: privacy:scan, typecheck, focused tests, push `--no-verify`, PR with template. diff --git a/devlog/_plan/260907_sponsor_branches/010_phase1.md b/devlog/_plan/260907_sponsor_branches/010_phase1.md new file mode 100644 index 0000000000..d95bdaf334 --- /dev/null +++ b/devlog/_plan/260907_sponsor_branches/010_phase1.md @@ -0,0 +1,3 @@ +# 010 shared sponsor mechanism + +See 000_plan.md section Shared mechanism. Diff targets: src/types/provider.ts (registry entry type), src/providers/derive.ts, gui/src/components/provider-catalog/provider-presets.ts, ProviderCatalog.tsx, gui/src/i18n/*.ts, src/cli/provider-runtime.ts, tests. diff --git a/devlog/_plan/260907_sponsor_branches/020_phase2.md b/devlog/_plan/260907_sponsor_branches/020_phase2.md new file mode 100644 index 0000000000..5a718d7605 --- /dev/null +++ b/devlog/_plan/260907_sponsor_branches/020_phase2.md @@ -0,0 +1,3 @@ +# 020 OrcaRouter branch + +See 000_plan.md section OrcaRouter. diff --git a/devlog/_plan/260907_sponsor_branches/030_phase3.md b/devlog/_plan/260907_sponsor_branches/030_phase3.md new file mode 100644 index 0000000000..3f83c5eff2 --- /dev/null +++ b/devlog/_plan/260907_sponsor_branches/030_phase3.md @@ -0,0 +1,3 @@ +# 030 PackyCode branch + +See 000_plan.md section PackyCode. diff --git a/devlog/_plan/260907_track2_protocol/000_plan.md b/devlog/_plan/260907_track2_protocol/000_plan.md new file mode 100644 index 0000000000..42ded4b4ff --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/000_plan.md @@ -0,0 +1,28 @@ +# Track 2 protocol delivery + +- Archetype: satisfy-spec repair with an evidence-backed defer outcome. +- Trigger: maintainer assigns track 2 and authorizes ordinary PR chains, no-verify pushes, final remote CI first, and admin integration. +- Goal: preserve Chat JSON/SSE semantics (#3770/#3779), refusal (#3767), supported custom efforts (#3775), hosted-search execution (#3761), and opt-in Claude compatibility (#3730), or document a concrete unresolved blocker. +- Non-goals: native GitHub stacks, local tests/typechecks/builds/install, other tracks, service/config changes outside repository, releases/deployments. Do not weaken CI definitions or treat skipped tests as passing. +- Baseline: dev 7d8523eed75a67f7a4a15b533744fcd0e6059aa8, including #3771. +- Verifier: existing workflow_dispatch ci.yml lane=all at the final integration head; lower diagnostic CI only if final fails. Commands are NOT RUN locally by explicit instruction. Read workflow definitions to establish target coverage. Independent source review precedes remote execution. +- Stop: feasible reviewed changes land through dev PRs with verification evidence; other items receive explicit evidence-backed dispositions. No completion claims for deferred issues. +- Artifact: this numbered unit; private security analysis and raw tool results only in /tmp/cf54-*. +- Expected outcomes: landed, already implemented, deferred with concrete blocker, or blocked by external CI/service state. +- Escalation: parent reclaims failed worker slices; never widen auth/routing trust or retry provider work to make a test pass. No user budget was set. + +## Roadmap + +1. Docs-only roadmap and independent audit. +2. 010: JSON Responses to streaming Chat semantics; carry #3779 with attribution. +3. 020: refusal across live SSE, final snapshots, JSON, collection, and JSON-to-SSE. +4. 030: custom effort provenance/capability repair after independent Codex source check. +5. 040: hosted-search path feasibility, then scoped execution/continuation repair or defer. +6. 050: opt-in Claude compatibility gate after independent official-contract/security audit or defer. +7. 060: final source audit, remote CI, ordinary PR-chain integration and exact dev ancestry proof. + +The semantic stack is JSON fallback -> refusal. Catalog and Claude slices have disjoint implementation owners and join the integration tip. Source refs are ordinary branches, not registered native stacks. Do not run a separate lower-level CI before the final integration failure. + +## Process availability + +Installed cxc skills resolved to 0.2.20 because the named 0.2.19 directory is absent. No SessionStart binding was injected into this task; SESSION-IDENTITY-01 forbids borrowing a prior/transcript id. Therefore no FSM activation is claimed. Durable P/A/B/C/D artifacts and the native active goal still track authorized work; tests remain pending until remote evidence exists. diff --git a/devlog/_plan/260907_track2_protocol/010_chat_json_sse.md b/devlog/_plan/260907_track2_protocol/010_chat_json_sse.md new file mode 100644 index 0000000000..437133fce5 --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/010_chat_json_sse.md @@ -0,0 +1,22 @@ +# JSON Responses to streaming Chat + +Depends on roadmap only. Class C3; PR #3779 is the public implementation source. + +## File delta + +- MODIFY src/server/chat-completions.ts:455: keep responsesJsonToChatCompletion as semantic authority. Replace text-only extraction and constant stop with converted choice.message content/reasoning_content/refusal; project tool_calls with stable array-order indices; preserve converted finish_reason. One role event, at most one combined delta, one terminal and one DONE. No extra upstream inference. +- NEW tests/responses/chat-json-sse-fallback.test.ts from the source PR: actual loopback Responses upstream -> handleChatCompletions. Include one/two tools, reasoning+text, incomplete length, empty completion, cancellation and translator-budget release. +- MODIFY scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json: register new test under responses. +- MODIFY docs-site/src/content/docs/reference/proxy-formats.md and structure/04_transports-and-sidecars.md: buffered fallback delivery, semantic parity and no additional request. + +## Activation and oracle + +Streaming Chat request + JSON Responses upstream is the trigger. Native Chat and real SSE bypass this path. Hardcoded official Chat fixtures require indexed function calls, nullable finish for intermediate chunks and original terminal finish. First-choice scope follows the existing Responses single-result contract. Official openai-node ChatCompletionChunk/ChatCompletionMessage are the independent shape oracle; source PR tests are evidence candidates, not a passing result. + +## Check and delivery + +No local execution. Final remote CI must execute tests/responses/chat-json-sse-fallback.test.ts and existing chat-completions-endpoint coverage, typecheck and test-layout guards. Preserve upstream author credit in carried commit and final PR body. Lower refs are published with --no-verify; no native stack registration. + +## Build checkpoint + +Carried #3779 and applied independent-audit corrections: shared native serializer, indexed tools, typed unknown incompletes, correct length/content_filter precedence, and explicit converted/serialized byte ownership. New tests preserve the source PR cases and add official-contract boundary/accounting cases. Local suites/typecheck/build NOT RUN by instruction; git diff --check is a whitespace check only. Remote verification remains pending. diff --git a/devlog/_plan/260907_track2_protocol/011_chat_audit_amendment.md b/devlog/_plan/260907_track2_protocol/011_chat_audit_amendment.md new file mode 100644 index 0000000000..4634664830 --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/011_chat_audit_amendment.md @@ -0,0 +1,15 @@ +# Chat plan audit resolution + +Independent reviewer Fermat returned GO-WITH-FIXES, five blockers. No local command was executed. + +1. Accepted: source oracle differs from current converter. Incomplete max_output_tokens -> length and content_filter -> content_filter take precedence over tools. Incomplete missing/max_messages/steered/other reason becomes typed upstream truncation rather than fabricated token exhaustion, matching existing live-SSE handling of unknown incompletes. Both JSON and SSE public handlers map typed error, without success DONE. +2. Accepted with scope: use existing budget owner; add optional budget to pure converter so runtime caller charges retained copied content/reasoning/tools. Reuse existing native jsonCompletionSse owner for both JSON-to-SSE routes after it gains optional budget-aware serialization and proper final tool indexes. Charge serialized strings and output buffer while simultaneously live; release temporaries on transfer, retain response bytes until consumption/cancel finalization. No general translator-budget refactor. Positive charge and small configured-budget overflow tests required; no local runs. +3. Accepted: refusal parts indexed by original output_index/content_index; item.id/item_id are optional correlation constraints and a present mismatch fails. Preserve original array positions. Buffer refusal parts until terminal and emit in output/content order, avoiding interleaved-part reordering. Deltas append; equal snapshots deduplicate; extending snapshots fill suffix; shorter-prefix/empty snapshot preserves known data (sparse compatible provider); absent field is no new evidence; explicit non-string/contradictory non-prefix snapshot fails. Budget text plus per-entry metadata/key bytes, release on terminal/cancel/fail. Zero-length parts cannot bypass map accounting. Final JSON and collector use nullable refusal field. +4. Accepted: native JSON-to-SSE uses same shared helper and preserves refusal. Matrix covers native/translated upstream JSON/SSE with client JSON/SSE. Native streaming passthrough remains opaque. +5. Accepted: collectChatCompletion catch cancels reader before releasing lock, invoking upstream translator cancel; tests prove cancellation under processing overflow and no successful partial JSON. Existing outer budget finalizer remains final response owner. + +These replace conflicting portions of 010/020. Re-audit before source edits. Official SDK source field definitions retained in /tmp/cf54-openai-responses-types.ts and /tmp/cf54-openai-chat-types.ts. Aside page-open reached its host deadline without content; no browser-source proof claimed. + +## Re-audit resolution + +Fermat re-audit accepted the five resolutions and found one remaining terminal-order blocker. Accepted: stage all final role/tool/refusal/finish/DONE frames as one bounded terminal batch; serialize and reserve every frame before enqueueing any success frame. On reservation failure, release the staged reservations/refusal state and emit only the bounded typed overflow error (no success finish or DONE). Commit terminated success only after batch admission. Merely moving the terminated assignment is insufficient. Add a small-budget fixture that fails at final batch admission and asserts absence of success finish/DONE plus typed error and cancellation. diff --git a/devlog/_plan/260907_track2_protocol/020_refusal.md b/devlog/_plan/260907_track2_protocol/020_refusal.md new file mode 100644 index 0000000000..8bacb10a5d --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/020_refusal.md @@ -0,0 +1,17 @@ +# Preserve refusal across Chat projections + +Depends on 010 for JSON-to-SSE field delivery. Class C3 public wire contract. + +## File delta + +- MODIFY src/chat/outbound.ts: responsesJsonToChatCompletion accumulates content parts with type refusal and their refusal string into message.refusal, alongside existing content/reasoning/tool fields. +- MODIFY same file live translator: map response.refusal.delta to delta.refusal. Track each output/content part separately with the existing translator budget. Final response.refusal.done, content_part.done, output_item.done and completed/incomplete snapshots may add only an unseen matching suffix. Repeated final representations must not duplicate text. Conflicting snapshots cannot be represented as append-only deltas and must terminate as a typed translation failure, not false success. Map storage and release follow existing turn-budget lifecycle. +- MODIFY collectChatCompletion: collect delta.refusal with retained_collectors budget and serialize message.refusal. Never coerce refusal into ordinary assistant answer text. +- NEW tests/responses/chat-refusal.test.ts (register both test-layout inventories): cover direct JSON, split live deltas plus all snapshot representations, done-only, terminal-only, multiple parts, mixed text/refusal, stream collection, contradictory snapshot failure, cancellation/overflow, and JSON-to-SSE handler path inherited from 010. +- MODIFY proxy-formats.md and structure/04_transports-and-sidecars.md: document refusal field and parity across delivery shapes without claiming a new policy decision. + +## Independent oracle and acceptance + +OpenAI Responses docs define refusal.delta.delta and refusal.done.refusal, indexed by output_index/content_index; official Chat SDK defines delta.refusal and message.refusal. Local official Codex corpus is read only for consumer behavior; it is not automatically the sole Chat API schema authority. + +Trigger known refusal events/parts, expect exactly one concatenated refusal, unchanged normal content/tool semantics, one terminal+DONE on valid completion, typed failure without success DONE on invalid final snapshot or overflow. Tests use inert fixture messages rather than provoking live model refusals. No local suites run; remote final CI owns executable proof. diff --git a/devlog/_plan/260907_track2_protocol/030_custom_efforts.md b/devlog/_plan/260907_track2_protocol/030_custom_efforts.md new file mode 100644 index 0000000000..0a8856a0bb --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/030_custom_efforts.md @@ -0,0 +1,18 @@ +# Proven custom native capability projection + +Depends on roadmap; independent from Chat semantics. Class C3. Issue #3775 remains partial because an arbitrary gateway model name does not prove native capability, and current official source is not a binary proof for Desktop 0.153.4. + +## File delta + +- MODIFY src/codex/catalog/provider-fetch.ts: in current custom-row producer, retain existing canonical openai forward destination and capability-backed model ID proof. AFTER custom/inherited metadata merge, intersect explicit reasoningEfforts with nativeReasoningEfforts for that proved model. If explicit [] preserve [] and remove default; if nonempty declared list has no supported entry, use proved native default as singleton. Otherwise choose declared default only if present, then proved default if present, then first surviving effort. Do not clamp arbitrary provider/model names, destination overrides, or ordinary routed custom models. +- MODIFY src/codex/catalog/sync.ts: in retained sync merge, current invocation's live custom rows must not have max re-added. Use current config/producer provenance rather than a disk marker. Keep ordinary provider/combo/Reserve rules. +- MODIFY existing catalog-custom-models, sync-hardening, convergence and Claude model-discovery tests after reading actual filenames: canonical Astra with none/minimal + valid ladder; explicit []; all-invalid nonempty; valid default preserved; same-name noncanonical gateway unchanged; destination override unchanged; second sync no max resurrection; both gather entry points and /models client-version projection. +- MODIFY relevant English catalog/reasoning reference plus structure/03_catalog-and-subagents.md to state the narrow capability proof and explicit empty-list behavior. Translations must not claim broader gateway/client repair. + +## Official evidence and deferrals + +Local official source corpus 121_openai-codex: protocol/src/openai_models.rs allows nonempty custom effort strings; models-manager/src/manager.rs qualified lookup is consumer lookup, not gateway provenance; multi_agents_common.rs validates chosen-row membership. Codex source supports ultra and translates it on wire. API model docs lacking ultra do not justify deleting Codex ultra. + +Field chain is existing custom config -> fetch custom row -> deriveEntry/retained merge -> catalog file/direct /models consumers. No new schema field or request-time effort override. Existing threads and version-specific stale runtime state are outside this repair. + +Source/code inspection is completed; proposed regression commands are NOT RUN locally. Final remote CI must exercise modified tests and typecheck. #3775 must remain open for gateway destination and exact Desktop/version evidence; this fix addresses only proven canonical rows. diff --git a/devlog/_plan/260907_track2_protocol/040_hosted_search_disposition.md b/devlog/_plan/260907_track2_protocol/040_hosted_search_disposition.md new file mode 100644 index 0000000000..44e3c7714e --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/040_hosted_search_disposition.md @@ -0,0 +1,13 @@ +# Hosted search passthrough disposition + +Outcome: DEFER #3761; no production diff in this track. + +## Source findings + +src/web-search/loop.ts mutates normalized messages, while src/adapters/openai-responses.ts serializes passthrough _rawBody. Existing loop parsing is compaction-oriented and does not preserve the native tool/reasoning conversation needed for search-result continuation. src/server/sse-payload-rewrite.ts is synchronous rewriting rather than an asynchronous execution loop. Mixed ordinary tools/search and replay need a separate raw conversation contract; continuation storage alone does not supply it. + +Official Ollama local middleware supports hosted Responses search, including cloud model execution, but that does not establish the direct ollama.com/v1 endpoint contract. References opened during investigation: https://github.com/ollama/ollama/pull/17686 and https://docs.ollama.com/integrations/codex . Local official client source: corpus 121_openai-codex. No direct-cloud authenticated run was performed. + +## Resume criteria + +Define destination/backend execution policy; preserve raw Responses tools/reasoning when inserting search results; exercise mixed tools, cancellation, bounded iteration, SSE/JSON/WS, compact and replay remotely with a confirmed destination contract. A guard-only change is rejected because it cannot deliver these results. The issue remains open and will receive this disposition; no claimed fix, workflow or client change. diff --git a/devlog/_plan/260907_track2_protocol/050_claude_compatibility.md b/devlog/_plan/260907_track2_protocol/050_claude_compatibility.md new file mode 100644 index 0000000000..7cf73f0b0b --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/050_claude_compatibility.md @@ -0,0 +1,23 @@ +# Claude compatibility carry + +Depends on roadmap and an independent source/security audit. Class C3 with C4 review of admission and persisted diagnostics. Public source is PR #3730 at 18d64748ade8001e05327726b2ae4b22e8393418. + +## Published scope and file map + +- NEW src/claude/compatibility.ts: opt-in analysis for translated Messages; no Lab imports, source envelope, credential access or adapter execution. +- MODIFY src/server/claude-messages.ts: gate translated requests after real native passthrough returns and before inference. Preserve existing auth/origin and logging ownership. +- MODIFY src/types/config.ts: compatibility mode property. +- MODIFY src/server/request-log.ts and src/usage/log.ts: bounded optional metadata, persisted-row normalization and hydration. +- NEW tests/claude-integration/claude-compatibility.test.ts and MODIFY existing endpoint/usage tests; register new filename in both test-layout inventories. +- MODIFY server configuration reference and Claude guide only where current scope needs clarification. + +The source PR needs substantial classifier corrections before adoption. Detailed security-sensitive findings, official feature matrix, exact corrections and review evidence are held in task scratch space. The parent accepts a uniform conservative translated-path contract; no per-adapter exemption based on preliminary routing. Existing unset/native behavior remains unchanged. Shadow is observational and enforce is endpoint compatibility admission, not a global security boundary. + +## Field chain and acceptance + +Mode: typed config -> persisted JSON -> existing loader -> translated Messages gate. Present invalid mode must produce a fixed visible configuration failure rather than silently disable checking. No global config fallback change. +Evidence: gate -> request context -> ring -> usage row -> normalized disk row -> hydration. Only closed protocol codes may be stored; no body/header/credential/signature material. Old rows remain valid. + +Test unset/shadow/enforce/invalid modes, real native bypass versus translated Anthropic, zero-inference rejected requests, normal tools versus hosted feature declarations, nested supported content positions, large headers, persistence and reload. Exact behavior follows the private reviewed feature matrix. No local test/typecheck/build/install; final remote CI and explicit independent security review required. + +Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> diff --git a/devlog/_plan/260907_track2_protocol/060_remote_delivery.md b/devlog/_plan/260907_track2_protocol/060_remote_delivery.md new file mode 100644 index 0000000000..612f9342fd --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/060_remote_delivery.md @@ -0,0 +1,15 @@ +# Remote validation and delivery + +Depends on all accepted implementation slices. Class C3 integration; any admission changes require independent security review. + +## Delta + +- MODIFY only this unit's outcome/evidence record after source review. +- Publish ordinary branches using git push --no-verify. Native stack registration and repo-wide workflow edits are excluded. +- Final integration branch contains all accepted layers and current dev. Dispatch existing .github/workflows/ci.yml lane=all at its exact SHA; inspect every expected Linux/macOS/Windows shard and supporting gate. No local tests, typecheck, builds or install. +- If final CI fails, inspect log/artifact and then use lower-head or bounded remote cases to isolate it. Never claim skipped/cancelled checks passed or silently weaken assertions. Existing automatic PR jobs may run; no fabricated status. +- Open template-complete ordinary parent/child PRs; preserve contributor trailers. Record local NOT RUN, final integration proof, independently verified issue scope and explicit maintainer integration. +- Merge accepted work through dev PRs with admin authority. Refresh actual base/head and maintainer objections before each write, retain parent refs while children target them, and prove each final merge is an ancestor of fetched dev. If dev changes in another track, review integration delta and refresh final proof where needed. +- Close only fully resolved issues and superseded source PRs with attribution and replacement links; partial/deferred issues remain open with precise status. + +No source test is executed merely to verify that its command exists. CI workflow and package scripts are the source-inspection evidence of coverage. Report what each remote job actually did. diff --git a/devlog/_plan/260907_track2_protocol/070_windows_fixture_cache.md b/devlog/_plan/260907_track2_protocol/070_windows_fixture_cache.md new file mode 100644 index 0000000000..9006019834 --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/070_windows_fixture_cache.md @@ -0,0 +1,31 @@ +# Windows composed-fixture cache ownership + +## Trigger and plan amendment + +Final integrated run 34049728209 failed only Windows shard 2/6's composed-toggle B case with a request timeout. Product changes did not run in that management-only case. Three-head diagnostic run 34051777446 kept all deadlines/assertions, comparing base 24c761a, prior d60a0716 and current 84c94f3; all samples passed, but OFF consistently consumed 20-26 seconds of a 30-second request budget. + +## Controlled evidence + +Same-VM run https://github.com/lidge-jun/opencodex/actions/runs/34053472964 changed only fixture child PowerShell module-cache policy, on fixed source 84c94f3. The parent cache was read into a private job-owned regular file; no original cache path was passed to experiment subprocesses. All test assertions and deadlines remained unchanged. + +| Condition | Startup | OFF request | Result | +| --- | --- | --- | --- | +| Original A1 | 35.26 s | abort at 30.00 s | timeout; held-sync 45 s abort also recorded | +| Original A2 | 30.11 s | 25.50 s | pass | +| Owned empty, two samples | 26.71-27.41 s | 26.07-26.96 s | pass | +| Owned prepared copy, two samples | 6.12-6.29 s | 3.36-3.37 s | pass | +| Restored original | 27.50 s | 26.15 s | pass | + +The copied-cache whole composed file also passed (7 pass, 1 pre-existing skip, 0 fail). A1 has mixed abort observations, so the latency comparison uses the complete A2 and restored-control samples. A mere empty destination did not remove the delay. In copied samples the stale sync completed after the provider release; original controls exhausted discovery before release. No guard-ablation claim is made. + +## Scoped delta and acceptance + +MODIFY tests/codex-integration/codex-composed-acceptance.test.ts only: seed a Windows-only constructor-owned module cache using the existing Desktop fixture policy, outside the Codex manifest root, and pass only that owned destination to children. Preserve HOME/USERPROFILE variants, real SID/service evidence, coordinator paths, provider hold/release, cleanup, all assertions and all deadlines. No product, workflow, or identity-policy changes. + +The isolated diagnostic workflows are not delivery changes. Independent source/security review and a fresh full cross-platform run of the combined latest-dev integration head remain required. Local suites/typecheck/build are NOT RUN per instruction. This support layer is separate from the four product PRs. + +## Follow-up source review + +External review of #3808 identified a construction-failure cleanup gap: the fixture is registered only after its constructor returns. The cache-setup block now removes its own temporary root with bounded retries before rethrowing. Parent cache sources and shared coordinator paths remain outside that cleanup. Independent source review passed; final remote verification will include the delta. + +Run 34054412656 passed Windows composed shard 2, including B at 8.511 seconds. It failed a different competing-OFF fixture in shard 6 before the flip started; this does not negate the cache-control result and is tracked separately. diff --git a/devlog/_plan/260907_track2_protocol/080_windows_sync_preparation.md b/devlog/_plan/260907_track2_protocol/080_windows_sync_preparation.md new file mode 100644 index 0000000000..cd8c79bcc3 --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/080_windows_sync_preparation.md @@ -0,0 +1,18 @@ +# Windows competing-OFF preparation allocation + +## Failure and bounded plan + +Final run 34054412656 passed every individual job except Windows shard 6. The competing-OFF test refused to begin its real second-process flip: 32,262 ms remained of the 85,000 ms child budget after 52,738 ms of preparation. The unchanged flip plus reap requires 45,000 ms. This was a pre-flip budget rejection, not an outer timeout or a product assertion failure. Unlike the composed fixture, this test already inherits the ambient child environment; no cache-causation claim is made. + +MODIFY only the test's named preparation allocation: on Windows reserve two existing boot budgets for import, identity and admission preparation, then retain the original single flip and reap budgets. Windows CI child/test limits become 125/130 seconds; other platforms retain 85/90. No product deadline, assertion, coordinator, identity or service evidence changes. + +## Remote control + +Diagnostic run https://github.com/lidge-jun/opencodex/actions/runs/34056824267 checked out fixed source 0f8936b1f692d72ff1d2c1dd6218183dd0e9b882 and used a 52-second TOTAL preparation floor before the original remaining-budget guard. + +- Unchanged control passed and completed its real flip. +- Old allocation rejected the floor before flip, without timeout. +- New allocation completed the real flip and passed the original skip and unchanged-config assertions. +- With the new allocation, a one-site diagnostic mutation stored ON for the OFF request. The real flip process completed successfully, but the original result assertion failed: applied instead of skipped/desired_disabled. No timeout or budget refusal contaminated that failure. + +The isolated job restored both files byte-for-byte. Its synthetic floor, traces, workflow and production mutation are excluded from delivery. This proves the preparation allocation and test sensitivity to broken OFF persistence, not every downstream guard. Independent diagnostic security review passed. Full integration CI remains the delivery gate; no local suites, typecheck, install or build were run. diff --git a/devlog/_plan/260907_track2_protocol/090_windows_shim_budget.md b/devlog/_plan/260907_track2_protocol/090_windows_shim_budget.md new file mode 100644 index 0000000000..7d85053bbd --- /dev/null +++ b/devlog/_plan/260907_track2_protocol/090_windows_shim_budget.md @@ -0,0 +1,14 @@ +# Windows unreadable-config shim fixture deadline + +Final run 34057173038 passed 23 individual jobs and failed only Windows shard 2 (plus aggregate). The original shim unreadable-config test had a 10-second outer timeout and no owned child deadline. Bun killed the dangling child at 10.24 seconds, yielding status null. Fixture and inspected runtime source were unchanged since 8615f1a. The specific slow runtime stage is unproven; this is not an environmental or cache-causation claim. + +MODIFY only that subprocess fixture: use the existing 45-second spawn budget on Windows and a named 5-second outer cleanup allowance. Preserve POSIX's 10-second limit, all temporary paths, fake launcher, real install/diagnosis/advisory path and original exit/output assertions. Error/signal diagnostics are fixed and omit captured output. Product behavior is unchanged. + +Remote diagnostic https://github.com/lidge-jun/opencodex/actions/runs/34058337624 pinned c7a96b14f and demonstrated: + +- Unchanged control: real CLI exit 0 and original assertions pass. +- With a 12-second preload delay, old limit: child is signalled and test times out. +- Same delayed CLI with owned Windows deadline: exit 0 and original assertions pass. +- Same new limit with the readiness collector's advisory catch changed to rethrow: real CLI exits 1 without timeout/signal; the original exit-0 assertion fails. + +The diagnostic restored source bytes and is excluded from delivery. Its initial run 34058187661 failed before tests because Python selected a Windows legacy codec; explicit UTF-8 corrected that diagnostic-only error. No passing rerun was used to erase a product failure. Source/security review of the diagnostic and independent source review of the candidate passed. No local suites, installs, typecheck or builds were run. Final cross-platform CI remains required for the published combined head. diff --git a/docs-site/public/pr-screenshots/raycast-integration.png b/docs-site/public/pr-screenshots/raycast-integration.png new file mode 100644 index 0000000000..e17261c158 Binary files /dev/null and b/docs-site/public/pr-screenshots/raycast-integration.png differ diff --git a/docs-site/public/pr-screenshots/subagent-fallback-settings.png b/docs-site/public/pr-screenshots/subagent-fallback-settings.png new file mode 100644 index 0000000000..04f72f140c Binary files /dev/null and b/docs-site/public/pr-screenshots/subagent-fallback-settings.png differ diff --git a/docs-site/public/pr-screenshots/t4-credit-bars-81a8.png b/docs-site/public/pr-screenshots/t4-credit-bars-81a8.png new file mode 100644 index 0000000000..3d63f634b7 Binary files /dev/null and b/docs-site/public/pr-screenshots/t4-credit-bars-81a8.png differ diff --git a/docs-site/public/pr-screenshots/t4-log-polling-81a8.png b/docs-site/public/pr-screenshots/t4-log-polling-81a8.png new file mode 100644 index 0000000000..4dd53042e9 Binary files /dev/null and b/docs-site/public/pr-screenshots/t4-log-polling-81a8.png differ diff --git a/docs-site/public/pr-screenshots/t4-model-picker-81a8.png b/docs-site/public/pr-screenshots/t4-model-picker-81a8.png new file mode 100644 index 0000000000..0754aefc3c Binary files /dev/null and b/docs-site/public/pr-screenshots/t4-model-picker-81a8.png differ 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 08e77f2992..b1e5b6e51b 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -290,8 +290,16 @@ anciens alias hachés et les identifiants `claude-ocx---` des c toujours résolus. Si le sélecteur situé au bas de Claude Desktop ne modifie pas le modèle d'une conversation 3P déjà en cours, -utilisez `/model ` dans cette conversation. OpenCodex ne peut pas observer l'état du sélecteur ; il -achemine l’identifiant du modèle porté par chaque requête. Confirmez le résultat sous **Journaux → requestModel**. +vous pouvez essayer `/model `, mais ce contournement peut également échouer sur les versions de Desktop +concernées. Le [ticket #3782](https://github.com/lidge-jun/opencodex/issues/3782) rapporte que sous Windows, +avec Claude Desktop 1.46388.4, la conversation continue d'utiliser son modèle initial après des changements +via le sélecteur du bas comme via `/model`. Ce signalement ne permet pas d'établir quel composant du client +ou du routage est à l'origine de ce comportement. + +Vous pouvez aussi essayer de sélectionner le modèle par défaut souhaité dans le profil Claude Desktop +d'OpenCodex, de réappliquer ce profil et de démarrer une nouvelle conversation. Il s'agit d'une étape de +dépannage, sans garantie de résolution. OpenCodex ne peut pas observer l'état du sélecteur ; il achemine +l'identifiant du modèle porté par chaque requête. Vérifiez ce que le client envoie sous **Logs → requestedModel**. Les modèles dont la fenêtre de contexte de référence atteint 1M obtiennent une ligne supplémentaire `…[1m]` dans le sélecteur. Sa sélection indique à Claude Code la fenêtre complète de 1M pour ce modèle, tout en maintenant le compactage automatique ; le proxy retire @@ -526,12 +534,14 @@ Le proxy traduit chaque requête Anthropic Messages API au format Codex Response | Texte assistant | `output_text` | | Assistant `tool_use` | `function_call` (`input` → JSON-stringifié `arguments`) | | Utilisateur `tool_result` | `function_call_output` (`is_error` → préfixe `[tool error]`) | -| Relecture de `thinking` / `redacted_thinking` | Ignorée | +| Relecture de `thinking` / `redacted_thinking` | Éléments `reasoning` avec enveloppes `ocxr1` bornées pour les signatures et les contenus masqués | | Outils fonctionnels | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, fonction nommée→`{type:"function",name}`, hébergée WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +Sur l’adaptateur Anthropic prévu, les blocs signés non masqués (y compris thinking vide) et les blocs redacted opaques sont préservés. `hideThinkingSummary` reste inchangé : le texte signé masqué localement n’est pas exposé aux clients Claude ; sa relecture sans perte via cette frontière reste non établie. Les anciennes enveloppes combinées ne permettent pas de rétablir l’ordre après émission du texte en streaming. `claudeCode.compatibility: "enforce"` refuse toujours la relecture thinking. Cela ne prouve ni l’acceptation réelle par Anthropic ni une amélioration du cache ; [#3719](https://github.com/lidge-jun/opencodex/issues/3719) reste ouvert. + **Cas d'erreur (400) :** JSON mal formé ; `model` absent ou vide ; `messages` absent ou vide ; rôle non pris en charge ; `tool_result` sans `tool_use_id` ; `tool_use` sans identifiant ni nom ; `tool_choice` nommé sans nom. @@ -542,7 +552,8 @@ Le proxy traduit chaque requête Anthropic Messages API au format Codex Response | `response.created` | `message_start` + `ping` | | Battement de coeur | `ping` | | Deltas de texte | `content_block_start` → `content_block_delta` (texte) → `content_block_stop` | -| Résumé ou texte de raisonnement | Bloc `thinking` avec signature synthétique | +| Résumé ou texte de raisonnement | Bloc `thinking` avec la signature relue, ou une enveloppe de secours `ocxr1` bornée | +| Raisonnement expurgé | Blocs `redacted_thinking` relus depuis l'enveloppe de raisonnement | | Trames d'appel de fonction | Bloc `tool_use` avec `input_json_delta` | | Événement terminal | `message_delta` → `message_stop` | | EOF avant la borne | style 502 `api_error` | diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 02ffa8a211..a351adeae5 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -233,6 +233,14 @@ opencodex encode cette déclaration et son historique sous forme d'outil de fonc cycle de vie diffusé de l'appel de fonction en `custom_tool_call` avant que Codex ne le reçoive. Le routage natif par transfert OpenAI et l'outil personnalisé `apply_patch`, qui est pris en charge, restent inchangés. +Avant le premier appel, les tours routés en mode code reçoivent aussi les règles de l'hôte pour les +outils auxiliaires imbriqués : `tools.apply_patch` prend une seule chaîne qui commence et se termine +par les lignes de marqueur de patch seules, sans habillage ; l'isolate ne dispose pas de `import`, +et les commandes longues sont interrogées via `write_stdin`. Lorsqu'un résultat exec en mode code +sur le chemin natif Responses routé, Kiro ou Cursor contient encore l'un des messages d'échec de +l'hôte, opencodex ajoute une indication d'une ligne qui nomme la règle. Cette modification ne +réécrit ni le code du modèle ni le texte de son patch. + Le fournisseur sélectionné doit prendre en charge les appels de fonctions ou d'outils. Un fournisseur purement textuel dépourvu de cette prise en charge ne peut pas utiliser `exec`, Browser ni Computer Use. Les lignes OpenAI natives conservent leur mode d'outil en amont. diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index c65531a4d3..97babedd18 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, Gajae Code, DeepSeek Harness et MiniMax Code 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, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside et Raycast depuis le tableau de bord — un commutateur par client, avec une sauvegarde avant chaque écriture. --- L'onglet **Intégrations** écrit le bloc fournisseur d'opencodex dans le fichier de configuration du client, -puis peut le retirer. Neuf clients fonctionnent ainsi, chacun avec son propre commutateur : +puis peut le retirer. Treize clients fonctionnent ainsi, chacun avec son propre commutateur : | Client | Fichier de configuration | Format | Prise d'effet de la modification | Identifiant | |---|---|---|---|---| @@ -17,6 +17,10 @@ puis peut le retirer. Neuf clients fonctionnent ainsi, chacun avec son propre co | Gajae Code | `~/.gjc/agent/models.yml` | YAML | dans les nouvelles sessions ou à l'ouverture de `/model` |`OPENCODEX_GAJAE_API_KEY` | | DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml` (`~/.dsh/settings.yaml` par défaut) | YAML | rechargement à chaud | jeton porteur fictif et non secret pour le bouclage | | MiniMax Code | `~/.minimax/config.yaml` | YAML | dans les nouvelles sessions ou après l’ouverture du sélecteur de modèles | valeur fictive de bouclage | +| Prime Agent | `~/.prime/agent/models.json` | JSON | dans les nouvelles sessions | valeur fictive de bouclage | +| ZCode | `~/.zcode/v2/config.json` | JSON | au redémarrage | valeur fictive de bouclage | +| Aside | `~/.aside/u//models.json` | JSON | après avoir quitté complètement puis rouvert Aside | valeur fictive de bouclage | +| Raycast | `~/.config/raycast/ai/providers.yaml` | YAML | immédiatement à l'enregistrement — Raycast surveille le fichier | aucun — bouclage uniquement | La prise en charge gérée de DSH exige au minimum **DSH 0.1.0-rc.6**. OpenCodex ne possède que le fragment `llm-pi-ai.providers.opencodex` : **Appliquer** et **Actualiser** remplacent ce fragment, **Désactiver** ne @@ -33,6 +37,37 @@ L’actualisation de l’intégration met également à jour les fenêtres de co d’effort de raisonnement faisant autorité ; les capacités inconnues sont omises et l’effort courant, qui appartient à la session MCode, est préservé. +Raycast a deux prérequis. Les fournisseurs personnalisés (Custom Providers) sont une fonctionnalité +**Raycast Pro** : avec un forfait gratuit, le fichier est tout de même écrit, mais +`ocx integration client status --client raycast` et la page Intégrations signalent un avertissement, +car Raycast ne le lira pas. Et Raycast ne crée son dossier `ai` que lorsque vous ouvrez une fois +Raycast → Settings → AI → **Reveal Providers Config** ; opencodex utilise ce dossier comme signal +d'installation et indique que le client n'est pas installé tant qu'il n'existe pas. Raycast lit +`~/.config/raycast/ai/providers.yaml` aussi bien sur macOS que sur Windows et n'honore pas +`XDG_CONFIG_HOME` ; ce chemin ne peut donc pas être déplacé. + +Le bloc géré est un seul élément, `id: opencodex`, dans la séquence `providers` du fichier : +`name: OpenCodex`, `base_url: http://:/v1`, et chaque modèle routé avec ses `abilities` — +`tools` et `system_message` sont définis à `true` par convention d’export, `vision` suit les modalités d'entrée du +catalogue, `reasoning_effort` est défini lorsque le modèle dispose d'une échelle d'effort, et +`temperature` est désactivé pour les modèles de raisonnement. Les autres fournisseurs du fichier sont +préservés, et la désactivation ne retire que l'élément OpenCodex. Raycast prend en compte la +modification dès l'enregistrement du fichier, sans redémarrage ; les modèles apparaissent dans le +sélecteur de modèles de Raycast regroupés sous **OpenCodex**. Raycast accepte le champ facultatif +`api_keys`, mais OpenCodex l’omet volontairement et refuse les cibles hors bouclage ou exigeant +authentification : cette intégration ne fournit pas l’en-tête d’admission requis par OpenCodex. +Le signal Pro issu d’une préférence privée macOS est indicatif ; Windows ne la lit jamais et +renvoie un état inconnu. Il ne bloque pas l’écriture. Les métadonnées exportées ne prouvent pas +la prise en charge des outils pour chaque modèle. Les valeurs des autres fournisseurs sont +préservées, sans garantie pour les commentaires ou la mise en forme YAML. Le format est documenté sur +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers). + +Les exports Raycast en CLI et les téléchargements utilisent la destination et la politique +d’admission du serveur actif, y compris son listener de bouclage sans authentification. +`ocx ensure` ne réactualise pas Raycast depuis sa copie de configuration enregistrée, qui peut +différer du serveur actif. Le démarrage du serveur et la synchronisation explicite restent disponibles. + + Les chemins respectent les variables de remplacement propres à chaque client, lorsqu'elles existent. Pour OMP, la présence de `OMP_PROFILE` l'emporte sur `PI_PROFILE`, même si sa valeur est explicitement vide. Un profil nommé emploie `PI_CONFIG_DIR` comme nom de répertoire relatif au dossier personnel de l'utilisateur @@ -93,7 +128,7 @@ niveaux. Dans ces cas, le commutateur est verrouillé afin que rien ne soit modi **OMP** n'est pas affecté non plus par les modifications voisines, mais pour une autre raison : son outil d'écriture ne modifie, octet par octet, que sa propre plage `providers.opencodex` ; le reste du fichier n'est jamais réécrit. Pour les autres formats susceptibles de contenir des commentaires (Hermes, OpenClaw, -Kimi Code, Gajae Code et MiniMax Code — documents YAML, JSON5 et TOML réécrits en entier), ou lorsque les propres entrées +Kimi Code, Gajae Code, MiniMax Code et Raycast — documents YAML, JSON5 et TOML réécrits en entier), ou lorsque les propres entrées d'opencodex ont été modifiées, le commutateur se verrouille et la désactivation est refusée plutôt que de deviner quelles modifications vous appartiennent. @@ -169,9 +204,11 @@ ocx integration client enable --client mcode ocx mcode ``` -Une fois l’intégration connectée, `ocx sync` actualise également le bloc MCode géré avec les fenêtres de -contexte et les niveaux d’effort de raisonnement actuels. Les blocs absents, modifiés par un tiers, non sûrs -ou jamais gérés restent intacts ; réactivez explicitement l’intégration lorsque vous souhaitez la reconnecter. +Une fois l’intégration connectée, `ocx sync` et `POST /api/sync` actualisent les catalogues MCode, +Pi, Aside et Raycast gérés. Le démarrage du proxy actualise aussi le catalogue Raycast géré. +Les changements de visibilité, de fournisseur ou de préréglage actualisent Pi, Aside et Raycast. +Les blocs absents, modifiés par un tiers, non sûrs ou supprimés manuellement restent intacts ; +réactivez explicitement l’intégration lorsque vous souhaitez la reconnecter. Le CLI distinct de la plateforme MiniMax (`mmx`) n’est pas une intégration à commutateur de fichier. Ses commandes textuelles utilisent le point de terminaison compatible avec Anthropic de MiniMax ; OpenCodex diff --git a/docs-site/src/content/docs/fr/guides/model-ordering.md b/docs-site/src/content/docs/fr/guides/model-ordering.md index ada196c8fc..b22bf9b827 100644 --- a/docs-site/src/content/docs/fr/guides/model-ordering.md +++ b/docs-site/src/content/docs/fr/guides/model-ordering.md @@ -113,7 +113,7 @@ Utilisez `subagentModels` pour choisir et ordonner les premiers modèles que Cod `spawn_agent`. La page **Sous-agents** du tableau de bord peut réorganiser les identifiants natifs non qualifiés et les identifiants routés. Utilisez `ocx agent subagents set` ou modifiez la configuration OpenCodex pour définir des choix exacts de la forme `/` ; le tableau de bord -ne les répertorie pas et les omet s’il enregistre la liste. Configurez au maximum cinq identifiants. Avec +conserve ces identifiants déjà enregistrés, même indisponibles. Configurez au maximum cinq identifiants. Avec des sélecteurs de compte, un choix natif non qualifié peut se décliner en plusieurs lignes de catalogue qualifiées par sélecteur ; les choix configurés et les lignes annoncées ne correspondent donc pas nécessairement un à un. @@ -178,3 +178,11 @@ l’outil natif. `disabledModels` et `selectedModels` de chaque fournisseur restent des champs de visibilité, pas des contrôles d’ordre. Il n’existe aucun paramètre distinct `modelOrder`, `providerOrder` ou de carte de priorité. + +## Préréglages du sélecteur + +Dans **Models**, choisissez Par défaut, A–Z par modèle, Par fournisseur ou Instantané des usages, puis appliquez l’ordre. Les identifiants routés actuellement disponibles et `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`) sont enregistrés. Les usages conservés sont lus une seule fois lors de l’application ; un rechargement ou un changement de modèles ne recalcule rien. Un ordre personnalisé ou natif complet reste intact jusqu’à une application explicite. Par défaut efface les deux champs même sans modèle disponible. + +`GET/PUT /api/subagent-models` conserve les choix enregistrés désactivés ou absents dans `chosen` et `available` ; `pickerAvailable` contient les identifiants routés admissibles. Models envoie `pickerOrder` et `pickerOrderMode`, jamais `models`. Une sauvegarde du roster seul conserve l’ordre. Entrée invalide ou échec de sauvegarde préserve l’état précédent. + +Les plages prioritaires et natives restent en place. Les préréglages affectent le catalogue Codex et les groupes routés de la découverte Claude, sans modifier le préfixe natif Claude ni les profils Desktop explicites ou la propriété des alias. Les rangs de guidage OpenCodex et les réglages de repli sont conservés ; les cinq choix annoncés nativement par Codex et le défaut recommandé peuvent changer. Aucun client n’est redémarré ; une actualisation peut rester en attente et nécessiter de rouvrir le client. diff --git a/docs-site/src/content/docs/fr/guides/pi.md b/docs-site/src/content/docs/fr/guides/pi.md index eab8960063..030d91679c 100644 --- a/docs-site/src/content/docs/fr/guides/pi.md +++ b/docs-site/src/content/docs/fr/guides/pi.md @@ -27,6 +27,9 @@ d’exportation de la variable d’environnement et le nombre de modèles dotés "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -41,6 +44,8 @@ d’exportation de la variable d’environnement et le nombre de modèles dotés } ``` +Les fournisseurs Pi générés activent `compat.sendSessionAffinityHeaders`. Conservez ce réglage lors de la fusion ou de la modification manuelle du fournisseur : Pi transmet un identifiant de session stable, dont OpenCodex dérive l’affinité pour la destination canonique OpenCode Go. Pi peut omettre cet identifiant lorsque `cacheRetention` vaut `none`. + Les identifiants de modèle sont les sélecteurs canoniques du proxy : les modèles routés apparaissent donc sous la forme `provider/model` (`anthropic/claude-opus-5`) et les slugs natifs OpenAI restent sans préfixe (`gpt-5.6-sol`). Le `name` suffixe — `(anthropic)`, `(native)`, `(routed)` — permet de distinguer, dans le sélecteur de Pi, deux modèles de même nom diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 92aa565e52..cc543feb40 100644 --- a/docs-site/src/content/docs/fr/guides/providers.md +++ b/docs-site/src/content/docs/fr/guides/providers.md @@ -124,6 +124,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | Connexion PKCE expérimentale, transport HTTP/2 en direct et découverte de modèles filtrés par compte. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Expérimental. Flux d'appareil GitHub et échange `copilot_internal` (client OAuth de VS Code). Nécessite un abonnement Copilot actif ; il ne s'agit pas d'une API tierce officielle. | +Les vérifications de quota Google Antigravity utilisent des points de terminaison Google fixes, y compris le repli vers la liste des modèles. Elles prennent en charge le DNS Fake-IP transparent pour ces destinations en conservant la vérification TLS, le refus des redirections et les contrôles des adresses privées. Une URL de base personnalisée ne modifie que les requêtes de modèles ; `NO_PROXY` conserve la politique de connexion directe. + + Après un échec définitif d'actualisation de Nous, exécutez `ocx login nous` pour vous réauthentifier. Pour les préréglages canoniques du forfait Kimi Coding (`kimi` pour la connexion au compte et `kimi-code` @@ -309,6 +312,7 @@ promotionnels de Cline ne sont accessibles que dans l'IDE ou la CLI Cline, pas p | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (liste statique)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Forfait à jetons (par défaut) : `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Facturation à l'usage : `https://dashscope.aliyuncs.com/compatible-mode/v1` · ou personnalisé | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -500,7 +504,7 @@ une barre trompeuse. > programmation interactifs. L'automatisation générale par API, les services applicatifs personnalisés et les > traitements par lots non interactifs sont interdits et peuvent entraîner la suspension de la clé du forfait. -> **Deux routes GLM :** `zai` correspond à l'abonnement international Z.AI Coding Plan ; `zhipu-bigmodel` +> **Facturation GLM :** `zai` correspond à l'abonnement international Z.AI Coding Plan ; `zhipu-bigmodel` > correspond au point de terminaison national BigModel de Zhipu, facturé à l'usage. Les hôtes, les clés et la > facturation diffèrent : une clé émise pour l'un ne permet pas de s'authentifier auprès de l'autre. @@ -547,8 +551,8 @@ flux d'appareil contre un jeton d'API Copilot de courte durée, et non contre un reste une passerelle à clé ou jeton d'abonnement sur son point de terminaison compatible OpenAI. **Cloudflare AI Gateway** exige que les identifiants de votre compte et de votre passerelle figurent dans l'URL. -Copilot présente un catalogue qui utilise plusieurs protocoles : sa famille GPT-5 (`gpt-5.3-codex`, `gpt-5.4`, -`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) rejette +Copilot présente un catalogue qui utilise plusieurs protocoles : ces modèles (`gpt-5.3-codex`, `gpt-5.4`, +`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) rejettent `/chat/completions` pour le trafic d'agent. opencodex route donc ces modèles sur l'API Responses par défaut, tandis que tous les autres modèles Copilot restent sur Chat Completions. L'ordre de priorité est le suivant : verrouillage explicite du protocole → entrée [`modelAdapters`](/fr/reference/configuration/providers/) définie diff --git a/docs-site/src/content/docs/fr/guides/remote-hub.md b/docs-site/src/content/docs/fr/guides/remote-hub.md index d02269815b..15d5392c18 100644 --- a/docs-site/src/content/docs/fr/guides/remote-hub.md +++ b/docs-site/src/content/docs/fr/guides/remote-hub.md @@ -60,16 +60,35 @@ La rotation garde les deux clés valides sous le même `apiKeyId` pendant dix mi ## Docker, retour arrière et dépannage -Il n’existe pas d’image Docker officielle, mais le dépôt fournit un `Dockerfile` et un `compose.yaml` maintenus pour construire localement une image Bun épinglée par digest. Au premier démarrage normal, le conteneur crée un certificat TLS auto-signé dans `/home/bun/.opencodex/container-tls/cert.pem` et sa clé privée dans `/home/bun/.opencodex/container-tls/key.pem`. La clé reste accessible au seul propriétaire dans le volume `ocx-state`, et le point de terminaison de données utilise HTTPS dès ce démarrage. +Lors d'un retour arrière, conservez les deux volumes et leurs points de montage. Les droits des volumes existants ne sont pas corrigés automatiquement. Consultez le [guide canonique](/guides/remote-hub/#docker-compose) pour les montages nommés hors Compose et les chemins d'état personnalisés. -Avant ce premier démarrage, initialisez une seule fois le jeton de données via stdin. L’outil d’amorçage accepte au plus une ligne de 512 octets, ne l’affiche jamais, refuse de remplacer un jeton existant et l’enregistre dans le fichier privé canonique `service-api-token`. +Deux volumes distincts conservent l'état : `ocx-state` pour +`OPENCODEX_HOME=/home/bun/.opencodex` et `codex-state` pour +`CODEX_HOME=/home/bun/.codex`. Leurs fichiers `auth.json` ont des formats incompatibles : +ne fusionnez pas ces répertoires. Ils restent accessibles en écriture malgré la racine en lecture seule. -Installez Git et Bun sur l’hôte. Avant chaque construction, générez le manifeste canonique depuis les sources et fichiers de définition du conteneur suivis par Git, sans les modifier entre la génération et la construction. Le JSON généré reste non suivi ; `.git` est exclu du contexte Docker. Le port hôte est lié à `127.0.0.1` par défaut. Pour un accès distant, utilisez explicitement `OPENCODEX_BIND_ADDRESS= docker compose up -d` ; `0.0.0.0` expose toutes les interfaces. Protégez cet accès par un pare-feu et un frontal TLS/tailnet authentifié. +Le catalogue n'est pas généré automatiquement. Avant de tester `/v1/catalog` avec authentification, +créez ou importez un fichier valide dans `/home/bun/.codex/opencodex-catalog.json`. +Un répertoire vide renvoie normalement 404 `catalog_not_found`. Une mise à jour conserve +`ocx-state` et ajoute `codex-state`, sans déplacer les fichiers. Sauvegardez tout catalogue +précédemment placé dans `.opencodex`, puis transférez seulement ce catalogue avec des permissions +réservées au propriétaire ; ne remplacez pas un `auth.json` par celui de l'autre produit. +Si vous redéfinissez `CODEX_HOME`, montez ce répertoire exact en écriture et placez le catalogue +par défaut dans `${CODEX_HOME}/opencodex-catalog.json`. Si `model_catalog_json` désigne un autre +fichier, son chemin résolu doit aussi être persistant. Conservez les variables et montages +personnalisés jusqu'à la fin d'une migration explicite. +`docker compose down` conserve les deux volumes ; `docker compose down --volumes` supprime +`ocx-state` et `codex-state`, avec les identifiants, l'historique d'utilisation, la clé de données, +l'état et le catalogue Codex. Ce n'est pas une commande de mise à jour ou de redémarrage. -La construction authentifie par manifeste `Dockerfile`, `compose.yaml`, `.dockerignore`, chaque fichier suivi faisant autorité sous `docker/`, les sources sous `src/` et les fichiers de paquet obligatoires, dont `package.json`, `bun.lock` et `scripts/model-metadata.source.json`. Elle compare chaque SHA-256 au contexte puis à l’image ; les fichiers manquants ou divergents, tout fichier source ou fichier Docker faisant autorité supplémentaire et les liens symboliques sont refusés. +Il n’existe pas d’image Docker officielle, mais le dépôt fournit un `Dockerfile` et un `compose.yaml` maintenus pour construire localement une image Bun épinglée par digest. Initialisez une seule fois la clé de données via stdin ; elle est enregistrée avec des permissions réservées au propriétaire dans le volume `ocx-state` et n’est jamais affichée. + +Installez Git et Bun sur l’hôte. Avant chaque construction, générez le manifeste canonique depuis les sources suivies par Git, sans modifier les sources entre la génération et la construction. Le JSON généré reste non suivi ; `.git` est exclu du contexte Docker. Le port hôte est lié à `127.0.0.1` par défaut. Pour un accès distant, utilisez explicitement `OPENCODEX_BIND_ADDRESS= docker compose up -d` ; `0.0.0.0` expose toutes les interfaces. Protégez cet accès par un pare-feu et un frontal TLS/tailnet authentifié. + +La construction rejette les manifestes périmés en comparant chaque SHA-256 aux fichiers du contexte puis de l’image. Les fichiers manquants ou divergents, les sources supplémentaires et les liens symboliques sont refusés. `package.json`, `bun.lock` et le seul fichier autorisé de `scripts/`, `scripts/model-metadata.source.json`, sont obligatoires. ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -77,40 +96,6 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -Copiez uniquement le certificat public pour vérifier le point de terminaison HTTPS local : - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -`OPENCODEX_PORT` règle à la fois le port publié sur l’hôte et le `tls.publicOrigin` géré par Compose ; le listener interne reste sur `10100` : - -```bash -OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz -``` - -Un volume conservé datant d’avant TLS est migré automatiquement au démarrage : l’identité par volume est créée et l’origine HTTPS reprend le port publié. Les chemins de certificat gérés par l’opérateur sont préservés. Le certificat généré ne couvre que `localhost` et `127.0.0.1`. Pour publier directement sous un nom distant, installez un certificat et une clé pour ce nom, puis fournissez son origine HTTPS exacte avec `OPENCODEX_PUBLIC_ORIGIN` et l’adresse de publication : - -```bash -OPENCODEX_PUBLIC_ORIGIN=https://hub-name.tailnet-name.ts.net \ -OPENCODEX_BIND_ADDRESS=100.64.0.10 \ -docker compose up -d -``` - -Les sondes internes de santé et de disponibilité peuvent désactiver la vérification du certificat uniquement vers l’adresse fixe de loopback du conteneur, `https://127.0.0.1:10100`. Cette exception ne vaut jamais comme validation externe : l’acceptation du déploiement doit vérifier le nom d’hôte exact avec le certificat public copié ou la chaîne de confiance du système. - -Conservez ces variables lors des invocations Compose suivantes. Pour revenir à une ancienne image HTTP, retirez le réglage TLS pendant que l’image actuelle est encore disponible, puis démarrez l’ancienne image. Les fichiers d’identité peuvent rester dans le volume : - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# sélectionner ou construire l’ancienne image, puis recréer le hub -docker compose up -d -``` - Le conteneur s’exécute avec l’utilisateur non-root `bun`, un système de fichiers racine en lecture seule et uniquement le port `10100` publié. Ne publiez jamais `10101` et ne placez aucun secret dans `ARG`, `ENV`, `COPY`, Compose, l’historique d’image ou argv. Après le healthcheck, vérifiez séparément `/readyz`, le catalogue authentifié et une réponse réelle. `docker compose down` conserve le volume ; `docker compose down --volumes` supprime aussi la configuration, les identifiants et la clé. - Hub indisponible : `ocx disconnect` restaure localement, mais la révocation reste à faire. 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 8fe6e049e3..f329d9a63d 100644 --- a/docs-site/src/content/docs/fr/reference/cli/agents.md +++ b/docs-site/src/content/docs/fr/reference/cli/agents.md @@ -164,7 +164,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 @@ -175,7 +175,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. | @@ -205,6 +205,17 @@ propres valeurs par défaut à ces lignes. | `mcode` | `~/.minimax/config.yaml` (`MINIMAX_DATA_DIR`, puis l'ancien `MAVIS_DATA_DIR`, l'emportent une fois définis ; une valeur relative est refusée) | `mcode-config.yaml` | aucun — espace réservé de bouclage | | `zcode` | `~/.zcode/v2/config.json` (`ZCODE_DATA_DIR` l'emporte une fois défini ; une valeur relative est refusée) | `config.json` | aucun — espace réservé de bouclage | | `prime` | `~/.prime/agent/models.json` (`PRIME_AGENT_CODING_AGENT_DIR` l'emporte une fois défini ; une valeur relative est refusée) | `prime-models.json` | aucun — espace réservé de bouclage | +| `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 | + +L'exportation Raycast est un document `providers.yaml` autonome contenant un seul élément `id: opencodex` +dans la séquence `providers` : `name: OpenCodex`, l'URL de base `/v1` du proxy et chaque modèle routé avec +ses `abilities` (`tools` et `system_message` toujours pris en charge, `vision` d'après les modalités d'entrée +du catalogue, `reasoning_effort` lorsque le modèle dispose d'une échelle d'effort, `temperature` désactivé +pour les modèles de raisonnement). Les fournisseurs personnalisés sont une fonctionnalité Raycast Pro, et +Raycast surveille le fichier : une modification enregistrée prend effet sans redémarrage. Le format est +documenté sur [manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers). +Aucune entrée `api_keys` n'est écrite ; cette exportation est donc limitée au bouclage et une liaison hors +bouclage est refusée. L'exportation DSH gérée nécessite DSH 0.1.0-rc.6 ou plus récent et ne possède que `llm-pi-ai.providers.opencodex`. DSH recharge à chaud ce fournisseur ; le modèle par défaut de l'utilisateur et diff --git a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md index af85f764aa..d5d4fb6f47 100644 --- a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md @@ -14,7 +14,7 @@ Gestion des fournisseurs non interactive. Les entrées de registre sont classée | Sous-commande | Drapeaux pris en charge | Actions | | --- | --- | --- | -| `list` | `--json` | Répertoriez les fournisseurs configurés et les entrées de registre restantes. | +| `list` | `--json`, `--jsonl` | Répertoriez les fournisseurs configurés et les entrées de registre restantes. `--jsonl` émet un objet JSON par fournisseur configuré et par ligne. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Ajoutez un fournisseur registry/custom. `--force` écrase ; `--sync` actualise un proxy en cours d'exécution en mode sortie humaine. | | `edit ` | indicateurs de champ du fournisseur, `--headers `, `--json` | Modifiez les champs de fournisseur en direct validés sans remplacer les pools de clés. `--headers` fusionne les en-têtes de requête personnalisés ; passez `{}` ou `-` pour les effacer. | | `test ` | `--json` | Sondez le véritable point de terminaison du modèle en amont. | @@ -28,6 +28,7 @@ Gestion des fournisseurs non interactive. Les entrées de registre sont classée ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -36,6 +37,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` écrit uniquement les fournisseurs configurés, un objet JSON par ligne. Chaque objet contient les mêmes champs qu’un élément du tableau `configured` de `--json`, sans le résumé `registryCount`. Les scripts peuvent traiter les objets ligne par ligne. `--json` et `--jsonl` ne peuvent pas être combinés. + :::caution[Les en-têtes personnalisés ne sont pas un canal d'identification] `--headers` est destiné aux métadonnées de requête non secrètes : conseils de routage, locataire ou sélecteurs de projets, identifiants de traçage. Ce n'est **pas** un endroit pour mettre l'authentification @@ -203,12 +206,11 @@ renvoient 1 ; une sonde de quota en amont qui échoue ou expire produit plutôt ### `ocx account auto-switch > [--json]` -Contrôle uniquement le groupe de comptes Codex `openai`. `on` règle 80 %, `off` règle 0 %, `status` lit la -valeur actuelle et `threshold ` accepte un entier de 0 à 100. Les autres fournisseurs et les valeurs -invalides entraînent le code de sortie 1. `--json` renvoie : +Contrôle le seuil du pool Codex `openai`, ou enregistre celui d’un pool OAuth générique. `on` enregistre 80 %, `off` 0 % et `threshold ` accepte 0–100. Les seuils génériques sont actuellement inactifs : leur sauvegarde ne change ni le basculement par seuil, ni l’activation du fournisseur, ni la rotation réactive après une erreur 429. Pour les pools génériques, les sorties utilisent la réponse confirmée du serveur. Pour un pool générique, `poolEnabled` est le réglage enregistré (`null` signifie non spécifié), pas l’état effectif hérité. `inert: true` indique que le seuil ne s’applique pas ; une capacité inconnue ne produit jamais `enabled: true`. Les fournisseurs à clé API, Anthropic et les valeurs invalides sont refusés. ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 aefefa414b..0babf299e3 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -111,7 +111,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `modelAutoCompactTokenLimits?` | `Record` | Budgets souples de compactage automatique par modèle, sous forme d'entiers sûrs positifs. Ils peuvent uniquement abaisser l'enveloppe effective de 90 % du contexte ou de l'entrée maximale et sont omis lorsqu'aucune fenêtre de contexte faisant autorité n'est connue. Pour le fournisseur canonique `openai`, les clés doivent être les identifiants exacts de modèles natifs pris en charge, sans préfixe de fournisseur ni de sélecteur de compte. PATCH fusionne les entrées ; `null` supprime une clé, tandis que `null` pour le champ entier efface la table. Ces marqueurs `null` sont réservés à PATCH. | | `defaultMaxOutputTokens?` | `number` | Solution de secours `openai-chat` à l’échelle du fournisseur lorsque le client omet `max_output_tokens`. | | `modelMaxOutputTokens?` | `Record` | Budgets de repli `openai-chat` positifs par modèle ; les correspondances exactes ou par motif priment sur la valeur par défaut du fournisseur. | -| `modelCosts?` | `Record` | Prix affichés par modèle (USD par 1M de jetons), indexés par l'identifiant exact du modèle en amont de ce fournisseur — et non par un identifiant de fournisseur ni par une étiquette routée `provider/model`, par exemple `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Tout identifiant de modèle constitue une clé valide : les fournisseurs personnalisés peuvent cibler n'importe quel point de terminaison compatible avec OpenAI au moyen de l'adaptateur `openai-chat`, et les identifiants de fournisseur locaux ou internes fonctionnent même s'ils sont absents des catalogues intégrés. Les prix configurés par l'utilisateur priment sur les catalogues intégrés dans les estimations des pages Journaux (`~$`) et Utilisation. Les entrées historiques sont recalculées à partir de la surcharge actuelle ; modifier un prix peut donc changer les totaux antérieurs. L'ordre de repli est le suivant : `modelCosts` défini par l'utilisateur → catalogue jawcode → surcharge des prix attendus → repli propre au fournisseur au niveau du modèle. Une entrée entièrement nulle passe à la source suivante. Chaque tarif doit être un nombre fini positif ou nul, inférieur ou égal à 1 000 000 (USD par 1M de jetons) ; les lignes hors plage sont rejetées par l'interface de gestion et ignorées au chargement. Ces valeurs servent uniquement à l'estimation lors de l'affichage : les surcharges n'affectent jamais le routage, la sélection des comptes, les quotas ni la facturation. | +| `modelCosts?` | `Record` | Prix affichés par modèle (USD par 1M de jetons), indexés par l'identifiant exact du modèle en amont de ce fournisseur — et non par un identifiant de fournisseur ni par une étiquette routée `provider/model`, par exemple `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Tout identifiant de modèle constitue une clé valide : les fournisseurs personnalisés peuvent cibler n'importe quel point de terminaison compatible avec OpenAI au moyen de l'adaptateur `openai-chat`, et les identifiants de fournisseur locaux ou internes fonctionnent même s'ils sont absents des catalogues intégrés. Les prix configurés par l'utilisateur priment sur les catalogues intégrés dans les estimations des pages Journaux (`~$`) et Utilisation. Les entrées historiques sont recalculées à partir de la surcharge actuelle ; modifier un prix peut donc changer les totaux antérieurs. L'ordre de repli est le suivant : `modelCosts` défini par l'utilisateur → catalogue jawcode → surcharge des prix attendus → repli propre au fournisseur au niveau du modèle. Une surcharge utilisateur explicitement définie à zéro produit une estimation nulle connue ; supprimez cette entrée pour rétablir la tarification automatique. Les prix de catalogue entièrement nuls restent soumis au repli. Chaque tarif doit être un nombre fini positif ou nul, inférieur ou égal à 1 000 000 (USD par 1M de jetons) ; les lignes hors plage sont rejetées par l'interface de gestion et ignorées au chargement. Ces valeurs servent uniquement à l'estimation lors de l'affichage : les surcharges n'affectent jamais le routage, la sélection des comptes, les quotas ni la facturation. | | `headers?` | `Record` | En-têtes supplémentaires en amont. L'autorisation, les cookies, les en-têtes de clé API, les nouvelles lignes intégrées et les noms invalides sont rejetés. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Préférences OpenRouter `order`, `only` et `allowFallbacks` par défaut ; valable uniquement pour les OpenRouter canoniques avec `openai-chat`. | | `modelOpenRouterRouting?` | `Record` | Remplacements exacts de l'ID de modèle qui remplacent la préférence OpenRouter à l'échelle du fournisseur. | @@ -123,7 +123,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `modelReasoningEfforts?` | `Record` | Libellés propres à chaque modèle. Une liste vide masque le contrôle de l'effort. Comme pour `reasoningEfforts`, chaque échelle configurée avec l'adaptateur `google` déclare la capacité `thinkingLevel` ; les requêtes directes et Vertex sans image utilisent le chemin Gemini à plat, tandis que Cloud Code Assist l'envoie dans son enveloppe de requête. | | `modelSupportsReasoningSummaries?` | `Record` | Définissez un modèle sur `false` pour arrêter la publicité des résumés et supprimer les champs de livraison du résumé. | | `modelReasoningSummaryDelivery?` | `Record` | Énumération de livraison des réponses par modèle ; réécrit un champ de livraison existant. | -| `modelAdapters?` | `Record` | Remplacement du protocole `openai-chat` ou `openai-responses` par modèle pour les passerelles multiprotocoles. Les entrées explicites priment sur les valeurs par défaut du registre. Le préréglage OpenCode Go sélectionne Responses pour `gpt-5.6-luna` tout en laissant les modèles apparentés sur leurs protocoles documentés ; DeepSeek peut sélectionner Responses natif pour `deepseek-v4-flash` ; GitHub Copilot déclare des valeurs par défaut limitées à Responses pour sa famille GPT-5 (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`), car ces modèles rejettent `/chat/completions` pour le trafic des agents. Les modèles sans valeur intégrée par défaut, comme `gpt-5.4-nano`, peuvent être activés ici. Les services en amont à protocole unique et le transfert canonique ChatGPT rejettent ces remplacements. | +| `modelAdapters?` | `Record` | Remplacement du protocole `openai-chat` ou `openai-responses` par modèle pour les passerelles multiprotocoles. Les entrées explicites priment sur les valeurs par défaut du registre. Le préréglage OpenCode Go sélectionne Responses pour `gpt-5.6-luna` tout en laissant les modèles apparentés sur leurs protocoles documentés ; DeepSeek peut sélectionner Responses natif pour `deepseek-v4-flash` ; GitHub Copilot déclare des valeurs par défaut limitées à Responses pour ces modèles (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`), car ces modèles rejettent `/chat/completions` pour le trafic des agents. Les modèles sans valeur intégrée par défaut, comme `gpt-5.4-nano`, peuvent être activés ici. Les services en amont à protocole unique et le transfert canonique ChatGPT rejettent ces remplacements. | | Activation Responses xAI (tableau de bord) | interrupteur | Pour `xai` uniquement, définit ou efface atomiquement les entrées `modelAdapters` de `grok-4.5` et `grok-4.6`. Une seule entrée apparaît comme un état mixte jusqu’à la prochaine écriture. Les autres remplacements et le comportement des tiers restent inchangés. | | `xaiResponsesXSearch?` | `boolean` | Désactivé par défaut. Sur une destination xAI Responses, ajoute la déclaration `x_search` hébergée par le fournisseur uniquement lorsqu’un outil `web_search` actif subsiste après la normalisation finale de la requête. Les déclarations existantes ne sont pas dupliquées, les sélecteurs `tool_choice`/`allowed_tools` de l’appelant ne sont jamais élargis, et cette option est distincte des options `search.xSearch` du service auxiliaire de recherche web. | | `modelPreferHostedTools?` | `Record` | Activation explicite par modèle exact pour les passerelles Responses hors transfert qui réservent un espace de noms aux outils hébergés. Seul `["image_generation"]` est actuellement accepté ; le modèle correspondant doit utiliser le protocole `openai-responses` et prendre en charge cet outil hébergé. Le proxy supprime les déclarations clientes `image_gen` en conflit et réécrit leurs sélecteurs afin de préserver le choix d'outil de l'appelant. Pour les modèles virtuels `-pro` de l'API OpenAI, l'identifiant public sélectionné est comparé en premier et l'identifiant résolu du modèle de base sur le protocole sert de repli. `modelAdapters` résout d'abord l'identifiant public, puis celui de base ; la seconde résolution détermine le protocole final. Les autres modèles conservent le comportement normal des alias. | @@ -482,6 +482,24 @@ avec un contexte de `922000` et une entrée maximale de `922000` ; OpenRouter i } ``` +## Éditeur de noms d'affichage des modèles + +Dans le tableau de bord, **Models** permet d'enregistrer durablement des noms lisibles pour les modèles découverts. Développez le fournisseur, +repérez un modèle découvert et choisissez **Name**. La boîte de dialogue garde le sélecteur exact +`provider/model` visible pendant que vous enregistrez un libellé lisible. Choisissez **Reset name** +pour revenir aux métadonnées du fournisseur ou au sélecteur utilisé par défaut. **Name** ne change +que l'affichage ; le crayon distinct consacré à l'alias modifie l'alias court de routage et n'est +pas un éditeur de nom d'affichage. Les lignes OpenAI natives et celles des modèles personnalisés +conservent leurs commandes existantes. + +Si la modification est enregistrée mais que l'actualisation échoue, la boîte de dialogue reflète +la valeur enregistrée et garde **Retry** disponible. Retry relance la convergence du catalogue +si le serveur a signalé son échec, ou recharge la liste si seule la requête de liste a échoué. +La reprise d'une réinitialisation conserve cette opération ; elle ne rétablit pas l'ancien nom. +Les requêtes ont un délai maximal de 60 secondes couvrant l'écriture et l'actualisation de la liste +qui suit. Un dépassement de délai n'annule pas une écriture : utilisez **Retry** pour vérifier +le nom actuel avant d'effectuer une autre modification. + ## Exemple complet ```json diff --git a/docs-site/src/content/docs/getting-started/for-agents.md b/docs-site/src/content/docs/getting-started/for-agents.md index daacbadb25..15b48fbf91 100644 --- a/docs-site/src/content/docs/getting-started/for-agents.md +++ b/docs-site/src/content/docs/getting-started/for-agents.md @@ -34,8 +34,10 @@ second terminal: ocx init ``` -The wizard writes `$OPENCODEX_HOME/config.json` (normally -`~/.opencodex/config.json`). It can also inject the proxy address into Codex's `config.toml` and +The wizard creates `$OPENCODEX_HOME/config.json` (normally +`~/.opencodex/config.json`) only if it is missing. Rerunning init keeps an existing config; it has +no force/overwrite flag. Invalid or concurrently created config is preserved and setup stops. +It can also inject the proxy address into Codex's `config.toml` and install the optional Codex autostart shim. `ocx init` never starts the proxy. For a fully non-interactive setup, configure providers with `ocx provider add` as shown below instead of driving the wizard. diff --git a/docs-site/src/content/docs/getting-started/how-it-works.mdx b/docs-site/src/content/docs/getting-started/how-it-works.mdx index 0344037b75..c75ffed90e 100644 --- a/docs-site/src/content/docs/getting-started/how-it-works.mdx +++ b/docs-site/src/content/docs/getting-started/how-it-works.mdx @@ -50,10 +50,20 @@ account before the request is forwarded upstream. The rule is intentionally spli its minimal non-stored account warmup request through the exact account whose window is due, coalesces simultaneous windows into one request, and durably persists both reset timestamps to prevent duplicate work after restarts. Paused accounts and accounts - requiring reauthentication are skipped; the next normal quota poll reports the activated window. + requiring reauthentication are skipped. Activation captures successful response quota headers; + opted-in idle accounts also refresh stale quota metadata at most once every five minutes, + without needing an open dashboard. Observed reset boundaries are retained across restarts + until completed, so a moving idle-window timestamp cannot erase a pending activation. + Metadata refresh uses the existing bounded authentication recovery; an inference 401 marks + the rejected credential for reauthentication instead of repeatedly spending retries on it. + Failures log only an opaque account label and a status-only reason. This is separate from reset-window routing: routing chooses an account for incoming work, while activation sends one request to a specific opted-in account only after its own reset is due. +**Downgrade note:** Before running an older version, remove only `nextFiveHourResetAt` and +`nextWeeklyResetAt` from automatic activation settings. Older strict readers reject these new +fields and can disable the entire activation settings block. + ## Sub-agent model selection On a fresh install, `subagentModels` features `gpt-6-astra`, the GPT-5.6 Sol/Terra/Luna trio, and diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 57f6c0ac9e..1d4b6b1b26 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -25,6 +25,17 @@ ocx init The result is saved to `$OPENCODEX_HOME/config.json` (default `~/.opencodex/config.json`). +`ocx init` creates a config only when none exists. An existing valid config is kept and setup +exits; use `ocx config` or the dashboard to update it. Invalid, unreadable, or symlinked config +entries are preserved and reported as errors. If another process creates the config during the +wizard, its file wins and setup stops before backup housekeeping or integration prompts. + +EOF or Ctrl+C before creation cancels setup. Cancellation after creation keeps the saved config. +Initial publication requires hard-link support and permission on the config filesystem; failures +stop setup without falling back to an overwrite. If publication or temporary-file cleanup cannot +finish, inspect the config directory before retrying: a complete config or private temporary file +may remain. + :::note[GPT-5.6 rollout entries] The current stable release seeds GPT-5.6 Sol/Terra/Luna for ChatGPT passthrough, OpenAI API-key, OpenRouter, and diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 8ce6387dc8..0c3cb32697 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -3,27 +3,19 @@ title: Claude Code description: Use any routed model from Claude Code — opencodex serves the Anthropic Messages API and gateway model discovery on the same port. --- -opencodex serves `POST /v1/messages` and `POST /v1/messages/count_tokens` alongside `/v1/responses`, so Claude +opencodex serves `POST /v1/messages` (plus `count_tokens`) alongside `/v1/responses`, so Claude Code can use every routed provider — OAuth logins, account pools, key failover and sidecars included — with zero extra auth work. -Generated OpenCodex roster definitions carry signed route and (when configured) effort directives. -The proxy verifies those directives before dispatching a provider request: an invalid or altered -signed directive fails closed with `400 invalid_request_error`. For compatibility with older, -unsigned definitions, a directive is honored only when it exactly matches an active -OpenCodex-owned roster entry; arbitrary unsigned text is ignored. See [Roster agents](#roster-agents-injectagents) -for operational details. - ## Claude OAuth account pool (experimental) You can log in multiple Claude accounts via the Providers dashboard (`ocx login anthropic` / add-account). By default every request uses the **active** account only. An **experimental, opt-in** Claude account pool (`anthropicAccountPool.enabled`) adds sticky -session affinity and usage-aware new-session selection across those OAuth accounts. When the -setting is omitted, 429 failover is enabled by the presence of two or more usable accounts, so a -rate-limited request can move to another account. Setting `anthropicAccountPool.enabled` explicitly -to `false` disables that reactive failover as well as the pool. For **new** +session affinity and usage-aware new-session selection across those OAuth accounts. It does +**not** gate 429 failover: with two or more usable accounts stored, a rate-limited request moves +to another account whether the pool is on or off, and that cannot be switched off. For **new** sessions, `anthropicAccountPool.strategy` selects among eligible accounts: `quota` (default) picks the lowest known usage in the window set by `quotaWindow` (`five-hour` by default, or `weekly` / @@ -33,11 +25,20 @@ reauthentication, or threshold, then advances. It is **off by default**, shows a and is not battle-tested — Anthropic may restrict accounts that look like automated rotation; rotation does not protect against provider enforcement. -Operational contract when failover is active: - -- Upstream **429** cools that account using `Retry-After` when present (else a default backoff), - clears its affinities, and may rotate to another eligible account within the same request - (bounded). +Operational contract when enabled: + +- Upstream **429** cools that account, clears its affinities, and may rotate to another eligible + account within the same request (bounded). The cooldown uses a usable `Retry-After` when present, + otherwise the latest valid reset time among windows Anthropic marks `rejected`, including + weekly windows. Valid upstream deadlines are not shortened to a fixed cooldown ceiling. + A refusal with no usable deadline falls back to a 60-second default backoff. +- Responses report the serving account's 5-hour and weekly utilization, and whichever of those + two the response carries is recorded for that account — each window independently, and a + refusal counts as well as a success. Usage-aware selection works from ordinary traffic, + without waiting for a dashboard poll. Headers preserve model-specific quota windows and do + not postpone usage probes or clear a failed usage probe's unavailable status. Measurements + whose known reset time has passed are discarded as unknown, including retained model-specific + windows. Values without a known reset are preserved; missing data is never reported as zero usage. - Affinity is **process-local** (lost on proxy restart). - **401/403** credential failures quarantine the account (`needsReauth`) so it is excluded from selection until re-authenticated. @@ -308,8 +309,16 @@ canonical ids. The synthetic 2026 date is an internal slot, not a release date. and `claude-ocx---` ids from older configs still resolve. If Claude Desktop's footer picker does not change the model for an already-running 3P -conversation, use `/model ` in that conversation. OpenCodex cannot observe picker state; it -routes the model id carried by each request. Confirm the result under **Logs → requestedModel**. +conversation, you can try `/model `, but this workaround may also fail on affected Desktop +builds. [Issue #3782](https://github.com/lidge-jun/opencodex/issues/3782) reports that on Windows +with Claude Desktop 1.46388.4, the conversation continues using its initial model after both +footer-picker and `/model` changes. The report does not establish which client or routing +component causes the behavior. + +You can also try selecting the intended default model in the OpenCodex Claude Desktop profile, +reapplying the profile, and starting a new conversation. This is a troubleshooting step, not a +guaranteed fix. OpenCodex cannot observe picker state; it routes the model id carried by each +request. Confirm what the client sends under **Logs → requestedModel**. Models with an authoritative 1M context window get an extra `…[1m]` picker row: selecting it makes Claude Code account a full 1M context for that model (auto-compaction stays on) — the proxy strips @@ -404,32 +413,6 @@ Proxy startup/ensure, `ocx claude`, and relevant dashboard saves sync your featu Dispatch: `subagent_type: "ocx-gpt-5-6-sol"`. 1M-capable targets carry `[1m]` automatically. -**Directive trust:** the generated definitions are signed. Each `ocx-*` agent body carries -`` (plus `` when an effort is configured) together -with a matching `` signature that OpenCodex creates with a local signing key -it manages automatically. `ocx doctor` reports that key's presence and permissions without ever -printing the key itself. The proxy verifies the signature on every request **before any provider -dispatch**: when a signature is present but malformed, altered, corrupted, or otherwise invalid, the -request rejects with a `400 invalid_request_error` — it is never routed to another provider. A -definition with no signature may use the compatibility path, but only when its directive exactly -matches an active OpenCodex-owned roster entry; arbitrary unsigned text is ignored. A failed -signature check never falls back to that roster path. - -## Unauthenticated loopback listener (opt-in) - -When the proxy listens on a non-loopback address that requires a credential, a local client that -never receives that credential would be refused. For that exact case OpenCodex can open a second -listener bound to `127.0.0.1`: set `unauthenticatedLoopbackListener` in the configuration -(**off by default**; the port is required, must differ from the proxy port, and is never -OS-assigned). See -[Local clients that cannot receive the token](/reference/configuration/#local-clients-that-cannot-receive-the-token). - -When enabled, the listener for Claude Code admits exactly two routes: `POST /v1/messages` and -`POST /v1/messages/count_tokens` (both POST-only). Every other method and path — including -`/api/*` and the dashboard — returns `404`. Public-listener authentication is unchanged: -enabling this listener never relaxes the main listener. Codex-specific routes on this listener -are described in the configuration reference. - ## Bundled-skill elision (blockedSkills) Claude Code's bundled `claude-api` skill injects ~840KB (~136k tokens) of Anthropic documentation @@ -467,45 +450,6 @@ Lookup order: discovery alias → exact id → id with date suffix stripped (`-2 See [Desktop alias resolution](#desktop-alias-resolution) for the rejection policy. -## Compatibility mode - -Routed Claude requests are evaluated for feature compatibility before the proxy contacts any upstream. The analyzer is a small pure function with no network or routing side effects and no Lab dependency. - -```json -{ - "claudeCode": { - "compatibility": "enforce" - } -} -``` - -| Mode | Value | Behavior | -| --- | --- | --- | -| `enforce` | default | Pre-network check: requests carrying incompatible features are rejected with `400 invalid_request_error` and a reason naming the feature codes. Compatible requests pass through unchanged. | -| `shadow` | opt-in escape | Records ordinary incompatibilities without rejecting. Safety invariants such as genuine Anthropic signed-thinking ownership still reject before upstream activity. | - -Invalid values are ignored on load and therefore use the `enforce` default. - -Feature codes (stable, also visible in the bounded debug ring): - -| Code | Meaning | Enforce result | -| --- | --- | --- | -| `cache_control` | Positional Anthropic prompt-cache marker | informational on translated targets; preserved and validated on Anthropic targets | -| `context_management` | Top-level `context_management` field | the exact Claude Code `clear_thinking_20251015` + `keep: "all"` no-op is allowed; mutating forms reject | -| `thinking_block` | `thinking` param or `thinking`/`redacted_thinking` content blocks | allowed (informational) | -| `signed_thinking` | Genuine Anthropic signature or redacted-thinking payload | reject on every non-Anthropic target, including in `shadow`; preserve on Anthropic targets | -| `tool_search` | Claude tool-search declaration, call, or result | translate through Responses tool search | -| `web_search_tool` | Claude web-search declaration, call, or result | translate through the existing web-search path | -| `deferred_tools` | Tools with `defer_loading: true` or a top-level deferred flag | translate only on the native Responses adapter; reject elsewhere | -| `input_examples` | Anthropic-native tool input examples | preserve on Anthropic targets; reject on translated targets | -| `documents`, `code_execution`, `computer_use`, `mcp_tool`, `server_tool` | Anthropic-native content or server tools without a lossless Responses lowering | reject on translated targets; preserve on Anthropic targets | -| `container`, `inference_geo`, `user_profile`, `unknown_body_field`, `unknown_content_block` | Anthropic-only or unrecognized semantic request fields | reject on translated targets; preserve on Anthropic targets | -| `structured_output` | `output_config.format` `json_schema` | translate | -| `service_tier` | Anthropic service tier | translate through provider capability sanitation | -| `beta_*` | Each `anthropic-beta` token, sanitized to `beta_` (sorted, de-duplicated) | allowed (informational) | - -Diagnostics: the inbound debug ring (`GET /api/claude/inbound-debug`) carries `featureCodes`, `adapter`, and `decision` (`allow`/`reject`/`shadow`) per entry when capture is enabled. Check `featureCodes` there before changing the mode. Native passthrough is unchanged and never gated by this mode. - ## Sidecar matrix: web search and image understanding Routed models do not all have the same hosted tools or image support. opencodex fills those gaps @@ -586,12 +530,14 @@ The proxy translates every Anthropic Messages API request into the Codex Respons | Assistant text | `output_text` | | Assistant `tool_use` | `function_call` (`input` → JSON-stringified `arguments`) | | User `tool_result` | `function_call_output` (`is_error` → `[tool error]` prefix) | -| `thinking` / `redacted_thinking` replay | Ordered Responses reasoning items using the `ocxr1` continuity envelope | +| `thinking` / `redacted_thinking` replay | `reasoning` items with bounded `ocxr1` envelopes for signatures and redacted payloads | | Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named function→`{type:"function",name}`, hosted WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +Replay preserves non-hidden signed blocks (including empty thinking) and opaque redacted blocks on the intended Anthropic adapter. `hideThinkingSummary` remains unchanged: locally hidden signed text is not exposed to Claude clients, and lossless replay through that hidden Claude boundary is not established. Older combined reasoning envelopes cannot recover original block order once streaming text has been emitted. `claudeCode.compatibility: "enforce"` still rejects thinking replay. This does not establish live Anthropic acceptance or cache-hit improvements; [#3719](https://github.com/lidge-jun/opencodex/issues/3719) remains open. + **Error cases (400):** malformed JSON; missing/empty `model`; missing/empty `messages`; unsupported role; `tool_result` without `tool_use_id`; `tool_use` without id/name; named `tool_choice` without name. @@ -603,14 +549,14 @@ name. | `response.created` | `message_start` + `ping` | | Heartbeat | `ping` | | Text deltas | `content_block_start` → `content_block_delta` (text) → `content_block_stop` | -| Reasoning summary/text | `thinking` block with a verified Anthropic signature when ownership matches, otherwise an OpenCodex `ocxr1` continuity signature | +| Reasoning summary/text | `thinking` block with the replayed signature, or a bounded `ocxr1` fallback envelope | +| Redacted reasoning | `redacted_thinking` blocks replayed from the reasoning envelope | | Function-call frames | `tool_use` block with `input_json_delta` | | Terminal event | `message_delta` → `message_stop` | | EOF before terminal | 502-style `api_error` | **Stop reason mapping:** `completed` → `tool_use` (if any tool call) or `end_turn`; -`incomplete/max_output_tokens` or retained `model_context_window_exceeded` → `max_tokens`; -`incomplete/content_filter` → `refusal`; retained `pause_turn` → `pause_turn`. +`incomplete/max_output_tokens` → `max_tokens`; `incomplete/content_filter` → `refusal`. **Error taxonomy:** 400 `invalid_request_error`, 401 `authentication_error`, 402 `billing_error`, 403 `permission_error`, 404 `not_found_error`, 409 `conflict_error`, @@ -619,15 +565,13 @@ other 5xx `api_error`. `Retry-After` is preserved. ## Prompt caching and token usage -**Anthropic-routed requests:** explicit client breakpoints are preserved in Anthropic wire order -(`tools` → `system` → `messages`) and validated before the request is sent: at most four markers, -with 1-hour markers before 5-minute/default markers. Requests translated to another protocol do not -promise equivalent positional caching; their `cache_control` markers are diagnostic only. +**Anthropic-routed requests:** the adapter manages cache breakpoints for tools, system content, +and the penultimate user message, plus top-level automatic `cache_control`. Stable turns normally +produce about a 99.9% cache hit rate. -**Native OpenAI/ChatGPT routing:** derives a session-scoped `prompt_cache_key` from -`x-claude-code-session-id` or `metadata.user_id`, and emits a `session_id` header only for that real client session. -A system-content cohort hash remains a shared prompt-cache hint, never a continuation identity or session header. -The cache key includes model and full tool schemas. +**Native OpenAI/ChatGPT routing:** derives a session-scoped `prompt_cache_key` (from +`metadata.user_id` when present, falling back to a system-content hash) and `session_id` header +for cache affinity. The cache key includes model and full tool schemas. **Token math:** Anthropic output subtracts `cached_tokens` and `cache_write_tokens` from `input_tokens`, exposing them as `cache_read_input_tokens` and `cache_creation_input_tokens`. @@ -702,45 +646,3 @@ it by default (`blockedSkills: ["claude-api"]`). **Subagent dispatches to wrong model** — Roster agents (`ocx-*`) use `` directives, not the Agent tool's `model` argument. Make sure the directive matches the intended route. Pass `"haiku"` as the model placeholder. - -## Client compatibility diagnostics - -Before `ocx claude` launches, opencodex checks the Claude Code version against the **2.1.201** -compatibility floor. The probe resolves to one of five states, each with actionable guidance: - -| State | Meaning | What to do | -| --- | --- | --- | -| `compatible` | Version is at or above the floor | Nothing | -| `outdated` | Version is below the floor | `npm install -g @anthropic-ai/claude-code` | -| `missing` | Claude Code is not installed | Install it with `npm install -g @anthropic-ai/claude-code` | -| `timed-out` | The version check timed out | Retry; repair or upgrade Claude Code if it persists | -| `unparseable` | The version could not be recognized | Repair or upgrade Claude Code, then retry | - -The probe is advisory: a below-floor, missing, timed-out, or unrecognized client **never blocks -launch** — the warning prints and `ocx claude` proceeds. `ocx doctor` and `ocx status --json` -surface the same client state. This floor is separate from the **2.1.129** native `/model` -gateway-picker capability. - -### Token-count benchmark (opt-in, may incur charges) - -The routed-path token approximation can be measured against real provider counts with -`bun run benchmark:claude-tokens -- --provider --model --confirm-live-provider-charges [--json]`. -The command **is** the consent: without `--confirm-live-provider-charges` it performs argument -validation only and sends nothing. When confirmed, it sends real requests and **can incur -provider charges**. Never automate or unattended-script it — run it deliberately, with an eye on -the account. - -What it does: - -- Targets Anthropic-adapter provider/model pairs only (the provider must be key-authed and list - the model), so the upstream reports authoritative `input_tokens`. -- Sends a deterministic, sanitized fixture set — no customer text is read or embedded. -- Sends fixtures one at a time; failures are typed and never retried, with no concurrency and no - fallback. -- Emits a closed, non-persistent report: fixture ids, digests, states, metrics, and the provider - kind + model id only. No request bodies, credentials, or account identifiers are ever written. -- Applies a per-fixture tolerance of max(32 tokens, 20%) and passes only when the weighted - aggregate absolute error stays within 10%. - -Routed `/v1/messages/count_tokens` behavior itself is unchanged by the benchmark: it stays -local for routed models and passes through to Anthropic only for native `sk-ant-` credentials. diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index da01d969c5..a83719c7fd 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -64,6 +64,23 @@ or grant account entitlement. The separately billed `openai-apikey/daybreak-blue-latest` API row is a different route and its 1,050,000 / 922,000 limits are never copied into the Codex-login row. +For custom Astra and Daybreak rows on that canonical `openai` Codex-forward destination, +explicit `reasoningEfforts` are bounded by the model's pinned Codex capabilities. A custom +`["none", "minimal", "low"]` becomes `["low"]` in the catalog; a nonempty list with no +supported values also falls back to the native default as a single choice. An explicit `[]` +stays empty and has no advertised default. A declared default is retained only if it belongs to +the resulting list; otherwise the native default is used when present, then the first surviving +choice. Stored custom configuration is unchanged, and repeated syncs do not add `max` back to a +narrow custom list. + +This requires the exact provider, destination, and capability-backed model identity. An arbitrary +gateway such as `YYLJ/gpt-6-astra` does not inherit native capabilities from its name. Its explicit +custom ladder continues to override discovered provider metadata under the normal routed rules. +Codex's native Astra `ultra` choice is retained: it is a client delegation mode converted to a +supported wire effort, distinct from the [API model's effort list](https://developers.openai.com/api/docs/models/gpt-6-astra). +Catalog normalization does not rewrite existing thread settings or establish support for a +particular installed Desktop version. + When the `codexAccountNamespaces` map is empty, account-qualified picker rows are off. If `codexAccountPickerEnabled` is omitted with a non-empty map, they are treated as enabled for backward compatibility. Set it to `false` to hide generated qualified rows and restore bare native diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 64466b61a4..f5592c0a72 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -215,6 +215,15 @@ HTTP/SSE. ### Authless Codex Desktop (opt-in) +In **Dashboard → Overview**, **Open Codex without signing in** controls this existing +opt-in preference. The switch defaults to **off** when the setting is absent or false; +an existing explicit `codexDesktopAuthless: true` stays enabled. The dashboard saves +the preference and runs a full sync. Restart Codex Desktop after changing it. +If synchronization fails, the saved preference remains and the dashboard shows the error; +retry **Sync** before restarting. Account-gated Desktop features may be unavailable +when enabled. Upstream credentials, local eligibility, remote admission authentication +and user-owned gateway settings retain their existing requirements. + Codex Desktop shows its ChatGPT login screen whenever the active provider requires OpenAI auth. If your OpenCodex setup never uses ChatGPT credentials (routed providers only, or a blocked `chatgpt.com`), you can opt out of that gate: @@ -330,6 +339,13 @@ Codex. Native custom calls and converted function calls use the same completion patch previews are held while their executable form is unresolved. JavaScript that merely contains patch text and unrelated native custom payloads stay unchanged. +Routed code-mode turns are also told the host's rules for the nested helpers before the first +call: `tools.apply_patch` takes one string that opens and closes with the bare patch marker lines, +the isolate has no `import`, and long-running commands are polled through `write_stdin`. When a +code-mode exec result on the native routed Responses, Kiro, or Cursor path still carries one of the host's +failure messages, opencodex appends a one-line hint naming the rule. This change does not rewrite +the model's code or its patch text. + Ordinary routed Responses function calls also use the original declared parameter schema at completion: integral floats in integer fields and integral numbers in string-only fields are normalized, while fractions and numeric unions stay unchanged. An explicitly empty completed @@ -570,3 +586,10 @@ ocx restore back # point plain Codex at the running proxy again When opencodex runs as a managed [background service](/reference/cli/#ocx-service), it sets `OCX_SERVICE=1` so a service-driven restart does **not** thrash the Codex config — only an explicit `ocx stop` / `ocx service stop` restores native Codex. + + +### Sub-agent fallback and V2 compatibility + +In **Subagents → Delegation settings**, edit the ordered fallback chain and its availability polling interval (5000–600000 ms), then save it separately from the featured roster. A configured target that is no longer advertised remains in the chain until you remove it. The roster and fallback chain are separate settings; this editor does not make the roster replace the fallback policy. + +When a routed preferred model may receive V2 work from a native ChatGPT parent, the panel explains the upstream encrypted-task limitation. Readable tasks from routed parents are unaffected. The guidance uses `/api/v2` mode and native V1 pin state; the current API does not expose recovery activation or request-specific eligibility, so the panel reports those as unknown. V1/plaintext-compatible delegation remains an alternative. Experimental V2 recovery, where eligible and explicitly enabled, adds quota usage, latency, backend dependence and possible fidelity loss; it does not repair the upstream protocol. See [sub-agent surfaces](/guides/sub-agent-surface/) and [the upstream limitation](https://github.com/lidge-jun/opencodex/issues/92). diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index 0c95908206..b475b71e20 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, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent and Aside 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, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside and Raycast from the dashboard — one switch per client, with a backup taken before every write. --- The **Integrations** tab writes opencodex's provider block into a client's own config -file, and removes it again. Twelve clients work this way, each with a switch: +file, and removes it again. Thirteen clients work this way, each with a switch: | Client | Config file | Format | When the change takes effect | Credential | |---|---|---|---|---| @@ -20,6 +20,7 @@ file, and removes it again. Twelve clients work this way, each with a switch: | Prime Agent | `~/.prime/agent/models.json` | JSON | new sessions | loopback placeholder | | ZCode | `~/.zcode/v2/config.json` | JSON | on restart | loopback placeholder | | Aside | `~/.aside/u//models.json` | JSON | after fully quitting and reopening Aside | loopback placeholder | +| Raycast | `~/.config/raycast/ai/providers.yaml` | YAML | immediately on save — Raycast watches the file | none — loopback only | Generated catalogs include only enabled models from each provider selection. This applies to both downloads and managed integrations, including Pi and Aside. The management model list still shows @@ -61,6 +62,50 @@ One caveat specific to Aside: the running app rewrites `models.json` itself, so fully quit and reopen Aside after applying, the same way Claude Desktop needs a restart. Aside's block is loopback-only and never carries a real credential. +The managed Raycast integration supports **macOS and Windows**. Custom Providers +is a **Raycast Pro** feature: on a free plan the file is still written, but +`ocx integration client status --client raycast` and the Integrations page report +a warning, because Raycast will not read it. On macOS or Windows, open Raycast → +Settings → AI → **Reveal Providers Config** once so the `ai` folder exists. +On these supported platforms, opencodex uses that folder as its install signal +and reports the client as not installed until it exists. Linux is unsupported, +even if the folder exists. + +The status field `aiDirPresent` reports only whether `~/.config/raycast/ai` exists, +independently of whether the Raycast app is installed or the platform is supported. +It does not prove that Raycast is installed or usable. The CLI prints `plan` on a +separate line and adds the macOS/Windows setup instruction when `aiDirPresent` is +false; `--json` preserves the raw status, including the nested `raycast` block. +Raycast reads `~/.config/raycast/ai/providers.yaml` on macOS and Windows alike and +does not honor `XDG_CONFIG_HOME`, so that path is not relocatable. + +The managed block is one element, `id: opencodex`, in the file's `providers` +sequence: `name: OpenCodex`, `base_url: http://:/v1`, and every +routed model with its `abilities` — the exporter sets `tools` and `system_message` to +`true` as a client-export convention, `vision` follows the catalog's input modalities, `reasoning_effort` +is set when the model has an effort ladder, and `temperature` is turned off for +reasoning models. Other providers in the file are preserved, and disable removes +only the OpenCodex element. Raycast picks up the change as soon as the file is +saved, no restart needed; the models appear in Raycast's model picker grouped +under **OpenCodex**. Raycast supports optional `api_keys`, but OpenCodex intentionally +omits them and refuses non-loopback or admission-authenticated targets; this integration +cannot supply OpenCodex's required admission header. + +The macOS private preference is only an advisory Pro hint; Windows never reads it and +reports the plan as unknown. Plan detection does not authorize or block a write. +The export metadata has no authoritative tool-support flag, so `tools: true` does not +prove every routed model supports tools. Vision and effort flags follow catalog metadata; +turning temperature off for an effort ladder is conservative export behavior. +Provider values are preserved; YAML formatting and comments are not guaranteed to survive. +The format is documented at +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers). + +Raycast CLI exports and dashboard downloads use the running server's destination and +admission policy, including a configured unauthenticated loopback listener. `ocx ensure` +does not refresh Raycast from its saved configuration snapshot: that can differ from the +running server. Server startup and explicit sync remain the catalog refresh paths. + + Cursor has a tab but is not one of these switches. Regular Cursor calls custom endpoints from its own backend, so a loopback proxy is unreachable without a public tunnel, and Cursor's separate Private Inference build is configured inside Cursor. The **Cursor** tab is read-only: @@ -130,7 +175,7 @@ than 1000 levels — which locks the switch instead, so nothing is silently chan **OMP** is unaffected by sibling edits too, for a different reason: its writer patches only its own `providers.opencodex` range byte-wise, so the rest of the file is never rewritten. For the remaining formats that can carry comments -(Hermes, OpenClaw, Kimi Code, Gajae Code, MiniMax Code — YAML, JSON5 and TOML +(Hermes, OpenClaw, Kimi Code, Gajae Code, MiniMax Code, Raycast — YAML, JSON5 and TOML written as whole documents), or whenever our own entries were edited, the switch locks and disable refuses rather than guessing which edits were yours. @@ -216,10 +261,12 @@ ocx integration client enable --client mcode ocx mcode ``` -Once connected, `ocx sync` refreshes owned MCode, Pi, and Aside catalogs with the current -model selection, context windows, and reasoning-effort ladders. Changes to model visibility, -provider selection, or presets also refresh connected Pi and Aside catalogs. Foreign-edited -or unsafe blocks stay untouched, as do previously owned blocks you removed manually. +Once connected, `ocx sync` and `POST /api/sync` refresh owned MCode, Pi, Aside, and +Raycast catalogs with the current model selection, context windows, and reasoning-effort +ladders. Proxy startup refreshes an owned Raycast catalog. Changes to model visibility, +provider selection, or presets also refresh connected Pi, Aside, and Raycast catalogs. +Missing, foreign-edited, or unsafe blocks stay untouched, as do previously owned blocks +you removed manually. An enabled Aside profile is an exception to the usual owned-only refresh: if its account directory exists and it has never had an owned block, sync may create its first block when that slot is empty. A prior Aside connection enables this behavior for all registered diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md index 79e74e8e2d..5333b839ce 100644 --- a/docs-site/src/content/docs/guides/model-ordering.md +++ b/docs-site/src/content/docs/guides/model-ordering.md @@ -112,8 +112,7 @@ have expanded into selector-qualified groups. Use `subagentModels` to choose and order the leading models that Codex also advertises to `spawn_agent`. The dashboard's **Sub-agents** page can reorder bare native and routed ids. Use `ocx agent subagents set` or edit the opencodex configuration for exact -`/` choices; the dashboard does not list those choices and omits them -if it saves the roster. Use at most five configured ids. With account selectors, one bare native +`/` choices; the dashboard retains those ids once saved, including choices currently unavailable. Use at most five configured ids. With account selectors, one bare native choice can expand into multiple selector-qualified catalog rows, so configured choices and advertised rows are not necessarily one-to-one. @@ -171,3 +170,32 @@ not reorder the native tool's advertised list. `disabledModels` and each provider's `selectedModels` remain visibility fields, not ordering controls. There is no separate `modelOrder`, `providerOrder`, or priority-map setting. + +## Dashboard picker presets + +On **Models**, choose **Default**, **A–Z by model**, **Group by provider**, or **Most used snapshot**, then **Apply order**. This saves the currently visible routed ids and `modelPickerOrderMode` (`alphabetical`, `provider`, or `most-used`). Most used reads all retained usage once when applied; reloads restore the snapshot without fetching usage again. New or removed models do not automatically recompute it. A manually saved order, including a complete/native order, remains untouched until you explicitly apply a replacement. Default clears both picker fields, even when no routed models are available. + +The controls use `GET/PUT /api/subagent-models`: `chosen` and `available` retain saved roster choices, including disabled or missing models; `pickerAvailable` contains only eligible routed catalog ids. The Models page sends `pickerOrder` and `pickerOrderMode`, never `models`. Roster-only saves preserve picker settings. Invalid combined updates and failed persistence leave the previous picker/roster state intact. + +Routed-only presets keep the existing featured/native priority bands. They affect the Codex catalog and Claude discovery's routed groups; Claude's native prefix and explicit Desktop profile/alias ownership remain unchanged. OpenCodex guidance ranks and configured fallback settings are preserved, but native Codex's advertised five and recommended default can change with display priority. Saving does not restart clients; a catalog refresh may remain pending, and clients holding an old catalog may need reopening. + + +### Custom routed order + +Choose **Custom order** on Models to load a fresh routed snapshot. Drag a movable row before +another row, or use its Up/Down buttons, then **Save draft**. Featured routed rows stay at the +front in their configured rank and cannot move. Native rows are not shown; this is not a preview +of the complete native picker. Surviving saved rows keep their relative order and new candidates +follow the current candidate list. Every save sends the complete routed list, without changing +the featured roster. + +An order containing bare native ids remains protected until you explicitly apply a routed preset +or Default. Selecting a different option alone does not replace it. Unknown featured state blocks +editing. Before saving, the editor checks a fresh snapshot; changes preserve your draft and block +saving until **Reload and discard draft** loads current settings. Request failures retain the +draft. Accepted saves can still have a pending catalog refresh; reload before editing again. + +The editor also requires an unambiguous model identity for every routed candidate. If the model +catalog is incomplete, refresh the Models page before editing; reloading picker settings alone +cannot restore missing catalog identities. Featured choices are matched exactly without trimming; +duplicate choices use their last configured position, and canonical ids take precedence over raw ids. diff --git a/docs-site/src/content/docs/guides/pi.md b/docs-site/src/content/docs/guides/pi.md index c44b97f12a..f44e4be381 100644 --- a/docs-site/src/content/docs/guides/pi.md +++ b/docs-site/src/content/docs/guides/pi.md @@ -27,6 +27,9 @@ export line, and how many models carry authoritative context limits. "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -41,6 +44,8 @@ export line, and how many models carry authoritative context limits. } ``` +Generated Pi providers enable `compat.sendSessionAffinityHeaders`. Keep this flag when merging or manually editing the provider: Pi supplies a stable session identity and OpenCodex derives canonical OpenCode Go affinity from it. Pi may omit the identity when `cacheRetention` is `none`. + Model ids are the proxy's canonical selectors, so routed models appear as `provider/model` (`anthropic/claude-opus-5`) and native OpenAI slugs stay unprefixed (`gpt-5.6-sol`). The `name` suffix — `(anthropic)`, `(native)`, `(routed)` — is what makes two same-named models from diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 48ae3261d9..0279ee96ba 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -95,9 +95,10 @@ The ChatGPT passthrough catalog also layers in the bare GPT-5.6 Sol/Terra/Luna s ## 2. Account login (OAuth) -Eight provider presets use OAuth login — plus GitHub Copilot via an experimental unofficial +Provider presets can use account login — including GitHub Copilot via an experimental unofficial device-flow bridge. opencodex stores their credentials in -`~/.opencodex/auth.json` and refreshes them automatically. `chatgpt` is also accepted by the login +`~/.opencodex/auth.json`; refreshable tokens are refreshed automatically, while durable keys are +reused until the provider revokes them. `chatgpt` is also accepted by the login CLI; it acquires a ChatGPT credential while creating a `forward`-mode provider entry. ```bash @@ -109,6 +110,7 @@ ocx login kiro # import kiro-cli credentials (or token fallback) ocx login google-antigravity ocx login cursor # standalone Cursor PKCE login ocx login command-code # Command Code browser OAuth (or import ~/.commandcode/auth.json) +ocx login orcarouter-oauth # OrcaRouter browser consent + PKCE ocx login github-copilot # GitHub device flow → Copilot token (Copilot Pro/Business) ocx login chatgpt # standalone ChatGPT OAuth login ocx logout @@ -121,24 +123,13 @@ ocx logout | `kimi` | `openai-chat` | `https://api.kimi.com/coding/v1` | Kimi K2.7/K2.6/K2.5 coding models. | | `nous` | `openai-chat` | `https://inference-api.nousresearch.com/v1` | Nous Research subscription gateway (same backend Hermes Agent uses). Device-grant login against `portal.nousresearch.com`; the access token is the per-request inference JWT. Mixed paid + `:free` model catalog (`tencent/hy3:free`, `stepfun/step-3.7-flash:free`, ...) discovered live from the signed-in account. Refresh tokens are single-use and rotated on every refresh. | | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | Initial login imports the installed, signed-in `kiro-cli` session (on Unix, install with `curl -fsSL https://cli.kiro.dev/install` | `bash`; on Windows PowerShell, use `irm 'https://cli.kiro.dev/install.ps1'` | `iex`; then run `kiro-cli login`). **Add account** logs `kiro-cli` out, starts a fresh browser login that switches the account used by `kiro-cli`, and stores account-scoped profile metadata. Existing OpenCodex accounts are preserved, and cancellation or failure restores the previous `kiro-cli` session. | -| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. Live discovery uses CCA's authenticated `v1internal:fetchAvailableModels` endpoint and publishes the agent models available to the signed-in account; the maintained catalog remains the fallback. Quota is probed live via `retrieveUserQuota` and `retrieveUserQuotaSummary` (8-second timeout). CCA chat/adapter requests use SSE (`v1internal:streamGenerateContent?alt=sse`) and buffer that stream for unary callers. Built-in image generation uses the separate unary `v1internal:generateContent` endpoint. The adapter retries its maintained daily/production peer at most once after a first-host transport failure, empty stream, 404, or `UNAVAILABLE`; authentication, geoblock, invalid-request, and exhausted-quota responses do not trigger host failover. See [Claude on Antigravity](#claude-on-antigravity-cloud-code-assist) below. | +| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth over the Cloud Code Assist wire. Live discovery uses CCA's authenticated `v1internal:fetchAvailableModels` endpoint and publishes the agent models available to the signed-in account; the maintained catalog remains the fallback. | | `cursor` | `cursor` | `https://api2.cursor.sh` | Experimental PKCE login, live HTTP/2 transport with an opt-in HTTP/1.1 compatibility path, and account-filtered model discovery. | +| `orcarouter-oauth` | `openai-chat` | `https://api.orcarouter.ai/v1` | Browser consent and key exchange use `https://www.orcarouter.ai` with S256 PKCE. The returned user-owned `sk-orca-…` API key is stored in the existing credential store and reused until revoked. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. | -### Antigravity pacing and TLS profile +Google Antigravity account and provider quota probes use fixed Google accounting endpoints, including the models fallback. They support transparent Fake-IP DNS for those destinations while retaining TLS verification, redirect rejection and private-address checks. A custom provider base URL changes model requests, not quota destinations; `NO_PROXY` continues to select the direct-route policy. -The built-in `google-antigravity` provider uses conservative request pacing by default: 30 RPM, -at least 2,000 ms between request starts, and up to 500 ms of positive jitter. Existing explicit -`requestPacing` settings remain authoritative; `jitterMs` may be set from 0 through 60,000 ms and -only delays a start. Model rules can make the provider slower, never faster. - -The dashboard can explicitly enable `tlsProfile: "antigravity-browser"` for this provider. This is -an experimental, unofficial compatibility mechanism, not a compliance feature. -It may make traffic more distinctive, and initialization failures fall back to Bun; requests that -already reached the native transport are not replayed. The profile is limited to canonical Cloud -Code Assist hosts, keeps certificate and hostname verification enabled, and leaves OAuth/token/ -onboarding requests on standard Bun TLS. Users who prioritize account-policy safety should use the -official Gemini API-key, Vertex, or documented Gemini Code Assist routes. After a terminal Nous refresh failure, run `ocx login nous` to reauthenticate. @@ -234,23 +225,6 @@ cat accounts.json | ocx account import google-antigravity --format cockpit-tools Inline JSON and extra positional arguments are rejected. Keep exported files private and delete or store them securely after import. -### Claude on Antigravity (Cloud Code Assist) - -The `google-antigravity` provider routes Claude models through Google's Cloud Code Assist (Antigravity) -wire rather than Anthropic's native API. opencodex translates requests and responses at the Gemini -format envelope: tool use/result pairing follows Anthropic semantics (including stable `functionCall.id` -/ `functionResponse.id` fields), and Claude thinking blocks keep their `thoughtSignature` values across -turns. - -CCA Claude models reject histories that end with an assistant (model) turn — upstream treats that as -prefill. opencodex strips trailing model turns when safe and appends a `(continue)` user nudge when the -history would otherwise end on model output (for example after context compaction or interrupted-turn -replay). Histories that already end on a user message or tool result are left unchanged. - -Antigravity exposes only SSE transport. Unary (non-streaming) callers still go through the same -`parseStream` path; plain JSON bodies without `data:` framing are rejected as truncated SSE rather -than parsed as a separate JSON response format. - ### OAuth reliability opencodex coordinates token refresh and Codex pool routing so concurrent requests do not race the @@ -339,25 +313,6 @@ from the live CLI store, or when an existing primary CLI database has no recogni Repair or remove the unreadable database under the normal `kiro-cli` data path, unset those import selectors, then retry. Signing in from a machine with no existing `kiro-cli` session is unaffected. -## Azure OpenAI identity - -Azure OpenAI can use the Azure SDK's default credential chain instead of an API -key. Configure `adapter: "azure-openai"` (or `"azure"`), a real resource -`baseUrl`, and `azureCredential: { "type": "default-azure-credential" }`. -For a user-assigned managed identity, add the non-secret -`managedIdentityClientId`; it selects only that managed-identity leg. Identity -uses the exact scope `https://cognitiveservices.azure.com/.default`, sends one -`Authorization: Bearer` header, and reports credential/import failures only as -`Azure identity credential unavailable` without returning SDK diagnostics or -tokens. - -Set `models` and `liveModels: false` for the supported static catalog; Azure -identity does not use generic `/models` discovery. Do not combine -`azureCredential` with `apiKey`, `apiKeyPool`, or a non-key `authMode`. API-key -mode remains supported separately and uses the adapter's `api-key` header. -See the [Azure OpenAI authentication configuration reference](/reference/configuration/providers/#azure-openai-authentication) -for copyable identity and API-key examples. - ## 3. API-key catalog opencodex ships 79 built-in presets: 67 key-based, eight OAuth, three local, and one default @@ -400,6 +355,7 @@ free-experimentation model. | Vultr Serverless Inference | `https://api.vultrinference.com/v1` | | Baseten Model APIs | `https://inference.baseten.co/v1` | | Command Code | `https://api.commandcode.ai/provider/v1` | +| OrcaRouter | `https://api.orcarouter.ai/v1` | | Meta Model API | `https://api.meta.ai/v1` | | Meta Muse Code (CLI credential) | `https://api.meta.ai/v1` | | SambaNova Cloud | `https://api.sambanova.ai/v1` | @@ -415,6 +371,7 @@ free-experimentation model. | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| BigModel Coding Plan (Responses, static roster) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token plan (default): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · or Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -426,6 +383,22 @@ free-experimentation model. | Cloudflare AI Gateway | `https://gateway.ai.cloudflare.com/v1/{account-id}/{gateway}/anthropic` | | …and more | opencode zen, Vercel AI Gateway, Venice, NanoGPT, Synthetic, Qianfan, Alibaba, Parallel, ZenMux, LiteLLM | +**OpenCode Go** requires a stable session identifier for routing. OpenCodex derives +its Go session header from Codex thread/session headers, or from a client's +`x-opencode-session` header when Codex headers are absent. This applies to direct +Chat Completions requests and requests bridged to Responses. Even an `ocx_`-prefixed +inbound value is treated as client input and +hashed into Go affinity; the internal bridge carries the original value, so native +Chat, bridged Chat, and Responses derive the same result. Explicit provider-config +session headers are operator overrides and are sent unchanged. Clients must keep the +identifier stable within a conversation and distinct across conversations; requests +without a session identifier cannot receive automatic session affinity. +Generated Pi provider configurations enable `compat.sendSessionAffinityHeaders` +so Pi sends its per-session identity to the proxy. Existing manually managed Pi +configurations can set this option on their `opencodex` provider as well. +Pi can omit session affinity when `cacheRetention` is `none`; enable cache retention +when a stable upstream session is required. + **OpenCode Zen** (`opencode-zen`) and the keyless **OpenCode Free** preset share `https://opencode.ai/zen/v1`. Free models on that gateway often hit a short-window burst limit around 15–20 requests/minute (community-measured; OpenCode does not publish RPM). @@ -496,6 +469,49 @@ preset (`commandcode`) uses the active configured Bearer key for chat requests; (`command-code`) uses the stored account bearer for authenticated discovery and chat. Create Provider-API keys at [Command Code Studio](https://commandcode.ai/studio/). +**OrcaRouter authentication and discovery.** Choose either `ocx login orcarouter-oauth` for +one-click browser authorization or `ocx login orcarouter` to paste an existing API key. The PKCE +flow starts a loopback listener first, sends a fresh S256 challenge and state to +`https://www.orcarouter.ai/auth`, exchanges the single-use code at +`https://www.orcarouter.ai/api/v1/auth/keys`, and stores the returned user-owned key in +`~/.opencodex/auth.json`. The manual-key preset continues to use the normal provider key store. +Both modes route to `https://api.orcarouter.ai/v1` and discover the public live catalog with +`capability=chat`; non-chat media/rerank rows are excluded, and reported input modalities control +whether Codex offers image attachments. Because the catalog itself is public, manual key setup +reports validation as unknown instead of accepting that response as proof that the key works. + +For a one-origin self-hosted deployment, set the shared origin before the first PKCE login; the saved +inference URL is derived from the same origin: + +```bash +ORCAROUTER_BASE_URL=https://router.example ocx login orcarouter-oauth +``` + +For a split self-hosted deployment, set `ORCAROUTER_API_BASE_URL` and +`ORCAROUTER_AUTH_BASE_URL` separately. + +The value must be an HTTPS origin (or HTTP loopback for local development) with no credentials, +query, or fragment. Before the first login to a loopback/private self-hosted endpoint, explicitly +allow that destination in your `~/.opencodex/config.json` provider row. For example, merge this +entry into the existing `providers` object for a local development server: + +```json +{ + "orcarouter-oauth": { + "adapter": "openai-chat", + "baseUrl": "http://127.0.0.1:9999/v1", + "authMode": "oauth", + "allowPrivateNetwork": true + } +} +``` + +Then run `ORCAROUTER_BASE_URL=http://127.0.0.1:9999 ocx login orcarouter-oauth`. +Login preserves this explicit consent; setting the URL alone never enables private-network access. +Without the opt-in, destination validation rejects inference and model discovery for that endpoint. +This requirement concerns the provider endpoint; the browser callback listener needs no such opt-in. +Re-run the login after a relay `401`; OrcaRouter keys are durable and do not have a refresh-token grant. + **Meta Model API (`meta-model`).** Muse Spark on Meta's own OpenAI-compatible endpoint, served over `/v1/responses`. Create a key in [the Meta developer console](https://dev.meta.ai/docs/authentication) — Meta calls this @@ -552,13 +568,6 @@ account bearer; the Provider-API key preset (`commandcode`) uses the active conf key. A user-edited lookalike base URL is never probed. Remaining monthly, purchased, and free credits are shown as a USD window when Command Code also reports period spend. -**Command Code project context.** Optional `projectContext: "on"` on the OAuth `command-code` provider -only (not the API-key `commandcode` preset) fills `/alpha/generate` `memory`, `taste`, and `skills` -from the proxy working directory. Set it on `providers.command-code` via **Providers → Command Code → -Edit JSON**, start the proxy from the trusted Codex project, and restart after saving. Absent or -`"off"` keeps the empty envelope even when `AGENTS.md` or taste files exist. See -[Adapters](/reference/adapters/#command-code) for file paths, caps, and fail-soft behavior. - **SambaNova Cloud discovery.** The preset reads SambaNova Cloud's public `/v1/models` list from the fixed API host, preserves provider-native ids, and caps discovery at 128 KiB and 128 raw rows. Because the catalog is unauthenticated, the CLI login flow reports the key as unverifiable instead of treating @@ -637,10 +646,44 @@ negative, or internally inconsistent billing totals produce no report rather tha > interactive coding tools only. General API automation, custom application backends, and > non-interactive batch use are prohibited and may cause the plan key to be suspended. -> **Two GLM routes:** `zai` is the Z.AI international coding-plan subscription; `zhipu-bigmodel` +> **GLM billing routes:** `zai` is the Z.AI international coding-plan subscription; `zhipu-bigmodel` > is Zhipu's domestic BigModel pay-as-you-go endpoint. Different hosts, different keys, different > billing — a key issued for one will not authenticate against the other. +### BigModel Coding Plan over Responses + +Select **Zhipu AI — BigModel Coding Plan (Responses)** (`zhipu-bigmodel-responses`) +for the `openai-responses` endpoint `https://open.bigmodel.cn/api/v1`. This is separate +from `zhipu-bigmodel-coding`, which uses Chat Completions at `/api/coding/paas/v4`. + +The preset uses a **static roster** (`liveModels: false`) taken from the +[official BigModel Codex example](https://docs.bigmodel.cn/cn/coding-plan/tool/codex.md): + +| Model | Context tokens | Upstream selectable effort | Default effort | Reasoning summaries | +| --- | ---: | --- | --- | --- | +| `glm-5.3` | 1,048,576 | `low`, `high`, `max` | `max` | Supported | +| `glm-5-turbo` | 204,800 | None (empty list) | `max` | Supported | + +Both entries declare upstream text-only input. The Codex catalog advertises text and +image because opencodex's existing vision sidecar can describe images for text-only +models. Image handling requires an available, enabled vision sidecar; this does not +declare native BigModel image support. + +The default model is `glm-5.3`; Responses reasoning content is preserved on replay. +The existing Codex export adds its compatibility +`ultra` tier to GLM-5.3 and omits Turbo's default-effort field because Turbo has no +selectable ladder; the provider metadata still records `max` for both models. +For Turbo, outgoing Responses requests omit `reasoning.effort`, including a caller's +`max` or `ultra`, while preserving requested reasoning summaries. This leaves effort +selection to the upstream default; opencodex does not inject a selectable or wire `max`. + +The example's `models.json` is a local catalog file, not a documented HTTP model-list +response. This preset does not perform live model discovery. `glm-5.3-flash` is not +seeded here because its exact Responses metadata is not verified. An existing custom +provider with the same name keeps its configured destination and metadata. +CLI key login also skips the undocumented `/models` probe and reports validation as +unknown; successful key authentication is established by a subsequent inference request. + ### Multiple API keys Key-based providers can also keep multiple keys. Adding a key through the Providers page stores it @@ -689,8 +732,8 @@ device-flow login for a short-lived Copilot API token — not a pasted API key. a key/subscription-token gateway on its OpenAI-compatible endpoint. **Cloudflare AI Gateway** needs your account + gateway ids filled into the URL. -Copilot fronts a mixed-wire catalog: its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, -`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) rejects +Copilot fronts a mixed-wire catalog: the following models (`gpt-5.3-codex`, `gpt-5.4`, +`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) reject `/chat/completions` for agent traffic, so opencodex routes those models over the Responses API by built-in default while every other Copilot model stays on chat completions. The precedence is: hard wire pin → your explicit diff --git a/docs-site/src/content/docs/guides/remote-hub.md b/docs-site/src/content/docs/guides/remote-hub.md index 238452eaa5..6a2b2a33bd 100644 --- a/docs-site/src/content/docs/guides/remote-hub.md +++ b/docs-site/src/content/docs/guides/remote-hub.md @@ -163,35 +163,75 @@ input checks. ## Docker Compose opencodex does not publish an official container image. The repository does maintain a source-build -[`Dockerfile`](https://github.com/yansigit/opencodex/blob/main/Dockerfile), -[`compose.yaml`](https://github.com/yansigit/opencodex/blob/main/compose.yaml), and a narrow +[`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 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. On first normal startup it -creates a self-signed TLS certificate and owner-only private key in the state volume; later starts -validate and reuse that identity. +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 +of restoring a native desktop configuration. Docker supplies supervision; no OS service manager +is installed in the image. Use Compose to restart/recreate the container; this does not extend +support to every dashboard restart path. -The image seeds a first-run `hub` configuration that binds the TLS container listener to `0.0.0.0`. +The image seeds a first-run `hub` configuration that binds the container listener to `0.0.0.0`. Before the first normal start, stream a freshly generated data-plane token into the bootstrap helper. -The helper accepts at most one 512-byte line, never prints the token, refuses to replace an existing +The helper accepts at most one 4096-byte line, never prints the token, refuses to replace an existing token, and persists it as the canonical owner-only `service-api-token` in the `ocx-state` volume. +The deployment persists two separate homes: `ocx-state` at `/home/bun/.opencodex` for +OpenCodex configuration, provider credentials and usage, and `codex-state` at +`/home/bun/.codex` for Codex state and `opencodex-catalog.json`. The image and Compose +explicitly set `CODEX_HOME=/home/bun/.codex`, so this catalog path remains writable +with `read_only: true` and survives container recreation. The image creates both +directories for the non-root `bun` user with mode `0700`; existing volume +ownership and permissions are not migrated automatically. + +Do not combine `CODEX_HOME` and `OPENCODEX_HOME`: both products use an `auth.json` +filename with different formats. This packaging change adds persistence, not a +catalog generator. Materialize or import a valid catalog into +`/home/bun/.codex/opencodex-catalog.json` before the catalog acceptance check below; +without one, `catalog_not_found` remains the expected response. + +Upgrading preserves the existing `ocx-state` volume and adds `codex-state`; no files +are migrated automatically. If a previous workaround placed a catalog directly +under `/home/bun/.opencodex`, back it up and deliberately copy only the catalog to +the new Codex home, preserving owner-only access. Do not copy either product's +`auth.json` over the other. Deployments with a custom `CODEX_HOME` should retain +their explicit environment and writable volume mapping until migration is complete. +When overriding `CODEX_HOME`, mount that exact directory writable and persist the +default catalog at `${CODEX_HOME}/opencodex-catalog.json`. If `model_catalog_json` +explicitly selects another file, that resolved path must also be persisted. + +Keep the Compose project name stable during upgrades so the same named volumes are reused. +Mounts with existing foreign ownership, read-only mounts, and mounts using `volume-nocopy` +are not repaired by the image's directory setup. Persist separately selected catalog or SQLite +paths separately; an OS credential store is not backed up by these two volumes. + +When running without Compose, explicitly supply both named mounts. Dockerfile `VOLUME` +declarations alone create anonymous volumes that a later `docker run` does not automatically +reuse. These mount options use standalone example names; to reuse Compose data, substitute +its actual project-prefixed volume names: + +```sh +--mount type=volume,src=ocx-state,dst=/home/bun/.opencodex \ +--mount type=volume,src=codex-state,dst=/home/bun/.codex +``` + Install Git and Bun on the host first. Before **every** image build, run the existing canonical -generator from this Git checkout. It hashes Git-tracked working-tree sources and container authority -(stage any newly added files first), not an arbitrary directory scan. Do not change those files between +generator from this Git checkout. It hashes Git-tracked working-tree sources (stage any newly +added source files first), not an arbitrary directory scan. Do not change source files between generation and build. Only its untracked `src/generated/compatibility-version.json` artifact enters the image; `.git` remains outside the Docker context. Do not commit or hand-edit the manifest. The build rejects stale manifests: it verifies every recorded SHA-256 against the -read-only build context and again against the copied runtime files. It requires the Dockerfile, -Compose, `.dockerignore`, every tracked Docker bootstrap/config/probe file, `package.json`, -`bun.lock`, and `scripts/model-metadata.source.json`; only that exact scripts artifact is included, -not the rest of `scripts/`. Missing or mismatched files, extra source or Docker-authority files -absent from the manifest, and symlinks (including parent directories) fail the build. The only -source file exempt from the inventory is the generated manifest itself. If validation fails, -reconcile the tracked files, remove unintended files, and rerun the canonical generator. +read-only build context and again against the copied runtime files. It requires `package.json`, +`bun.lock`, and `scripts/model-metadata.source.json`; only that exact scripts artifact is +included, not the rest of `scripts/`. Missing or mismatched files, extra source files absent +from the manifest, and symlinks (including parent directories) fail the build. The only source +file exempt from the inventory is the generated manifest itself. If validation fails, reconcile +the tracked sources, remove unintended source files, and rerun the canonical generator. ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -199,20 +239,10 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -To verify the default loopback publication from the host, copy out the public certificate (never -the private key) and use it as the local CA: - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - Set an alternate host port without changing the container's fixed `10100` listener: ```bash OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz ``` Remote access is an explicit opt-in. Set `OPENCODEX_BIND_ADDRESS` to the host's LAN or Tailscale @@ -222,33 +252,11 @@ IP, or use `0.0.0.0` to publish on **all** host interfaces: OPENCODEX_BIND_ADDRESS=0.0.0.0 docker compose up -d ``` -The generated certificate covers only `localhost` and `127.0.0.1`. Prefer keeping the default -loopback publication and putting an authenticated TLS/tailnet frontend on the same host; configure -that frontend to validate the copied public certificate as its upstream CA. Direct publication -requires replacing the per-volume certificate and key with an identity for the exact remote name -and updating `tls.publicOrigin` before exposure. Use a firewall in either case. The bind override -changes only the host publication; the container listener remains `0.0.0.0:10100`. +Use a firewall and an authenticated TLS/tailnet frontend before exposing the port. The bind +override changes only the host publication; the container listener remains `0.0.0.0:10100`. Keep the same bind override on subsequent Compose invocations that recreate the hub. To update an existing deployment, regenerate the manifest, run `docker compose build`, and recreate the -hub with `docker compose up -d`; do not repeat the one-time token initialization. Startup migrates -a retained pre-TLS volume by installing the per-volume identity and an HTTPS origin using the -published host port. It preserves operator-managed certificate paths. To roll back to an older -HTTP-only image, stop the hub, remove only the TLS setting while the current image is still -available, and then start the older image; the identity files may remain in the volume: - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# select/build the older image, then recreate the hub -docker compose up -d -``` - -Startup fails closed when the managed certificate is expired, malformed, mismatched with its key, -or has unsafe ownership/permissions. Rotate a generated identity while the hub is stopped: move -`/home/bun/.opencodex/container-tls` to an owner-only backup name in the same volume, start the hub -to publish a complete replacement identity, copy out the new public certificate, and update every -pinned client before removing the backup. If acceptance fails, stop the hub and move the backup -back into place. Operator-managed certificate paths are never rotated by the bootstrap. +hub with `docker compose up -d`; do not repeat the one-time token initialization. Configure providers with the dashboard through an operator-owned management frontend, or with one-shot CLI commands that share the state volume. The commands below show the existing Remote Hub @@ -262,7 +270,7 @@ docker compose restart hub ``` Do not put a token in `ARG`, `ENV`, `COPY`, Compose YAML, image history, or command arguments. Do not -mount the Docker socket, host home, Codex home, SSH agent, or provider-key files. A management +mount the Docker socket, the host's home or Codex home, SSH agent, or provider-key files. A management ingress bound to `127.0.0.1:10101` inside the container is reachable only by a TLS/tailnet frontend in the same network namespace; never publish `10101` as a shortcut. @@ -270,23 +278,24 @@ After the container is healthy, run a separate readiness promotion check: ```bash docker compose exec hub bun -e \ - "const r=await fetch('https://127.0.0.1:10100/readyz',{tls:{rejectUnauthorized:false}});console.log(r.status,await r.text());if(!r.ok)process.exit(1)" + "const r=await fetch('http://127.0.0.1:10100/readyz');console.log(r.status,await r.text());if(!r.ok)process.exit(1)" docker compose exec hub bun -e \ - "const t=(await Bun.file('/home/bun/.opencodex/service-api-token').text()).trim();const r=await fetch('https://127.0.0.1:10100/v1/catalog',{headers:{'x-opencodex-api-key':t},tls:{rejectUnauthorized:false}});console.log(r.status);if(!r.ok)process.exit(1)" + "const t=(await Bun.file('/home/bun/.opencodex/service-api-token').text()).trim();const r=await fetch('http://127.0.0.1:10100/v1/catalog',{headers:{'x-opencodex-api-key':t}});console.log(r.status);if(!r.ok)process.exit(1)" ``` -These two fixed-loopback probes deliberately skip certificate identity verification and prove only -the local listener/readiness and authenticated route. They work with operator-managed certificate -paths and names, but do not replace the externally verified `curl --cacert` check above (or normal -system trust plus the exact configured hostname for a CA-signed certificate). - Then send one real authenticated routed response with a configured model. If the secret is absent or unreadable, a non-loopback hub must not be accepted as ready. Never treat liveness alone as proof. -`docker compose down` removes the container and network but retains the named volume. Treat +`docker compose down` removes the container and network but retains both named volumes. Treat `docker compose down --volumes` as destructive: it deletes configuration, OAuth credentials, usage -history, and the data-plane token together. +history, the data-plane token, and persisted Codex state together. + +Cross-platform CI builds the source image and checks startup, data-plane token admission, and +container recreation using an isolated Compose project with throwaway credentials. It verifies that +both named volumes and a synthetic catalog survive replacement. This check does not validate a +real provider account, OAuth callback, custom mount migration, or every CPU architecture; perform +the authenticated routed-response check above for your deployment. ## Rollback @@ -300,7 +309,9 @@ ocx config set hub.managementIngress '{"enabled":false}' ocx service repair ``` -For a container rollback, remove or replace the container while retaining the named state volume. +For a container rollback, retain both named state volumes and their mappings. An older image +can still use `CODEX_HOME=/home/bun/.codex` when that directory remains mounted; do not revert +to an older Compose file that drops the Codex mount. Do not merge the homes or rerun token bootstrap. For a service rollback, stop the branch service and repair the prior release against the same `OPENCODEX_HOME`. Disabling management ingress or Serve does not require changing the data listener. diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 32cd198174..72adcdcd70 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -134,6 +134,10 @@ on. **Logs** works the same way with `#logs` and `#logs/debug`. An older `#provi bookmark now lands on `#providers`. Cost values in **Logs** and **Usage** are API list-price equivalents calculated from reported tokens. +For a custom usage interval, the server must confirm the exact requested start and end times. +If an older running proxy does not support those bounds, the dashboard and CLI reject its report; +upgrade and restart that proxy before retrying. Resetting a manual model price affects only that +model, preserving other rates saved independently. They are not billing receipts or evidence of an actual charge; subscription usage or provider credits may apply instead. diff --git a/docs-site/src/content/docs/ja/getting-started/how-it-works.mdx b/docs-site/src/content/docs/ja/getting-started/how-it-works.mdx index 0970b0e7c0..2e0e87f1ee 100644 --- a/docs-site/src/content/docs/ja/getting-started/how-it-works.mdx +++ b/docs-site/src/content/docs/ja/getting-started/how-it-works.mdx @@ -39,6 +39,22 @@ Codex は OpenAI **Responses API** を使います。opencodex は HTTP と Serv `GET /api/codex-auth/accounts?refresh=1` でクォータを強制再照会できます。成功した上流 応答はクォータヘッダーを保存し、429 はアカウントをクールダウンに置き、401/403 は再認証必要状態としてマークします。 +- **アイドル状態の利用枠も自動開始できます。** 詳細設定の自動開始はデフォルトでオフです。 + 現在のメインアカウントと追加アカウントが報告する 5 時間枠・週間枠をまとめて切り替えます。 + 新しく追加したアカウントには自動で適用されません。Pool モードでは、期限が来たアカウントへ + 利用枠を消費する最小限の非保存リクエストを送ります。同時に期限が来た枠は 1 回にまとめ、 + 一時停止中・再認証が必要なアカウントやメインアカウントのハードロックは回避しません。 + 完了した応答のクォータヘッダーを保存し、有効なアイドルアカウントの古いメタデータも + 最大 5 分に 1 回更新するため、ダッシュボードを開いておく必要はありません。 + 観測済みの期限は完了まで再起動をまたいで保持し、後の照会で動く時刻に上書きされません。 + メタデータ照会は既存の回数制限付き認証回復を使い、推論の 401 は拒否された認証情報を + 再認証必要として扱います。失敗ログには不透明なアカウントラベルと安全な状態理由だけを記録します。 + これは入力リクエストのアカウント選択とは別の機能です。 + +**旧バージョンへ戻す場合:** 自動開始設定の `nextFiveHourResetAt` と `nextWeeklyResetAt` だけを +削除してから旧バージョンを起動してください。旧版の厳密な設定検証はこれらの新しいフィールドを +受け付けず、自動開始設定全体を無効にする場合があります。 + ## サブエージェントモデルの選択 新規インストールすると `subagentModels` のデフォルトで `gpt-6-astra`、GPT-5.6 Sol/Terra/Luna の 3 モデル、 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 164c3c23d4..a2c4c461c2 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -163,9 +163,18 @@ Claude Code 2.1.129 以降は `GET /v1/models?limit=1000` でゲートウェイ 提供します。両系列は継続してデコードできるため、どちらの形式でも `settings.json` に保存したモデルは 引き続き動作します。 -Claude Desktop のフッターピッカーで実行中の 3P 会話のモデルが切り替わらない場合は、その会話で -`/model ` を使用してください。OpenCodex はピッカーの状態を直接参照できず、各リクエストに -含まれるモデル ID をルーティングします。結果は **Logs → requestedModel** で確認できます。 +Claude Desktop のフッターピッカーで実行中の 3P 会話のモデルが切り替わらない場合は、 +`/model ` を試せますが、影響を受ける Desktop ビルドではこの回避策も失敗することがあります。 +[Issue #3782](https://github.com/lidge-jun/opencodex/issues/3782) では、Windows 上の +Claude Desktop 1.46388.4 で、フッターピッカーと `/model` のどちらで変更しても、会話が最初の +モデルを使い続けると報告されています。この報告だけでは、クライアントやルーティングのどの +コンポーネントがこの動作の原因なのかは確定できません。 + +OpenCodex の Claude Desktop プロファイルで希望するデフォルトモデルを選択し、プロファイルを +再適用して、新しい会話を開始することも試せます。これはトラブルシューティングの手順であり、 +解決を保証するものではありません。OpenCodex はピッカーの状態を参照できず、各リクエストに +含まれるモデル ID をルーティングします。クライアントが何を送信しているかは +**Logs → requestedModel** で確認してください。 **エイリアス構文ルール:** provider には `/` や `--` を含められず `native` と同じでもいけません。 `/` も `~` も含まない plain な model ID は v1 接頭辞 `claude-ocx-…` のままです。`/` または `~` を含む @@ -392,12 +401,14 @@ Claude Code の `/effort` 設定はアダプターでも維持されます。 | Assistant テキスト | `output_text` | | Assistant `tool_use` | `function_call`(`input` → JSON 文字列に変換した `arguments`) | | ユーザー `tool_result` | `function_call_output`(`is_error` → `[tool error]` 接頭辞) | -| `thinking` / `redacted_thinking` 再生 | 破棄 | +| `thinking` / `redacted_thinking` 再生 | シグネチャと秘匿ペイロードを境界付き `ocxr1` エンベロープに保持した `reasoning` 項目 | | Function ツール | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`、`none`→`none`、`any`→`required`、名前指定関数→`{type:"function",name}`、ホスト型 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +意図した Anthropic アダプターでは、非表示でない署名付きブロック(空の thinking を含む)と不透明な redacted ブロックを保持します。`hideThinkingSummary` は変更しません。ローカルで隠した署名付きテキストは Claude クライアントに公開せず、この非表示境界での無損失再生は未確認です。旧形式の結合エンベロープは、テキスト送信後に元のブロック順を復元できません。`claudeCode.compatibility: "enforce"` は引き続き thinking 再生を拒否します。実際の Anthropic 受理やキャッシュ改善の証明ではなく、[#3719](https://github.com/lidge-jun/opencodex/issues/3719) は未解決です。 + **エラー条件(400):** 不正な JSON、欠落または空の `model`、欠落または空の `messages`、未サポートの role、`tool_use_id` のない `tool_result`、id/name のない `tool_use`、name のない名前指定 `tool_choice` です。 @@ -408,7 +419,8 @@ role、`tool_use_id` のない `tool_result`、id/name のない `tool_use`、na | `response.created` | `message_start` + `ping` | | Heartbeat | `ping` | | テキスト delta | `content_block_start` → `content_block_delta`(text) → `content_block_stop` | -| 推論要約/テキスト | 合成シグネチャ付きの `thinking` ブロック | +| 推論要約/テキスト | 再生されたシグネチャ、または境界付き `ocxr1` フォールバックを持つ `thinking` ブロック | +| 秘匿化された推論 | 推論エンベロープから再生される `redacted_thinking` ブロック | | Function-call フレーム | `input_json_delta` を持つ `tool_use` ブロック | | 終了イベント | `message_delta` → `message_stop` | | 終了前に EOF | 502 形式 `api_error` | diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index d1d977b35c..46c33320f2 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -147,6 +147,14 @@ Codex の `exec` custom-tool grammar を受け付けない key-auth Responses pr `custom_tool_call` へ復元します。ネイティブ OpenAI の forward routing と、対応済みの `apply_patch` custom tool は 変更されません。 +ルーティングされた code-mode のターンには、最初の呼び出し前に、ネストされたヘルパーに関する +ホストの規則も伝えられます。`tools.apply_patch` は、装飾を付けないパッチマーカー行で始まり、 +同様のマーカー行で終わる単一の文字列を受け取ります。isolate では `import` を使用できず、 +長時間実行されるコマンドは `write_stdin` でポーリングします。ネイティブのルーティング済み Responses、 +Kiro、または Cursor の経路で、code-mode の exec 結果にホストの失敗メッセージがまだ含まれている場合、 +opencodex は該当する規則を示す 1 行のヒントを追加します。この変更でモデルのコードやパッチのテキストを +書き換えることはありません。 + 選択した provider は function/tool calling をサポートしている必要があります。tool call に対応しない text-only provider では `exec`、Browser、Computer Use は使用できません。ネイティブ OpenAI の項目は上流の tool mode を そのまま維持します。 diff --git a/docs-site/src/content/docs/ja/guides/model-ordering.md b/docs-site/src/content/docs/ja/guides/model-ordering.md index d9c782b3fb..74febb1e78 100644 --- a/docs-site/src/content/docs/ja/guides/model-ordering.md +++ b/docs-site/src/content/docs/ja/guides/model-ordering.md @@ -109,8 +109,7 @@ account selector がある場合、5 項目の制限は bare native の選択が 先頭モデルの順序を変更するサポート手段は、`subagentModels` を並べ替えることです。 ダッシュボードの **Sub-agents** ページでは bare native id と routed id を並べ替えられます。 設定と `ocx agent subagents set` は exact account-qualified -`/` id も受け付けますが、ダッシュボードはこれらの id を表示せず、 -リストの保存時にも保持しません。設定する id は最大 5 つにしてください。account selector がある +`/` id も受け付けます。ダッシュボードは保存済みの id を現在利用できなくても保持します。設定する id は最大 5 つにしてください。account selector がある 場合は 1 つの bare native が複数の selector-qualified 行に展開されるため、設定した選択肢と公開 される行は必ずしも一対一ではありません。 @@ -155,3 +154,11 @@ V1 には OpenCodex の推奨候補リストを注入しません。V2 にはク `disabledModels` と各プロバイダーの `selectedModels` は 表示の有無を制御するフィールドです。別の `modelOrder`、`providerOrder`、priority map 設定はありません。 + +## ダッシュボードの並び順プリセット + +**Models**でデフォルト、モデル名A–Z、プロバイダー別、使用量スナップショットを選び、順序を適用します。現在選択可能なルーティングIDと `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)を保存します。使用量は適用時に保持された全履歴を一度だけ読み、再読込やモデルの増減では再計算しません。既存のカスタム・ネイティブ全体順は明示的な適用まで保持されます。デフォルトは候補が空でも両フィールドを削除できます。 + +`GET/PUT /api/subagent-models` の `chosen` と `available` は無効・欠落した保存済みrosterも保持します。`pickerAvailable` は選択可能なルーティングIDのみです。Modelsは `pickerOrder` と `pickerOrderMode` を送り、`models` は送りません。Rosterだけの保存は順序設定を維持し、不正入力・保存失敗は以前の状態を保ちます。 + +featured・ネイティブの優先順位帯を保ち、CodexカタログとClaude検出のルーティンググループに適用します。Claudeのネイティブ先頭グループと明示的なDesktopプロファイル・alias所有権は維持されます。OpenCodexのガイド順位とfallback設定は変わりませんが、ネイティブCodexの上位5候補や推奨デフォルトは変わる場合があります。保存による再起動は行いません。更新が保留中、または古いカタログを保持するクライアントでは開き直しが必要な場合があります。 diff --git a/docs-site/src/content/docs/ja/guides/pi.md b/docs-site/src/content/docs/ja/guides/pi.md index 788fe48c60..9b637e84e4 100644 --- a/docs-site/src/content/docs/ja/guides/pi.md +++ b/docs-site/src/content/docs/ja/guides/pi.md @@ -23,6 +23,9 @@ ocx export --client pi "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -37,6 +40,8 @@ ocx export --client pi } ``` +生成される Pi プロバイダーでは `compat.sendSessionAffinityHeaders` が有効です。設定をマージしたり手動で編集したりする際も、このフラグを保持してください。Pi が送る安定したセッション識別子から、OpenCodex が正規の OpenCode Go 接続先用の affinity を生成します。`cacheRetention` が `none` の場合、Pi は識別子を送信しないことがあります。 + モデル ID はプロキシの正規セレクターであるため、ルーティングされたモデルは `provider/model` (`anthropic/claude-opus-5`) として表示され、ネイティブ OpenAI スラグはプレフィックスなし (`gpt-5.6-sol`) のままになります。 `name` サフィックス (`(anthropic)`、`(native)`、`(routed)`) により、異なるアップストリームの 2 つの同じ名前のモデルが Pi のピッカーで区別できるようになります。 ## どこへ行くのか diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index cd5223573f..659a3fed8a 100644 --- a/docs-site/src/content/docs/ja/guides/providers.md +++ b/docs-site/src/content/docs/ja/guides/providers.md @@ -114,6 +114,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 実験的。GitHub デバイスフロー + `copilot_internal` 交換(VS Code OAuth クライアント)。有効な Copilot サブスクリプションが必要で、公式のサードパーティ API ではありません。 | +Google Antigravity のアカウント・プロバイダーのクォータ確認は、モデル一覧へのフォールバックも含め、固定の Google エンドポイントを使用します。その宛先では透過 Fake-IP DNS に対応し、TLS 検証、リダイレクト拒否、プライベートアドレス検査を維持します。カスタム base URL はモデル要求にのみ適用されます。`NO_PROXY` は直接接続のポリシーを維持します。 + + Nous の refresh が終端失敗した場合は、再認証に `ocx login nous` を実行してください。 正規の Kimi Coding Plan プリセット(`kimi` アカウントログインと `kimi-code` API key)では、 @@ -213,6 +216,7 @@ Cline IDE/CLI のみで API からは使えません。`minimax/minimax-m2.5` | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (静的モデル一覧)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | トークンプラン(デフォルト): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 従量課金: `https://dashscope.aliyuncs.com/compatible-mode/v1` · またはカスタム | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -337,7 +341,7 @@ model ごとに capability が異なるため、provider 全体の parallel tool > コーディングツール専用としています。一般的な API 自動化、カスタムアプリのバックエンド、 > 非対話型バッチ利用は禁止されており、プランキーが停止される場合があります。 -> **GLM の経路は 2 つあります:** `zai` は Z.AI の国際コーディングプラン契約、`zhipu-bigmodel` +> **GLM の課金経路:** `zai` は Z.AI の国際コーディングプラン契約、`zhipu-bigmodel` > は Zhipu の中国国内向け BigModel 従量課金エンドポイントです。ホストもキーも課金も別で、 > 一方で発行したキーはもう一方では認証されません。 @@ -382,8 +386,8 @@ Amazon Bedrock ネイティブ API のような、これらの実装のいずれ **サブスクリプショントークン**(通常の API キーではない)で認証します。**Cloudflare AI Gateway** は URL にアカウント + ゲートウェイ ID を埋める必要があります。 -Copilot は混在 wire カタログを提供します。GPT-5 系モデル(`gpt-5.3-codex`、`gpt-5.4`、 -`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`)はエージェント +Copilot は混在 wire カタログを提供します。モデル(`gpt-5.3-codex`、`gpt-5.4`、 +`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)はエージェント 通信の `/chat/completions` を拒否するため、opencodex はこれらのモデルを組み込みデフォルトで Responses API 経由にルーティングし、他の Copilot モデルはすべて chat completions のままです。 優先順位は次のとおりです: ハード wire ピン → 明示的な diff --git a/docs-site/src/content/docs/ja/guides/remote-hub.md b/docs-site/src/content/docs/ja/guides/remote-hub.md index 09f3f6bc66..cffc233143 100644 --- a/docs-site/src/content/docs/ja/guides/remote-hub.md +++ b/docs-site/src/content/docs/ja/guides/remote-hub.md @@ -60,16 +60,36 @@ OAuth は `POST /api/oauth/login` で開始し、コールバックできない ## Docker とトラブルシューティング -公式 Docker イメージはありませんが、リポジトリには digest 固定の Bun イメージをローカルビルドするための、管理された `Dockerfile` と `compose.yaml` があります。初回の通常起動時に、自己署名 TLS 証明書と秘密鍵を `ocx-state` ボリュームの `/home/bun/.opencodex/container-tls/cert.pem` と `/home/bun/.opencodex/container-tls/key.pem` に生成します。秘密鍵は所有者だけが読み取れ、以降の起動では同じ証明書と鍵を検証して再利用します。データエンドポイントは HTTPS です。 - -初回の通常起動前に、データキーを stdin から一度だけ初期化します。bootstrap helper が受け付けるのは最大 512 バイトの 1 行だけです。キーは表示されず、既存のキーは上書きせず、`ocx-state` ボリューム内の所有者限定 `service-api-token` に保存されます。 - -ホストに Git と Bun が必要です。イメージをビルドするたびに、Git 管理下のソースから正規のマニフェストを生成し、生成後はビルドまでソースを変更しないでください。生成 JSON は Git に追加せず、`.git` は Docker コンテキストから除外します。ホスト側は既定で `127.0.0.1:10100` にバインドします。`OPENCODEX_PORT` はホスト側ポートと管理対象 TLS の `publicOrigin` の両方を変更しますが、コンテナ内のリスナーは `10100` のままです。 - -ビルドは古いマニフェストを拒否し、すべての SHA-256 をコンテキストとコピー後のファイルに照合します。マニフェストは `Dockerfile`、`compose.yaml`、`.dockerignore`、Git 管理下のすべての Docker authority ファイル、`src/`、`package.json`、`bun.lock`、`scripts/model-metadata.source.json` を認証します。欠落・不一致のファイル、マニフェストにない余分なソースまたは Docker authority ファイル、シンボリックリンクは拒否されます。 +ロールバック時も両方のボリュームとマウント先を維持してください。既存ボリュームの所有者や権限は自動修復されません。Compose を使わない場合の名前付きマウントと独自の状態パスについては、[正本ガイド](/guides/remote-hub/#docker-compose)を参照してください。 + +状態は二つのボリュームに分けて永続化します。`ocx-state` は +`OPENCODEX_HOME=/home/bun/.opencodex`、`codex-state` は +`CODEX_HOME=/home/bun/.codex` に対応します。両製品の `auth.json` は形式が +異なるため、ホームを同じディレクトリにしないでください。読み取り専用の +ルートでも、この二つのホームは書き込み可能です。 + +カタログは自動生成されません。認証付き `/v1/catalog` の確認前に、有効な +`/home/bun/.codex/opencodex-catalog.json` を生成または取り込んでください。 +空のホームでは `catalog_not_found` の 404 が正常です。アップグレードは既存の +`ocx-state` を保持して `codex-state` を追加しますが、ファイルは自動移行しません。 +以前 `.opencodex` に置いたカタログはバックアップし、カタログだけを所有者限定の +権限で移してください。`auth.json` を相互に上書きしないでください。 +`CODEX_HOME` を変更する場合は、そのディレクトリ自体を書き込み可能なボリュームに +マウントし、既定のカタログを `${CODEX_HOME}/opencodex-catalog.json` に置きます。 +`model_catalog_json` で別のファイルを指定した場合は、その解決先も永続化します。 +カスタム構成は、明示的な移行が完了するまで環境変数とボリュームの対応を維持します。 +`docker compose down` は両ボリュームを保持しますが、`docker compose down --volumes` +は `ocx-state` と `codex-state` の両方を削除し、認証情報・使用履歴・データキー・ +Codex の状態とカタログも失われます。更新や再起動の代わりに使わないでください。 + +公式 Docker イメージはありませんが、リポジトリには digest 固定の Bun イメージをローカルビルドするための、管理された `Dockerfile` と `compose.yaml` があります。初回起動前にデータキーを stdin から一度だけ初期化します。キーは表示されず、`ocx-state` ボリューム内に所有者限定の権限で保存されます。 + +ホストに Git と Bun が必要です。イメージをビルドするたびに、Git 管理下のソースから正規のマニフェストを生成し、生成後はビルドまでソースを変更しないでください。生成 JSON は Git に追加せず、`.git` は Docker コンテキストから除外します。ホスト側は既定で `127.0.0.1` にバインドします。リモート公開は `OPENCODEX_BIND_ADDRESS= docker compose up -d` で明示的に指定し、`0.0.0.0` は全インターフェースを公開します。ファイアウォールと認証付き TLS/tailnet フロントエンドで保護してください。 + +ビルドは古いマニフェストを拒否し、すべての SHA-256 をコンテキストとコピー後のファイルに照合します。欠落・不一致のファイル、余分なソース、シンボリックリンクは拒否されます。`package.json`、`bun.lock`、および `scripts/` から唯一取り込む `scripts/model-metadata.source.json` が必須です。 ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -77,33 +97,7 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -ホストから確認するには、公開証明書だけをコピーしてローカル CA として使います。秘密鍵はコピーしないでください。 - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -別のホスト側ポートを使う場合は、以降の Compose 実行でも同じ値を指定します。 - -```bash -OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz -``` - -リモート公開は `OPENCODEX_BIND_ADDRESS=` で明示的に選択し、`0.0.0.0` は全インターフェースを公開します。生成される証明書が対象とするのは `localhost` と `127.0.0.1` だけです。直接リモート公開する場合は、生成済みの証明書と鍵を正確なリモート名に対応する証明書と鍵に置き換え、`OPENCODEX_PUBLIC_ORIGIN=https://hub.example.com:10100` のように、パス、認証情報、クエリ、フラグメントを含まない正確な HTTPS origin を指定してください。ファイアウォールと認証付き TLS/tailnet フロントエンドで保護します。 - -保持されている TLS 導入前のボリュームは、次の起動時にボリューム固有の TLS identity と公開ホストポートを使う HTTPS origin へ自動移行されます。独自の証明書パスは保持されます。古い HTTP 専用イメージへ戻す場合は、現行イメージが利用できるうちに hub を停止して TLS 設定だけを削除してから、古いイメージを起動してください。証明書ファイルはボリュームに残してかまいません。 - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# 古いイメージを選択またはビルドしてから hub を再作成する -docker compose up -d -``` - -コンテナは非 root の `bun` ユーザー、読み取り専用のルートファイルシステムで実行され、公開するのはデータポートだけです。`10101` は公開せず、秘密値を `ARG`、`ENV`、`COPY`、Compose、イメージ履歴、argv に入れないでください。コンテナ内の health/readiness probe が証明書検証を省略できるのは、固定されたコンテナループバックへの接続だけです。外部の受入確認では、コピーした公開証明書またはシステムの信頼ストアを使い、実際に接続する正確なホスト名を必ず検証してください。healthcheck 後にも認証済みカタログと実リクエストを別途確認します。`docker compose down` はボリュームを保持し、`docker compose down --volumes` は設定、認証情報、キーも削除します。 +コンテナは非 root の `bun` ユーザー、読み取り専用のルートファイルシステムで実行され、公開するのは `10100` だけです。`10101` は公開せず、秘密値を `ARG`、`ENV`、`COPY`、Compose、イメージ履歴、argv に入れないでください。healthcheck 後にも readiness、認証済みカタログ、実リクエストを別途確認します。`docker compose down` はボリュームを保持し、`docker compose down --volumes` は設定、認証情報、キーも削除します。 - hub 停止時はオフライン切断できますが、キー失効は未完了のままです。 - 一時障害時だけ検証済み LKG を維持し、認証・スキーマ・サイズ・プロトコル障害でローカルへフォールバックしません。 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 d3daafb356..2bb539f5b1 100644 --- a/docs-site/src/content/docs/ja/reference/cli/agents.md +++ b/docs-site/src/content/docs/ja/reference/cli/agents.md @@ -125,7 +125,7 @@ Grok Build モデル フェンスを管理および適用します。 ## クライアント設定のエクスポート -### `ocx export --client ` +### `ocx export --client ` 実行中のプロキシに接続するクライアント設定を出力します。このコマンドは、ベース URL、モデル一覧、およびクライアントに応じた認証情報参照または `opencodex-loopback` プレースホルダーを含む `opencodex` プロバイダーブロックを、選択したクライアントのネイティブ形式でシリアル化します。 @@ -133,7 +133,7 @@ Grok Build モデル フェンスを管理および適用します。 |旗 |アクション | | --- | --- | -| `--client ` |必須。クライアントの設定形式を選択します。 | +| `--client ` |必須。クライアントの設定形式を選択します。 | | `--json` |構成 JSON のみを標準出力に出力するため、リダイレクトはバイト正確な出力をキャプチャします。 `--out` 書き込みメモを含むすべての診断は stderr に送られます。 | | `--out ` |設定を `` に書き込みます。既存のファイルの置き換えを拒否します。 | | `--force` | `--out` が既存のファイルを置き換えることを許可します。 | @@ -160,6 +160,9 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `mcode` | `~/.minimax/config.yaml` (`MINIMAX_DATA_DIR`、次に旧 `MAVIS_DATA_DIR` が設定時に優先。相対値は拒否されます) | `mcode-config.yaml` | なし — loopback placeholder | | `zcode` | `~/.zcode/v2/config.json` (`ZCODE_DATA_DIR` が設定時に優先。相対値は拒否されます) | `config.json` | なし — loopback placeholder | | `prime` | `~/.prime/agent/models.json` (`PRIME_AGENT_CODING_AGENT_DIR` が設定時に優先。相対値は拒否されます) | `prime-models.json` | なし — loopback placeholder | +| `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS と Windows で同じ。Raycast は `XDG_CONFIG_HOME` を尊重しません) | `raycast-providers.yaml` | なし — loopback のみ。`api_keys` エントリは書き込まれません | + +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 以外のバインドは拒否されます。 opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。opencodex が生成する Pi のエクスポートには環境変数が不要で、リテラルのプレースホルダー `opencodex-loopback` が入ります。この値は必須です。Pi はモデル リストを構築する際に `apiKey` を解決し、既存の設定に未設定の環境変数参照がある場合はプロバイダー全体を隠すためです。ループバックでは、生成されたプレースホルダーをプロキシが検査することはありません。 diff --git a/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md index 8ae5610435..ef31fdd9e2 100644 --- a/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ja/reference/cli/providers-accounts.md @@ -13,7 +13,7 @@ description: プロバイダー構成、資格情報、クォータ、および |サブコマンド |サポートされているフラグ |アクション | | --- | --- | --- | -| `list` | `--json` |構成されたプロバイダーと残りのレジストリ エントリを一覧表示します。 | +| `list` | `--json`, `--jsonl` |構成されたプロバイダーと残りのレジストリ エントリを一覧表示します。 `--jsonl` は設定済みプロバイダーごとに1行の JSON オブジェクトを出力します。 | | `add ` | `--adapter `、`--base-url `、`--api-key `、`--default-model `、`--set-default`、`--force`、`--json`、`--sync` |レジストリ/カスタムプロバイダーを追加します。 `--force` は上書きします。 `--sync` は、実行中のプロキシを人間出力モードで更新します。 | | `edit ` |プロバイダーフィールドフラグ、`--headers `、`--json` |キー プールを置き換えずに、検証済みのライブ プロバイダー フィールドを編集します。`--headers` はカスタム要求ヘッダーをマージします。`{}` または `-` を渡すとクリアします。 | | `test ` | `--json` |実際の上流モデルのエンドポイントを調査します。 | @@ -27,6 +27,7 @@ description: プロバイダー構成、資格情報、クォータ、および ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -35,6 +36,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` は設定済みプロバイダーのみを、1行につき1つの JSON オブジェクトとして出力します。各オブジェクトのフィールドは `--json` の `configured` 配列の要素と同じで、`registryCount` の集計は含みません。スクリプトは各行のオブジェクトを順に処理できます。`--json` と `--jsonl` は同時に指定できません。 + :::caution[カスタムヘッダーは認証情報の経路ではありません] `--headers` は秘密ではないリクエストメタデータ用です — ルーティングヒント、テナントや プロジェクトのセレクター、トレース ID など。認証情報を入れる場所ではなく、バリデーターは @@ -149,10 +152,11 @@ OAuth プロバイダーと API キー プロバイダーの場合、これに ### `ocx account auto-switch > [--json]` -`openai` Codex アカウント プールのみを制御します。 `on` は 80% を設定し、`off` は 0% を設定します。`status` は現在の値を読み取り、`threshold ` は 0 ~ 100 の整数を受け入れます。他のプロバイダーと無効な値は 1 を終了します。`--json` は次を返します。 +`openai` Codex プールのしきい値を制御するか、汎用 OAuth プールのしきい値を保存します。`on` は 80%、`off` は 0%、`threshold ` は 0–100 を保存します。汎用プールのしきい値は現在適用されません。保存しても、しきい値による切り替え、プロバイダーの有効化設定、429 エラー時のローテーションは変更されません。汎用プールの照会と変更の結果はサーバーの確認値を使用します。汎用プールの `poolEnabled` は保存された設定で、`null` は未指定です。継承後の実効状態ではありません。`inert: true` は未適用を示し、機能が不明な場合も `enabled: true` とは表示しません。API キープロバイダー、Anthropic、不正な値は拒否されます。 ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 20389c353e..ed41ad4a93 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -100,7 +100,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `modelAutoCompactTokenLimits?` | `Record` | モデルごとの正の安全な整数によるソフト自動圧縮予算。実効値であるコンテキストまたは最大入力の 90% の上限を下げることだけができ、信頼できるコンテキストウィンドウが不明な場合は出力されません。canonical `openai` では、キーは provider や account-selector の接頭辞を含まない、サポート対象の正確なネイティブモデル ID でなければなりません。provider PATCH はエントリをマージし、キーを `null` にするとそのキーを削除し、フィールド全体を `null` にするとマップを消去します。これらの `null` tombstone は PATCH 専用です。 | | `defaultMaxOutputTokens?` | `number` |クライアントが `max_output_tokens` を省略した場合の、プロバイダー全体の `openai-chat` フォールバック。 | | `modelMaxOutputTokens?` | `Record` |モデルごとの `openai-chat` フォールバック バジェットがプラスになります。正確な/パターン一致はプロバイダーのデフォルトを上回ります。 | -| `modelCosts?` | `Record` | モデルごとの表示価格(100万トークンあたりの米ドル)。そのプロバイダーの正確なアップストリーム モデル ID をキーにします(プロバイダー識別子やルーティングされた `provider/model` ラベルではありません)。値は `input`, `output`, `cacheRead`, `cacheWrite` の 4 フィールドです(例: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。組み込みカタログにないモデル ID も、任意の OpenAI 互換エンドポイントを対象とするカスタムプロバイダーや、ローカル・内部プロバイダーで有効です。ユーザー設定の価格は Logs の `~$` と Usage の見積もりで組み込みカタログより優先されます。過去のエントリも現在のオーバーレイで再計算されるため、価格を編集すると過去の合計が変わることがあります(フォールバック順: ユーザー設定 → jawcode カタログ → expected-price オーバーレイ → モデル別ベンダー価格)。全ゼロのエントリは次のソースにフォールバックします。各レートは 0 以上の有限数で、最大 1,000,000(100万トークンあたりの米ドル)です。範囲外の行は管理境界で拒否され、読み込み時に破棄されます。表示専用の見積もりであり、ルーティング・アカウント選択・クォータ・請求には影響しません。 | +| `modelCosts?` | `Record` | モデルごとの表示価格(100万トークンあたりの米ドル)。そのプロバイダーの正確なアップストリーム モデル ID をキーにします(プロバイダー識別子やルーティングされた `provider/model` ラベルではありません)。値は `input`, `output`, `cacheRead`, `cacheWrite` の 4 フィールドです(例: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。組み込みカタログにないモデル ID も、任意の OpenAI 互換エンドポイントを対象とするカスタムプロバイダーや、ローカル・内部プロバイダーで有効です。ユーザー設定の価格は Logs の `~$` と Usage の見積もりで組み込みカタログより優先されます。過去のエントリも現在のオーバーレイで再計算されるため、価格を編集すると過去の合計が変わることがあります(フォールバック順: ユーザー設定 → jawcode カタログ → expected-price オーバーレイ → モデル別ベンダー価格)。ユーザーが明示的に全レートを 0 にした場合は、既知のゼロ料金として見積もります。自動料金に戻すにはそのモデルの設定を削除してください。カタログの全ゼロ料金は引き続きフォールバックします。各レートは 0 以上の有限数で、最大 1,000,000(100万トークンあたりの米ドル)です。範囲外の行は管理境界で拒否され、読み込み時に破棄されます。表示専用の見積もりであり、ルーティング・アカウント選択・クォータ・請求には影響しません。 | | `headers?` | `Record` |追加の上流ヘッダー。認証、Cookie、API キー ヘッダー、埋め込まれた改行、および無効な名前は拒否されます。 | | `openRouterRouting?` | `OpenRouterProviderRouting` |デフォルトの OpenRouter `order`、`only`、および `allowFallbacks` 設定。 `openai-chat` を持つ正規 OpenRouter に対してのみ有効です。 | | `modelOpenRouterRouting?` | `Record` |プロバイダー全体の OpenRouter 設定を置き換える正確なモデル ID のオーバーライド。 | @@ -112,7 +112,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `modelReasoningEfforts?` | `Record` |モデルごとのラベル。空のリストは努力制御を非表示にします。 | | `modelSupportsReasoningSummaries?` | `Record` |モデルを `false` に設定して、概要の広告を停止し、概要配信フィールドを削除します。 | | `modelReasoningSummaryDelivery?` | `Record` |モデルごとの応答配信列挙型。既存の配信フィールドを書き換えます。 | -| `modelAdapters?` | `Record` | 混合配線ゲートウェイのモデルごとの `openai-chat` または `openai-responses` 配線オーバーライド。明示的なエントリはレジストリのデフォルトを破ります。DeepSeek のプリセットは `deepseek-v4-flash` のネイティブ Responses を選択でき、GitHub Copilot は GPT-5 ファミリー (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) を Responses 専用デフォルトとして宣言します。これらのモデルはエージェント トラフィックで `/chat/completions` を拒否するためです。`gpt-5.4-nano` のようなビルトイン デフォルトのないモデルはここでオプトインできます。単線アップストリーム ピンと正規の ChatGPT 転送はオーバーライドを拒否します。 | +| `modelAdapters?` | `Record` | 混合配線ゲートウェイのモデルごとの `openai-chat` または `openai-responses` 配線オーバーライド。明示的なエントリはレジストリのデフォルトを破ります。DeepSeek のプリセットは `deepseek-v4-flash` のネイティブ Responses を選択でき、GitHub Copilot は モデル (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) を Responses 専用デフォルトとして宣言します。これらのモデルはエージェント トラフィックで `/chat/completions` を拒否するためです。`gpt-5.4-nano` のようなビルトイン デフォルトのないモデルはここでオプトインできます。単線アップストリーム ピンと正規の ChatGPT 転送はオーバーライドを拒否します。 | | xAI Responses オプトイン(ダッシュボード) | スイッチ | `xai` のみで、`grok-4.5` と `grok-4.6` の `modelAdapters` エントリを原子的に設定または削除します。片方だけの場合は、次のスイッチ操作で両方が正規化されるまで混合状態を表示します。他のオーバーライドと tier 動作は変わりません。 | | `xaiResponsesXSearch?` | `boolean` | デフォルトでは無効です。xAI Responses の宛先では、最終的なリクエスト正規化後もライブの `web_search` ツールが残っている場合にのみ、プロバイダーがホストする `x_search` 宣言を追加します。既存の宣言は重複させず、呼び出し元の `tool_choice` / `allowed_tools` セレクターの範囲を拡張することもありません。また、これは `search.xSearch` オプションを持つウェブ検索サイドカーとは別です。 | | `modelPreferHostedTools?` | `Record` | hosted tool namespace を予約する非 forward Responses gateway 向けの完全一致モデル opt-in。現在は `["image_generation"]` のみを受け付けます。一致したモデルは `openai-responses` wire を使い、その hosted tool をサポートする必要があります。競合するクライアント `image_gen` 宣言を除去し、呼び出し元の tool choice を維持するため selector も書き換えます。OpenAI API の仮想 `-pro` モデルでは、まず選択した公開 ID に一致させ、解決後のベース wire-model ID をフォールバックとして使用します。`modelAdapters` は公開 ID、次にベース ID の順に解決し、後者の結果が最終 wire を決めます。未設定のモデルは通常の alias 動作を維持します。 | @@ -379,6 +379,14 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ 表示名には `modelDisplayNames` を使用します。優先順位は、運用者が設定した `modelDisplayNames`、プロバイダーカタログのメタデータ、通常の `provider/model` 表示の順です。キーはこのプロバイダー内の正確なネイティブモデル ID です。例えば `xai/grok-4.6` のキーは `grok-4.6` です。ラベルは表示専用で、正確なルーティング ID や上流モデル ID を変更しません。`config.json` の既存プロバイダー設定にこのフィールドだけを追加し、他のすべてのフィールドを残してください。`PUT /api/providers/:provider/model-display-names` に `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }` を送ると保存され、`displayName: null` を送るとその名前だけがリセットされます。 +ローカル Codex カタログでサポートされるプレフィックスなしのネイティブ GPT 行にも、 +`providers.openai.modelDisplayNames` で正確な表示名を指定できます。例えば `"gpt-6-astra": "GPT 6 Astra"` です。 +起動時の同期とローカルカタログの収束処理は、どちらもこれらの名前を再適用します。名前の設定を削除すると、行の現在の表示名が +適用済みの上書きとまだ一致する場合にのみ、元のネイティブ名が復元されます。外部で変更された表示名にも既存のネイティブメタデータ正規化が適用されます。 +例えば Astra (`gpt-6-astra`) では、固定されたネイティブ名と異なる名前は引き続きその固定名に置き換えられます。 +表示名の上書きによってモデル ID、メタデータ(機能を含む)、順序、ルーティングされたコンボのエイリアス、アカウント修飾付きの行は変更されません。 +このローカルカタログの上書きは、HTTP のモデル一覧や仮想 `*-pro` 行の表示名には適用されません。 + プレビュー GPT-5.6 フォールバック エントリは同じメカニズムを使用します。 OpenAI API キー プリセットは、ベース ID と Pro ID にコンテキスト `922000` と最大入力 `922000` をシードします。 OpenRouter は、コンテキスト `922000` を持つ `openai/gpt-5.6-sol`、`openai/gpt-5.6-terra`、および `openai/gpt-5.6-luna` をシードします。プール/ダイレクトは `922000` をアドバタイズします。同期されたカタログは、`xhigh` を区別しつつ、`max` をアドバタイズします。 ```json @@ -395,6 +403,22 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ } ``` +## モデルの表示名エディター + +ダッシュボードの **Models** では、検出されたモデルに読みやすい名前を付けて永続的に保存できます。プロバイダーを展開し、検出された +モデルを見つけて **Name** を選択します。読みやすい名前を保存する間も、ダイアログには正確な +`provider/model` セレクターが表示されます。**Reset name** を選ぶと、プロバイダーのメタデータ、 +または通常のセレクター表示に戻ります。**Name** が変更するのは表示だけです。別のエイリアス用 +鉛筆アイコンは短いルーティングエイリアスを変更するもので、表示名エディターではありません。 +ネイティブ OpenAI とカスタムモデルの行では、既存の操作方法が維持されます。 + +変更は保存されたものの更新に失敗した場合、ダイアログは保存済みの上書き設定を反映し、**Retry** を +引き続き利用できます。サーバーがカタログの収束処理の失敗を報告した場合、Retry はその処理を再実行し、 +一覧取得のリクエストだけが失敗した場合は一覧を再読み込みします。リセット後の復旧でもリセット操作を +維持し、以前の名前には戻しません。リクエストには、書き込みとその後の一覧更新を合わせて 60 秒の +期限があります。タイムアウトしても書き込みは取り消されません。次の変更を行う前に **Retry** で +現在の名前を確認してください。 + ## 完全な例 ```json diff --git a/docs-site/src/content/docs/ja/reference/proxy-formats.md b/docs-site/src/content/docs/ja/reference/proxy-formats.md index 4fe37a385a..2526714048 100644 --- a/docs-site/src/content/docs/ja/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ja/reference/proxy-formats.md @@ -144,6 +144,11 @@ Responses ルートは元の Responses body に保持し、モデルが `openai- これらのエンドポイントは、Claude Code および互換性のあるクライアントによって使用される Anthropic Messages 言語を話します。ほとんどのリクエストはレスポンスに変換され、通常どおりルーティングされてから、Anthropic JSON または Anthropic SSE に変換されます。 +変換される Messages リクエストでは、推論の再送もリクエスト共通の変換バジェットを使います。 +この制限にはエンコード・デコード時のコピー分も含まれます。超過時は +`translation_buffer_limit` を伴う HTTP 413 を返し、署名や不透明な推論データを切り詰めません。 +ネイティブ Anthropic パススルーには、別の本文サイズ制限が適用されます。 + ネイティブ Anthropic パススルーは、次のすべてが当てはまる場合にのみ適格です。 - ネイティブ パススルーはクロード コード設定で無効になっていません。 diff --git a/docs-site/src/content/docs/ko/getting-started/how-it-works.mdx b/docs-site/src/content/docs/ko/getting-started/how-it-works.mdx index e2a75024d1..1538c701b3 100644 --- a/docs-site/src/content/docs/ko/getting-started/how-it-works.mdx +++ b/docs-site/src/content/docs/ko/getting-started/how-it-works.mdx @@ -39,6 +39,22 @@ pool 계정을 고를 수 있습니다. 규칙은 의도적으로 둘로 나뉩 `GET /api/codex-auth/accounts?refresh=1`로 할당량을 강제 재조회할 수 있습니다. 성공한 업스트림 응답은 할당량 헤더를 저장하고, 429는 계정을 cooldown에 넣으며, 401/403은 재인증 필요 상태로 표시합니다. +- **유휴 상태의 할당량 창도 자동으로 활성화할 수 있습니다.** 고급 설정의 자동 활성화는 기본적으로 + 꺼져 있으며 현재 메인 계정과 추가 계정이 보고하는 5시간·주간 창을 함께 제어합니다. + 새로 추가한 계정에는 자동 적용되지 않습니다. Pool 모드에서는 만료된 창의 정확한 계정으로 + 할당량을 소비하는 최소한의 비저장 요청을 보내며, 동시에 만료된 창은 요청 하나로 묶습니다. + 일시 중지 또는 재인증이 필요한 계정은 건너뛰고 메인 계정의 하드록도 준수합니다. + 완료 응답의 할당량 헤더를 반영하고, 활성화 대상인 유휴 계정의 오래된 메타데이터는 최대 5분에 + 한 번 갱신하므로 대시보드를 열어 둘 필요가 없습니다. 관측한 만료 시점은 활성화가 끝날 때까지 + 재시작 후에도 유지되어, 나중의 조회에서 시점이 밀려도 대기 작업을 잃지 않습니다. + 메타데이터 조회에는 기존의 횟수 제한 인증 복구를 사용합니다. 추론 401은 거부된 자격 증명을 + 재인증 필요로 표시하며, 실패 로그에는 불투명한 계정 라벨과 안전한 상태 사유만 기록합니다. + 이 기능은 들어오는 요청의 계정을 선택하는 라우팅과 별개입니다. + +**다운그레이드 안내:** 이전 버전을 실행하기 전에 자동 활성화 설정에서 `nextFiveHourResetAt`과 +`nextWeeklyResetAt`만 제거하세요. 이전 버전의 엄격한 설정 검증은 이 새 필드를 허용하지 않아 +자동 활성화 설정 전체를 비활성화할 수 있습니다. + ## Sub-agent 모델 선택 새로 설치하면 `subagentModels` 기본값으로 `gpt-6-astra`, GPT-5.6 Sol/Terra/Luna 세 모델, 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 7894531b5b..33adf2bcbf 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -199,9 +199,17 @@ Claude Code 2.1.129 이상은 `GET /v1/models?limit=1000`에서 게이트웨이 제공해요. 두 계열은 계속 디코딩할 수 있으므로 어느 형식이든 `settings.json`에 저장한 모델이 계속 작동해요. -Claude Desktop의 하단 선택기로 이미 실행 중인 3P 대화의 모델이 바뀌지 않는다면, 그 대화에서 -`/model `를 사용하세요. OpenCodex는 선택기 상태를 따로 볼 수 없고 각 요청에 실린 모델 ID를 -라우팅해요. 적용 결과는 **Logs → requestedModel**에서 확인할 수 있어요. +Claude Desktop의 하단 선택기로 이미 실행 중인 3P 대화의 모델이 바뀌지 않는다면, +`/model `를 시도할 수 있지만, 문제가 있는 Desktop 빌드에서는 이 우회 방법도 실패할 수 있어요. +[이슈 #3782](https://github.com/lidge-jun/opencodex/issues/3782)에는 Windows의 +Claude Desktop 1.46388.4에서 하단 선택기와 `/model`로 각각 변경해도 대화가 처음 모델을 계속 +사용한다는 보고가 있어요. 이 보고만으로는 클라이언트나 라우팅의 어느 구성 요소가 이 동작을 +일으키는지 확정할 수 없어요. + +OpenCodex의 Claude Desktop 프로필에서 원하는 기본 모델을 선택하고, 프로필을 다시 적용한 뒤 +새 대화를 시작하는 방법도 시도할 수 있어요. 이는 문제 해결을 위한 시도이며 해결을 보장하지는 +않아요. OpenCodex는 선택기 상태를 볼 수 없고 각 요청에 실린 모델 ID를 라우팅해요. +클라이언트가 실제로 무엇을 보내는지는 **Logs → requestedModel**에서 확인하세요. **별칭 문법 규칙:** provider에는 `/`나 `--`를 넣을 수 없고 `native`와 같아도 안 돼요. `/`와 `~`가 없는 plain model ID는 v1 접두사 `claude-ocx-…`를 유지해요. `/` 또는 `~`가 있는 model ID는 v2 @@ -433,12 +441,14 @@ Claude Code의 `/effort` 설정은 어댑터에서도 유지돼요. | Assistant 텍스트 | `output_text` | | Assistant `tool_use` | `function_call`(`input` → JSON 문자열로 변환한 `arguments`) | | 사용자 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 접두사) | -| `thinking` / `redacted_thinking` 재생 | 버려요 | +| `thinking` / `redacted_thinking` 재생 | 서명과 비공개 페이로드를 제한된 `ocxr1` 봉투에 담은 `reasoning` 항목 | | Function 도구 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, 이름 지정 함수→`{type:"function",name}`, 호스팅 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +의도한 Anthropic 어댑터에서는 숨기지 않은 서명 블록(빈 thinking 포함)과 불투명 redacted 블록을 보존해요. `hideThinkingSummary` 정책은 유지돼요. 로컬에서 숨긴 서명 텍스트를 Claude 클라이언트에 노출하지 않으며, 이 숨김 경계를 통한 무손실 재생은 아직 보장하지 않아요. 이전 결합 봉투는 스트리밍 텍스트가 이미 전송됐다면 원래 블록 순서를 복원할 수 없어요. `claudeCode.compatibility: "enforce"`는 여전히 thinking 재생을 거절해요. 실제 Anthropic 수락이나 캐시 적중 개선을 증명한 것은 아니며 [#3719](https://github.com/lidge-jun/opencodex/issues/3719)는 열어 둬요. + **오류 조건(400):** 잘못된 JSON, 누락되거나 빈 `model`, 누락되거나 빈 `messages`, 지원하지 않는 role, `tool_use_id` 없는 `tool_result`, id/name 없는 `tool_use`, name 없는 이름 지정 `tool_choice`예요. @@ -449,7 +459,8 @@ role, `tool_use_id` 없는 `tool_result`, id/name 없는 `tool_use`, name 없는 | `response.created` | `message_start` + `ping` | | Heartbeat | `ping` | | 텍스트 delta | `content_block_start` → `content_block_delta`(text) → `content_block_stop` | -| 추론 요약/텍스트 | 합성 signature가 있는 `thinking` 블록 | +| 추론 요약/텍스트 | 재생된 서명 또는 제한된 `ocxr1` 폴백이 있는 `thinking` 블록 | +| 비공개 추론 | 추론 봉투에서 재생되는 `redacted_thinking` 블록 | | Function-call 프레임 | `input_json_delta`가 있는 `tool_use` 블록 | | 종료 이벤트 | `message_delta` → `message_stop` | | 종료 전에 EOF | 502 형식 `api_error` | diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 44551de837..1f324adaf2 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -149,6 +149,13 @@ history를 업스트림 function tool로 인코딩한 다음 스트리밍된 fun `custom_tool_call`로 복원합니다. 네이티브 OpenAI forward routing과 지원되는 `apply_patch` custom tool은 변경되지 않습니다. +라우팅된 code-mode 턴에는 첫 호출 전에 중첩 helper에 대한 호스트 규칙도 전달됩니다. +`tools.apply_patch`는 별도 장식 없이 패치 마커만 있는 줄로 시작하고 끝나는 하나의 문자열을 받습니다. +isolate에서는 `import`를 사용할 수 없으며, 오래 실행되는 명령은 `write_stdin`으로 폴링합니다. +네이티브 라우팅 Responses, Kiro 또는 Cursor 경로의 code-mode exec 결과에 호스트의 실패 메시지 중 +하나가 여전히 포함되어 있으면, opencodex는 해당 규칙을 명시하는 한 줄짜리 힌트를 덧붙입니다. +이 변경은 모델의 코드나 패치 텍스트를 다시 작성하지 않습니다. + 선택한 provider는 function/tool calling을 지원해야 합니다. tool call을 지원하지 않는 text-only provider에서는 `exec`, Browser 또는 Computer Use를 사용할 수 없습니다. 네이티브 OpenAI 항목은 업스트림 tool mode를 그대로 유지합니다. diff --git a/docs-site/src/content/docs/ko/guides/model-ordering.md b/docs-site/src/content/docs/ko/guides/model-ordering.md index 365ea476fc..e921bfdfbc 100644 --- a/docs-site/src/content/docs/ko/guides/model-ordering.md +++ b/docs-site/src/content/docs/ko/guides/model-ordering.md @@ -109,7 +109,7 @@ account selector가 있으면 bare native 선택이 selector-qualified 그룹으 선두 모델 순서를 바꾸는 지원 수단은 `subagentModels`를 재정렬하는 것입니다. 대시보드의 **Sub-agents** 페이지에서는 bare native와 routed id의 순서를 바꿀 수 있습니다. 설정과 `ocx agent subagents set`은 exact account-qualified `/` id도 -지원하지만, 대시보드는 이러한 id를 제공하지 않으며 목록을 저장할 때도 보존하지 않습니다. 설정 id는 +지원합니다. 대시보드는 이미 저장된 id를 현재 사용할 수 없어도 보존합니다. 설정 id는 최대 5개만 사용하세요. account selector가 있으면 bare native 하나가 여러 selector-qualified 행으로 확장될 수 있으므로 설정 항목과 노출 행이 항상 일대일로 대응하지는 않습니다. @@ -153,3 +153,11 @@ V1에는 OpenCodex의 선호 후보 목록을 주입하지 않습니다. V2는 `disabledModels`와 각 공급자의 `selectedModels`는 노출 여부를 정하는 필드입니다. 별도의 `modelOrder`, `providerOrder`, priority map 설정은 없습니다. + +## 대시보드 정렬 프리셋 + +**Models**에서 기본값·모델 이름순·프로바이더별·사용량순 스냅샷을 선택한 뒤 **순서 적용**을 누르세요. 현재 표시 가능한 라우팅 ID와 `modelPickerOrderMode`(`alphabetical`, `provider`, `most-used`)를 저장합니다. 사용량순은 적용 시 보관된 전체 사용량을 한 번 읽습니다. 다시 열거나 모델이 추가·삭제되어도 자동 재계산하지 않습니다. 기존 사용자 지정·네이티브 전체 순서는 명시적으로 적용하기 전까지 유지됩니다. 기본값은 사용 가능한 모델이 없어도 두 피커 필드를 지웁니다. + +`GET/PUT /api/subagent-models`의 `chosen`·`available`은 비활성·누락된 저장 roster도 보존하고, `pickerAvailable`은 선택 가능한 라우팅 ID만 제공합니다. Models는 `pickerOrder`·`pickerOrderMode`만 보내며 `models`를 보내지 않습니다. Roster만 저장하면 피커 설정은 유지됩니다. 잘못된 요청이나 저장 실패는 이전 상태를 보존합니다. + +라우팅 전용 프리셋은 featured·네이티브 우선순위 구간을 유지하며 Codex 카탈로그와 Claude 검색 목록의 라우팅 그룹에 적용됩니다. Claude 네이티브 선두 그룹과 명시적 Desktop 프로필·alias 소유권은 유지됩니다. OpenCodex 가이드 순위와 fallback 설정은 유지되지만 네이티브 Codex에 표시되는 상위 5개·권장 기본 모델은 달라질 수 있습니다. 저장은 클라이언트를 재시작하지 않으며 카탈로그 갱신이 미완료이면 나중에 다시 열어야 할 수 있습니다. diff --git a/docs-site/src/content/docs/ko/guides/pi.md b/docs-site/src/content/docs/ko/guides/pi.md index 648d71060e..6bda9c2b36 100644 --- a/docs-site/src/content/docs/ko/guides/pi.md +++ b/docs-site/src/content/docs/ko/guides/pi.md @@ -27,6 +27,9 @@ ocx export --client pi "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -41,6 +44,8 @@ ocx export --client pi } ``` +생성된 Pi provider에는 `compat.sendSessionAffinityHeaders`가 활성화됩니다. provider를 병합하거나 직접 수정할 때 이 설정을 유지하세요. Pi가 안정적인 세션 식별자를 보내면 OpenCodex가 이를 바탕으로 정규 OpenCode Go 대상의 affinity를 계산합니다. `cacheRetention`이 `none`이면 Pi가 식별자를 보내지 않을 수 있습니다. + 모델 id는 프록시의 정규 선택자이므로, 라우팅된 모델은 `provider/model` (`anthropic/claude-opus-5`) 형태로 나타나고, 네이티브 OpenAI slug는 접두사 없이 (`gpt-5.6-sol`) 유지됩니다. `name` 접미사인 `(anthropic)`, `(native)`, `(routed)`는 diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 20eb9cf4b9..40fe351929 100644 --- a/docs-site/src/content/docs/ko/guides/providers.md +++ b/docs-site/src/content/docs/ko/guides/providers.md @@ -113,6 +113,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 실험적. GitHub 디바이스 플로우 + `copilot_internal` 교환(VS Code OAuth 클라이언트). 활성 Copilot 구독 필요; 공식 서드파티 API가 아닙니다. | +Google Antigravity 계정·제공자 할당량 확인은 모델 목록 폴백을 포함해 고정된 Google 회계 엔드포인트를 사용합니다. 해당 목적지의 투명 Fake-IP DNS를 지원하며 TLS 검증, 리다이렉트 거부, 사설 주소 검사는 유지합니다. 사용자 지정 base URL은 모델 요청에만 적용되며 할당량 목적지는 바꾸지 않습니다. `NO_PROXY`는 기존 직접 연결 정책을 유지합니다. + + Nous refresh가 종료 실패한 경우, `ocx login nous`로 재인증하세요. 정식 Kimi Coding Plan 프리셋(`kimi` 계정 로그인과 `kimi-code` API key)의 경우, opencodex는 @@ -213,6 +216,7 @@ Cline IDE/CLI에서만 제공되며 API로는 사용할 수 없습니다. `minim | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (정적 모델 목록)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token plan(기본): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 종량제: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 또는 사용자 지정 | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -327,7 +331,7 @@ provider 전체 parallel tool call이나 OpenAI `reasoning_effort`를 광고하 > 안내합니다. 일반 API 자동화, 사용자 애플리케이션 백엔드 및 비대화형 일괄 호출은 금지되며 > 플랜 키가 정지될 수 있습니다. -> **GLM 경로는 두 개입니다:** `zai`는 Z.AI 국제 코딩 플랜 구독이고, `zhipu-bigmodel`은 +> **GLM 과금 경로:** `zai`는 Z.AI 국제 코딩 플랜 구독이고, `zhipu-bigmodel`은 > Zhipu의 중국 내수 BigModel 종량제 엔드포인트입니다. 호스트도 키도 과금도 다르며, 한쪽에서 > 발급한 키는 다른 쪽에서 인증되지 않습니다. @@ -373,8 +377,8 @@ Amazon Bedrock 네이티브 API처럼 이 구현 중 어느 것과도 맞지 않 **구독 토큰**(일반 API 키가 아님)으로 인증합니다. **Cloudflare AI Gateway**는 URL에 계정 + 게이트웨이 id를 채워야 합니다. -Copilot은 혼합 wire 카탈로그를 제공합니다. GPT-5 계열 모델(`gpt-5.3-codex`, `gpt-5.4`, -`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`)은 에이전트 +Copilot은 혼합 wire 카탈로그를 제공합니다. 모델(`gpt-5.3-codex`, `gpt-5.4`, +`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)은 에이전트 트래픽에 대해 `/chat/completions`를 거부하므로 opencodex는 이 모델들을 내장 기본값으로 Responses API를 통해 라우팅하고, 다른 Copilot 모델은 모두 chat completions를 유지합니다. 우선순위는 하드 wire 핀 → 명시적 [`modelAdapters`](/ko/reference/configuration/providers/) diff --git a/docs-site/src/content/docs/ko/guides/remote-hub.md b/docs-site/src/content/docs/ko/guides/remote-hub.md index 6bd229aa3d..e924175672 100644 --- a/docs-site/src/content/docs/ko/guides/remote-hub.md +++ b/docs-site/src/content/docs/ko/guides/remote-hub.md @@ -86,16 +86,32 @@ ocx connect rotate --admin-token-stdin ## Docker -opencodex는 공식 컨테이너 이미지를 배포하지 않지만, 저장소 루트의 `Dockerfile`과 `compose.yaml`로 digest가 고정된 소스 이미지를 직접 빌드할 수 있습니다. 최초 정상 시작 시 자체 서명 TLS 인증서와 개인 키를 `ocx-state` 볼륨의 `/home/bun/.opencodex/container-tls/cert.pem`과 `/home/bun/.opencodex/container-tls/key.pem`에 생성합니다. 개인 키는 소유자만 읽을 수 있으며 이후 시작에서는 같은 인증서와 키를 검증한 뒤 다시 사용합니다. 데이터 엔드포인트는 HTTPS입니다. +롤백할 때도 두 볼륨과 마운트 경로를 유지하세요. 기존 볼륨의 소유권과 권한은 자동으로 복구되지 않습니다. Compose 없이 실행할 때의 named volume 지정과 별도 상태 경로는 [영문 기준 가이드](/guides/remote-hub/#docker-compose)를 참고하세요. -최초 정상 시작 전에 데이터 키를 stdin으로 한 번만 초기화하세요. bootstrap helper는 최대 512바이트인 한 줄만 허용합니다. 키를 출력하거나 기존 키를 덮어쓰지 않고 `ocx-state` 볼륨의 소유자 전용 `service-api-token`에 저장합니다. +상태는 두 볼륨에 분리해 보관합니다. `ocx-state`는 +`OPENCODEX_HOME=/home/bun/.opencodex`, `codex-state`는 +`CODEX_HOME=/home/bun/.codex`에 연결됩니다. 두 제품의 `auth.json` 형식이 다르므로 +홈을 같은 폴더로 합치지 마세요. 루트 파일 시스템이 read-only여도 이 두 홈은 쓰기 가능합니다. -호스트에 Git과 Bun이 필요합니다. 이미지를 빌드할 때마다 Git이 추적하는 소스로 정식 매니페스트를 생성하고, 생성부터 빌드 사이에는 소스를 변경하지 마세요. 생성된 JSON은 Git에 추가하지 않으며 `.git`은 Docker 컨텍스트에서 제외됩니다. 호스트 포트는 기본적으로 `127.0.0.1:10100`에 바인딩됩니다. `OPENCODEX_PORT`는 호스트 포트와 관리되는 TLS의 `publicOrigin`을 함께 변경하지만 컨테이너 내부 리스너는 `10100`을 유지합니다. +카탈로그는 자동 생성되지 않습니다. 인증된 `/v1/catalog` 검사 전에 유효한 +`/home/bun/.codex/opencodex-catalog.json`을 생성하거나 가져와야 합니다. +빈 홈에서 `catalog_not_found` 404는 정상입니다. 업그레이드는 기존 `ocx-state`를 +유지하고 `codex-state`를 추가하지만 파일을 자동 이동하지 않습니다. 이전 우회 설정으로 +`.opencodex`에 둔 카탈로그는 백업한 뒤 카탈로그만 owner-only 권한으로 옮기세요. +두 제품의 `auth.json`을 서로 덮어쓰면 안 됩니다. 사용자 지정 `CODEX_HOME`은 그 정확한 +디렉터리를 쓰기 가능한 볼륨에 연결하고, 기본 카탈로그를 +`${CODEX_HOME}/opencodex-catalog.json`에 준비해야 합니다. `model_catalog_json`으로 +별도 파일을 지정했다면 그 경로도 영속 보관하세요. 명시적 이전이 완료되기 전까지는 +기존 사용자 지정 환경 변수와 볼륨 경로의 대응을 유지하세요. -빌드는 오래된 매니페스트를 거부하며 모든 SHA-256을 컨텍스트와 복사된 파일에 각각 대조합니다. 매니페스트는 `Dockerfile`, `compose.yaml`, `.dockerignore`, Git이 추적하는 모든 Docker authority 파일, `src/`, `package.json`, `bun.lock`, `scripts/model-metadata.source.json`을 인증합니다. 누락되거나 일치하지 않는 파일, 매니페스트에 없는 추가 소스 또는 Docker authority 파일, 심볼릭 링크는 거부됩니다. +opencodex는 공식 컨테이너 이미지를 배포하지 않지만, 저장소 루트의 `Dockerfile`과 `compose.yaml`로 digest가 고정된 소스 이미지를 직접 빌드할 수 있습니다. 최초 실행 전에 데이터 키를 stdin으로 초기화하세요. 키는 출력되지 않으며 `ocx-state` 볼륨의 owner-only `service-api-token`에 저장됩니다. + +호스트에 Git과 Bun이 필요합니다. 이미지를 빌드할 때마다 Git이 추적하는 소스로 정식 매니페스트를 생성하고, 생성부터 빌드 사이에는 소스를 변경하지 마세요. 생성된 JSON은 Git에 추가하지 않으며 `.git`은 Docker 컨텍스트에서 제외됩니다. 호스트 포트는 기본적으로 `127.0.0.1`에 바인딩됩니다. 원격 공개는 `OPENCODEX_BIND_ADDRESS= docker compose up -d`로 명시적으로 선택하며, `0.0.0.0`은 모든 인터페이스에 공개합니다. 방화벽과 인증된 TLS/tailnet 프런트엔드로 보호하세요. + +빌드는 오래된 매니페스트를 거부하며 모든 SHA-256을 컨텍스트와 복사된 파일에 각각 대조합니다. 누락·불일치 파일, 매니페스트에 없는 추가 소스, 심볼릭 링크는 거부됩니다. `package.json`, `bun.lock`과 `scripts/`에서 유일하게 포함하는 `scripts/model-metadata.source.json`이 필수입니다. ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -103,37 +119,11 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -호스트에서 기본 게시를 확인하려면 공개 인증서만 복사해 로컬 CA로 사용하세요. 개인 키는 복사하지 마세요. - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -다른 호스트 포트를 사용한다면 이후 Compose 실행에도 같은 값을 지정하세요. - -```bash -OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz -``` - -원격 공개는 `OPENCODEX_BIND_ADDRESS=`로 명시적으로 선택하며, `0.0.0.0`은 모든 인터페이스에 공개합니다. 생성된 인증서는 `localhost`와 `127.0.0.1`만 포함합니다. 원격으로 직접 공개하려면 생성된 인증서와 키를 정확한 원격 이름에 맞는 인증서와 키로 교체하고, `OPENCODEX_PUBLIC_ORIGIN=https://hub.example.com:10100`처럼 경로, 자격 증명, 쿼리, 프래그먼트가 없는 정확한 HTTPS origin을 지정하세요. 방화벽과 인증된 TLS/tailnet 프런트엔드로 보호해야 합니다. - -TLS 도입 전에 만든 볼륨을 유지하고 있다면 다음 시작에서 볼륨별 TLS identity와 게시된 호스트 포트를 사용하는 HTTPS origin으로 자동 마이그레이션합니다. 운영자가 지정한 인증서 경로는 보존됩니다. 이전 HTTP 전용 이미지로 롤백하려면 현재 이미지를 사용할 수 있을 때 hub를 중지하고 TLS 설정만 제거한 뒤 이전 이미지를 시작하세요. 인증서 파일은 볼륨에 남아 있어도 됩니다. - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# 이전 이미지를 선택하거나 빌드한 다음 hub를 다시 생성 -docker compose up -d -``` - -이미지는 non-root `bun` 사용자로 실행되고 루트 파일 시스템은 read-only이며 공개 포트는 `10100` 하나뿐입니다. 토큰을 `ARG`, `ENV`, `COPY`, Compose YAML, 이미지 기록, 명령행에 넣지 마세요. Docker socket, 호스트 홈, Codex 홈, SSH agent, 프로바이더 키도 마운트하지 마세요. 컨테이너 안의 `127.0.0.1:10101` 관리 포트는 같은 네트워크 네임스페이스의 TLS/tailnet 프런트엔드로만 연결하고 직접 publish하지 마세요. +이미지는 non-root `bun` 사용자로 실행되고 루트 파일 시스템은 read-only이며 공개 포트는 `10100` 하나뿐입니다. 토큰을 `ARG`, `ENV`, `COPY`, Compose YAML, 이미지 기록, 명령행에 넣지 마세요. Docker socket, 호스트의 홈이나 Codex 홈, SSH agent, 프로바이더 키도 마운트하지 마세요. 컨테이너 안의 `127.0.0.1:10101` 관리 포트는 같은 네트워크 네임스페이스의 TLS/tailnet 프런트엔드로만 연결하고 직접 publish하지 마세요. -컨테이너 내부 health/readiness probe가 인증서 검증을 생략할 수 있는 범위는 고정된 컨테이너 루프백 연결뿐입니다. 외부 인수 검사에서는 복사한 공개 인증서나 시스템 신뢰 저장소를 사용해 실제로 접속하는 정확한 호스트 이름을 반드시 검증하세요. 컨테이너 healthcheck의 `/healthz`가 통과한 뒤 인증된 `/v1/catalog`와 실제 모델 응답도 별도로 확인해야 합니다. +컨테이너 healthcheck의 `/healthz`가 통과한 뒤 `/readyz`, 인증된 `/v1/catalog`, 실제 모델 응답을 별도로 확인하세요. -`docker compose down`은 `ocx-state` 볼륨을 보존합니다. `docker compose down --volumes`는 설정, OAuth 인증 정보, 사용량 기록, 데이터 키를 함께 삭제하므로 파괴적 작업으로 취급하세요. +`docker compose down`은 `ocx-state`와 `codex-state`를 모두 보존합니다. `docker compose down --volumes`는 두 볼륨을 모두 삭제하여 설정, OAuth 인증 정보, 사용량 기록, 데이터 키, 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 0900a924bd..048b5f3ca3 100644 --- a/docs-site/src/content/docs/ko/reference/cli/agents.md +++ b/docs-site/src/content/docs/ko/reference/cli/agents.md @@ -131,7 +131,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의 네이티브 형식으로 직렬화합니다. @@ -139,7 +139,7 @@ Grok Build model fence를 관리하고 적용합니다. | 플래그 | 동작 | | --- | --- | -| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | +| `--client ` | 필수입니다. 클라이언트 설정 형식을 선택합니다. | | `--json` | config JSON만 stdout에 출력하므로, redirect가 byte-exact 출력을 캡처합니다. `--out` write note를 포함한 모든 진단 메시지는 stderr로 갑니다. | | `--out ` | config를 ``에 씁니다. 기존 파일이 있으면 덮어쓰지 않습니다. | | `--force` | `--out`이 기존 파일을 덮어쓰도록 허용합니다. | @@ -166,6 +166,9 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `mcode` | `~/.minimax/config.yaml` (`MINIMAX_DATA_DIR`, 그다음 레거시 `MAVIS_DATA_DIR`가 설정되면 우선. 상대 경로는 거부됩니다) | `mcode-config.yaml` | 없음 — loopback placeholder | | `zcode` | `~/.zcode/v2/config.json` (`ZCODE_DATA_DIR`가 설정되면 우선. 상대 경로는 거부됩니다) | `config.json` | 없음 — loopback placeholder | | `prime` | `~/.prime/agent/models.json` (`PRIME_AGENT_CODING_AGENT_DIR`가 설정되면 우선. 상대 경로는 거부됩니다) | `prime-models.json` | 없음 — loopback placeholder | +| `raycast` | `~/.config/raycast/ai/providers.yaml` (macOS와 Windows 모두 동일. Raycast는 `XDG_CONFIG_HOME`을 따르지 않습니다) | `raycast-providers.yaml` | 없음 — loopback 전용. `api_keys` 항목은 쓰지 않습니다 | + +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는 거부됩니다. opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 보간합니다. opencodex가 생성한 Pi 블록에는 환경 변수가 필요 없으며, 리터럴 placeholder인 `opencodex-loopback`이 들어갑니다. 이 값은 필수입니다. Pi는 모델 목록을 만들 때 `apiKey`를 해석하고, 기존 config에 설정되지 않은 env 참조가 있으면 provider 전체를 숨기기 때문입니다. 루프백에서 proxy는 생성된 placeholder를 검사하지 않습니다. 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 7bbf5e2334..df15047266 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -82,6 +82,19 @@ dedicated-provider history도 포함됩니다. 상태를 백업하고 이 전체 ### `ocx status [--json]` +status와 `ocx doctor`는 현재 CLI와 실행 중인 프록시의 버전을 비교합니다. CLI가 더 새로우면 +원하는 최신 설치로 프록시를 재시작하십시오. 백그라운드 서비스라면 `ocx service repair`를 +실행합니다(`ocx service restart`는 별칭). 프록시가 더 새로우면 CLI를 업그레이드하거나 +`PATH`가 원하는 설치를 가리키도록 수정하십시오. 이 진단은 서비스를 복구하거나 요청 허용 +여부를 바꾸지 않습니다. + +버전 문자열이 같거나 어느 쪽이 `unknown` / `0.0.0`이면 경고하지 않으며, 프록시 버전이 없어도 +경고하지 않습니다. doctor는 placeholder를 버전 일치로 확정하지 않습니다. 엄격한 SemVer로 +해석할 수 없는 서로 다른 문자열이나 build metadata만 다른 버전은 어느 쪽이 오래됐다고 +단정하지 않는 중립 경고를 표시합니다. 공백을 제거하거나 앞의 `v`를 정규화하지 않습니다. +JSON의 `versionSkew`에도 같은 안내가 들어가며 필드는 `cliVersion`, `proxyVersion`, `skewed`, +`warning` 그대로입니다. + 읽기 전용 진단 요약을 출력합니다. 프록시 PID, `/healthz` 도달 가능 여부, 대시보드 URL, 설정 경로, 기본 공급자, Codex 자동 시작 설정, 서비스 상태, shim 상태, 그리고 마스킹된 실제로 적용되는 Codex 홈이 포함됩니다. 명시적이고 높은 신뢰도의 Windows Orca 런타임 홈 시그니처만 diff --git a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md index 710a955894..20d1e96256 100644 --- a/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ko/reference/cli/providers-accounts.md @@ -13,7 +13,7 @@ description: 제공자 설정, 자격 증명, 할당량, 모델 카탈로그 명 | 하위 명령 | 지원 플래그 | 동작 | | --- | --- | --- | -| `list` | `--json` | 설정된 제공자와 남아 있는 레지스트리 항목을 나열합니다. | +| `list` | `--json`, `--jsonl` | 설정된 제공자와 남아 있는 레지스트리 항목을 나열합니다. `--jsonl`은 설정된 제공자마다 JSON 객체를 한 줄씩 출력합니다. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | 레지스트리/사용자 지정 제공자를 추가합니다. `--force`는 덮어쓰고, `--sync`는 사람이 읽는 출력 모드에서 실행 중인 프록시를 새로 고칩니다. | | `edit ` | 제공자 필드 플래그, `--headers `, `--json` | 키 풀을 바꾸지 않고 검증된 실시간 제공자 필드를 수정합니다. `--headers`는 사용자 지정 요청 헤더를 병합하며, `{}` 또는 `-`로 지울 수 있습니다. | | `test ` | `--json` | 실제 상위 모델 엔드포인트를 확인합니다. | @@ -27,6 +27,7 @@ description: 제공자 설정, 자격 증명, 할당량, 모델 카탈로그 명 ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -35,6 +36,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl`은 설정된 제공자만 JSON 객체 하나당 한 줄로 출력합니다. 각 객체의 필드는 `--json`의 `configured` 배열 항목과 같으며, `registryCount` 요약은 포함하지 않습니다. 스크립트에서 각 줄의 객체를 순서대로 처리할 수 있습니다. `--json`과 `--jsonl`은 함께 사용할 수 없습니다. + :::caution[커스텀 헤더는 자격증명 통로가 아닙니다] `--headers`는 비밀이 아닌 요청 메타데이터용입니다 — 라우팅 힌트, 테넌트나 프로젝트 선택자, 추적 id 같은 것들이요. 인증 정보를 넣는 자리가 아니고, 검증기는 표준 자격증명 @@ -209,10 +212,11 @@ OAuth 및 API 키 제공자에는 제공자의 할당량 보고 엔드포인트 ### `ocx account auto-switch > [--json]` -`openai` Codex 계정 풀만 제어합니다. `on`은 80%, `off`는 0%를 설정하고, `status`는 현재 값을 읽으며, `threshold `은 0부터 100까지의 정수를 받습니다. 다른 제공자와 잘못된 값은 종료 코드 1로 끝납니다. `--json`은 다음을 반환합니다: +`openai` Codex 풀의 임계값을 제어하거나 일반 OAuth 풀의 임계값을 저장합니다. `on`은 80%, `off`는 0%, `threshold `은 0–100을 저장합니다. 일반 풀의 임계값은 현재 동작에 적용되지 않습니다. 저장해도 임계값 기반 전환이나 제공자 활성화 설정이 바뀌지 않고, 429 오류에 따른 회전도 비활성화되지 않습니다. 일반 풀의 조회와 변경 결과는 서버가 확인한 값을 사용합니다. 일반 풀의 `poolEnabled`는 저장된 제공자별 설정이며 `null`은 미지정입니다. 전역 설정을 상속한 실제 상태를 뜻하지 않습니다. `inert: true`이면 임계값이 적용되지 않으며, 기능 지원을 알 수 없을 때도 `enabled: true`로 표시하지 않습니다. API 키 제공자, Anthropic 및 잘못된 값은 거부합니다. ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 6e773f0730..04b332d159 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -100,7 +100,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `modelAutoCompactTokenLimits?` | `Record` | 모델별 양의 안전 정수형 소프트 자동 압축 예산입니다. 유효한 컨텍스트 또는 최대 입력의 90% 한도를 낮출 수만 있으며, 신뢰할 수 있는 컨텍스트 창을 알 수 없으면 내보내지 않습니다. canonical `openai`에서는 키가 공급자나 계정 선택자 접두사가 없는 정확한 지원 네이티브 모델 ID여야 합니다. 공급자 PATCH는 항목을 병합하며, 키를 `null`로 지정하면 해당 키를 삭제하고 필드 전체를 `null`로 지정하면 맵을 지웁니다. 이 `null` tombstone은 PATCH에서만 사용할 수 있습니다. | | `defaultMaxOutputTokens?` | `number` | 클라이언트가 `max_output_tokens`를 생략했을 때 쓰는 공급자 전반의 `openai-chat` 폴백입니다. | | `modelMaxOutputTokens?` | `Record` | 양수 모델별 `openai-chat` 폴백 예산입니다. 정확한 일치와 패턴 일치가 공급자 기본값보다 우선합니다. | -| `modelCosts?` | `Record` | 모델별 표시 가격(100만 토큰당 USD). 해당 공급자의 정확한 업스트림 모델 ID를 키로 사용하며(공급자 식별자나 라우팅된 `provider/model` 레이블이 아님) 값은 `input`, `output`, `cacheRead`, `cacheWrite` 네 필드입니다(예: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). 커스텀 공급자는 `openai-chat` 어댑터로 임의의 OpenAI 호환 엔드포인트를 대상으로 할 수 있으며, 내장 카탈로그에 없는 로컬·내부 공급자 ID도 유효합니다. 사용자 구성 가격은 Logs `~$` 및 Usage 추정에서 내장 카탈로그보다 우선합니다. 기존 항목도 현재 오버레이로 다시 계산되므로 가격을 편집하면 과거 합계가 바뀔 수 있습니다(폴백 순서: 사용자 설정 → jawcode 카탈로그 → expected-price 오버레이 → 모델별 벤더 가격). 전부 0인 항목은 다음 소스로 폴백합니다. 각 요율은 0 이상의 유한한 숫자이며 최대 1,000,000(100만 토큰당 USD)입니다. 범위를 벗어난 행은 관리 경계에서 거부되고 로드 시 삭제됩니다. 표시 전용 추정이며 라우팅·계정 선택·할당량·청구에는 영향을 주지 않습니다. | +| `modelCosts?` | `Record` | 모델별 표시 가격(100만 토큰당 USD). 해당 공급자의 정확한 업스트림 모델 ID를 키로 사용하며(공급자 식별자나 라우팅된 `provider/model` 레이블이 아님) 값은 `input`, `output`, `cacheRead`, `cacheWrite` 네 필드입니다(예: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). 커스텀 공급자는 `openai-chat` 어댑터로 임의의 OpenAI 호환 엔드포인트를 대상으로 할 수 있으며, 내장 카탈로그에 없는 로컬·내부 공급자 ID도 유효합니다. 사용자 구성 가격은 Logs `~$` 및 Usage 추정에서 내장 카탈로그보다 우선합니다. 기존 항목도 현재 오버레이로 다시 계산되므로 가격을 편집하면 과거 합계가 바뀔 수 있습니다(폴백 순서: 사용자 설정 → jawcode 카탈로그 → expected-price 오버레이 → 모델별 벤더 가격). 사용자가 모든 요율을 명시적으로 0으로 설정하면 비용을 0으로 추정합니다. 자동 가격으로 되돌리려면 해당 모델 항목을 삭제하세요. 카탈로그의 전부 0인 요율은 계속 다음 소스로 폴백합니다. 각 요율은 0 이상의 유한한 숫자이며 최대 1,000,000(100만 토큰당 USD)입니다. 범위를 벗어난 행은 관리 경계에서 거부되고 로드 시 삭제됩니다. 표시 전용 추정이며 라우팅·계정 선택·할당량·청구에는 영향을 주지 않습니다. | | `headers?` | `Record` | 추가 상위 헤더입니다. Authorization, cookies, API-key 헤더, 내장 개행, 잘못된 이름은 허용하지 않습니다. | | `openRouterRouting?` | `OpenRouterProviderRouting` | 기본 OpenRouter `order`, `only`, `allowFallbacks` 선호도입니다. 정식 OpenRouter와 `openai-chat`에서만 유효합니다. | | `modelOpenRouterRouting?` | `Record` | 공급자 전반의 OpenRouter 선호도를 덮어쓰는 정확한 모델 id별 재정의입니다. | @@ -112,7 +112,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `modelReasoningEfforts?` | `Record` | 모델별 레이블입니다. 빈 목록이면 effort 제어를 숨깁니다. | | `modelSupportsReasoningSummaries?` | `Record` | 모델을 `false`로 두면 summary 광고를 멈추고 summary 전달 필드를 제거합니다. | | `modelReasoningSummaryDelivery?` | `Record` | 모델별 Responses 전달 enum입니다. 기존 delivery 필드를 다시 씁니다. | -| `modelAdapters?` | `Record` | 혼합 와이어 게이트웨이를 위한 모델별 `openai-chat` 또는 `openai-responses` 와이어 재정의입니다. 명시적 항목이 레지스트리 기본값보다 우선합니다. DeepSeek 프리셋은 `deepseek-v4-flash`에 네이티브 Responses를 선택할 수 있고, GitHub Copilot은 GPT-5 계열(`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`)을 Responses 전용 기본값으로 선언합니다. 이 모델들은 에이전트 트래픽에서 `/chat/completions`를 거부하기 때문입니다. `gpt-5.4-nano`처럼 기본값이 없는 모델은 여기서 직접 옵트인할 수 있습니다. 단일 와이어 상위 항목과 정식 ChatGPT forward는 재정의를 거부합니다. | +| `modelAdapters?` | `Record` | 혼합 와이어 게이트웨이를 위한 모델별 `openai-chat` 또는 `openai-responses` 와이어 재정의입니다. 명시적 항목이 레지스트리 기본값보다 우선합니다. DeepSeek 프리셋은 `deepseek-v4-flash`에 네이티브 Responses를 선택할 수 있고, GitHub Copilot은 모델(`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)을 Responses 전용 기본값으로 선언합니다. 이 모델들은 에이전트 트래픽에서 `/chat/completions`를 거부하기 때문입니다. `gpt-5.4-nano`처럼 기본값이 없는 모델은 여기서 직접 옵트인할 수 있습니다. 단일 와이어 상위 항목과 정식 ChatGPT forward는 재정의를 거부합니다. | | xAI Responses 옵트인(대시보드) | 스위치 | `xai`에서만 `grok-4.5`와 `grok-4.6`의 `modelAdapters` 항목을 원자적으로 설정하거나 지웁니다. 한 항목만 있으면 다음 스위치 쓰기가 둘을 정규화할 때까지 혼합 상태로 표시됩니다. 다른 재정의와 티어 동작은 바뀌지 않습니다. | | `xaiResponsesXSearch?` | `boolean` | 기본적으로 비활성화됩니다. xAI Responses 대상에서는 최종 요청 정규화 후에도 실제 `web_search` 도구가 남아 있을 때만 공급자가 호스팅하는 `x_search` 선언을 추가합니다. 기존 선언은 중복하지 않고, 호출자의 `tool_choice`/`allowed_tools` 선택기 범위를 확장하지 않으며, 웹 검색 사이드카의 `search.xSearch` 옵션과는 별개입니다. | | `modelPreferHostedTools?` | `Record` | hosted tool namespace를 예약하는 non-forward Responses gateway용 정확한 모델 ID opt-in입니다. 현재 `["image_generation"]`만 허용하며, 일치하는 모델은 `openai-responses` wire를 사용하고 해당 hosted tool을 지원해야 합니다. 충돌하는 클라이언트 `image_gen` 선언을 제거하고 호출자의 tool choice를 유지하도록 selector도 다시 씁니다. OpenAI API 가상 `-pro` 모델은 선택한 공개 ID를 먼저 일치시키고, 해석된 기본 wire-model ID를 대체값으로 사용합니다. `modelAdapters`는 공개 ID를 먼저, 그 다음 기본 ID를 해석하며, 두 번째 결과가 최종 wire를 결정합니다. 설정하지 않은 모델은 일반 alias 동작을 유지합니다. | @@ -386,6 +386,14 @@ Vercel AI Gateway는 하나의 모델을 여러 기반 추론 공급자에 걸 표시 이름은 `modelDisplayNames`로 설정합니다. 우선순위는 운영자가 설정한 `modelDisplayNames`, 공급자 카탈로그 메타데이터, 일반 `provider/model` 표시 순서입니다. 키는 이 공급자 안의 정확한 네이티브 모델 id입니다. 예를 들어 `xai/grok-4.6`의 키는 `grok-4.6`입니다. 이름은 표시 전용이며 정확한 라우팅 id나 업스트림 모델 id를 바꾸지 않습니다. `config.json`의 기존 공급자 설정에 이 필드만 추가하고 다른 모든 필드는 유지하세요. `PUT /api/providers/:provider/model-display-names`에 `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }`를 보내 저장하고, `displayName: null`을 보내 해당 이름만 초기화합니다. +로컬 Codex 카탈로그에서 지원되는 접두사 없는 네이티브 GPT 항목에도 +`providers.openai.modelDisplayNames`로 정확한 표시 이름을 지정할 수 있습니다. 예를 들어 `"gpt-6-astra": "GPT 6 Astra"`를 사용합니다. +시작 시 동기화와 로컬 카탈로그 수렴은 모두 이 이름을 다시 적용합니다. 이름 설정을 삭제하면 항목의 현재 표시 이름이 +적용된 재정의와 여전히 일치할 때만 원래 네이티브 이름을 복원합니다. 외부에서 변경된 표시 이름도 기존 네이티브 메타데이터 정규화 규칙을 따릅니다. +예를 들어 Astra (`gpt-6-astra`)는 고정된 네이티브 이름과 다른 이름을 여전히 그 고정 이름으로 교체합니다. +표시 이름 재정의는 모델 ID, 기능을 포함한 메타데이터, 정렬 순서, 라우팅된 콤보 별칭 및 계정 선택자가 붙은 항목을 바꾸지 않습니다. +이 로컬 카탈로그 재정의는 HTTP 모델 목록이나 가상 `*-pro` 항목의 이름을 바꾸지 않습니다. + 프리뷰 GPT-5.6 폴백 항목도 같은 메커니즘을 사용합니다. OpenAI API 키 프리셋은 base와 Pro id에 컨텍스트 `922000`, 최대 입력 `922000`을 채웁니다. OpenRouter는 `openai/gpt-5.6-sol`, `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`에 컨텍스트 `922000`을 채웁니다. Pool/Direct는 `922000`을 노출하고, 동기화된 카탈로그는 `xhigh`를 구분한 채 `max`를 노출합니다. ```json @@ -402,6 +410,20 @@ Vercel AI Gateway는 하나의 모델을 여러 기반 추론 공급자에 걸 } ``` +## 모델 표시 이름 편집기 + +대시보드의 **Models**에서 발견된 모델의 읽기 쉬운 이름을 저장해 유지할 수 있습니다. 공급자를 펼치고 발견된 모델을 +찾아 **Name**을 선택하세요. 읽기 쉬운 이름을 저장하는 동안에도 대화 상자는 정확한 `provider/model` +선택자를 표시합니다. **Reset name**을 선택하면 공급자 메타데이터 또는 기본 선택자 표시로 돌아갑니다. +**Name**은 표시만 바꿉니다. 별도의 별칭 연필 아이콘은 짧은 라우팅 별칭을 바꾸며, 표시 이름 편집기가 +아닙니다. 네이티브 OpenAI와 사용자 지정 모델 행은 기존 조작 방식을 유지합니다. + +변경은 저장됐지만 새로고침에 실패하면 대화 상자는 저장된 재정의를 반영하고 **Retry**를 계속 제공합니다. +서버가 카탈로그 수렴 실패를 보고했다면 Retry는 수렴을 다시 실행하고, 목록 요청만 실패했다면 목록을 +다시 불러옵니다. 초기화 후 복구는 초기화 작업을 유지하며 이전 이름을 복원하지 않습니다. 요청에는 +쓰기와 후속 목록 새로고침을 모두 포함하는 60초 제한이 있습니다. 시간 초과가 쓰기를 취소하지는 않습니다. +다른 변경을 하기 전에 **Retry**로 현재 이름을 확인하세요. + ## 전체 예시 ```json diff --git a/docs-site/src/content/docs/ko/reference/proxy-formats.md b/docs-site/src/content/docs/ko/reference/proxy-formats.md index d6d9f4001d..564357acec 100644 --- a/docs-site/src/content/docs/ko/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ko/reference/proxy-formats.md @@ -185,6 +185,11 @@ SSE 객체, choice delta, `finish_reason`이 있는 종료 choice, `data: [DONE] 이 엔드포인트는 Claude Code와 호환 클라이언트가 사용하는 Anthropic Messages 방언을 말합니다. 대부분의 요청은 Responses로 변환되어 일반적으로 라우팅된 뒤, Anthropic JSON 또는 Anthropic SSE로 다시 변환됩니다. +변환되는 Messages 요청의 reasoning 재전송은 요청 전체의 번역 예산을 공유합니다. 이 예산에는 +인코딩·디코딩 과정에서 생기는 복사본도 포함됩니다. 한도를 초과하면 `translation_buffer_limit`과 +HTTP 413을 반환하며, 한도에 맞추려고 서명이나 불투명 reasoning 데이터를 자르지 않습니다. +네이티브 Anthropic passthrough에는 별도의 본문 크기 제한이 적용됩니다. + 네이티브 Anthropic passthrough는 다음이 모두 참일 때만 적용됩니다. - Claude Code 설정에서 native passthrough가 비활성화되어 있지 않습니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 2f4d41a4e0..4853466c56 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -195,6 +195,10 @@ header and does not guarantee a provider cache hit. **Auth:** `key` (`x-api-key` by default, or `Authorization: Bearer` with `apiKeyTransport: "bearer"`) or `oauth` (Bearer + `anthropic-beta`, for Claude Pro/Max). - Converts messages to Anthropic content blocks (text, base64 image, `tool_use`, `thinking`). +- Translated Anthropic Messages reasoning replay shares the request translation budget, including + encoding/decoding copy overhead. Requests exceeding it return HTTP 413 with + `translation_buffer_limit`; signatures and opaque reasoning data are never truncated to fit. + Native Anthropic passthrough uses its separate body-size contract. - **Extended thinking math:** Anthropic requires `max_tokens > thinking.budget_tokens`. The adapter maps reasoning effort to a budget (minimal 1024 … max 32000), then computes a safe `max_tokens` with output headroom, and **drops `temperature`/`top_p`** when thinking is enabled (Anthropic forbids diff --git a/docs-site/src/content/docs/reference/architecture.md b/docs-site/src/content/docs/reference/architecture.md index 96a3aec6dc..e0fbc8bcba 100644 --- a/docs-site/src/content/docs/reference/architecture.md +++ b/docs-site/src/content/docs/reference/architecture.md @@ -200,3 +200,50 @@ The internal model lives in `types.ts`: `OcxParsedRequest`, `OcxContext`, the `O `OcxContentPart` (text / image), `OcxToolCall`, `OcxTool`, `AdapterEvent`, and the config types (`OcxConfig`, `OcxProviderConfig`). Two helpers are widely used: `namespacedToolName()` and `modelInList()` (tolerant `:size`-tag matching for `noVisionModels` / `noReasoningModels`). + + +### Incomplete quota terminals + +A native forward response that ends with quota or rate-limit evidence in an +`incomplete` terminal records account quota failure and spawn-fallback health. +Structured `incomplete_details.reason` and error codes are accepted without a +message; ordinary output-limit, filtering, steering and stall incompletes do not +cool an account. Cyber-policy classification retains precedence. The terminal is +not replayed after output, and fixed-account request selection remains fixed. + +Remote compact requests can buffer their response for longer than the server's +request-idle timeout. That listener timeout is disabled after the request body is +accepted; client cancellation and upstream operation deadlines still apply. + +Buffered routed compaction treats nonempty text and reasoning deltas as progress +without exposing partial summary text. Comments, empty deltas and gateway +keepalives do not reset the adapter-event stall watchdog. The default stall +timeout stays 300 seconds; encrypted compaction content is preserved unchanged. + +Native compact response buffering also enforces a body-byte inactivity deadline +using `stallTimeoutSec` (300 seconds by default). Nonempty chunks reset that +deadline; a stalled body returns HTTP 504, client cancellation retains HTTP 499, +and cleanup does not wait for a stuck upstream cancellation promise. The 32 MiB +response ceiling and the original body bytes are preserved. + +A canonical upstream WebSocket refused-create error can become an HTTP 4xx only +before the response is committed and after stream correlation checks. Permitted +quota headers are bounded and rebuilt without upstream framing headers; the JSON +response is not cacheable. Post-commit and 5xx errors keep the no-resend path. + +When encrypted agent-task recovery refuses a routed task, its existing 400 error +can include a bounded `recovery_reason`: `unsupported_envelope`, +`admission_denied`, `recovery_unavailable`, `caller_cancelled`, `input_changed`, +`recovery_http_rejected`, `recovery_timeout`, `recovery_aborted`, +`recovery_transport_error`, or `recovery_invalid_output`. +HTTP rejection requires an observed non-success response. Invalid output includes +invalid UTF-8, oversized bodies, malformed or incomplete recovery streams, and +invalid or conflicting assignments. A caller's cancellation takes precedence over +an owned deadline, which takes precedence over decode/transport failures. +`recovery_aborted` describes a shared recovery cancelled independently of that caller. +Shared-flight waiters receive the same underlying failure unless individually cancelled; +only successful plaintext is cached. Diagnostics contain no upstream error or payload text. +The field is omitted when no classified recovery result exists, and existing combo +branches that return the original target failure keep that response. +`recovery_unavailable` includes cache/singleflight capacity and does not prove an +upstream request was attempted. No retry or broader envelope acceptance is enabled. diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 4801187215..3af72db1d4 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -111,14 +111,21 @@ Inspect proxy requests, usage, storage, memory, and debug data. The direct alias | Alias | Equivalent resource | | --- | --- | | `ocx logs [filters] [--follow] [--json|--jsonl]` | `ocx observe logs` | -| `ocx usage [--range ] [--surface ] [--provider ] [--model ] [--json]` | `ocx observe usage` | +| `ocx usage [--range ] [--since --until ] [--surface ] [--provider ] [--model ] [--json]` | `ocx observe usage` | | `ocx storage [--json]` | `ocx observe storage` | | `ocx memory [--json]` | `ocx observe memory` | ```bash ocx observe usage --range 30d --json +ocx usage --since 2026-09-01T09:00:00Z --until 2026-09-01T10:59:59.999Z --json ``` +`--since` and `--until` must be supplied together. They accept integer epoch milliseconds or +full ISO datetimes with an explicit timezone, include both endpoints, and override `--range`. +Invalid or reversed bounds fail before the request. Human output prints the requested interval; +`--json` includes `customWindow`, `since`, and `until`. Existing surface/provider/model filters +still apply. These commands query the running proxy; they do not provide offline reports. + `--range today` (alias `1d`) reports the current local day. `--provider` and `--model` narrow the report to one upstream target — distinct from `--surface`, which selects the calling client (Codex, Claude Code, Grok) @@ -296,7 +303,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 @@ -307,7 +314,7 @@ models Codex can currently see. | 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. | @@ -337,6 +344,7 @@ client applies its own defaults for those). | `zcode` | `~/.zcode/v2/config.json` (`ZCODE_DATA_DIR` wins when set; a relative value is refused) | `config.json` | none — loopback placeholder | | `prime` | `~/.prime/agent/models.json` (`PRIME_AGENT_CODING_AGENT_DIR` wins when set; a relative value is refused) | `prime-models.json` | none — loopback placeholder | | `aside` | `~/.aside/u//models.json` for the account Aside's own `accounts.json` names as current; an unreadable manifest is refused rather than defaulting to an account | `aside-models.json` | none — loopback placeholder | +| `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 | The managed DSH export requires DSH 0.1.0-rc.6 or newer and owns only `llm-pi-ai.providers.opencodex`. DSH hot reloads that provider; the user's default model and @@ -349,6 +357,15 @@ hide the whole provider when an existing config contains an unset env reference. checks the generated placeholder on loopback. OMP supports provider-level headers, but this initial integration deliberately remains loopback-only; remote `x-opencodex-api-key` wiring is deferred. +The Raycast export is a standalone `providers.yaml` document with one `id: opencodex` element +in the `providers` sequence: `name: OpenCodex`, the proxy's `/v1` base URL, and every routed model +with its `abilities` (`tools` and `system_message` always supported, `vision` from the catalog's +input modalities, `reasoning_effort` when the model has an effort ladder, `temperature` off for +reasoning models). Custom Providers is a Raycast Pro feature, and Raycast watches the file, so a +saved change takes effect without a restart. The format is documented at +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers). No +`api_keys` entry is written, so this export is loopback-only and a non-loopback bind is refused. + The MCode, ZCode and Prime exports are loopback-only for the same reason and likewise carry the `opencodex-loopback` placeholder rather than a real credential. Prime Agent reads the same `models.json` contract Pi does, so the two exports produce the same document; only the destination diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 18539657a7..8235b63ead 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -88,6 +88,19 @@ are left in place. ### `ocx status [--json]` +Status and `ocx doctor` compare this CLI's version with the running proxy. If the CLI is newer, +restart the proxy using the intended current installation; for a background service, run +`ocx service repair` (`ocx service restart` is an alias). If the proxy is newer, upgrade the CLI +or resolve `PATH` to the intended installation. These diagnostics do not repair the service or +change whether requests are allowed. + +Identical version strings and the `unknown` / `0.0.0` placeholders suppress the warning, as does +an absent proxy version. Doctor does not report placeholders as a confirmed match. Different +strings still produce a neutral warning when they cannot be strictly parsed as SemVer or differ +only in build metadata; neither side is called older. Versions are not trimmed and a leading `v` +is not normalized. JSON exposes the same advice in `versionSkew`, whose fields remain +`cliVersion`, `proxyVersion`, `skewed`, and `warning`. + Print a read-only diagnostic summary: proxy PID, `/healthz` reachability, dashboard URL, config path, default provider, Codex autostart setting, service state, shim state, and the redacted effective Codex home. Only the explicit, high-confidence Windows Orca runtime-home signature adds an actionable App-home @@ -261,9 +274,10 @@ bundled Bun paths are deliberately rediscovered after upgrades instead of being Definitions installed before this change still carry the old versioned paths and cannot migrate themselves — once the old executable is deleted, no opencodex code runs to fix it. Run `ocx service repair` once after upgrading; after that, each service start follows the launcher. -An already-running proxy is not replaced by an external upgrade: restart the service (or run -`ocx service repair`) so the new build serves, and treat a CLI/proxy version mismatch warning as -exactly that signal. +An already-running proxy is not replaced by an external upgrade: when the installed CLI is newer +than the running proxy, restart the service (or run `ocx service repair`) so the new build serves. +If the proxy is newer instead, check the CLI installation and `PATH` as described under +[`ocx status`](#ocx-status---json). | Subcommand | Action | | --- | --- | 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 903e2bd655..223a825dc8 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -14,7 +14,7 @@ both `--adapter` and `--base-url`. | Subcommand | Supported flags | Action | | --- | --- | --- | -| `list` | `--json` | List configured providers and the remaining registry entries. | +| `list` | `--json`, `--jsonl` | List configured providers and the remaining registry entries; `--jsonl` emits one configured provider object per line. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Add a registry/custom provider. `--force` overwrites; `--sync` refreshes a running proxy in human-output mode. | | `edit ` | provider field flags, `--headers `, `--json` | Edit validated live provider fields without replacing key pools. `--headers` merges custom request headers; pass `{}` or `-` to clear them. | | `test ` | `--json` | Probe the real upstream model endpoint. | @@ -29,6 +29,7 @@ both `--adapter` and `--base-url`. ```bash ocx provider list --json +ocx provider list --jsonl # one configured provider object per line ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -37,6 +38,11 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` writes only configured providers, one JSON object per line, and omits the +`registryCount` summary from `--json`. Each object has the same fields as an item in the `configured` array. +Use it for scripts that process one configured provider object per line. +`--json` and `--jsonl` cannot be combined. + :::caution[Custom headers are not a credential channel] `--headers` is for non-secret request metadata — routing hints, tenant or project selectors, tracing ids. It is **not** a place to put authentication @@ -82,8 +88,9 @@ files or a raw network capture. ### `ocx login ` -Start the provider's registered login flow. OAuth providers open a browser and store auto-refreshed -credentials under `~/.opencodex/`; API-key login providers open their key dashboard, prompt for the +Start the provider's registered login flow. OAuth-style account providers open a browser and store +credentials under `~/.opencodex/` (refreshable tokens rotate automatically; durable key grants such +as OrcaRouter are reused until the provider revokes them); API-key login providers open their key dashboard, prompt for the key, validate it when possible, and save the resulting provider config. The command prints the currently accepted OAuth and API-key provider ids when the name is missing or unknown. @@ -95,6 +102,8 @@ account pool (Reauthenticate) or the headless `ocx account reauth` flow instead. ```bash ocx login xai ocx login anthropic +ocx login orcarouter-oauth # browser consent + S256 PKCE +ocx login orcarouter # paste an existing API key ``` OAuth reauthentication preserves operator settings such as model selections, pricing overrides, @@ -331,12 +340,11 @@ instead (exit 0), matching the dashboard's quota bars. ### `ocx account auto-switch > [--json]` -Controls only the `openai` Codex account pool. `on` sets 80%, `off` sets 0%, `status` reads the current -value, and `threshold ` accepts an integer from 0 through 100. Other providers and invalid values -exit 1. `--json` returns: +Controls the `openai` Codex pool threshold, or stores a threshold for a generic OAuth pool. `on` stores 80%, `off` stores 0%, and `threshold ` accepts 0–100. Generic pool thresholds are currently inert: saving one does not enable threshold-based switching, change the provider enablement override, or disable reactive 429 rotation. `status` and mutation output for generic pools use the confirmed server response. For generic pools, `poolEnabled` is the stored provider override (`null` means unspecified), not inherited effective state; `inert: true` means the threshold is not applied, and unknown capability never reports `enabled: true`. API-key providers, Anthropic and invalid values are rejected. ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` @@ -503,6 +511,8 @@ proxy to be running (`ocx start`, or an installed service). | --- | --- | --- | | `list` (default) | `--provider `, `--json` | List models seeded in configured providers. | | `live` | `--provider `, `--json` | Read the running catalog, including models discovered at runtime. Rows are flagged `native`/`routed`, `custom`, and `enabled`/`disabled`. | +| `price ` | `--json` | Read the model's saved manual price override; no override means automatic pricing. | +| `set-price ` | `--input `, `--output `, `--cache-read `, `--cache-write `, `--auto`, `--json` | Set display prices in USD per 1M tokens. Input/output are required when setting; omitted cache rates become zero. `--auto` removes only this model's override. | | `add ` | `--display-name `, `--context-window `, `--modalities ` | Register a model the provider catalog does not advertise. | | `edit ` | `--model-id `, `--display-name `, `--context-window `, `--modalities `, `--json` | Edit a custom model. `-` clears a field; `0` clears the context window. | | `remove ` | `--yes` | Delete a custom model. Requires `--yes` when stdin is not an interactive terminal. | diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index a75061a763..1296e0ced1 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -299,3 +299,29 @@ apply. `max` and `ultra` are accepted, while the dashboard offers `low` through For a beginner-oriented explanation of v1, default, and v2 behavior, see [Sub-agent surfaces](/guides/sub-agent-surface/). + +## Global model effort pins + +The optional root `modelPinnedEfforts` map fills or overrides incoming effort choices when +neither a provider model pin nor a provider-wide pin is configured. For example: + +```json +{ + "modelPinnedEfforts": { + "example-provider/example-model": "high" + } +} +``` + +Lookup checks the final selector before provider-prefix normalization, then the qualified +`provider/model` destination, then its bare upstream model ID. Original combo aliases and +synthetic effort-row selector IDs are not global pin keys; configure the concrete destination. +Synthetic-row effort and combo defaults are preserved as the effective input before pinning. +Each selected destination resolves its own pin, then applicable caps and wire normalization. +Compaction requests are exempt. `none` means effort omission and provider-default behavior, +not guaranteed reasoning disablement. + +`GET /api/effort-caps` includes the map. `PUT /api/effort-caps` accepts `modelPinnedEfforts` +alongside the existing caps: omitted fields stay unchanged, `null` clears the map, and a map +entry set to `null` or `""` deletes only that key. Invalid combined updates leave both caps +and pins unchanged. Saving a pin does not alter the featured subagent roster. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 9a773568eb..a6ecac02ae 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -122,10 +122,7 @@ predictions. Explicit provider/model price overrides still take precedence. | --- | --- | --- | | `adapter` | `string` | One of `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (or alias `azure`). | | `baseUrl` | `string` | Upstream API base URL. Most built-in fixed endpoints ignore a mismatch; collision-safe key presets preserve an older same-named custom destination. | -| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, jitterMs?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval; `jitterMs` adds only a positive random delay (0–60,000 ms). Provider limits apply across all models, while `models` entries use exact upstream model IDs and can only add delay. Queue waits do not consume the upstream response-header timeout. HTTP, Responses WebSocket, and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. | -| `tlsProfile?` | `"antigravity-browser"` | Explicit, experimental Antigravity-only TLS/HTTP2 compatibility profile. It requires Google OAuth Cloud Code Assist and canonical Antigravity hosts. It is unofficial, does not ensure Terms-of-Service compliance or prevent suspension, may make traffic more distinctive, and falls back to Bun if initialization fails. OAuth/token/onboarding traffic remains on standard Bun TLS. Prefer official Gemini API-key, Vertex, or documented Code Assist routes when policy safety matters. | -| `wsUpstream?` | `boolean` | Canonical ChatGPT Responses streaming uses HTTP/SSE by default. Set `true` to opt into the upstream WebSocket transport; `false` disables it. An explicitly set provider value takes precedence. When omitted, `OCX_CODEX_WS_UPSTREAM=true` or `1` enables it; `false`/`0`, absent, or invalid values use HTTP/SSE. | -| `maxWsFrameBytes?` | `number` | Maximum `response.create` request frame size before falling back to HTTP/SSE while the upstream WebSocket transport is enabled. Defaults to 16,711,680 bytes (16 MiB minus 64 KiB); invalid or non-positive values use that default. Ignored when WebSocket transport is disabled. | +| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. Provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay. Queue waits do not consume the upstream response-header timeout. HTTP, Responses WebSocket, and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. | | `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. | | `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. | | `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. | @@ -152,7 +149,7 @@ predictions. Explicit provider/model price overrides still take precedence. | `modelAutoCompactTokenLimits?` | `Record` | Positive safe-integer per-model soft auto-compaction budgets. Values can only lower the effective 90%-of-context/max-input envelope and are omitted when no authoritative context window is known. For canonical `openai`, keys must be exact supported native model IDs without provider or account-selector prefixes. Provider PATCH merges entries; set a key to `null` to delete it or the whole field to `null` to clear the map. These `null` tombstones are PATCH-only. | | `defaultMaxOutputTokens?` | `number` | Provider-wide `openai-chat` fallback when the client omits `max_output_tokens`. | | `modelMaxOutputTokens?` | `Record` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. | -| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an all-zero entry falls through to the next source in that sequence. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. | +| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an explicit all-zero user entry means a known-zero estimate; delete that model entry to restore automatic pricing. All-zero catalog metadata still falls through. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. | | `headers?` | `Record` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Default OpenRouter `order`, `only`, and `allowFallbacks` preferences; valid only for canonical OpenRouter with `openai-chat`. | | `modelOpenRouterRouting?` | `Record` | Exact model-id overrides that replace the provider-wide OpenRouter preference. | @@ -165,7 +162,7 @@ predictions. Explicit provider/model price overrides still take precedence. | `modelReasoningEfforts?` | `Record` | Per-model labels. An empty list hides effort control. As with `reasoningEfforts`, each configured `google`-adapter ladder asserts `thinkingLevel` capability; direct and Vertex non-image requests use the flat Gemini path, while Cloud Code Assist sends it under its request envelope. | | `modelSupportsReasoningSummaries?` | `Record` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. | | `modelReasoningSummaryDelivery?` | `Record` | Per-model Responses delivery enum; rewrites an existing delivery field. | -| `modelAdapters?` | `Record` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | +| `modelAdapters?` | `Record` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults. The OpenCode Go preset selects Responses for `gpt-5.6-luna` while leaving sibling models on their documented wires; DeepSeek can select native Responses for `deepseek-v4-flash`; and GitHub Copilot declares Responses-only defaults for the following models (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. | | xAI Chat Completions (dashboard / CLI) | switch | Grok 4.5/4.6 OAuth Responses requests default to Responses. Existing Chat overrides are migrated once on upgrade; later Chat choices are preserved. Turn on to select Chat for both models, off to select Responses. CLI: `ocx provider edit xai --xai-chat on` or `--xai-chat off` (running proxy required). Mixed means only one model currently uses Chat. Other overrides and tier policy stay unchanged. API-key and translated Chat/Anthropic defaults are unchanged. | | `xaiResponsesXSearch?` | `boolean` | Disabled by default. On an xAI Responses destination, append the provider-hosted `x_search` declaration only when a live `web_search` tool survives final request normalization. Existing declarations are not duplicated, caller `tool_choice`/`allowed_tools` selectors are never widened, and this is separate from the web-search sidecar's `search.xSearch` options. | | `modelPreferHostedTools?` | `Record` | Exact-model opt-in for non-forward Responses gateways that reserve a hosted-tool namespace. Currently accepts only `["image_generation"]`; a matching model must use the `openai-responses` wire and support that hosted tool. It removes colliding client `image_gen` declarations and rewrites their selectors to preserve caller tool choice. For OpenAI API virtual `-pro` models, the selected public ID is matched first and the resolved base wire-model ID is a fallback. `modelAdapters` resolves the public ID first, then the base ID; the second resolution determines the final wire. Other models retain normal alias behavior. | @@ -184,7 +181,7 @@ predictions. Explicit provider/model price overrides still take precedence. | `responsesItemIdRepair?` | `{ message?: string[]; reasoning?: string[]; repairMissingTerminalIds?: boolean; repairInvalidIds?: boolean }` | Disabled-by-default downstream SSE repair for exact placeholder ids, missing terminal ids, and (with `repairInvalidIds`) message/reasoning ids missing the canonical `msg_`/`rs_` prefix. Function-call ids are never rewritten. Built-in DeepSeek enables the last two by default. | | `responsesSnapshotRepair?` | `boolean` | Disabled-by-default client-facing repair for sparse Responses lifecycle snapshots in SSE and JSON. Fills missing canonical status, output, and tool metadata while raw inspection and persistence remain unchanged. | | `retryOn429?` | `{ enabled?: boolean; attempts?: number; intervalMs?: number; maxIntervalMs?: number; respectRetryAfter?: boolean }` | API-key providers only (`authMode: "key"`). Opt-in same-target 429 retry: when `retryOn429` is absent the feature is off; object presence enables it unless `enabled: false`. On 429 the proxy waits (upstream `Retry-After` or the fixed interval) and replays the identical request on the same key before any key failover — across the main text-turn recovery loop, the Responses passthrough wire, the image/video bridge, the web-search sidecar, and terminal continuations. Only pre-stream HTTP 429 responses are eligible for replay; custom `runTurn` transports are outside the HTTP retry loop. `attempts` counts same-key replays after the first 429 (total sends = `attempts` + 1) and is one request-wide budget shared by the main recovery loop, the terminal-guard continuation, and bridge retries. Exhausting `attempts` only stops further same-key replays: normal key failover or final-error handling then applies per the available targets — on the key-auth passthrough wire there is no failover, so the exhausted 429 surfaces as-is. Codex itself never retries 429, so this is the only defense for single-key providers. Defaults: `enabled: true`, `attempts: 3`, `intervalMs: 5000`, `maxIntervalMs: 60000` (any single wait is capped at `maxIntervalMs`, itself capped at 600000), `respectRetryAfter: true`. | -| `replayTransientFailures?` | `boolean` | `false` | Opt in to replaying pre-stream transient upstream failures (HTTP 500/502/503/504 and connection resets) on the same target. The bounded retry budget is shared by the request's recovery paths; omitted or `false` preserves fail-fast behavior for transient HTTP responses. | +| `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Key-auth `openai-chat` providers only. Opt-in retry for pre-stream transient upstream statuses (500, 502, 503, 504, 520, 521, 522): absent means off, object presence enables it unless `enabled: false`. Covers the initial Responses request, the terminal-guard continuation, and native `/v1/chat/completions`. `attempts` is the TOTAL number of upstream sends allowed for one request including the first (1..10, default 3) — it is one budget shared with connection-reset recovery, so `3` means at most three real requests reach the provider. Waits use a fixed 400 ms exponential backoff capped at 5 s and honor `Retry-After`. Separate from `retryOn429`, which handles rate limiting; mid-stream failures are never replayed. | | `autoToolChoiceOnlyModels?` | `string[]` | Models whose `tool_choice` accepts only `auto` or `none`; forced choices are downgraded. | | `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. | | `reasoningDetailsModels?` | `string[]` | Models whose endpoint returns thinking as a structured `reasoning_details` array (MiniMax M-series with `reasoning_split`); stream deltas are cumulative snapshots that are prefix-diffed, and preserved reasoning replays as a `reasoning_details` array instead of a `reasoning_content` string. | @@ -204,6 +201,43 @@ predictions. Explicit provider/model price overrides still take precedence. | `unsafeAllowNativeLocalExec?` | `boolean` | Cursor legacy boolean, equivalent to `nativeLocalExec: "on"` only when the newer field is unset. | | `nativeLocalExec?` | `"off" \| "codex-sandbox" \| "on"` | Cursor local-exec policy. `off` is default; `codex-sandbox` currently fails closed like `off`. | +Custom-model `reasoningEfforts` normally override discovered provider metadata. The bounded +exception is an explicit Astra or Daybreak custom row on the canonical `openai` Codex-forward +destination: its advertised list is intersected with that model's pinned native capabilities. +An explicit empty list remains empty with no default; a nonempty incompatible list falls back +to the native default as a single choice. Defaults must belong to the final list. This changes +the catalog projection, not stored configuration or arbitrary gateway models sharing a GPT name. +See [custom native catalog examples](/guides/codex-app-models/). + +### Operator-pinned reasoning effort + +Set `pinnedReasoningEffort` on an existing provider to override incoming effort choices, or +use `modelPinnedReasoningEfforts` for individual upstream model IDs. Per-model provider pins +win over the provider-wide pin; the root `modelPinnedEfforts` map is the fallback. These are +operator settings, not provider-registry defaults. They do not change model discovery or the +advertised effort ladder. + +```json +{ + "pinnedReasoningEffort": "high", + "modelPinnedReasoningEfforts": { + "example-model": "max" + } +} +``` + +Merge these fields into the existing provider row. Accepted values are `none`, `minimal`, +`low`, `medium`, `high`, `xhigh`, `max`, and `ultra`. **`none` removes the explicit effort field**; +it uses the provider's default behavior and does not guarantee that reasoning is disabled. +Applicable effort caps still run after the pin, and provider wire mapping/normalization can +lower or omit an unsupported value. `ultra` is normalized before it reaches an upstream wire. +Compaction maintenance requests are exempt from pins. + +`PATCH /api/providers?name=` accepts these fields. Omit a field to preserve it; +use `null` to clear a scalar or the whole map. A map entry set to `null` or `""` removes that +entry while preserving other entries. Malformed writes are rejected before saving. A malformed +optional pin in a hand-edited file is ignored on load without discarding the rest of the config. + ### Discovered model display names Use `modelDisplayNames` when a provider returns machine friendly ids but the Codex model picker @@ -225,6 +259,16 @@ all other provider settings. The example includes the surrounding required field } ``` +Supported bare native GPT rows in the local Codex catalog also accept exact labels in +`providers.openai.modelDisplayNames`, for example `"gpt-6-astra": "GPT 6 Astra"`. +Both startup synchronization and local catalog convergence reapply these labels. Removing a label +restores the original native name only when the row's display name still matches the applied +override. A newer external display name is preserved subject to existing native metadata normalization; +for example, Astra (`gpt-6-astra`) still replaces a non-pinned name with its pinned native name. +The label overlay leaves model IDs, metadata (including capabilities), ordering, +routed combo aliases, and account-qualified rows unchanged. This local catalog override does +not relabel the HTTP model listings or virtual `*-pro` rows. + The effective label order is operator `modelDisplayNames`, then provider catalog metadata, then the normal `provider/model` fallback. The routed selector remains `xai/grok-4.6`, while the upstream wire model remains `grok-4.6`. Labels are display only. They do not change authentication, adapter @@ -234,6 +278,20 @@ label. A management client can set or reset one label with `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }`; send `displayName: null` to reset it. Provider `PATCH` does not edit this map. Use this dedicated `PUT` endpoint to change or remove labels. +The dashboard exposes the same durable setting on **Models**. Expand the provider, find a +discovered model, and choose **Name**. The dialog keeps the exact `provider/model` selector visible +while you save a friendly label. Choose **Reset name** to return to provider metadata or the normal +selector fallback. **Name** changes presentation only; the separate alias pencil changes the +short routing alias and is not a display name editor. Native OpenAI and custom model rows keep their +existing controls. + +If the change is saved but refreshing fails, the dialog reflects the saved override and keeps +**Retry** available. Retry repeats catalog convergence when the server reported it failed, or +reloads the list when only the list request failed. Reset recovery keeps the reset operation; +it does not restore the old name. Requests have a 60-second deadline covering the write and its +follow-up list refresh. A timeout does not undo a write: use **Retry** to check the current name +before making another change. + ## Codex catalog and root `config.toml` settings These settings belong in the root of `$CODEX_HOME/config.toml`, alongside @@ -351,12 +409,18 @@ API-key providers may hold a literal key or an environment reference. OAuth prov credential store populated by `ocx login`; subscription-backed Claude Code launch behavior is configured under [`claudeCode.authMode`](/reference/configuration/server/#claude-code). -Google Antigravity consumer OAuth accounts should keep the registry default -`https://daily-cloudcode-pa.googleapis.com` endpoint. The production -`https://cloudcode-pa.googleapis.com` endpoint is retained for enterprise/GCP accounts and may -return `429 RESOURCE_EXHAUSTED` for consumer accounts even when quota remains. `ocx provider test -google-antigravity` and the dashboard connection test warn about that explicit override without -rewriting it. +OrcaRouter exposes both forms explicitly: `orcarouter` is the manual API-key provider and +`orcarouter-oauth` runs browser consent with S256 PKCE, then stores the returned durable API key as +an account credential. The public defaults intentionally split authentication +(`https://www.orcarouter.ai`) from inference (`https://api.orcarouter.ai/v1`). Set +`ORCAROUTER_BASE_URL` before the first account login for a one-origin self-hosted deployment, or use +`ORCAROUTER_AUTH_BASE_URL` and `ORCAROUTER_API_BASE_URL` for separate origins. +For a loopback/private self-hosted endpoint, **before the first login**, create or update +`providers["orcarouter-oauth"]` with `adapter: "openai-chat"`, the intended `baseUrl`, +`authMode: "oauth"`, and an explicit `allowPrivateNetwork: true`. Login preserves that operator +setting and never grants it from a URL override. Without it, destination validation rejects the +local endpoint for inference and model discovery. The OAuth browser callback listener itself +does not require this provider opt-in. See the [OrcaRouter setup example](/guides/providers/). ## Provider diagnostic outbound safety @@ -429,17 +493,34 @@ rotation may trigger provider restrictions. | Key | Type | Default | Description | | --- | --- | --- | --- | -| `anthropicAccountPool.enabled?` | `boolean` | `false` | Enable sticky session affinity and quota-ranked new-session selection. When this key is omitted, two or more usable accounts enable reactive 429 failover by presence. An explicit `false` disables that failover as well as the pool. | +| `anthropicAccountPool.enabled?` | `boolean` | `false` | Enable sticky session affinity and quota-ranked new-session selection. **429 failover is not gated here**: it activates whenever two or more usable accounts are stored, exactly like every other multi-credential provider, and cannot be switched off. | | `anthropicAccountPool.autoSwitchThreshold?` | `number` | `80` | For new sessions, when the active account reaches this threshold, choose the lowest known cached usage in the configured window; the account chosen does not itself have to be at or above the threshold. `0` disables **proactive** usage-based switching only — new-session selection and routing recovery after an eligible 429 still consult `quotaWindow`. | | `anthropicAccountPool.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | `"quota"` | New-session strategy; `quota` ranks accounts by the window set by `quotaWindow`, and `fill-first` evaluates its drain threshold in that same window. | -| `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | The cached provider-reported utilization bar used for usage-aware account selection. `five-hour` keeps the original behavior. `weekly` scores the weekly bar and skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to exhausted candidates when none do. `max-utilization` scores the highest known bar, so it can use 5-hour usage before weekly usage is available; if neither is known, the account follows unknown-usage ordering. Known usage ranks before unknown usage under the opt-in `weekly` and `max-utilization` windows only; an omitted or explicit `five-hour` preserves the legacy ordering. If every eligible account is unknown, selection still returns one in eligible order. After the documented lower-5-hour tie-break, exact ties preserve eligible order. A healthy affinity-bound session is not proactively rebalanced. For new-session assignment and routing recovery after an eligible 429 replacement, `quota` ranks eligible candidates directly with this window; `fill-first` advances in stable order using this window's threshold and exhaustion rules; `round-robin` ignores it. Cooldown, failover limits, and reauthentication eligibility remain separate local state. Per-account weekly bars are only known once the dashboard Providers page has polled them. | +| `anthropicAccountPool.quotaWindow?` | `"five-hour" \| "weekly" \| "max-utilization"` | `"five-hour"` | The cached provider-reported utilization bar used for usage-aware account selection. `five-hour` keeps the original behavior. `weekly` scores the weekly bar and skips accounts whose 5-hour bar is exhausted while another eligible account remains, but falls back to exhausted candidates when none do. `max-utilization` scores the highest known bar, so it can use 5-hour usage before weekly usage is available; if neither is known, the account follows unknown-usage ordering. Known usage ranks before unknown usage under the opt-in `weekly` and `max-utilization` windows only; an omitted or explicit `five-hour` preserves the legacy ordering. If every eligible account is unknown, selection still returns one in eligible order. After the documented lower-5-hour tie-break, exact ties preserve eligible order. A healthy affinity-bound session is not proactively rebalanced. For new-session assignment and routing recovery after an eligible 429 replacement, `quota` ranks eligible candidates directly with this window; `fill-first` advances in stable order using this window's threshold and exhaustion rules; `round-robin` ignores it. Cooldown, failover limits, and reauthentication eligibility remain separate local state. Per-account weekly bars come from usage probes or observed response headers. | | `anthropicAccountPool.stickyLimit?` | `number` | `1` | Successful new-session binds retained on one round-robin selection. Range 1–100. | -When reactive failover is active, 429 records bounded cooldown from `Retry-After` or a default backoff and may rotate -within the request. Affinity is process-local and size-bounded. Credential 401/403 marks the account -as needing reauthentication. If all eligible accounts are cooling, clients receive 429 with +When enabled, 429 records a cooldown and may rotate within the request. The cooldown length comes +from a usable `Retry-After`, otherwise from the latest valid reset time among rate-limit windows +Anthropic reports as `rejected`, including weekly windows. Valid upstream deadlines are not +shortened to a fixed cooldown ceiling; non-finite or unrepresentable deadlines are ignored. +A refusal with no usable deadline falls back to a 60-second default backoff. Affinity is process-local +and size-bounded. Credential 401/403 marks the account as needing reauthentication. If all eligible accounts are cooling, clients receive 429 with `Retry-After` when known, not an authentication error. +Anthropic responses also report the serving account's 5-hour and weekly utilization, and whichever +of those two a given response carries is recorded against that account — each window independently, +on refusals as well as successes. Usage-aware selection therefore works from the accounts you +actually use, without waiting for the dashboard Providers page to poll them. These readings refresh +the existing row rather than replacing it, so the model-scoped weekly bars that only the usage +endpoint reports are preserved until their known reset time passes. Expired measurements become +unknown, including retained standard windows omitted by later headers. A reset-only header cannot +extend an older utilization measurement. Values with no known reset retain their existing behavior; +missing measurements are never replaced with zero usage. + +Header observations do not postpone usage probes or clear a failed +probe's unavailable status. After restart, cached Anthropic observations remain available while +the next quota read probes again, because the saved observations do not include the probe clock. + :::caution[Experimental] Leave this disabled unless you understand Anthropic account policy risk. Prefer manual `ocx account use anthropic ` switching when unsure. @@ -451,24 +532,25 @@ Rotates to another logged-in account of the same provider when one is rate-limit providers that have no pool of their own — xAI, Cursor, Kimi, GitHub Copilot, Google Antigravity, and Nous. -When no relevant `enabled` setting is present, logging in a second account turns reactive rotation -on. Rotation then activates for any of those providers holding 2 or more accounts that are not -flagged for reauthentication — the same default `apiKeyPool` already applies to a 2+ key pool. An -explicit provider setting takes precedence over the global setting, and an explicit `false` -disables reactive rotation. A provider with one stored account behaves exactly as before. +**Logging in a second account is what turns this on, and nothing turns it off.** Rotation +activates for any of those providers holding 2 or more accounts that are not flagged for +reauthentication — the same rule `apiKeyPool` already applies to a 2+ key pool. A provider with +one stored account behaves exactly as before. -Rotation here runs only *after* upstream has already refused the request. Use an explicit `false` -when a second stored account must not receive a retry; otherwise account presence supplies the -backward-compatible default. +Rotation here runs only *after* upstream has already refused the request, so the only choice a +disable switch could offer is between retrying on a second account you deliberately logged in and +returning a 429 while that account sits idle. Refusing rotation is expressed by not storing a +second account. | Key | Type | Default | Description | | --- | --- | --- | --- | -| `oauthAccountFailover.enabled?` | `boolean` | presence-driven when omitted | Global control for pre-dispatch account preference and reactive 429 rotation. An explicit `false` disables both unless a provider-specific override is present. | -| `providers..oauthAccountFailover.enabled?` | `boolean` | inherits | Per-provider override; beats the global setting in either direction. `false` disables preference and reactive 429 rotation for this provider even when the global setting is `true`, and `true` opts this provider in even when the global setting is `false`. When both settings are absent, two eligible stored accounts enable reactive rotation by presence. | +| `oauthAccountFailover.enabled?` | `boolean` | presence-driven | Global override for the **pre-dispatch account preference** only. `false` stops a healthy request being steered toward the account with more known headroom. It does **not** disable 429 rotation. | +| `providers..oauthAccountFailover.enabled?` | `boolean` | inherits | Per-provider override for the same preference; beats the global setting in either direction. `false` declines the preference for this provider even when the global setting is `true`, and `true` opts this provider in even when the global setting is `false`. Reactive 429 rotation is unaffected either way. | | `providers..oauthAccountFailover.strategy?` | `"quota" \| "round-robin" \| "fill-first"` | — | Declared pool strategy for a generic OAuth provider (#695). Persisted through `ocx account strategy ` or `PUT /api/oauth/accounts/pool`; the generic selector does not act on it yet, so omitted and set behave the same today. | | `providers..oauthAccountFailover.autoSwitchThreshold?` | `number` | — | Declared 0–100 usage percent for a proactive switch on a generic OAuth provider (#695). Set with `ocx account auto-switch threshold `; inert until the selector consumes it. | -To disable both proactive account steering and reactive 429 rotation for one provider: +To decline proactive account steering for one provider whose terms you would rather not test, +while still recovering from a rate limit: ```json { @@ -486,8 +568,8 @@ Generic OAuth providers (Google Antigravity, xAI, Cursor, Kimi, GitHub Copilot, other OAuth provider outside the Codex and Anthropic pools) also accept `strategy` and `autoSwitchThreshold` on the same key, through `GET`/`PUT /api/oauth/accounts/pool?provider=` and the `ocx account strategy` / `ocx account auto-switch` verbs. The response carries -`"inert": true` for those two fields only — `enabled` is live and governs both the pre-dispatch -preference and reactive 429 rotation. `stickyLimit` and +`"inert": true` for those two fields only — `enabled` is live and governs the pre-dispatch +preference. `stickyLimit` and `quotaWindow` are not part of the generic contract. Codex (`/api/codex-auth`) and Anthropic (`anthropicAccountPool`) keep their own contracts unchanged. @@ -507,8 +589,6 @@ process-local, so a restart forgets them. Rotation carries the alternate account's **full** credential snapshot, not just its bearer, so a provider that pairs routing metadata with its token — Antigravity's Cloud Code Assist project id, for example — cannot end up sending one account's token with another account's metadata. -Antigravity rate limits embedded before output in a Cloud Code Assist SSE response are treated as -429 for this purpose, and a successful rotation rebinds that conversation to the alternate account. Current scope is the ordinary Responses request paths. Cursor reports rate limits as adapter events rather than an HTTP status, and the standalone Antigravity image endpoint has its own @@ -586,28 +666,6 @@ so passthrough stays byte-for-byte identical. The Cursor bridge is experimental. After `ocx login cursor`, add or edit `providers.cursor`. -Protocol-corruption guards are always active for Cursor-hosted external models. Billable transport -replay remains opt-in: set `replayTransientFailures: true` to retry only pre-commit connection resets -and transient 5xx failures within the shared three-send budget. The global `emptyCompletionRetry` -setting also remains off by default, and neither mechanism retries after a Cursor local side effect. - -For a stable client conversation and Cursor route, OpenCodex also bounds client-originated rate-limit -storms. After three consecutive logical requests finish as `429` within 45 seconds, later requests -receive a local `429` with `Retry-After` for a 30-second cooldown instead of reaching Cursor again. -A non-429 result, a different conversation/model/provider route, or expiry resets the sequence. -Request logs include protocol-only turn counters (logical-call ordinal, text byte counts, completed -tool calls, calls since the last completed tool step, and conservative repeat indicators). They do -not store assistant text, tool arguments, or a digest that can be correlated across installations. -Cursor models sometimes label a tool preamble as ordinary answer text. First occurrences stream -normally. For a recent repeat candidate, OpenCodex holds at most 512 bytes and discards it only when -it is a Unicode/case/punctuation-normalized repeat or has high token similarity to the prior short, -single-line first-person announcement before the same tool. Different tools, warnings, multi-line -explanations, dissimilar preambles, text-only answers, and over-limit text pass through unchanged. -When an external model emits Cursor's textual `` JSON dialect, or a leading bare object -with exactly `id`, `name`, and `arguments`, OpenCodex converts it only when the name resolves to a -tool advertised on that request and `arguments` is a JSON object; unknown, ordinary, malformed, and -oversized objects remain text. - If a proxy cannot carry Cursor's default HTTP/2 stream, set `upstreamHttpVersion` to `"http1.1"` or its `"h1"` alias. This switches inference to Cursor's `RunSSE` + `BidiAppend` compatibility transport and uses @@ -772,6 +830,12 @@ container usually has no unlocked keychain session, so requests would fail close `${ENV_VAR}` reference in the service environment there instead. Env references are left untouched by `store`. +The `zhipu-bigmodel-responses` preset seeds `glm-5.3` and `glm-5-turbo` with +`liveModels: false` for `https://open.bigmodel.cn/api/v1`. Its static roster and +per-model context, effort, and summary metadata come from the +[BigModel Responses guide](/guides/providers/#bigmodel-coding-plan-over-responses). +The official local `models.json` example does not establish a live `/models` API. + With `liveModels: false`, an empty or omitted `models` list seeds the configured `defaultModel` first, followed by `retainModels`; duplicate ids are removed while preserving first occurrence. A nonempty explicit `models` list instead seeds `models` followed by `retainModels`, without diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 019e01234c..a3f3d0e5c4 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -422,6 +422,7 @@ These settings govern `/v1/messages`, `/v1/messages/count_tokens`, the `ocx clau | --- | --- | --- | --- | | `claudeCode.bodyStallSec?` | `number` | `90` | Native-passthrough body inactivity budget in seconds while a read is pending, not total duration. Minimum 1; exactly `0` disables. | | `claudeCode.bodyMaxBytes?` | `number` | `67108864` | Cumulative native-passthrough body cap for streamed and buffered responses. Exactly `0` disables. | +| `claudeCode.compatibility?` | `"shadow" \| "enforce"` | unset | Optional compatibility admission for translated `/v1/messages` requests. `shadow` records unsupported features and continues; `enforce` returns an Anthropic-shaped 400 before inference. Native Anthropic passthrough remains unchanged. | | `claudeCode.authMode?` | `"proxy" \| "subscription"` | auto | How launch handles `ANTHROPIC_AUTH_TOKEN`. Auto detects auth each launch; an explicit value is never overridden. | | `claudeCode.authModeMigratedAt?` | `string` | unset | Internal one-time upgrade marker. Do not set manually. | | `claudeCode.classifierModel?` | `string` | unset | Explicit target for Claude Code Auto Mode classifier turns, as a qualified `provider/model` (for example `RelayA/claude-opus-5`). Auto Mode sends bare safety checks such as `claude-opus-5` with no provider, so without this they fall through to `defaultProvider` — which may not speak Anthropic at all. Nothing is inferred automatically: only a target you declare here is used. | @@ -429,6 +430,25 @@ These settings govern `/v1/messages`, `/v1/messages/count_tokens`, the `ocx clau | `claudeCode.subagentEffort?` | `"low" \| "medium" \| "high" \| "xhigh" \| "max"` | inherit | Effort written to generated `~/.claude/agents/ocx-*.md`; separate from Codex guidance and proxy caps. Restart through `ocx claude` to regenerate. | | `claudeCode.compatibility?` | `"shadow" \| "enforce"` | `enforce` | Compatibility gate for routed Claude ingress: `enforce` rejects unsupported requests before upstream activity with `400 invalid_request_error`; `shadow` records ordinary incompatibilities without rejecting, but signed-thinking ownership and other safety invariants still fail closed. | +The compatibility policy applies to Claude Code, Desktop and other clients using translated +Messages, including `?beta=true` and non-streaming requests. Every translated target uses the +same conservative policy, including Anthropic and native Responses adapters. It rejects +document content, thinking/redacted-thinking replay, hosted search and execution tools, +tool-search references, active deferred loading, strict tools, non-default caller modes, +structured-output formats, explicit service-tier intent, MCP connector features, context +management, containers, inference placement and unsupported protocol fields or blocks. + +Unset preserves legacy translation. Cache hints, tool input examples and ordinary +thinking/effort settings are deliberately admitted with possible degradation: this setting +does not guarantee cache breakpoints or TTL, retained examples, exact thinking budgets or +lossless translation. Beta headers alone are not validated for feature support. Shadow +evidence contains only fixed protocol codes and derived reasons, retained in request logs +and `usage.jsonl` and restored on restart. An invalid non-unset mode returns a fixed 503 +configuration error on translated Messages. Configure the value in `config.json` and +restart the proxy to load it; there is no dedicated GUI setter. Count-tokens and direct +Responses/Chat APIs are outside this policy; successful token counting does not imply +Messages admission. This setting does not add a global authorization boundary. + Auto auth selects subscription when stored Claude auth is found, proxy when none is found, and subscription with a warning when detection is inconclusive. See [Claude Code auth mode](/guides/claude-code/#auth-mode). diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 12d765d8db..60b945871e 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -238,6 +238,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou ### Logs, usage, and storage +`GET /api/logs` accepts an optional opaque `cursor` from its previous response. The envelope preserves +`logs`, `total`, `generatedAt` and `timeZone`, and adds `cursor` and `reset`. Without a cursor it returns +the full filtered window. A valid unchanged prefix returns only appended rows; `reset: true` replaces +the client window after edits, eviction, query changes or restart. Invalid cursors return HTTP 400 with +`error.code: "invalid_cursor"`. Authentication is unchanged. The dashboard falls back to full snapshots +for older servers. This reduces response bytes for stable windows; server projection remains bounded +by the current window size. + | Method and path | Purpose | Notable errors | | --- | --- | --- | | `GET /api/logs` | Query filtered in-memory request logs | — | @@ -246,7 +254,7 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `GET /api/debug/usage-logs` | Read bounded usage-debug entries | — | | `GET /api/debug/injection-logs` | Read bounded guidance-injection debug entries | — | | `GET /api/claude/inbound-debug` | Read Claude inbound debug state and entries | — | -| `GET /api/usage` | Stream the complete usage ledger into compact aggregates, then incrementally fold verified appends; summarize by range and client surface, with a Codex `accounts` breakdown keyed by stable non-PII log labels | Returns an `error: "read_failed"` summary if storage cannot be read | +| `GET /api/usage` | Stream the complete usage ledger into compact aggregates, then incrementally fold verified appends; summarize by preset or inclusive custom window and client surface, with a Codex `accounts` breakdown keyed by stable non-PII log labels | 400 invalid custom bounds; returns an `error: "read_failed"` summary if storage cannot be read | | `GET /api/storage` | Scan Codex storage usage by bucket | Returns an `error: "scan_failed"` payload on scan failure | | `POST /api/storage/cleanup/preview` | Preview archived-session cleanup and return a binding digest | 400 `invalid_json` or `invalid_percent` | | `POST /api/storage/cleanup` | Quarantine or permanently remove the previewed archived set | 400 invalid input; 409 stale/busy/referenced state; 500 filesystem/database failure | @@ -274,6 +282,23 @@ an earlier file prefix from 7-day, 30-day, or all-history totals. `managementUsa accepted for compatibility with bounded legacy readers, but changing it no longer expands or reduces the history summarized by this endpoint. +Pass both `since` and `until` to select an inclusive custom interval. Each accepts integer Unix +epoch **milliseconds**, or a full ISO datetime with an explicit timezone. Invalid dates, negative +or out-of-range values, reversed bounds, and a single bound are rejected. Custom bounds override +`range`; the response keeps the preset `range` field for compatibility and adds `customWindow: true`, +the exact `since`, and `until`. `generatedAt` remains the time the report was produced. + +Custom windows filter individual ledger entries before daily aggregation, including partial first +and last days. They preserve `surface`, `provider`, `model`, and `apiKeyId` filtering and never reuse +or overwrite unfiltered preset summaries. The daily chart remains capped at 366 local calendar days; +totals cover the full requested interval. Snapshot-window fields describe the scanned ledger before +the time filter, so they can extend beyond the requested bounds. + +The Usage page accepts local date/time inputs. Its selected ending minute includes the entire +minute through `:59.999`. Choosing a preset or clearing the custom window restores preset behavior. +This adds exact range selection and existing cost estimates; it does not add hourly chart buckets +or offline reporting. + The runtime ledger is append-only. Replacing or truncating it, or changing local pricing/time-zone inputs, triggers a complete rebuild. If you manually edit an older row in place while the proxy is running, restart the proxy (or replace the file) before relying on the new total; incremental refreshes @@ -292,6 +317,29 @@ re-estimated from the pricing active when the summary is read. This is an API-eq not a subscription charge. New main-pool requests use the reserved `main` label; legacy bare `openai` rows remain in an ambiguous bucket instead of being reassigned from current configuration. +Manual model prices can also be edited from **Models → Price**. A manual-pricing badge survives +catalog reloads. Prices are stored in `providers..modelCosts` and survive catalog sync. +Explicit all-zero user rates mean a known-zero estimate; **Reset to automatic** removes the +override and restores the usual catalog fallback. These remain display estimates, not bills. + +`GET /api/providers/{provider}/model-costs` returns `{ provider, modelCosts }`, with sanitized +four-rate entries keyed by exact upstream model ID. `PUT` on the same route accepts +`{ modelId, cost }`, where `cost` is `{ input, output, cacheRead, cacheWrite }` or `null` to reset. +All four rates must be finite numbers from 0 through 1,000,000, in USD per 1M tokens. +Unknown fields and malformed rates are rejected. A write preserves other models' overrides +and returns `{ ok: true, provider, modelId, cost }`; reset returns `cost: null`. + +```bash +ocx models price ollama/custom-model --json +ocx models set-price ollama/custom-model --input 0.50 --output 1.50 +ocx models set-price ollama/custom-model --input 0 --output 0 +ocx models set-price ollama/custom-model --auto +``` + +Omitted CLI cache-read/cache-write rates default to zero. Use `--cache-read` and `--cache-write` +to set them explicitly. A provider name remains an exact configuration identity; account display +labels are not editable provider names. + Rows in `models`, `providers`, and `days[].models` also carry `cacheHitRate`: the share of input tokens served from the provider's prompt cache, clamped to `[0, 1]`. It is `null` — never `0` — when the provider reported no cache telemetry or the row has no input tokens, because "no cache @@ -303,6 +351,10 @@ Storage cleanup endpoints can move or permanently remove archived session data. first and submit the returned digest. Prefer quarantine when recovery may be needed. ::: +Cleanup recovery manifests are published atomically, preserving the previous complete record +if a replacement fails before publication. This does not reverse a permanent purge: restore +can still fail when a recorded session has no surviving rollout file. + ### Models and catalog | Method and path | Purpose | Notable errors | @@ -407,7 +459,7 @@ whether to star the repository. | 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; raw errors and paths are never returned. | — | +| `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/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 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 | — | diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index e5e7a5413a..1f2e589252 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -52,66 +52,6 @@ non-empty `model`. `input` may be a string or an array of Responses items. | Service and execution | `stream`, `service_tier`, `parallel_tool_calls`, `instructions`, `metadata`, and `user` | | Extended Responses fields | `background`, `include`, `prompt`, `text`, and `truncation` are accepted for compatible routes | -### Google provider options - -Responses requests may opt into a strict Google GenerateContent extension under -`provider_options.google`: - -```json -{ - "model": "gemini-3.7-flash", - "input": "Explain this result", - "provider_options": { - "google": { - "thinking_budget": 4096, - "include_thoughts": false, - "safety_settings": [ - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_MEDIUM_AND_ABOVE" - } - ], - "cached_content": "cachedContents/my-cache" - } - } -} -``` - -The accepted keys are exactly `thinking_budget`, `include_thoughts`, -`safety_settings`, and `cached_content`; unknown keys at either nested level, -or unknown safety-setting keys, fail request validation. The parser maps these -snake-case request fields to typed internal fields; they are not arbitrary -provider passthrough data. - -- `thinking_budget` must be a safe integer greater than or equal to `-1`. - An explicit budget takes precedence over the routed model's derived thinking - level. `include_thoughts` augments the resulting thinking configuration, and - an explicit `false` is preserved. -- `safety_settings` accepts at most 16 entries, with no duplicate categories. - Categories are `HARM_CATEGORY_HATE_SPEECH`, - `HARM_CATEGORY_SEXUALLY_EXPLICIT`, `HARM_CATEGORY_DANGEROUS_CONTENT`, - `HARM_CATEGORY_HARASSMENT`, `HARM_CATEGORY_CIVIC_INTEGRITY`, and - `HARM_CATEGORY_JAILBREAK`. Thresholds are - `HARM_BLOCK_THRESHOLD_UNSPECIFIED`, `BLOCK_LOW_AND_ABOVE`, - `BLOCK_MEDIUM_AND_ABOVE`, `BLOCK_ONLY_HIGH`, `BLOCK_NONE`, and `OFF`. -- `cached_content` must be exactly one of these Google resource-name forms: - `cachedContents/{id}` for AI Studio, or - `projects/{project}/locations/{location}/cachedContents/{cachedContent}` for - Vertex. Each segment must be non-empty; whitespace, query strings, fragments, - and extra segments are rejected. - -The extension is supported only when the final route uses the Google adapter in -AI Studio or Vertex mode. Cloud Code Assist (including the -`google-antigravity` provider) and every non-Google route are rejected with a -400 before an upstream request is made. This check is applied after routing and -adapter overrides are resolved, including retry and fallback paths. - -`cached_content` opts into reuse of a provider-side Google cache that already -exists; it is not a local prompt-cache key. The resource name identifies -provider-managed content, so use it only when the caller is authorized to reuse -that content and accepts Google's retention and access policies. opencodex does -not create, inspect, or delete the provider cache through this field. - Unknown item types are accepted as loose typed items for forward compatibility. Translated adapters handle only the item types they recognize, and may reject a feature their provider cannot represent. On the canonical ChatGPT Codex forward route, text-only `system` input messages are folded into @@ -135,11 +75,7 @@ With `stream: true`, the response is `text/event-stream`. The bridge emits Respo `data: [DONE]`. With `stream: false` or no `stream`, the same adapter events are collected into one Responses JSON -object. The canonical ChatGPT Codex backend itself requires `stream: true`, so opencodex sends that -route as SSE and reconstructs a bounded JSON response for the non-streaming client. Reconstruction -retains indexed `response.output_item.done` records because the terminal snapshot can omit its -`output` array. Both client-facing forms preserve the selected model, output items, terminal status, -and usage. +object. Both forms preserve the selected model, output items, terminal status, and usage. For native HTTP/SSE passthrough, a client cancellation without an observed upstream terminal is logged as `499` with `closeReason: "client_cancel"` and does not penalize the account pool. @@ -159,13 +95,28 @@ bridge, the same condition emits a 502 `websocket_protocol_error` and cancels th A complete Responses terminal frame is authoritative: oversized or malformed trailing bytes after that terminal are dropped rather than replacing the completed turn with a transport failure. -For canonical ChatGPT forward streaming, stable Bun 1.4.0 or newer may use Codex's upstream -WebSocket transport when `wsUpstream: true` is set, or when `OCX_CODEX_WS_UPSTREAM=true`/`1` is -set with no provider override. Omitted, invalid, and explicit false values stay on HTTP/SSE. -Bundled Bun 1.3.14, prereleases, and unverifiable runtime identities also use HTTP/SSE. The -upstream WS adapter keeps the same downstream SSE contract, caps both the raw JSON frame and its -SSE envelope at 4 MiB, and closes the upstream when its 8 MiB byte queue would overflow. That -overflow emits a terminal downstream `response.failed` event followed by `[DONE]`. +:::note +For native passthrough, a Responses terminal event is authoritative. A premature `data: [DONE]` is +held until that event. On the ordinary native path, a clean HTTP 200 EOF without a parsed terminal +emits one `response.incomplete` with `incomplete_details.reason: "adapter_eof"`, followed by one +`data: [DONE]`; syntactically valid delimiter-less terminal JSON is accepted exactly once, while +malformed or truncated JSON remains incomplete. For providers opted into model-scoped terminal +repair, unframed terminal-like suffixes and a premature `data: [DONE]` at EOF fail closed with +`missing_terminal_event` when no complete lifecycle candidate can be promoted; a complete candidate +is promoted to `response.completed`. High-confidence `cyber_policy` +terminal shapes normalize to `response.failed` with `error.code: "cyber_policy"` for semantic +logging/accounting (status 400), while an already-started streamed HTTP response remains 200. This +committed-request boundary does not retry or replay and does not resolve +[#2423](https://github.com/lidge-jun/opencodex/issues/2423) or +[#2486](https://github.com/lidge-jun/opencodex/issues/2486). +::: + +For canonical ChatGPT forward streaming, stable Bun 1.4.0 or newer may transparently use +Codex's upstream WebSocket transport. Bundled Bun 1.3.14, prereleases, and unverifiable runtime +identities use HTTP/SSE. The upstream WS adapter keeps the same downstream SSE contract, caps both +the raw JSON frame and its SSE envelope at 4 MiB, and closes the upstream when its 8 MiB byte queue +would overflow. That overflow emits a terminal downstream `response.failed` event followed by +`[DONE]`. The upstream WebSocket checks `NO_PROXY`/`no_proxy` first. Otherwise it uses the first non-empty `HTTPS_PROXY`, `https_proxy`, `ALL_PROXY`, or `all_proxy` value; `HTTP_PROXY` alone does not proxy a @@ -287,31 +238,52 @@ effort default to `reasoning.summary: "auto"` so thinking streams back as `reasoning.summary: "none"`. An explicit `reasoning.summary` of `auto`, `concise`, `detailed`, or `none` wins over `include_reasoning`. -Structured output is part of that translation. `response_format` with `json_object` or -`json_schema` is forwarded to routed `openai-chat` models, subject to the provider's -`noStructuredOutputModels` opt-out: listed models omit `response_format`, while sibling models -keep it. Routed Google models lower supported requests to Gemini JSON mode -(`responseMimeType` / `responseSchema`), but skip that lowering when the request has tools, the -selected model is Claude, or the model is image-capable. Kiro rejects structured output. -Cursor has no structured-output wire field and rejects before transport. - -On `POST /v1/responses`, the equivalent request field is `text.format`: native Responses routes -preserve it in the raw Responses body, and it is translated to `response_format` when the model -routes to an `openai-chat` provider. Adapter behavior is capability-specific: an adapter may -forward, skip, ignore, or reject a feature according to its implementation, rather than every -unrepresentable feature failing closed. +Structured output is part of that translation: `response_format` with `json_object` or +`json_schema` is forwarded to routed `openai-chat` models. On `POST /v1/responses` the +equivalent request field is `text.format`: native Responses routes preserve it in the raw +Responses body, and it is translated to `response_format` when the model routes to an +`openai-chat` provider. A model listed in the provider's `noStructuredOutputModels` omits +`response_format` on that chat wire; sibling models keep the translation. Unclassified backends +receive the field and return their own error instead of the proxy guessing their capability. Non-streaming output has `object: "chat.completion"`. Streaming output uses SSE objects with `object: "chat.completion.chunk"`, choice deltas, a terminal choice with `finish_reason`, and `data: [DONE]`. Tool-call and usage information are translated back where the source events carry them. +If a streaming Chat request receives a complete JSON Responses result upstream, the proxy +synthesizes SSE from the converted completion. It preserves answer and reasoning content, +function tool calls (with a separate stream `index` for each call), usage, and the converted +`finish_reason`, including `tool_calls` and `length`. This fallback delivers the completed result +in chunks; it cannot provide token-by-token delivery before the upstream JSON response arrives. +It does not issue an additional inference request. An incomplete response caused by the output +token limit or content filtering retains `length` or `content_filter`, even if it includes tool +output. Other incomplete boundaries return an upstream error instead of claiming a normal finish. + +Refusal text stays separate from answer text: JSON completions use nullable `message.refusal`, +and streaming chunks use `delta.refusal`. Native Chat JSON-to-SSE and SSE-to-JSON conversions +preserve that field; native streaming relay preserves the provider's refusal deltas. On translated +Responses streams, refusal parts are buffered until the terminal event and emitted once in their +original output/content order. Compatible repeated or sparse snapshots do not duplicate or erase +text. Contradictory refusal snapshots and buffer overflow produce a typed error without a successful +finish or `[DONE]`. This preserves the upstream refusal; it does not introduce a proxy policy decision. + +Because the internal execution path is Responses-based, a provider adapter can impose a narrower +feature set. For example, a request feature that cannot be represented by the selected adapter is +returned as an error instead of silently changing its meaning. + ## `POST /v1/messages` and `count_tokens` These endpoints speak the Anthropic Messages dialect used by Claude Code and compatible clients. Most requests are translated to Responses, routed normally, then translated back to Anthropic JSON or Anthropic SSE. +On translated Messages requests, reasoning replay shares the request's translation budget. +Envelope admission includes encoding/decoding copy overhead, not just the original signature +length. Requests exceeding this budget return HTTP 413 with `translation_buffer_limit`; +signatures and opaque reasoning data are never truncated to make a request fit. Native +Anthropic passthrough retains its separate body-size contract. + Base64 and URL image sources are translated in user messages and nested tool results. File-backed images (`source.type: "file"`) require native Anthropic passthrough; translated routes return a fixed HTTP 400 error asking for base64 or URL input. OpenCodex does not resolve another provider's @@ -430,9 +402,16 @@ conversation. | Route type | Behavior | | --- | --- | -| Canonical ChatGPT or official OpenAI route | Forwards the request to the native `/responses/compact` endpoint with the resolved account and model authentication | +| Canonical ChatGPT or official OpenAI route | Tries the native `/responses/compact` endpoint with the resolved account and model authentication; HTTP 404 falls back to a regular Responses compaction turn | | Other routed model | Runs an internal, non-streaming, no-tools compaction turn with a `compaction_trigger`; requires exactly one synthetic `compaction` item whose `encrypted_content` is an `ocx1:` envelope; decodes that summary into v1 replacement history | +If the native compact endpoint returns HTTP 404, OpenCodex retries compaction through a regular +Responses turn with the same model selector and session headers. Canonical ChatGPT fallback +turns use upstream SSE; the compact caller still receives JSON. A completed native opaque +compaction item is preserved, while an `ocx1:` summary is decoded into replacement user history. +Failed or incomplete fallback turns return an error instead of replacement history. Other +native compact statuses retain their existing handling. + Codex names a bare OpenAI-family model (for example `gpt-5.6-sol`) for its compaction turns regardless of which provider the operator routes ordinary turns to. Ordinary requests reserve such ids for the canonical `openai` provider. On the compaction surface only — `POST @@ -443,7 +422,30 @@ default provider is enabled and is not itself an OpenAI-family entry; account-qu such as `side/gpt-5.6-sol` still fail closed. The proxy logs one notice per provider when this fallback engages. Configurations with an enabled canonical `openai` provider are unchanged. -Native compact responses are buffered with a 32 MiB maximum, including responses whose declared +Inbound bodies on both `/v1/responses` and `/v1/responses/compact` retain the shared 256 MiB +wire/decompression admission limit. Application-level size rejection returns HTTP 413 with +`type` and `code` both `invalid_request_error`. Its message includes a bounded diagnostic suffix, +for example: + +```text +Decompressed request body exceeds 268435456 bytes [measurement=decoded_lower_bound; bytes=268435457] +``` + +| Measurement | Meaning of `bytes` | +| --- | --- | +| `declared_wire` | Numeric `Content-Length` declared by the sender; rejected before reading, not a measured decoded size | +| `observed_wire_lower_bound` | Wire bytes encountered when reading stopped; the complete body may be larger | +| `decoded_exact` | Exact size of the buffer supplied to the identity decoder or returned by a decoder | +| `decoded_lower_bound` | Admission limit plus one after inflation aborts; a lower bound, never the exact decoded size | + +The suffix contains only a fixed category and a finite numeric byte value. Rejected bodies are +not read or inflated further, parsed for item counts, or retained for diagnostics. Legacy errors +without measurement provenance retain the limit-only message. Bun's listener can reject an +oversized wire body before application diagnostics run, so not every 413 carries this suffix. +A lower-bound diagnostic cannot establish the complete compact payload size. The admission +limit and retry behavior are unchanged. + +Native compact responses are buffered with a separate 32 MiB maximum, including responses whose declared `Content-Length` already exceeds the limit. The compact-specific failures include: | Status | Type or code | Meaning | diff --git a/docs-site/src/content/docs/ru/getting-started/how-it-works.mdx b/docs-site/src/content/docs/ru/getting-started/how-it-works.mdx index d18b081b12..ceeff22531 100644 --- a/docs-site/src/content/docs/ru/getting-started/how-it-works.mdx +++ b/docs-site/src/content/docs/ru/getting-started/how-it-works.mdx @@ -43,6 +43,25 @@ Codex даже не догадывается, что общается не с Op провайдера сохраняют заголовки квоты, 429 отправляет аккаунт в кулдаун, а 401/403 помечает его как требующий повторной аутентификации. +- **Неиспользуемые окна квоты можно активировать автоматически.** В расширенных настройках эта + функция по умолчанию выключена и управляет доступными 5-часовыми и недельными окнами всех + текущих основных и добавленных аккаунтов. Новые аккаунты не включаются автоматически. + В режиме Pool после наступления срока отправляется минимальный несохраняемый запрос именно + через нужный аккаунт; он расходует квоту. Одновременные сбросы объединяются в один запрос. + Приостановленные аккаунты и аккаунты, требующие повторной аутентификации, пропускаются; + жёсткая блокировка основного аккаунта также соблюдается. Заголовки квоты успешного ответа + обновляют кеш, а устаревшие метаданные включённых подходящих аккаунтов обновляются не чаще + одного раза в пять минут даже без открытой панели. Наблюдаемые сроки сохраняются до завершения + активации, включая перезапуски, поэтому сдвиг времени при следующем опросе не удаляет ожидающую + работу. Опрос использует существующее ограниченное восстановление аутентификации; ответ 401 + на запрос модели помечает отклонённые учётные данные для повторной аутентификации. + В журнал ошибок попадают только непрозрачная метка аккаунта и безопасная причина состояния. + Активация отличается от выбора аккаунта для входящего запроса. + +**Возврат к старой версии:** перед её запуском удалите только `nextFiveHourResetAt` и +`nextWeeklyResetAt` из настроек автоматической активации. Строгий валидатор старой версии +не принимает эти новые поля и может отключить весь блок настроек активации. + ## Выбор модели для подагентов После чистой установки `subagentModels` включает `gpt-6-astra`, тройку GPT-5.6 Sol/Terra/Luna и 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 fdc7b6075a..6a455b3ed6 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -176,8 +176,16 @@ user-agent `claude-code/*` получает читаемую CLI-форму, а продолжает работать. Если нижний селектор Claude Desktop не переключает модель в уже запущенном 3P-диалоге, -используйте `/model ` внутри этого диалога. OpenCodex не видит состояние селектора и -маршрутизирует id модели из каждого запроса. Результат можно проверить в **Logs → requestedModel**. +можно попробовать `/model `, но в затронутых сборках Desktop этот обходной способ тоже может +не сработать. В [issue #3782](https://github.com/lidge-jun/opencodex/issues/3782) сообщается, что +в Windows с Claude Desktop 1.46388.4 диалог продолжает использовать исходную модель после изменений +как через нижний селектор, так и через `/model`. Это сообщение не устанавливает, какой компонент +клиента или маршрутизации вызывает такое поведение. + +Можно также попробовать выбрать нужную модель по умолчанию в профиле Claude Desktop в OpenCodex, +повторно применить профиль и начать новый диалог. Это шаг по устранению неполадки, а не гарантированное +решение. OpenCodex не видит состояние селектора; он маршрутизирует id модели, переданный в каждом +запросе. Проверьте, что отправляет клиент, в **Logs → requestedModel**. **Правила грамматики алиасов:** provider не может содержать `/` или `--` и не может быть равен `native`. Обычные id моделей (без `/` и `~`) остаются с префиксом v1 `claude-ocx-…`. Id с `/` @@ -420,12 +428,14 @@ Claude Code — это лишь учётные данные для доступ | Текст ассистента | `output_text` | | `tool_use` ассистента | `function_call` (`input` → `arguments` в виде JSON-строки) | | `tool_result` пользователя | `function_call_output` (`is_error` → префикс `[tool error]`) | -| Повтор `thinking` / `redacted_thinking` | Отбрасывается | +| Повтор `thinking` / `redacted_thinking` | Элементы `reasoning` с ограниченными конвертами `ocxr1` для подписей и скрытых данных | | Function-инструменты | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, именованная функция→`{type:"function",name}`, размещённый WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +На выбранном адаптере Anthropic сохраняются нескрытые подписанные блоки (включая пустой thinking) и непрозрачные блоки redacted. Политика `hideThinkingSummary` не меняется: локально скрытый подписанный текст не раскрывается клиентам Claude, а воспроизведение без потерь через эту границу пока не подтверждено. Старые объединённые конверты не восстанавливают порядок после отправки потокового текста. `claudeCode.compatibility: "enforce"` по-прежнему отклоняет thinking replay. Приём реальным Anthropic и улучшение кеша не доказаны; [#3719](https://github.com/lidge-jun/opencodex/issues/3719) остаётся открытым. + **Случаи ошибок (400):** некорректный JSON; отсутствующий или пустой `model`; отсутствующий или пустой `messages`; неподдерживаемая роль; `tool_result` без `tool_use_id`; `tool_use` без id/name; именованный `tool_choice` без имени. @@ -437,7 +447,8 @@ id/name; именованный `tool_choice` без имени. | `response.created` | `message_start` + `ping` | | Heartbeat | `ping` | | Текстовые дельты | `content_block_start` → `content_block_delta` (text) → `content_block_stop` | -| Резюме/текст рассуждений | Блок `thinking` с синтетической подписью | +| Резюме/текст рассуждений | Блок `thinking` с повторно переданной подписью или ограниченным резервным конвертом `ocxr1` | +| Скрытое рассуждение | Блоки `redacted_thinking`, воспроизведённые из конверта рассуждений | | Кадры function-call | Блок `tool_use` с `input_json_delta` | | Завершающее событие | `message_delta` → `message_stop` | | EOF до завершающего события | `api_error` в стиле 502 | diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 9707a3ea44..23581e56f6 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -224,6 +224,14 @@ opencodex кодирует объявление и историю как functio потоковый lifecycle function call в `custom_tool_call` до передачи в Codex. Нативная forward- маршрутизация OpenAI и поддерживаемый custom tool `apply_patch` остаются без изменений. +Перед первым вызовом маршрутизируемые ходы в code-mode также получают правила хоста для вложенных +вспомогательных инструментов: `tools.apply_patch` принимает одну строку, которая начинается и +заканчивается отдельными строками маркеров патча без дополнительного оформления; в isolate нет +`import`, а длительные команды опрашиваются через `write_stdin`. Если результат exec в code-mode +на нативном маршрутизируемом пути Responses, Kiro или Cursor всё ещё содержит одно из сообщений +хоста об ошибке, opencodex добавляет однострочную подсказку с указанием правила. Это изменение +не переписывает код модели или текст её патча. + Выбранный provider должен поддерживать function/tool calling. Text-only provider без tool calls не может использовать `exec`, Browser или Computer Use. Нативные записи OpenAI сохраняют свой upstream tool mode без изменений. diff --git a/docs-site/src/content/docs/ru/guides/model-ordering.md b/docs-site/src/content/docs/ru/guides/model-ordering.md index 290194fcd2..5ff04e4892 100644 --- a/docs-site/src/content/docs/ru/guides/model-ordering.md +++ b/docs-site/src/content/docs/ru/guides/model-ordering.md @@ -118,7 +118,7 @@ native-выбора в selector-qualified группы. Поддерживаемый способ настроить порядок ведущих моделей — переставить элементы `subagentModels`. Страница **Sub-agents** в дашборде позволяет менять порядок bare native- и routed-id. Конфигурация и `ocx agent subagents set` также принимают точные account-qualified id -`/`, но дашборд не предлагает и не сохраняет их при записи списка. +`/`, а дашборд сохраняет уже записанные ID, даже если они недоступны. Используйте не более пяти настроенных id. При активных селекторах одна bare native-модель может развернуться в несколько selector-qualified строк, поэтому число настроенных вариантов и объявляемых строк не обязательно совпадает. @@ -170,3 +170,11 @@ native-выбора в selector-qualified группы. `disabledModels` и `selectedModels` каждого провайдера по-прежнему управляют видимостью. Отдельных настроек `modelOrder`, `providerOrder` или карты приоритетов нет. + +## Пресеты в дашборде + +На странице **Models** выберите порядок по умолчанию, по имени A–Z, по провайдеру или снимок использования и примените его. Сохраняются доступные маршрутизируемые ID и `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`). Сохранённая статистика читается один раз при применении; перезагрузка и изменения каталога не пересчитывают снимок. Пользовательский порядок, включая полный нативный, сохраняется до явного применения. Сброс удаляет оба поля даже при пустом списке моделей. + +`GET/PUT /api/subagent-models` сохраняет отключённые и отсутствующие выбранные ID в `chosen` и `available`; `pickerAvailable` содержит допустимые маршрутизируемые ID. Models отправляет `pickerOrder` и `pickerOrderMode`, но не `models`. Сохранение только roster не меняет порядок; неверный ввод и ошибка записи сохраняют предыдущее состояние. + +Диапазоны приоритетных и нативных моделей сохраняются. Порядок применяется к каталогу Codex и маршрутизируемым группам обнаружения Claude, сохраняя нативный префикс Claude, явные профили Desktop и владельцев alias. Ранги подсказок OpenCodex и настройки fallback не меняются, но пять вариантов, объявляемых нативным Codex, и рекомендуемая модель могут измениться. Сохранение не перезапускает клиенты; обновление может ожидать завершения, а старый каталог потребовать повторного открытия клиента. diff --git a/docs-site/src/content/docs/ru/guides/pi.md b/docs-site/src/content/docs/ru/guides/pi.md index 0960ecf49a..e36a73da7e 100644 --- a/docs-site/src/content/docs/ru/guides/pi.md +++ b/docs-site/src/content/docs/ru/guides/pi.md @@ -27,6 +27,9 @@ ocx export --client pi "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -41,6 +44,8 @@ ocx export --client pi } ``` +В создаваемой конфигурации Pi включён `compat.sendSessionAffinityHeaders`. Сохраняйте этот флаг при объединении или ручном редактировании провайдера: Pi передаёт стабильный идентификатор сессии, из которого OpenCodex формирует affinity для канонического OpenCode Go. При `cacheRetention: none` Pi может не передавать идентификатор. + Id моделей — это канонические селекторы прокси, поэтому маршрутизируемые модели появляются как `provider/model` (`anthropic/claude-opus-5`), а нативные slug OpenAI остаются без префикса (`gpt-5.6-sol`). Суффикс в `name` — `(anthropic)`, `(native)`, `(routed)` — как раз и позволяет diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 8b43b8d4c2..84af9747d8 100644 --- a/docs-site/src/content/docs/ru/guides/providers.md +++ b/docs-site/src/content/docs/ru/guides/providers.md @@ -123,6 +123,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. | +Проверки квот аккаунтов и провайдера Google Antigravity используют фиксированные адреса Google, включая резервный запрос списка моделей. Для этих адресов поддерживается прозрачный Fake-IP DNS с сохранением проверки TLS, запрета перенаправлений и проверки частных адресов. Пользовательский base URL меняет только запросы моделей; `NO_PROXY` сохраняет политику прямого подключения. + + После терминального сбоя обновления Nous выполните `ocx login nous`, чтобы пройти повторную аутентификацию. Для канонических пресетов Kimi Coding Plan (вход через аккаунт `kimi` и API-ключ `kimi-code`) @@ -226,6 +229,7 @@ opencodex поставляется с 79 встроенными пресетам | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (статический список)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token plan (по умолчанию): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Pay as you go: `https://dashscope.aliyuncs.com/compatible-mode/v1` · или Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -366,7 +370,7 @@ plan. Ключ создаётся в [дашборде Featherless](https://feat > в интерактивных инструментах программирования. Автоматизация общего API, серверы пользовательских > приложений и неинтерактивные пакетные вызовы запрещены и могут привести к блокировке ключа плана. -> **Два маршрута GLM:** `zai` — это международная подписка Z.AI на coding-план, а `zhipu-bigmodel` — +> **Тарификация GLM:** `zai` — это международная подписка Z.AI на coding-план, а `zhipu-bigmodel` — > внутренняя китайская конечная точка BigModel с оплатой по факту использования. Разные хосты, > разные ключи, разная тарификация: ключ от одного сервиса не подойдёт к другому. @@ -414,9 +418,9 @@ Assist), `azure` / `azure-openai`, `kiro` и `cursor`. Проприетарны **GitLab Duo** остаётся шлюзом с ключом/токеном подписки на своей OpenAI-совместимой конечной точке. **Cloudflare AI Gateway** требует подставить в URL id аккаунта и шлюза. -Copilot предоставляет каталог со смешанными проводами: его семейство GPT-5 (`gpt-5.3-codex`, -`gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) -отклоняет `/chat/completions` для агентного трафика, поэтому opencodex по умолчанию +Copilot предоставляет каталог со смешанными проводами: модели (`gpt-5.3-codex`, +`gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) +отклоняют `/chat/completions` для агентного трафика, поэтому opencodex по умолчанию маршрутизирует эти модели через Responses API, а все остальные модели Copilot остаются на chat completions. Приоритет: жёсткий wire-пин → явная запись [`modelAdapters`](/ru/reference/configuration/providers/) → дефолт реестра → adapter всего diff --git a/docs-site/src/content/docs/ru/guides/remote-hub.md b/docs-site/src/content/docs/ru/guides/remote-hub.md index 8c37d52108..0887baf9a8 100644 --- a/docs-site/src/content/docs/ru/guides/remote-hub.md +++ b/docs-site/src/content/docs/ru/guides/remote-hub.md @@ -60,16 +60,38 @@ OAuth запускается через `POST /api/oauth/login`. Если callba ## Docker и устранение неполадок -Официального Docker-образа нет, но репозиторий содержит поддерживаемые `Dockerfile` и `compose.yaml` для локальной сборки Bun-образа, закреплённого по digest. При первом обычном запуске контейнер создаёт самоподписанный TLS-сертификат `/home/bun/.opencodex/container-tls/cert.pem` и закрытый ключ `/home/bun/.opencodex/container-tls/key.pem`. Ключ доступен только владельцу в volume `ocx-state`, а endpoint данных с этого момента работает по HTTPS. - -До первого запуска один раз передайте токен данных через stdin. Bootstrap принимает не более одной строки размером 512 байт, никогда не выводит токен, отказывается заменять существующий и сохраняет его в каноническом защищённом файле `service-api-token`. - -На хосте нужны Git и Bun. Перед каждой сборкой создавайте канонический манифест из отслеживаемых Git исходников и файлов, определяющих контейнер, и не меняйте их до завершения сборки. Сгенерированный JSON не добавляйте в Git; `.git` исключён из контекста Docker. По умолчанию порт хоста привязан к `127.0.0.1`. Для удалённого доступа явно задайте `OPENCODEX_BIND_ADDRESS= docker compose up -d`; `0.0.0.0` открывает все интерфейсы. Защитите доступ брандмауэром и аутентифицированным TLS/tailnet-фронтендом. - -Манифест аутентифицирует `Dockerfile`, `compose.yaml`, `.dockerignore`, каждый отслеживаемый управляющий файл в `docker/`, исходники в `src/` и обязательные файлы пакета, включая `package.json`, `bun.lock` и `scripts/model-metadata.source.json`. Сборка сверяет каждый SHA-256 с контекстом и затем образом; отсутствующие или изменённые файлы, любой лишний исходник или управляющий файл Docker и символические ссылки запрещены. +При откате сохраняйте оба тома и их точки монтирования. Владельцы и права существующих томов не исправляются автоматически. Именованные тома вне Compose и отдельные пути состояния описаны в [основном руководстве](/guides/remote-hub/#docker-compose). + +Состояние хранится в двух отдельных томах: `ocx-state` для +`OPENCODEX_HOME=/home/bun/.opencodex` и `codex-state` для +`CODEX_HOME=/home/bun/.codex`. Форматы `auth.json` у двух продуктов несовместимы, +поэтому не объединяйте их домашние каталоги. Оба тома доступны для записи при +корневой файловой системе только для чтения. + +Каталог моделей автоматически не создаётся. Перед проверкой авторизованного +`/v1/catalog` создайте или импортируйте корректный файл +`/home/bun/.codex/opencodex-catalog.json`. Для пустого каталога состояния ответ +404 `catalog_not_found` ожидаем. Обновление сохраняет `ocx-state` и добавляет +`codex-state`, но не переносит файлы автоматически. Если обходное решение хранило +каталог моделей в `.opencodex`, сначала сделайте резервную копию, затем перенесите +только каталог моделей с доступом лишь для владельца. Не перезаписывайте один +`auth.json` другим. При переопределении `CODEX_HOME` монтируйте именно эту директорию +для записи и сохраняйте каталог по умолчанию в `${CODEX_HOME}/opencodex-catalog.json`. +Если `model_catalog_json` задаёт другой файл, его разрешённый путь также должен +храниться постоянно. До явного переноса сохраняйте прежнее соответствие переменных +окружения и томов. `docker compose down` сохраняет оба тома, а +`docker compose down --volumes` удаляет и `ocx-state`, и `codex-state`, включая +учётные данные, историю использования, ключ данных, состояние и каталог Codex. +Это разрушительная операция, а не способ обновления или перезапуска. + +Официального Docker-образа нет, но репозиторий содержит поддерживаемые `Dockerfile` и `compose.yaml` для локальной сборки Bun-образа, закреплённого по digest. Перед первым запуском один раз передайте ключ данных через stdin; он не выводится и сохраняется с доступом только для владельца в volume `ocx-state`. + +На хосте нужны Git и Bun. Перед каждой сборкой создавайте канонический манифест из отслеживаемых Git исходников и не меняйте их до завершения сборки. Сгенерированный JSON не добавляйте в Git; `.git` исключён из контекста Docker. По умолчанию порт хоста привязан к `127.0.0.1`. Для удалённого доступа явно задайте `OPENCODEX_BIND_ADDRESS= docker compose up -d`; `0.0.0.0` открывает все интерфейсы. Защитите доступ брандмауэром и аутентифицированным TLS/tailnet-фронтендом. + +Сборка отклоняет устаревший манифест, сверяя каждый SHA-256 с файлами контекста и затем образа. Отсутствующие или изменённые файлы, лишние исходники и символические ссылки запрещены. Обязательны `package.json`, `bun.lock` и единственный включаемый файл из `scripts/` — `scripts/model-metadata.source.json`. ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -77,40 +99,6 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -Для проверки локального HTTPS endpoint скопируйте только открытый сертификат: - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -`OPENCODEX_PORT` одновременно задаёт опубликованный порт хоста и управляемое Compose значение `tls.publicOrigin`; внутренний listener остаётся на `10100`: - -```bash -OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz -``` - -Сохранённый volume, созданный до появления TLS, мигрирует автоматически при запуске: для него создаётся отдельная TLS-идентификация, а HTTPS origin использует опубликованный порт. Пути к сертификатам, которыми управляет оператор, не изменяются. Созданный сертификат покрывает только `localhost` и `127.0.0.1`. Для прямой публикации по удалённому имени установите сертификат и ключ для этого имени, затем передайте точный HTTPS origin через `OPENCODEX_PUBLIC_ORIGIN` вместе с адресом публикации: - -```bash -OPENCODEX_PUBLIC_ORIGIN=https://hub-name.tailnet-name.ts.net \ -OPENCODEX_BIND_ADDRESS=100.64.0.10 \ -docker compose up -d -``` - -Внутренние проверки здоровья и готовности могут отключать проверку сертификата только при обращении к фиксированному loopback-адресу контейнера `https://127.0.0.1:10100`. Это исключение не подходит для внешней приёмки: при проверке развёртывания необходимо проверять точное имя хоста с помощью скопированного открытого сертификата или системного хранилища доверия. - -Используйте те же переменные при последующих вызовах Compose. Для возврата к старому HTTP-образу удалите настройку TLS, пока текущий образ ещё доступен, и только затем запустите старый образ. Файлы идентификации можно оставить в volume: - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# выбрать или собрать старый образ, затем заново создать hub -docker compose up -d -``` - Контейнер работает от непривилегированного пользователя `bun`, с корневой файловой системой только для чтения и публикует только `10100`. Не публикуйте `10101` и не помещайте секреты в `ARG`, `ENV`, `COPY`, Compose, историю образа или argv. После healthcheck отдельно проверьте readiness, аутентифицированный каталог и реальный запрос. `docker compose down` сохраняет volume; `docker compose down --volumes` удаляет также конфигурацию, учётные данные и ключ. - При недоступном hub можно отключиться офлайн, но отзыв ключа останется незавершённым. 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 228a6c40a4..1cb9595c81 100644 --- a/docs-site/src/content/docs/ru/reference/cli/agents.md +++ b/docs-site/src/content/docs/ru/reference/cli/agents.md @@ -152,7 +152,7 @@ override, но файлы на диске никогда не меняются. ## Экспорт client config -### `ocx export --client ` +### `ocx export --client ` Печатает client config, направленный на работающий прокси. Команда сериализует блок провайдера `opencodex` в нативном формате выбранного клиента: base URL, список моделей и, @@ -163,7 +163,7 @@ override, но файлы на диске никогда не меняются. | Флаг | Действие | | --- | --- | -| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | +| `--client ` | Обязателен. Выбирает формат конфигурации клиента. | | `--json` | Печатать только JSON-конфиг в stdout, чтобы redirect сохранял побайтно точный вывод. Вся диагностика, включая заметку о записи через `--out`, идёт в stderr. | | `--out ` | Записать конфиг в ``. Перезаписывать существующий файл не позволит. | | `--force` | Разрешить `--out` заменить существующий файл. | @@ -193,6 +193,17 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `mcode` | `~/.minimax/config.yaml` (`MINIMAX_DATA_DIR`, затем устаревшая `MAVIS_DATA_DIR`, имеют приоритет, если заданы; относительное значение отклоняется) | `mcode-config.yaml` | нет — loopback placeholder | | `zcode` | `~/.zcode/v2/config.json` (`ZCODE_DATA_DIR` имеет приоритет, если задана; относительное значение отклоняется) | `config.json` | нет — loopback placeholder | | `prime` | `~/.prime/agent/models.json` (`PRIME_AGENT_CODING_AGENT_DIR` имеет приоритет, если задана; относительное значение отклоняется) | `prime-models.json` | нет — loopback placeholder | +| `raycast` | `~/.config/raycast/ai/providers.yaml` одинаково на macOS и Windows (Raycast не учитывает `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | нет — только loopback, запись `api_keys` не создаётся | + +Экспорт для Raycast — это отдельный документ `providers.yaml` с одним элементом `id: opencodex` в +последовательности `providers`: `name: OpenCodex`, базовый URL прокси с `/v1` и каждая маршрутизируемая +модель с её `abilities` (`tools` и `system_message` поддерживаются всегда, `vision` берётся из входных +модальностей каталога, `reasoning_effort` задаётся, когда у модели есть шкала усилий, `temperature` +отключена для рассуждающих моделей). Custom Providers — функция Raycast Pro, а Raycast следит за файлом, +поэтому сохранённое изменение вступает в силу без перезапуска. Формат описан на +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers). Запись +`api_keys` не создаётся, поэтому этот экспорт работает только через loopback, а привязка вне loopback +отклоняется. opencode интерполирует `{env:OPENCODEX_OPENCODE_API_KEY}`. Сгенерированный opencodex экспорт для Pi не требует переменной окружения и несёт литеральную заглушку `opencodex-loopback`. Это значение diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index df9ffd2754..cefa6e3380 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -89,6 +89,19 @@ ocx eject back ### `ocx status [--json]` +Status и `ocx doctor` сравнивают версии текущего CLI и работающего прокси. Если CLI новее, +перезапустите прокси из нужной актуальной установки. Для фоновой службы используйте +`ocx service repair` (`ocx service restart` — её псевдоним). Если новее прокси, обновите CLI +или исправьте `PATH`, чтобы он указывал на нужную установку. Диагностика не ремонтирует службу +и не меняет разрешение запросов. + +При одинаковых строках версий, значениях `unknown` / `0.0.0` или отсутствии версии прокси +предупреждение подавляется. Doctor не считает placeholder подтверждённым совпадением. +Разные строки, которые нельзя строго разобрать как SemVer, и версии, отличающиеся только +build metadata, вызывают нейтральное предупреждение без указания устаревшей стороны. +Пробелы не удаляются, префикс `v` не нормализуется. JSON содержит ту же рекомендацию в +`versionSkew` с прежними полями `cliVersion`, `proxyVersion`, `skewed` и `warning`. + Печатает read-only диагностическую сводку: PID прокси, достижимость `/healthz`, URL дашборда, путь к конфигу, провайдера по умолчанию, настройку автозапуска Codex, состояние службы, состояние shim'а и redacted effective Codex home. Только явная и высокоуверенная сигнатура mismatch diff --git a/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md b/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md index f3f5098d77..d64c398a3d 100644 --- a/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/ru/reference/cli/providers-accounts.md @@ -15,7 +15,7 @@ pool'ами и контролируют каталог моделей, кото | Подкоманда | Поддерживаемые флаги | Действие | | --- | --- | --- | -| `list` | `--json` | Показать настроенных провайдеров и оставшиеся записи registry. | +| `list` | `--json`, `--jsonl` | Показать настроенных провайдеров и оставшиеся записи registry. `--jsonl` выводит по одному JSON-объекту настроенного провайдера на строку. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Добавить registry/custom-провайдера. `--force` перезаписывает; `--sync` обновляет живой прокси в human-output mode. | | `edit ` | provider field flags, `--headers `, `--json` | Изменить валидированные live-поля провайдера, не заменяя key-pool'ы. `--headers` объединяет пользовательские request-header'ы; передайте `{}` или `-`, чтобы очистить их. | | `test ` | `--json` | Пробный запрос к реальному upstream model-endpoint'у. | @@ -29,6 +29,7 @@ pool'ами и контролируют каталог моделей, кото ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -37,6 +38,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` выводит только настроенных провайдеров: один JSON-объект на строку. Поля каждого объекта совпадают с полями элемента массива `configured` в `--json`; сводка `registryCount` не включается. Скрипты могут обрабатывать объекты построчно. Флаги `--json` и `--jsonl` нельзя использовать вместе. + :::caution[Пользовательские заголовки — не канал для учётных данных] `--headers` предназначен для несекретных метаданных запроса — подсказок маршрутизации, селекторов тенанта или проекта, идентификаторов трассировки. Это не @@ -187,12 +190,11 @@ quota-bar'ов дашборда. ### `ocx account auto-switch > [--json]` -Управляет только пулом аккаунтов Codex `openai`. `on` ставит 80%, `off` — 0%, `status` читает -текущее значение, а `threshold ` принимает целое число от 0 до 100. Для других провайдеров и -некорректных значений команда завершается кодом 1. `--json` возвращает: +Управляет порогом пула Codex `openai` или сохраняет порог общего пула OAuth. `on` сохраняет 80 %, `off` — 0 %, а `threshold ` принимает 0–100. Пороги общих пулов пока не применяются: сохранение не включает переключение по порогу, не меняет настройку включения провайдера и не отключает ротацию после ошибки 429. Для общего пула результат чтения и изменения берётся из подтверждённого ответа сервера. Для общего пула `poolEnabled` — сохранённая настройка провайдера (`null` означает отсутствие настройки), а не итоговое унаследованное состояние. `inert: true` означает, что порог не применяется; неизвестная возможность также не даёт `enabled: true`. Провайдеры с ключом API, Anthropic и неверные значения отклоняются. ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 269ff8abe6..5dc59ff534 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -113,7 +113,7 @@ cross-route credential fallback не существует. Строки API GPT- | `modelAutoCompactTokenLimits?` | `Record` | Мягкие бюджеты автосжатия по моделям в виде положительных безопасных целых чисел. Они могут только уменьшать эффективную границу в 90 % контекста или максимального ввода и не выдаются, если авторитетное окно контекста неизвестно. Для канонического `openai` ключами могут быть только точные поддерживаемые ID нативных моделей без префиксов провайдера или селектора аккаунта. PATCH провайдера объединяет записи: `null` для ключа удаляет его, а `null` для всего поля очищает карту. Такие маркеры `null` допустимы только в PATCH. | | `defaultMaxOutputTokens?` | `number` | Provider-wide fallback для `openai-chat`, когда клиент не передал `max_output_tokens`. | | `modelMaxOutputTokens?` | `Record` | Положительные fallback-budget'ы `openai-chat` по моделям; exact/pattern-match имеет приоритет над provider-default. | -| `modelCosts?` | `Record` | Отображаемые цены по моделям (USD за 1M токенов), ключ — точный upstream id модели этого провайдера (не идентификатор провайдера и не маршрутизируемая метка `provider/model`), значение — четыре поля: `input`, `output`, `cacheRead`, `cacheWrite` (пример: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). Любой id допустим — кастомный провайдер может указывать на любой OpenAI-совместимый endpoint через адаптер `openai-chat`, а локальные и внутренние провайдеры работают даже без строки во встроенных каталогах. Пользовательские цены имеют приоритет над встроенными каталогами в оценках `~$` в Logs и Usage; исторические записи пересчитываются по текущему оверлею, поэтому изменение цены может сдвинуть прошлые суммы (порядок: пользователь → каталог jawcode → expected-price overlay → вендорская цена модели); полностью нулевая запись переходит к следующему источнику. Каждая ставка должна быть неотрицательным конечным числом не более 1 000 000 (USD за 1M токенов); строки вне диапазона отклоняются на управляющей границе и отбрасываются при загрузке. Только оценка для отображения: оверлеи не влияют на маршрутизацию, выбор аккаунта, квоты или биллинг. | +| `modelCosts?` | `Record` | Отображаемые цены по моделям (USD за 1M токенов), ключ — точный upstream id модели этого провайдера (не идентификатор провайдера и не маршрутизируемая метка `provider/model`), значение — четыре поля: `input`, `output`, `cacheRead`, `cacheWrite` (пример: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). Любой id допустим — кастомный провайдер может указывать на любой OpenAI-совместимый endpoint через адаптер `openai-chat`, а локальные и внутренние провайдеры работают даже без строки во встроенных каталогах. Пользовательские цены имеют приоритет над встроенными каталогами в оценках `~$` в Logs и Usage; исторические записи пересчитываются по текущему оверлею, поэтому изменение цены может сдвинуть прошлые суммы (порядок: пользователь → каталог jawcode → expected-price overlay → вендорская цена модели); явно заданный пользователем набор нулевых ставок означает известную нулевую оценку; удалите запись модели, чтобы восстановить автоматическую цену. Нулевые цены каталога по-прежнему переходят к следующему источнику. Каждая ставка должна быть неотрицательным конечным числом не более 1 000 000 (USD за 1M токенов); строки вне диапазона отклоняются на управляющей границе и отбрасываются при загрузке. Только оценка для отображения: оверлеи не влияют на маршрутизацию, выбор аккаунта, квоты или биллинг. | | `headers?` | `Record` | Дополнительные upstream-header'ы. Заголовки авторизации, cookie, API-key-header'ы, встроенные переводы строк и невалидные имена отклоняются. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Предпочтения по умолчанию для OpenRouter (`order`, `only`, `allowFallbacks`); валидно только для канонического OpenRouter с `openai-chat`. | | `modelOpenRouterRouting?` | `Record` | Exact override по model id, которые полностью заменяют provider-wide preference для OpenRouter. | @@ -125,7 +125,7 @@ cross-route credential fallback не существует. Строки API GPT- | `modelReasoningEfforts?` | `Record` | Label'ы по отдельным моделям. Пустой список скрывает управление effort. | | `modelSupportsReasoningSummaries?` | `Record` | Установите `false` для модели, чтобы перестать рекламировать summary и вырезать поля доставки summary. | | `modelReasoningSummaryDelivery?` | `Record` | Responses delivery enum по моделям; переписывает уже существующее поле delivery. | -| `modelAdapters?` | `Record` | Wire-override по модели для `openai-chat` или `openai-responses` в gateway с несколькими wire-форматами. Явные записи имеют приоритет над default'ами registry; preset DeepSeek может выбирать native Responses для `deepseek-v4-flash`, а GitHub Copilot объявляет Responses-only default'ы для семейства GPT-5 (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`), потому что эти модели отклоняют `/chat/completions` для агентного трафика. Модели без встроенного default'а (например, `gpt-5.4-nano`) можно включить здесь. Single-wire upstream pin'ы и canonical ChatGPT forward override не принимают. | +| `modelAdapters?` | `Record` | Wire-override по модели для `openai-chat` или `openai-responses` в gateway с несколькими wire-форматами. Явные записи имеют приоритет над default'ами registry; preset DeepSeek может выбирать native Responses для `deepseek-v4-flash`, а GitHub Copilot объявляет Responses-only default'ы для моделей (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`), потому что эти модели отклоняют `/chat/completions` для агентного трафика. Модели без встроенного default'а (например, `gpt-5.4-nano`) можно включить здесь. Single-wire upstream pin'ы и canonical ChatGPT forward override не принимают. | | Opt-in xAI Responses (панель) | переключатель | Только для `xai`: атомарно задаёт или удаляет записи `modelAdapters` для `grok-4.5` и `grok-4.6`. Одна запись отображается как смешанное состояние до следующего переключения. Остальные override и поведение tier не меняются. | | `xaiResponsesXSearch?` | `boolean` | По умолчанию отключено. Для назначения xAI Responses декларация `x_search`, размещённая у провайдера, добавляется только тогда, когда действующий инструмент `web_search` сохраняется после окончательной нормализации запроса. Существующие декларации не дублируются, селекторы вызывающей стороны `tool_choice`/`allowed_tools` никогда не расширяются, и эта настройка не связана с параметрами `search.xSearch` сайдкара веб-поиска. | | `modelPreferHostedTools?` | `Record` | Opt-in для точного model ID в non-forward Responses gateway, который резервирует namespace hosted tool. Сейчас допускается только `["image_generation"]`; совпавшая модель должна использовать wire `openai-responses` и поддерживать этот hosted tool. Прокси удаляет конфликтующие клиентские объявления `image_gen` и переписывает их selectors, сохраняя caller tool choice. Для виртуальных моделей OpenAI API `-pro` сначала сопоставляется выбранный публичный ID, а затем в качестве fallback используется ID базовой wire-модели. `modelAdapters` сначала разрешается по публичному ID, затем по базовому ID; второй результат определяет итоговый wire. Остальные модели сохраняют обычное alias-поведение. | @@ -498,6 +498,24 @@ Pool/Direct рекламирует `922000`; синхронизированны } ``` +## Редактор отображаемых имён моделей + +На странице **Models** в дашборде можно задать понятные имена для обнаруженных моделей и сохранить их для дальнейшего использования. Разверните провайдера, +найдите обнаруженную модель и выберите **Name**. При сохранении понятной подписи диалог оставляет +видимым точный селектор `provider/model`. Выберите **Reset name**, чтобы вернуться к metadata +провайдера или обычному селектору, используемому по умолчанию. **Name** меняет только отображение; +отдельный значок карандаша для alias меняет короткий routing alias и не является редактором +отображаемого имени. Нативные строки OpenAI и строки пользовательских моделей сохраняют +существующие элементы управления. + +Если изменение сохранено, но обновление не удалось, диалог отражает сохранённое переопределение +и оставляет **Retry** доступным. Retry повторяет приведение каталога к согласованному состоянию, +если сервер сообщил о сбое этого процесса, или перезагружает список, если не удался только запрос +списка. Восстановление после сброса сохраняет операцию сброса и не возвращает старое имя. +Для запросов действует общий срок в 60 секунд, включающий запись и последующее обновление списка. +Тайм-аут не отменяет запись: используйте **Retry**, чтобы проверить текущее имя перед следующим +изменением. + ## Полный пример ```json diff --git a/docs-site/src/content/docs/ru/reference/proxy-formats.md b/docs-site/src/content/docs/ru/reference/proxy-formats.md index 517d9c8de1..6da7d796ea 100644 --- a/docs-site/src/content/docs/ru/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ru/reference/proxy-formats.md @@ -190,6 +190,12 @@ Google преобразуют поддерживаемые запросы в Gem клиенты. Большинство запросов переводится в Responses, маршрутизируется обычным образом, а затем обратно в Anthropic JSON или Anthropic SSE. +Повторная передача reasoning в преобразуемых запросах Messages использует общий бюджет +преобразования запроса, включая копии при кодировании и декодировании. При превышении лимита +возвращается HTTP 413 с `translation_buffer_limit`; подписи и непрозрачные данные reasoning +не обрезаются для соблюдения лимита. Для нативного Anthropic passthrough действует отдельный +контракт ограничения размера тела. + Нативный Anthropic passthrough допустим только когда одновременно выполняются все условия: - native passthrough не отключён в конфигурации Claude Code; 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 347602a9d8..05f6352771 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -332,9 +332,19 @@ takma adlar ve eski yapılandırmalardan gelen `claude-ocx---` kimlikleri hala çözümlenir. Claude Desktop'ın altbilgi seçicisi zaten çalışan bir 3P görüşmesi için modeli -değiştirmezse, o görüşmede `/model ` komutunu kullanın. OpenCodex seçici -durumunu gözlemleyemez; her isteğin taşıdığı model kimliğini yönlendirir. Sonucu -**Logs → requestedModel** altında onaylayın. +değiştirmezse, `/model ` komutunu deneyebilirsiniz; ancak bu geçici çözüm de +etkilenen Desktop derlemelerinde başarısız olabilir. +[Sorun #3782](https://github.com/lidge-jun/opencodex/issues/3782), Windows üzerinde +Claude Desktop 1.46388.4 ile hem altbilgi seçicisi hem de `/model` üzerinden yapılan +değişikliklerden sonra görüşmenin ilk modelini kullanmaya devam ettiğini bildiriyor. +Bu bildirim, davranışa hangi istemci veya yönlendirme bileşeninin neden olduğunu +ortaya koymuyor. + +OpenCodex Claude Desktop profilinde istediğiniz varsayılan modeli seçmeyi, profili +yeniden uygulamayı ve yeni bir görüşme başlatmayı da deneyebilirsiniz. Bu bir sorun +giderme adımıdır; kesin çözüm değildir. OpenCodex seçici durumunu gözlemleyemez; +her isteğin taşıdığı model kimliğini yönlendirir. İstemcinin ne gönderdiğini +**Logs → requestedModel** altında kontrol edin. Yetkili 1M bağlam penceresine sahip modeller fazladan bir `…[1m]` seçici satırı alır: bunu seçmek Claude Code'un bu model için tam 1M bağlam hesabı yapmasını @@ -609,12 +619,14 @@ dönüştürür: | Asistan metni | `output_text` | | Asistan `tool_use` | `function_call` (`input` → JSON dizgeleştirilmiş `arguments`) | | Kullanıcı `tool_result` | `function_call_output` (`is_error` → `[tool error]` öneki) | -| `thinking` / `redacted_thinking` tekrarı | Bırakılır | +| `thinking` / `redacted_thinking` tekrarı | İmzaları ve gizli yükleri sınırlı `ocxr1` zarflarında taşıyan `reasoning` öğeleri | | Fonksiyon araçları | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, adlandırılmış fonksiyon→`{type:"function",name}`, barındırılan WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +Hedeflenen Anthropic adaptöründe gizlenmemiş imzalı bloklar (boş thinking dahil) ve opak redacted blokları korunur. `hideThinkingSummary` değişmez: yerel olarak gizlenen imzalı metin Claude istemcilerine gösterilmez; bu sınır üzerinden kayıpsız yeniden oynatma doğrulanmamıştır. Eski birleşik zarflarda metin akışla gönderildikten sonra özgün blok sırası geri getirilemez. `claudeCode.compatibility: "enforce"` thinking yeniden oynatmasını hâlâ reddeder. Bu, gerçek Anthropic kabulünü veya önbellek iyileşmesini kanıtlamaz; [#3719](https://github.com/lidge-jun/opencodex/issues/3719) açık kalır. + **Hata durumları (400):** hatalı biçimlendirilmiş JSON; eksik/boş `model`; eksik/boş `messages`; desteklenmeyen rol; `tool_use_id` içermeyen `tool_result`; kimlik/ad içermeyen `tool_use`; ad içermeyen adlandırılmış `tool_choice`. @@ -626,7 +638,8 @@ kimlik/ad içermeyen `tool_use`; ad içermeyen adlandırılmış `tool_choice`. | `response.created` | `message_start` + `ping` | | Kalp atışı (Heartbeat) | `ping` | | Metin farkları | `content_block_start` → `content_block_delta` (metin) → `content_block_stop` | -| Akıl yürütme özeti/metni | Sentetik imzalı `thinking` bloğu | +| Akıl yürütme özeti/metni | Tekrarlanan imzayı veya sınırlı bir `ocxr1` yedeğini taşıyan `thinking` bloğu | +| Gizli akıl yürütme | Akıl yürütme zarfından yeniden oynatılan `redacted_thinking` blokları | | Fonksiyon çağrısı çerçeveleri | `input_json_delta` ile `tool_use` bloğu | | Terminal olayı | `message_delta` → `message_stop` | | Terminalden önce EOF | 502 tarzı `api_error` | diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 02fae0f468..d13af52274 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -262,6 +262,14 @@ fonksiyon aracı olarak kodlar, ardından akışlı fonksiyon çağrısı yaşam Codex görmeden önce `custom_tool_call`'a geri yükler. Yerel OpenAI iletme yönlendirmesi ve desteklenen `apply_patch` özel aracı değişmeden kalır. +Yönlendirilen code-mode turlarına, ilk çağrıdan önce iç içe geçmiş yardımcılar için geçerli olan +ana makine kuralları da bildirilir: `tools.apply_patch`, yalnızca yama işaretçilerinden oluşan +satırlarla başlayan ve biten tek bir dize alır; isolate içinde `import` yoktur ve uzun süren +komutlar `write_stdin` üzerinden yoklanır. Yerel yönlendirilmiş Responses, Kiro veya Cursor yolundaki +bir code-mode exec sonucu hâlâ ana makinenin hata mesajlarından birini içeriyorsa opencodex, +ilgili kuralı belirten tek satırlık bir ipucu ekler. Bu değişiklik modelin kodunu veya yama metnini +yeniden yazmaz. + Seçilen sağlayıcı fonksiyon/araç çağrısını desteklemelidir. Araç çağrısı desteği olmayan salt metin bir sağlayıcı `exec`, Tarayıcı veya Bilgisayar Kullanımını kullanamaz. Yerel OpenAI satırları yukarı akış araç modunu değiştirmeden tutar. diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index fea4b37dd4..f068b0233f 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, Gajae Code, DeepSeek Harness ve MiniMax Code'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, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside ve Raycast'i opencodex'e bağlayın — istemci başına tek bir anahtar ve her yazmadan önce alınan bir yedek. --- **Entegrasyonlar** sekmesi, opencodex'in sağlayıcı bloğunu istemcinin kendi -yapılandırma dosyasına yazar ve tekrar kaldırır. Dokuz istemci bu şekilde +yapılandırma dosyasına yazar ve tekrar kaldırır. On üç 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 | @@ -18,6 +18,10 @@ yapılandırma dosyasına yazar ve tekrar kaldırır. Dokuz istemci bu şekilde | Gajae Code | `~/.gjc/agent/models.yml` | YAML | yeni oturumlarda veya `/model` açtığınızda | `OPENCODEX_GAJAE_API_KEY` | | DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml` (varsayılan `~/.dsh/settings.yaml`) | YAML | çalışırken yeniden yükleme | gizli olmayan geri döngü bearer yer tutucusu | | MiniMax Code | `~/.minimax/config.yaml` | YAML | yeni oturumlarda veya model seçici açıldıktan sonra | geri döngü (loopback) yer tutucusu | +| Prime Agent | `~/.prime/agent/models.json` | JSON | yeni oturumlarda | geri döngü yer tutucusu | +| ZCode | `~/.zcode/v2/config.json` | JSON | yeniden başlatmada | geri döngü yer tutucusu | +| Aside | `~/.aside/u//models.json` | JSON | Aside tamamen kapatılıp yeniden açıldıktan sonra | geri döngü yer tutucusu | +| Raycast | `~/.config/raycast/ai/providers.yaml` | YAML | kaydedildiği anda — Raycast dosyayı izler | yok — yalnızca geri döngü | Yönetilen DSH desteğinin en düşük uyumlu sürümü **DSH 0.1.0-rc.6**'dır. OpenCodex yalnızca `llm-pi-ai.providers.opencodex` bölümünü yönetir: Uygula ve Yenile bu bölümü değiştirir, Devre Dışı @@ -35,6 +39,39 @@ Entegrasyon yenilendiğinde model başına doğrulanmış bağlam pencereleri ve çabası seçenekleri de yenilenir; bilinmeyen yetenekler atlanır ve MCode oturumunun yönettiği geçerli çaba seçimi korunur. +Raycast'in iki ön koşulu vardır. Özel sağlayıcılar (Custom Providers) bir **Raycast Pro** +özelliğidir: ücretsiz planda dosya yine yazılır, ancak Raycast onu okumayacağı için +`ocx integration client status --client raycast` ve Entegrasyonlar sayfası bir uyarı +bildirir. Ayrıca Raycast `ai` klasörünü yalnızca Raycast → Settings → AI → +**Reveal Providers Config** seçeneğini bir kez açtığınızda oluşturur; opencodex bu +klasörü kurulum sinyali olarak kullanır ve klasör var olana kadar istemciyi kurulu değil +olarak bildirir. Raycast, `~/.config/raycast/ai/providers.yaml` dosyasını macOS ve +Windows'ta aynı şekilde okur ve `XDG_CONFIG_HOME` değerini dikkate almaz; bu nedenle bu +yol taşınamaz. + +Yönetilen blok, dosyanın `providers` dizisindeki tek bir öğedir: `id: opencodex`, +`name: OpenCodex`, `base_url: http://:/v1` ve `abilities` alanıyla birlikte +yönlendirilen her model — dışa aktarma kuralı olarak `tools` ve `system_message` değeri `true` olur, `vision` +kataloğun giriş modalitelerini izler, `reasoning_effort` modelin bir çaba merdiveni +varsa ayarlanır ve `temperature` akıl yürütme modelleri için kapatılır. Dosyadaki diğer +sağlayıcılar korunur ve devre dışı bırakma yalnızca OpenCodex öğesini kaldırır. Raycast +değişikliği dosya kaydedilir kaydedilmez, yeniden başlatma gerekmeden alır; modeller +Raycast'in model seçicisinde **OpenCodex** altında gruplanmış olarak görünür. Raycast şeması +isteğe bağlı `api_keys` alanını destekler; OpenCodex bu alanı bilerek yazmaz ve geri döngü +dışı veya kimlik doğrulaması gerektiren hedefleri reddeder. Bu entegrasyon OpenCodex'in +zorunlu kabul başlığını sağlayamaz. macOS'taki özel tercih yalnızca bir Pro ipucudur; +Windows bu tercihi hiç okumaz ve durumu bilinmiyor olarak bildirir. Bu bilgi yazmayı engellemez. +Dışa aktarılan meta veriler her modelin araç desteğini doğrulamaz. Diğer sağlayıcıların +değerleri korunur; YAML biçimlendirmesi ve yorumlarının korunması garanti edilmez. Format +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers) +adresinde belgelenmiştir. + +Raycast CLI dışa aktarmaları ve panel indirmeleri, yapılandırılmış kimlik doğrulamasız +geri döngü dinleyicisi dahil çalışan sunucunun adresini ve kabul politikasını kullanır. +`ocx ensure`, çalışan sunucudan farklı olabilecek kayıtlı yapılandırma kopyasıyla Raycast'i +yenilemez. Sunucu başlangıcı ve açık senkronizasyon katalog yenilemeye devam eder. + + Yollar, varsa her istemcinin kendi ortam geçersiz kılmalarını dikkate alır. OMP için `OMP_PROFILE`, açıkça boş olduğunda bile varlığıyla `PI_PROFILE`'a üstün gelir. Adlandırılmış bir profil, `PI_CONFIG_DIR`'i kullanıcının ev dizinine göre @@ -112,7 +149,7 @@ hiçbir şey sessizce değiştirilmez veya düşürülmez. **OMP** de yanındaki düzenlemelerden etkilenmez, ama başka bir nedenle: writer'ı yalnızca kendi `providers.opencodex` aralığını bayt bayt yamalar, dosyanın geri kalanı hiçbir zaman yeniden yazılmaz. Yorum taşıyabilen diğer biçimlerde (Hermes, OpenClaw, -Kimi Code, Gajae Code, MiniMax Code — bütün belge olarak yazılan YAML, JSON5 ve TOML) veya +Kimi Code, Gajae Code, MiniMax Code, Raycast — bütün belge olarak yazılan YAML, JSON5 ve TOML) veya kendi girdilerimiz düzenlenmişse, anahtar kilitlenir ve hangi düzenlemelerin size ait olduğunu tahmin etmek yerine devre dışı bırakmayı reddeder. @@ -192,10 +229,12 @@ ocx integration client enable --client mcode ocx mcode ``` -Bağlandıktan sonra `ocx sync`, yönetilen MCode bloğunu güncel bağlam pencereleri ve -akıl yürütme çabası seçenekleriyle de yeniler. Eksik, dışarıdan düzenlenmiş, güvenli -olmayan veya hiç sahiplenilmemiş bloklara dokunmaz; yeniden bağlamak istediğinizde -entegrasyonu açıkça yeniden etkinleştirin. +Bağlandıktan sonra `ocx sync` ve `POST /api/sync`, yönetilen MCode, Pi, Aside ve +Raycast kataloglarını yeniler. Proxy başlangıcı da yönetilen Raycast kataloğunu +yeniler. Model görünürlüğü, sağlayıcı veya ön ayar değişiklikleri Pi, Aside ve +Raycast kataloglarını günceller. Eksik, dışarıdan düzenlenmiş, güvenli olmayan +veya elle kaldırılmış bloklara dokunmaz; yeniden bağlamak istediğinizde +entegrasyonu açıkça etkinleştirin. Ayrı MiniMax platform CLI'si (`mmx`) bir dosya anahtarı entegrasyonu değildir. Metin komutları MiniMax'ın Anthropic uyumlu uç noktasını kullandığı için OpenCodex, diff --git a/docs-site/src/content/docs/tr/guides/model-ordering.md b/docs-site/src/content/docs/tr/guides/model-ordering.md index 336513225a..ea42630154 100644 --- a/docs-site/src/content/docs/tr/guides/model-ordering.md +++ b/docs-site/src/content/docs/tr/guides/model-ordering.md @@ -131,7 +131,7 @@ yeniden sıralamaktır. Kontrol panelinin **Alt Ajanlar** sayfası yalın yerel yönlendirilen kimlikleri yeniden sıralayabilir. Tam `/` seçimleri için `ocx agent subagents set` kullanın veya opencodex yapılandırmasını düzenleyin; kontrol paneli bu seçimleri -listelemez ve kadroyu kaydederse bunları atlar. En fazla beş yapılandırılmış +önceden kaydedilen kimlikleri, kullanılamasalar bile korur. En fazla beş yapılandırılmış kimlik kullanın. Hesap seçicileriyle tek bir yalın yerel seçenek birden çok seçici nitelikli katalog satırına genişleyebilir, bu nedenle yapılandırılmış seçimler ve tanıtılan satırlar birebir olmak zorunda değildir. @@ -181,3 +181,11 @@ tanıttığı listeyi yeniden sıralamaz. `disabledModels` ve her sağlayıcının `selectedModels` alanı görünürlüğü denetler. Ayrı bir `modelOrder`, `providerOrder` veya öncelik haritası ayarı yoktur. + +## Kontrol paneli sıra önayarları + +**Models** sayfasında Varsayılan, Model adına göre A–Z, Sağlayıcıya göre veya Kullanım anlık görüntüsünü seçip sırayı uygulayın. Kullanılabilir yönlendirilmiş kimlikler ve `modelPickerOrderMode` (`alphabetical`, `provider`, `most-used`) kaydedilir. Saklanan tüm kullanım yalnızca uygulamada bir kez okunur; yeniden yükleme veya model değişiklikleri yeniden hesaplamaz. Özel ve tam yerel sıra açıkça değiştirilene kadar korunur. Varsayılan, kullanılabilir model yokken bile iki alanı temizler. + +`GET/PUT /api/subagent-models`, devre dışı veya eksik kayıtlı seçimleri `chosen` ve `available` içinde korur; `pickerAvailable` uygun yönlendirilmiş kimliklerdir. Models yalnızca `pickerOrder` ve `pickerOrderMode` gönderir, `models` göndermez. Yalnız kadroyu kaydetmek sırayı değiştirmez. Geçersiz giriş veya kayıt hatası önceki durumu korur. + +Öne çıkan ve yerel öncelik aralıkları korunur. Sıra Codex kataloğu ve Claude keşfinin yönlendirilmiş gruplarına uygulanır; Claude yerel öneki, açık Desktop profilleri ve alias sahipliği korunur. OpenCodex rehberlik sıraları ve fallback ayarları değişmez; yerel Codex aracının sunduğu ilk beş seçenek ve önerilen varsayılan değişebilir. Kayıt istemcileri yeniden başlatmaz; katalog yenilemesi bekleyebilir ve eski kataloğu gösteren istemcinin yeniden açılması gerekebilir. diff --git a/docs-site/src/content/docs/tr/guides/pi.md b/docs-site/src/content/docs/tr/guides/pi.md index 0741f7be51..fe6044de28 100644 --- a/docs-site/src/content/docs/tr/guides/pi.md +++ b/docs-site/src/content/docs/tr/guides/pi.md @@ -31,6 +31,9 @@ export line, and how many models carry authoritative context limits. "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -45,6 +48,8 @@ export line, and how many models carry authoritative context limits. } ``` +Oluşturulan Pi sağlayıcılarında `compat.sendSessionAffinityHeaders` etkinleştirilir. Sağlayıcıyı birleştirirken veya elle düzenlerken bu ayarı koruyun: Pi sabit bir oturum kimliği gönderir ve OpenCodex bu kimlikten kanonik OpenCode Go hedefi için oturum yakınlığı üretir. `cacheRetention` değeri `none` olduğunda Pi kimliği göndermeyebilir. + Model ids are the proxy's canonical selectors, so routed models appear as `provider/model` (`anthropic/claude-opus-5`) and native OpenAI slugs stay unprefixed diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index e6a6dd5f1f..39ca117739 100644 --- a/docs-site/src/content/docs/tr/guides/providers.md +++ b/docs-site/src/content/docs/tr/guides/providers.md @@ -138,6 +138,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | Deneysel PKCE girişi, canlı HTTP/2 aktarımı ve hesap filtreli model keşfi. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Deneysel. GitHub cihaz akışı + `copilot_internal` değişimi (VS Code OAuth istemcisi). Aktif bir Copilot aboneliği gerektirir; resmi bir üçüncü taraf API değildir. | +Google Antigravity hesap ve sağlayıcı kota sorguları, model listesine geri dönüş dahil sabit Google uç noktalarını kullanır. Bu hedefler için şeffaf Fake-IP DNS desteklenirken TLS doğrulaması, yönlendirme reddi ve özel adres kontrolleri korunur. Özel base URL yalnızca model isteklerini değiştirir; `NO_PROXY` doğrudan bağlantı politikasını korur. + + Uç bir Nous yenileme hatasından sonra yeniden kimlik doğrulamak için `ocx login nous` çalıştırın. @@ -352,6 +355,7 @@ yalnızca Cline IDE/CLI içinde mevcuttur; `minimax/minimax-m2.5` belgelenmiş A | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Kodlama) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (statik model listesi)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token planı (varsayılan): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · Kullandıkça öde: `https://dashscope.aliyuncs.com/compatible-mode/v1` · veya Özel | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -536,7 +540,7 @@ tutarsız faturalandırma toplamları yanıltıcı bir çubuk yerine hiçbir rap > **Tencent Cloud Coding Plan kullanım kısıtlaması:** Tencent bu aboneliği yalnızca etkileşimli kodlama araçları için belgeler. Genel API otomasyonu, özel uygulama arka uçları ve etkileşimsiz toplu kullanım yasaktır ve plan anahtarının askıya alınmasına neden olabilir. -> **İki GLM rotası:** `zai`, Z.AI uluslararası kodlama planı aboneliğidir; `zhipu-bigmodel`, Zhipu'nun yerel BigModel kullandıkça öde uç noktasıdır. Farklı ana bilgisayarlar, farklı anahtarlar, farklı faturalandırma — biri için verilen bir anahtar diğerine karşı kimlik doğrulaması yapmaz. +> **GLM faturalandırma rotaları:** `zai`, Z.AI uluslararası kodlama planı aboneliğidir; `zhipu-bigmodel`, Zhipu'nun yerel BigModel kullandıkça öde uç noktasıdır. Farklı ana bilgisayarlar, farklı anahtarlar, farklı faturalandırma — biri için verilen bir anahtar diğerine karşı kimlik doğrulaması yapmaz. ### Birden fazla API anahtarı @@ -587,8 +591,8 @@ login github-copilot`). **GitLab Duo**, OpenAI uyumlu uç noktasında bir anahtar/abonelik belirteci ağ geçidi olarak kalır. **Cloudflare AI Gateway**, URL'ye doldurulan hesap + ağ geçidi kimliklerinize ihtiyaç duyar. -Copilot karma hatlı bir katalog sunar: GPT-5 ailesi (`gpt-5.3-codex`, `gpt-5.4`, -`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) ajan +Copilot karma hatlı bir katalog sunar: modeller (`gpt-5.3-codex`, `gpt-5.4`, +`gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) ajan trafiği için `/chat/completions`'ı reddeder, bu nedenle opencodex yerleşik varsayılan olarak bu modelleri Responses API üzerinden yönlendirirken diğer tüm Copilot modelleri sohbet tamamlamalarında kalır. Öncelik sırası: sabit hat diff --git a/docs-site/src/content/docs/tr/guides/remote-hub.md b/docs-site/src/content/docs/tr/guides/remote-hub.md index eeb97bdfaf..6499325908 100644 --- a/docs-site/src/content/docs/tr/guides/remote-hub.md +++ b/docs-site/src/content/docs/tr/guides/remote-hub.md @@ -60,14 +60,38 @@ Döndürme sırasında eski ve yeni anahtar aynı `apiKeyId` altında en fazla o ## Docker ve sorun giderme -Resmî Docker imajı yoktur; ancak depo, digest ile sabitlenmiş Bun imajını yerelde oluşturmak için bakımı yapılan bir `Dockerfile` ve `compose.yaml` sağlar. İlk normal başlatma, volume başına kendinden imzalı bir TLS kimliği oluşturur; herkese açık sertifika `/home/bun/.opencodex/container-tls/cert.pem`, özel anahtar ise aynı dizindedir ve yalnızca sahibi tarafından okunabilir. Veri anahtarını ilk başlatmadan önce stdin üzerinden bir kez başlatın. Yardımcı en fazla 512 baytlık tek satır kabul eder, anahtarı yazdırmaz ve `ocx-state` volume içindeki owner-only `service-api-token` dosyasını değiştirmeyi reddeder. - -Host üzerinde Git ve Bun gereklidir. Her imaj derlemesinden önce Git tarafından izlenen kaynaklardan kanonik manifesti üretin ve derleme bitene kadar kaynakları değiştirmeyin. Üretilen JSON dosyasını Git'e eklemeyin; `.git` Docker bağlamının dışında kalır. HTTPS host portu varsayılan olarak `127.0.0.1:10100` adresine bağlanır. `OPENCODEX_PORT=10190` hem yayınlanan host portunu hem de yönetilen `tls.publicOrigin` değerini `https://localhost:10190` yapar; konteyner içindeki port yine `10100` kalır. - -Manifest; `Dockerfile`, `compose.yaml`, `.dockerignore`, Git tarafından izlenen tüm `docker/` yetki dosyaları, `src/`, `package.json`, `bun.lock` ve `scripts/model-metadata.source.json` dosyalarını doğrular. Derleme her SHA-256 değerini önce bağlamdaki, ardından kopyalanan dosyalardaki baytlarla karşılaştırır; eksik veya uyuşmayan dosyaları, sembolik bağlantıları ve manifestte bulunmayan ek `src/` ya da `docker/` yetki dosyalarını reddeder. +Geri alırken iki volume'u ve bağlama yollarını koruyun. Mevcut volume sahipliği ve izinleri otomatik düzeltilmez. Compose dışındaki adlandırılmış bağlamalar ve özel durum yolları için [ana kılavuza](/guides/remote-hub/#docker-compose) bakın. + +Durum iki ayrı kalıcı volume'da tutulur: `ocx-state`, +`OPENCODEX_HOME=/home/bun/.opencodex` yoluna; `codex-state` ise +`CODEX_HOME=/home/bun/.codex` yoluna bağlanır. İki ürünün `auth.json` biçimleri +uyumsuzdur; bu dizinleri birleştirmeyin. Kök dosya sistemi salt okunur olsa da +bu iki volume yazılabilir durumda kalır. + +Katalog otomatik oluşturulmaz. Kimlik doğrulamalı `/v1/catalog` kontrolünden önce +`/home/bun/.codex/opencodex-catalog.json` konumunda geçerli bir katalog oluşturun +veya içe aktarın. Boş dizinde 404 `catalog_not_found` beklenen sonuçtur. Güncelleme +mevcut `ocx-state` volume'unu korur ve `codex-state` ekler; dosyaları otomatik taşımaz. +Önceden `.opencodex` içine konmuş kataloğu yedekleyin ve yalnızca katalog dosyasını, +sadece sahibine erişim veren izinlerle taşıyın. Bir ürünün `auth.json` dosyasını +diğerininkiyle değiştirmeyin. `CODEX_HOME` özelleştirilirse bu dizinin tam yolunu +yazılabilir bir volume'a bağlayın ve varsayılan kataloğu +`${CODEX_HOME}/opencodex-catalog.json` konumuna koyun. `model_catalog_json` başka +bir dosya seçiyorsa çözümlenen yol da kalıcı olmalıdır. Açık bir taşıma tamamlanana +kadar mevcut özel ortam ve volume eşlemesini koruyun. +`docker compose down` iki volume'u da korur; `docker compose down --volumes` hem +`ocx-state` hem `codex-state` ile birlikte kimlik bilgilerini, kullanım geçmişini, +veri anahtarını ve Codex durumunu/kataloğunu siler. Güncelleme veya yeniden başlatma +yerine kullanılmamalıdır. + +Resmî Docker imajı yoktur; ancak depo, digest ile sabitlenmiş Bun imajını yerelde oluşturmak için bakımı yapılan bir `Dockerfile` ve `compose.yaml` sağlar. İlk başlatmadan önce veri anahtarını stdin üzerinden bir kez başlatın; anahtar yazdırılmaz ve `ocx-state` volume içinde yalnızca sahibinin okuyabileceği izinlerle saklanır. + +Host üzerinde Git ve Bun gereklidir. Her imaj derlemesinden önce Git tarafından izlenen kaynaklardan kanonik manifesti üretin ve derleme bitene kadar kaynakları değiştirmeyin. Üretilen JSON dosyasını Git'e eklemeyin; `.git` Docker bağlamının dışında kalır. Host portu varsayılan olarak `127.0.0.1` adresine bağlanır. Uzak erişim için açıkça `OPENCODEX_BIND_ADDRESS= docker compose up -d` kullanın; `0.0.0.0` tüm arayüzleri açar. Erişimi güvenlik duvarı ve kimlik doğrulamalı TLS/tailnet ön ucu ile koruyun. + +Derleme, her SHA-256 değerini önce bağlamdaki, ardından kopyalanan dosyalardaki baytlarla karşılaştırarak eski manifestleri reddeder. Eksik veya uyuşmayan dosyalar, fazladan kaynak dosyaları ve sembolik bağlantılar reddedilir. `package.json`, `bun.lock` ve `scripts/` içinden yalnızca dahil edilen `scripts/model-metadata.source.json` zorunludur. ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -75,28 +99,7 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -Varsayılan HTTPS uç noktasını doğrulamak için yalnızca açık sertifikayı dışarı kopyalayın ve CA olarak kullanın: - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -Konteyner içi liveness/readiness kontrolleri sertifika doğrulamasını yalnızca sabit konteyner loopback hedefinde devre dışı bırakabilir. Harici kabul testleri ise tam host adını kopyalanmış açık sertifika veya sistem güven deposuyla doğrulamalıdır. - -Uzak erişim açıkça etkinleştirilmelidir: `OPENCODEX_BIND_ADDRESS= docker compose up -d`; `0.0.0.0` tüm arayüzleri açar. Üretilen sertifika yalnızca `localhost` ve `127.0.0.1` için geçerlidir. Yerel yayını koruyup kimlik doğrulamalı bir TLS/tailnet ön ucu kullanın veya uzak ad için özel sertifika/anahtar yollarını ve tam HTTPS `tls.publicOrigin` değerini yayına açmadan önce ayarlayın. Her iki durumda da güvenlik duvarı kullanın. - -Saklanan TLS öncesi bir volume, sonraki `docker compose up -d` sırasında volume kimliği ve `OPENCODEX_PORT` tabanlı HTTPS origin eklenerek otomatik taşınır; operatörün özel sertifika yolları korunur. Eski, yalnızca HTTP imajına geri dönmeden önce mevcut imajla yalnızca TLS ayarını kaldırın; kimlik dosyaları volume içinde kalabilir: - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# eski imajı seçin/derleyin, sonra hub'ı yeniden oluşturun -docker compose up -d -``` - -Konteyner root olmayan `bun` kullanıcısıyla, salt okunur kök dosya sistemiyle çalışır ve yalnızca `10100` portunu yayımlar. `10101` portunu yayımlamayın ve sırları `ARG`, `ENV`, `COPY`, Compose, imaj geçmişi veya argv içine koymayın. Healthcheck sonrasında HTTPS `/readyz`, kimlik doğrulamalı katalog ve gerçek yanıtı ayrıca doğrulayın. `docker compose down` volume'u korur; `docker compose down --volumes` yapılandırmayı, TLS kimliğini, kimlik bilgilerini ve veri anahtarını da siler. +Konteyner root olmayan `bun` kullanıcısıyla, salt okunur kök dosya sistemiyle çalışır ve yalnızca `10100` portunu yayımlar. `10101` portunu yayımlamayın ve sırları `ARG`, `ENV`, `COPY`, Compose, imaj geçmişi veya argv içine koymayın. Healthcheck sonrasında readiness, kimlik doğrulamalı katalog ve gerçek yanıtı ayrıca doğrulayın. `docker compose down` volume'u korur; `docker compose down --volumes` yapılandırmayı, kimlik bilgilerini ve anahtarı da siler. - Hub kapalıysa yerel geri dönüş yapılabilir; uzaktaki anahtarın iptali bekler. - Geçici arızada doğrulanmış LKG korunur; auth, şema, boyut veya protokol hatasında yerel fallback yoktur. 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 8553607895..d8c2235b1b 100644 --- a/docs-site/src/content/docs/tr/reference/cli/agents.md +++ b/docs-site/src/content/docs/tr/reference/cli/agents.md @@ -191,7 +191,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 @@ -203,7 +203,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. | @@ -233,6 +233,18 @@ için kendi varsayılanlarını uygular) gelir. | `mcode` | `~/.minimax/config.yaml` (ayarlandığında `MINIMAX_DATA_DIR`, ardından eski `MAVIS_DATA_DIR` öncelikli; göreli değer reddedilir) | `mcode-config.yaml` | yok — geri döngü yer tutucusu | | `zcode` | `~/.zcode/v2/config.json` (ayarlandığında `ZCODE_DATA_DIR` öncelikli; göreli değer reddedilir) | `config.json` | yok — geri döngü yer tutucusu | | `prime` | `~/.prime/agent/models.json` (ayarlandığında `PRIME_AGENT_CODING_AGENT_DIR` öncelikli; göreli değer reddedilir) | `prime-models.json` | yok — geri döngü yer tutucusu | +| `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 | + +Raycast dışa aktarımı, `providers` dizisinde tek bir `id: opencodex` öğesi içeren bağımsız +bir `providers.yaml` belgesidir: `name: OpenCodex`, proxy'nin `/v1` temel URL'si ve +`abilities` alanıyla birlikte yönlendirilen her model (`tools` ve `system_message` her +zaman destekli, `vision` kataloğun giriş modalitelerinden, `reasoning_effort` modelin bir +çaba merdiveni varsa, `temperature` akıl yürütme modelleri için kapalı). Özel sağlayıcılar +bir Raycast Pro özelliğidir ve Raycast dosyayı izlediği için kaydedilen bir değişiklik +yeniden başlatma gerekmeden etkili olur. Format +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers) +adresinde belgelenmiştir. Hiçbir `api_keys` girdisi yazılmaz; bu yüzden bu dışa aktarım +yalnızca geri döngü içindir ve geri döngü dışı bir bağlama reddedilir. opencode `{env:OPENCODEX_OPENCODE_API_KEY}` değerini enterpole eder. Üretilen Pi ve OMP dışa aktarımları bir ortam değişkeni gerektirmez: her biri değişmez diff --git a/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md b/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md index 01a844606a..cc357a27c2 100644 --- a/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/tr/reference/cli/providers-accounts.md @@ -16,7 +16,7 @@ bir ad hem `--adapter` hem de `--base-url` gerektirir. | Alt komut | Desteklenen bayraklar | Eylem | | --- | --- | --- | -| `list` | `--json` | Yapılandırılmış sağlayıcıları ve kalan kayıt defteri girdilerini listeleyin. | +| `list` | `--json`, `--jsonl` | Yapılandırılmış sağlayıcıları ve kalan kayıt defteri girdilerini listeleyin. `--jsonl`, yapılandırılmış her sağlayıcı için satır başına bir JSON nesnesi üretir. | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | Bir kayıt defteri/özel sağlayıcı ekleyin. `--force` üzerine yazar; `--sync`, insan çıktısı modunda çalışan bir proxy'yi yeniler. | | `edit ` | sağlayıcı alan bayrakları, `--headers `, `--json` | Anahtar havuzlarını değiştirmeden doğrulanmış canlı sağlayıcı alanlarını düzenleyin. `--headers` özel istek başlıklarını birleştirir; temizlemek için `{}` veya `-` iletin. | | `test ` | `--json` | Gerçek yukarı akış model uç noktasını araştırın. | @@ -30,6 +30,7 @@ bir ad hem `--adapter` hem de `--base-url` gerektirir. ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -38,6 +39,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` yalnızca yapılandırılmış sağlayıcıları, her satırda bir JSON nesnesi olacak şekilde yazar. Her nesne, `--json` çıktısındaki `configured` dizisinin bir öğesiyle aynı alanları içerir; `registryCount` özeti eklenmez. Betikler nesneleri satır satır işleyebilir. `--json` ve `--jsonl` birlikte kullanılamaz. + :::caution[Özel başlıklar bir kimlik bilgisi kanalı değildir] `--headers`, gizli olmayan istek meta verileri içindir — yönlendirme ipuçları, kiracı veya proje seçicileri, izleme kimlikleri. Kimlik doğrulama materyali @@ -229,13 +232,11 @@ eşleşen null veya eski bir rapora düşer (çıkış 0). ### `ocx account auto-switch > [--json]` -Yalnızca `openai` Codex hesap havuzunu denetler. `on` %80'i ayarlar, `off` %0'ı -ayarlar, `status` geçerli değeri okur ve `threshold ` 0 ile 100 arasında bir -tamsayı kabul eder. Diğer sağlayıcılar ve geçersiz değerler 1 ile çıkar. -`--json` şunu döndürür: +`openai` Codex havuzunun eşiğini yönetir veya genel OAuth havuzunun eşiğini kaydeder. `on` %80, `off` %0 kaydeder; `threshold ` 0–100 kabul eder. Genel havuz eşikleri şu anda uygulanmaz: kayıt işlemi eşik tabanlı geçişi, sağlayıcının etkinlik ayarını veya 429 hatasından sonraki otomatik hesap değişimini etkilemez. Genel havuz çıktısı sunucunun doğruladığı değerleri kullanır. Genel havuzlarda `poolEnabled`, kaydedilmiş sağlayıcı ayarıdır (`null` belirtilmemiş demektir); devralınmış etkin durumu göstermez. `inert: true`, eşiğin uygulanmadığını belirtir; yetenek bilinmiyorsa `enabled: true` bildirilmez. API anahtarlı sağlayıcılar, Anthropic ve geçersiz değerler reddedilir. ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 274664c323..4fca005ea6 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -119,7 +119,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `modelAutoCompactTokenLimits?` | `Record` | Model başına pozitif güvenli tamsayı biçiminde yumuşak otomatik sıkıştırma bütçeleri. Değerler yalnızca bağlamın veya maksimum girdinin etkin %90 zarfını düşürebilir ve yetkili bir bağlam penceresi bilinmiyorsa yayımlanmaz. Canonical `openai` için anahtarlar, sağlayıcı veya hesap seçici öneki olmadan desteklenen tam yerel model kimlikleri olmalıdır. Sağlayıcı PATCH girdileri birleştirir; bir anahtarı `null` yapmak o anahtarı siler, alanın tamamını `null` yapmak haritayı temizler. Bu `null` silme işaretleri yalnızca PATCH içindir. | | `defaultMaxOutputTokens?` | `number` | İstemci `max_output_tokens` değerini atladığında sağlayıcı genelinde `openai-chat` geri dönüşü. | | `modelMaxOutputTokens?` | `Record` | Pozitif model başına `openai-chat` geri dönüş bütçeleri; tam/kalıp eşleşmeleri sağlayıcı varsayılanını yener. | -| `modelCosts?` | `Record` | Sağlayıcının tam yukarı akış model kimliğine göre anahtarlanan model başına görüntüleme fiyatları (1M token başına USD) — bir sağlayıcı tanımlayıcısı veya yönlendirilen `provider/model` etiketi değil, örn. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Herhangi bir model kimliği geçerli bir anahtardır — özel sağlayıcılar `openai-chat` adaptörü aracılığıyla herhangi bir OpenAI uyumlu uç noktayı hedefleyebilir ve yerel veya dahili sağlayıcı kimlikleri yerleşik kataloglarda bulunmasalar bile çalışır. Kullanıcı tarafından yapılandırılan fiyatlar Günlükler `~$` ve Kullanım tahminlerinde yerleşik katalogları yener; geçmiş girdiler geçerli katmandan yeniden fiyatlandırılır, bu nedenle bir fiyatı düzenlemek geçmiş toplamları değiştirebilir. Geri dönüş sırası: kullanıcı `modelCosts` → jawcode kataloğu → beklenen fiyat katmanı → model düzeyinde satıcı geri dönüşü ve tamamen sıfır bir girdi bu dizideki bir sonraki kaynağa düşer. Her oran en fazla 1.000.000 (1M token başına USD) olan negatif olmayan sonlu bir sayı olmalıdır; aralık dışı satırlar yönetim sınırı tarafından reddedilir ve yükleme sırasında bırakılır. Yalnızca görüntüleme zamanı tahmini: katmanlar yönlendirmeyi, hesap seçimini, kotaları veya faturalandırmayı asla etkilemez. | +| `modelCosts?` | `Record` | Sağlayıcının tam yukarı akış model kimliğine göre anahtarlanan model başına görüntüleme fiyatları (1M token başına USD) — bir sağlayıcı tanımlayıcısı veya yönlendirilen `provider/model` etiketi değil, örn. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Herhangi bir model kimliği geçerli bir anahtardır — özel sağlayıcılar `openai-chat` adaptörü aracılığıyla herhangi bir OpenAI uyumlu uç noktayı hedefleyebilir ve yerel veya dahili sağlayıcı kimlikleri yerleşik kataloglarda bulunmasalar bile çalışır. Kullanıcı tarafından yapılandırılan fiyatlar Günlükler `~$` ve Kullanım tahminlerinde yerleşik katalogları yener; geçmiş girdiler geçerli katmandan yeniden fiyatlandırılır, bu nedenle bir fiyatı düzenlemek geçmiş toplamları değiştirebilir. Geri dönüş sırası: kullanıcı `modelCosts` → jawcode kataloğu → beklenen fiyat katmanı → model düzeyinde satıcı geri dönüşü ve kullanıcının açıkça sıfır olarak belirlediği oranlar bilinen sıfır maliyetli bir tahmin üretir; otomatik fiyatlandırmaya dönmek için model girdisini silin. Tamamen sıfır katalog fiyatları bir sonraki kaynağa geçmeye devam eder. Her oran en fazla 1.000.000 (1M token başına USD) olan negatif olmayan sonlu bir sayı olmalıdır; aralık dışı satırlar yönetim sınırı tarafından reddedilir ve yükleme sırasında bırakılır. Yalnızca görüntüleme zamanı tahmini: katmanlar yönlendirmeyi, hesap seçimini, kotaları veya faturalandırmayı asla etkilemez. | | `headers?` | `Record` | Ek yukarı akış başlıkları. Yetkilendirme, çerezler, API anahtarı başlıkları, gömülü yeni satırlar ve geçersiz adlar reddedilir. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Varsayılan OpenRouter `order`, `only` ve `allowFallbacks` tercihleri; yalnızca `openai-chat` ile kurallı OpenRouter için geçerlidir. | | `modelOpenRouterRouting?` | `Record` | Sağlayıcı genelindeki OpenRouter tercihinin yerini alan tam model kimliği geçersiz kılmaları. | @@ -131,7 +131,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `modelReasoningEfforts?` | `Record` | Model başına etiketler. Boş bir liste çaba denetimini gizler. `reasoningEfforts`'ta olduğu gibi, yapılandırılmış her `google` adaptör merdiveni `thinkingLevel` yeteneğini iddia eder; doğrudan ve Vertex görsel olmayan istekleri düz Gemini yolunu kullanırken, Cloud Code Assist bunu istek zarfı altında gönderir. | | `modelSupportsReasoningSummaries?` | `Record` | Özetlerin bildirilmesini durdurmak ve özet teslim alanlarını kaldırmak için bir modeli `false` olarak ayarlayın. | | `modelReasoningSummaryDelivery?` | `Record` | Model başına Responses teslim enum'ı; mevcut bir teslim alanını yeniden yazar. | -| `modelAdapters?` | `Record` | Karışık hatlı ağ geçitleri için model başına `openai-chat` veya `openai-responses` hat geçersiz kılma. Açık girdiler kayıt defteri varsayılanlarını yener. OpenCode Go önayarı, kardeş modelleri belgelenmiş hatlarında bırakırken `gpt-5.6-luna` için Responses'ı seçer; DeepSeek, `deepseek-v4-flash` için yerel Responses seçebilir; ve GitHub Copilot, GPT-5 ailesi (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) için yalnızca Responses varsayılanlarını bildirir çünkü bu modeller ajan trafiği için `/chat/completions`'ı reddeder. Yerleşik varsayılanı olmayan modeller (örneğin `gpt-5.4-nano`) burada dahil edilebilir. Tek hatlı yukarı akış pinleri ve kurallı ChatGPT iletme geçersiz kılmaları reddeder. | +| `modelAdapters?` | `Record` | Karışık hatlı ağ geçitleri için model başına `openai-chat` veya `openai-responses` hat geçersiz kılma. Açık girdiler kayıt defteri varsayılanlarını yener. OpenCode Go önayarı, kardeş modelleri belgelenmiş hatlarında bırakırken `gpt-5.6-luna` için Responses'ı seçer; DeepSeek, `deepseek-v4-flash` için yerel Responses seçebilir; ve GitHub Copilot, modeller (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`) için yalnızca Responses varsayılanlarını bildirir çünkü bu modeller ajan trafiği için `/chat/completions`'ı reddeder. Yerleşik varsayılanı olmayan modeller (örneğin `gpt-5.4-nano`) burada dahil edilebilir. Tek hatlı yukarı akış pinleri ve kurallı ChatGPT iletme geçersiz kılmaları reddeder. | | xAI Responses katılımı (panel) | anahtar | Yalnızca `xai` için `grok-4.5` ve `grok-4.6` `modelAdapters` girdilerini atomik olarak ayarlar veya temizler. Tek girdi, sonraki anahtar yazımı ikisini eşitleyene kadar karma durum olarak görünür. Diğer geçersiz kılmalar ve katman davranışı değişmez. | | `xaiResponsesXSearch?` | `boolean` | Varsayılan olarak devre dışıdır. Bir xAI Responses hedefinde, yalnızca canlı bir `web_search` aracı son istek normalleştirmesinden sağ çıktığında sağlayıcı tarafından barındırılan `x_search` bildirimini ekler. Mevcut bildirimler yinelenmez, çağıranın `tool_choice`/`allowed_tools` seçicileri hiçbir zaman genişletilmez ve bu, web araması yardımcı hizmetinin `search.xSearch` seçeneklerinden ayrıdır. | | `modelPreferHostedTools?` | `Record` | Barındırılan bir araç ad alanı ayıran iletme harici Responses ağ geçitleri için tam model dahil etme. Şu anda yalnızca `["image_generation"]` kabul eder; eşleşen bir model `openai-responses` hattını kullanmalı ve bu barındırılan aracı desteklemelidir. Çakışan istemci `image_gen` bildirimlerini kaldırır ve arayan araç seçimini korumak için seçicilerini yeniden yazar. OpenAI API sanal `-pro` modelleri için önce seçilen genel kimlik eşleştirilir ve çözümlenen temel hat model kimliği bir geri dönüştür. `modelAdapters` önce genel kimliği, ardından temel kimliği çözer; ikinci çözümleme son hattı belirler. Diğer modeller normal takma ad davranışını korur. | @@ -530,6 +530,23 @@ bildirir; senkronize edilen katalog `xhigh`'ı ayrı tutarken `max` bildirir. } ``` +## Model görünen adı düzenleyicisi + +Kontrol panelindeki **Models**, keşfedilen modeller için okunabilir adları kalıcı olarak kaydetmenizi sağlar. Sağlayıcıyı genişletin, keşfedilen +bir modeli bulun ve **Name** seçeneğini seçin. Okunabilir bir etiket kaydederken iletişim kutusu +tam `provider/model` seçicisini görünür tutar. Sağlayıcı meta verilerine veya varsayılan seçici +gösterimine dönmek için **Reset name** seçeneğini seçin. **Name** yalnızca görünümü değiştirir; +ayrı takma ad kalemi kısa yönlendirme takma adını değiştirir ve bir görünen ad düzenleyicisi +değildir. Yerel OpenAI ve özel model satırları mevcut kontrollerini korur. + +Değişiklik kaydedildiği halde yenileme başarısız olursa iletişim kutusu kaydedilen geçersiz kılma +değerini yansıtır ve **Retry** kullanılabilir kalır. Sunucu katalog yakınsamasının başarısız +olduğunu bildirdiyse Retry bu işlemi tekrarlar; yalnızca liste isteği başarısız olduysa listeyi +yeniden yükler. Sıfırlama sonrası kurtarma, sıfırlama işlemini korur ve eski adı geri getirmez. +İsteklerin, yazma işlemini ve ardından gelen liste yenilemesini kapsayan 60 saniyelik bir süresi +vardır. Zaman aşımı yazma işlemini geri almaz: başka bir değişiklik yapmadan önce **Retry** ile +geçerli adı kontrol edin. + ## Tam örnek ```json diff --git a/docs-site/src/content/docs/troubleshooting/windows-memory.md b/docs-site/src/content/docs/troubleshooting/windows-memory.md index cea5b3c233..10bc3799cc 100644 --- a/docs-site/src/content/docs/troubleshooting/windows-memory.md +++ b/docs-site/src/content/docs/troubleshooting/windows-memory.md @@ -57,7 +57,22 @@ runtime the leak itself remains an upstream problem: time show whether failures are accumulating or recovering in the same process. The last failure is a fixed privacy-safe class such as `EACCES`, `ENOSPC`, `ETIMEDOUT`, or `EACLRETRYEXHAUSTED`; raw error messages and filesystem paths - are never returned. These diagnostics stay on the authenticated management + are never returned. `spillLastWriteFailureOrigin` adds a fixed origin or null: + `retry_returned_timeout` means the existing second spill attempt returned a + timeout; `timeout_memo_refusal` means the ACL helper refused through its + remembered timeout state. Other failures use null. The cumulative + `spillAclRetryReturnedTimeouts` and `spillAclTimeoutMemoRefusals` count terminal + failed publications, not individual ACL commands or transient first attempts. + Success clears the failure streak but retains the last failure fields and + cumulative counts; a later unrelated failure sets the last origin to null. + These values are process-local, so compare snapshots from the same process. + Neither origin identifies an OS command: the attempt budget can expire before + a command starts, and an optional compliance inspection can run before a memo + refusal. A separate process succeeding does not prove that the live process's + memo recovered. These observations do not add retries, clear memos, weaken + required ACLs, or automatically restart the service. + + These diagnostics stay on the authenticated management endpoint and are intentionally absent from `/healthz`, which remains a liveness signal. The dashboard's **Memory observability** card renders the memory and continuation-size fields from this endpoint and offers a confirm-gated diff --git a/docs-site/src/content/docs/zh-cn/getting-started/how-it-works.mdx b/docs-site/src/content/docs/zh-cn/getting-started/how-it-works.mdx index e90bcb38b8..d234dd1ff5 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/how-it-works.mdx +++ b/docs-site/src/content/docs/zh-cn/getting-started/how-it-works.mdx @@ -21,6 +21,31 @@ Codex 使用 OpenAI **Responses API**。opencodex 接收通过 HTTP 与 Server-S └─────────────────────────────────────────────────────────────────────┘ ``` +![Codex 多账号路由:已有线程保持账号绑定,新会话可以查询额度并选择使用量更低的健康账号。](../../../../assets/multi-auth-routing.svg) + +## Codex 认证账号选择 + +当选择的 provider 使用 ChatGPT/Codex 直通时,opencodex 可以在转发请求前从已保存的账号池中选择账号。 + +- **已有线程保持绑定。** 线程绑定到开始时所选的账号代次,长时间运行的 SSH、tmux 或移动端 + Codex 会话不会在正常对话过程中重新分配账号。 +- **新会话可以重新分配。** 新线程按 `accountPoolStrategy` 选择可用账号,默认为 `quota`,也支持 + `round-robin` 和 `fill-first`。`quota` 比较已知的 5 小时、每周和 30 天额度使用量,并在当前账号 + 超过 `autoSwitchThreshold` 时选择使用量更低的账号。冷却中或需要重新认证的账号会被跳过。 +- **额度和失败信号参与路由。** 仪表盘通过 `GET /api/codex-auth/accounts?refresh=1` 强制刷新额度。 + 成功的上游响应会更新额度头信息;429 使账号进入冷却,401/403 会将账号标记为需要重新认证。 +- **空闲额度窗口可以自动激活。** 高级设置中的自动激活默认关闭,统一控制当前主账号和附加账号 + 已报告的 5 小时及每周窗口;新添加账号不会自动启用。在 Pool 模式下,窗口到期后会通过对应账号 + 发送最小化、不保存的请求,并消耗少量额度;同时到期的窗口合并为一次请求。暂停、需要重新认证 + 的账号会被跳过,主账号硬锁限制也会得到遵守。成功响应的额度头会更新缓存;已启用且符合条件的 + 空闲账号还会每隔至少 5 分钟刷新过期的额度元数据,无需保持仪表盘打开。已观察到的到期时间会保留 + 至激活完成,重启或后续查询的时间变化不会丢失待处理窗口。元数据查询复用现有的有次数限制的认证 + 恢复逻辑;推理请求返回 401 时,被拒绝的凭据会标记为需要重新认证。失败日志仅记录不透明账号标签 + 和安全的状态原因。该功能独立于为传入请求选择账号的路由逻辑。 + +**降级说明:** 运行旧版本前,请仅移除自动激活设置中的 `nextFiveHourResetAt` 和 +`nextWeeklyResetAt`。旧版严格校验不接受这两个新字段,可能因此禁用整个自动激活设置块。 + ## Sub-agent 模型选择 全新安装会通过 `subagentModels` 在 Codex 的 sub-agent 选择器中优先显示 `gpt-6-astra`、GPT-5.6 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 cc60e363ca..b14eb06bac 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 @@ -157,9 +157,15 @@ opencodex 会将已路由模型公开为稳定且可逆的别名: user-agent 会获得易读的 CLI 形式,其他客户端会获得 Desktop 哈希形式。两种别名族都会永久 保持可解码——以任一形式保存在 `settings.json` 中的模型都能继续工作。 -如果 Claude Desktop 底部的选择器没有切换已运行 3P 对话的模型,请在该对话中使用 -`/model `。OpenCodex 无法读取选择器状态,只会路由每个请求实际携带的模型 ID;可在 -**Logs → requestedModel** 中确认结果。 +如果 Claude Desktop 底部的选择器没有切换正在进行的 3P 对话的模型,可以尝试 +`/model `,但在受影响的 Desktop 版本中,这种变通方法也可能失败。 +[Issue #3782](https://github.com/lidge-jun/opencodex/issues/3782) 报告称,在 Windows 上使用 +Claude Desktop 1.46388.4 时,无论通过底部选择器还是 `/model` 更改模型,对话都会继续使用 +最初的模型。该报告并未确定是哪个客户端组件或路由组件导致了这一行为。 + +也可以尝试在 OpenCodex 的 Claude Desktop 配置档案中选择所需的默认模型,重新应用配置档案, +然后开始新对话。这是一项排查步骤,不保证能解决问题。OpenCodex 无法读取选择器状态, +而是根据每个请求携带的模型 ID 进行路由。请在 **Logs → requestedModel** 中确认客户端实际发送的内容。 **别名语法规则:**provider 不得包含 `/` 或 `--`,也不得等于 `native`。 不含 `/` 或 `~` 的普通 model ID 继续使用 v1 前缀 `claude-ocx-…`。包含 `/` 或 `~` 的 model ID @@ -352,12 +358,14 @@ Claude Code 的 `/effort` 设置会完整保留并传递给适配器: | Assistant 文本 | `output_text` | | Assistant `tool_use` | `function_call`(`input` → JSON 字符串化的 `arguments`) | | 用户 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 前缀) | -| 重放 `thinking` / `redacted_thinking` | 丢弃 | +| 重放 `thinking` / `redacted_thinking` | `reasoning` 项;签名和脱敏载荷保存在有界 `ocxr1` 信封中 | | Function 工具 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`,`none`→`none`,`any`→`required`,指定函数→`{type:"function",name}`,托管 WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +在预期的 Anthropic 适配器上,保留未隐藏的签名块(包括空 thinking)和不透明的 redacted 块。`hideThinkingSummary` 策略不变:不会向 Claude 客户端公开本地隐藏的签名文本,尚未证明经过此隐藏边界的无损重放。旧版组合信封在流式文本发出后无法恢复原始块顺序。`claudeCode.compatibility: "enforce"` 仍拒绝 thinking 重放。这不证明真实 Anthropic 接受请求或缓存命中改善;[#3719](https://github.com/lidge-jun/opencodex/issues/3719) 仍未关闭。 + **错误情况(400):**JSON 格式错误;缺少/空的 `model`;缺少/空的 `messages`;不支持的 role;`tool_result` 缺少 `tool_use_id`;`tool_use` 缺少 id/name;指定名称的 `tool_choice` 缺少 name。 @@ -369,7 +377,8 @@ role;`tool_result` 缺少 `tool_use_id`;`tool_use` 缺少 id/name;指定 | `response.created` | `message_start` + `ping` | | 心跳 | `ping` | | 文本增量 | `content_block_start` → `content_block_delta`(文本)→ `content_block_stop` | -| 推理摘要/文本 | 带合成签名的 `thinking` 块 | +| 推理摘要/文本 | 带重放签名或有界 `ocxr1` 回退信封的 `thinking` 块 | +| 脱敏推理 | 从推理信封重放的 `redacted_thinking` 块 | | Function-call 帧 | 带 `input_json_delta` 的 `tool_use` 块 | | 终止事件 | `message_delta` → `message_stop` | | 在终止事件前 EOF | 502 风格的 `api_error` | 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 552c0c3f53..0c3df0c0e2 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 @@ -196,6 +196,12 @@ Codex 显示的模型来自一个磁盘上的 catalog(默认是 `$CODEX_HOME/o 历史记录编码成上游 function tool,再在 Codex 收到结果前,把流式 function-call lifecycle 还原成 `custom_tool_call`。原生 OpenAI forward routing 和已支持的 `apply_patch` custom tool 保持不变。 +路由的 code-mode 轮次还会在首次调用前收到宿主对嵌套辅助工具的规则:`tools.apply_patch` +接收一个字符串,首尾必须是没有额外包装的独立补丁标记行;isolate 中没有 `import`,长时间运行的 +命令通过 `write_stdin` 轮询。如果原生路由 Responses、Kiro 或 Cursor 路径上的 code-mode exec +结果仍包含宿主的某条失败消息,opencodex 会追加一行提示,指出对应规则。此变更不会重写模型的 +代码或补丁文本。 + 所选 provider 必须支持 function/tool calling。不支持 tool call 的 text-only provider 无法使用 `exec`、 Browser 或 Computer Use。原生 OpenAI 条目会保持其上游 tool mode 不变。 diff --git a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md index a07d4cf01f..ffe4ffda87 100644 --- a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md @@ -104,7 +104,7 @@ subagentModels = [ 自定义开头模型顺序的受支持方式是重新排列 `subagentModels`。仪表盘的 **Sub-agents** 页面可以调整 裸原生和路由 id 的顺序。配置和 `ocx agent subagents set` 也接受精确的账户限定 -`/` id,但仪表盘不会提供这些 id,保存列表时也不会保留它们。配置的 +`/` id,仪表盘会保留已保存的 id,即使当前不可用。配置的 id 请勿超过五个。存在账户 selector 时,一个裸原生选项可能展开为多个 selector-qualified 行,因此 已配置的选项与公布的行不一定一一对应。 @@ -145,3 +145,11 @@ V2 在客户端目录状态允许时,可以额外接收基于原有优先级 `disabledModels` 和各提供商的 `selectedModels` 仍是可见性字段。没有独立的 `modelOrder`、 `providerOrder` 或优先级映射设置。 + +## 仪表盘排序预设 + +在 **Models** 中选择默认、按模型名 A–Z、按提供商或使用量快照,再应用顺序。保存当前可用的路由 ID 和 `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)。使用量排序仅在应用时读取一次保留的全部历史;重新打开或模型增减不会重新计算。已有自定义、原生完整顺序会保留,直到明确应用替换。即使没有可用模型,默认也能清除两个字段。 + +`GET/PUT /api/subagent-models` 的 `chosen`、`available` 保留禁用或缺失的已存 roster;`pickerAvailable` 只包含可选路由 ID。Models 只发送 `pickerOrder`、`pickerOrderMode`,不发送 `models`。只保存 roster 不影响排序,非法输入或保存失败会保留原状态。 + +预设保留精选、原生优先级区间,应用于 Codex 目录与 Claude 发现列表的路由分组。Claude 原生前缀、明确的 Desktop 配置及 alias 归属不变。OpenCodex 指导排序与 fallback 设置不变,但原生 Codex 工具显示的前五候选及推荐默认模型可能改变。保存不会重启客户端;目录刷新可能尚未完成,旧列表可能需要重新打开客户端。 diff --git a/docs-site/src/content/docs/zh-cn/guides/pi.md b/docs-site/src/content/docs/zh-cn/guides/pi.md index ad868e3194..c9ebf7b4a6 100644 --- a/docs-site/src/content/docs/zh-cn/guides/pi.md +++ b/docs-site/src/content/docs/zh-cn/guides/pi.md @@ -23,6 +23,9 @@ ocx export --client pi "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -37,6 +40,8 @@ ocx export --client pi } ``` +生成的 Pi 提供方配置启用了 `compat.sendSessionAffinityHeaders`。合并或手动编辑提供方时请保留该设置:Pi 提供稳定的会话标识,OpenCodex 据此为规范的 OpenCode Go 目标生成会话亲和标识。`cacheRetention` 为 `none` 时,Pi 可能不发送会话标识。 + 模型 id 是代理的规范选择器,因此已路由模型会显示为 `provider/model`(`anthropic/claude-opus-5`),而原生 OpenAI slug 会保持不带前缀(`gpt-5.6-sol`)。`name` 后缀 - `(anthropic)`、`(native)`、`(routed)` - 负责让两个同名但来自不同上游的模型在 Pi 的选择器中可区分。 ## 放置位置 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 290757bab7..58046b50fe 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -75,8 +75,9 @@ ChatGPT 透传目录也会加入 GPT-5.6 Sol/Terra/Luna 的裸 slug(`gpt-5.6-s ## 2. 账号登录(OAuth) -有八个提供商预设使用 OAuth 登录,另加通过实验性非官方设备流桥接的 GitHub Copilot。 -opencodex 会把凭据存入 `~/.opencodex/auth.json` 并自动刷新。登录 CLI 也接受 `chatgpt`: +有九个提供商预设使用 OAuth 登录,另加通过实验性非官方设备流桥接的 GitHub Copilot。 +opencodex 会把凭据存入 `~/.opencodex/auth.json`:可刷新的令牌会自动轮换;OrcaRouter +这类持久密钥会复用到提供商撤销为止。登录 CLI 也接受 `chatgpt`: 它会获取一份 ChatGPT 凭据,并创建一个 `forward` 模式的提供商条目。 ```bash @@ -88,6 +89,7 @@ ocx login kiro # 导入 kiro-cli 凭据(支持令牌回退) ocx login google-antigravity ocx login cursor # 独立的 Cursor PKCE 登录 ocx login command-code # Command Code 浏览器 OAuth(或导入 ~/.commandcode/auth.json) +ocx login orcarouter-oauth # OrcaRouter 浏览器授权 + PKCE ocx login github-copilot # GitHub 设备流 → Copilot 令牌(Copilot Pro/Business) ocx login chatgpt # 独立的 ChatGPT OAuth 登录 ocx logout @@ -102,8 +104,12 @@ ocx logout | `kiro` | `kiro` | `https://runtime.us-east-1.kiro.dev` | 首次登录会导入已安装并已登录的 Kiro CLI 会话(Unix 使用 `curl -fsSL https://cli.kiro.dev/install` | `bash`;Windows PowerShell 使用 `irm 'https://cli.kiro.dev/install.ps1'` | `iex`;然后运行 `kiro-cli login`)。**添加账户**会先退出 `kiro-cli`,再启动新的浏览器登录,从而切换 `kiro-cli` 自身使用的账户,并保存账户范围的配置文件元数据。现有 OpenCodex 账户会保留;如果取消或失败,则恢复之前的 `kiro-cli` 会话。 | | `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 通过 Cloud Code Assist 协议使用 Google OAuth。实时发现调用已认证的 CCA `v1internal:fetchAvailableModels` 端点,并仅发布当前登录账户可用的 agent 模型;维护中的目录仍作为回退。Quota 通过 `retrieveUserQuota` 和 `retrieveUserQuotaSummary` RPC 实时查询(8 秒超时)。CCA 聊天/adapter 请求使用 SSE(`v1internal:streamGenerateContent?alt=sse`),并为单次调用缓冲该流。内置图像生成使用单独的 unary `v1internal:generateContent` 端点。Adapter 在首个主机发生传输失败、空流、404 或 `UNAVAILABLE` 后,至多重试一次其维护的 daily/production peer;认证、地理封锁、无效请求和配额耗尽不会触发主机故障转移。 | | `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、带可选 HTTP/1.1 兼容路径的 HTTP/2 传输,以及按账号筛选的模型发现。 | +| `orcarouter-oauth` | `openai-chat` | `https://api.orcarouter.ai/v1` | 浏览器授权与密钥交换走 `https://www.orcarouter.ai` + S256 PKCE。交换结果是用户自己的普通 `sk-orca-…` API key,保存在现有凭据库中并持续复用,直到被撤销。 | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 实验性。GitHub 设备流 + `copilot_internal` 交换(VS Code OAuth 客户端)。需要有效的 Copilot 订阅;不是官方第三方 API。 | +Google Antigravity 账户和提供方的配额查询(包括模型列表回退)使用固定的 Google 计量端点。这些目标支持透明 Fake-IP DNS,同时保留 TLS 验证、重定向拒绝和私有地址检查。自定义 base URL 仅改变模型请求,不改变配额目标;`NO_PROXY` 仍使用直连策略。 + + Nous refresh 发生终止性失败后,请运行 `ocx login nous` 重新认证。 对于规范的 Kimi Coding Plan 预设(`kimi` 账号登录和 `kimi-code` API key),opencodex @@ -202,6 +208,7 @@ Cline IDE/CLI 中提供,不能通过 API 使用;`minimax/minimax-m2.5` 是 | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | 智谱 AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (静态模型列表)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token plan(默认): `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · 按量付费: `https://dashscope.aliyuncs.com/compatible-mode/v1` · 或自定义 | | 腾讯云 Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -259,6 +266,46 @@ inference key 可从 [Vultr Console](https://my.vultr.com) 的订阅概览复制 `~/.commandcode/auth.json` 导入本地 CLI 凭据);模型目录按账户隔离,并在登录后从经过认证的发现 端点获取。聊天请求使用已配置的 bearer 密钥。密钥可在 [Command Code Studio](https://commandcode.ai/studio/) 创建。 +**OrcaRouter 认证与模型发现:**可用 `ocx login orcarouter-oauth` 走浏览器一键授权, +也可用 `ocx login orcarouter` 粘贴已有 API key。PKCE 流程会先监听本机回环端口,为每次登录 +生成新的 S256 challenge 和 state;授权页使用 `https://www.orcarouter.ai/auth`,并通过 +`https://www.orcarouter.ai/api/v1/auth/keys` 交换一次性 code,再把返回的 +用户自有 key 保存到 `~/.opencodex/auth.json`;手填 key 仍使用项目原有的 provider key 存储。 +两种模式都访问 `https://api.orcarouter.ai/v1`,并使用 `capability=chat` 实时发现模型;图片生成、 +视频和 rerank 条目会被排除,模型返回的 input modalities 决定 Codex 是否允许图片附件。 +由于模型目录本身是公开的,手填 key 时会诚实显示“无法验证”,不会把公开目录的 200 响应误当成 +密钥有效证明。 + +单域名自托管环境可在第一次 PKCE 登录前设置统一 origin;推理地址会从同一个 origin 派生: + +```bash +ORCAROUTER_BASE_URL=https://router.example ocx login orcarouter-oauth +``` + +若自托管环境也分离登录域名与 API 域名,可分别设置 `ORCAROUTER_AUTH_BASE_URL` 和 +`ORCAROUTER_API_BASE_URL`。 + +该值必须是 HTTPS origin(本地开发可使用 HTTP loopback),且不能包含用户名密码、query 或 fragment。 +首次登录回环或私有网络中的自托管服务前,必须在 `~/.opencodex/config.json` 中明确允许访问该地址。 +例如,将以下条目合并到现有的 `providers` 对象中,用于本地开发服务: + +```json +{ + "orcarouter-oauth": { + "adapter": "openai-chat", + "baseUrl": "http://127.0.0.1:9999/v1", + "authMode": "oauth", + "allowPrivateNetwork": true + } +} +``` + +然后运行 `ORCAROUTER_BASE_URL=http://127.0.0.1:9999 ocx login orcarouter-oauth`。 +登录会保留这项明确授权;仅设置 URL 不会自动启用私有网络访问。 +未设置此选项时,目标地址校验会拒绝该服务的推理和模型发现请求。 +此要求针对 provider 的服务地址,浏览器回调监听器不需要此选项。 +若 relay 返回 `401`,重新运行登录即可;OrcaRouter 签发的是长期 API key,不存在 refresh-token grant。 + **Command Code 配额:**仪表盘和 `ocx account refresh` 会在规范主机 `https://api.commandcode.ai` 上探测 `/alpha/billing/credits` 窗口(5 小时和每周)。OAuth 预设 (`command-code`) 使用已保存的账户 bearer;Provider-API 密钥预设 (`commandcode`) 使用当前配置的有效密钥。用户改写后的仿冒 base URL 不会被探测。当 Command Code 同时返回周期消耗时,剩余的 monthly / purchased / free credits 会显示为 USD 窗口。 **Command Code 项目上下文。** 仅在 OAuth `command-code` 提供商上(不是 API 密钥 `commandcode` 预设)可选的 `projectContext: "on"` 会从代理工作目录填充 `/alpha/generate` 的 `memory`、`taste` 和 `skills`。通过 **Providers → Command Code → Edit JSON** 设置在 `providers.command-code` 上,从受信任的 Codex 项目启动代理,保存后重启。未设置或 `"off"` 时即使存在 `AGENTS.md` 或 taste 文件也保持空信封。文件路径、上限和 fail-soft 行为见 [Adapters](/zh-cn/reference/adapters/#command-code)。 @@ -315,7 +362,7 @@ Bearer key。公开模型列表只保留同时报告 `model_type: chat` 和 `cha > **腾讯云 Coding Plan 使用限制:**腾讯将此订阅限定为交互式编程工具使用。禁止通用 API > 自动化、自定义应用后端和非交互式批量调用;违规使用可能导致套餐密钥被停用。 -> **两条 GLM 线路:**`zai` 是 Z.AI 的国际 coding plan 订阅,`zhipu-bigmodel` 是智谱国内 +> **GLM 计费线路:**`zai` 是 Z.AI 的国际 coding plan 订阅,`zhipu-bigmodel` 是智谱国内 > BigModel 的按量付费端点。二者主机、密钥与计费均不同,为其中一方签发的密钥无法在另一方通过鉴权。 ### 多个 API 密钥 @@ -357,8 +404,8 @@ GPT-5.6 Sol/Terra/Luna 会预置在提供商的回退列表中,因此即使实 使用 Bearer **订阅令牌**(而非普通 API 密钥)进行认证。 **Cloudflare AI Gateway** 需要将 account 和 gateway id 填入 URL。 -Copilot 提供混合 wire 目录:其 GPT-5 系列模型(`gpt-5.3-codex`、`gpt-5.4`、 -`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`)会拒绝面向 +Copilot 提供混合 wire 目录:其模型(`gpt-5.3-codex`、`gpt-5.4`、 +`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)会拒绝面向 agent 流量的 `/chat/completions`,因此 opencodex 默认将这些模型路由到 Responses API,而其他 Copilot 模型仍走 chat completions。优先级为:硬 wire 固定 → 显式 [`modelAdapters`](/zh-cn/reference/configuration/providers/) 条目 → 注册表默认值 → 提供商级 diff --git a/docs-site/src/content/docs/zh-cn/guides/remote-hub.md b/docs-site/src/content/docs/zh-cn/guides/remote-hub.md index b1ca7f45de..6caa44fc23 100644 --- a/docs-site/src/content/docs/zh-cn/guides/remote-hub.md +++ b/docs-site/src/content/docs/zh-cn/guides/remote-hub.md @@ -60,14 +60,33 @@ ocx connect rotate --admin-token-stdin ## Docker、回滚与排障 -opencodex 不发布官方 Docker 镜像,但仓库提供维护的 `Dockerfile` 和 `compose.yaml`,用于在本地构建按 digest 固定的 Bun 镜像。首次正常启动会在 `ocx-state` 卷的 `/home/bun/.opencodex/container-tls/cert.pem` 和 `/home/bun/.opencodex/container-tls/key.pem` 生成并保存一套自签名 TLS 身份;私钥仅所有者可读。后续启动会验证并复用它,因此数据端点使用 HTTPS。首次正常启动前,通过 stdin 初始化一次数据密钥;引导程序最多接受一行 512 字节的内容,不会输出密钥,并以仅所有者可读的权限保存规范的 `service-api-token`。 +回滚时也要保留两个卷及其挂载路径。已有卷的所有权和权限不会自动修复。有关不使用 Compose 时的命名卷挂载及单独的状态路径,请参阅[英文基准指南](/guides/remote-hub/#docker-compose)。 + +部署使用两个独立持久卷:`ocx-state` 对应 +`OPENCODEX_HOME=/home/bun/.opencodex`,`codex-state` 对应 +`CODEX_HOME=/home/bun/.codex`。两个产品的 `auth.json` 格式不同,不能合并到同一个 +主目录。即使根文件系统只读,这两个目录也可通过各自的卷写入。 + +此设置不会自动生成模型目录。在检查认证后的 `/v1/catalog` 前,必须生成或导入有效的 +`/home/bun/.codex/opencodex-catalog.json`;空目录返回 `catalog_not_found` 404 属于正常行为。 +升级会保留现有 `ocx-state` 并新增 `codex-state`,但不会自动迁移文件。若之前的临时方案 +将模型目录放在 `.opencodex` 下,请先备份,再仅迁移模型目录文件,并保留仅所有者可访问的权限。 +不要用一个产品的 `auth.json` 覆盖另一个。自定义 `CODEX_HOME` 时,必须将该确切目录挂载为 +可写持久卷,并在 `${CODEX_HOME}/opencodex-catalog.json` 准备默认目录文件。 +若 `model_catalog_json` 指向其他文件,也必须持久保存其解析后的路径。 +在明确完成迁移前,请保留已有的环境变量与卷路径映射。 +`docker compose down` 保留两个卷;`docker compose down --volumes` 则会删除 +`ocx-state` 和 `codex-state`,包括配置、凭据、用量记录、数据密钥及 Codex 状态和模型目录。 +这是破坏性操作,不能当作升级或重启命令使用。 + +opencodex 不发布官方 Docker 镜像,但仓库提供维护的 `Dockerfile` 和 `compose.yaml`,用于在本地构建按 digest 固定的 Bun 镜像。首次启动前,通过 stdin 初始化一次数据密钥;密钥不会输出,并以仅所有者可读的权限保存在 `ocx-state` 卷中。 宿主机需要安装 Git 和 Bun。每次构建镜像前,都应从 Git 跟踪的源码生成规范兼容性清单,生成后到构建完成前不要修改源码。生成的 JSON 不加入 Git;`.git` 不进入 Docker 构建上下文。宿主机端口默认绑定 `127.0.0.1`。远程访问须显式使用 `OPENCODEX_BIND_ADDRESS= docker compose up -d`;`0.0.0.0` 会公开所有接口。请使用防火墙和经过身份验证的 TLS/tailnet 前端保护访问。 -构建会拒绝过期清单,并将每个 SHA-256 分别与构建上下文及复制后的文件进行核对。清单对 `Dockerfile`、`compose.yaml`、`.dockerignore`、所有 Git 跟踪的 Docker 权威文件(引导、配置和探针)、`src/`,以及必需的 `package.json`、`bun.lock` 和 `scripts/model-metadata.source.json` 进行完整性验证。缺失或不匹配的文件、清单之外的源码或 Docker 权威文件,以及符号链接都会导致失败;`scripts/` 中仅纳入上述模型元数据文件。 +构建会拒绝过期清单,并将每个 SHA-256 分别与构建上下文及复制后的文件进行核对。缺失或不匹配的文件、清单之外的源码和符号链接都会导致失败。必须包含 `package.json`、`bun.lock`,以及 `scripts/` 中唯一纳入的 `scripts/model-metadata.source.json`。 ```bash -git clone https://github.com/yansigit/opencodex.git +git clone https://github.com/lidge-jun/opencodex.git cd opencodex bun scripts/generate-compatibility-version.ts docker compose build @@ -75,33 +94,7 @@ openssl rand -hex 32 | docker compose run --rm -T hub bun run docker/bootstrap-t docker compose up -d ``` -容器以非 root 的 `bun` 用户运行,根文件系统只读,并且只发布 `10100`。复制公有证书(绝不要复制私钥),将其用作本地 CA 来验证默认的回环发布: - -```bash -mkdir -p .tmp -docker compose cp hub:/home/bun/.opencodex/container-tls/cert.pem .tmp/opencodex-container-ca.pem -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10100/healthz -``` - -`OPENCODEX_PORT` 会同时控制宿主机发布端口和自动管理的 `tls.publicOrigin`,而容器内监听端口始终是 `10100`: - -```bash -OPENCODEX_PORT=10190 docker compose up -d -curl --cacert .tmp/opencodex-container-ca.pem --fail --silent https://localhost:10190/healthz -``` - -远程发布必须显式设置 `OPENCODEX_BIND_ADDRESS`。自动生成的证书只覆盖 `localhost` 和 `127.0.0.1`;直接远程发布前,请用与准确远程名称匹配的证书和私钥替换卷中的身份,并设置 `OPENCODEX_PUBLIC_ORIGIN=https://准确的主机名和端口`。该值必须是准确的 HTTPS origin,不能包含路径、查询参数或片段。无论哪种方式都应使用防火墙;更推荐保留默认回环发布,并由经过身份验证的 TLS/tailnet 前端代理。 - -升级时,保留的旧版无 TLS 卷会在启动时迁移为每卷 TLS 身份,并根据已发布的宿主机端口写入 HTTPS origin;自定义证书路径会保留。若要回滚到旧版仅 HTTP 镜像,必须在当前镜像仍可用时先停止 hub 并仅移除 TLS 设置,然后再启动旧镜像;身份文件可以留在卷中: - -```bash -docker compose down -docker compose run --rm hub bun run src/cli/index.ts config unset tls -# 选择或构建旧镜像,然后重新创建 hub -docker compose up -d -``` - -不要发布 `10101`,也不要把密钥放入 `ARG`、`ENV`、`COPY`、Compose、镜像历史或 argv。仅限容器内固定的 `https://127.0.0.1:10100` 回环地址,内部 health/readiness 探针可以使用 `rejectUnauthorized:false` 跳过证书身份验证;这只证明本地监听器和路由正常,不能作为外部验收。外部验收必须使用复制出的公有证书或系统信任库,并通过证书对应的准确主机名验证 HTTPS。healthcheck 后仍需单独验证 readiness、已认证目录和真实请求。`docker compose down` 会保留卷;`docker compose down --volumes` 还会删除配置、凭据、TLS 身份和数据密钥。 +容器以非 root 的 `bun` 用户运行,根文件系统只读,并且只发布 `10100`。不要发布 `10101`,也不要把密钥放入 `ARG`、`ENV`、`COPY`、Compose、镜像历史或 argv。healthcheck 后仍需单独验证 readiness、认证目录和真实请求。`docker compose down` 会保留卷;`docker compose down --volumes` 还会删除配置、凭据和数据密钥。 - hub 宕机:可以离线断开,但远程密钥仍待吊销。 - 目录过期:仅在临时故障时保留已验证的 LKG;认证、架构、大小或协议错误不会回退到本地提供商。 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 62e0b5076e..c1f0be338b 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 @@ -132,7 +132,7 @@ ocx claude desktop import [--apply] Validate and import JSON ## Client config export -### `ocx export --client ` +### `ocx export --client ` 输出连接到正在运行代理的客户端配置。此命令会以所选客户端的原生格式序列化 `opencodex` provider 块,其中包含基础 URL、模型列表,以及该客户端适用的凭据引用或 `opencodex-loopback` 占位值。 @@ -140,7 +140,7 @@ ocx claude desktop import [--apply] Validate and import JSON | 标志 | 动作 | | --- | --- | -| `--client ` | 必需。选择客户端配置格式。 | +| `--client ` | 必需。选择客户端配置格式。 | | `--json` | 仅在 stdout 打印配置 JSON,这样重定向即可捕获字节级精确输出。包括 `--out` 写入提示在内的所有诊断信息都会输出到 stderr。 | | `--out ` | 将配置写入 ``。拒绝替换已存在的文件。 | | `--force` | 允许 `--out` 替换已存在的文件。 | @@ -167,6 +167,9 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `mcode` | `~/.minimax/config.yaml` (设置后 `MINIMAX_DATA_DIR` 优先,其次是旧的 `MAVIS_DATA_DIR`;相对路径会被拒绝) | `mcode-config.yaml` | 无 — loopback placeholder | | `zcode` | `~/.zcode/v2/config.json` (设置后 `ZCODE_DATA_DIR` 优先;相对路径会被拒绝) | `config.json` | 无 — loopback placeholder | | `prime` | `~/.prime/agent/models.json` (设置后 `PRIME_AGENT_CODING_AGENT_DIR` 优先;相对路径会被拒绝) | `prime-models.json` | 无 — loopback placeholder | +| `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 与 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 无 — 仅限回环,不会写入 `api_keys` 条目 | + +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` 条目,所以该导出仅限回环,非回环绑定会被拒绝。 opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。opencodex 生成的 Pi 导出不需要环境变量,而是携带字面占位值 `opencodex-loopback`。这个值是必需的:Pi 在构建模型列表时会解析 `apiKey`,如果已有配置包含未设置的环境变量引用,它就会隐藏整个 provider。回环上的代理从不校验生成的占位值。 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md index f9fec6a7b2..bb3904b551 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md @@ -14,7 +14,7 @@ description: 提供方配置、凭据、配额,以及模型目录命令。 | 子命令 | 支持的标志 | 操作 | | --- | --- | --- | -| `list` | `--json` | 列出已配置的提供方以及剩余的注册表条目。 | +| `list` | `--json`, `--jsonl` | 列出已配置的提供方以及剩余的注册表条目。 `--jsonl` 为每个已配置的提供方输出一行 JSON 对象。 | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | 添加一个注册表/自定义提供方。`--force` 会覆盖;`--sync` 会在有人类输出模式运行的代理上刷新配置。 | | `edit ` | 提供方字段标志,`--headers `,`--json` | 在不替换密钥池的情况下,编辑经过校验的在线提供方字段。`--headers` 会合并自定义请求头;传入 `{}` 或 `-` 可清空。 | | `test ` | `--json` | 探测真实的上游模型端点。 | @@ -28,6 +28,7 @@ description: 提供方配置、凭据、配额,以及模型目录命令。 ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -36,6 +37,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` 仅输出已配置的提供方,每行一个 JSON 对象。每个对象的字段与 `--json` 输出中 `configured` 数组的元素相同,不包含 `registryCount` 汇总。脚本可以逐行处理这些对象。`--json` 与 `--jsonl` 不能同时使用。 + :::caution[自定义请求头不是凭据通道] `--headers` 用于非机密的请求元数据 —— 路由提示、租户或项目选择器、追踪 ID 等。它不是 存放认证信息的地方,校验器会拒绝标准凭据请求头名称(`Authorization`、`X-Api-Key`、 @@ -169,12 +172,11 @@ token,也不是简单重读账号列表。`--json` 返回 ### `ocx account auto-switch > [--json]` -只控制 `openai` 的 Codex 账号池。`on` 会设为 80%,`off` 会设为 0%,`status` 会读取 -当前值,而 `threshold ` 接受 0 到 100 之间的整数。其他提供方和无效值都会以 1 -退出。`--json` 返回: +控制 `openai` Codex 账户池阈值,或保存通用 OAuth 账户池阈值。`on` 保存 80%,`off` 保存 0%,`threshold ` 接受 0–100。通用池的阈值目前不参与运行;保存阈值不会启用阈值切换、改变提供方启用设置或禁用 429 错误后的轮换。通用池的查询和修改结果使用服务器确认值。通用池的 `poolEnabled` 是已保存的提供方设置,`null` 表示未指定,并不代表继承后的实际状态。`inert: true` 表示阈值未应用;能力未知时也不会报告 `enabled: true`。API 密钥提供方、Anthropic 和无效值会被拒绝。 ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]` 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 f0dfb0eac0..a978d26d8f 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 @@ -100,7 +100,7 @@ selector,而不是分配一个新名称。 | `modelAutoCompactTokenLimits?` | `Record` | 按模型设置的正安全整数软自动压缩预算。该值只能降低“上下文或最大输入的 90%”这一有效上限;没有已知的权威上下文窗口时不会输出。对于规范 `openai`,键必须是受支持的精确原生模型 ID,且不得包含提供者或账户选择器前缀。提供者 PATCH 会合并条目;将某个键设为 `null` 会删除该键,将整个字段设为 `null` 会清空映射。这些 `null` 删除标记仅适用于 PATCH。 | | `defaultMaxOutputTokens?` | `number` | 当客户端省略 `max_output_tokens` 时,`openai-chat` 的提供者级回退值。 | | `modelMaxOutputTokens?` | `Record` | 正数型、按模型设置的 `openai-chat` 回退预算;精确/模式匹配优先于提供者默认值。 | -| `modelCosts?` | `Record` | 按模型设置的显示价格(每 100 万 token 的美元数),以该提供者的精确上游模型 ID 为键(不是提供者标识符或路由后的 `provider/model` 标签),值为四个字段:`input`、`output`、`cacheRead`、`cacheWrite`(示例:`{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。任何模型 ID 都是有效键——自定义提供者可以通过 `openai-chat` 适配器指向任意 OpenAI 兼容端点,即使不存在于内置目录中,本地 OpenAI 兼容和内部提供者的 ID 同样有效。用户配置的价格在 Logs 的 `~$` 和 Usage 估算中优先于内置目录;历史条目也会按当前覆盖项重新计价,因此修改价格可能改变过去的总额(回退顺序:用户配置 → jawcode 目录 → expected-price 覆盖 → 模型级厂商价格);全零条目会回退到该顺序中的下一个来源。每个费率必须是大于等于 0 的有限数字,且不超过 1,000,000(每 100 万 token 的美元数);超出范围的条目会在管理边界被拒绝,并在加载时被丢弃。仅用于显示的估算:覆盖项不影响路由、账户选择、配额或计费。 | +| `modelCosts?` | `Record` | 按模型设置的显示价格(每 100 万 token 的美元数),以该提供者的精确上游模型 ID 为键(不是提供者标识符或路由后的 `provider/model` 标签),值为四个字段:`input`、`output`、`cacheRead`、`cacheWrite`(示例:`{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。任何模型 ID 都是有效键——自定义提供者可以通过 `openai-chat` 适配器指向任意 OpenAI 兼容端点,即使不存在于内置目录中,本地 OpenAI 兼容和内部提供者的 ID 同样有效。用户配置的价格在 Logs 的 `~$` 和 Usage 估算中优先于内置目录;历史条目也会按当前覆盖项重新计价,因此修改价格可能改变过去的总额(回退顺序:用户配置 → jawcode 目录 → expected-price 覆盖 → 模型级厂商价格);用户明确将所有费率设为零时,会得到已知的零费用估算;删除该模型的覆盖项即可恢复自动定价。目录中的全零价格仍会回退到下一个来源。每个费率必须是大于等于 0 的有限数字,且不超过 1,000,000(每 100 万 token 的美元数);超出范围的条目会在管理边界被拒绝,并在加载时被丢弃。仅用于显示的估算:覆盖项不影响路由、账户选择、配额或计费。 | | `headers?` | `Record` | 额外的上游请求头。会拒绝 Authorization、cookie、API key 头、嵌入换行符以及无效名称。 | | `openRouterRouting?` | `OpenRouterProviderRouting` | 默认的 OpenRouter `order`、`only` 和 `allowFallbacks` 偏好;仅对使用 `openai-chat` 的规范 OpenRouter 有效。 | | `modelOpenRouterRouting?` | `Record` | 精确模型 id 级别的覆盖项,会替换提供者级 OpenRouter 偏好。 | @@ -112,7 +112,7 @@ selector,而不是分配一个新名称。 | `modelReasoningEfforts?` | `Record` | 按模型设置的标签。空列表会隐藏 effort 控件。 | | `modelSupportsReasoningSummaries?` | `Record` | 将某个模型设为 `false`,即可停止暴露摘要并移除摘要交付字段。 | | `modelReasoningSummaryDelivery?` | `Record` | 按模型设置的 Responses 交付枚举;会重写现有的 delivery 字段。 | -| `modelAdapters?` | `Record` | 按模型设置的 `openai-chat` 或 `openai-responses` 线协议覆盖项,用于混合线协议网关。显式条目优先于注册表默认值;DeepSeek 预设可以为 `deepseek-v4-flash` 选择原生 Responses,GitHub Copilot 则为 GPT-5 系列(`gpt-5.3-codex`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`)声明了 Responses 专用默认值,因为这些模型在代理流量下会拒绝 `/chat/completions`。没有内置默认值的模型(例如 `gpt-5.4-nano`)可以在此手动启用。单一线协议上游固定项和规范 ChatGPT forward 会拒绝覆盖。 | +| `modelAdapters?` | `Record` | 按模型设置的 `openai-chat` 或 `openai-responses` 线协议覆盖项,用于混合线协议网关。显式条目优先于注册表默认值;DeepSeek 预设可以为 `deepseek-v4-flash` 选择原生 Responses,GitHub Copilot 则为 模型(`gpt-5.3-codex`、`gpt-5.4`、`gpt-5.4-mini`、`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)声明了 Responses 专用默认值,因为这些模型在代理流量下会拒绝 `/chat/completions`。没有内置默认值的模型(例如 `gpt-5.4-nano`)可以在此手动启用。单一线协议上游固定项和规范 ChatGPT forward 会拒绝覆盖。 | | xAI Responses 启用项(仪表板) | 开关 | 仅用于 `xai`,以原子方式设置或清除 `grok-4.5` 和 `grok-4.6` 的 `modelAdapters` 条目。若只存在一个条目,则显示混合状态,直到下次开关写入将两者统一。其他覆盖项和层级行为不变。 | | `xaiResponsesXSearch?` | `boolean` | 默认禁用。在 xAI Responses 目标上,仅当有效的 `web_search` 工具在最终请求规范化后仍保留时,才附加由提供方托管的 `x_search` 声明。不会重复已有声明,绝不会扩大调用方的 `tool_choice`/`allowed_tools` 选择范围,并且此项独立于网络搜索辅助服务的 `search.xSearch` 选项。 | | `modelPreferHostedTools?` | `Record` | 非 forward Responses gateway 的精确模型 ID opt-in,用于上游预留 hosted tool namespace 的情况。目前只支持 `["image_generation"]`;匹配模型必须使用 `openai-responses` wire 且支持该 hosted 工具。它会移除冲突的客户端 `image_gen` 声明,并改写其 selector 以保持调用方的 tool choice。对于 OpenAI API 的虚拟 `-pro` 模型,先匹配所选公开 ID,未命中时才使用解析出的基础 wire-model ID 作为回退。`modelAdapters` 会先按公开 ID、再按基础 ID 解析;后一次结果决定最终 wire。未配置模型保持普通 alias 行为。 | @@ -381,6 +381,14 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型 请使用 `modelDisplayNames` 设置显示名称。优先顺序是操作者设置的 `modelDisplayNames`、提供者目录元数据,然后是普通的 `provider/model` 显示。键是此提供者内精确的原生模型 id,例如 `xai/grok-4.6` 的键是 `grok-4.6`。名称只改变显示,不会改变精确路由 id 或上游模型 id。请只把此字段加入 `config.json` 中现有的提供者设置,并保留所有其他字段。向 `PUT /api/providers/:provider/model-display-names` 发送 `{ "modelId": "grok-4.6", "displayName": "Grok 4.6" }` 可保存名称,发送 `displayName: null` 只重置该名称。 +本地 Codex 目录中受支持的不带前缀的原生 GPT 条目也可以通过 +`providers.openai.modelDisplayNames` 设置精确的显示名称, 例如 `"gpt-6-astra": "GPT 6 Astra"`。 +启动时同步和本地目录收敛都会重新应用这些名称。删除名称设置时, 只有条目的当前显示名称仍与已应用的覆盖值一致, +才会恢复原始原生名称。外部更改的显示名称仍受现有原生元数据规范化规则约束。 +例如,Astra (`gpt-6-astra`) 仍会将不同于固定原生名称的名称替换为该固定名称。 +显示名称覆盖不会改变模型 ID、元数据(包括能力)、排序、路由组合别名和带账户限定的条目。 +此本地目录覆盖不会重命名 HTTP 模型列表中的条目或虚拟 `*-pro` 条目。 + 预览版 GPT-5.6 回退条目使用相同机制。OpenAI API key 预设会为基础和 Pro id 设定 `922000` 上下文和 `922000` 最大输入;OpenRouter 会为 `openai/gpt-5.6-sol`、`openai/gpt-5.6-terra` 和 `openai/gpt-5.6-luna` 设定 `922000` 上下文。Pool/Direct 会声明 `922000`;同步后的目录会声明 `max`,同时保留 `xhigh` 的独立性。 ```json @@ -397,6 +405,18 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型 } ``` +## 模型显示名称编辑器 + +仪表板的 **Models** 可让你为已发现的模型持久保存易读名称。展开提供者,找到一个已发现的模型,然后选择 **Name**。 +保存易读名称时,对话框会一直显示精确的 `provider/model` 选择器。选择 **Reset name** 可恢复为 +提供者元数据中的名称,或默认的选择器显示。**Name** 只改变显示;单独的别名铅笔图标用于修改 +短路由别名,并不是显示名称编辑器。原生 OpenAI 和自定义模型条目保留现有控件。 + +如果更改已保存但刷新失败,对话框会反映已保存的覆盖值,并继续提供 **Retry**。如果服务器报告 +目录收敛失败,Retry 会重新执行目录收敛;如果只是列表请求失败,则重新加载列表。重置后的恢复 +会保留重置操作,不会恢复旧名称。请求的总时限为 60 秒,涵盖写入及后续的列表刷新。超时不会撤销 +写入:进行其他更改前,请使用 **Retry** 检查当前名称。 + ## 完整示例 ```json diff --git a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md index 14380a7530..b975226d7b 100644 --- a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md +++ b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md @@ -159,6 +159,10 @@ choice 增量、带 `finish_reason` 的终止 choice,以及 `data: [DONE]`。 这些端点使用 Claude Code 和兼容客户端所采用的 Anthropic Messages 方言。大多数请求会被转换为 Responses,按常规路由,然后再转换回 Anthropic JSON 或 Anthropic SSE。 +转换后的 Messages 请求在重放推理数据时共享整个请求的转换预算,其中包含编码和解码产生的副本开销。 +超出预算时返回 HTTP 413 和 `translation_buffer_limit`,不会为了满足限制而截断签名或不透明推理数据。 +原生 Anthropic 透传使用独立的请求体大小限制。 + 只有在满足以下全部条件时,原生 Anthropic 透传才有资格启用: - Claude Code 配置中尚未禁用原生透传; 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 28d3b5dcd7..f52f67bca7 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 @@ -428,12 +428,14 @@ Claude Code 的 `/effort` 設定會完整保留並傳遞給適配器: | Assistant 文字 | `output_text` | | Assistant `tool_use` | `function_call`(`input` → JSON 字串化的 `arguments`) | | 使用者 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 字首) | -| 重放 `thinking` / `redacted_thinking` | 丟棄 | +| 重放 `thinking` / `redacted_thinking` | `reasoning` 項目;簽名與遮蔽載荷保存在有界 `ocxr1` 信封中 | | Function 工具 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) | | `tool_choice` | `auto`→`auto`,`none`→`none`,`any`→`required`,指定名稱 function→`{type:"function",name}`,hosted WebSearch/web_search→`{type:"web_search"}` | | `max_tokens` | `max_output_tokens` | | `stop_sequences` | `stop` | +在預期的 Anthropic 適配器上,保留未隱藏的簽名區塊(包括空 thinking)和不透明的 redacted 區塊。`hideThinkingSummary` 政策不變:不會向 Claude 用戶端公開本地隱藏的簽名文字,尚未證明經過此隱藏邊界的無損重播。舊版組合信封在串流文字發出後無法恢復原始區塊順序。`claudeCode.compatibility: "enforce"` 仍拒絕 thinking 重播。這不證明真實 Anthropic 接受請求或快取命中改善;[#3719](https://github.com/lidge-jun/opencodex/issues/3719) 仍未關閉。 + **錯誤情況(400):**JSON 格式錯誤;缺少/空的 `model`;缺少/空的 `messages`;不支援的 role;`tool_result` 缺少 `tool_use_id`;`tool_use` 缺少 id/name;指定名稱的 `tool_choice` 缺少 name。 @@ -445,7 +447,8 @@ role;`tool_result` 缺少 `tool_use_id`;`tool_use` 缺少 id/name;指定 | `response.created` | `message_start` + `ping` | | 心跳 | `ping` | | 文字增量 | `content_block_start` → `content_block_delta`(文字)→ `content_block_stop` | -| 推理摘要/文字 | 帶合成簽名的 `thinking` 塊 | +| 推理摘要/文字 | 帶重播簽名或有界 `ocxr1` 備援信封的 `thinking` 塊 | +| 遮蔽推理 | 從推理信封重播的 `redacted_thinking` 塊 | | Function-call 幀 | 帶 `input_json_delta` 的 `tool_use` 塊 | | 終止事件 | `message_delta` → `message_stop` | | 在終止事件前 EOF | 502 風格的 `api_error` | diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index f371457be9..44ee5bbb3b 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -201,6 +201,12 @@ metadata,使用 Codex 的 `low | medium | high | xhigh | max | ultra` 檔位 歷史編碼成上游 function tool,再於 Codex 看見前將串流 function-call lifecycle 還原成 `custom_tool_call`。原生 OpenAI forward 路由與受支援的 `apply_patch` custom tool 維持不變。 +路由的 code-mode 回合也會在首次呼叫前收到主機對巢狀輔助工具的規則:`tools.apply_patch` +接收一個字串,開頭與結尾必須是沒有額外包裝的獨立補丁標記行;isolate 中沒有 `import`,長時間執行的 +命令透過 `write_stdin` 輪詢。如果原生路由 Responses、Kiro 或 Cursor 路徑上的 code-mode exec +結果仍包含主機的某則失敗訊息,opencodex 會附加一行提示,指出對應規則。這項變更不會重寫模型的 +程式碼或補丁文字。 + 所選 provider 必須支援 function/tool calling。不支援 tool call 的純文字 provider 無法使用 `exec`、 Browser 或 Computer Use。原生 OpenAI 列保留上游 tool mode 不變。 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 54751d5620..46b03df9a1 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、Gajae Code、DeepSeek Harness 與 MiniMax Code——每個客戶端一個開關,每次寫入前都會先備份。 +description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、OpenClaw、Kimi Code、Gajae Code、DeepSeek Harness、MiniMax Code、ZCode、Prime Agent、Aside 與 Raycast——每個客戶端一個開關,每次寫入前都會先備份。 --- -**整合(Integrations)** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有九個客戶端以這種方式運作,每個都有一個開關: +**整合(Integrations)** 分頁會把 opencodex 的 provider 區塊寫入客戶端自己的設定檔,也會把它移除。共有十三個客戶端以這種方式運作,每個都有一個開關: | 客戶端 | 設定檔 | 格式 | 變更生效時機 | 憑證 | |---|---|---|---|---| @@ -16,6 +16,10 @@ description: 從儀表板把 opencodex 連接到 OpenCode、Pi、OMP、Hermes、 | Gajae Code | `~/.gjc/agent/models.yml` | YAML | 新 sessions,或當你開啟 `/model` 時 | `OPENCODEX_GAJAE_API_KEY` | | DeepSeek Harness (DSH) | `$DSH_HOME/settings.yaml`(預設 `~/.dsh/settings.yaml`) | YAML | 熱重載 | 非秘密的 loopback bearer 佔位符 | | MiniMax Code | `~/.minimax/config.yaml` | YAML | 新 sessions,或開啟模型選擇器後 | loopback 佔位符 | +| Prime Agent | `~/.prime/agent/models.json` | JSON | 新 sessions | loopback 佔位符 | +| ZCode | `~/.zcode/v2/config.json` | JSON | 重新啟動時 | loopback 佔位符 | +| Aside | `~/.aside/u//models.json` | JSON | 完全結束並重新開啟 Aside 後 | loopback 佔位符 | +| Raycast | `~/.config/raycast/ai/providers.yaml` | YAML | 儲存後立即生效——Raycast 會監看該檔案 | 無——僅限 loopback | 受管理 DSH 支援的相容性下限是 **DSH 0.1.0-rc.6**。OpenCodex 只擁有 `llm-pi-ai.providers.opencodex`:Apply 與 Refresh 會取代該片段,Disable 只移除該片段, @@ -30,6 +34,30 @@ MiniMax Code 依序遵循 `MINIMAX_DATA_DIR`、`MAVIS_DATA_DIR`,最後才回 逐模型 context window 與 reasoning-effort 選項;未知能力會省略,而 MCode session 目前選取的 effort 不會被覆寫。 +Raycast 有兩個前提。Custom Providers 是 **Raycast Pro** 功能:免費方案下檔案仍會被寫入,但 +`ocx integration client status --client raycast` 與整合頁面會回報警告,因為 Raycast 不會讀取它。 +另外,Raycast 只有在你開啟一次 Raycast → Settings → AI → **Reveal Providers Config** 後才會建立 +`ai` 資料夾;opencodex 以該資料夾作為安裝訊號,在它存在之前都會回報客戶端尚未安裝。Raycast 在 +macOS 與 Windows 上同樣讀取 `~/.config/raycast/ai/providers.yaml`,且不遵循 `XDG_CONFIG_HOME`, +所以該路徑無法搬移。 + +受管理區塊是檔案 `providers` 序列中的單一元素 `id: opencodex`:`name: OpenCodex`、 +`base_url: http://:/v1`,以及每個路由模型及其 `abilities`——`tools` 與 +`system_message` 依匯出慣例設為 `true`,`vision` 依目錄的輸入模態而定,`reasoning_effort` 在模型有 effort +階梯時設定,`temperature` 對推理模型關閉。檔案中的其他 provider 會被保留,停用只移除 OpenCodex +元素。檔案一儲存 Raycast 就會套用變更,不需重新啟動;模型會在 Raycast 的模型選擇器中歸在 +**OpenCodex** 群組下。Raycast 支援選填的 `api_keys`,但 OpenCodex 刻意省略該欄位,並拒絕 +非 loopback 或需要准入驗證的目標,因為此整合無法提供 OpenCodex 要求的准入標頭。 +macOS 私有偏好設定僅提供 Pro 狀態提示;Windows 完全不讀取該設定,狀態會是未知。 +此提示不會阻擋寫入。匯出中繼資料並未證實每個模型的工具能力。其他 provider 的值會保留, +但不保證 YAML 格式與註解不變。格式說明見 +[manual.raycast.com/ai/custom-providers](https://manual.raycast.com/ai/custom-providers)。 + +Raycast CLI 匯出與儀表板下載會使用執行中伺服器的目標位址和准入規則,包含已設定的 +無驗證 loopback listener。`ocx ensure` 不會以可能與執行中伺服器不同的已儲存設定快照 +重新整理 Raycast;伺服器啟動與明確執行的同步仍會更新目錄。 + + 路徑遵循客戶端自己的環境覆寫(environment override)。對 OMP 而言,`OMP_PROFILE` 以存在與否優先於 `PI_PROFILE`,即使明確為空也一樣。具名 profile 會把 `PI_CONFIG_DIR` 當作相對於使用者家目錄的目錄名稱,並忽略 `PI_CODING_AGENT_DIR`;沒有具名 profile 時,`PI_CODING_AGENT_DIR` 勝出。OMP 支援 provider 層級的 headers,但這個最初的整合刻意只支援 loopback;遠端 `x-opencodex-api-key` 的連線設定被延後。搬移過的 `HERMES_HOME`、`KIMI_CODE_HOME` 與 `XDG_CONFIG_HOME` 路徑同樣會被遵循,而非猜測。表格列出每個客戶端的預設值。 對原生 OpenAI 模型,產生的 OMP 區塊會選用其模型層級的 Responses API,保留圖片輸入與 reasoning-effort 控制。路由模型則維持 provider 的 Chat Completions 方言,讓它們既有的 adapters 保持相容。 @@ -52,7 +80,7 @@ opencodex 從自己的環境讀取這些變數。如果你的 gateway 以 profil - **Restore this point…** 會出現在較舊的操作上,或當檔案在那次操作之後有變更時。跨過這樣的變更做回復會再詢問一次,才覆蓋你的較新編輯——並且也會備份它們,所以那次的回復本身也可以復原。 - 每個客戶端保留十份備份。超過之後,最舊的快照檔案會被移除,其歷史列顯示為 **Backup expired**。 -停用只移除 opencodex 記錄為自己寫入的條目。如果你的檔案在我們寫入之後有變更,後續行為取決於我們自己的條目是否完好,以及檔案的格式。對於嚴格 JSON 設定檔(OpenCode、Pi),在我們的區塊**旁邊**進行的編輯——例如新增 MCP 伺服器或你自己的 provider——會顯示為**需要更新**:重新整理會在保留你的條目的前提下合併寫入,但格式可能會被正規化。例外情況是 JSON 無法精確重寫的內容——例如 `1e999` 這類非有限數字、重寫會被四捨五入的數字(極大的整數,或小到會塌縮成零的數字)、`-0`、同一個物件裡重複出現的鍵,或巢狀層數超過 1000 層——此時開關會鎖定,確保沒有任何值被悄悄改動或刪除。**OMP** 同樣不受旁邊編輯影響,但原因不同:它的 writer 只逐位元組修補自己的 `providers.opencodex` 範圍,檔案其餘部分從不會被重寫。至於其餘可以包含註解的格式(Hermes、OpenClaw、Kimi Code、Gajae Code、MiniMax Code——以整份文件寫出的 YAML、JSON5 與 TOML),或當我們自己的條目被編輯過時,開關會鎖定,停用會拒絕執行,而不是猜測哪些編輯是你的。 +停用只移除 opencodex 記錄為自己寫入的條目。如果你的檔案在我們寫入之後有變更,後續行為取決於我們自己的條目是否完好,以及檔案的格式。對於嚴格 JSON 設定檔(OpenCode、Pi),在我們的區塊**旁邊**進行的編輯——例如新增 MCP 伺服器或你自己的 provider——會顯示為**需要更新**:重新整理會在保留你的條目的前提下合併寫入,但格式可能會被正規化。例外情況是 JSON 無法精確重寫的內容——例如 `1e999` 這類非有限數字、重寫會被四捨五入的數字(極大的整數,或小到會塌縮成零的數字)、`-0`、同一個物件裡重複出現的鍵,或巢狀層數超過 1000 層——此時開關會鎖定,確保沒有任何值被悄悄改動或刪除。**OMP** 同樣不受旁邊編輯影響,但原因不同:它的 writer 只逐位元組修補自己的 `providers.opencodex` 範圍,檔案其餘部分從不會被重寫。至於其餘可以包含註解的格式(Hermes、OpenClaw、Kimi Code、Gajae Code、MiniMax Code、Raycast——以整份文件寫出的 YAML、JSON5 與 TOML),或當我們自己的條目被編輯過時,開關會鎖定,停用會拒絕執行,而不是猜測哪些編輯是你的。 ## 誠實的預期 @@ -98,9 +126,11 @@ ocx integration client enable --client mcode ocx mcode ``` -完成一次連接後,`ocx sync` 也會以目前的 context window 與 reasoning-effort 階梯更新 -OpenCodex 已擁有的 MCode 區塊。若區塊已刪除、遭外部修改、不安全或從未由 OpenCodex -建立,sync 會保持原檔不動;只有在你確定要重新連接時才再次執行 enable。 +完成一次連接後,`ocx sync` 與 `POST /api/sync` 會更新 OpenCodex 已擁有的 +MCode、Pi、Aside 與 Raycast 目錄。proxy 啟動也會更新已擁有的 Raycast 目錄。 +模型可見性、provider 或 preset 變更會更新 Pi、Aside 與 Raycast。若區塊已刪除、 +遭外部修改、不安全或由你手動移除,sync 會保持原檔不動;只有在你確定要重新 +連接時才再次執行 enable。 另一個 MiniMax 平台 CLI(`mmx`)不是檔案開關整合。其文字命令使用 MiniMax 的 Anthropic 相容端點,因此 OpenCodex 提供憑證隔離、僅限 loopback 的 launcher: diff --git a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md index e0946db620..d944aef199 100644 --- a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md @@ -134,3 +134,11 @@ V2 在用戶端目錄狀態允許時,可以額外接收基於原有優先級 `disabledModels` 和各供應商的 `selectedModels` 仍是可見性欄位。沒有獨立的 `modelOrder`、 `providerOrder` 或優先級對應表設定。 + +## 儀表板排序預設 + +在 **Models** 選擇預設、依模型名稱 A–Z、依供應商或使用量快照,再套用順序。儲存目前可用的路由 ID 和 `modelPickerOrderMode`(`alphabetical`、`provider`、`most-used`)。使用量排序僅在套用時讀取一次保留的全部歷史;重新開啟或模型增減不會重新計算。現有自訂與原生完整順序會保留,直到明確套用替換。即使沒有可用模型,預設也能清除兩個欄位。 + +`GET/PUT /api/subagent-models` 的 `chosen`、`available` 保留停用或缺少的已存 roster;`pickerAvailable` 僅包含可選路由 ID。Models 只傳送 `pickerOrder`、`pickerOrderMode`,不傳送 `models`。只儲存 roster 不影響排序;無效輸入或儲存失敗會保留原狀態。 + +預設保留精選與原生優先級區間,套用於 Codex 目錄和 Claude 探索清單的路由群組。Claude 原生前綴、明確的 Desktop 設定及 alias 歸屬不變。OpenCodex 指引排序和 fallback 設定不變,但原生 Codex 工具顯示的前五個候選與建議預設模型可能改變。儲存不會重新啟動用戶端;目錄更新可能尚未完成,舊清單可能需要重新開啟用戶端。 diff --git a/docs-site/src/content/docs/zh-tw/guides/pi.md b/docs-site/src/content/docs/zh-tw/guides/pi.md index 0353338574..d8e9b62510 100644 --- a/docs-site/src/content/docs/zh-tw/guides/pi.md +++ b/docs-site/src/content/docs/zh-tw/guides/pi.md @@ -23,6 +23,9 @@ ocx export --client pi "baseUrl": "http://127.0.0.1:10100/v1", "api": "openai-completions", "apiKey": "$OPENCODEX_API_KEY", + "compat": { + "sendSessionAffinityHeaders": true + }, "models": [ { "id": "anthropic/claude-opus-5", @@ -37,6 +40,8 @@ ocx export --client pi } ``` +產生的 Pi 供應商設定會啟用 `compat.sendSessionAffinityHeaders`。合併或手動編輯供應商時請保留此設定:Pi 提供穩定的工作階段識別碼,OpenCodex 據此為標準 OpenCode Go 目標產生工作階段親和識別碼。當 `cacheRetention` 為 `none` 時,Pi 可能不傳送識別碼。 + 模型 id 是代理的規範選擇器,因此路由模型顯示為 `provider/model`(`anthropic/claude-opus-5`),而原生 OpenAI slug 保持無前綴(`gpt-5.6-sol`)。`name` 後綴 — `(anthropic)`、`(native)`、`(routed)` — 正是讓來自不同上游的兩個同名模型在 Pi 的 picker 中可區分的關鍵。 ## 放置位置 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 be149d5901..99e57e3894 100644 --- a/docs-site/src/content/docs/zh-tw/guides/providers.md +++ b/docs-site/src/content/docs/zh-tw/guides/providers.md @@ -112,6 +112,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | 實驗性 PKCE 登入、即時 HTTP/2 transport 與按帳號篩選的模型探索。 | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 實驗性。GitHub device flow + `copilot_internal` exchange(VS Code OAuth client)。需要有效 Copilot 訂閱;不是官方第三方 API。 | +Google Antigravity 帳戶與供應商的配額查詢(包括模型清單備援)使用固定的 Google 計量端點。這些目標支援透明 Fake-IP DNS,同時保留 TLS 驗證、重新導向拒絕與私有位址檢查。自訂 base URL 只改變模型請求,不改變配額目標;`NO_PROXY` 仍使用直連政策。 + + 終端 Nous refresh 失敗後,執行 `ocx login nous` 重新認證。 對 canonical Kimi Coding Plan preset(`kimi` 帳號登入與 `kimi-code` API key),opencodex 只會把 caller @@ -270,6 +273,7 @@ IDE/CLI,不透過 API;`minimax/minimax-m2.5` 是文件列出的 API 免費 | NVIDIA NIM | `https://integrate.api.nvidia.com/v1` | | Z.AI (GLM Coding) | `https://api.z.ai/api/coding/paas/v4` | | Zhipu AI (BigModel) | `https://open.bigmodel.cn/api/paas/v4` | +| [BigModel Coding Plan — Responses (靜態模型清單)](/guides/providers/#bigmodel-coding-plan-over-responses) | `https://open.bigmodel.cn/api/v1` | | Qwen Cloud | Token plan(預設):`https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` · pay as you go:`https://dashscope.aliyuncs.com/compatible-mode/v1` · 或 Custom | | Tencent Cloud Coding Plan | `https://api.lkeap.cloud.tencent.com/coding/v3` | | SiliconFlow | `https://api.siliconflow.cn/v1` | @@ -417,7 +421,7 @@ quota probe 只會把 active key 傳送到 canonical A6API host,並拒絕 redi > **Tencent Cloud Coding Plan 使用限制:** Tencent 文件將此訂閱限定為互動式 coding tool。一般 API > automation、自訂 application backend 與非互動 batch 使用都被禁止,並可能造成 plan key 被停用。 -> **兩條 GLM 路徑:** `zai` 是 Z.AI 國際 Coding Plan 訂閱;`zhipu-bigmodel` 是智譜國內 BigModel +> **GLM 計費路徑:** `zai` 是 Z.AI 國際 Coding Plan 訂閱;`zhipu-bigmodel` 是智譜國內 BigModel > pay-as-you-go endpoint。兩者 host、key 與 billing 都不同;其中一邊發出的 key 無法在另一邊通過認證。 ### 多個 API 金鑰 @@ -458,8 +462,8 @@ Antigravity/Cloud Code Assist 模式)、`azure` / `azure-openai`、`kiro`、 短效 Copilot API token,不是貼上 API key。**GitLab Duo** 仍是使用 OpenAI-compatible endpoint 的 key/subscription-token gateway。**Cloudflare AI Gateway** 需要在 URL 填入 account 與 gateway id。 -Copilot 的 catalog 混合多種 wire:GPT-5 family(`gpt-5.3-codex`、`gpt-5.4`、`gpt-5.4-mini`、 -`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`)會拒絕 agent traffic 的 +Copilot 的 catalog 混合多種 wire:模型(`gpt-5.3-codex`、`gpt-5.4`、`gpt-5.4-mini`、 +`gpt-5.5`、`gpt-5.6-luna`、`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-6-astra`, `grok-4.5`, `grok-4.6`, `mai-code-1.1-flash`, `mai-code-1-flash-picker`)會拒絕 agent traffic 的 `/chat/completions`,因此 opencodex 會依內建預設把這些模型路由到 Responses API;其他 Copilot 模型 仍使用 chat completions。優先順序為:hard wire pin → 你明確設定的 [`modelAdapters`](/zh-tw/reference/configuration/providers/) → registry default → provider-wide adapter。 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 b88ddf6e40..69f8010810 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 @@ -130,7 +130,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON ## 客戶端設定匯出 -### `ocx export --client ` +### `ocx export --client ` 印出連接到執行中代理的客戶端設定。此指令會用所選客戶端的原生格式,序列化含有 base URL、模型清單,以及適用的環境變數參考或 loopback 佔位符的 `opencodex` provider 區塊。 @@ -138,7 +138,7 @@ ocx claude desktop import [--apply] 驗證並匯入 JSON | 旗標 | 動作 | | --- | --- | -| `--client ` | 必填。選擇客戶端設定格式。 | +| `--client ` | 必填。選擇客戶端設定格式。 | | `--json` | 僅在 stdout 印出設定 JSON,使重導向能擷取逐位元組輸出。所有診斷訊息(含 `--out` 寫入提示)皆送至 stderr。 | | `--out ` | 將設定寫入 ``。拒絕覆寫既有檔案。 | | `--force` | 允許 `--out` 覆寫既有檔案。 | @@ -165,6 +165,9 @@ ocx export --client opencode --out ~/opencodex-opencode.json | `mcode` | `~/.minimax/config.yaml` (設定後 `MINIMAX_DATA_DIR` 優先,其次為舊的 `MAVIS_DATA_DIR`;相對路徑會被拒絕) | `mcode-config.yaml` | 無——loopback 佔位符 | | `zcode` | `~/.zcode/v2/config.json` (設定後 `ZCODE_DATA_DIR` 優先;相對路徑會被拒絕) | `config.json` | 無——loopback 佔位符 | | `prime` | `~/.prime/agent/models.json` (設定後 `PRIME_AGENT_CODING_AGENT_DIR` 優先;相對路徑會被拒絕) | `prime-models.json` | 無——loopback 佔位符 | +| `raycast` | `~/.config/raycast/ai/providers.yaml`(macOS 與 Windows 相同;Raycast 不遵循 `XDG_CONFIG_HOME`) | `raycast-providers.yaml` | 無——僅限 loopback,不會寫入 `api_keys` 項目 | + +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 會被拒絕。 opencode 會插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。Pi 與 OMP 的匯出不需要環境變數, 而是帶有字面值 `opencodex-loopback`。DSH 匯出需要 DSH 0.1.0-rc.6 或更新版本,且只擁有 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md index fbe9c5c1f0..5209ce57c7 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/providers-accounts.md @@ -13,7 +13,7 @@ description: 供應商設定、憑證、配額與模型目錄指令。 | 子指令 | 支援的旗標 | 動作 | | --- | --- | --- | -| `list` | `--json` | 列出已設定的供應商與剩餘的 registry 項目。 | +| `list` | `--json`, `--jsonl` | 列出已設定的供應商與剩餘的 registry 項目。 `--jsonl` 為每個已設定的供應商輸出一行 JSON 物件。 | | `add ` | `--adapter `, `--base-url `, `--api-key `, `--default-model `, `--set-default`, `--force`, `--json`, `--sync` | 新增 registry/自訂供應商。`--force` 覆寫;`--sync` 在人類輸出模式下重新整理執行中的代理。 | | `edit ` | 供應商欄位旗標, `--json` | 編輯已驗證的即時供應商欄位而不替換金鑰池。 | | `test ` | `--json` | 探測真實上游模型端點。 | @@ -27,6 +27,7 @@ description: 供應商設定、憑證、配額與模型目錄指令。 ```bash ocx provider list --json +ocx provider list --jsonl ocx provider test ark ocx provider add anthropic --api-key sk-ant-... --set-default --sync ocx provider add local-dev --adapter openai-chat --base-url http://localhost:11434/v1 @@ -35,6 +36,8 @@ ocx models --provider anthropic --json ocx models live --provider ark --json ``` +`--jsonl` 僅輸出已設定的供應商,每行一個 JSON 物件。每個物件的欄位與 `--json` 輸出中 `configured` 陣列的元素相同,不包含 `registryCount` 摘要。指令碼可以逐行處理這些物件。`--json` 與 `--jsonl` 不能同時使用。 + ## 認證 ### `ocx login ` @@ -129,10 +132,11 @@ Codex 池選擇套用於清除既有親和性後的下一個請求;進行中 ### `ocx account auto-switch > [--json]` -僅控制 `openai` Codex 帳號池。`on` 設為 80%,`off` 設為 0%,`status` 讀取目前值,而 `threshold ` 接受 0 到 100 的整數。其他供應商與無效值離開 1。`--json` 回傳: +控制 `openai` Codex 帳戶池閾值,或儲存通用 OAuth 帳戶池閾值。`on` 儲存 80%,`off` 儲存 0%,`threshold ` 接受 0–100。通用池的閾值目前不參與執行;儲存閾值不會啟用閾值切換、改變供應商啟用設定或停用 429 錯誤後的輪替。通用池的查詢與修改結果使用伺服器確認值。通用池的 `poolEnabled` 是已儲存的供應商設定,`null` 表示未指定,並不代表繼承後的實際狀態。`inert: true` 表示閾值未套用;能力未知時也不會回報 `enabled: true`。API 金鑰供應商、Anthropic 與無效值會被拒絕。 ```text -{ provider, autoSwitchThreshold: number, enabled: boolean } +openai: { provider, autoSwitchThreshold: number, enabled: boolean } +generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: true | null } ``` ### `ocx account login|reauth|code|cancel ...` 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 34b2e95cf8..b95538e447 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 @@ -361,6 +361,18 @@ Vercel AI Gateway 可在多個底層推論供應商之間路由一個模型。`v } ``` +## 模型顯示名稱編輯器 + +儀表板的 **Models** 可讓你為已探索到的模型持久儲存易讀名稱。展開供應商,找到已探索到的模型,然後選擇 **Name**。 +儲存易讀名稱時,對話方塊會持續顯示精確的 `provider/model` 選擇器。選擇 **Reset name** 可回到 +供應商中繼資料中的名稱,或預設的選擇器顯示。**Name** 只改變顯示;獨立的別名鉛筆圖示用來修改 +短路由別名,並不是顯示名稱編輯器。原生 OpenAI 與自訂模型列保留既有控制項。 + +若變更已儲存但重新整理失敗,對話方塊會反映已儲存的覆寫值,並繼續提供 **Retry**。若伺服器回報 +目錄收斂失敗,Retry 會重新執行目錄收斂;若只有清單請求失敗,則重新載入清單。重設後的復原 +會保留重設操作,不會還原舊名稱。請求的總期限為 60 秒,涵蓋寫入及後續的清單重新整理。逾時不會 +撤銷寫入:進行其他變更前,請使用 **Retry** 檢查目前名稱。 + ## 完整範例 ```json diff --git a/docs/pr-assets/codex-desktop-opt-in.jpg b/docs/pr-assets/codex-desktop-opt-in.jpg new file mode 100644 index 0000000000..d06203e711 Binary files /dev/null and b/docs/pr-assets/codex-desktop-opt-in.jpg differ diff --git a/gui/.eslint/i18n-allowlist.ts b/gui/.eslint/i18n-allowlist.ts index acab975933..805d7dcfbc 100644 --- a/gui/.eslint/i18n-allowlist.ts +++ b/gui/.eslint/i18n-allowlist.ts @@ -59,6 +59,9 @@ export function isTechnicalLiteral(value: string): boolean { // Version prefix / unit tokens if (TECHNICAL_UNITS.has(trimmed)) return true; + // Internal Models session-cache suffix, never rendered as user-facing copy. + if (trimmed === ":picker-order") return true; + // Absolute/relative URLs and localhost endpoints if (/^https?:\/\//i.test(trimmed)) return true; if (/^https?:\/\/[^\s]+$/i.test(trimmed)) return true; diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md index 1fc7c57857..f5e64b568b 100644 --- a/gui/public/provider-icons/README.md +++ b/gui/public/provider-icons/README.md @@ -47,6 +47,16 @@ Export-client marks (used by the API tab's connect rows, not the provider list): on the web (`aside.com/favicon.svg` is a 404), so the shipping application is the first-party source. +- `raycast.svg` — fetched 2026-09-04 from + `https://fz1sd71lwhbqy6sh.public.blob.vercel-storage.com/press/images/logo/raycast-logo-dark.svg`, + the "Logo (dark)" download Raycast's own press kit (`raycast.com/press`) links. + `raycast.com/favicon.svg` and the other conventional paths are 404s, so the + press kit is the first-party source. Path data and the `#FF6363` fill are + verbatim; the fixed `width`/`height` are dropped in favour of the `viewBox`, + and the `` wrapper — a full-frame white `` the export tool left + behind — is removed because the path never leaves the frame and the rect + would read as a second ink to the mark tooling here. + - `minimax.svg` — fetched 2026-08-31 from `https://raw.githubusercontent.com/MiniMax-AI/MiniMax-01/main/figures/minimax.svg`, MiniMax's own symbol as committed in their own model repository. The API-docs @@ -135,6 +145,9 @@ Decisions that are not obvious from looking at the file: - `aside.svg` **is masked.** It already paints with `currentColor`, so it would follow the theme either way; masking keeps it consistent with the other silhouettes rather than depending on inherited color. +- `raycast.svg` **is not masked.** One ink, but that ink is #FF6363 — Raycast + red, the same case as `openai.svg` and `deepseek-harness.svg`. Legible on both + surfaces as an image. Both directions are enforced in `gui/tests/integration-marks.test.ts`, including a luminance check that fails any single-ink near-neutral mark left as an image. That diff --git a/gui/public/provider-icons/raycast.svg b/gui/public/provider-icons/raycast.svg new file mode 100644 index 0000000000..b6a40c7ba2 --- /dev/null +++ b/gui/public/provider-icons/raycast.svg @@ -0,0 +1,3 @@ + + + diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index bab41b1eee..c5971ffb6b 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -100,6 +100,7 @@ export const INTEGRATION_TAB_HASHES = [ "integrations/zcode", "integrations/prime", "integrations/aside", + "integrations/raycast", ] as const; export function hashBelongsToPage(rawHash: string, page: Page): boolean { diff --git a/gui/src/components/AddProviderModal.tsx b/gui/src/components/AddProviderModal.tsx index 6ce422bd50..99c4505014 100644 --- a/gui/src/components/AddProviderModal.tsx +++ b/gui/src/components/AddProviderModal.tsx @@ -202,7 +202,11 @@ export default function AddProviderModal({ } }; - const { loginOAuth, submitManualCode: submitManualCodeApi } = useAddProviderOAuth({ apiBase, t, aliveRef, onAdded }); + const { + cancelLoginOAuth, + loginOAuth, + submitManualCode: submitManualCodeApi, + } = useAddProviderOAuth({ apiBase, t, aliveRef, onAdded }); const oauthSetters = { setOauthBusy: (busy: boolean) => dispatch({ type: "set-oauth-busy", busy }), @@ -308,12 +312,17 @@ export default function AddProviderModal({ manualCodeMsg={manualCodeMsg} manualCodeOk={manualCodeOk} onRequestLogin={requestLoginOAuth} + onCancelLogin={providerId => { void cancelLoginOAuth(providerId, oauthSetters, preset.label); }} onUseApiKeyInstead={() => { + if (oauthBusy && preset.oauthProvider) void cancelLoginOAuth(preset.oauthProvider, oauthSetters, preset.label); dispatch({ type: "use-api-key-instead", form: { ...form, authMode: "key" } }); }} onManualCodeChange={code => dispatch({ type: "set-manual-code", code })} onSubmitManualCode={providerId => { void submitManualCode(providerId); }} - onBack={() => dispatch({ type: "back" })} + onBack={() => { + if (oauthBusy && preset.oauthProvider) void cancelLoginOAuth(preset.oauthProvider, oauthSetters, preset.label); + dispatch({ type: "back" }); + }} /> ) : ( void; + onEdit?: () => void; + onSave: (displayName: string) => void; + onReset: () => void; + onClose: () => void; +} + +const SOURCE_LABEL_KEYS: Record, TKey> = { + operator: "models.displayNameSourceOperator", + provider: "models.displayNameSourceProvider", + fallback: "models.displayNameSourceFallback", +}; + +export default function ModelDisplayNameDialog({ + model, + saving, + requestError, + currentNamePending = false, + mutationOutcomeUnknown = false, + onRetry, + onEdit, + onSave, + onReset, + onClose, +}: ModelDisplayNameDialogProps) { + const t = useT(); + const dialogRef = useRef(null); + const inputRef = useRef(null); + const submitRef = useRef(null); + const wasSavingRef = useRef(saving); + const titleId = useId(); + const helpId = useId(); + const errorId = useId(); + const [draftSnapshot, setDraftSnapshot] = useState(model); + const [draft, setDraft] = useState(model.displayNameOverride ?? ""); + const [validationKey, setValidationKey] = useState(null); + + useEffect(() => { + const dialog = dialogRef.current; + if (dialog && !dialog.open) dialog.showModal(); + inputRef.current?.focus(); + return () => { if (dialog?.open) dialog.close(); }; + }, []); + + useEffect(() => { + const saveFailed = wasSavingRef.current && !saving && Boolean(requestError); + wasSavingRef.current = saving; + if (saveFailed) { + if (mutationOutcomeUnknown) submitRef.current?.focus(); + else inputRef.current?.focus(); + } + }, [requestError, saving, mutationOutcomeUnknown]); + + // Parent replaces this snapshot only after a confirmed mutation, not typing or polling. + // Adjust before committing children, preserving the mounted dialog and its focus refs. + if (draftSnapshot !== model) { + setDraftSnapshot(model); + setDraft(model.displayNameOverride ?? ""); + setValidationKey(null); + } + + const validationError = validationKey ? t(validationKey) : null; + const visibleError = validationError ?? requestError; + const sourceKey = model.displayNameSource + ? SOURCE_LABEL_KEYS[model.displayNameSource] + : "models.displayNameSourceFallback"; + + const requestClose = () => { + if (!saving) onClose(); + }; + + return ( + { + event.preventDefault(); + requestClose(); + }} + > + +
+ +
+ {t("models.displayNameModelId")} + {model.namespaced} +
+ +
+ {t("models.displayNameCurrent")} + {currentNamePending ? t("models.displayNameCurrentUnavailable") : model.displayName ?? model.namespaced} + {!currentNamePending && {t(sourceKey)}} +
+ + + { + if (saving || mutationOutcomeUnknown) return; + onEdit?.(); + setDraft(event.target.value); + setValidationKey(null); + }} + /> +

+ {t("models.displayNameHelp", { model: model.namespaced })} +

+ {visibleError && ( + + )} + +
+ + + +
+ + + ); +} diff --git a/gui/src/components/ModelPickerOrderEditor.tsx b/gui/src/components/ModelPickerOrderEditor.tsx new file mode 100644 index 0000000000..e5a29efcc9 --- /dev/null +++ b/gui/src/components/ModelPickerOrderEditor.tsx @@ -0,0 +1,194 @@ +import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useState, type DragEvent } from "react"; +import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch"; +import { readJsonOrThrow } from "../fetch-json"; +import { IconArrowDown, IconArrowUp, IconGrip } from "../icons"; +import { useT, type TKey } from "../i18n/shared"; +import { + customPickerRows, isPickerOrderSaved, isPickerOrderSettings, movePickerBefore, + pickerSnapshotSignature, stepPickerOrder, type PickerModelIdentity, type PickerOrderSaved, +} from "../model-picker-order"; + +type Receipt = PickerOrderSaved & { catalogRefresh?: unknown }; +type Snapshot = { signature: string; identities: string; order: string[]; fixed: string[] }; +const DRAG_TYPE = "application/x-ocx-picker-order"; +let dragSequence = 0; +/** Local drag identity, not a security token. Like newClientId, supports LAN HTTP. */ +function newDragToken(): string { + const sequence = ++dragSequence; + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + try { return `${sequence}:${crypto.randomUUID()}`; } + catch { /* Some browsers expose randomUUID but reject it outside secure contexts. */ } + } + return `picker-${Date.now().toString(36)}-${sequence}`; +} + +export default function ModelPickerOrderEditor({ apiBase, active, identities, onAccepted, onBusyChange }: { + apiBase: string; active: boolean; identities: readonly PickerModelIdentity[]; + onAccepted: (receipt: Receipt) => void; onBusyChange: (busy: boolean) => void; +}) { + const t = useT(); + const [snapshot, setSnapshot] = useState(null); + const [draft, setDraft] = useState([]); + const [busy, setBusy] = useState(false); + const [blocked, setBlocked] = useState(null); + const [error, setError] = useState(false); + const [announcement, setAnnouncement] = useState(""); + const [dragging, setDragging] = useState(null); + const [over, setOver] = useState(null); + const lifetime = useRef({ + generation: 0, + flight: null as BoundedFetch | null, + drag: null as { id: string; token: string } | null, + }); + const [activation, setActivation] = useState({ apiBase, active, onBusyChange }); + const identitySignature = JSON.stringify(identities.map(({ provider, id, namespaced }) => [provider, id, namespaced])); + const latestIdentitySignature = useRef(identitySignature); + useLayoutEffect(() => { latestIdentitySignature.current = identitySignature; }, [identitySignature]); + const identityChanged = snapshot !== null && snapshot.identities !== identitySignature; + const disabled = !active || busy || !snapshot || blocked !== null || identityChanged; + const dirty = snapshot !== null && JSON.stringify(draft) !== JSON.stringify(snapshot.order); + const clearDrag = useCallback(() => { lifetime.current.drag = null; setDragging(null); setOver(null); }, []); + + // Reconcile before committing children, like the existing display-name dialog. + if (activation.apiBase !== apiBase || activation.active !== active || activation.onBusyChange !== onBusyChange) { + setActivation({ apiBase, active, onBusyChange }); + setSnapshot(null); setDraft([]); setBlocked(null); setError(false); setBusy(false); + } + const [dragContext, setDragContext] = useState({ disabled, snapshot, identitySignature }); + if (dragContext.disabled !== disabled || dragContext.snapshot !== snapshot || dragContext.identitySignature !== identitySignature) { + setDragContext({ disabled, snapshot, identitySignature }); + setDragging(null); setOver(null); + } + + // Capture the stable holder, but always abort its CURRENT flight during cleanup. + useLayoutEffect(() => { + const holder = lifetime.current; + holder.generation++; + return () => { + holder.generation++; + holder.flight?.controller.abort(); holder.flight?.clear(); holder.flight = null; + holder.drag = null; onBusyChange(false); + }; + }, [apiBase, active, onBusyChange]); + useLayoutEffect(() => { lifetime.current.drag = null; }, [disabled, snapshot, identitySignature]); + + const run = async (save: boolean) => { + if (!active || lifetime.current.flight || (save && (disabled || !dirty))) return; + const owner = lifetime.current.generation, bounded = createBoundedFetch(15_000); + lifetime.current.flight = bounded; setBusy(true); onBusyChange(true); setError(false); clearDrag(); + const owns = () => lifetime.current.generation === owner && lifetime.current.flight === bounded; + const current = () => owns() && !bounded.signal.aborted + && latestIdentitySignature.current === identitySignature; + try { + const response = await fetch(`${apiBase}/api/subagent-models`, { signal: bounded.signal }); + if (!current()) return; + const settings = await readJsonOrThrow(response); + if (!current()) return; + if (!isPickerOrderSettings(settings)) throw new Error("Invalid picker settings"); + const signature = pickerSnapshotSignature(apiBase, owner, settings); + if (save && (!snapshot || signature !== snapshot.signature || identitySignature !== snapshot.identities)) { + setBlocked("models.pickerOrder.changed"); return; + } + const rows = customPickerRows(settings, identities); + if (!rows) { + setBlocked(settings.pickerOrder.some(id => !id.includes("/")) + ? "models.pickerOrder.nativeLocked" : settings.chosen === undefined + ? "models.pickerOrder.unknownChosen" : "models.pickerOrder.catalogRequired"); + return; + } + if (!save) { + setSnapshot({ ...rows, signature, identities: identitySignature }); setDraft(rows.order); + setBlocked(null); setAnnouncement(""); return; + } + const result = await fetch(`${apiBase}/api/subagent-models`, { + method: "PUT", headers: { "Content-Type": "application/json" }, signal: bounded.signal, + body: JSON.stringify({ pickerOrder: draft, pickerOrderMode: null }), + }); + if (!current()) return; + const receipt = await readJsonOrThrow(result); + if (!current()) return; + if (!isPickerOrderSaved(receipt) || !("ok" in receipt) || receipt.ok !== true) throw new Error("Invalid picker receipt"); + setDraft(receipt.pickerOrder); setBlocked("models.pickerOrder.savedReload"); + onAccepted({ pickerOrder: receipt.pickerOrder, pickerOrderMode: receipt.pickerOrderMode, + catalogRefresh: "catalogRefresh" in receipt ? receipt.catalogRefresh : undefined }); + } catch { + if (owns() && latestIdentitySignature.current === identitySignature) setError(true); + // Current-identity timeouts surface an error; stale identities retain the draft silently. + } finally { + bounded.clear(); + if (owns()) { lifetime.current.flight = null; setBusy(false); onBusyChange(false); } + } + }; + const enter = useEffectEvent(async () => { + const holder = lifetime.current, owner = holder.generation; + // Automatic startup is cancellable before issuing transport; event actions stay immediate. + await Promise.resolve(); + if (active && holder.generation === owner) void run(false); + }); + useEffect(() => { if (active) void enter(); }, [apiBase, active, onBusyChange]); + + const move = (id: string, next: string[]) => { + if (disabled) return; + setDraft(next); + setAnnouncement(t("models.pickerOrder.position", { model: id, position: next.indexOf(id) + 1, total: next.length })); + clearDrag(); + }; + const draftSet = new Set(draft); + const fixedSet = new Set(snapshot?.fixed); + const movable = (id: string) => !disabled && draftSet.has(id) && !fixedSet.has(id); + const dragOver = (event: DragEvent, id: string) => { + if (!lifetime.current.drag || lifetime.current.drag.id === id || !movable(lifetime.current.drag.id) || !movable(id) + || !event.dataTransfer.types.includes(DRAG_TYPE)) return; + event.preventDefault(); event.dataTransfer.dropEffect = "move"; setOver(id); + }; + return
+

{t("models.pickerOrder.editorHint")}

+ {(blocked || identityChanged) &&

{t(blocked ?? "models.pickerOrder.changed")}

} + {error &&

{t("models.pickerOrder.requestFailed")}

} + {snapshot && draft.length === 0 &&

{t("models.pickerOrder.empty")}

} +
    + {draft.map((id, index) => { + const fixed = fixedSet.has(id); + return
  1. dragOver(event, id)} + onDragLeave={() => setOver(null)} + onDrop={event => { + const source = lifetime.current.drag; + if (source && source.id !== id && source.token === event.dataTransfer.getData(DRAG_TYPE) && movable(source.id) && movable(id)) { + event.preventDefault(); move(source.id, movePickerBefore(draft, source.id, id, snapshot?.fixed ?? [])); + } + clearDrag(); + }} onDragEnd={clearDrag}> + + {id} + {fixed && {t("models.pickerOrder.featured")}} + + + + +
  2. ; + })} +
+

{announcement}

+
+ + +
+
; +} diff --git a/gui/src/components/ModelPriceDialog.tsx b/gui/src/components/ModelPriceDialog.tsx new file mode 100644 index 0000000000..59f385a841 --- /dev/null +++ b/gui/src/components/ModelPriceDialog.tsx @@ -0,0 +1,237 @@ +import { Fragment, useCallback, useEffect, useId, useRef, useState } from "react"; +import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch"; +import { readJsonOrThrow } from "../fetch-json"; +import { useT, type TKey } from "../i18n/shared"; +import type { ModelRow } from "../pages/models-shared"; + +interface Cost4 { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; +} + +const RATE_FIELDS = ["input", "output", "cacheRead", "cacheWrite"] as const; +const RATE_LABELS: Record = { + input: "pricing.override.input", + output: "pricing.override.output", + cacheRead: "pricing.override.cacheRead", + cacheWrite: "pricing.override.cacheWrite", +}; +const MAX_RATE = 1_000_000; +const REQUEST_TIMEOUT_MS = 60_000; +const EMPTY_DRAFT = { input: "", output: "", cacheRead: "", cacheWrite: "" }; +type Phase = "loading" | "loadFailed" | "ready" | "saving" | "unknown" | "refreshing" | "refreshFailed"; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isCost(value: unknown): value is Cost4 { + return isRecord(value) && RATE_FIELDS.every(field => ( + typeof value[field] === "number" && Number.isFinite(value[field]) + && value[field] >= 0 && value[field] <= MAX_RATE + )); +} + +interface ModelPriceDialogProps { + model: ModelRow; + apiBase: string; + onRefresh: (signal: AbortSignal) => Promise; + onClose: () => void; +} + +export default function ModelPriceDialog({ model, apiBase, onRefresh, onClose }: ModelPriceDialogProps) { + const t = useT(); + const id = useId(); + const dialogRef = useRef(null); + const inputRef = useRef(null); + const submitRef = useRef(null); + const requestRef = useRef(null); + const mutationPendingRef = useRef(false); + const [phase, setPhase] = useState("loading"); + const [draft, setDraft] = useState(EMPTY_DRAFT); + const [hasOverride, setHasOverride] = useState(false); + const [errorKey, setErrorKey] = useState(null); + const [recovered, setRecovered] = useState(false); + const endpoint = `${apiBase}/api/providers/${encodeURIComponent(model.provider)}/model-costs`; + const mutating = phase === "saving" || phase === "refreshing"; + const locked = phase !== "ready"; + + const readOverride = useCallback((recover = false) => { + if (requestRef.current) return; + const bounded = createBoundedFetch(REQUEST_TIMEOUT_MS); + requestRef.current = bounded; + void fetch(endpoint, { signal: bounded.signal, cache: "no-store" }).then(async response => { + const result = await readJsonOrThrow(response); + bounded.signal.throwIfAborted(); + if (!isRecord(result) || result.provider !== model.provider || !isRecord(result.modelCosts)) { + throw new Error("invalid model-costs response"); + } + const cost = Object.hasOwn(result.modelCosts, model.id) ? result.modelCosts[model.id] : undefined; + if (cost !== undefined && !isCost(cost)) throw new Error("invalid model cost"); + if (requestRef.current !== bounded) return; + setDraft(cost === undefined ? EMPTY_DRAFT : { + input: String(cost.input), output: String(cost.output), + cacheRead: String(cost.cacheRead), cacheWrite: String(cost.cacheWrite), + }); + setHasOverride(cost !== undefined); + // This read recovers an editable snapshot, not ordering against an earlier + // request still running on the server or writes from another client. + setRecovered(recover); + setPhase("ready"); + }).catch(() => { + if (requestRef.current !== bounded) return; + setPhase(recover ? "unknown" : "loadFailed"); + setErrorKey(recover ? "pricing.override.recoveryFailed" : "pricing.override.loadFailed"); + }).finally(() => { + bounded.clear(); + if (requestRef.current === bounded) requestRef.current = null; + }); + }, [endpoint, model.id, model.provider]); + + useEffect(() => { + const dialog = dialogRef.current; + if (dialog && !dialog.open) dialog.showModal(); + void readOverride(); + return () => { + requestRef.current?.controller.abort(); + requestRef.current?.clear(); + requestRef.current = null; + if (dialog?.open) dialog.close(); + }; + }, [readOverride]); + + useEffect(() => { + if (phase === "ready") inputRef.current?.focus(); + else if (phase === "unknown" || phase === "loadFailed" || phase === "refreshFailed") submitRef.current?.focus(); + }, [phase]); + + // undefined retries only catalog refresh after a validated persistence receipt. + const save = async (cost: Cost4 | null | undefined) => { + if (requestRef.current || (cost === undefined ? phase !== "refreshFailed" : phase !== "ready")) return; + const bounded = createBoundedFetch(REQUEST_TIMEOUT_MS); + requestRef.current = bounded; + setPhase(cost === undefined ? "refreshing" : "saving"); + mutationPendingRef.current = true; + setErrorKey(null); + let confirmed = cost === undefined; + try { + if (cost !== undefined) { + const response = await fetch(endpoint, { + method: "PUT", headers: { "content-type": "application/json" }, + body: JSON.stringify({ modelId: model.id, cost }), signal: bounded.signal, + }); + const result = await readJsonOrThrow(response); + bounded.signal.throwIfAborted(); + const receiptCost = isRecord(result) ? result.cost : undefined; + if (!isRecord(result) || result.ok !== true || result.provider !== model.provider + || result.modelId !== model.id || (cost === null ? receiptCost !== null + : !isCost(receiptCost) || !RATE_FIELDS.every(field => receiptCost[field] === cost[field]))) { + throw new Error("invalid model-costs receipt"); + } + if (requestRef.current !== bounded) return; + confirmed = true; + setPhase("refreshing"); + } + if (!await onRefresh(bounded.signal)) throw new Error("catalog refresh failed"); + bounded.signal.throwIfAborted(); + if (requestRef.current === bounded) onClose(); + } catch { + if (requestRef.current !== bounded) return; + setPhase(confirmed ? "refreshFailed" : "unknown"); + setErrorKey(confirmed ? "pricing.override.refreshFailed" : "pricing.override.outcomeUnknown"); + } finally { + bounded.clear(); + if (requestRef.current === bounded) { + requestRef.current = null; + mutationPendingRef.current = false; + } + } + }; + + const requestClose = () => { + if (!mutationPendingRef.current) onClose(); + }; + + return ( + { event.preventDefault(); requestClose(); }}> + + +
+ {t("pricing.override.modelId")} + {model.namespaced} +
+

{t("pricing.override.help")}

+ {phase === "loading" &&

{t("pricing.override.loading")}

} + {RATE_FIELDS.map(field => ( + + + { + if (locked || requestRef.current) return; + const value = event.target.value; + setDraft(current => ({ + ...current, + cacheRead: current.cacheRead || "0", cacheWrite: current.cacheWrite || "0", + [field]: value, + })); + setErrorKey(null); + }} /> + + ))} + {recovered &&

{t("pricing.override.recovered")}

} + {errorKey && } +
+ + + +
+ +
+ ); +} diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index 2715f48608..a1a32e1cfa 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -20,15 +20,38 @@ export type QuotaBarRow = { /** * Window ordering is computed from RAW wire identities BEFORE localization * (ranking on translated labels breaks the moment a locale changes copy): - * shorter windows first — 5h, weekly, cursor first-party, cursor API, monthly. + * shorter windows first — 5h, weekly, cursor first-party, cursor API, monthly, + * then subscription credits before other custom windows. */ function rawCustomWindowRank(rawLabel: string): number { if (rawLabel === "5h") return 0; if (rawLabel === "First-party models") return 2; if (rawLabel === "API usage") return 3; + if (rawLabel === "Total subscription credits") return 4.5; return 5; } +const SUBSCRIPTION_CREDITS_LABEL = "Total subscription credits"; + +function canonicalCustomWindowLabel(rawLabel: string): string { + return rawLabel.trim().toLowerCase() === SUBSCRIPTION_CREDITS_LABEL.toLowerCase() + ? SUBSCRIPTION_CREDITS_LABEL + : rawLabel; +} + +/** Coverage metadata carries raw labels, while subscription rows use a canonical identity. */ +export function isCustomQuotaWindowIncomplete( + customLabel: string | undefined, + incompleteLabels?: ReadonlySet, +): boolean { + if (customLabel === undefined || !incompleteLabels) return false; + const canonical = canonicalCustomWindowLabel(customLabel); + for (const label of incompleteLabels) { + if (canonicalCustomWindowLabel(label) === canonical) return true; + } + return false; +} + function localizeCustomQuotaLabel(rawLabel: string, t: TFn): string { switch (rawLabel) { case "First-party models": @@ -84,11 +107,12 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null | }); } for (const w of displayQuota.customWindows ?? []) { - const localized = localizeCustomQuotaLabel(w.label, t); + const customLabel = canonicalCustomWindowLabel(w.label); + const localized = localizeCustomQuotaLabel(customLabel, t); ranked.push({ - rank: rawCustomWindowRank(w.label), + rank: rawCustomWindowRank(customLabel), row: { - customLabel: w.label, + customLabel, label: localized, limitLabel: localized, percent: w.percent, @@ -96,6 +120,24 @@ export function buildQuotaRows(quota: AccountQuota | null, plan: string | null | }, }); } + if (displayQuota.creditsUsd && typeof displayQuota.creditsUsd.percent === "number") { + const hasSubscriptionCreditsCustom = displayQuota.customWindows?.some( + w => canonicalCustomWindowLabel(w.label) === SUBSCRIPTION_CREDITS_LABEL, + ); + if (!hasSubscriptionCreditsCustom) { + const localized = localizeCustomQuotaLabel(SUBSCRIPTION_CREDITS_LABEL, t); + ranked.push({ + rank: rawCustomWindowRank(SUBSCRIPTION_CREDITS_LABEL), + row: { + customLabel: SUBSCRIPTION_CREDITS_LABEL, + label: localized, + limitLabel: localized, + percent: displayQuota.creditsUsd.percent, + resetAt: displayQuota.creditsUsd.expiresAt, + }, + }); + } + } return ranked.sort((a, b) => a.rank - b.rank).map(entry => entry.row); } @@ -107,6 +149,12 @@ export function maxQuotaUtilisation(quota: AccountQuota | null): number { for (const w of quota.customWindows ?? []) { if (typeof w.percent === "number") vals.push(w.percent); } + const hasSubscriptionCreditsCustom = quota.customWindows?.some( + w => canonicalCustomWindowLabel(w.label) === SUBSCRIPTION_CREDITS_LABEL, + ); + if (!hasSubscriptionCreditsCustom && typeof quota.creditsUsd?.percent === "number") { + vals.push(quota.creditsUsd.percent); + } return vals.length ? Math.max(...vals) : -1; } @@ -279,7 +327,7 @@ export default function QuotaBars({ locale={locale} incomplete={row.windowKey ? incompleteWindowKeys?.has(row.windowKey) === true - : row.customLabel !== undefined && incompleteCustomWindowLabels?.has(row.customLabel) === true} + : isCustomQuotaWindowIncomplete(row.customLabel, incompleteCustomWindowLabels)} /> ))} @@ -291,6 +339,7 @@ export default function QuotaBars({ {rows.map(row => ( +
{label} {hasReset ? t("codexAuth.resets") : ""} {reset.day} diff --git a/gui/src/components/add-provider-oauth-pane.tsx b/gui/src/components/add-provider-oauth-pane.tsx index d3c06ac75a..c3b5f4d342 100644 --- a/gui/src/components/add-provider-oauth-pane.tsx +++ b/gui/src/components/add-provider-oauth-pane.tsx @@ -18,6 +18,7 @@ export function AddProviderOAuthPane({ manualCodeMsg, manualCodeOk, onRequestLogin, + onCancelLogin, onUseApiKeyInstead, onManualCodeChange, onSubmitManualCode, @@ -36,6 +37,7 @@ export function AddProviderOAuthPane({ manualCodeMsg: string; manualCodeOk: boolean; onRequestLogin: (providerId: string) => void; + onCancelLogin: (providerId: string) => void; onUseApiKeyInstead: () => void; onManualCodeChange: (value: string) => void; onSubmitManualCode: (providerId: string) => void; @@ -83,6 +85,11 @@ export function AddProviderOAuthPane({ {t("modal.useApiKeyInstead")}
+ {oauthBusy && preset.oauthProvider && ( + + )}
diff --git a/gui/src/components/apikeys-workspace/client-config-clients.ts b/gui/src/components/apikeys-workspace/client-config-clients.ts index c7c42d3e56..afd4484551 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"] as const; +export const CLIENTS = ["opencode", "pi", "omp", "hermes", "openclaw", "kimi", "gajae", "dsh", "mcode", "zcode", "prime", "aside", "raycast"] as const; export type ExportClientId = (typeof CLIENTS)[number]; export const CLIENT_LABEL_KEYS = { @@ -24,6 +24,7 @@ export const CLIENT_LABEL_KEYS = { zcode: "api.clientConfig.clientZcode", prime: "api.clientConfig.clientPrime", aside: "api.clientConfig.clientAside", + raycast: "api.clientConfig.clientRaycast", } as const; /** @@ -70,6 +71,8 @@ export const CLIENT_MARKS: Partial> = { zcode: "/provider-icons/zcode.svg", prime: "/provider-icons/prime-agent.svg", aside: "/provider-icons/aside.svg", + // Raycast red (#FF6363) is the brand, so like `dsh` it stays an image. + raycast: "/provider-icons/raycast.svg", }; /** diff --git a/gui/src/components/integration-marks.ts b/gui/src/components/integration-marks.ts index e8786224ec..eca38510bd 100644 --- a/gui/src/components/integration-marks.ts +++ b/gui/src/components/integration-marks.ts @@ -57,6 +57,7 @@ export const INTEGRATION_MARKS: Record = { zcode: CLIENT_MARKS.zcode ?? null, prime: CLIENT_MARKS.prime ?? null, aside: CLIENT_MARKS.aside ?? null, + raycast: CLIENT_MARKS.raycast ?? null, }; /** diff --git a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx index 214c814f62..a975786a94 100644 --- a/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx +++ b/gui/src/components/subagents-workspace/SubagentDelegationSection.tsx @@ -3,16 +3,16 @@ * * This panel used to sit on the Dashboard, which is otherwise a read-only status page — the * one place you could change something was also the first thing a new user saw. It reads - * better next to the roster it affects: the roster picks who may be called, while this panel - * holds guidance preferences and the native omitted-model synchronization setting. + * better next to the roster it affects: the roster picks who may be called, this picks who + * gets called first. */ -import { useState } from "react"; -import { Select, Switch, Tooltip } from "../../ui"; -import { IconInfo } from "../../icons"; +import { useLayoutEffect, useRef, useState } from "react"; +import { Select, Tooltip } from "../../ui"; +import { IconArrowDown, IconArrowUp, IconInfo, IconX } from "../../icons"; import { useT, type TKey } from "../../i18n/shared"; import { formatNamespacedModelId } from "../../provider-icons"; -import type { DelegationPatch, DelegationModelOption, NativeDefaultState } from "../../pages/use-subagent-delegation"; -import type { UltraModePatch, UltraModeState, V2NativeParentOverrideState, AgentTaskRecoveryState, V2RoutedDelegationBridgeState } from "../../pages/use-subagent-delegation"; +import type { DelegationPatch, DelegationModelOption } from "../../pages/use-subagent-delegation"; +import type { UltraModePatch, UltraModeState } from "../../pages/use-subagent-delegation"; export interface SubagentDelegationSectionProps { model: string; @@ -21,28 +21,20 @@ export interface SubagentDelegationSectionProps { available: DelegationModelOption[]; guidanceEnabled: boolean; syncCodexDefaults: boolean; - nativeDefaultState?: NativeDefaultState; saving: boolean; onSave: (patch: DelegationPatch) => void; - prompt: string; - childInstructions: string; - childInstructionsSaving: boolean; - onChildInstructionsSave: (value: string | null) => void; ultraMode: UltraModeState; ultraSaving: boolean; onUltraModeSave: (patch: UltraModePatch) => void; ultraLoadFailed: boolean; onUltraModeRetry: () => void; - keepNativeChatGptOnV1?: boolean; - nativeParentOverride?: V2NativeParentOverrideState; - nativeParentOverrideSaving?: boolean; - onNativeParentOverrideSave?: (state: V2NativeParentOverrideState) => void; - agentTaskRecovery?: AgentTaskRecoveryState; - agentTaskRecoverySaving?: boolean; - onAgentTaskRecoverySave?: (state: AgentTaskRecoveryState) => void; - routedDelegationBridge?: V2RoutedDelegationBridgeState; - routedDelegationBridgeSaving?: boolean; - onRoutedDelegationBridgeSave?: (enabled: boolean) => void; + fallback: string[]; + fallbackPollMs: number; + fallbackBusy: boolean; + availableModels: string[]; + onFallbackChange: (models: string[]) => void; + onFallbackPollMsChange: (pollMs: number) => void; + onFallbackSave: () => void; } export default function SubagentDelegationSection({ @@ -52,44 +44,74 @@ export default function SubagentDelegationSection({ available, guidanceEnabled, syncCodexDefaults, - nativeDefaultState = "disabled", saving, onSave, - prompt, - childInstructions, - childInstructionsSaving, - onChildInstructionsSave, ultraMode, ultraSaving, onUltraModeSave, ultraLoadFailed, onUltraModeRetry, - keepNativeChatGptOnV1 = false, - nativeParentOverride = { enabled: false, model: null, active: false }, - nativeParentOverrideSaving = false, - onNativeParentOverrideSave = () => {}, - agentTaskRecovery = { enabled: false, model: null }, - agentTaskRecoverySaving = false, - onAgentTaskRecoverySave = () => {}, - routedDelegationBridge = { enabled: false }, - routedDelegationBridgeSaving = false, - onRoutedDelegationBridgeSave = () => {}, + fallback, fallbackPollMs, fallbackBusy, availableModels, onFallbackChange, onFallbackPollMsChange, onFallbackSave, }: SubagentDelegationSectionProps) { const t = useT(); // A present empty/whitespace hint is an upstream override that suppresses the // Proactive message, so it must render as OFF (and the toggle can install the // preset). Only a nonblank hint is "on". const ultraOn = (ultraMode.hintText ?? "").trim().length > 0; - const safeNativeDefaultState = nativeDefaultState === "active" - || nativeDefaultState === "pending" - || nativeDefaultState === "blocked" - ? nativeDefaultState - : "disabled"; - const nativeParentTargets = available.filter(option => option.canonical !== true); - const nativeRecoveryTargets = available.filter(option => ( - option.provider === "openai" && option.namespaced === option.model - )); - const nativeParentCanActivate = ultraMode.multiAgentV2Enabled && !keepNativeChatGptOnV1 && nativeParentOverride.model !== null; + const routedPreferred = available.some(option => option.namespaced === model + && !(option.provider === "openai" && option.namespaced === option.model)); + const nativeMayUseV2 = ultraMode.enabled || (ultraMode.multiAgentMode !== "v1" + && !(ultraMode.multiAgentMode === "v2" && ultraMode.keepNativeChatGptOnV1)); + const showV2Compatibility = !ultraLoadFailed && ultraMode.loaded === true && routedPreferred && nativeMayUseV2; + const availableModelSet = new Set(availableModels); + const fallbackSet = new Set(fallback); + const [pollDraft, setPollDraft] = useState(() => ({ pollMs: fallbackPollMs, text: String(fallbackPollMs) })); + // Keep blank/invalid input text while reconciling accepted settings from a load or save. + if (!Object.is(pollDraft.pollMs, fallbackPollMs)) { + setPollDraft({ pollMs: fallbackPollMs, text: Number.isFinite(fallbackPollMs) ? String(fallbackPollMs) : "" }); + } + const fallbackControlsRef = useRef(null); + const [identity, setIdentity] = useState(() => ({ + models: fallback, + rows: fallback.map((rowModel, id) => ({ model: rowModel, id })), + nextId: fallback.length, + })); + let rows = identity.rows; + // Keys are render state. Guarded prop reconciliation retains each occurrence; + // event handlers move the same identities with their corresponding models. + if (identity.models !== fallback) { + const remaining = [...identity.rows]; + let nextId = identity.nextId; + rows = fallback.map(modelName => { + const old = remaining.findIndex(row => row.model === modelName); + return old >= 0 ? remaining.splice(old, 1)[0] : { model: modelName, id: nextId++ }; + }); + setIdentity({ models: fallback, rows, nextId }); + } + const pendingFocus = useRef<{ row: number; action: string } | null>(null); + useLayoutEffect(() => { + const target = pendingFocus.current; + if (!target) return; + pendingFocus.current = null; + const row = fallbackControlsRef.current?.querySelectorAll(".swi-fallback-row")[target.row]; + const enabledActions = row?.querySelectorAll("button[data-action]:not(:disabled)"); + const action = Array.from(enabledActions ?? []).find(button => button.dataset.action === target.action) + ?? row?.querySelector("button:not(:disabled)") + ?? fallbackControlsRef.current?.querySelector('button[role="combobox"]'); + action?.focus(); + }, [fallback]); + const validPollMs = Number.isInteger(fallbackPollMs) && fallbackPollMs >= 5000 && fallbackPollMs <= 600000; + const moveFallback = (index: number, direction: -1 | 1) => { + const next = [...fallback]; + const target = index + direction; + if (fallbackBusy || target < 0 || target >= next.length) return; + [next[index], next[target]] = [next[target], next[index]]; + const nextRows = [...rows]; + [nextRows[index], nextRows[target]] = [nextRows[target], nextRows[index]]; + setIdentity({ ...identity, models: next, rows: nextRows }); + pendingFocus.current = { row: target, action: direction === -1 ? "up" : "down" }; + onFallbackChange(next); + }; return (
@@ -104,22 +126,6 @@ export default function SubagentDelegationSection({
)} -
-
-
{t("sub.routedDelegationBridge")}
-
{t("sub.routedDelegationBridgeHint")}
-
- onRoutedDelegationBridgeSave(!routedDelegationBridge.enabled)} - disabled={routedDelegationBridgeSaving} - label={t("sub.routedDelegationBridge")} - /> - {!ultraMode.multiAgentV2Enabled && routedDelegationBridge.enabled && ( -
{t("sub.routedDelegationBridgeInactive")}
- )} -
-
{t("sub.delegation.model")}
@@ -153,11 +159,62 @@ export default function SubagentDelegationSection({
+ {showV2Compatibility && ( +
+
+
{t("sub.v2Compatibility.title")}
+

{t("sub.v2Compatibility.risk")}

+

{t("sub.v2Compatibility.recoveryUnknown")}

+ {t("sub.v2Compatibility.details")} +
+
+ )} + +
+
+
{t("sub.fallbackLabel")}
+
{t("sub.fallbackHint")}
+
+
+ {fallback.map((modelName, index) => ( +
+ {index + 1}. {modelName} + {!availableModelSet.has(modelName) && {t("sub.fallbackUnavailable")}} + + + + + + +
+ ))} + { + const text = e.currentTarget.value; + const parsed = Number(text); + const pollMs = text.trim() !== "" && Number.isFinite(parsed) ? parsed : Number.NaN; + setPollDraft({ pollMs, text }); + onFallbackPollMsChange(pollMs); + }} disabled={fallbackBusy} aria-invalid={!validPollMs} /> ms + + {!validPollMs &&
{t("sub.fallbackPollInvalid")}
} + +
+
+
{t("dash.syncCodexSubagentDefaults")}
{t("dash.syncCodexSubagentDefaultsHint")}
-
{t(`sub.nativeDefaultState.${safeNativeDefaultState}`)}
)} - -
-
-
{t("sub.nativeParentOverride")}
-
{t("sub.nativeParentOverrideHint")}
-
{t("sub.nativeParentOverridePrivacyWarning")}
-
-
- option.model === agentTaskRecovery.model) - ? [{ value: agentTaskRecovery.model, label: agentTaskRecovery.model }] - : []), - ...nativeRecoveryTargets.map(option => ({ - value: option.model, - label: formatNamespacedModelId(option.provider + "/" + option.model, t), - })), - ]} - onChange={v => onAgentTaskRecoverySave({ - enabled: agentTaskRecovery.enabled, - model: v || null, - })} - disabled={agentTaskRecoverySaving} - label={t("sub.agentTaskRecoveryModel")} - align="right" - /> - onAgentTaskRecoverySave({ - enabled: !agentTaskRecovery.enabled, - model: agentTaskRecovery.model, - })} - disabled={agentTaskRecoverySaving} - label={t("sub.agentTaskRecovery")} - /> -
-
- -
-
-
{t("sub.injectionPrompt")}
-
- {t("sub.injectionPromptHint")}{" "} - {"{{model}}"}{" "} - {"{{effort}}"}{" "} - {"{{roster}}"}{" "} - {"{{fallback}}"}{" "} - {"{{roles}}"} -
-
- onSave({ prompt: value.trim() ? value : null })} - /> -
- -
-
-
{t("sub.childInstructions")}
-
{t("sub.childInstructionsHint")}
-
- onChildInstructionsSave(value.trim() ? value : null)} - /> -
); @@ -452,40 +389,3 @@ function UltraModeEditor({ /** Canonical Proactive delegation text mirrored from codex-rs (multi_agent_mode_instructions.rs). */ export const ULTRA_MODE_PRESET = "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Delegate independent sub-tasks to sub-agents whenever parallel work would materially improve speed or quality — do not serialize work that can run concurrently. Each sub-agent runs in its own context and can use all available tools; prefer spawning specialists over doing everything yourself. This mode remains active until a later multi-agent mode developer message changes it."; - -function PromptDraftEditor({ - initialValue, - disabled, - ariaLabel, - saveLabel, - onSave, -}: { - initialValue: string; - disabled: boolean; - ariaLabel: string; - saveLabel: string; - onSave: (value: string) => void; -}) { - const [draft, setDraft] = useState(initialValue); - return ( -
-