diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55457c9dbf..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,17 +472,165 @@ jobs: - name: CLI help smoke run: bun run src/cli/index.ts help - # macOS is focused on dev pushes and relevant pull requests, while main, - # preview, and dispatch retain the full unsharded control. This keeps Darwin - # process coverage on the fast path without dropping the release control. platform-macos: - name: macos + name: macos ${{ matrix.shard }}/2 needs: changes - if: >- - (github.event_name != 'pull_request' && github.event_name != 'merge_group') || - (github.event_name == 'pull_request' && github.base_ref == 'main') || - (github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/main') || - needs.changes.outputs.macos == 'true' + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' + runs-on: macos-latest + # 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, 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 + uses: ./.github/actions/setup-project-bun + + - name: Install dependencies + run: | + bun install --frozen-lockfile + 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 + + # 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: | + # 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- - 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 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 - - - name: Full macOS suite - if: >- - 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')) - run: bash scripts/ci/run-bun-with-crash-retry.sh -- bun scripts/test.ts --timeout 60000 + # 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, @@ -645,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. @@ -671,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. @@ -709,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. @@ -738,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 @@ -812,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: @@ -878,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 @@ -903,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 @@ -920,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 @@ -943,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-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: @@ -952,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 . @@ -971,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/cleanup-closed-pr-branches.yml b/.github/workflows/cleanup-closed-pr-branches.yml index 0429e6deb1..4b0c1229a9 100644 --- a/.github/workflows/cleanup-closed-pr-branches.yml +++ b/.github/workflows/cleanup-closed-pr-branches.yml @@ -34,7 +34,7 @@ jobs: pull-requests: read steps: - name: Checkout trusted default-branch code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false sparse-checkout: .github/scripts diff --git a/.github/workflows/dev-version-bump.yml b/.github/workflows/dev-version-bump.yml index 21a7f5c79d..f02c0e89f4 100644 --- a/.github/workflows/dev-version-bump.yml +++ b/.github/workflows/dev-version-bump.yml @@ -1,52 +1,41 @@ -name: Dev version bump (dormant fallback) +name: Dev version bump -# Dormant upstream fallback for manually prepared recovery. The fork's live release -# path must not call this workflow: `promote-dev.yml` is the sole post-release writer -# and advances `dev` through an exact-SHA, tag-verified App update. Keeping two callers -# produced both a direct 2.40.1 bump and a conflicting 2.41.0 PR after v2.40.0. -# -# If explicitly restored as the sole authority in a future design, open a pull request -# that moves `dev` past the published -# version. Without this, `dev` keeps carrying a version that is at or behind a released -# one, and `tests/ci-workflows/release-version-line.test.ts` fails on `dev` and on every pull request -# opened against it - inherited red a contributor cannot fix from their own diff. +# Before a release publishes, open a pull request that moves `dev` past the intended +# version. Merge that pull request before promoting and publishing so `dev` and pull +# requests based on it never inherit a version-line failure from the new tag. # # That has been repaired by hand four times: 32529c2b2, e4a85d134, 076ad3036, befcac3e1. -# The second of those ADDED the detector and two more repairs followed it, so more -# visibility was never the missing piece; a prepared change was. +# The workflow now prepares the move before publication. Explicit repair mode retains +# the old catch-up capability if a release somehow publishes without the pre-move. # # WHAT THIS DOES NOT DO. It does not push to `dev`. It opens a pull request and a human # merges it, because ruleset `Protect dev` requires an approving review and code-owner -# sign-off that a bot cannot supply. Until that merge the red persists. This converts a -# forgotten chore into a queued, reviewable change - not into an automatic repair. -# -# WHY THIS IS CALLED, NOT TRIGGERED. It used to listen for `release: published`, and in -# that form it ran ZERO times across v2.37.0, v2.38.0 and v2.39.0 - every one of those -# bumps was still opened by hand (#3045, #3076, #3127). The workflow was not broken; the -# event never existed. `release.yml` creates the GitHub release with -# `GH_TOKEN: ${{ github.token }}`, and GitHub does not start workflow runs from events -# raised by the default `GITHUB_TOKEN`. A `release: published` listener therefore cannot -# observe a release this repository publishes itself, no matter which branch it sits on. -# -# The historical upstream fix kept the credential surface unchanged: no PAT or app -# token. It called this workflow from `release.yml`; the fork deliberately does not, -# because its verified promotion controller already owns that mutation. +# sign-off that a bot cannot supply. `release.yml` independently refuses publication +# until `dev` already outranks the intended version. # -# A `workflow_call` body resolves from the CALLER's ref. This file would have to exist on -# whichever release branch deliberately restored it as the sole authority before it -# could take effect. +# WHY THIS IS DISPATCHED. The intended version is known before publication, and this +# workflow's purpose is to queue the reviewed `dev` move first. It is not called by the +# release workflow after an irreversible publish, and it does not react to release events. # -# There is deliberately no `workflow_dispatch`: a branch-selected manual run executes -# THAT branch body with `contents: write`. Re-drive a missed run by running -# `bun scripts/bump-dev-version.ts package.json` locally and opening the pull -# request normally. +# A branch-selected dispatch executes that branch's workflow body with write permission. +# The in-job guard therefore rejects accidental non-default-ref dispatches. It is an early +# warning, not a security boundary: a writer could remove it on their branch. Protected +# release branches and the required review on `dev` remain the enforcement boundaries. on: - workflow_call: + workflow_dispatch: inputs: - released-version: - description: "The tag that just published, e.g. v2.39.0" + intended-version: + description: "Version about to be released (pre-move), or one already published (repair)" required: true type: string + mode: + description: "pre-move (default) or repair — repair allows an already-published version" + required: false + default: pre-move + type: choice + options: + - pre-move + - repair permissions: {} @@ -87,17 +76,59 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Refuse a dispatch from a non-default ref + run: | + test "$GITHUB_REF" = "refs/heads/${{ github.event.repository.default_branch }}" || { + echo "::error::this workflow may only be dispatched from the default branch" + exit 1 + } + + - name: Resolve the target version + id: target + env: + INTENDED: ${{ inputs.intended-version }} + MODE: ${{ inputs.mode }} + run: | + set -euo pipefail + target="${INTENDED:-}" + if [ -z "$target" ]; then + echo "::error::intended-version was not supplied" + exit 1 + fi + echo "version=${target}" >> "$GITHUB_OUTPUT" + if [ "${MODE:-pre-move}" = "repair" ]; then + echo "mode=repair" >> "$GITHUB_OUTPUT" + else + echo "mode=pre-move" >> "$GITHUB_OUTPUT" + fi + - name: Decide the version dev should carry id: decide env: - RELEASED_VERSION: ${{ inputs.released-version }} + RELEASED_VERSION: ${{ steps.target.outputs.version }} run: | set -euo pipefail bun scripts/bump-dev-version.ts "${RELEASED_VERSION}" package.json + - name: Prove the intended version is not already released + if: ${{ steps.target.outputs.mode == 'pre-move' }} + env: + INTENDED: ${{ steps.target.outputs.version }} + run: | + set -euo pipefail + git fetch --force --tags origin + if git rev-parse -q --verify "refs/tags/v${INTENDED#v}" >/dev/null; then + echo "::error::v${INTENDED#v} already exists; this is a catch-up, not a pre-move" + exit 1 + fi + if npm view "@bitkyc08/opencodex@${INTENDED#v}" version >/dev/null 2>&1; then + echo "::error::${INTENDED#v} is already on npm" + exit 1 + fi + - name: Prove the chosen version is unused if: ${{ steps.decide.outputs.changed == 'true' }} - # The script decides the candidate from the released version SHAPE, which is all + # The script decides the candidate from the target version SHAPE, which is all # a pure function can see. Whether that candidate is actually FREE is a property # of the tag set, so it is settled here by the detector that already owns the # question. If this fails, no pull request is opened and the job goes red asking @@ -108,21 +139,34 @@ jobs: if: ${{ steps.decide.outputs.changed == 'true' }} env: GH_TOKEN: ${{ github.token }} + MODE: ${{ steps.target.outputs.mode }} NEXT_VERSION: ${{ steps.decide.outputs.version }} - RELEASED_VERSION: ${{ inputs.released-version }} + TARGET_VERSION: ${{ steps.target.outputs.version }} run: | set -euo pipefail branch="codex/dev-version-${NEXT_VERSION}" + if [ "${MODE}" = "repair" ]; then + subject="fix(release): move dev to ${NEXT_VERSION} after ${TARGET_VERSION}" + reason="\`${TARGET_VERSION}\` has published, so \`dev\` is carrying a version at or behind a released one and \`tests/ci-workflows/release-version-line.test.ts\` fails on \`dev\` and on every pull request opened against it. This is the post-publish repair." + freeness="\`bun test tests/ci-workflows/release-version-line.test.ts\` proved the chosen development version is unused." + else + subject="chore(release): open dev at ${NEXT_VERSION} before releasing ${TARGET_VERSION}" + reason="\`${TARGET_VERSION}\` is about to be released. Merging this first means \`dev\` already outranks the new tag when it lands, so neither \`dev\` nor any open pull request ever inherits the version-line failure. \`release.yml\` refuses to publish until this has merged." + freeness="The workflow proved \`${TARGET_VERSION}\` has neither a Git tag nor an npm publication, and \`bun test tests/ci-workflows/release-version-line.test.ts\` proved the chosen development version is unused." + fi - # Idempotent: a second publish, a re-run, or a manual repair must not turn a - # successful release into a red job. + # Idempotent: a repeated dispatch, a re-run, or a manual repair must not turn + # an already-queued version move into a red job. # # Check the PULL REQUEST as well as the branch, not just the branch. A security # review caught that: an open bump pull request whose head branch was deleted # leaves the branch check passing, so the job would recreate the branch and then - # fail on `gh pr create` with "already exists" — turning a successful release red - # for a repair that was already queued. + # fail on `gh pr create` with "already exists" — turning a successful run red + # for a move that was already queued. + # Apply the repository owner and branch filter on the server. Filtering a + # paginated `gh pr list` result locally can miss this repository's pull request + # when newer same-named fork pull requests fill the fetched page (#3325). open_prs="$( gh api --method GET "repos/${GITHUB_REPOSITORY}/pulls" \ -f state=open \ @@ -137,15 +181,12 @@ jobs: fi # An existing branch is NOT terminal. If a previous run pushed the branch and then - # failed at `gh pr create`, exiting here would leave the repair permanently unqueued. - # Validate that the name still belongs to this automation, then rebuild it from the - # CURRENT dev head. Reusing an old commit would open an immediately stale PR and could - # reintroduce files that moved on dev while the failed run was waiting for a retry. - existing_branch_sha="" + # failed at `gh pr create`, exiting here would leave the move permanently unqueued + # while every rerun reports success - the exact failure mode a reviewer caught. So + # reuse the branch and fall through to pull-request creation instead. if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then echo "::notice::${branch} exists without an open pull request; validating it" git fetch origin "${branch}" - existing_branch_sha="$(git rev-parse "origin/${branch}")" # Fail closed on unexpected content. The branch carries the bot's own one-line # bump, so anything else on it means a human or another job is using that name and @@ -160,41 +201,34 @@ jobs: echo "::error::${branch} carries ${branch_version}, expected ${NEXT_VERSION}" exit 1 fi - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -B "${branch}" origin/dev - git add package.json - git commit -m "fix(release): move dev to ${NEXT_VERSION} after ${RELEASED_VERSION}" - if [ -n "${existing_branch_sha}" ]; then - # Exact lease: fail if anything moved the validated branch after our fetch. - git push --force-with-lease="refs/heads/${branch}:${existing_branch_sha}" origin "${branch}" + git checkout -B "${branch}" "origin/${branch}" else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "${branch}" + git add package.json + git commit -m "${subject}" git push origin "${branch}" fi gh pr create \ --base dev \ --head "${branch}" \ - --title "fix(release): move dev to ${NEXT_VERSION} after ${RELEASED_VERSION}" \ + --title "${subject}" \ --body "$(cat < !issue.pull_request && String(issue.body || "").includes(marker)); - const runUrl = process.env.RUN_URL; - - if (process.env.BUMP_RESULT !== "failure") { - if (!existing) return; - await github.rest.issues.createComment({ - owner, repo, issue_number: existing.number, - body: `The dev-version bump automation recovered successfully: ${runUrl}`, - }); - await github.rest.issues.update({ - owner, repo, issue_number: existing.number, state: "closed", state_reason: "completed", - }); - return; - } - - if (existing) { - await github.rest.issues.createComment({ - owner, repo, issue_number: existing.number, - body: `The dev-version bump automation failed again: ${runUrl}`, - }); - return; - } - - const labels = { - "agent:jules": ["8250df", "Trusted Jules implementation request"], - "agent:generated": ["0969da", "Trusted generated maintenance issue"], - "agent:queued": ["d4c5f9", "Maintenance task queued"], - }; - const known = new Set((await github.paginate( - github.rest.issues.listLabelsForRepo, { owner, repo, per_page: 100 } - )).map(label => label.name)); - for (const [name, [color, description]] of Object.entries(labels)) { - if (!known.has(name)) { - await github.rest.issues.createLabel({ owner, repo, name, color, description }); - } - } - - await github.rest.issues.create({ - owner, repo, - title: "[agent:release] Dev version bump automation failed", - labels: Object.keys(labels), - body: [ - marker, - "### Area", "", "Installation or packaging", "", - "### What are you trying to accomplish?", "", - "Keep the dev package version strictly ahead of every published release so inherited CI stays green.", "", - "### What prevents this today?", "", - `The trusted dev-version bump workflow failed. Exact run: ${runUrl}`, "", - "### What should OpenCodex do?", "", - "Diagnose the exact failed run, prepare the smallest safe repair, and preserve the human-reviewed pull-request boundary for changes to dev.", "", - "### Example usage or interface", "", - `Inspect ${runUrl}, reproduce the failed gate locally, and open a template-complete PR against dev.`, "", - "### Alternatives or workarounds", "", - "A maintainer can run scripts/bump-dev-version.ts locally and open the bump PR manually while the automation is repaired.", "", - "### Additional context", "", - "Generated by the trusted release-version supervisor. Workflow and release changes remain protected and require explicit security review.", "", - "### Checks", "", - "- [x] I searched existing issues and documentation.", - "- [x] This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.", - "- [x] I removed secrets and personal data.", - ].join("\n"), - }); diff --git a/.github/workflows/enforce-issue-quality.yml b/.github/workflows/enforce-issue-quality.yml index bf1e2cb9d2..a983da5d78 100644 --- a/.github/workflows/enforce-issue-quality.yml +++ b/.github/workflows/enforce-issue-quality.yml @@ -53,7 +53,7 @@ jobs: copilot-requests: write steps: - name: Checkout trusted workflow code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false @@ -458,7 +458,7 @@ jobs: copilot-requests: write steps: - name: Checkout trusted workflow code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false @@ -764,7 +764,7 @@ jobs: steps: - name: Checkout trusted workflow code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Always load validator scripts from the repository default branch so # a branch-selected workflow_dispatch cannot execute untrusted code @@ -1196,7 +1196,7 @@ jobs: issues: write steps: - name: Checkout trusted workflow code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false diff --git a/.github/workflows/enforce-pr-target.yml b/.github/workflows/enforce-pr-target.yml index f449b55547..6743730206 100644 --- a/.github/workflows/enforce-pr-target.yml +++ b/.github/workflows/enforce-pr-target.yml @@ -15,17 +15,6 @@ on: # branch, so a PR cannot suppress or rewrite this signal path. The status is # only a wake-up signal; the gate re-reads live reviews before any write. status: - check_run: - types: [completed] - workflow_run: - workflows: ["Cross-platform CI"] - types: [completed] - workflow_dispatch: - inputs: - pull_number: - description: Pull request to reconcile against live GitHub state - required: true - type: number # pull-requests:write covers title/comment/label updates. # contents:write is required for convertPullRequestToDraft / @@ -33,7 +22,6 @@ on: # (otherwise: "Resource not accessible by integration"). This workflow # never checks out PR head code. permissions: - checks: read contents: write pull-requests: write @@ -48,15 +36,6 @@ jobs: github.event.state == 'success' && github.event.sender.login == 'coderabbitai[bot]' && github.event.sender.id == 136622811) || - github.event_name == 'check_run' || - (github.event_name == 'workflow_run' && - github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && - github.event.workflow_run.name == 'Cross-platform CI' && - github.event.workflow_run.event == 'pull_request' && - github.event.workflow_run.status == 'completed' && - github.event.workflow_run.repository.full_name == github.repository) || - (github.event_name == 'workflow_dispatch' && - github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) || (github.event_name == 'pull_request_target' && ((github.event.action != 'labeled' && github.event.action != 'unlabeled') || github.event.label.name == 'gui-screenshot-waived' || @@ -65,11 +44,9 @@ jobs: github.event.label.name == 'test-exception-approved' || github.event.label.name == 'suppression-approved' || github.event.label.name == 'generated-change-approved' || - github.event.label.name == 'dependency-change-approved' || - github.event.label.name == 'review-bot-waived')) + github.event.label.name == 'dependency-change-approved')) runs-on: ubuntu-latest permissions: - checks: read contents: read pull-requests: read outputs: @@ -78,105 +55,11 @@ jobs: - name: Resolve trusted gate event to PR id: resolve uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - env: - CURSOR_BUGBOT_APP_ID: ${{ vars.CURSOR_BUGBOT_APP_ID }} with: script: | const { owner, repo } = context.repo; let pullNumber = context.payload.pull_request?.number ?? null; - if (context.eventName === "workflow_dispatch") { - const defaultBranch = context.payload.repository?.default_branch; - if (!defaultBranch || context.ref !== `refs/heads/${defaultBranch}`) { - core.info("Manual reconciliation is only permitted from the default branch; skipping."); - return; - } - const requested = Number(context.payload.inputs?.pull_number); - if (!Number.isSafeInteger(requested) || requested <= 0) { - core.info("Manual reconciliation requires a positive pull request number; skipping."); - return; - } - const live = (await github.rest.pulls.get({ - owner, repo, pull_number: requested - })).data; - if (live.number !== requested || live.state !== "open" || live.merged || live.merged_at) { - core.info(`Pull request #${requested} is not an open, unmerged PR; skipping.`); - return; - } - pullNumber = live.number; - } - - if (context.eventName === "check_run") { - const check = context.payload.check_run; - const expectedAppId = Number(process.env.CURSOR_BUGBOT_APP_ID); - const trustedBugbot = - check?.name === "Cursor Bugbot" && - Number.isSafeInteger(expectedAppId) && - check.app?.id === expectedAppId; - const trustedBaseline = - ["ci", "hygiene"].includes(check?.name) && - check.app?.id === 15368; - if (!trustedBugbot && !trustedBaseline) { - core.info("Check producer is not a trusted sync readiness check; skipping."); - return; - } - const openPrs = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - per_page: 100 - }); - const candidates = openPrs.filter( - candidate => candidate.head?.sha === check.head_sha - ); - if (candidates.length !== 1) { - core.info( - `Cursor Bugbot check ${check.head_sha} maps to ${candidates.length} open current-head PRs; skipping ambiguous/stale revalidation.` - ); - return; - } - pullNumber = candidates[0].number; - } - - if (context.eventName === "workflow_run") { - const workflowRun = context.payload.workflow_run; - const expectedRepository = `${owner}/${repo}`; - if ( - workflowRun?.name !== "Cross-platform CI" || - workflowRun?.event !== "pull_request" || - workflowRun?.status !== "completed" || - workflowRun?.repository?.full_name !== expectedRepository - ) { - core.info("Workflow completion is not a trusted same-repository Cross-platform CI pull_request run; skipping."); - return; - } - - const headSha = workflowRun.head_sha; - if (typeof headSha !== "string" || headSha.length === 0) { - core.info("Cross-platform CI workflow completion has no head SHA; skipping."); - return; - } - const openPrs = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: "open", - per_page: 100 - }); - const candidates = openPrs.filter( - candidate => - candidate.state === "open" && - !candidate.merged && - candidate.head?.sha === headSha - ); - if (candidates.length !== 1) { - core.info( - `Cross-platform CI workflow ${headSha} maps to ${candidates.length} open current-head PRs; skipping ambiguous/stale revalidation.` - ); - return; - } - pullNumber = candidates[0].number; - } - if (context.eventName === "status") { const sender = context.payload.sender; const trustedCodeRabbit = @@ -259,7 +142,6 @@ jobs: runs-on: ubuntu-latest # Job-scoped permissions replace, rather than extend, the workflow default. permissions: - checks: read contents: write pull-requests: write concurrency: @@ -270,7 +152,7 @@ jobs: steps: - name: Checkout trusted PR-quality scripts - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Source trusted scripts from an integration branch, never from the # PR's own base commit. A stacked child PR's base is another open @@ -283,7 +165,7 @@ jobs: # `main`-targeting PR must take its scripts from `main`, or the gate # runs a `main` workflow definition against `dev` scripts. Every # other base, including a stacked child's, resolves to `dev`. - ref: ${{ github.event_name != 'pull_request_target' && github.event.repository.default_branch || (github.event.pull_request.base.ref == 'main' && 'main' || 'dev') }} + ref: ${{ github.event_name == 'status' && github.event.repository.default_branch || (github.event.pull_request.base.ref == 'main' && 'main' || 'dev') }} persist-credentials: false sparse-checkout: | .github/scripts @@ -292,8 +174,6 @@ jobs: - name: Enforce PR target, ancestry, and description uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: - CURSOR_BUGBOT_APP_ID: ${{ vars.CURSOR_BUGBOT_APP_ID }} - CURSOR_BUGBOT_POLICY: ${{ vars.CURSOR_BUGBOT_POLICY }} RESOLVED_PULL_NUMBER: ${{ needs.resolve-pr.outputs.pull-number }} with: script: | @@ -306,11 +186,10 @@ jobs: isChangedFileListTruncated, extractReviewReadiness, appendReviewReadinessSection, - stripReviewReadinessSection, + stripReviewReadinessSection, uncheckReviewReadinessBoxes, REVIEW_READINESS_CLAIM_INDEX, - resetReviewReadinessSection, - isPromotionPr + resetReviewReadinessSection } = require( path.join(process.cwd(), ".github", "scripts", "pr-quality.cjs"), ); @@ -374,21 +253,8 @@ jobs: "pr-maintainers.cjs" ), ); - const { - exactHeadBugbotEvidence, - generatedSyncBaselineDisposition, - hasExactHeadMaintainerWaiver - } = require( - path.join( - process.cwd(), - ".github", - "scripts", - "agent-maintenance.cjs" - ), - ); - const ALLOWED_BASES = - context.repo.owner === "lidge-jun" ? ["dev"] : ["dev", "main"]; + const ALLOWED_BASES = ["dev"]; const DEFAULT_BASE = "dev"; const TITLE_PREFIX = "[WRONG BRANCH] "; const LEGACY_COMMENT_MARKER = ""; @@ -413,7 +279,7 @@ jobs: // Defense in depth: the resolver job is the primary event gate, but // the write-capable script also rejects event classes this workflow // never intends to mutate from. - if (!["pull_request_target", "status", "check_run", "workflow_run", "workflow_dispatch"].includes(context.eventName)) { + if (!["pull_request_target", "status"].includes(context.eventName)) { core.info(`Unsupported gate event ${context.eventName}; skipping.`); return; } @@ -826,74 +692,6 @@ jobs: }), ]; - const bugbotPolicy = process.env.CURSOR_BUGBOT_POLICY || "shadow"; - if (!["shadow", "required"].includes(bugbotPolicy)) { - throw new Error(`Invalid CURSOR_BUGBOT_POLICY: ${bugbotPolicy}`); - } - const bugbotAppId = Number(process.env.CURSOR_BUGBOT_APP_ID); - let bugbotEvidence = null; - let bugbotWaived = false; - const promotionPr = isPromotionPr(pr.base.ref, pr.title); - const syncGenerated = - pr.base.ref === "dev" && - /^sync\/upstream-[A-Za-z0-9._-]+-[0-9a-f]{7,64}$/i.test(pr.head.ref ?? ""); - let syncBaselineReady = !syncGenerated; - let syncBaselineFailed = false; - if (bugbotPolicy === "required" || bugbotPolicy === "shadow") { - try { - const checkRuns = await github.paginate( - github.rest.checks.listForRef, - { owner, repo, ref: pr.head.sha, per_page: 100 } - ); - bugbotEvidence = exactHeadBugbotEvidence({ - checkRuns, - liveHeadSha: pr.head.sha, - expectedAppId: bugbotAppId - }); - if (syncGenerated) { - // This job is the enforce-target baseline check. Requiring - // its previous result here would deadlock a draft after the - // first run fails only because CI/hygiene were incomplete; - // this run becomes the successful enforce-target result. - const syncBaselineStatus = generatedSyncBaselineDisposition({ - syncGenerated, - checkRuns, - headSha: pr.head.sha, - expectedAppId: 15368, - }); - syncBaselineReady = syncBaselineStatus === "success"; - syncBaselineFailed = syncBaselineStatus === "failed"; - } - } catch (error) { - core.warning(`Could not verify Cursor Bugbot: ${error.message}`); - syncBaselineFailed = syncGenerated; - } - if (!bugbotEvidence && labelNames.includes("review-bot-waived")) { - try { - const waiverReviews = await github.paginate( - github.rest.pulls.listReviews, - { owner, repo, pull_number, per_page: 100 } - ); - bugbotWaived = hasExactHeadMaintainerWaiver({ - labels: labelNames, - reviews: waiverReviews, - maintainers: readMaintainerLogins(), - headSha: pr.head.sha - }); - } catch (error) { - core.warning(`Could not verify Cursor Bugbot waiver: ${error.message}`); - } - } - if (!bugbotEvidence && !bugbotWaived && bugbotPolicy === "required") { - failures.push({ code: "bugbot_review" }); - } else if (!bugbotEvidence && !bugbotWaived) { - core.info("Cursor Bugbot exact-head evidence is absent (shadow mode)."); - } - } - if (syncGenerated && syncBaselineFailed) { - failures.push({ code: "sync_baseline" }); - } - // A maintainer issue comment saying the change does not touch // the GUI waives the screenshot gate. The flag is what tells the // author the screenshot is not required, even though the failure @@ -967,7 +765,7 @@ jobs: // lookup fails closed — the PR is treated as a contributor PR. const authorIsMaintainer = !permissionLookupFailed && authorHasPushPermission(authorPermission); - const checklistRequired = !authorIsMaintainer && !syncGenerated && !promotionPr; + const checklistRequired = !authorIsMaintainer; // A confirmed maintainer does not need the bot's checklist: retire // the injected section from the body so it stops rendering as a @@ -1240,14 +1038,10 @@ jobs: } } - const syncReady = syncGenerated && syncBaselineReady; - // A contributor PR stays a draft while the checklist is open, even // when every quality gate already passes. const mustDraft = - failures.length > 0 || - (checklistRequired && !checklistComplete) || - (syncGenerated && !syncReady); + failures.length > 0 || (checklistRequired && !checklistComplete); // Which reset notice (head drift vs claim check) accompanies the // draft path; only one can be active because the claim check is @@ -1280,16 +1074,6 @@ jobs: "Add a screenshot of the UI change to the PR description." ); } - if (failures.some(failure => failure.code === "bugbot_review")) { - actions.push( - "Wait for a successful Cursor Bugbot check on the current head, or obtain the exact-head outage waiver." - ); - } - if (failures.some(failure => failure.code === "sync_baseline")) { - actions.push( - "Wait for successful exact-head `ci`, `enforce-target`, and `hygiene` checks." - ); - } for (const failure of failures) { const hint = HYGIENE_FAILURE_HINTS[failure.code]; if (!hint) continue; @@ -1315,10 +1099,7 @@ jobs: // to labeled PRs (maintainer PRs never carry this label), so the // label is kept as a visible status marker only. const readyMoment = - failures.length === 0 && - ((checklistRequired && checklistComplete) || - (syncGenerated && syncReady) || - promotionPr); + checklistRequired && checklistComplete && failures.length === 0; const reviewReadyDesired = readyMoment; const hasReviewReadyLabel = (pr.labels ?? []).some( label => label.name === REVIEW_READY_LABEL @@ -1566,9 +1347,7 @@ jobs: let readyConverted = false; const shouldMarkReady = (gateState.active && gateState.autoDraftedByBot) || - (checklistRequired && checklistComplete) || - (syncGenerated && syncReady) || - promotionPr; + (checklistRequired && checklistComplete); if (shouldMarkReady && pr.draft) { try { await markReadyForReview(); @@ -1585,11 +1364,7 @@ jobs: ...gateState, active: readyConversionFailed ? true : false }; - if ( - (checklistRequired && checklistComplete) || - (syncGenerated && syncReady) || - promotionPr - ) { + if (checklistRequired && checklistComplete) { const maintainers = readMaintainerLogins().filter( login => login !== pr.user.login ); @@ -1624,11 +1399,7 @@ jobs: await upsertGateComment(readyState, { status: "READY", - statusReason: promotionPr - ? "all promotion PR quality gates passed; ready for human review and merge." - : (syncGenerated - ? "all exact-head sync PR quality gates passed; ready for human merge." - : "all PR quality gates passed; the review readiness checklist is complete."), + statusReason: "all PR quality gates passed; the review readiness checklist is complete.", actions: [], readiness, checklistRequired, diff --git a/.github/workflows/issue-quality-tests.yml b/.github/workflows/issue-quality-tests.yml index ae4a6e2819..0b6529f667 100644 --- a/.github/workflows/issue-quality-tests.yml +++ b/.github/workflows/issue-quality-tests.yml @@ -28,14 +28,6 @@ on: - ".github/scripts/run-copilot-inference*.cjs" - ".github/scripts/parse-issue-translation-response.cjs" - ".github/scripts/parse-issue-translation-response.test.cjs" - - ".github/scripts/pr-automation*.cjs" - - ".github/scripts/agent-maintenance*.cjs" - - ".github/scripts/automation-health*.cjs" - - ".github/scripts/closed-pr-branch-cleanup.cjs" - - ".github/scripts/closed-pr-branch-cleanup.test.cjs" - - ".github/workflows/pr-automation.yml" - - ".github/workflows/agent-maintenance.yml" - - ".github/workflows/automation-health.yml" - ".github/workflows/enforce-issue-quality.yml" - ".github/workflows/enforce-pr-target.yml" - ".github/workflows/pr-labeler.yml" @@ -69,14 +61,6 @@ on: - ".github/scripts/run-copilot-inference*.cjs" - ".github/scripts/parse-issue-translation-response.cjs" - ".github/scripts/parse-issue-translation-response.test.cjs" - - ".github/scripts/pr-automation*.cjs" - - ".github/scripts/agent-maintenance*.cjs" - - ".github/scripts/automation-health*.cjs" - - ".github/scripts/closed-pr-branch-cleanup.cjs" - - ".github/scripts/closed-pr-branch-cleanup.test.cjs" - - ".github/workflows/pr-automation.yml" - - ".github/workflows/agent-maintenance.yml" - - ".github/workflows/automation-health.yml" - ".github/workflows/enforce-issue-quality.yml" - ".github/workflows/enforce-pr-target.yml" - ".github/workflows/pr-labeler.yml" @@ -93,7 +77,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -112,10 +96,6 @@ jobs: node --test .github/scripts/copilot-workflows.test.cjs node --test .github/scripts/run-copilot-inference.test.cjs node --test .github/scripts/parse-issue-translation-response.test.cjs - node --test .github/scripts/pr-automation*.test.cjs - node --test .github/scripts/agent-maintenance*.test.cjs - node --test .github/scripts/automation-health.test.cjs - node --test .github/scripts/closed-pr-branch-cleanup.test.cjs - name: Validate issue-form YAML run: | diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 3bda0b7454..c3485054b7 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -20,7 +20,7 @@ jobs: matches: ${{ steps.parse.outputs.matches }} steps: - name: Checkout trusted triage scripts - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Issue events load this workflow from the default branch; keep scripts # aligned with that same trusted ref. @@ -169,7 +169,7 @@ jobs: issues: write steps: - name: Checkout trusted triage scripts - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false diff --git a/.github/workflows/pr-hygiene.yml b/.github/workflows/pr-hygiene.yml index cfb3fdbb9a..b60943b93a 100644 --- a/.github/workflows/pr-hygiene.yml +++ b/.github/workflows/pr-hygiene.yml @@ -34,7 +34,7 @@ jobs: pull-requests: write steps: - name: Checkout trusted hygiene script - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Source trusted scripts from an integration branch, never from the # PR's own base commit. A stacked child PR's base is another open diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 5b358f118a..309fd40b8f 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -32,7 +32,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout labeler script (default-branch trusted code) - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c9ec66505..685a13876b 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" @@ -444,61 +335,42 @@ jobs: - name: Publish (or dry-run) 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 fi # Confirm the registry actually has the new version (real publishes only). - name: Post-publish registry smoke - if: ${{ env.DISPATCH_DRY_RUN != 'true' }} + if: ${{ inputs.dry-run != true }} env: - RELEASE_VERSION: ${{ env.DISPATCH_VERSION }} + RELEASE_VERSION: ${{ inputs.version }} run: | - 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 + if VERSION=$(npm view "@bitkyc08/opencodex@${RELEASE_VERSION}" version 2>/dev/null); then echo "registry version=$VERSION" test "$VERSION" = "$RELEASE_VERSION" - npm dist-tag ls "$pkg_name" + npm dist-tag ls @bitkyc08/opencodex exit 0 fi - echo "::notice::${pkg_name}@${RELEASE_VERSION} not visible in npm registry yet (attempt $attempt/30)" + echo "::notice::@bitkyc08/opencodex@${RELEASE_VERSION} not visible in npm registry yet (attempt $attempt/30)" sleep 10 done echo "::error::npm registry smoke failed after 30 attempts" - npm view "$pkg_name" versions dist-tags --json || true + npm view @bitkyc08/opencodex versions dist-tags --json || true exit 1 - name: Create GitHub release - if: ${{ env.DISPATCH_DRY_RUN != 'true' }} + if: ${{ inputs.dry-run != 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 +390,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 - 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 + if [ -z "$existing_tag_sha" ]; then + git tag "$release_tag" "$GITHUB_SHA" + git push origin "refs/tags/${release_tag}" 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/.github/workflows/service-lifecycle.yml b/.github/workflows/service-lifecycle.yml index 26991ed0f3..df37f60561 100644 --- a/.github/workflows/service-lifecycle.yml +++ b/.github/workflows/service-lifecycle.yml @@ -19,10 +19,19 @@ on: # produced no run and the gate dead-ended until a manual dispatch. - ".github/workflows/release.yml" push: - # Release eligibility requires exact-head lifecycle evidence on every - # integration push, including commits whose diff does not touch service - # files. Do not add a push path filter here. - branches: [main, preview, dev] + paths: + - "src/service.ts" + # Keep in sync with the release.yml service-gate regex (see above). + - "src/cli.ts" + - "src/cli/index.ts" + - "src/lib/bun-runtime.ts" + - "package.json" + - "bun.lock" + - ".github/workflows/service-lifecycle.yml" + # release.yml gates on THIS workflow having run for the release SHA. A release-branch + # commit that touches only release.yml (e.g. the v2.40.0 permissions carry, #3263/#3264) + # produced no run and the gate dead-ended until a manual dispatch. + - ".github/workflows/release.yml" workflow_dispatch: permissions: 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/compose.yaml b/compose.yaml index cea1818568..b54795692a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,13 +9,14 @@ 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: - OCX_CONTAINER_PUBLIC_PORT: "${OPENCODEX_PORT:-10100}" - 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 +28,4 @@ services: volumes: ocx-state: + codex-state: 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_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_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/docker/bootstrap-token.ts b/docker/bootstrap-token.ts index 8160c6e8e7..2c647cd9dd 100644 --- a/docker/bootstrap-token.ts +++ b/docker/bootstrap-token.ts @@ -1,6 +1,6 @@ import { writeServiceApiTokenFile } from "../src/lib/service-secrets"; -const MAX_TOKEN_BYTES = 512; +const MAX_TOKEN_BYTES = 4096; const MAX_INPUT_BYTES = MAX_TOKEN_BYTES + 2; export async function readBoundedToken(stream: ReadableStream): Promise { @@ -14,7 +14,7 @@ export async function readBoundedToken(stream: ReadableStream): Prom const { done, value } = await reader.read(); if (done) break; bytes += value.byteLength; - if (bytes > MAX_INPUT_BYTES) throw new Error("token input exceeds 512 bytes"); + if (bytes > MAX_INPUT_BYTES) throw new Error("token input exceeds 4096 bytes"); raw += decoder.decode(value, { stream: true }); } raw += decoder.decode(); @@ -27,7 +27,7 @@ export async function readBoundedToken(stream: ReadableStream): Prom const token = line.trim(); if (!token) throw new Error("token input is empty"); - if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new Error("token input exceeds 512 bytes"); + if (Buffer.byteLength(token) > MAX_TOKEN_BYTES) throw new Error("token input exceeds 4096 bytes"); return token; } diff --git a/docker/verify-compatibility.ts b/docker/verify-compatibility.ts index 00427fd883..affd0af7f1 100644 --- a/docker/verify-compatibility.ts +++ b/docker/verify-compatibility.ts @@ -3,29 +3,8 @@ import { lstatSync, readFileSync, readdirSync } from "node:fs"; import { join, posix, resolve } from "node:path"; // Match the canonical generator without importing any not-yet-verified source. -const REQUIRED_COMPATIBILITY_FILES = [ - ".dockerignore", - "Dockerfile", - "bun.lock", - "compose.yaml", - "docker/bootstrap-tls.ts", - "docker/bootstrap-token.ts", - "docker/config.json", - "docker/healthcheck.ts", - "docker/verify-compatibility.ts", - "gui/bun.lock", - "gui/package.json", - "package.json", - "scripts/model-metadata.source.json", -]; +const REQUIRED_ROOT_FILES = ["package.json", "bun.lock", "scripts/model-metadata.source.json"]; const MANIFEST_PATH = "src/generated/compatibility-version.json"; -const BUILD_CONTEXT_ONLY_FILES = new Set([".dockerignore", "Dockerfile", "compose.yaml"]); - -function isBuildContextOnly(path: string): boolean { - // The build stage verifies every tracked GUI input before Vite derives gui/dist. - // The runtime stage intentionally contains only that derived output, not its sources. - return BUILD_CONTEXT_ONLY_FILES.has(path) || path.startsWith("gui/"); -} interface ManifestRow { path: string; @@ -52,9 +31,7 @@ function parseRows(raw: unknown): ManifestRow[] { const path = row.path; if (!path || /[\\\0]/.test(path) || posix.normalize(path) !== path || path.split("/").some(part => !part || part === "." || part === "..") - || (!path.startsWith("src/") && !path.startsWith("docker/") - && !path.startsWith("gui/") - && !REQUIRED_COMPATIBILITY_FILES.includes(path)) + || (!path.startsWith("src/") && !REQUIRED_ROOT_FILES.includes(path)) || path === MANIFEST_PATH) { throw new Error(`Invalid compatibility manifest path: ${JSON.stringify(path)}`); } @@ -80,55 +57,36 @@ function regularFile(root: string, path: string): string { return current; } -function treeFiles(root: string, path: string, label: string): string[] { +function sourceFiles(root: string, path = "src"): string[] { const stat = lstatSync(join(root, path)); - if (stat.isSymbolicLink()) throw new Error(`Symlink in ${label} tree: ${JSON.stringify(path)}`); + if (stat.isSymbolicLink()) throw new Error(`Symlink in source tree: ${JSON.stringify(path)}`); if (stat.isFile()) return [path]; - if (!stat.isDirectory()) throw new Error(`Non-regular ${label} entry: ${JSON.stringify(path)}`); - return readdirSync(join(root, path)).flatMap(name => treeFiles(root, `${path}/${name}`, label)); + if (!stat.isDirectory()) throw new Error(`Non-regular source entry: ${JSON.stringify(path)}`); + return readdirSync(join(root, path)).flatMap(name => sourceFiles(root, `${path}/${name}`)); } -/** Validate a Git-free build snapshot against the host-generated tracked-authority manifest. */ -export function verifyCompatibilitySnapshot( - snapshotRoot: string, - options: { runtime?: boolean } = {}, -): void { +/** Validate a Git-free build snapshot against the host-generated tracked-source manifest. */ +export function verifyCompatibilitySnapshot(snapshotRoot: string): void { const root = resolve(snapshotRoot); const stat = lstatSync(root); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Invalid compatibility snapshot root"); const manifestFile = regularFile(root, MANIFEST_PATH); const rows = parseRows(JSON.parse(readFileSync(manifestFile, "utf8"))); const expected = new Set(rows.map(row => row.path)); - for (const required of REQUIRED_COMPATIBILITY_FILES) { + for (const required of REQUIRED_ROOT_FILES) { if (!expected.has(required)) throw new Error(`Missing required manifest entry: ${required}`); } if (!rows.some(row => row.path.startsWith("src/"))) { throw new Error("Compatibility manifest contains no source files"); } - if (!rows.some(row => row.path.startsWith("gui/"))) { - throw new Error("Compatibility manifest contains no GUI build inputs"); - } // Inspect the full tree, including symlinks to files/directories not named in the manifest. - for (const path of treeFiles(root, "src", "source")) { + for (const path of sourceFiles(root)) { if (path !== MANIFEST_PATH && !expected.has(path)) { throw new Error(`Source file absent from compatibility manifest: ${JSON.stringify(path)}`); } } - for (const path of treeFiles(root, "docker", "container authority")) { - if (!expected.has(path)) { - throw new Error(`Container authority file absent from compatibility manifest: ${JSON.stringify(path)}`); - } - } - if (!options.runtime) { - for (const path of treeFiles(root, "gui", "GUI build input")) { - if (!expected.has(path)) { - throw new Error(`GUI build input absent from compatibility manifest: ${JSON.stringify(path)}`); - } - } - } for (const row of rows) { - if (options.runtime && isBuildContextOnly(row.path)) continue; const bytes = readFileSync(regularFile(root, row.path)); const actual = createHash("sha256").update(bytes).digest("hex"); if (actual !== row.sha256) { @@ -138,7 +96,5 @@ export function verifyCompatibilitySnapshot( } if (import.meta.main) { - const runtime = process.argv.includes("--runtime"); - const rootArg = process.argv.slice(2).find(arg => arg !== "--runtime"); - verifyCompatibilitySnapshot(rootArg ?? resolve(import.meta.dir, ".."), { runtime }); + verifyCompatibilitySnapshot(process.argv[2] ?? resolve(import.meta.dir, "..")); } 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/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..f9bd231855 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -526,12 +526,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 +544,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/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index c65531a4d3..c718801ddc 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, ZCode, Prime Agent, Aside 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/providers.md b/docs-site/src/content/docs/fr/guides/providers.md index 92aa565e52..92105e9823 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. 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/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/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..0b1c0e9db4 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. @@ -404,32 +405,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 +442,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 +522,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 +541,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 +557,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 +638,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/integrations.md b/docs-site/src/content/docs/guides/integrations.md index 0c95908206..ea3c93f2dd 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,42 @@ 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. +Raycast has two prerequisites. 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. And Raycast only creates its `ai` folder when you open Raycast → +Settings → AI → **Reveal Providers Config** once; opencodex uses that folder as +the install signal and reports the client as not installed until then. 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 +167,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 +253,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..2d33b409d8 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,11 @@ 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. diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 48ae3261d9..5e46979816 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -121,24 +121,12 @@ 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. | | `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 +222,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 +310,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 @@ -415,6 +367,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` | @@ -552,13 +505,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 +583,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 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/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 164c3c23d4..94e8d09e1c 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -392,12 +392,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 +410,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/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/providers.md b/docs-site/src/content/docs/ja/guides/providers.md index cd5223573f..a24f4b74db 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 従量課金エンドポイントです。ホストもキーも課金も別で、 > 一方で発行したキーはもう一方では認証されません。 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..049ad25bf6 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -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 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..d2be4b5d98 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -433,12 +433,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 +451,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/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/providers.md b/docs-site/src/content/docs/ko/guides/providers.md index 20eb9cf4b9..4005847368 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 종량제 엔드포인트입니다. 호스트도 키도 과금도 다르며, 한쪽에서 > 발급한 키는 다른 쪽에서 인증되지 않습니다. 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..f1fff8a3db 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -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 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..0eb7f4162e 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -296,7 +296,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 +307,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 +337,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 +350,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..88f21ea928 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 @@ -331,12 +337,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]` diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 9a773568eb..6aa8c78d00 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. | @@ -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,14 @@ 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/). + ### Discovered model display names Use `modelDisplayNames` when a provider returns machine friendly ids but the Codex model picker @@ -225,6 +230,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 +249,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,13 +380,6 @@ 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. - ## Provider diagnostic outbound safety Dashboard connection tests and live model discovery use a bounded GET-only transport. Without an @@ -429,17 +451,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 +490,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 +526,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 +547,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 +624,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 +788,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..8273af6d4b 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 | — | @@ -303,6 +311,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 +419,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..cb97ad7076 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,25 +238,40 @@ 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. @@ -443,7 +409,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/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index fdc7b6075a..0215cf87d7 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -420,12 +420,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 +439,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/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/providers.md b/docs-site/src/content/docs/ru/guides/providers.md index 8b43b8d4c2..ce44a79363 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 с оплатой по факту использования. Разные хосты, > разные ключи, разная тарификация: ключ от одного сервиса не подойдёт к другому. 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/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 347602a9d8..2116c5e36c 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -609,12 +609,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 +628,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/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/providers.md b/docs-site/src/content/docs/tr/guides/providers.md index e6a6dd5f1f..3ea48d2160 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ı 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/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/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index cc60e363ca..1e7e6fc126 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 @@ -352,12 +352,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 +371,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/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/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md index 290757bab7..6b96d5fe34 100644 --- a/docs-site/src/content/docs/zh-cn/guides/providers.md +++ b/docs-site/src/content/docs/zh-cn/guides/providers.md @@ -104,6 +104,9 @@ ocx logout | `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、带可选 HTTP/1.1 兼容路径的 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 @@ -202,6 +205,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` | @@ -315,7 +319,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 密钥 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..be2956558d 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 @@ -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 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/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/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md index be149d5901..0fd0953f66 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 金鑰 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/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/ModelDisplayNameDialog.tsx b/gui/src/components/ModelDisplayNameDialog.tsx new file mode 100644 index 0000000000..2a57ff8279 --- /dev/null +++ b/gui/src/components/ModelDisplayNameDialog.tsx @@ -0,0 +1,175 @@ +import { useEffect, useId, useRef, useState } from "react"; +import { useT, type TKey } from "../i18n/shared"; +import { + modelDisplayNameValidationKey, + type ModelRow, +} from "../pages/models-shared"; + +interface ModelDisplayNameDialogProps { + model: ModelRow; + saving: boolean; + requestError: string | null; + currentNamePending?: boolean; + onRetry?: () => 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, + onRetry, + onEdit, + onSave, + onReset, + onClose, +}: ModelDisplayNameDialogProps) { + const t = useT(); + const dialogRef = useRef(null); + const inputRef = 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) inputRef.current?.focus(); + }, [requestError, saving]); + + // 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)}} +
+ + + { + onEdit?.(); + setDraft(event.target.value); + setValidationKey(null); + }} + /> +

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

