diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 568a3d29d4..8c15cb696a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -445,6 +445,21 @@ jobs: cd gui bun run build + - name: Record dashboard preview source + if: needs.changes.outputs.gui == 'true' + run: | + git rev-parse HEAD > gui/dist/build-commit.txt + git rev-parse HEAD:gui > gui/dist/build-gui-tree.txt + + - name: Upload dashboard preview + if: needs.changes.outputs.gui == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dashboard-preview-${{ github.sha }} + path: gui/dist + retention-days: 7 + if-no-files-found: error + - name: CLI help smoke run: bun run src/cli/index.ts help @@ -515,33 +530,90 @@ jobs: # scripts/ci/run-bun-test-batches.sh. An assertion failure still fails on # the first attempt — only the crash signature is retried, exactly once. - name: Test + env: + MACOS_TEST_SHARD: ${{ matrix.shard }} run: | # GitHub Actions starts bash `run:` blocks with `-e`. Disable # errexit so a Bun crash reaches PIPESTATUS and the bounded retry. set +e set -uo pipefail - suite_log="$(mktemp -t ocx-macos-suite.XXXXXX)" - for attempt in 1 2; do - # --timeout: Bun's default 5s per-test ceiling is the recurring flake - # class on this loaded shared runner (real retry windows + server - # round-trips exceed 5s under contention; a 10s-floor in-test - # watchdog fired at 10.16s there). 60s keeps hangs bounded (the 30m - # job timeout is the outer backstop) while removing the timing - # flakes — assertions are untouched. Pairs with the 30s CI floor in - # tests/helpers/ci-watchdog.ts. - bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/2 2>&1 | tee "$suite_log" - suite_status="${PIPESTATUS[0]}" - if [ "$suite_status" -eq 0 ]; then - exit 0 + + run_macos_suite() { + local suite_log suite_status attempt + suite_log="$(mktemp -t ocx-macos-suite.XXXXXX)" || return $? + for attempt in 1 2; do + # Preserve the existing per-test ceiling and crash-only retry for + # every invocation, including each isolated serial file. + bun test --isolate --timeout 60000 "$@" 2>&1 | tee "$suite_log" + suite_status="${PIPESTATUS[0]}" + if [ "$suite_status" -eq 0 ]; then + rm -f "$suite_log" + return 0 + fi + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + rm -f "$suite_log" + return "$suite_status" + fi + echo "::warning::Bun runtime crash in the macOS suite (exit ${suite_status}, attempt ${attempt})." + done + echo "::error::Bun runtime crash repeated on the macOS suite; failing after one retry." + rm -f "$suite_log" + return "$suite_status" + } + + case "$MACOS_TEST_SHARD" in + 1|2) ;; + *) echo "::error::Invalid macOS test shard"; exit 64 ;; + esac + serial_manifest="$(bun -e 'import { SERIAL_FULL_SUITE_FILES } from "./scripts/test.ts"; console.log(SERIAL_FULL_SUITE_FILES.join("\n"));')" + manifest_status=$? + if [ "$manifest_status" -ne 0 ]; then + exit "$manifest_status" + fi + serial_files=() + ignore_args=() + serial_count=0 + while IFS= read -r file; do + if [[ ! "$file" =~ ^[[:alnum:]_./-]+$ || "$file" == /* || "/$file/" == *"/../"* || "/$file/" == *"/./"* ]]; then + echo "::error::Invalid serial test path" + exit 1 fi - if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then - echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." - exit "$suite_status" + for ((index=0; index [repo]` path checks + the authenticated actor against the trusted `dev` roster and live repository permissions, + preserves outstanding maintainer objections, and binds its result to the current head and base. + The helper emits a validation snapshot, not a ready-to-run privileged merge command: head + matching does not pin a PR's base, which may change after inspection. Revalidate the current + actor and `dev` base before a separately authorized merge. The helper is not proof of CI or + security review and not a barrier against an administrator bypassing it. Repository settings + remain authoritative for actual permissions. + - 2026-08-19 — [@Wibias](https://github.com/Wibias) stepped down as a maintainer and is now a contributor. This follows his own decision to stop developing opencodex; it is not a disciplinary action, and it was made with the owner's @@ -176,12 +198,11 @@ Adding or removing a maintainer requires: changes, and it blocks deletion and non-fast-forward pushes. Allowed merge methods are merge and squash; rebase merges are off. - The one carve-out is that the `maintain`/`admin` repository role holds a - `pull_request` bypass, so an owner can merge without the approval the rules - otherwise require. That is a bypass, not an exemption: "Authors do not approve - their own pull requests" above still governs, and an owner who uses the bypass - should record it on the pull request rather than leave it to be inferred from - a merge timestamp. Widening the security boundary is a separate decision. + At that time, the actual PR bypass covered `admin`; the earlier wording that + included `maintain` was inaccurate. The 2026-09-06 policy above adds the explicit + maintainer-integration exception for `dev` and the corresponding `maintain` role. + Both roles bypass through pull requests only. Force-push and deletion protections + remain in place, and the integrating maintainer records the decision and evidence. ## Security reports diff --git a/README.md b/README.md index bb134383f1..61b93b8240 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Two commands, and every one of them runs any LLM you point it at.

```bash npm install -g @bitkyc08/opencodex -ocx start # proxy + dashboard on localhost:10100 +ocx start ``` @@ -78,14 +78,29 @@ account while existing threads stay pinned to the account that started them. ## Quick start -### For humans +### Personal install ```bash npm install -g @bitkyc08/opencodex # Node 18+; the Bun runtime is bundled automatically -ocx start # or `ocx service` to run it in the background +ocx start # proxy + dashboard on localhost:10100 ``` -### Docker Compose +Use `ocx service` to run it in the background. + +Open **http://localhost:10100** and configure everything in the web dashboard — add providers +(40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. `ocx gui` +re-opens the dashboard at any time. +It can also manage a **ChatGPT account pool** for Codex auth. Add multiple ChatGPT / Codex accounts, +refresh their 5h / weekly / 30d quota in the dashboard. Under quota routing, new sessions can use +the lowest-usage healthy account; round-robin and fill-first use their own policies. Existing Codex +threads normally retain affinity to the account that started them, so long SSH, tmux, or +mobile-connected sessions do not jump accounts mid-conversation — but quota re-evaluation, failover, +account exclusion, affinity expiry, or 401/403 and 429 recovery can rebind them. Give the accounts a +selection order when one of them — usually your Codex Desktop login — should only be reached for +once the others are drained. + +
+Docker Compose The repository ships a digest-pinned, non-root Compose build. With Git and Bun installed on the host, generate the canonical compatibility manifest before every image build, then initialize @@ -116,6 +131,8 @@ image, Compose file, environment, or shell arguments. See the [Remote Hub deployment guide](https://opencodex.me/guides/remote-hub/#docker-compose) for provider setup, authenticated acceptance checks, remote management, and rollback. +
+
Install from source (latest dev) @@ -143,19 +160,8 @@ they reach the npm package.
-Open **http://localhost:10100** and configure everything in the web dashboard — add providers -(40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. `ocx gui` -re-opens the dashboard at any time. -It can also manage a **ChatGPT account pool** for Codex auth. Add multiple ChatGPT / Codex accounts, -refresh their 5h / weekly / 30d quota in the dashboard. Under quota routing, new sessions can use -the lowest-usage healthy account; round-robin and fill-first use their own policies. Existing Codex -threads normally retain affinity to the account that started them, so long SSH, tmux, or -mobile-connected sessions do not jump accounts mid-conversation — but quota re-evaluation, failover, -account exclusion, affinity expiry, or 401/403 and 429 recovery can rebind them. Give the accounts a -selection order when one of them — usually your Codex Desktop login — should only be reached for -once the others are drained. - -### For agents +
+For agents ```bash npm install -g @bitkyc08/opencodex @@ -172,6 +178,8 @@ when it is unreachable). `ocx status` / `ocx doctor` / `ocx health` report the r > star this repository — that is the user's decision, never an agent's. The CLI suppresses the > prompt for agent-driven runs and the API refuses them with `403 agent_consent_required`. +
+ ## Supported platforms | OS | Status | Service manager | diff --git a/devlog/_fin/260906_a_final_closeout/000_plan.md b/devlog/_fin/260906_a_final_closeout/000_plan.md new file mode 100644 index 0000000000..404af3c08c --- /dev/null +++ b/devlog/_fin/260906_a_final_closeout/000_plan.md @@ -0,0 +1,7 @@ +# A final verified landing + +Terminal outcome: all five source PRs are closed, credited changes are on dev, and integrated dev CI passed. See [the outcome](../260906_a_runtime_stack/090_outcome.md) and [final evidence](030_quota_followup.md). + +All five feature implementations and two additional verification repairs have independent review and remote regression evidence. Three feature carries and the Windows foundation are already on dev. Remaining chain:3708 (bounded macOS cleanup/replay-fixture foundation) →3692 (Command Code affinity) →3694 (effective capabilities). + +This final cycle preserves the original owner objective: all five source PRs dispositioned, credited changes on dev, and fresh final dev verification. No local suites/typecheck/build. Existing owner-authorized admin merge applies only after every actual current-head producer passed; a queued aggregation-only job may be evaluated with its exact allowlist and recorded honestly. No pending functional test or failed test is waived. diff --git a/devlog/_fin/260906_a_final_closeout/001_gate_audit.md b/devlog/_fin/260906_a_final_closeout/001_gate_audit.md new file mode 100644 index 0000000000..b3149e0741 --- /dev/null +++ b/devlog/_fin/260906_a_final_closeout/001_gate_audit.md @@ -0,0 +1,3 @@ +# Final gate audit resolution + +Accepted the independent audit finding: merge automation must not ignore a failed/cancelled aggregate or skipped applicable producer. The helper now enumerates all24 applicable producer names for the pinned manual-all workflow and requires every one completed/successful. No job-level skip is applicable to this workflow invocation. Exactly one ci aggregator must be successful or only queued; all other states reject. Queued aggregation is accepted only after the full producer predicate has been independently established and recorded. Prior three A merge records were rechecked and satisfy this stronger condition; no failed/skipped producer was previously bypassed. diff --git a/devlog/_fin/260906_a_final_closeout/010_landing.md b/devlog/_fin/260906_a_final_closeout/010_landing.md new file mode 100644 index 0000000000..27844a050a --- /dev/null +++ b/devlog/_fin/260906_a_final_closeout/010_landing.md @@ -0,0 +1,10 @@ +# Final actions and acceptance + +1. Refresh final integration PR #3716 and every carried layer head/base/review thread. Require all 24 actual producers at the exact final top head, including every Windows shard, both macOS shards and unsharded control, Linux shards, types/privacy and smokes. Preserve failed predecessor runs and the evidence for their repairs; a predecessor result is never relabeled successful. See 020 for the independently approved topology amendment. +2. Retarget the fully verified final top to dev and admin-merge it with a match-head guard. This carries all reviewed ancestor commits without rewriting them. Prove the top merge, all pending layer heads and original contributor commits are ancestors of freshly fetched dev. Close lower review PRs with the actual top integration evidence; preserve their actual GitHub state if GitHub automatically recognizes them as merged. Do not claim separate layer merge commits. +3. Immediately close each superseded original after checking it did not gain unique new changes. Source3679's rebase08d25 has verified identical patch; other originals are refreshed normally. Source3672/3679/3568 are already closed. Source3581/3671 close immediately after the verified top landing. Related3661 remains open for its explicitly excluded residual scope. +4. Merge latest dev into this own closeout branch only after code landings. Move only completed A unit directories from devlog/_plan to devlog/_fin, preserving historical contents. Add a concise public outcome table with source/carry/merge/CI/author proof and scope limitations. Never copy ignored logs or private investigations. +5. Publish a docs-only closeout PR using the repository template. Verify its changed paths, privacy and metadata; admin merge when checks allow. Runtime/tests/dependencies must be byte-identical to the last code merge. Existing code CI may prove that identical runtime tree; documentation metadata alone is not runtime test evidence. +6. Require final dev push-CI producer success on the last code head, verify any subsequent docs-only difference and final ancestry, and refresh all source/carry states. Complete the landing criterion and goal only after the durable ledger is complete and the FSM has closed. No release, deployment, service or account changes. + +Independent final audit checks this plan and later actual evidence. Any new valid implementation or CI finding returns to a narrowly scoped repair; it is not discarded to finish the goal. diff --git a/devlog/_fin/260906_a_final_closeout/020_verified_top_amendment.md b/devlog/_fin/260906_a_final_closeout/020_verified_top_amendment.md new file mode 100644 index 0000000000..d0ad70b43c --- /dev/null +++ b/devlog/_fin/260906_a_final_closeout/020_verified_top_amendment.md @@ -0,0 +1,9 @@ +# Verified descendant integration + +The original sequential landing plan was amended after independent review. The capability tip #3694 at b59a34ce5 passed all 24 actual functional CI producers in run33988434944. A preceding foundation run retained a distinct pre-ready transition-fixture timeout on Windows. The test-only repair and its outer-budget review correction form final top #3716 at782e21ed7; this top must independently pass all24 producers before any remaining code lands. + +One merge of the fully verified descendant preserves the complete reviewed stack and its original contributor commits. It avoids rewriting three branch heads for the same fixture repair. The completion bar remains exact-head functional verification, fresh source/review checks, dev ancestry for every layer, explicit original-author credit, immediate original closure and final aggregate dev evidence. Pending or failed functional jobs at the final candidate cannot be waived. + +Independent topology review: Lagrange PASS. The concrete execution helper is also reviewed before use. Historical failed predecessor jobs remain historical failures, while lower PRs are recorded as carried through the actual final integration. This is a change in landing topology, not a claim that unmerged review heads already shipped. + +The transition fixture passed four tests and typecheck remotely on pinned Bun1.4.0 at782e21ed7. A10.5-second controlled startup passed the new budget and failed the old10-second guard. An injected startup exception produced the direct pre-barrier diagnostic. Temporary mutations were restored; independent repair review passed after budgeting parent setup plus both sequential lock-test children. No local product tests, builds or typechecks ran. diff --git a/devlog/_fin/260906_a_final_closeout/030_quota_followup.md b/devlog/_fin/260906_a_final_closeout/030_quota_followup.md new file mode 100644 index 0000000000..54a86cac50 --- /dev/null +++ b/devlog/_fin/260906_a_final_closeout/030_quota_followup.md @@ -0,0 +1,7 @@ +# Final fixture follow-up and landing + +The final candidate advanced from782e21ed7 to5097e66fa after Windows1 exposed the quota observer fixture's unjoined queue. Only that test file and its record changed; runtime and dependency trees were identical. Independent review passed, remote16tests/typecheck passed, delayed-queue controls reproduced the exact old3failures and kept all16green after repair, and suppressed delivery still failed both event-count assertions. Temporary remote mutations were restored. + +Full509CI33991642514 passed all24actual producers plusci. Finaltop3716 was owner-authorized admin-merged into dev asa2f69c8aa60976345740ae6f3d2301f89297328e. Every pending layer head is an ancestor; originals3581/3671 closed immediately after proof. Earlier3672/3679/3568 remain closed. Live source/carry states, coauthor trailers and3661OPEN were independently re-read by the final state verifier. + +IntegrateddevCI33993960826 completed successfully: all17 applicable producers and the aggregate passed, with only the two dispatch-only jobs skipped. Independent final evidence review confirmed all five carry heads and all eight contributor commits are ancestors of the integration. The closeout contains only the five A documentation units; its runtime, tests and dependencies match the verified integration tree. diff --git a/devlog/_fin/260906_a_macos_verification/000_plan.md b/devlog/_fin/260906_a_macos_verification/000_plan.md new file mode 100644 index 0000000000..460d566fec --- /dev/null +++ b/devlog/_fin/260906_a_macos_verification/000_plan.md @@ -0,0 +1,5 @@ +# Final macOS verification repairs + +C4 spec-satisfaction repair of Unix probe cleanup classification. Consume the already reviewed replay-fixture commit7ff811ced to keep caller identity stable in the shared verification baseline. Main owns this new foundation PR below the two remaining A layers. No local suite/typecheck/build; all execution uses isolated remote Bun1.4.0 and CI. Existing GitHub/SSH identities and own branches only; no account/service/release changes. The only live processes exercised are temporary launchers created by the regression fixture. No additional termination signals or widened permissions are authorized. A2h checkpoint reassesses progress; no token/cost cap was specified. Detailed OS traces stay in ignored scratch. + +Goal: initial EPERM during an already-owned probe-group teardown does not prevent bounded observation of that group's disappearance. Success still requires an observed ESRCH. Persistent permission uncertainty or live groups continue to refuse installation and restore the launcher. Keep the existing one-second cleanup bound, one SIGKILL attempt, diagnostic sanitation and rollback guarantees. diff --git a/devlog/_fin/260906_a_macos_verification/010_cleanup_plan.md b/devlog/_fin/260906_a_macos_verification/010_cleanup_plan.md new file mode 100644 index 0000000000..403b1770e4 --- /dev/null +++ b/devlog/_fin/260906_a_macos_verification/010_cleanup_plan.md @@ -0,0 +1,11 @@ +# Diff-level cleanup plan + +1. Carry reviewed commit7ff811ced (test-only replay caller snapshots, forced second boundary and changed-token isolation) onto this dev foundation. Resolve only contextual offsets; do not introduce affinity production code or its cohort matrix. +2. MODIFY src/codex/shim.ts terminateUnixProcessGroup: retain the single initial SIGKILL. Save EPERM rather than immediately throwing it; other non-ESRCH errors still throw. Use the unchanged one-second passive signal-0 observation loop. An observed disappearance succeeds; if the group remains or cannot be observed, rethrow saved EPERM, otherwise retain the existing generic nontermination error. No new signal retry, timeout increase, cache change or test-only production export. +3. MODIFY tests/codex-integration/codex-shim.test.ts timeout rollback fixture. Keep its real native case, exact timeout message, restored launcher/no backup/no marker, native group-missing and child-dead/zombie assertions. Add scoped parent-only process.kill observation for its recorded negative PGID; unrelated calls delegate unchanged and spawned probes have independent native bindings. +4. Deterministic cases: SIGKILL throws sentinel EPERM then signal-0 EPERM→ESRCH must produce ordinary timeout refusal; persistent EPERM and continually-live signal-0 must retain fail-closed EPERM diagnostics. Assert one SIGKILL, actual passive probes, and the existing bounded runtime. Restore spies before native process cleanup proof; never count synthetic ESRCH as real cleanup. Passive bounded joining of the known fixture group is allowed for injected cases; native case retains its original immediate cleanup assertions. Finally restore environment/mocks and clean only fixture-owned paths/processes. +5. Emit bounded pid/state/error-code diagnostics on failure, with no commands, credentials or environment dumps. Actual CI EPERM is observed; the zombie-only-group explanation is a hypothesis, not claimed captured fact. +6. Remote proof: focused shim and replay/cache/security tests plus typecheck. Revert only the EPERM observation correction in remote scratch; the disappearing-group control must fail its exact diagnostic assertion. Candidate must pass transient, persistent and live controls, the native timeout integration and all original rollback checks. Restore source bytes. Independent implementation/security audit then exact-head full CI before admin landing. +7. Cascade verified foundation into affinity then capability, retain source-author commits, update PR bases before auto-deletion and reverify their current heads. No original remaining PR is closed before its change is on dev. Full current-head CI and final dev proof remain mandatory. + +Cleanup completion is not installation approval: existing timeout/recursive/descendant markers and the pre-cleanup group-survival result still refuse the launcher. The change only permits bounded absence proof before choosing the existing refusal diagnostic. No previously unsafe launcher is accepted. diff --git a/devlog/_fin/260906_a_macos_verification/020_direct_transport_watchdog.md b/devlog/_fin/260906_a_macos_verification/020_direct_transport_watchdog.md new file mode 100644 index 0000000000..708a1ce380 --- /dev/null +++ b/devlog/_fin/260906_a_macos_verification/020_direct_transport_watchdog.md @@ -0,0 +1,7 @@ +# Focused verification watchdog correction + +ClassC1: one test file, no production behavior or public API change. WindowsCIjob101361741694 hit the fixture's flat3000ms childwatchdog before routing assertions. The same child performs imports, an unbounded control fetch, two750ms probes and a2000ms read. The log cannot identify which stage consumed time. + +Use the existing CI-watchdog owner for a derived whole-child budget:3000ms startup +2000ms bounded control +750ms identity +750ms readiness +2000ms read +1000ms exit =9500ms. OnCI the existing30s/45s floor applies. Give the test itself the child budget plus1000ms cleanup. Add fixed child phase markers and bounded phase/request-count diagnostics, never capability values. Keep every exact routing/header assertion and existing per-operation budgets. Bound only the previously unbounded control fetch. + +Verify remotely on pinnedBun: originalfilechecks, an explicit3500ms pre-import delay underCI that succeeds withthecorrectbudget and fails withtheold3000ms guard, and an intentional memory-read misroute that fails the unchangedproxy/capability assertions despite valid-looking responses. No local execution. This is a causal verifier fix within the ongoing final landing repair loop, not an unconditional rerun or production timeout increase. diff --git a/devlog/_fin/260906_a_macos_verification/030_transition_probe_watchdog.md b/devlog/_fin/260906_a_macos_verification/030_transition_probe_watchdog.md new file mode 100644 index 0000000000..768c5104af --- /dev/null +++ b/devlog/_fin/260906_a_macos_verification/030_transition_probe_watchdog.md @@ -0,0 +1,9 @@ +# Transition probe readiness budget + +The next full Windows verification of the foundation (run33988432596, job101366851939) had one failure: the two-process transition initialization fixture reached its ten-second readiness deadline before both children published their barriers. No transition assertion ran. The same fixture passed in the fully verified stack tip33988434944. The failed log is retained; the exact slow operation on that runner was not captured. + +The harness nevertheless has a concrete budget defect: before publishing ready, each Windows child resolves the effective SID and the known folder through two separately bounded thirty-second PowerShell calls. A ten-second enclosing deadline can reject valid operation within those existing product limits. This is a C1 fixture-only follow-up within the final landing cycle. + +Derive the child budget from both identity calls plus startup headroom, use the existing CI watchdog on other platforms, and scale each outer test deadline to its sequential phases. Detect an exited child while waiting for a barrier so a crash cannot masquerade as slow startup, and await child exit before deleting its sandbox. Preserve every real process race, lock refusal, winner count, generation and database assertion; no product timing changes. + +Verification requires remote pinned-runtime focused tests and typecheck, a delayed-ready control that passes the new budget and fails the old ten-second budget, an early-exit diagnostic control, independent review, and full exact-head cross-platform CI on the final stacked follow-up. No local test, typecheck or build runs. diff --git a/devlog/_fin/260906_a_macos_verification/040_quota_observation_drain.md b/devlog/_fin/260906_a_macos_verification/040_quota_observation_drain.md new file mode 100644 index 0000000000..31e5091ce2 --- /dev/null +++ b/devlog/_fin/260906_a_macos_verification/040_quota_observation_drain.md @@ -0,0 +1,7 @@ +# Join asynchronous quota observations in fixtures + +The final top's Windows1 verification (run33990109175, job101372136435) found a concrete fixture ordering defect. The first two quota-reset seam assertions saw no event after six microtasks and five milliseconds. A later test that used the existing explicit drain received those earlier scheduled and surprise events instead. The fixed sleep did not join cold lazy imports or the serialized observation chain, and fixture reset replaced the capture sink while old work was still pending. + +This C1 test-only follow-up uses the existing flushQuotaObservationsForTests seam. Join observations before assertions; join before resetting a fixture or replacing its sink; and join asynchronous baseline forgetting after clearAccountQuota. Keep all event counts, reset kinds, account separation and no-notification assertions unchanged. Production quota logic and timing remain untouched. + +Verify on the remote pinned runtime with the full focused file and typecheck. Delay the existing observation/forget chain in scratch to prove the new drain still passes and the old five-millisecond fixture fails. Restore every temporary mutation. Require independent review and final exact-head CI before integration. No local tests, builds or typechecks. diff --git a/devlog/_fin/260906_a_replay_credentials/000_plan.md b/devlog/_fin/260906_a_replay_credentials/000_plan.md new file mode 100644 index 0000000000..01b5f57117 --- /dev/null +++ b/devlog/_fin/260906_a_replay_credentials/000_plan.md @@ -0,0 +1,3 @@ +# Stable replay-fixture caller identity + +C2 spec-satisfaction repair of a concrete macOS control failure. Two logical replay conversations generated a new synthetic credential for each request; a second-boundary change made them different callers. Preserve production credential scope and every existing response/cache assertion. Only tests/server/server-agent-task-recovery-replay.test.ts and this numbered unit change. No local tests/typecheck/build; pinned remote Bun1.4 isolated regressions, deterministic old/new control, typecheck and current-head CI before final landing. Owner-authorized no-verify pushes/admin merge remain scoped to A. No credential or service changes. Same session goal/ledger owns this extra mandatory cycle; no completion criteria removed. diff --git a/devlog/_fin/260906_a_replay_credentials/010_replay_plan.md b/devlog/_fin/260906_a_replay_credentials/010_replay_plan.md new file mode 100644 index 0000000000..abe95054b7 --- /dev/null +++ b/devlog/_fin/260906_a_replay_credentials/010_replay_plan.md @@ -0,0 +1,9 @@ +# Replay fixture diff plan + +MODIFY tests/server/server-agent-task-recovery-replay.test.ts only: + +1. In the two original real-handler tests (cached NEW_TASK continuation and MESSAGE replay), capture one headers object before the first post and reuse it for the second. Keep status200, one recovery, two provider bodies, plaintext-present and ciphertext-absent assertions. +2. Scope a Date.now spy to each test at a real current second plus995ms. Advance controlled time by10ms between posts. Assert a newly constructed unused credential differs across that boundary, while the actual conversation continues with its original headers. Restore the clock in finally. No sleep or timeout increase. +3. Add a changed-token isolation control using the existing fakeChatGptJwt claim override: same account/envelope and two valid tokens differing in exp must not share cached plaintext. Reusing the original request still restores. Assert no extra network recovery and unchanged encrypted input on the miss. +4. Main performs exact-head remote isolated replay/cache/security tests and typecheck. A scratch red control restores per-post codexHeaders() calls while keeping the forced boundary; both conversations must lose the expected plaintext. The changed-token negative remains a pass. Restore candidate bytes after the probe. +5. Independent review checks fixture identity, clock cleanup and unchanged production boundary. Publish the own affinity branch, cascade the capability child and obtain fresh CI after all recorded verification repairs. Original source author commits remain intact. No new production file or test-layout entry. diff --git a/devlog/_fin/260906_a_runtime_stack/000_plan.md b/devlog/_fin/260906_a_runtime_stack/000_plan.md new file mode 100644 index 0000000000..c724b27854 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/000_plan.md @@ -0,0 +1,43 @@ +# A runtime integration roadmap + +## Loop specification + +- Archetype: spec-satisfaction repair; C3 runtime, C4 proxy credential/recovery boundaries. +- Trigger: owner assigned A (#3672, #3679, #3568, #3581, #3671), authorized inherited parallel subagents, contributor-preserving stacked PRs, no-verify pushes, dev integration and immediate resolved-work closure. +- Goal: preserve transport termination, configured WS egress, native subagent MESSAGE recovery, conversation affinity and effective policy capabilities. +- Non-goals: B/C/D implementation, release promotion/publication, production service/config/credential changes. #3661 remains open unless its complete residual scope is independently proven solved. +- Verification: remote focused activation checks during each implementation cycle; required current-head hosted CI before readiness/merge; final dev ancestry and CI. No local tests, typecheck or builds. Git diff checks and prose validation only locally. +- Stop: all five changes or proven equivalents on dev; original PRs closed with attribution and landing references; fully solved linked issues closed; unresolved issue scope documented. +- Memory: this unit, the session-bound goalplan/ledger, and ignored `.tmp/a-runtime-stack/` evidence. +- Outcomes: DONE / proven NOOP; external blockers recorded, never inferred from ordinary conflicts or pending CI. +- Delegation: main owns FSM, branches, commits, pushes and merges. Plan/review lanes have disjoint file scope. Two distinct failed dispatches return ownership to main; new worker scope is added at P. +- Resource scope: existing git/gh identity, owned `codex/a-*` branches, public contributor PR reads, and existing the isolated remote verification host SSH for isolated verification. No new account credentials or provider requests. User imposed no subagent/model-inheritance budget cap; no model override. Two-hour checkpoint per work phase triggers evidence/reliability reassessment; pending CI is monitored with bounded waits, not abandoned. + +## Phase map + +| Cycle | Artifact | Consumes | Delivers | +|---|---|---|---| +| roadmap | 000 + 010..080 | live dev and public contributor changes | audited full integration plan; docs only | +| sse | 010_sse.md | existing SSE relay boundary | failure notification independent of tee cancellation | +| ws | 020_ws.md | prior transport baseline | WS outbound policy and pool identity | +| recovery | 030_recovery.md | validated transport stack | MESSAGE recovery + reparse/cache semantics | +| affinity | 040_affinity.md | recovery/reparse fields | stable Command Code conversation identity | +| capabilities | 050_capabilities.md | final effective dispatch behavior | policy selection congruent with dispatch | +| windows-fixtures | 070_windows_fixtures.md | current Windows failure evidence | deterministic verifier repair below A stack | +| landing | 080_landing.md | independently verified stack layers and verifier repair | current dev inclusion and closeout | + +The owner explicitly requested a stack. Independent transport fixes are retained as separate cumulative layers to expose interaction at each head; this publication order is not a claim of a hard dependency between SSE and WS. The actual code dependency is recovery before affinity. Each layer has its own PR diff, regression proof and CI. Bottom-up merge only; retarget before deleting parent branches. Keep stacks short by landing verified lower layers while subsequent cycles continue when possible. + +## Ownership + +A owns shared `src/server/responses/core.ts` integration for #3568 then #3581. C owns #3576 and may land its separate OAuth replay region first; both lanes refresh dev and preserve each other's changes. B owns `src/config.ts` final field reconciliation with #3679. Source snapshots use `refs/codex/a-original/N`, not remote-tracking scratch refs that concurrent fetch-prune can remove. + +## Evidence and provenance + +CI entry `.github/workflows/ci.yml` has unrestricted pull_request bases for stacks. `src/**`, `tests/**`, `scripts/**` are observed by its changes job; Linux test shards invoke `scripts/ci/run-bun-test-batches.sh`, gates run tsc/privacy, and macOS/Windows jobs validate platform behavior. These definitions were inspected without executing local suites. Remote-check scripts and real run IDs will be captured at C, not invented at P. Original source changes and review histories are public; any newly discovered security reasoning stays in ignored scratch. + +- #3672: `077dd61f66ac80678d071ae8fe516507f43a4264` +- #3679: `b05cccf264b4ab61db5d8dee8232c2f89bb1b541` +- #3568: `036a9321788464fdf33a387c9f44a834a844bdc1` +- #3581: `f60397d3408e0339ffc66acdcaca8133e40866c2` +- #3671: `7b1beb9c5eacd8dde22681a5df26804be52380b8` diff --git a/devlog/_fin/260906_a_runtime_stack/003_audit_resolution.md b/devlog/_fin/260906_a_runtime_stack/003_audit_resolution.md new file mode 100644 index 0000000000..f871eeb920 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/003_audit_resolution.md @@ -0,0 +1,8 @@ +# Roadmap audit resolution + +Independent reviewer returned GO-WITH-FIXES (2). Both findings accepted and folded before B: + +1. Implementation-cycle D previously implied full CI/dev landing, inconsistent with prepared stack layers. 010..050 now explicitly distinguish exact-head remote focused/type verified draft preparation from 080 full-gate landing. Final objective and full-CI-before-merge criteria remain unchanged. +2. Affinity reparse tests required a cohort option the shared post helper did not accept. 040 now names tests/helpers/agent-task-recovery.ts option extension, internal handler forwarding, and true/false/undefined observation in real initial/cache-only adapter calls. + +Private remote host/user paths were replaced with placeholders; exact machine setup remains ignored scratch. No product edits or local suites in roadmap cycle. diff --git a/devlog/_fin/260906_a_runtime_stack/004_windows_amendment.md b/devlog/_fin/260906_a_runtime_stack/004_windows_amendment.md new file mode 100644 index 0000000000..53b8712654 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/004_windows_amendment.md @@ -0,0 +1,3 @@ +# Windows verifier amendment + +Full Windows CI for SSE head failed two unchanged shutdown-spill fixtures. Logs and causal analysis are retained in ignored ci-triage/report.md. C confirms no concurrent ownership of responses-state.test.ts. Add a separate windows-fixtures PABCD after capabilities and before final landing. It repairs test-only clocks/fallback isolation, independently validates on Windows, publishes a small foundation PR and inserts its verified change beneath the source stack. Refresh descendants bottom-up while preserving contributor commits and required current-head checks. No production ACL/budget change, no test skip, no unexamined rerun. The final landing document moves to080; no existing completion criterion is weakened. Owner explicitly authorized admin merge. diff --git a/devlog/_fin/260906_a_runtime_stack/005_amendment_audit.md b/devlog/_fin/260906_a_runtime_stack/005_amendment_audit.md new file mode 100644 index 0000000000..f871b8eb37 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/005_amendment_audit.md @@ -0,0 +1,3 @@ +# Capability and verifier amendment audit + +Independent reviewer: capability plan PASS; roadmap GO-WITH-FIXES one prerequisite finding. Accepted. Added a new windows-fixtures prerequisite to landing while retaining its existing capabilities edge. No task/criterion completion states or existing prerequisite edges were removed. The durable dependency graph now prevents final landing from being selected before Windows verifier completion. Replaced stale060 landing references with080. Temporary Windows verification workflow still requires concrete security review before push. diff --git a/devlog/_fin/260906_a_runtime_stack/010_sse.md b/devlog/_fin/260906_a_runtime_stack/010_sse.md new file mode 100644 index 0000000000..27aee24e97 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/010_sse.md @@ -0,0 +1,218 @@ +# 010 — Surface SSE rewrite failure before tee cancellation (#3672) + +Status: candidate plan, docs-only; implementation class C3 (stream lifecycle). Evidence refreshed 2026-09-06 KST through GitHub API and local persistent refs. + +## Implementation-cycle completion versus landing + +This decade cycle ends with a reviewed prepared draft PR, exact-carried-head focused remote activation evidence and remote typecheck, with full CI dispatched. That cycle D does not claim the bug shipped, full CI passed, or an issue resolved. `080_landing.md` retains the mandatory full current-head cross-platform/type/privacy/docs evidence, review, dev ancestry and immediate source-PR/fully-resolved-issue closure gates. Later P consumes the verified prepared stack parent; it need not have landed yet. Only final landing yields feature DONE. + + +## Source, authorship and drift + +- Public PR: https://github.com/lidge-jun/opencodex/pull/3672 +- Exact original head/commit: `077dd61f66ac80678d071ae8fe516507f43a4264`, persistent ref `refs/codex/a-original/3672`. +- Original parent: `6585e6a70f42be8b6c81ff20d4fa0f39f7da03db`. +- Original author: Hako, GitHub `devswha`; trailer: `Co-authored-by: Hako <25837994+devswha@users.noreply.github.com>`. +- Planning dev/working HEAD: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`, also confirmed by live dev API. Although commits differ, comparing original parent to dev restricted to the three original touched files returns no changed paths. Original patch applies to the same source blobs; later P must repeat this check. +- Live reviewThreads: zero. PR body reports focused/affected passes but explicitly does not claim a green full suite. No outstanding published code-review fix is presently known; exact carried-head remote tests and maintainer review remain acceptance gates. + +## Behavior and necessity + +Current `src/server/sse-payload-rewrite.ts:249-253` releases budget/disposes the rewrite, then awaits `reader.cancel(error)` before `controller.error(error)`. With a real tee and an open inspection sibling, cancellation waits for that sibling; the outer relay cannot observe the failure and abort the work that releases it. Reuse the existing failed-tail owner at `src/server/relay.ts:259`; no new error wrapper, retry mechanism, stream type or configuration is needed. Doing nothing leaves the wait cycle; deleting cancellation loses cleanup; configuration cannot fix the ordering. + +After the change, release/dispose remain synchronous, cancellation rejection is handled asynchronously, and `controller.error(error)` runs immediately. The outer failed-tail relay emits one `response.failed` then `[DONE]` and aborts upstream while inspection remains open. Budget overflow keeps `translation_buffer_limit`. Normal EOF, explicit client cancellation, rewriting, and disposal idempotence remain unchanged. + +## Exact file manifest and diff contract + +| Operation | Path | Required change | +|---|---|---| +| MODIFY | `src/server/sse-payload-rewrite.ts` | At catch line 252 replace awaited cancellation with `void reader.cancel(error).catch(() => {});` and explain the tee dependency. Keep release/dispose/error ordering. | +| MODIFY | `tests/responses/sse-payload-rewrite.test.ts` | Append the original parameterized real-tee regression after the current last test (line 153); cover source cancel resolve and reject, bounded completion and cleanup. | +| MODIFY | `docs-site/src/content/docs/reference/proxy-formats.md` | After line 83 add the five-line native rewrite failure/terminal/budget contract. | + +NEW: none. DELETE: none. Existing test file is already registered; no layout manifest edit. The appendix contains the exact original patch for all three paths, not an outline. No production implementation has been performed by this planning task. + +## Regression activation and independent acceptance + +1. Remote RED: place the original two added tests on the layer's current parent without the one-line production change in an isolated remote verification checkout. Hold a real tee sibling open, exhaust a 64-byte test budget with `data: partial` plus 80 bytes, and require both cases to reject with the one-second inspection-wait deadline. Record that failure, then restore the candidate patch remotely. +2. Remote GREEN: for resolve and reject cancellation, terminal arrives before inspection settles; exactly one `response.failed`, `translation_buffer_limit`, final `data: [DONE]`, abort signal true, zero source cancel calls before sibling release, one dispose, zero current budget bytes and one overflow. +3. Release inspection afterwards: underlying source cancel executes once; late cancellation rejection is observed/handled; no unhandled asynchronous error; disposal stays once. Test `finally` releases locks and budgets even on RED timeout. +4. Run adjacent failed-tail tests remotely to preserve disconnect, terminal and cancellation behavior. Existing Windows-sensitive composition must remain covered by an actual Windows run. +5. A reviewer confirms no awaited sibling-dependent cancellation remains on this exception path, no cancellation errors escape, and no downstream terminal duplication. This layer does not depend on #3679 or recovery/cache work. + +Remote focused command, after verifying remote checkout SHA and installing its pinned runtime/dependencies: + +```sh +bun test tests/responses/sse-payload-rewrite.test.ts tests/responses/sse-failed-tail.test.ts +``` + +Static anchors: `sse-payload-rewrite.ts:145` disposal guard, `:192` budget release, `:249` exception path, `:256` consumer cancellation; `relay.ts:259` failed-tail entry. The original regression fixture itself is the activation instrument; contributor-reported previous RED is context, not this layer's proof. + +#3679 shares only `docs-site/src/content/docs/reference/proxy-formats.md` with this layer. Preserve both paragraphs when the child lands. No release promotion or linked issue is bundled. + +## Execution boundary and resource scope + +This document is candidate planning for a later implementation P, authored during the first docs-only cycle. Main owns roadmap, FSM, goal, implementation and stack integration. This delegated task writes only this document and its sibling `010_sse.md`/`020_ws.md`; it does not run tests, typecheck, builds, Git mutations, GitHub mutations, FSM transitions or goal commands. + +Later implementation scope uses existing `gh` credentials and writes only the assigned own stack branches. Inherited parallel reviewers are authorized. There is no explicit user token/cost cap; a two-hour checkpoint triggers reassessment, not automatic success or abandonment. No production account probes, deployment or release actions belong to this layer. User explicitly forbids local suites; every executable verification below is for a remote isolated checkout or GitHub Actions later. No local typecheck/build is permitted here either. Security investigation material stays in `.tmp`; this public plan records only already-public PR behavior and general integration requirements. + +At the later P, refresh live dev and original PR head through main, compare touched-path blobs and parent changes, and amend this plan before implementation. A changed original SHA invalidates the carried-patch assumption. Preserve unrelated workers' changes. Main may carry the original commit with author identity preserved; every carry/superseding PR and squash message must include the exact `Co-authored-by` trailer below. Publish with the user's authorized `--no-verify` push, never a direct push to dev. Local hook bypass does not supply CI evidence. + +## Main-confirmed remote execution handoff + +Main reports the existing remote repository at `REMOTE_HOST:REMOTE_SOURCE_CHECKOUT` and Bun `1.3.14` have been verified. These are main-provided environment facts, not a local execution claim by this planner. Implementation C uses an isolated remote clone at the exact carried SHA; do not alter the existing remote checkout or its service. Record `git rev-parse HEAD` and `bun --version` from that isolated remote clone with focused activation-test and typecheck receipts. If the carried tree requires a different pinned Bun version, reconcile and record that runtime difference remotely before treating results as representative. + +Carry PRs remain draft until full current-head GitHub CI is green. Focused remote tests/typecheck are implementation evidence, not permission to skip full gates. The final landing cycle requires every full gate described below, including an actually executed Windows lane where Windows behavior is claimed, current-head review, and dev ancestry proof. No local project command execution is allowed at any point. Deeper implementation review belongs to the next cycle; this handoff completes only the concrete candidate plan. + +## Static workflow coverage and later remote evidence + +Inspected at `dev@81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`: + +- `.github/workflows/ci.yml:7` uses `pull_request: {}` without a base branch filter: an open stacked child gets the same workflow. Push trigger at line 27 covers integration branches only; pushing an own feature branch without opening its PR does not establish CI coverage. +- Runtime/test changes activate the `changes` gate and four Linux test shards (`ci.yml:255`), two macOS shards (`ci.yml:451`), and gates (`ci.yml:392`, typecheck at 422, privacy at 430). Linux test discovery is `scripts/ci/run-bun-test-batches.sh:197`; these layer tests are not the storage/API-usage exclusions at line 52. +- Windows full test shards are **dispatch-only**, `ci.yml:658-686`; ordinary PR CI cannot prove Windows behavior. `workflow_dispatch` has only `lane` (`ci.yml:46`), so use the own branch as `--ref`, not a nonexistent SHA input. `lane=all` runs Windows plus the unsharded macOS control (`ci.yml:549`). +- The aggregate `ci` accepts intentional skips (`ci.yml:927`); a green aggregate alone cannot prove a Windows run, regression activation, or even runtime tests on a docs-only PR. Check producer job conclusions and logs. +- `.github/actions/setup-project-bun/action.yml:18` resolves the runtime from `package.json.dependencies.bun`. Record actual Bun version rather than substituting contributor-reported Bun 1.4.0 results. + +Later main-owned CI commands (not executed by this planning task): + +```sh +# Freeze/read own branch head first; then dispatch its checked-in workflow. +gh workflow run ci.yml --repo lidge-jun/opencodex --ref "$A_LAYER_BRANCH" -f lane=all +gh run list --repo lidge-jun/opencodex --workflow ci.yml --branch "$A_LAYER_BRANCH" --limit 10 --json databaseId,headSha,event,status,conclusion +gh run view "$A_RUN_ID" --repo lidge-jun/opencodex --json headSha,event,conclusion,jobs +gh run view "$A_RUN_ID" --repo lidge-jun/opencodex --log +``` + +Assert dispatch `headSha` equals the frozen layer head. For PR merge-ref runs record actual checkout SHA and its head/base parents. A refresh/restack/new commit requires evidence for that resulting tree. Capture URLs, SHA, OS, runtime, command, exit code, failed/skipped test counts and any baseline comparison in main's evidence receipt. `action_required`, pending/cancelled checks, hygiene-only success and author attestations are not green test evidence. Do not check a contributor's local-CI attestation when no such local execution occurred. + +Full relevant suite coverage, typecheck, privacy and docs build must run remotely before readiness. For separately authorized remote checkout verification, install pinned dependencies there, run `bun run typecheck`, `bun run privacy:scan`, `bun run test`, and `(cd docs-site && bun run build)` there. Do not run those commands in the local managed workspace. Failures require a named current-base comparison and repair/reassessment; historic Windows failures do not automatically excuse a new failure. + +## Integration and close-out + +Each layer must be reviewable and independently acceptable against its immediate parent. No acceptance depends on a later A layer fixing its behavior. Main merges bottom-up with current-head CI and review evidence, retargets/restacks children before parent branch deletion, and preserves author trailers in squash/carry history. After main verifies the resulting merge commit is an ancestor of freshly fetched dev, immediately close the superseded original PR with the carry PR/commit reference. Close a linked issue only when its full acceptance scope is satisfied; do not infer an issue from a similar title. This planning task performs none of those actions. + +## Original patch appendix (candidate implementation) + +The following is source material already published in the linked PR. Revalidate context at the later P; do not apply during the docs-only cycle. + +```diff +diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md +index 77a67147a..19049e87f 100644 +--- a/docs-site/src/content/docs/reference/proxy-formats.md ++++ b/docs-site/src/content/docs/reference/proxy-formats.md +@@ -83,6 +83,11 @@ This applies to both tee inspection and eager relay, including Windows rewrite t + even when the upstream read rejects before the response-body cancellation hook runs. + A terminal captured during the bounded post-disconnect drain retains its actual outcome. + ++If native passthrough rewriting fails, including when it exceeds the translation ++buffer budget, the relay reports the failure without waiting for upstream inspection ++to finish. It cancels the upstream work and emits `response.failed` followed by ++`data: [DONE]`; a budget overflow uses the `translation_buffer_limit` error code. ++ + Client-facing Responses SSE frames are limited to 4 MiB per frame, measured in raw bytes before the + SSE block delimiter. On HTTP, an unterminated upstream frame that exceeds the limit fails closed + with a synthetic `response.failed` event followed by `data: [DONE]`. On the Responses WebSocket +diff --git a/src/server/sse-payload-rewrite.ts b/src/server/sse-payload-rewrite.ts +index 3c6d825e6..f9fb62065 100644 +--- a/src/server/sse-payload-rewrite.ts ++++ b/src/server/sse-payload-rewrite.ts +@@ -249,7 +249,9 @@ export function relaySseWithBlockRewrite( + } catch (error) { + releaseBuffer(); + disposeRewrite(); +- try { await reader.cancel(error); } catch { /* already closed */ } ++ // Cancelling one tee branch waits for its sibling. Surface the failure ++ // now so downstream can abort upstream and release the inspection branch. ++ void reader.cancel(error).catch(() => {}); + controller.error(error); + } + }, +diff --git a/tests/responses/sse-payload-rewrite.test.ts b/tests/responses/sse-payload-rewrite.test.ts +index 34dae59e0..773665a05 100644 +--- a/tests/responses/sse-payload-rewrite.test.ts ++++ b/tests/responses/sse-payload-rewrite.test.ts +@@ -153,4 +153,82 @@ describe("SSE payload rewrite composition", () => { + expect(budget.snapshot().currentBytes).toBe(0); + budget.dispose(); + }); ++ ++ test.each(["resolve", "reject"] as const)( ++ "surfaces a rewrite failure before tee cancellation can %s", ++ async cancellationOutcome => { ++ const budget = createTestTranslatorBudget({ maxTurnBytes: 64 }); ++ const upstream = new AbortController(); ++ const cancellation = Promise.withResolvers(); ++ const cancellationError = new Error("upstream cancellation failed"); ++ let cancelCalls = 0; ++ let disposeCalls = 0; ++ const source = new ReadableStream({ ++ start(controller) { ++ controller.enqueue(new TextEncoder().encode("data: partial")); ++ controller.enqueue(new TextEncoder().encode("x".repeat(80))); ++ // Keep the source open after exhausting the rewrite budget. ++ }, ++ cancel() { ++ cancelCalls += 1; ++ return cancellation.promise; ++ }, ++ }); ++ const [native, inspection] = source.tee(); ++ const inspectionReader = inspection.getReader(); ++ await inspectionReader.read(); ++ await inspectionReader.read(); ++ let inspectionSettled = false; ++ const pendingInspection = inspectionReader.read().then(() => { inspectionSettled = true; }); ++ const rewrite = Object.assign((block: string) => [block], { ++ dispose() { disposeCalls += 1; }, ++ }); ++ const rewritten = relaySseWithBlockRewrite(native, rewrite, budget); ++ const client = relaySseWithFailedTail(rewritten, upstream); ++ const completion = readAll(client); ++ let deadline: ReturnType | undefined; ++ ++ try { ++ const out = await Promise.race([ ++ completion, ++ new Promise((_, reject) => { ++ deadline = setTimeout(() => reject(new Error("rewrite failure waited for the inspection tee")), 1_000); ++ }), ++ ]); ++ expect(out.match(/event: response.failed/g)).toHaveLength(1); ++ expect(out).toContain('"code":"translation_buffer_limit"'); ++ expect(out).toEndWith("data: [DONE]\n\n"); ++ expect(upstream.signal.aborted).toBe(true); ++ expect(inspectionSettled).toBe(false); ++ expect(cancelCalls).toBe(0); ++ expect(disposeCalls).toBe(1); ++ expect(budget.snapshot().currentBytes).toBe(0); ++ expect(budget.snapshot().overflows).toBe(1); ++ ++ // Releasing inspection settles both tee cancellation promises. A late ++ // rejection must be handled by the rewriter as well as this reader. ++ const siblingCancellation = inspectionReader.cancel("inspection cleanup"); ++ expect(cancelCalls).toBe(1); ++ if (cancellationOutcome === "reject") { ++ cancellation.reject(cancellationError); ++ await expect(siblingCancellation).rejects.toBe(cancellationError); ++ } else { ++ cancellation.resolve(); ++ await siblingCancellation; ++ } ++ await pendingInspection; ++ await Bun.sleep(0); // Let the runner observe any unhandled cancellation rejection. ++ expect(disposeCalls).toBe(1); ++ } finally { ++ clearTimeout(deadline); ++ const cleanup = inspectionReader.cancel().catch(() => {}); ++ cancellation.resolve(); ++ await cleanup; ++ await pendingInspection; ++ await completion.catch(() => {}); ++ inspectionReader.releaseLock(); ++ budget.dispose(); ++ } ++ }, ++ ); + }); +``` diff --git a/devlog/_fin/260906_a_runtime_stack/011_sse_refresh.md b/devlog/_fin/260906_a_runtime_stack/011_sse_refresh.md new file mode 100644 index 0000000000..506c051db2 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/011_sse_refresh.md @@ -0,0 +1,3 @@ +# SSE layer P refresh + +The original #3672 head remains 077dd61f66ac80678d071ae8fe516507f43a4264 and open. Fresh dev fetch and restricted original-parent/dev diff show no drift in all three touched files. Consume 010 unchanged. Implementation branch: codex/a-01-sse; base dev, including audited roadmap commit. Carry original Hako commit with -x and unchanged author. All project verification remote; actual tee failure/late reject regression plus adjacent failed-tail and typecheck on exact carried SHA. Full CI remains mandatory before landing. diff --git a/devlog/_fin/260906_a_runtime_stack/020_ws.md b/devlog/_fin/260906_a_runtime_stack/020_ws.md new file mode 100644 index 0000000000..2db3f2d07b --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/020_ws.md @@ -0,0 +1,886 @@ +# 020 — Honor upstream WebSocket proxy routing (#3679) + +Status: candidate plan after layer 010, docs-only; implementation class C4 for the outbound routing boundary. Evidence refreshed 2026-09-06 KST; the 01:28 update supersedes the earlier triage snapshot. + +## Implementation-cycle completion versus landing + +This decade cycle ends with a reviewed prepared draft PR, exact-carried-head focused remote activation evidence and remote typecheck, with full CI dispatched. That cycle D does not claim the bug shipped, full CI passed, or an issue resolved. `080_landing.md` retains the mandatory full current-head cross-platform/type/privacy/docs evidence, review, dev ancestry and immediate source-PR/fully-resolved-issue closure gates. Later P consumes the verified prepared stack parent; it need not have landed yet. Only final landing yields feature DONE. + + +## Source, authorship and drift + +- Public PR: https://github.com/lidge-jun/opencodex/pull/3679 +- Exact current original head/commit: `b05cccf264b4ab61db5d8dee8232c2f89bb1b541`, persistent ref `refs/codex/a-original/3679`. +- Original parent and current live dev: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. +- Original author: Clive Rosfield, GitHub `S0RYUASUKA`; trailer: `Co-authored-by: Clive Rosfield <64878945+S0RYUASUKA@users.noreply.github.com>`. +- Ref/head equality verified. All 13 original touched files have identical parent/dev blobs. Layer 010 will additionally change `proxy-formats.md`; preserve its failure paragraph. `src/config.ts` overlaps lane B ownership, so main must recheck fresh dev and coordinate its comment hunk at later P. +- The body still names earlier tested head `182006615c484756012f2d0c1ba72f47c4e5cf5b`. Its counts are author-reported evidence for that head, not proof of this updated head or a later carry. Full suite is explicitly incomplete/non-green in the body. + +## Current review resolution + +All three live review threads are now resolved, not outstanding: + +- Companion documentation request was addressed by this head: https://github.com/lidge-jun/opencodex/pull/3679#discussion_r3941233811 . Provider guide and adapter reference now distinguish adapter selection from transport selection. +- Proxy precedence request was withdrawn; preserve scheme-specific environment precedence and `config.proxy` filling absent scheme variables. The resulting HTTPS proxy intentionally precedes ALL_PROXY. Current patch adds uppercase/lowercase ALL_PROXY regression coverage: https://github.com/lidge-jun/opencodex/pull/3679#discussion_r3941252968 . Do not reintroduce the withdrawn behavior change. +- The separate proxy policy request was withdrawn; retain established operator-selected HTTP/HTTPS proxy support in this routing-only layer: https://github.com/lidge-jun/opencodex/pull/3679#discussion_r3941252966 . Any new investigation belongs in scratch, not this document. + +Remaining gates: independent current-head routing/security review under MAINTAINERS.md and remote executed verification. A resolved bot discussion does not substitute for that review. + +## Behavior and reuse decision + +Current `src/server/responses/codex-ws-session.ts:12-14` constructs WebSocket with headers only. `ws-upstream.ts:167-169` does not resolve or pass a proxy, and pool identity at `codex-ws-pool.ts:55` does not distinguish routes. `src/lib/proxy-env.ts:28` already owns HTTP fetch proxy matching; `src/lib/provider-outbound.ts:79` owns NO_PROXY matching. Reuse and move that matcher rather than adding a second implementation or altering Bun HTTP fetch rules. + +After the patch, choose a route once before dialing. NO_PROXY wins (WSS default 443, WS default 80). Otherwise choose first nonempty HTTPS_PROXY/https_proxy/ALL_PROXY/all_proxy for WSS; HTTP_PROXY alone is not a WSS proxy. An unusable selected value returns immediate HTTP/SSE fallback without dialing WebSocket or trying a lower-priority proxy. HTTP/SSE continues its existing scheme-specific behavior; ALL_PROXY does not become an HTTP fetch input. One-shot and retained sessions receive the same selected route. Pool reuse key includes the route while scope still identifies account/thread/turn; changed route retires the old session. Existing dispatch refusal, abort, headers, quota handling and post-send no-replay behavior remain intact. + +No-code/config-only options do not cover Bun WebSocket construction or retained-session route affinity; no new transport, package dependency, proxy discovery method or routing flag is necessary. + +## Exact file manifest and diff contract + +All operations are MODIFY; NEW and DELETE are none. The appendix is the complete diff against the pinned original parent. No new test file means no layout registration additions. + +| Path | Before → after / exact change | +|---|---| +| `src/lib/proxy-env.ts` | After ProxyEnvMap (line 5), add ProxyRoute direct/proxy/fallback union; exported normalizeProxyHostname and noProxyMatches moved from provider-outbound; matcher accepts an env map and WSS default port. Add resolveProxyRoute with first-nonempty selection, HTTP/HTTPS scheme acceptance and fallback on parse/unsupported value. Keep effectiveProxyFor semantics unchanged. | +| `src/lib/provider-outbound.ts` | Import the shared matcher/normalizer, delete private copies and configuredProxyFor wrapper, call outboundProxyConfigured directly. Keep DNS/destination admission and effective HTTP proxy snapshot logic unchanged. | +| `src/config.ts` | Update only the applyProxyEnv comment at line 3739 to explain transport use and scheme-versus-ALL precedence. No executable ALL_PROXY guard is added. | +| `src/server/responses/ws-upstream.ts` | Import resolver; after frame-size guard at line 151 compute wsUrl, route and optional proxy; fallback before creating socket on route fallback; pass same proxy to identity, pool acquire and one-shot constructor. Existing admission hooks remain effective through HTTP fallback and WS exchange. | +| `src/server/responses/codex-ws-pool.ts` | Add optional proxy to identity/acquire signatures at lines 28/78, include proxy-or-null in hashed key at 55 and forward it into retained constructor at 97. Do not change scope, bounds or eviction. | +| `src/server/responses/codex-ws-session.ts` | Add optional fifth constructor argument; append proxy option only when selected. Preserve headers and all listener/lease lifecycle behavior. | +| `tests/server/proxy-env.test.ts` | Add ALL_PROXY spellings to saved/restored fixture env. Add resolver precedence/bypass/fallback cases, direct Bun WebSocket CONNECT fixture, Windows-only NO_PROXY fetch fixture, and both config-versus-ALL precedence cases. | +| `tests/responses/ws-upstream.test.ts` | Capture constructor options, isolate/restore all proxy env values, assert option+header propagation, zero sockets/one fallback for malformed/unsupported selection, existing upgrade fallback through proxy, NO_PROXY header/custom destination behavior. | +| `tests/responses/ws-upstream-reuse.test.ts` | Isolate/restore proxy env, capture options, exercise proxy A→B→NO_PROXY with two requests per route; expect three sockets, two frames each, old two closed and last retained. | +| `docs-site/src/content/docs/reference/proxy-formats.md` | Add canonical WSS routing, invalid-route fallback and scheme/config/ALL precedence paragraphs after line 113; retain 010's earlier SSE failure paragraph. | +| `docs-site/src/content/docs/guides/providers.md` | After line 620 distinguish adapter selection from transport with link to canonical rules. | +| `docs-site/src/content/docs/reference/adapters.md` | After line 95 add companion transport note, link and HTTP-vs-WSS distinction. | +| `structure/04_transports-and-sidecars.md` | At lines 438 and 647 include route in reuse identity and explain WSS route/fallback without changing HTTP rules. | + +Localized pages currently omit the new behavior; public review records no contradiction. Recheck that remains true at later P; do not add unrelated locale rewrites. The 13-file breadth is one route-selection contract with tests/docs, not 13 independent product changes; keep it one independently reviewed layer. + +## Regression activation and independent acceptance + +Remote RED/GREEN must prove each mechanism rather than only compiling the added API: + +1. Resolver: uppercase/lowercase ordering, blank values, fallback priority, invalid selected proxy, unsupported scheme; NO_PROXY exact/suffix/wildcard/port/IPv6/URL entry, uppercase-empty overriding lowercase; retain HTTP fetch behavior through provider-outbound tests. +2. Construction: one-shot and retained constructors get the chosen option with unchanged authorization/beta/originator/header filtering. Before the production change, a constructor-option assertion must fail on the parent. The test-only resolver import must not be mistaken for sufficient behavioral RED. +3. Invalid route: zero created sockets and exactly one fallback. NO_PROXY produces direct option omission; HTTP_PROXY-only does not create a WSS proxy route. Existing dispatch-refusal/aborted/post-send tests must retain no duplicate dispatch or replay. +4. Affinity: two requests on route A reuse, route B causes replacement, NO_PROXY causes another replacement; sockets `[A,B,direct]`, two frames each, states `[closed,closed,open]`. Parent without proxy in key must fail this assertion remotely. +5. Real runtime: original `proxy-env.test.ts` local CONNECT fixture executes **on the remote test runner**, observing `proxy-probe.invalid:443` with a loopback HTTP proxy. This fixture directly constructs Bun WebSocket; it does not by itself prove codexWsUpstreamFetch integration. Combine it with option-propagation tests and capture a separate remote loopback harness through codexWsUpstreamFetch if an end-to-end integration claim is made. No production credentials required. +6. Actual Windows execution must exercise the new Windows-only NO_PROXY fetch fixture; a Linux skip is expected and not Windows proof. Check full CI and privacy independently; validate disposal/no lingering test listener behavior. + +Remote focused command (only inside verified remote checkout, using fixture-specific env cleanup and restoration): + +```sh +bun test tests/server/proxy-env.test.ts tests/providers/provider-outbound.test.ts tests/providers/provider-outbound-private-network.test.ts tests/responses/ws-upstream.test.ts tests/responses/ws-upstream-reuse.test.ts tests/responses/reserve-dispatch-ws.test.ts --timeout 20000 +``` + +Control the eight HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY case variants within the isolated remote test process; never clear the user's global environment. Original contributor observed inherited-environment failures on an older baseline; reproduce any new discrepancy against this layer's exact parent before classifying it. Any skipped runtime probe must be recorded as unproven rather than silently accepted. + +## Execution boundary and resource scope + +This document is candidate planning for a later implementation P, authored during the first docs-only cycle. Main owns roadmap, FSM, goal, implementation and stack integration. This delegated task writes only this document and its sibling `010_sse.md`/`020_ws.md`; it does not run tests, typecheck, builds, Git mutations, GitHub mutations, FSM transitions or goal commands. + +Later implementation scope uses existing `gh` credentials and writes only the assigned own stack branches. Inherited parallel reviewers are authorized. There is no explicit user token/cost cap; a two-hour checkpoint triggers reassessment, not automatic success or abandonment. No production account probes, deployment or release actions belong to this layer. User explicitly forbids local suites; every executable verification below is for a remote isolated checkout or GitHub Actions later. No local typecheck/build is permitted here either. Security investigation material stays in `.tmp`; this public plan records only already-public PR behavior and general integration requirements. + +At the later P, refresh live dev and original PR head through main, compare touched-path blobs and parent changes, and amend this plan before implementation. A changed original SHA invalidates the carried-patch assumption. Preserve unrelated workers' changes. Main may carry the original commit with author identity preserved; every carry/superseding PR and squash message must include the exact `Co-authored-by` trailer below. Publish with the user's authorized `--no-verify` push, never a direct push to dev. Local hook bypass does not supply CI evidence. + +## Main-confirmed remote execution handoff + +Main reports the existing remote repository at `REMOTE_HOST:REMOTE_SOURCE_CHECKOUT` and Bun `1.3.14` have been verified. These are main-provided environment facts, not a local execution claim by this planner. Implementation C uses an isolated remote clone at the exact carried SHA; do not alter the existing remote checkout or its service. Record `git rev-parse HEAD` and `bun --version` from that isolated remote clone with focused activation-test and typecheck receipts. If the carried tree requires a different pinned Bun version, reconcile and record that runtime difference remotely before treating results as representative. + +Carry PRs remain draft until full current-head GitHub CI is green. Focused remote tests/typecheck are implementation evidence, not permission to skip full gates. The final landing cycle requires every full gate described below, including an actually executed Windows lane where Windows behavior is claimed, current-head review, and dev ancestry proof. No local project command execution is allowed at any point. Deeper implementation review belongs to the next cycle; this handoff completes only the concrete candidate plan. + +## Static workflow coverage and later remote evidence + +Inspected at `dev@81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`: + +- `.github/workflows/ci.yml:7` uses `pull_request: {}` without a base branch filter: an open stacked child gets the same workflow. Push trigger at line 27 covers integration branches only; pushing an own feature branch without opening its PR does not establish CI coverage. +- Runtime/test changes activate the `changes` gate and four Linux test shards (`ci.yml:255`), two macOS shards (`ci.yml:451`), and gates (`ci.yml:392`, typecheck at 422, privacy at 430). Linux test discovery is `scripts/ci/run-bun-test-batches.sh:197`; these layer tests are not the storage/API-usage exclusions at line 52. +- Windows full test shards are **dispatch-only**, `ci.yml:658-686`; ordinary PR CI cannot prove Windows behavior. `workflow_dispatch` has only `lane` (`ci.yml:46`), so use the own branch as `--ref`, not a nonexistent SHA input. `lane=all` runs Windows plus the unsharded macOS control (`ci.yml:549`). +- The aggregate `ci` accepts intentional skips (`ci.yml:927`); a green aggregate alone cannot prove a Windows run, regression activation, or even runtime tests on a docs-only PR. Check producer job conclusions and logs. +- `.github/actions/setup-project-bun/action.yml:18` resolves the runtime from `package.json.dependencies.bun`. Record actual Bun version rather than substituting contributor-reported Bun 1.4.0 results. + +Later main-owned CI commands (not executed by this planning task): + +```sh +# Freeze/read own branch head first; then dispatch its checked-in workflow. +gh workflow run ci.yml --repo lidge-jun/opencodex --ref "$A_LAYER_BRANCH" -f lane=all +gh run list --repo lidge-jun/opencodex --workflow ci.yml --branch "$A_LAYER_BRANCH" --limit 10 --json databaseId,headSha,event,status,conclusion +gh run view "$A_RUN_ID" --repo lidge-jun/opencodex --json headSha,event,conclusion,jobs +gh run view "$A_RUN_ID" --repo lidge-jun/opencodex --log +``` + +Assert dispatch `headSha` equals the frozen layer head. For PR merge-ref runs record actual checkout SHA and its head/base parents. A refresh/restack/new commit requires evidence for that resulting tree. Capture URLs, SHA, OS, runtime, command, exit code, failed/skipped test counts and any baseline comparison in main's evidence receipt. `action_required`, pending/cancelled checks, hygiene-only success and author attestations are not green test evidence. Do not check a contributor's local-CI attestation when no such local execution occurred. + +Full relevant suite coverage, typecheck, privacy and docs build must run remotely before readiness. For separately authorized remote checkout verification, install pinned dependencies there, run `bun run typecheck`, `bun run privacy:scan`, `bun run test`, and `(cd docs-site && bun run build)` there. Do not run those commands in the local managed workspace. Failures require a named current-base comparison and repair/reassessment; historic Windows failures do not automatically excuse a new failure. + +## Integration and close-out + +Each layer must be reviewable and independently acceptable against its immediate parent. No acceptance depends on a later A layer fixing its behavior. Main merges bottom-up with current-head CI and review evidence, retargets/restacks children before parent branch deletion, and preserves author trailers in squash/carry history. After main verifies the resulting merge commit is an ancestor of freshly fetched dev, immediately close the superseded original PR with the carry PR/commit reference. Close a linked issue only when its full acceptance scope is satisfied; do not infer an issue from a similar title. This planning task performs none of those actions. + +## Original patch appendix (candidate implementation) + +The following is source material already published in the linked PR. Revalidate context at the later P; do not apply during the docs-only cycle. + +```diff +diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md +index 6a37cf8a7..255c0d8dc 100644 +--- a/docs-site/src/content/docs/guides/providers.md ++++ b/docs-site/src/content/docs/guides/providers.md +@@ -620,6 +620,12 @@ A provider is included when opencodex has a matching wire adapter, **not** based + (AI Studio, Vertex, and Antigravity/Cloud Code Assist modes), `azure` / `azure-openai`, `kiro`, and + `cursor`. A proprietary API without one of these implementations, such as native Amazon Bedrock, + is not supported directly. ++ ++Provider configuration selects the adapter; upstream transport selection is separate. Eligible ++Responses traffic can use WSS with [explicit proxy routing](/reference/proxy-formats/#json-and-sse-output). ++Invalid or unsupported WebSocket proxy settings fall back to HTTP/SSE, which uses Bun's HTTP ++proxy rules rather than the WSS-specific `ALL_PROXY` fallback. ++ + **GitHub Copilot** is an OAuth provider (`ocx login github-copilot`) that exchanges a GitHub + device-flow login for a short-lived Copilot API token — not a pasted API key. **GitLab Duo** remains + a key/subscription-token gateway on its OpenAI-compatible endpoint. **Cloudflare AI +diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md +index 1db98357d..e2a24c67d 100644 +--- a/docs-site/src/content/docs/reference/adapters.md ++++ b/docs-site/src/content/docs/reference/adapters.md +@@ -95,6 +95,11 @@ body and response, with narrow compatibility rewrites for routed gateways. + `forward` uses configured static headers without relaying caller authorization; `key` uses the + configured provider key. + ++Adapter selection does not select the upstream transport. Eligible requests can use the ++[upstream WebSocket proxy route](/reference/proxy-formats/#json-and-sse-output); invalid or unsupported ++WebSocket proxy settings fall back to HTTP/SSE. HTTP fetch-based Responses handling uses Bun's ++HTTP proxy rules and does not inherit the WSS-specific `ALL_PROXY` fallback. ++ + Noncanonical Responses gateways receive Codex's client-executed `tool_search` declaration as a + collision-safe public function tool. Matching request history and JSON/SSE function calls are + translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward +diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md +index 77a67147a..b4d7e5dea 100644 +--- a/docs-site/src/content/docs/reference/proxy-formats.md ++++ b/docs-site/src/content/docs/reference/proxy-formats.md +@@ -113,6 +113,19 @@ the raw JSON frame and its SSE envelope at 4 MiB, and closes the upstream when i + would overflow. That overflow emits a terminal downstream `response.failed` event followed by + `[DONE]`. + ++The upstream WebSocket checks `NO_PROXY`/`no_proxy` first. Otherwise it uses the first non-empty ++`HTTPS_PROXY`, `https_proxy`, `ALL_PROXY`, or `all_proxy` value; `HTTP_PROXY` alone does not proxy a ++WSS connection. HTTP and HTTPS proxy URLs are passed to Bun. If the selected value is invalid or ++uses an unsupported protocol, opencodex skips the WebSocket attempt and uses HTTP/SSE instead of ++dialing the upstream directly. ++ ++These rules belong to the upstream WebSocket transport, independently of the selected provider ++adapter. HTTP fetch-based Responses requests, including SSE fallback, use Bun's HTTP proxy rules ++and do not use `ALL_PROXY`. `config.proxy` fills missing `HTTP_PROXY`/`HTTPS_PROXY` values; the ++resulting scheme-specific value also takes precedence over an existing `ALL_PROXY` for WebSocket. ++For an HTTPS upstream that requires a proxy, set `HTTPS_PROXY` or `config.proxy`; `HTTP_PROXY` ++alone leaves both WSS and its HTTPS fallback without a scheme-matched proxy. ++ + Every terminal Responses usage object includes both detail objects, even when the provider did not + report those details: + +diff --git a/src/config.ts b/src/config.ts +index 5d67275dc..72da45538 100644 +--- a/src/config.ts ++++ b/src/config.ts +@@ -3738,11 +3738,12 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements + } + + /** +- * Mirror `config.proxy` into HTTP(S)_PROXY env vars so Bun's native fetch routes every outbound +- * provider call through the proxy — no per-callsite changes (verified: Bun honors these plus +- * NO_PROXY). User-set env vars always win; localhost/127.0.0.1 are appended to NO_PROXY so the +- * CLI's own health checks and running-proxy API calls stay direct. Call once per process entry +- * that makes outbound provider requests (server start, catalog sync). ++ * Mirror `config.proxy` into HTTP(S)_PROXY env vars. Bun fetch consumes them natively; transports ++ * such as the ChatGPT upstream WebSocket select the same environment explicitly. User-set HTTP(S)_PROXY ++ * variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS. ++ * localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and ++ * running-proxy API calls stay direct. Call once per process entry that makes outbound provider ++ * requests (server start, catalog sync). + */ + export function applyProxyEnv(config: OcxConfig): void { + applyProxyEnvWith(config); +diff --git a/src/lib/provider-outbound.ts b/src/lib/provider-outbound.ts +index 495fef0b8..02bdbc207 100644 +--- a/src/lib/provider-outbound.ts ++++ b/src/lib/provider-outbound.ts +@@ -7,7 +7,7 @@ import { + resolvePublicAddresses, + } from "./destination-policy"; + import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http"; +-import { effectiveProxyFor, outboundProxyConfigured } from "./proxy-env"; ++import { effectiveProxyFor, noProxyMatches, normalizeProxyHostname, outboundProxyConfigured } from "./proxy-env"; + import { publicProviderBaseUrl } from "./provider-url"; + + type ProviderGetInit = Omit; +@@ -37,10 +37,6 @@ function pickPinnedAddress(addresses: Array<{ address: string; family: number }> + return addresses.find(address => address.family === 4) ?? addresses[0]!; + } + +-function configuredProxyFor(): boolean { +- return outboundProxyConfigured(); +-} +- + /** + * Registry-owned fake-IP transparency exception (Clash/Surge/Mihomo TUN mode). + * +@@ -76,45 +72,6 @@ function transparentFakeIpException( + return isCanonicalUrl(name, url); + } + +-function normalizeProxyHostname(hostname: string): string { +- const normalized = hostname.trim().toLowerCase().replace(/\.+$/, ""); +- return normalized.startsWith("[") && normalized.endsWith("]") +- ? normalized.slice(1, -1) +- : normalized; +-} +- +-function noProxyMatches(url: URL): boolean { +- const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? ""; +- const hostname = normalizeProxyHostname(url.hostname); +- const port = url.port || (url.protocol === "https:" ? "443" : "80"); +- for (const rawEntry of raw.split(",")) { +- let entry = rawEntry.trim().toLowerCase(); +- if (!entry) continue; +- if (entry === "*") return true; +- entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!; +- +- let entryHost = entry; +- let entryPort = ""; +- const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry); +- if (bracketed) { +- entryHost = bracketed[1]!; +- entryPort = bracketed[2] ?? ""; +- } else if ((entry.match(/:/g)?.length ?? 0) === 1) { +- const separator = entry.lastIndexOf(":"); +- const possiblePort = entry.slice(separator + 1); +- if (/^\d+$/.test(possiblePort)) { +- entryHost = entry.slice(0, separator); +- entryPort = possiblePort; +- } +- } +- if (entryPort && entryPort !== port) continue; +- entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, "")); +- if (!entryHost) continue; +- if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true; +- } +- return false; +-} +- + let proxyBoundaryWarned = false; + let proxyDnsDegradationWarned = false; + +@@ -181,7 +138,7 @@ async function providerOutboundRequest( + return provider.fetch(url, { ...init, method, redirect: "manual" }); + } + const parsed = postUrl ?? new URL(url); +- const proxyConfigured = configuredProxyFor(); ++ const proxyConfigured = outboundProxyConfigured(); + // Snapshot the scheme-matched proxy once, before the DNS await, so admission and transport + // below reason about the same value. `null` here means "no proxy fetch would actually use", + // even if some other proxy variable is set. +diff --git a/src/lib/proxy-env.ts b/src/lib/proxy-env.ts +index 46df59268..0ac9ed735 100644 +--- a/src/lib/proxy-env.ts ++++ b/src/lib/proxy-env.ts +@@ -3,6 +3,73 @@ export const PROXY_ENV_KEYS = [...OUTBOUND_PROXY_ENV_KEYS, "NO_PROXY"] as const; + + export type ProxyEnvKey = typeof PROXY_ENV_KEYS[number]; + export type ProxyEnvMap = Record; ++export type ProxyRoute = ++ | { kind: "direct" } ++ | { kind: "proxy"; proxy: string } ++ | { kind: "fallback" }; ++ ++export function normalizeProxyHostname(hostname: string): string { ++ const normalized = hostname.trim().toLowerCase().replace(/\.+$/, ""); ++ return normalized.startsWith("[") && normalized.endsWith("]") ++ ? normalized.slice(1, -1) ++ : normalized; ++} ++ ++export function noProxyMatches( ++ url: URL, ++ env: ProxyEnvMap = process.env, ++): boolean { ++ const raw = env.NO_PROXY ?? env.no_proxy ?? ""; ++ const hostname = normalizeProxyHostname(url.hostname); ++ const port = url.port || (url.protocol === "https:" || url.protocol === "wss:" ? "443" : "80"); ++ for (const rawEntry of raw.split(",")) { ++ let entry = rawEntry.trim().toLowerCase(); ++ if (!entry) continue; ++ if (entry === "*") return true; ++ entry = entry.replace(/^(?:https?|wss?):\/\//, "").split("/", 1)[0]!; ++ ++ let entryHost = entry; ++ let entryPort = ""; ++ const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry); ++ if (bracketed) { ++ entryHost = bracketed[1]!; ++ entryPort = bracketed[2] ?? ""; ++ } else if ((entry.match(/:/g)?.length ?? 0) === 1) { ++ const separator = entry.lastIndexOf(":"); ++ const possiblePort = entry.slice(separator + 1); ++ if (/^\d+$/.test(possiblePort)) { ++ entryHost = entry.slice(0, separator); ++ entryPort = possiblePort; ++ } ++ } ++ if (entryPort && entryPort !== port) continue; ++ entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, "")); ++ if (entryHost && (hostname === entryHost || hostname.endsWith(`.${entryHost}`))) return true; ++ } ++ return false; ++} ++ ++export function resolveProxyRoute( ++ url: URL, ++ env: ProxyEnvMap = process.env, ++): ProxyRoute { ++ if (noProxyMatches(url, env)) return { kind: "direct" }; ++ const key = url.protocol === "https:" || url.protocol === "wss:" ++ ? "HTTPS_PROXY" ++ : "HTTP_PROXY"; ++ const proxy = [key, key.toLowerCase(), "ALL_PROXY", "all_proxy"] ++ .map(candidate => env[candidate]?.trim()) ++ .find(Boolean); ++ if (!proxy) return { kind: "direct" }; ++ try { ++ const protocol = new URL(proxy).protocol; ++ return protocol === "http:" || protocol === "https:" ++ ? { kind: "proxy", proxy } ++ : { kind: "fallback" }; ++ } catch { ++ return { kind: "fallback" }; ++ } ++} + + export function proxyEnvPresent( + key: ProxyEnvKey, +diff --git a/src/server/responses/codex-ws-pool.ts b/src/server/responses/codex-ws-pool.ts +index 378cf2d4a..5d406bee4 100644 +--- a/src/server/responses/codex-ws-pool.ts ++++ b/src/server/responses/codex-ws-pool.ts +@@ -25,7 +25,7 @@ function digest(input: unknown): string { + } + + /** Identity comes from the selected outgoing request, never a model label or caller hint. */ +-export function codexWsReuseIdentity(url: string, headers: Record, frameText: string): CodexWsReuseIdentity | null { ++export function codexWsReuseIdentity(url: string, headers: Record, frameText: string, proxy?: string): CodexWsReuseIdentity | null { + if (url !== CODEX_RESPONSES_HTTP_URL) return null; + let body: unknown; + try { body = JSON.parse(frameText); } catch { return null; } +@@ -52,7 +52,7 @@ export function codexWsReuseIdentity(url: string, headers: Record): CodexWsSession | null { ++ acquire(identity: CodexWsReuseIdentity, url: string, headers: Record, proxy?: string): CodexWsSession | null { + this.sweep(); + for (const entry of this.entries.values()) { + if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue; +@@ -94,7 +94,7 @@ export class CodexWsPool { + this.remove(oldest); + } + const createdAt = this.now(); +- const session = new CodexWsSession(url, headers, true, () => this.changed(entry)); ++ const session = new CodexWsSession(url, headers, true, () => this.changed(entry), proxy); + const entry: Entry = { identity, session, createdAt, idleAt: createdAt, retired: false }; + session.reserve(); + this.entries.set(identity.key, entry); +diff --git a/src/server/responses/codex-ws-session.ts b/src/server/responses/codex-ws-session.ts +index bbf62f813..32716a529 100644 +--- a/src/server/responses/codex-ws-session.ts ++++ b/src/server/responses/codex-ws-session.ts +@@ -10,8 +10,8 @@ export class CodexWsSession { + private readonly completedIds = new Set(); + + constructor(url: string, headers: Record, readonly retainable = false, +- private readonly changed: () => void = () => {}) { +- this.socket = new WebSocket(url, { headers } as unknown as string[]); ++ private readonly changed: () => void = () => {}, proxy?: string) { ++ this.socket = new WebSocket(url, { headers, ...(proxy ? { proxy } : {}) } as unknown as string[]); + this.socket.addEventListener("open", this.onOpen); + this.socket.addEventListener("message", this.onIdleMessage); + this.socket.addEventListener("close", this.onClose); +diff --git a/src/server/responses/ws-upstream.ts b/src/server/responses/ws-upstream.ts +index e9773d02a..87b3767d2 100644 +--- a/src/server/responses/ws-upstream.ts ++++ b/src/server/responses/ws-upstream.ts +@@ -13,6 +13,7 @@ + // (passthrough relay, adapter parsers, usage sniffing) is unchanged. + + import { compareBunVersions } from "../../lib/bun-stream-caps"; ++import { resolveProxyRoute } from "../../lib/proxy-env"; + import type { CodexWsQuotaObserver } from "./codex-ws-metadata"; + import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, prepareCodexHttpInit, prepareCodexWsRequest } from "./codex-ws-request"; + import { codexWsExchange } from "./codex-ws-exchange"; +@@ -150,6 +151,10 @@ export function codexWsUpstreamFetch( + return sseFallback(url, init); + } + ++ const wsUrl = wsUpstreamUrlFor(url); ++ const proxyRoute = resolveProxyRoute(new URL(wsUrl)); ++ if (proxyRoute.kind === "fallback") return sseFallback(url, init); ++ const proxy = proxyRoute.kind === "proxy" ? proxyRoute.proxy : undefined; + // A genuine caller `originator` is already in these headers via the forward + // set. Never fabricate one here: pool/forward traffic must not impersonate + // Codex CLI, per the metadata-integrity contract. (The backend's fast lane +@@ -164,9 +169,9 @@ export function codexWsUpstreamFetch( + } + let session: CodexWsSession; + try { +- const identity = codexWsReuseIdentity(url, headers, frameText); +- session = (identity ? codexWsPool.acquire(identity, wsUpstreamUrlFor(url), headers) : null) +- ?? new CodexWsSession(wsUpstreamUrlFor(url), headers); ++ const identity = codexWsReuseIdentity(url, headers, frameText, proxy); ++ session = (identity ? codexWsPool.acquire(identity, wsUrl, headers, proxy) : null) ++ ?? new CodexWsSession(wsUrl, headers, false, undefined, proxy); + if (!session.busy && !session.reserve()) { + session.dispose(); + return sseFallback(url, init); +diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md +index 4ee22c114..a45a98c87 100644 +--- a/structure/04_transports-and-sidecars.md ++++ b/structure/04_transports-and-sidecars.md +@@ -435,7 +435,7 @@ These are transport-fidelity guarantees, not a provider-billing guarantee. + + Eligible complete-input creates can retain a canonical upstream socket within + one selected account, credential, thread and turn. Model/tier and immutable +-handshake headers must also match. Turn-state and turn-metadata headers are ++handshake headers and the selected outbound proxy must also match. Turn-state and turn-metadata headers are + projected into their same-name per-frame metadata slots; explicit body values win. + The pool retains at most 32 sockets, expires idle sockets after 30 seconds, and + retires a socket after five minutes or 32 successful exchanges (after active work +@@ -644,7 +644,11 @@ the upgrade with 426 so Codex falls back to HTTP cleanly. + + That setting controls the client-facing upgrade only. The transparent upstream + ChatGPT WS optimization described above is selected independently and still +-returns the same downstream SSE contract. ++returns the same downstream SSE contract. Its WSS route checks NO_PROXY first, then selects the ++first non-empty HTTPS_PROXY, https_proxy, ALL_PROXY, or all_proxy value. HTTP_PROXY alone does not ++route WSS. Unsupported or malformed selected proxy values skip the WebSocket attempt and use the ++existing SSE path immediately; they never fall through to a lower-priority proxy or direct WebSocket ++egress. HTTP/SSE fallback retains Bun fetch's own proxy rules, which do not consult ALL_PROXY. + + The endpoint handles `response.create`, ignores `response.processed`, supports warmup + `generate: false`, and feeds the same request pipeline as HTTP/SSE. +diff --git a/tests/responses/ws-upstream-reuse.test.ts b/tests/responses/ws-upstream-reuse.test.ts +index fd0a8fb5a..b957fdb31 100644 +--- a/tests/responses/ws-upstream-reuse.test.ts ++++ b/tests/responses/ws-upstream-reuse.test.ts +@@ -6,6 +6,8 @@ import { prepareCodexWsRequest } from "../../src/server/responses/codex-ws-reque + + const URL = "https://chatgpt.com/backend-api/codex/responses"; + const realWebSocket = globalThis.WebSocket; ++const proxyEnvKeys = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"]; ++let savedProxyEnv: Record; + let sequence = 0; + + class Socket extends EventTarget { +@@ -13,7 +15,7 @@ class Socket extends EventTarget { + static onSend: (socket: Socket, frame: Record) => void = (socket) => socket.complete(); + readyState = 0; + frames: Record[] = []; +- constructor(readonly url: string) { ++ constructor(readonly url: string, readonly options?: { proxy?: string }) { + super(); + Socket.all.push(this); + queueMicrotask(() => { if (this.readyState === 0) { this.readyState = 1; this.dispatchEvent(new Event("open")); } }); +@@ -58,7 +60,11 @@ function bodyWith(fields: Record) { + options.body = JSON.stringify({ ...JSON.parse(options.body as string), ...fields }); + return options; + } +-beforeEach(() => { globalThis.WebSocket = Socket as unknown as typeof WebSocket; }); ++beforeEach(() => { ++ globalThis.WebSocket = Socket as unknown as typeof WebSocket; ++ savedProxyEnv = Object.fromEntries(proxyEnvKeys.map(key => [key, process.env[key]])); ++ for (const key of proxyEnvKeys) delete process.env[key]; ++}); + + afterEach(() => { + runOptionalShutdownHooks(); +@@ -67,6 +73,25 @@ afterEach(() => { + Socket.onSend = socket => socket.complete(); + sequence = 0; + globalThis.WebSocket = realWebSocket; ++ for (const key of proxyEnvKeys) delete process.env[key]; ++ for (const key of proxyEnvKeys) { ++ if (savedProxyEnv[key] !== undefined) process.env[key] = savedProxyEnv[key]; ++ } ++}); ++ ++test("proxy changes and NO_PROXY retire the old route while unchanged routes reuse", async () => { ++ for (const proxy of ["http://proxy-a.example:8080", "http://proxy-b.example:8080"]) { ++ process.env.HTTPS_PROXY = proxy; ++ await drain(); ++ await drain(); ++ } ++ process.env.NO_PROXY = "chatgpt.com:443"; ++ await drain(); ++ await drain(); ++ expect(Socket.all.map(socket => socket.options?.proxy)) ++ .toEqual(["http://proxy-a.example:8080", "http://proxy-b.example:8080", undefined]); ++ expect(Socket.all.map(socket => socket.frames.length)).toEqual([2, 2, 2]); ++ expect(Socket.all.map(socket => socket.readyState)).toEqual([3, 3, 1]); + }); + + test("same account/thread/turn reuses one socket without trimming either HTTP input", async () => { +diff --git a/tests/responses/ws-upstream.test.ts b/tests/responses/ws-upstream.test.ts +index fd0951307..cfb087a4b 100644 +--- a/tests/responses/ws-upstream.test.ts ++++ b/tests/responses/ws-upstream.test.ts +@@ -1,4 +1,4 @@ +-import { afterEach, describe, expect, jest, test } from "bun:test"; ++import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test"; + import { providerFetch } from "../../src/server/responses/fetch-helpers"; + import { handleResponses } from "../../src/server/responses"; + import { isEagerRelaySseResponse } from "../../src/server/relay"; +@@ -162,18 +162,24 @@ describe("shouldUseCodexWsUpstream", () => { + }); + + type Listener = (event: unknown) => void; ++type FakeWebSocketOptions = { ++ headers?: Record; ++ proxy?: string; ++}; + + /** Minimal scriptable stand-in for Bun's WebSocket. */ + class FakeWebSocket { + static instances: FakeWebSocket[] = []; + static script: (ws: FakeWebSocket) => void = () => {}; + url: string; ++ options?: FakeWebSocketOptions; + sent: string[] = []; + closed = false; + listeners = new Map(); + +- constructor(url: string) { ++ constructor(url: string, options?: FakeWebSocketOptions) { + this.url = url; ++ this.options = options; + FakeWebSocket.instances.push(this); + queueMicrotask(() => FakeWebSocket.script(this)); + } +@@ -205,12 +211,23 @@ class FakeWebSocket { + + const RealWebSocket = globalThis.WebSocket; + const RealFetch = globalThis.fetch; ++const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy"] as const; ++let savedProxyEnv: Record; ++ ++beforeEach(() => { ++ savedProxyEnv = Object.fromEntries(PROXY_ENV_KEYS.map(key => [key, process.env[key]])); ++ for (const key of PROXY_ENV_KEYS) delete process.env[key]; ++}); + + afterEach(() => { + globalThis.WebSocket = RealWebSocket; + globalThis.fetch = RealFetch; + FakeWebSocket.instances = []; + FakeWebSocket.script = () => {}; ++ for (const key of PROXY_ENV_KEYS) delete process.env[key]; ++ for (const key of PROXY_ENV_KEYS) { ++ if (savedProxyEnv[key] !== undefined) process.env[key] = savedProxyEnv[key]; ++ } + }); + + function installFake(script: (ws: FakeWebSocket) => void) { +@@ -525,6 +542,41 @@ describe("codexWsUpstreamFetch", () => { + expect(text).not.toContain("must-not-leak"); + }); + ++ test("passes the selected proxy without changing handshake headers", async () => { ++ process.env.HTTPS_PROXY = "http://proxy.example:8080"; ++ installFake(ws => { ++ ws.emit("open", {}); ++ ws.emit("message", { data: JSON.stringify({ type: "response.completed", response: {} }) }); ++ }); ++ ++ await codexWsUpstreamFetch(CODEX_URL, streamingInit(), (() => { ++ throw new Error("fallback must not run"); ++ }) as unknown as typeof fetch); ++ ++ const options = FakeWebSocket.instances[0]!.options; ++ expect(options?.proxy).toBe("http://proxy.example:8080"); ++ expect(options?.headers?.authorization).toBe("Bearer test"); ++ expect(options?.headers?.["openai-beta"]).toContain("responses_websockets"); ++ expect(options?.headers?.["content-type"]).toBeUndefined(); ++ }); ++ ++ test.each([ ++ ["unsupported protocol", "socks5://proxy.example:1080"], ++ ["invalid URL", "not a proxy URL"], ++ ])("falls back once without dialing for an %s", async (_label, proxy) => { ++ process.env.HTTPS_PROXY = proxy; ++ const sentinel = new Response("sse-fallback"); ++ let fallbackCalls = 0; ++ const response = await codexWsUpstreamFetch(CODEX_URL, streamingInit(), (async () => { ++ fallbackCalls += 1; ++ return sentinel; ++ }) as typeof fetch); ++ ++ expect(response).toBe(sentinel); ++ expect(fallbackCalls).toBe(1); ++ expect(FakeWebSocket.instances).toHaveLength(0); ++ }); ++ + test("relays event frames as an SSE response and sends one response.create frame", async () => { + installFake(ws => { + ws.emit("open", {}); +@@ -654,6 +706,7 @@ describe("codexWsUpstreamFetch", () => { + }); + + test("falls back to the HTTP fetch when the upgrade is rejected before open", async () => { ++ process.env.HTTPS_PROXY = "http://proxy.example:8080"; + installFake(ws => ws.close()); + const sentinel = new Response("sse-fallback", { status: 429 }); + let fallbackCalls = 0; +@@ -666,6 +719,7 @@ describe("codexWsUpstreamFetch", () => { + expect(response).toBe(sentinel); + expect(isCodexWsUpstreamResponse(response)).toBe(false); + expect(fallbackCalls).toBe(1); ++ expect(FakeWebSocket.instances[0]!.options?.proxy).toBe("http://proxy.example:8080"); + }); + + test("falls back to the HTTP fetch when the upgrade deadline elapses without open or close", async () => { +@@ -800,15 +854,17 @@ describe("codexWsUpstreamFetch", () => { + }); + + test("preserves caller headers on the handshake without fabricating an originator", async () => { +- const seen: Record[] = []; ++ process.env.HTTPS_PROXY = "http://proxy.example:8080"; ++ process.env.NO_PROXY = "chatgpt.com:443"; ++ const seen: FakeWebSocketOptions[] = []; + FakeWebSocket.script = ws => { + ws.emit("open", {}); + ws.emit("message", { data: JSON.stringify({ type: "response.completed", response: {} }) }); + }; + class HeaderCapturingWebSocket extends FakeWebSocket { +- constructor(url: string, options?: { headers?: Record }) { +- super(url); +- seen.push(options?.headers ?? {}); ++ constructor(url: string, options?: FakeWebSocketOptions) { ++ super(url, options); ++ seen.push(options ?? {}); + } + } + globalThis.WebSocket = HeaderCapturingWebSocket as unknown as typeof WebSocket; +@@ -817,18 +873,19 @@ describe("codexWsUpstreamFetch", () => { + await codexWsUpstreamFetch(CODEX_URL, streamingInit(), fallback); + // Without a caller originator none is invented: pool/forward traffic must + // not impersonate Codex CLI (metadata-integrity contract). +- expect(seen[0].originator).toBeUndefined(); +- expect(seen[0]["openai-beta"]).toContain("responses_websockets"); +- expect(seen[0].authorization).toBe("Bearer test"); ++ expect(seen[0].proxy).toBeUndefined(); ++ expect(seen[0].headers?.originator).toBeUndefined(); ++ expect(seen[0].headers?.["openai-beta"]).toContain("responses_websockets"); ++ expect(seen[0].headers?.authorization).toBe("Bearer test"); + // HTTP body-framing headers do not belong on a WS handshake. +- expect(seen[0]["content-type"]).toBeUndefined(); ++ expect(seen[0].headers?.["content-type"]).toBeUndefined(); + + // A genuine caller originator is forwarded verbatim. + await codexWsUpstreamFetch(CODEX_URL, { + ...streamingInit(), + headers: { ...streamingInit().headers as Record, originator: "codex_cli_rs" }, + }, fallback); +- expect(seen[1].originator).toBe("codex_cli_rs"); ++ expect(seen[1].headers?.originator).toBe("codex_cli_rs"); + }); + + test("aborting before open rejects like an aborted fetch", async () => { +@@ -1194,6 +1251,8 @@ describe("oversized Codex create frames", () => { + }); + + test("dials the configured provider's own wss URL for an opt-in upstream", async () => { ++ process.env.HTTPS_PROXY = "http://proxy.example:8080"; ++ process.env.NO_PROXY = "sub2api.example.com:443"; + installFake(ws => { + ws.emit("open", {}); + ws.emit("message", { data: JSON.stringify({ type: "response.completed", response: { id: "r-ws" } }) }); +@@ -1206,6 +1265,7 @@ describe("oversized Codex create frames", () => { + ); + expect(FakeWebSocket.instances).toHaveLength(1); + expect(FakeWebSocket.instances[0]!.url).toBe("wss://sub2api.example.com/v1/responses"); ++ expect(FakeWebSocket.instances[0]!.options?.proxy).toBeUndefined(); + expect(response.headers.get("content-type")).toContain("text/event-stream"); + expect(await response.text()).toContain("response.completed"); + }); +diff --git a/tests/server/proxy-env.test.ts b/tests/server/proxy-env.test.ts +index e43ad2d9b..c795c6cf2 100644 +--- a/tests/server/proxy-env.test.ts ++++ b/tests/server/proxy-env.test.ts +@@ -1,8 +1,10 @@ + import { afterEach, beforeEach, describe, expect, test } from "bun:test"; ++import { createServer } from "node:http"; + import { applyProxyEnv } from "../../src/config"; ++import { resolveProxyRoute } from "../../src/lib/proxy-env"; + import type { OcxConfig } from "../../src/types"; + +-const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", "OCX_TEST_PROXY_REF", "OCX_TEST_NO_PROXY_REF"] as const; ++const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy", "OCX_TEST_PROXY_REF", "OCX_TEST_NO_PROXY_REF"] as const; + let saved: Record; + + beforeEach(() => { +@@ -30,6 +32,128 @@ function configWithRawProxy(proxy: unknown, noProxy?: unknown): OcxConfig { + return { proxy, noProxy, providers: {} } as unknown as OcxConfig; + } + ++describe("resolveProxyRoute", () => { ++ test("wss uses HTTPS_PROXY and never HTTP_PROXY", () => { ++ const target = new URL("wss://chatgpt.com/backend-api/codex/responses"); ++ expect(resolveProxyRoute(target, { ++ HTTPS_PROXY: "http://secure-proxy.example:8443", ++ HTTP_PROXY: "http://plain-proxy.example:8080", ++ })).toEqual({ kind: "proxy", proxy: "http://secure-proxy.example:8443" }); ++ expect(resolveProxyRoute(target, { ++ HTTP_PROXY: "http://plain-proxy.example:8080", ++ })).toEqual({ kind: "direct" }); ++ }); ++ ++ test.each([ ++ ["exact host", "wss://chatgpt.com/path", "chatgpt.com", "direct"], ++ ["domain suffix", "wss://api.chatgpt.com/path", ".chatgpt.com", "direct"], ++ ["wildcard suffix", "wss://api.chatgpt.com/path", "*.chatgpt.com", "direct"], ++ ["wss default port", "wss://chatgpt.com/path", "chatgpt.com:443", "direct"], ++ ["ws default port", "ws://chatgpt.com/path", "chatgpt.com:80", "direct"], ++ ["port mismatch", "wss://chatgpt.com/path", "chatgpt.com:80", "proxy"], ++ ["bracketed IPv6", "wss://[2001:db8::1]/path", "[2001:db8::1]:443", "direct"], ++ ["URL-style entry", "wss://chatgpt.com/path", "https://chatgpt.com/ignored", "direct"], ++ ] as const)("honors NO_PROXY for %s", (_label, target, noProxy, expectedKind) => { ++ expect(resolveProxyRoute(new URL(target), { ++ HTTPS_PROXY: "http://secure-proxy.example:8443", ++ NO_PROXY: noProxy, ++ }).kind).toBe(expectedKind); ++ }); ++ ++ test("uses stable proxy precedence and fails closed on the first unusable proxy", () => { ++ const target = new URL("wss://chatgpt.com/backend-api/codex/responses"); ++ const route = (env: Record) => resolveProxyRoute(target, env); ++ expect([ ++ route({ HTTPS_PROXY: "http://upper-https:1", https_proxy: "http://lower-https:2", ALL_PROXY: "http://upper-all:3", all_proxy: "http://lower-all:4" }), ++ route({ HTTPS_PROXY: " ", https_proxy: "http://lower-https:2", ALL_PROXY: "http://upper-all:3" }), ++ route({ ALL_PROXY: "http://upper-all:3", all_proxy: "http://lower-all:4" }), ++ route({ all_proxy: "https://lower-all:4" }), ++ route({ HTTPS_PROXY: "socks5://unsupported:1080", ALL_PROXY: "http://must-not-win:3" }), ++ route({ HTTPS_PROXY: "not a proxy URL", ALL_PROXY: "http://must-not-win:3" }), ++ route({}), ++ ]).toEqual([ ++ { kind: "proxy", proxy: "http://upper-https:1" }, ++ { kind: "proxy", proxy: "http://lower-https:2" }, ++ { kind: "proxy", proxy: "http://upper-all:3" }, ++ { kind: "proxy", proxy: "https://lower-all:4" }, ++ { kind: "fallback" }, ++ { kind: "fallback" }, ++ { kind: "direct" }, ++ ]); ++ }); ++ ++ test("preserves uppercase NO_PROXY precedence when it is explicitly empty", () => { ++ expect(resolveProxyRoute(new URL("wss://chatgpt.com/path"), { ++ HTTPS_PROXY: "http://secure-proxy.example:8443", ++ NO_PROXY: "", ++ no_proxy: "chatgpt.com", ++ })).toEqual({ kind: "proxy", proxy: "http://secure-proxy.example:8443" }); ++ }); ++ ++ test("Bun WebSocket sends WSS through an HTTP CONNECT proxy", async () => { ++ let resolveConnect!: (target: string) => void; ++ const connected = new Promise(resolve => { resolveConnect = resolve; }); ++ const proxy = createServer(); ++ proxy.on("connect", (request, socket) => { ++ resolveConnect(request.url ?? ""); ++ socket.end("HTTP/1.1 502 Probe Complete\r\nContent-Length: 0\r\n\r\n"); ++ }); ++ await new Promise((resolve, reject) => { ++ proxy.once("error", reject); ++ proxy.listen(0, "127.0.0.1", resolve); ++ }); ++ const address = proxy.address(); ++ if (!address || typeof address === "string") throw new Error("proxy did not bind a TCP port"); ++ const socket = new WebSocket("wss://proxy-probe.invalid/backend-api/codex/responses", { ++ proxy: `http://127.0.0.1:${address.port}`, ++ } as unknown as string[]); ++ try { ++ expect(await Promise.race([ ++ connected, ++ new Promise((_, reject) => setTimeout(() => reject(new Error("CONNECT was not observed")), 5_000)), ++ ])).toBe("proxy-probe.invalid:443"); ++ } finally { ++ try { socket.close(); } catch { /* probe is already complete */ } ++ await new Promise(resolve => proxy.close(() => resolve())); ++ } ++ }, 10_000); ++ ++ test.skipIf(process.platform !== "win32")("Bun fetch honors NO_PROXY on Windows", async () => { ++ let providerRequests = 0; ++ let proxyRequests = 0; ++ const provider = createServer((_request, response) => { ++ providerRequests += 1; ++ response.end("direct"); ++ }); ++ const proxy = createServer((_request, response) => { ++ proxyRequests += 1; ++ response.end("proxied"); ++ }); ++ const listen = async (server: typeof provider): Promise => { ++ await new Promise((resolve, reject) => { ++ server.once("error", reject); ++ server.listen(0, "127.0.0.1", resolve); ++ }); ++ const address = server.address(); ++ if (!address || typeof address === "string") throw new Error("server did not bind a TCP port"); ++ return address.port; ++ }; ++ const [providerPort, proxyPort] = await Promise.all([listen(provider), listen(proxy)]); ++ process.env.HTTP_PROXY = `http://127.0.0.1:${proxyPort}`; ++ process.env.NO_PROXY = "127.0.0.1"; ++ try { ++ expect(await (await fetch(`http://127.0.0.1:${providerPort}/models`)).text()).toBe("direct"); ++ expect(providerRequests).toBe(1); ++ expect(proxyRequests).toBe(0); ++ } finally { ++ await Promise.all([ ++ new Promise(resolve => provider.close(() => resolve())), ++ new Promise(resolve => proxy.close(() => resolve())), ++ ]); ++ } ++ }); ++}); ++ + describe("applyProxyEnv with values the schema does not constrain", () => { + test("warns once per discarded proxy setting without exposing its raw value", () => { + const secret = "raw-proxy-credential-sentinel-2947"; +@@ -122,6 +246,14 @@ describe("applyProxyEnv", () => { + expect(process.env.HTTP_PROXY).toBe("http://proxy.corp:8080"); + }); + ++ test.each(["ALL_PROXY", "all_proxy"])("config fills a scheme proxy ahead of %s for WSS", key => { ++ process.env[key] = "http://fallback-proxy.example:8081"; ++ applyProxyEnv(configWithProxy("http://configured-proxy.example:8080")); ++ expect(process.env[key]).toBe("http://fallback-proxy.example:8081"); ++ expect(resolveProxyRoute(new URL("wss://chatgpt.com/backend-api/codex/responses"))) ++ .toEqual({ kind: "proxy", proxy: "http://configured-proxy.example:8080" }); ++ }); ++ + test("appends loopback entries to an existing NO_PROXY without duplicating", () => { + process.env.NO_PROXY = "internal.corp,localhost"; + applyProxyEnv(configWithProxy("http://proxy.corp:8080")); +@@ -217,4 +349,3 @@ describe("applyProxyEnv with proxy: \"auto\" (#1525)", () => { + expect(process.env.HTTP_PROXY).toBeUndefined(); + }); + }); +- +``` diff --git a/devlog/_fin/260906_a_runtime_stack/021_ws_refresh.md b/devlog/_fin/260906_a_runtime_stack/021_ws_refresh.md new file mode 100644 index 0000000000..f6de6471c3 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/021_ws_refresh.md @@ -0,0 +1,3 @@ +# WebSocket layer P refresh + +Consume 020 above prepared SSE parent 4b34cbb8d, with source #3679 b05cccf264b4ab61db5d8dee8232c2f89bb1b541. Public author updated the old head and resolved the three original review threads. Retain Clive Rosfield attribution and -x source identity. Existing shared proxy-formats documentation contains SSE paragraph; preserve both sections. B owns concurrent providerContextLimits config changes; A updates only applyProxyEnv. This layer stays independently verified and draft while full CI runs; main merges only after full required gates. No local project checks. diff --git a/devlog/_fin/260906_a_runtime_stack/030_recovery.md b/devlog/_fin/260906_a_runtime_stack/030_recovery.md new file mode 100644 index 0000000000..92accfca44 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/030_recovery.md @@ -0,0 +1,208 @@ +# 030 — Native MESSAGE recovery and cached replay (#3568) + +Status: candidate implementation plan, researched 2026-09-06 KST. This is a +docs-only deliverable. Revalidate during this layer's P after preceding layers +land; no implementation or verification pass is claimed here. + +## Implementation-cycle completion versus landing + +This decade cycle ends with a reviewed prepared draft PR, exact-carried-head focused remote activation evidence and remote typecheck, with full CI dispatched. That cycle D does not claim the bug shipped, full CI passed, or an issue resolved. `080_landing.md` retains the mandatory full current-head cross-platform/type/privacy/docs evidence, review, dev ancestry and immediate source-PR/fully-resolved-issue closure gates. Later P consumes the verified prepared stack parent; it need not have landed yet. Only final landing yields feature DONE. + + +## Loop specification and scope + +- Class: C4 for the existing recovery admission boundary; C3 for destination + normalization. Archetype: spec-satisfaction repair, one implementation PABCD + cycle for this decade document. +- Trigger: native parent MESSAGE delivery or replayed encrypted task history on + an opted-in routed child. Goal: preserve the admitted plaintext assignment and + deliver supported plaintext Go Responses agent messages. +- Non-goals: #3571 catalog/effort ordering, multipart recovery, native-backend + retry policy, new credential sources, recovery enabled by default, new routing + metadata protocol, deployment/release work, or a general solution to #3661. +- Verifier: exact-layer remote focused regressions, full Cross-platform CI, + privacy and type gates, and independent recovery-boundary review. Commands + below are planned for remote execution only; none ran during planning. +- Stop condition: reviewed prepared draft and exact-head remote focused/type evidence; full CI/dev inclusion are required by 080 before feature completion. Partial #3661 stays open. +- Memory artifact: this file and main-owned `000` roadmap/evidence ledger. +- Outcomes: DONE only with the evidence above; NOOP only if current dev already + contains equivalent behavior and regressions; BLOCKED for external CI/review + dependencies; UNSAFE/NEEDS_HUMAN for a necessary expansion of admission policy. +- Delegation: inherited parallel read-only reviewers authorized. Downward scope + changes require a P amendment; main reclaims a packet after two distinct worker + failures. Main owns FSM, implementation, commits and stack integration. +- Resource scope: existing gh credentials; later writes restricted to own stack + branches and scoped PR administration. This worker writes only this plan and + `040_affinity.md`. No explicit user token/cost cap; a 2-hour checkpoint triggers + reassessment, not an automatic success or exhaustion claim. No local tests, + typecheck, build, Git mutation or GitHub mutation in this planning task. +- Public record rule: this file describes already-public PR behavior and general + integration requirements. Any new security investigation belongs in `.tmp/`. + +## Provenance and current source + +Live GitHub dev and local HEAD both resolve to +`81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. Original PR +[3568](https://github.com/lidge-jun/opencodex/pull/3568) head is +`036a9321788464fdf33a387c9f44a834a844bdc1`, retained as +`refs/codex/a-original/3568`. The earlier `origin/a-original-*` refs were pruned; +do not depend on them. Read the complete feature diff using +`git diff origin/dev...refs/codex/a-original/3568`, not `HEAD^..HEAD` (the last +two commits are documentation corrections). + +Original author: `voiys ` (GitHub `voiys`). Preserve these +commits in order when carrying the work: + +1. `e8f8726040dbc45b1e946d59db6b9c477459b8d7` — recovery implementation. +2. `4464892336c75b8861ee4caeddfd97d6c4e0e6ab` — canonical Go destination docs. +3. `036a9321788464fdf33a387c9f44a834a844bdc1` — forward-auth exception docs. + +A rewritten/squashed carrying commit and final squash body must contain +`Co-authored-by: voiys `; cite the original PR in the new PR. +Do not force-push the contributor branch. + +Source anchors at the inspected dev SHA: + +- `src/server/responses/agent-task-recovery.ts:61`: envelope type; line 72 + accepts only NEW_TASK; line 74 selects the supported tail envelope; line 180 + injects validated plaintext; line 277 performs admission and line 287 creates + the existing cache key, including message type and parent scope. +- `src/server/responses/agent-task-recovery-cache.ts:23`: existing deletion/byte + accounting; line 43 sets original expiry; line 117 owns resolving cache/flight + behavior. Reuse these owners instead of adding another cache. +- `src/server/responses/core.ts:3233`: final-route recovery gate currently also + requires an unreadable current task. Lines 3261–3282 own reparsing, preserved + continuation fields and the existing non-persistable-body handling. +- `src/adapters/openai-responses.ts:2354`: body expansion/previous-response + handling before effort mapping is the original insertion point. +- `structure/10_adapter-registry.md:5`: adapter factory authority remains the + registry. `opencode-go.ts` below is a destination helper, not a new adapter id. + +Owner search used `isOpenCodeGo`, `normalizeOpenCodeGoAgentMessages`, +`recoverEncryptedAgentTask` and recovery-cache exports. No equivalent Go helper +exists in current dev. Doing nothing retains the public regression; configuration +alone cannot admit MESSAGE or restore history. Reuse admission, injection, cache +deletion and Responses construction; do not duplicate them. + +## Exact implementation change map + +| Action / path | Before → planned after | +|---|---| +| MODIFY `src/server/responses/agent-task-recovery.ts` | Widen `AgentEnvelope.messageType` and the local parse variable to `"NEW_TASK" \| "MESSAGE"`; ROUTING_HEADER captures either and assigns the captured value. Add `restoreCachedEncryptedAgentTasks(req,input,config,{parentThreadId})`: scan only agent_message entries, reuse `admittedRecovery` on each singleton, read the existing cache, and call `injectAssignment` only for a valid hit; return restored count. Fresh recovery continues to handle only the supported tail. | +| MODIFY `src/server/responses/agent-task-recovery-cache.ts` | Export `cachedAgentTaskRecovery(key): string \| null`; return null on miss; delete expired entries with existing `deleteRecoveryCacheEntry`; return live assignment without extending TTL, creating a flight or performing network I/O. | +| MODIFY `src/server/responses/core.ts` | Import restoration helper. Retain Responses/spawn/opt-in/final-route/combo/pass-through exclusions, remove only the outer unreadable-tail prerequisite, restore history first, recompute unreadability, and attempt fresh recovery only when still needed. Feed actual successful restoration/recovery into the existing reparse/route-selection path; preserve continuation fields and existing non-persistence handling. | +| NEW `src/adapters/opencode-go.ts` | Add `isOpenCodeGo(baseUrl)` using URL origin `https://opencode.ai` and normalized path `/zen/go/v1`; malformed/other URLs return false. Add `normalizeOpenCodeGoAgentMessages(body)` with unchanged-reference no-op; convert only nonempty agent_message content arrays entirely composed of input_text/input_image/input_file into user messages; preserve original content parts and add readable author/recipient context. No encrypted/unknown-part conversion. | +| MODIFY `src/adapters/openai-responses.ts` | Import helpers; after `stripPreviousResponseId`, apply normalization only for `!forward && isOpenCodeGo(provider.baseUrl)`, before effort mapping. Preserve raw replay body and existing session headers. | +| NEW `tests/providers/opencode-go-agent-messages.test.ts` | Carry original provider tests and add canonical-Go forward-auth, renamed-provider/trailing-slash URL, malformed/other URL and input_file/empty/mixed unknown-part cases. Assert adapter output and source-body identity, not helper existence. | +| NEW `tests/server/server-agent-task-recovery-replay.test.ts` | Carry original replay/MESSAGE/mixed-history tests. Extend real handler coverage for known history plus a fresh tail and for cache-only continued turns. Check outbound body and recovery fetch counts, not just helper return values. | +| MODIFY `tests/server/agent-task-recovery-cache.test.ts` | Exercise the new read-only accessor on hit, miss and exact expiry; assert repeated reads do not extend lifetime or create recovery flights and expiry uses existing byte-accounting deletion. Reuse existing clock isolation. | +| MODIFY `scripts/test-layout/layout.json` | Register `opencode-go-agent-messages.test.ts` under providers and `server-agent-task-recovery-replay.test.ts` under server in `explicit`. Preserve other registrations. | +| MODIFY `tests/fixtures/test-layout-expected.json` | Add the same two basename/domain mappings. | +| MODIFY `docs-site/src/content/docs/reference/adapters.md` | Carry original non-forward canonical-Go conversion paragraph and recovery link. | +| MODIFY `docs-site/src/content/docs/reference/configuration/providers.md` | Carry original Go section specifying URL, adapter, forward exclusion, cached history versus fresh-tail behavior and context-only identities. | + +No DELETE paths. Existing tests/security/fallback/combo helpers are read/reused; +extend an existing test file only by a documented P amendment if its fixture is +the right home for an uncovered acceptance row. No catalog files in this layer. + +The enum chain is complete: creation is ROUTING_HEADER capture in +`findEnvelope`; serialization is `recoveryPayload` at line 303 plus the existing +message-type cache-key hash at line 292; deserialization/unknown handling remains +the strict envelope matcher and assignment validation at line 171; consumers are +admission, fresh recovery, cache restoration and injection. There is no persisted +enum migration. Recipient consistency remains enforced by existing envelope +validation; do not claim a new independent recipient cache-key field. + +## Activation and independent acceptance + +| Trigger | Observable acceptance | +|---|---| +| Opted-in valid MESSAGE on routed spawned Responses | One recovery request containing MESSAGE; provider receives recovered text; response succeeds. NEW_TASK remains equivalent. | +| Previously admitted ciphertext replayed after tool output or user continuation | Restored plaintext reaches actual provider body; recovery-call count does not increase. | +| Cached NEW_TASK + cached MESSAGE + distinct uncached current MESSAGE | Each known entry restores its own payload; only tail creates one fresh recovery; later replay creates no further recoveries. | +| Unknown historical ciphertext and a recoverable tail | Historical entry remains unchanged; do not claim batch history recovery. Keep existing terminal decision behavior when unsupported unreadability remains. | +| Miss, exact expiry, repeated reads before expiry | No replacement/fetch on read miss; unchanged original expiry and bounded accounting. | +| Other parent/caller/account/message type, malformed envelope or unsupported type | No cache restoration; original input remains unchanged. Existing admission negative suite stays green. | +| Recovery absent/disabled, native forward, trusted pass-through, combo attempt | Existing routing/admission behavior remains; opt-out makes no newly introduced recovery request. | +| Canonical Go non-forward plaintext text/image/file message | Public user message with original parts and readable identities; raw replay input not mutated. | +| Go forward, another destination, unknown/encrypted part, empty content | No Go conversion. Test canonical Go forward directly, not only ChatGPT forward. | +| Recovery success followed by reparse | Continuation fields survive; current route/selection and existing non-persistable-body treatment remain correct. | + +This layer must pass without #3581 or #3571. Main integrates this core change +before #3581 and coordinates any C-lane #3576 core edits. Do not use stack order +to invent a dependency on unrelated SSE/WebSocket changes; revalidate shared +core and documentation context after their integration. + +## Reviews, drift and landing handoff + +Live PR is non-draft, MERGEABLE, REVIEW_REQUIRED. GraphQL returned two resolved +threads, zero unresolved. Preserve both corrections: +[canonical destination](https://github.com/lidge-jun/opencodex/pull/3568#discussion_r3939042861) +and [forward exception](https://github.com/lidge-jun/opencodex/pull/3568#discussion_r3939864549). +The earlier four-topic maintainer review was addressed by moving catalog work to +#3571; do not restore those removed hunks. Its mixed-history concern is represented +in current original tests and the acceptance table. Sender/recipient text is +model context only. The original author reports 19,287 full-suite passes and a +live test on an equivalent local release patch; neither proves the new stack head. + +At later P, compare original feature patch against the actual parent tree, +refresh PR head/reviews and identify new exact-path overlap. Carry all three +original commits, preserve authorship and review corrections, then add focused +integration corrections separately. Main may push own stack branches with +`--no-verify` as authorized. Parent merge/squash requires child replay onto the +new dev ancestry and new head evidence; retarget children before deleting parent +branches. Close carried #3568 only after dev contains the result. Reference +#3661 as partial coverage, never `Closes #3661` for this slice. + +## Remote-only verification plan + +Planning exception to PLAN-VERIFIER-REAL-01: user forbids running tests, +typecheck/build locally and requests static workflow inspection now. Every +command here has execution status **NOT RUN**, exit code **N/A**. Later main +records remote command, exact checkout SHA, result and log URL/receipt. + +Remote execution handoff (main verified): `the isolated remote verification host` has +`REMOTE_SOURCE_CHECKOUT` and Bun 1.3.14. Main creates an isolated remote clone +and checks out the exact carried SHA; the existing checkout is a source for +setup, not a shared mutable test directory. Implementation C runs focused +activation tests and typecheck there. Carry PR remains draft until full +current-head GitHub CI is green; final landing cycle requires every full gate. +The local package pins Bun 1.4.0, so the Bun 1.3.14 focused result is supplemental +and cannot replace the workflow's configured-runtime full gates. + +In that isolated remote checkout, focused C commands are: + +```sh +bun test tests/server/server-agent-task-recovery-replay.test.ts tests/providers/opencode-go-agent-messages.test.ts tests/server/agent-task-recovery-cache.test.ts +bun test tests/server/agent-task-recovery.test.ts tests/server/agent-task-recovery-security.test.ts tests/server/agent-task-recovery-fallback.test.ts tests/server/agent-task-recovery-combo.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts +bun run typecheck +``` + +Full landing gates, on remote runners only: + +```sh +bun run test +bun run privacy:scan +bun --cwd docs-site run build +``` + +Direct test arguments observe the named target/imports; layout guards observe +both manifests. `package.json:43` defines the full test script, +`scripts/test.ts:321` adds `./tests/`, and `tsconfig.json:15` includes `src`. +The docs build is a separate remote requirement; ordinary runtime CI does not +prove prose accuracy. Review the two docs against actual adapter conditions. + +Statically verified CI coverage: `.github/workflows/ci.yml:7` has no PR-base +filter, so child PRs qualify; lines 182–186 match `src/**`, `tests/**` and +`scripts/**`. Linux line 316 calls `scripts/ci/run-bun-test-batches.sh`, whose +line 197 enumerates tests recursively and line 58 accepts `.test.ts` files. +macOS line 532 and Windows line 754 run the tests directory in shards. Lines +422–431 run typecheck and privacy. Require actual producer jobs to succeed; +green intake/aggregate checks with skipped tests are insufficient. + +Main's alternative manual CI invocation is +`gh workflow run ci.yml --repo lidge-jun/opencodex --ref OWN_LAYER_BRANCH -f lane=all`. +The workflow supports lane, not an invented expected-SHA input. Capture the run's +headSha and checkout provenance and reject stale results; PR workflows normally +test the synthetic merge ref, so record both PR head and tested merge SHA. +No workflow or runner approval was issued by this planner. diff --git a/devlog/_fin/260906_a_runtime_stack/031_recovery_refresh.md b/devlog/_fin/260906_a_runtime_stack/031_recovery_refresh.md new file mode 100644 index 0000000000..9112a2c8d7 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/031_recovery_refresh.md @@ -0,0 +1,3 @@ +# Recovery layer P refresh + +Consume 030 above prepared WS parent10fbda2e0. Original #3568 remains open at036a9321788464fdf33a387c9f44a834a844bdc1; carry all three voiys commits in order. No catalog/effort hunks from #3571. Add the planned cache exact-expiry/no-TTL-extension and canonical-Go conversion negatives, with a scoped inherited worker owning only the named three regression files after original carry. Main owns production integration, author commits and review. Runtime correction: isolated checks now invoke repository node_modules/.bin/bun and assert package.json dependencies.bun=1.4.0 before any execution. Full per-head CI remains mandatory before landing. #3661 remains partial, with no automatic close reference. diff --git a/devlog/_fin/260906_a_runtime_stack/040_affinity.md b/devlog/_fin/260906_a_runtime_stack/040_affinity.md new file mode 100644 index 0000000000..8c1380a72e --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/040_affinity.md @@ -0,0 +1,210 @@ +# 040 — Command Code conversation affinity (#3581) + +Status: candidate implementation plan, researched 2026-09-06 KST. Depends on the +verified `030_recovery.md` layer for stack integration into its reparse owner. +This first-cycle artifact is docs only; re-read current source at this layer's P. + +## Implementation-cycle completion versus landing + +This decade cycle ends with a reviewed prepared draft PR, exact-carried-head focused remote activation evidence and remote typecheck, with full CI dispatched. That cycle D does not claim the bug shipped, full CI passed, or an issue resolved. `080_landing.md` retains the mandatory full current-head cross-platform/type/privacy/docs evidence, review, dev ancestry and immediate source-PR/fully-resolved-issue closure gates. Later P consumes the verified prepared stack parent; it need not have landed yet. Only final landing yields feature DONE. + + +## Loop specification and scope + +- Class: C4 for conversation/cohort isolation; archetype: spec-satisfaction + repair. One implementation PABCD cycle owns this document. +- Trigger: repeated Command Code requests from the same identifiable conversation. + Goal: stable opaque session affinity without treating shared cache cohorts as + individual conversations; enable API-key provider cache-key forwarding. +- Non-goals: Hermes #3433 diagnosis, measured cache-hit/cost promises, OAuth + refresh changes, a global session registry, prompt-text-derived identity, + default trust for unclassified cache keys, or extra OAuth cache-key forwarding. +- Verifier: remote identity/forwarding/reparse regressions, full current-head CI, + privacy/type gates and independent boundary review. No local verifier runs. +- Stop: reviewed prepared draft atop recovery, with exact-head remote focused/type evidence. Full current-head gates and dev ancestry remain required in 080. +- Memory artifact: this file plus main-owned roadmap/ledger. Main alone owns + FSM, goal, implementation, Git and stack integration. +- Resources: existing gh credentials and later own-branch writes only. Inherited + parallel reviewers authorized; downward changes are a P amendment and main + reclaims after two distinct worker failures. No explicit user token/cost cap; + 2-hour checkpoint triggers reassessment. This planner writes only the two + assigned documents; no Git/GitHub mutations or tests/typecheck/build. +- Public scope: already-public patch behavior and general integration plan only; + new security investigation notes belong in `.tmp/`. + +## Provenance and source anchors + +Live dev/local HEAD: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. +[Original PR #3581](https://github.com/lidge-jun/opencodex/pull/3581) head and its +single feature commit: `f60397d3408e0339ffc66acdcaca8133e40866c2`, retained at +`refs/codex/a-original/3581`. Author: `SB Yoon +<44089734+yansigit@users.noreply.github.com>` (GitHub `yansigit`), original +authored date 2026-09-05T01:58:50Z. Preserve original author on carry and include +`Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com>` in any +rewritten/squashed landing. Do not rewrite the contributor's branch. + +Current source still uses `randomUUID()` unconditionally at +`src/adapters/command-code.ts:528`. The helper insertion owner is the same file +after `projectSlug` at line 212. Current `src/server/responses/core.ts:2954` +parses the request, line 2987 assigns inbound thread id, lines 2990–3004 classify +the separate replay scope, and line 3262 preserves fields after recovery. +`src/types/request.ts:71` holds `_clientThreadId` without the proposed cohort +field. `src/providers/registry.ts:2169` is API-key `commandcode`; line 1332 is +OAuth `command-code`. They are separate transport contracts. + +Owner searches: `commandCodeSessionId`, `promptCacheKeyIsSharedCohort`, +`prompt_cache_key`, `_clientThreadId`, `_reasoningReplayScope`. Existing +classification, provider derivation and Chat serialization already exist; reuse +them. `src/providers/xai-transport.ts:101` has a different provider's derivation; +do not reuse its namespace/contract for Command Code. Doing nothing retains +random affinity, configuration cannot change the header builder, and no matching +Command Code helper exists. No new cache/service dependency is warranted. + +## Exact implementation change map + +| Action / path | Before → planned after | +|---|---| +| MODIFY `src/adapters/command-code.ts` | Import `createHash` alongside `randomUUID`; add exported `commandCodeSessionId(parsed)`. Select trimmed `_clientThreadId`, else trimmed replay `clientThreadId`, else trimmed `options.promptCacheKey` only when cohort marker is exactly false. With no identity return randomUUID. Hash `command-code:${kind}\0${identity}` with SHA-256 and form the original opaque UUID-shaped value, preserving explicit version/variant nibble comment. Use helper for x-session-id. | +| MODIFY `src/types/request.ts` | Add optional internal `_promptCacheKeyIsSharedCohort?: boolean` beside `_clientThreadId`; document true=shared, false=explicitly conversation-scoped, absent=unclassified. Do not expose it as a client JSON input field. | +| MODIFY `src/server/responses/core.ts` | Immediately after initial `parseRequest(body)`, copy `options.promptCacheKeyIsSharedCohort` onto parsed internal marker. Add marker to the existing `kept` list in recovery reparse, now containing #3568 restoration. Preserve all sibling fields and both true and false values (undefined-only filtering). | +| MODIFY `src/providers/registry.ts` | Add `promptCacheKey: true` only to `commandcode` API-key provider. Leave OAuth `command-code` transport setting unchanged. | +| MODIFY `tests/providers/command-code-provider.test.ts` | Carry stable/opaque identity, precedence, different-identity, UUID shape and random fallback tests; add whitespace-only fallback and same literal under different identity-kind cases. Assert actual built x-session-id as well as helper output. | +| MODIFY `tests/providers/commandcode-provider.test.ts` | Extend registry expectation and construct real Chat request with promptCacheKey, asserting prompt_cache_key body forwarding. Retain explicit disabled-provider override behavior. | +| MODIFY `tests/claude-integration/claude-code-thought-signature-scope.test.ts` | Carry true/false/undefined propagation assertions in existing drive helper; retain the independent replay-scope expectations. | +| MODIFY `tests/server/server-agent-task-recovery-replay.test.ts` | Parent-layer test file exists after 030. Add real handler/adaptor-boundary observation of marker preservation for recovery and cache-only restoration, with true/false/undefined cases. Use the existing fixture/post helper; no source-text assertion as a substitute for executing reparse. | +| MODIFY `docs-site/src/content/docs/reference/adapters.md` | Add a concise Command Code subsection describing OAuth x-session-id priority/random fallback and API-key commandcode prompt_cache_key forwarding separately; no cache-performance promise. This is a docs-sync addition beyond the original seven-file patch. | + +No NEW or DELETE production/test files. The modified replay test is owned by the +parent layer and already registered there. No layout manifest update is needed +for modifying it. Keep the new helper in its existing adapter: no parallel +factory registration or session cache. `structure/10_adapter-registry.md:5` +remains authoritative and needs no factory-policy change; adapters.md is the +user-visible contract sync target. + +## Explicit handler-fixture amendment + +MODIFY `tests/helpers/agent-task-recovery.ts:144-159`: extend the sixth `post` options argument with `promptCacheKeyIsSharedCohort?: boolean`, and forward it to the fourth `handleResponses` options argument alongside abortSignal and translatorBudget. Do not put this internal field in the JSON request body. Existing callers default to undefined and remain unchanged. + +MODIFY `tests/server/server-agent-task-recovery-replay.test.ts`: parameterize true/false/undefined, use the extended `post` helper for an initial admitted recovery and a continued cache-only replay, and observe the parsed request at the real selected adapter buildRequest boundary via a temporary spy restored after each test. Assert the exact internal marker and existing thread/replay metadata on both calls; assert only one recovery backend call. The later P must bind the spy to the actual exported adapter selector in that carried tree. A source-text assertion is not an alternative to the real reparse execution. + +## Complete field and value chain + +1. Creation: `src/server/claude-messages.ts:836` passes + `promptCacheKeyIsSharedCohort: cacheKeySource === "system"` into + `HandleResponsesOptions` (`core.ts:1548`). The new initial-parse assignment + carries true/false/undefined unchanged. `_clientThreadId` and replay scope use + their existing ingress owners; do not infer new trust from request content. +2. Internal transfer: `OcxParsedRequest` optional field and the `kept` list copy + it across `parseRequest` after both fresh and cached recovery. It is process + request metadata, not persisted configuration or continuation data. +3. Serialization/deserialization: the internal marker has no wire representation + and no persisted migration (N/A intentionally). `parseRequest` at + `src/responses/parser.ts:526` already maps public prompt_cache_key into options; + clients cannot supply the internal classification through that mapping. +4. Consumers: `commandCodeSessionId` permits the cache-key fallback only for + `=== false`; true/undefined both fail closed. Existing replay/cohort consumers + at `core.ts:2990`, `core.ts:3560` and + `src/oauth/anthropic-routing.ts:781` keep their distinct semantics; do not + broaden/rewrite those predicates as incidental cleanup. +5. Provider capability chain: registry promptCacheKey → + `src/providers/derive.ts:252` defaults and line 512 reconciliation → routed + provider config → `src/adapters/openai-chat.ts:1573` serialization (and raw + body forwarding at line 156). Original API-key regression observes the wire + body, rather than only asserting registry metadata. + +## Activation and independent acceptance + +| Trigger | Required observation | +|---|---| +| Same trimmed explicit thread, differing replay/cache values | Same opaque x-session-id in actual built requests; thread wins. | +| No explicit thread, same trimmed replay identity | Stable header; changing replay identity changes it. | +| Neither thread nor replay, nonempty key with marker false | Stable cache-derived header; whitespace trimmed. | +| Same literal in thread/replay/cache namespaces | Different opaque values by kind; preserve original hash namespace. | +| Shared=true or unclassified marker, only cache key/prompt text | Fresh UUID each request; no prompt/body-derived identity. | +| Empty/whitespace identity or no identity | Random fallback, no accidental stable empty-string cohort. | +| Explicit thread with shared=true | Explicit thread remains valid; shared classification disqualifies only cache fallback. | +| Initial parse then successful fresh or cache-only recovery reparse | Adapter observes original true/false/undefined marker and original thread/replay metadata; stable affinity semantics survive. | +| API-key commandcode using route-derived config | Chat body carries prompt_cache_key when present/enabled; absent key or explicit disabled capability omits it. | +| OAuth command-code | Uses proprietary x-session-id builder; this patch does not opt its registry entry into Chat cache-key forwarding. | +| Synthetic raw identity strings | Header matches UUID-shaped contract and contains no raw identity. No added identity logging. | + +C must drive both the helper and real adapter/handler paths. This plan claims a +stable request header, not proven provider cache savings or a provider guarantee +that distinct sessions receive distinct workers. Any credentialed live provider +smoke needs main's chosen authorized runtime scope; a synthetic wire test is not +misreported as real upstream acceptance. + +## Review disposition, drift and stack order + +Live PR is non-draft, MERGEABLE, REVIEW_REQUIRED. GraphQL has zero review threads; +there is no current formal approval. The author already incorporated UUID +nibble explanation and retained API-key-only forwarding/unclassified-key +fallback in the original head. Latest +[author update](https://github.com/lidge-jun/opencodex/pull/3581#issuecomment-5549518114) +reports 18,244 passes on `be81013fa` base; those historical results do not validate +the current parent tree. Older draft/failure commentary is superseded. + +The original patch context predates the current core: original initial-parse +line 2896 is now 2954 and original reparse area around 3210 is now 3262. Carry by +function/field ownership; never replace current core with the older file. Refresh +onto the completed 030 layer and preserve both restoration behavior and the new +cohort marker. Coordinate the shared core with C-lane #3576 through main. This +is not a fix for #3433 and must not close that issue. + +Main publishes a child PR targeting the recovery branch if that PR is still +open; after parent squash/merge, replay only this layer onto dev and retarget. +Revalidate exact diff, review and CI for every new head. Original contributor +credit survives cherry-pick/reimplementation/squash. Own-branch `--no-verify` +pushes are authorized; local prepush hooks must not start a suite. Close original +#3581 once the equivalent change is proven on dev; do not close merely because +a carrying child PR exists. No Git/GitHub action is performed by this planner. + +## Remote-only verification and CI coverage + +All commands below: **NOT RUN, exit N/A during planning**, per explicit user +instruction. Later main runs them only in the remote checkout of the exact layer +and records SHA, command result and artifact/CI URL. + +Main verified `REMOTE_HOST:REMOTE_SOURCE_CHECKOUT` and Bun 1.3.14. Use an isolated +remote clone at the exact carried SHA for focused activation tests/typecheck; +do not mutate the existing remote checkout for this layer. Its Bun version +differs from package.json's 1.4.0 pin, so this is supplemental evidence. Carry +PR stays draft until full current-head GitHub CI is green. Final landing cycle +requires every full gate on the configured remote runners. + +Implementation C, in the isolated remote clone: + +```sh +bun test tests/providers/command-code-provider.test.ts tests/providers/commandcode-provider.test.ts tests/claude-integration/claude-code-thought-signature-scope.test.ts tests/server/server-agent-task-recovery-replay.test.ts +bun run typecheck +``` + +Full landing gates, remote only: + +```sh +bun run test +bun run privacy:scan +bun --cwd docs-site run build +``` + +Focused direct arguments cover identity selection, actual request headers/body, +cohort propagation and parent recovery interaction. `tsconfig.json:15` includes +src; `package.json:43` maps full suite to `scripts/test.ts`, whose line 321 adds +`./tests/`. No claim that typecheck covers prose. Docs require remote build plus +manual comparison of actual transport semantics. + +Static workflow proof: `.github/workflows/ci.yml:7` permits child PR bases; +lines 182–186 select runtime/tests, line 316 runs Linux batches, and +`scripts/ci/run-bun-test-batches.sh:197` recursively enumerates tests (accepted +suffixes at line 58). macOS line 532 and Windows line 754 cover tests shards. +Lines 422–431 run typecheck/privacy. Thus this runtime layer should activate +real jobs even though its parent is not dev. Runtime CI does not guarantee the +new documentation subsection's accuracy; review it explicitly. + +Optional later manual dispatch: +`gh workflow run ci.yml --repo lidge-jun/opencodex --ref OWN_LAYER_BRANCH -f lane=all`. +Record run headSha and actual checkout SHA; this workflow exposes only lane, +not expected-SHA pinning. For PR CI record current PR head and synthetic merge +SHA. Require completed successful producer jobs and independent review of the +current patch; author-reported tests, skipped producers, stale green heads and +hygiene checks cannot complete this layer. diff --git a/devlog/_fin/260906_a_runtime_stack/041_affinity_refresh.md b/devlog/_fin/260906_a_runtime_stack/041_affinity_refresh.md new file mode 100644 index 0000000000..38515e7742 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/041_affinity_refresh.md @@ -0,0 +1,5 @@ +# Affinity layer P refresh + +Consume 040 on prepared recovery parent332a30e6d. Original #3581 remains f60397d3408e0339ffc66acdcaca8133e40866c2, with SB Yoon attribution preserved. Retain new recovery cache/history logic and termination WeakMap rebind when applying the two core hunks. The new cohort flag must survive initial parse and both fresh/cache-only reparse; true/undefined never authorize cache-key-based session identity. No changes to OAuth command-code cache-key forwarding; enable the existing API-key commandcode registry capability only. + +Scoped regression worker after carry owns tests/helpers/agent-task-recovery.ts, tests/server/server-agent-task-recovery-replay.test.ts and tests/providers/command-code-provider.test.ts. Use the actual ADAPTER_REGISTRY openai-chat create seam already proven in the parent regression to observe parsed fields at real buildRequest. Main owns production and adapters documentation. Remote helper asserts project Bun1.4.0; no local suites/typecheck/build. Full exact-head CI and --admin integration remain final gates. diff --git a/devlog/_fin/260906_a_runtime_stack/050_capabilities.md b/devlog/_fin/260906_a_runtime_stack/050_capabilities.md new file mode 100644 index 0000000000..51514b6215 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/050_capabilities.md @@ -0,0 +1,684 @@ +# 050 — Effective provider capabilities (#3671) + +## Implementation-cycle completion versus landing + +This decade cycle ends with a reviewed prepared draft PR, exact-carried-head focused remote activation evidence and remote typecheck, with full CI dispatched. That cycle D does not claim the bug shipped, full CI passed, or an issue resolved. `080_landing.md` retains the mandatory full current-head cross-platform/type/privacy/docs evidence, review, dev ancestry and immediate source-PR/fully-resolved-issue closure gates. Later P consumes the verified prepared stack parent; it need not have landed yet. Only final landing yields feature DONE. + + +## Candidate implementation contract + +Status: candidate planning, not implementation or merge approval. Revalidate at this layer's later P after its lower stack layer lands. This document is the delegated docs-only deliverable; the main agent owns roadmap registration, FSM, goal state, branch integration, CI dispatch and closure. + +- Class: C4 for the policy-boundary slice, based on the public PR's requested security review. Archetype: spec-satisfaction repair. +- Trigger: routing policy capability evidence must describe the effective provider dispatch uses, including unavailability. +- Goal: runtime selection and ordinary management dry-run agree on effective transport capabilities and exclude unresolved, missing, or disabled providers before scoring. +- Non-goals: new provider metadata, registry precedence redesign, catalog UI, OAuth refresh, request transport changes, Lab activation changes, release operations, or changing caller-supplied synthetic dry-run evidence semantics. +- Verifier: remote focused routing/API regressions plus exact-head full Cross-platform CI and a remote documentation build. No local tests, typecheck, builds, or verifier execution in this planning assignment. +- Stop: independently working reviewed draft with original authorship and exact-head remote focused/type evidence. Full current-head gates/dev ancestry remain required by 080. +- Memory artifact: this document and the main-owned roadmap/evidence ledger. +- Outcomes: DONE only after verified dev integration; NOOP only if current dev independently contains all behavior and regressions; BLOCKED for unavailable external CI/credentials; NEEDS_HUMAN/UNSAFE for a policy decision outside authorization; a resource checkpoint is reassessment, never fabricated completion. +- Delegation: inherited parallel read-only reviewers are authorized. Main reclaims a packet after two distinct failed workers; further write delegation requires a P amendment with exact ownership. +- Resources: existing gh credentials; future writes confined to the main's own stack branches and explicitly authorized PR/issue integration. This worker writes only this document. No explicit user token/cost cap. A two-hour checkpoint triggers reassessment and an evidence update. No deployment, account-state operation or provider request is necessary. + +## Provenance and refresh gate + +Inspected September 6, 2026 KST using read-only `gh pr view`, `gh api` reviews/workflow runs, `git show`, and `git diff`. + +- Public PR: https://github.com/lidge-jun/opencodex/pull/3671 +- Exact original head: `7b1beb9c5eacd8dde22681a5df26804be52380b8`. +- Stable source ref: `refs/codex/a-original/3671`; old `origin/a-original-*` refs were pruned by parallel workers and must not be relied upon. +- Original base: `6585e6a70f42be8b6c81ff20d4fa0f39f7da03db`. +- Inspected current dev/tree: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. +- Original commits, oldest first: `2b1e0e00c12d7287f9324a4a39ec7e966712affe` (effective capability evidence); `7b1beb9c5eacd8dde22681a5df26804be52380b8` (unresolved transport exclusion). +- Both commits authored by **Hako <25837994+devswha@users.noreply.github.com>**, GitHub `@devswha`. Preserve those authors when carrying commits. Any squash/reimplementation and the carrying PR must retain `Co-authored-by: Hako <25837994+devswha@users.noreply.github.com>` so attribution survives integration. +- A read-only diff of original base against inspected dev shows no drift in the eight original touched files. This is a snapshot, not a promise about the later stack parent. +- #3679's refreshed source head is `b05cccf264b4ab61db5d8dee8232c2f89bb1b541`; it does not replace #3671 provenance. Re-read parent changes and resolve integration ownership at later P. + +At later P, compare live PR head, stable ref, actual stack parent and dev tip. Inspect each named source hunk and public review again. If any changed, amend this document before carrying the patch. Treat stacked ordering as a user-requested integration constraint; #3671 does not need #3568/#3581 runtime code to function and must be independently testable. + +## Source ownership and before/after map + +Reuse the existing `routedProviderConfig` callback seam; no new resolver, registry, server endpoint or config option is required. Doing nothing leaves policy and effective transport divergent; changing configured URLs or deleting capability checks does not fix the contract; duplicating registry logic creates drift. + +| Operation | Exact path | Before → after | +|---|---|---| +| MODIFY | `src/routing/capability.ts` | Lines 153–160 read raw config plus registry by name → optional resolved-provider argument is authoritative; name-only registry fallback applies only to legacy three-argument callers. Add provider-wide reasoning ladder at lines 223–227, retaining no-reasoning precedence. | +| MODIFY | `src/routing/compatibility/assemble.ts` | Lines 52–60 derive capabilities directly → resolve each active configured candidate through the supplied callback, emit bounded unavailability state on missing/disabled/throw, and compute capabilities only from a resolved provider. | +| MODIFY | `src/routing/evaluator.ts` | Evidence type near line 54 and eligibility lines 280–313 lack transport status → optional `routeResolutionFailed`, `route-unavailable` exclusion and hard eligibility gate independent of unknown policy. | +| MODIFY | `tests/routing/routing-capability-model-matching.test.ts` | Existing model-family tests → retain them and add the complete original effective-transport regression group plus missing/disabled selection regressions below. | +| MODIFY | `tests/routing/routing-profile.test.ts` | Existing management dry-run parity fixture at line 446 → add ordinary dry-run missing/disabled candidate matrix without injected `candidates`. | +| MODIFY | `docs-site/src/content/docs/guides/routing-profile-editor.md` | Dry-run section near line 39 lacks effective transport contract → original explanation plus explicit missing/disabled exclusion. | +| MODIFY | `docs-site/src/content/docs/fr/guides/routing-profile-editor.md` | Same change in French near line 38, preserving corrected typographic apostrophe. | +| MODIFY | `docs-site/src/content/docs/tr/guides/routing-profile-editor.md` | Same change in Turkish near line 53. Original trailing blank-line removal is incidental. | +| MODIFY | `docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md` | Same change in Traditional Chinese near line 33. | +| MODIFY | `structure/01_runtime.md` | Router ownership row at line 18 says selection only → describe shared effective-provider evidence and hard unavailable-candidate exclusion. | +| NEW | None in production/tests | Existing test files already have layout entries; do not add layout manifest churn. | + +Only this plan file is created now. The future layer has ten MODIFY paths. General SOT follows `structure/01_runtime.md`; user-facing truth remains the routing guide. No unpublished investigation details belong in this public unit. + +Read-only caller proof: `src/router.ts:299` owns effective registry transport/metadata; `src/router.ts:622` supplies it to assembly and line 625 evaluates; lines 626–635 route the selected provider or throw. `src/server/management/routing-profile-routes.ts:100` supplies the same resolver; lines 384–390 use assembly when `body.candidates` is absent. Preserve the synthetic-evidence branch. `src/routing/capability.ts:130` classifies effective locality; lines 179–193 preserve no-vision precedence. Core/Lab imports remain behind the existing provider slot (`assemble.ts:45`), with no new import of router from assembly. + +## Public review disposition + +Two prior findings are resolved in original head: French typography and thrown route resolution under permissive unknown policy. One remains open: https://github.com/lidge-jun/opencodex/pull/3671#discussion_r3941006079 . At original-head `assemble.ts:57`, missing/disabled providers skip the resolver but leave failure false. Set the initial state to `!provider || provider.disabled === true` and prove both ordinary dry-run and runtime selection. Do not resolve the review on the basis of this plan. + +Current original-head Cross-platform CI run `33973108478` and React Doctor run `33973108496` have conclusion `action_required`; label/hygiene/target success is not product verification. The PR body reports focused successes and a timeout-adjusted affected run, but explicitly does not claim a green default full suite. No such reported run is accepted as this carried layer's verification. Maintainer approval and explicit security review remain pending under `MAINTAINERS.md:57–61`. + +## Exact original carry diff + +Apply this public source patch as one coherent layer, preserving both original commits/author identity. The subsequent corrections below are required in the same layer before review readiness. This is recorded patch text, not an instruction to run local Git mutations during planning. + +````diff +diff --git a/docs-site/src/content/docs/fr/guides/routing-profile-editor.md b/docs-site/src/content/docs/fr/guides/routing-profile-editor.md +index b437c28b3..84b4410f7 100644 +--- a/docs-site/src/content/docs/fr/guides/routing-profile-editor.md ++++ b/docs-site/src/content/docs/fr/guides/routing-profile-editor.md +@@ -37,6 +37,13 @@ résultat du plafond. + + ## Simuler un profil enregistré + ++Les capacités des candidats utilisent la configuration effective du fournisseur, ++après application du registre. Les exigences de localité (`localOnly` et ++`remoteAllowed`) utilisent donc l’adresse amont effective. Si elle ne peut pas être ++classée, `unknownEvidence.capability` détermine l’admissibilité du candidat. ++Une configuration de fournisseur invalide qui ne peut pas être résolue est toujours ++exclue avec `route-unavailable`, même si les capacités inconnues sont autorisées. ++ + Sélectionnez un profil enregistré et utilisez **Évaluation à sec** pour ajouter des éléments propres à la requête, tels que la taille de la fenêtre de contexte, l’utilisation d’outils, l’entrée d’images ou la sortie structurée. La simulation évalue l’admissibilité et la notation, mais n’envoie jamais de requête à un modèle en amont. + + Les modifications non enregistrées ne sont pas prises en compte par la simulation. Enregistrez d’abord le profil afin que la révision et l’évaluation affichées correspondent à la même configuration. +diff --git a/docs-site/src/content/docs/guides/routing-profile-editor.md b/docs-site/src/content/docs/guides/routing-profile-editor.md +index 5cf5fc6d7..7931f29ad 100644 +--- a/docs-site/src/content/docs/guides/routing-profile-editor.md ++++ b/docs-site/src/content/docs/guides/routing-profile-editor.md +@@ -38,6 +38,13 @@ cap outcome. + + ## Dry-run a saved profile + ++Candidate capabilities use the effective provider configuration after registry ++overrides are applied. Locality requirements (`localOnly` and `remoteAllowed`) ++therefore use the effective upstream address. If that address cannot be classified, ++the profile's `unknownEvidence.capability` setting decides eligibility. ++An invalid provider configuration that cannot be resolved is always excluded with ++`route-unavailable`, even when unknown capabilities are allowed. ++ + Select a saved profile and use **Dry-run evaluation** to add request evidence such as context-window size, tool use, image input, or structured output. Dry-run evaluates eligibility and scoring but never sends an upstream model request. + + Unsaved edits are not used by dry-run. Save the profile first so the displayed revision and evaluation refer to the same configuration. +diff --git a/docs-site/src/content/docs/tr/guides/routing-profile-editor.md b/docs-site/src/content/docs/tr/guides/routing-profile-editor.md +index dd7aa50d7..ec75bdd17 100644 +--- a/docs-site/src/content/docs/tr/guides/routing-profile-editor.md ++++ b/docs-site/src/content/docs/tr/guides/routing-profile-editor.md +@@ -52,6 +52,13 @@ ayrıdır. + + ## Kaydedilmiş bir profilde deneme çalıştırması (dry-run) yapma + ++Aday yetenekleri, kayıt defteri kuralları uygulandıktan sonraki etkin sağlayıcı ++yapılandırmasını kullanır. Yerellik gereksinimleri (`localOnly` ve `remoteAllowed`) ++bu nedenle etkin üst sunucu adresine göre değerlendirilir. Adres sınıflandırılamıyorsa, ++adayın uygunluğunu profilin `unknownEvidence.capability` ayarı belirler. ++Çözümlenemeyen geçersiz sağlayıcı yapılandırmaları, bilinmeyen yeteneklere izin ++verilse bile `route-unavailable` ile her zaman dışlanır. ++ + Kaydedilmiş bir profili seçin ve bağlam penceresi boyutu, araç kullanımı, görsel + girişi veya yapılandırılmış çıktı gibi istek kanıtları eklemek için **Deneme + çalıştırması değerlendirmesi (Dry-run evaluation)**'ı kullanın. Deneme +@@ -99,5 +106,3 @@ Düzenleyici şu uç noktaları kullanır: + } + } + ``` +- +- +diff --git a/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md b/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md +index e6ae93a76..0b54e70d5 100644 +--- a/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md ++++ b/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md +@@ -32,6 +32,9 @@ OpenCodex 儀表板中的 **Models → Routing** 分頁可以直接管理 `confi + + ## 試跑已儲存的設定檔 + ++候選能力使用套用 registry 覆寫後的有效供應商設定。因此,本地性需求(`localOnly` 與 `remoteAllowed`)會依據實際上游位址判定。若無法分類該位址,則由設定檔的 `unknownEvidence.capability` 決定候選是否合格。 ++無法解析的無效供應商設定一律以 `route-unavailable` 排除,即使原則允許未知能力也是如此。 ++ + 選取一個已儲存的設定檔,使用 **Dry-run evaluation** 加入請求證據,例如 context-window 大小、工具使用、圖片輸入或結構化輸出。試跑會評估資格與評分,但永遠不會送出上游模型請求。 + + 未儲存的編輯不會被試跑使用。請先儲存設定檔,讓顯示的 revision 與評估參照同一份設定。 +diff --git a/src/routing/capability.ts b/src/routing/capability.ts +index 8495951a0..7f26e8bbd 100644 +--- a/src/routing/capability.ts ++++ b/src/routing/capability.ts +@@ -10,7 +10,7 @@ + * how that affects eligibility. + */ + +-import { modelInList, type OcxConfig } from "../types"; ++import { modelInList, type OcxConfig, type OcxProviderConfig } from "../types"; + import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; + import { serviceTierSupportForModel } from "../providers/service-tier"; + import { PROVIDER_REGISTRY } from "../providers/registry"; +@@ -149,14 +149,20 @@ function localRemoteEvidence(baseUrl: string | undefined): Pick entry.id === providerName); ++ const provider = resolvedProvider ?? config.providers[providerName]; ++ const registryEntry = resolvedProvider === undefined ++ ? PROVIDER_REGISTRY.find(entry => entry.id === providerName) ++ : undefined; + const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId); + const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/"); + +@@ -224,6 +230,7 @@ export function candidateCapabilityEvidence( + ? [] + : modelRecordValue(provider?.modelReasoningEfforts, modelId) + ?? modelRecordValue(registryEntry?.modelReasoningEfforts, modelId) ++ ?? provider?.reasoningEfforts + ?? (isNative ? nativeReasoningEfforts(modelId) : undefined); + + const tierSupport = provider +diff --git a/src/routing/compatibility/assemble.ts b/src/routing/compatibility/assemble.ts +index 1d543690a..d7cebc94b 100644 +--- a/src/routing/compatibility/assemble.ts ++++ b/src/routing/compatibility/assemble.ts +@@ -52,11 +52,26 @@ export function assemblePolicyCandidateEvidence( + return profile.candidates.map(candidate => { + const key = `${candidate.provider}/${candidate.model}`; + const compatibility = compatibilityByCandidate?.get(key); ++ const provider = config.providers[candidate.provider]; ++ let routed: OcxProviderConfig | undefined; ++ let routeResolutionFailed = false; ++ if (provider && provider.disabled !== true) { ++ try { ++ routed = options.routedProviderConfig(candidate.provider, provider); ++ } catch { ++ // This is known unavailability, not unknown capability evidence. Keep ++ // the failure separate so permissive unknown policies cannot select it. ++ routeResolutionFailed = true; ++ } ++ } + + return { + provider: candidate.provider, + model: candidate.model, +- capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model), ++ ...(routeResolutionFailed ? { routeResolutionFailed: true } : {}), ++ capability: routed ++ ? candidateCapabilityEvidence(config, candidate.provider, candidate.model, routed) ++ : undefined, + health: policyCandidateHealthEvidence(config, candidate, now), + quota: quotaEvidenceForCandidate({ + provider: candidate.provider, +diff --git a/src/routing/evaluator.ts b/src/routing/evaluator.ts +index a07b83306..7cf801bfe 100644 +--- a/src/routing/evaluator.ts ++++ b/src/routing/evaluator.ts +@@ -54,6 +54,8 @@ export interface PolicyCandidateEvidence { + accountRef?: string; + /** Codex pool account id (provider "openai"); used to derive account-scoped quota evidence. */ + codexAccountId?: string; ++ /** A failed effective-transport resolution excludes the candidate under every unknown policy. */ ++ routeResolutionFailed?: boolean; + capability?: RouteCapabilityEvidence; + health?: RouteHealthEvidence; + quota?: RouteQuotaEvidence; +@@ -278,6 +280,8 @@ export function evaluatePolicyProfile( + ...requestRequirementFor(requestEvidence, evidence.capability), + ]; + const exclusions: RouteExclusionReason[] = []; ++ const routeUnavailable = evidence.routeResolutionFailed === true; ++ if (routeUnavailable) exclusions.push({ code: "route-unavailable" }); + const bad = unsatisfiedOrUnknown(requirements); + for (const requirement of bad) { + if (requirement.outcome === "unsatisfied") { +@@ -310,7 +314,7 @@ export function evaluatePolicyProfile( + if (unknownCostBlocked) { + exclusions.push({ code: "cost-limit-unknown", detail: "maxEstimatedCostUsd" }); + } +- let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked; ++ let eligible = !routeUnavailable && !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked; + + // Trace/dry-run copy only: report the profile cap that was applied and the + // operator-visible outcome. Do not feed this copy into costScore() — that +diff --git a/tests/routing/routing-capability-model-matching.test.ts b/tests/routing/routing-capability-model-matching.test.ts +index bb956c2d8..509eeec2e 100644 +--- a/tests/routing/routing-capability-model-matching.test.ts ++++ b/tests/routing/routing-capability-model-matching.test.ts +@@ -1,10 +1,19 @@ +-import { describe, expect, test } from "bun:test"; ++import { afterEach, beforeEach, describe, expect, test } from "bun:test"; ++import { mkdtempSync } from "node:fs"; ++import { tmpdir } from "node:os"; ++import { join } from "node:path"; ++import { validateConfigCandidate } from "../../src/config"; ++import { NoEligiblePolicyCandidateError, routeModel, routedProviderConfig } from "../../src/router"; + import { candidateCapabilityEvidence } from "../../src/routing/capability"; ++import { assemblePolicyCandidateEvidence } from "../../src/routing/compatibility/assemble"; + import { evaluatePolicyProfile } from "../../src/routing/evaluator"; ++import { closeRequestHistoryIndex } from "../../src/routing/history/indexer"; ++import { getRoutingProfile } from "../../src/routing/profile"; + import { PROVIDER_REGISTRY } from "../../src/providers/registry"; + import { modelRecordValue } from "../../src/reasoning-effort"; + import { isModelTextOnly } from "../../src/vision"; +-import type { OcxConfig, OcxProviderConfig } from "../../src/types"; ++import type { OcxConfig, OcxProviderConfig, OcxRoutingProfileConfig } from "../../src/types"; ++import { removeTreeWithRetry } from "../helpers/remove-tree"; + + /** + * `candidateCapabilityEvidence` describes what the resolver will do with a candidate, +@@ -35,6 +44,224 @@ function configFor(provider: OcxProviderConfig): OcxConfig { + return { providers: { custom: provider } } as unknown as OcxConfig; + } + ++describe("policy capability evidence uses the effective provider", () => { ++ let testDir: string; ++ let previousHome: string | undefined; ++ ++ beforeEach(() => { ++ previousHome = process.env.OPENCODEX_HOME; ++ testDir = mkdtempSync(join(tmpdir(), "ocx-effective-capability-")); ++ process.env.OPENCODEX_HOME = testDir; ++ }); ++ ++ afterEach(() => { ++ closeRequestHistoryIndex(); ++ if (previousHome === undefined) delete process.env.OPENCODEX_HOME; ++ else process.env.OPENCODEX_HOME = previousHome; ++ removeTreeWithRetry(testDir); ++ }); ++ ++ function policyConfig( ++ name: string, ++ provider: OcxProviderConfig, ++ model: string, ++ require: OcxRoutingProfileConfig["require"], ++ ): OcxConfig { ++ const result = validateConfigCandidate({ ++ port: 10100, ++ defaultProvider: name, ++ providers: { [name]: provider }, ++ routingProfiles: { guarded: { candidates: [{ provider: name, model }], require } }, ++ }); ++ if (!result.ok) throw new Error(result.error); ++ return result.config; ++ } ++ ++ const localOnly = { localOnly: true, remoteAllowed: false }; ++ const loopback = "http://127.0.0.1:11434/v1"; ++ ++ test("a loopback URL discarded by registry routing cannot satisfy a local-only policy", () => { ++ const config = policyConfig("deepseek", { ++ adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, ++ }, "deepseek-v4-flash", localOnly); ++ const before = structuredClone(config); ++ ++ expect(routeModel(config, "deepseek/deepseek-v4-flash").provider.baseUrl) ++ .toBe("https://api.deepseek.com"); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ expect(config).toEqual(before); ++ }); ++ ++ test.each(["custom-local", "ollama"])("a genuine local %s endpoint remains eligible", name => { ++ const config = policyConfig(name, { ++ adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, ++ }, "local-model", localOnly); ++ const before = structuredClone(config); ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.providerName).toBe(name); ++ expect(route.provider.baseUrl).toBe(loopback); ++ expect(route.routeDecision?.requirements).toEqual([ ++ { id: "local-only", expected: true, actual: true, outcome: "satisfied" }, ++ { id: "remote-allowed", expected: false, actual: false, outcome: "satisfied" }, ++ ]); ++ expect(config).toEqual(before); ++ }); ++ ++ test("an explicitly public endpoint remains ineligible for a local-only policy", () => { ++ const config = policyConfig("deepseek", { ++ adapter: "openai-chat", baseUrl: "https://api.deepseek.com", ++ }, "deepseek-v4-flash", localOnly); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ }); ++ ++ test("a local candidate is selected after excluding a registry-pinned remote candidate", () => { ++ const config = policyConfig("deepseek", { ++ adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, ++ }, "deepseek-v4-flash", localOnly); ++ config.providers.local = { adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true }; ++ config.routingProfiles!.guarded!.candidates.push({ provider: "local", model: "local-model" }); ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.providerName).toBe("local"); ++ expect(route.provider.baseUrl).toBe(loopback); ++ expect(route.routeDecision?.candidates.map(candidate => candidate.eligible)).toEqual([false, true]); ++ }); ++ ++ test("registry no-vision defaults participate before policy image requirements", () => { ++ const config = policyConfig("deepseek", { ++ adapter: "openai-chat", baseUrl: "https://api.deepseek.com", ++ modelInputModalities: { "deepseek-v4-flash": ["text", "image"] }, ++ }, "deepseek-v4-flash", { imageInput: true }); ++ const routed = routeModel(config, "deepseek/deepseek-v4-flash"); ++ expect(isModelTextOnly(routed.provider, routed.modelId)).toBe(true); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ }); ++ ++ test("the effective model context ceiling gates a policy requirement", () => { ++ const config = policyConfig("openai-apikey", { ++ adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", ++ modelContextWindows: { "gpt-6-astra": 2_000_000 }, ++ }, "gpt-6-astra", { minContextWindow: 1_500_000 }); ++ const routed = routeModel(config, "openai-apikey/gpt-6-astra"); ++ expect(routed.provider.modelContextWindows?.["gpt-6-astra"]).toBe(1_050_000); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ }); ++ ++ test("canonical forward auth filled by routing satisfies the encrypted-task requirement", () => { ++ const config = policyConfig("openai", { ++ adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", ++ }, "gpt-5.5", { encryptedCodexTasks: true }); ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.provider.authMode).toBe("forward"); ++ expect(route.routeDecision?.candidates[0]?.capability?.encryptedCodexTasks).toBe(true); ++ expect(config.providers.openai!.authMode).toBeUndefined(); ++ }); ++ ++ test("the effective provider-wide reasoning ladder participates in policy selection", () => { ++ const config = policyConfig("xiaomi-mimo", { ++ adapter: "openai-chat", baseUrl: "https://api.xiaomimimo.com/v1", ++ }, "mimo-v2.5", { reasoningEffort: "high" }); ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.provider.reasoningEfforts).toEqual(["low", "medium", "high"]); ++ expect(route.routeDecision?.candidates[0]?.capability?.reasoningEfforts) ++ .toEqual(["low", "medium", "high"]); ++ expect(config.providers["xiaomi-mimo"]!.reasoningEfforts).toBeUndefined(); ++ }); ++ ++ test("a same-named custom transport does not inherit an unrelated registry model map", () => { ++ const config = policyConfig("meta-model", { ++ adapter: "openai-responses", baseUrl: "https://custom.example/v1", ++ }, "muse-spark-1.3", { reasoningEffort: "high" }); ++ const routed = routeModel(config, "meta-model/muse-spark-1.3"); ++ expect(routed.provider.baseUrl).toBe("https://custom.example/v1"); ++ expect(routed.provider.modelReasoningEfforts).toBeUndefined(); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ }); ++ ++ test("an invalid unselected transport cannot prevent a healthy sibling from routing", () => { ++ const config = policyConfig("local", { ++ adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, ++ }, "local-model", {}); ++ config.providers.ollama = { adapter: "openai-chat", baseUrl: " " }; ++ config.routingProfiles!.guarded!.candidates.push({ provider: "ollama", model: "local-model" }); ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.providerName).toBe("local"); ++ expect(route.provider.baseUrl).toBe(loopback); ++ expect(route.routeDecision?.candidates[1]?.capability).toBeUndefined(); ++ }); ++ ++ test("an unresolved transport contributes no positive capability evidence", () => { ++ const config = policyConfig("ollama", { ++ adapter: "openai-chat", baseUrl: loopback, ++ modelInputModalities: { "local-model": ["text", "image"] }, ++ }, "local-model", { imageInput: true }); ++ config.providers.ollama!.baseUrl = " "; ++ ++ const evidence = assemblePolicyCandidateEvidence(config, getRoutingProfile(config, "guarded")!, Date.now(), { ++ routedProviderConfig, ++ }); ++ expect(evidence[0]?.capability).toBeUndefined(); ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ }); ++ ++ test("missing and disabled providers are not resolved for capability evidence", () => { ++ const config = policyConfig("local", { ++ adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, ++ }, "local-model", { tools: true }); ++ config.providers.disabled = { ...config.providers.local!, disabled: true }; ++ config.routingProfiles!.guarded!.candidates.push( ++ { provider: "missing", model: "model" }, ++ { provider: "disabled", model: "model" }, ++ ); ++ const resolved: string[] = []; ++ const evidence = assemblePolicyCandidateEvidence(config, getRoutingProfile(config, "guarded")!, Date.now(), { ++ routedProviderConfig: (name, provider) => { ++ resolved.push(name); ++ return routedProviderConfig(name, provider); ++ }, ++ }); ++ ++ expect(resolved).toEqual(["local"]); ++ expect(evidence[0]?.capability?.tools).toBe(true); ++ expect(evidence[1]?.capability).toBeUndefined(); ++ expect(evidence[2]?.capability).toBeUndefined(); ++ }); ++ ++ test.each(["allow", "penalize", "exclude"] as const)( ++ "an unresolved first candidate is excluded when unknown capabilities are %s", ++ capability => { ++ const config = policyConfig("ollama", { ++ adapter: "openai-chat", baseUrl: loopback, ++ }, "local-model", {}); ++ config.providers.ollama!.baseUrl = " "; ++ config.providers.local = { adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true }; ++ const profile = config.routingProfiles!.guarded!; ++ profile.candidates.push({ provider: "local", model: "local-model" }); ++ profile.unknownEvidence = { ...profile.unknownEvidence, capability }; ++ ++ const route = routeModel(config, "policy/guarded"); ++ expect(route.providerName).toBe("local"); ++ expect(route.routeDecision?.candidates.map(candidate => candidate.eligible)).toEqual([false, true]); ++ expect(route.routeDecision?.candidates[0]?.exclusions).toContainEqual({ code: "route-unavailable" }); ++ expect(JSON.stringify(route.routeDecision)).not.toContain("Invalid baseUrl"); ++ }, ++ ); ++ ++ test("all unresolved candidates produce a policy exclusion while explicit routing keeps validation", () => { ++ const config = policyConfig("ollama", { ++ adapter: "openai-chat", baseUrl: loopback, ++ }, "local-model", {}); ++ config.providers.ollama!.baseUrl = " "; ++ ++ expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); ++ expect(() => routeModel(config, "ollama/local-model")).toThrow('Invalid baseUrl for provider "ollama"'); ++ }); ++}); ++ + describe("candidateCapabilityEvidence model matching", () => { + test("a family entry covers its tagged siblings, as the resolver does", () => { + const provider = providerWithFamilyEntries(); + +```` + +## Required correction on top of the original head + +In `src/routing/compatibility/assemble.ts`, change exactly: + +```diff +- let routeResolutionFailed = false; ++ let routeResolutionFailed = !provider || provider.disabled === true; +``` + +Keep the active-provider `if` and catch intact. Missing/disabled providers must never invoke the resolver. Exception messages must not be copied into evidence or traces. + +Append this test inside the original effective-provider describe block in `tests/routing/routing-capability-model-matching.test.ts`, using its `policyConfig`, `loopback` and cleanup fixtures: + +```ts + for (const unavailable of ["missing", "disabled"] as const) { + test.each(["allow", "penalize", "exclude"] as const)( + `${unavailable} first candidate is excluded under %s unknown policy`, + capability => { + const config = policyConfig("local", { + adapter: "openai-chat", baseUrl: loopback, allowPrivateNetwork: true, + }, "local-model", {}); + if (unavailable === "disabled") { + config.providers.disabled = { ...config.providers.local!, disabled: true }; + } + const profile = config.routingProfiles!.guarded!; + profile.candidates.unshift({ provider: unavailable, model: "local-model" }); + profile.unknownEvidence = { ...profile.unknownEvidence, capability }; + const resolved: string[] = []; + const evidence = assemblePolicyCandidateEvidence( + config, getRoutingProfile(config, "guarded")!, Date.now(), { + routedProviderConfig: (name, provider) => { + resolved.push(name); + return routedProviderConfig(name, provider); + }, + }, + ); + expect(resolved).toEqual(["local"]); + expect(evidence[0]?.routeResolutionFailed).toBe(true); + expect(evidence[0]?.capability).toBeUndefined(); + const evaluation = evaluatePolicyProfile(config, "guarded", {}, evidence); + expect(evaluation.selectedIndex).toBe(1); + expect(evaluation.candidates[0]?.eligible).toBe(false); + expect(evaluation.candidates[0]?.exclusions).toContainEqual({ code: "route-unavailable" }); + expect(routeModel(config, "policy/guarded").providerName).toBe("local"); + profile.candidates.pop(); + expect(() => routeModel(config, "policy/guarded")).toThrow(NoEligiblePolicyCandidateError); + }, + ); + } +``` + +Append inside the existing describe in `tests/routing/routing-profile.test.ts`. Imports and `baseConfig`/ManagementRequest fixtures already exist: + +```ts + for (const unavailable of ["missing", "disabled"] as const) { + test.each(["allow", "penalize", "exclude"] as const)( + `API dry-run excludes ${unavailable} provider under %s unknown policy`, + async capability => { + const config = baseConfig({ + providers: { + local: { adapter: "openai-chat", baseUrl: "http://127.0.0.1:11434/v1", allowPrivateNetwork: true }, + }, + defaultProvider: "local", + routingProfiles: { + guarded: { + candidates: [ + { provider: unavailable, model: "local-model" }, + { provider: "local", model: "local-model" }, + ], + require: {}, + unknownEvidence: { capability }, + }, + }, + }); + if (unavailable === "disabled") { + config.providers.disabled = { ...config.providers.local!, disabled: true }; + } + for (const withSibling of [true, false]) { + if (!withSibling) config.routingProfiles!.guarded!.candidates.pop(); + const req = new ManagementRequest("http://localhost/api/routing-profiles/dry-run", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ profile: "guarded", evidence: {} }), + }); + const response = await handleManagementAPI(req, new URL(req.url), config, { + refreshCodexCatalog: async () => {}, + }); + expect(response!.status).toBe(200); + const body = await response!.json() as { + selectedIndex: number | null; + candidates: Array<{ eligible: boolean; exclusions: Array<{ code: string }> }>; + }; + expect(body.selectedIndex).toBe(withSibling ? 1 : null); + expect(body.candidates[0]?.eligible).toBe(false); + expect(body.candidates[0]?.exclusions).toContainEqual({ code: "route-unavailable" }); + } + }, + ); + } +``` + +The empty `require` is deliberate: it activates the bug even when capability unknown handling has no unsatisfied requirement to mask it. Existing synthetic `candidates` fixtures retain their meaning. No upstream request is needed for these scenarios. + +After each original guide paragraph, add the corresponding exact sentence: + +| Path | Added text | +|---|---| +| `docs-site/src/content/docs/guides/routing-profile-editor.md` | Missing or disabled providers are also excluded with `route-unavailable` before scoring. | +| `docs-site/src/content/docs/fr/guides/routing-profile-editor.md` | Les fournisseurs absents ou désactivés sont également exclus avec `route-unavailable` avant le calcul des scores. | +| `docs-site/src/content/docs/tr/guides/routing-profile-editor.md` | Eksik veya devre dışı sağlayıcılar da puanlama öncesinde `route-unavailable` ile dışlanır. | +| `docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md` | 缺少或停用的供應商也會在評分前以 `route-unavailable` 排除。 | + +SOT edit: + +```diff +-| `src/router.ts` | Provider/model selection before adapter dispatch. | ++| `src/router.ts` | Provider/model selection before adapter dispatch. Policy execution and ordinary management dry-run share effective-provider capability evidence; unresolved, missing, and disabled providers are excluded before scoring. | +``` + +## Activation and independent acceptance + +| Trigger | Observable proof, required remotely | +|---|---| +| Canonical DeepSeek with configured loopback URL overridden by registry | Direct resolution shows canonical remote destination; local-only policy rejects it. Genuine custom-local and Ollama loopback remain eligible; config snapshots are unchanged. | +| Registry no-vision defaults, capped native API context, filled forward auth and provider-wide reasoning ladder | Policy sees exactly the effective dispatch values; no-reasoning empty ladder remains a known negative. Same-name custom transport does not gain unrelated registry metadata. | +| Resolver throws for first candidate under allow/penalize/exclude | First candidate has `route-unavailable`, no positive capability and `eligible=false`; healthy sibling wins. All invalid candidates produce NoEligiblePolicyCandidateError; explicit routing preserves its validation error. | +| Missing/disabled first candidate; empty requirements under each unknown policy | Resolver spy only sees active sibling, unavailable evidence true; evaluator and runtime choose sibling, all-unavailable policy fails. | +| Ordinary management dry-run without supplied candidate evidence | Missing/disabled candidates remain excluded; selectedIndex is sibling index or null with no sibling. This proves the public review fix at the actual API owner. | +| Core-only runtime and compatibility provider slot | Core/Lab boundary tests pass; no new import chain, timer or asynchronous activation is introduced. | +| Existing three-argument capability helper callers and synthetic dry-run fixtures | Existing model-family/catalog/profile tests remain green; optional argument is backward compatible. | + +This layer is independently acceptable only with the original carry plus the review correction and all regression cases together. The predecessor contributes the integration baseline, not deferred tests. Main must independently review the public policy-boundary change; do not rely on the original CodeRabbit status alone. + +## Remote verifiers and static workflow coverage + +Main verified `the isolated remote verification host`, its existing `REMOTE_SOURCE_CHECKOUT` checkout, and Bun `1.3.14`. Implementation C uses a separate isolated remote clone at the exact carried SHA; do not alter or run checks in that existing checkout. Record the clone path and resolved SHA with every receipt. No local project command execution is permitted at any point. + +Implementation C runs focused activation regressions and typecheck remotely. The carrying PR stays draft until full current-head GitHub CI is green; focused success alone does not authorize readiness or landing. The final landing cycle requires every full gate, including the separately verified documentation build and required reviews. Deeper implementation review belongs to the next cycle, after candidate-plan revalidation. + +These commands are plans for an isolated remote checkout of the exact carried commit, **not commands to execute on the local Mac**. Record host, exact SHA, command, exit status and full output artifact. Use fixture homes and no real provider traffic. + +```sh +# REMOTE ONLY: exact-head focused behavior and core/Lab boundaries +bun test tests/routing/routing-capability-model-matching.test.ts tests/routing/routing-capability-catalog.test.ts tests/routing/policy-execution.test.ts tests/routing/routing-profile.test.ts tests/routing/routing-compatibility.test.ts tests/routing/compatibility-provider-equivalence.test.ts tests/lab/core-lab-boundary.test.ts +# REMOTE ONLY: complete source checks; hosted CI may supply this evidence instead +bun run typecheck +bun run test +bun run privacy:scan +# REMOTE ONLY: docs build in an isolated verification checkout, without publication +cd docs-site +bun install --frozen-lockfile +bun run build +``` + +For red/green proof, use the remote original carry head without the one-line correction but with the new regression cases: new missing/disabled cases must fail. Add correction on the remote verification candidate and show the same cases green. Do not run this experiment by rewriting this shared worktree. + +Static inspection at `81871b3fa` confirms: + +- `.github/workflows/ci.yml:7` uses `pull_request: {}` without a base filter, so a child PR targeting a parent branch is covered. Push alone to `codex/*` does not trigger it (`:26–27`); an opened PR or authorized workflow_dispatch is necessary. +- `ci.yml:182–185` includes `src/**` and `tests/**`; this layer must produce `changes.ci=true`. Original workflows awaiting approval provide no test evidence. +- `ci.yml:254–316` runs four Linux shards with `scripts/ci/run-bun-test-batches.sh`. Inspect logs for actual file execution and all shards, including split API/storage jobs, rather than only aggregate status. +- `ci.yml:422–431` covers typecheck and privacy. macOS and Windows suites are configured at `:451`, `:532`, `:625`, and `:754`; inspect the actual selected lane and successful test jobs, not skipped jobs. +- `ci.yml:906` aggregate permits intentionally skipped jobs. An aggregate green with skipped required product jobs is insufficient for this source-changing layer. +- `.github/workflows/deploy-docs.yml:3–9` triggers on main/docs changes or manual dispatch, and contains deployment. It is **not** a PR docs-build verifier. Do not dispatch publication to obtain validation. Use the remote build-only commands above. No CI workflow edits are needed in this layer. + +The roadmap cycle changes documentation only; expensive tests may correctly skip there. That success cannot be reused for the later implementation head. Each restack or review fix requires new exact-head evidence. + +## Main-owned stack delivery and closure + +Carry original commits in order, then commit the targeted review correction and regressions. Publish only own branches with the user's authorized `--no-verify` push policy. Do not rewrite the contributor branch. Populate every repository PR-template section, stack parent link and source attribution. No local gate bypass can substitute for remote product CI. + +Merge bottom-up once each layer's exact current head meets review/CI gates. When a lower PR is squash-merged, restack/retarget descendants before deleting lower branches; verify the new parent ancestry and each child diff. Preserve author trailers in the final squash body. + +After main verifies the carrying merge SHA is an ancestor of freshly fetched `origin/dev`, immediately close original #3671 as superseded if it did not auto-close, linking the carrying PR/merge. If original #3671 itself is merged, verify its merged state. Do not close it on branch push, PR creation, CI success alone, or merge only into a stack parent. No additional issue is identified as fully resolved by this unit. + +## Planning proof + +Only this document was written by this worker. The embedded original patch was read directly from the pinned original objects. Source/caller/workflow checks are static; no local tests, typecheck, build, Git mutation, GitHub mutation, goal or FSM transition was performed. The main-owned later P must revalidate all candidate hunks and review state. diff --git a/devlog/_fin/260906_a_runtime_stack/051_capability_refresh.md b/devlog/_fin/260906_a_runtime_stack/051_capability_refresh.md new file mode 100644 index 0000000000..190122e007 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/051_capability_refresh.md @@ -0,0 +1,5 @@ +# Effective-capability layer P refresh + +Consume050 on prepared affinity6b00fa8d6. Original #3671 remains7b1beb9c5, with two Hako commits. Carry both; fix the remaining public review by initializing routeResolutionFailed to !provider || provider.disabled===true before resolving enabled candidates. Preserve synthetic caller-supplied dry-run evidence semantics and the core/Lab slot boundary. Explicitly test missing and disabled candidates under allow/penalize/exclude, with healthy sibling and with none, at both runtime/evaluator and ordinary management dry-run. + +Regression worker owns only tests/routing/routing-capability-model-matching.test.ts and tests/routing/routing-profile.test.ts. Main owns production, four existing guide locales and runtime SOT. No local tests/typecheck/build; pinned remote Bun1.4 --isolate focused checks, actual API assertions, privacy/docs/fullCI before landing. Windows verifier repair is registered as an additional required cycle and does not weaken any earlier gate. diff --git a/devlog/_fin/260906_a_runtime_stack/070_windows_fixtures.md b/devlog/_fin/260906_a_runtime_stack/070_windows_fixtures.md new file mode 100644 index 0000000000..daab52ed55 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/070_windows_fixtures.md @@ -0,0 +1,123 @@ +# 070 — Deterministic Windows shutdown-spill fixtures + +Status: P amendment; documentation only. Future implementation is a separate C2 test-harness cycle after 050, before 080. C confirmed no ownership collision. Main owns the FSM, implementation, remote execution and insertion of this foundation beneath the runtime stack. + +## Evidence and boundary + +[Windows job 101339545421](https://github.com/lidge-jun/opencodex/actions/runs/33978547130/job/101339545421), head `4b34cbb8d3f308cd2b01e8d87784c65afb50a40f`, Bun 1.4.0: 3048 pass, 39 skip, 2 fail, 1 unhandled error. The two failures are in `tests/responses/responses-state.test.ts`: + +- Stable-tail (1297): the 500 ms drain timer selected synchronous fallback; its unmocked ACL runner failed with EICACLS. The actual async-delay cause is unmeasured. Global ACL call 7 is also an unreliable publication marker: snapshot and directory hardening share this runner. +- Reserved budget (1438): only the ACL clock is synthetic. `spill-store.ts:232` charged real serialization/filesystem elapsed time and exhausted the deadline before temp-file hardening. The expensive operation is not identified. + +Local evidence inputs: `.tmp/a-runtime-stack/ci-triage/report.md`, `sse-windows5.log:4038–4217`, and `prior-101262480176.log:3894–3897` in that same scratch directory. The earlier job passed the two cases; its overall run was not green. Unchanged source on the sampled dev is not an independently reproduced current-dev failure. Do not describe this as an SSE regression, a proven harmless transient, or a green Windows gate. + +Future edit set: **only `tests/responses/responses-state.test.ts`**. No production, workflow, manifest, shared fixture or budget changes. Reuse `forceWindowsAclLane`, `isSpillAclTarget`, `ICACLS_OK`, existing clock/runner setters, and spill event recording. Keep existing deadline, fallback, exhaustion and watchdog tests. No sleeps for synchronization, timeout increases, skips or relaxed assertions. + +Read-only owners inspected: + +| Owner | Contract retained | +|---|---| +| `src/responses/state.ts:595` | Drain races the observed tail against a real timer; `Date.now` alone cannot freeze that timer. | +| `src/responses/state.ts:771` and `:813` | Separate fallback reserve, remaining-budget forwarding, and repeated observation until the publication tail is stable. | +| `src/responses/spill-store.ts:100`, `:153`, `:225` | Existing I/O events and injectable spill clock; each harden gets min(per-call cap, remaining whole-write budget). | +| `src/lib/windows-secret-acl.ts:360`, `:410`, `:589` | Async runner timer; injected ACL clock; grant/inheritance/remove calls consume one harden deadline. | +| `tests/responses/ws-upstream.test.ts:725` | Existing Bun `jest.useFakeTimers` / `advanceTimersByTime` / `useRealTimers` convention. | + +## Hunk 1 — Stable-tail ordering, not elapsed disk time + +At the test import, add `jest`. Retain 1000/500 budgets. Use fake timers **only within this test**, with `Date.now` fixed to a captured real epoch and ACL/spill clocks fixed consistently. Capture native `setImmediate` before enabling fake timers for an event-loop checkpoint; this drains runnable promise work without a sleep or timer advance. No new shared helper. + +Replace the global `aclCalls === 1/7` runner with gates on the first two distinct spill temp paths at `/grant:r`: + +```ts +const gatedTemps = new Set(); +setAsyncIcaclsRunnerForTests(async args => { + const target = args[0] ?? ""; + if (!isSpillAclTarget(args) || !target.endsWith(".tmp") || args[1] !== "/grant:r") { + return ICACLS_OK; // includes snapshot, directory and later ACL steps + } + if (!gatedTemps.has(target)) { + gatedTemps.add(target); + if (gatedTemps.size === 1) { firstEntered(); await firstGate; } + if (gatedTemps.size === 2) { secondEntered(); await secondGate; } + } + return ICACLS_OK; +}); +let syncSpillCalls = 0; +setIcaclsRunnerForTests(args => { + if (isSpillAclTarget(args)) syncSpillCalls++; + return ICACLS_OK; +}); +``` + +Both principal resolvers remain synthetic through `forceWindowsAclLane`. Both ACL runners cover **every** target; filtering controls gating/counting, never whether a real subprocess is used. A fallback must fail the ordering oracle (`syncSpillCalls === 0`), rather than being hidden by the successful mock. + +Replace the current orchestration and 25 ms sleep with this exact ordering: + +1. Enter `try/finally` before the first enqueue/await. Enable fake timers and fixed epoch clock; install both clock setters. Enqueue first response and await its temp gate. +2. Start `flushResponseState`, immediately attach both settlement handlers, recording `flushed` and any error in a resolved outcome object. This avoids an unhandled rejection if an earlier assertion fails. +3. Enqueue second response **after** starting flush, then release first. Await second temp gate. Await a native `setImmediate` checkpoint, advance fake timers by 25 ms, then another native checkpoint. The drain timer stays below 500 ms; no real elapsed filesystem time can fire it. +4. Assert flush is still pending, exactly two distinct temp paths were gated, and no synchronous spill ACL calls occurred. Record `setSpillIoForTest({ record })` events and assert exactly one `stub-swap` so the first publication actually installed while the second is gated. +5. Release second, await the handled flush outcome and rethrow any captured error. Retain `{ residentCount: 0, spillStubCount: 2 }`; add pending `{ count: 0, bytes: 0 }`, two `stub-swap` events, and zero synchronous spill calls. Both stored response IDs must still expand to their distinct payloads. +6. `finally`: release **both** gates, await any started flush outcome and `flushPendingResponseSpillsForTests()` while mocks/clocks remain installed, then restore the Date spy and real timers in a nested `finally`. Existing `afterEach` restores setters. Never restore mocks while a gated async operation still owns work. + +Use a discriminated outcome (`{ ok: true } | { ok: false; error: unknown }`) rather than an undefined-error sentinel. Keep cleanup valid when either startup await/assertion fails. Fake-timer compatibility and the native checkpoint are remote Windows acceptance items, not assumed proof. Do not solve a failed fixture by globally suppressing timers or adding a production seam. + +## Hunk 2 — One logical fallback budget, actual drain timer + +At 1438, preserve `totalMs = 500`, `fallbackReserveMs = 300` and the pending async spill gate. Add the missing spill clock; scope a Date spy to the flush so outer fallback accounting and nested ACL accounting advance together. Keep native timers in this test: the unchanged 200 ms drain timer must expire while the async gate remains held. + +```diff + let aclClock = 0; + setNowForTests(() => aclClock); ++setResponseSpillNowForTests(() => aclClock); +``` + +Record `{ target, timeoutMs, spentBefore }` for **spill** synchronous ACL calls. Snapshot ACL calls return `ICACLS_OK` without charging the spill clock. For each spill call, record before incrementing `aclClock += 20`; preserve successful command results. + +```ts +const epoch = Date.now(); +const nowSpy = spyOn(Date, "now").mockImplementation(() => epoch + aclClock); +// Start only after the async spill gate announces entry. +try { + await flushResponseState(); // native 200 ms drain timer selects sync fallback +} finally { + release(); + try { await flushPendingResponseSpillsForTests(); } + finally { nowSpy.mockRestore(); } +} +``` + +An enclosing `try/finally` must also cover enqueue and `await started`, releasing the gate on early failure. Preserve all three original assertions: at least six spill commands, maximum deadline <= 150, and `200 + aclClock <= 500`. Add: + +- Every timeout is positive and <= `300 - spentBefore` (independent literal budget oracle). +- Within each target's grant/inheritance/remove sequence, each next timeout is exactly 20 ms smaller; do **not** assert global monotonicity across targets because a new harden has its own per-call cap. +- The async gate has not been released when synchronous spill work begins; fallback actually ran, pending count/bytes become zero, one spill stub remains, and replay contains the original payload. + +The Date spy prevents unmeasured real disk latency from consuming this logical-budget fixture. It does not disable the native drain timer. Real-time termination coverage remains in the unchanged cap-expiry test (1339) and `shutdown fallback budget exhaustion is contained by a child watchdog` (1613), using `tests/helpers/responses-state-shutdown-budget-child.ts`. Do not claim this test measures OS elapsed latency. + +## Windows red, control and proof + +Main executes these later on real Windows with the repository-pinned Bun, in isolated remote checkouts. Nothing below authorizes local tests in this documentation task. + +1. Preserve the failed root-head job/logs above. Run the original two tests on the pinned pre-fix baseline; record actual results, including a pass. Do not require random failure or accept retries as a fix. +2. In remote scratch only, force the old stable-tail drain to expire by holding the second gate until a recorded fallback entry. Use a counted synchronous sentinel that reports EICACLS instead of invoking native ACL tools. Confirm rejection and the fallback call; never infer the missing-mock path from elapsed time alone. This is a controlled mechanism probe, not proof that the same delay happened in CI. +3. In remote scratch only, use the existing spill `record("write")` event to advance a separate wall clock by 301 ms once synchronous fallback has begun. On the original reserved-budget fixture, spill uses that clock and fails before temp hardening; with the proposed shared logical spill clock, the same wall-clock perturbation cannot consume the ACL budget. Record entry and clock values. Keep this probe separate from production and from the committed passing fixture. +4. Prove oracle sensitivity with isolated remote mutations: (a) stop drain after its first observed tail, expecting the revised stable-tail pending/zero-fallback oracle to fail; (b) reset the ACL deadline for each command, expecting per-target 20 ms decrease assertions to fail. Separately advance the **injected spill clock** beyond 300 at the write event and require ETIMEDOUT, proving deadline enforcement remains active. Restore every mutation before green verification; retain diff and failing assertion for each probe. +5. Run the unchanged named cap-expiry and child-watchdog controls, then the whole focused file on the new exact head: + +```sh +# Remote Windows only; these commands are a future verifier recipe. +bun test --isolate --timeout 60000 tests/responses/responses-state.test.ts +bun run typecheck +``` + +6. Dispatch the actual Windows full-suite workflow on that exact head, including `bun test --isolate --timeout 60000 tests --shard=5/6` and every other required shard. Inspect job execution, not aggregate success with skipped tests. Record head SHA, Bun version, commands, job URLs, counts and absence of unhandled errors. Run current-head Linux/macOS gates and required scans as well. + +Implementation D means an independently reviewed prepared foundation draft with exact-head focused Windows evidence and remote typecheck; it is **not landing**. Main inserts the verified foundation beneath the stack, refreshes descendants bottom-up with original attribution intact, obtains required current-head gates, then admin-merges in dependency order. Verify each landed SHA is an ancestor of freshly fetched dev before closing a superseded PR or fully resolved issue. Partial issues retain their residual scope. See `080_landing.md`. + +Documentation acceptance: this file names both failed fixtures, all clock/timer boundaries, complete runner/cleanup coverage, executable negative controls, one-file implementation scope and separate landing gates. No test execution or implementation success is claimed here. + +## Remote execution fallback amendment + +The existing direct Windows SSH endpoint is unavailable; the reachable auxiliary host is Linux without Windows interop. Use GitHub Actions for actual Windows proof. If the existing full-suite workflow cannot execute focused causal probes, a separate owner-only `codex/a-verify-windows` branch may hold a temporary verification workflow triggered only by pushes to that exact branch. This workflow is never included in a product PR or merged to dev. It uses `windows-latest`, read-only contents permission, pinned checkout with `persist-credentials: false`, the existing pinned-Bun setup, fixed repository test commands and the exact carried fixture commit. No secrets, untrusted command inputs, self-hosted runner access or release permissions. It may execute the narrowly specified scratch mutations with guaranteed source restoration and upload logs. Independent security audit of the concrete workflow is required before pushing it. Standard per-head full CI remains the final gate; the temporary verifier cannot mark those checks green. diff --git a/devlog/_fin/260906_a_runtime_stack/080_landing.md b/devlog/_fin/260906_a_runtime_stack/080_landing.md new file mode 100644 index 0000000000..760192edb8 --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/080_landing.md @@ -0,0 +1,23 @@ +# Verified stacked integration and closeout + +## Changes + +No production changes planned. MODIFY phase records with evidence; move this completed public unit from `devlog/_plan/260906_a_runtime_stack/` to `_fin/` only after all units have a terminal outcome. Private security evidence remains ignored. + +## Procedure + +1. Refresh each original and replacement PR head, base, unresolved reviews, required checks, and live dev. If source author advanced, compare unique new commits before disposition. +2. Require actual current-head full cross-platform tests and typecheck/privacy gates, not intake-only green or old author attestations. Record remote focused triggers as supplementary proof. Repair test failures by cause; never claim retry alone fixed a failure. +3. Verify each child contains its current parent's head and its base points at the parent. Merge the bottom with source author's account-linked Co-authored-by trailer preserved in squash body, or use merge commits to retain ancestry. Push owned branches with --no-verify; any necessary rewrite uses explicit --force-with-lease on owned refs only. +4. Retarget the next child to dev, rebase/merge as needed after a squash, verify its diff and fresh checks. Do not delete a parent branch before child retargeting. +5. Immediately fetch dev and prove `git merge-base --is-ancestor origin/dev`. Close the superseded source PR with the replacement PR and landing SHA. For original PRs directly merged, record merged state. +6. Read linked issue acceptance scope. Close only fully solved issues; #3661 MESSAGE recovery is partial and must retain the residual issue. Do not use automatic Closes for partial work. +7. Check final dev CI on its exact current SHA. Record all original/replacement PR URLs, contributor trailers, merge SHAs and issue dispositions. Complete goal only when every criterion has captured evidence and all FSM cycles closed. + +## Activation and failure cases + +Source-author update: compare the new head, carry any still-needed change and reverify. Concurrent dev movement: integrate it without losing another lane's changes. Squash changes parent identity: cascade child before any merge. Failed CI: inspect exact job/step logs, fix the failing scope, re-run on new head. Unresolved issue scope: leave open with specific residual, never close to improve counts. + +## Verifiers + +Run read-only `gh pr view`, `gh pr checks`, `gh run view` and `git merge-base --is-ancestor` at the real current heads. Local git ancestry and static diff checks are allowed. Runtime/typecheck/build verification occurs on GitHub Actions or isolated the isolated remote verification host checkouts only. Full CI pending means this landing cycle remains active. diff --git a/devlog/_fin/260906_a_runtime_stack/090_outcome.md b/devlog/_fin/260906_a_runtime_stack/090_outcome.md new file mode 100644 index 0000000000..0534bb8ecb --- /dev/null +++ b/devlog/_fin/260906_a_runtime_stack/090_outcome.md @@ -0,0 +1,29 @@ +# A runtime and routing outcome + +All five assigned originals are closed and their credited changes are on dev. The final stack entered dev through [#3716](https://github.com/lidge-jun/opencodex/pull/3716), merge [`a2f69c8aa`](https://github.com/lidge-jun/opencodex/commit/a2f69c8aa60976345740ae6f3d2301f89297328e). GitHub automatically recognized the folded review PRs as merged; their actual dev integration is recorded below. + +| Original | Reviewed carry | Dev integration | Full CI before dev integration | +| --- | --- | --- | --- | +| [#3672](https://github.com/lidge-jun/opencodex/pull/3672) | [#3683](https://github.com/lidge-jun/opencodex/pull/3683) | [#3683](https://github.com/lidge-jun/opencodex/pull/3683) · [`c6d8678f7`](https://github.com/lidge-jun/opencodex/commit/c6d8678f73ce6e1ae9df004ab032af09837b5b45) | [33981578769](https://github.com/lidge-jun/opencodex/actions/runs/33981578769) | +| [#3679](https://github.com/lidge-jun/opencodex/pull/3679) | [#3686](https://github.com/lidge-jun/opencodex/pull/3686) | [#3686](https://github.com/lidge-jun/opencodex/pull/3686) · [`a6d1065cf`](https://github.com/lidge-jun/opencodex/commit/a6d1065cfbadc7d8f9c02e17549908b42d2bfd7a) | [33981581047](https://github.com/lidge-jun/opencodex/actions/runs/33981581047) | +| [#3568](https://github.com/lidge-jun/opencodex/pull/3568) | [#3690](https://github.com/lidge-jun/opencodex/pull/3690) | [#3690](https://github.com/lidge-jun/opencodex/pull/3690) · [`6e15dad6a`](https://github.com/lidge-jun/opencodex/commit/6e15dad6a42682d5dbf3e61c51493385091e37a6) | [33981582675](https://github.com/lidge-jun/opencodex/actions/runs/33981582675) | +| [#3581](https://github.com/lidge-jun/opencodex/pull/3581) | [#3692](https://github.com/lidge-jun/opencodex/pull/3692) | [#3716](https://github.com/lidge-jun/opencodex/pull/3716) · [`a2f69c8aa`](https://github.com/lidge-jun/opencodex/commit/a2f69c8aa60976345740ae6f3d2301f89297328e) | [33991642514](https://github.com/lidge-jun/opencodex/actions/runs/33991642514) | +| [#3671](https://github.com/lidge-jun/opencodex/pull/3671) | [#3694](https://github.com/lidge-jun/opencodex/pull/3694) | [#3716](https://github.com/lidge-jun/opencodex/pull/3716) · [`a2f69c8aa`](https://github.com/lidge-jun/opencodex/commit/a2f69c8aa60976345740ae6f3d2301f89297328e) | [33991642514](https://github.com/lidge-jun/opencodex/actions/runs/33991642514) | + +Original author identities and account-linked Co-authored-by trailers were retained: Hako, Clive Rosfield, voiys and SB Yoon. The carried contributor commits remain ancestors of the final integration. Each source head was checked again before closure; the original WebSocket author rebase had an identical verified patch. + +## Additional verified repairs + +- #3696 stabilized Windows shutdown-spill fixtures with controlled clocks and complete ACL mocks; hosted Windows causal and negative controls passed before integration. +- #3708 kept Unix probe cleanup bounded and fail-closed while allowing the existing observation interval to confirm disappearance after transient EPERM. Replay fixtures now keep one caller credential snapshot across a forced second boundary. +- #3716 budgeted transition-probe startup from the two bounded Windows identity lookups, reported early exits, and joined children before cleanup. Quota fixtures now join their ordered observation/forget queue instead of guessing completion after five milliseconds. + +The final candidate passed all 24 actual cross-platform producers and the aggregate CI check in run33991642514. Remote regression controls covered delayed process startup, direct early exit, delayed quota delivery and deliberately suppressed delivery. Reverting the quota fixture reproduced the exact three historical failures; the repaired fixture passed all sixteen cases under the controlled delay. No local product tests, typechecks or builds ran. + +## Scope and verification record + +[#3661](https://github.com/lidge-jun/opencodex/issues/3661) remains open: this work covers the proven native MESSAGE recovery slice, not its remaining multipart/backend/caller cases. The five source PRs expose no additional closing-issue links. + +Historical failed CI jobs were preserved. One earlier Cursor echo/close timeout has no established cause; the same source subsequently passed all final cross-platform checks. It was not claimed to be fixed by the fixture changes. + +Integrated dev `a2f69c8aa60976345740ae6f3d2301f89297328e` passed [run33993960826](https://github.com/lidge-jun/opencodex/actions/runs/33993960826): all17 applicable producers and the aggregate succeeded. Windows suite and unsharded macOS control are dispatch-only and were correctly skipped on this push; both were included in the successful24-producer final candidate run above. diff --git a/devlog/_fin/260906_a_windows_fixture/000_plan.md b/devlog/_fin/260906_a_windows_fixture/000_plan.md new file mode 100644 index 0000000000..b858312f61 --- /dev/null +++ b/devlog/_fin/260906_a_windows_fixture/000_plan.md @@ -0,0 +1,9 @@ +# A Windows verifier foundation + +This is the independently reviewed foundation required to unblock A runtime-stack current-head verification. Failed Windows job101339545421 exposed non-hermetic shutdown spill fixtures; no production regression is inferred. The full diff-level specification is010_fixture_plan.md, copied from the audited A roadmap amendment and rechecked against currentdev. + +Loop: spec-satisfaction repair. Test-only fixture classC2; temporary CI verification workflow classC4 with explicit independent security audit. Main owns this branch, commits, --no-verify pushes, source preservation and owner-authorized --admin merges. No local suites/typecheck/build; remote pinnedBun1.4 only. Existing credentials permit repository GitHubActions, read-only-content hostedWindows verification and ownbranch writes; no release/service/account changes. No new credential inputs or command injection. Token/cost cap not specified;2h checkpoint reassesses evidence and progress. + +One workphase owns fixture correctness plus its verified testing foundation; no A feature implementation is included. Only tests/responses/responses-state.test.ts and numberedunitdocs belong in the product PR. A separate owner-only codex/a-verify-windows branch may add the concrete audited probe workflow, never merged todev. Feature/runtime/test blobs must match the product candidate; probe-only workflow excluded by explicit path equality proof. Final full CI on the exact product candidate remains mandatory. + +P refreshes failedfixtures and clock hooks. A audits both minimalfix and concrete CI workflow beforepush. B changes ordering/budget fixtures, pinsverificationartifacts and commits. C executes realWindows controls and fullrelevantgates, plus independent review. D records a preparedverified foundation; the next landing phase inserts/merges this foundation below A layers, refreshesheads and obtains allrequiredCI before eachadminmerge. No test skip, broader production budget, or unverified retry. Existing source and issue disposition criteria remainunchanged. diff --git a/devlog/_fin/260906_a_windows_fixture/010_fixture_plan.md b/devlog/_fin/260906_a_windows_fixture/010_fixture_plan.md new file mode 100644 index 0000000000..d5f6bb017f --- /dev/null +++ b/devlog/_fin/260906_a_windows_fixture/010_fixture_plan.md @@ -0,0 +1,123 @@ +# 010 — Deterministic Windows shutdown-spill fixtures + +Status: P amendment; documentation only. Future implementation is a separate C2 test-harness cycle after 050, before 080. C confirmed no ownership collision. Main owns the FSM, implementation, remote execution and insertion of this foundation beneath the runtime stack. + +## Evidence and boundary + +[Windows job 101339545421](https://github.com/lidge-jun/opencodex/actions/runs/33978547130/job/101339545421), head `4b34cbb8d3f308cd2b01e8d87784c65afb50a40f`, Bun 1.4.0: 3048 pass, 39 skip, 2 fail, 1 unhandled error. The two failures are in `tests/responses/responses-state.test.ts`: + +- Stable-tail (1297): the 500 ms drain timer selected synchronous fallback; its unmocked ACL runner failed with EICACLS. The actual async-delay cause is unmeasured. Global ACL call 7 is also an unreliable publication marker: snapshot and directory hardening share this runner. +- Reserved budget (1438): only the ACL clock is synthetic. `spill-store.ts:232` charged real serialization/filesystem elapsed time and exhausted the deadline before temp-file hardening. The expensive operation is not identified. + +Local evidence inputs: `.tmp/a-runtime-stack/ci-triage/report.md`, `sse-windows5.log:4038–4217`, and `prior-101262480176.log:3894–3897` in that same scratch directory. The earlier job passed the two cases; its overall run was not green. Unchanged source on the sampled dev is not an independently reproduced current-dev failure. Do not describe this as an SSE regression, a proven harmless transient, or a green Windows gate. + +Future edit set: **only `tests/responses/responses-state.test.ts`**. No production, workflow, manifest, shared fixture or budget changes. Reuse `forceWindowsAclLane`, `isSpillAclTarget`, `ICACLS_OK`, existing clock/runner setters, and spill event recording. Keep existing deadline, fallback, exhaustion and watchdog tests. No sleeps for synchronization, timeout increases, skips or relaxed assertions. + +Read-only owners inspected: + +| Owner | Contract retained | +|---|---| +| `src/responses/state.ts:595` | Drain races the observed tail against a real timer; `Date.now` alone cannot freeze that timer. | +| `src/responses/state.ts:771` and `:813` | Separate fallback reserve, remaining-budget forwarding, and repeated observation until the publication tail is stable. | +| `src/responses/spill-store.ts:100`, `:153`, `:225` | Existing I/O events and injectable spill clock; each harden gets min(per-call cap, remaining whole-write budget). | +| `src/lib/windows-secret-acl.ts:360`, `:410`, `:589` | Async runner timer; injected ACL clock; grant/inheritance/remove calls consume one harden deadline. | +| `tests/responses/ws-upstream.test.ts:725` | Existing Bun `jest.useFakeTimers` / `advanceTimersByTime` / `useRealTimers` convention. | + +## Hunk 1 — Stable-tail ordering, not elapsed disk time + +At the test import, add `jest`. Retain 1000/500 budgets. Use fake timers **only within this test**, with `Date.now` fixed to a captured real epoch and ACL/spill clocks fixed consistently. Capture native `setImmediate` before enabling fake timers for an event-loop checkpoint; this drains runnable promise work without a sleep or timer advance. No new shared helper. + +Replace the global `aclCalls === 1/7` runner with gates on the first two distinct spill temp paths at `/grant:r`: + +```ts +const gatedTemps = new Set(); +setAsyncIcaclsRunnerForTests(async args => { + const target = args[0] ?? ""; + if (!isSpillAclTarget(args) || !target.endsWith(".tmp") || args[1] !== "/grant:r") { + return ICACLS_OK; // includes snapshot, directory and later ACL steps + } + if (!gatedTemps.has(target)) { + gatedTemps.add(target); + if (gatedTemps.size === 1) { firstEntered(); await firstGate; } + if (gatedTemps.size === 2) { secondEntered(); await secondGate; } + } + return ICACLS_OK; +}); +let syncSpillCalls = 0; +setIcaclsRunnerForTests(args => { + if (isSpillAclTarget(args)) syncSpillCalls++; + return ICACLS_OK; +}); +``` + +Both principal resolvers remain synthetic through `forceWindowsAclLane`. Both ACL runners cover **every** target; filtering controls gating/counting, never whether a real subprocess is used. A fallback must fail the ordering oracle (`syncSpillCalls === 0`), rather than being hidden by the successful mock. + +Replace the current orchestration and 25 ms sleep with this exact ordering: + +1. Enter `try/finally` before the first enqueue/await. Enable fake timers and fixed epoch clock; install both clock setters. Enqueue first response and await its temp gate. +2. Start `flushResponseState`, immediately attach both settlement handlers, recording `flushed` and any error in a resolved outcome object. This avoids an unhandled rejection if an earlier assertion fails. +3. Enqueue second response **after** starting flush, then release first. Await second temp gate. Await a native `setImmediate` checkpoint, advance fake timers by 25 ms, then another native checkpoint. The drain timer stays below 500 ms; no real elapsed filesystem time can fire it. +4. Assert flush is still pending, exactly two distinct temp paths were gated, and no synchronous spill ACL calls occurred. Record `setSpillIoForTest({ record })` events and assert exactly one `stub-swap` so the first publication actually installed while the second is gated. +5. Release second, await the handled flush outcome and rethrow any captured error. Retain `{ residentCount: 0, spillStubCount: 2 }`; add pending `{ count: 0, bytes: 0 }`, two `stub-swap` events, and zero synchronous spill calls. Both stored response IDs must still expand to their distinct payloads. +6. `finally`: release **both** gates, await any started flush outcome and `flushPendingResponseSpillsForTests()` while mocks/clocks remain installed, then restore the Date spy and real timers in a nested `finally`. Existing `afterEach` restores setters. Never restore mocks while a gated async operation still owns work. + +Use a discriminated outcome (`{ ok: true } | { ok: false; error: unknown }`) rather than an undefined-error sentinel. Keep cleanup valid when either startup await/assertion fails. Fake-timer compatibility and the native checkpoint are remote Windows acceptance items, not assumed proof. Do not solve a failed fixture by globally suppressing timers or adding a production seam. + +## Hunk 2 — One logical fallback budget, actual drain timer + +At 1438, preserve `totalMs = 500`, `fallbackReserveMs = 300` and the pending async spill gate. Add the missing spill clock; scope a Date spy to the flush so outer fallback accounting and nested ACL accounting advance together. Keep native timers in this test: the unchanged 200 ms drain timer must expire while the async gate remains held. + +```diff + let aclClock = 0; + setNowForTests(() => aclClock); ++setResponseSpillNowForTests(() => aclClock); +``` + +Record `{ target, timeoutMs, spentBefore }` for **spill** synchronous ACL calls. Snapshot ACL calls return `ICACLS_OK` without charging the spill clock. For each spill call, record before incrementing `aclClock += 20`; preserve successful command results. + +```ts +const epoch = Date.now(); +const nowSpy = spyOn(Date, "now").mockImplementation(() => epoch + aclClock); +// Start only after the async spill gate announces entry. +try { + await flushResponseState(); // native 200 ms drain timer selects sync fallback +} finally { + release(); + try { await flushPendingResponseSpillsForTests(); } + finally { nowSpy.mockRestore(); } +} +``` + +An enclosing `try/finally` must also cover enqueue and `await started`, releasing the gate on early failure. Preserve all three original assertions: at least six spill commands, maximum deadline <= 150, and `200 + aclClock <= 500`. Add: + +- Every timeout is positive and <= `300 - spentBefore` (independent literal budget oracle). +- Within each target's grant/inheritance/remove sequence, each next timeout is exactly 20 ms smaller; do **not** assert global monotonicity across targets because a new harden has its own per-call cap. +- The async gate has not been released when synchronous spill work begins; fallback actually ran, pending count/bytes become zero, one spill stub remains, and replay contains the original payload. + +The Date spy prevents unmeasured real disk latency from consuming this logical-budget fixture. It does not disable the native drain timer. Real-time termination coverage remains in the unchanged cap-expiry test (1339) and `shutdown fallback budget exhaustion is contained by a child watchdog` (1613), using `tests/helpers/responses-state-shutdown-budget-child.ts`. Do not claim this test measures OS elapsed latency. + +## Windows red, control and proof + +Main executes these later on real Windows with the repository-pinned Bun, in isolated remote checkouts. Nothing below authorizes local tests in this documentation task. + +1. Preserve the failed root-head job/logs above. Run the original two tests on the pinned pre-fix baseline; record actual results, including a pass. Do not require random failure or accept retries as a fix. +2. In remote scratch only, force the old stable-tail drain to expire by holding the second gate until a recorded fallback entry. Use a counted synchronous sentinel that reports EICACLS instead of invoking native ACL tools. Confirm rejection and the fallback call; never infer the missing-mock path from elapsed time alone. This is a controlled mechanism probe, not proof that the same delay happened in CI. +3. In remote scratch only, use the existing spill `record("write")` event to advance a separate wall clock by 301 ms once synchronous fallback has begun. On the original reserved-budget fixture, spill uses that clock and fails before temp hardening; with the proposed shared logical spill clock, the same wall-clock perturbation cannot consume the ACL budget. Record entry and clock values. Keep this probe separate from production and from the committed passing fixture. +4. Prove oracle sensitivity with isolated remote mutations: (a) stop drain after its first observed tail, expecting the revised stable-tail pending/zero-fallback oracle to fail; (b) reset the ACL deadline for each command, expecting per-target 20 ms decrease assertions to fail. Separately advance the **injected spill clock** beyond 300 at the write event and require ETIMEDOUT, proving deadline enforcement remains active. Restore every mutation before green verification; retain diff and failing assertion for each probe. +5. Run the unchanged named cap-expiry and child-watchdog controls, then the whole focused file on the new exact head: + +```sh +# Remote Windows only; these commands are a future verifier recipe. +bun test --isolate --timeout 60000 tests/responses/responses-state.test.ts +bun run typecheck +``` + +6. Dispatch the actual Windows full-suite workflow on that exact head, including `bun test --isolate --timeout 60000 tests --shard=5/6` and every other required shard. Inspect job execution, not aggregate success with skipped tests. Record head SHA, Bun version, commands, job URLs, counts and absence of unhandled errors. Run current-head Linux/macOS gates and required scans as well. + +Implementation D means an independently reviewed prepared foundation draft with exact-head focused Windows evidence and remote typecheck; it is **not landing**. Main inserts the verified foundation beneath the stack, refreshes descendants bottom-up with original attribution intact, obtains required current-head gates, then admin-merges in dependency order. Verify each landed SHA is an ancestor of freshly fetched dev before closing a superseded PR or fully resolved issue. Partial issues retain their residual scope. See `080_landing.md`. + +Documentation acceptance: this file names both failed fixtures, all clock/timer boundaries, complete runner/cleanup coverage, executable negative controls, one-file implementation scope and separate landing gates. No test execution or implementation success is claimed here. + +## Remote execution fallback amendment + +The existing direct Windows SSH endpoint is unavailable; the reachable auxiliary host is Linux without Windows interop. Use GitHub Actions for actual Windows proof. If the existing full-suite workflow cannot execute focused causal probes, a separate owner-only `codex/a-verify-windows` branch may hold a temporary verification workflow triggered only by pushes to that exact branch. This workflow is never included in a product PR or merged to dev. It uses `windows-latest`, read-only contents permission, pinned checkout with `persist-credentials: false`, the existing pinned-Bun setup, fixed repository test commands and the exact carried fixture commit. No secrets, untrusted command inputs, self-hosted runner access or release permissions. It may execute the narrowly specified scratch mutations with guaranteed source restoration and upload logs. Independent security audit of the concrete workflow is required before pushing it. Standard per-head full CI remains the final gate; the temporary verifier cannot mark those checks green. diff --git a/devlog/_fin/260906_a_windows_fixture/011_review_resolution.md b/devlog/_fin/260906_a_windows_fixture/011_review_resolution.md new file mode 100644 index 0000000000..5b230ea087 --- /dev/null +++ b/devlog/_fin/260906_a_windows_fixture/011_review_resolution.md @@ -0,0 +1,3 @@ +# Fixture review correction + +Independent implementation reviewer found that full flush settlement includes later snapshot I/O, which could hide an incorrect first-tail drain return. Accepted: the ordering test now starts the existing drain-only helper before appending the second publication and observes its settlement independently. Both drain and full-flush promises attach rejection handlers immediately and are joined during cleanup. Existing publication, replay and zero-fallback assertions remain. No production changes. A remote first-tail mutation must fail this direct drain oracle before completion. diff --git a/devlog/_fin/260906_c_lane/000_plan.md b/devlog/_fin/260906_c_lane/000_plan.md new file mode 100644 index 0000000000..5abf70bf69 --- /dev/null +++ b/devlog/_fin/260906_c_lane/000_plan.md @@ -0,0 +1,11 @@ +# C-lane integration coordination + +Scope: carry public PRs #3638, #3536, #3631, #3576, #3658 with original-author attribution and user-authorized stacked PR integration into dev. No local tests, typechecks or builds. + +The user explicitly requires security working plans and reviews to stay in gitignored scratch. Full numbered diff-level roadmap and evidence live in `.tmp/c-lane/` of the bound d778 checkout; this neutral index is the PABCD plan-unit anchor. This storage override follows AGENTS.md and does not weaken any implementation or verification criterion. + +Order: roadmap → service scheduler → account persistence → OAuth configuration → Antigravity refresh/replay → quota diagnostics → final stack integration. OAuth refresh consumes the configuration layer; other layers retain the user-requested stack order. Each layer is independently reviewed and tested on a remote host before cycle close. Hosted full CI runs at each PR head and gates final bottom-up merges. + +Original PR and fully solved linked issues close immediately after the matching change is proven on dev. Partial diagnostic work does not close a broader unresolved report. Release branches and live account settings are out of scope. + +Completed: see [010_result.md](010_result.md) for published landings, verification and residual scope. diff --git a/devlog/_fin/260906_c_lane/010_result.md b/devlog/_fin/260906_c_lane/010_result.md new file mode 100644 index 0000000000..2dcb6a338b --- /dev/null +++ b/devlog/_fin/260906_c_lane/010_result.md @@ -0,0 +1,20 @@ +# C-lane delivery result + +All five assigned code changes are merged into `dev`. Original-author credit survives in the landed history. This record contains published outcomes only; working security notes remain in scratch. + +| Source | Landed PR | Scope | Merge commit | CI | +|---|---|---|---|---| +| [#3638](https://github.com/lidge-jun/opencodex/pull/3638) | [#3682](https://github.com/lidge-jun/opencodex/pull/3682) | Windows scheduler priority | `9b3955a3345561b3310508793b40ac0813e26b78` | [run 33978490397](https://github.com/lidge-jun/opencodex/actions/runs/33978490397) | +| [#3536](https://github.com/lidge-jun/opencodex/pull/3536) | [#3687](https://github.com/lidge-jun/opencodex/pull/3687) | Account deletion persistence | `ed7ecc5780ea0bd936468aff3828e60c7d9d0d34` | [run 33978685977](https://github.com/lidge-jun/opencodex/actions/runs/33978685977) | +| [#3631](https://github.com/lidge-jun/opencodex/pull/3631) | [#3688](https://github.com/lidge-jun/opencodex/pull/3688) | OAuth provider configuration | `789f69ab1bf57d74dcdd0d658f1bc13d9d486e7b` | [run 33979181943](https://github.com/lidge-jun/opencodex/actions/runs/33979181943) | +| [#3576](https://github.com/lidge-jun/opencodex/pull/3576) | [#3691](https://github.com/lidge-jun/opencodex/pull/3691) | Antigravity OAuth 401 recovery | `7e7ab281cca35600b41f1f80222f3462a87dd4e1` | [run 33979752516](https://github.com/lidge-jun/opencodex/actions/runs/33979752516) | +| [#3658](https://github.com/lidge-jun/opencodex/pull/3658) | [#3693](https://github.com/lidge-jun/opencodex/pull/3693) | Bounded main quota diagnostics | `71edeec8807d99e8e56a8c093f74da27d163d47a` | [run 33985146886](https://github.com/lidge-jun/opencodex/actions/runs/33985146886) | + +Verification: + +- Each recorded code head passed the hosted Cross-platform CI with executed Linux/macOS suites and typecheck. Independent source/security reviews passed on those heads. +- The service change also passed native Linux, macOS and Windows lifecycle run [33978490408](https://github.com/lidge-jun/opencodex/actions/runs/33978490408). +- Focused remote checks passed for each layer; the top diagnostic head passed 554 tests across eleven files, including the bounded subprocess regression. Documentation built 425 pages at that head. +- No local product test suite, typecheck or build was run. Pushes used the maintainer-authorized `--no-verify` path; admin merges followed verified code gates and review evidence. +- Original PRs #3638, #3536, #3631, #3576 and #3658 were closed after dev ancestry was proven. Resolved issues #3634 and #3575 were closed at their respective landings. +- Issue [#3644](https://github.com/lidge-jun/opencodex/issues/3644) remains open: diagnostics were delivered, while its underlying Windows/WHAM failure is still a separate investigation. diff --git a/devlog/_fin/260906_d_integrations_delivery/000_plan.md b/devlog/_fin/260906_d_integrations_delivery/000_plan.md new file mode 100644 index 0000000000..57641fac6e --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/000_plan.md @@ -0,0 +1,46 @@ +# D delivery roadmap + +## Loop specification + +- Archetype: spec-satisfaction repair; C3 integration train. Alias routing is C4 where it affects upstream credential destinations; keep any undisclosed security analysis in ignored scratch. +- Trigger: owner assigned D: #3669, #3673, #3628, #3625, #3646. +- Goal: integrate these five bounded outcomes into dev with contributor attribution, current-head remote CI and immediate disposition of superseded originals/resolved issues. +- Non-goals: A/B/C implementation, main/preview/release, dogfood service, personal accounts/credentials, unrelated thinking/cache behavior. +- Verifier: GitHub Cross-platform CI gates and platform jobs for every layer; GUI lint/build plus real isolated browser smoke for Logs; targeted CI failures repaired without local tests. NEVER run local test suites, focused tests, test:changed, or typecheck. This owner instruction overrides local-gate defaults in AGENTS/skills. +- Stop: all five landed SHAs reachable from freshly fetched origin/dev, original PRs and genuinely resolved issues closed, independent reviews clear, evidence recorded. +- Memory: this unit, .tmp/d-delivery evidence and session-bound goalplan/ledger in this checkout. +- Tool/credential scope: local source/git, gh for this repository, inherited-model agents, isolated browser QA. No purchases or new credential/account actions. +- Bounds: no owner-set numerical token/cost cap or arbitrary delegation count; 12-hour per-phase wall-clock review bound, checkpoint and reassess if reached. Context compaction is not exhaustion. +- Escalation up: main reclaims any packet after two distinct agents fail it. Down: worker scopes must be fixed in the corresponding P document before B. No speculative next-phase implementation. +- Outcomes: DONE/NOOP only with fresh evidence; unresolved external conditions remain pending and do not weaken the final criteria. + +## Checkout and source snapshot + +Worktree is adopted in place. Initial dev is 81871b3fa7034250b8d5ba2cbbfde44e40f0e69c. Live source bodies/comments/commits and exact heads are saved in .tmp/d-delivery/pr-N.json. Source refs are origin/d-source-N. No source suites were executed during planning. + +## Structure and sequence + +1. roadmap: docs-only complete PABCD; lock 010–040 designs and the scratch-backed 050 work item. +2. toml / 010: config parse admission foundation; carry #3669. +3. toolalias / 020: stream argument identity; carry #3673. +4. cursor / 030: executable schema projection on current adapter layout; carry #3628. +5. logs / 040: expose existing filter predicate in actual UI; carry #3625. +6. remotealias / 050: bind generated client aliases to hub-owned routing; resolve #3646. +7. landing: bottom-up dev integration and original-item closeout. + +The five fixes are distinct functional units; the owner explicitly requested stacked PRs, so the delivery chain imposes an integration order, not a claim that TOML is a functional dependency of Cursor. Each layer has its own tests/docs and is independently reviewable. Create the documentation parent first, then stack the five item branches. Land eligible lower layers early when CI and review permit, immediately retarget remaining children and verify ancestry. Each implementation cycle certifies its current-head candidate; final landing criteria retain every dev-ancestry and closeout obligation. + +## Shared ownership + +- A owns shared Responses core integration; D #3673 modifies openai-chat.ts, not core.ts. +- B #3659 and D #3625 share locale modules; integrate both sets of keys. +- B #3649 and D #3646 may both touch Claude aliases/claude-messages.ts. Re-read remote dev before 050 and preserve Fable selector normalization. +- New tests must register both layout manifests where applicable. Existing test edits retain current paths. + +## Attribution and GitHub operations + +Original author commits or valid Co-authored-by trailers are retained. Every push uses --no-verify. Own rewritten stack refs use --force-with-lease only if required; never rewrite another active task branch. All PR bodies fill Summary/Verification/Checklist and show stack base, source PR, evidence and screenshot for visible GUI changes. Merge bottom-up; refresh head, CI, review and origin/dev immediately before each merge. After merge prove git merge-base --is-ancestor landed-sha origin/dev, then close superseded source PR and any fully solved issue. Partial issues stay open with exact remaining scope. + +## Verification route inspected + +.github/workflows/ci.yml uses pull_request without a base filter (stack support); src/tests/gui/docs changes are selected by changes job. gates executes Typecheck (lines 422–425), GUI tests (427–428), privacy (430–431), GUI lint/build when relevant; platform shards run the repository tests. Read-only git diff origin/dev...origin/d-source-3669 --check exited 0 and observes the source delta. Roadmap validation is a documentation-only Python check, not an application test suite. diff --git a/devlog/_fin/260906_d_integrations_delivery/001_roadmap_result.md b/devlog/_fin/260906_d_integrations_delivery/001_roadmap_result.md new file mode 100644 index 0000000000..ab86cff0e3 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/001_roadmap_result.md @@ -0,0 +1,9 @@ +# Roadmap lock result + +Independent reviewer 01a0726c-c782-7701-962f-2607911a33af returned VERDICT: PASS, no actionable blockers. All five implementation designs, provenance, CI targets and separate final landing obligations were checked. + +The documentation-only verifier passed. An initial whitespace check flagged two blank context lines inside the embedded TOML diff; the command sequence did not stop and the B-to-C narrative incorrectly said the whitespace check passed. The whitespace was removed and the final complete roadmap diff was checked again successfully before closeout. No production test or typecheck ran locally. + +Next: enter the TOML cycle, refresh 010 against the current parent, carry the original authored commit, add the architecture contract, publish as a child of the documentation PR and obtain current-head hosted CI. The full delivery goal remains open. + +External review subsequently required correcting planning-artifact placement and tightening two future tool-contract designs. The detailed review synthesis is retained in ignored scratch. The public 050 entry now contains only a work-item pointer; its implementation is still pending. The independent initial PASS did not detect these issues and does not substitute for the corrective review. diff --git a/devlog/_fin/260906_d_integrations_delivery/010_toml_guard.md b/devlog/_fin/260906_d_integrations_delivery/010_toml_guard.md new file mode 100644 index 0000000000..e7014cf8ca --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/010_toml_guard.md @@ -0,0 +1,122 @@ +# 010 — TOML rewrite admission + +Depends on: roadmap lock. Class C2/C3 parser-admission preservation. Owner: main; bounded implementation reviewer during B, no future-phase code writes. + +Source: PR #3669, commit f6db9cae8e8854c6df06087288a074d767f9787d, Hako <25837994+devswha@users.noreply.github.com>. Preserve this author via cherry-pick and add Co-authored-by on carry PR. Existing review has no unresolved threads; reported local results are not our current-head CI proof. + +## File change map + +MODIFY src/integrations/config-io.ts: replace direct Bun.TOML.parse return with iterative document walk. Reject non-array objects with prototypes other than Object.prototype or null before JSON cloning can coerce typed date/time scalars to strings. Scalars/quoted dates/plain objects/arrays remain accepted. +MODIFY tests/clients/integrations-state.test.ts: exercise all supported TOML temporal kinds at root, nested tables and inline arrays; quoted equivalents stay accepted. +MODIFY tests/clients/integrations-writer.test.ts: real temp Kimi config produces unsafe state; apply refuses without changing original bytes, operation journal or ownership records. +MODIFY docs-site/src/content/docs/{guides,fr/guides,tr/guides,zh-tw/guides}/integrations.md: carry the source commit descriptions of refused date/time rewrites. +MODIFY structure/09_client-integrations.md: add typed TOML temporal values to the existing round-trip refusal contract after the classifier paragraph. +No new fields, enums, serializers, dependencies, runtime options or management endpoints. The parser is the existing common admission point for status and writers. Bypass is explicit manual editing outside managed rewrite; this guard does not control that user action. + +## Exact source patch + +```diff +diff --git a/src/integrations/config-io.ts b/src/integrations/config-io.ts +index 4f2a83482..9cb5f97ba 100644 +--- a/src/integrations/config-io.ts ++++ b/src/integrations/config-io.ts +@@ -162,7 +162,22 @@ export function parseConfig(text: string | null, format: ConfigFormat): unknown + * evidence is gone. + */ + if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED; +- return Bun.TOML.parse(text); ++ const document = Bun.TOML.parse(text); ++ // TOML date/time scalars are Temporal objects with toJSON methods. ++ // The merge layer JSON-clones documents, which silently turns these ++ // into strings. Refuse before either status or a writer can admit a ++ // lossy rewrite, including dates nested in arrays and inline tables. ++ const pending: unknown[] = [document]; ++ while (pending.length > 0) { ++ const value = pending.pop(); ++ if (value === null || typeof value !== "object") continue; ++ if (!Array.isArray(value)) { ++ const prototype = Object.getPrototypeOf(value); ++ if (prototype !== Object.prototype && prototype !== null) return PARSE_FAILED; ++ } ++ for (const child of Object.values(value)) pending.push(child); ++ } ++ return document; + } + } + } catch { +diff --git a/tests/clients/integrations-state.test.ts b/tests/clients/integrations-state.test.ts +index 54ab80de1..872e9b382 100644 +--- a/tests/clients/integrations-state.test.ts ++++ b/tests/clients/integrations-state.test.ts +@@ -401,6 +401,25 @@ describe("classifier unit behavior", () => { + expect(parseConfig("{{{", "json")).toBe(PARSE_FAILED); + }); + ++ test("parseConfig refuses typed TOML dates before a JSON clone can turn them into strings", () => { ++ for (const literal of [ ++ "2026-09-05T10:00:00Z", ++ "2026-09-05T10:00:00-07:00", ++ "2026-09-05T10:00:00.123456", ++ "2026-09-05", ++ "10:00:00.123456", ++ ]) { ++ for (const text of [ ++ `expires = ${literal}\n`, ++ `[user]\nexpires = ${literal}\n`, ++ `items = [{ expires = ${literal} }]\n`, ++ ]) { ++ expect(parseConfig(text, "toml")).toBe(PARSE_FAILED); ++ } ++ expect(parseConfig(`expires = "${literal}"\n`, "toml")).toEqual({ expires: literal }); ++ } ++ }); ++ + test("parseConfig refuses json number literals a rewrite would change", () => { + // Overflow to Infinity — a rewrite would bake in null. + expect(parseConfig("{\"a\": 1e999}", "json")).toBe(PARSE_FAILED); +diff --git a/tests/clients/integrations-writer.test.ts b/tests/clients/integrations-writer.test.ts +index 0bf81fdb5..de2f16471 100644 +--- a/tests/clients/integrations-writer.test.ts ++++ b/tests/clients/integrations-writer.test.ts +@@ -141,6 +141,24 @@ function reverseJsonObjectKeys(value: unknown): unknown { + } + + describe("apply", () => { ++ test("refuses Kimi TOML date rewrites without changing the file or ownership store", () => { ++ const spec = INTEGRATION_CLIENTS.kimi; ++ mkdirSync(spec.detectDir(TEST_ENV, home), { recursive: true }); ++ const configPath = spec.configPath(TEST_ENV, home); ++ mkdirSync(dirname(configPath), { recursive: true }); ++ const original = "[user]\nexpires = 2026-09-05T10:00:00Z\n"; ++ writeFileSync(configPath, original); ++ const request = input({ clientId: "kimi" }); ++ ++ expect(readIntegrationState(request).state).toBe("unsafe"); ++ const result = applyIntegration(request); ++ expect(result.ok).toBe(false); ++ if (!result.ok) expect(result.reason).toBe("unsafe"); ++ expect(readFileSync(configPath, "utf8")).toBe(original); ++ expect(store.listOperations()).toHaveLength(0); ++ expect(store.readRecords().kimi).toBeUndefined(); ++ }); ++ + test("refuses a client that is not installed, and writes nothing", () => { + const result = applyIntegration(input()); + expect(result.ok).toBe(false); +``` + +## Additional structure diff + +After “Status and mutation must use the same classifier” paragraph add: + +> TOML temporal scalars cannot survive the JSON-cloned merge representation with their types intact. The common parser refuses documents containing them before either status or mutation proceeds, including nested arrays and inline tables. Quoted date strings remain supported. + +## Acceptance and activation + +- Unquoted offset/local date-time, local date, local time at every tested nesting returns PARSE_FAILED. +- Identical quoted values remain plain strings and can be managed. +- Kimi apply on typed temporal input activates unsafe classification and writes nothing, including bookkeeping. +- Existing special-float admission and other formats are unchanged. +- C consumes hosted current-head CI actual gates/platform jobs; no local suites/typecheck. Original focused paths named above are included in the CI repository tests. +- Independently review prototype traversal and actual parser shapes; unexpected compatibility gaps change the plan before implementation. +- Once integrated, refresh dev ancestry and close source #3669 immediately with attributed carry PR evidence. diff --git a/devlog/_fin/260906_d_integrations_delivery/011_toml_refresh.md b/devlog/_fin/260906_d_integrations_delivery/011_toml_refresh.md new file mode 100644 index 0000000000..5c2850fa4d --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/011_toml_refresh.md @@ -0,0 +1,8 @@ +# TOML cycle P refresh + +Parent: cb75f49c9401e10f8bd37f4817cdef32b0a5cbe1, documentation PR #3681. +Source: f6db9cae8e8854c6df06087288a074d767f9787d by Hako. + +Read-only git comparison from source parent to the current parent returned no changes in config-io.ts and the two affected client regression files. The 010 diff remains applicable. The shared parser admits both status and writers; no caller-specific exception or new option is required. + +Implementation scope stays as 010. Main will cherry-pick the original commit and add the structure contract. An inherited independent reviewer audits the candidate; no local application tests or typecheck are permitted. Hosted CI supplies runtime verification; docs build may run in a fresh macmini-cf scratch checkout with no real credentials or service changes. diff --git a/devlog/_fin/260906_d_integrations_delivery/020_tool_aliases.md b/devlog/_fin/260906_d_integrations_delivery/020_tool_aliases.md new file mode 100644 index 0000000000..9d1b646314 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/020_tool_aliases.md @@ -0,0 +1,134 @@ +# 020 — Retain late Chat tool-call index aliases (#3673) + +## Loop specification + +- Class: C2 adapter repair; spec-satisfaction loop, one implementation PABCD cycle. +- Trigger: an upstream Chat stream introduces a call by ID, associates an index later, then sends index-only fragments. +- Goal: one complete call retains its original ID/name and argument budget ownership. +- Non-goals: guessing associations between unindexed calls, changing other malformed-field tolerance, changing budget limits, transport/core changes, unrelated adapter refactors. +- Verifier: exact-head hosted CI executes the focused cases below plus repository typecheck/full-suite gates. NO local tests, suites, typecheck, or test:changed; commands below are runner-only specifications. +- Stop: all acceptance rows and required hosted jobs pass on the delivered head, review findings resolved, and main proves delivery to dev. A docs-only result does not satisfy implementation criteria. +- Memory artifact: this decade document and main-owned 000/CI evidence ledger in the same unit. +- Outcomes: DONE after proof; NOOP only if current dev already has equivalent behavior and CI proof; otherwise BLOCKED/NEEDS_HUMAN with the concrete missing external evidence. Main controls orchestration and goals. +- Escalation: report upstream to main if the refreshed source no longer matches these contracts; main reclaims after two failed distinct delegates. Further delegated scope must be recorded during P, not improvised in B. +- Resources: local source/refs and supplied PR snapshot are read-only inputs; this planning delegate writes only this document and 030. Implementation write scope is the map below; main owns credentials, publication, CI dispatch, merge and its session-wide resource bound. No paid/provider calls are needed. + +## Provenance and stale check + +Baseline: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`, inspected 2026-09-06 KST. +Source ref `origin/d-source-3673` resolves to `c8240c51d664f7cfb790b6d60679adfe0490b5c9`. +Original author: **Hako <25837994+devswha@users.noreply.github.com>** (`devswha`). +Source patch parent: `6585e6a70f42be8b6c81ff20d4fa0f39f7da03db`. +Snapshot: `.tmp/d-delivery/pr-3673.json` (`headRefOid`, body, comments, checks). +Read-only comparison `git diff c8240c51d^ 81871b3fa --` across the three source-PR paths returned no diff: source patch applies to the same relevant baseline. Recheck at this cycle's P because other lanes may land first. + +Preserve author identity when carrying the commit; include `Co-authored-by: Hako <25837994+devswha@users.noreply.github.com>` in the eventual squash description/commit. Main may carry with a cherry-pick or reimplementation; neither is performed by this document writer. + +## Exact change map + +| Operation | Path | Change | +|---|---|---| +| MODIFY | `src/adapters/openai-chat.ts` | Add first-observed index alias to pending call identity lookup; keep budget key immutable. | +| MODIFY | `tests/adapters/openai/openai-chat-parallel-stream.test.ts` | Port the complete original regression patch, extending T9b and adding collision/budget controls. | +| MODIFY | `docs-site/src/content/docs/reference/adapters.md` | Port the original five-line paragraph under openai-chat. | +| MODIFY | `structure/04_transports-and-sidecars.md` | Append the contract block below in C. | +| NEW | none | Existing test file already appears in both layout manifests; no new helper/module/manifest entry. | + +Read dependencies: `tests/helpers/translator-budget.ts`, `src/lib/translator-budget.ts`; reuse `createTestTranslatorBudget`, `withTestTranslatorBudget`, existing `collect`, `sse`, `chunkOf`, and `assembled`. No additional registry or identity map is necessary. Configuration cannot fix missing association state; deletion/NOOP would leave the observed sequence broken. + +## Concrete patch contract + +The exact original patch is the complete diff `git show c8240c51d664f7cfb790b6d60679adfe0490b5c9 -- src/adapters/openai-chat.ts tests/adapters/openai/openai-chat-parallel-stream.test.ts docs-site/src/content/docs/reference/adapters.md`. Preserve all hunks, including test import/helper changes; do not port just T9b. + +Current anchors: `src/adapters/openai-chat.ts:1661` pending interface, `:1856` identity lookup, `:1873` budget opening, `:1912` argument-byte accounting, `:1679` budget closing. Replace the lookup block with: + +```ts +if (rawIndex !== undefined && rawIndex !== null + && (typeof rawIndex !== "number" + || !Number.isSafeInteger(rawIndex) + || rawIndex < 0)) { + return yield* terminateWithError({ + ...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage), + message: "upstream response contained invalid tool calls (invalid index)", + }); +} +const indexKey = typeof rawIndex === "number" ? `i:${rawIndex}` : undefined; +const key = indexKey ?? (idDelta + ? `id:${idDelta}` + : pendingToolCalls[pendingToolCalls.length - 1]?.key); +let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined; +if (!call && indexKey !== undefined) call = pendingToolCalls.find(c => c.indexKey === indexKey); +if (!call && idDelta) call = pendingToolCalls.find(c => c.id === idDelta); +``` + +Add `indexKey?: string` after `PendingToolCall.key`. Immediately after the existing new-call allocation/openCall block, add the source comment and: + +```ts +if (indexKey !== undefined && call.indexKey === undefined) call.indexKey = indexKey; +``` + +Before: the ID+index delta finds the ID-owned call through ID fallback but does not retain its index; the next index-only delta allocates another unnamed call. After: direct key wins, then remembered index alias, then existing ID fallback. The original `call.key` never changes and alias registration does not call `budget.openCall` a second time. First observed index remains authoritative; no second alias is added for a repeated ID on a different index. Keep resolve-before-validation, `sawArgumentsString`, heartbeat emission, overflow conversion, flush and EOF logic unchanged. + +## Regression activation and oracle + +Port exact fixtures/assertions from the source commit; all paths are reachable through `createOpenAIChatAdapter(...).parseStream(new Response(sse(...)), budget)`. + +| Activation | Required observation | +|---|---| +| T9b ID-only `call_b/read/{"p"`, then index 0 + same ID + `:"x"`, then index 0 + `}` | Exactly `call_b/read/{"p":"x"}`; final done; budget activeCalls/currentBytes/overflows all zero. Current T9b at test line 214 ends at ID+index and misses the defect. | +| Two ID-only calls, learn indexes 9 and 4 in reverse order, index-only tails plus ID-only trailing space | Separate read/write calls and exact original fixture args; peak active calls 2, no duplicate owners, final zero retained bytes. | +| Two unindexed calls then unrelated index-only fragments without any ID/index association | Final error and no done; never guess by position. | +| Existing index with conflicting ID | Index ownership wins; neither call rebound. | +| Established indexed calls later share the same ID, followed by ID-only continuation | Existing first-match ID fallback stays intact. | +| Same ID repeats with a second index after index 0 was observed | Index 0 remains the alias; exact fixture completes one call. | +| `{"p":"é"}` split over ID/ID+index/index frames, maxCallArgumentBytes 9 then 10 | At 9: translation_buffer_limit, no tool_call_start, one overflow. At 10: exact completed args, done, zero overflow. Both release retained bytes/calls. | + +Optional additional mutation experiment (not a completion prerequisite): run the final regression file against the baseline adapter and observe T9b fail for split/unnamed calls; restore patched adapter and rerun the same file green. Store both outputs; until observed, describe RED as planned rather than proven. Do not disable original tests or change timeouts to mask failures. + +Runner-only focused command: + +```sh +bun test tests/adapters/openai/openai-chat-parallel-stream.test.ts tests/adapters/openai/openai-chat-hardening.test.ts tests/adapters/openai/openai-chat-eof.test.ts +``` + +Then hosted typecheck/full test jobs, privacy scan and docs build; `.github/workflows/ci.yml:255` owns test jobs, `:392` gates, `:422` typecheck. Record actual head SHA, run/job URLs and executed job conclusions; intake labels and skipped jobs do not prove tests. Main pushes with `--no-verify` as authorized, bypassing local prepush only. Do not attest that local CI ran. + +## Documentation and architecture sync + +Apply source paragraph before `## ollama-native` at adapters reference line 52. Reconcile this same-file edit with A's #3568 docs and 030's Cursor section without overwriting either. English is canonical; inspect translated adapter pages for contradictory identity claims, and enumerate any required locale changes in P before widening the map. + +Append to `structure/04_transports-and-sidecars.md`: + +```md +## Chat streamed tool-call identity + +`src/adapters/openai-chat.ts` retains a call's first observed numeric index as an +alias when the call started by ID. Lookup preserves direct-key precedence, then +index alias, then ID fallback. The initial key continues to own all translator +budget reservations and release; learning an alias creates no additional owner. +Unassociated index-only fragments are not guessed onto pending ID-only calls. +`tests/adapters/openai/openai-chat-parallel-stream.test.ts` covers late aliases, +parallel/colliding identities and UTF-8 byte-limit boundaries. +``` + +## Review blockers and integration exit + +Snapshot says MERGEABLE. Source body leaves draft/readiness open: 124 focused passes and 6,152 affected passes are author-reported, not delivered-head evidence; full baseline has reported timeout failures and does not establish green. CodeRabbit's latest comment reports no actionable comments; its docstring coverage warning is not product execution proof. No independent approval or review-thread completeness can be inferred solely from the empty `reviews` array. Main must refresh threads and CI at the candidate head. + +This layer follows 010 in the D stack as an integration sequence, not a runtime dependency. After lower-layer edits, main cascades refreshed descendants and revalidates changed heads. Main merges bottom-up, proves merge-commit ancestry on fetched dev, and immediately closes superseded #3673 only after that proof. A new PR's squash must retain the original trailer. This docs-only delivery neither merges nor closes anything. + +## Roadmap lock clarification + +The implementation cycle certifies its published current-head candidate. Every dev-ancestry and original-closeout obligation remains mandatory in the separate landing work-phase, allowing the owner-requested stack to exist without treating publication as dev integration. Eligible lower layers may land early and are closed immediately after ancestry proof. + +## External review amendment: numeric index contract + +Only non-negative safe-integer indexes may become an alias. Immediately after reading rawIndex, if it is numeric but not an integer or is negative, terminate through the existing invalidToolCallsEvent/terminateWithError path; do not treat an invalid numeric index as absent and append its data to the last pending call. Other tolerated placeholder fields retain their existing rules. Add reachable negative/fractional numeric-index regressions with two distinct pending calls: one error, no done, no fragment reassignment, and all budget reservations released. Preserve all original positive and collision cases. This is an explicit source-patch amendment, not a claim the original commit already implements validation. + +## Safe-integer review repair + +The numeric guard uses Number.isSafeInteger: parsed indices beyond the safe range can already have lost identity precision. Add a raw-wire regression containing distinct large integer literals (not JS values rounded before serialization), and retain a positive MAX_SAFE_INTEGER boundary. Capture error/no tool success plus existing reservation-release coverage. The correction must be verified in this same unit; no original source tests are removed. + +## Claimed-type boundary update + +022 supersedes the earlier non-numeric-index tolerance assumption: only missing/null indexes are absent. Every other claimed value must be a non-negative safe integer; no coercion of strings/objects/bools/arrays. Repeated ID/name/argument-field tolerance is unchanged. diff --git a/devlog/_fin/260906_d_integrations_delivery/021_tool_alias_refresh.md b/devlog/_fin/260906_d_integrations_delivery/021_tool_alias_refresh.md new file mode 100644 index 0000000000..c8d03ce3f5 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/021_tool_alias_refresh.md @@ -0,0 +1,9 @@ +# Tool-call alias cycle P refresh + +Historical refresh: its non-numeric-index policy is superseded by the explicit null/missing boundary in 022_index_type_repair.md. + +Current parent: 22da7a4bc80040f66b819239c5028e578f9a1ede, after TOML delivery. Original source c8240c51d664f7cfb790b6d60679adfe0490b5c9 remains open and authored by Hako. Relevant baseline comparison is retained in scratch; implementation uses the current tree and preserves adjacent changes. + +Apply the original commit, then the independently reviewed 020 numeric-index amendment. Missing/non-numeric placeholders keep existing tolerance; negative/fractional numeric indexes terminate before matching. Preserve the immutable reservation key and first observed valid index alias. Add direct malformed-index activation coverage alongside all original positive/collision/UTF-8 budget cases. Update the transport structure contract as planned. + +Main owns cherry-pick/commits/PR/CI/merge. An inherited worker may edit only src/adapters/openai-chat.ts, tests/adapters/openai/openai-chat-parallel-stream.test.ts, and structure/04_transports-and-sidecars.md after A passes. Main owns this document and all other files. Independent reviewer checks resulting code; all tests/typechecks execute remotely or in GitHub Actions. Full-suite readiness remains remote; no local application checks. macmini shared test lock is respected. diff --git a/devlog/_fin/260906_d_integrations_delivery/022_index_type_repair.md b/devlog/_fin/260906_d_integrations_delivery/022_index_type_repair.md new file mode 100644 index 0000000000..704278ae34 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/022_index_type_repair.md @@ -0,0 +1,41 @@ +# 022 — Reject claimed invalid index types + +## Loop specification + +Class C2/C3 bounded parent repair. Source: current #3702 at d6bfb044a; late reviews PRRT_kwDOS-0Gi86fl4vM and fl4vC. Goal: a present invalid index cannot be mistaken for an absent index and routed to the last pending call. Non-goals: changing repeated ID/name/argument placeholder tolerance, parsing numeric strings, new adapters or unrelated Logs work. Remote/CI verification only; no local tests/typecheck. Same session resource bounds apply. Main owns Git/FSM/integration; one worker may edit only the adapter, its parallel-stream test and structure04. Main reclaims after two failed delegates. + +This additive repair preempts unfinished Logs planning. No previous work-phase completion marks or final criteria were removed. Detailed review synthesis is in scratch. Resume Logs after this full cycle and cascade. + +## Exact change map + +MODIFY src/adapters/openai-chat.ts, before all key matching: + +```ts +if (rawIndex !== undefined && rawIndex !== null + && (typeof rawIndex !== "number" + || !Number.isSafeInteger(rawIndex) + || rawIndex < 0)) { + return yield* terminateWithError({ + ...invalidToolCallsEvent(rawToolCalls, "stream", pendingUsage), + message: "upstream response contained invalid tool calls (invalid index)", + }); +} +``` + +Before: only invalid numbers reject; present strings/objects/bools become no indexKey and may select the last pending call. After: only missing/null is absent; every other claimed index must be a non-negative safe integer. The existing terminateWithError closes all budget reservations before the error is yielded. Keep the alias/key precedence and immutable reservation keys unchanged. No new fields/enums/dependencies. + +MODIFY tests/adapters/openai/openai-chat-parallel-stream.test.ts: retain all Hako and safe-integer cases. Update expected diagnostic wording. Add labeled table cases for numeric string, empty string, true/false, object and array, with pending complete JSON calls so a silent fallback could otherwise produce success; assert one terminal502, no tool/done event and released reservations. Include explicit missing/null positive continuation through a later valid numeric alias. Use tuple wrappers for array-valued cases so test.each cannot mistake an index array for argument tuples. + +MODIFY docs-site/src/content/docs/reference/adapters.md: specify non-negative safe integers; explicitly reject non-numeric values and negative/fractional/unsafe numbers; missing/null remain absent-index placeholders. Do not call valid JSON numbers malformed JSON. + +MODIFY structure/04_transports-and-sidecars.md: align the same index contract and source/test ownership. + +MODIFY 020_tool_aliases.md: carry the corrected guard and compatibility boundary. Annotate 021's former non-numeric-placeholder policy as superseded by this repair; retain its historical source snapshot. + +## Verification and exit + +- Independent plan and implementation review; original source authorship retained. +- Exact-head pinned remote typecheck/full suite/docs build, hosted CI registration and no unresolved findings. Full final integrated CI remains mandatory under c-2; build readiness is not merge permission. +- Existing numeric/unsafe/UTF-8/collision cases remain green; new claimed-type cases actually observe pending allocations before early failure, and null/missing positive cases still assemble one correct tool. +- Cascade new parent into Cursor with a merge preserving both authors' commits and both structure sections; fast-forward the still-unpublished Logs branch to updated Cursor. Verify both ancestry edges. Do not mark the updated Cursor head verified until its own new evidence exists. +- Main returns to parent for the repair receipt/D, then resumes original Logs planning. Shipping #3702 still requires strict merge verification and actual dev ancestry before source #3673 closes. diff --git a/devlog/_fin/260906_d_integrations_delivery/030_cursor_schemas.md b/devlog/_fin/260906_d_integrations_delivery/030_cursor_schemas.md new file mode 100644 index 0000000000..2150e7af91 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/030_cursor_schemas.md @@ -0,0 +1,151 @@ +# 030 — Preserve Cursor executable tool schemas (#3628) + +## Loop specification + +- Class: C3 adapter contract carry across the current module split; spec-satisfaction, one implementation PABCD cycle. +- Trigger: bare exec_command advertisement omits supported execution fields, and freeform tools advertise an empty parameter object. +- Goal: preserve shell fields and a required string freeform input through advertisement and argument normalization, with reserved shell-name rejection. +- Non-goals: executing commands, changing approval/sandbox policy, nativeLocalExec defaults, OAuth or transport changes, changing generated protobuf code, rejoining split modules. +- Verifier: exact delivered-head hosted focused Cursor regressions, typecheck/full-suite, privacy and docs build. NO local tests, suites, typecheck or test:changed. Commands in this document run only on CI runners. +- Stop/outcomes: DONE only after acceptance rows, current-head required jobs and independent review pass and main proves dev integration. NOOP requires equivalent current-dev implementation plus evidence; external validation/permission gaps are BLOCKED/NEEDS_HUMAN, never success. +- Memory: this document plus the main-owned research/CI ledger. Main owns goals and FSM; this planning delegate does not alter either. +- Escalation: changed contracts/conflicts return to main at P; two failed distinct worker packets cause main reclaim. Further downward delegation is a P amendment. +- Resource/write scope: read local refs and supplied PR JSON, write only the two delegated roadmap files during this task; later implementation is restricted to the exact map below. Main owns authorized GitHub credentials, publication/merge and session resource bounds. No paid endpoint probes or tool execution are required. + +## Provenance, owner migration and blockers + +Inspected baseline `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c` on 2026-09-06 KST. +Source `origin/d-source-3628` is `37e6115c8a2ad3ffe20fee1e5a1e79a054625a56`. +Carry both original commits, in order: + +1. `1b29236c5bee9dd166b9d23983a2f1f1c2f0b793` — preserve executable tool schemas. +2. `37e6115c8a2ad3ffe20fee1e5a1e79a054625a56` — reject reserved freeform shell names. + +Both are authored by **SB Yoon <44089734+yansigit@users.noreply.github.com>** (`yansigit`). Preserve original authorship and add `Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com>` to the eventual squash commit/description. + +`.tmp/d-delivery/pr-3628.json:133` records source head, `:134` CONFLICTING. Its body reports 32 focused tests/full-suite success on source head; this is not candidate CI proof. The earlier reviewer finding at old `tool-definitions.ts:429` rejects bare freeform exec_command/shell_command. The author comment references pre-rebase `ae871bd19`; the fetched source's actual second commit above contains the correction. Carrying only the first commit would reintroduce the finding. Refresh actual current threads at integration; the supplied reviews/comments snapshot is not a complete unresolved-thread query. + +Current schema owner is **`src/adapters/cursor/tool-schemas.ts`**, moved by `3435d03983fdec305c6f2f4633650a15699a28e0` (split S04 L1/5). `tool-definitions.ts:6` imports schemas and `:8` preserves the public re-export facade. Do not cherry-pick a whole stale file over the split. Translate original schema hunks by symbol, retain current helpers, and add the new constant to the facade. + +## Exact file change map + +| Operation | Path | Change | +|---|---|---| +| MODIFY | `src/adapters/cursor/tool-schemas.ts` | All original production schema additions and both freeform guards, adapted from old tool-definitions.ts. | +| MODIFY | `src/adapters/cursor/tool-definitions.ts` | Add CURSOR_FREEFORM_INPUT_SCHEMA to the existing line-8 re-export only. | +| MODIFY | `tests/providers/cursor/cursor-tool-definitions.test.ts` | Port both source commits' complete regression hunks, preserving current file additions. | +| MODIFY | `docs-site/src/content/docs/reference/adapters.md` | Add exact Cursor contract bullet below under existing cursor section. | +| MODIFY | `structure/04_transports-and-sidecars.md` | Add schema ownership/normalization contract below. | +| NEW | none | Reuse existing file and test registration; no dependency or generated protobuf changes. | + +Read-only consumers: `tool-naming.ts:76` isBareCodexShellBridgeTool (`!namespace` plus reserved name), `tool-definitions.ts:80` buildCursorToolDefinitions and `:92` schema encoding, `live-transport.ts:672` toolSchemas normalization map, `arg-normalize.ts:69` normalizeArgKeys. Existing tool choice filtering and namespaced names must remain unchanged. Configuration/NOOP cannot supply missing schema declarations; reuse existing schema owners rather than add a parallel abstraction. + +## Exact patch references and adaptation + +The authoritative complete patch is: + +```sh +git diff 6b85485f32f783bafc61c79185d0cb937848859d 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56 -- src/adapters/cursor/tool-definitions.ts tests/providers/cursor/cursor-tool-definitions.test.ts +``` + +Apply all production hunks from the old path to these current symbols in `tool-schemas.ts`: + +1. `CURSOR_EXEC_COMMAND_INPUT_SCHEMA` at line 4: after max_output_tokens, add the original sandbox_permissions string enum (`use_default`, `require_escalated`), justification string, prefix_rule string array, login boolean, including original descriptions. Preserve required `["cmd"]` and additionalProperties false. +2. Add immediately after that constant: + +```ts +/** Cursor represents a Responses freeform tool body as one string-valued input field. */ +export const CURSOR_FREEFORM_INPUT_SCHEMA = { + type: "object", + properties: { input: { type: "string" } }, + required: ["input"], + additionalProperties: false, +} as const; +``` + +3. `CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA` at line 64: add the same four property shapes from the source diff, keeping command as the canonical fallback and preserving max_output_chars. +4. At the start of BOTH `cursorToolInputSchema` (line 80) and `cursorToolArgNormalizeSchema` (line 89), insert this complete block before the current shell/function fallback: + +```ts +if (tool.freeform) { + if (isBareCodexShellBridgeTool(tool)) { + throw new Error(`freeform Cursor tools cannot use reserved shell bridge name ${tool.name}; use a namespace`); + } + return CURSOR_FREEFORM_INPUT_SCHEMA; +} +``` + +5. Add `CURSOR_FREEFORM_INPUT_SCHEMA` to the existing `export { ... } from "./tool-schemas"` facade at tool-definitions.ts:8. Existing tests import through that facade; do not introduce a second definition or silently change the public import surface. +6. Port original test imports for CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA and CURSOR_FREEFORM_INPUT_SCHEMA and the complete 94-line regression addition. Imports remain `../../../src/...` in the existing providers/cursor test directory. Existing manifests already register this file (layout.json:554, test-layout-expected.json:391). + +Before: bare exec advertisement lacks four fields; normal/freeform schema lookup falls through to `parameters ?? {}`. After: both normalization and advertisement use one input string for freeform; bare reserved freeform names are rejected before either can acquire shell semantics. Namespaced shell-like tools stay ordinary freeform. Ordinary shell_command converts cmd to command; caller-supplied cmd-only exec_command stays cmd-only via existing shellBridgeArgNormalizeSchema. Keep this helper and current required-command validation intact. + +## Regression activation scenarios + +| Constructible input | Observable assertion | +|---|---| +| Bare non-freeform exec_command passed to buildCursorToolDefinitions | Decode protobuf ValueSchema and verify cmd schema plus enum/string/array/boolean field shapes; required cmd and additionalProperties false remain. | +| Freeform apply_patch with parameters `{}` | Both schema functions and decoded protobuf require a string input. | +| Freeform bare exec code-mode tool without parameters | Both schema functions return the same required-input contract. | +| Bare freeform exec_command and shell_command, each through both schema functions and buildCursorToolDefinitions | Throw the explicit reserved-shell-name error; include both names, not one representative. | +| Namespaced freeform exec_command under mcp__custom | Accepted required-input schema; never interpreted as bare shell bridge. | +| Bare ordinary function exec_command with cmd-only parameters | Advertised Cursor exec schema; normalization retains original cmd-only schema. | +| shell_command declared with command, receive cmd plus sandbox_permissions=require_escalated, justification, prefix_rule, login=false | Only cmd rewrites to command; all four values survive exactly, especially false. | +| exec_command declared cmd-only, same fields | cmd remains cmd, other values survive; no added command key. | +| Existing canonical command and an alias simultaneously | Existing normalizeArgKeys canonical precedence remains covered by adjacent tests. | + +Use literal expected contracts and decoded protobuf values, not only equality against the newly added constant (both could be wrong together). Strengthen the ported freeform test with literal `{type:"object", properties:{input:{type:"string"}}, required:["input"], additionalProperties:false}`. Verify both ordinary shell directions already at current test lines 131 and 159. Existing code-mode/structured-edit tests later in the file protect unchanged routing and tool-choice behavior. + +Optional additional hosted mutation experiment (not a completion prerequisite): with final tests and baseline schema code, observe missing-property/freeform assertions fail; restore final schema code and obtain green. Separately remove only the reserved-name guard in an isolated runner checkout to prove both rejection tests fail, then restore and rerun. Do not claim RED before these logs exist. + +Runner-only commands: + +```sh +bun test tests/providers/cursor/cursor-tool-definitions.test.ts +bun test tests/providers/cursor +``` + +Follow with existing hosted typecheck, full-suite, privacy scan and docs build. Capture exact head and actual executed jobs; label/hygiene green or action_required does not establish validation. Preserve no-local policy even on failure; inspect CI artifacts and repair the specific defect. Main uses authorized --no-verify pushes to avoid local prepush, not server policy. No workflow edits are planned. + +## User and architecture documentation patch + +The source body's claim that no user documentation is needed is not adopted: the advertised tool contract changes and the root instructions require documentation sync. + +Append this bullet inside `## cursor` (`docs-site/src/content/docs/reference/adapters.md:304`), before `## azure-openai`: + +```md +- Codex-compatible shell schemas retain sandbox permissions, justification, reusable + prefix rules and login mode. Freeform tools expose one required string `input`; + bare `exec_command` and `shell_command` names are reserved for non-freeform shell + bridges. Namespace a custom freeform tool that uses either name. These schema + declarations do not grant approval or change execution policy. +``` + +Append this block to the existing transport SOT, preserving 020 and peer additions: + +```md +## Cursor executable tool schema ownership + +`src/adapters/cursor/tool-schemas.ts` owns advertised and argument-normalization +schemas; `tool-definitions.ts` remains the public facade and protobuf encoder. +Advertisement and normalization intentionally differ for shell bridges: Cursor may +emit `cmd`, while the declared Responses contract decides whether it becomes +`command`. Both paths preserve execution-control fields. Freeform tools use one +required string `input`; bare shell bridge names are rejected on the freeform path. +Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in +`tests/providers/cursor/cursor-tool-definitions.test.ts`. +``` + +Inspect directly affected translated adapter sections at P; add exact locale paths to this map if they contradict the English contract. No locale edit is justified solely by adding optional detail. Docs build remains CI-only. + +## Integration handoff + +030 follows 020 in the requested D stack; their runtime paths are independent, but the adapter reference and SOT are shared. Cascade stack updates after lower-layer changes, preserve each layer's review delta and attribution, and do not overwrite new split-owner behavior while resolving source conflicts. Main refreshes reviews and exact-head CI, merges bottom-up, verifies the landed commit is an ancestor of fetched dev, then promptly closes superseded #3628. Do not close on carry creation or CI success alone. This planning task writes no production code and performs no Git/GitHub mutations. + +## Roadmap lock clarification + +The implementation cycle certifies its published current-head candidate. Every dev-ancestry and original-closeout obligation remains mandatory in the separate landing work-phase, allowing the owner-requested stack to exist without treating publication as dev integration. Eligible lower layers may land early and are closed immediately after ancestry proof. + +## External review amendment: closed freeform object + +The advertised freeform schema must include additionalProperties:false, matching the existing custom-tool compatibility envelope. Include this literal property in schema and protobuf assertions; preserve ordinary named function schemas and reserved-name guards. diff --git a/devlog/_fin/260906_d_integrations_delivery/031_cursor_input_guidance.md b/devlog/_fin/260906_d_integrations_delivery/031_cursor_input_guidance.md new file mode 100644 index 0000000000..6f206a0f57 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/031_cursor_input_guidance.md @@ -0,0 +1,103 @@ +# Cursor freeform input guidance — follow-up repair plan + +Status: dedicated repair P cycle; source still248177c9. Prior Logs D completed its candidate verification; shipping obligations remain separate. +Inspected current checkout HEAD: `248177c9eccc639557c1770c59384dd6a6e27934` (2026-09-06 KST), after the original Cursor delivery and with ongoing Logs work. This is newer than the task's shorthand `6005+Logs`; all line anchors below describe the inspected source. Recheck them at the repair cycle's P after Logs C. + +## Review disposition and delivery boundary + +Accept the metadata-loss finding in `PRRT_kwDOS-0Gi86fmCS9` as a source-confirmed P2 regression. GitHub's read-only GraphQL response shows the thread unresolved on `src/adapters/cursor/tool-schemas.ts:115`, reviewing `6005ea8017dc7d113bba0d8dcef061d4f677c60f`: +https://github.com/lidge-jun/opencodex/pull/3707#discussion_r3941721795 + +The actual loss is confirmed by inspecting parser and schema code, not by running a test or making a live Cursor request. The claim that losing guidance can increase rejected model calls is plausible, but its runtime frequency was not measured here. + +Original #3628 was integrated by `6dd23d6314c41f1113639e042353aae9e6614e62`, also recorded in `.tmp/d-delivery/cursor-admin-audit.json`. Main should implement this as a new follow-up PR after Logs C. Do not amend, reset, rebase, or replace landed commits. Preserve SB Yoon's existing authorship and cite #3707/#3628 as provenance; do not attribute this later repair to an unperformed original-author change. + +## Actual source path and loss point + +1. `src/responses/parser-tools.ts:41` exports `buildTools`; `pushCustom` at line 67 handles custom/freeform tools. Lines 82–84 select a tool-scoped input description: apply_patch gets exact Begin Patch envelope guidance, other custom tools get generic freeform guidance. Line 88 stores it in `parameters.properties.input.description`, and line 89 marks `freeform: true`. Namespace children route through this same function (lines 109–111), so the metadata also exists for namespaced custom tools. Reserved `functions` groups flatten to bare tools. +2. `src/responses/parser.ts:465` and `:466` call buildTools for declared and discovered tool specs. This is production ingress, not a test-only construction. +3. `src/adapters/cursor/request-builder.ts:476` createCursorRequest filters the request-visible tools then applies the existing byte/count budget at `:483`; returned tools enter the Cursor request at `:497`. `applyCursorToolBudget` (`:79`) copies/filter-selects tool objects and measures actual definitions via cursorMcpToolsEncodedSize. It does not strip the nested description. +4. `src/adapters/cursor/tool-schemas.ts:110` cursorToolInputSchema rejects reserved bare freeform shell names, but line 115 then unconditionally returns CURSOR_FREEFORM_INPUT_SCHEMA. The constant at `:37` has input.type=string, required input, and additionalProperties=false, but no input.description. This is the loss point. The normalization branch at `:125`–`:130` repeats the same replacement. +5. `src/adapters/cursor/tool-definitions.ts:80` buildCursorToolDefinitions copies only tool.description to the top-level protobuf description (`:91`), and encodes cursorToolInputSchema(tool) into inputSchema at `:92`. A top-level description of “Apply a patch” cannot replace the parser-generated nested envelope guidance. +6. Both outgoing callers use the same definitions: `src/adapters/cursor/live-transport.ts:645` stores them in execContext; `src/adapters/cursor/protobuf-request.ts:1594` constructs them for the Run request and `:1699` includes them in McpTools. The latter also decodes inputSchema for model-visible text measurement (`:1333`, consumed at `:1714`). Fixing the schema owner therefore updates both advertisement sites and their byte accounting. +7. `live-transport.ts:672` independently stores cursorToolArgNormalizeSchema(tool) in its normalization map. `src/adapters/cursor/arg-normalize.ts:69` normalizes property names; descriptions do not alter key normalization. Preserve the description in both schema selectors for a consistent per-tool schema, without changing normalization rules. + +Falsification checks: `tests/responses/responses-parser.test.ts:102` already proves the parser emits apply_patch guidance, but stops before Cursor encoding. Current Cursor tests (`tests/providers/cursor/cursor-tool-definitions.test.ts:147`, `:188`) use empty parameters or missing metadata and expect the generic schema, so they cannot catch this loss. `tool-guidance.ts:187` contains code-mode/nested-helper prose and structured-edit tools provide another editing path, but neither preserves the discarded per-tool input metadata. This finding is metadata loss, not a claim that every Cursor editing path lacks all patch guidance. + +## Minimal implementation scope + +Class C2 bounded adapter repair, one separate implementation PABCD cycle owned by main. No new dependencies, parser changes, transport changes, tool execution, approval policy changes, facade exports, or generated protobuf updates. + +| Operation | Path | Planned change | +|---|---|---| +| MODIFY | `src/adapters/cursor/tool-schemas.ts` | Add a private schema builder that copies only a string-valued input.description onto the canonical closed freeform envelope; use it after the existing reserved-name guard in both schema selectors. | +| MODIFY | `tests/providers/cursor/cursor-tool-definitions.test.ts` | Add actual buildTools-to-protobuf regressions and schema/isolation controls; retain every existing shell, reserved-name, login=false and closed-schema test. | +| MODIFY | `structure/04_transports-and-sidecars.md` | In the existing Cursor executable schema ownership section, state that tool-specific input descriptions survive canonicalization while structure remains closed. Main owns this doc during planning. | +| MODIFY | `docs-site/src/content/docs/reference/adapters.md` | Amend the existing Cursor freeform bullet to state that tool-specific input guidance is retained; no duplicate section or unrelated locales. | +| NEW | none | Reuse the current test file and registration; no new test-layout entries. | + +Necessity/owner search: searched buildTools, CURSOR_FREEFORM_INPUT_SCHEMA, input.description, cursorToolInputSchema, cursorToolArgNormalizeSchema and modelVisibleToolText. A configuration change cannot recover metadata that the adapter unconditionally drops. Reuse the existing schema constant, schema module, parser, and protobuf encoder; do not copy apply_patch prose into Cursor code or import the Responses parser's object guard into the runtime adapter leaf merely for this repair. + +### Proposed source patch + +Insert this private helper immediately after the freeform constant (name is new; no equivalent per-tool builder exists in the inspected schema module): + +```ts +function cursorFreeformInputSchema(tool: OcxTool): unknown { + const properties = tool.parameters?.properties; + const input = properties && typeof properties === "object" && !Array.isArray(properties) + ? (properties as Record).input + : undefined; + const description = input && typeof input === "object" && !Array.isArray(input) + ? (input as Record).description + : undefined; + if (typeof description !== "string") return CURSOR_FREEFORM_INPUT_SCHEMA; + return { + ...CURSOR_FREEFORM_INPUT_SCHEMA, + properties: { + input: { ...CURSOR_FREEFORM_INPUT_SCHEMA.properties.input, description }, + }, + }; +} +``` + +Replace exactly the two `return CURSOR_FREEFORM_INPUT_SCHEMA;` statements in cursorToolInputSchema/cursorToolArgNormalizeSchema with `return cursorFreeformInputSchema(tool);`. Do not replace the helper's fallback return. Keep both reserved-name guards before the call. + +Preserve empty descriptions as strings; do not trim or synthesize guidance. Copy no arbitrary input schema keys, sibling properties, required lists, additionalProperties flags, enums, or constraints from the input parameters. Never mutate the shared constant or tool.parameters. A freeform tool with no valid description still receives exactly the current closed canonical schema. Ordinary function/shell schemas remain untouched. + +## Regression cases and independent oracles + +Add an import of `buildTools` from `../../../src/responses/parser-tools` to the existing Cursor test file. Reuse existing fromBinary/toJson/ValueSchema and buildCursorToolDefinitions. Do not test only an invented OcxTool: the primary regression must run the real parser conversion. + +1. **Real apply_patch ingress → schema → encoded protobuf.** Feed `buildTools([{type:"custom", name:"apply_patch", description:"Apply a patch"}])`. Assert one freeform tool and a source input.description equal to this literal: + ``Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope.`` + Assert both schema selectors AND decoded `buildCursorToolDefinitions(tools)[0].inputSchema` equal an independently written object: type object; properties exactly `{input:{type:"string",description: }}`; required exactly `["input"]`; additionalProperties exactly false. Top-level protobuf description remains `Apply a patch`. This fails at the current schema replacement, even though the parser's own test passes. +2. **Generic and namespaced custom ingress.** Build custom exec plus one namespace-wrapped custom tool (e.g. mcp__custom/exec_command) through buildTools. Their input.description must equal the independent literal `Raw freeform input for this tool.`; protobuf names retain the established namespace convention and neither schema receives apply_patch text. The namespaced shell-like custom tool remains accepted, while bare freeform exec_command/shell_command still fail existing tests. +3. **Per-tool isolation/no shared mutation.** Build two freeform fixtures with different input descriptions, e.g. `guidance-A` and `guidance-B`, plus a third metadata-free tool. Invoke both selectors and encode all three in one batch. Each output must retain only its own literal description; the third and CURSOR_FREEFORM_INPUT_SCHEMA must remain equal to the original description-free closed literal. Freeze the supplied nested parameter objects or compare their before/after values so accidental mutation is detected. +4. **Metadata is copied, shape is not.** Supply a freeform tool whose parameters declare input.type=number, extra input constraints, sibling command, required command, and additionalProperties=true, but input.description=`guidance-A`. Expected schema is still exactly closed `{input:string}` with only guidance-A retained. This prevents “fixing” the regression by returning/spreading arbitrary tool.parameters. Parameters are a Record; this is reachable from direct integration callers and guards against widening their freeform contract. +5. **Absent or ill-typed description fallback.** Keep all current `{}`/missing-input positive cases. Add representative numeric/null description and non-object properties/input cases; expect the same description-free closed literal, no throw and no metadata bleed. Use labeled tuple wrappers if an array-valued case is included. An empty string description should be copied, not replaced. +6. **Existing behavior controls.** Preserve all original executable shell field assertions, both cmd/command normalization directions with login=false, reserved bare names, namespaced acceptance, code-mode and structured-edit tests. Do not refresh existing expected generic schemas into values derived from the implementation constant. + +The main test is a runtime-source-to-wire contract comparison, not a test for prose in a markdown file. Full-object literal assertions catch closure/type/extra-property drift, while source-to-wire assertions prove the actual description transport path. Do not add a parallel prose owner in production. + +## Verification plan (remote/CI only) + +No tests, typecheck, builds, commits, or GitHub writes were performed for this investigation. Read-only source inspection and a read-only GraphQL review fetch are the evidence so far. + +After Logs C, main should capture a new pinned head and run remotely: + +```sh +bun test tests/providers/cursor/cursor-tool-definitions.test.ts tests/responses/responses-parser.test.ts tests/providers/cursor/cursor-request-builder.test.ts +``` + +Then required pinned-head typecheck/full-suite/privacy/docs build and hosted CI under the existing workflow. Preserve evidence that the new primary parser-to-protobuf regression fails on the old schema owner and passes after the repair if main performs the isolated remote RED/GREEN check; do not claim that proof before it exists. + +Byte accounting risk: retaining descriptions increases encoded catalog size, so a catalog already near the cap may omit a lower-priority tool. Both budget and wire use buildCursorToolDefinitions and the same schema helper; do not bypass the cap to conceal this correction. Existing `cursor-request-builder.test.ts` budget controls around lines 537–612 must remain green. No new budget mechanism is needed. + +Re-request independent review for blocker closure on the follow-up head. Main owns PR publication, exact-head CI, final review, merge proof and resolving the original late review with a link to the landed follow-up. This scratch plan does not certify the old merge gates or authorize rewriting landed history. + +## Repair cycle binding + +Loop archetype: bounded adapter regression repair. Trigger: review thread PRRT_kwDOS-0Gi86fmCS9. Goal: retain parser-owned per-tool input descriptions in both Cursor schema consumers without widening the closed freeform contract. Non-goals: parser semantics, shell execution, transport or approval changes. Verifier: pinned remote regression tests/typecheck/full suite and hosted CI; no local application checks. Stop: reviewed correction published and proven on dev, then original review resolved with its commit. Memory: this031 record plus ignored execution receipts. Outcomes: DONE only with evidence; a failed or unresolved gate remains open. Escalation: main reclaims the packet after two distinct worker failures; all added write scopes require a P amendment. + +Main owns FSM, docs, GitHub and remote orchestration. Inherited Godel worker owns only tool-schemas.ts and cursor-tool-definitions.test.ts. Independent Nash audits this plan and a separate reviewer verifies implementation. No local test/typecheck; shared remote test lock remains respected. The repair is a child of open Logs3712, then retargets dev when its parent lands. Every original authored commit remains intact. Remotealias now depends on this correction; final landing still verifies all D changes. diff --git a/devlog/_fin/260906_d_integrations_delivery/031_cursor_refresh.md b/devlog/_fin/260906_d_integrations_delivery/031_cursor_refresh.md new file mode 100644 index 0000000000..599084c6e0 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/031_cursor_refresh.md @@ -0,0 +1,9 @@ +# Cursor schema cycle P refresh + +Parent: d6bfb044a5dc6494cba57c1238ded7c23faf5586, open PR #3702. Original #3628 remains at 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56, author SB Yoon (yansigit). + +The source commits 1b29236c5bee9dd166b9d23983a2f1f1c2f0b793 and 37e6115c8a2ad3ffe20fee1e5a1e79a054625a56 are prepared as mailbox patches with only production diff paths mapped from tool-definitions.ts to current tool-schemas.ts. `git apply --check` accepted the first mapped patch. Apply both in order during B, retaining their original author/date/message. Main then adds the new constant to the existing public re-export, closes the freeform object with additionalProperties:false, strengthens literal/protobuf assertions and updates the planned docs/structure. + +The current naming path preserves namespaces through namespacedToolName; the existing bare-shell helper remains the authority for the original rejection. No tool execution or approval policy changes are introduced. Read current 030 for all activation cases and complete scope. + +Main owns authored patch application, public facade and documentation edits, commits and PR publication. An inherited worker may amend only tool-schemas.ts and cursor-tool-definitions.test.ts after A; no Git or local tests/typecheck. Independent review plus current-head remote full/typecheck/docs and hosted CI supply proof. The candidate can remain open in the stack while shipping/closure criteria remain separately pending. diff --git a/devlog/_fin/260906_d_integrations_delivery/040_logs_filters.md b/devlog/_fin/260906_d_integrations_delivery/040_logs_filters.md new file mode 100644 index 0000000000..574e864bd7 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/040_logs_filters.md @@ -0,0 +1,210 @@ +# 040 — Composable Logs filters (#3625) + +## Loop specification and scope + +- Class: C2 product slice, developer-console dashboard, global/i18n, existing dense visual language, feedback-only motion. This file is a docs-only deliverable in the main agent's roadmap P cycle; implementation is a later, separate PABCD work-phase. +- Archetype: spec-satisfaction repair and integration. Trigger: #3625 exposes the already-landed rich Logs predicate through usable controls. +- Goal: combine surface, intercepted-request, provider, exact model, time, speed, status and conversation filters over the loaded log ring, with clear result counts, reset, and accessible keyboard controls. +- Non-goals: new log API parameters, persistence/URL schema, retention/export, incremental polling #3250, request transport changes, provider/model configuration, new dependencies, redesign of the Logs table, unrelated locale cleanup. +- Verifier: current-head hosted CI covering the tests below, GUI lint/build/typecheck, privacy and repository gates; rendered screenshot/interaction evidence from an isolated same-head Vite preview, CI-built artifact or hosted preview. No local tests, suites, typecheck, or build that invokes typecheck. No verification was executed during this planning task. +- Stop: implemented behavior, all acceptance rows, docs sync, fresh screenshots, author credit, current-head CI and main-owned dev ancestry proof. An author comment or green intake check is not completion evidence. +- Outcomes: DONE only with those receipts; NOOP only if current dev independently contains equivalent behavior and evidence; BLOCKED for an unavailable CI/preview/required review; NEEDS_HUMAN for an unresolved external scope decision. Never mark an incomplete slice done. +- Memory artifact: this document plus main-owned 000 roadmap/evidence ledger. No goal or orchestration mutations by this document owner. +- Bounds: this delegate reads local source refs and the supplied metadata and writes only this document; zero paid provider requests, zero local test/build processes, zero Git/GitHub mutation. Implementation inherits main's resource bound and credentials; no separate cost allocation is invented here. +- Escalation: main reclaims a packet after two distinct failed workers. Further implementation delegation is a P amendment with inherited user model settings; no mid-B widening. + +## P stale check, provenance and exact source patch + +Planning tree: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c` (read 2026-09-06 KST). +Source ref: `origin/d-source-3625` = `4f79746b4cedffeb61700113977cd72adf25c51f`. +Source base: `be81013fab6d83ff630ca5f38e7881678a303871`. +Metadata: `.tmp/d-delivery/pr-3625.json`; recorded PR author `yansigit`, display name **SB Yoon**. The JSON head agrees with the source ref; its mergeable/readiness fields are a captured snapshot, not fresh merge authorization. Its body still cites `232e324...`; the later author comment cites `4f79746...`. Both test reports are contributor claims, not integration-head proof. + +The complete baseline implementation is the exact four-commit sequence below. Read/apply its patch at the later B, then apply the explicit amendments in this document. Do not restore whole historical files over current files. + +| Order | Source commit | Authored change | +| --- | --- | --- | +| 1 | `6602c5610c6d7d8a1179b05c9f86598c4acd8fee` | Initial composable controls, state wiring, locales, styles and tests | +| 2 | `e053045e9a2d49b8d70546223b0d02313d4031fe` | Exact identities, option invalidation, relative clock, keyboard navigation and review corrections | +| 3 | `232e324b45afa617ccabb97374137c9faf7654ae` | Turkish copy and assertion refinements | +| 4 | `4f79746b4cedffeb61700113977cd72adf25c51f` | Test-global cleanup in finally | + +All four commits identify `SB Yoon <44089734+yansigit@users.noreply.github.com>`. +Preserve authored commits where feasible. A carried/reimplemented or squash commit and its PR description must retain: + +```text +Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> +``` + +Read-only patch locator (not an instruction to execute tests or mutate Git): + +```sh +git diff be81013fab6d83ff630ca5f38e7881678a303871 4f79746b4cedffeb61700113977cd72adf25c51f -- gui +git log --format='%H %an <%ae> %s' be81013fab6d83ff630ca5f38e7881678a303871..4f79746b4cedffeb61700113977cd72adf25c51f +``` + +Current `Logs.tsx`, `logs-filter.ts`, `logs-auto-refresh.test.tsx`, and `logs-filter.test.ts` have no delta from that source base. All nine locale catalogs and `styles.css` do have intervening dev changes. Recheck these facts at implementation P and rebase the patch semantically against the actual stacked parent. Do not import source-base package versions, lockfiles, locale-wide rewrites or old CSS. + +## Exact implementation file map + +The immutable range above is the exact before/after source patch for all 17 carried files. “MODIFY” below means apply that path's hunks to the current parent, preserving unrelated edits; “NEW” means take the source blob and the test amendments specified below. + +| Path | Operation | Before → after and immutable source locator | +| --- | --- | --- | +| `gui/src/pages/Logs.tsx` | MODIFY | At current lines 376–380 replace five independent states with `filters: LogFilterState` and `filterClockNow`; import `useMemo`, bar and existing engine. Replace line 475's fresh empty array with module-level `EMPTY_LOGS`. Replace lines 502–521 with the source clock/hash/options/predicate block. Replace lines 600–659 toolbar with `LogsFilterBar`; distinguish filtered empty state at line 717; detail conversation action updates the shared state. Exact diff: the range above, this path. | +| `gui/src/pages/logs-filter-bar.tsx` | NEW | Source-head lines 1–127: controlled `LogsFilterBar`, no global store. Native labeled selects, intercepted checkbox, conversation input, active count/reset. Surface radios have roving tabIndex and keydown helper. Speed values map to `[−∞,15)`, `[15,50)`, `[50,+∞)` bounds. | +| `gui/src/pages/logs-surface-keydown.ts` | NEW | Source-head lines 1–24: ordered all/claude/codex/grok; wrapping ArrowLeft/Right/Up/Down, Home/End, preventDefault only for handled keys, select then focus matching radio id. | +| `gui/src/pages/logs-filter.ts` | MODIFY | Current lines 119–126: `value?.includes(modelQuery)` → `value === modelQuery` for requested, resolved and attempted model identities. Keep whitespace/case normalization. Standalone `logs-model-filter.ts` retains substring semantics. | +| `gui/src/styles.css` | MODIFY | Add source two selectors after current `.logs-toolbar` at 2145, then the bounded responsive amendment below. Keep table widths/clipping and all Models rules. | +| `gui/src/i18n/en.ts` | MODIFY | Add source's 20 keys after current `logs.filter.surface.label` at 700; English owns `TKey`. | +| `gui/src/i18n/de.ts` | MODIFY | Same 20 source locale keys after current line 667. | +| `gui/src/i18n/fr.ts` | MODIFY | Same keys after line 681; preserve final number-neutral `Affichage de {count} sur {total}`. | +| `gui/src/i18n/ja.ts` | MODIFY | Same keys after line 643. | +| `gui/src/i18n/ko.ts` | MODIFY | Same keys after line 686; source copy includes `필터 초기화`, `{total}개 중 {count}개 표시`. | +| `gui/src/i18n/ru.ts` | MODIFY | Same keys after line 684. | +| `gui/src/i18n/tr.ts` | MODIFY | Same keys after line 691, final `jeton/sn` speed wording; update `logs.metric.tokPerSecTitle` at 723 to `Tam istek süresince saniye başına çıktı jetonu`. | +| `gui/src/i18n/zh.ts` | MODIFY | Same keys after line 679 (GUI Simplified Chinese). | +| `gui/src/i18n/zh-TW.ts` | MODIFY | Same keys after line 536 (GUI Traditional Chinese). | +| `gui/tests/logs-filter.test.ts` | MODIFY | Source patch lines 49 onward replaces substring expectations with complete identities; adds partial/stale negative cases; preserve status/time/speed/malformed-attempt cases at current 80–143. | +| `gui/tests/logs-auto-refresh.test.tsx` | MODIFY | Source patch confines intercepted-row assertions at 542–570 to `.logs-table tbody`, since select options legitimately retain hidden model names. Add behavioral integration cases below using this file's existing renderer/cache/fake-clock harness. | +| `gui/tests/logs-filter-bar.test.ts` | NEW | Source-head 115-line file as baseline; replace its first three source-string “wiring” checks with observable controls/interaction coverage. Retain and expand the actual keyboard/reset tests, with cleanup on assertion failure. | + +New documentation changes beyond the source PR: `structure/05_gui-and-management-api.md`, and all eight existing `docs-site/src/content/docs/{,ko/,fr/,ja/,ru/,tr/,zh-cn/,zh-tw/}guides/web-dashboard.md` paths, specified below. No German dashboard page exists at this head; do not create an unrelated locale tree. No root test-layout manifest change is required for tests under `gui/tests/`; preserve the repository's `tests/` manifests unchanged. + +### Existing state and behavior to preserve + +- `gui/src/pages/Logs.tsx:463` owns the resource fetch, cache, 2-second poll and backoff. Filters consume this ring; they do not fetch a new dataset. Keep stale/cold/loading states at 482–499 and the table/details transport untouched. +- `Logs.tsx:526` virtualizes `filteredLogs`, rendering newest first by reverse indexing. Retain stable request keys, column schema and detail behavior. Do not sort the input merely for filter selection. +- `logs-filter.ts:95` remains the sole predicate. Model/provider option extraction at 162 includes failover attempts, normalized duplicate handling and stable code-point ordering. Options derive from the full loaded ring, not the filtered subset. +- All filters compose with AND at row level. A requested model and a provider appearing on another attempt can both match that same row; do not silently introduce same-attempt pairing. +- Clock refresh is 30 seconds only while `timeWindow !== 'all'` and tab is Logs. It is independent of auto-refresh, so paused network refresh does not freeze time-relative filtering. Cleanup on window changes, Debug tab and unmount. +- If the loaded ring loses the selected model/provider, clear only each vanished identity; leave status/time/conversation and still-present identities intact. No permanent state persistence is introduced. +- Conversation hashing retains cancellation against obsolete input; reset clears both input and hash. Preserve the existing opaque-id path, and verify delayed hash completion cannot restore a reset filter. + +### Responsive CSS amendment (bounded to this toolbar) + +The source's two rules alone do not address current `.logs-filter-field .input { min-width:220px; max-width:360px; }` at `styles.css:2174`, or the four 64px-minimum surface buttons. At narrow widths labels plus 220px controls can exceed the content area. Add these rules adjacent to the two source additions; never change global `.select-sm`, `.input` or `.btn`: + +```css +.logs-filter-container { min-width: 0; } +.logs-filter-container .logs-filter-field { min-width: 0; max-width: 100%; flex-wrap: wrap; } +.logs-filter-container .logs-filter-field .input { min-width: 0; max-width: 100%; } +.logs-filter-container .logs-filter-status { flex-wrap: wrap; max-width: 100%; } +.logs-filter-container .logs-segmented { max-width: 100%; flex-wrap: wrap; } +``` + +Keep source `.logs-filter-status` flex alignment/gap/margin-left and `.logs-toolbar-secondary` spacing. Controls may wrap by field and radios may wrap as a group; native select identity remains readable from its option menu. The table keeps its independent horizontal scroller and 1100px minimum width. Browser acceptance, not CSS-string presence, decides containment. If these narrowly scoped rules are insufficient in the measured screenshot, amend only this block and document the measured overflow before the repair. + +### Locale and B3659 ownership handshake + +D owns the 20 new Logs keys plus Turkish speed-title correction; B3659 owns its Models Hide/Delete/cleanup/sync keys. Exact D key set: + +```text +logs.filter.model.all +logs.filter.provider.label / .all +logs.filter.status.label / .all / .success / .errors +logs.filter.time.label / .all / .15m / .1h / .24h +logs.filter.speed.label / .all / .slow / .medium / .fast +logs.filter.reset +logs.filter.showingCount +logs.noMatchingRequests +``` + +The source range supplies the exact translated values for every key, including both `{count}` and `{total}` placeholders. Retain now-unused `logs.filter.model.placeholder` and conversation-clear keys; deleting them is unrelated churn. + +Before B starts, main exchanges the actual B3659 head and changed-key/selector inventory. No B3659 source ref was supplied to this delegate, so no fresh claim of hunk disjointness is made. B confirmed on this run that source #3659 changes nine locale files but no stylesheet, and its implementation has not started. Preserve both lanes by keys; recheck any later B style additions rather than assuming a current stylesheet overlap. B preserves `.logs-*`; D preserves `.models-*` and B's shared control fixes. Shared `.select-sm` or global token changes belong to main's integration review. Re-read the final union after either lower stack layer changes; CI must run against that union. Public dashboard docs can also overlap: D inserts the Logs subsection and preserves B's Models wording. + +## Behavioral regression amendments and acceptance + +Use existing `gui/tests/logs-auto-refresh.test.tsx:1–164`: Happy DOM, `mountLogs`, virtualizer layout stubs, isolated resource stores, mocked `/api/settings` and `/api/logs`, `act`, fake timers and explicit microtask settlement. All execution is CI-only. Do not add a new test runner or assert only source substrings. Expected row ids/counts must be hardcoded independently of `filterLogs`. + +| ID | Reachable activation | Required observable result / owner | +| --- | --- | --- | +| L01 defaults | Mock loaded ring containing Codex, Claude and Grok entries, no filters | All rows remain newest-first; no active count/reset; existing loading/error/detail tests still pass. `logs-auto-refresh.test.tsx`. | +| L02 composition | Rows differ independently in surface, provider, exact model, status and intercepted marker; select controls sequentially | Only the hand-selected intersection row remains; displayed count uses filtered length and unfiltered ring length. Model/provider options still include excluded rows. `logs-auto-refresh.test.tsx`. | +| L03 identities | Include `model-a`, `model-a-plus`, requested/resolved/fallback-only identities and case/space variants | `model-a` does not match `model-a-plus`; full fallback/resolved identity matches; normalized duplicates produce one stable option. Preserve standalone substring helper tests. `logs-filter.test.ts`. | +| L04 status/speed | Include 200, 299, 300, 400, 599 and malformed status; finite rates 14.99, 15, 49.99, 50 plus unavailable | Success only 2xx; errors only 4xx/5xx; slow <15, medium >=15 and <50, fast >=50; unavailable excluded only with speed bound. UI maps every speed option to these bounds. Engine file plus bar rendered events. | +| L05 time expiry | Fake now T; timestamp T−15m+1s; select 15m, disable auto-refresh, retain identical log snapshot | Row initially visible, disappears on first 30s clock tick; fetch count stays unchanged after pause. Repeat predicate boundaries for 1h and 24h with injected clock. No real sleep. `logs-auto-refresh.test.tsx` plus existing engine windows. | +| L06 clock lifecycle | Activate 15m then change 1h, switch Debug, return Logs, finally unmount | Track the 30,000ms interval handle via spies on window setInterval/clearInterval; old handle cleared on each deactivation; one live filter interval after reactivation; none after unmount. Do not count unrelated Happy DOM/virtualizer timers. | +| L07 ring rollover | Select model/provider from snapshot A; refresh with B lacking only selected model, then C lacking selected provider | Missing select resets to All; still-present selection and unrelated status/time/conversation remain. Labels never become blank while a hidden stale value excludes rows. | +| L08 reset/hash | Enter conversation, allow hash resolve; combine with status/time; click reset. Repeat with first hash resolution deferred until after reset | All controls default, full ring visible, count/reset disappear; late old hash does not resurrect filtering. Detail “filter conversation” action updates shared state and closes dialog. | +| L09 empty/error distinction | Cold empty API ring; separately populated ring excluded by status; separately cold failure and stale failed poll | Empty ring shows no-requests, filtered ring shows no-matching, cold failure retains error, stale failure keeps rows/banner. No empty-state flash during refresh. | +| L10 keyboard | Focus selected surface radio; ArrowRight from Grok, ArrowLeft from All, Up/Down, Home/End and unrelated key | Selection and focus wrap correctly; exactly one radio tab stop; unrelated key neither changes selection nor prevents default. Test actual rendered `aria-checked`/tabIndex plus helper; reset remains keyboard reachable. | +| L11 presentation | EN/KO/FR/DE, dark/light, widths 1440/768/390/320; long model and provider labels | Toolbar remains within page; count/reset wrap; focus visible; no clipped functional labels. Table scrolls inside its wrapper, not whole page. Browser receipts below. | + +In `logs-filter-bar.test.ts`, replace the source-oracle tests at source-head lines 10–36 with real rendered field/change assertions; move clock behavior proof to L05/L06. Expand reset fixture to several active fields rather than status only. Ensure every root unmounts in `finally` before restoring globals, including the existing rendered-reset test; restore property descriptors where practical. Keep the final source commit's keyboard-test `finally` fix. These changes strengthen observable oracles, not lower coverage to obtain green. + +## Documentation exact additions + +`docs-site/src/content/docs/guides/web-dashboard.md:87` retains the existing Logs overview row (it remains true); insert a new subsection immediately before `### Linking to a section` at line 92. Add the corresponding localized subsection immediately before the existing translated section-link heading in each of the seven translated guides. The following is the exact new English block: + +```md +### Filtering request logs + +Logs filters combine surface, intercepted requests, provider, exact model, status, time, +speed, and conversation ID over the currently loaded request ring. Provider and model +choices also include fallback attempts; model matching ignores case and surrounding spaces +but does not match partial names. Choices that disappear from the ring reset to All. + +Time windows cover the last 15 minutes, hour, or day and refresh every 30 seconds while the +Logs tab is active, even with auto-refresh off. Speed uses output tokens per second over the +full request duration: below 15, 15 to below 50, or at least 50. Unavailable speed values are +excluded when a speed filter is active. Success means 2xx; errors mean 4xx or 5xx. + +Active filters show the matching count out of the loaded total. Reset filters restores all +rows; “No matching requests” differs from an empty log ring. Use arrow keys or Home/End in +the surface selector. These controls do not query historical records beyond the loaded ring. +``` + +Translations must preserve every threshold, exact-identity rule and loaded-ring scope. Use the following exact localized summary blocks at the same insertion seam; they cover the same contract without rewriting the rest of each page: + +| Locale path segment | Heading and paragraph to insert | +| --- | --- | +| `ko/` | `### 요청 로그 필터` — `Logs에서 화면 종류, 가로챈 요청, 공급자, 정확한 모델명, 상태, 시간, 속도, 대화 ID를 함께 필터링합니다. 현재 불러온 로그만 대상이며 공급자·모델 선택지에는 폴백 시도도 포함됩니다. 모델명은 대소문자와 앞뒤 공백을 무시하지만 부분 이름은 일치하지 않습니다. 로그에서 사라진 선택지는 전체로 돌아갑니다. 시간 범위는 최근 15분·1시간·1일이며 Logs 탭에서는 자동 새로고침을 꺼도 30초마다 갱신됩니다. 속도는 전체 요청 시간 기준 초당 출력 토큰으로, 15 미만·15 이상 50 미만·50 이상입니다. 속도 필터를 켜면 측정값 없는 요청은 제외됩니다. 성공은 2xx, 오류는 4xx·5xx입니다. 일치 건수와 불러온 전체 건수를 표시하며 필터 초기화로 모든 행을 복원합니다. 일치하는 요청이 없는 상태와 빈 로그는 구분합니다. 화면 종류 선택은 방향키와 Home/End로 조작할 수 있습니다. 불러온 범위 밖의 과거 로그는 조회하지 않습니다.` | +| `fr/` | `### Filtrer les requêtes` — `Les filtres combinent interface, requêtes interceptées, fournisseur, modèle exact, statut, période, vitesse et identifiant de conversation dans le journal chargé. Les choix incluent les tentatives de repli ; les modèles ignorent la casse et les espaces externes, sans correspondance partielle. Un choix disparu revient à Tous. Les périodes de 15 minutes, une heure et un jour évoluent toutes les 30 secondes dans l’onglet Logs, même sans actualisation automatique. La vitesse mesure les jetons de sortie par seconde sur toute la durée : moins de 15, de 15 à moins de 50, ou au moins 50 ; les valeurs indisponibles sont exclues quand ce filtre est actif. Réussite : 2xx ; erreur : 4xx/5xx. Le compteur compare les résultats au total chargé ; la réinitialisation restaure toutes les lignes. Aucun résultat diffère d’un journal vide. Flèches et Home/End pilotent le sélecteur d’interface. Aucun historique au-delà du journal chargé n’est interrogé.` | +| `ja/` | `### リクエストログの絞り込み` — `Logsではサーフェス、インターセプトされたリクエスト、プロバイダー、完全なモデル名、ステータス、時間、速度、会話IDを組み合わせて、読み込み済みログを絞り込みます。選択肢にはフォールバック試行も含まれます。モデル名は大文字小文字と前後の空白を無視しますが、部分一致ではありません。ログから消えた選択肢は全件に戻ります。時間は直近15分・1時間・1日で、Logsタブでは自動更新をオフにしても30秒ごとに更新します。速度はリクエスト全体の時間あたりの毎秒出力トークン数で、15未満、15以上50未満、50以上です。速度フィルター中は測定不能な行を除外します。成功は2xx、エラーは4xx/5xxです。一致件数と読み込み総数を表示し、リセットで全行を復元します。一致なしと空ログを区別します。サーフェスは矢印キーとHome/Endで操作できます。読み込み範囲外の履歴は検索しません。` | +| `ru/` | `### Фильтрация запросов` — `Фильтры объединяют источник, перехваченные запросы, провайдера, точную модель, статус, время, скорость и ID диалога в загруженном журнале. Варианты включают резервные попытки; модель сравнивается без учёта регистра и крайних пробелов, но не по подстроке. Исчезнувший вариант сбрасывается на все записи. Периоды 15 минут, час и сутки обновляются каждые 30 секунд на вкладке Logs даже при выключенном автообновлении. Скорость — выходные токены в секунду за полную длительность запроса: меньше 15, от 15 до менее 50, не менее 50; недоступные значения исключаются при активном фильтре скорости. Успех — 2xx, ошибки — 4xx/5xx. Счётчик показывает совпадения из загруженного общего числа; сброс возвращает все строки. Нет совпадений и пустой журнал различаются. Источник выбирается стрелками и Home/End. История вне загруженного журнала не запрашивается.` | +| `tr/` | `### İstek günlüklerini filtreleme` — `Filtreler yüklü günlükte yüzey, yakalanan istekler, sağlayıcı, tam model adı, durum, zaman, hız ve konuşma kimliğini birleştirir. Seçenekler yedek denemeleri de içerir; model eşleşmesi büyük/küçük harfi ve dış boşlukları yok sayar, kısmi adları eşleştirmez. Kaybolan seçenek tüm kayıtlara döner. Son 15 dakika, saat ve gün pencereleri Logs sekmesinde otomatik yenileme kapalıyken de 30 saniyede bir güncellenir. Hız, tam istek süresindeki saniyelik çıktı jetonudur: 15 altı, 15 dahil 50 altı, en az 50; hız filtresi açıkken ölçülemeyenler dışlanır. Başarı 2xx, hata 4xx/5xx anlamındadır. Sayaç eşleşen ve yüklü toplam sayıları gösterir; sıfırlama tüm satırları geri getirir. Eşleşme olmaması boş günlükten ayrılır. Yüzey seçimi oklar ve Home/End ile çalışır. Yüklü günlüğün dışındaki geçmiş sorgulanmaz.` | +| `zh-cn/` | `### 筛选请求日志` — `Logs 可组合界面、被拦截请求、提供商、完整模型名、状态、时间、速度和会话 ID,筛选当前已加载的日志。选项包含回退尝试;模型匹配忽略大小写及首尾空格,但不做部分匹配。日志中消失的选项恢复为全部。时间范围为最近 15 分钟、1 小时或 1 天;Logs 标签页每 30 秒更新一次,即使关闭自动刷新也会更新。速度按完整请求耗时计算每秒输出 token,分为小于 15、15 至小于 50、至少 50;启用速度筛选时排除无测量值的请求。成功为 2xx,错误为 4xx/5xx。显示匹配数与已加载总数;重置恢复全部行,并区分无匹配与空日志。界面选择支持方向键及 Home/End,不查询已加载范围之外的历史记录。` | +| `zh-tw/` | `### 篩選請求日誌` — `Logs 可組合介面、被攔截請求、供應商、完整模型名稱、狀態、時間、速度和對話 ID,篩選目前已載入的日誌。選項包含回退嘗試;模型比對忽略大小寫及頭尾空白,但不做部分比對。日誌中消失的選項恢復為全部。時間範圍為最近 15 分鐘、1 小時或 1 天;Logs 分頁每 30 秒更新一次,即使關閉自動重新整理也會更新。速度按完整請求耗時計算每秒輸出 token,分為小於 15、15 至小於 50、至少 50;啟用速度篩選時排除無測量值的請求。成功為 2xx,錯誤為 4xx/5xx。顯示符合數與已載入總數;重設恢復全部列,並區分無符合結果與空日誌。介面選擇支援方向鍵及 Home/End,不查詢已載入範圍以外的歷史記錄。` | + +At `structure/05_gui-and-management-api.md:130`, in the Logs & Debug row replace only `Logs tab: request/runtime logs for local diagnosis.` with: + +```text +Logs tab: request/runtime logs for local diagnosis. `LogsFilterBar` owns controls over the shared `LogFilterState`; `filterLogs` composes filters over the loaded ring without changing the log API. Provider/model options include attempts, model choices match normalized complete identities, and relative-time filtering refreshes every 30 seconds while the Logs tab is active, independently of network auto-refresh. +``` + +Keep Debug/API/auth sections and Models content intact. The user guide is the behavior source of truth; the structure row records ownership rather than duplicating every label. + +## CI-only verification and screenshot receipt + +Commands below are a handoff to hosted CI, not permission to run locally. This roadmap task runs none of them. `gui build` runs `tsc -b`, so it is also prohibited locally. + +1. Current `.github/workflows/ci.yml:416–446` runs GUI lint, root typecheck, `cd gui && bun test --isolate tests`, privacy scan and GUI build. Require those steps to execute on the implementation head, plus required repository/platform jobs; an aggregate success with skipped tests is insufficient. +2. Focused CI receipts identify `gui/tests/logs-filter.test.ts`, `gui/tests/logs-filter-bar.test.ts`, `gui/tests/logs-auto-refresh.test.tsx`, `gui/tests/logs-model-filter.test.ts`, `gui/tests/logs-surface-filter.test.ts`, `gui/tests/logs-tab-keydown.test.ts`, and `gui/tests/logs-table-overflow.test.ts`. Full GUI test execution may supply these receipts; avoid redundant unchanged reruns. +3. Verify visible-copy checking in hosted CI: `cd gui && bun run lint:i18n`. This exact script is not a separate step in the inspected CI workflow; main must prove equivalent lint coverage or arrange a hosted run. Do not claim it ran merely because general CI is green. +4. Docs validation is `cd docs-site && bun install --frozen-lockfile && bun run build` on CI, per docs-site instructions. Confirm actual job/step coverage rather than assume a workflow filename. If absent, main arranges a narrowly scoped hosted verifier before readiness. +5. Screenshots come from the final candidate in an isolated Vite dev preview (bundling only; no typecheck/test command), a CI-built artifact, or a hosted preview. Do not use the existing live port 10100 to claim this patch works; that service is not this candidate. Use native in-app browser inspect → act → inspect; do not install Playwright or run any local suite. +6. Use synthetic request metadata in the isolated preview: distinct surface/provider/model/status/rate combinations and opaque conversation ids, no real accounts, secrets or request bodies. Capture 1440×1000 EN/light and KO/dark with combined filters and count/reset, 768×1024 DE, and 390×844 plus 320×800 FR/KO. Include one no-matches state, one empty-ring state and keyboard-focused radio/reset state. The 320px capture must show toolbar containment separately from intended table scrolling. +7. Main stores screenshots under its ignored evidence directory (suggested `.tmp/d-delivery/screenshots/3625/`), with a manifest naming head SHA, preview URL, viewport, locale/theme, scenario, observed result and file. Inspect each actual image. Publish a durable screenshot URL in the integration PR description; the old source PR screenshot is reference only, not final-head proof. +8. For L05/L06 use deterministic CI test output for elapsed-time proof, not a screenshot or timed sleep. For browser flows record console/network state and check filter changes add no new request parameters or extra fetches beyond existing polling. + +## Stack, attribution and closure handoff + +Main owns branch creation, cherry-pick/reimplementation, commit, `--no-verify` push, PR template, review, merge and closure. This delegate performs none. Place this logical slice after the preceding D stack layer chosen in 000; its GUI/source changes have no semantic dependency on the earlier Cursor/tool-call work, but inherit that parent for stack topology. Re-read the actual parent at P, and cascade lower-layer updates before pushing upper layers. + +Merge bottom-up. Before landing, require current-head CI, screenshot URL, contributor trailer and applicable maintainer review. Retarget children before deleting a parent branch. After merge, main fetches dev and proves the integration merge commit is its ancestor. Close original #3625 immediately after that proof if a carry PR superseded it, linking the landing. No issue is linked in the supplied source PR metadata; do not close another D issue or #3659 as a side effect. + +## Open gates and document verification + +- No blocker to writing this roadmap. Implementation remains pending. +- B3659's actual current changed-key/selector inventory must be checked by main before merging shared files; no coordination message was sent by this delegate. +- Source tests lack behavioral clock/rollover coverage; L05–L08 are required amendments, not verified results. +- Source PR has no docs-site delta; the explicit documentation additions above are required. +- Current integrated CI, hosted preview/screenshots, independent review and final dev ancestry are not yet available from this document's read-only snapshot. +- Author's green local reports and cached readiness state do not satisfy these gates. +- Verification for this docs-only deliverable: inspected the source range, current consumers/styles/test harness and supplied PR JSON; read back this document and checked only its own diff/paths. No production code, peer document, Git state, GitHub state, tests, typecheck, goal or orchestration was changed. + +## Roadmap lock clarification + +The implementation cycle certifies its current-head published candidate. Final dev-ancestry and original-closeout requirements remain in the separate landing work-phase; lower layers may land early after all gates pass. An isolated Vite preview is permitted for UI evidence; local test/typecheck commands remain forbidden. diff --git a/devlog/_fin/260906_d_integrations_delivery/041_logs_refresh.md b/devlog/_fin/260906_d_integrations_delivery/041_logs_refresh.md new file mode 100644 index 0000000000..6848309c2c --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/041_logs_refresh.md @@ -0,0 +1,29 @@ +# Logs cycle P refresh + +Current stack parent: Cursor #3707 at6005ea8017dc7d113bba0d8dcef061d4f677c60f, including the parent index repair and current dev. Original #3625 remains4f79746b4cedffeb61700113977cd72adf25c51f; its four SB Yoon-authored mailbox patches are retained in scratch. The first patch dry-run applies on this tree. Apply all four during B, preserving every authored commit, then add only the 040 amendments. + +B coordination confirms3659 implementation has not started; its original scope overlaps nine locales, not styles.css. D owns logs.* keys and scoped.logs-* rules; B will carry both sets when its work starts. No whole-file overwrite of locale dictionaries. + +## Design read and ownership + +Existing dense diagnostic dashboard, existing colors/fonts/native selects and table. Primary workflow is immediate local filtering of already-loaded rows; reset restores defaults. Distinguish an empty ring, no matches, cold network failure, and stale refresh error. No URL-persistence feature, wizard, new icon library or redesign is introduced. Those are outside the adopted original contract. + +Main applies original commits and owns Git/CI/stack, QA fixture and screenshot capture. After A, an inherited implementation worker owns only gui/src/pages/{Logs.tsx,logs-filter-bar.tsx,logs-filter.ts,logs-surface-keydown.ts}, gui/src/styles.css and the four existing/new Logs test files named in040. A separate document worker owns eight web-dashboard guides and structure05. Main preserves original locale commits and resolves any local-key conflicts. + +## Browser verification construction + +Use an ignored .tmp fixture that mounts the real Logs component with the real LanguageProvider and stylesheet. Logs accepts apiBase and consumes only settings/logs for the Logs tab, so a Vite middleware serves canonical synthetic LogEntry arrays and settings under an isolated same-origin /__qa/ path. No real proxy/account data or port10100 is used. Vite performs bundling only; no local test/typecheck/build script is executed. The fixture selects locale/theme and dataset from its own query parameters through normal React/DOM initialization. Browser interaction uses the native in-app browser tooling and actual controls; do not inspect private browser stores. + +Capture component behavior with the source branch at its final UI commit: composition/reset/exact model, no matches/empty, keyboard radio navigation, desktop/mobile containment. Use stable synthetic timestamps away from range boundaries; deterministic timer/rollover behavior remains remote-test evidence. Publish sanitized screenshots under the existing docs-site/public/screenshots convention with immutable commit URLs in the PR. Record head/URL/viewport/locale/theme/scenario for each actual image. + +Remote validation uses project Bun1.4 and explicit Node22.22 in a private macmini checkout: complete suite/typecheck, GUI lint/i18n/build, docs build. Hosted CI remains recorded separately and required at final integration. No local application suites/typecheck. + +## Audit amendment: production layout constraints + +The preview must use the actual stylesheet's `.app` → `.main` → `.main-inner` structure, not mount Logs at full viewport width. Include a `.sidebar` rail occupying the production232px desktop grid column, and the production `.mobile-topbar`/off-canvas sidebar arrangement at the existing breakpoint. The base main-inner max-width980px is overridden to1200px by `.main-inner:has(.logs-page)`; preserve that actual cascade and32px/36px/64px desktop padding, plus22px/18px/48px mobile padding, remain untouched. Render an inert representative navigation rail using existing classes; only Logs functionality is under test. No fixture CSS may widen the main container or shrink these paddings. + +Before each containment capture, inspect rendered `.app`, `.main`, `.main-inner`, toolbar and table-wrapper rectangles at the requested viewport. The toolbar must fit the actual content box; the table's deliberate horizontal scroller is checked separately. This folds audit blocker1 and prevents falsely passing a wide standalone preview. + +## Tooling preparation + +Local GUI dependencies may be installed exactly from the committed lockfile with lifecycle scripts disabled, solely to run the Vite preview. This runs no local application test/typecheck/build script. All suite, lint, typecheck and production-build gates remain remote. diff --git a/devlog/_fin/260906_d_integrations_delivery/042_logs_review_corrections.md b/devlog/_fin/260906_d_integrations_delivery/042_logs_review_corrections.md new file mode 100644 index 0000000000..7568eed82e --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/042_logs_review_corrections.md @@ -0,0 +1,33 @@ +# Logs review corrections: focus and proxy-relative windows + +## Loop specification + +Archetype: bounded correctness repair (C3). Trigger: #3712 review threads PRRT_kwDOS-0Gi86fmmdd and PRRT_kwDOS-0Gi86fmpGe. Goal: keyboard Reset returns focus to a stable selected surface control; relative windows use the proxy clock despite browser wall-clock skew. Non-goals: changing log history, server-side filtering, auth, URLs, or UI layout. Verifier: rendered GUI cases, real management envelope test, pinned remote typecheck/root/GUI checks plus browser activation. Stop: candidate verified; final logs-proof/c-2 still require dev integration and hosted CI. Memory: this042 and ignored receipts. Failed gates stay open. Main reclaims a delegated packet after two distinct worker failures; new scopes require a P amendment. + +Prior D accepted the Cursor guidance candidate92f848e8, with final c-4 shipping still open. These parent Logs findings stopped the admin merge before any mutation. Build the correction on Logs248177c9, preserve the child3715 commit, then merge the corrected parent into that child. Do not rewrite either original author history. Existing screenshots describe the unchanged layout; browser verification adds reset-focus and a deliberately skewed synthetic proxy clock. + +## Grounded owners and changes + +- `src/server/management/logs-usage-routes.ts` GET /api/logs already returns an envelope with timeZone,total,logs (older array support remains in the GUI). Add `generatedAt: Date.now()` in that envelope. It is the proxy epoch milliseconds sampled while preparing the response. No headers/auth/CORS or existing fields change. +- `tests/server/logs-timezone.test.ts`: real handleManagementAPI response must include a finite generatedAt between pre/post request wall-clock samples and still carry logs/total/timeZone. Do not mirror the implementation in a fake formatter. +- `gui/src/pages/Logs.tsx`: capture valid numeric generatedAt on successful logs reads and anchor it to browser performance.now at receipt. The filter clock advances from server epoch plus monotonic elapsed time, not browser Date.now. New samples resynchronize the anchor. An aborted/stale response must not change the clock; clear/replace the anchor when apiBase/resourceKey changes. Network failures keep the last accepted anchor. Legacy arrays/envelopes lacking a usable timestamp preserve the existing browser-clock fallback until a server sample exists; do not pretend old servers supply precision they lack. Poll backoff remains separate. +- Small pure clock logic may live in `gui/src/pages/logs-clock.ts` if that keeps the large page readable; no generic clock framework. Document creation (generatedAt), serialization (envelope), validation/deserialization (finite nonnegative number), and consumers (relative-window timer and immediate selection) in the helper/page. Timezone formatting still uses timeZone independently. +- `gui/src/pages/logs-filter-bar.tsx`: the Reset click handler first performs the existing state reset, then restores focus to the stable All surface radio with a component-owned ref; avoid document-global queries or per-render focus stealing. Pointer and keyboard activation share the handler. The surface control remains mounted and selected after reset; no permanently disabled toolbar or layout expansion. +- `gui/tests/logs-auto-refresh.test.tsx` and `logs-filter-bar.test.ts`: actual rendered Reset activation verifies document.activeElement points to All after reset and the filters/default rows are restored. Add generatedAt envelope fixtures for browser clocks ahead AND behind by hours; a fresh row stays in15m while an older row is excluded. Advance the monotonic clock without network/with auto-refresh OFF to expire the row. A browser wall-clock jump after sampling must not shift the window. Cover fresh sample resync, malformed/legacy fallback, API-base switch and aborted late response not poisoning the active clock. Existing rollover/hash/timer tests remain meaningful, adapting their clock seam where required. A small logs-clock.test.ts is allowed for pure fallback validation; the skew acceptance must exercise rendered Logs. +- `docs-site/src/content/docs/guides/web-dashboard.md`, Korean counterpart and `structure/05_gui-and-management-api.md`: explain proxy-clock windows and stable reset focus, with older-proxy fallback stated accurately. Other translations must not contradict the source; no new untranslated UI strings are needed. + +## Activation matrix + +1. Apply a filter, focus Reset, activate it with keyboard: button can disappear but focus lands on selected All; subsequent navigation continues within the stable surface group. Also click Reset and verify focus. +2. Browser Date.now is six hours ahead, then six hours behind proxy generatedAt. Fifteen-minute window still selects only the same fresh proxy row; all-time remains unaffected. +3. Pause auto refresh, advance monotonic30s tick past a relative cutoff: old row expires with no fetch. Change browser wall-clock separately: rows do not jump. +4. Later successful server sample updates the anchor. A failed or aborted request cannot replace it; a different apiBase cannot inherit the previous server epoch. Legacy/malformed metadata uses documented fallback, never NaN windows. +5. Real management API returns generatedAt in the request time interval and preserves the envelope shape. Existing metrics/authorization tests remain green. + +## Delegation and verification + +Main owns server route/test, docs, Git/CI/FSM and native browser QA. Inherited Huygens owns only the GUI page/filter bar/optional clock helper and the corresponding GUI tests. Nash audits before B; independent implementation review follows. All application test/typecheck/lint/build commands run remotely; no local suite. Remote GUI checks include all GUI tests, lint/i18n/build, plus root typecheck/full suite and docs build. Final receipts state any queued GitHub jobs explicitly rather than marking them passed. + +## Check-phase React Doctor correction + +A cold pinned0.9.11 scan with the actual6005 base available reported two concrete diagnostics: rollover selection was adjusted in a post-render effect, and the rendered filter-bar test assigned an external observer during render. Move reconciliation into acceptance of the latest valid log response, preserving permanent reset when an identity disappears and current spelling when it remains. Keep component rendering pure by observing test state from an effect/event. No rule suppression is planned. Re-run the cold changed-scope scan before the regression suites. The earlier same-head hosted success is retained as an observed run result; it is not evidence that these diagnostics were absent. diff --git a/devlog/_fin/260906_d_integrations_delivery/050_remote_aliases.md b/devlog/_fin/260906_d_integrations_delivery/050_remote_aliases.md new file mode 100644 index 0000000000..b4e0452a28 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/050_remote_aliases.md @@ -0,0 +1,10 @@ +# 050 — Delivered remote Desktop integration + +The remote-hub Desktop alias and connection-lifecycle work associated with +[issue #3646](https://github.com/lidge-jun/opencodex/issues/3646) landed on dev in +[PR #3720](https://github.com/lidge-jun/opencodex/pull/3720). + +[070 — D delivery result](070_result.md) records the verified public behavior, merge and CI +references, contributor attribution, and original issue disposition. The separate thinking/replay +and prompt-cache request remains open in [#3719](https://github.com/lidge-jun/opencodex/issues/3719). +Detailed working notes are not reproduced in this public outcome record. diff --git a/devlog/_fin/260906_d_integrations_delivery/060_landing.md b/devlog/_fin/260906_d_integrations_delivery/060_landing.md new file mode 100644 index 0000000000..78adc6dcf8 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/060_landing.md @@ -0,0 +1,32 @@ +# 060 — D integration and final verification + +This operations/verification cycle consumes all five implemented candidates and any explicitly recorded repair work. Main owns it; no new feature scope is implied. + +## Loop specification and current handoff + +Archetype: integration/evidence closeout. Trigger: all five candidate cycles and their repair cycles have completed. Goal: published D changes and contributor history are on dev, originals are closed with honest remainder tracking, and the completed record is archived. Non-goals: new runtime features, release promotion, dogfooding or unrelated cleanup. Verifier: `.tmp/d-delivery/verify-final.py --ci-head ` and its later `--docs-pr ` form, plus remote privacy verification of the docs-only closeout. Stop: merged archive record, successful actual integrated runtime CI and all durable criteria met. Memory: this unit,070 result and ignored JSON/log receipts. A failed or pending gate remains open. Main owns operations; an independent reviewer audits the plan and final evidence. Main reclaims any failed sidecar after two distinct failed packets; new write scopes require a plan amendment. + +The preceding D accepted remotealias candidate022887702 with remote full19,751/15/0,292 focused tests, typecheck/docs425, privacy/final-patch Gitleaks and independent reviews. PR3720 still needs integration when this handoff is written. Replay/cache remains separately tracked in3719. Earlier D originals3669/3673/3628/3625 and Cursor follow-up3715 are already on dev; final verification rereads their actual state. + +The verifier deliberately refuses an OPEN3720 or a CI head preceding its merge. Its existence and refusal can be audited now; a passing final receipt is only possible after those prerequisites occur. It fetches dev, checks each merge's ancestry in both dev and the tested CI head, checks original/follow-up disposition and preserved authored commits, rejects unresolved carry-review threads, and requires actual Linux four-shard/macOS two-shard/gates/aggregate success. Dispatch-only skips are recorded separately. + +## Exact action map + +- Read live head/base/reviews/checks for every D carry; compare source-original current heads before closing them. Preserve contributor commits and Co-authored-by trailers. +- Integrate bottom-up with owner-authorized admin merge. Verify fetched origin/dev contains each merge SHA. Retarget every open child immediately; preserve its unique commits and branch identity. A squash requires cascading descendants before further readiness claims. +- Close superseded originals3669/3673/3628/3625 only after actual dev ancestry proof. Resolve3646's alias slice and explicitly preserve its separate thinking/cache request in an exact-scope existing or templated follow-up before closing it; never claim the alias patch fixes cache behavior. +- Reconcile all valid late findings and real CI failures. Any required source repair is planned with exact files/activation before patching; a new independent feature becomes a separate appended cycle, not hidden work here. +- Capture a final integrated dev SHA containing all five fixes and verify actual GitHub CI producers and aggregate on that SHA. Queued/cancelled/skipped application tests are not passes. Record platform dispatch-only limitations honestly. c-2 cannot be met until integrated CI succeeds. +- Recheck shared B locale/model-alias changes and A/C touched seams at final dev. If their source inputs changed the rendered Logs surface, repeat only affected browser scenarios; retain immutable sanitized screenshot URLs. +- Move this completed unit to devlog/_fin only after the published outcome is verified, recording final heads/run URLs/author/source dispositions. Detailed pending security notes remain in ignored scratch; publish only resolved outcomes. + +## Evidence and boundaries + +Use .tmp/d-delivery JSON/log receipts, current GitHub APIs and git merge-base --is-ancestor checks, never old labels or remembered output. Preserve the managed worktree and unrelated edits. No local suites/typecheck, no release/main/preview or dogfood operations. Every push uses --no-verify. Final goal completion requires all original criteria plus late-review and final-CI criteria; this page does not weaken any earlier bar. + + +## Archive implementation and checks + +After source integration and actual integrated CI succeed, merge the current dev into the closeout branch with hooks disabled. Write `070_result.md` using only verified public outcomes (PR/issue links, merge/run hashes, test scope and attribution), update050's neutral pointer to the published outcome, and move this unit to `devlog/_fin/260906_d_integrations_delivery/`. Keep private050/051 threat/design notes in ignored scratch. Preserve all unrelated A/B/C records and runtime files. + +Publish a template-compliant docs-only PR targeting dev. Verify its changed paths are solely this unit's devlog records, and validate the current archive checkout through remote privacy scan and source-tree equality. Admin-merge that record after the docs checks, fetch its ancestry and run the final verifier with its PR number. The P-to-A plan-artifact gate only requires the current plan directory at entry; archiving the verified unit later in B/C is the intended terminal operation, not a new feature. Do not hand-edit FSM/task completion flags. diff --git a/devlog/_fin/260906_d_integrations_delivery/070_result.md b/devlog/_fin/260906_d_integrations_delivery/070_result.md new file mode 100644 index 0000000000..f9add40b91 --- /dev/null +++ b/devlog/_fin/260906_d_integrations_delivery/070_result.md @@ -0,0 +1,60 @@ +# 070 — D delivery result + +Verified 2026-09-06 against integrated `dev@014061a7ea908118225314538b607afdac2015b1`. +All five assigned units landed through six PRs. The four source PRs and issue #3646 are closed; +the separate Anthropic thinking/replay/cache request remains open as +[#3719](https://github.com/lidge-jun/opencodex/issues/3719). +This records the verified public implementation outcome. + +## Delivered changes + +| Unit | Landing | Merge commit on dev | Outcome | +| --- | --- | --- | --- | +| #3669 | [#3684](https://github.com/lidge-jun/opencodex/pull/3684) | `22da7a4bc80040f66b819239c5028e578f9a1ede` | Refuse lossy TOML temporal-value rewrites before client configuration mutation. | +| #3673 | [#3702](https://github.com/lidge-jun/opencodex/pull/3702) | `eeca697b6fecddb507fdab6808ccbe7eb9de2f74` | Retain late tool-call index aliases while preserving budget ownership and cleanup. | +| #3628 | [#3707](https://github.com/lidge-jun/opencodex/pull/3707) | `6dd23d6314c41f1113639e042353aae9e6614e62` | Preserve Cursor executable tool schemas and reserved-name handling. | +| Cursor guidance follow-up | [#3715](https://github.com/lidge-jun/opencodex/pull/3715) | `67fdf24eb6e661f4d9e84aaa86a4eb39c6f3ba58` | Retain parser-owned freeform input descriptions, including apply_patch guidance. | +| #3625 | [#3712](https://github.com/lidge-jun/opencodex/pull/3712) | `cf6f30727e71c59a4c50f0be87d6fe7614564fc3` | Composable loaded-row Logs filters, reset/focus behavior, proxy-relative time, responsive controls and translated documentation. | +| #3646 | [#3720](https://github.com/lidge-jun/opencodex/pull/3720) | `014061a7ea908118225314538b607afdac2015b1` | Hub-issued Desktop IDs/origin, restart routing, owned restoration, key migration/recovery and explicit legacy standard fallback. Unresolved date-shaped IDs return mapping-unavailable 503; unknown legacy hashes return 400, without fallback. | + +## Verification + +| Evidence | Verified result / limit | +| --- | --- | +| [Integrated CI 34001966922](https://github.com/lidge-jun/opencodex/actions/runs/34001966922) | Exact integrated head `014061a7ea908118225314538b607afdac2015b1`: all four Linux shards, both macOS shards, common gates and aggregate succeeded. | +| Dispatch-only jobs | Six Windows shards and the macOS control job were **skipped**, not passed. This run does not establish full-suite Windows coverage. | +| Remote candidate `500aa73a760993d95f3e96f9ff9cfd240de2b7b4` | Bun 1.4.0 / Node 22.22.0: full suite **20,098 pass / 15 skip / 0 fail**; TypeScript checking and **425-page** docs build passed. | +| Same-candidate focused validation | **562 tests across 26 files**, exit 0; privacy scan passed; final-patch Gitleaks scan found no leaks. | +| Logs at `2221aed73cf8ef5b24452f22eda369c6539b2273` (#3712) | Browser evidence covers composition/reset, exact models, empty/offline states, keyboard navigation and widths 320–1440. Remote GUI suite: **1,499 pass / 0 fail**, with lint/i18n/build and docs validation. | +| Final Logs surface comparison | The recorded comparison with candidate `500aa73a7` preserves the browser-verified Logs surface and relevant inputs. Existing screenshots remain applicable; no new final-head browser run is claimed. | + +All application test/typecheck execution was remote or hosted. No local application tests or +typechecks were run. The final verifier checked every landing's ancestry in both dev and the +integrated CI head, original/follow-up disposition, carry-review resolution and retained authorship. +Evidence receipts: `.tmp/d-delivery/final-verification.json`, +`.tmp/d-delivery/final-014061a7e-verifier.log`, `remotealias-remote-proof.json`, +`remotealias-focused-proof.json` and `logs-final-surface-identity.json` in the same evidence directory. + +Synthetic screenshots published with #3712: +[English desktop](https://raw.githubusercontent.com/lidge-jun/opencodex/2221aed73cf8ef5b24452f22eda369c6539b2273/docs-site/public/screenshots/logs-filters-desktop-en.png), +[proxy-clock window](https://raw.githubusercontent.com/lidge-jun/opencodex/2221aed73cf8ef5b24452f22eda369c6539b2273/docs-site/public/screenshots/logs-filters-proxy-clock.png), +[Korean mobile](https://raw.githubusercontent.com/lidge-jun/opencodex/2221aed73cf8ef5b24452f22eda369c6539b2273/docs-site/public/screenshots/logs-filters-mobile-ko.png). + +## Attribution and remaining work + +The verifier confirms eight contributor-authored commits retained in dev: + +| Author | Retained commits | +| --- | --- | +| Hako | `08c7d3784d0cfa96c61467b8c7a581ea661378e3`, `fef024a69cfbe735d3ce0a6d33e65e911461bd2d` | +| SB Yoon | `4f0c278420998778e1341f7c7ed88e818c7ae048`, `3a7e4996435e68fd8caf8374dc75b3c759133582`, `f13cf27a22d975db2927e71960cec6e5fea02288`, `0073dd331b075578d1616d39a915bf87f00befaa`, `846197c91efeb3e411c3650e884260fdf6e45a7b`, `e7c3495b73bf73ab199bf283d0d8a079eed929e1` | + +The earlier PR CI run for candidate `500aa73a7` timed out in macOS shard 1's +`shellStreamExec completion acknowledgement` test. Its native root cause remains unproven; +the successful integrated run is new evidence, not proof that the timeout cause was fixed. +macOS CI runner-policy alignment remains a separate maintenance task; this record does not +claim a native-shell root-cause repair. + +Closing #3646 records delivery of its remote-alias and connection-lifecycle slice only. +Thinking/redacted-thinking replay and prompt-cache behavior remain separate in #3719; +no cache-fidelity, cache-hit or quota-saving fix is claimed here. diff --git a/devlog/_fin/260906_manual_account_selection/000_plan.md b/devlog/_fin/260906_manual_account_selection/000_plan.md new file mode 100644 index 0000000000..3ee8ea01cd --- /dev/null +++ b/devlog/_fin/260906_manual_account_selection/000_plan.md @@ -0,0 +1,33 @@ +# Manual account selection must control dispatch + +Loop: single-cycle satisfy-spec, C4 (credential/account allocation). Trigger: the user selected a healthy OAuth account in the dashboard, but every request was silently assigned to another account. Goal: disabled pools do not proactively reassign healthy requests; explicit selection wins; permitted automatic assignment is reflected in active-account state and dashboard. One cohesive PR targeting dev, pushed with `--no-verify` and merged as explicitly authorized. No stacks. + +Boundaries: existing account/key owners, request credential pairing, dashboard account synchronization, regression coverage, matching public docs. No provider API changes, real-account configuration changes, paid inference probes, releases, service restarts, or unrelated refactors. Use existing dependencies and temp credentials. No token, cost, or wall-clock budget was set; no paid oracle is required. Completion requires fresh direct verification and actual remote merge; a pending PR is not DONE. Escalate only a genuine tool/access block or a necessary authority not already granted. Main reclaims a lane after two distinct worker failures; new worker scope requires a P amendment. + +Memory: this numbered unit, `.tmp/manual-account-selection/` evidence, session-bound goalplan. The implementation and test map is in 010. One PABCD work-phase covers this single contract across its existing owners; frontend/runtime lanes are subtasks rather than separate deliverables. + +## Evidence and rival hypotheses + +GUI `useProviderAccountPools.ts:247` sends selected account to PUT `/api/oauth/accounts/active`. `oauth-account-routes.ts:322` calls `setActiveAccount`, which saves the selected id. `generic-account-failover.ts:185-196` defaults healthy proactive selection on when two accounts exist. `preferredInitialAccount` ranks by quota and can replace the selected healthy account. `responses/core.ts` resolves that other credential and logs it without updating stored selection. Provider quota reads stored active selection separately. Exact user-provided request IDs matched another account on attempt1, sendCount1, no retry. This is not an upstream429 recovery or a failed GUI save. + +H1 failed persistence is disproved by stored selected id and successful route semantics. H2 only a2s roster cache is insufficient: quota ranking would choose the other account after the cache expires. H3 manual authority absent from the selector is supported by the complete route-to-store-to-request chain. We will prove H2/H3 with isolated real owner functions before implementation. + +## Contract + +- Presence of multiple stored accounts does not enable proactive healthy-request allocation. Absent generic OAuth proactive enablement is off; explicit false wins for proactive allocation. REACTIVE429 recovery remains mandatory whenever another usable account exists, regardless of the pool switch, per the latest explicit user correction. +- With a pool enabled, the healthy active/manual account stays eligible and preferred. A quota percentage alone below exhaustion must not replace it. On a real account-scoped refusal or known exhaustion, enabled rotation may choose another usable account. +- Committing an automatic account selection must be conditional on the selection generation that produced the request. A newer manual selection (including A→B→A) wins. +- The selected account and its access token/project/origin metadata travel together; unsupported/unknown quota is not exhaustion. +- Codex, Anthropic, and API keys keep their own established contracts, but any contradiction with these user requirements is repaired in the same PR. Their specific findings must be folded into010 before their edits. + +Enforcement: runtime selectors plus guarded persisted active-account transition; execution surface covers first dispatch and every reactive replay. Known bypass: external callers can intentionally route exact account-targeting selectors, which remain their own explicit contract. Residual: concurrent requests can already be in flight on different credentials; dashboard reports the latest committed allocation, never retroactively cancels an already sent request. Wording: no claim that a UI highlight can reassign a request already upstream. + +Verification: focused Bun store/management/selection/retry tests first; full `bun run typecheck` and `bun run test` before PR review-ready; relevant GUI tests/lint/build and a rendered state transition if frontend changes. Regression tests use synthetic identities only. Public SoT: `structure/05_gui-and-management-api.md` and existing account/pool guide pages. Final record includes rejected hypotheses, unmodified owners with proof, security/concurrency audit, and remote merge. + +Latest steering: the user reports Codex works correctly. Preserve Codex routing/controller semantics and use its existing manual-selection behavior as the reference; include regression-only checks for Codex. Concentrate changes on the other quota-aware account paths where a concrete mismatch is established. + +Latest explicit design instruction: GUI selection and pool selection must share one selection owner, as Codex does. Both must commit the same authoritative selection BEFORE dispatch; requests use the committed account. Do not bolt on a separate UI-only mirror or route around manual selection while pretending the old active account remains selected. A concurrent newer user selection wins. This strengthens the existing planned store-owned selection transaction and applies with pool on or off. + +Latest correction: 429 automatic account switching is ALWAYS allowed, including poolOFF. Withdraw the planned reactive disable gate. Manual selection wins ordinary dispatch; a real429 may replace it using the same guarded selection owner, and the GUI follows that committed replacement. + +Latest explicit verification restriction: do not run repository-wide tests. The earlier full-suite requirement is superseded. One attempted full run was interrupted by user at exit130; it is not completion proof. Resolve observed failures with their specific test files, run focused affected checks and typecheck, then push --no-verify and merge the single PR. diff --git a/devlog/_fin/260906_manual_account_selection/010_implementation.md b/devlog/_fin/260906_manual_account_selection/010_implementation.md new file mode 100644 index 0000000000..85bf466a3c --- /dev/null +++ b/devlog/_fin/260906_manual_account_selection/010_implementation.md @@ -0,0 +1,71 @@ +# Implementation — one account-selection contract + +Depends on000. One PABCD work-phase, one PR. Existing subsystem owners stay intact. + +## Shared OAuth store and generic allocation (main) + +MODIFY `src/oauth/types.ts`, `src/oauth/store.ts`: add optional non-secret `ProviderAccountSet.selectionRevision` and a typed selection snapshot `{ accountId, revision? }`. Legacy files without the field remain valid. Normalize/persist/copy it through the existing auth-store boundary; every active-selection change (including re-selecting the same account) advances it. Add a store-owned capture function and conditional active-selection commit that compares both original active id and revision inside `mutateStore` before writing. Credential-only refresh must preserve the selection revision. Consumers: generic and Anthropic request admission/promotion. Management DTO need not expose the revision: existing active id remains the public selection. No credential value is logged. + +MODIFY `src/oauth/generic-account-failover.ts`: proactive allocation requires effective pool `enabled === true` (provider override then global). Merely storing2 accounts does not enable healthy-request steering. Preserve presence-based REACTIVE429 switching even when disabled, as the user expressly requires. Keep a healthy manually active account first; use quota ranking only when the chosen account is ineligible/exhausted or when an enabled pool recovers a real refusal. Unknown quota never implies exhaustion. Preserve existing cooldowns and bounded attempts. Clear roster on manual selection so an old2s cache cannot dispatch the previously selected account. + +MODIFY `src/server/management/oauth-account-routes.ts`: manual selection retains successful persistence and cache invalidation, and invalidates relevant generic selection state. Existing Anthropic manual handler remains its owner. Update outdated pool-settings contract comments/DTO docs in `src/oauth/pool-settings-capability.ts`, `src/types/provider.ts`, `src/types/config.ts` to match effective enablement; do not introduce a new UI toggle simply to repair a default. + +MODIFY `src/server/responses/core.ts`: capture the OAuth selection snapshot before awaited token materialization; preserve token/project/origin pairing. For actual automatic initial/retry selection, await the guarded active-account commit before publishing that allocation. A rejected promotion caused by a newer user selection must not overwrite it; continue via current valid selected account or return the original request failure as appropriate. Apply consistently to direct upstream errors, runTurn on429 callback, passthrough/combo retries and downstream stream recovery call sites. The same shared core serves Responses/Chat/Messages surfaces. + +## Anthropic and API keys (backend lane) + +MODIFY `src/oauth/anthropic-routing.ts` and its existing tests: preserve reactive429 rotation even when the pool is off (latest user correction). Manual selection must seed a preference that wins the next eligible dispatch, including quota strategy, clearing stale affinities. Guard automatic active-account promotion with the same store selection snapshot; main owns core call-site integration. Maintain account-scoped refresh restrictions. + +MODIFY `src/providers/key-failover.ts` and relevant caller/rotation types only if the isolated repro confirms env/keychain reference identity mismatch: match failed attempts by stable pool-entry identity/reference, never by comparing a resolved secret with a stored reference. Preserve newer manual key selection and committed config-to-dashboard mapping. API-key pools have no separate enable boolean: an explicitly configured multi-key pool remains their existing enable contract. No speculative new mode is added. Codex is unchanged and regression-only, per latest user steering. + +## Dashboard (frontend lane) + +MODIFY existing `gui/src/hooks/useProviderAccountPools.ts` / `gui/src/pages/Providers.tsx` runtime-read integration and existing tests as needed: periodically reconcile cheap local OAuth/key roster active state through the shared scheduler, without forcing upstream quota probes on every tick. Reuse quota rows and reject stale read results around manual mutation. Do not alter Codex controller behavior. Keep layout and existing localized labels. Render a synthetic selected-account transition and retain a screenshot in the unit for PR evidence. + +## Proof / reachable cases + +- Pool absent/false with2 accounts: manual A30%, B11%; ordinary dispatch staysA. Simulated429 MUST auto-switch to another usable account even with pool off, and persist that selection. Pool enabled: manual A remains preferred; known exhaustedA or actual refusal chooses usableB and active DTO becomesB. +- A delayed token refresh/429 starts onA; manual choiceB or A→B→A occurs before completion; older selection revision cannot change active state. Removal/reauth during candidate resolution cannot promote an invalid target. +- Generic matrix runs xAI, Cursor, Kimi, Copilot, Antigravity, Nous and representative passive-quota provider using synthetic snapshots. Copilot regional origin and Antigravity project remain paired to the selected bearer. +- Anthropic off/on, quota/RR manual priority, stale affinity, failed token resolution, and promotion races; Codex direct/manual/pin existing regressions stay green. +- Literal/env/keychain-supported API-key identities: rejected attempted key rotates to another distinct key, newer manual key wins, chosen key is the persisted active key. +- Dashboard backendA→B read updates highlighted selection and current quota association; an older quota/roster poll cannot revert a newer manual choice; a roster-only poll never initiates a paid/upstream quota read. + +Reuse existing tests: `tests/oauth/generic-oauth-failover.test.ts`, `tests/oauth/oauth-store-multi.test.ts`, `tests/oauth/adapter-event-oauth-failover.test.ts`, `tests/server/account-pool-management-api.test.ts`, provider quota/Anthropic/key failover suites discovered by owner search, and existing `gui/tests/provider-account-quota-loading.test.tsx` / provider revalidation tests. Prefer these files to new layout entries. Verify focused red before repair, then green; run `bun run typecheck`, `bun run test`, `bun run privacy:scan`, and relevant GUI tests/lint/build. New failures outside scope are diagnosed and recorded, not ignored. Fresh independent security/concurrency review before delivery. + +SoT sync: `structure/05_gui-and-management-api.md`, existing English configuration/account pool guide plus translated statements that would otherwise contradict changed enablement. Record provider coverage and limitations in011 evidence. Push single branch with `git push --no-verify`; create one PR using repository template againstdev, attach rendered UI evidence if GUI changed, verify remote head/CI, then merge as authorized and verify integration SHA. + +Known design risk for A audit: making selection persistence part of dispatch must not serialize all independent successful requests; commit only actual account changes, and use the existing guarded store writer. Manual selection while an upstream request is already running applies to subsequent allocation; no retroactive cancellation claim. + +## P clarification from frontend owner + +Current scheduler is `useKeyedClientResource`/`client-resource.ts`, not `useRuntimeRead`. Exact frontend writes: `gui/src/hooks/useProviderAccountPools.ts`, `gui/src/pages/Providers.tsx`, and `gui/src/pages/use-providers-oauth.ts`; tests: existing `provider-account-quota-loading.test.tsx` and `provider-revalidation-policy.test.tsx`. Register one local-roster refresh through App's existing30s shared scheduler, key by server+sorted provider list, not active ids. Preserve initial quota enrichment; never add quota=1 to periodic reads. Invalidate per-provider read generation at manual PUT start; apply successful response active id; late login-status hydration may seed only a missing roster. Codex controller stays unchanged. Existing relevant GUI baseline52 tests passed in separate file processes; grouped globals can collide, so run each file separately. + +## Shared-selection requirement (latest steering) + +GUI PUT and pool choice both use the store-owned active-selection transaction. Make the common operation return/confirm the committed selection, and dispatch from its matching credential snapshot; a pool proposal is not authoritative until it commits. Do not mutate per-request bearer first and asynchronously update GUI afterward. The generation guard is a concurrency condition inside that same shared operation, not a competing selection state. Keep the public active-id DTO unchanged. Main and backend lane must align on this seam before writing callers. + +Authoritative429 exception: poolOFF suppresses only proactive steering. Every generic/Anthropic/key recovery test must preserve automatic429 failover. Any earlier statement blocking429 whileOFF is superseded. + +## Immediate synchronization amendment + +Latest user rejects waiting for a poll after automatic selection. The repository has no dashboard EventSource subscription to reuse. Add a narrow authenticated management SSE invalidation channel for committed account/key selection (`/api/accounts/events`) with bounded subscriber count, lightweight heartbeat, disconnect cleanup, and no credentials/account identifiers in events (provider plus kind/revision only). A dependency-leaf `src/lib/account-selection-events.ts` owns subscription/publication; it must not import server or Lab. Shared authoritative OAuth/key selection writers publish only after successful persistence. `src/server/management/oauth-account-routes.ts` serves the channel behind existing management auth; close it through existing optional shutdown hooks if necessary. Frontend lane adds a single lifecycle-owned EventSource for this screen, invalidates cheap roster via current generation guards, reconnects with a full local refresh, and keeps30s scheduler as recovery only. Existing test files cover event arrival→highlight change without advancing poll clock, blocked/failed writes emitting no selection event, and subscription cleanup. New endpoint is authenticated and carries no authority to select; data-plane keys cannot subscribe. This replaces the earlier30s-only plan. + +## A synthesis — accepted bounded corrections + +Independent reviewer verdict: GO-WITH-FIXES(blockers=5). All five are folded into the implementation, none rebutted: +1. Revision lifecycle covers manual reselect, new activation, removal promotion, replacement/recreation; rollback replacement receives a new revision, never resurrects an old one. Credential-only writes preserve it. +2. Common store operation `commitOAuthAccountSelection(provider, accountId, {expectedSelection?, expectedCredentialGeneration?, requireUsableAccount?})` returns committed `{accountId,revision?}` ornull. GUI's existing `setActiveAccount` boolean API wraps this same operation. `captureOAuthAccountSelection` supplies the expected snapshot. Validate unchanged-account admission too; retry current selection after a failed CAS, never send the rejected candidate. Cover generic core sites4868/5753/6100/6834/7244 and initial selection. Failed CAS emits nothing. GUI invalidates reads at both PUT start and settle and preserves settled quota state. +3. Anthropic affinity/rotation success bookkeeping occurs only after selection commits. All four promotion callers await it; background local-CLI token restrictions remain checked before commit. +4. API-key attempt carries stable pool identity/reference plus selection generation across all callers including nativeChat; common manual/automatic selection commit guards ABA and notifies only after persistence. +5. Quota eligibility explicitly distinguishes known exhaustion from unknown, including Kiro overage rules. Parameterized provider coverage includes Kiro and passive providers. + +B lane allocation (approved plan): main owns core.ts, OAuth management route/SSE route registration, generic selector/rank and integration proof/docs; store lane owns oauth/types.ts+store.ts, leaf account-selection event bus, and oauth-store-multi.test.ts; backend lane owns Anthropic routing+tests and API-key source/router/transport+tests including types/provider.ts; frontend lane owns the3GUI sourcefiles and2testfiles above. No worker changes maincore or another lane's files. Independent context review follows integration. + +Latest explicit verification restriction: do not run repository-wide tests. The earlier full-suite requirement is superseded. One attempted full run was interrupted by user at exit130; it is not completion proof. Resolve observed failures with their specific test files, run focused affected checks and typecheck, then push --no-verify and merge the single PR. + +## C corrective review amendment + +Accepted independent review findings: dispatch must revalidate after pacing/build waits;401 replay must use the common selection owner; CCA project must always come from the admitted account; Anthropic initial manual choice must survive restart; selection SSE must stop on session revocation/expiry; hub relay must not apply its15s total deadline to an established selection stream; late initial quota data must survive manual selection without restoring old active flags. Main owns physical dispatch,401,CCA; backend lane owns Anthropic/API-key corrections; frontend lane owns reconnect/quota fixes. For bounded parallel C repair, the completed store worker is reassigned to SSE/management session liveness and hub-relay fixes only; no concurrent write ownership overlaps. All verification remains focused; full suite is prohibited. Draft PR3768 is open and CI runs asynchronously. + +C second-review correction: a rebuilt adapter must replace the active adapter/cache, and physical admission must be bound to the particular wire request's originating credential, not merely shared request state. Image/search model loops need the same request-specific executor. The runtime reviewer is reassigned as an exclusive repair worker for core/fetch-helpers and those two loops plus focused regression tests; main pauses edits there and independently verifies the returned delta. API-key helper/native Chat remains the backend lane; runtime worker integrates its exported helpers. Codex forward path remains unchanged. No full local tests. diff --git a/devlog/_fin/260906_manual_account_selection/011_verification.md b/devlog/_fin/260906_manual_account_selection/011_verification.md new file mode 100644 index 0000000000..ac944beeb8 --- /dev/null +++ b/devlog/_fin/260906_manual_account_selection/011_verification.md @@ -0,0 +1,50 @@ +# Verification and delivery record + +The fix uses a common committed selection for manual and automatic OAuth/API-key allocation. +A healthy manual selection has priority; reactive429 recovery remains enabled with poolOFF. +The physical request carries the binding of the adapter that built it. A stale binding is rebuilt, +and the new adapter and request cache remain authoritative for later retries and continuations. +Image/search loops share the request-specific executor. Codex routing/controller semantics are unchanged. + +## Focused evidence + +| Surface | Evidence | +| --- | --- | +| Generic OAuth | Parameterized xAI, Cursor, Kimi, Copilot, Antigravity, Nous, Kiro, Meta-Muse manual priority;36 focused checks passed | +| Store |13 failing regression cases before repair;36 store checks passed, including ABA/removal/recreation and refresh-only preservation | +| Actual dispatch | Copilot build/pacing races,413 follow-up, image/search pacing, and runTurn first-send coverage;31 checks passed with3 final boundary cases demonstrated RED→GREEN | +| API keys | Literal/env/keychain identity and newer manual selection; native Chat pacing revalidation; focused12+59 checks passed | +| Anthropic | Manual selection, guarded promotion, restart bootstrap, and always-on429; focused96-test group passed | +| Antigravity |20 OAuth401/project tests passed; a project-less account is refused before dispatch; every admitted request uses its account's project | +| Image/search | Image loops31, search61, timeout contract7 passed in separate processes | +| Management/relay |40 focused checks passed; authenticated invalidation stream, client cancellation, byte/subscriber bounds, expiration/revocation, and established SSE lifetime | +| Dashboard | Probe/passive quota hydration, stale selection guards and immediate event/reconnect behavior;38 roster+8 page checks passed | +| CI fixes | Upsert fixtures now persist like the real login flow and verify disk; GUI source binding check updated; React Doctor0.9.11 changed-file scan has0 errors/0 warnings | +| Static/privacy | Typecheck, privacy scan and diff check passed at integration checkpoints | + +Counts identify each recorded check group; they overlap and must not be added into a unique-test total. +The user prohibited repository-wide local tests. An earlier full run was interrupted with exit130; +it is not completion evidence and was not repeated. CI runs asynchronously on PR3768. +At dd5aec571, all23 applicable CI checks passed, with2 intentional skips. + +## Current browser proof + +Aside opened a local synthetic fixture rendering the real Providers component and styles at1440×900. +The current GUI moved ChoiceA→ChoiceB from a selection event without advancing the poll clock; +upstream quota-read count stayed2→2. Both screenshots were inspected by main; no Korean clipping or +incorrect active indicator was observed. The fixture and owned browser tabs were stopped afterward. +The screenshots contain only synthetic account names and masked IDs. + +- [Before](evidence/011_selection-before.png) +- [After](evidence/012_selection-after.png) + +Independent C reviews identified and drove repairs for cached adapter reuse, sidecar dispatch, +credential refresh priority, account/project pairing, restart priority, stream lifetime and quota +hydration. All identified findings were implemented and the repaired cases were exercised. + +## Delivery scope + +One PR: https://github.com/lidge-jun/opencodex/pull/3768 . Every push uses`git push --no-verify` as +explicitly requested. The maintainer explicitly authorized an administrator merge. Remote merge +state and its final SHA are verified separately from local implementation proof; no runtime service +restart or real-account configuration mutation is part of this change. diff --git a/devlog/_fin/260906_manual_account_selection/evidence/011_selection-before.png b/devlog/_fin/260906_manual_account_selection/evidence/011_selection-before.png new file mode 100644 index 0000000000..e48fdf946f Binary files /dev/null and b/devlog/_fin/260906_manual_account_selection/evidence/011_selection-before.png differ diff --git a/devlog/_fin/260906_manual_account_selection/evidence/012_selection-after.png b/devlog/_fin/260906_manual_account_selection/evidence/012_selection-after.png new file mode 100644 index 0000000000..1806056ab7 Binary files /dev/null and b/devlog/_fin/260906_manual_account_selection/evidence/012_selection-after.png differ diff --git a/devlog/_plan/260906_aside_profiles/000_research.md b/devlog/_plan/260906_aside_profiles/000_research.md new file mode 100644 index 0000000000..b1e32b995f --- /dev/null +++ b/devlog/_plan/260906_aside_profiles/000_research.md @@ -0,0 +1,21 @@ +# Aside profile synchronization roadmap + +User scope extension: synchronize all Aside profiles and expose independent profile switches in GUI and CLI. Continue the original Grok catalog/Responses stabilization stack; no local test suites or local typecheck, no release/service deployment. Existing push --no-verify and admin-merge authorization applies to these scoped layers. + +Observed installed contract: accounts.json has currentAccountId plus accounts[] with numeric id and name; profileAccountBindings maps browser profiles to accountId. This machine has three account-backed profiles (one cloud, two local), with a models.json only in the current account. Every model catalog lives under the configured Aside root/u//models.json. Browser profilePath is metadata, never a write destination. Multiple bindings sharing one account share one model catalog and therefore one control row. Keep only id/name/current metadata; never serialize sessions, tokens, user IDs, email or subscription metadata from the manifest. + +Current owners: config-export.ts asideCurrentAccountId resolves only currentAccountId. registry.ts aside.resolvePaths freezes that current path pair. writer.ts synchronous input supports resolvedPaths but its async freeze recomputes current paths; state.ts has no frozen-pair input. The ownership store is keyed by clientId, so a single root can retain only one Aside account. FileIntegrationPage already owns safe toggle/overwrite/history/restore, and all its resource keys currently use only client ID. CLI is a thin management caller with no profile flag. + +Decision: enumerate account-backed profiles; derive paths strictly from numeric IDs under Aside root, not profilePath. Partition each new profile's ownership and journal into /aside-profiles/; retain exactly one stable writable legacy root owner; all sibling writes use independent child stores. Older mixed legacy history remains readable by exact profile path and can be imported into the correct child store only for explicit restore. Freeze the chosen profile's paths for status/write/restore. Do not move user files, change currentAccountId, or copy credentials. + +Desired state: add asideProfileSync:{allProfiles?:boolean,profiles?:Record,legacyProfileId?:number|null} to OcxConfig. Absent defaults to whether a legacy Aside ownership record establishes prior connection. That legacy connection enables all discovered profiles by default, satisfying the user's all-profile request. A per-profile override persists independently. Before modifying a per-profile override, materialize the prior global default so disabling the legacy profile does not flip siblings. Explicit actions persist desired policy before any file writes; a save failure aborts with no file mutation. Bulk intent sets allProfiles and clears overrides, while actual per-profile applied states and refusals remain separate. A failed file mutation leaves visible pending intent, never an all-applied claim. Restore reconciles only its target profile policy with validated prior ownership so Undo cannot be silently reversed by the next sync. Per-profile-only enable when previously disconnected leaves other profiles off. Implicit sync refreshes owned enabled profiles and may safely apply an absent block in an explicitly/legacy-enabled unowned profile; it never overwrites foreign blocks or recreates a manually removed previously-owned block. + +Cycle map: docs-only roadmap; 010 backend/profile ownership/API/CLI (foundation and API can be separate dependent PRs within this single implementation unit); 020 GUI controls/QA and final full-stack landing. Every original exact-head CI and merge-ancestry criterion remains open until terminal delivery. + +Design Read: a repeated-use integration settings page using the existing monochrome dashboard: --bg white/#212121, --surface white/#262626, --accent #0d0d0d/#ececec, existing --font-ui and ClientMark. Compact profile rows show name/current marker, state, and switch; a global switch and enabled/total count summarize all profiles. Details reuse the existing FileIntegrationPage scoped to a selected profile so history/restore stays available. No new visual framework, assets or motion. DESIGN_VARIANCE2, MOTION_INTENSITY1, densityD5. Loading/error/empty/partial/busy states are explicit; the current browser account never changes when an integration switch changes. + +Resource bounds inherited: six-hour window from original goal, no requested token budget, original at-most24 live synthetic provider requests. Profile probes use temporary roots with three profiles; bulk production discovery is bounded to128 account entries. Existing local/GitHub credentials only for authorized repo work. Actual user profile files remain read-only during development. Runtime file writes are tested only in isolated fixtures. C4 ownership/path review is required before production merge; security working notes stay ignored scratch. + +## Baseline + +`bun .tmp/aside-profiles/baseline.ts` runs only synthetic temp files: manifest has0/1/2, legacy owned0, current model-selection route runs refresh, and configuredAsideProfiles remains1. This reproduces the user report without editing any real profile. Browser profile bindings resolve to three distinct account IDs in the current install. diff --git a/devlog/_plan/260906_aside_profiles/010_profiles_backend_cli.md b/devlog/_plan/260906_aside_profiles/010_profiles_backend_cli.md new file mode 100644 index 0000000000..570ec2a3ea --- /dev/null +++ b/devlog/_plan/260906_aside_profiles/010_profiles_backend_cli.md @@ -0,0 +1,47 @@ +# 010 Profile data, ownership, API and CLI + +Class C4 for controlled multi-file writes; spec-satisfaction repair. Goal: all account-backed Aside profiles receive the selected catalog and can be independently enabled/disabled. Non-goals: login/account switching, browser profile data, credential changes, unowned overwrite without the existing explicit flag, other clients redesign. + +NEW src/clients/aside-profiles.ts: typed AsideProfile {id:number,name?:string,current:boolean,configPath:string,detectDir:string}; read configured asideHomeDir accounts.json, validate bounded account array, dedupe safe nonnegative integer IDs, fall back to current-only legacy manifest when accounts is absent, fail on malformed identities. Map only safe metadata; derive root/u/id paths. A numeric query selector must refer to this enumeration. No path from browser profile bindings reaches writes. +MODIFY src/types/config.ts + src/config.ts: asideProfileSync optional object with allProfiles boolean, numeric-key boolean overrides, and optional nullable safe-integer legacyProfileId provenance. Per-field validity must not erase unrelated configuration; preserve unknown future policy fields where existing conventions require. Full field chain: creation in Aside mutation service; persistence via saveConfigPreservingClaudeCode; deserialization in config schema; consumers profile status, explicit toggles and implicit sync; serialization GUI/CLI receives effective enabled per row, not raw credentials. +MODIFY src/integrations/state.ts: optional resolvedPaths in IntegrationStateInput, use it instead of resolving current profile. MODIFY writer.ts freezeIntegrationInput to clone a supplied internal resolved pair, preserving existing resolution otherwise. This is an internal seam only; routes never accept caller-provided paths. +NEW src/integrations/aside-profiles.ts: resolve profile-specific store/path input. Exactly one profile may use the writable legacy root: a matching current legacy ownership record wins; only if no record exists may the newest legacy Aside operation choose it. An unrecognized existing record makes the root unassigned. Persist the resolved legacyProfileId (number or null) before the first explicit mutation, so disabling/reloading cannot reassign it. All other profiles use isolated child stores. Read statuses with same classifier. Model load memoized across profiles. Compute effective default and per-profile override. Explicit enable/disable/overwrite uses existing coordinated writer and mutation-flight exclusivity, serializes profiles, returns per-profile results; persist desired preferences through the caller save seam before file mutation, under the same exclusive operation. On save failure restore the in-memory prior policy and abort before filesystem changes. Report desired enabled separately from actual state and per-profile refusals; do not fabricate all-applied success. Missing/foreign/unsafe/drifted profile remains untouched with explicit refusal. A manual deletion with a surviving ownership record stays absent on implicit refresh. First safe creation in enabled unowned profile uses apply without overwrite. Never switch the active account. +MODIFY src/integrations/owned-refresh.ts optional internal resolvedPaths; MODIFY catalog-refresh.ts Aside fan-out to profile service and preserve per-profile outcome IDs; update CLI explicit sync logs/type projection to identify profiles. + +NEW src/server/management/aside-profile-routes.ts: GET /api/client-integrations/aside/profiles returns {profiles:[{profileId,name?,current,enabled,...IntegrationStatus}],allEnabled,enabledCount,total}; GET /aside without profile returns aggregate IntegrationStatus+profiles, PUT /aside without profile acts on all discovered profiles. Existing /aside?profile= handles one explicit profile with same mutation/refusal semantics. Numeric profile parsing is strict, membership checked, non-Aside use rejected. Reuse existing jsonResponse/body parsing/CSRF outer boundary. Return partial failures visibly; do not turn mixed outcomes into a successful all-applied status. +MODIFY integration-routes.ts: route Aside list/status/toggle to profile service; collection projects Aside aggregate while other clients remain unchanged. Bind optional profile scope for journal/delete/restore query paths to the same selected store and frozen paths; no snapshot can restore into another profile. Existing no-profile legacy history remains accessible. Existing test hooks (root store/env/home/io/lock seams) must propagate. New prefs writes use deps.saveConfigPreservingClaudeCode, never bypass fixture isolation. +MODIFY src/cli/integrations.ts: --profile for Aside status/show/list, enable/disable, history/journal and restore/delete equivalents that exist; reject on other clients and malformed IDs. No --profile on Aside enable/disable means all. Route flag through query profile; status prints all per-profile rows, JSON preserves metadata; mixed failure exits nonzero with structured result retained. Update usage/capability source if help registry owns it, and operating docs. + +Tests: new profile enumeration/store/writer domain tests registered in both layout manifests; management and CLI tests cover current0+local1+local2, all-enable, individual-off persists through sync, legacy-default all, explicit one-only enable, active-account changes do not retarget a pinned write, unowned/drifted/removed/symlink/missing profile refusals, malformed selectors, unknown ID, partial outcome, per-profile journal/restore isolation and old legacy history. Actual temporary fixtures and original writer/management calls; no live user config mutation. + +Verification: standalone temp-root production probe establishes three distinct file outputs and one-off persistence across refresh; remote Bun focused regressions/typecheck/privacy gates; independent ownership/API review. Final exact-head hosted CI and all PR ancestry are terminal obligations, not satisfied by queueing. Candidate new paths source-checked before B. Escalation only for a concrete unresolvable external constraint, not routine design choices. + +## Audit-locked operational contracts + +- One outer Aside mutation flight owns the complete action, including policy persistence and every coordinated writer call. Its key includes root fingerprint, sorted selected profile IDs, operation/overwrite/restore semantics and a unique operation nonce. Overlap returns busy; no profile ever joins another result. Do not nest refreshOwnedIntegration inside that flight; call coordinated refresh/apply directly after the service's ownership checks. Different profile roots cannot coalesce either. +- Profile status and every writer use the concrete filesystem validation/guard contract recorded in ignored .tmp/aside-profiles/security-scope.md. Frozen path pairs alone are not the boundary. The guard is rechecked immediately before file mutation and is shared with status. +- Restore resolves the operation's exact profile independently of currentAccountId. Before policy persistence validate operation/snapshot availability, target identity and ordinary drift preflight. Desired state after Undo is true only when priorRecord describes the exact snapshot bytes as owned; absent/foreign/conflicted snapshots set a target false override. Global defaults and sibling overrides remain unchanged. Persist that target intent first; writer refuses or restores under the same flight. Cover enable->undo->sync and disable->undo->sync after reload. A later filesystem refusal remains visible as desired/actual mismatch, not success. +- NEW src/integrations/aside-profile-journal.ts (if separation needed): path-filtered profile history combines its writable store and matching legacy operations, deduping operation IDs. Snapshot reads use each operation's source store. A restore of an older sibling legacy operation imports only that immutable operation and its available snapshot into the target child store (same opId, exact priorRecord/configPath, no original deletion), then uses the existing coordinated restore there; it never changes the legacy owner's record. Expired snapshots stay expired. Profile history deletion checks the newest operation within that profile and retires duplicate imported/source copies together so a deleted row cannot reappear. Generic history/restore paths resolve Aside operation scope by exact configPath when no profile query is supplied, and reject an operation whose profile is no longer registered instead of retargeting it. +- Add profileId to journal/API rows, and treat (clientId,profileId/configPath) as history ownership for latest/undo/delete checks. Existing non-Aside behavior stays unchanged. + +C4 audit findings and concrete filesystem guard details are kept in ignored scratch; the public roadmap records feature contracts only. + +## P implementation interfaces at37b3a7f9b + +Delegation is within this one010 cycle with disjoint write sets. Path worker owns clients/aside-profiles.ts and tests/clients/aside-profile-paths.test.ts. Engine worker owns integrations/aside-profile-context.ts, aside-profiles.ts, aside-profile-journal.ts and tests/clients/aside-profiles.test.ts. Main owns type/config schemas, resolved-path seams in state/writer, management routes, CLI, implicit fan-out wiring and route/CLI tests. No worker commits, orchestration, local suites or real profile mutation. + +Path module exports AsideProfile {id,name?,current,root,configPath,detectDir}; listAsideProfiles(env?,home?) and guardAsideProfileIO(profile,io,profiles?) plus assertAsideProfileBoundary(profile,profiles?,mutation?). Invalid manifest/selector/path raises ClientPathError with safe text. Engine module exports AsideProfilesInput (config, models array/lazy, port, env/home/store/io, persistConfig?, lockSeams?), AsideProfileState (IntegrationStatus plus profileId/name/current/enabled and optional safe error), AsideProfileList (clientId,profiles,allEnabled,enabledCount,appliedCount,total plus aggregate state fields), listAsideProfileStates, getAsideProfileState(input,id), mutateAsideProfiles(input,{enabled,profileId?,overwriteConflict?}), refreshAsideProfiles. Mutations return {ok,clientId,changed,state,message,results:[WriteOutcome+profileId]}; singleton result stays accessible for the existing refusal serializer. + +Journal module exports listAsideOperations(input,profileId?) -> [{profileId,entry,store}], findAsideOperation(input,opId,profileId?) -> row|null, restoreAsideProfile(input,{opId,profileId?,confirmDrift?}) -> WriteOutcome+profileId and deleteAsideOperation(input,{opId,profileId?,principal?}). Main serializes journal metadata using each source store; profile-scoped newest protection and duplicate retirement live in the journal service. Journal discovery can return null for unrecognized non-Aside operations so the existing route handles them. + +The context owner centralizes exact scope/store resolution, desired policy, guarded IO and outer flight; engine/journal import it without circular imports. Scope includes a safe ownership-store root as well as the client file target. No writable legacy root may be shared across profiles. Domain errors carry safe code/status for route mapping; no manifest/session payload reaches diagnostics. + +## Implementation evidence and review scope + +`bun .tmp/aside-profiles/api-cli-probe.ts` passed against an isolated live HTTP management handler and actual CLI: three-profile bulk enable, individual-off after persisted reload/model selection, Undo followed by sync, unrelated settings and metadata privacy. Default unconfigured/disabled Aside now skips implicit fan-out before manifest/catalog discovery. + +This C4 backend layer is larger than the default review-size guideline because the new filesystem scope, one-owner store model, reversible desired state, and API/CLI consumers must be assessed as one complete contract; these are new cohesive modules with focused fixtures, not unrelated cleanup. UI implementation remains a separate dependent PR/cycle, and the original Grok work is already four separate reviewed PRs. + +## Coordinated client interface amendment + +CLI Aside refresh runs through POST /api/client-integrations/aside/sync on the live server, never through the local file writer; MCode/Pi keep their existing paths. Add a deterministic two-process CLI/server coordination regression. Dedicated primary profile paths are /aside/profiles (GET list, PUT bulk), /aside/profiles/ (GET/PUT one), /aside/profiles//journal (GET/DELETE) and /aside/profiles//restore (POST). CLI and new UI use these paths so unsupported old servers refuse rather than ignore a profile query. The new server may retain validated query compatibility, but Aside can never fall through to a legacy generic writer. Journal source availability and request selector consistency are part of the final regression matrix; detailed review synthesis stays ignored scratch. diff --git a/devlog/_plan/260906_aside_profiles/020_profiles_gui.md b/devlog/_plan/260906_aside_profiles/020_profiles_gui.md new file mode 100644 index 0000000000..974d91f335 --- /dev/null +++ b/devlog/_plan/260906_aside_profiles/020_profiles_gui.md @@ -0,0 +1,33 @@ +# 020 Aside GUI profile controls and terminal delivery + +Depends on 010 verified API and CLI. Class C3 UI with C4 backend unchanged. Goal: all discovered profiles are visible, bulk and individual switches operate on exact profiles, and existing history/restore is still usable. + +NEW gui/src/pages/integrations/AsideProfilesPage.tsx: useDataSurface GET profiles endpoint, existing Notice/Switch/ClientMark/IntegrationStateBadge. Global switch sets desired sync for all; rows show profile name or translated numeric fallback, current marker, actual state, independent switch, and details action. Single pending target serializes interactions consistently with backend. Switches read desired enabled; badges and applied/total count read actual file state. Show pending mismatch and per-profile refusal after partial failure, never optimistic applied success for siblings. A retry repeats the same desired action. Empty profile list prompts opening Aside; errors offer existing refresh action; inactive tabs do not fetch. A selected profile opens the existing FileIntegrationPage with profileId plus name and a back action; do not duplicate its rollback machinery. +MODIFY gui/src/pages/Integrations.tsx: Aside renders new page; remaining file clients stay on existing page. +NEW or MODIFY integration-api.ts profile contract/types and load function; optional profileId selects dedicated nested profile paths for state/toggle/history/restore/delete; old servers must refuse unsupported scoped mutations. Preserve old call signatures for other clients. Runtime response validation must accept only safe profile IDs and recognized IntegrationStatus states, and retain partial outcomes for UI display. +MODIFY FileIntegrationPage.tsx: optional profileId/profileLabel, read optional desired enabled on scoped status, include profile in every resource/cache/dependency key and every state/history/mutation call. MODIFY RestoreDialog.tsx if needed to pass profile scope through; rollback/delete remain on selected profile. +MODIFY styles-integrations.css: compact row layout using existing tokens; responsive wrapping for long labels/paths. No new color system or decorative assets. +MODIFY every gui/src/i18n locale module: profile list/title, sync-all, enabled count, current profile, details/back, empty, per-profile switch labels and partial failure copy. All visible text uses t/useT; names and numeric IDs are API metadata. +UPDATE guides/integrations.md and operating CLI docs with all-profile default, --profile examples, active-profile independence, per-profile exclusions and restart behavior. Translations must not contradict new all-profile behavior. + +Verification: remote focused GUI/API tests, GUI lint/i18n/build, root typecheck and required CI. Browser QA on local dev UI against three synthetic profiles, never real user profile mutation: initial mixed state, global enable, one profile disable, return to list after details, correct request selector, reload retains off state, failed profile does not imply sibling success, keyboard switches and narrow viewport. Capture actual screenshot for PR body using existing browser plugin, view it, and fix layout if needed. Screenshot contains synthetic labels only. A PR mentioning GUI includes screenshot. No local test suite or local typecheck; local dev server/browser probes are permitted. + +Terminal: verify every PR current head and all applicable hosted checks; native stack registration, owner-authorized admin merge, async merge completion, fetch dev and prove every merge SHA ancestry. Resolve CI or reviews rather than bypass evidence. No release or live service deployment. All original Grok/Pi/Codex and added Aside-profile criteria must be met before host goal completion. + +## P revalidation at1d4da9f9b + +Backend primary routes are now dedicated nested profile paths; server-only POST /aside/sync owns synchronization and preserves exclusions. The UI Sync-now button uses it, while the bulk switch sets all desired flags and per-row switches set one. Shared Switch already supports mixed state and aria-pressed. Preserve existing monochrome tokens/ClientMark (variance2/motion1/densityD5). StatusDTO carries desired enabled plus actual state, counts and per-profile errors; show partial/error states explicitly. FileIntegrationPage and RestoreDialog/Overview actions carry optional profile IDs into paths and cache keys. + +Main owns AsideProfilesPage, integration-api profile DTO/functions, parentpage/FileIntegrationPage/RestoreDialog/Overview wiring and CSS. A disjoint locale/test worker may own gui/src/i18n/{en,de,fr,ko,zh,zh-TW,ru,ja,tr}.ts and new gui/tests/aside-profiles-page.test.tsx after exactkeys/APIcontract are fixed. All GUI checks run remotely; local Vite and browser probes only. Capture wide+narrow realcomponent screenshots against synthetic three-profile management fixtures, not real accounts. + +A audit passed: DTO error precedes empty rendering; stale errors remain visible; always refetch after refused mutations because intent may already be saved; use mixed Switch and void refresh semantics. Exact locale keys are locked in ignored .tmp/aside-profiles/ui-keys.json. Main exports loadAsideProfiles and syncAsideProfiles from integration-api, and adds profileId as last optional argument to existing state/toggle/history/restore/delete functions. Toggle rejects207 partial only through returnedokfalse; UI reports it and refetches. + +Implementation modularization: profile DTO validation and load/sync readers live in new aside-profile-api.ts, reusing the existing integration transport/error owner without a circular re-export. Existing integration-api functions keep their non-Aside signatures and unscoped cache identities; profile scope is an optional final argument. Scoped successful state/toggle/restore/delete responses and journal rows must match the requested profile. + +Review fold-back: per-profile refusal/recovery outcomes remain typed and visible in bulk and Sync-now failures, including snapshotPath/residual; failed Aside restore reconciles owner resources while retaining the dialog/error; list and detail share one pure profile-status validator. Add a localized no-snapshot recovery warning and the corresponding regression cases. No source permission or confirmation boundary is weakened. + +B verification: the final interface provides bulk and individual desired-state switches, actual applied counts, per-profile retry/refusal and recovery details, profile-scoped history/restore/delete, and nine-locale copy. Remote checks passed: 74 interface/API cases, then 54 affected interface/cache cases after type narrowing, GUI build and both lint commands; 27 engine cases with real failed compensation, 62 CLI cases, and root typecheck. Initial failing compensation fixture was corrected to fail ownership after a successful file write, then fail rollback. Browser probes on three synthetic profiles covered bulk enable, exclusion persistence, scoped Undo, keyboard control, narrow layout without horizontal overflow, and per-profile external-edit refusal. Screenshot: docs-site/public/screenshots/aside-profiles.jpg. No real Aside profile files were written. Hosted exact-head CI and stack landing remain open. + +Hosted-review follow-up stays within terminal stabilization: Models selection/preset consumers must distinguish saved selection from refused client-file refresh. Add a persistent warning listing each failed client/profile and recovery details while retaining truthful selection-success feedback; clear it after a later successful refresh result. Failed HTTP saves never claim saved selection. Reuse shared refusal formatting and add nine-locale copy, focused interface cases and browser evidence. The ordinary owned-refresh producer also preserves backup/residual metadata. Native schema review follow-up uses structural object-key equality while retaining array order and true-conflict refusal; test both explicit and loaded duplicate declarations. These are review repairs to the existing stack slices, not new product scope. + +Final review verification: model-warning fixes passed 35 remote interface cases, build and lint; 80 schema/owned-refresh cases passed. Full remote interface suite passed 1,484 cases with one ownership-path copy failure; the missing per-profile path was restored in all nine locale sentences, and the unchanged five-case locale-parity file, build and i18n lint then passed. No test was weakened. Browser evidence confirms a saved selection with an independent affected-profile warning and clears the warning after a subsequent successful refresh. Independent reviews passed for the interface recovery, registry reconciliation, model-warning concurrency/fallback behavior and structural schema comparator. A complete source hash manifest matched 576e6b557 for the final root typecheck and privacy scan. Hosted exact-head checks and merge ancestry remain terminal evidence to capture in the session ledger. diff --git a/devlog/_plan/260906_grok_catalog_and_patch/000_research.md b/devlog/_plan/260906_grok_catalog_and_patch/000_research.md new file mode 100644 index 0000000000..7b97dc7bee --- /dev/null +++ b/devlog/_plan/260906_grok_catalog_and_patch/000_research.md @@ -0,0 +1,17 @@ +# Grok catalog selection and Codex patch parity + +Class C3, spec-satisfaction repair. The user clarified that filtering means enabled model visibility in Pi/Aside, not assistant output filtering. No output-filter changes are authorized by this unit. Prior Chat patch fixes must remain effective at every Codex Responses tool completion boundary. + +Scope: export catalog selection, refresh of already-owned Pi/Aside integrations, native Responses custom-tool repair, focused regression tests, matching docs. No live user config changes, deployment, release, unrelated cleanup, local test suites or local typecheck. Push --no-verify and admin merge are explicitly authorized. Probes and hosted CI are authorized. + +Evidence: src/clients/config-export/constants.ts selects openai-completions for Pi; Aside uses the same builder. src/server/management/model-rows.ts:218 filters disabled only. src/cli/opencode.ts:373 likewise ignores selectedModels. src/codex/catalog/provider-fetch.ts:1992 is canonical for allowlist plus disabled and pending selection. src/server/management/model-routes.ts visibility writes converge Codex only. Both explicit sync paths refresh only MCode among owned file integrations. Live read-only snapshot: /v1/models and existing Aside managed file currently contain xai/grok-4.6 only; do not claim that the live snapshot reproduced a full catalog leak. Synthetic allowlist and stale-owned-file scenarios will establish the gaps. + +Independent patch analysis: native custom exec is skipped by repairable||aliased in responses-custom-tool-repair.ts:206. Its item.done/input.done preserve raw patch while response.completed repairs it. Function apply_patch helper aliases can stream raw patch before compiling final JS. Existing arbitrary JS and foreign namespace boundaries stay byte-exact. + +Dependencies: 010 export selection -> 020 owned file convergence -> 030 Codex patch completion parity. Each has its own PABCD and reviewable PR. The patch layer is a separate user-requested stabilization concern published after catalog layers in the requested stack. + +Resource scope: existing GitHub repo credentials, read-only local configuration with no secret output; at most 24 synthetic live-provider calls, each <=120 seconds; six-hour execution window. No explicit token budget. Probe scripts stay ignored .tmp; public notes contain no credentials or private requests. DONE = regression probes, actual hosted exact-head test/typecheck CI, independent review, registered stack merged and fetched-dev ancestry. BLOCKED only for a persistent external dependency; no stopping on CI queueing. Each later P rechecks current source and carries earlier evidence. Escalation: reclaim failed delegated scope; new write delegation requires P amendment. + +## Baseline probes + +`bun .tmp/grok-stabilization/catalog-probe.ts` exit 0: management/CLI × pi/aside each emitted grok-4.3, grok-4.5, grok-4.6 despite selectedModels=[grok-4.6]; full management roster was three. `bun .tmp/grok-stabilization/patch-probe.ts` exit 0: native custom exec emitted one raw delta, input.done uncompiled and item.done uncompiled; function apply_patch alias emitted one raw preview despite compiled final. Both are observation probes before repair, not passing acceptance assertions. `python3 .tmp/grok-stabilization/verify-roadmap.py` exit 0 checks numbered roadmap artifacts and actual existing source target paths. No local suite or typecheck was run. diff --git a/devlog/_plan/260906_grok_catalog_and_patch/010_export_selection.md b/devlog/_plan/260906_grok_catalog_and_patch/010_export_selection.md new file mode 100644 index 0000000000..3b687e9b1c --- /dev/null +++ b/devlog/_plan/260906_grok_catalog_and_patch/010_export_selection.md @@ -0,0 +1,10 @@ +# 010 Export catalog visibility + +Loop: spec-satisfaction repair; trigger: selectedModels ignored by export projection. Goal: Pi/Aside export obeys the same provider allowlist, blocklist, pending selection as routed catalog. No change to full management catalog or routing authorization. + +MODIFY src/server/management/model-rows.ts: import filterCatalogVisibleModels. In loadExportModels compute visible routed row identities from filterCatalogVisibleModels(rows.filter(row => !row.native), config); return only !row.disabled and (row.native || visible set contains row), then map toExportModel. Preserve native visibility semantics. +MODIFY src/cli/opencode.ts: use same canonical filter once for rows with non-native provider/id identity, then exclude those not retained before seen.add. Do not infer provider identities for legacy rows missing them; keep existing disabled and Direct-native checks. Preserve order, custom/combo aliases and per-row metadata; do not duplicate allowlist matching. +MODIFY tests/server/management-client-config-route.test.ts and tests/cli/cli-export-command.test.ts: fixtures with xai selectedModels=[grok-4.6], full three-model roster, blocklist override, empty allowlist, slash-bearing ids, disabled duplicate. Render both pi and aside through production loader and CLI projection. A nonempty allowlist retains only selected IDs; a ready provider with an empty allowlist retains its full otherwise-visible roster; pending initial selection keeps routed rows hidden. Management still offers all IDs. CLI consumers must reload their configured state after discovery because the request can persist the initial selection. +MODIFY docs-site/src/content/docs/guides/integrations.md: explain selected list applies to generated catalogs. + +Verifier: standalone synthetic imports of loadExportModels/exportModelsFromProxyRows plus Pi/Aside serializers; no bun:test. CI runs existing focused regressions, typecheck and full platform suite. Before C record exact source SHA and probe output. Stop after export boundaries agree; next cycle refreshes old owned files. diff --git a/devlog/_plan/260906_grok_catalog_and_patch/020_owned_refresh.md b/devlog/_plan/260906_grok_catalog_and_patch/020_owned_refresh.md new file mode 100644 index 0000000000..8ae79ae938 --- /dev/null +++ b/devlog/_plan/260906_grok_catalog_and_patch/020_owned_refresh.md @@ -0,0 +1,27 @@ +# 020 Converge already-owned Pi/Aside catalogs + +Depends on 010 filtered loader. Loop spec-satisfaction repair. Goal: a model visibility/selection change and explicit sync refresh existing connected Pi/Aside files. No adoption of unowned/manual files, no recreation of removed blocks, no override of drift. + +NEW src/integrations/catalog-refresh.ts: bounded helper refreshOwnedCatalogIntegrations(input, clientIds) defaults clientIds to [pi, aside]; callers may supply an explicit list including mcode. It passes a lazy cached models loader to refreshOwnedIntegration, catches per-client errors and returns existing outcome shape. Use existing ownership store, mutation flight and coordinated writer; never bypass fingerprints. The later Aside-profile layer delegates Aside to its server-owned profile engine; direct CLI sync passes [mcode, pi] here and invokes the Aside server helper once separately, including an explicit unavailable-server diagnostic. +MODIFY src/server/management/model-routes.ts: local async convergence helper calls existing convergeCodexCatalog then new owned refresh for pi/aside with port from URL/config and lazy loadExportModels(config); attach clientIntegrations outcome to disabled-models, model-visibility, selected-models and model-preset writes. Keep successful config persistence even when one file refuses refresh; return warning outcome. +MODIFY src/server/management/config-routes.ts and src/cli/dispatch.ts: expand current MCode-only owned refresh to mcode/pi/aside via helper; preserve native Grok/Desktop gates and refused-sync behavior. +MODIFY existing tests/clients/sync-client-integrations.test.ts and tests/server/management-integration-routes.test.ts: fake IO/store or isolated home seeds owned pi/aside with two models, refresh with selected one, assert hidden row removed and other provider fields preserved. Prove unowned, removed and drifted configs untouched; one failure does not block other client. Add route-driven visibility refresh coverage using injected convergence. +UPDATE structure/09_client-integrations.md and owning docs page with ownership/refusal semantics. + +Verification: standalone isolated writer probe using synthetic models and temp homes, then exact-head hosted CI. C4 care for automatic owned-file writes: independent review must confirm ownership/no-clobber and per-client failure boundaries. Final enforcement is existing coordinated writer; refresh helper is an early caller, not a permission boundary. Known bypass: manually calling writer with explicit adoption; no such call in this unit. Stop when file projection converges or produces truthful refusal. + +## Audit amendment: overlapping refreshes + +The existing constant refresh mutation-flight key incorrectly joins different model selections. MODIFY src/integrations/owned-refresh.ts to use a unique per-refresh operation key (crypto.randomUUID), making overlapping refreshes explicitly busy rather than reporting another desired catalog as success. Implicit refresh never joins an explicit HTTP mutation. Add controlled overlap with distinct old/new rosters: second call reports integration_mutation_busy; first result describes only its own write. Subsequent retry applies the new roster. Return per-client failures; never retry stale snapshots automatically. + +Add a ManagementApiDeps refreshOwnedCatalogIntegrations seam for route verification, defaulting to the real helper. Creation: exported helper/deps type; consumption: model routes and explicit sync. No serialization/deserialization: runtime-only dependency injection. Tests use fake IO/store or temporary home, never actual user-owned files. + +## P revalidation and implementation interface + +010 b8010aebd passes the four standalone visibility probes and source review; all original hosted-CI/merge criteria are retained under the terminal stack cycle, not marked complete. Helper signature: refreshOwnedCatalogIntegrations(input: Omit, clientIds: readonly IntegrationClientId[] = ["pi", "aside"]): Promise. Memoize the lazy model load per fan-out; no owned record means no catalog load. Catch and redact each failure. Explicit sync passes [mcode,pi,aside]. Visibility routes attach both catalogRefresh and clientIntegrations; native Codex failure does not undo an already persisted selection. + +Delegate tests only to one worker: tests/clients/sync-client-integrations.test.ts owns helper refresh+overlap coverage; main owns implementation and route regression tests. The worker has no production writes, suite execution, FSM or git mutations. + +## Implementation audit synthesis + +Averroes found an indirect source-oracle dependency: codex-convergence-contract.test.ts counts direct convergence calls and two preset calls. The shared visibility helper changes direct count but preserves fourteen logical paths. Update the inventory to subtract the helper definition and add its five callers, assert exactly one Codex convergence inside the helper, and preserve the marker-only custom preset negative. Run that affected file remotely in addition to the writer/route tests. No runtime blockers in the ownership audit. diff --git a/devlog/_plan/260906_grok_catalog_and_patch/030_responses_patch.md b/devlog/_plan/260906_grok_catalog_and_patch/030_responses_patch.md new file mode 100644 index 0000000000..a7778f6dc0 --- /dev/null +++ b/devlog/_plan/260906_grok_catalog_and_patch/030_responses_patch.md @@ -0,0 +1,21 @@ +# 030 Codex native Responses patch completion parity + +Depends on recorded export layers for stack delivery; runtime independent. Class C3, spec-satisfaction repair. Goal: same repaired executable input at deltas/input.done/item.done/response.completed for complete patches misrouted as exec. No arbitrary JavaScript rewriting. + +MODIFY src/server/responses-custom-tool-repair.ts: register same-name routed custom calls in addition to aliases. Track original wire name and target; hold custom input deltas when code-mode exec may be a patch envelope or when helper alias requires compilation. Accumulate under TranslatorBudget, release on done/dispose. Run restoreRoutedCustomCalls for same-name custom items, and use existing resolveCodeModeHelperName/compileCodeModeHelperInput at input.done. Do not place exec in repairNames. Preserve ordinary JavaScript streaming where monotonic; once raw prefix would diverge, withhold to authoritative completion. Suppress function helper-alias progressive previews rather than emitting raw patch before compiled JS. +MODIFY tests/responses/responses-custom-tool-repair.test.ts: native custom exec raw/wrapped complete patch, fragmented marker, input.done and output_item.done plus terminal snapshots; function apply_patch wrapper alias; invalid/incomplete envelopes and valid JS remain exact; flat catalogs and foreign namespaces do not retarget; cancellation frees retained buffers. +UPDATE existing patch compatibility docs and structure/11_compatibility-contracts.md to describe completion-boundary parity. + +Verifier: pure standalone synthetic SSE-block imports, compare outputs at each lifecycle edge and execute generated JS against a recording tools.apply_patch stub (no filesystem writes). Probe must assert monotonic preview or held preview, one call, exact canonical patch data. CI runs added regressions and existing bridge/native compatibility tests plus full suite/typecheck. Complete only after independent review and exact-head CI; register requested stack and admin merge after verified heads. Fetch dev and prove every merge SHA ancestor. D records parity inventory and public PR links. + +## Audit amendment + +Executable repair is limited to authorized code-mode exec and recognized helper aliases; unrelated same-name native custom tools keep raw input byte-for-byte. Explicit negative: render_diagram input JSON string {"input":"literal"} is not unwrapped. Separate scenarios cover missing input.done, terminal-only completion, failed/incomplete after held deltas, and disposal. Authoritative completion wins over previews. Failure never synthesizes successful completion. All retained buffers release. One simulated execution means choose the client-consumed completed item once, not execute every redundant lifecycle representation. + +## P revalidation + +Consume 020 ff388977a with isolated route/writer proof and remote75+19 tests. 030 remains scoped to the two patch lifecycle gaps. Append040 for independently confirmed ordinary function and dotted-namespace parity gaps; all terminal CI/merge obligations move there unchanged. Main owns production030; a disjoint worker may add tests only in tests/responses/responses-custom-tool-repair.test.ts. Native code-mode exec previews may be held until final when they could be complete raw/wrapped envelopes; unrelated native custom JSON bodies remain raw. + +## Implementation audit synthesis + +A fragmented pretty JSON wrapper beginning with brace-newline escaped the compact-prefix guard, so preview bytes could contradict compiled completion. The completion parser accepts arbitrary whitespace, escaped property names and property order; native exec now conservatively holds all object-leading inputs to completion. Ordinary JavaScript stays byte-exact, though a block-leading program waits for completion. Added per-character pretty-wrapper and escaped-key regressions. diff --git a/devlog/_plan/260906_grok_catalog_and_patch/040_native_tool_parity.md b/devlog/_plan/260906_grok_catalog_and_patch/040_native_tool_parity.md new file mode 100644 index 0000000000..8d85a71fa0 --- /dev/null +++ b/devlog/_plan/260906_grok_catalog_and_patch/040_native_tool_parity.md @@ -0,0 +1,30 @@ +# 040 Native function and namespace parity + +Depends on 030 custom-call restoration. Class C3 spec-satisfaction repair. User requested all Chat-era tool repairs be checked. Independent source/probe inventory finds native ordinary calls retain integer-as-float and numeric-as-string mismatches, completed empty arguments, and dotted namespace names that bridge already repairs. These are in scope; assistant output filtering is not. + +NEW src/responses/function-call-compat.ts: collect original current-turn ordinary function declarations using collectResponsesToolGroups, preserving namespace/kind and original parameter schema. Lookup exact declared identity, including reserved functions children as bare and authorized canonical namespace aliases. Do not consume historical-only declarations or provider-normalized schemas. Pure completed-item transform calls coerceIntegerToolArguments(raw||"{}", original.parameters, original.namespace ? undefined : original.name). Only explicit completed empty payload becomes {}; unknown/missing, malformed nonempty, fractions, numeric unions, unsafe integers, custom/hosted/helper calls remain unchanged. +NEW src/server/responses-function-tool-repair.ts: SseBlockRewrite tracks item identity by item_id/output_index and uses same pure completion transform at arguments.done, item.done and terminal snapshots. Preserve in-progress placeholders. Budget any buffered data and release on done/terminal/dispose. Original declaration is authority; attempt wire alias is transport spelling only. Final representations must agree and failed/incomplete never synthesize successful executable input. Revalidate whether delta holding is necessary against existing bridge closeCurrentToolCall (which already repairs authoritative final arguments after streamed numeric previews); use one compatible completion contract rather than introducing arbitrary JSON rewriting. +MODIFY src/server/responses/core.ts: derive ordinary schemas from currentTurnWireToolCatalogBody before lowering; compose native function repair after namespace/custom restores and before undeclared guard. Apply pure repair in JSON, SSE final snapshots, bounded JSON-to-SSE, and rememberPassthroughResponseChecked so client and replay state agree. Canonical forward auth remains byte-pass-through. Rebuild only attempt-specific aliases on retries. +MODIFY src/responses/namespace-tool-compat.ts and, if needed, responses-undeclared-tool-guard.ts: reuse existing collectAmbiguousDottedAliases ownership algorithm rather than duplicate. Add unambiguous dotted aliases after canonical authorization, collisions computed from whole original current-turn catalog including bare spellings before selection. Never reinterpret explicit conflicting namespaces or different kinds; canonical identities retain precedence. +MODIFY existing native Responses repair and namespace tests, or register new domain tests in both layout manifests: integer/string and no-arg scenarios at JSON/each SSE completion/replay, namespace wait exception boundaries, same-inner-name schemas, forbidden selectors/replay-only names, early/interleaved events, terminal/dispose cleanup, dotted collision order independence, unchanged030 code/patch semantics. +UPDATE structure/11_compatibility-contracts.md and guides/codex-integration.md with completion parity boundary and inventory. + +C: standalone synthetic imports with stub tools (no real execution) and remote focused tests/typecheck; all stack PR exact-head hosted CI must pass before merge. Keep original pi-filter/owned-refresh/responses-patch terminal criteria unchanged and satisfy them at final D with PR heads/CI/merge ancestry. Register native GitHub stack, merge approved prefix using async REST and SHA guard, wait for actual merged status, fetch dev and prove all merged SHAs ancestors. No release/deploy/local suites. Stop only verified DONE or actual external blocking evidence. Resource bounds inherited from000. + +## P revalidation at b477b731e + +030 lifecycle+raw payload boundaries are independently reviewed and55 remote tests pass. The existing bridge streams ordinary function argument previews then uses coerceIntegerToolArguments at authoritative arguments.done/item.done;040 mirrors that contract. Unlike executable exec source compilation, numeric representation repair does not require withholding previews. Do not synthesize corrected deltas; correct the authoritative completion events and JSON snapshots, and verify downstream Chat collector consumes those finals. Buffer only early identity-less completion events if correlation needs them; all retention remains budgeted. + +Implementation interfaces locked for disjoint delegation: function-call-compat.ts exports collectFunctionCallRepairSchemas(body), repairFunctionCalls(value, schemas): {value,changed}, repairFunctionCallsInJson(text,schemas). responses-function-tool-repair.ts exports createResponsesFunctionToolRepairBlockRewrite(schemas,budget?). The collector reads only original current-turn ordinary declarations and honors original function-kind/namespace selector restrictions; it can reuse pure namespace lowering to resolve selectors while preserving original schema values. Native forward routing receives an empty repair map. Empty ordinary completed arguments become{}; custom/native wrappers never enter. + +Main owns namespace-tool-compat.ts, extraction of existing ambiguity helpers to new responses/tool-name-aliases.ts, guard import updates, core integration, namespace tests, layout registration, docs. Worker owns only the two new function repair modules and one new tests/responses/responses-function-tool-repair.test.ts. No shared write paths, no worker commits/FSM/local suites. Core captures schemas after successful adapter buildRequest from the previously captured clientToolAuthorizationBody, then uses same pure repair in remembered continuation and clientJSON, and block repair after custom/tool-search restores before final declaration guard. Each attempt receives fresh block state. + +## Implementation observations + +Namespace aliases are built after custom lowering (openai-responses.ts2410-2432), so an original custom tool can carry lowered kind=function. Preserve that existing namespace restoration kind behavior; only original-schema function repair enforces ordinary function kind. Dotted restoration adds spelling parity, not a new kind conversion. Explicit conflicting namespaces stay untouched. Reserved functions children participate in the shared collision inventory as bare names. The existing namespace tests are updated for additional alias entries rather than weakening their authorization assertions. + +Review-size exception: keep original-schema collection, native SSE/JSON/replay wiring and their end-to-end regressions in one layer because they jointly define the completion contract. Roughly half the added lines are focused regressions; the alias inventory is moved, not reimplemented. Prior catalog and patch concerns are already separate PRs. Additional Aside profile work remains separate future cycles. + +## Review synthesis, round1 + +Accept three medium findings: (1) sparse JSON receives inferred completion status after the new repair, so normalize snapshot/required fields before function repair and reuse that normalization for stored replay; (2) an index-only early completion can be correlated but still lacks item_id, so attach the known id even if arguments stay unchanged; (3) current-turn tool_search_output declarations are promoted by the adapter but absent from the original-schema collector, so include their original definitions in collector/selector resolution after the replay-prefix cut. Do not broaden collectResponsesToolGroups globally or include historical loaded declarations. Main owns normalization order/replay regression; existing worker owns early-frame id and loaded-declaration collector fixes/tests. Original authorization and preservation constraints remain. diff --git a/devlog/_plan/260906_key_login_ci_timeout/000_plan.md b/devlog/_plan/260906_key_login_ci_timeout/000_plan.md new file mode 100644 index 0000000000..6e43cf1a09 --- /dev/null +++ b/devlog/_plan/260906_key_login_ci_timeout/000_plan.md @@ -0,0 +1,31 @@ +# 000 — Diagnose the key-login live-update CI timeout + +One focused PABCD repair cycle. Baseline dev `922bfa653a013647881316f3d95f0631a87acb10` differs from failed CI head `73190c20443876fe1dbf4e9dde5d25644e48e71a` only by the previous lane's outcome record. + +## Evidence and outcome + +Public CI run 33999342751, job 101395411095, failed `tests/oauth/key-login-live-update.test.ts:61` after 15,046ms against a 15,000ms test budget. The shard finished 9,470 pass, 7 skip, 1 fail. The log records server startup but no failing assertion or awaited-operation trace. Other execution jobs passed; Windows six-shard tests were intentionally skipped by the push workflow. + +Keep the same disk/live modelCosts and rotated-key assertions. Locate the wait before changing code. A passing retry alone does not explain the failure. + +## Investigation and conditional change map + +- Read `tests/oauth/key-login-live-update.test.ts` and instrument its asynchronous boundaries only in remote scratch: key-login commit, management read and server stop. +- Trace `src/oauth/login-cli.ts` notify, `src/server/local-provider-reload-client.ts` request, `src/server/management/provider-routes.ts` reload validation/convergence, and the server's shutdown hooks. Preserve every admission predicate. +- The fixture currently installs the real Umans hostname both before start and through the replacement preset. Check whether DNS/network dependence causes the observed wait. If established, modify only the test fixture: use a synthetic controlled destination via the existing baseUrl override, preserve real local attestation/reload/convergence, assert the reload outcome, and clean owned resources. No timeout extension, skip or mock of the operation under test. +- If the wait is a production lifecycle defect instead, amend this plan with the observed boundary and smallest production correction before B. Do not introduce speculative cancellation or security changes. +- Put the final evidence and failure disposition in `010_outcome.md`; unpublished security findings, if any, remain in ignored scratch. + +## Execution and acceptance + +Class C2 for a hermetic fixture correction; promote to C4 with independent security review if executed auth/admission logic must change. Main owns refs/PR/FSM; one remote worker owns serialized macOS reproduction under the shared test-user lock, and an independent reviewer owns static analysis. Inherit the parent model. No local suite, typecheck, build or hooks. Remote pinned Bun 1.4.0 and synthetic fixtures only; do not access personal accounts/services. No requested token/cost budget; six-hour checkpoint, not an automatic success condition. + +Required evidence: original failure and causal trace, focused remote original/fixed comparison, original assertions intact, relevant adjacent tests and typecheck, independent review, current-head hosted CI, admin merge and actual dev ancestry. Follow final dev CI for this repair. Existing user push/admin authorization applies; every push uses --no-verify. No release, deployment, global relink, integration-branch direct push or changes to another lane's jobs. + +DONE requires those actual outcomes. NOOP needs proof current dev already resolves the failure. Unknown cause, an expired wait, and a red CI are not completion. Append a separate cycle only if a distinct necessary repair appears. + +## A evidence amendment: controlled failure path + +Remote pinned-Bun macOS reproduction: original fixture passes in 95.42ms with controlled outbound HTTP. Delaying only the real Umans DNS answer gives reload transport-unavailable at ~10.28s, successful local config GET at ~10.29s, then `server.stop(true)` begins and remains unsettled until the original 15s test timeout (15,008.86ms). The trace identifies `providerDestinationResolvedError` as the DNS caller. This reproduces the CI timeout shape; the uninstrumented historical CI log still does not prove which external delay occurred there. + +Selected change is test-only: an owned literal-loopback upstream in beforeEach with a deterministic catalog response, `umansKeyConfig(baseUrl, port)` using explicit private-network opt-in, the existing key-provider constructor's URL override plus private-network opt-in on the replacement row, and awaited upstream teardown after the proxy. Preserve all four original assertions and the 15s ceiling; additionally assert reload outcome is `reloaded` and config GET is HTTP200. No production auth, destination validation, transport or shutdown change. Focused remote gate covers this test, key-login overlay merge, OAuth live update, local reload client and direct transport, plus root typecheck. A controlled delayed-DNS run must remain green with zero DNS calls from the fixed fixture. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/000_plan.md b/devlog/_plan/260906_lane_b_catalog_stack/000_plan.md new file mode 100644 index 0000000000..d2be407c30 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/000_plan.md @@ -0,0 +1,65 @@ +# Lane B catalog carry roadmap + +## Loop specification + +- Archetype: spec-satisfaction repair and attributable integration. +- Trigger: owner assigned catalog lane B and authorized stacked PRs, no-verify pushes, merges and immediate closure of completed source work. +- Goal: manual OpenAI visibility, persistent context limits, Go effort/ordering, provider model management and Fable 1M selectors work together on dev. +- Non-goals: other lanes, release/main/preview promotion, deployment, global proxy/config changes, new dependencies, broad cleanup. +- Tool/credential scope: git and authenticated GitHub CLI for this repository; inherited-model subagents; read-only local inspection; isolated QA or remote checks only when needed. +- Write scope: this unit, the exact source-PR files named by each decade plan, necessary focused regression/SoT follow-ups, and ignored scratch/evidence. Preserve peer changes. +- Resource policy: user authorized inherited parallel agents without a numeric cap. No imposed token/cost limit. Six-hour work-phase checkpoint; a reached bound is reported honestly, never as success. Context compaction only checkpoints work. +- Verifier: current-head Cross-platform CI, GUI tests/lint/build and privacy checks from repository CI; independent diff review; GUI observation where rendering changed; git ancestry and attribution checks. Local tests, suites, typechecks and builds are prohibited for this run. +- Stop: all five outcomes verified on dev, replacements merged, original PRs closed and fully resolved issues closed. +- Memory artifact: this unit plus the session-bound goalplan; volatile source/review/CI snapshots in `.tmp/lane-b/`. +- Outcomes: DONE after proof; NOOP only with current-code proof; external BLOCKED/UNSAFE/NEEDS_HUMAN requires evidence and no other authorized progress. Pending CI is continuing work. +- Escalation up: main reclaims a packet after two distinct agents fail it. Down: delegate only explicit bounded tasks recorded at P; no speculative implementation of a later phase. + +## Current tree and source anchors + +Initial dev is `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. The managed checkout stays in place and is adopted as `codex/lane-b-01-visibility`. + +| Phase | Source PR/head | Contract | Branch plan | +|---|---|---|---| +| roadmap | current dev | lock these documents only | base visibility branch | +| visibility / 010 | #3653 / `956eedac439922cf7645f130ef8432833e813a9a` | distinguish native and configured manual rows | `codex/lane-b-01-visibility`, base dev | +| context / 020 | #3654 / `8facdb0d8c10109701015c0f6109fc67b1d9dd3c` | preserve selection independently of enabled state | `codex/lane-b-02-context`, base 01 | +| ordering / 030 | #3571 / `0a935c5694229760c8c1cd5a62072107d8ae6696` | separate picker/spawn rank and exact efforts | `codex/lane-b-03-ordering`, base 02 | +| management / 040 | #3659 / `ff4e5cd5352b9c1bd05e3de0091f3483ca130be5` | consume visibility contract for hide/delete and static sync | `codex/lane-b-04-management`, base 03 | +| fable / 050 | #3649 / `95becce94255982667cef10308806770d49cc05b` | preserve 1M selector and canonical upstream route | `codex/lane-b-05-fable`, base 04 | +| landing / 060 | all verified replacement heads | bottom-up dev integration and closure | retain parent refs until child retarget | + +The owner explicitly requests stacked PR delivery. Context and ordering share persisted catalog configuration; model management consumes the visibility and catalog contracts. Fable is functionally independent and placed last only to satisfy the requested stack delivery; no runtime dependency is claimed. One work-phase is one full PABCD cycle; each implementation phase is verified before the next. + +## Existing owners and SoT + +Runtime management lives in `src/server/management/`, catalog publication in `src/codex/catalog/`, persistence in `src/config.ts` and `src/providers/context-cap.ts`, dashboard rows in `gui/src/models-groups.ts`, provider workspace in `gui/src/components/provider-workspace/`. Focused tests remain in domain directories; new files update both test-layout manifests. Read nested AGENTS before changes. + +SoT synchronization targets are `structure/02_config-and-codex-home.md` (context persistence), `structure/03_catalog-and-subagents.md` (efforts and ordering), and `structure/05_gui-and-management-api.md` (visibility and model operations), plus the source PR's public documentation. Add narrow contract notes only when existing text would otherwise be incomplete or contradictory. + +## Verification execution policy + +`.github/workflows/ci.yml:7` accepts all PR bases, including open stack heads. Its `changes` filter controls actual test execution; a green aggregate with skipped test jobs is insufficient. `workflow_dispatch` supports all lanes. Inspect each actual run's head SHA, event, test jobs and conclusions. Author-reported historical test counts do not certify a carry head. + +`git diff --check` and a Python document-completeness checker are documentation/static artifact checks, not repository test execution. These are the only local checks in the docs-only cycle. Implementation C receipts invoke a read-only GitHub evidence verifier that asserts the actual checked-out SHA and successful test jobs; the verifier never starts local tests. Screenshot paths already in original PRs preserve author evidence; rendering changes require an actual observation of the carried state or an explicitly identified outstanding gate. + +## Attribution and publication + +Carry source non-merge commits with `git cherry-pick -x` when compatible; otherwise apply the exact merge-base diff preserving binaries and create a scoped commit with actual source-author `Co-authored-by` trailers. Keep source PR/head references in every replacement description. Do not cherry-pick upstream merge commits as new feature content. Every push uses `git push --no-verify`; no direct dev pushes or contributor-branch rewrites. + +GitHub operations stay sequential. Bottom-up merge commits preserve ancestry; if squash is required, restack the children immediately and revalidate. Before merging, inspect current head, exact-head CI, outstanding reviews and any peer dev drift. Preserve all author trailers. Close source PRs as superseded and issues #3650/#3651 as completed only after their replacement is reachable from dev and solves the full report. + +## Shared surfaces + +- A #3679 and B #3654 share `src/config.ts`; B reconciles both independent field additions. +- D #3625 and B #3659 share locale modules; retain all keys. +- D #3646 and B #3649 share Claude alias routing; D owns hub alias resolution, B owns Fable native selector round-trip. +- A #3568 and B #3571 share layout manifests and provider docs; preserve both additions. + +Independent review findings involving security stay in ignored scratch space. Public plans describe the already-public source changes, never unpublished vulnerability analysis. + +## Owner steering and verification checkpoints + +The owner explicitly authorized admin merges during execution. Once a child PR is open, land a verified parent with `--admin --merge`, prove dev ancestry and immediately close its completed source work; retain/retarget the child before any parent-ref cleanup. The final landing cycle reconciles all outcomes rather than delaying every already-ready parent until the end. + +An implementation preparation cycle may close after the exact-head functional CI jobs (Linux/macOS full tests, typecheck, GUI tests and privacy), independent review and applicable remote GUI/docs checks pass. Queued aggregate packaging/keyring jobs remain explicit PR merge gates; do not claim them passed or merge before resolving required checks. This allows the next stack layer to be prepared while ancillary jobs queue, without weakening final verification or source-closure requirements. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/009_roadmap_lock.md b/devlog/_plan/260906_lane_b_catalog_stack/009_roadmap_lock.md new file mode 100644 index 0000000000..f5c1ce2a2b --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/009_roadmap_lock.md @@ -0,0 +1,5 @@ +# Roadmap lock + +The seven numbered roadmap documents passed independent read-only audit. The roadmap cycle produced documentation only. Original final binary diffs are pinned in scratch for attributable carry. GUI and docs observation will use an isolated remote checkout; macmini-cf has Bun 1.3.14. Ordinary PR CI proves Linux/macOS and quality gates, and the final stack receives an explicit Windows all-lane run. No local repository tests, typecheck or builds were run. + +Next cycle: 010 visibility. Carry the final #3653 diff rather than only its early commits, preserving the final Fast-row assertions and original PNG. Add the planned mixed-group and client-export behavior coverage, obtain independent management-boundary review and verify the carry head in CI. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/010_visibility.md b/devlog/_plan/260906_lane_b_catalog_stack/010_visibility.md new file mode 100644 index 0000000000..3577713269 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/010_visibility.md @@ -0,0 +1,298 @@ +# 010 — Manual OpenAI visibility and replacement rows + +Status: planned; source-inspection only. Research captured 2026-09-05T16:34:39.759005+00:00. Local anchor: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. Source: [PR #3653](https://github.com/lidge-jun/opencodex/pull/3653), issue [#3650](https://github.com/lidge-jun/opencodex/issues/3650). Source head `956eedac439922cf7645f130ef8432833e813a9a`, source base `0b7f60ee259bdd0e5c68b62936fe153af151e9dd`. Live GraphQL confirmed this head remains OPEN with zero unresolved threads on 2026-09-05 UTC (2026-09-06 KST). + +## Execution contract + +This is an implementation design for one later PABCD cycle, not an implementation receipt. The main agent owns the FSM, host goal, 000 roadmap, branch stack, publication, and merge. This delegated research made no production edits, ran no local tests/typecheck/build, and changed no refs. + +Loop archetype: spec-satisfaction repair. Trigger: the linked public issue and source PR. Verifier: exact carried-head GitHub CI plus targeted behavior evidence below. Stop: all activation scenarios accounted for, CI producers successful, author attribution retained, and merge commit proven reachable from dev. Expected outcomes: DONE after that evidence, NOOP only if current dev already implements the same behavior; otherwise retain explicit BLOCKED/NEEDS_HUMAN evidence without claiming completion. Upward escalation: main reclaims a slice after two distinct agents fail its packet; downward delegation requires a P-phase amendment. Resource and credential limits inherit the main lane-B 000 plan; this document authorizes no independent goal, workflow dispatch, deployment, or account change. + +The future executor must re-read the nearest src/GUI/docs AGENTS before code changes. No local tests, suites, typecheck, lint, builds, or dependency installation: CI is the execution verifier. Read-only `git apply --check` below checks textual portability only, not correctness. + +## CI evidence contract + +Source inspection at the recorded local HEAD establishes coverage, not passing execution: + +- `.github/workflows/ci.yml:182-201` selects runtime/tests/GUI changes; both source diffs select `ci` and `gui`. +- `ci.yml:255-316`: four Linux shards invoke `bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD"`. `scripts/ci/run-bun-test-batches.sh:46-64,196-204` enumerates test files and excludes only the dedicated storage/API-usage families; the targeted files below are included. +- `ci.yml:422-428`: root TypeScript checks and `cd gui && bun test --isolate tests`; GUI lint/build at 416-420 and 442-446, privacy scan at 430-431. These are CI commands, never instructions to run locally. +- macOS test execution is at `ci.yml:532`; Windows is **dispatch-only** at `ci.yml:661-662` and runs six shards at 754. A normal PR check does not prove Windows test execution. Main must obtain appropriate exact-head dispatch evidence before claiming three-platform coverage. +- `ci.yml:917-933` permits skipped producers. Inspect actual test/gates job results and tested commit (including the PR merge ref and its head parent), not just aggregate `ci=success`. +- `.github/workflows/react-doctor.yml:15-18,46-57` scans PR changes in `gui` and blocks warnings. It is an additional review gate, not a replacement for GUI tests. +- `.github/workflows/deploy-docs.yml:3-10,25-32` builds docs only on main push or dispatch; normal PR CI has no Astro-docs build. Do not dispatch a deploying workflow merely to get validation. Main must arrange non-deploy hosted docs-build evidence or explicitly retain that verification gap. Local builds remain prohibited. + +Before merging a carried layer, refresh source/head/base/review status, inspect exact-head CI jobs and remaining findings, preserve coauthor credit through squash, and verify the resulting merge SHA is an ancestor of fetched dev. Only then close the superseded source PR and its resolved issue; a source PR carried through another PR is not automatically merged/closed. Retarget/rebase the next child onto dev after its parent lands; do not delete a parent branch while an open child still targets it. These are main-owned future actions. + +## Scope and caller proof + +C3 product slice, with the existing management validation boundary retained for independent review. Outcome: manually configured `openai/gpt-5.5` can be toggled without HTTP 400; it replaces the matching bare dashboard row, while account-qualified native rows and native provider controls survive. No route renaming, entitlement change, catalog order redesign, model deletion API, or runtime transport change. + +Current `src/server/management/model-routes.ts:567-570` rejects every non-native OpenAI target. `gui/src/pages/Models.tsx:1463` sends the actual row's `native` flag; group visibility at 1218 sends mixed native/manual targets. `gui/src/model-visibility.ts:58-70` serializes these unchanged. Reuse this caller and existing atomic visibility handler, rather than adding an endpoint. + +Current `src/server/management/model-rows.ts:125-172` deduplicates routed custom rows but concatenates all native rows. Add bare-native filtering after `visibleCustomModels`/`customNamespaced` are known, before Fast-row metadata at 173-184. `loadExportModels` at 214-216 consumes the same rows and removes disabled entries, so client-config export needs explicit regression coverage. `model-routes.ts:357` returns these rows; `model-routes.ts:490-510` serializes client config via the existing export path. `gui/src/models-groups.ts:84` loses native controls when all visible rows are manual; derive `nativeProviderGroup` also from configured canonical OpenAI `authMode: forward`, while leaving `native` false for manual-only groups. + +## Exact source carry map + +| Operation | Path | +|---|---| +| NEW | `docs-site/public/screenshots/manual-openai-model-toggle.png` | +| MODIFY | `docs-site/src/content/docs/reference/management-api.md` | +| MODIFY | `gui/src/models-groups.ts` | +| MODIFY | `gui/tests/models-native-group-controls.test.ts` | +| MODIFY | `src/server/management/model-routes.ts` | +| MODIFY | `src/server/management/model-rows.ts` | +| MODIFY | `tests/codex-integration/model-visibility-management-api.test.ts` | + +All six textual files were reviewed, including all test hunks. The PNG is accounted for as a binary evidence asset: blob identity verified, pixels not inspected in this docs-only pass. Existing modules and test files are reused; no new runtime abstraction or test manifest entry is needed. + +### Carry method and attribution + +Prefer a **base-to-final-head diff port**. The source contains merge commits, and the last one resolved `model-rows.ts` against Fast metadata. Applying only the original feature commit loses the final regression assertions. Actual `git show -s` commit metadata identifies **Robin Bially <7304732+RobinBially@users.noreply.github.com>** on: + +- `7c5b4d918401d086dc633ab941be1ff9f844b13e`: original feature. +- `6c1b8b2d2f0abc4baa6618cea2e485374dda2aeb`: account-qualified and pending-selection regression additions. +- `956eedac439922cf7645f130ef8432833e813a9a`: final merge resolution, parents `e520ee5e437e6fc1d8482f51950722db9b58049a` and the source base above; adds Fast availability assertions in the existing test. + +Use `Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com>` in the carry commit and squash description. Do not blindly cherry-pick merge commits with `-m`. A selective cherry-pick is possible only if the executor separately ports the final merge resolution and compares resulting source delta to the final PR diff. + +Read-only `git apply --check --exclude='*.png' .tmp/lane-b/3653.patch` returned 0 against the recorded HEAD. Cached patch and `git diff ` match after normalizing Git's abbreviated index-hash lines. The patch has no binary payload; the future executor must retrieve `docs-site/public/screenshots/manual-openai-model-toggle.png` from the pinned source commit, blob `d8a0dab0de58bdfee4764341465eeff6a41b4dec`, or capture a replacement from the carried-head UI. + +## Implementation sequence within this phase + +1. Port the validation hunk without moving existing malformed-request or initial-selection-pending checks (`model-routes.ts:531-542`). Preserve mixed native/routed key handling at 593-635 and catalog convergence. +2. Port bare-native row filtering before current Fast annotations; retain combo precedence, account-qualified IDs and export metadata. +3. Port canonical forward-OpenAI grouping and all source regression tests. +4. Update public API documentation and attach honest UI evidence. Update the translated API error rows listed below so they do not imply that 400 is the only rejection contract. +5. Obtain hosted CI and independent review; then main performs authorized stack merge/issue closure. + +## Activation and regression matrix + +| Scenario / trigger | Observable proof | Owning test/evidence | +|---|---|---| +| Configured manual OpenAI row, `native:false`, enable then disable | 200; namespaced disabled key changes, native key preserved | source-added `tests/codex-integration/model-visibility-management-api.test.ts` | +| Unconfigured routed OpenAI or unsupported native target | 400, no config mutation | same file, retain existing negatives | +| Pending initial selection with configured or unconfigured manual target | 409 `initial_model_selection_pending`, config equal to before | source-added same file; `tests/providers/initial-selection-write-fence.test.ts` | +| Malformed scope while pending | 400 before pending check; no mutation | source-added same file | +| Bare `gpt-5.5` custom/native collision | one manual row with routed selector and 128k metadata | source-added row-list test | +| Exact account-qualified collision `desktop/` | account-qualified native survives custom collision and deletion | source-added row-list test | +| Remove manual entries | bare native row returns, qualified row remains | source-added row-list test | +| Replacement enabled / disabled | `fastRowAvailable` true / false, pending also false | final-head source assertions plus existing pending tests | +| Manual-only canonical forward OpenAI group | `nativeProviderGroup:true`, `native:false`; controls remain | `gui/tests/models-native-group-controls.test.ts` | +| Client-config export after replacement, disable, restoration | uses manual selector once; excludes disabled replacement; restores native selector when manual row removed | extend `tests/server/management-client-config-route.test.ts` using existing export fixture; preserve `tests/config/client-config-export.test.ts` coverage | +| Combined group visibility with bare + custom rows | both target kinds accepted atomically; unrelated provider keys unchanged | add explicit mixed group-scope case to existing visibility test if existing fixtures do not cover OpenAI | + +The source GUI grouping test exercises data grouping, not a rendered switch. Future browser evidence must show one manual row, toggle success, account-qualified row retention and native controls in the carried version. Use synthetic accounts; record build/head, DOM/API result and screenshot. The source PNG is prior evidence, not proof that the carried build works. + +## Docs additions beyond the source diff + +The source English API reference at `docs-site/src/content/docs/reference/management-api.md:194-204` is the public SoT. MODIFY each existing locale's `PUT /api/model-visibility` error cell to append `409 initial_model_selection_pending` and refresh/retry guidance; retain existing translated 400 text. Carry the manual-row paragraph's same semantics without changing API identifiers. Exact existing paths: + +- MODIFY `docs-site/src/content/docs/fr/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ja/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ko/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ru/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/tr/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/zh-cn/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/zh-tw/reference/management-api.md`. + +No GUI copy key is introduced by this patch. Do not alter unrelated locale content or the structure ownership table (`structure/05_gui-and-management-api.md:138`) which already names the correct owner. + +## Interphase dependencies and readiness + +010 establishes manual/native group identity consumed by 020's context controls and the later #3659 hide/delete layer. 020 is not mechanically dependent on this change, but must preserve 010's appended GUI test and API paragraph. Later #3659 shares `src/server/management/model-routes.ts`; port it after this visibility contract. Current dev changes since the source base do not touch any of these seven source paths. + +Live review thread `discussion_r3940553047` is resolved; source final tests include its account-qualified fixture. The out-of-diff 409 documentation request is also carried. No unresolved source review finding was returned. Contributor-reported passes are not our validation. Source Cross-platform CI run **33974042485** and React Doctor run **33974042542** were `action_required`; neither establishes passing product CI. Merge remains blocked on carried-head executed checks, independent review and valid GUI/docs evidence. + +## Pinned public source diff + +The following is the full textual base-to-head source patch (PNG retrieval is described above). Apply against current owners, not by copying entire stale source files. Plan amendments above add focused coverage/docs; keep them in this same phase. + +```diff +diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md +index 784c6e17f5..ae24513a97 100644 +--- a/docs-site/src/content/docs/reference/management-api.md ++++ b/docs-site/src/content/docs/reference/management-api.md +@@ -191,12 +191,20 @@ first and submit the returned digest. Prefer quarantine when recovery may be nee + | `GET /api/models` | Return the dashboard/CLI model rows | `catalog_busy` when gathering is saturated | + | `GET /api/client-config?client=...` | Build a read-only client config for any supported file integration | 400 unsupported client; 503 catalog unavailable | + | `PUT /api/disabled-models` | Replace the shared disabled-model list | 400 invalid JSON | +-| `PUT /api/model-visibility` | Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body | ++| `PUT /api/model-visibility` | Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body; 409 `initial_model_selection_pending` (refresh the model list and retry) | + | `GET, POST /api/custom-models` | List custom models or add one | 400 invalid fields; 404 provider missing; 409 duplicate model | + | `PUT, DELETE /api/custom-models/{id}` | Edit or delete one custom model | 400 invalid id/fields; 404 not found; 409 duplicate model | + | `GET, PUT /api/selected-models` | Read provider allowlists and availability, or replace one allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 `initial_model_selection_pending` | + | `GET, PUT /api/model-presets` | Read preset summaries or choose preset/all/custom mode | 400 invalid mode or unsupported preset; 404 unknown provider; PUT 409 `initial_model_selection_pending` | + ++A manual model replaces the Models dashboard row with the same provider and model ID. ++For OpenAI, the manual row keeps `openai/` and supports the same visibility controls ++as other routed models; removing it restores the bare native dashboard row. Explicit ++account-qualified native rows stay separate. This does not rename bare native routes or ++change account entitlements. Non-native OpenAI visibility targets must match a configured ++manual model. ++ ++ + Valid PUT requests to `/api/selected-models` and `/api/model-presets` return HTTP 409 with code `initial_model_selection_pending` until a reliable initial model list is available. Refresh model discovery (for example, `GET /api/models`) and retry after it succeeds. + + ### OAuth accounts, provider keys, and data-plane keys +diff --git a/gui/src/models-groups.ts b/gui/src/models-groups.ts +index a8d6ddc69c..3e24aaf459 100644 +--- a/gui/src/models-groups.ts ++++ b/gui/src/models-groups.ts +@@ -81,7 +81,8 @@ export function buildProviderModelGroups 0 && providerRows.every(row => row.native === true), +- nativeProviderGroup: providerRows.some(row => row.native === true), ++ nativeProviderGroup: providerRows.some(row => row.native === true) ++ || (provider === "openai" && configured?.authMode === "forward"), + liveModels: configured?.liveModels !== false, + configuredModels: configured?.models ?? [], + contextWindow: configured?.contextWindow, +diff --git a/gui/tests/models-native-group-controls.test.ts b/gui/tests/models-native-group-controls.test.ts +index ffd27ad17f..14c2f2c6d8 100644 +--- a/gui/tests/models-native-group-controls.test.ts ++++ b/gui/tests/models-native-group-controls.test.ts +@@ -98,3 +98,9 @@ test("the native group exposes the context modal alongside the custom-model and + // The custom-add and cap controls no longer sit behind an isNative guard. + expect(src).not.toMatch(/\{!isNative && { ++ const groups = buildProviderModelGroups([customRow("gpt-5.5")], [{name:"openai",authMode:"forward"}]); ++ expect(groups[0]!.nativeProviderGroup).toBe(true); ++ expect(groups[0]!.native).toBe(false); ++}); +diff --git a/src/server/management/model-routes.ts b/src/server/management/model-routes.ts +index e9ea26a90e..c3e9d58cf9 100644 +--- a/src/server/management/model-routes.ts ++++ b/src/server/management/model-routes.ts +@@ -566,7 +566,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise model.provider === provider && model.modelId === id); ++ if (!id || (native && (provider !== "openai" || !supportedNative.has(id))) ++ || (provider === "openai" && !native && !configuredOpenAiCustom)) { + return jsonResponse({ error: "invalid model visibility target" }, 400); + } + const key = `${native ? "native" : "routed"}:${id}`; +diff --git a/src/server/management/model-rows.ts b/src/server/management/model-rows.ts +index 4a3fbeaa64..4635a9fbfd 100644 +--- a/src/server/management/model-rows.ts ++++ b/src/server/management/model-rows.ts +@@ -169,7 +169,11 @@ export async function listManagementModelRows( + ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}), + }; + }).filter((row): row is ManagementModelRow => row !== null); +- const rows = [...native, ...dedupedRouted, ...visibleCustomModels]; ++ // Manual OpenAI rows retain their routed selector but replace the bare dashboard row. ++ // Account-qualified rows remain distinct, explicitly selected routes. ++ const visibleNative = native.filter(model => model.id.includes("/") ++ || !customNamespaced.has(routedSlug(model.provider, model.id))); ++ const rows = [...visibleNative, ...dedupedRouted, ...visibleCustomModels]; + // Include disabled rows and configured aliases before the export visibility filter: + // a hidden real `x--fast` must never become a synthetic selector for another model. + const knownIds = config.fastRows === false ? new Set() : knownEffortRowIds(config); +diff --git a/tests/codex-integration/model-visibility-management-api.test.ts b/tests/codex-integration/model-visibility-management-api.test.ts +index 6259818667..15bc17f808 100644 +--- a/tests/codex-integration/model-visibility-management-api.test.ts ++++ b/tests/codex-integration/model-visibility-management-api.test.ts +@@ -1,5 +1,5 @@ + import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +-import { existsSync, mkdirSync} from "node:fs"; ++import { existsSync, mkdirSync, writeFileSync } from "node:fs"; + import { join } from "node:path"; + import { nativeModelRows } from "../../src/codex/catalog"; + import { loadConfig, saveConfig } from "../../src/config"; +@@ -7,6 +7,8 @@ import { handleManagementAPI } from "../../src/server/management-api"; + import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; + import { catalogConvergenceFactory } from "../helpers/catalog-convergence"; + import { removeTreeWithRetry } from "../helpers/remove-tree"; ++import { ManagementRequest as Request } from "../helpers/management-auth"; ++import { listManagementModelRows } from "../../src/server/management/model-rows"; + + const TEST_DIR = join(import.meta.dir, `.tmp-model-visibility-management-${process.pid}`); + const previousOpencodexHome = process.env.OPENCODEX_HOME; +@@ -365,4 +367,77 @@ describe("atomic model visibility management", () => { + expect(loadConfig()).toEqual(before); + }); + }); +-import { ManagementRequest as Request } from "../helpers/management-auth"; ++ ++test("configured manual OpenAI rows can be toggled alongside native rows", async () => { ++ const config = loadConfig(); ++ config.providers.openai = {adapter:"openai-responses",authMode:"forward",baseUrl:"https://chatgpt.com/backend-api/codex",liveModels:false}; ++ config.customModels = [{id:"manual-gpt",provider:"openai",modelId:"gpt-5.5",contextWindow:128_000}]; ++ config.disabledModels = ["openai/gpt-5.5", "gpt-5.4"]; ++ expect((await putWithConfig({scope:"models",provider:"openai",targets:[{id:"gpt-5.5",native:false}],enabled:true},config)).status).toBe(200); ++ expect(config.disabledModels).toEqual(["gpt-5.4"]); ++ expect((await putWithConfig({scope:"models",provider:"openai",targets:[{id:"gpt-5.5",native:false},{id:"gpt-5.4",native:true}],enabled:false},config)).status).toBe(200); ++ expect(config.disabledModels).toContain("openai/gpt-5.5"); ++ expect(config.disabledModels).toContain("gpt-5.4"); ++ expect((await putWithConfig({scope:"models",provider:"openai",targets:[{id:"not-configured",native:false}],enabled:true},config)).status).toBe(400); ++}); ++ ++test("manual models replace management rows with the same provider/id and deletion restores natives", async () => { ++ const config = loadConfig(); ++ config.providers.openai = {adapter:"openai-responses",authMode:"forward",baseUrl:"https://chatgpt.com/backend-api/codex",liveModels:false}; ++ config.customModels = [ ++ {id:"manual-gpt",provider:"openai",modelId:"gpt-5.5",contextWindow:128_000}, ++ {id:"manual-google",provider:"google-antigravity",modelId:"gemini-3.1-pro",contextWindow:128_000}, ++ ]; ++ config.codexAccountNamespaces = { desktop: "@main" }; ++ config.codexAccountPickerEnabled = true; ++ const accountModel = "gpt-5.5-account-fixture"; ++ const qualifiedId = `desktop/${accountModel}`; ++ writeFileSync(join(isolatedCodexHome!.path, "models_cache.json"), JSON.stringify({ ++ models: [{ ++ slug: accountModel, supported_in_api: true, visibility: "list", ++ base_instructions: "You are Codex.", comp_hash: null, shell_type: "unified_exec", ++ supported_reasoning_levels: [{ effort: "medium" }], model_messages: {}, ++ }], ++ })); ++ // Even an exact qualified-ID collision must preserve the account-bound native route. ++ config.customModels.push({ id: "manual-qualified", provider: "openai", modelId: qualifiedId }); ++ const rows = await listManagementModelRows(config,{entitlementWaitMs:0}); ++ expect(rows.filter(row=>row.provider==="openai" && row.id==="gpt-5.5")).toEqual([ ++ expect.objectContaining({namespaced:"openai/gpt-5.5",custom:true,customId:"manual-gpt",contextWindow:128_000,fastRowAvailable:true}), ++ ]); ++ expect(rows.filter(row=>row.provider==="google-antigravity" && row.id==="gemini-3.1-pro")).toHaveLength(1); ++ expect(rows.filter(row => row.id === qualifiedId && row.native)).toEqual([ ++ expect.objectContaining({ namespaced: qualifiedId, provider: "openai", native: true }), ++ ]); ++ config.disabledModels = ["openai/gpt-5.5"]; ++ const disabledRows = await listManagementModelRows(config, { entitlementWaitMs: 0 }); ++ expect(disabledRows.find(row => row.namespaced === "openai/gpt-5.5")).toMatchObject({ ++ custom: true, disabled: true, fastRowAvailable: false, ++ }); ++ config.disabledModels = []; ++ config.customModels = []; ++ const restored = await listManagementModelRows(config,{entitlementWaitMs:0}); ++ expect(restored.some(row => row.id === qualifiedId && row.native)).toBe(true); ++ expect(restored.filter(row=>row.provider==="openai" && row.id==="gpt-5.5")).toEqual([ ++ expect.objectContaining({namespaced:"gpt-5.5",native:true}), ++ ]); ++}); ++ ++test("manual OpenAI visibility preserves the pending-selection error contract", async () => { ++ const config = loadConfig(); ++ config.providers.openai = { ++ adapter: "openai-responses", authMode: "forward", liveModels: false, ++ baseUrl: "https://chatgpt.com/backend-api/codex", ++ initialModelSelection: { version: 1, registrationId: "11111111-1111-4111-8111-111111111111", status: "pending" }, ++ }; ++ config.customModels = [{ id: "manual-gpt", provider: "openai", modelId: "gpt-5.5" }]; ++ const before = structuredClone(config); ++ for (const target of [{ id: "gpt-5.5", native: false }, { id: "not-configured", native: false }]) { ++ const response = await putWithConfig({ scope: "models", provider: "openai", targets: [target], enabled: true }, config); ++ expect(response.status).toBe(409); ++ expect(await response.json()).toMatchObject({ code: "initial_model_selection_pending" }); ++ expect(config).toEqual(before); ++ } ++ expect((await putWithConfig({ scope: "invalid", provider: "openai", targets: [], enabled: true }, config)).status).toBe(400); ++ expect(config).toEqual(before); ++}); +``` diff --git a/devlog/_plan/260906_lane_b_catalog_stack/011_visibility_build.md b/devlog/_plan/260906_lane_b_catalog_stack/011_visibility_build.md new file mode 100644 index 0000000000..987134d7ed --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/011_visibility_build.md @@ -0,0 +1,9 @@ +# Visibility carry build + +Replacement PR: #3685, branch `codex/lane-b-01-visibility`, source #3653 at `956eedac439922cf7645f130ef8432833e813a9a`. + +The complete final diff and binary screenshot were carried in `daee875fe` with Robin Bially as commit author and an explicit coauthor trailer. Translated API references now document manual/native identity and pending discovery rejection. Three additional regression cases cover mixed provider-group toggles, atomic invalid trailing targets and client-export replacement/disable/restoration. + +Independent production and management-boundary review of `53649bab..daee875f` returned PASS with no actionable findings. The reviewer traced authentication, ownership, pending-state ordering, atomic updates, row identity and native entitlement behavior. Added tests receive final independent review; hosted CI and isolated remote GUI/document checks remain pending at this build checkpoint. No local repository suite, typecheck or build was run. + +C/D evidence is recorded in session scratch and the goalplan ledger without editing the tested head while CI runs. The later landing record will publish the final verified SHA and closure outcome. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/020_context.md b/devlog/_plan/260906_lane_b_catalog_stack/020_context.md new file mode 100644 index 0000000000..71f9519a10 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/020_context.md @@ -0,0 +1,640 @@ +# 020 — Preserve selected provider context limits + +Status: planned; source-inspection only. Research captured 2026-09-05T16:34:39.759005+00:00. Local anchor: `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. Source: [PR #3654](https://github.com/lidge-jun/opencodex/pull/3654), issue [#3651](https://github.com/lidge-jun/opencodex/issues/3651). Source head `8facdb0d8c10109701015c0f6109fc67b1d9dd3c`, source base `0b7f60ee259bdd0e5c68b62936fe153af151e9dd`. Live GraphQL confirmed this head remains OPEN with zero unresolved threads on 2026-09-05 UTC (2026-09-06 KST). + +## Execution contract + +This is an implementation design for one later PABCD cycle, not an implementation receipt. The main agent owns the FSM, host goal, 000 roadmap, branch stack, publication, and merge. This delegated research made no production edits, ran no local tests/typecheck/build, and changed no refs. + +Loop archetype: spec-satisfaction repair. Trigger: the linked public issue and source PR. Verifier: exact carried-head GitHub CI plus targeted behavior evidence below. Stop: all activation scenarios accounted for, CI producers successful, author attribution retained, and merge commit proven reachable from dev. Expected outcomes: DONE after that evidence, NOOP only if current dev already implements the same behavior; otherwise retain explicit BLOCKED/NEEDS_HUMAN evidence without claiming completion. Upward escalation: main reclaims a slice after two distinct agents fail its packet; downward delegation requires a P-phase amendment. Resource and credential limits inherit the main lane-B 000 plan; this document authorizes no independent goal, workflow dispatch, deployment, or account change. + +The future executor must re-read the nearest src/GUI/docs AGENTS before code changes. No local tests, suites, typecheck, lint, builds, or dependency installation: CI is the execution verifier. Read-only `git apply --check` below checks textual portability only, not correctness. + +## CI evidence contract + +Source inspection at the recorded local HEAD establishes coverage, not passing execution: + +- `.github/workflows/ci.yml:182-201` selects runtime/tests/GUI changes; both source diffs select `ci` and `gui`. +- `ci.yml:255-316`: four Linux shards invoke `bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD"`. `scripts/ci/run-bun-test-batches.sh:46-64,196-204` enumerates test files and excludes only the dedicated storage/API-usage families; the targeted files below are included. +- `ci.yml:422-428`: root TypeScript checks and `cd gui && bun test --isolate tests`; GUI lint/build at 416-420 and 442-446, privacy scan at 430-431. These are CI commands, never instructions to run locally. +- macOS test execution is at `ci.yml:532`; Windows is **dispatch-only** at `ci.yml:661-662` and runs six shards at 754. A normal PR check does not prove Windows test execution. Main must obtain appropriate exact-head dispatch evidence before claiming three-platform coverage. +- `ci.yml:917-933` permits skipped producers. Inspect actual test/gates job results and tested commit (including the PR merge ref and its head parent), not just aggregate `ci=success`. +- `.github/workflows/react-doctor.yml:15-18,46-57` scans PR changes in `gui` and blocks warnings. It is an additional review gate, not a replacement for GUI tests. +- `.github/workflows/deploy-docs.yml:3-10,25-32` builds docs only on main push or dispatch; normal PR CI has no Astro-docs build. Do not dispatch a deploying workflow merely to get validation. Main must arrange non-deploy hosted docs-build evidence or explicitly retain that verification gap. Local builds remain prohibited. + +Before merging a carried layer, refresh source/head/base/review status, inspect exact-head CI jobs and remaining findings, preserve coauthor credit through squash, and verify the resulting merge SHA is an ancestor of fetched dev. Only then close the superseded source PR and its resolved issue; a source PR carried through another PR is not automatically merged/closed. Retarget/rebase the next child onto dev after its parent lands; do not delete a parent branch while an open child still targets it. These are main-owned future actions. + +## Scope and caller proof + +C3 context-state contract; provider-removal integration receives the affected persistence review. Outcome: off → reload → on preserves an explicit cap such as 128,000 and does not force 922,000. Persist selection independently of activation. No new context-cap endpoint, automatic enable on read, changed account entitlement, arbitrary native-window expansion or redesign of the context modal. + +`gui/src/pages/Models.tsx:733-741` currently sends `NATIVE_GPT56_OPT_IN_WINDOW` when a native group is enabled. `src/providers/context-cap.ts:46-54` deletes active state on off and uses global value on every implicit enable. `src/server/management/provider-routes.ts:1399-1503` owns all three public request branches and refreshes live state/catalog after writes. `src/config.ts:1137` and `src/types/config.ts:607` carry only active limits. `src/codex/catalog/metadata.ts:301-304` exempts ordinary native windows from the 922k ceiling. + +Reuse `context-cap.ts` for two maps: `providerContextCaps` is the only active input; new `providerContextCapValues` remembers the last selection. `selectedProviderContextCaps` merges sanitized remembered values first, active values last. Existing `providerContextCap` (line 10) remains unchanged, so disabled selections never activate catalog capping. `nativeContextLimits` at `metadata.ts:242-263` reads only active caps; long-window opt-in at 278-299 retains per-model ceilings. `src/codex/catalog/provider-fetch.ts:626` keys discovery by active caps; remembered-only changes do not need a new runtime cache key. + +Provider removal/rename consumers are not optional: `providerEditorCandidate` at `provider-routes.ts:267-271`, editor adoption at 288-291, persisted editor callback at 849-852, direct deletion at 1381-1385, and `provider-id-rewrite.ts:113-124`. All must move/clear remembered state along with active state, without enabling it. + +## Exact source carry map + +| Operation | Path | +|---|---| +| NEW | `docs-site/public/screenshots/openai-context-cap-off.png` | +| NEW | `docs-site/public/screenshots/openai-context-cap-on.png` | +| MODIFY | `docs-site/src/content/docs/reference/management-api.md` | +| MODIFY | `gui/src/pages/Models.tsx` | +| MODIFY | `gui/src/pages/models-shared.ts` | +| MODIFY | `gui/tests/models-native-group-controls.test.ts` | +| MODIFY | `gui/tests/models-status-toast.test.tsx` | +| MODIFY | `src/codex/catalog/metadata.ts` | +| MODIFY | `src/config.ts` | +| MODIFY | `src/providers/context-cap.ts` | +| MODIFY | `src/providers/provider-id-rewrite.ts` | +| MODIFY | `src/server/management/provider-routes.ts` | +| MODIFY | `src/types/config.ts` | +| MODIFY | `tests/codex-integration/native-model-toggle.test.ts` | +| MODIFY | `tests/providers/provider-id-rewrite.test.ts` | +| MODIFY | `tests/server/management-provider-validation.test.ts` | + +All 14 textual source files were reviewed. Both PNGs are accounted for as evidence assets; blob identity verified but pixels not inspected. Existing tests are extended, so source carry requires no new test-layout registration. + +### Carry method and attribution + +Prefer a **final diff port** atop the 010 child branch. Actual commit metadata names **Robin Bially <7304732+RobinBially@users.noreply.github.com>** on original `216c11a4941e1b00dc8a069de4ab75128c5f8abf` and clarification `202028670b8f3ec8b8b51761a89cccae081b32a7`. Preserve both contributions with `Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com>` in the carry commit and eventual squash body. Merge commits brought in evolving dev; do not cherry-pick them blindly. The two non-merge commits can be considered for selective cherry-pick, but no cherry-pick was tested and the final delta must be compared to the pinned final PR diff. + +Read-only `git apply --check --exclude='*.png' .tmp/lane-b/3654.patch` returned 0 against the recorded HEAD, before 010 is applied. Cached text equals the local source base-to-head diff after normalizing abbreviated index-hash lines. This is not proof against the future stacked parent. Preserve dev's unrelated xAI changes: `src/config.ts:583-587` and provider creation/patch handling changed since the source base; port hunks rather than replacing those files. + +Binary source assets to carry from the pinned head (the cached patch omits payload): + +- `docs-site/public/screenshots/openai-context-cap-off.png`, blob `092f7377f9781e9ea6b52b04cee5b1069c59f9c6`. +- `docs-site/public/screenshots/openai-context-cap-on.png`, blob `204cbd24a72b0d37efe900cedc02f72d9515fb51`. + +## Implementation sequence within this phase + +1. Add the optional positive-integer map to OcxConfig and Zod config schema; no default map is required. Existing configs lacking it must retain active values via the merged selector. +2. Add selector/forget helpers and modify per-provider/global/all-provider mutations exactly as the source diff. Use existing top-level deletion provenance helper when a map empties. Keep active-only readers unchanged. +3. Extend rename handling to both fields and preserve collision reporting. Switch editor candidate, persisted editor and direct provider removal from disable to forget. Adopt remembered values back into live config after successful editor persistence. +4. Expose `values` on GET and every successful PUT response. Update the route comment at current `provider-routes.ts:1433-1436`: implicit enable restores remembered value, then global default; it no longer always chooses global. Retain existing validation and catalog-refresh branches. +5. Add GUI response/cache/state for `values`; old server/cache fallback is `values ?? caps ?? {}`. Remove the native-only forced enable value and display `active ?? remembered ?? global`. Keep the select present but disabled while cap is off; custom drafts use that same displayed selection. +6. Remove only the special 922k bypass for ordinary native windows in `metadata.ts`; keep `longWindowOptInCeiling`, provider/per-model overlay precedence, and supported ceiling clamps intact. +7. Carry source tests, fill the concrete branch-coverage gaps below, synchronize directly affected documentation, and obtain hosted CI/rendered evidence. Do not implement any later layer here. + +## Activation matrix and exact test changes + +| Scenario / trigger | Required observed result | Test owner | +|---|---|---| +| First enable, no saved selection, global 350k | active and returned selected value 350k | source-added `tests/server/management-provider-validation.test.ts` | +| Explicit 128k, off, reload, implicit on | no active map while off, persisted remembered 128k; returns to active 128k | same source test | +| Remembered 128k while off | `providerContextCap` undefined and native/routed catalog not narrowed by remembered map | extend same test to inspect off-state catalog, not only config | +| Active legacy config with no remembered map | selection response derives active value; first off records it; reload/on restores it | extend same test file with legacy active 128k fixture | +| Global value change without setAll | existing enabled and disabled choices unchanged; first-time provider gets new global | existing test at 4168-4189 plus remembered assertion | +| Enabled A=128k, disabled B remembers 256k, `{value:600000,setAll:true}` | A active/remembered 600k, B still disabled/remembered 256k; B later restores 256k | add explicit two-provider case near existing 4196-4202; source only exercises all-active case | +| Same initial state, `{setAll:true}` without value | every configured provider active and remembered at global; replaces B's 256k | extend existing 4212-4218 and source-added setAll case | +| `{setAll:false}` | all active caps removed, all selections retained | source test plus multi-provider extension | +| Invalid/mixed body, unknown provider, fractional value floors to zero | existing 400/404, no active or remembered mutation | extend existing negatives at 4243-4321 to snapshot both maps | +| Rename while disabled | remembered key moved, no active cap; destination collision preserved/reported | source `tests/providers/provider-id-rewrite.test.ts` + collision case for remembered map | +| Direct removal or editor removal | both maps lose removed ID after persisted reload and live adoption; other provider selections stay | add cases in `tests/server/management-provider-validation.test.ts` near existing delete/editor fixtures | +| GUI native OpenAI 128k off/on | display stays 128k, aria-pressed changes, request body contains only provider/enabled | source `gui/tests/models-status-toast.test.tsx` | +| GUI reload while disabled, old cached/server response without values | remembered 128k restored after reload; old shape falls back to caps/global without crashing | extend the same rendered test with remount and legacy-response fixtures | +| Cap=922k on gpt-5.4 | window becomes 922k, not 1M | source expectation update in `tests/codex-integration/native-model-toggle.test.ts:299` | +| Supported long window ceilings / narrower overlays | gpt-5.6-sol ≤922k, Astra ≤872k; gpt-5.5 remains 272k; smaller cap and model overlay win | retain/extend existing native toggle cases around 289-315, inspect `metadata.ts:289-304` | + +`gui/tests/models-native-group-controls.test.ts` is a source-oracle guard and cannot replace the rendered behavior test. The source GUI test does not remount while disabled, and the source backend test does not exercise a disabled provider through global setAll; those are explicit amendments, not already-proven coverage. Existing `tests/providers/context-cap-unknown-window.test.ts` remains relevant to active-only routed fallback. + +## Public documentation synchronization + +Source updates `docs-site/src/content/docs/reference/management-api.md:237` with both request shapes. Additional MODIFY paths are required because `docs-site/src/content/docs/reference/configuration/providers.md:31-32` currently contradicts that new API text and lacks the stored-selection field. Apply this exact English row contract: + +```diff +-| `providerContextCaps?` | `Record` | `{}` | Per-provider Codex-visible context caps. A cap only lowers a known context window. | ++| `providerContextCaps?` | `Record` | `{}` | Active provider context limits. Ordinary windows are lowered; native models with a supported long window can expand only up to their own supported ceiling. | ++| `providerContextCapValues?` | `Record` | `{}` | Last selected provider limits, retained while disabled. These values do not activate a cap. An enabled value takes precedence over a remembered value. | +-| `contextCapValue?` | `number` | `350000` | Default value used by the dashboard context-cap controls. Changing it applies the value to every routed provider — including providers without an existing `providerContextCaps` entry — only when "apply to every routed provider" is toggled on; otherwise each provider keeps its own cap. | ++| `contextCapValue?` | `number` | `350000` | Default used on first enable. A later enable restores the selected provider value. Updating the global value with `setAll: true` changes enabled caps only; `setAll: true` without a value enables all configured providers at the current global value. | +``` + +MODIFY `docs-site/src/content/docs/guides/model-routing.md:94-97` by adding after the existing active-cap paragraph: “Switching a cap off retains its selection in `providerContextCapValues`; switching it on restores that selection. A remembered selection never applies a limit while disabled.” Keep the current enabled-only global-update wording. Keep the valid explicit 922k opt-in example in `reference/configuration/providers.md:87-91`; eliminating the switch's forced value does not remove explicit native opt-in support. + +Mirror these same key/default/activation semantics in the existing translated references and routing guides below; keep the source English identifiers verbatim and use the established locale prose. For API references append the source `caps`/`values` explanation and two concrete JSON payloads; for providers references replace the stale all-providers global-update claim, insert `providerContextCapValues`, and distinguish supported native ceilings. This is contract synchronization, not unrelated translation cleanup. + +- MODIFY `docs-site/src/content/docs/fr/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/fr/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/fr/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/ja/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ja/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/ja/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/ko/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ko/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/ko/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/ru/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/ru/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/ru/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/tr/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/tr/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/tr/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/zh-cn/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/zh-cn/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/zh-cn/guides/model-routing.md`. +- MODIFY `docs-site/src/content/docs/zh-tw/reference/management-api.md`. +- MODIFY `docs-site/src/content/docs/zh-tw/reference/configuration/providers.md`. +- MODIFY `docs-site/src/content/docs/zh-tw/guides/model-routing.md`. + +No new GUI visible string is required. The API reference remains the public behavior SoT; `structure/05_gui-and-management-api.md:137` already names the owner and endpoint and needs no ownership rewrite. + +## Interphase dependencies and readiness + +Stack this as the next layer after 010. Preserve 010's manual-only OpenAI group identity, appended GUI test and manual-visibility documentation. Shared paths with 010: `gui/tests/models-native-group-controls.test.ts`, API references after translation sync. The #3571 layer later shares `src/types/config.ts`; #3659 later shares provider configuration docs. Lane A's proxy work can touch `src/config.ts`; main must reconcile that file rather than overwrite whole snapshots. + +Source review thread `discussion_r3940577476` is resolved; carry clarification commit `202028670b8f3ec8b8b51761a89cccae081b32a7` and both setAll meanings. Zero unresolved source review threads was returned; this does not waive independent carried-head review. Source Cross-platform CI run **33974043191** and React Doctor run **33974043207** were `action_required`. No product CI execution pass is established. The concrete remaining gates are hosted execution, branch-coverage additions, documentation parity and rendered carried-head off/reload/on evidence. + +Browser evidence must show chosen 128k, disabled 128k after reload, enabled 128k, corresponding request/response payloads, and ordinary-native 922k ceiling behavior. Label the off-state selector as the next-enable choice through the existing context-cap label; do not report the displayed value as an active window. Use the existing browser tooling on a CI/remote-built isolated surface; no local build or service mutation is authorized by this research packet. + +## Pinned public source diff + +Full textual source diff follows. Binary blobs are pinned above. Amend only the paths and behaviors explicitly named in this phase; do not replace entire files with stale source versions. + +```diff +diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md +index 784c6e17f5..133084ba93 100644 +--- a/docs-site/src/content/docs/reference/management-api.md ++++ b/docs-site/src/content/docs/reference/management-api.md +@@ -237,6 +237,18 @@ keys are not returned to dashboard clients. + | `GET, PUT /api/provider-context-caps` | Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider | + | `GET /api/provider-presets` | Return GUI provider presets derived from the runtime registry | — | + ++The provider context-cap response includes `caps` (active limits) and `values` (last selected ++values, retained while disabled). Enabling a provider without `value` restores its selection, ++or uses the global `contextCapValue` on first enable. This also applies to OpenAI: the switch ++does not select a special 922k mode. An active cap bounds every native window; models with a ++supported long-context window may expand only up to their own supported ceiling. ++Updating the global value with `{ "value": 600000, "setAll": true }` changes only enabled ++provider caps; disabled providers keep their remembered selections when later enabled. ++In contrast, `{ "setAll": true }` without `value` enables every configured provider at the ++current global value, replacing their remembered selections. Turning a cap off does not ++activate its remembered value or erase the selection. ++ ++ + `provider_has_dependent_combos` is a safety barrier: remove or edit the dependent combos before + deleting their provider. + +diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx +index 91971cf54d..5b90f29a73 100644 +--- a/gui/src/pages/Models.tsx ++++ b/gui/src/pages/Models.tsx +@@ -51,8 +51,6 @@ import { + fmtK, + NATIVE_CAP_OPTIONS, + NATIVE_CAP_OPTION_SET, +- NATIVE_GPT56_DEFAULT_WINDOW, +- NATIVE_GPT56_OPT_IN_WINDOW, + PAGE, + readCollapsedProviders, + THREAD_OPTION_SET, +@@ -75,6 +73,7 @@ type CachedModelsPage = { + selectedModels: ProviderModelMap; + disabled: string[]; + contextCaps: Record; ++ contextCapValues?: Record; + contextCapValue: number; + }; + +@@ -213,6 +212,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + const [search, setSearch] = useState>({}); + const [limit, setLimit] = useState>({}); + const [contextCaps, setContextCaps] = useState>(() => cached?.contextCaps ?? {}); ++ const [contextCapValues, setContextCapValues] = useState>(() => cached?.contextCapValues ?? {}); + const [contextCapValue, setContextCapValue] = useState(() => cached?.contextCapValue ?? 350_000); + const [customCap, setCustomCap] = useState(""); + const [showCustom, setShowCustom] = useState(false); +@@ -428,6 +428,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + selectedModels: selectionData, + disabled: [...nextDisabled], + contextCaps: capsData.caps ?? {}, ++ contextCapValues: capsData.values ?? capsData.caps ?? {}, + contextCapValue: nextCapValue, + } satisfies CachedModelsPage; + writeSessionListCache(cacheKey, next); +@@ -447,6 +448,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + setSelectedModels(next.selectedModels); + setContextCapValue(next.contextCapValue); + setContextCaps(next.contextCaps); ++ setContextCapValues(next.contextCapValues ?? next.contextCaps); + }, []); + + const catalogResource = useDataSurface( +@@ -722,7 +724,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + } + }; + +- const toggleProviderCap = async (provider: string, nativeGroup = false) => { ++ const toggleProviderCap = async (provider: string) => { + setBusy(true); + busyRef.current = true; + setStatus(""); +@@ -733,13 +735,12 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + const r = await fetch(`${apiBase}/api/provider-context-caps`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, +- body: JSON.stringify(enabled && nativeGroup +- ? { provider, enabled, value: NATIVE_GPT56_OPT_IN_WINDOW } +- : { provider, enabled }), ++ body: JSON.stringify({ provider, enabled }), + }); + try { + const data = await readJsonOrThrow(r, t("models.capSaveFailed")); + setContextCaps(data?.caps ?? {}); ++ setContextCapValues(data?.values ?? data?.caps ?? {}); + setOk(true); + setStatus(t("models.capApplied")); + await load(true); +@@ -784,6 +785,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + const data = await readJsonOrThrow(r, t("models.capSaveFailed")); + if (typeof data?.value === "number" && Number.isFinite(data.value) && data.value > 0) setContextCapValue(data.value); + setContextCaps(data?.caps ?? {}); ++ setContextCapValues(data?.values ?? data?.caps ?? {}); + setOk(true); + setStatus(t("models.capApplied")); + await load(true); +@@ -828,7 +830,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + const onSelectProviderCap = (provider: string, raw: string) => { + if (raw === CUSTOM_OPTION) { + setProviderCapCustomOpen(prev => ({ ...prev, [provider]: true })); +- setProviderCapCustomDraft(prev => ({ ...prev, [provider]: String(contextCaps[provider] ?? contextCapValue) })); ++ setProviderCapCustomDraft(prev => ({ ...prev, [provider]: String(contextCaps[provider] ?? contextCapValues[provider] ?? contextCapValue) })); + return; + } + setProviderCapCustomOpen(prev => ({ ...prev, [provider]: false })); +@@ -1176,18 +1178,8 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + const recentForProvider = modelDiscovery?.recentArrivals[provider] ?? []; + const recentIds = new Set(recentForProvider.map(row => row.id)); + const capOn = contextCaps[provider] !== undefined; +- const providerCap = contextCaps[provider] ?? contextCapValue; +- // With the cap off, `providerCap` is only the value a future toggle would apply — for the +- // native group that is the 350k default, which says nothing true about what Codex sees. +- // The honest number there is the largest window the rows actually advertise. +- const widestRowWindow = rows.reduce((widest, row) => { +- const window = typeof row.contextWindow === "number" && row.contextWindow > 0 ? row.contextWindow : undefined; +- if (window === undefined) return widest; +- return widest === undefined || window > widest ? window : widest; +- }, undefined); +- const capDisplayValue = capOn +- ? providerCap +- : (nativeProviderGroup ? NATIVE_GPT56_DEFAULT_WINDOW : (widestRowWindow ?? providerCap)); ++ // Show the value the next enable will actually use, including a remembered selection. ++ const capDisplayValue = contextCaps[provider] ?? contextCapValues[provider] ?? contextCapValue; + // The native group offers only the three windows GPT-5.6 actually has contracts for + // (272k live, 372k legacy, 1.05M measured); routed providers keep the generic ladder. + // The set has to follow the list, or a saved value outside it loses its option. +@@ -1348,7 +1340,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; + screen-reader user was not told this governs the context window. + The number belongs to the adjacent Select, which is where a value + goes (020_control_affordances.md). */} +- toggleProviderCap(provider, nativeProviderGroup)} disabled={busy} label={t("models.contextCapLabel")} showLabel /> ++ toggleProviderCap(provider)} disabled={busy} label={t("models.contextCapLabel")} showLabel /> + {/* Always rendered, disabled when the cap is off. A cap-off provider used to + drop this control entirely, which is the defect the user reported: openai + showed 1.05M and anthropic showed nothing, so the two rows started at +diff --git a/gui/src/pages/models-shared.ts b/gui/src/pages/models-shared.ts +index 1575a52ac9..fdc487301c 100644 +--- a/gui/src/pages/models-shared.ts ++++ b/gui/src/pages/models-shared.ts +@@ -56,6 +56,7 @@ export interface ProviderContextCapsResponse { + cap?: number; + value?: number; + caps?: Record; ++ values?: Record; + } + + export interface V2Status { +diff --git a/gui/tests/models-native-group-controls.test.ts b/gui/tests/models-native-group-controls.test.ts +index ffd27ad17f..dbe0d68a3c 100644 +--- a/gui/tests/models-native-group-controls.test.ts ++++ b/gui/tests/models-native-group-controls.test.ts +@@ -72,14 +72,9 @@ test("every provider keeps its window readable with the cap switched off", async + // slot is occupied on every card (040_cap_cluster_and_occupied_slot.md), which makes the + // property this test protects strictly wider than it was. + expect(src).not.toContain("{(capOn || nativeProviderGroup) && ("); +- // With the cap off the stored value is only what a future toggle would apply — the 350k +- // default — so the display falls back to the widest window the rows actually advertise. +- // Matched as separate fragments because the expression is wrapped across lines now, and +- // it grew a native branch: with the cap off the native group shows its default window +- // rather than the widest advertised row. A single-line literal pinned the formatting +- // instead of the behaviour and broke on the reflow that introduced that branch. +- expect(src).toContain("const capDisplayValue = capOn"); +- expect(src).toContain("nativeProviderGroup ? NATIVE_GPT56_DEFAULT_WINDOW : (widestRowWindow ?? providerCap)"); ++ // The disabled select previews the persisted choice or global default used by enable. ++ expect(src).toContain("contextCaps[provider] ?? contextCapValues[provider] ?? contextCapValue"); ++ expect(src).not.toContain("value: NATIVE_GPT56_OPT_IN_WINDOW"); + // The select is inert until the cap is actually on: showing a number is not the same as + // offering to change one. + expect(src).toContain("disabled={busy || !capOn}"); +diff --git a/gui/tests/models-status-toast.test.tsx b/gui/tests/models-status-toast.test.tsx +index 29c902c2cb..5ecadb078e 100644 +--- a/gui/tests/models-status-toast.test.tsx ++++ b/gui/tests/models-status-toast.test.tsx +@@ -175,3 +175,40 @@ test("success toast expires after 6s and a repeated action re-arms it", async () + await fireTimers(6000); + expect(container.querySelector(".action-toast")).toBeNull(); + }); ++ ++test("OpenAI context switch restores the selected cap instead of forcing 922k", async () => { ++ testWindow.sessionStorage.clear(); ++ let caps: Record = {openai:128_000}; ++ const values = {openai:128_000}; ++ const bodies: unknown[] = []; ++ const fallback = globalThis.fetch; ++ globalThis.fetch = (async (input, init) => { ++ const url = String(input); ++ if (url.endsWith("/api/models")) return Response.json([ ++ {provider:"openai",id:"gpt-5.5",namespaced:"gpt-5.5",native:true,disabled:false,contextWindow:caps.openai??272_000}, ++ ]); ++ if (url.endsWith("/api/providers")) return Response.json([{name:"openai",authMode:"forward",liveModels:false}]); ++ if (url.endsWith("/api/provider-context-caps")) { ++ if (init?.method === "PUT") { ++ const body=JSON.parse(String(init.body)); bodies.push(body); ++ caps=body.enabled ? {openai:values.openai} : {}; ++ } ++ return Response.json({caps,values,value:350_000}); ++ } ++ return fallback(input,init); ++ }) as typeof fetch; ++ const { createRoot } = await import("react-dom/client"); ++ await act(async () => { root=createRoot(container); root.render(); }); ++ const settle=async()=>{await new Promise(resolve=>testWindow.setTimeout(resolve,0));}; ++ await act(settle); ++ const cluster=()=>container.querySelector(".models-cap-cluster")!; ++ const toggle=()=>cluster().querySelector("button.switch")!; ++ expect(cluster().textContent).toContain("128k"); ++ await act(async()=>{toggle().click();await settle();}); ++ expect(toggle().getAttribute("aria-pressed")).toBe("false"); ++ expect(cluster().textContent).toContain("128k"); ++ await act(async()=>{toggle().click();await settle();}); ++ expect(toggle().getAttribute("aria-pressed")).toBe("true"); ++ expect(cluster().textContent).toContain("128k"); ++ expect(bodies).toEqual([{provider:"openai",enabled:false},{provider:"openai",enabled:true}]); ++}); +diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts +index a50dd9469f..f239ce48b1 100644 +--- a/src/codex/catalog/metadata.ts ++++ b/src/codex/catalog/metadata.ts +@@ -299,8 +299,6 @@ function narrowToLimits(raw: number | undefined, slug: string, input: NativeCont + return overlay !== undefined && cap !== undefined ? Math.min(window, cap) : window; + } + const narrowed = overlay === undefined ? raw : Math.min(raw, overlay); +- // 922k is the GPT-5.6 1M opt-in, not a request to shrink gpt-5.4's 1M window. +- if (cap === NATIVE_GPT56_MAX_INPUT_TOKENS) return narrowed; + return applyProviderContextCap(narrowed, cap) ?? narrowed; + } + +diff --git a/src/config.ts b/src/config.ts +index d2b0bb707a..cd14e26b9c 100644 +--- a/src/config.ts ++++ b/src/config.ts +@@ -1134,6 +1134,7 @@ const configSchema = z.object({ + subagentModels: z.array(z.string().min(1)).optional().catch(undefined), + clientIntegrations: clientIntegrationsSchema.optional().catch(undefined), + providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(), ++ providerContextCapValues: z.record(z.string(), z.number().int().positive()).optional(), + contextCapValue: z.number().int().positive().optional(), + multiAgentGuidanceEnabled: z.boolean().optional(), + // Invalid optional recovery config must not discard unrelated provider/account state. +diff --git a/src/providers/context-cap.ts b/src/providers/context-cap.ts +index d10807ced2..9dd10126ad 100644 +--- a/src/providers/context-cap.ts ++++ b/src/providers/context-cap.ts +@@ -43,13 +43,21 @@ export function globalContextCapValue(config: Pick + return isValidContextCap(value) ? Math.floor(value) : DEFAULT_PROVIDER_CONTEXT_CAP; + } + ++/** Active caps win over remembered values from an earlier switch-off. */ ++export function selectedProviderContextCaps(config: Pick): Record { ++ return { ...providerContextCaps({ providerContextCaps: config.providerContextCapValues }), ...providerContextCaps(config) }; ++} ++ + export function setProviderContextCap(config: OcxConfig, provider: string, enabled: boolean, value?: number): void { + const next = providerContextCaps(config); ++ const selected = selectedProviderContextCaps(config); + if (enabled) { +- next[provider] = isValidContextCap(value) ? Math.floor(value) : globalContextCapValue(config); ++ next[provider] = isValidContextCap(value) ? Math.floor(value) : (selected[provider] ?? globalContextCapValue(config)); ++ selected[provider] = next[provider]; + } else { + delete next[provider]; + } ++ if (Object.keys(selected).length > 0) config.providerContextCapValues = selected; + if (Object.keys(next).length > 0) config.providerContextCaps = next; + else deleteConfigTopLevelKey(config, "providerContextCaps"); + } +@@ -66,18 +74,33 @@ export function setGlobalContextCapValue(config: OcxConfig, value: number, apply + if (!applyToAll) return; + const caps = providerContextCaps(config); + for (const provider of Object.keys(caps)) caps[provider] = next; +- if (Object.keys(caps).length > 0) config.providerContextCaps = caps; ++ if (Object.keys(caps).length > 0) { ++ config.providerContextCaps = caps; ++ config.providerContextCapValues = { ...selectedProviderContextCaps(config), ...caps }; ++ } + } + + /** Enable the cap for every named provider at the current value, or clear all caps. */ + export function setAllProviderContextCaps(config: OcxConfig, providerNames: string[], enabled: boolean): void { ++ const selected = selectedProviderContextCaps(config); + if (!enabled) { ++ if (Object.keys(selected).length > 0) config.providerContextCapValues = selected; + deleteConfigTopLevelKey(config, "providerContextCaps"); + return; + } + const value = globalContextCapValue(config); + const next: Record = {}; +- for (const name of providerNames) next[name] = value; ++ for (const name of providerNames) { next[name] = value; selected[name] = value; } ++ if (Object.keys(selected).length > 0) config.providerContextCapValues = selected; + if (Object.keys(next).length > 0) config.providerContextCaps = next; + else deleteConfigTopLevelKey(config, "providerContextCaps"); + } ++ ++/** Provider removal clears both the active limit and its remembered selection. */ ++export function forgetProviderContextCap(config: OcxConfig, provider: string): void { ++ setProviderContextCap(config, provider, false); ++ const values = { ...config.providerContextCapValues }; ++ delete values[provider]; ++ if (Object.keys(values).length > 0) config.providerContextCapValues = values; ++ else deleteConfigTopLevelKey(config, "providerContextCapValues"); ++} +diff --git a/src/providers/provider-id-rewrite.ts b/src/providers/provider-id-rewrite.ts +index 10a6f3f211..0111a8674a 100644 +--- a/src/providers/provider-id-rewrite.ts ++++ b/src/providers/provider-id-rewrite.ts +@@ -112,14 +112,16 @@ export function rewriteProviderReferences(config: OcxConfig, from: string, to: s + + // Keys. `providerContextCaps` is KEYED by provider id — a prefix rewrite would + // silently orphan the cap — and a destination key may already be occupied. +- const caps = config.providerContextCaps; +- if (caps && Object.hasOwn(caps, from)) { +- if (Object.hasOwn(caps, to)) { +- collisions.push(`providerContextCaps.${to}`); +- } else { +- caps[to] = caps[from]!; +- delete caps[from]; +- changed += 1; ++ for (const field of ["providerContextCaps", "providerContextCapValues"] as const) { ++ const caps = config[field]; ++ if (caps && Object.hasOwn(caps, from)) { ++ if (Object.hasOwn(caps, to)) { ++ collisions.push(`${field}.${to}`); ++ } else { ++ caps[to] = caps[from]!; ++ delete caps[from]; ++ changed += 1; ++ } + } + } + +diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts +index e26420e003..7b4e10dec6 100644 +--- a/src/server/management/provider-routes.ts ++++ b/src/server/management/provider-routes.ts +@@ -60,7 +60,7 @@ import { clearThreadAccountMap } from "../../codex/routing"; + import { primeCodexPoolQuotas } from "../../codex/auth-api"; + import { clearModelCache, getProviderDiscoveryStatus } from "../../codex/model-cache"; + import { getCodexModelEntitlementStatus } from "../../codex/model-entitlements"; +-import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap"; ++import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, selectedProviderContextCaps, forgetProviderContextCap, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap"; + import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget"; + import { resolveCodexHomeDir } from "../../codex/home"; + import { readUsageEntries } from "../../usage/log"; +@@ -267,7 +267,7 @@ function providerEditorCandidate( + candidate.providers = providers; + for (const name of removedProviders) { + dropProviderCustomModels(candidate, name); +- setProviderContextCap(candidate, name, false); ++ forgetProviderContextCap(candidate, name); + } + const validated = validateConfigCandidate(candidate); + if (!validated.ok) { +@@ -288,6 +288,8 @@ function adoptProviderEditorCandidate(live: OcxConfig, persisted: OcxConfig): vo + else live.customModels = structuredClone(persisted.customModels); + if (persisted.providerContextCaps === undefined) delete live.providerContextCaps; + else live.providerContextCaps = structuredClone(persisted.providerContextCaps); ++ if (persisted.providerContextCapValues === undefined) delete live.providerContextCapValues; ++ else live.providerContextCapValues = structuredClone(persisted.providerContextCapValues); + if (persisted.disabledModels === undefined) delete live.disabledModels; + else live.disabledModels = [...persisted.disabledModels]; + if (persisted.modelDiscovery === undefined) delete live.modelDiscovery; +@@ -847,7 +849,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise; ++ /** Last selected provider caps; retained while a cap is switched off. Not an active limit. */ ++ providerContextCapValues?: Record; + /** Global Codex-visible context cap value (tokens). Falls back to DEFAULT_PROVIDER_CONTEXT_CAP. */ + contextCapValue?: number; + /** Bind hostname. Default "127.0.0.1" (loopback only). Set "0.0.0.0" to expose on all interfaces. */ +diff --git a/tests/codex-integration/native-model-toggle.test.ts b/tests/codex-integration/native-model-toggle.test.ts +index 52044cde5a..eac9ec0960 100644 +--- a/tests/codex-integration/native-model-toggle.test.ts ++++ b/tests/codex-integration/native-model-toggle.test.ts +@@ -296,7 +296,7 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { + const over = nativeModelRows({ providerContextCaps: { openai: 2_000_000 } }); + expect(over.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(922_000); + expect(raised.find(r => r.slug === "gpt-5.5")?.contextWindow).toBe(272_000); +- expect(raised.find(r => r.slug === "gpt-5.4")?.contextWindow).toBe(1_000_000); ++ expect(raised.find(r => r.slug === "gpt-5.4")?.contextWindow).toBe(922_000); + }); + + test("nativeModelRows applies providerContextCaps.openai as a ceiling (#1430)", () => { +diff --git a/tests/providers/provider-id-rewrite.test.ts b/tests/providers/provider-id-rewrite.test.ts +index 1df8753b82..cc87ae55fc 100644 +--- a/tests/providers/provider-id-rewrite.test.ts ++++ b/tests/providers/provider-id-rewrite.test.ts +@@ -209,3 +209,10 @@ test("removal leaves the custom-model ownership marker untouched", () => { + legacyOwnedSlugs: ["agnes-ai/agnes-2.5-flash", "huggingface/DeepSeek-V4-Flash-0731"], + }); + }); ++ ++ test("moves remembered provider caps without activating them", () => { ++ const config = { providerContextCapValues: { [FROM]: 128_000 } } as unknown as OcxConfig; ++ expect(rewriteProviderReferences(config, FROM, TO)).toEqual({ changed: 1, collisions: [] }); ++ expect(config.providerContextCapValues).toEqual({ [TO]: 128_000 }); ++ expect(providerContextCap(config, TO)).toBeUndefined(); ++}); +diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts +index 35a7924ebe..36bf70be57 100644 +--- a/tests/server/management-provider-validation.test.ts ++++ b/tests/server/management-provider-validation.test.ts +@@ -4664,3 +4664,31 @@ describe("provider transport option management contract (#1668, #2816)", () => { + }); + }); + }); ++ ++test("OpenAI provider cap remembers an explicit window across off, reload, and on", async () => { ++ mkdirSync(TEST_DIR, { recursive: true }); ++ process.env.OPENCODEX_HOME = TEST_DIR; ++ let live: OcxConfig = { ++ port: 0, defaultProvider: "openai", contextCapValue: 350_000, ++ providers: { openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", liveModels: false } }, ++ }; ++ saveConfig(live); ++ const put = async (body: unknown) => { ++ const url = new URL("http://localhost/api/provider-context-caps"); ++ const response = await handleManagementAPI(new Request(url, {method:"PUT", headers:{"content-type":"application/json"}, body:JSON.stringify(body)}), url, live, {createManagementConvergeCodex:catalogConvergenceFactory()}); ++ expect(response?.status).toBe(200); ++ return response!.json(); ++ }; ++ expect(await put({provider:"openai",enabled:true})).toMatchObject({caps:{openai:350_000}}); ++ await put({provider:"openai",enabled:true,value:128_000}); ++ expect(await put({provider:"openai",enabled:false})).toMatchObject({caps:{},values:{openai:128_000}}); ++ live = loadConfig(); ++ expect(live.providerContextCaps).toBeUndefined(); ++ expect(await put({provider:"openai",enabled:true})).toMatchObject({caps:{openai:128_000}}); ++ const {nativeModelRows} = await import("../../src/codex/catalog"); ++ expect(nativeModelRows(live).filter(row=>row.contextWindow !== undefined).every(row=>row.contextWindow! <= 128_000)).toBe(true); ++ await put({setAll:false}); ++ expect(loadConfig().providerContextCapValues?.openai).toBe(128_000); ++ await put({setAll:true}); ++ expect(loadConfig().providerContextCaps?.openai).toBe(350_000); ++}); +``` + +## Consuming P refresh + +Source #3654 remains OPEN at 8facdb0d8c10109701015c0f6109fc67b1d9dd3c. Full binary-preserving patch applicability passes on verified visibility head e556cc9f7. The actual persistence field is providerContextCapValues. Preserve the prior visibility tests and all translated API paragraphs. A confirmed its config overlap is documentation-only; the executable proxy resolver changes live elsewhere. Owner admin steering and preparation-vs-merge gates are recorded in 000. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/021_context_build.md b/devlog/_plan/260906_lane_b_catalog_stack/021_context_build.md new file mode 100644 index 0000000000..3327f7e5b2 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/021_context_build.md @@ -0,0 +1,7 @@ +# Context selection carry build + +Replacement #3695 carries #3654 final head `8facdb0d8c10109701015c0f6109fc67b1d9dd3c` with Robin Bially's author identity, coauthor trailer, source screenshots and setAll clarification. The new `providerContextCapValues` map preserves inactive selections without applying them to catalog metadata. + +Additional regression cases cover both setAll payload shapes, legacy active-only reloads, invalid-request memory/disk atomicity, removal/editor cleanup, rename collision, disabled native budgets and GUI remount/old-response fallback. The source/security review identified a numeric-selection lookup defect for valid inherited property names; the carry now requires an own numeric remembered value and covers first-enable/off/reload/on for toString and valueOf. Final review and remote/hosted execution remain pending at this checkpoint. Public API, provider config and routing documentation are synchronized across existing locales. + +Parent #3685 completed full exact-head CI 33978686258 and independent code/security plus remote GUI/docs verification. It was admin-merged into dev as `9115b179a29f1366561139b8502cebb17bf816e9`; source #3653 and issue #3650 were immediately closed after ancestry proof. #3695 was retargeted to dev before parent merge to preserve the stack safely. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/030_ordering.md b/devlog/_plan/260906_lane_b_catalog_stack/030_ordering.md new file mode 100644 index 0000000000..10d33d01fb --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/030_ordering.md @@ -0,0 +1,679 @@ +# 030 — Preserve configured Go efforts and separate complete picker order + +Class: C3 cross-module catalog contract. One future PABCD cycle consumes this document after the model-toggle/context layers (010/020), before management (040). This cycle carries only #3571; message recovery #3568 is explicitly outside it. + +## Outcome and necessity + +Configured canonical `opencode-go` efforts survive generation and retained sync without injected max/ultra. A nonblank bare catalog id in `modelPickerOrder` opts into complete-picker display ordering; exact ids outrank raw/encoded equivalents. Routed-only and empty configurations retain legacy behavior. Display sorting must leave OpenCodex's natural-priority guidance candidates unchanged. Native Codex advertisements are a separate consumer and may follow the changed display order. Existing `applyReasoningLevels`, `slugEquivalenceKey`, `SPAWN_PRIORITY_FIELD`, and observed-state merge own these behaviors; reuse them, with no new catalog engine or provider roster. + +## Current owners and amendment anchors + +- `src/codex/catalog/sync.ts:315,358,412`: `deriveEntry` currently preserves exact combo/forward ladders, but Go uses ordinary synthetic tiers. Pass a separate `preserveExactReasoning` predicate to both derive branches; do not alter exact-combo metadata policy. +- `src/codex/catalog/effort.ts:223-247`: `preserveExact` already skips synthetic max/ultra insertion; retain this owner unchanged. +- `src/codex/catalog/sync.ts:518,654-668`: builder currently applies routed display ordering and records natural spawn priority. Reject whitespace-only entries consistently, without trimming significant ids or changing routed-only ordering. +- `src/codex/catalog/sync.ts:781,814`: add `modelPickerRank`, `applyFullModelPickerOrder`, optional order/selectors on `ObservedCatalogMergeInput`; retain the backwards-compatible wrapper at `sync.ts:1215` with empty defaults. +- `src/codex/convergence.ts:344` and `src/codex/catalog/sync.ts:1764`: both production merge callers must pass order and account selectors. A helper-only test is not proof of caller wiring. +- Retained native rows restore natural priority before recomputing featured priority; retained OCX routed rows rebuild featured rank with selector stride and reset obsolete display overrides. Apply full order only after native/routed admission and multi-agent version assignment. +- `src/codex/catalog/sync.ts:177-208`: `effectiveSubagentRoster` actually reads `opencodex_spawn_priority`, then visibility/v2 filters and the five-row cap. Inspect this consumer on every carry amendment, not only emitted priorities. +- `src/types/config.ts` documents `modelPickerOrder`; it has diverged since the source base and the previous context phase also touches this file. Apply only the comment delta, preserving the new context contract. + +## Focused implementation deltas + +The appendix contains the full pinned textual source delta, including complete NEW test files. Key reviewable boundaries are: + +```diff + const preserveExact = isExactComboCatalogModel(model, exactComboSlugs); ++const preserveExactReasoning = preserveExact || model?.provider === "opencode-go"; +-applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); ++applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning); +``` + +Use the same predicate in the native-template branch, preserving `codexForwardNativeCapabilityAlias !== null`. Retained merge must independently exclude `opencode-go/` from mock-max insertion while preserving the existing Reserve and exact-combo exclusions. + +```diff + const mergedModels = mergeCatalogEntriesFromObservedState({ ++ modelPickerOrder, ++ accountSelectors, + catalogModels, +``` + +Repeat at retained sync. Complete ordering preserves `entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9`, records that natural value, and sets display priority to exact/equivalent rank or `pickerOrder.length + natural`; empty/routed-only input returns before mutation. The retained-row block must execute before final routed filtering/merge, using fresh `featured` and selector stride. Do not transplant the whole 1,800-line sync module. + +## Activation and regression matrix + +| Test owner | Activate | Required observation | +|---|---|---| +| NEW `tests/codex-integration/catalog-go-exact-efforts.test.ts` | Derive Go with null and native template, `[high,max]` and `[high,xhigh]`; merge both disk-only and fresh-only Muse | Exact effort/default ladders, no synthetic max for Muse; other provider still has max/ultra | +| NEW `tests/codex-integration/catalog-full-picker-order.test.ts` | Bare native id + Go routed ids, then apply twice | Specified complete display order; unchanged stored natural ranks and byte-equivalent repeated result | +| Same | Empty, whitespace-only, routed-only, raw slash upstream id plus encoded id | Legacy behavior; no whitespace activation; exact rank wins equivalence and no suffix aliasing | +| Same | Start full order, switch to empty/routed-only during provider outage; change featured order, promote/demote; zero/two selectors and nonzero picker index | Healthy and degraded rows agree on both display and spawn rank; second merge is stable; input snapshot unmutated | +| Same plus existing `codex-v2-gate.test.ts` | Change picker only while retaining configured subagent roster; use v2 eligibility | Same five OpenCodex guidance candidates and valid exact Go effort membership | +| Existing `tests/codex-integration/codex-catalog.test.ts` | Existing normalization/recovery fixtures | Existing native Reserve/exact ladders and account rows retain their contracts; align assertions only for intentional Go tier change | +| Existing `tests/test-layout.test.ts`, `tests/test-layout-tooling.test.ts` | NEW file registration | Both explicit layout map and expected fixture contain both file names in codex-integration | + +Source tests cover most matrix rows. Add a production-entry convergence/retained-sync assertion to the existing catalog tests if source tests only call the helper: configure order, run each entry under fixture IO, then compare displayed ids and `effectiveSubagentRoster` before/after. Use known fixture helpers, no live service. CI commands to select this family for a focused rerun are `bun test tests/codex-integration/catalog-full-picker-order.test.ts tests/codex-integration/catalog-go-exact-efforts.test.ts tests/codex-integration/codex-catalog.test.ts tests/codex-integration/codex-v2-gate.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts` on a CI runner only; full required CI still applies. + +## Docs, dependencies, and unresolved acceptance + +- English and French `guides/model-ordering.md` must explicitly describe opt-in and migration: old lists containing previously ignored bare ids now change complete ordering. Do not introduce a pinned-native allowlist restriction: the public source contract deliberately allows new bare catalog ids. +- English provider reference adds Go efforts/config-key examples and a roster link. The source's dated endpoint claims are not independently provider-validated by this research; carry as configured examples or require fresh primary evidence before describing them as current supported roster. No provider requests are authorized here. +- Proposed SoT amendment, MODIFY `structure/03_catalog-and-subagents.md:35`: add: “Complete picker order is enabled by a nonblank bare id in modelPickerOrder. Display priority is independent of opencodex_spawn_priority; retained rows recompute natural ranks from the current featured roster and account-selector stride. Canonical opencode-go rows preserve configured reasoning ladders in generation and retained merges.” Main owns applying this documented delta in C. +- 020 → 030 shares `src/types/config.ts`; 030 → 040 shares `tests/codex-integration/codex-catalog.test.ts` and English provider reference. Coordinate one sequential integration owner; no recovery dependency on lane A's #3568. +- No dashboard JSX change in #3571. For functional UI evidence obtain isolated CI-produced catalog/model-list output and a Codex picker capture showing native-first order plus unchanged subagent list; an old author's local-release screenshot is not carried-head proof. No local build or live default service mutation. +- No newly established algorithm blocker in this read-only source review. Pending: exact-head CI, caller-level coverage, docs build gap, source-era bare-id warning disposition, and refreshed independent review. Source metadata has no guaranteed complete review-thread list; stale CodeRabbit prose is not an unresolved-thread verdict. + +## Source and baseline + +Read on 2026-09-06 KST in `/Users/jun/.codex/worktrees/f80e/opencodex` at `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. Pinned source: [PR #3571](https://github.com/lidge-jun/opencodex/pull/3571), head `0a935c5694229760c8c1cd5a62072107d8ae6696`, source base `6585e6a70f42be8b6c81ff20d4fa0f39f7da03db`. Inputs are captured `.tmp/lane-b/3571.json` and `.patch`; no claim of a fresh remote status check is made. `git show -s` independently confirmed the head commit author below. + +| Source commit | Actual commit author | Subject | +|---|---|---| +| `e57a57d5f0299fefd37d0f3d661e7b8d81afda1d` | voiys <matej2714@gmail.com> | fix(catalog): preserve Go efforts and support native-first picker order | +| `d745d8a417dc8b56372625b656bde778270ddf41` | voiys <matej2714@gmail.com> | fix(codex): reset retained picker order after provider outages | +| `90eaaddd815f5f70263fbfe90f4968c41856fa2a` | voiys <matej2714@gmail.com> | fix(codex): normalize picker orders and retain slug compatibility | +| `0a935c5694229760c8c1cd5a62072107d8ae6696` | voiys <matej2714@gmail.com> | fix(codex): refresh retained spawn ranks during discovery outages | + +Preserve original commit authors on a clean replay; for reimplementation or squash put `Co-authored-by: voiys ` in each carried logical commit and the final squash body. PR login alone is not an author trailer. + +Safe carry strategy: main revalidates pinned source/head and incoming parent; replay the complete reviewed source series in order or reproduce its exact delta with attribution. Preserve source follow-up commits, not only the initial feature commit. Publish a child against its still-open parent; after parent squash, rebuild the child on the new dev ancestry and re-run exact-head CI. Retarget surviving children before parent branch deletion. Push uses the user's authorized `--no-verify` to avoid local hooks; this does not substitute for CI. Once dev contains the complete carried behavior, close the superseded source PR with a carry reference; do not close it for a partial/default-only slice. No linked issue is invented. + +## Exact change ledger + +Every source changed file is accounted for below. “Same base” means a read-only `git hash-object` of current file bytes matches the patch's old blob prefix; it does not prove future cherry-pick cleanliness. “Drift” requires contextual reconciliation. Source binary is explicitly unreviewed. All textual hunks were inspected as source behavior; appendix preserves exact before/after, including complete NEW test content. No source production file was edited. + +| Operation | Exact path | Baseline / disposition | +|---|---|---| +| MODIFY | `docs-site/src/content/docs/fr/guides/model-ordering.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/guides/model-ordering.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/reference/configuration/providers.md` | Drift; preserve current unrelated edits | +| MODIFY | `scripts/test-layout/layout.json` | Same base; reviewed textual delta | +| MODIFY | `src/codex/catalog/sync.ts` | Same base; reviewed textual delta | +| MODIFY | `src/codex/convergence.ts` | Same base; reviewed textual delta | +| MODIFY | `src/types/config.ts` | Drift; preserve current unrelated edits | +| NEW | `tests/codex-integration/catalog-full-picker-order.test.ts` | New source file, absent locally | +| NEW | `tests/codex-integration/catalog-go-exact-efforts.test.ts` | New source file, absent locally | +| MODIFY | `tests/codex-integration/codex-catalog.test.ts` | Same base; reviewed textual delta | +| MODIFY | `tests/codex-integration/codex-v2-gate.test.ts` | Same base; reviewed textual delta | +| MODIFY | `tests/fixtures/test-layout-expected.json` | Same base; reviewed textual delta | +| MODIFY (planned SoT addition) | `structure/03_catalog-and-subagents.md` | Public contract prose delta specified above; not in source PR | + +## Execution boundary and verifier + +This is a docs-only roadmap deliverable, not an implementation or merge receipt. The main agent owns 000, the goal/FSM, branch operations, integration, and final acceptance. No local tests, suites, typecheck, builds, hooks, provider requests, commits, pushes, or GitHub writes were run by this researcher. The next implementation cycle must re-read this plan against its actual parent tip. + +Loop archetype: spec-satisfaction repair. Trigger: carry the pinned public source PR into lane B. Stop: required behaviors, exact-head CI, reviewer disposition and parent integration all have durable evidence. Expected result: DONE only after verified dev ancestry; NOOP only if equivalent behavior is already landed; unresolved correctness/CI evidence is pending, not DONE. Scope and unattended resource bounds are inherited from main's 000; this document does not arm or amend the goal. Upward escalation: return concrete caller/test evidence to main if the carry contract cannot be satisfied; additional worker dispatch requires main's planned scope. + +CI-only verifier, inspected at the baseline below: + +- `.github/workflows/ci.yml:5` uses unfiltered `pull_request`, so child PR bases are supported. `changes` at lines 181-218 admits `src/**`, `tests/**`, `scripts/**`, `gui/**`; source changes also admit packaging. +- Linux `test 1/4..4/4`, lines 255-316, invokes `scripts/ci/run-bun-test-batches.sh`. That runner enumerates `tests` (line 197), admits `.test.ts` files (lines 46-68), and excludes only storage/API-usage families into dedicated jobs; the catalog/management files in this plan are included. Each actual batch runs `bun test --isolate --timeout 60000` under a process timeout (lines 121-126). Read logs to prove the named files ran; aggregate green alone is insufficient. +- `gates`, lines 390-449, runs root TypeScript (`bun x tsc --noEmit`), GUI tests (`cd gui && bun test --isolate tests`), privacy scan and skill-surface check. GUI changes additionally run `bun run lint` and `bun run build`; `gui/package.json:8` expands build to `tsc -b && vite build`. GUI lint is `oxlint .`, including changed locale/UI inputs; the separately named `lint:i18n` script is not a dedicated CI step. +- macOS runs two full-suite shards (lines 451-547). Windows full-suite six shards (lines 658-769) run only on `workflow_dispatch` with lane `all`; do not infer Windows full-suite coverage from packaging smoke or PR aggregate success. Main must obtain an exact-ref dispatch if Windows full-suite evidence is required, then verify the run head. +- `.github/workflows/deploy-docs.yml:3-10,24-33` builds Astro only on main push or manual dispatch and then deploys. Normal PR CI has no Astro docs build. Do not trigger this deploy workflow merely to obtain a pre-merge check. Main must arrange an approved non-deploy CI verifier on the exact candidate commit or explicitly retain this as a readiness gap; this research does not add workflow code or authorize deployment. +- Save head SHA, parent/base SHA, run URL, executed job conclusions, named test logs, approvals and unresolved-thread disposition. Source-author reported passes are historical claims, not carried-head validation. Never attest local checks that were intentionally prohibited. + + +## Pinned public source delta + +Reconcile only the touched hunks at the implementation parent. The conceptual deltas above and acceptance amendments take precedence over copying this source verbatim. This appendix records public source-PR behavior only. + +````diff +diff --git a/docs-site/src/content/docs/fr/guides/model-ordering.md b/docs-site/src/content/docs/fr/guides/model-ordering.md +index cac2b0667c..64408efa53 100644 +--- a/docs-site/src/content/docs/fr/guides/model-ordering.md ++++ b/docs-site/src/content/docs/fr/guides/model-ordering.md +@@ -23,7 +23,7 @@ priorités `i * N + j`, où `j` est la position du sélecteur en base zéro ; un + sont déplacées hors de ces groupes de sélecteurs. Codex continue de n’annoncer que les cinq premières + lignes visibles dans le sélecteur. + +-Les priorités sans sélecteur pertinentes sont : ++Sans ordre global du sélecteur, les priorités sans sélecteur pertinentes sont : + + | Entrée du catalogue | Priorité | Source | + | --- | --- : | --- | +@@ -134,8 +134,31 @@ au-delà de ce bloc mis en avant : + Les lignes routées indiquées apparaissent dans l’ordre configuré. Une ligne absente du tableau conserve sa + priorité normale et reste donc devant la bande d’affichage de `modelPickerOrder` ; indiquez toutes les + lignes routées dont vous souhaitez contrôler l’ordre relatif. Une ligne également présente dans +-`subagentModels` conserve sa priorité de mise en avant. `modelPickerOrder` ne réorganise ni les lignes +-natives non qualifiées ni celles qualifiées par un compte ; utilisez `subagentModels` pour celles-ci. ++`subagentModels` conserve sa priorité de mise en avant. Une liste contenant uniquement des identifiants ++routés conserve la position normale des lignes natives. ++ ++Pour ordonner tout le sélecteur, incluez un identifiant natif non qualifié : ++ ++```json ++{ ++ "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] ++} ++``` ++ ++Les lignes indiquées apparaissent d’abord dans l’ordre du tableau, puis les lignes absentes ++selon leur priorité naturelle. La correspondance est exacte : `gpt-5.6-sol` et ++`openai/gpt-5.6-sol` désignent deux lignes distinctes. Pour une ligne qualifiée par un compte, ++indiquez son identifiant complet, sélecteur inclus. Les formes brute et encodée du même ++identifiant routé sont acceptées, avec priorité aux correspondances exactes. Les entrées ++vides sont ignorées. ++ ++### Migration : identifiants natifs dans les listes existantes ++ ++Auparavant, les identifiants natifs dans `modelPickerOrder` étaient ignorés. Une liste ++existante contenant un identifiant natif non qualifié ordonne désormais tout le sélecteur, ++y compris les lignes mises en avant. Supprimez ces identifiants pour conserver l’ancien ++comportement limité aux lignes routées. Les listes absentes, vides ou uniquement routées ++conservent leur comportement ; les priorités des candidats sous-agents ne changent pas. + + `modelPickerOrder` ne modifie jamais l’ensemble des candidats de `spawn_agent`. Il change uniquement la + priorité visible par Codex dans le sélecteur, tandis qu’OpenCodex conserve la priorité naturelle de chaque +diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md +index 696f631a58..352c8ddb12 100644 +--- a/docs-site/src/content/docs/guides/model-ordering.md ++++ b/docs-site/src/content/docs/guides/model-ordering.md +@@ -23,7 +23,7 @@ priorities `i * N + j`, where `j` is the selector's zero-based position; a route + rows are moved outside those selector groups. Codex still advertises only the first five + picker-visible rows. + +-The relevant no-selector priorities are: ++Without complete-picker ordering, the relevant no-selector priorities are: + + | Catalog entry | Priority | Source | + | --- | ---: | --- | +@@ -133,8 +133,28 @@ featured block: + Listed routed rows appear in the configured order. A routed row omitted from the array keeps its + normal priority, so it remains ahead of the `modelPickerOrder` display band; list every routed row + whose relative position you want to control. A row also present in `subagentModels` keeps its +-featured priority. Bare native and account-qualified native rows are not reordered by +-`modelPickerOrder`; use `subagentModels` for those rows. ++featured priority. With a routed-only list, native rows keep their normal positions. ++ ++To order the complete picker, include a bare native id: ++ ++```json ++{ ++ "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] ++} ++``` ++ ++Listed rows appear first in array order, followed by unlisted rows in natural priority ++order. Matching uses exact catalog ids: `gpt-5.6-sol` and `openai/gpt-5.6-sol` are separate ++rows. Raw and encoded spellings of the same routed id are also accepted, with exact ++matches taking precedence. Empty entries are ignored. Account-qualified rows need ++their selector-qualified id in the list. ++ ++### Migration note: native ids in existing orders ++ ++Previously, native ids in `modelPickerOrder` were ignored. An existing list containing ++a bare native id now activates complete-picker ordering, including featured rows. ++Remove bare native ids to keep the previous routed-only behavior. Unset, empty and ++routed-only lists retain their behavior; subagent candidate priorities are unchanged. + + `modelPickerOrder` never changes the `spawn_agent` candidate set. It changes only the + Codex-visible picker priority while opencodex retains each moved row's natural priority for +diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md +index ab8a154ecb..24f175a09a 100644 +--- a/docs-site/src/content/docs/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/reference/configuration/providers.md +@@ -810,3 +810,21 @@ ids with context `922000` and max input `922000`; OpenRouter seeds `openai/gpt-5 + "visionSidecar": { "enabled": true } + } + ``` ++ ++ ++## OpenCode Go reasoning efforts ++ ++Go catalog rows preserve their configured reasoning efforts exactly, including during ++catalog sync. OpenCodex does not append synthetic `max` or `ultra` choices to these rows. ++Use `modelReasoningEfforts` and `modelDefaultReasoningEfforts` for each model's accepted ++upstream values. Key these per-provider maps by upstream model ID, not the routed ++`opencode-go/` catalog slug. For example, Omen Alpha (`omen-alpha`) accepts `low`, `high`, ++and `max`; Muse Spark 1.3 Contributor (`muse-spark-1.3-contributor`) accepts `minimal`, `low`, `medium`, `high`, and `xhigh` (Go endpoint validation, 2026-09-05). ++See the [OpenCode Go model list](https://opencode.ai/docs/go/#models) for the current roster. ++A configured subset can exclude the lower tiers. Other providers retain their existing behavior. ++ ++For a native-first picker, include native ids in `modelPickerOrder` followed by the ++routed ids. This orders the complete picker while preserving the separate subagent ++candidate priorities. Routed-only orders keep their previous behavior. See the ++[ordering migration note](/guides/model-ordering/#migration-note-native-ids-in-existing-orders). ++`modelDisplayNames` on a provider controls readable labels without changing wire ids. +diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json +index 29dd2c5f1c..7f3fa69999 100644 +--- a/scripts/test-layout/layout.json ++++ b/scripts/test-layout/layout.json +@@ -255,6 +255,8 @@ + "bun-stream-caps.test.ts": "lib", + "cancel-body-on-abort.test.ts": "server", + "catalog-cursor-search.test.ts": "codex-integration", ++ "catalog-full-picker-order.test.ts": "codex-integration", ++ "catalog-go-exact-efforts.test.ts": "codex-integration", + "catalog-input-modality-enum.test.ts": "codex-integration", + "catalog-llamacpp-capabilities.test.ts": "codex-integration", + "catalog-oauth-observation.test.ts": "codex-integration", +diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts +index 3f5f472baf..7b0be6e19b 100644 +--- a/src/codex/catalog/sync.ts ++++ b/src/codex/catalog/sync.ts +@@ -315,6 +315,8 @@ export function deriveEntry( + contextCap?: NativeContextLimitsInput, + ): RawEntry { + const preserveExact = isExactComboCatalogModel(model, exactComboSlugs); ++ // Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides. ++ const preserveExactReasoning = preserveExact || model?.provider === "opencode-go"; + const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true + ? upstreamNativeEntry(model.id) + : null; +@@ -359,7 +361,7 @@ export function deriveEntry( + e, + model?.reasoningEfforts, + model?.defaultReasoningEffort, +- preserveExact || codexForwardNativeCapabilityAlias !== null, ++ preserveExactReasoning || codexForwardNativeCapabilityAlias !== null, + ); + // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned + // native tool/search/responses-lite contract while preserving the routed slug and wire id. +@@ -409,7 +411,7 @@ export function deriveEntry( + }; + if (isRouted) { + applyRoutedCodexToolMode(entry, model?.codexToolMode); +- applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); ++ applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning); + } + else { + applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]); +@@ -518,7 +520,7 @@ export function buildCatalogEntriesFromObservedState({ + // before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so + // this display reorder cannot change which rows are spawn candidates. + const pickerOrder = Array.isArray(modelPickerOrder) +- ? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0) ++ ? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.trim().length > 0) + : []; + const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const)); + const pickerOrderActive = pickerOrder.length > 0; +@@ -779,12 +781,33 @@ export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly< + unsupportedNativeEntries: "drop", + }); + ++/** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */ ++function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined { ++ const exact = new Map(order.map((slug, index) => [slug, index])); ++ const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index])); ++ return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug)); ++} ++ ++/** A picker order containing native ids orders the whole list, without changing spawn ranks. */ ++export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void { ++ const pickerOrder = order.filter(slug => slug.trim().length > 0); ++ if (!pickerOrder.some(slug => !slug.includes("/"))) return; ++ const rankOf = modelPickerRank(pickerOrder); ++ for (const entry of entries) { ++ const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9; ++ entry[SPAWN_PRIORITY_FIELD] = natural; ++ entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural); ++ } ++} ++ + export interface ObservedCatalogMergeInput { + readonly catalogModels: readonly RawEntry[]; + readonly baselineCatalogModels: readonly RawEntry[]; + readonly routedEntries: readonly RawEntry[]; + readonly baseline: ReadonlyMap; + readonly featured: readonly string[]; ++ readonly modelPickerOrder?: readonly string[]; ++ readonly accountSelectors?: readonly string[]; + readonly wsEnabled: boolean; + readonly template: RawEntry | null; + readonly disabledModels: ReadonlySet; +@@ -817,6 +840,8 @@ export function mergeCatalogEntriesFromObservedState({ + routedEntries, + baseline, + featured, ++ modelPickerOrder = [], ++ accountSelectors = [], + wsEnabled, + template, + disabledModels, +@@ -975,7 +1000,9 @@ export function mergeCatalogEntriesFromObservedState({ + finished.priority = nativePriority(slug, upstream.priority); + return finished; + } +- const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) }); ++ const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) }); ++ // Recompute spawn rank from current featured models, not a prior picker override. ++ delete preserved[SPAWN_PRIORITY_FIELD]; + // Older natives kept from disk still need the mock top tiers (max + ultra always + // for subagent max spawns; wire-clamped to the model's real top rung). + if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved); +@@ -1060,6 +1087,32 @@ export function mergeCatalogEntriesFromObservedState({ + // remain outside provider ownership and survive unless a fresh row replaces their exact slug. + return !isOcxAuthoredRoutedEntry(entry); + }); ++ // Retained rows bypass the builder. Recompute managed spawn ranks from current config ++ // before either display-order mode; a saved display override is not current roster authority. ++ const pickerOrder = modelPickerOrder.filter(slug => slug.trim().length > 0); ++ const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/")); ++ const rankOf = modelPickerRank(pickerOrder); ++ const featuredRankOf = modelPickerRank(featured); ++ const priorityStride = Math.max(accountSelectors.length, 1); ++ for (const entry of preservedRoutedEntries) { ++ const natural = entry[SPAWN_PRIORITY_FIELD]; ++ if (typeof natural === "number") { ++ entry.priority = natural; ++ delete entry[SPAWN_PRIORITY_FIELD]; ++ } ++ const slug = String(entry.slug); ++ if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue; ++ const featuredRank = featuredRankOf(slug); ++ entry.priority = featuredRank !== undefined ++ ? featuredRank * priorityStride ++ : (accountSelectors.length > 0 ? 1_000 : 0) + 5; ++ if (featuredRank !== undefined || fullPickerOrder) continue; ++ const pickerIndex = rankOf(slug); ++ if (pickerIndex !== undefined) { ++ entry[SPAWN_PRIORITY_FIELD] = entry.priority; ++ entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride; ++ } ++ } + let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries]; + finalRoutedEntries = finalRoutedEntries.filter(entry => { + const slug = typeof entry.slug === "string" ? entry.slug : ""; +@@ -1134,7 +1187,7 @@ export function mergeCatalogEntriesFromObservedState({ + // Mock-max universality (260709): preserved routed entries from disk may predate + // the max rung — ensure it here so subagent max spawns validate on every + // reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact. +- if (!exactCombo && !reserveProjection) { ++ if (!exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) { + const levels = Array.isArray(e.supported_reasoning_levels) + ? e.supported_reasoning_levels as Array<{ effort?: string }> + : []; +@@ -1161,6 +1214,7 @@ export function mergeCatalogEntriesFromObservedState({ + multiAgentV2Enabled, + { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection }, + ); ++ applyFullModelPickerOrder(versionedEntries, modelPickerOrder); + for (const entry of versionedEntries) { + const kind = entry.opencodex_catalog_kind; + if (trustedAccountBoundNativeCatalogSlug(entry) === undefined +@@ -1762,6 +1816,8 @@ function writeRetainedCatalogSync({ + }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined) + : []; + catalog.models = mergeCatalogEntriesFromObservedState({ ++ modelPickerOrder, ++ accountSelectors, + catalogModels: catalogModelsForMerge, + baselineCatalogModels: baselineCatalog?.models ?? [], + routedEntries: goEntries, +diff --git a/src/codex/convergence.ts b/src/codex/convergence.ts +index df765a7853..8b30bb9eb2 100644 +--- a/src/codex/convergence.ts ++++ b/src/codex/convergence.ts +@@ -342,6 +342,8 @@ function prepareCatalog( + )), + ); + const mergedModels = mergeCatalogEntriesFromObservedState({ ++ modelPickerOrder, ++ accountSelectors, + catalogModels, + baselineCatalogModels, + routedEntries, +diff --git a/src/types/config.ts b/src/types/config.ts +index 8cf1246979..425a8e095a 100644 +--- a/src/types/config.ts ++++ b/src/types/config.ts +@@ -418,17 +418,14 @@ export interface OcxConfig { + /** One-time featured-roster upgrade marker; later user ordering is preserved. */ + subagentModelsVersion?: number; + /** +- * Optional full picker ordering for the Codex model catalog, independent of the +- * 5-slot `subagentModels` spawn_agent cap. DISPLAY-ONLY: it controls the visual order of +- * the Codex model picker for large routed catalogs (10-20+ models) that would otherwise sort +- * arbitrarily and reshuffle on every rebuild. Values are routed `/` catalog +- * slugs (matched by exact slug or `provider/id`); native OpenAI passthrough rows and +- * account-qualified native rows are not reordered (order native rows via `subagentModels`). +- * Listed routed rows appear in array order; rows not listed keep their normal display order. +- * `subagentModels`-featured rows keep their top position. When unset or empty, catalog +- * priority is unchanged. This changes ONLY what the user sees in the picker: the spawn_agent +- * candidate set is derived from each row's natural priority and is provably unaffected, even +- * when every routed row is listed (see opencodex_spawn_priority / effectiveSubagentRoster). ++ * Display-only order for the Codex picker, independent of subagentModels. ++ * Routed-only lists order non-featured routed rows; featured and native rows keep ++ * their normal positions. Including a bare native id opts into ordering the complete ++ * picker: listed ids appear first in array order, followed by unlisted rows in their ++ * natural priority order. Exact catalog ids take precedence over equivalent raw/encoded ++ * routed ids; empty entries are ignored. The separate natural spawn ++ * priority is preserved, so display order does not change subagent candidates. ++ * Unset or empty leaves catalog priorities unchanged. + */ + modelPickerOrder?: string[]; + /** +diff --git a/tests/codex-integration/catalog-full-picker-order.test.ts b/tests/codex-integration/catalog-full-picker-order.test.ts +new file mode 100644 +index 0000000000..1dbda27090 +--- /dev/null ++++ b/tests/codex-integration/catalog-full-picker-order.test.ts +@@ -0,0 +1,111 @@ ++import { routedSlug } from "../../src/providers/slug-codec"; ++import { expect, test } from "bun:test"; ++import { buildCatalogEntriesFromObservedState, mergeCatalogEntriesFromObservedState, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, applyFullModelPickerOrder, deriveEntry, mergeCatalogEntriesForSync, SPAWN_PRIORITY_FIELD } from "../../src/codex/catalog/sync"; ++ ++test("native-first picker order preserves Go subagent ranks and is repeatable", () => { ++ const rows: any[] = [ ++ { slug: "opencode-go/glm-5.3", priority: 0 }, ++ { slug: "gpt-5.6-sol", priority: 9 }, ++ { slug: "gpt-6-astra", priority: 9 }, ++ ]; ++ const order = ["gpt-6-astra", "gpt-5.6-sol", "opencode-go/glm-5.3"]; ++ applyFullModelPickerOrder(rows, order); ++ expect([...rows].sort((a,b) => a.priority-b.priority).map(r => r.slug)).toEqual(order); ++ expect(rows.map(r => r[SPAWN_PRIORITY_FIELD])).toEqual([0,9,9]); ++ const once = structuredClone(rows); ++ applyFullModelPickerOrder(rows, order); ++ expect(rows).toEqual(once); ++}); ++ ++test("existing routed-only ordering retains its behavior", () => { ++ const rows: any[] = [{ slug: "opencode-go/glm-5.3", priority: 1000 }]; ++ applyFullModelPickerOrder(rows, ["opencode-go/glm-5.3"]); ++ expect(rows).toEqual([{ slug: "opencode-go/glm-5.3", priority: 1000 }]); ++}); ++ ++ ++test("sync refreshes native spawn rank when featured models change", () => { ++ const sol = deriveEntry(null, "gpt-5.6-sol", "Sol", 105); ++ const order = ["gpt-5.6-sol"]; ++ applyFullModelPickerOrder([sol], order); ++ expect(sol[SPAWN_PRIORITY_FIELD]).toBe(105); ++ ++ const baseline = new Map([["gpt-5.6-sol", 9]]); ++ const promoted = mergeCatalogEntriesForSync([sol], [], baseline, ["gpt-5.6-sol"], false); ++ applyFullModelPickerOrder(promoted, order); ++ expect(promoted.find(entry => entry.slug === sol.slug)?.[SPAWN_PRIORITY_FIELD]).toBe(0); ++ ++ const demoted = mergeCatalogEntriesForSync(promoted, [], baseline, ["opencode-go/glm-5.3"], false); ++ applyFullModelPickerOrder(demoted, order); ++ expect(demoted.find(entry => entry.slug === sol.slug)?.[SPAWN_PRIORITY_FIELD]).toBe(101); ++}); ++ ++ ++test("bare native ids and routed slugs match exactly, without suffix aliases", () => { ++ const rows: any[] = [ ++ { slug: "openai/gpt-5.6-sol", priority: 2 }, ++ { slug: "gpt-5.6-sol", priority: 9 }, ++ { slug: "other/gpt-5.6-sol", priority: 3 }, ++ ]; ++ applyFullModelPickerOrder(rows, ["gpt-5.6-sol", "openai/gpt-5.6-sol"]); ++ expect(rows.map(row => row.priority)).toEqual([1, 0, 5]); ++ expect(rows.map(row => row[SPAWN_PRIORITY_FIELD])).toEqual([2, 9, 3]); ++}); ++ ++test.each([ ++ { order: [] as string[] }, ++ { order: ["gpt-5.6-sol", "opencode-go/glm-5.3"], after: ["opencode-go/glm-5.3"] }, ++ { order: ["gpt-5.6-sol", "opencode-go/glm-5.3"], before: ["opencode-go/glm-5.3"], after: [] }, ++ { order: ["gpt-5.6-sol", "opencode-go/team/model"], modelId: "team/model", before: ["other/model", "opencode-go/team/model"], after: ["opencode-go/team/model", "other/model"] }, ++ ++ { order: ["", "opencode-go/glm-5.3"] }, ++ { order: [" ", "opencode-go/glm-5.3"] }, ++ { order: [""] }, ++ { order: ["opencode-go/team/model"], modelId: "team/model" }, ++ { order: ["opencode-go/glm-5.3"] }, ++ { order: ["other/model", "opencode-go/glm-5.3"] }, ++])("degraded discovery refreshes ranks and remains stable for %j", ({ order, modelId = "glm-5.3", before = [], after = [] }) => { ++ for (const accountSelectors of [[], ["account-a", "account-b"]]) { ++ const slug = routedSlug("opencode-go", modelId); ++ const fresh = (modelPickerOrder: readonly string[], featured: readonly string[] = []) => buildCatalogEntriesFromObservedState({ ++ template: null, gptSlugs: [], ++ goModels: [{ id: modelId, provider: "opencode-go", displayName: "GLM 5.3", reasoningEfforts: ["high", "max"] }], ++ featured, modelPickerOrder, wsEnabled: false, multiAgentMode: "default", ++ exactComboSlugs: new Set(), accountSelectors, suppressedBareNativeSlugs: new Set(), ++ disabledNativeAccountSlugs: new Set(), multiAgentV2Enabled: false, ++ }); ++ const merge = (catalogModels: Record[], routedEntries: Record[], modelPickerOrder: readonly string[], degraded: boolean, featured: readonly string[] = []) => ++ mergeCatalogEntriesFromObservedState({ ++ catalogModels, routedEntries, modelPickerOrder, accountSelectors, ++ baselineCatalogModels: [], baseline: new Map(), featured, wsEnabled: false, ++ template: null, disabledModels: new Set(), selectedModelsByProvider: new Map(), ++ gatheredProviderNames: new Set(["opencode-go"]), ++ degradedProviderNames: new Set(degraded ? ["opencode-go"] : []), ++ legacyCustomModelSlugs: new Set(), multiAgentMode: "default", multiAgentV2Enabled: false, ++ exactComboSlugs: new Set(), hasPhysicalComboProvider: false, includeNativeOpenAi: true, ++ accountBoundEntries: [], ++ policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, warningPolicy: "suppress" }, ++ }); ++ const fullOrder = ["gpt-5.6-sol", slug]; ++ const previous = merge([], fresh(fullOrder, before), fullOrder, false, before); ++ const saved = structuredClone(previous); ++ const healthy = merge(previous, fresh(order, after), order, false, after); ++ const degraded = merge(previous, [], order, true, after); ++ const row = (entries: Record[]) => entries.find(entry => entry.slug === slug)!; ++ expect(row(degraded).priority).toBe(row(healthy).priority); ++ expect(row(degraded)[SPAWN_PRIORITY_FIELD]).toBe(row(healthy)[SPAWN_PRIORITY_FIELD]); ++ expect(merge(degraded, [], order, true, after)).toEqual(degraded); ++ expect(previous).toEqual(saved); ++ } ++}); ++ ++ ++test("full ordering ignores empty entries and accepts raw upstream ids with slashes", () => { ++ const slug = routedSlug("vendor", "team/model"); ++ const rows = [{ slug, priority: 1000 }, { slug: "gpt-5.6-sol", priority: 9 }]; ++ applyFullModelPickerOrder(rows, ["", "gpt-5.6-sol", "vendor/team/model"]); ++ expect(rows.map(row => row.priority)).toEqual([1, 0]); ++ const exact = [{ slug, priority: 5 }]; ++ applyFullModelPickerOrder(exact, ["gpt-5.6-sol", slug, "vendor/team/model"]); ++ expect(exact[0]!.priority).toBe(1); ++}); +diff --git a/tests/codex-integration/catalog-go-exact-efforts.test.ts b/tests/codex-integration/catalog-go-exact-efforts.test.ts +new file mode 100644 +index 0000000000..5fa4da816b +--- /dev/null ++++ b/tests/codex-integration/catalog-go-exact-efforts.test.ts +@@ -0,0 +1,39 @@ ++import { expect, test } from "bun:test"; ++import { deriveEntry, mergeCatalogEntriesForSync } from "../../src/codex/catalog/sync"; ++ ++for (const template of [null, { slug: "gpt-5.6-sol", supported_reasoning_levels: [{ effort: "ultra" }] }]) { ++ test(`Go preserves exact configured efforts (${template ? "template" : "fallback"})`, () => { ++ for (const [id, efforts] of [ ++ ["glm-5.3", ["high", "max"]], ++ ["glm-5.3-flash", ["high", "max"]], ++ ["omen-alpha", ["high", "max"]], ++ ["deepseek-v4-flash-vision-exp", ["high", "max"]], ++ ["muse-spark-1.3-contributor", ["high", "xhigh"]], ++ ] as const) { ++ const entry = deriveEntry(template, `opencode-go/${id}`, "Go", 1, { ++ provider: "opencode-go", id, reasoningEfforts: [...efforts], defaultReasoningEffort: efforts[1], ++ }); ++ expect(entry.supported_reasoning_levels.map((level: { effort: string }) => level.effort)).toEqual([...efforts]); ++ expect(entry.default_reasoning_level).toBe(efforts[1]); ++ } ++ }); ++} ++ ++test("other providers retain their existing virtual tiers", () => { ++ const entry = deriveEntry(null, "other/model", "Other", 1, { ++ provider: "other", id: "model", reasoningEfforts: ["high"], ++ }); ++ expect(entry.supported_reasoning_levels.map((level: { effort: string }) => level.effort)).toEqual(["high", "max", "ultra"]); ++}); ++ ++test("sync does not reintroduce max for Muse", () => { ++ const muse = deriveEntry(null, "opencode-go/muse-spark-1.3-contributor", "Muse", 1, { ++ provider: "opencode-go", id: "muse-spark-1.3-contributor", ++ reasoningEfforts: ["high", "xhigh"], defaultReasoningEffort: "xhigh", ++ }); ++ for (const [disk, fresh] of [[[muse], []], [[], [muse]]]) { ++ const entries = mergeCatalogEntriesForSync(disk, fresh, new Map(), [], false); ++ const entry = entries.find(e => e.slug === muse.slug)!; ++ expect(entry.supported_reasoning_levels.map((level: { effort: string }) => level.effort)).toEqual(["high", "xhigh"]); ++ } ++}); +diff --git a/tests/codex-integration/codex-catalog.test.ts b/tests/codex-integration/codex-catalog.test.ts +index 37d8c69798..cc394a548d 100644 +--- a/tests/codex-integration/codex-catalog.test.ts ++++ b/tests/codex-integration/codex-catalog.test.ts +@@ -5445,11 +5445,11 @@ describe("Codex catalog routed normalization", () => { + const expected = [ + { slug: "deepseek/deepseek-v4-flash", efforts: ["low", "high", "max", "ultra"] }, + { slug: "deepseek/deepseek-v4-pro", efforts: ["low", "high", "max", "ultra"] }, +- { slug: "opencode-go/deepseek-v4-flash", efforts: ["low", "high", "max", "ultra"] }, +- { slug: "opencode-go/deepseek-v4-pro", efforts: ["low", "high", "max", "ultra"] }, +- { slug: "opencode-go/glm-5.2", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, +- { slug: "opencode-go/glm-5.1", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, +- { slug: "opencode-go/glm-5", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, ++ { slug: "opencode-go/deepseek-v4-flash", efforts: ["low", "high", "max"] }, ++ { slug: "opencode-go/deepseek-v4-pro", efforts: ["low", "high", "max"] }, ++ { slug: "opencode-go/glm-5.2", efforts: ["low", "medium", "high", "xhigh", "max"] }, ++ { slug: "opencode-go/glm-5.1", efforts: ["low", "medium", "high", "xhigh", "max"] }, ++ { slug: "opencode-go/glm-5", efforts: ["low", "medium", "high", "xhigh", "max"] }, + { slug: "zai/glm-5.2", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, + { slug: "zai/glm-5.2[1m]", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, + { slug: "zhipu-bigmodel/glm-4.6", efforts: ["low", "medium", "high", "xhigh", "max", "ultra"] }, +diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts +index 8d3e2d9dc5..6e8b6a18c1 100644 +--- a/tests/codex-integration/codex-v2-gate.test.ts ++++ b/tests/codex-integration/codex-v2-gate.test.ts +@@ -100,14 +100,13 @@ function installModeHintRuntime(supported = true): string { + describe("catalog ultra (always-on)", () => { + const routed = [{ id: "glm-5.2", provider: "opencode-go", reasoningEfforts: ["low", "medium", "high", "xhigh"] }]; + +- test("routed + old natives always advertise mock max AND ultra", () => { ++ test("Go keeps declared efforts while old natives retain mock tiers", () => { + const entries = buildCatalogEntries(template(), ["gpt-5.5"], routed as never, [], false); + const native = entries.find(e => e.slug === "gpt-5.5")!; + const glm = entries.find(e => e.slug === "opencode-go/glm-5.2")!; + expect(efforts(native)).toContain("ultra"); + expect(efforts(native)).toContain("max"); +- expect(efforts(glm)).toContain("ultra"); +- expect(efforts(glm)).toContain("max"); // mock max: adapters/wire clamp keep it honest ++ expect(efforts(glm)).toEqual(["low", "medium", "high", "xhigh"]); + }); + + test("gpt-5.6-sol keeps native ultra + max; luna has max but no native ultra (upstream ladder)", () => { +diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json +index 114c699eaf..8dba5cfb55 100644 +--- a/tests/fixtures/test-layout-expected.json ++++ b/tests/fixtures/test-layout-expected.json +@@ -92,6 +92,8 @@ + "bun-stream-caps.test.ts": "lib", + "cancel-body-on-abort.test.ts": "server", + "catalog-cursor-search.test.ts": "codex-integration", ++ "catalog-full-picker-order.test.ts": "codex-integration", ++ "catalog-go-exact-efforts.test.ts": "codex-integration", + "catalog-input-modality-enum.test.ts": "codex-integration", + "catalog-llamacpp-capabilities.test.ts": "codex-integration", + "catalog-oauth-observation.test.ts": "codex-integration", + +```` + +## Consuming P refresh + +Parent preparation head is 29f98462c4a63cf217347c26668733169fd65736. Source #3571 remains OPEN at 0a935c5694229760c8c1cd5a62072107d8ae6696, and its full patch passes applicability on this parent. All four non-merge source commits identify voiys . The existing modelPickerOrder field survives config loading through the established root passthrough schema; no new persistence field is introduced. Preserve providerContextCapValues from 020. + +The initial roadmap listed source English/French edits, but six other existing model-ordering guides also contain the legacy native-order contract. MODIFY docs-site/src/content/docs/{ja,ko,ru,tr,zh-cn,zh-tw}/guides/model-ordering.md with the same complete-order opt-in, exact/equivalent matching, unchanged spawn roster and existing-list migration warning. Do not create new locales or alter unrelated routing semantics. The runtime/template output remains separately verified from any native client capture; a synthetic rendering must never be described as an actual client capture. + +Delegation: main carries the final source diff and owns SoT/commits; catalog worker supplies caller-level coverage and a captured generated-list comparison; docs worker owns the six translated guides; independent code reviewer checks priorities/retained paths; remote verifier uses isolated exact-head tests/docs plus a native client capture if the installed client can be run safely with synthetic state. No local test/build/typecheck and no real personal proxy/account calls. Final merge gates remain unchanged. + +## C evidence-driven contract clarification + +The independent native-consumer audit distinguishes three concepts: OpenCodex natural-priority guidance (must remain unchanged), native advertised five (can follow changed display priority), and exact-name override eligibility (not restricted to the advertised five). This preserves the already-recorded #1649 design while correcting the earlier unqualified wording. No wire rewriting or native-client patch is added. The source appendix above remains an immutable record of the original PR and is not a current universal native-advertisement guarantee. + +Native source d2d5b702 (local upstream checkout, not claimed to match binary0.153.4) shows both V1/exposedV2 using native priority; current valid generated before/after data demonstrates the expected displacement. The actual0.153.4 capture proves picker/data consumption only until a separate toolspec capture is obtained. V1 has no OCX preferred-roster injection; V2 guidance is conditional on catalog state. New production-writer fixture failures remain blockers for the natural-guidance criterion and cannot be waived by this wording correction. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/031_ordering_build.md b/devlog/_plan/260906_lane_b_catalog_stack/031_ordering_build.md new file mode 100644 index 0000000000..7f68b1ca11 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/031_ordering_build.md @@ -0,0 +1,9 @@ +# Ordering carry build + +Replacement #3700 carries all four source #3571 commits through `0a935c5694229760c8c1cd5a62072107d8ae6696`, retaining voiys as author and coauthor. It preserves configured canonical OpenCode Go ladders in generation/retention and separates full-picker display order from natural spawn priority. + +Production-writer tests cover both convergence and retained sync, healthy/outage equivalence, refreshed featured ranks, idempotence and the same five eligible candidates. Source review found that the new merge paths lacked the builder's runtime normalization for the existing passthrough modelPickerOrder field. All three boundaries now share the same nonarray/nonstring/blank filtering while preserving significant ID spelling. Malformed-input production-writer cases and remote causal checks verify that repair. English/French source documentation is synchronized with the six other existing ordering guides and the catalog SoT. + +Parent #3695 was admin-merged on dev as `ab6762bdb35db24efbe1ceac77a1f9e5e6139616` after every actual CI producer succeeded. The aggregation-only ci job was still queued and explicitly recorded as an owner-authorized administrative exception; no actual test was bypassed. Independent reviews and remote backend/component/typecheck/docs/browser/red-green evidence passed. Source #3654 and issue #3651 were closed after dev ancestry proof, and #3700 was safely retargeted to dev. + +Final ordering review and exact-head remote/hosted execution are pending at this checkpoint. No local repository tests, typechecks or builds were run. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/032_ordering_repair.md b/devlog/_plan/260906_lane_b_catalog_stack/032_ordering_repair.md new file mode 100644 index 0000000000..1d1538cf4e --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/032_ordering_repair.md @@ -0,0 +1,11 @@ +# Ordering check repair + +The first remote check of 1c2616bfd failed 14 new production-writer cases; no failing result was treated as a pass. Investigation separated fixture isolation from a production defect. + +The fixture now provides a runnable deterministic Codex command through forced refresh, asserts runtime identity, uses the current featured-roster migration marker, and checks effort arrays without mutating metadata. Full catalog equality and the same five OpenCodex guidance candidates remain required. + +Fresh row derivation could copy opencodex_spawn_priority from a previously ordered native template. Assigning a new featured priority did not replace that inherited private rank, so repeated healthy writes could change the guidance window. Fresh clones now clear that previous row's private marker; retained-row markers and reader behavior are unchanged. Direct dirty-template and repeated real-writer regressions cover the cause. Remote causal confirmation and reruns are required before closing this repair. + +The native-consumer audit also corrected an overbroad explanation: OpenCodex natural-priority guidance and native Codex's advertised five are separate. Native advertisement may follow display priority on V1 and exposed V2; exact-name override eligibility is not limited to that advertisement. This clarification preserves the existing #1649 design and does not waive the failing natural-guidance assertions. Current code comments, configuration reference and eight ordering guides now make the distinction explicit; the original source-diff appendix remains historical evidence. + +No local tests, builds or typechecks were run. Verification must use the repaired committed head and retain red/green, runtime identity and teardown evidence. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/033_ordering_control.md b/devlog/_plan/260906_lane_b_catalog_stack/033_ordering_control.md new file mode 100644 index 0000000000..ea2d9e7110 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/033_ordering_control.md @@ -0,0 +1,7 @@ +# Matched retained-discovery control + +After the template-rank correction, the direct regression and repeated production-writer guidance cases passed remotely. Ten malformed-order cases still compared a static healthy catalog (14 rows in that snapshot) against a live/degraded catalog (36 rows). Maintained Go metadata augmentation is skipped for liveModels:false and enabled for liveModels:true, so changing that setting admitted additional rows independently of picker-order validity. + +The test now restores identical catalog/cache bytes before a valid-filtered retained control and a malformed retained run. Both use the same live/empty-model/failure settings; only picker order differs. It still compares complete model arrays, the full guidance roster and all original fixture models' exact effort ladders. Healthy valid-versus-malformed equality is retained. No registry model count is hardcoded and no production fallback behavior is changed. + +Previous failed outputs remain evidence. The revised counterfactual requires an exact-head remote rerun before a success claim. No local tests were run. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/034_verification_followup.md b/devlog/_plan/260906_lane_b_catalog_stack/034_verification_followup.md new file mode 100644 index 0000000000..4b220a619d --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/034_verification_followup.md @@ -0,0 +1,3 @@ +# Verification follow-up + +The ordering CI run reported an unrelated Lab supervision test failure. A bounded verification prerequisite is reviewed separately from the catalog change. Detailed pre-publication analysis and the implementation plan remain in ignored scratch under the repository security-working-note policy. Product limits and existing assertions are not relaxed. The original ordering branch and failed outputs remain preserved; no success is claimed at this planning checkpoint. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/035_ordering_landing.md b/devlog/_plan/260906_lane_b_catalog_stack/035_ordering_landing.md new file mode 100644 index 0000000000..1149e41f05 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/035_ordering_landing.md @@ -0,0 +1,25 @@ +# 035 — Ordering and verification prerequisite landed + +PR #3700 landed with an admin merge at `76356176c86aa123220c82b65321453e81897405`. +Its tree `f1950aecabdb3b73dbb4bdea18a845b27da70222` matches the tested GitHub merge +candidate. Both ordering head `e59b730b1` and Lab prerequisite head `8b5dbde02` +were verified as ancestors of dev. GitHub automatically marked #3713 merged. + +Source #3571 was closed immediately after that proof. Its refreshed head +`09acfba64596011c308f0d9cbac070123bb9faeb` rebases the carried source: eight of +twelve feature-file blobs are identical; four differences are established dev +changes, and the three rewritten follow-ups have matching stable patch IDs. +The source author and `Co-authored-by: voiys ` are retained. + +- Exact source-head [CI 33989738843](https://github.com/lidge-jun/opencodex/actions/runs/33989738843) + completed successfully, including Linux four shards, macOS two shards and all producers. +- Remote merge candidate `33ed4751` passed the canonical full suite: 19,606 pass, + 0 fail, 15 skip, plus build, typecheck and privacy checks. +- After the intervening Logs landing, candidate `88a67043` passed all dashboard + tests, lint/i18n/build, five affected root test files, typecheck, privacy and docs + build. Catalog, Lab, adapters and dependencies match the full-suite baseline. +- The earlier standalone prerequisite run is retained as cancelled: its macOS + log stopped at the client-connect file before Lab execution. It is not called green. + +Independent source, contract and security reviews passed. The ordering PABCD +cycle is closed; model management and Fable remain separate incomplete work phases. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/040_management.md b/devlog/_plan/260906_lane_b_catalog_stack/040_management.md new file mode 100644 index 0000000000..7fbc46047b --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/040_management.md @@ -0,0 +1,1299 @@ +# 040 — Provider model deletion, visibility and static default-only sync + +Class C3 with C4 review for model identity and management mutations. Active P baseline: `67fdf24eb6e661f4d9e84aaa86a4eb39c6f3ba58` (2026-09-06 KST), including the verified ordering/Lab landing `76356176c` and D Cursor description preservation. Source #3659 remains OPEN at `ff4e5cd5352b9c1bd05e3de0091f3483ca130be5`; all five original commits are by gqchen <276851182@qq.com>. This active contract supersedes conflicting proposals in the historical source appendix. The source patch authority is the actual merge base `6585e6a70f42be8b6c81ff20d4fa0f39f7da03db`; target snapshot `af50c6d3` is not a valid substitute. Current independent preparation found no need for a new endpoint, store, dependency or visual redesign. + +The previous ordering cycle is complete. The new implementation stays in bound f80e on `codex/lane-b-04-management`. User authorization includes no-verify pushes, admin merges and immediate source closure after verified dev inclusion. No local tests, suites, typechecks or builds; execution is isolated remote or hosted CI. No release/deployment, global account or service changes. + +## Accepted behavior + +Delete removes one stored custom definition by stable ID. It sends exactly one DELETE and never an automatic visibility PUT. A native or discovered counterpart can return and keep the inventory count unchanged. Hide sends exactly one visibility PUT using a confirmed server row. Add saves a definition and preserves independent visibility/selection policy. No permanent browser tombstone survives a refresh. The existing Models page is the recovery surface for hidden rows, including when the provider tab is empty. + +The frontend consumes existing /api/models DTOs once per parent refresh alongside the full /api/selected-models response. It does not add disabled to selected-models or introduce another identity classifier. Existing full available, selected and liveModelCounts retain their meanings. The only production backend change is the source static-default seed plus any narrowly demonstrated regression repair approved in this cycle. + +## Planned interfaces and owners + +- Reuse type-only ModelRow from gui/src/pages/models-shared.ts. Add a strict row-array boundary parser/grouping beside current provider-workspace helpers, with dedicated model-inventory module if size warrants. Validate nonblank provider/id/namespaced, boolean disabled and optional native/custom/pending flags, plus nonblank customId for custom rows. Invalid destructive identity fails the whole refresh. Preserve raw strings; use Map/null-prototype records and namespaced uniqueness. +- ProviderWorkspaceShell owns one paired read per refresh epoch. Adopt both successful responses together; do not claim cross-endpoint transactional consistency. Maintain a current refresh key/revision and the revision of the adopted snapshot. Invalidate readiness immediately on retry, external refresh or mutation reconciliation; deferred effect loading alone is insufficient. Keep cancellation/generation rejection and use existing bounded fetch conventions. +- Pass modelRows: ModelRow[] | null and refresh revision/readiness through DetailSlotData, Providers, ProviderDetails and keyed ProviderModels. null is unavailable; [] is a successful empty projection. Add onOpenModels from Providers using existing navigateHash("models"). +- ProviderModels refreshes its full custom-definition GET whenever the parent revision changes, and records the successful ownership revision. Controls require a current row snapshot and current custom ownership, no load error, no pending mutation and matching customId/provider/modelId for Delete. Refetch both resources after success, failure or an ambiguous response. Successful ownership GET must not erase unresolved mutation feedback. +- On confirmed snapshots, render non-disabled DTOs; key chips, copy and busy state by namespaced. Identical raw labels with distinct selectors are disambiguated using namespaced. An unavailable snapshot may retain old/read-only fallback data; successful empty must not insert configured/default/native rows. Pending/unknown identity has no mutation action. +- Rail count uses the full unique non-disabled DTO inventory before query or render cap. Detail search/truncation count is separately understood. An allowlist badge does not change inventory count, and native selection does not borrow a routed same-raw-id badge. Full available/provenance stay separate. +- Delete custom records only; other confirmed rows Hide with row.provider, row.id and row.native === true. Block both handlers and all buttons on the shared busy/readiness condition. Do not infer native from provider name or substitute Hide when custom ownership is unavailable. +- Preserve existing Add duplicate/encoded-collision checks using full raw configured/discovered/custom inputs. Do not newly reject a valid native override solely because a native DTO has the same raw id. Existing hidden custom definitions remain duplicates and use Models to restore visibility. Validate POST 201 identity and stable ID before adoption. Saved, saved-but-hidden, refresh-pending and unconfirmed-save outcomes are distinct; no automatic POST retry or implicit unhide. + +## Source carry disposition + +Preserve gqchen <276851182@qq.com> in the carry commit and Co-authored-by trailer. Carry the static default-only patch, source UI controls/icons/locales and docs with adaptation. Omit the source selected-models disabled-map API hunk and its redundant parser/prop chain. Replace source Delete-then-Hide and raw-ID tombstones with the contract above. Preserve the complete historical source appendix as evidence, explicitly superseded where it conflicts with this amendment. Refresh the screenshot from the actual amended UI; the source image is historical. + +## Verification required before completion + +- GUI exact request counts for cancel/Delete/Hide; custom-only delete/re-add/remount; native and discovered replacement after Delete; raw/encoded and account-qualified collisions; invalid DTO/native/custom metadata; pending rows; failed custom GET; three-resource refresh readiness with reversed responses, external custom-ID replacement and provider switch; malformed/ambiguous POST reconciliation without repeat writes; independent hidden/allowlist state preserved; empty and 300+ inventories; recovery link visible in empty state; counts match the canonical pre-search inventory. +- Actual management DELETE→GET round trips, preserving native/routed/account hides and existing validation. No new management write semantics or relaxation. +- Static omitted/empty models + default + retain union/dedupe, explicit list precedence, no default/no lists, forward early return, successful-empty live discovery; no extra network activity in static cases. +- Independent C4 source/security review and final UI/code review. Actual isolated compiled GUI with synthetic API state, screenshots and remount/error evidence. Root/GUI focused checks, typecheck, docs build and exact-head hosted functional CI. No local suite/test/typecheck/build. +- Update English and translations for static seeding and the amended Delete/Hide meaning, and the existing source of truth. Do not document an unimplemented disabled-map API. + +Obtain an independent full plan audit before B. One B implements this management slice only; Fable remains a separate later cycle. Preserve the original patch below as historical source evidence, not current implementation instructions. + +## Concrete component contract and file inventory + +The parent passes `modelRows: ModelRow[] | null`, `modelRevision: string` and `modelRowsReady: boolean` through its existing detail chain. `modelRevision` represents the current API base, external refresh token and local retry epoch. The adopted parent snapshot carries its own revision; readiness requires equality. The child custom-record load is keyed by the same revision and separately records successful ownership observation. A revision mismatch disables actions immediately, even before deferred loading effects run. Mutation start has an immediate single-flight guard; parent and ownership reconciliation must finish before that guard reopens. Failed/old reads cannot certify a new revision. + +The existing fetch owner gains cancellation and generation checks without a new cache/store. Reuse `readJsonOrThrow` and the shared `putModelVisibility`. Mutation responses must be read and their confirmed-save versus catalog-refresh status distinguished. Never treat abort/transport loss as a rollback. The UI keeps previous display under loading/error treatment where appropriate and has a retry path. When the removed focused chip disappears, return focus to a stable search/recovery control without stealing focus from a user who moved elsewhere. + +| Action | Exact paths and ownership | +| --- | --- | +| MODIFY | `src/codex/catalog/provider-fetch.ts`: static default seed and adjacent comment, preserving forward/static/live boundaries. | +| ADD | `gui/src/provider-workspace/model-inventory.ts`: strict existing-DTO parser, provider grouping/count projection, and narrowly needed custom-response identity parsing; no network or second native classifier. Search existing owners before each helper. | +| ADD | `gui/src/components/provider-workspace/ProviderModelChip.tsx`: focused existing chip markup with accessible copy/Delete/Hide controls; authority remains in ProviderModels. This keeps its stateful parent below the 400-line limit. | +| MODIFY | `gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx`: paired parent reads, revision-bound snapshot/readiness, canonical inventory counts and props. | +| MODIFY | `gui/src/pages/Providers.tsx`, `gui/src/components/provider-workspace/ProviderDetails.tsx`: pass rows/revision/readiness and existing Models navigation. Preserve keyed provider mounts. | +| MODIFY | `gui/src/components/provider-workspace/ProviderModels.tsx`: stable custom ownership, revision readiness, disjoint one-request mutations, confirmed identity, feedback/reconciliation, canonical row view and Add policy. No session-long removed-ID set. | +| MODIFY | `gui/src/icons.tsx`: source EyeOff utility icon, preserving existing icon grammar. | +| MODIFY | `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,zh,zh-TW}.ts`: every displayed new label/outcome/confirmation across all nine files; retain D Logs keys. Use existing keys where their meaning fits. | +| MODIFY | `gui/tests/provider-model-custom-add.test.tsx`: preserve Add coverage, update realistic DTO/revision fixtures and the native override/ambiguous-save cases. | +| ADD | `gui/tests/provider-model-management.test.tsx`: stateful server-backed Delete/Hide/reload/recovery and asynchronous readiness/focus cases. | +| ADD | `gui/tests/provider-model-inventory.test.ts`: malformed DTO/identity, namespace collisions, unique inventory counts and successful-empty semantics. GUI tests are outside the root layout registry. | +| MODIFY | Existing workspace/Providers tests that render the touched chain: update their paired endpoint fixtures and verify counts/provenance. Only actual affected callers, found by search, are changed. | +| MODIFY | `tests/codex-integration/codex-catalog.test.ts`: static-default/retain/explicit/forward/live-empty behavior and no-network oracles. | +| MODIFY | `tests/codex-integration/model-visibility-management-api.test.ts`: actual custom DELETE then GET restoration/identity, independent hide state and unchanged target validation. | +| OMIT source hunk | `src/server/management/model-routes.ts`, `gui/src/provider-workspace/usage.ts`, `tests/server/model-discovery-management-api.test.ts`: do not add the proposed disabled-map API/parser/assertion; existing response and helper meanings stay intact. | +| MODIFY | The source's provider-reference and codex-integration guide changes in English plus fr/ja/ko/ru/tr/zh-cn/zh-tw: static seeding and existing visibility policy. | +| MODIFY | `docs-site/src/content/docs/{,fr/,ja/,ko/,ru/,tr/,zh-cn/,zh-tw/}guides/web-dashboard.md`: Delete definition versus Hide, count semantics and existing Models recovery; retain D Logs descriptions. | +| MODIFY | `structure/03_catalog-and-subagents.md`: ordered static seed union and provider-workspace inventory/identity/Delete/Hide contract. No direct-routing permission change. | +| REPLACE artifact | `docs-site/public/pr-screenshots/3659-provider-model-removal.png`: capture the actual amended UI in isolated compiled QA; the source image is historical. Add only necessary state/viewport evidence. | + +Canonical count is unique non-disabled projected inventory before search and the 300-chip cap. SelectedModels remains a routed allowlist badge; native rows do not borrow a routed same-ID selection. A successful empty DTO never activates raw fallback. Native-only DTO raw IDs must not newly enter Add's duplicate set; preserve the pre-existing configured/discovered/custom and encoded-collision validation. Custom/native equal raw IDs remain distinct namespaced chips where both are projected. + +## Design and verification contract + +Keep the existing wrapping chip layout, typography, tokens and icons. Delete confirmation explicitly says it removes the custom definition and may reveal an underlying model. Hide confirmation explains catalog visibility and preserves direct routing policy. Keep an always-visible, keyboard-accessible Models recovery action, including empty/after-Hide/error states. No hidden panel, new deep-link protocol, additional permission flow or visual redesign. + +Final GUI checks run on the remote exact branch: `cd gui && bun test --isolate tests`, `bun run lint`, `bun run lint:i18n`, `bun run build`. Root focused checks include catalog, model-discovery management, model-visibility API and the import-connected set; source-oracle/subprocess paths are explicitly covered. Root typecheck/privacy and docs build run remotely. Hosted exact-head Linux/macOS functional CI and actual target composition remain merge gates; final all-Windows dispatch remains in 060. + +Browser QA uses the actual remotely compiled dashboard with an isolated synthetic management state. Capture desktop and narrow Korean layouts plus: custom-only deletion/re-add, custom/native and custom/live restoration, independent Hide and remount, existing Models recovery, error/ambiguous-save and pending-ownership states. Observe requests and resulting rows/counts; screenshots alone do not prove persistence. No real provider accounts, global proxy or deployment are touched. + +Delegation after A: main owns source carry/static seed/branch/FSM/PR integration; frontend writer owns the component/helper chain; separate GUI test writer owns behavioral fixtures; catalog/API worker owns backend regression cases; docs worker owns translations and public guides; independent reviewer owns C4 identity/security and final source audit; remote QA owns exact-head execution and browser evidence. Workers inherit the model and may delegate bounded subwork; no worker mutates main FSM or pushes/merges. + +## Reviewable PR layers within this work phase + +Use two dependent PRs for the two capabilities in source #3659. This is one management work-phase/PABCD cycle, not the Fable cycle. Layer 1 (`codex/lane-b-04-static-default`) carries static default seeding, source catalog regressions and static-provider documentation. Layer 2 (`codex/lane-b-04-management`) builds on layer 1 and contains the canonical DTO workspace, Delete/Hide controls, UI/API regression coverage, translations and rendered evidence. Both preserve gqchen attribution. The UI layer remains one coherent interaction contract; its larger regression matrix is necessary to review the three asynchronous resources and identity boundaries together. + +Main commits and publishes the static parent before switching the same bound checkout to its UI child, then delegates UI writers. No branch movement occurs under a running test/build. Verification for both prepared heads is collected in C; land the reviewed stack bottom-up (or a separately audited verified composition), retarget children before parent cleanup, and close original #3659 only after both capabilities are verified on dev. No source closure is claimed for the partial static landing. + +## Structural decision and dependency map + +The pressure is adding trusted row actions and refresh state to the existing 276-line ProviderModels while preserving a single server identity owner. Current edges are `ProviderWorkspaceShell -> usage/report`, `Providers -> ProviderDetails -> ProviderModels -> report/slug-codec`; `/api/models` identity comes from `src/server/management/model-rows.ts -> catalog/config`. The new parser is an HTTP read boundary, not a second identity policy. + +Chosen edges: the parent and child import the colocated pure `provider-workspace/model-inventory.ts`; its ModelRow dependency is type-only from `pages/models-shared.ts`. ProviderModels imports the colocated ProviderModelChip; that leaf uses existing icons/i18n and receives callbacks, never fetches or chooses authority. No barrel/public export, runtime server-to-GUI import, shared mutable store or backend layering change is introduced. Blast radius is this provider-workspace feature and its existing prop/test callers. + +The source disabled-map alternative is rejected because it duplicates native/custom identity policy and misses fallback/native state. Putting every new parser, state transition and chip into ProviderModels is rejected because it mixes response validation, authority and markup while approaching the 400-line boundary. The selected split leaves operation/state ownership visible in the existing parent/child. Existing component fetch conventions are retained rather than adding a query-library dependency or migrating unrelated server/cache ownership. Verify the paired read cost, no duplicate per-row request, exact prop callers, strict boundary parsing, focus and state generations in the planned remote tests/browser QA. + +## Historical source snapshot and full source patch + +Everything below is the original roadmap snapshot and public upstream diff. Its superseded two-write removal, disabled-map API, unconditional native inference and always-decrement count are not accepted implementation requirements. Active requirements are above. + + +Read on 2026-09-06 KST in `/Users/jun/.codex/worktrees/f80e/opencodex` at `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. Pinned source: [PR #3659](https://github.com/lidge-jun/opencodex/pull/3659), head `ff4e5cd5352b9c1bd05e3de0091f3483ca130be5`, source base `af50c6d3451078a7d298b044c08fd2684c9e8eeb`. Inputs are captured `.tmp/lane-b/3659.json` and `.patch`; no claim of a fresh remote status check is made. `git show -s` independently confirmed the head commit author below. + +| Source commit | Actual commit author | Subject | +|---|---|---| +| `2a41fea0229f7d2bcc9e90d6b614ad94bbd6802f` | gqchen <276851182@qq.com> | feat(gui): remove models from provider catalog | +| `34ace947a31aa154d77cd6d0eac67669304dd72b` | gqchen <276851182@qq.com> | fix(codex): sync static default-only providers | +| `13e6ea29e6904afff68c6eccd177c9e929f494d7` | gqchen <276851182@qq.com> | docs(pr): add provider model removal screenshot | +| `e005d028d3b4697676f17817b10de4c8ea4e2987` | gqchen <276851182@qq.com> | fix(gui): address provider model removal review | +| `ff4e5cd5352b9c1bd05e3de0091f3483ca130be5` | gqchen <276851182@qq.com> | fix(gui): distinguish hidden provider models | + +Preserve original commit authors on a clean replay; for reimplementation or squash put `Co-authored-by: gqchen <276851182@qq.com>` in each carried logical commit and the final squash body. PR login alone is not an author trailer. + +Safe carry strategy: main revalidates pinned source/head and incoming parent; replay the complete reviewed source series in order or reproduce its exact delta with attribution. Preserve source follow-up commits, not only the initial feature commit. Publish a child against its still-open parent; after parent squash, rebuild the child on the new dev ancestry and re-run exact-head CI. Retarget surviving children before parent branch deletion. Push uses the user's authorized `--no-verify` to avoid local hooks; this does not substitute for CI. Once dev contains the complete carried behavior, close the superseded source PR with a carry reference; do not close it for a partial/default-only slice. No linked issue is invented. + +## Exact change ledger + +Every source changed file is accounted for below. “Same base” means a read-only `git hash-object` of current file bytes matches the patch's old blob prefix; it does not prove future cherry-pick cleanliness. “Drift” requires contextual reconciliation. Source binary is explicitly unreviewed. All textual hunks were inspected as source behavior; appendix preserves exact before/after, including complete NEW test content. No source production file was edited. + +| Operation | Exact path | Baseline / disposition | +|---|---|---| +| NEW | `docs-site/public/pr-screenshots/3659-provider-model-removal.png` | Binary skipped: payload absent from text patch; visual proof required | +| MODIFY | `docs-site/src/content/docs/fr/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/fr/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/ja/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/ja/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/ko/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/ko/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/reference/configuration/providers.md` | Drift; preserve current unrelated edits | +| MODIFY | `docs-site/src/content/docs/ru/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/ru/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/tr/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/tr/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/zh-cn/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/zh-cn/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/zh-tw/guides/codex-integration.md` | Same base; reviewed textual delta | +| MODIFY | `docs-site/src/content/docs/zh-tw/reference/configuration/providers.md` | Same base; reviewed textual delta | +| MODIFY | `gui/src/components/provider-workspace/ProviderDetails.tsx` | Same base; reviewed textual delta | +| MODIFY | `gui/src/components/provider-workspace/ProviderModels.tsx` | Same base; reviewed textual delta | +| MODIFY | `gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx` | Same base; reviewed textual delta | +| MODIFY | `gui/src/i18n/de.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/en.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/fr.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/ja.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/ko.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/ru.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/tr.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/zh-TW.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/i18n/zh.ts` | Drift; preserve current unrelated edits | +| MODIFY | `gui/src/icons.tsx` | Same base; reviewed textual delta | +| MODIFY | `gui/src/pages/Providers.tsx` | Same base; reviewed textual delta | +| MODIFY | `gui/src/provider-workspace/usage.ts` | Same base; reviewed textual delta | +| MODIFY | `gui/tests/provider-model-custom-add.test.tsx` | Same base; reviewed textual delta | +| MODIFY | `src/codex/catalog/provider-fetch.ts` | Same base; reviewed textual delta | +| MODIFY | `src/server/management/model-routes.ts` | Same base; reviewed textual delta | +| MODIFY | `tests/codex-integration/codex-catalog.test.ts` | Same base; reviewed textual delta | +| MODIFY | `tests/server/model-discovery-management-api.test.ts` | Same base; reviewed textual delta | +| MODIFY (planned SoT addition) | `structure/03_catalog-and-subagents.md` | Public contract prose delta specified above; not in source PR | +| MODIFY (planned API doc addition) | `docs-site/src/content/docs/reference/management-api.md` | Add disabled response field alongside preceding phases | + +## Execution boundary and verifier + +This is a docs-only roadmap deliverable, not an implementation or merge receipt. The main agent owns 000, the goal/FSM, branch operations, integration, and final acceptance. No local tests, suites, typecheck, builds, hooks, provider requests, commits, pushes, or GitHub writes were run by this researcher. The next implementation cycle must re-read this plan against its actual parent tip. + +Loop archetype: spec-satisfaction repair. Trigger: carry the pinned public source PR into lane B. Stop: required behaviors, exact-head CI, reviewer disposition and parent integration all have durable evidence. Expected result: DONE only after verified dev ancestry; NOOP only if equivalent behavior is already landed; unresolved correctness/CI evidence is pending, not DONE. Scope and unattended resource bounds are inherited from main's 000; this document does not arm or amend the goal. Upward escalation: return concrete caller/test evidence to main if the carry contract cannot be satisfied; additional worker dispatch requires main's planned scope. + +CI-only verifier, inspected at the baseline below: + +- `.github/workflows/ci.yml:5` uses unfiltered `pull_request`, so child PR bases are supported. `changes` at lines 181-218 admits `src/**`, `tests/**`, `scripts/**`, `gui/**`; source changes also admit packaging. +- Linux `test 1/4..4/4`, lines 255-316, invokes `scripts/ci/run-bun-test-batches.sh`. That runner enumerates `tests` (line 197), admits `.test.ts` files (lines 46-68), and excludes only storage/API-usage families into dedicated jobs; the catalog/management files in this plan are included. Each actual batch runs `bun test --isolate --timeout 60000` under a process timeout (lines 121-126). Read logs to prove the named files ran; aggregate green alone is insufficient. +- `gates`, lines 390-449, runs root TypeScript (`bun x tsc --noEmit`), GUI tests (`cd gui && bun test --isolate tests`), privacy scan and skill-surface check. GUI changes additionally run `bun run lint` and `bun run build`; `gui/package.json:8` expands build to `tsc -b && vite build`. GUI lint is `oxlint .`, including changed locale/UI inputs; the separately named `lint:i18n` script is not a dedicated CI step. +- macOS runs two full-suite shards (lines 451-547). Windows full-suite six shards (lines 658-769) run only on `workflow_dispatch` with lane `all`; do not infer Windows full-suite coverage from packaging smoke or PR aggregate success. Main must obtain an exact-ref dispatch if Windows full-suite evidence is required, then verify the run head. +- `.github/workflows/deploy-docs.yml:3-10,24-33` builds Astro only on main push or manual dispatch and then deploys. Normal PR CI has no Astro docs build. Do not trigger this deploy workflow merely to obtain a pre-merge check. Main must arrange an approved non-deploy CI verifier on the exact candidate commit or explicitly retain this as a readiness gap; this research does not add workflow code or authorize deployment. +- Save head SHA, parent/base SHA, run URL, executed job conclusions, named test logs, approvals and unresolved-thread disposition. Source-author reported passes are historical claims, not carried-head validation. Never attest local checks that were intentionally prohibited. + + +## Pinned public source delta + +Reconcile only the touched hunks at the implementation parent. The conceptual deltas above and acceptance amendments take precedence over copying this source verbatim. This appendix records public source-PR behavior only. + +````diff +diff --git a/docs-site/public/pr-screenshots/3659-provider-model-removal.png b/docs-site/public/pr-screenshots/3659-provider-model-removal.png +new file mode 100644 +index 0000000000..dafe828720 +Binary files /dev/null and b/docs-site/public/pr-screenshots/3659-provider-model-removal.png differ +diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md +index 091a63a787..fe0a5d687a 100644 +--- a/docs-site/src/content/docs/fr/guides/codex-integration.md ++++ b/docs-site/src/content/docs/fr/guides/codex-integration.md +@@ -311,8 +311,9 @@ S'il manque un modèle dans Codex, ou si l'ordre ou la visibilité du catalogue + d'autorisation n'atteint jamais le catalogue. + 2. **`disabledModels`** au niveau supérieur — masque les modèles dans le catalogue comme dans `/v1/models`, et + fait passer les identifiants GPT natifs non qualifiés à `visibility: "hide"`. +-3. **`liveModels: false` avec `models` vide** — lorsque la découverte en direct est désactivée et que `models` +- est vide ou absent, opencodex n'expose aucun modèle routé pour ce fournisseur. ++3. **`liveModels: false`** — lorsque la découverte en direct est désactivée, les modèles routés proviennent de ++ `models` et `retainModels`. Si `models` est vide ou absent, un `defaultModel` configuré est également inclus ; ++ si aucun de ces champs ne fournit d'identifiant, opencodex n'expose aucun modèle routé. + 4. **Cursor `GetUsableModels`** — l'adaptateur Cursor découvre les modèles par son appel RPC protobuf + `GetUsableModels`, et non par `/models` ; une modification côté Cursor peut donc changer les identifiants visibles + indépendamment des autres fournisseurs. +diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md +index bec7932c09..dcfcc0af63 100644 +--- a/docs-site/src/content/docs/fr/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md +@@ -93,7 +93,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Style de l'en-tête de clé Anthropic. La valeur par défaut est l'en-tête natif `x-api-key` ; ce champ n'est valable que pour les fournisseurs `anthropic` authentifiés par clé. | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Pool multi-clés. `apiKey` reflète l'entrée active ; chaque élément a `id`, `key`, `label` facultatif et `addedAt` numérique facultatif. | + | `defaultModel?` | `string` | Modèle utilisé lorsque ce fournisseur est sélectionné sans modèle explicite. | +-| `models?` | `string[]` | Liste initiale ou de repli des modèles. Avec `liveModels: false`, ce sont les seuls modèles découverts. | ++| `models?` | `string[]` | Liste initiale ou de repli. Avec `liveModels: false`, les modèles routés proviennent de `models` et `retainModels` ; `defaultModel` est aussi inclus lorsque `models` est vide. | + | `liveModels?` | `boolean` | Récupère le catalogue actif au démarrage et lors de la synchronisation (true par défaut). Les fournisseurs personnalisés utilisent `${baseUrl}/models` ; les fournisseurs intégrés peuvent employer une URL de registre et un filtre. | + | `selectedModels?` | `string[]` | Liste autorisée du catalogue après la découverte. Non vide expose uniquement ces identifiants ; vide ou omis expose tous les modèles découverts. | + | `contextWindow?` | `number` | Repli contextuel à l’échelle du fournisseur lorsque les métadonnées en amont sont absentes ; sinon, un plafond qui conserve des métadonnées en direct plus petites. Le tableau de bord Modèles expose cela séparément de `providerContextCaps`. | +@@ -435,8 +435,8 @@ modèle. Le même mappage s'applique à un sélecteur natif `vercel/` + + ## Listes autorisées de modèles statiques + +-Réglez `liveModels: false` pour exposer uniquement `models`. Si `models` est vide ou omis, le fournisseur n'expose +-aucun modèle routé. La découverte dynamique rejette plus de 4 Mio ou 2 000 lignes de modèle brutes avant leur mise en cache ; ++Réglez `liveModels: false` pour exposer uniquement les modèles configurés dans `models` et `retainModels`. Si `models` est vide ou omis, ++un `defaultModel` configuré est également inclus. Si aucun de ces champs ne fournit d'identifiant, aucun modèle routé n'est exposé. La découverte dynamique rejette plus de 4 Mio ou 2 000 lignes de modèle brutes avant leur mise en cache ; + les préréglages intégrés peuvent appliquer des limites inférieures et filtrer les lignes admissibles à la conversation. Les résultats trop volumineux ou mal formés + utilisent le catalogue obsolète ou configuré comme solution de repli. Un résultat valide ne contenant aucun modèle admissible fait autorité et n'est pas + silencieusement remplacé ou tronqué. +diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md +index b2ae7fcb91..62f5b0a662 100644 +--- a/docs-site/src/content/docs/guides/codex-integration.md ++++ b/docs-site/src/content/docs/guides/codex-integration.md +@@ -454,8 +454,9 @@ If a model is missing from Codex, or the catalog order/visibility looks wrong, c + catalog. + 2. **`disabledModels`** (top level) — hides models from both the catalog and `/v1/models`, and flips + bare native GPT slugs to `visibility: "hide"`. +-3. **`liveModels: false` with empty `models`** — when live discovery is off and `models` is empty or +- omitted, opencodex exposes no routed models for that provider. ++3. **`liveModels: false`** — with live discovery off, routed models come from `models` and ++ `retainModels`. When `models` is empty or omitted, a configured `defaultModel` is included too; ++ if none of those fields supplies an id, opencodex exposes no routed models. + 4. **Cursor `GetUsableModels`** — the Cursor adapter discovers models through its protobuf + `GetUsableModels` RPC, not `/models`, so a Cursor-side change can alter which ids are visible + independently of other providers. +diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md +index 06cd590084..d49fae078c 100644 +--- a/docs-site/src/content/docs/ja/guides/codex-integration.md ++++ b/docs-site/src/content/docs/ja/guides/codex-integration.md +@@ -197,8 +197,9 @@ ocx sync-cache + 空または省略すると、検出されたすべてのモデルが公開されます。ホワイトリストにない ID はカタログに到達しません。 + 2. **`disabledModels`** (トップレベル) — カタログと `/v1/models` の両方からモデルを非表示にし、反転します + 裸のネイティブ GPT スラッグを `visibility: "hide"` にします。 +-3. **`liveModels: false` と空の `models`** — ライブ検出がオフで、`models` が空の場合、または +-省略すると、opencodex はそのプロバイダーのルーティング モデルを公開しません。 ++3. **`liveModels: false`** — ライブ検出がオフの場合、ルーティングモデルは `models` と ++`retainModels` から取得されます。`models` が空または省略されている場合は構成済みの `defaultModel` も含まれ、 ++いずれのフィールドにも ID がない場合のみルーティングモデルを公開しません。 + 4. **Cursor `GetUsableModels`** — Cursor アダプターはその protobuf を通じてモデルを検出します。 + `/models` ではなく `GetUsableModels` RPC であるため、カーソル側の変更により、他のプロバイダーとは独立して表示される ID が変更される可能性があります。 + 5. **キャッシュと `ocx sync`** - ライブ カタログは約 5 分間キャッシュされます (`modelCacheTtlMs`、 +diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md +index bd33d34a3f..41e1de7aa3 100644 +--- a/docs-site/src/content/docs/ja/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md +@@ -81,7 +81,7 @@ account を削除しても mapping は保持され、同じ id を再追加す + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic キーのヘッダー スタイル。デフォルトはネイティブ `x-api-key` です。キー認証 `anthropic` プロバイダーにのみ有効です。 | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` |マルチキープール。 `apiKey` はアクティブなエントリをミラーリングします。各項目には `id`、`key`、オプションの `label`、およびオプションの数値 `addedAt` があります。 | + | `defaultModel?` | `string` |このプロバイダーが明示的なモデルなしで選択された場合に使用されるモデル。 | +-| `models?` | `string[]` |シード/フォールバック モデルのリスト。 `liveModels: false` では、発見されたモデルはこれらのみです。 | ++| `models?` | `string[]` |シード/フォールバック モデルのリスト。`liveModels: false` ではルーティングモデルは `models` と `retainModels` から取得され、`models` が空の場合は `defaultModel` も含まれます。 | + | `liveModels?` | `boolean` |開始/同期時にライブ カタログをフェッチします (デフォルトは `true`)。カスタムプロバイダーは `${baseUrl}/models` を使用します。組み込みはレジストリ URL とフィルターを使用する場合があります。 | + | `selectedModels?` | `string[]` |検出後のカタログ許可リスト。空でない場合は、それらの ID のみが公開されます。空または省略すると、検出されたすべてのモデルが公開されます。 | + | `modelDisplayNames?` | `Record` | このプロバイダーの正確なネイティブモデル ID をキーにした、永続的な表示専用ラベルです。大文字と小文字は区別されます。ラベルはプロバイダーカタログのメタデータより優先され、認証、アダプター、ルーティング、課金、上流リクエストには影響しません。マップは検出上限と同じ 2,000 件までです。 | +@@ -354,7 +354,7 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ + + ## 静的モデルのホワイトリスト + +-`models` のみを公開するように `liveModels: false` を設定します。 `models` が空であるか省略されている場合、プロバイダーはルーティングされたモデルを公開しません。ライブ ディスカバリは、キャッシュする前に 4 MiB または 2,000 を超える生のモデル行を拒否します。組み込みのプリセットは下限を使用し、チャットに適した行にフィルターをかけることができます。サイズが大きすぎる、または形式が正しくない結果は、古い/構成されたフォールバックに続きます。ゼロに適格な有効な結果は引き続き権威を持ち、暗黙的に置き換えられたり切り捨てられたりすることはありません。 ++構成済みモデルのみを公開するように `liveModels: false` を設定します。ルーティングモデルは `models` と `retainModels` から取得され、`models` が空または省略されている場合は構成済みの `defaultModel` も含まれます。いずれのフィールドにも ID がない場合のみ、ルーティングモデルを公開しません。ライブ ディスカバリは、キャッシュする前に 4 MiB または 2,000 を超える生のモデル行を拒否します。組み込みのプリセットは下限を使用し、チャットに適した行にフィルターをかけることができます。サイズが大きすぎる、または形式が正しくない結果は、古い/構成されたフォールバックに続きます。ゼロに適格な有効な結果は引き続き権威を持ち、暗黙的に置き換えられたり切り捨てられたりすることはありません。 + + 検出を実行する必要があるが、選択した ID のみが Codex および `/v1/models` に表示される必要がある場合は、`selectedModels` を使用します。ダッシュボードには、後で許可リストを変更できるように、検出された完全なリストが保持されます。 + +diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md +index 3f777153ec..ece36df6af 100644 +--- a/docs-site/src/content/docs/ko/guides/codex-integration.md ++++ b/docs-site/src/content/docs/ko/guides/codex-integration.md +@@ -197,7 +197,7 @@ Codex에서 model이 빠졌거나 catalog 순서/가시성이 이상해 보이 + + 1. provider의 **`selectedModels`** - 비어 있지 않은 allowlist는 해당 id만 Codex에 노출합니다. 비어 있거나 생략하면 발견된 model이 모두 노출됩니다. allowlist에 없는 id는 catalog에 절대 들어가지 않습니다. + 2. **`disabledModels`**(top level) - catalog와 `/v1/models`에서 model을 숨기고, bare native GPT slug는 `visibility: "hide"`로 바꿉니다. +-3. **`liveModels: false`와 비어 있는 `models`** - live discovery가 꺼져 있고 `models`가 비어 있거나 생략되면, opencodex는 그 provider에 대해 routed model을 하나도 노출하지 않습니다. ++3. **`liveModels: false`** - live discovery가 꺼져 있으면 routed model은 `models`와 `retainModels`에서 가져옵니다. `models`가 비어 있거나 생략되면 구성된 `defaultModel`도 포함되며, 어느 필드에도 ID가 없을 때만 routed model을 노출하지 않습니다. + 4. **Cursor `GetUsableModels`** - Cursor adapter는 `/models`가 아니라 protobuf `GetUsableModels` RPC로 model을 찾습니다. 그래서 Cursor 쪽 변경이 다른 provider와 무관하게 어떤 id가 보이는지 바꿀 수 있습니다. + 5. **캐시와 `ocx sync`** - live catalog는 약 5분(`modelCacheTtlMs`, 기본값 `300000`) 동안 캐시됩니다. `ocx sync`를 실행하면 새로 가져와서 catalog를 즉시 다시 쓸 수 있습니다. + 6. **실행 중인 Codex `app-server`** - 오래 살아 있는 Codex `app-server`(Desktop / CLI background host)가 이전 목록을 메모리에 쥐고 있으면 디스크 catalog를 다시 쓰는 것만으로는 부족합니다. `ocx sync`와 `ocx sync-cache`는 그런 process를 감지하면 경고합니다. `ocx sync --restart-codex`로 다시 시작하거나(아니면 일치하는 `app-server` process를 직접 중지한 뒤), Codex가 다시 만들게 해서 새 목록이 보이게 하세요. +diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md +index 3d65dbfb4d..49b911d2c8 100644 +--- a/docs-site/src/content/docs/ko/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md +@@ -81,7 +81,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic 키 헤더 형식입니다. 기본값은 네이티브 `x-api-key`이며, 키 인증 `anthropic` 공급자에만 유효합니다. | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 다중 키 풀입니다. `apiKey`는 활성 항목을 그대로 반영하며, 각 항목에는 `id`, `key`, 선택적 `label`, 선택적 숫자 `addedAt`가 들어갑니다. | + | `defaultModel?` | `string` | 이 공급자를 선택할 때 모델을 따로 지정하지 않으면 사용하는 모델입니다. | +-| `models?` | `string[]` | 시드/폴백 모델 목록입니다. `liveModels: false`이면 이 목록만 발견된 모델로 취급합니다. | ++| `models?` | `string[]` | 시드/폴백 모델 목록입니다. `liveModels: false`이면 라우팅 모델은 `models`와 `retainModels`에서 가져오며, `models`가 비어 있으면 `defaultModel`도 포함됩니다. | + | `liveModels?` | `boolean` | 시작 또는 동기화 시 라이브 카탈로그를 가져옵니다. 기본값은 `true`입니다. 사용자 지정 공급자는 `${baseUrl}/models`를 사용하고, 내장은 레지스트리 URL을 사용한 뒤 필터링할 수 있습니다. | + | `selectedModels?` | `string[]` | 발견 후 카탈로그 허용 목록입니다. 값이 비어 있지 않으면 그 id만 노출하고, 비어 있거나 생략하면 발견된 모델을 모두 노출합니다. | + | `modelDisplayNames?` | `Record` | 이 공급자의 정확한 네이티브 모델 id를 키로 쓰는 영구 표시 전용 이름입니다. 키는 대소문자를 구분합니다. 이름은 공급자 카탈로그 메타데이터보다 우선하며 인증, 어댑터, 라우팅, 청구 또는 업스트림 요청을 바꾸지 않습니다. 맵은 발견 한도와 같은 최대 2,000개 항목을 가질 수 있습니다. | +@@ -361,7 +361,7 @@ Vercel AI Gateway는 하나의 모델을 여러 기반 추론 공급자에 걸 + + ## 정적 모델 허용 목록 + +-`liveModels: false`로 두면 `models`만 노출합니다. `models`가 비어 있거나 생략되면 공급자는 어떤 라우팅 모델도 노출하지 않습니다. 라이브 발견은 캐싱 전에 4 MiB 또는 원시 모델 행 2,000개를 넘으면 거부합니다. 내장 프리셋은 더 낮은 한도를 쓰고 chat 가능한 행만 필터링할 수 있습니다. 너무 크거나 형식이 잘못된 결과는 오래된/설정된 폴백을 따릅니다. 유효하지만 선택 가능한 항목이 0개인 결과는 그대로 권위가 있으며, 조용히 다른 값으로 바꾸거나 잘라내지 않습니다. ++`liveModels: false`로 두면 구성된 모델만 노출합니다. 라우팅 모델은 `models`와 `retainModels`에서 가져오며, `models`가 비어 있거나 생략되면 구성된 `defaultModel`도 포함됩니다. 어느 필드에도 ID가 없을 때만 라우팅 모델을 노출하지 않습니다. 라이브 발견은 캐싱 전에 4 MiB 또는 원시 모델 행 2,000개를 넘으면 거부합니다. 내장 프리셋은 더 낮은 한도를 쓰고 chat 가능한 행만 필터링할 수 있습니다. 너무 크거나 형식이 잘못된 결과는 오래된/설정된 폴백을 따릅니다. 유효하지만 선택 가능한 항목이 0개인 결과는 그대로 권위가 있으며, 조용히 다른 값으로 바꾸거나 잘라내지 않습니다. + + `selectedModels`는 발견은 계속하되, 선택된 id만 Codex와 `/v1/models`에 나타나게 하고 싶을 때 사용합니다. 대시보드는 나중에 허용 목록을 바꿀 수 있도록 발견된 전체 목록을 보관합니다. + +diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md +index ab8a154ecb..2a56f4ceb9 100644 +--- a/docs-site/src/content/docs/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/reference/configuration/providers.md +@@ -136,7 +136,7 @@ predictions. Explicit provider/model price overrides still take precedence. + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key header style. Defaults to native `x-api-key`; valid only for key-auth `anthropic` providers. | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Multi-key pool. `apiKey` mirrors the active entry; each item has `id`, `key`, optional `label`, and optional numeric `addedAt`. | + | `defaultModel?` | `string` | Model used when this provider is selected without an explicit model. | +-| `models?` | `string[]` | Seed/fallback model list. With `liveModels: false`, these are the only discovered models. | ++| `models?` | `string[]` | Seed/fallback model list. With `liveModels: false`, routed models come from `models` and `retainModels`; `defaultModel` is also included when `models` is empty. | + | `liveModels?` | `boolean` | Fetch the live catalog on start/sync (default `true`). Custom providers use `${baseUrl}/models`; built-ins may use a registry URL and filter. | + | `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. | + | `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. | +@@ -738,8 +738,8 @@ container usually has no unlocked keychain session, so requests would fail close + `${ENV_VAR}` reference in the service environment there instead. Env references are left untouched + by `store`. + +-Set `liveModels: false` to expose only `models`. If `models` is empty or omitted, the provider exposes +-no routed models. Live discovery rejects more than 4 MiB or 2,000 raw model rows before caching; ++Set `liveModels: false` to expose only configured models from `models` and `retainModels`. If `models` ++is empty or omitted, a configured `defaultModel` is included too. If none of those fields supplies an id, the provider exposes no routed models. Live discovery rejects more than 4 MiB or 2,000 raw model rows before caching; + built-in presets may use lower limits and filter to chat-eligible rows. Oversized or malformed results + follow stale/configured fallback. A valid zero-eligible result remains authoritative and is not + silently replaced or truncated. +diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md +index e33bb6c835..ebdec89431 100644 +--- a/docs-site/src/content/docs/ru/guides/codex-integration.md ++++ b/docs-site/src/content/docs/ru/guides/codex-integration.md +@@ -304,8 +304,9 @@ Codex на встроенный провайдер `openai` и удалите л + allowlist, никогда не попадёт в каталог. + 2. **`disabledModels`** (верхний уровень) — скрывает модели и из каталога, и из `/v1/models`, а у + голых нативных GPT-slug устанавливает `visibility: "hide"`. +-3. **`liveModels: false` и пустой `models`** — если живое обнаружение выключено, а `models` пуст +- или отсутствует, opencodex не показывает ни одной маршрутизируемой модели этого провайдера. ++3. **`liveModels: false`** — если живое обнаружение выключено, маршрутизируемые модели берутся из ++ `models` и `retainModels`. Если `models` пуст или отсутствует, также включается настроенный `defaultModel`; ++ если ни одно из этих полей не содержит идентификатор, opencodex не показывает маршрутизируемых моделей. + 4. **Cursor `GetUsableModels`** — адаптер Cursor получает модели через protobuf RPC + `GetUsableModels`, а не через `/models`, поэтому изменение на стороне Cursor может менять + видимые id независимо от остальных провайдеров. +diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md +index a058fdb842..f31d0ffc13 100644 +--- a/docs-site/src/content/docs/ru/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md +@@ -94,7 +94,7 @@ cross-route credential fallback не существует. Строки API GPT- + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Header-style для ключа Anthropic. По умолчанию нативный `x-api-key`; допустим только для key-auth-провайдеров `anthropic`. | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Пул из нескольких ключей. `apiKey` зеркалит активную запись; каждый элемент содержит `id`, `key`, необязательный `label` и необязательное числовое `addedAt`. | + | `defaultModel?` | `string` | Модель, используемая когда этот провайдер выбран без явной модели. | +-| `models?` | `string[]` | Seed/fallback-список моделей. При `liveModels: false` это и есть единственный список обнаруженных моделей. | ++| `models?` | `string[]` | Seed/fallback-список. При `liveModels: false` маршрутизируемые модели берутся из `models` и `retainModels`; если `models` пуст, также включается `defaultModel`. | + | `liveModels?` | `boolean` | Получать live-каталог на start/sync (по умолчанию `true`). Custom-провайдеры используют `${baseUrl}/models`; built-in могут использовать registry URL и дополнительно фильтровать результат. | + | `selectedModels?` | `string[]` | Allowlist каталога после discovery. Непустой список показывает только эти id; пустой или отсутствующий показывает всё, что было обнаружено. | + | `modelDisplayNames?` | `Record` | Постоянные display-only имена с точным нативным id модели этого провайдера в качестве ключа. Ключи чувствительны к регистру. Имена имеют приоритет над metadata каталога провайдера и не меняют аутентификацию, adapter, routing, billing или upstream-запросы. Карта содержит не более 2 000 записей, как и discovery. | +@@ -439,8 +439,8 @@ Chat-запросов не добавляют поле `provider`, а Vercel AI + + ## Статические allowlist'ы моделей + +-Задайте `liveModels: false`, чтобы показывать только `models`. Если `models` пуст или отсутствует, +-провайдер не будет показывать ни одной маршрутизируемой модели. Live-discovery отвергает ответы ++Задайте `liveModels: false`, чтобы показывать только настроенные модели из `models` и `retainModels`. Если `models` пуст или отсутствует, ++также включается настроенный `defaultModel`. Если ни одно из этих полей не содержит идентификатор, провайдер не показывает маршрутизируемых моделей. Live-discovery отвергает ответы + размером более 4 MiB или более 2000 сырых model-row до кэширования; built-in preset'ы могут + использовать меньшие лимиты и фильтровать список до chat-совместимых строк. Oversized или + malformed-результаты откатываются к stale/configured fallback. Валидный результат с нулём +diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md +index 7692980e91..39ef68406d 100644 +--- a/docs-site/src/content/docs/tr/guides/codex-integration.md ++++ b/docs-site/src/content/docs/tr/guides/codex-integration.md +@@ -353,9 +353,9 @@ sırayla kontrol edin: + 2. **`disabledModels`** (üst düzey) — modelleri hem katalogdan hem de + `/v1/models` listesinden gizler ve yalın yerel GPT slug'larını `visibility: + "hide"` olarak değiştirir. +-3. **Boş `models` ile `liveModels: false`** — canlı keşif kapalı olduğunda ve +- `models` boş veya atlandığında opencodex bu sağlayıcı için hiçbir +- yönlendirilmiş model göstermez. ++3. **`liveModels: false`** — canlı keşif kapalı olduğunda yönlendirilmiş modeller `models` ve ++ `retainModels` alanlarından gelir. `models` boş veya atlanmışsa yapılandırılmış `defaultModel` da eklenir; ++ bu alanların hiçbiri bir kimlik sağlamıyorsa opencodex yönlendirilmiş model göstermez. + 4. **Cursor `GetUsableModels`** — Cursor adaptörü modelleri `/models` üzerinden + değil, protobuf `GetUsableModels` RPC'si üzerinden keşfeder; bu nedenle + Cursor tarafındaki bir değişiklik diğer sağlayıcılardan bağımsız olarak hangi +diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md +index 4213ab6001..27fe115fc1 100644 +--- a/docs-site/src/content/docs/tr/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md +@@ -100,7 +100,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic anahtar başlığı stili. Varsayılan olarak yerel `x-api-key`; yalnızca anahtar kimlik doğrulamalı `anthropic` sağlayıcıları için geçerlidir. | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Çoklu anahtar havuzu. `apiKey` aktif girdiyi yansıtır; her öğe `id`, `key`, isteğe bağlı `label` ve isteğe bağlı sayısal `addedAt` değerine sahiptir. | + | `defaultModel?` | `string` | Bu sağlayıcı açık bir model olmadan seçildiğinde kullanılan model. | +-| `models?` | `string[]` | Tohum/geri dönüş model listesi. `liveModels: false` olduğunda bunlar keşfedilen tek modellerdir. | ++| `models?` | `string[]` | Tohum/geri dönüş listesi. `liveModels: false` iken yönlendirilen modeller `models` ve `retainModels` alanlarından gelir; `models` boşsa `defaultModel` da eklenir. | + | `liveModels?` | `boolean` | Başlatmada/senkronizasyonda canlı kataloğu getirin (varsayılan `true`). Özel sağlayıcılar `${baseUrl}/models` kullanır; yerleşikler bir kayıt defteri URL'si ve filtresi kullanabilir. | + | `selectedModels?` | `string[]` | Keşiften sonra katalog izin listesi. Boş olmaması yalnızca bu kimlikleri gösterir; boş veya atlanmış olması keşfedilen tüm modelleri gösterir. | + | `contextWindow?` | `number` | Yukarı akış meta verileri olmadığında sağlayıcı genelinde bağlam geri dönüşü; aksi takdirde daha küçük canlı meta verileri koruyan bir sınır. Modeller kontrol paneli bunu `providerContextCaps` alanından ayrı olarak gösterir. | +@@ -476,8 +476,8 @@ uygulamadan önce yerel `zai/glm-5.2` kimliğini geri yükler. Aynı eşleme yer + + ## Statik model izin listeleri + +-Yalnızca `models`'ı göstermek için `liveModels: false` ayarlayın. `models` boşsa +-veya atlanırsa sağlayıcı yönlendirilen hiçbir modeli göstermez. Canlı keşif, ++Yalnızca yapılandırılmış modelleri göstermek için `liveModels: false` ayarlayın. Yönlendirilen modeller `models` ve `retainModels` alanlarından gelir; ++`models` boşsa veya atlanırsa yapılandırılmış `defaultModel` da eklenir. Bu alanların hiçbiri bir kimlik sağlamıyorsa yönlendirilmiş model gösterilmez. Canlı keşif, + önbelleğe almadan önce 4 MiB'den veya 2.000 ham model satırından fazlasını + reddeder; yerleşik önayarlar daha düşük sınırlar kullanabilir ve sohbete uygun + satırlara filtre uygulayabilir. Büyük boyutlu veya hatalı biçimlendirilmiş +diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +index e2a5601d62..e9ddcf3307 100644 +--- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md ++++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +@@ -260,8 +260,8 @@ provider 形式一样,从 `OPENCODEX_API_AUTH_TOKEN` 传入 `x-opencodex-api-k + 所有发现到的模型。一个不在 allowlist 里的 id 永远不会进入 catalog。 + 2. **`disabledModels`**(顶层) - 会同时隐藏 catalog 和 `/v1/models` 中的模型,并把裸原生 GPT slug + 切成 `visibility: "hide"`。 +-3. **`liveModels: false` 且 `models` 为空** - 当 live discovery 关闭而 `models` 为空或省略时,opencodex +- 不会为那个 provider 暴露任何路由模型。 ++3. **`liveModels: false`** - 关闭 live discovery 后,路由模型来自 `models` 和 `retainModels`。 ++ 当 `models` 为空或省略时,还会包含已配置的 `defaultModel`;这些字段都没有提供 id 时,opencodex 才不暴露路由模型。 + 4. **Cursor `GetUsableModels`** - Cursor adapter 通过它的 protobuf `GetUsableModels` RPC 发现模型,而不是 + `/models`,所以 Cursor 侧的变动会独立于其他 provider 改变哪些 id 可见。 + 5. **缓存和 `ocx sync`** - live catalog 的缓存时间大约是五分钟(`modelCacheTtlMs`,默认 `300000`)。 +diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +index f2d245b5ec..142faba847 100644 +--- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +@@ -81,7 +81,7 @@ selector,而不是分配一个新名称。 + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key 头部样式。默认使用原生 `x-api-key`;仅对 key-auth `anthropic` 提供者有效。 | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 多 key 池。`apiKey` 会镜像当前激活条目;每个条目都有 `id`、`key`、可选 `label`,以及可选的数值 `addedAt`。 | + | `defaultModel?` | `string` | 当选择该提供者但未显式指定模型时使用的模型。 | +-| `models?` | `string[]` | 种子/回退模型列表。配合 `liveModels: false` 时,这些就是唯一发现到的模型。 | ++| `models?` | `string[]` | 种子/回退模型列表。配合 `liveModels: false` 时,路由模型来自 `models` 和 `retainModels`;`models` 为空时还会包含 `defaultModel`。 | + | `liveModels?` | `boolean` | 启动/同步时获取实时目录(默认 `true`)。自定义提供者使用 `${baseUrl}/models`;内置项可能使用注册表 URL 并进行过滤。 | + | `selectedModels?` | `string[]` | 发现之后的目录允许列表。非空时只暴露这些 id;为空或省略时则暴露全部发现到的模型。 | + | `modelDisplayNames?` | `Record` | 持久的仅显示名称,以此提供者的精确原生模型 id 为键。键区分大小写。名称优先于提供者目录元数据,并且不会改变身份验证、适配器、路由、计费或上游请求。该映射最多可包含 2,000 个条目,与发现上限相同。 | +@@ -357,7 +357,7 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型 + + ## 静态模型允许列表 + +-将 `liveModels: false` 设为只暴露 `models`。如果 `models` 为空或省略,该提供者将不暴露任何路由模型。实时发现会在缓存前拒绝超过 4 MiB 或 2,000 条原始模型行;内置预设可能使用更低的限制,并过滤为可聊天的行。过大或格式错误的结果会走陈旧/配置回退。合法的、零可用结果的发现仍然具有权威性,不会被静默替换或截断。 ++将 `liveModels: false` 设为只暴露已配置模型。路由模型来自 `models` 和 `retainModels`;如果 `models` 为空或省略,还会包含已配置的 `defaultModel`。这些字段都没有提供 id 时才不暴露路由模型。实时发现会在缓存前拒绝超过 4 MiB 或 2,000 条原始模型行;内置预设可能使用更低的限制,并过滤为可聊天的行。过大或格式错误的结果会走陈旧/配置回退。合法的、零可用结果的发现仍然具有权威性,不会被静默替换或截断。 + + 当需要继续运行发现,但只有选定 id 应该出现在 Codex 和 `/v1/models` 中时,请使用 `selectedModels`。仪表板会保留完整的已发现列表,以便之后调整允许列表。 + +diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +index 4166276fbc..f096f22502 100644 +--- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md ++++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +@@ -266,8 +266,8 @@ OpenCodex 直接注入路由,請先將 Codex 切回內建 `openai` provider, + 已發現模型。不在 allowlist 中的 id 永遠不會進入目錄。 + 2. **`disabledModels`(頂層)**:會同時從目錄與 `/v1/models` 隱藏模型,並把裸原生 GPT slug 設為 + `visibility: "hide"`。 +-3. **`liveModels: false` 且 `models` 為空**:當即時探索關閉,且 `models` 為空或省略時,opencodex +- 不會為該 provider 暴露任何路由模型。 ++3. **`liveModels: false`**:關閉即時探索後,路由模型來自 `models` 和 `retainModels`。 ++ 當 `models` 為空或省略時,還會包含已設定的 `defaultModel`;這些欄位皆未提供 id 時,opencodex 才不暴露路由模型。 + 4. **Cursor `GetUsableModels`**:Cursor adapter 透過 protobuf `GetUsableModels` RPC 探索模型,而不是 + `/models`,所以 Cursor 端變更可獨立改變可見 id。 + 5. **cache 與 `ocx sync`**:即時目錄約快取五分鐘(`modelCacheTtlMs`,預設 `300000`)。執行 +diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +index 7a27de4f61..5154957052 100644 +--- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md ++++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +@@ -63,7 +63,7 @@ ocx models provider openrouter on + | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic 金鑰標頭風格。預設為原生 `x-api-key`;僅對 key-auth `anthropic` 供應商有效。 | + | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 多金鑰池。`apiKey` 反映現用項目;每個項目有 `id`、`key`、可選 `label` 與可選數值 `addedAt`。 | + | `defaultModel?` | `string` | 在未指定明確模型時選擇此供應商所使用的模型。 | +-| `models?` | `string[]` | 播種/後備模型清單。在 `liveModels: false` 時,這些是唯一探索的模型。 | ++| `models?` | `string[]` | 播種/後備模型清單。`liveModels: false` 時,路由模型來自 `models` 和 `retainModels`;`models` 為空時還會包含 `defaultModel`。 | + | `liveModels?` | `boolean` | 在啟動/同步時擷取即時目錄(預設 `true`)。自訂供應商使用 `${baseUrl}/models`;內建可能使用 registry URL 並過濾。 | + | `selectedModels?` | `string[]` | 探索後的目錄允許清單。非空時僅暴露那些 id;空或省略時暴露所有探索的模型。 | + | `contextWindow?` | `number` | 供應商範圍的 Codex 可見 context 上限。較小的即時中繼資料被保留。 | +@@ -324,7 +324,7 @@ Vercel AI Gateway 可在多個底層推論供應商之間路由一個模型。`v + + ## 靜態模型允許清單 + +-設定 `liveModels: false` 以僅暴露 `models`。若 `models` 為空或省略,供應商暴露無路由模型。即時探索在快取前拒絕超過 4 MiB 或 2,000 個原始模型列;內建預設可能使用較低限制並過濾到 chat 合格列。過大或格式錯誤的結果遵循過時/設定的後備。有效的零合格結果恆為權威,且不被靜默取代或截斷。 ++設定 `liveModels: false` 以僅暴露已設定模型。路由模型來自 `models` 和 `retainModels`;若 `models` 為空或省略,還會包含已設定的 `defaultModel`。這些欄位皆未提供 id 時才不暴露路由模型。即時探索在快取前拒絕超過 4 MiB 或 2,000 個原始模型列;內建預設可能使用較低限制並過濾到 chat 合格列。過大或格式錯誤的結果遵循過時/設定的後備。有效的零合格結果恆為權威,且不被靜默取代或截斷。 + + 當探索應仍然執行但只有 selected id 應出現在 Codex 與 `/v1/models` 時,請使用 `selectedModels`。儀表板保留完整的探索清單供日後允許清單變更。 + +diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx +index 645511f69d..7a1f66a213 100644 +--- a/gui/src/components/provider-workspace/ProviderDetails.tsx ++++ b/gui/src/components/provider-workspace/ProviderDetails.tsx +@@ -32,6 +32,7 @@ export default function ProviderDetails({ + availableModels, + hasLiveModels, + selectedModels, ++ disabledModels, + modelsLoading, + modelsLoadFailed, + onRetryModels, +@@ -65,6 +66,7 @@ export default function ProviderDetails({ + /** Server-reported live-catalog provenance; see filterModels(). */ + hasLiveModels: boolean; + selectedModels: string[]; ++ disabledModels: string[]; + modelsLoading?: boolean; + modelsLoadFailed?: boolean; + onRetryModels?: () => void; +@@ -293,6 +295,7 @@ export default function ProviderDetails({ + availableModels={availableModels} + hasLiveModels={hasLiveModels} + selectedModels={selectedModels} ++ disabledModels={disabledModels} + modelsLoading={modelsLoading} + modelsLoadFailed={modelsLoadFailed} + needsReauth={ +diff --git a/gui/src/components/provider-workspace/ProviderModels.tsx b/gui/src/components/provider-workspace/ProviderModels.tsx +index 56cc588292..2f89b886d6 100644 +--- a/gui/src/components/provider-workspace/ProviderModels.tsx ++++ b/gui/src/components/provider-workspace/ProviderModels.tsx +@@ -7,14 +7,19 @@ import { useEffect, useMemo, useRef, useState } from "react"; + import { useT } from "../../i18n/shared"; + import type { WorkspaceItem } from "../../provider-workspace/catalog"; + import { filterModels } from "../../provider-workspace/report"; ++import { IconEyeOff, IconTrash } from "../../icons"; ++import { putModelVisibility } from "../../model-visibility"; + import { encodedModelIdCollides } from "../../../../src/providers/slug-codec"; + ++type CustomModelRef = { id?: string; modelId: string }; ++ + export default function ProviderModels({ + item, + apiBase, + availableModels, + hasLiveModels, + selectedModels, ++ disabledModels, + modelsLoading = false, + modelsLoadFailed = false, + needsReauth = false, +@@ -25,6 +30,7 @@ export default function ProviderModels({ + apiBase: string; + availableModels: string[]; + selectedModels: string[]; ++ disabledModels: string[]; + /** Server-reported: did the last successful discovery return any rows? */ + hasLiveModels: boolean; + modelsLoading?: boolean; +@@ -38,16 +44,20 @@ export default function ProviderModels({ + const [query, setQuery] = useState(""); + const [customModelId, setCustomModelId] = useState(""); + const [customSaving, setCustomSaving] = useState(false); ++ const [removingModelId, setRemovingModelId] = useState(null); ++ const [removedModelIds, setRemovedModelIds] = useState>(() => new Set()); + const [customError, setCustomError] = useState(""); + const [customSuccess, setCustomSuccess] = useState(""); +- const [customModelIds, setCustomModelIds] = useState([]); ++ const [customModels, setCustomModels] = useState([]); + const [customModelsReady, setCustomModelsReady] = useState(false); + const [customModelsLoadFailed, setCustomModelsLoadFailed] = useState(false); + const [customModelsLoadEpoch, setCustomModelsLoadEpoch] = useState(0); + const [copiedId, setCopiedId] = useState(null); + const copyResetRef = useRef(null); + const selectedSet = useMemo(() => new Set(selectedModels), [selectedModels]); ++ const hiddenSet = useMemo(() => new Set([...disabledModels, ...removedModelIds]), [disabledModels, removedModelIds]); + const configuredModels = useMemo(() => item.models ?? [], [item.models]); ++ const customModelIds = useMemo(() => customModels.map(model => model.modelId), [customModels]); + const trimmedCustomModelId = customModelId.trim(); + const knownModelIds = [ + ...availableModels, +@@ -63,8 +73,9 @@ export default function ProviderModels({ + || item.defaultModel === trimmedCustomModelId + || encodedModelIdCollides(trimmedCustomModelId, knownModelIds); + const models = useMemo( +- () => filterModels(availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels), +- [availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels], ++ () => filterModels(availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels) ++ .filter(modelId => !hiddenSet.has(modelId)), ++ [availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels, hiddenSet], + ); + + useEffect(() => { +@@ -76,17 +87,20 @@ export default function ProviderModels({ + const rows: unknown = await response.json(); + if (!Array.isArray(rows)) throw new Error("Invalid custom model list"); + if (!active) return; +- setCustomModelIds(rows.flatMap(row => { ++ setCustomModels(rows.flatMap(row => { + if (!row || typeof row !== "object") return []; +- const model = row as { provider?: unknown; modelId?: unknown }; +- return model.provider === item.name && typeof model.modelId === "string" ? [model.modelId] : []; ++ const model = row as { id?: unknown; provider?: unknown; modelId?: unknown }; ++ return model.provider === item.name ++ && typeof model.modelId === "string" ++ ? [{ ...(typeof model.id === "string" ? { id: model.id } : {}), modelId: model.modelId }] ++ : []; + })); + setCustomModelsLoadFailed(false); + setCustomError(""); + setCustomModelsReady(true); + } catch { + if (!active) return; +- setCustomModelIds([]); ++ setCustomModels([]); + // Without this the component stays permanently unable to add a model: `customModelsReady` + // never flips back and the effect has no trigger left, so a single transient GET failure + // disabled Add until the whole panel remounted. +@@ -136,7 +150,20 @@ export default function ProviderModels({ + body: JSON.stringify({ provider: item.name, modelId: trimmedCustomModelId }), + }); + if (response.ok) { +- setCustomModelIds(ids => ids.includes(trimmedCustomModelId) ? ids : [...ids, trimmedCustomModelId]); ++ const added: unknown = await response.json(); ++ if (!added || typeof added !== "object" || typeof (added as { id?: unknown }).id !== "string") { ++ setCustomError(t("models.customSaveFailed")); ++ return; ++ } ++ const id = (added as { id: string }).id; ++ setCustomModels(models => models.some(model => model.modelId === trimmedCustomModelId) ++ ? models ++ : [...models, { id, modelId: trimmedCustomModelId }]); ++ setRemovedModelIds(ids => { ++ const next = new Set(ids); ++ next.delete(trimmedCustomModelId); ++ return next; ++ }); + setCustomModelId(""); + setCustomSuccess(t("models.customAdded")); + onRetryModels?.(); +@@ -150,6 +177,39 @@ export default function ProviderModels({ + } + }; + ++ const removeModel = async (modelId: string) => { ++ const customModel = customModels.find(model => model.modelId === modelId && model.id); ++ if (removingModelId || !window.confirm(t(customModel ? "models.customDeleteConfirm" : "models.hideConfirm", { name: modelId }))) return; ++ const visibilityTarget = { id: modelId, ...(item.name === "openai" ? { native: true } : {}) }; ++ setRemovingModelId(modelId); ++ setCustomError(""); ++ setCustomSuccess(""); ++ try { ++ if (customModel?.id) { ++ const deleteResponse = await fetch(`${apiBase}/api/custom-models/${encodeURIComponent(customModel.id)}`, { method: "DELETE" }); ++ if (!deleteResponse.ok) { ++ setCustomError(t("models.customSaveFailed")); ++ return; ++ } ++ setCustomModels(models => models.filter(model => model.modelId !== modelId)); ++ } ++ const visibilityResponse = await putModelVisibility(apiBase, "models", item.name, [visibilityTarget], false); ++ if (!visibilityResponse.ok) { ++ onRetryModels?.(); ++ setCustomError(t("models.saveFailed")); ++ return; ++ } ++ setRemovedModelIds(ids => new Set(ids).add(modelId)); ++ setCustomSuccess(t(customModel ? "models.customDeleted" : "models.applied")); ++ onRetryModels?.(); ++ } catch { ++ onRetryModels?.(); ++ setCustomError(t("models.networkError")); ++ } finally { ++ setRemovingModelId(null); ++ } ++ }; ++ + const emptyBase = availableModels.length === 0 + && configuredModels.length === 0 + && customModelIds.length === 0 +@@ -247,7 +307,9 @@ export default function ProviderModels({ + {visibleModels.map(modelId => { + const isDefault = modelId === item.defaultModel; + const isSelected = selectedSet.has(modelId); ++ const isCustom = customModels.some(model => model.modelId === modelId && model.id); + const copied = copiedId === modelId; ++ const removeLabel = t(isCustom ? "models.customDelete" : "models.hide"); + return ( +
  • + +
  • + ); + })} +diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx +index 91faecb6fc..24fe187f94 100644 +--- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx ++++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx +@@ -24,7 +24,7 @@ import { + import { providerKind } from "../../provider-workspace/kind"; + import { readJsonIfOk, readJsonOrThrow } from "../../fetch-json"; + import { readSessionListCache, writeSessionListCache } from "../../session-list-cache"; +-import { buildProviderModelUsage, buildProviderUsageTotals, countAvailableModels, parseAvailableModels, parseLiveModelCounts, parseSelectedModels, type ProviderAvailableModels, type ProviderLiveModelCounts, type ProviderModelCounts, type ProviderSelectedModels } from "../../provider-workspace/usage"; ++import { buildProviderModelUsage, buildProviderUsageTotals, countAvailableModels, parseAvailableModels, parseDisabledModels, parseLiveModelCounts, parseSelectedModels, type ProviderAvailableModels, type ProviderDisabledModels, type ProviderLiveModelCounts, type ProviderModelCounts, type ProviderSelectedModels } from "../../provider-workspace/usage"; + import { + freshQuotaReportRecord, + freshQuotaReportsFromResponse, +@@ -47,6 +47,7 @@ export interface DetailSlotData { + /** Did the last successful discovery return rows? Server-reported, never inferred. */ + hasLiveModels: boolean; + selectedModels: string[]; ++ disabledModels: string[]; + modelsLoading: boolean; + modelsLoadFailed: boolean; + onRetryModels?: () => void; +@@ -141,6 +142,7 @@ export default function ProviderWorkspaceShell({ + const [availableModels, setAvailableModels] = useState({}); + const [liveModelCounts, setLiveModelCounts] = useState({}); + const [selectedModels, setSelectedModels] = useState({}); ++ const [disabledModels, setDisabledModels] = useState({}); + const [modelsLoading, setModelsLoading] = useState(false); + const [modelsLoadFailed, setModelsLoadFailed] = useState(false); + const quotasCacheKey = `ocx.providers.quotas.v1:${apiBase}`; +@@ -189,6 +191,7 @@ export default function ProviderWorkspaceShell({ + setAvailableModels(parseAvailableModels(data)); + setLiveModelCounts(parseLiveModelCounts(data)); + setSelectedModels(parseSelectedModels(data)); ++ setDisabledModels(parseDisabledModels(data)); + setModelsLoadFailed(false); + succeeded = true; + } catch { +@@ -559,6 +562,7 @@ export default function ProviderWorkspaceShell({ + availableModels: availableModels[selectedItem.name] ?? [], + hasLiveModels: (liveModelCounts[selectedItem.name] ?? 0) > 0, + selectedModels: selectedModels[selectedItem.name] ?? [], ++ disabledModels: disabledModels[selectedItem.name] ?? [], + modelsLoading, + modelsLoadFailed, + onRetryModels: retryModels, +diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts +index 4d4b79e5d5..c29aad7355 100644 +--- a/gui/src/i18n/de.ts ++++ b/gui/src/i18n/de.ts +@@ -577,6 +577,8 @@ export const de: Record = { + "models.customEdit": "Bearbeiten", + "models.customDelete": "Löschen", + "models.customDeleteConfirm": "Modell {name} löschen?", ++ "models.hide": "Ausblenden", ++ "models.hideConfirm": "Modell {name} aus dem Katalog ausblenden?", + "models.customBadge": "Benutzerdefiniert", + "models.customSummary": "{count} benutzerdefiniert", + "models.customFieldModelId": "Modell-ID (Endpunkt-Slug)", +diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts +index cf9eb253dd..ae4761051f 100644 +--- a/gui/src/i18n/en.ts ++++ b/gui/src/i18n/en.ts +@@ -602,6 +602,8 @@ export const en = { + "models.customEdit": "Edit", + "models.customDelete": "Delete", + "models.customDeleteConfirm": "Delete the {name} model?", ++ "models.hide": "Hide", ++ "models.hideConfirm": "Hide the {name} model from the catalog?", + "models.customBadge": "Custom", + "models.customSummary": "{count} custom", + "models.customFieldModelId": "Model ID (endpoint slug)", +diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts +index 2d90382f1a..455898688e 100644 +--- a/gui/src/i18n/fr.ts ++++ b/gui/src/i18n/fr.ts +@@ -587,6 +587,8 @@ export const fr: Record = { + "models.customEdit": "Modifier", + "models.customDelete": "Supprimer", + "models.customDeleteConfirm": "Supprimer le modèle {name} ?", ++ "models.hide": "Masquer", ++ "models.hideConfirm": "Masquer le modèle {name} du catalogue ?", + "models.customBadge": "Personnalisé", + "models.customSummary": "{count} personnalisés", + "models.customFieldModelId": "ID du modèle (slug du point de terminaison)", +diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts +index c9d2e9ea4a..d413daa401 100644 +--- a/gui/src/i18n/ja.ts ++++ b/gui/src/i18n/ja.ts +@@ -2315,6 +2315,8 @@ export const ja: Record = { + "models.customEdit": "Edit", + "models.customDelete": "Delete", + "models.customDeleteConfirm": "Delete the {name} model?", ++ "models.hide": "非表示", ++ "models.hideConfirm": "モデル {name} をカタログから非表示にしますか?", + "models.customBadge": "Custom", + "models.customSummary": "{count} custom", + "models.customFieldModelId": "Model ID (endpoint slug)", +diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts +index d9c983a5fb..7ffa869a59 100644 +--- a/gui/src/i18n/ko.ts ++++ b/gui/src/i18n/ko.ts +@@ -588,6 +588,8 @@ export const ko: Record = { + "models.customEdit": "편집", + "models.customDelete": "삭제", + "models.customDeleteConfirm": "{name} 모델을 삭제하시겠습니까?", ++ "models.hide": "숨기기", ++ "models.hideConfirm": "{name} 모델을 카탈로그에서 숨기시겠습니까?", + "models.customBadge": "커스텀", + "models.customSummary": "커스텀 {count}개", + "models.customFieldModelId": "모델 ID (엔드포인트 슬러그)", +diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts +index 950cea7a81..324eae13fd 100644 +--- a/gui/src/i18n/ru.ts ++++ b/gui/src/i18n/ru.ts +@@ -590,6 +590,8 @@ export const ru: Record = { + "models.customEdit": "Изменить", + "models.customDelete": "Удалить", + "models.customDeleteConfirm": "Удалить модель {name}?", ++ "models.hide": "Скрыть", ++ "models.hideConfirm": "Скрыть модель {name} из каталога?", + "models.customBadge": "Пользовательская", + "models.customSummary": "Пользовательских: {count}", + "models.customFieldModelId": "ID модели (slug эндпоинта)", +diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts +index 5db3ca23b5..37865f57c8 100644 +--- a/gui/src/i18n/tr.ts ++++ b/gui/src/i18n/tr.ts +@@ -593,6 +593,8 @@ export const tr: Record = { + "models.customEdit": "Düzenle", + "models.customDelete": "Sil", + "models.customDeleteConfirm": "{name} modeli silinsin mi?", ++ "models.hide": "Gizle", ++ "models.hideConfirm": "{name} modeli katalogda gizlensin mi?", + "models.customBadge": "Özel", + "models.customSummary": "{count} özel", + "models.customFieldModelId": "Model ID", +diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts +index 06f8e6fa4b..b8a842bbcf 100644 +--- a/gui/src/i18n/zh-TW.ts ++++ b/gui/src/i18n/zh-TW.ts +@@ -456,6 +456,8 @@ export const zhTW: Record = { + "models.customEdit": "編輯", + "models.customDelete": "刪除", + "models.customDeleteConfirm": "要刪除模型 {name} 嗎?", ++ "models.hide": "隱藏", ++ "models.hideConfirm": "要從目錄中隱藏模型 {name} 嗎?", + "models.customBadge": "自訂", + "models.customSummary": "{count} 個自訂模型", + "models.customFieldModelId": "模型 ID(端點標識)", +diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts +index 315869d88d..28758e6157 100644 +--- a/gui/src/i18n/zh.ts ++++ b/gui/src/i18n/zh.ts +@@ -585,6 +585,8 @@ export const zh: Record = { + "models.customEdit": "编辑", + "models.customDelete": "删除", + "models.customDeleteConfirm": "要删除模型 {name} 吗?", ++ "models.hide": "隐藏", ++ "models.hideConfirm": "要从目录中隐藏模型 {name} 吗?", + "models.customBadge": "自定义", + "models.customSummary": "{count} 个自定义模型", + "models.customFieldModelId": "模型 ID(端点标识)", +diff --git a/gui/src/icons.tsx b/gui/src/icons.tsx +index 6ec2ebf096..70afe03db9 100644 +--- a/gui/src/icons.tsx ++++ b/gui/src/icons.tsx +@@ -24,6 +24,7 @@ export const IconRefresh = (p: P) => (); + export const IconPlay = (p: P) => (); + export const IconTrash = (p: P) => (); ++export const IconEyeOff = (p: P) => (); + export const IconPencil = (p: P) => (); + export const IconAlert = (p: P) => (); + export const IconInfo = (p: P) => (); +diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx +index 8701b3acd3..84832e2d1b 100644 +--- a/gui/src/pages/Providers.tsx ++++ b/gui/src/pages/Providers.tsx +@@ -459,6 +459,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { + availableModels={data.availableModels} + hasLiveModels={data.hasLiveModels} + selectedModels={data.selectedModels} ++ disabledModels={data.disabledModels} + modelsLoading={data.modelsLoading} + modelsLoadFailed={data.modelsLoadFailed} + onRetryModels={data.onRetryModels} +diff --git a/gui/src/provider-workspace/usage.ts b/gui/src/provider-workspace/usage.ts +index 033bdbcee2..4cae58417a 100644 +--- a/gui/src/provider-workspace/usage.ts ++++ b/gui/src/provider-workspace/usage.ts +@@ -16,6 +16,7 @@ import type { ProviderModelUsageRow } from "../components/provider-workspace/typ + export type ProviderModelCounts = Record; + export type ProviderAvailableModels = Record; + export type ProviderSelectedModels = Record; ++export type ProviderDisabledModels = Record; + + /** Parse `/api/selected-models` available map into provider -> model id list. */ + export function parseAvailableModels(data: unknown): ProviderAvailableModels { +@@ -64,10 +65,26 @@ export function parseSelectedModels(data: unknown): ProviderSelectedModels { + return models; + } + ++/** Parse `/api/selected-models` disabled map into provider -> hidden model id list. */ ++export function parseDisabledModels(data: unknown): ProviderDisabledModels { ++ if (!data || typeof data !== "object") return {}; ++ const disabled = (data as { disabled?: unknown }).disabled; ++ if (!disabled || typeof disabled !== "object" || Array.isArray(disabled)) return {}; ++ ++ const models: ProviderDisabledModels = {}; ++ for (const [provider, ids] of Object.entries(disabled)) { ++ if (!Array.isArray(ids)) continue; ++ models[provider] = ids.filter((id): id is string => typeof id === "string"); ++ } ++ return models; ++} ++ + export function countAvailableModels(data: unknown): ProviderModelCounts { + const counts: ProviderModelCounts = {}; ++ const disabled = parseDisabledModels(data); + for (const [provider, models] of Object.entries(parseAvailableModels(data))) { +- counts[provider] = models.length; ++ const hidden = new Set(disabled[provider] ?? []); ++ counts[provider] = models.filter(model => !hidden.has(model)).length; + } + return counts; + } +diff --git a/gui/tests/provider-model-custom-add.test.tsx b/gui/tests/provider-model-custom-add.test.tsx +index 6ebc04134a..ee3046845b 100644 +--- a/gui/tests/provider-model-custom-add.test.tsx ++++ b/gui/tests/provider-model-custom-add.test.tsx +@@ -5,6 +5,7 @@ import type { Root } from "react-dom/client"; + import { LanguageProvider } from "../src/i18n/provider"; + import ProviderModels from "../src/components/provider-workspace/ProviderModels"; + import type { WorkspaceItem } from "../src/provider-workspace/catalog"; ++import { countAvailableModels } from "../src/provider-workspace/usage"; + + const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; + const originalFetch = globalThis.fetch; +@@ -40,11 +41,17 @@ const item = { + defaultModel: "claude-opus-5", + } as WorkspaceItem; + ++test("provider model counts exclude removed models", () => { ++ expect(countAvailableModels({ available: { vendor: ["a", "b", "c"] }, disabled: { vendor: ["b"] } })) ++ .toEqual({ vendor: 2 }); ++}); ++ + async function mountProviderModels( + availableModels = ["claude-opus-5"], + onRetryModels?: () => void, + providerItem = item, + hasLiveModels = true, ++ disabledModels: string[] = [], + ): Promise<{ root: Root; container: HTMLElement; input: HTMLInputElement; addButton: HTMLButtonElement }> { + const container = document.createElement("div"); + document.body.append(container); +@@ -59,6 +66,7 @@ async function mountProviderModels( + availableModels={availableModels} + hasLiveModels={hasLiveModels} + selectedModels={[]} ++ disabledModels={disabledModels} + apiBase="http://localhost:10100" + onRetryModels={onRetryModels} + /> +@@ -205,6 +213,194 @@ test("custom-only catalog keeps configured fallback models visible", async () => + await act(async () => { root.unmount(); }); + }); + ++test("custom models use their stable id and persist discovered-model visibility when deleted", async () => { ++ const requests: Array<{ url: string; method: string; body: unknown }> = []; ++ globalThis.fetch = (async (input, init) => { ++ if (!init?.method || init.method === "GET") { ++ return Response.json([ ++ { id: "custom-1", provider: "AiCodeWith", modelId: "claude-opus-5.1-custom" }, ++ ]); ++ } ++ requests.push({ ++ url: String(input), ++ method: init.method, ++ body: typeof init.body === "string" ? JSON.parse(init.body) : undefined, ++ }); ++ return Response.json({ ok: true }); ++ }) as typeof fetch; ++ testWindow.confirm = () => true; ++ ++ let refreshes = 0; ++ const { root, container } = await mountProviderModels( ++ ["claude-opus-5", "claude-opus-5.1-custom"], ++ () => { refreshes += 1; }, ++ ); ++ await act(async () => { await Promise.resolve(); }); ++ ++ const customChip = [...container.querySelectorAll(".pws-model-chip")] ++ .find(chip => chip.querySelector(".pws-model-id")?.textContent === "claude-opus-5.1-custom")!; ++ const deleteButton = customChip.querySelector('button[aria-label="Delete"]')!; ++ await act(async () => { ++ deleteButton.click(); ++ await Promise.resolve(); ++ await Promise.resolve(); ++ }); ++ ++ expect(requests).toEqual([ ++ { ++ url: "http://localhost:10100/api/custom-models/custom-1", ++ method: "DELETE", ++ body: undefined, ++ }, ++ { ++ url: "http://localhost:10100/api/model-visibility", ++ method: "PUT", ++ body: { ++ scope: "models", ++ provider: "AiCodeWith", ++ targets: [{ id: "claude-opus-5.1-custom" }], ++ enabled: false, ++ }, ++ }, ++ ]); ++ expect(refreshes).toBe(1); ++ expect([...container.querySelectorAll(".pws-model-id")].map(node => node.textContent)) ++ .toEqual(["claude-opus-5"]); ++ expect(container.querySelector('[role="status"]')?.textContent).toContain("Custom model deleted"); ++ ++ await act(async () => { root.unmount(); }); ++}); ++ ++test("a custom model stays visible when its visibility update fails", async () => { ++ globalThis.fetch = (async (_input, init) => { ++ if (!init?.method || init.method === "GET") { ++ return Response.json([ ++ { id: "custom-1", provider: "AiCodeWith", modelId: "claude-opus-5.1-custom" }, ++ ]); ++ } ++ if (init.method === "DELETE") return Response.json({ ok: true }); ++ return Response.json({ error: "failed" }, { status: 500 }); ++ }) as typeof fetch; ++ testWindow.confirm = () => true; ++ ++ const { root, container } = await mountProviderModels(["claude-opus-5.1-custom"]); ++ await act(async () => { await Promise.resolve(); }); ++ ++ await act(async () => { ++ container.querySelector('button[aria-label="Delete"]')!.click(); ++ await Promise.resolve(); ++ await Promise.resolve(); ++ }); ++ ++ expect([...container.querySelectorAll(".pws-model-id")].map(node => node.textContent)) ++ .toEqual(["claude-opus-5.1-custom"]); ++ expect(container.querySelector('[role="alert"]')?.textContent).toContain("Save failed"); ++ ++ await act(async () => { root.unmount(); }); ++}); ++ ++test("discovered models are labeled as hidden and removed from the local catalog", async () => { ++ const requests: Array<{ url: string; method: string; body: unknown }> = []; ++ globalThis.fetch = (async (input, init) => { ++ if (!init?.method || init.method === "GET") return Response.json([]); ++ requests.push({ ++ url: String(input), ++ method: init.method, ++ body: typeof init.body === "string" ? JSON.parse(init.body) : undefined, ++ }); ++ return Response.json({ ok: true }); ++ }) as typeof fetch; ++ let confirmation = ""; ++ testWindow.confirm = message => { ++ confirmation = String(message); ++ return true; ++ }; ++ ++ let refreshes = 0; ++ const { root, container } = await mountProviderModels( ++ ["claude-opus-5", "claude-sonnet-5"], ++ () => { refreshes += 1; }, ++ ); ++ await act(async () => { await Promise.resolve(); }); ++ ++ const discoveredChip = [...container.querySelectorAll(".pws-model-chip")] ++ .find(chip => chip.querySelector(".pws-model-id")?.textContent === "claude-sonnet-5")!; ++ const hideButton = discoveredChip.querySelector('button[aria-label="Hide"]')!; ++ expect(hideButton.title).toBe("Hide"); ++ await act(async () => { ++ hideButton.click(); ++ await Promise.resolve(); ++ }); ++ ++ expect(confirmation).toBe("Hide the claude-sonnet-5 model from the catalog?"); ++ expect(requests).toEqual([{ ++ url: "http://localhost:10100/api/model-visibility", ++ method: "PUT", ++ body: { ++ scope: "models", ++ provider: "AiCodeWith", ++ targets: [{ id: "claude-sonnet-5" }], ++ enabled: false, ++ }, ++ }]); ++ expect(refreshes).toBe(1); ++ expect([...container.querySelectorAll(".pws-model-id")].map(node => node.textContent)) ++ .toEqual(["claude-opus-5"]); ++ expect(container.querySelector('[role="status"]')?.textContent).toContain("Applied"); ++ ++ await act(async () => { root.unmount(); }); ++}); ++ ++test("native OpenAI models use a native visibility target when removed", async () => { ++ const requests: unknown[] = []; ++ globalThis.fetch = (async (_input, init) => { ++ if (!init?.method || init.method === "GET") return Response.json([]); ++ requests.push(typeof init.body === "string" ? JSON.parse(init.body) : undefined); ++ return Response.json({ ok: true }); ++ }) as typeof fetch; ++ testWindow.confirm = () => true; ++ const openAiItem = { ++ ...item, ++ name: "openai", ++ models: ["gpt-5.5"], ++ defaultModel: "gpt-5.5", ++ } as WorkspaceItem; ++ ++ const { root, container } = await mountProviderModels(["gpt-5.5"], undefined, openAiItem); ++ await act(async () => { await Promise.resolve(); }); ++ ++ await act(async () => { ++ container.querySelector('button[aria-label="Hide"]')!.click(); ++ await Promise.resolve(); ++ }); ++ ++ expect(requests).toEqual([{ ++ scope: "models", ++ provider: "openai", ++ targets: [{ id: "gpt-5.5", native: true }], ++ enabled: false, ++ }]); ++ ++ await act(async () => { root.unmount(); }); ++}); ++ ++test("disabled discovered models stay out of the provider model list", async () => { ++ globalThis.fetch = (async () => Response.json([])) as typeof fetch; ++ const { root, container } = await mountProviderModels( ++ ["claude-opus-5", "claude-sonnet-5"], ++ undefined, ++ item, ++ true, ++ ["claude-sonnet-5"], ++ ); ++ await act(async () => { await Promise.resolve(); }); ++ ++ expect([...container.querySelectorAll(".pws-model-id")].map(node => node.textContent)) ++ .toEqual(["claude-opus-5"]); ++ ++ await act(async () => { root.unmount(); }); ++}); ++ + // A single transient GET used to leave `customModelsReady` false forever: the effect had no + // remaining trigger, so Add stayed disabled until the whole panel remounted. Drive the full + // recovery in one mount: failed load -> retry -> successful load -> Add enabled -> exactly one POST. +diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts +index f810bfb422..55b90cfea4 100644 +--- a/src/codex/catalog/provider-fetch.ts ++++ b/src/codex/catalog/provider-fetch.ts +@@ -1506,11 +1506,14 @@ async function fetchProviderModelsWithAuth( + && prov.googleMode === "vertex" + && (prov.models?.length ?? 0) === 0 + && Boolean(prov.defaultModel); +- // Ordered dedupe union: Vertex seed, then `models`, then `retainModels`. `configured` is the ++ const seedStaticDefault = prov.liveModels === false ++ && (prov.models?.length ?? 0) === 0 ++ && Boolean(prov.defaultModel); ++ // Ordered dedupe union: implicit default seed, then `models`, then `retainModels`. `configured` is the + // single seed for the static path, the degraded fallback, drop diagnostics, and provider hints, + // so a retain-only id must enter here or it never exists to be retained (#1690). + const configuredIds = [...new Set([ +- ...(seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : []), ++ ...((seedVertexDefault || seedStaticDefault) && prov.defaultModel ? [prov.defaultModel] : []), + ...(prov.models ?? []), + ...(prov.retainModels ?? []), + ])]; +@@ -1563,9 +1566,8 @@ async function fetchProviderModelsWithAuth( + : resolveAuth.resolve(name, prov)); + const apiKey = auth.apiKey; + // A configured default is a real callable selector and must remain discoverable when a +- // compatible provider's live /models request fails (issue #308). Keep this separate from the +- // explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero +- // rows, while a failed live discovery may degrade to the default selector. ++ // compatible provider's live /models request fails (issue #308). Static providers already seed ++ // their default selector above when no explicit model list exists. + const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic" + ? configured + : [{ +diff --git a/src/server/management/model-routes.ts b/src/server/management/model-routes.ts +index e9ea26a90e..fd3f1eb43c 100644 +--- a/src/server/management/model-routes.ts ++++ b/src/server/management/model-routes.ts +@@ -787,7 +787,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise = {}; +- for (const m of models) (available[m.provider] ??= []).push(m.id); ++ const disabled: Record = {}; ++ const disabledSlugs = config.disabledModels ?? []; ++ for (const m of models) { ++ (available[m.provider] ??= []).push(m.id); ++ if (disabledSlugs.some(slug => slugEquals(slug, m.provider, m.id))) { ++ (disabled[m.provider] ??= []).push(m.id); ++ } ++ } + const selected: Record = {}; + // Live-catalog provenance. The GUI cannot infer this by subtracting known custom ids: an id + // that is both custom and discovered would make a real live catalog look custom-only. +@@ -797,7 +804,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise { + .toEqual([]); + }); + ++ test("filters dashboard-hidden provider models before catalog sync", () => { ++ const models = [ ++ { provider: "vendor", id: "visible-model" }, ++ { provider: "vendor", id: "hidden-model" }, ++ ]; ++ ++ expect(filterCatalogVisibleModels(models, { ++ disabledModels: ["vendor/hidden-model"], ++ providers: { vendor: {} }, ++ })).toEqual([{ provider: "vendor", id: "visible-model" }]); ++ }); ++ + test("repairs a provider row after its shadowing combo alias is disabled", () => { + const alias = "vendor/deepseek-v4-flash"; + const combo = deriveComboCatalogModel( +@@ -4061,6 +4073,36 @@ describe("Codex catalog routed normalization", () => { + } + }); + ++ test("liveModels false uses the default model when no static list is configured", async () => { ++ const originalFetch = globalThis.fetch; ++ let fetchCalls = 0; ++ globalThis.fetch = (() => { ++ fetchCalls += 1; ++ throw new Error("fetch should not be called"); ++ }) as typeof fetch; ++ try { ++ const models = await gatherRoutedModels({ ++ providers: { ++ "static-default": { ++ baseUrl: "https://example.invalid/v1", ++ adapter: "openai-chat", ++ authMode: "key", ++ liveModels: false, ++ defaultModel: "only-model", ++ }, ++ }, ++ }); ++ ++ expect(fetchCalls).toBe(0); ++ expect(models.map(m => `${m.provider}/${m.id}`)).toEqual([ ++ "static-default/only-model", ++ ]); ++ } finally { ++ globalThis.fetch = originalFetch; ++ clearModelCache("static-default"); ++ } ++ }); ++ + test("Google Antigravity honors an explicit static catalog and suppresses stale discovery", async () => { + const providerName = "google-antigravity"; + const provider = structuredClone(OAUTH_PROVIDERS[providerName].providerConfig); +diff --git a/tests/server/model-discovery-management-api.test.ts b/tests/server/model-discovery-management-api.test.ts +index ad132847bb..6d44c2f9ea 100644 +--- a/tests/server/model-discovery-management-api.test.ts ++++ b/tests/server/model-discovery-management-api.test.ts +@@ -39,4 +39,12 @@ describe("model discovery management API", () => { + expect(live.modelDiscovery.recentArrivals?.vendor).toEqual([]); + expect(live.disabledModels).toEqual(["vendor/new"]); + }); ++ ++ test("selected-models reports disabled discovered model ids by provider", async () => { ++ const live = config(); ++ live.disabledModels = ["vendor/known", "other/ignored"]; ++ const result = await call(live, "/api/selected-models"); ++ expect(result.json.available).toEqual({ vendor: ["known"] }); ++ expect(result.json.disabled).toEqual({ vendor: ["known"] }); ++ }); + }); + +```` diff --git a/devlog/_plan/260906_lane_b_catalog_stack/041_static_verification.md b/devlog/_plan/260906_lane_b_catalog_stack/041_static_verification.md new file mode 100644 index 0000000000..bc52d90e7b --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/041_static_verification.md @@ -0,0 +1,17 @@ +# 041 — Static default verification boundary + +Head `2e9006609` produced 281 passing catalog tests and one failure in both remote +and hosted verification. Its old Go fixture expected no models, but existing +registry ownership and capture-time enrichment already supplied `kimi-k2.7-code` +as that provider's effective default. The new static seed intentionally publishes it. + +The production seed remains unchanged. The replacement assertion requires exactly +that one inherited default and no metadata-roster augmentation, for omitted and +empty model lists, with zero upstream requests. A distinct custom MiMo destination +checks the existing strict transport guard, no inherited default or list, and an +exactly empty authoritative result. Unregistered no-default, explicit-list, +retention, forward and OAuth no-request controls remain in place. + +Independent source review confirmed this is an obsolete expectation for the +intentional new contract, not evidence of a new endpoint-ownership defect. The +initial failing result stays recorded; the amended tests require fresh verification. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/042_management_build.md b/devlog/_plan/260906_lane_b_catalog_stack/042_management_build.md new file mode 100644 index 0000000000..5dd7c2a5a1 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/042_management_build.md @@ -0,0 +1,23 @@ +# 042 — Canonical provider-workspace model controls + +The UI child carries the remaining source #3659 changes by gqchen, then adapts +them to the existing model-row authority. The proposed disabled-map API, parser +and server assertion were removed; their net diff from the static parent is zero. + +The workspace adopts `/api/models` with the full selection/provenance response. +Actions require the current parent revision and matching custom ownership. +Delete removes one custom definition; Hide changes the represented row's +visibility. Neither adds a second mutation, an implicit unhide or a browser-only +removal marker. Namespaced identity separates account-native/custom collisions. + +The implementation preserves ordinary raw-ID copying, native default badges and +the configured-fallback hint's prior condition. Missing discovery provenance +remains unknown; malformed present data and invalid action identity are rejected. +Icon-only actions expose their exact target in accessible names. + +Regression additions cover real API persistence/restoration and UI response-order, +single-flight, uncertain-result, remount, count, focus and identity scenarios. +All nine locales and eight dashboard guides describe the same contract, retaining +other lanes' changes. Independent C4 source review passed; execution and actual +compiled-browser evidence remain C gates. The inherited source screenshot is +historical and must be replaced before this child is review-ready. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/043_carry_boundary.md b/devlog/_plan/260906_lane_b_catalog_stack/043_carry_boundary.md new file mode 100644 index 0000000000..2bf9f77144 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/043_carry_boundary.md @@ -0,0 +1,20 @@ +# 043 — Preserve changes already integrated before the source carry + +The first UI verification at `1b90dba7` passed 23 API tests and 157 GUI tests, +with one GUI locator failure. Its build also exposed three missing Grok text keys. + +The carry used the PR's target-tip snapshot `af50c6d3` as a diff base, but source +`ff4e5cd5` actually shares merge base `6585e6a7` with that target. This accidentally +reversed an already integrated change in nine locale files and one English +provider-reference row. The original contributor's feature did not make those +reversals; this was a carry-boundary error. + +Restore only that established target delta, preserving the new management keys +and controls. Future carries use the actual Git merge base and review target +drift separately. The earlier B sources #3653, #3654 and #3571 were checked: +their recorded bases equal their actual merge bases, with no overlapping drift. + +The GUI locator must use the displayed provider name rather than assuming its +capitalization; all inventory, search/cap and native-selection assertions remain. +The repaired head requires fresh GUI execution and build evidence. Initial +failures remain recorded and are not reported as passing checks. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/044_react_doctor.md b/devlog/_plan/260906_lane_b_catalog_stack/044_react_doctor.md new file mode 100644 index 0000000000..26c2c72c74 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/044_react_doctor.md @@ -0,0 +1,18 @@ +# 044 — Separate React Doctor gate + +The cross-platform workflow passed at `b33d9347`, but the separate pinned React +Doctor 0.9.11 workflow failed. A cold remote reproduction reported one repeated +array-lookup warning, three test-render global-publication errors, and one unused +timestamp binding in a changed test file. Ordinary lint/build success did not +resolve this gate. + +Use a Set for selected-model membership while preserving the native exclusion. +Publish test controls from an effect, release only their owned handles on unmount, +and call the current harness setter directly from its retry callback. Remove the +unused pure timestamp calculation without changing the stale-coverage fixture. +No assertions, waiting bounds, scanner rules or warning threshold are relaxed. + +The repaired candidate needs a cold pinned scan, the affected GUI tests and its +current-head hosted checks. Existing browser evidence remains evidence of the +recorded source; reuse requires an explicit comparison of the small membership +lookup change rather than pretending the application bytes are unchanged. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/050_fable.md b/devlog/_plan/260906_lane_b_catalog_stack/050_fable.md new file mode 100644 index 0000000000..24e6e61080 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/050_fable.md @@ -0,0 +1,344 @@ +# 050 — Preserve Fable 1M picker selection (source PR #3649) + +Status: implementation plan only; no implementation or runtime validation performed. +Anchored 2026-09-06 to checkout `81871b3fa7034250b8d5ba2cbbfde44e40f0e69c`. +All paths below are repository-relative. Main owns 000, the goalplan, integration, and phase transitions. + +## Loop specification + +- Class: C3 catalog-to-ingress contract; the native passthrough portion requires explicit security review under MAINTAINERS.md:57-71. This research task is docs-only. +- Archetype: spec-satisfaction repair; one later PABCD cycle for this entire numbered plan. +- Trigger: Fable base and marked canonical selectors collapse into one client picker family (public source-PR report). +- Goal: retain independently selectable canonical Fable and 1M rows while forwarding canonical Fable on Messages/count_tokens. +- Non-goals: other Anthropic families, model-cap changes, generalized alias rewrite, Desktop registry repair (#3646), auth-policy changes, GUI component work, release operations. +- Verifier: exact-head hosted Cross-platform CI with actual Linux/macOS tests and gates; client picker evidence is a separate required artifact, not inferred from a unit test. +- Stop: all acceptance rows below have evidence, source contribution survives carry/squash, and main proves the carry is on dev before closing #3649. DONE is not available from this document alone. +- Memory artifact: this document plus main's 000 index and scratch review `.tmp/lane-b/plan-fable-review.md`. +- Bounds for this delegated research: local read-only source/metadata, two permitted documentation writes, no credentials or paid provider requests, no recursive workers needed; finish one bounded research pass. The consuming implementation inherits 000_plan.md: no numeric token/cost cap, six-hour work-phase checkpoint; restate this at the consuming P. +- Escalation: return concrete blockers to main; downstream delegation requires a P amendment; main reclaims a packet after two distinct workers fail it. +- Terminal meanings: DONE = later verified landing; NOOP = fresh dev already supplies the exact behavior/tests; BLOCKED = missing external CI/client evidence; UNSAFE/NEEDS_HUMAN = unresolved review decision; resource bounds never imply DONE. + +## Source and attribution + +Public source: https://github.com/lidge-jun/opencodex/pull/3649 + +- Source base: `45f3bed84be10a7e045a20aae1db46ab822bf7d0`. +- Source head: `95becce94255982667cef10308806770d49cc05b`. +- Behavior commit: `9a7795aa34df219654512366040d87a219fb4ada` (`fix(gateway): preserve Fable 1M picker selection`). +- Follow-up regression: `284fe8ca0b793d51c6cdd609f1c9acf219f2eaf9` (`test(gateway): cover marked Fable picker alias`). +- Head is a merge of `284fe8ca0` and `45f3bed84`; do not cherry-pick that merge as a third implementation change. +- Actual Git author AND committer of all three: `Éverton Toffanetto ` (`everton-dgn`). Verified with `git show -s --format=fuller` against locally available commit objects, corroborated by the supplied JSON commit list. +- Any squash or rewritten carry must include `Co-authored-by: Éverton Toffanetto `. Preserve original authors on cherry-picked commits; attribution in prose alone is insufficient. +- Input evidence: `.tmp/lane-b/3649.json` and `.tmp/lane-b/3649.patch`. JSON reports OPEN/MERGEABLE; this is a supplied snapshot, not a new live readiness claim. It contains no successful hosted CI evidence. + +## Current owners and activation path + +1. `src/server/index.ts:1479-1516` serves Anthropic discovery and calls `buildAnthropicModelInfos`. `?ids=cli` or a `claude-code/` UA selects readable IDs; explicit desktop/unknown UA retains Desktop IDs. Native registry setup remains untouched. +2. `src/claude/model-info.ts:146-165` owns 1M row generation. It requires authoritative context >= 1M, rejects already-marked IDs, deduplicates, and caps advertised input at min(1M, maxInputTokens). +3. `src/claude/model-info.ts:196-227` owns routed row emission; `listedModelId` already reflects the Cursor Fast exception. Put the Fable condition here, not in the generic alias encoder. +4. `src/claude/alias.ts:141-148` leaves canonical Anthropic IDs bare and exposes reversible native aliases. `resolveAlias` at :112-128 returns bare slugs for the native pseudo-provider. Reuse these functions without changing their public exports. +5. `src/claude/inbound-model-options.ts:39-68` resolves aliases before modelMap. `src/claude/inbound.ts` exports the resolver used by the server. Preserve that order. +6. `src/server/claude-messages.ts:634-668` strips `[1m]`, honors ocx-route, then parses Fast/effort. Insert the narrowly scoped Fable restoration between route override and synthetic-row parsing. `wantsNativePassthrough` at :151-168 subsequently examines the canonical model. +7. `src/server/claude-messages.ts:1069-1096` has the corresponding count_tokens path; insert restoration after countRoute and before Fast-only normalization. + +No configuration-only remedy repairs the emitted selector identity. No-op is ruled out by the current generic `${base.id}[1m]` at model-info.ts:153 and the absent helper/call sites. Reuse wins over a new registry, provider, flag, or global decoder. + +## Exact change map for the later implementation cycle + +| Operation | Path | Change | +|---|---|---| +| MODIFY | `src/claude/model-info.ts` | Optional selectorId in local push1mVariant; readable canonical Anthropic Fable-only 1M alias | +| MODIFY | `src/server/claude-messages.ts` | Import existing encoder; private Fable decoder; two ingress call sites | +| MODIFY | `tests/claude-integration/claude-model-info.test.ts` | Source positive test plus Fable-specific style/window regressions below | +| MODIFY | `tests/claude-integration/claude-native-passthrough.test.ts` | Source three-request regression plus canonical legacy/marker compatibility assertions | +| MODIFY | `docs-site/src/content/docs/guides/claude-code.md` | Explain the bounded Fable 1M exception in both canonical-ID statements | +| NEW | none (production/tests) | Existing owners and registered tests suffice | + +This research writes only this numbered document and the delegated scratch report. No schema/layout manifest update is needed: both test basenames already exist in `scripts/test-layout/layout.json:296-298` and `tests/fixtures/test-layout-expected.json:133-135`. + +## Focused source patch to carry + +Use the complete public four-file patch below. It includes the follow-up marked Messages case; carrying only the first commit drops that regression. Context line numbers belong to the source patch; match current owners above and refresh at P. + +```diff +diff --git a/src/claude/model-info.ts b/src/claude/model-info.ts +index cbecf8c60a..bcd047a281 100644 +--- a/src/claude/model-info.ts ++++ b/src/claude/model-info.ts +@@ -143,14 +143,19 @@ export function buildAnthropicModelInfos( + // the auto-context widening that let a 372K route carry the marker (and be + // over-filled) is the #854 defect and does not come back. Guards (audit R1#11): + // same dedupe set, never double-suffix. +- const push1mVariant = (base: AnthropicModelInfo, contextWindow: number | undefined, maxInputTokens?: number) => { ++ const push1mVariant = ( ++ base: AnthropicModelInfo, ++ contextWindow: number | undefined, ++ maxInputTokens?: number, ++ selectorId?: string, ++ ) => { + // The [1m] marker makes Claude Code account 1e6 tokens for the row, so it + // may only name models whose AUTHORITATIVE effective window is >= 1M — + // never the auto-context widening, which would mark a 372K route and have + // Claude Code over-fill it (the #854 defect). + if (contextWindow === undefined || contextWindow < ONE_MILLION) return; + if (base.id.includes("[1m]")) return; +- const id = `${base.id}[1m]`; ++ const id = selectorId ?? `${base.id}[1m]`; + if (seen.has(id)) return; + seen.add(id); + // The marker fixes Claude Code's accounting at 1e6, but a model may accept less input +@@ -220,7 +225,15 @@ export function buildAnthropicModelInfos( + out.push(info); + // Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude + // routes — only a genuine >=1M window earns the variant row there. +- push1mVariant(info, m.contextWindow, routedMaxInput); ++ // Claude Code groups canonical Fable ids before it compares the [1m] marker. This ++ // reversible alias only separates picker families; it is not an OpenAI-native route. ++ // The Messages ingress restores the canonical Anthropic id before passthrough. ++ const oneMillionSelector = idStyle === "readable" ++ && m.provider === "anthropic" ++ && listedModelId.startsWith("claude-fable-") ++ ? `${claudeCodeNativeAlias(listedModelId)}[1m]` ++ : undefined; ++ push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector); + // The whole model is passed, not a (provider, id) pair: a combo row lives in its own + // namespace with no config.providers entry, so the caller classifies it from the + // aggregated supportsServiceTier the row already carries. +diff --git a/src/server/claude-messages.ts b/src/server/claude-messages.ts +index 70572f9c67..d2eedfa4ab 100644 +--- a/src/server/claude-messages.ts ++++ b/src/server/claude-messages.ts +@@ -12,6 +12,7 @@ import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/a + import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize"; + import { AnthropicRequestError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound"; + import { resolveDesktop3pAlias } from "../claude/desktop-3p"; ++import { claudeCodeNativeAlias } from "../claude/alias"; + import { recordDesktopRequest } from "../claude/desktop-health"; + import { stripOneMillionMarker } from "../claude/context-windows"; + import { captureClaudeInbound } from "../claude/inbound-debug"; +@@ -78,6 +79,13 @@ function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): st + return decodedBase === bare ? exact : `${decodedBase}--fast`; + } + ++/** Restore the reversible Fable picker alias before Anthropic passthrough checks. */ ++function decodeFablePickerAlias(raw: string, cc?: OcxConfig["claudeCode"]): string { ++ const decoded = resolveInboundModel(raw, cc); ++ if (!decoded.startsWith("claude-fable-")) return raw; ++ return claudeCodeNativeAlias(decoded) === raw ? decoded : raw; ++} ++ + function isRec(v: unknown): v is Rec { + return !!v && typeof v === "object" && !Array.isArray(v); + } +@@ -648,6 +656,9 @@ async function handleClaudeMessagesWithBudget( + effortOverride = extractOcxEffortDirective(anthropicBody); + } + } ++ if (isRec(anthropicBody) && typeof anthropicBody.model === "string") { ++ anthropicBody.model = decodeFablePickerAlias(anthropicBody.model, config.claudeCode); ++ } + if (isRec(anthropicBody) && typeof anthropicBody.model === "string") { + requestedModel = anthropicBody.model; + // Decode for Fast only. A Claude alias is `claude-ocx---`, so it +@@ -1070,6 +1081,8 @@ export async function handleClaudeCountTokens( + model = stripOneMillionMarker(countRoute); + raw.model = model; + } ++ model = decodeFablePickerAlias(model, config.claudeCode); ++ raw.model = model; + // Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a + // token estimate and sends no tier, so only the IDENTITY is corrected - without this the + // synthetic id reaches native passthrough as a model Anthropic has never heard of. +diff --git a/tests/claude-integration/claude-model-info.test.ts b/tests/claude-integration/claude-model-info.test.ts +index 0a2d151a1b..b9a23342af 100644 +--- a/tests/claude-integration/claude-model-info.test.ts ++++ b/tests/claude-integration/claude-model-info.test.ts +@@ -44,6 +44,22 @@ describe("anthropic-flavor ModelInfo discovery entries (devlog 130 B4b)", () => + expect(info!.capabilities.effort.max.supported).toBe(true); + }); + ++ test("readable Fable rows keep base and 1M selections distinct in Claude Code", () => { ++ const infos = buildAnthropicModelInfos([], [{ ++ provider: "anthropic", ++ id: "claude-fable-5-1", ++ contextWindow: 1_000_000, ++ maxInputTokens: 1_000_000, ++ }], undefined, "readable"); ++ ++ expect(infos.map(info => info.id)).toEqual([ ++ "claude-fable-5-1", ++ "claude-ocx-native--claude-fable-5-1[1m]", ++ ]); ++ expect(infos[1]!.display_name).toBe("claude-fable-5-1 (anthropic) · 1M"); ++ expect(infos[1]!.max_input_tokens).toBe(1_000_000); ++ }); ++ + test("native effective ladder only advertises clamp-identity rungs (audit R4#1)", () => { + for (const slug of ["gpt-5.5", "gpt-5.4", "gpt-5.6-sol"]) { + for (const rung of nativeEffectiveLadder(slug)) { +diff --git a/tests/claude-integration/claude-native-passthrough.test.ts b/tests/claude-integration/claude-native-passthrough.test.ts +index c8c798ac9a..af87aa83cb 100644 +--- a/tests/claude-integration/claude-native-passthrough.test.ts ++++ b/tests/claude-integration/claude-native-passthrough.test.ts +@@ -206,6 +206,47 @@ test("count_tokens passes through with native credentials", async () => { + } + }); + ++test("Fable 1M picker alias preserves native passthrough on both Messages endpoints", async () => { ++ const captured: Captured[] = []; ++ const upstream = mockAnthropicUpstream(captured); ++ saveConfig(cfg(upstream.url.toString().replace(/\/$/, ""))); ++ const server = startServer(0); ++ const pickerModel = "claude-ocx-native--claude-fable-5-1"; ++ try { ++ const messagesWithoutMarker = await fetch(new URL("/v1/messages", server.url), { ++ method: "POST", ++ headers: OAUTH_HEADERS, ++ body: JSON.stringify({ ...claudeBody(), model: pickerModel }), ++ }); ++ expect(messagesWithoutMarker.status).toBe(200); ++ await messagesWithoutMarker.text(); ++ ++ const messagesWithMarker = await fetch(new URL("/v1/messages", server.url), { ++ method: "POST", ++ headers: OAUTH_HEADERS, ++ body: JSON.stringify({ ...claudeBody(), model: `${pickerModel}[1m]` }), ++ }); ++ expect(messagesWithMarker.status).toBe(200); ++ await messagesWithMarker.text(); ++ ++ const countTokens = await fetch(new URL("/v1/messages/count_tokens", server.url), { ++ method: "POST", ++ headers: OAUTH_HEADERS, ++ body: JSON.stringify({ model: `${pickerModel}[1m]`, messages: [{ role: "user", content: "hi" }] }), ++ }); ++ expect(countTokens.status).toBe(200); ++ expect(await countTokens.json()).toEqual({ input_tokens: 4242 }); ++ ++ expect(captured).toHaveLength(3); ++ expect(captured[0]!.body.model).toBe("claude-fable-5-1"); ++ expect(captured[1]!.body.model).toBe("claude-fable-5-1"); ++ expect(captured[2]!.body.model).toBe("claude-fable-5-1"); ++ } finally { ++ await server.stop(true); ++ upstream.stop(true); ++ } ++}); ++ + test("exposed native passthrough requires dedicated admission and never forwards admission credentials", async () => { + const admissionSecret = "sk-ant-api03-key"; + const providerBearer = "sk-ant-oat01-provider"; +``` + +## Additional bounded acceptance edits + +In the existing model-info test file, directly after the carried Fable test, add this behavioral matrix (existing imports suffice): + +```ts +test("Fable 1M aliases preserve style, window and input-ceiling boundaries", () => { + const fable = { provider: "anthropic", id: "claude-fable-5-1", contextWindow: 1_000_000, maxInputTokens: 922_000 }; + const readable = buildAnthropicModelInfos([], [fable], undefined, "readable"); + expect(readable[1]!.id).toBe("claude-ocx-native--claude-fable-5-1[1m]"); + expect(readable[1]!.max_input_tokens).toBe(922_000); + const desktop = buildAnthropicModelInfos([], [fable], undefined, "desktop3p"); + expect(desktop[1]!.id).toBe(`${desktop[0]!.id}[1m]`); + const smaller = buildAnthropicModelInfos([], [{ ...fable, contextWindow: 200_000 }], undefined, "readable"); + expect(smaller.map(row => row.id)).toEqual(["claude-fable-5-1"]); + const unknown = buildAnthropicModelInfos([], [{ provider: "anthropic", id: "claude-fable-5-1" }], undefined, "readable"); + expect(unknown.map(row => row.id)).toEqual(["claude-fable-5-1"]); + const other = buildAnthropicModelInfos([], [{ ...fable, id: "claude-opus-5" }], undefined, "readable"); + expect(other.map(row => row.id)).toEqual(["claude-opus-5", "claude-opus-5[1m]"]); +}); +``` + +Extend the carried native-passthrough test after its first three requests with the following legacy selectors. Adjust captured length from 3 to 6; assert every captured model is canonical. Existing mock upstream and credential fixture are reused. + +```ts +for (const model of ["claude-fable-5-1", "claude-fable-5-1[1m]", `${pickerModel}[1M]`]) { + const response = await fetch(new URL("/v1/messages", server.url), { + method: "POST", headers: OAUTH_HEADERS, + body: JSON.stringify({ ...claudeBody(), model }), + }); + expect(response.status).toBe(200); + await response.text(); +} +expect(captured).toHaveLength(6); +for (const call of captured) { + expect(call.body.model).toBe("claude-fable-5-1"); + expect(call.headers.get("anthropic-beta")).toBe(OAUTH_HEADERS["anthropic-beta"]); +} +``` + +The round-trip guard and prefix guard must remain; do not replace them with generic decoding of every alias. Existing alias, mapped-model, disabled-passthrough, and exposed-listener regressions remain required CI coverage. Review-specific additions, if needed, are recorded only in scratch. + +## Documentation diff and GUI evidence + +The source PR modifies no docs even though its body checks the docs box. Add the bounded exception to the English guide; do not claim all canonical 1M rows are encoded or that base Fable loses its native ID. + +```diff +--- a/docs-site/src/content/docs/guides/claude-code.md ++++ b/docs-site/src/content/docs/guides/claude-code.md +@@ +-Desktop's third-party gateway mode can offer its effort selector. Real Anthropic models keep their +-canonical ids. The synthetic 2026 date is an internal slot, not a release date. Legacy hash aliases ++Desktop's third-party gateway mode can offer its effort selector. Real Anthropic base rows keep ++their canonical ids. For Claude Code, a canonical Fable model with an authoritative 1M window ++uses `claude-ocx-native--claude-fable-5-1[1m]` for its separate 1M selection. This reversible ++selector distinguishes picker families; Messages and count_tokens restore the canonical Fable ++id before native passthrough. Desktop 3P selectors keep their existing format. ++The synthetic 2026 date is an internal slot, not a release date. Legacy hash aliases +@@ +-(reasoning-effort ladder, thinking types) in the official `ModelInfo` shape. Real Anthropic models +-keep their canonical ids on both surfaces. ++(reasoning-effort ladder, thinking types) in the official `ModelInfo` shape. Real Anthropic base ++rows keep their canonical ids; the readable Fable 1M exception is described above. +``` + +Do not describe the unmarked row as necessarily a 200K upstream model: both rows may advertise a genuine 1M window; the marker controls client accounting. Preserve the existing rule that beta headers, not a model suffix alone, convey provider context semantics. + +Translation coordination before readiness: main's documentation owner must inspect `docs-site/src/content/docs/{ko,ja,zh-cn,zh-tw,fr,ru,tr}/guides/claude-code.md` for equivalent unconditional canonical-ID statements and add the same scoped exception where needed. No translation edits are delegated to this research worker; record the final exact locale touch set at consuming P. This is a readiness debt, not permission to leave contradictory translations. + +No OCX React component changes are required. Capture client evidence for fresh selection, saved old bare/marked selection, switching Fable→Opus→Fable 1M, settings persistence, and restart. Record Claude Code version, discovery payload, selected ID and resulting upstream model in a sanitized artifact. Screenshots should show both picker rows and retained selection. Do not present source-author harness claims as current reproduced evidence. If the PR description mentions GUI, include its screenshot as required by repository PR policy. + +## CI-only verification contract + +NO local tests, typecheck, builds, suite helpers or provider probes. Commands below describe CI execution, not commands to execute on the local workstation. + +- `.github/workflows/ci.yml:7` triggers on every pull_request base, including stacked children; no dev-only base filter. +- `changes` at :182-187 includes src/tests, so this implementation triggers expensive jobs. A docs-only roadmap CI success can legitimately skip tests and proves no runtime behavior. +- Linux `test` at :255-267 is four shards; :314-316 invokes `bash scripts/ci/run-bun-test-batches.sh "$TEST_SHARD"`. +- The helper at :46-68 excludes only storage-policy/storage/api-usage families, not these tests. At :196-211 it sorts all test files and distributes every eligible file exactly once across shard indices; :122-125 executes `bun test --isolate --timeout 60000` on the selected filenames. Both existing changed test paths are included. +- `gates` at :392-431 runs `bun x tsc --noEmit`, the additional contract tsconfig, GUI tests, and privacy scan. GUI lint is conditional on gui changes; do not mistake that skip for a failure on this backend-only layer. +- macOS at :451-465 runs two shards; :532 invokes `bun test --isolate --timeout 60000 tests --shard=.../2`. +- Windows at :658-686 is **dispatch-only**, lane `all`; :754 invokes six shards. Ordinary PR green is not Windows full-suite evidence. Main decides/dispatches any required exact-head Windows run; this research starts none. +- Focused failure diagnosis target, if a CI runner needs it: `bun test tests/claude-integration/claude-model-info.test.ts tests/claude-integration/claude-native-passthrough.test.ts tests/claude-integration/claude-models-discovery.test.ts tests/claude-integration/claude-alias.test.ts`. Do not add a workflow solely to run this command. +- Save run URL, event, head_sha, checkout/merge SHA, non-skipped job conclusions and test log paths. Resolve fork `action_required` via main's normal approval process; hygiene labels/author checkboxes/old-head approval are not product-test evidence. + +| Acceptance | Activation and observable evidence | +|---|---| +| Distinct rows | Readable Anthropic Fable >=1M emits bare base and reversible `[1m]` sibling with honest display name | +| Narrow family/style | Opus retains canonical marker; Desktop retains its prior selector; no other provider is rewritten | +| Capacity guard | 200K or undefined Fable window emits no sibling; 922K input cap under 1M window remains 922K | +| Marker compatibility | New alias with/without marker plus upper-case marker and legacy canonical selectors reach canonical Fable on mock upstream | +| Both endpoints | Messages status/stream consumed; count_tokens returns upstream 4242, not local estimate; captured model canonical | +| Routing coexistence | Existing alias/modelMap/Fast/effort regressions stay green; no global resolver or Desktop registry edit | +| Client persistence | Versioned client evidence demonstrates saved selection after switching and restart, including legacy behavior | +| Gates | All required exact-head CI jobs execute/pass; Windows status reported separately | + +## Dependencies, carry and close-out + +The four source paths plus English guide have **zero touched-path overlap** with supplied source PRs #3653/#3654/#3571/#3659 (JSON file-list intersection inspected). Fable is independently reviewable: no semantic requirement to land context persistence or Go ordering first. Per 000_plan.md, the user-requested stack uses `codex/lane-b-05-fable` based on `codex/lane-b-04-management`; do not invent a runtime dependency. Discovery still consumes the current predecessor's catalog and limits, so rerun exact-head gates after restack. + +Lane D owns #3646 remote Desktop aliases. Coordinate before altering `claude-messages.ts` or decoder ordering; do not carry its unknown-registry fallback into this Fable slice. This plan deliberately changes neither `desktop-3p.ts` nor `inbound-model-options.ts`. + +Safe later carry: apply the two non-merge source commits in order on main's selected layer, preserving author metadata, then add the bounded tests/docs as own commits. Do not reset the bound checkout, cherry-pick the source merge parent, or replace other lanes' edits. A lower-layer squash means refresh onto the new dev ancestry and replay only unique Fable commits, then obtain fresh CI/review. Main owns `--no-verify` pushes and bottom-up merges. Retarget children before removing a parent branch. Close source PR #3649 only after proving its replacement commit is in dev, and link the replacement. No linked issue is specified in the source body; do not close #3646 or unrelated issues. + +## Unresolved items for main + +- Exact-head hosted verification and actual picker compatibility are not established by this research. +- Legacy request forwarding is testable with the additions above; saved client selection migration remains a separate client-level observation. +- The old review's missing marked Messages case is already resolved by `284fe8ca0`; Fable-only scope is explicit in the current source body and comments. Do not repeat these as open code defects. +- Translation touch set must be fixed at P before implementation readiness. +- Relevant security review is tracked only in `.tmp/lane-b/plan-fable-review.md`; this tracked plan contains public source behavior, not unpublished findings. + +Roadmap handoff: this document is ready for main's A audit. The unresolved checks above are explicit later B/C acceptance work, not a request to implement during the docs-only cycle. Final landing and closure follow `060_landing.md`. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/060_landing.md b/devlog/_plan/260906_lane_b_catalog_stack/060_landing.md new file mode 100644 index 0000000000..4a1ca51e5d --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/060_landing.md @@ -0,0 +1,31 @@ +# Land the verified catalog stack + +## Before and after + +Before: five original contribution PRs are open, their carried changes form separate reviewed branches, and dev may have advanced from peer lanes. After: all five behavior contracts are reachable from dev, source authors remain credited, replacements and originals are closed appropriately, and resolved issues #3650/#3651 are closed with landing proof. + +## Exact change map + +- MODIFY this unit's numbered completion record with replacement PR numbers, source/current SHAs, CI URLs, merge SHAs and issue closure results. +- MODIFY child PR base refs from the open parent branch to dev after parent landing. Keep local and remote parent refs until all children are safely retargeted. +- MODIFY a branch only for demonstrated integration conflicts or failing current-head checks. Preserve unrelated A/C/D work; resolve shared config fields, locale keys and alias helpers by combining contracts, never choosing an entire side blindly. +- CLOSE original #3653/#3654/#3571/#3659/#3649 as superseded only after the respective replacement's merge commit is on dev. +- CLOSE #3650/#3651 only after full visibility/context acceptance criteria are satisfied. +- MOVE the finished public unit from `_plan` to `_fin` only at terminal completion. No credentials or private audit material enters this unit. + +## Sequence and activation scenarios + +1. Fetch dev; compare each queued replacement to its reviewed head. Trigger: peer dev advanced. Effect: inspect actual overlap, merge/reconcile dev into the affected layer, cascade to children and rerun exact-head checks when the tested tree changed. +2. Confirm each lower layer's CI has actual typecheck, functional tests and GUI gates where applicable; inspect required review findings including security review. No stale approval is reused after code changes. +3. Merge the bottom PR with a merge commit when allowed to preserve ancestry; verify GitHub merge state plus `git merge-base --is-ancestor origin/dev` after fetching. +4. Immediately close the carried source PR with replacement and landing evidence; close linked issue if its complete report is addressed. Preserve original contribution trailers in merge/squash content. +5. Retarget the child before cleanup; compare `git diff ...` to its intended layer. If a squash changed ancestry, restack rather than leaving already-squashed content in the child diff. +6. Repeat until all five are landed. Assert final ancestry, original PR/issue states and clean tracked work. Record remaining unrelated items without expanding scope. + +## Verification + +Before final landing, dispatch `ci.yml` with `lane=all` on the final integrated stack head and verify all six Windows test shards in addition to Linux/macOS. Ordinary PR runs skip Windows test shards; Windows keyring/package smoke alone is not full Windows test evidence. + +Read-only `gh pr view`, `gh run view` and `gh issue view` supply fresh state; assertions operate on exact numbers/SHAs, not titles. Git ancestry checks run locally; repository tests do not. A C receipt wraps the read-only verifier and must fail if any required original remains open, intended merge is absent, CI did not execute real tests, or attribution is missing. + +Expected outcome is DONE. Pending CI, a conflict or a repairable review finding continues the same goal. An external blocker is recorded with exact evidence rather than closing the remaining work as complete. diff --git a/devlog/_plan/260906_lane_b_catalog_stack/061_landed.md b/devlog/_plan/260906_lane_b_catalog_stack/061_landed.md new file mode 100644 index 0000000000..a55d1bc5a9 --- /dev/null +++ b/devlog/_plan/260906_lane_b_catalog_stack/061_landed.md @@ -0,0 +1,25 @@ +# 061 — Lane B integration outcome + +Recorded 2026-09-06. All five assigned source contributions are integrated into dev. Source PRs are closed; issues #3650 and #3651 are closed. + +| Source | Replacement | Merge commit | +| --- | --- | --- | +| #3653 | #3685 | `9115b179a29f1366561139b8502cebb17bf816e9` | +| #3654 | #3695 | `ab6762bdb35db24efbe1ceac77a1f9e5e6139616` | +| #3571 | #3700 | `76356176c86aa123220c82b65321453e81897405` | +| #3659 | #3721 | `330bf609790c968006fb8922ab30cd75a680b06e` | +| #3649 | #3722 | `73190c20443876fe1dbf4e9dde5d25644e48e71a` | + +## Attribution + +Original contributions retain Robin Bially (#3653/#3654), voiys (#3571), gqchen (#3659), and Éverton Toffanetto (#3649) through original commit authorship or Co-authored-by trailers. The static model-management parent #3717 is superseded by #3721; its rebased changes are included by content, not old-SHA ancestry. + +## Verification and final maintainer direction + +The first three replacements passed their recorded hosted functional CI before landing. Model-management pre-rebase head 9af03d0c passed CI 33998617606 and React Doctor 33998617609; its earlier remote API, browser and responsive evidence remains tied to the documented tested revisions. + +The maintainer then explicitly requested only rebasing onto dev and admin merging. #3721 was rebased onto 2f124a167 and landed at 330bf6097; the rebased tree dbb5c190 matches the inspected clean composition. #3722 preserves the original two Fable commits as an unchanged feature patch (stable patch ID ceed1fa86d57fac9706aaae7fe9de7b5d6f8802c) on dev330bf6097. Independent bounded static review found no composition blocker in that tree. + +No local suites, typechecks or builds were run. No post-rebase CI wait or new runtime verification is claimed for these last two landings. Fable source-author test reports are historical and were not reproduced here. Newer work from other lanes is outside that evidence. + +Actual merge commits were verified as ancestors of fetched origin/dev, and original PR/linked-issue states were refreshed through GitHub. This final record is documentation only; it does not certify a release, deployment, or final Windows run. diff --git a/devlog/_plan/260906_opaque_transport_finality/000_plan.md b/devlog/_plan/260906_opaque_transport_finality/000_plan.md new file mode 100644 index 0000000000..3ff99fa965 --- /dev/null +++ b/devlog/_plan/260906_opaque_transport_finality/000_plan.md @@ -0,0 +1,21 @@ +# Opaque preflight transport and terminal outcomes + +Class C4. Mandatory parent-PR review repair under the existing authorized release +chain; work phase opaque-transport-finality, criterion c-2. Parent #3753 remains +open/draft at b73809f7e, child #3754 remains open/draft at f5c88beb9 with its parent +base restored. No parent merge occurred. The original #3535 was briefly closed +by an out-of-order follow-up, immediately reopened, and its comment corrected. +No completion, approval or release gate is waived. + +Public review references: PRRT_kwDOS-0Gi86fqEUo (preflight read failure escapes) +and PRRT_kwDOS-0Gi86fqEUq (tee EOF reports incomplete despite failed client tail). +The earlier full CI and independent reviews did not cover these paths. The +unfinished combo cycle is preserved and must consume the repaired parent before +its final verification. All execution remains hosted; no local suite/typecheck/ +build or live Kiro request. + +Implementation is one bounded failure-contract unit in 010_failure_boundaries.md. +Update the existing parent PR, run exact-head CI, cascade its commit into #3754, +and require fresh composed CI and review before bottom-up integration. Do not +close an original or retarget a child as a side effect of an unverified merge: +verify each preceding command and actual merged state before dependent actions. diff --git a/devlog/_plan/260906_opaque_transport_finality/010_failure_boundaries.md b/devlog/_plan/260906_opaque_transport_finality/010_failure_boundaries.md new file mode 100644 index 0000000000..ccc1552b5b --- /dev/null +++ b/devlog/_plan/260906_opaque_transport_finality/010_failure_boundaries.md @@ -0,0 +1,67 @@ +# Preserve preflight read failures and inspection finality + +## Current ownership + +Core selects native encrypted-output candidates and awaits combo-stream-preflight +before exposing headers. The preflight owns a bounded retained prefix and one +reader; replayBufferedResponse already emits that prefix and forwards later read +errors. Client relays own synthetic failed tails. consumeForInspection owns the +independent tee terminal callback used by native account health. The shared SSE +inspector reports real terminals and exposes parsed payload callbacks. + +## Planned change + +- src/server/responses/combo-stream-preflight.ts: native-only replayReadErrors + option, default false. Catch only reader.read rejection; opted-in callers get + an accepted reconstructed stream retaining the bounded prefix and the errored + reader. Never cancel that errored reader: its original rejection must survive + the replay into relay/inspection. Default combo callers preserve their prior throw behavior. Do not retry + or classify a read reset as a decrypt rejection, swallow it, or grow buffers. +- src/server/responses/core.ts: enable that option only on the native opaque + preflight. After its await, caller abort takes the existing cancellation cleanup + path before any replay/rebuild. Other read failures reach the normal mid-stream + relay and inspection path, not a connect-phase error classifier. +- src/server/relay.ts: reuse a bounded/redacted bare-error message helper at the + client boundary and within consumeForInspection's parsed-payload callback. + Keep that evidence local to this reader rather than borrowing stale log state. + At clean EOF without a real terminal, a witnessed bare error reports failed + using the shared terminal HTTP mapper; an error-free EOF remains incomplete. + Preserve the caller's parsed-payload callback. Real terminals and cancellation + retain precedence; no extra terminal callback or healthy-account reset. + +## Rejected alternatives and scope + +A blanket core catch mapped as a connect error can misclassify an already-started +response's account outcome. Globally replaying all preflight errors changes combo +behavior. Reporting failure at the first bare error would override a later real +terminal. Borrowing the client relay's mutable state revives tee scheduling races. +Use the existing preflight/relay ownership and callback seams instead; no new +public inspector method, provider policy or retry budget. + +## Verification + +Existing native request fixtures add created-then-reset and created-then-caller- +abort cases: no uncaught handleResponses rejection, no sanitize resend, normal +failed stream or 499 cancellation and appropriate attempt/terminal metadata. +Run tee/eager variants where selected by the existing harness. Preflight tests +prove default read-error behavior is unchanged and native opt-in preserves prefix +and exact failure. Inspection/account-health fixtures cover flat/nested bare +errors at EOF, prior failure/avoidance not cleared, real-terminal precedence, +error-free EOF compatibility and cancellation neutrality. Existing redaction, +byte bounds, no-persistence and one-shot recovery tests remain. + +Independent plan/source/final review; exact parent and cascaded child hosted +Linux/macOS/gates CI. Final Windows six-shard and release gates remain mandatory. + +## Usage-marker parity amendment + +Source review confirms the account-health blocker is closed by failed EOF. The +existing eager callback still labels every synthetic failure as streamAborted, +though a clean EOF after an explicit upstream error is a semantic failure, not a +body-read reset (PersistedUsageAttempt documents that distinction). Criterion c-2 +also requires usage outcome parity, so include this small related correction: +relay-eager passes optional upstream_error provenance only for that clean-EOF tail; +core records its semantic failed status without streamAborted. Ordinary reset +callbacks retain their one-argument shape, 502 and streamAborted. Add request-level +tee/eager assertions for repeated bare errors versus actual reset; do not infer +this marker from a stale log message or change real-terminal precedence. diff --git a/devlog/_plan/260906_release_244_followups/000_plan.md b/devlog/_plan/260906_release_244_followups/000_plan.md new file mode 100644 index 0000000000..3b807cdba3 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/000_plan.md @@ -0,0 +1,47 @@ +# Release 2.44 follow-up integration + +## Loop contract + +- Archetype: spec-satisfaction repair; class C4 for governance, replay and release; C3 for bounded client changes. +- Trigger: owner authorized the named backlog, bottom-up stacked PR integration, --no-verify pushes, admin merges, maintainer dev policy and release on 2026-09-06. +- Goal: publish the verified next release after these narrowly scoped fixes. +- Non-goals: new providers, authless Desktop defaults (#3689), Anthropic replay/cache redesign (#3719), unrelated branch cleanup, direct live Kiro calls. +- Verification: GitHub Actions only for all test/typecheck/build/privacy commands. Local reads, git diff --check, JSON validation and review are allowed. User prohibition overrides local verification defaults. Existing ci.yml dispatch lane=all is the Windows six-shard authority; service-lifecycle.yml has workflow_dispatch. Command existence checked by reading workflow inputs and scripts, not running prohibited suites. +- Stop: every mapped criterion proved, final npm/tag/provenance validation complete. A red gate is repaired, never relabeled green. Old bug reports without current reproduction receive explicit evidence-limited outcomes. +- Memory: numbered unit docs and session-bound .codexclaw goalplan/ledger. Sensitive log analysis and draft security reviews stay in .tmp/release-244. +- Delegation: xai/grok-4.6 only, bounded disjoint workers and independent reviewers. Main owns every FSM edge, commits, pushes and GitHub writes. Reclaim after two distinct failed agents; delegation changes enter at P. +- Resources: existing GitHub account, repository and release OIDC only; no new credentials/purchases. Unlimited requested-model delegation within available concurrency; no owner token/cost cap. Each subprocess <=30 minutes, CI polls <=60 seconds, each phase investigation checkpoint at 60 minutes with evidence-based continuation. No implicit exhausted outcome. + +## Snapshot and sequence + +Baseline dev: af344a28eabcee09a5e04c48ab897449792719c2, version 2.44.0. Latest published stable is 2.43.0. Refresh before every layer. + +| Work phase | Design | Dependency / independent proof | +|---|---|---| +| roadmap | this unit | Lock all decade designs; docs only | +| policy | 010_policy.md | Establish truthful maintainer integration authority | +| task-input | 020_task_input.md | Shared Responses parser contract | +| task-guidance | ../260906_stateful_task_guidance/010_raw_boundary.md | Review follow-up: align stored raw guidance before Kiro resumes | +| kiro-results | 030_kiro_results.md | Consume parsed tool-result sequence | +| opaque-recovery | 040_opaque_recovery.md | Retry and terminal semantics on composed routing | +| combo-recovery | 050_combo_recovery.md | Route recoverable parsed payloads | +| grok-terminal | 060_grok_terminal.md | Client terminal reconstruction on composed relay | +| quota-proxy | 070_quota_proxy.md | Refresh network-path evidence on integrated runtime | +| usage-source | 080_usage_source.md | Attribute actual selected transport after routing | +| dashboard | 090_dashboard.md | Presentation on integrated behavior | +| release | 100_release.md | Final ancestry, Windows, lifecycle, publish | + +One work-phase is one PABCD cycle. Publish short dependency stacks; use merge commits for parents with live children, squash bounded terminal carries if safe, and recascade after any squash. Independent presentation/governance slices remain their own PRs even though execution is sequential. Every carried contributor receives account-linked Co-authored-by credit. Preserve snapshots of source heads. + +## Evidence boundaries + +#3735/#3734 are public current-SHA reports; independently inspect code, author local-pass statements remain reports. Kiro proof is recorded-log shape plus synthetic CI tests, never a live quota-consuming request. #3644 has a network A/B report and landed diagnostic #3693; do not claim a Windows runtime reproduction from mocked tests. Detailed private logs are never committed. + +## Owner steering: asynchronous CI + +From the Grok unit onward, implementation/review and PR publication proceed +without waiting for hosted CI. Each cycle records exact-head CI submission; its +runtime acceptance criterion stays open under release convergence. CI failures +are handled asynchronously and stacks cascade after repairs. Bottom-up merges +and release publication still require successful checks on their final heads. +This changes scheduling only; no test, platform or release criterion is removed. diff --git a/devlog/_plan/260906_release_244_followups/001_roadmap_lock.md b/devlog/_plan/260906_release_244_followups/001_roadmap_lock.md new file mode 100644 index 0000000000..5a9a7483f5 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/001_roadmap_lock.md @@ -0,0 +1,26 @@ +# Roadmap lock and audit dispositions + +The docs-only cycle locks 000_plan and all ten decade designs. Three independent +Grok discovery lanes checked protocol, recovery and integration scopes; a fourth +reviewed maintainer authority and a fifth reviewed the complete roadmap. + +The roadmap review returned GO-WITH-FIXES with two accepted amendments: every +criterion now identifies its own proof, and Windows lane=all is required on each +actual publish SHA as well as the frozen candidate. The quota investigation must +explicitly retain an open field-validation outcome when the original failure is +unreproduced. Publication cannot stand in for that evidence. + +The policy review was corrected and independently rechecked PASS: Maintain is +GitHub role id 2, Admin is 5; Write is 4 and is outside the authorized exception. +The opt-in CLI path parses its flag independently of positional arguments and +skips only the two approval requirements, retaining identity, role, objection and +race checks. No repository rules have been changed in this documentation cycle. + +Verification is documentation structure and independent source review only. +Runtime tests, typecheck, builds and privacy scanning will execute in hosted CI; +no local suite or live Kiro call was made. Existing log metadata is historical +evidence and does not establish current live Kiro behavior. + +Next cycle: consume 010_policy.md, implement the helper/docs and verify the exact +head in CI, then apply and read back the authorized dev-only role configuration. +All remaining runtime and release criteria stay open. diff --git a/devlog/_plan/260906_release_244_followups/010_policy.md b/devlog/_plan/260906_release_244_followups/010_policy.md new file mode 100644 index 0000000000..dc7b655aaa --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/010_policy.md @@ -0,0 +1,29 @@ +# Maintainer dev integration policy + +Depends on roadmap. Class C4; spec-satisfaction. Owner authorizes maintain/admin integration through PRs without a second maintainer approval, including self-authored PRs. Actual inspected roles for both rostered maintainers are admin; current dev rules already permit role 5 PR bypass. The contradiction is primarily normative documentation, plus future Maintain role coverage. + +## Exact change map + +- MODIFY MAINTAINERS.md review policy and dated change log: distinguish contributor approvals from explicitly opted-in maintainer integration to dev. Preserve actual independent technical/security review and CI duties; do not call self-integration a second-person approval. Main/preview promotions retain existing rules. +- MODIFY AGENTS.md branch/review summary: align with maintainer dev exception; PRs still required, force pushes and deletions still blocked. +- MODIFY scripts/ci/assert-mergeable-review.sh: parse explicit --maintainer-integration in any argv position, retaining optional repository positional argument. Default strict contributor-review path unchanged. Opt-in skips exactly the reviewDecision=APPROVED and qualified non-self approval checks, not review retrieval, objections or race checks. For override, require baseRefName=dev, current authenticated human actor from gh api user, membership in trusted base dev MAINTAINERS roster, and live maintain/admin role. Preserve complete review parsing, maintainer CHANGES_REQUESTED blocking and final head/base/actor authorization recheck. Print only a truthful validation snapshot with head/base/actor; do not emit a privileged merge recipe because head matching cannot atomically bind the PR base. Never accept a CLI-supplied actor, PR-authored roster, bot or unknown role. +- MODIFY tests/ci-workflows/assert-mergeable-review.test.ts: extend fake gh with actor/base/permissions APIs and cases while retaining all existing default strict cases. +- MODIFY docs-site/src/content/docs/contributing.md and structure/06_docs-and-release.md: link canonical exception and correct Windows dispatch-only whole-suite description found stale in structure. +- External UPDATE dev ruleset 20763889 only: add RepositoryRole actor_id=2 bypass_mode=pull_request, preserve actor_id=5 and all conditions/rules. Read snapshot immediately before update; compare after. Verify role names through GraphQL repositoryRoleName: maintain=2, admin=5; role4 is write and must never be added. Do not change main 20764415 or preview 20764486. Rollback is the saved before JSON projected to accepted API fields. + +## Activation matrix and verifier + +CI test fixture: authorized admin and maintain actors with no second approval on dev pass ONLY opt-in; write/outsider/bot/missing actor/role API error fail; main/preview/stack base fail; pending maintainer objections, API pagination failures, head/base races fail. Default no flag retains all prior strict failures. shell syntax can be read/checked; Bun tests and typecheck run remotely. Live REST readback proves only dev actor list changed; compare main/preview snapshots unchanged. + +## Trust / bypass record + +Assets repository integration history; entry script and authenticated GitHub rules API; boundary contributor metadata versus trusted dev roster/live permissions. E7 human policy plus E8 GitHub branch rules; admin can alter rules outside this helper, so helper is an early review check, not universal enforcement. PR bypass does not remove deletion/non-fast-forward rules outside PRs. Security review recorded independently in scratch; final disposition may be published after diff is public. + + +## Policy-cycle P refresh and delegation + +Current dev remains the roadmap baseline; source helper and ruleset snapshots were reread. The preceding D locked the roadmap and made policy the next cycle. Worker owns only scripts/ci/assert-mergeable-review.sh and tests/ci-workflows/assert-mergeable-review.test.ts; main owns MAINTAINERS.md, AGENTS.md, contributing.md, structure/06_docs-and-release.md and GitHub settings. No overlapping writes or local tests. An independent reviewer audits the final script/docs delta before remote CI and dev-only ruleset application. + +## Dispatch repair / policy P amendment + +The first helper+tests worker repeatedly read unrelated plan pages and produced no source delta after a scope correction and bounded waits. It was retired without edits. Main now owns scripts/ci/assert-mergeable-review.sh in addition to documentation/settings; a fresh worker owns ONLY tests/ci-workflows/assert-mergeable-review.test.ts. The protocol remains --maintainer-integration in any argv slot, optional repo, actor from gh api user (login/type), baseRefName from PR metadata, maintain/admin role_name from collaborator permission. Final metadata rechecks head/base/author, then actor/role/roster authorization again. Default strict path does not require new fields. No expected evidence or scope was removed. This replan changes dispatch ownership, not the approved policy. diff --git a/devlog/_plan/260906_release_244_followups/011_policy_implementation.md b/devlog/_plan/260906_release_244_followups/011_policy_implementation.md new file mode 100644 index 0000000000..2a21aaa922 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/011_policy_implementation.md @@ -0,0 +1,31 @@ +# Maintainer integration implementation + +The helper retains its strict approval path by default. Explicit +--maintainer-integration parses independently of positional arguments and requires +the authenticated human actor, the trusted dev roster and live maintain/admin +permission. It retains complete review parsing and maintainer objections. Before +emitting a validation snapshot, it reloads the roster and actor authorization, +rejects roster/actor drift, then checks the final PR head, dev base and author. + +The existing regression matrix is preserved. Thirty-one additional scenarios +cover authorized integration, refusal cases, argument order and concurrent state +changes. Passing fixtures also verify the dev-bound roster and repeated identity +queries, so an accidental default-branch lookup cannot satisfy the tests. + +MAINTAINERS, AGENTS, the contributing guide and architecture notes now distinguish +maintainer integration from approving one's own work. The dev-only GitHub payload +adds Maintain role 2 alongside Admin role 5, both PR-only. The reviewed applicator +checks fresh before/after state and verifies role names without writing main or +preview. Settings application is recorded separately after hosted verification. + +No local test suite, typecheck, build or live provider request was run. The shell +syntax and diff were checked locally; behavioral proof is the exact-head hosted +CI recorded on PR #3739 and in the session's source-bound evidence receipt. The +first broad worker was retired without edits; main implemented the helper and a +fresh bounded worker supplied the regression matrix. + +Final C review removed the opt-in copy-paste admin merge recipe. Head matching +does not bind a PR's base at execution time, and another read in the same shell +command would only move that race. The helper now states its snapshot boundary; +the separately authorized integration step must revalidate current actor/base. +Passing fixtures require that no privileged merge command is emitted. diff --git a/devlog/_plan/260906_release_244_followups/020_task_input.md b/devlog/_plan/260906_release_244_followups/020_task_input.md new file mode 100644 index 0000000000..4c9b62f501 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/020_task_input.md @@ -0,0 +1,35 @@ +# External Codex task-input envelopes + +Depends on policy; class C4 for protocol admission. Fix public issue #3735, observed on baseline dev. Preserve the existing unpaired-tool HTTP 400 guard from #3471. + +## Diff-level change map + +- MODIFY src/responses/parser.ts at function_call_output classification before tool lookup: route only a complete external task-input envelope to an Ocx user message. Eligibility: type function_call_output, no call_id property (including inherited properties for direct helper calls), nonempty string id/name/namespace, nonempty fully representable text/image output. Do not require specific names, prefixes, namespaces or XML content. Existing standard tool results and custom_tool_call_output keep current path. +- NEW src/responses/task-input.ts: pure recognition returning supported Ocx user content or undefined, no request mutation/network/storage. Reuse existing content converters only when they preserve every accepted output part and reject invalid mixed arrays rather than silently drop them. +- MODIFY tests/responses/responses-parser.test.ts, tests/responses/responses-compaction-routing.test.ts and tests/responses/openai-responses-passthrough.test.ts with narrow positive/negative fixtures. No new test file or layout registry entry is needed. +- MODIFY docs-site/src/content/docs/reference/adapters.md and docs-site/src/content/docs/guides/sub-agent-surface.md and structure/04_transports-and-sidecars.md: describe external task input as user-supplied task coordination, not fabricated tool completion. Keep passthrough/compaction raw-body contracts. + +Before: result-shaped external task input enters toolResult branch with undefined call id, then translated-adapter guard returns 400. After: the complete external shape enters user message with intact supported text/images; malformed/orphan tool results still fail. No secret or raw logged transcript is copied to tests. + +## Activation / verifier + +Remote parser tests exercise arbitrary tool names/namespaces, blank/empty content remains ineligible, multiple ordered text parts and supported images; retain exact content without orphan marker. Explicit call_id empty/null/number/undefined-own-property remain invalid, as do custom outputs missing identity, partial provenance, unsupported/mixed malformed arrays. Existing genuine call ids remain tool results. Remote endpoint/compaction/passthrough fixtures prove unchanged raw body forwarding and guard failures. ci.yml runtime jobs + typecheck/privacy establish fresh proof. Local saved log provides provenance only; no live Kiro request. + +## Boundary / alternatives + +No-op leaves current task creation unusable; configuration cannot distinguish this parser envelope; generic orphan-to-user repair would reverse #3471 and is rejected. Reuse current message types; no persisted schema fields. Classification is compatibility handling, not authentication: no privilege is granted by envelope metadata. + + +## Source follow-up folded at roadmap lock + +Author yrlan-montagnier (Yrlan), GitHub id 71253160: preserve Co-authored-by: Yrlan <71253160+yrlan-montagnier@users.noreply.github.com>. Posted helper may manufacture an encrypted-content-omitted marker that makes encrypted-only input look usable; reject encrypted-only and mixed opaque/unsupported input, never use placeholder text as eligibility. Keep every pre-existing #3471 regression, adding tests rather than replacing them. Add tests/responses/responses-compaction-routing.test.ts and tests/responses/openai-responses-passthrough.test.ts to explicit remote verification. Prefer a dedicated small predicate over relocating passthrough helpers unless byte-for-byte behavior is proved. + +## Task-input cycle P refresh at 25c8d2b4e + +The preceding D landed policy #3739 and actual Maintain/Admin settings. Issue #3735 is still open and the author has no open PR; retain the account-linked Yrlan trailer. Source parser at lines 150-160 currently recognizes only message/agent_message as the continuation conversation boundary. Compute the optional external content once near effectiveType and include a recognized envelope in that existing boundary predicate. In the function_call_output branch, clear pendingReasoning, emit a user message and continue; leave the ordinary result branch and core guard unchanged. + +Concrete new leaf: src/responses/task-input.ts exports externalTaskInputContent(item: unknown): string | OcxContentPart[] | undefined. It imports only type OcxContentPart and existing isObj/inputContentParts. Require exact function_call_output, no call_id property, nonblank id/name/namespace, and a nonblank string or fully supported array. Array parts are input_text/text/output_text with string text or input_image with nonblank string image_url and optional auto/low/high/original detail. Normalize output_text to input_text before calling the existing input converter; original image detail maps to high by that converter. Require at least one nonblank text or usable image. Reject any unsupported/opaque/malformed member, invalid detail or file-id-only reference as a whole; placeholder text never establishes eligibility. Preserve accepted text bytes, order and image references; no raw-body mutation or helper relocation from passthrough. + +Field chain: external JSON shape -> pure leaf validation -> parser user message + existing `_continuationConversationMessageIndex` -> translated adapter's existing user-content serialization. No new persisted field/schema/config. Passthrough and compact use unchanged raw body. Tests include pending reasoning reset and previous_response_id boundary index=0 for a new envelope without a replay prefix, alongside all old #3471 controls. + +Dispatch: main owns new leaf, parser, endpoint/passthrough regressions and English/structure docs; a bounded worker owns only tests/responses/responses-parser.test.ts. Independent A/C reviewer reads named leaf/parser boundaries. No local tests/typecheck/build; remote ci.yml runtime/gates and existing parser/compaction/passthrough suites provide proof. Parser leaves add no core/Lab dependency. No-op/configuration cannot fix this shape; existing input converter is reused behind strict validation. diff --git a/devlog/_plan/260906_release_244_followups/021_task_input_implementation.md b/devlog/_plan/260906_release_244_followups/021_task_input_implementation.md new file mode 100644 index 0000000000..df34b94500 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/021_task_input_implementation.md @@ -0,0 +1,25 @@ +# External task input implementation + +The pure task-input leaf validates the complete external envelope before using the +existing input-content converter. It accepts text and URL-backed images, rejects +partial or opaque arrays as a whole, and preserves accepted content order. The +parser uses the result for both the continuation boundary and a user turn that +clears pending reasoning. Ordinary tool results, the core call-id guard and raw +passthrough handling remain unchanged. + +Existing unpaired-result regressions remain in place. Added parser cases cover +shape/content controls, original image detail, frozen input, continuation and +reasoning separation; HTTP cases exercise accepted text/images and rejected +envelopes before upstream work. A passthrough case verifies the existing raw +orphan-output behavior alongside the new parsed user representation. + +The implementation preserves Yrlan's contributor attribution from the public +issue and supplied proposal. Protocol/security review and hosted CI are recorded +on the fixing PR and source-bound cycle receipt. No local test suite, typecheck, +build or live Kiro request is part of this validation. + +The first hosted run exposed two invalid HTTP test stimuli: short text in an +encrypted_content slot follows the existing plaintext normalization path before +the parser. The negative fixtures now use synthetic ciphertext-shaped content +with an explicit classifier check; a separate positive control retains plaintext +slot compatibility. The 400/no-upstream assertions and production logic are unchanged. diff --git a/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md b/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md new file mode 100644 index 0000000000..f74adf55c4 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/022_management_auth_port_fixture.md @@ -0,0 +1,27 @@ +# Check-phase port allocation repair + +CI34011124632 passed the corrected task-input cases and Linux shards, but two +unchanged macOS management-auth tests failed at the public Bun.serve bind with +EADDRINUSE. Both tests used findAvailablePort, whose Node probe closes its socket +before returning a number. The probes bind 127.0.0.1 while remoteConfig makes the public listener bind 0.0.0.0, so a loopback-only availability check also has the wrong address scope. reservedPort prevents the two selected numbers from +being equal; it does not keep either port reserved until Bun binds. The identity +of the intervening occupier is not established by the CI log. + +This is a prerequisite repair to the failing verification instrument, not a +change to authentication or production port policy. Modify only +tests/server/server-management-auth.test.ts: replace those two probe-close +setups with a small test helper that wraps Bun.serve synchronously, changes only +port to zero, calls the real Bun.serve and captures the real public/management +listeners while preserving each original hostname and fetch handler. Restore the spy before requests or any awaited cleanup. Derive the +management URL from its actual listener port and assert distinct live listeners. +Keep a valid positive configured ingress port so production config validation +remains unchanged; the fixture explicitly owns ephemeral bind allocation. + +The helper joins captured-listener cleanup if startup/fixture validation fails; +the existing finally blocks continue using the real composite server.stop. +Retain every trust, origin, credential, health, consent and pairing assertion. +No retry, sleep, skip, wider auth rule, or production test seam is added. + +Verification: independent fixture review followed by fresh exact-head hosted +CI. The same two real HTTP tests must pass, along with the new task-input cases +and full Linux/macOS checks. No local test suite is run. diff --git a/devlog/_plan/260906_release_244_followups/030_kiro_results.md b/devlog/_plan/260906_release_244_followups/030_kiro_results.md new file mode 100644 index 0000000000..c66ba1fccf --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/030_kiro_results.md @@ -0,0 +1,54 @@ +# Adjacent Kiro result coalescing + +Depends on task-input; class C4 for protocol identity. Fix #3734 from recorded Codex code-mode output shape, never by spending live Kiro quota. + +## Diff-level change map + +- MODIFY src/adapters/kiro.ts pushUser/turn-construction helper: when adding results in immediately adjacent parsed messages, tracked separately from collapsed user turns, combine only adjacent results with identical normalized toolUseId. Append content in exact input order and propagate error if any constituent is error. Preserve images via the adapter's supported representation; ensure no image is dropped or reordered relative to supported content semantics. +- Preserve the pendingToolUses.delete validation: call-a, call-b, call-a remains invalid. Do not globally deduplicate by id or merge across assistant/tool boundaries, intervening ordinary input, or unrelated result. +- MODIFY tests/providers/kiro/kiro-adapter.test.ts and relevant kiro-images.test.ts fixtures for three adjacent results, error later in group, different ids and nonadjacent repeats, text+image preservation. No new fixture uses real call ids or messages. +- MODIFY docs-site/src/content/docs/reference/adapters.md and structure/04_transports-and-sidecars.md with narrow multi-output contract. + +Before: pushUser appends each result, wire validation consumes the first matching toolUseId and rejects the next duplicate. After: consecutive same-call outputs become one ordered result before validation. Opaque encrypted output rejection remains unchanged. + +## Activation / verifier + +CI tests feed one assistant exec call followed by notify/notify/final results; assert one toolResult and ordered content. Mixed error/success reduces to error; unrelated result boundaries cannot be crossed. Same-id nonadjacent repeat still throws matching error. Exercise retained images using existing adapter representation; enforce maximum/shape constraints already owned by Kiro wire. Run existing Kiro adapter/image suites through ci.yml, plus full typecheck/privacy. Saved local log shape is supporting evidence only; live Kiro correctness remains untested and explicitly reported. + +## Non-goals + +No Kiro account/OAuth/quota changes, no aggressive malformed-history healing, no parser changes beyond prior layer, no global result deduplication. + + +## Source follow-up folded at roadmap lock + +Track adjacency in original message iteration; reset on every non-toolResult message including user/developer/assistant, even if pushUser collapses it into one user turn. Retain Kiro images on the current user image list as the existing wire format requires; do not promise unsupported text/image interleaving in the wire. Preserve Co-authored-by: Yrlan <71253160+yrlan-montagnier@users.noreply.github.com>. Local log metadata contains old Kiro activity and is not a current live reproduction. + +## Kiro-cycle P refresh on parent b24ed35a + +Parent #3743 is verified and ready, still open as this branch base; fixture prerequisite #3745 is merged. Issue #3734 remains open without an author PR. kiroPayloadMessages currently returns parsed.context.messages unchanged, so tracking adjacency at the top of its loop observes original Ocx message barriers even when a reasoning-only assistant is later skipped or user/developer turns collapse. + +Concrete source edits in src/adapters/kiro.ts only: priorCalls values retain rawId alongside wireName; validate each result against that exact raw id after normalizing for wire lookup. This rejects different raw ids sharing a replacement/truncation result without banning legitimate paired non-wire ids. Track adjacentRawToolResultId, reset it for every non-toolResult before any early continue; for matching adjacent raw id and last user turn/last wire result, append text content and images, set status error if any constituent isError. Otherwise retain pushUser and final conversation validation. No global dedup, cross-turn merge or normalizer change. + +MODIFY tests/providers/kiro/kiro-adapter.test.ts only for regressions: parse a real Codex custom_call plus three adjacent custom outputs (optionally preceded by the parent external task input), assert one ordered result; error remains sticky and images survive including image-only later output; single-result control; A/B/A and user/developer/assistant/reasoning-only barriers reject. Raw-id controls cover pipe/underscore, whitespace, truncation and case mismatches; exact raw pairs still normalize and merge. Keep every orphan/encrypted and catalog test. No new test/layout files. + +MODIFY docs-site/src/content/docs/reference/adapters.md Kiro section and structure/04_transports-and-sidecars.md with this bounded contract. Preserve Co-authored-by: Yrlan <71253160+yrlan-montagnier@users.noreply.github.com>. Resolve roadmap review thread PRRT_kwDOS-0Gi86fozIF only after the raw identity fix is verified. + +Local evidence limit: saved Kiro conversation data and OCX diagnostic artifacts were inspected for field shapes only; no current Codex multi-output Kiro trace was available. No raw message, id or credential was emitted, and no live Kiro request was made. Synthetic CI fixtures are protocol regression evidence, not a field-success claim. + +Dispatch: main owns adapter/docs; bounded worker owns only kiro-adapter.test.ts. Independent A/C reviewers inspect raw identity, original-message adjacency, error/image propagation and unchanged encrypted rejection. Full runtime CI is remote only, including existing Kiro image/adapter tests; live Kiro is forbidden. + +## Resumed P after verified guidance parent b7e67d84d + +The separate task-guidance cycle is complete, parent3743 P1 is resolved and CI34014313740 is green. Its verified head was merged into this preserved Kiro branch before implementation. Prior Euler review is folded below and must be rechecked before B. + +A contiguous group is finalized before any non-toolResult (including skipped reasoning-only assistant), before a different raw id, and after the loop. Track only local bookkeeping: rawId, reference to the fresh KiroToolResult, count, raw text parts and whether this group carried images; never put these fields on wire objects. A single-result group keeps the exact existing normalized text/fallback. For 2+ results, preserve ordered raw text parts except successful empty-exec wrappers, append images and keep any isError sticky. If the whole group has meaningful text, use those parts and remove any first-chunk empty fallback. Preserve whitespace text parts when meaningful text exists. If all text is empty, retain one existing fallback; use the neutral KIRO_EMPTY_TOOL_RESULT_MESSAGE when images or an error flag make an empty-success exec hint inappropriate. Failed exec wrappers are meaningful failure information and remain raw text in multi-result groups even when the incoming isError flag is false; preserve existing FAILED_EXEC_OUTPUT_MESSAGE for a single result. No new normalizer or message template. + +Read evidence: normalizeEmptyExecToolResultText distinguishes EMPTY_EXEC_OUTPUT_MESSAGE from FAILED_EXEC_OUTPUT_MESSAGE, and failed wrappers can arrive with isError=false. The wire validator requires at least one nonblank text part for each result; finalize groups before that unchanged validator. Keep the encrypted-content throw ahead of every grouping branch, and enforce exact raw id for every result, not only on coalescing. + +Additional regressions: later image-only/empty/success-empty wrapper does not inject placeholders into an already-populated result; initial empty then real text removes the empty hint; all-empty groups retain a valid nonblank result; multi-result failed wrapper retains its failure signal; later encrypted adjacent result still rejects; whitespace between meaningful chunks survives. Existing single empty/failed exec normalization tests must pass unchanged. + +## Resumed A dispositions + +Accept whitespace concern: collect a nonzero-length raw text part when trim is empty OR the shared normalizer did not classify it as EMPTY_EXEC_OUTPUT_MESSAGE. This preserves whitespace between/before actual text while discarding only true empty-success wrapper text; failed wrappers are never in that drop category. Finalization decides whether the aggregate has meaningful text. +Rebut the need for duplicated tool-name bookkeeping: create the first fresh wire result using the EXISTING normalizeEmptyExecToolResultText(text,{toolName,toolNamespace}) call before registering the group. A one-result group is never rewritten at finalization, so its exact precomputed fallback is retained; no normalization without identity occurs. Multi-result finalization replaces that initial content only with raw aggregate parts (or neutral empty text for image/error groups). Tests pin the existing single-result behavior and no bookkeeping keys on wire. diff --git a/devlog/_plan/260906_release_244_followups/031_kiro_result_implementation.md b/devlog/_plan/260906_release_244_followups/031_kiro_result_implementation.md new file mode 100644 index 0000000000..39dde71c1c --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/031_kiro_result_implementation.md @@ -0,0 +1,19 @@ +# Adjacent Kiro result implementation + +The adapter retains each original call ID beside its normalized wire ID and checks +that exact identity on every result. A local group tracks only adjacent results; +non-tool messages, another ID and end-of-input finalize it before the existing +conversation validator runs. The encrypted-result rejection still happens first. + +Single results keep their precomputed, tool-identity-aware normalization. Multiple +results keep ordered raw text, real whitespace and failed-exec wrapper information, +while empty-success wrappers do not become extra messages. An initial empty hint +is replaced when later text exists. Images remain on the user turn with existing +limits, error status is sticky, and entirely text-empty image/error groups use one +neutral fallback. Group bookkeeping remains outside Kiro wire objects. + +Regression coverage is added to the existing adapter test file, including the +parent task-input plus code-mode-output sequence, collision controls, barriers, +empty/failed wrappers and images. Yrlan's source contribution is attributed. +Proof is independent review and exact-head hosted CI; saved local metadata did not +contain a current multi-output reproduction and no live Kiro call is performed. diff --git a/devlog/_plan/260906_release_244_followups/032_review_doc_format.md b/devlog/_plan/260906_release_244_followups/032_review_doc_format.md new file mode 100644 index 0000000000..ba50f43360 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/032_review_doc_format.md @@ -0,0 +1,11 @@ +# Review documentation formatting + +C0 follow-up for PR3743 review threads: add blank lines after headings, format +replay-field identifiers as inline code, and correct the audit heading/references. +The same heading pattern is normalized only within this release's two owned units. +No runtime, test behavior or release gate changes. Validation is diff inspection +and git diff --check; no local test suite is required or run. + +Publish as a documentation-only layer above the Kiro PR so the verified runtime +heads remain stable. Resolve the parent formatting notes with this concrete fix +and land the layer bottom-up before release. diff --git a/devlog/_plan/260906_release_244_followups/033_web_search_deadline_fixture.md b/devlog/_plan/260906_release_244_followups/033_web_search_deadline_fixture.md new file mode 100644 index 0000000000..d67cca89f6 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/033_web_search_deadline_fixture.md @@ -0,0 +1,24 @@ +# Check-phase cumulative deadline fixture repair + +CI34016017020 passed Kiro checks but macOS1 failed an unrelated elapsed <500ms +assertion (644ms) in web-search-timeout-contract.test.ts. The contract uses a +45ms response-header deadline; the wall measurement also includes preparation +and host scheduling. Source still starts one deadline before the rotation loop +and does not await the first response body's cancellation promise. + +Modify only that test file. For this one cancellation/rotation case, spy on the +existing clearableDeadline export and provide a controlled original deadline. +Hold the body-cancel promise until fixture cleanup; queue controlled expiry at the +next timer task when cancellation is requested. The immediate rotated fetch must +record that cancellation is still pending and that the same signal is unexpired. +This catches an added timer wait as well as awaiting the broken cancellation. +Assert one deadline factory call, +one real rotated fetch, cancellation/rotation/expiry ordering, cleanup and the +same exact504 response. Keep the existing1000ms test timeout unchanged. + +The real-timer header-timeout and abort-library tests stay unchanged. The fixture +tests deadline ownership and nonblocking cancellation rather than a loaded host's +wall time. Restore the spy and release/abort controlled resources in both finally +and afterEach, including a failing or timed-out test. No production timeout, +retry, skip or local suite is introduced. Verify by independent review and fresh +hosted CI in a separate prerequisite test-only PR beneath Kiro. diff --git a/devlog/_plan/260906_release_244_followups/040_opaque_recovery.md b/devlog/_plan/260906_release_244_followups/040_opaque_recovery.md new file mode 100644 index 0000000000..e3f994d7a9 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/040_opaque_recovery.md @@ -0,0 +1,56 @@ +# Opaque output rejection and terminal error recovery + +Depends on parsed-input/Kiro integration; C4. Carry #3535 2396829bded6d2aaf319e67dddb5918d83d1d3a0 (base 7e7ab281cca35600b41f1f80222f3462a87dd4e1), Co-authored-by: yxr1995-maker <257504378+yxr1995-maker@users.noreply.github.com>. + +## Exact diff map + +- MODIFY src/lib/errors.ts: one ENCRYPTED_FUNCTION_OUTPUT_REJECTION constant, flat error message extraction alongside existing nested form. +- MODIFY src/server/responses/combo-stream-preflight.ts: optional narrow retryableTerminal predicate; existing two-argument callers retain default behavior. Bare error events count as uncommitted only where correctly retryable, not blanket authorization to replay effects. +- MODIFY src/server/relay.ts createSseTerminalOutputBoundary/upstreamErrorTailFrame and src/server/relay-eager.ts: observe upstream error on their own bounded client frame reader; at repeated bare-error EOF emit response.failed carrying real error instead of adapter_eof. Avoid async inspection branch race. +- MODIFY src/server/responses/core.ts encrypted function/custom outputs and agent_message detection, prepareOpaqueBlobRecovery, preflight: one sanitized rebuild before client output commitment; exact decrypt rejection predicate, not all HTTP 502. Mutate existing raw-body identity to preserve nonpersistable WeakSet marker. Preserve current rewrite ordering, cancellation and missing-call-id rejection. +- MODIFY tests/responses/responses-opaque-blob-recovery.test.ts, sse-failed-tail.test.ts, passthrough-abort.test.ts, tests/routing/combo-stream-preflight.test.ts as needed. +- MODIFY docs-site/src/content/docs/guides/sub-agent-surface.md and structure/04_transports-and-sidecars.md with bounded recovery/terminal behavior. + +Before: encrypted function output rejection can terminate without Responses terminal and surface adapter_eof; recovery handles fewer opaque shapes. After: one narrow sanitize/rebuild attempt; a repeated error is surfaced as failed with the actual message from the reader that delivers output. + +## Activation / review + +Remote tests: encrypted function-output or agent_message + exact decrypt failure permits one recovery; nondecrypt 502 stays unchanged; repeated flat/nested bare errors in tee and eager produce response.failed once; valid existing terminal wins; after client output commit no retry; raw-body identity/no-persist preserved; default combo caller compatibility maintained; caller cancellation remains cancellation. +Existing maintainer CHANGES_REQUESTED targeted older 2d90f9684 reader race; independent review of port must confirm remedy rather than asserting GitHub approval was granted. Remaining review threads checked for substance against final head. No preemptive stripping of all previous_response_id history, no broader retry policy. + +## Stack and proof + +Owner explicitly requests stacked PR workflow; use this relay foundation before combo-recovery and Grok terminal integration as an integration-validation stack, even though fixes are independently useful. Each layer remains independently tested via exact-head ci.yml runtime/gates. Security analysis stays scratch until public diff; no live Kiro. + + +## Current-dev carry amendment (2026-09-06) + +The carry starts at adb696197 after the task-input, Kiro and fixture layers. +The source remains 2396829bd. The current core rewrite order also contains +tool-search restoration and function completion repair; preserve both and the +shared prompt-cache cohort field. Source review is not current-head approval. + +Default two-argument preflight callers keep their previous event classification. +Only the explicitly supplied exact decrypt predicate may make a matching bare +error replayable; unrelated errors still commit the stream, and an existing +unrelated response.failed stays an SSE terminal. The retry predicate accepts +only error/failed/incomplete envelopes, never output events carrying a message. +The new failed tail uses existing redactSecretString before the 512-character +limit. Test bounded synthesized messages in tee and eager paths with synthetic +credential canaries; retain original upstream frame passthrough semantics. + +This cohesive carry exceeds the default 500-line review size because the source +includes a large request-level regression matrix. Keep source and regression +commits distinct inside this one layer, with independent protocol/security review; +splitting the tests into a later PR would leave recovery unverified. Existing +large core/relay files retain their current ownership for this bounded carry: +no export moves or broad refactor amid replay/cancellation changes. A new generic +retry abstraction or core extraction would enlarge the behavior under review. +Remote CI verifies all source and tests together; no local suite/typecheck/build. + +The existing core recognizes successful streaming Responses without Content-Type. +Keep that parity in the new preflight through an explicit fourth options argument +allowMissingContentType, enabled only by the same core streaming condition; default +combo callers still require text/event-stream. Add missing-header recovery and +non-SSE refusal controls. This avoids a source-PR gap where core selected recovery +but its preflight returned early solely because the header was absent. diff --git a/devlog/_plan/260906_release_244_followups/041_opaque_recovery_implementation.md b/devlog/_plan/260906_release_244_followups/041_opaque_recovery_implementation.md new file mode 100644 index 0000000000..a2dbe6f54c --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/041_opaque_recovery_implementation.md @@ -0,0 +1,20 @@ +# Opaque recovery implementation evidence + +Source 3b8cf8a8f carries PR3535 with a narrowly scoped preflight opt-in. The default +combo event classifier is unchanged; only a matched bare error supplied by the +native decrypt caller is replayable. Headerless streaming is an explicit option +under the existing core condition. Client-reader error evidence is redacted and +bounded before a failed tail is synthesized; real terminals remain authoritative. + +Independent plan audit accepted the scoped seam. Independent source/security +review passed: exact 502 gate, one sanitized rebuild, raw-body object identity, +no replay after visible output, cancellation and current rewrite ordering remain. +The source contributor is credited in the carry commit and PR. + +Regression commits cover native function and agent-message history, repeated +flat/nested errors, both relay shapes, unrelated errors and default combo byte +preservation, output commitment, missing-header and wrong-media-type controls, +and bounded synthesized-message redaction. The headerless fixture uses bytes +and asserts the absence of Content-Type because a string body supplies text/plain. +No local test suite, typecheck, build or live Kiro request was run. Final evidence +comes from hosted CI on the complete PR head and a fresh independent review. diff --git a/devlog/_plan/260906_release_244_followups/050_combo_recovery.md b/devlog/_plan/260906_release_244_followups/050_combo_recovery.md new file mode 100644 index 0000000000..33e4a905c4 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/050_combo_recovery.md @@ -0,0 +1,54 @@ +# Mixed encrypted combo recovery + +Depends on opaque-recovery for tested preflight/terminal composition; class C4. Carry #3706 c311e9598f9c4f3daf8cccdf1e27ba913ba94b30, source base 6dd23d6314c41f1113639e042353aae9e6614e62. Co-authored-by: yxr1995-maker <257504378+yxr1995-maker@users.noreply.github.com>. Preserve source commit snapshots, avoid replaying obsolete source branch merge commit 97f453ab. + +## Exact diff map + +- MODIFY src/combos/resolve.ts targetProviderIsUsable and pickComboTarget/pickComboTargetWithWait: canonical OpenAI account/model selector owns quota decisions, provider cached summary cannot veto canonical target; third-party/noncanonical provider quota still filters, including wait eligibility. +- MODIFY src/server/responses/core.ts handleComboResponses: select actually payload-compatible target before deciding recovery; extract bounded recoverUnreadableEncryptedTask and encryptedTaskRecoveryAttempted; if native configured but disabled/cooling/no selectable native, recover once only when a usable routed target exists. Native model/account authorization exhaustion permits one recovered routed dispatch, excluding attempted targets. Preserve lastFailure and no-readable-target failures. +- Preserve clientCancelledResponse mapping at BOTH recovery sites when recovery aborts. The source PR helper returning false must not turn caller cancellation into unreadable-task HTTP 400. +- MODIFY tests/server/agent-task-recovery-combo.test.ts and tests/codex-integration/combos.test.ts; broader existing recovery/security/fallback/combo-preflight fixtures remain authoritative. +- MODIFY all eight existing docs-site/src/content/docs/**/reference/configuration/agents.md pages, describing actual selectable-native vs configured-native behavior. + +Before: a merely configured native target suppresses recovery even when not usable; canonical provider summary may veto before account selection. After: native direct preference stays, usable routed recovery becomes reachable only once with explicit opt-in and no plaintext persistence. + +## Activation / verifier + +Remote tests cover native disabled/cooldown, native 401 exhaustion, canonical summary exhausted with eligible account, noncanonical quota veto, caller eligibility, cooldown waiting, all targets unavailable skips recovery, recovery failure never dispatches plaintext/ciphertext, aborted recovery at both sites returns cancellation, no retry after client output. Preserve 32-inflight and no-persist safeguards where owned by recovery helper. +CodeRabbit HTTPS-only suggestion is assessed against existing http provider policy: do not invent combo-only URL permission changes. Record evidence-backed rebuttal or a narrowly necessary fix during P/security audit. This carry does not change provider URL policy or credentials. Exact-head CI + independent security review required; no live Kiro or local suites. + + +## Current composition and cancellation amendment + +The lower stack PR #3753 is merged as b9f2acc82 from cd6d4d346 (full +CI34020474748 and independent security/final reviews passed). Source #3706 remains c311e9598; its source-only +patch applies cleanly to this foundation. Preserve every opaque preflight and +client-reader repair; only handleComboResponses changes in core. + +At the initial unreadable-task recovery site, a false helper result returns 499 +when the caller signal is aborted, otherwise the existing unreadable-task 400. +At native exhaustion, recheck caller cancellation after routed-target waiting and +recovery, before adopting the last native failure. A successful helper remains +one-shot; normal failed recovery preserves the prior failure and never dispatches +unreadable ciphertext or persists recovered plaintext. Add deterministic abort +fixtures at both recovery sites using the existing fake upstream boundary. + +Canonical forward providers defer account/model quota admission to the existing +native selector; caller eligibility, target cooldowns and attempted exclusions +still apply. Noncanonical hosts and third-party cached quota remain filtered. + +No combo-only HTTPS restriction is added: this routes recovered content through +the same operator-configured provider transport as the already-supported all-routed +recovery case. Recovery credentials still go only to its existing fixed backend, +and explicit opt-in, loopback/caller guards and no-persist policy remain unchanged. +Introducing a new URL policy only for this combo branch would contradict the +existing configured-provider contract without evidence of a distinct boundary. + +Also update the English guides/sub-agent-surface.md paragraph that currently says +combo routing is unchanged and native-only. The configuration pages alone would +leave that guide contradicting the newly reachable opt-in routed recovery path. + +The parent now also preserves native preflight read resets/cancellation and +tee/eager failed terminal accounting, including semantic streamAborted parity. +The combo delta remains unchanged through that cascade; a fresh composition +review confirmed the same patch and the complete child runtime passed CI34020475627. diff --git a/devlog/_plan/260906_release_244_followups/051_combo_recovery_implementation.md b/devlog/_plan/260906_release_244_followups/051_combo_recovery_implementation.md new file mode 100644 index 0000000000..a3ec651598 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/051_combo_recovery_implementation.md @@ -0,0 +1,35 @@ +# Mixed combo recovery implementation + +The carry changes only combo selection in core and provider usability in the +combo resolver. A selectable native target keeps priority. If native candidates +are unavailable or exhausted, an available routed target may be selected after +one explicitly enabled encrypted-task recovery. Existing caller admission, +fixed recovery backend, attempt exclusions and plaintext no-persistence remain. + +Canonical native quota belongs to account/model selection; cached summaries keep +filtering third-party and noncanonical providers. Both initial and late recovery +failures recheck caller cancellation, including cancellation during target waiting, +before returning an unreadable-task or prior native error. + +Original contributor tests cover disabled/cooldown/native-401, failed recovery, +unavailable targets, canonical/noncanonical quota and eligibility. The new paired +abort fixture waits for the recovery fetch to start, then cancels its actual signal; +499/client_cancelled, no routed call and empty cache/continuation stores are asserted. +No local suites/typecheck/build or live Kiro request are used. Hosted exact-head CI +and independent source/security/final reviews supply integration evidence. + +## Verified composition + +- Source fd5e90f1b and regressions cd054d926 passed independent source/security + and final reviews. The initial full hosted run was CI34019564577. +- Parent #3753 required a separate repair cycle for preflight read failures and + tee EOF account outcomes. That repair is merged on dev as b9f2acc82; source + cd6d4d346 passed CI34020474748 and its two review threads are resolved. +- The resulting child e1f5a5b8d passed full CI34020475627. Stable patch ID + 8b62ad9ebb675f63a6dd4933e22663b48e1d95f2 matches the original combo delta, + and a fresh composition review passed. This documentation closeout changes + no runtime or tests. Final PR-head checks remain visible on #3754. +- #3706 remains open until #3754 actually merges. Closure requires a fresh + merged-state and dev-ancestry check; a successful merge command is not assumed. + +No local suite, typecheck, build or live Kiro call was used for these results. diff --git a/devlog/_plan/260906_release_244_followups/052_shutdown_fixture.md b/devlog/_plan/260906_release_244_followups/052_shutdown_fixture.md new file mode 100644 index 0000000000..3242fea9a0 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/052_shutdown_fixture.md @@ -0,0 +1,19 @@ +# Shutdown fallback fixture clock + +CI34021352755 on the documentation-only combo closeout failed one macOS test: +shutdown drain cap expiry enters the synchronous spill fallback. The run had +10,050 passes and one failure. This file and production state.ts were unchanged +from the previously green e1f5a5b8d runtime. + +The fixture freezes ACL and spill clocks but the shutdown reserve uses Date.now. +An 80 ms reserve therefore still races host disk/scheduling latency (the failure +was ETIMEDOUT inside fallbackPendingResponseSpills). Freeze that third clock only +around flush, using the existing spy pattern from the neighboring ordering test. +The real 40 ms drain timer still expires while the async publication gate stays +held; positive synchronous-call, empty-pending and installed-stub assertions remain. +Release the gate, await the publication tail and restore the clock in finally. + +This C1 verifier repair changes one fixture, no production timeout, skip or retry +policy. Budget-exhaustion/watchdog cases remain untouched. Land as a separate +prerequisite PR and cascade the combo branch. Independent fixture review and new +exact parent/child hosted CI are required; no local suite/typecheck/build runs. diff --git a/devlog/_plan/260906_release_244_followups/060_grok_terminal.md b/devlog/_plan/260906_release_244_followups/060_grok_terminal.md new file mode 100644 index 0000000000..e921cb620d --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/060_grok_terminal.md @@ -0,0 +1,57 @@ +# Grok Build sparse terminal snapshot compatibility + +Depends on composed relay stack; C3. Carry #3388 645180ceaf123c954ab5306969cf82da83566648, old base 3c920af5f7b18ecd98f87a589d21d299f5cbe172. Co-authored-by: Maple (zleo-ai). Preserve current dev f121348a9 sparse JSON function-repair fixture when resolving EOF conflict. + +## Exact diff map + +- MODIFY src/server/responses-snapshot-repair.ts: add createGrokResponsesSparseTerminalBlockRewrite and narrow item validators; if file exceeds existing size significantly, extract separate src/server/grok-responses-snapshot-repair.ts for Grok-only tracker while retaining existing exports. Record extraction in P before B. +- MODIFY src/server/responses/core.ts existing rewrite list: enable only logCtx.surface === grok and insert Grok terminal tracker immediately before createResponsesSnapshotBlockRewrite. Preserve current order custom-tool restore -> tool-search restore -> Copilot -> Grok -> provider snapshot -> field backfill -> function repair -> undeclared-tool guard. +- MODIFY tests/responses/responses-snapshot-repair.test.ts and responses-snapshot-repair-server.test.ts; preserve existing sparse JSON function completion inference tests. +- MODIFY structure/04_transports-and-sidecars.md and public adapters reference with client-specific boundary. + +Before: Grok Build renders deltas but sees empty completed.response.output and may retry. After: only marked Grok requests reconstruct empty/missing completed output from raw unique contiguous bounded semantically validated done items. Ordinary clients and default provider responsesSnapshotRepair flag unchanged. Require nonempty call_id on reconstructed function/custom calls; incomplete/failed/contradictory/gapped/duplicate/oversized shapes remain unchanged or fail closed according to current contract. No output fabrication from deltas alone. + +## Activation / verifier + +Remote unit and server fixtures: Grok positive text/function/custom output, missing vs explicit-empty terminal, ordinary-client byte preservation, explicit provider snapshot + Grok coexistence, invalid item shapes/indexes/ids, duplicate/gap/bound checks, failed/incomplete terminal cannot become completed, raw done order retained. CI typecheck/privacy/runtime gates on final head; contributor reported old baseline failures are not accepted without current evidence. This is Grok Build terminal compatibility, not Cursor/Grok semantic no-progress issue #3506. + + +## Current composition and module decision + +Base: verified combo #3754 at 1697a7748. Source #3388 remains 645180cea. +The existing snapshot module is 621 lines; the source adds 327 lines for a +separate client policy. Keep the provider policy stable and put the Grok tracker +in new src/server/grok-responses-snapshot-repair.ts. Extract only the existing +isPlainObject, jsonBlock and RetainedOutputItem into a leaf +src/server/responses-snapshot-codec.ts so both trackers share their wire codec. +Core and Grok tests import the new tracker directly; existing public snapshot +exports stay unchanged and no convenience re-export or circular edge is added. +The tracker imports the existing relay retention limits, SSE block type/parser +and budget type. The codec imports nothing. This local functional dependency +replaces duplication; the stream order is an explicit temporal dependency. + +Keeping everything in the old file would mix two different opt-in contracts and +push it near 950 lines. A broad provider-tracker refactor is also rejected. The +old module remains above the default size limit but shrinks without behavioral +changes; the new tracker stays below 400 lines. Its stateful closure remains one +cohesive retention owner. The source/test carry exceeds 500 lines because its +regression matrix must land with the behavior, not as an untested upper layer. + +Keep the source Grok describe as one top-level block before the existing provider +snapshot describe; do not split the latter. Preserve the current server file's +f121348a9 sparse JSON/function-repair EOF fixture. Add missing/empty/whitespace +call_id negatives for function/custom calls, a valid custom call alongside a +visible message, and a same-provider absent-marker/marker=1 server control. + +x-opencodex-grok: 1 is a client-selected compatibility opt-in, not authenticated +client identity. Do not add authentication or infer privileges from it. Public +adapters documentation must describe that boundary. No live Grok or Kiro probe +is required for this synthetic protocol repair. + +## Asynchronous verification + +The user directed CI to run after implementation asynchronously. Close this +implementation cycle after source audit, attributed PR and exact-head CI queue +verification, then proceed to the next unit. c-grok-terminal remains open until +hosted runtime CI succeeds; release convergence owns that unchanged criterion. +Do not merge or publish an unverified head. No local suites/typecheck/build. diff --git a/devlog/_plan/260906_release_244_followups/070_quota_proxy.md b/devlog/_plan/260906_release_244_followups/070_quota_proxy.md new file mode 100644 index 0000000000..c8d98e1373 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/070_quota_proxy.md @@ -0,0 +1,46 @@ +# Windows quota network-path evidence + +Depends on composed runtime; class C3 investigation and diagnostic documentation. #3644 remains a current-version evidence gap, not a proven entitlement or retry defect. + +## Exact map / before-after + +- READ src/codex/auth-api.ts fetchMainAccountInfoWhileOwned and listCodexAuthAccountsSnapshot: WHAM uses Bun fetch; quotaRefresh result is identity-fenced. READ src/codex/quota-refresh-outcome.ts enum/projector, src/cli/account-api.ts fetchCodexRows, src/config.ts applyProxyEnvWith, src/lib/windows-system-proxy.ts readWindowsSystemProxy, src/server/index.ts applyProxyEnv call. +- MODIFY docs-site/src/content/docs/reference/configuration/server.md and its seven existing translated counterparts: explain explicit proxy:auto/HTTP proxy versus an unset config and service-start environment; show privacy-bounded ocx account list openai --quota --refresh --json fields quotaRefresh.status and optional httpStatus. Do not paste account ids or credentials. Explain that WinINET/PAC/SOCKS and TUN are not equivalent transport evidence. +- MODIFY numbered outcome record only if current docs already fully cover this; NO runtime policy change without a reproduced categorized failure. Existing diagnostic #3693 (71edeec8807d99e8e56a8c093f74da27d163d47a) already carries Ingwannu's implementation, so no redundant reimplementation. +- Existing tests/codex-integration/codex-auth-api.test.ts, tests/cli/cli-account.test.ts, tests/server/proxy-env.test.ts are remote verifier paths; add fixture only for an uncovered documented config contract. + +Before: reporter's 2.43.0 output lacks newly landed quotaRefresh; system proxy mode null quota cannot distinguish direct network failure, HTTP failure or parsing. After: next release exposes already-implemented categories and explicit network setup guidance. A/B same machine/account: TUN on versus TUN off with explicit auto/HTTP configuration; observe status/HTTP code, not raw payload. No Windows environment is fabricated locally. + +## Acceptance / completion + +Fresh source and CI show diagnostic fields travel enum -> main-account cache -> snapshot -> CLI, with malformed/unrecognized extras dropped and null not converted to zero. Document unsupported PAC/SOCKS-only behavior according to actual code. Leave issue open if reporter evidence is still absent and record FIELD_VALIDATION_PENDING, rather than calling the underlying incident fixed. This evidence-limited investigation outcome satisfies this named investigation slice, not a false runtime fix. No outbound credentials or system configuration changes here. + + +## Current-source documentation decision + +The reporter's latest correction still uses 2.43.0; the maintainer explicitly +keeps #3644 open until a build containing #3693 supplies categorized A/B evidence. +Current main-account fetch, identity-fenced snapshot and CLI projector confirm the +diagnostic contract. CLI account.ts declares the existing command used below. +No new runtime fix or field-validation result is available. + +Add an English Codex quota network diagnostics section to server.md and concise +translated sections in ko/ja/fr/ru/tr/zh-cn/zh-tw linking that canonical anchor. +Scope the field to the main Codex account row, not every stored Pool account. +Explain that quotaRefresh (not the quota numbers themselves) is diagnostic only, +that cached/no-attempt output can omit it, and null quota is not zero quota. +Show the real account-list command with a PowerShell projection that outputs only +quotaRefresh, never complete account records. Use the seven fixed status strings +and optional httpStatus only for http_error; no inference of entitlement failure. + +Keep user guidance about the service environment, unset proxy and startup-only +WinINET auto detection; omit internal function/file names from the guide. State +that PAC/WPAD/SOCKS-only and live changes are unsupported by this auto discovery. +Do not claim TUN as a fix. Keep FIELD_VALIDATION_PENDING and the issue's open state +in 071 outcome notes/PR description, rather than hard-coding transient issue status +into all eight evergreen user pages. No local network or account calls. + +Per owner steering, documentation CI runs asynchronously. c-quota-proxy remains +open under release convergence until its scoped checks and unchanged-runtime +source evidence are reconciled. Do not add implementation-mirroring tests for +this documentation-only outcome; existing quota/CLI/proxy tests cover the code. diff --git a/devlog/_plan/260906_release_244_followups/071_quota_proxy_outcome.md b/devlog/_plan/260906_release_244_followups/071_quota_proxy_outcome.md new file mode 100644 index 0000000000..144266fcce --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/071_quota_proxy_outcome.md @@ -0,0 +1,20 @@ +# Windows quota investigation outcome + +FIELD_VALIDATION_PENDING. Issue #3644 remains open. The latest reporter correction +still concerns stable 2.43.0: TUN works, system proxy/rule mode without TUN returns +null plan/quota. The maintainer explicitly requested a categorized comparison from +a build containing #3693. No such current-build result is present. + +The existing #3693 diagnostic is on dev: main-account WHAM fetch outcome, identity- +fenced snapshot, and CLI projector preserve the fixed status vocabulary and optional +HTTP code. Null quota is not rewritten to zero. Current CLI declares +ocx account list openai --quota --refresh --json. Source inspection also confirms +service-start environment handling and static WinINET auto discovery. + +This unit adds canonical user guidance and seven translated links. It changes no +runtime retry, credentials, quota admission, proxy defaults or user configuration. +No live account/network probes or local suites/typecheck/build were run. Existing +quota/CLI/proxy regressions remain; documentation CI is submitted asynchronously. +The incident is not claimed fixed, and no reporter message is needed beyond the +already posted maintainer request. Final release reconciliation retains the open +field-validation status. diff --git a/devlog/_plan/260906_release_244_followups/080_usage_source.md b/devlog/_plan/260906_release_244_followups/080_usage_source.md new file mode 100644 index 0000000000..8c0f22ef89 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/080_usage_source.md @@ -0,0 +1,19 @@ +# Canonical xAI usage-attempt provenance + +Depends on routing/replay changes; class C4 due credential-derived logging. Carry #3642 head 146ed679c9633e5d68726217fcadc8e0b107339b, preserving Co-authored-by: olddonkey . Refresh source head before carry. + +## Exact map / field chain + +- MODIFY src/server/request-log.ts after sealRequestAttemptIdentity: recordAttemptCredentialSource clears stale value and derives only grok-oauth or xai-api-key from resolved canonical xAI transport and authMode. Require https, correct host/path policy and no userinfo/query/custom port; unknown/custom/provider mismatch omits. +- MODIFY src/server/responses/core.ts after initial identity seal and every reseal that can change selected transport. Inspect later seals individually: OAuth retry same physical attempt retains source; new transport clears/rederives it. +- MODIFY src/server/chat-native.ts buildActiveRequest: record from activeProvider at initial build and key-pool rebuild, after resolution. +- MODIFY src/usage/log.ts: UsageCredentialSource union, optional persisted attempt field, normalizeUsageAttempt sanitizer accepts only fixed enum for xai attempts; unknown/historic/non-xai values omitted. +- MODIFY docs-site/src/content/docs/reference/adapters.md and docs-site/src/content/docs/reference/management-api.md, plus directly contradictory translated rows if any. +- MODIFY tests/usage/request-log.test.ts, tests/usage/usage-log.test.ts and tests/server/server-xai-oauth-401-replay.test.ts, retaining original behavioral tests and adding any uncovered reseal case. + +Creation resolved runtime provider -> attempt helper; serialization usage append; deserialization normalizeUsageAttempt; consumers request history/management JSON/CodexBar integration read optional per-attempt value. No top-level combo attribution and no backfill from today's config. No UI enum interpretation added in this PR. + +## Activation / verifier + +Remote tests prove canonical OAuth Responses 401/replay source with sendCount=2, native Chat API-key source, pool rebuild, combo mixed attempts, stale label clearing, unknown enum/custom host/query/userinfo/port/non-xai/historic omissions, privacy canary excluded. All schema fields existing default behavior retained. Run ci.yml runtime+gates and explicit security review; no local tests or xAI live traffic required. Proof records final PR head and source identity, not fork author attestation alone. + diff --git a/devlog/_plan/260906_release_244_followups/082_recovery_doc_alignment.md b/devlog/_plan/260906_release_244_followups/082_recovery_doc_alignment.md new file mode 100644 index 0000000000..faf812d68b --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/082_recovery_doc_alignment.md @@ -0,0 +1,7 @@ +# Recovery wording alignment + +C1 documentation follow-up to public PR3754 threads PRRT_kwDOS-0Gi86fqklW and PRRT_kwDOS-0Gi86fqklX. Parent3762 at63282e49c; runtime3754 at8de126998 passedCI34025899357. No runtime behavior changes. + +Modify guides/sub-agent-surface.md and fr/zh-cn/zh-tw reference/configuration/agents.md only: native absence/exhaustion may activate explicitly enabled recovery toward an eligible routed target; unreadable ciphertext is not sent if recovery cannot provide a usable task. Clarify pre-dispatch unreadable400 versus preservation of a concrete failed native attempt; cancellation499 remains as implemented. Existing recovery auth, quota and no-persistence boundaries stay intact. + +Verifier: source-bound semantic comparison with existing helpers/fixtures, exact-head docsCI submission and outcome; no local test, typecheck or docsbuild under user limits. No external new permission, dependency or release rule changes. Close the public wording finding after published corrected pages; late runtime-status suggestion is rebutted with preserved native-failure contract and passing stored-Pool regression. diff --git a/devlog/_plan/260906_release_244_followups/090_dashboard.md b/devlog/_plan/260906_release_244_followups/090_dashboard.md new file mode 100644 index 0000000000..fcf4f54bb3 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/090_dashboard.md @@ -0,0 +1,31 @@ +# Dashboard alignment carry + +Depends on integrated runtime for final presentation; independent PR, class C2. Carry #3697 head 49a9c79392babd9413831437d6ad71839737b148 (base cededd5ad1b8f8c437813c315c0705ace6c950c3), preserving Co-authored-by: Robin Bially <7304732+RobinBially@users.noreply.github.com>. #3689 authless-default change is outside this train. + +## Exact change map + +- MODIFY gui/src/styles-dashboard-workspace.css: shared label/control columns, --dash-controls-width around 26rem, container-based collapse, full-width delegation/sync rows. +- MODIFY gui/src/styles.css: consistent status card alignment and responsive version badge behavior. +- MODIFY gui/src/pages/dashboard-overview-head.tsx and dashboard-overview-sections.tsx: carry original layout classes only; preserve all handlers, state and new controls from current dev. +- MODIFY gui/src/App.tsx: sidebar/mobile version width yields to product name and retains full-value hover. +- MODIFY gui/tests/mobile-topbar-layout.test.ts: version flex-shrink and stable small-layout contract. +- MODIFY docs-site/src/content/docs/guides/web-dashboard.md; ADD original screenshot docs/pr-assets/dashboard-settings-aligned.jpg only as supplied by source PR, mark its source/version clearly. Capture updated screenshot if final rendered content differs. + +Before: uneven columns, two-up tool cards squeeze controls, version text can take product space. After: wide single label/control grid; narrow stacks preserve reading order and 320px selector fit. No visible strings added; any necessary additions require all locale modules. + +## Acceptance / verifier + +Remote GUI lint/stylelint, GUI tests and Vite build from ci.yml; verify rendered wide/narrow state using existing browser tooling with CI-built/static artifact when available (no local suite/build). Inspect original screenshot at exact source SHA and do not claim it proves later changed content. New screenshots must show final UI, with no account info. Regression test alone is not visual proof; independently inspect UI screenshot and CSS breakpoints. + +## Limits + +No authless setting, quota semantics or model management expansion. Preserve current state labels and accessibility. P rechecks any intervening same-file changes before carrying. + + +## Hosted artifact verification + +Main owns the eight-file attributed carry; handlers and visible copy remain unchanged. The existing `ci.yml` gates job uploads a preview after its GUI build when `changes.outputs.gui` is true. `actions/upload-artifact` v7.0.1 is pinned to `043fb46d1a93c77aae656e7c1c64a875d1fc6a0a`, verified against the official release and tag. The artifact contains only `gui/dist`, including generated `build-commit.txt` and `build-gui-tree.txt`, with `retention-days: 7` and `if-no-files-found: error`. Triggers, permissions, secrets, checkout behavior and release eligibility remain unchanged. This workflow surface makes the unit C4 and requires independent security review. + +PR CI may build a merge ref, so compare the artifact's GUI tree with the reviewed head's `gui` tree before using its screenshots as final evidence. Serve the downloaded build with an isolated fixture API and inspect it at 1440, 1024, 768, 390 and 320 CSS pixels, including keyboard operation, focus and overflow. No local build, typecheck or repository test suite is run. Public screenshots contain synthetic data only. The original contributor screenshot is a reference, not final-head evidence. + +Use manual dependent PRs after the owner's native-stack removal. CI and admin integration continue asynchronously without a local rebase. Hosted CI owns lint, typecheck, tests, build and privacy checks. The release goal remains open until publication proof is complete. There is no user-imposed token or cost cap; individual tooling runs are bounded at 30 minutes and waits at 60 seconds. diff --git a/devlog/_plan/260906_release_244_followups/091_dashboard_verification.md b/devlog/_plan/260906_release_244_followups/091_dashboard_verification.md new file mode 100644 index 0000000000..bd3f413b41 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/091_dashboard_verification.md @@ -0,0 +1,30 @@ +# Dashboard visual verification + +Source PR #3764 at 42689e02a60ca230e53dee1f872864af5d6b0872. Hosted CI 34029024036 passed the runtime matrix, gates and installation checks. Gates built artifact 9988003853 from checkout 274d2f8cb7ad8c381db29fa1799ad446c034da39. Its GUI tree 1eff4e4d3485133600e4fdb6e9ba78c36c0bf1e4 equals the reviewed source tree. The artifact SHA256 is c4f507ac7ab269170d6ec82e5351b8a526072524baa95403046e52293ae831de. + +The real built frontend was served on an isolated loopback fixture API. Displayed model selections, request totals, memory figures and the deliberately long preview version come from synthetic fixtures. No live provider or running OpenCodex service was used. No repository local test suite, typecheck or build was run. + +## Observed results + +| Scenario | Evidence | Result | +| --- | --- | --- | +| Desktop 1440 | [capture](screenshots/dashboard-1440.png) | Controls align, long model labels stay inside their buttons | +| Split-screen 1024 | [capture](screenshots/dashboard-1024.png) | Shared control rows stack when the content area narrows | +| Tablet 768 | [capture](screenshots/dashboard-768.png) | No horizontal page or control overflow | +| Mobile 390 | [capture](screenshots/dashboard-390.png) | Radio group, effort pair and version badge fit | +| Narrow 320 | [capture](screenshots/dashboard-320.png) | No horizontal page or control overflow | +| Narrow shadow row | [capture](screenshots/dashboard-320-lower.png) | Full Korean heading is one 21px line; source badge wraps below | +| Keyboard selection | [open](screenshots/dashboard-320-keyboard-open.png), [saved](screenshots/dashboard-320-interaction.png) | Visible keyboard ring; high to xhigh produced one fixture PUT and persisted the value | +| Empty/repeated choice | [capture](screenshots/dashboard-320-empty-repeat.png) | Selecting no limit twice keeps the null state and readable placeholder | +| Dark/reduced motion | [capture](screenshots/dashboard-1440-dark.png) | Readable control labels and boundaries; reduced-motion media active | +| Two-times pinch zoom | [capture](screenshots/dashboard-1440-zoom2.png) | Zoomed viewport captured; reflow is established by the separate CSS-width matrix | + +Each PNG has its signature, nonzero size and exact requested width by 900px height verified. Main inspected every referenced frame; two independent rubric-bound reviewers passed the final set. DOM measurements show page scrollWidth equals viewport width and every select label remains within its button. The shadow heading height equals its 21px line-height at all five widths. Fresh browser console capture contained no output; loaded assets and fixture calls used the loopback origin. + +The first artifact at eb35039fd reproduced a long delegation label reaching 1425px beyond a button ending 1218px. ec88720e6 added scoped span shrink/ellipsis rules. The first narrow capture then exposed a Korean heading orphan; 42689e02a wraps shadow metadata below the heading on narrow containers. These were observed corrections, not inference from green CI. + +Shared Select post-save focus behavior and unchanged sticky chrome were not represented as repaired. Malformed free-form input is not exposed by these select-only layout controls; HTTP parsing is unchanged. Source compatibility and the 15-line artifact workflow addition received independent reviews. The upload remains contents-read, uses an immutable action pin, uploads only built output and expires after seven days. + +## Teardown + +The fixture process was terminated, the isolated Chrome profile was stopped, and both listening ports were confirmed closed after the captures. Raw captures, exact invocations, DOM measurements and the validated three-scenario QA receipt remain in the local session evidence directory. This publication commit adds documentation and captures only; the GUI tree is unchanged from 42689e02a. diff --git a/devlog/_plan/260906_release_244_followups/100_release.md b/devlog/_plan/260906_release_244_followups/100_release.md new file mode 100644 index 0000000000..c3a53873d2 --- /dev/null +++ b/devlog/_plan/260906_release_244_followups/100_release.md @@ -0,0 +1,24 @@ +# Release verification and publication + +Depends on all preceding delivery criteria. Class C4. No local test/build/typecheck run. + +## Exact actions and file map + +- MODIFY package.json via scripts/bump-dev-version.ts for pre-move: for target 2.44.0 dev must outrank target before publish (normally 2.45.0). Freeze feature RC before pre-move and pin it. +- Use existing scripts/release.ts authority and .github/workflows/release.yml, ci.yml, service-lifecycle.yml. No changes planned unless an evidenced defect blocks this train; add a dedicated phase for such repairs. +- Create bounded promotion branches from verified feature RC independently for preview/main; version-only preparation matches intended preview/stable targets. Never mix unrelated current-main state or overwrite the bound worktree. Reviewable promotion PRs include target exception and verified UI screenshot/link from current delta. +- Push --no-verify; merge --admin --match-head-commit after exact-SHA gates. Prefer merge ancestry on live stacks; squash only terminal branches with cascade accounted for. +- Dispatch ci.yml lane=all on the frozen candidate AND each exact preview/main publish expected-sha (version-only promotion commits are separate heads); require actual successful windows 1/6 through 6/6 plus Linux/macOS, gates/privacy/typecheck and package install jobs where applicable. Dispatch service-lifecycle.yml on exact final promotion heads if not triggered. Do not count skipped windows as passed. Wait for the main promotion head docs build from deploy-docs.yml before stable publication; it is separate from ci.yml, which does not build documentation. +- Dispatch release.yml using verified inputs version, tag, expected-sha, dry-run=false. Preview precedes stable; npm target @bitkyc08/opencodex. No helper rehearsal assumed nonmutating. +- Verify npm version metadata, dist-tags, tarball sha512, gitHead, signed provenance, git tag target and GitHub release for each channel. If publish succeeded but smoke failed, inspect before retry; never republish blindly. +- Close fully resolved issues and superseded source PRs with original-author credit and actual landing references. Keep #3644 open if network root cause remains unproved and clearly communicate its tested diagnosis outcome. Document Kiro live test absence. +- MODIFY this unit's numbered evidence/closeout; archive to devlog/_fin only after outcome is public. Complete goal only after E8 criteria and every D closure succeeds. + +## Failure activation / proof + +A failed exact-SHA run triggers log-based RCA and repair; newer dev invalidates ancestor assumptions and is fetched before merge. A missing service run is dispatched, not skipped. Registry already-published check prevents duplicate publication. Final source head, artifact head and tag head must match documented promotion topology. Rollback means redeploy prior known package/version; immutable npm version is not deleted or overwritten. + +## Resources and security + +GitHub Actions/OIDC and existing registry read access, no static npm secret introduced. Existing main/preview protection retained; per-user admin merge authorization applies to this train. Commands bounded at 30 minutes, polls <=60s, continue across CI windows with persistent evidence. Source runtime and artifact validation use remote CI only. + diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1024.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1024.png new file mode 100644 index 0000000000..4bbd3ec147 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1024.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-dark.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-dark.png new file mode 100644 index 0000000000..06655de300 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-dark.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-lower.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-lower.png new file mode 100644 index 0000000000..8fee2e6757 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-lower.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-zoom2.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-zoom2.png new file mode 100644 index 0000000000..8464793fa0 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440-zoom2.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440.png new file mode 100644 index 0000000000..097a22f043 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-1440.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-empty-repeat.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-empty-repeat.png new file mode 100644 index 0000000000..b3fe763d4c Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-empty-repeat.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-interaction.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-interaction.png new file mode 100644 index 0000000000..f9ecfe445c Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-interaction.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-keyboard-open.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-keyboard-open.png new file mode 100644 index 0000000000..60e2c8fbf6 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-keyboard-open.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-lower.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-lower.png new file mode 100644 index 0000000000..b26cf9acbb Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320-lower.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320.png new file mode 100644 index 0000000000..b0faf7667d Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-320.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-390.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-390.png new file mode 100644 index 0000000000..7e513265b1 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-390.png differ diff --git a/devlog/_plan/260906_release_244_followups/screenshots/dashboard-768.png b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-768.png new file mode 100644 index 0000000000..7b96c13217 Binary files /dev/null and b/devlog/_plan/260906_release_244_followups/screenshots/dashboard-768.png differ diff --git a/devlog/_plan/260906_release_244_publish/000_plan.md b/devlog/_plan/260906_release_244_publish/000_plan.md new file mode 100644 index 0000000000..02192a924f --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/000_plan.md @@ -0,0 +1,16 @@ +# 2.44.0 release train + +Loop: satisfy-spec / C4. Trigger: maintainer explicitly requested main + preview merges and deployment after PR3771 CI passes, with cxc-loop. Goal: verified npm preview and stable2.44.0 plus release docs. Non-goals: local suites/typecheck/build, liveKiro, unrelated features, native stacks, rebase, direct protected-ref pushes, credential/settings changes. Verifier: GitHub exact-SHA jobs and actual Test steps, release workflow immutable SHA guards, npm metadata/digests and source-bound docs deploy. Stop: all five units done with receipts; never count pending/skipped as pass. Memory artifact: this numbered unit + .tmp/release-244 + bound goalplan. Outcomes: DONE only with published channel proof; failed gates remain unresolved; ambiguous publish requires registry inspection. Escalation: missing actual account authority or outstanding maintainer objection, unplanned security defect, or exhausted evidence-driven attempts. Existing GitHub/npm OIDC access only; user supplied no numeric token/time budget. Operational review checkpoint: six hours or five failed evidence-driven attempts per release surface; do not call that success. Leaves have read-only audit scope; main reclaims failed dispatches. + +## Dependency order +1. Roadmap docs only (010). +2. Exact regression candidate integration and freeze (020). +3. dev pre-move2.45.0 (030). +4. Independent preview promotion, dry run and publication (040). +5. Independent stable promotion, dry run, publication and docs proof (050). + +Fresh baseline: main06ec553630fa2ee51a96b5cbf694089021249194/latest2.43.0; preview53c784c2a635b061799e4f7542432a921f548bf9/2.43.0-preview.20260906; devbd1cda99c162e3b4b41b14f6ad5ca2cf6f1a1f03. Candidate69f9e07c4fa7b80bcda9e4ba28e3c64f42187828 includes that dev. Service34034184142 all3pass; manualCI34034178072 completed with one Windows3/6 cold-restart apply deadlinefailure; originalWindows25 and macOScontrol20404/0 nowpass. #3763 deferred documentation remains user-withdrawn; #3644 field report remains unresolved, with no runtime-fix claim. This train does not silently reinstate either task. + +Authority: MAINTAINERS.md and scripts/release.ts / .github/workflows/release.yml. Local release helper is not invoked because it runs local suites and can push. Existing workflows perform build/audit/pack/publication on hosted runners. No new runtime field/enum or enforcement is added. GitHub required checks/immutable workflow guards are enforcement; manual admin integration remains bypassable owner authority and is documented accurately. SoT sync: no architecture/CLI contract changes; public release notes generated by the existing changelog builder. Existing dashboard evidence from prior verification is reused with exact source provenance, never claimed as a new render. + +Latest user steering: no heartbeat automation; track all CI and release workflows by direct bounded polling in this task. Automation3771-ci-dev is absent (delete returnednot_found). No replacement automation is authorized. diff --git a/devlog/_plan/260906_release_244_publish/010_roadmap.md b/devlog/_plan/260906_release_244_publish/010_roadmap.md new file mode 100644 index 0000000000..4b176d7ba3 --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/010_roadmap.md @@ -0,0 +1,6 @@ +# Roadmap lock +Dependencies: none; consumes previous verification-only conclusion, whose no-release limit is superseded by the new explicit maintainer request. +NEW local numbered000/010/020/030/040/050 documents and goalplan. No product delta or remote publication in this cycle. Before: no release-authorized active plan. After: dependency-ordered audited plans with all final-head/registry criteria. Review complete docs and live workflow inputs; source audit commands are read-only. Check `git diff --check`, required doc existence and nonempty criteria; these verify documents, not product runtime. Commit these records on codex/release-244-publish-07c0 only. Keep this commit out of PR3771's already-running head. D records independent audit and next020. Existing no-local-suite constraint still applies. + +## Locked CI event contract after independent audit +Windows1/6 through6/6 and macOScontrol are RC/#3771 validation gates. For version-only independentpreview/main promotions, require each finalSHA's successful push-event Cross-platformCI and sameSHAServiceLifecycle; do not launch laneall on a releasebranch while its pushrun is active, because branch-ref cancel-in-progress can cancel the requiredpushrun. If any runtime/source drift from frozenRC appears, stoppromotion and returnto RCvalidation. An extra manualrun, if actuallyneeded, starts only afterpushCI completion. This is the predeclared gate mapping, not a waiver of a failedtest. diff --git a/devlog/_plan/260906_release_244_publish/020_integrate.md b/devlog/_plan/260906_release_244_publish/020_integrate.md new file mode 100644 index 0000000000..4fdff56abd --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/020_integrate.md @@ -0,0 +1,43 @@ +# Regression integration and immutable RC +Dependencies: roadmap. Initial pre-diagnostic scope: no new production code was planned; the later test-only SPAWN_BUDGET_MS change for apply.child.exited is recorded below. PR3771 already contains tests/preload.ts, tests/ci-workflows/test-runner.test.ts, src/adapters/cursor/native-exec.ts and tests/providers/cursor/cursor-blob.test.ts. Before: open draft69f9e07c4. After: reviewed green PR squashed into dev and recorded integrated RC with package2.44.0. +Read `gh pr view 3771 --json headRefOid,baseRefName,state,statusCheckRollup,reviews` and GraphQL reviewThreads plus live maintainer permission. Read manualCI34034178072 and lifecycle34034184142, asserting head equality and actual Windows1..6 Test/macOScontrol execution. Original25 Windows cases must pass; Cursor4096/4097 original bound and added expiry/accounting tests must pass. Trigger scenarios are original failures and missingcapability/expired-pin cases already encoded in regression tests. If red, inspect failing job logs and only repair the scoped cause in an amended unit; no retry-as-fix. +When all required exacthead checks/reviews pass, record maintainer integration evidence in PR body, mark ready and `gh pr merge 3771 --admin --squash --match-head-commit `. Validate dev base immediately before merge. Fetchdev, verify merge commit ancestry, and record RC as the integrateddev commit. Any later devcommit is not silently added to RC. No directdevpush. Host CI verifier APIs already ran successfully (exit0) and read this exact head, while final conclusions remain pending. + +## Fresh release blocker discovered during roadmap audit +At69f9e07c4 manualCI34034178072 Windows3/6 job101489123778 fails tests/claude-integration/claude-desktop-remote-hub.test.ts stored-profile=true on its30second RemoteDesktop apply deadline; falsecase passes in61.7seconds total. macOScontrol is nowgreen. Extend this unit to diagnose/fix the newly observed Windows release-validation failure using explicit hypotheses and hosted baseline/candidate evidence, preserving original behavior assertions and isolation. Candidate file scope is that fixture and its direct CLI apply dependency only if diagnosis establishes runtimecause. No change justified by merely increasingbudget/retrying. Update this plan with exactdiff and independentreview before implementation. #3771 cannotmerge until allgates pass. + +## Diagnostic diff before repair +PreviousD locked releaseorder and gateeventcontract. H1 loopbackdownloadfails5secondbound; H2 Windowschildnativework (PowerShell/icacls) occupies30seconddeadline; H3 commandfails to reachprocess.exit. Falsifiers: fetchstart/endtiming, nativeprocesscategory+duration, processexitmarker and alive-at-deadline. Per-process timers afterprocess.exit cannotexplainfailure. +FirstB diagnostic only: temporarycontents:read Windowsworkflow on separatecodex/diagnose-desktop-apply-07c0 rooted at69f9; pinnedcheckout/setup, existinginstall/build. MODIFY tests/fixtures/claude-desktop-network-guard.ts onrunner to wrapBun.spawnSync/fetch/process.exit with fixedevent/category/millisecond receipts to a fixtureownedfile; preserveoriginalnetworkpermit. MODIFY failingtest onrunner only to print atmost512fixedrecords in a finallyblock aroundunchanged30000msdeadline. No rawargs/URLs/tokens/stdout/stderr or productionpatch. Exact instrumentation and workflow are .tmp/release-244/ci-repair/instrument-desktop-apply.py anddesktop-apply-ci.yml. BaselineWindowstrue/falsepassed82.1/79.5seconds total; candidate69.3failure/61.7pass doesnotisolateapplycost. Reviewdiagnosticthenexecute; use findings to amend exactrepairdiff and re-audit before production/testpatch. + +Diagnostic audit synthesis: reviewerconfirmedpermissions/pins/networkguard/30sdeadline andprivacyscope. Added asynchronousBun.spawn completiontraces (not justspawnSync) tocover asyncACL. Removedredundantglobal--timeout60000 fromisolateddiagnostic (eachcasealready240000 andcleanup90000). JSONLsplit findingrebuffed withgeneratedTS static inspection: Pythonwrites oneescaped\\n intoTSstring, whose runtimevalueisnewline; replacingitwithphysicalnewlinewouldbreakTS. No repositorytests/typecheck/buildran; onlyfilegenerationintoscratchwasinspected. + +## Measured cause and proposed one-file harness repair +Diagnostic34035944642 at9358fad1: apply25,929ms exit0; nativeknownfolderPowerShell22,811ms, SID197ms, hubdownload10ms, ACLsteps16-19ms each, registry24/16ms, process.exit0. H1downloadstall and H3exit/lingeringhandle rejected in this trace. H2 refined to actualWindowsknownfolder lookup cost, notACL. src/codex/user-identity.ts explicitly gives this lookup30seconds; an end-to-end30secondapply test budget can expire before a validnear-budget lookup plus requiredCLIwork finishes. Producttimeout remains30seconds. +Proposed MODIFY tests/claude-integration/claude-desktop-remote-hub.test.ts ONLY: importexistingSPAWN_BUDGET_MS from ../helpers/test-budget; replace apply.exited30_000 withSPAWN_BUDGET_MS(45_000). Explain measuredlookupcost in comment. Preserveoveralltest240s, cleanup90s, networkguard and allmodel/profile/restartassertions. No productguard/ACL/identity changes. Beforepermanentedit, hostedcontrolledslowlookup: nativePowerShell command pads its realexecution to28s inside the unchangedproduct30sbound, only in diagnosticclient. Original30sbudget mustfaildeadline;45s candidate mustpassoriginalassertions. Then removeinjectedslowlookup and mutate productionapplysnapshotchosenalias20260211->validbutwrong20260911; the45s test mustfailonmodelassertion (notdeadline), satisfyingtests/helpers/test-budget.ts requiredablation. Followwith uninstrumentedexactheadall-laneCI andlifecycle. No retry-as-fix and no change to4096/4097or original25cases. + +Independent Hilbert review: VERDICT PASS. The30s wholeCLI deadline is narrower than the product's30s lookup plus measuredrequiredwork; existing45sSPAWN_BUDGET remainsbelow240stest. Acceptance remainsconditional on controlledvalid28slookup red/green and wrongalias modelassertion ablation. Diagnostic34036362222 at29fa76f07 isrunning these onhostedWindows with unchangedproductguards. No permanenttestbudgetedit yet. Primaryoperator explicitly requested directpoll; noheartbeat exists. + +## Hosted proof and permanent delta +34036362222 (29fa76f07): controllednativeknownfolderlookup28,226ms completedwithinproduct30s; oldwholeCLIdeadlinefailed30,004ms withchildstillalive. Canonical45s case exited0 at31,381ms andall22originalassertionspassed. Itsablationstep didnotexecute because diagnosticPythondefaultCP1252 couldnotreadKoreanUTF8source; notaproductfailure. +34036626846 (247651739): UTF8-correct standaloneablationran with45sbudget; validbutwrong20260911aliasmadeoriginalwritten.inferenceModels.toEqual(snapshot.models) fail in57,052ms testcase, withoutapplydeadlinefailure. This proves thelongerbudgetdoesnotmaskthemodelidentityregression. Permanentdelta nowonlyimportsSPAWN_BUDGET_MS andusesitforapply.child.exited; allproducttimeouts/guards/assertions unchanged. Fulluninstrumentedlatest-headCI/lifecyclewillrunagain beforeintegration. Diagnosticworkflows andfaultinjections remainoffPR. + +## Replan after remaining product refusal +C at9d624987c: Windows3/6 run34036848646 job101496387805 returnedclient_lifecycle_lock_failed in storedprofiletrue (72.4scase), whilefalsepassed102.8s. Thisisnot45sdeadline; nofurtherbudgetraise. Cdidnotpass, resettoPwithsameunfinishedunit. H1 identitylookuprefusal/timeout (knownfoldertrace22.8s); H2 ACL/filesystempreparation failure; H3 SQLite/namespace safetyrefusal. Classify error.cause codes usingfixedallowlist andidentity-timeout/namespacebooleans inthrowawaydiagnostic, plusnativeexitcode/timedOutflags. Threefreshsamplesmeasurefailurecondition ratherthanretryingforgreen; noobservedfailuremeansunresolved, notsuccess. Scriptsinstrument-lifecycle-cause.py/lifecycle-cause-ci.yml stay.tmp anddiagnosticrefonly, basedon921docsheadwhichpreserves9druntime. FutureFFIlookupoptionsare read-onlyresearch untilcauseconfirmed andsecurityreviewed. Priorinternalattemptcountisareviewcheckpoint, notuser-requestedterminationbudget; do not abandonthereleasegoalorclaimcompletion. + +Three-sample diagnostic34038264294 didnotreproduceclientrefusal (allpass; apply25.8-26.3s, nofailureflags). ItdoesnotclearfullCI. Nextboundedprobe keepsfullWindows3/6context alongsideisolatedcase, addingonlyfixedPowerShellphase timings aroundAdd-Type andSHGetKnownFolderPath. This separatescompiler/startupcost fromnativeAPIlatency beforeconsideringanyin-processFFIcall; a slowOSAPIwouldmake synchronousFFIanunsafeperformancefix. Exactscriptinstrument-known-folder-stages.py andworkflowknown-folder-stages-ci.yml arediagnosticonly. No budgetincrease orproductionlookupedit authorizedbyemptyflags. + +Newfixture hypothesis fromworkingcomparison: tests/codex-integration/codex-user-identity.test.ts givesrealchildprocessesownedTEMP/TMP andexistingLOCALAPPDATA within10schild/20stestbounds; Desktopfixtureallowlist omitsTEMP/TMP andpointsAPPDATA/LOCALAPPDATA atuncreateddirectories. Isolate missing/temp-only/profile-only/both withunchangedproductionlookupandallassertions. This canexplaincompilerlatencywithoutaproductrewrite; fixture-onlyrepairpreferredifmeasured. Diagnosticfixture-environment-ci.yml usesfourfixedmodes, freshWindowsjobs andprivateownedfolders. No nativeAPIcodechange. + +Fixtureenv experiment34039649747 rejectsTEMP/AppData-onlyrootcause: missing/temp/profile/both allretain22-27scompilationcost; no failureflags. Separatestageprobe establishesAdd-Type17.8s versusnativeAPI16ms, andfullshardclientcasespassed with16.7scompiler (hygiene failuresareexpectedbecauseitsdiagnosticCIyamlreplacesnormalworkflow). Thisdoesnotidentifytheoldgenericrefusal, butitproves a removablecompilerhotspot threatening the existing30schildbudget. + +Proposed experimentalruntime delta, notyetappliedtoproduct: replaceonlythefixedknown-folderAdd-Typebinding with public.NETFramework Reflection.Emit metadata in the same trustedPowerShellchild. Keep30stimeout, outputUTF16/base64, successfulcache, SIDquery, GUID, DEFAULT_PATH0x400/null-currenttoken, canonicalization andACLchecks. Noin-processFFI/unboundednativecall. DefinePInvokeMethod signatureGuid&,UInt32,IntPtr,IntPtr& ->Int32, Winapi,Unicode; PreserveSig required; outparameter metadata; FreeCoTaskMemfinallyevenfailure. DLLpathcomesfrom.NETSystemDirectory, notenv. Hostedprototype comparesunchangedlegacyC#referencepath, envshadowpath, negativeGUIDHRESULT, andfocusedidentity/Desktoptests beforepermanentpatch. Exactemit-known-folder.py andknown-folder-conformance.ts in.tmp. PrimaryMicrosoftDefinePInvokeMethod/AppDomain/PreserveSigdocs were opened; generatedpublicAPIexampleconfirms thismetadataapproach. Nativegenericfailure remainsunclassifieduntilfullgate; donotclaimitfixedfromprototypealone. + +Prototype security review (Locke): PASS forprototype-onlydispatch; originalreference savedbeforeedit, 9-argentrypoint/PreserveSig/finally-free,30sproductionbound,45slegacyoracleonly, no rawpathlogging. Run34040992290 atad982feb53 uses thisprobe andexistingfocusedtests. No productionlookupedit yet. + +Windows1 whole-job bound: cancelledrun completed2736passingtests versus2981in priorcompletedrun. Matchedtests took1244s vs996s (25%slower); remaining245tests took49.4s previously, about61.7s atobservedratio. Therewere no reportedtestfailuresbeforejobcancellation. Proposedci.yml platform-windows timeout25->30minutes gives finitebatch/cleanupmargin whilepreserving allsixshards, everytestcommand, per-testdeadlines and crash-onlyretry. Updateexistingci-workflows.test.ts budgetexpectation25->30. This isnot a code-failurewaiver and must stillcompletealltests; securityreviewmustconfirmno trigger/permission/runner/pin changes. + +Prototype34040992290 passedsamepath/shadow/HRESULTconformance (legacy3579ms, emitted402ms, shadow382ms, productionbound30000), and9focusedtests. Howeverminimal-environmentDesktopcasesstilltook82.7/76.9s. Thereforedo notlandtheproductionrewrite: itdoesnotremovefixture-pathlatency. Nextdiagnostic34041357794 isolatesPSModulePath, executablePATH/PATHEXT, andWindowsinfrastructureenv asfourstaticmodeswithoriginalproductionlookup. Theseareonlyrunner-localdiagnosticchanges; fullPATHinheritanceisnotauthorizedasapermanentfixturefixbecauseitwouldwidenexecutablevisibility. + +Confirmedfixturecause: keybisect34041973010 andsingle-variable34042207026 isolatePSModuleAnalysisCachePath. Inheritedpreparedcache: apply3731ms/AddType215ms; missing27309/22840ms, NUL23473/20008ms, emptyowned25964/22720ms. Thusfreshcacheanalysis—notjustC#binding—is thecost. Owned-copy34042446427 preservesoriginalcacheisolation andpassesoriginalscenario in12.9s withapply4632ms/AddType190ms. No productionlookuprewrite willland. +Permanentfixturechange: Windows-only ownedmodule-analysis-cache path seededbycopy from anabsolute, regular, non-symlinkparentcachewhenavailable; no blanketenv/PATHinheritance. Missing/staleseedorchangedsizefallsbacktocoldownedcache; otherIOfailuresremainfailureswithoutloggingprivatepaths. Alloriginalmodel/credential/restartassertionsandguardsremain. KeplerindependentreviewPASS forownedseedingandracehandling; Windows30minwholejobbound separatelyPASS. Fulllatest-headCIstillrequired. Allinterpreter/FFIexperimentsremainonlyondiagnosticrefs. diff --git a/devlog/_plan/260906_release_244_publish/030_dev_bump.md b/devlog/_plan/260906_release_244_publish/030_dev_bump.md new file mode 100644 index 0000000000..275e8961c7 --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/030_dev_bump.md @@ -0,0 +1,5 @@ +# Development version pre-move +Dependencies: integrated frozen RC. MODIFY package.json only in a PR based on currentdev: version2.44.0 ->2.45.0, or NOOP only when freshdev already strictly outranks intendedstable2.44.0. No runtime/code/lock dependency change. Keep frozenRC at2.44.0. +Inspect the defaultmain version of dev-version-bump.yml. If workflow_dispatch exists, dispatch frommain with intended-version=2.44.0 mode=pre-move and use generatedPR. If older main has onlyworkflow_call, create the established one-file versionPR via scripts/bump-dev-version.ts (--help/CLI inspected first) or exact JSON version rewrite. No local build/tests. Push scopedbranch --no-verify; require hosted exacthead CI and reviewed version-onlydiff, then authorizedadminPR merge todev. Verify origin/dev:package.json and ancestry fresh. Releaseworkflow assert-ahead independently enforces pre-move. A version collision is a blocker, not an automatic unreviewed versionchoice. Capture sourceSHA/version/PR/run proof in this unit. + +Current defaultmain workflow_dispatch was fetched and confirmed (exit0) on2026-09-06; the manual workflow path is selected. diff --git a/devlog/_plan/260906_release_244_publish/040_preview.md b/devlog/_plan/260906_release_244_publish/040_preview.md new file mode 100644 index 0000000000..1f45fb3ee5 --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/040_preview.md @@ -0,0 +1,4 @@ +# Preview promotion and publication +Dependencies: frozenRC plus verified dev-ahead. NEW ordinary promotionbranch from freshpreview; MERGE immutableRC with normalmerge (no rebase/force); resolve only reviewed branch/version conflicts. MODIFY package.json fromRC2.44.0 to2.44.0-preview.YYYYMMDD (execution-day date in the maintainer timezone (Asia/Seoul), choose next suffix only if existing version requires it after explicit freshregistry inspection). All other source tree entries must equalRC; assert `git diff --exit-code RC HEAD -- . ':(exclude)package.json'` after resolving history. No2.45.0dev pre-move in releasecandidate. +Open template-complete preview promotionPR; include actualprior dashboard screenshot and exact source evidence because release delta includes dashboard changes. Target exemption is release promotion, not a featurePR. Inspect required checks and maintainer objections; user explicitly authorizes this promotion. Merge using allowedPR method/admin as authorized with exacthead guard; neverdirectpreviewpush. Fetch finalpreviewSHA, proveRCancestor and packageversion. +For finalpreviewSHA require successful ci.yml eventpush and service-lifecycle.yml sameSHA (manual lifecycle if auto absent); Windows6 and macOScontrol are already required on the unchangedRC; do not claim push'sskippedWindows tested. Never dispatch manualCI on preview/main while the requiredpushrun is active (sharedref concurrency cancels it). Runtime drift returns toRCvalidation beforepromotion. RC all-lane evidence is separate from final push gate; source-only-equivalent versiondelta is documented. Dry-run release.yml frompreview with version, tag=preview, expected-sha full40 and dry-run=true. Waitsuccess, inspect dry-run buildpack, then sameSHA dry-run=false. Neveroverlap publish workflows; existing release concurrencyserializes. Read back npm exactversion metadata+dist-tag+gitHead+dist.integrity+attestations, verify tarball digest andprovenance against releaseSHA; verify vversiontag and prerelease. No localinstall/suite/build. If publish response ambiguous inspectregistry/tagfirst, never duplicatepublish. Oldpublishedversion remainsrollback installtarget; changing dist-tag/rollback is onlydone if actuallyneeded andauthorized, not as a test. diff --git a/devlog/_plan/260906_release_244_publish/050_stable.md b/devlog/_plan/260906_release_244_publish/050_stable.md new file mode 100644 index 0000000000..2d1e2fc68e --- /dev/null +++ b/devlog/_plan/260906_release_244_publish/050_stable.md @@ -0,0 +1,4 @@ +# Stable promotion, publication, and closure +Dependencies: previewpublished proof, immutableRC and devahead. NEW ordinary main promotionbranch from freshmain; merge the SAME RC independently, preservingmainhistory. Finaltree equalsRC and package2.44.0; do not merge previewversion ordev2.45.0. Resolve explicit conflicts and verify RC ancestry and tree identity. PromotionPR uses full template, release exemption, previous dashboard screenshot/evidence and author attribution preserved by originalhistory. Honor outstandingmaintainer objections and exacthead checks; allowedadminmerge remains explicitowner action, not independentapproval. +After mainPRmerge fetchfinalmainSHA and require sameSHA successful push-event CI plus a successful `service-lifecycle.yml` run for the same finalmainSHA, covering all three jobs. Run hosted release.yml dry-run=true then false frommain, version=2.44.0 tag=latest `expected-sha=`; serialize afterpreviewpublication. Verify npm latest/version/gitHead/integrity/provenance, tagv2.44.0 and GitHub release target. Read deploy-docs.yml triggers and default branchsource; wait successful Pages deployment for finalmainSHA or dispatch the existingworkflow if required bypathfilters, then inspect site response/sourceproof. If workflowredafter acceptednpm publish, inspectactualregistry/tag/release state before recovery; create only missingmetadata using existing validatedchangelog, never republishsameversion. +Finalcheck rereads dev/main/preview refs and versions, verifiesboth tags/artifactdigests and publishedinstall-smoke evidence from releaseCI. Record GUI/runtime field limitations accurately (Kiroquota absent, Windowsquota fieldissue3644 not newlyvalidated). Update boundgoalplan/ledger and verify no heartbeat automation remains and complete directpoll tracking after allcriteria met. Keep cleanup scoped: no worktreedeletion, no massbranchcleanup, no localdaemon/installmutation. D reports both publishedversions and proofURLs. Any incompletegate remainsopen. diff --git a/devlog/_plan/260906_stateful_task_guidance/000_plan.md b/devlog/_plan/260906_stateful_task_guidance/000_plan.md new file mode 100644 index 0000000000..b7fd48e7b9 --- /dev/null +++ b/devlog/_plan/260906_stateful_task_guidance/000_plan.md @@ -0,0 +1,30 @@ +# Stateful external-task guidance consistency + +Parent PR #3743 recognizes a complete external task-input envelope as a user turn +and starts the parsed continuation boundary there. Its review identified the +remaining raw insertion predicate in collaboration.ts, which still recognizes +only ordinary user/assistant messages and agent_message. In a stateful delta, +generated guidance can therefore precede the task in parsed messages but follow +it in the stored raw input; reparsing changes the delivered order. + +This C4 protocol/replay follow-up is a separate PABCD work-phase before Kiro +implementation resumes. The Kiro phase remains open with no code changes; the +goalplan gained an additional criterion and an explicit focus cursor, without +marking any unfinished task complete or weakening existing criteria. + +Archetype: spec-satisfaction repair. Goal: the same conversational boundary in +parsed and raw stateful representations. Non-goals: new envelope forms, broader +tool-output repair, stateless insertion changes, auth changes or live Kiro. +Verifier: hosted ci.yml runtime/type/privacy gates and focused regression cases +in tests/codex-integration/multi-agent-compat.test.ts. No local test suite, +typecheck or build. Stop only after exact-head CI and independent review pass, +parent review is resolved and its verified head is ready for the Kiro cascade. + +Resources inherit the authorized release loop: existing repository/GitHub access, +requested xai/grok-4.6 reviewers, no new credentials or purchases, no fixed model +cost cap, bounded processes and status waits. Main owns code/FSM/GitHub actions; +reviewers are read-only. Reclaim failed dispatches; no implicit phase movement. +Design and final source/CI evidence reside in this unit and the bound goalplan. + +The complete implementation map is 010_raw_boundary.md. Apply the verified delta +to parent #3743, then refresh the saved Kiro branch from that parent before B. diff --git a/devlog/_plan/260906_stateful_task_guidance/010_raw_boundary.md b/devlog/_plan/260906_stateful_task_guidance/010_raw_boundary.md new file mode 100644 index 0000000000..ef68f482f2 --- /dev/null +++ b/devlog/_plan/260906_stateful_task_guidance/010_raw_boundary.md @@ -0,0 +1,41 @@ +# Align the stateful raw conversation boundary + +## Exact diff map + +- MODIFY src/server/responses/collaboration.ts: import the existing pure + externalTaskInputContent helper. In isConversationalItem, recognize a complete + external task envelope with helper(item) !== undefined, alongside existing + agent_message and user/assistant message handling. Do not duplicate its shape + validator or alter statefulRawInsertionIndex's replay-prefix/fallback logic. +- MODIFY tests/codex-integration/multi-agent-compat.test.ts near injectDeveloperMessage: + stateful external envelope alone and after a leading ordinary call result must + receive guidance before the external task in both parsed context and raw input. + Reparse the stored raw body and compare role/content order. Add an expanded + replay-prefix case so historical external inputs are not selected as the new + boundary. Keep ordinary stateful protocol, compaction and guidance-dedup tests. +- MODIFY docs-site/src/content/docs/guides/sub-agent-surface.md and + structure/04_transports-and-sidecars.md: distinguish unchanged payload content + from intentional generated-guidance placement; both representations use the + same complete-envelope boundary during stateful injection. + +Before: parsed [developer, user] while raw [external-envelope, developer]. +After: parsed [developer, user], raw [developer, external-envelope], and reparsed +role/content order agrees. Leading protocol results remain before guidance; +historical replay-prefix items remain in place. + +## Activation and boundary proof + +The new predicate executes only when stateful guidance inspects raw input. Tests +set previous_response_id, invoke the real injector and assert raw/parsed/reparsed +arrays. Ordinary tool outputs with call_id remain protocol items because the +shared helper rejects them. Invalid/partial/opaque envelopes retain their current +classification; the complete validator is already covered by parent regressions. + +No persisted schema, configuration or role changes. Existing input shape -> shared +validation -> raw insertion index -> stored raw input -> later parser is the full +data flow. The helper remains pure and adds no optional subsystem dependency. +Review uses the actual diff; all runtime checks execute in GitHub Actions. + +## An audit amendment + +Use the parse-time previous_response_id pattern from multi-agent-compat.test.ts:1075-1089 for envelope-alone and leading-result cases. The raw body must contain that field before parseRequest and retain it during reparse; do not copy the post-hoc parsed.previousResponseId assignment fixture at 1029. For historical-prefix coverage use the 1043-1072 pattern with explicit `_replayPrefixLen` and `_continuationConversationMessageIndex`, and put an old external envelope inside that preserved prefix. Assert parsed boundary before injection as well as raw/parsed/reparsed ordering. This closes the auditor's false-green fixture concern. diff --git a/devlog/_plan/260906_stateful_task_guidance/011_implementation.md b/devlog/_plan/260906_stateful_task_guidance/011_implementation.md new file mode 100644 index 0000000000..1bd823fba8 --- /dev/null +++ b/devlog/_plan/260906_stateful_task_guidance/011_implementation.md @@ -0,0 +1,16 @@ +# Implementation and verification boundary + +The raw conversational-item predicate now reuses externalTaskInputContent, matching +the parsed continuation predicate without another envelope validator. Replay-prefix +skipping and the existing fallback remain unchanged. + +Three new cases parse with previous_response_id already in the raw body, exercise +external input alone or after a real protocol result, preserve a historical external +envelope in the replay prefix, and compare raw/parsed/reparsed role-content order. +They retain the stateful field during reparse and assert the initial parsed boundary, +avoiding a fixture that could accidentally validate stateless behavior. + +Apply this review fix to #3743. Source review and exact-head hosted CI are recorded +on that PR and in the cycle receipt; no local test suite or live Kiro request is run. +After verification, resolve the review and refresh the preserved Kiro branch before +its implementation cycle continues. diff --git a/devlog/_plan/260906_unix_shim_ci_restore/000_plan.md b/devlog/_plan/260906_unix_shim_ci_restore/000_plan.md new file mode 100644 index 0000000000..007d0f9f2f --- /dev/null +++ b/devlog/_plan/260906_unix_shim_ci_restore/000_plan.md @@ -0,0 +1,23 @@ +# 000 — Unix shim CI follow-up roadmap + +This is the next independent cycle of the active dev-CI repair goal. The prior key-login cycle is complete: PR3724 landed at41ab5c2dc, its exact-head PR CI passed and the actual dev macOS1 key-login test passed116.67ms. The dev aggregate still failed in a different test; it is not labeled green. + +Public baseline: dev41ab5c2dcd49ac6bdfaec4cf091324dbc1d41b95, CI34001170755, macOS2 job101400209887. `tests/codex-integration/codex-shim.test.ts:109` expected the fixture install result's installed flag to be true, received false after6004.17ms. The named test at1444 concerns obsolete-shim auto-restore, but setup failed before those assertions. The shard reported10416pass/1fail. Raw evidence is kept under ignored .tmp/lane-b/key-login-repair-qa/dev-macos-2.log. + +## Phase map and next decision + +The earlier cycle established a repeatable DNS-dependent timeout and repaired only its fixture. That finding does not explain this shim failure. Consume `010_repair.md` in the shim-repair cycle; no production edit before a bounded causal trace and independent audit. This P amendment pays the newly discovered multi-cycle roadmap debt before further implementation. + +Current known call chain: withInstalledShim creates an owned temporary PATH/home and an executable echo launcher; installCodexShim performs the real transactional install and launcher probe. The test sets the successful-probe observation interval to20ms. The production probe runs a Bun child with a5s launcher budget and1s cleanup budget; the child launches a detached process group with stderr and descendant-lease pipes. The historical boolean assertion discards the install result message. Its6s duration alone cannot tell which child/cleanup boundary refused the install. + +Existing ownership/context: prior changes51057b611 and2ea9ba7df preserved bounded failure diagnostics and cleanup fail-closed behavior. D does not change shim.ts or this test, has a previous serialized81-test pass, and currently owns a macmini-cf full-suite queue slot. B must respect that slot and not induce competing test load. + +## Bounds + +Treat any executed launcher-validation or cleanup change as C4. Allow read-only GH logs/history and owned remote macOS probes with pinned Bun1.4.0, temporary PATH/home and the shared test-user lock. Write only the owning test, the demonstrated faulty probe boundary if required, and numbered outcome/contract documentation. No global launcher install, service restart, personal account or credential access, release/deploy, integration-branch direct push, or local test/typecheck/build. Existing no-verify/admin and inherited-agent authorization applies. No requested token/cost cap; six-hour checkpoint is a reporting bound. Investigative security details stay in ignored scratch until the fix is public. + +## Baseline discriminator + +The first isolated named-case trace passed. Both the fresh install and obsolete refresh ran the unmodified embedded probe with20ms observation,5s launcher and6s parent ceilings; their child results were status0 with expected group/stderr metadata, in169ms and33ms respectively. This does not resolve the intermittent CI failure. Next bounded probe is the owning file/fixture neighborhood or sequential positive repetitions with every failure retained, to distinguish test-state leakage from starter/process/stream timing. No artificial bootstrap stall will be presented as proof of this historical cause. + +Prior owner A confirms no reproduced6s positive-fixture failure: its51057b611 change only concerns the existing passive cleanup interval after EPERM; the old deliberately negative timeout case and current unexpected setup refusal must remain separate. diff --git a/devlog/_plan/260906_unix_shim_ci_restore/010_repair.md b/devlog/_plan/260906_unix_shim_ci_restore/010_repair.md new file mode 100644 index 0000000000..7427b3e49e --- /dev/null +++ b/devlog/_plan/260906_unix_shim_ci_restore/010_repair.md @@ -0,0 +1,36 @@ +# 010 — Isolate and repair the shim setup failure + +Status: investigation plan; no fix selected or applied. + +## Planned edits and gates + +1. Remote scratch only: preserve exact dev test/source bytes; run the named case with the existing15/other declared test budget and real installer in isolated fixtures. Capture the full bounded install result before its installed assertion, and probe status/signal/error code plus marker/group/stderr-file shape. Capture times at child start, launcher start/exit, pipe-end, probe finish and parent return only if the first result needs them. No credential-bearing or arbitrary launcher output in public diagnostics. +2. Distinguish initial Bun-process startup, launcher/descendant validation, stream completion, and parent cleanup. A fast isolated pass is not proof of resolution; only a controlled reproduction/falsifier selects a correction. Keep stale-file/PID hypotheses separate from actual process identity. +3. Test change owner: `tests/codex-integration/codex-shim.test.ts`, specifically withInstalledShim at89-125 and the obsolete-upgrade case1444 onward. Preserve old-backup/state byte equality, successful revalidation, all existing negative cases and declared budgets. Add bounded install-result diagnostics so a future refusal exposes its category instead of only false. Do not replace real installer success with a stub or add retries/skips. +4. Conditional production owner: `src/codex/shim.ts`, embedded install-probe script44-205, probeUnixShimInstall859-943 and cleanup helper960-984. Amend this document with the observed faulty branch and exact smallest correction before B. No broader rewrite or timer increase. Preserve fail-closed recursion, descendant/group termination, immutable file identity and transaction rollback behavior. Reuse existing seams; do not invent production test modes. +5. Independent reviewer audits the selected delta and counterexamples before implementation. Remote verification includes the named case, complete owning shim file, relevant explicit negative/cross-shell fixtures, strict targeted test typecheck when test types change, root typecheck/privacy for changed inputs and owned-process cleanup. Use a source mutation/fault to demonstrate the regression can fail for the intended reason. +6. Push the reviewed correction with --no-verify, use exact-head hosted CI, admin merge through a PR, prove actual dev ancestry and follow the resulting dev CI. Do not claim the earlier key-login PR's CI certifies this change. A distinct later failure becomes another evidence-backed phase rather than being hidden as success. + +DONE requires causal evidence, unchanged safety predicates or independently reviewed corrections, passing relevant checks and real delivery. BLOCKED needs an external condition with no remaining authorized progress; queue waiting alone is not completion. Current missing information is the actual failure category from the discarded install result and probe boundary trace. + +## First B step: preserve the failure category in hosted evidence + +The original named case and ten bounded sequential copies passed (20probes,32-176ms). Owning-file verification is being collected. Current dev advanced to014061a7e through D3720; shim.ts and its owning test are unchanged. No source/runtime fix is selected. + +Apply only this diagnostic delta to withInstalledShim: capture `const installed = installCodexShim();` and assert `expect(installed.installed, installed.message).toBe(true)`. The repository already uses Bun's custom assertion-message argument in tests/cli/cli-registry.test.ts59-60. This preserves the exact predicate and adds the installer's existing bounded refusal category to a future hosted failure. Validate the targeted test type and unchanged owning tests remotely, then publish a draft diagnostic PR for the current dev tree. Do not merge or describe this as the root-cause fix; the next diagnostic result governs any production change. No retries, loops, budget changes or assertions removed from the committed test. + +## Candidate implementation amendment: CI isolation policy alignment + +A second independent observation strengthens the execution-topology hypothesis: D's PR CI34001613444 failed Cursor shell completion acknowledgement after60s, while its same-source remote full suite passed that file in its explicit serial lane. Both Cursor shell and Codex shim belong to `SERIAL_FULL_SUITE_FILES` in scripts/test.ts. The raw macOS shard command bypasses this existing policy. Historical native wait causes remain unknown; this proposal fixes the observable CI execution-policy gap, not a claimed Bun internals defect. + +Precise delta for audit: + +- `.github/workflows/ci.yml`, platform-macos Test step only: load the six canonical paths from the existing `SERIAL_FULL_SUITE_FILES` export; fail if the manifest cannot be loaded/is empty/names absent files. Build quoted basename ignore arguments for the general shard. Refactor the existing two-attempt crash-only loop into a shell function that accepts test arguments; preserve the same crash signatures, per-test60s ceiling and exact failure code. Run the general shard with all six exclusions, then assign serial file index modulo2 to a single owning shard and run each assigned file with `--parallel=1` in its own Bun process. Any assertion failure returns immediately, and any repeated crash fails. No new job, runner, action, trigger, permissions, dependency or security credential. The explicit unsharded macos-control remains unchanged. +- `tests/ci-workflows/ci-workflows.test.ts`: update the existing platform-macos command-owner checks to the argument-taking retry function and manifest-driven invocation. Preserve matrix[1,2],20min job bound, shared change gate, crash fingerprint and no-unbounded-retry checks. Do not weaken the whole-pool control assertions. +- New `tests/ci-workflows/macos-serial-lanes.test.ts`: execute the actual platform-macos run block in a sandbox with a fake Bun command that records invocation argv/PID and returns controlled outcomes. Cover both shards; every fixture serial file executes exactly once across the pair, all are excluded from the general command, fresh process IDs and--parallel=1 are observed. Verify main/serial assertion failures are not retried, runtime crashes retry once only, repeated crashes fail, and manifest errors cannot silently remove coverage. A regression run against the old workflow must fail its execution-ownership oracle. No actual repository suite inside this harness. +- Register that test in `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json` using existing layout contracts. +- `tests/codex-integration/codex-shim.test.ts`: keep the two-line diagnostic improvement described above, with the original positive expectation and installer unchanged. + +All source process-validation, recursion, descendant, rollback and5s/1s/6s probe contracts remain untouched. Verification includes remote harness RED(oldworkflow)/GREEN(newworkflow), owning81shim tests and Cursor shell owning tests under the new serial dispatch, existing workflow/test-runner/layout guards and relevant typechecks/privacy, independent workflow security review, full hosted exact-head CI and actual dev integration. Root native failure diagnosis remains explicitly limited; CI isolation is assessed on its own observable policy contract and actual execution, not a lucky repeated green. + +Implementation detail accepted for exclusion integrity: retain the repository's established `**/` Bun ignore syntax, but require each manifest basename to match exactly its one declared file under tests before use (`find tests -type f -name ` equals that canonical path). Fail duplicate manifest entries, invalid relative paths, missing files, basename collisions, an empty manifest or nonzero producer status. The fake-Bun harness includes collision and duplicate/empty/failed-manifest cases. Use standard find/Bash3 constructs only; no new dependency or assumption about an unverified full-path glob grammar. diff --git a/docs-site/public/pr-screenshots/3659-provider-model-removal.png b/docs-site/public/pr-screenshots/3659-provider-model-removal.png new file mode 100644 index 0000000000..1c5cb0573a Binary files /dev/null and b/docs-site/public/pr-screenshots/3659-provider-model-removal.png differ diff --git a/docs-site/public/screenshots/aside-profiles.jpg b/docs-site/public/screenshots/aside-profiles.jpg new file mode 100644 index 0000000000..7e78846321 Binary files /dev/null and b/docs-site/public/screenshots/aside-profiles.jpg differ diff --git a/docs-site/public/screenshots/logs-filters-desktop-en.png b/docs-site/public/screenshots/logs-filters-desktop-en.png new file mode 100644 index 0000000000..004d94d2e1 Binary files /dev/null and b/docs-site/public/screenshots/logs-filters-desktop-en.png differ diff --git a/docs-site/public/screenshots/logs-filters-mobile-ko.png b/docs-site/public/screenshots/logs-filters-mobile-ko.png new file mode 100644 index 0000000000..ba72049f56 Binary files /dev/null and b/docs-site/public/screenshots/logs-filters-mobile-ko.png differ diff --git a/docs-site/public/screenshots/logs-filters-proxy-clock.png b/docs-site/public/screenshots/logs-filters-proxy-clock.png new file mode 100644 index 0000000000..a82351915b Binary files /dev/null and b/docs-site/public/screenshots/logs-filters-proxy-clock.png differ diff --git a/docs-site/public/screenshots/manual-openai-model-toggle.png b/docs-site/public/screenshots/manual-openai-model-toggle.png new file mode 100644 index 0000000000..d8a0dab0de Binary files /dev/null and b/docs-site/public/screenshots/manual-openai-model-toggle.png differ diff --git a/docs-site/public/screenshots/models-client-refresh-warning.jpg b/docs-site/public/screenshots/models-client-refresh-warning.jpg new file mode 100644 index 0000000000..2b5a79a8b1 Binary files /dev/null and b/docs-site/public/screenshots/models-client-refresh-warning.jpg differ diff --git a/docs-site/public/screenshots/openai-context-cap-off.png b/docs-site/public/screenshots/openai-context-cap-off.png new file mode 100644 index 0000000000..092f7377f9 Binary files /dev/null and b/docs-site/public/screenshots/openai-context-cap-off.png differ diff --git a/docs-site/public/screenshots/openai-context-cap-on.png b/docs-site/public/screenshots/openai-context-cap-on.png new file mode 100644 index 0000000000..204cbd24a7 Binary files /dev/null and b/docs-site/public/screenshots/openai-context-cap-on.png differ diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md index bff8bc2753..19f694d3eb 100644 --- a/docs-site/src/content/docs/contributing.md +++ b/docs-site/src/content/docs/contributing.md @@ -147,6 +147,12 @@ The current maintainers, their responsibilities, and the review and merge policy [`MAINTAINERS.md`](https://github.com/lidge-jun/opencodex/blob/main/MAINTAINERS.md). GitHub review ownership for the repository and security-sensitive paths is declared in `.github/CODEOWNERS`. +Contributor pull requests normally need a maintainer's approval. A current maintainer with +GitHub `maintain` or `admin` access may explicitly integrate a PR into `dev`, including their +own, without a second maintainer approval. The decision and exact-head verification must be +recorded; CI, security review and outstanding maintainer objections still apply. This exception +does not change `main`/`preview` review rules or allow direct pushes, force-pushes or deletion. + ## Conventions - **ES Modules only** (`import`/`export`), TypeScript, `strict` mode. Keep `bun x tsc --noEmit` clean. 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 9899797ea6..5c13e041b6 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -134,9 +134,12 @@ est temporairement indisponible, la première route disponible de la famille est Vous pouvez également gérer le même profil depuis la ligne de commande : +Les instructions de modification ci-dessous concernent le profil local. L'application via un hub connecté est décrite séparément plus bas. + ```bash ocx claude desktop [apply] ocx claude desktop show [--json] +ocx claude desktop status [--json] ocx claude desktop move [--default] ocx claude desktop default ocx claude desktop export @@ -204,6 +207,67 @@ l'en-tête d'admission dédié du proxy est valide. Par conséquent, l'avertisse Désactivez ce comportement avec `claudeCode.nativePassthrough: false` ; définissez une autre destination avec `claudeCode.anthropicBaseUrl`. +## Claude Desktop connecté à un hub distant + +Sur une machine connectée, `ocx claude desktop apply` ou `ocx claude desktop` récupère +l'instantané Desktop du hub et écrit son origine ainsi que ses identifiants exacts dans la +configuration Desktop locale, sans créer d'alias locaux. Les modes static/hybrid copient les +entrées ; discovery-only utilise l'origine du hub sans intégrer la liste. + +Le hub gère le profil, les familles et les valeurs par défaut. Modifiez-les sur le hub, puis +réappliquez côté client et sélectionnez à nouveau le modèle dans Desktop. Les anciens alias +créés uniquement sur le client nécessitent aussi cette opération. `show`, les modifications +locales et import/export restent locaux. En connexion distante, +`ocx claude desktop import --apply` est refusé avant l'enregistrement ; sans `--apply`, +l'importation reste locale. + +La lecture utilise l'identifiant d'accès aux données de la connexion existante, sans jeton +administrateur ni envoi de profil. Un ancien hub incompatible, une réponse invalide ou une liste +Desktop vide fait échouer l'application, sans catalogue local ni adresse de bouclage de secours. +Mettez à jour ou configurez le hub, puis réappliquez. + +Ce changement d'alias ne résout pas la demande distincte de [#3719](https://github.com/lidge-jun/opencodex/issues/3719) concernant la relecture de +`thinking` / `redacted_thinking` et le cache de prompts. L'accès au proxy seul n'active pas le +passthrough Anthropic natif ; les routes Anthropic traduites peuvent néanmoins utiliser le cache. +La fidélité de relecture et la comparaison des accès au cache restent à traiter séparément. + +### Rotation des clés, récupération et déconnexion + +La rotation et la récupération mettent à jour la clé du profil Desktop géré par la connexion +avec celle de la connexion locale, sans réapplication manuelle pour migrer la clé. Les ID de +modèles, familles, valeurs par défaut et la sélection courante sont conservés ; la rotation ne +resélectionne pas le profil géré et ne réactive pas une intégration désactivée. Dans le JSON CLI, +`rotation: "committed"` signifie que la nouvelle clé est active ; `rotation: "rolled_back"` signifie +que l'ancienne a été conservée ou restaurée, sans prétendre qu'elle a été révoquée. Une récupération +incertaine ou incomplète n'est pas annoncée comme une rotation réussie. + +La première application connectée conserve les paramètres gérés et la sélection antérieurs pour +les restaurer. Réapplication et rotation ne remplacent pas cette référence initiale. +`ocx disconnect` restaure les paramètres appartenant à la connexion en préservant les champs +ajoutés par l'utilisateur et les autres profils. La sélection antérieure n'est restaurée que si +le profil géré reste sélectionné ; un autre profil valide choisi depuis reste sélectionné. +Un profil créé puis enrichi par l'utilisateur est conservé en mode standard lisible. +`--keep-catalog` conserve le catalogue, pas la clé Desktop de la connexion. + +Un ancien profil géré sans historique peut être migré s'il appartient sans ambiguïté au hub +courant et à une clé de connexion reconnue. Apply, rotation/récupération ou déconnexion directe +le prennent en charge sans nouveau drapeau ni réapplication préalable. Un avertissement précise +que la déconnexion utilisera le mode standard faute de paramètres antérieurs enregistrés. +Seuls les paramètres de passerelle appartenant à la connexion sont retirés ; les champs utilisateur +et une sélection distincte valide restent intacts. Ce résultat est un repli standard, pas une +restauration de l'original. + +Les conflits de paramètres gérés, identifiants inconnus ou données de restauration endommagées +sont conservés et signalés. Un nettoyage interrompu reprend uniquement pour la même connexion, +sans effacer une nouvelle connexion ni annoncer une restauration incomplète comme terminée. +Terminez la récupération de rotation avant la déconnexion et gardez le même choix de conservation +du catalogue lors d'une nouvelle tentative. + +Quittez complètement puis rouvrez Claude Desktop après application, rotation/récupération ou +restauration : le processus en cours peut garder l'ancienne clé. Aucun redémarrage automatique +n'est effectué. La déconnexion locale ne révoque pas automatiquement la clé du hub et n'efface +pas les copies externes ; révoquez-la séparément sur le hub si nécessaire. + ## Le sélecteur /model (« Depuis la passerelle ») Claude Code 2.1.129+ découvre les modèles de passerelle via `GET /v1/models?limit=1000` et les répertorie dans @@ -248,6 +312,16 @@ utilisent l'alias haché. Les identifiants de modèle peuvent contenir `--` (la **Ordre de résolution du modèle :** retrait du marqueur `[1m]` → décodage de l'alias lisible → décodage de l'alias haché de Claude Desktop → correspondance exacte dans `modelMap` → correspondance sans date (suffixe `-20250514` retiré) → transfert direct. + + +Un ID Desktop de forme datée non résolu peut aussi être un véritable modèle natif absent de +la découverte. Messages et count-tokens renvoient HTTP 503 avec l’erreur fixe `desktop_model_mapping_unavailable` lorsque les informations disponibles ne permettent pas de résoudre cet ID ; cela ne +prouve pas que le modèle est invalide. Les anciens alias de type hash inconnus restent rejetés +avec HTTP 400. Aucun des deux cas ne retire la date ni ne choisit une autre route. Les ID connus, +les correspondances enregistrées et les entrées exactes de `modelMap`, dont les véritables ID +natifs reconnus, conservent leur traitement. Actualisez la découverte ou réappliquez le profil du +hub connecté avant de réessayer ; une simple nouvelle tentative ne garantit pas la résolution. + Chaque entrée porte un nom d'affichage tel que `gemini-3-pro (gemini)`, ainsi que toutes les fonctionnalités du modèle (échelle d'effort de raisonnement et types de réflexion) dans la structure officielle `ModelInfo`. Les véritables modèles Anthropic conservent leurs identifiants canoniques sur les deux interfaces. @@ -345,6 +419,8 @@ l'élision). Le contenu de remplacement préserve l'association entre l'appel d' Ordre de recherche : alias de découverte → identifiant exact → identifiant sans le suffixe de date (`-20250514`) → transfert direct. +Voir la [résolution des alias Desktop](#desktop-alias-resolution) pour les règles de rejet. + ## Matrice des services auxiliaires : recherche web et compréhension des images Les modèles routés ne disposent pas tous des mêmes outils hébergés ou de la même prise en charge des images. opencodex comble ces lacunes diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index 091a63a787..02ffa8a211 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -311,8 +311,15 @@ S'il manque un modèle dans Codex, ou si l'ordre ou la visibilité du catalogue d'autorisation n'atteint jamais le catalogue. 2. **`disabledModels`** au niveau supérieur — masque les modèles dans le catalogue comme dans `/v1/models`, et fait passer les identifiants GPT natifs non qualifiés à `visibility: "hide"`. -3. **`liveModels: false` avec `models` vide** — lorsque la découverte en direct est désactivée et que `models` - est vide ou absent, opencodex n'expose aucun modèle routé pour ce fournisseur. +3. **`liveModels: false`** — Avec `liveModels: false`, si `models` est vide ou absent, la liste initiale commence par le + `defaultModel` configuré, puis les identifiants de `retainModels`. Les doublons sont supprimés + en conservant leur première occurrence. Une liste `models` explicite non vide est au contraire + suivie de `retainModels`, sans ajout implicite d’un autre `defaultModel`. Ce dernier peut toujours + être inscrit explicitement dans `models` ou `retainModels`. Si aucun de ces champs ne fournit + d’identifiant, la liste initiale est vide. Cet ordre ne garantit pas l’ordre final du sélecteur. + `selectedModels`, `disabledModels` et la désactivation du fournisseur restent applicables. + `authMode: "forward"` conserve sa branche distincte et n’utilise pas cette liste statique routée. + Ces règles ne changent pas le repli en cas d’échec de la découverte en direct. 4. **Cursor `GetUsableModels`** — l'adaptateur Cursor découvre les modèles par son appel RPC protobuf `GetUsableModels`, et non par `/models` ; une modification côté Cursor peut donc changer les identifiants visibles indépendamment des autres fournisseurs. diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index d54d941a2f..c65531a4d3 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -114,6 +114,11 @@ l'application s'arrête et le signale au lieu d'écrire une valeur modifiée en réussi. Le fichier concerné est indiqué et rien n'est déplacé sur le disque. Vous pouvez toujours modifier ce fichier manuellement ; seule la réécriture automatique est refusée. +Les dates et heures TOML empêchent également la réécriture automatique : la fusion +les convertirait en chaînes entre guillemets, y compris dans les tableaux et les +tables en ligne. Les dates déjà écrites entre guillemets restent prises en charge. +Pour conserver une date typée sans guillemets, modifiez manuellement la configuration. + **Pi, Kimi Code, Gajae Code, MiniMax Code et l'intégration DSH gérée fonctionnent uniquement avec une adresse de bouclage.** Les quatre premiers n'ont aucun champ de configuration pour l'en-tête `x-opencodex-api-key` qu'exige une liaison hors bouclage. DSH possède une table d'en-têtes générique, mais rc.6 ne documente pas diff --git a/docs-site/src/content/docs/fr/guides/model-ordering.md b/docs-site/src/content/docs/fr/guides/model-ordering.md index cac2b0667c..ada196c8fc 100644 --- a/docs-site/src/content/docs/fr/guides/model-ordering.md +++ b/docs-site/src/content/docs/fr/guides/model-ordering.md @@ -23,7 +23,7 @@ priorités `i * N + j`, où `j` est la position du sélecteur en base zéro ; un sont déplacées hors de ces groupes de sélecteurs. Codex continue de n’annoncer que les cinq premières lignes visibles dans le sélecteur. -Les priorités sans sélecteur pertinentes sont : +Sans ordre global du sélecteur, les priorités sans sélecteur pertinentes sont : | Entrée du catalogue | Priorité | Source | | --- | --- : | --- | @@ -134,11 +134,47 @@ au-delà de ce bloc mis en avant : Les lignes routées indiquées apparaissent dans l’ordre configuré. Une ligne absente du tableau conserve sa priorité normale et reste donc devant la bande d’affichage de `modelPickerOrder` ; indiquez toutes les lignes routées dont vous souhaitez contrôler l’ordre relatif. Une ligne également présente dans -`subagentModels` conserve sa priorité de mise en avant. `modelPickerOrder` ne réorganise ni les lignes -natives non qualifiées ni celles qualifiées par un compte ; utilisez `subagentModels` pour celles-ci. +`subagentModels` conserve sa priorité de mise en avant. Une liste contenant uniquement des identifiants +routés conserve la position normale des lignes natives. -`modelPickerOrder` ne modifie jamais l’ensemble des candidats de `spawn_agent`. Il change uniquement la -priorité visible par Codex dans le sélecteur, tandis qu’OpenCodex conserve la priorité naturelle de chaque -ligne déplacée pour la sélection des sous-agents. `disabledModels` et `selectedModels` de chaque fournisseur +Pour ordonner tout le sélecteur, incluez un identifiant natif non qualifié : + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +Les lignes indiquées apparaissent d’abord dans l’ordre du tableau, puis les lignes absentes +selon leur priorité naturelle. La correspondance est exacte : `gpt-5.6-sol` et +`openai/gpt-5.6-sol` désignent deux lignes distinctes. Pour une ligne qualifiée par un compte, +indiquez son identifiant complet, sélecteur inclus. Les formes brute et encodée du même +identifiant routé sont acceptées, avec priorité aux correspondances exactes. Les entrées +vides sont ignorées. + +### Migration : identifiants natifs dans les listes existantes + +Auparavant, les identifiants natifs dans `modelPickerOrder` étaient ignorés. Une liste +existante contenant un identifiant natif non qualifié ordonne désormais tout le sélecteur, +y compris les lignes mises en avant. Supprimez ces identifiants pour conserver l’ancien +comportement limité aux lignes routées. Les listes absentes, vides ou uniquement routées +conservent leur comportement ; le calcul des candidats pour les consignes d’OpenCodex selon les priorités naturelles reste inchangé. + +`modelPickerOrder` préserve le calcul d’OpenCodex qui retient jusqu’à cinq candidats préférés +pour les consignes aux sous-agents, selon leur priorité naturelle. Chaque ligne déplacée conserve +cette priorité séparément de son `priority` natif ; changer uniquement l’ordre du sélecteur ne doit +pas modifier ce calcul. Cela ne restreint pas l’admissibilité d’un modèle désigné par son nom exact : +la liste annoncée n’est pas une liste d’autorisation. Les contraintes d’authentification, de modèle, +d’effort et de backend restent applicables. + +Codex natif utilise le `priority` natif pour annoncer les cinq premiers modèles admissibles et +visibles dans le sélecteur via `spawn_agent`, en V1 et en V2 lorsque les substitutions de modèle +sont exposées. Ces cinq modèles peuvent donc changer avec l’ordre du sélecteur, même si les +candidats préférés d’OpenCodex restent identiques. La V1 ne reçoit aucune injection de liste +préférée d’OpenCodex. La V2 peut recevoir en plus des consignes fondées sur les priorités naturelles +si l’état du catalogue client le permet ; ces consignes ne réordonnent pas la liste annoncée par +l’outil natif. + +`disabledModels` et `selectedModels` de chaque fournisseur restent des champs de visibilité, pas des contrôles d’ordre. Il n’existe aucun paramètre distinct `modelOrder`, `providerOrder` ou de carte de priorité. diff --git a/docs-site/src/content/docs/fr/guides/model-routing.md b/docs-site/src/content/docs/fr/guides/model-routing.md index 927d2ef863..273e2d1901 100644 --- a/docs-site/src/content/docs/fr/guides/model-routing.md +++ b/docs-site/src/content/docs/fr/guides/model-routing.md @@ -90,13 +90,17 @@ Le routage et la visibilité dans le catalogue sont deux mécanismes distincts : catalogue et `/v1/models` sont restreintes. - `provider.disabled: true` retire ce fournisseur de la découverte du catalogue. Les requêtes explicites `provider/model` échouent, et les recherches dans `defaultModel` et `models[]` l'ignorent. -- `providerContextCaps` applique des plafonds de contexte visibles par Codex, fournisseur par fournisseur. - `contextCapValue` est la valeur par défaut du tableau de bord (350 000 par défaut), mais n'a aucun effet à - lui seul tant qu'un fournisseur ne figure pas dans `providerContextCaps`. La modification de la valeur dans - le tableau de bord réaffecte tous les fournisseurs activés uniquement lorsque l'option « appliquer à tous les - fournisseurs routés » est activée ; sinon, chaque fournisseur conserve son propre plafond. Un plafond peut - seulement réduire une fenêtre de contexte connue : il ne peut ni l'augmenter ni modifier la limite réelle du - modèle en amont. +- `providerContextCaps` définit les plafonds de contexte visibles par Codex pour chaque fournisseur. + `contextCapValue` est la valeur par défaut du tableau de bord (350 000) ; elle n’applique aucun + plafond tant que le fournisseur ne figure pas dans `providerContextCaps`. Modifier cette valeur + ne met à jour les plafonds actifs que si « appliquer à tous les fournisseurs routés » est activé ; + sinon, chaque fournisseur conserve son plafond. Les fenêtres ordinaires connues ne peuvent + qu’être réduites ; les modèles natifs prenant en charge une fenêtre longue peuvent être étendus + jusqu’à leur propre plafond pris en charge, sans modifier la limite réelle du modèle en amont. + Désactiver un plafond conserve sa sélection dans `providerContextCapValues`, même après + rechargement ; le réactiver restaure cette sélection. Une sélection mémorisée n’impose aucune + limite tant que le plafond est désactivé. `{ "setAll": true }` sans `value` active tous les + fournisseurs configurés à la valeur globale actuelle et remplace leurs sélections mémorisées. ```json { diff --git a/docs-site/src/content/docs/fr/guides/routing-profile-editor.md b/docs-site/src/content/docs/fr/guides/routing-profile-editor.md index b437c28b3d..18575f21c8 100644 --- a/docs-site/src/content/docs/fr/guides/routing-profile-editor.md +++ b/docs-site/src/content/docs/fr/guides/routing-profile-editor.md @@ -37,6 +37,14 @@ résultat du plafond. ## Simuler un profil enregistré +Les capacités des candidats utilisent la configuration effective du fournisseur, +après application du registre. Les exigences de localité (`localOnly` et +`remoteAllowed`) utilisent donc l’adresse amont effective. Si elle ne peut pas être +classée, `unknownEvidence.capability` détermine l’admissibilité du candidat. +Une configuration de fournisseur invalide qui ne peut pas être résolue est toujours +exclue avec `route-unavailable`, même si les capacités inconnues sont autorisées. +Les fournisseurs absents ou désactivés sont également exclus avec `route-unavailable` avant le calcul des scores. + Sélectionnez un profil enregistré et utilisez **Évaluation à sec** pour ajouter des éléments propres à la requête, tels que la taille de la fenêtre de contexte, l’utilisation d’outils, l’entrée d’images ou la sortie structurée. La simulation évalue l’admissibilité et la notation, mais n’envoie jamais de requête à un modèle en amont. Les modifications non enregistrées ne sont pas prises en compte par la simulation. Enregistrez d’abord le profil afin que la révision et l’évaluation affichées correspondent à la même configuration. diff --git a/docs-site/src/content/docs/fr/guides/web-dashboard.md b/docs-site/src/content/docs/fr/guides/web-dashboard.md index 9970b26bda..2e333e39d8 100644 --- a/docs-site/src/content/docs/fr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/fr/guides/web-dashboard.md @@ -57,6 +57,14 @@ gestionnaire de mots de passe. | **Stockage** | Consultez en lecture seule la répartition du disque de CODEX_HOME — sessions, archives, bases de données et pièces jointes. Pour le nettoyage facultatif des archives, prévisualisez les N % les plus anciennes, puis placez-les en quarantaine dans `CODEX_HOME/.trash` (par défaut) ou supprimez-les définitivement après avoir coché une case explicite. **La stratégie de nettoyage automatique** est facultative et **désactivée par défaut** (`storageCleanupPolicy.enabled`) ; configurez son seuil, sa cible, sa planification et son mode sur la page **Stockage**, ou lancez **Exécuter maintenant**. Les entrées mises en quarantaine peuvent être restaurées depuis cette page (JSONL et fils). Les sessions actives restent en lecture seule. Le nettoyage et la restauration sont refusés tant que Codex verrouille le fichier `state_*.sqlite` le plus récent ou actif. | | **Arrêter** | Arrêtez proprement le proxy et le service d'arrière-plan installé, restaurez Codex natif et quittez (`POST /api/stop`). Sur Windows avec le backend Planificateur de tâches, le tableau de bord refuse et vous demande d'exécuter `ocx stop` : le wrapper peut relancer le proxy après la fin de la tâche, et seul un stop exécuté hors du proxy peut vérifier cette fenêtre de redémarrage avant de restaurer votre configuration client. Rien n'est modifié en cas de refus. | +### Filtrer les requêtes + +Les filtres combinent interface, requêtes interceptées, fournisseur, modèle exact, statut, période, vitesse et identifiant de conversation dans le journal chargé. Les choix incluent les tentatives de repli ; les modèles ignorent la casse et les espaces externes, sans correspondance partielle. Un choix disparu revient à Tous. + +Les périodes de 15 minutes, une heure et un jour évoluent toutes les 30 secondes dans l’onglet Logs, même sans actualisation automatique. La vitesse mesure les jetons de sortie par seconde sur toute la durée : moins de 15, de 15 à moins de 50, ou au moins 50 ; les valeurs indisponibles sont exclues quand ce filtre est actif. Réussite : 2xx ; erreur : 4xx/5xx. + +Le compteur compare les résultats au total chargé ; la réinitialisation restaure toutes les lignes. Aucun résultat diffère d’un journal vide. Flèches et Home/End pilotent le sélecteur d’interface. Aucun historique au-delà du journal chargé n’est interrogé. + ### Liens directs vers une section Il n'existe qu'une seule mise en page, donc aucun commutateur de disposition n'est à configurer. Les sections @@ -76,6 +84,27 @@ uniquement si la liste d'autorisation de son fournisseur l'inclut — ou si aucu s'il n'est pas désactivé. Activer un modèle réconcilie atomiquement les deux filtres ; **Tout activer** efface la liste d'autorisation du fournisseur afin que les modèles découverts ultérieurement soient eux aussi actifs. +### Gérer les modèles dans l’espace fournisseur + +Dans l’onglet **Modèles** d’un fournisseur, **Supprimer** retire la définition personnalisée +stockée. Le modèle natif ou découvert sous-jacent peut alors réapparaître ; le nombre de modèles +peut donc rester identique. **Masquer** change uniquement la visibilité dans le catalogue, sans +supprimer la définition ni modifier la politique de routage direct. **Gérer la visibilité dans +Modèles** ouvre la page **Modèles** pour rétablir la visibilité, même si l’onglet du fournisseur +ne contient plus aucune ligne. + +**Ajouter** enregistre une définition personnalisée sans effacer un masquage existant ni les +règles de sélection du fournisseur. Un modèle enregistré peut donc rester masqué. Si le modèle +est déjà connu, gérez sa visibilité dans **Modèles**. Un enregistrement confirmé reste valable +même si l’actualisation du catalogue échoue : suivez le message d’actualisation au lieu d’ajouter +le modèle à nouveau. Si la modification n’est pas confirmée, actualisez l’état des modèles avant +de réessayer. + +Le compteur du fournisseur indique le nombre d’entrées uniques non désactivées dans l’inventaire +courant renvoyé par le serveur, avant recherche ou limitation de l’affichage. Il ne mesure ni la +liste d’autorisation ni les résultats de découverte en direct et ne prouve pas l’origine d’une +entrée. Les badges de sélection et les informations de découverte restent distincts. + ## Sélecteur de délégation et routage des créations de sous-agents Le sélecteur **Délégation de sous-agent** du tableau de bord enregistre `injectionModel` et, facultativement, @@ -176,7 +205,7 @@ L'interface graphique est un client léger de l'API JSON de gestion du proxy. Pa | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Sélectionner le compte de la prochaine requête et configurer le routage du pool. | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | Lire le compte effectif — notamment `pinned` et le compte désigné par `pinnedAccountId` — et définir l'ordre de sélection d'un compte. | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Ajouter un compte au groupe au moyen d’une connexion dans le navigateur. | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Lire les métadonnées des requêtes récentes avec des filtres facultatifs de fin de journal, de fournisseur et d'état exact ou par classe. Avec `limit`/`offset`, la pagination remonte depuis la ligne la plus récente (`offset=0` renvoie la dernière page). Forme de la réponse : `{ timeZone, total, logs }`, où `total` est le nombre de lignes filtrées avant pagination. | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Lire les métadonnées des requêtes récentes avec des filtres facultatifs de fin de journal, de fournisseur et d'état exact ou par classe. Avec `limit`/`offset`, la pagination remonte depuis la ligne la plus récente (`offset=0` renvoie la dernière page). Forme de la réponse : `{ timeZone, generatedAt, total, logs }`, où `total` est le nombre de lignes filtrées avant pagination. | | `GET` / `PUT /api/subagent-models` | Lire ou définir les cinq modèles de remplacement `spawn_agent` mis en avant. | | `POST /api/stop` | Arrêter le proxy et le service, restaurer Codex natif et quitter. Refusé avec `respawnable_service` sur le backend Planificateur de tâches Windows, et avec `service_state_unknown` lorsque cet état ne peut pas être lu ; rien n'est modifié dans les deux cas. | diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md index ac152db316..59652bbaef 100644 --- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md @@ -152,6 +152,12 @@ Invalide le cache local du sélecteur de modèles de Codex afin qu’il soit rec Exécute opencodex comme service d’arrière-plan géré à l’ouverture de session — **launchd** sous macOS, **unité utilisateur systemd** sous Linux et **Task Scheduler** sous Windows — qui démarre automatiquement à la connexion et redémarre après un plantage. Les services définissent `OCX_SERVICE=1` afin qu’un redémarrage ne réécrive pas inutilement la configuration Codex. +Les installations via le Planificateur de tâches Windows utilisent une priorité de processus normale (`Priority=4`). +L’ancienne priorité d’arrière-plan (`7`, également la valeur par défaut si le paramètre est omis) peut retarder les réponses +aux contrôles de santé en cas de contention CPU : la zone de notification affiche alors Offline même si le processus fonctionne. +Après la mise à jour, exécutez `ocx service repair` pour migrer cette priorité enregistrée et redémarrer le service. +Une confirmation UAC peut être nécessaire. Une priorité déjà normale ou haute ne déclenche pas, à elle seule, de réenregistrement. + | Sous-commande | Action | | --- | --- | | aucune | Installe et démarre le service s’il est absent ; sinon, actualise et redémarre le service existant. Une définition Task Scheduler Windows saine est réutilisée ; une définition obsolète peut être réenregistrée et nécessiter une élévation. | 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 dace4ce567..7af82372b9 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/fr/reference/configuration/agents.md @@ -58,7 +58,7 @@ Pour un tour enfant créé, l’ordre de repli est le suivant : Les chaînes de repli propres à un rôle doivent résider dans la configuration d’opencodex. L’ajout de `model_fallback` dans `$CODEX_HOME/agents/*.toml` amène Codex 0.146+ à rejeter le fichier de rôle entier à cause de ce champ inconnu, puis à ignorer le rôle (#1190). Une ancienne ligne `model_fallback` dans le fichier TOML reste lue par souci de rétrocompatibilité, mais `ocx doctor` la signale. -opencodex ignore les candidats désactivés, non routables, en mauvais état, en période de temporisation ou ayant atteint le seuil de quota. L’instantané de disponibilité est mis en cache pendant `subagentModelFallbackPollMs`. Les tâches enfants chiffrées limitent la chaîne aux cibles ChatGPT natives canoniques et aux routes Responses directes avec authentification par clé explicitement approuvées via `allowEncryptedV2AgentTasks: true` ; si aucune ne peut consommer la charge chiffrée, la requête échoue au lieu d’envoyer un texte chiffré illisible à une autre destination. Les combos restent limités aux cibles natives canoniques. +opencodex ignore les candidats désactivés, non routables, en mauvais état, en période de temporisation ou ayant atteint le seuil de quota. L’instantané de disponibilité est mis en cache pendant `subagentModelFallbackPollMs`. Les tâches enfants chiffrées limitent la chaîne aux cibles ChatGPT natives canoniques et aux routes Responses directes avec authentification par clé explicitement approuvées via `allowEncryptedV2AgentTasks: true` ; si aucune ne peut consommer la charge chiffrée et que la récupération facultative ne permet pas un envoi routé, la requête échoue sans transmettre de texte chiffré illisible. Un combo essaie d’abord une cible native canonique disponible ; si aucune n’est sélectionnable ou si les tentatives natives sont épuisées, et que `agentTaskRecovery` est activé, un `NEW_TASK` chiffré est récupéré une fois avant l’envoi routé du combo. ```json { @@ -111,7 +111,7 @@ Ce mécanisme ne protège pas contre un autre processus exécuté sous le même N’activez cette option que si la requête authentifiée supplémentaire, la consommation de quota, la présence de texte en clair dans le processus et la dépendance à un service privé sont acceptables. Dans le cas contraire, privilégiez un enfant ChatGPT natif ou une délégation hétérogène v1. -Ce mécanisme de récupération s’applique aux enfants routés directement. Au maximum 32 requêtes de récupération peuvent être actives simultanément ; toute absence supplémentaire dans le cache échoue de manière sûre. Pour les tâches chiffrées, le routage par combinaison conserve son filtre existant limité aux cibles natives et n’utilise pas la récupération. +Ce mécanisme de récupération s’applique aux enfants routés directement et aux `NEW_TASK` chiffrés d’un combo. Au maximum 32 requêtes de récupération peuvent être actives simultanément ; toute absence supplémentaire dans le cache échoue de manière sûre. Un combo disposant d’une cible native canonique disponible continue d’envoyer directement le texte chiffré ; la récupération peut s’exécuter si aucune cible native n’est sélectionnable ou si les tentatives natives sont épuisées. Si la récupération est désactivée ou échoue, ou si aucune cible routée n’est disponible, le texte chiffré illisible n’est pas transmis à un fournisseur routé. ## Plafonds d’effort diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md index bec7932c09..32b6a28023 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md @@ -28,8 +28,9 @@ Après une inscription ou une connexion OAuth dans l’interface, une boîte de | `providers` | `Record` | — | Mappage du nom du fournisseur avec la configuration du fournisseur. | | `openaiProviderTierVersion?` | `2` | défini par la migration | Marque la projection OpenAI prenant en compte les options uniques comme terminée. | | `disabledModels?` | `string[]` | — | Modèles masqués du catalogue de Codex et de `/v1/models`, mais non bloqués des appels proxy directs. Un identifiant acheminé est supprimé des listes. Un identifiant natif qualifié de compte masque uniquement cette ligne de sélecteur ; un identifiant GPT natif nu masque la ligne nue et chaque ligne de sélecteur de compte pour ce modèle. La page Modèles du tableau de bord expose uniquement les lignes natives routées et nues ; utilisez ce champ de configuration directement pour masquer une ligne qualifiée par le sélecteur. | -| `providerContextCaps?` | `Record` | `{}` | Limites de contexte Codex-visibles par fournisseur. Un plafond abaisse uniquement une fenêtre de contexte connue. | -| `contextCapValue?` | `number` | `350000` | Valeur par défaut utilisée par les contrôles de plafond de contexte du tableau de bord. La modifier applique la valeur à chaque fournisseur routé — y compris ceux qui ne possèdent aucune entrée `providerContextCaps` — uniquement lorsque l'option « appliquer à chaque fournisseur routé » est activée ; sinon, chaque fournisseur conserve son propre plafond. | +| `providerContextCaps?` | `Record` | `{}` | Limites de contexte actives par fournisseur. Les fenêtres ordinaires sont réduites ; les modèles natifs prenant en charge une fenêtre longue peuvent être étendus uniquement jusqu’à leur propre plafond pris en charge. | +| `providerContextCapValues?` | `Record` | `{}` | Dernières limites sélectionnées par fournisseur, conservées après désactivation. Ces valeurs n’activent aucun plafond. Une valeur active est prioritaire sur une valeur mémorisée. | +| `contextCapValue?` | `number` | `350000` | Valeur par défaut lors de la première activation. Les activations suivantes restaurent la sélection du fournisseur. Modifier la valeur globale avec `setAll: true` ne modifie que les plafonds actifs ; `setAll: true` sans valeur active tous les fournisseurs configurés à la valeur globale actuelle. | | `codexAccounts?` | `CodexAccount[]` | `[]` | Métadonnées du compte pool ChatGPT/Codex gérées par Codex Auth. Les secrets vivent séparément dans `codex-accounts.json`. | | `pausedCodexAccountIds?` | `string[]` | `[]` | Comptes exclus de la sélection du pool jusqu'à la reprise, y compris le compte principal `__main__` lorsqu'il est mis en pause. | | `codexAccountNamespaces?` | `Record` | — | Mappage facultatif d’un sélecteur de modèle public arbitraire vers une cible de compte Codex stockée. Lorsque les lignes du sélecteur qualifié par compte sont activées, chaque sélecteur dont la cible est présente ajoute des lignes `/` distinctes au sélecteur Codex ; chaque ligne utilise uniquement ce compte. Dès qu'un sélecteur est actif, les lignes natives non qualifiées sont masquées dans le sélecteur, mais leurs identifiants restent routables et figurent toujours dans la réponse brute de `/v1/models`, sauf désactivation explicite. | @@ -93,7 +94,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Style de l'en-tête de clé Anthropic. La valeur par défaut est l'en-tête natif `x-api-key` ; ce champ n'est valable que pour les fournisseurs `anthropic` authentifiés par clé. | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Pool multi-clés. `apiKey` reflète l'entrée active ; chaque élément a `id`, `key`, `label` facultatif et `addedAt` numérique facultatif. | | `defaultModel?` | `string` | Modèle utilisé lorsque ce fournisseur est sélectionné sans modèle explicite. | -| `models?` | `string[]` | Liste initiale ou de repli des modèles. Avec `liveModels: false`, ce sont les seuls modèles découverts. | +| `models?` | `string[]` | Liste initiale ou de repli. Avec `liveModels: false`, une liste `models` non vide est suivie de `retainModels` ; si `models` est vide ou absent, la liste commence par `defaultModel` (si configuré), puis `retainModels`, en conservant la première occurrence de chaque identifiant. | | `liveModels?` | `boolean` | Récupère le catalogue actif au démarrage et lors de la synchronisation (true par défaut). Les fournisseurs personnalisés utilisent `${baseUrl}/models` ; les fournisseurs intégrés peuvent employer une URL de registre et un filtre. | | `selectedModels?` | `string[]` | Liste autorisée du catalogue après la découverte. Non vide expose uniquement ces identifiants ; vide ou omis expose tous les modèles découverts. | | `contextWindow?` | `number` | Repli contextuel à l’échelle du fournisseur lorsque les métadonnées en amont sont absentes ; sinon, un plafond qui conserve des métadonnées en direct plus petites. Le tableau de bord Modèles expose cela séparément de `providerContextCaps`. | @@ -435,8 +436,17 @@ modèle. Le même mappage s'applique à un sélecteur natif `vercel/` ## Listes autorisées de modèles statiques -Réglez `liveModels: false` pour exposer uniquement `models`. Si `models` est vide ou omis, le fournisseur n'expose -aucun modèle routé. La découverte dynamique rejette plus de 4 Mio ou 2 000 lignes de modèle brutes avant leur mise en cache ; +Avec `liveModels: false`, si `models` est vide ou absent, la liste initiale commence par le +`defaultModel` configuré, puis les identifiants de `retainModels`. Les doublons sont supprimés +en conservant leur première occurrence. Une liste `models` explicite non vide est au contraire +suivie de `retainModels`, sans ajout implicite d’un autre `defaultModel`. Ce dernier peut toujours +être inscrit explicitement dans `models` ou `retainModels`. Si aucun de ces champs ne fournit +d’identifiant, la liste initiale est vide. Cet ordre ne garantit pas l’ordre final du sélecteur. +`selectedModels`, `disabledModels` et la désactivation du fournisseur restent applicables. +`authMode: "forward"` conserve sa branche distincte et n’utilise pas cette liste statique routée. +Ces règles ne changent pas le repli en cas d’échec de la découverte en direct. + +La découverte dynamique rejette plus de 4 Mio ou 2 000 lignes de modèle brutes avant leur mise en cache ; les préréglages intégrés peuvent appliquer des limites inférieures et filtrer les lignes admissibles à la conversation. Les résultats trop volumineux ou mal formés utilisent le catalogue obsolète ou configuré comme solution de repli. Un résultat valide ne contenant aucun modèle admissible fait autorité et n'est pas silencieusement remplacé ou tronqué. 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 207f8a4c08..27fd80b36a 100644 --- a/docs-site/src/content/docs/fr/reference/configuration/server.md +++ b/docs-site/src/content/docs/fr/reference/configuration/server.md @@ -258,3 +258,7 @@ compte et la charge de travail prévus. `runtimeRole` vaut `standalone` par défaut. Un hub utilise `hub.managementPublicOrigin`, `hub.managementIngress` limité au loopback (`enabled:false` si absent) et les identités exactes de `remoteGui.allowedTailscaleUsers` (liste vide si absente). La clé client reste dans `service-api-token`, jamais dans `config.json`; `service-api-token.prev` peut exister pendant une rotation. Les usages ne sont pas répliqués. `remoteGui.allowInsecureHttp` est un ancien no-op déprécié, conservé uniquement pour que les anciens fichiers passent encore le schéma strict. Supprimez-le de la configuration : les grants de pairing ne sont acceptés que sur loopback ou via HTTPS authentifié, et `true` ne réactive pas le pairing HTTP en clair. + +## Diagnostic réseau des quotas Codex + +Le champ `quotaRefresh` de la ligne du compte Codex principal décrit la récupération du quota, pas le quota restant ni les droits d’accès au modèle. Il peut être absent lorsque les données sont en cache ou qu’aucune récupération n’a eu lieu. La requête utilise l’environnement du service proxy en cours d’exécution, pas celui du terminal interactif. Sans `proxy`, l’environnement existant est conservé ; `"auto"` lit uniquement le proxy statique Windows au démarrage. PAC/WPAD, les paramètres SOCKS seuls et les changements à chaud ne sont pas pris en compte automatiquement. Un succès avec TUN ne valide pas à lui seul le chemin du proxy HTTP. Consultez [les commandes et les états en anglais](/reference/configuration/server/#codex-quota-network-diagnostics). 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 0ea012916e..0732df6dca 100644 --- a/docs-site/src/content/docs/fr/reference/management-api.md +++ b/docs-site/src/content/docs/fr/reference/management-api.md @@ -172,12 +172,15 @@ d’abord et soumettez le résumé renvoyé. Préférez la quarantaine lorsqu’ | `GET /api/models` | Renvoyer les lignes de modèles destinées au tableau de bord et à l'interface en ligne de commande | `catalog_busy` lorsque la collecte est saturée | | `GET /api/client-config?client=...` | Créez une configuration client en lecture seule pour toute intégration de fichiers prise en charge | 400 client non pris en charge ; 503 catalogue indisponible | | `PUT /api/disabled-models` | Remplacer la liste partagée des modèles désactivés | 400 invalide JSON | -| `PUT /api/model-visibility` | Modifier atomiquement la visibilité au niveau du fournisseur ou du modèle | 400 fournisseur, portée, cible ou corps non valide | +| `PUT /api/model-visibility` | Modifier atomiquement la visibilité au niveau du fournisseur ou du modèle | 400 fournisseur, portée, cible ou corps non valide; 409 `initial_model_selection_pending` (Actualisez la liste des modèles, puis réessayez.) | | `GET, POST /api/custom-models` | Répertoriez les modèles personnalisés ou ajoutez-en un | 400 champs invalides ; 404 fournisseur manquant ; 409 dupliquer le modèle | | `PUT, DELETE /api/custom-models/{id}` | Modifier ou supprimer un modèle personnalisé | 400 invalide id/fields ; 404 introuvable ; 409 modèle en double | | `GET, PUT /api/selected-models` | Lire les listes autorisées et la disponibilité des fournisseurs, ou remplacer une liste autorisée | 400 fournisseur ou corps manquant ; 404 fournisseur inconnu; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Lire les préréglages ou choisir le mode preset/all/custom | 400 mode invalide ou préréglage indisponible; 404 fournisseur inconnu; PUT 409 `initial_model_selection_pending` | +Un modèle manuel remplace la ligne du tableau de bord Models ayant le même fournisseur et identifiant de modèle. Pour OpenAI, la ligne manuelle conserve `openai/` et ses contrôles de visibilité. Sa suppression restaure la ligne native sans qualificatif de compte. Les lignes natives qualifiées par compte restent distinctes. Les routes natives et les droits du compte ne changent pas. Une cible de visibilité OpenAI non native doit correspondre à un modèle manuel configuré. + + Tant qu’une liste initiale fiable n’est pas disponible, les requêtes PUT valides vers `/api/selected-models` et `/api/model-presets` renvoient HTTP 409 avec le code `initial_model_selection_pending`. Actualisez la découverte des modèles (par exemple, `GET /api/models`), puis réessayez après sa réussite. ### Comptes OAuth, clés de fournisseur et clés du plan de données @@ -217,6 +220,18 @@ fournisseurs ne sont pas renvoyés aux clients du tableau de bord. | `GET, PUT /api/provider-context-caps` | Lire ou mettre à jour les plafonds de contexte globaux, communs à tous les fournisseurs ou propres à un fournisseur | 400 requête invalide ; 404 fournisseur inconnu | | `GET /api/provider-presets` | Renvoyer les préréglages de fournisseur de l'interface graphique dérivés du registre d'exécution | — | +La réponse des plafonds de contexte comprend `caps` (limites actives) et `values` (dernières +sélections, conservées après désactivation). Activer un fournisseur sans `value` restaure sa +sélection, ou utilise la valeur globale `contextCapValue` lors de la première activation. +Cela vaut aussi pour OpenAI : le commutateur ne sélectionne pas un mode spécial à 922k. +Un plafond actif borne chaque fenêtre native ; les modèles prenant en charge un contexte long +peuvent être étendus uniquement jusqu’à leur propre plafond pris en charge. +`{ "value": 600000, "setAll": true }` modifie la valeur globale et uniquement les plafonds actifs ; +les fournisseurs dont le plafond est désactivé conservent leur sélection pour une réactivation ultérieure. +`{ "setAll": true }` sans `value` active tous les fournisseurs configurés à la valeur globale +actuelle et remplace leurs sélections mémorisées. La désactivation conserve la sélection, +même après rechargement, sans l’appliquer comme limite. + `provider_has_dependent_combos` est une barrière de sécurité : supprimez ou modifiez les combinaisons dépendantes avant de supprimer leur fournisseur. 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 f224e61bd2..d4f3dc266c 100644 --- a/docs-site/src/content/docs/fr/reference/proxy-formats.md +++ b/docs-site/src/content/docs/fr/reference/proxy-formats.md @@ -28,7 +28,7 @@ doit choisir parmi plusieurs cibles. | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `chat.completion.chunk` SSE se terminant par `[DONE]` | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Comptage des jetons Anthropic | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | Sans objet | -| Découverte de modèles | `GET /v1/models` | L'un des trois contrats du catalogue | Sans objet | +| Découverte de modèles | `GET /v1/models` | Catalogue ou instantané Desktop explicite | Sans objet | | Voix et temps réel | `POST /v1/live`, `POST /v1/realtime/calls` | Réponse de création d'appel relayée | Une bande latérale séparée WebSocket relaie les trames dans les deux sens | | Compactage des réponses | `POST /v1/responses/compact` | Historique de remplacement JSON | Sans objet | @@ -227,10 +227,17 @@ estimation documentée du contenu du système, des messages et des outils et ret { "input_tokens": 123 } ``` +Un ID Desktop de forme datée non résolu peut aussi être un véritable modèle natif absent de +la découverte. Messages et count-tokens renvoient HTTP 503 avec l’erreur fixe `desktop_model_mapping_unavailable` lorsque les informations disponibles ne permettent pas de résoudre cet ID ; cela ne +prouve pas que le modèle est invalide. Les anciens alias de type hash inconnus restent rejetés +avec HTTP 400. Aucun des deux cas ne retire la date ni ne choisit une autre route. Les ID connus, +les correspondances enregistrées et les entrées exactes de `modelMap`, dont les véritables ID +natifs reconnus, conservent leur traitement. Actualisez la découverte ou réappliquez le profil du +hub connecté avant de réessayer ; une simple nouvelle tentative ne garantit pas la résolution. + ## `GET /v1/models` -Le même itinéraire dessert trois clients qui attendent des enveloppes de catalogue incompatibles. -La variante Anthropic est prioritaire, sauf si `client_version` est également présent. +Sans `format=desktop-config`, les contrats de catalogue ordinaires sont les suivants : | Contrat | Déclencheur | Forme de niveau supérieur | Comportement de l’identifiant du modèle | | --- | --- | --- | --- | @@ -238,6 +245,31 @@ La variante Anthropic est prioritaire, sauf si `client_version` est également p | Codex catalogue | `client_version` paramètre de requête | `{ "models": [...] }` | Les entrées natives et routées contiennent les champs de catalogue Codex les plus riches, la visibilité, l'effort, WebSocket et les métadonnées multi-agents | | Liste simple OpenAI | Ni l'un ni l'autre déclencheur | `{ "object": "list", "data": [...] }` | Les identifiants natifs visibles sont nus ; les identifiants routés sont des alias ou `provider/model` | +### Instantané de configuration Desktop + +`GET /v1/models?ids=desktop&format=desktop-config` sélectionne explicitement le snapshot +Desktop, indépendamment du user-agent. La réponse est `{ "version": 1, "models": [...] }` +avec `Cache-Control: no-store`. Le client envoie `Accept: application/json`, +`anthropic-version: 2023-06-01` et ses identifiants existants d'accès aux données, sans jeton +administrateur ni envoi de profil. Les entrées sont les modèles de configuration Desktop émis +par le hub, pas les lignes du catalogue Codex. + +Avec `ids=cli` ou un paramètre `client_version`, ce format renvoie HTTP 400. Sans le sélecteur +de format, les contrats ordinaires ci-dessus restent inchangés. Si Claude est désactivé, +`{ "version": 1, "models": [] }` indique l'indisponibilité à Desktop apply, qui n'écrit aucun +profil de remplacement. Un ancien hub renvoyant un catalogue ordinaire au lieu de la version 1 +n'est pas pris en charge ; aucun identifiant local de secours n'est généré. + +Le snapshot reste une lecture de modèles, pas une API de rotation ou d'envoi de profil. +Migration des clés Desktop, récupération et déconnexion utilisent le cycle de vie client existant. +La rotation conserve modèles et sélection ; le champ CLI `rotation` distingue `committed` et +`rolled_back`. La déconnexion restaure les paramètres gérés ou signale un repli standard pour un +ancien profil reconnu, en préservant champs utilisateur et choix valides ultérieurs. Conflits et +récupération incomplète empêchent de déclarer l'opération terminée. Redémarrez Desktop pour lire +les changements ; la déconnexion ne révoque pas automatiquement la clé du hub. +Voir [le guide Desktop](/fr/guides/claude-code/). Relecture thinking et cache restent dans +[#3719](https://github.com/lidge-jun/opencodex/issues/3719). + ## `POST /v1/live` et bande latérale en temps réel `POST /v1/live` accepte la surface de création d'appel ChatGPT/Codex App sans cadre. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 9dafc37c21..5ed946c72a 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -139,6 +139,8 @@ is temporarily unavailable, the first available route in that family is used unt You can also manage the same profile from the command line: +The profile-editing instructions below describe the local profile. Connected remote apply is described separately below. + ```bash ocx claude desktop [apply] ocx claude desktop show [--json] @@ -217,6 +219,66 @@ dedicated proxy admission header is valid. This also means the Disable with `claudeCode.nativePassthrough: false`; point elsewhere with `claudeCode.anthropicBaseUrl`. +## Claude Desktop on a connected remote hub + +When this machine is connected to a hub, `ocx claude desktop apply` (or `ocx claude desktop`) +uses the hub's Desktop model snapshot. It writes the connected hub origin and the hub-issued +model IDs into the local Desktop configuration without generating replacement aliases locally. +Static and hybrid modes copy the snapshot entries; discovery-only mode uses the hub origin +without embedding the model list. + +The hub owns the Desktop profile, family assignments and defaults. Change those on the hub, +then apply again on the connected client and reselect the model in Desktop. Old aliases created +only on the client require reapply/reselection; they are not automatically migrated. Local `show`, +profile edits, and import/export remain local views and operations, not hub-profile management. +While connected, `ocx claude desktop import --apply` is unsupported and refuses the import +before saving. Import without `--apply` remains local. + +Apply reads the snapshot using the existing connection's data credential. It needs no admin token +and uploads no profile. If the hub is too old to support the snapshot, the response is invalid, +or no Desktop models are available, apply fails without substituting a local catalog or loopback +origin. Upgrade/configure the hub and apply again. + +This alias change does not fix the separate `thinking` / `redacted_thinking` replay and prompt-cache +request in [#3719](https://github.com/lidge-jun/opencodex/issues/3719). Proxy admission alone does not enable native Anthropic passthrough; translated +Anthropic routes can still use prompt caching. Replay fidelity and cache-hit comparisons remain +separate work. + +### Key rotation, recovery and disconnect + +Key rotation and recovery update the credential stored in the connection-owned Desktop profile +alongside the local connection credential. No manual Desktop reapply is required just to migrate +the key. Existing model IDs, family/default choices and the user's current profile selection are +preserved; rotation does not select the managed profile again or re-enable a disabled integration. +CLI JSON `rotation: "committed"` means the new key is active. `rotation: "rolled_back"` means the +previous key was retained or restored, not that a new key was committed or the previous key revoked. +Uncertain or incomplete recovery is reported as such, rather than as successful rotation. + +The first connected apply records the prior managed settings and selection for restoration. +Repeated apply and key rotation retain that original baseline. `ocx disconnect` restores the +connection-owned settings while preserving current user-added fields and unrelated profiles. +The previous selection is restored only if the managed profile is still selected; a later valid +user selection stays selected. A newly created profile with user additions is retained in readable +standard mode instead of deleting those additions. `--keep-catalog` keeps the catalog, not the +Desktop connection credential. + +For an older managed profile without an original record, OpenCodex can migrate it when it +unambiguously belongs to the current hub and a recognized connection key. Apply, rotation/recovery +or direct disconnect can handle this case without a new flag or prerequisite reapply. A warning +explains that disconnect will use standard mode because the previous settings were not recorded. +That fallback removes only the connection-owned gateway settings, preserves user fields and a +separate valid selection, and is reported as standard fallback, not original restoration. + +Conflicting managed fields, unrecognized credentials or damaged restoration records are preserved +and reported for resolution. Interrupted cleanup can resume for the same connection; it does not +clear a newer connection or claim completion while restoration remains incomplete. Finish pending +rotation recovery before starting disconnect, and retain the same catalog choice when retrying it. + +Fully quit and reopen Claude Desktop after apply, rotation/recovery or restoration: changing files +does not replace a credential already held by the running app. OpenCodex does not kill/restart the +app automatically. Disconnect works locally without automatically revoking the hub key or erasing +arbitrary external copies; revoke separately on the hub if desired. + ## The /model picker ("From gateway") Claude Code 2.1.129+ discovers gateway models via `GET /v1/models?limit=1000` and lists them in @@ -261,6 +323,16 @@ express fall back to the hashed alias. Model ids MAY contain `--` (resolution sp **Model resolution order:** `[1m]` marker stripped → readable alias decoded → Desktop hashed alias decoded → `modelMap` exact match → date-stripped match (`-20250514` removed) → passthrough. + + +An unresolved date-shaped Desktop ID can also be a genuine native model missing from discovery. +Messages and count-tokens return HTTP 503 with the fixed `desktop_model_mapping_unavailable` error when the available +evidence cannot resolve that ID; this does not establish that the model is invalid. Unknown legacy +hash aliases still return HTTP 400. Neither case strips the date or falls back to another route. +Known IDs, registered mappings and exact `modelMap` matches keep their existing behavior, including +recognized real native IDs. Refresh model discovery or reapply the connected hub profile before +trying again; retrying alone does not guarantee resolution. + Each entry carries a display name like `gemini-3-pro (gemini)`, plus full model capabilities (reasoning-effort ladder, thinking types) in the official `ModelInfo` shape. Real Anthropic models keep their canonical ids on both surfaces. @@ -359,6 +431,8 @@ entirely). The stub keeps tool call/result pairing intact. Lookup order: discovery alias → exact id → id with date suffix stripped (`-20250514`) → passthrough. +See [Desktop alias resolution](#desktop-alias-resolution) for the rejection policy. + ## Sidecar matrix: web search and image understanding Routed models do not all have the same hosted tools or image support. opencodex fills those gaps diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index b2ae7fcb91..64466b61a4 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -324,6 +324,18 @@ encodes that declaration and its history as an upstream function tool, then rest function-call lifecycle to `custom_tool_call` before Codex sees it. Native OpenAI forward routing and the supported `apply_patch` custom tool stay unchanged. +If a routed model sends a complete patch as the entire code-mode `exec` input, opencodex +converts it to the nested `tools.apply_patch` call before the tool-completion events reach +Codex. Native custom calls and converted function calls use the same completion rule; +patch previews are held while their executable form is unresolved. JavaScript that merely +contains patch text and unrelated native custom payloads stay unchanged. + +Ordinary routed Responses function calls also use the original declared parameter schema at +completion: integral floats in integer fields and integral numbers in string-only fields are +normalized, while fractions and numeric unions stay unchanged. An explicitly empty completed +argument string becomes `{}`. Final events and locally stored continuation history agree. +Unambiguous dotted namespace spellings are restored to the declared namespace and tool name. + The selected provider must support function/tool calling. A text-only provider without tool-call support cannot use `exec`, Browser, or Computer Use. Native OpenAI rows keep their upstream tool mode unchanged. @@ -454,8 +466,14 @@ If a model is missing from Codex, or the catalog order/visibility looks wrong, c catalog. 2. **`disabledModels`** (top level) — hides models from both the catalog and `/v1/models`, and flips bare native GPT slugs to `visibility: "hide"`. -3. **`liveModels: false` with empty `models`** — when live discovery is off and `models` is empty or - omitted, opencodex exposes no routed models for that provider. +3. **`liveModels: false`** — With `liveModels: false`, an empty or omitted `models` list seeds the configured `defaultModel` + first, followed by `retainModels`; duplicate ids are removed while preserving first occurrence. + A nonempty explicit `models` list instead seeds `models` followed by `retainModels`, without + implicitly adding a different `defaultModel`. That default can still be listed explicitly in + `models` or `retainModels`. If none of these fields supplies an id, the static seed is empty. + This is seed order, not a promise of final picker order. `selectedModels`, `disabledModels` and + provider-disabled policy still apply. `authMode: "forward"` keeps its separate branch and does + not use this routed static seed. These rules do not change live-discovery failure fallback. 4. **Cursor `GetUsableModels`** — the Cursor adapter discovers models through its protobuf `GetUsableModels` RPC, not `/models`, so a Cursor-side change can alter which ids are visible independently of other providers. diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index da1273ad9a..0c95908206 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -21,6 +21,10 @@ file, and removes it again. Twelve clients work this way, each with a switch: | ZCode | `~/.zcode/v2/config.json` | JSON | on restart | loopback placeholder | | Aside | `~/.aside/u//models.json` | JSON | after fully quitting and reopening Aside | loopback placeholder | +Generated catalogs include only enabled models from each provider selection. This applies to both +downloads and managed integrations, including Pi and Aside. The management model list still shows +the full roster so you can enable additional models. + The managed OpenCode integration owns two fragments: `provider.opencodex` (opencode V1) and `providers.opencodex` (opencode V2). Only the V2 block carries the per-model reasoning-effort variants, so both are written and kept in sync; they name the same provider and model ids, and @@ -48,12 +52,10 @@ disagree about which file is meant. Its managed block owns only stay untouched. Prime Agent reads `models.json` when a session starts, so start a new session after connecting it. -Aside is per-account: its state lives under `~/.aside/u//` and opencodex -writes the catalog of whichever account Aside's own `accounts.json` names as -current. If that manifest is missing or unreadable the integration refuses rather -than guessing an account, because a guess on a multi-account machine would write -into a different account's catalog. Its managed block owns only -`providers.opencodex`, so your other Aside providers stay untouched. +Aside keeps a separate model catalog for each registered profile, including local profiles. OpenCodex lists +all registered profiles, including local profiles, and can synchronize them together or control +one profile at a time. Switching an integration never changes Aside's active account. A prior +Aside connection enables all profiles by default; individual exclusions survive later syncs. One caveat specific to Aside: the running app rewrites `models.json` itself, so fully quit and reopen Aside after applying, the same way Claude Desktop needs a @@ -159,6 +161,11 @@ changed value and calling it success. You will see the file named and nothing on disk will have moved. Editing that file by hand still works; it is only our automatic rewrite that declines. +TOML dates and times also refuse managed rewrites: the merge step would turn these +typed values into quoted strings. This includes values inside arrays and inline +tables. Quoted date strings remain supported; an unquoted date must be preserved +by editing the configuration manually. + **Pi, Kimi Code, Gajae Code, MiniMax Code, Prime Agent and the managed DSH integration only work against a loopback bind.** The first four have no config field for the `x-opencodex-api-key` header a non-loopback bind requires. DSH has a generic headers map, but rc.6 does not document that dedicated admission @@ -209,9 +216,25 @@ ocx integration client enable --client mcode ocx mcode ``` -Once connected, `ocx sync` also refreshes the owned MCode block with current context -windows and reasoning-effort ladders. It leaves missing, foreign-edited, unsafe, and -never-owned blocks untouched; re-enable explicitly when you intend to reconnect one. +Once connected, `ocx sync` refreshes owned MCode, Pi, and Aside catalogs with the current +model selection, context windows, and reasoning-effort ladders. Changes to model visibility, +provider selection, or presets also refresh connected Pi and Aside catalogs. Foreign-edited +or unsafe blocks stay untouched, as do previously owned blocks you removed manually. +An enabled Aside profile is an exception to the usual owned-only refresh: if its account +directory exists and it has never had an owned block, sync may create its first block when +that slot is empty. A prior Aside connection enables this behavior for all registered +profiles by default. Sync does not create missing account directories or replace manual blocks. +A refused or overlapping refresh is reported separately for each client. Start a new Pi +session or fully quit and reopen Aside to load the updated file. +Aside refresh requires a [compatible running proxy](#aside-profile-controls). + +If Models reports **“Model selection saved”** together with a client-refresh warning, the +selection is already saved; one or more client files could not be updated. The warning names +the affected client and Aside profile, when applicable, and explains the refusal. Open +**Integrations** to inspect that client or profile before starting a new session. Resolve the +reported issue, then retry `ocx sync`; an overlapping operation must finish first. If the +warning includes a backup path or says recovery did not finish, inspect that recovery state +before retrying. A successful selection save alone does not confirm client-file recovery. The separate MiniMax platform CLI (`mmx`) is not a file-toggle integration. Its text commands use MiniMax's Anthropic-compatible endpoint, so OpenCodex provides a @@ -236,3 +259,39 @@ decision to make. Client details were verified against each project's own configuration format; see the research notes in `devlog/_fin/260802_client_toggle_api/002_client_toggle_matrix.md` for what was checked and when. + +## Aside profile controls + +Aside profile controls and the Aside refresh performed by `ocx sync` require a running +ocx proxy that supports the Aside profile APIs. Updating the CLI alone does not update an +already-running proxy. If the proxy is unavailable or too old, the Aside operation cannot +complete; the CLI never falls back to writing Aside profile files locally. + +Upgrade the ocx installation used by the proxy, then restart the proxy (or start it if it +is stopped). Retry `ocx sync` or the profile command. After the profile files update +successfully, fully quit and reopen Aside so it loads the new catalogs. + +```bash +ocx integration client status --client aside --json +ocx integration client enable --client aside +ocx integration client disable --client aside --profile 1 +ocx integration client history --client aside --profile 1 +ocx integration client restore --client aside --profile 1 --op +``` + +The profile number is the account ID shown by the status command. Omitting `--profile` on an +Aside toggle applies the desired state to every registered profile. A per-profile change leaves +siblings unchanged. Desired sync settings are saved before file changes; actual state and any +refusal are reported for each profile. A partial bulk result is not an all-applied success and +the CLI exits nonzero. Undo restores the selected profile's synchronization intent as well as +its file, so a later sync does not silently reverse Undo. + +The [profile API](/reference/management-api/#aside-profile-controls) returns HTTP 200 for a +successful bulk operation and HTTP 207 with `ok: false` if any profile refuses. Inspect every +entry in `results`: successful profiles are not rolled back when another fails. Desired +settings remain saved, so retry after addressing the affected profile rather than assuming +the entire change failed. If saving those settings fails, no profile files are changed. + +Each profile has separate ownership and history. Existing user edits, unsafe paths and linked +catalogs are refused; the existing explicit overwrite and drift-confirmation controls remain +available. Fully quit and reopen Aside to load changed model files. diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md index 696f631a58..79e74e8e2d 100644 --- a/docs-site/src/content/docs/guides/model-ordering.md +++ b/docs-site/src/content/docs/guides/model-ordering.md @@ -23,7 +23,7 @@ priorities `i * N + j`, where `j` is the selector's zero-based position; a route rows are moved outside those selector groups. Codex still advertises only the first five picker-visible rows. -The relevant no-selector priorities are: +Without complete-picker ordering, the relevant no-selector priorities are: | Catalog entry | Priority | Source | | --- | ---: | --- | @@ -133,10 +133,41 @@ featured block: Listed routed rows appear in the configured order. A routed row omitted from the array keeps its normal priority, so it remains ahead of the `modelPickerOrder` display band; list every routed row whose relative position you want to control. A row also present in `subagentModels` keeps its -featured priority. Bare native and account-qualified native rows are not reordered by -`modelPickerOrder`; use `subagentModels` for those rows. +featured priority. With a routed-only list, native rows keep their normal positions. -`modelPickerOrder` never changes the `spawn_agent` candidate set. It changes only the -Codex-visible picker priority while opencodex retains each moved row's natural priority for -sub-agent selection. `disabledModels` and each provider's `selectedModels` remain visibility fields, +To order the complete picker, include a bare native id: + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +Listed rows appear first in array order, followed by unlisted rows in natural priority +order. Matching uses exact catalog ids: `gpt-5.6-sol` and `openai/gpt-5.6-sol` are separate +rows. Raw and encoded spellings of the same routed id are also accepted, with exact +matches taking precedence. Empty entries are ignored. Account-qualified rows need +their selector-qualified id in the list. + +### Migration note: native ids in existing orders + +Previously, native ids in `modelPickerOrder` were ignored. An existing list containing +a bare native id now activates complete-picker ordering, including featured rows. +Remove bare native ids to keep the previous routed-only behavior. Unset, empty and +routed-only lists retain their behavior; OpenCodex's natural-priority guidance candidate calculation is unchanged. + +`modelPickerOrder` preserves OpenCodex's natural-priority calculation of up to five preferred +candidates for subagent guidance. Each moved row retains its natural priority separately from +its native `priority`; changing picker order alone must not change that OpenCodex calculation. +It does not restrict eligibility for an exact-name model override: the native advertised list +is not an allowlist, and existing authentication, model/effort and backend constraints still apply. + +Native Codex uses native `priority` to select the first five eligible picker-visible models +advertised by `spawn_agent` on V1 and on V2 when model overrides are exposed. Those advertised +five may therefore change with picker order, even when OpenCodex's preferred candidates remain +unchanged. V1 receives no OpenCodex preferred-roster injection. V2 may additionally receive +OpenCodex's natural-priority guidance when the client catalog state permits; that guidance does +not reorder the native tool's advertised list. + +`disabledModels` and each provider's `selectedModels` remain visibility fields, not ordering controls. There is no separate `modelOrder`, `providerOrder`, or priority-map setting. diff --git a/docs-site/src/content/docs/guides/model-routing.md b/docs-site/src/content/docs/guides/model-routing.md index 59198f6815..b9f1a6b34d 100644 --- a/docs-site/src/content/docs/guides/model-routing.md +++ b/docs-site/src/content/docs/guides/model-routing.md @@ -93,10 +93,14 @@ Routing and catalog visibility are separate controls: `provider/model` requests fail, and `defaultModel` / `models[]` scans skip it. - `providerContextCaps` applies per-provider Codex-visible context caps. `contextCapValue` is the dashboard default (350,000 by default), but it does nothing by itself until a provider is - present in `providerContextCaps`. Changing the dashboard value re-points every enabled provider + present in `providerContextCaps`. Changing the dashboard value updates every enabled cap only when "apply to every routed provider" is toggled on; otherwise each provider keeps its own - cap. Caps only lower a known context window; they never raise one or change the upstream model's - actual limit. + cap. Ordinary known windows can only be lowered; native models that support a longer window + can expand up to their own supported ceiling. Caps never change the upstream model's actual limit. + Switching a cap off retains its selection in `providerContextCapValues`, including after reload; + switching it on restores that selection. A remembered selection never applies a limit while disabled. + Sending `{ "setAll": true }` without `value` enables all configured providers at the current + global value and replaces their remembered selections. ```json { diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 6a37cf8a70..255c0d8dc4 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -620,6 +620,12 @@ A provider is included when opencodex has a matching wire adapter, **not** based (AI Studio, Vertex, and Antigravity/Cloud Code Assist modes), `azure` / `azure-openai`, `kiro`, and `cursor`. A proprietary API without one of these implementations, such as native Amazon Bedrock, is not supported directly. + +Provider configuration selects the adapter; upstream transport selection is separate. Eligible +Responses traffic can use WSS with [explicit proxy routing](/reference/proxy-formats/#json-and-sse-output). +Invalid or unsupported WebSocket proxy settings fall back to HTTP/SSE, which uses Bun's HTTP +proxy rules rather than the WSS-specific `ALL_PROXY` fallback. + **GitHub Copilot** is an OAuth provider (`ocx login github-copilot`) that exchanges a GitHub device-flow login for a short-lived Copilot API token — not a pasted API key. **GitLab Duo** remains a key/subscription-token gateway on its OpenAI-compatible endpoint. **Cloudflare AI diff --git a/docs-site/src/content/docs/guides/routing-profile-editor.md b/docs-site/src/content/docs/guides/routing-profile-editor.md index 5cf5fc6d71..d53e0d3616 100644 --- a/docs-site/src/content/docs/guides/routing-profile-editor.md +++ b/docs-site/src/content/docs/guides/routing-profile-editor.md @@ -38,6 +38,14 @@ cap outcome. ## Dry-run a saved profile +Candidate capabilities use the effective provider configuration after registry +overrides are applied. Locality requirements (`localOnly` and `remoteAllowed`) +therefore use the effective upstream address. If that address cannot be classified, +the profile's `unknownEvidence.capability` setting decides eligibility. +An invalid provider configuration that cannot be resolved is always excluded with +`route-unavailable`, even when unknown capabilities are allowed. +Missing or disabled providers are also excluded with `route-unavailable` before scoring. + Select a saved profile and use **Dry-run evaluation** to add request evidence such as context-window size, tool use, image input, or structured output. Dry-run evaluates eligibility and scoring but never sends an upstream model request. Unsaved edits are not used by dry-run. Save the profile first so the displayed revision and evaluation refer to the same configuration. 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 9e8ac4a894..43c5c09d90 100644 --- a/docs-site/src/content/docs/guides/sub-agent-surface.md +++ b/docs-site/src/content/docs/guides/sub-agent-surface.md @@ -32,6 +32,20 @@ Start with **base**. Choose **v1** when cross-provider delegation must work pred only when you specifically want its newer session model across every catalog entry. ::: +## External task input + +Codex can deliver a task's initial input or follow-up in a result-shaped envelope +without a `call_id`. On translated routes, OpenCodex recognizes only the complete +`function_call_output` shape with nonblank `id`, `name` and `namespace` and supported +text/image output, then treats it as a user turn. This also starts the new conversation +boundary during continuation and clears pending reasoning from the preceding turn. +Generated developer guidance is placed before the current task in both parsed +messages and saved raw history, preserving the same order when that history is replayed. + +Malformed, empty, opaque or incomplete envelopes still fail validation. Actual tool +results keep their required `call_id`; native passthrough and compaction retain their +existing raw-input handling. See [the adapter contract](/reference/adapters/#external-task-input-on-translated-responses-routes). + ## How it works The selected mode controls the `multi_agent_version` field in every catalog entry Codex reads: @@ -133,8 +147,9 @@ opencodex fails safely instead of forwarding an empty or unreadable task: `error.code = "unreadable_encrypted_agent_task"` and does not echo the ciphertext. An eligible direct key-auth Responses provider that explicitly opts in with `allowEncryptedV2AgentTasks: true` instead receives the opaque ciphertext and bypasses this error. -- A combo considers only canonical native ChatGPT targets for that task, including retries. If none - is available, it returns the same 400 error. +- A combo first considers canonical native ChatGPT targets. If none is available or their attempts + are exhausted, enabled recovery may make the task readable for an available routed target. + Without successful recovery and an eligible target, unreadable ciphertext is never forwarded. - A readable plaintext task keeps the normal route and fallback behavior. Recovery options are to select a native ChatGPT child, explicitly trust a direct key-auth Responses @@ -151,12 +166,28 @@ authentication, another provider credential, or another Codex account. Only `aut `content-type` and `accept` are generated locally, and no other caller headers cross the boundary. It consumes quota, adds latency, briefly retains recovered plaintext in a bounded in-memory cache, and depends on undocumented ChatGPT backend behavior. Because a model returns the recovered text, -byte-for-byte fidelity is not guaranteed. It rejects generic/API-key proxy callers and preserves -`unreadable_encrypted_agent_task` on any failure. See +byte-for-byte fidelity is not guaranteed. It rejects generic/API-key proxy callers. Failed recovery before any native attempt returns +`unreadable_encrypted_agent_task`; after native attempts have failed, their last error is retained. See [Agent configuration: Encrypted v2 task recovery](/reference/configuration/agents/#encrypted-v2-task-recovery) for the full trust boundary and configuration. -Combo routing remains unchanged and continues to consider only canonical native ChatGPT targets for -encrypted tasks. +Combo routing prefers a selectable canonical native ChatGPT target for encrypted tasks. If none +is usable, or native authorization attempts are exhausted, an explicitly enabled recovery may +make the task readable for one available routed target. All recovery trust and no-persistence +guards above still apply; a configured but disabled or cooling native target does not block this +fallback, and cancellation never becomes an unreadable-task error. + +## Rejected encrypted history + +An upstream Responses server can reject encrypted parts in earlier function/custom-tool +output or `agent_message` content with `Encrypted function output content could not be decrypted or decoded.`. Before +any output is committed, opencodex replaces those parts with `[encrypted content omitted]` +and rebuilds the request once. The surrounding readable content stays intact; the +omitted content is not decrypted or recovered by this retry. + +If the rebuilt request receives another bare SSE `error` followed by EOF, both relay +modes preserve the error message in a `response.failed` terminal instead of reporting +`adapter_eof`. Other upstream `response.failed` events remain SSE failures. This history +recovery does not change the encrypted v2 task-delivery restrictions described above. ## Changing the mode diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 67224fbcf2..32cd198174 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -68,6 +68,13 @@ missing credential — the proxy did not recognise the request as loopback. Open the address the proxy prints on startup (usually `http://127.0.0.1:`), and prefer that exact host and port over a LAN IP or an alias. +## Dashboard layout + +Overview uses matching status cards and full-width settings rows. On wide screens, labels share +one column and model/effort controls share another. On narrower screens, controls move below their +labels in the same reading order. Long version labels are shortened visually; hover the version +badge or the version value to read the full value. + ## What you can do | Area | What it does | @@ -89,6 +96,35 @@ host and port over a LAN IP or an alias. | **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. | | **Stop** | Gracefully stop the proxy and installed background service, restore native Codex, and exit (`POST /api/stop`). On Windows with the Task Scheduler backend the dashboard refuses and asks you to run `ocx stop` instead: that wrapper can respawn the proxy after the task ends, and only a stop running outside this process can verify the restart window before restoring your client config. Nothing is changed when it refuses. | +### Account selection + +Account selection is shared with request routing. Selecting an OAuth account takes effect on the +next request even when a pool is enabled. A healthy selection is not replaced merely because +another generic OAuth account has more unused quota. If the account returns 429, automatic +failover can still select another usable account with the pool off. A committed automatic +selection updates the dashboard immediately; account changes do not wait for the quota refresh +timer. Requests already sent upstream retain their original credentials. + +### Filtering request logs + +Logs filters combine surface, intercepted requests, provider, exact model, status, time, +speed, and conversation ID over the currently loaded request ring. Provider and model +choices also include fallback attempts; model matching ignores case and surrounding spaces +but does not match partial names. Choices that disappear from the ring reset to All. + +Time windows cover the last 15 minutes, hour, or day and refresh every 30 seconds while the +Logs tab is active, even with auto-refresh off. Windows use the proxy timestamp from +the logs response and advance with elapsed browser time, so a different browser clock +does not shift the cutoff. Older proxies without that timestamp retain the browser-clock +fallback until a valid sample is available. Speed uses output tokens per second over the +full request duration: below 15, 15 to below 50, or at least 50. Unavailable speed values are +excluded when a speed filter is active. Success means 2xx; errors mean 4xx or 5xx. + +Active filters show the matching count out of the loaded total. Reset filters restores all +rows and returns keyboard focus to the All surface control; “No matching requests” +differs from an empty log ring. Use arrow keys or Home/End in +the surface selector. These controls do not query historical records beyond the loaded ring. + ### Linking to a section There is a single layout, so there is no layout switch to configure. Dashboard sections are @@ -121,6 +157,25 @@ new or that every upstream measurement was refreshed. The **Models** switches show final Codex visibility: a routed model is on only when its provider allowlist includes it (or no allowlist is set) and it is not disabled. Turning a model on reconciles both filters atomically; **All on** clears the provider allowlist so newly discovered models are also on. +### Managing models in a provider workspace + +In a provider’s **Models** tab, **Delete** removes the stored custom definition. An underlying +native or live-discovered model may then appear again, so the model count can stay the same. +**Hide** changes catalog visibility only: it does not delete the definition or change direct +routing policy. Use **Manage visibility in Models** to open the **Models** page and restore +visibility, even when the provider tab has no rows left. + +**Add** saves a custom definition; it does not clear an existing hide or provider selection rule. +A saved model can therefore remain hidden. If the model is already known, manage its visibility +in **Models**. A confirmed save with a failed catalog refresh is still saved: follow the refresh +message instead of adding it again. If the change cannot be confirmed, refresh the model state +before retrying. + +The provider’s model count is the number of unique, non-disabled entries in the current model +inventory returned by the server, before search or display truncation. It is not the provider +allowlist size, a live-discovery count, or proof that an entry was discovered upstream. Selection +badges and discovery information remain separate from that count. + ## Delegation picker vs spawn routing The Dashboard's **Sub-agent delegation** picker stores `injectionModel` and, optionally, @@ -270,7 +325,7 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Select the account for the next request and configure pool routing. | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | Read the effective account (including `pinned` and which account is `pinnedAccountId`) and set one account's selection order. | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Add a pool account through browser login. | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Read recent request metadata with optional tail, provider, and exact/class status filters. With `limit`/`offset`, paging walks backward from the newest row (`offset=0` returns the latest page). Response shape: `{ timeZone, total, logs }` where `total` is the filtered row count before pagination. | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Read recent request metadata with optional tail, provider, and exact/class status filters. With `limit`/`offset`, paging walks backward from the newest row (`offset=0` returns the latest page). Response shape: `{ timeZone, generatedAt, total, logs }` where `total` is the filtered row count before pagination. | | `GET` / `PUT /api/subagent-models` | Read or set the five featured `spawn_agent` override models. | | `POST /api/stop` | Stop the proxy/service, restore native Codex, and exit. Refused with `respawnable_service` on the Windows Task Scheduler backend, and with `service_state_unknown` when that state cannot be read; nothing is changed either way. | 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 6d45bf63af..8c9f433956 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -94,6 +94,59 @@ hook を削除します。Claude Desktop は独立した profile を使用し、 `claudeCode.nativePassthrough: false` でオフにでき、`claudeCode.anthropicBaseUrl` で別のアドレスを 指定できます。 +## リモートハブに接続した Claude Desktop + +接続中のマシンで `ocx claude desktop apply` または `ocx claude desktop` を実行すると、 +ハブの Desktop スナップショットを取得し、ハブの origin と発行済みモデル ID をそのまま +ローカル Desktop 設定に書き込みます。ローカルの別名は生成しません。static/hybrid は +モデル一覧もコピーし、discovery-only は一覧を埋め込まずハブの origin を使います。 + +プロファイル、ファミリー、デフォルトはハブ側で管理します。ハブで変更してからクライアントで +再適用し、Desktop でモデルを選び直してください。以前クライアントだけで作成した別名も +再適用・再選択が必要です。`show`、ローカル編集、import/export はローカル設定だけを扱います。 +接続中の `ocx claude desktop import --apply` は未対応で、保存前に拒否します。 +`--apply` なしの import はローカル操作のままです。 + +取得には既存の接続のデータ用認証情報を使い、管理者トークンもプロファイルのアップロードも +不要です。古いハブが未対応の場合、不正な応答や空の Desktop 一覧の場合は適用に失敗します。 +ローカル一覧やループバック URL への代替は行いません。ハブを更新・設定して再適用してください。 + +この別名変更では、[#3719](https://github.com/lidge-jun/opencodex/issues/3719) の `thinking` / `redacted_thinking` 再送とプロンプトキャッシュの +別件は修正しません。プロキシの接続認証だけではネイティブ Anthropic パススルーは有効に +なりませんが、変換された Anthropic ルートでもキャッシュは利用できます。再送の保持と +キャッシュヒットの比較は別の作業です。 + +### キーのローテーション、復旧、切断 + +キーのローテーションと復旧では、ローカル接続の認証情報とともに接続管理下の Desktop +プロファイルのキーも更新します。キー移行のための手動再適用は不要です。モデル ID、 +ファミリー、デフォルト、現在のプロファイル選択は維持し、管理プロファイルの再選択や無効な +統合の再有効化は行いません。CLI JSON の `rotation: "committed"` は新しいキーが有効に +なったことを示します。`rotation: "rolled_back"` は以前のキーを保持または復元したことを +示し、新しいキーの確定や以前のキーの失効を意味しません。不確実・未完了の復旧は成功として +報告しません。 + +最初の接続中の適用で、復元対象の元の管理設定と選択を保存します。再適用やキー更新で +この最初の記録を置き換えません。`ocx disconnect` は接続が管理する設定を復元し、ユーザーが +追加したフィールドや他のプロファイルを保持します。管理プロファイルがまだ選択されている +場合だけ元の選択に戻し、その後選んだ別の有効なプロファイルは変更しません。新規プロファイルに +ユーザー設定が追加されていれば削除せず、読み込み可能な標準モードで残します。 +`--keep-catalog` が保持するのはカタログであり、Desktop の接続キーではありません。 + +元の設定記録がない旧管理プロファイルも、現在のハブと認識済みの接続キーへの所属が明確なら +移行できます。apply、ローテーション・復旧、直接の disconnect で処理でき、新しいフラグや +事前の再適用は不要です。元の設定が未記録のため切断時に標準モードを使うという警告を表示します。 +接続所有のゲートウェイ設定だけを除去し、ユーザーフィールドと別の有効な選択は保持します。 +この結果は元の復元ではなく標準モードへのフォールバックとして報告します。 + +管理設定の競合、不明な認証情報、破損した復元記録は上書きせず報告します。中断した処理は +同じ接続について再開でき、新しい接続を消したり復元前に完了と報告したりしません。 +切断前に保留中のキー復旧を完了し、切断を再試行するときは同じカタログ保持設定を使ってください。 + +適用、ローテーション・復旧、復元後は Claude Desktop を完全に終了して開き直してください。 +ディスク上の更新では実行中のアプリが保持するキーは変わらず、自動終了・再起動もしません。 +ローカルの切断はハブのキーや外部コピーを自動失効・削除しません。必要ならハブで別途失効させてください。 + ## /model ピッカー("From gateway") Claude Code 2.1.129 以降は `GET /v1/models?limit=1000` でゲートウェイモデルを探し、デフォルトの `/model` @@ -126,6 +179,14 @@ v2 エイリアスはエスケープを展開します。読みやすい形式 **モデル解決順序:** `[1m]` 標識の削除 → 読みやすいエイリアスのデコード → Desktop ハッシュエイリアスのデコード → `modelMap` の完全一致 → 日付を削除した値との一致(`-20250514` 削除) → パススルー順です。 +解決できない日付形式の Desktop ID は、モデル検出に含まれていない実際のネイティブモデル +かもしれません。判断材料が足りず ID を解決できない場合、Messages と count-tokens は固定エラー +`desktop_model_mapping_unavailable`と HTTP 503 を返します。これはモデルが無効だという判定ではありません。 +不明な旧ハッシュ別名は引き続き HTTP 400 で拒否します。どちらも日付を除去したり別ルートへ +フォールバックしたりしません。既知の ID、登録済みマッピング、正確な `modelMap` 一致、 +認識済みの実ネイティブ ID の処理は変わりません。モデル検出を更新するか接続先ハブの +プロファイルを再適用してから試してください。再試行だけで解決する保証はありません。 + 各項目には `gemini-3-pro (gemini)` のような表示名と公式 `ModelInfo` 形式の完全なモデル能力 (推論負荷段階、thinking 型)が含まれます。実際の Anthropic モデルは両画面で正式 ID を維持します。 @@ -221,6 +282,14 @@ Anthropic パススルーはそのまま維持します。 照合順序: 検索エイリアス → 完全一致 ID → 日付接尾辞を削除した ID(`-20250514`) → パススルー順です。 +解決できない日付形式の Desktop ID は、モデル検出に含まれていない実際のネイティブモデル +かもしれません。判断材料が足りず ID を解決できない場合、Messages と count-tokens は固定エラー +`desktop_model_mapping_unavailable`と HTTP 503 を返します。これはモデルが無効だという判定ではありません。 +不明な旧ハッシュ別名は引き続き HTTP 400 で拒否します。どちらも日付を除去したり別ルートへ +フォールバックしたりしません。既知の ID、登録済みマッピング、正確な `modelMap` 一致、 +認識済みの実ネイティブ ID の処理は変わりません。モデル検出を更新するか接続先ハブの +プロファイルを再適用してから試してください。再試行だけで解決する保証はありません。 + ## サイドカーマトリクス: ウェブ検索と画像理解 ルーティングモデルごとに使えるホスト型ツールと画像サポート範囲が異なります。opencodex はメインモデルが diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index 06cd590084..d1d977b35c 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -197,8 +197,14 @@ ocx sync-cache 空または省略すると、検出されたすべてのモデルが公開されます。ホワイトリストにない ID はカタログに到達しません。 2. **`disabledModels`** (トップレベル) — カタログと `/v1/models` の両方からモデルを非表示にし、反転します 裸のネイティブ GPT スラッグを `visibility: "hide"` にします。 -3. **`liveModels: false` と空の `models`** — ライブ検出がオフで、`models` が空の場合、または -省略すると、opencodex はそのプロバイダーのルーティング モデルを公開しません。 +3. **`liveModels: false`** — `liveModels: false` で `models` が空または省略されている場合、初期一覧には設定済みの + `defaultModel`、`retainModels` の順で ID を追加し、重複は最初の出現だけを残します。 + 空でない `models` が明示されている場合は、`models`、`retainModels` の順になり、別の + `defaultModel` を暗黙に追加しません。そのモデルも `models` または `retainModels` に明示すれば + 含められます。どのフィールドにも ID がなければ初期一覧は空です。この順序は最終的なピッカーの + 表示順を保証しません。`selectedModels`、`disabledModels`、プロバイダーの無効化は引き続き適用されます。 + `authMode: "forward"` は別の分岐を維持し、このルーティング用の静的一覧を使いません。 + これらの規則はライブ検出失敗時のフォールバックを変更しません。 4. **Cursor `GetUsableModels`** — Cursor アダプターはその protobuf を通じてモデルを検出します。 `/models` ではなく `GetUsableModels` RPC であるため、カーソル側の変更により、他のプロバイダーとは独立して表示される ID が変更される可能性があります。 5. **キャッシュと `ocx sync`** - ライブ カタログは約 5 分間キャッシュされます (`modelCacheTtlMs`、 diff --git a/docs-site/src/content/docs/ja/guides/model-ordering.md b/docs-site/src/content/docs/ja/guides/model-ordering.md index 6108c08771..d9c782b3fb 100644 --- a/docs-site/src/content/docs/ja/guides/model-ordering.md +++ b/docs-site/src/content/docs/ja/guides/model-ordering.md @@ -22,6 +22,8 @@ account-qualified native id にはその selector の `i * N + j` が使用さ selector がない場合の priority は次のとおりです。 +以下の優先順位表と例は、ピッカー全体の並び替えを有効にしていない場合のものです。 + | カタログ項目 | Priority | 根拠 | --- | ---: | --- | | `subagentModels[i]` | `i`(`0` から `4`) | `src/codex/catalog/sync.ts` の featured rank map | @@ -112,6 +114,44 @@ account selector がある場合、5 項目の制限は bare native の選択が 場合は 1 つの bare native が複数の selector-qualified 行に展開されるため、設定した選択肢と公開 される行は必ずしも一対一ではありません。 -現在 `OcxConfig` には一般 `modelOrder`、`providerOrder`、priority map 設定はありません。サポートされるソート -フィールドは `subagentModels` です。`disabledModels` と各プロバイダーの `selectedModels` は公開 -フィールドです。そのため残りのピッカー順序を変えるには設定変更ではなくコード動作の変更が必要です。 +`modelPickerOrder` はピッカーの表示順だけを指定します。ルーティング ID +`/` だけを指定した場合、一覧にある非 featured 行は指定順の表示帯 +(`1000 + i`)に並びます。一覧にないルーティング行は通常の優先順位を保ち、この表示帯より前に +残ります。`subagentModels` にも含まれる行は featured の優先順位を保ち、ネイティブ行の位置も変わりません。 +相対的な順序を指定したいルーティング行はすべて一覧に含めてください。 + +ピッカー全体を並び替えるには、`/` を含まない、空でも空白だけでもないカタログ ID +(例:`gpt-5.6-sol`)を含めます。 + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +指定した行が配列の順序で先頭に並び、未指定の行は本来の優先順位でその後に続きます。 +カタログ ID は完全一致で照合します。`gpt-5.6-sol` と `openai/gpt-5.6-sol` は別の行です。 +同じルーティング ID の未エンコード表記とエンコード済み表記も照合できますが、完全一致が優先されます。 +空の項目と空白だけの項目は無視します。アカウント別の行には selector を含む完全な ID を指定してください。 + +### 移行時の注意:既存の一覧に含まれるネイティブ ID + +以前は `modelPickerOrder` 内の bare native ID が無視されていました。既存の一覧にこのような ID が +あると、今後は featured 行を含むピッカー全体の並び替えが有効になります。従来のルーティング行だけの +動作を保つには、bare ID を取り除いてください。未設定、空、空白だけ、ルーティング ID だけの一覧は +従来どおり動作します。 + +`modelPickerOrder` は、自然な優先順位から最大 5 件の推奨候補を選ぶ OpenCodex の +サブエージェント向けガイダンス計算を保持します。移動した各行の自然な優先順位はネイティブの +`priority` とは別に残り、ピッカー順だけを変えてもこの計算結果は変わりません。 +正確なモデル名を指定する override の利用資格を制限するものでもありません。広告リストは許可リストではなく、 +認証、モデル、effort、バックエンドに関する既存の制約は引き続き適用されます。 + +ネイティブ Codex はネイティブの `priority` に従い、利用可能でピッカーに表示されるモデルの先頭 5 件を +`spawn_agent` に広告します。これは V1 と、モデル override を公開している V2 に当てはまります。 +そのため、OpenCodex の推奨候補が同じでも、ピッカー順を変えると広告される 5 件は変わる場合があります。 +V1 には OpenCodex の推奨候補リストを注入しません。V2 にはクライアントのカタログ状態が許す場合に +自然な優先順位に基づくガイダンスを追加できますが、ネイティブツールの広告リストは並び替えません。 + +`disabledModels` と各プロバイダーの `selectedModels` は +表示の有無を制御するフィールドです。別の `modelOrder`、`providerOrder`、priority map 設定はありません。 diff --git a/docs-site/src/content/docs/ja/guides/model-routing.md b/docs-site/src/content/docs/ja/guides/model-routing.md index 3fb9e5f55f..7dda1864c0 100644 --- a/docs-site/src/content/docs/ja/guides/model-routing.md +++ b/docs-site/src/content/docs/ja/guides/model-routing.md @@ -86,11 +86,15 @@ model ID は変更しません。`openai-apikey/` は API key transport - `provider.disabled: true` のプロバイダーはカタログ探索から除外されます。明示的 `provider/model` リクエストは 失敗し、`defaultModel` / `models[]` 検査でもスキップします。 - `providerContextCaps` はプロバイダーごとに Codex に表示するコンテキスト上限を指定します。 - `contextCapValue` はダッシュボードが併用する値でデフォルトは 350,000 です。ただしこの値だけを設定しても - 変化はなく、`providerContextCaps` にプロバイダーが含まれていて初めて適用されます。ダッシュボードの値を - 変更すると、「すべてのルーティング対象プロバイダーに適用」がオンになっている場合にのみ、有効なすべての - プロバイダーに再適用されます。それ以外の場合、各プロバイダーは独自の上限を維持します。既知のコンテキスト - サイズを下げるだけで、上げたり上流モデルの実際の上限を変えたりはしません。 + `contextCapValue` はダッシュボードの既定値(350,000)です。この値だけでは上限は適用されず、 + `providerContextCaps` にプロバイダーが含まれている必要があります。ダッシュボードの値を変更すると、 + 「すべてのルーティング対象プロバイダーに適用」がオンの場合に限り、有効な上限をすべて更新します。 + オフの場合は各プロバイダーの上限を保持します。通常の既知のウィンドウは縮小のみ可能ですが、 + 長いウィンドウに対応したネイティブモデルは、そのモデルが対応する上限まで拡張できます。 + 上流モデルの実際の制限は変わりません。上限を無効にしても選択値は `providerContextCapValues` に + 保存され、再読み込み後も残ります。再び有効にすると選択値を復元します。無効な間は保存値を制限として + 適用しません。`value` なしの `{ "setAll": true }` は、設定済みの全プロバイダーの上限を現在の + グローバル値で有効にし、保存された選択値も置き換えます。 ```json { diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md index 4b7cbd70ad..310baa4a5a 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -48,6 +48,14 @@ bun run dev:gui | **ストレージ** | CODEX_HOME のディスク内訳(セッション、アーカイブ、DB、添付)を読み取り専用で表示。任意のアーカイブクリーンアップ: 最古 N% をプレビューし、既定では `CODEX_HOME/.trash` へ隔離、または明示チェックで完全削除。**自動クリーンアップ方針**はオプトインで**既定 OFF**(`storageCleanupPolicy.enabled`)。Storage ページでしきい値/目標/スケジュール/モードを設定するか **今すぐ実行**。隔離エントリは Storage ページから復元可能(JSONL + スレッド)。アクティブセッションは読み取り専用。最新/アクティブな `state_*.sqlite` がロック中はクリーンアップと復元を拒否。 | | **停止** | プロキシとインストールされたバックグラウンドサービスを正常終了しネイティブ Codex を復元した後終了します(`POST /api/stop`)。ただし Windows のタスク スケジューラ バックエンドではダッシュボードが拒否し、`ocx stop` の実行を促します。タスク終了後もラッパーがプロキシを再起動しうるため、クライアント設定を戻す前にその再起動区間を確認できるのはプロキシの外で動く stop だけです。拒否されたときは何も変更されません。 | +### リクエストログの絞り込み + +Logsではサーフェス、インターセプトされたリクエスト、プロバイダー、完全なモデル名、ステータス、時間、速度、会話IDを組み合わせて、読み込み済みログを絞り込みます。選択肢にはフォールバック試行も含まれます。モデル名は大文字小文字と前後の空白を無視しますが、部分一致ではありません。ログから消えた選択肢は全件に戻ります。 + +時間は直近15分・1時間・1日で、Logsタブでは自動更新をオフにしても30秒ごとに更新します。速度はリクエスト全体の時間あたりの毎秒出力トークン数で、15未満、15以上50未満、50以上です。速度フィルター中は測定不能な行を除外します。成功は2xx、エラーは4xx/5xxです。 + +一致件数と読み込み総数を表示し、リセットで全行を復元します。一致なしと空ログを区別します。サーフェスは矢印キーとHome/Endで操作できます。読み込み範囲外の履歴は検索しません。 + ### セクションへのリンク レイアウトは 1 つだけなので、切り替える設定はありません。代わりに Dashboard の各セクションに URL があります。`#dashboard` は Overview、`#dashboard/providers` と `#dashboard/models` は残りの 2 つです。再読み込み・ブックマーク・戻る操作のいずれでも、表示していたセクションが保たれます。**Logs** も `#logs` と `#logs/debug` で同じように動作します。以前の `#providers/workspace` のブックマークは `#providers` に移動します。 @@ -59,6 +67,24 @@ bun run dev:gui **モデル** スイッチは Codex での最終的な表示状態を示します。ルーティングモデルはプロバイダーの allowlist に含まれる(または allowlist がない)うえで、無効化されていない場合だけオンになります。オン操作は両方のフィルターを原子的に調整し、**すべてオン** は allowlist を解除して新しいモデルも含めます。 +### プロバイダー画面でモデルを管理する + +プロバイダーの **モデル** タブで **Delete(削除)** を選ぶと、保存されたカスタム定義を削除します。 +元のネイティブモデルやライブ検出されたモデルが再び表示され、モデル数が変わらない場合があります。 +**非表示** はカタログの表示だけを変更し、定義の削除や直接ルーティングのポリシー変更は行いません。 +**モデルで表示を管理** から **モデル** ページを開き、表示を復元できます。プロバイダーのタブが +空になっていても、この操作は利用できます。 + +**追加** はカスタム定義を保存しますが、既存の非表示設定やプロバイダーの選択ルールを解除しません。 +保存後もモデルが非表示のままになることがあります。すでに登録されたモデルの表示は **モデル** で +管理してください。保存が確認できた場合、カタログ更新に失敗しても保存自体は完了しています。 +再追加せず、更新の案内に従ってください。変更を確認できない場合は、モデルの状態を再読み込みしてから +再試行してください。 + +プロバイダーのモデル数は、サーバーが返した現在のモデル一覧のうち、無効でない重複なしの項目数です。 +検索や表示件数の制限を適用する前に数えます。許可リストの件数やライブ検出件数ではなく、上流で検出した +項目であることを示す値でもありません。選択バッジと検出情報は、この件数とは別に扱います。 + ## 委任セレクターとスポーンルーティングの違い ダッシュボードの **サブエージェント委任** セレクターは `injectionModel` とオプションの `injectionEffort` を @@ -149,7 +175,7 @@ GUI はプロキシの JSON 管理 API を使うシンクライアントです | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | 次のリクエストで使うアカウントとプールルーティングポリシーを設定します。 | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | 実効アカウント(固定中かどうかを示す `pinned` と、固定されているアカウントを示す `pinnedAccountId` を含む)を読み、アカウント 1 件の選択順序を設定します。 | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | ブラウザログインでプールアカウントを追加します。 | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | tail、プロバイダー、正確な状態コードまたは状態等級で最近のリクエストメタデータを参照します。`limit`/`offset` は最新行から過去方向にページングします(`offset=0` が最新ページ)。応答は `{ timeZone, total, logs }` で、`total` はページング前の一致件数です。 | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | tail、プロバイダー、正確な状態コードまたは状態等級で最近のリクエストメタデータを参照します。`limit`/`offset` は最新行から過去方向にページングします(`offset=0` が最新ページ)。応答は `{ timeZone, generatedAt, total, logs }` で、`total` はページング前の一致件数です。 | | `GET` / `PUT /api/subagent-models` | `spawn_agent` に優先公開するモデル 5 つを読むか設定します。 | | `POST /api/stop` | プロキシ/サービスを停止しネイティブ Codex を復元した後終了します。Windows タスク スケジューラ バックエンドでは `respawnable_service`、その状態を読み取れない場合は `service_state_unknown` で拒否し、どちらの場合も何も変更されません。 | diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index d6e9425b52..b187ff7fd3 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -156,6 +156,12 @@ Codex のローカル モデル ピッカー キャッシュを無効にし、 opencodex を、ログイン時に自動起動し、クラッシュ時に自動再起動するログイン管理バックグラウンド サービス (macOS **launchd**、Linux **systemd ユーザー ユニット**、Windows **タスク スケジューラ**) として実行します。サービスは `OCX_SERVICE=1` を設定して実行されるため、再起動によって Codex 設定が変更されることはありません。 +Windows タスク スケジューラでインストールするサービスは、通常のプロセス優先度(`Priority=4`)を使用します。 +以前のバックグラウンド優先度(`7`。省略時もスケジューラの既定値は `7`)では、CPU の競合により +ヘルスチェックへの応答が遅れ、プロセスが動作中でもトレイに Offline と表示されることがあります。 +アップグレード後に `ocx service repair` を実行すると、この登録済み優先度を移行してサービスを再起動します。 +移行時に UAC の承認が必要になる場合があります。すでに通常または高優先度の場合、優先度だけを理由に再登録しません。 + |サブコマンド |アクション | | --- | --- | |なし |未インストールなら作成して開始し、既存なら更新して再起動します。正常な Windows タスク スケジューラ定義は再利用しますが、古い定義は再登録され、昇格が必要になる場合があります。 | 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 f0453461a4..2b185b81c4 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ja/reference/configuration/agents.md @@ -53,7 +53,7 @@ V1 ガイダンスは、`max` または `ultra` でのみプロアクティブ 拒否し、ロールをスキップします(#1190)。TOML 内のレガシー `model_fallback` 行は後方互換性の ために引き続き読み取られますが、`ocx doctor` がそれをフラグ付けします。 -opencodex は、無効、ルーティング不能、異常、冷却期間、またはクォータしきい値の候補をスキップします。可用性スナップショットは `subagentModelFallbackPollMs` に対してキャッシュされます。暗号化された子タスクでは、チェーンを正規のネイティブ ChatGPT ターゲットと、`allowEncryptedV2AgentTasks: true` で明示的に信頼された直接のキー認証 Responses ルートに制限します。暗号化されたペイロードを処理できる対象がない場合、読み取り不可能な暗号文を別の場所へ送らず、リクエストは失敗します。コンボは引き続き正規のネイティブ対象だけを使用します。 +opencodex は、無効、ルーティング不能、異常、冷却期間、またはクォータしきい値の候補をスキップします。可用性スナップショットは `subagentModelFallbackPollMs` に対してキャッシュされます。暗号化された子タスクでは、チェーンを正規のネイティブ ChatGPT ターゲットと、`allowEncryptedV2AgentTasks: true` で明示的に信頼された直接のキー認証 Responses ルートに制限します。暗号化されたペイロードを処理できる対象がない場合、読み取り不可能な暗号文を別の場所へ送らず、リクエストは失敗します。コンボはまず利用可能な正規ネイティブ対象を試し、選択できるネイティブ対象がなく `agentTaskRecovery` が有効な場合、暗号化された `NEW_TASK` をルーティングされたコンボ送信の前に一度だけ復旧します。 ```json { diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index bd33d34a3f..d4bfc49a6f 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -27,8 +27,9 @@ GUI で登録または OAuth ログインが完了すると、Models ページ | `providers` | `Record` | — |プロバイダー名からプロバイダー設定へのマップ。 | | `openaiProviderTierVersion?` | `2` |移行によって設定される |単一のオプション対応 OpenAI プロジェクションを完了としてマークします。 | | `disabledModels?` | `string[]` | — | Codex catalog と `/v1/models` から非表示にする model。直接の proxy 呼び出しはブロックしません。routed id は一覧から削除されます。account-qualified native id は該当する selector row だけを非表示にし、bare native GPT id は bare row とその model の全 account-selector row を非表示にします。Models ページに表示されるのは bare native 行と routed 行だけです。selector-qualified 行を 1 つだけ非表示にするには、この設定フィールドを直接編集してください。 | -| `providerContextCaps?` | `Record` | `{}` |プロバイダーごとの Codex に表示されるコンテキストの上限。キャップは既知のコンテキスト ウィンドウを下げるだけです。 | -| `contextCapValue?` | `number` | `350000` |ダッシュボードのコンテキストキャップ コントロールで使用される既定値。「すべてのルーティング済みプロバイダーに適用」がオンになっている場合のみ、変更によってすべてのルーティング済みプロバイダー(`providerContextCaps` エントリがまだないプロバイダーを含む)に値が適用されます。それ以外では各プロバイダーは独自のキャップを保持します。 | +| `providerContextCaps?` | `Record` | `{}` | プロバイダーごとの有効なコンテキスト上限。通常のウィンドウは縮小されます。長いウィンドウに対応したネイティブモデルは、そのモデルが対応する上限まで拡張できます。 | +| `providerContextCapValues?` | `Record` | `{}` | プロバイダーごとに最後に選択した上限。無効にしても保持され、この値だけで上限が有効になることはありません。有効な値が保存済みの値より優先されます。 | +| `contextCapValue?` | `number` | `350000` | 初回の有効化で使う既定値。再び有効にすると、そのプロバイダーの選択値を復元します。`setAll: true` とともにグローバル値を変更すると、有効な上限だけを更新します。値を指定せずに `setAll: true` を送ると、設定済みの全プロバイダーの上限を現在のグローバル値で有効にします。 | | `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex プール アカウントのメタデータは Codex Auth によって管理されます。秘密は`codex-accounts.json`に別に住んでいます。 | | `pausedCodexAccountIds?` | `string[]` | `[]` |再開するまでプールの選択から除外されるアカウント (一時停止時のメイン `__main__` アカウントを含む)。 | | `codexAccountNamespaces?` | `Record` | — | 任意の公開 model selector を保存済み Codex アカウント target に対応付ける任意の map。account-qualified picker row が有効な場合、target が存在する各 selector は Codex picker に個別の `/` row を追加し、各 row はそのアカウントだけを使用します。selector が 1 つでも有効な場合、bare native row は picker で非表示になりますが、明示的に無効化されない限り id は引き続き routing でき、raw `/v1/models` にも表示されます。 | @@ -81,7 +82,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic キーのヘッダー スタイル。デフォルトはネイティブ `x-api-key` です。キー認証 `anthropic` プロバイダーにのみ有効です。 | | `apiKeyPool?` | `ApiKeyPoolEntry[]` |マルチキープール。 `apiKey` はアクティブなエントリをミラーリングします。各項目には `id`、`key`、オプションの `label`、およびオプションの数値 `addedAt` があります。 | | `defaultModel?` | `string` |このプロバイダーが明示的なモデルなしで選択された場合に使用されるモデル。 | -| `models?` | `string[]` |シード/フォールバック モデルのリスト。 `liveModels: false` では、発見されたモデルはこれらのみです。 | +| `models?` | `string[]` | 初期/フォールバックモデル一覧。`liveModels: false` で `models` が空でなければ、その後に `retainModels` を追加します。`models` が空または省略されている場合は、設定済みの `defaultModel`、`retainModels` の順に初期一覧を作り、重複 ID は最初の出現だけを残します。 | | `liveModels?` | `boolean` |開始/同期時にライブ カタログをフェッチします (デフォルトは `true`)。カスタムプロバイダーは `${baseUrl}/models` を使用します。組み込みはレジストリ URL とフィルターを使用する場合があります。 | | `selectedModels?` | `string[]` |検出後のカタログ許可リスト。空でない場合は、それらの ID のみが公開されます。空または省略すると、検出されたすべてのモデルが公開されます。 | | `modelDisplayNames?` | `Record` | このプロバイダーの正確なネイティブモデル ID をキーにした、永続的な表示専用ラベルです。大文字と小文字は区別されます。ラベルはプロバイダーカタログのメタデータより優先され、認証、アダプター、ルーティング、課金、上流リクエストには影響しません。マップは検出上限と同じ 2,000 件までです。 | @@ -354,7 +355,16 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ ## 静的モデルのホワイトリスト -`models` のみを公開するように `liveModels: false` を設定します。 `models` が空であるか省略されている場合、プロバイダーはルーティングされたモデルを公開しません。ライブ ディスカバリは、キャッシュする前に 4 MiB または 2,000 を超える生のモデル行を拒否します。組み込みのプリセットは下限を使用し、チャットに適した行にフィルターをかけることができます。サイズが大きすぎる、または形式が正しくない結果は、古い/構成されたフォールバックに続きます。ゼロに適格な有効な結果は引き続き権威を持ち、暗黙的に置き換えられたり切り捨てられたりすることはありません。 +`liveModels: false` で `models` が空または省略されている場合、初期一覧には設定済みの +`defaultModel`、`retainModels` の順で ID を追加し、重複は最初の出現だけを残します。 +空でない `models` が明示されている場合は、`models`、`retainModels` の順になり、別の +`defaultModel` を暗黙に追加しません。そのモデルも `models` または `retainModels` に明示すれば +含められます。どのフィールドにも ID がなければ初期一覧は空です。この順序は最終的なピッカーの +表示順を保証しません。`selectedModels`、`disabledModels`、プロバイダーの無効化は引き続き適用されます。 +`authMode: "forward"` は別の分岐を維持し、このルーティング用の静的一覧を使いません。 +これらの規則はライブ検出失敗時のフォールバックを変更しません。 + +ライブ ディスカバリは、キャッシュする前に 4 MiB または 2,000 を超える生のモデル行を拒否します。組み込みのプリセットは下限を使用し、チャットに適した行にフィルターをかけることができます。サイズが大きすぎる、または形式が正しくない結果は、古い/構成されたフォールバックに続きます。ゼロに適格な有効な結果は引き続き権威を持ち、暗黙的に置き換えられたり切り捨てられたりすることはありません。 検出を実行する必要があるが、選択した ID のみが Codex および `/v1/models` に表示される必要がある場合は、`selectedModels` を使用します。ダッシュボードには、後で許可リストを変更できるように、検出された完全なリストが保持されます。 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 86b6cbfa5f..b1dd316c7f 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/server.md +++ b/docs-site/src/content/docs/ja/reference/configuration/server.md @@ -167,3 +167,7 @@ Anthropic OAuth サイドカーは、opencodex の既存のクロード コー `runtimeRole` の既定値は `standalone` です。hub は `hub.managementPublicOrigin`、loopback 限定の `hub.managementIngress`(未設定時 `enabled:false`)、正確な `remoteGui.allowedTailscaleUsers`(未設定時は空)を使います。クライアントキーは `config.json` ではなく `service-api-token` に保存され、更新中だけ `service-api-token.prev` が存在する場合があります。使用量はミラーリングされません。 `remoteGui.allowInsecureHttp` は、古い strict-schema 設定を読み込むためだけに残された非推奨の no-op です。設定から削除してください。pairing grant は loopback または認証済み HTTPS でのみ受け付けられ、この値を `true` にしても平文 HTTP pairing は再び有効になりません。 + +## Codex クォータのネットワーク診断 + +メイン Codex アカウント行の `quotaRefresh` はクォータ取得の診断情報であり、残量やモデルへのアクセス権を示すものではありません。キャッシュ利用時や取得を行わない場合は省略されることがあります。取得には操作中のシェルではなく、実行中のプロキシサービスの環境が使われます。`proxy` 未設定では既存の環境を維持し、`"auto"` は起動時に Windows の静的プロキシ設定だけを読みます。PAC/WPAD、SOCKS のみの設定、実行中の変更は自動反映されません。TUN での成功だけでは HTTP プロキシ経路の正常性は確認できません。[コマンドと状態の説明(英語)](/reference/configuration/server/#codex-quota-network-diagnostics)を参照してください。 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 8caca6d535..fecada7216 100644 --- a/docs-site/src/content/docs/ja/reference/management-api.md +++ b/docs-site/src/content/docs/ja/reference/management-api.md @@ -144,12 +144,15 @@ Authorization: Bearer | `GET /api/models` |ダッシュボード/CLI モデルの行を返す |収集が飽和したときの `catalog_busy` | | `GET /api/client-config?client=...` |サポートされているファイル連携の読み取り専用クライアント設定を作成する | 400 クライアントがサポートされていません。 503 カタログは利用できません | | `PUT /api/disabled-models` |共有の無効モデル リストを置き換える | 400 無効な JSON | -| `PUT /api/model-visibility` |プロバイダーレベルまたはモデルレベルの可視性をアトミックに変更 | 400 プロバイダー、スコープ、ターゲット、または本文が無効です。 +| `PUT /api/model-visibility` |プロバイダーレベルまたはモデルレベルの可視性をアトミックに変更 | 400 プロバイダー、スコープ、ターゲット、または本文が無効です。; 409 `initial_model_selection_pending` (モデル一覧を更新してから再試行してください。) | | `GET, POST /api/custom-models` |カスタム モデルをリストするか追加する | 400 個の無効なフィールド。 404 プロバイダーがありません。 409 複製モデル | | `PUT, DELETE /api/custom-models/{id}` | 1 つのカスタム モデルを編集または削除する | 400 個の無効な ID/フィールド。 404 が見つかりません。 409 複製モデル | | `GET, PUT /api/selected-models` | プロバイダーの許可リストと可用性を読む、または許可リストを置き換える | 400 プロバイダー/本文の不足; 404 不明なプロバイダー; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | プリセット情報を読む、または preset/all/custom モードを選ぶ | 400 不正なモードまたは未提供のプリセット; 404 不明なプロバイダー; PUT 409 `initial_model_selection_pending` | +手動モデルは、Models ダッシュボードで provider と model ID が一致する行を置き換えます。OpenAI の手動行は `openai/` を維持し、表示状態を変更できます。削除すると、アカウント修飾子のないネイティブ行が復元されます。アカウント修飾付きのネイティブ行は別に保持されます。ネイティブルートやアカウントの権限は変更しません。OpenAI の非ネイティブ表示対象は、設定済みの手動モデルと一致する必要があります。 + + 信頼できる初回モデル一覧が確定するまで、有効な `PUT /api/selected-models` と `PUT /api/model-presets` も HTTP 409 とコード `initial_model_selection_pending` を返します。`GET /api/models` などでモデル一覧を更新し、取得に成功してから再試行してください。 ### OAuth アカウント、プロバイダー キー、およびデータプレーン キー @@ -188,6 +191,16 @@ Authorization: Bearer | `GET, PUT /api/provider-context-caps` |グローバル、全プロバイダー、または 1 つのプロバイダーのコンテキスト キャップを読み取りまたは更新します。 400 無効なリクエスト。 404 不明なプロバイダ | | `GET /api/provider-presets` |ランタイム レジストリから派生した GUI プロバイダー プリセットを返します。 — | +コンテキスト上限のレスポンスには `caps`(有効な上限)と `values`(無効化後も保持される最後の選択値)が +含まれます。`value` を指定せずにプロバイダーの上限を有効にすると選択値を復元し、初回はグローバルの +`contextCapValue` を使います。OpenAI でも同様で、スイッチが特別な 922k モードを選ぶことはありません。 +有効な上限はすべてのネイティブウィンドウに適用されます。長いコンテキストに対応したモデルは、 +そのモデルが対応する上限まで拡張できます。 +`{ "value": 600000, "setAll": true }` はグローバル値と有効な上限だけを更新します。 +上限が無効なプロバイダーは選択値を保持し、後で有効にすると復元します。 +`value` なしの `{ "setAll": true }` は、設定済みの全プロバイダーの上限を現在のグローバル値で有効にし、 +保存された選択値も置き換えます。無効化しても選択値は再読み込み後まで保持されますが、制限としては適用されません。 + `provider_has_dependent_combos` は安全バリアです。プロバイダーを削除する前に、依存するコンボを削除または編集してください。 ### サイドバーと同意に基づくアクション 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 93c2be4904..68d7ce5c75 100644 --- a/docs-site/src/content/docs/ja/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ja/reference/proxy-formats.md @@ -22,7 +22,7 @@ provider events → internal adapter events → client dialect | OpenAI チャットの完了 | `POST /v1/chat/completions` | JSON | `chat.completion` `chat.completion.chunk` SSE で終わる `[DONE]` | |人間的なメッセージ | `POST /v1/messages` |人類 `message` JSON |人間的メッセージ SSE | |人間トークン数 | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` |該当なし | -|モデルの発見 | `GET /v1/models` | 3 つのカタログ契約のうちの 1 つ |該当なし | +|モデルの発見 | `GET /v1/models` | カタログまたは明示的な Desktop スナップショット |該当なし | |音声とリアルタイム | `POST /v1/live`、`POST /v1/realtime/calls` |中継されたコール作成応答 |別のサイドバンド WebSocket がフレームを両方向に中継します。 |応答の圧縮 | `POST /v1/responses/compact` |置換履歴 JSON |該当なし | @@ -151,16 +151,46 @@ admission secret も削除され、別の実際の Anthropic 認証情報は維 { "input_tokens": 123 } ``` +解決できない日付形式の Desktop ID は、モデル検出に含まれていない実際のネイティブモデル +かもしれません。判断材料が足りず ID を解決できない場合、Messages と count-tokens は固定エラー +`desktop_model_mapping_unavailable`と HTTP 503 を返します。これはモデルが無効だという判定ではありません。 +不明な旧ハッシュ別名は引き続き HTTP 400 で拒否します。どちらも日付を除去したり別ルートへ +フォールバックしたりしません。既知の ID、登録済みマッピング、正確な `modelMap` 一致、 +認識済みの実ネイティブ ID の処理は変わりません。モデル検出を更新するか接続先ハブの +プロファイルを再適用してから試してください。再試行だけで解決する保証はありません。 + ## `GET /v1/models` -同じルートは、互換性のないカタログ エンベロープを予期する 3 つのクライアントにサービスを提供します。 `client_version` も存在しない限り、人間味が優先されます。 +`format=desktop-config` を指定しない場合、通常のカタログ契約は次のとおりです。 -|契約 |トリガー |トップレベルの形状 |モデル ID の動作 | | --- | --- | --- | --- | |人類モデルのリスト | `anthropic-version` ヘッダーまたは `?flavor=anthropic`、`client_version` なし | Anthropic モデル情報エントリのある `{ "data": [...] }` |クロード コードは読み取り可能な ID を受け取ります。デスクトップはプロファイル固有のエイリアス ファミリを受け取ることができます。 |Codexカタログ | `client_version` クエリパラメータ | `{ "models": [...] }` |ネイティブおよびルーティングされたエントリには、より豊富な Codex カタログ フィールド、可視性、労力、WebSocket、およびマルチエージェント メタデータが含まれています。 |プレーンな OpenAI リスト |どちらのトリガーもありません | `{ "object": "list", "data": [...] }` |表示されるネイティブ ID は裸です。ルーティング ID はエイリアスまたは `provider/model` | +### Desktop 設定スナップショット + +`GET /v1/models?ids=desktop&format=desktop-config` は user-agent に関係なく Desktop +スナップショットを明示的に選択します。応答は `{ "version": 1, "models": [...] }` で、 +`Cache-Control: no-store` を含みます。クライアントは `Accept: application/json`、 +`anthropic-version: 2023-06-01` と既存のデータ用認証情報を送ります。管理者トークンや +プロファイルのアップロードは不要です。項目はハブが発行した Desktop 設定用モデルであり、 +Codex カタログの行ではありません。 + +この形式に `ids=cli` または `client_version` を併用すると HTTP 400 になります。形式指定が +なければ上記の通常の契約を維持します。Claude が無効なら `{ "version": 1, "models": [] }` +を返し、接続中の Desktop apply は利用不可として設定を書き換えません。バージョン 1 ではなく +通常のカタログを返す古いハブは未対応で、ローカル生成 ID に切り替えることはありません。 + +スナップショットは読み取り専用のモデル一覧であり、キーローテーションやプロファイル送信の +API ではありません。Desktop のキー移行・復旧・切断は既存の接続ライフサイクルで処理します。 +ローテーションはモデルと選択を保持し、CLI の `rotation` は `committed` と `rolled_back` を +区別します。切断は管理設定を復元するか、確認済み旧プロファイルを標準モードへ戻し、 +ユーザーフィールドと後から選んだ有効なプロファイルを保持します。競合や未完了の復旧を完了とは +報告しません。ファイル変更の反映には Desktop の再起動が必要で、切断はハブのキーを自動失効 +させません。[Desktop ガイド](/ja/guides/claude-code/)を参照してください。thinking 再送と +キャッシュは別件 [#3719](https://github.com/lidge-jun/opencodex/issues/3719)です。 + ## `POST /v1/live` とRealtime サイドバンド `POST /v1/live` は、ChatGPT/Codex アプリのフレームレス通話作成サーフェスを受け入れます。 `POST /v1/realtime/calls` は、OpenAI Realtime 呼び出し作成サーフェスを受け入れます。 opencodex は、適格な OpenAI ファミリ ルートを選択し、アップストリーム認証モードのコール作成リクエストを正規化し、制限付き応答を中継します。 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 a7ed659c59..1368cf5698 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -117,6 +117,62 @@ hook을 제거해요. Claude Desktop은 별도 profile을 사용하며 shell hoo `claudeCode.nativePassthrough: false`로 끌 수 있고, `claudeCode.anthropicBaseUrl`로 다른 주소를 지정할 수 있어요. +## 원격 허브에 연결된 Claude Desktop + +허브에 연결된 컴퓨터에서 `ocx claude desktop apply` 또는 `ocx claude desktop`을 실행하면 +허브의 Desktop 모델 스냅샷을 받아요. 로컬 별칭을 새로 만들지 않고 허브가 발급한 모델 ID와 +연결된 허브 origin을 로컬 Desktop 설정에 써요. static·hybrid 모드는 모델 목록도 복사하고, +discovery-only 모드는 목록을 넣지 않고 허브 origin을 사용해요. + +Desktop 프로필과 모델 계열 배치·기본값은 허브에서 관리해요. 허브에서 바꾼 뒤 연결된 +클라이언트에서 다시 적용하고 Desktop에서 모델을 다시 선택하세요. 과거에 클라이언트에서만 +만든 별칭은 자동 이전되지 않으므로 재적용·재선택이 필요해요. 로컬 `show`, 프로필 편집, +import/export는 로컬 설정만 다뤄요. 허브 프로필을 바꾸지 않아요. 연결 중에는 +`ocx claude desktop import --apply`를 지원하지 않으며 저장 전에 거절해요. +`--apply` 없는 import는 로컬 작업으로 남아요. + +스냅샷은 기존 연결의 데이터 자격 증명으로 읽어요. 관리자 토큰이나 프로필 업로드는 +필요하지 않아요. 구형 허브가 스냅샷을 지원하지 않거나 응답이 잘못됐거나 Desktop 모델이 +없으면 적용에 실패해요. 로컬 목록이나 루프백 주소로 대신 적용하지 않아요. +허브를 업데이트하거나 설정을 확인한 뒤 다시 적용하세요. + +이번 별칭 변경에는 [#3719](https://github.com/lidge-jun/opencodex/issues/3719)의 별도 `thinking` / `redacted_thinking` 재전송과 프롬프트 캐시 +요청은 포함되지 않아요. 프록시 접속 자격 증명만으로 네이티브 Anthropic 패스스루가 켜지지는 +않지만, 번역된 Anthropic 요청도 프롬프트 캐시를 쓸 수 있어요. 재전송 보존과 캐시 적중률 +비교는 별도 작업으로 남아요. + +### 키 회전·복구와 연결 해제 + +키 회전과 복구는 로컬 연결 자격 증명과 함께 이 연결이 관리하는 Desktop 프로필의 키도 +갱신해요. 키를 바꾸려고 Desktop apply를 수동으로 다시 실행할 필요는 없어요. 기존 모델 ID, +계열·기본값과 현재 프로필 선택을 유지하며, 관리 프로필을 다시 선택하거나 꺼둔 통합을 켜지 +않아요. CLI JSON의 `rotation: "committed"`는 새 키가 활성화됐다는 뜻이에요. +`rotation: "rolled_back"`는 이전 키를 유지하거나 복원했다는 뜻이며, 새 키 적용이나 이전 키 +폐기를 뜻하지 않아요. 복구 결과가 불확실하거나 미완료면 성공으로 표시하지 않아요. + +처음 연결된 Desktop 설정을 적용할 때 복원에 필요한 기존 관리 설정과 선택을 기록해요. +재적용과 키 회전은 이 최초 기록을 유지해요. `ocx disconnect`는 연결이 관리하던 설정을 +복원하면서 사용자가 추가한 필드와 다른 프로필을 보존해요. 관리 프로필이 아직 선택돼 있을 +때만 이전 선택으로 돌아가며, 이후 사용자가 다른 유효한 프로필을 선택했다면 그대로 둬요. +새로 만든 프로필에 사용자 설정이 추가됐다면 지우지 않고 읽을 수 있는 표준 모드로 남겨요. +`--keep-catalog`는 카탈로그를 남기는 옵션이지 Desktop의 연결 키를 남기는 옵션이 아니에요. + +이전 설정 기록이 없는 구형 관리 프로필도 현재 허브와 확인된 연결 키에 속하면 이전할 수 +있어요. apply, 키 회전·복구 또는 바로 disconnect를 실행하면 되고, 새 플래그나 사전 재적용은 +필요하지 않아요. 이 경우 이전 설정이 기록되지 않아 연결 해제 시 표준 모드로 바뀐다는 +경고를 표시해요. 연결이 관리하던 게이트웨이 설정만 제거하고 사용자 필드와 별도로 선택한 +유효한 프로필을 보존해요. 이 결과는 원본 복원이 아닌 표준 모드 전환으로 표시해요. + +관리 설정 충돌, 알 수 없는 자격 증명, 손상된 복원 기록은 덮어쓰지 않고 문제를 알려줘요. +중단된 정리는 같은 연결에 한해 이어갈 수 있으며, 새 연결을 지우거나 복원이 끝나기 전에 +완료됐다고 하지 않아요. 연결 해제 전에 진행 중인 키 회전 복구를 마치고, 연결 해제를 +재시도할 때는 처음 고른 카탈로그 유지 옵션을 그대로 쓰세요. + +적용·키 회전·복구·설정 복원 후에는 Claude Desktop을 완전히 종료하고 다시 여세요. +파일을 바꿔도 실행 중인 앱이 가진 키는 바뀌지 않으며, 앱을 자동 종료하거나 재시작하지 +않아요. 연결 해제는 로컬에서 처리하고 허브 키나 외부에 따로 복사한 키를 자동 폐기하지 +않아요. 폐기가 필요하면 허브에서 별도로 처리하세요. + ## /model 선택기("From gateway") 각 항목은 `gemini-3-pro (gemini)` 같은 정직한 표시 이름과 함께, 공식 ModelInfo 형태의 모델 능력 정보(추론 강도 사다리, thinking 타입)를 실어 보냅니다 — Claude Desktop의 서드파티 @@ -159,6 +215,14 @@ v2 별칭은 이스케이프를 펼쳐요. 읽기 쉬운 형식으로 표현할 **모델 해석 순서:** `[1m]` 표식 제거 → 읽기 쉬운 별칭 디코딩 → Desktop 해시 별칭 디코딩 → `modelMap` 정확히 일치 → 날짜를 제거한 값과 일치(`-20250514` 제거) → 패스스루 순서예요. +해결되지 않은 날짜형 Desktop ID는 모델 탐색에서 빠진 실제 네이티브 모델일 수도 있어요. +확인된 정보만으로 ID를 해석할 수 없으면 Messages와 count-tokens는 고정된 `desktop_model_mapping_unavailable` +오류와 HTTP 503을 반환해요. 모델이 잘못됐다고 확정한 것은 아니에요. 알 수 없는 레거시 +해시 별칭은 계속 HTTP 400으로 거절해요. 두 경우 모두 날짜를 떼거나 다른 경로로 폴백하지 +않아요. 알려진 ID, 등록된 매핑, 정확한 `modelMap` 일치와 인식된 실제 네이티브 ID는 기존 +방식대로 처리해요. 모델 탐색을 새로 하거나 연결된 허브 프로필을 다시 적용한 뒤 시도하세요. +재시도만으로 해결된다는 보장은 없어요. + 각 항목에는 `gemini-3-pro (gemini)` 같은 표시 이름과 공식 `ModelInfo` 형식의 전체 모델 기능 (reasoning-effort 단계, thinking 유형)이 들어 있어요. 실제 Anthropic 모델은 두 화면 모두에서 정식 ID를 유지해요. @@ -256,6 +320,14 @@ Anthropic 패스스루는 그대로 유지해요. 조회 순서: 검색 별칭 → 정확한 ID → 날짜 접미사를 제거한 ID(`-20250514`) → 패스스루 순서예요. +해결되지 않은 날짜형 Desktop ID는 모델 탐색에서 빠진 실제 네이티브 모델일 수도 있어요. +확인된 정보만으로 ID를 해석할 수 없으면 Messages와 count-tokens는 고정된 `desktop_model_mapping_unavailable` +오류와 HTTP 503을 반환해요. 모델이 잘못됐다고 확정한 것은 아니에요. 알 수 없는 레거시 +해시 별칭은 계속 HTTP 400으로 거절해요. 두 경우 모두 날짜를 떼거나 다른 경로로 폴백하지 +않아요. 알려진 ID, 등록된 매핑, 정확한 `modelMap` 일치와 인식된 실제 네이티브 ID는 기존 +방식대로 처리해요. 모델 탐색을 새로 하거나 연결된 허브 프로필을 다시 적용한 뒤 시도하세요. +재시도만으로 해결된다는 보장은 없어요. + ## 사이드카 매트릭스: 웹 검색과 이미지 이해 라우팅 모델마다 쓸 수 있는 호스팅 도구와 이미지 지원 범위가 달라요. opencodex는 메인 모델이 diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 3f777153ec..44551de837 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -197,7 +197,14 @@ Codex에서 model이 빠졌거나 catalog 순서/가시성이 이상해 보이 1. provider의 **`selectedModels`** - 비어 있지 않은 allowlist는 해당 id만 Codex에 노출합니다. 비어 있거나 생략하면 발견된 model이 모두 노출됩니다. allowlist에 없는 id는 catalog에 절대 들어가지 않습니다. 2. **`disabledModels`**(top level) - catalog와 `/v1/models`에서 model을 숨기고, bare native GPT slug는 `visibility: "hide"`로 바꿉니다. -3. **`liveModels: false`와 비어 있는 `models`** - live discovery가 꺼져 있고 `models`가 비어 있거나 생략되면, opencodex는 그 provider에 대해 routed model을 하나도 노출하지 않습니다. +3. **`liveModels: false`** — `liveModels: false`에서 `models`가 비어 있거나 생략되면 초기 목록은 설정된 `defaultModel`, + `retainModels` 순으로 구성합니다. 중복 ID는 처음 나온 항목만 남깁니다. 비어 있지 않은 `models`를 + 명시하면 `models`, `retainModels` 순으로 구성하며, 다른 `defaultModel`을 자동으로 추가하지 않습니다. + 그 모델도 `models`나 `retainModels`에 직접 넣으면 포함할 수 있습니다. 어느 필드에도 ID가 없으면 + 초기 목록은 비어 있습니다. 이 순서는 최종 선택기의 표시 순서를 보장하지 않습니다. + `selectedModels`, `disabledModels`, 공급자 비활성화 정책은 그대로 적용됩니다. + `authMode: "forward"`는 기존 별도 분기를 따르며 이 정적 라우팅 목록을 사용하지 않습니다. + 이 규칙은 라이브 발견 실패 시 폴백 동작을 바꾸지 않습니다. 4. **Cursor `GetUsableModels`** - Cursor adapter는 `/models`가 아니라 protobuf `GetUsableModels` RPC로 model을 찾습니다. 그래서 Cursor 쪽 변경이 다른 provider와 무관하게 어떤 id가 보이는지 바꿀 수 있습니다. 5. **캐시와 `ocx sync`** - live catalog는 약 5분(`modelCacheTtlMs`, 기본값 `300000`) 동안 캐시됩니다. `ocx sync`를 실행하면 새로 가져와서 catalog를 즉시 다시 쓸 수 있습니다. 6. **실행 중인 Codex `app-server`** - 오래 살아 있는 Codex `app-server`(Desktop / CLI background host)가 이전 목록을 메모리에 쥐고 있으면 디스크 catalog를 다시 쓰는 것만으로는 부족합니다. `ocx sync`와 `ocx sync-cache`는 그런 process를 감지하면 경고합니다. `ocx sync --restart-codex`로 다시 시작하거나(아니면 일치하는 `app-server` process를 직접 중지한 뒤), Codex가 다시 만들게 해서 새 목록이 보이게 하세요. diff --git a/docs-site/src/content/docs/ko/guides/model-ordering.md b/docs-site/src/content/docs/ko/guides/model-ordering.md index 3c960b1840..365ea476fc 100644 --- a/docs-site/src/content/docs/ko/guides/model-ordering.md +++ b/docs-site/src/content/docs/ko/guides/model-ordering.md @@ -22,6 +22,8 @@ native id는 해당 selector의 `i * N + j`를 사용합니다. Codex는 계속 selector가 없을 때의 priority는 다음과 같습니다. +아래 우선순위 표와 예시는 선택기 전체 정렬을 켜지 않은 경우를 설명합니다. + | 카탈로그 항목 | Priority | 근거 | | --- | ---: | --- | | `subagentModels[i]` | `i` (`0`부터 `4`) | `src/codex/catalog/sync.ts`의 featured rank map | @@ -111,6 +113,43 @@ account selector가 있으면 bare native 선택이 selector-qualified 그룹으 최대 5개만 사용하세요. account selector가 있으면 bare native 하나가 여러 selector-qualified 행으로 확장될 수 있으므로 설정 항목과 노출 행이 항상 일대일로 대응하지는 않습니다. -현재 `OcxConfig`에는 일반 `modelOrder`, `providerOrder`, priority map 설정이 없습니다. 지원되는 정렬 -필드는 `subagentModels`입니다. `disabledModels`와 각 프로바이더의 `selectedModels`는 노출 -필드입니다. 따라서 나머지 선택기 순서를 바꾸려면 설정 수정이 아니라 코드 동작 변경이 필요합니다. +`modelPickerOrder`는 선택기의 표시 순서만 지정합니다. 라우팅 ID인 `/`만 +넣으면 목록에 있는 비 featured 행이 지정 순서대로 별도 표시 구간(`1000 + i`)에 배치됩니다. +목록에 없는 라우팅 행은 원래 우선순위를 유지하므로 이 구간보다 앞에 남습니다. `subagentModels`에도 +들어 있는 행은 featured 우선순위를 유지하고, 네이티브 행도 원래 위치를 유지합니다. +상대적 순서를 정할 라우팅 행은 모두 목록에 넣어야 합니다. + +선택기 전체를 정렬하려면 `/`가 없는 카탈로그 ID를 하나 이상 넣으세요. `gpt-5.6-sol`처럼 실제 문자가 +있는 bare ID여야 하며, 빈 문자열이나 공백만 있는 항목은 해당하지 않습니다. + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +지정한 행이 배열 순서대로 먼저 나오고, 나머지 행은 원래 우선순위대로 뒤에 나옵니다. +카탈로그 ID는 정확히 일치하는 값으로 찾습니다. `gpt-5.6-sol`과 `openai/gpt-5.6-sol`은 서로 다른 행입니다. +같은 라우팅 ID의 원문 표기와 인코딩 표기도 허용하지만, 정확히 일치하는 항목이 우선합니다. +빈 항목과 공백뿐인 항목은 무시합니다. 계정별 행을 지정할 때는 selector가 포함된 전체 ID를 써야 합니다. + +### 마이그레이션 주의: 기존 목록에 들어 있는 네이티브 ID + +이전에는 `modelPickerOrder`의 bare native ID를 무시했습니다. 이제 기존 목록에 이런 ID가 있으면 +featured 행을 포함한 선택기 전체 정렬이 활성화됩니다. 기존 라우팅 전용 동작을 유지하려면 bare ID를 +제거하세요. 미설정 목록, 빈 목록, 공백만 있는 목록, 라우팅 ID만 있는 목록은 기존 동작을 유지합니다. + +`modelPickerOrder`는 자연 우선순위로 최대 5개의 선호 후보를 고르는 OpenCodex의 +서브에이전트 안내용 계산을 보존합니다. 이동한 각 행의 자연 우선순위는 네이티브 `priority`와 별도로 +남으며, 선택기 순서만 바꿔서는 이 계산 결과가 달라지지 않습니다. 정확한 모델 이름으로 override를 +지정할 자격도 제한하지 않습니다. 광고 목록은 허용 목록이 아니며, 기존 인증·모델·effort·백엔드 제약은 +그대로 적용됩니다. + +네이티브 Codex는 네이티브 `priority` 순서에서 사용 가능하고 선택기에 표시되는 모델 중 앞의 5개를 +`spawn_agent`에 광고합니다. V1과 모델 override를 공개하는 V2가 여기에 해당합니다. +따라서 OpenCodex의 선호 후보가 그대로여도 선택기 순서에 따라 광고되는 5개는 달라질 수 있습니다. +V1에는 OpenCodex의 선호 후보 목록을 주입하지 않습니다. V2는 클라이언트 카탈로그 상태가 허용할 때 +자연 우선순위 기반 안내를 추가로 받을 수 있지만, 이 안내가 네이티브 도구의 광고 목록을 재정렬하지는 않습니다. + +`disabledModels`와 각 공급자의 `selectedModels`는 노출 여부를 정하는 필드입니다. +별도의 `modelOrder`, `providerOrder`, priority map 설정은 없습니다. diff --git a/docs-site/src/content/docs/ko/guides/model-routing.md b/docs-site/src/content/docs/ko/guides/model-routing.md index f9b1eff92c..ac6425f342 100644 --- a/docs-site/src/content/docs/ko/guides/model-routing.md +++ b/docs-site/src/content/docs/ko/guides/model-routing.md @@ -84,12 +84,15 @@ fallback하지 않습니다. 직접 라우팅은 그대로 두고, 카탈로그와 `/v1/models`에 내보낼 모델만 줄입니다. - `provider.disabled: true`인 프로바이더는 카탈로그 탐색에서 제외됩니다. 명시적 `provider/model` 요청은 실패하고, `defaultModel` / `models[]` 검사에서도 건너뜁니다. -- `providerContextCaps`는 프로바이더별로 Codex에 표시할 컨텍스트 상한을 지정합니다. - `contextCapValue`는 대시보드가 함께 쓰는 값이며 기본값은 350,000입니다. 다만 이 값만 설정해서는 - 아무 변화가 없고 `providerContextCaps`에 프로바이더가 들어 있어야 적용됩니다. 대시보드 값을 변경하면 - '모든 라우팅 대상 프로바이더에 적용' 토글이 켜져 있을 때만 모든 활성 프로바이더에 다시 적용되며, - 그렇지 않으면 각 프로바이더는 자체 한도를 유지합니다. 이미 알려진 컨텍스트 크기를 낮추기만 하며, - 더 키우거나 업스트림 모델의 실제 한도를 바꾸지는 않습니다. +- `providerContextCaps`는 공급자별로 Codex에 표시할 컨텍스트 상한을 지정합니다. + `contextCapValue`는 대시보드의 기본값이며 기본 설정은 350,000입니다. 이 값만으로는 상한이 적용되지 않고, + `providerContextCaps`에 공급자가 있어야 적용됩니다. '모든 라우팅 대상 공급자에 적용' 토글을 켠 상태에서 + 대시보드 값을 바꾸면 활성 상한만 갱신합니다. 토글이 꺼져 있으면 각 공급자의 상한을 유지합니다. + 일반적인 기존 윈도는 줄일 수만 있지만, 장문 윈도를 지원하는 네이티브 모델은 해당 모델의 지원 상한까지 + 확장할 수 있습니다. 업스트림 모델의 실제 한도는 바뀌지 않습니다. 상한을 꺼도 선택값은 + `providerContextCapValues`에 남고 다시 불러와도 유지됩니다. 다시 켜면 이 선택값을 복원하며, + 꺼져 있는 동안에는 저장된 값을 제한으로 적용하지 않습니다. `value` 없이 `{ "setAll": true }`를 보내면 + 설정된 모든 공급자의 상한을 현재 전역 값으로 켜고, 저장된 선택값도 이 값으로 바꿉니다. ```json { diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index 6f340ddd8b..efdd80179f 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -48,6 +48,27 @@ bun run dev:gui | **Storage** | CODEX_HOME 디스크 사용량(세션, 보관, DB, 첨부)을 읽기 전용으로 표시합니다. 선택적 보관 정리: 가장 오래된 N%를 미리본 뒤 기본으로 `CODEX_HOME/.trash`에 격리하거나, 명시 체크 후 영구 삭제합니다. **자동 정리 정책**은 opt-in이며 **기본 OFF**(`storageCleanupPolicy.enabled`)입니다. Storage 페이지에서 임계값/목표/일정/모드를 설정하거나 **지금 실행**하세요. Storage 페이지에서 격리 항목을 복원할 수 있습니다(JSONL + 스레드). 활성 세션은 읽기 전용입니다. Codex가 최신/활성 `state_*.sqlite`를 잠그면 정리와 복원을 거절합니다. | | **Stop** | 프록시와 설치된 백그라운드 서비스를 정상 종료하고 네이티브 Codex를 복원한 뒤 끝냅니다(`POST /api/stop`). 단, Windows 작업 스케줄러로 관리되는 경우에는 대시보드가 거절하고 `ocx stop`을 안내합니다. 작업이 끝나도 래퍼가 프록시를 다시 띄울 수 있어서, 클라이언트 설정을 되돌리기 전에 그 재시작 구간을 확인할 수 있는 건 프록시 바깥에서 도는 stop뿐입니다. 거절될 때는 아무것도 바뀌지 않습니다. | +### 요청 로그 필터 + +Logs에서는 클라이언트 종류, 가로챈 요청, 공급자, 정확한 모델명, 상태, 시간, +속도, 대화 ID 조건을 함께 적용할 수 있습니다. 현재 불러온 로그만 필터링하며, +공급자·모델 선택지에는 폴백 시도도 포함됩니다. 모델명은 대소문자와 앞뒤 공백을 +무시하지만 부분 이름은 일치하지 않습니다. 선택한 공급자나 모델이 불러온 로그에서 +사라지면 해당 필터만 전체로 돌아갑니다. + +시간 범위는 최근 15분·1시간·1일입니다. Logs 탭에서는 자동 새로고침을 꺼도 +30초마다 시간 필터를 갱신합니다. 프록시가 응답에 담은 시각에 브라우저의 경과 시간을 +더해 계산하므로, 두 기기의 시계가 달라도 범위가 밀리지 않습니다. 시각을 보내지 않는 +이전 프록시에서는 유효한 응답을 받기 전까지 브라우저 시계를 사용합니다. 속도는 전체 요청 시간으로 계산한 초당 출력 토큰 +수이며, 15 미만·15 이상 50 미만·50 이상으로 나뉩니다. 속도 필터를 켜면 측정값이 +없는 요청은 제외됩니다. 성공은 2xx, 오류는 4xx·5xx입니다. + +필터를 적용하면 일치하는 건수와 불러온 전체 건수가 표시됩니다. 필터를 초기화하면 +불러온 모든 행이 다시 나타나고 키보드 포커스는 클라이언트 종류의 전체 선택으로 돌아갑니다. +조건에 맞는 요청이 없는 상태와 로그 자체가 빈 +상태는 구분해서 표시합니다. 클라이언트 종류는 방향키와 Home/End로 선택할 수 있습니다. +불러온 범위 밖의 과거 로그는 조회하지 않습니다. + ### 섹션으로 바로 가기 레이아웃은 하나뿐이라 전환할 설정이 없습니다. 대신 Dashboard의 섹션마다 주소가 있습니다. `#dashboard`는 Overview, `#dashboard/providers`와 `#dashboard/models`는 나머지 두 섹션입니다. 새로고침하거나 북마크해도, 뒤로 가도 보던 섹션이 그대로 유지됩니다. **Logs**도 `#logs`와 `#logs/debug`로 똑같이 동작합니다. 예전 `#providers/workspace` 북마크는 `#providers`로 넘어갑니다. @@ -64,6 +85,22 @@ bun run dev:gui **Models** 스위치는 Codex의 최종 노출 상태를 나타냅니다. 라우팅 모델은 프로바이더 allowlist에 포함되거나 allowlist가 없고, 동시에 비활성화되지 않았을 때만 켜집니다. 모델을 켜면 두 필터를 원자적으로 조정하며, **모두 활성화**는 allowlist를 해제해 새로 발견되는 모델도 켭니다. +### 프로바이더 화면에서 모델 관리하기 + +프로바이더의 **모델** 탭에서 **삭제**를 누르면 저장된 커스텀 정의를 지웁니다. 원래 네이티브 모델이나 +라이브 발견 모델이 다시 나타날 수 있으므로 모델 수는 그대로일 수 있습니다. **숨기기**는 카탈로그 노출만 +바꾸며, 정의를 삭제하거나 직접 라우팅 정책을 바꾸지 않습니다. **모델에서 노출 관리**를 누르면 **모델** +페이지에서 다시 표시할 수 있습니다. 프로바이더 탭에 행이 하나도 없어도 이 이동 버튼을 사용할 수 있습니다. + +**추가**는 커스텀 정의를 저장하며, 기존 숨김 상태나 프로바이더 선택 규칙을 해제하지 않습니다. +저장된 모델이 계속 숨겨져 있을 수 있습니다. 이미 등록된 모델은 **모델**에서 노출 상태를 관리하세요. +저장이 확인됐다면 카탈로그 갱신이 실패해도 정의는 저장된 상태입니다. 다시 추가하지 말고 갱신 안내를 +따르세요. 변경 여부를 확인하지 못했다면 모델 상태를 새로고침한 뒤 다시 시도하세요. + +프로바이더의 모델 수는 서버가 반환한 현재 모델 목록에서 비활성 항목을 제외하고 중복 없이 센 값입니다. +검색이나 표시 개수 제한을 적용하기 전에 계산합니다. 허용 목록의 크기나 라이브 발견 개수가 아니며, +업스트림에서 발견한 모델임을 뜻하지도 않습니다. 선택 배지와 발견 정보는 이 개수와 별도로 표시합니다. + ## 위임 선택기와 스폰 라우팅의 차이 Dashboard의 **Sub-agent delegation** 선택기는 `injectionModel`과 선택적인 `injectionEffort`를 @@ -175,7 +212,7 @@ GUI는 프록시의 JSON 관리 API를 사용하는 얇은 클라이언트입니 | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | 다음 요청에 사용할 계정과 풀 라우팅 정책을 설정합니다. | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | 실효 계정(고정 여부를 나타내는 `pinned`와 고정된 계정을 알려주는 `pinnedAccountId` 포함)을 읽고 계정 하나의 선택 순서를 설정합니다. | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | 브라우저 로그인으로 pool 계정을 추가합니다. | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | tail, 프로바이더, 정확한 상태 코드 또는 상태 등급으로 최근 요청 메타데이터를 조회합니다. `limit`/`offset`은 최신 행에서 과거 방향으로 페이지네이션합니다(`offset=0`이 최신 페이지). 응답은 `{ timeZone, total, logs }`이며 `total`은 페이지네이션 전 필터 일치 건수입니다. | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | tail, 프로바이더, 정확한 상태 코드 또는 상태 등급으로 최근 요청 메타데이터를 조회합니다. `limit`/`offset`은 최신 행에서 과거 방향으로 페이지네이션합니다(`offset=0`이 최신 페이지). 응답은 `{ timeZone, generatedAt, total, logs }`이며 `total`은 페이지네이션 전 필터 일치 건수입니다. | | `GET` / `PUT /api/subagent-models` | `spawn_agent`에 우선 노출할 모델 5개를 읽거나 설정합니다. | | `POST /api/stop` | 프록시/서비스를 멈추고 네이티브 Codex를 복원한 뒤 종료합니다. Windows 작업 스케줄러 백엔드에서는 `respawnable_service`로, 그 상태를 읽을 수 없으면 `service_state_unknown`으로 거절하며, 두 경우 모두 아무것도 바뀌지 않습니다. | @@ -184,3 +221,11 @@ GUI는 프록시의 JSON 관리 API를 사용하는 얇은 클라이언트입니 프로바이더 설정에 복사됩니다. 별도 분류 작업 없이도 [비전 사이드카](/ko/guides/sidecars/)가 올바른 조건에서만 실행됩니다. ::: + +### 계정 선택과 자동 전환 + +GUI에서 OAuth 계정을 선택하면 풀 모드에서도 다음 요청에 반영돼요. 일반 OAuth 계정은 +정상적으로 사용할 수 있는 선택 계정을 유지하며, 다른 계정의 남은 할당량이 더 많다는 +이유만으로 바꾸지 않아요. 선택 계정이 429를 반환하면 풀이 꺼져 있어도 사용 가능한 다른 +계정으로 자동 전환해요. 자동 선택이 저장되면 GUI의 활성 표시도 즉시 바뀌어요. +이미 서버로 보낸 요청의 인증 정보는 바꾸지 않아요. diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 081c791bbb..4847614674 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -203,6 +203,12 @@ Codex의 로컬 모델 선택기 캐시를 무효화하여, 활성 opencodex 카 유닛, Windows **Task Scheduler**). 로그인 시 자동 시작하고 충돌 시 자동 재시작합니다. 서비스 실행은 `OCX_SERVICE=1`을 설정하므로 재시작해도 Codex 설정이 흔들리지 않습니다. +Windows 작업 스케줄러로 설치하는 서비스는 보통 프로세스 우선순위(`Priority=4`)를 사용합니다. +이전의 백그라운드 우선순위(`7`, 생략 시에도 스케줄러 기본값은 `7`)에서는 CPU 경합으로 상태 확인 응답이 +늦어져 프로세스가 살아 있어도 트레이에 Offline이 표시될 수 있습니다. 업그레이드 후 `ocx service repair`를 +실행하면 등록된 해당 우선순위를 변경하고 서비스를 재시작합니다. 이 과정에서 UAC 승인이 필요할 수 있습니다. +이미 보통 또는 높음 우선순위인 경우 우선순위만을 이유로 다시 등록하지 않습니다. + | 하위 명령 | 동작 | | --- | --- | | 없음 | 서비스가 없으면 설치하고 시작하며, 이미 있으면 새로 고쳐 재시작합니다. 정상인 Windows 작업 스케줄러 정의는 재사용하지만, 오래된 정의는 다시 등록되어 관리자 권한 승인이 필요할 수 있습니다. | 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 ec5764bbd2..1c999536f2 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ko/reference/configuration/agents.md @@ -53,7 +53,7 @@ V1 안내는 `max` 또는 `ultra`에서만 선제 텍스트로 제공됩니다. 거부하고 역할을 건너뜁니다 (#1190). TOML의 기존 `model_fallback` 줄은 하위 호환성을 위해 계속 읽히지만 `ocx doctor`가 이를 표시합니다. -opencodex는 비활성, 라우팅 불가, 비정상, 쿨다운 중, 또는 할당량 임계값에 걸린 후보를 건너뜁니다. 사용 가능성 스냅샷은 `subagentModelFallbackPollMs` 동안 캐시됩니다. 암호화된 하위 작업은 정규 네이티브 ChatGPT 대상과 `allowEncryptedV2AgentTasks: true`로 명시적으로 신뢰한 직접 키 인증 Responses 라우트만 후보로 사용합니다. 암호화된 페이로드를 처리할 수 있는 대상이 없으면 읽을 수 없는 암호문을 다른 곳으로 보내지 않고 요청이 실패합니다. 콤보는 계속 정규 네이티브 대상만 사용합니다. +opencodex는 비활성, 라우팅 불가, 비정상, 쿨다운 중, 또는 할당량 임계값에 걸린 후보를 건너뜁니다. 사용 가능성 스냅샷은 `subagentModelFallbackPollMs` 동안 캐시됩니다. 암호화된 하위 작업은 정규 네이티브 ChatGPT 대상과 `allowEncryptedV2AgentTasks: true`로 명시적으로 신뢰한 직접 키 인증 Responses 라우트만 후보로 사용합니다. 암호화된 페이로드를 처리할 수 있는 대상이 없으면 읽을 수 없는 암호문을 다른 곳으로 보내지 않고 요청이 실패합니다. 콤보는 먼저 사용 가능한 정규 네이티브 대상을 시도하고, 선택 가능한 네이티브 대상이 없으며 `agentTaskRecovery`가 켜져 있으면 암호화된 `NEW_TASK`를 라우팅된 콤보 전송 전에 한 번 복구합니다. ```json { diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index 3d65dbfb4d..c9c4de5ede 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -27,8 +27,9 @@ GUI에서 등록이나 OAuth 로그인을 마치면 Models 페이지로 이동 | `providers` | `Record` | — | 공급자 이름을 공급자 설정에 매핑합니다. | | `openaiProviderTierVersion?` | `2` | 마이그레이션으로 설정됨 | 옵션을 인식하는 단일 OpenAI 투영이 완료되었음을 표시합니다. | | `disabledModels?` | `string[]` | — | Codex catalog와 `/v1/models`에서는 숨기지만 직접 proxy 호출은 차단하지 않습니다. routed id는 목록에서 제거됩니다. account-qualified native id는 해당 selector row만 숨기고, bare native GPT id는 bare row와 그 model의 모든 account-selector row를 숨깁니다. Models 페이지에는 bare native 행과 routed 행만 표시됩니다. selector-qualified 행 하나만 숨기려면 이 설정 필드에 직접 추가하세요. | -| `providerContextCaps?` | `Record` | `{}` | 공급자별 Codex 표시 컨텍스트 상한입니다. 상한은 이미 알려진 컨텍스트 윈도만 낮춥니다. | -| `contextCapValue?` | `number` | `350000` | 대시보드의 컨텍스트 상한 컨트롤이 사용하는 기본값입니다. "모든 라우팅된 공급자에 적용" 토글이 켜져 있을 때만 값을 변경하면 기존 `providerContextCaps` 항목이 없는 공급자를 포함해 모든 라우팅된 공급자에 값이 적용됩니다. 그렇지 않으면 각 공급자는 자체 상한을 유지합니다. | +| `providerContextCaps?` | `Record` | `{}` | 공급자별 활성 컨텍스트 상한입니다. 일반 윈도는 줄어들며, 장문 윈도를 지원하는 네이티브 모델은 해당 모델의 지원 상한까지만 확장할 수 있습니다. | +| `providerContextCapValues?` | `Record` | `{}` | 공급자별로 마지막에 선택한 상한입니다. 꺼도 선택값이 남으며, 저장된 값만으로는 상한이 활성화되지 않습니다. 활성 값이 저장된 선택값보다 우선합니다. | +| `contextCapValue?` | `number` | `350000` | 처음 켤 때 쓰는 기본값입니다. 다시 켜면 공급자별 선택값을 복원합니다. `setAll: true`와 함께 전역 값을 바꾸면 활성 상한만 갱신합니다. 값 없이 `setAll: true`를 보내면 설정된 모든 공급자의 상한을 현재 전역 값으로 켭니다. | | `codexAccounts?` | `CodexAccount[]` | `[]` | Codex Auth가 관리하는 ChatGPT/Codex 풀 계정 메타데이터입니다. 비밀 정보는 `codex-accounts.json`에 따로 저장됩니다. | | `pausedCodexAccountIds?` | `string[]` | `[]` | 일시 중지된 `__main__` 계정을 포함해, 재개될 때까지 Pool 선택에서 제외되는 계정입니다. | | `codexAccountNamespaces?` | `Record` | — | 임의의 공개 model selector를 저장된 Codex 계정 target에 연결하는 선택적 map입니다. 계정 한정 선택기 행이 활성화되어 있으면 target이 존재하는 각 selector는 Codex picker에 별도의 `/` row를 추가하며, 각 row는 해당 계정만 사용합니다. selector가 하나라도 활성화되면 bare native row는 picker에서 숨겨지지만, 명시적으로 비활성화하지 않는 한 해당 id는 계속 routing 가능하고 raw `/v1/models`에 표시됩니다. | @@ -81,7 +82,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic 키 헤더 형식입니다. 기본값은 네이티브 `x-api-key`이며, 키 인증 `anthropic` 공급자에만 유효합니다. | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 다중 키 풀입니다. `apiKey`는 활성 항목을 그대로 반영하며, 각 항목에는 `id`, `key`, 선택적 `label`, 선택적 숫자 `addedAt`가 들어갑니다. | | `defaultModel?` | `string` | 이 공급자를 선택할 때 모델을 따로 지정하지 않으면 사용하는 모델입니다. | -| `models?` | `string[]` | 시드/폴백 모델 목록입니다. `liveModels: false`이면 이 목록만 발견된 모델로 취급합니다. | +| `models?` | `string[]` | 초기/폴백 모델 목록입니다. `liveModels: false`에서 `models`가 비어 있지 않으면 `models`, `retainModels` 순으로 구성합니다. `models`가 비어 있거나 생략되면 설정된 `defaultModel`, `retainModels` 순으로 구성하고, 중복 ID는 처음 나온 항목만 남깁니다. | | `liveModels?` | `boolean` | 시작 또는 동기화 시 라이브 카탈로그를 가져옵니다. 기본값은 `true`입니다. 사용자 지정 공급자는 `${baseUrl}/models`를 사용하고, 내장은 레지스트리 URL을 사용한 뒤 필터링할 수 있습니다. | | `selectedModels?` | `string[]` | 발견 후 카탈로그 허용 목록입니다. 값이 비어 있지 않으면 그 id만 노출하고, 비어 있거나 생략하면 발견된 모델을 모두 노출합니다. | | `modelDisplayNames?` | `Record` | 이 공급자의 정확한 네이티브 모델 id를 키로 쓰는 영구 표시 전용 이름입니다. 키는 대소문자를 구분합니다. 이름은 공급자 카탈로그 메타데이터보다 우선하며 인증, 어댑터, 라우팅, 청구 또는 업스트림 요청을 바꾸지 않습니다. 맵은 발견 한도와 같은 최대 2,000개 항목을 가질 수 있습니다. | @@ -361,7 +362,16 @@ Vercel AI Gateway는 하나의 모델을 여러 기반 추론 공급자에 걸 ## 정적 모델 허용 목록 -`liveModels: false`로 두면 `models`만 노출합니다. `models`가 비어 있거나 생략되면 공급자는 어떤 라우팅 모델도 노출하지 않습니다. 라이브 발견은 캐싱 전에 4 MiB 또는 원시 모델 행 2,000개를 넘으면 거부합니다. 내장 프리셋은 더 낮은 한도를 쓰고 chat 가능한 행만 필터링할 수 있습니다. 너무 크거나 형식이 잘못된 결과는 오래된/설정된 폴백을 따릅니다. 유효하지만 선택 가능한 항목이 0개인 결과는 그대로 권위가 있으며, 조용히 다른 값으로 바꾸거나 잘라내지 않습니다. +`liveModels: false`에서 `models`가 비어 있거나 생략되면 초기 목록은 설정된 `defaultModel`, +`retainModels` 순으로 구성합니다. 중복 ID는 처음 나온 항목만 남깁니다. 비어 있지 않은 `models`를 +명시하면 `models`, `retainModels` 순으로 구성하며, 다른 `defaultModel`을 자동으로 추가하지 않습니다. +그 모델도 `models`나 `retainModels`에 직접 넣으면 포함할 수 있습니다. 어느 필드에도 ID가 없으면 +초기 목록은 비어 있습니다. 이 순서는 최종 선택기의 표시 순서를 보장하지 않습니다. +`selectedModels`, `disabledModels`, 공급자 비활성화 정책은 그대로 적용됩니다. +`authMode: "forward"`는 기존 별도 분기를 따르며 이 정적 라우팅 목록을 사용하지 않습니다. +이 규칙은 라이브 발견 실패 시 폴백 동작을 바꾸지 않습니다. + +라이브 발견은 캐싱 전에 4 MiB 또는 원시 모델 행 2,000개를 넘으면 거부합니다. 내장 프리셋은 더 낮은 한도를 쓰고 chat 가능한 행만 필터링할 수 있습니다. 너무 크거나 형식이 잘못된 결과는 오래된/설정된 폴백을 따릅니다. 유효하지만 선택 가능한 항목이 0개인 결과는 그대로 권위가 있으며, 조용히 다른 값으로 바꾸거나 잘라내지 않습니다. `selectedModels`는 발견은 계속하되, 선택된 id만 Codex와 `/v1/models`에 나타나게 하고 싶을 때 사용합니다. 대시보드는 나중에 허용 목록을 바꿀 수 있도록 발견된 전체 목록을 보관합니다. 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 879b9d40a6..1ccaa8851a 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/server.md +++ b/docs-site/src/content/docs/ko/reference/configuration/server.md @@ -167,3 +167,7 @@ Anthropic OAuth 사이드카는 opencodex의 기존 Claude Code OAuth fingerprin `runtimeRole` 기본값은 `standalone`입니다. 허브는 `hub.managementPublicOrigin`, 로컬에만 열리는 `hub.managementIngress`(없으면 `enabled:false`), 정확한 `remoteGui.allowedTailscaleUsers`(없으면 빈 목록)를 사용합니다. 클라이언트 데이터 키는 `config.json`이 아니라 `service-api-token`에 저장되며 교체 중에는 `service-api-token.prev`가 잠시 생길 수 있습니다. 사용량 기록은 서로 복제하지 않습니다. `remoteGui.allowInsecureHttp`는 이전 strict-schema 설정을 계속 읽기 위해서만 남겨 둔 폐기된 no-op입니다. 설정에서 제거하세요. 페어링 grant는 loopback 또는 인증된 HTTPS에서만 허용되며, 이 값을 `true`로 설정해도 평문 HTTP 페어링은 다시 활성화되지 않습니다. + +## Codex 할당량 네트워크 진단 + +메인 Codex 계정 행의 `quotaRefresh`는 할당량 조회 결과를 분류하는 진단값입니다. 남은 할당량이나 모델 접근 권한을 뜻하지 않으며, 캐시를 쓰거나 조회하지 않았다면 생략될 수 있습니다. 요청은 명령을 입력한 터미널이 아니라 실행 중인 프록시 서비스의 환경을 따릅니다. `proxy`를 지정하지 않으면 기존 환경을 유지하고, `"auto"`는 시작할 때 Windows의 정적 프록시 설정만 읽습니다. PAC/WPAD, SOCKS 전용 설정과 실행 중 변경은 자동으로 반영하지 않습니다. TUN에서 성공했다고 HTTP 프록시 경로도 정상이라는 뜻은 아닙니다. 명령과 상태값은 [네트워크 진단(영문)](/reference/configuration/server/#codex-quota-network-diagnostics)에서 확인하세요. 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 e8347adfaa..a9a54c69c3 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -147,12 +147,15 @@ Authorization: Bearer | `GET /api/models` | 대시보드/CLI model 행을 반환합니다 | 수집이 포화 상태이면 `catalog_busy` | | `GET /api/client-config?client=...` | 지원되는 파일 연동의 읽기 전용 client config를 만듭니다 | 400 지원되지 않는 client; 503 catalog 사용 불가 | | `PUT /api/disabled-models` | 공유 disabled-model 목록을 교체합니다 | 400 잘못된 JSON | -| `PUT /api/model-visibility` | provider 또는 model 수준의 visibility를 원자적으로 변경합니다 | 400 잘못된 provider, scope, target, 또는 본문 | +| `PUT /api/model-visibility` | provider 또는 model 수준의 visibility를 원자적으로 변경합니다 | 400 잘못된 provider, scope, target, 또는 본문; 409 `initial_model_selection_pending` (목록을 새로고침한 뒤 다시 시도하세요.) | | `GET, POST /api/custom-models` | custom model을 나열하거나 하나를 추가합니다 | 400 잘못된 필드; 404 provider 없음; 409 중복 model | | `PUT, DELETE /api/custom-models/{id}` | custom model 하나를 수정하거나 삭제합니다 | 400 잘못된 id/필드; 404 찾을 수 없음; 409 중복 model | | `GET, PUT /api/selected-models` | provider allowlist와 가용성을 읽거나 allowlist 하나를 교체합니다 | 400 provider/body 누락; 404 알 수 없는 provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | 프리셋 정보를 읽거나 preset/all/custom 모드를 선택합니다 | 400 잘못된 mode 또는 지원하지 않는 프리셋; 404 알 수 없는 provider; PUT 409 `initial_model_selection_pending` | +수동 모델은 Models 대시보드에서 provider와 model ID가 같은 행을 대체합니다. OpenAI 수동 행은 `openai/`을 유지하며 표시 여부를 바꿀 수 있습니다. 수동 행을 삭제하면 계정 한정자가 없는 네이티브 행이 다시 나타납니다. 계정 한정자가 있는 네이티브 행은 별도로 유지됩니다. 네이티브 경로나 계정 권한은 바뀌지 않습니다. OpenAI의 비네이티브 표시 대상은 설정된 수동 모델과 일치해야 합니다. + + 신뢰할 수 있는 초기 모델 목록을 확보하기 전에는 유효한 `PUT /api/selected-models`와 `PUT /api/model-presets` 요청도 HTTP 409와 `initial_model_selection_pending` 코드를 반환합니다. `GET /api/models` 등으로 모델 목록을 정상적으로 갱신한 뒤 재시도하세요. ### OAuth 계정, provider key, 데이터 평면 키 @@ -191,6 +194,15 @@ Authorization: Bearer | `GET, PUT /api/provider-context-caps` | 전역, 모든 provider, 또는 하나의 provider context cap을 읽거나 업데이트합니다 | 400 잘못된 요청; 404 알 수 없는 provider | | `GET /api/provider-presets` | 런타임 registry에서 파생된 GUI provider preset을 반환합니다 | — | +컨텍스트 상한 응답에는 `caps`(활성 상한)와 `values`(꺼도 유지되는 마지막 선택값)가 포함됩니다. +`value` 없이 공급자의 상한을 켜면 선택값을 복원하고, 처음 켤 때는 전역 `contextCapValue`를 씁니다. +OpenAI도 같은 규칙을 따르며, 스위치를 켠다고 별도의 922k 모드가 선택되지는 않습니다. +활성 상한은 모든 네이티브 윈도에 적용됩니다. 장문 컨텍스트를 지원하는 모델은 해당 모델의 지원 상한까지만 +확장할 수 있습니다. `{ "value": 600000, "setAll": true }`는 전역 값과 활성 상한만 갱신합니다. +상한이 꺼진 공급자는 선택값을 유지하고, 나중에 켜면 그 값을 복원합니다. +`value` 없이 `{ "setAll": true }`를 보내면 설정된 모든 공급자의 상한을 현재 전역 값으로 켜고, +저장된 선택값도 바꿉니다. 상한을 꺼도 선택값은 다시 불러온 뒤까지 유지되지만 제한으로 적용되지는 않습니다. + `provider_has_dependent_combos`는 안전 장치입니다. provider를 삭제하기 전에 종속된 combo를 제거하거나 수정하십시오. ### 사이드바 및 동의가 필요한 작업 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 a3b82e4f63..f7ff7f5f27 100644 --- a/docs-site/src/content/docs/ko/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ko/reference/proxy-formats.md @@ -27,7 +27,7 @@ Responses 표현이 이 연결의 중심입니다. 네이티브 호환 경로는 | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `[DONE]`으로 끝나는 `chat.completion.chunk` SSE | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Anthropic token count | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | 해당 없음 | -| 모델 탐색 | `GET /v1/models` | 세 가지 카탈로그 계약 중 하나 | 해당 없음 | +| 모델 탐색 | `GET /v1/models` | 카탈로그 또는 명시적 Desktop 스냅샷 | 해당 없음 | | Voice and Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | 릴레이된 call-creation 응답 | 별도의 sideband WebSocket이 양방향 프레임을 릴레이함 | | Responses compaction | `POST /v1/responses/compact` | 대체 히스토리 JSON | 해당 없음 | @@ -197,10 +197,17 @@ Responses로 변환되어 일반적으로 라우팅된 뒤, Anthropic JSON 또 { "input_tokens": 123 } ``` +해석되지 않은 날짜형 Desktop ID는 탐색 결과에서 빠진 실제 네이티브 모델일 수도 있습니다. +정보가 부족해 ID를 해석할 수 없으면 Messages와 count-tokens는 고정된 `desktop_model_mapping_unavailable` 오류와 +HTTP 503을 반환합니다. 모델이 잘못됐다는 판정은 아닙니다. 미등록 레거시 해시 별칭은 계속 +HTTP 400을 반환합니다. 두 경우 모두 날짜 제거나 다른 경로로의 폴백은 하지 않습니다. +알려진 ID, 등록된 매핑, 정확한 `modelMap` 일치와 인식된 실제 네이티브 ID의 처리는 유지됩니다. +모델 탐색을 갱신하거나 연결된 허브 프로필을 다시 적용한 뒤 시도하세요. 재시도만으로 +해결된다는 보장은 없습니다. + ## `GET /v1/models` -같은 경로가 서로 호환되지 않는 카탈로그 envelope를 기대하는 세 가지 클라이언트를 모두 처리합니다. -`client_version`이 함께 있지 않으면 Anthropic 형식이 우선합니다. +`format=desktop-config`를 지정하지 않으면 다음 기본 카탈로그 계약을 사용합니다. | 계약 | 트리거 | 최상위 형식 | 모델 id 동작 | | --- | --- | --- | --- | @@ -208,6 +215,29 @@ Responses로 변환되어 일반적으로 라우팅된 뒤, Anthropic JSON 또 | Codex 카탈로그 | `client_version` 쿼리 파라미터 | `{ "models": [...] }` | 네이티브 및 라우팅 항목은 더 풍부한 Codex 카탈로그 필드, 표시 여부, effort, WebSocket, 다중 에이전트 메타데이터를 담음 | | 일반 OpenAI list | 어느 트리거도 아님 | `{ "object": "list", "data": [...] }` | 보이는 네이티브 id는 그대로이며, 라우팅 id는 alias 또는 `provider/model` | +### Desktop 설정 스냅샷 + +`GET /v1/models?ids=desktop&format=desktop-config`는 user-agent와 관계없이 Desktop +스냅샷을 선택합니다. 응답은 `{ "version": 1, "models": [...] }`이며 +`Cache-Control: no-store`를 포함합니다. 연결된 클라이언트는 `Accept: application/json`, +`anthropic-version: 2023-06-01`과 기존 데이터 자격 증명을 보냅니다. 관리자 토큰이나 프로필 +업로드는 필요하지 않습니다. 항목은 Codex 카탈로그 행이 아니라 허브가 발급한 Desktop 설정용 모델입니다. + +이 형식에 `ids=cli` 또는 `client_version`을 함께 보내면 HTTP 400을 반환합니다. 형식 선택자가 +없으면 위의 기본 응답 계약을 유지합니다. Claude가 꺼져 있으면 +`{ "version": 1, "models": [] }`를 반환하며, 연결된 Desktop apply는 사용 불가로 처리하고 +대체 프로필을 쓰지 않습니다. 버전 1 대신 일반 카탈로그를 반환하는 구형 허브는 지원하지 않으며, +클라이언트가 로컬에서 만든 ID로 대신 적용하지 않습니다. + +스냅샷은 읽기 전용 모델 목록이며 키 회전이나 프로필 업로드 API가 아닙니다. 연결된 Desktop의 +키 이전·복구·연결 해제는 기존 클라이언트 수명주기에서 처리합니다. 회전은 모델 항목과 선택을 +유지하며 CLI의 `rotation`은 `committed`와 `rolled_back`을 구분합니다. 연결 해제는 관리 설정을 +복원하거나 확인된 구형 프로필을 표준 모드로 전환하고, 사용자 필드와 이후의 유효한 선택을 +보존합니다. 충돌이나 미완료 복구를 완료로 표시하지 않습니다. 디스크 변경을 읽으려면 Desktop을 +재시작해야 하며, 연결 해제는 허브 키를 자동 폐기하지 않습니다. +[Claude Desktop 안내](/ko/guides/claude-code/)를 참고하세요. thinking 재전송과 프롬프트 캐시는 +별도 [#3719](https://github.com/lidge-jun/opencodex/issues/3719)에서 다룹니다. + ## `POST /v1/live`와 Realtime sideband `POST /v1/live`는 ChatGPT/Codex App Frameless call-creation 표면을 받습니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 1db98357d3..03c56f329b 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -23,12 +23,31 @@ adapter own retries/timeouts, while `runTurn` supports transports that cannot be HTTP fetch followed by one response stream. [`bridge.ts`](/reference/architecture/#the-bridge) then turns the events into Responses SSE. +## External task input on translated Responses routes + +Codex task coordination can deliver input as `function_call_output` with nonblank +`id`, `name` and `namespace` fields and no `call_id` property. OpenCodex maps this +complete envelope to a user message before adapter translation. Its output must be +nonblank text or a fully supported array of text and `input_image` URL parts. Text +and image order are preserved; image detail `original` maps to `high`. + +Empty content, malformed or opaque parts, file-id-only images and partial envelopes +remain invalid. Ordinary function/custom tool results still require a nonempty +`call_id`. The envelope metadata identifies a compatibility shape and grants no +additional permissions. Native passthrough and compaction retain their raw-body rules. + ## `openai-chat` **Targets:** OpenAI **Chat Completions** (`POST {baseUrl}/chat/completions`; a trailing `/chat/completions` or `/` on `baseUrl` is stripped first) and every compatible provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local), and more. **Auth:** `key` (Bearer). +For xAI, the resolved upstream adapter can be `openai-chat` or `openai-responses`, +depending on model defaults and explicit `modelAdapters` overrides. Both support +public xAI API-key authentication and Grok CLI OAuth. The usage log's +[`attempts[].credentialSource`](/reference/management-api/) follows that resolved +transport; it does not infer subscription attribution from the inbound protocol. + - Converts internal messages to OpenAI roles; maps tools to `{type:"function", function:{…}}` and `tool_choice` (`auto`/`none`/`required` or a named function). - **Tool-result images** ride in a follow-up user vision message (`image_url` parts) released once @@ -49,6 +68,15 @@ provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local), and mor tiers, accepts reasoning deltas from either `delta.reasoning_content` or `delta.reasoning`, requests streamed usage with `stream_options.include_usage`, and reads usage from non-stream response envelopes. +Streaming tool calls retain their identity when a provider first sends an ID, +then associates that ID with an index, and later sends index-only argument +fragments. Those fragments assemble into one call with the original name and +complete arguments; parallel calls retain separate identities. +When present, streamed tool-call indexes must be non-negative safe integers. Non-numeric +values and negative, fractional, or unsafe numbers terminate the stream with an upstream +error before identity matching. Missing and null indexes remain absent-index placeholders; +numeric strings are not coerced. + ## `ollama-native` **Targets:** Ollama's own **Chat API** (`POST /api/chat`) rather than its OpenAI-compatible @@ -95,11 +123,23 @@ body and response, with narrow compatibility rewrites for routed gateways. `forward` uses configured static headers without relaying caller authorization; `key` uses the configured provider key. +Adapter selection does not select the upstream transport. Eligible requests can use the +[upstream WebSocket proxy route](/reference/proxy-formats/#json-and-sse-output); invalid or unsupported +WebSocket proxy settings fall back to HTTP/SSE. HTTP fetch-based Responses handling uses Bun's +HTTP proxy rules and does not inherit the WSS-specific `ALL_PROXY` fallback. + Noncanonical Responses gateways receive Codex's client-executed `tool_search` declaration as a collision-safe public function tool. Matching request history and JSON/SSE function calls are translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward keeps the native private type unchanged. +For OpenCode Go at `https://opencode.ai/zen/go/v1`, requests with `authMode` other +than `"forward"` convert plaintext Codex `agent_message` items into public user messages, preserving content parts and readable author/recipient +metadata. This conversion leaves encrypted or unknown content unchanged and does not apply +to other destinations. Providers using `authMode: "forward"` retain these items unchanged. +See [Go agent messages](/reference/configuration/providers/#opencode-go-session-and-agent-messages) +for the separate opt-in encrypted-task recovery behavior. + The canonical ChatGPT Codex forward destination also normalizes two public Responses shapes that its stricter backend rejects: fully textual `system` messages inside `input` are appended to the top-level `instructions` string in request order, and the top-level `truncation` field is removed. @@ -136,6 +176,19 @@ of the HTTP retry loop. ChatGPT account id, and the OpenAI beta/originator/session headers. This is the ChatGPT-login path that also powers the [sidecars](/guides/sidecars/). +## Command Code session affinity + +The OAuth `command-code` adapter derives an opaque `x-session-id` from the client +thread identity, then the reasoning-replay conversation identity. When neither is +available, it uses a prompt-cache key only if the integration has explicitly +classified that key as belonging to one conversation. Shared or unclassified cache +keys do not establish session affinity; requests without a usable identity receive +a fresh session ID. Recovery and cached-history replay preserve this classification. + +The API-key `commandcode` provider uses the `openai-chat` adapter and supports +forwarding `prompt_cache_key`. This is separate from the OAuth adapter's session +header and does not guarantee a provider cache hit. + ## `anthropic` **Targets:** Anthropic **Messages** (`/v1/messages`). @@ -214,6 +267,13 @@ of the HTTP retry loop. - Builds Kiro `conversationState`, maps Codex tools and tool results, and sends image blocks supported by the Kiro wire. +- Coalesces adjacent outputs from the same original tool call into one Kiro result. Text remains + ordered, images retain the existing per-message limits, and any error flag remains set. User, + developer, assistant or another tool's output ends the group. Distinct original IDs that map + to the same normalized Kiro ID are rejected. +- Combined outputs keep real text and failure information without inserting an empty-output hint + for a later blank chunk. A single result keeps its existing normalization; an entirely text-empty + group receives one fallback, with neutral wording when images or an error flag are present. - Treats a client `parallel_tool_calls: true` value as permission rather than a wire requirement. Kiro remains serialized: the routed catalog advertises no parallel-tool capability and the adapter sends no parallel-control field upstream, but ordinary Codex tool turns are not rejected @@ -344,6 +404,13 @@ compatibility pair: `agent.v1.AgentService/RunSSE` for server output and broader built-in executor and bypasses Codex approval/sandbox semantics, and legacy `unsafeAllowNativeLocalExec: true` remains equivalent only when `nativeLocalExec` is unset. +Codex-compatible shell schemas retain sandbox permissions, justification, reusable +prefix rules and login mode. Freeform tools expose one required string `input` +and preserve its tool-specific guidance, such as the required patch envelope; +bare `exec_command` and `shell_command` names are reserved for non-freeform shell +bridges. Namespace a custom freeform tool that uses either name. These schema +declarations do not grant approval or change execution policy. + ## `azure-openai` (alias: `azure`) **Targets:** **Azure OpenAI**. Wraps `openai-responses` (so also `passthrough: true`). @@ -361,3 +428,18 @@ Shared helpers used by the vision-aware adapters: Anthropic/Google image blocks. - `contentPartsToText(content)` — flatten content parts to text for text-only tool messages (an undescribed image becomes a short `[image]` marker, never a token-exploding base64 blob). + +## Grok Build terminal snapshots + +Requests marked with `x-opencodex-grok: 1` opt into a narrow Responses terminal +repair. If `response.completed.response.output` is missing or empty, opencodex +can reconstruct it from real, uniquely indexed, contiguous `output_item.done` +items whose raw fields satisfy the supported shapes. Deltas alone do not create +output. Malformed, contradictory, duplicated, gapped or oversized evidence keeps +the empty terminal unchanged; failed and incomplete responses never become success. + +The marker is a client-selected compatibility option, not authenticated identity +or a permission grant. Unmarked clients retain their existing behavior. This +repair runs before the separate provider `responsesSnapshotRepair` option and +does not enable that broader lifecycle repair. Existing tool-search, custom-tool, +function-completion and undeclared-tool handling keep their established order. diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 0e91777061..e75a2b6241 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -237,6 +237,12 @@ Run opencodex as a login-managed background service (macOS **launchd**, Linux ** Windows **Task Scheduler**) that auto-starts on login and auto-restarts on crash. Service runs set `OCX_SERVICE=1` so a restart does not churn the Codex config. +Windows Task Scheduler installs use normal process priority (`Priority=4`). The older background +priority (`7`, also the scheduler default when omitted) can delay the proxy's health responses under +CPU contention, making the tray report Offline even while the process is alive. After upgrading, +run `ocx service repair` to migrate that registered priority and restart the service. This migration +may request UAC approval; a priority already set to normal or high does not itself trigger replacement. + The Windows wrapper verifies its baked Bun runtime and CLI entry before every start attempt. If an interrupted package update removed either file, it logs one `installation is incomplete` message and stops instead of retrying the same missing executable every five seconds. Reinstall opencodex, then 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 3bcb6092bc..cc2471a527 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -58,6 +58,28 @@ Use `--api-key` or an OAuth login for anything secret. ## Authentication +### Diagnosing missing main-account quota + +`ocx account list openai --quota --refresh --json` includes a `quotaRefresh` object on +the main-account row when that operation attempts a WHAM usage read. The existing +`GET /api/codex-auth/accounts?refresh=1` response exposes the same diagnostic. + +Its `status` is `ok`, `not_reported` (no parseable quota in a successful response), +`http_error`, `timeout`, `network_error`, `invalid_response`, or `internal_error`. +Only `http_error` includes a numeric `httpStatus`. No raw response, error message, +credential, or account identifier is included in this object. Cache-only reads, +credential deferrals, and invalidated account snapshots omit it; older servers +also omit it. Absence is not proof of success. A non-success HTTP status remains +`http_error` even if its error body cannot be read; `timeout` and `network_error` +describe failures before headers or while reading a successful response. + +A valid login does not guarantee that this separate usage request succeeds. +These categories do not change authentication, account selection, or quota +freshness rules, and do not turn unknown quota into zero usage. This diagnostic +currently covers the native main account, not pool-account refreshes. When +reporting missing quota, share the category and HTTP status rather than credential +files or a raw network capture. + ### `ocx login ` Start the provider's registered login flow. OAuth providers open a browser and store auto-refreshed @@ -75,6 +97,16 @@ ocx login xai ocx login anthropic ``` +OAuth reauthentication preserves operator settings such as model selections, pricing overrides, +and account failover preferences. Login-owned transport/authentication fields and registry-owned +catalog metadata are refreshed. A live-discovery provider keeps its selected default model; a +static provider can replace a default that no longer exists in its refreshed catalog. + +For Antigravity, an upstream `401` can refresh the rejected account’s OAuth credential and +retry the request once. The retry uses that credential’s Cloud Code Assist project. If refresh +fails or no usable project is available, the request returns an authentication error; use the +reauthentication flow above. A second `401` does not start another refresh/retry cycle. + A proxy that is already running picks up the new credential without a restart: the CLI asks it to reload that one provider from disk, and the request carries no credential of its own. If the running proxy cannot accept that request — most often because it started from a build that predates diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 8b1c536032..54affc94ad 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -117,8 +117,9 @@ opencodex skips disabled, unroutable, unhealthy, cooling-down, or quota-threshol availability snapshot is cached for `subagentModelFallbackPollMs`. Encrypted child tasks restrict the chain to canonical native ChatGPT targets plus direct key-auth Responses routes explicitly trusted with `allowEncryptedV2AgentTasks: true`; if none can consume the encrypted payload, the -request fails instead of routing unreadable ciphertext elsewhere. Combo routing remains -canonical-native-only. +request fails instead of routing unreadable ciphertext elsewhere. Combo routing first tries an +available canonical native target; when none is selectable and `agentTaskRecovery` is enabled, +an encrypted `NEW_TASK` is recovered once before routed combo dispatch. ```json { @@ -203,9 +204,13 @@ Enable this only when the additional authenticated request, quota use, plaintext and private-backend dependency are acceptable. Prefer a native ChatGPT child or v1 heterogeneous delegation when they are not. -This recovery path applies to direct-routed children. At most 32 recovery requests can be active at -once; additional misses fail closed. Combo routing keeps its existing native-only filter for -encrypted tasks and does not invoke recovery. +This recovery path applies to direct-routed children and encrypted combo `NEW_TASK` spawns. At +most 32 recovery requests can be active at once; additional misses fail closed. A combo with an +available canonical native target still sends ciphertext directly; recovery runs only when no +native target is selectable. After a stored Pool account's refresh and same-account replay are +exhausted, recovery can use the incoming caller credential for one available routed target without +trying another native account. Policy refusals remain terminal. Failed recovery, exhausted targets, +or unavailable targets still fail closed without forwarding ciphertext to a routed provider. ## Effort caps diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d6adbdac9c..cd5c9657d6 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -28,8 +28,9 @@ After GUI registration or OAuth login, the confirmation dialog lets you open the | `providers` | `Record` | — | Map of provider name to provider config. | | `openaiProviderTierVersion?` | `2` | set by migration | Marks the single option-aware OpenAI projection as complete. | | `disabledModels?` | `string[]` | — | Models hidden from Codex's catalog and `/v1/models`, but not blocked from direct proxy calls. A routed id is removed from listings. An account-qualified native id hides only that selector row; a bare native GPT id hides the bare row and every account-selector row for that model. The dashboard Models page exposes only routed and bare native rows; use this configuration field directly to hide one selector-qualified row. | -| `providerContextCaps?` | `Record` | `{}` | Per-provider Codex-visible context caps. A cap only lowers a known context window. | -| `contextCapValue?` | `number` | `350000` | Default value used by the dashboard context-cap controls. Changing it applies the value to every routed provider — including providers without an existing `providerContextCaps` entry — only when "apply to every routed provider" is toggled on; otherwise each provider keeps its own cap. | +| `providerContextCaps?` | `Record` | `{}` | Active provider context limits. Ordinary windows are lowered; native models with a supported long window can expand only up to their own supported ceiling. | +| `providerContextCapValues?` | `Record` | `{}` | Last selected provider limits, retained while disabled. These values do not activate a cap. An enabled value takes precedence over a remembered value. | +| `contextCapValue?` | `number` | `350000` | Default used on first enable. A later enable restores the selected provider value. Updating the global value with `setAll: true` changes enabled caps only; `setAll: true` without a value enables all configured providers at the current global value. | | `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by Codex Auth. Secrets live separately in `codex-accounts.json`. | | `pausedCodexAccountIds?` | `string[]` | `[]` | Accounts excluded from Pool selection until resumed, including the main `__main__` account when paused. | | `codexQuotaAutoRefresh?` | `Record` | `{}` | Per-Codex-login-account opt-in for automatic `fiveHour` and `weekly` window activation in Pool mode; Direct mode does not run this worker. In Providers/Codex Auth **Advanced settings**, one control enables or disables both supported windows across all current main and added accounts. New accounts are not opted in automatically. Enable skips windows absent from live WHAM data; disable also clears stale enabled windows. The UI reuses granular `/api/settings` writes, reconciles partial failures, and retries the original ON/OFF intent without replacing unrelated settings or completed reset markers. The API still rejects enabling an unavailable window with HTTP 409. At a reported reset time, opencodex sends one minimal non-stored Codex message using that account's quota and persists the activated timestamp. This does not apply to API-key providers. | @@ -136,7 +137,7 @@ predictions. Explicit provider/model price overrides still take precedence. | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key header style. Defaults to native `x-api-key`; valid only for key-auth `anthropic` providers. | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Multi-key pool. `apiKey` mirrors the active entry; each item has `id`, `key`, optional `label`, and optional numeric `addedAt`. | | `defaultModel?` | `string` | Model used when this provider is selected without an explicit model. | -| `models?` | `string[]` | Seed/fallback model list. With `liveModels: false`, these are the only discovered models. | +| `models?` | `string[]` | Seed/fallback model list. With `liveModels: false`, a nonempty `models` list is followed by `retainModels`; an empty or omitted `models` list instead seeds `defaultModel` (if configured), then `retainModels`, removing duplicate ids in first-seen order. | | `liveModels?` | `boolean` | Fetch the live catalog on start/sync (default `true`). Custom providers use `${baseUrl}/models`; built-ins may use a registry URL and filter. | | `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. | | `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. | @@ -738,8 +739,16 @@ container usually has no unlocked keychain session, so requests would fail close `${ENV_VAR}` reference in the service environment there instead. Env references are left untouched by `store`. -Set `liveModels: false` to expose only `models`. If `models` is empty or omitted, the provider exposes -no routed models. Live discovery rejects more than 4 MiB or 2,000 raw model rows before caching; +With `liveModels: false`, an empty or omitted `models` list seeds the configured `defaultModel` +first, followed by `retainModels`; duplicate ids are removed while preserving first occurrence. +A nonempty explicit `models` list instead seeds `models` followed by `retainModels`, without +implicitly adding a different `defaultModel`. That default can still be listed explicitly in +`models` or `retainModels`. If none of these fields supplies an id, the static seed is empty. +This is seed order, not a promise of final picker order. `selectedModels`, `disabledModels` and +provider-disabled policy still apply. `authMode: "forward"` keeps its separate branch and does +not use this routed static seed. These rules do not change live-discovery failure fallback. + +Live discovery rejects more than 4 MiB or 2,000 raw model rows before caching; built-in presets may use lower limits and filter to chat-eligible rows. Oversized or malformed results follow stale/configured fallback. A valid zero-eligible result remains authoritative and is not silently replaced or truncated. @@ -810,3 +819,51 @@ ids with context `922000` and max input `922000`; OpenRouter seeds `openai/gpt-5 "visionSidecar": { "enabled": true } } ``` + +## OpenCode Go reasoning efforts + +Go catalog rows preserve their configured reasoning efforts exactly, including during +catalog sync. OpenCodex does not append synthetic `max` or `ultra` choices to these rows. +Use `modelReasoningEfforts` and `modelDefaultReasoningEfforts` for each model's accepted +upstream values. Key these per-provider maps by upstream model ID, not the routed +`opencode-go/` catalog slug. For example, a configured `["high", "max"]` list +remains exactly those two choices; a configured `["high", "xhigh"]` list does not gain `max`. +See the [OpenCode Go model list](https://opencode.ai/docs/go/#models) for the current roster. +A configured subset can exclude the lower tiers. Other providers retain their existing behavior. + +For a native-first picker, include native ids in `modelPickerOrder` followed by the +routed ids. This orders the complete picker while preserving OpenCodex's separate natural-priority +guidance calculation. Native Codex's advertised five follow picker priority and may change; +exact-name override eligibility is not limited to that advertisement. Routed-only orders keep +their previous behavior. See the +[ordering migration note](/guides/model-ordering/#migration-note-native-ids-in-existing-orders). +`modelDisplayNames` on a provider controls readable labels without changing wire ids. + +## OpenCode Go session and agent messages + +With the [`openai-responses` adapter](/reference/adapters/#openai-responses) and +base URL `https://opencode.ai/zen/go/v1`, plaintext Codex `agent_message` items +become user messages when `authMode` is not `"forward"` (for example, `"key"`). +Providers using `authMode: "forward"` retain these items unchanged. This conversion is scoped to that destination, including +renamed provider entries; other Responses destinations keep their input unchanged. +Author and recipient remain explicit text metadata, and the content parts are preserved. +Encrypted and unknown content is not normalized; native encrypted tasks still require the +separate opt-in [task recovery](/reference/configuration/agents/#encrypted-v2-task-recovery). + +With task recovery enabled, replayed `NEW_TASK` and `MESSAGE` items reuse a cached assignment only +after validating the caller and matching the parent-thread scope. Replay restoration +does not make a new recovery request or extend cache expiry. Expired or unseen +ciphertext is not replaced. Fresh encrypted `NEW_TASK` and `MESSAGE` items use the same +opt-in recovery path, including native-parent `send_message` delivery. Message type, +sender, recipient, parent scope and caller credentials remain part of validation or cache identity. + +When a request contains several agent messages, cached replay restoration checks each +message independently. The cache separates message type, sender, recipient and ciphertext +within the admitted caller/account and parent scope. Fresh recovery only handles the +current tail message (ignoring trailing `compaction_trigger` or `additional_tools` metadata). +It does not batch-recover unseen historical messages; those remain unchanged. A cache miss +or expiry does not extend the history-recovery contract. + +Sender and recipient on Go Responses are context for the receiving model, not a new +machine-readable routing protocol. Tool routing continues to use the existing collaboration +contracts. diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 832817a205..b793f159c1 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -50,6 +50,56 @@ If an older development build changed resume-history metadata before backup supp It force-relabels every user-message `opencodex` row, including legitimate dedicated-provider history; review the full-scope warning in the lifecycle reference before running it. +## Codex quota network diagnostics + +The main Codex account row may include `quotaRefresh` when a quota fetch was +attempted. This describes that fetch, not remaining quota, model access or +permission to retry. Cached reads and rows without a fetch may omit it; absence +does not mean success. A `null` quota value means unavailable, not zero quota. + +To request fresh data and display only the diagnostic in PowerShell: + +```powershell +$quotaReport = ocx account list openai --quota --refresh --json | ConvertFrom-Json +$quotaReport.accounts | + ForEach-Object { if ($_.quotaRefresh) { $_.quotaRefresh } } | + ConvertTo-Json -Depth 3 +``` + +If no diagnostic is present, this projection produces no diagnostic object. Share +only these fields when comparing network modes, rather than the full account list. + +| `quotaRefresh.status` | Meaning | +| --- | --- | +| `ok` | The fetch completed and a quota object was parsed. | +| `not_reported` | The response contained no usable quota object. | +| `http_error` | The upstream returned an HTTP failure; `httpStatus` contains its status code. | +| `timeout` | The quota fetch timed out. | +| `network_error` | The request failed before a classified HTTP response. | +| `invalid_response` | The response was not a usable quota document. | +| `internal_error` | An internal refresh step failed. | + +Only `http_error` includes `httpStatus`. Other statuses do not imply HTTP 0 or an +account entitlement problem. + +### Which proxy path is used? + +The running proxy service fetches quota. It uses its own environment, not the +interactive shell that later runs `ocx account list`. Configure the service's +proxy setting or environment, then restart it; changing variables in another +terminal does not update an already running service. + +An unset `proxy` leaves inherited proxy variables unchanged. An explicit HTTP(S) +proxy URL fills `HTTP_PROXY` and `HTTPS_PROXY` only where they are unset. +`"proxy": "auto"` reads the Windows static WinINET proxy once at startup; existing +proxy environment variables take precedence. Auto discovery does not resolve +PAC/WPAD, SOCKS-only settings or live proxy changes. Use a supported static HTTP +proxy setting or an explicit HTTP(S) proxy URL when needed. + +Compare the diagnostic on the same machine and account under the two network +modes. A successful TUN test alone does not identify why the service's HTTP proxy +path failed, and does not establish a general fix. + ## Remote access The default `127.0.0.1` bind is loopback-only. A non-loopback address such as `0.0.0.0` requires diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 784c6e17f5..a12303cfd7 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -94,7 +94,47 @@ For the concepts behind the model roster and encrypted worker-task behavior, see | `DELETE /api/client-integrations/journal?opId=...` | Retire one older rollback operation and remove its snapshot when possible. Success returns `snapshotRemoved`; `false` means cleanup was retained for maintenance retry. | 400 missing `opId`; 404 missing or already retired operation; 409 newest operation for that client | Deletion appends a tombstone instead of rewriting the journal. The newest operation for each client -is protected server-side so the current undo point remains available. +is protected server-side so the current undo point remains available. For Aside, protection is +per profile, and journal rows include `profileId`. + +### Aside profile controls + +Use these dedicated paths with a compatible running proxy. `{profileId}` is a registered +nonnegative integer account ID returned by the profile list; it is not a browser path. + +| Method and path | Purpose | Notable errors | +| --- | --- | --- | +| `GET /api/client-integrations/aside/profiles` | List `profiles[]`, desired `enabledCount`/`allEnabled`, actual `appliedCount`, and `total` | HTTP 200 may contain an empty, unsafe aggregate with an `error` when discovery is unavailable | +| `PUT /api/client-integrations/aside/profiles` | Set every registered profile's desired state and apply it; body `{ "enabled": true }`, with optional `overwriteConflict` when enabling | 400 invalid body; 409 operation busy; 500 preference-save failure; 207 per-profile refusals | +| `GET /api/client-integrations/aside/profiles/{profileId}` | Read one profile's desired `enabled` and actual integration status | 400 invalid ID; 404 unregistered profile | +| `PUT /api/client-integrations/aside/profiles/{profileId}` | Change one profile with the same body as bulk PUT, leaving sibling preferences unchanged | 400 invalid body/ID; 404 unknown profile; 409 busy or refusal; 500 save/write failure | +| `GET /api/client-integrations/aside/profiles/journal` | List history across registered Aside profiles | Rows include `profileId`, snapshot availability, `undoable`, and `deletable` | +| `GET /api/client-integrations/aside/profiles/{profileId}/journal` | List one profile's history, including matching legacy operations | 400 invalid ID; 404 unknown profile | +| `DELETE /api/client-integrations/aside/profiles/journal?opId=...` | Retire an older operation, resolving its profile from history | 400 missing ID; 404 missing operation; 409 newest operation for its profile | +| `DELETE /api/client-integrations/aside/profiles/{profileId}/journal?opId=...` | Retire an older operation belonging to the selected profile | Same deletion errors; an operation cannot target a different profile | +| `POST /api/client-integrations/aside/profiles/{profileId}/restore` | Undo an operation using `{ "opId": "..." }`; optional `confirmDrift: true` permits replacing later edits | 404 missing operation/profile; 409 busy, mismatch, or required drift confirmation; 410 expired snapshot; 500 save/write failure | +| `POST /api/client-integrations/aside/sync` | Refresh enabled profiles through the server's mutation owner; body `{}` | 400 nonempty body/profile selector; 409 busy; 207 per-profile refusals | + +Bulk PUT returns `{ ok, clientId, changed, state, message, results }`; each result identifies +its `profileId` and reports the writer outcome. Sync returns `{ ok, clientId, results }`, with +per-profile refresh outcomes. Both return HTTP 200 when all returned attempts succeed and +HTTP 207 with `ok: false` when any attempt refuses. HTTP 207 is a partial-result envelope, +including when every attempted profile refuses: inspect each result rather than treating a +2xx response as complete success. A successful no-op can have `changed: false`; sync does not +attempt disabled profiles. Single-profile writes and restores return HTTP 200 on success or +the corresponding error status on refusal. + +Explicit changes save desired preferences before writing files. A preference-save failure +leaves profile files unchanged. A later file refusal preserves saved intent and successful +sibling writes; inspect the affected profile before retrying. Refusals may include +`snapshotPath` and `residual: true` when recovery did not finish. Restore also reconciles the +target profile's desired state, so the next sync does not reverse Undo. Deletion returns +`snapshotRemoved`; `false` means snapshot cleanup still needs maintenance. + +The legacy `GET, PUT /api/client-integrations/aside` aliases remain available. New clients +should use the dedicated paths above so an older proxy cannot ignore a profile selector. +See [Aside profile controls](/guides/integrations/#aside-profile-controls) for CLI commands and +the proxy upgrade, restart, and retry sequence. ### Combos @@ -145,6 +185,14 @@ See [Combos](/guides/combos/) for target strategies, cooldowns, aliases, and rou | `POST /api/storage/cleanup-policy/run` | Start a manual cleanup-policy run | 409 `already_running`; 500 `cleanup_failed` | | `GET /api/storage/cleanup-policy/test-stream` | Test-only policy stream hook | 404 `not_found` when unavailable | +New xAI attempts in `usage.jsonl` include a request-time `credentialSource`: `grok-oauth` +for the resolved Grok CLI OAuth transport, or `xai-api-key` for the public xAI API key +transport. This fixed label contains no credential or account identifier. It belongs to +each item in `attempts`, so a combo's aggregate token total must not be attributed to its +final provider. Custom destinations and historic rows omit the field; consumers must not +infer subscription usage from the current configuration, model name, or inbound API key. +The log reports usage, not subscription invoice amounts. + `GET /api/usage` reads `~/.opencodex/usage.jsonl` from the beginning through the current ledger snapshot on a cold start. It processes fixed 1 MiB chunks and retains compact aggregate state rather than every normalized request row. Later refreshes validate the previous line boundary and fold only @@ -191,14 +239,31 @@ first and submit the returned digest. Prefer quarantine when recovery may be nee | `GET /api/models` | Return the dashboard/CLI model rows | `catalog_busy` when gathering is saturated | | `GET /api/client-config?client=...` | Build a read-only client config for any supported file integration | 400 unsupported client; 503 catalog unavailable | | `PUT /api/disabled-models` | Replace the shared disabled-model list | 400 invalid JSON | -| `PUT /api/model-visibility` | Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body | +| `PUT /api/model-visibility` | Atomically change provider- or model-level visibility | 400 invalid provider, scope, target, or body; 409 `initial_model_selection_pending` (refresh the model list and retry) | | `GET, POST /api/custom-models` | List custom models or add one | 400 invalid fields; 404 provider missing; 409 duplicate model | | `PUT, DELETE /api/custom-models/{id}` | Edit or delete one custom model | 400 invalid id/fields; 404 not found; 409 duplicate model | | `GET, PUT /api/selected-models` | Read provider allowlists and availability, or replace one allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Read preset summaries or choose preset/all/custom mode | 400 invalid mode or unsupported preset; 404 unknown provider; PUT 409 `initial_model_selection_pending` | +A manual model replaces the Models dashboard row with the same provider and model ID. +For OpenAI, the manual row keeps `openai/` and supports the same visibility controls +as other routed models; removing it restores the bare native dashboard row. Explicit +account-qualified native rows stay separate. This does not rename bare native routes or +change account entitlements. Non-native OpenAI visibility targets must match a configured +manual model. + Valid PUT requests to `/api/selected-models` and `/api/model-presets` return HTTP 409 with code `initial_model_selection_pending` until a reliable initial model list is available. Refresh model discovery (for example, `GET /api/models`) and retry after it succeeds. +Successful visibility/selection writes to `/api/disabled-models`, `/api/model-visibility`, +`/api/selected-models`, and `/api/model-presets` report follow-up outcomes in `catalogRefresh` +and `clientIntegrations` when that refresh path runs. HTTP 200 and `ok: true` confirm the +selection save; they do not guarantee every client catalog updated. Inspect +`clientIntegrations[]` for `ok: false`, `client`, optional Aside `profileId`, and the refusal +`reason`; recovery details may also include `refusalReason`, `snapshotPath`, and `residual`. +The Models page keeps the saved selection and shows a separate client-refresh warning. +Inspect Integrations and resolve the reported issue before retrying `ocx sync`. Missing +outcome fields from an older server do not establish successful recovery. + ### OAuth accounts, provider keys, and data-plane keys | Method and path | Purpose | Notable errors | @@ -237,6 +302,18 @@ keys are not returned to dashboard clients. | `GET, PUT /api/provider-context-caps` | Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider | | `GET /api/provider-presets` | Return GUI provider presets derived from the runtime registry | — | +The provider context-cap response includes `caps` (active limits) and `values` (last selected +values, retained while disabled). Enabling a provider without `value` restores its selection, +or uses the global `contextCapValue` on first enable. This also applies to OpenAI: the switch +does not select a special 922k mode. An active cap bounds every native window; models with a +supported long-context window may expand only up to their own supported ceiling. +Updating the global value with `{ "value": 600000, "setAll": true }` changes only enabled +provider caps; disabled providers keep their remembered selections when later enabled. +In contrast, `{ "setAll": true }` without `value` enables every configured provider at the +current global value, replacing their remembered selections. Turning a cap off does not +activate its remembered value or erase the selection. + + `provider_has_dependent_combos` is a safety barrier: remove or edit the dependent combos before deleting their provider. diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index 77a67147ac..bba98afef1 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -28,7 +28,7 @@ should select among several targets. | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `chat.completion.chunk` SSE ending in `[DONE]` | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Anthropic token count | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | Not applicable | -| Model discovery | `GET /v1/models` | One of three catalog contracts | Not applicable | +| Model discovery | `GET /v1/models` | Catalog or explicit Desktop snapshot | Not applicable | | Voice and Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | Relayed call-creation response | A separate sideband WebSocket relays frames in both directions | | Responses compaction | `POST /v1/responses/compact` | Replacement-history JSON | Not applicable | @@ -83,6 +83,11 @@ This applies to both tee inspection and eager relay, including Windows rewrite t even when the upstream read rejects before the response-body cancellation hook runs. A terminal captured during the bounded post-disconnect drain retains its actual outcome. +If native passthrough rewriting fails, including when it exceeds the translation +buffer budget, the relay reports the failure without waiting for upstream inspection +to finish. It cancels the upstream work and emits `response.failed` followed by +`data: [DONE]`; a budget overflow uses the `translation_buffer_limit` error code. + Client-facing Responses SSE frames are limited to 4 MiB per frame, measured in raw bytes before the SSE block delimiter. On HTTP, an unterminated upstream frame that exceeds the limit fails closed with a synthetic `response.failed` event followed by `data: [DONE]`. On the Responses WebSocket @@ -113,6 +118,19 @@ the raw JSON frame and its SSE envelope at 4 MiB, and closes the upstream when i would overflow. That overflow emits a terminal downstream `response.failed` event followed by `[DONE]`. +The upstream WebSocket checks `NO_PROXY`/`no_proxy` first. Otherwise it uses the first non-empty +`HTTPS_PROXY`, `https_proxy`, `ALL_PROXY`, or `all_proxy` value; `HTTP_PROXY` alone does not proxy a +WSS connection. HTTP and HTTPS proxy URLs are passed to Bun. If the selected value is invalid or +uses an unsupported protocol, opencodex skips the WebSocket attempt and uses HTTP/SSE instead of +dialing the upstream directly. + +These rules belong to the upstream WebSocket transport, independently of the selected provider +adapter. HTTP fetch-based Responses requests, including SSE fallback, use Bun's HTTP proxy rules +and do not use `ALL_PROXY`. `config.proxy` fills missing `HTTP_PROXY`/`HTTPS_PROXY` values; the +resulting scheme-specific value also takes precedence over an existing `ALL_PROXY` for WebSocket. +For an HTTPS upstream that requires a proxy, set `HTTPS_PROXY` or `config.proxy`; `HTTP_PROXY` +alone leaves both WSS and its HTTPS fallback without a scheme-matched proxy. + Every terminal Responses usage object includes both detail objects, even when the provider did not report those details: @@ -284,10 +302,17 @@ documented estimate over system content, messages, and tools and return: { "input_tokens": 123 } ``` +An unresolved date-shaped Desktop ID can also be a genuine native model missing from discovery. +Messages and count-tokens return HTTP 503 with the fixed `desktop_model_mapping_unavailable` error when the available +evidence cannot resolve that ID; this does not establish that the model is invalid. Unknown legacy +hash aliases still return HTTP 400. Neither case strips the date or falls back to another route. +Known IDs, registered mappings and exact `modelMap` matches keep their existing behavior, including +recognized real native IDs. Refresh model discovery or reapply the connected hub profile before +trying again; retrying alone does not guarantee resolution. + ## `GET /v1/models` -The same route serves three clients that expect incompatible catalog envelopes. Anthropic flavor -wins unless `client_version` is also present. +Without `format=desktop-config`, the ordinary catalog contracts are: | Contract | Trigger | Top-level shape | Model-id behavior | | --- | --- | --- | --- | @@ -295,6 +320,29 @@ wins unless `client_version` is also present. | Codex catalog | `client_version` query parameter | `{ "models": [...] }` | Native and routed entries carry the richer Codex catalog fields, visibility, effort, WebSocket, and multi-agent metadata | | Plain OpenAI list | Neither trigger | `{ "object": "list", "data": [...] }` | Visible native ids are bare; routed ids are aliases or `provider/model` | +### Desktop configuration snapshot + +`GET /v1/models?ids=desktop&format=desktop-config` explicitly selects the Desktop snapshot, +independently of user-agent detection. The response is `{ "version": 1, "models": [...] }` +with `Cache-Control: no-store`. The connected client sends `Accept: application/json`, +`anthropic-version: 2023-06-01` and its existing data credential; no admin token or profile +upload is involved. Entries are the hub-issued Desktop configuration models, not Codex catalog rows. + +Combining this format with `ids=cli` or any `client_version` returns HTTP 400. Without the +format selector, the ordinary contracts above remain unchanged. When Claude is disabled, +the snapshot is `{ "version": 1, "models": [] }`; connected Desktop apply treats this as +unavailable and does not write a replacement profile. Old hubs returning an ordinary catalog +instead of version 1 are unsupported; the client does not fall back to locally generated IDs. + +The snapshot remains a read-only model-list contract; it is not a key-rotation or profile-upload +API. Connected Desktop key migration, recovery and disconnect operate through the existing client +lifecycle. Rotation preserves model entries and selections; CLI `rotation` distinguishes +`committed` from `rolled_back`. Disconnect restores owned settings or reports a known-legacy +standard fallback, preserving user fields and later valid selections. Conflicts or incomplete +recovery prevent a completion claim. Restart Desktop to load disk changes; disconnect does not +automatically revoke the hub key. See [Claude Desktop lifecycle](/guides/claude-code/). +Thinking replay and prompt-cache work remain separate in [#3719](https://github.com/lidge-jun/opencodex/issues/3719). + ## `POST /v1/live` and Realtime sideband `POST /v1/live` accepts the ChatGPT/Codex App Frameless call-creation surface. 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 3f4285cfe6..3f6c07a4aa 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -101,6 +101,63 @@ Proxy admission secret в любом provider-заголовке удаляет Отключается параметром `claudeCode.nativePassthrough: false`; другой адрес задаётся через `claudeCode.anthropicBaseUrl`. +## Claude Desktop через удалённый хаб + +На подключённой машине `ocx claude desktop apply` или `ocx claude desktop` получает снимок +Desktop с хаба и записывает его origin и точные выданные им ID моделей в локальную конфигурацию. +Локальные псевдонимы заново не создаются. Режимы static/hybrid копируют список моделей; +discovery-only использует origin хаба без встроенного списка. + +Профиль, распределение по семействам и значения по умолчанию принадлежат хабу. Измените их +на хабе, повторите применение на клиенте и заново выберите модель в Desktop. Это нужно и для +старых псевдонимов, созданных только на клиенте. `show`, локальное редактирование и import/export +остаются локальными операциями. При подключении `ocx claude desktop import --apply` +не поддерживается и отклоняется до сохранения; import без `--apply` остаётся локальным. + +Снимок читается с учётными данными существующего подключения для доступа к данным, без +администраторского токена и загрузки профиля на хаб. Старый несовместимый хаб, некорректный ответ +или пустой список Desktop приводят к отказу применения, без подстановки локального каталога или +loopback-адреса. Обновите или настройте хаб и повторите применение. + +Это изменение псевдонимов не исправляет отдельный запрос [#3719](https://github.com/lidge-jun/opencodex/issues/3719) о повторной передаче +`thinking` / `redacted_thinking` и кеше промптов. Сам по себе доступ к прокси не включает нативный +проброс Anthropic, но преобразованные маршруты Anthropic могут использовать кеш. Сохранение +блоков при повторной передаче и сравнение попаданий в кеш остаются отдельной работой. + +### Ротация ключей, восстановление и отключение + +Ротация и восстановление обновляют ключ в управляемом подключением профиле Desktop вместе +с локальными учётными данными. Повторять apply вручную ради смены ключа не нужно. ID моделей, +семейства, значения по умолчанию и текущий выбор профиля сохраняются; ротация не выбирает +управляемый профиль заново и не включает отключённую интеграцию. В JSON CLI +`rotation: "committed"` означает, что новый ключ активен, а `rotation: "rolled_back"` — что +предыдущий сохранён или восстановлен, а не отозван. Неопределённое или неполное восстановление +не выдаётся за успешную ротацию. + +Первое применение при подключении сохраняет прежние управляемые настройки и выбор профиля. +Повторное применение и ротация не заменяют эту исходную запись. `ocx disconnect` восстанавливает +настройки подключения, сохраняя добавленные пользователем поля и другие профили. Прежний выбор +возвращается только если управляемый профиль всё ещё выбран; более поздний выбор другого +действительного профиля сохраняется. Созданный профиль с пользовательскими добавлениями остаётся +читаемым в стандартном режиме. `--keep-catalog` сохраняет каталог, а не ключ подключения в Desktop. + +Старый управляемый профиль без исходной записи можно мигрировать, если он однозначно относится +к текущему хабу и распознанному ключу подключения. Это делают apply, ротация/восстановление или +прямое отключение, без нового флага и предварительного apply. Предупреждение объясняет, что +при отключении будет использован стандартный режим, поскольку прежние настройки не записаны. +Удаляются только настройки шлюза, принадлежащие подключению; пользовательские поля и отдельный +действительный выбор сохраняются. Это стандартный fallback, а не восстановление оригинала. + +Конфликты управляемых настроек, неизвестные ключи и повреждённые записи восстановления +сохраняются и сообщаются пользователю. Прерванная очистка продолжается только для того же +подключения, не удаляя новое и не объявляя неполное восстановление завершённым. До отключения +завершите восстановление ротации; при повторе отключения сохраняйте тот же выбор сохранения каталога. + +После применения, ротации/восстановления или возврата настроек полностью закройте и снова откройте +Claude Desktop: работающий процесс может хранить прежний ключ. Автоматического перезапуска нет. +Локальное отключение не отзывает ключ хаба и не стирает внешние копии; при необходимости +отзовите ключ на хабе отдельно. + ## Селектор /model («From gateway») Claude Code 2.1.129+ обнаруживает модели шлюза через `GET /v1/models?limit=1000` и показывает их @@ -136,6 +193,15 @@ user-agent `claude-code/*` получает читаемую CLI-форму, а декодирование Desktop-хеша → точное совпадение в `modelMap` → совпадение без даты (удаляется `-20250514`) → проброс. +Неразрешённый Desktop ID в формате даты может быть реальным нативным модельным ID, +отсутствующим в результатах обнаружения. Если имеющихся данных недостаточно для разрешения ID, +Messages и count-tokens возвращают HTTP 503 с фиксированной ошибкой `desktop_model_mapping_unavailable`; +это не доказывает недействительность модели. Неизвестные старые хеш-псевдонимы по-прежнему дают +HTTP 400. В обоих случаях дата не удаляется и другая маршрутизация не подставляется. Известные ID, +зарегистрированные сопоставления и точные записи `modelMap`, включая распознанные реальные +нативные ID, обрабатываются как прежде. Обновите обнаружение моделей или повторно примените +профиль подключённого хаба перед новой попыткой; один лишь повтор не гарантирует разрешения. + Каждая запись содержит отображаемое имя вида `gemini-3-pro (gemini)` и полные возможности модели (шкала уровней рассуждений, типы thinking) в официальном формате `ModelInfo`. Настоящие модели Anthropic сохраняют канонические id в обоих интерфейсах. @@ -237,6 +303,15 @@ Anthropic и автоматически срабатывает при упоми Порядок поиска: алиас обнаружения → точный id → id без датировочного суффикса (`-20250514`) → проброс. +Неразрешённый Desktop ID в формате даты может быть реальным нативным модельным ID, +отсутствующим в результатах обнаружения. Если имеющихся данных недостаточно для разрешения ID, +Messages и count-tokens возвращают HTTP 503 с фиксированной ошибкой `desktop_model_mapping_unavailable`; +это не доказывает недействительность модели. Неизвестные старые хеш-псевдонимы по-прежнему дают +HTTP 400. В обоих случаях дата не удаляется и другая маршрутизация не подставляется. Известные ID, +зарегистрированные сопоставления и точные записи `modelMap`, включая распознанные реальные +нативные ID, обрабатываются как прежде. Обновите обнаружение моделей или повторно примените +профиль подключённого хаба перед новой попыткой; один лишь повтор не гарантирует разрешения. + ## Матрица сайдкаров: веб-поиск и понимание изображений Не у всех маршрутизируемых моделей одинаковый набор серверных (hosted) инструментов и поддержка diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index e33bb6c835..9707a3ea44 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -304,8 +304,14 @@ Codex на встроенный провайдер `openai` и удалите л allowlist, никогда не попадёт в каталог. 2. **`disabledModels`** (верхний уровень) — скрывает модели и из каталога, и из `/v1/models`, а у голых нативных GPT-slug устанавливает `visibility: "hide"`. -3. **`liveModels: false` и пустой `models`** — если живое обнаружение выключено, а `models` пуст - или отсутствует, opencodex не показывает ни одной маршрутизируемой модели этого провайдера. +3. **`liveModels: false`** — При `liveModels: false`, если `models` пуст или отсутствует, начальный список содержит сначала + настроенный `defaultModel`, затем `retainModels`. Дубликаты удаляются с сохранением первого вхождения. + Если явно задан непустой `models`, за ним следует `retainModels`, а другой `defaultModel` автоматически + не добавляется. Его можно явно указать в `models` или `retainModels`. Если ни одно из этих полей + не содержит ID, начальный список пуст. Этот порядок не гарантирует итоговый порядок в селекторе. + Правила `selectedModels`, `disabledModels` и отключения провайдера продолжают действовать. + `authMode: "forward"` сохраняет отдельную ветвь и не использует этот статический список + маршрутизируемых моделей. Эти правила не меняют резервное поведение при сбое живого обнаружения. 4. **Cursor `GetUsableModels`** — адаптер Cursor получает модели через protobuf RPC `GetUsableModels`, а не через `/models`, поэтому изменение на стороне Cursor может менять видимые id независимо от остальных провайдеров. diff --git a/docs-site/src/content/docs/ru/guides/model-ordering.md b/docs-site/src/content/docs/ru/guides/model-ordering.md index d5a3683834..290194fcd2 100644 --- a/docs-site/src/content/docs/ru/guides/model-ordering.md +++ b/docs-site/src/content/docs/ru/guides/model-ordering.md @@ -25,6 +25,8 @@ selector-групп. Приоритеты без селекторов: +Таблицы приоритетов и пример ниже описывают режим без сортировки всего селектора. + | Запись каталога | Priority | Источник | | --- | ---: | --- | | `subagentModels[i]` | `i` (от `0` до `4`) | Карта рангов избранных в `src/codex/catalog/sync.ts` | @@ -121,7 +123,50 @@ native-выбора в selector-qualified группы. развернуться в несколько selector-qualified строк, поэтому число настроенных вариантов и объявляемых строк не обязательно совпадает. -Общих настроек `modelOrder`, `providerOrder` или карты приоритетов в `OcxConfig` сейчас нет. -Поддерживаемое поле порядка — `subagentModels`; `disabledModels` и `selectedModels` каждого -провайдера — поля видимости. Изменение остальной части порядка селектора потребовало бы изменения -поведения на уровне кода, а не правки конфигурации. +`modelPickerOrder` управляет только порядком отображения в селекторе. Если список содержит лишь +маршрутизируемые ID `/`, указанные строки вне избранных попадают в отдельный +диапазон отображения (`1000 + i`) в порядке списка. Неуказанные маршрутизируемые строки сохраняют +обычный приоритет и остаются перед этим диапазоном. Строки из `subagentModels` сохраняют приоритет +избранных, а нативные строки — обычные позиции. Укажите все маршрутизируемые строки, относительный +порядок которых нужно задать. + +Чтобы сортировать весь селектор, включите хотя бы один непустой ID каталога без `/`, например +`gpt-5.6-sol`. Строка из одних пробелов не включает этот режим. + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +Указанные строки идут первыми в порядке массива, затем неуказанные — по исходному приоритету. +Сопоставление учитывает точный ID каталога: `gpt-5.6-sol` и `openai/gpt-5.6-sol` — разные строки. +Допускаются исходная и закодированная формы одного маршрутизируемого ID, но точное совпадение +имеет приоритет над эквивалентным. Пустые строки и строки из одних пробелов игнорируются. +Для строки конкретного аккаунта укажите полный ID с селектором. + +### Миграция: нативные ID в существующих списках + +Раньше нативные ID без префикса в `modelPickerOrder` игнорировались. Теперь такой ID в существующем +списке включает сортировку всего селектора, включая избранные строки. Удалите ID без префикса, +чтобы сохранить прежнее поведение только для маршрутизируемых строк. Отсутствующий или пустой +список, список из одних пробельных строк и список только с маршрутизируемыми ID работают как раньше. + +`modelPickerOrder` сохраняет расчёт OpenCodex, который выбирает до пяти предпочтительных +кандидатов для рекомендаций субагентам по исходному приоритету. У каждой перемещённой строки +этот приоритет хранится отдельно от нативного `priority`; изменение только порядка селектора +не должно менять результат этого расчёта. Оно также не ограничивает допустимость переопределения +модели по точному имени: объявленный список не является списком разрешений. Существующие +ограничения аутентификации, модели, effort и бэкенда продолжают действовать. + +Нативный Codex использует нативный `priority`, чтобы объявить через `spawn_agent` первые пять +допустимых моделей, видимых в селекторе. Это относится к V1 и к V2 с открытыми переопределениями +моделей. Поэтому объявленные пять моделей могут меняться вместе с порядком селектора, даже если +предпочтительные кандидаты OpenCodex не изменились. В V1 OpenCodex не внедряет список +предпочтительных моделей. V2 может дополнительно получать рекомендации по исходным приоритетам, +если состояние каталога клиента это допускает; эти рекомендации не меняют порядок списка, +объявленного нативным инструментом. + +`disabledModels` и `selectedModels` каждого провайдера +по-прежнему управляют видимостью. Отдельных настроек `modelOrder`, `providerOrder` или карты +приоритетов нет. diff --git a/docs-site/src/content/docs/ru/guides/model-routing.md b/docs-site/src/content/docs/ru/guides/model-routing.md index eb4178cd3f..ce96fc95b8 100644 --- a/docs-site/src/content/docs/ru/guides/model-routing.md +++ b/docs-site/src/content/docs/ru/guides/model-routing.md @@ -91,12 +91,16 @@ description: Как opencodex решает, какой провайдер буд - `provider.disabled: true` убирает провайдера из обнаружения каталога. Явные запросы `provider/model` завершаются ошибкой, а проверки `defaultModel` / `models[]` его пропускают. - `providerContextCaps` задаёт видимые для Codex лимиты контекста по провайдерам. - `contextCapValue` — значение по умолчанию для дашборда (по умолчанию 350 000), но сам по себе он - ничего не делает, пока провайдер не указан в `providerContextCaps`. Изменение значения на дашборде - переназначает все включённые провайдеры только при включённом переключателе «применить ко всем - маршрутизируемым провайдерам»; в противном случае каждый провайдер сохраняет собственный лимит. - Лимиты только понижают известное контекстное окно; они никогда не повышают его и не меняют - фактический предел вышестоящей модели. + `contextCapValue` — значение по умолчанию для дашборда (350 000); само по себе оно не применяет + ограничение, пока провайдер не указан в `providerContextCaps`. Изменение значения в дашборде + обновляет только активные лимиты и только при включённом переключателе «применить ко всем + маршрутизируемым провайдерам»; иначе каждый провайдер сохраняет свой лимит. Обычные известные + окна можно только уменьшать; нативные модели с поддержкой длинного контекста могут расширять + окно до собственного поддерживаемого предела. Фактический предел вышестоящей модели не меняется. + При отключении лимита выбор сохраняется в `providerContextCapValues`, в том числе после + перезагрузки. Повторное включение восстанавливает выбор. Сохранённое значение не ограничивает + окно, пока лимит отключён. `{ "setAll": true }` без `value` включает лимиты всех настроенных + провайдеров с текущим глобальным значением и заменяет их сохранённые значения. ```json { diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index 3dce504aa9..4600b51ace 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -48,6 +48,14 @@ bun run dev:gui | **Storage** | Только чтение разбивки диска CODEX_HOME (сессии, архивы, БД, вложения). Опциональная очистка архива: предпросмотр самых старых N%, затем карантин в `CODEX_HOME/.trash` (по умолчанию) или безвозвратное удаление по явному флажку. **Политика автоочистки** — opt-in и **по умолчанию ВЫКЛ** (`storageCleanupPolicy.enabled`); порог/цель/расписание/режим на странице Storage или **Запустить сейчас**. Записи карантина можно восстановить со страницы Storage (JSONL + threads). Активные сессии только для чтения. Очистка и восстановление отклоняются, пока Codex держит блокировку новейшего/активного `state_*.sqlite`. | | **Stop** | Корректная остановка прокси и установленного фонового сервиса, восстановление нативного Codex и выход (`POST /api/stop`). На Windows с бэкендом планировщика заданий дашборд отказывает и просит выполнить `ocx stop`: обёртка может перезапустить прокси после завершения задачи, и проверить это окно перезапуска до восстановления клиентской конфигурации способен только stop, работающий вне прокси. При отказе ничего не изменяется. | +### Фильтрация запросов + +Фильтры объединяют источник, перехваченные запросы, провайдера, точную модель, статус, время, скорость и ID диалога в загруженном журнале. Варианты включают резервные попытки; модель сравнивается без учёта регистра и крайних пробелов, но не по подстроке. Исчезнувший вариант сбрасывается на все записи. + +Периоды 15 минут, час и сутки обновляются каждые 30 секунд на вкладке Logs даже при выключенном автообновлении. Скорость — выходные токены в секунду за полную длительность запроса: меньше 15, от 15 до менее 50, не менее 50; недоступные значения исключаются при активном фильтре скорости. Успех — 2xx, ошибки — 4xx/5xx. + +Счётчик показывает совпадения из загруженного общего числа; сброс возвращает все строки. Нет совпадений и пустой журнал различаются. Источник выбирается стрелками и Home/End. История вне загруженного журнала не запрашивается. + ### Ссылки на разделы Макет теперь один, поэтому переключать нечего. Зато у разделов Dashboard есть собственные адреса: `#dashboard` открывает Overview, а `#dashboard/providers` и `#dashboard/models` — два других раздела. Перезагрузка, закладка и кнопка «Назад» сохраняют выбранный раздел. **Logs** работает так же — `#logs` и `#logs/debug`. Старая закладка `#providers/workspace` теперь ведёт на `#providers`. @@ -60,6 +68,25 @@ bun run dev:gui Переключатели **Models** показывают итоговую видимость в Codex. Маршрутизируемая модель включена, только если она входит в allowlist провайдера (или allowlist отсутствует) и не отключена. Включение атомарно согласует оба фильтра, а **Включить все** удаляет allowlist и включает новые модели. +### Управление моделями в рабочей области провайдера + +На вкладке **Модели** провайдера действие **Удалить** удаляет сохранённое пользовательское +определение. Исходная нативная или обнаруженная модель может появиться снова, поэтому счётчик +моделей может не измениться. **Скрыть** меняет только видимость в каталоге, не удаляя определение +и не меняя политику прямой маршрутизации. Кнопка **Управлять видимостью в разделе «Модели»** +открывает страницу **Модели**, где можно восстановить видимость, даже если вкладка провайдера пуста. + +**Добавить** сохраняет пользовательское определение, но не отменяет существующее скрытие или +правила выбора провайдера. Сохранённая модель может остаться скрытой. Если модель уже известна, +управляйте её видимостью в разделе **Модели**. Подтверждённое сохранение остаётся действительным +при сбое обновления каталога: следуйте сообщению об обновлении, не добавляя модель повторно. +Если изменение не подтверждено, обновите состояние моделей перед повторной попыткой. + +Счётчик провайдера показывает число уникальных неотключённых записей в текущем списке моделей, +полученном от сервера, до поиска и ограничения числа отображаемых строк. Это не размер списка +разрешений, не число моделей из живого обнаружения и не доказательство происхождения записи. +Метки выбора и сведения об обнаружении учитываются отдельно. + ## Селектор делегирования и маршрутизация порождений Селектор **Sub-agent delegation** в дашборде сохраняет `injectionModel` и, при желании, @@ -159,7 +186,7 @@ GUI — это тонкий клиент поверх JSON-API управлен | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Выбор аккаунта для следующего запроса и настройка маршрутизации пула. | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | Чтение эффективного аккаунта (включая признак закрепления `pinned` и закреплённый аккаунт `pinnedAccountId`) и установка порядка выбора для одного аккаунта. | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Добавление аккаунта пула через вход в браузере. | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Чтение метаданных недавних запросов с необязательными фильтрами tail, провайдера и точного/классового статуса. `limit`/`offset` листают назад от самой новой строки (`offset=0` — последняя страница). Ответ: `{ timeZone, total, logs }`, где `total` — число совпадений до пагинации. | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | Чтение метаданных недавних запросов с необязательными фильтрами tail, провайдера и точного/классового статуса. `limit`/`offset` листают назад от самой новой строки (`offset=0` — последняя страница). Ответ: `{ timeZone, generatedAt, total, logs }`, где `total` — число совпадений до пагинации. | | `GET` / `PUT /api/subagent-models` | Чтение или настройка пяти выделенных моделей переопределения `spawn_agent`. | | `POST /api/stop` | Остановка прокси/сервиса, восстановление нативного Codex и выход. Отклоняется с `respawnable_service` на бэкенде планировщика заданий Windows и с `service_state_unknown`, когда это состояние не удаётся прочитать; в обоих случаях ничего не изменяется. | diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index 30b4e627a3..1ace7cc10f 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -220,6 +220,12 @@ unit**, Windows **Task Scheduler**), которая автоматически перезапускается при crash. Запуски службы выставляют `OCX_SERVICE=1`, чтобы restart не дёргал конфиг Codex. +При установке через Windows Task Scheduler используется обычный приоритет процесса (`Priority=4`). +Прежний фоновый приоритет (`7`, также значение планировщика по умолчанию при отсутствии параметра) +при конкуренции за CPU может задерживать ответы проверки состояния: трей показывает Offline, хотя процесс работает. +После обновления выполните `ocx service repair`, чтобы изменить этот зарегистрированный приоритет и перезапустить службу. +Может потребоваться подтверждение UAC. Если уже задан обычный или высокий приоритет, сам приоритет не вызывает перерегистрацию. + | Подкоманда | Действие | | --- | --- | | none | Установить и запустить службу, если её нет; иначе обновить и перезапустить существующую службу. Исправная конфигурация Windows Task Scheduler используется повторно; устаревшая может быть перерегистрирована и потребовать повышения прав. | 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 1b8def013e..a3a0bc434b 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/agents.md +++ b/docs-site/src/content/docs/ru/reference/configuration/agents.md @@ -84,7 +84,8 @@ cooldown либо уже достигли порога quota. Availability-сн native ChatGPT-target'ами и прямыми key-auth Responses-маршрутами, явно доверенными через `allowEncryptedV2AgentTasks: true`. Если ни один из них не может обработать encrypted payload, запрос завершается ошибкой вместо отправки нечитаемого ciphertext наружу. Combo по-прежнему -использует только канонические native-цели. +сначала выбирает доступную каноническую native-цель; если её нельзя выбрать и включён +`agentTaskRecovery`, encrypted `NEW_TASK` восстанавливается один раз перед routed combo dispatch. ```json { diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index a058fdb842..7279179991 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -28,8 +28,9 @@ ocx models provider openrouter on | `providers` | `Record` | — | Map вида provider name → provider config. | | `openaiProviderTierVersion?` | `2` | set by migration | Отмечает, что единая projection OpenAI с учётом режима уже завершена. | | `disabledModels?` | `string[]` | — | Модели, скрытые из каталога Codex и `/v1/models`, но не заблокированные для прямых вызовов прокси. Routed-id удаляются из списков. Account-qualified native-id скрывает только строку этого селектора; bare native GPT-id скрывает bare-строку и строки всех селекторов аккаунтов для этой модели. Страница Models показывает только bare native- и routed-строки; чтобы скрыть одну selector-qualified строку, задайте это поле конфигурации напрямую. | -| `providerContextCaps?` | `Record` | `{}` | Context cap'ы, видимые Codex, по каждому провайдеру. Cap может только понижать известное context window. | -| `contextCapValue?` | `number` | `350000` | Значение по умолчанию для элементов управления context-cap в дашборде. При изменении значение применяется ко всем маршрутизируемым провайдерам — включая провайдеров без существующей записи `providerContextCaps` — только при включённом переключателе «применить ко всем маршрутизируемым провайдерам»; в противном случае каждый провайдер сохраняет собственный лимит. | +| `providerContextCaps?` | `Record` | `{}` | Активные лимиты контекста по провайдерам. Обычные окна уменьшаются; нативные модели с поддержкой длинного контекста могут расширять окно только до собственного поддерживаемого предела. | +| `providerContextCapValues?` | `Record` | `{}` | Последние выбранные лимиты по провайдерам, сохраняемые после отключения. Эти значения сами по себе не включают ограничение. Активное значение имеет приоритет над сохранённым. | +| `contextCapValue?` | `number` | `350000` | Значение по умолчанию при первом включении. При повторном включении восстанавливается выбранное значение провайдера. Изменение глобального значения с `setAll: true` обновляет только активные лимиты; `setAll: true` без значения включает лимиты всех настроенных провайдеров с текущим глобальным значением. | | `codexAccounts?` | `CodexAccount[]` | `[]` | Метаданные аккаунтов пула ChatGPT/Codex, которыми управляет Codex Auth. Секреты живут отдельно в `codex-accounts.json`. | | `pausedCodexAccountIds?` | `string[]` | `[]` | Аккаунты, исключённые из выбора Pool до снятия паузы, включая основной аккаунт `__main__`, если он поставлен на паузу. | | `codexAccountNamespaces?` | `Record` | — | Необязательное сопоставление произвольного публичного селектора модели с сохранённым аккаунтом Codex. Когда строки picker'а с указанием аккаунта включены, каждый селектор с существующей целью добавляет в model picker Codex отдельные строки `/`; каждая строка использует только этот аккаунт. Если активен хотя бы один селектор, bare native-строки скрываются в picker, но их id остаются маршрутизируемыми и перечисляются raw `/v1/models`, если они не отключены явно. | @@ -94,7 +95,7 @@ cross-route credential fallback не существует. Строки API GPT- | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Header-style для ключа Anthropic. По умолчанию нативный `x-api-key`; допустим только для key-auth-провайдеров `anthropic`. | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Пул из нескольких ключей. `apiKey` зеркалит активную запись; каждый элемент содержит `id`, `key`, необязательный `label` и необязательное числовое `addedAt`. | | `defaultModel?` | `string` | Модель, используемая когда этот провайдер выбран без явной модели. | -| `models?` | `string[]` | Seed/fallback-список моделей. При `liveModels: false` это и есть единственный список обнаруженных моделей. | +| `models?` | `string[]` | Начальный список моделей или список для резервного режима. При `liveModels: false` за непустым `models` следует `retainModels`; если `models` пуст или отсутствует, сначала берётся настроенный `defaultModel`, затем `retainModels`. Для каждого ID сохраняется только первое вхождение. | | `liveModels?` | `boolean` | Получать live-каталог на start/sync (по умолчанию `true`). Custom-провайдеры используют `${baseUrl}/models`; built-in могут использовать registry URL и дополнительно фильтровать результат. | | `selectedModels?` | `string[]` | Allowlist каталога после discovery. Непустой список показывает только эти id; пустой или отсутствующий показывает всё, что было обнаружено. | | `modelDisplayNames?` | `Record` | Постоянные display-only имена с точным нативным id модели этого провайдера в качестве ключа. Ключи чувствительны к регистру. Имена имеют приоритет над metadata каталога провайдера и не меняют аутентификацию, adapter, routing, billing или upstream-запросы. Карта содержит не более 2 000 записей, как и discovery. | @@ -439,8 +440,16 @@ Chat-запросов не добавляют поле `provider`, а Vercel AI ## Статические allowlist'ы моделей -Задайте `liveModels: false`, чтобы показывать только `models`. Если `models` пуст или отсутствует, -провайдер не будет показывать ни одной маршрутизируемой модели. Live-discovery отвергает ответы +При `liveModels: false`, если `models` пуст или отсутствует, начальный список содержит сначала +настроенный `defaultModel`, затем `retainModels`. Дубликаты удаляются с сохранением первого вхождения. +Если явно задан непустой `models`, за ним следует `retainModels`, а другой `defaultModel` автоматически +не добавляется. Его можно явно указать в `models` или `retainModels`. Если ни одно из этих полей +не содержит ID, начальный список пуст. Этот порядок не гарантирует итоговый порядок в селекторе. +Правила `selectedModels`, `disabledModels` и отключения провайдера продолжают действовать. +`authMode: "forward"` сохраняет отдельную ветвь и не использует этот статический список +маршрутизируемых моделей. Эти правила не меняют резервное поведение при сбое живого обнаружения. + +Live-discovery отвергает ответы размером более 4 MiB или более 2000 сырых model-row до кэширования; built-in preset'ы могут использовать меньшие лимиты и фильтровать список до chat-совместимых строк. Oversized или malformed-результаты откатываются к stale/configured fallback. Валидный результат с нулём 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 306534a3e1..f8bc9f2a25 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/server.md +++ b/docs-site/src/content/docs/ru/reference/configuration/server.md @@ -215,3 +215,7 @@ opencodex. Перед использованием прогоните soak-test `runtimeRole` по умолчанию равен `standalone`. Hub использует `hub.managementPublicOrigin`, loopback-only `hub.managementIngress` (`enabled:false`, если отсутствует) и точные `remoteGui.allowedTailscaleUsers` (пустой список, если отсутствует). Ключ клиента хранится в `service-api-token`, не в `config.json`; во время ротации может появиться `service-api-token.prev`. Статистика не зеркалируется. `remoteGui.allowInsecureHttp` — устаревший no-op, оставленный только для загрузки старых файлов со строгой схемой. Удалите его из конфигурации: pairing grants принимаются лишь через loopback или аутентифицированный HTTPS, а значение `true` не включает pairing по открытому HTTP. + +## Сетевая диагностика квоты Codex + +Поле `quotaRefresh` в строке основного аккаунта Codex описывает получение квоты, а не её остаток или право доступа к модели. Оно может отсутствовать при чтении кэша или если запрос не выполнялся. Используется окружение работающего прокси-сервиса, а не текущего терминала. Если `proxy` не задан, существующее окружение сохраняется; `"auto"` читает только статические настройки прокси Windows при запуске. PAC/WPAD, настройки только SOCKS и изменения во время работы автоматически не учитываются. Успех через TUN сам по себе не подтверждает исправность пути HTTP-прокси. См. [команды и состояния на английском](/reference/configuration/server/#codex-quota-network-diagnostics). 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 308a385ede..aefc4e09cf 100644 --- a/docs-site/src/content/docs/ru/reference/management-api.md +++ b/docs-site/src/content/docs/ru/reference/management-api.md @@ -166,12 +166,15 @@ Endpoint'ы storage cleanup могут перемещать или навсег | `GET /api/models` | Вернуть model-row'ы для дашборда и CLI | `catalog_busy`, когда сборка перегружена | | `GET /api/client-config?client=...` | Собрать read-only client config для любой поддерживаемой файловой интеграции | 400 unsupported client; 503 catalog unavailable | | `PUT /api/disabled-models` | Полностью заменить общий список disabled-models | 400 invalid JSON | -| `PUT /api/model-visibility` | Атомарно изменить видимость на уровне провайдера или модели | 400 invalid provider, scope, target or body | +| `PUT /api/model-visibility` | Атомарно изменить видимость на уровне провайдера или модели | 400 invalid provider, scope, target or body; 409 `initial_model_selection_pending` (Обновите список моделей и повторите попытку.) | | `GET, POST /api/custom-models` | Показать список custom-моделей или добавить одну | 400 invalid fields; 404 provider missing; 409 duplicate model | | `PUT, DELETE /api/custom-models/{id}` | Изменить или удалить одну custom-модель | 400 invalid id/fields; 404 not found; 409 duplicate model | | `GET, PUT /api/selected-models` | Прочитать allowlist'ы и availability провайдеров либо заменить один allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Прочитать пресеты или выбрать режим preset/all/custom | 400 неверный режим или неподдерживаемый пресет; 404 неизвестный провайдер; PUT 409 `initial_model_selection_pending` | +Ручная модель заменяет строку панели Models с тем же провайдером и идентификатором модели. Для OpenAI ручная строка сохраняет `openai/` и поддерживает управление видимостью. При её удалении восстанавливается нативная строка без уточнения аккаунта. Нативные строки с указанием аккаунта остаются отдельными. Нативные маршруты и права аккаунта не меняются. Ненативная цель видимости OpenAI должна соответствовать настроенной ручной модели. + + Пока достоверный исходный список моделей не получен, корректные PUT-запросы к `/api/selected-models` и `/api/model-presets` возвращают HTTP 409 с кодом `initial_model_selection_pending`. Обновите список моделей, например через `GET /api/models`, и повторите запрос после успешного получения списка. ### OAuth-аккаунты, ключи провайдеров и ключи data plane @@ -211,6 +214,18 @@ Endpoint'ы storage cleanup могут перемещать или навсег | `GET, PUT /api/provider-context-caps` | Прочитать или обновить context cap глобально, для всех провайдеров или для одного провайдера | 400 invalid request; 404 unknown provider | | `GET /api/provider-presets` | Вернуть GUI-presets провайдеров, выведенные из runtime registry | — | +Ответ API лимитов контекста содержит `caps` (активные лимиты) и `values` (последние выбранные +значения, сохраняемые после отключения). Включение лимита провайдера без `value` восстанавливает +его выбор, а при первом включении использует глобальное значение `contextCapValue`. +Это относится и к OpenAI: переключатель не выбирает специальный режим 922k. Активный лимит +ограничивает каждое нативное окно; модели с поддержкой длинного контекста могут расширять окно +только до собственного поддерживаемого предела. +`{ "value": 600000, "setAll": true }` меняет глобальное значение и только активные лимиты. +Провайдеры с отключённым лимитом сохраняют свой выбор для последующего включения. +`{ "setAll": true }` без `value` включает лимиты всех настроенных провайдеров с текущим глобальным +значением и заменяет сохранённый выбор. Отключение сохраняет выбор даже после перезагрузки, +но не применяет его как ограничение. + `provider_has_dependent_combos` — это safety-барьер: сначала удалите или отредактируйте зависящие combo, и лишь потом удаляйте их провайдера. 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 573c5ff32c..26d4db5709 100644 --- a/docs-site/src/content/docs/ru/reference/proxy-formats.md +++ b/docs-site/src/content/docs/ru/reference/proxy-formats.md @@ -29,7 +29,7 @@ control и safety ответа всё равно происходят на гр | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `chat.completion.chunk` SSE, заканчивающийся `[DONE]` | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Подсчёт токенов Anthropic | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | Не применяется | -| Обнаружение моделей | `GET /v1/models` | Один из трёх контрактов каталога | Не применяется | +| Обнаружение моделей | `GET /v1/models` | Каталог или явно запрошенный снимок Desktop | Не применяется | | Голос и Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | Ответ создания вызова после ретрансляции | Отдельный sideband WebSocket ретранслирует frame'ы в обе стороны | | Компактизация Responses | `POST /v1/responses/compact` | JSON истории-замены | Не применяется | @@ -205,10 +205,18 @@ passthrough. Native-eligible-запрос пересылается в count-endp { "input_tokens": 123 } ``` +Неразрешённый Desktop ID в формате даты может быть реальным нативным модельным ID, +отсутствующим в результатах обнаружения. Если имеющихся данных недостаточно для разрешения ID, +Messages и count-tokens возвращают HTTP 503 с фиксированной ошибкой `desktop_model_mapping_unavailable`; +это не доказывает недействительность модели. Неизвестные старые хеш-псевдонимы по-прежнему дают +HTTP 400. В обоих случаях дата не удаляется и другая маршрутизация не подставляется. Известные ID, +зарегистрированные сопоставления и точные записи `modelMap`, включая распознанные реальные +нативные ID, обрабатываются как прежде. Обновите обнаружение моделей или повторно примените +профиль подключённого хаба перед новой попыткой; один лишь повтор не гарантирует разрешения. + ## `GET /v1/models` -Один и тот же маршрут обслуживает три клиента, ожидающих несовместимые envelope'ы каталога. -Форма Anthropic имеет приоритет, если только одновременно не присутствует `client_version`. +Без `format=desktop-config` действуют следующие обычные контракты каталога: | Контракт | Триггер | Форма верхнего уровня | Поведение id модели | | --- | --- | --- | --- | @@ -216,6 +224,30 @@ passthrough. Native-eligible-запрос пересылается в count-endp | Каталог Codex | Query-параметр `client_version` | `{ "models": [...] }` | Нативные и маршрутизируемые записи несут более богатые поля каталога Codex: visibility, effort, WebSocket и multi-agent metadata | | Обычный список OpenAI | Ни один триггер не сработал | `{ "object": "list", "data": [...] }` | Видимые native-id идут без префикса; routed-id — как alias или `provider/model` | +### Снимок конфигурации Desktop + +`GET /v1/models?ids=desktop&format=desktop-config` явно выбирает снимок Desktop независимо +от user-agent. Ответ — `{ "version": 1, "models": [...] }` с `Cache-Control: no-store`. +Клиент отправляет `Accept: application/json`, `anthropic-version: 2023-06-01` и существующие +учётные данные для доступа к данным; администраторский токен и загрузка профиля не нужны. +Элементы — модели конфигурации Desktop, выданные хабом, а не строки каталога Codex. + +Этот формат вместе с `ids=cli` или любым `client_version` возвращает HTTP 400. Без выбора +формата обычные контракты выше не меняются. При выключенном Claude ответ имеет вид +`{ "version": 1, "models": [] }`: подключённый Desktop apply считает модели недоступными и +не записывает заменяющий профиль. Старые хабы с обычным каталогом вместо версии 1 не +поддерживаются; перехода к локально созданным ID нет. + +Снимок остаётся списком моделей только для чтения, а не API ротации или загрузки профиля. +Миграция ключа Desktop, восстановление и отключение используют существующий цикл подключения. +Ротация сохраняет модели и выбор; CLI-поле `rotation` различает `committed` и `rolled_back`. +Отключение восстанавливает управляемые настройки либо сообщает о стандартном fallback для +распознанного старого профиля, сохраняя пользовательские поля и более поздний действительный +выбор. Конфликты и неполное восстановление не считаются завершением. Перезапустите Desktop для +чтения изменений; отключение не отзывает ключ хаба автоматически. +См. [руководство Desktop](/ru/guides/claude-code/). Повторная передача thinking и кеш остаются +отдельно в [#3719](https://github.com/lidge-jun/opencodex/issues/3719). + ## `POST /v1/live` и Realtime sideband `POST /v1/live` принимает surface Frameless call-creation из ChatGPT/Codex App. 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 2b092dc621..5450d6b748 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -161,6 +161,8 @@ ailedeki ilk kullanılabilir rota kullanılır. Aynı profili komut satırından da yönetebilirsiniz: +Aşağıdaki profil düzenleme yönergeleri yerel profil içindir. Bağlı uzak hub üzerinden uygulama aşağıda ayrıca açıklanır. + ```bash ocx claude desktop [apply] ocx claude desktop show [--json] @@ -247,6 +249,63 @@ ile görünmediği anlamına gelir. `claudeCode.nativePassthrough: false` ile devre dışı bırakın; `claudeCode.anthropicBaseUrl` ile başka bir yeri işaret edin. +## Uzak hub'a bağlı Claude Desktop + +Bağlı makinede `ocx claude desktop apply` veya `ocx claude desktop`, hub'ın Desktop anlık +görüntüsünü alır ve hub origin'ini ve verdiği model kimliklerini yerel Desktop yapılandırmasına +aynen yazar. Yerel takma ad üretmez. static/hybrid model listesini de kopyalar; +discovery-only listeyi gömmeden hub origin'ini kullanır. + +Profil, aile atamaları ve varsayılanlar hub'da yönetilir. Hub'da değiştirin, istemcide yeniden +uygulayın ve Desktop'ta modeli yeniden seçin. Yalnızca istemcide oluşturulmuş eski takma adlar +için de yeniden uygulama/seçim gerekir. `show`, yerel düzenleme ve import/export yerel kalır. +Bağlıyken `ocx claude desktop import --apply` desteklenmez ve kaydetmeden reddedilir; +`--apply` olmadan import yerel bir işlemdir. + +Okuma, mevcut bağlantının veri erişim kimlik bilgilerini kullanır; yönetici belirteci veya profil +yüklemesi gerekmez. Eski hub desteği yoksa, yanıt geçersizse veya Desktop listesi boşsa uygulama +başarısız olur; yerel katalog ya da loopback adresi kullanılmaz. Hub'ı güncelleyin veya +yapılandırın, ardından yeniden uygulayın. + +Bu takma ad değişikliği, [#3719](https://github.com/lidge-jun/opencodex/issues/3719)'daki ayrı `thinking` / `redacted_thinking` yeniden gönderim ve +istem önbelleği talebini çözmez. Proxy erişimi tek başına yerel Anthropic geçişini etkinleştirmez; +çevrilen Anthropic rotaları yine de önbellek kullanabilir. Yeniden gönderim doğruluğu ve önbellek +isabetlerinin karşılaştırılması ayrı iş olarak kalır. + +### Anahtar döndürme, kurtarma ve bağlantıyı kesme + +Anahtar döndürme ve kurtarma, yerel bağlantı kimlik bilgileriyle birlikte bağlantının yönettiği +Desktop profilindeki anahtarı da günceller. Yalnızca anahtarı taşımak için elle apply gerekmez. +Model kimlikleri, aileler, varsayılanlar ve geçerli profil seçimi korunur; yönetilen profil tekrar +seçilmez veya kapalı entegrasyon açılmaz. CLI JSON'unda `rotation: "committed"` yeni anahtarın +etkin olduğunu, `rotation: "rolled_back"` önceki anahtarın korunduğunu ya da geri yüklendiğini +belirtir. Geri alma, yeni anahtarın kesinleştiği veya öncekinin iptal edildiği anlamına gelmez. +Belirsiz veya eksik kurtarma başarılı döndürme olarak bildirilmez. + +İlk bağlı uygulama, geri yüklemek için önceki yönetilen ayarları ve seçimi kaydeder. Tekrar +uygulama ve döndürme bu ilk kaydı değiştirmez. `ocx disconnect`, kullanıcı alanlarını ve diğer +profilleri koruyarak bağlantıya ait ayarları geri yükler. Önceki seçim yalnızca yönetilen profil +hâlâ seçiliyse geri gelir; kullanıcının sonradan seçtiği başka geçerli profil korunur. Yeni +oluşturulmuş profile kullanıcı eklemeleri yapılmışsa silinmez, okunabilir standart modda kalır. +`--keep-catalog`, Desktop bağlantı anahtarını değil kataloğu tutar. + +İlk ayar kaydı olmayan eski yönetilen profil, geçerli hub'a ve tanınan bağlantı anahtarına açıkça +aitse taşınabilir. Apply, döndürme/kurtarma veya doğrudan disconnect bunu yeni bayrak ya da önceden +apply gerektirmeden yapar. Önceki ayarlar kaydedilmediği için bağlantı kesildiğinde standart moda +geçileceği uyarısı gösterilir. Yalnızca bağlantıya ait ağ geçidi ayarları kaldırılır; kullanıcı +alanları ve ayrı geçerli seçim korunur. Sonuç özgün ayarların geri yüklenmesi değil standart +moda dönüş olarak bildirilir. + +Yönetilen ayar çatışmaları, tanınmayan kimlik bilgileri ve bozuk geri yükleme kayıtları korunup +bildirilir. Kesilen temizlik yalnızca aynı bağlantı için sürdürülür; yeni bağlantı silinmez ve +eksik geri yükleme tamamlanmış sayılmaz. Bağlantıyı kesmeden bekleyen döndürme kurtarmasını bitirin; +yeniden denerken katalog saklama tercihini değiştirmeyin. + +Uygulama, döndürme/kurtarma veya geri yükleme sonrası Claude Desktop'ı tamamen kapatıp yeniden +açın; disk değişikliği çalışan uygulamanın anahtarını değiştirmez. Uygulama otomatik yeniden +başlatılmaz. Yerel bağlantı kesme hub anahtarını iptal etmez veya dış kopyaları silmez; +gerekirse anahtarı hub'da ayrıca iptal edin. + ## /model seçici ("From gateway") Claude Code 2.1.129+, `GET /v1/models?limit=1000` aracılığıyla ağ geçidi @@ -303,6 +362,15 @@ slug'lar karma forma geri döner. çözülür → Desktop karma takma adı çözülür → `modelMap` tam eşleşmesi → tarih kaldırılmış eşleşme (`-20250514` kaldırılır) → doğrudan geçiş. +Çözümlenemeyen tarih biçimli bir Desktop kimliği, keşifte yer almayan gerçek bir yerel model +kimliği de olabilir. Mevcut bilgi kimliği çözmeye yetmiyorsa Messages ve count-tokens sabit +`desktop_model_mapping_unavailable` hatasıyla HTTP 503 döndürür; bu, modelin geçersiz olduğunu kanıtlamaz. +Bilinmeyen eski hash takma adları HTTP 400 ile reddedilmeye devam eder. Her iki durumda da tarih +kaldırılmaz ve başka rotaya geçilmez. Bilinen kimlikler, kayıtlı eşlemeler, tam `modelMap` +eşleşmeleri ve tanınan gerçek yerel kimlikler aynı şekilde işlenir. Yeniden denemeden önce model +keşfini yenileyin veya bağlı hub profilini yeniden uygulayın; yalnızca tekrar denemek çözümü +garanti etmez. + Her girdi, `gemini-3-pro (gemini)` gibi bir görünen adın yanı sıra resmi `ModelInfo` biçiminde tam model yeteneklerini (akıl yürütme çabası merdiveni, düşünme türleri) taşır. Gerçek Anthropic modelleri her iki yüzeyde de kurallı @@ -422,6 +490,15 @@ yeniden yazar: Arama sırası: keşif takma adı → tam kimlik → tarih soneki kaldırılmış kimlik (`-20250514` kaldırılır) → doğrudan geçiş. +Çözümlenemeyen tarih biçimli bir Desktop kimliği, keşifte yer almayan gerçek bir yerel model +kimliği de olabilir. Mevcut bilgi kimliği çözmeye yetmiyorsa Messages ve count-tokens sabit +`desktop_model_mapping_unavailable` hatasıyla HTTP 503 döndürür; bu, modelin geçersiz olduğunu kanıtlamaz. +Bilinmeyen eski hash takma adları HTTP 400 ile reddedilmeye devam eder. Her iki durumda da tarih +kaldırılmaz ve başka rotaya geçilmez. Bilinen kimlikler, kayıtlı eşlemeler, tam `modelMap` +eşleşmeleri ve tanınan gerçek yerel kimlikler aynı şekilde işlenir. Yeniden denemeden önce model +keşfini yenileyin veya bağlı hub profilini yeniden uygulayın; yalnızca tekrar denemek çözümü +garanti etmez. + ## Sidecar matrisi: web araması ve görsel anlama Yönlendirilen modellerin tümü aynı barındırılan araçlara veya görsel desteğine diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 7692980e91..02fae0f468 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -353,9 +353,14 @@ sırayla kontrol edin: 2. **`disabledModels`** (üst düzey) — modelleri hem katalogdan hem de `/v1/models` listesinden gizler ve yalın yerel GPT slug'larını `visibility: "hide"` olarak değiştirir. -3. **Boş `models` ile `liveModels: false`** — canlı keşif kapalı olduğunda ve - `models` boş veya atlandığında opencodex bu sağlayıcı için hiçbir - yönlendirilmiş model göstermez. +3. **`liveModels: false`** — `liveModels: false` iken `models` boşsa veya atlanmışsa başlangıç listesine önce yapılandırılmış + `defaultModel`, ardından `retainModels` eklenir. Yinelenen kimliklerde ilk geçen korunur. + Açıkça belirtilmiş, boş olmayan `models` listesini ise `retainModels` izler; farklı bir `defaultModel` + kendiliğinden eklenmez. Bu model yine de `models` veya `retainModels` içinde açıkça belirtilebilir. + Bu alanların hiçbiri kimlik sağlamıyorsa başlangıç listesi boştur. Bu sıra, son seçici sırasını + garanti etmez. `selectedModels`, `disabledModels` ve sağlayıcının devre dışı bırakılması kuralları + geçerliliğini korur. `authMode: "forward"` ayrı dalında kalır ve bu yönlendirilmiş statik listeyi + kullanmaz. Bu kurallar canlı keşif başarısızlığındaki geri dönüş davranışını değiştirmez. 4. **Cursor `GetUsableModels`** — Cursor adaptörü modelleri `/models` üzerinden değil, protobuf `GetUsableModels` RPC'si üzerinden keşfeder; bu nedenle Cursor tarafındaki bir değişiklik diğer sağlayıcılardan bağımsız olarak hangi diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index 636e86af79..fea4b37dd4 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -136,6 +136,11 @@ değişen bir değer yazıp buna başarı demek yerine durur ve bunu söyler. Do adlandırıldığını ve diskte hiçbir şeyin taşınmadığını görürsünüz. Bu dosyayı elle düzenlemek hala çalışır; yalnızca otomatik yeniden yazmamız reddeder. +TOML tarih ve saat değerleri de otomatik yeniden yazmayı engeller: birleştirme adımı, +diziler ve satır içi tablolar dahil bu türlenmiş değerleri tırnaklı metne dönüştürür. +Zaten tırnak içinde yazılmış tarihler desteklenir. Tırnaksız tarih türünü korumak +için yapılandırmayı elle düzenleyin. + **Pi, Kimi Code, Gajae Code, MiniMax Code ve yönetilen DSH entegrasyonu yalnızca geri döngü (loopback) bağlantısına karşı çalışır.** İlk dördünün yapılandırmasında geri döngü olmayan bir bağlantının gerektirdiği `x-opencodex-api-key` başlığı için alan yoktur. DSH genel bir headers haritası sunar, ancak rc.6 diff --git a/docs-site/src/content/docs/tr/guides/model-ordering.md b/docs-site/src/content/docs/tr/guides/model-ordering.md index 54a19f22d4..336513225a 100644 --- a/docs-site/src/content/docs/tr/guides/model-ordering.md +++ b/docs-site/src/content/docs/tr/guides/model-ordering.md @@ -28,6 +28,8 @@ görünen ilk beş satırı tanıtır. İlgili seçicisiz öncelikler şunlardır: +Aşağıdaki öncelik tabloları ve örnek, seçicinin tamamını sıralama modu kapalıyken geçerlidir. + | Katalog girdisi | Öncelik | Kaynak | | --- | ---: | --- | | `subagentModels[i]` | `i` (`0` - `4`) | `src/codex/catalog/sync.ts` içindeki öne çıkan sıra haritası | @@ -134,10 +136,48 @@ kimlik kullanın. Hesap seçicileriyle tek bir yalın yerel seçenek birden çok seçici nitelikli katalog satırına genişleyebilir, bu nedenle yapılandırılmış seçimler ve tanıtılan satırlar birebir olmak zorunda değildir. -Şu anda `OcxConfig` içinde genel bir `modelOrder`, `providerOrder` veya öncelik -haritası ayarı yoktur. Desteklenen sıralama alanı `subagentModels`'dır; -`disabledModels` ve her sağlayıcının `selectedModels` alanı görünürlük -alanlarıdır. Kalan seçici sırasını değiştirmek bir yapılandırma düzenlemesinden -ziyade kod düzeyinde bir davranış değişikliği gerektirir. +`modelPickerOrder` yalnızca seçicideki görüntüleme sırasını belirler. Liste yalnızca yönlendirilmiş +`/` kimlikleri içeriyorsa, listelenen ve öne çıkarılmamış satırlar ayrı bir +görüntüleme aralığında (`1000 + i`) liste sırasıyla yer alır. Listelenmeyen yönlendirilmiş satırlar +normal önceliklerini korur ve bu aralıktan önce kalır. `subagentModels` içindeki satırlar öne çıkan +önceliklerini, yerel satırlar da normal konumlarını korur. Göreli sırasını belirlemek istediğiniz +tüm yönlendirilmiş satırları listeleyin. + +Seçicinin tamamını sıralamak için `gpt-5.6-sol` gibi `/` içermeyen en az bir yalın katalog kimliği +ekleyin. Boş veya yalnızca boşluk içeren girdiler bu modu etkinleştirmez. +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` +Listelenen satırlar önce dizi sırasıyla, listelenmeyenler ise ardından doğal öncelik sırasıyla gelir. +Eşleştirme tam katalog kimliğini kullanır: `gpt-5.6-sol` ile `openai/gpt-5.6-sol` farklı satırlardır. +Aynı yönlendirilmiş kimliğin ham ve kodlanmış yazımları da kabul edilir; tam eşleşme, eşdeğer +eşleşmeden önceliklidir. Boş ve yalnızca boşluk içeren girdiler yok sayılır. Hesaba özel satırlar +için seçiciyi içeren tam kimliği yazın. + +### Geçiş uyarısı: mevcut listelerdeki yerel kimlikler + +Önceden `modelPickerOrder` içindeki yalın yerel kimlikler yok sayılıyordu. Mevcut bir listede böyle +bir kimlik bulunması artık öne çıkan satırlar dahil tüm seçicinin sıralanmasını etkinleştirir. +Eski, yalnızca yönlendirilmiş satırlara uygulanan davranışı korumak için yalın kimlikleri kaldırın. +Tanımlanmamış, boş, yalnızca boşluk girdileri içeren veya yalnızca yönlendirilmiş kimliklerden oluşan +listeler önceki davranışlarını korur. + +`modelPickerOrder`, OpenCodex'in alt ajan rehberliği için doğal önceliğe göre en fazla beş tercih +edilen adayı seçen hesaplamasını korur. Taşınan her satırın doğal önceliği, yerel `priority` değerinden +ayrı saklanır; yalnızca seçici sırasını değiştirmek bu hesaplamanın sonucunu değiştirmemelidir. +Tam model adıyla geçersiz kılma uygunluğunu da kısıtlamaz: tanıtılan liste bir izin listesi değildir. +Mevcut kimlik doğrulama, model, effort ve arka uç kısıtlamaları geçerliliğini korur. + +Yerel Codex, `spawn_agent` içinde tanıtılacak beş modeli yerel `priority` sırasındaki uygun ve +seçicide görünür modellerden seçer. Bu, V1 ve model geçersiz kılmalarının sunulduğu V2 için geçerlidir. +Dolayısıyla OpenCodex'in tercih edilen adayları değişmese bile, tanıtılan beş model seçici sırasıyla +birlikte değişebilir. V1'e OpenCodex tercih listesi enjekte edilmez. V2, istemci katalog durumu izin +verdiğinde ek olarak doğal önceliğe dayalı OpenCodex rehberliği alabilir; bu rehberlik yerel aracın +tanıttığı listeyi yeniden sıralamaz. + +`disabledModels` ve her sağlayıcının `selectedModels` alanı +görünürlüğü denetler. Ayrı bir `modelOrder`, `providerOrder` veya öncelik haritası ayarı yoktur. diff --git a/docs-site/src/content/docs/tr/guides/model-routing.md b/docs-site/src/content/docs/tr/guides/model-routing.md index 50f3ca4ccc..c3183bdcb6 100644 --- a/docs-site/src/content/docs/tr/guides/model-routing.md +++ b/docs-site/src/content/docs/tr/guides/model-routing.md @@ -102,15 +102,16 @@ Yönlendirme ve katalog görünürlüğü ayrı kontrollerdir: - `provider.disabled: true`, bu sağlayıcıyı katalog keşfinden kaldırır. Açık `sağlayıcı/model` istekleri başarısız olur ve `defaultModel` / `models[]` taramaları bunu atlar. -- `providerContextCaps`, sağlayıcı başına Codex tarafından görülebilen bağlam - sınırlarını uygular. `contextCapValue` kontrol paneli varsayılanıdır - (varsayılan olarak 350.000), ancak bir sağlayıcı `providerContextCaps` içinde - yer alana kadar tek başına hiçbir şey yapmaz. Kontrol paneli değerini - değiştirmek, yalnızca "tüm yönlendirilen sağlayıcılara uygula" açık olduğunda - etkinleştirilmiş her sağlayıcıyı yeniden yönlendirir; aksi takdirde her - sağlayıcı kendi sınırını korur. Sınırlar yalnızca bilinen bir bağlam - penceresini düşürür; asla bir pencereyi yükseltmez veya yukarı akış modelinin - gerçek sınırını değiştirmez. +- `providerContextCaps`, sağlayıcı başına Codex tarafından görülebilen bağlam sınırlarını belirler. + `contextCapValue`, kontrol panelinin varsayılan değeridir (350.000); sağlayıcı `providerContextCaps` + içinde bulunmadıkça tek başına sınır uygulamaz. Kontrol paneli değerini değiştirmek, yalnızca + "tüm yönlendirilen sağlayıcılara uygula" açıkken etkin sınırları günceller; aksi halde her sağlayıcı + kendi sınırını korur. Bilinen normal pencereler yalnızca küçültülebilir; uzun pencereyi destekleyen + yerel modeller kendi desteklenen üst sınırlarına kadar genişletilebilir. Yukarı akış modelinin + gerçek sınırı değişmez. Sınır kapatıldığında seçim `providerContextCapValues` içinde saklanır + ve yeniden yüklemeden sonra da korunur. Yeniden açıldığında bu seçim geri yüklenir; kapalıyken + saklanan değer bir sınır uygulamaz. `value` olmadan `{ "setAll": true }`, yapılandırılmış tüm + sağlayıcıların sınırlarını geçerli genel değerle etkinleştirir ve saklanan seçimlerini değiştirir. ```json { diff --git a/docs-site/src/content/docs/tr/guides/routing-profile-editor.md b/docs-site/src/content/docs/tr/guides/routing-profile-editor.md index dd7aa50d72..74ab8a7bdc 100644 --- a/docs-site/src/content/docs/tr/guides/routing-profile-editor.md +++ b/docs-site/src/content/docs/tr/guides/routing-profile-editor.md @@ -52,6 +52,14 @@ ayrıdır. ## Kaydedilmiş bir profilde deneme çalıştırması (dry-run) yapma +Aday yetenekleri, kayıt defteri kuralları uygulandıktan sonraki etkin sağlayıcı +yapılandırmasını kullanır. Yerellik gereksinimleri (`localOnly` ve `remoteAllowed`) +bu nedenle etkin üst sunucu adresine göre değerlendirilir. Adres sınıflandırılamıyorsa, +adayın uygunluğunu profilin `unknownEvidence.capability` ayarı belirler. +Çözümlenemeyen geçersiz sağlayıcı yapılandırmaları, bilinmeyen yeteneklere izin +verilse bile `route-unavailable` ile her zaman dışlanır. +Eksik veya devre dışı sağlayıcılar da puanlama öncesinde `route-unavailable` ile dışlanır. + Kaydedilmiş bir profili seçin ve bağlam penceresi boyutu, araç kullanımı, görsel girişi veya yapılandırılmış çıktı gibi istek kanıtları eklemek için **Deneme çalıştırması değerlendirmesi (Dry-run evaluation)**'ı kullanın. Deneme @@ -99,5 +107,3 @@ Düzenleyici şu uç noktaları kullanır: } } ``` - - diff --git a/docs-site/src/content/docs/tr/guides/web-dashboard.md b/docs-site/src/content/docs/tr/guides/web-dashboard.md index 282c1e4106..ebdf946ffd 100644 --- a/docs-site/src/content/docs/tr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/tr/guides/web-dashboard.md @@ -60,6 +60,14 @@ kararıdır. | **Depolama** | Salt okunur CODEX_HOME disk dökümü (oturumlar, arşivler, DB'ler, ekler). İsteğe bağlı arşivlenmiş temizleme: en eski %N'yi önizleyin, ardından `CODEX_HOME/.trash` konumuna karantinaya alın (varsayılan) veya açık bir onay kutusu arkasında kalıcı olarak silin. **Otomatik temizleme politikası** isteğe bağlıdır ve **varsayılan olarak KAPALIDIR** (`storageCleanupPolicy.enabled`); Depolama sayfasında eşik/hedef/zamanlama/mod yapılandırın veya **Şimdi çalıştır (Run now)**'ı tetikleyin. Karantinaya alınan girdiler Depolama sayfasından geri yüklenebilir (JSONL + iş parçacıkları). Aktif oturumlar salt okunur kalır. Codex en yeni/aktif `state_*.sqlite` dosyasını kilitli tuttuğu sürece temizleme ve geri yükleme reddedilir. | | **Durdur** | Proxy'yi ve kurulu arka plan servisini zarif bir şekilde durdurun, yerel Codex'i geri yükleyin ve çıkın (`POST /api/stop`). Windows'ta Görev Zamanlayıcı arka ucunda panel reddeder ve `ocx stop` çalıştırmanızı ister: görev bittikten sonra sarmalayıcı proxy'yi yeniden başlatabilir ve bu yeniden başlatma penceresini istemci yapılandırmanız geri yüklenmeden önce yalnızca proxy dışında çalışan bir stop doğrulayabilir. Reddedildiğinde hiçbir şey değiştirilmez. | +### İstek günlüklerini filtreleme + +Filtreler yüklü günlükte yüzey, yakalanan istekler, sağlayıcı, tam model adı, durum, zaman, hız ve konuşma kimliğini birleştirir. Seçenekler yedek denemeleri de içerir; model eşleşmesi büyük/küçük harfi ve dış boşlukları yok sayar, kısmi adları eşleştirmez. Kaybolan seçenek tüm kayıtlara döner. + +Son 15 dakika, saat ve gün pencereleri Logs sekmesinde otomatik yenileme kapalıyken de 30 saniyede bir güncellenir. Hız, tam istek süresindeki saniyelik çıktı jetonudur: 15 altı, 15 dahil 50 altı, en az 50; hız filtresi açıkken ölçülemeyenler dışlanır. Başarı 2xx, hata 4xx/5xx anlamındadır. + +Sayaç eşleşen ve yüklü toplam sayıları gösterir; sıfırlama tüm satırları geri getirir. Eşleşme olmaması boş günlükten ayrılır. Yüzey seçimi oklar ve Home/End ile çalışır. Yüklü günlüğün dışındaki geçmiş sorgulanmaz. + ### Bir bölüme bağlantı verme Tek bir düzen vardır, bu nedenle yapılandırılacak bir düzen anahtarı yoktur. @@ -82,6 +90,25 @@ ayarlanmadığında) ve devre dışı bırakılmadığında açıktır. Bir mode iki filtreyi de atomik olarak uzlaştırır; **Tümünü aç (All on)** sağlayıcı izin listesini temizler, böylece yeni keşfedilen modeller de açık olur. +### Sağlayıcı çalışma alanında modelleri yönetme + +Sağlayıcının **Modeller** sekmesinde **Sil**, kayıtlı özel tanımı kaldırır. Alttaki yerel veya canlı +keşfedilmiş model yeniden görünebilir; bu nedenle model sayısı aynı kalabilir. **Gizle** yalnızca +katalog görünürlüğünü değiştirir; tanımı silmez veya doğrudan yönlendirme ilkesini değiştirmez. +**Modeller bölümünde görünürlüğü yönet**, görünürlüğü geri yükleyebileceğiniz **Modeller** sayfasını +açar. Sağlayıcı sekmesinde hiç satır kalmasa da bu bağlantı kullanılabilir. + +**Ekle**, özel tanımı kaydeder; mevcut gizleme durumunu veya sağlayıcı seçim kurallarını kaldırmaz. +Kaydedilen model gizli kalabilir. Model zaten biliniyorsa görünürlüğünü **Modeller** bölümünden +yönetin. Kayıt doğrulandıysa katalog yenilemesi başarısız olsa bile tanım kaydedilmiştir. Yeniden +eklemek yerine yenileme mesajını izleyin. Değişiklik doğrulanamıyorsa tekrar denemeden önce +modellerin durumunu yenileyin. + +Sağlayıcının model sayısı, sunucunun döndürdüğü güncel model envanterindeki devre dışı olmayan +benzersiz girdileri, arama ve görüntüleme sınırı uygulanmadan önce sayar. Bu sayı izin listesinin +boyutu veya canlı keşif sayısı değildir; girdinin sağlayıcıdan keşfedildiğini de kanıtlamaz. +Seçim rozetleri ve keşif bilgileri bu sayıdan ayrıdır. + ## Yetkilendirme seçicisi ve spawn yönlendirmesi Kontrol Panelinin **Alt ajan yetkilendirmesi** seçicisi `injectionModel`'i ve @@ -247,7 +274,7 @@ noktalar şunları içerir: | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Bir sonraki istek için hesabı seçin ve havuz yönlendirmesini yapılandırın. | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | Geçerli hesabı okuyun (`pinned` ve hangi hesabın `pinnedAccountId` olduğu dahil) ve bir hesabın seçim sırasını ayarlayın. | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Tarayıcı girişi aracılığıyla bir havuz hesabı ekleyin. | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | İsteğe bağlı kuyruk, sağlayıcı ve tam/sınıf durum filtreleriyle son istek meta verilerini okuyun. `limit`/`offset` ile sayfalama en yeni satırdan geriye doğru ilerler (`offset=0` en son sayfayı döndürür). Yanıt şekli: `{ timeZone, total, logs }` burada `total`, sayfalamadan önceki filtrelenmiş satır sayısıdır. | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | İsteğe bağlı kuyruk, sağlayıcı ve tam/sınıf durum filtreleriyle son istek meta verilerini okuyun. `limit`/`offset` ile sayfalama en yeni satırdan geriye doğru ilerler (`offset=0` en son sayfayı döndürür). Yanıt şekli: `{ timeZone, generatedAt, total, logs }` burada `total`, sayfalamadan önceki filtrelenmiş satır sayısıdır. | | `GET` / `PUT /api/subagent-models` | Öne çıkan beş `spawn_agent` geçersiz kılma modelini okuyun veya ayarlayın. | | `POST /api/stop` | Proxy'yi/servisi durdurun, yerel Codex'i geri yükleyin ve çıkın. Windows Görev Zamanlayıcı arka ucunda `respawnable_service`, bu durum okunamadığında `service_state_unknown` ile reddedilir; her iki durumda da hiçbir şey değiştirilmez. | diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md index e26f4c8662..6a7a565139 100644 --- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md @@ -244,6 +244,12 @@ kullanıcı birimi**, Windows **Görev Zamanlayıcı**) olarak çalıştırın. çalıştırmaları `OCX_SERVICE=1` ayarlar, böylece bir yeniden başlatma Codex yapılandırmasını dalgalandırmaz. +Windows Görev Zamanlayıcı kurulumları normal işlem önceliğini (`Priority=4`) kullanır. Eski arka plan +önceliği (`7`; değer belirtilmediğinde de zamanlayıcının varsayılanı `7` olur), CPU çekişmesi sırasında +sağlık denetimi yanıtlarını geciktirebilir ve işlem çalışırken bile sistem tepsisinde Offline görünmesine neden olabilir. +Güncellemeden sonra kayıtlı bu önceliği değiştirmek ve servisi yeniden başlatmak için `ocx service repair` komutunu çalıştırın. +UAC onayı gerekebilir. Zaten normal veya yüksek öncelik ayarlanmışsa yalnızca öncelik nedeniyle yeniden kayıt yapılmaz. + | Alt komut | Eylem | | --- | --- | | none | Servis yoksa kurup başlatın; varsa yenileyip yeniden başlatın. Sağlıklı bir Windows Task Scheduler tanımı yeniden kullanılır; eski bir tanım yeniden kaydedilebilir ve yükseltme gerektirebilir. | 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 7b07247a73..3bf154f14b 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/agents.md +++ b/docs-site/src/content/docs/tr/reference/configuration/agents.md @@ -122,7 +122,9 @@ görevlerinde zincir, kurallı yerel ChatGPT hedefleriyle ve `allowEncryptedV2AgentTasks: true` kullanılarak açıkça güvenilen doğrudan anahtar kimlik doğrulamalı Responses rotalarıyla sınırlıdır. Hiçbiri şifrelenmiş yükü işleyemezse istek, okunamayan şifreli metni başka bir yere yönlendirmek yerine -başarısız olur. Kombolar yalnızca kurallı yerel hedefleri kullanmaya devam eder. +başarısız olur. Kombo önce kullanılabilir kurallı yerel hedefi dener; seçilebilir +yerel hedef kalmazsa ve `agentTaskRecovery` etkinse, şifrelenmiş `NEW_TASK` yönlendirilen +kombo gönderiminden önce bir kez kurtarılır. ```json { @@ -226,10 +228,13 @@ sınırı ve özel arka uç bağımlılığı kabul edilebilir olduğunda etkinl Olmadıklarında yerel bir ChatGPT çocuğunu veya v1 heterojen yetkilendirmesini tercih edin. -Bu kurtarma yolu doğrudan yönlendirilen çocuklara uygulanır. Aynı anda en fazla -32 kurtarma isteği etkin olabilir; ek ıskalamalar kapalı olarak başarısız olur. -Kombo yönlendirmesi şifrelenmiş görevler için mevcut yalnızca yerel filtresini -korur ve kurtarmayı çağırmaz. +Bu kurtarma yolu doğrudan yönlendirilen çocuklara ve bir kombodaki şifrelenmiş +`NEW_TASK` oluşturma isteklerine uygulanır. Aynı anda en fazla 32 kurtarma isteği +etkin olabilir; ek ıskalamalar kapalı olarak başarısız olur. Kullanılabilir kanonik +yerel hedefi olan bir kombo şifreli metni yine doğrudan gönderir; kurtarma yalnızca +seçilebilir yerel hedef kalmadığında çalışır. Kurtarma hatası, tükenen hedefler veya +kullanılamayan hedefler, şifreli metin yönlendirilen sağlayıcıya gönderilmeden yine +kapalı biçimde başarısız olur. ## Çaba sınırları @@ -248,4 +253,3 @@ ile `xhigh` arasını sunar. v1, varsayılan ve v2 davranışının yeni başlayanlara yönelik açıklaması için [Alt ajan yüzeyleri](/tr/guides/sub-agent-surface/) sayfasına bakın. - diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md index 4213ab6001..91cb923fc3 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/providers.md +++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md @@ -28,8 +28,9 @@ Arayüzde kayıt veya OAuth girişi tamamlanınca Models sayfasını açan bir b | `providers` | `Record` | — | Sağlayıcı adından sağlayıcı yapılandırmasına eşleme haritası. | | `openaiProviderTierVersion?` | `2` | geçiş tarafından ayarlanır | Tek seçenek duyarlı OpenAI projeksiyonunu tamamlandı olarak işaretler. | | `disabledModels?` | `string[]` | — | Codex kataloğundan ve `/v1/models` listesinden gizlenen, ancak doğrudan proxy çağrılarından engellenmeyen modeller. Yönlendirilen bir kimlik listelerden kaldırılır. Hesap nitelikli bir yerel kimlik yalnızca o seçici satırını gizler; yalın bir yerel GPT kimliği, yalın satırı ve o model için her hesap seçici satırını gizler. Kontrol paneli Modeller sayfası yalnızca yönlendirilen ve yalın yerel satırları gösterir; seçici nitelikli bir satırı gizlemek için doğrudan bu yapılandırma alanını kullanın. | -| `providerContextCaps?` | `Record` | `{}` | Sağlayıcı başına Codex tarafından görülebilen bağlam sınırları. Bir sınır yalnızca bilinen bir bağlam penceresini düşürür. | -| `contextCapValue?` | `number` | `350000` | Kontrol paneli bağlam sınırı kontrolleri tarafından kullanılan varsayılan değer. Değiştirilmesi, yalnızca "tüm yönlendirilen sağlayıcılara uygula" açık olduğunda değeri mevcut bir `providerContextCaps` girdisi olmayan sağlayıcılar da dahil olmak üzere yönlendirilen her sağlayıcıya uygular; aksi takdirde her sağlayıcı kendi sınırını korur. | +| `providerContextCaps?` | `Record` | `{}` | Sağlayıcı başına etkin bağlam sınırları. Normal pencereler küçültülür; uzun pencereyi destekleyen yerel modeller yalnızca kendi desteklenen üst sınırlarına kadar genişletilebilir. | +| `providerContextCapValues?` | `Record` | `{}` | Sağlayıcı başına son seçilen sınırlar; devre dışı bırakıldığında da saklanır. Bu değerler tek başına sınırı etkinleştirmez. Etkin değer, saklanan değerden önceliklidir. | +| `contextCapValue?` | `number` | `350000` | İlk etkinleştirmede kullanılan varsayılan değer. Sonraki etkinleştirmelerde sağlayıcının seçimi geri yüklenir. Genel değeri `setAll: true` ile güncellemek yalnızca etkin sınırları değiştirir; değer olmadan `setAll: true`, yapılandırılmış tüm sağlayıcıların sınırlarını geçerli genel değerle etkinleştirir. | | `codexAccounts?` | `CodexAccount[]` | `[]` | Codex Auth tarafından yönetilen ChatGPT/Codex havuz hesabı meta verileri. Sırlar ayrı olarak `codex-accounts.json` içinde yer alır. | | `pausedCodexAccountIds?` | `string[]` | `[]` | Duraklatıldığında ana `__main__` hesabı da dahil olmak üzere, devam ettirilene kadar Havuz seçiminden hariç tutulan hesaplar. | | `codexAccountNamespaces?` | `Record` | — | İsteğe bağlı olarak rastgele bir genel model seçiciden saklanan bir Codex hesap hedefine eşleme. Hesap nitelikli seçici satırları etkinleştirildiğinde, hedefi mevcut olan her seçici, Codex seçicisine ayrı `/` satırları ekler; her satır yalnızca o hesabı kullanır. Herhangi bir seçici etkinken, yalın yerel satırlar seçicide gizlenir, ancak açıkça devre dışı bırakılmadıkça kimlikleri yönlendirilebilir kalır ve ham `/v1/models` tarafından listelenir. | @@ -100,7 +101,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar. | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic anahtar başlığı stili. Varsayılan olarak yerel `x-api-key`; yalnızca anahtar kimlik doğrulamalı `anthropic` sağlayıcıları için geçerlidir. | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | Çoklu anahtar havuzu. `apiKey` aktif girdiyi yansıtır; her öğe `id`, `key`, isteğe bağlı `label` ve isteğe bağlı sayısal `addedAt` değerine sahiptir. | | `defaultModel?` | `string` | Bu sağlayıcı açık bir model olmadan seçildiğinde kullanılan model. | -| `models?` | `string[]` | Tohum/geri dönüş model listesi. `liveModels: false` olduğunda bunlar keşfedilen tek modellerdir. | +| `models?` | `string[]` | Başlangıç/geri dönüş model listesi. `liveModels: false` iken boş olmayan `models` listesini `retainModels` izler; `models` boşsa veya atlanmışsa önce yapılandırılmış `defaultModel`, sonra `retainModels` kullanılır. Yinelenen kimliklerin yalnızca ilk geçtiği konum korunur. | | `liveModels?` | `boolean` | Başlatmada/senkronizasyonda canlı kataloğu getirin (varsayılan `true`). Özel sağlayıcılar `${baseUrl}/models` kullanır; yerleşikler bir kayıt defteri URL'si ve filtresi kullanabilir. | | `selectedModels?` | `string[]` | Keşiften sonra katalog izin listesi. Boş olmaması yalnızca bu kimlikleri gösterir; boş veya atlanmış olması keşfedilen tüm modelleri gösterir. | | `contextWindow?` | `number` | Yukarı akış meta verileri olmadığında sağlayıcı genelinde bağlam geri dönüşü; aksi takdirde daha küçük canlı meta verileri koruyan bir sınır. Modeller kontrol paneli bunu `providerContextCaps` alanından ayrı olarak gösterir. | @@ -476,8 +477,16 @@ uygulamadan önce yerel `zai/glm-5.2` kimliğini geri yükler. Aynı eşleme yer ## Statik model izin listeleri -Yalnızca `models`'ı göstermek için `liveModels: false` ayarlayın. `models` boşsa -veya atlanırsa sağlayıcı yönlendirilen hiçbir modeli göstermez. Canlı keşif, +`liveModels: false` iken `models` boşsa veya atlanmışsa başlangıç listesine önce yapılandırılmış +`defaultModel`, ardından `retainModels` eklenir. Yinelenen kimliklerde ilk geçen korunur. +Açıkça belirtilmiş, boş olmayan `models` listesini ise `retainModels` izler; farklı bir `defaultModel` +kendiliğinden eklenmez. Bu model yine de `models` veya `retainModels` içinde açıkça belirtilebilir. +Bu alanların hiçbiri kimlik sağlamıyorsa başlangıç listesi boştur. Bu sıra, son seçici sırasını +garanti etmez. `selectedModels`, `disabledModels` ve sağlayıcının devre dışı bırakılması kuralları +geçerliliğini korur. `authMode: "forward"` ayrı dalında kalır ve bu yönlendirilmiş statik listeyi +kullanmaz. Bu kurallar canlı keşif başarısızlığındaki geri dönüş davranışını değiştirmez. + +Canlı keşif, önbelleğe almadan önce 4 MiB'den veya 2.000 ham model satırından fazlasını reddeder; yerleşik önayarlar daha düşük sınırlar kullanabilir ve sohbete uygun satırlara filtre uygulayabilir. Büyük boyutlu veya hatalı biçimlendirilmiş 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 47c6147904..3b6ee8a9f3 100644 --- a/docs-site/src/content/docs/tr/reference/configuration/server.md +++ b/docs-site/src/content/docs/tr/reference/configuration/server.md @@ -288,3 +288,7 @@ yeniden kullanır. Hedeflenen hesap ve iş yükünü kapsamlı bir şekilde test `runtimeRole` varsayılan olarak `standalone` değerindedir. Hub; `hub.managementPublicOrigin`, yalnız loopback `hub.managementIngress` (yokken `enabled:false`) ve tam `remoteGui.allowedTailscaleUsers` (yokken boş) kullanır. İstemci anahtarı `config.json` yerine `service-api-token` içinde kalır; döndürme sırasında `service-api-token.prev` geçici olarak bulunabilir. Kullanım kayıtları yansıtılmaz. `remoteGui.allowInsecureHttp`, yalnızca eski strict-schema yapılandırmalarının yüklenebilmesi için tutulan, kullanımdan kaldırılmış bir no-op'tur. Yapılandırmadan silin: pairing grant'leri yalnız loopback veya kimliği doğrulanmış HTTPS üzerinden kabul edilir ve `true` değeri düz HTTP pairing'i yeniden açmaz. + +## Codex kota ağı tanılaması + +Ana Codex hesabının satırındaki `quotaRefresh`, kalan kotayı veya model erişim yetkisini değil, kota sorgusunun sonucunu açıklar. Önbellek kullanıldığında ya da sorgu yapılmadığında alan bulunmayabilir. Sorgu, etkileşimli terminalin değil çalışan proxy servisinin ortamını kullanır. `proxy` ayarlanmazsa mevcut ortam korunur; `"auto"` yalnızca başlangıçta Windows’un statik proxy ayarlarını okur. PAC/WPAD, yalnızca SOCKS ayarları ve çalışma sırasındaki değişiklikler otomatik uygulanmaz. TUN ile başarı, HTTP proxy yolunun da çalıştığını tek başına göstermez. [Komutlar ve durumlar için İngilizce bölüme](/reference/configuration/server/#codex-quota-network-diagnostics) bakın. diff --git a/docs-site/src/content/docs/tr/reference/management-api.md b/docs-site/src/content/docs/tr/reference/management-api.md index 262ad8eabc..afdb835e9c 100644 --- a/docs-site/src/content/docs/tr/reference/management-api.md +++ b/docs-site/src/content/docs/tr/reference/management-api.md @@ -184,12 +184,15 @@ gönderin. Kurtarma gerekebileceğinde karantinayı tercih edin. | `GET /api/models` | Kontrol paneli/CLI model satırlarını döndürün | Toplama doyduğunda `catalog_busy` | | `GET /api/client-config?client=...` | Desteklenen herhangi bir dosya entegrasyonu için salt okunur bir istemci yapılandırması oluşturun | 400 desteklenmeyen istemci; 503 katalog kullanılamıyor | | `PUT /api/disabled-models` | Paylaşılan devre dışı model listesini değiştirin | 400 geçersiz JSON | -| `PUT /api/model-visibility` | Sağlayıcı veya model düzeyindeki görünürlüğü atomik olarak değiştirin | 400 geçersiz sağlayıcı, kapsam, hedef veya gövde | +| `PUT /api/model-visibility` | Sağlayıcı veya model düzeyindeki görünürlüğü atomik olarak değiştirin | 400 geçersiz sağlayıcı, kapsam, hedef veya gövde; 409 `initial_model_selection_pending` (Model listesini yenileyip tekrar deneyin.) | | `GET, POST /api/custom-models` | Özel modelleri listeleyin veya bir tane ekleyin | 400 geçersiz alanlar; 404 sağlayıcı eksik; 409 yinelenen model | | `PUT, DELETE /api/custom-models/{id}` | Bir özel modeli düzenleyin veya silin | 400 geçersiz kimlik/alanlar; 404 bulunamadı; 409 yinelenen model | | `GET, PUT /api/selected-models` | Sağlayıcı izin listelerini ve kullanılabilirliğini okuyun veya bir izin listesini değiştirin | 400 eksik sağlayıcı/gövde; 404 bilinmeyen sağlayıcı; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Ön ayarları okuyun veya preset/all/custom modunu seçin | 400 geçersiz mod veya desteklenmeyen ön ayar; 404 bilinmeyen sağlayıcı; PUT 409 `initial_model_selection_pending` | +Manuel model, Models panosunda aynı sağlayıcı ve model kimliğine sahip satırın yerini alır. OpenAI manuel satırı `openai/` kimliğini ve görünürlük kontrollerini korur. Silindiğinde hesap niteleyicisi olmayan yerel satır geri gelir. Hesapla nitelenen yerel satırlar ayrı kalır. Yerel rotalar ve hesap yetkileri değişmez. Yerel olmayan OpenAI görünürlük hedefi, yapılandırılmış bir manuel modelle eşleşmelidir. + + Güvenilir ilk model listesi hazır olana kadar `/api/selected-models` ve `/api/model-presets` için geçerli PUT istekleri de HTTP 409 ve `initial_model_selection_pending` kodunu döndürür. Model keşfini örneğin `GET /api/models` ile yenileyin ve başarılı olduktan sonra yeniden deneyin. ### OAuth hesapları, sağlayıcı anahtarları ve veri düzlemi anahtarları @@ -230,6 +233,17 @@ döndürülmez. | `GET, PUT /api/provider-context-caps` | Küresel, tüm sağlayıcılar veya tek sağlayıcı bağlam sınırlarını okuyun veya güncelleyin | 400 geçersiz istek; 404 bilinmeyen sağlayıcı | | `GET /api/provider-presets` | Çalışma zamanı kayıt defterinden türetilen GUI sağlayıcı önayarlarını döndürün | — | +Bağlam sınırı yanıtı `caps` (etkin sınırlar) ve `values` (devre dışıyken de saklanan son seçimler) +alanlarını içerir. Sağlayıcının sınırını `value` olmadan etkinleştirmek seçimini geri yükler; +ilk etkinleştirmede genel `contextCapValue` kullanılır. Bu kural OpenAI için de geçerlidir: +anahtar özel bir 922k modu seçmez. Etkin sınır tüm yerel pencereleri sınırlar; uzun bağlamı +destekleyen modeller yalnızca kendi desteklenen üst sınırlarına kadar genişletilebilir. +`{ "value": 600000, "setAll": true }`, genel değeri ve yalnızca etkin sınırları günceller. +Sınırı kapalı olan sağlayıcılar, daha sonra yeniden etkinleştirildiğinde kullanılacak seçimlerini korur. +`value` olmadan `{ "setAll": true }`, yapılandırılmış tüm sağlayıcıların sınırlarını geçerli genel +değerle etkinleştirir ve saklanan seçimlerini değiştirir. Devre dışı bırakmak seçimi silmez; +yeniden yüklemeden sonra da saklar, ancak bir sınır olarak uygulamaz. + `provider_has_dependent_combos` bir güvenlik engelidir: sağlayıcılarını silmeden önce bağımlı komboları kaldırın veya düzenleyin. 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 13d921719e..6989e86a22 100644 --- a/docs-site/src/content/docs/tr/reference/proxy-formats.md +++ b/docs-site/src/content/docs/tr/reference/proxy-formats.md @@ -31,7 +31,7 @@ genel model kimliği birkaç hedef arasından seçim yapması gerektiğinde | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `[DONE]` ile biten `chat.completion.chunk` SSE | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Anthropic belirteç sayısı | `POST /v1/messages/count_tokens` | `{ "input_tokens": sayi }` | Geçerli değil | -| Model keşfi | `GET /v1/models` | Üç katalog sözleşmesinden biri | Geçerli değil | +| Model keşfi | `GET /v1/models` | Katalog veya açıkça istenen Desktop anlık görüntüsü | Geçerli değil | | Ses ve Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | İletilen çağrı oluşturma yanıtı | Ayrı bir yan bant WebSocket her iki yönde de çerçeveleri iletir | | Responses sıkıştırması | `POST /v1/responses/compact` | Değiştirme geçmişi JSON'ı | Geçerli değil | @@ -233,10 +233,18 @@ yerel belgelenmiş tahmini kullanır ve şunu döndürür: { "input_tokens": 123 } ``` +Çözümlenemeyen tarih biçimli bir Desktop kimliği, keşifte yer almayan gerçek bir yerel model +kimliği de olabilir. Mevcut bilgi kimliği çözmeye yetmiyorsa Messages ve count-tokens sabit +`desktop_model_mapping_unavailable` hatasıyla HTTP 503 döndürür; bu, modelin geçersiz olduğunu kanıtlamaz. +Bilinmeyen eski hash takma adları HTTP 400 ile reddedilmeye devam eder. Her iki durumda da tarih +kaldırılmaz ve başka rotaya geçilmez. Bilinen kimlikler, kayıtlı eşlemeler, tam `modelMap` +eşleşmeleri ve tanınan gerçek yerel kimlikler aynı şekilde işlenir. Yeniden denemeden önce model +keşfini yenileyin veya bağlı hub profilini yeniden uygulayın; yalnızca tekrar denemek çözümü +garanti etmez. + ## `GET /v1/models` -Aynı rota uyumsuz katalog zarfları bekleyen üç istemciye hizmet verir. -`client_version` da mevcut olmadıkça Anthropic türü kazanır. +`format=desktop-config` belirtilmezse aşağıdaki olağan katalog sözleşmeleri kullanılır: | Sözleşme | Tetikleyici | Üst düzey şekil | Model kimliği davranışı | | --- | --- | --- | --- | @@ -244,6 +252,29 @@ Aynı rota uyumsuz katalog zarfları bekleyen üç istemciye hizmet verir. | Codex kataloğu | `client_version` sorgu parametresi | `{ "models": [...] }` | Yerel ve yönlendirilen girdiler daha zengin Codex katalog alanlarını, görünürlüğü, çabayı, WebSocket ve çoklu ajan meta verilerini taşır | | Düz OpenAI listesi | Hiçbir tetikleyici yok | `{ "object": "list", "data": [...] }` | Görünür yerel kimlikler yalındır; yönlendirilen kimlikler takma adlar veya `sağlayıcı/model`'dir | +### Desktop yapılandırma anlık görüntüsü + +`GET /v1/models?ids=desktop&format=desktop-config`, user-agent'tan bağımsız olarak Desktop +anlık görüntüsünü seçer. Yanıt `{ "version": 1, "models": [...] }` ve `Cache-Control: no-store` +başlığıdır. İstemci `Accept: application/json`, `anthropic-version: 2023-06-01` ve mevcut veri +erişim kimlik bilgilerini gönderir; yönetici belirteci veya profil yüklemesi gerekmez. +Girdiler Codex katalog satırları değil, hub'ın verdiği Desktop yapılandırma modelleridir. + +Bu biçim `ids=cli` veya herhangi bir `client_version` ile kullanılırsa HTTP 400 döner. Biçim +seçicisi yoksa yukarıdaki olağan sözleşmeler değişmez. Claude kapalıysa +`{ "version": 1, "models": [] }` döner; bağlı Desktop apply bunu kullanılamaz sayar ve yeni +profil yazmaz. Sürüm 1 yerine olağan katalog döndüren eski hub'lar desteklenmez; yerel üretilmiş +kimliklere geçilmez. + +Anlık görüntü salt okunur model listesidir; anahtar döndürme veya profil yükleme API'si değildir. +Desktop anahtar taşıma, kurtarma ve bağlantıyı kesme mevcut istemci yaşam döngüsünü kullanır. +Döndürme modelleri ve seçimi korur; CLI `rotation` alanı `committed` ile `rolled_back` sonucunu +ayırır. Bağlantıyı kesme yönetilen ayarları geri yükler veya tanınan eski profil için standart +moda dönüşü bildirir; kullanıcı alanları ve sonraki geçerli seçimler korunur. Çatışma veya eksik +kurtarma tamamlanmış sayılmaz. Disk değişiklikleri için Desktop'ı yeniden başlatın; bağlantıyı +kesmek hub anahtarını otomatik iptal etmez. [Desktop kılavuzuna](/tr/guides/claude-code/) bakın. +Thinking yeniden gönderimi ve önbellek, ayrı [#3719](https://github.com/lidge-jun/opencodex/issues/3719) işidir. + ## `POST /v1/live` ve Realtime yan bandı `POST /v1/live`, ChatGPT/Codex App Frameless çağrı oluşturma yüzeyini kabul 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 e2db5ddec4..3bbe49646b 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 @@ -88,6 +88,52 @@ Anthropic。若任一提供方请求头包含代理准入密钥,该密钥会 可以设置 `claudeCode.nativePassthrough: false` 来禁用;也可以通过 `claudeCode.anthropicBaseUrl` 指向其他位置。 +## 连接远程 hub 的 Claude Desktop + +已连接的机器运行 `ocx claude desktop apply` 或 `ocx claude desktop` 时,会读取 hub 的 +Desktop 快照,将 hub origin 和 hub 发放的完整模型 ID 原样写入本机 Desktop 配置,不再本地 +生成别名。static/hybrid 模式也复制模型列表;discovery-only 模式使用 hub origin,不嵌入列表。 + +Desktop 配置、模型家族分组及默认值由 hub 管理。在 hub 上修改后,请在客户端重新应用, +并在 Desktop 中重新选择模型。以前只在客户端生成的别名也需要重新应用、重新选择,不会自动 +迁移。`show`、本地编辑和 import/export 仍只操作本地配置。连接期间不支持 +`ocx claude desktop import --apply`,会在保存前拒绝;不带 `--apply` 的 import 仍是本地操作。 + +读取使用现有连接的数据访问凭证,不需要管理员令牌,也不上传配置。旧版 hub 不支持快照、 +响应无效或 Desktop 列表为空时,应用会失败,不会改用本地目录或回环地址。 +请更新或配置 hub 后重新应用。 + +本次别名修改不解决 [#3719](https://github.com/lidge-jun/opencodex/issues/3719) 中独立的 `thinking` / `redacted_thinking` 重放与提示缓存请求。 +只有代理接入凭证不会启用原生 Anthropic 透传,但经过转换的 Anthropic 路由仍可使用提示缓存。 +重放保真和缓存命中率对比仍是独立工作。 + +### 密钥轮换、恢复与断开连接 + +密钥轮换和恢复会同步更新本地连接凭证与该连接管理的 Desktop 配置中的密钥,无需为了迁移 +密钥而手动重新 apply。模型 ID、家族分组、默认值及当前配置选择都会保留;轮换不会重新选中 +管理配置,也不会启用已关闭的集成。CLI JSON 的 `rotation: "committed"` 表示新密钥已生效, +`rotation: "rolled_back"` 表示保留或恢复了旧密钥,不代表新密钥已提交或旧密钥已撤销。 +结果不确定或恢复未完成时,不会报告轮换成功。 + +首次连接应用会保存原先的管理设置和选择,用于恢复;后续 apply 和轮换不会覆盖这份初始记录。 +`ocx disconnect` 恢复连接管理的设置,同时保留用户新增字段和其他配置。只有管理配置仍被选中 +时才恢复之前的选择;用户后来选择的其他有效配置保持不变。新建配置若已包含用户新增内容, +会保留为可读取的标准模式,而不是删除这些内容。`--keep-catalog` 保留的是目录,不是 Desktop +连接密钥。 + +没有原始记录的旧管理配置,只要能明确确认属于当前 hub 和已识别的连接密钥,就能迁移。 +apply、轮换/恢复或直接 disconnect 均可处理,无需新参数或事先重新 apply。系统会警告: +之前的设置未记录,断开连接时将使用标准模式。只移除连接拥有的网关设置,保留用户字段和 +另行选择的有效配置;结果标为标准回退,而非恢复原始设置。 + +管理字段冲突、无法识别的凭证或损坏的恢复记录会保留并报告,不会覆盖。中断的清理仅针对 +同一连接继续,不会删除新连接,也不会在恢复未完成时声称完成。断开前先完成待处理的密钥 +轮换恢复;重试断开时保持原来的目录保留选项。 + +应用、轮换/恢复或恢复设置后,请完全退出并重新打开 Claude Desktop。修改磁盘文件不会替换 +运行中应用持有的密钥,也不会自动退出或重启应用。断开在本地完成,不会自动撤销 hub 密钥或 +删除外部副本;如有需要,请另行在 hub 撤销。 + ## /model 选择器(“From gateway”) 每个条目带有诚实的显示名(如 `gemini-3-pro (gemini)`),并以官方 ModelInfo 形态附带模型能力 信息(推理强度梯度、thinking 类型),使 Claude Desktop 的第三方网关模式能够启用推理强度选择 @@ -126,6 +172,14 @@ v1 别名按字面解码(历史上 model ID 中包含的两字符序列 `~s` / **模型解析顺序:**移除 `[1m]` 标记 → 解码易读别名 → 解码 Desktop 哈希别名 → `modelMap` 精确匹配 → 移除日期后的匹配(移除 `-20250514`)→ 透传。 + + +无法解析的日期型 Desktop ID 也可能是发现结果中缺失的真实原生模型 ID。现有信息不足以 +解析该 ID 时,Messages 和 count-tokens 返回 HTTP 503 及固定错误 `desktop_model_mapping_unavailable`;这并不证明 +模型无效。未知的旧版哈希别名仍返回 HTTP 400。两种情况都不会去除日期或回退到其他路由。 +已知 ID、已注册映射、精确 `modelMap` 匹配及已识别的真实原生 ID 保持原有处理方式。 +请刷新模型发现或重新应用已连接 hub 的配置后再试;仅重试本身不能保证解决。 + 每个条目都带有类似 `gemini-3-pro (gemini)` 的显示名称,以及官方 `ModelInfo` 结构中的完整 模型能力(推理强度阶梯、思考类型)。真正的 Anthropic 模型在两个界面上都保留其规范 ID。 @@ -217,6 +271,8 @@ opencodex 会在**已路由**请求中将该技能内容替换为一个短占位 查找顺序:发现别名 → 精确 ID → 移除日期后缀的 ID(`-20250514`)→ 透传。 +拒绝规则见 [Desktop 别名解析](#desktop-alias-resolution)。 + ## Sidecar 矩阵:Web Search 与图像理解 不同路由模型拥有的托管工具和图像能力并不相同。opencodex 会在主模型回答前补齐这些能力: diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index e2a5601d62..552c0c3f53 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -260,8 +260,12 @@ provider 形式一样,从 `OPENCODEX_API_AUTH_TOKEN` 传入 `x-opencodex-api-k 所有发现到的模型。一个不在 allowlist 里的 id 永远不会进入 catalog。 2. **`disabledModels`**(顶层) - 会同时隐藏 catalog 和 `/v1/models` 中的模型,并把裸原生 GPT slug 切成 `visibility: "hide"`。 -3. **`liveModels: false` 且 `models` 为空** - 当 live discovery 关闭而 `models` 为空或省略时,opencodex - 不会为那个 provider 暴露任何路由模型。 +3. **`liveModels: false`** — `liveModels: false` 时,若 `models` 为空或省略,初始列表先加入已配置的 `defaultModel`, + 再加入 `retainModels`,重复 ID 仅保留首次出现的位置。若显式设置了非空 `models`,则按 + `models`、`retainModels` 顺序构建,不会自动加入另一个 `defaultModel`;仍可将该模型明确写入 + `models` 或 `retainModels`。这些字段均未提供 ID 时,初始列表为空。此顺序不保证最终选择器的显示顺序。 + `selectedModels`、`disabledModels` 和提供商禁用策略仍然适用。`authMode: "forward"` 保留原有独立分支, + 不使用此静态路由列表。这些规则不改变实时发现失败时的回退行为。 4. **Cursor `GetUsableModels`** - Cursor adapter 通过它的 protobuf `GetUsableModels` RPC 发现模型,而不是 `/models`,所以 Cursor 侧的变动会独立于其他 provider 改变哪些 id 可见。 5. **缓存和 `ocx sync`** - live catalog 的缓存时间大约是五分钟(`modelCacheTtlMs`,默认 `300000`)。 diff --git a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md index 2f229176cd..a07d4cf01f 100644 --- a/docs-site/src/content/docs/zh-cn/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-cn/guides/model-ordering.md @@ -20,6 +20,8 @@ priority 为 `i * N + j` 的 selector 行,其中 `j` 是从 0 开始的 select 没有 selector 时的相关 priority 如下: +以下优先级表和示例适用于未开启完整选择器排序的情况。 + | 目录条目 | Priority | 来源 | | --- | ---: | --- | | `subagentModels[i]` | `i`(`0` 至 `4`) | `src/codex/catalog/sync.ts` 中的 featured rank map | @@ -106,6 +108,40 @@ subagentModels = [ id 请勿超过五个。存在账户 selector 时,一个裸原生选项可能展开为多个 selector-qualified 行,因此 已配置的选项与公布的行不一定一一对应。 -目前 `OcxConfig` 中没有通用的 `modelOrder`、`providerOrder` 或 priority map 设置。受支持的排序 -字段是 `subagentModels`;`disabledModels` 和各 provider 的 `selectedModels` 都是可见性字段。 -因此,要更改选择器其余部分的顺序,需要修改代码行为,而不是调整配置。 +`modelPickerOrder` 只控制选择器的显示顺序。如果列表只有路由 ID `/`, +其中未置顶的行会按列表顺序进入独立的显示区间(`1000 + i`)。未列出的路由行保留原有优先级, +因此仍排在该区间之前。同时列在 `subagentModels` 中的行保留置顶优先级,原生行也保持原有位置。 +需要控制相对顺序的路由行都应列入列表。 + +要对整个选择器排序,请加入至少一个不含 `/` 的裸目录 ID,例如 `gpt-5.6-sol`。 +空字符串或只有空白的条目不会启用此模式。 + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +列出的行按数组顺序排在最前面,未列出的行随后按原有优先级排列。匹配使用精确的目录 ID: +`gpt-5.6-sol` 和 `openai/gpt-5.6-sol` 是不同的行。同一路由 ID 的原始写法和编码写法也可匹配, +但精确匹配优先于等价匹配。空条目和只有空白的条目会被忽略。账户限定行必须使用包含 selector 的完整 ID。 + +### 迁移提醒:现有列表中的原生 ID + +以前 `modelPickerOrder` 中的裸原生 ID 会被忽略。现在,现有列表只要包含这样的 ID,就会启用 +整个选择器的排序,包括置顶行。要保持以前只调整路由行的行为,请移除裸 ID。 +未设置、空列表、只有空白条目的列表以及只有路由 ID 的列表都保留原有行为。 + +`modelPickerOrder` 保留 OpenCodex 按原有优先级计算最多五个首选候选项的规则,供子代理指导使用。 +每个移动行的原有优先级与原生 `priority` 分开保存;仅改变选择器顺序不得改变这一计算结果。 +它也不会限制通过精确模型名称指定 override 的资格:公布的列表不是允许列表,现有的认证、模型、 +effort 和后端限制仍然适用。 + +原生 Codex 按原生 `priority` 排序,从符合条件且在选择器中可见的模型中取前五个,公布在 +`spawn_agent` 中。这适用于 V1,以及公开模型 override 的 V2。因此,即使 OpenCodex 的首选候选项 +不变,原生公布的五个模型仍可能随选择器顺序改变。V1 不接收 OpenCodex 注入的首选模型列表。 +V2 在客户端目录状态允许时,可以额外接收基于原有优先级的 OpenCodex 指导;这些指导不会重排 +原生工具公布的列表。 + +`disabledModels` 和各提供商的 `selectedModels` 仍是可见性字段。没有独立的 `modelOrder`、 +`providerOrder` 或优先级映射设置。 diff --git a/docs-site/src/content/docs/zh-cn/guides/model-routing.md b/docs-site/src/content/docs/zh-cn/guides/model-routing.md index 70f798b9d0..5bfe9d28bc 100644 --- a/docs-site/src/content/docs/zh-cn/guides/model-routing.md +++ b/docs-site/src/content/docs/zh-cn/guides/model-routing.md @@ -75,9 +75,12 @@ transport;这些凭证路径互不 fallback。 - `provider.disabled: true` 会把该提供商排除在目录发现之外。显式 `provider/model` 请求会失败, `defaultModel` / `models[]` 扫描也会跳过它。 - `providerContextCaps` 为各提供商设置 Codex 可见的上下文上限。`contextCapValue` 是仪表盘的默认值, - 默认为 350,000;但只有 `providerContextCaps` 中列出了提供商时才会生效。仅当勾选“应用到所有已路由的 - 提供方”时,修改仪表盘值才会重新指向所有已启用提供商;否则每个提供商保留自己的上限。上限只能降低 - 已知上下文,不会把它调高,也不会改变上游模型的实际限制。 + 默认为 350,000;仅设置这个值不会应用上限,提供商必须出现在 `providerContextCaps` 中才会生效。 + 勾选“应用到所有已路由的提供方”后,修改仪表盘值只会更新已开启的上限;未勾选时,各提供商保留自己的上限。 + 普通的已知窗口只能缩小;支持长窗口的原生模型可以扩展到该模型支持的上限,但不会改变上游模型的实际限制。 + 关闭上限后,选择值保存在 `providerContextCapValues` 中,重新加载后仍保留;再次开启时恢复该选择值。 + 关闭期间不会把保存的值作为限制应用。不带 `value` 的 `{ "setAll": true }` 会按当前全局值开启所有 + 已配置提供商的上限,并替换它们保存的选择值。 ```json { diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 66a9187987..dbb6383120 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -47,6 +47,14 @@ bun run dev:gui | **Storage** | 只读查看 CODEX_HOME 磁盘占用(会话、归档、数据库、附件)。可选归档清理:预览最旧 N%,默认隔离到 `CODEX_HOME/.trash`,或勾选后永久删除。**自动清理策略**为可选且**默认关闭**(`storageCleanupPolicy.enabled`);可在 Storage 页配置阈值/目标/计划/模式,或点「立即运行」。可在 Storage 页从隔离区恢复(JSONL + 线程)。活动会话保持只读。Codex 锁定最新/活动的 `state_*.sqlite` 时拒绝清理与恢复。 | | **Stop** | 优雅地停止代理和已安装的后台服务,恢复原生 Codex 并退出(`POST /api/stop`)。在使用任务计划程序后端的 Windows 上,仪表板会拒绝并提示改用 `ocx stop`:任务结束后包装器仍可能重新拉起代理,只有运行在代理之外的 stop 才能在恢复客户端配置前确认这个重启窗口。被拒绝时不会做任何更改。 | +### 筛选请求日志 + +Logs 可组合界面、被拦截请求、提供商、完整模型名、状态、时间、速度和会话 ID,筛选当前已加载的日志。选项包含回退尝试;模型匹配忽略大小写及首尾空格,但不做部分匹配。日志中消失的选项恢复为全部。 + +时间范围为最近 15 分钟、1 小时或 1 天;Logs 标签页每 30 秒更新一次,即使关闭自动刷新也会更新。速度按完整请求耗时计算每秒输出 token,分为小于 15、15 至小于 50、至少 50;启用速度筛选时排除无测量值的请求。成功为 2xx,错误为 4xx/5xx。 + +显示匹配数与已加载总数;重置恢复全部行,并区分无匹配与空日志。界面选择支持方向键及 Home/End,不查询已加载范围之外的历史记录。 + ### 链接到某个部分 布局只有一种,无需切换。Dashboard 的各个部分都有自己的地址:`#dashboard` 打开 Overview,`#dashboard/providers` 与 `#dashboard/models` 打开另外两个。刷新、收藏和后退都会保留当前所在的部分。**Logs** 同理,使用 `#logs` 与 `#logs/debug`。旧的 `#providers/workspace` 书签现在会跳转到 `#providers`。 @@ -58,6 +66,19 @@ bun run dev:gui **Models** 开关表示 Codex 中的最终可见状态。路由模型只有在 provider allowlist 中(或未设置 allowlist)且未被禁用时才会开启。开启模型会原子地协调两个过滤条件;**全部开启** 会清除 allowlist,因此以后新发现的模型也会开启。 +### 在提供方工作区管理模型 + +在提供方的**模型**标签页中,**删除**会移除已保存的自定义定义。原有的原生模型或实时发现的模型可能 +重新显示,因此模型数量可能保持不变。**隐藏**只改变目录可见性,不会删除定义,也不会改变直接路由策略。 +点击**在模型中管理可见性**可打开**模型**页面并恢复显示;即使提供方标签页已没有任何模型行,也能使用此入口。 + +**添加**会保存自定义定义,但不会清除已有的隐藏状态或提供方选择规则。保存后的模型可能仍被隐藏。 +如果模型已存在,请在**模型**页面管理其可见性。已确认保存时,即使目录刷新失败,定义也已保存; +请按刷新提示操作,不要重复添加。若无法确认更改结果,请先刷新模型状态,再重试。 + +提供方的模型数量统计服务器返回的当前模型清单中未禁用的唯一条目,计数在搜索和显示数量限制之前进行。 +它不是允许列表的大小或实时发现的模型数量,也不能证明条目来自上游发现。选择标记和发现信息与该计数分开显示。 + ## 委派选择器与生成路由的区别 Dashboard 的 **Sub-agent delegation** 选择器会保存 `injectionModel`,以及可选的 @@ -141,7 +162,7 @@ GUI 是代理 JSON 管理 API 之上的轻量客户端。常用 endpoint 包括 | `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | 选择下一次请求使用的账号并配置账号池路由。 | | `GET /api/codex-auth/active` · `PUT /api/codex-auth/accounts/priority` | 读取实际生效的账号(含表示是否固定的 `pinned` 和指明被固定账号的 `pinnedAccountId`),并设置单个账号的选择顺序。 | | `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | 通过浏览器登录添加池账号。 | -| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | 使用 tail、provider、精确状态码或状态类别筛选近期请求元数据。`limit`/`offset` 从最新一行向前分页(`offset=0` 为最新一页)。响应为 `{ timeZone, total, logs }`,其中 `total` 为分页前的匹配行数。 | +| `GET /api/logs?tail=50&limit=20&offset=0&provider=...&status=5xx` | 使用 tail、provider、精确状态码或状态类别筛选近期请求元数据。`limit`/`offset` 从最新一行向前分页(`offset=0` 为最新一页)。响应为 `{ timeZone, generatedAt, total, logs }`,其中 `total` 为分页前的匹配行数。 | | `GET` / `PUT /api/subagent-models` | 读取或设置五个置顶的 `spawn_agent` override 模型。 | | `POST /api/stop` | 停止代理/服务,恢复原生 Codex 并退出。在 Windows 任务计划程序后端会以 `respawnable_service` 拒绝,无法读取该状态时以 `service_state_unknown` 拒绝;两种情况都不会做任何更改。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index dfae403438..f0c6ee5a59 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -153,6 +153,10 @@ ocx status --json 将 opencodex 作为登录管理的后台服务运行(macOS **launchd**、Linux **systemd user unit**、Windows **Task Scheduler**),在登录时自动启动,在崩溃时自动重启。服务运行会设置 `OCX_SERVICE=1`,因此重启时不会反复改动 Codex 配置。 +Windows 任务计划程序安装使用普通进程优先级(`Priority=4`)。旧的后台优先级(`7`,省略时调度器也默认使用 `7`) +可能在 CPU 竞争时延迟健康检查响应,导致进程仍存活时托盘显示 Offline。升级后运行 `ocx service repair`, +即可迁移该注册优先级并重启服务;过程中可能需要批准 UAC 提示。已设为普通或高优先级时,不会仅因优先级而重新注册。 + | 子命令 | 操作 | | --- | --- | | none | 服务不存在时安装并启动;已存在时刷新并重启。正常的 Windows 任务计划程序定义会复用;过时定义可能会重新注册并需要提升权限。 | 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 bcdb51cf05..1c8d71eeca 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 @@ -52,7 +52,7 @@ per-role fallback 链必须放在 opencodex 配置里。把 `model_fallback` 写 `$CODEX_HOME/agents/*.toml` 会让 Codex 0.146+ 把整个角色文件当作未知字段拒绝并跳过该角色 (#1190)。TOML 中的旧版 `model_fallback` 仍会被读取以保持向后兼容,但 `ocx doctor` 会标记它。 -opencodex 会跳过已禁用、不可路由、不健康、处于冷却中,或已达到配额阈值的候选项。可用性快照会在 `subagentModelFallbackPollMs` 期间缓存。对于加密的子任务,候选链只包含规范的原生 ChatGPT 目标,以及通过 `allowEncryptedV2AgentTasks: true` 明确信任的直接密钥认证 Responses 路由。如果没有任何目标能处理加密载荷,请求就会失败,而不是把不可读的密文路由到别处。combo 仍然只使用规范的原生目标。 +opencodex 会跳过已禁用、不可路由、不健康、处于冷却中,或已达到配额阈值的候选项。可用性快照会在 `subagentModelFallbackPollMs` 期间缓存。对于加密的子任务,候选链只包含规范的原生 ChatGPT 目标,以及通过 `allowEncryptedV2AgentTasks: true` 明确信任的直接密钥认证 Responses 路由。如果没有目标能处理加密载荷,且可选恢复无法支持路由发送,请求就会失败,不会转发不可读的密文。combo 会先尝试可用的规范原生目标;如果没有可选择的原生目标或原生尝试已耗尽,且已启用 `agentTaskRecovery`,会在路由到 combo 目标前对加密的 `NEW_TASK` 恢复一次。 ```json { diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index f2d245b5ec..b7339cd4c2 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -27,8 +27,9 @@ ocx models provider openrouter on | `providers` | `Record` | — | 提供者名称到提供者配置的映射。 | | `openaiProviderTierVersion?` | `2` | 由迁移设置 | 标记单一、可感知选项的 OpenAI 投影已完成。 | | `disabledModels?` | `string[]` | — | 从 Codex catalog 和 `/v1/models` 中隐藏、但不阻止直接 proxy 调用的 model。routed id 会从列表中移除。account-qualified native id 只隐藏对应 selector row;bare native GPT id 会隐藏 bare row 以及该 model 的所有 account-selector row。Models 页面只显示裸原生行和路由行;若只隐藏一个 selector-qualified 行,请直接设置此配置字段。 | -| `providerContextCaps?` | `Record` | `{}` | 按提供者设置、对 Codex 可见的上下文上限。上限只会降低已知的上下文窗口。 | -| `contextCapValue?` | `number` | `350000` | 仪表板上下文上限控件使用的默认值。仅当勾选“应用到所有已路由的提供方”时,修改它才会把值应用到所有已路由提供方(包括没有现有 `providerContextCaps` 条目的提供方);否则每个提供方保留自己的上限。 | +| `providerContextCaps?` | `Record` | `{}` | 按提供商设置的有效上下文上限。普通窗口只能缩小;支持长窗口的原生模型可以扩展到该模型支持的上限。 | +| `providerContextCapValues?` | `Record` | `{}` | 各提供商最后选择的上限,关闭后仍保留。仅保存这些值不会启用上限。有效值优先于保存的选择值。 | +| `contextCapValue?` | `number` | `350000` | 首次开启时使用的默认值。再次开启时恢复该提供商的选择值。修改全局值时附带 `setAll: true` 只会更新已开启的上限;不带值的 `setAll: true` 会按当前全局值开启所有已配置提供商的上限。 | | `codexAccounts?` | `CodexAccount[]` | `[]` | 由 Codex Auth 管理的 ChatGPT/Codex 池账户元数据。密钥单独存放在 `codex-accounts.json` 中。 | | `pausedCodexAccountIds?` | `string[]` | `[]` | 在恢复之前从 Pool 选择中排除的账户,包括被暂停时的主 `__main__` 账户。 | | `codexAccountNamespaces?` | `Record` | — | 将任意公开 model selector 映射到已保存 Codex account target 的可选配置。启用账户限定的选择器行后,target 存在的每个 selector 都会在 Codex picker 中添加独立的 `/` row,且每个 row 只使用对应账户。只要有 selector 生效,bare native row 就会在 picker 中隐藏;但除非显式禁用,其 id 仍可路由,并继续列在 raw `/v1/models` 中。 | @@ -81,7 +82,7 @@ selector,而不是分配一个新名称。 | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key 头部样式。默认使用原生 `x-api-key`;仅对 key-auth `anthropic` 提供者有效。 | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 多 key 池。`apiKey` 会镜像当前激活条目;每个条目都有 `id`、`key`、可选 `label`,以及可选的数值 `addedAt`。 | | `defaultModel?` | `string` | 当选择该提供者但未显式指定模型时使用的模型。 | -| `models?` | `string[]` | 种子/回退模型列表。配合 `liveModels: false` 时,这些就是唯一发现到的模型。 | +| `models?` | `string[]` | 初始/回退模型列表。`liveModels: false` 时,非空 `models` 后接 `retainModels`;若 `models` 为空或省略,则按已配置的 `defaultModel`、`retainModels` 顺序构建初始列表,重复 ID 仅保留首次出现的位置。 | | `liveModels?` | `boolean` | 启动/同步时获取实时目录(默认 `true`)。自定义提供者使用 `${baseUrl}/models`;内置项可能使用注册表 URL 并进行过滤。 | | `selectedModels?` | `string[]` | 发现之后的目录允许列表。非空时只暴露这些 id;为空或省略时则暴露全部发现到的模型。 | | `modelDisplayNames?` | `Record` | 持久的仅显示名称,以此提供者的精确原生模型 id 为键。键区分大小写。名称优先于提供者目录元数据,并且不会改变身份验证、适配器、路由、计费或上游请求。该映射最多可包含 2,000 个条目,与发现上限相同。 | @@ -357,7 +358,14 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型 ## 静态模型允许列表 -将 `liveModels: false` 设为只暴露 `models`。如果 `models` 为空或省略,该提供者将不暴露任何路由模型。实时发现会在缓存前拒绝超过 4 MiB 或 2,000 条原始模型行;内置预设可能使用更低的限制,并过滤为可聊天的行。过大或格式错误的结果会走陈旧/配置回退。合法的、零可用结果的发现仍然具有权威性,不会被静默替换或截断。 +`liveModels: false` 时,若 `models` 为空或省略,初始列表先加入已配置的 `defaultModel`, +再加入 `retainModels`,重复 ID 仅保留首次出现的位置。若显式设置了非空 `models`,则按 +`models`、`retainModels` 顺序构建,不会自动加入另一个 `defaultModel`;仍可将该模型明确写入 +`models` 或 `retainModels`。这些字段均未提供 ID 时,初始列表为空。此顺序不保证最终选择器的显示顺序。 +`selectedModels`、`disabledModels` 和提供商禁用策略仍然适用。`authMode: "forward"` 保留原有独立分支, +不使用此静态路由列表。这些规则不改变实时发现失败时的回退行为。 + +实时发现会在缓存前拒绝超过 4 MiB 或 2,000 条原始模型行;内置预设可能使用更低的限制,并过滤为可聊天的行。过大或格式错误的结果会走陈旧/配置回退。合法的、零可用结果的发现仍然具有权威性,不会被静默替换或截断。 当需要继续运行发现,但只有选定 id 应该出现在 Codex 和 `/v1/models` 中时,请使用 `selectedModels`。仪表板会保留完整的已发现列表,以便之后调整允许列表。 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 bee7942398..211e141650 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 @@ -181,3 +181,7 @@ Anthropic OAuth 侧车会复用 opencodex 现有的 Claude Code OAuth 指纹。 `runtimeRole` 默认为 `standalone`。Hub 使用 `hub.managementPublicOrigin`、仅回环的 `hub.managementIngress`(缺省为 `enabled:false`)和准确的 `remoteGui.allowedTailscaleUsers`(缺省为空)。客户端密钥保存在 `service-api-token` 而不是 `config.json`;轮换期间可能暂时存在 `service-api-token.prev`。使用记录不会镜像。 `remoteGui.allowInsecureHttp` 是已弃用的 no-op,仅为让旧的严格 schema 配置继续加载而保留。请从配置中删除它:pairing grant 只接受 loopback 或已认证的 HTTPS;设为 `true` 也不会重新开放明文 HTTP pairing。 + +## Codex 额度网络诊断 + +主 Codex 账户行中的 `quotaRefresh` 描述额度查询结果,并不代表剩余额度或模型访问权限。读取缓存或未执行查询时,该字段可能省略。查询使用正在运行的代理服务的环境,而不是当前终端的环境。未设置 `proxy` 时保留现有环境;`"auto"` 只在启动时读取 Windows 静态代理设置,不自动处理 PAC/WPAD、仅 SOCKS 的设置或运行中的更改。TUN 测试成功并不能单独证明 HTTP 代理路径正常。命令和状态说明见[英文网络诊断章节](/reference/configuration/server/#codex-quota-network-diagnostics)。 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 103921d1ed..9391afb5fd 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 @@ -144,12 +144,15 @@ Authorization: Bearer | `GET /api/models` | 返回仪表板/CLI 模型行 | 收集饱和时返回 `catalog_busy` | | `GET /api/client-config?client=...` | 为任意支持的文件集成构建只读客户端配置 | 400 不支持的客户端;503 目录不可用 | | `PUT /api/disabled-models` | 替换共享的禁用模型列表 | 400 无效 JSON | -| `PUT /api/model-visibility` | 原子性地更改 provider 级或 model 级可见性 | 400 provider、scope、target 或请求体无效 | +| `PUT /api/model-visibility` | 原子性地更改 provider 级或 model 级可见性 | 400 provider、scope、target 或请求体无效; 409 `initial_model_selection_pending` (刷新模型列表后重试。) | | `GET, POST /api/custom-models` | 列出自定义模型或添加一个 | 400 字段无效;404 provider 缺失;409 模型重复 | | `PUT, DELETE /api/custom-models/{id}` | 编辑或删除一个自定义模型 | 400 id/字段无效;404 未找到;409 模型重复 | | `GET, PUT /api/selected-models` | 读取 provider 允许列表和可用性,或替换一个允许列表 | 400 缺少 provider/请求体;404 未知 provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | 读取预设信息或选择 preset/all/custom 模式 | 400 模式无效或不支持该预设;404 未知提供者; PUT 409 `initial_model_selection_pending` | +手动模型会替换 Models 仪表板中 provider 和 model ID 相同的行。OpenAI 手动行保留 `openai/`,并支持可见性控制。删除手动行后,不带账户限定符的原生行会恢复。带账户限定符的原生行仍单独保留。原生路由和账户权限不会改变。非原生 OpenAI 可见性目标必须匹配已配置的手动模型。 + + 可靠的初始模型列表尚未确认时,有效的 `PUT /api/selected-models` 和 `PUT /api/model-presets` 请求也会返回 HTTP 409 和代码 `initial_model_selection_pending`。请使用 `GET /api/models` 等方式刷新模型列表,成功后再重试。 ### OAuth 账户、provider 密钥和数据平面密钥 @@ -188,6 +191,14 @@ Authorization: Bearer | `GET, PUT /api/provider-context-caps` | 读取或更新全局、全部 provider,或单个 provider 的上下文上限 | 400 请求无效;404 未知 provider | | `GET /api/provider-presets` | 返回从运行时注册表派生的 GUI provider 预设 | — | +上下文上限响应包含 `caps`(当前有效的上限)和 `values`(关闭后仍保留的最后选择值)。 +开启提供商的上限时,如果未指定 `value`,则恢复其选择值;首次开启时使用全局 `contextCapValue`。 +OpenAI 也遵循此规则:开关不会选择特殊的 922k 模式。有效上限约束每个原生窗口;支持长上下文的模型 +只能扩展到该模型支持的上限。 +`{ "value": 600000, "setAll": true }` 修改全局值,并且只更新已开启的上限;上限已关闭的提供商保留 +自己的选择值,供之后开启时恢复。不带 `value` 的 `{ "setAll": true }` 会按当前全局值开启所有 +已配置提供商的上限,并替换保存的选择值。关闭上限不会清除选择值,重新加载后仍保留,但不会将其作为限制应用。 + `provider_has_dependent_combos` 是一个安全屏障:在删除 provider 之前,先移除或编辑依赖它的 combos。 ### 侧边栏与基于同意的动作 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 d087650071..21948d6264 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 @@ -27,7 +27,7 @@ Responses 表示是这座桥的中心。原生兼容的路由可以跳过部分 | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | 以 `chat.completion.chunk` SSE 结尾并带 `[DONE]` | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Anthropic token count | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | 不适用 | -| 模型发现 | `GET /v1/models` | 三种目录契约之一 | 不适用 | +| 模型发现 | `GET /v1/models` | 目录或显式 Desktop 快照 | 不适用 | | 语音和 Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | 转发的调用创建响应 | 独立的 sideband WebSocket 双向转发帧 | | Responses compaction | `POST /v1/responses/compact` | 替换历史 JSON | 不适用 | @@ -168,9 +168,15 @@ choice 增量、带 `finish_reason` 的终止 choice,以及 `data: [DONE]`。 { "input_tokens": 123 } ``` +无法解析的日期型 Desktop ID 也可能是发现结果中缺失的真实原生模型 ID。现有信息不足以 +解析该 ID 时,Messages 和 count-tokens 返回 HTTP 503 及固定错误 `desktop_model_mapping_unavailable`;这并不证明 +模型无效。未知的旧版哈希别名仍返回 HTTP 400。两种情况都不会去除日期或回退到其他路由。 +已知 ID、已注册映射、精确 `modelMap` 匹配及已识别的真实原生 ID 保持原有处理方式。 +请刷新模型发现或重新应用已连接 hub 的配置后再试;仅重试本身不能保证解决。 + ## `GET /v1/models` -同一路由要服务三种期望不兼容目录封装的客户端。除非同时存在 `client_version`,否则 Anthropic 形态优先。 +未指定 `format=desktop-config` 时,使用以下普通目录契约: | 契约 | 触发条件 | 顶层形态 | 模型 ID 行为 | | --- | --- | --- | --- | @@ -178,6 +184,25 @@ choice 增量、带 `finish_reason` 的终止 choice,以及 `data: [DONE]`。 | Codex catalog | `client_version` 查询参数 | `{ "models": [...] }` | 原生和路由条目携带更丰富的 Codex catalog 字段、可见性、effort、WebSocket 和 multi-agent 元数据 | | Plain OpenAI list | 两个触发条件都没有 | `{ "object": "list", "data": [...] }` | 可见的原生 ID 是裸值;路由 ID 是别名或 `provider/model` | +### Desktop 配置快照 + +`GET /v1/models?ids=desktop&format=desktop-config` 显式选择 Desktop 快照,不依赖 +user-agent。响应为 `{ "version": 1, "models": [...] }`,带有 `Cache-Control: no-store`。 +客户端发送 `Accept: application/json`、`anthropic-version: 2023-06-01` 及现有数据访问凭证; +不需要管理员令牌,也不上传配置。条目是 hub 发放的 Desktop 配置模型,不是 Codex 目录行。 + +此格式与 `ids=cli` 或任意 `client_version` 一起使用时返回 HTTP 400。不指定格式时,上述普通 +契约保持不变。Claude 关闭时返回 `{ "version": 1, "models": [] }`;已连接的 Desktop apply +会视为不可用,不写入替代配置。返回普通目录而非版本 1 的旧 hub 不受支持,客户端不会回退到 +本地生成的 ID。 + +快照仍是只读模型列表,不是密钥轮换或配置上传 API。Desktop 密钥迁移、恢复与断开由现有 +客户端连接流程处理。轮换保留模型条目和选择;CLI 的 `rotation` 区分 `committed` 与 +`rolled_back`。断开会恢复管理设置,或对已确认的旧配置报告标准回退,同时保留用户字段和 +后来有效的选择。冲突或未完成的恢复不会标为完成。需要重启 Desktop 才会读取磁盘变更; +断开不会自动撤销 hub 密钥。参见 [Desktop 指南](/zh-cn/guides/claude-code/)。 +thinking 重放与提示缓存仍由独立的 [#3719](https://github.com/lidge-jun/opencodex/issues/3719) 跟进。 + ## `POST /v1/live` 和 Realtime sideband `POST /v1/live` 接受 ChatGPT/Codex App 的 Frameless call-creation 表面。 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 f07b2f97c6..ccfb3b9ddd 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 @@ -112,9 +112,12 @@ Claude Desktop 使用與 Claude Code 分開的設定檔。在儀表板開啟 **C 你也可以用命令列管理同一份設定檔: +以下設定檔編輯說明適用於本機設定檔;連接遠端 hub 時的套用方式另見下節。 + ```bash ocx claude desktop [apply] ocx claude desktop show [--json] +ocx claude desktop status [--json] ocx claude desktop move [--default] ocx claude desktop default ocx claude desktop export @@ -168,6 +171,52 @@ Anthropic。若任一供應商標頭含有代理許可密鑰,該密鑰會被 可以設定 `claudeCode.nativePassthrough: false` 來停用;也可以透過 `claudeCode.anthropicBaseUrl` 指向其他位置。 +## 連接遠端 hub 的 Claude Desktop + +已連接的機器執行 `ocx claude desktop apply` 或 `ocx claude desktop` 時,會讀取 hub 的 +Desktop 快照,將 hub origin 和 hub 發出的完整模型 ID 原樣寫入本機 Desktop 設定,不再於本機 +產生別名。static/hybrid 模式也複製模型清單;discovery-only 模式使用 hub origin,不嵌入清單。 + +Desktop 設定檔、模型家族分組及預設值由 hub 管理。在 hub 上修改後,請在客戶端重新套用, +並在 Desktop 中重新選擇模型。以前只在客戶端產生的別名也需要重新套用、重新選擇,不會自動 +移轉。`show`、本機編輯及 import/export 仍只操作本機設定。連接期間不支援 +`ocx claude desktop import --apply`,會在儲存前拒絕;不帶 `--apply` 的 import 仍是本機操作。 + +讀取使用現有連線的資料存取憑證,不需要管理員權杖,也不會上傳設定檔。舊版 hub 不支援快照、 +回應無效或 Desktop 清單為空時,套用會失敗,不會改用本機目錄或回環位址。 +請更新或設定 hub 後重新套用。 + +本次別名修改不解決 [#3719](https://github.com/lidge-jun/opencodex/issues/3719) 中獨立的 `thinking` / `redacted_thinking` 重播與提示快取請求。 +只有代理存取憑證不會啟用原生 Anthropic 透傳,但經過轉換的 Anthropic 路由仍可使用提示快取。 +重播保真與快取命中率比較仍是獨立工作。 + +### 金鑰輪換、復原與中斷連線 + +金鑰輪換和復原會同步更新本機連線憑證與該連線管理的 Desktop 設定中的金鑰,無須為了移轉 +金鑰而手動重新 apply。模型 ID、家族分組、預設值及目前設定選擇都會保留;輪換不會重新選取 +管理設定,也不會啟用已關閉的整合。CLI JSON 的 `rotation: "committed"` 表示新金鑰已生效, +`rotation: "rolled_back"` 表示保留或還原了舊金鑰,不代表新金鑰已提交或舊金鑰已撤銷。 +結果不確定或復原未完成時,不會回報輪換成功。 + +首次連線套用會儲存原先的管理設定和選擇,以供還原;後續 apply 和輪換不會覆寫這份初始紀錄。 +`ocx disconnect` 還原連線管理的設定,同時保留使用者新增欄位和其他設定檔。只有管理設定檔 +仍被選取時才還原之前的選擇;使用者後來選取的其他有效設定檔保持不變。新建設定檔若已有 +使用者新增內容,會保留為可讀取的標準模式,而不是刪除這些內容。`--keep-catalog` 保留的是 +目錄,不是 Desktop 連線金鑰。 + +沒有原始紀錄的舊管理設定檔,只要能明確確認屬於目前 hub 和已識別的連線金鑰,就能移轉。 +apply、輪換/復原或直接 disconnect 均可處理,無須新參數或事先重新 apply。系統會警告: +先前的設定未記錄,中斷連線時將使用標準模式。只移除連線擁有的閘道設定,保留使用者欄位和 +另行選取的有效設定檔;結果標為標準回退,而非還原原始設定。 + +管理欄位衝突、無法識別的憑證或損壞的還原紀錄會保留並回報,不會覆寫。中斷的清理僅針對 +同一連線繼續,不會刪除新連線,也不會在還原未完成時宣稱完成。中斷前先完成待處理的金鑰 +輪換復原;重試中斷時保持原來的目錄保留選項。 + +套用、輪換/復原或還原設定後,請完全退出並重新開啟 Claude Desktop。修改磁碟檔案不會替換 +執行中應用程式持有的金鑰,也不會自動退出或重新啟動應用程式。中斷連線在本機完成,不會 +自動撤銷 hub 金鑰或刪除外部副本;如有需要,請另行在 hub 撤銷。 + ## /model 選擇器(“From gateway”) Claude Code 2.1.129+ 透過 `GET /v1/models?limit=1000` 發現閘道器模型,並在原生 `/model` @@ -199,6 +248,14 @@ user-agent 會獲得易讀的 CLI 形式,其他用戶端會獲得 Desktop 雜 **模型解析順序:**移除 `[1m]` 標記 → 解碼易讀別名 → 解碼 Desktop 雜湊別名 → `modelMap` 精確匹配 → 移除日期後的匹配(移除 `-20250514`)→ 透傳。 + + +無法解析的日期型 Desktop ID 也可能是探索結果中缺少的真實原生模型 ID。現有資訊不足以 +解析該 ID 時,Messages 和 count-tokens 回傳 HTTP 503 及固定錯誤 `desktop_model_mapping_unavailable`;這不代表 +模型無效。未知的舊版雜湊別名仍回傳 HTTP 400。兩種情況都不會移除日期或回退到其他路由。 +已知 ID、已註冊映射、精確 `modelMap` 匹配及已識別的真實原生 ID 維持原有處理方式。 +請重新整理模型探索或重新套用已連接 hub 的設定後再試;僅重試本身不能保證解決。 + 每個條目都帶有類似 `gemini-3-pro (gemini)` 的顯示名稱,以及官方 `ModelInfo` 結構中的完整 模型能力(推理強度階梯、思考型別)。真正的 Anthropic 模型在兩個介面上都保留其規範 ID。 @@ -290,6 +347,8 @@ opencodex 會在**已路由**請求中將該技能內容替換為一個短佔位 查詢順序:發現別名 → 精確 ID → 移除日期字尾的 ID(`-20250514`)→ 透傳。 +拒絕規則請見 [Desktop 別名解析](#desktop-alias-resolution)。 + ## Sidecar 矩陣:Web Search 與圖像理解 不同路由模型擁有的託管工具和圖像能力並不相同。opencodex 會在主模型回答前補齊這些能力: diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index 4166276fbc..f371457be9 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -266,8 +266,12 @@ OpenCodex 直接注入路由,請先將 Codex 切回內建 `openai` provider, 已發現模型。不在 allowlist 中的 id 永遠不會進入目錄。 2. **`disabledModels`(頂層)**:會同時從目錄與 `/v1/models` 隱藏模型,並把裸原生 GPT slug 設為 `visibility: "hide"`。 -3. **`liveModels: false` 且 `models` 為空**:當即時探索關閉,且 `models` 為空或省略時,opencodex - 不會為該 provider 暴露任何路由模型。 +3. **`liveModels: false`** — `liveModels: false` 時,若 `models` 為空或省略,初始列表先加入已設定的 `defaultModel`, + 再加入 `retainModels`,重複 ID 僅保留首次出現的位置。若明確設定了非空 `models`,則按 + `models`、`retainModels` 順序建立,不會自動加入另一個 `defaultModel`;仍可將該模型明確寫入 + `models` 或 `retainModels`。這些欄位均未提供 ID 時,初始列表為空。此順序不保證最終選擇器的顯示順序。 + `selectedModels`、`disabledModels` 與供應商停用規則仍然適用。`authMode: "forward"` 保留原有獨立分支, + 不使用此靜態路由列表。這些規則不改變即時探索失敗時的後備行為。 4. **Cursor `GetUsableModels`**:Cursor adapter 透過 protobuf `GetUsableModels` RPC 探索模型,而不是 `/models`,所以 Cursor 端變更可獨立改變可見 id。 5. **cache 與 `ocx sync`**:即時目錄約快取五分鐘(`modelCacheTtlMs`,預設 `300000`)。執行 diff --git a/docs-site/src/content/docs/zh-tw/guides/integrations.md b/docs-site/src/content/docs/zh-tw/guides/integrations.md index 426cdae162..54751d5620 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -60,6 +60,8 @@ opencodex 從自己的環境讀取這些變數。如果你的 gateway 以 profil **如果某個值無法忠實重寫,開關會拒絕執行。** 往返覆蓋這些格式在實務上會用到的值種類;當它做不到時——例如使用 `inf` 或 `nan` 的 TOML 檔案,我們可用的 parser 無法準確讀回——套用會停止並說明,而不是寫入被改動的值然後宣稱成功。你會看到檔案被指名,磁碟上沒有任何東西被移動。手動編輯那個檔案仍然有效;只有我們的自動重寫會拒絕。 +TOML 日期與時間值也會阻止自動重寫:合併步驟會將這些帶有型別的值轉成加引號的字串,陣列和行內表格中的值也一樣。原本就加引號的日期字串仍受支援;若要保留不加引號的日期型別,請手動編輯設定。 + **Pi、Kimi Code、Gajae Code、MiniMax Code 與受管理 DSH 整合只能對 loopback bind 運作。** 前四者的設定沒有非 loopback bind 所需的 `x-opencodex-api-key` header 欄位。DSH 雖然提供通用 headers map,但 rc.6 並未把這個專用准入 header 記錄為受支援的整合契約,因此受管理 writer 會選擇安全拒絕,而不自行猜測。請改用 SSH tunnel,或由本機 forwarder 加上該 header 後再以 loopback 存取。 **產生的 OMP 整合也刻意只支援 loopback。** OMP 確實支援 provider 層級的 headers,但這個最初的整合不會發出遠端 `x-opencodex-api-key` 憑證連線。手動的遠端 OMP 設定目前不在受管理的整合範圍內。 diff --git a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md index efd505db85..e0946db620 100644 --- a/docs-site/src/content/docs/zh-tw/guides/model-ordering.md +++ b/docs-site/src/content/docs/zh-tw/guides/model-ordering.md @@ -14,6 +14,8 @@ Codex 的 models-manager 按 `priority` 升序排列選擇器中可見的目錄 因此,opencodex 透過分配更低的 priority 控制置頂位置,而不依賴陣列位置。相關 priority 如下: +以下優先級表與範例適用於未啟用完整選擇器排序的情況。 + | 目錄條目 | Priority | 來源 | | --- | ---: | --- | | `subagentModels[i]` | `i`(`0` 至 `4`) | `src/codex/catalog/sync.ts` 中的 featured rank map | @@ -92,10 +94,43 @@ subagentModels = [ ## 更改順序 -自訂開頭模型順序的唯一受支援方式是重新排列 `subagentModels`。你可以在儀表板的 +要調整 `spawn_agent` 候選模型的順序,請重新排列 `subagentModels`。你可以在儀表板的 **Sub-agents** 頁面或 opencodex 設定中修改它。該列表最多接受五個模型,其陣列順序有實際意義。 -目前 `OcxConfig` 中沒有通用的 `modelOrder`、`providerOrder` 或 priority map 設定。受支援的排序 -欄位是 `subagentModels`(`src/types.ts:238-246`);`disabledModels` 和各 provider 的 -`selectedModels` 都是可見性欄位(`src/types.ts:276-282`、`src/types.ts:439-446`)。因此,要更改 -選擇器其餘部分的順序,需要修改程式碼行為,而不是調整設定。 +`modelPickerOrder` 只控制選擇器的顯示順序。如果列表只有路由 ID `/`, +其中未置頂的列會按列表順序進入獨立的顯示區間(`1000 + i`)。未列出的路由列保留原有優先級, +因此仍排在該區間之前。同時列在 `subagentModels` 中的列保留置頂優先級,原生列也維持原有位置。 +需要控制相對順序的路由列都應列入列表。 + +要對整個選擇器排序,請加入至少一個不含 `/` 的裸目錄 ID,例如 `gpt-5.6-sol`。 +空字串或只有空白的項目不會啟用此模式。 + +```json +{ + "modelPickerOrder": ["gpt-5.6-sol", "opencode-go/glm-5.3"] +} +``` + +列出的項目按陣列順序排在最前面,未列出的項目隨後按原有優先級排列。比對使用精確的目錄 ID: +`gpt-5.6-sol` 和 `openai/gpt-5.6-sol` 是不同的列。同一路由 ID 的原始寫法和編碼寫法也可比對, +但精確比對優先於等價比對。空項目和只有空白的項目會被忽略。帳號限定列必須使用包含 selector 的完整 ID。 + +### 遷移提醒:現有列表中的原生 ID + +以前 `modelPickerOrder` 中的裸原生 ID 會被忽略。現在,現有列表只要包含這類 ID,就會啟用 +整個選擇器的排序,包括置頂列。要保留以前只調整路由列的行為,請移除裸 ID。 +未設定、空列表、只有空白項目的列表以及只有路由 ID 的列表都保留原有行為。 + +`modelPickerOrder` 保留 OpenCodex 按原有優先級計算最多五個偏好候選項的規則,供子代理指引使用。 +每個移動列的原有優先級與原生 `priority` 分開儲存;僅改變選擇器順序不得改變這項計算結果。 +它也不會限制以精確模型名稱指定 override 的資格:公佈的列表不是允許清單,既有的驗證、模型、 +effort 與後端限制仍然適用。 + +原生 Codex 按原生 `priority` 排序,從符合條件且在選擇器中可見的模型中取前五個,公佈在 +`spawn_agent` 中。這適用於 V1,以及開放模型 override 的 V2。因此,即使 OpenCodex 的偏好候選項 +不變,原生公佈的五個模型仍可能隨選擇器順序改變。V1 不接收 OpenCodex 注入的偏好模型列表。 +V2 在用戶端目錄狀態允許時,可以額外接收基於原有優先級的 OpenCodex 指引;這些指引不會重排 +原生工具公佈的列表。 + +`disabledModels` 和各供應商的 `selectedModels` 仍是可見性欄位。沒有獨立的 `modelOrder`、 +`providerOrder` 或優先級對應表設定。 diff --git a/docs-site/src/content/docs/zh-tw/guides/model-routing.md b/docs-site/src/content/docs/zh-tw/guides/model-routing.md index a3cb08885f..0a716351ee 100644 --- a/docs-site/src/content/docs/zh-tw/guides/model-routing.md +++ b/docs-site/src/content/docs/zh-tw/guides/model-routing.md @@ -55,9 +55,13 @@ OpenAI 的 bare `gpt-*` 使用單一 `openai` provider。`codexAccountMode` 在 目錄和 `/v1/models` 輸出的模型範圍。 - `provider.disabled: true` 會把該供應商排除在目錄發現之外。顯式 `provider/model` 請求會失敗, `defaultModel` / `models[]` 掃描也會跳過它。 -- `providerContextCaps` 為各供應商設定 Codex 可見的上下文上限。`contextCapValue` 是儀表板共用的值, - 預設為 350,000;但只有 `providerContextCaps` 中列出了供應商時才會生效。上限只能降低已知上下文, - 不會把它調高,也不會改變上游模型的實際限制。 +- `providerContextCaps` 為各供應商設定 Codex 可見的上下文上限。`contextCapValue` 是儀表板的預設值, + 預設為 350,000;僅設定此值不會套用上限,供應商必須列在 `providerContextCaps` 中才會生效。 + 勾選「套用至所有路由供應商」後,修改儀表板值只會更新已啟用的上限;未勾選時,各供應商保留自己的上限。 + 一般已知視窗只能縮小;支援長視窗的原生模型可以擴展到該模型支援的上限,但不會改變上游模型的實際限制。 + 停用上限後,選擇值儲存在 `providerContextCapValues` 中,重新載入後仍保留;再次啟用時恢復該選擇值。 + 停用期間不會將儲存值套用為限制。不帶 `value` 的 `{ "setAll": true }` 會以目前全域值啟用所有 + 已設定供應商的上限,並取代其儲存的選擇值。 ```json { diff --git a/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md b/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md index e6ae93a76e..4aa10fcaa7 100644 --- a/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md +++ b/docs-site/src/content/docs/zh-tw/guides/routing-profile-editor.md @@ -32,6 +32,10 @@ OpenCodex 儀表板中的 **Models → Routing** 分頁可以直接管理 `confi ## 試跑已儲存的設定檔 +候選能力使用套用 registry 覆寫後的有效供應商設定。因此,本地性需求(`localOnly` 與 `remoteAllowed`)會依據實際上游位址判定。若無法分類該位址,則由設定檔的 `unknownEvidence.capability` 決定候選是否合格。 +無法解析的無效供應商設定一律以 `route-unavailable` 排除,即使原則允許未知能力也是如此。 +缺少或停用的供應商也會在評分前以 `route-unavailable` 排除。 + 選取一個已儲存的設定檔,使用 **Dry-run evaluation** 加入請求證據,例如 context-window 大小、工具使用、圖片輸入或結構化輸出。試跑會評估資格與評分,但永遠不會送出上游模型請求。 未儲存的編輯不會被試跑使用。請先儲存設定檔,讓顯示的 revision 與評估參照同一份設定。 diff --git a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md index ccc126a2df..358624f2f9 100644 --- a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md @@ -51,6 +51,14 @@ GUI session 簽發到服務的頁面中,並在到期或代理重啟時靜默 | **Usage / Debug** | 檢視 token usage 覆蓋率與趨勢,或啟用可選的 provider transport 和 usage 提取診斷。 | | **Stop** | 優雅地停止代理和已安裝的後臺服務,恢復原生 Codex 並退出(`POST /api/stop`)。在使用工作排程器後端的 Windows 上,儀表板會拒絕並提示改用 `ocx stop`:工作結束後包裝程序仍可能重新啟動 Proxy,只有執行在 Proxy 之外的 stop 才能在還原用戶端設定前確認這個重啟視窗。被拒絕時不會做任何變更。 | +### 篩選請求日誌 + +Logs 可組合介面、被攔截請求、供應商、完整模型名稱、狀態、時間、速度和對話 ID,篩選目前已載入的日誌。選項包含回退嘗試;模型比對忽略大小寫及頭尾空白,但不做部分比對。日誌中消失的選項恢復為全部。 + +時間範圍為最近 15 分鐘、1 小時或 1 天;Logs 分頁每 30 秒更新一次,即使關閉自動重新整理也會更新。速度按完整請求耗時計算每秒輸出 token,分為小於 15、15 至小於 50、至少 50;啟用速度篩選時排除無測量值的請求。成功為 2xx,錯誤為 4xx/5xx。 + +顯示符合數與已載入總數;重設恢復全部列,並區分無符合結果與空日誌。介面選擇支援方向鍵及 Home/End,不查詢已載入範圍以外的歷史記錄。 + ### 連結到某個部分 佈局只有一種,無需切換。Dashboard 的各個部分都有自己的地址:`#dashboard` 開啟 Overview,`#dashboard/providers` 與 `#dashboard/models` 開啟另外兩個。重新整理、收藏和後退都會保留目前所在的部分。**Logs** 同理,使用 `#logs` 與 `#logs/debug`。舊的 `#providers/workspace` 書籤現在會跳轉到 `#providers`。 @@ -62,6 +70,19 @@ GUI session 簽發到服務的頁面中,並在到期或代理重啟時靜默 **Models** 開關表示 Codex 中的最終可見狀態。路由模型只有在 provider allowlist 中(或未設定 allowlist)且未被停用時才會開啟。開啟模型會原子地協調兩個過濾條件;**全部開啟** 會清除 allowlist,因此以後新發現的模型也會開啟。 +### 在供應商工作區管理模型 + +在供應商的**模型**分頁中,**刪除**會移除已儲存的自訂定義。原有的原生模型或即時探索到的模型可能 +重新顯示,因此模型數量可能維持不變。**隱藏**只改變目錄可見性,不會刪除定義,也不會改變直接路由規則。 +點選**在模型中管理可見性**可開啟**模型**頁面並恢復顯示;即使供應商分頁已沒有任何模型列,也能使用此入口。 + +**新增**會儲存自訂定義,但不會清除既有的隱藏狀態或供應商選擇規則。儲存後的模型可能仍被隱藏。 +如果模型已存在,請在**模型**頁面管理其可見性。已確認儲存時,即使目錄重新整理失敗,定義也已儲存; +請依重新整理提示操作,不要重複新增。若無法確認變更結果,請先重新整理模型狀態,再重試。 + +供應商的模型數量統計伺服器回傳的目前模型清單中未停用的唯一項目,計數在搜尋與顯示數量限制之前進行。 +它不是允許清單的大小或即時探索的模型數量,也不能證明項目來自上游探索。選擇標記與探索資訊和此計數分開顯示。 + ## 委派選擇器與生成路由的區別 Dashboard 的 **Sub-agent delegation** 選擇器會儲存 `injectionModel`,以及可選的 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md index bb377466fd..71d575e774 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md @@ -147,6 +147,10 @@ ocx status --json 將 opencodex 作為登入管理的背景服務執行(macOS **launchd**、Linux **systemd user unit**、Windows **Task Scheduler**),在登入時自動啟動並在崩潰時自動重啟。服務執行時設定 `OCX_SERVICE=1`,使重啟不會折騰 Codex 設定。 +Windows 工作排程器安裝使用一般處理程序優先順序(`Priority=4`)。舊的背景優先順序(`7`,省略時排程器也預設使用 `7`) +可能在 CPU 競爭時延遲健康檢查回應,導致處理程序仍在執行時系統匣顯示 Offline。升級後執行 `ocx service repair`, +即可遷移該註冊優先順序並重新啟動服務;過程中可能需要核准 UAC 提示。已設為一般或高優先順序時,不會僅因優先順序而重新註冊。 + | 子指令 | 動作 | | --- | --- | | 無 | 服務不存在時安裝並啟動;已存在時重新整理並重啟。正常的 Windows 工作排程器定義會沿用;過時的定義可能會重新註冊並需要提高權限。 | 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 87141789a1..b911a174b5 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 @@ -50,7 +50,7 @@ V1 指引僅在 `max` 或 `ultra` 時為主動文字。V2 僅在存在偏好模 Codex 0.146+ 會將角色檔案中的 `model_fallback` 視為未知欄位並略過整個角色;`ocx doctor` 也會對此發出警告。因此新的角色級 fallback 應設定在 opencodex,而不是角色 TOML 中。 -opencodex 會跳過已停用、不可路由、不健康、冷卻中或達到配額閾值的候選項。可用性快取保存 `subagentModelFallbackPollMs`。對於加密的子任務,候選鏈僅包含規範的原生 ChatGPT 目標,以及透過 `allowEncryptedV2AgentTasks: true` 明確信任的直接金鑰驗證 Responses 路由。若無任何目標可處理加密 payload,請求會失敗,而不會將無法讀取的密文路由到別處。組合仍只使用規範的原生目標。 +opencodex 會跳過已停用、不可路由、不健康、冷卻中或達到配額閾值的候選項。可用性快取保存 `subagentModelFallbackPollMs`。對於加密的子任務,候選鏈僅包含規範的原生 ChatGPT 目標,以及透過 `allowEncryptedV2AgentTasks: true` 明確信任的直接金鑰驗證 Responses 路由。若無目標可處理加密 payload,且選用的恢復功能無法支援路由傳送,請求會失敗,不會轉送無法讀取的密文。組合會先嘗試可用的規範原生目標;若沒有可選擇的原生目標或原生嘗試已耗盡,且已啟用 `agentTaskRecovery`,會在路由到組合目標前對加密的 `NEW_TASK` 恢復一次。 ```json { diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md index 7a27de4f61..2a53c4d33a 100644 --- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md @@ -27,8 +27,9 @@ ocx models provider openrouter on | `providers` | `Record` | — | 供應商名稱到供應商設定的映射。 | | `openaiProviderTierVersion?` | `2` | 由遷移設定 | 標記單一選項感知的 OpenAI projection 已完成。 | | `disabledModels?` | `string[]` | — | 對 Codex 目錄與 `/v1/models` 隱藏的模型,但不阻擋直接代理呼叫。路由 id 從清單中移除;裸原生 GPT id 取得 `visibility: "hide"`。 | -| `providerContextCaps?` | `Record` | `{}` | Per-供應商的 Codex 可見 context 上限。上限只會降低已知的 context window。 | -| `contextCapValue?` | `number` | `350000` | 儀表板 context-cap 控制使用的值;變更它會更新每個啟用的 `providerContextCaps` 項目。 | +| `providerContextCaps?` | `Record` | `{}` | 各供應商目前生效的上下文上限。一般視窗只能縮小;支援長視窗的原生模型可以擴展到該模型支援的上限。 | +| `providerContextCapValues?` | `Record` | `{}` | 各供應商最後選擇的上限,停用後仍保留。僅儲存這些值不會啟用上限。生效中的值優先於儲存的選擇值。 | +| `contextCapValue?` | `number` | `350000` | 首次啟用時使用的預設值。再次啟用時恢復該供應商的選擇值。修改全域值時附帶 `setAll: true` 只會更新已啟用的上限;不帶值的 `setAll: true` 會以目前全域值啟用所有已設定供應商的上限。 | | `codexAccounts?` | `CodexAccount[]` | `[]` | 由 Codex Auth 管理的 ChatGPT/Codex 池帳號中繼資料。秘密分別存在 `codex-accounts.json`。 | | `pausedCodexAccountIds?` | `string[]` | `[]` | 被排除於池選擇直到恢復的帳號,包含暫停時的 main `__main__` 帳號。 | | `codexAccountNamespaces?` | `Record` | — | 公開模型選擇器命名空間到已儲存 Codex 帳號目標。這會驗證並持久化映射,但不會自行新增 picker 列或變更路由。 | @@ -63,7 +64,7 @@ ocx models provider openrouter on | `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic 金鑰標頭風格。預設為原生 `x-api-key`;僅對 key-auth `anthropic` 供應商有效。 | | `apiKeyPool?` | `ApiKeyPoolEntry[]` | 多金鑰池。`apiKey` 反映現用項目;每個項目有 `id`、`key`、可選 `label` 與可選數值 `addedAt`。 | | `defaultModel?` | `string` | 在未指定明確模型時選擇此供應商所使用的模型。 | -| `models?` | `string[]` | 播種/後備模型清單。在 `liveModels: false` 時,這些是唯一探索的模型。 | +| `models?` | `string[]` | 初始/後備模型列表。`liveModels: false` 時,非空 `models` 後接 `retainModels`;若 `models` 為空或省略,則按已設定的 `defaultModel`、`retainModels` 順序建立初始列表,重複 ID 僅保留首次出現的位置。 | | `liveModels?` | `boolean` | 在啟動/同步時擷取即時目錄(預設 `true`)。自訂供應商使用 `${baseUrl}/models`;內建可能使用 registry URL 並過濾。 | | `selectedModels?` | `string[]` | 探索後的目錄允許清單。非空時僅暴露那些 id;空或省略時暴露所有探索的模型。 | | `contextWindow?` | `number` | 供應商範圍的 Codex 可見 context 上限。較小的即時中繼資料被保留。 | @@ -324,7 +325,14 @@ Vercel AI Gateway 可在多個底層推論供應商之間路由一個模型。`v ## 靜態模型允許清單 -設定 `liveModels: false` 以僅暴露 `models`。若 `models` 為空或省略,供應商暴露無路由模型。即時探索在快取前拒絕超過 4 MiB 或 2,000 個原始模型列;內建預設可能使用較低限制並過濾到 chat 合格列。過大或格式錯誤的結果遵循過時/設定的後備。有效的零合格結果恆為權威,且不被靜默取代或截斷。 +`liveModels: false` 時,若 `models` 為空或省略,初始列表先加入已設定的 `defaultModel`, +再加入 `retainModels`,重複 ID 僅保留首次出現的位置。若明確設定了非空 `models`,則按 +`models`、`retainModels` 順序建立,不會自動加入另一個 `defaultModel`;仍可將該模型明確寫入 +`models` 或 `retainModels`。這些欄位均未提供 ID 時,初始列表為空。此順序不保證最終選擇器的顯示順序。 +`selectedModels`、`disabledModels` 與供應商停用規則仍然適用。`authMode: "forward"` 保留原有獨立分支, +不使用此靜態路由列表。這些規則不改變即時探索失敗時的後備行為。 + +即時探索在快取前拒絕超過 4 MiB 或 2,000 個原始模型列;內建預設可能使用較低限制並過濾到 chat 合格列。過大或格式錯誤的結果遵循過時/設定的後備。有效的零合格結果恆為權威,且不被靜默取代或截斷。 當探索應仍然執行但只有 selected id 應出現在 Codex 與 `/v1/models` 時,請使用 `selectedModels`。儀表板保留完整的探索清單供日後允許清單變更。 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 e85594740d..4649b51f97 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 @@ -201,3 +201,7 @@ Anthropic OAuth sidecar 重用 opencodex 既有的 Claude Code OAuth 指紋。 `runtimeRole` 預設為 `standalone`。Hub 使用 `hub.managementPublicOrigin`、僅限迴路的 `hub.managementIngress`(缺省為 `enabled:false`)與正確的 `remoteGui.allowedTailscaleUsers`(缺省為空)。用戶端金鑰保存在 `service-api-token` 而不是 `config.json`;輪替期間可能暫時存在 `service-api-token.prev`。用量不會鏡像。 `remoteGui.allowInsecureHttp` 是已棄用的 no-op,只為讓舊的 strict-schema 設定繼續載入而保留。請從設定移除:pairing grant 僅接受 loopback 或已驗證的 HTTPS;設為 `true` 也不會重新開放明文 HTTP pairing。 + +## Codex 配額網路診斷 + +主 Codex 帳戶列中的 `quotaRefresh` 描述配額查詢結果,並不代表剩餘配額或模型存取權限。讀取快取或未執行查詢時,這個欄位可能省略。查詢使用執行中代理服務的環境,而不是目前終端機的環境。未設定 `proxy` 時保留既有環境;`"auto"` 只在啟動時讀取 Windows 靜態代理設定,不會自動處理 PAC/WPAD、僅 SOCKS 的設定或執行中的變更。TUN 測試成功本身不能證明 HTTP 代理路徑正常。命令與狀態說明請見[英文網路診斷章節](/reference/configuration/server/#codex-quota-network-diagnostics)。 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 0d91e4b98d..ca899bc730 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 @@ -144,12 +144,15 @@ Session 簽發在需要 data-plane 認證時停用,這包含遠端綁定。遠 | `GET /api/models` | 回傳儀表板/CLI 模型列 | 收集飽和時 `catalog_busy` | | `GET /api/client-config?client=...` | 為 `opencode`、`pi`、`omp`、`hermes`、`openclaw`、`kimi`、`gajae` 或 `dsh` 建構唯讀客戶端設定 | 400 不支援客戶端;503 目錄不可用 | | `PUT /api/disabled-models` | 取代共享的 disabled-model 清單 | 400 無效 JSON | -| `PUT /api/model-visibility` | 原子地變更供應商或模型層級可見性 | 400 無效供應商、scope、目標或 body | +| `PUT /api/model-visibility` | 原子地變更供應商或模型層級可見性 | 400 無效供應商、scope、目標或 body; 409 `initial_model_selection_pending` (重新整理模型清單後再試。) | | `GET, POST /api/custom-models` | 列出自訂模型或新增一個 | 400 無效欄位;404 供應商缺失;409 重複模型 | | `PUT, DELETE /api/custom-models/{id}` | 編輯或刪除一個自訂模型 | 400 無效 id/欄位;404 未找到;409 重複模型 | | `GET, PUT /api/selected-models` | 讀取供應商允許清單與可用性,或取代一個允許清單 | 400 缺失供應商/body;404 未知供應商; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | 讀取預設資訊或選擇 preset/all/custom 模式 | 400 模式無效或不支援該預設;404 未知供應商; PUT 409 `initial_model_selection_pending` | +手動模型會取代 Models 儀表板中 provider 與 model ID 相同的列。OpenAI 手動列保留 `openai/`,並支援可見性控制。刪除手動列後,不含帳戶限定符的原生列會恢復。含帳戶限定符的原生列仍獨立保留。原生路由與帳戶權限不變。非原生 OpenAI 可見性目標必須符合已設定的手動模型。 + + 尚未確認可靠的初始模型清單時,有效的 `PUT /api/selected-models` 和 `PUT /api/model-presets` 請求也會回傳 HTTP 409 和代碼 `initial_model_selection_pending`。請使用 `GET /api/models` 等方式更新模型清單,成功後再重試。 ### OAuth 帳號、供應商金鑰與 data-plane 金鑰 @@ -188,6 +191,14 @@ Session 簽發在需要 data-plane 認證時停用,這包含遠端綁定。遠 | `GET, PUT /api/provider-context-caps` | 讀取或更新全域、所有供應商或單一供應商的 context 上限 | 400 無效請求;404 未知供應商 | | `GET /api/provider-presets` | 回傳從 runtime registry 衍生的 GUI 供應商預設 | — | +上下文上限回應包含 `caps`(目前生效的上限)和 `values`(停用後仍保留的最後選擇值)。 +啟用供應商的上限時,若未指定 `value`,便會恢復其選擇值;首次啟用時使用全域 `contextCapValue`。 +OpenAI 也遵循此規則:開關不會選擇特殊的 922k 模式。生效中的上限會限制每個原生視窗;支援長上下文 +的模型只能擴展到該模型支援的上限。 +`{ "value": 600000, "setAll": true }` 會修改全域值,並且只更新已啟用的上限;上限已停用的供應商會 +保留自己的選擇值,供之後啟用時恢復。不帶 `value` 的 `{ "setAll": true }` 會以目前全域值啟用所有 +已設定供應商的上限,並取代儲存的選擇值。停用不會清除選擇值,重新載入後仍保留,但不會將其套用為限制。 + `provider_has_dependent_combos` 是安全屏障:在刪除其供應商前,先移除或編輯相依的組合。 ### 側邊欄與同意約束動作 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 e77a327776..a4baa921c9 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 @@ -22,7 +22,7 @@ Responses 表示是橋接的中心。原生相容的路由可跳過部分轉譯 | OpenAI Chat Completions | `POST /v1/chat/completions` | `chat.completion` JSON | `chat.completion.chunk` SSE,以 `[DONE]` 結束 | | Anthropic Messages | `POST /v1/messages` | Anthropic `message` JSON | Anthropic Messages SSE | | Anthropic token 計數 | `POST /v1/messages/count_tokens` | `{ "input_tokens": number }` | 不適用 | -| 模型探索 | `GET /v1/models` | 三種目錄契約之一 | 不適用 | +| 模型探索 | `GET /v1/models` | 目錄或明確指定的 Desktop 快照 | 不適用 | | 語音與 Realtime | `POST /v1/live`, `POST /v1/realtime/calls` | 中繼的 call-creation 回應 | 一個獨立的 sideband WebSocket 雙向中繼 frame | | Responses compaction | `POST /v1/responses/compact` | 取代歷史 JSON | 不適用 | @@ -150,9 +150,15 @@ Responses 表示是橋接的中心。原生相容的路由可跳過部分轉譯 { "input_tokens": 123 } ``` +無法解析的日期型 Desktop ID 也可能是探索結果中缺少的真實原生模型 ID。現有資訊不足以 +解析該 ID 時,Messages 和 count-tokens 回傳 HTTP 503 及固定錯誤 `desktop_model_mapping_unavailable`;這不代表 +模型無效。未知的舊版雜湊別名仍回傳 HTTP 400。兩種情況都不會移除日期或回退到其他路由。 +已知 ID、已註冊映射、精確 `modelMap` 匹配及已識別的真實原生 ID 維持原有處理方式。 +請重新整理模型探索或重新套用已連接 hub 的設定後再試;僅重試本身不能保證解決。 + ## `GET /v1/models` -相同路由服務三個期待不相容目錄封裝的客戶端。除非也存在 `client_version`,否則 Anthropic flavor 勝出。 +未指定 `format=desktop-config` 時,使用以下一般目錄契約: | 契約 | 觸發 | 頂層結構 | 模型 id 行為 | | --- | --- | --- | --- | @@ -160,6 +166,25 @@ Responses 表示是橋接的中心。原生相容的路由可跳過部分轉譯 | Codex 目錄 | `client_version` query 參數 | `{ "models": [...] }` | 原生與路由項目帶有更豐富的 Codex 目錄欄位、可見性、effort、WebSocket 與多代理中繼資料 | | 普通 OpenAI 清單 | 無觸發 | `{ "object": "list", "data": [...] }` | 可見的原生 id 為裸 id;路由 id 為別名或 `provider/model` | +### Desktop 設定快照 + +`GET /v1/models?ids=desktop&format=desktop-config` 明確選擇 Desktop 快照,不依賴 +user-agent。回應為 `{ "version": 1, "models": [...] }`,帶有 `Cache-Control: no-store`。 +客戶端送出 `Accept: application/json`、`anthropic-version: 2023-06-01` 及現有資料存取憑證; +不需要管理員權杖,也不上傳設定檔。項目是 hub 發出的 Desktop 設定模型,不是 Codex 目錄列。 + +此格式與 `ids=cli` 或任何 `client_version` 一起使用時回傳 HTTP 400。未指定格式時,上述一般 +契約維持不變。Claude 關閉時回傳 `{ "version": 1, "models": [] }`;已連接的 Desktop apply +會視為無法使用,不寫入替代設定。回傳一般目錄而非版本 1 的舊 hub 不受支援,客戶端不會改用 +本機產生的 ID。 + +快照仍是唯讀模型清單,不是金鑰輪換或設定檔上傳 API。Desktop 金鑰移轉、復原與中斷由既有 +客戶端連線流程處理。輪換保留模型項目和選擇;CLI 的 `rotation` 區分 `committed` 與 +`rolled_back`。中斷會還原管理設定,或對已確認的舊設定檔回報標準回退,同時保留使用者欄位和 +後來有效的選擇。衝突或未完成的復原不會標為完成。需要重新啟動 Desktop 才會讀取磁碟變更; +中斷不會自動撤銷 hub 金鑰。參見 [Desktop 指南](/zh-tw/guides/claude-code/)。 +thinking 重播與提示快取仍由獨立的 [#3719](https://github.com/lidge-jun/opencodex/issues/3719) 跟進。 + ## `POST /v1/live` 與 Realtime sideband `POST /v1/live` 接受 ChatGPT/Codex App Frameless call-creation 介面。 diff --git a/docs/pr-assets/dashboard-settings-aligned.jpg b/docs/pr-assets/dashboard-settings-aligned.jpg new file mode 100644 index 0000000000..8cf7c1f26f Binary files /dev/null and b/docs/pr-assets/dashboard-settings-aligned.jpg differ diff --git a/gui/src/App.tsx b/gui/src/App.tsx index c7fb89b6e2..91890ce664 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -265,7 +265,7 @@ export default function App() { > opencodex - v{displayedVersion} + v{displayedVersion} ); diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index 645511f69d..d6065e0298 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -13,6 +13,7 @@ import { ProviderIcon } from "./ProviderRail"; import { Switch } from "../../ui"; import { IconChevron, IconTrash } from "../../icons"; import ProviderOverview from "./ProviderOverview"; +import type { ModelRow } from "../../pages/models-shared"; import ProviderModels from "./ProviderModels"; import ProviderUsage from "./ProviderUsage"; import ProviderAuthPanel from "./ProviderAuthPanel"; @@ -32,6 +33,10 @@ export default function ProviderDetails({ availableModels, hasLiveModels, selectedModels, + modelRows, + modelRevision, + modelRowsReady, + onOpenModels, modelsLoading, modelsLoadFailed, onRetryModels, @@ -65,6 +70,10 @@ export default function ProviderDetails({ /** Server-reported live-catalog provenance; see filterModels(). */ hasLiveModels: boolean; selectedModels: string[]; + modelRows: ModelRow[] | null; + modelRevision: string; + modelRowsReady: boolean; + onOpenModels: () => void; modelsLoading?: boolean; modelsLoadFailed?: boolean; onRetryModels?: () => void; @@ -293,6 +302,10 @@ export default function ProviderDetails({ availableModels={availableModels} hasLiveModels={hasLiveModels} selectedModels={selectedModels} + modelRows={modelRows} + modelRevision={modelRevision} + modelRowsReady={modelRowsReady} + onOpenModels={onOpenModels} modelsLoading={modelsLoading} modelsLoadFailed={modelsLoadFailed} needsReauth={ diff --git a/gui/src/components/provider-workspace/ProviderModelChip.tsx b/gui/src/components/provider-workspace/ProviderModelChip.tsx new file mode 100644 index 0000000000..9ab2e66739 --- /dev/null +++ b/gui/src/components/provider-workspace/ProviderModelChip.tsx @@ -0,0 +1,37 @@ +import type { MouseEvent } from "react"; +import type { ModelRow } from "../../pages/models-shared"; +import { useT } from "../../i18n/shared"; +import { IconEyeOff, IconTrash } from "../../icons"; + +/** Presentation only: ProviderModels owns identity, readiness and all mutations. */ +export default function ProviderModelChip({ row, disambiguate, copied, isDefault, selected, action, disabled, onCopy, onRemove }: { + row: ModelRow; + disambiguate: boolean; + copied: boolean; + isDefault: boolean; + selected: boolean; + action: "delete" | "hide" | null; + disabled: boolean; + onCopy: () => void; + onRemove: (button: HTMLButtonElement) => void; +}) { + const t = useT(); + const label = t(action === "delete" ? "models.customDelete" : "models.hide"); + return ( +
  • + + {isDefault && {t("prov.defaultBadge")}} + {selected && {t("pws.selected")}} + {action && } +
  • + ); +} diff --git a/gui/src/components/provider-workspace/ProviderModels.tsx b/gui/src/components/provider-workspace/ProviderModels.tsx index 56cc588292..bb158ed780 100644 --- a/gui/src/components/provider-workspace/ProviderModels.tsx +++ b/gui/src/components/provider-workspace/ProviderModels.tsx @@ -1,276 +1,262 @@ -/** - * ProviderModels — the models tab: searchable wrapping model chips with - * default/selected flags and copy-to-clipboard ids. Uses a wrap layout so - * short lists fill horizontal space instead of a tall single-column stack. - */ -import { useEffect, useMemo, useRef, useState } from "react"; +/** Canonical inventory and revision-bound custom-definition operations for one provider. */ +import { useEffect, useRef, useState } from "react"; import { useT } from "../../i18n/shared"; import type { WorkspaceItem } from "../../provider-workspace/catalog"; -import { filterModels } from "../../provider-workspace/report"; +import type { ModelRow } from "../../pages/models-shared"; +import { putModelVisibility } from "../../model-visibility"; +import { readJsonOrThrow } from "../../fetch-json"; +import { createBoundedFetch } from "../../bounded-fetch"; +import { catalogRefreshPending, parseCustomModelCreated, parseCustomModelInventory, type CustomModelRecord } from "../../provider-workspace/model-inventory"; import { encodedModelIdCollides } from "../../../../src/providers/slug-codec"; +import ProviderModelChip from "./ProviderModelChip"; -export default function ProviderModels({ - item, - apiBase, - availableModels, - hasLiveModels, - selectedModels, - modelsLoading = false, - modelsLoadFailed = false, - needsReauth = false, - onRetryModels, - onOpenAccounts, -}: { +type Mutation = { + revision: string; + outcome: "saved" | "deleted" | "hidden" | "unconfirmed" | "rejected"; + refreshPending: boolean; + created?: CustomModelRecord; +}; +const CHIP_RENDER_CAP = 300; + +type ProviderModelsProps = { item: WorkspaceItem; apiBase: string; availableModels: string[]; - selectedModels: string[]; - /** Server-reported: did the last successful discovery return any rows? */ hasLiveModels: boolean; + selectedModels: string[]; + modelRows: ModelRow[] | null; + modelRevision: string; + modelRowsReady: boolean; modelsLoading?: boolean; modelsLoadFailed?: boolean; - /** Active OAuth account needs a fresh login before live discovery works. */ needsReauth?: boolean; onRetryModels?: () => void; onOpenAccounts?: () => void; -}) { + onOpenModels: () => void; +}; + +export default function ProviderModels(props: ProviderModelsProps) { + return ; +} + +function ProviderModelInventory({ item, apiBase, availableModels, selectedModels, + modelRows, modelRevision, modelRowsReady, modelsLoading = false, modelsLoadFailed = false, + needsReauth = false, onRetryModels, onOpenAccounts, onOpenModels, +}: ProviderModelsProps) { const t = useT(); const [query, setQuery] = useState(""); - const [customModelId, setCustomModelId] = useState(""); - const [customSaving, setCustomSaving] = useState(false); - const [customError, setCustomError] = useState(""); - const [customSuccess, setCustomSuccess] = useState(""); - const [customModelIds, setCustomModelIds] = useState([]); - const [customModelsReady, setCustomModelsReady] = useState(false); - const [customModelsLoadFailed, setCustomModelsLoadFailed] = useState(false); - const [customModelsLoadEpoch, setCustomModelsLoadEpoch] = useState(0); + const [draft, setDraft] = useState(""); + const [ownershipEpoch, setOwnershipEpoch] = useState(0); + const ownershipKey = JSON.stringify([apiBase, item.name, modelRevision, ownershipEpoch]); + const [ownership, setOwnership] = useState<{ key: string; rows: CustomModelRecord[] } | null>(null); + const [ownershipError, setOwnershipError] = useState(null); + const [requestPending, setRequestPending] = useState(false); + const [mutation, setMutation] = useState(null); const [copiedId, setCopiedId] = useState(null); - const copyResetRef = useRef(null); - const selectedSet = useMemo(() => new Set(selectedModels), [selectedModels]); - const configuredModels = useMemo(() => item.models ?? [], [item.models]); - const trimmedCustomModelId = customModelId.trim(); - const knownModelIds = [ - ...availableModels, - ...customModelIds, - ...configuredModels, - ...(item.defaultModel ? [item.defaultModel] : []), - ]; - const customModelInvalid = !customModelsReady - || !trimmedCustomModelId - || availableModels.includes(trimmedCustomModelId) - || customModelIds.includes(trimmedCustomModelId) - || configuredModels.includes(trimmedCustomModelId) - || item.defaultModel === trimmedCustomModelId - || encodedModelIdCollides(trimmedCustomModelId, knownModelIds); - const models = useMemo( - () => filterModels(availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels), - [availableModels, item.defaultModel, query, configuredModels, customModelIds, hasLiveModels], - ); + const copyReset = useRef | null>(null); + const flight = useRef(false); + const active = useRef(true); + const currentRevision = useRef(modelRevision); + const searchRef = useRef(null); + const recoveryRef = useRef(null); + const focusIntent = useRef<{ button: HTMLButtonElement; retained: boolean } | null>(null); + const ownershipReady = ownership?.key === ownershipKey && ownershipError !== ownershipKey; + const ready = modelRows !== null && modelRowsReady && !modelsLoading && !modelsLoadFailed && ownershipReady; + const reconciled = ready && (!mutation || modelRevision !== mutation.revision); + const busy = requestPending || (!!mutation && !reconciled); + const rows = (modelRows ?? []).filter(row => row.provider === item.name); + const pendingSelection = rows.some(row => row.initialSelectionPending); + const actionsBlocked = !ready || busy || pendingSelection; + const customModels = ownership?.rows.filter(row => row.provider === item.name) ?? []; + const selectedSet = new Set(selectedModels); + // Full raw inputs retain duplicate/collision protection. Native-only DTO ids are not definitions. + const known = [...availableModels, ...(item.models ?? []), ...customModels.map(row => row.modelId), ...(item.defaultModel ? [item.defaultModel] : [])]; + const modelId = draft.trim(); + const duplicate = !!modelId && (known.includes(modelId) || encodedModelIdCollides(modelId, known)); + const visible = rows.filter(row => !row.disabled); + const normalizedQuery = query.trim().toLowerCase(); + const filtered = visible.filter(row => [row.id, row.namespaced].some(value => value.toLowerCase().includes(normalizedQuery))); + const labels = new Map(); + for (const row of visible) labels.set(row.id, (labels.get(row.id) ?? 0) + 1); + useEffect(() => { currentRevision.current = modelRevision; }, [modelRevision]); useEffect(() => { - let active = true; - const load = async () => { - try { - const response = await fetch(`${apiBase}/api/custom-models`); - if (!response.ok) throw new Error(); - const rows: unknown = await response.json(); - if (!Array.isArray(rows)) throw new Error("Invalid custom model list"); - if (!active) return; - setCustomModelIds(rows.flatMap(row => { - if (!row || typeof row !== "object") return []; - const model = row as { provider?: unknown; modelId?: unknown }; - return model.provider === item.name && typeof model.modelId === "string" ? [model.modelId] : []; - })); - setCustomModelsLoadFailed(false); - setCustomError(""); - setCustomModelsReady(true); - } catch { - if (!active) return; - setCustomModelIds([]); - // Without this the component stays permanently unable to add a model: `customModelsReady` - // never flips back and the effect has no trigger left, so a single transient GET failure - // disabled Add until the whole panel remounted. - setCustomModelsReady(false); - setCustomModelsLoadFailed(true); - setCustomError(t("models.networkError")); + active.current = true; + const onFocus = (event: FocusEvent) => { + if (focusIntent.current && event.target !== focusIntent.current.button && event.target !== document.body) { + focusIntent.current.retained = false; } }; - void load(); - return () => { active = false; }; - }, [apiBase, item.name, t, customModelsLoadEpoch]); + document.addEventListener("focusin", onFocus); + return () => { + active.current = false; + document.removeEventListener("focusin", onFocus); + if (copyReset.current !== null) clearTimeout(copyReset.current); + }; + }, []); - const retryCustomModels = () => { - setCustomModelsReady(false); - setCustomModelsLoadFailed(false); - setCustomError(""); - setCustomModelsLoadEpoch(epoch => epoch + 1); - }; + useEffect(() => { + let cancelled = false; + const bounded = createBoundedFetch(20_000); + void fetch(`${apiBase}/api/custom-models`, { signal: bounded.signal }) + .then(readJsonOrThrow).then(parseCustomModelInventory) + .then(records => { + if (cancelled) return; + setOwnership({ key: ownershipKey, rows: records }); + setOwnershipError(null); + }).catch(() => { if (!cancelled) setOwnershipError(ownershipKey); }) + .finally(() => bounded.clear()); + return () => { cancelled = true; bounded.controller.abort(); bounded.clear(); }; + }, [apiBase, ownershipKey]); - useEffect(() => () => { - if (copyResetRef.current != null) window.clearTimeout(copyResetRef.current); - }, []); + useEffect(() => { + if (!requestPending && reconciled) flight.current = false; + const focus = focusIntent.current; + if (focus && !focus.button.isConnected) { + if (focus.retained && document.activeElement === document.body) (searchRef.current ?? recoveryRef.current)?.focus(); + focusIntent.current = null; + } + }, [requestPending, reconciled, modelRows]); - const copyModelId = async (modelId: string) => { + const retry = () => { + setOwnershipEpoch(epoch => epoch + 1); + onRetryModels?.(); + }; + const copyModel = async (row: ModelRow) => { try { - await navigator.clipboard.writeText(modelId); - setCopiedId(modelId); - if (copyResetRef.current != null) window.clearTimeout(copyResetRef.current); - copyResetRef.current = window.setTimeout(() => { - setCopiedId(prev => (prev === modelId ? null : prev)); - copyResetRef.current = null; - }, 1200); - } catch { - /* ignore clipboard failures */ - } + await navigator.clipboard.writeText((labels.get(row.id) ?? 0) > 1 ? row.namespaced : row.id); + if (!active.current) return; + setCopiedId(row.namespaced); + if (copyReset.current !== null) clearTimeout(copyReset.current); + copyReset.current = setTimeout(() => setCopiedId(null), 1200); + } catch { /* Clipboard availability does not affect inventory authority. */ } + }; + const owns = (row: ModelRow) => row.custom === true && row.native !== true && customModels.some(custom => + custom.id === row.customId && custom.provider === row.provider && custom.modelId === row.id); + const actionFor = (row: ModelRow): "delete" | "hide" | null => { + if (!ready || row.initialSelectionPending) return null; + if (row.custom) return owns(row) ? "delete" : null; + // A new stored override with an old DTO is not authority to hide the old representation. + if (row.native !== true && customModels.some(custom => custom.modelId === row.id)) return null; + return "hide"; + }; + const finish = (result: Omit) => { + // A completed write still invalidates the parent if its provider tab was closed meanwhile. + if (!active.current) { onRetryModels?.(); return; } + // Reconciliation must observe a revision started AFTER the response, not a concurrent old read. + setMutation({ ...result, revision: currentRevision.current }); + setRequestPending(false); + retry(); }; const addCustomModel = async () => { - if (customModelInvalid || customSaving) return; - setCustomSaving(true); - setCustomError(""); - setCustomSuccess(""); + if (actionsBlocked || flight.current || !modelId || duplicate) return; + flight.current = true; + setRequestPending(true); + setMutation(null); + const bounded = createBoundedFetch(60_000); + let result: Omit = { outcome: "unconfirmed", refreshPending: false }; try { const response = await fetch(`${apiBase}/api/custom-models`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: item.name, modelId: trimmedCustomModelId }), + method: "POST", signal: bounded.signal, headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider: item.name, modelId }), }); + if (response.status === 201) { + const body = await readJsonOrThrow(response); + const created = parseCustomModelCreated(body, item.name, modelId); + if (ownership?.rows.some(row => row.id === created.id)) throw new Error("Reused custom identity"); + result = { outcome: "saved", created, refreshPending: catalogRefreshPending(body) }; + if (active.current) setDraft(""); + } else if (response.status >= 400 && response.status < 500) { + result = { outcome: "rejected", refreshPending: false }; + } + } catch { /* Transport/invalid acknowledgement cannot prove a rollback. */ } + finally { bounded.clear(); finish(result); } + }; + + const removeModel = async (row: ModelRow, button: HTMLButtonElement) => { + const action = actionFor(row); + if (actionsBlocked || flight.current || !action) return; + if (!window.confirm(t(action === "delete" ? "models.customDeleteConfirm" : "models.hideConfirm", { name: row.namespaced }))) return; + flight.current = true; + setRequestPending(true); + setMutation(null); + focusIntent.current = { button, retained: document.activeElement === button }; + const bounded = createBoundedFetch(60_000); + let result: Omit = { outcome: "unconfirmed", refreshPending: false }; + try { + const response = action === "delete" + ? await fetch(`${apiBase}/api/custom-models/${encodeURIComponent(row.customId!)}`, { method: "DELETE", signal: bounded.signal }) + : await putModelVisibility(apiBase, "models", row.provider, [{ id: row.id, native: row.native === true }], false, + (input, init) => fetch(input, { ...init, signal: bounded.signal })); if (response.ok) { - setCustomModelIds(ids => ids.includes(trimmedCustomModelId) ? ids : [...ids, trimmedCustomModelId]); - setCustomModelId(""); - setCustomSuccess(t("models.customAdded")); - onRetryModels?.(); - } else { - setCustomError(t("models.customSaveFailed")); + const body = await readJsonOrThrow(response); + if (body && typeof body === "object" && !Array.isArray(body) && "ok" in body && body.ok === true) { + result = { outcome: action === "delete" ? "deleted" : "hidden", refreshPending: catalogRefreshPending(body) }; + } + } else if (response.status >= 400 && response.status < 500) { + result = { outcome: "rejected", refreshPending: false }; } - } catch { - setCustomError(t("models.networkError")); - } finally { - setCustomSaving(false); - } + } catch { /* Re-read both resources even when the write acknowledgement was lost. */ } + finally { bounded.clear(); finish(result); } }; - const emptyBase = availableModels.length === 0 - && configuredModels.length === 0 - && customModelIds.length === 0 - && !item.defaultModel; - const showingConfiguredFallback = availableModels.length === 0 && configuredModels.length > 0; - // Aggregators (OpenRouter etc.) can return thousands of ids; capping the mounted - // chips keeps the tab responsive. Filtering narrows the list, so the cap only - // bites on the unfiltered full catalog. - const CHIP_RENDER_CAP = 300; - const capped = models.length > CHIP_RENDER_CAP; - const visibleModels = capped ? models.slice(0, CHIP_RENDER_CAP) : models; + const savedHidden = mutation?.created && reconciled && rows.some(row => row.customId === mutation.created?.id && row.disabled); + const refreshFailed = mutation && (mutation.refreshPending || modelsLoadFailed || ownershipError === ownershipKey); + const feedback = !mutation ? null + : mutation.outcome === "unconfirmed" ? t("pws.modelMutationUnconfirmed") + : mutation.outcome === "rejected" ? t("models.customSaveFailed") + : mutation.outcome === "saved" ? t(refreshFailed ? "pws.modelSavedRefreshPending" : savedHidden ? "pws.modelSavedHidden" : "pws.modelSaved") + : refreshFailed ? t("pws.modelRemovedRefreshPending") + : t(mutation.outcome === "deleted" ? "pws.modelDefinitionDeleted" : "pws.modelHidden"); return (

    {t("pws.tab.models")}

    - {models.length > 0 && ( - {t("pws.modelsAvailable", { count: models.length })} - )} + {modelRows !== null && {t("pws.modelsAvailable", { count: visible.length })}} +
    +
    +
    - {needsReauth && ( -
    - {t("pws.modelsNeedsReauth")} - {onOpenAccounts && ( - - )} -
    - )} - {showingConfiguredFallback && !needsReauth && ( -

    {t("pws.modelsConfiguredFallback")}

    - )} - + {needsReauth &&
    + {t("pws.modelsNeedsReauth")} + {onOpenAccounts && } +
    } + {modelRowsReady && availableModels.length === 0 && visible.length > 0 && (item.models?.length ?? 0) > 0 && !needsReauth && +

    {t("pws.modelsConfiguredFallback")}

    } + {pendingSelection &&

    {t("pws.modelSelectionPending")}

    } +
    - setCustomModelId(event.target.value)} - onKeyDown={event => { if (event.key === "Enter") void addCustomModel(); }} - placeholder={t("models.customFieldModelIdPlaceholder")} - aria-label={t("models.customAdd")} - disabled={customSaving} - /> - + setDraft(event.target.value)} onKeyDown={event => { if (event.key === "Enter") void addCustomModel(); }} + placeholder={t("models.customFieldModelIdPlaceholder")} aria-label={t("models.customAdd")} disabled={requestPending} /> +
    - {customSuccess &&

    {customSuccess}

    } - {customError && ( -

    - {customError} - {customModelsLoadFailed && ( - - )} -

    - )} - {!emptyBase && ( - setQuery(e.target.value)} - aria-label={t("pws.modelSearchPlaceholder")} - /> - )} - {modelsLoading && emptyBase ? ( -

    {t("pws.modelsLoading")}

    - ) : modelsLoadFailed && emptyBase ? ( -
    - {t("pws.modelsLoadFailed")} - {onRetryModels && ( - - )} -
    - ) : emptyBase ? ( -

    {t("pws.noModels")}

    - ) : models.length === 0 ? ( -

    {t("pws.noModelMatch")}

    - ) : ( -
      - {visibleModels.map(modelId => { - const isDefault = modelId === item.defaultModel; - const isSelected = selectedSet.has(modelId); - const copied = copiedId === modelId; - return ( -
    • - - {isDefault ? {t("prov.defaultBadge")} : null} - {isSelected ? {t("pws.selected")} : null} -
    • - ); - })} -
    - )} - {capped && ( -

    - {t("pws.modelsTruncated", { shown: String(CHIP_RENDER_CAP), total: String(models.length) })} -

    - )} + {duplicate &&

    {t("pws.modelKnown")}

    } + {feedback &&

    {feedback}

    } + {savedHidden && refreshFailed &&

    {t("pws.modelSavedHidden")}

    } + {mutation?.refreshPending &&

    {t("codexAuth.catalogRefreshPending")}

    } + {(modelsLoadFailed || ownershipError === ownershipKey) ?
    + {t(modelsLoadFailed ? "pws.modelsLoadFailed" : "pws.modelOwnershipFailed")} + +
    : (!ready || busy) &&

    {t(!modelRowsReady || modelsLoading ? "pws.modelsLoading" : "pws.modelOwnershipLoading")}

    } + {mutation && (mutation.outcome === "unconfirmed" || mutation.refreshPending) && !modelsLoadFailed && ownershipError !== ownershipKey && + } + setQuery(event.target.value)} aria-label={t("pws.modelSearchPlaceholder")} /> + {modelRows !== null && visible.length === 0 ?

    {t("pws.noModels")}

    + : filtered.length === 0 && modelRows !== null ?

    {t("pws.noModelMatch")}

    + :
      {filtered.slice(0, CHIP_RENDER_CAP).map(row => 1} copied={copiedId === row.namespaced} + isDefault={row.id === item.defaultModel} selected={row.native !== true && selectedSet.has(row.id)} + action={actionFor(row)} disabled={actionsBlocked} onCopy={() => { void copyModel(row); }} + onRemove={button => { void removeModel(row, button); }} />)}
    } + {filtered.length > CHIP_RENDER_CAP &&

    + {t("pws.modelsTruncated", { shown: String(CHIP_RENDER_CAP), total: String(filtered.length) })} +

    }
    ); } diff --git a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx index 91faecb6fc..73f0aad616 100644 --- a/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx +++ b/gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx @@ -24,7 +24,7 @@ import { import { providerKind } from "../../provider-workspace/kind"; import { readJsonIfOk, readJsonOrThrow } from "../../fetch-json"; import { readSessionListCache, writeSessionListCache } from "../../session-list-cache"; -import { buildProviderModelUsage, buildProviderUsageTotals, countAvailableModels, parseAvailableModels, parseLiveModelCounts, parseSelectedModels, type ProviderAvailableModels, type ProviderLiveModelCounts, type ProviderModelCounts, type ProviderSelectedModels } from "../../provider-workspace/usage"; +import { buildProviderModelUsage, buildProviderUsageTotals } from "../../provider-workspace/usage"; import { freshQuotaReportRecord, freshQuotaReportsFromResponse, @@ -36,6 +36,9 @@ import type { PricingFilter, ProviderModelUsageRow, ProviderUsageTotals, StatusF import ProviderOverviewDashboard from "./ProviderOverviewDashboard"; import ProviderJsonEditor, { type JsonEditorState } from "./ProviderJsonEditor"; +import type { ModelRow } from "../../pages/models-shared"; +import { parseModelInventory, countModelInventory, parseModelSelection } from "../../provider-workspace/model-inventory"; + export type AddProviderIntent = { tier?: "accounts" | "free" | "paid"; custom?: boolean }; /** Detail-slot data plumbed per selected provider (props-down; no shared hook). */ @@ -47,6 +50,9 @@ export interface DetailSlotData { /** Did the last successful discovery return rows? Server-reported, never inferred. */ hasLiveModels: boolean; selectedModels: string[]; + modelRows: ModelRow[] | null; + modelRevision: string; + modelRowsReady: boolean; modelsLoading: boolean; modelsLoadFailed: boolean; onRetryModels?: () => void; @@ -137,10 +143,9 @@ export default function ProviderWorkspaceShell({ const [sortMode, setSortMode] = useState("az"); const [filterOpen, setFilterOpen] = useState(false); const [railFocusName, setRailFocusName] = useState(null); - const [modelCounts, setModelCounts] = useState({}); - const [availableModels, setAvailableModels] = useState({}); - const [liveModelCounts, setLiveModelCounts] = useState({}); - const [selectedModels, setSelectedModels] = useState({}); + const [modelSnapshot, setModelSnapshot] = useState<{ + revision: string; rows: ModelRow[]; selection: ReturnType; + } | null>(null); const [modelsLoading, setModelsLoading] = useState(false); const [modelsLoadFailed, setModelsLoadFailed] = useState(false); const quotasCacheKey = `ocx.providers.quotas.v1:${apiBase}`; @@ -160,6 +165,11 @@ export default function ProviderWorkspaceShell({ return !cached || Object.keys(cached).length === 0; }); const [modelsLoadEpoch, setModelsLoadEpoch] = useState(0); + const modelRevision = JSON.stringify([apiBase, modelsRefreshToken, modelsLoadEpoch]); + const modelRowsReady = modelSnapshot?.revision === modelRevision && !modelsLoading && !modelsLoadFailed; + const modelCounts = useMemo(() => countModelInventory(modelSnapshot?.rows ?? []), [modelSnapshot]); + const modelsSettled = useRef(onModelsSettled); + useEffect(() => { modelsSettled.current = onModelsSettled; }, [onModelsSettled]); const filterWrapRef = useRef(null); // Shared usage-summary key: all four subscribers raise the deadline together (30d usage is ~5s cold). const usageResource = useKeyedClientResource(usageSummary30dResourceKey(apiBase), [apiBase], async (signal) => { const res = await fetch(apiBase + "/api/usage?range=30d", { signal }); if (!res.ok) throw new Error(String(res.status)); return await res.json(); }, { deadlineMs: 60_000 }); @@ -170,40 +180,47 @@ export default function ProviderWorkspaceShell({ }, [providers, activeAccountNeedsReauth]); const retryModels = useCallback(() => { + setModelsLoading(true); + setModelsLoadFailed(false); setModelsLoadEpoch(epoch => epoch + 1); }, []); useEffect(() => { - // Deferred load (matches Models/Usage/ClaudeCode): avoids synchronous setState - // inside the effect, per the react-hooks/set-state-in-effect lint gate. let cancelled = false; + const bounded = createBoundedFetch(60_000); const timeout = window.setTimeout(() => { setModelsLoading(true); void (async () => { let succeeded = false; try { - const res = await fetch(`${apiBase}/api/selected-models`); - const data = await readJsonOrThrow(res); + // Adopt this pair together. The server does not promise a transaction across reads. + const [selection, rows] = await Promise.all([ + fetch(`${apiBase}/api/selected-models`, { signal: bounded.signal }) + .then(readJsonOrThrow).then(parseModelSelection), + fetch(`${apiBase}/api/models`, { signal: bounded.signal }) + .then(readJsonOrThrow).then(parseModelInventory), + ]); if (cancelled) return; - setModelCounts(countAvailableModels(data)); - setAvailableModels(parseAvailableModels(data)); - setLiveModelCounts(parseLiveModelCounts(data)); - setSelectedModels(parseSelectedModels(data)); + setModelSnapshot({ revision: modelRevision, selection, rows }); setModelsLoadFailed(false); succeeded = true; } catch { if (cancelled) return; setModelsLoadFailed(true); } finally { - if (!cancelled) { setModelsLoading(false); onModelsSettled?.(succeeded); } + bounded.controller.abort(); + bounded.clear(); + if (!cancelled) { setModelsLoading(false); modelsSettled.current?.(succeeded); } } })(); }, 0); return () => { cancelled = true; window.clearTimeout(timeout); + bounded.controller.abort(); + bounded.clear(); }; - }, [apiBase, modelsRefreshToken, modelsLoadEpoch, onModelsSettled]); + }, [apiBase, modelRevision]); useEffect(() => { let cancelled = false; @@ -509,7 +526,7 @@ export default function ProviderWorkspaceShell({ item={item} selected={selectedName === item.name} tabbable={railTabbableName === item.name} - modelCount={modelCounts[item.name]} + modelCount={modelSnapshot ? (Object.hasOwn(modelCounts, item.name) ? modelCounts[item.name] : 0) : undefined} isDefault={defaultProvider === item.name} showConfigId={duplicateDisplayNames.has(formatProviderDisplayName(item.name, t))} onClick={() => onSelect(item.name)} @@ -556,10 +573,13 @@ export default function ProviderWorkspaceShell({ usageTotals: usageTotals[selectedItem.name], modelUsage: usageModels[selectedItem.name], quotaReport: quotaReports[selectedItem.name], - availableModels: availableModels[selectedItem.name] ?? [], - hasLiveModels: (liveModelCounts[selectedItem.name] ?? 0) > 0, - selectedModels: selectedModels[selectedItem.name] ?? [], - modelsLoading, + availableModels: modelSnapshot?.selection.available[selectedItem.name] ?? [], + hasLiveModels: (modelSnapshot?.selection.liveModelCounts[selectedItem.name] ?? 0) > 0, + selectedModels: modelSnapshot?.selection.selected[selectedItem.name] ?? [], + modelRows: modelSnapshot?.rows.filter(row => row.provider === selectedItem.name) ?? null, + modelRevision, + modelRowsReady, + modelsLoading: modelsLoading || (!modelRowsReady && !modelsLoadFailed), modelsLoadFailed, onRetryModels: retryModels, }) ?? ( diff --git a/gui/src/hooks/useProviderAccountPools.ts b/gui/src/hooks/useProviderAccountPools.ts index 255ad4dfbe..0197a547d5 100644 --- a/gui/src/hooks/useProviderAccountPools.ts +++ b/gui/src/hooks/useProviderAccountPools.ts @@ -24,6 +24,32 @@ export interface OAuthAccount extends AccountQuotaReading { healthAction?: string; } export interface ApiKeyEntry extends AccountQuotaReading { id: string; label?: string; masked: string; active: boolean } +export interface AccountSelectionTarget { provider: string; kind: "oauth" | "api-key" } + +function selectionRows(rows: T[], id: string | null | undefined): T[] { + return id === undefined ? rows : rows.map(row => ({ ...row, active: row.id === id })); +} + +/** An invalidation read changes membership/selection, not quota probe state. */ +function mergeRosterRows(rows: T[], previous: T[]): T[] { + const prior = new Map(previous.map(row => [row.id, row])); + return mergeQuotaRows(rows, previous, false).map(row => supportsQuotaRead(row) ? { + ...row, + quotaPending: prior.get(row.id)?.quotaPending ?? false, + quotaUnavailable: prior.get(row.id)?.quotaUnavailable ?? false, + } : row); +} + +/** A probe started before a newer roster may update quota only on surviving IDs. */ +function mergeLateQuotaRows(rows: T[], enriched: T[]): T[] { + const byId = new Map(enriched.map(row => [row.id, row])); + return rows.map(row => { + const incoming = byId.get(row.id); + if (!incoming || incoming.quotaMode !== row.quotaMode) return row; + const quota = mergeQuotaRows([incoming], [row], true)[0]; + return { ...row, quota: quota.quota, quotaPending: quota.quotaPending, quotaUnavailable: quota.quotaUnavailable }; + }); +} type QuotaRow = AccountQuotaReading & { id: string }; const supportsQuotaRead = (row: AccountQuotaReading) => row.quotaMode === "probe" || row.quotaMode === "passive"; @@ -47,8 +73,9 @@ function mergeQuotaRows(rows: T[], previous: T[], enriched: }); } -function unavailableQuotaRows(rows: T[]): T[] { - return rows.map(row => supportsQuotaRead(row) +function unavailableQuotaRows(rows: T[], attempted?: T[]): T[] { + const attemptedModes = attempted && new Map(attempted.map(row => [row.id, row.quotaMode])); + return rows.map(row => supportsQuotaRead(row) && (!attemptedModes || attemptedModes.get(row.id) === row.quotaMode) ? { ...row, quotaUnavailable: true, quotaPending: false } : row); } @@ -91,12 +118,18 @@ export function useProviderAccountPools(deps: { const [addingKeyFor, setAddingKeyFor] = useState(null); const [newKeyValue, setNewKeyValue] = useState(""); const accountRequestGenerationRef = useRef>({}); + const rosterGenerationRef = useRef>({}); + const quotaGenerationRef = useRef>({}); + const selectionMutationsRef = useRef(new Map()); const requestsRef = useRef(new Set()); const mountedRef = useRef(true); const serverRef = useRef(apiBase); useEffect(() => { const generations = accountRequestGenerationRef.current; const requests = requestsRef.current; + const rosterGenerations = rosterGenerationRef.current; + const quotaGenerations = quotaGenerationRef.current; + const mutations = selectionMutationsRef.current; mountedRef.current = true; const serverChanged = serverRef.current !== apiBase; serverRef.current = apiBase; @@ -109,6 +142,9 @@ export function useProviderAccountPools(deps: { return () => { mountedRef.current = false; for (const key of Object.keys(generations)) generations[key] += 1; + for (const key of Object.keys(rosterGenerations)) rosterGenerations[key] += 1; + for (const key of Object.keys(quotaGenerations)) quotaGenerations[key] += 1; + mutations.clear(); for (const controller of requests) controller.abort(); requests.clear(); }; @@ -119,8 +155,11 @@ export function useProviderAccountPools(deps: { const keyPoolsKeyRef = useRef(null); const switchingAccountRef = useRef<{ provider: string; accountId: string } | null>(null); - const readRoster = useCallback(async (url: string): Promise => { + const readRoster = useCallback(async (url: string, signal?: AbortSignal): Promise => { const bounded = createBoundedFetch(20_000); + const abort = () => bounded.controller.abort(); + if (signal?.aborted) abort(); + signal?.addEventListener("abort", abort, { once: true }); requestsRef.current.add(bounded.controller); try { const response = await fetch(url, { signal: bounded.signal }); @@ -130,6 +169,7 @@ export function useProviderAccountPools(deps: { return data; } finally { bounded.clear(); + signal?.removeEventListener("abort", abort); requestsRef.current.delete(bounded.controller); } }, []); @@ -146,7 +186,10 @@ export function useProviderAccountPools(deps: { const key = `oauth:${provider}`; const generation = (accountRequestGenerationRef.current[key] ?? 0) + 1; accountRequestGenerationRef.current[key] = generation; + const rosterGeneration = (rosterGenerationRef.current[key] ?? 0) + 1; + rosterGenerationRef.current[key] = rosterGeneration; const currentRequest = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && accountRequestGenerationRef.current[key] === generation; + const currentRoster = () => currentRequest() && rosterGenerationRef.current[key] === rosterGeneration; const url = `${apiBase}/api/oauth/accounts?provider=${encodeURIComponent(provider)}`; try { // Cheap local read first so account switch / reauth / remove controls appear @@ -154,32 +197,39 @@ export function useProviderAccountPools(deps: { const data = await readRoster<{ activeAccountId?: string | null; accounts?: OAuthAccount[] }>(url); if (!Array.isArray(data.accounts)) throw new Error("Invalid account roster"); if (!currentRequest()) return false; - const rows = data.accounts; - setAccountSets(current => currentRequest() ? { ...current, [provider]: { + const rows = selectionRows(data.accounts, data.activeAccountId); + setAccountSets(current => currentRoster() ? { ...current, [provider]: { activeAccountId: data.activeAccountId ?? null, accounts: mergeQuotaRows(rows, current[provider]?.accounts ?? [], false), } } : current); - setAccountLoadStates(current => currentRequest() ? { ...current, [provider]: "ready" } : current); + setAccountLoadStates(current => currentRoster() ? { ...current, [provider]: "ready" } : current); if (!rows.some(supportsQuotaRead)) return true; const enrich = async (): Promise => { + // Manual selection invalidates roster reads, not a per-ID quota probe already sent. + const quotaGeneration = (quotaGenerationRef.current[key] ?? 0) + 1; + quotaGenerationRef.current[key] = quotaGeneration; + const currentQuota = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase + && quotaGenerationRef.current[key] === quotaGeneration; try { const quotaData = await readRoster<{ activeAccountId?: string | null; accounts?: OAuthAccount[] }>(`${url}"a=1${refresh ? "&refresh=1" : ""}`); if (!Array.isArray(quotaData.accounts)) throw new Error("Invalid account quota roster"); - if (!currentRequest()) return false; - const enriched = quotaData.accounts; - setAccountSets(current => currentRequest() ? { + if (!currentQuota()) return false; + const enriched = selectionRows(quotaData.accounts, quotaData.activeAccountId); + setAccountSets(current => !currentQuota() ? current : !currentRoster() ? { + ...current, [provider]: { ...current[provider], accounts: mergeLateQuotaRows(current[provider]?.accounts ?? [], enriched) }, + } : { ...current, [provider]: { - activeAccountId: quotaData.activeAccountId ?? data.activeAccountId ?? null, + activeAccountId: quotaData.activeAccountId === undefined ? data.activeAccountId ?? null : quotaData.activeAccountId, accounts: mergeQuotaRows(enriched, current[provider]?.accounts ?? [], true), }, - } : current); + }); return !enriched.some(row => row.quotaUnavailable === true); } catch { - if (!currentRequest()) return false; - setAccountSets(current => currentRequest() && current[provider] ? { - ...current, [provider]: { ...current[provider], accounts: unavailableQuotaRows(current[provider].accounts) }, + if (!currentQuota()) return false; + setAccountSets(current => currentQuota() && current[provider] ? { + ...current, [provider]: { ...current[provider], accounts: unavailableQuotaRows(current[provider].accounts, rows) }, } : current); return false; } @@ -188,9 +238,9 @@ export function useProviderAccountPools(deps: { void enrich(); return true; } catch { - if (!currentRequest()) return false; - setAccountLoadStates(current => currentRequest() ? { ...current, [provider]: "error" } : current); - setAccountSets(current => currentRequest() && current[provider] ? { + if (!currentRoster()) return false; + setAccountLoadStates(current => currentRoster() ? { ...current, [provider]: "error" } : current); + setAccountSets(current => currentRoster() && current[provider] ? { ...current, [provider]: { ...current[provider], accounts: unavailableQuotaRows(current[provider].accounts) }, } : current); return false; @@ -205,29 +255,42 @@ export function useProviderAccountPools(deps: { const key = `key:${name}`; const generation = (accountRequestGenerationRef.current[key] ?? 0) + 1; accountRequestGenerationRef.current[key] = generation; + const rosterGeneration = (rosterGenerationRef.current[key] ?? 0) + 1; + rosterGenerationRef.current[key] = rosterGeneration; const currentRequest = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && accountRequestGenerationRef.current[key] === generation; + const currentRoster = () => currentRequest() && rosterGenerationRef.current[key] === rosterGeneration; const url = `${apiBase}/api/providers/keys?name=${encodeURIComponent(name)}`; const failed = () => { - if (currentRequest()) setKeyPools(current => currentRequest() + if (currentRoster()) setKeyPools(current => currentRoster() ? { ...current, [name]: unavailableQuotaRows(current[name] ?? []) } : current); return false; }; try { - const data = await readRoster<{ keys?: ApiKeyEntry[] }>(url); + const data = await readRoster<{ activeId?: string | null; keys?: ApiKeyEntry[] }>(url); if (!Array.isArray(data.keys)) throw new Error("Invalid key roster"); if (!currentRequest()) return false; - const rows = data.keys; - setKeyPools(current => currentRequest() ? { ...current, [name]: mergeQuotaRows(rows, current[name] ?? [], false) } : current); + const rows = selectionRows(data.keys, data.activeId); + setKeyPools(current => currentRoster() ? { ...current, [name]: mergeQuotaRows(rows, current[name] ?? [], false) } : current); if (!rows.some(supportsQuotaRead)) return true; const enrich = async (): Promise => { + const quotaGeneration = (quotaGenerationRef.current[key] ?? 0) + 1; + quotaGenerationRef.current[key] = quotaGeneration; + const currentQuota = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase + && quotaGenerationRef.current[key] === quotaGeneration; try { - const data = await readRoster<{ keys?: ApiKeyEntry[] }>(`${url}"a=1${refresh ? "&refresh=1" : ""}`); + const data = await readRoster<{ activeId?: string | null; keys?: ApiKeyEntry[] }>(`${url}"a=1${refresh ? "&refresh=1" : ""}`); if (!Array.isArray(data.keys)) throw new Error("Invalid key quota roster"); - if (!currentRequest()) return false; - const enriched = data.keys; - setKeyPools(current => currentRequest() ? { ...current, [name]: mergeQuotaRows(enriched, current[name] ?? [], true) } : current); + if (!currentQuota()) return false; + const enriched = selectionRows(data.keys, data.activeId); + setKeyPools(current => currentQuota() ? { ...current, [name]: currentRoster() + ? mergeQuotaRows(enriched, current[name] ?? [], true) + : mergeLateQuotaRows(current[name] ?? [], enriched) } : current); return !enriched.some(row => row.quotaUnavailable === true); - } catch { return failed(); } + } catch { + if (currentQuota()) setKeyPools(current => currentQuota() + ? { ...current, [name]: unavailableQuotaRows(current[name] ?? [], rows) } : current); + return false; + } }; if (refresh) return await enrich(); void enrich(); @@ -237,22 +300,88 @@ export function useProviderAccountPools(deps: { return results.every(Boolean); }, [apiBase, aliveRef, readRoster]); + const refreshAccountRosters = useCallback(async (target?: AccountSelectionTarget, signal?: AbortSignal): Promise => { + if (!aliveRef.current || !mountedRef.current || serverRef.current !== apiBase || signal?.aborted) return false; + const targets: AccountSelectionTarget[] = target ? [target] : Object.entries(config?.providers ?? {}).flatMap(([provider, p]) => + p.authMode === "oauth" && provider !== "openai" ? [{ provider, kind: "oauth" as const }] + : p.hasApiKey && p.authMode !== "oauth" && p.authMode !== "forward" ? [{ provider, kind: "api-key" as const }] : []); + const results = await Promise.all(targets.map(async ({ provider, kind }) => { + const key = `${kind === "oauth" ? "oauth" : "key"}:${provider}`; + // PUT settlement always reconciles, including events received while it is pending. + if (selectionMutationsRef.current.has(key)) return false; + const generation = (rosterGenerationRef.current[key] ?? 0) + 1; + rosterGenerationRef.current[key] = generation; + const currentRequest = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase + && !signal?.aborted && rosterGenerationRef.current[key] === generation && !selectionMutationsRef.current.has(key); + try { + if (kind === "oauth") { + const data = await readRoster<{ activeAccountId?: string | null; accounts?: OAuthAccount[] }>( + `${apiBase}/api/oauth/accounts?provider=${encodeURIComponent(provider)}`, signal); + if (!Array.isArray(data.accounts) || !currentRequest()) return false; + const rows = selectionRows(data.accounts, data.activeAccountId); + setAccountSets(current => currentRequest() ? { ...current, [provider]: { + activeAccountId: data.activeAccountId === undefined ? rows.find(row => row.active)?.id ?? null : data.activeAccountId, + accounts: mergeRosterRows(rows, current[provider]?.accounts ?? []), + } } : current); + setAccountLoadStates(current => currentRequest() ? { ...current, [provider]: "ready" } : current); + } else { + const data = await readRoster<{ activeId?: string | null; keys?: ApiKeyEntry[] }>( + `${apiBase}/api/providers/keys?name=${encodeURIComponent(provider)}`, signal); + if (!Array.isArray(data.keys) || !currentRequest()) return false; + const rows = selectionRows(data.keys, data.activeId); + setKeyPools(current => currentRequest() ? { ...current, [provider]: mergeRosterRows(rows, current[provider] ?? []) } : current); + } + return true; + } catch { + // A missed invalidation read does not change quota health; recovery retries it. + return false; + } + })); + return results.every(Boolean); + }, [aliveRef, apiBase, config, readRoster]); + + const invalidateSelectionReads = (provider: string, kind: AccountSelectionTarget["kind"]) => { + const key = `${kind === "oauth" ? "oauth" : "key"}:${provider}`; + accountRequestGenerationRef.current[key] = (accountRequestGenerationRef.current[key] ?? 0) + 1; + rosterGenerationRef.current[key] = (rosterGenerationRef.current[key] ?? 0) + 1; + // The independent quota generation still owns pending/error flags for surviving IDs. + return key; + }; + const switchAccount = async (provider: string, account: OAuthAccount) => { if (account.active || account.needsReauth || switchingAccountRef.current) return; const target = { provider, accountId: account.id }; switchingAccountRef.current = target; setSwitchingAccount(target); + const key = invalidateSelectionReads(provider, "oauth"); + const mutation = Symbol(); + selectionMutationsRef.current.set(key, mutation); + const currentMutation = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && selectionMutationsRef.current.get(key) === mutation; const label = oauthAccountDisplayLabel(accountSets[provider]?.accounts ?? [account], account, t); try { const res = await fetch(`${apiBase}/api/oauth/accounts/active`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider, accountId: account.id }) }); + if (!currentMutation()) return; if (!res.ok) { notify(t("prov.accountSwitchFail"), false); return; } - const refreshed = await fetchAccountSets([provider]); + const result = await res.json().catch(() => ({})) as { activeAccountId?: string | null }; + if (!currentMutation()) return; + invalidateSelectionReads(provider, "oauth"); + const selected = result.activeAccountId === undefined ? account.id : result.activeAccountId; + setAccountSets(current => current[provider] ? { ...current, [provider]: { + ...current[provider], activeAccountId: selected, accounts: selectionRows(current[provider].accounts, selected), + } } : current); + selectionMutationsRef.current.delete(key); + const refreshed = await refreshAccountRosters({ provider, kind: "oauth" }); await Promise.all([fetchOauth(), fetchProviderQuotas(true)]); if (!refreshed) { notify(t("pws.accountsLoadFailed"), false); return; } notify(t("prov.accountSwitched", { email: label }), true); } catch { - notify(t("prov.accountSwitchFail"), false); + if (currentMutation()) notify(t("prov.accountSwitchFail"), false); } finally { + if (currentMutation()) { + invalidateSelectionReads(provider, "oauth"); + selectionMutationsRef.current.delete(key); + void refreshAccountRosters({ provider, kind: "oauth" }); + } if (switchingAccountRef.current?.provider === target.provider && switchingAccountRef.current.accountId === target.accountId) { switchingAccountRef.current = null; if (aliveRef.current) setSwitchingAccount(null); @@ -261,15 +390,34 @@ export function useProviderAccountPools(deps: { }; const switchApiKey = async (provider: string, entry: ApiKeyEntry) => { - if (entry.active) return; - const res = await fetch(`${apiBase}/api/providers/keys/active`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: provider, id: entry.id }) }); - if (res.ok) { + if (entry.active || selectionMutationsRef.current.has(`key:${provider}`)) return; + const key = invalidateSelectionReads(provider, "api-key"); + const mutation = Symbol(); + selectionMutationsRef.current.set(key, mutation); + const currentMutation = () => aliveRef.current && mountedRef.current && serverRef.current === apiBase && selectionMutationsRef.current.get(key) === mutation; + try { + const res = await fetch(`${apiBase}/api/providers/keys/active`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: provider, id: entry.id }) }); + if (!currentMutation()) return; + if (!res.ok) { + const failure = await res.json().catch(() => ({})) as { error?: string }; + if (currentMutation()) notify(failure.error || t("prov.keySwitchFail"), false); + return; + } + const data = await res.json().catch(() => ({})) as { activeId?: string | null }; + if (!currentMutation()) return; + invalidateSelectionReads(provider, "api-key"); + const selected = data.activeId === undefined ? entry.id : data.activeId; + setKeyPools(current => current[provider] ? { ...current, [provider]: selectionRows(current[provider], selected) } : current); notify(t("prov.keySwitched", { key: entry.label ?? entry.masked }), true); - void fetchKeyPools(Object.keys(keyPools)); void fetchProviderQuotas(true); - } else { - const data = await res.json().catch(() => ({})); - notify(data.error || t("prov.keySwitchFail"), false); + } catch { + if (currentMutation()) notify(t("prov.keySwitchFail"), false); + } finally { + if (currentMutation()) { + invalidateSelectionReads(provider, "api-key"); + selectionMutationsRef.current.delete(key); + void refreshAccountRosters({ provider, kind: "api-key" }); + } } }; @@ -382,7 +530,7 @@ export function useProviderAccountPools(deps: { return { accountSets, accountLoadStates, switchingAccount, openAccounts, keyPools, addingKeyFor, newKeyValue, setAccountSets, setAccountLoadStates, setSwitchingAccount, setOpenAccounts, setKeyPools, setAddingKeyFor, setNewKeyValue, - fetchAccountSets, fetchKeyPools, switchAccount, switchApiKey, removeApiKey, addApiKeyValue, addApiKey, editCredentialAlias, removeAccount, + fetchAccountSets, fetchKeyPools, refreshAccountRosters, switchAccount, switchApiKey, removeApiKey, addApiKeyValue, addApiKey, editCredentialAlias, removeAccount, oauthCardProviders, keyCardProviders, activeAccountNeedsReauth, }; } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index cf04d02f67..429379396f 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -576,7 +576,21 @@ export const de: Record = { "models.customEditBtn": "Aktualisieren", "models.customEdit": "Bearbeiten", "models.customDelete": "Löschen", - "models.customDeleteConfirm": "Modell {name} löschen?", + "pws.manageModelVisibility": "Sichtbarkeit unter Modelle verwalten", + "pws.modelOwnershipLoading": "Modelldefinitionen werden geprüft…", + "pws.modelOwnershipFailed": "Modelldefinitionen konnten nicht geladen werden. Vor Änderungen erneut versuchen.", + "pws.modelSelectionPending": "Schließe die Modellauswahl unter Modelle ab, bevor du Änderungen vornimmst.", + "pws.modelSaved": "Benutzerdefinierte Modelldefinition gespeichert.", + "pws.modelSavedHidden": "Definition gespeichert. Dieses Modell ist ausgeblendet; verwalte die Sichtbarkeit unter Modelle.", + "pws.modelSavedRefreshPending": "Definition gespeichert, aber der Modellkatalog konnte nicht aktualisiert werden. Wiederhole die Aktualisierung; füge das Modell nicht erneut hinzu.", + "pws.modelMutationUnconfirmed": "Die Änderung konnte nicht bestätigt werden. Aktualisiere die Modelle, bevor du es erneut versuchst.", + "pws.modelRemovedRefreshPending": "Die Änderung wurde gespeichert, aber der Modellkatalog konnte nicht aktualisiert werden. Wiederhole die Aktualisierung.", + "pws.modelHidden": "Modell ausgeblendet. Stelle die Sichtbarkeit unter Modelle wieder her.", + "pws.modelDefinitionDeleted": "Benutzerdefinierte Definition gelöscht. Ein zugrunde liegendes Modell kann weiterhin angezeigt werden.", + "pws.modelKnown": "Dieses Modell ist bereits bekannt. Verwalte seine Sichtbarkeit unter Modelle.", + "models.customDeleteConfirm": "Benutzerdefinierte Definition für {name} löschen? Ein zugrunde liegendes natives oder erkanntes Modell kann wieder erscheinen.", + "models.hide": "Ausblenden", + "models.hideConfirm": "{name} im Modellkatalog ausblenden? Die Definition wird nicht entfernt und die Richtlinie für direkte Weiterleitung bleibt unverändert.", "models.customBadge": "Benutzerdefiniert", "models.customSummary": "{count} benutzerdefiniert", "models.customFieldModelId": "Modell-ID (Endpunkt-Slug)", @@ -601,6 +615,7 @@ export const de: Record = { "models.tipActive": "Aktiv", "models.tipDisabled": "Deaktiviert", "models.applied": "Angewendet — greift bei der nächsten Codex-Runde.", + "models.integrationRefreshWarning": "Modellauswahl gespeichert. Einige Client-Kataloge konnten nicht aktualisiert werden. Prüfe vor dem Start einer neuen Sitzung die Integrationen.", "models.saveFailed": "Speichern fehlgeschlagen", "models.networkError": "Netzwerkfehler — läuft der Proxy?", "models.loadFail": "Modelle konnten nicht geladen werden — läuft der Proxy?", @@ -665,6 +680,26 @@ export const de: Record = { "logs.noRequests": "Noch keine Anfragen.", "logs.loadError": "Anfrageprotokolle konnten nicht geladen werden.", "logs.filter.surface.label": "Oberfläche", + "logs.filter.model.all": "Alle Modelle", + "logs.filter.provider.label": "Anbieter", + "logs.filter.provider.all": "Alle Anbieter", + "logs.filter.status.label": "Status", + "logs.filter.status.all": "Alle Status", + "logs.filter.status.success": "Erfolg (2xx)", + "logs.filter.status.errors": "Fehler (4xx/5xx)", + "logs.filter.time.label": "Zeit", + "logs.filter.time.all": "Alle Zeiten", + "logs.filter.time.15m": "Letzte 15 Min.", + "logs.filter.time.1h": "Letzte Stunde", + "logs.filter.time.24h": "Letzter Tag", + "logs.filter.speed.label": "Geschwindigkeit", + "logs.filter.speed.all": "Alle Geschwindigkeiten", + "logs.filter.speed.slow": "< 15 Tok/s", + "logs.filter.speed.medium": "15–< 50 Tok/s", + "logs.filter.speed.fast": "≥ 50 Tok/s", + "logs.filter.reset": "Filter zurücksetzen", + "logs.filter.showingCount": "{count} von {total} angezeigt", + "logs.noMatchingRequests": "Keine passenden Anfragen.", "logs.filter.surface.all": "Alle", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1044,6 +1079,21 @@ export const de: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside-Profile", + "integrations.aside.profilesHint": "Wähle, welche Profile die ausgewählten Modelle erhalten. Das aktive Aside-Profil bleibt unverändert.", + "integrations.aside.all": "Alle Profile synchronisieren", + "integrations.aside.syncNow": "Jetzt synchronisieren", + "integrations.aside.applied": "Bei {count} von {total} Profilen angewendet", + "integrations.aside.current": "Aktuelles Profil", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "{name} synchronisieren", + "integrations.aside.details": "{name} verwalten", + "integrations.aside.back": "Alle Aside-Profile", + "integrations.aside.empty": "Öffne Aside und erstelle ein Profil, um es zu verbinden.", + "integrations.aside.partial": "Einige Profile benötigen Aufmerksamkeit. Deine Synchronisierungsauswahl ist gespeichert. Prüfe den Status der einzelnen Profile.", + "integrations.aside.pending": "Synchronisierungsauswahl gespeichert; Dateiaktualisierung steht aus.", + "integrations.aside.retry": "Für {name} erneut versuchen", + "integrations.aside.loadError": "Aside-Profile konnten nicht geladen werden. Versuche es erneut, um ihren Status zu prüfen.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Die Codex-Anbindung wird vom Proxy-Dienst verwaltet. Beim Start von opencodex wird sie angewendet; beim Stoppen des Dienstes wird das native Routing wiederhergestellt.", "integrations.codex.openService": "Dienststeuerung öffnen", @@ -1162,6 +1212,7 @@ export const de: Record = { "integrations.bulk.success": "Angewendete Client-Integrationen wurden deaktiviert.", "integrations.retention.degraded": "Die Sicherungsbereinigung ist im Rückstand; ältere Sicherungen könnten noch auf dem Datenträger liegen.", "integrations.error.residual": "Die Datei könnte sich in einem Zwischenzustand befinden: {message} Stellen Sie sie aus {path} wieder her.", + "integrations.error.residualNoSnapshot": "{message} Die automatische Wiederherstellung wurde nicht abgeschlossen. Prüfe die Client-Konfiguration, bevor du es erneut versuchst.", "integrations.error.recover": "{message} Eine Sicherung liegt unter {path}.", "integrations.kind.apply": "Angewendet", "integrations.kind.disable": "Deaktiviert", @@ -1186,7 +1237,7 @@ export const de: Record = { "integrations.semantics.mcode": "Verwaltet nur custom_provider.opencodex. Standardmodell und MiniMax-Anmeldung bleiben unverändert.", "integrations.semantics.zcode": "Verwaltet nur provider.opencodex in ~/.zcode/v2/config.json. Z.ai-Anmeldung und andere Provider bleiben unverändert. ZCode nach Änderungen neu starten.", "integrations.semantics.prime": "Verwaltet nur providers.opencodex in der models.json von Prime Agent — ~/.prime/agent, sofern PRIME_AGENT_CODING_AGENT_DIR sie nicht umleitet. Andere Provider und Modell-Overrides bleiben unverändert. Gilt für neue Sitzungen.", - "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der models.json von Aside für das angemeldete Konto (~/.aside/u/). Andere Provider bleiben unverändert. Aside überschreibt diese Datei im laufenden Betrieb, daher nach dem Anwenden vollständig beenden und neu öffnen.", + "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der ~/.aside/u//models.json dieses Profils. Andere Provider bleiben unverändert. Beende Aside nach dem Anwenden vollständig und öffne es erneut.", "codexAuth.mainAccount": "Hauptkonto", "codexAuth.logLabel": "Log-Kennung", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 5d0c0b0983..9cbf8699fd 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -592,6 +592,18 @@ export const en = { "models.customAdd": "Add custom model", "models.customAddTitle": "Add custom model — {provider}", "models.customEditTitle": "Edit custom model — {provider}", + "pws.manageModelVisibility": "Manage visibility in Models", + "pws.modelOwnershipLoading": "Checking model definitions…", + "pws.modelOwnershipFailed": "Model definitions could not be loaded. Retry before making changes.", + "pws.modelSelectionPending": "Finish model selection in Models before making changes.", + "pws.modelSaved": "Custom model definition saved.", + "pws.modelSavedHidden": "Definition saved. This model is hidden; manage visibility in Models.", + "pws.modelSavedRefreshPending": "Definition saved, but the model catalog could not be refreshed. Retry the refresh; do not add it again.", + "pws.modelMutationUnconfirmed": "The change could not be confirmed. Refresh the models before trying again.", + "pws.modelRemovedRefreshPending": "The change was saved, but the model catalog could not be refreshed. Retry the refresh.", + "pws.modelHidden": "Model hidden. Restore visibility in Models.", + "pws.modelDefinitionDeleted": "Custom definition deleted. An underlying model may still appear.", + "pws.modelKnown": "This model is already known. Manage its visibility in Models.", "models.customAdded": "Custom model added", "models.customUpdated": "Custom model updated", "models.customDeleted": "Custom model deleted", @@ -601,7 +613,9 @@ export const en = { "models.customEditBtn": "Update", "models.customEdit": "Edit", "models.customDelete": "Delete", - "models.customDeleteConfirm": "Delete the {name} model?", + "models.customDeleteConfirm": "Delete the custom definition for {name}? An underlying native or discovered model may appear again.", + "models.hide": "Hide", + "models.hideConfirm": "Hide {name} from the model catalog? This does not remove its definition or change direct routing policy.", "models.customBadge": "Custom", "models.customSummary": "{count} custom", "models.customFieldModelId": "Model ID (endpoint slug)", @@ -626,6 +640,7 @@ export const en = { "models.tipActive": "Active", "models.tipDisabled": "Disabled", "models.applied": "Applied — takes effect on the next Codex turn.", + "models.integrationRefreshWarning": "Model selection saved. Some client catalogs could not be refreshed. Check Integrations before starting a new session.", "models.saveFailed": "Save failed", "models.networkError": "Network error — is the proxy running?", "models.loadFail": "Failed to load models — is the proxy running?", @@ -698,6 +713,26 @@ export const en = { "logs.noRequests": "No requests yet.", "logs.loadError": "Could not load request logs.", "logs.filter.surface.label": "Surface", + "logs.filter.model.all": "All models", + "logs.filter.provider.label": "Provider", + "logs.filter.provider.all": "All providers", + "logs.filter.status.label": "Status", + "logs.filter.status.all": "All statuses", + "logs.filter.status.success": "Success (2xx)", + "logs.filter.status.errors": "Errors (4xx/5xx)", + "logs.filter.time.label": "Time", + "logs.filter.time.all": "All time", + "logs.filter.time.15m": "Last 15m", + "logs.filter.time.1h": "Last 1h", + "logs.filter.time.24h": "Last 1d", + "logs.filter.speed.label": "Speed", + "logs.filter.speed.all": "All speeds", + "logs.filter.speed.slow": "< 15 tok/s", + "logs.filter.speed.medium": "15–< 50 tok/s", + "logs.filter.speed.fast": "≥ 50 tok/s", + "logs.filter.reset": "Reset filters", + "logs.filter.showingCount": "Showing {count} of {total}", + "logs.noMatchingRequests": "No matching requests.", "logs.filter.surface.all": "All", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1551,6 +1586,21 @@ export const en = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside profiles", + "integrations.aside.profilesHint": "Choose which profiles receive the selected models. Aside’s active profile stays unchanged.", + "integrations.aside.all": "Sync all profiles", + "integrations.aside.syncNow": "Sync now", + "integrations.aside.applied": "{count} of {total} profiles applied", + "integrations.aside.current": "Current profile", + "integrations.aside.profile": "Profile {id}", + "integrations.aside.toggle": "Sync {name}", + "integrations.aside.details": "Manage {name}", + "integrations.aside.back": "All Aside profiles", + "integrations.aside.empty": "Open Aside and create a profile to connect it.", + "integrations.aside.partial": "Some profiles need attention. Your sync choices are saved; check each profile’s state.", + "integrations.aside.pending": "Sync choice saved; file update pending.", + "integrations.aside.retry": "Retry {name}", + "integrations.aside.loadError": "Could not load Aside profiles. Retry to check their state.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex wiring is owned by the proxy service. Starting opencodex applies it; stopping the service restores native routing.", "integrations.codex.openService": "Open service controls", @@ -1709,6 +1759,7 @@ export const en = { "integrations.bulk.success": "Applied client integrations were disabled.", "integrations.retention.degraded": "Backup cleanup is behind; older backups may still be on disk.", "integrations.error.residual": "The file may be in an intermediate state: {message} Restore it from {path}.", + "integrations.error.residualNoSnapshot": "{message} Automatic recovery did not finish. Check the client configuration before retrying.", "integrations.error.recover": "{message} A backup is at {path}.", "integrations.kind.apply": "Applied", "integrations.kind.disable": "Disabled", @@ -1733,7 +1784,7 @@ export const en = { "integrations.semantics.mcode": "Manages only custom_provider.opencodex. Your default model and MiniMax login stay unchanged.", "integrations.semantics.zcode": "Manages only provider.opencodex in ~/.zcode/v2/config.json. Your Z.ai login and other providers stay unchanged. Restart ZCode after changes.", "integrations.semantics.prime": "Manages only providers.opencodex in Prime Agent's models.json — ~/.prime/agent unless PRIME_AGENT_CODING_AGENT_DIR redirects it. Your other providers and model overrides stay unchanged. Applies to new sessions.", - "integrations.semantics.aside": "Manages only providers.opencodex in Aside's models.json for the signed-in account (~/.aside/u/). Your other providers stay unchanged. Aside rewrites this file while running, so fully quit and reopen it after applying.", + "integrations.semantics.aside": "Manages only providers.opencodex in this profile’s ~/.aside/u//models.json. Your other providers stay unchanged. Fully quit and reopen Aside after applying.", "codexAuth.mainAccount": "Main Account", "codexAuth.logLabel": "Log label", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index ac0d9f17c3..ec171e627c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -586,7 +586,21 @@ export const fr: Record = { "models.customEditBtn": "Mettre à jour", "models.customEdit": "Modifier", "models.customDelete": "Supprimer", - "models.customDeleteConfirm": "Supprimer le modèle {name} ?", + "pws.manageModelVisibility": "Gérer la visibilité dans Modèles", + "pws.modelOwnershipLoading": "Vérification des définitions de modèles…", + "pws.modelOwnershipFailed": "Impossible de charger les définitions de modèles. Réessayez avant toute modification.", + "pws.modelSelectionPending": "Terminez la sélection dans Modèles avant toute modification.", + "pws.modelSaved": "Définition de modèle personnalisé enregistrée.", + "pws.modelSavedHidden": "Définition enregistrée. Ce modèle est masqué ; gérez sa visibilité dans Modèles.", + "pws.modelSavedRefreshPending": "Définition enregistrée, mais le catalogue des modèles n’a pas pu être actualisé. Relancez l’actualisation ; ne l’ajoutez pas à nouveau.", + "pws.modelMutationUnconfirmed": "La modification n’a pas pu être confirmée. Actualisez les modèles avant de réessayer.", + "pws.modelRemovedRefreshPending": "La modification a été enregistrée, mais le catalogue des modèles n’a pas pu être actualisé. Relancez l’actualisation.", + "pws.modelHidden": "Modèle masqué. Rétablissez sa visibilité dans Modèles.", + "pws.modelDefinitionDeleted": "Définition personnalisée supprimée. Un modèle sous-jacent peut toujours apparaître.", + "pws.modelKnown": "Ce modèle est déjà connu. Gérez sa visibilité dans Modèles.", + "models.customDeleteConfirm": "Supprimer la définition personnalisée de {name} ? Un modèle natif ou découvert sous-jacent peut réapparaître.", + "models.hide": "Masquer", + "models.hideConfirm": "Masquer {name} du catalogue des modèles ? Cela ne supprime pas sa définition et ne change pas la politique de routage direct.", "models.customBadge": "Personnalisé", "models.customSummary": "{count} personnalisés", "models.customFieldModelId": "ID du modèle (slug du point de terminaison)", @@ -611,6 +625,7 @@ export const fr: Record = { "models.tipActive": "Actif", "models.tipDisabled": "Désactivé", "models.applied": "Appliqué — prend effet au prochain tour Codex.", + "models.integrationRefreshWarning": "Sélection des modèles enregistrée. Certains catalogues clients n’ont pas pu être actualisés. Vérifiez les intégrations avant de démarrer une nouvelle session.", "models.saveFailed": "Échec de l’enregistrement", "models.networkError": "Erreur réseau — le proxy est-il en cours d’exécution ?", "models.loadFail": "Échec du chargement des modèles — le proxy est-il en cours d’exécution ?", @@ -679,6 +694,26 @@ export const fr: Record = { "logs.noRequests": "Aucune requête pour le moment.", "logs.loadError": "Impossible de charger les journaux des requêtes.", "logs.filter.surface.label": "Interface", + "logs.filter.model.all": "Tous les modèles", + "logs.filter.provider.label": "Fournisseur", + "logs.filter.provider.all": "Tous les fournisseurs", + "logs.filter.status.label": "Statut", + "logs.filter.status.all": "Tous les statuts", + "logs.filter.status.success": "Réussites (2xx)", + "logs.filter.status.errors": "Erreurs (4xx/5xx)", + "logs.filter.time.label": "Temps", + "logs.filter.time.all": "Toutes les périodes", + "logs.filter.time.15m": "15 dernières min", + "logs.filter.time.1h": "Dernière heure", + "logs.filter.time.24h": "Dernier jour", + "logs.filter.speed.label": "Vitesse", + "logs.filter.speed.all": "Toutes les vitesses", + "logs.filter.speed.slow": "< 15 jetons/s", + "logs.filter.speed.medium": "15–< 50 jetons/s", + "logs.filter.speed.fast": "≥ 50 jetons/s", + "logs.filter.reset": "Réinitialiser les filtres", + "logs.filter.showingCount": "Affichage de {count} sur {total}", + "logs.noMatchingRequests": "Aucune requête correspondante.", "logs.filter.surface.all": "Toutes", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1523,6 +1558,21 @@ export const fr: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Profils Aside", + "integrations.aside.profilesHint": "Choisissez les profils qui recevront les modèles sélectionnés. Le profil actif dans Aside reste inchangé.", + "integrations.aside.all": "Synchroniser tous les profils", + "integrations.aside.syncNow": "Synchroniser maintenant", + "integrations.aside.applied": "Appliqué à {count} profils sur {total}", + "integrations.aside.current": "Profil actuel", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "Synchroniser {name}", + "integrations.aside.details": "Gérer {name}", + "integrations.aside.back": "Tous les profils Aside", + "integrations.aside.empty": "Ouvrez Aside et créez un profil pour le connecter.", + "integrations.aside.partial": "Certains profils nécessitent votre attention. Vos choix de synchronisation sont enregistrés ; vérifiez l’état de chaque profil.", + "integrations.aside.pending": "Choix de synchronisation enregistré ; mise à jour du fichier en attente.", + "integrations.aside.retry": "Réessayer pour {name}", + "integrations.aside.loadError": "Impossible de charger les profils Aside. Réessayez pour vérifier leur état.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Le câblage de Codex est géré par le service proxy. Le démarrage d’opencodex l’applique ; l’arrêt du service rétablit le routage natif.", "integrations.codex.openService": "Ouvrir les commandes du service", @@ -1641,6 +1691,7 @@ export const fr: Record = { "integrations.bulk.success": "Les intégrations client appliquées ont été désactivées.", "integrations.retention.degraded": "Le nettoyage des sauvegardes est en retard ; d’anciennes sauvegardes peuvent encore se trouver sur le disque.", "integrations.error.residual": "Le fichier peut être dans un état intermédiaire : {message} Restaurez-le depuis {path}.", + "integrations.error.residualNoSnapshot": "{message} La récupération automatique n’a pas abouti. Vérifiez la configuration du client avant de réessayer.", "integrations.error.recover": "{message} Une sauvegarde se trouve dans {path}.", "integrations.kind.apply": "Appliqué", "integrations.kind.disable": "Désactivé", @@ -1665,7 +1716,7 @@ export const fr: Record = { "integrations.semantics.mcode": "Gère uniquement custom_provider.opencodex. Votre modèle par défaut et votre connexion MiniMax restent inchangés.", "integrations.semantics.zcode": "Gère uniquement provider.opencodex dans ~/.zcode/v2/config.json. Votre connexion Z.ai et les autres fournisseurs restent inchangés. Redémarrez ZCode après toute modification.", "integrations.semantics.prime": "Gère uniquement providers.opencodex dans le models.json de Prime Agent — ~/.prime/agent, sauf si PRIME_AGENT_CODING_AGENT_DIR le redirige. Vos autres fournisseurs et surcharges de modèles restent inchangés. S'applique aux nouvelles sessions.", - "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le models.json d'Aside pour le compte connecté (~/.aside/u/). Vos autres fournisseurs restent inchangés. Aside réécrit ce fichier pendant son exécution : quittez-le complètement et relancez-le après application.", + "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le fichier ~/.aside/u//models.json de ce profil. Vos autres fournisseurs restent inchangés. Quittez complètement Aside et relancez-le après application.", "codexAuth.mainAccount": "Compte principal", "codexAuth.logLabel": "Libellé du journal", "codexAuth.codexApp": "Application Codex", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 7c6eab5675..c71bd7a045 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -573,6 +573,7 @@ export const ja: Record = { "models.customApply": "適用", "models.customPlaceholder": "トークン (例: 420000)", "models.applied": "適用しました — 次回の Codex ターンで有効になります。", + "models.integrationRefreshWarning": "モデルの選択を保存しました。一部のクライアントのモデル一覧を更新できませんでした。新しいセッションを始める前に「連携」を確認してください。", "models.saveFailed": "保存に失敗しました", "models.networkError": "ネットワークエラー — プロキシは起動していますか?", "models.loadFail": "モデルの読み込みに失敗しました — プロキシは起動していますか?", @@ -641,6 +642,26 @@ export const ja: Record = { "logs.noRequests": "まだリクエストがありません。", "logs.loadError": "リクエストログを読み込めませんでした。", "logs.filter.surface.label": "サーフェス", + "logs.filter.model.all": "すべてのモデル", + "logs.filter.provider.label": "プロバイダー", + "logs.filter.provider.all": "すべてのプロバイダー", + "logs.filter.status.label": "ステータス", + "logs.filter.status.all": "すべてのステータス", + "logs.filter.status.success": "成功 (2xx)", + "logs.filter.status.errors": "エラー (4xx/5xx)", + "logs.filter.time.label": "時間", + "logs.filter.time.all": "すべての時間", + "logs.filter.time.15m": "過去15分", + "logs.filter.time.1h": "過去1時間", + "logs.filter.time.24h": "過去1日", + "logs.filter.speed.label": "速度", + "logs.filter.speed.all": "すべての速度", + "logs.filter.speed.slow": "< 15 トークン/秒", + "logs.filter.speed.medium": "15–< 50 トークン/秒", + "logs.filter.speed.fast": "≥ 50 トークン/秒", + "logs.filter.reset": "フィルターをリセット", + "logs.filter.showingCount": "{total} 件中 {count} 件を表示", + "logs.noMatchingRequests": "一致するリクエストはありません。", "logs.filter.surface.all": "すべて", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1478,6 +1499,21 @@ export const ja: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Asideのプロファイル", + "integrations.aside.profilesHint": "選択したモデルを同期するプロファイルを選んでください。Asideで使用中のプロファイルは変わりません。", + "integrations.aside.all": "すべてのプロファイルを同期", + "integrations.aside.syncNow": "今すぐ同期", + "integrations.aside.applied": "{total}件中{count}件のプロファイルに適用済み", + "integrations.aside.current": "使用中のプロファイル", + "integrations.aside.profile": "プロファイル {id}", + "integrations.aside.toggle": "{name}を同期", + "integrations.aside.details": "{name}を管理", + "integrations.aside.back": "Asideの全プロファイル", + "integrations.aside.empty": "Asideを開き、接続するプロファイルを作成してください。", + "integrations.aside.partial": "一部のプロファイルに確認が必要です。同期設定は保存されています。各プロファイルの状態を確認してください。", + "integrations.aside.pending": "同期設定を保存しました。ファイルの更新待ちです。", + "integrations.aside.retry": "{name}を再試行", + "integrations.aside.loadError": "Asideのプロファイルを読み込めませんでした。再試行して状態を確認してください。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex の接続はプロキシサービスが管理します。opencodex を起動すると適用され、サービスを停止するとネイティブのルーティングに戻ります。", "integrations.codex.openService": "サービス制御を開く", @@ -1596,6 +1632,7 @@ export const ja: Record = { "integrations.bulk.success": "適用済みのクライアント連携を無効にしました。", "integrations.retention.degraded": "バックアップの整理が遅れています。古いバックアップがディスクに残っている可能性があります。", "integrations.error.residual": "ファイルが中間状態のままの可能性があります: {message} {path} から復元してください。", + "integrations.error.residualNoSnapshot": "{message} 自動復旧が完了しませんでした。再試行する前にクライアントの設定を確認してください。", "integrations.error.recover": "{message} バックアップは {path} にあります。", "integrations.kind.apply": "適用", "integrations.kind.disable": "解除", @@ -1620,7 +1657,7 @@ export const ja: Record = { "integrations.semantics.mcode": "custom_provider.opencodex のみを管理します。既定モデルと MiniMax ログインは変更しません。", "integrations.semantics.zcode": "~/.zcode/v2/config.json の provider.opencodex のみを管理します。Z.ai ログインと他のプロバイダーは変更しません。変更後は ZCode を再起動してください。", "integrations.semantics.prime": "Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。", - "integrations.semantics.aside": "サインイン中のアカウントの Aside models.json 内の providers.opencodex のみを管理します。場所は ~/.aside/u/<アカウント> です。他のプロバイダーは変更しません。Aside は実行中にこのファイルを書き換えるため、適用後は Aside を完全に終了して再度開いてください。", + "integrations.semantics.aside": "このプロファイルの ~/.aside/u//models.json 内の providers.opencodex のみを管理します。他のプロバイダーは変更しません。適用後は Aside を完全に終了してから開き直してください。", "codexAuth.mainAccount": "メインアカウント", "codexAuth.logLabel": "ログラベル", "codexAuth.codexApp": "Codex App", @@ -2314,7 +2351,21 @@ export const ja: Record = { "models.customEditBtn": "Update", "models.customEdit": "Edit", "models.customDelete": "Delete", - "models.customDeleteConfirm": "Delete the {name} model?", + "pws.manageModelVisibility": "モデルで表示を管理", + "pws.modelOwnershipLoading": "モデル定義を確認中…", + "pws.modelOwnershipFailed": "モデル定義を読み込めませんでした。変更する前に再試行してください。", + "pws.modelSelectionPending": "変更する前に モデルでモデル選択を完了してください。", + "pws.modelSaved": "カスタムモデル定義を保存しました。", + "pws.modelSavedHidden": "定義を保存しました。このモデルは非表示です。モデルで表示を管理してください。", + "pws.modelSavedRefreshPending": "定義を保存しましたが、モデルカタログを更新できませんでした。更新を再試行してください。モデルを再追加しないでください。", + "pws.modelMutationUnconfirmed": "変更を確認できませんでした。再試行する前にモデルを再読み込みしてください。", + "pws.modelRemovedRefreshPending": "変更を保存しましたが、モデルカタログを更新できませんでした。更新を再試行してください。", + "pws.modelHidden": "モデルを非表示にしました。モデルで表示を復元できます。", + "pws.modelDefinitionDeleted": "カスタム定義を削除しました。元のモデルが引き続き表示される場合があります。", + "pws.modelKnown": "このモデルはすでに登録されています。モデルで表示を管理してください。", + "models.customDeleteConfirm": "{name} のカスタム定義を削除しますか?元のネイティブモデルや検出済みモデルが再び表示される場合があります。", + "models.hide": "非表示", + "models.hideConfirm": "{name} をモデルカタログから非表示にしますか?定義は削除されず、直接ルーティングのポリシーも変わりません。", "models.customBadge": "Custom", "models.customSummary": "{count} custom", "models.customFieldModelId": "Model ID (endpoint slug)", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 21d9ab45eb..63ac304426 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -587,7 +587,21 @@ export const ko: Record = { "models.customEditBtn": "수정", "models.customEdit": "편집", "models.customDelete": "삭제", - "models.customDeleteConfirm": "{name} 모델을 삭제하시겠습니까?", + "pws.manageModelVisibility": "모델에서 노출 관리", + "pws.modelOwnershipLoading": "모델 정의 확인 중…", + "pws.modelOwnershipFailed": "모델 정의를 불러오지 못했습니다. 변경하기 전에 다시 시도하세요.", + "pws.modelSelectionPending": "변경하기 전에 모델에서 모델 선택을 완료하세요.", + "pws.modelSaved": "커스텀 모델 정의를 저장했습니다.", + "pws.modelSavedHidden": "정의를 저장했습니다. 이 모델은 숨겨져 있습니다. 모델에서 노출 상태를 관리하세요.", + "pws.modelSavedRefreshPending": "정의를 저장했지만 모델 카탈로그를 갱신하지 못했습니다. 갱신을 다시 시도하고, 모델을 중복 추가하지 마세요.", + "pws.modelMutationUnconfirmed": "변경 여부를 확인하지 못했습니다. 모델 목록을 새로고침한 뒤 다시 시도하세요.", + "pws.modelRemovedRefreshPending": "변경 사항을 저장했지만 모델 카탈로그를 갱신하지 못했습니다. 갱신을 다시 시도하세요.", + "pws.modelHidden": "모델을 숨겼습니다. 모델에서 다시 표시할 수 있습니다.", + "pws.modelDefinitionDeleted": "커스텀 정의를 삭제했습니다. 원래 모델은 계속 표시될 수 있습니다.", + "pws.modelKnown": "이미 등록된 모델입니다. 모델에서 노출 상태를 관리하세요.", + "models.customDeleteConfirm": "{name}의 커스텀 정의를 삭제하시겠습니까? 원래 네이티브 모델이나 발견된 모델이 다시 나타날 수 있습니다.", + "models.hide": "숨기기", + "models.hideConfirm": "{name}을 모델 카탈로그에서 숨기시겠습니까? 정의는 삭제되지 않으며 직접 라우팅 정책도 바뀌지 않습니다.", "models.customBadge": "커스텀", "models.customSummary": "커스텀 {count}개", "models.customFieldModelId": "모델 ID (엔드포인트 슬러그)", @@ -612,6 +626,7 @@ export const ko: Record = { "models.tipActive": "활성", "models.tipDisabled": "비활성", "models.applied": "적용됨 — 다음 Codex 턴부터 반영됩니다.", + "models.integrationRefreshWarning": "모델 선택을 저장했습니다. 일부 클라이언트의 모델 목록을 갱신하지 못했습니다. 새 세션을 시작하기 전에 연동 메뉴를 확인하세요.", "models.saveFailed": "저장 실패", "models.networkError": "네트워크 오류 — 프록시가 실행 중인가요?", "models.loadFail": "모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?", @@ -684,6 +699,26 @@ export const ko: Record = { "logs.noRequests": "아직 요청이 없습니다.", "logs.loadError": "요청 로그를 불러오지 못했습니다.", "logs.filter.surface.label": "표면", + "logs.filter.model.all": "모든 모델", + "logs.filter.provider.label": "공급자", + "logs.filter.provider.all": "모든 공급자", + "logs.filter.status.label": "상태", + "logs.filter.status.all": "모든 상태", + "logs.filter.status.success": "성공 (2xx)", + "logs.filter.status.errors": "오류 (4xx/5xx)", + "logs.filter.time.label": "시간", + "logs.filter.time.all": "전체 시간", + "logs.filter.time.15m": "최근 15분", + "logs.filter.time.1h": "최근 1시간", + "logs.filter.time.24h": "최근 1일", + "logs.filter.speed.label": "속도", + "logs.filter.speed.all": "모든 속도", + "logs.filter.speed.slow": "< 15 토큰/초", + "logs.filter.speed.medium": "15–< 50 토큰/초", + "logs.filter.speed.fast": "≥ 50 토큰/초", + "logs.filter.reset": "필터 초기화", + "logs.filter.showingCount": "{total}개 중 {count}개 표시", + "logs.noMatchingRequests": "일치하는 요청이 없습니다.", "logs.filter.surface.all": "전체", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1068,6 +1103,21 @@ export const ko: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 프로필", + "integrations.aside.profilesHint": "선택한 모델을 동기화할 프로필을 고르세요. Aside에서 사용 중인 프로필은 바뀌지 않습니다.", + "integrations.aside.all": "모든 프로필 동기화", + "integrations.aside.syncNow": "지금 동기화", + "integrations.aside.applied": "{total}개 중 {count}개 적용됨", + "integrations.aside.current": "사용 중", + "integrations.aside.profile": "프로필 {id}", + "integrations.aside.toggle": "{name} 동기화", + "integrations.aside.details": "{name} 관리", + "integrations.aside.back": "전체 Aside 프로필", + "integrations.aside.empty": "Aside를 열고 연결할 프로필을 만드세요.", + "integrations.aside.partial": "일부 프로필을 확인해 주세요. 동기화 설정은 저장됐습니다. 각 프로필의 상태를 확인하세요.", + "integrations.aside.pending": "동기화 설정 저장됨 · 파일 반영 대기 중", + "integrations.aside.retry": "{name} 다시 시도", + "integrations.aside.loadError": "Aside 프로필을 불러오지 못했습니다. 다시 시도해 상태를 확인하세요.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 연결은 프록시 서비스가 관리합니다. opencodex를 시작하면 적용되고 서비스를 중지하면 기본 라우팅으로 복원됩니다.", "integrations.codex.openService": "서비스 제어 열기", @@ -1186,6 +1236,7 @@ export const ko: Record = { "integrations.bulk.success": "적용된 클라이언트 연동을 해제했습니다.", "integrations.retention.degraded": "백업 정리가 밀려 있습니다 — 오래된 백업이 남아 있을 수 있습니다.", "integrations.error.residual": "파일이 중간 상태로 남았을 수 있습니다: {message} {path}에서 복원하세요.", + "integrations.error.residualNoSnapshot": "{message} 자동 복구를 마치지 못했습니다. 다시 시도하기 전에 클라이언트 설정을 확인하세요.", "integrations.error.recover": "{message} 백업 위치는 {path}입니다.", "integrations.kind.apply": "적용", "integrations.kind.disable": "해제", @@ -1210,7 +1261,7 @@ export const ko: Record = { "integrations.semantics.mcode": "custom_provider.opencodex만 관리하며 기본 모델과 MiniMax 로그인은 변경하지 않습니다.", "integrations.semantics.zcode": "~/.zcode/v2/config.json의 provider.opencodex만 관리하며 Z.ai 로그인과 다른 프로바이더는 변경하지 않습니다. 변경 후 ZCode를 재시작하세요.", "integrations.semantics.prime": "Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.", - "integrations.semantics.aside": "로그인된 계정의 Aside models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.aside/u/<계정>이며 다른 프로바이더는 변경하지 않습니다. Aside는 실행 중에 이 파일을 다시 쓰기 때문에 적용한 뒤 Aside를 완전히 종료하고 다시 여세요.", + "integrations.semantics.aside": "이 프로필의 ~/.aside/u//models.json에서 providers.opencodex만 관리합니다. 다른 프로바이더는 그대로 유지됩니다. 적용 후 Aside를 완전히 종료하고 다시 여세요.", "codexAuth.mainAccount": "메인 계정", "codexAuth.logLabel": "로그 라벨", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index c97b62e9ac..9f220ba2b1 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -589,7 +589,21 @@ export const ru: Record = { "models.customEditBtn": "Обновить", "models.customEdit": "Изменить", "models.customDelete": "Удалить", - "models.customDeleteConfirm": "Удалить модель {name}?", + "pws.manageModelVisibility": "Управлять видимостью в разделе «Модели»", + "pws.modelOwnershipLoading": "Проверка определений моделей…", + "pws.modelOwnershipFailed": "Не удалось загрузить определения моделей. Повторите попытку перед внесением изменений.", + "pws.modelSelectionPending": "Завершите выбор моделей в разделе «Модели» перед внесением изменений.", + "pws.modelSaved": "Определение пользовательской модели сохранено.", + "pws.modelSavedHidden": "Определение сохранено. Модель скрыта; управляйте её видимостью в разделе «Модели».", + "pws.modelSavedRefreshPending": "Определение сохранено, но каталог моделей не удалось обновить. Повторите обновление; не добавляйте модель заново.", + "pws.modelMutationUnconfirmed": "Не удалось подтвердить изменение. Обновите список моделей перед повторной попыткой.", + "pws.modelRemovedRefreshPending": "Изменение сохранено, но каталог моделей не удалось обновить. Повторите обновление.", + "pws.modelHidden": "Модель скрыта. Восстановите видимость в разделе «Модели».", + "pws.modelDefinitionDeleted": "Пользовательское определение удалено. Исходная модель может по-прежнему отображаться.", + "pws.modelKnown": "Эта модель уже известна. Управляйте её видимостью в разделе «Модели».", + "models.customDeleteConfirm": "Удалить пользовательское определение {name}? Исходная нативная или обнаруженная модель может появиться снова.", + "models.hide": "Скрыть", + "models.hideConfirm": "Скрыть {name} из каталога моделей? Определение не будет удалено, а политика прямой маршрутизации не изменится.", "models.customBadge": "Пользовательская", "models.customSummary": "Пользовательских: {count}", "models.customFieldModelId": "ID модели (slug эндпоинта)", @@ -614,6 +628,7 @@ export const ru: Record = { "models.tipActive": "Активна", "models.tipDisabled": "Отключена", "models.applied": "Применено — вступит в силу на следующем ходе Codex.", + "models.integrationRefreshWarning": "Выбор моделей сохранён. Не удалось обновить каталоги некоторых клиентов. Проверьте раздел «Интеграции» перед началом нового сеанса.", "models.saveFailed": "Не удалось сохранить", "models.networkError": "Ошибка сети — запущен ли прокси?", "models.loadFail": "Не удалось загрузить модели — запущен ли прокси?", @@ -682,6 +697,26 @@ export const ru: Record = { "logs.noRequests": "Запросов пока нет.", "logs.loadError": "Не удалось загрузить журнал запросов.", "logs.filter.surface.label": "Источник", + "logs.filter.model.all": "Все модели", + "logs.filter.provider.label": "Провайдер", + "logs.filter.provider.all": "Все провайдеры", + "logs.filter.status.label": "Статус", + "logs.filter.status.all": "Все статусы", + "logs.filter.status.success": "Успешные (2xx)", + "logs.filter.status.errors": "Ошибки (4xx/5xx)", + "logs.filter.time.label": "Время", + "logs.filter.time.all": "За всё время", + "logs.filter.time.15m": "Последние 15 мин", + "logs.filter.time.1h": "Последний час", + "logs.filter.time.24h": "Последний день", + "logs.filter.speed.label": "Скорость", + "logs.filter.speed.all": "Все скорости", + "logs.filter.speed.slow": "< 15 ток/с", + "logs.filter.speed.medium": "15–< 50 ток/с", + "logs.filter.speed.fast": "≥ 50 ток/с", + "logs.filter.reset": "Сбросить фильтры", + "logs.filter.showingCount": "Показано {count} из {total}", + "logs.noMatchingRequests": "Подходящих запросов нет.", "logs.filter.surface.all": "Все", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1534,6 +1569,21 @@ export const ru: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Профили Aside", + "integrations.aside.profilesHint": "Выберите профили, в которые будут добавлены выбранные модели. Активный профиль Aside не изменится.", + "integrations.aside.all": "Синхронизировать все профили", + "integrations.aside.syncNow": "Синхронизировать сейчас", + "integrations.aside.applied": "Применено к профилям: {count} из {total}", + "integrations.aside.current": "Текущий профиль", + "integrations.aside.profile": "Профиль {id}", + "integrations.aside.toggle": "Синхронизировать {name}", + "integrations.aside.details": "Управление: {name}", + "integrations.aside.back": "Все профили Aside", + "integrations.aside.empty": "Откройте Aside и создайте профиль для подключения.", + "integrations.aside.partial": "Некоторые профили требуют внимания. Настройки синхронизации сохранены; проверьте состояние каждого профиля.", + "integrations.aside.pending": "Настройка синхронизации сохранена; обновление файла ожидается.", + "integrations.aside.retry": "Повторить для {name}", + "integrations.aside.loadError": "Не удалось загрузить профили Aside. Повторите попытку, чтобы проверить их состояние.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Подключением Codex управляет прокси-сервис. При запуске opencodex оно применяется, а при остановке сервиса восстанавливается нативная маршрутизация.", "integrations.codex.openService": "Открыть управление сервисом", @@ -1652,6 +1702,7 @@ export const ru: Record = { "integrations.bulk.success": "Применённые интеграции клиентов отключены.", "integrations.retention.degraded": "Очистка резервных копий отстаёт; старые копии могут всё ещё находиться на диске.", "integrations.error.residual": "Файл может остаться в промежуточном состоянии: {message} Восстановите его из {path}.", + "integrations.error.residualNoSnapshot": "{message} Автоматическое восстановление не завершено. Проверьте настройки клиента перед повторной попыткой.", "integrations.error.recover": "{message} Резервная копия находится в {path}.", "integrations.kind.apply": "Применено", "integrations.kind.disable": "Отключено", @@ -1676,7 +1727,7 @@ export const ru: Record = { "integrations.semantics.mcode": "Управляет только custom_provider.opencodex. Модель по умолчанию и вход MiniMax не меняются.", "integrations.semantics.zcode": "Управляет только provider.opencodex в ~/.zcode/v2/config.json. Вход Z.ai и другие провайдеры не меняются. Перезапустите ZCode после изменений.", "integrations.semantics.prime": "Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.", - "integrations.semantics.aside": "Управляет только providers.opencodex в models.json Aside для выполнившего вход аккаунта (~/.aside/u/<аккаунт>). Другие провайдеры не меняются. Aside перезаписывает этот файл во время работы, поэтому после применения полностью закройте и снова откройте его.", + "integrations.semantics.aside": "Управляет только providers.opencodex в файле ~/.aside/u//models.json этого профиля. Другие провайдеры остаются без изменений. После применения полностью закройте и снова откройте Aside.", "codexAuth.mainAccount": "Основной аккаунт", "codexAuth.logLabel": "Метка журнала", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 7a6f5107c0..aee152cd39 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -592,7 +592,21 @@ export const tr: Record = { "models.customEditBtn": "Güncelle", "models.customEdit": "Düzenle", "models.customDelete": "Sil", - "models.customDeleteConfirm": "{name} modeli silinsin mi?", + "pws.manageModelVisibility": "Modeller bölümünde görünürlüğü yönet", + "pws.modelOwnershipLoading": "Model tanımları kontrol ediliyor…", + "pws.modelOwnershipFailed": "Model tanımları yüklenemedi. Değişiklik yapmadan önce yeniden deneyin.", + "pws.modelSelectionPending": "Değişiklik yapmadan önce Modeller bölümünde model seçimini tamamlayın.", + "pws.modelSaved": "Özel model tanımı kaydedildi.", + "pws.modelSavedHidden": "Tanım kaydedildi. Bu model gizli; görünürlüğünü Modeller bölümünde yönetin.", + "pws.modelSavedRefreshPending": "Tanım kaydedildi ancak model kataloğu yenilenemedi. Yenilemeyi tekrar deneyin; modeli yeniden eklemeyin.", + "pws.modelMutationUnconfirmed": "Değişiklik doğrulanamadı. Yeniden denemeden önce modelleri yenileyin.", + "pws.modelRemovedRefreshPending": "Değişiklik kaydedildi ancak model kataloğu yenilenemedi. Yenilemeyi tekrar deneyin.", + "pws.modelHidden": "Model gizlendi. Modeller bölümünden yeniden görünür yapabilirsiniz.", + "pws.modelDefinitionDeleted": "Özel tanım silindi. Alttaki model görünmeye devam edebilir.", + "pws.modelKnown": "Bu model zaten biliniyor. Görünürlüğünü Modeller bölümünde yönetin.", + "models.customDeleteConfirm": "{name} için özel tanım silinsin mi? Alttaki yerel veya keşfedilmiş model yeniden görünebilir.", + "models.hide": "Gizle", + "models.hideConfirm": "{name} model kataloğundan gizlensin mi? Tanımı silinmez ve doğrudan yönlendirme ilkesi değişmez.", "models.customBadge": "Özel", "models.customSummary": "{count} özel", "models.customFieldModelId": "Model ID", @@ -617,6 +631,7 @@ export const tr: Record = { "models.tipActive": "Aktif", "models.tipDisabled": "Devre Dışı", "models.applied": "Uygulandı.", + "models.integrationRefreshWarning": "Model seçimi kaydedildi. Bazı istemcilerin model katalogları yenilenemedi. Yeni bir oturum başlatmadan önce Entegrasyonlar bölümünü kontrol edin.", "models.saveFailed": "Kaydetme başarısız", "models.networkError": "Ağ hatası — proxy çalışıyor mu?", "models.loadFail": "Modeller yüklenemedi — proxy çalışıyor mu?", @@ -689,6 +704,26 @@ export const tr: Record = { "logs.noRequests": "Henüz istek yok.", "logs.loadError": "İstek günlükleri yüklenemedi.", "logs.filter.surface.label": "Yüzey", + "logs.filter.model.all": "Tüm modeller", + "logs.filter.provider.label": "Sağlayıcı", + "logs.filter.provider.all": "Tüm sağlayıcılar", + "logs.filter.status.label": "Durum", + "logs.filter.status.all": "Tüm durumlar", + "logs.filter.status.success": "Başarılı (2xx)", + "logs.filter.status.errors": "Hatalar (4xx/5xx)", + "logs.filter.time.label": "Zaman", + "logs.filter.time.all": "Tüm zamanlar", + "logs.filter.time.15m": "Son 15 dk", + "logs.filter.time.1h": "Son 1 saat", + "logs.filter.time.24h": "Son 1 gün", + "logs.filter.speed.label": "Hız", + "logs.filter.speed.all": "Tüm hızlar", + "logs.filter.speed.slow": "< 15 jeton/sn", + "logs.filter.speed.medium": "15–< 50 jeton/sn", + "logs.filter.speed.fast": "≥ 50 jeton/sn", + "logs.filter.reset": "Filtreleri sıfırla", + "logs.filter.showingCount": "{total} içinden {count} gösteriliyor", + "logs.noMatchingRequests": "Eşleşen istek yok.", "logs.filter.surface.all": "Tümü", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -720,7 +755,7 @@ export const tr: Record = { "logs.col.tokens": "Jetonlar", "logs.col.tokPerSec": "jeton/sn", "logs.col.estimatedCost": "~$", - "logs.metric.tokPerSecTitle": "Çıktı jetonu / saniye", + "logs.metric.tokPerSecTitle": "Tam istek süresince saniye başına çıktı jetonu", "logs.metric.estimatedCostTitle": "Tahmini API liste fiyatı", "usage.cost.total": "API liste fiyatı eşdeğeri", "usage.cost.disclaimer": "Fatura makbuzu değildir.", @@ -1541,6 +1576,21 @@ export const tr: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside profilleri", + "integrations.aside.profilesHint": "Seçili modellerin hangi profillere aktarılacağını seçin. Aside’ın etkin profili değişmez.", + "integrations.aside.all": "Tüm profilleri eşitle", + "integrations.aside.syncNow": "Şimdi eşitle", + "integrations.aside.applied": "{total} profilden {count} tanesine uygulandı", + "integrations.aside.current": "Geçerli profil", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "{name} profilini eşitle", + "integrations.aside.details": "{name} profilini yönet", + "integrations.aside.back": "Tüm Aside profilleri", + "integrations.aside.empty": "Bağlamak için Aside’ı açıp bir profil oluşturun.", + "integrations.aside.partial": "Bazı profillerle ilgilenmeniz gerekiyor. Eşitleme tercihleriniz kaydedildi; her profilin durumunu kontrol edin.", + "integrations.aside.pending": "Eşitleme tercihi kaydedildi; dosya güncellemesi bekleniyor.", + "integrations.aside.retry": "{name} için yeniden dene", + "integrations.aside.loadError": "Aside profilleri yüklenemedi. Durumlarını kontrol etmek için yeniden deneyin.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex bağlantısı proxy servisine aittir.", "integrations.codex.openService": "Servis kontrollerini aç", @@ -1659,6 +1709,7 @@ export const tr: Record = { "integrations.bulk.success": "Uygulanan istemci entegrasyonları devre dışı bırakıldı.", "integrations.retention.degraded": "Yedek temizliği geride kaldı.", "integrations.error.residual": "{path} konumunda {message}", + "integrations.error.residualNoSnapshot": "{message} Otomatik kurtarma tamamlanamadı. Yeniden denemeden önce istemci yapılandırmasını kontrol edin.", "integrations.error.recover": "{path} kurtarılırken {message}", "integrations.kind.apply": "Uygulandı", "integrations.kind.disable": "Devre Dışı Bırakıldı", @@ -1682,7 +1733,7 @@ export const tr: Record = { "integrations.semantics.mcode": "Yalnızca custom_provider.opencodex bölümünü yönetir. Varsayılan model ve MiniMax oturumu değişmez.", "integrations.semantics.zcode": "Yalnızca ~/.zcode/v2/config.json içindeki provider.opencodex bölümünü yönetir. Z.ai oturumu ve diğer sağlayıcılar değişmez. Değişikliklerden sonra ZCode'u yeniden başlatın.", "integrations.semantics.prime": "Yalnızca Prime Agent'ın models.json dosyasındaki providers.opencodex bölümünü yönetir — PRIME_AGENT_CODING_AGENT_DIR ayarlı değilse ~/.prime/agent. Diğer sağlayıcılar ve model geçersiz kılmaları değişmez. Yeni oturumlarda geçerli olur.", - "integrations.semantics.aside": "Yalnızca oturum açmış hesabın Aside models.json dosyasındaki providers.opencodex bölümünü yönetir (~/.aside/u/). Diğer sağlayıcılar değişmez. Aside çalışırken bu dosyayı yeniden yazar; bu nedenle uyguladıktan sonra Aside'ı tamamen kapatıp yeniden açın.", + "integrations.semantics.aside": "Yalnızca bu profilin ~/.aside/u//models.json dosyasındaki providers.opencodex bölümünü yönetir. Diğer sağlayıcılarınız değişmez. Uyguladıktan sonra Aside’ı tamamen kapatıp yeniden açın.", "integrations.semantics.omp": "Kataloğu yüklemek için OMP'yi yeniden başlatın.", "codexAuth.mainAccount": "Ana Hesap", "codexAuth.logLabel": "Günlük etiketi", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index ed0aeba2d4..39c9e2f0b3 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -455,7 +455,21 @@ export const zhTW: Record = { "models.customEditBtn": "更新", "models.customEdit": "編輯", "models.customDelete": "刪除", - "models.customDeleteConfirm": "要刪除模型 {name} 嗎?", + "pws.manageModelVisibility": "在模型中管理可見性", + "pws.modelOwnershipLoading": "正在檢查模型定義…", + "pws.modelOwnershipFailed": "無法載入模型定義。請重試後再進行變更。", + "pws.modelSelectionPending": "請先在模型中完成模型選擇,再進行變更。", + "pws.modelSaved": "自訂模型定義已儲存。", + "pws.modelSavedHidden": "定義已儲存。此模型處於隱藏狀態,請在模型中管理可見性。", + "pws.modelSavedRefreshPending": "定義已儲存,但模型目錄重新整理失敗。請重試重新整理,不要再次新增模型。", + "pws.modelMutationUnconfirmed": "無法確認變更結果。請重新整理模型列表後再試。", + "pws.modelRemovedRefreshPending": "變更已儲存,但模型目錄重新整理失敗。請重試重新整理。", + "pws.modelHidden": "模型已隱藏。可在模型中恢復顯示。", + "pws.modelDefinitionDeleted": "自訂定義已刪除。原有模型可能仍會顯示。", + "pws.modelKnown": "此模型已存在。請在模型中管理其可見性。", + "models.customDeleteConfirm": "要刪除 {name} 的自訂定義嗎?原有的原生模型或已探索到的模型可能會重新顯示。", + "models.hide": "隱藏", + "models.hideConfirm": "要從模型目錄中隱藏 {name} 嗎?這不會刪除其定義,也不會改變直接路由規則。", "models.customBadge": "自訂", "models.customSummary": "{count} 個自訂模型", "models.customFieldModelId": "模型 ID(端點標識)", @@ -480,6 +494,7 @@ export const zhTW: Record = { "models.tipActive": "已啟用", "models.tipDisabled": "已停用", "models.applied": "已套用 — 將在下一個 Codex 回合生效。", + "models.integrationRefreshWarning": "已儲存模型選擇。部分用戶端的模型目錄無法更新。開始新工作階段前,請檢查「整合」頁面。", "models.saveFailed": "儲存失敗", "models.networkError": "網路錯誤 — 代理在執行嗎?", "models.loadFail": "載入模型失敗 — 代理在執行嗎?", @@ -534,6 +549,26 @@ export const zhTW: Record = { "logs.noRequests": "暫無請求。", "logs.loadError": "無法載入請求日誌。", "logs.filter.surface.label": "介面", + "logs.filter.model.all": "所有模型", + "logs.filter.provider.label": "提供者", + "logs.filter.provider.all": "所有提供者", + "logs.filter.status.label": "狀態", + "logs.filter.status.all": "所有狀態", + "logs.filter.status.success": "成功 (2xx)", + "logs.filter.status.errors": "錯誤 (4xx/5xx)", + "logs.filter.time.label": "時間", + "logs.filter.time.all": "所有時間", + "logs.filter.time.15m": "最近 15 分鐘", + "logs.filter.time.1h": "最近 1 小時", + "logs.filter.time.24h": "最近 1 天", + "logs.filter.speed.label": "速度", + "logs.filter.speed.all": "所有速度", + "logs.filter.speed.slow": "< 15 權杖/秒", + "logs.filter.speed.medium": "15–< 50 權杖/秒", + "logs.filter.speed.fast": "≥ 50 權杖/秒", + "logs.filter.reset": "重設篩選", + "logs.filter.showingCount": "顯示 {count}/{total}", + "logs.noMatchingRequests": "沒有相符的請求。", "logs.filter.surface.all": "全部", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -2129,6 +2164,21 @@ export const zhTW: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 設定檔", + "integrations.aside.profilesHint": "選擇要接收所選模型的設定檔。Aside 目前使用的設定檔不會改變。", + "integrations.aside.all": "同步所有設定檔", + "integrations.aside.syncNow": "立即同步", + "integrations.aside.applied": "已套用 {count}/{total} 個設定檔", + "integrations.aside.current": "目前的設定檔", + "integrations.aside.profile": "設定檔 {id}", + "integrations.aside.toggle": "同步 {name}", + "integrations.aside.details": "管理 {name}", + "integrations.aside.back": "所有 Aside 設定檔", + "integrations.aside.empty": "開啟 Aside 並建立設定檔以進行連線。", + "integrations.aside.partial": "部分設定檔需要處理。同步設定已儲存,請檢查各設定檔的狀態。", + "integrations.aside.pending": "同步設定已儲存,等待更新檔案。", + "integrations.aside.retry": "重試 {name}", + "integrations.aside.loadError": "無法載入 Aside 設定檔。請重試以查看狀態。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 連線由代理服務管理。啟動 opencodex 時套用;停止服務時還原原生路由。", "integrations.codex.openService": "開啟服務控制", @@ -2247,6 +2297,7 @@ export const zhTW: Record = { "integrations.bulk.success": "已套用的用戶端整合已停用。", "integrations.retention.degraded": "備份清理進度落後;磁碟上可能仍有較舊的備份。", "integrations.error.residual": "檔案可能處於中間狀態:{message} 請從 {path} 還原。", + "integrations.error.residualNoSnapshot": "{message} 自動復原未完成。請先檢查用戶端設定,再重試。", "integrations.error.recover": "{message} 備份位於 {path}。", "integrations.kind.apply": "已套用", "integrations.kind.disable": "已停用", @@ -2271,7 +2322,7 @@ export const zhTW: Record = { "integrations.semantics.mcode": "僅管理 custom_provider.opencodex,不會變更預設模型或 MiniMax 登入狀態。", "integrations.semantics.zcode": "僅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不會變更 Z.ai 登入狀態或其他供應商。變更後請重新啟動 ZCode。", "integrations.semantics.prime": "僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。", - "integrations.semantics.aside": "僅管理已登入帳號的 Aside models.json 中的 providers.opencodex,位於 ~/.aside/u/<帳號>。不會變更其他供應商。Aside 在執行時會重寫該檔案,因此套用後請完全結束並重新開啟 Aside。", + "integrations.semantics.aside": "僅管理此設定檔的 ~/.aside/u//models.json 中的 providers.opencodex。其他供應商維持不變。套用後請完全結束並重新開啟 Aside。", "codexAuth.pinned": "已固定", "codexAuth.pinnedHint": "你手動選取了此帳號,因此較高的選擇順序不會越過它。此固定會持續到該帳號用盡、你改選其他帳號,或你變更任一選擇順序為止。", "codexAuth.requestUserInput": "在 Default 模式中要求輸入", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 44450d8785..1ba4cabfa8 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -584,7 +584,21 @@ export const zh: Record = { "models.customEditBtn": "更新", "models.customEdit": "编辑", "models.customDelete": "删除", - "models.customDeleteConfirm": "要删除模型 {name} 吗?", + "pws.manageModelVisibility": "在模型中管理可见性", + "pws.modelOwnershipLoading": "正在检查模型定义…", + "pws.modelOwnershipFailed": "无法加载模型定义。请重试后再进行更改。", + "pws.modelSelectionPending": "请先在模型中完成模型选择,再进行更改。", + "pws.modelSaved": "自定义模型定义已保存。", + "pws.modelSavedHidden": "定义已保存。此模型处于隐藏状态,请在模型中管理可见性。", + "pws.modelSavedRefreshPending": "定义已保存,但模型目录刷新失败。请重试刷新,不要再次添加模型。", + "pws.modelMutationUnconfirmed": "无法确认更改结果。请刷新模型列表后再试。", + "pws.modelRemovedRefreshPending": "更改已保存,但模型目录刷新失败。请重试刷新。", + "pws.modelHidden": "模型已隐藏。可在模型中恢复显示。", + "pws.modelDefinitionDeleted": "自定义定义已删除。原有模型可能仍会显示。", + "pws.modelKnown": "此模型已存在。请在模型中管理其可见性。", + "models.customDeleteConfirm": "要删除 {name} 的自定义定义吗?原有的原生模型或已发现模型可能会重新显示。", + "models.hide": "隐藏", + "models.hideConfirm": "要从模型目录中隐藏 {name} 吗?这不会删除其定义,也不会改变直接路由策略。", "models.customBadge": "自定义", "models.customSummary": "{count} 个自定义模型", "models.customFieldModelId": "模型 ID(端点标识)", @@ -609,6 +623,7 @@ export const zh: Record = { "models.tipActive": "已启用", "models.tipDisabled": "已禁用", "models.applied": "已应用 — 将在下一个 Codex 回合生效。", + "models.integrationRefreshWarning": "模型选择已保存。部分客户端的模型目录未能刷新。开始新会话前,请检查“集成”页面。", "models.saveFailed": "保存失败", "models.networkError": "网络错误 — 代理在运行吗?", "models.loadFail": "加载模型失败 — 代理在运行吗?", @@ -677,6 +692,26 @@ export const zh: Record = { "logs.noRequests": "暂无请求。", "logs.loadError": "无法加载请求日志。", "logs.filter.surface.label": "界面", + "logs.filter.model.all": "所有模型", + "logs.filter.provider.label": "提供商", + "logs.filter.provider.all": "所有提供商", + "logs.filter.status.label": "状态", + "logs.filter.status.all": "所有状态", + "logs.filter.status.success": "成功 (2xx)", + "logs.filter.status.errors": "错误 (4xx/5xx)", + "logs.filter.time.label": "时间", + "logs.filter.time.all": "所有时间", + "logs.filter.time.15m": "最近 15 分钟", + "logs.filter.time.1h": "最近 1 小时", + "logs.filter.time.24h": "最近 1 天", + "logs.filter.speed.label": "速度", + "logs.filter.speed.all": "所有速度", + "logs.filter.speed.slow": "< 15 令牌/秒", + "logs.filter.speed.medium": "15–< 50 令牌/秒", + "logs.filter.speed.fast": "≥ 50 令牌/秒", + "logs.filter.reset": "重置筛选", + "logs.filter.showingCount": "显示 {count}/{total}", + "logs.noMatchingRequests": "没有匹配的请求。", "logs.filter.surface.all": "全部", "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", @@ -1061,6 +1096,21 @@ export const zh: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 配置文件", + "integrations.aside.profilesHint": "选择要接收所选模型的配置文件。Aside 当前使用的配置文件不会改变。", + "integrations.aside.all": "同步所有配置文件", + "integrations.aside.syncNow": "立即同步", + "integrations.aside.applied": "已应用 {count}/{total} 个配置文件", + "integrations.aside.current": "当前配置文件", + "integrations.aside.profile": "配置文件 {id}", + "integrations.aside.toggle": "同步 {name}", + "integrations.aside.details": "管理 {name}", + "integrations.aside.back": "所有 Aside 配置文件", + "integrations.aside.empty": "打开 Aside 并创建配置文件以进行连接。", + "integrations.aside.partial": "部分配置文件需要处理。同步设置已保存,请检查各配置文件的状态。", + "integrations.aside.pending": "同步设置已保存,等待更新文件。", + "integrations.aside.retry": "重试 {name}", + "integrations.aside.loadError": "无法加载 Aside 配置文件。请重试以查看状态。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 连接由代理服务管理。启动 opencodex 时应用该连接;停止服务时恢复原生路由。", "integrations.codex.openService": "打开服务控制", @@ -1179,6 +1229,7 @@ export const zh: Record = { "integrations.bulk.success": "已禁用已应用的客户端集成。", "integrations.retention.degraded": "备份清理进度滞后;磁盘上可能仍有较旧的备份。", "integrations.error.residual": "文件可能处于中间状态:{message} 请从 {path} 恢复。", + "integrations.error.residualNoSnapshot": "{message} 自动恢复未完成。请先检查客户端配置,再重试。", "integrations.error.recover": "{message} 备份位于 {path}。", "integrations.kind.apply": "已应用", "integrations.kind.disable": "已停用", @@ -1203,7 +1254,7 @@ export const zh: Record = { "integrations.semantics.mcode": "仅管理 custom_provider.opencodex,不会更改默认模型或 MiniMax 登录状态。", "integrations.semantics.zcode": "仅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不会更改 Z.ai 登录状态或其他提供商。更改后请重启 ZCode。", "integrations.semantics.prime": "仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。", - "integrations.semantics.aside": "仅管理已登录账号的 Aside models.json 中的 providers.opencodex,位于 ~/.aside/u/<账号>。不会更改其他提供商。Aside 在运行时会重写该文件,因此应用后请完全退出并重新打开 Aside。", + "integrations.semantics.aside": "仅管理此配置文件的 ~/.aside/u//models.json 中的 providers.opencodex。其他提供商保持不变。应用后请完全退出并重新打开 Aside。", "codexAuth.mainAccount": "主账号", "codexAuth.logLabel": "日志标签", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/icons.tsx b/gui/src/icons.tsx index 6ec2ebf096..70afe03db9 100644 --- a/gui/src/icons.tsx +++ b/gui/src/icons.tsx @@ -24,6 +24,7 @@ export const IconRefresh = (p: P) => (); export const IconPlay = (p: P) => (); export const IconTrash = (p: P) => (); +export const IconEyeOff = (p: P) => (); export const IconPencil = (p: P) => (); export const IconAlert = (p: P) => (); export const IconInfo = (p: P) => (); diff --git a/gui/src/model-visibility.ts b/gui/src/model-visibility.ts index 5007d979d2..df73ff5186 100644 --- a/gui/src/model-visibility.ts +++ b/gui/src/model-visibility.ts @@ -4,6 +4,26 @@ export interface ModelVisibilityTarget { id: string; native?: boolean; } export type ModelVisibilityScope = "models" | "provider"; +export interface ClientCatalogRefreshFailure { + client: string; profileId?: number; reason?: string; refusalReason?: string; + snapshotPath?: string; residual?: boolean; +} + +/** Selection persistence and client-file refresh have separate success outcomes. */ +export function clientCatalogRefreshFailures(body: unknown): ClientCatalogRefreshFailure[] | undefined { + // Missing outcomes (old servers or preset-empty fallback) do not prove recovery. + if (!isRecord(body) || !Array.isArray(body.clientIntegrations)) return undefined; + return body.clientIntegrations.filter((row): row is Record => isRecord(row) && row.ok === false) + .map(row => ({ + client: typeof row.client === "string" ? row.client : "", + ...(Number.isSafeInteger(row.profileId) && (row.profileId as number) >= 0 ? { profileId: row.profileId as number } : {}), + ...(typeof row.reason === "string" ? { reason: row.reason } : {}), + ...(typeof row.refusalReason === "string" ? { refusalReason: row.refusalReason } : {}), + ...(typeof row.snapshotPath === "string" ? { snapshotPath: row.snapshotPath } : {}), + ...(row.residual === true ? { residual: true } : {}), + })); +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/gui/src/models-groups.ts b/gui/src/models-groups.ts index a8d6ddc69c..3e24aaf459 100644 --- a/gui/src/models-groups.ts +++ b/gui/src/models-groups.ts @@ -81,7 +81,8 @@ export function buildProviderModelGroups 0 && providerRows.every(row => row.native === true), - nativeProviderGroup: providerRows.some(row => row.native === true), + nativeProviderGroup: providerRows.some(row => row.native === true) + || (provider === "openai" && configured?.authMode === "forward"), liveModels: configured?.liveModels !== false, configuredModels: configured?.models ?? [], contextWindow: configured?.contextWindow, diff --git a/gui/src/pages/Integrations.tsx b/gui/src/pages/Integrations.tsx index b173771106..b7d7d86fc2 100644 --- a/gui/src/pages/Integrations.tsx +++ b/gui/src/pages/Integrations.tsx @@ -8,6 +8,7 @@ import Claude from "./Claude"; import Grok from "./Grok"; import CursorIntegrationPage from "./integrations/CursorIntegrationPage"; import IntegrationsOverview from "./integrations/IntegrationsOverview"; +import AsideProfilesPage from "./integrations/AsideProfilesPage"; import FileIntegrationPage, { type FileIntegrationClientId, } from "./integrations/FileIntegrationPage"; @@ -197,7 +198,7 @@ export default function Integrations({ apiBase, machineApiBase = apiBase, connec {definition.id === "grok" && } {definition.id === "cursor" && } {FILE_CLIENTS.has(definition.id as FileIntegrationClientId) && ( - : (null); - const [surfaceFilter, setSurfaceFilter] = useState("all"); - const [interceptedHelpersOnly, setInterceptedHelpersOnly] = useState(false); - const [conversationFilter, setConversationFilter] = useState(""); - const [modelFilter, setModelFilter] = useState(""); - const [conversationQueryHash, setConversationQueryHash] = useState(); + const [filters, setFilters] = useState(DEFAULT_LOG_FILTER_STATE); + const [filterClockNow, setFilterClockNow] = useState(() => Date.now()); + const filterClockRef = useRef<{ + key: string; anchor?: LogsClockAnchor; active: boolean; request: number; + }>({ key: resourceKey, active: false, request: 0 }); + // Invalidate the old resource at commit, before passive resource-loader effects. + // A late body read must not mutate this page's clock, cache or retry state. + useLayoutEffect(() => { + const clock = { key: resourceKey, active: true, request: 0 }; + filterClockRef.current = clock; + setFilterClockNow(Date.now()); + return () => { clock.active = false; }; + }, [resourceKey]); + const readFilterClockNow = useCallback(() => { + const clock = filterClockRef.current; + return logsClockNow(clock.key === resourceKey ? clock.anchor : undefined, performance.now(), Date.now()); + }, [resourceKey]); const scrollContainerRef = useRef(null); const logRetryRef = useRef<{ key: string; failures: number; nextAttemptAt: number; error: unknown }>( { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }, @@ -430,6 +449,13 @@ export default function Logs({ apiBase }: { apiBase: string }) { const selectTab = selectLogsTab; const loadLogs = useCallback(async (signal: AbortSignal): Promise => { + const clock = filterClockRef.current; + if (signal.aborted || !clock.active || clock.key !== resourceKey) { + throw signal.reason ?? new DOMException("Obsolete log request", "AbortError"); + } + const request = ++clock.request; + const isCurrent = () => !signal.aborted && clock.active + && filterClockRef.current === clock && clock.request === request; let retry = logRetryRef.current; if (retry.key !== resourceKey) { retry = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; @@ -439,14 +465,37 @@ export default function Logs({ apiBase }: { apiBase: string }) { try { const res = await fetch(`${apiBase}/api/logs?limit=2000`, { signal }); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim()); - const body = await res.json() as LogEntry[] | { logs?: LogEntry[] }; + const body = await res.json() as LogEntry[] | { logs?: LogEntry[]; generatedAt?: unknown }; + const receivedAt = performance.now(); const raw = Array.isArray(body) ? body : (body.logs ?? []); const next = raw.map(sanitizeLogEntryRouteDecision); + // The resource-store generation guard runs only after this loader returns. + // Guard these local side effects here as fetch/body readers may ignore abort. + if (!isCurrent()) throw signal.reason ?? new DOMException("Obsolete log request", "AbortError"); + // Reconcile when the accepted snapshot changes, using the latest user state + // rather than filters captured when the request started. Persist disappearance + // as All so a later ring cannot resurrect a cleared selection. + const options = extractLogFilterOptions(next); + setFilters(previous => { + const model = previous.model.trim().toLowerCase(); + const provider = previous.provider.trim().toLowerCase(); + const nextModel = model + ? options.models.find(option => option.trim().toLowerCase() === model) ?? "" + : ""; + const nextProvider = provider + ? options.providers.find(option => option.trim().toLowerCase() === provider) ?? "" + : ""; + if (previous.model === nextModel && previous.provider === nextProvider) return previous; + return { ...previous, model: nextModel, provider: nextProvider }; + }); + const sample = logsClockAnchor(Array.isArray(body) ? undefined : body.generatedAt, receivedAt); + if (sample) clock.anchor = sample; + setFilterClockNow(logsClockNow(clock.anchor, receivedAt, Date.now())); logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; writeSessionListCache(resourceKey, next); return next; } catch (error) { - if (signal.aborted) throw error; + if (!isCurrent()) throw error; const normalized = error ?? new Error("log request failed"); const failures = retry.failures + 1; const backoffMs = LOGS_POLL_INTERVAL_MS * (2 ** Math.min( @@ -472,7 +521,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { }, ); const logsState = logsResource.state; - const logs = logsState.data ?? cachedLogs ?? []; + const logs = logsState.data ?? cachedLogs ?? EMPTY_LOGS; const fetchLogs = logsResource.refresh; const retryLogs = useCallback(() => { logRetryRef.current = { key: resourceKey, failures: 0, nextAttemptAt: 0, error: null }; @@ -499,26 +548,30 @@ export default function Logs({ apiBase }: { apiBase: string }) { || (!autoRefresh && settledFailure); const detailInfo = detail ? statusCodeInfo(detail.status, locale) : null; - const conversationQuery = conversationFilter.trim(); + const conversationQuery = filters.conversationId.trim(); + + useEffect(() => { + if (filters.timeWindow === "all" || tab !== "logs") return; + setFilterClockNow(readFilterClockNow()); + const timer = window.setInterval(() => setFilterClockNow(readFilterClockNow()), LOGS_FILTER_CLOCK_INTERVAL_MS); + return () => window.clearInterval(timer); + }, [filters.timeWindow, tab, readFilterClockNow]); useEffect(() => { let cancelled = false; if (!conversationQuery) { - setConversationQueryHash(undefined); + setFilters(prev => prev.conversationQueryHash === undefined ? prev : { ...prev, conversationQueryHash: undefined }); return; } void hashLogConversationQuery(conversationQuery).then(hash => { - if (!cancelled) setConversationQueryHash(hash); + if (!cancelled) setFilters(prev => prev.conversationQueryHash === hash ? prev : { ...prev, conversationQueryHash: hash }); }); return () => { cancelled = true; }; }, [conversationQuery]); - const filteredLogs = logs.filter(log => ( - logMatchesSurface(log, surfaceFilter) - && (!interceptedHelpersOnly || Boolean(log.shadowCallRewrittenFrom)) - && logMatchesModelQuery(log, modelFilter) - && (!conversationQuery || matchesLogConversationId(log.conversationId, conversationQuery, conversationQueryHash)) - )); + const filterOptions = useMemo(() => extractLogFilterOptions(logs), [logs]); + const activeFilters = hasActiveLogFilters(filters); + const filteredLogs = useMemo(() => filterLogs(logs, filters, filterClockNow), [logs, filters, filterClockNow]); const conversationTotals = conversationQuery ? summarizeFilteredLogs(filteredLogs) : null; // TanStack Virtual returns unstable function identities; React Compiler skips this call. @@ -597,65 +650,16 @@ export default function Logs({ apiBase }: { apiBase: string }) { hidden={tab !== "logs"} > -
    - {t("logs.filter.surface.label")} -
    - {(["all", "claude", "codex", "grok"] as const).map(surface => ( - - ))} -
    - {/* - "Intercepted", not "helper". The marker only exists when Shadow Call Intercept - rewrote the request, so a helper request that was not intercepted looks exactly like - ordinary traffic here. A broader label would promise a classification this data - cannot support. - */} - - - - {conversationQuery && ( - - )} -
    + setFilters(DEFAULT_LOG_FILTER_STATE)} + /> {conversationTotals && (
    @@ -714,7 +718,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { {logsState.kind === "failed-cold" ? null : logsState.showSkeleton && logs.length === 0 ? ( ) : filteredLogs.length === 0 ? ( - + 0 && activeFilters ? t("logs.noMatchingRequests") : t("logs.noRequests")} /> ) : ( <>
    @@ -864,7 +868,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { t={t} onClose={() => setDetail(null)} onFilterConversation={id => { - setConversationFilter(id); + setFilters(prev => ({ ...prev, conversationId: id })); setDetail(null); }} /> diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index 91971cf54d..a78a57e6a8 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -10,6 +10,7 @@ import type { TFn, TKey } from "../i18n/shared"; import { modelLabel } from "../model-display"; import { formatNamespacedModelId, formatProviderDisplayName, providerDisplaySlug } from "../provider-icons"; import { readJsonIfOk, readJsonOrThrow } from "../fetch-json"; +import { describeIntegrationRefusalParts } from "./integrations/refusal-copy"; import { readSessionListCache, writeSessionListCache } from "../session-list-cache"; import { setClientResourceData } from "../client-resource"; import { createBoundedFetch } from "../bounded-fetch"; @@ -37,6 +38,8 @@ import { fetchSelectedModels, modelVisible, putModelVisibility, + clientCatalogRefreshFailures, + type ClientCatalogRefreshFailure, shouldApplyLoadGeneration, type ProviderModelMap, type ModelVisibilityScope, @@ -51,8 +54,6 @@ import { fmtK, NATIVE_CAP_OPTIONS, NATIVE_CAP_OPTION_SET, - NATIVE_GPT56_DEFAULT_WINDOW, - NATIVE_GPT56_OPT_IN_WINDOW, PAGE, readCollapsedProviders, THREAD_OPTION_SET, @@ -75,6 +76,7 @@ type CachedModelsPage = { selectedModels: ProviderModelMap; disabled: string[]; contextCaps: Record; + contextCapValues?: Record; contextCapValue: number; }; @@ -213,6 +215,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [search, setSearch] = useState>({}); const [limit, setLimit] = useState>({}); const [contextCaps, setContextCaps] = useState>(() => cached?.contextCaps ?? {}); + const [contextCapValues, setContextCapValues] = useState>(() => cached?.contextCapValues ?? {}); const [contextCapValue, setContextCapValue] = useState(() => cached?.contextCapValue ?? 350_000); const [customCap, setCustomCap] = useState(""); const [showCustom, setShowCustom] = useState(false); @@ -222,6 +225,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const [collapsed, setCollapsed] = useState>(() => initialCollapsed ?? new Set()); const needsDefaultCollapseRef = useRef(initialCollapsed === null); const [status, setStatus] = useState(""); + const [integrationFailures, setIntegrationFailures] = useState([]); const [ok, setOk] = useState(false); // Feedback generation: a repeated identical message (same success string, same validation // error) must still re-arm the toast timer. Clearing `status` alone is not enough — a @@ -244,6 +248,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; }, [status, ok, feedbackGen]); const [busy, setBusy] = useState(false); const busyRef = useRef(false); + const catalogMutationRef = useRef(false); const loadGenerationRef = useRef(0); const loadPendingRef = useRef(false); // multi_agent_v2 / ultra gate. null = endpoint unavailable (older proxy build) -> section hidden. @@ -428,6 +433,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; selectedModels: selectionData, disabled: [...nextDisabled], contextCaps: capsData.caps ?? {}, + contextCapValues: capsData.values ?? capsData.caps ?? {}, contextCapValue: nextCapValue, } satisfies CachedModelsPage; writeSessionListCache(cacheKey, next); @@ -447,6 +453,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; setSelectedModels(next.selectedModels); setContextCapValue(next.contextCapValue); setContextCaps(next.contextCaps); + setContextCapValues(next.contextCapValues ?? next.contextCaps); }, []); const catalogResource = useDataSurface( @@ -698,6 +705,8 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; targets: ModelVisibilityTarget[], enabled: boolean, ) => { + if (catalogMutationRef.current) return; + catalogMutationRef.current = true; ++loadGenerationRef.current; setBusy(true); busyRef.current = true; @@ -706,6 +715,10 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; try { const response = await putModelVisibility(apiBase, scope, provider, targets, enabled); if (!response.ok) errorKey = "models.saveFailed"; + else { + const failures = clientCatalogRefreshFailures(await response.json()); + if (failures !== undefined) setIntegrationFailures(failures); + } } catch { errorKey = "models.networkError"; } finally { @@ -719,10 +732,11 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; } setBusy(false); busyRef.current = false; + catalogMutationRef.current = false; } }; - const toggleProviderCap = async (provider: string, nativeGroup = false) => { + const toggleProviderCap = async (provider: string) => { setBusy(true); busyRef.current = true; setStatus(""); @@ -733,13 +747,12 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const r = await fetch(`${apiBase}/api/provider-context-caps`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(enabled && nativeGroup - ? { provider, enabled, value: NATIVE_GPT56_OPT_IN_WINDOW } - : { provider, enabled }), + body: JSON.stringify({ provider, enabled }), }); try { const data = await readJsonOrThrow(r, t("models.capSaveFailed")); setContextCaps(data?.caps ?? {}); + setContextCapValues(data?.values ?? data?.caps ?? {}); setOk(true); setStatus(t("models.capApplied")); await load(true); @@ -784,6 +797,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const data = await readJsonOrThrow(r, t("models.capSaveFailed")); if (typeof data?.value === "number" && Number.isFinite(data.value) && data.value > 0) setContextCapValue(data.value); setContextCaps(data?.caps ?? {}); + setContextCapValues(data?.values ?? data?.caps ?? {}); setOk(true); setStatus(t("models.capApplied")); await load(true); @@ -828,7 +842,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const onSelectProviderCap = (provider: string, raw: string) => { if (raw === CUSTOM_OPTION) { setProviderCapCustomOpen(prev => ({ ...prev, [provider]: true })); - setProviderCapCustomDraft(prev => ({ ...prev, [provider]: String(contextCaps[provider] ?? contextCapValue) })); + setProviderCapCustomDraft(prev => ({ ...prev, [provider]: String(contextCaps[provider] ?? contextCapValues[provider] ?? contextCapValue) })); return; } setProviderCapCustomOpen(prev => ({ ...prev, [provider]: false })); @@ -966,8 +980,11 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; }; const applyPreset = async (provider: string, mode: "preset" | "all") => { - if (presetBusy) return; + if (catalogMutationRef.current) return; + catalogMutationRef.current = true; setPresetBusy(provider); + setBusy(true); + busyRef.current = true; try { const bounded = createBoundedFetch(30_000); const r = await fetch(`${apiBase}/api/model-presets`, { @@ -976,12 +993,15 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; body: JSON.stringify({ provider, mode }), signal: bounded.signal, }); - const res = await readJsonIfOk<{ fallback?: string; selected?: string[] }>(r) ?? {}; + const res = await readJsonOrThrow<{ fallback?: string; selected?: string[]; clientIntegrations?: unknown }>(r, t("models.saveFailed")); + if (!res) throw new Error(t("models.saveFailed")); if (res.fallback === "preset-empty") { // Never silently narrow to nothing: the server kept the previous selection, so say so // rather than showing a success that changed nothing. publishFeedback(false, t("models.presetEmpty", { provider })); } else { + const failures = clientCatalogRefreshFailures(res); + if (failures !== undefined) setIntegrationFailures(failures); publishFeedback(true, mode === "all" ? t("models.presetClearedToast", { provider }) : t("models.presetAppliedToast", { provider, count: String(res.selected?.length ?? 0) })); @@ -991,6 +1011,9 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; publishFeedback(false, error instanceof Error ? error.message : String(error)); } finally { setPresetBusy(null); + setBusy(false); + busyRef.current = false; + catalogMutationRef.current = false; } }; @@ -1176,18 +1199,8 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; const recentForProvider = modelDiscovery?.recentArrivals[provider] ?? []; const recentIds = new Set(recentForProvider.map(row => row.id)); const capOn = contextCaps[provider] !== undefined; - const providerCap = contextCaps[provider] ?? contextCapValue; - // With the cap off, `providerCap` is only the value a future toggle would apply — for the - // native group that is the 350k default, which says nothing true about what Codex sees. - // The honest number there is the largest window the rows actually advertise. - const widestRowWindow = rows.reduce((widest, row) => { - const window = typeof row.contextWindow === "number" && row.contextWindow > 0 ? row.contextWindow : undefined; - if (window === undefined) return widest; - return widest === undefined || window > widest ? window : widest; - }, undefined); - const capDisplayValue = capOn - ? providerCap - : (nativeProviderGroup ? NATIVE_GPT56_DEFAULT_WINDOW : (widestRowWindow ?? providerCap)); + // Show the value the next enable will actually use, including a remembered selection. + const capDisplayValue = contextCaps[provider] ?? contextCapValues[provider] ?? contextCapValue; // The native group offers only the three windows GPT-5.6 actually has contracts for // (272k live, 372k legacy, 1.05M measured); routed providers keep the generic ladder. // The set has to follow the list, or a saved value outside it loses its option. @@ -1281,7 +1294,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; // control — a provider with nothing to curate would show a dead switch. const preset = presets[provider]; if (!preset) return null; - const busyHere = presetBusy === provider; + const busyHere = busy || presetBusy !== null; const stale = preset.mode === "custom" && preset.appliedVersion !== undefined && preset.appliedVersion < preset.availableVersion; @@ -1348,7 +1361,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; screen-reader user was not told this governs the context window. The number belongs to the adjacent Select, which is where a value goes (020_control_affordances.md). */} - toggleProviderCap(provider, nativeProviderGroup)} disabled={busy} label={t("models.contextCapLabel")} showLabel /> + toggleProviderCap(provider)} disabled={busy} label={t("models.contextCapLabel")} showLabel /> {/* Always rendered, disabled when the cap is off. A cap-off provider used to drop this control entirely, which is the defect the user reported: openai showed 1.05M and anthropic showed nothing, so the two rows started at @@ -2130,6 +2143,17 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; )} {/* Keep the last-good catalog interactive but make a failed revalidation explicit. */} {catalogState.showError && {t("models.loadFail")}} + {integrationFailures.length > 0 &&
    + + {t("models.integrationRefreshWarning")} +
      {integrationFailures.map(row =>
    • + {row.client}{row.profileId === undefined ? "" : `:${row.profileId}`}: {describeIntegrationRefusalParts(t, { + clientId: row.client, message: row.reason === "integration_mutation_busy" ? t("integrations.error.busy") : row.reason, + reason: row.refusalReason, snapshotPath: row.snapshotPath, residual: row.residual, + })} +
    • )}
    +
    +
    }