diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 28ea7a672f8..bfb9a1352a7 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2665,6 +2665,7 @@ dependencies = [ "tauri-utils", "tokio", "uuid", + "winreg 0.55.0", ] [[package]] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 96d14b98e3a..f162e1d8411 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -38,6 +38,10 @@ tokio = { version = "=1.45.1", features = ["sync", "time"] } [target.'cfg(target_os = "linux")'.dependencies] dbus = "=0.9.12" +# Already present in the lock graph; only Windows writes its quoted Run command. +[target.'cfg(target_os = "windows")'.dependencies] +winreg = "=0.55.0" + [profile.release] codegen-units = 1 lto = "thin" diff --git a/desktop/src-tauri/src/first_run.rs b/desktop/src-tauri/src/first_run.rs index 3fc7616b93f..cf0c5b9e87c 100644 --- a/desktop/src-tauri/src/first_run.rs +++ b/desktop/src-tauri/src/first_run.rs @@ -6,8 +6,13 @@ use tauri_plugin_autostart::ManagerExt; const MARKER: &str = "start-at-login-claimed"; /// Marker file recording that the login item names the launch-origin argument. +#[cfg(not(target_os = "windows"))] const ORIGIN_MARKER: &str = "start-at-login-origin-flag"; +// Windows must revisit registrations claimed by the earlier, unquoted writer. +#[cfg(target_os = "windows")] +const ORIGIN_MARKER: &str = "start-at-login-quoted-origin-flag"; + /// What the one-time Start at Login decision did on this launch. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StartAtLogin { @@ -63,7 +68,7 @@ pub fn apply_start_at_login_default(app: &AppHandle) -> StartAtLogin { if app.autolaunch().is_enabled().unwrap_or(false) { return StartAtLogin::AlreadyDecided; } - match app.autolaunch().enable() { + match crate::login_autostart::enable(app) { Ok(()) => StartAtLogin::Enabled, Err(_) => StartAtLogin::Unavailable, } @@ -99,7 +104,7 @@ pub fn adopt_launch_origin_argument(app: &AppHandle) { // leave a login launch showing its window forever. match app.autolaunch().is_enabled() { Ok(true) => { - if app.autolaunch().enable().is_err() { + if crate::login_autostart::enable(app).is_err() { return; } } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index b8e6e9fe7d7..0dcd286717d 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -9,6 +9,9 @@ mod first_run; mod formatting; mod identity; mod logging; +mod login_autostart; +#[cfg(any(target_os = "windows", test))] +mod windows_autostart_command; // macOS only: it exists to replace one item in a menu no other platform installs. Compiling it // elsewhere would leave its contents unreachable, which -D warnings rejects. #[cfg(target_os = "macos")] diff --git a/desktop/src-tauri/src/login_autostart.rs b/desktop/src-tauri/src/login_autostart.rs new file mode 100644 index 00000000000..0e45bbd43ce --- /dev/null +++ b/desktop/src-tauri/src/login_autostart.rs @@ -0,0 +1,45 @@ +use tauri::AppHandle; + +/// All shell-owned enables use this entry point; the plugin still owns status and disable. +#[cfg(not(target_os = "windows"))] +pub fn enable(app: &AppHandle) -> Result<(), String> { + use tauri_plugin_autostart::ManagerExt; + app.autolaunch().enable().map_err(|error| error.to_string()) +} + +#[cfg(target_os = "windows")] +pub fn enable(app: &AppHandle) -> Result<(), String> { + use winreg::enums::{HKEY_CURRENT_USER, KEY_SET_VALUE, REG_BINARY}; + use winreg::{RegKey, RegValue}; + + let executable = std::env::current_exe().map_err(|error| error.to_string())?; + let executable = executable + .to_str() + .ok_or("autostart executable path is not Unicode")?; + let command = + crate::windows_autostart_command::command(executable, crate::startup::AUTOSTART_FLAG)?; + let name = &app.package_info().name; + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + hkcu.open_subkey_with_flags( + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", + KEY_SET_VALUE, + ) + .and_then(|key| key.set_value(name, &command)) + .map_err(|error| error.to_string())?; + + // Match the plugin's explicit-enable behavior when Task Manager has an override. + if let Ok(key) = hkcu.open_subkey_with_flags( + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run", + KEY_SET_VALUE, + ) { + key.set_raw_value( + name, + &RegValue { + vtype: REG_BINARY, + bytes: vec![2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + ) + .map_err(|error| error.to_string())?; + } + Ok(()) +} diff --git a/desktop/src-tauri/src/tray.rs b/desktop/src-tauri/src/tray.rs index 479a5a90906..eb06e2dd1dc 100644 --- a/desktop/src-tauri/src/tray.rs +++ b/desktop/src-tauri/src/tray.rs @@ -208,7 +208,7 @@ pub fn install(app: &AppHandle) -> tauri::Result<()> { if enabled { let _ = app.autolaunch().disable(); } else { - let _ = app.autolaunch().enable(); + let _ = crate::login_autostart::enable(app); } } "stop-proxy" => { diff --git a/desktop/src-tauri/src/windows_autostart_command.rs b/desktop/src-tauri/src/windows_autostart_command.rs new file mode 100644 index 00000000000..4afa82b90fa --- /dev/null +++ b/desktop/src-tauri/src/windows_autostart_command.rs @@ -0,0 +1,41 @@ +/// Build the Run value without letting spaces split the executable path. +pub fn command(executable: &str, argument: &str) -> Result { + if executable.is_empty() || executable.contains(['"', '\0']) { + return Err("invalid executable path for Windows autostart"); + } + Ok(format!("\"{executable}\" {argument}")) +} + +#[cfg(test)] +mod tests { + use super::command; + + #[test] + fn quotes_program_files_path_and_keeps_launch_origin() { + assert_eq!( + command( + r"C:\Program Files\OpenCodex\opencodex-desktop.exe", + "--autostart" + ) + .unwrap(), + r#""C:\Program Files\OpenCodex\opencodex-desktop.exe" --autostart"# + ); + } + + #[test] + fn quotes_paths_without_spaces_and_preserves_unicode() { + for path in [r"C:\OpenCodex\app.exe", r"C:\用户\OpenCodex\app.exe"] { + assert_eq!( + command(path, "--autostart").unwrap(), + format!("\"{path}\" --autostart") + ); + } + } + + #[test] + fn rejects_paths_that_cannot_be_represented_in_a_run_command() { + for path in ["", "bad\"path.exe", "bad\0path.exe"] { + assert!(command(path, "--autostart").is_err()); + } + } +} diff --git a/devlog/_plan/261003_train_recovery/000_plan.md b/devlog/_plan/261003_train_recovery/000_plan.md new file mode 100644 index 00000000000..328d396f927 --- /dev/null +++ b/devlog/_plan/261003_train_recovery/000_plan.md @@ -0,0 +1,97 @@ +# Recover the integration train and prepare the production candidate + +The previous integration stopped after combining 31 contributor changes and before +integrating two completed issue implementations. Preserve those commits, review the +combined behavior in five isolated worktree lanes, repair demonstrated defects, and +land reviewed integration candidates on dev. The final platform run establishes +production readiness; this unit does not publish a release. + +Reader: the integrating maintainer, deciding what can land and what remains unverified. + +## Scope and completion contract + +- Archetype: satisfy the recovered integration specification (C4 for sensitive surfaces). +- Trigger: continuation of the interrupted train with five worktree chats and inherited subagents. +- Goal: reviewed dev integration, attributable commits, final cross-platform evidence. +- Non-goals: release/promotion/deployment/install, credential changes, unrelated backlog, + revival of policy-blocked items, native GitHub stack registration. +- Verifier: diff/ancestry and attribution inspection; focused explicit regression files; + one final root/GUI typecheck as relevant; existing structure/layout/privacy gates; + required final PR checks and final dev workflow_dispatch CI lane=all. +- Stop: only after the disposition ledger and exact-SHA CI evidence support readiness; + absent/skipped/cancelled checks stay unverified. External acceptance limits stay explicit. +- Memory artifact: this numbered unit; private drafts and raw session evidence stay in scratch. +- Outcomes: DONE with evidence, NOOP for proven already-landed items, or a named unresolved + blocker. No arbitrary time/token bound was supplied and none is invented. +- Escalation: unresolved product policy, external access, or new out-of-scope authority. + The user authorized branch preparation, PR integration and coordinator merge judgment. +- Resources: existing local git/gh identity, five lane tasks, bounded inherited subagents; + no full local suites, test:changed, prepush, dependency install or native builds. + CI dispatch only once the candidate is assembled; necessary failure repair remains in scope. + +## Dependency-ordered work phases + +1. Roadmap: lock source commits, lane ownership and verification/closure boundaries. +2. Integration: consume existing patches, review/repair each lane, reconcile the union, + publish ready A-D first, then E, and land eligible work through the maintainer integration policy. +3. Verification: run full cross-platform CI on final dev and prepare the readiness handoff. + +Detailed executable operations are in 010_integration.md and 020_verification.md. +Architecture/source-of-truth remains structure/INDEX.md and its owned documents; each +runtime repair must update its owner doc in the same change. + +## Baseline and exclusions + +Local train: da40c734a558ea32ce391613f5a0309d14cfce81. Remote dev observed: +2e3acab46e (TokenLab #6474); Grok #6482 is also already landed. The existing train +contains a historical #6474 carry; preserve history but introduce no duplicate content. + +Accepted source sets (recovered lane-a.md through lane-d.md packets): A #6474 #6463 #6445 #6470 #6417 #6254 #6382 #6416 #6461; +B #6450 #6449 #6451 #6452 #6453 #6192 #6466; +C #6459 #6479 #6119 #6455 #6477 #6457 #6460 #6415 #6335; +D #6426 #6471 #6149 #6151 #6405 #6458. +Issue implementations #6313 and #6223 are explicitly included. + +Deferred dispositions: #6378 #6143 #6472 #6370 #6076 #6301 #6381 #6436 #6336; +#5253 is superseded by the selected locale contribution. Research-only #2511 #4961 +#4198 stay deferred for unresolved policy/access. Preserve existing rationale; do not +mistake an effort estimate for a product rejection. + +Do not close #6220 without real launcher acceptance or #6473 without native Windows +acceptance. Passing source tests alone is insufficient. Other issue closures require +fresh acceptance review, not just a commit message. + +## Consultation and evidence + +Recovery explorer established the source scope and original cancellation; main checked +the original user request and follow-up dispatch. Existing lane reports and logs are +historical evidence, not current verification. The architect proposal, main decisions, +reflection, independent audit and current lane manifest are recorded in scratch and +summarized here when received. No private transcript is copied into public git. + +Architect consultation: REC-ARCH-01/04/06/07 accepted; REC-ARCH-02/03/05 amended +to keep original A-D carry ownership and put both complete issue slices in E. +REC-ARCH-08 amended to final dev dispatch after merge, preserving pre-merge PR CI. +Same architect returned ALIGNED on the three documents; its two verification gaps +were folded in: check returned headSha and require fresh comprehensive evidence after +a repaired candidate changes SHA. Independent audit remains a separate gate. + +Roadmap audit: independent reviewer PASS, no blockers. Fresh reader understood the +answer (preserve and review the recovered train), evidence (pinned git objects and +workflow/policy contracts), and next action (bounded implementation handoff). No +reader-structure repair remained. Document checker passed on numbered documents, +source object existence/parentage, workflow inputs and diff whitespace. + +## Integration cycle entry + +Previous D: roadmap 995dee54a0 locked after semantic audit and document receipt. +Direction remains to integrate five lane handoffs. The executable 010 design is +unchanged. Fresh preflight confirms the contributor train is clean, original work +remains preserved, and current dev is 2e3acab46e. Five lane tasks now own bounded +implementation/verification, with production code changes limited to demonstrated +defects and the two authorized issue implementations. Root/GUI dependency manifests +and lockfiles match an available existing dependency tree; no install is required. + +User steering: land ready work progressively. A-D is ready for its PR now; E follows +in a separate PR. Do not wait for E to publish or land A-D. Final manual lane=all +remains after both landings. diff --git a/devlog/_plan/261003_train_recovery/010_integration.md b/devlog/_plan/261003_train_recovery/010_integration.md new file mode 100644 index 00000000000..84b3572286a --- /dev/null +++ b/devlog/_plan/261003_train_recovery/010_integration.md @@ -0,0 +1,69 @@ +# Assemble and review the existing patches + +Depends on: roadmap closure. Existing production code is not recreated during roadmap. + +## Exact baseline delta + +In the coordinator checkout on codex/recover-train-261003, preserve current docs commit, +then merge da40c734a558ea32ce391613f5a0309d14cfce81, then origin/dev after a fresh fetch. +Inspect each conflict with both source hunks and the owning invariant; never concatenate +JSON or prose mechanically. Preserve the two existing union fixes 2a3652c4da and +da40c734a5. Exact inherited file deltas are the existing git objects, inspected with +`git diff 762501439442fa00ec950802e9285f72af949167...da40c734a558ea32ce391613f5a0309d14cfce81`. +No deletion or rewrite of original worktrees/branches is needed. + +Lane E adds the exact existing issue deltas from 8e2152fa30913955b6640b61b14f6bc522eb3301 +and 3fec4a4e312cefe7fe5e11f313e1b6bdefc66148, based on da40c734a5. Their authoritative +before/after patches are `git show `. The first changes Claude model-force config, +launch env, management routes, CLI, Subagents UI/tests/docs. The second changes RemoteLink +UI, all locale catalogs, its focused tests, docs and structure/remote-link.md. +These are modifications/new files exactly enumerated by `git diff-tree --no-commit-id +--name-status -r `; no speculative implementation outside those deltas is prescribed. +New defect repairs require a concrete lane plan with paths and activation evidence first. + +## Five lane ownerships + +- A: original A Responses/SSE/provider/header/log contracts and adjacent tests/docs. +- B: original B account/credit/trust/buffer boundaries and negative tests/docs. +- C: original C platform/CLI/service/shim/desktop zoom and adjacent tests/docs. +- D: original D GUI/usage/compaction/client exports/locale and adjacent tests/docs. +- E: #6313 and #6223 complete implementation, their tests and interface/doc updates. + +Each task starts from the same train snapshot in its own managed worktree and uses +inherited subagents for bounded investigation/review. Preliminary dispatch is read-only. +Only after the roadmap closes does the coordinator grant implementation. Each lane +creates its own codex/recover-train-261003- branch and returns commits; no lane +push/PR/merge/CI permission. Shared locale/layout/docs conflicts are coordinator-owned +at integration time. E resolves conflicts between its two issue patches; main resolves +conflicts between lanes. No concurrent writer touches the coordinator branch. + +## Activation and review acceptance + +A: forced-answer tool declarations, CR/LF framing, opt-in metadata/header behavior and +ordinary-path stripping; no credential headers. B: default-off credit spending, scoped +provider access, oversized catalog/image/stream bounds and trust rejection. C: quoting +and stopped-port detection, nonblocking mirror reads, stable fnm launch, zoom persistence. +D: rapid visibility toggles, weighted throughput, source-scoped compaction, editor override +preservation and exhaustive locale coverage. E: unset force preserves launch behavior; +set force reaches runtime/CLI/API/UI; invalid model rejected; RemoteLink failed/empty/retry, +manual entry, keyboard access and fingerprint confirmation remain distinct and usable. + +Run only explicit focused test files in bounded sequential batches (GUI --isolate). +New tests register both layout maps. Reuse available dependency trees without installing. +Missing dependencies are missing evidence. Coordinate typechecks to avoid resource contention. +Rendered UI evidence must be observed; keep screenshots off the PR branch and publish +through the existing pr-assets mechanism if needed for the final PR description. + +## Final integration operation + +Inspect lane diffs and negative-path evidence, fix union conflicts and generated maps, +check co-author trailers and docs ownership, record explicit security review. Preserve +required CI and existing maintainer objections. User steering now requires rolling landing: publish the verified A-D candidate first, +then E as a separate follow-up PR. Open each ready PR targeting dev with all template sections, accurate verification limits and +GUI evidence. Attach it to this chat. Revalidate live actor, base, head, reviews, membership +and check evidence before coordinator-authorized maintainer integration. Do not bypass +failed required checks. Use an ordinary PR, not native stack operations. + +User delivery correction: do not hold verified A-D behind E. Required checks gate each +PR; the full manual cross-platform run remains after the last dev landing. This changes +delivery order, not scope or verification requirements. diff --git a/devlog/_plan/261003_train_recovery/020_verification.md b/devlog/_plan/261003_train_recovery/020_verification.md new file mode 100644 index 00000000000..48939396d80 --- /dev/null +++ b/devlog/_plan/261003_train_recovery/020_verification.md @@ -0,0 +1,30 @@ +# Verify final dev and record production readiness + +Depends on: reviewed integration landing. No production code change is planned here; +any failing behavior returns to a concrete repair plan and receives fresh evidence. + +1. Read .github/workflows/ci.yml at final dev, enumerate expected jobs and supported + workflow_dispatch inputs. Confirm the selected reference resolves to the landed SHA. +2. Dispatch the existing Cross-platform CI with lane=all on dev, after all train changes + have landed. Preserve dispatch run ID/event/head SHA/attempt and every expected job. + Require the returned run headSha to equal the intended candidate. At completion, + verify dev still equals it or limit readiness explicitly to the tested SHA. +3. Watch one aggregate observation at a time with intervals appropriate for hosted jobs; + avoid parallel polling by lanes. Pending, skipped, cancelled, approval-blocked and + failed have distinct recorded outcomes. Do not call a zero-failure empty set passing. +4. Diagnose any failures from the actual job logs, distinguish baseline/environment from + changed-code failures, and repair authorized regressions through PRs. Rerun only the + failed/missing jobs only for the same SHA. A repair that changes the candidate needs + a fresh comprehensive run; do not combine passing jobs from different SHAs. +5. Record final dev SHA, merged PR, original carry/issue dispositions, security review, + executed Linux/macOS/Windows coverage, GUI evidence and residual live/native limits. + No release, npm publish, main promotion, install, or deployment is part of this unit. + +MODIFY 000_plan.md: append evidence-backed terminal summary and remaining acceptance limits. +NEW 090_outcome.md: concise reader handoff with exact commit/run links and deferred items. +Move this unit to devlog/_fin only after a real terminal outcome is recorded. Public records +contain shipped outcomes, never private vulnerability drafts or raw personal session data. + +Proposed CI command is subject to the workflow-source check before execution: +`gh workflow run ci.yml -R lidge-jun/opencodex --ref dev -f lane=all`. +This command has not run during roadmap and does not itself prove a job passed. diff --git a/devlog/_plan/261003_train_recovery/030_carry_ledger.md b/devlog/_plan/261003_train_recovery/030_carry_ledger.md new file mode 100644 index 00000000000..e60d829e65c --- /dev/null +++ b/devlog/_plan/261003_train_recovery/030_carry_ledger.md @@ -0,0 +1,69 @@ +# Recovered contributor changes + +The train preserves 31 original carry commits. #6474 already landed separately; +the original recovery scope therefore contained 30 remaining source-PR changes plus +the separately reviewed issue implementations. Three source PRs subsequently landed +independently, leaving 27 source changes for the A-D integration PR. Source PRs are carried or reimplemented, not claimed +to be merged by retaining the carry commit ancestry. + +| Source PR | Carry commit | Contributor | +| --- | --- | --- | +| #6474 | `11bbc17d9ed40ed27121a526dbe80039c2b951cd` | hedging8563 | +| #6463 | `f5572a0031471b933a4bafe0236fb509ab78ddfd` | lcxhh521 | +| #6459 | `763dda2117e6d1817f62e0c142d8a45c3b5c9c1a` | luvs01 | +| #6445 | `10975564724b93789493522154919f2f6d5e8e19` | xianhongtao | +| #6426 | `6d4e40443c451039f7d215743aea925852565c49` | andrew05060414 | +| #6479 | `b0b884b2c940ed80de3e47256bb2f878882679c1` | Yuxin-Qiao | +| #6119 | `1ad42a70aeb6448f91a8a73037042f37e2f27480` | kdm1jkm | +| #6455 | `9f5194c96dcc1a757467eefe75e16cee6f6f9380` | luvs01 | +| #6471 | `9d3e6e252a362dd01e756bb48b3af3a75225bf4d` | lcxhh521 | +| #6457 | `26755d9622a8531e242ff06ba649c39c94ab4d3a` | luvs01 | +| #6477 | `00c69c38fc43636a6cb7bcd159ccff13886afed4` | andrew05060414 | +| #6460 | `b2d27e35a263817bb0b115dac5a97ab7f9206f4a` | luvs01 | +| #6415 | `4ef04ff37b2513a4355730ee452537cda6dfb041` | andrew05060414 | +| #6335 | `c294e5811999551272cf2196301cfe8b68fcc325` | sungyongcho | +| #6470 | `a85a43755ae164a324afa7ebd05b9f6e612baa86` | lcxhh521 | +| #6417 | `ce0e7672c8b5a974b61c4f593c0ee0ce2b437dec` | arikon | +| #6254 | `60ed4c65b93e449e77b29dedf83f2cfdf3228b9c` | codingbooo | +| #6382 | `7ccc5929cce938a20f38a50533ff1d79a79dd502` | 2836048681 | +| #6416 | `2abd7341a3c761bba14f54c91128ce656ab99ed6` | xyjk0511 | +| #6461 | `2753ef9e43c445fa0843c6a50fe3bbb970344fdf` | luvs01 | +| #6450 | `ce87c68077216ad0275036315410d1a1eab9e0c0` | luvs01 | +| #6449 | `f5483034009781f5492c884b37bd39b48c38de6d` | luvs01 | +| #6451 | `1108236af701a0a5cac4d44ae99a2d20c9de2da2` | luvs01 | +| #6452 | `dbed9c7b9a9d9266f7358805dbe784acbe2f1ff6` | luvs01 | +| #6453 | `0358e72c8c8ec9a4708aff7401632454ff178a3e` | luvs01 | +| #6192 | `f2641871d0871563810b4e87a27047d670310320` | agentHits | +| #6466 | `6ef255fd067342214ffd0518d11c8ecbe0cc894d` | Hylouis233 | +| #6149 | `3e65384642dd6d02d558a425c1a78654cea24ce6` | yuanyuanlove | +| #6151 | `de828619f84659603f6a13740401049f28dd4dde` | shawn-kim-ai | +| #6405 | `56ea6d76e36370bc341ec1592a9a290a459942d4` | imranshaiedi-byte | +| #6458 | `5d7efbb749ffb72efa6696571b73a4a874a8c61d` | rriosfelipe | + +The original B/C carry messages contain credit text but lack a trailer separator. +A clean consolidated contributor block accompanies the recovery ledger commit and +will accompany the final merge message, preserving graph attribution without +rewriting the recovered history. + +Potential resolved issues after acceptance: #6425, #6309, #6464 and #6465. +The latter retains explicit per-account paid-credit opt-in. #6313 and #6223 require +their complete issue acceptance, not just the old commit close lines. #6220 remains +open pending real launcher acceptance; #6473 remains open pending native Windows +acceptance. #6406/#6290 are references only. #5253 contributes no carried content. + +Maintainer requests on #6416, #6192, #6119, #6149 and #6151 require explicit +evidence-backed dispositions. Current source-head review state is not proof of +a defect in the recovered carry, but it must not be silently discarded. + +## Progressive source landings + +Under the updated delivery direction, #6459 landed at ee2e15f86da111bfac527842aade9ad7780e15fa, +#6455 at 115fa0322cd938da846957e237d4b97b38527bf6, and #6457 at +9fb79230ba8f1df8b6af13bbb6067c359a4f9344. Each retained successful exact-head PR CI, +no unresolved review threads or maintainer objection, and a recorded owner-integration +decision. The reviewed runtime/test blobs matched the recovery carries. Source PR +changes now in dev are reconciled into the train without duplicating their content. + +The selected pt-BR contribution supersedes #5253. Although no file content from that +proposal was copied, the superseded author's trailer is included in final delivery +as required by the repository's attribution policy. diff --git a/devlog/_plan/261003_train_recovery_a/000_plan.md b/devlog/_plan/261003_train_recovery_a/000_plan.md new file mode 100644 index 00000000000..cfd21d7c4bb --- /dev/null +++ b/devlog/_plan/261003_train_recovery_a/000_plan.md @@ -0,0 +1,17 @@ +# Lane A recovery + +Repair two demonstrated edge cases in the carried train: split CRLF completion repair and a discarded web-search recovery request at the iteration ceiling. Align Portuguese decode-window wording with the existing generation metric. Preserve the working carries from `da40c734a558ea32ce391613f5a0309d14cfce81`. + +- Satisfy-spec, single bounded implementation cycle, authorized by the coordinator after roadmap `995dee54a0`. +- Scope: existing terminal-repair and web-search owners, adjacent regressions, owning contracts, one locale string. No new abstraction or configuration. +- Non-goals: pushes, PRs, merges, installs, CI dispatch, releases, live account/provider mutation, full suites, broad typechecks, builds. Coordinator owns final integration and cross-platform proof. +- Verifiers: explicit focused Bun test files, GUI `--isolate`, structure/layout/privacy checks and independent inherited review. Baseline two-file regression suite recorded privately before implementation. +- Stop: local commits with passing focused checks, independent findings addressed, and private report in `.tmp/train-recovery/`. Unavailable live/native proof remains explicit, not a success claim. +- Resource envelope: bounded sequential test batches, at most eight files each; no user-specified token/time budget. Escalate scope growth or blocked dependencies to the coordinator; do not weaken acceptance. +- Detailed change map and activation scenarios: `010_repairs.md`. Security working notes remain ignored scratch only. + +## Consultation + +The coordinator's audited roadmap assigns these existing surfaces to Lane A. Local architect consultation and independent review evidence are recorded in the private recovery report; this plan retains the bounded repair decisions and public verification scope. + +Architect `01a10042-39d1-7980-a06f-f60df848a3ee` proposed A1/A2/A3; main accepted all three without changing owners or contracts. The same architect reviewed `010_repairs.md` after those decisions were recorded and returned ALIGNED with no gaps. diff --git a/devlog/_plan/261003_train_recovery_a/010_repairs.md b/devlog/_plan/261003_train_recovery_a/010_repairs.md new file mode 100644 index 00000000000..e1f25b5481d --- /dev/null +++ b/devlog/_plan/261003_train_recovery_a/010_repairs.md @@ -0,0 +1,20 @@ +# Bounded repairs + +Depends on: coordinator roadmap `995dee54a0`, train `da40c734a5` and Lane A discovery. + +1. A1: modify `src/server/responses-terminal-repair.ts` to remember a consumed delimiter ending in CR only when its buffer is empty. Consume exactly one following LF as a delimiter continuation while forwarding its byte unchanged. Empty chunks retain pending state; any non-LF content settles it. Do not relax genuinely unframed suffix rejection or lifecycle eligibility. + Extend `tests/responses/responses-terminal-repair.test.ts`: complete terminal-less CRLF lifecycle split before final LF versus unsplit, repeated CRLF splits, empty intervening chunks, CR/CR and LF/CR delayed-LF delimiters, a second LF, bare CR, non-LF suffix, real terminal, and budget/timer release. Update `structure/transports/responses-wire-shapes.md`. +2. A2: modify `src/web-search/run-turn-loop.ts` to check the remaining iteration before empty-answer recovery side effects (retry state, warning, heartbeat and dispatch). Keep the existing cap and terminal error. Extend `tests/web-search/web-search-forced-declaration.test.ts` with search/search/search/empty at maxSearches=1: exactly four model attempts, one physical search, terminal cap error, no fifth dispatch. Also preserve recovery below the ceiling. Update `structure/runtime.md`; the fetch loop does not dispatch eagerly and needs no production change. +3. A3: modify only `logs.detail.reason.decode_window_too_short` in `gui/src/i18n/pt.ts` to describe the measured output window. Existing metric calculation and all other locales remain unchanged. + +No new persisted fields, enums or interfaces. The pending-LF flag is request-local parser state only; it is created false, set at consumed CR delimiter boundaries, consumed by the next nonempty fragment, and discarded with the relay. No serialization boundary exists. + +## Verification + +- Baseline: `bun test tests/responses/responses-terminal-repair.test.ts tests/web-search/web-search-forced-declaration.test.ts`; output in private recovery evidence. +- Red/green for added regressions, followed by `bun test tests/responses/responses-terminal-repair.test.ts tests/responses/sse-rewrite-line-endings.test.ts tests/responses/chat-sse-framing-guard.test.ts`. +- `bun test tests/web-search/web-search-forced-declaration.test.ts tests/web-search/web-search-run-turn-loop.test.ts`. +- Existing header/replay and generation-window focused regressions, including management metric consumers, within the eight-file batch limit. +- GUI locale tests with `--isolate` and `bun run lint:i18n`; no rendered layout is changed and no build is authorized. +- `bun run structure:check`, file-size ratchet, layout guards, privacy scan and `git diff --check`. +- Root/GUI typechecks and full cross-platform CI are deferred to the coordinator's frozen union. Tests only establish their exercised local/mock contracts. diff --git a/devlog/_plan/261003_train_recovery_d/000_verification.md b/devlog/_plan/261003_train_recovery_d/000_verification.md new file mode 100644 index 00000000000..c0b5a7b37e6 --- /dev/null +++ b/devlog/_plan/261003_train_recovery_d/000_verification.md @@ -0,0 +1,105 @@ +# Lane D recovery verification — 2026-10-03 + +The six accepted Lane D carries were reviewed at +`da40c734a558ea32ce391613f5a0309d14cfce81`. Focused tests and rendered fixture +scenarios found no demonstrated defect requiring a product-code repair. This +recovery adds only this verification record; it preserves the carries and their +contributor attribution. Final integration CI remains pending. + +## Accepted source + +| Source PR | Carry commit | Verified contract | +| --- | --- | --- | +| #6426 | `6d4e40443c451039f7d215743aea925852565c49` | Optimistic visibility, ordered writes, reconciliation and target cancellation | +| #6471 | `9d3e6e252a362dd01e756bb48b3af3a75225bf4d` | End-to-end output throughput uses token sums divided by duration sums | +| #6149 | `3e65384642dd6d02d558a425c1a78654cea24ce6` | Compaction source allowlists and conditional conversation disclosure | +| #6151 | `de828619f84659603f6a13740401049f28dd4dde` | Owned Droid defaults, explicit caller precedence and concrete-target validation | +| #6405 | `56ea6d76e36370bc341ec1592a9a290a459942d4` | Effective export metadata and managed OpenCode/Kilo refresh | +| #6458 | `5d7efbb749ffb72efa6696571b73a4a874a8c61d` | Brazilian Portuguese catalog, placeholders and auxiliary locale maps | + +All six commits are ancestors of the verified head. The train's existing +Portuguese zoom-key repair is included. No dropped item was revived: #6436 +retains its product hold, #6336 retains its credential-destination evidence +blocker, and #5253 remains superseded by #6458. + +## Executed verification + +Local macOS, Bun 1.4.0. Batches ran sequentially; full outputs were inspected. +The root command uses the repository test preload and sandbox: + +```sh +bun test ./tests/usage/usage-throughput.test.ts ./tests/responses/responses-compaction-override.test.ts ./tests/responses/droid-reasoning-defaults.test.ts ./tests/clients/droid-managed-reasoning-defaults.test.ts ./tests/server/management-droid-reasoning-defaults.test.ts ./tests/clients/client-export-effective-metadata.test.ts ./tests/clients/client-export-reasoning-controls.test.ts ./tests/clients/opencode-kilo-refresh.test.ts +``` + +Exit 0: **144 passed, 0 failed**, 1,175 assertions across eight files; no skips +reported. The GUI command ran from `gui/`: + +```sh +bun test --isolate ./tests/models-visibility-queue.test.tsx ./tests/usage-custom-range.test.tsx ./tests/compaction-routing-panel.test.tsx ./tests/integrations-surfaces.test.tsx ./tests/i18n-locales.test.ts ./tests/locale-parity.test.ts +``` + +Exit 0: **132 passed, 0 failed**, 4,920 assertions across six files; no skips +reported. `bun run structure:check` and `bun scripts/file-size-ratchet.ts` +also passed. Runtime locale tests confirm equal English/Portuguese key sets, +placeholder parity and registration; static extraction counted 3,769 keys each. + +## Rendered scenarios + +Real source components and production CSS ran through the existing Vite dev +server against fixture-only loopback responses. Vite inherited no proxy config; +unmatched fixture API calls failed closed. The browser used an isolated profile. +No user configuration, credentials, accounts or installed client files changed. + +- Models: repeated toggles displayed the latest intent while a write was held; + request records confirmed click order. Bulk changes reconciled, refused saves + restored authoritative state, and switching targets discarded the old draft. +- Usage: summary and tables displayed measured throughput and an unavailable + marker for missing telemetry. This metric is not decode speed. +- Compaction: a 350-model roster rendered a bounded subset. Selecting a search + result beyond the first 300 retained an existing hidden selection on save. +- Droid: the selected effort reached the review request, refused apply displayed + an error, and an external target change cleared the draft and confirmation. +- Portuguese: all four views were inspected at 390×844; document width stayed + within the viewport. Wide tables retained their local horizontal scrolling. + +Twelve screenshots were inspected, including English desktop states at 1440×1000. +Screenshots and raw logs remain outside tracked source for the integration review. +Browser execution resumed after fixture-only middleware ordering, searchbox +locator and modal-covered fixture-control corrections; it was not one uninterrupted +passing run. Passing portions were retained without repetition. Both the fixture +server and isolated browser were stopped and their loopback listeners closed. + +## Older review dispositions + +- **#6149, review5343376705:** `gui/src/i18n/ja.ts:466` and `:469` express retry + as a possibility. `gui/src/i18n/ru.ts:466` and `:469` identify target providers + as recipients of the conversation. Tests in + `gui/tests/compaction-routing-panel.test.tsx:261` and `:271` pin the conditional + Japanese meaning and both Russian scopes; all three cases passed. +- **#6151, review5341490252:** `src/server/chat-completions.ts:159` preserves the + default separately from explicit effort before translation; `:452` carries + that provenance into combo/policy dispatch. Literal validation occurs against + each target in `src/server/droid-reasoning-default.ts:25`, + `src/server/responses/core-combo.ts:719`, and + `src/server/responses/core-combo-native.ts:212`. The regression at + `tests/responses/droid-reasoning-defaults.test.ts:269` covers both empty and + incompatible nonempty first ladders across bridge, native and policy routes. + Ownership fingerprint checks in `src/integrations/state.ts:485` and the + managed-default tests reject edited or foreign rows. These cases passed. +- **#6426 scope comment:** the carry's English and Chinese dashboard-document + hunks contain only queue-behavior additions, without unrelated metadata or + reflow changes. Render evidence supports maintainer UI acceptance review; + this technical verification is not itself that approval. + +An inherited independent final reviewer read both focused logs, checked these +dispositions and fixture isolation, inspected representative screenshots, and +returned **PASS with no blocking findings**. No duplicate rewrite was needed. + +## Remaining acceptance + +Frozen-union root/GUI typechecks, production bundling and comprehensive +cross-platform CI remain coordinator-owned and pending. No full local suite, +dependency installation, native build or live provider check ran. Fixture page +rendering does not establish authenticated app-shell, packaged Tauri, real +Droid/OpenCode/Kilo wire behavior, or native-speaker Portuguese acceptance. +No security approval, release, deployment or issue closure is claimed. diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md index 7588d5526fe..83e6c7eebb2 100644 --- a/docs-site/src/content/docs/fr/guides/integrations.md +++ b/docs-site/src/content/docs/fr/guides/integrations.md @@ -198,7 +198,10 @@ une configuration cliente. **Pour `ocx opencode`, le bloc fournisseur du lanceur l'emporte.** Le lanceur injecte `provider.opencodex` par `OPENCODE_CONFIG_CONTENT`, qui est prioritaire sur la même entrée enregistrée sur -le disque ; le reste de votre configuration opencode continue de s'appliquer normalement. Le commutateur +le disque ; le reste de votre configuration opencode continue de s'appliquer normalement. Ces blocs sont +construits à partir des mêmes métadonnées canoniques effectives que les exports : capacités, choix de +raisonnement et limites connues sont repris tels quels, la limite de sortie recourant au besoin au +repli de 32000 exigé par le schéma, borné au contexte. Le commutateur décrit ici est celui qui compte lorsque vous lancez directement `opencode`. ## Depuis le terminal @@ -231,10 +234,12 @@ ocx mcode ``` Une fois l’intégration connectée, `ocx sync` et `POST /api/sync` actualisent les catalogues MCode, -Pi, Aside, Raycast et omo gérés. Le démarrage du proxy actualise aussi le catalogue Raycast géré. -Les changements de visibilité, de fournisseur ou de préréglage actualisent Pi, Aside, Raycast et omo. -Les blocs absents, modifiés par un tiers, non sûrs ou supprimés manuellement restent intacts ; -réactivez explicitement l’intégration lorsque vous souhaitez la reconnecter. +Pi, Aside, Raycast, omo, OpenCode et Kilo gérés. Le démarrage du proxy actualise aussi le +catalogue Raycast géré. Les changements de visibilité, de fournisseur ou de préréglage actualisent +Pi, Aside, Raycast, omo, OpenCode et Kilo. Les blocs absents, modifiés par un tiers, non sûrs ou +supprimés manuellement restent intacts ; réactivez explicitement l’intégration lorsque vous +souhaitez la reconnecter. Démarrez une nouvelle session Pi, OpenCode ou Kilo pour charger le +fichier actualisé. Le CLI distinct de la plateforme MiniMax (`mmx`) n’est pas une intégration à commutateur de fichier. Ses commandes textuelles utilisent le point de terminaison compatible avec Anthropic de MiniMax ; OpenCodex @@ -304,7 +309,7 @@ ocx integration client restore --op ## Kilo -Kilo n’écrit que `provider.opencodex` dans le premier fichier global existant sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire ; `kilo.jsonc` est créé si aucun candidat n’existe). Si un autre fichier candidat définit aussi `provider.opencodex`, l’état signale un conflit et Appliquer refuse. Les autres clés restent inchangées. Appliquer réécrit tout le fichier ; commentaires et virgules finales ne sont pas conservés. Sélectionnez `opencodex/` dans Kilo. +Kilo n’écrit que `provider.opencodex` dans le premier fichier global existant sous `~/.config/kilo` (`XDG_CONFIG_HOME` déplace ce répertoire ; `kilo.jsonc` est créé si aucun candidat n’existe). Si un autre fichier candidat définit aussi `provider.opencodex`, l’état signale un conflit et Appliquer refuse. Les autres clés restent inchangées. Les choix de raisonnement par modèle sont écrits sous forme de carte de variantes dans le bloc fournisseur, à partir des mêmes métadonnées canoniques effectives que l’export OpenCode. `ocx sync` et `POST /api/sync` actualisent un bloc Kilo possédé par OpenCodex ; démarrez une nouvelle session Kilo pour charger le fichier actualisé. Appliquer réécrit tout le fichier ; commentaires et virgules finales ne sont pas conservés. Sélectionnez `opencodex/` dans Kilo. Désactiver peut retirer le bloc appartenant à OpenCodex du fichier enregistré même si un autre candidat est en conflit ou ne peut pas être analysé ; cet autre fichier reste intact. diff --git a/docs-site/src/content/docs/guides/chatgpt-desktop.md b/docs-site/src/content/docs/guides/chatgpt-desktop.md index dfa3f62725b..761862d2ea3 100644 --- a/docs-site/src/content/docs/guides/chatgpt-desktop.md +++ b/docs-site/src/content/docs/guides/chatgpt-desktop.md @@ -74,11 +74,23 @@ bundled binary of the discovered bundle; if that bundle has no app-server binary Before writing the launcher, `launch` also checks that the bundle and its app-server binary are owned by you or root, are not writable by group or others, and pass strict code-signature verification under OpenAI's team ID -(`2DC432GLL2`). A bundle that fails any of these is refused, so a copy placed by -another account cannot be made to run inside your session. The launcher file is +(`2DC432GLL2`). A bundle that fails any of these checks is refused, including +one owned by another account. The launcher file is written to a temporary file and renamed into place; an existing symbolic link at that path is replaced, not followed. +`restore` applies the same ownership, permissions, and signature checks to the +bundle and its main app executable before quitting or opening it. It works with +the experimental flag off and without a bundled app-server binary. If trust +verification or relaunch fails, the existing launcher is kept for recovery. + +Both commands also check the folders containing the bundle up to the filesystem +root. Folders owned by another account, symbolic links, and ordinary group- or +world-writable parents are refused. Root-owned administrator-group installation +folders and trusted sticky folders retain their normal permissions behavior. +These are ownership, POSIX-permission, and signature checks; native ACL and +volume ownership-policy behavior has not been verified. + This integration installs no certificate, network listener, PAC, or background watcher. It does not log the app's messages or environment. Status reports whether the running ChatGPT bundle process carries the expected launcher override. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index f8d37cfb74c..49b1243c529 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -255,6 +255,8 @@ trust a local certificate authority in the login keychain. That authority is con and its subdomains. Its signing key exists only inside the running OpenCodex process, so every OpenCodex restart publishes a fresh authority and macOS asks you to trust it again — approve the prompt, or later run `ocx claude desktop picker trust`, after each restart. +Startup also attempts to remove a legacy on-disk picker signing key before checking whether +interception is enabled. Cleanup is best-effort and does not enable interception or block startup. On restart OpenCodex first removes the previous authority from the keychain. If that removal fails (for example because you decline the keychain prompt), the picker stays off for this run so two @@ -271,7 +273,9 @@ or turn it off with `ocx claude desktop picker off`. The dashboard has the same **Claude → Desktop**. After the picker profile is selected, fully quit and reopen Claude Desktop. Picker mode is part of first-party mode, so the [first-party account risk](#first-party-opt-in) -applies to it as well. +applies to it as well. Desktop and CLI catalog rewrites share bounded row and metadata limits: +if adding routed models would exceed a limit, OpenCodex returns the original Anthropic catalog +unchanged rather than publishing a partial list. ### Use opencodex models from the Desktop Code tab (first-party bindings) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index e0b148927d2..114376d265b 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -244,7 +244,9 @@ Remote and generic API clients retain the provider's hosted response format. Partial previews and URL-only results are not rendered by this compatibility layer. Artifacts use the existing retention limit, so save images you want to keep before older files are pruned. An invalid image or a failed local write produces a visible -failure message instead of a broken image link. +failure message instead of a broken image link. Display batches reject duplicate image +identities, oversized routing metadata, and output that exceeds the display byte limit; +these failures return HTTP 502 before streaming or a failed terminal event after it starts. ### Built-in image generation (`image_gen`) diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md index 03116c3125d..c2daf6ae260 100644 --- a/docs-site/src/content/docs/guides/desktop-app.md +++ b/docs-site/src/content/docs/guides/desktop-app.md @@ -66,6 +66,11 @@ if it fails, use **Retry** to resolve the current runtime again. On macOS, closing the dashboard keeps the app running in the menu bar. Open OpenCodex again from Dock or Finder to restore the dashboard without restarting the proxy. +On Windows, **Start at Login** quotes the executable path in the current-user startup +registration, including installations under `Program Files`. Previously enabled +registrations are updated once on launch. Startup entries you disabled in the tray +or Task Manager remain disabled. + ## Startup safety on macOS Startup safety reports **Desktop app** protection when OpenCodex's recorded ownership, @@ -78,6 +83,16 @@ and launcher installation or repair stays disabled while that ownership remains; Normal desktop updates replace the bundled CLI with the fixed startup probe. No local patch needs to be preserved across an update. +## Zoom + +On macOS and Linux, Cmd (macOS) or Ctrl (Linux) with `+`, `-` and `0`, or Ctrl with the mouse wheel, +zooms the window between 50% and 300% in 10% steps, and `0` returns to 100%. The sidebar shows the +same level with `-`, `+` and a reset button beside the theme and language rows. The level is +remembered and applied again the next time the app starts. The dashboard comes from the proxy the +app is attached to, so an app attached to an older proxy keeps the earlier behaviour (20% steps, not +remembered, no sidebar control) until that proxy is updated. Windows uses the browser engine's own +zoom, which is not remembered. + ## Keeping the proxy running The app keeps the proxy it started running. When that proxy restarts itself — after diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index b4488b926d8..03462e50a7a 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -56,10 +56,31 @@ Keep your chosen `modelProfile.default` to apply it when plain `gjc` starts. The GJC models with a supported reasoning-effort ladder export `reasoning: true`, `thinking.levels`, and `compat.supportsReasoningEffort`, so GJC can offer an effort choice. Native Codex models receive their standard ladder even when the catalog omits it. Models without a known ladder omit these fields; `none` and `ultra` are not offered because `none` sends no effort and `ultra` folds to `max` on the wire. Refresh the integration to update these model options. 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 -opencode V2 merges them into one provider entry. Apply, Refresh, Disable, and Restore act on both -fragments, and your other providers, agents, keybinds, and MCP entries stay untouched. +`providers.opencodex` (opencode V2). Both carry the same declared per-model reasoning choices and +defaults — the legacy block spells a model's default in its options and its choices as a variant +map, the V2 block uses the model's settings and a native variant array — so both are written and +kept in sync; they name the same provider and model ids, and opencode V2 merges them into one +provider entry. Apply, Refresh, Disable, and Restore act on both fragments, and your other +providers, agents, keybinds, and MCP entries stay untouched. + +Both fragments are built from the effective canonical model metadata — as are the Kilo export and +the `ocx opencode` launcher. An explicit custom override always wins, and a ladder you cleared to +empty stays empty; a reasoning ladder or default is never invented for a model that declares none. +An authoritative context limit is carried with an output budget beside it: a known output value — +explicit or from catalog metadata — wins, otherwise the schema-required 32000 fallback clamped to +the context, and without an authoritative context the whole limit block is omitted. Capabilities, +effort ladders, defaults, and an optional input limit appear only when known; the V2 block's +native capabilities object requires a known tools value, so when tool support is unknown it is +omitted entirely rather than written partially, and the legacy modality declarations stand. +"No ladder invented" still writes the controls, never choices: the V2 block always carries an +explicit variant array — empty when nothing is declared, because omitting it would make OpenCode +synthesize low/medium/high — and a model with known fixed reasoning but no adjustable ladder gets +disabled-only suppression instead of an effort: the legacy OpenCode block and Kilo both disable +every rung id the client would otherwise generate. Neither adds a selectable effort. The +selectable variants override the per-model +default — `none` included, offered only when the model's own declared ladder contains it. Whatever +the client selects, the proxy's pinned upstream reasoning policy still governs the request that +leaves it. Managed DSH support has a compatibility floor of **DSH 0.1.0-rc.6**. OpenCodex owns only `llm-pi-ai.providers.opencodex`; Apply and Refresh replace that fragment, Disable removes only that @@ -299,7 +320,9 @@ into any client config. **For `ocx opencode`, the launcher's provider blocks win.** That launcher injects `provider.opencodex` and `providers.opencodex` through `OPENCODE_CONFIG_CONTENT`, which outranks the same entries on disk — the rest of your opencode config still applies as -usual. The switch here is what matters when you launch `opencode` directly. +usual. Those blocks are built from the same effective canonical model metadata as the +exports, so the launcher and the managed integration describe the same models, limits, and +reasoning choices. The switch here is what matters when you launch `opencode` directly. ## From the terminal @@ -331,18 +354,18 @@ ocx mcode ``` Once connected, `ocx sync` and `POST /api/sync` refresh owned MCode, Pi, Aside, -Raycast, and omo catalogs with the current model selection, context windows, and +Raycast, omo, OpenCode, and Kilo catalogs with the current model selection, context windows, and reasoning-effort ladders. Proxy startup refreshes an owned Raycast catalog. Changes to model visibility, provider selection, or presets also refresh connected Pi, Aside, -Raycast, and omo catalogs. +Raycast, omo, OpenCode, and Kilo catalogs. Missing, foreign-edited, or unsafe blocks stay untouched, as do previously owned blocks you removed manually. An enabled Aside profile is an exception to the usual owned-only refresh: if its account directory exists and it has never had an owned block, sync may create its first block when that slot is empty. A prior Aside connection enables this behavior for all registered 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. +A refused or overlapping refresh is reported separately for each client. Start a new Pi, +OpenCode, or Kilo 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 @@ -545,7 +568,8 @@ press Save: - The same value is written to `codex.agents..model` in `~/.omo/omo.jsonc`, which LazyCodex 5.1.1 and later reads. If that file does not exist it is not created. If it contains comments it is left untouched, because saving would remove them; the tab says so, and you can - set the value there by hand. + set the value there by hand. Symlinks and non-regular files are rejected; on macOS and Linux, + a FIFO is rejected without waiting for a writer. A skipped mirror does not undo the role-file save. Nothing happens until you press Save; syncing or restarting opencodex never changes a role file. New Codex sessions pick up the change. The same controls exist on the command line: @@ -600,14 +624,18 @@ while another candidate conflicts or cannot be parsed; the other candidate is le The owned fragment is only `provider.opencodex` (OpenCode V1 shape: `npm`, `options`, `models`). Kilo's published schema has no OpenCode V2 `providers` key, so that block is -not emitted. `$schema`, `model`, `enabled_providers`, MCP, and other keys stay +not emitted; Kilo instead receives the per-model reasoning choices as a variant map inside +its provider block, built from the same effective canonical metadata as the OpenCode +export. `$schema`, `model`, `enabled_providers`, MCP, and other keys stay user-owned. Select `opencodex/` in Kilo after applying. Loopback uses `{env:OPENCODEX_KILO_API_KEY}` as `options.apiKey`. A non-loopback bind moves admission to `options.headers["x-opencodex-api-key"]` and never serializes a real key. Apply rewrites the whole global file as pretty JSON, so comments and trailing -commas in other keys are not preserved. Kilo is not on the implicit catalog fan-out; -refresh it explicitly after changing the routed model selection. +commas in other keys are not preserved. Kilo is on the implicit catalog fan-out: `ocx +sync`, `POST /api/sync`, and visibility, provider, or preset changes refresh an owned Kilo +block under the same safe owned-only rules as every other client, and a new Kilo session +loads the updated file. ```bash ocx integration client enable --client kilo @@ -623,6 +651,30 @@ Chat Completions endpoint. Choose a row from Droid's `/model` picker. Disable removes the managed rows; Undo restores the exact saved file. Other settings and custom models remain yours. +Open **Integrations → Factory Droid** (`/#integrations/droid`) to set a reasoning +default for each connected model. Choose from the model's supported efforts, +review the changes, then confirm. **No default** clears that model's draft setting; +use **Save / review changes** and confirm to apply the removal. +Models without a declared effort list show that no default is available. + +The default applies only when Droid omits an effort from its request. An explicit +request effort takes precedence over this default; existing OpenCodex pins and +caps still apply. Droid may continue to display **Dynamic** even when OpenCodex +applies the configured default. Requests without the Droid default header are +unaffected; the header is a request preference, not proof of client identity. +For combos and routing policies, each concrete target checks the preference +against its own effort list, so an incompatible first target does not remove it +from a compatible fallback. + +A saved default that the routed model no longer supports is ignored for requests. +Clear it in the panel, then review and confirm the change to remove it from Droid. + +Refresh preserves defaults while the exact `provider/model` selector remains +connected. Renaming a provider, model, or combo alias replaces that managed row +and clears its default; choose a default for the renamed row again. Disable +removes the defaults with the managed model rows, and Undo restores the saved +rows and their defaults together. + Models whose IDs or display names contain `,` or `]` are skipped because the managed selector cannot address them safely; export and managed settings show the same rows. A nonempty catalog with no addressable models is refused. diff --git a/docs-site/src/content/docs/guides/minimax.md b/docs-site/src/content/docs/guides/minimax.md index b09247dabfc..03a837406e8 100644 --- a/docs-site/src/content/docs/guides/minimax.md +++ b/docs-site/src/content/docs/guides/minimax.md @@ -12,6 +12,11 @@ the protocol boundary it actually exposes: ## MiniMax Code +In the OpenCodex Models page, both MiniMax Coding Plan regions advertise image input for +`MiniMax-M3` and `MiniMax-M3.1-Flash-Preview`. A Combo's **Image / multimodal** switch is +available only when every selected model supports image input. The switch controls image +attachments; it does not enable video uploads. + Install and sign in to MiniMax Code using MiniMax's instructions first. Then start OpenCodex and connect the reversible file integration: diff --git a/docs-site/src/content/docs/guides/model-routing.md b/docs-site/src/content/docs/guides/model-routing.md index acb84ddd43e..4ccfde4b97a 100644 --- a/docs-site/src/content/docs/guides/model-routing.md +++ b/docs-site/src/content/docs/guides/model-routing.md @@ -143,3 +143,14 @@ Routing and catalog visibility are separate controls: name (e.g. `anthropic` or `groq`) is actually configured. See [Configuration](/reference/configuration/) for the provider fields these rules read. + + +## Structured output on Cursor routes + +Cursor routes translate Responses `text.format` (`json_object` or `json_schema`) into explicit +final-answer instructions in the system context and active request, including tool-result +continuations. This is a prompt fallback: Cursor does not provide native constrained JSON decoding +on this transport, and models can still return an invalid answer. The adapter does not turn prose +into an approval decision. Auto-review callers should validate the returned JSON and use a +structured-output-capable route when strict enforcement is required. Ordinary text requests and +intermediate tool calls keep their existing behavior. diff --git a/docs-site/src/content/docs/guides/opencode.md b/docs-site/src/content/docs/guides/opencode.md index 09af3339c78..cc61a911c53 100644 --- a/docs-site/src/content/docs/guides/opencode.md +++ b/docs-site/src/content/docs/guides/opencode.md @@ -17,8 +17,8 @@ ocx opencode This ensures the proxy is running and launches opencode with the generated `provider.opencodex` and `providers.opencodex` blocks injected for that process — the -legacy spelling opencode V1 reads, and the V2 spelling that carries the reasoning-effort -variants. Extra arguments pass through: +legacy spelling opencode V1 reads, and the native V2 spelling. Both carry reasoning-effort +variants in the format their client reads. Extra arguments pass through: `ocx opencode run "hello"`. Routed models appear in the picker under the `opencodex` provider: @@ -31,32 +31,34 @@ opencodex/gpt-5.6-sol # native slugs stay unprefixed ## Reasoning effort opencode exposes reasoning effort as model *variants*. opencodex writes one variant per -declared effort for every model that advertises a ladder — `none` is skipped, because the -chat ingress has no wire effort for it — so the effort is selectable in opencode's model -picker instead of being pinned by the proxy. +declared effort, including `none` when the model explicitly supports disabling reasoning. +The selection reaches the proxy as a reasoning effort, not just a picker label. Existing +upstream effort pins in opencodex still apply. Two provider blocks are generated for this: | Block | Read by | Carries variants | |---|---|---| -| `provider.opencodex` | opencode V1 (`npm` + `options`) | no | -| `providers.opencodex` | opencode V2 (`package` + `settings`) | yes | +| `provider.opencodex` | opencode V1 (`npm` + `options`) | legacy options map | +| `providers.opencodex` | opencode V2 (`package` + `settings`) | native settings array | -Only the V2 spelling applies `variants`; a variant written under the legacy block is parsed -and then dropped, which is why both blocks are emitted. They name the same provider and -model ids, and opencode V2 merges them into a single provider entry, so no model appears -twice in the picker. Models with no declared effort ladder carry no `variants` key at all. +V2 uses its native variant array rather than the legacy map. The blocks name the same provider +and model IDs and merge into one provider entry. No model appears twice. V2 always writes a +variant array, empty when no choices are declared, to suppress automatic low/medium/high +choices. Known fixed-depth reasoners in the legacy block disable every automatically generated +rung for the same reason; those disabled entries are not selectable efforts. -No model-level default effort is written. The proxy keeps applying its own configured -default whenever a request carries no effort, so a default you change in opencodex stays -in force instead of being frozen into the config. +A known model default is exported; selecting a variant overrides that client default. +No default is invented when the catalog declares none. The launcher regenerates these +settings each time; refresh a managed integration or re-export a manual snapshot after +changing the default in opencodex. ## Images and attachments opencode decides whether a model takes an image from the model entry itself, and it cannot ask models.dev about `opencodex` — this provider is not there. The generated blocks therefore -carry opencode's own per-model capability fields, `attachment` and `modalities`, taken from -the metadata the proxy reports at `GET /api/models`: +carry per-model capability fields taken from the effective metadata the proxy reports at +`GET /api/models`. For example, the legacy block carries `attachment` and `modalities`: ```json "gpt-5.6-luna": { @@ -67,17 +69,21 @@ the metadata the proxy reports at `GET /api/models`: } ``` -Without those fields opencode assumes the model is text-only and refuses the paste on the -client side, so the image never reaches the proxy. That applies to text-only models too: when +V2 receives native `capabilities.input` and `capabilities.output` fields; known tool support +uses `capabilities.tools`. Legacy clients read the fields in the example above. When the catalog reports image input for a model the vision sidecar covers, opencode lets the attachment through so the sidecar can describe it before the upstream call. -What is written comes from the row's declared input modalities in `GET /api/models`. For a -discovered model the catalog adds `image` itself for the sidecar case; a custom row is written -from the modalities stored on it, so a custom entry that declares text only stays text-only -even when the sidecar would cover it. A row that declares none — an undeclared custom model, -for example — keeps the plain entry (`name`, plus `limit` when its context window is known), -and opencode treats it as text-only. +V2 requires a `tools` boolean whenever the native capabilities object exists. When tool support +is unknown, that object is omitted; known input modalities still reach V2 through the legacy +block's migration. This avoids inventing tool support or invalidating the provider's native settings. + +Unknown capabilities leave OpenCode's own fallback assumptions in place; they are not detected +provider facts. Explicit text-only metadata prevents image requests on the client side. + +Custom models inherit known catalog metadata unless an explicit override is stored. A custom +entry declaring text only stays text-only. Unknown capabilities are omitted rather than +invented; an explicit empty reasoning ladder does not declare the model incapable of reasoning. ## Your own config is never modified @@ -129,7 +135,9 @@ ocx export --client opencode --out ~/opencodex-opencode.json ::: Unlike the launcher's runtime block, a merged block is a static snapshot: it does not follow your -catalog. Re-run `ocx export` after you add a provider or change model visibility. +catalog. Re-run `ocx export` after you add a provider or change model visibility or metadata. +Alternatively, [enable the managed integration](/guides/integrations/): `ocx sync` safely +refreshes its already-owned blocks, leaving hand-edited or removed blocks untouched. Once merged, export the admission key before launching opencode — unless the proxy is on loopback, where none is needed: @@ -187,6 +195,9 @@ does not, the whole `limit` block is omitted and opencode keeps its own defaults Output limits use the model’s known maximum from catalog or generated metadata. Only unknown limits fall back to `32000`. The output limit is always clamped to the context window, including known limits below `32000`. +Known input limits are also carried and clamped to context; unknown input limits are omitted. +CLI export, dashboard export, managed refresh and the launcher share the effective metadata. + The `opencodex` provider block is regenerated on every launch, so per-model tweaks made inside it will not survive. Keep custom entries under a provider key of your own instead. diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 3a4c01848ff..d94e830e2a5 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -154,11 +154,20 @@ fallback until a valid sample is available. Speed uses output tokens per second 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. +The request detail also shows **Decode rate (est.)**. When the proxy observed both ends, it is +output tokens over the generation window: from the first output item or block, reasoning included, +to the last output delta. Older rows without that window use the time after the first visible +token instead. Both are proxy-side observations, not the provider's internal token timing, so the +value is always an estimate, and a window under one second shows as unavailable. The end-to-end +tok/s column and speed filter above are not affected. + 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. +The language picker includes **Português** (Brazilian Portuguese, `pt-BR`). Portuguese browser languages select it automatically unless a supported language preference is already saved. + ### Linking to a section There is a single layout, so there is no layout switch to configure. Dashboard sections are @@ -172,6 +181,8 @@ For a custom usage interval, the server must confirm the exact requested start a If an older running proxy does not support those bounds, the dashboard and CLI reject its report; upgrade and restart that proxy before retrying. Resetting a manual model price affects only that model, preserving other rates saved independently. +The **Usage** summary and Models/Providers tables show end-to-end output throughput from usage history for the selected range and filters: summed output tokens divided by summed wall-clock seconds, not an average of individual rates. The sample count is measured attempts (or requests for legacy rows without attempts); samples lacking positive finite output tokens or duration are excluded. An em dash means no sample qualified. This includes pre-decode waiting and is not estimated decode speed. + The **Usage** Models and Providers tables show the estimated priced portion for each row. Requests without a matching price or usable usage are counted as excluded beside that amount when the proxy reports pricing coverage fields. A row with only excluded requests shows an em dash with that count @@ -201,6 +212,11 @@ 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. +Switches respond immediately so you can keep changing models while saves run in the background in +click order. Saved feedback appears after the queue finishes and the list is reconciled with the +server. Failed saves restore the server's state when it can be read and show an error. Wait for that +feedback before leaving Models or changing servers: unsent queued changes are discarded on departure. + ### Managing models in a provider workspace In a provider’s **Models** tab, **Delete** removes the stored custom definition. An underlying diff --git a/docs-site/src/content/docs/ja/guides/integrations.md b/docs-site/src/content/docs/ja/guides/integrations.md index 15e2f8ba560..5012d52484e 100644 --- a/docs-site/src/content/docs/ja/guides/integrations.md +++ b/docs-site/src/content/docs/ja/guides/integrations.md @@ -39,7 +39,7 @@ modelProfile: 対応する推論負荷の段階を持つ GJC モデルは、`reasoning: true`、`thinking.levels`、`compat.supportsReasoningEffort` を出力し、GJC で負荷を選べるようにします。ネイティブ Codex モデルでは、カタログに段階がなくても標準の段階を出力します。段階が不明なモデルではこれらの項目を省略します。`none` は負荷を送信せず、`ultra` は送信時に `max` に変換されるため、選択肢には含めません。モデルの選択肢を更新するには統合を更新してください。 -管理対象の OpenCode 統合は、`provider.opencodex`(opencode V1)と `providers.opencodex`(opencode V2)の 2 つの部分を所有します。モデルごとの推論負荷の選択肢は V2 ブロックだけに含まれるため、両方を書き込んで同期します。両者は同じプロバイダー ID とモデル ID を指定し、opencode V2 は 1 つのプロバイダー項目に統合します。Apply、Refresh、Disable、Restore は両方に作用し、他のプロバイダー、エージェント、キー割り当て、MCP 項目には触れません。 +管理対象の OpenCode 統合は、`provider.opencodex`(opencode V1)と `providers.opencodex`(opencode V2)の 2 つの部分を所有します。両ブロックとも、宣言されたモデルごとの推論負荷の選択肢と既定値を同じ内容で持ちます。レガシーブロックではモデルの options の既定値とバリアントマップで、V2 ブロックではモデルの settings とネイティブなバリアント配列で表現され、両方が書き込まれて同期します。両者は同じプロバイダー ID とモデル ID を指定し、opencode V2 は 1 つのプロバイダー項目に統合します。Apply、Refresh、Disable、Restore は両方に作用し、他のプロバイダー、エージェント、キー割り当て、MCP 項目には触れません。これらのモデル定義は、Kilo のエクスポートや `ocx opencode` ランチャーと同じ実効的な正規モデルメタデータから作られます。明示的なカスタム上書きは常に優先され、空にクリアされた段階はそのまま維持され、段階も既定値も捏造されません。信頼できるコンテキスト上限が携帯され、その横に出力上限が並びます。出力は既知の値(明示的またはカタログメタデータ)が優先され、未知の場合はスキーマ必須の 32000 フォールバック(コンテキストにクランプ)が使われ、信頼できるコンテキストがなければ limit ブロック自体が省略されます。機能、負荷の段階、既定値、任意の入力上限は既知の場合にのみ書き込まれます。V2 ブロックのネイティブな capabilities オブジェクトには既知の tools 値が必要なため、ツール対応が不明な場合は一部分だけを書くのではなくオブジェクトごと省略され、レガシー側のモダリティ宣言はそのまま残ります。段階を捏造しない代わりに制御だけを書きます。V2 ブロックは常に明示的なバリアント配列を持ち、宣言がない場合は空になり、省略すると OpenCode が low/medium/high を合成するためです。固定された推論は既知だが調整可能な段階がないモデルには、選択可能な負荷ではなく無効化専用の抑止が書かれます。レガシー OpenCode ブロックも Kilo も、クライアントが本来生成する各段階 ID をすべて無効化します。どちらも選択可能な負荷は追加しません。選択可能なバリアント(モデル自身の段階が `none` を宣言している場合の `none` を含む)は、既知の場合に書き込まれる既定の推論負荷を上書きできます。クライアントが何を選んでも、プロキシの上流向けに固定されたポリシーは引き続き実際のリクエストに適用されます。 管理対象の DSH 統合には **DSH 0.1.0-rc.6** 以降が必要です。OpenCodex が所有するのは `llm-pi-ai.providers.opencodex` のみです。Apply と Refresh はその部分を置き換え、Disable はその部分のみを削除し、Restore は記録されたスナップショットを戻します。DSH はプロバイダー変更をホットリロードします。これらの操作はユーザーのデフォルトモデルやネイティブの `deepseek-official` プロバイダーを変更しません。管理対象の DSH 統合は現在ループバック専用で、実際の認証情報は書き込みません。 @@ -115,7 +115,7 @@ TOML の日付と時刻も、管理対象の書き換えでは拒否されます **Kimi Code は環境変数への参照を保持できません。** そのため設定にはキーではなく `opencodex-loopback` プレースホルダーが入ります。どのクライアント設定にも実際の認証情報は書き込みません。 -**`ocx opencode` ではランチャーのプロバイダーブロックが優先されます。** このランチャーは `OPENCODE_CONFIG_CONTENT` を通じて `provider.opencodex` と `providers.opencodex` を注入し、ディスク上の同じ項目より優先します。他の opencode 設定は通常どおり適用されます。このスイッチが重要になるのは `opencode` を直接起動する場合です。 +**`ocx opencode` ではランチャーのプロバイダーブロックが優先されます。** このランチャーは `OPENCODE_CONFIG_CONTENT` を通じて `provider.opencodex` と `providers.opencodex` を注入し、ディスク上の同じ項目より優先します。他の opencode 設定は通常どおり適用されます。これらのブロックはエクスポートと同じ実効的な正規モデルメタデータから構築されるため、ランチャーと管理対象の統合は同じモデル、上限、推論の選択肢を記述します。このスイッチが重要になるのは `opencode` を直接起動する場合です。 ## ターミナルから操作する @@ -144,9 +144,9 @@ ocx integration client enable --client mcode ocx mcode ``` -接続後、`ocx sync` と `POST /api/sync` は、所有する MCode、Pi、Aside、Raycast、omo のカタログを現在のモデル選択、コンテキストウィンドウ、推論負荷の段階で更新します。プロキシ起動時には所有する Raycast カタログを更新します。モデルの表示設定、プロバイダーの選択、プリセットの変更でも、接続済みの Pi、Aside、Raycast、omo カタログが更新されます。存在しないブロック、他者が編集したブロック、安全でないブロック、および手動で削除した以前の所有ブロックには触れません。 +接続後、`ocx sync` と `POST /api/sync` は、所有する MCode、Pi、Aside、Raycast、omo、OpenCode、Kilo のカタログを現在のモデル選択、コンテキストウィンドウ、推論負荷の段階で更新します。プロキシ起動時には所有する Raycast カタログを更新します。モデルの表示設定、プロバイダーの選択、プリセットの変更でも、接続済みの Pi、Aside、Raycast、omo、OpenCode、Kilo カタログが更新されます。存在しないブロック、他者が編集したブロック、安全でないブロック、および手動で削除した以前の所有ブロックには触れません。 -有効な Aside プロファイルは、通常の「所有済みのみ更新」の例外です。アカウントディレクトリが存在し、まだ所有ブロックがなく、その場所が空であれば、同期時に最初のブロックを作れます。以前の Aside 接続があれば、登録済みの全プロファイルでこの動作がデフォルトで有効になります。同期は存在しないアカウントディレクトリを作らず、手動のブロックも置き換えません。拒否または重複する更新は、クライアントごとに別々に報告されます。更新されたファイルを読み込むには、新しい Pi セッションを開始するか、Aside を完全に終了して開き直してください。Aside の更新には[対応する稼働中のプロキシ](#aside-プロファイルの管理)が必要です。 +有効な Aside プロファイルは、通常の「所有済みのみ更新」の例外です。アカウントディレクトリが存在し、まだ所有ブロックがなく、その場所が空であれば、同期時に最初のブロックを作れます。以前の Aside 接続があれば、登録済みの全プロファイルでこの動作がデフォルトで有効になります。同期は存在しないアカウントディレクトリを作らず、手動のブロックも置き換えません。拒否または重複する更新は、クライアントごとに別々に報告されます。更新されたファイルを読み込むには、新しい Pi、OpenCode、Kilo セッションを開始するか、Aside を完全に終了して開き直してください。Aside の更新には[対応する稼働中のプロキシ](#aside-プロファイルの管理)が必要です。 Models に **“Model selection saved”** とクライアント更新の警告が一緒に表示された場合、モデルの選択自体はすでに保存されていますが、1 件以上のクライアントファイルを更新できていません。警告には該当するクライアントと、必要に応じて Aside プロファイル、拒否理由が示されます。新しいセッションを始める前に **Integrations** で対象を確認してください。報告された問題を解消して `ocx sync` を再試行します。重複する操作は先に完了させてください。警告にバックアップパスがある、または復旧が未完了と表示される場合は、再試行前にその状態を調べてください。モデル選択の保存成功だけでは、クライアントファイルの復旧は確認できません。 @@ -224,9 +224,9 @@ Kilo CLI、VS Code、JetBrains は同じグローバル設定を共有します Kilo はこれらのグローバルファイルをすべてマージします。別の候補も `provider.opencodex` を定義する場合、状態に競合ファイルが表示され、適用と置換は拒否されます。有効化する前に、そのファイルから `provider.opencodex` を削除してください。所有済みファイルの無効化は競合があっても実行できます。読み取れない候補や安全に扱えない候補も書き込みを妨げます。 -管理対象は OpenCode V1 形式の `provider.opencodex`(`npm`、`options`、`models`)だけです。OpenCode V2 の `providers` は出力しません。`$schema`、`model`、`enabled_providers`、MCP などのキーはユーザーが管理します。適用後、Kilo で `opencodex/` を選択してください。 +管理対象は OpenCode V1 形式の `provider.opencodex`(`npm`、`options`、`models`)だけです。OpenCode V2 の `providers` は出力しません。代わりに、モデルごとの推論負荷の選択肢は、プロバイダーブロック内のバリアントマップとして、OpenCode エクスポートと同じ実効的な正規メタデータから書き込まれます。`$schema`、`model`、`enabled_providers`、MCP などのキーはユーザーが管理します。適用後、Kilo で `opencodex/` を選択してください。 -ループバックでは `options.apiKey` に `{env:OPENCODEX_KILO_API_KEY}` を使います。ループバック以外へのバインドでは認証を `options.headers["x-opencodex-api-key"]` に移し、実際のキーは保存しません。適用時はグローバルファイル全体を整形済み JSON として書き直すため、他のキーのコメントと末尾カンマは保持されません。Kilo は自動カタログ更新の対象外です。ルーティング対象のモデル選択を変更したら、明示的に更新してください。 +ループバックでは `options.apiKey` に `{env:OPENCODEX_KILO_API_KEY}` を使います。ループバック以外へのバインドでは認証を `options.headers["x-opencodex-api-key"]` に移し、実際のキーは保存しません。適用時はグローバルファイル全体を整形済み JSON として書き直すため、他のキーのコメントと末尾カンマは保持されません。Kilo は自動カタログ更新の対象です。`ocx sync`、`POST /api/sync`、および表示設定・プロバイダー・プリセットの変更は、他のクライアントと同じ所有ブロックのみを対象にする安全な規則で所有済みの Kilo ブロックを更新し、新しい Kilo セッションが更新後のファイルを読み込みます。 ```bash ocx integration client enable --client kilo diff --git a/docs-site/src/content/docs/ko/guides/integrations.md b/docs-site/src/content/docs/ko/guides/integrations.md index e39df9ea9da..5f27b57b392 100644 --- a/docs-site/src/content/docs/ko/guides/integrations.md +++ b/docs-site/src/content/docs/ko/guides/integrations.md @@ -39,7 +39,7 @@ modelProfile: 지원되는 추론 강도 단계가 있는 GJC 모델은 `reasoning: true`, `thinking.levels`, `compat.supportsReasoningEffort`를 내보내 GJC에서 강도를 선택할 수 있게 합니다. 네이티브 Codex 모델은 카탈로그에 단계가 없어도 표준 단계를 내보냅니다. 알려진 단계가 없는 모델은 이 필드들을 생략합니다. `none`은 강도를 보내지 않고 `ultra`는 전송 시 `max`로 바뀌므로 선택지에서 제외합니다. 모델 옵션을 갱신하려면 연동을 새로 고치세요. -관리형 OpenCode 연동은 `provider.opencodex`(opencode V1)와 `providers.opencodex`(opencode V2) 두 조각을 소유합니다. 모델별 추론 강도 변형은 V2 블록에만 있으므로 둘 다 기록하고 동기화합니다. 두 블록은 같은 프로바이더와 모델 ID를 가리키고 opencode V2는 이를 프로바이더 항목 하나로 병합합니다. Apply, Refresh, Disable, Restore는 두 조각 모두에 작용하며 다른 프로바이더, 에이전트, 단축키, MCP 항목은 유지됩니다. +관리형 OpenCode 연동은 `provider.opencodex`(opencode V1)와 `providers.opencodex`(opencode V2) 두 조각을 소유합니다. 두 블록 모두 선언된 모델별 추론 선택지와 기본값을 같은 내용으로 가집니다. 레거시 블록은 모델 options의 기본값과 변형 맵으로, V2 블록은 모델 settings와 네이티브 변형 배열로 표현하며 둘 다 기록하고 동기화합니다. 두 블록은 같은 프로바이더와 모델 ID를 가리키고 opencode V2는 이를 프로바이더 항목 하나로 병합합니다. Apply, Refresh, Disable, Restore는 두 조각 모두에 작용하며 다른 프로바이더, 에이전트, 단축키, MCP 항목은 유지됩니다. 이 모델 정의는 Kilo 내보내기와 `ocx opencode` 런처와 같은 유효 정규 모델 메타데이터에서 만들어집니다. 명시적 사용자 지정 오버라이드는 항상 우선하고 비워 둔 단계는 그대로 유지되며, 단계나 기본값을 지어내지 않습니다. 신뢰할 수 있는 컨텍스트 한도는 출력 한도와 함께 전달됩니다. 출력은 알려진 값(명시적 또는 카탈로그 메타데이터)이 우선하고, 알려지지 않았으면 스키마 필수 32000 폴백(컨텍스트로 상한)이 사용되며, 신뢰할 수 있는 컨텍스트가 없으면 limit 블록 전체가 생략됩니다. 기능, 강도 단계, 기본값, 선택적 입력 한도는 알려진 경우에만 기록됩니다. V2 블록의 네이티브 capabilities 객체에는 알려진 tools 값이 필요해서 도구 지원 여부를 알 수 없으면 일부만 쓰지 않고 객체 전체를 생략하며, 레거시 쪽 모달리티 선언은 그대로 남습니다. 단계를 지어내는 대신 제어만 기록합니다. V2 블록은 항상 명시적인 변형 배열을 가지며 선언이 없으면 비어 있는데, 생략하면 OpenCode가 low/medium/high를 합성하기 때문입니다. 고정된 추론은 알려졌지만 조절 가능한 단계가 없는 모델에는 선택 가능한 강도 대신 비활성 전용 억제가 기록됩니다. 레거시 OpenCode 블록과 Kilo 모두 클라이언트가 그냥 생성했을 각 단계 ID를 전부 비활성화합니다. 어느 쪽도 선택 가능한 강도를 추가하지 않습니다. 선택 가능한 변형(모델 자신의 단계가 `none`을 선언한 경우의 `none` 포함)은 알려진 경우에 기록되는 기본 추론 강도를 덮어쓸 수 있습니다. 클라이언트가 무엇을 선택하든 프록시의 업스트림 고정 정책은 계속 실제 요청에 적용됩니다. 관리형 DSH 지원의 최저 호환 버전은 **DSH 0.1.0-rc.6**입니다. opencodex는 `llm-pi-ai.providers.opencodex`만 소유합니다. Apply와 Refresh는 해당 조각을 교체하고, Disable은 그 조각만 제거하며, Restore는 기록된 스냅샷을 되돌립니다. DSH는 프로바이더 변경을 즉시 다시 읽습니다. 이 작업은 사용자의 기본 모델이나 네이티브 `deepseek-official` 프로바이더를 바꾸지 않습니다. 관리형 DSH 연동은 현재 루프백 전용이며 실제 자격 증명을 기록하지 않습니다. @@ -115,7 +115,7 @@ TOML 날짜와 시간도 관리형 재작성을 거부합니다. 병합 과정 **Kimi Code는 환경 변수 참조를 담을 수 없어** 설정에 키 대신 `opencodex-loopback` 자리표시자를 넣습니다. 어느 클라이언트 설정에도 실제 자격 증명을 기록하지 않습니다. -**`ocx opencode`에서는 런처의 프로바이더 블록이 우선합니다.** 런처는 `OPENCODE_CONFIG_CONTENT`를 통해 `provider.opencodex`와 `providers.opencodex`를 주입합니다. 디스크의 같은 항목보다 우선하지만 opencode 설정의 나머지는 평소처럼 적용됩니다. 여기의 스위치는 `opencode`를 직접 실행할 때 중요합니다. +**`ocx opencode`에서는 런처의 프로바이더 블록이 우선합니다.** 런처는 `OPENCODE_CONFIG_CONTENT`를 통해 `provider.opencodex`와 `providers.opencodex`를 주입합니다. 디스크의 같은 항목보다 우선하지만 opencode 설정의 나머지는 평소처럼 적용됩니다. 이 블록은 내보내기와 같은 유효 정규 모델 메타데이터로 구성되므로 런처와 관리형 연동이 같은 모델, 한도, 추론 선택지를 기술합니다. 여기의 스위치는 `opencode`를 직접 실행할 때 중요합니다. ## 터미널에서 사용하기 @@ -144,7 +144,7 @@ ocx integration client enable --client mcode ocx mcode ``` -연결 후 `ocx sync`와 `POST /api/sync`는 소유한 MCode, Pi, Aside, Raycast, omo 카탈로그를 현재 모델 선택, 컨텍스트 창, 추론 강도 단계로 갱신합니다. 프록시 시작 시 소유한 Raycast 카탈로그도 갱신합니다. 모델 표시 여부, 프로바이더 선택, 프리셋이 바뀌어도 연결된 Pi, Aside, Raycast, omo 카탈로그를 갱신합니다. 누락되거나 외부에서 수정되었거나 안전하지 않은 블록, 그리고 이전에 소유했지만 사용자가 직접 삭제한 블록은 그대로 둡니다. 활성화된 Aside 프로필은 일반적인 소유 블록만 갱신하는 규칙의 예외입니다. 계정 디렉터리가 있고 소유 블록이 생긴 적이 없다면 해당 슬롯이 비어 있을 때 동기화로 첫 블록을 만들 수 있습니다. 이전 Aside 연결이 있으면 이 동작이 기본적으로 등록된 모든 프로필에 적용됩니다. 동기화는 없는 계정 디렉터리를 만들거나 수동 블록을 교체하지 않습니다. 거부되거나 겹친 갱신은 클라이언트마다 따로 보고합니다. 갱신 파일을 읽으려면 새 Pi 세션을 시작하거나 Aside를 완전히 종료하고 다시 여세요. Aside 갱신에는 [호환되는 실행 중 프록시](#aside-프로필-제어)가 필요합니다. +연결 후 `ocx sync`와 `POST /api/sync`는 소유한 MCode, Pi, Aside, Raycast, omo, OpenCode, Kilo 카탈로그를 현재 모델 선택, 컨텍스트 창, 추론 강도 단계로 갱신합니다. 프록시 시작 시 소유한 Raycast 카탈로그도 갱신합니다. 모델 표시 여부, 프로바이더 선택, 프리셋이 바뀌어도 연결된 Pi, Aside, Raycast, omo, OpenCode, Kilo 카탈로그를 갱신합니다. 누락되거나 외부에서 수정되었거나 안전하지 않은 블록, 그리고 이전에 소유했지만 사용자가 직접 삭제한 블록은 그대로 둡니다. 활성화된 Aside 프로필은 일반적인 소유 블록만 갱신하는 규칙의 예외입니다. 계정 디렉터리가 있고 소유 블록이 생긴 적이 없다면 해당 슬롯이 비어 있을 때 동기화로 첫 블록을 만들 수 있습니다. 이전 Aside 연결이 있으면 이 동작이 기본적으로 등록된 모든 프로필에 적용됩니다. 동기화는 없는 계정 디렉터리를 만들거나 수동 블록을 교체하지 않습니다. 거부되거나 겹친 갱신은 클라이언트마다 따로 보고합니다. 갱신 파일을 읽으려면 새 Pi, OpenCode, Kilo 세션을 시작하거나 Aside를 완전히 종료하고 다시 여세요. Aside 갱신에는 [호환되는 실행 중 프록시](#aside-프로필-제어)가 필요합니다. Models에 **“Model selection saved”**와 클라이언트 갱신 경고가 함께 표시되면 선택 자체는 저장되었지만 클라이언트 파일 하나 이상을 갱신하지 못한 상태입니다. 경고는 해당 클라이언트와, 필요하면 Aside 프로필을 알려주고 거부 이유를 설명합니다. 새 세션을 시작하기 전에 **Integrations**에서 해당 클라이언트나 프로필을 확인하세요. 문제를 해결한 뒤 `ocx sync`를 다시 실행합니다. 겹친 작업은 먼저 끝나야 합니다. 경고에 백업 경로가 있거나 복구가 완료되지 않았다고 나오면 재시도 전 복구 상태를 확인하세요. 선택 저장 성공만으로 클라이언트 파일 복구까지 확인된 것은 아닙니다. @@ -222,9 +222,9 @@ Kilo CLI, VS Code, JetBrains는 전역 설정을 공유합니다. 이 연동은 Kilo는 이 전역 파일을 모두 병합합니다. 다른 후보 파일에도 `provider.opencodex`가 있으면 상태에 충돌 파일을 표시하고 적용과 교체를 거부합니다. 연동을 켜기 전에 해당 파일에서 `provider.opencodex`를 제거하세요. 이미 소유한 파일의 블록은 충돌 중에도 비활성화할 수 있습니다. 읽을 수 없거나 안전하지 않은 후보 파일도 쓰기를 막습니다. -관리하는 부분은 OpenCode V1 형식의 `provider.opencodex`(`npm`, `options`, `models`)뿐입니다. OpenCode V2의 `providers` 키는 내보내지 않습니다. `$schema`, `model`, `enabled_providers`, MCP 등의 키는 사용자가 관리합니다. 적용한 뒤 Kilo에서 `opencodex/`을 선택하세요. +관리하는 부분은 OpenCode V1 형식의 `provider.opencodex`(`npm`, `options`, `models`)뿐입니다. OpenCode V2의 `providers` 키는 내보내지 않습니다. 대신 모델별 추론 선택지는 프로바이더 블록 안의 변형 맵으로, OpenCode 내보내기와 같은 유효 정규 메타데이터에서 기록됩니다. `$schema`, `model`, `enabled_providers`, MCP 등의 키는 사용자가 관리합니다. 적용한 뒤 Kilo에서 `opencodex/`을 선택하세요. -루프백에서는 `options.apiKey`로 `{env:OPENCODEX_KILO_API_KEY}`를 사용합니다. 루프백이 아닌 바인드에서는 인증을 `options.headers["x-opencodex-api-key"]`로 옮기며 실제 키를 저장하지 않습니다. 적용 시 전역 파일 전체를 보기 좋은 JSON으로 다시 쓰므로 다른 키의 주석과 후행 쉼표는 보존되지 않습니다. Kilo는 자동 카탈로그 갱신 대상이 아닙니다. 라우팅 모델 선택을 바꾼 뒤에는 명시적으로 갱신하세요. +루프백에서는 `options.apiKey`로 `{env:OPENCODEX_KILO_API_KEY}`를 사용합니다. 루프백이 아닌 바인드에서는 인증을 `options.headers["x-opencodex-api-key"]`로 옮기며 실제 키를 저장하지 않습니다. 적용 시 전역 파일 전체를 보기 좋은 JSON으로 다시 쓰므로 다른 키의 주석과 후행 쉼표는 보존되지 않습니다. Kilo는 자동 카탈로그 갱신 대상입니다. `ocx sync`, `POST /api/sync`, 표시 여부·프로바이더·프리셋 변경은 다른 클라이언트와 같은 소유 블록만 갱신하는 안전한 규칙으로 소유한 Kilo 블록을 갱신하며, 새 Kilo 세션이 갱신된 파일을 읽습니다. ```bash ocx integration client enable --client kilo @@ -258,4 +258,27 @@ GitHub Copilot 데스크톱 앱에서 opencodex를 OpenAI 호환 모델 프로 ## Factory Droid +**연동 → Factory Droid** (`/#integrations/droid`)에서 연결된 모델별 추론 기본값을 +설정할 수 있습니다. 모델이 지원하는 effort를 선택하고 변경 내용을 검토한 뒤 +확인하세요. **기본값 없음**은 해당 모델의 편집 중인 값을 지웁니다. 실제로 적용하려면 +**변경 사항 저장 / 검토**에서 확인해야 합니다. 지원 effort +목록이 없는 모델에는 사용 가능한 기본값이 없다고 표시됩니다. + +이 값은 Droid 요청에 effort가 없을 때만 적용됩니다. 요청이 effort를 명시하면 +그 값을 사용하며, 기존 OpenCodex pin과 cap 정책은 계속 적용됩니다. OpenCodex가 +기본값을 적용하더라도 Droid에는 **Dynamic**으로 표시될 수 있습니다. Droid 기본값 +헤더가 없는 요청에는 영향을 주지 않습니다. 이 헤더는 요청의 선호 값이며, +클라이언트 신원을 증명하지 않습니다. +콤보와 라우팅 정책에서는 각 실제 대상이 자체 effort 목록으로 이 선호 값을 +확인하므로, 첫 대상이 지원하지 않아도 지원하는 다음 대상으로 전달됩니다. + +저장된 기본값을 라우팅된 모델이 더 이상 지원하지 않으면 요청에는 적용하지 +않습니다. Droid 설정에서도 제거하려면 패널에서 지운 뒤 변경 내용을 검토하고 +확인하세요. + +새로고침은 정확히 같은 `provider/model` 선택자가 계속 연결된 동안 기본값을 +유지합니다. 프로바이더, 모델, 콤보 별칭의 이름을 바꾸면 관리 행이 교체되고 +기본값이 지워지므로 새 행에서 다시 선택하세요. 연동을 해제하면 관리되는 모델 +행과 기본값이 함께 제거되며, 되돌리기는 저장된 행과 기본값을 함께 복원합니다. + Factory Droid는 `~/.factory/settings.json`(Windows에서는 `%USERPROFILE%\.factory\settings.json`)을 사용합니다. `ocx integration client enable --client droid`로 명시적으로 활성화한 다음 `/model`에서 사용자 지정 모델을 선택하세요. 관리되는 항목에는 키가 없으며 루프백에서만 동작합니다. 비활성화하면 관리되는 항목이 제거되고, Undo는 저장된 원본 바이트를 복원합니다. 기존 `config.json`에 OpenCodex 항목이 있거나 `settings.local.json`이 `customModels`를 덮어쓰면 활성화 전에 충돌을 해결하세요. [Factory BYOK 문서](https://docs.factory.ai/model-independence/byok)를 참고하세요. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 130e9e91e27..a765419710e 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -137,8 +137,10 @@ configured provider key. The adapter preserves the incoming client's `User-Agent` as a fallback in both auth modes because some Responses-compatible providers use the Codex client fingerprint for compatibility behavior. An explicitly configured provider `User-Agent` remains authoritative regardless of header casing; -if the caller sends none, OpenCodex does not invent one. No other caller header is widened by this -exception. +if the caller sends none, OpenCodex does not invent one. Additional caller metadata can be selected +with `forwardClientHeaders`; provider `headers` win for this option, and credential or transport-owned +names are refused. Canonical ChatGPT forward auth retains its separate fixed header allowlist. +Only `originator`, `x-client-request-id`, `x-codex-app-version`, and `user-agent` are supported by `forwardClientHeaders`; arbitrary names are rejected on load/write and ignored at runtime. 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 diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 727a5699fc8..9ab7f619d38 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -34,6 +34,9 @@ prints a proposed model and effort per role without writing anything. `--apply` through the same write as `set`, skipping and naming the roles whose model and effort already match. See [Auto-assign](/guides/integrations/#auto-assign). +Human-readable suggestion output displays terminal control characters as visible escapes. +Use `--json` when you need the original suggestion values without presentation escaping. + `ocx agent injection suggest ` does the same for the delegation model: it sizes the described work, proposes the cheapest sufficient model and an effort from the delegation picker's list, and writes nothing unless `--apply` is given, which saves through the same write as `injection set`. See @@ -305,8 +308,8 @@ remain supported. Use `ocx claude config ...` for Claude Code setti Ensure the proxy is running, then launch opencode with the generated `provider.opencodex` and `providers.opencodex` blocks in OpenCode's inline runtime layer (`OPENCODE_CONFIG_CONTENT`). The -legacy block keeps V1 clients working; the V2 block is the one carrying the selectable -reasoning-effort variants. Existing inline config is preserved and only those two keys are replaced +legacy block keeps V1 clients working with variant maps; the V2 block carries native arrays +for the same reasoning-effort choices and defaults. Existing inline config is preserved and only those two keys are replaced for this launch. Global or project `opencode.json` files may be read to warn about an existing override, but on-disk files are never modified. Routed models appear as `opencodex//`. Launching plain `opencode` later behaves exactly as before. @@ -326,6 +329,11 @@ reference or loopback placeholder — in the selected client's native format. The proxy must be running; the command resolves its live port, reads `/api/models`, and emits only models Codex can currently see. +OpenCode and Kilo exports preserve effective model limits, known capabilities, declared reasoning +choices and defaults, including metadata inherited by custom rows. Explicit overrides still win; +unknown capabilities and defaults are not invented. The OpenCode launcher uses the same metadata. +See [client integrations](/guides/integrations/) for managed refresh and upgrade behavior. + | Flag | Action | | --- | --- | | `--client ` | Required. Selects the client config dialect. | diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index 64d990df618..e6b11cd11b5 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -677,7 +677,9 @@ clears the stale job. On Windows, `ocx service status` reports Task Scheduler registration separately from identity-verified OpenCodex proxy reachability. It does not print the localized `schtasks` table, -so the summary remains readable across Windows code pages. +so the summary remains readable across Windows code pages. Transactional-backup recovery +logs a fixed success message rather than the backup directory name; backup names containing +shell metacharacters do not become commands in that log message. On Windows, creating the Task Scheduler entry requires elevation. Recognized localized access-denied text keeps the existing guidance path. If that text is unreadable, the fallback @@ -706,6 +708,16 @@ If installation is refused or the resulting shim is unhealthy, the command exits the dashboard reports the failure reason. A healthy existing shim still counts as success. For Windows installations that expose only `codex.exe`, use `ocx service install` for autostart. +For fnm-managed Codex, installation resolves the temporary multishell directory to the durable +Node installation while preserving the launcher filename. If that directory cannot be resolved, +installation refuses instead of wrapping a temporary path. When Codex is selected, `ocx connect` +also reports shim readiness. This readiness check skips special-file PATH entries while preserving +the order of regular launchers, including npm and fnm symlinks. If a different PATH wrapper hides a healthy shim, fix PATH order; +reinstalling the same shim does not change which command your shell finds first. If the tracked shim +is healthy but no `codex` command is found, connect reports it as inactive and asks you to add its +directory to PATH. A failed PATH inspection reports activation as unverified instead of claiming +that no command exists. These warnings do not change the connect command's exit status. + Before an install or repair is committed, OpenCodex runs the saved launcher with `--version` while service startup is bypassed. It refuses the change and rolls back when the launcher resolves `codex` back to the shim, exits nonzero, exceeds five seconds, leaves descendants running, or @@ -839,6 +851,18 @@ Unix-only check. A failure aborts while the tray and proxy are still running. A then stopped before files are replaced; an installed service is rebuilt and started automatically, while a foreground installation prints `ocx start` as the next step. Dashboard update records redact profile/cache paths and UID/GID values before they are persisted. +When a stopped listener's literal IP address drops the liveness dial instead of refusing it +(for example on a tailnet), the updater briefly tries binding that same address and port. +A successful bind confirms absence at that instant; a failed bind or an inconclusive hostname +probe still prevents the update. + +On Windows, Scoop's default `nodejs` and `nodejs-lts` `npm` installations can be used +from the user home directory when their `current` junction stays inside the Node app +directory; the default persistent `bin` directory is also supported. Running inside +that installation (including its resolved version directory or persistent `bin`), +project-local `npm`, `NO_JUNCTION` version-directory entries, and custom Scoop roots +under the home remain excluded. + If the install step fails, the previous version stays installed and its service is restarted; the terminal output names the next step, and [Update Failed on Windows](/troubleshooting/update-failed/) covers finishing the update and the folders a failed attempt can leave behind. 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 839bc7161b4..947bf9080e5 100644 --- a/docs-site/src/content/docs/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md @@ -118,6 +118,10 @@ For Antigravity, an upstream `401` can refresh the rejected account’s OAuth cr 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 `403` asking to verify the account quarantines that credential as `needs-reauth(verify)`; +when account failover is enabled, the request can retry on another eligible account. Complete +Google's account verification, then run `ocx login google-antigravity`. Silent token refresh +does not clear this verification requirement. If OpenCodex cannot save the quarantine, this adapter exchange preserves the original `403` without another recovery send; the account has not been durably quarantined. An enclosing combo or policy route can still apply its existing fallback rules. 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 @@ -795,6 +799,15 @@ Use `ocx provider add mine --adapter openai-chat --base-url https://example.com/ ### Cached quota history +Automatic OpenAI account exhaustion checks distinguish purchased usage credits from reset +tickets. Spending remains off by default: enable **Use credits** for that account (stored in +`creditCodexAccountIds`). Only that opt-in together with a fresh positive spendable balance or +unlimited credits can keep an account eligible after included usage reaches 100%; an upstream refusal or overage limit still +blocks that evidence. Credit evidence expires after five minutes, and usage headers do not +renew its clock. `pause-exhausted` uses this rule. Reset tickets alone never grant automatic +headroom, and a credits-only response cannot clear a request cooldown. The default-on main-account +hard lock remains a separate local policy and is not lifted by the credits switch. + `ocx account history openai [--limit 1-200] [--json]` reads stored observations without contacting the provider. The output separates actual observation time, WHAM or response-header source, window family and usage percentage. At most 200 observations per account are retained for 30 days, with global storage bounds. Ordinary token refresh preserves history. Reauthentication, removal or account replacement retires the old publication. Native main and probes performed before a login is published are not included. Missing history means insufficient observations, not zero usage. This command does not spend quota. Effective estimates, when supported by observations, carry the limitations below. diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 04eafa67712..729d00601ff 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -225,6 +225,8 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `chatServiceTier?` | `boolean` | Provider-wide Chat-wire opt-in for forwarding caller `service_tier` values. On a classified route it governs foreign values such as `flex`, not proxy-owned canonical Fast after capability validation; on an unclassified route it governs every caller value because no Fast capability has been validated. Exact model capability does not authorize foreign forwarding. Responses routes retain their capability-based caller forwarding behavior. | | `promptCacheKey?` | `boolean` | Provider-wide `openai-chat` opt-in for forwarding a `prompt_cache_key`. The adapter forwards the key it is given and never invents one, but the key is not always the caller's: Claude Messages translation derives one from `metadata.user_id`, or from a model/system/tools cohort when no metadata is sent. Default off. Enable only when the upstream documents support, because strict gateways may reject the unknown field with HTTP 400. | | `preserveResponsesReasoningContent?` | `boolean` | Keep plaintext reasoning content on replayed Responses reasoning items instead of blanking it (blanking is the ChatGPT backend's rule). Enable for upstreams whose contract accepts reasoning replay, such as DeepSeek. Proxy-minted `ocxr1` envelopes are always stripped. | +| `preserveResponsesInputItemIds?` | `boolean` | Default off. Preserve valid `input[].id` when `store: false` for explicitly trusted launcher relays that require turn identity. Ordinary Responses providers keep stripping IDs; xAI custom-call ID repair remains active. | +| `preserveResponsesMessageMetadata?` | `boolean` | Default off. Preserve private `internal_chat_message_metadata_passthrough` on input items for explicitly trusted launcher relays. This forwards private turn/session metadata to that destination; do not enable for ordinary gateways. | | `dropResponsesReasoningItems?` | `boolean` | Remove replayed Responses `reasoning` items from continuation input entirely, instead of forwarding them with blanked content. This is lossy: summaries, item ids, and `encrypted_content` go with them, so no reasoning item from the previous turn remains in the forwarded input. It does not remove `previous_response_id`, so an upstream holding its own turn state may still reach it. Enable only for an upstream that rejects reasoning replay outright — the built-in Volcengine Ark Coding Plan preset sets it because validated tool continuations answered `400 InvalidParameter` otherwise. Set it to `false` on that provider to forward reasoning items again. | | `disabled?` | `boolean` | Keep the provider on disk but exclude it from routing and model/catalog listings. | | `apiKey?` | `string` | API key, an `${ENV_VAR}` / `$ENV_VAR` reference, or a `keychain:` reference written by `ocx provider keychain store`. References resolve at request time. See [Storing keys in the OS keychain](#storing-keys-in-the-os-keychain). | @@ -247,6 +249,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `modelMaxOutputTokens?` | `Record` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. | | `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an explicit all-zero user entry means a known-zero estimate; delete that model entry to restore automatic pricing. All-zero catalog metadata still falls through. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. | | `headers?` | `Record` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. Requests that ride the provider outbound wrapper — model discovery, connection tests, and other proxy-originated diagnostics such as the Ollama show probe — send `User-Agent: opencodex` unless a User-Agent is set here or by the provider preset. | +| `forwardClientHeaders?` | `string[]` | `openai-responses` only: opt in to copying named inbound client metadata headers onto inference requests when the provider has not already set that header. Credential and transport-owned names such as `Authorization`, cookies, API-key headers, `Content-Type`, `Content-Length`, `Host`, and `x-oai-attestation` are rejected and also filtered at runtime. Header names are case-insensitive and may not repeat. `headers` remains authoritative; the existing caller `User-Agent` fallback still applies when neither surface sets one. Supported names: `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`; all other names are refused. | | `openRouterRouting?` | `OpenRouterProviderRouting` | Default OpenRouter `order`, `only`, and `allowFallbacks` preferences; valid only for canonical OpenRouter with `openai-chat`. | | `modelOpenRouterRouting?` | `Record` | Exact model-id overrides that replace the provider-wide OpenRouter preference. | | `vercelGatewayRouting?` | `VercelGatewayRouting` | Default Vercel AI Gateway `order`, `only`, and `sort` (`"cost"` \| `"ttft"` \| `"tps"`) preferences; valid only for canonical Vercel AI Gateway with `openai-chat`. | diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index bce76e5594a..1b7c6d1de8f 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -182,7 +182,10 @@ only currently eligible members of `targets`; missing, failed, or invalid decisi eligible member, while caller cancellation remains terminal. Adding the provider or Combo never changes `defaultProvider` or hides direct model rows. See [Decision method](/guides/combos/#decision-method) for the three methods, setup, privacy bounds, and -the one-decision-per-call contract. +the one-decision-per-call contract. API-key provider/model scopes also apply to TypeSafe and +self-hosted decision destinations, before their credentials or request state are read. If the key +denies the optional decision destination, no decision request is sent; the ordinary eligible +inference fallback still has to satisfy that key's scope. ### Self-hosted decision model (e.g. Ollama tev1) diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index e9fc0cde0ca..2f24185556d 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -642,8 +642,10 @@ subscription with a warning when detection is inconclusive. See ## Compaction routing In **Dashboard → Overview → Compaction routing**, choose a model, which triggers it applies to, -and an optional reasoning effort, then click **Save**. Select **Use conversation model** and save -to remove the override. Changes apply to the next compaction request without restarting the proxy. +an optional reasoning effort, and whether the override covers all source models or only selected +sources. For selected sources, check individual models or provider-wide `provider/*` entries, +then click **Save**. Select **Use conversation model** and save to remove the override. Changes +apply to the next compaction request without restarting the proxy. Set `compactionRouting` in OpenCodex `config.json` to override the model Codex's compaction requests use. The setting is disabled when omitted. @@ -659,6 +661,17 @@ requests use. The setting is disabled when omitted. ``` `model` accepts native model IDs, provider-qualified model IDs, and configured combos. +Optional `sourceModels` limits the override to exact incoming model IDs or `provider/*` patterns, +for example `["kimi/*", "google-antigravity/*"]`. Matching is case-sensitive. Omit it for the +existing all-model behavior; empty, duplicate, or malformed lists are invalid. The Dashboard +source-scope picker can edit the allowlist and preserves saved selectors absent from the current +catalog. The model checklist shows at most 300 matching entries; use its search field to narrow +large catalogs without removing saved selections. You can also edit it through `config.json` or `PUT /api/settings`. +This checks the incoming compaction model, not an inferred conversation model. If Codex sends a +bare native compaction model for a routed thread, it does not match those provider patterns and +keeps its existing route. This conservative behavior protects GPT but can leave some automatic +compactions on the native path. Ordinary non-compaction turns are never changed. + `reasoningEffort` is optional; omit it to preserve the incoming effort. Supported declarations are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, and `ultra`. Existing provider effort rules still apply. The native `/responses/compact` endpoint keeps @@ -675,7 +688,9 @@ the canonical OpenAI quota is exhausted. Codex picks a bare native model for the turn, and OpenCodex reserves that for an enabled canonical `openai` provider whenever one exists, so the compaction fails with a quota error before the routed turn begins even though the thread itself runs elsewhere. Naming `"auto"` here points the compaction at a -provider-qualified model with its own credentials and quota. +provider-qualified model with its own credentials and quota only when `sourceModels` is omitted +or matches the incoming model. A list containing only `provider/*` entries does not cover a bare +native compaction model. OpenCodex changes only requests with explicit `request_kind: "compaction"` metadata whose `compaction.trigger` is one of the values you listed, sent to `/v1/responses/compact` or to @@ -686,8 +701,8 @@ metadata are supplied they must name the same trigger. WebSocket requests use ea metadata rather than the connection's earlier handshake metadata. The selected model's provider receives the entire conversation for summarization, including -conversations that normally run on another provider. A combo selector sends it to every combo -target, including failover targets. The dashboard panel states this next to the model picker +conversations that normally run on another provider. A combo selects a target by its routing strategy and may retry the same conversation on another +target after a retryable failure, so one or more targets can receive it. The dashboard panel states this next to the model picker and names the destination provider, or the combo's target providers, once a model is chosen. When the override covers automatic compaction, that transfer happens without you asking for it, at whatever point Codex decides to compact; the dashboard panel says so as well. @@ -804,6 +819,8 @@ Explicit selection fails closed when the provider is missing, disabled, incompat usable key; it never falls back to another paid upstream. The endpoint must implement the OpenAI Images API paths and response shape expected by Codex. +Both search loops keep `web_search` declared after the search budget is exhausted. Further calls receive a paired limit-reached result without another physical search; at most `maxSearchesPerTurn + 3` model iterations run before a terminal error. Ordinary caller tools and cancellation still end the loop. Empty-answer recovery alone removes all tools. + ### `webSearchSidecar` (`OcxWebSearchSidecarConfig`) RunTurn adapters also use the configured search sidecar. Search turns preserve progress heartbeats. A first-event OAuth 429 rotates the account on the initial request and on each post-search answer request. The replay keeps the search tool and the gathered results. With `emptyCompletionRetry: true`, an empty answer before the search limit receives one retry using the current conversation and gathered results. The existing tool-free recovery after the search limit remains available independently of that setting. Upstream failures stop the turn instead of triggering another search. diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md index bad29c7e258..818d6a8fa47 100644 --- a/docs-site/src/content/docs/reference/proxy-formats.md +++ b/docs-site/src/content/docs/reference/proxy-formats.md @@ -248,6 +248,10 @@ buffer budget, the relay reports the failure without waiting for upstream inspec 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. +Responses SSE accepts CR, LF and CRLF line endings, including mixed endings and CRLF split across +network chunks. Tool validation, Chat translation, terminal inspection and WebSocket projection +use the same event boundaries. A CR-only completion does not wait for the upstream connection to close. + 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/docs-site/src/content/docs/ru/guides/integrations.md b/docs-site/src/content/docs/ru/guides/integrations.md index d6ae86fa896..09080b48fca 100644 --- a/docs-site/src/content/docs/ru/guides/integrations.md +++ b/docs-site/src/content/docs/ru/guides/integrations.md @@ -48,12 +48,37 @@ modelProfile: Модели GJC с поддерживаемыми уровнями усилия рассуждения экспортируют `reasoning: true`, `thinking.levels` и `compat.supportsReasoningEffort`, чтобы в GJC можно было выбрать уровень. Нативные модели Codex получают стандартные уровни, даже если каталог их не указывает. Без известных уровней эти поля не выводятся. `none` не отправляет уровень, а `ultra` при отправке становится `max`, поэтому их не предлагают. Обновите интеграцию, чтобы обновить параметры моделей. Управляемой интеграции OpenCode принадлежат два фрагмента: `provider.opencodex` -(opencode V1) и `providers.opencodex` (opencode V2). Только блок V2 содержит -варианты effort для каждой модели, поэтому оба фрагмента записываются и +(opencode V1) и `providers.opencodex` (opencode V2). Оба блока несут одни и те же +объявленные варианты effort и значения по умолчанию для каждой модели: в устаревшем +блоке это значение по умолчанию в options модели и карта вариантов, в блоке V2 — +settings модели и собственный массив вариантов, поэтому оба фрагмента записываются и поддерживаются синхронно. Они называют одни и те же id провайдера и моделей, а opencode V2 объединяет их в одну запись провайдера. Apply, Refresh, Disable и Restore действуют на оба фрагмента; другие ваши провайдеры, агенты, -сочетания клавиш и записи MCP остаются без изменений. +сочетания клавиш и записи MCP остаются без изменений. Оба фрагмента строятся из +фактических канонических метаданных моделей — как и экспорт Kilo и запускатель +`ocx opencode`. Явное пользовательское переопределение всегда выигрывает, а +очищенная лестница остаётся пустой; лестница или значение по умолчанию никогда +не выдумываются для модели, которая их не объявляет. Достоверный лимит контекста +переносится вместе с лимитом выхода: известное значение выхода — явное или из +метаданных каталога — выигрывает, иначе применяется обязательный по схеме запасной +лимит 32000, ограниченный контекстом; без достоверного контекста весь блок limit +опускается. Возможности, лестницы effort, значения по умолчанию и необязательный +лимит входа записываются только известными. Собственный объект capabilities блока V2 +требует известного значения tools, поэтому при неизвестной поддержке инструментов он +опускается целиком, а не частично; объявления модальностей в устаревшем блоке +остаются. Вместо выдуманной лестницы пишутся только элементы управления: блок V2 +всегда несёт явный массив вариантов — пустой, если ничего не объявлено, потому что +его отсутствие заставило бы OpenCode синтезировать low/medium/high, — а модель с +известной фиксированной рассуждающей способностью, но без регулируемой лестницы +получает +вместо effort только неактивное подавление: и устаревший блок OpenCode, и Kilo отключают +каждый id ступени, который клиент иначе сгенерировал бы. Ни один из них не добавляет +выбираемый effort. +Выбираемые варианты могут переопределить значение effort по +умолчанию — включая `none`, и только если лестница самой модели его объявляет. +Что бы ни выбрал клиент, закреплённая политика effort на прокси +по-прежнему применяется к уходящему запросу. Управляемая поддержка DSH требует как минимум **DSH 0.1.0-rc.6**. OpenCodex владеет только `llm-pi-ai.providers.opencodex`: Apply и Refresh заменяют этот @@ -304,8 +329,11 @@ OMP поддерживает заголовки уровня провайдер **При `ocx opencode` побеждают блоки провайдера, заданные запускателем.** Запускатель внедряет `provider.opencodex` и `providers.opencodex` через `OPENCODE_CONFIG_CONTENT`, что имеет приоритет над такими же записями на диске. -Остальная конфигурация opencode действует как обычно. Переключатель на этой -странице важен при прямом запуске `opencode`. +Остальная конфигурация opencode действует как обычно. Эти блоки строятся из тех +же фактических канонических метаданных моделей, что и экспорты, поэтому +запускатель и управляемая интеграция описывают одни и те же модели, лимиты и +варианты effort. Переключатель на этой странице важен при прямом запуске +`opencode`. ## Из терминала @@ -339,10 +367,11 @@ ocx mcode ``` После подключения `ocx sync` и `POST /api/sync` обновляют принадлежащие -интеграции каталоги MCode, Pi, Aside, Raycast и omo с текущим выбором моделей, -контекстными окнами и ступенями effort. Запуск прокси обновляет принадлежащий -ему каталог Raycast. Изменения видимости моделей, выбора провайдера или пресетов -также обновляют подключённые каталоги Pi, Aside, Raycast и omo. +интеграции каталоги MCode, Pi, Aside, Raycast, omo, OpenCode и Kilo с текущим +выбором моделей, контекстными окнами и ступенями effort. Запуск прокси обновляет +принадлежащий ему каталог Raycast. Изменения видимости моделей, выбора провайдера +или пресетов также обновляют подключённые каталоги Pi, Aside, Raycast, omo, +OpenCode и Kilo. Отсутствующие, изменённые извне или небезопасные блоки остаются нетронутыми, как и ранее принадлежавшие интеграции блоки, удалённые вами вручную. Есть исключение для включённого профиля Aside: если каталог его аккаунта @@ -351,8 +380,8 @@ ocx mcode такое поведение для всех зарегистрированных профилей. Синхронизация не создаёт отсутствующие каталоги аккаунтов и не заменяет ручные блоки. Отказ или пересечение операций обновления сообщается отдельно для каждого -клиента. Чтобы загрузить новый файл, начните новую сессию Pi или полностью -закройте и откройте Aside. +клиента. Чтобы загрузить новый файл, начните новую сессию Pi, OpenCode или Kilo +либо полностью закройте и откройте Aside. Обновление Aside требует [совместимого работающего прокси](#управление-профилями-aside). Если Models показывает **“Model selection saved”** вместе с предупреждением об @@ -538,9 +567,9 @@ Kilo CLI, VS Code и JetBrains используют общую глобальн Kilo объединяет все эти глобальные файлы. Если другой файл-кандидат тоже определяет `provider.opencodex`, статус перечисляет конфликтующие файлы, а применение и замена отклоняются. Перед включением удалите `provider.opencodex` из этих файлов. Отключение уже принадлежащего OpenCodex блока доступно и при таком конфликте. Нечитаемый или небезопасный файл-кандидат также блокирует запись. -Интеграции принадлежит только `provider.opencodex` в формате OpenCode V1 (`npm`, `options`, `models`). Поле OpenCode V2 `providers` не создаётся. `$schema`, `model`, `enabled_providers`, MCP и прочие ключи остаются под управлением пользователя. После применения выберите в Kilo `opencodex/`. +Интеграции принадлежит только `provider.opencodex` в формате OpenCode V1 (`npm`, `options`, `models`). Поле OpenCode V2 `providers` не создаётся; вместо этого варианты effort для каждой модели записываются в блоке провайдера как карта вариантов — из тех же фактических канонических метаданных, что и экспорт OpenCode. `$schema`, `model`, `enabled_providers`, MCP и прочие ключи остаются под управлением пользователя. После применения выберите в Kilo `opencodex/`. -Для loopback значение `options.apiKey` — `{env:OPENCODEX_KILO_API_KEY}`. При привязке не к loopback авторизация переносится в `options.headers["x-opencodex-api-key"]`; настоящий ключ не записывается. Применение переписывает весь глобальный файл как форматированный JSON, поэтому комментарии и завершающие запятые в других ключах не сохраняются. Kilo не участвует в автоматическом обновлении каталога; после изменения выбора маршрутизируемых моделей обновите интеграцию явно. +Для loopback значение `options.apiKey` — `{env:OPENCODEX_KILO_API_KEY}`. При привязке не к loopback авторизация переносится в `options.headers["x-opencodex-api-key"]`; настоящий ключ не записывается. Применение переписывает весь глобальный файл как форматированный JSON, поэтому комментарии и завершающие запятые в других ключах не сохраняются. Kilo входит в автоматическое обновление каталога: `ocx sync`, `POST /api/sync` и изменения видимости, провайдера или пресетов обновляют принадлежащий блок Kilo по тем же безопасным правилам «только свои блоки», что и остальные клиенты, а новая сессия Kilo загружает обновлённый файл. ```bash ocx integration client enable --client kilo diff --git a/docs-site/src/content/docs/tr/guides/integrations.md b/docs-site/src/content/docs/tr/guides/integrations.md index 7d1731ca433..cf0b3872d37 100644 --- a/docs-site/src/content/docs/tr/guides/integrations.md +++ b/docs-site/src/content/docs/tr/guides/integrations.md @@ -223,7 +223,10 @@ asla gerçek bir kimlik bilgisi yazılmaz. **`ocx opencode` için başlatıcının sağlayıcı bloğu kazanır.** Bu başlatıcı, `provider.opencodex`'i diskteki aynı girdiden üstün olan `OPENCODE_CONFIG_CONTENT` aracılığıyla enjekte eder — opencode yapılandırmanızın -geri kalanı her zamanki gibi geçerli olmaya devam eder. Buradaki anahtar, +geri kalanı her zamanki gibi geçerli olmaya devam eder. Başlatıcının yazdığı bloklar, +dışa aktarımlarla aynı geçerli kanonik model meta verisinden (bilinen yetenekler ve +akıl yürütme seçenekleri) kurulur; bilinmeyen değerler uydurulmaz, yalnızca çıkış sınırı +bilinmiyorsa şemanın zorunlu 32000 yedeğine (bağlama göre kırpılmış) düşer. Buradaki anahtar, `opencode`'u doğrudan başlattığınızda önemlidir. ## Terminalden @@ -256,11 +259,12 @@ ocx mcode ``` Bağlandıktan sonra `ocx sync` ve `POST /api/sync`, yönetilen MCode, Pi, Aside, -Raycast ve omo kataloglarını yeniler. Proxy başlangıcı da yönetilen Raycast +Raycast, omo, OpenCode ve Kilo kataloglarını yeniler. Proxy başlangıcı da yönetilen Raycast kataloğunu yeniler. Model görünürlüğü, sağlayıcı veya ön ayar değişiklikleri Pi, -Aside, Raycast ve omo kataloglarını günceller. Eksik, dışarıdan düzenlenmiş, güvenli olmayan +Aside, Raycast, omo, OpenCode ve Kilo kataloglarını günceller. Eksik, dışarıdan düzenlenmiş, güvenli olmayan veya elle kaldırılmış bloklara dokunmaz; yeniden bağlamak istediğinizde -entegrasyonu açıkça etkinleştirin. +entegrasyonu açıkça etkinleştirin. Güncellenen dosyayı yüklemek için yeni bir +Pi, OpenCode veya Kilo oturumu başlatın. Ayrı MiniMax platform CLI'si (`mmx`) bir dosya anahtarı entegrasyonu değildir. Metin komutları MiniMax'ın Anthropic uyumlu uç noktasını kullandığı için OpenCodex, @@ -329,7 +333,7 @@ ocx integration client restore --op ## Kilo -Kilo yalnızca `~/.config/kilo` altındaki ilk mevcut genel dosyada `provider.opencodex` yazar (`XDG_CONFIG_HOME` bu dizini taşır; hiçbir aday yoksa `kilo.jsonc` oluşturulur). Başka bir aday dosya da `provider.opencodex` tanımlıyorsa durum çakışma bildirir ve Uygula işlemi reddedilir. Diğer anahtarlar değişmez. Uygula dosyanın tamamını yeniden yazar; yorumlar ve sondaki virgüller korunmaz. Kilo’da `opencodex/` seçin. +Kilo yalnızca `~/.config/kilo` altındaki ilk mevcut genel dosyada `provider.opencodex` yazar (`XDG_CONFIG_HOME` bu dizini taşır; hiçbir aday yoksa `kilo.jsonc` oluşturulur). Başka bir aday dosya da `provider.opencodex` tanımlıyorsa durum çakışma bildirir ve Uygula işlemi reddedilir. Diğer anahtarlar değişmez. Modellerin akıl yürütme seçenekleri, sağlayıcı bloğu içinde bir varyant haritası olarak OpenCode dışa aktarımıyla aynı geçerli kanonik meta veriden yazılır. `ocx sync` ve `POST /api/sync` OpenCodex'e ait Kilo bloğunu yeniler; güncellenen dosyayı yeni bir Kilo oturumu yükler. Uygula dosyanın tamamını yeniden yazar; yorumlar ve sondaki virgüller korunmaz. Kilo’da `opencodex/` seçin. Başka bir aday çakışsa veya ayrıştırılamasa bile Devre Dışı Bırak, kaydedilen dosyadaki OpenCodex'e ait bloğu kaldırabilir; diğer aday dosya değişmez. diff --git a/docs-site/src/content/docs/zh-cn/guides/integrations.md b/docs-site/src/content/docs/zh-cn/guides/integrations.md index 90018e3100a..9510139aae3 100644 --- a/docs-site/src/content/docs/zh-cn/guides/integrations.md +++ b/docs-site/src/content/docs/zh-cn/guides/integrations.md @@ -39,7 +39,7 @@ modelProfile: 具有受支持推理强度梯度的 GJC 模型会导出 `reasoning: true`、`thinking.levels` 和 `compat.supportsReasoningEffort`,让 GJC 提供强度选择。原生 Codex 模型即使未在目录中列出梯度,也会获得标准梯度。没有已知梯度的模型会省略这些字段;`none` 不发送强度,而 `ultra` 在传输时折叠为 `max`,因此不会列为选项。刷新集成即可更新模型选项。 -托管 OpenCode 集成管理两个片段:`provider.opencodex`(opencode V1)和 `providers.opencodex`(opencode V2)。只有 V2 配置块包含各模型的推理强度变体,因此两者都会写入并保持同步;它们使用相同的提供商与模型 id,opencode V2 会将它们合并为一个提供商条目。Apply、Refresh、Disable 和 Restore 都作用于两个片段;其他提供商、代理、快捷键与 MCP 条目保持不变。 +托管 OpenCode 集成管理两个片段:`provider.opencodex`(opencode V1)和 `providers.opencodex`(opencode V2)。两个片段携带相同的各模型已声明推理选择与默认值:旧版块以模型 options 中的默认值加变体映射表达,V2 块以模型 settings 加原生变体数组表达,因此两者都会写入并保持同步;它们使用相同的提供商与模型 id,opencode V2 会将它们合并为一个提供商条目。Apply、Refresh、Disable 和 Restore 都作用于两个片段;其他提供商、代理、快捷键与 MCP 条目保持不变。这两个片段都基于生效的规范模型元数据构建——Kilo 导出和 `ocx opencode` 启动器也是如此。显式自定义覆盖始终优先,被清空的推理阶梯保持为空;不会为未声明阶梯或默认值的模型捏造任何值。携带权威的上下文上限时会同时携带输出上限:已知的输出值(显式或目录元数据)优先,否则使用 schema 必需的 32000 回退值(按上下文截断);没有权威上下文上限时整个 limit 块都会省略。能力、推理阶梯、默认值和可选的输入上限只在已知时写入。V2 块的原生 capabilities 对象需要已知的 tools 值,因此工具支持未知时会整体省略而不写残缺的对象,旧版块中的模态声明保持不变。不捏造阶梯,但仍会写入控制而非选项:V2 块始终携带显式的变体数组——无声明时为空数组,因为省略会让 OpenCode 自行合成 low/medium/high;已知有固定推理但无可调阶梯的模型获得的是仅禁用的抑制而非推理强度:旧版 OpenCode 配置块和 Kilo 都会禁用客户端原本会生成的每一个阶梯 id。两者都不会添加可选的推理强度。可选变体可以覆盖已知时写入的逐模型推理默认值——包括 `none`,且仅当模型自身声明的阶梯包含它时才会提供。无论客户端选择什么,代理面向上游的固定策略仍作用于实际发出的请求。 托管 DSH 支持的最低兼容版本为 **DSH 0.1.0-rc.6**。OpenCodex 只管理 `llm-pi-ai.providers.opencodex`;Apply 和 Refresh 替换该片段,Disable 只移除该片段,Restore 恢复已记录的快照。DSH 会热重载提供商变更。这些操作不会改变用户的默认模型或原生 `deepseek-official` 提供商。托管 DSH 集成目前仅支持回环地址,绝不会写入真实凭据。 @@ -115,7 +115,7 @@ TOML 日期和时间也会拒绝托管重写,因为合并步骤会将这些带 **Kimi Code 不能保存环境变量引用,**因此其配置包含 `opencodex-loopback` 占位符而非密钥。任何客户端配置中都不会写入真实凭据。 -**对于 `ocx opencode`,启动器的提供商配置块优先。** 启动器通过 `OPENCODE_CONFIG_CONTENT` 注入 `provider.opencodex` 和 `providers.opencodex`,其优先级高于磁盘上的相同条目;其余 opencode 配置照常生效。直接启动 `opencode` 时,此处的开关才是关键。 +**对于 `ocx opencode`,启动器的提供商配置块优先。** 启动器通过 `OPENCODE_CONFIG_CONTENT` 注入 `provider.opencodex` 和 `providers.opencodex`,其优先级高于磁盘上的相同条目;其余 opencode 配置照常生效。这些配置块与导出使用相同的生效规范模型元数据构建,因此启动器和托管集成描述的是同一批模型、上限和推理选项。直接启动 `opencode` 时,此处的开关才是关键。 ## 从终端操作 @@ -144,7 +144,7 @@ ocx integration client enable --client mcode ocx mcode ``` -连接后,`ocx sync` 和 `POST /api/sync` 会按当前模型选择、上下文窗口及推理强度级别刷新已管理的 MCode、Pi、Aside、Raycast 和 omo 目录。代理启动时会刷新已管理的 Raycast 目录。模型可见性、提供商选择或预设变化,也会刷新已连接的 Pi、Aside、Raycast 和 omo 目录。缺失、被外部编辑或不安全的配置块不会被触碰;此前归 OpenCodex 管理、但被你手动删除的配置块也不会重建。已启用的 Aside 配置文件是“仅刷新已管理配置块”规则的例外:如果账户目录存在且从未有过已管理配置块,当该位置为空时,同步可以创建首个配置块。此前连接过 Aside 会默认对所有已注册配置文件启用这一行为。同步不会创建缺失的账户目录,也不会替换手动配置块。拒绝或重叠的刷新会按客户端分别报告。启动新 Pi 会话,或完全退出并重新打开 Aside,才能加载更新后的文件。Aside 刷新要求[运行中的兼容代理](#aside-配置文件控制)。 +连接后,`ocx sync` 和 `POST /api/sync` 会按当前模型选择、上下文窗口及推理强度级别刷新已管理的 MCode、Pi、Aside、Raycast、omo、OpenCode 和 Kilo 目录。代理启动时会刷新已管理的 Raycast 目录。模型可见性、提供商选择或预设变化,也会刷新已连接的 Pi、Aside、Raycast、omo、OpenCode 和 Kilo 目录。缺失、被外部编辑或不安全的配置块不会被触碰;此前归 OpenCodex 管理、但被你手动删除的配置块也不会重建。已启用的 Aside 配置文件是“仅刷新已管理配置块”规则的例外:如果账户目录存在且从未有过已管理配置块,当该位置为空时,同步可以创建首个配置块。此前连接过 Aside 会默认对所有已注册配置文件启用这一行为。同步不会创建缺失的账户目录,也不会替换手动配置块。拒绝或重叠的刷新会按客户端分别报告。启动新的 Pi、OpenCode 或 Kilo 会话,或完全退出并重新打开 Aside,才能加载更新后的文件。Aside 刷新要求[运行中的兼容代理](#aside-配置文件控制)。 如果 Models 同时显示 **“Model selection saved”** 和客户端刷新警告,说明选择已保存,但一个或多个客户端文件未能更新。警告会指出受影响的客户端,以及适用时的 Aside 配置文件,并解释拒绝原因。启动新会话前,请打开 **Integrations** 检查该客户端或配置文件。处理报告的问题后,重试 `ocx sync`;重叠操作必须先完成。如果警告包含备份路径,或指出恢复未完成,请在重试前检查恢复状态。仅有选择保存成功的提示,并不能证明客户端文件恢复完成。 @@ -222,9 +222,9 @@ Kilo CLI、VS Code 和 JetBrains 共用一份全局配置。此集成只在 `~/. Kilo 会合并所有这些全局文件。如果另一个候选文件也定义了 `provider.opencodex`,状态会列出冲突文件,应用和替换都会被拒绝。启用集成前,请从那些文件中移除 `provider.opencodex`。即使发生这种冲突,仍可禁用已归 OpenCodex 所有的配置块。无法读取或不安全的候选文件也会阻止写入。 -仅 `provider.opencodex` 属于此集成,采用 OpenCode V1 结构(`npm`、`options`、`models`);不会输出 OpenCode V2 的 `providers` 键。`$schema`、`model`、`enabled_providers`、MCP 等键仍由用户管理。应用后,在 Kilo 中选择 `opencodex/`。 +仅 `provider.opencodex` 属于此集成,采用 OpenCode V1 结构(`npm`、`options`、`models`);不会输出 OpenCode V2 的 `providers` 键。模型级的推理选择会以变体映射的形式写入 provider 配置块,与 OpenCode 导出使用相同的生效规范元数据。`$schema`、`model`、`enabled_providers`、MCP 等键仍由用户管理。应用后,在 Kilo 中选择 `opencodex/`。 -回环连接使用 `{env:OPENCODEX_KILO_API_KEY}` 作为 `options.apiKey`。非回环绑定将认证移到 `options.headers["x-opencodex-api-key"]`,且不会写入真实密钥。应用时会将整个全局文件重写为格式化 JSON,因此其他键中的注释和尾随逗号不会保留。Kilo 不参与自动目录刷新;更改路由模型选择后,请明确刷新此集成。 +回环连接使用 `{env:OPENCODEX_KILO_API_KEY}` 作为 `options.apiKey`。非回环绑定将认证移到 `options.headers["x-opencodex-api-key"]`,且不会写入真实密钥。应用时会将整个全局文件重写为格式化 JSON,因此其他键中的注释和尾随逗号不会保留。Kilo 参与自动目录刷新:`ocx sync`、`POST /api/sync` 以及可见性、提供商或预设变更,会按与其他客户端相同的仅刷新自有配置块的安全规则刷新归 OpenCodex 所有的 Kilo 配置块,新的 Kilo 会话会加载更新后的文件。 ```bash ocx integration client enable --client kilo diff --git a/docs-site/src/content/docs/zh-cn/guides/minimax.md b/docs-site/src/content/docs/zh-cn/guides/minimax.md index 48e633e63e7..4ab74ee3a00 100644 --- a/docs-site/src/content/docs/zh-cn/guides/minimax.md +++ b/docs-site/src/content/docs/zh-cn/guides/minimax.md @@ -10,6 +10,10 @@ MiniMax 发布两种不同的命令行产品。OpenCodex 在它们实际提供 ## MiniMax Code +在 OpenCodex 的模型管理页面,MiniMax Coding Plan 两个区域的 `MiniMax-M3` 和 +`MiniMax-M3.1-Flash-Preview` 均声明支持图片输入。只有所有选定模型均支持图片输入时, +Combo 的“图片 / 多模态”开关才可用。该开关控制图片附件,不提供视频上传能力。 + 先按照 MiniMax 的说明安装并登录 MiniMax Code,然后启动 OpenCodex 并连接可逆的文件集成: ```bash 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 5b1a432af82..e2235e720bb 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 @@ -78,6 +78,10 @@ Logs 可组合界面、被拦截请求、提供商、完整模型名、状态、 ## 模型可见性 +开关会立即响应,保存则按点击顺序在后台执行,因此可以连续调整多个模型。队列处理完并与服务器 +核对列表后,才显示保存结果。保存失败会提示错误,并在能读取服务器状态时恢复实际选择。 +请等保存结果出现后再离开模型页或切换服务器;离开时会丢弃尚未发送的排队修改。 + **Models** 开关表示 Codex 中的最终可见状态。路由模型只有在 provider allowlist 中(或未设置 allowlist)且未被禁用时才会开启。开启模型会原子地协调两个过滤条件;**全部开启** 会清除 allowlist,因此以后新发现的模型也会开启。 ### 在提供方工作区管理模型 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 20a31542115..8227cd2b9d5 100644 --- a/docs-site/src/content/docs/zh-tw/guides/integrations.md +++ b/docs-site/src/content/docs/zh-tw/guides/integrations.md @@ -119,7 +119,7 @@ TOML 日期與時間值也會阻止自動重寫:合併步驟會將這些帶有 **Kimi Code 無法持有環境變數參考,** 所以它的設定攜帶的是 `opencodex-loopback` 佔位符而非金鑰。絕不會有任何真實憑證被寫入任何客戶端設定。 -**對 `ocx opencode` 而言,launcher 的 provider 區塊勝出。** 那個 launcher 透過 `OPENCODE_CONFIG_CONTENT` 注入 `provider.opencodex`,比磁碟上相同的條目優先——你其餘的 opencode 設定仍照常套用。當你直接啟動 `opencode` 時,這裡的開關才是關鍵。 +**對 `ocx opencode` 而言,launcher 的 provider 區塊勝出。** 那個 launcher 透過 `OPENCODE_CONFIG_CONTENT` 注入 `provider.opencodex`,比磁碟上相同的條目優先——你其餘的 opencode 設定仍照常套用。launcher 寫入的區塊與匯出使用相同的生效規範模型中繼資料(已知能力與推論選項),未知值不會被捏造;僅輸出上限在未知時使用 schema 必需的 32000 後備值(按 context 截斷)。當你直接啟動 `opencode` 時,這裡的開關才是關鍵。 ## 從終端機 @@ -150,10 +150,10 @@ ocx mcode ``` 完成一次連接後,`ocx sync` 與 `POST /api/sync` 會更新 OpenCodex 已擁有的 -MCode、Pi、Aside、Raycast 與 omo 目錄。proxy 啟動也會更新已擁有的 Raycast 目錄。 -模型可見性、provider 或 preset 變更會更新 Pi、Aside、Raycast 與 omo。若區塊已刪除、 +MCode、Pi、Aside、Raycast、omo、OpenCode 與 Kilo 目錄。proxy 啟動也會更新已擁有的 Raycast 目錄。 +模型可見性、provider 或 preset 變更會更新 Pi、Aside、Raycast、omo、OpenCode 與 Kilo。若區塊已刪除、 遭外部修改、不安全或由你手動移除,sync 會保持原檔不動;只有在你確定要重新 -連接時才再次執行 enable。 +連接時才再次執行 enable。啟動新的 Pi、OpenCode 或 Kilo 工作階段,才能載入更新後的檔案。 另一個 MiniMax 平台 CLI(`mmx`)不是檔案開關整合。其文字命令使用 MiniMax 的 Anthropic 相容端點,因此 OpenCodex 提供憑證隔離、僅限 loopback 的 launcher: @@ -206,7 +206,7 @@ ocx integration client restore --op ## Kilo -Kilo 只會把 `provider.opencodex` 寫入 `~/.config/kilo` 下最先存在的全域檔(`XDG_CONFIG_HOME` 會移動該目錄;若沒有任何候選檔則建立 `kilo.jsonc`)。若另一個候選檔也定義 `provider.opencodex`,狀態會回報衝突且套用會拒絕。其他鍵保持不變。套用會重寫整個檔案,因此不會保留註解與尾隨逗號。請在 Kilo 中選擇 `opencodex/<模型>`。 +Kilo 只會把 `provider.opencodex` 寫入 `~/.config/kilo` 下最先存在的全域檔(`XDG_CONFIG_HOME` 會移動該目錄;若沒有任何候選檔則建立 `kilo.jsonc`)。若另一個候選檔也定義 `provider.opencodex`,狀態會回報衝突且套用會拒絕。其他鍵保持不變。套用會重寫整個檔案,因此不會保留註解與尾隨逗號。每個模型的推論選擇會以變體映射的形式寫入 provider 區塊,與 OpenCode 匯出使用相同的生效規範中繼資料。`ocx sync` 與 `POST /api/sync` 會重新整理 OpenCodex 擁有的 Kilo 區塊,新的 Kilo 工作階段會載入更新後的檔案。請在 Kilo 中選擇 `opencodex/<模型>`。 即使其他候選檔發生衝突或無法剖析,停用仍可移除已記錄檔案中由 OpenCodex 管理的區塊;其他候選檔不會變動。 diff --git a/gui/design-system/components.md b/gui/design-system/components.md index 309bbaf5948..bbc15fe18d9 100644 --- a/gui/design-system/components.md +++ b/gui/design-system/components.md @@ -8,6 +8,7 @@ - `.nav-item`은 아이콘 17px, control text, 4px 세로 간격을 사용한다. - hover와 active는 같은 surface family를 쓰되 active는 semibold로 구분한다. - 메뉴마다 margin을 직접 추가하지 않고 `.sidebar nav`의 `gap`을 사용한다. +- 데스크톱 레이아웃에서 `.sidebar nav`는 스크롤 영역이고 footer(언어, 테마, 줌, 프록시)는 항상 보인다. 창이 낮거나 페이지 줌으로 CSS 뷰포트가 줄어도 footer 컨트롤에 닿을 수 있어야 한다. 데스크톱 셸의 줌 컨트롤(`.zoom-control`)은 테마 행과 같은 행 높이를 쓴다. ## Buttons diff --git a/gui/index.html b/gui/index.html index 33b3b525656..10cc833c9f8 100644 --- a/gui/index.html +++ b/gui/index.html @@ -17,6 +17,7 @@ // FOUT guard: set before first paint so :lang() font stacks apply immediately. // Mirrors detectInitial() + LOCALES in src/i18n/shared.ts. var langMap = { en: "en", de: "de", fr: "fr", ko: "ko", zh: "zh-CN", "zh-TW": "zh-TW", ru: "ru", ja: "ja", tr: "tr", vi: "vi" }; + langMap.pt = "pt-BR"; var stored = null; try { stored = localStorage.getItem("ocx-lang"); @@ -29,7 +30,7 @@ if (nav.indexOf("zh") === 0) { locale = /tw|hk|mo|hant/.test(nav) ? "zh-TW" : "zh"; } else { - locale = ["de", "fr", "ko", "ru", "ja", "tr", "vi"].filter(function (c) { return nav.indexOf(c) === 0; })[0] || "en"; + locale = ["de", "fr", "ko", "ru", "ja", "tr", "vi", "pt"].filter(function (c) { return nav.indexOf(c) === 0; })[0] || "en"; } } document.documentElement.lang = langMap[locale]; diff --git a/gui/src/App.tsx b/gui/src/App.tsx index a12548a13a9..f6b49bfffde 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -30,7 +30,10 @@ import { requestProxyStop } from "./stop-proxy"; import { useCodexRestart } from "./use-codex-restart"; import { confirmAction } from "./action-dialogs"; import { hostOs, isDesktopShell, isExternalLink, openDesktopUpdatePage } from "./lib/desktop-shell"; +import { zoomManagedOn } from "./lib/desktop-zoom"; import { useSidebarCollapse } from "./use-sidebar-collapse"; +import { useDesktopZoom } from "./use-desktop-zoom"; +import { DesktopZoomControl } from "./components/desktop-zoom-control"; import { MainTopStrip, SidebarTopStrip } from "./components/app-titlebar"; import { watchMacTitlebarMetrics, windowChromeHandlers } from "./lib/window-chrome"; @@ -212,6 +215,8 @@ export default function App() { const desktopShell = isDesktopShell(); const { collapsed: navCollapsed, toggle: toggleNavCollapse } = useSidebarCollapse({ shortcut: desktopShell }); const desktopMac = desktopShell && hostOs() === "macos"; + const zoomManaged = desktopShell && zoomManagedOn(hostOs()); + const desktopZoom = useDesktopZoom({ managed: zoomManaged }); const appRef = useRef(null); useEffect(() => { if (desktopMac && appRef.current) return watchMacTitlebarMetrics(appRef.current); @@ -484,6 +489,10 @@ export default function App() { aria-label={`${t("theme.label")}: ${t(THEME_TKEY[theme])}`} title={`${t("theme.label")}: ${t(THEME_TKEY[theme])}`}> {t(THEME_TKEY[theme])} + {zoomManaged && ( + + )}
{t("dash.actions")}
diff --git a/gui/src/components/CompactionRoutingPanel.tsx b/gui/src/components/CompactionRoutingPanel.tsx index 6875b7ab3fa..912a7a028a4 100644 --- a/gui/src/components/CompactionRoutingPanel.tsx +++ b/gui/src/components/CompactionRoutingPanel.tsx @@ -7,9 +7,10 @@ import { requireJson, type ModelInfo } from "../pages/dashboard-shared"; import { comboModelId, parseComboList } from "../combo-workspace-data"; import { formatNamespacedModelId } from "../provider-icons"; -type Setting = { model: string; reasoningEffort?: string; triggers?: string[] } | null; +type Setting = { model: string; reasoningEffort?: string; triggers?: string[]; sourceModels?: string[] } | null; const EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]; const TRIGGERS = ["manual", "auto"]; +const SOURCE_RENDER_CAP = 300; /** * One picker over the two trigger sets the config allows. "manual" is sent as an omitted * `triggers`, so the default save keeps the exact payload the manual-only override used. @@ -20,6 +21,16 @@ const TRIGGER_LABELS: Record = { "manual+auto": "compactionRouting.triggersBoth", "auto": "compactionRouting.triggersAuto", }; +/** + * "all" is sent as an omitted `sourceModels`, so the default save keeps the exact payload the + * unscoped override used. `selected` requires at least one checked selector: the config schema + * rejects an empty list and would silently drop the whole override. + */ +const SCOPE_CHOICES = ["all", "selected"] as const; +const SCOPE_LABELS: Record = { + "all": "compactionRouting.sourcesAll", + "selected": "compactionRouting.sourcesSelected", +}; function triggersToChoice(value: string[] | undefined): string { if (!value) return "manual"; @@ -62,6 +73,9 @@ function readSetting(payload: { compactionRouting?: unknown }): Setting { const effort = "reasoningEffort" in value ? value.reasoningEffort : undefined; if (effort !== undefined && (typeof effort !== "string" || !EFFORTS.includes(effort))) throw new Error("invalid effort"); const triggers = "triggers" in value ? value.triggers : undefined; + const sourceModels = "sourceModels" in value ? value.sourceModels : undefined; + if (sourceModels !== undefined && (!Array.isArray(sourceModels) || !sourceModels.length + || !sourceModels.every(entry => typeof entry === "string" && entry.trim()))) throw new Error("invalid sourceModels"); if (triggers !== undefined && (!Array.isArray(triggers) || triggers.length === 0 || !triggers.every(entry => typeof entry === "string" && TRIGGERS.includes(entry)) || new Set(triggers).size !== triggers.length)) throw new Error("invalid triggers"); @@ -69,19 +83,38 @@ function readSetting(payload: { compactionRouting?: unknown }): Setting { model: value.model, ...(effort ? { reasoningEffort: effort as string } : {}), ...(triggers ? { triggers: triggers as string[] } : {}), + ...(sourceModels ? { sourceModels: sourceModels as string[] } : {}), }; } -export default function CompactionRoutingPanel(props: { apiBase: string; models: ModelInfo[] }) { +/** A provider name says who is configured; the endpoint host says where bytes actually go. */ +function hostOf(baseUrl: string): string { + if (!baseUrl) return ""; + try { + return new URL(baseUrl).hostname || baseUrl; + } catch { + return baseUrl; + } +} + +function endpointLabel(name: string, hosts: ReadonlyMap): string { + const host = hosts.get(name); + return host ? `${name} (${host})` : name; +} + +export default function CompactionRoutingPanel(props: { apiBase: string; models: ModelInfo[]; providers?: Array<{ name: string; baseUrl: string }> }) { return ; } -function CompactionRoutingControls({ apiBase, models }: { apiBase: string; models: ModelInfo[] }) { +function CompactionRoutingControls({ apiBase, models, providers: providerList = [] }: { apiBase: string; models: ModelInfo[]; providers?: Array<{ name: string; baseUrl: string }> }) { const t = useT(); const [saved, setSaved] = useState(undefined); const [model, setModel] = useState(""); const [effort, setEffort] = useState(""); const [triggers, setTriggers] = useState("manual"); + const [scope, setScope] = useState("all"); + const [sources, setSources] = useState([]); + const [sourceQuery, setSourceQuery] = useState(""); const [busy, setBusy] = useState(false); const [loadError, setLoadError] = useState(false); const [feedback, setFeedback] = useState<"saved" | "failed" | null>(null); @@ -94,8 +127,17 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model setModel(value?.model ?? ""); setEffort(value?.reasoningEffort ?? ""); setTriggers(triggersToChoice(value?.triggers)); + setScope(value?.sourceModels ? "selected" : "all"); + setSources(value?.sourceModels ?? []); }, []); + const toggleSource = (selector: string) => { + setSources(current => current.includes(selector) + ? current.filter(entry => entry !== selector) + : [...current, selector]); + setFeedback(null); + }; + const load = useCallback(async () => { if (pending.current) return; const request = createBoundedFetch(15_000); @@ -144,6 +186,7 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model model, ...(effort ? { reasoningEffort: effort } : {}), ...(choiceToTriggers(triggers) ? { triggers: choiceToTriggers(triggers) } : {}), + ...(scope === "selected" && sources.length > 0 ? { sourceModels: sources } : {}), } : null, }), @@ -166,10 +209,37 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model const options = [{ value: "", label: t("compactionRouting.currentModel") }, ...[...new Set([...models.map(item => item.namespaced), ...(model ? [model] : [])])] .map(value => ({ value, label: formatNamespacedModelId(value, t) }))]; + // Configured providers count even without a catalog row: they can still serve + // provider-qualified ids, so their wildcard must be selectable here. + const providerWildcards = [...new Set([ + ...providerList.map(provider => provider.name), + ...models.flatMap(item => item.provider ? [item.provider] : []), + ])].map(provider => `${provider}/*`); + const knownSelectors = new Set([...providerWildcards, ...models.map(item => item.namespaced)]); + // Saved selectors the catalog no longer lists stay visible, so saving never drops them by + // omission and the operator removes them deliberately. + const savedOnlySelectors = (saved?.sourceModels ?? []).filter(selector => !knownSelectors.has(selector)); const disabled = busy || saved === undefined || loadError; + const savedScope = saved?.sourceModels ? "selected" : "all"; + const savedSources = new Set(saved?.sourceModels); const dirty = model !== (saved?.model ?? "") || effort !== (saved?.reasoningEffort ?? "") - || triggers !== triggersToChoice(saved?.triggers); + || triggers !== triggersToChoice(saved?.triggers) + || scope !== savedScope + || (scope === "selected" + && (sources.length !== (saved?.sourceModels?.length ?? 0) + || sources.some(selector => !savedSources.has(selector)))); + const scopeEmpty = scope === "selected" && sources.length === 0; + const selectedSources = new Set(sources); + const filteredSources = [...new Set(models.map(item => item.namespaced))] + .filter(selector => selector.toLowerCase().includes(sourceQuery.trim().toLowerCase())); + const sourceRow = (selector: string) => ( + + ); // Ask what the selection resolves to instead of reading its name. An aliased combo answers // here exactly like a prefixed one (#5216). const comboTargets = comboProviders.get(model); @@ -178,9 +248,21 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model // an ordinary provider named "combo" — worse than the alias gap this fixes. const isCombo = comboTargets !== undefined || model.startsWith(comboModelId("")); const namespace = model.slice(0, Math.max(model.indexOf("/"), 0)); - const provider = isCombo ? "" : (namespace || model); - const providers = comboTargets?.join(", ") || t("compactionRouting.comboProvidersUnknown"); + const endpointHosts = new Map(providerList.map(entry => [entry.name, hostOf(entry.baseUrl)] as const).filter(([, host]) => host)); + const provider = isCombo ? "" : endpointLabel(namespace || model, endpointHosts); + const providers = comboTargets?.map(name => endpointLabel(name, endpointHosts)).join(", ") || t("compactionRouting.comboProvidersUnknown"); const routesAutomatic = triggers !== "manual"; + const scopedSources = sources.join(", "); + // A scoped override only covers the checked sources, so the disclosure names them instead of + // claiming every request; an empty selection covers nothing and shows no destination claim. + const warningText = !model || scopeEmpty ? null + : isCombo + ? (scope === "selected" + ? t("compactionRouting.comboWarningScoped", { combo: model, providers, sources: scopedSources }) + : t("compactionRouting.comboWarning", { combo: model, providers })) + : (scope === "selected" + ? t("compactionRouting.providerWarningScoped", { provider, sources: scopedSources }) + : t("compactionRouting.providerWarning", { provider })); return (
@@ -190,27 +272,66 @@ function CompactionRoutingControls({ apiBase, models }: { apiBase: string; model
{t("compactionRouting.description")}
{t("compactionRouting.dataNotice")}
{t("compactionRouting.effortHint")}
+
{t("compactionRouting.sourcesHint")}
-
- ({ value, label: t(TRIGGER_LABELS[value]!) }))} - onChange={value => { setTriggers(value); setFeedback(null); }} /> - { setModel(value); if (!value) { setEffort(""); setTriggers("manual"); setScope("all"); setSources([]); } setFeedback(null); }} /> +
+
+ + ({ value, label: t(SCOPE_LABELS[value]!) }))} + onChange={value => { setScope(value); setFeedback(null); }} /> +
+
+ + setSourceQuery(event.target.value)} /> + {filteredSources.slice(0, SOURCE_RENDER_CAP).map(sourceRow)} + {filteredSources.length > SOURCE_RENDER_CAP &&

+ {t("pws.modelsTruncated", { shown: SOURCE_RENDER_CAP, total: filteredSources.length })} +

} + } + {savedOnlySelectors.length > 0 &&
+ {t("compactionRouting.sourcesSaved")} + {savedOnlySelectors.map(sourceRow)} +
} + {scopeEmpty &&
{t("compactionRouting.sourcesNone")}
} +
+
} + {warningText &&
{warningText}
} {model && routesAutomatic &&
{t("compactionRouting.autoNotice")}
} {loadError &&
{t("compactionRouting.loadFailed")}
} {feedback === "failed" &&
{t("compactionRouting.saveFailed")}
} diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index ec18b78b65a..fc72b0bb5f3 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -180,6 +180,8 @@ function bcp47(locale: Locale): string { return "tr-TR"; case "vi": return "vi-VN"; + case "pt": + return "pt-BR"; default: { const _exhaustive: never = locale; return _exhaustive; diff --git a/gui/src/components/desktop-zoom-control.tsx b/gui/src/components/desktop-zoom-control.tsx new file mode 100644 index 00000000000..03998109417 --- /dev/null +++ b/gui/src/components/desktop-zoom-control.tsx @@ -0,0 +1,32 @@ +import { IconMinus, IconPlus } from "../icons"; +import { useT } from "../i18n/shared"; +import type { ZoomAction } from "../lib/desktop-zoom"; + +interface DesktopZoomControlProps { + percent: number; + canZoomIn: boolean; + canZoomOut: boolean; + onStep: (action: ZoomAction) => void; +} + +/** Sidebar row for the desktop window's page zoom; the same steps as Ctrl/Cmd + plus, minus and zero. */ +export function DesktopZoomControl({ percent, canZoomIn, canZoomOut, onStep }: DesktopZoomControlProps) { + const t = useT(); + return ( +
+ {t("zoom.label")} + + + +
+ ); +} diff --git a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx index dce977c5c63..992e9d6df3c 100644 --- a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx +++ b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx @@ -27,6 +27,7 @@ function bcp47(locale: Locale): string { case "ja": return "ja-JP"; case "tr": return "tr-TR"; case "vi": return "vi-VN"; + case "pt": return "pt-BR"; default: { const _exhaustive: never = locale; return _exhaustive; diff --git a/gui/src/i18n/catalogs.ts b/gui/src/i18n/catalogs.ts index dd5f41b78c4..d64033f412d 100644 --- a/gui/src/i18n/catalogs.ts +++ b/gui/src/i18n/catalogs.ts @@ -8,6 +8,7 @@ import { ru } from "./ru"; import { ja } from "./ja"; import { tr } from "./tr"; import { vi } from "./vi"; +import { pt } from "./pt"; import { LAB_CATALOG_OVERRIDES, type LabLocale } from "./lab-translations"; /** React-free locale catalog registry for formatters and other shared helpers. */ @@ -33,6 +34,7 @@ export const DICTS: Record> = { ja: withLabTranslations("ja", ja), tr: withLabTranslations("tr", tr), vi: withLabTranslations("vi", vi), + pt: withLabTranslations("pt", pt), }; /** Native language names shown by the language picker, kept inside i18n rather than UI metadata. */ diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 0d41a71b036..0e3083c4811 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -238,6 +238,10 @@ export const de: Record = { "theme.light": "Hell", "theme.dark": "Dunkel", "theme.system": "System", + "zoom.label": "Zoom", + "zoom.out": "Verkleinern", + "zoom.in": "Vergrößern", + "zoom.reset": "Zoom auf 100 % zurücksetzen", "lang.label": "Sprache", "lang.nativeName": "Deutsch", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -435,14 +439,25 @@ export const de: Record = { "compactionRouting.triggersManual": "Nur manuelles /compact", "compactionRouting.triggersBoth": "Manuell und automatisch", "compactionRouting.triggersAuto": "Nur automatisch", + "compactionRouting.sources": "Quellen", + "compactionRouting.sourcesAll": "Alle Konversationsmodelle", + "compactionRouting.sourcesSelected": "Nur ausgewählte Quellen", + "compactionRouting.sourcesProviders": "Ganze Anbieter", + "compactionRouting.sourcesModels": "Einzelne Modelle", + "compactionRouting.sourcesSaved": "Gespeicherte Selektoren außerhalb des aktuellen Katalogs", + "compactionRouting.sourcesNone": "Wähle mindestens eine Quelle oder wechsle zurück zu allen Konversationsmodellen.", + "compactionRouting.sourcesHint": "Wendet die Überschreibung nur an, wenn das Quellmodell einer Komprimierungsanfrage einem ausgewählten Modell oder provider/* entspricht. Andere Anfragen verwenden weiterhin ihr eigenes Modell.", + "compactionRouting.sourcesGridTitle": "Komprimierungsanfragen umleiten, deren Quellmodell hiermit übereinstimmt:", "compactionRouting.currentModel": "Gesprächsmodell verwenden", "compactionRouting.currentEffort": "Anfrageaufwand beibehalten", "compactionRouting.effortHint": "Der Denkaufwand gilt, wenn der Komprimierungsendpunkt ihn unterstützt. Das Modell muss das gesamte Gespräch verarbeiten können.", "compactionRouting.dataNotice": "Eine abgedeckte Komprimierungsanfrage sendet das gesamte Gespräch zur Zusammenfassung an den Anbieter des gewählten Modells, auch wenn das Gespräch bei einem anderen Anbieter läuft.", "compactionRouting.autoNotice": "Automatische Komprimierung läuft von selbst, daher kann ein langes Gespräch an diesen Anbieter gehen, ohne dass du es angefordert hast.", "compactionRouting.providerWarning": "Mit dieser Einstellung sendet jede abgedeckte Komprimierungsanfrage den vollständigen Gesprächsinhalt zur Zusammenfassung an {provider}.", - "compactionRouting.comboWarning": "Mit dieser Einstellung sendet jede abgedeckte Komprimierungsanfrage den vollständigen Gesprächsinhalt zur Zusammenfassung an die Combo {combo}. Die Combo versucht ihre Ziele ({providers}) der Reihe nach und verwendet das erste, das antwortet, sodass jedes davon das Gespräch erhalten kann.", + "compactionRouting.comboWarning": "Mit dieser Einstellung sendet jede abgedeckte Komprimierungsanfrage den vollständigen Gesprächsinhalt zur Zusammenfassung an die Combo {combo}. Die Combo wählt nach ihrer Routing-Strategie eines ihrer Ziele ({providers}) aus, kann dieselbe Anfrage mit dem vollständigen Gesprächsinhalt nach einem wiederholbaren Fehler aber bei einem anderen Ziel wiederholen, sodass eines oder mehrere Zielanbieter das Gespräch erhalten können.", "compactionRouting.comboProvidersUnknown": "ihre konfigurierten Zielanbieter", + "compactionRouting.providerWarningScoped": "Mit dieser Einstellung senden nur Komprimierungsanfragen, deren Quellmodell {sources} entspricht, den vollständigen Gesprächsinhalt zur Zusammenfassung an {provider}; andere Anfragen verwenden weiterhin ihr eigenes Modell.", + "compactionRouting.comboWarningScoped": "Mit dieser Einstellung senden nur Komprimierungsanfragen, deren Quellmodell {sources} entspricht, den vollständigen Gesprächsinhalt zur Zusammenfassung an die Combo {combo}; andere Anfragen verwenden weiterhin ihr eigenes Modell. Die Combo wählt nach ihrer Routing-Strategie eines ihrer Ziele ({providers}) aus, kann dieselbe Anfrage mit dem vollständigen Gesprächsinhalt nach einem wiederholbaren Fehler aber bei einem anderen Ziel wiederholen, sodass eines oder mehrere Zielanbieter das Gespräch erhalten können.", "compactionRouting.loadFailed": "Komprimierungseinstellungen konnten nicht geladen werden.", "compactionRouting.saved": "Komprimierungseinstellungen gespeichert.", "compactionRouting.saveFailed": "Speichern fehlgeschlagen. Deine Änderungen sind noch vorhanden; versuche es erneut.", @@ -1011,7 +1026,7 @@ export const de: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Dekodierrate (geschätzt)", "logs.detail.reason.ttft_missing": "Für diese Anfrage wurde keine Zeit bis zum ersten Token erfasst, es gibt also kein Dekodierfenster zum Messen.", - "logs.detail.reason.decode_window_too_short": "Das Fenster nach dem ersten Token lag unter einer Sekunde und ist zu kurz für eine Schätzung der Dekodierrate.", + "logs.detail.reason.decode_window_too_short": "Das gemessene Ausgabefenster lag unter einer Sekunde und ist zu kurz für eine Schätzung der Dekodierrate.", "logs.detail.costTotal": "Listenpreis-Äquivalent", "logs.detail.totalTokens": "Tokens gesamt", "logs.detail.matchedKey": "Zugeordneter Preisschlüssel", @@ -1267,9 +1282,12 @@ export const de: Record = { "usage.col.reported": "Gemeldet", "usage.col.inputTokens": "Eingabe-Tokens", "usage.col.outputTokens": "Ausgabe-Tokens", + "usage.col.tokPerSec": "Ausgabe tok/s", "usage.col.cacheHits": "Cache-Treffer", "usage.col.cacheWrites": "Cache-Schreibvorgänge", "usage.col.cacheHitRate": "Trefferquote", + "usage.throughput.title": "End-to-End-Ausgabedurchsatz: Summe der Ausgabe-Tokens geteilt durch die Summe der Wanduhrzeiten über {samples} gemessene Versuche. Enthält die Wartezeit vor der Dekodierung und liegt daher unter der stabilen Dekodiergeschwindigkeit; Anfragen ohne gemessene Tokens oder Dauer werden ausgeschlossen.", + "usage.throughput.unmeasured": "Keine Anfrage dieser Zeile hat sowohl Ausgabe-Tokens als auch Dauer gemeldet, daher gibt es keinen Durchschnitt.", "usage.cacheHitRate.partial": "Gemittelt über {measured} von insgesamt {total} Eingabe-Tokens; für die übrigen Anfragen wurden keine Cache-Details gemeldet.", "usage.cacheHitRate.unmeasured": "Für keine Anfrage in dieser Zeile wurden Cache-Details gemeldet, daher kann keine durchschnittliche Trefferquote berechnet werden.", "usage.unavailable": "—", @@ -1483,6 +1501,16 @@ export const de: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Standard für Reasoning-Aufwand", + "integrations.droidReasoning.description": "Der Standard gilt nur, wenn eine Anfrage keinen Reasoning-Aufwand angibt. Deaktiviere Factory Droid, um diese gespeicherten Standards zu entfernen.", + "integrations.droidReasoning.noDefault": "Kein Standard", + "integrations.droidReasoning.noEfforts": "Keine Optionen für die Reasoning-Stärke verfügbar", + "integrations.droidReasoning.noModels": "Keine exportierten Modelle verfügbar.", + "integrations.droidReasoning.modelDefault": "Standard-Reasoning-Aufwand für {model}", + "integrations.droidReasoning.unsupportedTitle": "Einige gespeicherte Standards werden nicht mehr unterstützt. Entferne sie; speichere und prüfe anschließend die Änderungen, um sie anzuwenden.", + "integrations.droidReasoning.unsupportedEffort": "Nicht unterstützter Aufwand: {effort}", + "integrations.droidReasoning.clear": "Standard entfernen", + "integrations.droidReasoning.saveReview": "Änderungen speichern / prüfen", "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", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index b71168448c5..b34dfaa62a7 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -176,6 +176,10 @@ export const en = { "theme.light": "Light", "theme.dark": "Dark", "theme.system": "System", + "zoom.label": "Zoom", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.reset": "Reset zoom to 100%", "lang.label": "Language", "lang.nativeName": "English", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -453,14 +457,25 @@ export const en = { "compactionRouting.triggersManual": "Manual /compact only", "compactionRouting.triggersBoth": "Manual and automatic", "compactionRouting.triggersAuto": "Automatic only", + "compactionRouting.sources": "Sources", + "compactionRouting.sourcesAll": "All conversation models", + "compactionRouting.sourcesSelected": "Selected sources only", + "compactionRouting.sourcesProviders": "Entire providers", + "compactionRouting.sourcesModels": "Individual models", + "compactionRouting.sourcesSaved": "Saved selectors not in the current catalog", + "compactionRouting.sourcesNone": "Select at least one source, or switch back to all conversation models.", + "compactionRouting.sourcesHint": "Apply the override only when a compaction request's source model matches a selected model or provider/*. Other requests keep using their own model.", + "compactionRouting.sourcesGridTitle": "Reroute compaction requests whose source model matches:", "compactionRouting.currentModel": "Use conversation model", "compactionRouting.currentEffort": "Keep request effort", "compactionRouting.effortHint": "Reasoning applies where supported by the compaction endpoint. The model must accept the full conversation.", "compactionRouting.dataNotice": "A covered compaction request sends the entire conversation to the selected model's provider for summarization, even when the conversation runs on another provider.", "compactionRouting.autoNotice": "Automatic compaction runs on its own, so a long conversation can be sent to that provider without you asking for it.", "compactionRouting.providerWarning": "With this setting, every covered compaction request sends the full conversation contents to {provider} for summarization.", - "compactionRouting.comboWarning": "With this setting, every covered compaction request sends the full conversation contents to combo {combo} for summarization. The combo attempts its targets ({providers}) in order and uses the first that answers, so any one of them can receive the conversation.", + "compactionRouting.comboWarning": "With this setting, every covered compaction request sends the full conversation contents to combo {combo} for summarization. The combo picks one of its targets ({providers}) by its routing strategy, but after a retryable failure it may retry the same full-conversation request on another target, so one or more target providers can receive the conversation.", "compactionRouting.comboProvidersUnknown": "its configured target providers", + "compactionRouting.providerWarningScoped": "With this setting, only compaction requests whose source model matches {sources} send the full conversation contents to {provider} for summarization; other requests keep using their own model.", + "compactionRouting.comboWarningScoped": "With this setting, only compaction requests whose source model matches {sources} send the full conversation contents to combo {combo} for summarization; other requests keep using their own model. The combo picks one of its targets ({providers}) by its routing strategy, but after a retryable failure it may retry the same full-conversation request on another target, so one or more target providers can receive the conversation.", "compactionRouting.loadFailed": "Could not load compaction settings.", "compactionRouting.saved": "Compaction settings saved.", "compactionRouting.saveFailed": "Could not save. Your changes are still here; try again.", @@ -1069,7 +1084,7 @@ export const en = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Decode rate (est.)", "logs.detail.reason.ttft_missing": "No time-to-first-token was recorded for this request, so there is no decode window to measure.", - "logs.detail.reason.decode_window_too_short": "The window after the first token was under a second, which is too short to estimate a decode rate from.", + "logs.detail.reason.decode_window_too_short": "The measured output window was under a second, which is too short to estimate a decode rate from.", "logs.detail.costTotal": "List-price equivalent", "logs.detail.totalTokens": "Total tokens", "logs.detail.matchedKey": "Matched price key", @@ -1329,9 +1344,12 @@ export const en = { "usage.col.reported": "Reported", "usage.col.inputTokens": "Input tokens", "usage.col.outputTokens": "Output tokens", + "usage.col.tokPerSec": "Output tok/s", "usage.col.cacheHits": "Cache hits", "usage.col.cacheWrites": "Cache writes", "usage.col.cacheHitRate": "Hit rate", + "usage.throughput.title": "End-to-end output throughput: summed output tokens over summed wall-clock duration across {samples} measured attempt(s). It includes pre-decode wait time, so it runs below steady-state decode speed; requests without measured tokens or duration are excluded.", + "usage.throughput.unmeasured": "No request for this row reported both output tokens and duration, so there is no throughput to average.", "usage.cacheHitRate.partial": "Averaged over {measured} of {total} input tokens; the remaining requests reported no cache detail.", "usage.cacheHitRate.unmeasured": "No request for this row reported cache detail, so there is no hit rate to average.", "usage.unavailable": "—", @@ -2049,6 +2067,16 @@ export const en = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Default reasoning effort", + "integrations.droidReasoning.description": "A default is used only when a request omits its reasoning effort. Disable Factory Droid to remove these saved defaults.", + "integrations.droidReasoning.noDefault": "No default", + "integrations.droidReasoning.noEfforts": "No reasoning-effort options available", + "integrations.droidReasoning.noModels": "No exported models are available.", + "integrations.droidReasoning.modelDefault": "Default reasoning effort for {model}", + "integrations.droidReasoning.unsupportedTitle": "Some saved defaults are no longer supported. Clear them, then save / review changes to apply.", + "integrations.droidReasoning.unsupportedEffort": "Unsupported effort: {effort}", + "integrations.droidReasoning.clear": "Clear default", + "integrations.droidReasoning.saveReview": "Save / review changes", "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", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 01b2f499219..0e9495ed5aa 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -173,6 +173,10 @@ export const fr: Record = { "theme.light": "Clair", "theme.dark": "Sombre", "theme.system": "Système", + "zoom.label": "Zoom", + "zoom.out": "Zoom arrière", + "zoom.in": "Zoom avant", + "zoom.reset": "Réinitialiser le zoom à 100 %", "lang.label": "Langue", "lang.nativeName": "Français", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -443,14 +447,25 @@ export const fr: Record = { "compactionRouting.triggersManual": "/compact manuel uniquement", "compactionRouting.triggersBoth": "Manuelle et automatique", "compactionRouting.triggersAuto": "Automatique uniquement", + "compactionRouting.sources": "Sources concernées", + "compactionRouting.sourcesAll": "Tous les modèles de conversation", + "compactionRouting.sourcesSelected": "Sources sélectionnées uniquement", + "compactionRouting.sourcesProviders": "Fournisseurs entiers", + "compactionRouting.sourcesModels": "Modèles individuels", + "compactionRouting.sourcesSaved": "Sélecteurs enregistrés absents du catalogue actuel", + "compactionRouting.sourcesNone": "Sélectionnez au moins une source, ou revenez à tous les modèles de conversation.", + "compactionRouting.sourcesHint": "Applique le remplacement uniquement si le modèle source de la requête de compaction correspond à un modèle choisi ou à provider/*. Les autres requêtes gardent leur propre modèle.", + "compactionRouting.sourcesGridTitle": "Rerouter les requêtes de compaction dont le modèle source correspond à :", "compactionRouting.currentModel": "Utiliser le modèle de la conversation", "compactionRouting.currentEffort": "Conserver l’effort de la requête", "compactionRouting.effortHint": "Le raisonnement s’applique si le point de terminaison de compaction le prend en charge. Le modèle doit accepter toute la conversation.", "compactionRouting.dataNotice": "Une requête de compaction couverte envoie toute la conversation au fournisseur du modèle choisi pour la résumer, même si la conversation s’exécute chez un autre fournisseur.", "compactionRouting.autoNotice": "La compaction automatique se déclenche d’elle-même : une longue conversation peut donc partir chez ce fournisseur sans que vous l’ayez demandé.", "compactionRouting.providerWarning": "Avec ce réglage, chaque requête de compaction couverte envoie l’intégralité du contenu de la conversation à {provider} pour la résumer.", - "compactionRouting.comboWarning": "Avec ce réglage, chaque requête de compaction couverte envoie l’intégralité du contenu de la conversation au combo {combo} pour le résumer. Le combo essaie ses cibles ({providers}) dans l’ordre et retient la première qui répond, donc n’importe laquelle peut recevoir la conversation.", + "compactionRouting.comboWarning": "Avec ce réglage, chaque requête de compaction couverte envoie l’intégralité du contenu de la conversation au combo {combo} pour le résumer. Le combo choisit l’une de ses cibles ({providers}) selon sa stratégie de routage, mais après un échec réessayable il peut réessayer la même requête avec l’intégralité de la conversation sur une autre cible, donc une ou plusieurs cibles peuvent recevoir la conversation.", "compactionRouting.comboProvidersUnknown": "ses fournisseurs cibles configurés", + "compactionRouting.providerWarningScoped": "Avec ce réglage, seules les requêtes de compaction dont le modèle source correspond à {sources} envoient l’intégralité de la conversation à {provider} pour la résumer ; les autres requêtes gardent leur propre modèle.", + "compactionRouting.comboWarningScoped": "Avec ce réglage, seules les requêtes de compaction dont le modèle source correspond à {sources} envoient l’intégralité de la conversation au combo {combo} pour la résumer ; les autres requêtes gardent leur propre modèle. Le combo choisit l’une de ses cibles ({providers}) selon sa stratégie de routage, mais après un échec réessayable il peut réessayer la même requête avec l’intégralité de la conversation sur une autre cible, donc une ou plusieurs cibles peuvent recevoir la conversation.", "compactionRouting.loadFailed": "Impossible de charger les paramètres de compaction.", "compactionRouting.saved": "Paramètres de compaction enregistrés.", "compactionRouting.saveFailed": "Échec de l’enregistrement. Vos modifications sont conservées ; réessayez.", @@ -1050,7 +1065,7 @@ export const fr: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Débit de décodage (est.)", "logs.detail.reason.ttft_missing": "Aucun délai jusqu’au premier token n’a été enregistré pour cette requête, il n’y a donc pas de fenêtre de décodage à mesurer.", - "logs.detail.reason.decode_window_too_short": "La fenêtre après le premier token durait moins d’une seconde, ce qui est trop court pour estimer un débit de décodage.", + "logs.detail.reason.decode_window_too_short": "La fenêtre de sortie mesurée durait moins d’une seconde, ce qui est trop court pour estimer un débit de décodage.", "logs.detail.costTotal": "Équivalent au tarif catalogue", "logs.detail.totalTokens": "Nombre total de jetons", "logs.detail.matchedKey": "Clé de tarif correspondante", @@ -1306,9 +1321,12 @@ export const fr: Record = { "usage.col.reported": "Communiquées", "usage.col.inputTokens": "Jetons d’entrée", "usage.col.outputTokens": "Jetons de sortie", + "usage.col.tokPerSec": "Sortie tok/s", "usage.col.cacheHits": "Lectures du cache", "usage.col.cacheWrites": "Écritures dans le cache", "usage.col.cacheHitRate": "Taux de succès du cache", + "usage.throughput.title": "Débit de sortie de bout en bout : somme des jetons de sortie divisée par la somme des durées horloge sur {samples} tentative(s) mesurée(s). Il inclut l'attente avant décodage, donc il reste sous la vitesse de décodage stable ; les requêtes sans jetons mesurés ou sans durée mesurée sont exclues.", + "usage.throughput.unmeasured": "Aucune requête de cette ligne n'a rapporté à la fois des jetons de sortie et une durée ; aucun débit à moyenner.", "usage.cacheHitRate.partial": "Moyenne calculée sur {measured} des {total} jetons d’entrée ; les autres requêtes n’ont fourni aucun détail sur le cache.", "usage.cacheHitRate.unmeasured": "Aucune requête de cette ligne n’a fourni de détails sur le cache ; il n’y a donc pas de taux de succès moyen à calculer.", "usage.unavailable": "—", @@ -2021,6 +2039,16 @@ export const fr: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Niveau de raisonnement par défaut", + "integrations.droidReasoning.description": "Le niveau par défaut s’applique uniquement si la requête ne précise aucun niveau de raisonnement. Désactivez Factory Droid pour supprimer ces valeurs enregistrées.", + "integrations.droidReasoning.noDefault": "Aucune valeur par défaut", + "integrations.droidReasoning.noEfforts": "Aucune option de niveau de raisonnement disponible", + "integrations.droidReasoning.noModels": "Aucun modèle exporté n’est disponible.", + "integrations.droidReasoning.modelDefault": "Niveau de raisonnement par défaut pour {model}", + "integrations.droidReasoning.unsupportedTitle": "Certaines valeurs enregistrées ne sont plus prises en charge. Supprimez-les, puis enregistrez / vérifiez les changements pour les appliquer.", + "integrations.droidReasoning.unsupportedEffort": "Niveau non pris en charge : {effort}", + "integrations.droidReasoning.clear": "Supprimer la valeur", + "integrations.droidReasoning.saveReview": "Enregistrer / vérifier les changements", "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", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index e3f01974249..49427bc61a6 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -243,6 +243,10 @@ export const ja: Record = { "theme.light": "ライト", "theme.dark": "ダーク", "theme.system": "システム", + "zoom.label": "表示倍率", + "zoom.out": "縮小", + "zoom.in": "拡大", + "zoom.reset": "倍率を100%に戻す", "lang.label": "言語", "lang.nativeName": "日本語", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -444,14 +448,25 @@ export const ja: Record = { "compactionRouting.triggersManual": "手動の /compact のみ", "compactionRouting.triggersBoth": "手動と自動", "compactionRouting.triggersAuto": "自動のみ", + "compactionRouting.sources": "対象ソース", + "compactionRouting.sourcesAll": "すべての会話モデル", + "compactionRouting.sourcesSelected": "選択したソースのみ", + "compactionRouting.sourcesProviders": "プロバイダー全体", + "compactionRouting.sourcesModels": "個別モデル", + "compactionRouting.sourcesSaved": "現在のカタログにない保存済みセレクター", + "compactionRouting.sourcesNone": "ソースを少なくとも 1 つ選択するか、すべての会話モデルに戻してください。", + "compactionRouting.sourcesHint": "圧縮リクエストの元モデルが選択したモデルまたは provider/* に一致する場合だけ上書きします。一致しないリクエストは元のモデルを使います。", + "compactionRouting.sourcesGridTitle": "元モデルが以下に一致する圧縮リクエストをリルートします:", "compactionRouting.currentModel": "会話のモデルを使用", "compactionRouting.currentEffort": "リクエストの推論強度を維持", "compactionRouting.effortHint": "圧縮エンドポイントが対応している場合に推論設定が適用されます。モデルは会話全体を受け入れられる必要があります。", "compactionRouting.dataNotice": "適用対象の圧縮リクエストは、会話が別のプロバイダーで動いていても、会話全体を選択したモデルのプロバイダーへ送信して要約します。", "compactionRouting.autoNotice": "自動圧縮はユーザーが求めなくても実行されるため、長い会話が知らないうちにそのプロバイダーへ送信されることがあります。", "compactionRouting.providerWarning": "この設定では、適用対象の圧縮リクエストのたびに会話の全内容が要約のために {provider} へ送信されます。", - "compactionRouting.comboWarning": "この設定では、適用対象の圧縮リクエストのたびに会話の全内容が要約のためコンボ {combo} へ送信されます。コンボはターゲット({providers})を順に試し、最初に応答したものを使うため、そのいずれもが会話を受け取る可能性があります。", + "compactionRouting.comboWarning": "この設定では、適用対象の圧縮リクエストのたびに会話の全内容が要約のためコンボ {combo} へ送信されます。コンボはルーティング戦略に従ってターゲット({providers})のいずれかを選びますが、再試行可能な失敗の後には同じ会話全内容のリクエストを別のターゲットで再試行する可能性があるため、ターゲットのうち 1 つ以上が会話を受け取る可能性があります。", "compactionRouting.comboProvidersUnknown": "設定済みのターゲットプロバイダー", + "compactionRouting.providerWarningScoped": "この設定では、元モデルが {sources} に一致する圧縮リクエストだけが会話の全内容を要約のため {provider} へ送信します。一致しないリクエストは元のモデルを使います。", + "compactionRouting.comboWarningScoped": "この設定では、元モデルが {sources} に一致する圧縮リクエストだけが会話の全内容を要約のためコンボ {combo} へ送信します。一致しないリクエストは元のモデルを使います。コンボはルーティング戦略に従ってターゲット({providers})のいずれかを選びますが、再試行可能な失敗の後には同じ会話全内容のリクエストを別のターゲットで再試行する可能性があるため、ターゲットのうち 1 つ以上が会話を受け取る可能性があります。", "compactionRouting.loadFailed": "圧縮設定を読み込めませんでした。", "compactionRouting.saved": "圧縮設定を保存しました。", "compactionRouting.saveFailed": "保存できませんでした。変更内容は保持されています。再試行してください。", @@ -952,7 +967,7 @@ export const ja: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "デコード速度(推定)", "logs.detail.reason.ttft_missing": "このリクエストでは最初のトークンまでの時間が記録されていないため、測定できるデコード区間がありません。", - "logs.detail.reason.decode_window_too_short": "最初のトークン以降の区間が1秒未満で、デコード速度を推定するには短すぎます。", + "logs.detail.reason.decode_window_too_short": "計測された出力区間が1秒未満で、デコード速度を推定するには短すぎます。", "logs.detail.costTotal": "定価相当額", "logs.detail.totalTokens": "合計トークン", "logs.detail.matchedKey": "一致した価格キー", @@ -1212,9 +1227,12 @@ export const ja: Record = { "usage.col.reported": "報告", "usage.col.inputTokens": "入力トークン", "usage.col.outputTokens": "出力トークン", + "usage.col.tokPerSec": "出力 tok/s", "usage.col.cacheHits": "キャッシュヒット", "usage.col.cacheWrites": "キャッシュ書き込み", "usage.col.cacheHitRate": "ヒット率", + "usage.throughput.title": "エンドツーエンドの出力スループット: 出力トークンの合計 ÷ 経過時間の合計。トークンと時間の両方を報告した {samples} 件の試行が対象です。デコード前の待ち時間を含むため定常デコード速度より低く、トークンまたは時間が未報告のリクエストは対象外です。", + "usage.throughput.unmeasured": "この行には出力トークンと時間の両方を報告したリクエストがなく、スループットを平均できません。", "usage.cacheHitRate.partial": "入力トークン全体 {total} のうち {measured} を対象に平均しています。残りのリクエストではキャッシュの詳細が報告されていません。", "usage.cacheHitRate.unmeasured": "この行のリクエストはいずれもキャッシュの詳細を報告していないため、平均ヒット率はありません。", "usage.unavailable": "—", @@ -1916,6 +1934,16 @@ export const ja: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "推論レベルの既定値", + "integrations.droidReasoning.description": "リクエストに推論レベルが指定されていない場合にのみ既定値が使われます。Factory Droid を無効にすると、保存済みの既定値も削除されます。", + "integrations.droidReasoning.noDefault": "既定値なし", + "integrations.droidReasoning.noEfforts": "利用可能な推論強度の選択肢はありません", + "integrations.droidReasoning.noModels": "エクスポートされたモデルはありません。", + "integrations.droidReasoning.modelDefault": "{model} の推論レベルの既定値", + "integrations.droidReasoning.unsupportedTitle": "一部の保存済み既定値はサポートされなくなりました。削除した後、変更を保存 / 確認して適用してください。", + "integrations.droidReasoning.unsupportedEffort": "未サポートのレベル: {effort}", + "integrations.droidReasoning.clear": "既定値を削除", + "integrations.droidReasoning.saveReview": "変更を保存 / 確認", "integrations.aside.profilesTitle": "Asideのプロファイル", "integrations.aside.profilesHint": "選択したモデルを同期するプロファイルを選んでください。Asideで使用中のプロファイルは変わりません。", "integrations.aside.all": "すべてのプロファイルを同期", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 7c667aadc75..e4183eb6e02 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -238,6 +238,10 @@ export const ko: Record = { "theme.light": "라이트", "theme.dark": "다크", "theme.system": "시스템", + "zoom.label": "확대/축소", + "zoom.out": "축소", + "zoom.in": "확대", + "zoom.reset": "확대/축소를 100%로 재설정", "lang.label": "언어", "lang.nativeName": "한국어", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -439,14 +443,25 @@ export const ko: Record = { "compactionRouting.triggersManual": "수동 /compact만", "compactionRouting.triggersBoth": "수동과 자동", "compactionRouting.triggersAuto": "자동만", + "compactionRouting.sources": "소스 범위", + "compactionRouting.sourcesAll": "모든 대화 모델", + "compactionRouting.sourcesSelected": "선택한 소스만", + "compactionRouting.sourcesProviders": "전체 제공자", + "compactionRouting.sourcesModels": "개별 모델", + "compactionRouting.sourcesSaved": "현재 카탈로그에 없는 저장된 선택자", + "compactionRouting.sourcesNone": "소스를 하나 이상 선택하거나 모든 대화 모델로 되돌리세요.", + "compactionRouting.sourcesHint": "압축 요청의 원본 모델이 선택한 모델 또는 provider/*와 일치할 때만 재정의를 적용합니다. 일치하지 않는 요청은 원래 모델을 사용합니다.", + "compactionRouting.sourcesGridTitle": "원본 모델이 다음 선택 항목과 일치하는 압축 요청을 다시 라우팅합니다:", "compactionRouting.currentModel": "대화 모델 사용", "compactionRouting.currentEffort": "요청의 추론 수준 유지", "compactionRouting.effortHint": "압축 엔드포인트가 지원하는 경우 추론 설정이 적용됩니다. 모델은 전체 대화를 수용할 수 있어야 합니다.", "compactionRouting.dataNotice": "적용 대상 압축 요청은 대화가 다른 프로바이더에서 실행 중이더라도 전체 대화를 선택한 모델의 프로바이더로 보내 요약합니다.", "compactionRouting.autoNotice": "자동 압축은 사용자가 요청하지 않아도 실행되므로, 긴 대화가 예고 없이 해당 프로바이더로 전송될 수 있습니다.", "compactionRouting.providerWarning": "이 설정을 사용하면 적용 대상 압축 요청마다 전체 대화 내용이 요약을 위해 {provider}로 전송됩니다.", - "compactionRouting.comboWarning": "이 설정을 사용하면 적용 대상 압축 요청마다 전체 대화 내용이 요약을 위해 콤보 {combo}로 전송됩니다. 콤보는 대상({providers})을 순서대로 시도해 먼저 응답한 하나를 사용하므로, 그중 어느 것이든 대화를 받을 수 있습니다.", + "compactionRouting.comboWarning": "이 설정을 사용하면 적용 대상 압축 요청마다 전체 대화 내용이 요약을 위해 콤보 {combo}로 전송됩니다. 콤보는 라우팅 전략에 따라 대상({providers}) 중 하나를 선택하지만, 재시도 가능한 실패가 발생하면 동일한 전체 대화 요청을 다른 대상에서 재시도할 수 있으므로, 대상 중 하나 이상이 대화를 받을 수 있습니다.", "compactionRouting.comboProvidersUnknown": "구성된 대상 프로바이더", + "compactionRouting.providerWarningScoped": "이 설정을 사용하면 원본 모델이 {sources}와 일치하는 압축 요청만 전체 대화 내용을 요약을 위해 {provider}로 전송합니다. 일치하지 않는 요청은 원래 모델을 사용합니다.", + "compactionRouting.comboWarningScoped": "이 설정을 사용하면 원본 모델이 {sources}와 일치하는 압축 요청만 전체 대화 내용을 요약을 위해 콤보 {combo}로 전송합니다. 일치하지 않는 요청은 원래 모델을 사용합니다. 콤보는 라우팅 전략에 따라 대상({providers}) 중 하나를 선택하지만, 재시도 가능한 실패가 발생하면 동일한 전체 대화 요청을 다른 대상에서 재시도할 수 있으므로, 대상 중 하나 이상이 대화를 받을 수 있습니다.", "compactionRouting.loadFailed": "압축 설정을 불러올 수 없습니다.", "compactionRouting.saved": "압축 설정을 저장했습니다.", "compactionRouting.saveFailed": "저장하지 못했습니다. 변경 사항은 유지됩니다. 다시 시도하세요.", @@ -1050,7 +1065,7 @@ export const ko: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "디코드 속도(추정)", "logs.detail.reason.ttft_missing": "이 요청은 첫 토큰까지 걸린 시간이 기록되지 않아서 측정할 디코드 구간이 없어요.", - "logs.detail.reason.decode_window_too_short": "첫 토큰 이후 구간이 1초도 안 돼서 디코드 속도를 추정하기엔 너무 짧아요.", + "logs.detail.reason.decode_window_too_short": "측정된 출력 구간이 1초도 안 돼서 디코드 속도를 추정하기엔 너무 짧아요.", "logs.detail.costTotal": "정가 환산치", "logs.detail.totalTokens": "전체 토큰", "logs.detail.matchedKey": "매칭된 가격 키", @@ -1309,9 +1324,12 @@ export const ko: Record = { "usage.col.reported": "측정됨", "usage.col.inputTokens": "입력 토큰", "usage.col.outputTokens": "출력 토큰", + "usage.col.tokPerSec": "출력 tok/s", "usage.col.cacheHits": "캐시 히트", "usage.col.cacheWrites": "캐시 쓰기", "usage.col.cacheHitRate": "히트율", + "usage.throughput.title": "엔드투엔드 출력 처리량: 출력 토큰 합계 ÷ 벽시계 시간 합계, 토큰과 시간을 모두 보고한 {samples}개 시도이 대상입니다. 디코딩 전 대기 시간을 포함하므로 안정적 디코딩 속도보다 낮으며, 토큰이나 시간을 보고하지 않은 요청은 제외됩니다.", + "usage.throughput.unmeasured": "이 행에는 출력 토큰과 시간을 모두 보고한 요청이 없어 처리량을 평균할 수 없습니다.", "usage.cacheHitRate.partial": "전체 입력 토큰 {total} 중 {measured}에 대해 계산한 평균입니다. 나머지 요청에는 캐시 세부 정보가 없습니다.", "usage.cacheHitRate.unmeasured": "이 행의 요청에는 모두 캐시 세부 정보가 없어 평균 히트율을 계산할 수 없습니다.", "usage.unavailable": "—", @@ -1527,6 +1545,16 @@ export const ko: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "기본 추론 강도", + "integrations.droidReasoning.description": "요청에 추론 강도가 지정되지 않은 경우에만 기본값을 사용합니다. Factory Droid를 비활성화하면 저장된 기본값도 제거됩니다.", + "integrations.droidReasoning.noDefault": "기본값 없음", + "integrations.droidReasoning.noEfforts": "사용 가능한 추론 강도 없음", + "integrations.droidReasoning.noModels": "내보낸 모델이 없습니다.", + "integrations.droidReasoning.modelDefault": "{model}의 기본 추론 강도", + "integrations.droidReasoning.unsupportedTitle": "저장된 기본값 중 일부가 더 이상 지원되지 않습니다. 지운 다음 변경 사항을 저장 / 검토해야 적용됩니다.", + "integrations.droidReasoning.unsupportedEffort": "지원하지 않는 강도: {effort}", + "integrations.droidReasoning.clear": "기본값 지우기", + "integrations.droidReasoning.saveReview": "변경 사항 저장 / 검토", "integrations.aside.profilesTitle": "Aside 프로필", "integrations.aside.profilesHint": "선택한 모델을 동기화할 프로필을 고르세요. Aside에서 사용 중인 프로필은 바뀌지 않습니다.", "integrations.aside.all": "모든 프로필 동기화", diff --git a/gui/src/i18n/lab-translations.ts b/gui/src/i18n/lab-translations.ts index 59d1639e80a..8b4405f74fe 100644 --- a/gui/src/i18n/lab-translations.ts +++ b/gui/src/i18n/lab-translations.ts @@ -1,6 +1,6 @@ import type { TKey } from "./en"; -export type LabLocale = "en" | "de" | "fr" | "ko" | "zh" | "zh-TW" | "ru" | "ja" | "tr" | "vi"; +export type LabLocale = "en" | "de" | "fr" | "ko" | "zh" | "zh-TW" | "ru" | "ja" | "tr" | "vi" | "pt"; export type LabCatalogKey = Exclude, `lab.production.${string}`>; export type LabSupplementKey = | "subjectKindUnknown" @@ -514,6 +514,56 @@ const vi: Record = { "lab.layer.task_effectiveness": "Hiệu quả tác vụ", }; +const pt: Record = { + "lab.title": "Laboratório de Compatibilidade", + "lab.subtitle": "Matriz de vereditos de compatibilidade somente leitura, gerada a partir das evidências da projeção do laboratório.", + "lab.loadFailed": "Não foi possível carregar os dados do laboratório de compatibilidade", + "lab.projectionUnavailable": "A projeção do laboratório não está disponível. Execute primeiro os testes de conformidade ou as sondagens ao vivo.", + "lab.projectionIncompatible": "O esquema da projeção do laboratório é incompatível. Reconstrua a projeção.", + "lab.statusTitle": "Status da projeção", + "lab.matrixTitle": "Matriz de compatibilidade", + "lab.verdictsTitle": "Registros de veredito", + "lab.filter.layer": "Camada de evidência", + "lab.filter.verdict": "Veredito", + "lab.filter.subject": "ID do sujeito", + "lab.filter.all": "Todos", + "lab.col.subject": "Sujeito", + "lab.col.layer": "Camada", + "lab.col.suite": "Suíte", + "lab.col.verdict": "Veredito", + "lab.col.asOf": "Data de referência", + "lab.col.protocol": "Conformidade de protocolo", + "lab.col.live": "Compatibilidade da rota ao vivo", + "lab.col.task": "Eficácia nas tarefas", + "lab.empty": "Ainda não há vereditos de compatibilidade na projeção.", + "lab.subjectKind": "Tipo", + "lab.observationCount": "Observações", + "lab.eventCount": "Eventos", + "lab.verdictCount": "Vereditos", + "lab.subjectCount": "Sujeitos", + "lab.builtAt": "Gerada em", + "lab.loading": "Carregando evidências de compatibilidade…", + "lab.loadMore": "Carregar mais", + "lab.detailTitle": "Detalhes do veredito", + "lab.detailClose": "Fechar", + "lab.detailSubject": "Sujeito", + "lab.detailObservations": "Observações", + "lab.detailEvents": "Eventos de evidência", + "lab.detailArtifacts": "Metadados dos artefatos", + "lab.detailLoadFailed": "Não foi possível carregar os detalhes do veredito", + "lab.refresh": "Atualizar", + "lab.verdict.UNKNOWN": "Desconhecido", + "lab.verdict.CLAIMED": "Declarado", + "lab.verdict.PROBED": "Sondado", + "lab.verdict.VERIFIED": "Verificado", + "lab.verdict.DEGRADED": "Degradado", + "lab.verdict.BLOCKED": "Bloqueado", + "lab.verdict.UNSUPPORTED": "Não suportado", + "lab.layer.protocol_conformance": "Conformidade de protocolo", + "lab.layer.live_route_compatibility": "Compatibilidade da rota ao vivo", + "lab.layer.task_effectiveness": "Eficácia nas tarefas", +}; + export const LAB_CATALOG_OVERRIDES: Record> = { en, de, @@ -525,6 +575,7 @@ export const LAB_CATALOG_OVERRIDES: Record> = { @@ -648,6 +699,18 @@ const supplements: Record> = { "community.activeRecords": "Bản ghi đang hoạt động", "community.revokedRecords": "Bản ghi đã thu hồi", }, + pt: { + subjectKindUnknown: "Desconhecido", + "artifact.present": "Presente", + "artifact.corrupt": "Corrompido", + "artifact.purged_unavailable": "Removido / indisponível", + selectVerdict: "Ver veredito de {subject}", + "community.title": "Evidências da comunidade", + "community.notLocalVerdict": "Contexto somente leitura não confiável. Não incluído neste veredito local.", + "community.bundles": "Pacotes", + "community.activeRecords": "Registros ativos", + "community.revokedRecords": "Registros revogados", + }, }; export function labSupplement( diff --git a/gui/src/i18n/log-guard-labels.ts b/gui/src/i18n/log-guard-labels.ts index 7c0d329fb9e..c32a5662efb 100644 --- a/gui/src/i18n/log-guard-labels.ts +++ b/gui/src/i18n/log-guard-labels.ts @@ -298,6 +298,33 @@ const LABELS: Record> = { "error.database_error": "Không thể cập nhật cơ sở dữ liệu nhật ký Codex.", "error.config_write_failed": "Cơ sở dữ liệu đã thay đổi nhưng OpenCodex không thể lưu cài đặt bảo vệ. Vui lòng sửa lỗi lưu trữ cấu hình, sau đó chạy Sửa chữa.", }, + pt: { + compact: 'Compactar', + compactComplete: "Compactação concluída (lógico / recuperado em disco)", + pagesUnit: "páginas", + compactPartial: "Compactação parcial (lógico / recuperado em disco)", + confirmCompact: 'Confirmar compactação', + cancel: 'Cancelar', + inspectionOnly: 'Somente inspeção', + externalSqliteHome: 'Armazenamento SQLite externo', + inspectionUnavailable: "A inspeção do log de diagnóstico não está disponível.", + metricsSkippedLarge: "Métricas de linhas ignoradas: o banco de dados excede {threshold} e a varredura travaria o proxy.", + protection: "Proteção", + compat: "Compatibilidade", + quiet: "Silencioso", + disable: "Desativar proteção", + repair: "Reparar proteção", + applying: "Aplicando proteção…", + "error.generic": "Não foi possível alterar a proteção de logs do Codex.", + "error.codex_running": "Feche o Codex antes de alterar a proteção de logs.", + "error.process_enumeration_failed": "Não foi possível verificar se o Codex está parado. A proteção não foi alterada.", + "error.busy": "O banco de dados de logs do Codex está ocupado. Feche o Codex e tente novamente.", + "error.unsupported_schema": "Este esquema de logs do Codex não é suportado para proteção.", + "error.trigger_collision": "Um nome de trigger reservado do Log Guard já está em uso. A proteção não foi alterada.", + "error.unsafe_path": "O caminho do banco de dados de logs do Codex falhou na verificação de segurança.", + "error.database_error": "Não foi possível atualizar o banco de dados de logs do Codex.", + "error.config_write_failed": "O banco de dados foi alterado, mas o OpenCodex não conseguiu salvar a configuração de proteção. Corrija o armazenamento de configuração e execute Reparar.", + }, }; export function logGuardLabel(locale: Locale, key: LogGuardLabelKey): string { diff --git a/gui/src/i18n/log-guard-operation-labels.ts b/gui/src/i18n/log-guard-operation-labels.ts index 8eded1eeddf..173fee49002 100644 --- a/gui/src/i18n/log-guard-operation-labels.ts +++ b/gui/src/i18n/log-guard-operation-labels.ts @@ -133,6 +133,18 @@ const LABELS: Record> = { "error.auto_vacuum_not_incremental": "Cơ sở dữ liệu nhật ký Codex này không được định cấu hình cho tính năng incremental vacuum, do đó không thể thu hồi dung lượng nếu không xây dựng lại hoàn toàn.", "error.integrity_check_failed": "Cơ sở dữ liệu nhật ký Codex không vượt qua được kiểm tra tính toàn vẹn. Không có dung lượng nào được thu hồi.", }, + pt: { + applying: "Aplicando alteração do Log Guard…", + "error.generic": "Não foi possível atualizar o armazenamento de logs do Codex.", + "error.codex_running": "Feche o Codex antes de alterar o armazenamento de logs do Codex.", + "error.process_enumeration_failed": "Não foi possível verificar se o Codex está parado. A operação do Log Guard não foi iniciada.", + "error.busy": "O banco de dados de logs do Codex está ocupado. Feche o Codex e tente novamente.", + "error.unsupported_schema": "Este esquema de logs do Codex não é suportado para esta operação.", + "error.unsafe_path": "O caminho do banco de dados de logs do Codex falhou na verificação de segurança.", + "error.database_error": "Não foi possível atualizar o banco de dados de logs do Codex.", + "error.auto_vacuum_not_incremental": "Este banco de dados de logs do Codex não está configurado para incremental vacuum; por isso, não é possível recuperar espaço sem uma reconstrução completa.", + "error.integrity_check_failed": "O banco de dados de logs do Codex falhou na verificação de integridade. Nenhum espaço foi recuperado.", + }, }; export function logGuardOperationLabel(locale: Locale, key: LogGuardOperationLabelKey): string { diff --git a/gui/src/i18n/log-guard-state-labels.ts b/gui/src/i18n/log-guard-state-labels.ts index 32296ee0eb5..71c789cbb1c 100644 --- a/gui/src/i18n/log-guard-state-labels.ts +++ b/gui/src/i18n/log-guard-state-labels.ts @@ -61,6 +61,11 @@ const LABELS: Record = { protection: { off: "Tắt", active: "Đang hoạt động", drifted: "Cần sửa chữa", unsupported: "Không được hỗ trợ", unknown: "Không xác định" }, mode: { off: "Tắt", compat: "Tương thích", quiet: "Yên tĩnh", collision: "Không xác định" }, }, + pt: { + schema: { compatible: "Compatível", missing: "Banco de dados não encontrado", unreadable: "Banco de dados indisponível", unsupported: "Não suportado" }, + protection: { off: "Desativada", active: "Ativa", drifted: "Requer reparo", unsupported: "Não suportada", unknown: "Desconhecida" }, + mode: { off: "Desativado", compat: "Compatibilidade", quiet: "Silencioso", collision: "Desconhecido" }, + }, }; export function logGuardSchemaStateLabel(locale: Locale, state: LogGuardSchemaState): string { diff --git a/gui/src/i18n/native-main-copy.ts b/gui/src/i18n/native-main-copy.ts index 71091e37468..09add82754b 100644 --- a/gui/src/i18n/native-main-copy.ts +++ b/gui/src/i18n/native-main-copy.ts @@ -3,7 +3,7 @@ import { NATIVE_MAIN_TRANSLATIONS, type NativeMainTKey } from "./native-main-tra export type NativeMainTFn = (key: NativeMainTKey, vars?: Record) => string; -/** Closed, nine-locale namespace like the adjacent Log Guard label modules. +/** Closed locale namespace like the adjacent Log Guard label modules. * Keep these keys out of the global catalog contract used by existing screens. */ export function nativeMainTranslator(locale: Locale): NativeMainTFn { diff --git a/gui/src/i18n/native-main-translations.ts b/gui/src/i18n/native-main-translations.ts index a1f53768ae2..59ae5f2d294 100644 --- a/gui/src/i18n/native-main-translations.ts +++ b/gui/src/i18n/native-main-translations.ts @@ -382,4 +382,42 @@ export const NATIVE_MAIN_TRANSLATIONS = { "nativeMain.confirm": "Xác nhận thay đổi đăng nhập gốc", "nativeMain.unavailable": "Không thể thay đổi hồ sơ. Hãy kiểm tra mã chẩn đoán trước khi tiếp tục." }, -} satisfies Record<"en" | "de" | "fr" | "ko" | "zh" | "zh-TW" | "ru" | "ja" | "tr" | "vi", Record>; + "pt": { + "nativeMain.title": "Login principal do Codex nativo", + "nativeMain.manage": "Gerenciar login principal", + "nativeMain.close": "Fechar o gerenciador de login principal", + "nativeMain.separate": "Estes controles alteram o login do Codex nativo, não a conta selecionada para a próxima requisição do Pool.", + "nativeMain.home": "CODEX_HOME efetivo", + "nativeMain.current": "Perfil ativo registrado", + "nativeMain.unregistered": "Não registrado", + "nativeMain.physicalHint": "Este diretório home pertence ao servidor do OpenCodex, que pode ser outro computador. O backend verifica o login físico antes de qualquer troca.", + "nativeMain.busyOther": "Outra operação de login nativo está em andamento. Conclua ou cancele-a primeiro.", + "nativeMain.working": "Processando…", + "nativeMain.changed": "O diretório home, o perfil ativo ou o estado de recuperação mudou. Revise o estado atualizado e confirme novamente.", + "nativeMain.error": "Não foi possível confirmar a operação. Leia o estado atualizado antes de tentar de novo; uma resposta perdida não prova que houve reversão. Use o comando de diagnóstico no servidor abaixo.", + "nativeMain.doctorHint": "Execute {cmd} no servidor para ver os detalhes.", + "nativeMain.saved": "Login atual salvo como perfil. O login físico não foi alterado.", + "nativeMain.restart": "Troca de login concluída. Reinicie o Codex nativo usando o CODEX_HOME exibido antes de continuar.", + "nativeMain.done": "Operação concluída. O servidor não solicitou a reinicialização do Codex nativo.", + "nativeMain.refreshFailed": "Não foi possível atualizar todas as informações da conta. Uma alteração já concluída não foi desfeita. Atualize antes de outra operação.", + "nativeMain.refresh": "Atualizar status", + "nativeMain.unsupported": "Este armazenamento de credenciais não é suportado. O gerenciamento de perfis nativos exige o armazenamento de credenciais em arquivo.", + "nativeMain.recoveryHint": "Uma transação interrompida precisa de recuperação. Recuperar conclui a reconciliação; restaurar reverte a transação pendente. Nenhuma das ações é executada até você confirmar.", + "nativeMain.recover": "Recuperar alteração interrompida", + "nativeMain.rollback": "Restaurar transação pendente", + "nativeMain.empty": "Nenhum perfil nativo registrado ainda. Salve primeiro o login atual.", + "nativeMain.active": "Ativo", + "nativeMain.switchTo": "Trocar o login nativo para {label}", + "nativeMain.switch": "Trocar", + "nativeMain.restore": "Voltar ao perfil exibido anteriormente: {label}", + "nativeMain.restoreHint": "Este atalho seleciona o perfil exibido antes da sua última troca, não um histórico de transações verificado pelo servidor. Ele é mantido apenas nesta página.", + "nativeMain.label": "Rótulo do perfil", + "nativeMain.save": "Salvar atual como perfil", + "nativeMain.saveHint": "Para um login ativo já registrado, salvar atualiza o rótulo. Não adiciona um novo login.", + "nativeMain.confirmHint": "Isto altera apenas o login nativo no diretório home abaixo. Feche primeiro o Codex nativo naquele computador. Este painel não edita estratégia do Pool, membros, chaves de provedor nem dados de tarefas/histórico.", + "nativeMain.stopped": "Parei o Codex nativo que usa este CODEX_HOME e entendo que pode ser necessário reiniciá-lo.", + "nativeMain.cancel": "Cancelar", + "nativeMain.confirm": "Confirmar alteração do login nativo", + "nativeMain.unavailable": "As alterações de perfil estão indisponíveis. Verifique o código de diagnóstico antes de continuar." +}, +} satisfies Record<"en" | "de" | "fr" | "ko" | "zh" | "zh-TW" | "ru" | "ja" | "tr" | "vi" | "pt", Record>; diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts new file mode 100644 index 00000000000..abf2c3af17c --- /dev/null +++ b/gui/src/i18n/pt.ts @@ -0,0 +1,3779 @@ +// Brazilian Portuguese — generated from en.ts. Must match TKey set (compile-checked). +import type { TKey } from "./en"; + +/** + * Brazilian Portuguese (pt-BR) i18n catalog. Must match the `TKey` set (compile-checked). + * Technical terms and model identifiers intentionally remain English. + */ +export const pt: Record = { + "compactionRouting.sources": "Origens", + "compactionRouting.sourcesAll": "Todos os modelos de conversa", + "compactionRouting.sourcesSelected": "Somente origens selecionadas", + "compactionRouting.sourcesProviders": "Provedores inteiros", + "compactionRouting.sourcesModels": "Modelos individuais", + "compactionRouting.sourcesSaved": "Seletores salvos ausentes no catálogo atual", + "compactionRouting.sourcesNone": "Selecione pelo menos uma origem ou volte para todos os modelos de conversa.", + "compactionRouting.sourcesHint": "Aplique a substituição somente quando o modelo de origem da requisição de compactação corresponder a um modelo selecionado ou a provedor/*. As demais requisições continuam usando seu próprio modelo.", + "compactionRouting.sourcesGridTitle": "Redirecionar requisições de compactação cujo modelo de origem corresponda a:", + "compactionRouting.providerWarningScoped": "Com esta configuração, somente requisições de compactação cujo modelo de origem corresponda a {sources} enviam todo o conteúdo da conversa para {provider} resumir; as demais continuam usando seu próprio modelo.", + "compactionRouting.comboWarningScoped": "Com esta configuração, somente requisições de compactação cujo modelo de origem corresponda a {sources} enviam todo o conteúdo da conversa ao combo {combo} para resumo; as demais continuam usando seu próprio modelo. O combo escolhe um de seus destinos ({providers}) pela estratégia de roteamento, mas após uma falha recuperável pode repetir a mesma requisição com a conversa completa em outro destino. Assim, um ou mais provedores de destino podem receber a conversa.", + "usage.col.tokPerSec": "Saída tok/s", + "usage.throughput.title": "Taxa de saída de ponta a ponta: soma dos tokens de saída dividida pela soma dos tempos totais de {samples} tentativas medidas. Inclui a espera antes da decodificação, não apenas sua velocidade estável; requisições sem tokens medidos ou sem duração medida são excluídas.", + "usage.throughput.unmeasured": "Nenhuma requisição desta linha informou tokens de saída e duração válidos; não há taxa para calcular.", + "integrations.droidReasoning.title": "Esforço de raciocínio padrão", + "integrations.droidReasoning.description": "O padrão só é usado quando a requisição omite o esforço de raciocínio. Desative o Factory Droid para remover estes padrões salvos.", + "integrations.droidReasoning.noDefault": "Sem padrão", + "integrations.droidReasoning.noEfforts": "Nenhuma opção de esforço de raciocínio disponível", + "integrations.droidReasoning.noModels": "Nenhum modelo exportado está disponível.", + "integrations.droidReasoning.modelDefault": "Esforço de raciocínio padrão para {model}", + "integrations.droidReasoning.unsupportedTitle": "Alguns padrões salvos não são mais compatíveis. Limpe-os e salve / revise as alterações para aplicar.", + "integrations.droidReasoning.unsupportedEffort": "Esforço não compatível: {effort}", + "integrations.droidReasoning.clear": "Limpar padrão", + "integrations.droidReasoning.saveReview": "Salvar / revisar alterações", + + "nav.claude": "Claude", + "claude.tabAccount": "Conta", + "claude.tabSettings": "Configurações", + "claude.pageSub": "Contas e roteamento do OpenCodex para o Claude Code e o Claude Desktop.", + "claude.addAnthropic": "Adicionar Anthropic", + "claude.changeOnCode": "Altere na aba {tab}", + "claude.interceptRunning": "Em execução", + "claude.interceptStopped": "Parada", + "claude.routingLoadFail": "Não foi possível carregar o status do roteamento do Claude.", + "claude.interceptLoadFail": "Não foi possível carregar o status da interceptação.", + "claude.accountEmpty": "Adicione o provedor Anthropic e entre com sua conta Claude. Suas contas, cota e configurações aparecerão aqui.", + "claude.accountEmptyTitle": "A Anthropic ainda não foi configurada", + "claude.interceptStatus": "Interceptação", + "claude.interceptDesc": "Encaminha pelo proxy local o tráfego do Claude Code/Desktop destinado aos serviços da Anthropic.", + "claude.interceptPort": "Porta de interceptação", + "claude.interceptPortDesc": "Porta local do proxy de interceptação, separada da porta do proxy do OpenCodex.", + "claude.stateOn": "Ativada", + "claude.stateOff": "Desativada", + "claude.settingsHint": "As configurações de compatibilidade, instruções do agente e contexto podem ser alteradas na aba Code.", + "pws.anthropicAccountThresholdHint": "Substitui o padrão do pool do Claude. 0 desativa a troca por uso apenas para esta conta; a pausa e a recuperação de limite de taxa continuam valendo.", + "kiroLogin.title": "Entrar no Kiro", + "kiroLogin.chooseMethod": "Escolha um método de login", + "kiroLogin.cli": "Kiro CLI", + "kiroLogin.builderId": "Builder ID", + "kiroLogin.google": "Google", + "kiroLogin.github": "GitHub", + "kiroLogin.addDescription": "Adicione uma conta Kiro com login por dispositivo ou importe uma sessão do Kiro CLI.", + "kiroLogin.loginDescription": "Entre com um dispositivo ou importe sua sessão do Kiro CLI.", + "kiroLogin.starting": "Iniciando login por dispositivo…", + "kiroLogin.pending": "Digite este código na página de verificação e aguarde aqui.", + "kiroLogin.done": "Conta Kiro adicionada.", + "kiroLogin.expired": "O login por dispositivo expirou. Tente novamente.", + "kiroLogin.ended": "Login encerrado. Verifique a lista de contas.", + "kiroLogin.failed": "Falha no login do Kiro.", + "kiroLogin.startFailed": "Não foi possível iniciar o login do Kiro. Tente novamente.", + "kiroLogin.tryAgain": "Tentar novamente", + "kiroLogin.copyCode": "Copiar código", + "kiroLogin.copied": "Código copiado", + "kiroLogin.copyUnavailable": "Cópia indisponível", + "kiroLogin.openVerification": "Abrir página de verificação", + "kiroLogin.unexpectedHost": "Host de verificação inesperado. Confira o endereço antes de copiá-lo.", + "kiroLogin.duplicate": "Este perfil do Kiro já está na lista de contas.", + "kiroLogin.manualReview": "Esta conta pode precisar de revisão manual.", + "kiroSelection.suspended": "Não selecionada automaticamente: suspensa", + "kiroSelection.quotaExhausted": "Não selecionada automaticamente: cota esgotada", + "kiroSelection.cooldown": "Não selecionada automaticamente: em espera", + "kiroSelection.generic": "Não selecionada automaticamente", + "tray.updated": "Atualizado {time} · a cada 60 s", + "tray.today": "Hoje", + "tray.input": "Entrada", + "tray.output": "Saída", + "tray.cost": "Custo · est.", + "tray.cached": "{percent} em cache", + "usage.incomplete": "Alguns registros de uso não puderam ser incluídos. Contagens, datas e rankings refletem apenas os registros legíveis.", + "models.pickerOrder.usageIncomplete": "Não é possível salvar a ordem por mais usados porque o histórico de uso está incompleto. Escolha outra ordem ou repare o histórico primeiro.", + "api.attribution.noRecordedUse": "Sem uso nos registros legíveis", + "audio.dictation": "Ditado", + "audio.liveVoice": "Voz ao vivo", + "audio.configured": "Configurado, não verificado", + "audio.notConfigured": "Não configurado", + "audio.unknown": "Metadados de áudio indisponíveis", + "audio.key": "Chave de dados do OpenCodex", + "audio.file": "Arquivo de áudio (máx. 25 MB)", + "audio.transcribe": "Transcrever", + "audio.transcribing": "Transcrevendo...", + "audio.transcript": "Transcrição", + "audio.emptyTranscript": "Nenhuma fala detectada", + "audio.examples": "Exemplos de API", + "audio.streaming": "Ditado em streaming", + "audio.connect": "Verificar conexão", + "audio.disconnect": "Desconectar", + "audio.events": "Eventos da sessão", + "audio.state.idle": "Não verificado", + "audio.state.connecting": "Conectando...", + "audio.state.connected": "Sessão pronta", + "audio.state.disconnected": "Desconectado", + "audio.state.failed": "Falha na conexão", + "audio.error.auth": "Chave rejeitada. Verifique sua chave de dados do OpenCodex.", + "audio.error.unavailable": "Upstream indisponível. Verifique a conta do provedor.", + "audio.error.rateLimit": "Limite de taxa atingido. Tente novamente mais tarde.", + "audio.error.invalid": "Requisição rejeitada. Verifique o arquivo e o provedor.", + "audio.error.size": "Escolha um arquivo de áudio não vazio de até 25 MB.", + "audio.error.network": "Falha na conexão. Verifique o endereço do proxy.", + "audio.error.timeout": "A requisição expirou. Tente novamente.", + "audio.error.protocol": "Resposta de áudio inesperada. Verifique a compatibilidade do provedor.", + "models.pickerOrder.label": "Ordem do seletor", + "models.pickerOrder.default": "Padrão", + "models.pickerOrder.alphabetical": "A–Z por modelo", + "models.pickerOrder.provider": "Agrupar por provedor", + "models.pickerOrder.mostUsed": "Instantâneo dos mais usados", + "models.pickerOrder.custom": "Ordem personalizada", + "models.pickerOrder.apply": "Aplicar ordem", + "models.pickerOrder.applying": "Aplicando…", + "models.pickerOrder.saved": "Ordem do seletor salva. Reabra os clientes que ainda mostram o catálogo antigo.", + "models.pickerOrder.pending": "Ordem salva; a atualização do catálogo está pendente.", + "models.pickerOrder.usageFailed": "Não foi possível carregar o uso dos modelos.", + "models.pickerOrder.loadFailed": "Não foi possível carregar as configurações do seletor.", + "models.pickerOrder.retry": "Tentar novamente", + "models.pickerOrder.hint": "Salva a ordem roteada para a descoberta do Codex e do Claude. As faixas em destaque/nativas permanecem no lugar; Mais usados é um instantâneo. As opções nativas anunciadas podem mudar.", + "models.fastRows.title": "Mostrar linhas de modelos Fast", + "models.fastRows.desc": "Adiciona seletores “Model Fast” elegíveis aos seletores de modelos de clientes externos. Isso não altera as opções de esforço de raciocínio do modelo base. Ao salvar, as integrações conectadas são atualizadas quando possível; caso contrário, atualize a integração ou o catálogo do cliente.", + "models.fastRows.refreshHint": "Salvo. A atualização do catálogo está pendente.", + "models.fastRows.enabled": "Linhas de modelos Fast ativadas.", + "models.fastRows.disabled": "Linhas de modelos Fast desativadas.", + "models.fastRows.loadFailed": "Não foi possível carregar a configuração das linhas Fast.", + "models.fastRows.updateFailed": "Falha ao atualizar a configuração das linhas Fast.", + "codexAuth.quotaAutoRefreshAllHint": "Controla juntas as janelas compatíveis de 5 horas e semanal de todas as contas atuais. No modo Pool, uma pequena requisição é enviada após cada redefinição e consome cota.", + "codexAuth.quotaAutoRefreshMixed": "Algumas janelas estão ativadas.", + "codexAuth.quotaAutoRefreshEmpty": "Nenhuma janela de cota compatível. Atualize as cotas das contas para verificar novamente.", + "codexAuth.quotaAutoRefreshLoadFailed": "Não foi possível carregar as configurações de ativação. Tente novamente para verificar o estado delas.", + "codexAuth.quotaAutoRefreshPartial": "Algumas configurações não puderam ser salvas. Tente novamente para concluir a mesma alteração.", + "nav.dashboard": "Painel", + "uptime.day": "d", + "uptime.hour": "h", + "uptime.minute": "m", + "uptime.second": "s", + "nav.startup": "Inicialização", + "nav.providers": "Provedores", + "nav.models": "Modelos", + "nav.combos": "Combos", + "nav.subagents": "Subagentes", + "nav.logs": "Logs e depuração", + "nav.usage": "Uso", + "common.github": "GitHub", + "sidebar.star": "Dar estrela no GitHub", + "sidebar.starred": "Estrela dada no GitHub", + "sidebar.starUnauthenticated": "Abra o GitHub para dar a estrela (a CLI gh não está autenticada)", + "sidebar.starFailed": "Não foi possível dar a estrela pelo gh. Abrindo o GitHub.", + "onboarding.star.title": "Gostando do OpenCodex?", + "onboarding.star.body": "O OpenCodex é gratuito e de código aberto. Uma estrela no GitHub ajuda outros desenvolvedores a encontrá-lo e mantém o projeto vivo.", + "onboarding.star.cta": "Dar estrela no GitHub", + "onboarding.star.ghHint": "Dá a estrela no repositório com a CLI do GitHub (gh) autenticada. Nada é marcado até você clicar.", + "onboarding.star.browserHint": "Abre o repositório em github.com no navegador.", + "onboarding.star.openPage": "Abrir a página do GitHub", + "onboarding.star.later": "Talvez depois", + "onboarding.star.failed": "Não foi possível dar a estrela pela CLI do GitHub (gh). Você pode dá-la na página do GitHub.", + "onboarding.star.starring": "Dando estrela…", + "onboarding.star.thanks": "Obrigado pela estrela!", + "onboarding.star.thanksBody": "Ajuda muito. O link do GitHub na barra lateral leva você de volta ao repositório a qualquer momento.", + "onboarding.star.done": "Concluído", + "sidebar.updateAvailable": "Atualização disponível: {version}", + "sidebar.checkUpdate": "Verificar atualizações", + "sidebar.desktopUpdate": "Abrir atualizações do desktop", + "common.save": "Salvar", + "common.saving": "Salvando…", + "common.cancel": "Cancelar", + "common.discard": "Descartar", + "common.delete": "Excluir", + "common.close": "Fechar", + "common.ok": "OK", + "common.remove": "Remover", + "common.loading": "Carregando…", + "common.retry": "Tentar novamente", + "auth.adminTokenTitle": "Token de administrador do OpenCodex (OPENCODEX_ADMIN_AUTH_TOKEN)", + "auth.adminAccountLabel": "Conta", + "auth.adminTokenFieldLabel": "Token de administrador", + "auth.adminTokenRejected": "Esse token de administrador foi rejeitado. Confira-o e tente novamente.", + "auth.adminTokenUnavailable": "Não foi possível verificar o token de administrador. Tente novamente.", + "auth.adminTokenHelp": "Este é o token de administrador de gerenciamento do OpenCodex, não uma chave de API de provedor. O proxy o grava em ~/.opencodex/admin-api-token (ou $OPENCODEX_HOME/admin-api-token) na primeira inicialização, e OPENCODEX_ADMIN_AUTH_TOKEN o substitui.", + "auth.adminTokenDocsLink": "Como encontrá-lo", + "app.logoAria": "logotipo do opencodex", + "app.claudeOn": "Claude ON", + "app.claudeOff": "Claude OFF", + "theme.label": "Tema", + "theme.light": "Claro", + "theme.dark": "Escuro", + "theme.system": "Sistema", + "zoom.label": "Zoom", + "zoom.out": "Diminuir zoom", + "zoom.in": "Aumentar zoom", + "zoom.reset": "Redefinir zoom para 100%", + "lang.label": "Idioma", + "lang.nativeName": "Português", + "provider.name.commandCodeAuth": "Command Code - Auth", + "provider.name.commandCodeApi": "Command Code - API", + "provider.name.orcaRouterApi": "OrcaRouter - API", + "provider.name.orcaRouterAuth": "OrcaRouter - Auth", + "provider.name.volcengine": "Volcengine Ark", + "provider.name.volcengineCodingPlan": "Volcengine Ark Coding Plan", + "provider.name.volcengineAgentPlan": "Volcengine Ark Agent Plan", + "errorBoundary.title": "Falha ao carregar a página", + "errorBoundary.message": "Esta seção encontrou um erro de renderização. Recarregue-a para tentar novamente.", + "errorBoundary.details": "Erro", + "errorBoundary.reload": "Recarregar", + "routing.title": "Inteligência de roteamento (beta)", + "routing.subtitle": "Perfis de política, avaliação em dry-run e análises de roteamento com base em fontes.", + "routing.loadFailed": "Não foi possível carregar os dados de roteamento", + "routing.empty": "Nenhum perfil de roteamento configurado. Adicione `routingProfiles` ao config.json.", + "routing.revision": "rev", + "routing.detail": "Perfil", + "routing.createProfile": "Criar perfil", + "routing.dryRunError": "Falha no dry-run (HTTP {status})", + "routing.removeConfirm": "Remover o perfil {id}?", + "routing.unknownEvidence.allow": "permitir", + "routing.unknownEvidence.penalize": "penalizar", + "routing.unknownEvidence.exclude": "excluir", + "routing.removeCandidate": "Remover candidato {provider}/{model}", + "routing.candidates": "Candidatos", + "routing.require": "Requisitos obrigatórios", + "routing.optimize": "Pesos de otimização", + "routing.limits": "Limites", + "routing.unknownEvidence": "Política para evidência desconhecida", + "routing.compatibility.title": "Política de compatibilidade", + "routing.compatibility.enabled": "Exigir evidência do Compatibility Lab", + "routing.compatibility.requiredSuites": "Suítes obrigatórias", + "routing.compatibility.loadingCatalog": "Carregando catálogo do lab…", + "routing.compatibility.catalogUnavailable": "Catálogo do lab indisponível — informe os ids das suítes manualmente no config.json.", + "routing.compatibility.layer.protocol_conformance": "Conformidade de protocolo", + "routing.compatibility.layer.live_route_compatibility": "Compatibilidade de rota ao vivo", + "routing.compatibility.minStatus": "Status mínimo de compatibilidade", + "routing.none": "nenhum", + "routing.unavailable": "–", + "routing.dryRun": "Avaliação em dry-run", + "routing.dryRunContext": "Janela de contexto da requisição (tokens)", + "routing.dryRunTools": "A requisição exige ferramentas", + "routing.dryRunImage": "A requisição exige entrada de imagem", + "routing.dryRunStructured": "A requisição exige saída estruturada", + "routing.dryRunRun": "Avaliar candidatos", + "routing.candidate": "Candidato", + "routing.eligible": "Elegível", + "routing.exclusions": "Exclusões", + "routing.costCap": "Teto de custo", + "routing.capOutcome.satisfied": "dentro do limite", + "routing.capOutcome.exceeded": "acima do limite", + "routing.capOutcome.unknown-allowed": "desconhecido (permitido)", + "routing.capOutcome.unknown-excluded": "desconhecido (excluído)", + "routing.exclusion.capability-unsatisfied": "capacidade não atendida", + "routing.exclusion.unknown-capability": "capacidade desconhecida", + "routing.exclusion.cost-limit": "acima do teto de custo", + "routing.exclusion.cost-limit-unknown": "custo desconhecido sob o teto", + "routing.exclusion.cooldown": "em espera", + "routing.exclusion.unknown-health": "saúde desconhecida", + "routing.exclusion.unknown-quota": "cota desconhecida", + "routing.exclusion.unknown-price": "preço desconhecido", + "routing.exclusion.other": "exclusão: {code}", + "routing.score": "Pontuação", + "routing.selected": "selecionado", + "routing.yes": "sim", + "routing.no": "não", + "routing.analytics": "Análises de roteamento", + "routing.analyticsTotal": "Requisições", + "routing.analyticsSuccessRate": "Sucesso", + "routing.analyticsFallbackRate": "Fallback", + "routing.analyticsP50": "p50", + "routing.analyticsP95": "p95", + "routing.analyticsP99": "p99", + "routing.analyticsCooldown": "Falhas de espera", + "routing.analyticsConfidence": "Confiança", + "routing.analyticsTruncated": "histórico truncado", + "routing.analyticsRequests": "Requisições", + "routing.analyticsEmpty": "Ainda não há análises — envie algumas requisições primeiro.", + "startup.title": "Segurança na inicialização", + "startup.subtitle": "Verifique se o Codex consegue alcançar o opencodex após uma reinicialização, antes que o roteamento pelo proxy local vire um loop de reconexão.", + "startup.refresh": "Atualizar", + "startup.backToDashboard": "Voltar ao painel", + "startup.loading": "Verificando a proteção de inicialização…", + "startup.error": "Não foi possível ler a proteção de inicialização.", + "startup.staleData": "A última verificação de inicialização falhou. Os valores abaixo estão desatualizados e não devem ser tratados como prova de proteção.", + "startup.status.native": "Roteamento nativo", + "startup.status.protected": "Protegido contra reinicialização", + "startup.status.atRisk": "Ação necessária", + "startup.summary.native": "O Codex não depende do proxy local", + "startup.summary.protected": "O opencodex estará disponível após a reinicialização", + "startup.summary.atRisk": "O Codex pode perder o acesso aos modelos após a reinicialização", + "startup.riskDetail": "O Codex está fixado no proxy local, mas nenhum serviço persistente ou shim de inicializador íntegro o iniciará novamente.", + "startup.riskDetailCustomLocal": "O Codex aponta para um gateway local personalizado. O opencodex não consegue gerenciar nem verificar o ciclo de reinicialização desse gateway.", + "startup.riskDetailWindowsShim": "O shim do inicializador protege os scripts de CLI compatíveis, mas o Codex Desktop e as execuções diretas de codex.exe podem contorná-lo no Windows.", + "startup.safeDetail": "O roteamento atual e o mecanismo de inicialização são consistentes. Nenhum ocx start manual deve ser necessário após a reinicialização.", + "startup.routing": "Roteamento do Codex", + "startup.routing.proxy": "Proxy local", + "startup.routing.native": "OpenAI nativo", + "startup.routing.customLocal": "Gateway local personalizado", + "startup.routing.customRemote": "Gateway remoto personalizado", + "startup.routing.unknown": "Roteamento desconhecido ou inválido", + "startup.restartProtection": "Proteção contra reinicialização", + "startup.preference": "Inicialização sob demanda", + "startup.enabled": "Ativada", + "startup.disabled": "Desativada", + "startup.protection.desktop": "App desktop", + "startup.desktopHint": "Inicia no login; o app desktop supervisiona o proxy embutido. A propriedade e os processos ativos são verificados.", + "startup.desktopRecovery": "O OpenCodex é dono deste proxy. Reabra o app desktop e marque Iniciar no login. As alterações de serviço e de inicializador ficam desativadas enquanto o app desktop for o dono.", + "startup.protection.service": "Serviço em segundo plano", + "startup.protection.shim": "Shim do inicializador", + "startup.protection.none": "Não instalado", + "startup.details": "Detalhes da proteção", + "startup.service": "Serviço em segundo plano", + "startup.serviceHint": "Inicia no login e reinicia o proxy após uma falha.", + "startup.installed": "Instalado", + "startup.notInstalled": "Não instalado", + "startup.unsupported": "Sem suporte", + "startup.shim": "Shim do inicializador do Codex", + "startup.shimHint": "Executa ocx ensure quando um inicializador de script do Codex compatível é iniciado.", + "startup.healthy": "Íntegro", + "startup.cliOnly": "Somente CLI", + "startup.stale": "Desatualizado", + "startup.viable": "Pronto", + "startup.unhealthy": "Instalado, mas com problemas", + "startup.conflict": "Conflito de serviço", + "startup.installedDisabled": "Instalado, mas desativado", + "startup.install": "Instalar", + "startup.installing": "Instalando…", + "startup.repair": "Reparar", + "startup.repairing": "Reparando…", + "startup.serviceInstalled": "Serviço em segundo plano instalado com sucesso.", + "startup.serviceRepaired": "Serviço em segundo plano reparado com sucesso.", + "startup.shimInstalled": "Shim do inicializador do Codex instalado com sucesso.", + "startup.shimRepaired": "Shim do inicializador do Codex reparado com sucesso.", + "startup.installFailed": "Falha na instalação:", + "startup.tray.title": "Bandeja do sistema do Windows", + "startup.tray.hint": "Instale um ícone de bandeja no login para iniciar, parar e reiniciar o proxy, abrir o painel e ver o status com um clique.", + "startup.tray.login": "Iniciar a bandeja no login do Windows", + "startup.tray.notProtection": "A bandeja é um controlador, não uma proteção contra reinicialização. Ainda é necessário um serviço em segundo plano viável para a recuperação autônoma do proxy.", + "startup.tray.running": "Em execução", + "startup.tray.stopped": "Instalada, oculta", + "startup.tray.stale": "Reparo necessário", + "startup.tray.notInstalled": "Não instalada", + "startup.tray.loading": "Verificando…", + "startup.tray.unavailable": "Status indisponível", + "startup.tray.install": "Instalar e mostrar a bandeja", + "startup.tray.start": "Mostrar ícone da bandeja", + "startup.tray.stop": "Sair do ícone da bandeja", + "startup.tray.uninstall": "Remover bandeja do login", + "startup.tray.error": "A ação da bandeja do Windows falhou. Consulte ocx tray status para mais detalhes.", + "startup.recovery": "Opções de reparo", + "startup.recoveryHint": "Use os instaladores de um clique acima ou copie um comando para reparo manual. O serviço em segundo plano é recomendado para o Codex Desktop e para executáveis do Windows.", + "startup.command.service": "Recomendado: serviço persistente em segundo plano", + "startup.command.shim": "Alternativa: shim do inicializador da CLI", + "startup.command.native": "À prova de falhas: restaurar o roteamento nativo do Codex", + "startup.copy": "Copiar", + "startup.copied": "Copiado", + "startup.recommended": "Reparo recomendado: {cmd}", + "startup.navRisk": "A proteção de inicialização requer atenção", + "startup.codexRuntime.clampHidden": "Algumas opções de esforço de raciocínio foram ocultadas porque o OpenCodex usou o Codex {version}.", + "startup.codexRuntime.clampHiddenWithEfforts": "Algumas opções de esforço de raciocínio foram ocultadas porque o OpenCodex usou o Codex {version} (removidas: {efforts}).", + "startup.codexRuntime.olderBinary": "O OpenCodex está usando um binário do Codex mais antigo ({version}). Há uma instalação mais recente disponível.", + "dash.subtitle": "Status ao vivo do proxy local do opencodex, de seus provedores e dos modelos roteados para o Codex.", + "dash.workspace.overview": "Visão geral", + "dash.workspace.sections": "Seções", + "dash.status": "Status", + "dash.online": "Online", + "dash.offline": "Offline", + "dash.version": "Versão", + "dash.uptime": "Tempo ativo", + "dash.providers": "Provedores", + "dash.tokens30d": "Tokens (30d)", + "dash.coverage": "{pct} de cobertura", + "dash.mem.title": "Observabilidade de memória", + "dash.mem.hint": "Diagnóstico de runtime somente leitura. A memória observada é max(RSS, external, ArrayBuffers), para que o corte do working set no Windows não esconda retenção já alocada.", + "dash.mem.rss": "Conjunto residente (RSS)", + "dash.mem.jsHeap": "Heap JS em uso", + "dash.mem.jsHeapArena": "arena {total}", + "dash.mem.pressure": "Em relação ao limite de alerta", + "dash.mem.pressureOf": "{pct}% do limite", + "dash.mem.pressureUnknown": "Nenhum limite informado", + "dash.mem.jscHeap": "Heap JSC", + "dash.mem.external": "Externa", + "dash.mem.arrayBuffers": "ArrayBuffers", + "dash.mem.observed": "Observada", + "dash.mem.runtime": "Contadores de runtime", + "dash.mem.growth": "Deriva observada / hora", + "dash.mem.perHour": "/h", + "dash.mem.store": "Armazenamento de continuação", + "dash.mem.storeHint": "Cache de previous_response_id do proxy. Um total de bytes crescente com um heap crescente aponta para retenção de conversas, e não para o alocador do runtime.", + "dash.mem.storeEntries": "Entradas", + "dash.mem.storeTotal": "Total", + "dash.mem.storeLargest": "Maior", + "dash.mem.storeOldest": "Mais antiga", + "dash.mem.threshold": "Limite de alerta", + "dash.mem.lastWarn": "Último alerta", + "dash.mem.never": "Nunca", + "dash.mem.details": "Detalhes", + "dash.mem.unavailable": "Diagnóstico de memória indisponível (proxy mais antigo).", + "dash.mem.inFlight": "Requisições em andamento", + "dash.mem.restart": "Esvaziar e reiniciar", + "dash.mem.restartConfirm": "Aguarde {count} requisição(ões) em andamento e reinicie (até {seconds}s; as requisições restantes são cortadas ao esgotar o tempo).", + "dash.mem.draining": "Esvaziando {count} requisição(ões)… reiniciando ao concluir", + "dash.mem.reconnecting": "Proxy reiniciando… aguardando reconexão", + "dash.mem.restartFailed": "Falha ao esvaziar e reiniciar. Verifique se o proxy está em execução.", + "dash.mem.restartNoSupervisor": "Nenhuma proteção contra reinicialização detectada. O proxy pode permanecer parado após a reinicialização, a menos que você o inicie novamente.", + "dash.activeProviders": "Provedores ativos", + "dash.noProviders": "Nenhum provedor configurado. Execute {cmd}.", + "dash.col.name": "Nome", + "dash.col.adapter": "Adaptador", + "dash.col.baseUrl": "URL base", + "dash.col.model": "Modelo", + "dash.modelsNoResults": "Nenhum modelo corresponde à sua busca.", + "dash.availableModels": "Modelos disponíveis", + "dash.noModels": "Nenhum modelo encontrado. Verifique as chaves de API dos provedores.", + "dash.cannotConnect": "Não foi possível conectar ao proxy. Ele está em execução?", + "dash.runStart": "Execute {cmd} para iniciar o proxy.", + "dash.stop": "Parar proxy", + "dash.stopConfirm": "Parar o proxy e restaurar o Codex nativo?", + "dash.stopFailed": "Falha ao parar o proxy (HTTP {status}).", + "dash.stopStillRunning": "O proxy ainda está respondendo, então não foi parado.", + "dash.stopUnknown": "O proxy não confirmou a parada. Verifique se ele ainda está em execução antes de tentar novamente.", + "dash.maSwitchFailed": "Falha na troca de modo (HTTP {status}).", + "dash.maNetworkError": "Erro de rede — o proxy está em execução?", + "dash.stopping": "Parando…", + "dash.actions": "Proxy", + "dash.codexRestart": "Recarregar modelos do Codex", + "dash.codexRestarting": "Parando…", + "dash.codexRestartConfirm": "Parar os app-servers do Codex para que recarreguem a lista de modelos? Qualquer turno do Codex em andamento será interrompido, e o Codex não é reiniciado sozinho — reabra-o depois.", + "dash.codexRestartDone": "{count} app-server(s) do Codex parado(s). Reabra o Codex para carregar a lista de modelos atual.", + "dash.codexRestartNothing": "Nenhum app-server do Codex em execução. A próxima inicialização lerá a lista de modelos atual.", + "dash.codexRestartUnknown": "Não foi possível listar os processos, então nada foi parado.", + "dash.codexRestartPartial": "{count} app-server(s) não encerrou(aram). Pare-os manualmente se a lista de modelos continuar desatualizada.", + "dash.codexRestartFailed": "Falha ao recarregar os modelos do Codex (HTTP {status}).", + "dash.codexRestartUnreachable": "Não foi possível alcançar o proxy.", + "dash.codexRestartMalformed": "O proxy retornou uma resposta inesperada.", + "dash.codexRestartTimeout": "O proxy não respondeu a tempo. Ele ainda pode estar parando os app-servers.", + "models.staleBanner": "O Codex está mostrando uma lista de modelos mais antiga que este catálogo. Reinicie o Codex para recarregá-la.", + "dash.codexAutoStart": "Iniciar o opencodex com o Codex", + "dash.codexAutoStartHint": "Permite que um shim de inicializador instalado execute ocx ensure. Esta configuração não instala proteção contra reinicialização; confira Segurança na inicialização para ver o estado efetivo.", + "dash.searchModel": "Modelo do sidecar de busca", + "dash.searchModelHint": "Modelo usado para web_search em modelos roteados que não são da OpenAI. Requer login no ChatGPT.", + "dash.searchReasoning": "Esforço de raciocínio da busca", + "dash.visionModel": "Modelo do sidecar de visão", + "dash.visionModelHint": "Modelo usado para descrever imagens para modelos roteados somente de texto. Requer login no ChatGPT.", + "dash.webSearchSidecar": "Sidecar de busca na web", + "dash.webSearchSidecarHint": "Escolha o backend e o modelo usados para busca na web em modelos roteados.", + "dash.webSearchOff": "Desligado", + "dash.webSearchCodexSync": "Salvo. A configuração do Codex não foi reescrita — execute Sincronizar modelos para aplicá-la.", + "dash.webSearchStream": "Transmitir respostas ao vivo", + "dash.webSearchStreamHint": "Transmite ao vivo o texto inicial e o raciocínio do modelo até ele decidir por uma chamada de ferramenta; o restante do turno continua em buffer para a interceptação da busca. O texto escrito antes de uma busca pode se repetir parcialmente.", + "dash.visionSidecar": "Sidecar de visão", + "dash.visionSidecarHint": "Escolha o backend e o modelo usados para descrever imagens para modelos roteados somente de texto.", + "dash.visionOff": "Desligado", + "dash.visionAdvanced": "Configurações avançadas", + "dash.visionMaxDescriptions": "Máximo de descrições por turno", + "dash.visionMaxDescriptionsInvalid": "Informe um número inteiro positivo.", + "dash.visionTimeout": "Tempo limite", + "dash.visionTimeoutInvalid": "Informe um número inteiro de {min} a {max} milissegundos.", + "dash.visionAdvancedPopover": "Configurações avançadas de visão", + "compactionRouting.title": "Roteamento de compactação", + "compactionRouting.description": "Escolha um modelo para as requisições de compactação do Codex e quais gatilhos ele cobre. As mensagens seguintes mantêm as configurações da conversa.", + "compactionRouting.model": "Modelo de compactação", + "compactionRouting.effort": "Esforço de raciocínio", + "compactionRouting.triggers": "Aplica-se a", + "compactionRouting.triggersManual": "Somente /compact manual", + "compactionRouting.triggersBoth": "Manual e automática", + "compactionRouting.triggersAuto": "Somente automática", + "compactionRouting.currentModel": "Usar o modelo da conversa", + "compactionRouting.currentEffort": "Manter o esforço da requisição", + "compactionRouting.effortHint": "O raciocínio se aplica onde o endpoint de compactação der suporte. O modelo precisa aceitar a conversa completa.", + "compactionRouting.dataNotice": "Uma requisição de compactação coberta envia a conversa inteira ao provedor do modelo selecionado para resumo, mesmo quando a conversa roda em outro provedor.", + "compactionRouting.autoNotice": "A compactação automática roda por conta própria, então uma conversa longa pode ser enviada a esse provedor sem que você peça.", + "compactionRouting.providerWarning": "Com esta configuração, toda requisição de compactação coberta envia o conteúdo completo da conversa para {provider} para resumo.", + "compactionRouting.comboWarning": "Com esta configuração, toda requisição de compactação coberta envia o conteúdo completo da conversa ao combo {combo} para resumo. O combo escolhe um de seus destinos ({providers}) pela estratégia de roteamento, mas após uma falha recuperável pode repetir a mesma requisição com a conversa completa em outro destino. Assim, um ou mais provedores de destino podem receber a conversa.", + "compactionRouting.comboProvidersUnknown": "seus provedores de destino configurados", + "compactionRouting.loadFailed": "Não foi possível carregar as configurações de compactação.", + "compactionRouting.saved": "Configurações de compactação salvas.", + "compactionRouting.saveFailed": "Não foi possível salvar. Suas alterações continuam aqui; tente novamente.", + "memoryModels.title": "Roteamento de memória", + "memoryModels.description": "O Codex grava memórias após o término de uma sessão. Escolha um modelo para cada fase ou mantenha a rota existente.", + "memoryModels.infoLabel": "O que são Extração e Consolidação?", + "memoryModels.info": "O Codex transforma sessões encerradas em memória em duas etapas. A Extração lê uma sessão encerrada e anota o que aconteceu: uma nota por sessão, então faz muitas chamadas pequenas. A Consolidação pega essas notas e as grava nos arquivos de memória que o Codex lê no início das próximas sessões. Roda raramente, mas edita arquivos. Cada etapa pede seu próprio modelo, por isso aparecem separadas aqui.", + "memoryModels.extract": "Extração", + "memoryModels.extractHint": "Resume cada sessão encerrada em uma memória bruta. Roda uma vez por sessão.", + "memoryModels.consolidation": "Consolidação", + "memoryModels.consolidationHint": "Mescla as memórias brutas nos arquivos de memória que o Codex lê depois. Roda raramente e edita arquivos.", + "memoryModels.model": "Modelo", + "memoryModels.effort": "Esforço de raciocínio", + "memoryModels.off": "Desativado", + "memoryModels.defaultEffort": "Padrão do Codex", + "memoryModels.dataNotice": "O modelo escolhido recebe a entrada daquela fase: a sessão encerrada na Extração e as memórias brutas na Consolidação.", + "memoryModels.accountNotice": "Somente uma fase é roteada aqui; a outra mantém a rota existente. O Shadow Call Intercept também pode enviar as chamadas de memória dessa fase ao modelo configurado nele.", + "memoryModels.loadFailed": "Não foi possível carregar as configurações de memória.", + "memoryModels.saved": "Configurações de memória salvas.", + "memoryModels.saveFailed": "Não foi possível salvar. Suas alterações continuam aqui; tente novamente.", + "dash.shadowCallIntercept": "Shadow Call Intercept", + "dash.shadowCallInterceptHint": "Intercepta as chamadas auxiliares em segundo plano do Codex App ({models}) para geração de títulos e mensagens de commit e as redireciona ao modelo escolhido.", + "dash.shadowCallWarning": "⚠ Quando ativado, TODAS as requisições para {models} serão substituídas pelo modelo selecionado.", + "dash.shadowCallOriginal": "Original", + "dash.shadowCallModel": "Modelo substituto", + "dash.shadowCallTooltip": "O Codex App faz chamadas auxiliares em segundo plano para geração de título de thread, geração de mensagem de commit e orquestração de skills. O modelo auxiliar mudou entre versões do cliente, então o opencodex intercepta todos os modelos deste conjunto: {models}. Ative para redirecionar essas chamadas ao modelo escolhido.", + "models.shadowCallIntercept": "Shadow Call Intercept", + "models.shadowCallInterceptHint": "Intercepta as chamadas auxiliares em segundo plano do Codex App ({models}) para títulos e mensagens de commit e as redireciona ao modelo escolhido.", + "dash.sidecarBackend": "Backend", + "dash.sidecarModel": "Modelo", + "dash.backendAuto": "Automático", + "dash.backendOpenAI": "OpenAI", + "dash.backendAnthropic": "Anthropic", + "dash.sidecarSaved": "Configurações do sidecar salvas. Aplicadas na próxima requisição.", + "dash.sidecarSaveFailed": "Falha ao salvar as configurações do sidecar.", + "dash.injectionLabel": "Delegação a subagentes", + "dash.injectionHint": "Escolha o modelo para o qual o Codex deve delegar o trabalho de subagentes. As duas opções abaixo definem onde essa escolha é usada.", + "dash.injectionManage": "Abrir configurações", + "dash.syncCodexSubagentDefaults": "Salvar também como padrão do Codex", + "dash.syncCodexSubagentDefaultsHint": "Ativado, a escolha acima é gravada na configuração do próprio Codex, e novas tarefas também começam com esse modelo. Desativado, ela fica salva apenas aqui. Entra em vigor na próxima sincronização ou reinicialização, e suas configurações [agents] escritas à mão são preservadas.", + "dash.multiAgentGuidance": "Orientar o Codex a dividir o trabalho", + "dash.multiAgentGuidanceHint": "Envia uma nota curta dizendo ao Codex como repassar trabalho a subagentes. Na v2, ela cita os modelos que podem ser usados e qual preferir; na v1, só se aplica com esforço de raciocínio max ou ultra. Desativado, nenhuma nota é adicionada.", + "dash.injectionNone": "Nenhum", + "dash.injectionEffortLabel": "Esforço de raciocínio", + "dash.injectionEffortNone": "Padrão do modelo", + "dash.effortCapLabel": "Limite de esforço ultra na V2", + "dash.subagentEffortCapLabel": "Limite de esforço de subagentes na V2", + "dash.effortCapHelp": "Limita o esforço de raciocínio dos turnos em modo ultra da V2. Quando definido, requisições de esforço máximo (do modo ultra) são limitadas ao nível selecionado. O limite de subagentes se aplica apenas aos agentes filhos criados. Os limites só reduzem o esforço, nunca o aumentam. Se um modelo não suportar o nível limitado, ele desce para o nível suportado mais próximo.", + "dash.effortCapNone": "Sem limite", + "dash.maintenance": "Manutenção", + "dash.maintenanceHint": "Atualize o catálogo de modelos do Codex ou instale uma versão mais nova do opencodex.", + "dash.syncModels": "Sincronizar modelos", + "dash.syncModelsHint": "Reescreve o catálogo de modelos do Codex a partir dos provedores conectados.", + "dash.syncRun": "Sincronizar agora", + "dash.syncing": "Sincronizando…", + "dash.syncOk": "Sincronização concluída. {count} modelo(s) adicionado(s).", + "dash.syncStaleHint": "Se o Codex ainda mostrar uma lista antiga, reinicie o app-server de longa duração dele ({cmd}).", + "dash.syncFailed": "Falha na sincronização: {error}", + "dash.projectConfigTitle": "A configuração de Codex do projeto ignora o OpenCodex", + "dash.projectConfigHint": "Estas configurações locais do repositório substituem o proxy do OpenCodex (por exemplo, roteando direto para o OpenCode Go). Remova-as para que o roteamento de ~/.codex/config.toml valha nesse projeto.", + "dash.checkUpdate": "Verificar atualização", + "dash.updateTitle": "Atualizar o opencodex", + "dash.updateDesc": "Verifica no npm o canal selecionado e depois permite escolher se o proxy será reiniciado após a instalação.", + "dash.updateChannel": "Canal", + "dash.updateChecking": "Verificando atualizações…", + "dash.updateInstalled": "Instalada", + "dash.updateLatest": "Mais recente", + "dash.updateAvailable": "Atualização disponível", + "dash.updateCurrent": "Atualizado", + "dash.updateCommand": "Comando", + "dash.updateSource": "Esta é uma cópia do código-fonte. Atualize-a pelo terminal com o comando exibido.", + "dash.updateUnavailable": "Não foi possível ler a versão mais recente no npm. Tente novamente mais tarde.", + "dash.updateRetry": "Tentar novamente", + "dash.updateRecheck": "Verificar de novo", + "dash.updateCannotAuto": "A atualização com um clique não está disponível ({reason}).", + "dash.updateReason.source_checkout": "cópia do código-fonte", + "dash.updateReason.latest_unavailable": "registro do npm inacessível", + "dash.updateReason.already_latest": "já está na versão mais recente", + "dash.updateReason.externally_managed": "gerenciado externamente pelo mise; execute o comando exibido", + "dash.updateReason.external_ownership_invalid": "os metadados de propriedade do mise estão ilegíveis ou inconsistentes", + "dash.updateReason.unknown": "atualização indisponível", + "dash.updateRestart": "Reiniciar após atualizar", + "dash.updateRestartHint": "Recomendado. A GUI atual continua executando o código antigo até o proxy reiniciar.", + "dash.runUpdate": "Atualizar", + "dash.updateReconnecting": "Aguardando o proxy reiniciado…", + "dash.updateStatus.running": "Atualizando o opencodex.", + "dash.updateStatus.restarting": "Atualização instalada. Reiniciando o proxy.", + "dash.updateStatus.succeeded": "Atualização concluída.", + "dash.updateVersionTransition": "{currentVersion} -> {latestVersion}.", + "dash.updateStatus.failed": "Falha na atualização.", + "prov.subtitle": "Configure os provedores upstream para os quais o opencodex roteia o Codex. Faça login com uma conta, adicione um provedor ou edite a configuração bruta.", + "prov.add": "Adicionar provedor", + "prov.editJson": "Editar JSON", + "prov.accountLogin": "Login de conta", + "prov.noOauth": "Nenhum provedor OAuth disponível.", + "prov.loggedIn": "conectado", + "prov.notLoggedIn": "não conectado", + "prov.logout": "Sair", + "prov.login": "Entrar", + "prov.loginWith": "Entrar com {provider}", + "prov.waitingBrowser": "Aguardando o navegador…", + "prov.didntOpen": "Não abriu? Clique aqui", + "prov.copyLink": "Copiar link", + "prov.dontOpenBrowser": "Não abrir um navegador na máquina do proxy", + "prov.dontOpenBrowserHint": "Útil para usar outro perfil de navegador ou quando o painel não está na máquina do proxy.", + "prov.linkCopied": "Copiado", + "prov.linkCopyUnavailable": "Área de transferência indisponível", + "prov.deviceCode": "Código do dispositivo", + "prov.copyCode": "Copiar código", + "prov.copyCodeAndOpen": "Copiar código e abrir", + "prov.openSignInPage": "Abrir página de login", + "prov.browserLaunchFailed": "O navegador não abriu automaticamente. Abra a página de login abaixo ou copie o link.", + "prov.codeCopied": "Código copiado", + "prov.editAlias": "Editar apelido", + "prov.aliasPrompt": "Nome de exibição (deixe vazio para limpar)", + "prov.aliasSaved": "Apelido salvo", + "prov.aliasSaveFailed": "Não foi possível salvar o apelido", + "prov.aliasInvalid": "Use no máximo 80 caracteres, sem caracteres de controle.", + "prov.accountId": "ID", + "prov.pasteRedirect": "Cole a URL de redirecionamento ou o código", + "prov.pasteRedirectHint": "Se o navegador mostrar um erro de localhost, copie a URL completa da barra de endereços e cole aqui (ou cole o código de autorização).", + "prov.pasteSubmit": "Enviar", + "prov.pasteSubmitting": "Enviando…", + "prov.pasteOk": "Código enviado — concluindo o login…", + "prov.pasteFail": "Não foi possível enviar o código: {error}", + "prov.port": "Porta", + "prov.default": "Padrão", + "prov.loadingConfig": "Carregando…", + "prov.saved": "Salvo! Reinicie o proxy para aplicar.", + "prov.loadConfigFail": "Falha ao carregar a configuração", + "prov.invalidJson": "JSON inválido", + "prov.saveFailed": "Falha ao salvar", + "prov.loginFailStart": "O login de {provider} não conseguiu iniciar", + "prov.loginError": "Erro no login de {provider}: {error}", + "prov.loginRequestFail": "A requisição de login de {provider} falhou", + "prov.loginCancelled": "Login de {provider} cancelado", + "prov.loginTimeout": "O login de {provider} expirou — o navegador foi fechado ou o processo nunca terminou. Tente novamente.", + "prov.loginOk": "Login em {provider} concluído. Execute {cmd} (ou ele se aplica ao vivo) para listar seus modelos.", + "prov.loginSameAccount": "Ainda é a mesma conta de {provider} — troque de conta no navegador e tente adicionar a conta novamente.", + "oauthTos.highTitle": "{provider}: risco do OAuth de assinatura", + "oauthTos.elevatedTitle": "{provider}: ponte OAuth não oficial", + "oauthTos.anthropicBody": "Isto conecta uma assinatura do Claude por meio do proxy de terceiros do OpenCodex. A Anthropic não endossou esta conexão nem o pooling automatizado de contas; o acesso depende dos termos e da fiscalização vigentes.", + "oauthTos.highBody": "O OpenCodex conecta {provider} por um caminho OAuth de terceiros. O uso sem suporte pode levar a limites de acesso ou suspensão.", + "oauthTos.elevatedBody": "O OpenCodex conecta {provider} por um caminho OAuth não oficial. Use o cliente oficial sempre que possível; tráfego incomum ou automatizado pode ser tratado como abuso, e o acesso pode ser limitado ou suspenso.", + "oauthTos.saferPath": "Opção mais segura: configure uma chave de API no OpenCodex.", + "oauthTos.acknowledge": "Entendo o risco e quero continuar com OAuth mesmo assim.", + "oauthTos.continue": "Continuar com OAuth", + "oauthTos.anthropicTitle": "Conexão de assinatura do Claude", + "oauthTos.anthropicConditions": "Use uma conta que você possua ou esteja autorizado a usar. Usar o cliente Claude Code genuíno com uma pessoa supervisionando a sessão é diferente de enviar tokens de assinatura por outros clientes. O OpenCodex também oferece suporte a esses outros clientes, portanto esta conexão não garante que todo uso seja permitido. As integrações com o Agent SDK compatíveis são tratadas à parte.", + "oauthTos.anthropicSaferPath": "Para outros clientes ou automação sem supervisão, configure uma chave de API da Anthropic.", + "oauthTos.anthropicAcknowledge": "Entendo que esta é uma conexão de assinatura de terceiros e verificarei os termos aplicáveis.", + "oauthTos.anthropicContinue": "Continuar com a assinatura do Claude", + "subagentSurface.selectionTitle": "Mudar a superfície de subagentes para {mode}?", + "subagentSurface.selectionBody": "Em {mode}, os modelos do ChatGPT que usam a superfície v2 — Sol e Terra na base, todos os modelos na v2 — entregam sua tarefa a um modelo roteado, como Grok ou Claude, criptografada para o backend do ChatGPT, e o modelo roteado não consegue lê-la. Essa delegação falha com unreadable_encrypted_agent_task até que seja corrigido upstream. A v1 delega entre provedores de forma confiável.", + "subagentSurface.advisoryTitle": "A v1 agora é a superfície de subagentes padrão", + "subagentSurface.advisoryBody": "Esta instalação roda {mode}, em que um modelo do ChatGPT na superfície v2 entrega a um modelo roteado uma tarefa criptografada que ele não consegue ler, e a delegação entre provedores falha. Recomendamos a v1 até que isso seja corrigido upstream. Sua configuração atual não muda até você escolher.", + "subagentSurface.continue": "Continuar", + "subagentSurface.switchToV1": "Mudar para a v1", + "subagentSurface.learnMore": "Por que isso falha", + "prov.logoutOk": "Sessão de {provider} encerrada.", + "prov.logoutFail": "Não foi possível sair de {provider}. O estado da sua conta não mudou.", + "prov.removed": "\"{name}\" removido.", + "prov.removedDefault": "\"{name}\" removido. O provedor padrão agora é \"{defaultProvider}\".", + "prov.removedShadowTarget": "\"{name}\" removido. O Shadow Call Intercept ainda aponta para \"{model}\", então as chamadas auxiliares vão falhar até você escolher outro destino.", + "prov.removeFail": "Falha ao remover \"{name}\".", + "prov.removeLastProvider": "Você não pode remover este provedor quando nenhum outro provedor habilitado pode se tornar o padrão.", + "prov.removeHasDependentCombos": "Remova ou atualize antes estes combos dependentes: {combos}.", + "prov.setDefault": "Definir como padrão", + "prov.setDefaultSuccess": "\"{name}\" agora é o provedor padrão.", + "prov.setDefaultFail": "Não foi possível definir \"{name}\" como provedor padrão.", + "prov.defaultDisabled": "Habilite este provedor antes de torná-lo o padrão.", + "prov.updateFail": "Não foi possível atualizar este provedor.", + "prov.networkError": "Erro de rede. Verifique se o proxy está em execução e tente novamente.", + "prov.added": "\"{name}\" adicionado. Já está ativo — execute {cmd} (ou reinicie) para listar seus modelos no seletor do Codex.", + "prov.modelsNoticeTitle": "Escolha os modelos", + "prov.modelsNoticeChecking": "Verificando a lista de modelos. Alternar modelos não desativa o provedor.", + "prov.modelsNoticePending": "A lista inicial de modelos ainda não foi confirmada. Os modelos ficam ocultos até a descoberta terminar.", + "prov.modelsNoticeOff": "Todas as chaves de modelo foram DESLIGADAS no registro. Ative os modelos desejados na página Modelos.", + "prov.modelsNoticeReady": "Escolha quais modelos aparecem na página Modelos. Alternar modelos não desativa o provedor.", + "prov.modelsNoticeFailed": "O provedor foi salvo, mas não foi possível atualizar a lista de modelos. Tente novamente.", + "prov.modelsNoticeCount": "{count} modelos", + "prov.modelsNoticeOpen": "Abrir Modelos", + "models.initialSelectionPending": "Descoberta inicial pendente", + "prov.removeConfirm": "Remover o provedor \"{name}\"? Os modelos dele somem do seletor do Codex.", + "prov.hasApiKey": "chave de API configurada", + "prov.hasHeaders": "cabeçalhos personalizados configurados", + "prov.accounts": "Contas ({n})", + "prov.accountsAria": "Alternar contas de {name}", + "prov.accountActive": "Ativa", + "prov.accountReauth": "Entrar de novo", + "prov.reauthenticate": "Reautenticar", + "prov.reauthAccountMissing": "A conta selecionada não foi encontrada após o login", + "prov.reauthIdentityMismatch": "A conta conectada não corresponde à conta selecionada", + "prov.accountAdd": "Adicionar conta", + "prov.accountNoLabel": "conta {id}", + "prov.accountSwitchTitle": "Usar esta conta", + "prov.accountSwitched": "Trocado para {email}.", + "prov.accountSwitchFail": "Falha ao trocar de conta", + "prov.accountRemoved": "{email} removida.", + "prov.accountRemoveFail": "Não foi possível remover {email}. A conta não foi alterada.", + "prov.accountRemoveAria": "Remover {email}", + "prov.accountRemoveConfirm": "Remover a conta {email}? O login dela é excluído deste proxy.", + "prov.keyAdd": "Adicionar chave de API", + "prov.keyAdded": "Chave de API adicionada a {name}.", + "prov.keyAddFail": "Falha ao adicionar a chave de API", + "prov.keyPlaceholder": "Cole a chave de API", + "prov.keySwitchTitle": "Usar esta chave", + "prov.keySwitched": "Trocado para a chave {key}.", + "prov.keySwitchFail": "Falha ao trocar de chave", + "prov.keyRemoved": "Chave {key} removida.", + "prov.keyRemoveAria": "Remover a chave {key}", + "prov.keyRemoveConfirm": "Remover a chave de API {key}? Ela é excluída da configuração deste proxy.", + "prov.activeBadge": "Ativa", + "prov.disabledBadge": "Desativado", + "prov.defaultBadge": "Padrão", + "prov.enable": "Habilitar", + "prov.disable": "Desabilitar", + "prov.enabled": "\"{name}\" habilitado. Seus modelos podem voltar a aparecer no Codex.", + "prov.disabled": "\"{name}\" desabilitado. As configurações são mantidas, mas os modelos ficam ocultos.", + "prov.disabledShadowTarget": "\"{name}\" desabilitado. O Shadow Call Intercept ainda aponta para \"{model}\", então as chamadas auxiliares vão falhar até você escolher outro destino ou reabilitar \"{name}\".", + "prov.enableFail": "Falha ao habilitar \"{name}\".", + "prov.disableFail": "Falha ao desabilitar \"{name}\".", + "prov.enableAria": "Habilitar o provedor {name}", + "prov.disableAria": "Desabilitar o provedor {name}", + "prov.defaultCannotDisable": "O provedor padrão não pode ser desabilitado", + "prov.openaiAccountMode": "Modo de conta do Codex", + "prov.openaiModePool": "Pool", + "prov.openaiModeDirect": "Direto", + "prov.openaiPoolDesc": "Padrão. Alterna entre o login principal e as contas adicionadas usando afinidade, cota, cooldown e failover.", + "prov.openaiDirectDesc": "Usa somente o login atual/principal do Codex. As contas armazenadas no pool não são lidas nem alternadas.", + "prov.openaiModeSaved": "Modo de conta da OpenAI alterado para {mode}.", + "prov.openaiModeSaveFailed": "Não foi possível alterar o modo de conta da OpenAI.", + "prov.openaiApiDesc": "Usa uma chave de API da OpenAI e nunca usa as credenciais da conta do Codex.", + "prov.manageCodexAccounts": "Gerenciar contas do Codex", + "prov.openaiApiMissing": "Chave de API obrigatória", + "prov.openaiApiSetup": "Configurar chave de API", + "models.tab.catalog": "Modelos", + "models.tab.combos": "Combos", + "models.tab.compatibility": "Compatibilidade", + "models.tab.routing": "Roteamento (beta)", + "models.tabsLabel": "Superfícies de modelos", + "models.subtitle.combos": "Grupos ordenados de modelos que respondem como um único id. Encadeie destinos com failover ou distribua a carga com uma estratégia de balanceamento.", + "models.subtitle.compatibility": "Matriz somente leitura de veredictos de compatibilidade, com base em evidências de projeção de laboratório.", + "models.subtitle.routing": "Perfis de política, avaliação em modo de simulação (dry-run) e análises de roteamento com base em fontes.", + "models.subtitle": "Alterne quais modelos o Codex enxerga — passthrough nativo do GPT e provedores roteados, agrupados por provedor (clique em um cabeçalho para recolher). Modelos ocultos ficam fora do catálogo e do seletor de modelos, mas continuam acessíveis diretamente pelo id exato.", + "models.delivery.title": "Como as alterações chegam ao Codex", + "models.delivery.chip.saved": "Salvo aqui", + "models.delivery.chip.savedHub": "Salvo no hub", + "models.delivery.chip.synced": "Sincronizado {time}", + "models.delivery.chip.syncedUnknown": "Nenhuma sincronização registrada", + "models.delivery.chip.loaded": "O Codex carrega ao reiniciar", + "models.delivery.saved.title": "Salvo neste proxy", + "models.delivery.saved.body": "Salvar nesta página reescreve o catálogo de modelos que este proxy OpenCodex entrega ao Codex, que é a lista exibida no seletor de modelos do Codex. A confirmação após salvar significa que esta etapa foi concluída.", + "models.delivery.savedHub.title": "Salvo no hub", + "models.delivery.savedHub.body": "Esta máquina está conectada a um hub OpenCodex compartilhado, então salvar aqui altera o catálogo do hub. A confirmação após salvar significa que o hub o recebeu; esta máquina ainda precisa baixá-lo.", + "models.delivery.synced.title": "Sincronizado nesta máquina", + "models.delivery.synced.bodyAt": "Esta máquina baixou o catálogo do hub pela última vez em {time} e o gravou onde o Codex lê. Esse horário indica apenas quando o download ocorreu. Se você salvou no hub depois disso, esta máquina mantém a lista anterior até a próxima sincronização.", + "models.delivery.synced.bodyUnknown": "Esta máquina ainda não tem registro de download do catálogo do hub, então o Codex pode estar lendo uma lista antiga. A próxima sincronização fará o download.", + "models.delivery.loaded.title": "Carregado pelo Codex", + "models.delivery.loaded.body": "O Codex lê o catálogo ao iniciar, então uma janela do Codex que já estava aberta mantém a lista de modelos antiga. O OpenCodex não consegue ver dentro de um Codex em execução para saber qual lista ele usa. Quando consegue perceber que o Codex iniciou antes da sua alteração, um aviso de reinicialização aparece acima das abas.", + "models.delivery.hint": "Se um modelo que você ativou não aparecer no Codex, clique em Recarregar modelos do Codex no canto superior direito desta página e reabra o Codex.", + "models.nativeGroupLabel": "OpenAI nativo", + "models.nativeHint": "Os modelos em passthrough usam a opção de conta Pool ou Direto selecionada em Provedores. Desativar um deles o oculta do seletor do Codex (a entrada do catálogo é mantida, então reativar restaura exatamente como estava). Adicionar um modelo aqui registra um seletor roteado `openai/`, não um novo id de passthrough simples.", + "models.active": "{active}/{total} visíveis", + "models.workspace.providers": "Provedores", + "models.workspace.allProviders": "Todos os provedores", + "models.workspace.mainAria": "Detalhes do modelo", + "models.allOn": "Ativar todos", + "models.allOff": "Desativar todos", + "models.presetLabel": "Modelos", + "models.presetMode_preset": "Preset", + "models.presetMode_all": "Todos", + "models.presetMode_custom": "Personalizado", + "models.presetSummary": "{count} de {total} exibidos — preset básico v{version}", + "models.presetUpdateAvailable": "Preset v{version} disponível", + "models.presetAppliedToast": "{provider}: preset aplicado — {count} modelos selecionados", + "models.presetClearedToast": "{provider}: exibindo todos os modelos", + "models.presetEmpty": "{provider}: o preset não correspondeu a nenhum modelo — seleção inalterada", + "models.presetConfirmReplace": "Substituir sua seleção pelo preset de {count} modelos?", + "models.cap350k": "Limite de 350k", + "models.capApplied": "Limite de contexto aplicado — vale a partir do próximo turno do Codex.", + "models.capSaveFailed": "Falha ao salvar o limite de contexto", + "models.contextCapped": "limite de 350k", + "models.contextCapLabel": "Janela padrão / limite", + "models.v2Label": "Subagente", + "models.shadowCallOriginal": "{models} →", + "models.v2DocsLink": "O que são v1 / v2?", + "models.v2Mode_v1": "v1", + "models.v2Mode_default": "base", + "models.v2Mode_v2": "v2", + "models.v2ModeDesc_v1": "Todos os modelos → superfície v1", + "models.v2ModeDesc_default": "Padrões do upstream (sol/terra=v2, luna=v1)", + "models.v2ModeDesc_v2": "Todos os modelos → superfície v2", + "models.keepNativeOnV1": "Manter o ChatGPT na v1", + "models.keepNativeOnV1Hint": "O ChatGPT só criptografa as tarefas filhas da v2 quando um pai nativo do ChatGPT permanece na v2, então Grok e Claude não conseguem lê-las. Ative para manter Sol/Terra na v1 e evitar essa criptografia. Pais roteados continuam na v2.", + "models.v2Help": "Controla a superfície multiagente para todos os modelos.\n\nv1: agente clássico de thread única. Todos os modelos usam a superfície collab da v1.\nbase: padrões do upstream — sol/terra usam a v2, luna usa a v1, os demais seguem a feature flag do codex.\nv2: agente multithread com spawn_agent. Todos os modelos usam a superfície collab da v2.\n\nNa v2, Manter o ChatGPT na v1 deixa Sol/Terra na superfície v1 para que ainda possam acionar Grok ou Claude. O ChatGPT criptografa as tarefas filhas da v2; modelos roteados não conseguem lê-las. Pais roteados permanecem na v2.\n\nAs alterações valem para novas sessões.", + "dash.multiAgent": "Subagente", + "dash.codexDesktopAuthless": "Abrir o Codex sem fazer login", + "dash.codexDesktopAuthlessHint": "Desativado por padrão. Dispensa o login separado do Desktop em conexões locais elegíveis. As credenciais upstream continuam sendo necessárias. Reinicie o Codex após alterar esta configuração. Recursos do Desktop que dependem de conta podem ficar indisponíveis.", + "dash.codexClientCompaction": "Usar compactação no cliente", + "dash.codexClientCompactionHint": "Desativado por padrão; somente loopback autenticado. As próximas compactações armazenam resumos portáveis em texto simples enquanto o roteamento de provedores do OpenCodeX e da V2 permanece ativo; o provedor configurado pode processá-los e consumir cota. O histórico não é alterado, e as threads existentes continuam roteando pelo proxy por meio do override openai_base_url gerenciado pelo OpenCodeX; se você definir essa linha manualmente, ela é mantida, e essas threads seguem o seu destino. O histórico ocx1 existente continua recuperável; recupere uma thread separadamente apenas antes de reproduzi-la no Codex nativo. Reinicie o Codex após alterar esta configuração.", + "dash.codexRemoteHistoryHint": "O roteamento por tabela de provedores pode ocultar threads existentes marcadas como openai em algumas listas remotas do celular. O histórico não é excluído. O cliente remoto precisa listar todos os provedores; esta opção não corrige o filtro desse cliente.", + "models.v2Conflict": "[agents] max_threads está definido — o codex se recusará a iniciar; remova-o do config.toml", + "models.v2Applied": "Modo de subagente atualizado — vale para novas sessões (reinicie o app do Codex para atualizar o seletor)", + "models.v2ThreadsLabel": "Máx. de threads", + "models.v2ThreadsDefault": "padrão (4)", + "models.v2ThreadsApplied": "Limite de threads atualizado — vale para novas sessões", + "models.v2ThreadsInvalid": "O limite de threads deve ser um inteiro >= 1", + "models.v2ThreadsApply": "Aplicar", + "models.capValue": "Padrão {value}", + "models.contextSettings": "Janelas personalizadas", + "models.contextSettingsTitle": "Janelas personalizadas — {provider}", + "models.contextDefault": "Padrão do provedor", + "models.contextModel": "Modelo", + "models.contextModelOverride": "Substituição do modelo", + "models.contextHint": "Informe aqui a janela real do Codex quando já a conhecer. Isso preenche uma janela upstream ausente e só reduz uma janela maior informada. Deixe em branco para usar a Janela padrão / limite do provedor, ou 128k se esse limite estiver desativado.", + "models.contextAutomatic": "Descoberta automática", + "models.contextSaved": "Janelas de contexto atualizadas — valem a partir do próximo turno do Codex.", + "models.contextUnchanged": "Nenhuma alteração de janela de contexto para salvar.", + "models.contextSaveFailed": "Falha ao salvar as janelas de contexto", + "models.contextInvalid": "As janelas de contexto devem ser números inteiros positivos", + "models.contextCappedValue": "limite de {value}", + "models.setAll": "Definir para todos", + "models.setAllHint": "Ativa a janela padrão de {value} para todos os provedores roteados. Relays que omitem context_window / context_length recebem este valor como a janela real do Codex. Use Janelas personalizadas na linha de um provedor para ajustar um modelo manualmente. Provedores nativos não são afetados.", + "models.collapseAll": "Recolher tudo", + "models.expandAll": "Expandir tudo", + "models.orderHint": "A ordem padrão segue as seleções em destaque e a prioridade do catálogo, com ordenação por provedor/modelo para linhas roteadas empatadas. Uma ordem salva do seletor pode sobrepor a ordem de exibição; as chaves de visibilidade filtram quais linhas aparecem. Um cliente pode manter um catálogo antigo até buscá-lo novamente.", + "models.settingsPanel.title": "Configurações do modelo", + "models.settingsPanel.off": "desativado", + "models.settingsPanel.attention": "Requer atenção", + "models.custom": "Personalizado…", + "models.customApply": "Aplicar", + "models.customPlaceholder": "Tokens (ex.: 420000)", + "models.customAdd": "Adicionar modelo personalizado", + "models.customAddTitle": "Adicionar modelo personalizado — {provider}", + "models.customEditTitle": "Editar modelo personalizado — {provider}", + "models.settingsTitle": "Editar modelo — {model}", + "models.settingsContextInherit": "Sem substituição — atualmente {value}", + "models.settingsContextUnknown": "desconhecido", + "models.settingsReload": "Recarregar", + "models.settingsReloaded": "Lista de modelos recarregada. Reabra {model} para ver os valores armazenados.", + "models.settingsNoChange": "Nenhuma configuração alterada para {model}", + "models.settingsSaved": "Configurações do modelo salvas para {model}", + "models.settingsSaveFailed": "Não foi possível confirmar se as configurações foram salvas. Recarregue a lista de modelos antes de tentar novamente.", + "models.settingsRefreshFailed": "Salvo. A lista de modelos não foi recarregada, então a linha pode mostrar valores antigos. Recarregue para conferir.", + "models.settingsSavedCodexStale": "{model} salvo, mas o catálogo de modelos do Codex não foi atualizado. Clique em Sincronizar agora no Painel para o Codex receber a alteração.", + "models.settingsRejected": "O servidor rejeitou estas configurações, então nada foi salvo. Confira os valores e tente novamente.", + "models.settingsDefaultEffort": "Nível padrão", + "models.settingsModalitiesInherit": "Sem substituição — seguindo atualmente a declaração do upstream: {modalities}", + "models.settingsModalitiesUnknown": "não declarado", + "models.settingsRestore": "Restaurar", + "models.settingsRestoreHint": "Limpa todas as substituições deste modelo e volta aos valores calculados.", + "models.settingsRestoreConfirm": "Restaurar {model} para os valores calculados? Isso limpa as substituições de janela de contexto, modalidade e raciocínio.", + "models.settingsRestored": "{model} restaurado para os valores calculados", + "models.settingsNothingToRestore": "{model} não tem substituições; já está nos valores calculados", + "pws.manageModelVisibility": "Gerenciar visibilidade em Modelos", + "pws.modelOwnershipLoading": "Verificando as definições de modelos…", + "pws.modelOwnershipFailed": "Não foi possível carregar as definições de modelos. Tente novamente antes de fazer alterações.", + "pws.modelSelectionPending": "Conclua a seleção de modelos em Modelos antes de fazer alterações.", + "pws.modelSaved": "Definição de modelo personalizado salva.", + "pws.modelSavedHidden": "Definição salva. Este modelo está oculto; gerencie a visibilidade em Modelos.", + "pws.modelSavedRefreshPending": "Definição salva, mas não foi possível atualizar o catálogo de modelos. Tente a atualização novamente; não o adicione de novo.", + "pws.modelMutationUnconfirmed": "Não foi possível confirmar a alteração. Atualize os modelos antes de tentar novamente.", + "pws.modelRemovedRefreshPending": "A alteração foi salva, mas não foi possível atualizar o catálogo de modelos. Tente a atualização novamente.", + "pws.modelHidden": "Modelo oculto. Restaure a visibilidade em Modelos.", + "pws.modelDefinitionDeleted": "Definição personalizada excluída. Um modelo subjacente ainda pode aparecer.", + "pws.modelKnown": "Este modelo já é conhecido. Gerencie a visibilidade dele em Modelos.", + "models.customAdded": "Modelo personalizado adicionado", + "models.customUpdated": "Modelo personalizado atualizado", + "models.customDeleted": "Modelo personalizado excluído", + "models.customSaveFailed": "Falha ao salvar o modelo personalizado", + "models.customSaving": "Salvando…", + "models.customAddBtn": "Adicionar", + "models.customEditBtn": "Atualizar", + "models.customEdit": "Editar", + "models.customDelete": "Excluir", + "models.customDeleteConfirm": "Excluir a definição personalizada de {name}? Um modelo nativo ou descoberto subjacente pode voltar a aparecer.", + "models.hide": "Ocultar", + "models.hideConfirm": "Ocultar {name} do catálogo de modelos? Isso não remove a definição nem altera a política de roteamento direto.", + "models.customBadge": "Personalizado", + "models.customSummary": "{count} personalizado(s)", + "models.customFieldModelId": "ID do modelo (slug do endpoint)", + "models.customFieldModelIdPlaceholder": "ex.: qwen4-max-preview", + "models.customFieldDisplayName": "Nome de exibição (opcional)", + "models.customFieldDisplayNamePlaceholder": "ex.: Qwen 4 Max Preview", + "models.customFieldContext": "Janela de contexto", + "models.customFieldModalities": "Modalidades de entrada", + "models.customFieldReasoning": "Esforço de raciocínio", + "models.customFieldReasoningOverride": "Substituir o esforço de raciocínio", + "models.reasoningEffort.none": "Nenhum", + "models.reasoningEffort.minimal": "Mínimo", + "models.reasoningEffort.low": "Baixo", + "models.reasoningEffort.medium": "Médio", + "models.reasoningEffort.high": "Alto", + "models.reasoningEffort.xhigh": "Extra alto", + "models.reasoningEffort.max": "Máximo", + "models.reasoningEffort.ultra": "Ultra", + "models.tipProvider": "Provedor", + "models.tipContext": "Contexto", + "models.tipModalities": "Modalidades", + "models.tipStatus": "Status", + "models.tipActive": "Ativo", + "models.tipDisabled": "Desativado", + "models.applied": "Salvo. O Codex mostra a alteração depois de recarregar a lista de modelos.", + "models.integrationRefreshWarning": "Seleção de modelos salva. Alguns catálogos de clientes não puderam ser atualizados. Verifique Integrações antes de iniciar uma nova sessão.", + "models.saveFailed": "Falha ao salvar", + "models.networkError": "Erro de rede — o proxy está em execução?", + "models.loadFail": "Falha ao carregar os modelos — o proxy está em execução?", + "models.noRouted": "Nenhum modelo roteado", + "models.noRoutedHint": "Faça login em um provedor ou adicione um primeiro.", + "models.emptyDiscovery": "Nenhum modelo foi descoberto. Verifique o endpoint do provedor ou adicione um modelo estático/personalizado.", + "models.emptyDiscoveryDisabled": "A descoberta de modelos em tempo real está desativada e nenhum modelo estático está configurado.", + "models.discoveryFailedBadge": "Falha na descoberta", + "models.discoveryFailedHttp": "A descoberta de modelos falhou (HTTP {status}).", + "models.discoveryFailedBlocked": "A descoberta de modelos foi bloqueada pela política do destino.", + "models.discoveryFailedInvalidResponse": "A descoberta de modelos retornou uma resposta inválida.", + "models.discoveryFailedNetwork": "A descoberta de modelos falhou por erro de rede.", + "models.discoveryFailedProvider": "O provedor informou um erro na descoberta de modelos.", + "models.discoveryFailedGeneric": "A descoberta de modelos falhou.", + "models.openProviderSettings": "Abrir configurações do provedor", + "models.inactiveNoCredit": "Sem crédito", + "models.inactiveNoCreditHint": "Todos os provedores por trás desta entrada estão sem crédito no momento, então uma requisição falharia. Ela continua listada e volta a ser utilizável quando o crédito for renovado ou recarregado.", + "models.discoveryFailedDependency": "A descoberta de modelos está ativada para este provedor. Enquanto continuar falhando, desative “{control}” nas configurações do provedor para usar modelos adicionados manualmente ou estáticos.", + "models.loading": "Carregando…", + "models.search": "Buscar modelos…", + "models.showMore": "Mostrar mais {n}", + "models.freeOnly": "Somente gratuitos", + "models.noFreeMatch": "Nenhum modelo gratuito aqui. Desative “Somente gratuitos” para ver os demais.", + "models.allowlistLabel": "Somente selecionados", + "models.allowlistHint": "Somente os modelos marcados vão para o catálogo (vazio = todos). Útil para provedores que expõem milhares de modelos.", + "models.selectedCount": "{n} selecionados", + "sub.subtitle": "O {cmd} do Codex anuncia apenas os 5 primeiros modelos (por prioridade) como substituições. Escolha até 5 aqui — gpt nativo ou roteado — e o opencodex define a prioridade deles no catálogo para que exatamente esses fiquem à frente. Qualquer outro modelo continua acessível pelo nome exato; isto controla apenas o que é exibido.", + "sub.featured": "Em destaque", + "sub.advanced": "Avançado", + "sub.orderHintAria": "Como esta ordem é usada", + "sub.orderHint": "A ordem exibida aqui define as posições 1 a 5 no topo do seletor de modelos do Codex e os modelos padrão candidatos para {cmd}.", + "sub.noneSelected": "Nenhum selecionado — escolha na lista abaixo.", + "sub.models": "Modelos", + "sub.search": "Buscar modelos (gpt nativo + roteados)…", + "sub.settings": "Configurações", + "sub.sections": "Seções de subagentes", + "sub.delegation.model": "Modelo a chamar primeiro", + "sub.delegation.modelHint": "O modelo que o Codex aciona primeiro ao delegar trabalho. “Em destaque” acima é a lista que ele pode chamar; este é o que ele chama primeiro.", + "sub.suggest.button": "Sugerir", + "sub.suggest.label": "Trabalho delegado", + "sub.suggest.placeholder": "O que o Codex costuma delegar? Ex.: buscas somente leitura em todo o repositório ou refatorações em vários módulos", + "sub.suggest.submit": "Dimensionar", + "sub.suggest.running": "Dimensionando…", + "sub.suggest.failed": "Não foi possível dimensionar o trabalho delegado.", + "sub.suggest.title": "Modelo sugerido", + "sub.suggest.accept": "Usar este", + "sub.noModels": "Nenhum modelo — faça login em um provedor ou adicione um primeiro.", + "sub.saved": "{n} modelos salvos. Inicie uma nova sessão do Codex (ou execute {cmd}) para vê-los como substituições de spawn_agent.", + "sub.saveFailed": "Falha ao salvar", + "sub.networkError": "Erro de rede — o proxy está em execução?", + "sub.loadFail": "Falha ao carregar os modelos — o proxy está em execução?", + "sub.loading": "Carregando…", + "sub.moveUp": "Mover {m} para cima", + "sub.moveDown": "Mover {m} para baixo", + "sub.removeAria": "Remover {m}", + "sub.workspace.addToFeatured": "Adicionar {m} aos destaques", + "sub.workspace.allModels": "Todos os modelos", + "sub.workspace.featuredFull": "A lista de destaques está cheia (máx. 5)", + "sub.workspace.mainAria": "Detalhes do modelo de subagente", + "sub.workspace.notFeatured": "Fora dos destaques", + "sub.workspace.priority": "Prioridade", + "sub.ultraMode": "Delegação sempre proativa", + "sub.ultraModeHint": "Ativa a política de delegação multiagente proativa para todos os modelos e esforços de raciocínio (não altera o esforço de raciocínio em si). Grava features.multi_agent_v2.multi_agent_mode_hint_text no config.toml.", + "sub.ultraModeV2Required": "Requer a superfície multiagente v2 — ative multi_agent_v2 e selecione v2 no controle de modo de subagente primeiro.", + "sub.ultraModeText": "Texto da delegação proativa", + "sub.ultraModePreset": "Restaurar predefinição", + "sub.ultraModeLoadFail": "Falha ao carregar as configurações de delegação proativa — o proxy está em execução?", + "sub.ultraModeSaveFail": "Falha ao salvar as configurações de delegação proativa", + "sub.ultraModeSaved": "Delegação proativa salva. Vale para novas sessões do Codex.", + "sub.workspace.removeFromFeatured": "Remover {m} dos destaques", + "sub.workspace.selectModel": "Selecione um modelo", + "sub.workspace.selectModelDesc": "Escolha um modelo na lista para ver os detalhes e colocá-lo em destaque para o spawn_agent.", + "sub.workspace.selector": "Seletor público", + "sub.fallbackLabel": "Cadeia de fallback de subagente", + "sub.fallbackHint": "Modelos tentados, em ordem, quando o modelo do subagente estiver indisponível ou falhar.", + "sub.fallbackAdd": "Adicionar modelo de fallback…", + "sub.fallbackPoll": "Intervalo de verificação de disponibilidade", + "sub.fallbackSaved": "Configurações de fallback de subagente salvas.", + "sub.fallbackSaveFailed": "Falha ao salvar as configurações de fallback", + "sub.fallbackUnavailable": "Não anunciado no momento; mantido na cadeia.", + "sub.fallbackPollInvalid": "Informe um número inteiro de 5000 a 600000 ms.", + "sub.v2Compatibility.title": "Compatibilidade V2 com pai nativo", + "sub.v2Compatibility.risk": "Se um pai nativo do ChatGPT delegar a este modelo roteado usando V2, a tarefa pode ser criptografada e falhar antes da execução. Tarefas legíveis vindas de pais roteados não são afetadas.", + "sub.v2Compatibility.recoveryUnknown": "O estado de recuperação ativada/elegibilidade não é exposto por este servidor. Use delegação V1/compatível com texto simples, ou ative a recuperação V2 experimental somente se elegível. A recuperação adiciona consumo de cota, latência, dependência do backend e possível perda de fidelidade; ela não corrige o protocolo upstream.", + "sub.v2Compatibility.details": "Detalhes de compatibilidade", + "logs.title": "Logs de requisições", + "logs.tabLogs": "Logs", + "logs.tabDebug": "Debug", + "logs.subtitle": "Requisições recentes roteadas pelo proxy local do opencodex, das mais novas para as mais antigas.", + "logs.autoRefresh": "Atualização automática", + "logs.noRequests": "Nenhuma requisição ainda.", + "logs.loadError": "Não foi possível carregar os logs de requisições.", + "logs.filter.surface.label": "Superfície", + "logs.filter.model.all": "Todos os modelos", + "logs.filter.provider.label": "Provedor", + "logs.filter.provider.all": "Todos os provedores", + "logs.filter.status.label": "Status", + "logs.filter.status.all": "Todos os status", + "logs.filter.status.success": "Sucesso (2xx)", + "logs.filter.status.errors": "Erros (4xx/5xx)", + "logs.filter.time.label": "Período", + "logs.filter.time.all": "Todo o período", + "logs.filter.time.15m": "Últimos 15 min", + "logs.filter.time.1h": "Última 1 h", + "logs.filter.time.24h": "Último 1 dia", + "logs.filter.speed.label": "Velocidade", + "logs.filter.speed.all": "Todas as velocidades", + "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": "Limpar filtros", + "logs.filter.showingCount": "Exibindo {count} de {total}", + "logs.noMatchingRequests": "Nenhuma requisição correspondente.", + "logs.filter.surface.all": "Todas", + "logs.filter.surface.claude": "Claude", + "logs.filter.surface.codex": "Codex", + "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Somente auxiliares interceptados", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Requisição de auxiliar interceptada", + "logs.filter.conversation.label": "Conversa", + "logs.filter.conversation.placeholder": "Cole o ID da conversa", + "logs.filter.conversation.clear": "Limpar", + "logs.filter.model.label": "Modelo", + "logs.filter.model.placeholder": "Filtrar por modelo ou provedor", + "logs.filter.conversation.apply": "Filtrar logs", + "logs.conversation.totals": "{requests} requisições · {tokens} tokens · {cost}", + "logs.conversation.scope": "Os totais cobrem apenas o buffer de logs atualmente carregado.", + "logs.conversation.excluded": "({unpriced} sem preço, {unmetered} sem medição excluídas do ~$)", + "logs.cost.approximate": "{amount}", + "logs.cost.lowerBound": "≥{amount}", + "logs.cost.unavailable": "—", + "logs.detail.conversation": "Conversa", + "logs.badge.claude": "Claude", + "logs.badge.grok": "Grok", + "logs.col.time": "Hora", + "logs.col.request": "Requisição", + "logs.col.model": "Modelo", + "logs.col.effort": "Esforço", + "logs.col.provider": "Provedor", + "logs.col.status": "Status", + "logs.col.tokens": "Tokens", + "logs.col.tokPerSec": "tok/s", + "logs.col.estimatedCost": "~$", + "logs.metric.tokPerSecTitle": "Tokens de saída por segundo durante toda a duração da requisição", + "logs.metric.estimatedCostTitle": "Equivalente ao preço de tabela da API, não uma cobrança real; preços sem correspondência ficam indisponíveis", + "usage.cost.total": "Equivalente ao preço de tabela da API (este período)", + "usage.cost.disclaimer": "Não é um comprovante de cobrança. Pode valer o uso da assinatura ou créditos do provedor.", + "usage.cost.unpricedNote": "{count} requisições excluídas (sem preço ou uso)", + "usage.cost.excluded": "({count} requisições excluídas)", + "usage.cost.excludedOne": "({count} requisição excluída)", + "logs.detail.section.basic": "Informações básicas", + "logs.detail.route.section": "Decisão de rota", + "logs.detail.route.kind": "Tipo de rota", + "logs.detail.route.profile": "Perfil", + "logs.detail.route.selected": "Selecionado", + "logs.detail.route.candidates": "Candidatos", + "logs.detail.route.unknown": "Nenhum rastreio de rota registrado para esta requisição (linha anterior ao rastreio).", + "logs.filter.protocol.label": "Caminho do protocolo", + "logs.filter.protocol.all": "Todos os caminhos", + "logs.filter.protocol.none": "Sem dados de caminho", + "logs.protocol.mode.native": "Nativo", + "logs.protocol.mode.translated": "Traduzido", + "logs.protocol.mode.legacyBridge": "Ponte legada", + "logs.protocol.mode.blocked": "Bloqueado", + "logs.protocol.disposition.passthrough": "Repassado", + "logs.protocol.disposition.translated": "Traduzido", + "logs.protocol.disposition.degraded": "Degradado", + "logs.protocol.disposition.unsupported": "Descartado", + "logs.protocol.wire.responses": "Responses", + "logs.protocol.wire.chat": "Chat", + "logs.protocol.wire.messages": "Messages", + "logs.protocol.wire.other": "Outro adaptador", + "logs.protocol.hop.ir": "IR", + "logs.protocol.hop.internal": "Responses (interno)", + "logs.protocol.badgeTitle": "Caminho do protocolo: {path} ({mode})", + "logs.detail.protocol.section": "Caminho do protocolo", + "logs.detail.protocol.inbound": "API do cliente", + "logs.detail.protocol.mode": "Modo de entrega", + "logs.detail.protocol.upstream": "Wire upstream", + "logs.detail.protocol.requestPath": "Caminho da requisição", + "logs.detail.protocol.responsePath": "Caminho da resposta", + "logs.detail.protocol.reasons": "Motivos", + "logs.detail.protocol.features": "Efeitos nos recursos", + "logs.detail.protocol.attempts": "Caminhos das tentativas", + "logs.detail.protocol.attempt": "Tentativa {ordinal}", + "logs.detail.protocol.none": "Nenhum dado de caminho registrado para esta requisição.", + "logs.detail.section.performance": "Desempenho", + "logs.detail.section.cost": "Equivalente ao preço de tabela da API", + "logs.detail.section.attempts": "Tentativas do combo", + "logs.detail.section.usage": "Uso bruto", + "logs.detail.ttft": "TTFT", + "logs.detail.decodeTokPerSec": "Taxa de decodificação (est.)", + "logs.detail.reason.ttft_missing": "Nenhum tempo até o primeiro token foi registrado para esta requisição, portanto não há janela de decodificação para medir.", + "logs.detail.reason.decode_window_too_short": "A janela de saída medida foi inferior a um segundo, curta demais para estimar a taxa de decodificação.", + "logs.detail.costTotal": "Equivalente ao preço de tabela", + "logs.detail.totalTokens": "Total de tokens", + "logs.detail.matchedKey": "Chave de preço correspondente", + "logs.detail.priceSource": "Fonte do preço", + "logs.detail.unavailableReason": "Motivo da indisponibilidade", + "logs.detail.copyRequestId": "Copiar ID da requisição", + "logs.detail.copied": "Copiado", + "logs.detail.source.jawcode": "catálogo jawcode", + "logs.detail.source.expected": "Sobreposição de preço esperado", + "logs.detail.source.user": "Sobreposição de preço configurada no provedor", + "logs.detail.verification.verified": "Verificado", + "logs.detail.verification.derived": "Derivado do modelo base", + "logs.detail.attempt.target": "Provedor / modelo", + "logs.detail.attempt.reason": "Resultado / motivo", + "logs.detail.attempt.completed": "Concluída", + "logs.detail.attempt.e2eNote": "O tok/s de nível superior é ponta a ponta; cada tentativa usa sua própria duração.", + "logs.detail.attempt.recovery.transient5xx": "5xx transitório", + "logs.detail.attempt.recovery.connectionReset": "Conexão reiniciada", + "logs.detail.attempt.recovery.oauth401": "Reautenticação OAuth", + "logs.detail.attempt.recovery.key429": "Chave com limite de taxa (429)", + "logs.detail.attempt.recovery.rateLimit429": "Limite de taxa (429)", + "logs.detail.attempt.recovery.anthropicOauth429": "OAuth da Anthropic com limite de taxa (429)", + "logs.detail.attempt.recovery.image413": "Payload de imagem grande demais (413)", + "logs.detail.attempt.recovery.emptyCompletion": "Nova tentativa por conclusão vazia", + "logs.detail.attempt.recovery.consoleGoUpload": "Nova tentativa de upload no console", + "logs.detail.attempt.recovery.key401": "Reautenticação da chave de API", + "logs.detail.attempt.recovery.oauthAccount403": "Validação da conta necessária (403)", + "logs.detail.attempt.recovery.oauthAccount429": "Conta com limite de taxa (429)", + "logs.detail.attempt.recovery.opaqueBlobRejection": "Estado criptografado obsoleto descartado", + "logs.detail.attempt.recovery.reasoningEffortDowngrade": "Esforço de raciocínio reduzido", + "logs.detail.attempt.recovery.anthropicFastDowngrade": "Modo rápido recusado, repetido em velocidade padrão", + "logs.detail.outcome.label": "Resultado", + "logs.detail.outcome.completed": "Concluída", + "logs.detail.outcome.failed": "Falhou", + "logs.detail.outcome.incomplete": "Incompleta", + "logs.detail.outcome.aborted": "Abortada", + "logs.detail.sends.label": "Envios upstream", + "logs.detail.sends.unresolved": "não resolvido", + "logs.detail.attempt.recovery.unknown": "Motivo de recuperação desconhecido", + "logs.detail.cause.label": "Causa da falha", + "logs.detail.stage.label": "chegou a", + "logs.detail.resend.label": "Reenvio", + "logs.detail.cause.transportUnsent": "Nunca enviada ao upstream", + "logs.detail.cause.transportAmbiguous": "Conexão perdida, estado do upstream desconhecido", + "logs.detail.cause.upstreamDeclined": "O upstream recusou iniciar", + "logs.detail.cause.rateLimit": "Limite de taxa atingido", + "logs.detail.cause.quotaExhausted": "Cota esgotada", + "logs.detail.cause.credentialRejected": "Credenciais rejeitadas", + "logs.detail.cause.policyRefusal": "Recusada pela política de conteúdo", + "logs.detail.cause.parameterRejected": "Parâmetro da requisição rejeitado", + "logs.detail.cause.ciphertextRefusal": "Estado criptografado rejeitado", + "logs.detail.cause.payloadTooLarge": "Payload grande demais", + "logs.detail.cause.payloadRejected": "Payload rejeitado", + "logs.detail.cause.upstreamFault": "Falha do upstream", + "logs.detail.cause.emptyOutput": "Nenhuma saída utilizável", + "logs.detail.cause.clientCancelled": "Cancelada pelo cliente", + "logs.detail.cause.localRefusal": "Recusada por este proxy", + "logs.detail.stage.preHeader": "sem cabeçalho de resposta", + "logs.detail.stage.headersOnly": "somente cabeçalhos", + "logs.detail.stage.protocolPrelude": "preâmbulo do protocolo", + "logs.detail.stage.semanticOutput": "saída entregue", + "logs.detail.stage.sideEffect": "efeito colateral emitido", + "logs.detail.stage.terminal": "resposta entregue", + "logs.detail.resend.permitted": "Poderia ser reenviada", + "logs.detail.resend.permittedAfterRepair": "Poderia ser reenviada após reparo", + "logs.detail.resend.refusedAmbiguous": "Não reenviada: estado do upstream desconhecido", + "logs.detail.resend.refusedCommitted": "Não reenviada: o chamador já viu a saída", + "logs.detail.resend.refusedFutile": "Não reenviada: a mesma requisição falharia de novo", + "logs.detail.reason.usage_missing": "O uso não foi informado.", + "logs.detail.reason.usage_unsupported": "Este provedor não informa o uso.", + "logs.detail.reason.output_missing": "Nenhuma contagem positiva de tokens de saída foi informada.", + "logs.detail.reason.invalid_duration": "A duração da requisição não é válida.", + "logs.detail.reason.price_unmatched": "Nenhum preço correspondente foi encontrado.", + "logs.detail.reason.invalid_cache_breakdown": "Os detalhes de tokens de cache conflitam com o total de tokens de entrada.", + "logs.detail.reason.invalid_usage": "O uso contém um valor de token inválido.", + "logs.detail.reason.combo_attempt_unavailable": "Pelo menos uma tentativa do combo não pôde ser precificada.", + "logs.detail.estimate.usage_estimated": "O uso do provedor é estimado.", + "logs.detail.estimate.cache_detail_missing": "Os detalhes de cache estavam indisponíveis; a entrada é uma estimativa de limite superior.", + "logs.detail.estimate.expected_price_overlay": "Foi usado um preço de tabela esperado verificado.", + "logs.detail.estimate.provider_cost_overlay": "Foi usada uma sobreposição de preço configurada no provedor.", + "logs.detail.estimate.priority_lower_bound": "O preço Priority confirmado está indisponível; a estimativa exibida é um limite inferior conhecido.", + "logs.col.error": "Erro", + "logs.col.upstreamReason": "Motivo do upstream", + "logs.col.duration": "Duração", + "logs.modelTooltip.model": "modelo", + "logs.modelTooltip.resolvedModel": "modelo resolvido", + "logs.modelTooltip.servedModel": "modelo atendido", + "logs.modelTooltip.wireModel": "modelo no wire", + "logs.modelRerouteTitle": "O upstream atendeu um modelo diferente do enviado", + "logs.modelTooltip.requestedTier": "nível solicitado", + "logs.modelTooltip.configuredTier": "nível configurado", + "logs.modelTooltip.responseTier": "nível da resposta", + "logs.modelTooltip.supportsTier": "suporte a nível", + "logs.modelTooltip.tierOutcome.confirmed": "confirmado", + "logs.modelTooltip.tierOutcome.assumed": "presumido", + "logs.modelTooltip.tierOutcome.downgraded": "rebaixado", + "logs.modelTooltip.tierOutcome.unknown": "desconhecido", + "logs.tokens.reported": "informado", + "logs.tokens.unreported": "não informado", + "logs.tokens.unsupported": "não suportado", + "logs.tokens.estimated": "estimado", + "logs.tokens.input": "entrada", + "logs.tokens.output": "saída", + "logs.tokens.cacheRead": "leitura de cache (c)", + "logs.tokens.cacheWrite": "gravação de cache (w)", + "logs.tokens.reasoning": "raciocínio", + "logs.tokens.noCache": "sem dados de cache", + "logs.tokens.contextTotal": "contexto ativo", + "logs.tokens.noCacheNote": "este provedor não informa tokens de cache", + "logs.tokens.noCacheCursor": "Detalhe de cache do Cursor não informado", + "logs.tokens.noCacheCursorNote": "O Cursor não expõe as contagens de tokens de leitura/gravação de cache; isso é desconhecido, não um cache miss confirmado", + "logs.tokens.estimatedNote": "estimado (o provedor não informa o uso exato)", + "logs.details": "Detalhes", + "logs.detailTitle": "Detalhes da requisição", + "logs.detailRaw": "Entrada de log bruta", + "debug.title": "Debug", + "debug.subtitle": "Diagnósticos opcionais de transporte do provedor e de extração de uso. Erros de requisição e 502 permanecem na aba Logs.", + "debug.debug": "Debug do provedor", + "debug.usage": "Extração de uso", + "debug.injection": "Log de injeção", + "debug.claude": "Entrada do Claude", + "debug.claudeInbound.title": "Requisições de entrada do Claude", + "debug.claudeInbound.sub": "O que o Claude Code/Desktop realmente envia (thinking, esforço, metadados) — nenhum texto de prompt é armazenado.", + "debug.claudeInbound.empty": "Nenhuma requisição capturada ainda. Envie uma mensagem pelo Claude enquanto isto estiver ativado.", + "debug.claudeInbound.time": "Hora", + "debug.claudeInbound.endpoint": "Endpoint", + "debug.claudeInbound.model": "Modelo", + "debug.claudeInbound.none": "nenhum", + "debug.reset": "Limpar substituições em tempo de execução", + "debug.refresh": "Atualizar", + "debug.follow": "Acompanhar", + "debug.streamProvider": "Provedor", + "debug.streamUsage": "Uso", + "debug.streamInjection": "Injeção", + "debug.loading": "Carregando configurações de debug…", + "debug.loadFailed": "Não foi possível carregar as configurações de debug.", + "debug.emptyTitle": "O log de debug está desativado", + "debug.empty": "Ative Debug do provedor ou Extração de uso no cartão acima. As linhas aparecem aqui depois que você enviar uma requisição pelo proxy.", + "debug.noLinesTitle": "Aguardando linhas", + "debug.noLines.provider": "O debug do provedor está ativado, mas registra apenas anomalias de transporte (frames descartados ou malformados e eventos de conexão/nova tentativa do Cursor). Uma requisição normal por um provedor como a Anthropic pode não gerar nenhuma linha.", + "debug.noLines.usage": "A extração de uso está ativada, mas nada foi capturado ainda. Envie um chat/requisição pelo Codex e ele aparecerá aqui.", + "debug.noLines.injection": "O log de injeção está ativado, mas nada foi capturado ainda. Ele registra a injeção de orientação multiagente e as decisões de limite de esforço em turnos de colaboração e de subagentes.", + "usage.title": "Uso", + "usage.subtitle": "Contabilização local de tokens do seu proxy. O uso ausente nunca é exibido como zero.", + "usage.loading": "Carregando dados de uso…", + "usage.empty": "Nenhum uso registrado ainda. Envie uma requisição pelo proxy para ver a atividade aqui.", + "usage.loadError": "Não foi possível carregar os dados de uso.", + "usage.range.all": "Tudo", + "usage.range.available": "Histórico disponível", + "usage.historyTruncated": "Os totais cobrem apenas o histórico disponível porque o uso mais antigo não foi carregado.", + "usage.historyTruncatedWindow": "As linhas carregadas têm horários de início de requisição entre {start} e {end}. Entradas anteriores do arquivo foram omitidas pelo limite de leitura, então qualquer período selecionado pode estar incompleto.", + "usage.range.30d": "30d", + "usage.range.7d": "7d", + "usage.card.requests": "Requisições", + "usage.card.measured": "Medidas", + "usage.card.reported": "Informadas", + "usage.card.totalTokens": "Total de tokens", + "usage.card.cachedTokens": "Leituras de cache", + "usage.card.cachedTokensHint": "Tokens do prompt atendidos pelo cache do provedor (leituras). As gravações de cache aparecem abaixo quando houver.", + "usage.card.cacheWriteTokens": "gravações de cache", + "usage.card.coverage": "Cobertura", + "usage.card.activeDays": "Dias ativos", + "usage.section.heatmap": "Atividade diária", + "usage.section.overview": "Visão geral", + "usage.section.models": "Modelos", + "usage.section.providers": "Provedores", + "usage.section.coverage": "Detalhamento da cobertura", + "usage.section.companion": "Barra de menus e widget", + "usage.companion.title": "Barra de menus e widget", + "usage.companion.description": "As configurações aqui controlam o app desktop/de barra de menus do OpenCodex e seu widget.", + "usage.companion.installGuide": "Guia de instalação", + "usage.companion.loading": "Carregando linha do tempo…", + "usage.companion.timelineUnavailable": "Linha do tempo indisponível", + "usage.companion.empty": "Nenhum uso nas últimas {hours} h", + "usage.companion.chartLabel": "Linha do tempo de uso", + "usage.companion.olderRecordsSkipped": "Registros mais antigos foram ignorados", + "usage.companion.settingsUnavailable": "Configurações do companion indisponíveis", + "usage.companion.corrupt": "O arquivo de configurações do companion está corrompido. Os controles mostram os padrões; o salvamento fica pausado até você substituir o arquivo.", + "usage.companion.corruptReset": "Substituir pelos padrões", + "usage.companion.connected": "App da barra de menus conectado · {age}", + "usage.companion.connectedDesktop": "App desktop conectado · {age}", + "usage.companion.installTitle": "Instalar o app desktop", + "usage.companion.installOs": "Sistema operacional", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "Você está no app desktop do OpenCodex {version}", + "usage.companion.openInBrowser": "Abrir no navegador", + "usage.companion.installMacStep1": "Baixe OpenCodex--macos.dmg da versão mais recente e arraste OpenCodex.app para Aplicativos.", + "usage.companion.installMacStep2": "Primeira execução: clique com o botão direito em OpenCodex.app → Abrir (o Gatekeeper pergunta uma vez até o app ser notarizado).", + "usage.companion.installMacStep3": "O app encontra este proxy sozinho; o widget aparece na galeria de widgets depois que o app for executado.", + "usage.companion.installWinStep1": "Baixe OpenCodex--windows-x64.msi da versão mais recente e execute-o.", + "usage.companion.installWinStep2": "Se o SmartScreen avisar, escolha Mais informações → Executar assim mesmo (o instalador ainda não tem assinatura de código).", + "usage.companion.installWinStep3": "O OpenCodex aparece na bandeja do sistema e se conecta a este proxy, ou inicia o proxy embutido.", + "usage.companion.installLinuxStep1": "Baixe OpenCodex--linux-x86_64.AppImage (ou o .deb) da versão mais recente.", + "usage.companion.installLinuxStep2": "Use chmod +x no AppImage e execute-o; o ícone na bandeja exige um desktop compatível com AppIndicator.", + "usage.companion.installLinuxStep3": "O app se conecta a este proxy ou inicia o proxy embutido.", + "usage.companion.notConnected": "Nenhum app da barra de menus se conectou a este proxy ainda.", + "usage.companion.lastSeen": "Visto pela última vez {age}", + "usage.companion.installAnother": "Instalar em outro dispositivo", + "usage.companion.saved": "Salvo · {time}", + "usage.companion.saveFailed": "Não foi possível salvar: {error}", + "usage.companion.reset": "Restaurar padrões", + "usage.companion.footer": "O widget mostra as requisições, os tokens e o custo de hoje, além do gráfico configurado aqui, e é atualizado quando o app consulta o proxy.", + "usage.companion.menuBarShows": "A barra de menus mostra", + "usage.companion.menuRequests": "Requisições", + "usage.companion.menuTokens": "Tokens", + "usage.companion.menuCost": "Custo", + "usage.companion.menuQuota": "Cota", + "usage.companion.menuNone": "Somente ícone", + "usage.companion.window": "Janela", + "usage.companion.window6": "6h", + "usage.companion.window24": "24h", + "usage.companion.window72": "3d", + "usage.companion.window168": "7d", + "usage.companion.style": "Estilo", + "usage.companion.styleLine": "Linha", + "usage.companion.styleStacked": "Empilhado", + "usage.companion.metric": "Métrica", + "usage.companion.metricTotal": "Total", + "usage.companion.metricInput": "Entrada", + "usage.companion.metricOutput": "Saída", + "usage.companion.metricCached": "Em cache", + "usage.companion.groupBy": "Agrupar por", + "usage.companion.groupModel": "Modelo", + "usage.companion.groupAccount": "Modelo + conta", + "usage.companion.popoverSections": "Seções do popover", + "usage.companion.sectionToday": "Hoje", + "usage.companion.sectionChart": "Gráfico", + "usage.companion.sectionModels": "Modelos", + "usage.companion.sectionCost": "Custo", + "usage.companion.sectionAccounts": "Contas", + "usage.companion.advanced": "Avançado", + "usage.companion.aggregation": "Agregação", + "usage.companion.aggregationSum": "Soma", + "usage.companion.aggregationAverage": "Média", + "usage.companion.aggregationMax": "Máx.", + "usage.companion.menuText": "Texto da barra de menus", + "usage.companion.placeholders": "Marcadores:", + "usage.companion.modelsOnChart": "Modelos no gráfico", + "usage.companion.modelsCount": "{selected} de {total} no gráfico", + "usage.companion.modelsShowAll": "Mostrar todos", + "usage.companion.hideProviders": "Ocultar provedores", + "usage.workspace.report": "Relatório de uso", + "usage.workspace.sections": "Seções de uso", + "usage.coverage.measured": "Medido", + "usage.coverage.reported": "Informado pelo provedor", + "usage.coverage.estimated": "Estimado", + "usage.coverage.note": "As entradas medidas incluem contagens de tokens informadas pelo provedor e estimadas. Requisições sem informação ou sem suporte são registradas, mas nunca contabilizadas como zero token.", + "usage.search.models": "Buscar modelos…", + "usage.col.requests": "Requisições", + "usage.col.measured": "Medido", + "usage.col.reported": "Informado", + "usage.col.inputTokens": "Tokens de entrada", + "usage.col.outputTokens": "Tokens de saída", + "usage.col.cacheHits": "Acertos de cache", + "usage.col.cacheWrites": "Gravações em cache", + "usage.col.cacheHitRate": "Taxa de acerto", + "usage.cacheHitRate.partial": "Média sobre {measured} de {total} tokens de entrada; as demais requisições não informaram detalhes de cache.", + "usage.cacheHitRate.unmeasured": "Nenhuma requisição desta linha informou detalhes de cache, então não há taxa de acerto para calcular a média.", + "usage.unavailable": "—", + "usage.col.tokens": "Tokens", + "usage.col.apiListPrice": "Preço de tabela da API", + "usage.col.share": "Participação", + "usage.heatmap.less": "Menos", + "usage.heatmap.more": "Mais", + "usage.dayMon": "Seg", + "usage.dayWed": "Qua", + "usage.dayFri": "Sex", + "usage.heatmap.tooltipTokens": "{tokens} tokens", + "usage.chart.dayDetail": "{date}: {requests} requisições, {tokens} tokens", + "usage.heatmap.keyboardLabel": "Use Cima e Baixo para mover por dia; Esquerda e Direita para mover por semana.", + "usage.heatmap.tooltipRequests": "{requests} requisições", + "nav.storage": "Armazenamento", + "storage.title": "Armazenamento", + "storage.subtitle": "Veja o que está ocupando o CODEX_HOME. A limpeza nunca afeta sessões ativas.", + "storage.loading": "Analisando o armazenamento…", + "storage.empty": "CODEX_HOME está vazio ou ausente — nada a relatar.", + "storage.error": "Falha na análise do armazenamento. Verifique se CODEX_HOME aponta para um diretório válido.", + "storage.refresh": "Reanalisar", + "storage.rescanned": "Análise concluída.", + "storage.card.total": "Tamanho total", + "storage.card.files": "Arquivos", + "storage.card.home": "CODEX_HOME", + "storage.snapshot.lastScan": "Última análise", + "storage.snapshot.scanning": "Analisando…", + "storage.snapshot.unavailable": "Nenhuma análise ainda.", + "storage.cleanupCard.title": "Liberar espaço", + "storage.cleanupCard.tabs": "Opções de limpeza", + "storage.cleanupCard.tab.policy": "Política", + "storage.cleanupCard.tab.quarantine": "Quarentena", + "storage.cleanup.noArchives": "Nenhuma sessão arquivada para limpar.", + "storage.section.buckets": "Categorias", + "storage.section.largest": "Maiores arquivos", + "storage.workspace.overview": "Visão geral", + "storage.workspace.selectBucket": "Selecione uma categoria na lista para ver o detalhamento.", + "storage.col.bucket": "Categoria", + "storage.col.size": "Tamanho", + "storage.col.files": "Arquivos", + "storage.col.oldest": "Mais antigo", + "storage.col.newest": "Mais recente", + "storage.col.rows": "Linhas do BD", + "storage.rows.unknown": "desconhecido (bloqueado)", + "storage.bucket.sessions": "Sessões ativas", + "storage.bucket.archived_sessions": "Sessões arquivadas", + "storage.bucket.logs_db": "Banco de logs", + "storage.bucket.state_db": "Banco de estado", + "storage.bucket.attachments": "Anexos", + "storage.bucket.deletion_manifests": "Manifestos de exclusão", + "storage.bucket.other": "Outros", + "storage.cleanup.title": "Limpeza de arquivados", + "storage.cleanup.help": "Remove as sessões arquivadas mais antigas por porcentagem. Sessões ativas nunca são afetadas. A quarentena é o padrão — os arquivos são movidos para CODEX_HOME/.trash.", + "storage.cleanup.slider": "Porcentagem dos arquivados mais antigos", + "storage.cleanup.percent": "{percent}%", + "storage.cleanup.preset": "{percent}", + "storage.cleanup.preview": "Pré-visualizar", + "storage.cleanup.confirmTitle": "Confirmar limpeza de arquivados", + "storage.cleanup.confirmBody": "Isto processará {count} arquivo(s) arquivado(s) (~{size}), os {percent}% mais antigos.", + "storage.cleanup.moreFiles": "…e mais {n}", + "storage.cleanup.permanent": "Excluir permanentemente (ignorar quarentena)", + "storage.cleanup.permanentWarn": "A exclusão permanente não pode ser desfeita.", + "storage.cleanup.quarantineNote": "Os arquivos são movidos para .trash em CODEX_HOME. Você pode restaurá-los na aba Quarentena.", + "storage.cleanup.cancel": "Cancelar", + "storage.cleanup.confirmQuarantine": "Colocar em quarentena", + "storage.cleanup.confirmPermanent": "Excluir permanentemente", + "storage.cleanup.doneQuarantine": "{count} arquivo(s) em quarentena ({size}).", + "storage.cleanup.donePermanent": "{count} arquivo(s) excluído(s) permanentemente ({size}).", + "storage.cleanup.skippedReferenced": "{count} arquivo(s) referenciado(s) ignorado(s).", + "storage.cleanup.previewFailed": "Falha na pré-visualização.", + "storage.cleanup.cleanupFailed": "Falha na limpeza.", + "storage.cleanup.err.codex_busy": "O Codex está usando o state.sqlite — tente novamente após encerrar o Codex.", + "storage.cleanup.err.stale_preview": "Os arquivos arquivados mudaram desde a pré-visualização — execute a pré-visualização novamente.", + "storage.cleanup.err.restore_pending_overlap": "Os arquivos selecionados se sobrepõem a uma restauração incompleta da lixeira — conclua ou tente novamente a restauração primeiro.", + "storage.cleanup.err.referenced_history": "Os arquivos selecionados ainda são referenciados por históricos bifurcados ou paginados.", + "storage.cleanup.err.invalid_digest": "O digest da pré-visualização está ausente ou é inválido.", + "storage.cleanup.err.invalid_mode": "O modo de limpeza deve ser quarentena ou permanente.", + "storage.cleanup.err.fs_failed": "Falha na limpeza do sistema de arquivos. Algumas alterações podem já ter sido aplicadas — verifique CODEX_HOME/.trash e qualquer caminho de recuperação exibido.", + "storage.cleanup.err.fs_failed_trash": "Falha na limpeza do sistema de arquivos. Algumas alterações podem já ter sido aplicadas — verifique {trashDir} e manifest.json para arquivos recuperáveis.", + "storage.cleanup.err.db_reconcile_failed": "Não foi possível atualizar o banco de estado do Codex.", + "storage.cleanup.err.cleanup_failed": "Falha na limpeza.", + "storage.trash.title": "Quarentena", + "storage.trash.help": "Sessões arquivadas movidas para CODEX_HOME/.trash. A restauração devolve os arquivos JSONL e as linhas de thread.", + "storage.trash.empty": "Nenhuma entrada em quarentena.", + "storage.trash.loading": "Carregando quarentena…", + "storage.trash.col.when": "Em quarentena desde", + "storage.trash.col.files": "Arquivos", + "storage.trash.col.size": "Tamanho", + "storage.trash.col.mode": "Modo", + "storage.trash.col.id": "Entrada", + "storage.trash.restore": "Restaurar", + "storage.trash.confirmTitle": "Restaurar entrada da quarentena?", + "storage.trash.confirmBody": "Restaurar {count} arquivo(s) (~{size}) de {id} para as sessões arquivadas.", + "storage.trash.cancel": "Cancelar", + "storage.trash.confirmRestore": "Restaurar", + "storage.trash.done": "{count} arquivo(s) restaurado(s) ({size}).", + "storage.trash.restoreFailed": "Falha na restauração.", + "storage.trash.listFailed": "Não foi possível listar as entradas da quarentena.", + "storage.trash.mode.quarantine": "quarentena", + "storage.trash.mode.permanent": "permanente (incompleto)", + "storage.trash.err.codex_busy": "O Codex está usando o state.sqlite — tente novamente após encerrar o Codex.", + "storage.trash.err.invalid_trash": "O ID da entrada da lixeira está ausente ou é inválido.", + "storage.trash.err.missing_trash": "A entrada da lixeira não foi encontrada.", + "storage.trash.err.dest_exists": "O destino da restauração já existe — remova ou renomeie o arquivo arquivado e tente novamente.", + "storage.trash.err.fs_failed": "Falha na restauração do sistema de arquivos. Alguns arquivos podem já ter sido restaurados — verifique archived_sessions e .trash.", + "storage.trash.err.db_reconcile_failed": "Não foi possível restaurar as linhas do banco de estado do Codex.", + "storage.trash.err.storage_mutation_busy": "Outra limpeza ou restauração de armazenamento está em andamento — tente novamente em instantes.", + "storage.trash.err.restore_failed": "Falha na restauração.", + "storage.trash.err.restore_worker_timeout": "A restauração demorou demais (mais de 10 minutos) e foi interrompida.", + "storage.trash.err.restore_worker_aborted": "A restauração foi cancelada durante o encerramento.", + "storage.trash.err.restore_worker_failed": "O worker de restauração travou ou falhou inesperadamente.", + "storage.policy.title": "Política de limpeza automática", + "storage.policy.help": "Limpeza em lote opcional quando as sessões arquivadas excedem um limite. Desativada por padrão — nunca é ativada automaticamente.", + "storage.policy.loading": "Carregando política…", + "storage.policy.loadFailed": "Não foi possível carregar a política de limpeza.", + "storage.policy.saveFailed": "Não foi possível salvar a política de limpeza.", + "storage.policy.runFailed": "Falha na execução da política.", + "storage.policy.alreadyRunning": "Já há uma execução da política de limpeza em andamento.", + "storage.policy.invalid": "Valores de política inválidos.", + "storage.policy.enabled": "Ativar limpeza automática", + "storage.policy.enabledHint": "O padrão é desativada. Quando ativada, executa apenas no agendamento escolhido (ou em Executar agora).", + "storage.policy.threshold": "Quando o tamanho dos arquivados exceder (GiB)", + "storage.policy.trigger": "Gatilho", + "storage.policy.target": "Alvo da limpeza", + "storage.policy.targetPercent": "Remover os arquivados mais antigos (%)", + "storage.policy.targetReduce": "Reduzir o tamanho dos arquivados para (GiB)", + "storage.policy.thresholdInc": "Aumentar limite", + "storage.policy.thresholdDec": "Diminuir limite", + "storage.policy.percentInc": "Aumentar porcentagem", + "storage.policy.percentDec": "Diminuir porcentagem", + "storage.policy.reduceInc": "Aumentar tamanho alvo", + "storage.policy.reduceDec": "Diminuir tamanho alvo", + "storage.policy.schedule": "Agendamento", + "storage.policy.schedule.manual": "Somente manual", + "storage.policy.schedule.startup": "Ao iniciar o proxy", + "storage.policy.schedule.daily": "Diário", + "storage.policy.schedule.weekly": "Semanal", + "storage.policy.mode": "Modo de exclusão", + "storage.policy.mode.quarantine": "Quarentena (padrão)", + "storage.policy.mode.permanent": "Exclusão permanente", + "storage.policy.permanentWarn": "O modo permanente não pode ser desfeito. Prefira a quarentena, a menos que tenha certeza.", + "storage.policy.lastRun": "Última execução", + "storage.policy.lastRunDetail": "Removidos {count} · liberados {size}", + "storage.policy.nextRun": "Próxima execução", + "storage.policy.never": "Nunca", + "storage.policy.save": "Salvar", + "storage.policy.runNow": "Executar agora", + "storage.policy.running": "Executando…", + "storage.policy.saved": "Política salva.", + "storage.policy.skippedDisabled": "A política está desativada — ative-a primeiro.", + "storage.policy.skippedUnder": "O tamanho dos arquivados está abaixo do limite — nada a fazer.", + "storage.policy.skippedEmpty": "Nenhum candidato arquivado correspondeu ao alvo.", + "storage.policy.doneQuarantine": "A política colocou {count} arquivo(s) em quarentena ({size}).", + "storage.policy.donePermanent": "A política excluiu permanentemente {count} arquivo(s) ({size}).", + "storage.policy.metadataSaveWarning": "A execução da política terminou, mas seus metadados de agendamento não puderam ser salvos.", + "modal.addNamed": "Adicionar: {label}", + "modal.add": "Adicionar provedor", + "modal.search": "Buscar provedores…", + "modal.logInWith": "Entrar com {label}", + "modal.waitingBrowser": "Aguardando o navegador…", + "modal.providerName": "Nome do provedor", + "modal.adapter": "Adaptador", + "modal.baseUrl": "URL base", + "modal.endpoint": "Endpoint", + "modal.endpoint.tokenPlan": "Plano de tokens", + "modal.endpoint.payAsYouGo": "Pague conforme o uso", + "modal.endpoint.custom": "Personalizado", + "modal.defaultModel": "Modelo padrão (opcional)", + "modal.allowPrivateNetwork": "Permitir rede local/privada", + "modal.allowPrivateNetworkHint": "Ative apenas para provedores hospedados por você de propósito. Os endpoints de metadados continuam bloqueados.", + "modal.nameRequired": "O nome do provedor é obrigatório", + "modal.baseUrlRequired": "A URL base é obrigatória", + "modal.networkError": "Erro de rede — o proxy está em execução?", + "modal.loginFailStart": "Falha ao iniciar o login", + "modal.waitingLogin": "Aguardando login no navegador…", + "modal.loggingIn": "Entrando…", + "modal.loginTimeout": "Tempo de login esgotado — tente novamente.", + "modal.back": "Voltar", + "modal.badge.oauth": "OAuth", + "modal.customProvider": "Provedor personalizado", + "modal.failedStatus": "Falhou ({status})", + "modal.loginError": "Erro de login: {error}", + "modal.badge.codexLogin": "Login do Codex", + "modal.badge.local": "Local", + "modal.badge.apiKey": "Chave de API", + "modal.badge.direct": "Direto", + "modal.badge.pool": "Pool", + "modal.badge.free": "Grátis", + "modal.badge.sponsor": "Patrocinador", + "modal.badge.subscriptionCli": "CLI por assinatura", + "pws.sponsor.orcaTitle": "Um modelo para cada prompt", + "pws.sponsor.orcaDescription": "Um gateway compatível com OpenAI com roteamento adaptativo e failover automático.", + "pws.sponsor.packyTitle": "Claude Code, Codex e Gemini em um só lugar", + "pws.sponsor.packyDescription": "Um relay de API para suas ferramentas de programação com IA. Comece com um token do grupo Codex.", + "pws.sponsor.tokenlabTitle": "Uma chave de API para os principais modelos", + "pws.sponsor.tokenlabDescription": "Responses, Chat Completions, streaming e chamada de ferramentas para agentes de programação, com modo de entrega à sua escolha e cobrança conforme o uso.", + "pws.sponsor.visit": "Conhecer {provider}", + "pws.sponsor.console": "Abrir console", + "modal.invalidPreset": "Este preset de provedor integrado está incompleto. Reinicie o proxy e tente novamente.", + "modal.freeTierTitle": "Plano gratuito", + "modal.subscriptionCliTitle": "CLI por assinatura, não chave de API", + "modal.subscriptionCliWarning": "Este provedor não usa chave de API. Ele aciona a CLI oficial do Claude Code autenticada nesta máquina ({cmd}) e cobra da conta de assinatura do Claude. O OpenCodex não armazena nenhuma chave para ele, e uma chave informada aqui nunca é usada. Para cobrança por chave de API, adicione {apiProvider}.", + "modal.freeTierDefault": "Não requer chave de API. Funciona de imediato.", + "modal.tab.accounts": "Contas", + "modal.tab.free": "Grátis", + "modal.tab.local": "Local", + "modal.tab.paid": "Pago", + "modal.noteMore": "Mostrar descrição completa", + "modal.searchResults": "{count} resultados em {tiers}", + "modal.accountsHint": "Entre aqui no ChatGPT/Codex, em provedores OAuth e em contas com chave de API. A OpenAI já está integrada — faça login em vez de adicioná-la novamente.", + "modal.accountsCodexAuthLink": "Autenticação do Codex", + "modal.notListed": "Provedor não listado? Adicione um personalizado", + "modal.catalogLoading": "Carregando catálogo…", + "modal.accountLogin": "Entrar", + "modal.accountLogout": "Sair", + "modal.accountAdd": "Adicionar conta", + "modal.accountManage": "Gerenciar", + "modal.accountCodexPool": "Pool de contas do ChatGPT", + "modal.accountLoggedIn": "Conectado", + "modal.accountLoggedOut": "Não conectado", + "quota.fiveHourLimit": "Limite de 5 horas", + "quota.ageMinutes": "{n}min", + "quota.ageHours": "{n}h", + "quota.ageDays": "{n}d", + "quota.observedAgo": "Observado há {age}", + "quota.observedHint": "A Meta informa o uso apenas durante uma resposta em streaming, então este é o último valor visto, não uma leitura em tempo real.", + "quota.weeklyLimit": "Limite semanal", + "quota.monthlyLimit": "Limite de 30 dias", + "quota.cursorFirstParty": "Modelos próprios", + "quota.cursorApiUsage": "Uso da API", + "quota.totalSubscriptionCredits": "Total de créditos da assinatura", + "quota.creditsBalance": "Saldo de créditos", + "quota.creditsPeriodEnds": "O período de cobrança termina em {date}", + "quota.usedPercent": "{pct}% usado", + "quota.limitReached": "Limite atingido", + "quota.resetsToday": "Redefine hoje às {time}", + "quota.resetsTomorrow": "Redefine amanhã às {time}", + "quota.resetsAt": "Redefine {when}", + "quota.resetsRelativeMinutes": "Redefine em {n} min", + "quota.resetsRelativeHours": "Redefine em {n} h", + "pws.status.ready": "Pronto", + "pws.status.needsSetup": "Requer configuração", + "pws.status.needsAttention": "Requer atenção", + "pws.auth.chatgptPassthrough": "Passthrough do ChatGPT", + "pws.auth.noKey": "Sem chave necessária", + "pws.freeTitle": "Preço gratuito (uma chave ainda pode ser necessária)", + "pws.localTitle": "Runtime local", + "pws.modelCountOne": "1 modelo", + "pws.modelCount": "{count} modelos", + "pws.rail.suffixDefault": " · padrão", + "pws.rail.suffixLocal": " · local", + "pws.rail.suffixFree": " · grátis", + "pws.rail.selectAria": "Selecionar {name} — {status}{suffix}", + "pws.searchPlaceholder": "Buscar provedores…", + "pws.filterAria": "Filtrar provedores", + "pws.providerFiltersAria": "Filtros de provedores", + "pws.filters": "Filtros", + "pws.filterStatus": "Status", + "pws.pricing": "Preço", + "pws.paid": "Pago", + "pws.filterType": "Tipo", + "pws.type.cloud": "Nuvem", + "pws.type.local": "Local", + "pws.type.selfHosted": "Autohospedado", + "pws.type.login": "Login", + "pws.sort": "Ordenar", + "pws.sortProvidersAria": "Ordenar provedores", + "pws.sort.az": "A–Z", + "pws.sort.za": "Z–A", + "pws.sort.freePaid": "Grátis primeiro", + "pws.sort.paidFree": "Pagos primeiro", + "pws.sort.accountsFirst": "Contas primeiro", + "pws.resetAll": "Limpar tudo", + "pws.providerList": "Lista de provedores", + "pws.providersAria": "Provedores", + "pws.groupReady": "Prontos ({count})", + "pws.groupNeedsSetup": "Requerem configuração ({count})", + "pws.groupDisabled": "Desativados ({count})", + "pws.noSearchResults": "Nenhum provedor corresponde à busca.", + "pws.noMatchFilters": "Nenhum provedor corresponde aos filtros.", + "pws.noProvidersConfigured": "Nenhum provedor configurado.", + "pws.workspaceMainAria": "Detalhes do provedor", + "pws.detailComingSoon": "Visão detalhada em breve — use a visão clássica para gerenciar este provedor.", + "pws.selectPrompt": "Selecione um provedor na lista.", + "pws.connectFirst": "Conecte seu primeiro provedor", + "pws.empty.browseFree": "Explorar provedores gratuitos", + "pws.empty.browseFreeDesc": "Comece sem assinatura", + "pws.empty.connectAccount": "Conectar uma conta", + "pws.empty.connectAccountDesc": "Use seu login do ChatGPT ou do provedor", + "pws.empty.addEndpoint": "Adicionar um endpoint", + "pws.empty.addEndpointDesc": "URL base e chave de API personalizadas", + "pws.tab.overview": "Visão geral", + "pws.tab.models": "Modelos", + "pws.tab.usage": "Uso", + "pws.tab.accounts": "Contas", + "pws.tab.settings": "Configurações", + "pws.connection": "Conexão", + "pws.status.connected": "Conectado", + "pws.attentionTitle": "Requer atenção", + "pws.attention.reauth": "A conta ativa precisa ser autenticada novamente", + "pws.attention.reauthForward": "A conta ativa do Codex precisa ser autenticada novamente — abra Contas para corrigir", + "pws.attention.missingCredentials": "Credenciais ausentes", + "pws.cell.auth": "Autenticação", + "pws.cell.note": "Observação", + "pws.cell.defaultModel": "Modelo padrão", + "pws.statsAria": "Estatísticas do provedor", + "pws.statsTitle": "Estatísticas", + "pws.stats.totalRequests": "Requisições (30d)", + "pws.stats.totalTokens": "Tokens (30d)", + "pws.stats.quotaUpdated": "Cota atualizada", + "pws.stats.quotaTracked": "Limites de taxa acompanhados na aba Uso.", + "pws.stats.source": "Fonte", + "pws.usageLast30d": "Uso (últimos 30 dias)", + "pws.estimatedCost": "Custo estimado", + "pws.costDisclaimer": "Estimativa pelo preço de tabela da API, não uma cobrança real.", + "pws.unresolvedRequestedModel": "Inclui uso de modelo solicitado não resolvido", + "pws.currentAccountUsage": "Uso da conta atual", + "pws.quotaUnsupported": "A consulta de cota não é compatível com esta conta.", + "pws.quotaUnobserved": "Nenhuma observação de uso ainda.", + "pws.quotaCheckCompleted": "Verificação de cota concluída", + "pws.modelBreakdown": "Detalhamento por modelo", + "pws.col.model": "Modelo", + "pws.col.cost": "Custo est.", + "pws.col.tokens": "Tokens", + "pws.col.requests": "Req.", + "pws.col.share": "Part.", + "pws.tokenInput": "Entrada", + "pws.tokenOutput": "Saída", + "pws.metricRequests": "requisições", + "pws.metricTokens": "tokens", + "pws.usageUnavailable": "Nenhum uso registrado ainda.", + "pws.rateLimits": "Limites de taxa", + "pws.quotaUnavailable": "Sem dados de cota para este provedor.", + "pws.accountQuotaUnavailable": "Dados de limite de taxa temporariamente indisponíveis; exibindo os últimos valores conhecidos, quando houver.", + "pws.quotaFailure.account_unavailable": "Os detalhes da conta estão indisponíveis para esta verificação de cota.", + "pws.quotaFailure.access_denied": "O provedor negou acesso aos dados de cota.", + "pws.quotaFailure.rate_limited": "O provedor limitou a taxa da verificação de cota.", + "pws.quotaFailure.upstream_error": "O provedor não conseguiu concluir a verificação de cota.", + "pws.quotaFailure.redirect_blocked": "O endpoint de cota retornou um redirecionamento bloqueado.", + "pws.quotaFailure.destination_blocked": "O destino da cota foi bloqueado pela política de rede.", + "pws.quotaFailure.dns_failed": "Não foi possível resolver o hostname da cota.", + "pws.quotaFailure.timeout": "A requisição de cota expirou.", + "pws.quotaFailure.transport_error": "A conexão de cota falhou.", + "pws.quotaFailure.response_unusable": "O provedor retornou dados de cota inutilizáveis.", + "pws.selected": "Selecionado", + "pws.copyModelId": "Copiar ID", + "pws.modelCopied": "Copiado!", + "pws.modelsAvailable": "{count} disponíveis", + "pws.modelSearchPlaceholder": "Filtrar modelos…", + "pws.modelsLoading": "Carregando modelos…", + "pws.modelsLoadFailed": "Não foi possível carregar os modelos.", + "pws.modelsNeedsReauth": "A conta precisa fazer login novamente para a descoberta de modelos ao vivo funcionar. Exibindo os modelos configurados por enquanto.", + "pws.modelsConfiguredFallback": "Exibindo os modelos configurados (descoberta ao vivo indisponível).", + "pws.modelsTruncated": "Exibindo os primeiros {shown} de {total} modelos. Use o filtro para refinar a lista.", + "pws.retry": "Tentar novamente", + "pws.noModels": "Nenhum modelo descoberto para este provedor.", + "pws.noModelMatch": "Nenhum modelo corresponde ao filtro.", + "pws.adapterBaseRequired": "Adaptador e URL base são obrigatórios.", + "pws.addAccount": "Adicionar conta", + "pws.addKey": "Adicionar chave de API", + "pws.apiKeys": "Chaves de API", + "pws.authMode": "Modo de autenticação", + "pws.availableAccounts": "Contas disponíveis", + "pws.accountOrdinal": "Conta {count}", + "pws.accountsLoading": "Carregando contas…", + "pws.accountsLoadFailed": "Não foi possível carregar as contas.", + "pws.retryAccounts": "Tentar novamente", + "pws.noAccounts": "Nenhuma conta conectada ainda.", + "pws.cockpitImportDescription": "Importe uma exportação JSON do Antigravity do Cockpit Tools a partir deste dispositivo. O conteúdo do arquivo não é exibido.", + "pws.cockpitImportFileLabel": "Exportação JSON do Antigravity do Cockpit Tools", + "pws.cockpitImportChooseFile": "Escolher arquivo JSON", + "pws.cockpitImporting": "Importando…", + "pws.cockpitImportInvalid": "O arquivo selecionado não é uma exportação JSON válida ou é grande demais.", + "pws.cockpitImportFailed": "Não foi possível concluir a importação das contas.", + "pws.cockpitImportComplete": "Importação concluída: {imported} importadas, {updated} atualizadas, {failed} com falha, {unsupported} sem suporte.", + "pws.accountSwitching": "Alternando…", + "pws.accountCurrent": "Conta atual", + "pws.accountPausedHint": "Excluída da seleção automática, das novas tentativas, da recuperação de cooldown, da seleção manual e da renovação proativa de token até ser retomada.", + "pws.defaultModelNone": "Nenhum (usar o padrão do provedor)", + "pws.discardSettings": "Descartar", + "pws.jsonEditorDesc": "Edite a configuração JSON bruta do provedor. As alterações são salvas imediatamente.", + "pws.jsonEditorTitle": "Editor JSON — {name}", + "pws.jsonRestore": "Restaurar", + "pws.jsonSave": "Salvar", + "pws.loggedInTitle": "Conectado", + "pws.notLoggedInTitle": "Não conectado", + "pws.note": "Observação", + "pws.allowPrivateNetwork": "Permitir rede local/privada", + "pws.liveModels": "Descobrir modelos a partir do provedor", + "pws.liveModelsDesc": "Busca o catálogo de modelos ao vivo do provedor. Desative para usar apenas os modelos configurados/estáticos.", + "pws.xaiChatOptIn": "Usar Chat Completions para Grok 4.5 e 4.6", + "pws.xaiChatOptInDesc": "Desativado seleciona Responses. Requisições OAuth Responses o usam por padrão. Os demais modelos Grok e o comportamento por plano permanecem inalterados.", + "pws.xaiChatOptInMixed": "Apenas um modelo usa Chat.", + "pws.cursorTransport": "Transporte do Cursor", + "pws.cursorTransportHttp2": "HTTP/2 (padrão)", + "pws.cursorTransportHttp1": "HTTP/1.1 (compatibilidade com proxy)", + "pws.cursorTransportDesc": "Use HTTP/1.1 quando seu proxy não conseguir transportar de forma confiável o stream HTTP/2 do Cursor.", + "pws.optionalPlaceholder": "Opcional", + "pws.providerId": "ID do provedor", + "pws.reauth": "Requer nova autenticação", + "pws.reauthenticate": "Autenticar novamente", + "pws.copyDoctor": "Copiar ocx doctor", + "pws.doctorCopied": "Copiado", + "pws.doctorCopyUnavailable": "Área de transferência indisponível", + "pws.healthCooldownHint": "Aguarde o fim do cooldown. Não teste esta conta ainda.", + "pws.healthLabel.rateLimited": "Limite de requisições atingido", + "pws.healthLabel.quotaLimited": "Cota esgotada", + "pws.healthLabel.reauthRequired": "Reautenticação necessária", + "pws.healthLabel.refreshFailed": "Falha na renovação", + "pws.healthLabel.metadataMismatch": "Metadados divergentes", + "pws.healthLabel.validationPending": "Validação pendente", + "pws.healthSummary.validationPending": "{provider} {account}: registrada com a cota esgotada. Após a cota se recuperar, atualize as cotas para executar uma pequena requisição de validação. O roteamento permanece desativado até que ela tenha êxito.", + "pws.healthLabel.credentialConflict": "Conflito de credenciais", + "pws.healthSummary.rateLimited": "{provider} {account}: limite de requisições atingido até {until}. O roteamento desta conta fica pausado até lá.", + "pws.healthSummary.quotaLimited": "{provider} {account}: cota esgotada até {until}. O roteamento desta conta fica pausado até lá.", + "pws.healthSummary.reauthRequired": "{provider} {account}: reautenticação necessária.", + "pws.healthSummary.credentialConflict": "{provider} {account}: conflito de credenciais.", + "pws.healthSummary.metadataMismatch": "{provider} {account}: metadados divergentes.", + "pws.healthSummary.staleCredentials": "{provider} {account}: credenciais incompletas.", + "pws.removeConfirm": "Remover", + "pws.removeConfirmBody": "Remover o provedor \"{name}\"? Esta ação não pode ser desfeita.", + "pws.removeDefaultConfirmBody": "Remover o provedor padrão \"{name}\"? \"{defaultProvider}\" passará a ser o provedor padrão. Esta ação não pode ser desfeita.", + "pws.removeConfirmTitle": "Remover provedor", + "pws.saveSettings": "Salvar", + "pws.pacingTitle": "Ritmo de requisições", + "pws.pacingDesc": "Distribui uniformemente o início das requisições de saída deste provedor. Respostas em streaming podem se sobrepor.", + "pws.pacingEnabled": "Ativado", + "pws.pacingRpm": "Requisições por minuto", + "pws.pacingConcurrency": "Máximo de requisições simultâneas", + "pws.pacingRpmUnit": "RPM", + "pws.pacingCapUnit": "×", + "pws.pacingDelay": "Intervalo mínimo (ms)", + "pws.pacingSlowerWins": "Prevalece o limite mais lento do provedor. As substituições por modelo só podem acrescentar mais atraso ou reduzir o limite de simultaneidade.", + "pws.pacingQueued": "na fila", + "pws.pacingNextSlot": "até o próximo slot", + "pws.pacingLastModel": "último modelo", + "pws.pacingNone": "Nenhum", + "pws.pacingModelOverrides": "Substituições por modelo", + "pws.pacingModel": "Modelo", + "pws.pacingAdd": "Adicionar substituição", + "pws.pacingRemove": "Remover", + "pws.pacingRemoveModel": "Remover a substituição de ritmo de requisições de {model}", + "pws.pacingRuleRequired": "Ative o ritmo de requisições somente após definir um limite do provedor ou uma substituição por modelo.", + "pws.pacingCapInvalid": "O máximo de requisições simultâneas deve ser um número inteiro igual ou maior que 1.", + "pws.saving": "Salvando…", + "pws.settingsSaved": "Configurações salvas.", + "pws.accountModeSaved": "Modo de conta salvo.", + "pws.accountModeFailed": "Não foi possível alternar o modo de conta.", + "pws.accountModeConfirm": "Alternar o modo de conta da OpenAI? As conversas em andamento serão reatribuídas ao conjunto de contas do outro modo, e o uso de cota passará a ser contabilizado no novo modo.", + "pws.settingsUnsavedBar": "Você tem alterações não salvas.", + "pws.unsavedLeaveBody": "Você tem alterações não salvas. Salvar antes de sair?", + "pws.unsavedLeaveTitle": "Alterações não salvas", + "pws.attentionRequired": "Requer atenção", + "pws.attentionAria": "{name}: {reason}", + "pws.missingCredentials": "Credenciais ausentes", + "pws.editJsonDesc": "Edite a configuração bruta do proxy em JSON", + "pws.updatesUnavailable": "As atualizações de provedores não estão disponíveis.", + "pws.dashboard.title": "Visão geral dos provedores", + "pws.dashboard.subtitle": "Gerencie todos os seus provedores de modelos em um só lugar.", + "pws.dashboard.rateLimits": "LIMITES DE REQUISIÇÕES", + "pws.capacity.estimate": "Estimativa do pool por pesos configurados", + "pws.capacity.currentAccount": "Conta efetiva atual", + "pws.capacity.nextRecovery": "Próxima recuperação de capacidade", + "pws.capacity.recoveryShare": "+{percent}% de capacidade do pool", + "pws.capacity.incomplete": "Cobertura incompleta: {excluded} conta(s) excluída(s)", + "pws.capacity.uncalibratedPlan": "{count} conta(s) em um plano não calibrado são contadas com o peso de assento de referência, portanto esta estimativa pode ser conservadora", + "pws.capacity.partial": "Cobertura parcial de janelas: {count} conta(s) não informam todas as janelas de limite exibidas", + "pws.capacity.windowPartial": "Parcial", + "pws.capacity.windowPartialA11y": "{window}: cobertura de contas incompleta", + "pws.dashboard.recentlyUsed": "USADOS RECENTEMENTE", + "pws.dashboard.requests": "{count} requisições", + "pws.dashboard.checkedAgo": "Verificado {time}", + "pws.dashboard.noQuota": "Sem dados de cota", + "pws.dashboard.noUsage": "Ainda sem dados de uso", + "pws.dashboard.noRateLimits": "Ainda sem dados de limites de requisições", + "pws.allProviders": "Visão geral dos provedores", + "pws.enabledLabel": "Ativado", + "pws.testConnection": "Testar conexão", + "pws.testing": "Testando…", + "pws.connectionOk": "Conexão OK", + "pws.connectionFailed": "Falha na conexão", + "pws.connectionNotApplicable": "Não se aplica — este provedor usa um catálogo de modelos estático.", + "pws.editSettings": "Editar configurações", + "pws.viewUsage": "Ver uso detalhado", + "pws.allSystemsOk": "Tudo funcionando normalmente", + "pws.apiKeyConfigured": "Chave de API configurada", + "pws.addApiKey": "Adicionar chave de API", + "pws.loggedInAs": "Conectado como {email}", + "pws.notLoggedIn": "Não conectado", + "pws.passthrough": "Passthrough do Codex", + "pws.notes": "OBSERVAÇÕES", + "pws.notePlaceholder": "Adicione uma observação sobre este provedor...", + "pws.noteSaved": "Observação salva", + "pws.authSummary": "AUTENTICAÇÃO", + "time.justNow": "Agora mesmo", + "time.notChecked": "Não verificado", + "time.minutesAgo": "há {n} min", + "time.hoursAgo": "há {n} h", + "time.daysAgo": "há {n} d", + "modal.noMatch": "Nenhuma correspondência.", + "modal.oauthDefaultNote": "Entre com sua conta — sem necessidade de chave de API.", + "modal.oauthComingSoon": "O login OAuth para {label} chega na próxima atualização. Por enquanto, use uma chave de API.", + "modal.oauthComingSoonShort": "O login OAuth para este provedor chega na próxima atualização — por enquanto, use uma chave de API.", + "modal.useApiKeyInstead": "Usar uma chave de API", + "modal.setupGuide": "Guia de configuração", + "modal.setupStep1Prefix": "Acesse o", + "modal.setupDashboardLink": "painel de {label}", + "modal.setupStep1Suffix": "e copie sua chave de API", + "modal.setupStep2": "Cole-a no campo de chave de API abaixo", + "modal.setupStep3": "Clique em Adicionar provedor — os modelos são descobertos automaticamente", + "modal.namePlaceholder": "ex.: openrouter", + "modal.duplicateWarn": "O provedor \"{name}\" já existe e será sobrescrito.", + "modal.forwardHintPrefix": "Não é necessária chave — o proxy encaminha suas credenciais de", + "modal.forwardCredentials": "codex login", + "modal.forwardHintSuffix": "para este provedor.", + "modal.localHint": "Nenhuma chave de API é armazenada. Isto adiciona o catálogo estático público de modelos do Cursor para o Codex, mas o transporte ao vivo do Cursor e a execução nativa de arquivos/shell permanecem desativados até serem auditados.", + "modal.getApiKey": "Obtenha sua chave de API de {label}", + "modal.apiKey": "Chave de API", + "modal.apiKeyTransport": "Cabeçalho da chave de API", + "modal.apiKeyTransportNative": "x-api-key (nativo da Anthropic)", + "modal.apiKeyTransportBearer": "Authorization: Bearer", + "modal.apiKeyPlaceholder": "sk-… (ou $ENV_VAR)", + "modal.defaultModelPlaceholder": "ex.: gpt-5.5", + "modal.baseUrlPlaceholder": "https://...", + "modal.baseUrlPlaceholderError": "A URL base contém um {placeholder} não resolvido. Substitua-o pelo valor real.", + "modal.baseUrlPlaceholderHint": "Substitua o {placeholder} na URL base pelo ID real da sua conta antes de adicionar.", + "modal.adding": "Adicionando…", + "modal.useOauthLogin": "← Usar login OAuth", + "nav.codexAuth": "Autenticação do Codex", + "nav.codexSet": "Config. do Codex", + "codexSet.tab.multiauth": "Multiautenticação", + "codexSet.tab.prompt": "Prompt", + "codexSet.prompt.title": "Camadas de prompt", + "codexSet.prompt.timing": "Aplica-se a sessões recém-iniciadas. As sessões em andamento mantêm suas configurações de prompt atuais.", + "codexSet.prompt.staleRevision": "A configuração foi alterada em outro lugar. A lista foi recarregada.", + "codexSet.prompt.writeFailed": "Não foi possível salvar a alteração.", + "codexSet.prompt.loadFailed": "Não foi possível carregar as camadas de prompt.", + "codexSet.prompt.repair": "Reparar", + "codexSet.prompt.repairFailed": "Não foi possível concluir o reparo.", + "codexSet.drift.journalPresent": "Uma gravação anterior não foi concluída. A recuperação é executada automaticamente na próxima gravação.", + "codexSet.drift.projectionStale": "As camadas salvas e o valor em config.toml divergem. O reparo reescreve o valor a partir das suas camadas.", + "codexSet.drift.storeMissing": "O arquivo de camadas sumiu, mas as instruções permanecem em config.toml. O reparo mantém o texto como uma única camada e grava um backup antes.", + "codexSet.drift.ownedMalformed": "A linha gerada em config.toml foi remodelada manualmente e, por isso, não é mais seguro reescrevê-la.", + "codexSet.custom.adoptUnsupported": "O valor em {path}, linha {line}, não é uma string de linha única e não pode ser importado. Mova-o manualmente para gerenciá-lo aqui.", + "codexSet.prompt.unreadable": "O arquivo de configuração do Codex existe, mas não pôde ser lido; por isso as alterações são recusadas.", + "codexSet.layer.permissions": "Permissões", + "codexSet.layer.collaboration": "Modo de colaboração", + "codexSet.layer.environment": "Contexto do ambiente", + "codexSet.layer.apps": "Apps", + "codexSet.layer.skills": "Skills", + "codexSet.prompt.extensionsUnknown": "As extensões podem adicionar suas próprias camadas. O Codex não as expõe, portanto não é possível listá-las aqui.", + "codexSet.group.transition": "Avisos de transição", + "codexSet.group.transitionDesc": "Estes anunciam uma mudança em vez de descrever um estado, por isso aparecem apenas quando a sessão entra em tempo real ou troca de modelo.", + "codexSet.custom.slotNote": "As camadas personalizadas são unidas em uma única seção, nesta ordem.", + "codexSet.row.alwaysOn": "Sempre ativa", + "codexSet.row.onChange": "Disparada em mudanças", + "codexSet.row.featureGated": "Configurada em [features]", + "codexSet.row.openFeatures": "Abrir configurações", + "codexSet.dialog.setValue": "{value} (padrão {fallback})", + "codexSet.dialog.copyKey": "Copiar chave", + "codexSet.dialog.unknownLayer": "Esta versão não tem descrição para esta camada. Ela vem de um runtime do Codex mais recente que o dashboard.", + "codexSet.custom.heading": "Camadas personalizadas", + "codexSet.custom.add": "+ Adicionar camada", + "codexSet.custom.newTitle": "Nova camada", + "codexSet.custom.editTitle": "Editar camada", + "codexSet.custom.titleLabel": "Título", + "codexSet.custom.bodyLabel": "Instruções", + "codexSet.custom.bodySize": "{bytes} de {max} bytes", + "codexSet.custom.normalized": "Tabulações viraram quatro espaços e as quebras de linha viraram LF.", + "codexSet.custom.titleRequired": "Informe um título.", + "codexSet.custom.titleTooLong": "O título tem {count} caracteres; o limite é {max}.", + "codexSet.custom.titleMultiline": "O título deve ter uma única linha.", + "codexSet.custom.bodyTooLarge": "Esta camada tem {bytes} bytes; o limite é {max}.", + "codexSet.custom.composedTooLarge": "Somadas, as camadas ativadas teriam {bytes} bytes, acima do limite.", + "codexSet.custom.invalidCharacter": "Um caractere de controle na posição {position} não pode ser salvo.", + "codexSet.custom.discardPrompt": "Descartar suas alterações?", + "codexSet.custom.discardOthersAndSave": "Descartar as edições não salvas das outras camadas e salvar esta camada?", + "codexSet.custom.keepEditing": "Continuar editando", + "codexSet.custom.delete": "Excluir {title}", + "codexSet.custom.deleteConfirm": "Excluir esta camada? Não há como desfazer.", + "codexSet.custom.layerGone": "Essa camada foi removida em outro lugar, então o editor foi fechado.", + "codexSet.custom.deleteConfirmNamed": "Excluir “{title}”? Não há como desfazer.", + "codexSet.custom.moveUp": "Mover {title} para cima", + "codexSet.custom.prevLayer": "Camada anterior", + "codexSet.custom.nextLayer": "Próxima camada", + "codexSet.custom.navPosition": "{position} / {total}", + "codexSet.custom.moveDown": "Mover {title} para baixo", + "codexSet.custom.limitReached": "Você pode manter até {max} camadas personalizadas.", + "codexSet.custom.notOwned": "developer_instructions foi gravado fora do opencodex e, por isso, não é editado aqui. Importe-o para gerenciá-lo como uma camada.", + "codexSet.custom.adopt": "Importar instruções existentes", + "codexSet.custom.adoptConfirm": "Importar como camada", + "codexSet.custom.adoptRefused": "Não foi possível importar o valor existente.", + "codexSet.custom.baseReplaced": "model_instructions_file está definido como {path}, ou seja, algo fora do opencodex substituiu o prompt base.", + "codexSet.lint.identity": "Isto declara uma identidade diferente da que o Codex estabelece.", + "codexSet.lint.foreignTool": "As ferramentas vêm do registro; nomeá-las aqui não as cria.", + "codexSet.lint.placeholder": "Nenhum mecanismo de template é executado sobre as instruções, então isto é enviado literalmente.", + "codexSet.lint.applyPatch": "apply_patch é definido pelo registro de ferramentas, não pelas instruções.", + "codexSet.lint.approvalVocab": "O Codex injeta seu próprio vocabulário de aprovação; isto pode contradizê-lo.", + "codexSet.lint.environment": "Os fatos do ambiente são gerados depois e podem contradizer isto.", + "codexSet.lint.size": "Esta camada tem mais de 8 KB. Ela ainda é salva, mas consome tokens a cada requisição.", + "codexSet.preset.blank": "Camada em branco", + "codexSet.preset.concise.name": "Saída concisa", + "codexSet.preset.concise.description": "Respostas curtas, sem preâmbulo, com formatação mínima.", + "codexSet.preset.concise.provenance": "Adaptado das diretrizes de concisão do Claude Code. Redação nossa, não uma cópia.", + "codexSet.preset.planFirst.name": "Planejar antes de editar", + "codexSet.preset.planFirst.description": "Apresente o plano e depois faça a alteração.", + "codexSet.preset.planFirst.provenance": "Adaptado da postura de planejamento do Claude Code. Redação nossa, não uma cópia.", + "codexSet.preset.explainWhy.name": "Explicar o raciocínio", + "codexSet.preset.explainWhy.description": "Diga o porquê, não apenas o quê.", + "codexSet.preset.explainWhy.provenance": "Adaptado do estilo de confirmação do Grok Build. Redação nossa, não uma cópia.", + "codexSet.preset.testFirst.name": "Testar primeiro", + "codexSet.preset.testFirst.description": "Escreva o teste que falha antes da correção.", + "codexSet.preset.testFirst.provenance": "Adaptado de práticas comuns de agentes. Redação nossa, não uma cópia.", + "codexSet.preset.korean.name": "Respostas em coreano", + "codexSet.preset.korean.description": "Responda em coreano, qualquer que seja o idioma da requisição.", + "codexSet.preset.korean.provenance": "Escrito para o opencodex a partir de um pedido recorrente de usuários. Redação nossa, não uma cópia.", + "codexSet.dialog.class": "Tipo", + "codexSet.dialog.key": "Chave de configuração", + "codexSet.dialog.fileValue": "Valor neste arquivo", + "codexSet.dialog.absentDefault": "não definido (padrão {value})", + "codexSet.dialog.noRenderedText": "O Codex não expõe o texto montado de uma camada nativa; por isso este diálogo descreve a camada e indica sua chave, em vez de mostrar seu conteúdo.", + "codexSet.dialog.sourceText": "Texto enviado ao modelo", + "codexSet.dialog.sourceBytes": "{bytes} bytes", + "codexSet.dialog.notRendered": "Esta camada não enviou nada no turno que lemos. As seções só são reenviadas quando mudam, então uma camada inalterada fica ausente de uma única amostra.", + "codexSet.dialog.emptySource": "O arquivo em {path} existe, mas está vazio, então esta camada não envia nada.", + "codexSet.dialog.notExposed": "O prompt base trafega fora da lista de mensagens que o Codex consegue imprimir, então não pode ser exibido aqui. É possível substituí-lo por meio de model_instructions_file.", + "codexSet.dialog.unmapped": "Esta camada não tem mapeamento confirmado na saída do prompt, então seu texto ainda não pode ser exibido aqui.", + "codexSet.dialog.textUnavailable": "Não foi possível ler o prompt do Codex nesta máquina, então o texto está indisponível.", + "codexSet.class.base": "Instruções base", + "codexSet.class.config-toggle": "Alternável aqui", + "codexSet.class.feature-gated": "Controlada por recurso", + "codexSet.class.runtime-conditional": "Condicional ao runtime", + "codexSet.class.extension-unknown": "Camada de extensão", + "codexSet.layer.base-instructions": "Instruções base", + "codexSet.layer.model-switch": "Aviso de troca de modelo", + "codexSet.layer.personality": "Personalidade", + "codexSet.layer.context-window-guidance": "Orientação sobre a janela de contexto", + "codexSet.layer.realtime": "Tempo real", + "codexSet.layer.agents-md": "AGENTS.md", + "codexSet.layer.environments-instructions": "Ambientes", + "codexSet.layer.plugins": "Plugins", + "codexSet.layer.tools": "Ferramentas", + "codexSet.layer.multi-agent-mode": "Modo multiagente", + "codexSet.layer.git-attribution": "Atribuição de commits", + "codexSet.about.base-instructions": "As instruções do próprio Codex. Elas viajam com a própria requisição e não podem ser desativadas.", + "codexSet.about.model-switch": "Adicionada quando a sessão troca de modelo no meio da conversa.", + "codexSet.about.personality": "Orientação de tom e voz, controlada por uma feature flag.", + "codexSet.about.context-window-guidance": "Orientação sobre o orçamento de contexto restante, controlada por uma feature flag.", + "codexSet.about.realtime": "Adicionada em sessões em tempo real.", + "codexSet.about.agents-md": "Os arquivos AGENTS.md do seu projeto. Esta página apenas informa a camada; nunca edita a documentação do seu projeto.", + "codexSet.about.permissions": "Explica as configurações de sandbox e de aprovação em vigor.", + "codexSet.about.collaboration": "Explica o modo de colaboração ativo.", + "codexSet.about.environment": "Diretório de trabalho, plataforma e outros fatos do ambiente.", + "codexSet.about.environments-instructions": "Orientação para ambientes de execução adiada, controlada por uma feature flag.", + "codexSet.about.apps": "Como usar os apps conectados.", + "codexSet.about.plugins": "Adicionada quando um plugin é selecionado ou quando qualquer plugin anuncia uma capacidade.", + "codexSet.about.tools": "Descrições de ferramentas adiadas, controladas por uma feature flag.", + "codexSet.about.skills": "A lista de skills disponíveis.", + "codexSet.about.multi-agent-mode": "Instruções de subagentes, controladas por uma feature flag.", + "codexSet.about.git-attribution": "Instrui o modelo a adicionar um trailer Co-authored-by: Codex aos commits que ele escreve e uma linha Generated with Codex. aos pull requests que ele abre. O Codex resolve isso a partir da sua conta, portanto não há configuração para isso aqui nem em [features]. Quando sua conta o desativa, o Codex envia a instrução oposta em vez de não enviar nada.", + "codexSet.condition.model-switch": "Emitida apenas após uma troca de modelo no meio da sessão.", + "codexSet.condition.realtime": "Emitida apenas em uma sessão em tempo real.", + "codexSet.condition.agents-md": "Emitida quando um documento de projeto é encontrado para o diretório de trabalho.", + "codexSet.condition.plugins": "Emitida quando um plugin é selecionado ou quando qualquer plugin anuncia uma capacidade.", + "codexSet.condition.git-attribution": "Definida pela política de atribuição da sua conta.", + "codexSet.base.title": "Prompt base", + "codexSet.base.prev": "Opção anterior", + "codexSet.base.next": "Próxima opção", + "codexSet.base.position": "{position} / {total}", + "codexSet.base.swipeHint": "Deslize para o lado, use as teclas de seta ou pressione as setas para alternar entre as opções. Aplica-se a sessões recém-iniciadas.", + "codexSet.base.defaultTitle": "Prompt base do próprio Codex", + "codexSet.base.defaultBody": "O padrão não é armazenado aqui, então não há nada para editar ou excluir: escolhê-lo simplesmente remove model_instructions_file da sua configuração, e o Codex usa o prompt que já acompanha o produto.", + "codexSet.base.variantTitle": "Nome", + "codexSet.base.variantBody": "Prompt", + "codexSet.base.replacesWarning": "Isto SUBSTITUI o prompt base do Codex em vez de acrescentar a ele. Um prompt curto aqui significa um modelo com instruções curtas.", + "codexSet.base.use": "Usar este", + "codexSet.base.inUse": "Em uso", + "codexSet.base.externalBlocked": "model_instructions_file já aponta para {path}, que não foi gravado pelo opencodex. Limpe-o você mesmo antes de escolher uma opção aqui.", + "nav.api": "API", + "nav.integrations": "Integrações", + "nav.openMenu": "Abrir menu", + "nav.closeMenu": "Fechar menu", + "nav.goHome": "Ir para o painel", + "nav.collapseSidebar": "Recolher barra lateral", + "nav.expandSidebar": "Expandir barra lateral", + "pws.refreshAllQuotas": "Atualizar todas as cotas", + "pws.quotaRefreshDone": "Verificação de cotas concluída", + "integrations.subtitle": "Conecte clientes ao opencodex, gerencie credenciais e restaure a configuração dos clientes.", + "integrations.tabsLabel": "Superfícies de integração", + "integrations.tab.overview": "Visão geral", + "integrations.tab.keys": "Chaves de API", + "integrations.tab.codex": "Codex", + "integrations.tab.claude": "Claude", + "integrations.tab.grok": "Grok Build", + "integrations.tab.cursor": "Cursor", + "integrations.tab.opencode": "OpenCode", + "integrations.tab.pi": "Pi", + "integrations.tab.omp": "OMP", + "integrations.tab.hermes": "Hermes", + "integrations.tab.openclaw": "OpenClaw", + "integrations.tab.kimi": "Kimi Code", + "integrations.tab.gajae": "gjc", + "integrations.tab.dsh": "DSH", + "integrations.tab.mcode": "MiniMax Code", + "integrations.tab.zcode": "ZCode", + "integrations.tab.prime": "Prime Agent", + "integrations.tab.aside": "Aside", + "integrations.tab.raycast": "Raycast", + "integrations.tab.omo": "omo", + "integrations.tab.cline": "Cline CLI", + "integrations.tab.kilo": "Kilo", + "integrations.tab.droid": "Factory Droid", + "integrations.aside.profilesTitle": "Perfis do Aside", + "integrations.aside.profilesHint": "Escolha quais perfis recebem os modelos selecionados. O perfil ativo do Aside permanece inalterado.", + "integrations.aside.all": "Sincronizar todos os perfis", + "integrations.aside.syncNow": "Sincronizar agora", + "integrations.aside.applied": "{count} de {total} perfis aplicados", + "integrations.aside.current": "Perfil atual", + "integrations.aside.profile": "Perfil {id}", + "integrations.aside.toggle": "Sincronizar {name}", + "integrations.aside.details": "Gerenciar {name}", + "integrations.aside.back": "Todos os perfis do Aside", + "integrations.aside.empty": "Abra o Aside e crie um perfil para conectá-lo.", + "integrations.aside.partial": "Alguns perfis requerem atenção. Suas escolhas de sincronização foram salvas; verifique o estado de cada perfil.", + "integrations.aside.pending": "Escolha de sincronização salva; atualização do arquivo pendente.", + "integrations.aside.retry": "Tentar {name} novamente", + "integrations.aside.loadError": "Não foi possível carregar os perfis do Aside. Tente novamente para verificar o estado deles.", + "integrations.codex.title": "Codex CLI", + "integrations.codex.body": "A integração do Codex pertence ao serviço do proxy. Iniciar o opencodex a aplica; parar o serviço restaura o roteamento nativo.", + "integrations.codex.openService": "Abrir controles do serviço", + "integrations.state.notInstalled": "Não instalado", + "integrations.state.unknown": "Verificando…", + "integrations.detail.codexRouted": "As requisições do Codex passam por este proxy", + "integrations.detail.codexAbsent": "O Codex ainda não é roteado por este proxy", + "integrations.detail.keyCount": "{count} chave(s) emitida(s)", + "integrations.detail.keyNone": "Nenhuma chave emitida", + "integrations.detail.keyChecking": "Verificando…", + "integrations.detail.keyUnavailable": "Status da chave indisponível", + "integrations.detail.claudeOff": "A conexão está desativada", + "integrations.detail.desktopCurrent": "O Desktop está executando este perfil", + "integrations.detail.desktopStale": "O arquivo de perfil foi alterado após ser aplicado", + "integrations.detail.desktopNotServed": "O perfil existe, mas o Desktop serve outro", + "integrations.detail.desktopAbsent": "Nenhum perfil aplicado", + "integrations.detail.desktopDesiredOff": "A integração com o Claude Desktop está desativada", + "integrations.detail.desktopDesiredOffCleanupPending": "O Claude Desktop ainda está usando o gateway; limpeza pendente", + "integrations.detail.desktopDesiredOnNotApplied": "A integração está ativada, mas o Desktop não está usando o perfil do gateway", + "integrations.detail.desktopSelectedElsewhere": "O Desktop está usando outro perfil", + "integrations.detail.desktopProfileDrift": "O perfil selecionado do Desktop foi alterado", + "integrations.detail.desktopObservedUnsafe": "O perfil selecionado do Desktop não pode ser alterado com segurança", + "integrations.detail.desktopNotInstalled": "A biblioteca de configuração do Claude Desktop não está instalada", + "integrations.detail.grokModels": "{count} modelo(s) conectado(s)", + "integrations.detail.grokAbsent": "Nenhum bloco do opencodex na configuração", + "integrations.detail.cursorSeen": "O Cursor chamou este proxy recentemente", + "integrations.detail.cursorNeverSeen": "Private Inference instalado; nenhuma requisição vista ainda", + "integrations.detail.cursorAbsent": "Cursor Private Inference não encontrado", + "integrations.cursor.title": "Cursor", + "integrations.cursor.intro": "O Cursor Private Inference executa seu agente localmente e se comunica com o opencodex em loopback. O Cursor comum não consegue: o backend dele chama o endpoint personalizado e exige uma URL HTTPS pública. Esta página nunca grava no Cursor; cole os valores abaixo no Cursor você mesmo.", + "integrations.cursor.loading": "Lendo o status do Cursor…", + "integrations.cursor.unavailable": "Não foi possível ler o status do Cursor a partir do proxy.", + "integrations.cursor.detection": "Versões instaladas", + "integrations.cursor.privateInference": "Cursor Private Inference", + "integrations.cursor.regular": "Cursor (padrão)", + "integrations.cursor.detected": "Detectado", + "integrations.cursor.notFound": "Não encontrado", + "integrations.cursor.regularOnly": "Apenas o Cursor regular foi encontrado. Ele roteia endpoints personalizados pelos servidores do Cursor, então um proxy em loopback fica inacessível sem um túnel público. Consulte o guia da versão Private Inference.", + "integrations.cursor.installerIntro": "O Cursor Private Inference é uma versão separada em modo local. O canal de atualização do Cursor anuncia o instalador, versão {version}; instale-o e cole a Base URL e a chave de API abaixo em Settings > Models > Gateway.", + "integrations.cursor.installerOpen": "Baixar o instalador", + "integrations.cursor.installerUnavailable": "Não foi possível resolver o instalador do Private Inference a partir do canal de atualização do Cursor para este computador. Os valores do gateway abaixo valem depois que ele for instalado.", + "integrations.cursor.installerCheck": "Procurar o instalador do Private Inference", + "integrations.cursor.installerChecking": "Consultando o canal de atualização do Cursor…", + "integrations.cursor.nothingFound": "Nenhuma instalação do Cursor foi encontrada nos locais habituais. Se estiver instalado em outro lugar, os valores abaixo continuam valendo.", + "integrations.cursor.gateway": "Valores do gateway", + "integrations.cursor.gatewayHint": "No Cursor Private Inference, abra Settings > Models > Gateway, cole estes dois valores e clique em Refresh model list.", + "integrations.cursor.baseUrl": "Base URL", + "integrations.cursor.apiKey": "Chave de API", + "integrations.cursor.apiKeyCredential": "Uma das suas chaves de API do opencodex (este bind exige uma credencial)", + "integrations.cursor.copy": "Copiar", + "integrations.cursor.copied": "Copiado", + "integrations.cursor.connection": "Conexão", + "integrations.cursor.seen": "Última requisição do Cursor: {time} ({ua})", + "integrations.cursor.neverSeen": "Nenhuma requisição do Cursor desde que o proxy iniciou. Depois de salvar o gateway, clique em Refresh model list no Cursor.", + "integrations.cursor.models": "O que o Cursor vai mostrar", + "integrations.cursor.modelsHint": "O Cursor escolhe a escala de Reasoning a partir da própria tabela de modelos, então o opencodex só consegue prevê-la. Context lista a janela padrão e a opcional (Max Mode do Cursor).", + "integrations.cursor.ladderFromBundle": "Escalas de Reasoning lidas do bundle instalado do Cursor Private Inference {version}. Quem decide é o Cursor; o opencodex apenas informa a tabela dele.", + "integrations.cursor.ladderFromStatic": "As escalas de Reasoning são um espelho estático do Cursor 3.18.25 (nenhum bundle legível do Private Inference foi encontrado). Context lista a janela padrão e a opcional.", + "integrations.cursor.unknownVersion": "versão desconhecida", + "integrations.cursor.noControl": "—", + "integrations.cursor.singleWindow": "janela única", + "integrations.cursor.noControlTitle": "Este id não está na tabela de esforço embutida do Cursor, então o Cursor não mostra controle de Reasoning.", + "integrations.cursor.effortRowsOne": "1 linha de esforço publicada", + "integrations.cursor.effortRowsMany": "{n} linhas de esforço publicadas", + "integrations.cursor.effortRowsOff": "sem linhas de esforço", + "integrations.cursor.tableLessHint": "Linhas marcadas com — ficam sem controle de Reasoning no Cursor. Ative cursorEffortRows para publicar uma entrada no seletor por esforço (id--effort) ou defina modelDefaultReasoningEfforts no provedor para um padrão fixo.", + "integrations.cursor.colModel": "Modelo", + "integrations.cursor.colReasoning": "Raciocínio", + "integrations.cursor.colContext": "Contexto", + "integrations.cursor.guide": "Abrir o guia do Cursor Private Inference", + "integrations.dialog.codex.title": "Desativar a integração com o Codex?", + "integrations.dialog.codex.changes": "O opencodex vai remover seu roteamento de {path}, remover o perfil gerado, restaurar o catálogo de modelos nativo e reetiquetar as threads retomáveis para o Codex nativo.", + "integrations.dialog.codex.breakage": "O codex puro vai se conectar direto à OpenAI, e os modelos roteados de outros provedores vão desaparecer do Codex. O proxy e o /v1/responses continuam rodando para outros clientes.", + "integrations.dialog.codex.undo": "Ativar de novo reconstrói o catálogo roteado com os modelos disponíveis na hora e injeta o Codex novamente. O histórico de retomada é adaptado à direção correspondente, mas seus arquivos não são restaurados byte a byte.", + "integrations.dialog.codex.sideEffect": "Se você selecionou um modelo raiz roteado depois que o opencodex injetou a configuração, desativar remove essa seleção e reativar a integração não consegue reconstruí-la; selecione o modelo de novo. Se um model_provider externo controla o Codex, o opencodex remove apenas seu journal obsoleto e deixa a configuração, o catálogo e o histórico inalterados.", + "integrations.dialog.codex.confirm": "Desativar", + "integrations.dialog.grok.title": "Desativar a integração com o Grok Build?", + "integrations.dialog.grok.changes": "Apenas o bloco marcado pelo opencodex será removido de {path}. O conteúdo escrito fora do bloco permanece inalterado.", + "integrations.dialog.grok.breakage": "Desativar remove os aliases de modelo do opencodex no Grok Build. Os modelos usados com sua conta xAI continuam disponíveis.", + "integrations.dialog.grok.undo": "Se o opencodex estiver rodando em um endereço loopback, reativar grava um novo bloco com os modelos disponíveis no momento.", + "integrations.dialog.grok.confirm": "Desativar", + "integrations.dialog.desktop.title": "Desativar a integração com o Claude Desktop?", + "integrations.dialog.desktop.changes": "Se {path} contiver um perfil de gateway gerenciado pelo opencodex, o Desktop primeiro selecionará um novo perfil padrão sem credenciais e depois removerá o perfil antigo e o backup.", + "integrations.dialog.desktop.breakage": "O Claude Desktop voltará ao Claude padrão, em vez dos modelos roteados pelo opencodex.", + "integrations.dialog.desktop.undo": "Reativar regenera o perfil do opencodex a partir das suas atribuições de modelo salvas.", + "integrations.dialog.desktop.restart": "O Claude Desktop lê esta configuração apenas na inicialização. Feche-o completamente e reabra para que a mudança tenha efeito.", + "integrations.dialog.desktop.confirm": "Desativar", + "integrations.native.msg.nonLoopbackRemoved": "O Grok Build só pode ser registrado automaticamente enquanto o opencodex roda em um endereço loopback. O bloco anterior que apontava para loopback foi removido.", + "integrations.native.msg.nonLoopbackRemovedNoop": "O Grok Build só pode ser registrado automaticamente enquanto o opencodex roda em um endereço loopback. Não havia bloco anterior para remover.", + "integrations.native.msg.nonLoopbackSuperseded": "O Grok Build só pode ser registrado automaticamente enquanto o opencodex roda em um endereço loopback. Outro processo gravou um novo bloco nesse meio-tempo, então o bloco atual no arquivo não foi criado por esta requisição.", + "integrations.native.error.orphanedMarker": "{path} tem um marcador de início do opencodex, mas nenhum marcador de fim. O arquivo foi mantido inalterado porque o opencodex não consegue determinar onde seu bloco termina.", + "integrations.native.error.homeMismatch": "O home do serviço instalado não corresponde ao home atual, então o arquivo foi mantido inalterado.", + "integrations.native.error.notInstalled": "O Grok Build não está instalado, então não há nada a alterar.", + "integrations.native.error.configBusy": "A configuração está sendo salva em outro lugar e não pôde ser alterada. Tente novamente em instantes.", + "integrations.native.error.desktopUnsafeMetadata": "Os metadados do Claude Desktop em {path} não puderam ser lidos com segurança, então sua biblioteca não foi alterada.", + "integrations.native.error.desktopCleanupIncomplete": "O Claude Desktop está apontado para o modo padrão, mas ainda restam arquivos de credencial antigos do opencodex em: {paths}.", + "integrations.native.msg.desktopDisabled": "Integração com o Claude Desktop desativada.", + "integrations.native.msg.desktopEnabled": "Integração com o Claude Desktop ativada.", + "integrations.state.absent": "Não aplicado", + "integrations.state.current": "Aplicado", + "integrations.state.stale": "Atualização necessária", + "integrations.state.conflict": "Conflito", + "integrations.state.unsafe": "Não foi possível verificar", + "integrations.summary.detected": "Clientes detectados", + "integrations.summary.applied": "Clientes configurados", + "integrations.summary.stale": "Atualização necessária", + "integrations.summary.lastChange": "Última alteração", + "integrations.summary.disableAll": "Desativar todos…", + "integrations.onboarding": "Aplicar grava um bloco de provedor do opencodex depois de salvar um backup. Desativar remove apenas esse bloco, e um snapshot retido pode ser restaurado.", + "integrations.empty.title": "Nenhum cliente instalado foi detectado", + "integrations.empty.body": "Instale um cliente compatível e volte aqui para aplicar o opencodex.", + "integrations.action.apply": "Aplicar", + "integrations.action.disable": "Desativar", + "integrations.action.refresh": "Atualizar", + "integrations.action.settings": "Configurações", + "integrations.action.manageKeys": "Gerenciar chaves", + "integrations.action.restore": "Restaurar…", + "integrations.action.undo": "Desfazer", + "integrations.action.restorePoint": "Restaurar este ponto…", + "integrations.action.snapshotExpired": "Backup expirado", + "integrations.rollback.title": "Central de rollback", + "integrations.rollback.empty": "Ainda não há histórico de aplicações", + "integrations.rollback.emptyBody": "Toda gravação bem-sucedida mantém antes um snapshot do estado anterior.", + "integrations.catalog.title": "Clientes", + "integrations.rollback.older": "Operações anteriores", + "integrations.rollback.showMore": "Mostrar mais {n}", + "integrations.rollback.failed": "Não foi possível carregar o histórico de rollback.", + "integrations.rollback.delete": "Excluir", + "integrations.rollback.deleteAria": "Excluir a entrada de rollback de {at}", + "integrations.rollback.deleteNewest": "A entrada mais recente deste cliente é mantida para que você ainda possa desfazer.", + "integrations.rollback.deleteGone": "Esta entrada já foi excluída. A lista será atualizada.", + "integrations.dialog.deleteEntry.title": "Excluir esta entrada de rollback?", + "integrations.dialog.deleteEntry.changes": "Esta entrada some da lista de rollback, e qualquer backup que ela ainda guarde de {path} é apagado do disco.", + "integrations.dialog.deleteEntry.breakage": "Você não poderá mais restaurar o arquivo para este ponto. As entradas mais novas e o próprio arquivo não são afetados.", + "integrations.dialog.deleteEntry.undo": "Isso não pode ser desfeito. A entrada mais recente de cada cliente é mantida e não pode ser excluída.", + "integrations.dialog.deleteEntry.confirm": "Excluir entrada", + "integrations.restore.title": "Restaurar este snapshot?", + "integrations.restore.body": "O arquivo atual recebe um backup primeiro, e então o snapshot selecionado o substitui.", + "integrations.restore.driftTitle": "Foram detectadas edições mais novas", + "integrations.restore.driftBody": "As alterações feitas depois deste snapshot receberão backup, e então o arquivo será substituído.", + "integrations.restore.confirm": "Restaurar", + "integrations.restore.confirmDrift": "Fazer backup das edições novas e restaurar", + "integrations.restore.pending": "Restaurando…", + "integrations.restore.manual": "A restauração automática falhou: {reason}. Restaure manualmente a partir de {path}.", + "integrations.error.load": "Não foi possível carregar o estado das integrações.", + "integrations.error.stale": "A última atualização falhou. Os valores abaixo podem estar desatualizados.", + "integrations.error.busy": "Outra alteração para este cliente ainda está em andamento. Tente novamente em instantes.", + "integrations.error.conflict": "A configuração mudou depois que o opencodex a gravou. Nada foi removido.", + "integrations.error.unsafe": "A configuração não pode ser alterada com segurança.", + "integrations.error.generic": "A alteração da integração falhou. Seu estado anterior foi mantido.", + "integrations.error.nonLoopback": "{client} só consegue alcançar um proxy em localhost: a configuração dele não tem onde colocar o cabeçalho de admissão que um bind remoto exige, então gravá-lo manualmente também não ajudaria. Dê a ele acesso loopback, por meio de um túnel ou de um encaminhador local.", + "integrations.status.installed": "Instalado", + "integrations.status.notInstalled": "Não instalado", + "integrations.status.appliedAt": "Aplicado", + "integrations.status.supersededStore": "Este cliente agora lê seus provedores de {path}, que o opencodex não grava, então ativá-lo aqui não mudaria nada do que ele carrega.", + "integrations.status.candidateConflict": "Outro arquivo de configuração do Kilo, {path}, também define provider.opencodex. Remova provider.opencodex desse arquivo antes de aplicar.", + "integrations.status.backup": "Backup", + "integrations.status.lastRestore": "Última restauração", + "integrations.status.unknown": "Desconhecido", + "integrations.bulk.title": "Desativar as integrações de clientes aplicadas?", + "integrations.bulk.body": "Apenas o bloco pertencente ao opencodex é removido. Um snapshot anterior à gravação é mantido para cada cliente.", + "integrations.bulk.partial": "Alguns clientes não puderam ser desativados: {clients}", + "integrations.bulk.success": "As integrações de clientes aplicadas foram desativadas.", + "integrations.retention.degraded": "A limpeza de backups está atrasada; backups mais antigos ainda podem estar no disco.", + "integrations.error.residual": "O arquivo pode estar em um estado intermediário: {message} Restaure-o a partir de {path}.", + "integrations.error.residualNoSnapshot": "{message} A recuperação automática não foi concluída. Verifique a configuração do cliente antes de tentar de novo.", + "integrations.error.recover": "{message} Há um backup em {path}.", + "integrations.kind.apply": "Aplicado", + "integrations.kind.disable": "Desativado", + "integrations.kind.refresh": "Atualizado", + "integrations.kind.restore": "Restaurado", + "integrations.kind.overwrite": "Sobrescrito", + "integrations.dialog.overwrite.title": "Substituir o bloco nesta configuração?", + "integrations.dialog.overwrite.changesUnowned": "Um bloco que não foi escrito por nós ocupa as configurações de que o opencodex precisa em {path}. Aplicar o substitui pelo bloco que o opencodex gravaria.", + "integrations.dialog.overwrite.changesForeign": "Sua edição dentro do bloco do opencodex em {path} será descartada e substituída pelo bloco que o opencodex gravaria.", + "integrations.dialog.overwrite.breakage": "Tudo o que o outro bloco configurava deixa de valer. O restante do arquivo não é tocado.", + "integrations.dialog.overwrite.undo": "Um snapshot é salvo antes, então isto aparece na lista de rollback abaixo e pode ser desfeito.", + "integrations.dialog.overwrite.confirm": "Substituir", + "integrations.action.overwrite": "Substituir", + "integrations.preview.loading": "Carregando o plano exato de alterações…", + "integrations.preview.failed": "Não foi possível carregar o plano de alterações. Nada foi alterado.", + "integrations.preview.stale": "A integração mudou enquanto você confirmava. Revise o plano atualizado e confirme novamente.", + "integrations.dialog.apply.title": "Aplicar esta integração?", + "integrations.dialog.apply.changes": "O opencodex vai adicionar suas configurações de provedor gerenciadas à configuração do cliente.", + "integrations.dialog.apply.breakage": "O cliente passará a usar os modelos expostos atualmente por esta instância do opencodex.", + "integrations.dialog.apply.undo": "Um backup é salvo antes, e a alteração pode ser desfeita pela central de rollback.", + "integrations.dialog.apply.confirm": "Aplicar", + "integrations.dialog.disable.title": "Desativar esta integração?", + "integrations.dialog.disable.changes": "Apenas as configurações pertencentes ao opencodex serão removidas.", + "integrations.dialog.disable.breakage": "O cliente deixará de usar os modelos roteados por esta instância do opencodex.", + "integrations.dialog.disable.undo": "Um backup é salvo antes, e a alteração pode ser desfeita pela central de rollback.", + "integrations.dialog.disable.confirm": "Desativar", + "integrations.plan.heading": "Plano de alterações do servidor", + "integrations.plan.operation": "Operação: {operation}", + "integrations.plan.operation.apply": "Aplicar", + "integrations.plan.operation.overwrite": "Substituir", + "integrations.plan.operation.disable": "Desativar", + "integrations.plan.operation.restore": "Restaurar", + "integrations.plan.change.add": "Adicionar", + "integrations.plan.change.replace": "Substituir", + "integrations.plan.change.remove": "Remover", + "integrations.plan.change.snapshot": "Salvar backup", + "integrations.plan.change.ownership": "Atualizar o registro de propriedade", + "integrations.plan.change.journal": "Registrar entrada de rollback", + "integrations.plan.foreign.none": "Nenhuma edição externa foi detectada.", + "integrations.plan.foreign.unowned": "Um bloco de configurações que não pertence ao opencodex será substituído.", + "integrations.plan.foreign.foreignEdit": "As edições dentro do bloco gerenciado pelo opencodex serão substituídas.", + "integrations.plan.foreign.drift": "O arquivo mudou desde este ponto de rollback.", + "integrations.plan.noop.applied": "Nenhuma alteração é necessária no documento do cliente gerenciado; as configurações da integração já estão presentes.", + "integrations.plan.noop.disabled": "Nenhuma alteração é necessária no documento do cliente gerenciado; as configurações da integração já estão ausentes.", + "integrations.plan.noop.profilePreference": "A preferência de sincronização do perfil ainda será salva quando você confirmar.", + "integrations.plan.refused": "O servidor não consegue aplicar este plano com segurança.", + "integrations.plan.refusal.notInstalled": "O cliente não está instalado. Instale-o, atualize esta página e visualize a alteração novamente.", + "integrations.plan.refusal.conflict": "As configurações gerenciadas entram em conflito com o arquivo atual. Revise o arquivo ou use Substituir quando essa ação estiver disponível.", + "integrations.plan.refusal.unsafe": "A configuração do cliente não pode ser alterada com segurança. Repare ou substitua o arquivo antes de tentar de novo.", + "integrations.plan.refusal.nonLoopback": "Este cliente exige um endereço de proxy loopback. Use acesso por localhost, um túnel ou um encaminhador local antes de tentar de novo.", + "integrations.plan.refusal.supersededStore": "Este cliente agora lê seus provedores de um arquivo que o opencodex não grava, então esta operação não mudaria nada do que ele carrega.", + "integrations.plan.refusal.driftRequiresConfirm": "O arquivo mudou depois deste ponto de rollback. Revise o plano com as diferenças antes de restaurar.", + "integrations.plan.refusal.snapshotExpired": "O backup deste ponto de rollback expirou, então ele não pode mais ser restaurado.", + "integrations.plan.refusal.writeFailed": "O servidor não conseguiu preparar uma gravação segura. Atualize o estado da integração antes de tentar de novo.", + "integrations.mutation.pending": "Aplicando a alteração confirmada…", + "integrations.bulk.breakage": "Cada cliente deixará de usar os modelos roteados por esta instância do opencodex.", + "integrations.bulk.undo": "Cada cliente mantém seu próprio backup e sua entrada de rollback.", + "integrations.bulk.confirm": "Desativar todos", + "integrations.semantics.opencode": "Apenas inicializações diretas do disco; a injeção de ambiente do ocx opencode tem precedência.", + "integrations.semantics.pi": "Vale para novas sessões.", + "integrations.semantics.omp": "Reinicie o OMP para carregar o catálogo.", + "integrations.semantics.hermes": "Vale para novas sessões.", + "integrations.semantics.openclaw": "Vale imediatamente para um gateway em execução.", + "integrations.semantics.kimi": "Reinicie ou execute /reload para aplicar (a v2 observa o arquivo).", + "integrations.semantics.gajae": "Vale para uma nova sessão ou ao abrir /model.", + "integrations.semantics.dsh": "O OpenCodex gerencia apenas llm-pi-ai.providers.opencodex em $DSH_HOME/settings.yaml. O DSH recarrega esse provedor a quente; seu modelo padrão e o deepseek-official permanecem inalterados. Atualmente somente loopback; nenhuma credencial real é gravada.", + "integrations.semantics.mcode": "Gerencia apenas custom_provider.opencodex. Seu modelo padrão e o login do MiniMax permanecem inalterados.", + "integrations.semantics.zcode": "Gerencia apenas provider.opencodex em ~/.zcode/v2/config.json. Seu login do Z.ai e os outros provedores permanecem inalterados. Reinicie o ZCode após as alterações.", + "integrations.semantics.prime": "Gerencia apenas providers.opencodex no models.json do Prime Agent — ~/.prime/agent, a menos que PRIME_AGENT_CODING_AGENT_DIR o redirecione. Seus outros provedores e substituições de modelo permanecem inalterados. Vale para novas sessões.", + "integrations.semantics.aside": "Gerencia apenas providers.opencodex no ~/.aside/u//models.json deste perfil. Seus outros provedores permanecem inalterados. Feche completamente e reabra o Aside após aplicar.", + "integrations.semantics.raycast": "Adiciona uma entrada de provedor OpenCodex ao providers.yaml do Raycast, para que todos os modelos roteados apareçam no seletor de modelos do Raycast AI. Requer Raycast Pro.", + "integrations.semantics.omo": "Gerencia apenas providers.opencodex no models.json do omo — ~/.omo/agent, a menos que OMO_CODING_AGENT_DIR, SENPI_CODING_AGENT_DIR ou PI_CODING_AGENT_DIR o redirecione. Seus outros provedores permanecem inalterados. Vale para novas sessões.", + "integrations.lazycodexRoles.title": "omo (Codex / LazyCodex) · Modelos das funções de agente do Codex", + "integrations.lazycodexRoles.hint": "Escolha o modelo que o Codex executa em cada função de agente. Ao salvar, só a linha do modelo no arquivo da função em $CODEX_HOME/agents é alterada, e a escolha é espelhada em omo.jsonc para o LazyCodex apenas quando esse arquivo puder ser reescrito com segurança.", + "integrations.lazycodexRoles.role": "Função", + "integrations.lazycodexRoles.current": "Modelo atual", + "integrations.lazycodexRoles.model": "Novo modelo", + "integrations.lazycodexRoles.modelFor": "Modelo para {role}", + "integrations.lazycodexRoles.none": "Nenhum modelo fixado", + "integrations.lazycodexRoles.choose": "Escolha um modelo", + "integrations.lazycodexRoles.empty": "Nenhuma função de agente do Codex foi encontrada.", + "integrations.lazycodexRoles.loadFailed": "Não foi possível carregar as funções de agente do Codex.", + "integrations.lazycodexRoles.saveFailed": "Não foi possível salvar o modelo de {role}.", + "integrations.lazycodexRoles.saved": "{role} agora usa {model}.", + "integrations.lazycodexRoles.omoAbsent": "{role} agora usa {model}. O omo.jsonc não foi encontrado, então só o arquivo da função foi alterado.", + "integrations.lazycodexRoles.omoComments": "{role} agora usa {model} no arquivo da função. O omo.jsonc não foi alterado porque salvar removeria seus comentários; defina codex.agents.{role}.model nele manualmente.", + "integrations.lazycodexRoles.omoInvalid": "{role} agora usa {model} no arquivo da função. O omo.jsonc não foi alterado porque não é um objeto JSON com um objeto codex.agents.", + "integrations.lazycodexRoles.omoFailed": "{role} agora usa {model} no arquivo da função, mas não foi possível gravar o omo.jsonc.", + "integrations.lazycodexRoles.retryMirror": "Tentar omo.jsonc novamente", + "integrations.lazycodexRoles.omoCommentsState": "O omo.jsonc contém comentários, então salvar aqui altera apenas os arquivos das funções. Edite codex.agents no omo.jsonc manualmente para mantê-lo sincronizado.", + "integrations.lazycodexRoles.auto.button": "Atribuir automaticamente", + "integrations.lazycodexRoles.auto.running": "Dimensionando funções…", + "integrations.lazycodexRoles.auto.hint": "Dimensiona cada função com seu modelo padrão e propõe o modelo suficiente mais barato para cada uma. Nada muda até você aplicar.", + "integrations.lazycodexRoles.auto.failed": "Não foi possível dimensionar as funções de agente do Codex.", + "integrations.lazycodexRoles.auto.title": "Modelos propostos", + "integrations.lazycodexRoles.auto.sizedWith": "Dimensionado com {model}", + "integrations.lazycodexRoles.auto.applyAll": "Aplicar todos", + "integrations.lazycodexRoles.auto.discard": "Descartar", + "integrations.lazycodexRoles.auto.sizingFailed": "A chamada de dimensionamento falhou: {error}", + "integrations.lazycodexRoles.auto.appliedCount": "{count} de {total} propostas aplicadas.", + "integrations.lazycodexRoles.auto.proposalFor": "Proposta para {role}", + "integrations.lazycodexRoles.auto.tierEffort": "Nível {tier}, esforço {effort}", + "integrations.lazycodexRoles.auto.tierFast": "Rápido", + "integrations.lazycodexRoles.auto.tierStandard": "Padrão", + "integrations.lazycodexRoles.auto.tierFrontier": "Frontier", + "integrations.lazycodexRoles.auto.effortGlance": "olhada rápida", + "integrations.lazycodexRoles.auto.effortMeasured": "moderado", + "integrations.lazycodexRoles.auto.effortThorough": "minucioso", + "integrations.lazycodexRoles.auto.effortExhaustive": "exaustivo", + "integrations.lazycodexRoles.auto.unsized": "Não dimensionado: {reason}", + "integrations.lazycodexRoles.auto.unassigned": "Sem modelo: {reason}", + "integrations.lazycodexRoles.auto.applied": "Aplicado", + "integrations.lazycodexRoles.auto.alreadySet": "Já definido", + "integrations.lazycodexRoles.auto.apply": "Aplicar", + "integrations.lazycodexRoles.auto.moveUp": "Suba de nível se: {text}", + "integrations.lazycodexRoles.auto.moveDown": "Desça de nível se: {text}", + "integrations.semantics.cline": "Gerencia o OpenCodex no providers.json e no models.json do Cline CLI. Pare o Cline antes de alterar ou sincronizar esses arquivos e reinicie depois. Desfazer restaura os dois originais. Seu provedor padrão permanece inalterado; selecione o OpenCodex no Cline.", + "integrations.semantics.kilo": "Gerencia apenas provider.opencodex na configuração global do Kilo — o primeiro arquivo existente entre kilo.jsonc, kilo.json, opencode.jsonc, opencode.json ou config.json em ~/.config/kilo (XDG_CONFIG_HOME realoca esse diretório; kilo.jsonc é criado quando nenhum existe). As demais chaves permanecem inalteradas. Aplicar reescreve o arquivo inteiro, então comentários e vírgulas finais não são preservados. Selecione opencodex/ no Kilo.", + "integrations.semantics.droid": "Adiciona modelos personalizados do OpenCodex ao settings.json do Factory Droid. Desativar remove apenas as linhas gerenciadas; desfazer restaura o arquivo salvo.", + "integrations.raycast.proRequired": "Custom Providers é um recurso do Raycast Pro. O arquivo será gravado, mas o Raycast o ignora até que uma assinatura Pro esteja ativa.", + "integrations.raycast.planUnknown": "Não foi possível determinar se o Raycast Pro está ativo; Custom Providers requer o Raycast Pro.", + "integrations.raycast.revealConfig": "Abra Raycast → Settings → AI e clique uma vez em Reveal Providers Config para que a pasta de provedores exista.", + "codexAuth.mainAccount": "Conta principal", + "codexAuth.logLabel": "Rótulo no log", + "codexAuth.codexApp": "App Codex", + "codexAuth.moreActions": "Mostrar mais ações", + "codexAuth.copyId": "Copiar ID da conta", + "codexAuth.appLogin": "Login do app", + "codexAuth.accountPool": "Pool de contas", + "codexAuth.accountModeTitle": "Modo de conta OpenAI", + "codexAuth.accountModePool": "Modo Pool", + "codexAuth.accountModePoolDesc": "O login principal e as contas adicionadas elegíveis se alternam aqui.", + "codexAuth.accountModeDirect": "Modo direto", + "codexAuth.accountModeDirectDesc": "As requisições usam apenas o login principal; as contas adicionadas continuam armazenadas para o modo Pool.", + "codexAuth.openaiMissing": "O provedor OpenAI integrado não está configurado.", + "codexAuth.openaiDisabled": "O provedor OpenAI integrado está desativado.", + "codexAuth.openaiUnavailableDesc": "Suas contas OpenAI continuam disponíveis. Ative o provedor para rotear as requisições do Codex.", + "codexAuth.enableOpenai": "Ativar OpenAI", + "codexAuth.enablingOpenai": "Ativando...", + "codexAuth.enableOpenaiFailed": "Falha ao ativar o provedor OpenAI.", + "codexAuth.openaiPresetLoadFailed": "Falha ao carregar o preset do provedor OpenAI.", + "codexAuth.openaiPresetUnavailable": "O preset do provedor OpenAI não está disponível.", + "codexAuth.openProviders": "Abrir Provedores", + "codexAuth.add": "Adicionar", + "codexAuth.credits": "Créditos", + "codexAuth.creditsRemaining": "restantes", + "codexAuth.creditsUnlimited": "Ilimitado", + "codexAuth.creditsOverage": "Limite de excedente atingido", + "codexAuth.creditsApprox": "Mensagens aprox.: local {local} · nuvem {cloud}", + "codexAuth.creditsToggle": "Créditos do Codex", + "codexAuth.creditsToggleHint": "Mostra os créditos restantes do Codex nos cartões das contas. Oculto por padrão.", + "codexAuth.creditsShown": "Créditos do Codex visíveis", + "codexAuth.creditsHidden": "Créditos do Codex ocultos", + "codexAuth.creditsToggleFailed": "Não foi possível alterar a configuração de créditos do Codex", + "codexAuth.refreshQuota": "Atualizar cotas", + "codexAuth.ultraFastTitle": "Nível de serviço Ultra Fast", + "codexAuth.mainHardLockTitle": "Bloquear conta principal (5h {short}%, longa {long}%)", + "codexAuth.mainHardLockDesc": "Bloqueia em {short}% na janela de 5h ou em {long}% na janela semanal (mensal, para contas apenas mensais). Desbloqueia quando todas as janelas bloqueantes ficam abaixo do limite; a proteção continua ativa. Ativada por padrão.", + "codexAuth.mainHardLockConfirmTitle": "Ativar a proteção da conta principal (5h {short}%, longa {long}%)?", + "codexAuth.mainHardLockConfirmBody": "Enquanto estiver bloqueada, a conta principal não pode usar a Luna Reserve. Manter o uso normal abaixo do esgotamento pode impedir a ativação da Reserve. As contas adicionadas e os outros provedores continuam disponíveis. Requisições em andamento, credenciais de keyring sem correspondência e tráfego fora deste proxy não são protegidos.", + "codexAuth.mainHardLockConfirm": "Ativar proteção", + "codexAuth.mainHardLockEnabled": "A proteção está ativada (5h {short}%, longa {long}%).", + "codexAuth.mainHardLockDisabled": "A proteção da conta principal está desativada. Os limites das outras contas continuam valendo.", + "codexAuth.mainHardLockLoadFailed": "Não foi possível carregar esta configuração. Tente de novo para verificar o estado atual.", + "codexAuth.mainHardLockSaveFailed": "Não foi possível confirmar o salvamento. Recarregue a configuração antes de tentar de novo.", + "codexAuth.mainHardLockRefreshFailed": "Configuração salva, mas não foi possível atualizar o status das contas. Tente novamente.", + "codexAuth.mainHardLockBlocked": "Bloqueada pela proteção da conta principal (5h {short}%, longa {long}%)", + "codexAuth.mainHardLockUnknown": "Proteção ativa · uso desconhecido", + "codexAuth.mainHardLockMonitoring": "Proteção ativa · monitorando", + "codexAuth.mainExternalUsageWarning": "Possível uso fora do opencodex. O bloqueio pode não impedir o esgotamento.", + "codexAuth.mainHardLockManage": "Ver configuração de proteção", + "codexAuth.ultraFastDesc": "Impede que um nível de serviço ultrarrápido configurado por você seja removido quando o catálogo é regenerado e o nomeia nos logs de requisição. Não adiciona o Ultra Fast ao seletor de modelos: o upstream anuncia apenas o Fast, então uma linha no seletor ofereceria uma velocidade que a conexão não consegue entregar.", + "codexAuth.ultraFastLoadFailed": "Não foi possível ler a configuração do Ultra Fast.", + "codexAuth.ultraFastEnabled": "Nível Ultra Fast ativado", + "codexAuth.ultraFastDisabled": "Nível Ultra Fast desativado", + "codexAuth.ultraFastFailed": "Não foi possível alterar a configuração do Ultra Fast", + "codexAuth.refreshingQuota": "Atualizando...", + "codexAuth.quotaRefreshed": "Cotas atualizadas", + "codexAuth.quotaRefreshFailed": "Falha ao atualizar as cotas", + "codexAuth.pauseExhausted": "Pausar esgotadas", + "codexAuth.pausingExhausted": "Verificando cotas...", + "codexAuth.pauseExhaustedSucceeded": "Contas no limite pausadas: {count}", + "codexAuth.pauseExhaustedNone": "Nenhuma conta tem 100% de uso confirmado.", + "codexAuth.pauseExhaustedFailed": "Falha ao verificar e pausar as contas esgotadas.", + "codexAuth.noPool": "Nenhuma conta do pool foi adicionada ainda.", + "codexAuth.pause": "Pausar", + "codexAuth.resume": "Retomar", + "codexAuth.paused": "PAUSADA", + "codexAuth.planExcluded": "Não selecionada automaticamente", + "codexAuth.planExcludedHint": "O plano {plan} está excluído da seleção automática. As rotas explícitas por conta continuam disponíveis.", + "codexAuth.pauseSucceeded": "{email} está pausada", + "codexAuth.resumeSucceeded": "{email} voltou a ficar disponível para o pool", + "codexAuth.pauseFailed": "Não foi possível pausar {email}. Nada foi alterado.", + "codexAuth.resumeFailed": "Não foi possível retomar {email}. Nada foi alterado.", + "codexAuth.pausedHint": "Excluída da troca automática, das novas tentativas, da recuperação de cooldown e da seleção manual até ser retomada.", + "codexAuth.creditsAfterLimit": "Usar créditos após o limite", + "codexAuth.creditsAfterLimitHint": "Desativado por padrão: uma conta cuja janela de uso chega a 100% é retirada de uso até a janela reiniciar, para que seus créditos do ChatGPT não sejam gastos. Ative em uma conta para que ela continue trabalhando com seus créditos. Contas novas começam desativadas.", + "codexAuth.creditsAfterLimitAria": "Usar créditos após o limite de uso para {email}", + "codexAuth.creditsAfterLimitMainHint": "O bloqueio rígido da conta principal (ativado por padrão) ainda a interrompe primeiro. Desative o bloqueio para que a conta principal gaste créditos.", + "codexAuth.creditSpend": "Usar créditos", + "codexAuth.creditSpendAria": "Usar créditos do ChatGPT após o limite de uso", + "codexAuth.creditsOn": "Usa créditos", + "codexAuth.creditsOnSucceeded": "{email} pode usar créditos após o limite de uso.", + "codexAuth.creditsOffSucceeded": "{email} será retirada de uso ao atingir o limite, e seus créditos serão preservados.", + "codexAuth.creditsUpdateFailed": "Não foi possível atualizar a configuração de créditos de {email}. Nada foi alterado.", + "codexAuth.creditsAllOnSucceeded": "Agora todas as contas podem usar créditos após o limite de uso.", + "codexAuth.creditsAllOffSucceeded": "Nenhuma conta gastará créditos. As contas são retiradas de uso ao chegar a 100%.", + "codexAuth.creditsAllUpdateFailed": "Não foi possível atualizar a configuração de créditos. Nada foi alterado.", + "codexAuth.pinned": "FIXADA", + "codexAuth.pinnedHint": "Você selecionou esta conta manualmente, então uma ordem de seleção mais alta não passará por cima dela. A fixação dura até esta conta ser esgotada, você selecionar outra ou alterar qualquer ordem de seleção.", + "codexAuth.fiveHour": "5h", + "codexAuth.weekly": "Semana", + "codexAuth.quotaAutoRefresh": "Ativação automática da janela", + "codexAuth.quotaAutoRefreshHint": "Envia uma requisição mínima de aquecimento do Codex, sem armazenamento, quando esta janela de cota é reiniciada.", + "codexAuth.quotaAutoRefreshUpdated": "Ativação automática da janela atualizada.", + "codexAuth.quotaAutoRefreshFailed": "Não foi possível atualizar a ativação automática da janela.", + "codexAuth.monthly": "30d", + "codexAuth.resets": "reinicia", + "codexAuth.today": "Hoje", + "codexAuth.current": "ATUAL", + "codexAuth.nextSession": "SELECIONADA", + "codexAuth.poolPrepared": "PREPARADA PARA O POOL", + "codexAuth.preparePoolTitle": "Preparar esta conta para o modo Pool?", + "codexAuth.preparePoolDesc": "As requisições diretas continuam usando o login principal. Esta conta passa a ser a seleção preparada do Pool quando o modo Pool for ativado.", + "codexAuth.prepareForPool": "Preparar para o Pool", + "codexAuth.poolPreparedToast": "{email} está preparada para o modo Pool", + "codexAuth.switchTitle": "Trocar a conta ativa?", + "codexAuth.switchDesc": "Vale imediatamente. As threads com afinidade de conta e as requisições já em andamento mantêm a conta capturada; as requisições novas ou sem vínculo usam a faixa de ordem da conta selecionada, e as contas com a mesma ordem de seleção continuam se revezando.", + "codexAuth.cacheWarning": "O cache de prompt é reiniciado ao trocar de conta. A nova sessão começa com o cache vazio.", + "codexAuth.switchExceedsThresholdWarning": "O uso desta conta atinge ou excede o limite de troca ({threshold}%). A seleção fixada será liberada se não houver folga de cota.", + "codexAuth.setAsNext": "Usar esta conta em seguida", + "codexAuth.cancel": "Cancelar", + "codexAuth.switchBack": "Voltar para a conta principal?", + "codexAuth.switchBackDesc": "Vale imediatamente. As threads com afinidade de conta e as requisições já em andamento mantêm a conta capturada; as requisições novas ou sem vínculo usam a faixa de ordem da conta do login do app, e as contas com a mesma ordem de seleção continuam se revezando.", + "codexAuth.autoSwitch": "Troca proativa por uso", + "codexAuth.autoSwitchQuotaDesc": "Cota: com {threshold}% de uso ou mais, a próxima requisição sem vínculo pode ir para uma conta elegível de menor uso. As tarefas vinculadas mantêm a afinidade por padrão e só mudam quando a conta não consegue atender, e apenas para uma conta com folga real de cota; Go/Free usam somente 30d.", + "codexAuth.autoSwitchQuotaOffDesc": "A troca proativa por uso está desativada. A atribuição de tarefas novas/sem vínculo e a recuperação de falhas continuam valendo.", + "codexAuth.autoSwitchRoundRobinDesc": "A atribuição round-robin não usa este limite; continua alternando as tarefas novas/sem vínculo.", + "codexAuth.autoSwitchFillFirstDesc": "Fill-first: {threshold}% é o ponto de esgotamento para tarefas novas/sem vínculo; as tarefas vinculadas saudáveis mantêm a conta.", + "codexAuth.autoSwitchFillFirstOffDesc": "O fill-first não tem ponto de esgotamento por uso para tarefas novas/sem vínculo; cooldown, reautenticação e recuperação de falhas ainda podem mudar o roteamento.", + "codexAuth.failureRecoveryNote": "A recuperação de falhas é separada: uma requisição rejeitada antes de qualquer saída com 429/402, cooldown, reautenticação, exclusão ou failover transitório configurado pode selecionar outra conta elegível.", + "codexAuth.autoSwitchThreshold": "Limite de uso", + "codexAuth.autoSwitchThresholdAria": "Limite de uso, em porcentagem", + "codexAuth.autoSwitchThresholdInc": "Aumentar o limite de uso", + "codexAuth.autoSwitchThresholdDec": "Diminuir o limite de uso", + "codexAuth.autoSwitchLoadFailed": "Não foi possível carregar a configuração de troca por uso.", + "codexAuth.autoSwitchThresholdInvalid": "Digite um número inteiro de 1 a 100", + "codexAuth.autoSwitchUpdated": "Troca proativa por uso atualizada", + "codexAuth.autoSwitchUpdateFailed": "Não foi possível confirmar a atualização da troca por uso. O último valor confirmado é exibido.", + "codexAuth.requestUserInput": "Pedir entrada no modo Default", + "codexAuth.requestUserInputDesc": "Permite que o Codex pause uma sessão no modo Default e faça perguntas a você com a ferramenta request_user_input.", + "codexAuth.requestUserInputUpdated": "Feature flag atualizada - vale para novas sessões.", + "codexAuth.requestUserInputUpdatedRestart": "Feature flag atualizada - vale para novas sessões. Reinicie o app Codex para aplicá-la.", + "codexAuth.requestUserInputUpdateFailed": "Não foi possível atualizar a feature flag. Nada foi alterado.", + "codexAuth.requestUserInputLoadFailed": "Não foi possível ler a feature flag do config.toml.", + "codexAuth.accountPickerTitle": "Direcionar uma conta específica do Codex pelo seletor de modelos", + "codexAuth.accountPickerOffDesc": "Quando ativado, as linhas comuns de GPT no seletor são substituídas por uma entrada por seletor de conta, para que você escolha a conta exata de uma conversa sem precisar sair da sessão. Desativar não remove nenhuma conta.", + "codexAuth.accountPickerOnDesc": "Cada seletor é um rótulo público de uma conta armazenada. Escolhê-lo trava a conversa na conta mapeada: ela nunca alterna nem recorre a outra, e não muda a conta ativa do Pool.", + "codexAuth.accountPickerCompatibility": "O login integrado do app Codex tem seu próprio seletor; os mapas gerados normalmente o chamam de main e usam um sufixo à prova de colisão, como main-2, quando necessário. As contas adicionadas recebem rótulos estáveis que preservam a privacidade, enquanto os rótulos personalizados de seletor permanecem inalterados. As conversas existentes e as seleções de modelo salvas continuam roteando. Desativar oculta as entradas geradas, mas preserva os seletores e as rotas exatas. Os IDs simples de modelos GPT mantêm o comportamento de Pool ou Direct.", + "codexAuth.accountPickerUpdated": "Direcionamento de conta atualizado.", + "codexAuth.accountPickerUpdateFailed": "Não foi possível atualizar o direcionamento de conta. A última configuração confirmada é exibida.", + "codexAuth.accountPickerLoadFailed": "Não foi possível carregar a configuração de direcionamento de conta.", + "codexAuth.accountPickerRefreshFailed": "Não foi possível atualizar esta configuração. O último valor confirmado continua sendo exibido.", + "codexAuth.advancedSettings": "Configurações avançadas", + "codexAuth.advancedSettingsAria": "Mostrar ou ocultar as configurações avançadas do Codex Auth", + "codexAuth.catalogRefreshPending": "A alteração foi salva, mas a atualização do catálogo de modelos do Codex está pendente. Execute ocx sync para tentar de novo.", + "anthropicPool.title": "Pool de contas Claude (experimental)", + "anthropicPool.enabledDesc": "Sessões fixas, e as novas sessões preferem contas com uso abaixo de {threshold}% ({window}).", + "anthropicPool.enabledNoProactiveDesc": "Os limites de uso não movem uma sessão saudável existente nem uma conta ativa saudável. Quando a conta ativa fica indisponível, e durante a recuperação de recusa, é escolhida a conta com menor uso ({window}).", + "anthropicPool.enabledFillFirstDesc": "Sessões fixas. As novas sessões permanecem na conta ativa até ela atingir {threshold}% ({window}), entrar em cooldown ou precisar de novo login; depois passam para a próxima conta, em ordem. A recuperação de recusa também usa a próxima conta, em ordem.", + "anthropicPool.enabledFillFirstNoThresholdDesc": "Sessões fixas. As novas sessões permanecem na conta ativa até ela entrar em cooldown ou precisar de novo login; depois passam para a próxima conta, em ordem. A recuperação de recusa também usa a próxima conta, em ordem.", + "anthropicPool.enabledRoundRobinDesc": "Sessões fixas. As novas sessões e a recuperação de recusa se revezam entre as contas elegíveis; uso, limite e janela de cota não são considerados.", + "anthropicPool.disabledDesc": "A seleção proativa de contas está desativada. O OpenCodex usa a conta selecionada e ainda pode trocar após uma resposta de limite de taxa (429) ou uma recusa de conta classificada, quando houver outra conta elegível conectada.", + "anthropicPool.experimentalWarning": "Use apenas assinaturas que você possua ou esteja autorizado a usar. O pooling automatizado de contas não é endossado pela Anthropic e continua experimental. Use o cliente Claude Code genuíno com uma pessoa supervisionando a sessão e verifique os termos vigentes da Anthropic. As contas podem compartilhar a cota de uma organização, então adicionar uma conta pode não aumentar a capacidade.", + "anthropicPool.detailsSummary": "Como funciona a seleção de contas", + "anthropicPool.detailsEnabling": "Ativar o pool mantém cada sessão em sua conta enquanto ela estiver saudável e permite que a estratégia selecionada escolha as contas das novas sessões e após uma recusa.", + "anthropicPool.detailsFailover": "Desativá-lo não desativa o failover: após um 429 ou uma recusa de conta classificada, uma requisição ainda pode passar para outra conta conectada. Pause uma conta para mantê-la fora do failover.", + "anthropicPool.detailsActivity": "O OpenCodex não envia requisições keep-warm. Por padrão, não renova tokens do Claude nem consulta o uso em segundo plano; o uso só é lido quando o painel, o app da barra de menus ou um comando ocx o solicita.", + "anthropicPool.detailsGuide": "Guia do pool de contas", + "anthropicPool.needTwoAccounts": "Adicione pelo menos duas contas Claude OAuth antes de ativar o pool.", + "anthropicPool.threshold": "Limite de uso para novas sessões", + "anthropicPool.thresholdAria": "Limite de uso para novas sessões, em porcentagem", + "anthropicPool.thresholdHelp": "É uma preferência para a seleção de contas, não um limite rígido de uso ou de cobrança. 0 desativa a troca proativa por este limite. Padrão: 80%.", + "anthropicPool.thresholdInvalid": "Digite um número inteiro de 0 a 100", + "anthropicPool.loadFailed": "Não foi possível carregar as configurações do pool Claude.", + "anthropicPool.saveFailed": "Não foi possível salvar as configurações do pool Claude.", + "anthropicPool.on": "Ligado", + "anthropicPool.off": "Desligado", + "accountPool.strategy": "Estratégia de rotação", + "accountPool.strategyDesc": "Como o OpenCodex atribui uma conta a uma tarefa nova/sem vínculo.", + "accountPool.strategyResetFirst": "Reinício mais próximo primeiro", + "accountPool.strategyHintResetFirst": "Prefere o reinício de 5 horas ou semanal futuro mais próximo entre as contas abaixo do limite de uso. As tarefas vinculadas seguem a política de afinidade configurada. Cotas independentes por modelo usam a ordenação por cota.", + "accountPool.strategyQuota": "Cota", + "accountPool.strategyRoundRobin": "Round-robin", + "accountPool.strategyFillFirst": "Fill-first", + "accountPool.strategyHintQuota": "A cota revincula uma tarefa existente no limite de uso apenas quando `pool.cacheAffinity` está desativado (por padrão, ele vem ativado). Caso contrário, as tarefas vinculadas permanecem até a conta não conseguir atender, e então só vão para uma conta com folga real de cota.", + "accountPool.strategyHintRoundRobin": "O round-robin alterna apenas as tarefas sem vínculo ativo; o limite de uso não altera a rotação normal.", + "accountPool.strategyHintFillFirst": "O fill-first usa o limite como ponto de esgotamento para tarefas sem vínculo; as tarefas vinculadas saudáveis mantêm a afinidade.", + "accountPool.unboundDefinition": "Tarefa nova/sem vínculo é uma requisição sem vínculo atual com uma conta; uma tarefa visível existente pode ficar sem vínculo após um reset do proxy ou da afinidade.", + "accountPool.stickyLimit": "Atribuições novas/sem vínculo antes de alternar", + "accountPool.stickyLimitAria": "Atribuições novas/sem vínculo antes de alternar", + "accountPool.stickyLimitInc": "Aumentar o limite de fixação", + "accountPool.stickyLimitDec": "Diminuir o limite de fixação", + "accountPool.stickyLimitHelp": "Mantém a conta selecionada por esta quantidade de atribuições de tarefas novas/sem vínculo antes de avançar; o contador aumenta quando a tarefa é vinculada, não após o sucesso no upstream.", + "accountPool.stickyLimitInvalid": "Digite um número inteiro de 1 a 100", + "accountPool.strategyLoadFailed": "Não foi possível carregar a estratégia de rotação.", + "accountPool.strategyUpdateFailed": "Não foi possível salvar a estratégia de rotação.", + "accountPool.switchAtThreshold": "trocar em {threshold}%", + "accountPool.drainAtThreshold": "esgotar em {threshold}%", + "accountPool.resetBelowThreshold": "reinício mais próximo abaixo de {threshold}%", + "accountPool.thresholdNotUsed": "limite não utilizado", + "accountPool.proactiveSwitchingOff": "troca proativa desativada", + "accountPool.quotaWindow": "Janela de cota", + "accountPool.quotaWindowDesc": "Qual barra de uso em cache controla a seleção de novas sessões baseada em cota, as verificações de limite do fill-first e as substituições elegíveis por 429.", + "accountPool.quotaWindowFiveHour": "Barra de 5 horas", + "accountPool.quotaWindowWeekly": "Barra semanal", + "accountPool.quotaWindowMaxUtilization": "Barra mais alta", + "accountPool.quotaWindowHint": "A semanal ignora as contas cuja barra de 5 horas está esgotada enquanto houver outra conta elegível, mas recorre a elas quando não houver. Empates na semanal preferem o menor uso de 5 horas; as barras semanais por conta só são conhecidas depois que a página Provedores as consultar.", + "accountPool.quotaWindowInert": "Só a cota — ou o fill-first com limite acima de 0 — pontua uma barra de uso, então esta configuração não muda nada na estratégia de rotação atual.", + "accountPool.priority": "Ordem de seleção", + "accountPool.priorityAria": "Ordem de seleção desta conta", + "accountPool.priorityHint": "Números maiores são usados primeiro. O pool passa para um número menor apenas quando todas as contas acima dele estiverem esgotadas ou indisponíveis.", + "accountPool.priorityFirst": "Primeira", + "accountPool.priorityEarlier": "Antes", + "accountPool.priorityNormal": "Normal", + "accountPool.priorityLater": "Depois", + "accountPool.priorityLast": "Última", + "accountPool.priorityOption": "{name} ({value})", + "accountPool.priorityCustom": "Personalizada", + "accountPool.priorityUpdated": "Ordem de seleção atualizada para {email}", + "accountPool.priorityUpdateFailed": "Não foi possível salvar a ordem de seleção de {email}. O último valor confirmado é exibido.", + "accountPool.autoSwitchThreshold": "Limite personalizado da conta", + "accountPool.autoSwitchThresholdAria": "Limite de uso de {email}", + "accountPool.autoSwitchOverrideAria": "Substituir o limite de uso global de {email}", + "accountPool.autoSwitchHint": "Substitui o limite global desta conta. 0 desativa a troca proativa por uso a partir desta conta.", + "accountPool.autoSwitchUpdated": "Limite de uso atualizado para {email}", + "accountPool.autoSwitchUpdateFailed": "Não foi possível salvar o limite de uso de {email}. O último valor confirmado é exibido.", + "codexAuth.switched": "{email} está selecionada para a próxima requisição", + "codexAuth.loadFailed": "Não foi possível carregar as configurações da conta Codex.", + "codexAuth.accountsRefreshFailed": "A última atualização das contas falhou. As contas abaixo são as últimas confirmadas.", + "codexAuth.switchFailed": "Não foi possível trocar de conta. Sua seleção anterior foi mantida.", + "codexAuth.removeConfirm": "Remover {id}?", + "codexAuth.removeFailed": "Não foi possível remover a conta. Nada foi alterado.", + "codexAuth.addTitle": "Adicionar conta Codex", + "codexAuth.addIdLabel": "ID da conta (slug)", + "codexAuth.addIdPlaceholder": "codex-work, codex-alt, team...", + "codexAuth.resetCreditsAria": "{count} crédito(s) de reset", + "codexAuth.addJsonLabel": "Conteúdo do auth.json", + "codexAuth.addHelp": "Copie do ~/.codex/auth.json de outra máquina ou use codex-auth export.", + "codexAuth.importBtn": "Importar", + "codexAuth.importInvalidJson": "JSON inválido", + "codexAuth.importMissingTokens": "access_token ou refresh_token ausente no JSON", + "codexAuth.importMissingId": "O ID da conta é obrigatório", + "codexAuth.accountAdded": "Conta adicionada ao pool", + "codexAuth.addPickDesc": "Faça login com outra conta do ChatGPT para adicioná-la ao pool.", + "codexAuth.oauthLogin": "Login OAuth", + "codexAuth.oauthDesc": "Abre o login do ChatGPT no navegador", + "codexAuth.deviceLogin": "Login por código de dispositivo", + "codexAuth.deviceDesc": "Para proxy headless ou remoto: digite um código curto em outro dispositivo", + "codexAuth.importAuthJson": "Importar auth.json", + "codexAuth.importAuthJsonDesc": "De outra instalação do Codex ou de um codex-auth export", + "codexAuth.back": "Voltar", + "codexAuth.oauthAlreadyInProgress": "Já há um login em andamento. Conclua-o no navegador.", + "codexAuth.oauthWaiting": "Aguardando a conclusão do login do ChatGPT no navegador...", + "codexAuth.oauthSubmittingCode": "Enviando código…", + "codexAuth.oauthCodeSubmitted": "Código enviado — aguardando a conclusão do login…", + "codexAuth.oauthStatusRetrying": "Erro de rede ou de proxy ao verificar o status do login — tentando novamente…", + "codexAuth.oauthCancelled": "O login foi cancelado.", + "codexAuth.loginFailed": "Falha no login", + "codexAuth.needsReauth": "Refazer login", + "codexAuth.reauthenticate": "Reautenticar", + "codexAuth.tokenExpired": "Token expirado — reautentique esta conta", + "codexAuth.mainTokenExpired": "Token expirado — refaça o login com um código de dispositivo abaixo ou pelo login do Codex App", + "codexAuth.mainReauthSucceeded": "Login realizado", + "codexAuth.mainReauthFailed": "Falha ao refazer o login", + "codexAuth.mainReauthCancel": "Cancelar", + "codexAuth.mainReauthCode": "Código", + "codexAuth.mainReauthOpen": "Abrir", + "codexAuth.mainReauthPending": "Aguardando o login…", + "codexAuth.mainReauthDevice": "Refazer login com código de dispositivo", + "codexAuth.emailCollision": "Esta conta corresponde ao seu login principal do Codex. Use outra conta.", + "codexAuth.resetCreditsTitle": "Créditos de reset", + "codexAuth.resetCreditsAvailable": "Você tem {count} crédito(s) de reset disponível(is).", + "codexAuth.resetCreditsDesc": "Cada crédito zera imediatamente seus limites de uso por hora e por semana.", + "codexAuth.noResetCredits": "Você não tem créditos de reset.", + "codexAuth.earnCreditsHint": "Os créditos são ganhos mensalmente e pelo programa de indicação.", + "codexAuth.creditsExpireNote": "Os créditos expiram 30 dias após serem ganhos.", + "codexAuth.useOneCredit": "Usar 1 crédito", + "codexAuth.confirmResetTitle": "Usar crédito de reset?", + "codexAuth.confirmResetDesc": "Isso zerará imediatamente seus limites de taxa atuais. Restam {count} crédito(s).", + "codexAuth.irreversible": "Esta ação não pode ser desfeita.", + "codexAuth.useCredit": "Usar crédito", + "codexAuth.redeeming": "Zerando...", + "codexAuth.resetSuccess": "Limites de taxa zerados! Restam {remaining} crédito(s).", + "codexAuth.resetSuccessGeneric": "Limites de taxa zerados!", + "codexAuth.resetAlreadyRedeemed": "Este crédito já foi resgatado. Créditos inalterados.", + "codexAuth.resetNothingToReset": "Nenhuma janela de limite de taxa precisa ser zerada no momento.", + "codexAuth.resetNoCredit": "Nenhum crédito de reset disponível.", + "codexAuth.resetError": "Falha ao resgatar o crédito de reset. Tente novamente.", + "codexAuth.fifoNote": "O crédito mais antigo é usado primeiro.", + "codexAuth.confirmWhichCredit": "O crédito de {date} será usado.", + "codexAuth.creditNext": "Próximo a usar", + "codexAuth.creditLabel": "Crédito nº {n}", + "codexAuth.creditNextBadge": "PRÓXIMO", + "codexAuth.creditGranted": "Concedido em {date}", + "codexAuth.creditExpires": "Expira em {date} ({days}d restantes)", + "grokCoupon.badgeAria": "{count} cupom(ns) de reset do Grok", + "grokCoupon.badgeErrorAria": "Não foi possível ler os cupons de reset do Grok", + "grokCoupon.title": "Cupons de reset do Grok", + "grokCoupon.available": "Esta conta tem {count} cupom(ns) de reset.", + "grokCoupon.desc": "Um cupom zera imediatamente a janela de uso do Grok desta conta.", + "grokCoupon.none": "Esta conta não tem cupons de reset.", + "grokCoupon.loadFailed": "Não foi possível ler os cupons de reset desta conta.", + "grokCoupon.loadFailedAuth": "Faça login novamente nesta conta xAI para ler os cupons.", + "grokCoupon.retry": "Tentar novamente", + "grokCoupon.couponLabel": "Cupom nº {n}", + "grokCoupon.couponNext": "Próximo a usar", + "grokCoupon.couponNextBadge": "PRÓXIMO", + "grokCoupon.validFrom": "Válido a partir de {date}", + "grokCoupon.expires": "Expira em {date} ({days}d restantes)", + "grokCoupon.useOne": "Usar 1 cupom", + "grokCoupon.fifoNote": "O cupom mais próximo do vencimento é usado primeiro.", + "grokCoupon.confirmTitle": "Usar cupom de reset?", + "grokCoupon.confirmDesc": "Isso zera agora a janela de uso do Grok desta conta. Você tem {count} cupom(ns).", + "grokCoupon.confirmWhich": "O cupom que expira em {date} será usado.", + "grokCoupon.irreversible": "Esta ação não pode ser desfeita.", + "grokCoupon.redeem": "Usar cupom", + "grokCoupon.redeeming": "Resgatando…", + "grokCoupon.redeemSuccess": "Cupom resgatado. Restam {count} cupom(ns).", + "grokCoupon.redeemReplayed": "Este resgate já foi concluído antes. Nada foi consumido novamente.", + "grokCoupon.redeemFailed": "Falha no resgate. Confira a lista de cupons antes de tentar novamente.", + "grokCoupon.authFailed": "Faça login novamente nesta conta xAI para usar os cupons.", + "grokCoupon.noAccount": "Nenhuma conta xAI está disponível para este resgate.", + "grokCoupon.noneAvailable": "Não há cupons de reset disponíveis para resgatar.", + "grokCoupon.identityMismatch": "Esse resgate pertence a outra conta. Feche este diálogo e comece de novo.", + "grokCoupon.networkError": "Não foi possível alcançar o proxy.", + "grokCoupon.capacity": "O registro de resgates está cheio no momento. Tente novamente em instantes.", + "grokCoupon.noOperationId": "Este navegador não consegue gerar um ID de resgate, então o resgate está desativado aqui.", + "grokCoupon.unknownOutcome": "O resgate não respondeu a tempo, então seu resultado é desconhecido. Releia a conta para ver se o cupom foi gasto.", + "grokCoupon.checkResult": "Reler conta", + "grokCoupon.checking": "Relendo…", + "grokCoupon.consumedElsewhere": "O cupom não está mais nesta conta, então o resgate foi concluído.", + "grokCoupon.unresolved": "O cupom ainda aparece na lista. O resgate pode ainda estar em andamento no upstream — releia em um minuto antes de gastar outro cupom.", + "anthropicGrant.badgeAria": "{count} reset(s) de uso do Claude", + "anthropicGrant.badgeErrorAria": "Não foi possível ler os resets de uso do Claude", + "anthropicGrant.title": "Resets de uso do Claude", + "anthropicGrant.available": "Esta conta tem {count} reset(s) de uso.", + "anthropicGrant.desc": "Um reset recompõe imediatamente os limites de 5 horas e semanais do Claude desta conta. O dia do reset semanal permanece o mesmo.", + "anthropicGrant.none": "Esta conta não tem resets de uso.", + "anthropicGrant.ineligible": "Esta conta não é elegível para resets de uso no momento.", + "anthropicGrant.loadFailed": "Não foi possível ler os resets de uso desta conta.", + "anthropicGrant.loadFailedAuth": "Faça login novamente nesta conta Anthropic para ler os resets.", + "anthropicGrant.retry": "Tentar novamente", + "anthropicGrant.resetsLeft": "{left} de {total} restante(s)", + "anthropicGrant.validFrom": "Válido a partir de {date}", + "anthropicGrant.expires": "Expira em {date} ({days}d restantes)", + "anthropicGrant.windowFiveHour": "5 horas {percent}%", + "anthropicGrant.windowWeekly": "Semanal {percent}%", + "anthropicGrant.windowOverage": "Extra {percent}%", + "anthropicGrant.paused": "Pausado", + "anthropicGrant.notUsable": "Ainda não utilizável", + "anthropicGrant.requiresLimit": "Utilizável quando esta conta atingir um limite", + "anthropicGrant.useOne": "Usar 1 reset", + "anthropicGrant.confirmTitle": "Zerar os limites de uso agora?", + "anthropicGrant.confirmDesc": "Isso recompõe agora os limites de uso do Claude desta conta e consome um reset.", + "anthropicGrant.confirmWhich": "Usa: {label}", + "anthropicGrant.irreversible": "Esta ação não pode ser desfeita.", + "anthropicGrant.redeem": "Zerar limites", + "anthropicGrant.redeeming": "Zerando…", + "anthropicGrant.resultReset": "Limites zerados. Restam {count} reset(s).", + "anthropicGrant.resultAlreadyUsed": "Este reset já foi usado.", + "anthropicGrant.resultNotLimited": "Esta conta não está em um limite, então nada foi usado.", + "anthropicGrant.resultCooldown": "Os resets estão em período de espera para esta conta. Nada foi usado.", + "anthropicGrant.resultIneligible": "Esta conta não é mais elegível. Nada foi usado.", + "anthropicGrant.resultUnavailable": "Os resets estão indisponíveis no momento. Nada foi usado.", + "anthropicGrant.resultRateLimited": "Requisições demais. Nada foi usado; tente novamente em um minuto.", + "anthropicGrant.resultAuthError": "Faça login novamente nesta conta Anthropic. Nada foi usado.", + "anthropicGrant.replayed": "Este reset já foi concluído antes. Nada foi usado novamente.", + "anthropicGrant.notUsableNow": "Este reset não pode ser usado no momento. Releia a conta.", + "anthropicGrant.inFlight": "Este reset ainda está sendo processado. Aguarde um instante e releia a conta.", + "anthropicGrant.unresolvedPrior": "Uma tentativa anterior deste reset ainda não foi confirmada. Repita essa tentativa em vez de iniciar uma nova.", + "anthropicGrant.expired": "A tentativa anterior não pôde ser confirmada a tempo. Releia a conta antes de usar um reset.", + "anthropicGrant.identityMismatch": "Esse reset pertence a outra conta. Feche este diálogo e comece de novo.", + "anthropicGrant.sessionRequired": "Abra o painel pelo login próprio dele para usar os resets.", + "anthropicGrant.journalBusy": "O registro de resets está ocupado ou indisponível. Nada foi usado; tente novamente em instantes.", + "anthropicGrant.failed": "A requisição de reset falhou. Verifique a conta antes de tentar novamente.", + "anthropicGrant.noOperationId": "Este navegador não consegue gerar um ID de requisição, então os resets estão desativados aqui.", + "anthropicGrant.unknownOutcome": "O reset não respondeu a tempo, então seu resultado é desconhecido. Tentar de novo envia o mesmo ID de requisição, portanto não pode iniciar um segundo reset.", + "anthropicGrant.retrySame": "Repetir a mesma requisição", + "anthropicGrant.checkResult": "Reler conta", + "anthropicGrant.checking": "Relendo…", + "api.title": "Acesso à API", + "api.subtitle": "Use chaves de API geradas para acessar o proxy opencodex a partir de apps externos. As chaves se autenticam pelo cabeçalho {authHeader}; veja na tabela abaixo o que cada endpoint aceita.", + "api.baseUrl": "URL base", + "api.responsesEndpoint": "API Responses", + "api.chatCompletionsEndpoint": "API Chat Completions", + "api.messagesEndpoint": "API Messages", + "api.modelsEndpoint": "API Models", + "api.surface.on": "Ativado", + "api.surface.off": "Desativado", + "api.surface.source.fixed": "Sempre disponível", + "api.surface.source.explicit": "Configuração explícita", + "api.surface.source.inherited": "Herdado das configurações do Claude", + "api.surface.source.invalid": "Valor inválido — fechado", + "api.surface.messagesToggle": "Disponibilizar a API Messages", + "api.surface.messagesCloseNote": "Desativar o Messages também desativa a integração com o Claude, para que uma versão mais antiga do opencodex mantenha o endpoint fechado também.", + "api.surface.openClaude": "Configurações do Claude", + "api.surface.toggleFailed": "Não foi possível salvar a configuração do Messages.", + "api.surface.toggleUnavailable": "Esta versão do proxy não permite alterar a API Messages aqui. Use a página do Claude.", + "api.surface.closedNote": "As requisições a este endpoint são recusadas com 403.", + "api.endpointNote": "Use a URL base com clientes compatíveis com OpenAI. Responses e Chat Completions são expostos em /v1.", + "api.endpointsTitle": "Endpoints", + "api.authTitle": "Autenticação", + "api.authLoopback": "O acesso por loopback depende da rota; clientes de áudio independentes exigem uma chave de dados do OpenCodex. Binds remotos exigem uma chave de dados ou OPENCODEX_API_AUTH_TOKEN.", + "api.authBaseUrlNote": "Configure os clientes com a URL base e depois escolha o endpoint específico do protocolo abaixo.", + "api.newKeyTitle": "Nova chave criada", + "api.newKeyNote": "Copie esta chave agora — ela não será exibida novamente.", + "api.copy": "Copiar", + "api.copied": "Copiado", + "api.dismiss": "Dispensar", + "api.generateTitle": "Gerar chave", + "api.keyNamePlaceholder": "Nome da chave (opcional)", + "api.generate": "Gerar", + "api.generating": "Criando…", + "api.activeKeys": "Chaves ativas ({count})", + "api.activeKeysLoading": "Chaves ativas", + "api.noKeys": "Nenhuma chave de API ainda. Gere uma acima.", + "api.workspace.sections": "Seções da API", + "api.section.keys": "Chaves", + "api.section.connect": "Conectar", + "api.section.endpoints": "Endpoints", + "api.section.models": "Modelos", + "api.section.examples": "Exemplos", + "api.section.plan": "Prévia do caminho", + "api.plan.title": "Prévia do caminho da requisição", + "api.plan.description": "Mostra o caminho que uma requisição para este modelo percorreria pelo proxy. A prévia não envia nada ao upstream e não tem custo.", + "api.plan.modeNote": "Nativo descreve como a requisição é entregue. Não é um resultado de verificação.", + "api.plan.unavailable": "Esta versão do proxy não oferece prévias de caminho.", + "api.plan.noModels": "Ainda não há modelos para pré-visualizar.", + "api.plan.model": "Modelo", + "api.plan.inbound": "API do cliente", + "api.plan.features": "Recursos da requisição", + "api.plan.preview": "Pré-visualizar", + "api.plan.previewing": "Pré-visualizando…", + "api.plan.failed": "Não foi possível calcular a prévia.", + "api.plan.overallMode": "Entrega", + "api.plan.routeKind": "Rota", + "api.plan.reasons": "Motivos", + "api.plan.guaranteed": "Garantido por todos os candidatos", + "api.plan.partial": "Apenas alguns candidatos", + "api.plan.policyRevision": "Revisão da política", + "api.plan.noCandidates": "Nenhuma rota corresponde a este modelo.", + "api.plan.ineligible": "Recusado pela política atual", + "api.plan.requestPath": "Caminho da requisição", + "api.plan.responsePath": "Caminho da resposta", + "api.plan.fidelity": "Fidelidade", + "api.plan.none": "Nenhum", + "api.plan.noPath": "Sem caminho", + "api.plan.noFeatures": "Nenhum recurso de requisição selecionado.", + "api.plan.mode.native": "Nativo", + "api.plan.mode.translated": "Traduzido", + "api.plan.mode.legacyBridge": "Ponte legada", + "api.plan.mode.blocked": "Bloqueado", + "api.plan.fidelity.preserved": "Preservada", + "api.plan.fidelity.degraded": "Degradada", + "api.plan.fidelity.unknown": "Desconhecida", + "api.plan.routeKind.direct": "Direta", + "api.plan.routeKind.combo": "Combo", + "api.plan.routeKind.policy": "Política de roteamento", + "api.plan.routeKind.unknown": "Modelo desconhecido", + "api.plan.disposition.passthrough": "Repassado", + "api.plan.disposition.translated": "Traduzido", + "api.plan.disposition.degraded": "Degradado", + "api.plan.disposition.unsupported": "Descartado", + "api.plan.disposition.unknown": "Desconhecido", + "api.workspace.details": "Detalhes da chave de API", + "api.workspace.keyDetails": "Detalhes da chave", + "api.workspace.keyPrefix": "Prefixo da chave", + "api.workspace.deleteKey": "Excluir chave", + "api.workspace.deleteConfirm": "Tem certeza de que deseja excluir esta chave? Isso não pode ser desfeito.", + "api.workspace.usageExamples": "Exemplos de uso", + "api.copyUrlHint": "Clique para copiar a URL", + "api.urlCopied": "URL copiada", + "api.copyExampleHint": "Clique para copiar o exemplo", + "api.exampleCopied": "Exemplo copiado", + "api.colName": "Nome", + "api.colKey": "Chave", + "api.colCreated": "Criada em", + "api.confirm": "Confirmar", + "api.deleteAria": "Excluir chave de API", + "api.modelsTitle": "Catálogo de modelos externos", + "api.modelsCount": "{count} chamável(is)", + "api.modelsLoading": "Carregando modelos…", + "api.modelsSearch": "Buscar modelos", + "api.modelsSubtitle": "Use exatamente estes IDs de modelo com /v1/models e o protocolo de entrada escolhido.", + "api.modelsEmpty": "Ainda não há modelos chamáveis externamente.", + "api.modelsNoMatch": "Nenhum modelo corresponde a “{query}”.", + "api.modelsLoadFailed": "Não foi possível carregar o catálogo de modelos externos.", + "api.colModel": "Modelo", + "api.colSource": "Origem", + "api.colProtocols": "Protocolos", + "api.sourceNative": "Pool do ChatGPT", + "api.sourceCombo": "Rota combo", + "api.sourceCustom": "Personalizado", + "api.protocolResponses": "Responses", + "api.protocolChatCompletions": "Chat Completions", + "api.protocolMessages": "Messages", + "api.copyModelId": "Copiar ID", + "api.modelCopied": "Copiado", + "api.testModel": "Testar", + "api.testingModel": "Testando…", + "api.testSucceeded": "OK", + "api.testFailed": "Falhou", + "api.usageChatTitle": "Exemplo de Chat Completions", + "api.usageResponsesTitle": "Exemplo de Responses", + "api.usageMessagesTitle": "Exemplo de Messages", + "api.usageSampleInput": "Olá, mundo!", + "api.clientConfig.title": "Configuração do cliente", + "api.clientConfig.rowsLabel": "Conectar um cliente", + "api.clientConfig.details": "Detalhes", + "api.clientConfig.detailsAria": "Detalhes da configuração de {client}", + "api.clientConfig.copyAria": "Copiar a configuração de {client}", + "api.clientConfig.downloadAria": "Baixar a configuração de {client}", + "api.clientConfig.rowMeta": "{destination} · {count} modelo(s)", + "api.clientConfig.rowError": "Não foi possível gerar a configuração de {client}.", + "api.clientConfig.copiedAnnounceClient": "Configuração de {client} copiada para a área de transferência.", + "api.clientConfig.clientOpencode": "OpenCode", + "api.clientConfig.clientPi": "Pi", + "api.clientConfig.clientOmp": "OMP", + "api.clientConfig.clientHermes": "Hermes", + "api.clientConfig.clientOpenclaw": "OpenClaw", + "api.clientConfig.clientKimi": "Kimi Code", + "api.clientConfig.clientGajae": "gjc", + "api.clientConfig.clientDsh": "DeepSeek Harness (DSH)", + "api.clientConfig.clientMcode": "MiniMax Code", + "api.clientConfig.clientZcode": "ZCode", + "api.clientConfig.clientPrime": "Prime Agent", + "api.clientConfig.clientAside": "Aside", + "api.clientConfig.clientRaycast": "Raycast", + "api.clientConfig.clientOmo": "omo", + "api.clientConfig.clientCline": "Cline CLI", + "api.clientConfig.clientKilo": "Kilo", + "api.clientConfig.clientDroid": "Factory Droid", + "api.clientConfig.clineBundle": "Este é um pacote de dois arquivos, não um arquivo de configurações do Cline. Mescle settings em providers.json e catalog no models.json vizinho. Use o interruptor de integração para uma gravação com backup.", + "api.clientConfig.clineDownloaded": "{filename} baixado. Nada foi alterado ainda; settings e catalog pertencem a dois arquivos separados do Cline.", + "api.clientConfig.copy": "Copiar configuração", + "api.clientConfig.download": "Baixar", + "api.clientConfig.loading": "Gerando configuração do cliente…", + "api.clientConfig.jsonLabel": "Configuração de {client}", + "api.clientConfig.destination": "Arquivo de destino", + "api.clientConfig.envHint": "Defina a chave antes de iniciar", + "api.clientConfig.mergeWarning": "Mescle isto no arquivo de destino. Substituí-lo apagaria seus outros provedores e configurações de MCP.", + "api.clientConfig.modelCount": "{count} modelo(s) exportado(s)", + "api.clientConfig.missingLimits": "{count} de {total} modelo(s) saem sem limite de contexto; o cliente aplica seus próprios padrões.", + "api.clientConfig.noKeyYet": "{env} ainda não tem nenhuma chave por trás. Gere uma chave acima antes de usar esta configuração fora do loopback.", + "api.clientConfig.loadFailed": "Não foi possível ler a lista de modelos, então nenhuma configuração de cliente foi gerada.", + "api.clientConfig.copiedAnnounce": "Configuração do cliente copiada para a área de transferência.", + "api.clientConfig.copyFailed": "Não foi possível copiar a configuração do cliente.", + "api.clientConfig.downloadedAnnounce": "{filename} baixado. Nada foi alterado ainda — mescle-o você mesmo em {destination}.", + "api.clientConfig.whereDisclosure": "Onde este arquivo vai", + "api.clientConfig.whereBody": "O destino acima é o caminho global. Um arquivo de configuração local do projeto, no diretório de trabalho, tem precedência sobre ele, e o cliente lê a chave da variável de ambiente indicada na configuração — nunca deste arquivo.", + "api.keysLoadFailed": "Não foi possível carregar as chaves de API.", + "api.createFailed": "Não foi possível criar a chave de API.", + "api.deleteFailed": "Não foi possível excluir a chave de API.", + "api.auth.endpoint": "Endpoint", + "api.auth.required": "Obrigatória", + "api.auth.accepted": "Aceita", + "api.auth.rejected": "Não aceita", + "api.auth.testProtocol": "Testar {protocol}", + "api.auth.testNeedsFreshKey": "Gere uma chave e mantenha seu valor único na tela para executar um teste autenticado.", + "api.key.name": "Nome da chave", + "api.key.rename": "Renomear", + "api.key.saveName": "Salvar nome", + "api.key.renaming": "Salvando…", + "api.key.renameFailed": "Não foi possível renomear a chave. Seu rascunho foi mantido.", + "api.key.deleting": "Excluindo…", + "api.rotation.title": "Rotação de chave", + "api.rotation.description": "Emita uma chave substituta enquanto a chave atual continua válida por um curto período de sobreposição.", + "api.rotation.start": "Iniciar rotação", + "api.rotation.starting": "Iniciando…", + "api.rotation.pending": "A rotação está pendente. Atualize e verifique o cliente antes de confirmar.", + "api.rotation.expires": "A sobreposição expira em:", + "api.rotation.secretOnce": "Chave substituta — exibida uma única vez. Copie-a antes de fechar este aviso.", + "api.rotation.commit": "Confirmar rotação", + "api.rotation.abort": "Cancelar rotação", + "api.rotation.failed": "A ação de rotação não foi concluída. Atualize a página antes de tentar novamente.", + "api.rotation.startFailed": "Não foi possível iniciar a rotação da chave.", + "api.key.copyFailed": "Não foi possível copiar a chave. Selecione-a e copie manualmente antes de fechar este painel.", + "api.attribution.title": "Uso atribuído", + "api.attribution.requests7d": "Requisições, últimos 7 dias", + "api.attribution.totalRequests": "Total de requisições atribuídas", + "api.attribution.totalRequestsAvailable": "Requisições no histórico disponível", + "api.attribution.sinceAvailable": "Atribuição disponível desde", + "api.attribution.lastUsed": "Último uso", + "api.attribution.since": "Atribuição disponível desde", + "api.attribution.neverUsed": "Não usada desde o início da atribuição", + "api.attribution.unavailable": "Uso indisponível", + "api.attribution.unavailableDetail": "Nenhum uso foi atribuído ainda. As requisições registradas antes do início da atribuição não podem ser atribuídas retroativamente.", + "api.attribution.ambiguous": "Duas chaves compartilham este ID, então o uso não pode ser atribuído a nenhuma delas. Dê a cada chave um ID exclusivo no arquivo de configuração.", + "api.attribution.railAmbiguous": "ID duplicado", + "claude.subtitle": "Use GPT, Gemini e outros modelos dentro do Claude Code.", + "claude.pageTitle": "Claude Code", + "claude.workspace.settings": "Geral", + "claude.enabledLabel": "Conexão com o Claude", + "claude.enabledHint": "Quando desativada, o Claude Code não consegue usar este proxy.", + "claude.authMode": "Modo de autenticação", + "claude.authModeHint": "Assinatura exige conta Claude; Proxy funciona sem conta Anthropic", + "claude.authModeSubscription": "Assinatura (conta Claude)", + "claude.authModeProxy": "Proxy (sem necessidade de conta)", + "claude.authModeAuto": "Automático (detectar autenticação do Claude)", + "claude.effectiveMode.label": "Vale a partir da próxima inicialização", + "claude.effectiveMode.manual": "Manual: {mode}", + "claude.effectiveMode.autoPresent": "Automático: assinatura — autenticação do Claude encontrada via {source}", + "claude.effectiveMode.autoAbsent": "Automático: modo proxy — nenhuma autenticação do Claude encontrada", + "claude.effectiveMode.autoUnknown": "Automático: assinatura — não foi possível verificar a autenticação", + "claude.effectiveMode.admissionKey": "A chave de API deste proxy continua sendo enviada.", + "claude.authSource.claude-json-oauth": "conta Claude", + "claude.authSource.claude-credentials-file": "arquivo de credenciais", + "claude.authSource.macos-keychain": "Keychain do macOS", + "claude.authSource.exported-env": "variável de ambiente", + "claude.authSource.unknown": "uma credencial detectada", + "claude.systemEnv": "Conexão automática", + "claude.systemEnvDesc": "Quando ativada, executar claude em qualquer terminal passa automaticamente pelo proxy.", + "claude.systemEnvUnsupported": "A conexão automática está disponível apenas no macOS. Neste sistema, inicie o Claude com {cmd}.", + "claude.systemEnvWarn": "⚠ É preciso fechar totalmente e reabrir o aplicativo de terminal para que isto tenha efeito. Não recomendado.", + "claude.fastMode": "Modo rápido (OpenAI)", + "claude.fastModeDesc": "Controla o service_tier dos modelos OpenAI. ON = priority (mais rápido). OFF = default. Auto = repasse (o cliente decide).", + "claude.fastAuto": "Auto", + "claude.fastOn": "ON", + "claude.fastOff": "OFF", + "claude.autoContext": "Usar contexto grande automaticamente", + "claude.autoContextDesc": "Controla até onde vai a marcação de 1M. ON: qualquer modelo cuja janela comporte o limite de compactação ganha uma linha de contexto grande. OFF: só os modelos de 1M de verdade ganham uma.", + "claude.autoContextInert": "Inativo porque existe um valor legado de tamanho de contexto (maxContextTokens) no arquivo de configuração. Remova-o de lá para reativar.", + "claude.autoCompactWindow": "Ponto de resumo automático", + "claude.autoCompactDefault": "{value} (padrão)", + "claude.autoCompactWindowDesc": "As mensagens mais antigas são resumidas quando a conversa chega a este ponto. Ele nunca excede o limite de cada modelo, então modelos de 200k não são afetados.", + "claude.autoCompactWindowWarn": "Alterar isto pode quebrar modelos GPT — se for definido acima do limite real do modelo, as conversas darão erro antes de o resumo entrar em ação.", + "claude.injectAgents": "Registrar subagentes automaticamente", + "claude.injectAgentsDesc": "Registra os modelos escolhidos na aba Subagentes (mais o modelo padrão atual) como agentes despacháveis do Claude Code (ocx-*). Vale a partir da próxima sessão.", + "claude.webSearchSidecar": "Substituição do sidecar de busca na web", + "claude.webSearchSidecarHint": "Substitui o sidecar principal de busca na web nas requisições do Claude Code.", + "claude.visionSidecar": "Substituição do sidecar de visão", + "claude.visionSidecarHint": "Substitui o sidecar principal de visão nas requisições do Claude Code.", + "claude.useMainSetting": "Usar configuração principal", + "claude.sidecarModelPlaceholder": "Modelo da configuração principal", + "claude.quickstart": "Primeiros passos", + "claude.quickstartHint": "{cmd} abre o Claude Code por meio do proxy. Seu login no claude.ai continua ativo.", + "claude.manualEnv": "Configuração manual (avançado)", + "claude.smallFastModel": "Modelo auxiliar em segundo plano", + "claude.smallFastModelHint": "O modelo que o Claude Code usa em tarefas de segundo plano, como resumos de conversa e detecção de tópico. O alias de subagente haiku também o utiliza. Vazio = padrão do Claude (Haiku).", + "claude.smallFastModelAccurateHint": "O modelo que o Claude Code usa em tarefas de segundo plano, como resumos de conversa e detecção de tópico. O alias de subagente haiku também o utiliza.", + "claude.smallFastModelUnsetOption": "Deixar o Claude Code escolher (modelo nativo)", + "claude.smallFastModelNativeWarning": "Quando não definido, o OpenCodex deixa sem definir as substituições do modelo auxiliar. O Claude Code pode usar seu modelo Sonnet nativo, o que pode gerar cobranças do seu provedor nativo.", + "claude.slotUnset": "Usar padrão do Claude", + "claude.modelMap": "Interceptação de modelo", + "claude.modelMapHint": "Intercepta requisições de um modelo específico e as redireciona para o modelo que você escolher. Vazio por padrão: nada acontece até você adicionar uma regra.", + "claude.mapFrom": "Modelo original (ex.: claude-sonnet-4-5)", + "claude.mapTo": "Trocar por (ex.: gemini/gemini-3-pro)", + "claude.addMapping": "Adicionar regra", + "claude.removeMapping": "Remover regra", + "claude.aliases": "Modelos disponíveis", + "claude.aliasesHint": "Modelos que aparecem no menu /model do Claude Code.", + "claude.aliasProviderOther": "Outros", + "claude.loading": "Carregando…", + "claude.loadFail": "Falha ao carregar as configurações do Claude", + "claude.saved": "Salvo.", + "claude.saveFailed": "Falha ao salvar", + "claude.networkError": "Erro de rede — o proxy está em execução?", + "claude.toggleAria": "Alternar conexão com o Claude", + "claude.none": "Nenhum", + "cws.loading": "Carregando combos…", + "cws.loadFailed": "Não foi possível carregar os combos.", + "cws.saveFailed": "Não foi possível salvar o combo.", + "cws.removeFailed": "Não foi possível remover o combo.", + "cws.saved": "Combo salvo.", + "cws.created": "{model} criado.", + "cws.removed": "combo/{id} removido.", + "cws.renamed": "{from} renomeado para {to}.", + "cws.add": "Adicionar combo", + "cws.addTitle": "Adicionar combo", + "cws.addSubtitle": "Crie um modelo virtual entre provedores e escolha o nome exato do modelo que os clientes vão solicitar.", + "cws.create": "Criar combo", + "cws.jev.create": "Criar JEV Auto", + "cws.jev.exists": "O JEV Auto já existe.", + "cws.jev.setupHint": "Crie um Combo opcional e totalmente editável. O JEV escolhe um destino permitido e um esforço de raciocínio para cada requisição.", + "cws.jev.decisionService": "Serviço de decisão", + "cws.jev.decisionServiceDefault": "TypeSafe JEV (padrão)", + "cws.err.invalidDecisionProvider": "O serviço de decisão {name} não pode ser usado ({reason}). Escolha outro serviço ou o TypeSafe JEV.", + "cws.jev.decisionIssue.missing": "não configurado", + "cws.jev.decisionIssue.notDecision": "não é um serviço de decisão", + "cws.jev.decisionIssue.disabled": "desativado", + "cws.jev.decisionIssue.endpoint": "a URL base deve terminar em /systemone", + "cws.jev.decisionIssue.model": "sem modelo padrão", + "cws.jev.decisionServiceDefaultHint": "O serviço de decisão TypeSafe hospedado, autenticado com a chave do provedor jev.", + "cws.jev.decisionServiceHint": "Serviço de decisão auto-hospedado. Apenas a chave de API do próprio provedor é enviada ao seu endpoint:", + "cws.jev.decisionServicesTitle": "Serviços de decisão do JEV", + "cws.jev.decisionTimeout": "Timeout da decisão (ms)", + "cws.jev.decisionTimeoutHint": "Deixe vazio para o padrão ({default} ms); permitido de {min} a {max} ms. Quando expira, o JEV recorre ao primeiro destino elegível. Aumente para modelos auto-hospedados lentos ou frios.", + "cws.jev.decisionTimeoutShort": "Timeout de {ms} ms", + "cws.jev.decisionTimeoutDefaultShort": "Timeout padrão", + "cws.jev.decisionMethod": "Método de decisão", + "cws.jev.method.typesafe": "TypeSafe JEV (padrão)", + "cws.jev.method.systemone": "Servidor compatível com System One", + "cws.jev.method.model": "modelo do opencodex", + "cws.jev.method.systemoneEmpty": "Nenhum servidor System One está configurado. Adicione um provedor com o adaptador jev-decision cuja URL base termine em /systemone, por exemplo um servidor Ollama executando o tev1.", + "cws.jev.decisionModel": "Modelo de decisão", + "cws.jev.decisionModelHint": "Qualquer modelo que o opencodex roteie. Ele recebe o estado de decisão limitado e as opções permitidas e deve responder com uma chave de opção em JSON. Usa as credenciais armazenadas em seu próprio provedor, nunca as do chamador, e não pode ser este combo nem outro combo JEV.", + "cws.err.invalidDecisionModel": "Escolha um modelo de decisão que o opencodex consiga rotear. Ele não pode ser este combo nem outro combo JEV.", + "cws.jev.test": "Testar", + "cws.jev.testing": "Testando…", + "cws.jev.testHint": "Envia uma tarefa sintética com opções de exemplo ao método selecionado. Nada é salvo.", + "cws.jev.testOk": "Respondeu em {ms} ms via {backend}.", + "cws.jev.testFailOpen": "Falhou em modo aberto ({gate}) após {ms} ms. As requisições usariam o primeiro destino elegível.", + "cws.jev.testError": "Não foi possível executar o teste: {error}", + "cws.jev.backend.typesafe": "TypeSafe", + "cws.jev.backend.systemone": "Servidor System One", + "cws.jev.backend.model": "modelo do opencodex", + "cws.jev.backend.unknown": "Desconhecido (registros antigos)", + "cws.jev.stats.backends": "Decisões por método", + "cws.jev.stats.backend": "Método", + "cws.jev.stats.applied": "Aplicadas", + "cws.jev.stats.latency": "Latência média", + "cws.err.invalidDecisionTimeout": "O timeout da decisão deve ser um inteiro de {min} a {max} ms, ou vazio para o padrão.", + "cws.jev.failOpen": "Destino de fail-open", + "cws.jev.modelProfile": "Notas adicionais de modelo para o JEV", + "cws.jev.modelProfilePlaceholder": "Opcional: deixe em branco para nenhuma nota adicional.", + "cws.jev.modelProfileHint": "Notas opcionais que complementam (nunca substituem) o perfil padrão integrado: capacidade, contexto e custo relativo da assinatura. Enviadas ao serviço de decisão selecionado a cada decisão do JEV.", + "cws.err.invalidModelProfile": "As notas de modelo devem ter no máximo 512 caracteres; quebras de linha e tabulações são os únicos caracteres de controle permitidos.", + "cws.jev.allowedEfforts": "O JEV pode selecionar", + "cws.jev.efforts": "Esforços de raciocínio: {efforts}", + "cws.jev.effortsUnknown": "Esforços de raciocínio não informados", + "cws.jev.effortsNone": "Sem esforço de raciocínio explícito", + "cws.jev.stats.tab": "Estatísticas", + "cws.jev.stats.range": "Período das estatísticas do JEV", + "cws.jev.stats.refresh": "Atualizar", + "cws.jev.stats.loading": "Carregando estatísticas do JEV", + "cws.jev.stats.loadFailed": "Não foi possível carregar as estatísticas do JEV.", + "cws.jev.stats.historyIncomplete": "Alguns registros antigos do JEV foram omitidos, então estes totais podem estar incompletos.", + "cws.jev.stats.emptyTitle": "Ainda não há decisões do JEV", + "cws.jev.stats.emptyBody": "Execute este combo para registrar as escolhas e o uso de tokens dos modelos. Decisões tomadas antes da instalação do suporte a estatísticas não estão disponíveis.", + "cws.jev.stats.decisions": "Decisões", + "cws.jev.stats.appliedAndFailOpen": "{applied} aplicadas · {failOpen} em fail-open", + "cws.jev.stats.modelTokens": "Tokens do modelo", + "cws.jev.stats.decisionTokens": "Tokens de decisão do JEV", + "cws.jev.stats.measuredAttempts": "{measured}/{total} tentativas medidas", + "cws.jev.stats.measuredDecisions": "{measured}/{total} decisões informaram uso", + "cws.jev.stats.successful": "{count} requisições bem-sucedidas", + "cws.jev.stats.fallbackOne": "{count} fallback de modelo", + "cws.jev.stats.fallbackMany": "{count} fallbacks de modelo", + "cws.jev.stats.averageLatency": "Latência média do JEV: {value}", + "cws.jev.stats.averageConfidence": "Confiança média: {value}", + "cws.jev.stats.gates": "Portões de decisão do JEV", + "cws.jev.stats.model": "Modelo", + "cws.jev.stats.otherModels": "Outros modelos", + "cws.jev.stats.picks": "Escolhas", + "cws.jev.stats.efforts": "Esforços", + "cws.jev.stats.attempts": "Tentativas", + "cws.jev.stats.input": "Entrada", + "cws.jev.stats.output": "Saída", + "cws.jev.stats.reasoning": "Raciocínio", + "cws.jev.stats.cacheReadWrite": "Cache L/E", + "cws.jev.stats.total": "Total", + "cws.jev.stats.noEffort": "nenhum", + "cws.jev.stats.measuredShort": "medido", + "cws.jev.stats.tokenFootnote": "Os tokens do modelo vêm das tentativas físicas nos destinos, incluindo novas tentativas e fallbacks. Os tokens de decisão do JEV são informados separadamente pelo serviço de decisão.", + "cws.railAria": "Lista de combos", + "cws.searchPlaceholder": "Buscar combos ou destinos…", + "cws.noSearchResults": "Nenhum combo corresponde à busca.", + "cws.group.failover": "Failover", + "cws.group.roundRobin": "Round-robin", + "cws.group.other": "Outras estratégias", + "cws.targetCount": "{count} destinos", + "cws.targetCountOne": "1 destino", + "cws.overviewTitle": "Combos", + "cws.overviewBlurb": "Modelos virtuais que roteiam entre destinos provedor/modelo com failover, round-robin, sorteio ponderado, menos usado ou reinício de cota mais próximo.", + "cws.count.total": "Total", + "cws.count.failover": "Failover", + "cws.count.roundRobin": "Round-robin", + "cws.count.other": "Outros", + "cws.howTitle": "Como funciona", + "cws.howBody": "Peça ao Codex o nome público do modelo do combo. Se não houver um, o padrão é combo/. O OpenCodex seleciona um destino e só passa ao próximo em falhas recuperáveis do upstream. Se nenhum destino estiver disponível, a requisição falha em modo fechado em vez de usar o provedor padrão global.", + "cws.attentionTitle": "Requer atenção", + "cws.attention.empty": "Nenhum destino configurado", + "cws.attention.few": "Apenas um destino — o failover não tem para onde ir", + "cws.attention.catalogOmitted": "Ausente do catálogo de modelos — as capacidades dos membros estão incompletas ou incompatíveis (janela de contexto/metadados ausentes, ou interseção de modalidades vazia). O roteamento por alias continua funcionando", + "cws.attention.allTargetsExhausted": "Todos os destinos habilitados estão sem cota", + "cws.emptyTitle": "Crie seu primeiro combo", + "cws.empty.createDesc": "Dê nome a um modelo virtual e encadeie dois ou mais backends.", + "cws.backToAll": "Voltar para todos os combos", + "cws.allCombos": "Todos os combos", + "cws.copyModel": "Copiar id", + "cws.copied": "Copiado", + "cws.tabsLabel": "Seções de detalhe do combo", + "cws.tab.config": "Configuração", + "cws.tab.about": "Sobre", + "cws.strategy": "Estratégia", + "cws.strategy.failover": "Failover", + "cws.strategy.roundRobin": "Round-robin", + "cws.strategy.random": "Aleatória", + "cws.strategy.leastUsed": "Menos usado", + "cws.strategy.resetWindow": "Janela de reinício", + "cws.strategy.jev": "JEV", + "cws.strategy.failoverHint": "Tenta os destinos em ordem. Se o primeiro falhar com um erro recuperável (limite de taxa, indisponibilidade, bloqueio de assinatura), passa ao próximo.", + "cws.strategy.roundRobinHint": "Distribui o tráfego de forma determinística por peso. Mantém cada destino selecionado por um lote de requisições bem-sucedidas e depois avança.", + "cws.strategy.randomHint": "Sorteia um destino elegível por requisição, com probabilidade proporcional ao peso. Sem afinidade entre requisições.", + "cws.strategy.leastUsedHint": "Encaminha cada requisição ao destino elegível com menos sucessos registrados. As contagens reiniciam com o proxy.", + "cws.strategy.resetWindowHint": "Prefere o destino elegível cuja janela de cota reinicia primeiro. Recorre à ordem da configuração quando faltam dados de cota.", + "cws.strategy.jevHint": "Pede ao serviço de decisão JEV selecionado que escolha um destino elegível e um esforço compatível. Se a decisão estiver indisponível, usa o primeiro destino elegível.", + "cws.field.id": "Id do combo", + "cws.field.idHint": "Os clientes solicitarão {model}", + "cws.field.idInternalHint": "Id interno do combo. Você pode alterá-lo após a criação.", + "cws.field.idHintEdit": "Renomear move o combo para um novo id. Os clientes solicitam {model}.", + "cws.field.alias": "Nome público do modelo", + "cws.field.aliasPlaceholder": "deepseek-v4-flash ou fornecedor/modelo", + "cws.field.aliasHint": "Opcional. Use um nome simples sem prefixo, um prefixo personalizado como fornecedor/modelo, ou deixe em branco para usar combo/.", + "cws.field.nativeAlias": "Alias nativo da OpenAI", + "cws.field.nativeAliasHint": "Permite que este combo assuma um id de modelo nativo da OpenAI sem qualificador e compatível. As rotas da OpenAI qualificadas por conta e por provedor permanecem separadas.", + "cws.field.displayName": "Nome de exibição", + "cws.field.displayNameHint": "Rótulo deste combo no seletor. Obrigatório quando o alias nativo da OpenAI está habilitado.", + "cws.field.stickyLimit": "Sucessos fixos antes de alternar", + "cws.field.stickyLimitHint": "Mantém o destino selecionado por este número de requisições bem-sucedidas antes de o seletor ponderado avançar.", + "cws.field.defaultEffort": "Raciocínio padrão", + "cws.field.defaultEffortNone": "Nenhum (padrão do destino)", + "cws.field.defaultEffortHint": "Usado somente quando o cliente omite o esforço de raciocínio. As opções são a interseção dos esforços informados pelos destinos selecionados; destinos sem metadados de esforço no catálogo não oferecem nenhuma.", + "cws.capability.imageInputUnavailable": "Indisponível até que todos os destinos selecionados aceitem entrada de imagem.", + "cws.capability.imageInputHint": "Ativado por padrão quando todos os destinos aceitam imagens. Desative para aceitar somente texto.", + "cws.capability.imageInput": "Imagem / multimodal", + "cws.capability.adaptiveEffort": "Escala de raciocínio adaptativa", + "cws.capability.adaptiveEffortHint": "Desativado: um destino sem controle de raciocínio oculta o seletor de esforço para o combo inteiro. Ativado: esses destinos continuam utilizáveis e o seletor mantém os níveis compartilhados pelos demais destinos.", + "cws.capabilities": "Capacidades", + "cws.field.defaultEffortUnsupported": "Este esforço não está na escala comum dos destinos — será ignorado ou ajustado no momento da requisição.", + "cws.field.defaultEffortUnsupportedOption": "fora da interseção", + "cws.targets": "Destinos", + "cws.targets.failoverHint": "A ordem importa — o primeiro é o principal.", + "cws.targets.roundRobinHint": "Os pesos controlam a seleção relativa determinística; a ordem desempata no anel de rotação.", + "cws.targets.randomHint": "Os pesos controlam a probabilidade de cada sorteio; a ordem não importa.", + "cws.targets.leastUsedHint": "A ordem só desempata entre destinos igualmente usados.", + "cws.targets.resetWindowHint": "A ordem vale quando faltam dados de cota ou há empate.", + "cws.targets.jevHint": "Somente estes destinos podem ser selecionados. A ordem define o destino de fail-open e o fallback posterior do Combo.", + "cws.target.provider": "Provedor", + "cws.target.model": "Modelo", + "cws.target.weight": "Peso", + "cws.target.pickProvider": "Selecione o provedor…", + "cws.target.pickProviderFirst": "Selecione um provedor primeiro…", + "cws.target.pickModel": "Selecione o modelo…", + "cws.target.noModels": "Nenhum modelo para este provedor", + "cws.target.modelPlaceholder": "id do modelo", + "cws.target.add": "Adicionar destino", + "cws.target.drag": "Arraste para reordenar", + "cws.target.moveUp": "Mover para cima", + "cws.target.moveDown": "Mover para baixo", + "cws.quota.available": "Disponível", + "cws.quota.exhausted": "Sem cota", + "cws.quota.unknown": "Cota desconhecida", + "cws.quota.allExhausted": "Todos os destinos habilitados estão sem cota. Escolha outro destino ou aguarde a recuperação da cota.", + "cws.aboutTitle": "Execução", + "cws.aboutBody": "Destinos que falham entram em pausa breve, respeitando o Retry-After. Erros de requisição inválida ou de contexto não passam ao próximo destino. Cada destino adapta o esforço às próprias capacidades; combos esgotados falham em modo fechado. Logs e Uso mantêm as tentativas físicas em ordem e o uso por tentativa.", + "cws.removeConfirmTitle": "Remover {model}?", + "cws.removeConfirmDesc": "Isso remove o modelo virtual da configuração e do catálogo do Codex. Nenhum provedor é excluído.", + "cws.unsavedTitle": "Alterações não salvas", + "cws.unsavedDesc": "Descartar as edições deste combo e continuar?", + "cws.keepEditing": "Continuar editando", + "cws.err.missingId": "O id do combo é obrigatório.", + "cws.err.invalidId": "O id deve começar com letra ou número e usar apenas letras, números, pontos, sublinhados ou hifens (máx. 64).", + "cws.err.duplicateId": "Já existe um combo com este id.", + "cws.err.invalidAlias": "O alias deve usar letras, números, pontos, sublinhados ou hifens, com no máximo um segmento \"/\".", + "cws.err.aliasReservedNamespace": "O alias não pode usar o namespace reservado \"combo/\".", + "cws.err.aliasNativeFamily": "Aliases simples da família nativa da OpenAI (gpt-*, o1-*, o3-*, o4-*, codex-*) não são permitidos.", + "cws.err.unsupportedNativeAlias": "O alias nativo deve ser um id de modelo simples da OpenAI atualmente compatível.", + "cws.err.missingNativeAliasDisplayName": "É necessário um nome de exibição para aliases nativos.", + "cws.err.invalidDisplayName": "O nome de exibição deve ter no máximo 128 caracteres e não conter caracteres de controle.", + "cws.err.duplicateAlias": "Outro combo já usa este alias.", + "cws.err.noTargets": "Adicione pelo menos um destino.", + "cws.err.incompleteTarget": "Cada destino precisa de um provedor e um modelo.", + "cws.target.disabled": "{name} (desabilitado)", + "cws.err.reservedNamespace": "Um provedor físico chamado combo precisa ser renomeado antes de criar combos.", + "cws.err.providerCollision": "O id do combo conflita com o nome de um provedor configurado.", + "cws.err.unknownProvider": "Cada destino deve usar um provedor configurado.", + "cws.err.duplicateTarget": "O mesmo destino provedor/modelo só pode aparecer uma vez.", + "cws.err.invalidStickyLimit": "Os sucessos fixos devem ser um número inteiro de 1 a 100.", + "cws.err.invalidWeight": "Cada peso de round-robin deve ser um número inteiro de 1 a 10000.", + "cws.err.invalidReasoningEfforts": "Cada destino do JEV deve permitir pelo menos um esforço de raciocínio único e compatível.", + "cws.err.noEnabledTarget": "Pelo menos um destino deve usar um provedor habilitado.", + "claude.tabsLabel": "Cliente Claude", + "claude.tabCode": "Code", + "claude.tabDesktop": "Desktop", + "claudeDesktop.title": "Claude Desktop", + "claudeDesktop.subtitle": "Roteia cada família de modelos do Claude por um modelo disponível na porta {port} do proxy.", + "claudeDesktop.importJson": "Importar JSON", + "claudeDesktop.exportJson": "Exportar JSON", + "claudeDesktop.loading": "Carregando perfil do Claude Desktop…", + "claudeDesktop.loadFail": "Falha ao carregar o perfil do Claude Desktop.", + "claudeDesktop.retry": "Tentar novamente", + "claudeDesktop.saveFailed": "Falha ao salvar o perfil do Claude Desktop.", + "claudeDesktop.applyFailed": "O perfil foi salvo, mas não pôde ser aplicado.", + "claudeDesktop.updateFailed": "Falha na atualização do Claude Desktop.", + "claudeDesktop.savedApplied": "Perfil salvo e aplicado ao Claude Desktop.", + "claudeDesktop.appliedMarkerUnsaved": "Aplicado ao Claude Desktop, mas o marcador de aplicação não foi salvo — o estado salvo versus aplicado abaixo pode aparecer desatualizado até você aplicar novamente.", + "claudeDesktop.savedAppliedAnnounce": "Perfil do Claude Desktop salvo e aplicado.", + "claudeDesktop.saved": "Perfil salvo.", + "claudeDesktop.savedAnnounce": "Perfil do Claude Desktop salvo.", + "claudeDesktop.exported": "Perfil exportado como JSON.", + "claudeDesktop.importExpected": "Era esperado um perfil do Claude Desktop versão 1.", + "claudeDesktop.importReady": "JSON importado. Revise o rascunho e depois salve e aplique.", + "claudeDesktop.importedAnnounce": "JSON do perfil importado. As alterações não salvas estão prontas para revisão.", + "claudeDesktop.importInvalid": "O arquivo selecionado não é um perfil válido.", + "claudeDesktop.importFailed": "Falha na importação. {error}", + "claudeDesktop.moved": "{route} movido para {family}.", + "claudeDesktop.unsaved": "Alterações não salvas", + "claudeDesktop.upToDate": "O perfil está atualizado", + "claudeDesktop.saving": "Salvando…", + "claudeDesktop.applying": "Aplicando…", + "claudeDesktop.saveApply": "Salvar e aplicar", + "claudeDesktop.emptyTitle": "Nenhum modelo disponível", + "claudeDesktop.emptyHint": "Adicione ou habilite um provedor e volte para atribuir rotas ao Claude Desktop.", + "claudeDesktop.assignmentsLabel": "Atribuições das famílias de modelos do Claude", + "claudeDesktop.family.opus": "Opus", + "claudeDesktop.family.fable": "Fable", + "claudeDesktop.family.sonnet": "Sonnet", + "claudeDesktop.family.haiku": "Haiku", + "claudeDesktop.modelCountOne": "{count} modelo", + "claudeDesktop.modelCountMany": "{count} modelos", + "claudeDesktop.chooseDefault": "Escolha um padrão", + "claudeDesktop.temporaryDefault": "Padrão temporário", + "claudeDesktop.laneEmpty": "Solte um modelo aqui ou use o controle Mover.", + "claudeDesktop.laneNoMatch": "Nenhum modelo desta família corresponde à sua busca.", + "nav.grok": "Grok", + "grok.title": "Grok Build", + "grok.subtitle": "Modelos que o opencodex registrou na sua configuração do Grok.", + "grok.loading": "Carregando status do Grok…", + "grok.loadFail": "Não foi possível ler a configuração do Grok.", + "grok.notConfiguredTitle": "O Grok Build não está configurado", + "grok.notConfiguredHint": "Inicie ou reinicie o proxy com o Grok instalado e o opencodex grava um bloco gerenciado em:", + "grok.endpoint": "Endpoint", + "grok.colModel": "Modelo", + "grok.colAlias": "Alias do Grok", + "grok.colContext": "Contexto", + "grok.groupNative": "Modelos nativos", + "grok.groupRouted": "Modelos roteados", + "grok.enabledCount": "{on} de {total} registrados", + "grok.saved": "Seleção salva.", + "grok.savedApplied": "Seleção salva e gravada na sua configuração do Grok.", + "grok.saveFailed": "Não foi possível salvar a seleção do Grok.", + "grok.applyFailed": "Seleção salva, mas a configuração do Grok não pôde ser atualizada.", + "grok.applySkipped": "Seleção salva. A configuração do Grok não foi alterada.", + "grok.saveApply": "Salvar e aplicar", + "grok.saving": "Salvando…", + "grok.applying": "Aplicando…", + "grok.unsaved": "Alterações não salvas", + "grok.upToDate": "A seleção está atualizada", + "grok.toggleModel": "Registrar {id} no Grok", + "claudeDesktop.available": "Disponível", + "claudeDesktop.defaultBadge": "Padrão", + "claudeDesktop.supports1m": "1M", + "claudeDesktop.unavailable": "Indisponível", + "claudeDesktop.contextM": "contexto de {n}M", + "claudeDesktop.contextK": "contexto de {n}k", + "claudeDesktop.contextUnknown": "contexto desconhecido", + "claudeDesktop.alias": "Alias", + "claudeDesktop.useAsDefault": "Usar como padrão de {family}", + "claudeDesktop.moveTo": "Mover para", + "claudeDesktop.move": "Mover", + "claudeDesktop.status.applied": "Aplicado ao Desktop", + "claudeDesktop.status.stale": "Configuração desatualizada — aplique novamente", + "claudeDesktop.status.notApplied": "Não aplicado", + "claudeDesktop.status.notActiveProfile": "O Desktop está usando outro perfil — aplique novamente", + "claudeDesktop.status.disabled": "A integração com o Claude Desktop está desativada. Feche completamente e reabra o Desktop após ativá-la.", + "claudeDesktop.enableApply": "Ativar e aplicar", + "claudeDesktop.switchModeApply": "Trocar de modo e aplicar", + "claudeDesktop.status.appliedFirstParty": "Primeira parte: a aba Code do Desktop roteada pelo proxy local", + "claudeDesktop.mode.legend": "Modo de conexão", + "claudeDesktop.mode.firstParty": "Primeira parte", + "claudeDesktop.mode.gateway": "Gateway (terceiros)", + "claudeDesktop.mode.defaultBadge": "padrão", + "claudeDesktop.mode.current": "atual", + "claudeDesktop.mode.firstPartyHint": "O Desktop continua conectado ao claude.ai. A aba Code usa o OpenCodex quando a primeira parte do Desktop está ativada; a CLI independente tem seu próprio interruptor. Ambos leem o mesmo env das configurações.", + "claude.intercept.start": "Iniciar interceptação", + "claude.intercept.starting": "Iniciando interceptação…", + "claude.intercept.reason.disabled": "Ative primeiro o roteamento e a interceptação do Claude.", + "claude.intercept.reason.client_role": "Este processo é um cliente do hub. Ative a interceptação em um servidor local.", + "claude.intercept.reason.ephemeral_port": "Defina claudeCode.intercept.port quando a porta pública for efêmera.", + "claude.intercept.reason.port_in_use": "A porta {port} está em uso. Libere-a ou altere claudeCode.intercept.port.", + "claude.intercept.reason.port_mismatch": "Esta execução usa a porta {bound}; a porta configurada {configured} é diferente. Use a porta vinculada ou restaure o valor configurado.", + "claude.intercept.reason.failed": "Não foi possível iniciar a interceptação. Tente novamente após verificar a configuração do proxy local.", + "claude.intercept.reason.stopped": "O ciclo de vida da interceptação foi encerrado. Inicie um serviço OpenCodex em execução.", + "claude.firstParty.label": "Primeira parte da CLI do Claude Code", + "claude.firstParty.desc": "Permite que a CLI claude independente mantenha o login do Claude enquanto seu tráfego passa pelo proxy de interceptação local.", + "claude.firstParty.aria": "Alternar primeira parte da CLI do Claude Code", + "claude.firstParty.risk": "Risco para a conta: a primeira parte encaminha o tráfego da assinatura do Claude por um proxy local de interceptação. A Anthropic pode tratar isso como violação dos termos e suspender a conta.", + "claude.firstParty.shared": "Quando o env compartilhado do proxy é aplicado, o outro cliente do Claude também passa pelo proxy local como um relay inalterado; o TLS é encerrado localmente. Defina NO_PROXY='*' no shell para uso totalmente nativo no terminal.", + "claude.firstParty.deadProxy": "O env das configurações do Claude aponta para um proxy que não está em execução. Inicie o opencodex ou desative a primeira parte do Desktop/CLI para remover as configurações antes de usar o claude puro.", + "claude.firstParty.notApplied": "A primeira parte da CLI está ativada, mas suas configurações de proxy não foram aplicadas. Verifique o env das configurações do Claude antes de usar o claude puro.", + "claude.firstParty.brokenProxy": "Não foi possível iniciar a interceptação. Tente novamente após verificar a configuração do proxy local.", + "claude.firstParty.residual": "As configurações do Claude ainda apontam para um proxy do OpenCodex, embora nem a primeira parte do Desktop nem a da CLI esteja ativada. Remova a configuração de proxy restante ou execute `ocx ensure`.", + "claude.firstParty.routingOff": "O opencodex não está atendendo à interceptação do Claude na configuração atual (o roteamento do Claude ou a interceptação está desativado, ou esta máquina é cliente de outro hub opencodex), então estas configurações apontam para um proxy que não vai atendê-las. Reative-o nesta máquina ou desative a primeira parte para remover as configurações.", + "claude.firstParty.disabled": "Ative primeiro o roteamento e a interceptação do Claude.", + "claude.firstParty.foreign": "O ~/.claude/settings.json aponta para o proxy do opencodex, mas confia em um certificado que o opencodex não gerencia, então as requisições falham. Corrija HTTPS_PROXY / NODE_EXTRA_CA_CERTS manualmente ali.", + "claude.firstParty.local": "O ~/.claude/settings.json envia o Claude Code por um proxy local em 127.0.0.1 que o opencodex não consegue confirmar como seu. Se você não o usa mais, remova HTTPS_PROXY dali.", + "claude.firstParty.unknown": "O opencodex não conseguiu determinar se o ~/.claude/settings.json ainda aponta para o seu proxy.", + "claude.firstParty.refusal.interceptDisabled": "O proxy de interceptação está desativado.", + "claude.firstParty.refusal.interceptUnavailable": "Não foi possível iniciar a interceptação. Tente novamente após verificar a configuração do proxy local.", + "claude.firstParty.refusal.foreignEnv": "Outro programa é dono das configurações de proxy do Claude.", + "claude.firstParty.refusal.caUnavailable": "A autoridade certificadora local está indisponível.", + "claude.firstParty.refusal.unreadable": "Não foi possível ler as configurações do Claude.", + "claude.firstParty.refusal.writeFailed": "Não foi possível gravar as configurações do Claude.", + "claudeDesktop.mode.firstPartyRisk": "Risco para a conta: a primeira parte envia o tráfego da sua assinatura do Claude por um proxy local de interceptação. A Anthropic pode tratar isso como violação dos termos e suspender a conta. O Gateway é o padrão.", + "claudeDesktop.mode.gatewayHint": "O aplicativo inteiro passa a usar o OpenCodex como gateway. O chat roda localmente; os recursos do claude.ai ficam indisponíveis.", + "claudeDesktop.mode.switchNote": "A troca substitui a configuração do outro modo. Depois, feche completamente e reabra o Desktop.", + "claudeDesktop.firstParty.proxyRunning": "Proxy local em 127.0.0.1:{port}", + "claudeDesktop.firstParty.proxyStopped": "Não foi possível iniciar a interceptação. Tente novamente após verificar a configuração do proxy local. ({port})", + "claudeDesktop.firstParty.interceptDisabled": "O proxy de interceptação está desativado na configuração (claudeCode.intercept.enabled)", + "claudeDesktop.picker.title": "Seletor do Claude Desktop", + "claudeDesktop.picker.hint": "Mostra os modelos do OpenCodex no seletor da aba Code do Desktop por meio do proxy local dedicado.", + "claudeDesktop.picker.toggle": "Ativar o seletor do Claude Desktop", + "claudeDesktop.picker.state.active": "O seletor está ativo", + "claudeDesktop.picker.state.restart": "Feche completamente e reabra o Claude Desktop para concluir a aplicação do perfil do seletor.", + "claudeDesktop.picker.state.trustPending": "Aguardando a etapa do chaveiro", + "claudeDesktop.picker.state.trustDeclined": "A confiança no chaveiro foi recusada", + "claudeDesktop.picker.state.proxyUnavailable": "O proxy do seletor não está em execução", + "claudeDesktop.picker.state.unsupported": "O modo seletor não é compatível com este sistema operacional", + "claudeDesktop.picker.state.notFirstParty": "O seletor só está disponível no modo de primeira parte", + "claudeDesktop.picker.state.profileFailed": "Não foi possível aplicar o perfil do seletor", + "claudeDesktop.picker.models": "{count} modelos disponíveis no seletor", + "claudeDesktop.picker.offlineNote": "Com o modo seletor ativado, o Claude Desktop acessa a rede por meio do OpenCodex. Se o OpenCodex parar, o Desktop fica offline até ele reiniciar ou o modo seletor ser desativado.", + "claudeDesktop.firstParty.bindings.title": "Vínculos de modelo da aba Code", + "claudeDesktop.firstParty.bindings.hint": "O Claude Desktop mantém os nomes da Anthropic no seletor da aba Code. Vincule um modelo do seletor a um modelo do OpenCodex e as requisições dessa entrada do seletor serão atendidas pelo modelo vinculado. Somente o tráfego do Claude Code pelo proxy local (a aba Code do Desktop e a CLI claude) usa estes vínculos; o ocx claude não é afetado. Os vínculos valem a partir da próxima requisição — não é preciso fechar e reabrir completamente.", + "claudeDesktop.firstParty.bindings.empty": "Ainda não há vínculos — os modelos do seletor seguem a rota padrão.", + "claudeDesktop.firstParty.bindings.pickerLabel": "Id do modelo no seletor", + "claudeDesktop.firstParty.bindings.pickerPlaceholder": "claude-sonnet-4-6", + "claudeDesktop.firstParty.bindings.routeLabel": "Modelo do OpenCodex", + "claudeDesktop.firstParty.bindings.routePlaceholder": "Escolha um modelo", + "claudeDesktop.firstParty.bindings.add": "Adicionar vínculo", + "claudeDesktop.firstParty.bindings.remove": "Remover vínculo {id}", + "claudeDesktop.firstParty.bindings.saveFailed": "Não foi possível salvar os vínculos de modelo", + "claudeDesktop.health.lastRequest": "Última requisição", + "claudeDesktop.health.stats": "{count} req / {errors} err", + "claudeDesktop.effort.supported": "esforço", + "claudeDesktop.effort.displayOnly": "esforço (apenas exibição)", + "lab.title": "Laboratório de Compatibilidade", + "lab.subtitle": "Matriz de veredictos de compatibilidade somente leitura, a partir de evidências da projeção do laboratório.", + "lab.loadFailed": "Não foi possível carregar os dados do laboratório de compatibilidade", + "lab.projectionUnavailable": "A projeção do laboratório não está disponível. Execute primeiro a conformidade ou as sondagens ao vivo.", + "lab.projectionIncompatible": "O esquema da projeção do laboratório é incompatível. Reconstrua a projeção.", + "lab.statusTitle": "Status da projeção", + "lab.matrixTitle": "Matriz de compatibilidade", + "lab.verdictsTitle": "Registros de veredicto", + "lab.filter.layer": "Camada de evidência", + "lab.filter.verdict": "Veredicto", + "lab.filter.subject": "ID do assunto", + "lab.filter.all": "Todos", + "lab.col.subject": "Assunto", + "lab.col.layer": "Camada", + "lab.col.suite": "Suíte", + "lab.col.verdict": "Veredicto", + "lab.col.asOf": "Data de referência", + "lab.col.protocol": "Conformidade de protocolo", + "lab.col.live": "Compatibilidade de rota ao vivo", + "lab.col.task": "Eficácia na tarefa", + "lab.empty": "Ainda não há veredictos de compatibilidade na projeção.", + "lab.subjectKind": "Tipo", + "lab.observationCount": "Observações", + "lab.eventCount": "Eventos", + "lab.verdictCount": "Veredictos", + "lab.subjectCount": "Assuntos", + "lab.builtAt": "Gerado em", + "lab.loading": "Carregando evidências de compatibilidade…", + "lab.loadMore": "Carregar mais", + "lab.detailTitle": "Detalhe do veredicto", + "lab.detailClose": "Fechar", + "lab.detailSubject": "Assunto", + "lab.detailObservations": "Observações", + "lab.detailEvents": "Eventos contribuintes", + "lab.detailArtifacts": "Metadados dos artefatos", + "lab.production.title": "Tráfego de produção observado", + "lab.production.notVerification": "Não é verificação do Laboratório", + "lab.production.attempts": "Tentativas", + "lab.production.successes": "Sucessos", + "lab.production.routeErrors": "Erros de rota", + "lab.production.lastObserved": "Última observação", + "lab.detailLoadFailed": "Não foi possível carregar o detalhe do veredicto", + "lab.refresh": "Atualizar", + "lab.verdict.UNKNOWN": "Desconhecido", + "lab.verdict.CLAIMED": "Declarado", + "lab.verdict.PROBED": "Sondado", + "lab.verdict.VERIFIED": "Verificado", + "lab.verdict.DEGRADED": "Degradado", + "lab.verdict.BLOCKED": "Bloqueado", + "lab.verdict.UNSUPPORTED": "Sem suporte", + "lab.layer.protocol_conformance": "Conformidade de protocolo", + "lab.layer.live_route_compatibility": "Compatibilidade de rota ao vivo", + "lab.layer.task_effectiveness": "Eficácia na tarefa", + "models.newPolicyGlobal": "Novos modelos começam desativados", + "models.newPolicyProvider": "Política de novos modelos", + "models.fastProvider": "Modo rápido", + "models.fastProviderHint": "Consome créditos de uso a 2x o preço", + "models.fastEnabled": "Modo rápido ativado", + "models.fastDisabled": "Modo rápido desativado", + "models.fastSaveFailed": "Não foi possível salvar o modo Fast", + "models.newPolicy_inherit": "Herdar", + "models.newPolicy_off": "Desativado", + "models.newPolicy_on": "Ativado", + "models.newBadge": "NOVO", + "models.newCount": "{count} novos, desativados", + "models.aliases": "Aliases", + "models.aliasesTable": "Tabela de aliases", + "models.aliasPrompt": "Alias do provedor (deixe vazio para limpar)", + "models.modelAliasPrompt": "Alias do modelo (deixe vazio para limpar)", + "models.aliasSaved": "Alias salvo", + "models.aliasConflict": "Esse alias conflita com um nome existente", + "models.editProviderAlias": "Editar alias do provedor", + "models.editModelAlias": "Editar alias do modelo", + "models.useDefaultAliases": "Usar aliases padrão", + "models.useDefaultAliasesGlobal": "Usar aliases padrão globalmente", + "models.aliasAuto": "auto", + "models.aliasUser": "usuário", + "models.aliasStale": "obsoleto", + "connection.discovering": "Descobrindo destinos locais e compartilhados…", + "connection.machineUnavailable": "O plano da máquina local está indisponível. As requisições compartilhadas não foram redirecionadas para o local.", + "connection.disconnect": "Desconectar do hub", + "connection.disconnectConfirm": "Desconectar esta máquina do hub e reiniciá-la no modo standalone?", + "connection.pairing.hub": "Hub", + "connection.pairing.getCode": "Execute este comando no hub para este navegador:", + "connection.pairing.askOperator": "Se outra pessoa opera o hub, envie a ela este comando e peça um código de pareamento de uso único.", + "connection.pairing.notApiKey": "Cole aqui um código de pareamento de uso único. Chaves de API de dados e tokens de administrador não devem ser colocados neste campo.", + "connection.pairing.networkError": "Não foi possível alcançar o hub. Verifique a conexão e tente novamente; seu código continua aqui.", + "connection.pairing.requestError": "O hub não conseguiu concluir a solicitação de pareamento. Verifique o status dele e tente novamente.", + "connection.pairing.responseError": "O hub não retornou uma sessão de navegador válida. Atualize o hub ou fale com o operador dele e tente novamente.", + "dash.authRequired": "É necessária autenticação do navegador para ver este dashboard.", + "dash.permissionDenied": "Este navegador não tem permissão para ler o dashboard. Verifique as configurações de acesso com o operador do servidor.", + "dash.dataUnavailable": "Não foi possível carregar os dados do dashboard. Verifique a conexão e tente novamente.", + "dash.staleData": "Exibindo os últimos dados recebidos; podem estar desatualizados.", + "connection.pairing.title": "Conectar este dashboard ao hub", + "connection.pairing.body": "Cole o código de pareamento de uso único criado no hub.", + "connection.pairing.relayWarning": "Este código é trocado por meio do relay fixo do hub. O relay não pode ser redirecionado para outro host.", + "connection.pairing.code": "Código de pareamento de uso único", + "connection.pairing.submit": "Conectar", + "connection.pairing.submitting": "Conectando…", + "connection.pairing.error": "O código de pareamento foi recusado ou expirou. O código foi mantido para que você possa conferi-lo.", + "connection.machine.title": "Esta máquina", + "connection.machine.shimHealthy": "O shim do Codex está saudável.", + "connection.machine.shimNeedsAttention": "O shim do Codex precisa de atenção.", + "connection.machine.repairShim": "Reparar shim", + "connection.machine.removeShim": "Remover shim", + "connection.clients.title": "Clientes conectados", + "connection.clients.none": "Nenhum status de cliente disponível", + "connection.clients.sync": "Sincronizar agora", + "connection.clients.syncing": "Sincronizando…", + "connection.sessionLogout": "Encerrar sessão remota", + "connection.sessionLoggingOut": "Encerrando sessão remota…", + "connection.sessionLogoutFailed": "Não foi possível encerrar a sessão remota. A sessão atual foi mantida.", + "usage.source.connected": "Fonte: uso do hub", + "usage.source.local": "Fonte: usage.jsonl local", + "usage.scope.label": "Escopo de uso", + "usage.scope.machine": "Esta máquina", + "usage.scope.hub": "Todo o hub", + "usage.hubOffline": "O uso do hub está indisponível. O uso local não foi usado como substituto.", + "models.displayNameSavedRefreshFailed": "A alteração foi salva, mas não foi possível atualizar a lista de modelos. Tente novamente para atualizá-la.", + "models.displayNameOutcomeUnknown": "A requisição não foi concluída. A alteração pode ter sido salva. Tente novamente para verificar o nome atual antes de fazer outra alteração.", + "models.displayNameCurrentUnavailable": "Nome atual indisponível até a atualização", + "models.displayNameReloaded": "Lista de modelos atualizada", + "models.displayNameAction": "Nome", + "models.displayNameActionLabel": "Editar nome amigável de {model}", + "models.displayNameTitle": "Nome amigável", + "models.displayNameModelId": "ID do modelo", + "models.displayNameCurrent": "Nome atual", + "models.displayNameSourceOperator": "Seu nome", + "models.displayNameSourceProvider": "Nome do provedor", + "models.displayNameSourceFallback": "Fallback para o ID do modelo", + "models.displayNameField": "Nome amigável", + "models.displayNamePlaceholder": "ex.: Grok 4.6", + "models.displayNameHelp": "Altera apenas a apresentação. O roteamento continua {model}.", + "models.displayNameReset": "Redefinir nome", + "models.displayNameSaved": "Nome de exibição salvo", + "models.displayNameResetDone": "Nome de exibição redefinido", + "models.displayNameSaveFailed": "Falha ao salvar o nome de exibição", + "models.displayNameRequired": "Informe um nome amigável ou use Redefinir nome.", + "models.displayNameTooLong": "O nome amigável deve ter no máximo 128 caracteres.", + "models.displayNameNoSlash": "O nome amigável não pode conter /.", + "models.displayNameNoControl": "O nome amigável não pode conter caracteres de controle.", + "pricing.override.action": "Preço", + "pricing.override.actionLabel": "Editar preço de {model}", + "pricing.override.badge": "Preço manual", + "pricing.override.title": "Preço do modelo", + "pricing.override.modelId": "ID do modelo", + "pricing.override.help": "USD por 1M de tokens. Informe as taxas de entrada e saída; taxas de cache em branco usam 0. As quatro taxas em 0 significam gratuito.", + "pricing.override.input": "Entrada", + "pricing.override.output": "Saída", + "pricing.override.cacheRead": "Leitura de cache", + "pricing.override.cacheWrite": "Escrita de cache", + "pricing.override.loading": "Carregando preço salvo…", + "pricing.override.loadFailed": "Não foi possível carregar o preço salvo. Recarregue para tentar de novo.", + "pricing.override.outcomeUnknown": "A requisição não terminou de forma confiável. O preço pode ter mudado. Recarregue o preço salvo antes de editar novamente.", + "pricing.override.recoveryFailed": "Não foi possível recuperar o preço salvo. A edição continua bloqueada; recarregue para tentar de novo.", + "pricing.override.recovered": "Último preço salvo carregado. A requisição anterior ou outro cliente ainda pode alterá-lo.", + "pricing.override.refreshFailed": "O preço foi salvo, mas não foi possível atualizar a lista de modelos. Tente atualizar a lista novamente.", + "pricing.override.invalid": "Informe as taxas de entrada e saída. Cada taxa deve ser um número finito de 0 a 1.000.000.", + "pricing.override.reset": "Voltar ao automático", + "pricing.override.save": "Salvar", + "pricing.override.saving": "Salvando…", + "pricing.override.reload": "Recarregar preço", + "pricing.override.refresh": "Atualizar lista", + "pricing.override.cancel": "Cancelar", + "pricing.override.close": "Fechar", + "usage.range.custom": "Intervalo de datas personalizado", + "usage.range.start": "Início (hora local)", + "usage.range.end": "Fim (hora local)", + "usage.range.apply": "Aplicar", + "usage.range.clear": "Limpar", + "usage.range.help": "Hora local. Inclui o minuto final inteiro.", + "usage.range.required": "Informe a data e hora de início e de fim.", + "usage.range.invalid": "Informe datas e horas locais válidas, a partir de 01/01/1970 UTC.", + "usage.range.reversed": "O fim deve ser igual ou posterior ao início.", + "usage.range.applied": "Intervalo selecionado: {start} – {end} (ambos inclusivos).", + "models.pickerOrder.editorHint": "Reordene os modelos roteados e salve seu rascunho. As linhas em destaque são fixas; modelos nativos não são exibidos.", + "models.pickerOrder.nativeLocked": "Esta ordem salva inclui modelos nativos. Aplique um preset roteado ou Padrão antes de editar o Personalizado.", + "models.pickerOrder.unknownChosen": "As escolhas em destaque são desconhecidas. Recarregue antes de editar.", + "models.pickerOrder.changed": "As configurações do seletor mudaram. Seu rascunho foi mantido; recarregue para descartá-lo e usar as configurações atuais.", + "models.pickerOrder.savedReload": "Ordem salva. Recarregue as configurações atuais antes de editar novamente.", + "models.pickerOrder.requestFailed": "A requisição falhou. Seu rascunho foi mantido; tente novamente ou recarregue.", + "models.pickerOrder.empty": "Nenhum modelo roteado disponível.", + "models.pickerOrder.dragModel": "Arrastar {model}", + "models.pickerOrder.featured": "Em destaque", + "models.pickerOrder.upModel": "Mover {model} para cima", + "models.pickerOrder.downModel": "Mover {model} para baixo", + "models.pickerOrder.position": "{model}: posição {position} de {total}", + "models.pickerOrder.saveDraft": "Salvar rascunho", + "models.pickerOrder.reloadDraft": "Recarregar e descartar rascunho", + "models.pickerOrder.catalogRequired": "As identidades dos modelos estão ausentes ou ambíguas. Recarregue a página Modelos para atualizar o catálogo antes de editar o Personalizado.", + "nav.remote": "Remote Link", + "nav.remoteWorkspace": "Workspace remoto", + "link.title": "Remote Link", + "link.subtitle": "Conecte outro computador com OpenCodex por SSH.", + "link.switch": "Vinculação remota", + "link.switchOffHint": "Ative a vinculação para escolher a função deste computador.", + "link.role.title": "Escolha a função deste computador", + "link.role.hint": "O Home compartilha os provedores deste computador com computadores Child. O Child conecta este computador a um Home quando ele roda no modo standalone.", + "link.role.home": "Home", + "link.role.homeHint": "Gerencie computadores Child a partir deste dashboard.", + "link.role.child": "Child", + "link.role.childHint": "Use a configuração do OpenCodex de outro computador.", + "link.continue": "Continuar", + "link.sessionRequired": "Entre na sessão local do dashboard para gerenciar vínculos.", + "link.workspaceUnavailable": "O Remote Workspace não está disponível nesta instalação.", + "link.loading": "Carregando…", + "link.loadFailed": "Não foi possível carregar o status do Remote Link.", + "link.refresh": "Atualizar", + "link.children": "Computadores Child", + "link.addChild": "Adicionar Child", + "link.sheetTitle": "Adicionar um computador Child", + "link.candidates": "Hosts SSH", + "link.noCandidates": "Nenhum host SSH candidato foi encontrado.", + "link.alias": "Alias do host SSH", + "link.aliasPlaceholder": "Informe um alias da sua configuração SSH", + "link.probe": "Testar conexão", + "link.probing": "Testando conexão…", + "link.hostFingerprint": "Impressão digital da chave do host", + "link.confirmFingerprint": "Reconheço esta chave do host", + "link.confirming": "Confirmando…", + "link.confirm": "Confirmar host", + "link.ocxVersion": "Versão do OpenCodex {version}", + "link.apply": "Conectar Child", + "link.applying": "Conectando…", + "link.retry": "Tentar novamente", + "link.disconnect": "Desconectar", + "link.disconnectConfirm": "Desconectar {alias}? A chave de vínculo dele será revogada.", + "link.close": "Fechar", + "link.cancel": "Cancelar", + "remoteLink.childDisabled": "Vínculos Child só podem ser iniciados a partir de um runtime standalone.", + "remoteLink.findHome.title": "Encontrar Home", + "remoteLink.findHome.body": "Escolha o computador Home ao qual conectar este Child.", + "remoteLink.findHome.action": "Encontrar Home", + "remoteLink.findHome.connect": "Conectar como Child", + "remoteLink.findHome.notice": "Conectar reinicia o OpenCodex neste computador. Os turnos do Codex em andamento terminam primeiro, e novas requisições podem falhar por até um minuto. Depois disso, o Codex mantém o mesmo endereço local, e o Home o atende com os próprios provedores e contas.", + "remoteLink.findHome.empty": "Ainda não conectado a um Home.", + "remoteLink.joining": "Entrando no Home…", + "remoteLink.restart.title": "Este computador será reiniciado para se conectar como Child.", + "remoteLink.restart.body": "Os turnos do Codex em execução terminam primeiro; depois o OpenCodex é reiniciado como Child. Esta página recarrega sozinha quando o Child estiver pronto.", + "remoteLink.restart.waiting": "Aguardando este computador se reconectar como Child…", + "remoteLink.restart.slow": "A reinicialização está demorando mais que o normal. Esta página continua verificando e recarrega sozinha assim que o Child responder. Se o OpenCodex parou, inicie-o novamente.", + "remoteLink.error.standalone_required": "Vínculos Child só podem ser iniciados a partir de um runtime standalone.", + "remoteLink.error.join_tunnel_failed": "Não foi possível iniciar o túnel para o Home. Verifique o acesso SSH e tente novamente.", + "remoteLink.error.admission_failed": "O Home não aceitou o novo vínculo. Verifique se ele está em execução e tente novamente.", + "remoteLink.error.join_issue_failed": "O Home não conseguiu emitir um vínculo. Verifique se o OpenCodex está em execução no Home e tente novamente.", + "remoteLink.error.join_in_progress": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.join_port_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.join_port_mismatch": "O OpenCodex não está rodando na porta configurada, por isso não pode reiniciar como Child. Reinicie o OpenCodex na porta configurada e tente novamente.", + "remoteLink.error.join_rollback_failed": "A entrada falhou e o vínculo no Home não pôde ser removido. Tente a limpeza novamente ou execute ocx link revoke no Home.", + "remoteLink.error.join_restart_failed": "O vínculo está pronto. Reinicie o OpenCodex neste computador para concluir a conexão como Child.", + "remoteLink.error.join_connect_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "link.noChildren": "Nenhum computador Child conectado.", + "remoteLink.status.connecting": "Conectando", + "remoteLink.status.connected": "Conectado", + "remoteLink.status.reconnecting": "Reconectando", + "remoteLink.status.failed": "A conexão precisa de atenção", + "remoteLink.status.idle": "Ocioso", + "remoteLink.role.standalone": "Independente", + "remoteLink.role.home": "Home", + "remoteLink.role.child": "Child", + "remoteLink.direction.hub": "Iniciado pelo hub", + "remoteLink.direction.client": "Iniciado pelo cliente", + "remoteLink.error.admission_timeout": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.compensation_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.fingerprint_failed": "Não foi possível ler a chave do host SSH. Teste a conexão novamente.", + "remoteLink.error.forbidden": "Esta sessão do dashboard não pode gerenciar vínculos remotos. Abra o dashboard neste computador (instalação standalone) ou use uma sessão de Hub pareada.", + "remoteLink.error.host_confirmation_expired": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.host_fingerprint_mismatch": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.host_not_confirmed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.invalid_alias": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.invalid_body": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.invalid_link_id": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.key_issue_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.key_revoke_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.link_apply_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.link_exists": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.link_not_found": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.link_remove_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.link_unavailable": "O Remote Link não está disponível neste runtime. Reinicie o OpenCodex e tente novamente.", + "remoteLink.error.listener_unavailable": "O listener do Remote Link não pôde ser iniciado neste computador. Verifique se há conflito de porta e tente novamente.", + "remoteLink.error.probe_failed": "Não foi possível conectar ao host SSH. Verifique se ele aceita sua chave SSH e se o auxiliar de ProxyCommand está instalado.", + "remoteLink.error.remote_connect_failed": "O computador remoto não conseguiu se conectar a este Home. Verifique o serviço OpenCodex dele e tente novamente.", + "remoteLink.error.remote_disconnect_failed": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.remote_ocx_missing": "O ocx não foi encontrado no computador remoto. Instale o OpenCodex lá ou garanta que o ocx esteja no PATH das sessões SSH não interativas.", + "remoteLink.error.remote_ocx_outdated": "O OpenCodex no computador remoto é antigo demais para o Remote Link. Execute ocx update lá (2.66.0 ou posterior) e tente novamente.", + "remoteLink.error.remote_ocx_unrecognized": "O computador remoto não informou uma versão do OpenCodex. O Remote Link exige o OpenCodex 2.66.0 ou posterior no macOS ou Linux.", + "remoteLink.error.remote_port_failed": "O computador remoto não conseguiu informar a porta de vínculo. Verifique se o OpenCodex 2.66.0 ou posterior está em execução lá e tente novamente.", + "remoteLink.error.tailscale_session_refused": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.error.version_probe_failed": "Não foi possível executar o ocx no computador remoto por SSH. Verifique o acesso SSH e tente novamente.", + "remoteLink.error.generic": "Não foi possível concluir a solicitação de vínculo remoto.", + "remoteLink.reason.auth": "Falha na autenticação.", + "remoteLink.reason.hostkey": "Não foi possível verificar a chave do host.", + "remoteLink.reason.forward": "Falha no encaminhamento de porta.", + "remoteLink.reason.timeout": "A conexão expirou.", + "remoteLink.reason.bind": "Não foi possível associar a porta local.", + "remoteLink.reason.persist": "Não foi possível salvar o estado do vínculo.", + "remoteLink.reason.compensation_failed": "A limpeza após a vinculação falhou.", + "remoteLink.reason.staleTunnel": "Um túnel obsoleto ainda pode estar mantendo a porta.", + "remoteLink.reason.generic": "Motivo:", + "remoteLink.workspaceMoved.title": "O Remote Workspace agora tem página própria", + "remoteLink.workspaceMoved.body": "Use a página separada do Remote Workspace.", + "remoteLink.workspaceMoved.open": "Abrir Remote Workspace", + "remoteLink.forceRemove.title": "Esta máquina não conseguiu alcançar {alias}", + "remoteLink.forceRemove.body": "Remover o vínculo somente nesta máquina? Depois, execute {cmd} você mesmo em {alias}.", + "remoteLink.forceRemove.confirm": "Remover somente aqui", + "remote.title": "Workspace remoto", + "remote.subtitle": "Execute Codex, Claude Code ou Pi a partir deste Hub enquanto arquivos, comandos, testes e builds permanecem no computador selecionado.", + "remote.loading": "Carregando Remote Workspace…", + "remote.loadFailed": "Não foi possível carregar o Remote Workspace.", + "remote.hubRequired": "Use o modo Hub e inicie o Hub com OCX_REMOTE_WORKSPACE_ENABLED=1 para ativar o Remote Workspace.", + "remote.refresh": "Atualizar", + "remote.addComputer": "Adicionar um computador", + "remote.addComputerHint": "Aprove uma ou mais pastas localmente e mantenha o executor exclusivo do OCX conectado a este Hub.", + "remote.createPairing": "Criar código de pareamento", + "remote.pairingCode": "Código de pareamento de uso único", + "remote.pairingExpires": "Expira às {time}", + "remote.pairingCommand": "Execute no computador que você está adicionando", + "remote.pairingCommandPosix": "Terminal Linux / macOS", + "remote.pairingCommandWindows": "Windows PowerShell", + "remote.copyCommand": "Copiar comando", + "remote.copied": "Copiado", + "remote.devices": "Computadores", + "remote.noDevices": "Nenhum computador pareado ainda.", + "remote.online": "Online", + "remote.offline": "Offline", + "remote.revoke": "Revogar computador", + "remote.revokeConfirm": "Revogar {name}? As sessões ativas neste computador serão interrompidas.", + "remote.newSession": "Nova sessão remota", + "remote.device": "Computador", + "remote.folder": "Pasta do workspace", + "remote.runtime": "Agente de código", + "remote.access": "Acesso ao workspace", + "remote.access.readOnly": "Somente leitura", + "remote.access.workspace": "Editar arquivos e executar comandos", + "remote.access.workspaceFilesOnly": "Somente editar arquivos", + "remote.unavailable": "Indisponível", + "remote.capability.full": "Arquivos + comandos em sandbox", + "remote.capability.files": "Somente ferramentas de arquivo", + "remote.runsOnHub": "O modelo e o login permanecem neste Hub", + "remote.runsReadOnly": "Os arquivos só podem ser lidos neste computador", + "remote.runsFilesCommands": "Arquivos, builds e comandos são executados aqui", + "remote.runsFilesOnly": "As ferramentas de arquivo rodam aqui; sandbox de comandos indisponível", + "remote.execUnavailable": "Este computador pode editar arquivos, mas builds e comandos de terminal estão desativados porque não há um sandbox de SO compatível.", + "remote.notResumable": "Esta sessão parou antes de o agente de código criar um histórico durável. Inicie uma nova sessão remota.", + "remote.startSession": "Iniciar sessão remota", + "remote.sessionStarted": "A sessão remota está pronta.", + "remote.sessions": "Sessões", + "remote.noSessions": "Escolha um computador online, uma pasta e um agente de código para começar.", + "remote.events": "Atividade da sessão remota", + "remote.noEvents": "Nenhuma atividade ainda.", + "remote.prompt": "Mensagem", + "remote.promptPlaceholder": "Peça ao agente do Hub para trabalhar dentro da pasta remota selecionada…", + "remote.send": "Enviar", + "remote.stop": "Parar sessão", + "remote.requestFailed": "A requisição do Remote Workspace falhou.", + "remote.submissionUnknown": "O status do envio é desconhecido. Verifique a sessão antes de enviar novamente.", + "remote.status.starting": "Iniciando", + "remote.status.ready": "Pronto", + "remote.status.running": "Em execução", + "remote.status.waiting": "Executor offline", + "remote.status.failed": "Falhou", + "remote.status.stopped": "Parado", + "remote.event.status": "Status", + "remote.event.tool": "Ferramenta remota", + "remote.event.error": "Erro", + "quotaSummary.aria": "Resumo de cota dos provedores", + "quotaSummary.updated": "Atualizado {time}", + "quotaSummary.dataAt": "Dados de {time}", + "quotaSummary.observedAt": "Observado em {time}", + "quotaSummary.warn": "70%+ usado", + "quotaSummary.critical": "90%+ usado", + "quotaSummary.credits": "Créditos", + "quotaSummary.refreshFailed": "A última atualização falhou; exibindo a leitura anterior", + "quotaSummary.scrollPrev": "Rolar provedores para a esquerda", + "quotaSummary.scrollNext": "Rolar provedores para a direita", + "quotaSummary.openAccounts": "Abrir gerenciamento de contas", + "pws.protocol.title": "Wire upstream", + "pws.protocol.note": "O formato de API que o opencodex usa para falar com este provedor. Ele não ativa nem desativa nenhuma API de cliente; a página de API controla isso.", + "pws.protocol.adapterLabel": "Wire upstream que este provedor recebe", + "pws.protocol.source": "Definido por", + "pws.protocol.source.hardPin": "Fixado pelo opencodex para este modelo", + "pws.protocol.source.operator": "Sua configuração", + "pws.protocol.source.registry": "Catálogo de provedores", + "pws.protocol.source.providerDefault": "Padrão do provedor", + "pws.protocol.pending": "Depois de salvar, este provedor receberá {adapter}.", + "pws.protocol.overrides": "Modelos em outro wire", + "pws.protocol.noOverrides": "Todos os modelos usam o wire do provedor.", + "pws.protocol.overridesTruncated": "Exibindo os primeiros {count} modelos.", + "pws.protocol.col.model": "Modelo", + "pws.protocol.col.wire": "Wire", + "pws.protocol.col.source": "Definido por", + "pws.protocol.loadFailed": "Não foi possível carregar o wire upstream.", + "compatProtocol.filter.inbound": "API do cliente", + "compatProtocol.filter.upstream": "Wire upstream", + "compatProtocol.anyProtocol": "qualquer", + "compatProtocol.pair": "{inbound} → {upstream}", + "compatProtocol.loading": "Lendo o par de protocolos de cada subject…", + "compatProtocol.unverified": "Ainda não há evidência do Lab para {pair}. O par não foi verificado, não que tenha falhado.", + "compatProtocol.unresolved": "Não foi possível ler o par de protocolos de {count} subjects; eles ficam fora deste filtro.", + "compatProtocol.axisNote": "Estes são apenas veredictos do Lab. A forma como uma requisição é entregue (nativa ou traduzida) é mostrada na prévia do caminho da requisição, na página de API.", + "protocolLinks.labEvidence": "Evidência do Lab para este par", + "protocolLinks.providerSettings": "Configurações do provedor", + "cws.plan.title": "Caminhos candidatos", + "cws.plan.description": "Pergunta ao proxy como cada destino carregaria uma requisição com todos os recursos que a API do cliente consegue expressar, e quais recursos todos os destinos preservam. Nada é enviado ao upstream.", + "cws.plan.run": "Mostrar caminhos candidatos", + "cws.plan.savedOnly": "Mostra o combo salvo. Salve suas alterações para visualizá-las.", +}; diff --git a/gui/src/i18n/routing-compatibility-labels.ts b/gui/src/i18n/routing-compatibility-labels.ts index c4e3de76b6b..42b8d75a8ad 100644 --- a/gui/src/i18n/routing-compatibility-labels.ts +++ b/gui/src/i18n/routing-compatibility-labels.ts @@ -63,4 +63,9 @@ export const ROUTING_COMPATIBILITY_FIELD_LABELS: Record = { "theme.light": "Светлая", "theme.dark": "Тёмная", "theme.system": "Системная", + "zoom.label": "Масштаб", + "zoom.out": "Уменьшить", + "zoom.in": "Увеличить", + "zoom.reset": "Сбросить масштаб до 100 %", "lang.label": "Язык", "lang.nativeName": "Русский", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -444,14 +448,25 @@ export const ru: Record = { "compactionRouting.triggersManual": "Только ручной /compact", "compactionRouting.triggersBoth": "Ручное и автоматическое", "compactionRouting.triggersAuto": "Только автоматическое", + "compactionRouting.sources": "Источники", + "compactionRouting.sourcesAll": "Все модели диалога", + "compactionRouting.sourcesSelected": "Только выбранные источники", + "compactionRouting.sourcesProviders": "Провайдеры целиком", + "compactionRouting.sourcesModels": "Отдельные модели", + "compactionRouting.sourcesSaved": "Сохранённые селекторы вне текущего каталога", + "compactionRouting.sourcesNone": "Выберите хотя бы один источник или вернитесь ко всем моделям диалога.", + "compactionRouting.sourcesHint": "Применяет переопределение, только если исходная модель запроса сжатия совпадает с выбранной моделью или provider/*. Остальные запросы используют свою модель.", + "compactionRouting.sourcesGridTitle": "Перенаправлять запросы сжатия, исходная модель которых совпадает с:", "compactionRouting.currentModel": "Использовать модель разговора", "compactionRouting.currentEffort": "Сохранить уровень из запроса", "compactionRouting.effortHint": "Уровень рассуждений применяется, если его поддерживает конечная точка сжатия. Модель должна вмещать весь разговор.", "compactionRouting.dataNotice": "Охваченный запрос сжатия отправляет весь разговор провайдеру выбранной модели для составления сводки, даже если разговор идёт у другого провайдера.", "compactionRouting.autoNotice": "Автоматическое сжатие запускается само, поэтому длинный разговор может уйти этому провайдеру без вашего запроса.", "compactionRouting.providerWarning": "С этой настройкой каждый охваченный запрос сжатия отправляет полное содержимое разговора провайдеру {provider} для составления сводки.", - "compactionRouting.comboWarning": "С этой настройкой каждый охваченный запрос сжатия отправляет полное содержимое разговора комбо {combo} для составления сводки. Комбо пробует свои цели ({providers}) по порядку и использует первую ответившую, поэтому разговор может получить любая из них.", + "compactionRouting.comboWarning": "С этой настройкой каждый охваченный запрос сжатия отправляет полное содержимое разговора комбо {combo} для составления сводки. Комбо выбирает одну из целей ({providers}) по своей стратегии маршрутизации, но после сбоя, допускающего повторную попытку, может повторить тот же запрос с полным содержимым разговора на другой цели, поэтому одна или несколько целей могут получить полное содержимое разговора.", "compactionRouting.comboProvidersUnknown": "его настроенные целевые провайдеры", + "compactionRouting.providerWarningScoped": "При этой настройке только запросы сжатия, исходная модель которых совпадает с {sources}, отправляют полное содержимое диалога в {provider} для составления сводки; остальные запросы используют свою модель.", + "compactionRouting.comboWarningScoped": "При этой настройке только запросы сжатия, исходная модель которых совпадает с {sources}, отправляют полное содержимое диалога комбо {combo} для составления сводки; остальные запросы используют свою модель. Комбо выбирает одну из целей ({providers}) по своей стратегии маршрутизации, но после повторяемого сбоя может повторить тот же запрос с полным содержимым разговора на другой цели, поэтому одна или несколько целей могут получить полное содержимое разговора.", "compactionRouting.loadFailed": "Не удалось загрузить настройки сжатия.", "compactionRouting.saved": "Настройки сжатия сохранены.", "compactionRouting.saveFailed": "Не удалось сохранить. Изменения остались; попробуйте снова.", @@ -1028,7 +1043,7 @@ export const ru: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Скорость декодирования (оценка)", "logs.detail.reason.ttft_missing": "Для этого запроса не записано время до первого токена, поэтому измерять нечего.", - "logs.detail.reason.decode_window_too_short": "Окно после первого токена длилось меньше секунды — слишком мало для оценки скорости декодирования.", + "logs.detail.reason.decode_window_too_short": "Измеренное окно вывода длилось меньше секунды — слишком мало для оценки скорости декодирования.", "logs.detail.costTotal": "Эквивалент по прайс-листу", "logs.detail.totalTokens": "Всего токенов", "logs.detail.matchedKey": "Совпавший ключ цены", @@ -1288,9 +1303,12 @@ export const ru: Record = { "usage.col.reported": "Сообщено", "usage.col.inputTokens": "Входные токены", "usage.col.outputTokens": "Выходные токены", + "usage.col.tokPerSec": "Выходные tok/s", "usage.col.cacheHits": "Попадания в кэш", "usage.col.cacheWrites": "Записи в кэш", "usage.col.cacheHitRate": "Доля попаданий", + "usage.throughput.title": "Сквозная пропускная способность вывода: сумма выходных токенов, делённая на сумму астрономических длительностей (число измеренных попыток — {samples}). Включает ожидание до декодирования, поэтому ниже установившейся скорости декодирования; запросы без измеренных токенов и длительности исключаются.", + "usage.throughput.unmeasured": "Ни один запрос этой строки не сообщил одновременно токены вывода и длительность — усреднять нечего.", "usage.cacheHitRate.partial": "Среднее рассчитано по {measured} из {total} входных токенов; для остальных запросов сведения о кэше не поступили.", "usage.cacheHitRate.unmeasured": "Ни один запрос в этой строке не содержал сведений о кэше, поэтому среднюю долю попаданий рассчитать нельзя.", "usage.unavailable": "—", @@ -2007,6 +2025,16 @@ export const ru: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Уровень рассуждений по умолчанию", + "integrations.droidReasoning.description": "Значение по умолчанию применяется, только если запрос не задаёт уровень рассуждений. Отключите Factory Droid, чтобы удалить сохранённые значения.", + "integrations.droidReasoning.noDefault": "Без значения по умолчанию", + "integrations.droidReasoning.noEfforts": "Нет доступных вариантов уровня рассуждения", + "integrations.droidReasoning.noModels": "Нет доступных экспортированных моделей.", + "integrations.droidReasoning.modelDefault": "Уровень рассуждений по умолчанию для {model}", + "integrations.droidReasoning.unsupportedTitle": "Некоторые сохранённые значения больше не поддерживаются. Удалите их, затем сохраните / проверьте изменения, чтобы применить их.", + "integrations.droidReasoning.unsupportedEffort": "Неподдерживаемый уровень: {effort}", + "integrations.droidReasoning.clear": "Удалить значение", + "integrations.droidReasoning.saveReview": "Сохранить / проверить изменения", "integrations.aside.profilesTitle": "Профили Aside", "integrations.aside.profilesHint": "Выберите профили, в которые будут добавлены выбранные модели. Активный профиль Aside не изменится.", "integrations.aside.all": "Синхронизировать все профили", diff --git a/gui/src/i18n/shared.ts b/gui/src/i18n/shared.ts index 30ba5b8c031..e0acadf3f7c 100644 --- a/gui/src/i18n/shared.ts +++ b/gui/src/i18n/shared.ts @@ -14,6 +14,7 @@ export const LOCALES: { code: Locale; htmlLang: string }[] = [ { code: "ja", htmlLang: "ja" }, { code: "tr", htmlLang: "tr" }, { code: "vi", htmlLang: "vi" }, + { code: "pt", htmlLang: "pt-BR" }, ]; const LANG_KEY = "ocx-lang"; @@ -23,7 +24,7 @@ let activeLocale: Locale | null = null; export function detectInitial(): Locale { try { const stored = localStorage.getItem(LANG_KEY); - if (stored === "en" || stored === "de" || stored === "fr" || stored === "ko" || stored === "zh" || stored === "zh-TW" || stored === "ru" || stored === "ja" || stored === "tr" || stored === "vi") return stored; + if (stored === "en" || stored === "de" || stored === "fr" || stored === "ko" || stored === "zh" || stored === "zh-TW" || stored === "ru" || stored === "ja" || stored === "tr" || stored === "vi" || stored === "pt") return stored; } catch { /* ignore */ } const nav = typeof navigator !== "undefined" && navigator?.language ? navigator.language.toLowerCase() : "en"; if (nav.startsWith("de")) return "de"; @@ -45,6 +46,7 @@ export function detectInitial(): Locale { if (nav.startsWith("ja")) return "ja"; if (nav.startsWith("tr")) return "tr"; if (nav.startsWith("vi")) return "vi"; + if (nav.startsWith("pt")) return "pt"; return "en"; } diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 596ddc56929..1dccc2d3623 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -175,6 +175,10 @@ export const tr: Record = { "theme.light": "Açık", "theme.dark": "Koyu", "theme.system": "Sistem", + "zoom.label": "Yakınlaştırma", + "zoom.out": "Uzaklaştır", + "zoom.in": "Yakınlaştır", + "zoom.reset": "Yakınlaştırmayı %100'e sıfırla", "lang.label": "Dil", "lang.nativeName": "Türkçe", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -445,14 +449,25 @@ export const tr: Record = { "compactionRouting.triggersManual": "Yalnızca manuel /compact", "compactionRouting.triggersBoth": "Manuel ve otomatik", "compactionRouting.triggersAuto": "Yalnızca otomatik", + "compactionRouting.sources": "Kaynaklar", + "compactionRouting.sourcesAll": "Tüm konuşma modelleri", + "compactionRouting.sourcesSelected": "Yalnızca seçili kaynaklar", + "compactionRouting.sourcesProviders": "Tüm sağlayıcılar", + "compactionRouting.sourcesModels": "Tekil modeller", + "compactionRouting.sourcesSaved": "Geçerli katalogda olmayan kayıtlı seçiciler", + "compactionRouting.sourcesNone": "En az bir kaynak seçin veya tüm konuşma modellerine dönün.", + "compactionRouting.sourcesHint": "Geçersiz kılmayı yalnızca özetleme isteğinin kaynak modeli seçilen modelle veya provider/* ile eşleştiğinde uygular. Eşleşmeyen istekler kendi modelini kullanır.", + "compactionRouting.sourcesGridTitle": "Kaynak modeli aşağıdakilerle eşleşen özetleme isteklerini yeniden yönlendir:", "compactionRouting.currentModel": "Konuşma modelini kullan", "compactionRouting.currentEffort": "İsteğin düzeyini koru", "compactionRouting.effortHint": "Akıl yürütme, özetleme uç noktası destekliyorsa uygulanır. Model tüm konuşmayı kabul edebilmelidir.", "compactionRouting.dataNotice": "Kapsama giren bir özetleme isteği, konuşma başka bir sağlayıcıda yürütülse bile konuşmanın tamamını özetlenmek üzere seçilen modelin sağlayıcısına gönderir.", "compactionRouting.autoNotice": "Otomatik özetleme kendiliğinden çalışır; bu nedenle uzun bir konuşma siz istemeden o sağlayıcıya gidebilir.", "compactionRouting.providerWarning": "Bu ayarla kapsama giren her özetleme isteği, konuşmanın tüm içeriğini özetlenmek üzere {provider} sağlayıcısına gönderir.", - "compactionRouting.comboWarning": "Bu ayarla kapsama giren her özetleme isteği, konuşmanın tüm içeriğini özetlenmek üzere {combo} kombosuna gönderir. Kombo hedeflerini ({providers}) sırayla dener ve yanıt veren ilkini kullanır; dolayısıyla bunlardan herhangi biri konuşmayı alabilir.", + "compactionRouting.comboWarning": "Bu ayarla kapsama giren her özetleme isteği, konuşmanın tüm içeriğini özetlenmek üzere {combo} kombosuna gönderir. Kombo, yönlendirme stratejisine göre hedeflerinden ({providers}) birini seçer, ancak yeniden denenebilir bir hata sonrasında aynı tam konuşma isteğini başka bir hedefte yeniden deneyebilir; dolayısıyla bu hedeflerden biri veya birden fazlası konuşmayı alabilir.", "compactionRouting.comboProvidersUnknown": "yapılandırılmış hedef sağlayıcıları", + "compactionRouting.providerWarningScoped": "Bu ayarla yalnızca kaynak modeli {sources} ile eşleşen özetleme istekleri konuşmanın tüm içeriğini {provider} hedefine gönderir; eşleşmeyen istekler kendi modelini kullanır.", + "compactionRouting.comboWarningScoped": "Bu ayarla yalnızca kaynak modeli {sources} ile eşleşen özetleme istekleri konuşmanın tüm içeriğini {combo} kombosuna gönderir; eşleşmeyen istekler kendi modelini kullanır. Kombo, yönlendirme stratejisine göre hedeflerinden ({providers}) birini seçer, ancak yeniden denenebilir bir hata sonrasında aynı tam konuşma isteğini başka bir hedefte yeniden deneyebilir; dolayısıyla bu hedeflerden biri veya birden fazlası konuşmayı alabilir.", "compactionRouting.loadFailed": "Özetleme ayarları yüklenemedi.", "compactionRouting.saved": "Özetleme ayarları kaydedildi.", "compactionRouting.saveFailed": "Kaydedilemedi. Değişiklikleriniz korunuyor; tekrar deneyin.", @@ -1055,7 +1070,7 @@ export const tr: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Çözme hızı (tahmini)", "logs.detail.reason.ttft_missing": "Bu istek için ilk token süresi kaydedilmediğinden ölçülecek bir çözme aralığı yok.", - "logs.detail.reason.decode_window_too_short": "İlk token sonrasındaki aralık bir saniyenin altındaydı; çözme hızını tahmin etmek için fazla kısa.", + "logs.detail.reason.decode_window_too_short": "Ölçülen çıktı aralığı bir saniyenin altındaydı; çözme hızını tahmin etmek için fazla kısa.", "logs.detail.costTotal": "Liste fiyatı eşdeğeri", "logs.detail.totalTokens": "Toplam jeton", "logs.detail.matchedKey": "Eşleşen anahtar", @@ -1315,9 +1330,12 @@ export const tr: Record = { "usage.col.reported": "Bildirilen", "usage.col.inputTokens": "Girdi jetonları", "usage.col.outputTokens": "Çıktı jetonları", + "usage.col.tokPerSec": "Çıktı tok/s", "usage.col.cacheHits": "Önbellek isabetleri", "usage.col.cacheWrites": "Önbellek yazma", "usage.col.cacheHitRate": "İsabet oranı", + "usage.throughput.title": "Uçtan uca çıktı verimi: {samples} ölçülen deneme için çıktı jetonları toplamı ÷ geçen süre toplamı. Kod çözme öncesi bekleme süresini içerir, bu yüzden kararlı kod çözme hızının altındadır; ölçülen jetonu ve süresi olmayan istekler hariç tutulur.", + "usage.throughput.unmeasured": "Bu satırdaki hiçbir istek hem çıktı jetonlarını hem de süreyi bildirmedi; ortalanacak verim yok.", "usage.cacheHitRate.partial": "Toplam {total} girdi jetonunun {measured} kadarı üzerinden ortalama hesaplandı; kalan isteklerde önbellek ayrıntısı bildirilmedi.", "usage.cacheHitRate.unmeasured": "Bu satırdaki hiçbir istek önbellek ayrıntısı bildirmediği için hesaplanabilecek bir ortalama isabet oranı yok.", "usage.unavailable": "—", @@ -2034,6 +2052,16 @@ export const tr: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Varsayılan akıl yürütme düzeyi", + "integrations.droidReasoning.description": "Varsayılan yalnızca istek akıl yürütme düzeyi belirtmediğinde kullanılır. Kaydedilmiş varsayılanları kaldırmak için Factory Droid'u devre dışı bırakın.", + "integrations.droidReasoning.noDefault": "Varsayılan yok", + "integrations.droidReasoning.noEfforts": "Kullanılabilir akıl yürütme düzeyi seçeneği yok", + "integrations.droidReasoning.noModels": "Dışa aktarılmış model yok.", + "integrations.droidReasoning.modelDefault": "{model} için varsayılan akıl yürütme düzeyi", + "integrations.droidReasoning.unsupportedTitle": "Kaydedilmiş bazı varsayılanlar artık desteklenmiyor. Bunları temizleyin, ardından uygulamak için değişiklikleri kaydedip inceleyin.", + "integrations.droidReasoning.unsupportedEffort": "Desteklenmeyen düzey: {effort}", + "integrations.droidReasoning.clear": "Varsayılanı temizle", + "integrations.droidReasoning.saveReview": "Değişiklikleri kaydet / incele", "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", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index fda93dd64fb..b5e1c921f4b 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -173,6 +173,10 @@ export const vi: Record = { "theme.light": "Sáng", "theme.dark": "Tối", "theme.system": "Hệ thống", + "zoom.label": "Thu phóng", + "zoom.out": "Thu nhỏ", + "zoom.in": "Phóng to", + "zoom.reset": "Đặt lại thu phóng về 100%", "lang.label": "Ngôn ngữ", "lang.nativeName": "Tiếng Việt", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -443,14 +447,25 @@ export const vi: Record = { "compactionRouting.triggersManual": "Chỉ /compact thủ công", "compactionRouting.triggersBoth": "Thủ công và tự động", "compactionRouting.triggersAuto": "Chỉ tự động", + "compactionRouting.sources": "Nguồn áp dụng", + "compactionRouting.sourcesAll": "Tất cả model hội thoại", + "compactionRouting.sourcesSelected": "Chỉ nguồn đã chọn", + "compactionRouting.sourcesProviders": "Toàn bộ nhà cung cấp", + "compactionRouting.sourcesModels": "Từng model", + "compactionRouting.sourcesSaved": "Bộ chọn đã lưu không có trong danh mục hiện tại", + "compactionRouting.sourcesNone": "Chọn ít nhất một nguồn, hoặc quay lại tất cả model hội thoại.", + "compactionRouting.sourcesHint": "Chỉ áp dụng ghi đè khi model nguồn của yêu cầu nén khớp với model đã chọn hoặc provider/*. Các yêu cầu không khớp vẫn dùng model của chính mình.", + "compactionRouting.sourcesGridTitle": "Định tuyến lại yêu cầu nén có model nguồn khớp với:", "compactionRouting.currentModel": "Dùng model của cuộc trò chuyện", "compactionRouting.currentEffort": "Giữ mức suy luận của yêu cầu", "compactionRouting.effortHint": "Mức suy luận được áp dụng nếu điểm cuối nén hỗ trợ. Model phải tiếp nhận được toàn bộ cuộc trò chuyện.", "compactionRouting.dataNotice": "Yêu cầu nén thuộc phạm vi áp dụng sẽ gửi toàn bộ cuộc trò chuyện đến nhà cung cấp của model đã chọn để tóm tắt, ngay cả khi cuộc trò chuyện đang chạy ở nhà cung cấp khác.", "compactionRouting.autoNotice": "Nén tự động tự chạy, nên một cuộc trò chuyện dài có thể được gửi đến nhà cung cấp đó mà bạn không yêu cầu.", "compactionRouting.providerWarning": "Với thiết lập này, mỗi yêu cầu nén thuộc phạm vi áp dụng sẽ gửi toàn bộ nội dung cuộc trò chuyện đến {provider} để tóm tắt.", - "compactionRouting.comboWarning": "Với thiết lập này, mỗi yêu cầu nén thuộc phạm vi áp dụng sẽ gửi toàn bộ nội dung cuộc trò chuyện đến combo {combo} để tóm tắt. Combo thử lần lượt các đích ({providers}) và dùng đích trả lời đầu tiên, nên bất kỳ đích nào cũng có thể nhận cuộc trò chuyện.", + "compactionRouting.comboWarning": "Với thiết lập này, mỗi yêu cầu nén thuộc phạm vi áp dụng sẽ gửi toàn bộ nội dung cuộc trò chuyện đến combo {combo} để tóm tắt. Combo chọn một trong các đích ({providers}) theo chiến lược định tuyến của nó, nhưng sau lỗi có thể thử lại, nó có thể thử lại cùng một yêu cầu chứa toàn bộ cuộc trò chuyện ở một đích khác, nên một hoặc nhiều đích có thể nhận cuộc trò chuyện.", "compactionRouting.comboProvidersUnknown": "các nhà cung cấp đích đã cấu hình của nó", + "compactionRouting.providerWarningScoped": "Với thiết lập này, chỉ các yêu cầu nén có model nguồn khớp với {sources} mới gửi toàn bộ nội dung cuộc trò chuyện đến {provider} để tóm tắt; yêu cầu không khớp vẫn dùng model nguồn.", + "compactionRouting.comboWarningScoped": "Với thiết lập này, chỉ các yêu cầu nén có model nguồn khớp với {sources} mới gửi toàn bộ nội dung cuộc trò chuyện đến combo {combo} để tóm tắt; yêu cầu không khớp vẫn dùng model nguồn. Combo chọn một trong các đích ({providers}) theo chiến lược định tuyến của nó, nhưng sau lỗi có thể thử lại, nó có thể thử lại cùng một yêu cầu chứa toàn bộ cuộc trò chuyện ở một đích khác, nên một hoặc nhiều đích có thể nhận cuộc trò chuyện.", "compactionRouting.loadFailed": "Không thể tải cài đặt nén.", "compactionRouting.saved": "Đã lưu cài đặt nén.", "compactionRouting.saveFailed": "Không thể lưu. Thay đổi của bạn vẫn còn; hãy thử lại.", @@ -1049,7 +1064,7 @@ export const vi: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "Tốc độ decode (ước tính)", "logs.detail.reason.ttft_missing": "Không ghi nhận thời gian đến token đầu tiên (time-to-first-token) cho request này, nên không có cửa sổ decode nào để đo lường.", - "logs.detail.reason.decode_window_too_short": "Cửa sổ sau token đầu tiên nhỏ hơn một giây, quá ngắn để ước tính tốc độ decode.", + "logs.detail.reason.decode_window_too_short": "Cửa sổ đầu ra đo được nhỏ hơn một giây, quá ngắn để ước tính tốc độ decode.", "logs.detail.costTotal": "Tương đương giá niêm yết", "logs.detail.totalTokens": "Tổng số tokens", "logs.detail.matchedKey": "Khoá giá trùng khớp (Matched price key)", @@ -1305,9 +1320,12 @@ export const vi: Record = { "usage.col.reported": "Đã báo cáo", "usage.col.inputTokens": "Token đầu vào", "usage.col.outputTokens": "Token đầu ra", + "usage.col.tokPerSec": "Đầu ra tok/s", "usage.col.cacheHits": "Lượt truy cập cache", "usage.col.cacheWrites": "Lần ghi cache", "usage.col.cacheHitRate": "Tỷ lệ truy cập cache", + "usage.throughput.title": "Thông lượng đầu ra end-to-end: tổng token đầu ra chia cho tổng thời gian đồng hồ qua {samples} lần thử được đo. Bao gồm thời gian chờ trước khi giải mã nên thấp hơn tốc độ giải mã ổn định; các yêu cầu không đo được token và thời lượng bị loại.", + "usage.throughput.unmeasured": "Không có yêu cầu nào trong dòng này báo cả token đầu ra lẫn thời lượng, nên không có thông lượng để trung bình.", "usage.cacheHitRate.partial": "Giá trị trung bình được tính dựa trên {measured} trong tổng số {total} token đầu vào; các yêu cầu còn lại không cung cấp thông tin chi tiết về cache.", "usage.cacheHitRate.unmeasured": "Không có yêu cầu nào trong hàng này cung cấp thông tin chi tiết về cache, nên không thể tính tỷ lệ truy cập cache trung bình.", "usage.unavailable": "—", @@ -2004,6 +2022,16 @@ export const vi: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "Mức suy luận mặc định", + "integrations.droidReasoning.description": "Chỉ dùng mức mặc định khi yêu cầu không chỉ định mức suy luận. Tắt Factory Droid để xóa các mức mặc định đã lưu.", + "integrations.droidReasoning.noDefault": "Không có mặc định", + "integrations.droidReasoning.noEfforts": "Không có tùy chọn mức độ suy luận khả dụng", + "integrations.droidReasoning.noModels": "Không có mô hình nào được xuất.", + "integrations.droidReasoning.modelDefault": "Mức suy luận mặc định cho {model}", + "integrations.droidReasoning.unsupportedTitle": "Một số mức mặc định đã lưu không còn được hỗ trợ. Hãy xóa chúng, sau đó lưu / xem lại thay đổi để áp dụng.", + "integrations.droidReasoning.unsupportedEffort": "Mức không được hỗ trợ: {effort}", + "integrations.droidReasoning.clear": "Xóa mặc định", + "integrations.droidReasoning.saveReview": "Lưu / xem lại thay đổi", "integrations.aside.profilesTitle": "Cấu hình Aside", "integrations.aside.profilesHint": "Chọn profile nào sẽ nhận các model được chọn. Profile đang hoạt động của Aside sẽ được giữ nguyên.", "integrations.aside.all": "Đồng bộ tất cả profile", diff --git a/gui/src/i18n/vision-reasoning-labels.ts b/gui/src/i18n/vision-reasoning-labels.ts index a551f7dc4a6..600272c2510 100644 --- a/gui/src/i18n/vision-reasoning-labels.ts +++ b/gui/src/i18n/vision-reasoning-labels.ts @@ -13,6 +13,7 @@ const VISION_REASONING_LABELS = { ja: { low: "低", medium: "中", high: "高", xhigh: "非常に高い", max: "最大" }, tr: { low: "Düşük", medium: "Orta", high: "Yüksek", xhigh: "Çok yüksek", max: "Maksimum" }, vi: { low: "Thấp", medium: "Trung bình", high: "Cao", xhigh: "Rất cao", max: "Tối đa" }, + pt: { low: "Baixo", medium: "Médio", high: "Alto", xhigh: "Muito alto", max: "Máximo" }, } satisfies Record>; /** Localized display label for the wire-level vision reasoning value. */ diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 46d84a91689..d29c2669583 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -141,6 +141,10 @@ export const zhTW: Record = { "theme.light": "淺色", "theme.dark": "深色", "theme.system": "跟隨系統", + "zoom.label": "縮放", + "zoom.out": "縮小", + "zoom.in": "放大", + "zoom.reset": "重設縮放為 100%", "lang.label": "語言", "errorBoundary.title": "頁面載入失敗", "errorBoundary.message": "此部分在渲染時發生錯誤。請重新載入後再試。", @@ -321,14 +325,25 @@ export const zhTW: Record = { "compactionRouting.triggersManual": "僅手動 /compact", "compactionRouting.triggersBoth": "手動與自動", "compactionRouting.triggersAuto": "僅自動", + "compactionRouting.sources": "來源範圍", + "compactionRouting.sourcesAll": "所有對話模型", + "compactionRouting.sourcesSelected": "僅選中的來源", + "compactionRouting.sourcesProviders": "整個提供商", + "compactionRouting.sourcesModels": "單個模型", + "compactionRouting.sourcesSaved": "已保存但不在目前目錄中的選取器", + "compactionRouting.sourcesNone": "至少選擇一個來源,或改回所有對話模型。", + "compactionRouting.sourcesHint": "僅當壓縮請求的來源模型符合所選模型或 provider/* 時才使用覆寫;其他請求仍使用自身模型壓縮。", + "compactionRouting.sourcesGridTitle": "改路由來源模型符合以下選擇器的壓縮請求:", "compactionRouting.currentModel": "使用對話模型", "compactionRouting.currentEffort": "保留請求的推理強度", "compactionRouting.effortHint": "推理設定僅在壓縮端點支援時生效。模型必須能容納完整對話。", "compactionRouting.dataNotice": "涵蓋的壓縮請求會將整個對話傳送給所選模型的供應商進行摘要,即使對話正在其他供應商上執行。", "compactionRouting.autoNotice": "自動壓縮會自行觸發,因此長對話可能在你沒有主動要求的情況下被傳送給該供應商。", "compactionRouting.providerWarning": "啟用此設定後,每個涵蓋的壓縮請求都會將完整對話內容傳送給 {provider} 進行摘要。", - "compactionRouting.comboWarning": "啟用此設定後,每個涵蓋的壓縮請求都會將完整對話內容傳送給組合 {combo} 以進行摘要。該組合會依序嘗試其目標({providers}),並使用第一個回應的目標,因此其中任一個都可能收到該對話。", + "compactionRouting.comboWarning": "啟用此設定後,每個涵蓋的壓縮請求都會將完整對話內容傳送給組合 {combo} 以進行摘要。該組合會依其路由策略選擇其中一個目標({providers}),但在發生可重試失敗後,可能於其他目標重試相同的完整對話請求,因此其中一個或多個目標都可能收到該對話。", "compactionRouting.comboProvidersUnknown": "其已設定的目標供應商", + "compactionRouting.providerWarningScoped": "啟用此設定後,僅當壓縮請求的來源模型符合 {sources} 時,完整對話內容才會傳送給 {provider} 進行摘要;其他請求仍使用自身模型壓縮。", + "compactionRouting.comboWarningScoped": "啟用此設定後,僅當壓縮請求的來源模型符合 {sources} 時,完整對話內容才會傳送給組合 {combo} 進行摘要;其他請求仍使用自身模型壓縮。該組合會依其路由策略選擇其中一個目標({providers}),但在發生可重試失敗後,可能於其他目標重試相同的完整對話請求,因此其中一個或多個目標都可能收到該對話。", "compactionRouting.loadFailed": "無法載入壓縮設定。", "compactionRouting.saved": "壓縮設定已儲存。", "compactionRouting.saveFailed": "儲存失敗。變更仍然保留,請重試。", @@ -829,7 +844,7 @@ export const zhTW: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "解碼速率(估算)", "logs.detail.reason.ttft_missing": "此請求沒有記錄第一個 token 的時間,因此沒有可測量的解碼區間。", - "logs.detail.reason.decode_window_too_short": "第一個 token 之後的區間不到一秒,太短,無法估算解碼速率。", + "logs.detail.reason.decode_window_too_short": "測得的輸出區間不到一秒,太短,無法估算解碼速率。", "logs.detail.costTotal": "標價折算", "logs.detail.totalTokens": "Token 總數", "logs.detail.matchedKey": "符合的 jawcode 鍵", @@ -1023,9 +1038,12 @@ export const zhTW: Record = { "usage.col.reported": "已上報", "usage.col.inputTokens": "輸入 Token", "usage.col.outputTokens": "輸出 Token", + "usage.col.tokPerSec": "輸出 tok/s", "usage.col.cacheHits": "快取命中", "usage.col.cacheWrites": "快取寫入", "usage.col.cacheHitRate": "命中率", + "usage.throughput.title": "端到端輸出吞吐:輸出 token 總和 ÷ 牆鐘時長總和,樣本為 {samples} 個同時上報了 token 與時長的嘗試。包含解碼前的等待時間,因此低於穩態解碼速度;未上報 token 或時長的請求不計入。", + "usage.throughput.unmeasured": "該行沒有請求同時上報輸出 token 與時長,無法計算吞吐。", "usage.cacheHitRate.partial": "全部 {total} 個輸入 Token 中,有 {measured} 個提供了快取明細,命中率取這些資料的平均值;其餘請求未回報快取明細。", "usage.cacheHitRate.unmeasured": "此資料列的所有請求都未回報快取明細,因此無法計算平均命中率。", "usage.unavailable": "—", @@ -2896,6 +2914,16 @@ export const zhTW: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "預設推理強度", + "integrations.droidReasoning.description": "只有在要求未指定推理強度時才會使用預設值。停用 Factory Droid 會移除這些已儲存的預設值。", + "integrations.droidReasoning.noDefault": "無預設值", + "integrations.droidReasoning.noEfforts": "沒有可用的推理強度選項", + "integrations.droidReasoning.noModels": "沒有可匯出的模型。", + "integrations.droidReasoning.modelDefault": "{model} 的預設推理強度", + "integrations.droidReasoning.unsupportedTitle": "部分已儲存的預設值已不再支援。清除後,請儲存 / 檢查變更以套用。", + "integrations.droidReasoning.unsupportedEffort": "不支援的強度:{effort}", + "integrations.droidReasoning.clear": "清除預設值", + "integrations.droidReasoning.saveReview": "儲存 / 檢查變更", "integrations.aside.profilesTitle": "Aside 設定檔", "integrations.aside.profilesHint": "選擇要接收所選模型的設定檔。Aside 目前使用的設定檔不會改變。", "integrations.aside.all": "同步所有設定檔", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 0ffc79db81f..a68297f5d00 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -238,6 +238,10 @@ export const zh: Record = { "theme.light": "浅色", "theme.dark": "深色", "theme.system": "跟随系统", + "zoom.label": "缩放", + "zoom.out": "缩小", + "zoom.in": "放大", + "zoom.reset": "将缩放重置为 100%", "lang.label": "语言", "lang.nativeName": "中文", "provider.name.commandCodeAuth": "Command Code - Auth", @@ -439,14 +443,25 @@ export const zh: Record = { "compactionRouting.triggersManual": "仅手动 /compact", "compactionRouting.triggersBoth": "手动和自动", "compactionRouting.triggersAuto": "仅自动", + "compactionRouting.sources": "来源范围", + "compactionRouting.sourcesAll": "所有对话模型", + "compactionRouting.sourcesSelected": "仅选中的来源", + "compactionRouting.sourcesProviders": "整个提供商", + "compactionRouting.sourcesModels": "单个模型", + "compactionRouting.sourcesSaved": "已保存但不在当前目录中的选择器", + "compactionRouting.sourcesNone": "至少选择一个来源,或改回所有对话模型。", + "compactionRouting.sourcesHint": "仅当压缩请求的来源模型匹配所选模型或 provider/* 时才使用覆盖;其他请求仍使用自身模型压缩。", + "compactionRouting.sourcesGridTitle": "改路由来源模型匹配以下选择器的压缩请求:", "compactionRouting.currentModel": "使用对话模型", "compactionRouting.currentEffort": "保留请求的推理强度", "compactionRouting.effortHint": "推理设置仅在压缩端点支持时生效。模型必须能容纳完整对话。", "compactionRouting.dataNotice": "被覆盖的压缩请求会将整个对话发送给所选模型的提供商进行摘要,即使对话正在其他提供商上运行。", "compactionRouting.autoNotice": "自动压缩会自行触发,因此长对话可能在你没有主动要求的情况下被发送给该提供商。", "compactionRouting.providerWarning": "启用此设置后,每个被覆盖的压缩请求都会将完整对话内容发送给 {provider} 进行摘要。", - "compactionRouting.comboWarning": "启用此设置后,每个被覆盖的压缩请求都会将完整对话内容发送给组合 {combo} 以进行摘要。该组合会按顺序尝试其目标({providers}),并使用第一个响应的目标,因此其中任意一个都可能收到该对话。", + "compactionRouting.comboWarning": "启用此设置后,每个被覆盖的压缩请求都会将完整对话内容发送给组合 {combo} 以进行摘要。该组合会按其路由策略选择其中一个目标({providers}),但在发生可重试失败后,可能在其他目标重试相同的完整对话请求,因此其中一个或多个目标都可能收到该对话。", "compactionRouting.comboProvidersUnknown": "其已配置的目标提供商", + "compactionRouting.providerWarningScoped": "启用此设置后,仅当压缩请求的来源模型匹配 {sources} 时,完整对话内容才会发送给 {provider} 进行摘要;其他请求仍使用自身模型压缩。", + "compactionRouting.comboWarningScoped": "启用此设置后,仅当压缩请求的来源模型匹配 {sources} 时,完整对话内容才会发送给组合 {combo} 进行摘要;其他请求仍使用自身模型压缩。该组合会按其路由策略选择其中一个目标({providers}),但在发生可重试失败后,可能在其他目标重试相同的完整对话请求,因此其中一个或多个目标都可能收到该对话。", "compactionRouting.loadFailed": "无法加载压缩设置。", "compactionRouting.saved": "压缩设置已保存。", "compactionRouting.saveFailed": "保存失败。更改仍然保留,请重试。", @@ -1023,7 +1038,7 @@ export const zh: Record = { "logs.detail.ttft": "TTFT", "logs.detail.decodeTokPerSec": "解码速率(估算)", "logs.detail.reason.ttft_missing": "该请求没有记录首个 token 的时间,因此没有可测量的解码区间。", - "logs.detail.reason.decode_window_too_short": "首个 token 之后的区间不足一秒,太短,无法估算解码速率。", + "logs.detail.reason.decode_window_too_short": "测得的输出区间不足一秒,太短,无法估算解码速率。", "logs.detail.costTotal": "标价折算", "logs.detail.totalTokens": "Token 总数", "logs.detail.matchedKey": "匹配的价格键", @@ -1282,9 +1297,12 @@ export const zh: Record = { "usage.col.reported": "已上报", "usage.col.inputTokens": "输入 Token", "usage.col.outputTokens": "输出 Token", + "usage.col.tokPerSec": "输出 tok/s", "usage.col.cacheHits": "缓存命中", "usage.col.cacheWrites": "缓存写入", "usage.col.cacheHitRate": "命中率", + "usage.throughput.title": "端到端输出吞吐:输出 token 总和 ÷ 墙钟时长总和,样本为 {samples} 个同时上报了 token 与时长的尝试。包含解码前的等待时间,因此低于稳态解码速度;未上报 token 或时长的请求不计入。", + "usage.throughput.unmeasured": "该行没有请求同时上报输出 token 与时长,无法计算吞吐。", "usage.cacheHitRate.partial": "全部 {total} 个输入 Token 中,有 {measured} 个提供了缓存明细,命中率取这些数据的平均值;其余请求未报告缓存明细。", "usage.cacheHitRate.unmeasured": "此行没有任何请求报告缓存明细,因此无法计算平均命中率。", "usage.unavailable": "—", @@ -1500,6 +1518,16 @@ export const zh: Record = { "integrations.tab.cline": "Cline CLI", "integrations.tab.kilo": "Kilo", "integrations.tab.droid": "Factory Droid", + "integrations.droidReasoning.title": "默认推理强度", + "integrations.droidReasoning.description": "仅当请求未指定推理强度时才使用默认值。停用 Factory Droid 会删除这些已保存的默认值。", + "integrations.droidReasoning.noDefault": "无默认值", + "integrations.droidReasoning.noEfforts": "没有可用的推理强度选项", + "integrations.droidReasoning.noModels": "没有可导出的模型。", + "integrations.droidReasoning.modelDefault": "{model} 的默认推理强度", + "integrations.droidReasoning.unsupportedTitle": "部分已保存的默认值已不再受支持。清除后,请保存 / 检查更改以应用。", + "integrations.droidReasoning.unsupportedEffort": "不支持的强度:{effort}", + "integrations.droidReasoning.clear": "清除默认值", + "integrations.droidReasoning.saveReview": "保存 / 检查更改", "integrations.aside.profilesTitle": "Aside 配置文件", "integrations.aside.profilesHint": "选择要接收所选模型的配置文件。Aside 当前使用的配置文件不会改变。", "integrations.aside.all": "同步所有配置文件", diff --git a/gui/src/icons.tsx b/gui/src/icons.tsx index a30ba9f18d9..cf462aec3c4 100644 --- a/gui/src/icons.tsx +++ b/gui/src/icons.tsx @@ -21,6 +21,7 @@ export const IconHardDrive = (p: P) => ( (); export const IconX = (p: P) => (); export const IconPlus = (p: P) => (); +export const IconMinus = (p: P) => (); export const IconRefresh = (p: P) => (); export const IconPause = (p: P) => (); export const IconPlay = (p: P) => (); diff --git a/gui/src/lib/desktop-zoom.ts b/gui/src/lib/desktop-zoom.ts new file mode 100644 index 00000000000..25ce0d3a87f --- /dev/null +++ b/gui/src/lib/desktop-zoom.ts @@ -0,0 +1,121 @@ +/** + * Page zoom for the desktop shell, owned by the dashboard. + * + * Tauri's own zoom-hotkey polyfill keeps the level in a script variable that starts at 1 on + * every page load: a restart forgot the level, and so did the hop from the bootstrap page to + * the dashboard, after which the first shortcut jumped from the real level to 120%. The + * dashboard therefore keeps the level itself, remembers it, and drives the webview through the + * one command `desktop/src-tauri/capabilities/dashboard-zoom.json` already grants to this + * origin. Outside the shell the global is absent and every call is a no-op. + */ +import type { HostOs } from "./desktop-shell"; + +export const ZOOM_STORAGE_KEY = "ocx-desktop-zoom"; +export const ZOOM_MIN = 0.5; +export const ZOOM_MAX = 3; +export const ZOOM_STEP = 0.1; +export const ZOOM_DEFAULT = 1; + +export type ZoomAction = "in" | "out" | "reset"; + +declare global { + interface Window { + __TAURI_INTERNALS__?: { + invoke?: (command: string, args?: Record) => Promise; + }; + } +} + +/** + * Whether the dashboard drives zoom on this host. Windows keeps WebView2's native zoom, which + * the shell leaves enabled there, so a second owner would only disagree with it. + */ +export function zoomManagedOn(os: HostOs): boolean { + return os === "macos" || os === "linux"; +} + +/** Whole percent steps keep repeated presses from drifting into 1.2000000000000002. */ +export function clampZoom(value: number): number { + if (!Number.isFinite(value)) return ZOOM_DEFAULT; + const bounded = Math.min(Math.max(value, ZOOM_MIN), ZOOM_MAX); + return Math.round(bounded * 100) / 100; +} + +export function stepZoom(current: number, action: ZoomAction): number { + if (action === "reset") return ZOOM_DEFAULT; + return clampZoom(current + (action === "in" ? ZOOM_STEP : -ZOOM_STEP)); +} + +export function zoomPercent(zoom: number): number { + return Math.round(zoom * 100); +} + +export function readSavedZoom( + storage: Pick | undefined = typeof localStorage === "undefined" ? undefined : localStorage, +): number { + try { + const raw = storage?.getItem(ZOOM_STORAGE_KEY); + if (raw === null || raw === undefined) return ZOOM_DEFAULT; + return clampZoom(Number(raw)); + } catch { + return ZOOM_DEFAULT; + } +} + +export function writeSavedZoom( + zoom: number, + storage: Pick | undefined = typeof localStorage === "undefined" ? undefined : localStorage, +): void { + try { + storage?.setItem(ZOOM_STORAGE_KEY, String(zoom)); + } catch { + // A storageless context still zooms for the session. + } +} + +/** + * The key combinations the shell always answered to: Cmd on macOS, Ctrl elsewhere, with plus, + * minus and zero. `=` is accepted because plus shares its key on most layouts. + */ +export function zoomKeyAction( + event: Pick, + os: HostOs, +): ZoomAction | null { + const modifier = os === "macos" ? event.metaKey : event.ctrlKey; + if (!modifier) return null; + if (event.key === "-" || event.key === "_") return "out"; + if (event.key === "=" || event.key === "+") return "in"; + if (event.key === "0") return "reset"; + return null; +} + +/** Distance a Ctrl + wheel gesture must add up to before it steps once. */ +export const WHEEL_STEP_PX = 50; + +/** + * Ctrl + wheel, as before: up zooms in. A mouse notch is one step, while a touchpad pinch + * arrives as many tiny deltas, so the distance accumulates instead of stepping on every event. + */ +export function accumulateWheel( + accumulated: number, + deltaY: number, +): { accumulated: number; action: ZoomAction | null } { + if (!Number.isFinite(deltaY) || deltaY === 0) return { accumulated, action: null }; + // A change of direction starts a new gesture rather than cancelling the last one. + const carried = Math.sign(accumulated) === Math.sign(deltaY) ? accumulated : 0; + const total = carried + deltaY; + if (Math.abs(total) < WHEEL_STEP_PX) return { accumulated: total, action: null }; + return { accumulated: 0, action: total < 0 ? "in" : "out" }; +} + +/** Asks the shell to set the webview zoom. Resolves false when there is no shell to ask. */ +export async function applyWebviewZoom(zoom: number): Promise { + const invoke = window.__TAURI_INTERNALS__?.invoke ?? window.__TAURI__?.core?.invoke; + if (!invoke) return false; + try { + await invoke("plugin:webview|set_webview_zoom", { value: zoom }); + return true; + } catch { + return false; + } +} diff --git a/gui/src/main.tsx b/gui/src/main.tsx index 49db60f99e5..332f0bb3869 100644 --- a/gui/src/main.tsx +++ b/gui/src/main.tsx @@ -19,6 +19,7 @@ import "./styles/lazycodex-role-models.css"; import "./styles/claude-desktop-picker.css"; import "./styles/anthropic-reset-grants.css"; import "./styles/star-onboarding.css"; +import "./styles/sidebar-zoom.css"; import "./styles/protocol-evidence.css"; import "./pages/tray.css"; diff --git a/gui/src/model-visibility.ts b/gui/src/model-visibility.ts index df73ff51862..2cece6b7115 100644 --- a/gui/src/model-visibility.ts +++ b/gui/src/model-visibility.ts @@ -82,11 +82,13 @@ export async function putModelVisibility( targets: ModelVisibilityTarget[], enabled: boolean, fetchImpl: typeof fetch = fetch, + signal?: AbortSignal, ): Promise { return fetchImpl(`${apiBase}/api/model-visibility`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope, provider, targets, enabled }), + ...(signal ? { signal } : {}), }); } diff --git a/gui/src/pages/Models.tsx b/gui/src/pages/Models.tsx index cfd1d89f057..6c0a446fb81 100644 --- a/gui/src/pages/Models.tsx +++ b/gui/src/pages/Models.tsx @@ -26,6 +26,7 @@ import { type ModelPickerOrderMode, type PickerOrderSettings, type PickerOrderSaved, type ModelPickerUsage, } from "../model-picker-order"; import { startVisibilityPoll } from "../visibility-poll"; +import { useModelVisibility } from "../use-model-visibility"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; import ErrorBoundary from "../components/ErrorBoundary"; @@ -48,8 +49,7 @@ import { } from "../models-groups"; import { fetchSelectedModels, - modelVisible, - putModelVisibility, + modelVisible as savedModelVisible, clientCatalogRefreshFailures, type ClientCatalogRefreshFailure, shouldApplyLoadGeneration, @@ -340,6 +340,18 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c const catalogMutationRef = useRef(false); const loadGenerationRef = useRef(0); const loadPendingRef = useRef(false); + const visibility = useModelVisibility(apiBase, { + onQueued: () => { ++loadGenerationRef.current; setStatus(""); }, + onBusy: value => { ++loadGenerationRef.current; loadPendingRef.current = false; catalogMutationRef.current = value; busyRef.current = value; setBusy(value); }, + onResponse: body => { + const failures = clientCatalogRefreshFailures(body); + if (failures !== undefined) setIntegrationFailures(failures); + }, + refresh: signal => load(true, signal), + onSettled: error => { setOk(!error); setStatus(t(error ?? "models.applied")); }, + }); + const modelVisible = (selected: ProviderModelMap, provider: string, id: string, native: boolean, blocked: boolean) => + visibility.visible(provider, id, native, savedModelVisible(selected, provider, id, native, blocked)); // multi_agent_v2 / ultra gate. null = endpoint unavailable (older proxy build) -> section hidden. const [v2, setV2] = useState(null); // #2465: per-provider model-preset state. Keyed by provider so one card's busy state cannot @@ -497,6 +509,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c }, [apiBase]); const fetchCatalog = useCallback(async (signal: AbortSignal): Promise => { + const generation = loadGenerationRef.current; const [modelsRes, capsRes, providersRes, selectionData] = await Promise.all([ // Every request carries the resource signal, so leaving the catalog tab cancels // the work rather than only discarding its result. @@ -528,7 +541,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c contextCapValues: capsData.values ?? capsData.caps ?? {}, contextCapValue: nextCapValue, } satisfies CachedModelsPage; - writeSessionListCache(cacheKey, next); + if (generation === loadGenerationRef.current) writeSessionListCache(cacheKey, next); return next; }, [apiBase, cacheKey]); @@ -552,11 +565,12 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c cacheKey, [apiBase], async (signal) => { + const generation = loadGenerationRef.current; const next = await fetchCatalog(signal); // A manual mutation refresh may have invalidated this request while its JSON was decoding. // Do not let the aborted catalog repaint controls after the newer result is applied. if (signal.aborted) throw new Error("models request aborted"); - applyCatalog(next); + if (!catalogMutationRef.current && generation === loadGenerationRef.current) applyCatalog(next); return next; }, // Gated on the catalog tab: a 10-second poll that keeps running while the user @@ -888,7 +902,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c model.native === true, disabled.has(model.namespaced), )).length; - }, [disabled, models, selectedModels]); + }, [disabled, models, selectedModels, visibility.overrides]); /* * Quiet per-tab counts. A count is omitted, never zeroed, while it is unknown: the @@ -910,35 +924,8 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c targets: ModelVisibilityTarget[], enabled: boolean, ) => { - if (catalogMutationRef.current) return; - catalogMutationRef.current = true; - ++loadGenerationRef.current; - setBusy(true); - busyRef.current = true; - setStatus(""); - let errorKey: "models.saveFailed" | "models.networkError" | null = null; - 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 { - const refreshed = await load(true); - if (errorKey) { - setOk(false); - setStatus(t(errorKey)); - } else if (refreshed) { - setOk(true); - setStatus(t("models.applied")); - } - setBusy(false); - busyRef.current = false; - catalogMutationRef.current = false; - } + if (busyRef.current && !visibility.isRunning()) return; + visibility.enqueue(scope, provider, targets, enabled); }; const toggleProviderCap = async (provider: string) => { @@ -1589,8 +1576,8 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c ); })()} - - + +
{/* The label names the FUNCTION. It used to be `models.capValue` - "기본 128k" - which is a value masquerading as a name: even a @@ -1730,7 +1717,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c }} >
- void applyVisibility("models", provider, [{ id: m.id, native: m.native === true }], off)} disabled={busy || m.initialSelectionPending} label={m.native ? m.id : m.namespaced} /> + void applyVisibility("models", provider, [{ id: m.id, native: m.native === true }], off)} disabled={(busy && !visibility.pending) || m.initialSelectionPending} label={m.native ? m.id : m.namespaced} /> {m.initialSelectionPending && {t("models.initialSelectionPending")}} {/* #1711: listed and selectable, but every usable target is out of credit. Not a visibility change and not the operator's disable flag — the row is @@ -2568,7 +2555,7 @@ export default function Models({ apiBase, restartEpoch = 0, connected = false, c pickerMode: modelPickerOrderMode(pickerSettings?.pickerAvailable ?? [], pickerSettings?.pickerOrder ?? [], pickerSettings?.pickerOrderMode) })}> {controlsBlock} -
+