+ {visibleError && ( + + )} + +
+ + + +
+ +
+ ); +} 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/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/i18n/de.ts b/gui/src/i18n/de.ts index 032b50f919..4f933b7923 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -2740,4 +2740,47 @@ export const de: Record = { "logs.agent.internal": "Intern", "logs.agent.unknown": "Unbekannt", "logs.agent.badgeTitle": "Anfrageherkunft", + "models.pickerOrder.label": "Modellreihenfolge", + "models.pickerOrder.default": "Standard", + "models.pickerOrder.alphabetical": "A–Z nach Modell", + "models.pickerOrder.provider": "Nach Anbieter", + "models.pickerOrder.mostUsed": "Nutzungsschnappschuss", + "models.pickerOrder.custom": "Eigene Reihenfolge", + "models.pickerOrder.apply": "Reihenfolge anwenden", + "models.pickerOrder.applying": "Wird angewendet…", + "models.pickerOrder.saved": "Modellreihenfolge gespeichert. Clients mit altem Katalog erneut öffnen.", + "models.pickerOrder.pending": "Reihenfolge gespeichert; Katalogaktualisierung ausstehend.", + "models.pickerOrder.usageFailed": "Modellnutzung konnte nicht geladen werden.", + "models.pickerOrder.loadFailed": "Auswahleinstellungen konnten nicht geladen werden.", + "models.pickerOrder.retry": "Erneut versuchen", + "models.pickerOrder.hint": "Speichert geroutete Modelle für Codex- und Claude-Listen. Prioritätsbereiche bevorzugter/nativer Modelle bleiben erhalten. Nutzung ist eine Momentaufnahme; nativ angebotene Optionen können sich ändern.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Fügt einen OpenCodex-Provider-Eintrag in die providers.yaml von Raycast ein, damit jedes geroutete Modell in der Modellauswahl von Raycast AI erscheint. Raycast Pro erforderlich.", + "integrations.raycast.proRequired": "Custom Providers ist eine Funktion von Raycast Pro. Die Datei wird geschrieben, aber Raycast ignoriert sie, bis ein Pro-Abonnement aktiv ist.", + "integrations.raycast.planUnknown": "Es konnte nicht festgestellt werden, ob Raycast Pro aktiv ist; Custom Providers erfordert Raycast Pro.", + "integrations.raycast.revealConfig": "Öffnen Sie Raycast → Einstellungen → AI und klicken Sie einmal auf „Reveal Providers Config“, damit der Providers-Ordner existiert.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "Die Änderung wurde gespeichert, aber die Modellliste konnte nicht aktualisiert werden. Versuchen Sie es erneut.", + "models.displayNameOutcomeUnknown": "Die Anfrage wurde nicht abgeschlossen. Die Änderung wurde möglicherweise gespeichert. Prüfen Sie den aktuellen Namen durch erneutes Versuchen, bevor Sie ihn weiter ändern.", + "models.displayNameCurrentUnavailable": "Aktueller Name erst nach Aktualisierung verfügbar", + "models.displayNameReloaded": "Modellliste aktualisiert", + "models.displayNameAction": "Name", + "models.displayNameActionLabel": "Anzeigenamen für {model} bearbeiten", + "models.displayNameTitle": "Anzeigename", + "models.displayNameModelId": "Modell-ID", + "models.displayNameCurrent": "Aktueller Name", + "models.displayNameSourceOperator": "Ihr Name", + "models.displayNameSourceProvider": "Anbietername", + "models.displayNameSourceFallback": "Modell-ID als Ersatz", + "models.displayNameField": "Anzeigename", + "models.displayNamePlaceholder": "z. B. Grok 4.6", + "models.displayNameHelp": "Ändert nur die Anzeige. Das Routing bleibt {model}.", + "models.displayNameReset": "Name zurücksetzen", + "models.displayNameSaved": "Anzeigename gespeichert", + "models.displayNameResetDone": "Anzeigename zurückgesetzt", + "models.displayNameSaveFailed": "Anzeigename konnte nicht gespeichert werden", + "models.displayNameRequired": "Geben Sie einen Anzeigenamen ein oder verwenden Sie Name zurücksetzen.", + "models.displayNameTooLong": "Der Anzeigename darf höchstens 128 Zeichen lang sein.", + "models.displayNameNoSlash": "Der Anzeigename darf kein / enthalten.", + "models.displayNameNoControl": "Der Anzeigename darf keine Steuerzeichen enthalten.", }; diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index b4baf4901a..8c22b7e920 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -2774,6 +2774,49 @@ export const en = { "pws.aiStudio.connect": "Connect", "claudeDesktop.catalogChanged": "The model catalog changed while you were editing. The unavailable model was restored; review the profile and save again.", "claudeDesktop.mappingDetails": "Advanced model mappings", + "models.pickerOrder.label": "Picker order", + "models.pickerOrder.default": "Default", + "models.pickerOrder.alphabetical": "A–Z by model", + "models.pickerOrder.provider": "Group by provider", + "models.pickerOrder.mostUsed": "Most used snapshot", + "models.pickerOrder.custom": "Custom order", + "models.pickerOrder.apply": "Apply order", + "models.pickerOrder.applying": "Applying…", + "models.pickerOrder.saved": "Picker order saved. Reopen clients that still show the old catalog.", + "models.pickerOrder.pending": "Order saved; catalog refresh is pending.", + "models.pickerOrder.usageFailed": "Could not load model usage.", + "models.pickerOrder.loadFailed": "Could not load picker settings.", + "models.pickerOrder.retry": "Retry", + "models.pickerOrder.hint": "Saves routed order for Codex and Claude discovery. Featured/native bands stay in place; Most used is a snapshot. Native advertised choices may change.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Adds an OpenCodex provider entry to Raycast's providers.yaml so every routed model appears in the Raycast AI model picker. Raycast Pro required.", + "integrations.raycast.proRequired": "Custom Providers is a Raycast Pro feature. The file will be written, but Raycast ignores it until a Pro subscription is active.", + "integrations.raycast.planUnknown": "Could not determine whether Raycast Pro is active; Custom Providers requires Raycast Pro.", + "integrations.raycast.revealConfig": "Open Raycast → Settings → AI and click Reveal Providers Config once so the providers folder exists.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "The change was saved, but the model list could not be refreshed. Retry to refresh it.", + "models.displayNameOutcomeUnknown": "The request did not finish. The change may have been saved. Retry to check the current name before making another change.", + "models.displayNameCurrentUnavailable": "Current name unavailable until refresh", + "models.displayNameReloaded": "Model list refreshed", + "models.displayNameAction": "Name", + "models.displayNameActionLabel": "Edit friendly name for {model}", + "models.displayNameTitle": "Friendly name", + "models.displayNameModelId": "Model ID", + "models.displayNameCurrent": "Current name", + "models.displayNameSourceOperator": "Your name", + "models.displayNameSourceProvider": "Provider name", + "models.displayNameSourceFallback": "Model ID fallback", + "models.displayNameField": "Friendly name", + "models.displayNamePlaceholder": "e.g. Grok 4.6", + "models.displayNameHelp": "Changes presentation only. Routing remains {model}.", + "models.displayNameReset": "Reset name", + "models.displayNameSaved": "Display name saved", + "models.displayNameResetDone": "Display name reset", + "models.displayNameSaveFailed": "Failed to save display name", + "models.displayNameRequired": "Enter a friendly name, or use Reset name.", + "models.displayNameTooLong": "Friendly name must be 128 characters or fewer.", + "models.displayNameNoSlash": "Friendly name cannot contain /.", + "models.displayNameNoControl": "Friendly name cannot contain control characters.", } as const; export type TKey = keyof typeof en; diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 6f4163c821..69226d7e12 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -2727,4 +2727,47 @@ export const fr: Record = { "logs.agent.internal": "Interne", "logs.agent.unknown": "Inconnu", "logs.agent.badgeTitle": "Origine de la requête", + "models.pickerOrder.label": "Ordre des modèles", + "models.pickerOrder.default": "Par défaut", + "models.pickerOrder.alphabetical": "A–Z par modèle", + "models.pickerOrder.provider": "Par fournisseur", + "models.pickerOrder.mostUsed": "Instantané des usages", + "models.pickerOrder.custom": "Ordre personnalisé", + "models.pickerOrder.apply": "Appliquer l’ordre", + "models.pickerOrder.applying": "Application…", + "models.pickerOrder.saved": "Ordre enregistré. Rouvrez les clients affichant encore l’ancien catalogue.", + "models.pickerOrder.pending": "Ordre enregistré ; actualisation du catalogue en attente.", + "models.pickerOrder.usageFailed": "Impossible de charger les usages.", + "models.pickerOrder.loadFailed": "Impossible de charger les réglages du sélecteur.", + "models.pickerOrder.retry": "Réessayer", + "models.pickerOrder.hint": "Enregistre l’ordre des modèles routés pour Codex et la découverte Claude. Les plages prioritaires et natives sont conservées. Les usages sont un instantané ; les choix annoncés nativement peuvent changer.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Ajoute une entrée de fournisseur OpenCodex dans le providers.yaml de Raycast afin que chaque modèle routé apparaisse dans le sélecteur de modèles de Raycast AI. Raycast Pro requis.", + "integrations.raycast.proRequired": "Custom Providers est une fonctionnalité Raycast Pro. Le fichier sera écrit, mais Raycast l'ignore tant qu'un abonnement Pro n'est pas actif.", + "integrations.raycast.planUnknown": "Impossible de déterminer si Raycast Pro est actif ; Custom Providers nécessite Raycast Pro.", + "integrations.raycast.revealConfig": "Ouvrez Raycast → Réglages → AI et cliquez une fois sur « Reveal Providers Config » pour que le dossier des fournisseurs existe.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "La modification a été enregistrée, mais la liste des modèles n’a pas pu être actualisée. Réessayez.", + "models.displayNameOutcomeUnknown": "La requête n’a pas abouti. La modification a peut-être été enregistrée. Réessayez pour vérifier le nom actuel avant toute autre modification.", + "models.displayNameCurrentUnavailable": "Nom actuel indisponible avant actualisation", + "models.displayNameReloaded": "Liste des modèles actualisée", + "models.displayNameAction": "Nom", + "models.displayNameActionLabel": "Modifier le nom d’affichage de {model}", + "models.displayNameTitle": "Nom d’affichage", + "models.displayNameModelId": "ID du modèle", + "models.displayNameCurrent": "Nom actuel", + "models.displayNameSourceOperator": "Votre nom d’affichage", + "models.displayNameSourceProvider": "Nom du fournisseur", + "models.displayNameSourceFallback": "ID du modèle par défaut", + "models.displayNameField": "Nom d’affichage", + "models.displayNamePlaceholder": "p. ex. Grok 4.6", + "models.displayNameHelp": "Modifie uniquement l’affichage. Le routage reste {model}.", + "models.displayNameReset": "Réinitialiser le nom", + "models.displayNameSaved": "Nom d’affichage enregistré", + "models.displayNameResetDone": "Nom d’affichage réinitialisé", + "models.displayNameSaveFailed": "Impossible d’enregistrer le nom d’affichage", + "models.displayNameRequired": "Saisissez un nom d’affichage ou utilisez Réinitialiser le nom.", + "models.displayNameTooLong": "Le nom d’affichage doit contenir au maximum 128 caractères.", + "models.displayNameNoSlash": "Le nom d’affichage ne peut pas contenir /.", + "models.displayNameNoControl": "Le nom d’affichage ne peut pas contenir de caractères de contrôle.", }; diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 9b70253190..25d77df178 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -2761,4 +2761,47 @@ export const ja: Record = { "logs.agent.internal": "内部", "logs.agent.unknown": "不明", "logs.agent.badgeTitle": "リクエストの発信元", + "models.pickerOrder.label": "モデル選択順", + "models.pickerOrder.default": "デフォルト", + "models.pickerOrder.alphabetical": "モデル名のA–Z順", + "models.pickerOrder.provider": "プロバイダー別", + "models.pickerOrder.mostUsed": "使用量のスナップショット", + "models.pickerOrder.custom": "カスタム順", + "models.pickerOrder.apply": "順序を適用", + "models.pickerOrder.applying": "適用中…", + "models.pickerOrder.saved": "選択順を保存しました。古い一覧が表示される場合はクライアントを開き直してください。", + "models.pickerOrder.pending": "順序を保存しました。カタログの更新は保留中です。", + "models.pickerOrder.usageFailed": "モデル使用量を読み込めませんでした。", + "models.pickerOrder.loadFailed": "モデル選択設定を読み込めませんでした。", + "models.pickerOrder.retry": "再試行", + "models.pickerOrder.hint": "CodexとClaudeの検出一覧のルーティングモデル順を保存します。優先・ネイティブの順位帯は維持されます。使用量順はスナップショットで、ネイティブツールの候補表示は変わる場合があります。", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Raycast の providers.yaml に OpenCodex のプロバイダーエントリを追加し、ルーティングされたすべてのモデルを Raycast AI のモデル選択に表示します。Raycast Pro が必要です。", + "integrations.raycast.proRequired": "Custom Providers は Raycast Pro の機能です。ファイルは書き込まれますが、Pro サブスクリプションが有効になるまで Raycast はこれを無視します。", + "integrations.raycast.planUnknown": "Raycast Pro が有効かどうか確認できませんでした。Custom Providers には Raycast Pro が必要です。", + "integrations.raycast.revealConfig": "Raycast → 設定 → AI を開き、「Reveal Providers Config」を一度クリックして providers フォルダを作成してください。", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "変更は保存されましたが、モデル一覧を更新できませんでした。再試行してください。", + "models.displayNameOutcomeUnknown": "リクエストが完了しませんでした。変更が保存されている可能性があります。再度変更する前に再試行して現在の名前を確認してください。", + "models.displayNameCurrentUnavailable": "更新するまで現在の名前を確認できません", + "models.displayNameReloaded": "モデル一覧を更新しました", + "models.displayNameAction": "名前", + "models.displayNameActionLabel": "{model} の表示名を編集", + "models.displayNameTitle": "表示名", + "models.displayNameModelId": "モデル ID", + "models.displayNameCurrent": "現在の名前", + "models.displayNameSourceOperator": "設定した名前", + "models.displayNameSourceProvider": "プロバイダー名", + "models.displayNameSourceFallback": "モデル ID の既定値", + "models.displayNameField": "表示名", + "models.displayNamePlaceholder": "例: Grok 4.6", + "models.displayNameHelp": "表示だけを変更します。ルーティングは {model} のままです。", + "models.displayNameReset": "名前をリセット", + "models.displayNameSaved": "表示名を保存しました", + "models.displayNameResetDone": "表示名をリセットしました", + "models.displayNameSaveFailed": "表示名を保存できませんでした", + "models.displayNameRequired": "表示名を入力するか、名前をリセットしてください。", + "models.displayNameTooLong": "表示名は 128 文字以内にしてください。", + "models.displayNameNoSlash": "表示名に / は使用できません。", + "models.displayNameNoControl": "表示名に制御文字は使用できません。", }; diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 5b2292855d..b0c371df01 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -2762,4 +2762,47 @@ export const ko: Record = { "logs.agent.internal": "내부", "logs.agent.unknown": "알 수 없음", "logs.agent.badgeTitle": "요청 출처", + "models.pickerOrder.label": "모델 선택 순서", + "models.pickerOrder.default": "기본값", + "models.pickerOrder.alphabetical": "모델 이름순", + "models.pickerOrder.provider": "프로바이더별", + "models.pickerOrder.mostUsed": "사용량순 스냅샷", + "models.pickerOrder.custom": "사용자 지정 순서", + "models.pickerOrder.apply": "순서 적용", + "models.pickerOrder.applying": "적용 중…", + "models.pickerOrder.saved": "모델 선택 순서를 저장했습니다. 이전 목록이 보이면 클라이언트를 다시 열어 주세요.", + "models.pickerOrder.pending": "순서를 저장했습니다. 카탈로그 갱신은 아직 완료되지 않았습니다.", + "models.pickerOrder.usageFailed": "모델 사용량을 불러오지 못했습니다.", + "models.pickerOrder.loadFailed": "모델 선택 설정을 불러오지 못했습니다.", + "models.pickerOrder.retry": "다시 시도", + "models.pickerOrder.hint": "Codex·Claude 검색 목록의 라우팅 모델 순서를 저장합니다. 지정 모델·네이티브 모델의 우선순위 구간은 유지됩니다. 사용량순은 스냅샷이며, 네이티브 도구에 표시되는 후보는 달라질 수 있습니다.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Raycast의 providers.yaml에 OpenCodex 프로바이더 항목을 추가해 라우팅된 모든 모델이 Raycast AI 모델 선택기에 표시되도록 합니다. Raycast Pro가 필요합니다.", + "integrations.raycast.proRequired": "Custom Providers는 Raycast Pro 기능입니다. 파일은 기록되지만 Pro 구독이 활성화될 때까지 Raycast는 이를 무시합니다.", + "integrations.raycast.planUnknown": "Raycast Pro 활성 여부를 확인할 수 없습니다. Custom Providers에는 Raycast Pro가 필요합니다.", + "integrations.raycast.revealConfig": "Raycast → 설정 → AI를 열고 「Reveal Providers Config」를 한 번 클릭해 providers 폴더를 만드세요.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "변경 사항은 저장되었지만 모델 목록을 새로 고치지 못했습니다. 다시 시도해 주세요.", + "models.displayNameOutcomeUnknown": "요청이 완료되지 않았습니다. 변경 사항이 저장되었을 수 있습니다. 다시 변경하기 전에 재시도하여 현재 이름을 확인하세요.", + "models.displayNameCurrentUnavailable": "새로 고침 전까지 현재 이름을 확인할 수 없음", + "models.displayNameReloaded": "모델 목록을 새로 고쳤습니다", + "models.displayNameAction": "이름", + "models.displayNameActionLabel": "{model}의 표시 이름 편집", + "models.displayNameTitle": "표시 이름", + "models.displayNameModelId": "모델 ID", + "models.displayNameCurrent": "현재 이름", + "models.displayNameSourceOperator": "운영자 지정 이름", + "models.displayNameSourceProvider": "프로바이더 제공 이름", + "models.displayNameSourceFallback": "모델 ID 기본값", + "models.displayNameField": "표시 이름", + "models.displayNamePlaceholder": "예: Grok 4.6", + "models.displayNameHelp": "표시 방식만 변경합니다. 라우팅은 {model}로 유지됩니다.", + "models.displayNameReset": "이름 초기화", + "models.displayNameSaved": "표시 이름이 저장되었습니다", + "models.displayNameResetDone": "표시 이름이 초기화되었습니다", + "models.displayNameSaveFailed": "표시 이름을 저장하지 못했습니다", + "models.displayNameRequired": "표시 이름을 입력하거나 이름 초기화를 사용하세요.", + "models.displayNameTooLong": "표시 이름은 128자 이하여야 합니다.", + "models.displayNameNoSlash": "표시 이름에 /를 사용할 수 없습니다.", + "models.displayNameNoControl": "표시 이름에 제어 문자를 사용할 수 없습니다.", }; diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 9b299def35..34b30d8535 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -2763,4 +2763,47 @@ export const ru: Record = { "logs.agent.internal": "Внутренний", "logs.agent.unknown": "Неизвестно", "logs.agent.badgeTitle": "Источник запроса", + "models.pickerOrder.label": "Порядок моделей", + "models.pickerOrder.default": "По умолчанию", + "models.pickerOrder.alphabetical": "По имени A–Z", + "models.pickerOrder.provider": "По провайдеру", + "models.pickerOrder.mostUsed": "Снимок использования", + "models.pickerOrder.custom": "Свой порядок", + "models.pickerOrder.apply": "Применить порядок", + "models.pickerOrder.applying": "Применение…", + "models.pickerOrder.saved": "Порядок сохранён. Перезапустите клиенты, показывающие старый каталог.", + "models.pickerOrder.pending": "Порядок сохранён; обновление каталога ожидается.", + "models.pickerOrder.usageFailed": "Не удалось загрузить статистику моделей.", + "models.pickerOrder.loadFailed": "Не удалось загрузить настройки выбора.", + "models.pickerOrder.retry": "Повторить", + "models.pickerOrder.hint": "Сохраняет порядок маршрутизируемых моделей для Codex и обнаружения Claude. Диапазоны приоритетных и нативных моделей сохраняются. Использование — снимок; нативно объявляемые варианты могут измениться.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Добавляет запись провайдера OpenCodex в providers.yaml Raycast, чтобы каждая маршрутизируемая модель появилась в выборе моделей Raycast AI. Требуется Raycast Pro.", + "integrations.raycast.proRequired": "Custom Providers — функция Raycast Pro. Файл будет записан, но Raycast игнорирует его, пока не активна подписка Pro.", + "integrations.raycast.planUnknown": "Не удалось определить, активен ли Raycast Pro; для Custom Providers требуется Raycast Pro.", + "integrations.raycast.revealConfig": "Откройте Raycast → Настройки → AI и один раз нажмите «Reveal Providers Config», чтобы папка провайдеров появилась.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "Изменение сохранено, но список моделей не удалось обновить. Повторите попытку.", + "models.displayNameOutcomeUnknown": "Запрос не завершён. Изменение могло сохраниться. Повторите попытку, чтобы проверить текущее имя перед следующим изменением.", + "models.displayNameCurrentUnavailable": "Текущее имя недоступно до обновления", + "models.displayNameReloaded": "Список моделей обновлён", + "models.displayNameAction": "Имя", + "models.displayNameActionLabel": "Изменить понятное имя для {model}", + "models.displayNameTitle": "Понятное имя", + "models.displayNameModelId": "ID модели", + "models.displayNameCurrent": "Текущее имя", + "models.displayNameSourceOperator": "Ваше имя", + "models.displayNameSourceProvider": "Имя провайдера", + "models.displayNameSourceFallback": "ID модели по умолчанию", + "models.displayNameField": "Понятное имя", + "models.displayNamePlaceholder": "например, Grok 4.6", + "models.displayNameHelp": "Меняет только отображение. Маршрут остаётся {model}.", + "models.displayNameReset": "Сбросить имя", + "models.displayNameSaved": "Понятное имя сохранено", + "models.displayNameResetDone": "Понятное имя сброшено", + "models.displayNameSaveFailed": "Не удалось сохранить понятное имя", + "models.displayNameRequired": "Введите понятное имя или используйте Сбросить имя.", + "models.displayNameTooLong": "Понятное имя должно содержать не более 128 символов.", + "models.displayNameNoSlash": "Понятное имя не может содержать /.", + "models.displayNameNoControl": "Понятное имя не может содержать управляющие символы.", }; diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index a4a61c33d9..e502d9a686 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -2763,4 +2763,47 @@ export const tr: Record = { "logs.agent.internal": "Dahili", "logs.agent.unknown": "Bilinmiyor", "logs.agent.badgeTitle": "İstek kaynağı", + "models.pickerOrder.label": "Model sırası", + "models.pickerOrder.default": "Varsayılan", + "models.pickerOrder.alphabetical": "Model adına göre A–Z", + "models.pickerOrder.provider": "Sağlayıcıya göre", + "models.pickerOrder.mostUsed": "Kullanım anlık görüntüsü", + "models.pickerOrder.custom": "Özel sıra", + "models.pickerOrder.apply": "Sırayı uygula", + "models.pickerOrder.applying": "Uygulanıyor…", + "models.pickerOrder.saved": "Model sırası kaydedildi. Eski kataloğu gösteren istemcileri yeniden açın.", + "models.pickerOrder.pending": "Sıra kaydedildi; katalog yenilemesi bekleniyor.", + "models.pickerOrder.usageFailed": "Model kullanımı yüklenemedi.", + "models.pickerOrder.loadFailed": "Seçici ayarları yüklenemedi.", + "models.pickerOrder.retry": "Yeniden dene", + "models.pickerOrder.hint": "Codex ve Claude keşfi için yönlendirilen model sırasını kaydeder. Öne çıkan/yerel öncelik aralıkları korunur. Kullanım bir anlık görüntüdür; yerel araçta sunulan seçenekler değişebilir.", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "Raycast'in providers.yaml dosyasına bir OpenCodex sağlayıcı girdisi ekler; böylece yönlendirilen her model Raycast AI model seçicisinde görünür. Raycast Pro gerekir.", + "integrations.raycast.proRequired": "Custom Providers bir Raycast Pro özelliğidir. Dosya yazılır, ancak bir Pro aboneliği etkin olana kadar Raycast bunu yok sayar.", + "integrations.raycast.planUnknown": "Raycast Pro’nun etkin olup olmadığı belirlenemedi; Custom Providers için Raycast Pro gerekir.", + "integrations.raycast.revealConfig": "Raycast → Ayarlar → AI bölümünü açıp sağlayıcı klasörünün oluşması için „Reveal Providers Config“ seçeneğine bir kez tıklayın.", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "Değişiklik kaydedildi ancak model listesi yenilenemedi. Yenilemek için tekrar deneyin.", + "models.displayNameOutcomeUnknown": "İstek tamamlanmadı. Değişiklik kaydedilmiş olabilir. Başka bir değişiklik yapmadan önce geçerli adı kontrol etmek için tekrar deneyin.", + "models.displayNameCurrentUnavailable": "Geçerli ad yenilemeye kadar kullanılamıyor", + "models.displayNameReloaded": "Model listesi yenilendi", + "models.displayNameAction": "Ad", + "models.displayNameActionLabel": "{model} için görünen adı düzenle", + "models.displayNameTitle": "Görünen ad", + "models.displayNameModelId": "Model kimliği", + "models.displayNameCurrent": "Geçerli ad", + "models.displayNameSourceOperator": "Sizin adınız", + "models.displayNameSourceProvider": "Sağlayıcı adı", + "models.displayNameSourceFallback": "Model kimliği varsayılanı", + "models.displayNameField": "Görünen ad", + "models.displayNamePlaceholder": "örn. Grok 4.6", + "models.displayNameHelp": "Yalnızca görünümü değiştirir. Yönlendirme {model} olarak kalır.", + "models.displayNameReset": "Adı sıfırla", + "models.displayNameSaved": "Görünen ad kaydedildi", + "models.displayNameResetDone": "Görünen ad sıfırlandı", + "models.displayNameSaveFailed": "Görünen ad kaydedilemedi", + "models.displayNameRequired": "Bir görünen ad girin veya Adı sıfırla seçeneğini kullanın.", + "models.displayNameTooLong": "Görünen ad en fazla 128 karakter olabilir.", + "models.displayNameNoSlash": "Görünen ad / içeremez.", + "models.displayNameNoControl": "Görünen ad denetim karakterleri içeremez.", }; diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index b1f3bae4b3..f2f6138b13 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2725,4 +2725,47 @@ export const zhTW: Record = { "logs.agent.internal": "內部", "logs.agent.unknown": "未知", "logs.agent.badgeTitle": "請求來源", + "models.pickerOrder.label": "模型選擇順序", + "models.pickerOrder.default": "預設", + "models.pickerOrder.alphabetical": "依模型名稱 A–Z", + "models.pickerOrder.provider": "依供應商分組", + "models.pickerOrder.mostUsed": "使用量快照", + "models.pickerOrder.custom": "自訂順序", + "models.pickerOrder.apply": "套用順序", + "models.pickerOrder.applying": "正在套用…", + "models.pickerOrder.saved": "選擇順序已儲存。若仍顯示舊目錄,請重新開啟用戶端。", + "models.pickerOrder.pending": "順序已儲存,目錄更新尚未完成。", + "models.pickerOrder.usageFailed": "無法載入模型使用量。", + "models.pickerOrder.loadFailed": "無法載入模型選擇設定。", + "models.pickerOrder.retry": "重試", + "models.pickerOrder.hint": "儲存 Codex 與 Claude 探索清單中的路由模型順序。保留精選與原生模型的優先級區間;使用量排序是快照,原生工具顯示的候選可能改變。", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中新增一個 OpenCodex 供應商項目,讓所有已路由的模型出現在 Raycast AI 模型選擇器中。需要 Raycast Pro。", + "integrations.raycast.proRequired": "Custom Providers 是 Raycast Pro 功能。檔案會被寫入,但在 Pro 訂閱生效之前 Raycast 會忽略它。", + "integrations.raycast.planUnknown": "無法確認 Raycast Pro 是否已啟用;Custom Providers 需要 Raycast Pro。", + "integrations.raycast.revealConfig": "開啟 Raycast → 設定 → AI,點一次「Reveal Providers Config」,以便建立 providers 資料夾。", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "變更已儲存,但無法重新整理模型清單。請重試。", + "models.displayNameOutcomeUnknown": "請求未完成。變更可能已儲存。再次變更之前,請重試以檢查目前名稱。", + "models.displayNameCurrentUnavailable": "重新整理之前無法取得目前名稱", + "models.displayNameReloaded": "模型清單已重新整理", + "models.displayNameAction": "名稱", + "models.displayNameActionLabel": "編輯 {model} 的友善名稱", + "models.displayNameTitle": "友善名稱", + "models.displayNameModelId": "模型 ID", + "models.displayNameCurrent": "目前名稱", + "models.displayNameSourceOperator": "你的名稱", + "models.displayNameSourceProvider": "供應商名稱", + "models.displayNameSourceFallback": "模型 ID 預設值", + "models.displayNameField": "友善名稱", + "models.displayNamePlaceholder": "例如 Grok 4.6", + "models.displayNameHelp": "只變更顯示方式。路由仍為 {model}。", + "models.displayNameReset": "重設名稱", + "models.displayNameSaved": "友善名稱已儲存", + "models.displayNameResetDone": "友善名稱已重設", + "models.displayNameSaveFailed": "無法儲存友善名稱", + "models.displayNameRequired": "請輸入友善名稱,或使用重設名稱。", + "models.displayNameTooLong": "友善名稱不能超過 128 個字元。", + "models.displayNameNoSlash": "友善名稱不能包含 /。", + "models.displayNameNoControl": "友善名稱不能包含控制字元。", }; diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index e32eeef87a..cf22e3f9d1 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -2761,4 +2761,47 @@ export const zh: Record = { "logs.agent.internal": "内部", "logs.agent.unknown": "未知", "logs.agent.badgeTitle": "请求来源", + "models.pickerOrder.label": "模型选择顺序", + "models.pickerOrder.default": "默认", + "models.pickerOrder.alphabetical": "按模型名 A–Z", + "models.pickerOrder.provider": "按提供商分组", + "models.pickerOrder.mostUsed": "使用量快照", + "models.pickerOrder.custom": "自定义顺序", + "models.pickerOrder.apply": "应用顺序", + "models.pickerOrder.applying": "正在应用…", + "models.pickerOrder.saved": "选择顺序已保存。若仍显示旧目录,请重新打开客户端。", + "models.pickerOrder.pending": "顺序已保存,目录刷新尚未完成。", + "models.pickerOrder.usageFailed": "无法加载模型使用量。", + "models.pickerOrder.loadFailed": "无法加载模型选择设置。", + "models.pickerOrder.retry": "重试", + "models.pickerOrder.hint": "保存 Codex 和 Claude 发现列表中的路由模型顺序。保留精选与原生模型的优先级区间;使用量排序是快照,原生工具显示的候选可能变化。", + "integrations.tab.raycast": "Raycast", + "integrations.semantics.raycast": "在 Raycast 的 providers.yaml 中添加一个 OpenCodex 提供商条目,让所有已路由的模型出现在 Raycast AI 模型选择器中。需要 Raycast Pro。", + "integrations.raycast.proRequired": "Custom Providers 是 Raycast Pro 功能。文件会被写入,但在 Pro 订阅生效之前 Raycast 会忽略它。", + "integrations.raycast.planUnknown": "无法确定 Raycast Pro 是否已激活;Custom Providers 需要 Raycast Pro。", + "integrations.raycast.revealConfig": "打开 Raycast → 设置 → AI,点击一次“Reveal Providers Config”,以便创建 providers 文件夹。", + "api.clientConfig.clientRaycast": "Raycast", + "models.displayNameSavedRefreshFailed": "更改已保存,但无法刷新模型列表。请重试以刷新。", + "models.displayNameOutcomeUnknown": "请求未完成。更改可能已保存。再次更改之前,请重试以检查当前名称。", + "models.displayNameCurrentUnavailable": "刷新之前无法获取当前名称", + "models.displayNameReloaded": "模型列表已刷新", + "models.displayNameAction": "名称", + "models.displayNameActionLabel": "编辑 {model} 的友好名称", + "models.displayNameTitle": "友好名称", + "models.displayNameModelId": "模型 ID", + "models.displayNameCurrent": "当前名称", + "models.displayNameSourceOperator": "你的名称", + "models.displayNameSourceProvider": "提供商名称", + "models.displayNameSourceFallback": "模型 ID 默认值", + "models.displayNameField": "友好名称", + "models.displayNamePlaceholder": "例如 Grok 4.6", + "models.displayNameHelp": "仅更改显示方式。路由仍为 {model}。", + "models.displayNameReset": "重置名称", + "models.displayNameSaved": "友好名称已保存", + "models.displayNameResetDone": "友好名称已重置", + "models.displayNameSaveFailed": "无法保存友好名称", + "models.displayNameRequired": "请输入友好名称,或使用重置名称。", + "models.displayNameTooLong": "友好名称不能超过 128 个字符。", + "models.displayNameNoSlash": "友好名称不能包含 /。", + "models.displayNameNoControl": "友好名称不能包含控制字符。", }; diff --git a/gui/src/model-picker-order.ts b/gui/src/model-picker-order.ts new file mode 100644 index 0000000000..dfa5073b08 --- /dev/null +++ b/gui/src/model-picker-order.ts @@ -0,0 +1,106 @@ +export type ModelPickerOrderMode = "default" | "alphabetical" | "provider" | "most-used" | "custom"; +export type SavedModelPickerOrderMode = Exclude; +export interface ModelPickerUsage { + provider: string; + model: string; + resolvedModel?: string; + requests: number; +} +export interface PickerModelIdentity { provider: string; id: string; namespaced: string } +export interface PickerOrderSaved { + pickerOrder: string[]; + pickerOrderMode: SavedModelPickerOrderMode | null; +} +export interface PickerOrderSettings extends PickerOrderSaved { pickerAvailable: string[] } + +function stringList(value: unknown): value is string[] { + return Array.isArray(value) && value.every(id => typeof id === "string" && id.trim().length > 0); +} +function savedMode(value: unknown): value is SavedModelPickerOrderMode | null { + return value === null || value === "alphabetical" || value === "provider" || value === "most-used"; +} +export function isPickerOrderSaved(value: unknown): value is PickerOrderSaved { + if (value === null || typeof value !== "object") return false; + const row = value as Record; + return stringList(row.pickerOrder) && savedMode(row.pickerOrderMode); +} +export function isPickerOrderSettings(value: unknown): value is PickerOrderSettings { + return isPickerOrderSaved(value) && stringList((value as PickerOrderSettings).pickerAvailable); +} +export function isModelPickerUsage(value: unknown): value is ModelPickerUsage[] { + return Array.isArray(value) && value.every(row => row !== null && typeof row === "object" + && typeof row.provider === "string" && typeof row.model === "string" + && (row.resolvedModel === undefined || typeof row.resolvedModel === "string") + && typeof row.requests === "number" && Number.isFinite(row.requests) && row.requests >= 0); +} +function parts(slug: string): [string, string] { + const slash = slug.indexOf("/"); + return slash < 0 ? ["", slug] : [slug.slice(0, slash), slug.slice(slash + 1)]; +} +// Fixed locale makes snapshots independent of the user's display language/OS locale. +const compare = (a: string, b: string) => a.localeCompare(b, "en"); +function byProvider(a: string, b: string): number { + const [ap, am] = parts(a), [bp, bm] = parts(b); + return compare(ap, bp) || compare(am, bm); +} + +export function modelPickerOrder( + mode: Exclude, + models: readonly string[], + usage: readonly ModelPickerUsage[] = [], + identities: readonly PickerModelIdentity[] = [], +): string[] | null { + if (mode === "default") return null; + const unique = [...new Set(models)]; + if (mode === "alphabetical") return unique.sort((a, b) => compare(parts(a)[1], parts(b)[1]) || byProvider(a, b)); + if (mode === "provider") return unique.sort(byProvider); + const candidates = new Set(unique); + const raw = new Map>(); + const owners = new Map>(); + for (const row of identities) { + if (!candidates.has(row.namespaced)) continue; + const key = JSON.stringify([row.provider, row.id]); + const values = raw.get(key) ?? new Set(); + values.add(row.namespaced); + raw.set(key, values); + const sources = owners.get(row.namespaced) ?? new Set(); + sources.add(key); + owners.set(row.namespaced, sources); + } + const unambiguous = (slug: string): string | null => (owners.get(slug)?.size ?? 0) > 1 ? null : slug; + const resolve = (provider: string, id: string): string | null | undefined => { + const exact = raw.get(JSON.stringify([provider, id])); + if (exact) return exact.size === 1 ? unambiguous([...exact][0]!) : null; + // A raw upstream slash is not a namespace. Use the observed identity table above; + // only fall back to an exact same-provider catalog id or an ordinary bare model id. + if (id.startsWith(`${provider}/`) && candidates.has(id)) return unambiguous(id); + const slug = `${provider}/${id}`; + return !id.includes("/") && candidates.has(slug) ? unambiguous(slug) : undefined; + }; + const counts = new Map(); + for (const row of usage) { + // Summary buckets are keyed by requested model. resolvedModel is only a + // representative observation, not proof that every request used that target. + const target = resolve(row.provider, row.model); + if (target) counts.set(target, (counts.get(target) ?? 0) + row.requests); + } + return unique.sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0) || byProvider(a, b)); +} + +export function modelPickerOrderMode( + models: readonly string[], saved: readonly string[], mode?: SavedModelPickerOrderMode | null, +): ModelPickerOrderMode { + if (saved.length === 0) return "default"; + // Existing complete/native orders are never silently replaced by a routed preset. + if (saved.some(id => !id.includes("/"))) return "custom"; + if (mode === "alphabetical" || mode === "provider" || mode === "most-used") return mode; + const candidates = new Set(models); + if (new Set(saved).size !== saved.length || saved.length !== candidates.size + || saved.some(id => !candidates.has(id))) return "custom"; + for (const preset of ["alphabetical", "provider"] as const) { + const expected = modelPickerOrder(preset, models); + if (expected !== null && expected.length === saved.length + && expected.every((id, index) => id === saved[index])) return preset; + } + return "custom"; +} diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index edf201143f..03a0a45005 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -14,15 +14,7 @@ import { EmptyState, Notice } from "../ui"; import Debug from "./Debug"; import { LogsFilterBar } from "./logs-filter-bar"; import { logsClockAnchor, logsClockNow, type LogsClockAnchor } from "./logs-clock"; -import { - DEFAULT_LOG_FILTER_STATE, - extractLogFilterOptions, - filterLogs, - hasActiveLogFilters, - normalizedAgentKind, - type LogFilterState, - type PersistedAgentKind, -} from "./logs-filter"; +import { DEFAULT_LOG_FILTER_STATE, extractLogFilterOptions, filterLogs, hasActiveLogFilters, type LogFilterState } from "./logs-filter"; import type { LogsTab } from "./logs-tab-keydown"; import { logsTabKeyDown, readTabFromHash, selectLogsTab } from "./logs-tab-keydown"; @@ -35,6 +27,7 @@ import { sanitizeLogEntryRouteDecision, validCachedRouteDecision, } from "./log-route-decision"; +import { mergeLogDelta, parseLogPollResponse } from "./log-poll"; function logsCacheKey(apiBase: string): string { return `ocx.logs.list.v1:${apiBase}`; @@ -114,11 +107,6 @@ type AttemptRecoveryKind = | "rate-limit-429" | "anthropic-oauth-429" | "image-413" - | "cursor-envelope-echo" - | "cursor-routing-commentary" - | "cursor-duplicate-tool-call" - | "cursor-overflow-remint" - | "cursor-invalid-argument" | "empty-completion"; interface LogAttempt { @@ -148,7 +136,6 @@ export interface LogEntry { timestamp: number; model: string; provider: string; - agentKind?: PersistedAgentKind | string; surface?: LogSurface; conversationId?: string; /** @@ -193,20 +180,6 @@ export interface LogEntry { }; } -function agentKindLabelKey(kind: LogEntry["agentKind"]): "logs.agent.main" | "logs.agent.subagent" | "logs.agent.internal" | "logs.agent.unknown" { - const keys = { - main: "logs.agent.main", - subagent: "logs.agent.subagent", - internal: "logs.agent.internal", - unknown: "logs.agent.unknown", - } as const; - return keys[normalizedAgentKind(kind)]; -} - -function AgentKindBadge({ kind, t }: { kind: LogEntry["agentKind"]; t: TFn }) { - return {t(agentKindLabelKey(kind))}; -} - function validCachedLogs(cached: LogEntry[] | null): LogEntry[] | null { if (!Array.isArray(cached)) return null; for (const entry of cached) { @@ -325,11 +298,6 @@ const RECOVERY_KIND_KEYS = { "rate-limit-429": "logs.detail.attempt.recovery.rateLimit429", "anthropic-oauth-429": "logs.detail.attempt.recovery.anthropicOauth429", "image-413": "logs.detail.attempt.recovery.image413", - "cursor-envelope-echo": "logs.detail.attempt.recovery.cursorEnvelopeEcho", - "cursor-routing-commentary": "logs.detail.attempt.recovery.cursorRoutingCommentary", - "cursor-duplicate-tool-call": "logs.detail.attempt.recovery.cursorDuplicateToolCall", - "cursor-overflow-remint": "logs.detail.attempt.recovery.cursorOverflowRemint", - "cursor-invalid-argument": "logs.detail.attempt.recovery.cursorInvalidArgument", "empty-completion": "logs.detail.attempt.recovery.emptyCompletion", } as const satisfies Record; @@ -418,11 +386,16 @@ export default function Logs({ apiBase }: { apiBase: string }) { const filterClockRef = useRef<{ key: string; anchor?: LogsClockAnchor; active: boolean; request: number; }>({ key: resourceKey, active: false, request: 0 }); + const logPollRef = useRef<{ key: string; cursor: string | null; rows: LogEntry[] }>( + { key: resourceKey, cursor: null, rows: [] }, + ); // Invalidate the old resource at commit, before passive resource-loader effects. // A late body read must not mutate this page's clock, cache or retry state. useLayoutEffect(() => { const clock = { key: resourceKey, active: true, request: 0 }; filterClockRef.current = clock; + // Cached display rows never establish a cursor, including A -> B -> A. + logPollRef.current = { key: resourceKey, cursor: null, rows: [] }; setFilterClockNow(Date.now()); return () => { clock.active = false; }; }, [resourceKey]); @@ -495,29 +468,39 @@ export default function Logs({ apiBase }: { apiBase: string }) { logRetryRef.current = retry; } if (retry.failures > 0 && Date.now() < retry.nextAttemptAt) throw retry.error; + const poll = logPollRef.current; + const cursor = poll.key === resourceKey ? poll.cursor : null; + const url = `${apiBase}/api/logs?limit=2000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`; try { - const res = await fetch(`${apiBase}/api/logs?limit=2000`, { signal }); + const res = await fetch(url, { signal }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim()); - const body = await res.json() as LogEntry[] | { logs?: LogEntry[]; generatedAt?: unknown }; + const body: unknown = await res.json(); const receivedAt = performance.now(); - const raw = Array.isArray(body) ? body : (body.logs ?? []); - const next = raw.map(sanitizeLogEntryRouteDecision); + const parsed = parseLogPollResponse(body); + const incoming = parsed.rows.map(sanitizeLogEntryRouteDecision); + const next = cursor && parsed.cursor && !parsed.reset + ? mergeLogDelta(poll.rows, incoming) : incoming; // The resource-store generation guard runs only after this loader returns. // Guard these local side effects here as fetch/body readers may ignore abort. if (!isCurrent()) throw signal.reason ?? new DOMException("Obsolete log request", "AbortError"); - // Reconcile the selected provider when the accepted snapshot changes, using the - // latest user state rather than filters captured when the request started. The model - // value is an intentional free-text query and must survive ring rollover. + logPollRef.current = { key: resourceKey, cursor: parsed.cursor, rows: next }; + // Reconcile when the accepted snapshot changes, using the latest user state + // rather than filters captured when the request started. Persist disappearance + // as All so a later ring cannot resurrect a cleared selection. const options = extractLogFilterOptions(next); setFilters(previous => { + const model = previous.model.trim().toLowerCase(); const provider = previous.provider.trim().toLowerCase(); + const nextModel = model + ? options.models.find(option => option.trim().toLowerCase() === model) ?? "" + : ""; const nextProvider = provider ? options.providers.find(option => option.trim().toLowerCase() === provider) ?? "" : ""; - if (previous.provider === nextProvider) return previous; - return { ...previous, provider: nextProvider }; + if (previous.model === nextModel && previous.provider === nextProvider) return previous; + return { ...previous, model: nextModel, provider: nextProvider }; }); - const sample = logsClockAnchor(Array.isArray(body) ? undefined : body.generatedAt, receivedAt); + const sample = logsClockAnchor(parsed.generatedAt, receivedAt); if (sample) clock.anchor = sample; setFilterClockNow(logsClockNow(clock.anchor, receivedAt, Date.now())); logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; @@ -554,6 +537,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const fetchLogs = logsResource.refresh; const retryLogs = useCallback(() => { logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; + logPollRef.current = { key: resourceKey, cursor: null, rows: [] }; fetchLogs({ forceLoading: true }); }, [fetchLogs, resourceKey]); @@ -837,7 +821,6 @@ export default function Logs({ apiBase }: { apiBase: string }) { {modelLabel(log.resolvedModel ?? log.model)} - {log.shadowCallRewrittenFrom && ( {t("logs.col.model")}{modelLabel(detail.resolvedModel ?? detail.model)} {t("logs.col.provider")}{formatProviderDisplayName(detail.provider, t)} - {t("logs.filter.agent.label")} {(detail.requestedEffort || detail.effectiveEffort) && ( <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} )} diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index c6fc006dfd..c342866d7e 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -1,19 +1,24 @@ import { CodexStaleBanner } from "../components/codex-stale-banner"; +import ModelDisplayNameDialog from "../components/ModelDisplayNameDialog"; import { fetchCodexAppServerState } from "../codex-app-server-state"; import type { AppServerStateOutcome } from "../codex-app-server-state"; import { useCodexRestart } from "../use-codex-restart"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Switch, Notice, EmptyState, Select, Tooltip } from "../ui"; -import { IconChevron, IconBoxes, IconInfo, IconCheck, IconAlert, IconRefresh, IconPencil, IconTrash } from "../icons"; +import { IconChevron, IconBoxes, IconInfo, IconCheck, IconAlert, IconRefresh, IconPencil } from "../icons"; import { useT } from "../i18n/shared"; import type { TFn, TKey } from "../i18n/shared"; import { modelLabel } from "../model-display"; -import { formatNamespacedModelId, formatProviderDisplayName, providerDisplaySlug } from "../provider-icons"; +import { formatProviderDisplayName, providerDisplaySlug } from "../provider-icons"; import { readJsonIfOk, readJsonOrThrow } from "../fetch-json"; import { describeIntegrationRefusalParts } from "./integrations/refusal-copy"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; import { setClientResourceData } from "../client-resource"; -import { createBoundedFetch } from "../bounded-fetch"; +import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch"; +import { + isModelPickerUsage, isPickerOrderSaved, isPickerOrderSettings, modelPickerOrder, modelPickerOrderMode, + type ModelPickerOrderMode, type PickerOrderSettings, type ModelPickerUsage, +} from "../model-picker-order"; import { startVisibilityPoll } from "../visibility-poll"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; @@ -67,7 +72,7 @@ import { type V2Status, } from "./models-shared"; import { EmptyProviderHint } from "./models-provider-hints"; -import { shadowCallModelOptions, useModalDialog } from "./dashboard-shared"; +import { shadowCallModelOptions } from "./dashboard-shared"; import { shadowSourceModelBadge, shadowSourceModelLabel } from "./shadow-call-source"; type CachedModelsPage = { @@ -142,26 +147,48 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; return () => { appServerMounted.current = false; }; }, []); - const reloadAppServerState = useCallback((signal?: AbortSignal) => { - void fetchCodexAppServerState(apiBase, { signal }).then(outcome => { - if (signal?.aborted || !appServerMounted.current) return; + const appServerRead = useRef(null); + const appServerReadGeneration = useRef(0); + const appServerReadBase = useRef(apiBase); + const cancelAppServerRead = useCallback(() => { + appServerReadGeneration.current++; + appServerRead.current?.controller.abort(); + appServerRead.current?.clear(); + appServerRead.current = null; + }, []); + const reloadAppServerState = useCallback(async () => { + // An old restart callback must not start an A read after the page moved to B. + if (!appServerMounted.current || appServerReadBase.current !== apiBase) return; + cancelAppServerRead(); + const generation = appServerReadGeneration.current; + const bounded = createBoundedFetch(15_000); + appServerRead.current = bounded; + try { + const outcome = await fetchCodexAppServerState(apiBase, { signal: bounded.signal }); + if (bounded.signal.aborted || !appServerMounted.current + || appServerReadBase.current !== apiBase || generation !== appServerReadGeneration.current + || appServerRead.current !== bounded) return; setAppServerState(outcome.state); - }); - }, [apiBase]); + } finally { + // The observation owns its deadline until settlement, independently of PUT. + bounded.clear(); + if (appServerRead.current === bounded) appServerRead.current = null; + } + }, [apiBase, cancelAppServerRead]); // onSettled, not a per-button callback: the sidebar control knows nothing about // this page, and a restart succeeding there must still clear the banner here. const { restarting: codexRestarting, restart: handleCodexRestart } = useCodexRestart(apiBase, { - onSettled: () => reloadAppServerState(), + onSettled: () => { void reloadAppServerState(); }, }); useEffect(() => { - // Once on mount, on apiBase change, and when a restart settles anywhere in the - // app (restartEpoch) — never a timer. - const controller = new AbortController(); - reloadAppServerState(controller.signal); - return () => controller.abort(); - }, [reloadAppServerState, restartEpoch]); + // Once on mount/base change or restart completion, never on a timer. + appServerReadBase.current = apiBase; + setAppServerState(null); + void reloadAppServerState(); + return cancelAppServerRead; + }, [apiBase, cancelAppServerRead, reloadAppServerState, restartEpoch]); @@ -217,6 +244,40 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [contextCaps, setContextCaps] = useState>(() => cached?.contextCaps ?? {}); const [contextCapValues, setContextCapValues] = useState>(() => cached?.contextCapValues ?? {}); const [contextCapValue, setContextCapValue] = useState(() => cached?.contextCapValue ?? 350_000); + const pickerCacheKey = `${cacheKey}:picker-order`; + const cachedPicker = useMemo(() => { + const value = readSessionListCache(pickerCacheKey); + return isPickerOrderSettings(value) ? value : undefined; + }, [pickerCacheKey]); + const [pickerDraft, setPickerDraft] = useState(null); + const [pickerBusy, setPickerBusy] = useState(false); + const pickerFlight = useRef(null); + const pickerResource = useDataSurface( + pickerCacheKey, [apiBase], + useCallback(async (signal: AbortSignal) => { + const response = await fetch(`${apiBase}/api/subagent-models`, { signal }); + const data = await readJsonOrThrow(response); + if (!isPickerOrderSettings(data)) throw new Error("picker settings payload missing"); + if (signal.aborted) throw new Error("picker settings request aborted"); + writeSessionListCache(pickerCacheKey, data); + return data; + }, [apiBase, pickerCacheKey]), + { isEmpty: () => false, enabled: catalogActive, deadlineMs: 15_000, initialData: cachedPicker }, + ); + const pickerSettings = pickerResource.state.data; + const pickerMode = pickerDraft ?? modelPickerOrderMode( + pickerSettings?.pickerAvailable ?? [], pickerSettings?.pickerOrder ?? [], pickerSettings?.pickerOrderMode, + ); + useEffect(() => { + setPickerDraft(null); + setPickerBusy(false); + return () => { + pickerFlight.current?.controller.abort(); + pickerFlight.current?.clear(); + pickerFlight.current = null; + cancelAppServerRead(); + }; + }, [apiBase, catalogActive, cancelAppServerRead]); const [customCap, setCustomCap] = useState(""); const [showCustom, setShowCustom] = useState(false); const [providerCapCustomOpen, setProviderCapCustomOpen] = useState>({}); @@ -267,11 +328,23 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [threadsCustom, setThreadsCustom] = useState(""); const [showThreadsCustom, setShowThreadsCustom] = useState(false); const [v2HelpOpen, setV2HelpOpen] = useState(false); - const v2HelpTriggerRef = useRef(null); - const v2HelpDialogRef = useModalDialog(v2HelpOpen, v2HelpTriggerRef); const [customModalOpen, setCustomModalOpen] = useState(false); - const customModalTriggerRef = useRef(null); - const customDialogRef = useModalDialog(customModalOpen, customModalTriggerRef); + const [displayNameModel, setDisplayNameModel] = useState(null); + const [displayNameSaving, setDisplayNameSaving] = useState(false); + const [displayNameRequestError, setDisplayNameRequestError] = useState(null); + const [displayNameRecovery, setDisplayNameRecovery] = useState<{ + value: string | null | undefined; + confirmed: boolean; + } | null>(null); + const [displayNameCurrentPending, setDisplayNameCurrentPending] = useState(false); + const displayNameRequestRef = useRef(null); + const displayNameSavingRef = useRef(false); + useEffect(() => () => { + displayNameRequestRef.current?.controller.abort(); + displayNameRequestRef.current?.clear(); + displayNameRequestRef.current = null; + }, []); + const displayNameTriggerRef = useRef(null); const reloadAliases = useCallback(async (signal?: AbortSignal) => { const response = await fetch(`${apiBase}/api/aliases`, { signal }); @@ -280,11 +353,8 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; }, [apiBase]); useEffect(() => { const controller = new AbortController(); - const timeout = window.setTimeout(() => void reloadAliases(controller.signal), 0); - return () => { - window.clearTimeout(timeout); - controller.abort(); - }; + void reloadAliases(controller.signal); + return () => controller.abort(); }, [reloadAliases]); const saveProviderAlias = async (provider: string) => { @@ -352,8 +422,6 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [contextDefaultTouched, setContextDefaultTouched] = useState(false); const [contextSaving, setContextSaving] = useState(false); const [contextError, setContextError] = useState(""); - const contextModalTriggerRef = useRef(null); - const contextDialogRef = useModalDialog(contextModalProvider !== null, contextModalTriggerRef); const [hoveredModel, setHoveredModel] = useState<{ namespaced: string; rect: DOMRect } | null>(null); const hoverTimerRef = useRef | null>(null); const [shadowCall, setShadowCall] = useState(null); @@ -484,17 +552,18 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; ); const catalogState = catalogResource.state; - const load = useCallback(async (force = false): Promise => { + const load = useCallback(async (force = false, signal?: AbortSignal): Promise => { if (loadPendingRef.current && !force) return false; loadPendingRef.current = true; const generation = ++loadGenerationRef.current; try { - const next = await fetchCatalog(new AbortController().signal); + const next = await fetchCatalog(signal ?? new AbortController().signal); if (!shouldApplyLoadGeneration(generation, loadGenerationRef.current)) return false; applyCatalog(next); // Follow-up mutation refreshes retain their existing awaitable contract while publishing // the result through the same shared store used by the initial catalog subscription. setClientResourceData(cacheKey, next); + pickerResource.refresh(); return true; } catch { return false; @@ -503,32 +572,119 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; loadPendingRef.current = false; } } - }, [applyCatalog, cacheKey, fetchCatalog]); + }, [applyCatalog, cacheKey, fetchCatalog, pickerResource.refresh]); + + const finishDisplayNameEdit = useCallback(() => { + const trigger = displayNameTriggerRef.current; + setDisplayNameModel(null); + setDisplayNameRequestError(null); + setDisplayNameRecovery(null); + setDisplayNameCurrentPending(false); + window.setTimeout(() => { + if (trigger?.isConnected) trigger.focus(); + }, 0); + }, []); - /** #2465: load the per-provider preset preview. */ - const loadPresets = useCallback(async () => { - const bounded = createBoundedFetch(15_000); + const closeDisplayNameEdit = useCallback(() => { + if (!displayNameSavingRef.current) finishDisplayNameEdit(); + }, [finishDisplayNameEdit]); + + // undefined retries only the read after a confirmed write or an unknown outcome. + const saveDisplayName = useCallback(async (displayName: string | null | undefined) => { + const model = displayNameModel; + if (!model || displayNameSavingRef.current) return; + const bounded = createBoundedFetch(60_000); + displayNameRequestRef.current = bounded; + displayNameSavingRef.current = true; + setDisplayNameSaving(true); + setDisplayNameRequestError(null); + // A failed convergence retry cannot invalidate an earlier persistence receipt + // for the same value. Editing the draft clears recovery and starts a new intent. + let confirmed = displayNameRecovery?.confirmed === true + && (displayName === undefined || displayName === displayNameRecovery.value); + let receivedReceipt = displayName === undefined; + let refreshOnly = displayName === undefined; try { - const response = await fetch(`${apiBase}/api/model-presets`, { signal: bounded.signal }); - const data = await readJsonIfOk<{ providers?: Record }>(response); - setPresets(data?.providers ?? {}); - } catch { - // A preset preview is decoration on top of a working Models page; failing to load it must - // not take the page down. - setPresets({}); + if (displayName !== undefined) { + const response = await fetch( + `${apiBase}/api/providers/${encodeURIComponent(model.provider)}/model-display-names`, + { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ modelId: model.id, displayName }), + signal: bounded.signal, + }, + ); + // The route can persist the value and return 503 when catalog convergence fails. + // Keep that receipt instead of throwing away saved:true with the error body. + type DisplayNameReceipt = { + saved?: boolean; + error?: string; + displayName?: string; + displayNameOverride?: string | null; + displayNameSource?: ModelRow["displayNameSource"]; + }; + const result: DisplayNameReceipt | undefined = response.ok + ? await readJsonOrThrow(response, t("models.displayNameSaveFailed")) + : await response.json(); + bounded.signal.throwIfAborted(); + if (!result || typeof result !== "object" || Array.isArray(result) + || (!response.ok && result.saved !== true && typeof result.error !== "string")) { + throw new Error(t("models.displayNameSaveFailed")); + } + receivedReceipt = true; + const receiptConfirmed = response.ok || result.saved === true; + confirmed = confirmed || receiptConfirmed; + if (receiptConfirmed) { + const override = result.displayNameOverride === null ? undefined + : result.displayNameOverride ?? displayName ?? undefined; + const fields: Pick = { + displayName: result.displayName ?? override, + displayNameOverride: override, + displayNameSource: result.displayNameSource ?? (override ? "operator" : undefined), + }; + setModels(current => current.map(row => row.namespaced === model.namespaced ? { ...row, ...fields } : row)); + setDisplayNameModel({ ...model, ...fields }); + // A saved:true reset receipt omits the provider's effective fallback label. + setDisplayNameCurrentPending(fields.displayName === undefined); + } + if (!response.ok) { + throw new Error(result.error || t("models.displayNameSaveFailed")); + } + refreshOnly = true; + } + if (!await load(true, bounded.signal)) throw new Error(t("models.loadFail")); + bounded.signal.throwIfAborted(); + publishFeedback(true, confirmed + ? t(displayName === null || (displayName === undefined && displayNameRecovery?.value === null) + ? "models.displayNameResetDone" : "models.displayNameSaved") + : t("models.displayNameReloaded")); + finishDisplayNameEdit(); + } catch (error) { + if (displayNameRequestRef.current !== bounded) return; + // A dropped connection or unreadable body can hide a committed write just + // like a timeout. Reconcile by reading; never replay an unchanged old draft. + const unknownOutcome = !receivedReceipt || bounded.signal.aborted; + if (unknownOutcome && !confirmed) setDisplayNameCurrentPending(true); + setDisplayNameRecovery(confirmed || unknownOutcome || refreshOnly + ? { value: refreshOnly || unknownOutcome ? undefined : displayName, confirmed } + : null); + setDisplayNameRequestError(confirmed + ? t("models.displayNameSavedRefreshFailed") + : unknownOutcome || refreshOnly + ? t("models.displayNameOutcomeUnknown") + : error instanceof Error && error.message + ? error.message + : t("models.displayNameSaveFailed")); } finally { bounded.clear(); + if (displayNameRequestRef.current === bounded) { + displayNameRequestRef.current = null; + displayNameSavingRef.current = false; + setDisplayNameSaving(false); + } } - }, [apiBase]); - - const loadModelDiscovery = useCallback(async () => { - try { - const response = await fetch(`${apiBase}/api/model-discovery`); - setModelDiscovery((await readJsonIfOk(response)) ?? null); - } catch { - setModelDiscovery(null); - } - }, [apiBase]); + }, [apiBase, displayNameModel, displayNameRecovery, finishDisplayNameEdit, load, t]); // Shadow/v2 controls must not wait on the models catalog (live discovery can be slow). useEffect(() => { @@ -551,7 +707,18 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; window.clearTimeout(timeout); stop(); }; - }, [catalogActive, loadModelDiscovery, loadPresets, loadShadowCall, loadV2]); + // oxlint-disable-next-line react/react-compiler -- existing exhaustive-deps exception is intentional + // eslint-disable-next-line react-hooks/exhaustive-deps -- loadPresets is a plain async loader + // like the rest of this file's; a useCallback wrapper trips PreserveManualMemo, and the + // effect only ever needs the current closure. Verified 2026-08-27: converting both loaders + // to useCallback and completing the dep array turns ONE warning into five react-compiler + // errors - two PreserveManualMemo, two Immutability (they are declared ~430 lines below this + // effect), and one EffectSetState - so the note above still holds against oxlint 1.78. + // Both gates suppress this one rule for this one file by config rather than by comment: + // gui/.oxlintrc.json (override) and gui/doctor.config.json (ignore.overrides). An in-file + // react-doctor-disable comment was tried and removed - it changed nothing, and + // react/react-compiler penalises a component for carrying suppressions at all. + }, [catalogActive, loadShadowCall, loadV2]); const groups = useMemo( () => buildProviderModelGroups(models, providers), @@ -566,9 +733,6 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const catalogCountReady = models.length > 0 || catalogState.data !== undefined; const openContextSettings = (group: ProviderModelGroup) => { - contextModalTriggerRef.current = document.activeElement?.tagName === "BUTTON" - ? document.activeElement as HTMLButtonElement - : null; const modelIds = [...new Set([ ...group.rows.map(model => model.id), ...group.configuredModels, @@ -970,6 +1134,32 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; if (!v2 || v2.multiAgentMode === mode) return; await putV2Setting({ multiAgentMode: mode }); }; + + + /** + * #2465: load the per-provider preset preview. Rules are evaluated server-side against the + * CURRENT catalog, so the count shown is the count an apply would produce. + */ + const loadPresets = async () => { + try { + const bounded = createBoundedFetch(15_000); + const r = await fetch(`${apiBase}/api/model-presets`, { signal: bounded.signal }); + const data = await readJsonIfOk<{ providers?: Record }>(r); + setPresets(data?.providers ?? {}); + } catch { + // A preset preview is decoration on top of a working Models page; failing to load it must + // not take the page down. + setPresets({}); + } + }; + + const loadModelDiscovery = async () => { + try { + const r = await fetch(`${apiBase}/api/model-discovery`); + setModelDiscovery((await readJsonIfOk(r)) ?? null); + } catch { setModelDiscovery(null); } + }; + const saveModelDiscovery = async (policy: "on" | "off", provider?: string) => { const r = await fetch(`${apiBase}/api/model-discovery`, { method: "PUT", headers: { "content-type": "application/json" }, @@ -1091,6 +1281,10 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; hoverTimerRef.current = setTimeout(() => setHoveredModel(null), 120); }; + const keepRowTipOpen = () => { + if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current); + }; + const addCustomModel = async ( provider: string, modelId: string, @@ -1268,7 +1462,6 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; className="btn btn-ghost btn-sm text-caption" onClick={(e) => { e.stopPropagation(); - customModalTriggerRef.current = e.currentTarget; setCustomModalMode("add"); setCustomModalProvider(provider); setCustomModalId(""); @@ -1473,79 +1666,56 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; void applyVisibility("models", provider, [{ id: m.id, native: m.native === true }], off)} disabled={busy || m.initialSelectionPending} label={m.native ? m.id : m.namespaced} /> {m.initialSelectionPending && {t("models.initialSelectionPending")}} {aliases.models[provider]?.[m.id] && {aliases.models[provider][m.id].alias}} - {m.native ? modelLabel(m.id) : formatNamespacedModelId(m.namespaced, t)} + + {m.native ? modelLabel(m.id) : m.namespaced} + {!m.native && m.displayName?.trim() && m.displayName.trim() !== m.namespaced && ( + {m.displayName.trim()} + )} + {aliases.models[provider]?.[m.id]?.source === "builtin" && {t("models.aliasAuto")}} + {!m.native && !m.custom && ( + + )} {m.custom && ( {t("models.customBadge")} )} - {m.custom && m.customId && ( - - - - - )} {!m.custom && recentIds.has(m.id) && {t("models.newBadge")}} {m.contextCapped && {t("models.contextCappedValue", { value: fmtK(m.contextCap ?? contextCapValue) })}}
{hoveredModel?.namespaced === m.namespaced && (() => { const r = hoveredModel.rect; - const tipWidth = Math.max(0, Math.min(480, window.innerWidth - 16)); - const tipLeft = Math.max(8, Math.min(r.left + 24, window.innerWidth - tipWidth - 8)); - const tipTop = Math.max(8, r.bottom + 4); - const roomBelow = Math.max(0, window.innerHeight - tipTop - 8); - const roomAbove = Math.max(0, r.top - 12); - const flipUp = roomBelow < Math.min(360, roomAbove); - const tipMaxHeight = Math.min(360, flipUp ? roomAbove : roomBelow); + const tipTop = r.bottom + 4; + const flipUp = tipTop + 360 > window.innerHeight; return (
{m.native ? m.id : m.namespaced}
{m.displayName &&
{m.displayName}
} @@ -1572,6 +1742,46 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; {t("models.tipStatus")} {off ? t("models.tipDisabled") : t("models.tipActive")}
+ {m.custom && m.customId && ( +
+ + +
+ )} ); })()} @@ -1595,6 +1805,52 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; ? groups.filter(group => group.provider === selectedProvider) : groups; + const savePickerOrder = async () => { + if (pickerFlight.current || !pickerSettings || pickerResource.state.showError || pickerMode === "custom") return; + const mode = pickerMode; + const available = pickerSettings.pickerAvailable; + const bounded = createBoundedFetch(15_000); + pickerFlight.current = bounded; + setPickerBusy(true); + try { + let usage: ModelPickerUsage[] = []; + if (mode === "most-used") { + const response = await fetch(`${apiBase}/api/usage?range=all&surface=all`, { signal: bounded.signal }); + const payload = await readJsonOrThrow<{ models?: unknown }>(response, t("models.pickerOrder.usageFailed")); + if (!isModelPickerUsage(payload?.models)) throw new Error(t("models.pickerOrder.usageFailed")); + usage = payload.models; + } + const order = modelPickerOrder(mode, available, usage, models); + const response = await fetch(`${apiBase}/api/subagent-models`, { + method: "PUT", headers: { "Content-Type": "application/json" }, signal: bounded.signal, + body: JSON.stringify({ pickerOrder: order, pickerOrderMode: mode === "default" ? null : mode }), + }); + const data = await readJsonOrThrow(response, t("models.saveFailed")); + if (!isPickerOrderSaved(data) || !("ok" in data) || data.ok !== true) throw new Error(t("models.saveFailed")); + if (bounded.signal.aborted || pickerFlight.current !== bounded) return; + const next = { ...pickerSettings, pickerOrder: data.pickerOrder, pickerOrderMode: data.pickerOrderMode }; + // This aborts an older GET and advances the shared resource generation. + setClientResourceData(pickerCacheKey, next); + writeSessionListCache(pickerCacheKey, next); + setPickerDraft(null); + + const refresh = "catalogRefresh" in data ? data.catalogRefresh : undefined; + const converged = refresh !== null && typeof refresh === "object" + && "status" in refresh && refresh.status === "committed" + && "degraded" in refresh && refresh.degraded === false; + publishFeedback(converged, t(converged ? "models.pickerOrder.saved" : "models.pickerOrder.pending")); + // Durable save is already accepted. Observational failure must not undo it. + void reloadAppServerState(); + } catch (error) { + if (pickerFlight.current === bounded) { + publishFeedback(false, error instanceof Error ? error.message : t("models.networkError")); + } + } finally { + bounded.clear(); + if (pickerFlight.current === bounded) { pickerFlight.current = null; setPickerBusy(false); } + } + }; + const controlsBlock = ( <>
@@ -1641,10 +1897,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; className="btn btn-ghost btn-sm" style={{ width: 24, height: 24, minWidth: 24, flex: "0 0 24px", padding: 0, borderRadius: "var(--radius-pill)", color: "var(--muted)" }} disabled={!v2} - onClick={event => { - v2HelpTriggerRef.current = event.currentTarget; - setV2HelpOpen(true); - }} + onClick={() => setV2HelpOpen(true)} aria-label={t("models.v2Label")} aria-haspopup="dialog" > @@ -1757,6 +2010,35 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; {t("models.setAllHint", { value: fmtK(contextCapValue) })}
+
+ {t("models.pickerOrder.label")} +