diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af09564cd08..9c9c6af8afd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,6 +261,10 @@ jobs: - 'src/service/**' - 'src/cli/index.ts' - 'src/lib/bun-runtime.ts' + - 'src/lib/standalone.ts' + - 'src/lib/keyring-native.ts' + - 'scripts/build-standalone.ts' + - 'scripts/standalone-keyring.ts' - 'package.json' - 'bun.lock' - '.github/workflows/ci.yml' @@ -276,6 +280,7 @@ jobs: - 'gui/**' - 'src/**' - 'scripts/build-standalone.ts' + - 'scripts/standalone-keyring.ts' - 'scripts/standalone-targets.ts' - 'package.json' - 'bun.lock' @@ -1400,10 +1405,11 @@ jobs: run: | set -euo pipefail triple="$(rustc -vV | sed -n 's/^host: //p')" - mkdir -p desktop/src-tauri/binaries desktop/src-tauri/resources/gui/dist + mkdir -p desktop/src-tauri/binaries desktop/src-tauri/resources/gui/dist desktop/src-tauri/resources/keyring : > "desktop/src-tauri/binaries/ocx-${triple}" chmod +x "desktop/src-tauri/binaries/ocx-${triple}" : > desktop/src-tauri/resources/gui/dist/.keep + : > desktop/src-tauri/resources/keyring/.keep - name: Check Rust formatting run: cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check @@ -1461,6 +1467,12 @@ jobs: cp -a "$DEB_BUNDLE/." "$BUNDLE_ROOT/deb/" chmod -R a-w "$BUNDLE_ROOT" + - name: Verify packaged Linux sidecar keyring + if: needs.changes.outputs.desktop == 'true' + env: + BUNDLE_ROOT: ${{ runner.temp }}/opencodex-linux-bundles + run: bash desktop/scripts/verify-linux-sidecar.sh "$BUNDLE_ROOT/appimage" + - name: Run Linux packaged-shell E2E if: needs.changes.outputs.desktop == 'true' env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d76276829d..08a5e66cd05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,18 +126,28 @@ jobs: include: - os: ubuntu-latest target: bun-linux-x64 + dependency_os: linux + dependency_cpu: x64 smoke: true - os: macos-latest target: bun-darwin-arm64 + dependency_os: darwin + dependency_cpu: arm64 smoke: true - os: macos-latest target: bun-darwin-x64 + dependency_os: darwin + dependency_cpu: x64 smoke: false - os: windows-latest target: bun-windows-x64 + dependency_os: win32 + dependency_cpu: x64 smoke: true - os: ubuntu-latest target: bun-linux-arm64 + dependency_os: linux + dependency_cpu: arm64 smoke: false runs-on: ${{ matrix.os }} timeout-minutes: 25 @@ -153,7 +163,7 @@ jobs: uses: ./.github/actions/setup-project-bun - name: Install dependencies - run: bun install --frozen-lockfile + run: bun install --frozen-lockfile --os=${{ matrix.dependency_os }} --cpu=${{ matrix.dependency_cpu }} - name: Build dashboard run: bun run build:gui @@ -200,9 +210,9 @@ jobs: set -euo pipefail cd "dist/standalone/$STANDALONE_TARGET" if [[ "$RUNNER_OS" == "Windows" ]]; then - powershell -NoProfile -Command 'Compress-Archive -Path ocx.exe,gui -DestinationPath ("../../ocx-{0}-{1}.zip" -f $env:RELEASE_VERSION,$env:STANDALONE_TARGET) -Force' + powershell -NoProfile -Command 'Compress-Archive -Path ocx.exe,gui,keyring -DestinationPath ("../../ocx-{0}-{1}.zip" -f $env:RELEASE_VERSION,$env:STANDALONE_TARGET) -Force' else - tar -czf "../../ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" ocx gui + tar -czf "../../ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" ocx gui keyring fi cd ../.. # The pre-publication verifier resolves every recorded checksum from @@ -232,16 +242,22 @@ jobs: include: - os: macos-latest target: universal-apple-darwin + dependency_os: darwin + dependency_cpu: "*" bundles: app,dmg sidecar-targets: macos artifact-suffixes: macos.dmg,macos.app.tar.gz - os: windows-latest target: x86_64-pc-windows-msvc + dependency_os: win32 + dependency_cpu: x64 bundles: msi sidecar-targets: x86_64-pc-windows-msvc artifact-suffixes: windows-x64.msi - os: ubuntu-22.04 target: x86_64-unknown-linux-gnu + dependency_os: linux + dependency_cpu: x64 bundles: appimage,deb sidecar-targets: x86_64-unknown-linux-gnu artifact-suffixes: linux-x86_64.AppImage,linux-amd64.deb @@ -275,7 +291,7 @@ jobs: run: bun scripts/release-version-sources.ts check "$RELEASE_VERSION" - name: Install project dependencies - run: bun install --frozen-lockfile + run: bun install --frozen-lockfile --os=${{ matrix.dependency_os }} --cpu=${{ matrix.dependency_cpu }} - name: Build dashboard run: bun run build:gui @@ -430,6 +446,15 @@ jobs: # diagnostics on the first attempt; Apple signing commands stay non-verbose. run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config "${{ runner.os == 'Windows' && format('{0}/opencodex-msi.json', runner.temp) || '{}' }}" + - name: Verify the packaged universal macOS runtime + if: runner.os == 'macOS' + run: | + set -euo pipefail + app=desktop/src-tauri/target/universal-apple-darwin/release/bundle/macos/OpenCodex.app + test -f "$app/Contents/Resources/keyring/keyring.darwin-arm64.node" + test -f "$app/Contents/Resources/keyring/keyring.darwin-x64.node" + bash desktop/scripts/verify-macos-runtime.sh "$app" + # Tauri patches a bundle-type marker into the application binary for each Linux format. # Keep each format in its own Cargo target so the deb cannot inherit the AppImage marker # and linuxdeploy cannot mutate the binary later consumed by the deb build. diff --git a/desktop/README.md b/desktop/README.md index 786bfbfc9e4..1ef61fa78b2 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -10,7 +10,10 @@ bunx tauri dev ``` The sidecar is generated from the repository's standalone binary build and is -not checked into git. +not checked into git. That build also stages the target-matching native keyring addon +under `keyring/`; Tauri copies it as a resource because Bun cannot load a `.node` addon +from the compiled executable's virtual filesystem. Universal macOS preparation requires +both Darwin optional packages (`bun install --frozen-lockfile --os=darwin --cpu=*`). The macOS tray panel is a SwiftUI/AppKit static library built from `app/Sources/NativeTray` by the Rust build script and linked into this process. diff --git a/desktop/scripts/prepare-sidecar.ts b/desktop/scripts/prepare-sidecar.ts index 2403e130933..20f7eec61f7 100644 --- a/desktop/scripts/prepare-sidecar.ts +++ b/desktop/scripts/prepare-sidecar.ts @@ -1,4 +1,4 @@ -import { copyFileSync, cpSync, existsSync, mkdirSync } from "node:fs"; +import { copyFileSync, cpSync, mkdirSync } from "node:fs"; import { join, resolve } from "node:path"; import { adHocSignSidecar, shouldAdHocSignSidecar } from "./sidecar-signing"; @@ -38,20 +38,20 @@ if (!triple || !targetByTriple[triple]) { const target = targetByTriple[triple]; const source = join(repoRoot, "dist", "standalone", target); const executable = join(source, target.startsWith("bun-windows-") ? "ocx.exe" : "ocx"); -if (!existsSync(executable)) { - const result = Bun.spawnSync([ - process.execPath, - "run", - "build:standalone", - "--target", - target, - ], { cwd: repoRoot, stdout: "inherit", stderr: "inherit" }); - if (result.exitCode !== 0) process.exit(result.exitCode); -} +// Preparation must consume this checkout, never a stale executable/addon pair left in dist. +const result = Bun.spawnSync([ + process.execPath, + "run", + "build:standalone", + "--target", + target, +], { cwd: repoRoot, stdout: "inherit", stderr: "inherit" }); +if (result.exitCode !== 0) process.exit(result.exitCode); const desktopRoot = resolve(import.meta.dir, ".."); const binaries = join(desktopRoot, "src-tauri", "binaries"); const resources = join(desktopRoot, "src-tauri", "resources", "gui", "dist"); +const keyringResources = join(desktopRoot, "src-tauri", "resources", "keyring"); mkdirSync(binaries, { recursive: true }); mkdirSync(resources, { recursive: true }); const destination = join(binaries, `ocx-${triple}${target.startsWith("bun-windows-") ? ".exe" : ""}`); @@ -60,5 +60,6 @@ if (shouldAdHocSignSidecar(process.platform, target)) { const signed = adHocSignSidecar(destination); if (signed !== 0) process.exit(signed); } +cpSync(join(source, "keyring"), keyringResources, { recursive: true }); cpSync(join(repoRoot, "gui", "dist"), resources, { recursive: true }); console.log(`Prepared ${destination}`); diff --git a/desktop/scripts/verify-linux-sidecar.sh b/desktop/scripts/verify-linux-sidecar.sh index 7695767ebe6..c502bb5c8ba 100644 --- a/desktop/scripts/verify-linux-sidecar.sh +++ b/desktop/scripts/verify-linux-sidecar.sh @@ -18,8 +18,17 @@ trap 'rm -rf "$scratch"' EXIT cd "$scratch" "${images[0]}" --appimage-extract > /dev/null sidecar="$scratch/squashfs-root/usr/bin/ocx" +keyring="$scratch/squashfs-root/usr/lib/OpenCodex/keyring/keyring.linux-x64-gnu.node" test ! -L "$sidecar" +test -f "$keyring" cmp "$original" "$sidecar" sha256sum "$original" "$sidecar" mkdir "$scratch/home" timeout 30s env OPENCODEX_HOME="$scratch/home" "$sidecar" --version +timeout 15s env HOME="$scratch/home" OPENCODEX_HOME="$scratch/home/opencodex" \ + "$sidecar" __keyring-load-check > "$scratch/keyring.json" +python3 - "$scratch/keyring.json" <<'PY' +import json, pathlib, sys +value = json.loads(pathlib.Path(sys.argv[1]).read_text()) +assert value == {"schema": "ocx-keyring-load/1", "available": True}, "Packaged keyring binding is unavailable" +PY diff --git a/desktop/scripts/verify-macos-runtime.sh b/desktop/scripts/verify-macos-runtime.sh index 1d9da80d634..43f1a31375c 100755 --- a/desktop/scripts/verify-macos-runtime.sh +++ b/desktop/scripts/verify-macos-runtime.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash set -euo pipefail -app="${1:?usage: verify-macos-runtime.sh /path/to/OpenCodex.app}" +app_input="${1:?usage: verify-macos-runtime.sh /path/to/OpenCodex.app}" +app="$(cd "$(dirname "$app_input")" && pwd)/$(basename "$app_input")" [[ "$(uname -s)" == Darwin ]] || { echo 'macOS bundle verification requires macOS' >&2; exit 1; } executable="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")" [[ -n "$executable" && "$executable" != */* ]] || { echo 'Invalid app executable name' >&2; exit 1; } scratch="$(mktemp -d "${TMPDIR:-/tmp}/opencodex-bundle-check.XXXXXX")" -trap 'rm -rf "$scratch"' EXIT +cleanup() { + rm -rf "$scratch" +} +trap cleanup EXIT codesign --verify --strict --deep "$app" verify_member() { @@ -39,4 +43,30 @@ value = json.loads(pathlib.Path(sys.argv[1]).read_text()) assert value.get("schema") == "ocx-resolve/1", "Unexpected resolve schema" assert value.get("liveness", {}).get("status") in ("live", "absent-proven"), "Unusable resolve result" PY -printf '%s\n' 'PASS: macOS signatures, exact entitlements, hardened runtime, Liquid Glass and bundled CLI resolve' + +# Reproduce the packaged-keyring boundary from an unrelated cwd. This is deliberately load-only: +# an ad-hoc CI identity can trigger a Keychain consent dialog, while issue #6139 is module resolution. +mkdir -p "$scratch/home" "$scratch/work" +python3 - "$app/Contents/MacOS/ocx" "$scratch/work" "$scratch/home" "$scratch/keyring.json" <<'PY' +import os, pathlib, subprocess, sys +ocx, work, home, output = sys.argv[1:] +env = os.environ.copy() +env.update(HOME=home, OPENCODEX_HOME=str(pathlib.Path(home) / ".opencodex")) +try: + with open(output, "wb") as stdout: + subprocess.run( + [ocx, "__keyring-load-check"], cwd=work, env=env, stdout=stdout, + stderr=subprocess.PIPE, check=True, timeout=15, + ) +except subprocess.TimeoutExpired as error: + raise SystemExit("Packaged keyring load probe timed out") from error +except subprocess.CalledProcessError as error: + sys.stderr.buffer.write((error.stderr or b"")[-4096:]) + raise SystemExit(f"Packaged keyring load probe exited {error.returncode}") from error +PY +python3 - "$scratch/keyring.json" <<'PY' +import json, pathlib, sys +value = json.loads(pathlib.Path(sys.argv[1]).read_text()) +assert value == {"schema": "ocx-keyring-load/1", "available": True}, "Packaged keyring binding is unavailable" +PY +printf '%s\n' 'PASS: macOS signatures, entitlements, hardened runtime, Liquid Glass, bundled CLI resolve and packaged keyring' diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 8d650cfc89d..33e014aeeb0 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.72.0" +version = "2.73.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 0b995e022c6..274c198474d 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.72.0" +version = "2.73.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index c3058739b74..69e4305acc8 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.72.0", + "version": "2.73.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", @@ -22,7 +22,8 @@ "binaries/ocx" ], "resources": { - "resources/gui/dist": "gui/dist" + "resources/gui/dist": "gui/dist", + "resources/keyring": "keyring" }, "icon": [ "icons/icon.icns", diff --git a/devlog/_fin/260928_anthropic_cooldown_recovery/000_decision.md b/devlog/_fin/260928_anthropic_cooldown_recovery/000_decision.md new file mode 100644 index 00000000000..eb75d9b5ecd --- /dev/null +++ b/devlog/_fin/260928_anthropic_cooldown_recovery/000_decision.md @@ -0,0 +1,46 @@ +# Anthropic cooldown recovery ownership + +## Decision log + +- Purpose: let an authoritative Anthropic usage refresh release a stale reset-derived + cooldown without weakening explicit upstream backoff or clearing another account's state. +- Existing constraints: routing health is process-local, usage probes are asynchronous, and + credentials or a newer 429 can replace the state observed when a probe starts. +- Alternatives considered: clear every cooldown after any successful usage response; clear + only through the operator endpoint; or bind recovery to the observed refusal and credential. +- Decision: a probe captures the exact account credential generation and cooldown generation + before dispatch. Settlement requires the same live credential, the same reset-derived + cooldown, a fresh timestamp, and utilization below 100% for every window that the 429 marked + rejected. Account-level single-flight keys also include an active recovery generation, so a + forced post-429 refresh cannot join work dispatched before the refusal. Any later cooldown + mutation revokes publication ownership as well as settlement ownership. Partial, failed, + exhausted, stale, Retry-After, and default-backoff evidence does not recover anything. +- Why this option: a successful quota HTTP response alone says neither which credential it + measured nor whether a newer refusal arrived while it was in flight. Generation fences make + those ownership claims explicit while preserving the existing manual escape hatch. +- Impact and trade-off: recovered accounts re-enter routing immediately; uncertain evidence + remains fail-closed until expiry or `clear-cooldown`. The extra bookkeeping is process-local + and bounded to one generation plus one health record per account. Superseded probes return an + unavailable result instead of publishing quota that no longer describes the routing state. + +## Data flow + +1. A 429 records its source, rejected quota windows, and a monotonic cooldown generation. +2. A fresh usage probe captures that generation plus the stored credential generation. + When recovery is pending, both the provider-usage flight and the outer account-quota flight + are generation-scoped, so the probe dispatches after the claim instead of joining older work + that might describe pre-refusal state. +3. The usage response is parsed and checked for complete headroom evidence. +4. Publication and settlement both require the observed cooldown generation to remain current. + Settlement deletes only the still-matching reset-derived record. + +The CLI dispatches `openai` to `/api/codex-auth/accounts/clear-cooldown` and `anthropic` to +`/api/oauth/accounts/clear-cooldown`. Anthropic IDs and aliases are resolved through the OAuth +account list before the write; other providers remain rejected because they do not expose this +process-local cooldown owner. + +## Focused verification + +- `tests/providers/anthropic-cooldown-recovery.test.ts` +- `tests/cli/cli-account-pool-verbs.test.ts` +- `tests/adapters/anthropic/anthropic-ratelimit-headers.test.ts` diff --git a/devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md b/devlog/_fin/260929_tokenlab_sponsor/000_roadmap.md similarity index 100% rename from devlog/_plan/260929_tokenlab_sponsor/000_roadmap.md rename to devlog/_fin/260929_tokenlab_sponsor/000_roadmap.md diff --git a/devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md b/devlog/_fin/260929_tokenlab_sponsor/010_merge_6221.md similarity index 100% rename from devlog/_plan/260929_tokenlab_sponsor/010_merge_6221.md rename to devlog/_fin/260929_tokenlab_sponsor/010_merge_6221.md diff --git a/devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md b/devlog/_fin/260929_tokenlab_sponsor/020_sponsor_pr.md similarity index 100% rename from devlog/_plan/260929_tokenlab_sponsor/020_sponsor_pr.md rename to devlog/_fin/260929_tokenlab_sponsor/020_sponsor_pr.md diff --git a/devlog/_fin/260930_release_2_72_0/000_plan.md b/devlog/_fin/260930_release_2_72_0/000_plan.md new file mode 100644 index 00000000000..149e4c57f19 --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/000_plan.md @@ -0,0 +1,17 @@ +# 2.72.0 — TokenLab release + +Owner request (2026-09-30): merge the TokenLab sponsor PR, verify for regressions, release, check +TokenLab's payment in the WORKS inbox through Aside, and have Aside email Vincent. + +Previous unit: `devlog/_fin/260929_tokenlab_sponsor/` merged #6221 (preset, `f6cddd7d69`) and opened +#6240 (sponsor placement + CLI pinning, head `21cddd35c9`, 32/32 PR checks green). Release shape is +2.71.0 (`devlog/_fin/260929_release_2_71_0/040_release.md`). + +| Doc | Work phase | Outcome | +|-----|-----------|---------| +| [010](./010_merge_6240.md) | wp2 | #6240 merged on full-lane CI (incl. windows 1–9) at its exact head | +| [020](./020_release.md) | wp3 | npm latest 2.72.0, preview 2.72.0-preview.20260930, GitHub releases, gitHead | +| [030](./030_payment_and_email.md) | wp4 | Payment/DocuSign status from WORKS; Vincent emailed by Aside exec; outcome recorded | + +Release content since v2.71.0: #6221 (TokenLab preset), #6240 (sponsor placement, CLI sponsor +pinning), devlog-only commits. diff --git a/devlog/_fin/260930_release_2_72_0/010_merge_6240.md b/devlog/_fin/260930_release_2_72_0/010_merge_6240.md new file mode 100644 index 00000000000..ec33545400b --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/010_merge_6240.md @@ -0,0 +1,14 @@ +# 010 — Regression gate and merge #6240 (wp2) + +1. Dispatch full-lane Cross-platform CI on the PR head: + `gh workflow run ci.yml -R lidge-jun/opencodex --ref codex/tokenlab-sponsor -f lane=all` + (the pull_request event skips windows 1–9 and macOS control; 2.71.0 used the same dispatch). + Required: every job success, windows 1/9–9/9 present, on the final head `2b9295fea6` (the run on `21cddd35c9` was cancelled when the final sponsor copy landed; see 030). +2. Local regression scope: the lanes' focused tests plus `test:changed`; the full local suite is + not runnable from a worktree under `~/.codex` (test home guard), so CI is the full-suite proof. +3. `scripts/ci/assert-mergeable-review.sh --maintainer-integration 6240 lidge-jun/opencodex`, a PR + comment recording owner authorization, exact head and run IDs: PR-event Cross-platform CI, + Service lifecycle (triggered by `desktop/**` and `package.json`), and the `lane=all` dispatch. +4. `gh pr merge 6240 --admin --squash --match-head-commit `. C is that exact squash commit, + fixed before anything else lands on `dev`; assert `git show C:package.json` reads 2.72.0. + The `260929_tokenlab_sponsor` plan docs on the branch land inside the squash. diff --git a/devlog/_fin/260930_release_2_72_0/011_wp2_execution.md b/devlog/_fin/260930_release_2_72_0/011_wp2_execution.md new file mode 100644 index 00000000000..6105dfe65d9 --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/011_wp2_execution.md @@ -0,0 +1,14 @@ +# 011 wp2 execution: regression gate and merge + +| Step | Evidence | +|---|---| +| Final copy | #6240 took TokenLab's final blurbs and referral link at `2b9295fea6` (030 findings); the `lane=all` run on `21cddd35c9` was cancelled | +| PR-event CI on `2b9295fea6` | Cross-platform CI 36591071773, Service lifecycle 36591071699, React Doctor 36591071756: success; 32 pass, 5 path-skipped | +| Full-lane gate | Cross-platform CI `lane=all` 36591083341: attempt 1 failed windows 7/9 (`cli-connect-readiness` installed-root probe, exit null at 18 s) and windows 8/9 (`main quota policy at native admission`, 32 s cases); neither loads a changed module (`init`, `provider-runtime` are lazy CLI imports). One rerun (attempt 2): both shards and `ci` success | +| Review | Codex P2 and CodeRabbit sponsor-first-run fixed; CodeRabbit wording suggestion declined (verbatim sponsor copy, adapter chip visible, Responses-first pending) | +| Policy | `assert-mergeable-review.sh --maintainer-integration 6240`: OK; decision comment 5894282491 | +| Merge | `gh pr merge 6240 --admin --squash --match-head-commit 2b9295fea6` → dev `1cd9d25517` = C; `package.json` 2.72.0, version-sources check 2.72.0 passes | +| Pre-move | #6243 (four version sources 2.72.0 → 2.73.0), `maintainer-sponsored` after review, merged → dev `73289d46ae` (2.73.0) | + +Local regression scope: focused sponsor/registry/README/GUI suites and `test:changed`; a full local run +is not possible from a worktree under `~/.codex` (test home guard), so CI above is the full-suite proof. diff --git a/devlog/_fin/260930_release_2_72_0/020_release.md b/devlog/_fin/260930_release_2_72_0/020_release.md new file mode 100644 index 00000000000..98da49e340a --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/020_release.md @@ -0,0 +1,25 @@ +# 020 — Release 2.72.0 (wp3) + +Same procedure as `devlog/_fin/260929_release_2_71_0/041_wp4_execution.md`, with C = the #6240 +squash commit from 010 (never a later `dev` tip, which would carry 2.73.0). + +1. Pre-move: `gh workflow run dev-version-bump.yml -R lidge-jun/opencodex --ref main + -f intended-version=2.72.0 -f mode=pre-move` → PR moving exactly the four version sources to + 2.73.0; merge `--admin --squash --match-head-commit` after its CI. +2. Preview: branch `codex/promote-preview-2.72.0` from C, `git merge -s ours origin/preview`, + `bun scripts/release-version-sources.ts sync 2.72.0-preview.20260930`, commit; diff vs C must be + exactly the four version sources, and `bun scripts/release-version-sources.ts` check mode passes. + PR to preview with the #6240 pr-assets screenshots, `gh pr merge --admin --merge --match-head-commit`. +3. Main: branch `codex/promote-main-2.72.0` from C, `git merge -s ours origin/main`, tree equals C. + (`git diff --quiet C HEAD`). PR to main with the screenshots, merged the same way. enforce-target + flags promotion PRs as wrong base by design; it is not required on preview/main. +4. Gate each promotion SHA: push-event Cross-platform CI and Service lifecycle `success`. +5. Dispatch preview then stable: + `gh workflow run release.yml --ref preview -f version=2.72.0-preview.20260930 -f tag=preview + -f dry-run=false -f expected-sha=`, then `--ref main -f version=2.72.0 -f tag=latest + -f dry-run=false -f expected-sha=
`. After an npm-acknowledged failure, resume with + `-f resume-after-npm-publish=true`; never republish. +6. Verify dist-tags, `npm view @bitkyc08/opencodex@2.72.0 gitHead` = main sha, `gh release view v2.72.0` + (not prerelease, 25 assets as v2.71.0), preview release prerelease, latest.json 2.72.0 signed. + +The installed proxy/app on this machine is not updated (same as 2.70.0/2.71.0). diff --git a/devlog/_fin/260930_release_2_72_0/021_wp3_execution.md b/devlog/_fin/260930_release_2_72_0/021_wp3_execution.md new file mode 100644 index 00000000000..a268eb8e9ef --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/021_wp3_execution.md @@ -0,0 +1,29 @@ +# 021 wp3 execution: promotion and release + +| Step | Evidence | +|---|---| +| Candidate | C = dev `1cd9d25517` (#6240 squash), version sources 2.72.0 | +| Pre-move | #6243 → dev `73289d46ae` (2.73.0), `maintainer-sponsored` after review | +| Preview promotion | `codex/promote-preview-2.72.0`: `-s ours` merge of origin/preview + sync to 2.72.0-preview.20260930 (`14ccfe1a1a`); diff vs C = four version sources; PR #6245 merged (merge commit) → preview `4f9e3f0afb` | +| Main promotion | `codex/promote-main-2.72.0`: `-s ours` merge of origin/main (`77cb00512f`), tree equals C; PR #6246 merged → main `5ab6d52b2a` | +| Push-event gates | preview: Cross-platform CI 36597831993, Service lifecycle 36597831950; main: Cross-platform CI 36597841262, Service lifecycle 36597841450 | + +Dispatches (after both gates of a SHA succeed), preview first: + +```sh +gh workflow run release.yml -R lidge-jun/opencodex --ref preview -f version=2.72.0-preview.20260930 -f tag=preview -f dry-run=false -f expected-sha=4f9e3f0afbbcf54a2b0421db8e962ec3d5682d5e +gh workflow run release.yml -R lidge-jun/opencodex --ref main -f version=2.72.0 -f tag=latest -f dry-run=false -f expected-sha=5ab6d52b2a4da722d398e4ab50a6c621ac3ce087 +``` + +## Results + +| Check | Evidence | +|---|---| +| Preview gates | Cross-platform CI 36597831993 success (push), Service lifecycle 36597831950 success | +| Main gates | Service lifecycle 36597841450 success; Cross-platform CI 36597841262 attempt 1 failed only `test 2/4` (batch 10/48 hit the 120 s process bound; the attribution sweep reported every file passing alone, "the timeout lives in multi-file process state"); one rerun, attempt 2 success | +| Preview release | release.yml 36602348988 success; npm `preview` = 2.72.0-preview.20260930, gitHead `4f9e3f0afb`, bins `ocx`/`opencodex` intact; GitHub release prerelease, 25 assets | +| Stable release | release.yml 36603799783 success; npm `latest` = 2.72.0 (published 17:45 UTC, visible ~10 min later, as with 2.71.0), gitHead `5ab6d52b2a`, bins intact; GitHub release v2.72.0 not prerelease, 25 assets; latest.json 2.72.0 signed for darwin-aarch64, darwin-x86_64, linux-x86_64, linux-x86_64-deb, windows-x86_64 | + +npm printed `"bin[...]" script name bin/ocx.mjs was invalid and removed` during both publishes; 2.70.0 and +2.71.0 printed the same, and the registry metadata keeps both bins (the `./` prefix is normalized). +The installed proxy and desktop app on this machine were not updated. diff --git a/devlog/_fin/260930_release_2_72_0/030_payment_and_email.md b/devlog/_fin/260930_release_2_72_0/030_payment_and_email.md new file mode 100644 index 00000000000..c76199ee79f --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/030_payment_and_email.md @@ -0,0 +1,29 @@ +# 030 — Payment check and sponsor email (wp4) + +1. Aside exec, read-only, WORKS inbox: TokenLab messages since 2026-09-29 18:00 KST (payment + confirmation, transaction hash, amount), DocuSign completion status. Where a transaction hash is + given, confirm it read-only on the public chain explorer against the recipient addresses in the + agreement (1,200 USDT, TRC-20 or ERC-20). +2. Only after npm `latest` reads 2.72.0 (the term starts at that release), Aside exec replies in the TokenLab thread from the maintainer mailbox, politely: payment received (only if confirmed), #6221/#6240 merged, released in + opencodex 2.72.0 (npm `@bitkyc08/opencodex`, release link), the 3-month term starts on that + release date per the agreement, README/picker placement live, the Responses-first proposal will be + evaluated separately. No attachments, no other recipients. +3. Record 090_outcome.md; move this unit and `260929_tokenlab_sponsor` to `devlog/_fin/` through a + docs PR to dev. + +Wallet addresses and transaction hashes stay out of the repository; the outcome records only that +payment was confirmed and when. + +## Findings (2026-09-30, before release) + +- WORKS inbox (Aside exec, read-only): Vincent wrote on 2026-09-29 23:08 KST that he signed the + agreement and paid 1,200 USDT on TRC-20, with a transaction hash, and sent final sponsor copy: a + longer English blurb, a Chinese blurb, and `https://tokenlab.sh/r/OPENCODEX` as the README and + picker link. A 23:13 message offers a USD 20 API-credit code for integration testing. +- On-chain (Tronscan, read-only): the hash is a confirmed, successful transfer on the official + USDT contract of exactly 1,200.000000 USDT to the agreement's TRC-20 address, at 2026-09-29 + 13:53 UTC; not flagged as risky. +- DocuSign: the only DocuSign mail in WORKS is the 21:08 sender-verification notice. No completion notice reached the maintainer mailbox; status notices go to the envelope sender's address, which this + check did not cover. Signature completion stays unverified here. +- Consequence for 010: #6240 takes the final copy and referral link (`2b9295fea6`) before the + regression gate; the earlier `lane=all` run on `21cddd35c9` was cancelled. diff --git a/devlog/_fin/260930_release_2_72_0/090_outcome.md b/devlog/_fin/260930_release_2_72_0/090_outcome.md new file mode 100644 index 00000000000..e8aedc57c73 --- /dev/null +++ b/devlog/_fin/260930_release_2_72_0/090_outcome.md @@ -0,0 +1,23 @@ +# 090 Outcome — 2.72.0 TokenLab release + +Closed 2026-09-30 (KST). + +| Criterion | Result | +|---|---| +| #6240 merged after full-lane CI on its exact head | Merged `1cd9d25517` from head `2b9295fea6`; lane=all 36591083341 success on attempt 2 (windows 7/9 and 8/9 reran once; neither loads a changed module) | +| npm and GitHub releases | `latest` 2.72.0 (gitHead `5ab6d52b2a`, main via #6246), `preview` 2.72.0-preview.20260930 (gitHead `4f9e3f0afb`, preview via #6245); release.yml 36603799783 / 36602348988; v2.72.0 has 25 assets and a signed latest.json | +| Payment and sponsor email | TokenLab's 1,200 USDT payment confirmed on-chain (2026-09-29 13:53 UTC). Reply sent from the maintainer mailbox through Aside exec at 2026-09-30 02:59 KST, confirming receipt, the 2.72.0 release, the placements and the term start | + +Release content since 2.71.0: #6221 (TokenLab preset, by @hedging8563), #6240 (sponsor placement, +CLI sponsor pinning, final sponsor copy and referral link). #6243 moved `dev` to 2.73.0 after the +candidate was pinned and is not part of 2.72.0. + +Per the agreement, the three-month sponsorship term starts with the 2.72.0 npm release +(2026-09-29 17:45 UTC, 2026-09-30 KST). + +Open items, outside this unit: +- DocuSign completion is not confirmed from the maintainer mailbox; TokenLab reports signing. Envelope + status goes to the sender account. +- TokenLab's Responses-first preset proposal (`X-TokenLab-Delivery-Policy`) needs its own PR and evidence. +- TokenLab offered a USD 20 API credit for integration testing; redeeming it is the maintainer's choice. +- The installed proxy and desktop app on this machine were not updated. diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/000_README.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/000_README.md new file mode 100644 index 00000000000..48a97ac491c --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/000_README.md @@ -0,0 +1,21 @@ +# 260930 GPT-6.1 Sol rollout, TokenLab protocol follow-up, release + +Status: open. Loop session `01a0ef42-da06-7200-8394-aa35ebbe4ba9`, branch `codex/gpt-6-1-sol-rollout` from `origin/dev` `b78bfb8f00`. + +OpenAI released GPT-6.1 Sol on 2026-09-29 as the successor to GPT-6 Sol. Only Sol moved to 6.1; Astra and Luna stay on GPT-6. This unit adds the model everywhere GPT-6 Sol is served, moves every place where GPT-6 Sol is the *default* to GPT-6.1 Sol (the same move #5640 made from GPT-5.6 to GPT-6), folds in TokenLab's protocol request from mail 546, and ships a release. + +| Doc | Work-phase | Content | +|---|---|---| +| 010_research_digest.md | wp1 | Sourced facts for GPT-6.1 Sol and the TokenLab contract | +| 020_catalog_surfaces.md | wp2 | Diff-level list of provider, catalog, pricing and docs rows | +| 030_default_swap.md | wp2 | Defaults moving from gpt-6-sol to gpt-6.1-sol, roster migration v3 | +| 040_tokenlab_protocols.md | wp3 | Per-model wire routing; TokenLab JEV decision backend deferred to its own unit | +| 050_release.md | wp4 | PR, CI, merge, preview/main promotion, release.yml, npm verification | + +## Audit record (wp1) + +Read-only reviewer on gpt-6-sol (high), 2026-09-30: + +- Round 1 FAIL, four blockers. Three concerned the JEV backend (combo dispatch passes no combo settings, combo normalization/persistence and the GUI editor would drop new fields, credential/URL/outbound guard must switch together). One named roster tests that enumerate models by value. +- Fold: the roster tests are listed in 020; the JEV backend is deferred to its own unit (040 records why and sketches it). +- Round 2 PASS. Non-blocking notes kept for B: the v3 roster migration must run after the v2 step and touch only bare `gpt-6-sol`; TokenLab's anthropic wire sends `x-api-key` to `/v1/messages`, which TokenLab accepts; `kiro-adapter.test.ts` (2047/2050) and `codex-catalog.test.ts` (7974/7985) take in-place edits only. diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/010_research_digest.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/010_research_digest.md new file mode 100644 index 00000000000..258a8e74f7e --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/010_research_digest.md @@ -0,0 +1,53 @@ +# 010 Research digest + +Observed 2026-09-30 KST. Full report with every URL: Aside run, `/Users/jun/.aside/u/0/artifacts/gpt61sol-research.md` (kept outside the repo). Live probes re-run from this checkout are marked (live). + +## GPT-6.1 Sol, official + +| Fact | Value | Source | +|---|---|---| +| API id | `gpt-6.1-sol`, no dated snapshot | developers.openai.com/api/docs/models/gpt-6.1-sol | +| Released | 2026-09-29 (API, ChatGPT Work, Codex) | openai.com/index/introducing-gpt-6-1-sol/ | +| API context / max input / max output | 1,050,000 / 922,000 / 128,000 | model page | +| API efforts | low, medium (default), high, xhigh, max | model page | +| Price per 1M | $2 input, $0.10 cached, $2.50 cache write, $10 output | pricing.md | +| Long context (>272K prompt) | $4 / $0.20 / $5 / $15 | pricing.md | +| Fast | 2x standard | pricing.md | +| Modalities | text + image in, text out | model page | +| Codex row (live, openai/codex models.json, #49318) | priority 1 (catalog default), efforts low..ultra, default effort low, context 272,000 / 872,000, Fast tier "2x speed, increased usage", minimal client 0.153.0 | codex-rs/models-manager/models.json | +| gpt-6-sol | retained: visibility list, priority 3, no upgrade, not deprecated | same file; deprecations page | +| GPT-6.1 Luna / Astra | do not exist (doc pages 404, absent from pricing, models index and Codex catalog) | model pages | + +Only cached input changed versus GPT-6 Sol ($0.20 -> $0.10). + +## Third-party listing (drives 020) + +| Provider in this repo | Status | Id | +|---|---|---| +| OpenRouter | listed | `openai/gpt-6.1-sol` (1,050,000 ctx, 128,000 out) | +| Vercel AI Gateway | listed | `openai/gpt-6.1-sol` | +| GitHub Copilot | listed, GA rollout (github.blog 2026-09-29) | `gpt-6.1-sol` | +| Kilo | listed (models.dev) | `openai/gpt-6.1-sol` | +| OpenCode Zen | listed (live /zen/v1/models) | `gpt-6.1-sol` | +| Devin | listed (devin.ai blog); id not published | preemptive `gpt-6-1-sol` following its `gpt-5-6-sol` spelling | +| TokenLab | listed (live /v1/models/gpt-6.1-sol, Chat + Responses) | `gpt-6.1-sol` (live discovery) | +| Amazon Bedrock | not in models.dev; openai/codex source references `openai.gpt-6.1-sol` | preemptive | +| Cloudflare AI Gateway | not in catalog; OpenAI passthrough | preemptive | +| Kiro, CodeBuddy | not listed / unreadable; both carry preemptive gpt-6-sol rows today | preemptive, same policy as the Sonnet 5.5 Kiro rows | +| ZenMux | page explicitly "not a registered ZenMux page" | not added (live discovery picks it up) | +| DigitalOcean, Scaleway | not listed; DO naming for 6.1 unknown | not added | + +## TokenLab (mail 546, 2026-09-30; live contract re-checked) + +`GET https://api.tokenlab.sh/v1/models/{id}` returns `tokenlab.accepted_request_formats` (live): + +| Models | Formats | +|---|---| +| gpt-6-astra, gpt-6-sol, gpt-6-luna, gpt-6.1-sol | chat, responses | +| claude-opus-5, claude-opus-5-5, claude-sonnet-5, claude-sonnet-5-5, claude-fable-5, claude-fable-5-1 | chat, anthropic_messages | +| deepseek-v4.1-flash, deepseek-v4-pro, kimi-k3, glm-5.3 | chat, responses, anthropic_messages | +| grok-4.7 | chat, responses | +| gemini-3.8-flash | chat, gemini_generate_content | + +Endpoints from docs.tokenlab.sh/llms.txt: `/v1/chat/completions`, `/v1/responses`, `/v1/messages` (x-api-key or Bearer), `/v1/systemone`. System One takes `{model, state, questions}` with Bearer auth, current model `jev-1.13` — the same body shape `src/combos/jev.ts` already sends to TypeSafe. Vincent asks to keep the user's API-key delivery policy default (no forced `X-TokenLab-Delivery-Policy`). + diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/020_catalog_surfaces.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/020_catalog_surfaces.md new file mode 100644 index 00000000000..fc9511b2394 --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/020_catalog_surfaces.md @@ -0,0 +1,30 @@ +# 020 Catalog surfaces for gpt-6.1-sol (wp2) + +Rule: add a row wherever a gpt-6-sol row exists and 010 says listed or preemptive; comment preemptive rows `260930 preemptive`. No gpt-6.1-luna / gpt-6.1-astra anywhere. + +| File | Change | +|---|---| +| scripts/model-metadata.source.json | Clone the gpt-6-sol row as gpt-6.1-sol in openai, openai-codex, github-copilot, kilo, openrouter, vercel-ai-gateway, opencode-zen, amazon-bedrock (preemptive), cloudflare-ai-gateway (preemptive); cache_read 0.10 (0.20 on opencode-zen per its published figure); release_date 2026-09-29; knowledge 2026-04-30. ZenMux not added. Then `bun run generate:model-metadata`. | +| src/usage/expected-prices.ts | `GPT61_SOL` cost4 (2 / 10 / 0.10 / 2.50) with long-context tier; rows for openai-apikey (verified), openai (verified-derived), devin, devin-cli (verified-derived); pricing-list id. | +| src/providers/registry/model-seeds.ts | `OPENAI_GPT6_MODELS` gains gpt-6.1-sol (flows to openai-apikey 1,050,000 / 922,000 / 128,000 / low..max and OpenRouter models); OpenRouter context map adds openai/gpt-6.1-sol. | +| src/providers/registry/entries-core.ts | OpenRouter `modelSupportsServiceTier` adds openai/gpt-6.1-sol; BizRouter seed adds openai/gpt-6.1-sol; Devin seed adds gpt-6-1-sol (preemptive). | +| src/providers/registry/entries-extended.ts | Copilot models + `modelWireDefaults` gpt-6.1-sol -> openai-responses. | +| src/providers/codebuddy-models.ts, src/providers/kiro-models.ts, src/adapters/kiro/reasoning.ts, src/adapters/devin/live-models.ts | Mirror the gpt-6-sol rows (preemptive). | +| src/codex/catalog/native-models.ts | `NATIVE_GPT61_SOL_MODEL = "gpt-6.1-sol"` in built-in list, SELF_DESCRIBED set, drain-sentinel set; configured-native template moves to gpt-6.1-sol. | +| src/codex/catalog/metadata.ts | DOCUMENTED_NATIVE_OPENAI_ADDITIONS and context map (NATIVE_GPT6_CONTEXT). | +| src/codex/catalog/effort.ts | Ladder entry low..ultra like Sol. | +| src/codex/data/roster-pinned-models.json | Append the upstream gpt-6.1-sol row verbatim from openai/codex models.json (codex-rs bundle, #49318); pinned-models.ts comment updated. | +| src/codex/model-entitlements.ts | Comment: 6.1 Sol ungated like Sol. | +| docs-site providers.md, structure/catalog.md, structure/providers/openai-accounts.md | Document the row. | + +Tests that enumerate the roster by value and must gain gpt-6.1-sol (audit A1 blocker 4): + +- tests/codex-integration/native-model-toggle.test.ts:95 (built-in native roster) and :127 (flagship list) +- tests/providers/provider-registry-parity.test.ts:314 (exact openai-apikey model list) +- tests/codex-integration/codex-catalog.test.ts:6842 (catalog roster; file is 7974/7985 lines, so edit in place without adding lines) +- tests/providers/github-copilot/github-copilot-wire-defaults.test.ts:33, tests/service/service-tier-capability.test.ts:73 (OpenRouter tier map), tests/routing/subagent-model-fallback.test.ts:276 +- tests/providers/kiro/kiro-adapter.test.ts:1744 is at 2047/2050: edit the existing list in place only. +- tests/codex-integration/configured-native-models.test.ts:93-102: a configured native must now borrow gpt-6.1-sol's pinned capabilities and hash (wp2 audit NEAR-PASS item). catalog-routed-comp-hash.test.ts:75 keeps its gpt-6-sol forward-alias expectation. +- BizRouter is not added: 010 has no evidence it lists gpt-6.1-sol, and live discovery picks it up once it does. + +New focused test `tests/codex-integration/gpt61-sol-rows.test.ts` (registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json) asserting: native row ladder low..ultra, default effort low, 272,000/872,000 context; openai-apikey 1,050,000/922,000/128,000 and low..max; Copilot Responses wire; expected price 2/10/0.10/2.50; no gpt-6.1-luna/astra id in any registry entry. diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/030_default_swap.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/030_default_swap.md new file mode 100644 index 00000000000..3c7267d977a --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/030_default_swap.md @@ -0,0 +1,14 @@ +# 030 Default swap gpt-6-sol -> gpt-6.1-sol (wp2) + +Precedent: #5640 (`8adda594fd`) set the subagent default to the GPT-6 trio and upgraded stored rosters from 5.6 once, by version. + +| Surface | Change | +|---|---| +| src/config/subagent-models.ts | `DEFAULT_SUBAGENT_MODELS = [astra, gpt-6.1-sol, luna]`; `SUBAGENT_MODELS_VERSION = 3`; the v3 step replaces bare `gpt-6-sol` with `gpt-6.1-sol` in place (deduped), once. A later deliberate re-pick of gpt-6-sol stays. v0/v1 installs chain through v2 then v3. | +| scripts/ci/docker-smoke.ts | subagentModels default trio. | +| Codex configured-native template | borrows gpt-6.1-sol's row (020). | +| docs-site agents.md (10 locales), guides/claude-code.md (10 locales), structure/subagents.md | Name the new default. | +| Tests | subagent-roster-migration (v2 -> v3, idempotence, re-pick retained, routed ids untouched), claude picker/intercept/management expectations that read the default trio, server startup reconcile. | + +Out of scope: removing gpt-6-sol anywhere; it stays listed and selectable. + diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/040_tokenlab_protocols.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/040_tokenlab_protocols.md new file mode 100644 index 00000000000..bb762036c79 --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/040_tokenlab_protocols.md @@ -0,0 +1,20 @@ +# 040 TokenLab per-model protocols and JEV backend (wp3) + +## Wires + +Keep the preset's provider-wide adapter `openai-chat` (the released, verified path). Add registry defaults so each model rides its declared native wire: + +- `modelWireDefaults` -> `{ wire: "openai-responses", inbound: ["responses"] }` for gpt-6-astra, gpt-6-sol, gpt-6-luna, gpt-6.1-sol, grok-4.7, deepseek-v4.1-flash, deepseek-v4-pro, kimi-k3, glm-5.3. Scoped to Responses inbound (Codex) like the Alibaba Token Plan precedent (tests/providers/alibaba-token-plan-wire-defaults.test.ts): a Chat or Anthropic client keeps the verified Chat wire with no translation hop. User-overridable: an explicit `modelAdapters` entry of `openai-chat` wins. +- Claude ids -> Anthropic Messages through the existing endpoint-bound prefix pin (`WIRE_ADAPTER_PIN_PREFIXES` in src/types/wire.ts, the Command Code mechanism): `tokenlab: { endpoint: "https://api.tokenlab.sh/v1", prefixes: { "claude-": "anthropic" } }`. The anthropic adapter already normalizes `/v1` to `/v1/messages`. The pin is bound to the canonical endpoint, so a retargeted TokenLab row is untouched. Every live `claude-*` TokenLab id is checked to declare anthropic_messages before the prefix is used. +- Everything else, including gemini-3.8-flash, stays on Chat. Gemini native is deferred until tested, as Vincent proposed. +- No delivery-policy header is added: the user's API-key default stays authoritative. + +Rejected: a provider-wide switch to Responses (sends Claude/Gemini to an endpoint they do not accept); separate provider entries (duplicates models by default); widening `MODEL_ADAPTER_OVERRIDE_ALLOWED` to anthropic (needs the #404 credential threat model; the prefix pin already exists and is endpoint-bound). + +Tests: resolveWireProtocolOverride / resolved policy for each class on the canonical endpoint, user modelAdapters override back to Chat for a Responses default, retargeted base URL keeps Chat, anthropic URL resolves to https://api.tokenlab.sh/v1/messages. + +## JEV decision backend — deferred to its own unit + +Decision after audit A1 (blockers 1-3): not in this release. A per-combo backend has to travel through `src/server/responses/core-combo.ts:536` (the call passes no combo settings), combo normalization and persistence (`src/combos/types.ts:379`, `src/server/management/combo-routes.ts:231`), and the GUI combo editor round trip (`gui/src/combo-workspace-data.ts:266`, `:460`), and credential, fixed URL and canonical outbound guard (`src/combos/jev.ts:568`, `:599`) must switch together. It also sends conversation-derived decision state to a new third party, which deserves its own review and a GUI screenshot. That does not meet this unit's "fits cleanly" bar. + +Follow-up unit sketch: combo fields `jevBackend: "typesafe" | "tokenlab"` (default typesafe) and `jevModel` (`jev-*`), fixed endpoint per backend (`https://api.typesafe.ai/v1/systemone`, `https://api.tokenlab.sh/v1/systemone`, body `{model, state, questions}` confirmed identical in docs.tokenlab.sh/api-reference/systemone/create-decision), credentials never shared between backends, allowlist/timeout/cancellation/fail-open unchanged. diff --git a/devlog/_plan/260930_gpt_6_1_sol_rollout/050_release.md b/devlog/_plan/260930_gpt_6_1_sol_rollout/050_release.md new file mode 100644 index 00000000000..0da14177b60 --- /dev/null +++ b/devlog/_plan/260930_gpt_6_1_sol_rollout/050_release.md @@ -0,0 +1,39 @@ +# 050 Release (wp4) + +1. Isolated verification in a /private/tmp checkout of the exact head with fresh OPENCODEX_HOME / CODEX_HOME: typecheck, focused tests, test:changed, structure:check, privacy:scan, skill:surface:check, file-size ratchet. +2. One PR to dev from codex/gpt-6-1-sol-rollout with the template; wait for exact-head required CI; maintainer merge under MAINTAINERS.md dev policy. +3. Release train per scripts/release.ts and the 2.70.0 precedent: dev version pre-move PR, preview promotion PR, main promotion PR; push-event Cross-platform CI + Service lifecycle green on each promotion SHA; release.yml dispatched with expected-sha for preview then stable. +4. Verify GitHub release assets, npm `latest` / `preview` dist-tags, latest.json. +5. Close the unit: move to devlog/_fin with an outcome doc. + +## 2.73.0 concrete steps (revalidated at wp4 P, 2026-09-30) + +Scope, on the owner's request to bundle the RT6 stabilization chat: 2.73.0 ships everything on `dev` +since v2.72.0 — the RT6 train (13 PRs through #6203, recorded in `devlog/_plan/260930_release_train_6/` +by that chat), #6261 (`540af24384`, Windows keyring test portability), and this PR. That chat was asked +not to run the release train itself. `dev` already reads 2.73.0 (#6243). + +1. PR from `codex/gpt-6-1-sol-rollout` (rebased on `540af24384`). Exact-head PR CI plus a + `lane=all` Cross-platform CI dispatch on the head (the pull_request event skips windows 1-9). + Before merge, an explicit read-only security review of the TokenLab wire change + (MAINTAINERS.md: credential handling): where the API key is sent, which header, endpoint binding + of the Claude pin, no new logging; verdict recorded on the PR. + `scripts/ci/assert-mergeable-review.sh --maintainer-integration `, decision comment, + `gh pr merge --admin --squash --match-head-commit ` -> C. Assert `git show C:package.json` = 2.73.0. +2. Pre-move: `gh workflow run dev-version-bump.yml --ref main -f intended-version=2.73.0 -f mode=pre-move`; + merge its PR after CI -> dev 2.74.0. +3. Preview: branch `codex/promote-preview-2.73.0` from C, `git merge -s ours origin/preview`, + `bun scripts/release-version-sources.ts sync 2.73.0-preview.20260930`; diff vs C = four version sources. + PR to preview, `--admin --merge --match-head-commit` after its PR checks. The maintainer-integration + exception covers only `dev`; promotion merges run on the owner's explicit release authorization + for this unit (2026-09-30, "exec and release" / "배포해줘"), as 2.70.0-2.72.0 did, and each promotion + PR records that authorization. +4. Main: branch `codex/promote-main-2.73.0` from C, `git merge -s ours origin/main`, `git diff --quiet C HEAD`. + PR to main, same merge. +5. Push-event Cross-platform CI + Service lifecycle success on each promotion SHA. +6. `gh workflow run release.yml --ref preview -f version=2.73.0-preview.20260930 -f tag=preview -f dry-run=false -f expected-sha=`, + then `gh workflow run release.yml --ref main -f version=2.73.0 -f tag=latest -f dry-run=false -f expected-sha=
` + (release.yml defaults to a dry run and rejects other refs). Never republish; resume with + `resume-after-npm-publish=true` after an npm-acknowledged failure. +7. Verify npm dist-tags, gitHead, GitHub releases (stable not prerelease, asset count as v2.72.0), latest.json. +8. Record PR moving this unit to `devlog/_fin/`. 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 6eeed5840aa..cdc60bf5320 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -190,7 +190,7 @@ du sélecteur à une route opencodex : ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 f4e17ef8221..91b74330991 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 @@ -118,7 +118,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Stratégie du pool ; least-loaded est réservé à Kiro. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -214,10 +214,29 @@ faire basculer la requête vers un autre compte de pool admissible. Ces transiti { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +La commande CLI suspend ou reprend un compte Anthropic OAuth par id ou alias unique (correspondance exacte, puis sans distinction de casse). Utilise `PUT /api/oauth/accounts/pause` avec `{ provider: "anthropic", accountId, paused }`, également utilisé par le tableau de bord. L’état `paused` est enregistré dans le compte et exposé par `GET /api/oauth/accounts`. La suspension s’applique même si le pool proactif est désactivé : le compte est exclu de la sélection, des affinités et des successeurs 429. Si tous les comptes sont suspendus, les requêtes renvoient 403 jusqu’à une reprise. Les requêtes déjà envoyées continuent ; les identifiants et l’état de santé sont conservés. La suspension survit au redémarrage et à une nouvelle connexion, et disparaît avec la suppression du compte. Les seuils individuels ne font pas partie de cette commande. + ### `ocx account clear [--json]` Efface la sélection manuelle du compte Codex sans résoudre d'id de compte, donc fonctionne même lorsqu'un compte s'appelle littéralement `auto`. Pools Codex uniquement ; les autres types de fournisseur n'ont pas de sélection automatique à rétablir. +### `ocx account clear-cooldown [--json]` + +Supprime un délai d'échec local au processus sans modifier les identifiants enregistrés. Utilisez +`openai` pour un compte du pool Codex ou `anthropic` pour un compte OAuth Anthropic ; les autres +fournisseurs sont refusés. Les deux formes acceptent un id de compte ou un alias unique, tandis que +`main` est réservé au pool Codex. + +```bash +ocx account clear-cooldown anthropic +``` + +La commande réussit même sans délai actif, avec `cleared: false` dans le JSON. La suppression d'un +délai Anthropic avance aussi la génération du compte afin qu'une ancienne sonde de quota ne puisse +pas rétablir l'état supprimé ni publier une éligibilité périmée. + ### `ocx account refresh [--json]` Pour le groupe de comptes Codex, utilisez `ocx account refresh openai [--json]`. Cette commande force l'actualisation des quotas de compte et @@ -233,7 +252,11 @@ renvoient 1 ; une sonde de quota en amont qui échoue ou expire produit plutôt ### `ocx account auto-switch > [--json]` -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. Un seuil générique n’oriente la sélection que si `pool.kernel` est activé avec `strategy: "fill-first"` ; le drapeau désactivé, sa sauvegarde n’active pas le basculement par seuil. Dans les deux cas, elle ne change 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 un seuil enregistré mais non appliqué, `inert: false` un seuil que le pool applique réellement. L’absence d’`inert` signale une capacité inconnue, qui ne produit jamais `enabled: true`. Les fournisseurs à clé API, Anthropic et les valeurs invalides sont refusés. +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. Un seuil générique n’oriente la sélection que si `pool.kernel` est activé avec `strategy: "fill-first"` ; le drapeau désactivé, sa sauvegarde n’active pas le basculement par seuil. Dans les deux cas, elle ne change 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 un seuil enregistré mais non appliqué, `inert: false` un seuil que le pool applique réellement. L’absence d’`inert` signale une capacité inconnue, qui ne produit jamais `enabled: true`. Les fournisseurs à clé API et les valeurs invalides sont refusés. + +### `ocx account auto-switch anthropic … --account ` + +Pour Anthropic OAuth, `ocx account auto-switch anthropic threshold 90 --account ` enregistre un entier de 0 à 100. `off --account ` vaut 0, `on --account ` vaut 80, `inherit --account ` rétablit l’héritage et `status --account ` lit sans écrire ; `--json` est disponible. La carte propose le même réglage. Une valeur absente/null hérite de `anthropicAccountPool.autoSwitchThreshold` (80 par défaut) ; 0 désactive seulement le basculement selon l’utilisation de ce compte. Le réglage survit au redémarrage et à la reconnexion, et disparaît avec le compte. Les priorités manuelle/affinité, les replis en cas de quota inconnu ou de comptes épuisés et les routes de modèles restent inchangés. Les seuils sont inactifs si le pool est désactivé ; pause et reprise après 429 restent actives. ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/fr/reference/configuration/agents.md b/docs-site/src/content/docs/fr/reference/configuration/agents.md index 7d846a6e1f2..495c22ffdb6 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -11,7 +11,7 @@ Les paramètres des agents déterminent la surface de collaboration Codex annonc | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` marque tous les modèles du catalogue comme compatibles v1 ; `v2` les marque tous comme compatibles v2. `default` rétablit les choix imposés en amont (Sol/Terra en v2, Luna en v1) et suit sinon l’indicateur natif `multi_agent_v2`. S’applique aux nouvelles sessions. | | `keepNativeChatGptOnV1?` | `boolean` | `false` | Lorsque `multiAgentMode` vaut `"v2"`, marque les lignes natives ChatGPT (Sol/Terra et les autres modèles du backend ChatGPT) comme v1. Les parents routés restent en v2. Utilisez cette option pour qu'un parent ChatGPT puisse encore lancer Grok ou Claude — les tâches enfants v2 natives sont chiffrées par le service en amont ([#92](https://github.com/lidge-jun/opencodex/issues/92)). Ignoré en `v1` et `default`. | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | Jusqu’à cinq identifiants de modèles natifs non qualifiés, qualifiés par un compte sous la forme `/`, ou routés sous la forme `provider/model`, affichés en tête du sélecteur de sous-agents. Le tableau de bord ne propose que les identifiants natifs non qualifiés et les identifiants routés ; lors de l’enregistrement, il omet les choix exacts qualifiés par un compte. Pour les définir, utilisez `ocx agent subagents set` ou modifiez la configuration. Après la [migration unique vers Astra](/reference/configuration/agents/#astra-roster-upgrade), une liste explicitement vide est conservée. | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | Jusqu’à cinq identifiants de modèles natifs non qualifiés, qualifiés par un compte sous la forme `/`, ou routés sous la forme `provider/model`, affichés en tête du sélecteur de sous-agents. Le tableau de bord ne propose que les identifiants natifs non qualifiés et les identifiants routés ; lors de l’enregistrement, il omet les choix exacts qualifiés par un compte. Pour les définir, utilisez `ocx agent subagents set` ou modifiez la configuration. Après la [migration unique vers Astra](/reference/configuration/agents/#astra-roster-upgrade), une liste explicitement vide est conservée. | | `injectionModel?` | `string` | — | Modèle de sous-agent natif ou routé privilégié dans les consignes de délégation v2 produites par le proxy. | | `injectionEffort?` | `string` | — | Niveau d’effort privilégié (de `low` à `ultra`), pertinent uniquement avec `injectionModel`. | | `injectionPrompt?` | `string` | — | Remplace le corps des consignes v2 intégrées. Accepte `{{model}}`, `{{effort}}`, `{{roster}}` et `{{fallback}}`. La présence d’un `injectionModel` suffit pour produire le prompt personnalisé. | diff --git a/docs-site/src/content/docs/fr/reference/configuration/server.md b/docs-site/src/content/docs/fr/reference/configuration/server.md index 6fe894b567f..8dec2432524 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/server.md +++ b/docs-site/src/content/docs/fr/reference/configuration/server.md @@ -14,7 +14,7 @@ exécute des fonctionnalités d'assistance autour des demandes du fournisseur. | `hostname?` | `string` | `"127.0.0.1"` | Adresse de liaison. Les liaisons hors bouclage nécessitent `OPENCODEX_API_AUTH_TOKEN`. | | `proxy?` | `string` | — | URL du proxy HTTP(S) ou SOCKS5 sortant (`socks5://host:port`) ou `${ENV_VAR}`. Les URL HTTP s’appliquent à `HTTP_PROXY` / `HTTPS_PROXY` si elles sont vides. Les URL SOCKS5 utilisent le tunnel SOCKS5 intégré et sont aussi exposées via `ALL_PROXY` (`ocx start --socks5`); `HTTP(S)_PROXY` héritées sont effacées dans ce processus. Le bouclage reste dans `NO_PROXY`. | | `emptyCompletionRetry?` | `boolean` | `false` | Active une nouvelle tentative Responses identique lorsqu’une réponse ne contient ni texte ni appel d’outil. Cette tentative peut être facturée. `OCX_EMPTY_COMPLETION_RETRY=0` la désactive sans modifier la configuration ; les combinaisons et les tours de compactage routés restent exclus. | -| `stallTimeoutSec?` | `number` | `300` (public) / désactivé (local) | Secondes sans progression utile en amont (Responses et Chat natif) avant la coupure du flux. Sans réglage, un amont **local** (loopback, privé ou nom `.local`/`.lan`) est désactivé par défaut et un amont public vaut 300 s ; une valeur positive s'applique aux deux (minimum 1 s) ; `0` désactive le watchdog partout. Les lectures de corps en attente de `/v1/responses/compact` partagent ce budget mais valent 300 s par défaut même pour un amont local ; une valeur explicite, y compris `0`, prime. | +| `stallTimeoutSec?` | `number` | `300` (public) / désactivé (local) | Secondes sans progression utile en amont (Responses et Chat natif) avant la coupure du flux. Sans réglage, un amont **local** (loopback, privé ou nom `.local`/`.lan`) est désactivé par défaut et un amont public vaut 300 s ; une valeur positive s'applique aux deux (minimum 1 s) ; `0` désactive partout le watchdog de silence. Pour Responses qui replie le SSE canonique ChatGPT en JSON non-streaming, un plafond total indépendant de 15 minutes subsiste même lorsque ce watchdog est désactivé. Les lectures de corps en attente de `/v1/responses/compact` partagent ce budget mais valent 300 s par défaut même pour un amont local ; une valeur explicite, y compris `0`, prime. | | `connectTimeoutMs?` | `number` | `200000` | Délai maximal par tentative pour DNS/TCP/TLS et les en-têtes finaux ; il prend fin avant la génération du corps. | | `shutdownTimeoutMs?` | `number` | `5000` | Délai de vidange gracieux avant l’annulation des tours actifs. | | `websockets?` | `boolean` | `false` | Annonce et autorise la route WebSocket Responses destinée aux clients. La valeur false maintient les clients sur HTTP/SSE ; elle ne désactive pas une optimisation WebSocket canonique admissible vers ChatGPT en amont. | diff --git a/docs-site/src/content/docs/fr/reference/management-api.md b/docs-site/src/content/docs/fr/reference/management-api.md index 6b0f60fc5e5..8f0e9a10215 100644 --- a/docs-site/src/content/docs/fr/reference/management-api.md +++ b/docs-site/src/content/docs/fr/reference/management-api.md @@ -283,6 +283,7 @@ Tant qu’une liste initiale fiable n’est pas disponible, les requêtes PUT va | `GET, PUT, PATCH /api/oauth/accounts/pool` | Lire ou mettre à jour la stratégie du pool OAuth Anthropic | 400 fournisseur non Anthropic ou stratégie invalide | | `POST /api/oauth/accounts/clear-cooldown` | Effacer le temps de recharge d'un compte OAuth | 400 invalide provider/account | | `PUT /api/oauth/accounts/alias` | Définir ou supprimer un alias de compte OAuth | 400 invalide provider/account/alias | +| `PUT /api/oauth/accounts/pause` | Suspendre/reprendre Anthropic ou un compte OAuth générique. Body `{ provider, accountId, paused }` ; la suspension du compte actif sélectionne un autre compte utilisable s’il existe. | 400 fournisseur non pris en charge ou body invalide ; 404 compte absent ; `oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` | Répertorier les clés de fournisseur masquées, en ajouter ou en activer une, ou en supprimer une | 400 saisie invalide ; 404 fournisseur ou clé manquante | | `PUT /api/providers/keys/active` | Sélectionnez la clé active d'un fournisseur | 400 saisie invalide ; 404 provider/key manquant | | `PUT /api/providers/keys/alias` | Définir ou supprimer un alias de clé de fournisseur | 400 saisie invalide ; 404 provider/key manquant | @@ -291,6 +292,10 @@ Tant qu’une liste initiale fiable n’est pas disponible, les requêtes PUT va Les réponses qui répertorient les identifiants sont délibérément masquées. Les jetons d'accès OAuth et les clés API complètes des fournisseurs ne sont pas renvoyés aux clients du tableau de bord. +#### Anthropic OAuth: `pause` / `resume` + +La commande CLI suspend ou reprend un compte Anthropic OAuth par id ou alias unique (correspondance exacte, puis sans distinction de casse). Utilise `PUT /api/oauth/accounts/pause` avec `{ provider: "anthropic", accountId, paused }`, également utilisé par le tableau de bord. L’état `paused` est enregistré dans le compte et exposé par `GET /api/oauth/accounts`. La suspension s’applique même si le pool proactif est désactivé : le compte est exclu de la sélection, des affinités et des successeurs 429. Si tous les comptes sont suspendus, les requêtes renvoient 403 jusqu’à une reprise. Les requêtes déjà envoyées continuent ; les identifiants et l’état de santé sont conservés. La suspension survit au redémarrage et à une nouvelle connexion, et disparaît avec la suppression du compte. Les seuils individuels ne font pas partie de cette commande. + ### Fournisseurs | Méthode et chemin | Objectif | Erreurs notables | @@ -407,3 +412,13 @@ L'accès HTTP direct est surtout utile aux intégrations qui exigent les contrat ## Sessions distantes et rotation des clés de données `POST /api/keys/rotate {id}` démarre un chevauchement de dix minutes et renvoie le nouveau secret une seule fois. `POST /api/keys/rotate/commit {id,rotationId}` valide; `DELETE /api/keys/rotate {id,rotationId}` annule. L'authentification de gestion est obligatoire et une clé de données ne suffit pas. `POST /api/session/logout` exige la `gui-session` courante, l'Origin correspondante et CSRF. Un jeton admin reçoit 403 et ne peut jamais créer une session de consentement. + +## Seuil d’utilisation par compte Anthropic + +`PUT /api/oauth/accounts/auto-switch` + +Anthropic OAuth uniquement. `{ provider: "anthropic", accountId, threshold }` : entier 0–100 ou null pour hériter ; champ absent invalide. Conservé au redémarrage, supprimé avec le compte. + +Le DTO inclut `autoSwitchThresholdOverride` (entier/null), `autoSwitchThreshold` (défaut du pool) et `effectiveAutoSwitchThreshold`. 0 désactive seulement le basculement selon l’utilisation ; pause et reprise après 429 restent actives. + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/fr/reference/proxy-formats.md b/docs-site/src/content/docs/fr/reference/proxy-formats.md index 4483284851b..3d47bfaeae4 100644 --- a/docs-site/src/content/docs/fr/reference/proxy-formats.md +++ b/docs-site/src/content/docs/fr/reference/proxy-formats.md @@ -75,6 +75,16 @@ Avec `stream: true`, la réponse est `text/event-stream`. Le pont émet des év Avec `stream: false` ou pas de `stream`, les mêmes événements d'adaptateur sont collectés dans une seule réponse JSON objet. Les deux formulaires préservent le modèle sélectionné, les éléments de sortie, l'état du terminal et l'utilisation. +La route canonique ChatGPT Codex n'accepte que SSE en amont ; seul l'appel amont utilise donc +`stream: true`. OpenCodex valide le flux terminal dans des limites bornées, puis le replie dans la +forme JSON demandée par le client sans modifier une valeur `store` explicite. Un échec de validation +renvoie une erreur plutôt qu'un JSON partiel avec HTTP 200. Les limites sont de 4 Mio par trame, +32 Mio pour le transcript et la source de reconstruction, 100 000 trames SSE et 10 000 éléments de +sortie reconstruits. `stallTimeoutSec` régit le premier octet du corps et les silences suivants. +Lorsqu'il vaut `0`, y compris par défaut pour un upstream local, il n'expire pas immédiatement : seul +le plafond indépendant de 15 minutes pour le tour mis en mémoire reste actif. Les clients streaming +restent inchangés. + Les trames SSE des réponses destinées au client sont limitées à 4 Mio par trame, mesuré en octets bruts avant la SSE délimiteur de bloc. Sur HTTP, une trame amont non terminée qui dépasse la limite échoue fermée avec un événement synthétique `response.failed` suivi de `data: [DONE]`. Sur les réponses WebSocket diff --git a/docs-site/src/content/docs/getting-started/installation.md b/docs-site/src/content/docs/getting-started/installation.md index 9206ec9f30c..cd334936180 100644 --- a/docs-site/src/content/docs/getting-started/installation.md +++ b/docs-site/src/content/docs/getting-started/installation.md @@ -66,7 +66,8 @@ are not required. Download the archive for your platform, extract it, and run: ./ocx start ``` -The extracted `gui/dist` directory must stay beside the binary so `GET /` can serve the dashboard. +The extracted `gui/dist` and `keyring` directories must stay beside the binary. The first serves +the dashboard; the second carries the platform-native OS credential-store binding. ### Release channels diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 4526e23c91d..1d3394163fe 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -255,7 +255,7 @@ every request, so you bind a picker row to an opencodex route instead: ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 2049192ce2d..90fba7f99d6 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -354,6 +354,10 @@ five seconds. A valid immediate `Retry-After: 0` remains an immediate upstream directive rather than being replaced by a configured cooldown. +For an Anthropic OAuth or Codex pool, a 429 tied to one account that the pool has cooled does +not cool the whole combo target. Other accounts behind that target remain available. A 429 with +no identified, cooled pool account still cools the target, as do provider-wide failures. + ### Last-resort targets A brief cooldown on a preferred target otherwise routes straight to whatever diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 5bbf4735c66..ee2191f61b5 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -562,6 +562,20 @@ The preset uses [Chat Completions](https://docs.tokenlab.sh/quickstart) and disc `GET /v1/models?category=chat`, keeping only entries that declare `tool-use` capability. Image, video, audio, embedding and decision models are excluded from this chat preset. +Each model then uses the request format TokenLab declares for it +(`tokenlab.accepted_request_formats` on `GET /v1/models/{model}`): + +| Models | Codex (Responses clients) | Chat clients | Claude Code (Anthropic clients) | +| --- | --- | --- | --- | +| `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-sol`, `gpt-6-luna`, `grok-4.7`, `deepseek-v4.1-flash`, `deepseek-v4-pro`, `kimi-k3`, `glm-5.3` | [Responses](https://docs.tokenlab.sh/api-reference/responses/create-response) | Chat Completions | Chat Completions | +| `claude-*` | [Messages](https://docs.tokenlab.sh/api-reference/messages/create-message) | Messages | Messages | +| Every other model, including `gemini-3.8-flash` | Chat Completions | Chat Completions | Chat Completions | + +To keep a model on Chat Completions, add it to the provider's `modelAdapters`, for example +`"modelAdapters": { "gpt-6.1-sol": "openai-chat" }`. The Claude routing applies only while the +provider points at `https://api.tokenlab.sh/v1`. OpenCodex sends no delivery-policy header, so +your API key's own delivery policy decides how TokenLab serves each request. + The [model catalog](https://docs.tokenlab.sh/api-reference/models/list-models) is public without a key, but a supplied key is validated and scopes results to its model permissions and delivery policy. Use a valid key with a funded workspace for inference. `gpt-5.6-terra` is the seeded diff --git a/docs-site/src/content/docs/guides/sub-agent-surface.md b/docs-site/src/content/docs/guides/sub-agent-surface.md index f4abe08f22d..fc79bb1fc9e 100644 --- a/docs-site/src/content/docs/guides/sub-agent-surface.md +++ b/docs-site/src/content/docs/guides/sub-agent-surface.md @@ -34,6 +34,23 @@ routed one arrives encrypted and fails. The dashboard asks before either, and li [Why v1 is the default](/guides/subagent-v1-default/). ::: +## Model switches and side-chat compaction + +Codex can compact inherited history before the first turn of a side chat or after a model switch. +One trigger compares the previous and destination models' `comp_hash` compatibility markers: +when both are present and differ, Codex compacts even if the history fits the destination's context. + +OpenCodex represents unknown compatibility for routed models as `comp_hash: null`, instead of +inventing an `"opencodex"` marker or copying one from a native template. Catalog rebuilds also clear +those old markers on OpenCodex rows retained during a provider discovery outage. Native models and +explicit Codex-forward aliases keep their upstream markers, so genuine native incompatibility +checks still apply. This behavior is independent of the v1/v2 sub-agent surface and the model +configured to perform compaction. + +Normal context and token-limit compaction still applies. This change removes the synthetic +hash mismatch; it does not select which visible messages a side chat inherits or restore original +messages from an already compacted context. Provider content support remains a separate constraint. + ## External task input Codex can deliver a task's initial input or follow-up in a result-shaped envelope 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 6a074ece037..b657396615f 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -161,7 +161,7 @@ Anthropic モデル ID なので、代わりにピッカーの行を opencodex ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 479fac0f0da..5f46501bbb9 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 @@ -92,7 +92,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Pool placement strategy; omit the value to read it. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -159,10 +159,29 @@ Codex pool selection applies to the next request after clearing existing affinit { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +CLI コマンドは Anthropic OAuth アカウントを id または一意の別名で一時停止・再開します。別名は完全一致を優先し、次に大文字と小文字を区別せず照合します。ダッシュボードと同じ `PUT /api/oauth/accounts/pause` に `{ provider: "anthropic", accountId, paused }` を送信します。`paused` はアカウントに保存され、`GET /api/oauth/accounts` にも表示されます。プロアクティブなプールが無効でも、停止中のアカウントは選択、セッションの紐付け、429 の切り替え候補から除外されます。全アカウントが停止中なら、再開するまでリクエストは 403 を返します。送信済みのリクエストは継続し、認証情報と健全性の状態は保持されます。再起動や再ログインでも停止は維持され、アカウント削除時に消えます。アカウント別のしきい値はこの操作に含まれません。 + ### `ocx account clear [--json]` アカウント id を解決せずに Codex アカウントの手動選択を解除するため、`auto` という id のアカウントが存在しても機能します。Codex プール専用です。他のプロバイダー種別には復元する自動選択がありません。 +### `ocx account clear-cooldown [--json]` + +保存済み認証情報を変更せず、プロセスローカルな障害 cooldown を解除します。Codex Pool +アカウントには `openai`、Anthropic OAuth アカウントには `anthropic` を使い、その他の +provider は拒否されます。どちらもアカウント id または一意の alias を受け付けますが、 +`main` は Codex Pool 専用です。 + +```bash +ocx account clear-cooldown anthropic +``` + +有効な cooldown がなくても成功し、JSON では `cleared: false` になります。Anthropic の +cooldown を解除するとアカウント generation も進むため、古い quota probe が解除済み状態を +復元したり、古い quota ベースの eligibility を公開したりできません。 + ### `ocx account refresh [--json]` Codex プールの場合は、`ocx account refresh openai [--json]` を使用します。アカウント クォータを強制的に更新し、利用可能な週次/月次のパーセンテージとリセット時間を出力します。不足しているクォータ データは、0% ではなく不明として報告されます。その JSON エンベロープは `{ accounts: AccountRow[] }` で、Codex の各行に `quota` があります。 @@ -171,7 +190,11 @@ OAuth プロバイダーと API キー プロバイダーの場合、これに ### `ocx account auto-switch > [--json]` -`openai` Codex プールのしきい値を制御するか、汎用 OAuth プールのしきい値を保存します。`on` は 80%、`off` は 0%、`threshold ` は 0–100 を保存します。汎用プールのしきい値は `pool.kernel` が有効で `strategy: "fill-first"` の場合にのみ選択へ反映されます。フラグが無効なら、保存してもしきい値による切り替えは有効になりません。いずれの場合もプロバイダーの有効化設定と 429 エラー時のローテーションは変更されません。汎用プールの照会と変更の結果はサーバーの確認値を使用します。汎用プールの `poolEnabled` は保存された設定で、`null` は未指定です。継承後の実効状態ではありません。`inert: true` は保存済みで未適用、`inert: false` はプールが適用中であることを示します。`inert` が無い場合は機能が不明であり、その場合も `enabled: true` とは表示しません。API キープロバイダー、Anthropic、不正な値は拒否されます。 +`openai` Codex プールのしきい値を制御するか、汎用 OAuth プールのしきい値を保存します。`on` は 80%、`off` は 0%、`threshold ` は 0–100 を保存します。汎用プールのしきい値は `pool.kernel` が有効で `strategy: "fill-first"` の場合にのみ選択へ反映されます。フラグが無効なら、保存してもしきい値による切り替えは有効になりません。いずれの場合もプロバイダーの有効化設定と 429 エラー時のローテーションは変更されません。汎用プールの照会と変更の結果はサーバーの確認値を使用します。汎用プールの `poolEnabled` は保存された設定で、`null` は未指定です。継承後の実効状態ではありません。`inert: true` は保存済みで未適用、`inert: false` はプールが適用中であることを示します。`inert` が無い場合は機能が不明であり、その場合も `enabled: true` とは表示しません。API キープロバイダー、不正な値は拒否されます。 + +### `ocx account auto-switch anthropic … --account ` + +Anthropic OAuth では `ocx account auto-switch anthropic threshold 90 --account ` でアカウント別の整数 0–100 を保存します。`off --account ` は 0、`on --account ` は 80、`inherit --account ` は継承へ戻し、`status --account ` は読み取り専用です。`--json` も使えます。カードにも同じカスタム設定があります。未設定/null は `anthropicAccountPool.autoSwitchThreshold`(既定 80)を継承し、0 はそのアカウントの使用量による切り替えのみ無効にします。再起動・再ログインで保持され、削除時に消えます。手動選択、affinity、使用量不明・全候補消耗時のフォールバック、モデルルート制限は維持されます。プール無効時は適用されず、一時停止と 429 復旧は引き続き有効です。 ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/ja/reference/configuration/agents.md b/docs-site/src/content/docs/ja/reference/configuration/agents.md index 52a4f13bdea..19644ea5da2 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -10,7 +10,7 @@ description: マルチエージェント サーフェス、委任ガイダンス |フィールド |タイプ |デフォルト |意味 | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` はすべてのカタログ モデルを v1 としてスタンプします。 `v2` はすべてのモデルを v2 としてスタンプします。 `default` はアップストリーム ピン (Sol/Terra v2、Luna v1) を復元し、それ以外の場合はネイティブの `multi_agent_v2` フラグに従います。新しいセッションに適用されます。 | -| `subagentModels?` | `string[]` | `gpt-6-astra`、`gpt-6-sol`、`gpt-6-luna` |最大 5 つの bare native id、account-qualified `/` id、または routed `provider/model` id をサブエージェント ピッカーで優先表示します。Subagents ページで選べるのは bare native id と routed id だけで、保存時には exact account-qualified の選択が除外されます。exact の選択には `ocx agent subagents set` を使用するか、設定を直接編集してください。[Astra への一度限りの移行](/reference/configuration/agents/#astra-roster-upgrade)後は、明示的な空リストも保持されます。 | +| `subagentModels?` | `string[]` | `gpt-6-astra`、`gpt-6.1-sol`、`gpt-6-luna` |最大 5 つの bare native id、account-qualified `/` id、または routed `provider/model` id をサブエージェント ピッカーで優先表示します。Subagents ページで選べるのは bare native id と routed id だけで、保存時には exact account-qualified の選択が除外されます。exact の選択には `ocx agent subagents set` を使用するか、設定を直接編集してください。[Astra への一度限りの移行](/reference/configuration/agents/#astra-roster-upgrade)後は、明示的な空リストも保持されます。 | | `injectionModel?` | `string` | — |プロキシ作成の v2 委任ガイダンスで使用される、優先されるネイティブまたはルーティングされたサブエージェント モデル。 | | `injectionEffort?` | `string` | — |優先努力 (`low` ~ `ultra`)。`injectionModel` でのみ意味があります。 | | `injectionPrompt?` | `string` | — | 組み込みの v2 ガイダンス本文を置き換えます。`{{model}}`、`{{effort}}`、`{{roster}}`、`{{fallback}}`をサポートします。`injectionModel` が設定されていればカスタムプロンプトが生成されます。 | diff --git a/docs-site/src/content/docs/ja/reference/configuration/server.md b/docs-site/src/content/docs/ja/reference/configuration/server.md index 7eeac7e4776..c11d7af21c0 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/server.md +++ b/docs-site/src/content/docs/ja/reference/configuration/server.md @@ -14,7 +14,7 @@ description: リスナー、リモート アクセス、アドミッション | `proxy?` | `string` | — |送信 HTTP(S) または SOCKS5 プロキシ URL(`socks5://host:port`)または `${ENV_VAR}`。HTTP URL は未設定時のみ `HTTP_PROXY` / `HTTPS_PROXY` に適用されます。SOCKS5 URL は組み込みの SOCKS5 トンネルを使用し、`ALL_PROXY` にも適用されます(`ocx start --socks5`)。このプロセスで継承した `HTTP(S)_PROXY` はクリアされます。ループバックは `NO_PROXY` に残ります。 | | `emptyCompletionRetry?` | `boolean` | `false` | テキストもツール呼び出しもない Responses ターンを、ターミナルイベント前にストリームが終了した場合も含め、同一リクエストで 1 回再試行するよう明示的に有効化します。再試行は課金対象になる場合があります。`OCX_EMPTY_COMPLETION_RETRY=0` で設定を変更せず無効化できます。combo と routed-compaction turn は対象外です。 | | `dropCodexSafetyBuffering?` | `boolean` | `false` | Codex Responses パススルーから Codex の safety-buffering ヒントを除去します。対象は `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 応答ヘッダー、`safety_buffering` 型の `response.metadata` SSE イベント、およびその他の SSE イベントにある `safety_buffering` フィールドです。Codex TUI はこれらを、既定の操作でセッションをより弱いモデルに切り替える「より高速なモデルで再試行」プロンプトとして表示します。その他の `x-codex-*` ヘッダーと SSE イベントの内容は、そのフィールドの除去を除いて変更せずに転送されます。既定ではオフです。 | -| `stallTimeoutSec?` | `number` | `300`(public)/ 無効(local) | ストリームが遮断されるまでの、有効な上流進捗がない秒数(Responses とネイティブ Chat)。未設定では**ローカル**上流(loopback・プライベート・`.local`/`.lan` 名)は無効が既定、public 上流は 300 秒。正の値は両方に適用(最小 1 秒)、`0` で watchdog を全面無効化。`/v1/responses/compact` の保留ボディ読み取りもこの予算を共有するが、ローカル上流でも既定は 300 秒。明示値(`0` を含む)が優先される。 | +| `stallTimeoutSec?` | `number` | `300`(public)/ 無効(local) | ストリームが遮断されるまでの、有効な上流進捗がない秒数(Responses とネイティブ Chat)。未設定では**ローカル**上流(loopback・プライベート・`.local`/`.lan` 名)は無効が既定、public 上流は 300 秒。正の値は両方に適用(最小 1 秒)、`0` で無通信 watchdog を全面無効化。canonical ChatGPT SSE を非ストリーミング JSON にまとめる Responses リクエストには、watchdog が無効でも独立した 15 分の全体上限が残る。`/v1/responses/compact` の保留ボディ読み取りもこの予算を共有するが、ローカル上流でも既定は 300 秒。明示値(`0` を含む)が優先される。 | | `connectTimeoutMs?` | `number` | `200000` |試行ごとの DNS/TCP/TLS/最終ヘッダーの期限。本体が生成される前に終了します。 | | `shutdownTimeoutMs?` | `number` | `5000` |アクティブなターンが中止される前の正常な排出期限。 | | `websockets?` | `boolean` | `false` | クライアント向け Responses WebSocket パスを広告して許可します。false の場合クライアントは HTTP/SSE を使いますが、対象となる canonical ChatGPT upstream WS 最適化は無効にしません。 | diff --git a/docs-site/src/content/docs/ja/reference/management-api.md b/docs-site/src/content/docs/ja/reference/management-api.md index 9821c5d358f..cfdb48f8e77 100644 --- a/docs-site/src/content/docs/ja/reference/management-api.md +++ b/docs-site/src/content/docs/ja/reference/management-api.md @@ -245,6 +245,7 @@ Aside プロファイルの変更はこの場合でも一つだけ保存しま | `GET, PUT, PATCH /api/oauth/accounts/pool` | Anthropic OAuth プール ポリシーの読み取りまたは更新 | 400 非 Anthropic プロバイダーまたは無効なポリシー | | `POST /api/oauth/accounts/clear-cooldown` | 1 つの OAuth アカウントのランタイム クールダウンをクリアする | 400 無効なプロバイダー/アカウント | | `PUT /api/oauth/accounts/alias` | OAuth アカウント エイリアスを設定またはクリアする | 400 無効なプロバイダー/アカウント/エイリアス | +| `PUT /api/oauth/accounts/pause` | Anthropic または汎用 OAuth アカウントを一時停止・再開。Body `{ provider, accountId, paused }`。アクティブなアカウントを停止すると、利用可能な別のアカウントがあれば切り替えます。 | 400 未対応のプロバイダーまたは無効な body;404 アカウントなし;`oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` |マスクされたプロバイダー キーを一覧表示し、1 つを追加/アクティブ化するか、1 つを削除します。 400 無効な入力。 404 プロバイダー/キーがありません | | `PUT /api/providers/keys/active` |プロバイダーのアクティブなキーを選択します | 400 無効な入力。 404 プロバイダー/キーがありません | | `PUT /api/providers/keys/alias` |プロバイダー キー エイリアスを設定またはクリアする | 400 無効な入力。 404 プロバイダー/キーがありません | @@ -252,6 +253,10 @@ Aside プロファイルの変更はこの場合でも一つだけ保存しま 資格情報リストの応答は意図的にマスクされます。 OAuth アクセス トークンと完全なプロバイダー API キーはダッシュボード クライアントに返されません。 +#### Anthropic OAuth: `pause` / `resume` + +CLI コマンドは Anthropic OAuth アカウントを id または一意の別名で一時停止・再開します。別名は完全一致を優先し、次に大文字と小文字を区別せず照合します。ダッシュボードと同じ `PUT /api/oauth/accounts/pause` に `{ provider: "anthropic", accountId, paused }` を送信します。`paused` はアカウントに保存され、`GET /api/oauth/accounts` にも表示されます。プロアクティブなプールが無効でも、停止中のアカウントは選択、セッションの紐付け、429 の切り替え候補から除外されます。全アカウントが停止中なら、再開するまでリクエストは 403 を返します。送信済みのリクエストは継続し、認証情報と健全性の状態は保持されます。再起動や再ログインでも停止は維持され、アカウント削除時に消えます。アカウント別のしきい値はこの操作に含まれません。 + ### プロバイダー |メソッドとパス |目的 |注目すべきエラー | @@ -349,3 +354,13 @@ account の selector binding は残るため、欠落中の exact route は fail ## リモートセッションとデータキー更新 `POST /api/keys/rotate {id}` は10分間の移行を開始し、新しい秘密値を一度だけ返します。`POST /api/keys/rotate/commit {id,rotationId}` で確定し、`DELETE /api/keys/rotate {id,rotationId}` で中止します。管理認証が必須で、データキーからは呼べません。`POST /api/session/logout` には現在の `gui-session`、一致する Origin、CSRF が必要です。管理トークンは 403 となり、同意セッションを作成できません。 + +## Anthropic アカウント使用量しきい値 + +`PUT /api/oauth/accounts/auto-switch` + +Anthropic OAuth のみ。`{ provider: "anthropic", accountId, threshold }`: 整数 0–100、null は継承、欠落はエラー。再起動後も保持され、アカウント削除時に消えます。 + +DTO は `autoSwitchThresholdOverride`(整数/null)、`autoSwitchThreshold`(プール既定値)、`effectiveAutoSwitchThreshold` を含みます。0 は使用量による切り替えのみ無効にし、一時停止と 429 復旧は維持します。 + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/ja/reference/proxy-formats.md b/docs-site/src/content/docs/ja/reference/proxy-formats.md index 7c5cbbb4277..db698fd90cb 100644 --- a/docs-site/src/content/docs/ja/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ja/reference/proxy-formats.md @@ -63,6 +63,8 @@ provider events → internal adapter events → client dialect `stream: false` を指定するか、`stream` を指定しないと、同じアダプター イベントが 1 つの Responses JSON オブジェクトに収集されます。どちらの形式でも、選択したモデル、出力項目、端末の状態、使用状況が保存されます。 +canonical ChatGPT Codex ルートではアップストリームが SSE のみを受け付けるため、アップストリームへのリクエストだけを `stream: true` にします。OpenCodex は終端ストリームを制限内で検証し、クライアントが要求した JSON 形式へまとめます。明示された `store` は変更せず、検証に失敗した場合は不完全な JSON を HTTP 200 で返さずエラーにします。上限は 1 フレーム 4 MiB、transcript と再構築入力がそれぞれ 32 MiB、SSE フレーム 100,000 件、再構築される output item 10,000 件です。`stallTimeoutSec` は最初の body byte と以後の無通信時間の両方に適用されます。値が `0`、またはローカル upstream の既定値として無効な場合も即時には失効せず、独立した 15 分の全体上限だけが残ります。ストリーミング クライアントの動作は変わりません。 + クライアント向け Responses SSE フレームは、SSE ブロック区切りの前の生バイトで測って 1 フレームあたり 4 MiB に制限されます。HTTP では、区切りなしでこの上限を超えたアップストリーム フレームは、合成 `response.failed` イベントと続く `data: [DONE]` でフェイルクローズします。Responses WebSocket ブリッジでは、同じ条件で 502 `websocket_protocol_error` を送信し、アップストリーム リーダーをキャンセルします。完全な Responses 終端フレームがすでに到着している場合はそれが優先され、その後のサイズ超過または不正なバイトは、完了したターンをトランスポート障害に置き換えず破棄されます。 :::note 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 a6cff146937..d45ed65f471 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -184,7 +184,7 @@ opencodex 라우트에 묶어서 씁니다. ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 5f94fd837b1..7f8c672ca7f 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 @@ -187,7 +187,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Pool placement strategy; omit the value to read it. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -254,10 +254,28 @@ Codex pool selection applies to the next request after clearing existing affinit { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +CLI 명령은 Anthropic OAuth 계정을 id 또는 유일한 alias로 일시 정지하거나 재개합니다. alias는 정확히 일치하는 값을 먼저 찾고, 없으면 대소문자를 구분하지 않고 찾습니다. 대시보드와 같은 `PUT /api/oauth/accounts/pause`에 `{ provider: "anthropic", accountId, paused }`를 보냅니다. 계정에 저장되는 `paused` 상태는 `GET /api/oauth/accounts`에도 표시됩니다. 사전 계정 전환 풀이 꺼져 있어도 정지된 계정은 선택, 세션 바인딩, 429 대체 후보에서 제외됩니다. 모든 계정이 정지되면 하나를 재개할 때까지 요청은 403을 반환합니다. 이미 전송한 요청은 유지하며 자격 증명과 건강 상태를 지우지 않습니다. 재시작·재로그인 후에도 정지는 유지되고, 계정을 삭제하면 함께 제거됩니다. 계정별 전환 임계값은 이 기능에 포함되지 않습니다. + ### `ocx account clear [--json]` 계정 id를 해석하지 않고 Codex 계정의 수동 선택을 지우므로 `auto`라는 id의 계정이 있어도 동작합니다. Codex 풀 전용이며 다른 공급자 유형에는 복원할 자동 선택이 없습니다. +### `ocx account clear-cooldown [--json]` + +저장된 자격 증명은 바꾸지 않고 프로세스 로컬 실패 cooldown을 해제합니다. Codex Pool 계정에는 +`openai`, Anthropic OAuth 계정에는 `anthropic`을 사용하며 다른 provider는 거부됩니다. 두 경로 +모두 계정 id 또는 고유 alias를 받지만 `main`은 Codex Pool에서만 사용할 수 있습니다. + +```bash +ocx account clear-cooldown anthropic +``` + +활성 cooldown이 없어도 명령은 성공하고 JSON에는 `cleared: false`가 표시됩니다. Anthropic +cooldown을 해제하면 계정 generation도 전진하므로 이전 quota probe가 해제된 상태를 되살리거나 +오래된 quota 기반 eligibility를 게시할 수 없습니다. + ### `ocx account refresh [--json]` Codex 풀에는 `ocx account refresh openai [--json]`를 사용합니다. 계정 할당량을 강제로 새로 고치고 사용 가능 주간/월간 비율과 재설정 시간을 출력합니다. 할당량 데이터가 없으면 0%가 아니라 알 수 없음으로 보고합니다. JSON 봉투는 `{ accounts: AccountRow[] }`이며, Codex 행마다 `quota`가 붙습니다. @@ -266,7 +284,11 @@ OAuth 및 API 키 제공자에는 제공자의 할당량 보고 엔드포인트 ### `ocx account auto-switch > [--json]` -`openai` Codex 풀의 임계값을 제어하거나 일반 OAuth 풀의 임계값을 저장합니다. `on`은 80%, `off`는 0%, `threshold `은 0–100을 저장합니다. 일반 풀의 임계값은 `pool.kernel`이 켜져 있고 `strategy: "fill-first"`일 때만 선택에 반영됩니다. 플래그가 꺼져 있으면 저장해도 임계값 기반 전환이 켜지지 않습니다. 어느 쪽이든 제공자 활성화 설정은 바뀌지 않고, 429 오류에 따른 회전도 비활성화되지 않습니다. 일반 풀의 조회와 변경 결과는 서버가 확인한 값을 사용합니다. 일반 풀의 `poolEnabled`는 저장된 제공자별 설정이며 `null`은 미지정입니다. 전역 설정을 상속한 실제 상태를 뜻하지 않습니다. `inert: true`는 임계값이 저장만 되고 적용되지 않는 상태, `inert: false`는 풀이 실제로 적용하고 있는 상태를 뜻합니다. `inert`가 아예 없으면 기능 지원을 알 수 없는 경우이며, 이때도 `enabled: true`로 표시하지 않습니다. API 키 제공자, Anthropic 및 잘못된 값은 거부합니다. +`openai` Codex 풀의 임계값을 제어하거나 일반 OAuth 풀의 임계값을 저장합니다. `on`은 80%, `off`는 0%, `threshold `은 0–100을 저장합니다. 일반 풀의 임계값은 `pool.kernel`이 켜져 있고 `strategy: "fill-first"`일 때만 선택에 반영됩니다. 플래그가 꺼져 있으면 저장해도 임계값 기반 전환이 켜지지 않습니다. 어느 쪽이든 제공자 활성화 설정은 바뀌지 않고, 429 오류에 따른 회전도 비활성화되지 않습니다. 일반 풀의 조회와 변경 결과는 서버가 확인한 값을 사용합니다. 일반 풀의 `poolEnabled`는 저장된 제공자별 설정이며 `null`은 미지정입니다. 전역 설정을 상속한 실제 상태를 뜻하지 않습니다. `inert: true`는 임계값이 저장만 되고 적용되지 않는 상태, `inert: false`는 풀이 실제로 적용하고 있는 상태를 뜻합니다. `inert`가 아예 없으면 기능 지원을 알 수 없는 경우이며, 이때도 `enabled: true`로 표시하지 않습니다. API 키 제공자 및 잘못된 값은 거부합니다. + +### `ocx account auto-switch anthropic … --account ` + +Anthropic OAuth는 `ocx account auto-switch anthropic threshold 90 --account `로 계정별 정수 0–100을 저장합니다. `off --account `는 0, `on --account `는 80, `inherit --account `는 상속 복원, `status --account `는 조회입니다. `--json`도 지원합니다. 계정 카드에서 같은 사용자 지정 임계값을 편집합니다. 미설정/null은 풀 기본값 `anthropicAccountPool.autoSwitchThreshold`(기본 80)를 상속하고, 0은 해당 계정의 사용량 기반 전환만 끕니다. 재시작·재로그인 후에도 유지되고 계정 삭제 시 제거됩니다. 수동 선택, 세션 affinity, 사용량 미확인·전체 소진 시 fallback, 모델 경로 제한은 유지됩니다. 풀이 꺼져 있으면 임계값은 적용되지 않으며 pause와 429 복구는 계속 동작합니다. ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/ko/reference/configuration/agents.md b/docs-site/src/content/docs/ko/reference/configuration/agents.md index 768af611d5a..60a0af24f67 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -10,7 +10,7 @@ description: 멀티 에이전트 표면, 위임 안내, 선호 모델, 대체 | 필드 | 형식 | 기본값 | 의미 | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1`은 카탈로그의 모든 모델에 v1을 표시하고, `v2`는 모든 모델에 v2를 표시합니다. `default`는 상위 고정값(Sol/Terra는 v2, Luna는 v1)을 복원하고, 그 외에는 네이티브 `multi_agent_v2` 플래그를 따릅니다. 새 세션에 적용됩니다. | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | 최대 다섯 개의 bare native id, account-qualified `/` id 또는 routed `provider/model` id를 서브에이전트 선택기에서 우선 표시합니다. Subagents 페이지는 bare native와 routed id만 제공하며 저장할 때 exact account-qualified 선택을 제외합니다. exact 선택은 `ocx agent subagents set`을 사용하거나 설정을 직접 편집하세요. [Astra 최초 업그레이드](/reference/configuration/agents/#astra-roster-upgrade) 이후에는 빈 목록도 그대로 보존됩니다. | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | 최대 다섯 개의 bare native id, account-qualified `/` id 또는 routed `provider/model` id를 서브에이전트 선택기에서 우선 표시합니다. Subagents 페이지는 bare native와 routed id만 제공하며 저장할 때 exact account-qualified 선택을 제외합니다. exact 선택은 `ocx agent subagents set`을 사용하거나 설정을 직접 편집하세요. [Astra 최초 업그레이드](/reference/configuration/agents/#astra-roster-upgrade) 이후에는 빈 목록도 그대로 보존됩니다. | | `injectionModel?` | `string` | — | 프록시가 작성한 v2 위임 안내에서 사용하는 선호 네이티브 또는 라우팅된 서브에이전트 모델입니다. | | `injectionEffort?` | `string` | — | 선호 노력(`low`부터 `ultra`까지)입니다. `injectionModel`이 있을 때만 의미가 있습니다. | | `injectionPrompt?` | `string` | — | 내장 v2 안내 본문을 대체합니다. `{{model}}`, `{{effort}}`, `{{roster}}`, `{{fallback}}`를 지원합니다. `injectionModel`만 설정되어 있어도 사용자 정의 프롬프트가 발동합니다. | diff --git a/docs-site/src/content/docs/ko/reference/configuration/server.md b/docs-site/src/content/docs/ko/reference/configuration/server.md index e166d1e50ba..ee24fd4f16e 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/server.md +++ b/docs-site/src/content/docs/ko/reference/configuration/server.md @@ -14,7 +14,7 @@ description: 리스너, 원격 접근, admission 키, 타임아웃, 저장소, | `proxy?` | `string` | — | 송신용 HTTP(S) 또는 SOCKS5 프록시 URL(`socks5://host:port`) 또는 `${ENV_VAR}`입니다. HTTP URL은 해당 변수가 비어 있을 때 `HTTP_PROXY` / `HTTPS_PROXY`에 적용됩니다. SOCKS5 URL은 내장 SOCKS5 터널을 사용하고 `ALL_PROXY`에도 적용되며(`ocx start --socks5`), 이 프로세스에서 상속된 `HTTP(S)_PROXY`를 지웁니다. 루프백은 `NO_PROXY`에 그대로 남습니다. | | `emptyCompletionRetry?` | `boolean` | `false` | 텍스트나 도구 호출이 없는 Responses 턴을, 터미널 이벤트 전에 스트림이 종료된 경우를 포함해 동일한 요청으로 한 번 재시도하도록 선택합니다. 재시도에는 비용이 발생할 수 있습니다. `OCX_EMPTY_COMPLETION_RETRY=0`은 설정을 바꾸지 않고 비활성화하며, combo 및 routed-compaction turn은 제외됩니다. | | `dropCodexSafetyBuffering?` | `boolean` | `false` | Canonical Codex Responses 응답의 선택적 safety-buffering 헤더 두 개와 SSE 힌트를 제거합니다. 공급자의 안전 정책이나 거절 응답은 바뀌지 않습니다. Native WS 메타데이터와 compact는 제외됩니다. | -| `stallTimeoutSec?` | `number` | `300`(public) / 비활성(local) | 스트림이 끊기기까지 유효한 업스트림 진행이 없는 시간(초, Responses 및 네이티브 Chat). 미설정 시 **로컬** 업스트림(loopback, private, `.local`/`.lan` 이름)은 비활성이 기본이고 공개 업스트림은 300초. 양수 값은 둘 다에 적용(최소 1초), `0`은 전면 비활성. `/v1/responses/compact`의 보류 바디 읽기도 이 예산을 공유하지만 로컬 업스트림에서도 기본은 300초. 명시 값(`0` 포함)이 우선한다. | +| `stallTimeoutSec?` | `number` | `300`(public) / 비활성(local) | 스트림이 끊기기까지 유효한 업스트림 진행이 없는 시간(초, Responses 및 네이티브 Chat). 미설정 시 **로컬** 업스트림(loopback, private, `.local`/`.lan` 이름)은 비활성이 기본이고 공개 업스트림은 300초. 양수 값은 둘 다에 적용(최소 1초), `0`은 무응답 watchdog을 전면 비활성화한다. canonical ChatGPT SSE를 비스트리밍 JSON으로 접는 Responses 요청에는 이 watchdog이 꺼져도 별도의 15분 전체 상한이 남는다. `/v1/responses/compact`의 보류 바디 읽기도 이 예산을 공유하지만 로컬 업스트림에서도 기본은 300초. 명시 값(`0` 포함)이 우선한다. | | `connectTimeoutMs?` | `number` | `200000` | 시도별 DNS/TCP/TLS/최종 헤더 기한입니다. 본문 생성 전에 끝납니다. | | `shutdownTimeoutMs?` | `number` | `5000` | 진행 중인 turn을 중단하기 전에 허용하는 정상 종료 드레인 기한입니다. | | `websockets?` | `boolean` | `false` | 클라이언트용 Responses WebSocket 경로를 광고하고 허용합니다. `false`이면 클라이언트는 HTTP/SSE를 사용하며, 적격 canonical ChatGPT 업스트림 WS 최적화는 비활성화하지 않습니다. | diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index cd428d4ef35..a38309cb2cc 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -258,6 +258,7 @@ Aside 프로필 변경은 이때도 한 가지를 저장합니다. 확인을 보 | `GET, PUT, PATCH /api/oauth/accounts/pool` | Anthropic과 일반 OAuth provider의 기존 pool policy입니다. `/api/pool/settings`로 대체되었고 기존 클라이언트를 위해 유지합니다 | 400 codex 또는 API 키 provider, 잘못된 policy | | `POST /api/oauth/accounts/clear-cooldown` | OAuth 계정 하나의 런타임 cooldown을 지웁니다 | 400 잘못된 provider/account | | `PUT /api/oauth/accounts/alias` | OAuth 계정 alias를 설정하거나 지웁니다 | 400 잘못된 provider/account/alias | +| `PUT /api/oauth/accounts/pause` | Anthropic 또는 일반 OAuth 계정을 정지·재개합니다. Body `{ provider, accountId, paused }`. 활성 계정을 정지하면 사용 가능한 다른 계정이 있을 때 전환합니다. | 400 지원하지 않는 provider 또는 잘못된 body; 404 계정 없음; `oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` | 마스킹된 provider key를 나열, 추가/활성화, 또는 제거합니다 | 400 잘못된 입력; 404 provider/key 없음 | | `PUT /api/providers/keys/active` | provider의 활성 key를 선택합니다 | 400 잘못된 입력; 404 provider/key 없음 | | `PUT /api/providers/keys/alias` | provider-key alias를 설정하거나 지웁니다 | 400 잘못된 입력; 404 provider/key 없음 | @@ -265,6 +266,10 @@ Aside 프로필 변경은 이때도 한 가지를 저장합니다. 확인을 보 자격 증명 목록 응답은 의도적으로 마스킹됩니다. OAuth access token과 완전한 provider API key는 대시보드 클라이언트에 반환되지 않습니다. +#### Anthropic OAuth: `pause` / `resume` + +CLI 명령은 Anthropic OAuth 계정을 id 또는 유일한 alias로 일시 정지하거나 재개합니다. alias는 정확히 일치하는 값을 먼저 찾고, 없으면 대소문자를 구분하지 않고 찾습니다. 대시보드와 같은 `PUT /api/oauth/accounts/pause`에 `{ provider: "anthropic", accountId, paused }`를 보냅니다. 계정에 저장되는 `paused` 상태는 `GET /api/oauth/accounts`에도 표시됩니다. 사전 계정 전환 풀이 꺼져 있어도 정지된 계정은 선택, 세션 바인딩, 429 대체 후보에서 제외됩니다. 모든 계정이 정지되면 하나를 재개할 때까지 요청은 403을 반환합니다. 이미 전송한 요청은 유지하며 자격 증명과 건강 상태를 지우지 않습니다. 재시작·재로그인 후에도 정지는 유지되고, 계정을 삭제하면 함께 제거됩니다. 계정별 전환 임계값은 이 기능에 포함되지 않습니다. + ### 제공자 | HTTP 메서드와 경로 | 목적 | 주요 오류 | @@ -375,3 +380,13 @@ account의 selector binding은 남아 있어 계정이 없을 때 exact route가 ## 원격 세션과 데이터 키 교체 `POST /api/keys/rotate {id}`는 최대 10분의 전환을 시작하며 새 데이터 키를 한 번만 반환합니다. `POST /api/keys/rotate/commit {id,rotationId}`는 확정하고, `DELETE /api/keys/rotate {id,rotationId}`는 취소합니다. 모두 관리 인증이 필요하며 데이터 키로 호출할 수 없습니다. `POST /api/session/logout`은 현재 `gui-session`, 일치하는 Origin, CSRF가 필요합니다. 관리자 토큰은 403을 받고 동의 세션을 만들거나 교환할 수 없습니다. + +## Anthropic 계정 사용량 임계값 + +`PUT /api/oauth/accounts/auto-switch` + +Anthropic OAuth 전용. `{ provider: "anthropic", accountId, threshold }`: 정수 0–100, null은 상속, 누락은 오류. 재시작 후 유지되고 계정 삭제 시 제거됩니다. + +계정 DTO는 `autoSwitchThresholdOverride`(정수/null), `autoSwitchThreshold`(풀 기본값), `effectiveAutoSwitchThreshold`를 포함합니다. 0은 사용량 전환만 끄며 pause·429 복구는 유지합니다. + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/ko/reference/proxy-formats.md b/docs-site/src/content/docs/ko/reference/proxy-formats.md index 4848304cfbb..e93eb315644 100644 --- a/docs-site/src/content/docs/ko/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ko/reference/proxy-formats.md @@ -73,6 +73,14 @@ deltas, 그리고 정확히 하나의 종료 `response.completed`, `response.fai `stream: false`이거나 `stream`이 없으면, 같은 adapter 이벤트가 하나의 Responses JSON 객체로 수집됩니다. 두 형식 모두 선택한 모델, output item, 종료 상태, usage를 보존합니다. +canonical ChatGPT Codex 경로는 upstream이 SSE만 받으므로 upstream 요청에만 `stream: true`를 사용합니다. +OpenCodex는 종료 스트림을 제한된 크기 안에서 검증한 뒤 클라이언트가 요청한 JSON 형태로 접습니다. +명시적인 `store` 값은 바꾸지 않으며, 검증에 실패하면 일부 JSON을 HTTP 200으로 반환하지 않고 오류로 +끝냅니다. 한도는 프레임당 4 MiB, transcript와 재구성 입력 각각 32 MiB, 100,000 SSE 프레임, 재구성 +output item 10,000개입니다. `stallTimeoutSec`는 첫 body byte와 이후 무응답 간격에 모두 적용됩니다. +값이 `0`이거나 로컬 upstream 기본값으로 비활성화된 경우 즉시 만료하지 않고, 독립된 15분 전체 상한만 +적용합니다. 스트리밍 클라이언트의 동작은 바뀌지 않습니다. + 클라이언트로 전달되는 Responses SSE 프레임은 SSE 블록 구분자 앞의 원시 바이트 기준으로 프레임당 4 MiB로 제한됩니다. HTTP에서는 구분자 없이 이 한도를 초과한 업스트림 프레임을 합성 `response.failed` 이벤트와 이어지는 `data: [DONE]`으로 fail closed 처리합니다. Responses WebSocket 브리지에서는 같은 조건에서 502 `websocket_protocol_error`를 보내고 업스트림 reader를 취소합니다. 완전한 Responses 종료 프레임이 이미 수신된 경우에는 그 종료가 우선하며, 이후의 과도한 크기 또는 잘못된 바이트는 완료된 턴을 전송 오류로 바꾸지 않고 버립니다. :::note diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 6742dde2bb1..1dae3494998 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -55,6 +55,12 @@ transport; it does not infer subscription attribution from the inbound protocol. tool message as the anchor. - **Rewrites Codex's GPT-5 identity prompt** to a model-agnostic intro so routed models don't claim to be OpenAI. +- For translated `Qwen3.8-27B` requests, a text-only developer reminder after the leading system + message stays in its conversation slot but is sent as `user`. The model's + [chat template](https://huggingface.co/Qwen/Qwen3.8-27B/blob/1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/chat_template.jinja) + rejects later `system` messages and does not accept `developer`, while later `user` messages + are valid. This preserves order but cannot preserve developer-role precedence. Other models + keep their configured developer-role behavior; native Chat passthrough is unchanged. - **Clamps `reasoning_effort`** to the model's advertised subset when an exact tier is unavailable; `xhigh` and `max` remain distinct labels unless a provider explicitly configures an alias. The adapter **omits it entirely** for ids in `provider.noReasoningModels`. @@ -214,7 +220,9 @@ a fresh session ID. Recovery and cached-history replay preserve this classificat The API-key `commandcode` provider uses Chat Completions for most model ids and the Anthropic Messages adapter (`x-api-key`) for `claude-*` ids, which Command Code serves only on `/provider/v1/messages`; the pin applies only while the provider points at that -endpoint. It supports forwarding `prompt_cache_key`; this is separate +endpoint. The `tokenlab` provider uses the same endpoint-bound pin for `claude-*` ids, which +TokenLab declares for Chat and Messages only, on `https://api.tokenlab.sh/v1/messages`. +Command Code supports forwarding `prompt_cache_key`; this is separate from the OAuth adapter's session header and does not guarantee a provider cache hit. The OAuth `command-code` preset streams `/alpha/generate` as NDJSON. MiMo tool-call markup echoed by the gateway as text is removed when it duplicates a real call. Markup @@ -526,11 +534,14 @@ compatibility pair: `agent.v1.AgentService/RunSSE` for server output and Foreground `shellArgs` and `shellStreamArgs` are an exception: both are rejected before spawn on every platform until kernel-backed descendant ownership is available. Use client shell tools; background-shell execution and other native operations retain their existing policy. -- The denial reply is a silent redirect whose wording follows the request catalog. A catalog that - carries `shell_command`/`exec_command` or a unified `exec` keeps the bridge wording; a catalog - that carries neither — an orchestrator client exposing only its own Responses tools, for example — - is redirected to the request's actual wire names, so the model is pointed at a tool that exists - rather than at an alias it cannot see. +- The denial reply is a silent redirect whose wording follows the request catalog. + In code mode — a freeform unified `exec` and no bare shell bridge — the redirect points inside `exec`, where + shell, file, search, and fetch are nested `tools.(...)` helpers of the JavaScript cell, + and never recommends the top-level shell bridge code mode does not expose. A flat catalog that + carries `shell_command`/`exec_command` or a non-freeform unified `exec` keeps the bridge wording; + a catalog that carries neither — an orchestrator client exposing only its own Responses tools, + for example — is redirected to the request's actual wire names, so the model is pointed at a + tool that exists rather than at an alias it cannot see. - A recognized Cursor data-policy gate is reported with its title, the action it requires, and the Cursor Dashboard review URL instead of a bare `failed_precondition: Error`. Recognition is limited to the known structured detail: unknown or malformed details keep the generic Connect error, no @@ -610,6 +621,20 @@ configuration that names the old id is rewritten at startup. allowance on standalone turns surface the original 429 without an early retry. The final 429 preserves the stated delay as a cooldown hint. A `~` in its message marks a delay recovered from a secondhand trailer sentence rather than an exact header value. +- For Codex Responses streams, known typed rate-limit failures with a valid delay are normalized to + `rate_limit_exceeded` with `Please try again in Ns.` before the original redacted detail. + This lets Codex honor the stated delay and use its native reconnect notification without + adding a reasoning item to conversation history. Client retries are finite and controlled by + the client's `stream_max_retries`; this does not promise recovery after app shutdown or restart. + Leave `OPENCODEX_DEVIN_STATED_RESET_WAIT_MS` unset or `0` to let the client own the wait. + A positive proxy allowance keeps the existing proxy-owned wait; the client only learns of a + final refusal afterwards, and client retries can multiply the proxy's per-request attempts. + Combo target/account failover and Grok HTTP 429 handling retain their existing ordering. + The exact UI placement and text depend on the Codex version; this is not a custom countdown. + Message-only rate-limit errors use the same longest-delay-first formatting. Typed errors + without a usable delay retain their original code. Client retries create new HTTP requests; + they do not share one proxy request's send counter or cumulative wait allowance. This + compatibility mapping does not replace the controls of an explicitly enabled proxy wait. - Experimental unofficial bridge; not shown in the dashboard preset by default. See the [provider guide](/guides/providers/) for login instructions. 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 b8ce75d6486..46a973c1265 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -251,7 +251,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Pool placement strategy; least-loaded is Kiro-only. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -469,21 +469,40 @@ Clear the manual Codex account selection without resolving an account id, so it ### `ocx account pause|resume [--json]` -Pause or resume one account in the Codex pool or a generic OAuth provider pool, including +Pause or resume one account in the Codex, Anthropic, or generic OAuth provider pool, including `google-antigravity`. For the Codex pool, `main` identifies only the built-in Codex account; -generic OAuth accounts must be identified by id or a unique alias. A paused generic OAuth account +OAuth accounts must be identified by id or a unique alias. A paused OAuth account is excluded from request selection, 429 failover, and proactive token refresh, and cannot be selected manually. Pausing the active account switches to the next usable account when one exists. If every account is paused, requests that need that pool return 403 until an account is resumed. -For a generic OAuth provider, identify the account by id or by a unique exact or case-insensitive +For Anthropic and generic OAuth providers, identify the account by id or by a unique exact or case-insensitive alias. The JSON response reports the account id, pause state, and active account id. +Anthropic pause applies even when proactive pooling is disabled, including session affinity and +429 successors. It survives restart and reauthentication, preserves credentials and health, +and does not interrupt a turn already sent. Removing the account removes its pause state. +Per-account Anthropic auto-switch thresholds are not part of this control. + ```bash ocx account pause google-antigravity ocx account resume google-antigravity ``` +### `ocx account clear-cooldown [--json]` + +Drops a process-local failure cooldown without changing stored credentials. Use `openai` for a Codex +pool account or `anthropic` for an Anthropic OAuth account; other providers are rejected. Both forms +accept an account id or unique alias, while `main` is specific to the Codex pool. + +```bash +ocx account clear-cooldown anthropic +``` + +The command reports success even when no cooldown is active, with `cleared: false` in JSON. Clearing +an Anthropic cooldown also advances the account generation so an older in-flight quota probe cannot +restore the cleared state or publish stale quota-derived eligibility afterward. + ### `ocx account refresh [--json]` For the Codex pool, use `ocx account refresh openai [--json]`. It force-refreshes account quotas and @@ -499,7 +518,11 @@ instead (exit 0), matching the dashboard's quota bars. ### `ocx account auto-switch > [--json]` -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. A generic pool threshold steers selection only while `pool.kernel` is on with `strategy: "fill-first"`; with the flag off, saving one does not enable threshold-based switching. It never changes the provider enablement override or disables 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 stored but not applied, `inert: false` means the pool is applying it, and an absent `inert` is an unknown capability, which never reports `enabled: true`. API-key providers, Anthropic and invalid values are rejected. +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. A generic pool threshold steers selection only while `pool.kernel` is on with `strategy: "fill-first"`; with the flag off, saving one does not enable threshold-based switching. It never changes the provider enablement override or disables 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 stored but not applied, `inert: false` means the pool is applying it, and an absent `inert` is an unknown capability, which never reports `enabled: true`. API-key providers and invalid values are rejected. + +### `ocx account auto-switch anthropic … --account ` + +For Anthropic OAuth, use `ocx account auto-switch anthropic threshold 90 --account ` (integer 0–100), `off --account ` (0), `on --account ` (80), `inherit --account ` (reset), or `status --account ` (read-only); append `--json` for structured output. The account card offers the same custom-threshold toggle. Missing/null inherits `anthropicAccountPool.autoSwitchThreshold` (default 80); 0 disables usage-driven switching for that account, not pause or reactive 429 recovery. Overrides survive restart and re-login and are removed with the account. With pooling enabled, quota and fill-first compare each source/candidate against its own threshold in the selected quota window. Manual/affinity precedence, identity-less round-robin/fill-first behavior, unknown-quota fallback and all-drained fallback remain unchanged. Round-robin is not usage-driven; disabled pools ignore these thresholds. Model-route allowlists still constrain every candidate. ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/reference/codex-retry-diagnostics.md b/docs-site/src/content/docs/reference/codex-retry-diagnostics.md new file mode 100644 index 00000000000..532ada6ddfd --- /dev/null +++ b/docs-site/src/content/docs/reference/codex-retry-diagnostics.md @@ -0,0 +1,96 @@ +--- +title: Codex retry diagnostics +description: Distinguish rate-limit advice, automatic retransmission, provider recovery, and Desktop notifications. +--- + +OpenCodex's [Devin adapter](/reference/adapters/#devin) preserves a usable retry +hint in the error contract Codex understands. That is not a guarantee that +Desktop displays a reconnect row, or that the provider accepts the next request. + +## Who waits + +With `OPENCODEX_DEVIN_STATED_RESET_WAIT_MS` unset or `0`, the Devin adapter does +not hold a refused request for the stated cooldown. Codex receives the final +failure and owns its bounded retry policy. A positive value opts into the +existing proxy-owned wait; it delays delivery of the final failure and can +compound with client retries. This compatibility mapping changes neither that +setting nor Codex's retry count. A process-only override need not survive restart. + +`rate_limit_exceeded` plus `Please try again in Ns.` is retry advice, not a +countdown event. Codex's own retry policy, cancellation and lifecycle still +apply. Do not add synthetic reasoning, tool, assistant or success items to make +waiting visible: those items can contaminate history or imply work happened. +Do not shorten the provider delay or provoke an extra request to advance the +client's retry counter. + +## Why the first reconnect row can be absent + +The public Codex `rust-v0.158.0-alpha.2.1` source gates the ordinary stream-retry +notification in +[`handle_response_stream_error`](https://github.com/openai/codex/blob/rust-v0.158.0-alpha.2.1/codex-rs/core/src/responses_retry.rs): + +```rust +let report_error = retry_count > 1 + || cfg!(debug_assertions) + || !sess.services.model_client.responses_websocket_enabled(); +``` + +In a release build, the first retry in this loop can therefore wait without +emitting `Reconnecting...` when the internal WebSocket-enabled predicate is +true. The selected delay is not part of that notification predicate, and the +sleep is outside the `if report_error` block. A long server-advised wait can be +silent too. This is a version-specific source observation, not proof that any +particular Desktop request took that branch. Do not infer the internal predicate +from a model name, an HTTP status, or the transport seen on one proxy hop. + +An enabled notification subscription and a renderer capable of drawing the row +only establish that the receiving path exists. They do not prove that the engine +emitted an event for the affected turn. Likewise, collapsed provider details can +explain missing detail text, but not a missing reconnect row by themselves. + +The corresponding app-server +[`ErrorNotification`](https://github.com/openai/codex/blob/rust-v0.158.0-alpha.2.1/codex-rs/app-server-protocol/schema/typescript/v2/ErrorNotification.ts) +contains `willRetry`, `threadId` and `turnId`. Observe emission, delivery to the +matching turn, and rendering separately. A proxy `response.failed` frame is not +itself that app-server notification. Changing this engine-side first-notification +policy requires a Codex change; an OpenCodex error-message rewrite cannot force +an event through a branch that does not emit it. + +## Read-only verification + +Keep the existing app, proxy, settings and test turn unchanged while collecting +evidence. Restarting, sending a new turn, or switching transports changes the +experiment. In an isolated follow-up, compare the same controlled failure under +both internal WebSocket states and inspect the first and subsequent retries; +do not call that an actual Desktop-rendering test unless the UI is observed. + +For the affected conversation and active turn, distinguish these outcomes: + +| Question | Evidence required | +| --- | --- | +| Was usable advice delivered? | The final failure's code, normalized delay and response-end time. | +| Did automatic retransmission occur? | A correlated next request after that failure, without a manual send. Measure from the failure response end to the next request start, not from the first request start. | +| Did the provider recover? | A successful response and continuation of the affected work, not merely another request or an unrelated successful turn. | +| Was a retry notification emitted and delivered? | The matching app-server error event and its retry flag, not subscription configuration alone. | +| Was the notice visible? | Observation of the affected Desktop turn; protocol or source-code checks alone are insufficient. | + +Approximate provider advice and scheduling overhead can produce a small timing +difference. If retransmission reaches a socket-close error and a later attempt +receives a new rate limit, record retransmission as working but provider recovery +as incomplete. The new refusal's delay is a new observation, not a timer inherited +from the previous refusal. An `inProgress` turn by itself proves neither recovery +nor correct rendering. Retry exhaustion, cancellation and app-restart recovery +must be assessed separately. + +Publish only the validation scope and aggregate durations/outcomes. Keep raw logs, +request bodies, credentials, account data, private paths, and thread/conversation/ +request/trace identifiers out of public PRs and diagnostic reports. + +## Regression-test scope + +`tests/server/retry-delay-hardening.test.ts` checks that 120-, 900-, 1,800-, +2,460- and 3,600-second advice survives formatting unchanged; a longer hint stays +first when a shorter one also exists; and an intervening untimed disconnect does +not make a later refusal inherit an old delay. These are pure parser/formatter +checks. They do not wait for an hour, exercise Codex's notification predicate, +verify an app restart, or prove recovery from a live provider failure. diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 20165746900..0680b278d31 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -26,14 +26,19 @@ The previous default list therefore becomes Astra, Sol, Terra, Luna, 5.5. An unset legacy list receives the current defaults; an explicit empty legacy list becomes `["gpt-6-astra"]`. Existing Astra entries are not duplicated. -The current default is `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`. On the first +The current default is `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`. On the first start after upgrading, a stored list is cleaned of retired rows: `gpt-5.6-sol` and `gpt-5.6-luna` become `gpt-6-sol` and `gpt-6-luna` in the same position, and every other `gpt-5.5` or `gpt-5.6` model is removed. Ids with a `/` (routed `provider/model` or account-qualified choices) are left as written. A list that held only retired rows receives the current default; an empty list stays empty. -The internal `subagentModelsVersion` marker (currently `2`) makes each step a +GPT-6.1 Sol replaced GPT-6 Sol as the default on September 30, 2026. On the first start +after that upgrade, a bare `gpt-6-sol` in a stored list becomes `gpt-6.1-sol` in the same +position (without a duplicate if 6.1 Sol is already listed). GPT-6 Sol stays available: add it +back afterwards and it stays. + +The internal `subagentModelsVersion` marker (currently `3`) makes each step a one-time upgrade. Afterwards you can reorder, remove Astra, or save an empty list without startup changing your choices again. Disabled models remain disabled. Astra availability still depends on upstream support for your account. @@ -42,7 +47,7 @@ still depends on upstream support for your account. | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` stamps every catalog model as v1; `v2` stamps every model as v2. `default` restores upstream pins (Sol/Terra v2, Luna v1) and otherwise follows the native `multi_agent_v2` flag. Applies to new sessions. | | `keepNativeChatGptOnV1?` | `boolean` | `false` | When `multiAgentMode` is `"v2"`, disable the global V2 override, stamp ChatGPT-native rows as v1, and keep routed rows on v2. Codex resolves the global override before catalog pins, so both parts are required for a ChatGPT parent to spawn routed children without backend-encrypted tasks ([#92](https://github.com/lidge-jun/opencodex/issues/92)). Ignored in `v1` and `default`. | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | Up to five bare native, account-qualified `/`, or routed `provider/model` ids featured first in the sub-agent picker. The dashboard offers only bare native and routed ids and omits exact account-qualified choices when it saves; use `ocx agent subagents set` or edit the configuration for exact choices. After the [one-time Astra upgrade](#astra-roster-upgrade), an explicit empty list is preserved. | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | Up to five bare native, account-qualified `/`, or routed `provider/model` ids featured first in the sub-agent picker. The dashboard offers only bare native and routed ids and omits exact account-qualified choices when it saves; use `ocx agent subagents set` or edit the configuration for exact choices. After the [one-time Astra upgrade](#astra-roster-upgrade), an explicit empty list is preserved. | | `injectionModel?` | `string` | — | Preferred native or routed sub-agent model used in proxy-authored v2 delegation guidance. | | `injectionEffort?` | `string` | — | Preferred effort (`low` through `ultra`), meaningful only with `injectionModel`. | | `injectionPrompt?` | `string` | — | Replaces the built-in v2 guidance body. Supports `{{model}}`, `{{effort}}`, `{{roster}}`, and `{{fallback}}`. A configured `injectionModel` is sufficient to render the custom prompt. | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 745e3536ad0..e4250e11654 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -135,9 +135,21 @@ when your installed Codex catalog predates them. | `gpt-6-sol` | 272,000 | 872,000 | `medium` | `low` through `ultra` | | `gpt-6-luna` | 272,000 | 872,000 | `medium` | `low` through `max` (no `ultra`) | +[GPT-6.1 Sol](https://openai.com/index/introducing-gpt-6-1-sol/) (announced September 29, 2026) +is the upgrade to GPT-6 Sol; Astra and Luna did not get a 6.1 release. It is listed as +`gpt-6.1-sol` (**GPT-6.1-Sol**), ungated like Sol, and its row comes from the Codex catalog, where +it needs `client_version` **0.153.0 or later** and is the Codex default. GPT-6 Sol stays listed. + +| Model | Default context | Opt-in ceiling | Default effort | Reasoning ladder | +| --- | ---: | ---: | --- | --- | +| `gpt-6.1-sol` | 272,000 | 872,000 | `low` | `low` through `ultra` | + The same `providerContextCaps.openai`, `modelContextWindows` and `modelAutoCompactTokenLimits` -levers apply as for Astra. There are no `openai-apikey/` rows or built-in price estimates for Sol -or Luna yet. +levers apply as for Astra. On the OpenAI API (`openai-apikey`), `gpt-6.1-sol` has 1,050,000 +context, 922,000 maximum input, 128,000 maximum output and efforts `low` through `max`, priced +at $2 input, $0.10 cached input and $10 output per 1M tokens (prompts over 272K bill at the +long-context rate). GitHub Copilot, OpenRouter, Vercel AI Gateway, Kilo and OpenCode Zen also +list it. When OpenAI ships a GPT model that this release does not know yet, add it through config instead of waiting for an update, the same way a new Claude id goes under `providers.anthropic.models`: @@ -156,7 +168,7 @@ waiting for an update, the same way a new Claude id goes under `providers.anthro ``` Each bare `gpt-*` id listed there on the Codex-login provider appears as a native model (here -**GPT-6-Nova**) with GPT-6 Sol's reasoning ladder and modalities, a 272,000-token default context +**GPT-6-Nova**) with GPT-6.1 Sol's reasoning ladder, default effort and modalities, a 272,000-token default context and an 872,000-token opt-in ceiling. Raise or narrow it with `modelContextWindows`, for example `"modelContextWindows": { "gpt-6-nova": 872000 }`. It is never account-gated: if your account cannot use the model, the request still goes out and you see the upstream error. Ids that are diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index f0d92698dbf..8bbc6894c18 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -16,7 +16,7 @@ runs helper features around provider requests. | `noProxy?` | `string \| string[]` | — | Hosts that bypass `proxy`, merged with inherited `NO_PROXY` and loopback entries. A string may use comma-separated `NO_PROXY` syntax or `${ENV_VAR}`. | | `emptyCompletionRetry?` | `boolean` | `false` | Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. `OCX_EMPTY_COMPLETION_RETRY=0` disables it without changing config; combo and routed-compaction turns remain excluded. | | `dropCodexSafetyBuffering?` | `boolean` | `false` | Remove optional client-facing hints from canonical Codex Responses passthrough: the two `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` response headers, `response.metadata` events whose metadata type is `safety_buffering`, and top-level `safety_buffering` fields. Other headers, response data, policy refusals and failures are preserved. This does not disable provider safety enforcement or upstream buffering. Native `codex.response.metadata.headers` WebSocket metadata and `/responses/compact` are outside this filter. | -| `stallTimeoutSec?` | `number` | `300` (public) / disabled (local) | Seconds without meaningful upstream progress (Responses and native Chat) before the stream is cut. Unset, a **local** upstream (loopback, private, or a `.local`/`.lan` name) defaults to disabled and a public upstream to 300 s; a positive value applies to both (minimum 1 s); `0` disables the watchdog everywhere. Disabled leaves a silent-but-healthy local model connected (keep-alives still flow). Pending `/v1/responses/compact` body reads share this budget but default to 300 s even for a local upstream — the route buffers the complete body while holding an active-turn lease — and an explicit value, including `0`, still wins. | +| `stallTimeoutSec?` | `number` | `300` (public) / disabled (local) | Seconds without meaningful upstream progress (Responses and native Chat) before the stream is cut. Unset, a **local** upstream (loopback, private, or a `.local`/`.lan` name) defaults to disabled and a public upstream to 300 s; a positive value applies to both (minimum 1 s); `0` disables the silence watchdog everywhere. Disabled leaves a silent-but-healthy local model connected (keep-alives still flow). Canonical ChatGPT Responses folded from SSE into non-streaming JSON retain a separate 15-minute whole-turn ceiling even when the silence watchdog is disabled. Pending `/v1/responses/compact` body reads share this budget but default to 300 s even for a local upstream — the route buffers the complete body while holding an active-turn lease — and an explicit value, including `0`, still wins. | | `oauthOpenBrowser?` | `boolean` | `true` | Whether a login may open a browser on the machine running the proxy. Absent and `true` both open, so an existing install is unchanged; only an explicit `false` declines. Decline when you need the authorization link in a different browser profile, or when the dashboard is not on the proxy's machine — the login still starts and the URL is still returned and displayed. `POST /api/oauth/login` and `POST /api/codex-auth/login` accept a per-request `openBrowser` boolean that overrides this, and the dashboard exposes the same choice beside the login button. Device-code flows never open a browser either way. | | `connectTimeoutMs?` | `number` | `200000` | Per-attempt DNS/TCP/TLS/final-header deadline; it ends before body generation. | | `shutdownTimeoutMs?` | `number` | `5000` | Graceful drain deadline before active turns are aborted. | diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 5e2ea2b0602..41e40d28302 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -557,10 +557,10 @@ outcome fields from an older server do not establish successful recovery. | `POST /api/oauth/login/cancel` | Cancel a public in-progress OAuth flow | 400 unknown provider | | `GET /api/oauth/status` | Poll one provider's OAuth flow | 400 unknown provider | | `POST /api/oauth/logout` | Remove the selected provider credential | 400 unknown provider; `oauth_mutation_busy` | -| `GET /api/oauth/accounts` | List masked accounts; generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider | +| `GET /api/oauth/accounts` | List masked accounts; Anthropic and generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider | | `DELETE /api/oauth/accounts` | Remove one account | 400 invalid provider/id; 404 account missing; `oauth_mutation_busy` | | `PUT /api/oauth/accounts/active` | Select the active OAuth account | 400 invalid provider/account; 404 account missing; 409 account paused; `oauth_mutation_busy` | -| `PUT /api/oauth/accounts/pause` | Pause or resume one generic OAuth account. Body `{ provider, accountId, paused }`; pausing the active account selects the next usable account when available | 400 unsupported provider or invalid body; 404 account missing; `oauth_mutation_busy` | +| `PUT /api/oauth/accounts/pause` | Pause or resume one Anthropic or generic OAuth account. Body `{ provider, accountId, paused }`; pausing the active account selects the next usable account when available. Pause is durable and independent of pool enablement; resume preserves health and credentials. | 400 unsupported provider or invalid body; 404 account missing; `oauth_mutation_busy` | | `GET, PUT, PATCH /api/pool/settings` | Read or update pool policy for any kind (codex, anthropic, generic); answers with the same keys for all three and declares in `supported` which the kind honours | 400 unknown provider, a field the kind does not support, or an invalid value | | `GET, PUT, PATCH /api/oauth/accounts/pool` | Legacy per-pool policy for Anthropic and generic OAuth providers; superseded by `/api/pool/settings` and kept for existing clients | 400 codex or api-key provider, or invalid policy | | `POST /api/oauth/accounts/clear-cooldown` | Clear one OAuth account's runtime cooldown | 400 invalid provider/account | @@ -744,3 +744,13 @@ Direct HTTP is most useful for integrations that need the exact endpoint contrac ## Remote sessions and data-key rotation `POST /api/keys/rotate {id}` starts a ten-minute overlap and returns the new data secret once. `POST /api/keys/rotate/commit {id,rotationId}` commits it; `DELETE /api/keys/rotate {id,rotationId}` aborts it. All require management authentication; data keys cannot call them. `POST /api/session/logout` requires the current `gui-session`, matching Origin, and CSRF. An admin token receives 403 and can never mint or exchange into a consent session. + +## Anthropic account usage threshold + +`PUT /api/oauth/accounts/auto-switch` + +Anthropic OAuth only; `{ provider: "anthropic", accountId, threshold }` accepts integer 0–100 or null to inherit. Missing threshold is invalid. Stored override survives restart and is removed with the account. + +Account-list DTOs include `autoSwitchThresholdOverride` (integer/null), `autoSwitchThreshold` (pool default), and `effectiveAutoSwitchThreshold`. 0 disables usage-driven switching only; it never disables pause or 429 recovery. + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index db7b765d6cd..bad29c7e258 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -212,6 +212,18 @@ With `stream: true`, the response is `text/event-stream`. The bridge emits Respo With `stream: false` or no `stream`, the same adapter events are collected into one Responses JSON object. Both forms preserve the selected model, output items, terminal status, and usage. +The canonical ChatGPT Codex route still uses `stream: true` on its upstream-only request because that +destination is SSE-only; OpenCodex boundedly validates and folds the terminal stream back into the +JSON shape the client requested. This transport coercion does not alter an explicit `store` value. +The first terminal must be valid; a later terminal cannot replace an invalid first one. Output indices +must be contiguous and covered by a completed item or the terminal output, so a text/tool delta left +open by a sparse terminal fails closed rather than becoming partial JSON. The path caps each frame at +4 MiB, each transcript and reconstruction source at 32 MiB, the stream at 100,000 frames, and +reconstructed output at 10,000 items. The configured `stallTimeoutSec` governs both the first upstream +body byte and later silent gaps. When that stall clock is disabled (`0`, including the default for a +local upstream), it does not expire immediately; only the independent 15-minute buffered-turn ceiling +remains. An EOF, malformed or oversized frame, read error, stall, cancellation, or missing terminal +returns an error instead of partial JSON with HTTP 200. Streaming callers are unchanged. When a provider filters or truncates a response, an unfinished tool call remains `incomplete` in both JSON and SSE. Partial output is preserved, and the bridge does not emit an argument 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 bd8f7886319..7df5ba2bbec 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -193,7 +193,7 @@ Sonnet 5, Haiku 4.5 и более старые модели под **More models ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 7339d3f53bb..899594aa3ab 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 @@ -107,7 +107,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Pool placement strategy; omit the value to read it. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -190,10 +190,29 @@ credential'а, это состояние тоже печатается, но к { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +Команда CLI приостанавливает или возобновляет аккаунт Anthropic OAuth по id или уникальному alias: сначала точное совпадение, затем без учёта регистра. CLI и дашборд используют `PUT /api/oauth/accounts/pause` с `{ provider: "anthropic", accountId, paused }`. Поле `paused` сохраняется в аккаунте и возвращается через `GET /api/oauth/accounts`. Пауза действует и при отключённом проактивном пуле: аккаунт исключается из выбора, привязок сессий и кандидатов после 429. Если приостановлены все аккаунты, запросы получают 403 до возобновления одного из них. Уже отправленные запросы продолжаются; учётные данные и состояние здоровья сохраняются. Пауза переживает перезапуск и повторный вход, но удаляется вместе с аккаунтом. Индивидуальные пороги в эту операцию не входят. + ### `ocx account clear [--json]` Снимает ручной выбор аккаунта Codex без разрешения id, поэтому работает, даже когда аккаунт буквально называется `auto`. Только для пулов Codex; у других типов провайдеров нет автоматического выбора для восстановления. +### `ocx account clear-cooldown [--json]` + +Снимает локальный для процесса cooldown после сбоя, не меняя сохранённые учётные данные. Используйте +`openai` для аккаунта пула Codex или `anthropic` для OAuth-аккаунта Anthropic; другие провайдеры +отклоняются. Обе формы принимают id аккаунта или уникальный псевдоним, а `main` относится только к +пулу Codex. + +```bash +ocx account clear-cooldown anthropic +``` + +Команда завершается успешно и без активного cooldown, возвращая `cleared: false` в JSON. При снятии +cooldown Anthropic также увеличивается поколение аккаунта, поэтому старый quota probe не сможет +восстановить снятое состояние или опубликовать устаревшую доступность. + ### `ocx account refresh [--json]` Для пула Codex используйте `ocx account refresh openai [--json]`. Команда принудительно @@ -211,7 +230,11 @@ quota-bar'ов дашборда. ### `ocx account auto-switch > [--json]` -Управляет порогом пула Codex `openai` или сохраняет порог общего пула OAuth. `on` сохраняет 80 %, `off` — 0 %, а `threshold ` принимает 0–100. Порог общего пула влияет на выбор только при включённом `pool.kernel` и `strategy: "fill-first"`; при выключенном флаге сохранение не включает переключение по порогу. В обоих случаях оно не меняет настройку включения провайдера и не отключает ротацию после ошибки 429. Для общего пула результат чтения и изменения берётся из подтверждённого ответа сервера. Для общего пула `poolEnabled` — сохранённая настройка провайдера (`null` означает отсутствие настройки), а не итоговое унаследованное состояние. `inert: true` означает, что порог сохранён, но не применяется, а `inert: false` — что пул его применяет. Отсутствие `inert` означает неизвестную возможность, которая также не даёт `enabled: true`. Провайдеры с ключом API, Anthropic и неверные значения отклоняются. +Управляет порогом пула Codex `openai` или сохраняет порог общего пула OAuth. `on` сохраняет 80 %, `off` — 0 %, а `threshold ` принимает 0–100. Порог общего пула влияет на выбор только при включённом `pool.kernel` и `strategy: "fill-first"`; при выключенном флаге сохранение не включает переключение по порогу. В обоих случаях оно не меняет настройку включения провайдера и не отключает ротацию после ошибки 429. Для общего пула результат чтения и изменения берётся из подтверждённого ответа сервера. Для общего пула `poolEnabled` — сохранённая настройка провайдера (`null` означает отсутствие настройки), а не итоговое унаследованное состояние. `inert: true` означает, что порог сохранён, но не применяется, а `inert: false` — что пул его применяет. Отсутствие `inert` означает неизвестную возможность, которая также не даёт `enabled: true`. Провайдеры с ключом API и неверные значения отклоняются. + +### `ocx account auto-switch anthropic … --account ` + +Для Anthropic OAuth команда `ocx account auto-switch anthropic threshold 90 --account ` сохраняет целое число 0–100. `off --account ` задаёт 0, `on --account ` — 80, `inherit --account ` восстанавливает наследование, а `status --account ` только читает; доступен `--json`. Карточка аккаунта предлагает тот же контроль. Отсутствующее/null значение наследует `anthropicAccountPool.autoSwitchThreshold` (по умолчанию 80); 0 отключает только переключение по использованию этого аккаунта. Настройка переживает перезапуск и повторный вход, удаляется вместе с аккаунтом. Ручной выбор, affinity, резервный выбор при неизвестных или исчерпанных квотах и ограничения маршрутов не меняются. При выключенном пуле пороги не действуют; пауза и восстановление после 429 сохраняются. ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/ru/reference/configuration/agents.md b/docs-site/src/content/docs/ru/reference/configuration/agents.md index d3deef428cb..6b0afa7d7fe 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -11,7 +11,7 @@ description: Multi-agent surface, guidance при делегировании, pr | Поле | Тип | По умолчанию | Значение | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` штампует все модели как v1; `v2` штампует все модели как v2. `default` восстанавливает upstream pin'ы (Sol/Terra — v2, Luna — v1) и для остальных следует native flag `multi_agent_v2`. Применяется к новым сессиям. | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | До пяти bare native-id, account-qualified id `/` или routed-id `provider/model`, которые показываются первыми в picker'е подагентов. Страница Subagents предлагает только bare native- и routed-id и при сохранении исключает точные account-qualified варианты; для точного выбора используйте `ocx agent subagents set` или отредактируйте конфигурацию. После [однократного обновления Astra](/reference/configuration/agents/#astra-roster-upgrade) явный пустой список сохраняется. | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | До пяти bare native-id, account-qualified id `/` или routed-id `provider/model`, которые показываются первыми в picker'е подагентов. Страница Subagents предлагает только bare native- и routed-id и при сохранении исключает точные account-qualified варианты; для точного выбора используйте `ocx agent subagents set` или отредактируйте конфигурацию. После [однократного обновления Astra](/reference/configuration/agents/#astra-roster-upgrade) явный пустой список сохраняется. | | `injectionModel?` | `string` | — | Предпочитаемая native- или routed-модель подагента, которую proxy использует в собственном guidance v2. | | `injectionEffort?` | `string` | — | Предпочитаемый effort (`low`–`ultra`), имеющий смысл только вместе с `injectionModel`. | | `injectionPrompt?` | `string` | — | Заменяет встроенное тело guidance для v2. Поддерживает `{{model}}`, `{{effort}}`, `{{roster}}` и `{{fallback}}`. Настроенного `injectionModel` достаточно, чтобы отобразить пользовательский prompt. | diff --git a/docs-site/src/content/docs/ru/reference/configuration/server.md b/docs-site/src/content/docs/ru/reference/configuration/server.md index 2a4dcb20c68..73af6c0fbe8 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/server.md +++ b/docs-site/src/content/docs/ru/reference/configuration/server.md @@ -15,7 +15,7 @@ description: Listener, удалённый доступ, admission key, тайм | `proxy?` | `string` | — | URL исходящего HTTP(S) или SOCKS5-прокси (`socks5://host:port`) или `${ENV_VAR}`. HTTP URL пишутся в `HTTP_PROXY` / `HTTPS_PROXY`, если те не заданы. SOCKS5 используют встроенный SOCKS5-туннель и также пишутся в `ALL_PROXY` (`ocx start --socks5`); унаследованные `HTTP(S)_PROXY` сбрасываются в этом процессе. Loopback всегда остаётся в `NO_PROXY`. | | `emptyCompletionRetry?` | `boolean` | `false` | Явно включает один идентичный повтор Responses, если в turn нет ни текста, ни tool call, включая случай, когда stream завершается до terminal event. Повтор может тарифицироваться. `OCX_EMPTY_COMPLETION_RETRY=0` отключает его без изменения config; combo и routed-compaction turn исключены. | | `dropCodexSafetyBuffering?` | `boolean` | `false` | Удаляет подсказки Codex safety-buffering из passthrough-ответов Codex Responses: заголовки `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model`, SSE-события `response.metadata` типа `safety_buffering` и поле `safety_buffering` в других SSE-событиях. Codex TUI отображает их как предложение повторить запрос с более быстрой моделью, действие по умолчанию в котором переключает сессию на более слабую модель. Остальные заголовки `x-codex-*` и содержимое других SSE-событий передаются без изменений, кроме удаления этого поля. По умолчанию выключено. | -| `stallTimeoutSec?` | `number` | `300` (public) / выкл. (local) | Секунды без полезного прогресса upstream (Responses и нативный Chat) до обрыва потока. Без настройки **локальный** upstream (loopback, private, имя `.local`/`.lan`) по умолчанию выключен, публичный — 300 с; положительное значение действует на оба (минимум 1 с); `0` отключает watchdog везде. Ожидающие чтения тела `/v1/responses/compact` используют этот же бюджет, но по умолчанию 300 с даже для локального upstream; явное значение, включая `0`, имеет приоритет. | +| `stallTimeoutSec?` | `number` | `300` (public) / выкл. (local) | Секунды без полезного прогресса upstream (Responses и нативный Chat) до обрыва потока. Без настройки **локальный** upstream (loopback, private, имя `.local`/`.lan`) по умолчанию выключен, публичный — 300 с; положительное значение действует на оба (минимум 1 с); `0` отключает watchdog тишины везде. Для Responses, которые сворачивают canonical ChatGPT SSE в непотоковый JSON, даже при выключенном watchdog остаётся отдельный общий предел 15 минут. Ожидающие чтения тела `/v1/responses/compact` используют этот же бюджет, но по умолчанию 300 с даже для локального upstream; явное значение, включая `0`, имеет приоритет. | | `connectTimeoutMs?` | `number` | `200000` | Дедлайн одной попытки DNS/TCP/TLS/final-header; он завершается до генерации тела ответа. | | `shutdownTimeoutMs?` | `number` | `5000` | Дедлайн graceful-drain до принудительного прерывания активных turn'ов. | | `websockets?` | `boolean` | `false` | Объявляет и разрешает клиентский WebSocket-путь Responses. При false клиенты используют HTTP/SSE; это не отключает подходящую upstream WS-оптимизацию canonical ChatGPT. | diff --git a/docs-site/src/content/docs/ru/reference/management-api.md b/docs-site/src/content/docs/ru/reference/management-api.md index 30a02a70ed7..b7f827b36b6 100644 --- a/docs-site/src/content/docs/ru/reference/management-api.md +++ b/docs-site/src/content/docs/ru/reference/management-api.md @@ -276,6 +276,7 @@ Endpoint'ы storage cleanup могут перемещать или навсег | `GET, PUT, PATCH /api/oauth/accounts/pool` | Прежняя policy пула для Anthropic и обычных OAuth-провайдеров; заменена на `/api/pool/settings` и сохранена для существующих клиентов | 400 codex или api-key provider, либо недопустимая policy | | `POST /api/oauth/accounts/clear-cooldown` | Очистить runtime cooldown одного OAuth-аккаунта | 400 invalid provider/account | | `PUT /api/oauth/accounts/alias` | Задать или очистить alias OAuth-аккаунта | 400 invalid provider/account/alias | +| `PUT /api/oauth/accounts/pause` | Приостановить/возобновить Anthropic или обычный OAuth-аккаунт. Body `{ provider, accountId, paused }`; при паузе активного аккаунта выбирается другой пригодный аккаунт, если он есть. | 400 неподдерживаемый provider или неверный body; 404 аккаунт не найден; `oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` | Показать список masked provider-key'ов, добавить/активировать один или удалить один | 400 invalid input; 404 provider/key missing | | `PUT /api/providers/keys/active` | Выбрать активный ключ провайдера | 400 invalid input; 404 provider/key missing | | `PUT /api/providers/keys/alias` | Задать или очистить alias provider-key'а | 400 invalid input; 404 provider/key missing | @@ -284,6 +285,10 @@ Endpoint'ы storage cleanup могут перемещать или навсег Ответы со списками credential'ов намеренно маскируются. OAuth access-token'ы и полные API-key'и провайдеров клиентам дашборда не возвращаются. +#### Anthropic OAuth: `pause` / `resume` + +Команда CLI приостанавливает или возобновляет аккаунт Anthropic OAuth по id или уникальному alias: сначала точное совпадение, затем без учёта регистра. CLI и дашборд используют `PUT /api/oauth/accounts/pause` с `{ provider: "anthropic", accountId, paused }`. Поле `paused` сохраняется в аккаунте и возвращается через `GET /api/oauth/accounts`. Пауза действует и при отключённом проактивном пуле: аккаунт исключается из выбора, привязок сессий и кандидатов после 429. Если приостановлены все аккаунты, запросы получают 403 до возобновления одного из них. Уже отправленные запросы продолжаются; учётные данные и состояние здоровья сохраняются. Пауза переживает перезапуск и повторный вход, но удаляется вместе с аккаунтом. Индивидуальные пороги в эту операцию не входят. + ### Провайдеры | Метод и путь | Назначение | Особые ошибки | @@ -397,3 +402,13 @@ fail closed, пока аккаунт отсутствует, а при повт ## Удалённые сессии и ротация ключей данных `POST /api/keys/rotate {id}` начинает десятиминутный overlap и один раз возвращает новый секрет. `POST /api/keys/rotate/commit {id,rotationId}` подтверждает, `DELETE /api/keys/rotate {id,rotationId}` отменяет. Требуется management auth; ключ данных не подходит. `POST /api/session/logout` требует текущую `gui-session`, совпадающий Origin и CSRF. Admin token получает 403 и не может создать consent session. + +## Порог использования аккаунта Anthropic + +`PUT /api/oauth/accounts/auto-switch` + +Только Anthropic OAuth. `{ provider: "anthropic", accountId, threshold }`: целое 0–100 или null для наследования; отсутствие поля — ошибка. Сохраняется при перезапуске и удаляется вместе с аккаунтом. + +DTO содержит `autoSwitchThresholdOverride` (целое/null), `autoSwitchThreshold` (порог пула) и `effectiveAutoSwitchThreshold`. 0 отключает только переключение по использованию; пауза и восстановление после 429 сохраняются. + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/ru/reference/proxy-formats.md b/docs-site/src/content/docs/ru/reference/proxy-formats.md index 976972081e2..510b3d6eadf 100644 --- a/docs-site/src/content/docs/ru/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ru/reference/proxy-formats.md @@ -77,6 +77,15 @@ Translated-adapter'ы обрабатывают только известные При `stream: false` или при отсутствии `stream` те же события адаптера собираются в один JSON Responses. Обе формы сохраняют выбранную модель, output item'ы, terminal status и usage. +Канонический маршрут ChatGPT Codex принимает от upstream только SSE, поэтому `stream: true` +задаётся только для upstream-запроса. OpenCodex проверяет терминальный поток в заданных пределах и +сворачивает его в JSON, запрошенный клиентом, не меняя явно заданный `store`. Ошибка проверки не +возвращается как частичный JSON с HTTP 200. Ограничения: 4 MiB на frame, по 32 MiB на transcript и +источник реконструкции, 100 000 SSE-frame'ов и 10 000 восстановленных output item'ов. +`stallTimeoutSec` ограничивает ожидание первого body byte и последующие периоды тишины. Если значение +равно `0` или отключено по умолчанию для локального upstream, немедленного timeout нет и действует +только отдельный общий предел 15 минут. Поведение streaming-клиентов не меняется. + Клиентские frame'ы Responses SSE ограничены 4 MiB на frame, считая сырые байты до разделителя SSE-блока. В HTTP незавершённый upstream-frame, превысивший этот предел, завершается fail-closed синтетическим событием `response.failed`, после которого идёт `data: [DONE]`. В мосте Responses WebSocket то же условие даёт 502 `websocket_protocol_error` и отменяет upstream-reader. Если полноценный terminal-frame Responses уже получен, он остаётся авторитетным: слишком большие или некорректные байты после него отбрасываются и не заменяют завершённый ход транспортной ошибкой. :::note 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 5b933a74709..e88cd7f0ac7 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -271,7 +271,7 @@ kimliğidir; bu yüzden bir seçici satırını bir opencodex rotasına bağlars ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 15d2c125d1f..8e8bef8b9e0 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 @@ -126,7 +126,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Havuz stratejisi; least-loaded yalnızca Kiro içindir. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -235,10 +235,29 @@ ayar yalnızca kullanıma dayalı proaktif geçişi devre dışı bırakır. { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +CLI komutu Anthropic OAuth hesabını id veya benzersiz takma ad ile duraklatır ya da sürdürür. Önce tam eşleşme, ardından büyük/küçük harf duyarsız eşleşme aranır. CLI ve kontrol paneli aynı `PUT /api/oauth/accounts/pause` uç noktasına `{ provider: "anthropic", accountId, paused }` gönderir. `paused` hesapta saklanır ve `GET /api/oauth/accounts` yanıtında gösterilir. Proaktif havuz kapalı olsa bile duraklatılan hesap seçimden, oturum bağlarından ve 429 sonrası adaylardan çıkarılır. Tüm hesaplar duraklatılmışsa biri sürdürülene kadar istekler 403 döndürür. Önceden gönderilmiş istekler devam eder; kimlik bilgileri ve sağlık durumu korunur. Duraklatma yeniden başlatma ve yeniden girişten sonra da sürer, hesap silinince kaldırılır. Hesaba özel eşikler bu işleme dahil değildir. + ### `ocx account clear [--json]` Bir hesap id'si çözümlemeden Codex hesabının elle seçimini temizler; `auto` adında bir hesap olsa bile çalışır. Yalnızca Codex havuzları içindir; diğer sağlayıcı türlerinde geri yüklenecek otomatik seçim yoktur. +### `ocx account clear-cooldown [--json]` + +Kaydedilmiş kimlik bilgilerini değiştirmeden süreç içi hata cooldown durumunu kaldırır. Codex havuzu +hesabı için `openai`, Anthropic OAuth hesabı için `anthropic` kullanın; diğer sağlayıcılar reddedilir. +Her iki biçim de hesap id'sini veya benzersiz takma adı kabul eder, `main` ise yalnızca Codex havuzuna +özgüdür. + +```bash +ocx account clear-cooldown anthropic +``` + +Etkin cooldown olmasa da komut başarılı olur ve JSON'da `cleared: false` döner. Anthropic cooldown +temizliği hesap generation değerini de ilerletir; böylece eski bir quota probe temizlenen durumu geri +getiremez veya eski quota uygunluğunu yayımlayamaz. + ### `ocx account refresh [--json]` Codex havuzu için `ocx account refresh openai [--json]` kullanın. Hesap @@ -258,7 +277,11 @@ eşleşen null veya eski bir rapora düşer (çıkış 0). ### `ocx account auto-switch > [--json]` -`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şiği yalnızca `pool.kernel` açıkken ve `strategy: "fill-first"` seçiliyken seçimi yönlendirir; bayrak kapalıyken kayıt işlemi eşik tabanlı geçişi etkinleştirmez. Her iki durumda da 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 kaydedildiğini ama uygulanmadığını, `inert: false` ise havuzun onu uyguladığını belirtir. `inert` yoksa yetenek bilinmiyordur ve bu durumda da `enabled: true` bildirilmez. API anahtarlı sağlayıcılar, Anthropic ve geçersiz değerler reddedilir. +`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şiği yalnızca `pool.kernel` açıkken ve `strategy: "fill-first"` seçiliyken seçimi yönlendirir; bayrak kapalıyken kayıt işlemi eşik tabanlı geçişi etkinleştirmez. Her iki durumda da 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 kaydedildiğini ama uygulanmadığını, `inert: false` ise havuzun onu uyguladığını belirtir. `inert` yoksa yetenek bilinmiyordur ve bu durumda da `enabled: true` bildirilmez. API anahtarlı sağlayıcılar ve geçersiz değerler reddedilir. + +### `ocx account auto-switch anthropic … --account ` + +Anthropic OAuth için `ocx account auto-switch anthropic threshold 90 --account ` komutu 0–100 arasında tam sayı kaydeder. `off --account ` 0, `on --account ` 80 kaydeder; `inherit --account ` devralmayı geri getirir, `status --account ` yalnızca okur. `--json` desteklenir. Hesap kartı aynı ayarı sunar. Eksik/null değer `anthropicAccountPool.autoSwitchThreshold` varsayılanını (80) devralır; 0 yalnızca bu hesabın kullanıma dayalı geçişini kapatır. Yeniden başlatma ve girişte korunur, hesap silinince kaldırılır. Manuel seçim, affinity, bilinmeyen/tükenmiş kota yedek davranışı ve model rotaları değişmez. Havuz kapalıyken eşikler uygulanmaz; duraklatma ve 429 kurtarması sürer. ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/tr/reference/configuration/agents.md b/docs-site/src/content/docs/tr/reference/configuration/agents.md index 81b2e600135..5fec449caa1 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -12,7 +12,7 @@ kontrol eder. | Alan | Tip | Varsayılan | Anlamı | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` her katalog modelini v1 olarak damgalar; `v2` her modeli v2 olarak damgalar. `default` yukarı akış sabitlemelerini geri yükler (Sol/Terra v2, Luna v1) ve aksi takdirde yerel `multi_agent_v2` bayrağını takip eder. Yeni oturumlara uygulanır. | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | Alt ajan seçicisinde ilk olarak öne çıkan en fazla beş yalın yerel, hesap nitelikli `/` veya yönlendirilen `saglayici/model` kimliği. Kontrol paneli yalnızca yalın yerel ve yönlendirilen kimlikleri sunar ve kaydederken tam hesap nitelikli seçimleri atlar; tam seçimler için `ocx agent subagents set` kullanın veya yapılandırmayı düzenleyin. [Tek seferlik Astra yükseltmesinden](/reference/configuration/agents/#astra-roster-upgrade) sonra açık bir boş liste korunur. | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | Alt ajan seçicisinde ilk olarak öne çıkan en fazla beş yalın yerel, hesap nitelikli `/` veya yönlendirilen `saglayici/model` kimliği. Kontrol paneli yalnızca yalın yerel ve yönlendirilen kimlikleri sunar ve kaydederken tam hesap nitelikli seçimleri atlar; tam seçimler için `ocx agent subagents set` kullanın veya yapılandırmayı düzenleyin. [Tek seferlik Astra yükseltmesinden](/reference/configuration/agents/#astra-roster-upgrade) sonra açık bir boş liste korunur. | | `injectionModel?` | `string` | — | Proxy kaynaklı v2 yetkilendirme rehberliğinde kullanılan tercih edilen yerel veya yönlendirilen alt ajan modeli. | | `injectionEffort?` | `string` | — | Yalnızca `injectionModel` ile anlamlı olan tercih edilen çaba (`low` ile `ultra` arası). | | `injectionPrompt?` | `string` | — | Yerleşik v2 rehberlik gövdesinin yerini alır. `{{model}}`, `{{effort}}`, `{{roster}}` ve `{{fallback}}` destekler. Yapılandırılmış bir `injectionModel`, özel istemi oluşturmak için yeterlidir. | diff --git a/docs-site/src/content/docs/tr/reference/configuration/server.md b/docs-site/src/content/docs/tr/reference/configuration/server.md index a4d69c1f4a0..301faadfe16 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/server.md +++ b/docs-site/src/content/docs/tr/reference/configuration/server.md @@ -15,7 +15,7 @@ yardımcı özellikleri nasıl çalıştıracağını kontrol eder. | `hostname?` | `string` | `"127.0.0.1"` | Bağlama adresi. Geri döngü olmayan bağlamalar `OPENCODEX_API_AUTH_TOKEN` gerektirir. | | `proxy?` | `string` | — | Giden HTTP(S) veya SOCKS5 proxy URL'si (`socks5://host:port`) ya da `${ENV_VAR}`. HTTP URL'leri değişkenler boşsa `HTTP_PROXY` / `HTTPS_PROXY`'ye yazılır. SOCKS5 URL'leri yerleşik gerçek SOCKS5 tünelini kullanır ve `ALL_PROXY`'ye de yazılır (`ocx start --socks5`); bu süreçte miras `HTTP(S)_PROXY` temizlenir. Geri döngü `NO_PROXY` içinde kalır. | | `emptyCompletionRetry?` | `boolean` | `false` | Metin veya araç çağrısı içermeyen bir Responses tamamlamasını aynı istekle bir kez yeniden denemeyi açıkça etkinleştirir. Yeniden deneme ücretlendirilebilir. `OCX_EMPTY_COMPLETION_RETRY=0`, yapılandırmayı değiştirmeden devre dışı bırakır; combo ve routed-compaction turları hariçtir. | -| `stallTimeoutSec?` | `number` | `300` (public) / kapalı (local) | Akış kesilmeden önce anlamlı üst sunucu ilerlemesi olmadan geçen saniye (Responses ve yerel Chat). Ayarlanmamışsa **yerel** üst sunucu (loopback, private, `.local`/`.lan` adı) için varsayılan kapalı, genel üst sunucu için 300 sn; pozitif değer ikisine de uygulanır (en az 1 sn); `0` watchdog'u her yerde kapatır. `/v1/responses/compact` için bekleyen gövde okumaları bu bütçeyi paylaşır ama yerel üst sunucuda bile varsayılan 300 sn'dir; açık değer (`0` dahil) önceliklidir. | +| `stallTimeoutSec?` | `number` | `300` (public) / kapalı (local) | Akış kesilmeden önce anlamlı üst sunucu ilerlemesi olmadan geçen saniye (Responses ve yerel Chat). Ayarlanmamışsa **yerel** üst sunucu (loopback, private, `.local`/`.lan` adı) için varsayılan kapalı, genel üst sunucu için 300 sn; pozitif değer ikisine de uygulanır (en az 1 sn); `0` sessizlik watchdog'unu her yerde kapatır. Canonical ChatGPT SSE'yi akışsız JSON'a katlayan Responses isteklerinde watchdog kapalı olsa bile bağımsız 15 dakikalık toplam tur sınırı kalır. `/v1/responses/compact` için bekleyen gövde okumaları bu bütçeyi paylaşır ama yerel üst sunucuda bile varsayılan 300 sn'dir; açık değer (`0` dahil) önceliklidir. | | `connectTimeoutMs?` | `number` | `200000` | Deneme başına DNS/TCP/TLS/nihai başlık son tarihi; gövde üretiminden önce biter. | | `shutdownTimeoutMs?` | `number` | `5000` | Aktif turlar iptal edilmeden önce zarif boşaltma süresi sınırı. | | `websockets?` | `boolean` | `false` | Responses WebSocket yolu için `supports_websockets` bildirin. False, HTTP/SSE'yi tutar. | diff --git a/docs-site/src/content/docs/tr/reference/management-api.md b/docs-site/src/content/docs/tr/reference/management-api.md index 1bd688931ae..1666fb0a8de 100644 --- a/docs-site/src/content/docs/tr/reference/management-api.md +++ b/docs-site/src/content/docs/tr/reference/management-api.md @@ -301,6 +301,7 @@ Güvenilir ilk model listesi hazır olana kadar `/api/selected-models` ve `/api/ | `GET, PUT, PATCH /api/oauth/accounts/pool` | Anthropic OAuth havuz politikasını okuyun veya güncelleyin | 400 Anthropic olmayan sağlayıcı veya geçersiz politika | | `POST /api/oauth/accounts/clear-cooldown` | Bir OAuth hesabının çalışma zamanı soğuma süresini temizleyin | 400 geçersiz sağlayıcı/hesap | | `PUT /api/oauth/accounts/alias` | Bir OAuth hesap takma adını ayarlayın veya temizleyin | 400 geçersiz sağlayıcı/hesap/takma ad | +| `PUT /api/oauth/accounts/pause` | Anthropic veya genel OAuth hesabını duraklatın/sürdürün. Body `{ provider, accountId, paused }`; etkin hesap duraklatıldığında varsa kullanılabilir başka hesaba geçilir. | 400 desteklenmeyen sağlayıcı veya geçersiz body; 404 hesap yok; `oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` | Maskelenmiş sağlayıcı anahtarlarını listeleyin, bir tane ekleyin/etkinleştirin veya kaldırın | 400 geçersiz girdi; 404 sağlayıcı/anahtar eksik | | `PUT /api/providers/keys/active` | Bir sağlayıcının etkin anahtarını seçin | 400 geçersiz girdi; 404 sağlayıcı/anahtar eksik | | `PUT /api/providers/keys/alias` | Bir sağlayıcı anahtarı takma adını ayarlayın veya temizleyin | 400 geçersiz girdi; 404 sağlayıcı/anahtar eksik | @@ -310,6 +311,10 @@ Kimlik bilgisi listesi yanıtları kasıtlı olarak maskelenir. OAuth erişim belirteçleri ve eksiksiz sağlayıcı API anahtarları kontrol paneli istemcilerine döndürülmez. +#### Anthropic OAuth: `pause` / `resume` + +CLI komutu Anthropic OAuth hesabını id veya benzersiz takma ad ile duraklatır ya da sürdürür. Önce tam eşleşme, ardından büyük/küçük harf duyarsız eşleşme aranır. CLI ve kontrol paneli aynı `PUT /api/oauth/accounts/pause` uç noktasına `{ provider: "anthropic", accountId, paused }` gönderir. `paused` hesapta saklanır ve `GET /api/oauth/accounts` yanıtında gösterilir. Proaktif havuz kapalı olsa bile duraklatılan hesap seçimden, oturum bağlarından ve 429 sonrası adaylardan çıkarılır. Tüm hesaplar duraklatılmışsa biri sürdürülene kadar istekler 403 döndürür. Önceden gönderilmiş istekler devam eder; kimlik bilgileri ve sağlık durumu korunur. Duraklatma yeniden başlatma ve yeniden girişten sonra da sürer, hesap silinince kaldırılır. Hesaba özel eşikler bu işleme dahil değildir. + ### Sağlayıcılar | Yöntem ve yol | Amaç | Dikkate değer hatalar | @@ -433,3 +438,13 @@ entegrasyonlar için en yararlıdır. ## Uzak oturumlar ve veri anahtarı döndürme `POST /api/keys/rotate {id}` on dakikalık geçişi başlatır ve yeni sırrı yalnızca bir kez döndürür. `POST /api/keys/rotate/commit {id,rotationId}` onaylar, `DELETE /api/keys/rotate {id,rotationId}` iptal eder. Yönetim kimlik doğrulaması gerekir; veri anahtarı bunları çağıramaz. `POST /api/session/logout` mevcut `gui-session`, eşleşen Origin ve CSRF ister. Admin token 403 alır ve onay oturumu oluşturamaz. + +## Anthropic hesap kullanım eşiği + +`PUT /api/oauth/accounts/auto-switch` + +Yalnızca Anthropic OAuth. `{ provider: "anthropic", accountId, threshold }`: 0–100 tam sayı veya devralmak için null; eksik alan hatadır. Yeniden başlatmada korunur, hesapla birlikte silinir. + +DTO: `autoSwitchThresholdOverride` (tam sayı/null), `autoSwitchThreshold` (havuz varsayılanı), `effectiveAutoSwitchThreshold`. 0 yalnızca kullanıma dayalı geçişi kapatır; duraklatma ve 429 kurtarması sürer. + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/tr/reference/proxy-formats.md b/docs-site/src/content/docs/tr/reference/proxy-formats.md index 90ed019103b..3178dfae456 100644 --- a/docs-site/src/content/docs/tr/reference/proxy-formats.md +++ b/docs-site/src/content/docs/tr/reference/proxy-formats.md @@ -81,6 +81,17 @@ olayı gibi Responses olaylarını yayar. Normal bir akış `data: [DONE]` ile b Responses JSON nesnesinde toplanır. Her iki form da seçilen modeli, çıktı öğelerini, terminal durumunu ve kullanımı korur. +Canonical ChatGPT Codex rotasının yukarı akışı yalnızca SSE kabul ettiğinden, +yalnızca yukarı akış isteği `stream: true` kullanır. OpenCodex terminal akışı +sınırlı boyutlar içinde doğrular ve istemcinin istediği JSON biçimine katlar; +açık bir `store` değeri değişmez. Doğrulama başarısız olursa HTTP 200 ile kısmi +JSON yerine hata döner. Sınırlar çerçeve başına 4 MiB, transcript ve yeniden +oluşturma kaynağı için ayrı ayrı 32 MiB, 100.000 SSE çerçevesi ve 10.000 yeniden +oluşturulmuş çıktı öğesidir. `stallTimeoutSec` hem ilk body byte'ını hem de +sonraki sessiz aralıkları sınırlar. Değer `0` olduğunda veya yerel upstream için +varsayılan olarak devre dışı bırakıldığında hemen zaman aşımına uğramaz; yalnızca +bağımsız 15 dakikalık toplam tur sınırı kalır. Streaming istemcileri değişmez. + İstemciye yönelik Responses SSE çerçeveleri, SSE blok sınırlayıcısından önceki ham bayt cinsinden ölçülen çerçeve başına 4 MiB ile sınırlandırılmıştır. HTTP üzerinde sınırı aşan sonlandırılmamış bir yukarı akış çerçevesi, ardından `data: 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 c600581e8d0..1695a8a6ad7 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 @@ -171,7 +171,7 @@ opencodex 路由: ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-6 ``` 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 2a102ecec5c..6f77503e6b3 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 @@ -97,7 +97,7 @@ alias Set or clear an account's display pause Hold an account out of automatic selection. resume Return a paused account to automatic selection. pause-exhausted Pause every account whose quota is spent. -clear-cooldown Drop a cooldown the proxy set after an upstream failure. +clear-cooldown Drop a cooldown the proxy set after an upstream failure. strategy [] Pool placement strategy; omit the value to read it. sticky [<1-100>] Requests a bound thread keeps on one account; omit the value to read it. priority [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it. @@ -173,10 +173,27 @@ Codex Pool 选择会清除进程本地 affinity,并从下一次请求开始生 { ok: true, provider, type, activeId } ``` +### `ocx account pause|resume anthropic [--json]` + +CLI 命令通过 id 或唯一别名暂停或恢复 Anthropic OAuth 账户。别名先精确匹配,再进行不区分大小写的匹配。CLI 和仪表板使用同一个 `PUT /api/oauth/accounts/pause`,请求体为 `{ provider: "anthropic", accountId, paused }`。`paused` 保存在账户中,并通过 `GET /api/oauth/accounts` 返回。即使主动账户池已关闭,暂停账户也会从选择、会话绑定和 429 后继候选中排除。所有账户暂停时,请求返回 403,直到恢复一个账户。已经发送的请求继续执行,凭证和健康状态保持不变。重启或重新登录仍保留暂停,删除账户时一并清除。此操作不包含账户级自动切换阈值。 + ### `ocx account clear [--json]` 在不解析账号 id 的情况下清除 Codex 账号的手动选择,因此即使存在名为 `auto` 的账号也有效。仅适用于 Codex Pool;其他提供商类型没有可恢复的自动选择。 +### `ocx account clear-cooldown [--json]` + +清除进程本地的失败冷却,但不更改已保存的凭据。Codex Pool 账号使用 `openai`,Anthropic +OAuth 账号使用 `anthropic`;其他 provider 会被拒绝。两种形式都接受账号 id 或唯一别名, +而 `main` 仅适用于 Codex Pool。 + +```bash +ocx account clear-cooldown anthropic +``` + +即使没有活动冷却,命令也会成功,JSON 中的 `cleared` 为 `false`。清除 Anthropic 冷却还会 +推进账号 generation,因此旧的 quota probe 无法恢复已清除的状态或发布过期的配额资格。 + ### `ocx account refresh [--json]` 对于 Codex 池,请使用 `ocx account refresh openai [--json]`。它会强制刷新账号配额, @@ -192,7 +209,11 @@ token,也不是简单重读账号列表。`--json` 返回 ### `ocx account auto-switch > [--json]` -控制 `openai` Codex 账户池阈值,或保存通用 OAuth 账户池阈值。`on` 保存 80%,`off` 保存 0%,`threshold ` 接受 0–100。通用池的阈值只有在 `pool.kernel` 打开且 `strategy: "fill-first"` 时才参与选择;标志关闭时,保存阈值不会启用阈值切换。两种情况下都不会改变提供方启用设置或禁用 429 错误后的轮换。通用池的查询和修改结果使用服务器确认值。通用池的 `poolEnabled` 是已保存的提供方设置,`null` 表示未指定,并不代表继承后的实际状态。`inert: true` 表示阈值已保存但未应用,`inert: false` 表示账户池正在应用它。没有 `inert` 字段表示能力未知,此时同样不会报告 `enabled: true`。API 密钥提供方、Anthropic 和无效值会被拒绝。 +控制 `openai` Codex 账户池阈值,或保存通用 OAuth 账户池阈值。`on` 保存 80%,`off` 保存 0%,`threshold ` 接受 0–100。通用池的阈值只有在 `pool.kernel` 打开且 `strategy: "fill-first"` 时才参与选择;标志关闭时,保存阈值不会启用阈值切换。两种情况下都不会改变提供方启用设置或禁用 429 错误后的轮换。通用池的查询和修改结果使用服务器确认值。通用池的 `poolEnabled` 是已保存的提供方设置,`null` 表示未指定,并不代表继承后的实际状态。`inert: true` 表示阈值已保存但未应用,`inert: false` 表示账户池正在应用它。没有 `inert` 字段表示能力未知,此时同样不会报告 `enabled: true`。API 密钥提供方和无效值会被拒绝。 + +### `ocx account auto-switch anthropic … --account ` + +Anthropic OAuth 使用 `ocx account auto-switch anthropic threshold 90 --account ` 保存账户专属整数 0–100。`off --account ` 设为 0,`on --account ` 设为 80,`inherit --account ` 恢复继承,`status --account ` 只读查询;可加 `--json`。账户卡片提供相同控制。未设置/null 继承 `anthropicAccountPool.autoSwitchThreshold`(默认 80);0 只禁用该账户按用量切换。设置在重启和重新登录后保留,删除账户时移除。手动选择、affinity、未知或全部耗尽时的后备行为与模型路由限制不变。池禁用时不应用阈值;暂停与 429 恢复仍有效。 ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md index fcbfe7585ae..32b76f3bb5c 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/agents.md @@ -10,7 +10,7 @@ description: 多代理界面、委派引导、首选模型、回退链、原生 | 字段 | 类型 | 默认值 | 含义 | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` 会把目录中的每个模型都标记为 v1;`v2` 会把每个模型都标记为 v2。`default` 会恢复上游固定值(Sol/Terra 为 v2,Luna 为 v1),否则遵循原生 `multi_agent_v2` 标志。适用于新会话。 | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | 最多五个裸原生 id、账户限定的 `/` id 或路由 `provider/model` id 会优先显示在子代理选择器中。Subagents 页面只提供裸原生和路由 id,保存时会省略精确的账户限定选项;如需精确选择,请使用 `ocx agent subagents set` 或直接编辑配置。[Astra 一次性升级](/reference/configuration/agents/#astra-roster-upgrade)后,显式空列表会被保留。 | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | 最多五个裸原生 id、账户限定的 `/` id 或路由 `provider/model` id 会优先显示在子代理选择器中。Subagents 页面只提供裸原生和路由 id,保存时会省略精确的账户限定选项;如需精确选择,请使用 `ocx agent subagents set` 或直接编辑配置。[Astra 一次性升级](/reference/configuration/agents/#astra-roster-upgrade)后,显式空列表会被保留。 | | `injectionModel?` | `string` | — | 在代理生成的 v2 委派引导中使用的首选原生或路由后的子代理模型。 | | `injectionEffort?` | `string` | — | 首选 effort(`low` 到 `ultra`),只有在 `injectionModel` 存在时才有意义。 | | `injectionPrompt?` | `string` | — | 替换内置 v2 指引正文。支持 `{{model}}`、`{{effort}}`、`{{roster}}` 和 `{{fallback}}`。只要配置了 `injectionModel`,自定义提示词就会触发。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md index 1fc4ee8ccc7..dc96d8ff4b9 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/server.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/server.md @@ -15,7 +15,7 @@ description: 监听、远程访问、准入密钥、超时、存储、侧车、 | `proxy?` | `string` | — | 出站 HTTP(S) 或 SOCKS5 代理 URL(`socks5://host:port`),或 `${ENV_VAR}`。HTTP URL 仅在未设置时写入 `HTTP_PROXY` / `HTTPS_PROXY`。SOCKS5 URL 使用内置的真实 SOCKS5 隧道,也会写入 `ALL_PROXY`(`ocx start --socks5`);并清除本进程继承的 `HTTP(S)_PROXY`。回环地址始终保留在 `NO_PROXY` 中。 | | `emptyCompletionRetry?` | `boolean` | `false` | 显式启用:当 Responses turn 既无文本也无工具调用时,使用相同请求重试一次,包括流在终止事件之前结束的情况。重试可能产生费用。`OCX_EMPTY_COMPLETION_RETRY=0` 可在不修改配置的情况下禁用;combo 与 routed-compaction turn 不参与。 | | `dropCodexSafetyBuffering?` | `boolean` | `false` | 从 Codex Responses 透传响应中移除 Codex safety-buffering 提示:`x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 响应头、类型为 `safety_buffering` 的 `response.metadata` SSE 事件,以及其他 SSE 事件中的 `safety_buffering` 字段。Codex TUI 会将这些提示显示为“使用更快模型重试”的提示框,其默认操作会把会话切换到较弱的模型。其他 `x-codex-*` 响应头和其他所有 SSE 事件内容均保持不变,但会移除该字段。默认关闭。 | -| `stallTimeoutSec?` | `number` | `300`(public)/ 禁用(local) | 上游无有效进展(Responses 和原生 Chat)多少秒后切断流。未设置时**本地**上游(loopback、private、`.local`/`.lan` 名称)默认禁用,公网上游默认 300 秒;正值对两者生效(最小 1 秒);`0` 全面禁用。`/v1/responses/compact` 的挂起响应体读取共享此预算,但即使本地上游也默认 300 秒;显式值(含 `0`)优先。 | +| `stallTimeoutSec?` | `number` | `300`(public)/ 禁用(local) | 上游无有效进展(Responses 和原生 Chat)多少秒后切断流。未设置时**本地**上游(loopback、private、`.local`/`.lan` 名称)默认禁用,公网上游默认 300 秒;正值对两者生效(最小 1 秒);`0` 全面禁用静默 watchdog。对于把 canonical ChatGPT SSE 折叠为非流式 JSON 的 Responses 请求,即使 watchdog 已禁用,仍保留独立的 15 分钟整轮上限。`/v1/responses/compact` 的挂起响应体读取共享此预算,但即使本地上游也默认 300 秒;显式值(含 `0`)优先。 | | `connectTimeoutMs?` | `number` | `200000` | 每次尝试的 DNS/TCP/TLS/最终响应头截止时间;它在正文生成之前结束。 | | `shutdownTimeoutMs?` | `number` | `5000` | 优雅停机截止时间,超过后会中止仍在进行中的请求。 | | `websockets?` | `boolean` | `false` | 声明并允许面向客户端的 Responses WebSocket 路径。设为 false 时客户端使用 HTTP/SSE;它不会禁用符合条件的 canonical ChatGPT 上游 WS 优化。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md index 6280fb8b9d8..a9809b951f2 100644 --- a/docs-site/src/content/docs/zh-cn/reference/management-api.md +++ b/docs-site/src/content/docs/zh-cn/reference/management-api.md @@ -238,6 +238,7 @@ Aside 配置档的变更在这种情况下仍会保存一件事:确认之后 | `GET, PUT, PATCH /api/oauth/accounts/pool` | 读取或更新 Anthropic OAuth 池策略 | 400 非 Anthropic provider 或策略无效 | | `POST /api/oauth/accounts/clear-cooldown` | 清除一个 OAuth 账户的运行时冷却 | 400 provider/账户无效 | | `PUT /api/oauth/accounts/alias` | 设置或清除 OAuth 账户别名 | 400 provider/账户/别名无效 | +| `PUT /api/oauth/accounts/pause` | 暂停或恢复 Anthropic 或通用 OAuth 账户。Body `{ provider, accountId, paused }`;暂停活跃账户时,如有其他可用账户则切换过去。 | 400 不支持的 provider 或无效 body;404 账户不存在;`oauth_mutation_busy` | | `GET, POST, DELETE /api/providers/keys` | 列出已脱敏的 provider 密钥,添加/激活一个,或移除一个 | 400 输入无效;404 provider/密钥缺失 | | `PUT /api/providers/keys/active` | 选择某个 provider 的活跃密钥 | 400 输入无效;404 provider/密钥缺失 | | `PUT /api/providers/keys/alias` | 设置或清除 provider 密钥别名 | 400 输入无效;404 provider/密钥缺失 | @@ -245,6 +246,10 @@ Aside 配置档的变更在这种情况下仍会保存一件事:确认之后 凭证列表响应会刻意脱敏。OAuth 访问令牌和完整的 provider API 密钥不会返回给仪表板客户端。 +#### Anthropic OAuth: `pause` / `resume` + +CLI 命令通过 id 或唯一别名暂停或恢复 Anthropic OAuth 账户。别名先精确匹配,再进行不区分大小写的匹配。CLI 和仪表板使用同一个 `PUT /api/oauth/accounts/pause`,请求体为 `{ provider: "anthropic", accountId, paused }`。`paused` 保存在账户中,并通过 `GET /api/oauth/accounts` 返回。即使主动账户池已关闭,暂停账户也会从选择、会话绑定和 429 后继候选中排除。所有账户暂停时,请求返回 403,直到恢复一个账户。已经发送的请求继续执行,凭证和健康状态保持不变。重启或重新登录仍保留暂停,删除账户时一并清除。此操作不包含账户级自动切换阈值。 + ### Providers | 方法和路径 | 用途 | 典型错误 | @@ -343,3 +348,13 @@ OpenAI 也遵循此规则:开关不会选择特殊的 922k 模式。有效上 ## 远程会话与数据密钥轮换 `POST /api/keys/rotate {id}` 开始十分钟重叠期,并只返回一次新密钥。`POST /api/keys/rotate/commit {id,rotationId}` 提交,`DELETE /api/keys/rotate {id,rotationId}` 中止。它们都需要管理认证,数据密钥不能调用。`POST /api/session/logout` 需要当前 `gui-session`、匹配的 Origin 和 CSRF。Admin token 会收到 403,永远不能创建用户同意会话。 + +## Anthropic 账户用量阈值 + +`PUT /api/oauth/accounts/auto-switch` + +仅 Anthropic OAuth。`{ provider: "anthropic", accountId, threshold }`:整数 0–100 或 null 继承;缺少字段无效。重启后保留,随账户删除。 + +DTO 包含 `autoSwitchThresholdOverride`(整数/null)、`autoSwitchThreshold`(池默认值)、`effectiveAutoSwitchThreshold`。0 只禁用按用量切换;暂停和 429 恢复不变。 + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md index c2d1871cfae..91ea424435d 100644 --- a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md +++ b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md @@ -72,6 +72,13 @@ Responses 表示是这座桥的中心。原生兼容的路由可以跳过部分 当 `stream: false` 或未提供 `stream` 时,同样的适配器事件会被收集为一个 Responses JSON 对象。两种形式都会保留所选模型、输出项、终止状态和 usage。 +canonical ChatGPT Codex 路由的上游只接受 SSE,因此仅对上游请求使用 `stream: true`。 +OpenCodex 会在有界限制内验证终止流,再将其折叠为客户端请求的 JSON 形式;显式 `store` 值不会 +改变。验证失败时会返回错误,而不会以 HTTP 200 返回部分 JSON。限制为:每帧 4 MiB、transcript +和重建源各 32 MiB、100,000 个 SSE 帧,以及 10,000 个重建输出项。`stallTimeoutSec` 同时控制 +首个 body byte 和后续静默间隔;当其为 `0`,或因本地上游默认禁用时,不会立即超时,只保留独立的 +15 分钟整轮上限。流式客户端的行为不变。 + 面向客户端的 Responses SSE 帧按 SSE 块分隔符之前的原始字节计算,每帧限制为 4 MiB。对于 HTTP,未终止的上游帧一旦超过该限制,会以合成的 `response.failed` 事件并随后发送 `data: [DONE]` 的方式 fail closed。对于 Responses WebSocket 桥,相同情况会发送 502 `websocket_protocol_error` 并取消上游 reader。已经完整到达的 Responses 终止帧具有优先权;其后的超大或格式错误字节会被丢弃,而不会把已经完成的轮次替换为传输失败。 :::note 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 5206d7066e5..3d8e58d7f55 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 @@ -260,7 +260,7 @@ opencodex 路由: ```bash ocx claude desktop bind claude-sonnet-4-6 xai/grok-4.7 -ocx claude desktop bind claude-opus-4-6 native/gpt-6-sol +ocx claude desktop bind claude-opus-4-6 native/gpt-6.1-sol ocx claude desktop unbind claude-opus-4-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 141467062a6..e35f067cd8b 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 @@ -76,7 +76,7 @@ priority [first|earlier|normal|later|last|-100..100|r pause 將帳號移出自動選擇。 resume 將暫停的帳號放回自動選擇。 pause-exhausted 暫停所有配額已用盡的帳號。 -clear-cooldown 清除上游失敗後設定的冷卻。 +clear-cooldown 清除上游失敗後設定的冷卻。 strategy [] 帳號池放置策略;省略取值即讀取目前值。 sticky [<1-100>] 已綁定執行緒在同一帳號上保留的請求數;省略取值即讀取目前值。 remove --yes 在存在檢查後移除已儲存的帳號或金鑰。 @@ -144,20 +144,37 @@ Codex 池選擇套用於清除既有親和性後的下一個請求;進行中 ### `ocx account pause|resume [--json]` -暫停或恢復 Codex 帳號池或通用 OAuth 供應商池中的單一帳號,包括 -`google-antigravity`。在 Codex 池中,`main` 僅代表 Codex 內建帳號;通用 OAuth 帳號必須用 id 或唯一別名識別。 -已暫停的通用 OAuth 帳號不會參與請求選帳、429 輪替或主動 Token 刷新,也不能手動選取。 +暫停或恢復 Codex、Anthropic 或通用 OAuth 供應商池中的單一帳號,包括 +`google-antigravity`。在 Codex 池中,`main` 僅代表 Codex 內建帳號;OAuth 帳號必須用 id 或唯一別名識別。 +已暫停的 OAuth 帳號不會參與請求選帳、429 輪替或主動 Token 刷新,也不能手動選取。 若暫停目前使用中的帳號,系統會在有其他可用帳號時切換過去。若全部帳號都已暫停, 需要該池的請求會回覆 403,直到恢復其中一個帳號。 -通用 OAuth 供應商可用帳號 id,或唯一且完全相符/不區分大小寫的別名識別帳號。 +Anthropic 和通用 OAuth 供應商可用帳號 id,或唯一且完全相符/不區分大小寫的別名識別帳號。 JSON 回應會提供帳號 id、暫停狀態與目前 active 帳號 id。 +Anthropic 暫停不受帳號池啟用開關影響,包含工作階段綁定與 429 後繼選帳。 +重新啟動或登入仍保留暫停,憑證與健康狀態不會清除,已送出的請求不會中斷。 +刪除帳號會一併刪除暫停狀態;個別帳號的自動切換門檻不在此功能範圍內。 + ```bash ocx account pause google-antigravity ocx account resume google-antigravity ``` +### `ocx account clear-cooldown [--json]` + +清除行程本地的失敗冷卻,但不變更已儲存的憑證。Codex 池帳號使用 `openai`,Anthropic +OAuth 帳號使用 `anthropic`;其他供應商會被拒絕。兩種形式都接受帳號 id 或唯一別名, +而 `main` 僅適用於 Codex 池。 + +```bash +ocx account clear-cooldown anthropic +``` + +即使沒有作用中的冷卻,命令也會成功,JSON 中的 `cleared` 為 `false`。清除 Anthropic +冷卻也會推進帳號 generation,因此舊的 quota probe 無法恢復已清除狀態或發布過期的配額資格。 + ### `ocx account refresh [--json]` 對於 Codex 池,請使用 `ocx account refresh openai [--json]`。它強制重新整理帳號配額並印出可用的週/月百分比與重置時間;缺失的配額資料被回報為未知,而非 0%。其 JSON 封裝為 `{ accounts: AccountRow[] }`,每個 Codex 列上有 `quota`。 @@ -168,7 +185,11 @@ ocx account resume google-antigravity ### `ocx account auto-switch > [--json]` -控制 `openai` Codex 帳戶池閾值,或儲存通用 OAuth 帳戶池閾值。`on` 儲存 80%,`off` 儲存 0%,`threshold ` 接受 0–100。通用池的閾值只有在 `pool.kernel` 開啟且 `strategy: "fill-first"` 時才參與選擇;旗標關閉時,儲存閾值不會啟用閾值切換。兩種情況下都不會改變供應商啟用設定或停用 429 錯誤後的輪替。通用池的查詢與修改結果使用伺服器確認值。通用池的 `poolEnabled` 是已儲存的供應商設定,`null` 表示未指定,並不代表繼承後的實際狀態。`inert: true` 表示閾值已儲存但未套用,`inert: false` 表示帳戶池正在套用它。沒有 `inert` 欄位表示能力未知,此時同樣不會回報 `enabled: true`。API 金鑰供應商、Anthropic 與無效值會被拒絕。 +控制 `openai` Codex 帳戶池閾值,或儲存通用 OAuth 帳戶池閾值。`on` 儲存 80%,`off` 儲存 0%,`threshold ` 接受 0–100。通用池的閾值只有在 `pool.kernel` 開啟且 `strategy: "fill-first"` 時才參與選擇;旗標關閉時,儲存閾值不會啟用閾值切換。兩種情況下都不會改變供應商啟用設定或停用 429 錯誤後的輪替。通用池的查詢與修改結果使用伺服器確認值。通用池的 `poolEnabled` 是已儲存的供應商設定,`null` 表示未指定,並不代表繼承後的實際狀態。`inert: true` 表示閾值已儲存但未套用,`inert: false` 表示帳戶池正在套用它。沒有 `inert` 欄位表示能力未知,此時同樣不會回報 `enabled: true`。API 金鑰供應商與無效值會被拒絕。 + +### `ocx account auto-switch anthropic … --account ` + +Anthropic OAuth 使用 `ocx account auto-switch anthropic threshold 90 --account ` 儲存帳戶專屬整數 0–100。`off --account ` 設為 0,`on --account ` 設為 80,`inherit --account ` 恢復繼承,`status --account ` 唯讀查詢;可加 `--json`。帳戶卡片提供相同控制。未設定/null 繼承 `anthropicAccountPool.autoSwitchThreshold`(預設 80);0 只停用該帳戶依用量切換。設定在重啟和重新登入後保留,刪除帳戶時移除。手動選擇、affinity、未知或全部耗盡時的後備行為與模型路由限制不變。集區停用時不套用門檻;暫停與 429 復原仍有效。 ```text openai: { provider, autoSwitchThreshold: number, enabled: boolean } diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md index 314530a4454..6f417dffa89 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/agents.md @@ -10,7 +10,7 @@ Agent 設定控制要廣告哪個 Codex 協作介面,以及 opencodex 如何 | 欄位 | 型別 | 預設值 | 意義 | | --- | --- | --- | --- | | `multiAgentMode?` | `"v1" \| "default" \| "v2"` | `"default"` | `v1` 將每個目錄模型標記為 v1;`v2` 將每個模型標記為 v2。`default` 還原上游 pin(Sol/Terra v2、Luna v1),否則遵循原生的 `multi_agent_v2` 旗標。套用於新 session。 | -| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna` | 最多五個原生或路由 id,在子代理 picker 中優先顯示。[Astra 一次性升級](/reference/configuration/agents/#astra-roster-upgrade)後,明確的空清單會被保留。 | +| `subagentModels?` | `string[]` | `gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna` | 最多五個原生或路由 id,在子代理 picker 中優先顯示。[Astra 一次性升級](/reference/configuration/agents/#astra-roster-upgrade)後,明確的空清單會被保留。 | | `injectionModel?` | `string` | — | 在代理撰寫的 v2 委派指引中使用的偏好原生或路由子代理模型。 | | `injectionEffort?` | `string` | — | 偏好 effort(`low` 到 `ultra`),僅在搭配 `injectionModel` 時有意義。 | | `injectionPrompt?` | `string` | — | 取代內建指引本文。支援 `{{model}}`、`{{effort}}`、`{{roster}}` 與 `{{fallback}}`。觸發閘門保持不變。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/server.md b/docs-site/src/content/docs/zh-tw/reference/configuration/server.md index ee1b7e72c77..dd8a0e472dc 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/server.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/server.md @@ -13,7 +13,7 @@ description: 監聽器、遠端存取、許可金鑰、逾時、儲存、sidecar | `hostname?` | `string` | `"127.0.0.1"` | 綁定位址。非回送綁定需要 `OPENCODEX_API_AUTH_TOKEN`。 | | `proxy?` | `string` | — | 對外 HTTP(S) 或 SOCKS5 代理 URL(`socks5://host:port`)或 `${ENV_VAR}`。HTTP URL 僅在那些變數未設定時套用至 `HTTP_PROXY` / `HTTPS_PROXY`。SOCKS5 URL 使用內建的真實 SOCKS5 通道,也會套用至 `ALL_PROXY`(`ocx start --socks5`),並清除此行程繼承的 `HTTP(S)_PROXY`。回送保留在 `NO_PROXY` 中。 | | `emptyCompletionRetry?` | `boolean` | `false` | 明確啟用:當 Responses 完成時沒有文字或工具呼叫,以相同請求重試一次。重試可能產生費用。`OCX_EMPTY_COMPLETION_RETRY=0` 可在不變更設定的情況下停用;combo 與 routed-compaction turn 不適用。 | -| `stallTimeoutSec?` | `number` | `300`(public)/ 停用(local) | 上游無有效進展(Responses 與原生 Chat)多少秒後切斷串流。未設定時**本地**上游(loopback、private、`.local`/`.lan` 名稱)預設停用,公網上游預設 300 秒;正值對兩者生效(最小 1 秒);`0` 全面停用。`/v1/responses/compact` 的擱置回應本文讀取共用此預算,但即使本地上游也預設 300 秒;明確值(含 `0`)優先。 | +| `stallTimeoutSec?` | `number` | `300`(public)/ 停用(local) | 上游無有效進展(Responses 與原生 Chat)多少秒後切斷串流。未設定時**本地**上游(loopback、private、`.local`/`.lan` 名稱)預設停用,公網上游預設 300 秒;正值對兩者生效(最小 1 秒);`0` 全面停用靜默 watchdog。對於把 canonical ChatGPT SSE 折疊為非串流 JSON 的 Responses 請求,即使 watchdog 已停用,仍保留獨立的 15 分鐘整體上限。`/v1/responses/compact` 的擱置回應本文讀取共用此預算,但即使本地上游也預設 300 秒;明確值(含 `0`)優先。 | | `connectTimeoutMs?` | `number` | `200000` | 每次嘗試的 DNS/TCP/TLS/final-header 截止時間;它在 body 生成前結束。 | | `shutdownTimeoutMs?` | `number` | `5000` | 在中止活躍回合前的優雅排空截止時間。 | | `websockets?` | `boolean` | `false` | 廣告並允許面向 client 的 Responses WebSocket 路徑。False 時 client 使用 HTTP/SSE;不會停用符合條件的 canonical ChatGPT upstream WS 最佳化。 | diff --git a/docs-site/src/content/docs/zh-tw/reference/management-api.md b/docs-site/src/content/docs/zh-tw/reference/management-api.md index 9e3549dbb42..2ef37691a42 100644 --- a/docs-site/src/content/docs/zh-tw/reference/management-api.md +++ b/docs-site/src/content/docs/zh-tw/reference/management-api.md @@ -233,10 +233,10 @@ Aside 設定檔的變更在這種情況下仍會儲存一件事:確認之後 | `POST /api/oauth/login/cancel` | 取消公開進行中的 OAuth 流程 | 400 未知供應商 | | `GET /api/oauth/status` | 輪詢一個供應商的 OAuth 流程 | 400 未知供應商 | | `POST /api/oauth/logout` | 移除所選的供應商憑證 | 400 未知供應商;`oauth_mutation_busy` | -| `GET /api/oauth/accounts` | 列出遮罩帳號;通用 OAuth 帳號列也會提供 `paused` 狀態。Kiro 列包含自動選取狀態 `autoSelectable`,排除時還包含封閉集合的 `skipReason`。唯一的有效帳號仍可傳送請求,配額查詢仍為選用。 | 400 無效供應商 | +| `GET /api/oauth/accounts` | 列出遮罩帳號;Anthropic 與通用 OAuth 帳號列也會提供 `paused` 狀態。Kiro 列包含自動選取狀態 `autoSelectable`,排除時還包含封閉集合的 `skipReason`。唯一的有效帳號仍可傳送請求,配額查詢仍為選用。 | 400 無效供應商 | | `DELETE /api/oauth/accounts` | 移除一個帳號 | 400 無效供應商/id;404 帳號缺失;`oauth_mutation_busy` | | `PUT /api/oauth/accounts/active` | 選擇現用 OAuth 帳號 | 400 無效供應商/帳號;404 帳號缺失;409 帳號已暫停;`oauth_mutation_busy` | -| `PUT /api/oauth/accounts/pause` | 暫停或恢復一個通用 OAuth 帳號。Body `{ provider, accountId, paused }`;若暫停現用帳號,且有可用帳號,會切換至下一個 | 400 不支援的供應商或無效 body;404 帳號缺失;`oauth_mutation_busy` | +| `PUT /api/oauth/accounts/pause` | 暫停或恢復一個 Anthropic 或通用 OAuth 帳號。Body `{ provider, accountId, paused }`;若暫停現用帳號,且有可用帳號,會切換至下一個。暫停會持久儲存且不受帳號池開關影響,恢復保留健康狀態與憑證。 | 400 不支援的供應商或無效 body;404 帳號缺失;`oauth_mutation_busy` | | `GET, PUT, PATCH /api/oauth/accounts/pool` | 讀取或更新 Anthropic OAuth 池政策 | 400 非 Anthropic 供應商或無效政策 | | `POST /api/oauth/accounts/clear-cooldown` | 清除一個 OAuth 帳號的 runtime 冷卻 | 400 無效供應商/帳號 | | `PUT /api/oauth/accounts/alias` | 設定或清除 OAuth 帳號別名 | 400 無效供應商/帳號/別名 | @@ -327,3 +327,13 @@ OpenAI 也遵循此規則:開關不會選擇特殊的 922k 模式。生效中 ## 遠端工作階段與資料金鑰輪替 `POST /api/keys/rotate {id}` 開始十分鐘重疊期,且只回傳一次新金鑰。`POST /api/keys/rotate/commit {id,rotationId}` 提交,`DELETE /api/keys/rotate {id,rotationId}` 中止。全部都需要管理驗證,資料金鑰不能呼叫。`POST /api/session/logout` 需要目前的 `gui-session`、相符的 Origin 與 CSRF。Admin token 會收到 403,永遠不能建立使用者同意工作階段。 + +## Anthropic 帳戶用量門檻 + +`PUT /api/oauth/accounts/auto-switch` + +僅 Anthropic OAuth。`{ provider: "anthropic", accountId, threshold }`:整數 0–100 或 null 繼承;缺少欄位無效。重啟後保留,隨帳戶刪除。 + +DTO 包含 `autoSwitchThresholdOverride`(整數/null)、`autoSwitchThreshold`(集區預設值)、`effectiveAutoSwitchThreshold`。0 只停用依用量切換;暫停和 429 復原不變。 + +HTTP: 400 invalid/unsupported; 404 missing account; `oauth_mutation_busy` on lock contention. diff --git a/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md b/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md index 8e90432b06f..5f1ca4e8a34 100644 --- a/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md +++ b/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md @@ -63,6 +63,13 @@ Responses 表示是橋接的中心。原生相容的路由可跳過部分轉譯 在 `stream: false` 或無 `stream` 時,相同的 adapter 事件被收集為一個 Responses JSON 物件。兩種形式都保留所選模型、輸出項目、終端狀態與 usage。 +canonical ChatGPT Codex 路由的上游只接受 SSE,因此僅對上游請求使用 `stream: true`。OpenCodex +會在有界限制內驗證終端串流,再將其折疊成客戶端要求的 JSON 形式;明確的 `store` 值不會改變。 +驗證失敗時會傳回錯誤,而不會以 HTTP 200 傳回部分 JSON。限制為每個 frame 4 MiB、transcript +與重建來源各 32 MiB、100,000 個 SSE frame,以及 10,000 個重建 output item。 +`stallTimeoutSec` 同時控制第一個 body byte 與後續靜默間隔;當它是 `0`,或因本機 upstream +預設停用時,不會立即逾時,只保留獨立的 15 分鐘整體上限。串流客戶端的行為不變。 + 每個終端 Responses usage 物件都包含兩個 detail 物件,即使供應商未回報那些細節: ```json diff --git a/gui/src/components/AccountAutoSwitchControl.tsx b/gui/src/components/AccountAutoSwitchControl.tsx index 6f0343bdb44..078b65dec46 100644 --- a/gui/src/components/AccountAutoSwitchControl.tsx +++ b/gui/src/components/AccountAutoSwitchControl.tsx @@ -10,6 +10,7 @@ export interface AccountAutoSwitchControlProps { override: number | null; disabled?: boolean; inputId: string; + hintText?: string; onChange(threshold: number | null): Promise; } @@ -20,6 +21,7 @@ export default function AccountAutoSwitchControl({ override, disabled = false, inputId, + hintText, onChange, }: AccountAutoSwitchControlProps) { const t = useT(); @@ -40,7 +42,7 @@ export default function AccountAutoSwitchControl({ draft: String(current.override ?? globalThreshold), })); const blocked = disabled || saving; - const hint = t("accountPool.autoSwitchHint"); + const hint = hintText ?? t("accountPool.autoSwitchHint"); const hintId = useId(); const write = async (next: number | null) => { diff --git a/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx b/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx index 84120b54c41..15b5e20b9e1 100644 --- a/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx +++ b/gui/src/components/provider-workspace/AnthropicAccountPoolSettings.tsx @@ -2,7 +2,7 @@ * Opt-in Anthropic OAuth account pool controls (#294). * Experimental — shows a strong warning because the feature is not battle-tested. */ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { useT } from "../../i18n/shared"; import { getPoolSettings, putPoolSettings } from "../../pool-settings"; import { @@ -37,9 +37,11 @@ type PoolState = { export default function AnthropicAccountPoolSettings({ apiBase, accountCount, + onThresholdChange, }: { apiBase: string; accountCount: number; + onThresholdChange?: (threshold: number) => void; }) { const t = useT(); const [state, setState] = useState(null); @@ -48,6 +50,24 @@ export default function AnthropicAccountPoolSettings({ const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [loadError, setLoadError] = useState(false); + const onThresholdChangeRef = useRef(onThresholdChange); + const mountedRef = useRef(true); + const apiBaseRef = useRef(apiBase); + const saveAbortRef = useRef(null); + + useLayoutEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + saveAbortRef.current?.abort(); + saveAbortRef.current = null; + }; + }, []); + + useLayoutEffect(() => { + onThresholdChangeRef.current = onThresholdChange; + apiBaseRef.current = apiBase; + }, [apiBase, onThresholdChange]); useEffect(() => { let cancelled = false; @@ -79,6 +99,7 @@ export default function AnthropicAccountPoolSettings({ quotaWindow: normalizeAccountPoolQuotaWindow(json.quotaWindow), }); setDraft(String(nextThreshold)); + onThresholdChangeRef.current?.(nextThreshold); setStickyDraft(String(nextSticky)); setLoadError(false); }) @@ -99,6 +120,12 @@ export default function AnthropicAccountPoolSettings({ stickyLimit: number; quotaWindow: AccountPoolQuotaWindow; }) => { + const requestApiBase = apiBase; + saveAbortRef.current?.abort(); + const controller = new AbortController(); + saveAbortRef.current = controller; + const currentRequest = () => mountedRef.current && apiBaseRef.current === requestApiBase + && saveAbortRef.current === controller && !controller.signal.aborted; const previousState = state; setState({ enabled: next.enabled, @@ -112,27 +139,31 @@ export default function AnthropicAccountPoolSettings({ try { // The client owns the field mapping: `threshold` becomes `autoSwitchThreshold` and the // provider is always sent, so no call site can forget either. - const json = await putPoolSettings(apiBase, "anthropic", { + const json = await putPoolSettings(requestApiBase, "anthropic", { enabled: next.enabled, threshold: next.threshold, strategy: next.strategy, stickyLimit: next.stickyLimit, quotaWindow: next.quotaWindow, - }); + }, (input, init) => fetch(input, init), { signal: controller.signal }); + if (!currentRequest()) return; if (!json) throw new Error("save"); + const savedThreshold = typeof json.autoSwitchThreshold === "number" ? json.autoSwitchThreshold : next.threshold; const savedStrategy = normalizeAccountPoolStrategy(json?.strategy ?? next.strategy); const savedSticky = normalizeAccountPoolStickyLimit(json?.stickyLimit ?? next.stickyLimit); const savedWindow = normalizeAccountPoolQuotaWindow(json?.quotaWindow ?? next.quotaWindow); setState({ enabled: next.enabled, - threshold: next.threshold, + threshold: savedThreshold, strategy: savedStrategy, stickyLimit: savedSticky, quotaWindow: savedWindow, }); - setDraft(String(next.threshold)); + setDraft(String(savedThreshold)); + onThresholdChangeRef.current?.(savedThreshold); setStickyDraft(String(savedSticky)); } catch { + if (!currentRequest()) return; setError(t("anthropicPool.saveFailed")); if (previousState) { setState(previousState); @@ -140,7 +171,9 @@ export default function AnthropicAccountPoolSettings({ setStickyDraft(String(previousState.stickyLimit)); } } finally { - setSaving(false); + const ownsSave = saveAbortRef.current === controller; + if (ownsSave) saveAbortRef.current = null; + if (ownsSave && mountedRef.current && apiBaseRef.current === requestApiBase) setSaving(false); } }, [apiBase, state, t]); diff --git a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx index 58b7eb214e8..440d30653f0 100644 --- a/gui/src/components/provider-workspace/ProviderAuthPanel.tsx +++ b/gui/src/components/provider-workspace/ProviderAuthPanel.tsx @@ -9,6 +9,7 @@ import { IconLock, IconRefresh, IconTrash } from "../../icons"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { oauthAccountDisplayLabel, providerAuthSurface } from "../../provider-workspace/auth"; import { displayAccountId } from "../../lib/privacy"; +import AccountAutoSwitchControl from "../AccountAutoSwitchControl"; import { formatOAuthHealthLabel, formatOAuthHealthSummary, @@ -428,7 +429,12 @@ export default function ProviderAuthPanel({ {isOauth && ( <> {item.name === "anthropic" && ( - + { void authHandlers?.onAccountPoolThreshold?.(item.name, threshold); }} + /> )} {item.name === "google-antigravity" && (
@@ -621,6 +627,16 @@ export default function ProviderAuthPanel({
+ {item.name === "anthropic" && account.autoSwitchThresholdOverride !== undefined + && account.autoSwitchThreshold !== undefined && authHandlers.onAccountThreshold && ( + authHandlers.onAccountThreshold!(item.name, account, threshold)} + /> + )}
diff --git a/gui/src/components/provider-workspace/types.ts b/gui/src/components/provider-workspace/types.ts index 0fbca409b7c..697e43114ea 100644 --- a/gui/src/components/provider-workspace/types.ts +++ b/gui/src/components/provider-workspace/types.ts @@ -61,6 +61,9 @@ export type OAuthAccountRow = AccountQuotaReading & { autoSelectable?: boolean; skipReason?: "needs_reauth" | "paused" | "suspended" | "cooldown" | "quota_exhausted"; paused?: boolean; + autoSwitchThresholdOverride?: number | null; + autoSwitchThreshold?: number; + effectiveAutoSwitchThreshold?: number; health?: { status: OAuthAccountHealthStatus; reason?: string; until?: string }; healthLabel?: string; healthSummary?: string; @@ -91,6 +94,8 @@ export interface ProviderAuthHandlers { onReauth: (provider: string, accountId?: string) => void | Promise; onSwitchAccount: (provider: string, account: OAuthAccountRow) => void | Promise; onPauseAccount: (provider: string, account: OAuthAccountRow, paused: boolean) => void | Promise; + onAccountThreshold?: (provider: string, account: OAuthAccountRow, threshold: number | null) => Promise; + onAccountPoolThreshold?: (provider: string, threshold: number) => void | Promise; onRemoveAccount: (provider: string, account: OAuthAccountRow) => void | Promise; onRetryAccounts?: (provider: string) => void | Promise; onAddApiKey: (provider: string, key: string) => Promise; diff --git a/gui/src/hooks/useProviderAccountPools.ts b/gui/src/hooks/useProviderAccountPools.ts index a901a0ae133..d9a04b3e453 100644 --- a/gui/src/hooks/useProviderAccountPools.ts +++ b/gui/src/hooks/useProviderAccountPools.ts @@ -23,6 +23,9 @@ export interface OAuthAccount extends AccountQuotaReading { autoSelectable?: boolean; skipReason?: "needs_reauth" | "paused" | "suspended" | "cooldown" | "quota_exhausted"; paused?: boolean; + autoSwitchThresholdOverride?: number | null; + autoSwitchThreshold?: number; + effectiveAutoSwitchThreshold?: number; expiresAt?: number; health?: { status: "healthy" | "cooldown" | "reauth_required" | "warning"; reason?: string; until?: string }; healthLabel?: string; @@ -362,8 +365,28 @@ export function useProviderAccountPools(deps: { return key; }; + const setAccountPoolThreshold = async (provider: string, threshold: number): Promise => { + if (!aliveRef.current || !mountedRef.current || serverRef.current !== apiBase) return false; + // Pool settings and roster reads describe one server value. Invalidate older reads before + // publishing the confirmed save, then refresh so a concurrent external write can still win. + invalidateSelectionReads(provider, "oauth"); + setAccountSets(current => { + const existing = current[provider]; + return !existing ? current : { ...current, [provider]: { ...existing, + accounts: existing.accounts.map(row => ({ ...row, + autoSwitchThreshold: threshold, + effectiveAutoSwitchThreshold: typeof row.autoSwitchThresholdOverride === "number" + ? row.autoSwitchThresholdOverride : threshold, + })) } }; + }); + // Restart the full roster path, not only the cheap membership read. The settings card can + // resolve before the initial account load; cancelling that load without replacing its quota + // enrichment would leave usage bars empty until a manual refresh or remount. + return fetchAccountSets([provider]); + }; + const switchAccount = async (provider: string, account: OAuthAccount) => { - if (account.active || account.needsReauth || account.paused || switchingAccountRef.current || pausingAccountRef.current) return; + if (account.active || account.needsReauth || account.paused || switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(`oauth:${provider}`)) return; const target = { provider, accountId: account.id }; switchingAccountRef.current = target; setSwitchingAccount(target); @@ -403,8 +426,58 @@ export function useProviderAccountPools(deps: { } }; + const setAccountThreshold = async (provider: string, account: OAuthAccount, threshold: number | null): Promise => { + const key = `oauth:${provider}`; + if (switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(key)) return false; + const mutationKey = invalidateSelectionReads(provider, "oauth"); + const mutation = Symbol(); + selectionMutationsRef.current.set(mutationKey, mutation); + const currentMutation = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase + && selectionMutationsRef.current.get(mutationKey) === mutation; + const label = oauthAccountDisplayLabel(accountSets[provider]?.accounts ?? [account], account, t); + try { + const bounded = createBoundedFetch(20_000); + requestsRef.current.add(bounded.controller); + let result: Pick; + try { + const res = await fetch(`${apiBase}/api/oauth/accounts/auto-switch`, { + method: "PUT", headers: { "Content-Type": "application/json" }, signal: bounded.signal, + body: JSON.stringify({ provider, accountId: account.id, threshold }), + }); + if (!res.ok) throw new Error("account threshold write failed"); + result = await res.json() as typeof result; + if (bounded.signal.aborted) throw new Error("account threshold deadline exceeded"); + } finally { + bounded.clear(); + requestsRef.current.delete(bounded.controller); + } + const validPercent = (value: unknown) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100; + if (!result || (result.autoSwitchThresholdOverride !== null && !validPercent(result.autoSwitchThresholdOverride)) + || !validPercent(result.autoSwitchThreshold) || !validPercent(result.effectiveAutoSwitchThreshold)) throw new Error("invalid threshold response"); + if (!currentMutation()) return false; + invalidateSelectionReads(provider, "oauth"); + setAccountSets(current => { + const existing = current[provider]; + return !existing ? current : { ...current, [provider]: { ...existing, + accounts: existing.accounts.map(row => row.id === account.id ? { ...row, + autoSwitchThresholdOverride: result.autoSwitchThresholdOverride, + autoSwitchThreshold: result.autoSwitchThreshold, effectiveAutoSwitchThreshold: result.effectiveAutoSwitchThreshold } : row) } }; + }); + return true; + } catch { + if (currentMutation()) notify(t("accountPool.autoSwitchUpdateFailed", { email: label }), false); + return false; + } finally { + if (currentMutation()) { + invalidateSelectionReads(provider, "oauth"); + selectionMutationsRef.current.delete(mutationKey); + void refreshAccountRosters({ provider, kind: "oauth" }); + } + } + }; + const pauseAccount = async (provider: string, account: OAuthAccount, paused: boolean) => { - if (switchingAccountRef.current || pausingAccountRef.current) return; + if (switchingAccountRef.current || pausingAccountRef.current || selectionMutationsRef.current.has(`oauth:${provider}`)) return; const target = { provider, accountId: account.id }; pausingAccountRef.current = target; setPausingAccount({ ...target, paused }); @@ -623,7 +696,7 @@ export function useProviderAccountPools(deps: { return { accountSets, accountLoadStates, switchingAccount, pausingAccount, openAccounts, keyPools, addingKeyFor, newKeyValue, setAccountSets, setAccountLoadStates, setSwitchingAccount, setOpenAccounts, setKeyPools, setAddingKeyFor, setNewKeyValue, - fetchAccountSets, fetchKeyPools, refreshAccountRosters, switchAccount, pauseAccount, switchApiKey, removeApiKey, addApiKeyValue, addApiKey, editCredentialAlias, removeAccount, + fetchAccountSets, fetchKeyPools, refreshAccountRosters, switchAccount, pauseAccount, setAccountPoolThreshold, setAccountThreshold, switchApiKey, removeApiKey, addApiKeyValue, addApiKey, editCredentialAlias, removeAccount, oauthCardProviders, keyCardProviders, activeAccountNeedsReauth, }; } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 42ef7f400fc..6ddb8ba1d59 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -5,6 +5,7 @@ import type { TKey } from "./en"; * German i18n catalog, generated from en.ts. Must match the `TKey` set (compile-checked). */ export const de: Record = { + "pws.anthropicAccountThresholdHint": "Überschreibt den Standard des Claude-Pools. 0 deaktiviert den nutzungsbasierten Wechsel nur für dieses Konto; Pause und Wiederherstellung bei Ratenlimits gelten weiterhin.", "kiroLogin.title": "Bei Kiro anmelden", "kiroLogin.chooseMethod": "Anmeldemethode wählen", "kiroLogin.cli": "Mit Kiro CLI anmelden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 11db36231fa..cd0bf7c4953 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -6,6 +6,7 @@ * `{var}` are plain interpolations. */ export const en = { + "pws.anthropicAccountThresholdHint": "Overrides the Claude pool default. 0 disables usage-based switching only for this account; pause and rate-limit recovery still apply.", "kiroLogin.title": "Sign in to Kiro", "kiroLogin.chooseMethod": "Choose a sign-in method", "kiroLogin.cli": "Kiro CLI", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index fdc2b1be7de..b146c51151c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * French i18n catalog. Must match the `TKey` set. */ export const fr: Record = { + "pws.anthropicAccountThresholdHint": "Remplace le seuil par défaut du pool Claude. 0 désactive le basculement selon l’utilisation uniquement pour ce compte ; la pause et la reprise après limitation restent actives.", "kiroLogin.title": "Se connecter à Kiro", "kiroLogin.chooseMethod": "Choisir une méthode de connexion", "kiroLogin.cli": "Importer avec Kiro CLI", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 256be8bae52..479576d4484 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Japanese i18n catalog; must match the `TKey` set (compile-checked). */ export const ja: Record = { + "pws.anthropicAccountThresholdHint": "Claude プールの既定値を上書きします。0 はこのアカウントだけで使用量による切り替えを無効にします。一時停止とレート制限からの復旧は引き続き適用されます。", "kiroLogin.title": "Kiro にログイン", "kiroLogin.chooseMethod": "ログイン方法を選択", "kiroLogin.cli": "Kiro CLI から取り込む", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 843d22de50e..55ad082da30 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Korean i18n catalog; must match the `TKey` set (compile-checked). */ export const ko: Record = { + "pws.anthropicAccountThresholdHint": "Claude 풀 기본값을 재정의합니다. 0은 이 계정의 사용량 기반 전환만 끄며, 일시정지와 요청 제한 복구는 계속 적용됩니다.", "kiroLogin.title": "Kiro에 로그인", "kiroLogin.chooseMethod": "로그인 방법 선택", "kiroLogin.cli": "Kiro CLI에서 가져오기", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index f9e134bb6aa..545641e5a71 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Russian i18n catalog; must match the `TKey` set (compile-checked). */ export const ru: Record = { + "pws.anthropicAccountThresholdHint": "Переопределяет порог пула Claude для этого аккаунта. 0 отключает переключение по использованию только для этого аккаунта; пауза и восстановление после 429 продолжают работать.", "kiroLogin.title": "Войти в Kiro", "kiroLogin.chooseMethod": "Выберите способ входа", "kiroLogin.cli": "Импортировать через Kiro CLI", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 70f22b95bee..6de30bf8766 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -5,6 +5,7 @@ import type { TKey } from "./en"; * Turkish i18n catalog. Must match the `TKey` set (compile-checked). */ export const tr: Record = { + "pws.anthropicAccountThresholdHint": "Claude havuzunun varsayılan eşiğini geçersiz kılar. 0, yalnızca bu hesap için kullanıma dayalı geçişi kapatır; duraklatma ve hız sınırı kurtarması geçerliliğini korur.", "kiroLogin.title": "Kiro oturumu aç", "kiroLogin.chooseMethod": "Oturum açma yöntemi seç", "kiroLogin.cli": "Kiro CLI ile içe aktar", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 59ec009d37d..23a799cd258 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -6,6 +6,7 @@ import type { TKey } from "./en"; * Technical terms and model identifiers intentionally remain English. */ export const vi: Record = { + "pws.anthropicAccountThresholdHint": "Ghi đè ngưỡng mặc định của nhóm Claude. 0 chỉ tắt chuyển đổi dựa trên mức sử dụng của tài khoản này; tạm dừng và khôi phục khi bị giới hạn vẫn áp dụng.", "kiroLogin.title": "Đăng nhập Kiro", "kiroLogin.chooseMethod": "Chọn cách đăng nhập", "kiroLogin.cli": "Nhập từ Kiro CLI", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 6be095cb4a4..b8b8786e243 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2,6 +2,7 @@ import type { TKey } from "./en"; /** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */ export const zhTW: Record = { + "pws.anthropicAccountThresholdHint": "覆寫 Claude 集區預設門檻。0 僅停用此帳戶的依用量切換;暫停和速率限制復原仍然適用。", "kiroLogin.title": "登入 Kiro", "kiroLogin.chooseMethod": "選擇登入方式", "kiroLogin.cli": "從 Kiro CLI 匯入", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 446b16bb0d0..c93b513d65a 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Chinese i18n catalog; must match the `TKey` set (compile-checked). */ export const zh: Record = { + "pws.anthropicAccountThresholdHint": "覆盖 Claude 池默认阈值。0 仅禁用此账户的按用量切换;暂停和速率限制恢复仍然生效。", "kiroLogin.title": "登录 Kiro", "kiroLogin.chooseMethod": "选择登录方式", "kiroLogin.cli": "从 Kiro CLI 导入", diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx index da2c7787f9e..f45544e0376 100644 --- a/gui/src/pages/Providers.tsx +++ b/gui/src/pages/Providers.tsx @@ -386,7 +386,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { const { accountSets, setAccountSets, accountLoadStates, switchingAccount, pausingAccount, keyPools, fetchAccountSets, fetchKeyPools, refreshAccountRosters, oauthCardProviders, keyCardProviders, - switchAccount, pauseAccount, switchApiKey, removeApiKey, addApiKeyValue, editCredentialAlias, + switchAccount, pauseAccount, setAccountPoolThreshold, setAccountThreshold, switchApiKey, removeApiKey, addApiKeyValue, editCredentialAlias, removeAccount, activeAccountNeedsReauth, } = pools; const refreshSelection = useCallback((target?: AccountSelectionTarget) => { @@ -659,6 +659,8 @@ export default function Providers({ apiBase }: { apiBase: string }) { onReauth: (provider, accountId) => requestLoginOAuth(provider, true, accountId), onSwitchAccount: switchAccount, onPauseAccount: pauseAccount, + onAccountPoolThreshold: setAccountPoolThreshold, + onAccountThreshold: setAccountThreshold, onRemoveAccount: removeAccount, onRetryAccounts: async provider => { await fetchAccountSets([provider]); }, onAddApiKey: addApiKeyValue, diff --git a/gui/tests/anthropic-pool-quota-window.test.tsx b/gui/tests/anthropic-pool-quota-window.test.tsx index c8b2a86c8fc..c50ae3506de 100644 --- a/gui/tests/anthropic-pool-quota-window.test.tsx +++ b/gui/tests/anthropic-pool-quota-window.test.tsx @@ -79,7 +79,7 @@ function stubPool(initial: PoolPayload): Record[] { return puts; } -async function mountPool(): Promise { +async function mountPool(onThresholdChange?: (threshold: number) => void): Promise { const host = testWindow.document.createElement("div"); testWindow.document.body.appendChild(host as never); const { createRoot } = await import("react-dom/client"); @@ -88,7 +88,7 @@ async function mountPool(): Promise { mountedRoots.push(root); root.render( - + , ); }); @@ -120,6 +120,43 @@ afterEach(async () => { }); describe("Anthropic account pool quota window", () => { + test("only confirmed pool defaults seed account override controls", async () => { + let fail = false; + globalThis.fetch = (async (_input, init) => init?.method === "PUT" + ? fail ? new Response(null, { status: 500 }) : Response.json({ enabled: false, autoSwitchThreshold: 73 }) + : Response.json({ enabled: true, autoSwitchThreshold: 64, strategy: "quota", stickyLimit: 1, quotaWindow: "five-hour" })) as typeof fetch; + const values: number[] = []; + const host = await mountPool(value => { values.push(value); }); + expect(values).toEqual([64]); + const toggle = host.querySelector('button[aria-pressed]') as HTMLButtonElement; + await act(async () => { toggle.click(); await flush(); }); + expect(values).toEqual([64, 73]); + fail = true; + await act(async () => { toggle.click(); await flush(); }); + expect(values).toEqual([64, 73]); + }); + + test("an unmounted settings card aborts its save without publishing the old server value", async () => { + let aborted = false; + globalThis.fetch = (async (_input, init) => { + if (init?.method !== "PUT") return Response.json({ enabled: true, autoSwitchThreshold: 64, strategy: "quota", stickyLimit: 1, quotaWindow: "five-hour" }); + return new Promise((_resolve, reject) => { + init.signal?.addEventListener("abort", () => { + aborted = true; + reject(new Error("aborted")); + }, { once: true }); + }); + }) as typeof fetch; + const values: number[] = []; + const host = await mountPool(value => { values.push(value); }); + const toggle = host.querySelector('button[aria-pressed]') as HTMLButtonElement; + await act(async () => { toggle.click(); await Promise.resolve(); }); + const root = mountedRoots.pop(); + await act(async () => { root?.unmount(); await Promise.resolve(); }); + expect(aborted).toBe(true); + expect(values).toEqual([64]); + }); + test("quota window selector renders for quota and fill-first strategies", async () => { stubPool({ enabled: true, diff --git a/gui/tests/provider-account-pause-refresh.test.tsx b/gui/tests/provider-account-pause-refresh.test.tsx index 3bcdf9cee69..9580be1e6d0 100644 --- a/gui/tests/provider-account-pause-refresh.test.tsx +++ b/gui/tests/provider-account-pause-refresh.test.tsx @@ -54,6 +54,92 @@ afterEach(async () => { } }); +test("confirmed threshold persists in UI when follow-up read fails; failed writes preserve prior state", async () => { + let fail = false; const bodies: unknown[] = []; + respond = async (_url, init) => { + if (init?.method !== "PUT") return new Response(null, { status: 503 }); + bodies.push(JSON.parse(String(init.body))); + return fail ? new Response(null, { status: 500 }) : Response.json({ autoSwitchThresholdOverride: 0, autoSwitchThreshold: 70, effectiveAutoSwitchThreshold: 0 }); + }; + await act(async () => { expect(await pools.setAccountThreshold("fixture", row("b", false), 0)).toBe(true); }); + expect(pools.accountSets.fixture.accounts[1]?.autoSwitchThresholdOverride).toBe(0); + expect(bodies[0]).toEqual({ provider: "fixture", accountId: "b", threshold: 0 }); + fail = true; + await act(async () => { expect(await pools.setAccountThreshold("fixture", row("b", false), null)).toBe(false); }); + expect(pools.accountSets.fixture.accounts[1]?.autoSwitchThresholdOverride).toBe(0); + expect(notices.some(notice => notice.key === "accountPool.autoSwitchUpdateFailed")).toBe(true); +}); + +test("pending threshold owns its roster generation and blocks conflicting pause", async () => { + let settle!: (response: Response) => void; let writes = 0; + respond = async (_url, init) => { + if (init?.method !== "PUT") return new Response(null, { status: 503 }); + writes++; return new Promise(resolve => { settle = resolve; }); + }; + let pending!: Promise; + await act(async () => { pending = pools.setAccountThreshold("fixture", row("b", false), 40); }); + await act(async () => { await pools.pauseAccount("fixture", row("b", false), true); }); + expect(writes).toBe(1); + await act(async () => { settle(Response.json({ autoSwitchThresholdOverride: 40, autoSwitchThreshold: 70, effectiveAutoSwitchThreshold: 40 })); await pending; }); + expect(pools.accountSets.fixture.accounts[1]?.autoSwitchThresholdOverride).toBe(40); +}); + +test("a confirmed pool threshold invalidates an older roster while later external changes still win", async () => { + let settleStale!: (response: Response) => void; + let reads = 0; + const urls: string[] = []; + respond = async (url, init) => { + if (init?.method === "PUT") return new Response(null, { status: 500 }); + urls.push(url); + reads++; + if (reads === 1) return new Promise(resolve => { settleStale = resolve; }); + const threshold = reads <= 3 ? 70 : 55; + return Response.json({ activeAccountId: "a", accounts: [ + { ...row("a", true), quotaMode: "probe", autoSwitchThresholdOverride: null, autoSwitchThreshold: threshold, effectiveAutoSwitchThreshold: threshold }, + { ...row("b", false), quotaMode: "probe", autoSwitchThresholdOverride: 40, autoSwitchThreshold: threshold, effectiveAutoSwitchThreshold: 40 }, + ] }); + }; + + let stale!: Promise; + await act(async () => { stale = pools.refreshAccountRosters({ provider: "fixture", kind: "oauth" }); }); + await act(async () => { expect(await pools.setAccountPoolThreshold("fixture", 70)).toBe(true); }); + await act(async () => { await Promise.resolve(); }); + expect(urls.some(url => url.includes("quota=1"))).toBe(true); + expect(pools.accountSets.fixture.accounts[0]?.autoSwitchThreshold).toBe(70); + expect(pools.accountSets.fixture.accounts[1]?.effectiveAutoSwitchThreshold).toBe(40); + + await act(async () => { + settleStale(Response.json({ activeAccountId: "a", accounts: [ + { ...row("a", true), quotaMode: "probe", autoSwitchThresholdOverride: null, autoSwitchThreshold: 65, effectiveAutoSwitchThreshold: 65 }, + { ...row("b", false), quotaMode: "probe", autoSwitchThresholdOverride: 40, autoSwitchThreshold: 65, effectiveAutoSwitchThreshold: 40 }, + ] })); + await stale; + }); + expect(pools.accountSets.fixture.accounts[0]?.autoSwitchThreshold).toBe(70); + + await act(async () => { expect(await pools.refreshAccountRosters({ provider: "fixture", kind: "oauth" })).toBe(true); }); + expect(pools.accountSets.fixture.accounts[0]?.autoSwitchThreshold).toBe(55); +}); + +test("a stalled account threshold write is aborted when the hook unmounts", async () => { + let aborted = false; + respond = async (_url, init) => new Promise((_resolve, reject) => { + expect(init?.signal).toBeInstanceOf(AbortSignal); + init?.signal?.addEventListener("abort", () => { + aborted = true; + reject(new Error("aborted")); + }, { once: true }); + }); + let pending!: Promise; + await act(async () => { + pending = pools.setAccountThreshold("fixture", row("b", false), 40); + await Promise.resolve(); + }); + await act(async () => { root?.unmount(); root = null; }); + expect(await pending).toBe(false); + expect(aborted).toBe(true); +}); + test("a saved pause stays visible and only the failed roster refresh is reported", async () => { respond = async (_url, init) => init?.method === "PUT" ? Response.json({ ok: true, activeAccountId: "a", activeAccountChanged: false }) @@ -86,4 +172,3 @@ test("a rejected save reports the pause failure and leaves the row unpaused", as expect(pools.accountSets.fixture.accounts.find(account => account.id === "b")?.paused).toBe(false); expect(notices).toEqual([{ key: "codexAuth.pauseFailed", ok: false }]); }); - diff --git a/gui/tests/provider-quota-refresh-controls.test.tsx b/gui/tests/provider-quota-refresh-controls.test.tsx index 5766221a13f..0bab2820572 100644 --- a/gui/tests/provider-quota-refresh-controls.test.tsx +++ b/gui/tests/provider-quota-refresh-controls.test.tsx @@ -92,6 +92,23 @@ test("the usage tab reports the real outcome, not the click", async () => { expect(host.textContent).toContain("Quota check completed"); }); +test("Anthropic account threshold editor uses its pool default, preserves zero, and resets with null", async () => { + const calls: Array = []; + const item = { ...oauthItem, name: "anthropic", adapter: "anthropic" }; + const handlers = authHandlers({ onAccountThreshold: async (_provider, _account, value) => { calls.push(value); return true; } }); + const row = { id: "threshold-account", active: true, autoSwitchThresholdOverride: null, autoSwitchThreshold: 65 }; + await render(); + const toggle = () => host.querySelector('[aria-label^="Override global usage threshold"]') as HTMLButtonElement; + expect(toggle()).not.toBeNull(); + await act(async () => { toggle().click(); }); expect(calls).toEqual([65]); + await render(); + const input = host.querySelector('#anthropic-threshold-threshold-account') as HTMLInputElement; + expect(input.value).toBe("0"); + await act(async () => { toggle().click(); }); expect(calls).toEqual([65, null]); + await render(); + expect(toggle()).toBeNull(); // Old servers do not acquire a synthetic capability. +}); + test("a failed read is reported as a failure", async () => { const { handler, settle } = deferredHandler(); await render(); @@ -172,12 +189,12 @@ test("the accounts surface omits the control when the page cannot force a read", expect(findButton("Refresh quotas")).toBeNull(); }); -test("generic OAuth accounts expose pause and resume controls", async () => { +test.each(["google-antigravity", "anthropic"])("%s OAuth accounts expose pause and resume controls", async provider => { const calls: Array<{ provider: string; accountId: string; paused: boolean }> = []; const handlers = authHandlers({ onPauseAccount: async (provider, row, paused) => { calls.push({ provider, accountId: row.id, paused }); }, }); - const item = { ...oauthItem, name: "google-antigravity" }; + const item = { ...oauthItem, name: provider }; await render( { const pause = findButton("Pause"); expect(pause).not.toBeNull(); await act(async () => { pause!.click(); }); - expect(calls).toEqual([{ provider: "google-antigravity", accountId: "ga-active", paused: true }]); + expect(calls).toEqual([{ provider, accountId: "ga-active", paused: true }]); await render( { expect(host.textContent).not.toContain(en["codexAuth.pausedHint"]); expect(en["pws.accountPausedHint"]).not.toBe(en["codexAuth.pausedHint"]); await act(async () => { resume!.click(); }); - expect(calls[1]).toEqual({ provider: "google-antigravity", accountId: "ga-active", paused: false }); + expect(calls[1]).toEqual({ provider, accountId: "ga-active", paused: false }); }); test("API-key rows use independent shared credit readings and the same awaited refresh control", async () => { diff --git a/package.json b/package.json index ab92796acfa..9bf34d94c31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.72.0", + "version": "2.73.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", diff --git a/scripts/build-standalone.ts b/scripts/build-standalone.ts index fdbed98e128..bf023f5609a 100644 --- a/scripts/build-standalone.ts +++ b/scripts/build-standalone.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve, basename } from "node:path"; import { isStandaloneTarget, standaloneExecutableName } from "./standalone-targets"; +import { stageStandaloneKeyringAddon } from "./standalone-keyring"; function hostTarget(): string { const platform = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux"; @@ -77,6 +78,10 @@ try { } if (compileExitCode !== 0) process.exit(compileExitCode); +// N-API binaries cannot execute from Bun's virtual `$bunfs`. Keep the exact target addon outside +// the compiled executable so source/npm resolution and packaged resolution share one binding API. +stageStandaloneKeyringAddon(repoRoot, output, target); + // bun's ad-hoc linker signature does not always cover the embedded payload; // macOS kills the executable on launch (SIGKILL) unless it is re-signed. if (process.platform === "darwin") { diff --git a/scripts/ci/docker-smoke.ts b/scripts/ci/docker-smoke.ts index e108d129e42..a8424f93986 100644 --- a/scripts/ci/docker-smoke.ts +++ b/scripts/ci/docker-smoke.ts @@ -224,8 +224,8 @@ const stateProbe = ` const additions = { appOwnedMemoryBudgetMb: 256, fastRows: true, managementUsageMaxReadBytes: 67108864, openaiProviderTierVersion: 2, - subagentModels: ['gpt-6-astra', 'gpt-6-sol', 'gpt-6-luna'], - subagentModelsVersion: 2, + subagentModels: ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-luna'], + subagentModelsVersion: 3, }; for (const config of [persisted, loaded]) { if (Object.keys(config).some(key => !Object.hasOwn(seed, key) && !Object.hasOwn(additions, key))) throw new Error('unexpected startup config addition'); diff --git a/scripts/keyring-smoke.ts b/scripts/keyring-smoke.ts index 1135887451c..cd3be07ad57 100644 --- a/scripts/keyring-smoke.ts +++ b/scripts/keyring-smoke.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; +import { loadKeyringBinding } from "../src/lib/keyring-native"; export interface KeyringSmokeEntry { setSecret(secret: Uint8Array, signal?: AbortSignal): Promise; @@ -15,8 +16,8 @@ export interface KeyringSmokeOptions { } async function createOsEntry(service: string, account: string): Promise { - const { AsyncEntry } = await import("@napi-rs/keyring"); - return new AsyncEntry(service, account); + const { AsyncEntry } = loadKeyringBinding(); + return new AsyncEntry(service, account) as unknown as KeyringSmokeEntry; } export async function runKeyringSmoke({ diff --git a/scripts/model-metadata.source.json b/scripts/model-metadata.source.json index cfa750e50a1..53ad2fc6556 100644 --- a/scripts/model-metadata.source.json +++ b/scripts/model-metadata.source.json @@ -2885,6 +2885,31 @@ "maxLevel": "max" } }, + "openai.gpt-6.1-sol": { + "id": "openai.gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "bedrock-converse-stream", + "provider": "amazon-bedrock", + "baseUrl": "https://bedrock-runtime.us-east-1.amazonaws.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "minLevel": "low", + "maxLevel": "max" + } + }, "openai.gpt-5.6-terra": { "id": "openai.gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -5870,6 +5895,31 @@ "maxLevel": "max" } }, + "openai/gpt-6.1-sol": { + "id": "openai/gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "anthropic-messages", + "provider": "cloudflare-ai-gateway", + "baseUrl": "https://gateway.ai.cloudflare.com/v1///anthropic", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 0 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "minLevel": "low", + "maxLevel": "max" + } + }, "openai/gpt-5.6-terra": { "id": "openai/gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -11435,6 +11485,34 @@ "maxLevel": "max" } }, + "gpt-6.1-sol": { + "id": "gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "openai-responses", + "provider": "github-copilot", + "baseUrl": "https://api.githubcopilot.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "headers": { + "User-Agent": "opencode/1.3.15" + }, + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "gpt-5.6-terra": { "id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -20930,6 +21008,31 @@ "maxLevel": "max" } }, + "openai/gpt-6.1-sol": { + "id": "openai/gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "openai-completions", + "provider": "kilo", + "baseUrl": "https://api.kilo.ai/api/gateway", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "openai/gpt-5.6-sol-pro": { "id": "openai/gpt-5.6-sol-pro", "name": "OpenAI: GPT-5.6 Sol Pro", @@ -60134,6 +60237,32 @@ "maxLevel": "max" } }, + "gpt-6.1-sol": { + "id": "gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "api": "openai-responses", + "provider": "openai", + "baseUrl": "", + "reasoning": true, + "input": [ + "text", + "image" + ], + "contextWindow": 373000, + "maxTokens": 128000, + "applyPatchToolType": "freeform", + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "gpt-5.6-terra": { "id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -60943,6 +61072,33 @@ "maxLevel": "max" } }, + "gpt-6.1-sol": { + "id": "gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "api": "openai-codex-responses", + "provider": "openai-codex", + "baseUrl": "https://chatgpt.com/backend-api", + "reasoning": true, + "input": [ + "text", + "image" + ], + "contextWindow": 373000, + "maxTokens": 128000, + "preferWebsockets": true, + "applyPatchToolType": "freeform", + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "gpt-5.6-terra": { "id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -63249,6 +63405,31 @@ "maxLevel": "max" } }, + "gpt-6.1-sol": { + "id": "gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "openai-responses", + "provider": "opencode-zen", + "baseUrl": "https://opencode.ai/zen/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.2, + "cacheWrite": 2.5 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "gpt-5.6-terra": { "id": "gpt-5.6-terra", "name": "GPT-5.6 Terra", @@ -69393,6 +69574,31 @@ "maxLevel": "max" } }, + "openai/gpt-6.1-sol": { + "id": "openai/gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "openai-completions", + "provider": "openrouter", + "baseUrl": "https://openrouter.ai/api/v1", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "effort", + "minLevel": "low", + "maxLevel": "max" + } + }, "openai/gpt-5.6-sol-pro": { "id": "openai/gpt-5.6-sol-pro", "name": "OpenAI: GPT-5.6 Sol Pro", @@ -80831,6 +81037,31 @@ "maxLevel": "max" } }, + "openai/gpt-6.1-sol": { + "id": "openai/gpt-6.1-sol", + "name": "GPT-6.1 Sol", + "api": "anthropic-messages", + "provider": "vercel-ai-gateway", + "baseUrl": "https://ai-gateway.vercel.sh", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 2, + "output": 10, + "cacheRead": 0.1, + "cacheWrite": 2.5 + }, + "contextWindow": 373000, + "maxTokens": 128000, + "thinking": { + "mode": "budget", + "minLevel": "low", + "maxLevel": "max" + } + }, "openai/gpt-5.6-terra": { "id": "openai/gpt-5.6-terra", "name": "GPT-5.6 Terra", diff --git a/scripts/standalone-keyring.ts b/scripts/standalone-keyring.ts new file mode 100644 index 00000000000..a444d698ec8 --- /dev/null +++ b/scripts/standalone-keyring.ts @@ -0,0 +1,30 @@ +import { copyFileSync, existsSync, mkdirSync } from "node:fs"; +import { createRequire } from "node:module"; +import { basename, join } from "node:path"; +import { keyringAssetForStandaloneTarget } from "../src/lib/keyring-native"; + +/** Stage the platform N-API addon outside Bun's virtual filesystem beside a standalone binary. */ +export function stageStandaloneKeyringAddon(repoRoot: string, output: string, target: string): string { + const asset = keyringAssetForStandaloneTarget(target); + if (!asset) throw new Error(`No keyring native asset is declared for standalone target ${target}`); + // Resolve optional target packages from their declaring wrapper. This preserves the lockfile + // relationship without depending on Bun/npm/pnpm choosing a particular hoisting layout. + const projectRequire = createRequire(join(repoRoot, "package.json")); + const keyringRequire = createRequire(projectRequire.resolve("@napi-rs/keyring")); + let source: string; + try { + source = keyringRequire.resolve(asset.packageName); + } catch { + throw new Error( + `Missing ${asset.packageName}/${asset.filename}; install target optional dependencies before building ${target}`, + ); + } + if (basename(source) !== asset.filename || !existsSync(source)) { + throw new Error(`Resolved ${asset.packageName} to an unexpected native asset: ${source}`); + } + const keyringDir = join(output, "keyring"); + mkdirSync(keyringDir, { recursive: true }); + const destination = join(keyringDir, asset.filename); + copyFileSync(source, destination); + return destination; +} diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index fd19e2040b0..3fc00931e08 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -6,7 +6,7 @@ "codex-quota-query-backoff.test.ts": "codex-integration", "pnpm-command-isolation.test.ts": "update", "provider-antigravity-quota-retry.test.ts": "providers", "project-config-warning-snapshot.test.ts": "codex-integration", "codex-quota-auto-refresh-generation.test.ts": "codex-integration", "codex-account-clear-paused.test.ts": "codex-integration", "low-quota-protection.test.ts": "codex-integration", - "responses-compaction-recovery.test.ts": "responses", "compaction-recovery-settings.test.ts": "config", "responses-compaction-recovery-policy.test.ts": "responses", "plugin-loader.test.ts": "lib", "plugin-upstream-hooks.test.ts": "lib", + "responses-compaction-recovery.test.ts": "responses", "responses-canonical-nonstream.test.ts": "responses", "compaction-recovery-settings.test.ts": "config", "responses-compaction-recovery-policy.test.ts": "responses", "plugin-loader.test.ts": "lib", "plugin-upstream-hooks.test.ts": "lib", "cli-kiro-auto-selection.test.ts": "cli", "codebuddy-live-models.test.ts": "providers", "kiro-auto-selection.test.ts": "providers/kiro", "kiro-quota-metrics.test.ts": "providers/kiro", "management-provider-request-pacing.test.ts": "server", "desktop-supervised-restart.test.ts": "clients", "cli-restart-handoff.test.ts": "cli", "restart-replacement.test.ts": "server", "deepseek-artifact-tool-schema.test.ts": "providers", @@ -100,10 +100,16 @@ "ambiguous-resend-composition.test.ts": "lib", "ambiguous-resend-gate.test.ts": "lib", "anthropic-account-pool.test.ts": "adapters/anthropic", + "anthropic-account-pause-outbound.test.ts": "adapters/anthropic", + "anthropic-account-pause.test.ts": "adapters/anthropic", + "anthropic-account-threshold.test.ts": "adapters/anthropic", + "anthropic-combo-account-cooldown.test.ts": "adapters/anthropic", + "cli-anthropic-account-threshold.test.ts": "cli", "anthropic-model-routes.test.ts": "adapters/anthropic", "anthropic-agentrouter-language-framing.test.ts": "adapters/anthropic", "anthropic-baseurl-override.test.ts": "adapters/anthropic", "anthropic-compatible-stream.test.ts": "adapters/anthropic", + "anthropic-cooldown-recovery.test.ts": "adapters/anthropic", "anthropic-empty-content.test.ts": "adapters/anthropic", "anthropic-eof-tolerance.test.ts": "adapters/anthropic", "anthropic-error-body.test.ts": "adapters/anthropic", @@ -866,6 +872,7 @@ "google-wire-compiler.test.ts": "adapters/google", "google-wire-shape.test.ts": "adapters/google", "gpt6-native-rows.test.ts": "codex-integration", + "gpt61-sol-rows.test.ts": "codex-integration", "grok-attribution.test.ts": "providers/xai", "grok-config-inject.test.ts": "providers/xai", "grok-effort-inject.test.ts": "providers/xai", @@ -1197,6 +1204,7 @@ "openai-chat-native-policy.test.ts": "adapters/openai", "openai-chat-parallel-stream.test.ts": "adapters/openai", "openai-chat-path-override.test.ts": "adapters/openai", + "openai-chat-qwen38-leading-system.test.ts": "adapters/openai", "openai-chat-reasoning-wire-policy.test.ts": "adapters/openai", "openai-chat-sanitization-review-regressions.test.ts": "adapters/openai", "openai-chat-serialized-tool-call-content.test.ts": "adapters/openai", @@ -1213,6 +1221,7 @@ "openai-provider-option-tooling.test.ts": "adapters/openai", "openai-provider-option.test.ts": "adapters/openai", "openai-responses-passthrough.test.ts": "responses", + "openai-responses-summary-none.test.ts": "responses", "responses-forward-output-cap.test.ts": "responses", "opencode-cli.test.ts": "providers", "opencode-free-provider.test.ts": "providers", @@ -1229,6 +1238,7 @@ "openrouter-provider-routing.test.ts": "providers", "openrouter-quota-reset-cooldown-4024.test.ts": "providers", "opper-provider.test.ts": "providers", + "tokenlab-protocols.test.ts": "providers", "tokenlab-provider.test.ts": "providers", "optional-shutdown-hooks.test.ts": "lib", "orca-import.test.ts": "codex-integration", diff --git a/scripts/test-layout/seeds.json b/scripts/test-layout/seeds.json index 394627e716b..806bdabba99 100644 --- a/scripts/test-layout/seeds.json +++ b/scripts/test-layout/seeds.json @@ -55,12 +55,13 @@ "^(?:anthropic)-" ], "openai": [ - "^(?:openai)-" + "^openai-(?!responses-summary-none\\.test\\.ts$)" ] } }, "responses": { "match": [ + "^openai-responses-summary-none\\.test\\.ts$", "^(?:apply|chat|citation|continuation|eventstream|legacy|namespace|passthrough|responses|sse|thought|ws)-" ] }, diff --git a/skills/ocx/references/01_management_surface.md b/skills/ocx/references/01_management_surface.md index 1945073ed2b..15c52cd6c71 100644 --- a/skills/ocx/references/01_management_surface.md +++ b/skills/ocx/references/01_management_surface.md @@ -803,7 +803,7 @@ JSON mode: `payload`. ### `ocx account pause` -Exclude one account in a Codex or supported generic OAuth pool from automatic selection. +Exclude one account in a Codex, Anthropic or supported generic OAuth pool from automatic selection. | Method | Route | |---|---| @@ -817,11 +817,11 @@ Exclude one account in a Codex or supported generic OAuth pool from automatic se JSON mode: `envelope`. -- Codex pause unbinds pinned threads and selects a fallback when possible; with no fallback, a paused-but-selected Codex account still receives requests. Generic OAuth pause never dispatches to that account: it is excluded from new requests, failover and refresh, and an all-paused pool answers 403. Anthropic is unsupported. +- Codex pause unbinds pinned threads and selects a fallback when possible; with no fallback, a paused-but-selected Codex account still receives requests. Anthropic and generic OAuth pause exclude the account from new requests, failover and refresh, and an all-paused pool answers 403. Credentials and health are preserved; already-sent turns are not cancelled. ### `ocx account resume` -Return a paused account to a Codex or supported generic OAuth pool. +Return a paused account to a Codex, Anthropic or supported generic OAuth pool. | Method | Route | |---|---| @@ -919,15 +919,19 @@ Show or set the usage percentage at which a pool moves to another account. | PUT | `/api/codex-auth/auto-switch` | | GET | `/api/oauth/accounts/pool` | | PUT | `/api/oauth/accounts/pool` | +| GET | `/api/oauth/accounts` | +| PUT | `/api/oauth/accounts/auto-switch` | | Flag | Value | Meaning | |---|---|---| | `--json` | boolean | Emit the stored threshold and whether it is applied. | +| `--account` | string | Anthropic account ID; inherit restores the pool default, off stores zero. | JSON mode: `envelope`. - A bare invocation reads and never writes. - `on` stores 80%, `off` stores 0%, and `threshold ` accepts 0-100. +- Anthropic requires --account ; inherit sends null to restore its pool default. Manual/affinity precedence and pool-off recovery are unchanged. - For a generic OAuth pool, `inert: true` means the threshold is stored but not applied, `inert: false` means the pool is applying it, and an absent `inert` is an unknown capability. ### `ocx storage cleanup` diff --git a/src/adapters/cursor/native-exec.ts b/src/adapters/cursor/native-exec.ts index 51817ee2a0b..f08e98a9313 100644 --- a/src/adapters/cursor/native-exec.ts +++ b/src/adapters/cursor/native-exec.ts @@ -54,7 +54,8 @@ import { import { clientBytes, execBytes, execStreamCloseBytes, execThrowBytes } from "./native-exec-common"; import type { McpToolDefinition } from "./gen/agent_pb"; import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions"; -import { cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorToolWireName } from "./tool-naming"; +import { CODEX_UNIFIED_EXEC_TOOL, cursorRequestHasExecutionPath, cursorRequestHasShellAlias, cursorRequestUsesCodeMode, cursorToolWireName } from "./tool-naming"; +import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize"; import type { OcxTool } from "../../types"; export type CursorNativeExecDeps = CursorNativeNetworkDeps & CursorNativeToolDeps; @@ -84,20 +85,23 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps { const REDIRECT_HINT_MAX_TOOLS = 16; /** - * Redirect text for Cursor-native fs/shell/fetch attempts when the request catalog carries NO shell - * bridge or other execution-path tool (an orchestrator client that only exposes delegation tools, - * for example). The default refusal steers the model to `shell_command` / `exec_command`; when those - * are not in the catalog some models (kimi-k3 observed) conclude every tool is unavailable and give - * up instead of using the tools that ARE listed. Name the real catalog instead — the client tools - * plus any configured MCP tools advertised this turn — and stay neutral about what those tools can - * do, so a listed file/search/fetch tool is never contradicted. + * Catalog-aware redirect for denied Cursor-native fs/shell/fetch attempts. Code mode must point + * inside freeform `exec`, since the default refusal names top-level shell tools it does not expose. + * When no execution path exists, name the actual client and configured MCP tools instead and stay + * neutral about their capabilities, so a listed file/search/fetch tool is never contradicted. */ export function cursorNativeExecRedirectHint( - tools: readonly Pick[] | undefined, + tools: readonly Pick[] | undefined, mcpToolDefs: readonly Pick[] = [], ): string | undefined { const clientTools = tools ?? []; - if (cursorRequestHasShellAlias(clientTools) || cursorRequestHasExecutionPath(clientTools)) return undefined; + if (cursorRequestHasShellAlias(clientTools)) return undefined; + // Code mode (freeform unified `exec`, no bare shell bridge): the default bridge wording names + // top-level shell tools this catalog does not expose, so the model probes for tools that cannot + // exist. Redirect INSIDE `exec` instead — shell, file, search, and fetch are nested helpers of + // the code cell. The caller supplies the active-turn catalog, so no tool_choice re-filter here. + if (cursorRequestUsesCodeMode(clientTools)) return cursorCodeModeExecRedirectHint(); + if (cursorRequestHasExecutionPath(clientTools)) return undefined; // Client tools are advertised under OCX_RESPONSES_TOOL_PROVIDER, so the harness shows them as // `mcp__`; configured MCP servers are advertised under their own provider id. // A request with no client tools but configured MCP tools still gets those named; a request that @@ -118,6 +122,20 @@ export function cursorNativeExecRedirectHint( ); } +/** + * Code-mode half of the redirect above: the only execution surface is the freeform `exec` cell, + * so the denial names the nested helpers instead of the missing flat bridge. The result-echo + * sentence is the shared canonical wording tool-guidance emits for the same isolate. + */ +function cursorCodeModeExecRedirectHint(): string { + return ( + `Re-issue this operation NOW through the \`${CODEX_UNIFIED_EXEC_TOOL}\` tool: this turn uses Codex code mode, so \`${CODEX_UNIFIED_EXEC_TOOL}\` takes a JavaScript body and shell, file, search, and fetch are nested helpers called INSIDE that body as \`await tools.(...)\`, for example \`text(await tools.exec_command({cmd: "ls"}))\`. ` + + "Cursor-native Read/Glob/Grep/LS/Shell/Write/Fetch are not part of this request's catalog; do not retry them, and do not call `shell_command` or `exec_command` at the top level here — code mode exposes no bare shell bridge, only the nested helpers. Every other tool this turn lists remains callable at the top level as usual. " + + CODE_MODE_RESULT_ECHO_SENTENCE + " " + + `Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the \`${CODEX_UNIFIED_EXEC_TOOL}\` call.` + ); +} + export function cursorUnsafeNativeLocalExecEnabled(input: Pick = {}): boolean { return input.unsafeAllowNativeLocalExec === true; } diff --git a/src/adapters/devin/live-models.ts b/src/adapters/devin/live-models.ts index cb37c6e39e5..5b72bd56ce2 100644 --- a/src/adapters/devin/live-models.ts +++ b/src/adapters/devin/live-models.ts @@ -24,6 +24,9 @@ export const DEVIN_STATIC_MODELS = [ // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows. "gpt-6-sol", "gpt-6-luna", + // 260930 preemptive: GPT-6.1 Sol (devin.ai/blog/gpt-6-1-sol says it is live; the uid is not published). + // Spelled the way Devin spells gpt-5.6-sol; live discovery replaces this seed once a credential is present. + "gpt-6-1-sol", "claude-opus-4-8", "claude-fable-5-1", "claude-sonnet-5", @@ -61,6 +64,8 @@ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record = { "gpt-6-astra": 1_000_000, "gpt-6-sol": 1_000_000, "gpt-6-luna": 1_000_000, + // 260930 preemptive: unmeasured; mirrors the GPT-6 Sol row Cognition serves. + "gpt-6-1-sol": 1_000_000, "claude-opus-4-8": 1_000_000, // 260923: read from the live catalog (devin/claude-opus-5-5 context_length 1_000_000). "claude-opus-5-5": 1_000_000, diff --git a/src/adapters/kiro/reasoning.ts b/src/adapters/kiro/reasoning.ts index 6906d2fbe25..55a9c1ca192 100644 --- a/src/adapters/kiro/reasoning.ts +++ b/src/adapters/kiro/reasoning.ts @@ -26,6 +26,8 @@ export const KIRO_NATIVE_EFFORT_FIELDS: Record 0) next.reasoning = rest; + else delete next.reasoning; + return next; +} + export function stripUnsupportedReasoningSummaryDelivery(body: unknown, modelId: string): unknown { if (catalogModelSupportsReasoningSummaries(modelId) !== false) return body; if (!isPlainObject(body) || !isPlainObject(body.stream_options)) return body; diff --git a/src/bridge/internal.ts b/src/bridge/internal.ts index cd126ca60aa..4b1543d1f13 100644 --- a/src/bridge/internal.ts +++ b/src/bridge/internal.ts @@ -15,6 +15,7 @@ import { type OcxErrorPayload, } from "../lib/errors"; import { redactSecretString } from "../lib/redact"; +import { formatRetryAfterAdvice } from "../lib/retry-delay"; import { usageDisplayTotalTokens } from "../usage/totals"; export function uuid(): string { @@ -144,6 +145,17 @@ export function adapterFailureFromEvent(event: Extract { + const selector = args.indexOf("--account"); + const accountId = selector >= 0 ? args[selector + 1] : undefined; + if (selector >= 0) args.splice(selector, 2); + let threshold: number | null | undefined; + if (action === "inherit" && args.length === 0) threshold = null; + else if (action === "off" && args.length === 0) threshold = 0; + else if (action === "on" && args.length === 0) threshold = 80; + else if (action === "threshold" && args.length === 1 && /^\d+$/.test(args[0]!)) threshold = Number(args[0]); + else if (action !== "status" || args.length !== 0) return invalid(); + if (!accountId?.trim() || accountId.startsWith("--") || (threshold !== undefined && threshold !== null && threshold > 100)) return invalid(); + const base = await resolveBaseUrl(deps); + if (!base) return proxyUnreachable(); + const response = action === "status" + ? await apiJson(deps, base, "GET", "/api/oauth/accounts?provider=anthropic") + : await apiJson(deps, base, "PUT", "/api/oauth/accounts/auto-switch", { provider: "anthropic", accountId, threshold }); + if (response.status === 0) return proxyUnreachable(response.transportError); + if (response.status !== 200) return apiError(response.json, "failed to update account threshold", response.status); + if (!response.json || typeof response.json !== "object" || Array.isArray(response.json)) return apiError({}, "invalid account threshold response", 400); + const result = action === "status" + ? (Array.isArray(response.json.accounts) ? response.json.accounts : []).find((row: { id?: string } | null) => row?.id === accountId) + : response.json; + if (!result || typeof result !== "object") return apiError({}, "account not found", 404); + if (!Object.hasOwn(result, "autoSwitchThresholdOverride")) return apiError({}, "proxy does not support Anthropic account thresholds; upgrade and restart it", 400); + const validPercent = (value: unknown) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 100; + if ((result.autoSwitchThresholdOverride !== null && !validPercent(result.autoSwitchThresholdOverride)) + || !validPercent(result.effectiveAutoSwitchThreshold)) return apiError({}, "invalid account threshold response", 400); + const payload = { provider: "anthropic", accountId, autoSwitchThresholdOverride: result.autoSwitchThresholdOverride, + effectiveAutoSwitchThreshold: result.effectiveAutoSwitchThreshold }; + if (wantsJson) console.log(JSON.stringify(payload, null, 2)); + else console.log(`auto-switch: ${payload.autoSwitchThresholdOverride === null ? "inherited" : "custom"} (${payload.effectiveAutoSwitchThreshold === 0 ? "usage-based switching disabled" : `${payload.effectiveAutoSwitchThreshold}%`})`); + return 0; +} + +function invalid(): number { + console.error("Usage: ocx account auto-switch anthropic > --account [--json]"); + return 2; +} diff --git a/src/cli/account-api.ts b/src/cli/account-api.ts index 43199c43a23..6ad19b3105a 100644 --- a/src/cli/account-api.ts +++ b/src/cli/account-api.ts @@ -347,6 +347,7 @@ interface OAuthAccountDto { needsReauth?: boolean; /** Present only for providers that support operator pause (generic OAuth pools). */ paused?: boolean; + autoSwitchThresholdOverride?: number | null; autoSelectable?: boolean; skipReason?: unknown; /** Always sent by the management route; explicitly `null` when the tier is unknown. */ @@ -388,6 +389,7 @@ async function fetchOAuthRows( active: a.active ?? a.id === activeId, needsReauth: a.needsReauth, ...(a.paused === true ? { paused: true } : {}), + ...(name === "anthropic" && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}), ...(name === "kiro" && typeof a.autoSelectable === "boolean" ? { autoSelectable: a.autoSelectable } : {}), ...(name === "kiro" && a.autoSelectable === false && isKiroSkipReason(a.skipReason) diff --git a/src/cli/account-extended.ts b/src/cli/account-extended.ts index 4fce819000d..cf38d7b032f 100644 --- a/src/cli/account-extended.ts +++ b/src/cli/account-extended.ts @@ -1,4 +1,5 @@ import { loadConfig } from "../config"; +import { cmdAnthropicAccountThreshold } from "./account-anthropic-threshold"; import { isReservedCodexAccountWord, reportCodexAccountTargetError, resolveCodexAccountTarget } from "./account-target"; import { hasPassiveAccountQuota } from "../providers/quota"; import { closeSync, openSync, readSync, readFileSync, statSync } from "node:fs"; @@ -40,6 +41,7 @@ const AUTO_NOTE = "auto (no pin — lowest-usage account is selected per request const EXTENDED_USAGE = `Usage: ocx account refresh [--json] ocx account auto-switch > [--json] + ocx account auto-switch anthropic > --account [--json] ocx account alias [--json] ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json] ocx account pause [--json] @@ -358,6 +360,7 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise< const classified = configAndType(deps, name); // Anthropic keeps its threshold on its own pool contract; generic OAuth providers (#695) // and the Codex pool are accepted here. + if (!("error" in classified) && classified.type === "oauth" && name === "anthropic") return cmdAnthropicAccountThreshold(args, action, wantsJson, deps); if ("error" in classified || classified.type === "api-key" || name === "anthropic") { return usage("Error: auto-switch only applies to the openai Codex account pool or a generic OAuth provider pool"); } @@ -627,16 +630,7 @@ export async function cmdImport(args: string[], deps: AccountDeps): Promise 0 || result.unsupportedCount > 0 ? 1 : 0; } -/** - * Lift a quota cooldown on a Codex account. - * - * This is the user-facing escape from the lockout described in - * `devlog/_plan/260726_cooldown_lockout_hardening`: injected routing makes the proxy the - * only model path for Codex Desktop, so a stuck cooldown reads as "the whole app is dead". - * - * Codex accounts only. API-key pools already reset their own 429 cooldowns through key - * management (`clearKeyCooldowns`), and OAuth providers have no equivalent state here. - */ +/** Lift a process-local cooldown through the management route that owns that pool. */ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promise { const wantsJson = flag(args, "--json"); const name = args.shift(); @@ -644,16 +638,34 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi if (!name || !requestedId || args.length) return usage(); const classified = configAndType(deps, name); if ("error" in classified) return usage(`Error: ${classified.error}`); - if (classified.type !== "codex") { - return usage(`Error: ${name} is not a Codex account pool; cooldown clearing applies to Codex accounts only`); + if (classified.type !== "codex" && !(classified.type === "oauth" && name === "anthropic")) { + return usage(`Error: ${name} has no operator-clearable account cooldown`); } const baseUrl = await resolveBaseUrl(deps); if (!baseUrl) return proxyUnreachable(); - const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId); - if ("networkDown" in target) return proxyUnreachable(target.transportError); - if ("error" in target) return reportCodexAccountTargetError(target); - const id = target.id; - const response = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/accounts/clear-cooldown", { id }); + let id: string; + let response: Awaited>; + if (classified.type === "codex") { + const target = await resolveCodexAccountTarget(deps, baseUrl, requestedId); + if ("networkDown" in target) return proxyUnreachable(target.transportError); + if ("error" in target) return reportCodexAccountTargetError(target); + id = target.id; + response = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/accounts/clear-cooldown", { id }); + } else { + const list = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts?provider=${encodeURIComponent(name)}`); + if (list.status === 0) return proxyUnreachable(list.transportError); + if (list.status !== 200) return apiError(list.json, `failed to list ${name} OAuth accounts`, list.status); + const target = resolveGenericOAuthAccountTarget( + Array.isArray(list.json.accounts) ? list.json.accounts : [], + requestedId, + ); + if ("error" in target) return usage(`Error: ${target.error}`); + id = target.id; + response = await apiJson(deps, baseUrl, "POST", "/api/oauth/accounts/clear-cooldown", { + provider: name, + accountId: id, + }); + } if (response.status === 0) return proxyUnreachable(response.transportError); if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}`, response.status); const cleared = response.json?.cleared === true; @@ -773,7 +785,7 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise typeof value === "object" && value !== null && typeof (value as { id?: unknown }).id === "string", ); @@ -787,7 +799,7 @@ function resolveGenericOAuthPauseTarget(accounts: unknown[], requested: string): return { error: `Account not found: no OAuth account has the id or alias "${requested}"` }; } -/** Pause or resume a Codex account or a generic OAuth provider account. */ +/** Pause or resume a Codex or OAuth provider account, including Anthropic. */ export async function cmdPause(args: string[], deps: AccountDeps, paused: boolean): Promise { const wantsJson = flag(args, "--json"); const name = args.shift(); @@ -800,11 +812,10 @@ export async function cmdPause(args: string[], deps: AccountDeps, paused: boolea if (!baseUrl) return proxyUnreachable(); if (classified.type === "oauth") { - if (name === "anthropic") return usage(`Error: ${verb} is not supported for the Anthropic OAuth pool`); const list = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts?provider=${encodeURIComponent(name)}`); if (list.status === 0) return proxyUnreachable(list.transportError); if (list.status !== 200) return apiError(list.json, `failed to list ${name} OAuth accounts`, list.status); - const target = resolveGenericOAuthPauseTarget(Array.isArray(list.json.accounts) ? list.json.accounts : [], requestedId); + const target = resolveGenericOAuthAccountTarget(Array.isArray(list.json.accounts) ? list.json.accounts : [], requestedId); if ("error" in target) return usage(`Error: ${target.error}`); const response = await apiJson(deps, baseUrl, "PUT", "/api/oauth/accounts/pause", { diff --git a/src/cli/account.ts b/src/cli/account.ts index ab9a2089f7d..c702bd48fbb 100644 --- a/src/cli/account.ts +++ b/src/cli/account.ts @@ -49,6 +49,7 @@ const ACCOUNT_USAGE = `Usage: ocx account clear [--json] ocx account refresh [--json] ocx account auto-switch > [--json] + ocx account auto-switch anthropic > --account [--json] ocx account alias [--json] ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json] ocx account pause [--json] diff --git a/src/cli/capabilities.ts b/src/cli/capabilities.ts index 86fecb5a5f0..484454a802f 100644 --- a/src/cli/capabilities.ts +++ b/src/cli/capabilities.ts @@ -541,7 +541,7 @@ export const CAPABILITIES: readonly Capability[] = [ }, { command: ["account", "pause"], - summary: "Exclude one account in a Codex or supported generic OAuth pool from automatic selection.", + summary: "Exclude one account in a Codex, Anthropic or supported generic OAuth pool from automatic selection.", // Resume uses the same endpoints with `paused: false`. routes: [ { method: "PUT", path: "/api/codex-auth/accounts/pause" }, @@ -552,12 +552,12 @@ export const CAPABILITIES: readonly Capability[] = [ mutates: true, json: "envelope", details: [ - "Codex pause unbinds pinned threads and selects a fallback when possible; with no fallback, a paused-but-selected Codex account still receives requests. Generic OAuth pause never dispatches to that account: it is excluded from new requests, failover and refresh, and an all-paused pool answers 403. Anthropic is unsupported.", + "Codex pause unbinds pinned threads and selects a fallback when possible; with no fallback, a paused-but-selected Codex account still receives requests. Anthropic and generic OAuth pause exclude the account from new requests, failover and refresh, and an all-paused pool answers 403. Credentials and health are preserved; already-sent turns are not cancelled.", ], }, { command: ["account", "resume"], - summary: "Return a paused account to a Codex or supported generic OAuth pool.", + summary: "Return a paused account to a Codex, Anthropic or supported generic OAuth pool.", routes: [ { method: "PUT", path: "/api/codex-auth/accounts/pause" }, { method: "GET", path: "/api/oauth/accounts" }, @@ -636,13 +636,17 @@ export const CAPABILITIES: readonly Capability[] = [ { method: "PUT", path: "/api/codex-auth/auto-switch" }, { method: "GET", path: "/api/oauth/accounts/pool" }, { method: "PUT", path: "/api/oauth/accounts/pool" }, + { method: "GET", path: "/api/oauth/accounts" }, + { method: "PUT", path: "/api/oauth/accounts/auto-switch" }, ], - flags: [{ name: "--json", value: "boolean", summary: "Emit the stored threshold and whether it is applied." }], + flags: [{ name: "--json", value: "boolean", summary: "Emit the stored threshold and whether it is applied." }, + { name: "--account", value: "string", summary: "Anthropic account ID; inherit restores the pool default, off stores zero." }], mutates: true, json: "envelope", details: [ "A bare invocation reads and never writes.", "`on` stores 80%, `off` stores 0%, and `threshold ` accepts 0-100.", + "Anthropic requires --account ; inherit sends null to restore its pool default. Manual/affinity precedence and pool-off recovery are unchanged.", "For a generic OAuth pool, `inert: true` means the threshold is stored but not applied, `inert: false` means the pool is applying it, and an absent `inert` is an unknown capability.", ], }, diff --git a/src/cli/index.ts b/src/cli/index.ts index 834b37ecbeb..f4870bbe7f9 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -202,9 +202,9 @@ async function refreshOwnedRaycastCatalog( } initializeNodeLauncherContext(); - // The compiled executable is also the capture-only MCP server's launcher. // Handle this private entrypoint before CLI preflight or command dispatch. +if (process.argv[2] === "__keyring-load-check") { console.log(JSON.stringify((await import("../lib/keyring-native")).inspectKeyringBinding())); process.exit(0); } if (process.argv[2] === "__codebuddy-mcp") { const { runCodeBuddyMcpServer } = await import("../adapters/codebuddy/mcp-server"); await runCodeBuddyMcpServer(process.argv[3] ?? ""); diff --git a/src/codex/catalog/build-entries.ts b/src/codex/catalog/build-entries.ts index aef0f234054..4d35cd2c8c4 100644 --- a/src/codex/catalog/build-entries.ts +++ b/src/codex/catalog/build-entries.ts @@ -369,9 +369,8 @@ export function orderForModelPicker( * Every generated routed row — current full-slug form, the June–July 2026 * provider-name form, and legacy combo aliases — carries the stable * description prefix `Routed via opencodex → `; foreign rows from Cursor or - * user tooling do not. `owned_by` cannot serve as the signal (upstream - * ownership), and `comp_hash` defaults to "opencodex" for every normalized - * row. + * user tooling do not. `owned_by` describes upstream ownership, and `comp_hash` + * describes history compatibility; neither is an authorship signal. */ function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean { if (isNativeAliasCatalogEntry(entry)) return true; @@ -782,11 +781,13 @@ export function mergeCatalogEntriesFromObservedState({ delete entry[SPAWN_PRIORITY_FIELD]; } const slug = String(entry.slug); - if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue; + if (!isOcxAuthoredRoutedEntry(entry)) continue; // The builder no longer copies a template's comp_hash onto routed rows (#5796), but a row // kept from disk may still carry one. Custom rows, Codex-forward aliases included, never // reach this loop: they are rebuilt from config. - entry.comp_hash = "opencodex"; + // Clear the former synthetic "opencodex" marker as well: it is not upstream evidence. + entry.comp_hash = null; + if (isNativeAliasCatalogEntry(entry)) continue; const featuredRank = featuredRankOf(slug); entry.priority = featuredRank !== undefined ? featuredRank * priorityStride diff --git a/src/codex/catalog/derive-entry.ts b/src/codex/catalog/derive-entry.ts index e1d85541704..98bd424e712 100644 --- a/src/codex/catalog/derive-entry.ts +++ b/src/codex/catalog/derive-entry.ts @@ -144,10 +144,9 @@ export function deriveEntry( delete e.context_window; delete e.max_context_window; delete e.auto_compact_token_limit; - // Nor its comp_hash (#5796). Codex compacts a thread whenever the recorded value - // changes, and the template is whichever native row a rebuild found first, so an - // inherited value moves with rebuild order. Left unset, normalization gives every - // routed row the same "opencodex" marker. + // Nor its comp_hash (#5796): template selection is not evidence of history + // compatibility. Normalization represents the unknown value as null, avoiding + // both rebuild-dependent hashes and a synthetic native/routed mismatch. delete e.comp_hash; } if (typeof e.base_instructions === "string") { diff --git a/src/codex/catalog/gather-capture.ts b/src/codex/catalog/gather-capture.ts index 7f67743cc0c..64eadad1c2e 100644 --- a/src/codex/catalog/gather-capture.ts +++ b/src/codex/catalog/gather-capture.ts @@ -120,6 +120,8 @@ export interface CatalogGatherProviderModelOutcome { export interface ModelsAuthResolution { readonly apiKey: string | undefined; readonly observed: boolean; + readonly oauthAccountId?: string; + readonly oauthGeneration?: string; readonly oauthApiBaseUrl?: string; readonly oauthProjectId?: string; } diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 5e2dacf7ca1..ab75b7e52bc 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -47,6 +47,7 @@ import { NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT6_LUNA_MODEL, NATIVE_GPT6_SOL_MODEL, + NATIVE_GPT61_SOL_MODEL, NATIVE_RESERVE_MODEL, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, @@ -70,6 +71,7 @@ export { NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT6_LUNA_MODEL, NATIVE_GPT6_SOL_MODEL, + NATIVE_GPT61_SOL_MODEL, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, SELF_DESCRIBED_NATIVE_OPENAI_MODELS, @@ -89,6 +91,8 @@ export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [ // client_version >= 0.155.0, so an installed catalog built by an older client lacks them. // Astra Minor is deliberately absent: it is gated, and nativeOpenAiSlugs() would drop it anyway. NATIVE_GPT6_SOL_MODEL, NATIVE_GPT6_LUNA_MODEL, + // GPT-6.1 Sol needs client_version >= 0.153.0 upstream; older installed catalogs lack it. + NATIVE_GPT61_SOL_MODEL, ]; export function configuredNativeAliasSlugs( @@ -196,6 +200,8 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record