diff --git a/app/Sources/MenuBarCore/ProxyModels.swift b/app/Sources/MenuBarCore/ProxyModels.swift
index 0a1267303b3..bf9d0881ac2 100644
--- a/app/Sources/MenuBarCore/ProxyModels.swift
+++ b/app/Sources/MenuBarCore/ProxyModels.swift
@@ -163,6 +163,7 @@ public struct UsageAccountRow: Decodable, Equatable, Sendable {
}
public struct QuotaWindow: Decodable, Equatable, Sendable {
+ public let modelId: String?
public let label: String?
public let percent: Double?
public let resetAt: Double?
@@ -188,6 +189,7 @@ public struct QuotaReport: Decodable, Equatable, Sendable {
/// A provider-agnostic view of quota, since the window key differs per provider.
public struct NormalizedQuota: Equatable, Sendable {
+ public let modelId: String?
public let provider: String
public let providerLabel: String
public let percent: Double?
@@ -219,10 +221,10 @@ public extension QuotaReport {
let name = label ?? provider
var windows: [NormalizedQuota] = []
- func append(_ percent: Double?, _ windowLabel: String, _ resetAt: Double?) {
+ func append(_ percent: Double?, _ windowLabel: String, _ resetAt: Double?, _ modelId: String? = nil) {
guard percent != nil || resetAt != nil else { return }
windows.append(NormalizedQuota(
- provider: provider, providerLabel: name, percent: percent,
+ modelId: modelId, provider: provider, providerLabel: name, percent: percent,
windowLabel: windowLabel, resetAt: Self.date(from: resetAt)
))
}
@@ -232,7 +234,7 @@ public extension QuotaReport {
append(quota?.monthlyPercent, "month", quota?.monthlyResetAt)
for window in quota?.customWindows ?? [] {
- append(window.percent, window.label ?? "window", window.resetAt)
+ append(window.percent, window.label ?? "window", window.resetAt, window.modelId)
}
return windows
@@ -240,7 +242,8 @@ public extension QuotaReport {
/// The single window that best represents current pressure, for the compact row.
///
- /// Selection is **highest reported usage**, not longest horizon. Every window can
+ /// Diagnostic model rows are retained in details and excluded here. Selection is
+ /// **highest reported usage**, not longest horizon. Every aggregate window can
/// stop work: a provider at 99% of a five-hour limit and 10% of its monthly limit is
/// blocked right now, and showing the monthly 10% would paint that row green while
/// the user cannot make a request. Ties break toward the longer horizon, since that
@@ -250,7 +253,7 @@ public extension QuotaReport {
/// em dash rather than a misleading zero.
func normalized() -> NormalizedQuota {
let name = label ?? provider
- let windows = normalizedWindows()
+ let windows = normalizedWindows().filter { $0.modelId == nil }
// Longer horizons rank higher only as a tie-breaker.
func horizonRank(_ label: String) -> Int {
@@ -271,7 +274,7 @@ public extension QuotaReport {
} ?? windows.first
return preferred ?? NormalizedQuota(
- provider: provider, providerLabel: name, percent: nil,
+ modelId: nil, provider: provider, providerLabel: name, percent: nil,
windowLabel: "—", resetAt: nil
)
}
diff --git a/app/Sources/MenuBarCoreTests/ModelDecodingSuite.swift b/app/Sources/MenuBarCoreTests/ModelDecodingSuite.swift
index 0ec0012e621..f79580bbc49 100644
--- a/app/Sources/MenuBarCoreTests/ModelDecodingSuite.swift
+++ b/app/Sources/MenuBarCoreTests/ModelDecodingSuite.swift
@@ -230,6 +230,23 @@ enum ModelDecodingSuite {
t.equal(report.normalized().percent, 12)
}
+ t.test("quotas: model diagnostics stay detailed and cannot become compact pressure") {
+ let json = """
+ {"provider":"google-antigravity","quota":{"customWindows":[
+ {"label":"aux","modelId":"aux","percent":100},
+ {"label":"Gem","percent":20}]}}
+ """
+ let report = try decode(QuotaReport.self, json)
+ t.equal(report.normalized().percent, 20)
+ t.equal(report.normalized().windowLabel, "Gem")
+ t.equal(report.normalizedWindows().count, 2)
+ t.equal(report.quota?.customWindows?.first?.modelId, "aux")
+ t.equal(report.normalizedWindows().first?.modelId, "aux")
+ let onlyModel = try decode(QuotaReport.self,
+ #"{"provider":"p","quota":{"customWindows":[{"label":"aux","modelId":"aux","percent":100}]}}"#)
+ t.isNil(onlyModel.normalized().percent, "model-only compact percent")
+ }
+
t.test("quotas: an absent quota normalizes to a nil percent") {
let normalized = try decode(QuotaReport.self, #"{"provider":"p","label":"P"}"#).normalized()
t.isNil(normalized.percent, "percent")
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index b994d029dc7..27029e21b8c 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -2645,7 +2645,7 @@ dependencies = [
[[package]]
name = "opencodex-desktop"
-version = "2.86.0"
+version = "2.87.0"
dependencies = [
"base64 0.22.1",
"dbus",
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index a65970d63d9..bf22aa60fb7 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "opencodex-desktop"
-version = "2.86.0"
+version = "2.87.0"
description = "OpenCodex desktop shell"
authors = ["OpenCodex contributors"]
license = "MIT"
diff --git a/desktop/src-tauri/src/claim.rs b/desktop/src-tauri/src/claim.rs
index 22d0cfcbc24..d7e2d53ef51 100644
--- a/desktop/src-tauri/src/claim.rs
+++ b/desktop/src-tauri/src/claim.rs
@@ -163,7 +163,12 @@ pub async fn run(app: &AppHandle, argv: Vec, deadline: Instant) -> Claim
return ClaimResult::Failed(format!("the bundled CLI could not be started ({error})"))
}
};
- match timeout_at(deadline, command.output()).await {
+ match timeout_at(
+ deadline,
+ crate::process_spawn::with_spawn_async(command.output()),
+ )
+ .await
+ {
Ok(Ok(output)) => read(output.status.code(), &output.stdout, &output.stderr),
Ok(Err(error)) => {
ClaimResult::Failed(format!("the bundled CLI could not be run ({error})"))
diff --git a/desktop/src-tauri/src/cli_command.rs b/desktop/src-tauri/src/cli_command.rs
index a89361e9fd7..7e05e54f85e 100644
--- a/desktop/src-tauri/src/cli_command.rs
+++ b/desktop/src-tauri/src/cli_command.rs
@@ -454,18 +454,20 @@ mod tests {
}
fn selected_observation_case(case: &str) {
let t = Temp::new();
- let result = std::process::Command::new(std::env::current_exe().unwrap())
- .args([
- "--exact",
- "cli_command::tests::selected_observation_child",
- "--nocapture",
- ])
- .env("OCX_CLI_OBSERVATION_TEST", case)
- .env("HOME", &t.0)
- .env("ZDOTDIR", &t.0)
- .env("XDG_CONFIG_HOME", t.0.join(".config"))
- .output()
- .unwrap();
+ let result = crate::process_spawn::with_spawn(|| {
+ std::process::Command::new(std::env::current_exe().unwrap())
+ .args([
+ "--exact",
+ "cli_command::tests::selected_observation_child",
+ "--nocapture",
+ ])
+ .env("OCX_CLI_OBSERVATION_TEST", case)
+ .env("HOME", &t.0)
+ .env("ZDOTDIR", &t.0)
+ .env("XDG_CONFIG_HOME", t.0.join(".config"))
+ .output()
+ })
+ .unwrap();
assert!(result.status.success(), "selected-observation-failed");
assert!(
String::from_utf8_lossy(&result.stdout).contains("1 passed"),
@@ -578,16 +580,18 @@ mod tests {
}
}
fn disabled_selector_case(action: &str) {
- let result = std::process::Command::new(std::env::current_exe().unwrap())
- .args([
- "--exact",
- "cli_command::tests::disabled_selector_child",
- "--nocapture",
- ])
- .env("OCX_CLI_SELECTOR_TEST", action)
- .env("ZDOTDIR", "/invalid:zdotdir")
- .output()
- .unwrap();
+ let result = crate::process_spawn::with_spawn(|| {
+ std::process::Command::new(std::env::current_exe().unwrap())
+ .args([
+ "--exact",
+ "cli_command::tests::disabled_selector_child",
+ "--nocapture",
+ ])
+ .env("OCX_CLI_SELECTOR_TEST", action)
+ .env("ZDOTDIR", "/invalid:zdotdir")
+ .output()
+ })
+ .unwrap();
assert!(result.status.success(), "disabled-selector-failed");
assert!(
String::from_utf8_lossy(&result.stdout).contains("1 passed"),
diff --git a/desktop/src-tauri/src/cli_command_posix.rs b/desktop/src-tauri/src/cli_command_posix.rs
index 8ab0fcf8295..b3016bd9ff4 100644
--- a/desktop/src-tauri/src/cli_command_posix.rs
+++ b/desktop/src-tauri/src/cli_command_posix.rs
@@ -673,6 +673,7 @@ pub fn remove_plan(store: &Store, current: &Record) -> Result<(Record, Vec Temp {
@@ -701,13 +702,13 @@ mod tests {
command.arg("-N");
}
assert!(
- command
+ with_spawn(|| command
.arg(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
- .status()
- .unwrap()
- .success(),
+ .status())
+ .unwrap()
+ .success(),
"acl-command-failed"
);
}
@@ -969,7 +970,7 @@ mod tests {
if let Some(path) = path {
command.env("PATH", path);
}
- let output = command.output().unwrap();
+ let output = with_spawn(|| command.output()).unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}
@@ -988,14 +989,16 @@ mod tests {
0o700,
)
.unwrap();
- let out = std::process::Command::new(&shim)
- .env_clear()
- .env("ANTHROPIC_AUTH_TOKEN", "dummy")
- .env("ANTHROPIC_API_KEY", "")
- .env("OCX_NODE_LAUNCH_CONTEXT", "untrusted")
- .arg("status")
- .output()
- .unwrap();
+ let out = with_spawn(|| {
+ std::process::Command::new(&shim)
+ .env_clear()
+ .env("ANTHROPIC_AUTH_TOKEN", "dummy")
+ .env("ANTHROPIC_API_KEY", "")
+ .env("OCX_NODE_LAUNCH_CONTEXT", "untrusted")
+ .arg("status")
+ .output()
+ })
+ .unwrap();
assert!(out.status.success());
let text = String::from_utf8(out.stdout).unwrap();
let lines: Vec<_> = text.lines().collect();
@@ -1011,9 +1014,7 @@ mod tests {
assert!(!text.contains("dummy"));
fs::remove_file(cli).unwrap();
assert_eq!(
- std::process::Command::new(&shim)
- .env_clear()
- .output()
+ with_spawn(|| std::process::Command::new(&shim).env_clear().output())
.unwrap()
.status
.code(),
@@ -1035,13 +1036,18 @@ mod tests {
.replace("-N32", "-N1"),
] {
record::atomic(&t.0.join("cli"), None, inspect, 0o700).unwrap();
- fs::write(&shim, rendered).unwrap();
- let out = std::process::Command::new(&shim)
- .env_clear()
- .env("OCX_NODE_LAUNCH_CONTEXT", "inherited")
- .arg("status")
- .output()
- .unwrap();
+ {
+ let _writer = crate::process_spawn::executable_write();
+ fs::write(&shim, rendered).unwrap();
+ }
+ let out = with_spawn(|| {
+ std::process::Command::new(&shim)
+ .env_clear()
+ .env("OCX_NODE_LAUNCH_CONTEXT", "inherited")
+ .arg("status")
+ .output()
+ })
+ .unwrap();
assert!(out.status.success());
assert_eq!(out.stdout, b"\nstatus\n");
fs::remove_file(t.0.join("cli")).unwrap();
@@ -1197,15 +1203,17 @@ mod tests {
// System rc files may print to stdout (Ubuntu's /etc/bash.bashrc sudo hint), so only
// the marked result lines count; a failed lookup or CLI run still fails the shell, and
// the end marker makes any extra CLI output line fail the comparison.
- let output = command
- .args([
- "-i",
- "-c",
- "p=$(command -v ocx) || exit 11; o=$(ocx hello) || exit 12; \
+ let output = with_spawn(|| {
+ command
+ .args([
+ "-i",
+ "-c",
+ "p=$(command -v ocx) || exit 11; o=$(ocx hello) || exit 12; \
printf 'OCX-PATH:%s\\nOCX-OUT:%s:OCX-END\\n' \"$p\" \"$o\"",
- ])
- .output()
- .expect("cannot start real shell");
+ ])
+ .output()
+ })
+ .expect("cannot start real shell");
assert!(output.status.success(), "{shell}: command failed");
let stdout = String::from_utf8(output.stdout).expect("shell stdout is not UTF-8");
let marked = |tag: &str| {
diff --git a/desktop/src-tauri/src/cli_command_record.rs b/desktop/src-tauri/src/cli_command_record.rs
index 294c0d86346..950993a3b4f 100644
--- a/desktop/src-tauri/src/cli_command_record.rs
+++ b/desktop/src-tauri/src/cli_command_record.rs
@@ -548,6 +548,8 @@ fn replace(from: &Path, to: &Path) -> Result<()> {
io(fs::rename(from, to))
}
pub fn atomic(path: &Path, before: Option<&[u8]>, after: &[u8], mode: u32) -> Result<()> {
+ #[cfg(unix)]
+ let _executable_write = (mode & 0o111 != 0).then(crate::process_spawn::executable_write);
let parent = path.parent().ok_or("unsafe-file")?;
check(parent, true)?;
rc_acl_check(path)?;
@@ -593,6 +595,8 @@ pub fn atomic(path: &Path, before: Option<&[u8]>, after: &[u8], mode: u32) -> Re
if read_bytes(path)?.as_deref() != before {
return Err("concurrent-edit".into());
}
+ // Publish only after releasing our writer; a terminal may execute the new shim immediately.
+ drop(f);
replace(&tmp, path)?;
#[cfg(unix)]
io(fs::File::open(parent).and_then(|f| f.sync_all()))?;
@@ -1145,6 +1149,40 @@ impl Store {
#[cfg(test)]
pub(crate) mod tests {
use super::*;
+ #[cfg(unix)]
+ use crate::process_spawn::with_spawn;
+ #[cfg(unix)]
+ #[test]
+ fn executable_writes_wait_for_process_spawns_but_record_writes_do_not() {
+ use std::{sync::mpsc, thread, time::Duration};
+ let t = Temp::new();
+ let path = t.0.join("shim");
+ let (started_tx, started_rx) = mpsc::channel();
+ let (done_tx, done_rx) = mpsc::channel();
+ let (writer, early_completion) = with_spawn(|| {
+ // Non-executable record writes must still overlap the process-spawn stress test.
+ atomic(&t.0.join("record"), None, b"record", 0o600).unwrap();
+ let path = path.clone();
+ let writer = thread::spawn(move || {
+ started_tx.send(()).unwrap();
+ let result = atomic(&path, None, b"#!/bin/sh\nexit 0\n", 0o700);
+ done_tx.send(()).unwrap();
+ result
+ });
+ started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
+ let early_completion = done_rx.recv_timeout(Duration::from_millis(100));
+ (writer, early_completion)
+ });
+ if early_completion.is_err() {
+ done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
+ }
+ writer.join().unwrap().unwrap();
+ assert!(matches!(
+ early_completion,
+ Err(mpsc::RecvTimeoutError::Timeout)
+ ));
+ assert_eq!(fs::read(path).unwrap(), b"#!/bin/sh\nexit 0\n");
+ }
pub(crate) struct Temp(pub PathBuf);
impl Temp {
pub(crate) fn new() -> Self {
@@ -1554,13 +1592,13 @@ pub(crate) mod tests {
command.arg("-N");
}
assert!(
- command
+ with_spawn(|| command
.arg(path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
- .status()
- .unwrap()
- .success(),
+ .status())
+ .unwrap()
+ .success(),
"acl-command-failed"
);
}
@@ -1631,12 +1669,14 @@ pub(crate) mod tests {
fn linux_acl_inheritance_clear_and_existing_child_refusal() {
let t = Temp::new();
let set = |path: &Path, spec: &str| {
- std::process::Command::new("setfacl")
- .args(["-m", spec])
- .arg(path)
- .stdout(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
- .status()
+ with_spawn(|| {
+ std::process::Command::new("setfacl")
+ .args(["-m", spec])
+ .arg(path)
+ .stdout(std::process::Stdio::null())
+ .stderr(std::process::Stdio::null())
+ .status()
+ })
};
let result = set(&t.0, "d:u:65534:rwx");
if result
@@ -1906,7 +1946,9 @@ pub(crate) mod tests {
ready.wait();
let mut spawns = 0;
while !stop.load(Ordering::Relaxed) {
- assert!(Command::new(TRUE).status().unwrap().success());
+ assert!(with_spawn(|| Command::new(TRUE).status())
+ .unwrap()
+ .success());
spawns += 1;
}
spawns
diff --git a/desktop/src-tauri/src/exit.rs b/desktop/src-tauri/src/exit.rs
index 8950c338638..9c3d0040881 100644
--- a/desktop/src-tauri/src/exit.rs
+++ b/desktop/src-tauri/src/exit.rs
@@ -535,7 +535,7 @@ async fn finish_and_exit_after(app: &AppHandle, reason: ExitReason) {
match (reason, verdict) {
(ExitReason::UserQuit, _) => app.exit(0),
(ExitReason::CoordinatedRestart, DrainVerdict::Drained) => {
- app.restart();
+ crate::process_spawn::with_spawn(|| app.restart());
}
(ExitReason::CoordinatedRestart, _) => {
crate::logging::log_once("the update restart was refused", verdict.describe());
@@ -574,7 +574,7 @@ pub async fn prepare_restart(app: &AppHandle) -> RestartReadiness {
/// Come back, once the installer has finished and returned.
pub fn complete_restart(app: &AppHandle) -> ! {
- app.restart()
+ crate::process_spawn::with_spawn(|| app.restart())
}
/// Stop the runtime this app owns and confirm it is gone.
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index d4e3dec7e32..255954ed767 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -46,6 +46,7 @@ mod tray_login;
#[allow(dead_code)]
#[path = "popup.rs"]
mod popup_portable_test;
+mod process_spawn;
mod proxy;
mod resolve;
mod runtime_stop;
@@ -373,6 +374,9 @@ pub fn run() {
.inner_size(1100.0, 720.0)
.visible(false)
.user_agent(&window::webview_user_agent())
+ // Keep dashboard HTML drag/drop (including chat panes) in the webview;
+ // Tauri's native drop handler intercepts it on Windows.
+ .disable_drag_drop_handler()
// Cmd on macOS, Ctrl elsewhere, with + / - / 0. WebView2 zooms natively; on
// macOS and Linux Tauri injects a keydown polyfill whose one IPC call is granted
// to the loopback dashboard by `capabilities/dashboard-zoom.json`.
diff --git a/desktop/src-tauri/src/login_registration.rs b/desktop/src-tauri/src/login_registration.rs
index 43ea2ae71e2..4adf3f78550 100644
--- a/desktop/src-tauri/src/login_registration.rs
+++ b/desktop/src-tauri/src/login_registration.rs
@@ -527,13 +527,16 @@ mod command {
pub(super) fn run(program: &str, args: &[&str], timeout: Duration) -> Result {
let slot = Slot::take().ok_or(RunError::Failed)?;
let deadline = Instant::now() + timeout;
- let mut child = Command::new(program)
- .args(args)
- .stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .spawn()
- .map_err(|_| RunError::Failed)?;
+ let spawn = || {
+ Command::new(program)
+ .args(args)
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ };
+ let child = crate::process_spawn::with_spawn(spawn);
+ let mut child = child.map_err(|_| RunError::Failed)?;
let readers = [drain(child.stdout.take()), drain(child.stderr.take())];
let status = loop {
match child.try_wait() {
@@ -579,6 +582,29 @@ mod command {
#[test]
fn a_command_past_its_deadline_holds_the_only_slot_until_it_ends() {
+ // Measure command deadlines in a fresh process: the parent's executable-fixture
+ // writers may hold the test spawn lock longer than this test's timing budget.
+ const CHILD: &str = "OCX_LOGIN_COMMAND_DEADLINE_CHILD";
+ if std::env::var_os(CHILD).is_none() {
+ let output = crate::process_spawn::with_spawn(|| {
+ Command::new(std::env::current_exe().unwrap())
+ .args([
+ "--exact",
+ "login_registration::command::tests::a_command_past_its_deadline_holds_the_only_slot_until_it_ends",
+ "--nocapture",
+ ])
+ .env(CHILD, "1")
+ .output()
+ })
+ .unwrap();
+ assert!(
+ output.status.success(),
+ "{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
+ return;
+ }
let started = Instant::now();
assert_eq!(
run("/bin/sleep", &["1"], Duration::from_millis(50)),
diff --git a/desktop/src-tauri/src/process_spawn.rs b/desktop/src-tauri/src/process_spawn.rs
new file mode 100644
index 00000000000..51cdcd9936e
--- /dev/null
+++ b/desktop/src-tauri/src/process_spawn.rs
@@ -0,0 +1,203 @@
+//! Keep Unix forks from inheriting a writable descriptor for a soon-to-be-published executable.
+//! CLOEXEC only closes it at exec; closing the parent's copy does not close a child's copy.
+
+// A queued writer must not block another spawner: a plugin launcher can wait for the main
+// thread, where a second launcher may run. std::RwLock may prefer the queued writer and deadlock
+// those two readers. Use reader preference; only an active writer excludes process creation.
+#[cfg(unix)]
+static EXECUTABLE_IO: Gate = Gate::new();
+
+#[cfg(unix)]
+struct Gate {
+ access: std::sync::Mutex,
+ changed: std::sync::Condvar,
+}
+
+#[cfg(unix)]
+struct Access {
+ spawners: usize,
+ writing: bool,
+}
+
+#[cfg(unix)]
+struct Guard<'a> {
+ gate: &'a Gate,
+ writing: bool,
+}
+
+#[cfg(unix)]
+impl Gate {
+ const fn new() -> Self {
+ Self {
+ access: std::sync::Mutex::new(Access {
+ spawners: 0,
+ writing: false,
+ }),
+ changed: std::sync::Condvar::new(),
+ }
+ }
+
+ fn access(&self) -> std::sync::MutexGuard<'_, Access> {
+ self.access
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ }
+
+ fn acquire(&self, writing: bool) -> Guard<'_> {
+ let mut access = self
+ .changed
+ .wait_while(self.access(), |s| s.writing || (writing && s.spawners != 0))
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ if writing {
+ access.writing = true;
+ } else {
+ access.spawners += 1;
+ }
+ Guard {
+ gate: self,
+ writing,
+ }
+ }
+}
+
+#[cfg(unix)]
+impl Drop for Guard<'_> {
+ fn drop(&mut self) {
+ let mut access = self.gate.access();
+ if self.writing {
+ access.writing = false;
+ } else {
+ access.spawners -= 1;
+ }
+ self.gate.changed.notify_all();
+ }
+}
+
+#[cfg(unix)]
+pub(crate) fn executable_write() -> impl Drop {
+ EXECUTABLE_IO.acquire(true)
+}
+
+/// Cover process creation through exec, not the subsequent wait for the child's exit.
+/// Never request an executable writer from a spawner, or call this from a writer/pre_exec hook.
+pub(crate) fn with_spawn(spawn: impl FnOnce() -> T) -> T {
+ #[cfg(unix)]
+ let _guard = EXECUTABLE_IO.acquire(false);
+ spawn()
+}
+
+/// The pinned shell plugin's output future spawns synchronously when polled. Guard each poll
+/// so creation is covered even for a lazy future; release before Pending, never across await.
+pub(crate) async fn with_spawn_async(future: F) -> F::Output {
+ let mut future = std::pin::pin!(future);
+ std::future::poll_fn(|cx| with_spawn(|| future.as_mut().poll(cx))).await
+}
+
+#[cfg(all(test, unix))]
+mod tests {
+ use super::*;
+ use std::{
+ future::{poll_fn, Future},
+ sync::{mpsc, Arc},
+ task::{Context, Poll, Waker},
+ time::Duration,
+ };
+
+ #[test]
+ fn a_spawn_waits_for_the_writer_and_releases_before_child_exit() {
+ let writer = executable_write();
+ let (started_tx, started_rx) = mpsc::channel();
+ let (spawned_tx, spawned_rx) = mpsc::channel();
+ let spawner = std::thread::spawn(move || {
+ started_tx.send(()).unwrap();
+ let child = with_spawn(|| {
+ std::process::Command::new("/bin/cat")
+ .stdin(std::process::Stdio::piped())
+ .spawn()
+ });
+ spawned_tx.send(child).unwrap();
+ });
+ started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
+ let early = spawned_rx.recv_timeout(Duration::from_millis(100));
+ drop(writer);
+ let mut child = match early {
+ Err(mpsc::RecvTimeoutError::Timeout) => spawned_rx
+ .recv_timeout(Duration::from_secs(5))
+ .unwrap()
+ .unwrap(),
+ Ok(Ok(mut child)) => {
+ drop(child.stdin.take());
+ let _ = child.wait();
+ panic!("spawn overlapped an executable writer");
+ }
+ _ => panic!("spawn failed before the writer was released"),
+ };
+ spawner.join().unwrap();
+ // cat is still waiting for EOF, but executable publication must already be unblocked.
+ let writer = executable_write();
+ assert!(child.try_wait().unwrap().is_none());
+ drop(writer);
+ drop(child.stdin.take());
+ assert!(child.wait().unwrap().success());
+ }
+
+ #[test]
+ fn queued_writer_does_not_block_a_launchers_main_thread_callback() {
+ // An independent instance of the production gate keeps unrelated fixture/spawn stress
+ // from consuming this scheduling test's deadline.
+ let gate = Arc::new(Gate::new());
+ let launcher = gate.acquire(false);
+ let writer_gate = gate.clone();
+ let (started_tx, started_rx) = mpsc::channel();
+ let (done_tx, done_rx) = mpsc::channel();
+ let writer = std::thread::spawn(move || {
+ started_tx.send(()).unwrap();
+ let _writer = writer_gate.acquire(true);
+ done_tx.send(()).unwrap();
+ });
+ started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
+ assert!(matches!(
+ done_rx.recv_timeout(Duration::from_millis(100)),
+ Err(mpsc::RecvTimeoutError::Timeout)
+ ));
+ // Model an install helper waiting for a main-thread callback that launches a URL.
+ let callback = gate.acquire(false);
+ assert_eq!(gate.access().spawners, 2);
+ drop(callback);
+ drop(launcher);
+ done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
+ writer.join().unwrap();
+ }
+
+ #[test]
+ fn async_spawn_is_guarded_on_poll_without_holding_a_lock_across_pending() {
+ let mut polled = false;
+ let future = with_spawn_async(poll_fn(|_| {
+ assert!(
+ EXECUTABLE_IO.access().spawners > 0,
+ "async spawn poll must hold shared access"
+ );
+ if polled {
+ Poll::Ready(42)
+ } else {
+ polled = true;
+ Poll::Pending
+ }
+ }));
+ fn assert_send(_: &impl Send) {}
+ assert_send(&future);
+ let mut future = std::pin::pin!(future);
+ let mut cx = Context::from_waker(Waker::noop());
+ assert_eq!(future.as_mut().poll(&mut cx), Poll::Pending);
+ // A paused output reader cannot stop a reconcile from writing its shim.
+ drop(executable_write());
+ assert_eq!(future.as_mut().poll(&mut cx), Poll::Ready(42));
+ }
+
+ #[test]
+ fn failed_spawns_release_the_guard() {
+ let result = with_spawn(|| std::process::Command::new("/ocx-missing-executable").spawn());
+ assert!(result.is_err());
+ drop(executable_write());
+ }
+}
diff --git a/desktop/src-tauri/src/provider_icons.rs b/desktop/src-tauri/src/provider_icons.rs
index 2799453c795..b3e971f890a 100644
--- a/desktop/src-tauri/src/provider_icons.rs
+++ b/desktop/src-tauri/src/provider_icons.rs
@@ -37,6 +37,7 @@ const ALIASES: &[(&str, &str)] = &[
("commandcode", "commandcode-color.svg"),
("cursor", "cursor-color.svg"),
("deepseek", "deepseek-color.svg"),
+ ("dsh-account", "deepseek-color.svg"),
("devin", "devin.svg"),
("firepass", "firepass-color.svg"),
("fireworks", "fireworks-color.svg"),
diff --git a/desktop/src-tauri/src/resolve.rs b/desktop/src-tauri/src/resolve.rs
index 1f9ddcfdda0..7e590e53189 100644
--- a/desktop/src-tauri/src/resolve.rs
+++ b/desktop/src-tauri/src/resolve.rs
@@ -304,7 +304,12 @@ pub async fn run(app: &AppHandle, deadline: Instant) -> Resolution {
return Resolution::Unknown(format!("the bundled CLI could not be started ({error})"))
}
};
- match timeout_at(deadline, command.output()).await {
+ match timeout_at(
+ deadline,
+ crate::process_spawn::with_spawn_async(command.output()),
+ )
+ .await
+ {
Ok(Ok(output)) => read(output.status.code(), &output.stdout, &output.stderr),
Ok(Err(error)) => {
Resolution::Unknown(format!("the bundled CLI could not be run ({error})"))
diff --git a/desktop/src-tauri/src/runtime_stop.rs b/desktop/src-tauri/src/runtime_stop.rs
index c92528a5f4b..664da927067 100644
--- a/desktop/src-tauri/src/runtime_stop.rs
+++ b/desktop/src-tauri/src/runtime_stop.rs
@@ -270,7 +270,12 @@ async fn run_with_args(app: &AppHandle, deadline: Instant, argv: Vec) ->
return StopResult::Failed(format!("the bundled CLI could not be started ({error})"))
}
};
- match timeout_at(deadline, command.output()).await {
+ match timeout_at(
+ deadline,
+ crate::process_spawn::with_spawn_async(command.output()),
+ )
+ .await
+ {
Ok(Ok(output)) => read(output.status.code(), &output.stdout, &output.stderr),
Ok(Err(error)) => StopResult::Failed(format!("the bundled CLI could not be run ({error})")),
Err(_) => {
diff --git a/desktop/src-tauri/src/sidecar.rs b/desktop/src-tauri/src/sidecar.rs
index 71f16db8f0a..e601498fe0f 100644
--- a/desktop/src-tauri/src/sidecar.rs
+++ b/desktop/src-tauri/src/sidecar.rs
@@ -205,7 +205,8 @@ pub fn start(
.args(["start", "--port", &endpoint.port.to_string()])
.env("OPENCODEX_GUI_DIST", gui_dist)
.env(SUPERVISED_ENV, "1");
- let (events, child) = command.spawn().map_err(|error| error.to_string())?;
+ let (events, child) =
+ crate::process_spawn::with_spawn(|| command.spawn()).map_err(|error| error.to_string())?;
let watch = watch.for_child(child.pid());
watch.follow(events);
Ok(child)
diff --git a/desktop/src-tauri/src/tray.rs b/desktop/src-tauri/src/tray.rs
index c702bc7c240..ec76efdf4a7 100644
--- a/desktop/src-tauri/src/tray.rs
+++ b/desktop/src-tauri/src/tray.rs
@@ -224,9 +224,10 @@ pub fn install(app: &AppHandle) -> tauri::Result<()> {
else {
return;
};
- let _ = app
- .opener()
- .open_url(format!("{}#/usage", endpoint.url("/")), None::);
+ let _ = crate::process_spawn::with_spawn(|| {
+ app.opener()
+ .open_url(format!("{}#/usage", endpoint.url("/")), None::)
+ });
}
"start-at-login" => {
#[cfg(target_os = "macos")]
diff --git a/desktop/src-tauri/src/tray_login.rs b/desktop/src-tauri/src/tray_login.rs
index 5126c1f44e7..3a1948ed855 100644
--- a/desktop/src-tauri/src/tray_login.rs
+++ b/desktop/src-tauri/src/tray_login.rs
@@ -151,10 +151,11 @@ fn perform(app: &AppHandle, action: ClickAction) {
.map_err(|error| error.to_string()),
ClickAction::Enable => crate::login_autostart::enable(app),
// launchd keeps its override whatever the plist says; the person turns it back on.
- ClickAction::OpenLoginItems => app
- .opener()
- .open_url(login_registration::LOGIN_ITEMS_SETTINGS, None::)
- .map_err(|error| error.to_string()),
+ ClickAction::OpenLoginItems => crate::process_spawn::with_spawn(|| {
+ app.opener()
+ .open_url(login_registration::LOGIN_ITEMS_SETTINGS, None::)
+ })
+ .map_err(|error| error.to_string()),
ClickAction::Nothing => Ok(()),
};
if let Err(error) = result {
diff --git a/desktop/src-tauri/src/updater.rs b/desktop/src-tauri/src/updater.rs
index bcaf3349533..cc02ae9e9fc 100644
--- a/desktop/src-tauri/src/updater.rs
+++ b/desktop/src-tauri/src/updater.rs
@@ -410,7 +410,7 @@ pub async fn install(app: &AppHandle, update: Update) -> Result<(), String> {
));
}
- if let Err(error) = update.install(package) {
+ if let Err(error) = crate::process_spawn::with_spawn(|| update.install(package)) {
// The installer returned a failure. The runtime was stopped for an install that did not
// happen, so the app brings one back instead of sitting drained.
recover_after_failed_install(app);
diff --git a/desktop/src-tauri/src/window.rs b/desktop/src-tauri/src/window.rs
index 5c69ae17fe7..7b71614c64a 100644
--- a/desktop/src-tauri/src/window.rs
+++ b/desktop/src-tauri/src/window.rs
@@ -67,7 +67,9 @@ fn opens_in_default_browser(url: &Url) -> bool {
pub fn open_in_default_browser(url: &Url) {
if opens_in_default_browser(url) {
- let _ = tauri_plugin_opener::open_url(url.as_str(), None::<&str>);
+ let _ = crate::process_spawn::with_spawn(|| {
+ tauri_plugin_opener::open_url(url.as_str(), None::<&str>)
+ });
}
}
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index c1bb2b933d3..3f2e7ce5a4f 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCodex",
- "version": "2.86.0",
+ "version": "2.87.0",
"identifier": "com.opencodex.desktop",
"build": {
"frontendDist": "../ui",
diff --git a/devlog/_fin/261015_argon_thought_boundary/000_plan.md b/devlog/_fin/261015_argon_thought_boundary/000_plan.md
new file mode 100644
index 00000000000..7497e8bb94d
--- /dev/null
+++ b/devlog/_fin/261015_argon_thought_boundary/000_plan.md
@@ -0,0 +1,97 @@
+# Gemini Argon output separation
+
+A report of reasoning-looking assistant output prompted comparison of the Google response
+classifier with CLIProxyAPI and cc-switch. The inspected classifier already separates marked
+thoughts. This unit verifies that separation through every client protocol using synthetic
+markers, and documents the boundary without claiming the original observation is reproduced.
+
+## Loop specification
+
+- Archetype: satisfy-spec; one C3 verification/documentation work phase.
+- Trigger/goal: investigate Argon output separation and deliver an evidence-backed PR to dev.
+- In scope: synthetic Argon integration tests, their layout registration, and current Google docs.
+- Non-goals: heuristic text censorship, runtime changes without a demonstrated defect, private
+ session transcripts, user configuration changes, deployment, other worktrees, native PR stacks.
+- Verifier: new protocol test plus adjacent Google/direct-encoder tests; typecheck, test layout,
+ file-size, privacy and structure gates; docs build; required CI at the PR head.
+- Stop: coverage/docs merged with passing relevant checks, or a concrete blocker recorded.
+- Memory artifact: this unit and local `.tmp/argon-protocol/` evidence; no memory-store writes.
+- Outcomes: DONE for the verification PR; runtime NOOP if marked thoughts remain separated.
+ Do not claim the reported live symptom fixed merely because synthetic tests pass.
+- Escalation: a demonstrated defect changes the plan before patching; unavailable live evidence
+ remains an explicit limitation. Existing user authority includes push/PR/merge, no release.
+- Bounds: none stated; host limits apply. Tools: local Bun, read-only public Aside/GitHub research,
+ repository-scoped GitHub publication. No production credentials are needed for tests.
+
+## Evidence and hypotheses
+
+- H1: the adapter converts marked thoughts to answer text. Falsifier: mixed marked/unmarked
+ synthetic parts preserve separate channels in streaming and buffered responses.
+- H2: a client converter collapses channels. Falsifier: actual Responses, Chat and Messages
+ handlers keep thought markers out of ordinary answer fields with direct routing on and off.
+- H3: upstream ordinary text or client rendering explains the observation. Static tests cannot
+ establish this; never reclassify prose by its wording or infer a root cause from test success.
+
+Source anchors: `src/adapters/google.ts:716`, `:989`, `:1378`, `:1654`;
+`src/protocols/encoders/chat.ts:133`; `src/protocols/encoders/messages.ts:135`.
+CLIProxyAPI `5fc580b1d72d87a7bcfe5cfb6f479c1aea5625b3` maps `thought` to
+`reasoning_content`; cc-switch `76f99d41a66e24a5ed2e8982bc494399f306bfe6` filters
+marked thoughts from visible text. Public Aside reads succeeded with complete=true,
+truncated=false, contentVerified=true for Google thinking docs and CLIProxyAPI #6607.
+Those docs distinguish thought signatures from ordinary text visibility; #6607 is a buffering
+decision, not evidence of text leakage. Argon upstream behavior is not independently certified.
+
+Baseline: `bun test tests/adapters/google/google-hardening.test.ts
+tests/adapters/google/google-adapter.test.ts tests/adapters/google/gemini-4-argon-antigravity.test.ts
+tests/responses/responses-show-thinking-summary.test.ts`: exit 0, 158 pass at 7640a942b3.
+Work then refreshed to dev 0b13396af4; adjacent checks will run on the final patch.
+Typecheck's `include: ["src"]` does not validate new tests; Bun execution does.
+
+## Consultation
+
+Architect proposal and reflection are retained in the local coordinator evidence.
+- ARGON-SEP-01/02 accepted: keep one classifier and existing output channels.
+- ARGON-SEP-03 accepted: provenance is not proof of observed upstream behavior.
+- ARGON-SEP-04 amended: add handler-level coverage and current contract clarification.
+ Preserve historical ADR-0055 per `structure/AGENTS.md`; do not rewrite the old decision.
+- Same-architect reflection: ALIGNED against `010_verification.md`; require assertions on both
+ deltas and final accumulated payloads, and correct tool signature attachment where exposed.
+ Independent plan audit: PASS. Refreshed-base adjacent direct/encoder baseline: 131 pass, 0 fail.
+
+No new runtime fields or enums. No new runtime enforcement: tests are regression checks only;
+they can be bypassed by skipping tests, and cannot certify external client rendering.
+
+## Verification result
+
+The investigation's runtime outcome is NOOP: the tested marked thoughts stay out of ordinary
+answer fields. New coverage adds 75 cases. Deliberately changing the classifier to emit marked
+thoughts as text fails all 75; byte-for-byte restoration returns all 75 to green. No classifier
+mutation is part of the patch. The live observation's cause remains unresolved.
+
+Final focused validation on the repository-pinned Bun 1.4.3: 364 pass / 0 fail across the new
+matrix and eight adjacent Google/direct-protocol files; layout/tooling/size: 36 pass / 0 fail.
+`bun run test:changed` selects the new file only and passes all 75 cases. `bun run typecheck`,
+`bun run structure:check`, and `bun run privacy:scan` pass. `cd docs-site && bun run build`
+builds 577 pages and checks 81,202 links. Raw logs stay in local `.tmp/argon-protocol/`.
+An initial layout run under the shell's Bun 1.4.0 failed the pinned-runner check; using the
+already-installed Bun 1.4.3 resolved it without source changes.
+
+Initial fixture expectations incorrectly treated a disabled provider display default as a
+client opt-out and omitted the Messages thinking opt-in. The current tests explicitly request
+the protocol's display state, matching `core-normalize.ts` and `claude/inbound.ts`; they still
+force upstream marked thoughts when hidden to test output suppression, not just request flags.
+
+The full 2,330-file suite is not run locally: this tests/docs-only change has bounded integration
+coverage and shares the host with concurrent coding workloads. Broader coverage remains with
+the PR's required CI. Neither this exception nor synthetic tests certify external harness UI.
+
+Fresh independent final review: PASS, no actionable defects. The fresh reader identified the
+result as synthetic regression coverage, with the live symptom explicitly unverified. No prose
+change was requested. The investigation is closed with that limitation; PR integration is
+tracked by the coordinator. Runtime source is unchanged.
+
+Hosted review follow-up: restore all fixture globals in finally if spend-lease release throws;
+add independent client-only opt-outs with provider opt-in still true for all three protocols.
+A Chat opt-out mutation fails 18 cases; a Messages display-omitted mutation fails 6. All
+production files were restored byte-for-byte. These probes confirm the added cases detect
+the independent client preference, rather than passing only because the provider default is off.
diff --git a/devlog/_fin/261015_argon_thought_boundary/010_verification.md b/devlog/_fin/261015_argon_thought_boundary/010_verification.md
new file mode 100644
index 00000000000..8f328c5c6e7
--- /dev/null
+++ b/devlog/_fin/261015_argon_thought_boundary/010_verification.md
@@ -0,0 +1,33 @@
+# One phase: verify the Argon output boundary
+
+Depends on current Google adapter and existing client handlers; no runtime design changes.
+
+1. NEW `tests/responses/protocol-argon-thought-boundary.test.ts`: isolated homes and synthetic
+ credentials, mock Google transport only, invoke real Responses/Chat/Messages handlers.
+ Generate `thought: true` marker parts, ordinary text (absent and false markers), signature-only
+ parts, an unmarked signed text part, and an optional tool call with synthetic signature.
+ Exercise buffered and SSE, coalesced and split SSE frames, and direct/bridge Chat/Messages.
+ Assert exact ordinary answer text in both deltas and accumulated final payloads,
+ thought marker only in reasoning/thinking fields, no
+ signature-only text, preserved tool name/args and call-bound signature where the client wire
+ exposes it, successful terminal. For independent client-only opt-outs (Responses/Chat summary none and Messages display omitted),
+ force upstream thought parts anyway and require suppression while answer and tool survive.
+ Inspect protocol trace so the direct/bridge cases prove the intended lane executed.
+2. MODIFY `scripts/test-layout/layout.json` and `tests/fixtures/test-layout-expected.json`:
+ register the new test in responses, using existing shard conventions.
+3. MODIFY `structure/providers/google.md`: describe signature-independent text classification
+ and reference the new test; preserve the historical decision record.
+4. MODIFY `docs-site/src/content/docs/guides/providers.md`: clarify Gemini thought summaries,
+ client field separation, and that unmarked ordinary text is not censored by wording.
+5. Run new tests and adjacent Google/direct-lane/encoder tests. Validate the new oracle by a
+ temporary local classifier mutation (marked thoughts become text), require failure, restore
+ bytes, then require green. No mutation may be committed.
+6. Run typecheck, test-layout/tooling, file-size, privacy, structure and docs build. Full suite
+ default applies; if concurrent workload makes it disproportionate for tests/docs only, record
+ exact focused scope and leave remaining platform/integration coverage to required CI.
+7. Fresh independent review, fix valid findings, create ordinary PR to dev using template.
+ Inspect exact-head required CI and maintainer-integration policy before the authorized merge.
+
+Acceptance is protocol correctness for the synthetic fixtures and prevention of regression.
+The original live symptom and other harnesses' rendering remain unverified; the PR and final
+report must say so. A newly demonstrated runtime defect requires a plan amendment and audit.
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md b/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md
new file mode 100644
index 00000000000..530fa2bd582
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/000_plan.md
@@ -0,0 +1,32 @@
+# 5848: native remote-list provider policy before a backend relay
+
+Status: **proposal and executable specifications only; no production fix**.
+Date: 2026-09-28.
+
+Related: [#5848](https://github.com/lidge-jun/opencodex/issues/5848),
+[duplicate #5906](https://github.com/lidge-jun/opencodex/issues/5906),
+[upstream #48358](https://github.com/openai/codex/issues/48358).
+
+## Proposed decision
+
+Prefer a narrowly scoped, operator-opt-in policy in **native Codex app-server**
+for remote `thread/list` requests that do not supply a provider array. Keep the
+normal default-provider behavior, all existing authorization, and history intact.
+The mobile client's explicit all-provider request remains the simplest upstream
+client correction. Compare both with the previously tested local backend relay;
+do not turn that experimental relay into an installed feature in this PR.
+
+The current OpenCodex runtime and [ADR-5848](../../../structure/decisions/ADR-5848-provider-table-remote-history-visibility.md)
+remain unchanged. This proposal does **not** close #5848.
+
+## Work remaining
+
+- [x] Identify the native remote-connection boundary and existing provider predicate.
+- [x] Specify precedence and test the native-policy proposal offline.
+- [x] Retain and rerun the isolated loopback relay alternative.
+- [ ] Obtain upstream agreement on the native configuration/API contract.
+- [ ] Implement config/schema, trusted-origin plumbing, and Rust regressions upstream.
+- [ ] Verify native pagination, managed policy, account changes, and actual mobile resume.
+- [ ] Establish released-version/capability detection before adding any OpenCodex toggle.
+
+See [design](010_design.md) and [verification](020_verification.md).
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/010_design.md b/devlog/_plan/260928_remote_thread_provider_policy/010_design.md
new file mode 100644
index 00000000000..a0f8ca22474
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/010_design.md
@@ -0,0 +1,134 @@
+# Design comparison and proposed native contract
+
+This is an RFC, not an accepted architecture change or a released setting.
+`probes/native_policy.py` is an executable specification, **not native Rust code**.
+No probe is imported by OpenCodex or registered in its runtime/CLI/test suite.
+
+## Evidence and scope
+
+The source baseline is OpenAI Codex
+`1cc7e2361237ce7244430ee1d581c77f95c57ac8` and OpenCodex `dev`
+`eb7f0f0970c2298f8b2d66d170c4d4be869f301b`.
+
+- [Native list predicate](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server/src/request_processors/thread_processor.rs#L5456-L5530): explicit nonempty arrays filter to those ids; `[]` removes the provider predicate; omission defaults to the configured provider, except for related-thread queries.
+- [Remote connection origin](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/client_tracker.rs): native remote sessions open with `ConnectionOrigin::RemoteControl`; messages become native transport events.
+- [Original rationale, #5658](https://github.com/openai/codex/pull/5658): provider annotations/filtering address cross-provider resume/decryption failures. Visibility is not proof that a conversation is safe to resume under a different provider.
+- [OpenCodex mitigation, #6007](https://github.com/lidge-jun/opencodex/pull/6007): warnings and documentation without rewriting native history or intercepting native RPC.
+
+## Alternatives
+
+| Approach | Can leave official mobile app unchanged? | Additional ownership | Recommendation |
+| --- | --- | --- | --- |
+| Mobile explicitly sends `modelProviders: []` (or a chosen list) | No | Client's list behavior and resume UX | Simplest client correction; continue upstream tracking. |
+| Native, opt-in, remote-only default list policy | Yes, once a compatible native build ships | Native config and request policy, no extra service | **Preferred implementation proposal** when the host needs explicit control. |
+| Local relay selected through `chatgpt_base_url` | Potentially; live service not verified | Shared backend traffic, token forwarding, connection lifecycle | Research fallback, not a default or currently supported remedy. |
+| Globally change omitted filter to all providers | Yes | Changes behavior for every caller | Do not use: loses the existing default isolation behavior. |
+| Rewrite `openai` history tags to `opencodex` | Yes in some observations | Native SQLite/rollout state | Do not use: violates the retained history-writer boundary. |
+
+The native proposal minimizes new connection and credential handling; this is an
+engineering recommendation, not evidence of upstream acceptance or deployment.
+Neither upstream route can be delivered by modifying OpenCodex's `/v1` inference
+proxy alone. This PR publishes the comparison rather than disguising a probe as a fix.
+
+## Proposed native setting (name subject to upstream review)
+
+**Illustrative only; do not add this to current Codex/OpenCodex configuration.**
+
+```toml
+[remote_control]
+thread_list_model_providers = ["openai", "opencodex"]
+```
+
+Absent setting means opt-out. An explicitly empty array means all providers;
+a nonempty array means exactly those ids. Do not automatically infer equivalence
+from provider names, model names, or a shared URL. The setting changes listing,
+not authentication, permission checks, routing, compaction, or resume semantics.
+An operator who only needs two provider ids need not opt into every provider.
+
+### Precedence
+
+1. Existing authentication and managed remote-control policy still run first.
+2. A request's explicit provider **array**, including `[]`, always wins.
+3. With no array, parent/ancestor queries retain their current no-default-filter behavior.
+4. Only a server-identified remote connection may use the operator's configured policy.
+5. With no policy, and for every non-remote connection, preserve the existing default.
+
+Typed Rust `Option>` treats omission and JSON `null` alike. This
+proposal deliberately applies its default to both. The raw relay probe preserves
+all present JSON keys, including null; that difference is documented and must not
+be mistaken for identical behavior between the two alternatives.
+
+Use the native `ConnectionOrigin` attached to the connection. Never infer remote
+origin from clientInfo.name, a user-agent, JSON fields, or a caller-supplied header.
+The Python enum tests only model this trusted input; they do not prove authentication.
+
+### Native implementation boundary
+
+The upstream change must add and validate the config contract, snapshot it with
+the native app-server's effective configuration, carry the trusted connection
+origin to the `thread/list` handler, and resolve the provider predicate before
+calling the existing store pagination. A new RPC layer is unnecessary.
+
+Conceptual selection, **not a drop-in patch**:
+
+```text
+if request supplies provider array: use that array
+else if parent/ancestor query: use existing related-thread behavior
+else if trusted origin is RemoteControl and operator policy is configured:
+ use the configured array
+else: use existing configured-default behavior
+```
+
+Do not add a global exception to `list_threads_common` for all callers. Do not
+change `thread/start`, `thread/resume`, `turn/start`, stored provider metadata,
+or request error handling. Keep source/cwd/archive/project filters and ordering.
+The new configuration's trust/precedence should prevent project content from
+silently opting an operator into a broader default; upstream must choose and test
+the appropriate configuration layers and any managed restrictions.
+
+Hold one effective policy throughout a listing/pagination sequence. A changed
+policy requires a fresh listing cursor; do not combine pages obtained under
+incompatible filters. Native tests must pin behavior with actual cursor semantics.
+The fixture tests here use integer pagination, not native opaque cursors.
+
+## Local relay alternative: what was and was not shown
+
+[Native startup](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server/src/lib.rs)
+uses `config.chatgpt_base_url` for remote control. The
+[URL protocol](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs)
+accepts loopback URLs. A host-side relay is therefore a concrete design alternative:
+
+```text
+unchanged mobile <-> existing ChatGPT backend <-> local relay <-> native app-server
+```
+
+Connection establishment starts on the host. Enroll/pair/refresh can be forwarded
+rather than reimplemented. However, `chatgpt_base_url` is shared with
+[authentication configuration](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/core/src/config/auth_keyring.rs)
+and other backend consumers. A WebSocket-only implementation is insufficient.
+[Enrollment persistence](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs)
+also keys state by URL/account/client, so changing the URL can affect enrollment
+selection; whether live re-pairing is required remains unverified.
+
+The retained relay fixture accepts only literal `127.0.0.1` HTTP upstreams and
+synthetic credentials. It is not a production service and cannot be configured
+for ChatGPT. It handles regular and single-chunk frames; multi-chunk messages are
+passed unchanged. Actual protocol-v3 segmentation, native reconnect/ACK/cursors,
+account changes, managed network policy, and full backend compatibility remain
+release gates, not claims supported by these tests. See native
+[WebSocket handling](https://github.com/openai/codex/blob/1cc7e2361237ce7244430ee1d581c77f95c57ac8/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs).
+
+No global base-URL mutation, token-pool integration, production listener,
+configuration migration, history write, or authentication bypass is proposed here.
+
+## Upstream and downstream follow-through
+
+An upstream implementation needs Rust config/schema, origin-scoping, explicit/null/
+related-query, pagination, and managed-policy tests. Keep existing defaults intact.
+Document the omitted/default behavior and regenerate affected schema/TS fixtures.
+
+After an accepted implementation is released, OpenCodex may consider an opt-in
+integration with positive capability/version evidence and precise configuration
+ownership/restoration. Merely writing an unknown TOML key is not a feature test.
+Until then, retain #6007's warnings and keep #5848 open. Do not suppress the warning
+because an experimental setting was written or a fixture passed.
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md
new file mode 100644
index 00000000000..520f602b369
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/020_verification.md
@@ -0,0 +1,65 @@
+# Verification
+
+Status: local evidence for an RFC. The probes are executable specifications that
+run on demand; no CI workflow runs them, and nothing in the OpenCodex build, test,
+or runtime path imports them. All inputs, accounts, tokens, thread ids, and
+database rows are synthetic. No native Codex process, user home, installed
+config, or real service is accessed.
+
+## How to run
+
+From the repository root:
+
+```sh
+cd devlog/_plan/260928_remote_thread_provider_policy/probes
+python3 -m unittest -v test_native_policy test_probe
+```
+
+The socket suite needs aiohttp. Install it into a scratch directory, not the
+repository or a system site-packages:
+
+```sh
+python3 -m pip install --target "$SCRATCH_DIR" aiohttp
+PYTHONPATH="$SCRATCH_DIR" python3 -m unittest -v test_native_policy test_probe test_loopback_bridge
+```
+
+## Inventory
+
+63 tests across three suites:
+
+- 23 tests (`test_native_policy`) specify the proposed native policy: opt-out,
+ trusted-origin scoping, explicit arrays, null semantics, parent/ancestor
+ exceptions, immutable policy, exact ids, UUID thread-id validation, and
+ synthetic paginated fixture reads.
+- 29 raw-frame tests (`test_probe`) cover the relay alternative's
+ ordinary/single-chunk rewrites, byte-identical pass-through, limits, and
+ unsupported inputs.
+- 11 localhost HTTP/WebSocket tests (`test_loopback_bridge`) use a mock host and
+ mock backend, checking two-way traffic, fixture auth/headers, explicit filters,
+ enrollment, unrelated HTTP, fixture endpoint restrictions, and both directions
+ of close-code forwarding.
+
+The first 52 use the Python standard library only.
+
+The synthetic database has 5,200 `openai` rows, one `opencodex` row, and one
+`other` row. Default filtering yields one; the two-id opt-in yields 5,201; all
+providers yields 5,202. Page sizes 1, 37, and 1,000 are exercised. The fixture
+dump hash stays unchanged after reads. This demonstrates the specification, not
+native SQLite schema compatibility or actual mobile results.
+
+## Recorded runs
+
+| Date | Environment | Suites | Result |
+| --- | --- | --- | --- |
+| 2026-09-28 | author, local | native policy + raw frame | 52 passed |
+| 2026-10-13 | Linux, Python 3.12.14, aiohttp 3.14.4 | all three | 63 passed |
+| 2026-10-15 | macOS, Python 3.9.6, aiohttp 3.13.5 | all three | 63 passed |
+
+Each run reflects the probe source at that time; a later edit to a probe needs a
+fresh local run.
+
+## Not executed / not established
+
+- Native Rust implementation, compilation, config/schema generation, or native tests.
+- Actual ChatGPT mobile pairing, full pagination, resume, token renewal, or reconnect.
+- Production multi-segment relay support or management/security review of a relay.
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py
new file mode 100644
index 00000000000..80859ac8395
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/native_policy.py
@@ -0,0 +1,103 @@
+"""Executable specification for a PROPOSED native Codex policy, not an integration.
+
+No socket, authentication, config, history, or rollout access. The real Rust
+implementation must obtain origin from its authenticated server-side connection,
+not from JSON, clientInfo.name, or an HTTP header. This enum only models that input.
+None as the result means no provider predicate, not no results.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+from uuid import UUID
+import re
+
+# Rust's Uuid::parse_str accepts exactly four textual shapes - hyphenated, simple,
+# braced-hyphenated and the urn:uuid prefix. Python's UUID() is looser: it tolerates
+# arbitrary hyphen placement inside a 32-hex payload, so wrapper/hyphen variants
+# must be rejected by shape before parsing instead of leaning on the constructor.
+_UUID_ACCEPTED_SHAPES = re.compile(
+ r"(?:"
+ r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
+ r"|[0-9a-fA-F]{32}"
+ r"|\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}"
+ r"|urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
+ r")\Z"
+)
+
+
+class ConnectionOrigin(Enum):
+ STDIO = "stdio"
+ WEBSOCKET = "websocket"
+ IN_PROCESS = "in_process"
+ REMOTE_CONTROL = "remote_control"
+
+
+def _validate_ids(value: tuple[str, ...], label: str) -> None:
+ if not isinstance(value, tuple):
+ raise ValueError(f"{label} must be a tuple")
+ if any(not isinstance(item, str) or not item.strip() for item in value):
+ raise ValueError(f"{label} must contain non-empty strings")
+ if len(set(value)) != len(value):
+ raise ValueError(f"{label} must not contain duplicate identifiers")
+
+
+def _validate_thread_id(value: str, label: str) -> None:
+ """Upstream parses ThreadId as a UUID; a non-UUID string must not widen the list."""
+ if not isinstance(value, str) or not _UUID_ACCEPTED_SHAPES.match(value):
+ raise ValueError(f"{label} must be a UUID thread id")
+ try:
+ UUID(value)
+ except (ValueError, AttributeError, TypeError):
+ raise ValueError(f"{label} must be a UUID thread id")
+
+
+@dataclass(frozen=True)
+class RemoteListPolicy:
+ # None: opt-out; (): explicitly all; non-empty: exactly these provider ids.
+ providers: tuple[str, ...] | None = None
+
+ def __post_init__(self) -> None:
+ if self.providers is not None:
+ _validate_ids(self.providers, "providers")
+
+
+def resolve_provider_filter(
+ *,
+ origin: ConnectionOrigin,
+ default_provider: str,
+ requested: tuple[str, ...] | None = None,
+ parent_thread_id: str | None = None,
+ ancestor_thread_id: str | None = None,
+ policy: RemoteListPolicy = RemoteListPolicy(),
+) -> tuple[str, ...] | None:
+ """Model precedence after typed request decoding and existing authorization.
+
+ Rust Option> decodes both omission and JSON null as None. They
+ intentionally have the same semantics in this native-policy proposal. The
+ separate raw-frame relay probe instead preserves every present JSON key.
+ Empty requested tuple is an explicit client request for all providers.
+ The policy applies only to thread/list; callers must not reuse it for resume.
+ """
+ if not isinstance(origin, ConnectionOrigin):
+ raise ValueError("origin must be supplied by the trusted connection context")
+ _validate_ids((default_provider,), "default_provider")
+ for label, tid in (
+ ("parent_thread_id", parent_thread_id),
+ ("ancestor_thread_id", ancestor_thread_id),
+ ):
+ if tid is not None:
+ _validate_thread_id(tid, label)
+ if parent_thread_id is not None and ancestor_thread_id is not None:
+ raise ValueError("parent_thread_id and ancestor_thread_id are mutually exclusive")
+ if requested is not None:
+ # Preserve every typed client array, including duplicate/empty ids.
+ # Native Vec accepts them; this proposal must not add rejection.
+ if not isinstance(requested, tuple) or any(not isinstance(item, str) for item in requested):
+ raise ValueError("requested must model a typed string array")
+ return requested or None
+ if parent_thread_id is not None or ancestor_thread_id is not None:
+ return None
+ if origin is ConnectionOrigin.REMOTE_CONTROL and policy.providers is not None:
+ return policy.providers or None
+ return (default_provider,)
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py
new file mode 100644
index 00000000000..a2622cdf18e
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/remote_list_probe.py
@@ -0,0 +1,160 @@
+"""Offline research probe; NOT an installed OpenCodex feature or production proxy.
+
+Based on openai/codex commit 1cc7e2361237ce7244430ee1d581c77f95c57ac8.
+Only changes omitted thread/list.modelProviders in explicit opt-in mode.
+No network, authentication, filesystem, database, or rollout writes occur here.
+Complete multi-segment messages need a separate bounded streaming reassembler;
+this probe deliberately passes those messages through unchanged.
+"""
+from __future__ import annotations
+
+import base64
+import binascii
+import json
+from dataclasses import dataclass
+from typing import Any
+
+
+@dataclass(frozen=True)
+class Policy:
+ enabled: bool = False
+ # An empty tuple explicitly requests all providers, matching modelProviders: [].
+ providers: tuple[str, ...] = ("openai", "opencodex")
+ max_wire_bytes: int = 150 * 1024
+
+ def __post_init__(self) -> None:
+ if not isinstance(self.providers, tuple) or any(
+ not isinstance(p, str) or not p.strip() for p in self.providers
+ ):
+ raise ValueError("providers must be a tuple of non-empty strings")
+ if len(set(self.providers)) != len(self.providers):
+ raise ValueError("duplicate provider identifiers")
+ if self.max_wire_bytes < 64:
+ raise ValueError("max_wire_bytes must be at least 64")
+
+
+@dataclass(frozen=True)
+class Outcome:
+ text: str
+ changed: bool
+ reason: str
+
+
+def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
+ result: dict[str, Any] = {}
+ for key, value in pairs:
+ if key in result:
+ raise ValueError("duplicate JSON member")
+ result[key] = value
+ return result
+
+
+def _reject_constant(value: str) -> None:
+ raise ValueError("non-standard JSON constant")
+
+
+def decode(text: str | bytes) -> Any:
+ return json.loads(text, object_pairs_hook=_unique_object,
+ parse_constant=_reject_constant)
+
+
+def encode(value: Any) -> str:
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"),
+ allow_nan=False)
+
+
+def _patch_message(message: Any, policy: Policy) -> tuple[bool, str]:
+ if not isinstance(message, dict) or message.get("method") != "thread/list":
+ return False, "not_thread_list"
+ request_id = message.get("id")
+ if isinstance(request_id, bool) or not isinstance(request_id, (int, str)):
+ return False, "not_request"
+ if "result" in message or "error" in message:
+ return False, "ambiguous_request"
+ params = message.get("params", {})
+ if not isinstance(params, dict):
+ return False, "non_object_params"
+ # Preserve explicit client choices, including an explicit null.
+ if "modelProviders" in params:
+ return False, "explicit_filter_preserved"
+ # Native related-thread queries deliberately bypass the omitted-provider default.
+ if params.get("parentThreadId") is not None or params.get("ancestorThreadId") is not None:
+ return False, "relation_query_preserved"
+ params["modelProviders"] = list(policy.providers)
+ message["params"] = params
+ return True, "omitted_filter_patched"
+
+
+def rewrite_backend_frame(text: str, policy: Policy) -> Outcome:
+ """Process ONE backend-to-host text frame. No authentication decision is made.
+
+ The caller must already enforce the authorized connection and user opt-in.
+ Unmodified and unsupported messages are returned byte-for-byte unchanged.
+ """
+ if not isinstance(text, str):
+ raise TypeError("text frame must be str")
+ if not policy.enabled:
+ return Outcome(text, False, "disabled")
+ try:
+ if len(text.encode("utf-8")) > policy.max_wire_bytes:
+ return Outcome(text, False, "input_size_limit")
+ envelope = decode(text)
+ if not isinstance(envelope, dict):
+ return Outcome(text, False, "not_envelope")
+ if not isinstance(envelope.get("client_id"), str) or not envelope["client_id"]:
+ return Outcome(text, False, "missing_client_id")
+ kind = envelope.get("type")
+ if kind == "client_message":
+ changed, reason = _patch_message(envelope.get("message"), policy)
+ elif kind == "client_message_chunk":
+ if type(envelope.get("segment_count")) is not int or type(envelope.get("segment_id")) is not int:
+ return Outcome(text, False, "invalid_segment_index")
+ if envelope["segment_count"] != 1 or envelope["segment_id"] != 0:
+ return Outcome(text, False, "multi_segment_not_implemented")
+ size = envelope.get("message_size_bytes")
+ if type(size) is not int or size < 0 or size > policy.max_wire_bytes:
+ return Outcome(text, False, "invalid_segment_size")
+ payload_b64 = envelope.get("message_chunk_base64")
+ if not isinstance(payload_b64, str):
+ return Outcome(text, False, "invalid_segment_payload")
+ payload = base64.b64decode(payload_b64, validate=True)
+ if len(payload) != size:
+ return Outcome(text, False, "segment_length_mismatch")
+ message = decode(payload)
+ changed, reason = _patch_message(message, policy)
+ if changed:
+ encoded_payload = encode(message).encode("utf-8")
+ envelope["message_size_bytes"] = len(encoded_payload)
+ envelope["message_chunk_base64"] = base64.b64encode(encoded_payload).decode("ascii")
+ else:
+ return Outcome(text, False, "unrelated_envelope")
+ if not changed:
+ return Outcome(text, False, reason)
+ output = encode(envelope)
+ if len(output.encode("utf-8")) > policy.max_wire_bytes:
+ return Outcome(text, False, "output_size_limit")
+ return Outcome(output, True, reason)
+ except (ValueError, TypeError, UnicodeError, binascii.Error, RecursionError):
+ return Outcome(text, False, "invalid_frame_preserved")
+
+
+def make_envelope(message: dict[str, Any], *, chunk: bool = False) -> dict[str, Any]:
+ """Synthetic fixture builder; all identifiers below are test-only."""
+ envelope: dict[str, Any] = {
+ "client_id": "mock-client", "stream_id": "mock-stream",
+ "seq_id": 7, "cursor": "mock-backend-cursor",
+ }
+ if chunk:
+ payload = encode(message).encode("utf-8")
+ envelope.update(type="client_message_chunk", segment_id=0, segment_count=1,
+ message_size_bytes=len(payload),
+ message_chunk_base64=base64.b64encode(payload).decode("ascii"))
+ else:
+ envelope.update(type="client_message", message=message)
+ return envelope
+
+
+def extract_message(envelope: dict[str, Any]) -> dict[str, Any]:
+ if envelope["type"] == "client_message":
+ return envelope["message"]
+ return decode(base64.b64decode(envelope["message_chunk_base64"], validate=True))
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py
new file mode 100644
index 00000000000..2438a4334a6
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_loopback_bridge.py
@@ -0,0 +1,259 @@
+"""Loopback-only mock transport proof. NEVER connects to ChatGPT/OpenAI.
+
+This is a test fixture, not a production proxy. Native Codex is not executed.
+The mock backend validates a hard-coded, non-secret fixture token.
+"""
+from __future__ import annotations
+import asyncio
+import contextlib
+import unittest
+from urllib.parse import urlsplit
+
+import aiohttp
+from aiohttp import web
+from remote_list_probe import Policy, decode, encode, make_envelope, extract_message, rewrite_backend_frame
+
+BASE = "/backend-api"
+WS_PATH = BASE + "/wham/remote/control/server"
+MOCK_AUTH = "Bearer NOT-A-REAL-TOKEN-MOCK-ONLY"
+
+
+def assert_loopback_only(base: str) -> None:
+ url = urlsplit(base)
+ if url.scheme != "http" or url.hostname != "127.0.0.1" or not url.port or url.username or url.password:
+ raise ValueError("this research fixture permits only HTTP at literal 127.0.0.1")
+ if url.query or url.fragment or url.path:
+ raise ValueError("mock base must contain only origin")
+
+
+def selected_headers(headers) -> dict[str, str]:
+ return {key: value for key, value in headers.items()
+ if key.lower() in ("authorization", "chatgpt-account-id", "content-type")
+ or key.lower().startswith("x-codex-")}
+
+
+class LoopbackFixtureRelay:
+ def __init__(self, mock_backend: str, session: aiohttp.ClientSession, policy: Policy):
+ assert_loopback_only(mock_backend)
+ self.backend = mock_backend
+ self.session = session
+ self.policy = policy
+
+ async def handle(self, request: web.Request) -> web.StreamResponse:
+ # Test-only origin/host guard. No remote bind and no arbitrary upstream selection.
+ if request.headers.get("Origin"):
+ return web.Response(status=403, text="browser-origin-blocked")
+ if not request.host.startswith("127.0.0.1:"):
+ return web.Response(status=403, text="unexpected-host")
+ if request.path == WS_PATH and request.headers.get("Upgrade", "").lower() == "websocket":
+ return await self.handle_websocket(request)
+ target = self.backend + request.raw_path
+ async with self.session.request(request.method, target,
+ data=await request.read(),
+ headers=selected_headers(request.headers),
+ allow_redirects=False) as response:
+ body = await response.read()
+ headers = {key: value for key, value in response.headers.items()
+ if key.lower() in ("content-type", "x-request-id", "retry-after")}
+ return web.Response(status=response.status, body=body, headers=headers)
+
+ async def handle_websocket(self, request: web.Request) -> web.StreamResponse:
+ target = self.backend + request.raw_path
+ try:
+ upstream = await self.session.ws_connect(target,
+ headers=selected_headers(request.headers), autoping=False)
+ except aiohttp.WSServerHandshakeError as exc:
+ return web.Response(status=exc.status, text="mock-upstream-rejected")
+ downstream = web.WebSocketResponse(autoping=False)
+ await downstream.prepare(request)
+
+ async def pump(source, destination, transform: bool):
+ async for message in source:
+ if message.type == aiohttp.WSMsgType.TEXT:
+ text = rewrite_backend_frame(message.data, self.policy).text if transform else message.data
+ await destination.send_str(text)
+ elif message.type == aiohttp.WSMsgType.BINARY:
+ await destination.send_bytes(message.data)
+ elif message.type == aiohttp.WSMsgType.PING:
+ await destination.ping(message.data)
+ elif message.type == aiohttp.WSMsgType.PONG:
+ await destination.pong(message.data)
+ else:
+ break
+ # Forward the peer's close so the other side sees the same code
+ # instead of hanging on a connection that already ended.
+ close_code = getattr(source, "close_code", None) or 1000
+ if not destination.closed:
+ await destination.close(code=close_code)
+
+ tasks = [asyncio.create_task(pump(upstream, downstream, True)),
+ asyncio.create_task(pump(downstream, upstream, False))]
+ try:
+ done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
+ for task in done:
+ task.result()
+ finally:
+ for task in tasks:
+ task.cancel()
+ await asyncio.gather(*tasks, return_exceptions=True)
+ await upstream.close()
+ await downstream.close()
+ return downstream
+
+
+async def start_loopback_app(app: web.Application):
+ runner = web.AppRunner(app, access_log=None)
+ await runner.setup()
+ site = web.TCPSite(runner, "127.0.0.1", 0)
+ await site.start()
+ port = runner.addresses[0][1]
+ return runner, f"http://127.0.0.1:{port}"
+
+
+class LoopbackBridgeTests(unittest.IsolatedAsyncioTestCase):
+ async def asyncSetUp(self):
+ self.frames = []
+ self.received = asyncio.Queue()
+ self.handshake_headers = {}
+ self.http_received = []
+ self.backend_closed = asyncio.Event()
+ self.backend_close_code = None
+ self.backend_close_on_connect = None
+ self.backend_app = web.Application()
+ self.backend_app.router.add_route("*", "/backend-api/{tail:.*}", self.mock_backend)
+ self.backend_runner, self.backend_base = await start_loopback_app(self.backend_app)
+ self.relay_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5))
+ self.relay = LoopbackFixtureRelay(self.backend_base, self.relay_session, Policy(enabled=True))
+ relay_app = web.Application()
+ relay_app.router.add_route("*", "/backend-api/{tail:.*}", self.relay.handle)
+ self.relay_runner, self.relay_base = await start_loopback_app(relay_app)
+ self.host = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5))
+
+ async def asyncTearDown(self):
+ await self.host.close()
+ await self.relay_runner.cleanup()
+ await self.relay_session.close()
+ await self.backend_runner.cleanup()
+
+ async def mock_backend(self, request: web.Request):
+ if request.headers.get("Authorization") != MOCK_AUTH:
+ return web.Response(status=401, text="mock-auth-required")
+ if request.path == WS_PATH and request.headers.get("Upgrade", "").lower() == "websocket":
+ self.handshake_headers = dict(request.headers)
+ websocket = web.WebSocketResponse(autoping=False)
+ await websocket.prepare(request)
+ for frame in self.frames:
+ await websocket.send_str(frame)
+ if self.backend_close_on_connect is not None:
+ await websocket.close(code=self.backend_close_on_connect)
+ return websocket
+ async for message in websocket:
+ if message.type == aiohttp.WSMsgType.TEXT:
+ await self.received.put(message.data)
+ elif message.type == aiohttp.WSMsgType.PING:
+ await websocket.pong(message.data)
+ self.backend_close_code = websocket.close_code
+ self.backend_closed.set()
+ return websocket
+ body = await request.read()
+ self.http_received.append((request.path, body, selected_headers(request.headers)))
+ if request.path.endswith("/enroll"):
+ return web.json_response({"server_id": "mock-server", "environment_id": "mock-env",
+ "remote_control_token": "MOCK-ONLY", "expires_at": "2099-01-01T00:00:00Z"},
+ status=201, headers={"x-request-id": "mock-http-request"})
+ return web.Response(status=200, body=b"mock-unrelated-backend-endpoint")
+
+ def add_request(self, params=None, *, chunk=False):
+ request = {"id": 42, "method": "thread/list", "params": params or {"limit": 20}}
+ self.frames.append(encode(make_envelope(request, chunk=chunk)))
+
+ async def receive_host_frame(self, headers=None):
+ async with self.host.ws_connect(self.relay_base + WS_PATH,
+ headers=headers or {"Authorization": MOCK_AUTH}) as host:
+ message = await asyncio.wait_for(host.receive(), 2)
+ self.assertEqual(message.type, aiohttp.WSMsgType.TEXT)
+ return message.data
+
+ async def test_backend_to_host_rewrite_and_host_reply_passthrough(self):
+ self.add_request()
+ async with self.host.ws_connect(self.relay_base + WS_PATH,
+ headers={"Authorization": MOCK_AUTH}) as host:
+ incoming = await asyncio.wait_for(host.receive(), 2)
+ envelope = decode(incoming.data)
+ self.assertEqual(extract_message(envelope)["params"]["modelProviders"], ["openai", "opencodex"])
+ response = '{ "type":"server_message", "client_id":"mock-client", "stream_id":"mock-stream", "seq_id":9, "message":{"id":42,"result":{"data":[]}} }'
+ await host.send_str(response)
+ self.assertEqual(await asyncio.wait_for(self.received.get(), 2), response)
+
+ async def test_auth_and_protocol_handshake_headers_preserved(self):
+ self.add_request()
+ headers = {"Authorization": MOCK_AUTH, "chatgpt-account-id": "mock-account",
+ "x-codex-protocol-version": "3", "x-codex-server-id": "mock-server",
+ "x-codex-installation-id": "mock-installation", "x-codex-subscribe-cursor": "mock-resume-cursor"}
+ await self.receive_host_frame(headers)
+ actual = {k.lower(): v for k, v in self.handshake_headers.items()}
+ for name, value in headers.items():
+ self.assertEqual(actual[name.lower()], value)
+
+ async def test_invalid_auth_stays_denied(self):
+ with self.assertRaises(aiohttp.WSServerHandshakeError) as ctx:
+ await self.host.ws_connect(self.relay_base + WS_PATH,
+ headers={"Authorization": "Bearer WRONG-MOCK"})
+ self.assertEqual(ctx.exception.status, 401)
+
+ async def test_explicit_filter_frame_is_byte_identical(self):
+ self.add_request({"modelProviders": ["other"], "limit": 20})
+ self.assertEqual(await self.receive_host_frame(), self.frames[0])
+
+ async def test_single_chunk_transport(self):
+ self.add_request(chunk=True)
+ incoming = decode(await self.receive_host_frame())
+ self.assertEqual(extract_message(incoming)["params"]["modelProviders"], ["openai", "opencodex"])
+ self.assertEqual(incoming["seq_id"], 7)
+ self.assertEqual(incoming["cursor"], "mock-backend-cursor")
+
+ async def test_host_close_code_reaches_backend(self):
+ async with self.host.ws_connect(self.relay_base + WS_PATH,
+ headers={"Authorization": MOCK_AUTH}) as host:
+ await host.close(code=1001)
+ await asyncio.wait_for(self.backend_closed.wait(), 5)
+ self.assertEqual(self.backend_close_code, 1001)
+
+ async def test_backend_close_code_reaches_host(self):
+ self.backend_close_on_connect = 1011
+ async with self.host.ws_connect(self.relay_base + WS_PATH,
+ headers={"Authorization": MOCK_AUTH}) as host:
+ async for message in host:
+ if message.type in (aiohttp.WSMsgType.CLOSE,
+ aiohttp.WSMsgType.CLOSED,
+ aiohttp.WSMsgType.CLOSING):
+ break
+ self.assertEqual(host.close_code, 1011)
+
+ async def test_enrollment_http_forwarding_with_fake_credentials(self):
+ body = b'{"name":"mock-host","installation_id":"mock-installation"}'
+ async with self.host.post(self.relay_base + WS_PATH + "/enroll", data=body,
+ headers={"Authorization": MOCK_AUTH, "chatgpt-account-id": "mock-account"}) as response:
+ self.assertEqual(response.status, 201)
+ self.assertEqual(response.headers["x-request-id"], "mock-http-request")
+ self.assertEqual((await response.json())["server_id"], "mock-server")
+ self.assertEqual(self.http_received[0][1], body)
+
+ async def test_unrelated_http_backend_endpoint_forwarded(self):
+ async with self.host.get(self.relay_base + BASE + "/mock/account", headers={"Authorization": MOCK_AUTH}) as response:
+ self.assertEqual(response.status, 200)
+ self.assertEqual(await response.read(), b"mock-unrelated-backend-endpoint")
+
+ async def test_cross_origin_browser_request_denied(self):
+ async with self.host.get(self.relay_base + BASE + "/mock/account", headers={"Authorization": MOCK_AUTH, "Origin": "https://example.invalid"}) as response:
+ self.assertEqual(response.status, 403)
+ self.assertFalse(self.http_received)
+
+ async def test_fixture_rejects_real_service_upstream(self):
+ for base in ("https://chatgpt.com", "http://example.invalid", "http://0.0.0.0:1234"):
+ with self.subTest(base=base):
+ with self.assertRaises(ValueError):
+ LoopbackFixtureRelay(base, self.relay_session, Policy(enabled=True))
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py
new file mode 100644
index 00000000000..58d07be97f2
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_native_policy.py
@@ -0,0 +1,198 @@
+"""Offline contract tests. This does NOT run Rust, native Codex, or a mobile app."""
+from __future__ import annotations
+
+import hashlib
+import json
+import sqlite3
+import unittest
+from dataclasses import FrozenInstanceError
+from native_policy import ConnectionOrigin as Origin, RemoteListPolicy, resolve_provider_filter
+
+
+class NativePolicyTests(unittest.TestCase):
+ def resolve(self, **kwargs):
+ defaults = {"origin": Origin.REMOTE_CONTROL, "default_provider": "opencodex"}
+ defaults.update(kwargs)
+ return resolve_provider_filter(**defaults)
+
+ def test_opt_out_keeps_existing_default_for_every_origin(self):
+ for origin in Origin:
+ with self.subTest(origin=origin):
+ self.assertEqual(self.resolve(origin=origin), ("opencodex",))
+
+ def test_only_native_remote_origin_uses_opt_in(self):
+ policy = RemoteListPolicy(("openai", "opencodex"))
+ for origin in Origin:
+ with self.subTest(origin=origin):
+ expected = policy.providers if origin is Origin.REMOTE_CONTROL else ("opencodex",)
+ self.assertEqual(self.resolve(origin=origin, policy=policy), expected)
+
+ def test_explicit_nonempty_filter_wins_over_every_policy(self):
+ for policy in (RemoteListPolicy(), RemoteListPolicy(()), RemoteListPolicy(("openai",))):
+ for origin in Origin:
+ with self.subTest(policy=policy, origin=origin):
+ for requested in (("other",), ("other", "other"), ("",), (" other ",)):
+ self.assertEqual(self.resolve(origin=origin, requested=requested, policy=policy), requested)
+
+ def test_explicit_empty_filter_is_all_for_every_origin(self):
+ for origin in Origin:
+ with self.subTest(origin=origin):
+ self.assertIsNone(self.resolve(origin=origin, requested=(), policy=RemoteListPolicy(("openai",))))
+
+ def test_opt_in_all_is_not_the_default(self):
+ self.assertIsNone(self.resolve(policy=RemoteListPolicy(())))
+ self.assertEqual(self.resolve(), ("opencodex",))
+
+ def test_parent_query_keeps_native_no_default_filter(self):
+ for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))):
+ self.assertIsNone(self.resolve(parent_thread_id="00000000-0000-4000-8000-0000000000aa", policy=policy))
+
+ def test_ancestor_query_keeps_native_no_default_filter(self):
+ for policy in (RemoteListPolicy(), RemoteListPolicy(("opencodex",))):
+ self.assertIsNone(self.resolve(ancestor_thread_id="00000000-0000-4000-8000-0000000000bb", policy=policy))
+
+ def test_related_thread_ids_follow_native_validation(self):
+ # Upstream thread_list_response_inner rejects malformed ids and rejects
+ # parent+ancestor together; a malformed id must not silently widen the list.
+ for relation in (
+ {"parent_thread_id": ""},
+ {"ancestor_thread_id": " "},
+ {"parent_thread_id": "fixture-parent"},
+ {"ancestor_thread_id": "fixture-ancestor"},
+ {"parent_thread_id": "p"},
+ ):
+ with self.subTest(relation=relation), self.assertRaises(ValueError):
+ self.resolve(**relation)
+ # Python's UUID() tolerates hyphen shapes Rust Uuid::parse_str rejects:
+ # shifted hyphens, a bare 32-hex blob inside braces, an uppercased URN, and
+ # a braced simple string all stay invalid no matter the provider shape.
+ malformed = (
+ "000-00000-0000-4000-8000-0000000000aa",
+ "{000000000000400080000000000000aa}",
+ "URN:UUID:00000000-0000-4000-8000-0000000000aa",
+ "{000000000000-4000-8000-0000000000aa}",
+ )
+ for key in ("parent_thread_id", "ancestor_thread_id"):
+ for bad in malformed:
+ for requested in (None, ("openai",)):
+ with self.subTest(key=key, bad=bad, requested=requested), self.assertRaises(ValueError):
+ self.resolve(**{key: bad}, requested=requested)
+ # The four shapes upstream accepts stay valid for both relations.
+ for key in ("parent_thread_id", "ancestor_thread_id"):
+ for good in (
+ "00000000-0000-4000-8000-0000000000aa",
+ "000000000000400080000000000000aa",
+ "{00000000-0000-4000-8000-0000000000aa}",
+ "urn:uuid:00000000-0000-4000-8000-0000000000aa",
+ ):
+ with self.subTest(key=key, good=good):
+ self.assertIsNone(self.resolve(**{key: good}))
+ with self.assertRaises(ValueError):
+ self.resolve(
+ parent_thread_id="00000000-0000-4000-8000-0000000000aa",
+ ancestor_thread_id="00000000-0000-4000-8000-0000000000bb",
+ )
+
+ def test_explicit_filter_still_wins_for_related_queries(self):
+ for relation in (
+ {"parent_thread_id": "00000000-0000-4000-8000-0000000000aa"},
+ {"ancestor_thread_id": "00000000-0000-4000-8000-0000000000bb"},
+ ):
+ self.assertEqual(self.resolve(requested=("other",), policy=RemoteListPolicy(()), **relation), ("other",))
+
+ def test_null_and_omitted_match_typed_native_option_semantics(self):
+ for wire in ('{}', '{"modelProviders": null}'):
+ requested = json.loads(wire).get("modelProviders")
+ self.assertIsNone(requested)
+ self.assertEqual(self.resolve(requested=requested, policy=RemoteListPolicy(("openai",))), ("openai",))
+
+ def test_provider_names_are_not_hardcoded(self):
+ self.assertEqual(self.resolve(default_provider="custom"), ("custom",))
+ self.assertEqual(self.resolve(policy=RemoteListPolicy(("azure-team", "local"))), ("azure-team", "local"))
+
+ def test_provider_ids_are_exact_not_aliases(self):
+ self.assertEqual(self.resolve(policy=RemoteListPolicy(("OpenAI", " openai "))), ("OpenAI", " openai "))
+
+ def test_policy_cannot_be_mutated_during_pagination(self):
+ policy = RemoteListPolicy(("openai",))
+ with self.assertRaises(FrozenInstanceError):
+ policy.providers = ()
+
+ def test_rejects_invalid_policy(self):
+ for value in (["openai"], "openai", ("",), (" ",), (1,), ("openai", "openai")):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ RemoteListPolicy(value)
+
+ def test_rejects_untrusted_origin_string(self):
+ with self.assertRaises(ValueError):
+ self.resolve(origin="remote_control")
+
+ def test_rejects_invalid_default_provider(self):
+ for value in ("", " ", None, 1):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ self.resolve(default_provider=value)
+
+ def test_rejects_invalid_requested_filter(self):
+ for value in (["other"], "other", (1,), (None,)):
+ with self.subTest(value=value), self.assertRaises(ValueError):
+ self.resolve(requested=value)
+
+
+class SyntheticPaginationTests(unittest.TestCase):
+ """Fixture SQL illustrates the contract, not the native Codex store schema."""
+ def setUp(self):
+ self.db = sqlite3.connect(":memory:")
+ self.addCleanup(self.db.close)
+ self.db.execute("CREATE TABLE threads (id INTEGER PRIMARY KEY, provider TEXT NOT NULL)")
+ self.db.executemany("INSERT INTO threads VALUES (?, ?)",
+ [(i, "openai") for i in range(1, 5201)] + [(5201, "opencodex"), (5202, "other")])
+ self.before = self.digest()
+
+ def digest(self):
+ return hashlib.sha256("\n".join(self.db.iterdump()).encode()).hexdigest()
+
+ def list_all(self, policy, limit=37, requested=None):
+ providers = resolve_provider_filter(origin=Origin.REMOTE_CONTROL,
+ default_provider="opencodex", requested=requested, policy=policy)
+ rows, last_id = [], 0
+ while True:
+ values = [last_id]
+ sql = "SELECT id, provider FROM threads WHERE id > ?"
+ if providers is not None:
+ sql += " AND provider IN (" + ",".join("?" for _ in providers) + ")"
+ values.extend(providers)
+ sql += " ORDER BY id LIMIT ?"
+ values.append(limit)
+ page = self.db.execute(sql, values).fetchall()
+ if not page:
+ break
+ rows.extend(page)
+ last_id = page[-1][0]
+ self.assertEqual(self.digest(), self.before, "read must not retag fixture rows")
+ self.assertEqual(len({row[0] for row in rows}), len(rows))
+ return rows
+
+ def test_default_remains_one_thread(self):
+ self.assertEqual(self.list_all(RemoteListPolicy()), [(5201, "opencodex")])
+
+ def test_opt_in_two_provider_list_returns_5201_rows(self):
+ rows = self.list_all(RemoteListPolicy(("openai", "opencodex")))
+ self.assertEqual(len(rows), 5201)
+ self.assertNotIn("other", {row[1] for row in rows})
+
+ def test_explicit_all_returns_5202_rows(self):
+ self.assertEqual(len(self.list_all(RemoteListPolicy(()))), 5202)
+
+ def test_explicit_client_filter_not_broadened(self):
+ self.assertEqual(self.list_all(RemoteListPolicy(()), requested=("other",)), [(5202, "other")])
+
+ def test_unlisted_provider_returns_empty_without_fallback(self):
+ self.assertEqual(self.list_all(RemoteListPolicy(("not-in-fixture",))), [])
+
+ def test_every_page_uses_same_policy_without_duplicates(self):
+ policy = RemoteListPolicy(("openai", "opencodex"))
+ self.assertEqual(self.list_all(policy, limit=1), self.list_all(policy, limit=1000))
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py
new file mode 100644
index 00000000000..ffdb0c6e24d
--- /dev/null
+++ b/devlog/_plan/260928_remote_thread_provider_policy/probes/test_probe.py
@@ -0,0 +1,196 @@
+from __future__ import annotations
+import base64
+import hashlib
+import json
+import sqlite3
+import unittest
+from copy import deepcopy
+from remote_list_probe import Policy, decode, encode, make_envelope, extract_message, rewrite_backend_frame
+
+
+class RewriteTests(unittest.TestCase):
+ def setUp(self):
+ self.policy = Policy(enabled=True)
+ self.request = {"id": 27, "method": "thread/list", "params": {"limit": 20, "cursor": "opaque"}}
+
+ def frame(self, request=None, chunk=False):
+ return encode(make_envelope(deepcopy(self.request if request is None else request), chunk=chunk))
+
+ def result(self, request=None, chunk=False, policy=None):
+ return rewrite_backend_frame(self.frame(request, chunk), policy or self.policy)
+
+ def assert_preserved(self, raw):
+ outcome = rewrite_backend_frame(raw, self.policy)
+ self.assertFalse(outcome.changed)
+ self.assertEqual(raw, outcome.text)
+
+ def test_opt_in_required(self):
+ raw = self.frame()
+ self.assertEqual(rewrite_backend_frame(raw, Policy()).text, raw)
+ self.assertFalse(rewrite_backend_frame(raw, Policy()).changed)
+
+ def test_plain_omitted_filter(self):
+ out = self.result()
+ self.assertTrue(out.changed)
+ self.assertEqual(extract_message(decode(out.text))["params"]["modelProviders"], ["openai", "opencodex"])
+
+ def test_explicit_all_provider_mode(self):
+ out = self.result(policy=Policy(enabled=True, providers=()))
+ self.assertEqual(extract_message(decode(out.text))["params"]["modelProviders"], [])
+
+ def test_preserves_envelope_metadata(self):
+ before = decode(self.frame())
+ after = decode(self.result().text)
+ for key in ("client_id", "stream_id", "seq_id", "cursor"):
+ self.assertEqual(before[key], after[key])
+
+ def test_preserves_all_other_parameters(self):
+ self.request["params"].update(archived=True, cwd="C:/mock", sourceKinds=["cli"],
+ sortDirection="asc", searchTerm="mock", isPinned=False)
+ after = extract_message(decode(self.result().text))
+ del after["params"]["modelProviders"]
+ self.assertEqual(after, self.request)
+
+ def test_explicit_filter_preserved(self):
+ self.request["params"]["modelProviders"] = ["other"]
+ self.assert_preserved(self.frame())
+
+ def test_explicit_empty_filter_preserved(self):
+ self.request["params"]["modelProviders"] = []
+ self.assert_preserved(self.frame())
+
+ def test_explicit_null_filter_preserved(self):
+ self.request["params"]["modelProviders"] = None
+ self.assert_preserved(self.frame())
+
+ def test_missing_params_created(self):
+ del self.request["params"]
+ self.assertTrue(self.result().changed)
+
+ def test_non_object_params_preserved(self):
+ for value in (None, [], "not-an-object", 1):
+ with self.subTest(value=value):
+ self.request["params"] = value
+ self.assert_preserved(self.frame())
+
+ def test_resume_and_other_methods_unchanged(self):
+ for method in ("thread/resume", "thread/start", "turn/start", "turn/interrupt", "initialize", "thread/search"):
+ with self.subTest(method=method):
+ self.request["method"] = method
+ self.assert_preserved(self.frame())
+
+ def test_notification_without_id_unchanged(self):
+ del self.request["id"]
+ self.assert_preserved(self.frame())
+
+ def test_request_id_preserved_exactly(self):
+ for request_id in ("request-string", 9007199254740993, 0, -1):
+ with self.subTest(request_id=request_id):
+ self.request["id"] = request_id
+ self.assertEqual(extract_message(decode(self.result().text))["id"], request_id)
+
+ def test_response_like_object_unchanged(self):
+ self.request["result"] = {"data": []}
+ self.assert_preserved(self.frame())
+
+ def test_relation_query_bypass_preserved(self):
+ for key in ("parentThreadId", "ancestorThreadId"):
+ with self.subTest(key=key):
+ self.request["params"] = {key: "00000000-0000-4000-8000-0000000000aa"}
+ self.assert_preserved(self.frame())
+
+ def test_null_relation_does_not_block(self):
+ self.request["params"]["parentThreadId"] = None
+ self.assertTrue(self.result().changed)
+
+ def test_ping_ack_close_unchanged(self):
+ for kind in ("ping", "ack", "client_closed", "unknown_future_event"):
+ with self.subTest(kind=kind):
+ self.assert_preserved(encode({"type": kind, "client_id": "mock-client", "seq_id": 3}))
+
+ def test_invalid_json_preserved(self):
+ for raw in ("{broken", "[]", "null", '{"type":"client_message","client_id":"x","message":NaN}'):
+ with self.subTest(raw=raw):
+ self.assert_preserved(raw)
+
+ def test_duplicate_members_preserved(self):
+ raw = self.frame().replace('"method":"thread/list"', '"method":"thread/list","method":"thread/resume"')
+ self.assert_preserved(raw)
+
+ def test_unknown_envelope_fields_preserved(self):
+ envelope = decode(self.frame())
+ envelope["future_field"] = {"tag": [1, 2, 3]}
+ out = rewrite_backend_frame(encode(envelope), self.policy)
+ self.assertEqual(decode(out.text)["future_field"], envelope["future_field"])
+
+ def test_single_chunk_transformed_with_correct_byte_size(self):
+ self.request["params"]["searchTerm"] = "한글🙂"
+ out = self.result(chunk=True)
+ self.assertTrue(out.changed)
+ after = decode(out.text)
+ payload = base64.b64decode(after["message_chunk_base64"], validate=True)
+ self.assertEqual(len(payload), after["message_size_bytes"])
+ self.assertEqual(decode(payload)["params"]["modelProviders"], ["openai", "opencodex"])
+ self.assertEqual(after["seq_id"], 7)
+
+ def test_single_chunk_existing_filter_unchanged(self):
+ self.request["params"]["modelProviders"] = ["other"]
+ self.assert_preserved(self.frame(chunk=True))
+
+ def test_multi_segment_deliberately_not_implemented(self):
+ envelope = decode(self.frame(chunk=True))
+ envelope["segment_count"] = 2
+ out = rewrite_backend_frame(encode(envelope), self.policy)
+ self.assertEqual(out.reason, "multi_segment_not_implemented")
+ self.assertFalse(out.changed)
+
+ def test_bad_chunk_length_unchanged(self):
+ envelope = decode(self.frame(chunk=True))
+ envelope["message_size_bytes"] += 1
+ self.assert_preserved(encode(envelope))
+
+ def test_bad_chunk_base64_unchanged(self):
+ envelope = decode(self.frame(chunk=True))
+ envelope["message_chunk_base64"] = "not@@base64"
+ self.assert_preserved(encode(envelope))
+
+ def test_oversized_input_unchanged(self):
+ raw = self.frame()
+ out = rewrite_backend_frame(raw, Policy(enabled=True, max_wire_bytes=64))
+ self.assertFalse(out.changed)
+ self.assertEqual(out.text, raw)
+
+ def test_oversized_output_unchanged(self):
+ raw = self.frame()
+ out = rewrite_backend_frame(raw, Policy(enabled=True, max_wire_bytes=len(raw.encode())))
+ self.assertFalse(out.changed)
+ self.assertEqual(out.reason, "output_size_limit")
+
+ def test_invalid_policy_rejected(self):
+ for providers in (("",), ("a", "a"), (1,), ["a"]):
+ with self.subTest(providers=providers):
+ with self.assertRaises(ValueError):
+ Policy(enabled=True, providers=providers)
+
+ def test_synthetic_sqlite_visibility_and_no_mutation(self):
+ # Synthetic fixture, NOT the user's database and NOT native Codex.
+ db = sqlite3.connect(":memory:")
+ try:
+ db.execute("CREATE TABLE threads (id INTEGER, model_provider TEXT)")
+ db.executemany("INSERT INTO threads VALUES (?, ?)", [(i, "openai") for i in range(5200)])
+ db.execute("INSERT INTO threads VALUES (5200, 'opencodex')")
+ db.execute("INSERT INTO threads VALUES (5201, 'unrelated')")
+ before = hashlib.sha256("\n".join(db.iterdump()).encode()).hexdigest()
+ self.assertEqual(db.execute("SELECT count(*) FROM threads WHERE model_provider = 'opencodex'").fetchone()[0], 1)
+ providers = extract_message(decode(self.result().text))["params"]["modelProviders"]
+ placeholders = ",".join("?" for _ in providers)
+ scoped = db.execute(f"SELECT count(*) FROM threads WHERE model_provider IN ({placeholders})", providers).fetchone()[0]
+ self.assertEqual(scoped, 5201)
+ self.assertEqual(db.execute("SELECT count(*) FROM threads").fetchone()[0], 5202)
+ after = hashlib.sha256("\n".join(db.iterdump()).encode()).hexdigest()
+ self.assertEqual(before, after)
+ finally:
+ db.close()
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/docs-site/src/content/docs/fr/getting-started/quickstart.md b/docs-site/src/content/docs/fr/getting-started/quickstart.md
index a3eb2f9fea0..1306a62d864 100644
--- a/docs-site/src/content/docs/fr/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/fr/getting-started/quickstart.md
@@ -13,7 +13,7 @@ ocx init
`ocx init` vous accompagne dans les étapes suivantes :
-1. **Choix d’un fournisseur** — sélectionnez l’un des 104 préréglages intégrés au registre, ou `custom` pour saisir une
+1. **Choix d’un fournisseur** — sélectionnez l’un des 105 préréglages intégrés au registre, ou `custom` pour saisir une
URL de base et un adaptateur.
2. **Clé API** — collez une clé ou référencez une variable d’environnement telle que `${ANTHROPIC_API_KEY}`.
3. **Modèle par défaut** — pour les fournisseurs clés, locaux et personnalisés, acceptez le préréglage ou saisissez un identifiant de modèle.
diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md
index 49cd7d6113e..0405b2807d9 100644
--- a/docs-site/src/content/docs/fr/guides/claude-code.md
+++ b/docs-site/src/content/docs/fr/guides/claude-code.md
@@ -169,24 +169,24 @@ unknown signifie qu’opencodex ne peut pas déterminer si les réglages pointen
### Mode picker : modèles opencodex dans le sélecteur Code first-party
-Le mode picker fait partie du mode first-party. Sur macOS, il est activé par défaut lorsque first-party
+Le mode picker fait partie du mode first-party. Sur macOS et Windows, il est activé par défaut lorsque first-party
est sélectionné, sauf si `claudeCode.intercept.picker: false` est défini. Il modifie le sélecteur de
modèles de l'onglet Code de Desktop first-party pour y afficher les modèles opencodex disponibles par
leur nom. Lors de la première activation, macOS peut demander l'autorisation d'une autorité de certification
-locale dans le trousseau de connexion. Cette autorité est limitée à `claude.ai` et à ses sous-domaines.
+locale dans le trousseau de connexion, et Windows dans le magasin racine de l'utilisateur actuel. Cette autorité est limitée à `claude.ai` et à ses sous-domaines.
Son identité de signature exportable est protégée par le magasin d'identifiants du système et réutilisée
lors des redémarrages ordinaires. Aucune clé de signature picker en clair n'est enregistrée dans le
répertoire de configuration OpenCodex. La validation complète de l'autorité contrainte et la vérification
-de la confiance du système restent obligatoires. Avec la même identité approuvée et un magasin accessible,
-un redémarrage n'ajoute ni ne supprime de réglages de confiance des certificats. La restauration au démarrage
-n'installe jamais la confiance : si elle manque, est révoquée ou inconnue, le picker reste en attente.
-Exécutez explicitement `ocx claude desktop picker on` ou `ocx claude desktop picker trust` pour l'accorder.
+de la confiance du système restent obligatoires, avec sur Windows la correspondance exacte de l'empreinte et une feuille vérifiée. Avec la même identité approuvée et un magasin accessible,
+un redémarrage n'ajoute ni ne supprime de réglages de confiance des certificats macOS. La restauration au démarrage
+ou la vérification d'état n'installe jamais la confiance : si elle manque, est révoquée ou inconnue, le picker reste en attente.
+Exécutez explicitement `ocx claude desktop picker on` ou `ocx claude desktop picker trust` pour l'accorder, y compris la confirmation Windows si nécessaire.
Une migration unique depuis une ancienne identité peut demander votre consentement pour supprimer sa
confiance. Si ce nettoyage échoue, le picker reste indisponible et le profil appliqué utilise un relais
sans déchiffrement jusqu'à sa réussite. macOS peut aussi demander de déverrouiller le trousseau ou
-d'autoriser l'accès d'une application aux identifiants, y compris après un redémarrage ou une mise à jour.
-Le picker reste non pris en charge sous Windows et Linux : aucun travail de CA, de magasin d'identifiants
+d'autoriser l'accès d'une application aux identifiants, y compris après un redémarrage ou une mise à jour. Sur Windows, `on` ou `trust` explicite peut afficher une confirmation d'ajout de la racine.
+Le picker reste non pris en charge sous Linux : aucun travail de CA, de magasin d'identifiants
ou de proxy picker n'y démarre. L'interception Claude principale reste disponible ; ses fichiers CA locaux
sont protégés par des vérifications du propriétaire, des liens symboliques, des permissions et des ACL Windows.
diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md
index 4cb051fc727..7076e7df862 100644
--- a/docs-site/src/content/docs/fr/guides/combos.md
+++ b/docs-site/src/content/docs/fr/guides/combos.md
@@ -201,7 +201,7 @@ Les échecs d’un combo se répartissent entre ceux qui entraînent un **bascul
| HTTP 401, 403, 404, 408, 429, ou n'importe quel 5xx | Refroidissez la cible et passez à la prochaine cible éligible. |
| Erreur classée comme erreur d’authentification, d’abonnement, de quota, de limitation de débit, de surcharge ou de serveur en amont | Place la cible en période de refroidissement et bascule, même si le statut seul ne suffit pas. |
| Annulation client (499), `origin_rejected`, refus de cyber-politique, débordement de contexte ou autre demande invalide | Arrêtez et renvoyez l'erreur ; une autre cible ne rendrait pas la demande valide. |
-| Rejet structuré de `user`, valeur non prise en charge pour `reasoning.effort`/`reasoning_effort`, ou rejet d'entrée d'image propre à un modèle (`param: input`) | Bascule vers la cible admissible suivante avant le début de la sortie, sans délai de refroidissement ; voir Compatibilité des paramètres facultatifs ci-dessous. |
+| Rejet structuré de `user`, valeur non prise en charge pour `reasoning.effort`/`reasoning_effort`, rejet d'entrée d'image propre à un modèle (`param: input`), ou refus d'un média d'entrée que la route ne peut pas lire (`unsupported_input_modality`) | Bascule vers la cible admissible suivante avant le début de la sortie, sans délai de refroidissement ; voir Compatibilité des paramètres facultatifs ci-dessous. |
| Premier appel d'outil d'un tour Responses exécuté par un adaptateur interne (`runTurn`) que la requête courante n'a pas déclaré, avant toute sortie et tout effet de bord non rejouable | Met la cible en refroidissement et bascule avec le même catalogue d'outils. Après une sortie visible ou un effet de bord non rejouable, le refus est définitif. Les requêtes Chat Completions et Anthropic Messages ne changent pas. |
| Toute autre erreur non classifiée | Arrêtez et renvoyez l'erreur. |
@@ -277,7 +277,7 @@ flux de travail des tâches.
### Tableau de bord
Ouvrez le tableau de bord local et choisissez **Modèles → Combos**. L'espace de travail crée, modifie, renomme et supprime
-combos, et son sélecteur de cible exclut les modèles désactivés et les combos imbriqués.
+les combos. Les sélecteurs de modèles incluent les modèles masqués : la visibilité ne concerne que les listes externes, pas la sélection interne des combos. Sélectionner un modèle masqué ne le rend pas visible. Le sélecteur de cible exclut toujours les fournisseurs désactivés, les combos imbriqués et les fournisseurs JEV réservés au service de décision.
Chaque cible affiche aussi un badge de quota en direct : **Disponible**, **Quota épuisé** ou **Quota inconnu**.
L’éditeur bloque Enregistrer et Créer pour une raison de quota uniquement lorsque chaque cible utilisable dispose d’une confirmation serveur encore valide indiquant que la limite d’inférence liée à ses identifiants configurés est épuisée. Les quotas de compte, de modèle, de recherche et de MCP fournis uniquement à titre d’affichage, ainsi que les informations de routage absentes ou expirées, ne déclenchent pas ce blocage. Le blocage expire à la réinitialisation applicable ou à l’expiration de la validité des données et fait l’objet d’une nouvelle vérification lorsque la page devient active ou visible ; Actualiser recharge à la fois les données des combos et les quotas.
@@ -370,6 +370,6 @@ gérer un refus de politique ou corriger l’origine de la demande rejetée. Les
## Compatibilité des paramètres facultatifs
-Exception aux erreurs 400 terminales : un rejet structuré de `user`, une valeur non prise en charge pour `reasoning.effort`/`reasoning_effort`, ou un rejet d’entrée d’image propre à un modèle (`param: input`) peut faire passer le combo à la cible admissible suivante avant le début de la sortie, sans délai de refroidissement. Le refus de sécurité, l’annulation et une sortie déjà commencée restent non rejouables.
+Exception aux erreurs 400 terminales : un rejet structuré de `user`, une valeur non prise en charge pour `reasoning.effort`/`reasoning_effort`, un rejet d’entrée d’image propre à un modèle (`param: input`), ou le refus d’un média d’entrée que la route ne peut pas lire (`unsupported_input_modality`) peut faire passer le combo à la cible admissible suivante avant le début de la sortie, sans délai de refroidissement. Le refus de sécurité, l’annulation et une sortie déjà commencée restent non rejouables.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md
index f820ce88b5a..1e5af166a64 100644
--- a/docs-site/src/content/docs/fr/guides/providers.md
+++ b/docs-site/src/content/docs/fr/guides/providers.md
@@ -112,6 +112,7 @@ ocx login google-antigravity
ocx login cursor # standalone Cursor PKCE login
ocx login command-code # Command Code browser OAuth (or import ~/.commandcode/auth.json)
ocx login devin # Cognition/Devin : import de l'identifiant du Devin CLI, sinon connexion navigateur Auth0
+ocx login dsh-account # DeepSeek Harness Desktop (import de l'autorisation ~/.dsh/.credentials.yaml)
ocx login github-copilot # GitHub device flow → Copilot token (Copilot Pro/Business)
ocx login codex # pool de comptes Codex (alias : chatgpt, openai ; nécessite un proxy en cours d'exécution)
ocx logout
@@ -128,6 +129,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth avec le protocole Cloud Code Assist. La découverte en direct utilise le point de terminaison CCA authentifié `v1internal:fetchAvailableModels` et publie les modèles d'agent accessibles au compte connecté ; le catalogue maintenu reste la solution de repli. |
| `cursor` | `cursor` | `https://api2.cursor.sh` | Connexion PKCE expérimentale, transport HTTP/2 en direct et découverte de modèles filtrés par compte. |
| `devin` | `devin` | `https://server.codeium.com` | Passerelle Cognition/Devin non officielle et expérimentale. La connexion importe d'abord l'identifiant que le Devin CLI installé détient déjà (`devin auth login` écrit un `devin-session-token` dans son propre `credentials.toml`) ; à défaut, elle ouvre l'authentification Auth0 dans le navigateur puis échange le jeton collé via `RegisterUser` contre une clé d'API durable. `ocx login devin-cli` reste accepté comme alias déprécié. Les modèles sont découverts par compte avec `GetCascadeModelConfigs` ; le streaming passe uniquement par `runTurn` sur Connect-RPC. Absente du préréglage du tableau de bord par défaut. |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | Fournisseur de compte expérimental à importation prioritaire réutilisant un compte DeepSeek Harness (DSH) Desktop installé et connecté. Lit les identifiants en lecture seule stricte depuis `~/.dsh/.credentials.yaml` lors de l'importation explicite. Indépendant de la facturation par clé d'API DeepSeek. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Expérimental. Flux d'appareil GitHub et échange `copilot_internal` (client OAuth de VS Code). Nécessite un abonnement Copilot actif ; il ne s'agit pas d'une API tierce officielle. |
Les vérifications de quota Google Antigravity utilisent des points de terminaison Google fixes, y compris le repli vers la liste des modèles. Elles prennent en charge le DNS Fake-IP transparent pour ces destinations en conservant la vérification TLS, le refus des redirections et les contrôles des adresses privées. Une URL de base personnalisée ne modifie que les requêtes de modèles ; `NO_PROXY` conserve la politique de connexion directe.
@@ -293,9 +295,20 @@ reconnue. Réparez ou supprimez la base illisible dans le chemin de données nor
sélecteurs d'importation, puis réessayez. La connexion depuis une machine dépourvue de session `kiro-cli`
existante n'est pas concernée.
+### DeepSeek Account (DSH)
+
+`dsh-account` est un fournisseur de compte expérimental à importation prioritaire, conçu pour réutiliser les identifiants de compte authentifiés d'une application DeepSeek Harness (DSH) Desktop installée.
+
+- **Indépendant de la clé d'API DeepSeek :** `dsh-account` utilise le quota de session DeepSeek authentifiée de l'utilisateur (portefeuilles standard et bonus, dépenses totales), tandis que le fournisseur `deepseek` facture sur le solde de la clé d'API de la plateforme. Les deux domaines d'identifiants et de routage sont strictement distincts et ne basculent jamais l'un vers l'autre.
+- **Importation explicite uniquement :** OpenCodeX détecte si DeepSeek Harness est installé et connecté (`~/.dsh/.credentials.yaml`), mais ne copiera, n'importera ni n'utilisera jamais automatiquement le jeton sans une action explicite de l'utilisateur (`ocx login dsh-account` ou clic sur Importer dans le tableau de bord web).
+- **Magasin d'identifiants DSH en lecture seule :** OpenCodeX traite `~/.dsh/.credentials.yaml` comme strictement en lecture seule. OpenCodeX n'écrit, ne modifie, ne supprime ni ne change jamais les permissions des fichiers appartenant à DeepSeek Harness.
+- **Copie d'identifiant et déconnexion isolées :** Lors de l'importation, OpenCodeX valide le jeton auprès du point de terminaison d'identité officiel de DeepSeek (`/auth-api/v0/users/current`) et enregistre sa propre copie dans le magasin d'authentification OpenCodeX. Se déconnecter ou supprimer le compte dans OpenCodeX supprime uniquement la copie locale d'OpenCodeX ; cela ne déclenche jamais de déconnexion de session distante et ne vous déconnecte pas de DeepSeek Harness Desktop.
+- **Pas de rotation automatique en arrière-plan et réimportation en cas de 401 :** Les autorisations de session DSH ne disposent pas d'un point de terminaison d'actualisation automatique en arrière-plan. Si le serveur distant renvoie HTTP 401 (`ACCOUNT_TOKEN_INVALID`), OpenCodeX marque le compte comme `needsReauth` et interrompt le routage. Pour rétablir l'accès, l'utilisateur doit réimporter explicitement via `ocx login dsh-account`.
+- **Avertissement relatif aux tiers :** OpenCodeX ne revendique aucune approbation ni autorisation officielle de DeepSeek pour la réutilisation des autorisations de session par des clients tiers.
+
## 3. Catalogue des clés API
-opencodex fournit 104 préréglages intégrés : 85 à clé, 15 OAuth, trois locaux et un préréglage par défaut de
+opencodex fournit 105 préréglages intégrés : 85 à clé, 16 OAuth, trois locaux et un préréglage par défaut de
transfert ChatGPT. Dans le tableau de bord, le sélecteur **Ajouter un fournisseur** ouvre le tableau de bord du
fournisseur à clé, valide la clé et l'enregistre ; la validation dépend du fournisseur. Parmi les entrées notables :
diff --git a/docs-site/src/content/docs/fr/guides/sidecars.md b/docs-site/src/content/docs/fr/guides/sidecars.md
index 1fcc91cfec6..ee37d9ef37b 100644
--- a/docs-site/src/content/docs/fr/guides/sidecars.md
+++ b/docs-site/src/content/docs/fr/guides/sidecars.md
@@ -88,9 +88,12 @@ les échecs de génération postérieurs à l'envoi des en-têtes sont transmis
Lorsqu'un modèle routé figure dans le `noVisionModels` de son fournisseur — ou est déclaré texte seul pour ce modèle
via `modelInputModalities` — et qu'une requête porte une image, opencodex décrit chaque image **avant** l'appel principal
et la remplace par du texte, à condition qu'un plan de sidecar vision soit disponible. Sans plan disponible, l'image brute
-est supprimée au lieu d'être transmise à un backend texte seul. Le catalogue de modèles annonce l'entrée image pour chaque
-modèle couvert par le sidecar. Les combos annoncent l'entrée image seulement lorsque chaque membre accepte les images,
-nativement ou via un sidecar, et que le paramètre `imageInput` du combo n'est pas désactivé, afin que des clients comme
+est supprimée au lieu d'être transmise à un backend texte seul. Avec `visionSidecar.enabled: false`, le catalogue de modèles
+n'ajoute pas d'entrée image fournie par le sidecar aux modèles texte seuls, y compris les entrées personnalisées et les
+membres des combos. La prise en charge native des images et les modalités image explicitement configurées sont conservées.
+Si `enabled` est omis, l'annonce des capacités du sidecar reste activée ; la présence dans le catalogue ne garantit pas
+que les identifiants ou la capacité du sidecar sont disponibles. Les combos annoncent l'entrée image seulement lorsque
+chaque membre accepte les images, nativement ou via un sidecar activé, et que le paramètre `imageInput` du combo n'est pas désactivé, afin que des clients comme
l'application Codex autorisent les pièces jointes au lieu de les bloquer avant l'exécution du sidecar. Lorsque
`visionSidecar.model` est absent ou vide, le chemin d'exécution OpenAI, le tableau de bord et l'API de gestion
utilisent le modèle de repli `gpt-5.6-luna`. Au démarrage, une ancienne valeur `gpt-5.6-luna` explicitement enregistrée
@@ -164,6 +167,8 @@ le délai d'attente et la limite précédemment choisis.
les clés omises inchangées. `timeoutMs` utilise les limites entières de l'environnement d'exécution
(1–2147483647 ms).
+Un changement de l'état d'activation effectif de Vision via `PUT /api/sidecar-settings` actualise le catalogue et le cache gérés après l'enregistrement ; `catalogRefreshPending=true` signale que cette actualisation n'a pas abouti et ne constitue pas un accusé de réception du client.
+
La carte du service auxiliaire de recherche web reprend la même forme de contrôle : la première
ligne du sélecteur de modèle est **Désactivé (Off)**. La désactivation arrête l'interception de
`web_search` par OpenCodex et l'intégration Codex écrit `web_search = "disabled"` dans
diff --git a/docs-site/src/content/docs/fr/guides/subagent-v1-default.md b/docs-site/src/content/docs/fr/guides/subagent-v1-default.md
index cafcd8b6a32..d93aa422837 100644
--- a/docs-site/src/content/docs/fr/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/fr/guides/subagent-v1-default.md
@@ -148,3 +148,14 @@ ocx v2 mode v1
La CLI ne demande pas de confirmation. C'est le même paramètre : choisissez-le
en tenant compte de ce que décrit cette page.
+
+## Limite de sous-agents simultanés
+
+Dans **Models → Model settings**, le réglage de limite des sous-agents définit le nombre maximal
+de sous-agents simultanés, sans compter l'agent principal. Il reste disponible en v1, base et v2,
+y compris en mode hybride lorsque le drapeau v2 global est désactivé. Le choix du mode est
+indépendant de cette limite.
+
+Augmenter la limite autorise davantage de sous-agents, sans les créer automatiquement ni garantir
+une exécution plus rapide. Lancez une nouvelle session Codex après modification. Si un hôte App
+de longue durée utilise encore l'ancienne limite, redémarrez cet hôte.
diff --git a/docs-site/src/content/docs/fr/reference/cli.md b/docs-site/src/content/docs/fr/reference/cli.md
index 26211aae04d..2563cdc0de9 100644
--- a/docs-site/src/content/docs/fr/reference/cli.md
+++ b/docs-site/src/content/docs/fr/reference/cli.md
@@ -17,7 +17,7 @@ Exécutez `ocx help` (ou `ocx --help` / `ocx -h`) pour afficher l’aide génér
Les commandes de gestion communiquent avec l’API de gestion du proxy actif. Elles s’appuient sur le port d’exécution enregistré et sur des contrôles d’identité, plutôt que sur un second chemin de configuration. Un proxy arrêté ou inaccessible est représenté par une réponse HTTP 503 et entraîne un code de sortie CLI non nul. Les commandes explicitement documentées comme des opérations de configuration hors ligne peuvent, quant à elles, valider et modifier le fichier de configuration sans proxy actif.
-`ocx system codex-cli-update check` ne nécessite aucun proxy actif et n’interroge aucun registre de paquets. La commande inspecte, dans des limites strictes, les métadonnées de provenance du candidat d’installation configuré, notamment l’emplacement expurgé de l’exécutable et les preuves de propriété. Le contexte de confiance du lanceur publié authentifie uniquement cet instantané du candidat, et non l’exécution réussie de Codex. Comme cette commande ponctuelle n’exécute jamais Codex, les candidats issus de l’environnement ou de l’état persistant restent purement informatifs (`managed: false`, normalement `selection_unattested`) et `selectionAttested` reste `false`. La sortie JSON contient `candidateAvailable`, `candidateVersion`, `candidateSource` et `selectionAttested: false`. Une exécution directe via Bun ou depuis les sources ne fournit pas la preuve du lanceur, ignore les candidats issus de l’environnement ou de l’état persistant et peut signaler `candidate_unavailable` sous POSIX ou `windows_inspection_deferred` sous Windows. Sous Windows, cette première étape n’effectue aucune E/S de système de fichiers sur les chemins du candidat ou de configuration. Seul un candidat d’environnement absolu capturé par le lanceur de confiance peut recevoir une étiquette lexicale de bundle d’application ou de gestionnaire de versions ; tous les autres candidats Windows échouent de manière fermée. La commande n’installe ni ne répare de logiciel, n’exécute ni Codex ni npm, ne contrôle aucun processus actif et n’écrit aucun état de configuration ou de cache.
+`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
Pour observer une installation Windows x64, consultez [`attest`](/fr/reference/cli/agents/). Sans chemins explicites, la commande observe le candidat sélectionné identifié par l’instantané du lanceur lié à la preuve ; aucune autorité de mise à jour ni sélection du runtime n’est attestée.
diff --git a/docs-site/src/content/docs/fr/reference/cli/agents.md b/docs-site/src/content/docs/fr/reference/cli/agents.md
index 03019ca987d..1337a76d0ce 100644
--- a/docs-site/src/content/docs/fr/reference/cli/agents.md
+++ b/docs-site/src/content/docs/fr/reference/cli/agents.md
@@ -273,7 +273,9 @@ le CLI, l’API, et le GUI utilisent les mêmes octets.
Gérez les paramètres d'exécution sans tête, le démarrage, la synchronisation, les diagnostics et les mises à jour.
-`ocx system codex-restart --yes` redémarre les serveurs d'application Codex et quitte puis relance entièrement l'application Codex Desktop, via le même module que `ocx sync --restart-codex`. Lorsque le proxy lui-même s'exécute dans l'application Codex, la commande refuse avec un message actionnable au lieu de promettre un transfert qu'elle ne peut pas mener à bien.
+`ocx system codex-restart --yes` redémarre les serveurs d'application Codex via le même module que `ocx sync --restart-codex`. Elle quitte et relance l'application Codex Desktop si celle-ci a démarré avant l'écriture du catalogue de modèles actuel ou si cette requête a modifié sa liaison `model_catalog_json`, car le sélecteur de modèles est construit au lancement. Sinon l'application reste ouverte, relance son propre app-server et conserve les brouillons non envoyés et les approbations en attente. Si l'état de l'application ou du catalogue est illisible, elle effectue le redémarrage complet. Lorsque le proxy lui-même s'exécute dans l'application Codex, le redémarrage Desktop refuse avec un message actionnable au lieu de promettre un transfert qu'il ne peut pas mener à bien.
+
+Une liaison `model_catalog_json` modifiée par cette requête impose également le redémarrage Desktop, même si le processus a démarré après l’écriture du fichier du catalogue.
```bash
ocx system settings --stream-mode eager-relay
@@ -285,7 +287,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` n’interroge aucun registre de paquets et inspecte, dans des limites strictes, les éléments de provenance du candidat d’installation configuré, notamment l’emplacement expurgé de l’exécutable et les preuves de propriété. Le contexte de confiance du lanceur publié authentifie uniquement cet instantané du candidat, et non l’exécution réussie de Codex. Comme cette commande ponctuelle n’exécute jamais Codex, les candidats issus de l’environnement ou de l’état persistant restent purement informatifs (`managed: false`, normalement `selection_unattested`) et `selectionAttested` reste `false`. La sortie JSON contient `candidateAvailable`, `candidateVersion`, `candidateSource` et `selectionAttested: false`. Une exécution directe via Bun ou depuis les sources ne fournit pas la preuve du lanceur, ignore les candidats issus de l’environnement ou de l’état persistant et peut signaler `candidate_unavailable` sur les systèmes POSIX. Sous Windows, cette première étape n’effectue aucune E/S de système de fichiers sur les chemins du candidat ou de configuration. Seul un candidat d’environnement absolu capturé par le lanceur de confiance peut recevoir une étiquette lexicale de bundle d’application ou de gestionnaire de versions ; tous les autres candidats Windows échouent de manière fermée. Comme cette étape ne consulte jamais l’état persistant, une exécution Windows pour laquelle aucun candidat d’environnement n’a été capturé signale `windows_inspection_deferred` plutôt que `candidate_unavailable` : la commande ne peut pas observer si une CLI Codex est installée, elle signale donc le report de l’inspection au lieu d’affirmer qu’aucun candidat n’existe. La commande n’exécute ni Codex ni aucun gestionnaire de paquets, ne répare aucun shim, n’écrit ni dans la configuration ni dans le cache, n’arrête aucun processus et n’installe rien. Les candidats intégrés à une application, issus d’un gestionnaire de versions reconnu, autonomes mais non vérifiés, ou associés à un état de shim ambigu sont signalés comme non gérés ou inconnus et ne sont jamais classés comme gérés.
+`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
Sous Windows, une commande simple capturée comme `CODEX_CLI_PATH=codex`, un chemin distant ou un chemin de périphérique produit plutôt `candidate_path_unavailable`. Le candidat a été capturé, mais son chemin ne convient pas à cette inspection.
diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
index bf590b687a5..918e27f2e30 100644
--- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
@@ -68,7 +68,7 @@ Il s'agit d'un réétiquetage large et destructif : chaque fil contenant un mess
### `ocx recover-history --ocx-compaction --yes`
-Réparez l'historique d'une tâche compactée par un fournisseur routé avant de la reprendre avec Codex natif. La commande sélectionne exactement une tâche par UUID, enregistre d'abord une sauvegarde privée octet par octet, puis convertit uniquement l'état de compaction `ocx1:` propre à OpenCodeX en résumé ordinaire relisible par Codex natif. Le contenu chiffré natif et les autres tâches restent inchangés. Fermez la tâche sélectionnée avant d'exécuter la commande ; toute modification simultanée du rollout interrompt la récupération sans remplacer le fichier.
+Réparez l'historique d'une tâche compactée par un fournisseur routé avant de la reprendre avec Codex natif. La commande sélectionne exactement une tâche par UUID, enregistre d'abord une sauvegarde privée octet par octet, puis convertit uniquement l'état de compaction `ocx1:`/`ocx2:` propre à OpenCodeX en résumé ordinaire relisible par Codex natif. Le contenu chiffré natif et les autres tâches restent inchangés. Fermez la tâche sélectionnée avant d'exécuter la commande ; toute modification simultanée du rollout interrompt la récupération sans remplacer le fichier.
### `ocx uninstall` · `ocx remove`
@@ -155,6 +155,8 @@ La section **OAuth reliability** indique si le stockage des identifiants est acc
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+Un redémarrage explicitement demandé a également lieu après une modification de `config.toml` validée et enregistrée, même si les octets du catalogue et du cache restent identiques. Une opération sans changement de configuration, un refus, une validation préalable ou une écriture annulée ne le déclenche pas. Sans option de redémarrage, sync ne redémarre jamais Codex automatiquement.
+
Récupère la liste active des modèles de chaque fournisseur configuré et réinjecte le catalogue fusionné dans Codex. Exécutez cette commande après l’ajout d’un fournisseur ou pour actualiser les modèles disponibles.
Avant la découverte des fournisseurs ou le remplacement du catalogue et du cache, `ocx sync` vérifie que la configuration Codex gérée peut recevoir l’injection. Si cette validation refuse la configuration, la commande renvoie un code non nul, affiche la cause précise sur stderr et laisse le catalogue ainsi que le cache existants inchangés. `ocx restore back` effectue la même vérification préalable sans écriture avant de réactiver le routage.
diff --git a/docs-site/src/content/docs/fr/reference/configuration/advisor.md b/docs-site/src/content/docs/fr/reference/configuration/advisor.md
index baf92fbb1db..9c48ca2b7fc 100644
--- a/docs-site/src/content/docs/fr/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/fr/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: Conseiller
-description: Le sidecar de consultation experte d'OpenCodex — un modèle expert configuré conseille les workers routés, avec les politiques manual et preflight.
+description: Consultation indépendante avec les politiques manual, preflight et adaptive fondée sur des preuves.
---
Le conseiller est un modèle expert indépendant qui examine la tâche du worker et renvoie des
@@ -16,6 +16,8 @@ sidecar côté proxy invisible du client — même un worker qui ne spawn jamais
## Configuration
+Sur la page **Advisor**, choisissez le modèle dans le catalogue. Le sélecteur conserve exactement les identifiants natifs, de fournisseur, de compte, personnalisés et de combo. Les modèles désactivés ou en attente de sélection initiale ne sont pas proposés. Un identifiant enregistré absent du catalogue reste visible avec un avertissement, jusqu’à son remplacement ou sa suppression. Un échec de chargement conserve la sélection et propose de réessayer. Choisissez un modèle capable de fournir des conseils textuels : le catalogue ne garantit pas la compatibilité des consultations. La CLI et l’API acceptent toujours les chaînes de modèle prises en charge par le routeur.
+
```json
{
"advisor": {
@@ -33,12 +35,13 @@ sidecar côté proxy invisible du client — même un worker qui ne spawn jamais
| `enabled?` | `boolean` | `false` | Interrupteur principal. Désactivé : aucun comportement conseiller sur le chemin de requête. |
| `model?` | `string` | — | Le modèle expert. Toute chaîne de modèle acceptée par le routeur : modèle natif seul (`gpt-6-astra`), `provider/model` explicite (`anthropic/claude-sonnet-4-6`, `xai/grok-...`) ou modèle natif qualifié par compte. Inter-fournisseurs entièrement pris en charge. |
| `effort?` | `string` | `"max"` | Intensité de raisonnement de l'appel conseiller (`low`–`ultra`). |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | Quand consulter le conseiller. |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | Quand consulter le conseiller. |
+| `maxConsultationsPerTask?` | `integer` | `3` | Budget adaptatif par tâche (1 à 3 tentatives, appels manuels compris). |
| `timeoutMs?` | `number` | `120000` | Délai de la consultation en boucle locale. |
| `contextSharingConsent?` | `"v1"` | absent | Consentement de l'opérateur pour envoyer le contexte de la tâche au fournisseur conseiller configuré. Seul `"v1"` est courant. Une valeur absente, périmée ou autre n'autorise aucun envoi. `enabled: true` n'est pas ce consentement. |
Gérez-le via la page **Advisor** du tableau de bord ou
-`ocx advisor status|on|off|set --model --effort --policy `.
+`ocx advisor status|on|off|set --model --effort --policy `.
Sans consentement courant, `ocx advisor on` n'active pas le partage inter-fournisseurs : il affiche cette divulgation et s'arrête. `ocx advisor on --ack-context-sharing` et `ocx advisor consent` enregistrent `v1`. `ocx advisor consent --revoke` retire le consentement et arrête immédiatement l'envoi. `ocx advisor set` n'accorde pas le consentement. La case du tableau de bord n'est pas précochée.
@@ -105,7 +108,14 @@ codage, et une consultation ne change jamais le modèle principal de la session.
- Les tours natifs OpenAI en passthrough (workers du pool ChatGPT) ne reçoivent pas l'outil
synthétique ; le conseiller couvre les fournisseurs routés (traduits). La consultation
preflight s'applique aux adaptateurs run-turn ; l'outil non.
-- Pas de déclencheur adaptatif : pas de détection de blocage, d'analyse d'échecs répétés, de
- niveaux d'escalade, de conseillers multiples ni de vote. `manual` et `preflight` seulement.
+- Pas de détection sémantique de blocage, de revue finale, de niveaux d’escalade, de conseillers multiples ou de vote.
- Le registre de déduplication preflight vit dans le processus ; après un redémarrage du proxy,
une tâche en cours peut recevoir une tentative preflight de plus.
+
+## Consultation adaptative et activité
+
+`adaptive` ajoute à la consultation initiale une consultation lorsque la même vérification, dans le même répertoire, échoue, qu’une modification de fichier ultérieure réussit, puis qu’une nouvelle exécution échoue encore. Une réussite efface l’échec ; une vérification démarrée avant la modification ne compte pas comme une nouvelle tentative après réparation. Les étapes initiale et réparation ont des revendications distinctes. Une identité de conversation stable est requise pour l’automatisation. Les appels/résultats doivent être appariés avec un code de sortie explicite. Scripts complexes, lots, modifications shell et sorties ambiguës ne déclenchent rien.
+
+Configurez `ocx advisor set --policy adaptive --max-consultations 2`. Les tentatives manuelles et automatiques partagent un budget de 1 à 3 par tâche, réservé avant envoi dans `OPENCODEX_HOME/advisor-budget.sqlite`. Un redémarrage ou un changement de worker ne le remet pas à zéro. Seuls les condensats, compteurs et expirations sont conservés, pendant 24 heures après la première tentative (512 tâches au maximum). Si le budget est inaccessible ou épuisé, le worker continue. Les appels envoyés puis échoués ou annulés restent réservés car ils peuvent coûter. Sans identité stable, seuls les appels manuels limités par requête restent disponibles.
+
+La page Advisor et `ocx advisor activity --json` (`GET /api/advisor/activity`) montrent jusqu’à 100 entrées de métadonnées du processus sur 24 heures ; la page visible s’actualise toutes les cinq secondes. Elles incluent motif, état, modèles, durée, tokens d’entrée/sortie signalés et efforts demandé et traduit par l’adaptateur. Aucun texte, commande, conseil ou identifiant de conversation n’est conservé ; un redémarrage efface ces entrées. La traduction décrit les paramètres envoyés et ne garantit pas le raisonnement interne. Les données absentes restent inconnues ; adoption et coût exact ne sont pas déduits.
diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md
index 442c597f0db..fd362c344a0 100644
--- a/docs-site/src/content/docs/fr/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md
@@ -95,7 +95,9 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix
| --- | --- | --- |
| `adapter` | `string` | L'un des `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (ou alias `azure`). |
| `baseUrl` | `string` | URL de base de l'API en amont. La plupart des points de terminaison fixes intégrés ignorent une valeur incompatible ; les préréglages de clés protégés contre les collisions préservent une ancienne destination personnalisée portant le même nom. |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | Cadencement facultatif du démarrage des requêtes sortantes côté client, distinct de l’utilisation, de la facturation et des indicateurs de limitation en amont. `maxConcurrentRequests` est une limite entière positive des requêtes en cours ; une règle fournisseur ou modèle peut utiliser uniquement cette limite. Les limites du fournisseur s’appliquent à tous ses modèles, tandis que les entrées `models` ciblent les identifiants exacts des modèles en amont et peuvent ajouter du délai ou réduire la concurrence. L’attente dans la file ne consomme pas le délai d’expiration des en-têtes de réponse en amont. Les requêtes HTTP et les distributions explicites `fetchResponse`/`runTurn` des adaptateurs sont couvertes. Avec une limite de concurrence, un tour Responses WebSocket canonique utilise HTTP/SSE afin de libérer la capacité à la fin, en cas d’erreur ou d’annulation du corps de réponse. Pour les adaptateurs `runTurn`, dont Cursor, la limite compte les tours actifs plutôt que les envois physiques : RunSSE et BidiAppend peuvent se chevaucher dans un même tour, tandis qu’un autre tour attend. Les envois suivants respectent toujours les intervalles de démarrage. |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | Cadencement facultatif du démarrage des requêtes sortantes côté client, distinct de l’utilisation, de la facturation et des indicateurs de limitation en amont. `maxConcurrentRequests` est une limite entière positive des requêtes en cours ; une règle fournisseur ou modèle peut utiliser uniquement cette limite. Les limites du fournisseur s’appliquent à tous ses modèles, tandis que les entrées `models` ciblent les identifiants exacts des modèles en amont et peuvent ajouter du délai ou réduire la concurrence. L’attente dans la file ne consomme pas le délai d’expiration des en-têtes de réponse en amont. Les requêtes HTTP et les distributions explicites `fetchResponse`/`runTurn` des adaptateurs sont couvertes. Avec une limite de concurrence, un tour Responses WebSocket canonique utilise HTTP/SSE afin de libérer la capacité à la fin, en cas d’erreur ou d’annulation du corps de réponse. Pour les adaptateurs `runTurn`, dont Cursor, la limite compte les tours actifs plutôt que les envois physiques : RunSSE et BidiAppend peuvent se chevaucher dans un même tour, tandis qu’un autre tour attend. Les envois suivants respectent toujours les intervalles de démarrage. |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | Sélection des clés pour les requêtes normales : `active` par défaut conserve la clé enregistrée et la rotation sur erreur ; `round-robin` répartit les allocations entre les clés disponibles sans modifier la sélection enregistrée. Cela équilibre les allocations, pas les jetons ni les coûts. |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | Limites uniformes par référence de clé : RPM, intervalle minimal et concurrence, partagées entre modèles et cumulées avec les plafonds fournisseur et modèle. Les références identiques partagent leur capacité ; plusieurs références du même projet ne garantissent pas des quotas indépendants. TPM et quotas quotidiens ne sont pas gérés. |
| `responsesPath?` | `string` | Chemin de ressource relatif pour les requêtes d'authentification par clé `openai-responses`. Il doit commencer par `/` et ne contenir aucun schéma, requête ou fragment. |
| `chatCompletionsPath?` | `string` | Chemin de ressource relatif pour les requêtes `openai-chat`, miroir de `responsesPath` et soumis aux mêmes règles de forme. Nécessaire lorsqu'un même service en amont sert Chat Completions et Responses sous des préfixes différents : un override wire par modèle change l'adaptateur sans toucher `baseUrl`, donc sans ce réglage une requête Chat activée serait envoyée vers la base Responses. L'exemple fourni est Z.AI. |
| `upstreamWebsocket?` | `boolean` | Active le transport Responses WebSocket en amont pour les requêtes `openai-responses` (désactivé par défaut). N'est honoré que pour l'amont first-party `https://api.openai.com/v1` ; les points de terminaison des fournisseurs personnalisés utilisent toujours HTTP/SSE borné, car Bun ne peut pas appliquer de limite de taille aux messages WebSocket entrants avant d'avoir alloué le message complet. Pour le fournisseur canonique ChatGPT `openai`, l'omettre conserve le WebSocket en amont sur les tours éligibles, `false` envoie les tours en streaming via HTTP/SSE, et `true` est refusé ; avec `false`, le pilotage et l'injection natifs en cours de tour sont indisponibles. Ce champ est indépendant du réglage `websockets` côté client et ne change ni le point de terminaison ni les identifiants. Une base HTTP reste en SSE ; les chemins qui ne sont pas Responses et les requêtes `openai-chat` restent en HTTP. |
@@ -119,7 +121,7 @@ sauvegarde dont le contenu diffère, puis réécrit en identifiants sans préfix
| `modelAutoCompactTokenLimits?` | `Record` | Budgets souples de compactage automatique par modèle, sous forme d'entiers sûrs positifs. Ils peuvent uniquement abaisser l'enveloppe effective de 90 % du contexte ou de l'entrée maximale et sont omis lorsqu'aucune fenêtre de contexte effective n'est déterminée. Pour le fournisseur canonique `openai`, les clés doivent être les identifiants exacts de modèles natifs pris en charge, sans préfixe de fournisseur ni de sélecteur de compte. PATCH fusionne les entrées ; `null` supprime une clé, tandis que `null` pour le champ entier efface la table. Ces marqueurs `null` sont réservés à PATCH. |
| `defaultMaxOutputTokens?` | `number` | Solution de secours `openai-chat` à l’échelle du fournisseur lorsque le client omet `max_output_tokens`. |
| `modelMaxOutputTokens?` | `Record` | Budgets de repli `openai-chat` positifs par modèle ; les correspondances exactes ou par motif priment sur la valeur par défaut du fournisseur. |
-| `modelCosts?` | `Record` | Prix affichés par modèle (USD par 1M de jetons), indexés par l'identifiant exact du modèle en amont de ce fournisseur — et non par un identifiant de fournisseur ni par une étiquette routée `provider/model`, par exemple `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Tout identifiant de modèle constitue une clé valide : les fournisseurs personnalisés peuvent cibler n'importe quel point de terminaison compatible avec OpenAI au moyen de l'adaptateur `openai-chat`, et les identifiants de fournisseur locaux ou internes fonctionnent même s'ils sont absents des catalogues intégrés. Les prix configurés par l'utilisateur priment sur les catalogues intégrés dans les estimations des pages Journaux (`~$`) et Utilisation. Les entrées historiques sont recalculées à partir de la surcharge actuelle ; modifier un prix peut donc changer les totaux antérieurs. L'ordre de repli est le suivant : `modelCosts` défini par l'utilisateur → catalogue jawcode → surcharge des prix attendus → repli propre au fournisseur au niveau du modèle. Une surcharge utilisateur explicitement définie à zéro produit une estimation nulle connue ; supprimez cette entrée pour rétablir la tarification automatique. Les prix de catalogue entièrement nuls restent soumis au repli. Chaque tarif doit être un nombre fini positif ou nul, inférieur ou égal à 1 000 000 (USD par 1M de jetons) ; les lignes hors plage sont rejetées par l'interface de gestion et ignorées au chargement. Ces valeurs servent uniquement à l'estimation lors de l'affichage : les surcharges n'affectent jamais le routage, la sélection des comptes, les quotas ni la facturation. |
+| `modelCosts?` | `Record` | Prix affichés par modèle (USD par 1M de jetons), indexés par l'identifiant exact du modèle en amont de ce fournisseur — et non par un identifiant de fournisseur ni par une étiquette routée `provider/model`, par exemple `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Tout identifiant de modèle constitue une clé valide : les fournisseurs personnalisés peuvent cibler n'importe quel point de terminaison compatible avec OpenAI au moyen de l'adaptateur `openai-chat`, et les identifiants de fournisseur locaux ou internes fonctionnent même s'ils sont absents des catalogues intégrés. Les prix configurés par l'utilisateur priment sur les catalogues intégrés dans les estimations des pages Journaux (`~$`) et Utilisation. Les entrées historiques sont recalculées à partir de la surcharge actuelle ; modifier un prix peut donc changer les totaux antérieurs. L'ordre de repli est le suivant : `modelCosts` défini par l'utilisateur → catalogue jawcode → surcharge des prix attendus → repli propre au fournisseur au niveau du modèle. Une surcharge utilisateur explicitement définie à zéro produit une estimation nulle connue ; supprimez cette entrée pour rétablir la tarification automatique. Les prix de catalogue entièrement nuls restent soumis au repli. Chaque tarif doit être un nombre fini positif ou nul, inférieur ou égal à 1 000 000 (USD par 1M de jetons) ; les lignes hors plage sont rejetées par l'interface de gestion et ignorées au chargement. Ces valeurs servent uniquement à l'estimation lors de l'affichage : les surcharges n'affectent jamais le routage, la sélection des comptes, les quotas ni la facturation. |
| `headers?` | `Record` | En-têtes supplémentaires en amont. L'autorisation, les cookies, les en-têtes de clé API, les nouvelles lignes intégrées et les noms invalides sont rejetés. |
| `openRouterRouting?` | `OpenRouterProviderRouting` | Préférences OpenRouter `order`, `only` et `allowFallbacks` par défaut ; valable uniquement pour les OpenRouter canoniques avec `openai-chat`. |
| `modelOpenRouterRouting?` | `Record` | Remplacements exacts de l'ID de modèle qui remplacent la préférence OpenRouter à l'échelle du fournisseur. |
@@ -631,3 +633,8 @@ le nom actuel avant d'effectuer une autre modification.
### `anthropicAccountPool.routes`
Les routes `anthropicAccountPool.routes` associent un modèle à des identifiants de comptes OAuth Anthropic enregistrés. La première règle correspondante (`match`, glob sensible à la casse) limite la sélection initiale et les reprises 429 lorsque le pool est activé. `fallback: true` élargit la sélection seulement si aucun compte de la règle n’est éligible.
+
+
+### `promptLengthPricing`
+
+Le champ facultatif `promptLengthPricing` complète les quatre tarifs de base. `mode: "automatic"` (ou champ absent) conserve les règles de contexte du catalogue ; `mode: "flat"` les désactive. Avec `mode: "custom"`, `thresholdTokens` est un entier positif sûr, `comparison` vaut `"gt"` (>) ou `"gte"` (≥), et `rates` contient les quatre tarifs alternatifs absolus. Le seuil compte tous les jetons du prompt, y compris les lectures et écritures du cache, sans la sortie. Les tarifs sélectionnés couvrent toute la requête, sortie comprise, et remplacent la règle de contexte du catalogue des deux côtés du seuil. Les ajustements Fast/Priority s'appliquent une seule fois après la sélection des tarifs et suivent la relation Priority publiée dans le catalogue pour ce modèle ; l'estimation n'est indiquée comme borne inférieure que lorsque le catalogue déclare la relation `lower-bound` (actuellement xAI Grok) ; un modèle sans règle de catalogue applique le multiplicateur Priority une seule fois. Configurez ces options dans **Models → Price**. **Reset to automatic** supprime tous les tarifs et la règle personnalisés. Les anciennes configurations restent compatibles ; ces paramètres concernent les estimations affichées.
diff --git a/docs-site/src/content/docs/fr/reference/management-api.md b/docs-site/src/content/docs/fr/reference/management-api.md
index fb6a70a30aa..b41651ea12a 100644
--- a/docs-site/src/content/docs/fr/reference/management-api.md
+++ b/docs-site/src/content/docs/fr/reference/management-api.md
@@ -353,7 +353,7 @@ lui-même s'il souhaite ajouter une étoile au dépôt.
| `POST /api/system/restart` | Amorcer un redémarrage du processus qui attend l'évacuation des requêtes, sans retirer l'injection du client | Renvoie 202 ; les appels répétés signalent l'évacuation déjà en cours |
| `POST /api/stop` | Arrêter le service, restaurer Codex en mode natif, retirer l'injection Grok gérée et évacuer les requêtes du proxy | 409 conflit de propriété du service; 409 `respawnable_service` lorsqu'un wrapper du Planificateur de tâches Windows pourrait relancer le proxy et que l'appelant n'est pas `ocx stop` (rien n'est modifié) ; 409 lorsque le gestionnaire installé refuse de s'arrêter ; 409 `service_state_unknown` lorsque l'état du Planificateur de tâches ne peut pas être lu (rien n'est modifié ; réparez la requête puis réessayez) |
| `GET /api/system/codex-app-server` | Indiquer si les serveurs d'application Codex en cours d'exécution sont antérieurs au catalogue de modèles actuel | — |
-| `POST /api/system/codex-restart` | Actualiser le catalogue, puis redémarrer les serveurs d'application Codex obsolètes et quitter puis relancer entièrement l'application Codex Desktop afin que le sélecteur de modèles se recharge. Lorsque le proxy lui-même s'exécute dans l'application Codex, le redémarrage Desktop est refusé plutôt que transféré. | Renvoie 200 avec `code: partially_stopped` lorsqu'une cible ne s'arrête pas |
+| `POST /api/system/codex-restart` | Actualiser le catalogue, puis redémarrer les serveurs d'application Codex. L'application Codex Desktop n'est quittée puis relancée entièrement si elle a démarré avant l'écriture du catalogue actuel, si cette requête change la liaison `model_catalog_json` (même si le processus est plus récent que le fichier du catalogue), ou si l'état de son sélecteur ne peut pas être déterminé, afin que le sélecteur de modèles se recharge ; une application en cours avec un sélecteur à jour reste ouverte avec `desktopApp.reason: "picker_current"`. Une application fermée renvoie `desktopApp.reason: "no_targets"` ; son `pickerState` peut être `not_running`, ou `stale` si cette requête a changé la liaison au catalogue. `desktopApp.pickerState` indique la lecture qui a décidé (`current`, `stale`, `unknown` ou `not_running`). Lorsque le proxy lui-même s'exécute dans l'application Codex, le redémarrage Desktop est refusé plutôt que transféré. | Renvoie 200 avec `code: partially_stopped` lorsqu'une cible ne s'arrête pas |
### Délégation de l'authentification Codex
diff --git a/docs-site/src/content/docs/fr/reference/proxy-formats.md b/docs-site/src/content/docs/fr/reference/proxy-formats.md
index 5535c90ade6..ae904510a0e 100644
--- a/docs-site/src/content/docs/fr/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/fr/reference/proxy-formats.md
@@ -41,6 +41,8 @@ Les points de terminaison Chat Completions et Messages honorent les catalogues d
Par ailleurs, sur chaque voie entrante, un appel dans l’espace de noms `default.` d’un fournisseur est normalisé vers le nom d’outil nu déclaré par la requête (`default.Read` → `Read`).
+Messages natif reconnaît aussi les définitions d’outils en ligne dans les messages système avec `tools: []`. Ces noms restent appelables jusqu’à leur retrait permanent, sans autoriser d’autres outils clients. Les appels MCP hébergés et les flux d’arguments des outils serveur conservent leur format natif.
+
## Présentation du point de terminaison
| Espace client | Point de terminaison | Résultat non-stream réussi | Résultat de flux ou de socket réussi |
diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md
index 99ff1a1d5d7..e8db9304b51 100644
--- a/docs-site/src/content/docs/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` walks you through:
-1. **Pick a provider** — choose one of the 104 built-in registry presets or `custom` to type a base
+1. **Pick a provider** — choose one of the 105 built-in registry presets or `custom` to type a base
URL and adapter.
2. **API key** — paste a key, or reference an environment variable like `${ANTHROPIC_API_KEY}`.
3. **Default model** — for key, local, and custom providers, accept the preset or enter a model id.
@@ -146,6 +146,14 @@ ocx restore # restore native Codex without stopping (alias: ocx eject)
ocx restore back # route Codex through the still-running proxy again
```
+## Startup ownership could not be verified
+
+If startup reports `Shared-client owner could not be verified` and `refusing startup`,
+OpenCodex could not safely decide whether another proxy owns the shared client settings.
+It stops before publishing a new runtime or changing those settings. Retry startup after
+local connectivity recovers; an installed background service retries through its service manager.
+A timeout does not permanently turn the new process into a secondary instance.
+
## Next
- [How It Works](/getting-started/how-it-works/) — what happens to each request.
diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md
index f6a457ebcec..159886a85e0 100644
--- a/docs-site/src/content/docs/guides/claude-code.md
+++ b/docs-site/src/content/docs/guides/claude-code.md
@@ -271,6 +271,15 @@ not advertise an authoritative per-provider ladder, and the selected provider's
normal effort normalization still applies. This remains a client-version-specific
compatibility mechanism.
+Gateway apply refuses unreadable or non-regular configuration files, including dangling metadata
+links, before creating a new profile. If the metadata update fails, apply restores the profile's
+previous contents, or removes a profile it had just created, but only while the profile still holds
+what apply wrote; if another program changed it in the meantime, apply keeps that version and
+reports "profile rollback skipped". Apply rechecks the profile before
+backup and replacement; if it detects a concurrent edit, it refuses with `desktop_profile_changed`.
+Review the edit and reapply. If restoring the profile also fails after a metadata error, the
+reported reason includes both failures; reconcile the profile and metadata before retrying.
+
### First-party (opt-in)
:::caution[Account risk]
@@ -306,10 +315,15 @@ entries, is kept.
Claude Desktop first-party routes its Code tab and subagents through OpenCodex. The standalone Claude Code CLI has a separate first-party switch. Both clients read the same `~/.claude/settings.json` proxy and CA settings: if only one switch is on, the other client still transits the local proxy, where TLS terminates, but its Messages requests relay to Anthropic unchanged. Other Anthropic paths relay unchanged and unrelated hosts remain blind tunnels. With the CLI switch on, the standalone `claude` also lists routed models in `/model`; see [Model picker in the CLI](#model-picker-in-the-cli).
:::note[Windows system proxy (Clash, v2rayN, corporate proxies)]
-When a Windows system proxy is on, Claude Desktop hands it to the Code tab as `HTTPS_PROXY`, and
+When picker intent is off at OpenCodex startup, its Windows relay stays unbound. Turning picker
+mode on later starts it within the running service; OpenCodex does not need a restart.
+For picker-off installs or pending installs with no applied picker profile, a Windows system proxy
+is handed to the Code tab as `HTTPS_PROXY`, and
that value takes precedence over the OpenCodex proxy in `~/.claude/settings.json`. The Code tab
then goes around OpenCodex and routed models fail there, while the standalone CLI keeps working.
-Add `api.anthropic.com` to your proxy client's system-proxy bypass list (in Clash Verge,
+An applied selected picker profile uses Desktop's fixed egress proxy while it is available, even when
+missing trust leaves the picker pending and `claude.ai` relays blindly. System-proxy bypass advice
+applies when there is no selected available picker profile. Add `api.anthropic.com` to your proxy client's system-proxy bypass list (in Clash Verge,
`system_proxy_bypass`), then fully quit and reopen Claude Desktop. `ocx doctor` reports this
under "Claude Desktop first-party vs Windows system proxy". It cannot evaluate a PAC script or
automatic proxy detection (WPAD, "Automatically detect settings"), so it reports those as unknown;
@@ -340,17 +354,17 @@ hub, so `ocx claude desktop apply` there uses the gateway profile.
### Picker mode: opencodex models in the first-party Code-tab picker
-Picker mode is part of first-party mode. On macOS it is on by default when first-party is selected,
+Picker mode is part of first-party mode. On macOS and Windows it is on by default when first-party is selected,
unless `claudeCode.intercept.picker: false` is set. It changes the first-party Desktop Code-tab picker
so it lists available opencodex models by name. The first time it is enabled, macOS may ask you to
-trust a local certificate authority in the login keychain. That authority is constrained to `claude.ai`
+trust a local certificate authority in the login keychain, and Windows may ask you to trust it in the current-user Root store. That authority is constrained to `claude.ai`
and its subdomains. OpenCodex protects its exportable signing identity in the OS credential store
and reuses the same validated certificate and key across normal restarts. No plaintext picker signing
key is stored in the OpenCodex config directory. The full constrained CA validation and OS trust
-verification still apply. With the same approved identity and an available credential store, restarting
-OpenCodex does not add or remove Certificate Trust Settings. Startup restore never installs trust:
+verification still apply, including the exact fingerprint with a verified leaf on Windows. With the same approved identity and an available credential store, restarting
+OpenCodex does not add or remove macOS Certificate Trust Settings. Restart and status never install trust:
if trust is missing, revoked or unknown, the picker stays pending. Run `ocx claude desktop picker on`
-or `ocx claude desktop picker trust` explicitly to grant trust.
+or `ocx claude desktop picker trust` explicitly to grant trust, including the Windows confirmation when needed.
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.
@@ -358,9 +372,9 @@ One-time migration from an older picker identity may require consent to remove i
If cleanup cannot finish, the picker stays unavailable and the applied profile uses a blind relay
until cleanup succeeds; `ocx claude desktop picker status` reports that state. macOS may separately
ask you to unlock the keychain or approve an application's access to stored credentials. These native
-access prompts can still occur on restart or upgrade.
+access prompts can still occur on restart or upgrade. On Windows, explicit `on` or `trust` may show a confirmation to add the root.
-Picker mode remains unsupported on Windows and Linux: OpenCodex starts no picker CA, credential-store
+Picker mode remains unsupported on Linux: OpenCodex starts no picker CA, credential-store
or proxy work there. The main Claude intercept remains available with ownership, symlink, file-permission
and Windows ACL checks protecting its local CA files.
@@ -418,7 +432,7 @@ next request; Desktop does not need a restart.
Turn on the CLI switch in Connect → Claude, or run `ocx claude config set --first-party on`; use `off` to disable it. The switch is immediate and refuses `{enabled:false, cliFirstParty:true}` before any field is saved; it may also refuse to turn on if the local intercept is unavailable, the CA cannot be prepared, settings cannot be read, or a foreign proxy setting owns the keys. Off persists even when the intercept is unavailable; disabling Claude routing alone leaves an owned settings env untouched. For fully native terminal traffic with only Desktop first-party on, set `NO_PROXY='*'` in the shell. This still carries the first-party account risk stated above.
Disabling Claude routing leaves the owned settings env untouched. While the bound listener still runs, every Messages request relays unchanged; after it stops, plain `claude` cannot connect until OpenCodex runs or Desktop/CLI first-party is turned off. Native `ocx claude` sets `NO_PROXY=*` only for an owned env without a foreign inherited `HTTPS_PROXY`/`https_proxy`. With a foreign proxy it preserves that value and warns that the settings-owned intercept still applies; turn Desktop/CLI first-party off or unset the setting.
-The UI distinguishes uncertainty about whether settings still point at its proxy (unknown), a token-bearing opencodex proxy with a foreign CA (foreign: fix HTTPS_PROXY / NODE_EXTRA_CA_CERTS manually), and a tokenless loopback proxy beside a foreign CA (local: ownership is unconfirmed; remove HTTPS_PROXY if unused). With matching applied settings and a bound listener but Claude routing off, disabled means requests relay unchanged while it is bound; turn first-party off to remove settings. An owned URL with no listener is stopped; a bound listener with an owned CA but mismatched port or token is broken even when routing is off. With an intent on, stopped or broken plus ineligible interception displays routingOff: Claude routing or the intercept is off, or this machine is a client of another opencodex hub; enable interception on this machine or turn first-party off to remove the settings. When interception is stopped, use **Start interception** or `ocx claude intercept start` to retry within the running OpenCodex service. Saving first-party settings also starts it automatically. A refusal reports disabled routing, client role, an ephemeral public port, an occupied port, or a startup failure. For an occupied port, free it or set `claudeCode.intercept.port`. If a pair is already bound on a different port, the response names both ports; use the bound port or restore the configured value. CLI intent with no proxy is not applied; one intent with a live proxy gets the shared-relay notice; any remaining proxy with neither intent is residual, unless unknown, foreign, or local takes precedence.
+The UI distinguishes uncertainty about whether settings still point at its proxy (unknown), a token-bearing opencodex proxy with a foreign CA (foreign: fix HTTPS_PROXY / NODE_EXTRA_CA_CERTS manually), and a tokenless loopback proxy beside a foreign CA (local: ownership is unconfirmed; remove HTTPS_PROXY if unused). With matching applied settings and a bound listener but Claude routing off, disabled means requests relay unchanged while it is bound; turn first-party off to remove settings. An owned URL with no listener is stopped; a bound listener with an owned CA but mismatched port or token is broken even when routing is off. With an intent on, stopped or broken plus ineligible interception displays routingOff: Claude routing or the intercept is off, or this machine is a client of another opencodex hub; enable interception on this machine or turn first-party off to remove the settings. When interception is stopped, use **Start interception** or `ocx claude intercept start` to retry within the running OpenCodex service. Saving first-party settings also starts it automatically. A refusal reports disabled routing, client role, an ephemeral public port, an occupied port, or a startup failure. For an occupied port, free it or set `claudeCode.intercept.port`. Startup refreshes an existing managed Claude proxy environment only after the intercept proxy binds successfully, using its bound port; an occupied port leaves Claude settings unchanged. If a pair is already bound on a different port, the response names both ports; use the bound port or restore the configured value. CLI intent with no proxy is not applied; one intent with a live proxy gets the shared-relay notice; any remaining proxy with neither intent is residual, unless unknown, foreign, or local takes precedence.
- Gateway discovery (`/model` → "From gateway") is not used, because Claude Code only queries
`GET /v1/models` on a configured gateway. Routed models reach the picker another way; see
@@ -948,7 +962,7 @@ The proxy translates every Anthropic Messages API request into the Codex Respons
| User `tool_result` | `function_call_output` (`is_error` → `[tool error]` prefix) |
| `tool_reference` in a tool result | Text `Tool loaded: ` in the paired result; preserves loaded-tool names without adding declarations or enabling translated server-side deferral |
| `thinking` / `redacted_thinking` replay | `reasoning` items with bounded `ocxr1` envelopes for signatures and redacted payloads |
-| Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) |
+| Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`). On a route to `api.anthropic.com`, the typed `web_search_*` and `web_fetch_*` declarations are also sent to Anthropic as declared and run server-side, unless the web-search sidecar supplies `web_search` itself. Each search or fetch comes back to the client as its `server_tool_use` block and its `web_search_tool_result` or `web_fetch_tool_result` |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named function→`{type:"function",name}`, hosted WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md
index cd39beb0418..70fc8ccf02d 100644
--- a/docs-site/src/content/docs/guides/codex-app-models.md
+++ b/docs-site/src/content/docs/guides/codex-app-models.md
@@ -341,10 +341,18 @@ Desktop if it still has the old list, select the routed model, then choose **Ult
in its speed menu. Standard remains the default.
Only an eligible `service-tier` wire with an explicit `ultrafast → ultrafast` mapping adds
-this choice. Ordinary Fast support, a GPT-like name, native/account rows and combo aliases
-do not enable it. Removing the mapping or disabling that model's service-tier support
-removes the generated choice from both the catalog and cache on the next successful sync.
-The separate `ultraFastTier` setting still only retains hand-supplied catalog tiers.
+this choice to routed rows; ordinary Fast support or a GPT-like name is insufficient.
+That routed mapping does not apply to native/account rows or combo aliases. Removing the
+mapping or disabling that model's service-tier support removes the generated routed choice
+from both the catalog and cache on the next successful sync.
+The active declaration also controls the Ultra Fast choice when Codex loads its model list over HTTP.
+
+For routed rows, `ultraFastTier` retains independently supplied Ultra Fast tiers from the
+same exact persisted row; generated routed tiers are not treated as operator-supplied.
+For bare native `gpt-6-astra` / `gpt-6.1-sol` rows and their trusted account-bound clones,
+enabling `ultraFastTier` also generates a missing Ultrafast choice without changing the
+native default tier. Disabling it withdraws only native tier metadata tracked as generated;
+independently supplied or edited native tier descriptors are preserved.
[OpenAI's API guide](https://developers.openai.com/api/docs/guides/ultrafast-mode) documents
Ultrafast for GPT-6 Astra and GPT-6.1 Sol. API access and
diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md
index acf3da827d7..d4f611149c4 100644
--- a/docs-site/src/content/docs/guides/codex-integration.md
+++ b/docs-site/src/content/docs/guides/codex-integration.md
@@ -464,10 +464,10 @@ other providers keep their defaults.
Authenticated loopback routing normally keeps Codex on its built-in `openai` provider identity.
That preserves native thread identity, but it also makes Codex request native remote compaction.
When a routed provider cannot return a native compaction blob, OpenCodeX stores the summary in its
-own `ocx1:` envelope. Native ChatGPT cannot verify that envelope if OpenCodeX is later removed from
+own `ocx1:`/`ocx2:` envelope. Native ChatGPT cannot verify that envelope if OpenCodeX is later removed from
the request path.
-On an authenticated loopback route, enable client-side compaction to keep V2 sub-agent routing while preventing new `ocx1:` compaction summaries. Non-loopback and API-key routes retain their existing provider and authentication behavior:
+On an authenticated loopback route, enable client-side compaction to keep V2 sub-agent routing while preventing new `ocx1:`/`ocx2:` compaction summaries. Non-loopback and API-key routes retain their existing provider and authentication behavior:
```bash
ocx system settings --client-compaction on # or "codexClientCompaction": true in config.json
@@ -495,7 +495,7 @@ selected provider. V2 sub-agent requests keep their existing provider selection
accounting. Client-side compaction does not change plaintext delivery, encrypted task passthrough
through `allowEncryptedV2AgentTasks`, or configured recovery and fallback behavior.
-This preference affects future compactions only, and it rewrites no existing `ocx1:` payload in
+This preference affects future compactions only, and it rewrites no existing `ocx1:`/`ocx2:` payload in
any configuration, so use the explicit history recovery workflow for a thread that needs one.
Whether resume-history metadata is re-tagged depends on which form the injection takes. On its
@@ -1088,6 +1088,13 @@ an app updater can omit the managed connection argument. Save drafts, close Code
After a Codex update, an unassessed app version refuses correction until its compatibility is
reviewed; reinstalling the certificate does not make an unassessed build supported.
+The panel can also report the last completed build check as diagnostic metadata: the package
+version this build was assessed against, the package version observed at that check, whether
+they matched, and when the check finished. It is a past observation, not a live reading of the
+installed package. After the service stops, the app updates, or a check is still running or was
+interrupted, it can lag the actual installation; only a completed probe refreshes it, and a
+`verified` result describes the package observed at that check, not whatever is installed now.
+
On Windows, an explicit OpenCodex full-app restart preserves an already active loopback
compatibility PAC argument only when the current OpenCodex process owns the serving compatibility
runtime. A stale or foreign endpoint is refused before stopping Codex; replacement of the runtime
@@ -1614,3 +1621,50 @@ The shared SSE decoder accepts LF, CRLF and standalone CR line endings, even whe
Choosing an account-qualified model does not enable credit spending. Stored accounts obey **Use credits after limit** during authentication, when credentials are prepared, and before Responses HTTP or WebSocket dispatch after pacing or retry waits. A held request reports the credit policy, not an authentication failure; wait for the reset, choose another account, or explicitly enable that account’s credit spending.
Stored-account vision and web-search helpers also recheck this policy before sending, including retries. If consent changes or a limit is reached after helper selection, the helper reports the policy refusal without sending that attempt. The standalone search relay returns a reset-bound 429. Caller-owned Direct credentials retain their existing behavior.
+
+## Opt in a custom provider to managed local routing
+
+A custom provider such as `chatgpt-http` may use `supports_websockets = false`
+to keep HTTP transport. OpenCodex leaves custom providers untouched by default.
+To delegate only that provider's URL to OpenCodex, run:
+
+```sh
+ocx codex-provider allow chatgpt-http
+ocx start
+```
+
+The first command explains the scope and asks for confirmation, defaulting to No.
+For an explicitly approved scripted operation, use `allow chatgpt-http --yes`.
+Consent applies to this provider ID in the selected `CODEX_HOME`; it does not
+approve other providers or other homes. Start and sync update only its `base_url`
+to the local proxy. Its identity, WebSocket setting, other TOML fields and history
+remain unchanged. Keep the proxy running while using this route.
+Turning the Codex integration OFF also prevents approved-provider URL updates on
+start or sync; the saved provider consent remains available when you turn it back on.
+
+This mode supports explicit Responses provider tables with `requires_openai_auth = true`
+and no custom credentials or headers. Non-loopback targets, authless Desktop mode
+and client compaction require the existing dedicated integration instead.
+
+`ocx stop` and `ocx restore` restore recorded URLs while keeping consent for a later
+start. Run `ocx codex-provider status` to list approvals, or
+`ocx codex-provider revoke chatgpt-http` to restore its managed URL and remove consent.
+A URL changed outside OpenCodex is preserved and requires revoke followed by a new
+approval; OpenCodex does not silently adopt the replacement as a new baseline.
+The original URL assignment is retained in private recovery metadata under
+`OPENCODEX_HOME`, separately from the native config journal.
+
+Startup safety recognizes an approved provider's recorded managed URL as OpenCodex routing.
+This does not install restart protection: a manually started proxy still needs a viable
+background service to recover automatically after a restart.
+
+## Native Desktop relay CONNECT validation
+
+The Desktop relay's existing HTTP/HTTPS CONNECT transport now rejects malformed
+proxy response headers and ambiguous HTTP Basic usernames containing colons.
+Decoded credential pairs are limited to 4096 UTF-8 bytes. If your selected proxy
+is rejected, correct its configuration or response; the relay does not silently
+switch to a direct connection.
+
+Proxy selection, SOCKS5/SOCKS5h support, certificate verification, and connection
+lifecycle are unchanged. Both SOCKS spellings retain remote target DNS resolution.
diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md
index 60e3db08e6b..4fa8724c5c6 100644
--- a/docs-site/src/content/docs/guides/combos.md
+++ b/docs-site/src/content/docs/guides/combos.md
@@ -15,6 +15,17 @@ This is useful when you want either:
Combos sit in front of normal provider routing. Read [Model Routing](/guides/model-routing/) first
if `provider/model` selectors are new to you.
+## Metered fallback consent
+
+Mark a pay-per-use combo target with `"metered": true` when it follows subscription targets. Omitted or false consent denies metered dispatch. Choose consent for your deployment:
+
+- **Local Claude Code or Codex:** on a loopback-only proxy, set `"allowMeteredTargets": true` on the combo, or run `ocx combo set --allow-metered-targets on`. This explicitly permits local callers to use its marked targets. The default is false; `off` clears consent. Loopback authentication does not read API keys.
+- **Network or linked callers:** the caller's configured `apiKeys[]` entry must have `"allowMeteredComboTargets": true`. A proxy with a non-loopback listener ignores the combo flag, including on its secondary loopback listener. Linked callers also retain their configured-key consent requirement when the primary listener is loopback. The environment token never qualifies.
+
+Request headers cannot grant consent. Model/provider scopes still apply. This covers initial selection, failover and recovery across combo strategies, compaction, Messages, Chat Completions and WebSocket requests, and a subagent model fallback naming a combo alias. A collapsed fallback tries a usable unmetered target before refusing. Unmarked targets retain existing behavior; metering is declared by the operator rather than detected automatically.
+
+For example, a failover chain can target Anthropic OAuth, then Cursor, then `{ "provider": "anthropic-apikey", "model": "claude-opus-5-5", "metered": true }`. Without consent, exhaustion returns the last unmetered error, with one exception: an earlier 429 or 402 quota refusal takes precedence over a later 400, 401 or 403. If the subscription target returns 429 and Cursor then refuses with 401 or 403, the caller receives the subscription's 429, because the spent subscription explains the failure and Cursor's credential refusal does not. Dashboard saves retain omitted target metering and combo consent; explicitly setting either to false clears it. These consent fields are configured through the config file or API; the combo flag is also available through the CLI, while the dashboard does not expose it.
+
## 60-second quickstart
This example creates `combo/main` with Anthropic first and OpenAI second. Both providers must
@@ -540,7 +551,7 @@ Combo failures are divided into **hop** failures and **terminal** failures.
| Classified authentication, subscription, quota, rate-limit, overload, or upstream-server error | Cool the target and hop, even when the status alone is not sufficient. |
| Client cancellation (499), `origin_rejected`, cyber-policy refusal, context overflow, or other invalid request | Stop and return the error; another target would not make the request valid. |
| Exact HTTP 400 saying a model is not supported when using Codex with a ChatGPT account | Try the next declared target before output commitment. The message must be the whole refusal in `detail`, `error.message`, or bare text; competing `detail` and `error` envelopes stay terminal. This does not quarantine the account or cool every model on the provider. |
-| Structured HTTP 400 rejecting optional `user`, an unsupported reasoning effort, or model-scoped image input | Hop before output commitment without cooling; see request-local target compatibility below. |
+| Structured HTTP 400 rejecting optional `user`, an unsupported reasoning effort, model-scoped image input, or input media the route cannot read (`unsupported_input_modality`) | Hop before output commitment without cooling; see request-local target compatibility below. |
| First tool call of a Responses turn run by an in-process adapter (`runTurn`) that the current request did not declare, before any output or replay-unsafe side effect | Cool the target and hop with the same tool catalog. After visible output or a replay-unsafe side effect the refusal is final. Chat Completions and Anthropic Messages requests are unchanged. |
| Any other unclassified error | Stop and return the error. |
@@ -650,7 +661,7 @@ instead of growing memory without a bound.
When routing Claude Code to the canonical ChatGPT Codex backend, OpenCodex removes the unsupported top-level `user` metadata field without changing the session/cache key, input messages, tool schemas, or safety identifiers. Public Responses API and noncanonical forward gateways keep that field.
-A combo can also advance after an intact HTTP 400 `invalid_request_error` that specifically rejects `user`, reports `unsupported_value` for `reasoning.effort`/`reasoning_effort`, or reports `param: input` with an exact model-scoped `does not support image inputs` rejection. This is a mismatch for that request, not evidence that the target is unhealthy, so it records no cooldown. This compatibility recovery does not silently change `none` into a different effort or broaden this exception to arbitrary invalid requests. Policy refusals, cancellation and already-committed output remain non-replayable. A single-target request still returns an unresolved upstream rejection.
+A combo can also advance after an intact HTTP 400 `invalid_request_error` that specifically rejects `user`, reports `unsupported_value` for `reasoning.effort`/`reasoning_effort`, or reports `param: input` with an exact model-scoped `does not support image inputs` rejection. A route that refuses the request's media with code `unsupported_input_modality`, such as image input sent to a CLI provider route without a verified multimodal contract, advances the same way. This is a mismatch for that request, not evidence that the target is unhealthy, so it records no cooldown. This compatibility recovery does not silently change `none` into a different effort or broaden this exception to arbitrary invalid requests. Policy refusals, cancellation and already-committed output remain non-replayable. A single-target request still returns an unresolved upstream rejection.
## Default reasoning effort
@@ -702,6 +713,25 @@ even when every target supports images — the catalog drops `image` from `input
image-bearing requests are rejected with HTTP 400 before any target is called. `"auto"` (or
omitting the field) keeps the automatic intersection.
+In the dashboard, the **Image / multimodal** switch can be enabled whenever every target has
+known image or text input capabilities. Native image members stay unchanged; text-only members are
+automatically declared text-only on save (`modelCapabilities[model].inputModalities = ["text"]`)
+so the [Vision Sidecar](/guides/sidecars/) describes their images. The switch's hint names the
+members that will be enrolled. A member whose modalities are unknown or have no text input (for
+example an audio-only model) cannot be covered by the sidecar; it keeps the switch unavailable and
+is named in the hint. If the [Vision Sidecar](/guides/sidecars/) is disabled globally, enabling the
+switch shows a warning linking to the dashboard: enrollment still saves, but images are replaced
+with an omission note and requests continue without image descriptions until the sidecar is
+re-enabled. Turning the switch off disables image input for the combo
+but keeps those provider declarations, and `PUT /api/combos` accepts the same enrollment as an
+optional top-level `visionSidecarTargets` array of exact `{ provider, model }` targets
+(request-only; it is never stored on the combo and is rejected while `imageInput` is
+`"disabled"` when the enrollment list is non-empty). Entries that already accept image input or
+are declared without text are rejected with HTTP 400 rather than overwritten, entries the sidecar
+already covers are skipped, and a save refused before publication restores the combo and every declaration together.
+Removing a member from the combo does not remove its text-only declaration; clear it in the
+provider editor if you no longer want it.
+
## Encrypted v2 sub-agent tasks
There is one important limitation for Codex v2 sub-agents ([issue #92](https://github.com/lidge-jun/opencodex/issues/92)).
@@ -739,8 +769,9 @@ task workflow.
### Dashboard
Open the local dashboard and choose **Models → Combos**. The workspace creates, edits, renames, and removes
-combos, and its target picker excludes disabled models, nested combos, and the credential-only JEV
-provider. **Create JEV Auto** opens the same Combo editor with an editable decision target template;
+combos. Its model selectors include hidden models: catalog visibility only controls external model
+lists, not internal Combo selection, and selecting a hidden model does not make it visible externally.
+The target picker still excludes disabled providers, nested combos, and the credential-only JEV provider. **Create JEV Auto** opens the same Combo editor with an editable decision target template;
an existing `jev-auto` id or alias is reported instead of creating a duplicate. A JEV Combo also shows
**Decision service** (TypeSafe JEV or a configured `jev-decision` provider) and **Decision timeout
(ms)**, and the Combos overview lists each JEV Combo's decision service, endpoint, and timeout.
@@ -761,7 +792,7 @@ ocx combo remove --yes
```
`set` also accepts `--strategy`, `--sticky`, `--effort`, `--alias`, `--native-alias`,
-`--display-name`, `--decision-provider`, `--decision-timeout`, and `--rename-from`. Use `-` as the
+`--allow-metered-targets on|off`, `--display-name`, `--decision-provider`, `--decision-timeout`, and `--rename-from`. Use `-` as the
value of `--effort`, `--alias`, `--display-name`, `--decision-provider`, or `--decision-timeout` to
clear that field. The two decision flags apply only to `--strategy jev`. `--native-alias` requires a currently supported bare native
model alias and a non-empty display name. `create` and `update` are aliases for `set`; `delete` is an alias for
@@ -843,7 +874,10 @@ running opencodex instance that receives model requests.
Every target is currently ineligible: for example, its provider is disabled, it is cooling down,
it has already been attempted for this request, or an encrypted v2 task excludes it. Check target
-provider state and recent upstream errors. For cooldowns, follow an observed `Retry-After` value first;
+provider state and recent upstream errors. When no target was attempted, the error message lists
+why each target was skipped, for example
+`No available targets for combo: fast (openai/gpt-6: quota exhausted; anthropic/claude-opus-5-5: cooldown 42s)`.
+For cooldowns, follow an observed `Retry-After` value first;
Codex reset headers also take precedence over `cooldownMs`.
If neither upstream signal is usable, the configured `cooldownMs` applies, or the upstream fallback applies
when it is unset (5 seconds for request-rate codes `1302`/`1305`, 10 minutes for a spent usage
diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md
index 4ecad314138..a6c94f31389 100644
--- a/docs-site/src/content/docs/guides/desktop-app.md
+++ b/docs-site/src/content/docs/guides/desktop-app.md
@@ -116,8 +116,12 @@ means the registration could not be read. While the app runs the proxy, the Star
background service installer disabled; to use it instead, quit OpenCodex and run
`ocx service install` in a terminal. The Codex launcher shim can still be installed there.
When a Codex launch finds no proxy, the shim waits for OpenCodex to restart it, opens
-OpenCodex on macOS if it quit, and otherwise reports an error instead of starting a second
-proxy. If the shim reads "activation not confirmed", add
+OpenCodex on macOS if it quit and its recorded ownership matches a valid **Start at Login**
+registration, and otherwise reports an error instead of starting a second proxy. Automatic
+reopening selects that registration's app bundle path and checks that its declared main
+executable matches the registered Desktop executable. If registration is missing, stale or
+inconsistent, open OpenCodex manually; another copy with the same bundle identifier is not
+selected as a fallback. If the shim reads "activation not confirmed", add
`. ~/.opencodex/codex-shell-env.sh` to your shell startup file after other PATH setup.
The bundled CLI is `/Applications/OpenCodex.app/Contents/MacOS/ocx` on macOS and
diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md
index 5bd17d8072e..78e83640a67 100644
--- a/docs-site/src/content/docs/guides/integrations.md
+++ b/docs-site/src/content/docs/guides/integrations.md
@@ -1,15 +1,16 @@
---
title: Integrations
-description: Connect opencodex to OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code and Factory Droid from the dashboard — one switch per client, with a backup taken before every write.
+description: Connect opencodex to OpenCode, Pi, Gentle Shell, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code and Factory Droid from the dashboard — one switch per client, with a backup taken before every write.
---
The **Connect** page writes opencodex's provider block into a client's own config
-file, and removes it again. Nineteen clients work this way, each with a switch:
+file, and removes it again. Twenty clients work this way, each with a switch:
| Client | Config file | Format | When the change takes effect | Credential |
|---|---|---|---|---|
| OpenCode | `~/.config/opencode/opencode.json` | JSON | next direct launch | `OPENCODEX_OPENCODE_API_KEY` |
| Pi | `~/.pi/agent/models.json` | JSON | new sessions | loopback placeholder |
+| Gentle Shell | `~/.gentle-shell/agent/models.json` (persistent selected home) | JSON | after restarting Gentle Shell | loopback placeholder |
| OMP | `~/.omp/agent/models.yml` | YAML | after restarting OMP | `opencodex-loopback` placeholder |
| Hermes | `~/.hermes/config.yaml` | YAML | new sessions | `OPENCODEX_HERMES_API_KEY` |
| OpenClaw | `~/.openclaw/openclaw.json` | JSON5 | immediately, on a running gateway | `OPENCODEX_OPENCLAW_API_KEY` |
@@ -28,6 +29,36 @@ file, and removes it again. Nineteen clients work this way, each with a switch:
| Command Code | `~/.commandcode/providers.json` (on Windows with the default home: use `HOME ?? USERPROFILE` only if nonblank and Windows-absolute, otherwise `homedir()`; a separately supplied home is preserved; append `.commandcode\providers.json`) | JSON | next launch of Command Code | none — keyless loopback (`apiKey: false`) |
| Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | JSON | immediately via file watching | none — keyless loopback |
+## Gentle Shell
+
+Choose **Connect → Gentle Shell**, or use the same managed-integration commands:
+
+```bash
+ocx export --client gentle-shell --json
+ocx integration client enable --client gentle-shell
+ocx integration client disable --client gentle-shell
+```
+
+Gentle Shell has a separate destination and rollback history from Pi. Only
+`providers.opencodex` is managed; other providers and preferences stay intact.
+Catalog changes refresh an already connected, unchanged block. An existing manually
+copied block is a conflict, not permission to overwrite it: inspect the preview and
+use the explicit overwrite confirmation only if you intend to replace it.
+
+The saved `home` in `~/.gentle-shell/config.json` selects the destination. A saved
+custom path wins; isolated/default mode uses `GENTLE_SHELL_HOME` or
+`~/.gentle-shell/agent`. `GENTLE_SHELL_CONFIG` selects an alternate launcher config.
+These environment variables must be set on the **OpenCodex process**, not just in a
+later shell. Selectors must be absolute paths; literal `~` and relative paths refuse.
+One-off `gentle-shell --home` flags cannot be discovered by a background proxy.
+
+Link mode or a home aliasing Pi refuses with a Pi-integration remedy; use **Connect → Pi**
+for that shared file instead. OpenCodex does not change launcher preferences. An
+unreadable/unsafe launcher config refuses; malformed JSON falls back to isolated,
+matching the launcher. If you move a connected home, disable it using its original
+selectors before reconnecting the new one. Restart Gentle Shell after a catalog update.
+This integration retains Pi’s loopback-only export policy and stores no real API key.
+
"omo" names three products that share the `~/.omo` folder. The **omo** tab manages Pi-based omo
(the senpi engine) through `~/.omo/agent/models.json`, as in the table above. Codex-based omo
(LazyCodex) gets its own section on the same tab, shown only when LazyCodex is detected, described in
diff --git a/docs-site/src/content/docs/guides/model-routing.md b/docs-site/src/content/docs/guides/model-routing.md
index 4ccfde4b97a..772a078eb74 100644
--- a/docs-site/src/content/docs/guides/model-routing.md
+++ b/docs-site/src/content/docs/guides/model-routing.md
@@ -117,6 +117,8 @@ Routing and catalog visibility are separate controls:
It raises the advertised window only when that exact model has a known per-model window
in `modelContextWindows` (for example, `1000000`); an unknown model keeps its live window.
An explicit per-model window wins, then the existing provider cap applies.
+ Saved windows and tier selections survive startup and OAuth re-login. OAuth presets fill
+ missing per-model defaults while keeping your configured entries, including on xAI.
The `"default"` tier retains normal live context metadata.
The dashboard control is planned separately.
diff --git a/docs-site/src/content/docs/guides/protocol-paths.md b/docs-site/src/content/docs/guides/protocol-paths.md
index 0c0f5c93fb9..9c79f5699ae 100644
--- a/docs-site/src/content/docs/guides/protocol-paths.md
+++ b/docs-site/src/content/docs/guides/protocol-paths.md
@@ -160,5 +160,11 @@ These paths still use the internal Responses bridge or are not covered:
top-level fields outside its allowlist without a feature effect, and does not apply
`claudeCode.stabilizePromptCache`.
A recognized Claude Code client keeps `context-management-2025-06-27` together with its
- `context_management` body field. Dropped beta names appear once in the proxy log as
- `anthropic-beta-dropped=`.
+ `context_management` body field. For first-party Anthropic key and OAuth requests, any native
+ Messages client can send `thinking-binding-controls-2026-08-01` with `thinking.block_binding`
+ on adaptive or enabled thinking. The proxy strips that nested field when the beta is absent or
+ the destination does not support it, preserving the rest of the thinking configuration. Cloud
+ vendor endpoints and third-party compatible providers do not inherit first-party beta support.
+ Pairing uses the final header, including operator-configured betas; a beta alone is valid for
+ [thinking transformation diagnostics](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking).
+ Dropped beta names appear once in the proxy log as `anthropic-beta-dropped=`.
diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index 520161b511b..45a3340944a 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -64,6 +64,17 @@ Shipped v1 configs migrate automatically to marker 2 and one option-aware row. T
is retained once at `~/.opencodex/config.json.pre-openai-tiers-v2.bak`; restore it with
`cp ~/.opencodex/config.json.pre-openai-tiers-v2.bak ~/.opencodex/config.json`.
+## Claude Opus 5.5 progress updates
+
+When the `anthropic` adapter sends Claude Opus 5.5 to `api.anthropic.com` (OAuth or API key),
+OpenCodex requests Anthropic's [progress-update display mode](https://platform.claude.com/docs/en/build-with-claude/thinking#progress-updates).
+Progress updates and mid-turn acknowledgements appear as visible assistant commentary in Codex,
+even when thinking summaries are hidden. Reasoning summaries are omitted in this mode because
+Anthropic cannot distinguish progress updates from reasoning under `display: "summarized"`.
+The original signed thinking blocks are preserved for tool continuation; no signature is decoded
+or rewritten. Updates appear once their signed block completes. Other models and compatible
+third-party endpoints retain their existing thinking display behavior.
+
## Anthropic image input
The built-in Claude model seeds advertise text and image input for both `anthropic` (OAuth) and
@@ -93,6 +104,8 @@ credentials must never be replayed on the same token, and local runtimes have no
preserve. It is opt-in: when the option is absent the feature is off; object presence enables
it unless `enabled: false`.
+When round-robin key selection is enabled, queued requests recheck key cooldowns before sending and acquire pacing capacity for any replacement key. This applies to native Chat, Messages, Responses and compact. Waiting for replacement-key capacity does not consume the upstream response-header timeout. Per-key pacing and round-robin use guarded HTTP/SSE even when upstream WebSockets are enabled; WebSocket-only steering and injection require a route without those key policies. This does not change the saved active key. An established upstream turn keeps its credential; later sends are refused if that key cools. Pre-output key rejection can recover to a healthy key within the existing request limit. Direct Images and compact requests record genuine 401/429 cooldowns without replaying that paid POST.
+
### Anthropic API-key credit exhaustion
Anthropic API-key pools with at least two keys automatically try a sibling key when a complete HTTP `400` or `402` error states that the credit balance is too low to access the Anthropic API. This applies to `invalid_request_error`, `billing_error`, and `permission_error` before any text or tool output. Key rotation and pool strategies skip the failed key for one hour, because an empty balance only clears when credits are added; when every key is out of credit, requests still use the active key and return Anthropic's original error; choosing the key as active in the dashboard, or restarting the proxy, clears that hold after a top-up. Replacement sends share the request's send limit. Ordinary `400` errors stay terminal, and this does not enable same-key retries or change OAuth account routing. A header override that sends a different credential cannot cool or rotate the selected pool key.
@@ -145,6 +158,9 @@ Cursor, Kiro, Zed Hosted AI and Nous Portal are login-only and have no API-key e
login-only too: `ocx login google-antigravity` signs in with your Google account over the Cloud Code
Assist wire, and the `google` preset beside it is the AI Studio Gemini API — a different product
reached with its own key, not a key mode for the same login.
+The Antigravity Accounts tab shows an observed membership tier as a badge beside the account
+label, using the upstream tier ID (for example `free-tier` or `gemini-pro`). No badge appears
+when the plan is unknown; it does not affect routing or quota.
Gemini 4 Argon (`gemini-4-argon`) is listed on the `google` preset with low, medium (the
default), and high thinking levels. Google offers it on the paid Gemini API tier only, not the free
@@ -154,6 +170,14 @@ eligible Enterprise accounts. Cloud Code Assist serves Argon only to the Antigra
OpenCodex sends Argon requests and model discovery with the CLI User-Agent
(`antigravity/cli/1.3.4`) and keeps the IDE User-Agent for every other model.
+For Gemini on Antigravity, including Argon, provider-marked thought summaries stay separate
+from the answer: Responses uses reasoning summary items, Chat Completions uses
+`reasoning_content`, and Messages uses `thinking` blocks. Clients decide how to display those
+fields. A Responses client can request `reasoning.summary: "none"` to suppress summaries;
+this does not turn off the model's internal reasoning. A thought signature is continuation
+metadata and can accompany ordinary text, so its presence alone does not hide that text.
+OpenCodex preserves unmarked answer text even when its wording sounds like planning or analysis.
+
The built-in `google-antigravity` model list is Gemini 3.8 Flash (`gemini-3.8-flash`), Gemini 4 Argon
(`gemini-4-argon`), Claude Opus 5.5 (`claude-opus-5-5`), and Claude Sonnet 5.5
(`claude-sonnet-5-5`), each with low, medium, and high efforts. What you actually see is your
@@ -213,6 +237,7 @@ ocx login zed # Zed native-app callback login (experimental)
ocx login command-code # Command Code browser OAuth (or import ~/.commandcode/auth.json)
ocx login orcarouter-oauth # OrcaRouter browser consent + PKCE
ocx login devin # Cognition/Devin: import Devin CLI credential, else Auth0 browser sign-in
+ocx login dsh-account # DeepSeek Harness Desktop (import ~/.dsh/.credentials.yaml grant)
ocx login github-copilot # GitHub device flow → Copilot token (Copilot account)
ocx login codex # Codex account pool (aliases: chatgpt, openai; needs a running proxy)
ocx logout
@@ -231,6 +256,7 @@ ocx logout
| `zed` | `zed` | `https://cloud.zed.dev` | Experimental Zed Hosted AI bridge. Uses Zed's native-app RSA callback, exchanges the account token for a short-lived hosted-inference token, and discovers the account's live roster. |
| `orcarouter-oauth` | `openai-chat` | `https://api.orcarouter.ai/v1` | Browser consent and key exchange use `https://www.orcarouter.ai` with S256 PKCE. The returned user-owned `sk-orca-…` API key is stored in the existing credential store and reused until revoked. |
| `devin` | `devin` | `https://server.codeium.com` | Experimental unofficial Cognition/Devin bridge. Login first imports the credential the installed Devin CLI already holds (`devin auth login` writes a `devin-session-token` to its own `credentials.toml`); when none is present it opens Auth0 browser sign-in and exchanges the pasted token via Cognition's `RegisterUser` for a long-lived API key. `ocx login devin-cli` remains as a deprecated alias. Models are discovered per account with `GetCascadeModelConfigs`. Account quota comes from `GetUserStatus` under one eight-second request and body deadline: dated daily and weekly windows, plus the monthly prompt and flex credit pool for a credit-billed plan or an unknown strategy with both reset dates absent when a balance field is present. Negative used credits omit the monthly window; valid zero available credits mark it exhausted. A timed-out probe keeps the last good quota. Not shown in the dashboard preset by default. Chat and usage reporting are verified against a live account across three models. |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | Experimental import-first account provider reusing an installed and signed-in DeepSeek Harness (DSH) Desktop account. Reads credentials strictly read-only from `~/.dsh/.credentials.yaml` upon explicit import. Independent from DeepSeek API key billing. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. |
Google Antigravity groups a model's complete `low` / `medium` / `high` variants into one entry with an effort selector. This includes Claude Opus and Sonnet 5.5 and future versions discovered during automatic refresh. Partial sets remain separate. Existing enabled/disabled choices carry into the grouped entry; genuinely new models still follow your New model policy. Saved suffix IDs continue to request their original tier. Grouped effort routing survives a proxy restart when model discovery is temporarily unavailable. A running Codex session may need to reload its model list after catalog refresh. Usage groups known Claude Sonnet and Opus 5.5 tier IDs under their base model, including historical tier records. Their displayed costs are derived Anthropic reference estimates, not Antigravity subscription charges.
@@ -278,6 +304,8 @@ provided, and the bridge infers the hosted backend family from the live row or t
model discovery is not an allowlist. The Zed access token has no refresh endpoint, so when Zed
revokes it, run `ocx login zed` again.
+For Zed-hosted OpenAI models, OpenCodex translates `developer` instructions to `system` messages and removes OpenAI-private fields and unsupported built-in tools such as `web_search`. Function and custom tools remain available through the delegate; a tool choice with no supported tools left becomes `none`.
+
For the canonical Kimi Coding Plan presets (`kimi` account login and `kimi-code` API key),
opencodex forwards only a caller-supplied stable `prompt_cache_key` to the Chat Completions request
(the Responses wire accepts the same field, but opencodex does not send it there today);
@@ -370,6 +398,29 @@ Two cases behave differently, and it is worth knowing which you are in:
Device-code providers never open a browser from the proxy in either case: they show a code and a
verification URL to open wherever you are signed in.
+### Headless Claude login (code-display mode)
+
+For Anthropic there is a cleaner path than pasting a dead redirect: the **code-display** login.
+Anthropic redirects to its own page and shows a `code#state` string instead of calling back to the
+proxy, so nothing needs to reach `127.0.0.1` on the proxy's host.
+
+```bash
+ocx login anthropic --headless
+```
+
+Over the management API, start with `POST /api/oauth/login` and `{"provider":"anthropic","mode":"code"}`;
+the response carries a `flowId` and an `expiresAt` (10 minutes). Submit the pasted string to
+`POST /api/oauth/login/code` with that `flowId`. The answer is the result of the token exchange —
+`{"ok":true,"account":…}` or `{"ok":false,"error":…}` — so a hosted caller with a single paste box
+knows whether the login actually worked. A paste with the wrong state, or with no code in it, is
+refused before any token request and leaves the flow open for another try; a paste that reaches the
+token exchange uses the flow up, so start a new login if that exchange fails.
+
+Only `anthropic` and `anthropic2` support this mode; other providers answer `code_mode_unsupported`. Several code
+logins can be in flight at once, each pinned to its `flowId`, but they all write the same account
+store: the most recent successful one becomes the active Claude account. This is meant for one
+operator on a headless host, not for isolating unrelated users on a shared instance.
+
### Multiple OAuth accounts
OAuth providers whose credentials include a stable account id or email can keep more than one
@@ -538,9 +589,20 @@ selectors, then retry. Signing in from a machine with no existing `kiro-cli` ses
The native dashboard choices are add-only and do not sign out `kiro-cli`. A device dialog shows the code and verification destination. Only recognized Kiro or Builder ID hosts are opened as links; an unexpected destination is shown as copyable text for review. Closing the dialog sends cancellation and leaves a bounded background status check to reconcile a commit already in progress. A stalled status response is retried; exhausting the flow deadline produces the neutral ended outcome rather than claiming success. Google and GitHub approval completes when Kiro reports `authorized` (or the legacy `approved` status) with valid tokens and a profile ARN.
The account list marks Kiro accounts excluded from automatic selection with a reason, when available.
+### DeepSeek Account (DSH)
+
+`dsh-account` is an experimental, import-first account provider designed to reuse the authenticated account credentials of an installed DeepSeek Harness (DSH) Desktop application.
+
+- **Independent from DeepSeek API Key:** `dsh-account` runs on the DeepSeek account signed in to DeepSeek Harness, whereas the `deepseek` provider bills against your platform API key balance. The two credential domains and routes are strictly separate and never fall through into one another.
+- **Explicit import only:** opencodex reads `~/.dsh/.credentials.yaml` (or `$DSH_HOME/.credentials.yaml`) only when you run `ocx login dsh-account` or select **Log in** on the DeepSeek Account (DSH) row of the dashboard's **Add provider** picker. It never copies, imports, or uses the token on its own.
+- **Read-only DSH credential store:** OpenCodeX treats `~/.dsh/.credentials.yaml` as strictly read-only. OpenCodeX never writes, modifies, removes, or alters the permissions of any files belonging to DeepSeek Harness.
+- **Isolated credential copy & logout:** Upon import, OpenCodeX validates the token against DeepSeek's authoritative identity endpoint (`/auth-api/v0/users/current`) and saves its own copy to the OpenCodeX auth store. Logging out or removing the account in OpenCodeX only deletes the OpenCodeX local copy; it never triggers upstream remote session logout and will not sign you out of DeepSeek Harness Desktop.
+- **No background token rotation & 401 re-import:** DSH session grants do not feature an automatic background refresh endpoint. If upstream returns HTTP 401 (`ACCOUNT_TOKEN_INVALID`), OpenCodeX marks the account `needsReauth` and halts routing. To restore access, the user must explicitly re-import via `ocx login dsh-account`.
+- **Third-party disclaimer:** OpenCodeX makes no claim of official endorsement or authorization by DeepSeek for third-party clients reusing session grants.
+
## 3. API-key catalog
-opencodex ships 104 built-in presets: 85 key-based, 15 OAuth, three local, and one default
+opencodex ships 105 built-in presets: 85 key-based, 16 OAuth, three local, and one default
ChatGPT-forward preset. The dashboard's **Add provider** picker opens a key provider's dashboard,
validates the key, and stores it; validation is provider-specific. Notable entries:
@@ -718,7 +780,22 @@ MiniMax model list is still the previous default receives it on the next start;
is left as it is; register the preview by hand with
`ocx models add minimax MiniMax-M3.1-Flash-Preview --context-window 1000000`.
-**OpenCode Go** requires a stable session identifier for routing. OpenCodex sends
+**OpenCode Go** routes `gemini-4-argon-contributor` through its Gemini API, including
+streaming and tool-result continuations. The existing `opencode-go` preset keeps its
+historical URL pinning: saved custom URLs, including `https://opencode.ai/inference/go/v1`,
+route through the canonical `https://opencode.ai/zen/go/v1` endpoint and inherit Go defaults.
+Gemini then uses Go's native model endpoint with key authentication. A differently named
+custom provider keeps its configured URL and does not receive this Gemini pin.
+Go selects protocols by exact model ID: GPT 6 Luna, GPT 5.6 Luna, Grok 4.6/4.7 and
+Muse Spark Contributor use Responses; Claude Haiku 5.5, MiniMax M2.7/M3 and
+Qwen3.7 Plus/3.8 Max/3.8 Flash use Anthropic Messages. Legacy MiniMax M2.5 and
+Union Alpha also retain Messages. Models without a listed wire retain Chat.
+New native pins apply to the pinned Go destination with key authentication.
+Responses models still accept an explicit `modelAdapters` override.
+When reading an older saved configuration, OpenCodex ignores obsolete Chat/Responses overrides for the newly pinned Gemini and Qwen models, along with their formerly valid Responses-only hosted-tool preferences. Other providers, accounts and routing remain available. Reads leave the file unchanged; a later explicit configuration edit saves the upgraded values. New overrides for these native models are still rejected.
+Gemini uses `low`, `medium` and `high` thinking levels; higher requested efforts clamp to `high`.
+
+OpenCode Go requires a stable session identifier for routing. OpenCodex sends
`claude-haiku-5-5`, like the MiniMax models, over the Anthropic Messages wire, matching
[OpenCode Go's endpoint table](https://opencode.ai/docs/go/#endpoints). OpenCodex derives
its Go session header from Codex thread/session headers, or from a client's
@@ -765,6 +842,8 @@ headers. That is separate from the keyless desktop quota OpenCode advertises
synthetic `Retry-After`; an upstream `Retry-After` still takes precedence. Same-key
wait-and-retry remains opt-in via [`retryOn429`](/reference/configuration/).
+Gemini models on `opencode-zen` use Google's native API automatically when the provider uses an API key, the default `googleMode` (`ai-studio`), and either `https://opencode.ai/zen/v1` or `https://opencode.ai/inference/v1`. Use the model IDs listed by Zen; OpenCodex preserves them, including Flash IDs without a `-tiered` suffix. Older Gemini Chat/Responses overrides and Responses-only hosted-tool preferences are ignored when reading saved configurations.
+
**The keyless `opencode-free` tier is currently closed to third-party clients.** Zen refuses
any request that arrives without an `x-opencode-session` header, answering with error type
`MissingSessionID` and the message "OpenCode's free tier can only be used in OpenCode". Presence
@@ -956,7 +1035,10 @@ stored by `muse login`. With no local credential, or on another platform, it sta
browser device-approval flow. Add-account and reauthentication skip local import to avoid
reusing the account being replaced. OpenCodex never launches the Muse CLI. If device login
fails without cancellation, an available manual-input surface can accept a pasted key;
-that key faces the same format and Model API validation as an imported key. See
+that key faces the same format and Model API validation as an imported key. On macOS,
+approve the Keychain prompt on the proxy host within 120 seconds. Import refusals
+show the failure reason and offer an existing-key paste field without starting another
+login. Add account on an already configured provider bypasses local import. See
[Platform support](/reference/platform-support/) for the full per-platform picture.
Starting any Meta Muse login through the management API requires a dashboard session,
@@ -1130,6 +1212,7 @@ OpenCodex provides official adapter support for Qoder through the `qoder` (Globa
([Global](https://qoder.com/account/integrations), [CN](https://qoder.cn/account/integrations)) and paste it as the provider's API key. The stored key reaches the CLI only as `QODER_PERSONAL_ACCESS_TOKEN` (Global) or `QODERCN_PERSONAL_ACCESS_TOKEN` (CN) in a scoped child environment.
- **Region Isolation:** Each preset accepts only its canonical destination (`https://qoder.com` or `https://qoder.cn`) and resolves its own executable. Credentials, model cache, usage, and health are independent; neither region falls back to the other. An older custom provider named `qoder` with a different destination keeps its existing adapter and URL.
- **Model Discovery:** `qoder --list-models` is the authoritative entitlement roster for the current PAT. The cache is bound to an irreversible fingerprint of the token, so switching accounts never reuses another account's roster. If discovery fails, the provider degrades to a stale cache and then the documented static seed.
+- **Token usage:** Qoder CLI reports zero token counts unless its usage switch is on, so each preset turns it on in its scoped CLI child environment: `QODER_EXPOSE_TOKEN_USAGE=true` for Global and `QODERCN_EXPOSE_TOKEN_USAGE=true` for CN, which reads only its own prefix.
- **Tool Ownership:** The CLI runs single-turn `stream-json` with `--tools ""`, `--strict-mcp-config`, setting sources disabled, and session persistence disabled, so Codex keeps exclusive tool ownership. Client-declared tools are exposed through an isolated request-scoped MCP server.
- **Image Input:** Qoder Global accepts image input through the Qoder CLI. OpenCodex forwards supported image inputs that reach the Global adapter to the CLI without maintaining a per-model vision capability allowlist there. Image compatibility and any model-specific preprocessing or routing are handled by Qoder. OpenCodex does not use the static discovery fallback roster to classify per-model vision capability. Qoder CN keeps its existing sidecar classification, and any residual raw-image request that reaches the CN adapter is rejected as `unsupported_input_modality`. OpenCodex serializes history before the current turn, but Qoder does not persist sessions, so prior images are replayed on later turns. In observed multi-turn image conversations, replayed images can become unreliably associated or effectively ordered relative to the historical text they originally accompanied.
- **PAT recovery:** For both Qoder presets, a multi-key API-key pool can switch to another PAT on a supported authentication rejection or account-level quota exhaustion, but only before response content or an event that makes replay unsafe. Recovery requires Qoder-specific classification and respects credential-hop and send budgets; an HTTP 401/429 or quota message alone is not permission to replay. Single-key configurations do not rotate. Sharing the CLI runner does not enable this API-key recovery for CodeBuddy or self-authenticated Claude CLI.
@@ -1183,15 +1266,28 @@ CLI headlessly (`claude -p`, `stream-json`) once per turn:
variable (a `claude` already pointed at this proxy therefore cannot loop back into it), telemetry,
feedback and the auto-updater disabled, and `--tools ""`, `--strict-mcp-config` plus
`--setting-sources ""`. The harness loads no CLAUDE.md, skill, hook, plugin or MCP server from the
- machine and can neither read, write, exec nor browse. No session is persisted between turns.
+ machine and can neither read, write, exec nor browse. No session is persisted between turns. The
+ official CLI is found on `PATH`, or at the native install location (`~/.local/bin/claude`) when
+ a service starts OpenCodex with an older `PATH`.
- **System prompt:** the caller's system and developer prompts replace the Claude Code preset
(`--system-prompt-file`), so the turn answers the client's contract rather than the harness
persona. The folded prompt is staged in a private per-turn file (mode `0600`) and passed by path,
because process arguments are world-readable through process listing; a request that carries
- neither a system nor a developer prompt gets an empty file, which replaces the preset with nothing.
-- **Tool ownership:** v1 is text and reasoning only, exactly like the CodeBuddy and Qoder presets:
- with no tool channel, approval, sandboxing and execution stay with the client. The shared
- capture-only tool bridge is the documented follow-up.
+ neither a system nor a developer prompt gets only the fixed conversation-replay instructions.
+- **Conversation caching:** each message is replayed as a separate stable text block, with the
+ current message formatted identically to its later history representation. Long history is cut
+ only at whole-message checkpoints. Tool catalogs serialize deterministically. Claude Code
+ controls cache breakpoints, so this makes prompt-cache reuse possible without guaranteeing it.
+- **Tool ownership:** when the request carries tools, an isolated capture-only MCP catalog exposes
+ only the client's selected tools (the same bridge CodeBuddy uses). The CLI captures the call and
+ the turn ends there; approval, sandboxing, execution and the returned result stay with Codex,
+ Pi, or another client. Built-in tools and every other MCP server remain disabled.
+- **Subscription limits:** before each launch OpenCodex asks Anthropic's usage endpoint, with the
+ CLI's own sign-in read in memory, whether the plan's 5-hour, weekly or model-specific window is
+ full. An exhausted plan is refused locally with HTTP 429 `claude_subscription_cooldown` and a
+ `Retry-After` for the reset, instead of starting a CLI that would fail. The answer is cached for
+ a minute in `claude-usage-admission.json` under the OpenCodex home (percentages, reset times and
+ a hashed account key only). If the check cannot run, the turn proceeds.
- **Destination:** the canonical row names `https://api.anthropic.com` because that is where the
subscription's traffic lands. OpenCodex never sends that request itself, and overriding the base
URL fails closed rather than handing the turn to another environment.
@@ -1439,8 +1535,17 @@ dashboard or `custom` in `ocx init` and enter the base URL. See the
Codex asks a second model to review approval requests, and takes that reviewer from
`auto_review_model_override` on the catalog row of the current turn's model. The root
-`auto_review_model` in `$CODEX_HOME/config.toml` applies one reviewer to every row. To give a
-routed provider its own — usually cheaper — reviewer, set the selector on that provider row in
+`auto_review_model` in `$CODEX_HOME/config.toml` is Codex's fallback. The dashboard's global
+**Auto-review override** in Model settings applies one reviewer to all catalog rows that do not
+have a provider-scoped reviewer. It stores `autoReviewOverride` in `~/.opencodex/config.json`; the
+setting must be enabled and name a model. An enabled setting with no model stays unapplied and
+leaves the Codex fallback in effect. A catalog refresh is a separate state: after a save it can
+stay pending, and the Models page reports it so you can retry with `ocx sync`. From the CLI, read
+the setting with `ocx models auto-review status` or
+change it with `ocx models auto-review set [--enabled ]`; use `-` to clear the
+model. CLI output recommends `ocx sync` when a refresh can be retried. If a refresh fails, the
+setting remains saved and the command exits 1. Resolve the failure before syncing. To give a routed
+provider its own — usually cheaper — reviewer, set the selector on that provider row in
`~/.opencodex/config.json`:
```json
@@ -1472,9 +1577,10 @@ no per-model override, leaving a valid provider-wide target as fallback. Whateve
keep the root selector, or upstream behavior when that is unset. Removing the root selector leaves
provider stamps alone, and removing a provider selector clears only that provider's stamps.
-These fields are available through configuration, `PATCH /api/providers?name=`, and
-the dashboard raw JSON provider editor; dedicated form controls are not present. The canonical `openai` provider
-rejects them. Field-by-field rules live in the
+Provider fields are available through configuration, `PATCH /api/providers?name=`, and
+the dashboard raw JSON provider editor; dedicated provider form controls are not present. The
+global override is available in Model settings. The canonical `openai` provider rejects provider
+reviewer fields. Field-by-field rules live in the
[provider configuration reference](/reference/configuration/providers/#auto-review-approval-model-selection).
## Rate limits in the providers overview
diff --git a/docs-site/src/content/docs/guides/remote-workspace.md b/docs-site/src/content/docs/guides/remote-workspace.md
index 939ea624de9..1ebc0f725a8 100644
--- a/docs-site/src/content/docs/guides/remote-workspace.md
+++ b/docs-site/src/content/docs/guides/remote-workspace.md
@@ -171,6 +171,29 @@ Prompt submission acknowledges acceptance immediately; the dashboard polls the s
cancels an active Executor command, and prevents a late response from reopening the stopped
session.
+## Work with sessions side by side
+
+The session list on the left opens a session in the focused pane. Drag a session to the left,
+right, top, or bottom edge of another pane to split the conversation area. You can show up to
+four different sessions at once. An already visible session moves between panes instead of
+opening a duplicate composer.
+
+Use **Split right** or **Split below** next to a session for the keyboard-accessible alternative.
+Drag a divider to resize the panes, or focus the divider and use its arrow keys. **Home** and
+**End** set the divider to its minimum and maximum size. Each session
+keeps its own draft, submission state, and Stop action, so a pending turn in one pane does not
+block a different session. **Close pane** changes the layout only; it does not stop the session.
+
+The pane layout is restored for this Hub in the same browser. Only session identifiers and pane
+positions and divider sizes are saved; draft text and model output are not written to browser storage. Reloading
+the page clears unsent drafts. On screens up to 760px wide, panes stack vertically without
+resize dividers; the saved split positions and sizes return when the screen is wider.
+
+This view is shared by the browser dashboard and the OpenCodex desktop dashboard. CLI users
+can open the dashboard with `ocx gui` and select **Remote Workspace** after its normal Hub setup.
+It does not change the terminal interface of `codex` or the conversation layout inside the
+separate official ChatGPT/Codex desktop app.
+
## Restart and reconnect behavior
The Hub persists bounded session metadata and a small recent event snapshot. After a Hub restart,
diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md
index 3d35989872b..1e353a4a48b 100644
--- a/docs-site/src/content/docs/guides/sidecars.md
+++ b/docs-site/src/content/docs/guides/sidecars.md
@@ -141,13 +141,23 @@ failures after response headers have started are delivered as `response.failed`
Image routing is capability-aware. Before an image-bearing upstream send, opencodex resolves the selected model's effective input modalities from runtime provider evidence, explicit operator declarations, backend/registry metadata, and generated vendor metadata. A target positively known to be text-only goes through the Vision Sidecar first; the image is described **before** the main call and replaced inline with text. A target positively known to support images receives the image directly. Unknown custom models keep the existing compatibility behavior rather than being guessed text-only.
For the canonical ChatGPT Codex route, opencodex uses the `openai-codex` metadata bundle rather than public OpenAI API metadata, so backend-specific modality differences are respected. The native Chat fast path uses the same gate and cannot bypass a known text-only verdict. Without an available sidecar plan, raw images are stripped before a proven text-only backend.
-Combos advertise image input only when every member accepts images, either natively or through a
+With `visionSidecar.enabled: false`, the model catalog does not add sidecar-backed image input to
+text-only models, including custom rows and combo members. Native image support and explicitly
+configured image modalities are preserved. Omitting `enabled` keeps sidecar advertisement on;
+catalog visibility does not establish that sidecar credentials or capacity are available.
+Combos advertise image input only when every member accepts images, either natively or through an enabled
sidecar, and the combo's `imageInput` setting is not disabled, so clients such as the Codex app
allow attachments instead of blocking them before the sidecar runs. When
`visionSidecar.model` is absent or blank, the OpenAI execution path, Dashboard, and management API
use the `gpt-5.6-luna` fallback. Startup still migrates an explicitly persisted legacy
`gpt-5.4-mini` value to `gpt-5.6-luna`; that migration applies to a stored value, not to an absent
model field.
+Saving a multimodal combo from the dashboard enrolls its non-image members automatically: the
+Combos page sends them as `visionSidecarTargets` on `PUT /api/combos`, which writes the exact
+text-only declaration on the member's provider, so no manual config edit and reload is needed.
+Enrollment never overwrites an existing image-capable declaration, and members whose modalities are
+unknown or have no text input are not enrolled — the dashboard names them and keeps the combo's
+image switch unavailable.
The first-party DeepSeek `deepseek-flash` model is native multimodal (`text` and `image`) and does
not use this sidecar by default. Explicit `noVisionModels` or text-only declarations remain
authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash` remain
@@ -222,6 +232,10 @@ timeout, and limit.
omitted keys unchanged. `timeoutMs` uses the runtime integer bounds
(1–2147483647 ms).
+When the effective Vision switch changes, saving also refreshes the managed Codex catalog and
+model cache. The response reports `catalogRefreshPending: true` if that refresh did not complete;
+the saved switch is retained, and catalog publication does not confirm that a running client has reloaded it.
+
The web-search sidecar card carries the same control shape: the model picker's first row is
**Off**. Off does two things, and the second one is the reason the row exists. OpenCodex stops
intercepting `web_search`, and the Codex integration writes Codex's own
diff --git a/docs-site/src/content/docs/guides/subagent-v1-default.md b/docs-site/src/content/docs/guides/subagent-v1-default.md
index 5f4fd79b58f..ab8c6654c6f 100644
--- a/docs-site/src/content/docs/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/guides/subagent-v1-default.md
@@ -130,3 +130,13 @@ ocx v2 mode v1
```
The CLI does not prompt. It is the same setting, so choose it knowing what this page describes.
+
+## Concurrent child limit
+
+In **Models → Model settings**, the subagent limit control sets the maximum number of concurrent
+child agents, excluding the primary agent. It remains available in v1, base and v2, including
+hybrid mode with the global v2 flag disabled. Choose the surface mode separately from this limit.
+
+Raising the cap permits more children; it does not create them automatically or guarantee faster
+execution. Start a new Codex session after changing the limit. If a long-running App host still
+uses the previous limit, restart that host.
diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md
index d808779cd34..77d8d4fcd7a 100644
--- a/docs-site/src/content/docs/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/guides/web-dashboard.md
@@ -71,7 +71,7 @@ host and port over a LAN IP or an alias.
## Dashboard layout
-The sidebar has eight rows, in order: **Dashboard**, **Connect**, **Codex**, **Providers**, **Models**, **Subagents**, **Usage & Logs**, and **Remote Link**. **Connect** opens the page headed **Connect**, with its tab strip ordered **Codex**, **Claude**, **Claude Desktop**, **Grok Build**, then the remaining integrations. Claude has no sub-tabs; it shows the Claude Code settings directly. Claude Desktop is a separate Connect tab at `#claude/desktop`. Connect has no section switcher. **Usage & Logs** opens Usage and groups Usage, Logs & Debug, and Storage with a pill-shaped section switcher. The last row, **Remote Link**, groups Remote Link (`#remote`) and Remote Workspace (`#remote-workspace`, when available) with the same switcher. **Codex** is the renamed **Codex Set** row; the preserved URLs include `#codex-set`, `#claude`, `#claude/code`, and `#claude/desktop`; the former `#claude/settings` bookmark opens Code. The former `#claude/account` bookmark redirects to `#providers?provider=anthropic&tab=accounts`. Startup (`#startup`) stays outside the sidebar.
+The sidebar has eight rows, in order: **Dashboard**, **Connect**, **Codex**, **Providers**, **Models**, **Subagents**, **Usage & Logs**, and **Remote Link**. **Connect** opens the page headed **Connect**, with its tab strip ordered **Codex**, **Claude**, **Claude Desktop**, **Grok Build**, then the remaining integrations. Claude has no sub-tabs; it shows the Claude Code settings directly. Claude Desktop is a separate Connect tab at `#claude/desktop`. Connect has no section switcher. **Subagents** opens Subagents and groups Subagents and Advisor (`#advisor`) with a pill-shaped section switcher. **Usage & Logs** opens Usage and groups Usage, Logs & Debug, and Storage with a pill-shaped section switcher. The last row, **Remote Link**, groups Remote Link (`#remote`) and Remote Workspace (`#remote-workspace`, when available) with the same switcher. **Codex** is the renamed **Codex Set** row; the preserved URLs include `#codex-set`, `#claude`, `#claude/code`, and `#claude/desktop`; the former `#claude/settings` bookmark opens Code. The former `#claude/account` bookmark redirects to `#providers?provider=anthropic&tab=accounts`. Startup (`#startup`) stays outside the sidebar.
Responses first-output timing includes streamed function arguments and custom-tool input, as well
as text and reasoning. A tool-only turn can therefore have a first-output time even without prose.
@@ -111,6 +111,7 @@ is visible) and never forces an upstream refresh.
| Area | What it does |
| --- | --- |
| **Dashboard summary** | Multi-agent mode, online state, version, uptime, provider count, 30-day token total, active providers, and available native/routed models. |
+| **Native reasoning retention** | Independently opt into reasoning ciphertext forwarding on native model or account changes. Both default off; account retention is experimental. See [the configuration scope](/reference/configuration/server/#native-reasoning-retention). |
| **Sub-agent delegation** | Choose a native or routed model and optional reasoning effort shared by OpenCodex delegation guidance and the separate native-default opt-in. This is not a proxy-side per-spawn router; see below. |
| **Sidecars** | Choose the web-search model and effort plus the vision-description model. Changes apply on the next request. |
| **Maintenance** | Resync the Codex model catalog, inspect project-local config bypass warnings, check the latest or preview release, and run an update with optional proxy restart. In the desktop shell, its update entry opens the native app update page instead of running the package updater. |
@@ -203,7 +204,11 @@ The **Usage** summary and Models/Providers tables show end-to-end output through
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
+reports pricing coverage fields. Usage that was only partly measured (`unreported` or `unsupported`)
+is not priced, and one excluded part excludes the whole request from that row's priced count. A
+leg that reached no upstream, or that upstream refused with an HTTP error before any output, spent
+nothing and does not; a leg whose response began (or ended without a status) but reported no usage
+may have been billed, so it does. A row with only excluded requests shows an em dash with that count
instead of a zero-dollar estimate, and a displayed zero-dollar value is a confirmed priced estimate.
An older proxy that reports no coverage fields keeps a bare em dash with no count, so missing
coverage stays distinct from an all-excluded row and from a confirmed zero estimate.
diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md
index 2294fc1d541..ed820bab874 100644
--- a/docs-site/src/content/docs/ja/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` では次の手順を説明します。
-1. **プロバイダーを選択してください** — 104 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。
+1. **プロバイダーを選択してください** — 105 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。
2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。
3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。
4. **プロキシ ポート** — デフォルトは `10100` です。
diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md
index 4d1cd5938b4..8476e7eb932 100644
--- a/docs-site/src/content/docs/ja/guides/claude-code.md
+++ b/docs-site/src/content/docs/ja/guides/claude-code.md
@@ -166,21 +166,21 @@ unknown は設定がまだ opencodex のプロキシを指すか判断できな
### Picker モード: 1P の Code タブで opencodex モデルを表示する
-Picker モードは 1P モードの一部です。macOS で 1P を選ぶとデフォルトで有効になりますが、
+Picker モードは 1P モードの一部です。macOS と Windows で 1P を選ぶとデフォルトで有効になりますが、
`claudeCode.intercept.picker: false` を設定した場合は無効です。1P の Desktop の Code タブにある
モデルピッカーを書き換え、利用できる opencodex モデルを名前付きで表示します。初回の有効化時は、
-macOS がログインキーチェーン内のローカル証明書認証局を信頼するよう求めることがあります。この認証局の
+macOS がログインキーチェーン内、Windows が現在のユーザーのルートストアでローカル証明書認証局を信頼するよう求めることがあります。この認証局の
制約は `claude.ai` とそのサブドメインに限られます。エクスポート可能な署名用 ID は OS の資格情報ストアで
保護され、通常の再起動でも同じ証明書と鍵を再利用します。OpenCodex の設定ディレクトリには Picker の
-署名鍵を平文で保存しません。制約付き CA の完全な検証と OS の信頼確認は引き続き必要です。
-承認済みの同じ ID と利用可能な資格情報ストアがあれば、再起動で証明書の信頼設定を追加・削除しません。
-起動時の復元は信頼をインストールしません。信頼が未設定、取り消し済み、不明の場合は保留となり、
-`ocx claude desktop picker on` または `ocx claude desktop picker trust` を明示的に実行して信頼を付与します。
+署名鍵を平文で保存しません。制約付き CA の完全な検証と OS の信頼確認は引き続き必要です(Windows では正確なフィンガープリントと検証済みリーフで確認します)。
+承認済みの同じ ID と利用可能な資格情報ストアがあれば、再起動で macOS の証明書の信頼設定を追加・削除しません。
+起動時の復元や状態確認は信頼をインストールしません。信頼が未設定、取り消し済み、不明の場合は保留となり、
+`ocx claude desktop picker on` または `ocx claude desktop picker trust` を明示的に実行して信頼を付与します(Windows では確認が表示されることがあります)。
旧 ID からの一度限りの移行では、以前の信頼を削除するために同意が必要になる場合があります。
削除が完了するまでは Picker は利用できず、適用済みプロファイルは復号しない中継を使います。
キーチェーンのロック解除やアプリの資格情報へのアクセス許可は macOS 独自の動作で、再起動や更新時にも
-表示される場合があります。Windows と Linux は Picker 非対応で、Picker の CA、資格情報ストア、
+表示される場合があります。Windows では `on` や `trust` の明示的な実行時にルート追加の確認が表示されることがあります。Linux は Picker 非対応で、Picker の CA、資格情報ストア、
プロキシの処理を開始しません。通常の Claude インターセプトは利用でき、ローカル CA ファイルには
所有者、シンボリックリンク、ファイル権限、Windows ACL の検査が適用されます。
@@ -535,7 +535,7 @@ Messages → Responses に変換されるリクエストでは、Claude Code の
| Assistant `tool_use` | `function_call`(`input` → JSON 文字列に変換した `arguments`) |
| ユーザー `tool_result` | `function_call_output`(`is_error` → `[tool error]` 接頭辞) |
| `thinking` / `redacted_thinking` 再生 | シグネチャと秘匿ペイロードを境界付き `ocxr1` エンベロープに保持した `reasoning` 項目 |
-| Function ツール | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) |
+| Function ツール | `{type: "function"}`(`web_search*` → `{type: "web_search"}`). `api.anthropic.com` への翻訳ルートでは、型付き `web_search_*` と `web_fetch_*` 宣言も Anthropic に送信され、サーバー側で実行されます。ただし Web 検索サイドカーが `web_search` を提供する場合はそちらが優先されます。検索と取得の結果は `server_tool_use` と対応する `web_search_tool_result` / `web_fetch_tool_result` としてクライアントに返されます |
| `tool_choice` | `auto`→`auto`、`none`→`none`、`any`→`required`、名前指定関数→`{type:"function",name}`、ホスト型 WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md
index 1a03b3e6a07..7ed25d4cb27 100644
--- a/docs-site/src/content/docs/ja/guides/combos.md
+++ b/docs-site/src/content/docs/ja/guides/combos.md
@@ -126,7 +126,7 @@ ocx combo set balanced \
|機密認証、サブスクリプション、クォータ、レート制限、過負荷、またはアップストリーム サーバー エラー |ステータスだけでは物足りない場合でもターゲットを冷やしてホップさせましょう。 |
|クライアントのキャンセル (499)、`origin_rejected`、サイバー ポリシーの拒否、コンテキスト オーバーフロー、またはその他の無効なリクエスト |停止してエラーを返します。別のターゲットではリクエストは有効になりません。 |
| ChatGPT アカウントで Codex を使う際に、そのモデルをサポートしないことを示す正確な HTTP 400 の拒否 | 出力を確定する前に、次の設定済みターゲットを試します。拒否文全体が `detail`、`error.message`、または本文そのものにある場合に限ります。`detail` と `error` が競合する応答は終了扱いです。この拒否だけでアカウントを再認証待ちにしたり、プロバイダーの全モデルをクールダウンしたりしません。 |
-|`user` の明示的な拒否、`reasoning.effort`/`reasoning_effort` の非対応値、またはモデル固有の画像入力拒否(`param: input`)を示す構造化 HTTP 400 |出力開始前に次の適格なターゲットへ進み、クールダウンを記録しません。任意パラメーターの互換性を参照してください。 |
+|`user` の明示的な拒否、`reasoning.effort`/`reasoning_effort` の非対応値、モデル固有の画像入力拒否(`param: input`)、またはルートが読み取れない入力メディアの拒否(`unsupported_input_modality`)を示す構造化 HTTP 400 |出力開始前に次の適格なターゲットへ進み、クールダウンを記録しません。任意パラメーターの互換性を参照してください。 |
|インプロセスアダプター(`runTurn`)が実行する Responses ターンで、現在のリクエストが宣言していない最初のツール呼び出し(出力やリプレイ不可の副作用より前) |ターゲットをクールダウンし、同じツールカタログで次へ進みます。可視出力やリプレイ不可の副作用の後は拒否が確定します。Chat Completions と Anthropic Messages のリクエストは変わりません。 |
|その他の未分類のエラー |停止してエラーを返します。 |
@@ -184,7 +184,7 @@ v1/base/v2 モードと完全な暗号化タスクのワークフローについ
### ダッシュボード
-ローカル ダッシュボードを開き、**Models → コンボ**を選択します。ワークスペースはコンボを作成、編集、名前変更、削除し、そのターゲット ピッカーは無効なモデルとネストされたコンボを除外します。
+ローカル ダッシュボードを開き、**Models → コンボ**を選択します。ワークスペースではコンボの作成、編集、名前変更、削除ができます。モデル選択には非表示モデルも含まれます。表示設定は外部のモデル一覧にのみ適用され、コンボ内部の選択には影響しません。非表示モデルを選択しても外部には表示されません。ターゲット選択では、無効なプロバイダー、ネストされたコンボ、決定サービス専用の JEV プロバイダーは引き続き除外されます。
各ターゲットには **利用可能**、**クォータを使い切りました**、**クォータ不明** のライブバッジも表示されます。
エディターがクォータを理由に保存と作成をブロックするのは、使用可能なすべてのターゲットについて、設定された認証情報の推論上限に達したことを示す、サーバーによる確認が現在も有効な場合だけです。表示専用のアカウント・モデル・検索・MCP クォータや、ルーティングの根拠情報の欠落・期限切れによって、このブロックが発生することはありません。ブロックは該当するリセット時刻またはデータの有効期限に解除され、ページがアクティブになるか表示状態になると再確認されます。「更新」はコンボデータとクォータの両方を再読み込みします。ダッシュボードのエディターではまだ `cooldownMs` と `waitForCooldownMs` を設定できません。後続の UI 作業が完了するまでは、構成ファイルまたは管理 API を使用してください。
@@ -269,6 +269,6 @@ ocx combo remove --yes
## 任意パラメーターの互換性
-通常の 400 エラーは終了扱いですが、`user` の明示的な拒否、`reasoning.effort`/`reasoning_effort` の非対応値、またはモデル固有の画像入力拒否(`param: input`)を示す構造化エラーでは、出力開始前に次の適格なターゲットへ進めます。この不一致ではクールダウンを記録しません。安全ポリシーによる拒否、キャンセル、出力開始後の再実行は禁止のままです。
+通常の 400 エラーは終了扱いですが、`user` の明示的な拒否、`reasoning.effort`/`reasoning_effort` の非対応値、モデル固有の画像入力拒否(`param: input`)、またはルートが読み取れない入力メディアの拒否(`unsupported_input_modality`)を示す構造化エラーでは、出力開始前に次の適格なターゲットへ進めます。この不一致ではクールダウンを記録しません。安全ポリシーによる拒否、キャンセル、出力開始後の再実行は禁止のままです。
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md
index 114fa62773f..3323995b0fc 100644
--- a/docs-site/src/content/docs/ja/guides/providers.md
+++ b/docs-site/src/content/docs/ja/guides/providers.md
@@ -101,6 +101,7 @@ ocx login google-antigravity
ocx login cursor # Cursor 専用 PKCE ログイン
ocx login command-code # Command Code のブラウザ OAuth (または ~/.commandcode/auth.json を取り込み)
ocx login devin # Cognition/Devin: Devin CLI の資格情報を優先インポート、なければ Auth0 ブラウザサインイン
+ocx login dsh-account # DeepSeek Harness Desktop (~/.dsh/.credentials.yaml の許可を取り込み)
ocx login github-copilot # GitHub デバイスフロー → Copilot トークン (Copilot Pro/Business)
ocx login codex # Codex アカウントプール (別名: chatgpt, openai / プロキシの起動が必要)
ocx logout
@@ -117,6 +118,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth を Cloud Code Assist wire で使用。ライブ探索は認証済みの CCA `v1internal:fetchAvailableModels` エンドポイントを使用し、ログイン中のアカウントで利用可能な agent モデルのみを公開します。管理されたカタログはフォールバックとして残ります。 |
| `cursor` | `cursor` | `https://api2.cursor.sh` | 実験的 PKCE ログイン、HTTP/2 トランスポート、アカウント別モデル探索をサポート。 |
| `devin` | `devin` | `https://server.codeium.com` | 実験的な非公式 Cognition/Devin ブリッジ。ログインはまず、インストール済み Devin CLI が保持する認証情報を取り込みます(`devin auth login` が `devin-session-token` を自身の `credentials.toml` に書き込みます)。なければ Auth0 のブラウザサインインを開き、貼り付けたトークンを `RegisterUser` で長期 API キーに交換します。`ocx login devin-cli` は非推奨エイリアスとして引き続き使えます。モデル一覧は `GetCascadeModelConfigs` でアカウントごとに取得し、ストリーミングは Connect-RPC 上の `runTurn` 経路のみを使います。ダッシュボードのプリセットには既定で含まれません。 |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | 実験的なインポート優先アカウントプロバイダー。インストールおよびサインイン済みの DeepSeek Harness (DSH) Desktop アカウントを再利用します。明示的なインポート時に `~/.dsh/.credentials.yaml` から厳格に読み取り専用で資格情報を読み込みます。DeepSeek API キー課金とは完全に独立しています。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 実験的。GitHub デバイスフロー + `copilot_internal` 交換(VS Code OAuth クライアント)。有効な Copilot サブスクリプションが必要で、公式のサードパーティ API ではありません。 |
Google Antigravity のアカウント・プロバイダーのクォータ確認は、モデル一覧へのフォールバックも含め、固定の Google エンドポイントを使用します。その宛先では透過 Fake-IP DNS に対応し、TLS 検証、リダイレクト拒否、プライベートアドレス検査を維持します。カスタム base URL はモデル要求にのみ適用されます。`NO_PROXY` は直接接続のポリシーを維持します。
@@ -191,9 +193,20 @@ Kiro のログインには Kiro CLI が必要です。Unix では `curl -fsSL ht
ロールバックはスナップショットがある場合にのみ可能なため、セッションストアが存在するのに取得できない場合(ファイルが読めない、スキーマの不一致、トークン選択があいまい)、`KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE` が実際の CLI ストアと異なるインポート先を指す場合、またはプライマリ CLI データベースに認識できるトークン行がない場合、**アカウントを追加**は `kiro-cli` のログアウトを拒否します。通常の `kiro-cli` データパス上の壊れたデータベースを修復または削除し、インポート専用セレクタが設定されていれば解除してから再試行してください。既存の `kiro-cli` セッションがまったくない環境には影響しません。
+### DeepSeek Account (DSH)
+
+`dsh-account` は、インストールおよびサインイン済みの DeepSeek Harness (DSH) Desktop アプリケーションの認証済みアカウント資格情報を再利用する、実験的なインポート優先アカウントプロバイダーです。
+
+- **DeepSeek API キーから独立:** `dsh-account` はユーザーの認証済み DeepSeek セッションクォータ(通常/ボーナスウォレット残高および総支出)を使用しますが、`deepseek` プロバイダーはプラットフォーム API キー残高から課金されます。この2つの資格情報ドメインおよびルーティングは厳格に分離されており、相互にフォールバックすることはありません。
+- **明示的インポートのみ:** OpenCodeX は DeepSeek Harness がインストールされサインインされているか(`~/.dsh/.credentials.yaml`)を検出しますが、ユーザーの明示的な操作(`ocx login dsh-account` または Web ダッシュボードでのインポートクリック)なしにトークンを自動的にコピー、インポート、または使用することはありません。
+- **DSH 資格情報ストアへの読み取り専用:** OpenCodeX は `~/.dsh/.credentials.yaml` を厳格に読み取り専用として扱います。OpenCodeX が DeepSeek Harness に属するファイルの書き込み、変更、削除、またはアクセス権の変更を行うことは決してありません。
+- **隔離された資格情報の複製とログアウト:** インポート時、OpenCodeX は DeepSeek の信頼できる認証エンドポイント(`/auth-api/v0/users/current`)に対してトークンを検証し、OpenCodeX 認証ストアに独自の複製を保存します。OpenCodeX でアカウントをログアウトまたは削除しても、OpenCodeX のローカル複製が削除されるだけであり、上流のリモートセッションログアウトがトリガーされたり、DeepSeek Harness Desktop からサインアウトされたりすることはありません。
+- **バックグラウンドトークン自動ローテーションなし・401 時の再インポート:** DSH セッショングラントには自動バックグラウンド更新エンドポイントがありません。上流から HTTP 401(`ACCOUNT_TOKEN_INVALID`)が返された場合、OpenCodeX はアカウントを `needsReauth` としてマークし、ルーティングを停止します。アクセスを再開するには、ユーザーが `ocx login dsh-account` で明示的に再インポートする必要があります。
+- **サードパーティ免責事項:** OpenCodeX は、サードパーティクライアントによるセッショングラントの再利用について、DeepSeek 公式からの承認や保証を主張するものではありません。
+
## 3. API キーカタログ
-opencodex には組み込みプリセットが 104 個含まれています。キー方式 85、OAuth 15、ローカル 3、
+opencodex には組み込みプリセットが 105 個含まれています。キー方式 85、OAuth 16、ローカル 3、
デフォルト ChatGPT 転送プリセット 1 です。ダッシュボードの **Add provider** ピッカーはキー発行ページを開き、
入力したキーを検証した後保存します(検証はプロバイダー固有です)。主な項目は以下のとおりです:
diff --git a/docs-site/src/content/docs/ja/guides/sidecars.md b/docs-site/src/content/docs/ja/guides/sidecars.md
index 4d623877c98..0ffc670f9a8 100644
--- a/docs-site/src/content/docs/ja/guides/sidecars.md
+++ b/docs-site/src/content/docs/ja/guides/sidecars.md
@@ -72,8 +72,11 @@ stall は全体生成 timeout ではありません。SSE 開始前の失敗は
ルーティングモデルが該当プロバイダーの `noVisionModels` にある、またはそのモデルが
`modelInputModalities` でテキスト専用と宣言され、リクエストに画像が来る場合、opencodex は利用可能な
ビジョンサイドカー計画があるときに限り、メイン呼び出し**前に**各画像を説明したテキストに差し替えます。
-計画が利用できない場合は、生の画像をテキスト専用バックエンドへ転送せず削除します。モデルカタログは
-サイドカー対象の各モデルに画像入力を広告します。コンボは、すべてのメンバーがネイティブまたはサイドカーを
+計画が利用できない場合は、生の画像をテキスト専用バックエンドへ転送せず削除します。`visionSidecar.enabled: false` の場合、
+モデルカタログはカスタムの項目やコンボのメンバーを含むテキスト専用モデルに、サイドカーによる画像入力を追加しません。
+ネイティブの画像対応と明示的に設定された画像モダリティは維持されます。`enabled` を省略した場合、サイドカーによる
+画像入力の広告は引き続き有効です。カタログに表示されることは、サイドカーの認証情報や処理容量が利用可能であることを
+保証しません。コンボは、すべてのメンバーがネイティブまたは有効化されたサイドカーを
通じて画像を受け入れ、かつコンボの `imageInput` 設定が無効でない場合にのみ画像入力を広告します。これにより
Codex アプリなどのクライアントは、サイドカー実行前に添付をブロックせず許可できます。`visionSidecar.model` が未設定または空の場合、
OpenAI 実行経路、ダッシュボード、管理 API は `gpt-5.6-luna` をフォールバックとして使います。起動時には
@@ -137,6 +140,8 @@ OpenAI 実行経路、ダッシュボード、管理 API は `gpt-5.6-luna` を
`PUT /api/sidecar-settings` は同じフィールドを受け付けます。部分更新では省略したキーをそのまま残します。`timeoutMs` はランタイムの整数範囲(1–2147483647 ms)を使います。
+`PUT /api/sidecar-settings` によりビジョンの実効的なオン/オフ状態が変わると、保存後に管理対象のカタログとキャッシュが更新されますが、`catalogRefreshPending=true` は更新が完了しなかったことを示し、クライアントによる受領確認を意味しません。
+
Web 検索サイドカーのカードも同じ構成です。モデルピッカーの先頭行が **オフ (Off)** です。オフにすると
OpenCodex は `web_search` への介入をやめ、Codex 統合は `~/.codex/config.toml` に
`web_search = "disabled"` を書き込みます。Codex は自身のモードがそうなるまでネイティブの
diff --git a/docs-site/src/content/docs/ja/guides/subagent-v1-default.md b/docs-site/src/content/docs/ja/guides/subagent-v1-default.md
index 7cdae1cc726..ab3814d4e71 100644
--- a/docs-site/src/content/docs/ja/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/ja/guides/subagent-v1-default.md
@@ -94,3 +94,9 @@ ocx v2 mode v1
```
CLI は確認を求めません。同じ設定を変更するため、このページの内容を理解したうえで選んでください。
+
+## 子エージェントの同時実行上限
+
+**Models → Model settings** のサブエージェント上限設定では、主エージェントを除く子エージェントの同時実行数の上限を指定します。v1、base、v2 のほか、グローバル v2 フラグが無効なハイブリッドモードでも利用できます。サーフェスのモードは、この上限とは別に選択します。
+
+上限を増やすとより多くの子を許可できますが、自動生成や高速化を保証するものではありません。変更後は新しい Codex セッションを開始してください。長時間動作している App ホストで以前の上限が使われ続ける場合は、そのホストを再起動してください。
diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md
index deead3c02df..c766a1cdfaf 100644
--- a/docs-site/src/content/docs/ja/reference/cli.md
+++ b/docs-site/src/content/docs/ja/reference/cli.md
@@ -20,7 +20,7 @@ opencodex CLI は `ocx` です。最初のコマンド名でディスパッチ
管理コマンドは、2 番目の構成パスを維持するのではなく、記録されたランタイム ポートと ID チェックを使用して、稼働中のプロキシの管理 API をラウンドトリップします。停止したプロキシまたは到達不能なプロキシは HTTP 503 として表され、ゼロ以外の CLI 終了が生成されます。オフライン構成操作として明示的に文書化されているコマンドは、代わりに、稼働中のプロキシを使用せずに設定ファイルを検証および編集できます。
-`ocx system codex-cli-update check` は稼働中のプロキシを必要とせず、パッケージレジストリにも問い合わせません。設定済みのインストール候補について、秘匿化された実行ファイルの場所や所有権を示す根拠を含む来歴メタデータを、範囲を限定して検査します。公開ランチャー由来の信頼済みコンテキストが真正性を裏付けるのは候補のスナップショットだけであり、Codex が正常に実行されたことではありません。この単発コマンドは Codex を一切実行しないため、環境または永続化された状態から得た候補は報告対象にとどまります(`managed: false`、通常は `selection_unattested`)。`selectionAttested` は常に `false` です。JSON 出力には `candidateAvailable`、`candidateVersion`、`candidateSource`、`selectionAttested: false` が含まれます。Bun またはソースから直接起動するとランチャーの証明がないため、環境由来および永続化された候補を無視し、POSIX では `candidate_unavailable`、Windows では `windows_inspection_deferred` を報告することがあります。Windows では、この最初のスライスは候補や構成のパスに対するファイルシステム I/O を一切行いません。信頼済みランチャーが取り込んだ絶対パスの環境候補だけを、アプリ同梱またはバージョンマネージャーとして字句的に報告でき、それ以外の Windows 候補はすべて失敗時閉鎖になります。このコマンドはソフトウェアのインストールや修復、Codex または npm の実行、稼働中プロセスの制御、設定やキャッシュ状態への書き込みを行いません。
+`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
Windows x64 インストールの観測は [`attest` コマンド](/ja/reference/cli/agents/)を参照してください。明示パスを省略すると、証明にバインドされたランチャースナップショットが特定した選択候補を観測します。更新権限やランタイム選択は証明しません。
diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md
index 9c891b8b913..ebe43edfffc 100644
--- a/docs-site/src/content/docs/ja/reference/cli/agents.md
+++ b/docs-site/src/content/docs/ja/reference/cli/agents.md
@@ -204,7 +204,9 @@ opencode は `{env:OPENCODEX_OPENCODE_API_KEY}` を補間します。opencodex
ヘッドレス ランタイムの設定、起動、同期、診断、更新を管理します。
-`ocx system codex-restart --yes` は `ocx sync --restart-codex` と同じモジュールで Codex app-server を再起動し、デスクトップ アプリも完全に終了して再起動します。プロキシ自体が Codex アプリ内で動いている場合、完了できない引き渡しを約束せず、実行可能な案内とともに拒否します。
+`ocx system codex-restart --yes` は `ocx sync --restart-codex` と同じモジュールで Codex app-server を再起動します。モデル ピッカーは起動時に構築されるため、デスクトップ アプリを完全に終了して再起動するのは、現在のモデル カタログが書き込まれる前にアプリが起動していた場合、またはこのリクエストで `model_catalog_json` の参照先が変わった場合です。それ以外の場合、アプリは開いたまま自身の app-server を再起動し、未送信の下書きや保留中の承認を保持します。アプリまたはカタログの状態を読み取れない場合は完全な再起動を行います。プロキシ自体が Codex アプリ内で動いている場合、デスクトップの再起動は完了できない引き渡しを約束せず、実行可能な案内とともに拒否します。
+
+このリクエストで `model_catalog_json` の参照先が変わった場合も、シェルがカタログファイルの書き込み後に起動していてもデスクトップを再起動します。
```bash
ocx system settings --stream-mode eager-relay
@@ -216,7 +218,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` はパッケージレジストリに問い合わせず、設定済みのインストール候補について、秘匿化された実行ファイルの場所や所有権を示す根拠を含む来歴情報を、範囲を限定して検査します。公開ランチャー由来の信頼済みコンテキストが真正性を裏付けるのは候補のスナップショットだけであり、Codex が正常に実行されたことではありません。この単発コマンドは Codex を一切実行しないため、環境または永続化された状態から得た候補は報告対象にとどまります(`managed: false`、通常は `selection_unattested`)。`selectionAttested` は常に `false` です。JSON 出力には `candidateAvailable`、`candidateVersion`、`candidateSource`、`selectionAttested: false` が含まれます。Bun またはソースから直接起動するとランチャーの証明がないため、環境由来および永続化された候補を無視し、POSIX では `candidate_unavailable` を報告することがあります。Windows では、この最初のスライスは候補や構成のパスに対するファイルシステム I/O を一切行いません。信頼済みランチャーが取り込んだ絶対パスの環境候補だけを、アプリ同梱またはバージョンマネージャーとして字句的に報告でき、それ以外の Windows 候補はすべて失敗時閉鎖になります。このスライスは永続化された選択状態を一切読み取らないため、環境候補がキャプチャされていない Windows 実行では `candidate_unavailable` ではなく `windows_inspection_deferred` を報告します。コマンドは Codex CLI が導入されているかどうかを観測できないので、候補が存在しないと断定せず、検査が延期されたことを報告します。このコマンドは Codex やパッケージマネージャーの実行、shim の修復、設定やキャッシュ状態への書き込み、プロセスの停止、インストールを行いません。アプリ同梱、認識済みのバージョンマネージャー、未検証のスタンドアロン、曖昧な shim の各候補は管理対象外または不明として報告され、管理対象と判定されることはありません。
+`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
Windows で `CODEX_CLI_PATH=codex` のような単純なコマンド名、リモートパス、デバイスパスが候補としてキャプチャされた場合は、`candidate_path_unavailable` を報告します。候補は取得されていますが、そのパスはこの検査の対象になりません。
diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
index 84acd32d7f1..df2d6de16f6 100644
--- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
@@ -63,7 +63,7 @@ ocx eject back
### `ocx recover-history --ocx-compaction --yes`
-ルーティングされたプロバイダーで圧縮されたタスクをネイティブ Codex で再開する前に、その履歴を修復します。このコマンドは UUID で 1 つのタスクだけを選択し、非公開のバイト単位バックアップを保存してから、OpenCodeX 所有の `ocx1:` 圧縮状態だけをネイティブ Codex が再生できる通常の要約に変換します。ネイティブの暗号化コンテンツと他のタスクは変更しません。実行前に対象タスクを閉じてください。処理中に rollout が変更された場合、ファイルを置き換えずに修復を中止します。
+ルーティングされたプロバイダーで圧縮されたタスクをネイティブ Codex で再開する前に、その履歴を修復します。このコマンドは UUID で 1 つのタスクだけを選択し、非公開のバイト単位バックアップを保存してから、OpenCodeX 所有の `ocx1:`/`ocx2:` 圧縮状態だけをネイティブ Codex が再生できる通常の要約に変換します。ネイティブの暗号化コンテンツと他のタスクは変更しません。実行前に対象タスクを閉じてください。処理中に rollout が変更された場合、ファイルを置き換えずに修復を中止します。
### `ocx uninstall`・`ocx remove`
@@ -156,6 +156,8 @@ ocx status --json
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+再起動フラグを明示した場合、カタログとキャッシュのバイト列が変わらなくても、`config.toml` の変更が正常にコミットされれば再起動します。設定だけを扱う操作が変更なし、拒否、事前検証、ロールバックとなった場合は再起動しません。再起動フラグがなければ、sync が Codex を自動的に再起動することはありません。
+
構成されているすべてのプロバイダーからライブ モデル リストを取得し、マージされたカタログを Codex に再挿入します。プロバイダーを追加した後、または利用可能なモデルを更新するために実行します。
存続期間の長い Codex `app-server` プロセスがまだ実行されている場合、`ocx sync` は、`opencodex-catalog.json` / `models_cache.json` が更新されても、以前のメモリ内モデル リストを提供し続ける可能性があることを警告します。`--restart-codex` を渡すと、一致する `codex … app-server` および `codex-code-mode-host` プロセスを再起動し、さらに macOS、Linux、Windows で Codex デスクトップ アプリを完全に終了して再起動します。モデル ピッカーがカタログを読み直すためです。進行中の会話は終了します。広範な `pkill -f codex` 一致は意図的に回避されます。
diff --git a/docs-site/src/content/docs/ja/reference/configuration/advisor.md b/docs-site/src/content/docs/ja/reference/configuration/advisor.md
index c80ac5da93b..d78a179f231 100644
--- a/docs-site/src/content/docs/ja/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/ja/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: アドバイザー
-description: OpenCodex 自身のエキスパート相談サイドカー — 設定されたエキスパートモデルがルーティングされたワーカーに助言を返します。manual と preflight の 2 つのポリシーを提供します。
+description: manual、preflight、証拠に基づく adaptive ポリシーによる独立した専門家への相談。
---
アドバイザーはワーカーのタスクをレビューし助言を返す独立したエキスパートモデルです。相談は OpenCodex がエンドツーエンドで所有します。プロキシはワーカーのターンに合成 `advisor` ツールを注入し、通常のルーティング権威を通じて相談を自ら実行し、助言を再注入して元のワーカーを継続させます。ワーカーは委譲も spawn もせず、プロバイダー資格情報も持ちません。
@@ -9,6 +9,8 @@ description: OpenCodex 自身のエキスパート相談サイドカー — 設
## 設定
+ダッシュボードの **Advisor** ページでカタログからモデルを選択します。ネイティブ、プロバイダー、アカウント指定、カスタム、コンボの識別子をそのまま保存します。無効なモデルと初期選択待ちのモデルは新規選択に表示されません。選択可能な一覧にない保存済み識別子は警告付きで保持され、別のモデルを選ぶか解除するまで変更されません。読み込みに失敗しても選択は保持され、再試行できます。テキストの助言を返せるモデルを選んでください。カタログは相談の互換性を保証しません。CLI と API は引き続きルーター対応のモデル文字列を受け付けます。
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ description: OpenCodex 自身のエキスパート相談サイドカー — 設
| `enabled?` | `boolean` | `false` | マスタースイッチ。無効ならリクエストパスにアドバイザーの動作は一切ありません。 |
| `model?` | `string` | — | エキスパートモデル。ルーターが受け付ける任意のモデル文字列:ネイティブモデル(`gpt-6-astra`)、明示的な `provider/model`(`anthropic/claude-sonnet-4-6`、`xai/grok-...`)、アカウント修飾ネイティブモデル。クロスプロバイダーを完全にサポートします。 |
| `effort?` | `string` | `"max"` | アドバイザー呼び出しの推論強度(`low`〜`ultra`)。 |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | 相談のタイミング。 |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | 相談のタイミング。 |
+| `maxConsultationsPerTask?` | `integer` | `3` | 適応型タスクの相談上限(1〜3回、手動呼び出しを含む)。 |
| `timeoutMs?` | `number` | `120000` | ループバック相談のタイムアウト。 |
| `contextSharingConsent?` | `"v1"` | なし | 設定されたアドバイザーのプロバイダーへタスク文脈を送ることへの操作者の同意。現在有効な値は `"v1"` だけです。欠落、古い値、その他の値ではタスク内容は送られません。`enabled: true` だけでは同意になりません。 |
-ダッシュボードの **Advisor** ページまたは `ocx advisor status|on|off|set --model --effort --policy ` で管理します。
+ダッシュボードの **Advisor** ページまたは `ocx advisor status|on|off|set --model --effort --policy ` で管理します。
現在の同意がないとき、`ocx advisor on` はプロバイダー間送信を有効にしません。開示を表示して停止します。`ocx advisor on --ack-context-sharing` と `ocx advisor consent` が `v1` を記録します。`ocx advisor consent --revoke` は同意を消し、送信を直ちに止めます。`ocx advisor set` は同意を与えません。ダッシュボードの同意チェックは初期状態でオフです。
@@ -78,5 +81,13 @@ OpenCodex はプロバイダー API キー、Authorization ヘッダー、OAuth
## PR1 の制限
- ネイティブ OpenAI パススルーのターン(ChatGPT プールのワーカー)には合成ツールが注入されません。アドバイザーはルーティング(翻訳)プロバイダーを対象とします。preflight 相談は run-turn アダプターに適用されますが、ツールは適用されません。
-- 適応トリガーはありません:詰み検出、繰り返し失敗の分析、エスカレーション階層、複数アドバイザー、投票はありません。`manual` と `preflight` のみです。
+- 意味的な停滞検出、完了前レビュー、段階的エスカレーション、複数専門家、投票は行いません。
- preflight の重複排除台帳はプロセス内です。プロキシ再起動後、進行中のタスクはもう一度 preflight 相談を受けることがあります。
+
+## 証拠に基づく適応型相談と活動記録
+
+`adaptive` は初回相談に加え、同じ作業ディレクトリの同じ検証が失敗し、その後のファイル編集が成功し、新たな検証実行も失敗した場合に相談します。成功した検証は失敗状態を解除します。編集前に開始した検証は修正後の再試行に数えません。初回と修正失敗は別々に重複を防ぎ、自動相談には安定した会話 ID が必要です。対応するツール呼び出しと結果、および明示的な終了コードが必要です。複雑なスクリプト、バッチ、shell による編集、曖昧な出力では発火しません。
+
+`ocx advisor set --policy adaptive --max-consultations 2` で設定します。手動と自動の試行はタスクごとに1〜3回の予算を共有し、送信前に `OPENCODEX_HOME/advisor-budget.sqlite` へ予約します。プロキシ再起動や Worker 変更でリセットされません。タスクのダイジェスト、回数、有効期限のみ保存し、最初の試行から24時間で期限切れになります(最大512タスク)。予算が使えない場合も Worker は継続します。送信済みの失敗・キャンセルには費用が発生する可能性があるため返却しません。会話 ID がなければリクエスト単位の手動相談のみです。
+
+Advisor ページと `ocx advisor activity --json`(`GET /api/advisor/activity`)は、このプロセスの24時間以内の最新100件のメタデータを表示します。画面が表示されている間は5秒ごとに更新します。理由、状態、モデル、所要時間、報告された入出力トークン、要求値とアダプターの推論変換値を表示します。本文、コマンド、助言、会話 ID は保存せず、再起動時に消えます。変換値は送信パラメーターの証拠であり、モデル内部の推論を保証しません。未報告は不明のままとし、採用状況や正確な費用は推測しません。
diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md
index 23e2efd7611..d80b0b82386 100644
--- a/docs-site/src/content/docs/ja/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md
@@ -89,7 +89,9 @@ account を削除しても mapping は保持され、同じ id を再追加す
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`、`openai-responses`、`anthropic`、`google`、`kiro`、`cursor`、`ollama-native`、`azure-openai` (または別名 `azure`) のいずれか。 |
| `baseUrl` | `string` |アップストリーム API のベース URL。ほとんどの組み込み固定エンドポイントは不一致を無視します。衝突安全キー プリセットは、古い同じ名前のカスタム宛先を保持します。 |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | 上流の使用量、請求、レート制限表示とは別の、クライアント側の送信開始間隔調整です。`maxConcurrentRequests` は実行中リクエスト数を制限する正の整数で、プロバイダー規則またはモデル規則だけに設定できます。プロバイダー制限は全モデルに適用され、`models` は上流の正確なモデル ID に一致し、遅延を増やすか同時実行数を減らします。キュー待機は応答ヘッダーのタイムアウトを消費しません。HTTP と明示的なアダプターの `fetchResponse`/`runTurn` 送信を対象にします。同時実行数の上限がある場合、標準の Responses WebSocket ターンは応答本文の完了・エラー・キャンセル時に枠を解放できるよう HTTP/SSE を使います。 Cursor を含む `runTurn` アダプターでは、上限は物理送信数ではなく進行中のターン数を数えます。同じターンの RunSSE と BidiAppend は重なり得ますが、別のターンは待機します。後続送信にも開始間隔は適用されます。 |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 上流の使用量、請求、レート制限表示とは別の、クライアント側の送信開始間隔調整です。`maxConcurrentRequests` は実行中リクエスト数を制限する正の整数で、プロバイダー規則またはモデル規則だけに設定できます。プロバイダー制限は全モデルに適用され、`models` は上流の正確なモデル ID に一致し、遅延を増やすか同時実行数を減らします。キュー待機は応答ヘッダーのタイムアウトを消費しません。HTTP と明示的なアダプターの `fetchResponse`/`runTurn` 送信を対象にします。同時実行数の上限がある場合、標準の Responses WebSocket ターンは応答本文の完了・エラー・キャンセル時に枠を解放できるよう HTTP/SSE を使います。 Cursor を含む `runTurn` アダプターでは、上限は物理送信数ではなく進行中のターン数を数えます。同じターンの RunSSE と BidiAppend は重なり得ますが、別のターンは待機します。後続送信にも開始間隔は適用されます。 |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 通常リクエストのキー選択です。既定の `active` は保存済みキーと既存の障害時切替を維持します。`round-robin` は保存済み選択を変えず、利用可能なプールのキーを順に割り当てます。トークンや料金ではなく割当回数を分散します。 |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | キー参照ごとの共通 RPM・開始間隔・同時実行上限です。全モデルで共有し、プロバイダーとモデルの上限も同時に適用します。同一参照は枠を共有しますが、同じ上流プロジェクトの別参照は独立クォータを意味しません。TPM と日次上限は管理しません。 |
| `responsesPath?` | `string` |キー認証 `openai-responses` リクエストの相対リソース パス。 `/` で始まり、スキーム、クエリ、またはフラグメントが含まれていない必要があります。 |
| `chatCompletionsPath?` | `string` | `openai-chat` リクエストの相対リソース パス。 `responsesPath` の対となる設定で、同じ形式ルールが適用されます。1つのアップストリームが Chat Completions と Responses を異なるプレフィックスで提供する場合に必要です。モデルごとの wire override はアダプターのみを変更し `baseUrl` は変更しないため、この設定がないと有効化された Chat リクエストが Responses ベースへ送信されます。同梱例は Z.AI です。 |
| `upstreamWebsocket?` | `boolean` | `openai-responses` リクエストで使用するアップストリーム Responses WebSocket トランスポート(既定値は無効)。ファーストパーティの `https://api.openai.com/v1` アップストリームでのみ有効です。カスタムプロバイダーのエンドポイントは常に制限付き HTTP/SSE を使用します。Bun はメッセージ全体を確保する前に受信 WebSocket メッセージのサイズ上限を適用できないためです。正規の ChatGPT `openai` プロバイダーでは、省略すると対象となるターンでアップストリーム WebSocket を使用し、`false` はストリーミングのターンを HTTP/SSE で送信し、`true` は拒否されます。`false` の間はネイティブのターン途中のステアリングとインジェクションを利用できません。このフィールドはクライアント側の `websockets` 設定とは独立しており、エンドポイントと認証情報のどちらも変更しません。HTTP のベース URL は SSE のままとなり、Responses 以外のパスと `openai-chat` リクエストは HTTP を使用します。 |
@@ -112,7 +114,7 @@ account を削除しても mapping は保持され、同じ id を再追加す
| `modelAutoCompactTokenLimits?` | `Record` | モデルごとの正の安全な整数によるソフト自動圧縮予算。実効値であるコンテキストまたは最大入力の 90% の上限を下げることだけができ、有効なコンテキストウィンドウを解決できない場合は出力されません。canonical `openai` では、キーは provider や account-selector の接頭辞を含まない、サポート対象の正確なネイティブモデル ID でなければなりません。provider PATCH はエントリをマージし、キーを `null` にするとそのキーを削除し、フィールド全体を `null` にするとマップを消去します。これらの `null` tombstone は PATCH 専用です。 |
| `defaultMaxOutputTokens?` | `number` |クライアントが `max_output_tokens` を省略した場合の、プロバイダー全体の `openai-chat` フォールバック。 |
| `modelMaxOutputTokens?` | `Record` |モデルごとの `openai-chat` フォールバック バジェットがプラスになります。正確な/パターン一致はプロバイダーのデフォルトを上回ります。 |
-| `modelCosts?` | `Record` | モデルごとの表示価格(100万トークンあたりの米ドル)。そのプロバイダーの正確なアップストリーム モデル ID をキーにします(プロバイダー識別子やルーティングされた `provider/model` ラベルではありません)。値は `input`, `output`, `cacheRead`, `cacheWrite` の 4 フィールドです(例: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。組み込みカタログにないモデル ID も、任意の OpenAI 互換エンドポイントを対象とするカスタムプロバイダーや、ローカル・内部プロバイダーで有効です。ユーザー設定の価格は Logs の `~$` と Usage の見積もりで組み込みカタログより優先されます。過去のエントリも現在のオーバーレイで再計算されるため、価格を編集すると過去の合計が変わることがあります(フォールバック順: ユーザー設定 → jawcode カタログ → expected-price オーバーレイ → モデル別ベンダー価格)。ユーザーが明示的に全レートを 0 にした場合は、既知のゼロ料金として見積もります。自動料金に戻すにはそのモデルの設定を削除してください。カタログの全ゼロ料金は引き続きフォールバックします。各レートは 0 以上の有限数で、最大 1,000,000(100万トークンあたりの米ドル)です。範囲外の行は管理境界で拒否され、読み込み時に破棄されます。表示専用の見積もりであり、ルーティング・アカウント選択・クォータ・請求には影響しません。 |
+| `modelCosts?` | `Record` | モデルごとの表示価格(100万トークンあたりの米ドル)。そのプロバイダーの正確なアップストリーム モデル ID をキーにします(プロバイダー識別子やルーティングされた `provider/model` ラベルではありません)。値は `input`, `output`, `cacheRead`, `cacheWrite` の 4 フィールドです(例: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。組み込みカタログにないモデル ID も、任意の OpenAI 互換エンドポイントを対象とするカスタムプロバイダーや、ローカル・内部プロバイダーで有効です。ユーザー設定の価格は Logs の `~$` と Usage の見積もりで組み込みカタログより優先されます。過去のエントリも現在のオーバーレイで再計算されるため、価格を編集すると過去の合計が変わることがあります(フォールバック順: ユーザー設定 → jawcode カタログ → expected-price オーバーレイ → モデル別ベンダー価格)。ユーザーが明示的に全レートを 0 にした場合は、既知のゼロ料金として見積もります。自動料金に戻すにはそのモデルの設定を削除してください。カタログの全ゼロ料金は引き続きフォールバックします。各レートは 0 以上の有限数で、最大 1,000,000(100万トークンあたりの米ドル)です。範囲外の行は管理境界で拒否され、読み込み時に破棄されます。表示専用の見積もりであり、ルーティング・アカウント選択・クォータ・請求には影響しません。 |
| `headers?` | `Record` |追加の上流ヘッダー。認証、Cookie、API キー ヘッダー、埋め込まれた改行、および無効な名前は拒否されます。 |
| `openRouterRouting?` | `OpenRouterProviderRouting` |デフォルトの OpenRouter `order`、`only`、および `allowFallbacks` 設定。 `openai-chat` を持つ正規 OpenRouter に対してのみ有効です。 |
| `modelOpenRouterRouting?` | `Record` |プロバイダー全体の OpenRouter 設定を置き換える正確なモデル ID のオーバーライド。 |
@@ -562,3 +564,8 @@ Vercel AI Gateway は、1 つのモデルを複数の基盤となる推論プロ
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes` は Anthropic OAuth の保存済みアカウント ID をモデルに割り当てます。有効なプールでは、大文字小文字を区別する `match` グロブの最初の一致が初回選択と 429 再試行を制限します。`fallback: true` はルート内に適格なアカウントがない場合だけ通常のプールを使います。
+
+
+### `promptLengthPricing`
+
+省略可能な `promptLengthPricing` は4つの基本料金に追加します。`mode: "automatic"`(または省略)はカタログのコンテキスト料金規則を維持し、`mode: "flat"` はその規則を無効にします。`mode: "custom"` では、`thresholdTokens` に正の安全な整数、`comparison` に `"gt"`(>)または `"gte"`(≥)、`rates` に4つの絶対額の代替料金を指定します。判定にはキャッシュの読み書きを含む全プロンプトトークンを使い、出力は含めません。選択された料金は出力を含むリクエスト全体に適用され、しきい値の上下どちらでもカタログの規則を置き換えます。Fast/Priority 調整は料金の選択後に1回だけ適用され、そのモデルについてカタログが公開している Priority との関係に従います。推定値が下限として表示されるのは、カタログがその関係を `lower-bound` と宣言している場合(現在は xAI Grok)だけです。カタログに規則がないモデルでは、Priority 倍率は1回だけ適用されます。**Models → Price** で設定できます。**Reset to automatic** は基本料金と規則を含む上書き全体を削除します。既存設定との互換性は維持され、表示される推定料金だけに関する設定です。
diff --git a/docs-site/src/content/docs/ja/reference/proxy-formats.md b/docs-site/src/content/docs/ja/reference/proxy-formats.md
index aa05232953d..50ddb2fe2ca 100644
--- a/docs-site/src/content/docs/ja/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/ja/reference/proxy-formats.md
@@ -35,6 +35,8 @@ Chat Completions と Messages の各エンドポイントは遅延ツールカ
これとは別に、すべての受信ワイヤで、プロバイダーの `default.` 名前空間の呼び出しはリクエストが宣言した素のツール名に正規化されます(`default.Read` → `Read`)。
+ネイティブ Messages では、`tools: []` と併用されたシステムメッセージ内のインラインツール定義も認識します。その名前は恒久的に削除されるまで呼び出せますが、無関係なクライアントツールは許可されません。ホスト型 MCP 呼び出しとサーバーツールの引数ストリームはネイティブ形式を維持します。
+
## エンドポイントの概要
|クライアントサーフェス |エンドポイント |非ストリームの結果が成功 |成功したストリームまたはソケットの結果 |
diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md
index 2c06b538f7f..d92ce979391 100644
--- a/docs-site/src/content/docs/ko/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init`은 다음 과정을 안내합니다:
-1. **프로바이더 선택** — 내장 레지스트리 프리셋 104개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다.
+1. **프로바이더 선택** — 내장 레지스트리 프리셋 105개 중 하나를 고르거나 `custom`을 선택해 base URL과 adapter를 직접 입력합니다.
2. **API 키** — 키를 붙여넣거나 `${ANTHROPIC_API_KEY}` 같은 환경 변수를 참조합니다.
3. **기본 모델** — 키, 로컬, custom 프로바이더에서는 프리셋을 그대로 쓰거나 모델 ID를 직접 입력합니다.
4. **프록시 포트** — 기본값은 `10100`입니다.
diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md
index f4a6f3354ec..14ebff76cba 100644
--- a/docs-site/src/content/docs/ko/guides/claude-code.md
+++ b/docs-site/src/content/docs/ko/guides/claude-code.md
@@ -192,21 +192,21 @@ unknown은 설정이 아직 opencodex 프록시를 가리키는지 판단할 수
### Picker 모드: 1P Code 탭에 opencodex 모델 표시하기
-Picker 모드는 1P 모드의 일부예요. macOS에서 1P를 선택하면 기본으로 켜지지만,
+Picker 모드는 1P 모드의 일부예요. macOS와 Windows에서 1P를 선택하면 기본으로 켜지지만,
`claudeCode.intercept.picker: false`를 설정하면 꺼져요. 1P Desktop의 Code 탭 모델 선택기를 바꿔서
-사용 가능한 opencodex 모델을 이름으로 보여줘요. 처음 켤 때 macOS 로그인 키체인에서 로컬 인증 기관을
+사용 가능한 opencodex 모델을 이름으로 보여줘요. 처음 켤 때 macOS 로그인 키체인이나 Windows 현재 사용자 Root 저장소에서 로컬 인증 기관을
신뢰하라는 메시지가 표시될 수 있어요. 이 인증 기관은 `claude.ai`와 그 하위 도메인으로 제한돼요.
내보낼 수 있는 서명용 인증서와 키는 OS 자격 증명 저장소로 보호하며, 일반적인 재시작에서는 같은 것을 재사용해요.
OpenCodex 설정 디렉터리에 Picker 서명 키를 평문으로 저장하지 않아요. 제한된 CA의 전체 검증과 OS 신뢰 확인은
-계속 적용돼요. 승인한 인증서와 키가 같고 자격 증명 저장소를 사용할 수 있다면 재시작 시 인증서 신뢰 설정을
-추가하거나 삭제하지 않아요. 시작 시 복원은 신뢰를 설치하지 않아요. 신뢰가 없거나 철회됐거나 확인할 수 없다면
+계속 적용돼요(Windows에서는 정확한 지문과 확인된 리프로 검증해요). 승인한 인증서와 키가 같고 자격 증명 저장소를 사용할 수 있다면 재시작 시 macOS 인증서 신뢰 설정을
+추가하거나 삭제하지 않아요. 시작 시 복원이나 상태 확인은 신뢰를 설치하지 않아요. 신뢰가 없거나 철회됐거나 확인할 수 없다면
Picker는 대기 상태로 남아요. `ocx claude desktop picker on` 또는 `ocx claude desktop picker trust`를 직접 실행해
-신뢰를 부여하면 돼요.
+신뢰를 부여하면 돼요(Windows에서는 확인 창이 나올 수 있어요).
이전 인증서에서 한 번 마이그레이션할 때는 기존 신뢰를 제거하기 위한 동의가 필요할 수 있어요. 정리가 끝나지
않으면 Picker는 사용할 수 없고 적용된 프로필은 복호화하지 않는 중계로 동작해요. 키체인 잠금 해제나 앱의
-자격 증명 접근 허용은 별도의 macOS 동작이며, 재시작이나 업데이트 때도 메시지가 나올 수 있어요.
-Windows와 Linux에서는 Picker를 지원하지 않으며 Picker CA, 자격 증명 저장소, 프록시 작업을 시작하지 않아요.
+자격 증명 접근 허용은 별도의 macOS 동작이며, 재시작이나 업데이트 때도 메시지가 나올 수 있어요. Windows에서는 `on`이나 `trust`를 직접 실행할 때 루트 추가 확인이 나올 수 있어요.
+Linux에서는 Picker를 지원하지 않으며 Picker CA, 자격 증명 저장소, 프록시 작업을 시작하지 않아요.
기본 Claude 인터셉트는 계속 사용할 수 있고, 로컬 CA 파일에 소유자, 심볼릭 링크, 파일 권한, Windows ACL 검사를 적용해요.
Picker 모드가 켜져 있는 동안 Claude Desktop의 네트워크는 OpenCodex를 거쳐요. OpenCodex가 중단되면
@@ -575,7 +575,7 @@ Messages → Responses로 변환되는 요청에서는 Claude Code의 `/effort`
| 사용자 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 접두사) |
| 도구 결과의 `tool_reference` | 짝지어진 결과에 `Tool loaded: ` 텍스트로 보존합니다. 도구 선언을 추가하거나 번역 경로의 서버 측 지연 로딩을 활성화하지는 않습니다. |
| `thinking` / `redacted_thinking` 재생 | 서명과 비공개 페이로드를 제한된 `ocxr1` 봉투에 담은 `reasoning` 항목 |
-| Function 도구 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) |
+| Function 도구 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`). `api.anthropic.com`으로 번역하여 보내는 경로에서는 타입이 지정된 `web_search_*`와 `web_fetch_*` 선언도 Anthropic에 전달되어 서버에서 실행됩니다. 웹 검색 사이드카가 `web_search`를 제공하면 사이드카가 우선합니다. 검색과 가져오기 결과는 `server_tool_use` 및 대응하는 `web_search_tool_result` / `web_fetch_tool_result` 블록으로 클라이언트에 반환됩니다 |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, 이름 지정 함수→`{type:"function",name}`, 호스팅 WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
diff --git a/docs-site/src/content/docs/ko/guides/codex-app-models.md b/docs-site/src/content/docs/ko/guides/codex-app-models.md
index 22a2035c24f..899ffc19752 100644
--- a/docs-site/src/content/docs/ko/guides/codex-app-models.md
+++ b/docs-site/src/content/docs/ko/guides/codex-app-models.md
@@ -198,11 +198,19 @@ opencodex의 Fast 모드를 **Auto**로 유지하세요(`fastMode` 생략). Fast
목록이 남아 있다면 Codex 데스크톱을 다시 시작한 뒤 라우팅 모델의 속도 메뉴에서 **Ultra Fast**를
선택하세요. 기본 속도는 Standard로 유지됩니다.
-사용 가능한 `service-tier` 전송 경로와 명시적인 `ultrafast → ultrafast` 매핑이 있어야 합니다.
-일반 Fast 지원, GPT와 비슷한 이름, 네이티브·계정별 항목, combo 별칭만으로는 추가되지 않습니다.
-매핑을 제거하거나 모델의 service-tier 지원을 끄면 다음 동기화 성공 시 카탈로그와 캐시에서
-자동 생성된 선택지도 제거됩니다. 별도의 `ultraFastTier` 설정은 기존처럼 사용자가 직접 넣은
-카탈로그 tier를 보존하는 용도입니다.
+이 선택지를 라우팅 항목에 자동 생성하려면 사용 가능한 `service-tier` 전송 경로와 명시적인
+`ultrafast → ultrafast` 매핑이 있어야 합니다. 일반 Fast 지원이나 GPT와 비슷한 이름만으로는
+추가되지 않습니다. 이 라우팅 매핑은 네이티브·계정별 네이티브 항목이나 combo 별칭에는
+적용되지 않습니다. 매핑을 제거하거나 모델의 service-tier 지원을 끄면 다음 동기화 성공 시
+카탈로그와 캐시에서 자동 생성된 라우팅 선택지도 제거됩니다.
+Codex가 HTTP로 모델 목록을 불러올 때도 현재 적용 중인 설정의 동일한 선언이 Ultra Fast 선택지를 결정합니다.
+
+라우팅 항목에서는 `ultraFastTier`가 `provider/model` 식별자가 정확히 일치하는 저장된 항목에 사용자가
+별도로 넣은 Ultra Fast tier를 보존합니다. 자동 생성된 라우팅 tier는 사용자 제공 tier로
+취급하지 않습니다. bare native `gpt-6-astra` / `gpt-6.1-sol` 항목과 신뢰된 계정별 복제
+항목에서는 `ultraFastTier`를 켜면 네이티브 기본 tier를 바꾸지 않고 누락된 Ultrafast 선택지도
+생성합니다. 끄면 자동 생성한 것으로 추적되는 네이티브 tier 메타데이터만 제거하며,
+사용자가 별도로 넣거나 수정한 네이티브 tier 항목은 보존합니다.
[OpenAI API 문서](https://developers.openai.com/api/docs/guides/ultrafast-mode)는 GPT-6 Astra와
GPT-6.1 Sol의 Ultrafast 지원을 안내합니다. API 사용 권한과
diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md
index c9ccf3757b3..a9a69b76b5a 100644
--- a/docs-site/src/content/docs/ko/guides/combos.md
+++ b/docs-site/src/content/docs/ko/guides/combos.md
@@ -132,7 +132,7 @@ ocx combo set balanced \
| 인증, 구독, 쿼터, 속도 제한, 과부하, 또는 상위 서버 오류로 분류됨 | 상태 코드만으로는 충분하지 않더라도 대상을 쿨다운으로 보내고 넘어갑니다. |
| 클라이언트 취소(499), `origin_rejected`, cyber-policy refusal, context overflow, 또는 기타 invalid request | 멈추고 오류를 반환합니다. 다른 대상을 써도 요청이 유효해지지 않기 때문입니다. |
| ChatGPT 계정으로 Codex를 사용할 때 해당 모델을 지원하지 않는다는 정확한 HTTP 400 거부 | 출력이 시작되기 전에 다음 설정 대상으로 넘어갑니다. 거부 문장 전체가 `detail`, `error.message` 또는 일반 텍스트에 있어야 하며, `detail`과 `error`가 충돌하면 중단합니다. 이 사유만으로 계정을 재인증 상태로 바꾸거나 공급자의 모든 모델에 쿨다운을 걸지는 않습니다. |
-| `user`를 명시적으로 거부하거나, `reasoning.effort`/`reasoning_effort`의 지원되지 않는 값 또는 모델별 이미지 입력 거부(`param: input`)를 나타내는 구조화된 HTTP 400 | 출력 시작 전에 쿨다운 기록 없이 다음 적격 대상으로 넘어갑니다. 선택적 매개변수 호환성을 참조하세요. |
+| `user`를 명시적으로 거부하거나, `reasoning.effort`/`reasoning_effort`의 지원되지 않는 값, 모델별 이미지 입력 거부(`param: input`) 또는 경로가 읽을 수 없는 입력 미디어 거부(`unsupported_input_modality`)를 나타내는 구조화된 HTTP 400 | 출력 시작 전에 쿨다운 기록 없이 다음 적격 대상으로 넘어갑니다. 선택적 매개변수 호환성을 참조하세요. |
| 인프로세스 어댑터(`runTurn`)가 실행하는 Responses 턴에서 현재 요청이 선언하지 않은 첫 도구 호출(출력이나 재전송 불가 부작용 이전) | 대상을 쿨다운하고 같은 도구 카탈로그로 다음 대상으로 넘어갑니다. 출력이 보였거나 재전송 불가 부작용이 생긴 뒤에는 거부가 그대로 확정됩니다. Chat Completions와 Anthropic Messages 요청은 바뀌지 않습니다. |
| 그 밖의 분류되지 않은 오류 | 멈추고 오류를 반환합니다. |
@@ -194,7 +194,7 @@ v1/base/v2 모드와 암호화된 작업의 전체 흐름은 [Sub-agent Surface]
### 대시보드
-로컬 대시보드를 열고 **Models → Combos**를 선택합니다. 워크스페이스는 콤보를 만들고, 편집하고, 이름을 바꾸고, 제거할 수 있으며, 대상 선택기에서는 비활성 모델과 중첩 콤보를 제외합니다.
+로컬 대시보드를 열고 **Models → Combos**를 선택합니다. 워크스페이스에서 콤보를 만들고, 편집하고, 이름을 바꾸고, 제거할 수 있습니다. 모델 선택기에는 숨겨진 모델도 포함됩니다. 표시 설정은 외부 모델 목록에만 적용되며 콤보 내부 선택에는 영향을 주지 않습니다. 숨겨진 모델을 선택해도 외부에 표시되지 않습니다. 대상 선택기에서는 비활성 공급자, 중첩 콤보, 결정 서비스 전용 JEV 공급자를 계속 제외합니다.
각 대상에는 **사용 가능**, **할당량 소진**, **할당량 알 수 없음** 실시간 배지도 표시됩니다. 편집기는 사용 가능한 모든 대상에 대해 설정된 인증 정보의 추론 한도가 소진되었다는 서버 확인이 현재 유효할 때만 할당량을 이유로 저장과 만들기를 차단합니다. 표시 전용 계정·모델·검색·MCP 할당량이나 누락되거나 만료된 라우팅 근거 정보는 이 차단을 일으키지 않습니다. 차단은 해당 한도의 초기화 시점이나 데이터 유효기간이 끝나면 해제되며 페이지가 활성화되거나 표시될 때 다시 확인됩니다. 새로 고침은 콤보 데이터와 할당량을 모두 다시 불러옵니다. 대시보드 편집기에서는 아직 `cooldownMs`나 `waitForCooldownMs`를 설정할 수 없습니다. 후속 UI 작업이 완료될 때까지 구성 파일이나 관리 API를 사용하세요.
@@ -285,6 +285,6 @@ opencodex 인스턴스에 기록했는지 확인하세요.
## 선택적 매개변수 호환성
-일반적인 400 오류는 종료되지만, `user`를 명시적으로 거부하거나 `reasoning.effort`/`reasoning_effort`의 지원되지 않는 값 또는 모델별 이미지 입력 거부(`param: input`)를 나타내는 구조화된 오류는 출력 시작 전에 다음 적격 대상으로 넘어갈 수 있습니다. 이 경우 쿨다운을 기록하지 않습니다. 보안 정책 거부, 취소 및 이미 시작된 출력은 재실행하지 않습니다.
+일반적인 400 오류는 종료되지만, `user`를 명시적으로 거부하거나 `reasoning.effort`/`reasoning_effort`의 지원되지 않는 값, 모델별 이미지 입력 거부(`param: input`) 또는 경로가 읽을 수 없는 입력 미디어 거부(`unsupported_input_modality`)를 나타내는 구조화된 오류는 출력 시작 전에 다음 적격 대상으로 넘어갈 수 있습니다. 이 경우 쿨다운을 기록하지 않습니다. 보안 정책 거부, 취소 및 이미 시작된 출력은 재실행하지 않습니다.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/ko/guides/desktop-app.md b/docs-site/src/content/docs/ko/guides/desktop-app.md
index 446a6e20c64..37c38585ff1 100644
--- a/docs-site/src/content/docs/ko/guides/desktop-app.md
+++ b/docs-site/src/content/docs/ko/guides/desktop-app.md
@@ -91,8 +91,12 @@ macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도
**status unknown**은 등록을 읽지 못했다는 뜻입니다. 앱이 프록시를 실행하는 동안 시작 안전 페이지는
백그라운드 서비스 설치를 막아 둡니다. 서비스를 대신 쓰려면 OpenCodex를 종료하고
터미널에서 `ocx service install`을 실행하세요. Codex 런처 shim은 이 페이지에서 설치할 수 있습니다.
-Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다리고, macOS에서
-앱이 종료돼 있으면 앱을 열며, 그 밖의 경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다.
+Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다립니다. macOS에서
+앱이 종료돼 있고 기록된 소유 정보가 유효한 **Start at Login** 등록과 일치하면 앱을 열고, 그 밖의
+경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. 자동으로 다시 열 때는 그 등록에 기록된 앱
+번들 경로를 사용하고, 번들이 선언한 주 실행 파일이 등록된 Desktop 실행 파일과 같은지 확인합니다.
+등록이 없거나 오래됐거나 서로 맞지 않으면 OpenCodex를 직접 여세요. 번들 식별자가 같은 다른 사본을
+대신 고르지 않습니다.
shim이 "활성화 미확인"으로 보이면 셸 시작 파일의 다른 PATH 설정 뒤에
`. ~/.opencodex/codex-shell-env.sh`를 추가하세요.
diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md
index f9c973b95d4..e4cd967e63c 100644
--- a/docs-site/src/content/docs/ko/guides/providers.md
+++ b/docs-site/src/content/docs/ko/guides/providers.md
@@ -99,6 +99,7 @@ ocx login google-antigravity
ocx login cursor # Cursor 전용 PKCE 로그인
ocx login command-code # Command Code 브라우저 OAuth (또는 ~/.commandcode/auth.json 가져오기)
ocx login devin # Cognition/Devin: Devin CLI 자격 임포트 우선, 없으면 Auth0 브라우저 로그인
+ocx login dsh-account # DeepSeek Harness Desktop (~/.dsh/.credentials.yaml 권한 가져오기)
ocx login github-copilot # GitHub 디바이스 플로우 → Copilot 토큰 (Copilot Pro/Business)
ocx login codex # Codex 계정 풀 (별칭: chatgpt, openai / 프록시가 실행 중이어야 함)
ocx logout
@@ -115,6 +116,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth를 Cloud Code Assist wire로 사용합니다. 실시간 탐색은 인증된 CCA `v1internal:fetchAvailableModels` 엔드포인트를 사용하며 로그인한 계정에서 사용할 수 있는 agent 모델만 게시합니다. 유지 관리되는 카탈로그는 폴백으로 남습니다. |
| `cursor` | `cursor` | `https://api2.cursor.sh` | 실험적 PKCE 로그인, HTTP/2 전송, 계정별 모델 탐색을 지원합니다. |
| `devin` | `devin` | `https://server.codeium.com` | 실험적인 비공식 Cognition/Devin 브리지. 로그인은 먼저 설치된 Devin CLI가 이미 보유한 자격을 가져옵니다(`devin auth login`이 `devin-session-token`을 자기 `credentials.toml`에 씁니다). 없으면 Auth0 브라우저 사인인을 열어 붙여넣은 토큰을 `RegisterUser`로 장기 API 키에 교환합니다. `ocx login devin-cli`는 deprecated alias로 계속 동작합니다. 모델 목록은 `GetCascadeModelConfigs`로 계정마다 조회하며, 스트리밍은 Connect-RPC 위에서 `runTurn` 경로만 씁니다. 대시보드 프리셋에는 기본으로 없으니 직접 추가하세요. |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | 설치 및 로그인된 DeepSeek Harness (DSH) Desktop 계정을 재사용하는 실험적 자격 임포트 우선 계정 프로바이더입니다. 명시적 임포트 시 `~/.dsh/.credentials.yaml`에서 엄격히 읽기 전용으로 자격을 읽습니다. DeepSeek API 키 요금 청구와 완전히 독립적입니다. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 실험적. GitHub 디바이스 플로우 + `copilot_internal` 교환(VS Code OAuth 클라이언트). 활성 Copilot 구독 필요; 공식 서드파티 API가 아닙니다. |
Google Antigravity 계정·제공자 할당량 확인은 모델 목록 폴백을 포함해 고정된 Google 회계 엔드포인트를 사용합니다. 해당 목적지의 투명 Fake-IP DNS를 지원하며 TLS 검증, 리다이렉트 거부, 사설 주소 검사는 유지합니다. 사용자 지정 base URL은 모델 요청에만 적용되며 할당량 목적지는 바꾸지 않습니다. `NO_PROXY`는 기존 직접 연결 정책을 유지합니다.
@@ -188,9 +190,20 @@ Kiro 로그인에는 Kiro CLI가 필요합니다. Unix에서는 `curl -fsSL http
롤백은 스냅샷이 있을 때만 가능하므로, 세션 저장소가 존재하지만 캡처할 수 없는 경우(파일을 읽을 수 없음, 스키마 불일치, 토큰 선택 모호), `KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE`이 실제 CLI 저장소와 다른 가져오기 경로를 가리키는 경우, 또는 기본 CLI 데이터베이스에 인식 가능한 토큰 행이 없는 경우 **계정 추가**는 `kiro-cli` 로그아웃을 거부합니다. 일반 `kiro-cli` 데이터 경로의 손상된 데이터베이스를 수리하거나 제거하고, 가져오기 전용 선택자가 설정돼 있으면 해제한 뒤 다시 시도하세요. 기존 `kiro-cli` 세션이 아예 없는 환경에서는 영향이 없습니다.
+### DeepSeek Account (DSH)
+
+`dsh-account`는 설치 및 로그인된 DeepSeek Harness (DSH) Desktop 애플리케이션의 인증된 계정 자격 증명을 재사용하는 실험적 자격 임포트 우선 계정 프로바이더입니다.
+
+- **DeepSeek API 키와 독립:** `dsh-account`는 사용자의 인증된 DeepSeek 세션 할당량(일반/보너스 지갑 잔액 및 총 지출)을 사용하며, `deepseek` 프로바이더는 플랫폼 API 키 잔액에서 청구됩니다. 두 자격 증명 도메인 및 라우팅은 엄격히 분리되어 상호 폴백되지 않습니다.
+- **명시적 가져오기 전용:** OpenCodeX는 DeepSeek Harness의 설치 및 로그인 여부(`~/.dsh/.credentials.yaml`)를 감지하지만, 사용자의 명시적 명령(`ocx login dsh-account` 또는 웹 대시보드에서 가져오기 클릭) 없이 토큰을 자동 복사, 가져오기 또는 사용하지 않습니다.
+- **DSH 자격 증명 저장소 읽기 전용:** OpenCodeX는 `~/.dsh/.credentials.yaml`을 엄격히 읽기 전용으로 취급합니다. OpenCodeX는 DeepSeek Harness에 속한 어떠한 파일도 쓰기, 수정, 삭제하거나 권한을 변경하지 않습니다.
+- **격리된 자격 증명 복사본 및 로그아웃:** 가져오기 시 OpenCodeX는 DeepSeek의 신뢰할 수 있는 ID 엔드포인트(`/auth-api/v0/users/current`)에서 토큰을 검증하고 자체 복사본을 OpenCodeX 인증 저장소에 저장합니다. OpenCodeX에서 로그아웃하거나 계정을 삭제해도 OpenCodeX 로컬 복사본만 삭제되며, 업스트림 원격 세션 로그아웃을 트리거하거나 DeepSeek Harness Desktop에서 로그아웃되지 않습니다.
+- **백그라운드 토큰 자동 갱신 없음 및 401 재가져오기:** DSH 세션 부여에는 자동 백그라운드 갱신 엔드포인트가 없습니다. 업스트림에서 HTTP 401(`ACCOUNT_TOKEN_INVALID`)을 반환하면 OpenCodeX는 계정을 `needsReauth`로 표시하고 라우팅을 중단합니다. 액세스를 복원하려면 사용자가 `ocx login dsh-account`로 명시적 재가져오기를 수행해야 합니다.
+- **서드파티 면책 조항:** OpenCodeX는 서드파티 클라이언트의 세션 부여 재사용에 대한 DeepSeek의 공식 보증이나 승인을 주장하지 않습니다.
+
## 3. API 키 카탈로그
-opencodex에는 빌트인 프리셋이 104개 들어 있습니다. 키 방식 85개, OAuth 15개, 로컬 3개,
+opencodex에는 빌트인 프리셋이 105개 들어 있습니다. 키 방식 85개, OAuth 16개, 로컬 3개,
기본 ChatGPT 포워드 프리셋 1개입니다. 대시보드의 **Add provider** 선택기는 키 발급 페이지를 열고,
입력한 키를 검증한 뒤 저장합니다(검증은 프로바이더별로 다릅니다). 주요 항목은 다음과 같습니다:
diff --git a/docs-site/src/content/docs/ko/guides/remote-workspace.md b/docs-site/src/content/docs/ko/guides/remote-workspace.md
index ef5b6490aec..338234f4da3 100644
--- a/docs-site/src/content/docs/ko/guides/remote-workspace.md
+++ b/docs-site/src/content/docs/ko/guides/remote-workspace.md
@@ -95,6 +95,28 @@ ocx remote-workspace status
프롬프트 실행 중에도 **Stop**을 사용할 수 있습니다. Hub 코딩 에이전트의 현재 턴을 중단하고 활성 Executor 명령을 취소하며, 늦게 온 응답이 중지된 세션을 다시 열지 못하게 합니다.
+## 여러 세션을 분할해서 보기
+
+왼쪽 세션 목록에서 세션을 클릭하면 현재 선택한 창에서 열립니다. 세션을 다른 창의
+왼쪽·오른쪽·위·아래 가장자리로 끌어다 놓으면 화면이 분할됩니다. 최대 네 개의 서로 다른
+세션을 동시에 표시할 수 있고, 이미 표시 중인 세션은 중복 창을 만드는 대신 이동합니다.
+
+세션 옆 **오른쪽으로 분할** 또는 **아래로 분할** 버튼도 사용할 수 있습니다. 경계선을
+드래그하거나 경계선에 키보드 초점을 놓고 방향키를 눌러 크기를 조절하세요. **Home**과
+**End** 키는 최소·최대 크기로 조절합니다. 각 세션의
+입력 중인 글, 전송 상태, 중지 동작은 독립적입니다. **창 닫기**는 배치만 변경하며 세션을
+중지하지 않습니다.
+
+같은 브라우저에서 이 Hub를 다시 열면 창 배치가 복원됩니다. 브라우저 저장소에는 세션
+식별자, 분할 배치와 경계선 크기만 저장하며, 작성 중인 글이나 모델 응답은 저장하지 않습니다.
+새로고침하면 아직 보내지 않은 글은 지워집니다. 화면 너비가 760px 이하이면 경계선 없이
+창을 세로로 쌓아 표시하고, 화면이 넓어지면 저장된 분할 배치와 크기로 돌아갑니다.
+
+이 화면은 OpenCodex 브라우저 대시보드와 데스크톱 앱에서 공통으로 사용합니다. CLI에서는
+`ocx gui`로 대시보드를 열고 정상적인 Hub 설정 후 **원격 워크스페이스**를 선택하세요.
+이 기능은 `codex` 터미널 화면이나 별도 공식 ChatGPT/Codex 앱 내부의 대화 배치를 변경하지
+않습니다.
+
## 재시작과 재연결 동작
Hub는 제한된 세션 메타데이터와 최근 이벤트의 작은 스냅샷을 저장합니다. Hub 재시작 뒤 완료되지 않은 세션은 원래 Executor를 기다립니다. 해당 기기가 다시 연결되면 다음 프롬프트가 원래 Codex 작업, Claude Code 세션 또는 Pi 세션 ID를 이어갑니다.
diff --git a/docs-site/src/content/docs/ko/guides/sidecars.md b/docs-site/src/content/docs/ko/guides/sidecars.md
index b1aca7ee1b1..9716fc27937 100644
--- a/docs-site/src/content/docs/ko/guides/sidecars.md
+++ b/docs-site/src/content/docs/ko/guides/sidecars.md
@@ -74,8 +74,11 @@ stall은 전체 생성 timeout이 아닙니다. SSE가 시작되기 전 실패
라우팅 모델이 해당 프로바이더의 `noVisionModels`에 있거나 해당 모델이 `modelInputModalities`에서
텍스트 전용으로 선언되어 있고 요청에 이미지가 들어오면, opencodex는 사용 가능한 비전 사이드카 계획이 있을 때에만
메인 호출 **전에** 각 이미지를 설명한 텍스트로 바꿉니다. 사용 가능한 계획이 없으면 원본 이미지는 텍스트 전용
-백엔드로 전달되지 않고 제거됩니다. 모델 카탈로그는 사이드카로 처리되는 모든 모델에 image input을 알립니다.
-콤보는 모든 멤버가 네이티브로 또는 사이드카를 통해 이미지를 수용하고 콤보의 `imageInput` 설정이 비활성화되지 않은
+백엔드로 전달되지 않고 제거됩니다. `visionSidecar.enabled: false`이면 모델 카탈로그는 사용자 정의 모델과
+콤보 멤버를 포함한 텍스트 전용 모델에 사이드카 기반 이미지 입력을 추가하지 않습니다. 네이티브 이미지 지원과
+명시적으로 설정한 이미지 입력은 유지됩니다. `enabled`를 생략하면 사이드카 기반 표시는 켜진 상태이며,
+카탈로그 표시는 사이드카의 인증 정보나 사용 가능 용량을 보장하지 않습니다.
+콤보는 모든 멤버가 네이티브로 또는 활성화된 사이드카를 통해 이미지를 수용하고 콤보의 `imageInput` 설정이 비활성화되지 않은
경우에만 image input을 알립니다. 따라서 Codex 앱 같은 클라이언트는 사이드카가 실행되기 전에 첨부를 차단하지 않고 허용합니다.
`visionSidecar.model`이 없거나 빈 값이면
OpenAI 실행 경로, Dashboard, 관리 API는 `gpt-5.6-luna`를 폴백으로 사용합니다. 시작 시 명시적으로
@@ -139,6 +142,10 @@ OpenAI 실행 경로, Dashboard, 관리 API는 `gpt-5.6-luna`를 폴백으로
`PUT /api/sidecar-settings`는 같은 필드를 받습니다. 부분 업데이트는 보내지 않은 키를 유지합니다. `timeoutMs`는 런타임 정수 범위(1–2147483647 ms)를 사용합니다.
+비전의 실제 활성화 상태가 바뀌면 저장 후 관리되는 Codex 카탈로그와 모델 캐시도 갱신합니다.
+갱신을 완료하지 못하면 응답의 `catalogRefreshPending`이 `true`가 되며 저장된 설정은 유지됩니다.
+카탈로그 갱신 완료가 실행 중인 클라이언트의 다시 읽기를 확인하는 것은 아닙니다.
+
웹 검색 사이드카 카드도 같은 구성입니다. 모델 선택기의 첫 행은 **끔 (Off)** 행입니다. 끄면
OpenCodex가 `web_search` 가로채기를 멈추고 Codex 통합이 `~/.codex/config.toml`에
`web_search = "disabled"`를 씁니다. Codex는 자체 모드가 그렇게 될 때까지 네이티브 호스팅
diff --git a/docs-site/src/content/docs/ko/guides/subagent-v1-default.md b/docs-site/src/content/docs/ko/guides/subagent-v1-default.md
index 9bb8994418d..dbf8fee9103 100644
--- a/docs-site/src/content/docs/ko/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/ko/guides/subagent-v1-default.md
@@ -94,3 +94,9 @@ ocx v2 mode v1
```
CLI는 확인 메시지를 띄우지 않습니다. 같은 설정이므로 이 문서의 동작을 이해한 뒤 선택하세요.
+
+## 자식 에이전트 동시 실행 상한
+
+**Models → Model settings**의 서브에이전트 상한 설정은 주 에이전트를 제외한 자식 에이전트의 최대 동시 실행 수를 정합니다. v1, base, v2와 전역 v2 플래그가 꺼진 하이브리드 모드에서도 사용할 수 있습니다. 서피스 모드는 이 상한과 별도로 선택합니다.
+
+상한을 높이면 더 많은 자식을 허용하지만, 자식을 자동으로 만들거나 실행 속도를 보장하지는 않습니다. 변경 후 새 Codex 세션을 시작하세요. 오래 실행된 App 호스트가 이전 상한을 계속 사용한다면 해당 호스트를 다시 시작하세요.
diff --git a/docs-site/src/content/docs/ko/reference/architecture.md b/docs-site/src/content/docs/ko/reference/architecture.md
index 68c30f9871e..59dc83a038c 100644
--- a/docs-site/src/content/docs/ko/reference/architecture.md
+++ b/docs-site/src/content/docs/ko/reference/architecture.md
@@ -69,7 +69,8 @@ HTTP 경계는 `server/index/serve-options.ts`가 맡고, Responses 데이터
건드리지 않은 채 종료 상태, 지연 시간, 프로바이더/모델, 최선 추정 토큰 사용량을 기록합니다.
요청 전 입력량 추정은 라우팅된 어댑터가 실제로 보내는 내용을 따릅니다. `openai-chat` 모델이
-`preserveReasoningContentModels`에 없으면 어댑터가 이전 assistant thinking을 보내지 않으므로
+명시적 `preserveReasoningContentModels` 목록 또는 미설정 시 DeepSeek Flash/Pro 감지에 따른
+유효 thinking 보존 정책에서 제외되면 어댑터가 이전 assistant thinking을 보내지 않으므로
추정에서도 뺍니다. 그래서 보내지도 않는 기록 때문에 로컬 컨텍스트 한도에서 잘못 거부되는 일이
없습니다. reasoning을 보존하는 모델과 다른 어댑터는 이전 thinking을
실제로 보내므로 계속 계산에 넣습니다.
diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md
index 21a88093a16..3abe8fbd115 100644
--- a/docs-site/src/content/docs/ko/reference/cli.md
+++ b/docs-site/src/content/docs/ko/reference/cli.md
@@ -17,7 +17,7 @@ opencodex CLI는 `ocx`입니다. 첫 번째 명령 이름으로 분기하며, `s
관리 명령은 기록된 런타임 포트와 신원 검사를 사용해 살아 있는 프록시의 management API와 왕복 통신하며, 두 번째 설정 경로를 따로 두지 않습니다. 멈췄거나 닿을 수 없는 프록시는 HTTP 503으로 표시되며 CLI는 0이 아닌 종료 코드를 반환합니다. 명시적으로 오프라인 설정 작업으로 문서화된 명령은 라이브 프록시 없이 설정 파일을 검증하고 수정할 수 있습니다.
-`ocx system codex-cli-update check`는 실행 중인 프록시가 없어도 되며 패키지 레지스트리를 조회하지 않습니다. 설정된 설치 후보에 대해 전체 경로를 숨긴 실행 파일 위치와 소유권 근거를 포함한 provenance 메타데이터를 제한된 범위에서 검사합니다. 신뢰할 수 있는 배포 런처 컨텍스트가 인증하는 것은 후보 스냅샷뿐이며, Codex가 성공적으로 실행되었다는 사실은 인증하지 않습니다. 이 단발성 명령은 Codex를 전혀 실행하지 않으므로 환경 또는 저장된 상태에서 얻은 후보는 보고 전용입니다(`managed: false`, 일반적으로 `selection_unattested`). `selectionAttested`는 항상 `false`입니다. JSON 출력에는 `candidateAvailable`, `candidateVersion`, `candidateSource`, `selectionAttested: false`가 포함됩니다. Bun이나 소스에서 직접 실행하면 런처 증거가 없으므로 환경 및 저장된 후보를 무시하고 POSIX에서는 `candidate_unavailable`, Windows에서는 `windows_inspection_deferred`을 보고할 수 있습니다. Windows에서는 이 첫 조각이 후보 또는 설정 경로의 파일시스템을 전혀 읽지 않습니다. 배포 런처가 증명한 절대 환경 후보에 한해서 앱 번들 또는 버전 관리자라는 어휘적 표지만 보고하며, 그 밖의 Windows 후보는 모두 실패 닫힘 처리합니다. 이 명령은 소프트웨어를 설치하거나 복구하지 않고, Codex나 npm을 실행하지 않으며, 실행 중인 프로세스를 제어하거나 설정 또는 캐시 상태를 쓰지 않습니다.
+`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
Windows x64 설치 관측은 [`attest` 명령](/ko/reference/cli/agents/)을 참조하세요. 명시적 경로 없이 증명에 바인딩된 런처 스냅샷이 식별한 선택 후보를 관측하며, 업데이트 권한이나 런타임 선택을 증명하지 않습니다.
diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md
index 545e1e4a790..c8bb7e53731 100644
--- a/docs-site/src/content/docs/ko/reference/cli/agents.md
+++ b/docs-site/src/content/docs/ko/reference/cli/agents.md
@@ -88,6 +88,8 @@ ocx route combo set reliable --targets ark/model-a:2,openai/gpt-5.5
## 관측성과 디버그
+`ocx usage --top 25` (`ocx observe usage --top 25`도 동일)는 사람이 읽는 모델 표의 행 수를 1~1000 정수로 지정합니다. 기본값은 10이며 `--search` 사용 시 100입니다. 검색 필터링 후 로컬 표시에만 적용되며 합계, 제공자/계정 행, API 매개변수는 바뀌지 않습니다. `--json`은 유효한 `--top`을 무시하고 기존 전체 보고서 또는 100행 검색 뷰를 유지합니다. 잘못된 값은 요청 전에 종료 코드 2를 반환합니다.
+
`ocx logs filter --json`은 최근 로그 한 구간을 읽고 조건에 맞는 행을 고릅니다. `--scan-limit`은 읽을 행 수, `--limit`은 출력할 행 수입니다. 결과가 비어 있어도 전체 기록에 일치 항목이 없다는 뜻은 아닙니다. `ocx usage --search `는 모델 행만 검색하고 보고서의 총계와 조회 권한 범위는 유지합니다.
`ocx companion usage --json`은 저장된 모델·공급자 표시 설정으로 오늘과 30일 사용량을 읽습니다. 한 구간이 실패하면 다른 구간은 남기고 `partial: true`와 종료 코드 1을 반환합니다. 설정 기본값을 사용했다면 `settingsFallback`, 손상된 설정의 기본값이라면 `settingsCorrupt`도 확인하세요. 자세한 옵션과 응답은 [영문 CLI 기준 문서](/reference/cli/agents/#filter-a-bounded-log-snapshot)를 참고하세요.
@@ -99,7 +101,7 @@ ocx route combo set reliable --targets ark/model-a:2,openai/gpt-5.5
| 별칭 | 대응 리소스 |
| --- | --- |
| `ocx logs [filters] [--follow] [--json|--jsonl]` | `ocx observe logs` |
-| `ocx usage [--range ] [--surface ] [--provider ] [--model ] [--json]` | `ocx observe usage` |
+| `ocx usage [--range ] [--surface ] [--provider ] [--model ] [--search ] [--top <1-1000>] [--json]` | `ocx observe usage` |
| `ocx storage [--json]` | `ocx observe storage` |
| `ocx memory [--json]` | `ocx observe memory` |
@@ -235,9 +237,14 @@ opencode는 `{env:OPENCODEX_OPENCODE_API_KEY}`를 보간합니다. opencodex가
헤드리스 런타임 설정, 시작, 동기화, 진단, 업데이트를 관리합니다.
`ocx system codex-restart --yes`는 `ocx sync --restart-codex`와 같은 모듈로 Codex
-app-server를 재시작하고 데스크톱 앱도 완전히 종료한 뒤 다시 띄웁니다. 프록시 자체가
-Codex 앱 안에서 실행 중이면 넘길 수 없는 handoff를 약속하지 않고, 대신 실행 가능한
-안내와 함께 거절합니다.
+app-server를 재시작합니다. 모델 선택기는 앱 실행 시점에 만들어지므로, 데스크톱 앱은 현재
+모델 카탈로그가 기록되기 전에 실행된 경우나 이 요청으로 `model_catalog_json` 경로가 바뀐 경우 완전히 종료한 뒤 다시 띄웁니다. 그렇지 않으면
+앱은 열린 채로 자체 app-server를 다시 띄우고, 보내지 않은 초안과 대기 중인 승인을 유지합니다.
+앱이나 카탈로그 상태를 읽을 수 없으면 전체 재시작을 수행합니다. 프록시 자체가 Codex 앱 안에서
+실행 중이면 데스크톱 재시작은 넘길 수 없는 handoff를 약속하지 않고, 대신 실행 가능한 안내와
+함께 거절합니다.
+
+이 요청으로 `model_catalog_json` 경로가 바뀌면 셸이 카탈로그 파일 작성 후 시작되었어도 데스크톱을 재시작합니다.
```bash
ocx system settings --stream-mode eager-relay
@@ -249,7 +256,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check`는 패키지 레지스트리를 조회하지 않고, 설정된 설치 후보에 대해 전체 경로를 숨긴 실행 파일 위치와 소유권 근거를 포함한 provenance 정보를 제한된 범위에서 검사합니다. 신뢰할 수 있는 배포 런처 컨텍스트가 인증하는 것은 후보 스냅샷뿐이며, Codex가 성공적으로 실행되었다는 사실은 인증하지 않습니다. 이 단발성 명령은 Codex를 전혀 실행하지 않으므로 환경 또는 저장된 상태에서 얻은 후보는 보고 전용입니다(`managed: false`, 일반적으로 `selection_unattested`). `selectionAttested`는 항상 `false`입니다. JSON 출력에는 `candidateAvailable`, `candidateVersion`, `candidateSource`, `selectionAttested: false`가 포함됩니다. Bun이나 소스에서 직접 실행하면 런처 증거가 없으므로 환경 및 저장된 후보를 무시하고 POSIX에서는 `candidate_unavailable`을 보고할 수 있습니다. Windows에서는 이 첫 조각이 후보 또는 설정 경로의 파일시스템을 전혀 읽지 않습니다. 배포 런처가 증명한 절대 환경 후보에 한해서 앱 번들 또는 버전 관리자라는 어휘적 표지만 보고하며, 그 밖의 Windows 후보는 모두 실패 닫힘 처리합니다. 이 조각은 저장된 선택 상태를 전혀 읽지 않으므로, 환경 후보가 캡처되지 않은 Windows 실행은 `candidate_unavailable`이 아니라 `windows_inspection_deferred`를 보고합니다. 명령이 Codex CLI 설치 여부를 관측할 수 없으므로, 후보가 없다고 단정하는 대신 검사가 연기되었음을 보고합니다. 이 명령은 Codex나 패키지 관리자를 실행하거나 shim을 복구하지 않고, 설정 또는 캐시 상태를 쓰거나 프로세스를 중지하거나 어떤 것도 설치하지 않습니다. 앱에 포함된 후보, 인식된 버전 관리자의 후보, 검증되지 않은 독립 실행형 후보, shim 상태가 모호한 후보는 관리 대상이 아니거나 알 수 없는 것으로 보고되며, 관리 대상으로 분류되지 않습니다.
+`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
Windows에서 `CODEX_CLI_PATH=codex` 같은 단순 명령 이름이나 원격 경로·장치 경로가 후보로 캡처되면 `candidate_path_unavailable`을 보고합니다. 후보는 캡처됐지만 해당 경로가 이 검사 대상에 적합하지 않은 경우입니다.
diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
index aa2e4a8db43..5c353e5bd80 100644
--- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
@@ -106,7 +106,7 @@ dedicated-provider history도 포함됩니다. 상태를 백업하고 이 전체
### `ocx recover-history --ocx-compaction --yes`
-라우팅된 provider를 통해 압축된 작업을 native Codex에서 다시 열기 전에 해당 기록을 복구합니다. 이 명령은 UUID로 정확히 하나의 작업을 선택하고 비공개 바이트 단위 백업을 저장한 뒤, OpenCodeX가 소유한 `ocx1:` 압축 상태만 native Codex가 재생할 수 있는 일반 요약으로 변환합니다. native 암호화 콘텐츠와 다른 작업은 변경하지 않습니다. 실행 전에 선택한 작업을 닫으십시오. 처리 중 rollout이 변경되면 파일을 교체하지 않고 복구를 중단합니다.
+라우팅된 provider를 통해 압축된 작업을 native Codex에서 다시 열기 전에 해당 기록을 복구합니다. 이 명령은 UUID로 정확히 하나의 작업을 선택하고 비공개 바이트 단위 백업을 저장한 뒤, OpenCodeX가 소유한 `ocx1:`/`ocx2:` 압축 상태만 native Codex가 재생할 수 있는 일반 요약으로 변환합니다. native 암호화 콘텐츠와 다른 작업은 변경하지 않습니다. 실행 전에 선택한 작업을 닫으십시오. 처리 중 rollout이 변경되면 파일을 교체하지 않고 복구를 중단합니다.
### `ocx uninstall` · `ocx remove`
@@ -238,6 +238,8 @@ single-flight/lock 파일을 만들 수 있는지, 건강하지 않은 OAuth 또
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+재시작 플래그를 명시하면 카탈로그와 캐시 바이트가 같아도 `config.toml` 변경이 성공적으로 커밋된 뒤 재시작합니다. 설정만 다루는 작업이 변경 없음, 거부, 사전 검증 또는 롤백으로 끝나면 재시작하지 않습니다. 재시작 플래그가 없으면 sync는 Codex를 자동으로 재시작하지 않습니다.
+
설정된 모든 공급자에서 라이브 모델 목록을 가져와 병합된 카탈로그를 Codex에 다시 주입합니다.
공급자를 추가한 뒤나 사용 가능한 모델을 새로 고칠 때 실행합니다.
diff --git a/docs-site/src/content/docs/ko/reference/configuration/advisor.md b/docs-site/src/content/docs/ko/reference/configuration/advisor.md
index d0539ae96e1..0e1b4d940fe 100644
--- a/docs-site/src/content/docs/ko/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/ko/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: 어드바이저
-description: OpenCodex가 소유한 전문가 상담 사이드카 — 구성된 전문가 모델이 라우팅된 워커에게 조언을 반환하며, manual과 preflight 정책을 제공합니다.
+description: manual, preflight 및 증거 기반 adaptive 정책을 제공하는 독립 전문가 상담.
---
어드바이저는 워커의 작업을 검토하고 조언을 반환하는 독립 전문가 모델입니다. 상담은 OpenCodex가 엔드투엔드로 소유합니다. 프록시가 워커 턴에 합성 `advisor` 도구를 주입하고, 정상적인 라우팅 권위를 통해 상담을 직접 실행하며, 조언을 재주입해 원래 워커가 계속 진행하게 합니다. 워커는 위임하지 않고, 아무것도 spawn하지 않으며, 제공자 자격 증명도 가지지 않습니다.
@@ -9,6 +9,8 @@ description: OpenCodex가 소유한 전문가 상담 사이드카 — 구성된
## 구성
+대시보드 **어드바이저** 페이지에서 카탈로그 선택기로 전문가 모델을 선택하세요. 네이티브, 공급자, 계정 지정, 사용자 정의 및 콤보 선택자의 정확한 카탈로그 식별자를 저장합니다. 비활성 모델과 초기 선택 대기 모델은 새 선택에 표시하지 않습니다. 선택 가능한 목록에 없는 저장된 식별자는 경고와 함께 유지되며, 다른 모델을 선택하거나 지우기 전까지 변경되지 않습니다. 카탈로그 로드 실패 시 현재 선택을 보존하고 재시도를 제공합니다. 텍스트 조언을 반환할 수 있는 모델을 선택하세요. 카탈로그가 상담 호환성을 보장하지는 않습니다. CLI와 API는 라우터가 지원하는 모델 문자열을 계속 허용합니다.
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ description: OpenCodex가 소유한 전문가 상담 사이드카 — 구성된
| `enabled?` | `boolean` | `false` | 마스터 스위치. 비활성화 시 요청 경로에 어드바이저 동작이 전혀 없습니다. |
| `model?` | `string` | — | 전문가 모델. 라우터가 허용하는 모든 모델 문자열: 네이티브 모델(`gpt-6-astra`), 명시적 `provider/model`(`anthropic/claude-sonnet-4-6`, `xai/grok-...`), 계정 한정 네이티브 모델. 크로스 프로바이더를 완전히 지원합니다. |
| `effort?` | `string` | `"max"` | 어드바이저 호출의 추론 강도(`low`~`ultra`). |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | 상담 시점. |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | 상담 시점. |
+| `maxConsultationsPerTask?` | `integer` | `3` | 적응형 작업 상담 한도(1–3회 시도, 수동 호출 포함). |
| `timeoutMs?` | `number` | `120000` | 루프백 상담 타임아웃. |
| `contextSharingConsent?` | `"v1"` | 없음 | 설정된 어드바이저 프로바이더로 작업 맥락을 보내는 운영자 동의. 현재 값은 `"v1"`뿐입니다. 없거나, 오래되었거나, 다른 값이면 작업 내용을 보내지 않습니다. `enabled: true`만으로는 동의가 아닙니다. |
-대시보드 **Advisor** 페이지 또는 `ocx advisor status|on|off|set --model --effort --policy `로 관리합니다.
+대시보드 **Advisor** 페이지 또는 `ocx advisor status|on|off|set --model --effort --policy `로 관리합니다.
현재 동의가 없으면 `ocx advisor on`은 프로바이더 간 전송을 켜지 않습니다. 고지를 출력하고 멈춥니다. `ocx advisor on --ack-context-sharing`와 `ocx advisor consent`가 `v1`을 기록합니다. `ocx advisor consent --revoke`는 동의를 지우고 전송을 즉시 멈춥니다. `ocx advisor set`은 동의를 부여하지 않습니다. 대시보드의 동의 확인란은 미리 선택되지 않습니다.
@@ -78,5 +81,13 @@ OpenCodex는 프로바이더 API 키, Authorization 헤더, OAuth 토큰, 백엔
## PR1 제한
- 네이티브 OpenAI 패스스루 턴(ChatGPT 풀 워커)에는 합성 도구가 주입되지 않습니다. 어드바이저는 라우팅(번역) 제공자를 대상으로 합니다. preflight 상담은 run-turn 어댑터에 적용되지만 도구는 적용되지 않습니다.
-- 적응형 트리거가 없습니다: 막힘 감지, 반복 실패 분석, 에스컬레이션 계층, 다중 어드바이저, 투표가 없습니다. `manual`과 `preflight`만 있습니다.
+- 의미 기반 막힘 감지, 완료 검토, 단계별 에스컬레이션, 다중 전문가 또는 투표는 제공하지 않습니다.
- preflight 중복 제거 원장은 프로세스 내에 있습니다. 프록시 재시작 후 진행 중인 작업은 preflight 상담을 한 번 더 받을 수 있습니다.
+
+## 증거 기반 적응형 상담과 활동 기록
+
+`adaptive`는 초기 상담 외에 같은 작업 디렉터리의 같은 검증이 실패하고, 이후 파일 편집이 성공한 뒤 새 검증 실행도 실패했을 때 상담합니다. 검증 성공은 실패 상태를 지웁니다. 편집 전에 시작한 검증은 수정 후 재시도가 아닙니다. 초기 상담과 수정 실패 상담은 별도로 중복을 방지하며 자동 상담에는 안정적인 대화 식별자가 필요합니다. 짝지어진 도구 호출/결과와 명시적 종료 코드가 필요합니다. 복잡한 스크립트, 일괄 호출, shell 편집 및 모호한 출력은 트리거하지 않습니다.
+
+`ocx advisor set --policy adaptive --max-consultations 2`로 설정합니다. 수동 및 자동 시도는 작업별 1–3회 예산을 공유하며 전송 전에 `OPENCODEX_HOME/advisor-budget.sqlite`에 예약합니다. 프록시 재시작이나 Worker 변경으로 초기화되지 않습니다. 작업 다이제스트, 횟수, 만료 시간만 보관하며 첫 시도 후 24시간에 만료됩니다(최대 512개 작업). 예산을 사용할 수 없어도 Worker는 계속합니다. 전송된 실패·취소 호출도 비용이 발생할 수 있으므로 예약을 돌려주지 않습니다. 안정적인 식별자가 없으면 요청별 수동 상담만 가능합니다.
+
+Advisor 페이지와 `ocx advisor activity --json`(`GET /api/advisor/activity`)은 이 프로세스의 최근 24시간 상담 메타데이터를 최대 100개 표시합니다. 화면이 보일 때 5초마다 갱신합니다. 이유, 상태, 모델, 시간, 보고된 입력/출력 토큰, 요청 및 어댑터 매핑 추론값을 포함합니다. 작업 본문, 명령, 조언, 대화 식별자는 저장하지 않으며 재시작 시 사라집니다. 매핑은 전송 파라미터의 증거일 뿐 내부 추론을 보장하지 않습니다. 미보고 값은 알 수 없음으로 유지하며 정확한 비용이나 조언 채택을 추정하지 않습니다.
diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md
index 6ece10e3718..b8eef4372b3 100644
--- a/docs-site/src/content/docs/ko/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md
@@ -89,7 +89,9 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` 중 하나이며, `azure`는 별칭입니다. |
| `baseUrl` | `string` | 상위 API 기본 URL입니다. 대부분의 내장 고정 엔드포인트는 불일치를 무시합니다. 충돌 안전 키 프리셋은 같은 이름의 이전 사용자 지정 목적지를 보존합니다. |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | 업스트림 사용량, 과금, rate-limit 지표와 별개인 선택적 클라이언트 측 아웃바운드 요청 시작 속도 조절입니다. `maxConcurrentRequests`는 진행 중 요청 수를 제한하는 양의 정수이며 provider 또는 model 규칙에서 단독으로 사용할 수 있습니다. Provider 제한은 모든 모델에 적용되고 `models` 항목은 정확한 업스트림 모델 ID와 일치하며 지연을 늘리거나 동시성을 줄일 수 있습니다. 큐 대기는 응답 헤더 타임아웃을 소모하지 않습니다. HTTP 및 명시적 어댑터 `fetchResponse`/`runTurn` 전송을 포함합니다. 동시성 제한이 있으면 표준 Responses WebSocket 턴은 응답 본문의 완료·오류·취소 때 점유 슬롯을 해제할 수 있도록 HTTP/SSE를 사용합니다. Cursor를 포함한 `runTurn` 어댑터에서는 물리적 전송 수가 아니라 진행 중인 턴 수를 제한합니다. 같은 턴의 RunSSE와 BidiAppend는 겹칠 수 있지만 다른 턴은 대기합니다. 후속 전송에도 시작 간격은 적용됩니다. |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 업스트림 사용량, 과금, rate-limit 지표와 별개인 선택적 클라이언트 측 아웃바운드 요청 시작 속도 조절입니다. `maxConcurrentRequests`는 진행 중 요청 수를 제한하는 양의 정수이며 provider 또는 model 규칙에서 단독으로 사용할 수 있습니다. Provider 제한은 모든 모델에 적용되고 `models` 항목은 정확한 업스트림 모델 ID와 일치하며 지연을 늘리거나 동시성을 줄일 수 있습니다. 큐 대기는 응답 헤더 타임아웃을 소모하지 않습니다. HTTP 및 명시적 어댑터 `fetchResponse`/`runTurn` 전송을 포함합니다. 동시성 제한이 있으면 표준 Responses WebSocket 턴은 응답 본문의 완료·오류·취소 때 점유 슬롯을 해제할 수 있도록 HTTP/SSE를 사용합니다. Cursor를 포함한 `runTurn` 어댑터에서는 물리적 전송 수가 아니라 진행 중인 턴 수를 제한합니다. 같은 턴의 RunSSE와 BidiAppend는 겹칠 수 있지만 다른 턴은 대기합니다. 후속 전송에도 시작 간격은 적용됩니다. |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 정상 요청의 키 선택 방식입니다. 기본 `active`는 저장된 키와 기존 오류 전환을 유지하며, `round-robin`은 저장된 활성 키를 변경하지 않고 사용 가능한 풀 키를 요청별 순서대로 배정합니다. 토큰이나 비용이 아니라 배정 횟수를 분산합니다. |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | 키 참조별 동일한 RPM·최소 시작 간격·동시 요청 제한입니다. 모든 모델에서 공유하며 프로바이더·정확한 모델 상한과 함께 적용합니다. 중복 참조는 한 슬롯을 공유하지만 같은 업스트림 프로젝트의 다른 참조가 독립 할당량을 뜻하지는 않습니다. TPM·일일 한도는 처리하지 않습니다. |
| `responsesPath?` | `string` | 키 인증 `openai-responses` 요청의 상대 리소스 경로입니다. 반드시 `/`로 시작해야 하며 스킴, query, fragment를 포함하면 안 됩니다. |
| `chatCompletionsPath?` | `string` | `openai-chat` 요청의 상대 리소스 경로로, `responsesPath`와 동일한 형식 규칙이 적용되는 대응 항목입니다. 하나의 업스트림이 Chat Completions와 Responses를 서로 다른 접두사로 제공할 때 필요합니다. 모델별 wire override는 어댑터만 바꾸고 `baseUrl`은 그대로 두므로, 이 설정이 없으면 옵트인된 Chat 요청이 Responses base로 전송됩니다. Z.AI가 제공되는 예시입니다. |
| `upstreamWebsocket?` | `boolean` | `openai-responses` 요청에 대한 업스트림 Responses WebSocket 전송을 선택적으로 활성화합니다(기본값 `false`). 퍼스트파티 `https://api.openai.com/v1` 업스트림에서만 적용되며, 사용자 지정 공급자 엔드포인트는 항상 제한된 HTTP/SSE를 사용합니다. Bun은 전체 메시지를 할당하기 전에는 수신 WebSocket 메시지 크기 제한을 적용할 수 없기 때문입니다. 정식 ChatGPT `openai` 공급자에서는 생략하면 대상 턴에서 업스트림 WebSocket을 사용하고, `false`는 스트리밍 턴을 HTTP/SSE로 전송하며, `true`는 거부됩니다. `false`이면 네이티브 턴 중 스티어링과 주입을 사용할 수 없습니다. 이 필드는 클라이언트 측 `websockets` 설정과 독립적이며 엔드포인트와 자격 증명을 변경하지 않습니다. HTTP 기본 URL은 SSE를 유지하고, Responses가 아닌 경로와 `openai-chat` 요청은 HTTP를 사용합니다. |
@@ -112,7 +114,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정
| `modelAutoCompactTokenLimits?` | `Record` | 모델별 양의 안전 정수형 소프트 자동 압축 예산입니다. 유효한 컨텍스트 또는 최대 입력의 90% 한도를 낮출 수만 있으며, 유효 컨텍스트 창을 결정할 수 없으면 내보내지 않습니다. canonical `openai`에서는 키가 공급자나 계정 선택자 접두사가 없는 정확한 지원 네이티브 모델 ID여야 합니다. 공급자 PATCH는 항목을 병합하며, 키를 `null`로 지정하면 해당 키를 삭제하고 필드 전체를 `null`로 지정하면 맵을 지웁니다. 이 `null` tombstone은 PATCH에서만 사용할 수 있습니다. |
| `defaultMaxOutputTokens?` | `number` | 클라이언트가 `max_output_tokens`를 생략했을 때 쓰는 공급자 전반의 `openai-chat` 폴백입니다. |
| `modelMaxOutputTokens?` | `Record` | 양수 모델별 `openai-chat` 폴백 예산입니다. 정확한 일치와 패턴 일치가 공급자 기본값보다 우선합니다. |
-| `modelCosts?` | `Record` | 모델별 표시 가격(100만 토큰당 USD). 해당 공급자의 정확한 업스트림 모델 ID를 키로 사용하며(공급자 식별자나 라우팅된 `provider/model` 레이블이 아님) 값은 `input`, `output`, `cacheRead`, `cacheWrite` 네 필드입니다(예: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). 커스텀 공급자는 `openai-chat` 어댑터로 임의의 OpenAI 호환 엔드포인트를 대상으로 할 수 있으며, 내장 카탈로그에 없는 로컬·내부 공급자 ID도 유효합니다. 사용자 구성 가격은 Logs `~$` 및 Usage 추정에서 내장 카탈로그보다 우선합니다. 기존 항목도 현재 오버레이로 다시 계산되므로 가격을 편집하면 과거 합계가 바뀔 수 있습니다(폴백 순서: 사용자 설정 → jawcode 카탈로그 → expected-price 오버레이 → 모델별 벤더 가격). 사용자가 모든 요율을 명시적으로 0으로 설정하면 비용을 0으로 추정합니다. 자동 가격으로 되돌리려면 해당 모델 항목을 삭제하세요. 카탈로그의 전부 0인 요율은 계속 다음 소스로 폴백합니다. 각 요율은 0 이상의 유한한 숫자이며 최대 1,000,000(100만 토큰당 USD)입니다. 범위를 벗어난 행은 관리 경계에서 거부되고 로드 시 삭제됩니다. 표시 전용 추정이며 라우팅·계정 선택·할당량·청구에는 영향을 주지 않습니다. |
+| `modelCosts?` | `Record` | 모델별 표시 가격(100만 토큰당 USD). 해당 공급자의 정확한 업스트림 모델 ID를 키로 사용하며(공급자 식별자나 라우팅된 `provider/model` 레이블이 아님) 값은 `input`, `output`, `cacheRead`, `cacheWrite` 네 필드입니다(예: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). 커스텀 공급자는 `openai-chat` 어댑터로 임의의 OpenAI 호환 엔드포인트를 대상으로 할 수 있으며, 내장 카탈로그에 없는 로컬·내부 공급자 ID도 유효합니다. 사용자 구성 가격은 Logs `~$` 및 Usage 추정에서 내장 카탈로그보다 우선합니다. 기존 항목도 현재 오버레이로 다시 계산되므로 가격을 편집하면 과거 합계가 바뀔 수 있습니다(폴백 순서: 사용자 설정 → jawcode 카탈로그 → expected-price 오버레이 → 모델별 벤더 가격). 사용자가 모든 요율을 명시적으로 0으로 설정하면 비용을 0으로 추정합니다. 자동 가격으로 되돌리려면 해당 모델 항목을 삭제하세요. 카탈로그의 전부 0인 요율은 계속 다음 소스로 폴백합니다. 각 요율은 0 이상의 유한한 숫자이며 최대 1,000,000(100만 토큰당 USD)입니다. 범위를 벗어난 행은 관리 경계에서 거부되고 로드 시 삭제됩니다. 표시 전용 추정이며 라우팅·계정 선택·할당량·청구에는 영향을 주지 않습니다. |
| `headers?` | `Record` | 추가 상위 헤더입니다. Authorization, cookies, API-key 헤더, 내장 개행, 잘못된 이름은 허용하지 않습니다. |
| `openRouterRouting?` | `OpenRouterProviderRouting` | 기본 OpenRouter `order`, `only`, `allowFallbacks` 선호도입니다. 정식 OpenRouter와 `openai-chat`에서만 유효합니다. |
| `modelOpenRouterRouting?` | `Record` | 공급자 전반의 OpenRouter 선호도를 덮어쓰는 정확한 모델 id별 재정의입니다. |
@@ -578,3 +580,8 @@ source 재정의가 0이면 꺼지고, 전역 0이어도 source에 양수 재정
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes`는 모델을 저장된 Anthropic OAuth 계정 ID에 연결합니다. 풀이 활성화되면 대소문자를 구분하는 `match` 글롭의 첫 일치가 최초 선택과 429 재시도를 제한합니다. `fallback: true`는 해당 경로에 적격 계정이 없을 때만 일반 풀로 확장합니다.
+
+
+### `promptLengthPricing`
+
+선택 항목 `promptLengthPricing`은 네 기본 요율을 확장합니다. `mode: "automatic"` 또는 생략은 카탈로그의 컨텍스트 규칙을 유지하고, `mode: "flat"`은 이를 비활성화합니다. `mode: "custom"`은 양의 안전한 정수 `thresholdTokens`, `"gt"` (>) 또는 `"gte"` (≥)인 `comparison`, 네 절대 대체 요율을 담은 `rates`를 사용합니다. 임계값에는 캐시 읽기와 쓰기를 포함한 모든 프롬프트 토큰이 포함되며 출력은 제외됩니다. 선택한 요율은 출력을 포함한 전체 요청에 적용되고 임계값 양쪽에서 카탈로그 규칙을 대체합니다. Fast/Priority 조정은 요율을 선택한 뒤 한 번만 적용되며, 해당 모델에 대해 카탈로그가 공개한 Priority 관계를 따릅니다. 추정치는 카탈로그가 그 관계를 `lower-bound`로 선언한 경우(현재 xAI Grok)에만 하한으로 표시됩니다. 카탈로그 규칙이 없는 모델은 Priority 배수를 한 번만 적용합니다. **Models → Price**에서 설정합니다. **Reset to automatic**은 기본 요율과 규칙을 포함한 전체 재정의를 삭제합니다. 기존 설정은 호환되며 이 설정은 표시되는 추정 비용에만 해당합니다.
diff --git a/docs-site/src/content/docs/ko/reference/proxy-formats.md b/docs-site/src/content/docs/ko/reference/proxy-formats.md
index 9cad2c81f9a..03ae6b806ca 100644
--- a/docs-site/src/content/docs/ko/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/ko/reference/proxy-formats.md
@@ -40,6 +40,8 @@ Chat Completions와 Messages 엔드포인트는 지연된 도구 카탈로그를
별도로, 모든 인바운드 와이어에서 공급자의 `default.` 네임스페이스 호출은 요청이 선언한 원래 도구 이름으로 정규화됩니다(`default.Read` → `Read`).
+네이티브 Messages는 `tools: []`와 함께 제공된 시스템 메시지의 인라인 도구 정의도 인식합니다. 해당 이름은 영구적으로 제거될 때까지 호출할 수 있으며, 다른 클라이언트 도구를 허용하지는 않습니다. 호스팅된 MCP 호출과 서버 도구의 인수 스트림은 네이티브 형식을 유지합니다.
+
## 엔드포인트 개요
| 클라이언트 표면 | 엔드포인트 | 성공한 비스트리밍 결과 | 성공한 스트리밍 또는 소켓 결과 |
diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md
index 70cf9514f98..435e83f1a90 100644
--- a/docs-site/src/content/docs/reference/adapters.md
+++ b/docs-site/src/content/docs/reference/adapters.md
@@ -164,6 +164,11 @@ body and response, with narrow compatibility rewrites for routed gateways.
`forward` uses configured static headers without relaying caller authorization; `key` uses the
configured provider key.
+When history from Google is replayed to canonical ChatGPT Codex or the official OpenAI Responses
+API, the adapter removes item-level `extra_content` from the outgoing HTTP request or WebSocket
+create. Native `/responses/compact` applies the same projection. The original history keeps the Google
+thought signature for later Google turns. Other Responses gateways retain their extensions.
+
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;
@@ -172,6 +177,46 @@ with `forwardClientHeaders`; provider `headers` win for this option, and credent
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.
+### Native compaction on custom gateways
+
+Responses compatibility alone is not evidence of native compaction support. Custom gateways
+continue using portable text summaries unless the operator declares the relevant capability:
+
+- `supportsNativeCompactionTrigger: true` preserves v2 `compaction_trigger` requests and native output.
+- `supportsNativeCompactEndpoint: true` opts key/local/forward routes into the separate v1 endpoint.
+ OAuth retains routed compaction and its existing credential lifecycle. V2 support does not imply v1 support.
+- `decodesNativeCompactionBlobs: true` preserves native blobs on subsequent turns when there is
+ no known serving-provider mismatch. Neither generation flag implies this replay capability.
+
+Custom native v1 also honors `preserveResponsesReasoningContent: true` for plaintext reasoning
+input, just like ordinary custom Responses requests. Absent or `false` keeps content blank;
+OpenAI-operated compact destinations keep blanking even if the flag is set. Internal `ocxr1:`
+envelopes are always removed. Plaintext preservation is independent of the generation flags,
+native blob replay capability, and serving-identity checks.
+
+If the custom provider also requires adjacent tool results (`requiresAdjacentResponsesToolResults: true`),
+both ordinary Responses and native v1 remove opaque reasoning ciphertext and require plaintext replay.
+Missing `reasoning_text` is filled from the item's `summary_text`, or a minimal placeholder if no
+usable summary exists. Existing plaintext is preserved. OpenAI-operated compact keeps its own policy.
+
+For a gateway whose **v2 generation and replay** have both been verified, merge these fields into
+its existing provider configuration, retaining its real URL and configured authentication:
+
+```json
+{
+ "adapter": "openai-responses",
+ "supportsNativeCompactionTrigger": true,
+ "decodesNativeCompactionBlobs": true
+}
+```
+
+These flags do not make the gateway an official OpenAI destination, forward caller credentials,
+change account selection, or bypass normal compatibility sanitization. Validate an actual compact
+response and continued conversation, not just HTTP 200. Keep blobs on the backend that minted them;
+cross-provider compaction routing overrides still use portable summaries. V1 fallback summaries
+remain portable even when the destination separately accepts v2 triggers. Native-capable routes
+are excluded from portable emergency compaction recovery for their respective protocol.
+
Adapter selection does not select the upstream transport. Eligible requests can use the
[upstream WebSocket proxy route](/reference/proxy-formats/#json-and-sse-output); invalid or unsupported
WebSocket proxy settings fall back to HTTP/SSE. HTTP fetch-based Responses handling uses the
diff --git a/docs-site/src/content/docs/reference/architecture.md b/docs-site/src/content/docs/reference/architecture.md
index d9a323ae3c5..02885f50ef5 100644
--- a/docs-site/src/content/docs/reference/architecture.md
+++ b/docs-site/src/content/docs/reference/architecture.md
@@ -73,7 +73,8 @@ the `server/responses.ts` facade and its `server/responses/*.ts` modules:
status, latency, provider/model labels, and best-effort token usage without changing the response.
The pre-dispatch input estimate follows what the routed adapter actually sends. For `openai-chat`
-models outside `preserveReasoningContentModels`, the adapter drops replayed assistant thinking, so
+models excluded by the effective Chat thinking replay policy (an explicit
+`preserveReasoningContentModels` list, or DeepSeek Flash/Pro detection when unset), the adapter drops replayed assistant thinking, so
the estimate excludes it too; history that never reaches the provider therefore cannot cause a local
context-limit refusal. Models that preserve reasoning, and other adapters,
send that thinking and still count it.
diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md
index 32368c3265f..22214a24ed2 100644
--- a/docs-site/src/content/docs/reference/cli.md
+++ b/docs-site/src/content/docs/reference/cli.md
@@ -146,8 +146,9 @@ proxy.
`ocx system codex-cli-update check` needs no live proxy and makes no package-registry request. It
inspects bounded provenance metadata for the configured install candidate, including its redacted
executable location and ownership evidence. Trusted published-launcher context authenticates that candidate snapshot,
-not a successful Codex execution. Because this one-shot command never executes Codex, environment and persisted candidates
-remain report-only (`managed: false`, normally `selection_unattested`) and `selectionAttested` remains `false`.
+not a successful Codex execution. On POSIX, `selectionAttested` can be `true` when the runtime resolver
+selects the same canonical environment candidate without executing it. A linked npm-global package
+manifest can then establish `managed: true` when it proves ownership and, if a candidate version is reported, that version matches the manifest; persisted candidates remain unattested.
The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`, and `selectionAttested`.
Inspecting the configured candidate requires a trusted published-launcher context;
a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report
@@ -156,6 +157,13 @@ only a proof-captured absolute environment candidate can receive lexical app-bun
every other Windows candidate fails closed. The command does not install or repair software, execute
Codex or npm, control a running process, or write configuration/cache state.
+`ocx system codex-cli-update plan --json` adds a read-only update decision for an attested POSIX
+npm-global installation. It resolves `latest` through the official npm registry, validates the
+exact version and SHA-512 integrity, and refuses active sessions or incomplete process observation.
+Registry queries use isolated temporary npm configuration, cache and logs with best-effort cleanup.
+No software is installed and no process is signalled. Windows returns `windows_inspection_deferred`.
+See [planning details](/reference/cli/agents/#read-only-codex-cli-update-plan).
+
For Windows x64 installation observation, see [the `attest` command](/reference/cli/agents/#explicit-installation-observation-on-windows-x64). Without explicit paths it observes the selected candidate identified from the proof-bound launcher snapshot; it does not grant update authority or attest runtime selection.
List or status is the default where unambiguous. Use `--json` only on leaves that support it, and
diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md
index 882c7ef59db..a5c2e39e01d 100644
--- a/docs-site/src/content/docs/reference/cli/agents.md
+++ b/docs-site/src/content/docs/reference/cli/agents.md
@@ -302,6 +302,7 @@ explicit `lastResort: false`. `--image-input auto|disabled` and
`--reasoning-effort-mode strict|adaptive` preserve omission, while explicit `auto`
and `strict` override saved disabled/adaptive values. Reasoning-effort mode is
separate from `--effort-mode fallback|force`, which governs the default effort.
+`--allow-metered-targets on|off` grants or clears operator consent for marked combo targets on a loopback-only proxy; public and linked callers still need configured-key consent.
`--native-alias on|off` preserves explicit false; the legacy bare flag means true.
Turning it off may require the intended alias clear, `--native-alias off --alias -`,
to avoid retaining an incompatible native alias. Set/rename is an upsert without
@@ -415,7 +416,8 @@ Inspect proxy requests, usage, storage, memory, and debug data. The direct alias
| Alias | Equivalent resource |
| --- | --- |
| `ocx logs [filters] [--follow] [--json|--jsonl|--events]` | `ocx observe logs` |
-| `ocx usage [--range ] [--since --until ] [--surface ] [--provider ] [--model ] [--api-key-id ] [--json]` | `ocx observe usage` |
+| `ocx usage [--range ] [--since --until ] [--surface ] [--provider ] [--model ] [--api-key-id ] [--search ] [--top <1-1000>] [--json]` | `ocx observe usage` |
+| `ocx usage compare [--range ] [--json]` | `ocx observe usage compare` |
| `ocx storage [--json]` | `ocx observe storage` |
| `ocx memory [--json]` | `ocx observe memory` |
@@ -450,6 +452,43 @@ Any displayed totals reflect readable records only. If a filter has no readable
the warning and guidance instead of total lines; skipped records may contain matches.
`--json` preserves the response-level `usageIncomplete` diagnostic and reason.
+### Compare a range with the previous equal window
+
+```bash
+ocx usage compare --range today --json
+ocx usage compare --range 7d --provider xai
+```
+
+`ocx observe usage compare` is the equivalent family form. The command reads `GET /api/usage`
+twice: the named range ending now, then the same range evaluated one equal window earlier
+(one local day for `today`/`1d`, seven for `7d`, thirty for `30d`). Default range is `30d`.
+`--range all` is refused because open history has no previous equal window. `--since` and
+`--until` are not accepted; the windows are derived. Surface, provider, model, and API-key
+filters apply to both reads. Human output prints current, previous, signed delta, and percent
+change. `--json` is `{schemaVersion,range,current,previous,delta,change,incomplete}`. The two
+reads are sequential, not an atomic snapshot. Custom `since`/`until` on each response must
+match the requested window. Both reads use the same connection and credential. If the
+connection, account, or credential changes during the command, it reports an owner
+mismatch and asks you to retry instead of printing a comparison.
+
+### Page indexed request history
+
+```bash
+ocx logs history --provider xai --status 200 --limit 50 --json
+ocx logs history --cursor '' --json
+ocx logs get --json
+```
+
+`ocx observe logs history` and `ocx observe logs get` are the equivalent family
+forms. These read the derived request-history index, not the recent `ocx logs`
+ring. `--limit` is 1–100 (default 50). `--from` and `--to` accept epoch
+milliseconds or full ISO datetimes with a timezone and are inclusive. `--json`
+returns `{entries,hasMore,nextCursor,index}`. `--jsonl` emits rows only; when
+more pages exist, `nextCursor=…` is printed on stderr so a caller can resume
+without mixing the token into the row stream. Empty matches succeed. `get`
+returns one canonical row by request ID; `ocx logs explain` remains the
+route-decision view for that same ID.
+
### Filter a bounded log snapshot
```bash
@@ -489,7 +528,7 @@ ocx usage --range 7d --search 'model-a' --json
`--search` matches a trimmed case-insensitive substring in model, provider or
resolved model after reading the report. It sorts model rows by descending total
-tokens with stable ties and keeps up to 100. JSON adds
+tokens with stable ties and keeps up to 100 by default. JSON adds
`modelView:{query,matchedModelCount,returnedModelCount,limit:100,truncated}`;
human output labels the view and shows its selected model rows. Report totals,
provider/day/account rows, exact filters and incomplete/window metadata remain
@@ -500,6 +539,8 @@ preserves the existing output. Exact `--provider`/`--model` still scope the
underlying report. Search does not broaden selected-key or connected-client
self scope and is never sent as a new Hub API query parameter.
+`ocx usage --top 25` (also `ocx observe usage --top 25`) sets the human model table limit to an integer from 1 to 1000. The default is 10, or 100 with `--search`; the limit applies after search filtering. It changes local display only: report totals, provider/account rows and API parameters stay unchanged. With `--json`, a valid `--top` is ignored; JSON retains its existing full report or 100-row search view. Invalid limits return exit 2 before fetching.
+
### Saved companion usage totals
```bash
@@ -1020,10 +1061,15 @@ the CLI, the API, and the GUI use the same bytes.
Manage headless runtime settings, startup, sync, diagnostics, and updates.
-`ocx system codex-restart --yes` restarts Codex app-servers and fully quits and relaunches the
-Codex desktop app, through the same module as `ocx sync --restart-codex`. When the proxy itself
-is running inside the Codex app, the command refuses with an actionable message instead of
-promising a handoff it cannot complete.
+`ocx system codex-restart --yes` restarts Codex app-servers through the same module as
+`ocx sync --restart-codex`. It quits and relaunches the Codex desktop app when the app launched before the current model catalog was written or this request changed
+its `model_catalog_json` binding, because the model picker is built at
+launch. Otherwise the app stays open, respawns its own app-server, and keeps unsaved drafts and
+pending approvals. If the app or catalog state cannot be read, it takes the full restart. When
+the proxy itself is running inside the Codex app, the desktop restart refuses with an actionable
+message instead of promising a handoff it cannot complete.
+
+A `model_catalog_json` binding changed by this request also forces the desktop restart, even when the shell started after the catalog file was written.
```bash
ocx system settings --stream-mode eager-relay
@@ -1077,9 +1123,9 @@ ocx system codex-cli-update check --json
`check` makes no package-registry request and inspects bounded configured-candidate provenance evidence,
including a redacted executable location and ownership evidence. Trusted published-launcher context authenticates
-the candidate snapshot, not successful Codex execution. Because this one-shot command never executes Codex,
-environment and persisted candidates remain report-only (`managed: false`, normally `selection_unattested`);
-`selectionAttested` remains `false`. The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`,
+the candidate snapshot, not successful Codex execution. On POSIX, selection is attested only when the runtime
+resolver selects the same canonical environment candidate without executing it. A linked npm-global manifest
+can then establish `managed: true` when it proves ownership and, if a candidate version is reported, that version matches the manifest; persisted candidates remain unattested. The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`,
and `selectionAttested`. Inspecting the configured candidate requires a trusted published-launcher context;
a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report
`candidate_unavailable` on POSIX. On Windows this first slice performs no candidate or configuration filesystem I/O:
@@ -1094,6 +1140,26 @@ and are never classified as managed.
On Windows, a captured bare command such as `CODEX_CLI_PATH=codex`, a remote path, or a device path reports `candidate_path_unavailable` instead. Those cases have a captured candidate; its path is not eligible for this inspection.
+#### Read-only Codex CLI update plan
+
+```bash
+ocx system codex-cli-update plan --channel latest --json
+```
+
+Run through the published Node launcher with the intended `CODEX_CLI_PATH` in the shell environment.
+The plan requires an attested POSIX npm-global installation and a linked package manifest version.
+It queries the official npm registry for an exact version, SHA-512 integrity and same-origin tarball,
+using the launcher's captured PATH and isolated temporary npm configuration, cache and logs. Proxy
+settings and `NODE_EXTRA_CA_CERTS` remain supported; cleanup is best-effort. No Codex command runs.
+
+An applicable plan reports `applicable: true` and a deterministic `planId` binding platform,
+installation-root digest, installed version, channel and target version/integrity. The plan is never
+persisted. It refuses downgrades, already-current versions, active Codex sessions and incomplete
+process observation. Windows returns `windows_inspection_deferred` before registry/process queries.
+A refusal exits 0: inspect `applicable` and `refusal`. The printed install argv is indicative; this
+command installs nothing, changes no application state and never signals a session. No apply command
+is exposed, and a plan ID is not installation authorization.
+
#### Explicit installation observation on Windows x64
```text
diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md
index 1a8f7a509d1..9a1d434ad74 100644
--- a/docs-site/src/content/docs/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/reference/cli/lifecycle.md
@@ -245,7 +245,7 @@ intended.
Repair one thread that was compacted through a routed provider before resuming it through native
Codex. The command reads the exact thread selected by UUID, saves a private byte-for-byte backup,
-then converts only OpenCodeX-owned `ocx1:` compaction state into a plain summary that native Codex
+then converts only OpenCodeX-owned `ocx1:`/`ocx2:` compaction state into a plain summary that native Codex
can replay. Native encrypted content and other threads are left unchanged. Close the selected
thread before running the command; a concurrent rollout change makes recovery stop without
replacing the file.
@@ -493,6 +493,8 @@ upstream status but never consumes or resumes a grant.
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+An explicitly requested restart also runs after a successful committed `config.toml` change, even if the catalog and cache bytes are unchanged. A configuration-only no-op, refusal, preflight, or rolled-back write does not trigger it. Without a restart flag, sync never automatically restarts Codex.
+
Fetch the live model list from every configured provider and re-inject the merged catalog into Codex.
Run it after adding a provider or to refresh available models.
@@ -1139,3 +1141,13 @@ Use `ocx connect --pairing-code-stdin`, `ocx connect status`, `ocx sync`,
On Linux, `ocx service install` checks `loginctl show-user -p Linger` before printing boot advice. Enabled linger is reported as already enabled. Disabled linger gets the `loginctl enable-linger $USER` hint; missing or unreadable diagnostics state the uncertainty and do not fail installation.
On macOS, `ocx service status` distinguishes a loaded Desktop login item from a valid enabled registration awaiting the next login. `ocx status` credits the latter in restart safety when the same Desktop app currently supervises the proxy. An incorrect plist label or command, a disabled entry, an unverified load probe, or changed process identity still cannot earn protection.
+
+## Custom Codex provider consent
+
+`ocx codex-provider status` lists the provider IDs approved for the selected Codex home.
+`ocx codex-provider allow ` asks before allowing URL-only management on start/sync;
+`--yes` is explicit noninteractive consent for that named provider.
+`ocx codex-provider revoke ` restores an unchanged managed URL and revokes permission.
+These commands are local and do not require a running proxy. See
+[custom provider routing](/guides/codex-integration/#opt-in-a-custom-provider-to-managed-local-routing)
+for eligibility, restore behavior and transport preservation.
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 27387b52951..f4ba4e6f112 100644
--- a/docs-site/src/content/docs/reference/cli/providers-accounts.md
+++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md
@@ -218,6 +218,13 @@ as OrcaRouter are reused until the provider revokes them); API-key login provide
key, validate it when possible, and save the resulting provider config. The command prints the
currently accepted OAuth and API-key provider ids when the name is missing or unknown.
+Failed OAuth logins exit with status 1 and print a fixed diagnostic: the HTTP status and standard
+OAuth error code (such as `invalid_grant`) when the provider returned one, `network_error` or
+`timeout` when the provider could not be reached, `configuration_error` with the reason when a
+local setting such as a Codex account namespace blocks the login, and `oauth_authentication_failed`
+otherwise. Pool 2 (`anthropic2`) also reports `configuration_error` before prompting when a custom
+provider owns that name or its config file cannot be read or parsed.
+
Use the same command to **reauthenticate** after `ocx status` / `ocx doctor` reports
reauthentication required or a terminal refresh failure (or use Reauthenticate in the dashboard).
Codex pool accounts are not one of those OAuth or API-key providers, but `ocx login codex` reaches
@@ -238,8 +245,18 @@ ocx login xai
ocx login anthropic
ocx login orcarouter-oauth # browser consent + S256 PKCE
ocx login orcarouter # paste an existing API key
+ocx login anthropic --headless # no browser, no local port
```
+`--headless` runs the **code-display** login for hosts whose browser cannot reach the proxy's loopback
+callback (containers, remote servers). The command prints an authorization URL; approve it in any
+browser, and the provider shows a `code#state` string to paste back at the prompt. No callback port
+is bound and no browser is launched. Only `anthropic` and `anthropic2` support it today; any other provider exits
+with the list of providers that do. For Codex accounts use the device-code login instead,
+`ocx account login codex --device` — `ocx login codex --headless` is refused rather than passed on.
+The paste window is 10 minutes, and each login accepts one code: if the exchange fails, start a new login. A successful login becomes the provider's active account, exactly
+like a browser login.
+
OAuth reauthentication preserves operator settings such as model selections, pricing overrides,
and account failover preferences. Login-owned transport/authentication fields and registry-owned
catalog metadata are refreshed. A live-discovery provider keeps its selected default model; a
@@ -302,7 +319,7 @@ Only fields named by `supported` may be written; null is not false, and an inert
policy is not active pooling. Threshold zero and `enabled:false` are independent.
Use `--enabled on|off`, `--threshold 0..100`, `--strategy`, `--sticky 1..100`, and
`--quota-window` only where supported. OpenAI does not support pool enabled/window
-writes; `reset-first` is OpenAI-only, `least-loaded` Kiro-only, and Anthropic windows
+writes; `reset-first` is for OpenAI and generic OAuth pools (never Anthropic), `least-loaded` Kiro-only, and Anthropic windows
are `five-hour`, `weekly`, `max-utilization`. Omitted fields stay unchanged.
Read DTO fields such as routes, per-account concurrency or Anthropic `nativeMessages`
do not imply a setter in this command; use only the listed CLI options. A save that
@@ -351,6 +368,32 @@ grant counts, nullable dates, cooldown, pending operation and journal availabili
Empty grants are not the same as unavailable status. It never consumes a grant
or resumes a pending spend. Consumption remains a human GUI operation.
+### Use remaining included quota
+
+To keep using a selected account past a proactive switching threshold or the main-account
+protection threshold, explicitly enable its remaining-quota preference:
+
+```bash
+ocx account use-remaining openai main on
+ocx account use openai main
+ocx account use-remaining openai main status --json
+ocx account use-remaining openai main off
+```
+
+Replace `main` with a unique pool-account ID or alias to target an added account.
+Enabling this disables proactive usage switching for that account. For the main login, it also
+moves both effective hard-lock thresholds to 100%, so a 95% reading can still admit a request.
+The preference persists across restarts until turned off; original threshold settings remain
+stored and resume applying when it is disabled. It does not select an account itself or override
+round-robin scheduling, manual/automatic pause, entitlement checks, reauthentication, or upstream
+cooldowns. Paid-credit permission remains a separate setting; enabling this never opts an account
+into credits or redeems a reset grant. The default-on main hard lock still refuses at 100%.
+
+`GET /api/codex-auth/accounts/use-remaining?id=` reads the live policy.
+`PUT /api/codex-auth/accounts/use-remaining` accepts `{ id, useRemainingQuota: boolean }`.
+Both require the existing management authentication. This control is currently exposed through
+the CLI and API.
+
### Main-account quota protection
In **Codex settings → Multi-auth → Advanced settings**, **Block main account**
@@ -374,6 +417,22 @@ percentages, for example `{ "short": 90, "long": 98 }`. Both must be integers fr
and `short` must not exceed `long`; omitted fields use the defaults. The settings API accepts the
same object and returns the effective values in `mainAccountHardLock.thresholds`.
+**While the lock blocks, requests can fall back to another model instead of failing.** The
+**Fallback while blocked** row sits beside the protection switch and works like the shadow-call
+row: picking a model — for example `deepseek/deepseek-chat` — turns the reroute on immediately,
+the optional thinking effort applies to it, and the switch pauses the reroute without forgetting
+the model. While the block is in force and no Codex account can serve a request for a native Codex
+model, that request is routed to the fallback model (with the configured effort) instead of being
+refused. A usable Pool account still serves the request first; provider-qualified selectors,
+combos, account-qualified rows and Luna Reserve keep their own destinations; and a fallback that
+itself names a native Codex model is ignored, because rerouting gpt-* to gpt-* would only re-enter
+the same refusal. The lock itself is unchanged: it releases on its own as before, and the reroute
+ends with it. Configure `codexMainAccountHardLockFallback` in `config.json` as
+`{ "model": "deepseek/deepseek-chat", "reasoningEffort": "low" }`; `enabled: false` pauses the
+reroute while keeping the model. An absent section, an omitted model, or a malformed section means
+requests are refused as usual; `reasoningEffort` must be a declared effort. The settings API
+accepts the same object, rejects invalid writes before mutation, and `null` clears the section.
+
The dashboard and `ocx status` show **possible usage outside opencodex** when fresh readings rise
by at least one point in the same reset episode without recent main-account activity through this
proxy. This is an advisory observation, not proof: a long-running turn can also cause it. The lock
@@ -638,7 +697,9 @@ from that account's own credential and Cloud Code Assist project id. The per-acc
talks to Google's Cloud Code Assist host through the pinned outbound transport, regardless of a
configured `baseUrl`: a custom base URL is a routing choice for requests, not a second source of
Google's accounting for a stored credential. An account without a project id, or one whose probe
-is redirected or fails, shows `unavailable`.
+is redirected or fails, shows `unavailable`. When fallback model discovery supplies quota readings,
+detailed displays retain up to 256 model diagnostics with distinct bounded labels. These rows do not
+affect provider summary headlines, severity, account ranking, routing, or reset notifications.
Devin rows come from Cognition's `GetUserStatus` for that account's own key, sent to its
allowlisted api-server host. If an older credential has no host, the probe uses the configured
@@ -815,12 +876,50 @@ openai: { provider, autoSwitchThreshold: number, enabled: boolean }
generic OAuth: { provider, autoSwitchThreshold: number | null, enabled: boolean, poolEnabled: boolean | null, inert: boolean | null }
```
+### `ocx account preserve-signed-in openai [on|off|status] [--apply-to-bound] [--json]`
+
+Opt in to **use other eligible accounts first and keep the signed-in ChatGPT/Codex Desktop
+identity as fallback**. A bare invocation or `status` reads the running proxy's policy, affected
+entry IDs, manual pin and next unbound choice. `on` persists `codexPreserveSignedInAccount: true`;
+`off` restores your existing custom account order without rewriting any priorities.
+
+The reserved group includes native `__main__` and ordinary pool entries with the same selected
+ChatGPT account/workspace **and** normalized member email. Sharing a workspace alone is insufficient.
+The group follows the latest physical login observed by the proxy's native-login lifecycle;
+no incoming request credential can define it. If member/workspace evidence is unavailable,
+`identityKnown: false` reports that only main can be identified. Status obtains a fresh owned
+observation; a busy or unreadable native profile returns 503 without applying the change.
+
+All four Codex strategies prefer other identities with quota headroom, preserving custom order
+within each group. Reserved entries remain fallback when preferred tiers drain. **Fallback can
+consume the signed-in quota**, subject to existing model access, pause, cooldown, reauthentication,
+hard-lock and paid-credit consent checks. This policy does not grant paid-credit consent.
+
+Healthy existing bindings stay in place, and manual pins and exact account-qualified model routes
+override the preference. To apply it to ongoing work, run:
+
+```bash
+ocx account preserve-signed-in openai on --apply-to-bound
+```
+
+This explicitly releases only affected bindings and any affected manual pin for the **next request**;
+in-flight requests keep their captured account. Unrelated bindings and pins remain. Moving work may
+lose its warm prompt cache, and account-scoped uploads or continuation state can prevent a move.
+`codexAccountPriorityFailback` remains a separate opt-in, with its existing quota-strategy,
+positive-threshold and fresh-observation conditions; it can also move work toward a preferred group.
+
+For **never use this identity through the pool**, use `ocx account pause openai main` instead:
+linked pause excludes main and matching pool entries without changing the desktop login file.
+Only requests that reach OpenCodex are controlled. This does not unlock an already-disabled
+desktop composer or promise to fix client-side quota gating.
+
### `ocx account priority [<-100..100|first|earlier|normal|later|last|reset>] [--json]`
Reads or sets one Codex pool account's selection order: **higher is used earlier**, the default is
`0`, and the range is `-100` through `100`. Only the `openai` Codex pool is ordered, so other
providers exit 1. `main` targets the Codex Desktop login, which is ordered like any other pool
-account — `ocx account priority openai main last` is how you keep it as the reserve.
+account. `ocx account priority openai main last` lowers only that entry; use
+`preserve-signed-in` above to reserve its matching ordinary pool entries too.
Preset words stand in for small integers: `first` is `+2`, `earlier` is `+1`, `normal` is `0`,
`later` is `-1`, and `last` is `-2`. `reset` returns the account to the default and drops its stored
@@ -1041,6 +1140,12 @@ verified official-price fallbacks. Unknown models return `null`; no price is inv
Automatic defaults are derived on read and do not populate `modelCosts` in your config,
so catalog updates remain effective. Use `set-price` to save provider-specific rates.
+`price --json` also preserves a saved `promptLengthPricing` policy in `cost`; the four
+`effectiveCost` rates are the base rates, before evaluating a request's prompt length or tier.
+Use **Models → Price** or the management API to edit a prompt-length policy. Existing `set-price`
+commands replace the four base rates and keep the model's saved Flat or Custom threshold policy;
+`--auto` removes both the rates and any policy.
+
Discover command syntax with `ocx models --help` before live work. `list-custom`
reads local configuration; `add` and `remove` save locally unless given `--live`.
Local custom writes opportunistically synchronize when a proxy exists. The remaining
diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md
index 06cf68b87c6..baba267dadf 100644
--- a/docs-site/src/content/docs/reference/configuration.md
+++ b/docs-site/src/content/docs/reference/configuration.md
@@ -48,6 +48,10 @@ Aliases are optional short request names. They never change the native model id
Aliases match case-insensitively. A model alias works as `or/opus` or, when globally unique, bare `opus`; an ambiguous bare alias reports its qualified candidates. Codex model pickers show the qualified alias while preserving the canonical `provider/model` routing id. A provider's `defaultAliases` value overrides `defaultModelAliases`. Built-ins are skipped when multiple models in one provider match the same pattern.
+### Native reasoning retention
+
+Native reasoning ciphertext retention on model and account changes has two opt-in settings, both off by default. Configure them on the dashboard overview or see [Native reasoning retention](/reference/configuration/server/#native-reasoning-retention) for the exact scope and experimental account behavior.
+
### Cursor effort rows
`cursorEffortRows` is an optional boolean and defaults to `false`. When enabled, the raw OpenAI-style
diff --git a/docs-site/src/content/docs/reference/configuration/advisor.md b/docs-site/src/content/docs/reference/configuration/advisor.md
index 139256cb206..036b322d589 100644
--- a/docs-site/src/content/docs/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: Advisor
-description: The OpenCodex-owned expert consultation sidecar — a configured expert model advises routed workers, with manual and preflight policies.
+description: Independent expert consultation with manual, preflight and evidence-based adaptive policies.
---
The advisor is an independent expert model that reviews the worker's task and returns advice.
@@ -16,6 +16,8 @@ never sees — even a worker that never spawns anything can be advised.
## Configuration
+On the dashboard **Advisor** page (under the **Subagents** sidebar row), choose the expert model from the catalog selector. It saves the exact catalog identifier, including native, provider, account-qualified, custom and combo selectors. Disabled models and models awaiting initial selection are not offered as new choices. A saved identifier missing from the selectable catalog remains visible with a warning and is preserved until you choose another model or clear it. Catalog loading failures preserve the selection and provide Retry. Choose a model that can return text advice: the catalog does not guarantee consultation compatibility. The CLI and API continue to accept router-supported model strings.
+
```json
{
"advisor": {
@@ -33,14 +35,15 @@ never sees — even a worker that never spawns anything can be advised.
| `enabled?` | `boolean` | `false` | Master switch. Disabled means zero advisor behavior on the request path. Enabling does not record consent. |
| `model?` | `string` | — | The expert model. Any model string the router accepts: a bare native model (`gpt-6-astra`), an explicit `provider/model` (`anthropic/claude-sonnet-4-6`, `xai/grok-...`), or an account-qualified native model. Cross-provider is fully supported: the worker and the advisor do not need to share a provider. |
| `effort?` | `string` | `"max"` | Reasoning effort for the advisor call (`low` through `ultra`). |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | When the advisor is consulted. |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | When the advisor is consulted. |
+| `maxConsultationsPerTask?` | `integer` | `3` | Adaptive task allowance (1–3 attempts), including manual calls. |
| `timeoutMs?` | `number` | `120000` | Loopback consultation timeout. |
| `contextSharingConsent?` | `"v1"` | absent | Operator consent to send task context to the configured Advisor provider. Only `"v1"` is current. Absent, stale, or any other value means no task context is sent. |
Manage it from the dashboard **Advisor** page (the context-sharing checkbox starts unchecked) or
with `ocx advisor status`, `ocx advisor on --ack-context-sharing`, `ocx advisor consent`,
`ocx advisor consent --revoke`, `ocx advisor off`, and
-`ocx advisor set --model --effort --policy `.
+`ocx advisor set --model --effort --policy `.
`ocx advisor on` without `--ack-context-sharing` does not enable cross-provider sharing when
consent is missing: it prints this disclosure and stops. `ocx advisor set` does not grant consent.
@@ -56,6 +59,42 @@ consent is missing: it prints this disclosure and stops. `ocx advisor set` does
attempted consultation that FAILS is not silently treated as advice: the task retries after
the failure's ledger entry expires, so a temporary advisor outage does not permanently
silence the policy.
+- **`adaptive`** — adds failed-repair consultations to the initial preflight attempt. The proxy
+ must observe a completed failing validation, a subsequent successful file-edit tool result,
+ and a new execution of the **same command in the same working directory** failing again.
+ A passing validation clears its failure state. A check started before the edit cannot count
+ as a retry after the edit. The initial and failed-repair stages have separate dedup claims.
+ Only clients with a stable conversation identity receive automatic adaptive consultations.
+
+### Adaptive budget and evidence
+
+```sh
+ocx advisor set --policy adaptive --max-consultations 2
+ocx advisor activity --json
+```
+
+The adaptive task budget counts manual and automatic **attempts**, across requests and worker
+model changes. The server reserves an attempt before dispatch in a private
+`advisor-budget.sqlite` file in `OPENCODEX_HOME`; proxy restarts do not reset it. A task is
+identified by a digest of conversation identity and the latest user-task boundary. Reservations
+expire 24 hours after the first attempt, so this is a bounded task-lifetime approximation,
+not a permanent session or daily monetary budget. Only digests, counts and expiry times are
+stored. There is room for 512 live tasks; a full, unavailable or locked budget skips the expert
+call while the worker continues. Failed or disconnected dispatched calls retain their reservation
+because they may have incurred usage. Revoked consent before dispatch spends no allowance.
+Clients without a stable identity keep manual, request-bounded access but receive no automatic
+adaptive consultation. Legacy `manual` and `preflight` retain their previous task-dedup behavior.
+
+The collector recognizes paired shell calls with explicit numeric exit status, including
+`exec_command`, `shell_command`, `shell`, `Bash`, and terminal `write_stdin` polls. Supported
+commands include common Bun/npm/pnpm/yarn test, typecheck, lint, check and build commands,
+`pytest`, `python -m pytest`, `tsc`, `eslint`, `cargo test/check/clippy/build` and `go test/vet`.
+File edits use successful `apply_patch`, `Write`, `Edit`, `write_file` or `edit_file` results.
+Single Code Mode helpers with literal JSON arguments (bare keys accepted) are recognized;
+arbitrary JavaScript, batches, shell pipelines/substitutions, shell-based edits, ambiguous
+output and error prose alone provide no trigger. At most the latest 512 messages since the
+latest user task are examined. Failed-repair context includes the validation, bounded first
+and repeated failure excerpts, and source message references before the general transcript.
## Consent
@@ -98,7 +137,7 @@ prevents structural breakout and forged fields; it cannot guarantee prompt-injec
A dedicated consultation-result protocol could distinguish advice from ordinary user input more
strongly.
-Each request allows at most three consultations and four Advisor-owned worker continuations.
+Each request allows at most three consultations, including automatic attempts, and four Advisor-owned worker continuations.
After consultation exhaustion the `advisor` tool is removed. A repeated call receives one final
paired limit result; if the worker calls it again, typed 502 `advisor_continuation_limit` ends the
request without another hidden worker call. The bound is shared with empty-completion retries.
@@ -113,6 +152,17 @@ model** — never merged into the worker's token counts — and each consultatio
`[advisor]` log line with trigger, duration, status, and usage, so an advisor call is always
provable from the logs.
+The **Recent advisor activity** section and `GET /api/advisor/activity` show the latest 100
+consultation metadata records from this process, retained for up to 24 hours. The dashboard
+refreshes them every five seconds while visible. Records include trigger, status, worker and
+expert models, duration, reported input/output tokens, requested effort and adapter-mapped
+effort. A budget refusal is shown as blocked. Records contain no task text, commands, advice,
+thread identifiers or provider error bodies and are cleared on proxy restart.
+
+Mapped effort describes what the adapter serialized, not a guarantee about the provider's
+hidden reasoning. Unreported effort or usage stays unknown; it is never presented as zero
+usage or confirmed reasoning support. No advice-adoption score or exact monetary cost is inferred.
+
## Failure behavior
The advisor fails open. A DISPATCHED consultation that fails (unavailable model, misconfigured
@@ -125,12 +175,12 @@ A manual `advisor()` call without current consent returns a consent-required too
sends nothing. An advisor failure never fails the coding request, and a
consultation never switches the session's main model.
-## PR1 limitations
+## Limitations
- Native OpenAI passthrough turns (ChatGPT-pool workers) do not get the synthetic tool; advisor
support covers routed (translated) providers. Preflight consultation applies to run-turn
adapters; the tool does not.
-- No adaptive trigger: no stuck detection, repeated-failure analysis, escalation tiers, multiple
- advisors, or advisor voting. `manual` and `preflight` are the only policies.
+- No semantic stuck detection, completion review, escalation tiers, multiple advisors or voting.
+ Adaptive consultation requires the structured validation/edit/retry evidence described above.
- The preflight dedup ledger is process-local; after a proxy restart, a task in progress may
receive one more preflight attempt.
diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md
index 50c3520b0df..700e060ca9b 100644
--- a/docs-site/src/content/docs/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/reference/configuration/providers.md
@@ -15,6 +15,13 @@ compatibility fallback. With it, the order is: discovered/configured limits (the
Azure metadata, model-ID metadata, enabled Context cap as the window, then 128k. An enabled
Context cap still limits the result. Built-in providers keep their existing behavior and ignore the flag.
+`GET /api/providers` includes `inferContextWindow` only when configured, preserving both `true` and
+`false`. To change it, `PATCH /api/providers?name=` accepts `true`, `false`, or `null` to
+clear the setting and restore the default-off behavior; other types return `400`. Omitting it keeps
+the current setting. A same-name `POST /api/providers` also keeps an omitted setting, including
+`false` and any change committed while the save was validating. A submitted boolean wins, including
+`false`, even if the stored value is `true`.
+
```json
{ "providers": { "my-relay": { "adapter": "anthropic", "baseUrl": "https://relay.example/v1", "inferContextWindow": true } } }
```
@@ -69,6 +76,7 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `contextCapValue?` | `number` | `350000` | Default used on first enable. A later enable restores the selected provider value. Updating the global value with `setAll: true` changes enabled caps only; `setAll: true` without a value enables all configured providers at the current global value. |
| `codexAccounts?` | `CodexAccount[]` | `[]` | ChatGPT/Codex pool account metadata managed by Codex Auth. Secrets live separately in `codex-accounts.json`. |
| `pausedCodexAccountIds?` | `string[]` | `[]` | Accounts excluded from Pool selection until resumed, including the main `__main__` account when paused. Manual pause and resume also update existing main and pool entries for the same account and workspace. |
+| `codexUseRemainingQuotaAccountIds?` | `string[]` | `[]` | Accounts opted into using their remaining included quota. Disables proactive usage switching for listed accounts; for `__main__`, effective short/long hard-lock thresholds become 100%. Original thresholds remain stored and apply again after disabling. Pause, authentication, eligibility, cooldown and paid-credit permission still apply. Managed by `ocx account use-remaining openai on`; this persistent preference does not select the account. |
| `creditCodexAccountIds?` | `string[]` | `[]` | Accounts allowed to keep serving from ChatGPT credits after a usage limit, including the main `__main__` account. Upstream does not refuse an account that holds credits at 100%; it serves the request and draws the balance. Spending is opt-in: an account not listed here is skipped by selection while one of its usage windows (weekly or monthly, only monthly on 30-day plans, or the 5-hour window) reads 100% with its reset still ahead, and returns once that reset passes. A weekly or monthly reading without a reset time does not hold the account; a 5-hour reading at 100% without a reset holds it only while that reading is still fresh. When no other account is available, automatic selection finds none rather than spending credits. An unlisted `__main__` is also refused, like a hard-lock refusal, when a request names it or carries its credential. Listing `__main__` does not lift the main-account hard lock (on by default at 98%), which still stops the main login first; turn the lock off to let the main account spend credits (a lock at 100% still stops it at 100%). New accounts start unlisted. Managed by the **Use credits** switch in the Codex Auth header and the **Use credits after limit** switch in each account card's **⋯** menu. |
| `codexQuotaAutoRefresh?` | `Record` | `{}` | Per-Codex-login-account opt-in for automatic `fiveHour` and `weekly` window activation in Pool mode; Direct mode does not run this worker. In Providers/Codex Auth **Advanced settings**, one control enables or disables both supported windows across all current main and added accounts. New accounts are not opted in automatically. Enable skips windows absent from live WHAM data; disable also clears stale enabled windows. The UI reuses granular `/api/settings` writes, reconciles partial failures, and retries the original ON/OFF intent without replacing unrelated settings or completed reset markers. The API still rejects enabling an unavailable window with HTTP 409. At a reported reset time, opencodex sends one minimal non-stored Codex message using that account's quota and persists the activated timestamp. This does not apply to API-key providers. |
| `codexAccountNamespaces?` | `Record` | — | Optional map from an arbitrary public model selector to a stored Codex account target. When account-qualified picker rows are enabled, each selector whose target is present adds separate `/` rows to the Codex picker; each row uses only that account. With any selector active, bare native rows are hidden in the picker, but their ids remain routable and listed by raw `/v1/models` unless explicitly disabled. |
@@ -76,6 +84,7 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `activeCodexAccountId?` | `string` | — | Manually selected Pool account for the next request. Selection clears thread affinity; in-flight requests keep captured credentials. |
| `codexAccountPriorities?` | `Record` | — | Per-account selection order for the Codex pool: account id → integer from `-100` to `100`, **higher is used earlier**, absent means `0`. This is an ordering boundary, not an eligibility one: selection narrows the already-eligible accounts to the highest tier that still has quota headroom, and `accountPoolStrategy` then picks within that tier. A tier is skipped only when every member is at or above its own nonzero effective usage threshold (account override or global default), cooling down, soft-avoided, paused, or needs reauthentication — unknown quota never drains a tier. Ordering never makes an ineligible account selectable and by default, never re-binds a thread that already has an account. The main `__main__` account participates on equal terms, which is how the Codex Desktop login can be set to drain last. With no entries the pool behaves exactly as before. A malformed map is ignored with a console warning (ordering off, no config repair). Managed by `ocx account priority` and the Codex Auth page. |
| `activeCodexAccountPinned?` | `string` | — | Account id the operator last selected by hand. While set, a higher `codexAccountPriorities` tier cannot preempt it until the pin is released by drain, exclusion, deletion, or an explicit failover/promotion away. Ordinary round-robin movement inside the capped tier does not release it. Writing any `codexAccountPriorities` entry also releases the pin, so a pin made before an order existed cannot outrank one set afterward. `GET /api/codex-auth/active` reports both whether the effective account is pinned (`pinned`) and the account carrying the ceiling (`pinnedAccountId`). |
+| `codexPreserveSignedInAccount?` | `boolean` | `false` | Prefer other eligible Codex Pool identities before native main and ordinary entries matching its selected workspace and member email. Existing custom priorities are preserved inside each group. Healthy bindings and manual pins remain; exact account routes and Direct mode are unchanged. Fallback may consume signed-in quota under all existing guards. Use [account preserve-signed-in](/reference/cli/providers-accounts/#ocx-account-preserve-signed-in-openai-onoffstatus---apply-to-bound---json) for status or an explicit next-request binding transition. |
| `codexAccountPriorityFailback?` | `boolean` | `false` | Opt-in return of ongoing Codex Pool tasks to a usable higher-priority account under the `quota` strategy. Requests trigger a background quota refresh at most once every five minutes, including failed refresh attempts. When recent quota observations show headroom, the next request may rebind even if its current account is below the threshold. This explicit preference can discard a warm prompt cache; it does not change default cache affinity. No requests means no polling. Manual pins, model access, cooldowns, pauses and reauthentication exclusions still apply; in-flight requests keep their captured account. Requires the current source account's effective threshold to be positive (an override of 0 disables it; a positive override works with global 0); exact account routes and Direct mode are unchanged. |
| `autoSwitchThreshold?` | `number` | `80` | Usage threshold for placing new/unbound work. `quota` can re-evaluate unbound tasks on their next request once usage crosses this threshold. Bound tasks keep their account past the threshold by default (`pool.cacheAffinity`); they leave only when that account is exhausted or otherwise cannot serve, and then only for an account with genuine quota headroom and strictly lower usage. Set `pool.cacheAffinity: false` to re-evaluate bound tasks at this threshold, still only onto such a destination. `fill-first` uses it only as the drain point for unbound assignment; normal `round-robin` selection does not use it. The score uses the hottest known 5h, weekly, or 30d quota window. `0` disables usage-based proactive switching only, not unbound assignment or failure recovery. |
| `codexAccountAutoSwitchThresholds?` | `Record` | — | Per-account overrides for `autoSwitchThreshold`: account id → integer from `0` to `100`. An absent entry inherits the global value; `0` disables usage-driven switching only when that account is the source. `quota` uses the current source account's effective threshold for bound-task re-evaluation and unbound switching. Fill-first drain and selection-order tier headroom are evaluated against each account's own effective threshold; main-account pins and subagent fallback also use the relevant account's effective threshold. Supports the main `__main__` account. A malformed map is ignored with a warning. Managed by each account card on the Codex Auth page; disabling an override removes its entry. Enabling the override copies the current global threshold into a fixed account value. The override, including `0`, takes precedence over later global changes. Disabling it sends `threshold: null`, removes the entry, and restores inheritance of the current global threshold and future global changes. |
@@ -91,7 +100,7 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `maxUpstreamBodyBytes?` | `number` | `0` | Opt-in ceiling, in bytes, on a serialized native Responses **passthrough** body. `0` or omitted disables it — no limit is inferred for any destination. When set, a built body above the ceiling is refused locally before the send: streaming turns receive a terminal `response.failed` / `context_length_exceeded` so the client compacts instead of resending, and non-streaming turns receive a `413` naming the size, the number of embedded `input_image` items, and roughly how many megabytes of image data they represent. Checked at every build and rebuild point, including OAuth-refresh replay and alternate-account retry. Translated adapter paths are not covered. There is deliberately no default: the only measured ceiling here belongs to the WebSocket transport, which already falls back to HTTP for oversized turns, so a default would refuse requests that currently succeed. Set it when your gateway has a known request-size limit and you would rather see an actionable local error than an opaque upstream failure. |
| `maxInboundBodyBytes?` | `number` | `0` | Opt-in ceiling, in bytes, on a decompressed **inbound** data-plane request body — the mirror of `maxUpstreamBodyBytes` above. `0` or omitted keeps the built-in 256 MiB default. Raise it when a large-context session can no longer compact: Codex replays the whole history to the compaction model, so on the 922k-token opt-in window the compaction request is itself the one that crosses the limit, and the session is stuck at the only operation that would have shrunk it. Clamped to 1 MiB–512 MiB. The ceiling is not negotiable: the reader materializes the body several times over (wire bytes, decoded bytes, the decoded string, and the parsed object graph), so peak memory is a multiple of whatever is admitted, and an unbounded value would be a memory exhaustion lever. Applies to `/v1/responses`, `/v1/responses/compact`, `/v1/chat/completions`, and `/v1/messages`. The listener's accept size is fixed when the proxy binds, so a change takes effect on restart. A body above the limit is refused locally with HTTP 413 and `code: "inbound_body_too_large"`, which is deliberately distinct from the `context_length_exceeded` 413 a provider size refusal produces. |
| `modelCacheTtlMs?` | `number` | `300000` | Freshness window for the per-provider `/models` cache. |
-| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. |
+| `cacheRetention?` | `"none" \| "short" \| "long"` | `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. When unset, a `/v1/messages` request on a translated route whose own `cache_control` breakpoints all ask for `ttl: "1h"` is treated as `"long"`; a request that mixes 1h and 5m breakpoints keeps the 5-minute default, and an explicit value always wins. |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | off | Optional proactive OAuth refresh and Codex-account warmup policy. |
Across these strategies, each account's effective threshold is its `codexAccountAutoSwitchThresholds` entry when present, otherwise global `autoSwitchThreshold`. A zero override disables only proactive usage switching; startup binding, hard-lock, cooldown, entitlement checks, and failure recovery remain enforced.
@@ -144,6 +153,15 @@ and `modelAutoCompactTokenLimits` can narrow its window and soft compaction budg
`modelAutoCompactTokenLimits: { "gpt-6-astra": 700000 }` lowers the long-window default of 784,800.
An explicit smaller provider cap or target limit still wins, including native-alias combos.
+Without a configured hard limit, the native catalog keeps the advertised `max_context_window`
+separate from the default `context_window` (for Astra, 872,000 versus 272,000). Compaction still
+uses the effective default window. To give one model 500,000 tokens and the others 300,000, set
+`providers.openai.contextWindow` to `300000` and
+`providers.openai.modelContextWindows` to `{ "gpt-6.1-sol": 500000 }`.
+The per-model value overrides the provider window; `providerContextCaps.openai`, when enabled,
+remains a ceiling for every native model and must be at least `500000` for this example.
+Each model's own capacity still bounds the result.
+
The API row has 1,050,000 context, 922,000 maximum input, 128,000 maximum output, text/image input,
and API reasoning efforts through `max`. OpenCodex's routed synthetic Ultra control retains its
existing wire-effort mapping; it is not an additional API effort. There is no Astra `-pro` alias.
@@ -245,12 +263,15 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `baseUrl` | `string` | Upstream API base URL. Most built-in fixed endpoints ignore a mismatch; collision-safe key presets preserve an older same-named custom destination. |
| `proxy?` | `string \| null` | Per-provider egress route. Omit it to inherit the global proxy decision; use `"direct"` or `null` to force direct egress; or provide an absolute `http://`, `https://`, `socks5://`, or `socks5h://` proxy URL. An empty string is rejected. |
| `noProxy?` | `string \| string[]` | Destinations this provider reaches directly, using `NO_PROXY` host-pattern syntax. A match bypasses both this provider's own proxy and an inherited global proxy. |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. `maxConcurrentRequests` is a positive integer cap on in-flight requests. A provider or model rule may use the concurrency cap alone; provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay or narrow concurrency. Queue waits do not consume the upstream response-header timeout. HTTP and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. A concurrency-capped canonical Responses WebSocket turn uses HTTP/SSE so its lease can be released when the response body completes, errors, or is cancelled. For `runTurn` adapters, including Cursor, the cap counts active turns rather than physical sends: RunSSE and BidiAppend may overlap within one turn, while another turn waits. Follow-up sends still obey start intervals. |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. `maxConcurrentRequests` is a positive integer cap on in-flight requests. A provider or model rule may use the concurrency cap alone; provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay or narrow concurrency. Queue waits do not consume the upstream response-header timeout. HTTP and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. A concurrency-capped canonical Responses WebSocket turn uses HTTP/SSE so its lease can be released when the response body completes, errors, or is cancelled. For `runTurn` adapters, including Cursor, the cap counts active turns rather than physical sends: RunSSE and BidiAppend may overlap within one turn, while another turn waits. Follow-up sends still obey start intervals. |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | Normal-request key selection. Default `active` preserves the saved key and existing failure rotation; `round-robin` distributes request-local selections across eligible API-key pool references without changing the saved active key. It balances allocations, not tokens or cost. |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | Uniform per-key-reference RPM, minimum start interval and concurrency limits, shared across models. These compose with provider and exact-model upper bounds. Duplicate references share a bucket; separate references in one upstream project do not imply independent quota. TPM and daily quotas are not enforced. |
| `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. |
| `tlsProfile?` | `"antigravity-browser"` | **Use at your own risk.** Opt-in browser-like TLS handshake (through the optional `wreq-js` dependency) for the canonical `google-antigravity` OAuth provider. It changes only how the connection looks on the wire; it is not an official Google client, and it does not change what Google's terms allow. Google can still detect, rate-limit, suspend, or ban the account you signed in with, and you alone carry that risk. It is off by default and accepted only with the Google adapter, Cloud Code Assist mode, and Google's canonical HTTPS Antigravity hosts. Redirects stay manual. The provider's own `proxy`/`noProxy` route is carried by the TLS transport; a route it cannot keep fails the request instead of leaving by another path, which includes any direct route while `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY` is set. `GET /api/providers` reports the profile state as `pending`, `active`, or `failed`. Omitting the field keeps the normal Bun transport and never loads the dependency. |
| `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. |
| `chatCompletionsPath?` | `string` | Relative resource path for `openai-chat` requests, the mirror of `responsesPath` and subject to the same shape rules. Needed when one upstream serves Chat Completions and Responses under different prefixes: a per-model wire override changes the adapter and leaves `baseUrl` alone, so without this an opted-in Chat request would be sent to the Responses base. Z.AI is the shipped example. |
| `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. |
+| `projectResponsesLiteHostedSearchHistory?` | `boolean` | Default off. Set the actual boolean `true` to opt this provider into lossy projection of unreferenced hosted-search history for ordinary requests to exact canonical ChatGPT Codex `/responses` with explicit Responses Lite intent. Omission or `false` retains structural history; strings and numbers are invalid. It does not enable Lite, change transport, or affect native compact, public OpenAI API/other destinations, or existing portable/routed compaction projection. See [Lite hosted-search history](/reference/proxy-formats/#codex-native-ordinary-lite-hosted-search-history). |
| `upstreamWebsocket?` | `boolean` | Opt-in upstream Responses WebSocket transport for `openai-responses` requests (default false). Honored only for the first-party `https://api.openai.com/v1` upstream; custom-provider endpoints always use bounded HTTP/SSE because Bun cannot enforce an inbound WebSocket message limit before allocating the complete message. For the canonical ChatGPT `openai` provider, omitting it keeps the upstream WebSocket on eligible turns, an explicit `false` sends streaming turns over HTTP/SSE, and provider management rejects `true`; with `false`, native mid-turn steering and injection are unavailable. The field is independent of the client-facing `websockets` setting and changes neither the endpoint nor the credential. Plain HTTP remains on SSE; non-Responses paths and `openai-chat` requests stay on HTTP. |
| `supportsServiceTier?` | `boolean` | Tri-state canonical Fast capability fallback. `true` publishes Fast in the catalog, satisfies service-tier routing requirements, contributes a supported fingerprint, and lets fast mode inject the provider's canonical wire value on a compatible final adapter. `false` strips the field and never injects, and exact model declarations cannot reopen it. Absent leaves the provider unclassified: fast mode does not inject or normalize a canonical caller value, and caller values obey the final wire's forwarding permission (`chatServiceTier` on Chat; passthrough on Responses). The registry classifies canonical OpenAI (`true`), DeepSeek, and Volcengine Ark (`false`); set it explicitly only for custom gateways that genuinely support tiers. |
| `responseTierAuthoritative?` | `boolean` | Whether the response tier can confirm or deny Fast. Set `false` explicitly for routes with non-authoritative response metadata; omission preserves existing behavior. This does not enable Fast or change outbound parameters. See [Response service-tier authority](#response-service-tier-authority). |
@@ -258,10 +279,10 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `modelSupportsServiceTier?` | `Record` | Exact upstream model capability overrides. Exact `true` enables canonical Fast for that model; exact `false` narrows provider defaults. An explicit provider-level `supportsServiceTier: false` remains fail-closed and cannot be reopened. Exact `true` does not authorize foreign caller-tier forwarding on Chat. Undeclared models fall back to provider-wide behavior. Management `PATCH /api/providers` merges entries and accepts `null` to clear one. |
| `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` | Default off for custom providers. In **Providers → Settings**, enable **Replay plaintext reasoning** for a custom Responses endpoint only if it accepts this history. This sends prior plaintext reasoning to that endpoint and may increase token use. 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 or a self-hosted Kimi on vLLM. Proxy-minted `ocxr1` envelopes are always stripped. A hand-configured `openai-responses` provider without it logs one warning per host the first time plaintext reasoning is dropped. `preserveReasoningContentModels` does not affect this wire. |
+| `preserveResponsesReasoningContent?` | `boolean` | Default off for custom providers. In **Providers → Settings**, enable **Replay plaintext reasoning** for a custom Responses endpoint only if it accepts this history. This sends prior plaintext reasoning to that endpoint and may increase token use. Keep plaintext reasoning content on replayed Responses reasoning items instead of blanking it, including custom native v1 compact input (blanking is the ChatGPT backend's rule). Enable for upstreams whose contract accepts reasoning replay, such as DeepSeek or a self-hosted Kimi on vLLM. Proxy-minted `ocxr1` envelopes are always stripped. A hand-configured `openai-responses` provider without it logs one warning per host the first time plaintext reasoning is dropped. `preserveReasoningContentModels` does not affect this wire. |
| `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. |
+| `preserveResponsesMessageMetadata?` | `boolean` | Default off. Preserve private `internal_chat_message_metadata_passthrough` on input items for explicitly trusted launcher relays, including custom native v1 compact requests. 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, including custom native v1 compact requests, 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). |
| `apiKeyTransport?` | `"x-api-key" \| "bearer"` | Anthropic key header style. Defaults to native `x-api-key`; valid only for key-auth `anthropic` providers. |
@@ -283,9 +304,12 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `minMaxOutputTokens?` | `number` | Optional positive integer floor for explicit positive `max_output_tokens` budgets on `azure-openai`/`azure` Responses routes. Unset preserves the caller's cap. Setting it authorizes raising a smaller cap, which can increase output usage. Omitted or invalid request budgets are unchanged. |
| `defaultMaxOutputTokens?` | `number` | Provider-wide `openai-chat` fallback when the client omits `max_output_tokens`. |
| `modelMaxOutputTokens?` | `Record` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. |
-| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an 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. |
+| `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. |
+| `supportsNativeCompactionTrigger?` | `boolean` | `openai-responses` only: declare support for native v2 `compaction_trigger` input items. Defaults off for noncanonical gateways; preserves native compaction rather than requesting a portable text summary. Independent of the v1 endpoint and blob replay capabilities. |
+| `supportsNativeCompactEndpoint?` | `boolean` | `openai-responses` only: declare support for native v1 `POST /responses/compact`. Defaults off for custom gateways. Key/local/forward routes use the provider's Responses URL plus `/compact` and its normal transport authentication; OAuth keeps routed compaction and its existing credential lifecycle. Does not enable v2 triggers or grant official OpenAI identity. |
+| `decodesNativeCompactionBlobs?` | `boolean` | Declare that this destination can replay native, non-`ocx1:` compaction blobs. Independent of either generation capability; a known serving-provider mismatch still degrades foreign ciphertext. Enable only after testing compact and a subsequent turn on the same backend. |
| `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`. |
@@ -339,7 +363,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `anthropicEofTolerance?` | `boolean` | Let an Anthropic-compatible gateway complete a stream that ends before `message_stop`, only when visible text or a complete JSON-object tool input was received. Off by default. |
| `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. |
| `googleToolSchemaPolicy?` | `"compatible" \| "reject-lossy"` | Google only. Omitted/`compatible` preserves the compatible tool schema and existing non-direct 400 repair. `reject-lossy` rejects initial schema loss or an indeterminate bounded comparison before dispatch and withholds a Vertex or Cloud Code Assist repair that would open constraints. Direct AI Studio never performs that repair. |
-| `directGeminiWireRenames?` | `boolean` | Google only. Applies only to direct AI Studio requests. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex preserves the requested model ID, and Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. |
+| `directGeminiWireRenames?` | `boolean` | Google only. Applies in AI Studio mode, including custom Google-compatible URLs. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex and canonical keyed OpenCode Zen routes preserve the requested model ID regardless of this setting; Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. |
| `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. |
| — | — | Antigravity account quota probes (`retrieveUserQuota` and `retrieveUserQuotaSummary`) always go to Google's own Cloud Code host through the pinned outbound transport, regardless of a configured `baseUrl`; the account bearer is never sent to an operator-configured endpoint and a redirect aborts the probe. Only the model-list fallback still honors `baseUrl`. |
| — | — | If Antigravity quota summary returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Inference and model discovery retain the IDE User-Agent. |
@@ -377,6 +401,23 @@ For example, this applies a provider-wide concurrency cap and a stricter cap to
The concurrency slot stays occupied until the upstream request finishes, including the final streamed response bytes. Requests above the applicable provider and model limits wait in the pacing queue; when a slot is released, the next eligible request is admitted. Queue waiting does not consume the upstream response-header timeout.
+For multiple API-key pool entries, opt into normal-request distribution separately from the existing failure-selection `apiKeyPoolStrategy`:
+
+```json
+{
+ "requestPacing": {
+ "enabled": true,
+ "keySelection": "round-robin",
+ "maxConcurrentRequests": 10,
+ "keys": { "requestsPerMinute": 15, "maxConcurrentRequests": 1 }
+ }
+}
+```
+
+This admits at most ten active requests across the provider and one per credential reference, with starts on each reference at least four seconds apart. These are local upper bounds, not a promise of upstream capacity. A provider-wide RPM setting, when present, remains an aggregate limit and does not multiply with key count. Key limits span models; model rules remain additional restrictions. Projects sharing quota need an appropriate aggregate limit; TPM and daily quotas are not tracked here.
+
+Round-robin selects keys per request without saving a new active key. Known 401/429 cooldowns are skipped; when none remains eligible, new requests receive a retryable 429 instead of repeatedly sending. Already queued work normally retains its selected key and may wait behind that key even when another is idle. If that selection becomes cooled or invalid, the request selects an eligible key again and reacquires pacing admission for it; the bounded queue expires rather than waiting forever. Selection and diagnostics are process-local, reset on restart, and do not equal balanced token usage or billing. Starts count pacing admissions, including work cancelled before dispatch. OAuth and forwarded credentials keep their existing selection. The dashboard shows key handles, starts, in-flight work, queued work and known cooldowns, never secret key values. These limits cover the inference pacing executor, not the standalone Images relay's direct fetch.
+
### Azure OpenAI model metadata discovery
For a base URL whose hostname ends in `.openai.azure.com`, discovery fills missing image/text
@@ -413,6 +454,10 @@ The destination is the adapter, the base URL (scheme and host compared without r
`hideRawReasoning` is an operator display policy, so a save of the same provider name keeps its stored value whenever the request omits it, including after a destination move. A value sent in the request wins, including `false`.
+The optional `inferContextWindow` setting also survives same-name saves that omit it, including
+destination moves. Its read, update, and clear behavior is described under
+[Context windows on custom providers](#context-windows-on-custom-providers).
+
`PATCH /api/providers?name=` changes only the fields it names and keeps every other stored field, whatever the destination. It accepts all eight settings; `null` clears one. It also accepts a boolean `hideRawReasoning` or `null` to clear it. For the two reasoning lists an empty array is stored as an explicit opt-out rather than removed.
With `webSearchBridge` enabled, a search continuation stays bound to the API-key selection that
@@ -536,9 +581,17 @@ optional pin in a hand-edited file is ignored on load without discarding the res
Codex reads `auto_review_model_override` from the catalog row of the current turn's model to
choose the model that reviews approval requests. The root `auto_review_model` setting in
-`$CODEX_HOME/config.toml` applies one reviewer to task catalog rows; the provider-scoped fields
-below override it per provider. The [provider guide](/guides/providers/#approval-reviewer-per-provider)
-has the operator workflow and a worked example.
+`$CODEX_HOME/config.toml` supplies Codex's fallback. The dashboard's global **Auto-review override**
+in Model settings stores `autoReviewOverride` in `~/.opencodex/config.json` and takes precedence
+over that fallback; the provider-scoped fields below still win on their own rows. The global setting
+must be enabled and name a model. An enabled setting with no model stays unapplied and leaves the
+Codex fallback in effect. A catalog refresh is a separate state: after a save it can stay pending,
+and the Models page reports it so you can retry with `ocx sync`. CLI users can read the setting with
+`ocx models auto-review status` or change it with
+`ocx models auto-review set [--enabled ]`; use `-` to clear the model. CLI output
+recommends `ocx sync` when a refresh can be retried. If a refresh fails, the setting remains saved
+and the command exits 1. Resolve the failure before syncing. The [provider
+guide](/guides/providers/#approval-reviewer-per-provider) has the operator workflow and a worked example.
When native OpenAI rows are included and the final catalog has an ordinary bare native row
other than Reserve (hidden rows count), OpenCodex keeps the hidden `codex-auto-review` row so
@@ -1137,7 +1190,8 @@ second account.
| --- | --- | --- | --- |
| `oauthAccountFailover.enabled?` | `boolean` | presence-driven | Global override for the **pre-dispatch account preference** only. `false` stops a healthy request being steered toward the account with more known headroom. It does **not** disable 429 rotation. |
| `providers..oauthAccountFailover.enabled?` | `boolean` | inherits | Per-provider override for the same preference; beats the global setting in either direction. `false` declines the preference for this provider even when the global setting is `true`, and `true` opts this provider in even when the global setting is `false`. Reactive 429 rotation is unaffected either way. |
-| `providers..oauthAccountFailover.strategy?` | `"quota" \| "round-robin" \| "fill-first" \| "least-loaded"` (Kiro only) | — | Kiro's `least-loaded` picks the eligible account with the fewest in-flight requests, using account order for ties. It requires `pool.kernel` and proactive preference; other strategies retain their existing rules. |
+| `providers..oauthAccountFailover.sessionAffinity?` | `"conversation" \| "none"` | `"none"` | Opt-in conversation pinning for generic OAuth pools. Requires proactive `enabled: true` (provider or inherited global value). Existing sessions keep their account ahead of the new-session strategy until it becomes unavailable. |
+| `providers..oauthAccountFailover.strategy?` | `"quota" \| "round-robin" \| "fill-first" \| "reset-first" \| "least-loaded"` (Kiro only) | — | Kiro's `least-loaded` picks the eligible account with the fewest in-flight requests, using account order for ties. It requires `pool.kernel` and proactive preference; other strategies retain their existing rules. `reset-first` also requires `pool.kernel`: among eligible accounts that are not exhausted, the first attempt (while the proactive preference is on) and a 429 rotation both prefer the account whose quota resets soonest (the weekly window when one is reported, otherwise the 5-hour window), with more headroom breaking ties. Exhausted accounts go last. Without quota evidence, or when a passive provider has evidence for only some accounts, it falls back to headroom order. The Anthropic pool does not accept it. |
| `providers.kiro.oauthAccountFailover.maxConcurrentPerAccount?` | integer 1–100 | unlimited | A process-local per-account queue holds a slot through response completion or cancellation. A full selected account waits at most 250 ms, then returns 503 `account_capacity` with `Retry-After: 1`. The cap does not move the request to another account. |
| `providers..oauthAccountFailover.autoSwitchThreshold?` | `number` | `80` | 0–100 usage percent at which `fill-first` advances off the active account (#695). Set with `ocx account auto-switch threshold `. Read only under `pool.kernel` with `strategy: "fill-first"`; an account with no measured usage counts as under the threshold. |
| `providers..oauthAccountFailover.stickyLimit?` | `number` | `1` | Successful dispatches retained on one `round-robin` selection, 1–100 (#695). Read only under `pool.kernel` with `strategy: "round-robin"`. |
@@ -1166,9 +1220,26 @@ and the `ocx account strategy` / `ocx account auto-switch` / `ocx account sticky
preference. `quotaWindow` is not part of the generic contract. Codex (`/api/codex-auth`) and Anthropic
(`anthropicAccountPool`) keep their own contracts unchanged.
-Deliberately narrower than `anthropicAccountPool`: no session affinity, no quota-ranked
-selection, no probe leases. It answers one question — the account that just returned 429 is
-cooled, is there another one available.
+Set `providers..oauthAccountFailover.sessionAffinity` to `"conversation"` together with
+`enabled: true` to keep a conversation on its admitted account. Session identity uses the client
+thread marker, session headers, thread header, then `prompt_cache_key`; a shared Desktop cache
+cohort is excluded. Bindings are isolated by provider, configuration home and authenticated caller
+principal. Unauthenticated local callers share the local scope. No session marker means no binding.
+
+New sessions use the existing strategy (`pool.kernel` still gates round-robin/fill-first).
+Existing sessions stay pinned even when another account has more quota. A pause, reauthentication
+requirement, family cooldown or incompatible model roster releases the binding; existing bounded
+failure recovery binds the admitted replacement. Kiro's concurrency cap still waits or refuses
+rather than moving a healthy bound conversation merely to balance load.
+
+In this mode automatic choices, including unkeyed requests and failovers, stay request-local:
+the dashboard's selected account remains the operator's selection. Request history still attributes
+the serving account. Manual selection, same-account reselection and roster/policy revision changes
+invalidate provider bindings, including writes from another process. An ordinary refresh keeps the
+same account. Fill-first retains its current account in memory without changing the saved selection.
+Bindings expire after 24 hours without admission, are bounded to 2,000 entries process-wide
+(including fill-first cursors), and are lost on restart. Set `sessionAffinity: "none"` or disable
+proactive preference to release them. Codex and Anthropic retain their own affinity contracts.
The Codex pool and the Anthropic pool are excluded and keep their own rotation; enabling this
changes neither. A provider with a single stored account is a strict no-op, and no cooldown is
@@ -1224,7 +1295,7 @@ provider in question.
### Managed record shapes
-`apiKeys[]` entries contain `id`, `name`, generated `key`, and ISO `createdAt` strings.
+`apiKeys[]` entries contain `id`, `name`, generated `key`, and ISO `createdAt` strings. Optional `allowMeteredComboTargets: true` explicitly permits targets marked `metered: true`; omitted or false consent denies them. The environment token never qualifies. Local loopback-only callers can instead use per-combo `allowMeteredTargets: true`; configured-key and linked callers still need key consent. See [Metered fallback consent](/guides/combos/#metered-fallback-consent).
`codexAccounts[]` entries require `id`, `email`, and `isMain`, with optional `plan`,
`chatgptAccountId`, and privacy-safe `logLabel`. These records are normally dashboard-managed.
@@ -1462,6 +1533,32 @@ model key.
## Storing keys in the OS keychain
+For Vertex, the dedicated API-key pool input (`POST /api/providers/keys`) accepts complete GCP
+`service_account` or `authorized_user` credential JSON after the provider has been created.
+The provider create/edit form and `ocx provider add --api-key` refuse credential JSON before
+saving: create the provider without a key, then add the JSON through its key pool. Paste the
+JSON contents, not a Windows, Unix, or relative file path. Multiple
+JSON objects separated by commas or whitespace are stored together as separate pool entries;
+the first becomes active. Configuration contains only `gcp-sa:` references, never the
+credential JSON. Mixed JSON/literal-key bulk pastes are rejected as a whole; add literal keys
+separately. Storage failures leave the configuration unchanged and report a fixed error without
+echoing credential material. Empty marker accounts are invalid and do not fall back to host credentials.
+Paste one credential object or a sequence of objects, without an enclosing array or quoted string.
+Malformed credential text is rejected. If a selected stored credential is removed or becomes unreadable,
+Vertex refuses the request even when it previously cached an access token for that credential.
+Removing a key or provider, or replacing a provider with `ocx provider add --force`, deletes its unreferenced GCP credentials after the configuration is
+saved; a credential referenced by another provider is retained. If OS-store cleanup fails, a
+warning requests manual inspection without undoing the committed configuration removal.
+Ordinary keychain store/restore actions leave GCP markers and JSON in the OS store. Restoring
+credential JSON through a `keychain:` alias is refused, and cleanup preserves accounts still
+referenced through either marker spelling.
+Using a `keychain:` alias containing GCP JSON as an API key also refuses the request; select its
+`gcp-sa:` reference for Vertex authentication instead.
+Literal API keys must be plain token text: JSON objects, arrays and quoted JSON strings are refused
+in literal-key resolution and restore, including wrappers containing escaped credential text.
+Credential writes and marker persistence share the cleanup transaction lock, so concurrent
+removal cannot delete the stored JSON between read-back and the marker commit.
+
By default a provider's `apiKey` and `apiKeyPool` sit in `config.json` (mode 0600, atomic writes).
If you would rather keep the key material out of the file, move it into the OS credential store:
@@ -1702,6 +1799,57 @@ image is absent from the list. A row declaring only `audio` or `video` therefore
image-incapable, rather than being treated as a text model by one predicate and an image target
by the other.
+### Prompt-length pricing
+
+In **Models → Price**, keep four base rates and choose a prompt-length mode:
+
+- **Automatic** keeps the catalog context rules. It is also the behavior of existing four-rate overrides without a policy.
+- **Flat rate** uses the base rates at every prompt length, disabling catalog context adjustments.
+- **Custom threshold** selects four absolute alternative rates using `>` or `≥` and one token threshold. The custom policy replaces the catalog context rule on both sides of the threshold.
+
+All prompt tokens count, including cache reads and writes, before billable-input subtraction.
+Output does not count toward the threshold. Once crossed, the alternative rates apply to the
+entire request, including output, not only the excess tokens. Custom rates are standard-speed
+rates: an applicable Fast/Priority premium applies once after the rates are selected and follows
+the catalog's relation between Priority and long-context pricing for that model. A confirmed
+Priority request shows the custom estimate as a lower bound only when the catalog declares that
+relation as `lower-bound`, as for xAI Grok, which publishes no combined rate. A model with no
+catalog context rule applies any Priority multiplier once. These settings control estimates; they
+do not change provider billing.
+**Reset to automatic** deletes the entire override and restores catalog prices and rules. Saving or
+editing the config updates live estimates and historical totals.
+
+For example, a Haiku 5.5 override with alternative rates strictly above 100,000 prompt tokens:
+
+```json
+{
+ "modelCosts": {
+ "claude-haiku-5-5": {
+ "input": 0.10,
+ "output": 0.50,
+ "cacheRead": 0.01,
+ "cacheWrite": 0.125,
+ "promptLengthPricing": {
+ "mode": "custom",
+ "thresholdTokens": 100000,
+ "comparison": "gt",
+ "rates": { "input": 0.50, "output": 2.50, "cacheRead": 0.05, "cacheWrite": 0.625 }
+ }
+ }
+ }
+}
+```
+
+This is an example of user-defined prices, not an official Haiku price list.
+Any positive safe-integer threshold, including `272000`, is supported; use `"gte"` for inclusive
+comparison. All four alternative rates are required and follow the base-rate limits, including zero.
+The other policy is `{ "mode": "flat" }`. Omit `promptLengthPricing` to retain automatic context
+rules with your base prices; an explicit `{ "mode": "automatic" }` is accepted and means the same,
+and saves from the dashboard or API store it as an absent field.
+
+Legacy `openai-multi` model price overrides retain valid prompt-length policies when startup
+migrates them to `openai`, including overrides keyed by `openai-multi/`.
+
### Renamed API-key presets
A provider saved under another name, such as `CommandCode`, inherits missing reasoning-effort metadata when its adapter and fixed API-key endpoint match a registry preset. Your explicit per-model lists, including `[]`, remain authoritative. An omitted provider-wide list inherits the preset default; an explicit list remains unchanged. This does not match OAuth, unrelated endpoints, or templated/custom endpoint presets.
@@ -1730,3 +1878,23 @@ raised output allowance would exceed that ceiling.
}
}
```
+
+### Native Chat thinking replay
+
+These Chat reasoning lists also apply to native `/v1/chat/completions` tool-call continuation.
+The proxy preserves supplied `reasoning_content` and restores omitted reasoning from upstream
+JSON or SSE responses using tool-call ids. Original reasoning replay requires a stable
+`thread-id`, `session_id`, or `session-id` header on each request and the same provider, endpoint,
+model and credential. The cache is process-local and bounded; a restart, expiry, eviction, or
+missing conversation header uses the configured minimal placeholder instead. A placeholder
+cannot reconstruct lost reasoning. Clients should retain upstream `reasoning_content` in their
+assistant history for reliable resumed conversations.
+
+When `preserveReasoningContentModels` is unset, DeepSeek Flash/Pro model names are recognized
+across versions, including `deepseek-v4.1-flash`, namespaced ids and gateway suffixes. Explicit
+lists remain authoritative: `preserveReasoningContentModels: []` disables automatic repair,
+while `requiresReasoningPlaceholderModels: []` disables placeholders and still allows cached
+original reasoning. Other model families require the preserve declaration.
+Tool-enabled DeepSeek requests also require reasoning on earlier assistant answers without tool
+calls. The configured placeholder fills these omissions; no original reasoning can be recovered
+from the tool-call cache for those turns. Preserve their supplied reasoning for full fidelity.
diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md
index ff299a2b440..3c8ef94a6a4 100644
--- a/docs-site/src/content/docs/reference/configuration/routing.md
+++ b/docs-site/src/content/docs/reference/configuration/routing.md
@@ -13,6 +13,16 @@ Routing turns the model id sent by a client into one concrete provider and upstr
| `combos?` | `Record` | `{}` | Virtual `combo/` models built from ordered provider/model targets. |
| `routingProfiles?` | `Record` | `{}` | Virtual `policy/` models that select among an explicit candidate allowlist using hard capability requirements and deterministic scoring. |
+### Model alias discovery
+
+Set `preferModelAliasIds: true` to publish configured or enabled built-in model aliases
+as the `id` in `GET /v1/models`. The default is `false`. Promoted rows retain their
+previous qualified ID in `native_id` and keep `alias_of`; both IDs remain routable.
+Models without aliases keep their existing IDs. If a short alias is ambiguous across
+providers or is shadowed by another route, the row keeps its qualified ID so selecting
+it still reaches the intended model. The qualified ID also stays when a built-in short
+alias would produce an unparseable synthetic Fast selector. Codex's own model catalog is unchanged.
+
### Codex Pool low-quota protection
`codexPool.lowQuotaProtection` applies only to stored Codex Pool accounts. The Desktop/main
@@ -138,7 +148,8 @@ namespace, and cannot use reserved bare native families such as `gpt-*`, `o1-*`,
| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
-| `targets` | `{ provider: string; model: string; weight?: number; lastResort?: boolean }[]` | required | Ordered concrete routes. `weight` is 1–10000 and defaults to `1`. `lastResort` marks an emergency-only target; see `cooldownWaitPolicy`. |
+| `targets` | `{ provider: string; model: string; weight?: number; lastResort?: boolean; metered?: boolean }[]` | required | Ordered concrete routes. `weight` is 1–10000 and defaults to `1`. `lastResort` marks an emergency-only target; see `cooldownWaitPolicy`. `metered: true` requires key consent or loopback-only combo consent; see [consent](/guides/combos/#metered-fallback-consent). |
+| `allowMeteredTargets?` | `boolean` | `false` | Operator consent for local callers on a loopback-only proxy. A non-loopback listener disables this flag; configured-key and linked callers still need key consent. |
| `strategy?` | `"failover" \| "round-robin" \| "random" \| "least-used" \| "reset-window" \| "jev"` | `"failover"` | Selection strategy. Target order is failover priority; weights shape round-robin and random draws; least-used follows recorded successes; reset-window follows the soonest quota reset; JEV makes one bounded decision for the initial eligible target and effort, then uses ordinary ordered fallback. |
| `stickyLimit?` | `number` | `1` | Successful requests retained in one round-robin batch. Range 1–100. Applies only to round-robin. |
| `cooldownMs?` | `number` | unset → upstream fallback (5 s for request-rate 429 codes `1302`/`1305`, otherwise 60 s) | Range 1–600000. When set, applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including request-rate 429s; when unset, uses the upstream fallback. Upstream signals take precedence. An explicit upstream `Retry-After` is capped at 24 hours; reset-derived, configured, and fallback cooldowns are capped at 10 minutes. |
@@ -367,7 +378,8 @@ Returned history and route-decision payloads expose only masked request metadata
(for example opaque `apiKeyId` labels). They do not include credentials, raw
prompt bodies, or provider secrets.
-CLI: `ocx logs explain `, `ocx logs rebuild-index`,
+CLI: `ocx logs history`, `ocx logs get `,
+`ocx logs explain `, `ocx logs rebuild-index`,
`ocx logs index-status`, `ocx route policy list | show | dry-run | evaluate`.
## Migration
diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md
index 45da7168de8..08bc166c3b9 100644
--- a/docs-site/src/content/docs/reference/configuration/server.md
+++ b/docs-site/src/content/docs/reference/configuration/server.md
@@ -23,6 +23,7 @@ runs helper features around provider requests.
| `websockets?` | `boolean` | `false` | Advertise and admit the client-facing Responses WebSocket path. False keeps clients on HTTP/SSE; it does not disable an eligible canonical ChatGPT upstream WS optimization. Complete-input requests may reuse an upstream connection within the same selected credential, account, thread and turn; changed handshake policy or missing identity keeps requests on separate connections. This does not trim HTTP input or create previous-response IDs. |
| `codexNativeSteering?` | `boolean` | `false` | Experimental, native-only mid-turn steering on the Responses WebSocket endpoint. Requires `websockets: true`, a compatible upstream/client, and a pinned account/model/tool surface. Validated generation settings can change in explicit saved-result continuations. Does not enable translated models or HTTP fallback. See [native steering](/guides/codex-integration/#experimental-native-mid-turn-steering). |
| `codexNativeInjection?` | `boolean` | `false` | Experimental saved function-result injection on compatible native multi-agent WebSocket turns. Requires `websockets: true`, explicit `multi_agent.enabled`, and an eligible provider. Separate from steering; no automatic tool rerun or recovery create. See [native injection](/guides/codex-integration/#experimental-native-function-result-injection). |
+| `nativeReasoningRetention?` | `{ modelSwitch?: boolean; accountSwitch?: boolean }` | both `false` | Permit reasoning ciphertext forwarding across a known model or account change on the same canonical ChatGPT backend. Account retention is experimental. See [Native reasoning retention](#native-reasoning-retention). |
| `corsAllowOrigins?` | `string[]` | `[]` | Additional exact origins allowed by CORS. Loopback origins are always allowed. Authority-based browser extension origins such as `chrome-extension://` are supported; `*` is not a wildcard. Firefox and Safari regenerate the extension UUID (per install / per browser launch), so update the entry when the origin changes. |
| `apiKeys?` | `OcxApiKey[]` | `[]` | Generated `ocx_…` data-plane admission credentials on non-loopback binds. They do not authorize management APIs; management access uses the separate credential documented in the [management reference](/reference/management-api/). Dashboard-managed. |
| `storageCleanupPolicy?` | `StorageCleanupPolicy` | disabled | Opt-in archived-session cleanup policy. Never enabled implicitly. |
@@ -41,6 +42,7 @@ runs helper features around provider requests.
| `codexProviderDisplayName?` | `string` | `"OpenCodex Proxy"` | Label Codex shows for the injected `opencodex` provider, written as its `name` field in `config.toml` and the reference profile. Presentation only: routing resolves through the provider id `opencodex`, so a rename never moves `model_provider = "opencodex"` or the `[model_providers.opencodex]` header and cannot orphan threads already tagged with that id. Codex refuses to load a provider with no name, so there is no way to omit the field — choose a neutral label instead. A blank, over-128-character, or control-character value is ignored and the default label is written. |
| `codexMainAccountHardLock?` | `boolean` | `true` | Block new identity-matched main-account requests at their per-window threshold. Only explicit `false` opts out. See [main-account protection](/reference/cli/providers-accounts/#main-account-quota-protection). |
| `codexMainAccountHardLockThresholds?` | `{ short?: number; long?: number }` | `{ short: 90, long: 98 }` | Independent 5h and weekly/monthly thresholds. Integers 80–100, with `short <= long`. Invalid persisted fields fall back to defaults; the settings API rejects invalid writes before mutation. |
+| `codexMainAccountHardLockFallback?` | `{ enabled?: boolean; model?: string; reasoningEffort?: string }` | off | Extended hard-lock settings: while the lock blocks and no Codex account can serve the request, qualifying native-Codex-model requests reroute to `model` with `reasoningEffort` instead of failing. `enabled: false` pauses the reroute and keeps the model. An absent section or model keeps the refusal behavior; a usable Pool account still serves first. `reasoningEffort` must be a declared effort; invalid persisted sections degrade to off and the settings API rejects invalid writes before mutation. See [main-account protection](/reference/cli/providers-accounts/#main-account-quota-protection). |
| `nativeMainRefresh?` | `"codex" \| "opencodex"` | auto | Who refreshes the native main ChatGPT login in `CODEX_HOME`. `"codex"` asks the Codex binary (app-server `getAuthStatus`, Codex 0.130.0 or newer, found through the same runtime resolution as `ocx sync`, including the ChatGPT app bundle) and never writes `auth.json`; this also works when Codex stores its login in the OS keyring. `"opencodex"` keeps OpenCodex's guarded `auth.json` writer. Unset picks `"codex"` when a supported binary is found; without one, both unset and `"codex"` fall back to the guarded writer. `ocx doctor` shows the active mode under Native main refresh. |
| `resetCreditAutoRedeem?` | `{ enabled?: boolean; leadTimeMinutes?: number }` | off | Opt-in: redeem the main Codex account's soonest-expiring reset credit `leadTimeMinutes` (1–60, default 10) before it expires. Every attempt re-reads the upstream credit list first and skips when the credit is gone (for example, redeemed by hand); the `redeem_request_id` is journaled in `$OPENCODEX_HOME/reset-credit-auto-redeem.json` before the call so a crash replays the same idempotent request instead of spending a second credit. Servers sharing this configuration directory coordinate reservations and settlements so one process does not replace another's request record. Logs carry a hashed account key only. |
| `syncResumeHistory?` | `boolean` | `true` | Reversible Codex App history compatibility. Original metadata is backed up and restored by `ocx stop` / `ocx restore`. |
@@ -51,6 +53,32 @@ runs helper features around provider requests.
| `visionSidecar?` | `OcxVisionSidecarConfig` | on when usable | Image-description sidecar options. |
| `images?` | `OcxImagesConfig` | automatic OpenAI selection | Standalone Images relay options for Codex `image_gen`. |
+On Linux x64/arm64 with pidfd support, service health recovery binds termination to the proxy's
+kernel process identity, so PID reuse cannot redirect it. The sentinel's watching log ends in
+`(pidfd)`. Unsupported kernels or architectures retain the existing live-parent PID check and log
+that fallback; other binding failures disable recovery for that proxy. macOS retains the live-parent
+check, and Windows retains its process handle binding.
+## Native reasoning retention
+
+The dashboard overview exposes two independent opt-ins: retain reasoning when switching models, and retain it when switching accounts. Both default off. The same policy can be saved in `config.json`:
+
+```jsonc
+{
+ "nativeReasoningRetention": {
+ "modelSwitch": true,
+ "accountSwitch": false
+ }
+}
+```
+
+This example retains reasoning `encrypted_content` on a known model switch within the same native ChatGPT destination and credential. Set `accountSwitch` to `true` only to opt into experimental cross-account forwarding. When both model and account change, both switches must be enabled. The policy applies to each actual native Combo target and pool retry; it does not grant retention to third-party or API-key destinations.
+
+Retention means that OpenCodex may forward the carried ciphertext. It cannot confirm that the upstream accepts or reuses that reasoning. A detected decrypt rejection still enters the existing bounded cleanup recovery, and the rejection memo takes precedence over either switch. Response and conversation references, foreign reasoning item ids, uploaded-file account restrictions and cache isolation continue using their own ownership rules. Native compaction blocks keep their existing cleanup behavior; direct native `/v1/responses/compact` sanitization is unchanged.
+
+Use `GET /api/native-reasoning-retention` to read the resolved booleans and `PUT` with a partial boolean object to update them. `PUT` with JSON `null` restores both defaults. Invalid fields or unknown keys are rejected; a malformed file block disables retention with a warning. Dashboard/API changes apply to subsequent requests. Local `ocx config set/unset` edits require a restart to converge the running proxy.
+
+## Upstream transport recovery
+
While the canonical ChatGPT upstream WebSocket waits for the first Responses event after
sending the create frame, it watches for liveness rather than a fixed deadline. When the socket
supports protocol pings, the proxy pings it every 15 seconds. Any inbound frame — quota,
@@ -672,6 +700,21 @@ Auto auth selects subscription when stored Claude auth is found, proxy when none
subscription with a warning when detection is inconclusive. See
[Claude Code auth mode](/guides/claude-code/#auth-mode).
+## Retained requests after compaction
+
+For translated providers and routed Responses gateways, OpenCodex automatically frames
+plain-text user requests before the latest readable Codex checkpoint as historical context.
+Requests the checkpoint reports as completed should not be repeated; retained text remains
+available for unfinished work within its existing authorization. Later user input takes priority,
+including pauses, cancellations and changes of scope. A checkpoint grants no new permission.
+
+This is enabled by default and needs no setting. Original request text and canonical thread/replay
+history are preserved. Native OpenAI Responses passthrough, summarizer turns, context wrappers,
+images/files and assistant/tool items are unchanged. The framing applies to plaintext checkpoints
+beginning with Codex's summary prefix; decoded OpenCodex checkpoints retain their existing
+continuation guidance. Client-side summarizer turns using Codex's standard compaction prompt
+also bypass framing. Framing is a prompt instruction, so model behavior can vary.
+
## Compaction routing
In **Dashboard → Overview → Compaction routing**, choose a model, which triggers it applies to,
@@ -757,6 +800,68 @@ without tool declarations. This changes only the summary request, not stored his
compaction; a failed summary does not replace the history. Restart the proxy after editing
`config.json` by hand. Dashboard saves apply immediately.
+### Reasoning retention during compaction
+
+Readable reasoning retention is an advanced, explicit opt-in and is **off by default**.
+Only `reasoningRetention.enabled: true` enables it; an omitted block, `enabled: false`,
+or legacy limits without `enabled: true` leave ordinary compaction behavior unchanged.
+When enabled for routed v1 `POST /v1/responses/compact` and routed v2 `compaction_trigger` requests, readable reasoning stays local while the selected
+model summarizes other context. After a readable summary succeeds, exact retained text returns
+alongside it in framed historical context messages, not as new assistant reasoning or instructions.
+Repeated compaction withholds those messages again. Ordinary subsequent turns replay them as history.
+
+```json
+{
+ "reasoningRetention": {
+ "enabled": true,
+ "maxContextPercent": 20,
+ "maxTokens": 100000
+ }
+}
+```
+
+Both limit fields are optional and, **when enabled**, default at runtime to `20` percent and `100000` estimated tokens.
+The effective limit is the smaller of the percentage of the compacting model's context window
+and the absolute token cap; when the context window is unknown, only the absolute cap applies.
+The estimate includes each retained message's historical-context frame, not just its reasoning text.
+Above the limit, readable text is written verbatim under the resolved OpenCodex home's
+`reasoning-archive/` (normally `~/.opencodex/reasoning-archive/`). The summarizer receives a
+path note instead of the text, and the finished readable summary includes a notice naming that file.
+New archives are private to the user on POSIX. An archive is provisional until successful replacement history
+returns its path, either in a readable summary or in a separate notice alongside a native result
+after routed v1 Combo failover. Failures, cancellation, and invalid summaries remove
+the current attempt's file. Internal retries reuse it; previously successful archives are preserved.
+Failed compaction returns no replacement history.
+
+The percentage/token limits bound reasoning returned in context, **not cumulative archive storage**.
+Each archive contains only readable reasoning admitted in that request; request admission is bounded
+by `maxInboundBodyBytes` (256 MiB by default, configurable up to 512 MiB), plus small block separators.
+There is **no aggregate archive byte/file limit, automatic expiry, or background archive cleanup**.
+Successful archives remain until you manually delete their files, including after disabling retention
+or resetting the setting. They are plaintext, contain potentially sensitive conversation reasoning,
+and share the resolved OpenCodex home of that proxy instance. Deleting an archive makes any existing
+path notice unable to recover its contents; save anything you need first. A process crash or failed
+filesystem write/cleanup can also leave a provisional file requiring manual cleanup.
+
+**Dashboard → Overview → Advanced settings → Reasoning retention** provides the explicit switch
+and both limits; **Reset and turn off** removes the override and switches retention off. GET/PUT `/api/settings` exposes `reasoningRetention`: null clears the block,
+and omission preserves it. `enabled` must be a boolean when present. Percentages must be greater than 0 and at most 100; token caps must
+be positive integers, and unknown fields are rejected. Invalid hand edits disable only this
+block with a warning, keeping unrelated providers and settings.
+
+Private provider ciphertext and signatures are not decrypted or copied into portable retained
+messages. An upstream native opaque compaction response retains its exact output item and
+ciphertext. If routed v1 Combo failover reaches a native result after withholding readable reasoning,
+replacement history also includes the retained historical messages or a notice naming their archive.
+Native compact endpoints keep their existing behavior.
+
+Routed v2 still returns exactly one compaction item. Its proxy-owned `ocx2:` envelope stores
+the summary and retained historical reasoning separately as JSON/Base64; this is encoding,
+not provider encryption. Subsequent requests restore separate historical messages, and
+repeated compaction withholds the reasoning again. Old `ocx1:` summaries remain supported.
+Opt-in emergency compaction recovery also preserves this separation for JSON and SSE.
+Explicit native-history recovery restores separate summary and historical reasoning messages too.
+
## Memory routing
In **Dashboard → Overview → Memory routing**, choose a model and an optional reasoning effort for
diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md
index 19c55968fa8..89cc545d264 100644
--- a/docs-site/src/content/docs/reference/management-api.md
+++ b/docs-site/src/content/docs/reference/management-api.md
@@ -270,6 +270,7 @@ See [Codex prompt layers](/guides/codex-prompt/) for the layer model and the key
| `GET /api/config` | Return the redacted, management-safe configuration DTO | — |
| `PUT /api/config` | Disabled full-config replacement guard | 405; use focused endpoints instead |
| `GET, PUT /api/settings` | Read runtime/startup settings or update auto-start, stream mode, app-owned memory budget, and `codexAccountPickerEnabled` | 400 invalid, non-object, or empty update |
+| `GET, PUT /api/native-reasoning-retention` | Read resolved model/account reasoning-retention booleans; partial PUT preserves omitted fields, JSON `null` restores defaults. See [native reasoning retention](/reference/configuration/server/#native-reasoning-retention). | 400 unknown field, invalid type or non-object/non-null body |
| `GET /api/startup-health` | Read cached service/shim startup health | — |
| `POST /api/startup-action` | Install or repair the service or Codex shim | 400 invalid action; 500 action failure |
| `GET, POST /api/windows-tray` | Read Windows tray state or install/start/stop/uninstall it | 400 unsupported platform/action; 500 operation failure |
@@ -290,10 +291,11 @@ upstream model evidence remains absent rather than being inferred from the reque
Failed request rows can include bounded `upstreamErrorType`, `upstreamErrorCode`, and
`upstreamRequestId` diagnostics. On the Codex upstream WebSocket path, a bare error followed by
-EOF retains recognized rate-limit or overload codes in the synthesized failed terminal.
-Request logs record these as 429 for `rate_limit_exceeded` / `rate_limit_error`, or 503 for
-`server_is_overloaded` / `overloaded_error`. A recognized type without a code uses the same
-status. On the streaming path, an explicit unknown code records the 5xx status the Codex
+EOF retains recognized rate-limit, overload, or client-refusal codes in the synthesized failed terminal.
+Request logs record these as 429 for `rate_limit_exceeded` / `rate_limit_error`, 503 for
+`server_is_overloaded` / `overloaded_error`, and 400/401/403/404 for `invalid_request_error` /
+`authentication_error` / `permission_error` / `not_found_error`. A recognized type without a code
+uses the same status. On the streaming path, an explicit unknown code records the 5xx status the Codex
WebSocket wrapper states as a top-level integer `status` or `status_code` (one of the two, not
both); otherwise, and for buffered JSON callers, it remains a generic 502 failure. The client
terminal keeps `upstream_server_error` for unknown codes either way. Bare-error status is provisional:
@@ -471,12 +473,29 @@ Explicit all-zero user rates mean a known-zero estimate; **Reset to automatic**
override and restores the usual catalog fallback. These remain display estimates, not bills.
`GET /api/providers/{provider}/model-costs` returns `{ provider, modelCosts }`, with sanitized
-four-rate entries keyed by exact upstream model ID. `PUT` on the same route accepts
-`{ modelId, cost }`, where `cost` is `{ input, output, cacheRead, cacheWrite }` or `null` to reset.
+four-rate entries and their optional `promptLengthPricing` policy, keyed by exact upstream model ID.
+`PUT` on the same route accepts `{ modelId, cost }`, where `cost` contains
+`{ input, output, cacheRead, cacheWrite }` and optionally `promptLengthPricing`, or is `null` to reset.
All four rates must be finite numbers from 0 through 1,000,000, in USD per 1M tokens.
Unknown fields and malformed rates are rejected. A write preserves other models' overrides
and returns `{ ok: true, provider, modelId, cost }`; reset returns `cost: null`.
+The **Model price** dialog offers **Automatic**, **Flat rate**, and **Custom threshold**
+prompt-length pricing. Automatic (also the default when the policy is absent) retains catalog
+context rules with the saved base rates. Flat disables context adjustments. Custom selects four
+absolute alternative rates using one positive safe-integer `thresholdTokens` and `comparison`
+(`"gt"` for `>`, `"gte"` for `≥`). All prompt tokens count, including cache reads and writes;
+output does not. The selected rates apply to the whole request, including output, not just the
+excess tokens. Custom replaces the catalog context rule both below and above its threshold.
+Custom rates are standard-speed rates: an applicable Fast/Priority adjustment applies once after
+rate selection and follows the catalog's Priority relation for that model. Only a model whose
+catalog relation is `lower-bound` reports `priorityLowerBound: true`; a model with no catalog rule
+applies any Priority multiplier once.
+`{ "mode": "automatic" }` is accepted on write and stored as an absent policy, so GET and PUT
+receipts never contain it.
+**Reset to automatic** removes the entire override, including base rates and the policy.
+See the [configuration example](../configuration/providers/#prompt-length-pricing).
+
```bash
ocx models price ollama/custom-model --json
ocx models set-price ollama/custom-model --input 0.50 --output 1.50
@@ -588,9 +607,9 @@ Labels are read on login/import or refresh, never inferred from quota percentage
| --- | --- | --- |
| `GET /api/oauth/providers` | List providers with public OAuth login flows | — |
| `GET /api/key-providers` | List providers configured through API-key login | — |
-| `POST /api/oauth/login` | Start an OAuth login or account-add flow | 400 unknown/invalid provider; `oauth_mutation_busy` |
-| `POST /api/oauth/login/code` | Submit a manual callback URL or authorization code | 400 invalid provider/code; `oauth_mutation_busy` |
-| `POST /api/oauth/login/cancel` | Cancel a public in-progress OAuth flow | 400 unknown provider |
+| `POST /api/oauth/login` | Start an OAuth login or account-add flow. `mode: "code"` starts a code-display login (Anthropic only) and returns `mode`, `flowId`, `expiresAt` | 400 unknown/invalid provider, invalid `mode`, `code_mode_unsupported`; `oauth_mutation_busy` |
+| `POST /api/oauth/login/code` | Submit a manual callback URL or authorization code. For a code-display flow (with `flowId`) returns the token-exchange outcome `{ok, account}` / `{ok:false, error}` | 400 invalid provider/code, `state_mismatch`, `invalid_or_expired_code`, `provider_unreachable`, `malformed_input`; 409 `no_pending_login`; `oauth_mutation_busy` |
+| `POST /api/oauth/login/cancel` | Cancel a public in-progress OAuth flow; an optional `flowId` names one code-display flow (a flow of another provider is refused) | 400 unknown provider |
| `GET /api/oauth/status` | Poll one provider's OAuth flow | 400 unknown provider |
| `POST /api/oauth/logout` | Remove the selected provider credential | 400 unknown provider; `oauth_mutation_busy` |
| `GET /api/oauth/accounts` | List masked accounts; Anthropic and generic OAuth account rows include their `paused` state. Kiro rows include `autoSelectable` and a closed `skipReason` when excluded from automatic selection; an active singleton may still send. Quota remains opt-in. | 400 invalid provider |
@@ -650,6 +669,16 @@ setting interacts with `claudeCode.enabled` across upgrades and downgrades.
| `GET, PUT /api/provider-context-caps` | Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider |
| `GET /api/provider-presets` | Return GUI provider presets derived from the runtime registry | — |
+Provider reads include the optional boolean `inferContextWindow` when configured. Provider PATCH
+accepts only a boolean or `null` to clear this setting; omission preserves it. A same-name POST
+also preserves an omitted setting, including the latest stored `false`; an explicit submitted
+boolean wins. See [Context windows on custom providers](/reference/configuration/providers/#context-windows-on-custom-providers)
+for its default-off, custom-provider-only behavior.
+
+When Antigravity's quota-summary endpoint has no usable response, quota reports may also include
+per-model Cloud Code Assist usage windows when the model-discovery response provides them. These
+measurements are diagnostic; an absent model window does not establish that the model is unavailable.
+
The provider context-cap response includes `caps` (active limits) and `values` (last selected
values, retained while disabled). Enabling a provider without `value` restores its selection,
or uses the global `contextCapValue` on first enable. This also applies to OpenAI: the switch
@@ -715,7 +744,7 @@ the 503 without reporting model capacity; provider overload errors retain their
| `POST /api/system/restart` | Begin a drain-aware process restart without removing client injection; optional JSON `drainGraceMs` explicitly shortens the default 60s grace | Returns 202; repeated calls report the existing grace; 400 invalid body or grace |
| `POST /api/stop` | Stop the service, restore native Codex, remove managed Grok injection, and drain the proxy | 409 service ownership conflict; 409 `respawnable_service` when a Windows Task Scheduler wrapper could respawn the proxy and the caller is not `ocx stop` (nothing is changed); 409 `self_unload_service` when this proxy is running as the installed launchd/systemd service, because stopping the manager from inside it would end the process before native Codex is restored — run `ocx stop` instead (nothing is changed); 409 when the installed manager refuses to stop; 409 `service_state_unknown` when the Task Scheduler state cannot be read (nothing is changed; repair the query and retry) |
| `GET /api/system/codex-app-server` | Report whether running Codex app-servers predate the current model catalog | — |
-| `POST /api/system/codex-restart` | Refresh the catalog, then restart stale Codex app-servers and fully quit and relaunch the Codex desktop app so the model picker reloads. When the proxy itself is running inside the Codex app, the desktop restart is refused rather than handed off. | Returns 200 with `code: partially_stopped` when a target survives |
+| `POST /api/system/codex-restart` | Refresh the catalog, then restart Codex app-servers. The Codex desktop app is fully quit and relaunched when it launched before the current catalog was written, when this request changes the `model_catalog_json` binding (even if the shell is newer than the catalog file), or when its picker state cannot be determined, so the model picker reloads; a running desktop with a current picker stays open with `desktopApp.reason: "picker_current"`. A closed desktop returns `desktopApp.reason: "no_targets"`; its `pickerState` may be `not_running`, or `stale` when this request changed the catalog binding. `desktopApp.pickerState` reports the reading that decided (`current`, `stale`, `unknown` or `not_running`). When the proxy itself is running inside the Codex app, the desktop restart is refused rather than handed off. | Returns 200 with `code: partially_stopped` when a target survives |
### Codex authentication delegation
@@ -742,6 +771,8 @@ manager. Its routes are:
| `POST /api/codex-auth/accounts/clear-cooldown` | Clear runtime cooldown for one account or all accounts | 400 invalid id |
| `GET, PUT /api/codex-auth/active` | Read or select the active account | 400 invalid or missing account; 409 paused/legacy-row conflict or `account_selection_unavailable` |
| `PUT /api/codex-auth/auto-switch` | Set the global quota threshold with `{ threshold }`, or an account override with `{ id, threshold }`; `null` restores global inheritance, and `__main__` selects the Desktop login | 400 invalid id/threshold; 404 missing account |
+| `GET /api/codex-auth/preserve-signed-in` | Read policy, current `affectedAccountIds`, `identityKnown`, `nextAccountId`, `nextUsesSignedInQuota` (actual next choice, even when disabled), `pinnedAccountId`, `bindings`, `priorityFailbackEnabled`, and `fallbackConsumesSignedInQuota` | 503 native profile busy/unreadable |
+| `PUT /api/codex-auth/preserve-signed-in` | Strict `{ enabled: boolean, applyToBound?: boolean }`; explicit `applyToBound: true` requires enabled, releases affected bindings/pin for next request; reports `releasedPin` | 400 invalid body; 503 native profile busy/unreadable |
| `PUT, PATCH /api/codex-auth/pool-strategy` | Update Codex account-pool selection strategy | 400 invalid strategy/config |
| `PUT /api/codex-auth/failover` | Set the account failover threshold | 400 invalid threshold |
| `GET /api/codex-auth/quota` | Read cached quota state by account | — |
@@ -854,6 +885,17 @@ ports or overwrite the existing connection record. Restart returns to Observe, n
process's bound OpenCodex listener. Runtime status may expose `contextFailure` for this condition
or `build_unverified`. These states disable response correction without claiming an app repair.
+A status response can also carry `buildAssessment`, the public metadata of the last completed
+build check: `assessedVersion` (the exact package this runtime was assessed against),
+`observedVersion`, `verified` and `completedAt`. It records a past observation, not a live
+install reading: a stopped runtime, an app update or a pending or interrupted check can leave it
+older than the installed package, and only a completed check replaces it. `observedVersion` is
+null unless discovery matched the assessed package family and entry point; an identified newer
+build keeps its own version with `verified` false, and `verified` always describes the package
+observed at `completedAt`, never whatever is installed later. The field is absent until the
+service creates its probe during start and `null` until that probe finishes a check; reading
+status never starts one.
+
Apply requires a recently observed eligible exhaustion snapshot and lasts at most three minutes
at the response layer. It changes two account-UI gate flags, not usage percentages, credits,
spending restrictions or server limits. The usage response does not identify the selected model:
diff --git a/docs-site/src/content/docs/reference/platform-support.md b/docs-site/src/content/docs/reference/platform-support.md
index 5309bf7badc..edbb09b4d43 100644
--- a/docs-site/src/content/docs/reference/platform-support.md
+++ b/docs-site/src/content/docs/reference/platform-support.md
@@ -59,6 +59,25 @@ offers an input surface. Pasted keys use the same format and Model API validatio
as imported ones. Management login requires a dashboard session before either
credential-acquisition path; see the [provider guide](/guides/providers/).
+The macOS Keychain prompt appears on the machine running the proxy, which may be
+separate from your dashboard browser. OpenCodex allows up to 120 seconds to approve
+it. Cancel the dashboard login to stop the pending read. Missing entries, denied
+access, unavailable Keychain interaction, timeout, malformed credentials, and
+Model API validation failures have separate recovery messages. If import is
+blocked, the dashboard offers a paste field for an existing Muse Code API key; it
+does not automatically start another device grant. To use browser sign-in instead,
+choose **Add account** on an already configured provider. OpenCodex does not change
+Keychain access controls.
+
+If login still fails, report the exact displayed Muse error (including any HTTP
+status), OpenCodex version, `muse --version`, whether the dashboard is remote from
+the proxy, and how long the prompt was visible. In browser developer tools →
+Network, copy only the `error` field from `/api/oauth/login` or
+`/api/oauth/status?provider=meta-muse`. Do not share a HAR, request headers,
+auth files, Keychain output, account details, or the rest of the status response.
+These safe diagnostics distinguish local import from Meta validation failures;
+no credential dump is needed.
+
For Responses requests to Meta, omitted or `auto` tool selection is supported.
Explicit `none` sends `tools: []` and removes `additional_tools` items from the
input. Forced, named, and `allowed_tools` selections return HTTP 400 before the
diff --git a/docs-site/src/content/docs/reference/proxy-formats.md b/docs-site/src/content/docs/reference/proxy-formats.md
index 0168480b8e6..b16260d19ee 100644
--- a/docs-site/src/content/docs/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/reference/proxy-formats.md
@@ -32,6 +32,14 @@ the same serialized request and records the recovery in Logs. Other 400 errors,
custom destinations, cancellations and repeated upload rejections remain failures.
This does not retry filtered model responses or interrupted streams.
+## Codex-native ordinary Lite hosted-search history
+
+Ordinary requests retain structural hosted-search history by default, including Responses Lite requests. To opt in to lossy reference notes, set `providers.openai.projectResponsesLiteHostedSearchHistory: true` in `config.json` (an actual boolean, not the string `"true"`). The per-provider setting defaults to off; omission or `false` keeps structural history. Even when enabled, projection applies only to ordinary requests whose final destination is exact canonical ChatGPT Codex `/responses` and whose effective request intent explicitly selects Responses Lite. It converts unreferenced remaining `web_search_call` history into labeled assistant reference notes. The final `x-openai-internal-codex-responses-lite` header selects Lite only when its value is exactly `true`; an explicit `false` overrides frame metadata. Without a recognized header value, a valid string-map `client_metadata.ws_request_header_x_openai_internal_codex_responses_lite` value of `true` is used, matching the existing WS metadata handling. Non-Lite or unrecognized effective intent retains structural history; malformed metadata does not override an explicit HTTP `true`. Model names, subscription authentication, missing hosted declarations and `additional_tools` alone do not select this policy; no header or metadata is rewritten to make it apply. Once enabled, it applies to new threads/forks, cross-model continuations and cache misses/restarts, without diagnostic settings. Only a valid same-scope bridge memo proves an original function call/result that can be restored first; an ID prefix, model name or missing memo cannot determine the issuer. A hosted cell exactly referenced by a same-input `item_reference.id` is retained. `store:false` removes references earlier, and its remaining hosted history can be projected. Tool declarations/Lite `additional_tools`, choices, client call/result pairs, reasoning/native ciphertext, non-hosted order and caller raw history retain their existing contracts.
+
+Within that opted-in Lite boundary, this is an explicit **lossy history policy**, not a claim that hosted calls are invalid schema or that all upstream errors are fixed. Notes carry untrusted status/action metadata and allowlisted URL/title/type from both action and top-level sources, never instructions or fetched page bodies: 2,048 UTF-16 units per string, 20 entries per list, 65,536 serialized bytes for all projected notes, then an omission note. IDs and unknown fields are dropped from converted cells. HTTP/SSE and WS share the final ordinary builder.
+
+Public OpenAI API and other destinations do not gain ordinary or native-compact projection. Native `/responses/compact` remains its existing endpoint/account-scrub/official opaque-output path and retains structural hosted history. Portable/routed summaries reuse their existing projection trigger and default reference behavior; the shared renderer additionally retains allowlisted top-level sources there. No new retry, source classification or compaction summarizer is introduced.
+
## Empty search answers
After hosted search, a clean but empty forced-answer pass receives one additional answer
@@ -85,7 +93,11 @@ declare a partial catalog and discover the rest at runtime through their own too
which — not the translated catalog snapshot — owns the full declaration boundary. A call
outside the declared list is relayed for the client to validate; a call invented against an
explicitly empty `tools: []` is still refused, because no deferred registry exists under an
-empty declaration. Those refusals keep the `undeclared_tool_call` code on every wire:
+empty declaration. This applies to managed native routes and translated responses, including
+client tool calls in final hosted-search, image or video responses. Only the current turn's declaration applies;
+omitted, partial, and hosted-only catalogs keep their existing behavior. Caller-forward routes
+retain their exemption, and internal hosted operations are unaffected. Those refusals keep the
+`undeclared_tool_call` code on every wire:
- **Chat Completions:** a `data:` frame containing an OpenAI-style `error` object carrying the
code, then the stream closes without a success completion or `[DONE]`; buffered JSON returns
@@ -96,6 +108,10 @@ empty declaration. Those refusals keep the `undeclared_tool_call` code on every
with the code. A deterministic catalog violation is never reported as a retryable
`overloaded_error` (HTTP 529).
+Native Messages also recognizes inline system-message tool definitions alongside `tools: []`.
+Those names remain callable until permanently removed; an inline declaration does not authorize
+unrelated client tools. Hosted MCP calls and server-tool argument streams retain their native shape.
+
Separately, on every inbound wire, a provider's `default.`-namespaced call is normalized back
to the bare tool name the request declared (`default.Read` → `Read`).
@@ -294,7 +310,26 @@ returns an error instead of partial JSON with HTTP 200. Streaming callers are un
When a provider filters or truncates a response, an unfinished tool call remains `incomplete`
in both JSON and SSE. Partial output is preserved, and the bridge does not emit an argument
completion event for that open call. Calls already completed keep their status. This preserves
-the provider outcome; client retry behavior for incomplete responses is unchanged.
+the provider outcome.
+
+On the Responses streaming wire, an explicit non-retryable content-filter stop from
+Anthropic (`refusal` / `content_filter`) or Kiro (`CONTENT_FILTERED` /
+`GUARDRAIL_INTERVENED`) ends with `response.failed`, `error.code: "invalid_prompt"`,
+and `retryable: false`. This prevents clients that retry incomplete streams from
+resending the refused context. Usage and partial output are preserved. Web-search
+and image sidecars stop without executing calls from that refused iteration or
+asking the model again, on every inbound wire.
+
+Buffered Responses JSON retains `status: "incomplete"`; Chat Completions retains
+`finish_reason: "content_filter"`, and Messages retains `stop_reason: "refusal"`.
+A content filter without an explicit non-retryable verdict, token limits, stalls,
+and EOFs keep their existing incomplete behavior.
+
+Explicit incomplete reasons use the same vocabulary in adapter-backed responses and native
+Responses JSON, SSE, and WebSocket delivery:
+output-limit aliases such as `length` and `max_tokens` become `max_output_tokens`, and refusal/filter
+aliases such as `refusal` become `content_filter`. Other reasons remain unchanged, along with the
+provider's message, retryability, and usage. Direct Chat and Messages encoders apply the same mapping.
On the pending `dev` implementation for #4112, a final upstream HTTP 413 on this surface
is classified as `invalid_request_error` / `context_length_exceeded`. Non-streaming callers
@@ -705,8 +740,14 @@ conversation.
| Route type | Behavior |
| --- | --- |
| Canonical ChatGPT or official OpenAI route | Tries the native `/responses/compact` endpoint with the resolved account and model authentication; HTTP 404 falls back to a regular Responses compaction turn |
+| Explicitly opted-in custom `openai-responses` gateway | `supportsNativeCompactEndpoint: true` tries the native v1 endpoint for key/local/forward auth using the provider's normal Responses transport; HTTP 404 uses portable fallback; OAuth retains routed compaction and credential resolution |
| Other routed model | Runs an internal, non-streaming, no-tools compaction turn with a `compaction_trigger`; requires exactly one synthetic `compaction` item whose `encrypted_content` is an `ocx1:` envelope; decodes that summary into v1 replacement history |
+The custom v1 endpoint capability is independent of v2 `supportsNativeCompactionTrigger`
+and of `decodesNativeCompactionBlobs` for replay. None grants canonical OpenAI identity or
+caller credential forwarding. Cross-provider overrides remain portable; v2 success alone
+does not establish support for `/responses/compact`. See [provider configuration](/reference/configuration/providers/).
+
If the native compact endpoint returns HTTP 404, OpenCodex retries compaction through a regular
Responses turn with the same model selector and session headers. Canonical ChatGPT fallback
turns use upstream SSE; the compact caller still receives JSON. A completed native opaque
@@ -851,3 +892,10 @@ An ordinary 400 and a 429 are never retried this way, and neither is a 5xx, with
exception: a 502 whose body is the exact encrypted tool-output decrypt rejection, sent for a request
that carries encrypted tool output, gets the same single resend. A second rejection reaches the
client unchanged. If that happens, start a new conversation on the destination provider.
+
+### Malformed tool-call history
+
+Routed requests tolerate malformed serialized function arguments as an empty object.
+Two bounded history repairs preserve usable arguments: a missing JSON object prefix,
+and MiMo's trailing `` envelope with a missing closing quote or brace.
+A repair must decode to a JSON object. Valid JSON values containing those tags are preserved.
diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md
index 67ff8415a92..7906982abe4 100644
--- a/docs-site/src/content/docs/ru/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` проведёт вас по следующим шагам:
-1. **Выбор провайдера** — выберите один из 104 встроенных пресетов реестра или `custom`, чтобы
+1. **Выбор провайдера** — выберите один из 105 встроенных пресетов реестра или `custom`, чтобы
ввести базовый URL и адаптер вручную.
2. **API-ключ** — вставьте ключ или сошлитесь на переменную окружения вида `${ANTHROPIC_API_KEY}`.
3. **Модель по умолчанию** — для провайдеров с ключом, локальных и `custom` примите значение из
diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md
index 13c5632fb0f..84bf72f2142 100644
--- a/docs-site/src/content/docs/ru/guides/claude-code.md
+++ b/docs-site/src/content/docs/ru/guides/claude-code.md
@@ -180,22 +180,22 @@ unknown означает, что opencodex не может определить,
### Режим picker: модели opencodex в селекторе Code first-party
-Режим picker входит в режим first-party. В macOS он включён по умолчанию при выборе first-party,
+Режим picker входит в режим first-party. В macOS и Windows он включён по умолчанию при выборе first-party,
если не задано `claudeCode.intercept.picker: false`. Он изменяет селектор моделей во вкладке Code
first-party Desktop и показывает доступные модели opencodex по именам. При первом включении macOS может
-попросить доверить локальному центру сертификации в связке ключей для входа. Этот центр ограничен
+попросить доверить локальному центру сертификации в связке ключей для входа, а Windows — в хранилище корней текущего пользователя. Этот центр ограничен
`claude.ai` и его поддоменами. Экспортируемая идентичность подписи защищена хранилищем учётных данных ОС
и повторно используется при обычных перезапусках. Ключ подписи picker не хранится открытым текстом в
-каталоге конфигурации OpenCodex. Полная проверка ограниченного CA и доверия ОС сохраняется. Если одобренная
+каталоге конфигурации OpenCodex. Полная проверка ограниченного CA и доверия ОС сохраняется, в Windows — по точному отпечатку с проверенным листом. Если одобренная
идентичность не изменилась и хранилище доступно, перезапуск не добавляет и не удаляет настройки доверия
-сертификатам. Восстановление при запуске никогда не устанавливает доверие: если оно отсутствует, отозвано
+сертификатам macOS. Восстановление при запуске и проверка состояния никогда не устанавливают доверие: если оно отсутствует, отозвано
или неизвестно, picker остаётся в ожидании. Для предоставления доверия явно выполните
-`ocx claude desktop picker on` или `ocx claude desktop picker trust`.
+`ocx claude desktop picker on` или `ocx claude desktop picker trust`, включая подтверждение Windows при необходимости.
При однократной миграции со старой идентичности может потребоваться согласие на удаление прежнего доверия.
Пока очистка не завершена, picker недоступен, а применённый профиль использует туннель без расшифровки.
macOS отдельно может попросить разблокировать связку ключей или разрешить приложению доступ к учётным
-данным, в том числе после перезапуска или обновления. Windows и Linux по-прежнему не поддерживают picker:
+данным, в том числе после перезапуска или обновления. В Windows явные `on` или `trust` могут запросить подтверждение добавления корня. Linux по-прежнему не поддерживает picker:
работа с его CA, хранилищем и прокси не запускается. Основной перехват Claude остаётся доступен; для его
локальных файлов CA проверяются владелец, символические ссылки, права доступа и ACL Windows.
@@ -571,7 +571,7 @@ Claude Code — это лишь учётные данные для доступ
| `tool_use` ассистента | `function_call` (`input` → `arguments` в виде JSON-строки) |
| `tool_result` пользователя | `function_call_output` (`is_error` → префикс `[tool error]`) |
| Повтор `thinking` / `redacted_thinking` | Элементы `reasoning` с ограниченными конвертами `ocxr1` для подписей и скрытых данных |
-| Function-инструменты | `{type: "function"}` (`web_search*` → `{type: "web_search"}`) |
+| Function-инструменты | `{type: "function"}` (`web_search*` → `{type: "web_search"}`). На переведённом маршруте к `api.anthropic.com` типизированные объявления `web_search_*` и `web_fetch_*` также передаются Anthropic для выполнения на сервере, если только сайдкар веб-поиска сам не предоставляет `web_search`. Поиск и загрузка возвращаются клиенту как `server_tool_use` и соответствующий `web_search_tool_result` / `web_fetch_tool_result` |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, именованная функция→`{type:"function",name}`, размещённый WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
diff --git a/docs-site/src/content/docs/ru/guides/combos.md b/docs-site/src/content/docs/ru/guides/combos.md
index fe966d0cbf9..29c59a402b4 100644
--- a/docs-site/src/content/docs/ru/guides/combos.md
+++ b/docs-site/src/content/docs/ru/guides/combos.md
@@ -163,7 +163,7 @@ ocx combo set balanced \
| Классифицированная ошибка аутентификации, подписки, квоты, rate-limit, перегрузки или upstream-server | Перевести цель в cooldown и переключиться, даже если одного статуса недостаточно. |
| Отмена клиентом (499), `origin_rejected`, отказ из-за cyber-policy, переполнение контекста или иной некорректный запрос | Остановиться и вернуть ошибку; другая цель не сделает такой запрос корректным. |
| Точный отказ HTTP 400 о том, что модель не поддерживается в Codex с аккаунтом ChatGPT | До начала вывода пробует следующую настроенную цель. Полная фраза отказа должна находиться в `detail`, `error.message` или обычном тексте; смешанные объекты `detail` и `error` завершают запрос. Сам этот отказ не переводит аккаунт в состояние повторного входа и не включает период ожидания для всех моделей провайдера. |
-| Структурированный HTTP 400, отклоняющий необязательный `user`, неподдерживаемое значение `reasoning.effort`/`reasoning_effort` или специфичный для модели отказ входного изображения (`param: input`) | До начала вывода переходит к следующей допустимой цели без охлаждения; см. «Совместимость необязательных параметров» ниже. |
+| Структурированный HTTP 400, отклоняющий необязательный `user`, неподдерживаемое значение `reasoning.effort`/`reasoning_effort`, специфичный для модели отказ входного изображения (`param: input`) или отказ маршрута, который не может прочитать входные медиаданные (`unsupported_input_modality`) | До начала вывода переходит к следующей допустимой цели без охлаждения; см. «Совместимость необязательных параметров» ниже. |
| Первый вызов инструмента в Responses-ходе внутрипроцессного адаптера (`runTurn`), который текущий запрос не объявлял, до любого вывода и до побочного эффекта, небезопасного для повтора | Охлаждает цель и переходит к следующей с тем же каталогом инструментов. После видимого вывода или небезопасного для повтора побочного эффекта отказ окончателен. Запросы Chat Completions и Anthropic Messages не меняются. |
| Любая другая неклассифицированная ошибка | Остановиться и вернуть ошибку. |
@@ -234,7 +234,7 @@ Failover намеренно ограничен. Он помогает при п
### Дашборд
Откройте локальный дашборд и выберите **Models → Combos**. Рабочая область умеет создавать, редактировать,
-переименовывать и удалять combo, а селектор целей исключает отключённые модели и вложенные combo.
+переименовывать и удалять combo. Селекторы моделей включают скрытые модели: видимость влияет только на внешние списки, а не на внутренний выбор в combo. Выбор скрытой модели не делает её видимой снаружи. Селектор целей по-прежнему исключает отключённых провайдеров, вложенные combo и провайдеров JEV, предназначенных только для сервиса принятия решений.
У каждой цели также отображается актуальный значок квоты: **Доступно**, **Квота исчерпана** или **Квота неизвестна**.
Редактор блокирует сохранение и создание из-за квоты только тогда, когда для каждой пригодной к использованию цели есть действующее подтверждение сервера об исчерпании лимита инференса для настроенных учётных данных. Квоты аккаунта, модели, поиска и MCP, предназначенные только для отображения, а также отсутствующие или просроченные данные для принятия решения о маршрутизации не вызывают эту блокировку. Блокировка истекает при соответствующем сбросе квоты или окончании срока актуальности данных и проверяется повторно, когда страница становится активной или видимой; «Обновить» повторно загружает и данные combo, и квоты. Редактор дашборда пока не предоставляет `cooldownMs` и `waitForCooldownMs`; до появления соответствующего UI используйте файл конфигурации или Management API.
@@ -331,6 +331,6 @@ CLI и дашборд показывают точное сообщение ва
## Совместимость необязательных параметров
-Исключение для завершающих ошибок 400: структурированный отказ от `user`, неподдерживаемое значение `reasoning.effort`/`reasoning_effort` или специфичный для модели отказ входного изображения (`param: input`) позволяет до начала вывода перейти к следующей допустимой цели без периода охлаждения. Отказ политики безопасности, отмена и уже начавшийся вывод по-прежнему запрещают повторное выполнение.
+Исключение для завершающих ошибок 400: структурированный отказ от `user`, неподдерживаемое значение `reasoning.effort`/`reasoning_effort`, специфичный для модели отказ входного изображения (`param: input`) или отказ маршрута, который не может прочитать входные медиаданные (`unsupported_input_modality`) позволяет до начала вывода перейти к следующей допустимой цели без периода охлаждения. Отказ политики безопасности, отмена и уже начавшийся вывод по-прежнему запрещают повторное выполнение.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/ru/guides/providers.md b/docs-site/src/content/docs/ru/guides/providers.md
index 1449dfbb822..85d9ab24996 100644
--- a/docs-site/src/content/docs/ru/guides/providers.md
+++ b/docs-site/src/content/docs/ru/guides/providers.md
@@ -110,6 +110,7 @@ ocx login google-antigravity
ocx login cursor # отдельный PKCE-вход Cursor
ocx login command-code # браузерный OAuth Command Code (или импорт ~/.commandcode/auth.json)
ocx login devin # Cognition/Devin: сначала импорт данных Devin CLI, иначе вход через браузер Auth0
+ocx login dsh-account # DeepSeek Harness Desktop (импорт разрешения ~/.dsh/.credentials.yaml)
ocx login github-copilot # device flow GitHub → токен Copilot (Copilot Pro/Business)
ocx login codex # пул аккаунтов Codex (псевдонимы: chatgpt, openai; нужен запущенный прокси)
ocx logout
@@ -126,6 +127,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Google OAuth поверх протокола Cloud Code Assist. Живое обнаружение использует аутентифицированный CCA-эндпоинт `v1internal:fetchAvailableModels` и публикует только agent-модели, доступные текущему аккаунту; поддерживаемый каталог остаётся резервным вариантом. |
| `cursor` | `cursor` | `https://api2.cursor.sh` | Экспериментальный PKCE-вход, живой транспорт HTTP/2 и обнаружение моделей с фильтрацией по аккаунту. |
| `devin` | `devin` | `https://server.codeium.com` | Экспериментальный неофициальный мост к Cognition/Devin. Вход сначала импортирует учётные данные, которые уже хранит установленный Devin CLI (`devin auth login` записывает `devin-session-token` в его `credentials.toml`); если их нет — открывает страницу Auth0 в браузере и обменивает вставленный токен через `RegisterUser` на долгоживущий API-ключ. `ocx login devin-cli` продолжает работать как устаревший алиас. Список моделей запрашивается для каждой учётной записи через `GetCascadeModelConfigs`; потоковая передача идёт только по пути `runTurn` поверх Connect-RPC. В пресете панели по умолчанию отсутствует. |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | Экспериментальный провайдер с приоритетом импорта, повторно использующий аккаунт установленного и авторизованного DeepSeek Harness (DSH) Desktop. Считывает учётные данные строго в режиме только для чтения из `~/.dsh/.credentials.yaml` при явном импорте. Полностью независим от тарификации API-ключей DeepSeek. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Экспериментально. Device flow GitHub + обмен `copilot_internal` (OAuth-клиент VS Code). Требуется активная подписка Copilot; это не официальный сторонний API. |
Проверки квот аккаунтов и провайдера Google Antigravity используют фиксированные адреса Google, включая резервный запрос списка моделей. Для этих адресов поддерживается прозрачный Fake-IP DNS с сохранением проверки TLS, запрета перенаправлений и проверки частных адресов. Пользовательский base URL меняет только запросы моделей; `NO_PROXY` сохраняет политику прямого подключения.
@@ -204,9 +206,20 @@ Inline JSON и лишние позиционные аргументы откло
Поскольку откат возможен только при наличии снимка, **Добавить аккаунт** откажется выходить из `kiro-cli`, если хранилище сессии существует, но его нельзя захватить (файл не читается, несовпадение схемы, неоднозначный выбор токена), если `KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE` направляют импорт не на активное хранилище CLI, или если в основной базе CLI нет распознаваемой строки токена. Исправьте или удалите повреждённую базу по обычному пути данных `kiro-cli`, снимите селекторы только для импорта и повторите попытку. На машины без существующей сессии `kiro-cli` это не влияет.
+### DeepSeek Account (DSH)
+
+`dsh-account` — экспериментальный провайдер учётных записей с приоритетом импорта, предназначенный для повторного использования аутентифицированных учётных данных установленного приложения DeepSeek Harness (DSH) Desktop.
+
+- **Независимость от API-ключа DeepSeek:** `dsh-account` использует квоту авторизованной сессии DeepSeek пользователя (баланс обычного/бонусного кошельков и общие расходы), тогда как провайдер `deepseek` списывает средства с баланса API-ключа платформы. Эти два домена учётных данных и маршрутизации строго разделены и никогда не переключаются друг на друга.
+- **Только явный импорт:** OpenCodeX определяет наличие установленного и авторизованного DeepSeek Harness (`~/.dsh/.credentials.yaml`), но никогда не копирует, не импортирует и не использует токен автоматически без явного действия пользователя (`ocx login dsh-account` или нажатия кнопки «Импорт» в веб-дашборде).
+- **Хранилище учётных данных DSH только для чтения:** OpenCodeX обращается с `~/.dsh/.credentials.yaml` исключительно в режиме только для чтения. OpenCodeX никогда не записывает, не изменяет, не удаляет файлы и не меняет права доступа к файлам DeepSeek Harness.
+- **Изолированная копия учётных данных и выход:** При импорте OpenCodeX проверяет токен через официальный эндпоинт идентификации DeepSeek (`/auth-api/v0/users/current`) и сохраняет собственную копию в хранилище аутентификации OpenCodeX. Выход из системы или удаление аккаунта в OpenCodeX удаляет только локальную копию OpenCodeX; это никогда не вызывает завершение удалённой сессии и не приводит к выходу из DeepSeek Harness Desktop.
+- **Без фонового обновления токена и повторный импорт при 401:** Разрешения сессий DSH не поддерживают эндпоинт автоматического фонового обновления. Если апстрим возвращает HTTP 401 (`ACCOUNT_TOKEN_INVALID`), OpenCodeX помечает аккаунт как `needsReauth` и прекращает маршрутизацию. Для восстановления доступа пользователь должен выполнить явный повторный импорт через `ocx login dsh-account`.
+- **Отказ от ответственности третьих сторон:** OpenCodeX не заявляет об официальном одобрении или авторизации со стороны DeepSeek для сторонних клиентов, повторно использующих сессионные разрешения.
+
## 3. Каталог API-ключей
-opencodex поставляется с 104 встроенными пресетами: 85 на основе ключей, 15 OAuth, три локальных и
+opencodex поставляется с 105 встроенными пресетами: 85 на основе ключей, 16 OAuth, три локальных и
один пресет ChatGPT-форварда по умолчанию. Селектор **Add provider** в дашборде открывает страницу
выдачи ключей провайдера, проверяет ключ и сохраняет его; проверка зависит от провайдера.
Наиболее заметные записи:
diff --git a/docs-site/src/content/docs/ru/guides/sidecars.md b/docs-site/src/content/docs/ru/guides/sidecars.md
index 337c69062cc..57db4fe1022 100644
--- a/docs-site/src/content/docs/ru/guides/sidecars.md
+++ b/docs-site/src/content/docs/ru/guides/sidecars.md
@@ -84,9 +84,13 @@ SSE-событие `response.failed`.
`modelInputModalities` явно указана только текстовая модальность — и запрос содержит изображение,
opencodex описывает каждое изображение **до** основного вызова и заменяет его текстом, если доступен
план vision-сайдкара. Без доступного плана исходное изображение удаляется, а не передаётся текстовому
-бэкенду. Каталог моделей объявляет вход изображений для каждой модели, покрытой сайдкаром. Комбо
-объявляют вход изображений только если каждый участник принимает изображения нативно или через
-сайдкар и параметр комбо `imageInput` не отключён; поэтому такие клиенты, как приложение Codex,
+бэкенду. При `visionSidecar.enabled: false` каталог моделей не добавляет поддержку ввода изображений
+через сайдкар текстовым моделям, включая пользовательские записи и участников комбо. Нативная
+поддержка изображений и явно настроенные модальности изображений сохраняются. Если `enabled`
+не указан, объявление поддержки через сайдкар остаётся включённым; наличие в каталоге не подтверждает
+доступность учётных данных или ресурсов сайдкара. Комбо объявляют вход изображений только если
+каждый участник принимает изображения нативно или через включённый сайдкар и параметр комбо
+`imageInput` не отключён; поэтому такие клиенты, как приложение Codex,
разрешают вложения вместо их блокировки до запуска сайдкара. Если `visionSidecar.model` отсутствует или пуст, путь выполнения OpenAI, дашборд и API управления
используют фолбэк `gpt-5.6-luna`. При запуске явно сохранённое устаревшее значение
`gpt-5.4-mini` по-прежнему мигрирует на `gpt-5.6-luna`; миграция применяется только к сохранённому
@@ -155,6 +159,8 @@ opencodex описывает каждое изображение **до** осн
`PUT /api/sidecar-settings` принимает те же поля. Частичное обновление оставляет непереданные ключи
без изменений. `timeoutMs` использует целочисленные границы рантайма (1–2147483647 мс).
+Если через `PUT /api/sidecar-settings` меняется действующее состояние переключателя Vision, после сохранения обновляются управляемые каталог и кэш; `catalogRefreshPending=true` сообщает, что обновление не завершилось, и не служит подтверждением от клиента.
+
Карточка сайдкара web-search устроена так же: первая строка выбора модели — **Выкл. (Off)**.
Выключение останавливает перехват `web_search` со стороны OpenCodex, а интеграция Codex
записывает `web_search = "disabled"` в `~/.codex/config.toml`: до этого Codex
diff --git a/docs-site/src/content/docs/ru/guides/subagent-v1-default.md b/docs-site/src/content/docs/ru/guides/subagent-v1-default.md
index 022b974c431..8cbc5f3a39a 100644
--- a/docs-site/src/content/docs/ru/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/ru/guides/subagent-v1-default.md
@@ -146,3 +146,13 @@ ocx v2 mode v1
CLI подтверждение не запрашивает. Настройка та же, поэтому выбирайте её,
учитывая изложенные здесь ограничения.
+
+## Лимит одновременно работающих подагентов
+
+В **Models → Model settings** настройка лимита подагентов задаёт максимальное число одновременно
+работающих подагентов без учёта основного агента. Она доступна в v1, base и v2, включая гибридный
+режим с отключённым глобальным флагом v2. Выбор режима не зависит от этого лимита.
+
+Повышение лимита разрешает больше подагентов, но не создаёт их автоматически и не гарантирует
+ускорения. После изменения начните новую сессию Codex. Если длительно работающий хост App
+продолжает использовать прежний лимит, перезапустите этот хост.
diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md
index c47b3a1d397..da9ff0c9096 100644
--- a/docs-site/src/content/docs/ru/reference/cli.md
+++ b/docs-site/src/content/docs/ru/reference/cli.md
@@ -31,7 +31,7 @@ runtime port и проверку identity, а не поддерживая вто
явно документированные как offline-операции с конфигурацией, вместо этого могут валидировать и
редактировать файл конфигурации без живого прокси.
-`ocx system codex-cli-update check` не требует работающего прокси и не обращается к реестру пакетов. Команда в строго ограниченном объёме проверяет метаданные происхождения настроенного кандидата, включая замаскированный путь к исполняемому файлу и подтверждения его принадлежности. Доверенный контекст опубликованного средства запуска подтверждает только подлинность снимка данных о кандидате, но не факт успешного запуска Codex. Поскольку команда выполняет только такую проверку и никогда не запускает Codex, кандидаты из окружения и сохранённых данных отображаются только в отчёте (`managed: false`, обычно `selection_unattested`). В выводе JSON присутствуют `candidateAvailable`, `candidateVersion`, `candidateSource` и `selectionAttested`, причём значение `selectionAttested` всегда равно `false`. Для проверки настроенного кандидата нужен доверенный контекст опубликованного средства запуска. При прямом запуске через Bun или из исходного кода такого подтверждения нет; в этом случае команда игнорирует кандидатов из окружения и сохранённых данных и может вернуть `candidate_unavailable` в POSIX или `windows_inspection_deferred` в Windows. В Windows этот первый этап вообще не выполняет файловый ввод-вывод по путям кандидата или конфигурации. Только абсолютный кандидат из окружения, зафиксированный доверенным средством запуска, может получить лексическую метку комплекта приложения или менеджера версий; все остальные кандидаты Windows отклоняются по принципу fail-closed. Команда не устанавливает и не восстанавливает ПО, не запускает Codex или npm, не управляет работающими процессами и ничего не записывает в конфигурацию или кеш.
+`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows откладывает проверку, не читая файлы кандидата и конфигурацию. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan).
Для наблюдения установки Windows x64 см. [`attest`](/ru/reference/cli/agents/). Без явных путей команда наблюдает выбранного кандидата, определённого привязанным к доказательству снимком средства запуска; полномочия на обновление и выбор runtime не подтверждаются.
diff --git a/docs-site/src/content/docs/ru/reference/cli/agents.md b/docs-site/src/content/docs/ru/reference/cli/agents.md
index 8ebf9402e23..1c24581fb08 100644
--- a/docs-site/src/content/docs/ru/reference/cli/agents.md
+++ b/docs-site/src/content/docs/ru/reference/cli/agents.md
@@ -256,7 +256,9 @@ env-reference, либо несекретную loopback-заглушку. Адр
Управляйте headless runtime-setting'ами, startup, sync, diagnostics и update.
-`ocx system codex-restart --yes` перезапускает Codex app-server'ы и полностью закрывает и заново запускает Desktop-приложение Codex тем же модулем, что и `ocx sync --restart-codex`. Если сам proxy запущен внутри приложения Codex, команда отказывается с actionable-сообщением вместо handoff, который она не может завершить.
+`ocx system codex-restart --yes` перезапускает Codex app-server'ы тем же модулем, что и `ocx sync --restart-codex`. Desktop-приложение Codex полностью закрывается и запускается заново, если оно было запущено до записи текущего каталога моделей или этот запрос изменил привязку `model_catalog_json`, потому что выбор модели строится при запуске. Иначе приложение остаётся открытым, само перезапускает свой app-server и сохраняет неотправленные черновики и ожидающие подтверждения. Если состояние приложения или каталога прочитать нельзя, выполняется полный перезапуск. Если сам proxy запущен внутри приложения Codex, перезапуск Desktop отказывается с actionable-сообщением вместо handoff, который он не может завершить.
+
+Изменение привязки `model_catalog_json` этим запросом также вызывает перезапуск Desktop, даже если процесс оболочки запущен после записи файла каталога.
```bash
ocx system settings --stream-mode eager-relay
@@ -268,7 +270,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` не обращается к реестру пакетов и в строго ограниченном объёме проверяет данные о происхождении настроенного кандидата, включая замаскированный путь к исполняемому файлу и подтверждения его принадлежности. Доверенный контекст опубликованного средства запуска подтверждает только подлинность снимка данных о кандидате, но не факт успешного запуска Codex. Поскольку команда выполняет только такую проверку и никогда не запускает Codex, кандидаты из окружения и сохранённых данных отображаются только в отчёте (`managed: false`, обычно `selection_unattested`). В выводе JSON присутствуют `candidateAvailable`, `candidateVersion`, `candidateSource` и `selectionAttested`, причём значение `selectionAttested` всегда равно `false`. Для проверки настроенного кандидата нужен доверенный контекст опубликованного средства запуска. При прямом запуске через Bun или из исходного кода такого подтверждения нет; в этом случае команда игнорирует кандидатов из окружения и сохранённых данных и может вернуть `candidate_unavailable` в POSIX-системах. В Windows этот первый этап вообще не выполняет файловый ввод-вывод по путям кандидата или конфигурации. Только абсолютный кандидат из окружения, зафиксированный доверенным средством запуска, может получить лексическую метку комплекта приложения или менеджера версий; все остальные кандидаты Windows отклоняются по принципу fail-closed. Поскольку этот этап вообще не читает сохранённое состояние выбора, запуск в Windows без захваченного кандидата из окружения возвращает `windows_inspection_deferred`, а не `candidate_unavailable`: команда не может определить, установлен ли Codex CLI, поэтому сообщает об отложенной проверке, а не утверждает, что кандидата нет. Команда не запускает Codex или менеджер пакетов, не восстанавливает shim, ничего не записывает в конфигурацию или кеш, не останавливает процессы и ничего не устанавливает. Кандидаты, входящие в комплект приложения, найденные в распознанных путях менеджеров версий, являющиеся непроверенными автономными установками или имеющие неоднозначное состояние shim, отображаются как `unmanaged` или `unknown` и никогда не классифицируются как `managed`.
+`check` не обращается к реестру и не запускает Codex. В POSIX `selectionAttested` может быть `true`, если разрешитель выбирает тот же канонический путь, что и кандидат окружения, зафиксированный опубликованным средством запуска. Связанный манифест npm-global может подтвердить `managed: true`, если он доказывает владение и, когда версия кандидата сообщается, она совпадает с версией манифеста; сохранённые кандидаты остаются неаттестованными. Windows откладывает проверку, не читая файлы кандидата и конфигурацию. Прямой запуск через Bun или исходный код не даёт доказательства средства запуска. `plan` обращается к официальному реестру с изолированными временными настройками, кешем и журналами npm, отказывая при активных сессиях или неполном наблюдении процессов. Он ничего не устанавливает и не посылает сигналы. См. [полное описание команд и причин отказа (англ.)](/reference/cli/agents/#read-only-codex-cli-update-plan).
В Windows захваченная команда без полного пути, например `CODEX_CLI_PATH=codex`, удалённый путь или путь устройства возвращает `candidate_path_unavailable`. Кандидат захвачен, но его путь не подходит для этой проверки.
diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md
index 8ed62d8c7d7..74f5088282c 100644
--- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md
@@ -101,7 +101,7 @@ ocx eject back
### `ocx recover-history --ocx-compaction --yes`
-Исправьте историю одной задачи, сжатой через маршрутизируемого провайдера, перед её возобновлением в нативном Codex. Команда выбирает ровно одну задачу по UUID, сначала сохраняет приватную побайтовую резервную копию, а затем преобразует только принадлежащее OpenCodeX состояние сжатия `ocx1:` в обычную сводку, которую может воспроизвести нативный Codex. Нативное зашифрованное содержимое и другие задачи не изменяются. Перед запуском закройте выбранную задачу; если rollout изменится во время обработки, восстановление остановится без замены файла.
+Исправьте историю одной задачи, сжатой через маршрутизируемого провайдера, перед её возобновлением в нативном Codex. Команда выбирает ровно одну задачу по UUID, сначала сохраняет приватную побайтовую резервную копию, а затем преобразует только принадлежащее OpenCodeX состояние сжатия `ocx1:`/`ocx2:` в обычную сводку, которую может воспроизвести нативный Codex. Нативное зашифрованное содержимое и другие задачи не изменяются. Перед запуском закройте выбранную задачу; если rollout изменится во время обработки, восстановление остановится без замены файла.
### `ocx uninstall` · `ocx remove`
@@ -234,6 +234,8 @@ credential'ы и не выполняет repair.
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+Явно запрошенный перезапуск также выполняется после успешного сохранения изменения `config.toml`, даже если байты каталога и кэша не изменились. Операция только с конфигурацией без изменений, отказ, предварительная проверка или отменённая запись не вызывают перезапуск. Без флага перезапуска sync никогда не перезапускает Codex автоматически.
+
Получить живой список моделей от каждого настроенного провайдера и заново внедрить объединённый
каталог в Codex. Запускайте после добавления провайдера или когда нужно обновить доступные
модели.
diff --git a/docs-site/src/content/docs/ru/reference/configuration/advisor.md b/docs-site/src/content/docs/ru/reference/configuration/advisor.md
index 2cd0e396e13..7619ab6724c 100644
--- a/docs-site/src/content/docs/ru/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/ru/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: Консультант
-description: Принадлежащий OpenCodex sidecar экспертных консультаций — настроенная экспертная модель консультирует маршрутизируемых воркеров; политики manual и preflight.
+description: Независимые консультации с политиками manual, preflight и adaptive на основе наблюдаемых данных.
---
Консультант — независимая экспертная модель, которая анализирует задачу воркера и возвращает рекомендации. Консультацией владеет OpenCodex от начала до конца: прокси внедряет синтетический инструмент `advisor` в ход воркера, сам выполняет консультацию через штатный маршрутизирующий механизм и возвращает рекомендацию, чтобы исходный воркер продолжил работу. Воркеру не нужно делегировать, spawn-ить что-либо или держать провайдерские учётные данные.
@@ -9,6 +9,8 @@ description: Принадлежащий OpenCodex sidecar экспертных
## Конфигурация
+На странице **Advisor** выберите модель из каталога. Селектор сохраняет точные идентификаторы нативных моделей, провайдеров, аккаунтов, пользовательских моделей и combo. Отключённые модели и модели, ожидающие начального выбора, не предлагаются. Сохранённый идентификатор вне доступного списка остаётся видимым с предупреждением до замены или очистки. Ошибка загрузки сохраняет выбор и позволяет повторить попытку. Выберите модель для текстовых советов: каталог не гарантирует совместимость консультаций. CLI и API по-прежнему принимают строки моделей, поддерживаемые маршрутизатором.
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ description: Принадлежащий OpenCodex sidecar экспертных
| `enabled?` | `boolean` | `false` | Главный выключатель. Выключено — ноль поведения консультанта на пути запроса. |
| `model?` | `string` | — | Экспертная модель. Любая строка модели, которую принимает роутер: «голая» нативная модель (`gpt-6-astra`), явный `provider/model` (`anthropic/claude-sonnet-4-6`, `xai/grok-...`) или модель с квалификацией аккаунта. Полная поддержка межпровайдерных сценариев. |
| `effort?` | `string` | `"max"` | Интенсивность рассуждений вызова консультанта (`low`–`ultra`). |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | Когда консультировать. |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | Когда консультировать. |
+| `maxConsultationsPerTask?` | `integer` | `3` | Бюджет адаптивной задачи: 1–3 попытки, включая ручные вызовы. |
| `timeoutMs?` | `number` | `120000` | Тайм-аут loopback-консультации. |
| `contextSharingConsent?` | `"v1"` | нет | Согласие оператора отправлять контекст задачи настроенному провайдеру консультанта. Текущая версия только `"v1"`. Отсутствие, устаревшее или иное значение означает, что содержимое задачи не отправляется. `enabled: true` само по себе не согласие. |
-Управляйте через страницу **Advisor** на дашборде или `ocx advisor status|on|off|set --model --effort --policy `.
+Управляйте через страницу **Advisor** на дашборде или `ocx advisor status|on|off|set --model --effort --policy `.
Без текущего согласия `ocx advisor on` не включает межпровайдерную отправку: команда печатает раскрытие и останавливается. `ocx advisor on --ack-context-sharing` и `ocx advisor consent` записывают `v1`. `ocx advisor consent --revoke` снимает согласие и сразу прекращает отправку. `ocx advisor set` согласие не выдаёт. Флажок на дашборде изначально снят.
@@ -78,5 +81,13 @@ OpenCodex не вставляет в этот запрос ключи API про
## Ограничения PR1
- Нативные passthrough-ходы OpenAI (воркеры пула ChatGPT) не получают синтетический инструмент; поддержка консультанта покрывает маршрутизируемых (переведённых) провайдеров. Preflight-консультация применяется к run-turn-адаптерам; инструмент — нет.
-- Нет адаптивного триггера: нет детекции застревания, анализа повторных сбоев, уровней эскалации, нескольких консультантов или голосования. Только `manual` и `preflight`.
+- Нет семантической детекции застревания, итоговой проверки, уровней эскалации, нескольких консультантов или голосования.
- Учётная книга дедупликации preflight живёт в процессе; после перезапуска прокси задача в работе может получить ещё одну preflight-консультацию.
+
+## Адаптивные консультации и журнал
+
+`adaptive` добавляет к начальной консультации обращение после неудачного исправления: та же проверка в том же каталоге не проходит, последующее редактирование файла успешно завершается, а новая проверка снова не проходит. Успех снимает состояние сбоя; проверка, начатая до редактирования, не считается повтором после исправления. Начальный этап и этап исправления дедуплицируются отдельно. Для автоматики нужен стабильный идентификатор беседы. Нужны парные вызовы/результаты инструментов и явный код выхода. Сложные скрипты, пакеты вызовов, правки через shell и неоднозначный вывод не запускают консультацию.
+
+Настройка: `ocx advisor set --policy adaptive --max-consultations 2`. Ручные и автоматические попытки делят бюджет 1–3 на задачу. До отправки сервер резервирует попытку в `OPENCODEX_HOME/advisor-budget.sqlite`; перезапуск и смена worker не сбрасывают счётчик. Хранятся только дайджесты задач, счётчики и сроки: 24 часа от первой попытки, максимум 512 задач. Если бюджет недоступен или исчерпан, worker продолжает. Уже отправленные ошибочные или отменённые вызовы не возвращают резерв, поскольку могли стоить денег. Без стабильного идентификатора остаются ручные вызовы с лимитом на запрос.
+
+Страница Advisor и `ocx advisor activity --json` (`GET /api/advisor/activity`) показывают до 100 записей метаданных процесса за 24 часа. Видимая страница обновляется каждые пять секунд. Записи включают причину, статус, модели, время, сообщённые токены ввода/вывода, запрошенный и переданный адаптером уровни рассуждения. Текст задачи, команды, советы и идентификаторы беседы не хранятся; перезапуск очищает журнал. Значение адаптера описывает отправленный параметр, а не внутренние рассуждения провайдера. Отсутствующие данные остаются неизвестными; принятие советов и точная стоимость не выводятся.
diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md
index 70e46f22113..b143a3e748a 100644
--- a/docs-site/src/content/docs/ru/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md
@@ -102,7 +102,9 @@ cross-route credential fallback не существует. Строки API GPT-
| --- | --- | --- |
| `adapter` | `string` | Один из `openai-chat`, `openai-responses`, `anthropic`, `google`, `kiro`, `cursor`, `ollama-native`, `azure-openai` (или alias `azure`). |
| `baseUrl` | `string` | Базовый URL API upstream'а. Большинство built-in fixed-endpoint'ов игнорируют несовпадение; collision-safe key-preset'ы сохраняют старый custom destination с тем же именем. |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | Опциональное клиентское выравнивание начала исходящих запросов, отдельное от учёта использования, биллинга и индикаторов rate limit апстрима. `maxConcurrentRequests` — положительное целое, ограничивающее число запросов в работе; правило провайдера или модели может содержать только этот параметр. Лимит провайдера действует на все модели, а `models` сопоставляется с точными ID моделей апстрима и может увеличить задержку или уменьшить параллелизм. Ожидание очереди не расходует таймаут заголовков ответа. Поддерживаются HTTP и явные вызовы адаптеров `fetchResponse`/`runTurn`. При ограничении параллелизма канонический запрос Responses WebSocket использует HTTP/SSE, чтобы освободить слот после завершения, ошибки или отмены тела ответа. Для адаптеров `runTurn`, включая Cursor, лимит считает активные ходы, а не физические отправки: RunSSE и BidiAppend могут пересекаться внутри одного хода, пока следующий ход ждёт. Последующие отправки по-прежнему соблюдают интервалы запуска. |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | Опциональное клиентское выравнивание начала исходящих запросов, отдельное от учёта использования, биллинга и индикаторов rate limit апстрима. `maxConcurrentRequests` — положительное целое, ограничивающее число запросов в работе; правило провайдера или модели может содержать только этот параметр. Лимит провайдера действует на все модели, а `models` сопоставляется с точными ID моделей апстрима и может увеличить задержку или уменьшить параллелизм. Ожидание очереди не расходует таймаут заголовков ответа. Поддерживаются HTTP и явные вызовы адаптеров `fetchResponse`/`runTurn`. При ограничении параллелизма канонический запрос Responses WebSocket использует HTTP/SSE, чтобы освободить слот после завершения, ошибки или отмены тела ответа. Для адаптеров `runTurn`, включая Cursor, лимит считает активные ходы, а не физические отправки: RunSSE и BidiAppend могут пересекаться внутри одного хода, пока следующий ход ждёт. Последующие отправки по-прежнему соблюдают интервалы запуска. |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | Выбор ключа для обычных запросов: `active` по умолчанию сохраняет выбранный ключ и переключение при ошибке; `round-robin` последовательно распределяет запросы между доступными ключами без изменения сохранённого выбора. Балансируются назначения, не токены или стоимость. |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | Общие для всех моделей ограничения RPM, интервала и параллелизма на ссылку ключа, вместе с лимитами провайдера и модели. Одинаковые ссылки делят ёмкость; разные ссылки одного проекта не означают независимые квоты. TPM и дневные квоты не контролируются. |
| `responsesPath?` | `string` | Relative resource path для key-auth запросов `openai-responses`. Должен начинаться с `/` и не может содержать scheme, query или fragment. |
| `chatCompletionsPath?` | `string` | Relative resource path для запросов `openai-chat`, зеркало `responsesPath` с теми же правилами формы. Нужен, когда один upstream обслуживает Chat Completions и Responses под разными префиксами: per-model wire override меняет адаптер и не трогает `baseUrl`, поэтому без него включённый Chat-запрос ушёл бы в Responses base. Поставляемый пример — Z.AI. |
| `upstreamWebsocket?` | `boolean` | Необязательный upstream Responses WebSocket для запросов `openai-responses` (по умолчанию `false`). Учитывается только для first-party upstream `https://api.openai.com/v1`; конечные точки пользовательских провайдеров всегда используют ограниченный HTTP/SSE, поскольку Bun не может применить ограничение размера входящего сообщения WebSocket до выделения памяти под всё сообщение. У канонического провайдера ChatGPT `openai` пропуск сохраняет upstream WebSocket для подходящих ходов, `false` отправляет потоковые ходы по HTTP/SSE, а `true` отклоняется; при `false` нативное управление и внедрение в середине хода недоступны. Это поле не зависит от клиентской настройки `websockets` и не меняет ни конечную точку, ни учётные данные. Для HTTP остаётся SSE; пути, не относящиеся к Responses, и запросы `openai-chat` остаются на HTTP. |
@@ -125,7 +127,7 @@ cross-route credential fallback не существует. Строки API GPT-
| `modelAutoCompactTokenLimits?` | `Record` | Мягкие бюджеты автосжатия по моделям в виде положительных безопасных целых чисел. Они могут только уменьшать эффективную границу в 90 % контекста или максимального ввода и не выдаются, если эффективное окно контекста не определено. Для канонического `openai` ключами могут быть только точные поддерживаемые ID нативных моделей без префиксов провайдера или селектора аккаунта. PATCH провайдера объединяет записи: `null` для ключа удаляет его, а `null` для всего поля очищает карту. Такие маркеры `null` допустимы только в PATCH. |
| `defaultMaxOutputTokens?` | `number` | Provider-wide fallback для `openai-chat`, когда клиент не передал `max_output_tokens`. |
| `modelMaxOutputTokens?` | `Record` | Положительные fallback-budget'ы `openai-chat` по моделям; exact/pattern-match имеет приоритет над provider-default. |
-| `modelCosts?` | `Record` | Отображаемые цены по моделям (USD за 1M токенов), ключ — точный upstream id модели этого провайдера (не идентификатор провайдера и не маршрутизируемая метка `provider/model`), значение — четыре поля: `input`, `output`, `cacheRead`, `cacheWrite` (пример: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). Любой id допустим — кастомный провайдер может указывать на любой OpenAI-совместимый endpoint через адаптер `openai-chat`, а локальные и внутренние провайдеры работают даже без строки во встроенных каталогах. Пользовательские цены имеют приоритет над встроенными каталогами в оценках `~$` в Logs и Usage; исторические записи пересчитываются по текущему оверлею, поэтому изменение цены может сдвинуть прошлые суммы (порядок: пользователь → каталог jawcode → expected-price overlay → вендорская цена модели); явно заданный пользователем набор нулевых ставок означает известную нулевую оценку; удалите запись модели, чтобы восстановить автоматическую цену. Нулевые цены каталога по-прежнему переходят к следующему источнику. Каждая ставка должна быть неотрицательным конечным числом не более 1 000 000 (USD за 1M токенов); строки вне диапазона отклоняются на управляющей границе и отбрасываются при загрузке. Только оценка для отображения: оверлеи не влияют на маршрутизацию, выбор аккаунта, квоты или биллинг. |
+| `modelCosts?` | `Record` | Отображаемые цены по моделям (USD за 1M токенов), ключ — точный upstream id модели этого провайдера (не идентификатор провайдера и не маршрутизируемая метка `provider/model`), значение — четыре поля: `input`, `output`, `cacheRead`, `cacheWrite` (пример: `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`). Любой id допустим — кастомный провайдер может указывать на любой OpenAI-совместимый endpoint через адаптер `openai-chat`, а локальные и внутренние провайдеры работают даже без строки во встроенных каталогах. Пользовательские цены имеют приоритет над встроенными каталогами в оценках `~$` в Logs и Usage; исторические записи пересчитываются по текущему оверлею, поэтому изменение цены может сдвинуть прошлые суммы (порядок: пользователь → каталог jawcode → expected-price overlay → вендорская цена модели); явно заданный пользователем набор нулевых ставок означает известную нулевую оценку; удалите запись модели, чтобы восстановить автоматическую цену. Нулевые цены каталога по-прежнему переходят к следующему источнику. Каждая ставка должна быть неотрицательным конечным числом не более 1 000 000 (USD за 1M токенов); строки вне диапазона отклоняются на управляющей границе и отбрасываются при загрузке. Только оценка для отображения: оверлеи не влияют на маршрутизацию, выбор аккаунта, квоты или биллинг. |
| `headers?` | `Record` | Дополнительные upstream-header'ы. Заголовки авторизации, cookie, API-key-header'ы, встроенные переводы строк и невалидные имена отклоняются. |
| `openRouterRouting?` | `OpenRouterProviderRouting` | Предпочтения по умолчанию для OpenRouter (`order`, `only`, `allowFallbacks`); валидно только для канонического OpenRouter с `openai-chat`. |
| `modelOpenRouterRouting?` | `Record` | Exact override по model id, которые полностью заменяют provider-wide preference для OpenRouter. |
@@ -658,3 +660,8 @@ base- и Pro-id с context `1050000` и max input `922000`; OpenRouter зара
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes` привязывает модель к сохранённым ID учётных записей Anthropic OAuth. При включённом пуле первое совпадение полного шаблона `match` с учётом регистра ограничивает начальный выбор и повтор после 429. `fallback: true` расширяет выбор только при отсутствии доступных учётных записей маршрута.
+
+
+### `promptLengthPricing`
+
+Необязательное поле `promptLengthPricing` дополняет четыре базовых тарифа. `mode: "automatic"` (или отсутствие поля) сохраняет контекстные правила каталога; `mode: "flat"` отключает их. Для `mode: "custom"` задайте положительное безопасное целое `thresholdTokens`, `comparison`: `"gt"` (>) или `"gte"` (≥), и четыре абсолютных альтернативных тарифа в `rates`. Порог учитывает все токены промпта, включая чтение и запись кэша, но исключает вывод. Выбранные тарифы применяются ко всему запросу, включая вывод, и заменяют правило каталога по обе стороны порога. Корректировки Fast/Priority применяются один раз после выбора тарифов и следуют опубликованному в каталоге соотношению Priority для этой модели; оценка помечается как нижняя граница, только если каталог объявляет соотношение `lower-bound` (сейчас это xAI Grok); для модели без правила в каталоге множитель Priority применяется один раз. Настройка доступна в **Models → Price**. **Reset to automatic** удаляет всё переопределение, включая базовые тарифы и правило. Старые конфигурации совместимы; настройки относятся к отображаемым оценкам.
diff --git a/docs-site/src/content/docs/ru/reference/proxy-formats.md b/docs-site/src/content/docs/ru/reference/proxy-formats.md
index ad8d9d3f3b8..3f1bcae5e90 100644
--- a/docs-site/src/content/docs/ru/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/ru/reference/proxy-formats.md
@@ -42,6 +42,8 @@ Endpoint'ы Chat Completions и Messages поддерживают отложен
Отдельно: на каждом входящем канале вызов провайдера в пространстве имён `default.` нормализуется обратно к «голому» имени инструмента из запроса (`default.Read` → `Read`).
+Нативный Messages также учитывает встроенные определения инструментов в системных сообщениях при `tools: []`. Эти имена можно вызывать до постоянного удаления; другие клиентские инструменты не разрешаются. Вызовы размещённых MCP и потоки аргументов серверных инструментов сохраняют нативный формат.
+
## Обзор endpoint'ов
| Клиентская поверхность | Endpoint | Успешный non-stream результат | Успешный результат потока или сокета |
diff --git a/docs-site/src/content/docs/tr/getting-started/quickstart.md b/docs-site/src/content/docs/tr/getting-started/quickstart.md
index baa5172c95b..4304cf7724b 100644
--- a/docs-site/src/content/docs/tr/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/tr/getting-started/quickstart.md
@@ -14,7 +14,7 @@ ocx init
`ocx init` adım adım size rehberlik eder:
-1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 104 önayardan birini
+1. **Bir sağlayıcı seçin** — yerleşik kayıt defterindeki 105 önayardan birini
veya bir temel URL ile adaptör yazmak için `custom` seçeneğini belirleyin.
2. **API anahtarı** — bir anahtar yapıştırın veya `${ANTHROPIC_API_KEY}` gibi
bir ortam değişkenine başvurun.
diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md
index 82340db7c25..3b6b953d344 100644
--- a/docs-site/src/content/docs/tr/guides/claude-code.md
+++ b/docs-site/src/content/docs/tr/guides/claude-code.md
@@ -191,22 +191,22 @@ unknown, ayarların hâlâ opencodex vekiline işaret edip etmediğinin belirlen
### Picker modu: first-party Code sekmesinde opencodex modelleri
-Picker modu first-party modunun bir parçasıdır. macOS'ta first-party seçildiğinde varsayılan olarak
+Picker modu first-party modunun bir parçasıdır. macOS ve Windows'ta first-party seçildiğinde varsayılan olarak
açıktır; `claudeCode.intercept.picker: false` ayarlanırsa kapalı kalır. First-party Desktop'ın Code
sekmesindeki model seçiciyi değiştirerek kullanılabilir opencodex modellerini adlarıyla listeler.
-İlk etkinleştirmede macOS, giriş anahtar zincirinde yerel bir sertifika yetkilisine güvenmenizi isteyebilir.
+İlk etkinleştirmede macOS, giriş anahtar zincirinde, Windows ise geçerli kullanıcının kök deposunda yerel bir sertifika yetkilisine güvenmenizi isteyebilir.
Bu yetkili `claude.ai` ve alt alan adlarıyla sınırlıdır. Dışa aktarılabilir imzalama kimliği işletim sisteminin
kimlik bilgisi deposunda korunur ve normal yeniden başlatmalarda aynı sertifika ve anahtar yeniden kullanılır.
OpenCodex yapılandırma dizininde düz metin picker imza anahtarı saklanmaz. Kısıtlı CA'nın tam doğrulaması ve
-sistemdeki güven denetimi devam eder. Onaylanmış kimlik aynıysa ve depo erişilebilirse yeniden başlatma,
-sertifika güven ayarlarını eklemez veya kaldırmaz. Başlangıçta geri yükleme hiçbir zaman güven yüklemez:
+sistemdeki güven denetimi devam eder (Windows'ta tam parmak izi ve doğrulanmış yaprak ile). Onaylanmış kimlik aynıysa ve depo erişilebilirse yeniden başlatma,
+macOS sertifika güven ayarlarını eklemez veya kaldırmaz. Başlangıçta geri yükleme ve durum denetimi hiçbir zaman güven yüklemez:
güven eksik, iptal edilmiş veya bilinmiyorsa picker beklemede kalır. Güven vermek için açıkça
-`ocx claude desktop picker on` veya `ocx claude desktop picker trust` komutunu çalıştırın.
+`ocx claude desktop picker on` veya `ocx claude desktop picker trust` komutunu çalıştırın, gerekiyorsa Windows onayıyla birlikte.
Eski bir kimlikten bir defalık geçişte önceki güveni kaldırmak için onay gerekebilir. Temizlik tamamlanana
kadar picker kullanılamaz ve uygulanmış profil şifre çözmeyen bir aktarma kullanır. macOS ayrıca anahtar
zincirinin kilidini açmanızı veya uygulamanın kimlik bilgilerine erişimini onaylamanızı isteyebilir;
-bu istemler yeniden başlatma veya güncelleme sonrasında da çıkabilir. Windows ve Linux'ta picker desteklenmez:
+bu istemler yeniden başlatma veya güncelleme sonrasında da çıkabilir. Windows'ta açık `on` veya `trust` kök ekleme onayını gösterebilir. Linux'ta picker desteklenmez:
picker CA, kimlik bilgisi deposu veya proxy işlemleri başlatılmaz. Ana Claude yakalama işlevi kullanılabilir;
yerel CA dosyaları için sahiplik, sembolik bağlantı, dosya izinleri ve Windows ACL denetimleri uygulanır.
diff --git a/docs-site/src/content/docs/tr/guides/combos.md b/docs-site/src/content/docs/tr/guides/combos.md
index d2c3be63d49..6e8b5444780 100644
--- a/docs-site/src/content/docs/tr/guides/combos.md
+++ b/docs-site/src/content/docs/tr/guides/combos.md
@@ -230,7 +230,7 @@ ikiye ayrılır.
| HTTP 401, 403, 404, 408, 429 veya herhangi bir 5xx | Hedefi soğutun ve bir sonraki uygun hedefe atlayın. |
| Sınıflandırılmış kimlik doğrulama, abonelik, kota, hız sınırı, aşırı yük veya yukarı akış sunucu hatası | Yalnızca durum yeterli olmadığında bile hedefi soğutun ve atlayın. |
| İstemci iptali (499), `origin_rejected`, siber politika reddi, bağlam taşması veya diğer geçersiz istek | Durun ve hatayı döndürün; başka bir hedef isteği geçerli kılmaz. |
-| `user` alanını açıkça reddeden, `reasoning.effort`/`reasoning_effort` için desteklenmeyen değer bildiren veya modele özgü görüntü girdisini reddeden (`param: input`) yapılandırılmış HTTP 400 | Çıktı başlamadan önce bekleme süresi kaydetmeden sonraki uygun hedefe atlar; aşağıdaki isteğe bağlı parametre uyumluluğuna bakın. |
+| `user` alanını açıkça reddeden, `reasoning.effort`/`reasoning_effort` için desteklenmeyen değer bildiren, modele özgü görüntü girdisini reddeden (`param: input`) veya rotanın okuyamadığı girdi medyasını reddeden (`unsupported_input_modality`) yapılandırılmış HTTP 400 | Çıktı başlamadan önce bekleme süresi kaydetmeden sonraki uygun hedefe atlar; aşağıdaki isteğe bağlı parametre uyumluluğuna bakın. |
| Süreç içi bir bağdaştırıcının (`runTurn`) yürüttüğü Responses turunda, geçerli isteğin bildirmediği ilk araç çağrısı (herhangi bir çıktıdan ve yeniden oynatılamaz yan etkiden önce) | Hedefi bekleme süresine alır ve aynı araç kataloğuyla sonraki hedefe atlar. Görünür çıktıdan veya yeniden oynatılamaz bir yan etkiden sonra ret kesindir. Chat Completions ve Anthropic Messages istekleri değişmez. |
| Diğer sınıflandırılmamış hatalar | Durun ve hatayı döndürün. |
@@ -305,8 +305,9 @@ Arayüzü](/tr/guides/sub-agent-surface/) sayfasına bakın.
Yerel kontrol panelini açın ve **Modeller → Kombolar** seçeneğini belirleyin.
Çalışma alanı komboları oluşturur, düzenler, yeniden adlandırır ve kaldırır;
-hedef seçicisi ise devre dışı bırakılmış modelleri ve iç içe geçmiş komboları
-hariç tutar.
+model seçicileri gizli modelleri de içerir. Görünürlük yalnızca dış model listelerini etkiler,
+kombo içindeki seçimi etkilemez; gizli bir modeli seçmek onu dışarıda görünür yapmaz.
+Hedef seçicisi devre dışı sağlayıcıları, iç içe komboları ve yalnızca karar hizmeti sunan JEV sağlayıcılarını hariç tutar.
Her hedef ayrıca canlı bir kota rozeti gösterir: **Kullanılabilir**, **Kota tükendi** veya **Kota bilinmiyor**.
Düzenleyici, kota nedeniyle Kaydet ve Oluştur işlemlerini yalnızca kullanılabilir hedeflerin tümü için yapılandırılmış kimlik bilgisine ait çıkarım sınırının tükendiğini doğrulayan geçerli sunucu bilgisi varsa engeller. Yalnızca görüntüleme amaçlı hesap, model, arama ve MCP kotaları ya da eksik veya süresi dolmuş yönlendirme kanıtları bu engellemeye neden olmaz. Engelleme, ilgili sıfırlama zamanında veya verinin güncellik süresi dolduğunda sona erer ve sayfa etkin ya da görünür olduğunda yeniden kontrol edilir; Yenile, hem kombo verilerini hem de kotaları yeniden yükler.
@@ -409,6 +410,6 @@ reddedilen istek kaynağını düzeltin. Kombolar bu durumlar için atlama yapma
## İsteğe bağlı parametre uyumluluğu
-Sonlandırıcı 400 hatalarının dar bir istisnası vardır: `user` alanını açıkça reddeden, `reasoning.effort`/`reasoning_effort` için desteklenmeyen değer bildiren veya modele özgü görüntü girdisini reddeden (`param: input`) yapılandırılmış hata, çıktı başlamadan önce sonraki uygun hedefe geçebilir. Bu uyumsuzluk için bekleme süresi kaydedilmez. Güvenlik politikası reddi, iptal ve başlamış çıktı yeniden yürütülmez.
+Sonlandırıcı 400 hatalarının dar bir istisnası vardır: `user` alanını açıkça reddeden, `reasoning.effort`/`reasoning_effort` için desteklenmeyen değer bildiren, modele özgü görüntü girdisini reddeden (`param: input`) veya rotanın okuyamadığı girdi medyasını reddeden (`unsupported_input_modality`) yapılandırılmış hata, çıktı başlamadan önce sonraki uygun hedefe geçebilir. Bu uyumsuzluk için bekleme süresi kaydedilmez. Güvenlik politikası reddi, iptal ve başlamış çıktı yeniden yürütülmez.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/tr/guides/providers.md b/docs-site/src/content/docs/tr/guides/providers.md
index d3758aa66b2..1c18a2fc04e 100644
--- a/docs-site/src/content/docs/tr/guides/providers.md
+++ b/docs-site/src/content/docs/tr/guides/providers.md
@@ -125,6 +125,7 @@ ocx login google-antigravity
ocx login cursor # bağımsız Cursor PKCE girişi
ocx login command-code # Command Code tarayıcı OAuth (veya ~/.commandcode/auth.json içe aktarma)
ocx login devin # Cognition/Devin: önce Devin CLI kimliği içe aktarılır, yoksa Auth0 tarayıcı girişi
+ocx login dsh-account # DeepSeek Harness Desktop (~/.dsh/.credentials.yaml yetkisi içe aktarılır)
ocx login github-copilot # GitHub cihaz akışı → Copilot belirteci (Copilot Pro/Business)
ocx login codex # Codex hesap havuzu (takma adlar: chatgpt, openai; çalışan bir proxy gerekir)
ocx logout
@@ -141,6 +142,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | Cloud Code Assist hattı üzerinden Google OAuth. Canlı keşif CCA'nın kimlik doğrulamalı `v1internal:fetchAvailableModels` uç noktasını kullanır ve oturum açmış hesap için kullanılabilir olan ajan modellerini yayınlar; sürdürülen katalog geri dönüş olarak kalır. |
| `cursor` | `cursor` | `https://api2.cursor.sh` | Deneysel PKCE girişi, canlı HTTP/2 aktarımı ve hesap filtreli model keşfi. |
| `devin` | `devin` | `https://server.codeium.com` | Deneysel, resmi olmayan Cognition/Devin köprüsü. Giriş önce kurulu Devin CLI'nin zaten tuttuğu kimlik bilgisini içe aktarır (`devin auth login`, `devin-session-token`'ı kendi `credentials.toml` dosyasına yazar); yoksa tarayıcıda Auth0 oturumunu açar ve yapıştırılan belirteci `RegisterUser` ile uzun ömürlü bir API anahtarına dönüştürür. `ocx login devin-cli` kullanımdan kaldırılmış bir takma ad olarak çalışmaya devam eder. Modeller hesaba göre `GetCascadeModelConfigs` ile keşfedilir; akış yalnızca Connect-RPC üzerindeki `runTurn` yolunu kullanır. Panel ön ayarında varsayılan olarak yer almaz. |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | Kurulu ve oturum açılmış DeepSeek Harness (DSH) Desktop hesabını yeniden kullanan deneysel, içe aktarma öncelikli hesap sağlayıcısı. Açık içe aktarma sırasında `~/.dsh/.credentials.yaml` dosyasından kesinlikle salt okunur kimlik bilgilerini okur. DeepSeek API anahtarı faturalandırmasından bağımsızdır. |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Deneysel. GitHub cihaz akışı + `copilot_internal` değişimi (VS Code OAuth istemcisi). Aktif bir Copilot aboneliği gerektirir; resmi bir üçüncü taraf API değildir. |
Google Antigravity hesap ve sağlayıcı kota sorguları, model listesine geri dönüş dahil sabit Google uç noktalarını kullanır. Bu hedefler için şeffaf Fake-IP DNS desteklenirken TLS doğrulaması, yönlendirme reddi ve özel adres kontrolleri korunur. Özel base URL yalnızca model isteklerini değiştirir; `NO_PROXY` doğrudan bağlantı politikasını korur.
@@ -325,9 +327,20 @@ altındaki okunamayan veritabanını onarın veya kaldırın, bu içe aktarma
seçicilerini kaldırın ve ardından yeniden deneyin. Mevcut bir `kiro-cli` oturumu
olmayan bir makineden oturum açmak bundan etkilenmez.
+### DeepSeek Account (DSH)
+
+`dsh-account`, kurulu bir DeepSeek Harness (DSH) Desktop uygulamasının kimliği doğrulanmış hesap bilgilerini yeniden kullanmak üzere tasarlanmış deneysel, içe aktarma öncelikli bir hesap sağlayıcısıdır.
+
+- **DeepSeek API Anahtarından Bağımsız:** `dsh-account`, kullanıcının kimliği doğrulanmış DeepSeek oturum kotasını (normal/bonus cüzdan bakiyeleri ve toplam harcama) kullanırken, `deepseek` sağlayıcısı platform API anahtarı bakiyesinden ücretlendirilir. Bu iki kimlik bilgisi alanı ve yönlendirmesi kesinlikle ayrıdır ve birbirine asla geri dönüş yapmaz.
+- **Yalnızca Açık İçe Aktarma:** OpenCodeX, DeepSeek Harness'in kurulu ve oturum açılmış olup olmadığını (`~/.dsh/.credentials.yaml`) algılar, ancak kullanıcının açık bir komutu (`ocx login dsh-account` veya Web Panelinde İçe Aktar'a tıklama) olmadan belirteci asla otomatik olarak kopyalamaz, içe aktarmaz veya kullanmaz.
+- **Salt Okunur DSH Kimlik Deposu:** OpenCodeX, `~/.dsh/.credentials.yaml` dosyasına kesinlikle salt okunur olarak yaklaşır. OpenCodeX, DeepSeek Harness'e ait hiçbir dosyayı asla yazmaz, değiştirmez, silmez veya dosya izinlerini değiştirmez.
+- **Yalıtılmış Kimlik Bilgisi Kopyası ve Oturumu Kapatma:** İçe aktarma sırasında OpenCodeX, belirteci DeepSeek'in yetkili kimlik uç noktasında (`/auth-api/v0/users/current`) doğrular ve kendi kopyasını OpenCodeX kimlik deposuna kaydeder. OpenCodeX'te oturumu kapatmak veya hesabı kaldırmak yalnızca OpenCodeX'in yerel kopyasını siler; yukarı akış uzak oturum kapatmayı asla tetiklemez ve DeepSeek Harness Desktop oturumunuzu kapatmaz.
+- **Arka Planda Belirteç Döndürme Yok ve 401 Durumunda Yeniden İçe Aktarma:** DSH oturum yetkileri otomatik bir arka plan yenileme uç noktasına sahip değildir. Yukarı akış HTTP 401 (`ACCOUNT_TOKEN_INVALID`) döndürürse OpenCodeX hesabı `needsReauth` olarak işaretler ve yönlendirmeyi durdurur. Erişimi geri yüklemek için kullanıcı `ocx login dsh-account` ile açıkça yeniden içe aktarmalıdır.
+- **Üçüncü Taraf Sorumluluk Reddi:** OpenCodeX, oturum yetkilerini yeniden kullanan üçüncü taraf istemciler için DeepSeek tarafından herhangi bir resmi onay veya yetkilendirme iddiasında bulunmaz.
+
## 3. API anahtarı kataloğu
-opencodex 104 yerleşik önayar ile birlikte gelir: 85 anahtar tabanlı, 15
+opencodex 105 yerleşik önayar ile birlikte gelir: 85 anahtar tabanlı, 16
OAuth, üç yerel ve bir varsayılan ChatGPT iletme önayarı. Kontrol panelinin
**Sağlayıcı ekle** seçicisi bir anahtar sağlayıcısının kontrol panelini açar,
anahtarı doğrular ve saklar; doğrulama sağlayıcıya özgüdür. Dikkate değer
diff --git a/docs-site/src/content/docs/tr/guides/sidecars.md b/docs-site/src/content/docs/tr/guides/sidecars.md
index fdd53a327ac..a52dab0127f 100644
--- a/docs-site/src/content/docs/tr/guides/sidecars.md
+++ b/docs-site/src/content/docs/tr/guides/sidecars.md
@@ -107,9 +107,13 @@ Yönlendirilen model sağlayıcısının `noVisionModels` listesinde yer aldığ
bu model için `modelInputModalities` ile salt metin olarak bildirildiğinde ve bir
istek görsel taşıdığında, opencodex kullanılabilir bir vision sidecar planı varsa
ana çağrıdan **önce** her görseli açıklar ve onu metinle değiştirir. Kullanılabilir
-bir plan yoksa ham görsel salt metin arka ucuna iletilmek yerine kaldırılır. Model
-kataloğu sidecar kapsamında olan her model için görsel girdisini bildirir. Kombolar,
-her üye görselleri yerel olarak veya bir sidecar üzerinden kabul ettiğinde ve kombonun
+bir plan yoksa ham görsel salt metin arka ucuna iletilmek yerine kaldırılır.
+`visionSidecar.enabled: false` olduğunda model kataloğu, özel kayıtlar ve kombo üyeleri
+dahil salt metin modellere sidecar destekli görsel girdisi eklemez. Yerel görsel desteği
+ve açıkça yapılandırılmış görsel modaliteleri korunur. `enabled` belirtilmezse sidecar
+destekli görsel girdisinin bildirilmesi açık kalır; katalogda görünmesi, sidecar kimlik
+bilgilerinin veya kapasitesinin kullanılabilir olduğunu doğrulamaz. Kombolar,
+her üye görselleri yerel olarak veya etkinleştirilmiş bir sidecar üzerinden kabul ettiğinde ve kombonun
`imageInput` ayarı devre dışı olmadığında görsel girdisini bildirir; böylece Codex
uygulaması gibi istemciler, sidecar çalışmadan önce ekleri engellemek yerine kabul eder.
`visionSidecar.model` olmadığında veya boş
@@ -196,6 +200,8 @@ akıl yürütmeyi, zaman aşımını ve sınırı korur.
atlanan anahtarları değiştirmeden bırakır. `timeoutMs` çalışma zamanı tamsayı
sınırlarını kullanır (1–2147483647 ms).
+Vizyon'un geçerli açma/kapatma durumunu `PUT /api/sidecar-settings` üzerinden değiştirmek, kayıttan sonra yönetilen kataloğu ve önbelleği yeniler; `catalogRefreshPending=true`, yenilemenin tamamlanmadığını bildirir ve istemci onayı anlamına gelmez.
+
Web arama sidecar kartı aynı denetim yapısını taşır: model seçicisinin ilk satırı **Kapalı (Off)**'dır.
Kapatmak OpenCodex'in `web_search` yakalamasını durdurur ve Codex entegrasyonu
`~/.codex/config.toml` dosyasına `web_search = "disabled"` yazar; çünkü Codex kendi
diff --git a/docs-site/src/content/docs/tr/guides/subagent-v1-default.md b/docs-site/src/content/docs/tr/guides/subagent-v1-default.md
index 3249a294bd5..11ebaff1e56 100644
--- a/docs-site/src/content/docs/tr/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/tr/guides/subagent-v1-default.md
@@ -130,3 +130,13 @@ ocx v2 mode v1
```
CLI onay istemez. Aynı ayardır; bu sayfanın anlattıklarını bilerek seçin.
+
+## Eşzamanlı alt ajan sınırı
+
+**Models → Model settings** içindeki alt ajan sınırı, ana ajan hariç eşzamanlı alt ajanların
+azami sayısını belirler. Genel v2 bayrağı kapalı olan hibrit mod dahil v1, base ve v2'de
+kullanılabilir. Arayüz modu bu sınırdan bağımsız olarak seçilir.
+
+Sınırı artırmak daha fazla alt ajana izin verir; onları otomatik oluşturmaz veya daha hızlı
+çalışmayı garanti etmez. Değişiklikten sonra yeni bir Codex oturumu başlatın. Uzun süredir çalışan
+App ana bilgisayarı eski sınırı kullanmaya devam ediyorsa o ana bilgisayarı yeniden başlatın.
diff --git a/docs-site/src/content/docs/tr/reference/cli.md b/docs-site/src/content/docs/tr/reference/cli.md
index 4a24b1bc2f0..e8bcea6d8fc 100644
--- a/docs-site/src/content/docs/tr/reference/cli.md
+++ b/docs-site/src/content/docs/tr/reference/cli.md
@@ -36,7 +36,7 @@ yönetim API'sine gidiş-dönüş yapar. Durdurulmuş veya erişilemeyen bir pro
yapılandırma işlemleri olarak açıkça belgelenen komutlar, bunun yerine canlı bir
proxy olmadan yapılandırma dosyasını doğrulayabilir ve düzenleyebilir.
-`ocx system codex-cli-update check` canlı proxy gerektirmez ve paket kayıt defterine istek göndermez. Yapılandırmada belirtilen kurulum adayına ilişkin provenance meta verilerini, maskelenmiş yürütülebilir dosya konumu ve sahiplik kanıtı dâhil, sınırlı biçimde inceler. Yayımlanmış başlatıcıdan gelen güvenilir bağlam aday anlık görüntüsünü doğrular; Codex'in başarıyla çalıştırıldığını doğrulamaz. Bu tek seferlik denetim Codex'i hiçbir zaman çalıştırmadığından, ortamdan ve kalıcı kayıtlardan gelen adaylar yalnızca raporlanır (`managed: false`, genellikle `selection_unattested`). JSON çıktısında `candidateAvailable`, `candidateVersion` ve `candidateSource` alanları bulunur; `selectionAttested` değeri ise `false` kalır. Yapılandırmada belirtilen kurulum adayını incelemek için yayımlanmış başlatıcıdan gelen güvenilir bağlam gerekir; Bun ile veya kaynak koddan doğrudan başlatıldığında bu kanıt bulunmadığından ortamdaki ve kalıcı kayıtlardaki aday durumu yok sayılır ve POSIX'te `candidate_unavailable`, Windows'ta ise `windows_inspection_deferred` bildirilebilir. Windows'ta bu ilk parça, aday veya yapılandırma yollarında hiçbir dosya sistemi G/Ç işlemi yapmaz. Yalnızca güvenilir başlatıcının yakaladığı mutlak bir ortam adayı sözcüksel olarak uygulama paketi ya da sürüm yöneticisi etiketi alabilir; diğer tüm Windows adayları kapalı başarısızlıkla reddedilir. Komut yazılım kurmaz veya onarmaz, Codex ya da npm çalıştırmaz, çalışan bir sürece müdahale etmez ve yapılandırmaya ya da önbellek durumuna yazmaz.
+`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows, aday veya yapılandırma dosyalarını okumadan incelemeyi erteler. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın.
Windows x64 kurulum gözlemi için [`attest`](/tr/reference/cli/agents/) komutuna bakın. Açık yollar verilmezse komut, kanıta bağlı başlatıcı anlık görüntüsünün belirlediği seçili adayı gözlemler; güncelleme yetkisi veya çalışma zamanı seçimi doğrulanmaz.
diff --git a/docs-site/src/content/docs/tr/reference/cli/agents.md b/docs-site/src/content/docs/tr/reference/cli/agents.md
index e328f84b62c..132a02705d3 100644
--- a/docs-site/src/content/docs/tr/reference/cli/agents.md
+++ b/docs-site/src/content/docs/tr/reference/cli/agents.md
@@ -302,10 +302,15 @@ Başsız çalışma zamanı ayarlarını, başlatmayı, senkronizasyonu, tanıla
güncellemeleri yönetin.
`ocx system codex-restart --yes`, `ocx sync --restart-codex` ile aynı modül
-üzerinden Codex app-server'larını yeniden başlatır ve Codex masaüstü
-uygulamasını tamamen kapatıp yeniden başlatır. Proxy'nin kendisi Codex
-uygulamasının içinde çalışıyorsa, tamamlayamayacağı bir devri vaat etmek
-yerine eyleme geçirilebilir bir iletiyle reddeder.
+üzerinden Codex app-server'larını yeniden başlatır. Model seçici uygulama
+açılırken oluşturulduğu için Codex masaüstü uygulamasını güncel model kataloğu yazılmadan önce başlatılmışsa veya bu istek `model_catalog_json` bağını değiştirdiyse tamamen kapatıp yeniden başlatır. Aksi
+halde uygulama açık kalır, kendi app-server'ını yeniden başlatır ve
+gönderilmemiş taslakları ve bekleyen onayları korur. Uygulama veya katalog
+durumu okunamazsa tam yeniden başlatma yapılır. Proxy'nin kendisi Codex
+uygulamasının içinde çalışıyorsa masaüstü yeniden başlatması, tamamlayamayacağı
+bir devri vaat etmek yerine eyleme geçirilebilir bir iletiyle reddeder.
+
+Bu isteğin `model_catalog_json` bağını değiştirmesi de kabuk katalog dosyası yazıldıktan sonra başlamış olsa bile masaüstünün yeniden başlatılmasını gerektirir.
```bash
ocx system settings --stream-mode eager-relay
@@ -317,7 +322,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` paket kayıt defterine istek göndermez ve yapılandırmada belirtilen kurulum adayına ilişkin provenance kanıtını, maskelenmiş yürütülebilir dosya konumu ve sahiplik kanıtı dâhil, sınırlı biçimde inceler. Yayımlanmış başlatıcıdan gelen güvenilir bağlam aday anlık görüntüsünü doğrular; Codex'in başarıyla çalıştırıldığını doğrulamaz. Bu tek seferlik komut Codex'i hiçbir zaman çalıştırmadığından, ortamdan ve kalıcı kayıtlardan gelen adaylar yalnızca raporlanır (`managed: false`, genellikle `selection_unattested`). JSON çıktısında `candidateAvailable`, `candidateVersion` ve `candidateSource` alanları bulunur; `selectionAttested` değeri ise `false` kalır. Yapılandırmada belirtilen kurulum adayını incelemek için yayımlanmış başlatıcıdan gelen güvenilir bağlam gerekir; Bun ile veya kaynak koddan doğrudan başlatıldığında bu kanıt bulunmadığından ortamdaki ve kalıcı kayıtlardaki aday durumu yok sayılır ve POSIX sistemlerinde `candidate_unavailable` bildirilebilir. Windows'ta bu ilk parça, aday veya yapılandırma yollarında hiçbir dosya sistemi G/Ç işlemi yapmaz. Yalnızca güvenilir başlatıcının yakaladığı mutlak bir ortam adayı sözcüksel olarak uygulama paketi ya da sürüm yöneticisi etiketi alabilir; diğer tüm Windows adayları kapalı başarısızlıkla reddedilir. Bu parça kalıcı seçim durumunu hiç okumadığından, ortam adayı yakalanmamış olan Windows çalıştırmaları `candidate_unavailable` yerine `windows_inspection_deferred` bildirir: komut bir Codex CLI'nin kurulu olup olmadığını gözlemleyemez, bu yüzden aday bulunmadığını iddia etmek yerine incelemenin ertelendiğini bildirir. Komut Codex veya bir paket yöneticisi çalıştırmaz, shim'i onarmaz, yapılandırmaya ya da önbellek durumuna yazmaz, hiçbir süreci durdurmaz ve hiçbir şey kurmaz. Uygulamayla birlikte paketlenmiş adaylar, tanınan sürüm yöneticisi yollarında bulunan adaylar, doğrulanmamış bağımsız adaylar ve belirsiz shim durumları `unmanaged` veya `unknown` olarak raporlanır; hiçbir zaman `managed` olarak sınıflandırılmaz.
+`check` kayıt defterini sorgulamaz ve Codex’i çalıştırmaz. POSIX üzerinde çözümleyicinin seçtiği kanonik yol, yayımlanmış başlatıcının yakaladığı ortam adayıyla aynıysa `selectionAttested: true` olabilir. Bağlantılı npm-global paket bildirimi sahipliği doğrularsa ve aday sürüm bildirildiyse bu sürüm bildirimdeki sürümle eşleşirse `managed: true` olabilir; kalıcı adaylar doğrulanmaz. Windows, aday veya yapılandırma dosyalarını okumadan incelemeyi erteler. Doğrudan Bun/kaynak çalıştırması başlatıcı kanıtı sağlamaz. `plan`, yalıtılmış geçici npm yapılandırması, önbelleği ve günlükleriyle resmî kayıt defterini sorgular; etkin oturumları veya eksik süreç gözlemini reddeder. Yazılım kurmaz ve süreçlere sinyal göndermez. [Komutların ve ret nedenlerinin tam açıklamasına (İngilizce)](/reference/cli/agents/#read-only-codex-cli-update-plan) bakın.
Windows üzerinde `CODEX_CLI_PATH=codex` gibi yalın bir komut, uzak yol veya aygıt yolu aday olarak yakalanırsa `candidate_path_unavailable` bildirilir. Aday yakalanmıştır; ancak yolu bu inceleme için uygun değildir.
diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md
index f5791a12523..69f053509e4 100644
--- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md
@@ -105,7 +105,7 @@ dahildir. Durumu yedekleyin ve yalnızca bu kapsamın tamamını istiyorsanız
### `ocx recover-history --ocx-compaction --yes`
-Yönlendirilmiş bir sağlayıcı üzerinden sıkıştırılmış bir görevi yerel Codex ile sürdürmeden önce geçmişini onarın. Komut UUID ile yalnızca bir görevi seçer, önce özel ve bayt bayt bir yedek kaydeder, ardından yalnızca OpenCodeX'e ait `ocx1:` sıkıştırma durumunu yerel Codex'in yeniden oynatabileceği düz bir özete dönüştürür. Yerel şifreli içerik ve diğer görevler değişmeden kalır. Komutu çalıştırmadan önce seçili görevi kapatın; işlem sırasında rollout değişirse kurtarma dosyayı değiştirmeden durur.
+Yönlendirilmiş bir sağlayıcı üzerinden sıkıştırılmış bir görevi yerel Codex ile sürdürmeden önce geçmişini onarın. Komut UUID ile yalnızca bir görevi seçer, önce özel ve bayt bayt bir yedek kaydeder, ardından yalnızca OpenCodeX'e ait `ocx1:`/`ocx2:` sıkıştırma durumunu yerel Codex'in yeniden oynatabileceği düz bir özete dönüştürür. Yerel şifreli içerik ve diğer görevler değişmeden kalır. Komutu çalıştırmadan önce seçili görevi kapatın; işlem sırasında rollout değişirse kurtarma dosyayı değiştirmeden durur.
### `ocx uninstall` · `ocx remove`
@@ -238,6 +238,8 @@ Doctor asla kimlik bilgilerini değiştirmez veya onarımlar uygulamaz.
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+Açıkça istenen yeniden başlatma, katalog ve önbellek baytları değişmese bile `config.toml` değişikliği başarıyla kaydedildikten sonra da çalışır. Yalnızca yapılandırmayı işleyen bir işlem değişiklik yapmadığında, reddedildiğinde, ön kontrolde kaldığında veya yazma geri alındığında yeniden başlatma tetiklenmez. Yeniden başlatma bayrağı olmadan sync, Codex’i otomatik olarak yeniden başlatmaz.
+
Yapılandırılmış her sağlayıcıdan canlı model listesini alın ve birleştirilmiş
kataloğu Codex'e yeniden enjekte edin. Bir sağlayıcı ekledikten sonra veya
kullanılabilir modelleri yenilemek için çalıştırın.
diff --git a/docs-site/src/content/docs/tr/reference/configuration/advisor.md b/docs-site/src/content/docs/tr/reference/configuration/advisor.md
index a2b7de6a13a..0a44487f45e 100644
--- a/docs-site/src/content/docs/tr/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/tr/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: Danışman
-description: OpenCodex'in sahibi olduğu uzman danışma sidecar'ı — yapılandırılan uzman model yönlendirilen worker'lara tavsiye döndürür; manual ve preflight politikaları.
+description: manual, preflight ve kanıta dayalı adaptive politikalarıyla bağımsız uzman danışması.
---
Danışman, worker'ın görevini inceleyen ve tavsiye döndüren bağımsız bir uzman modeldir. Danışmayı uçtan uca OpenCodex sahiplenir: proxy, worker'ın turuna sentetik `advisor` aracını enjekte eder, danışmayı normal yönlendirme otoritesi aracılığıyla kendisi yürütür ve tavsiyeyi geri enjekte ederek özgün worker'ın devam etmesini sağlar. Worker'ın bir şey devretmesine, spawn etmesine veya sağlayıcı kimlik bilgisi taşımasına gerek yoktur.
@@ -9,6 +9,8 @@ Bu, alt ajan yüzeyinden farklıdır (bkz. [Ajan yapılandırması](/tr/referenc
## Yapılandırma
+Kontrol panelindeki **Advisor** sayfasında katalogdan model seçin. Yerel, sağlayıcı, hesap, özel model ve combo tanımlayıcıları aynen kaydedilir. Devre dışı veya ilk seçimi bekleyen modeller yeni seçenek olarak sunulmaz. Listede bulunmayan kayıtlı tanımlayıcı, değiştirilene veya temizlenene kadar uyarıyla korunur. Yükleme hatası seçimi korur ve yeniden deneme sunar. Metin önerisi verebilen bir model seçin; katalog danışma uyumluluğunu garanti etmez. CLI ve API, yönlendiricinin desteklediği model dizelerini kabul etmeye devam eder.
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ Bu, alt ajan yüzeyinden farklıdır (bkz. [Ajan yapılandırması](/tr/referenc
| `enabled?` | `boolean` | `false` | Ana anahtar. Kapalıyken istek yolunda hiçbir danışman davranışı olmaz. |
| `model?` | `string` | — | Uzman model. Yönlendiricinin kabul ettiği herhangi bir model dizisi: çıplak yerel model (`gpt-6-astra`), açık `provider/model` (`anthropic/claude-sonnet-4-6`, `xai/grok-...`) veya hesap nitelemeli yerel model. Sağlayıcılar arası tam desteklenir. |
| `effort?` | `string` | `"max"` | Danışman çağrısının muhakeme düzeyi (`low`–`ultra`). |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | Ne zaman danışılır. |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | Ne zaman danışılır. |
+| `maxConsultationsPerTask?` | `integer` | `3` | Uyarlanabilir görev bütçesi: manuel çağrılar dahil 1–3 deneme. |
| `timeoutMs?` | `number` | `120000` | Loopback danışma zaman aşımı. |
| `contextSharingConsent?` | `"v1"` | yok | Yapılandırılmış danışman sağlayıcısına görev bağlamını gönderme onayı. Güncel değer yalnızca `"v1"`. Yok, eskimiş veya başka bir değer görev içeriğinin gönderilmemesi demektir. `enabled: true` bu onay değildir. |
-Panodaki **Advisor** sayfası veya `ocx advisor status|on|off|set --model --effort --policy ` ile yönetin.
+Panodaki **Advisor** sayfası veya `ocx advisor status|on|off|set --model --effort --policy ` ile yönetin.
Güncel onay yokken `ocx advisor on` sağlayıcılar arası gönderimi açmaz: açıklamayı basar ve durur. `ocx advisor on --ack-context-sharing` ile `ocx advisor consent` `v1` kaydeder. `ocx advisor consent --revoke` onayı kaldırır ve gönderimi hemen durdurur. `ocx advisor set` onay vermez. Panodaki onay kutusu önceden işaretli değildir.
@@ -78,5 +81,13 @@ Danışman fail-open davranır: gönderilmiş bir danışma başarısız olursa
## PR1 sınırlamaları
- Yerel OpenAI passthrough turları (ChatGPT havuzu worker'ları) sentetik aracı almaz; danışman desteği yönlendirilen (çevrilen) sağlayıcıları kapsar. Preflight danışması run-turn bağdaştırıcılarına uygulanır; araç uygulanmaz.
-- Uyarlanabilir tetikleyici yok: takılma algılama, tekrarlayan başarısızlık analizi, yükseltme katmanları, çoklu danışman veya oylama yok. Yalnızca `manual` ve `preflight`.
+- Anlamsal takılma algılama, tamamlanma incelemesi, yükseltme katmanları, çoklu danışman veya oylama yoktur.
- Preflight tekilleştirme defteri süreç içindedir; proxy yeniden başlatıldıktan sonra devam eden bir görev bir preflight danışması daha alabilir.
+
+## Kanıta dayalı uyarlanabilir danışma ve etkinlik
+
+`adaptive`, ilk danışmaya ek olarak aynı çalışma dizinindeki aynı doğrulama başarısız olduğunda, sonraki dosya düzenlemesi tamamlandığında ve yeni doğrulama tekrar başarısız olduğunda danışır. Başarı hata durumunu temizler; düzenleme öncesinde başlayan kontrol onarım sonrası deneme sayılmaz. İlk danışma ve başarısız onarım ayrı ayrı tekilleştirilir. Otomasyon sabit bir konuşma kimliği gerektirir. Eşleşen araç çağrıları/sonuçları ve açık çıkış kodu gerekir. Karmaşık betikler, toplu çağrılar, shell düzenlemeleri ve belirsiz çıktılar tetiklemez.
+
+`ocx advisor set --policy adaptive --max-consultations 2` ile ayarlayın. Manuel ve otomatik denemeler görev başına 1–3 bütçesini paylaşır. Sunucu göndermeden önce `OPENCODEX_HOME/advisor-budget.sqlite` dosyasına rezervasyon yazar; yeniden başlatma veya worker değişimi sayacı sıfırlamaz. Yalnızca görev özetleri, sayaçlar ve süreler tutulur: ilk denemeden itibaren 24 saat, en fazla 512 görev. Bütçe kullanılamıyorsa worker devam eder. Gönderilen başarısız veya iptal edilen çağrılar ücretlenmiş olabileceğinden rezervasyon iade edilmez. Sabit kimlik yoksa yalnızca istek düzeyinde sınırlı manuel çağrılar kullanılabilir.
+
+Advisor sayfası ve `ocx advisor activity --json` (`GET /api/advisor/activity`), bu işlemin son 24 saatinden en fazla 100 meta veri kaydını gösterir. Görünür sayfa beş saniyede bir yenilenir. Neden, durum, modeller, süre, bildirilen girdi/çıktı tokenları, istenen ve adaptörün eşlediği akıl yürütme düzeyi gösterilir. Görev metni, komutlar, tavsiye veya konuşma kimliği tutulmaz; yeniden başlatmada kayıtlar silinir. Eşleme gönderilen parametreyi gösterir, sağlayıcının iç akıl yürütmesini garanti etmez. Eksik veriler bilinmiyor olarak kalır; tavsiye benimsenmesi ve kesin maliyet çıkarılmaz.
diff --git a/docs-site/src/content/docs/tr/reference/configuration/providers.md b/docs-site/src/content/docs/tr/reference/configuration/providers.md
index 5f3309a0f31..cd588eb2da7 100644
--- a/docs-site/src/content/docs/tr/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/tr/reference/configuration/providers.md
@@ -126,7 +126,7 @@ alanlı seçilmiş kimlikleri yalın kimliklere yeniden yazar.
| `modelAutoCompactTokenLimits?` | `Record` | Model başına pozitif güvenli tamsayı biçiminde yumuşak otomatik sıkıştırma bütçeleri. Değerler yalnızca bağlamın veya maksimum girdinin etkin %90 zarfını düşürebilir ve etkin bir bağlam penceresi çözümlenemiyorsa yayımlanmaz. Canonical `openai` için anahtarlar, sağlayıcı veya hesap seçici öneki olmadan desteklenen tam yerel model kimlikleri olmalıdır. Sağlayıcı PATCH girdileri birleştirir; bir anahtarı `null` yapmak o anahtarı siler, alanın tamamını `null` yapmak haritayı temizler. Bu `null` silme işaretleri yalnızca PATCH içindir. |
| `defaultMaxOutputTokens?` | `number` | İstemci `max_output_tokens` değerini atladığında sağlayıcı genelinde `openai-chat` geri dönüşü. |
| `modelMaxOutputTokens?` | `Record` | Pozitif model başına `openai-chat` geri dönüş bütçeleri; tam/kalıp eşleşmeleri sağlayıcı varsayılanını yener. |
-| `modelCosts?` | `Record` | Sağlayıcının tam yukarı akış model kimliğine göre anahtarlanan model başına görüntüleme fiyatları (1M token başına USD) — bir sağlayıcı tanımlayıcısı veya yönlendirilen `provider/model` etiketi değil, örn. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Herhangi bir model kimliği geçerli bir anahtardır — özel sağlayıcılar `openai-chat` adaptörü aracılığıyla herhangi bir OpenAI uyumlu uç noktayı hedefleyebilir ve yerel veya dahili sağlayıcı kimlikleri yerleşik kataloglarda bulunmasalar bile çalışır. Kullanıcı tarafından yapılandırılan fiyatlar Günlükler `~$` ve Kullanım tahminlerinde yerleşik katalogları yener; geçmiş girdiler geçerli katmandan yeniden fiyatlandırılır, bu nedenle bir fiyatı düzenlemek geçmiş toplamları değiştirebilir. Geri dönüş sırası: kullanıcı `modelCosts` → jawcode kataloğu → beklenen fiyat katmanı → model düzeyinde satıcı geri dönüşü ve kullanıcının açıkça sıfır olarak belirlediği oranlar bilinen sıfır maliyetli bir tahmin üretir; otomatik fiyatlandırmaya dönmek için model girdisini silin. Tamamen sıfır katalog fiyatları bir sonraki kaynağa geçmeye devam eder. Her oran en fazla 1.000.000 (1M token başına USD) olan negatif olmayan sonlu bir sayı olmalıdır; aralık dışı satırlar yönetim sınırı tarafından reddedilir ve yükleme sırasında bırakılır. Yalnızca görüntüleme zamanı tahmini: katmanlar yönlendirmeyi, hesap seçimini, kotaları veya faturalandırmayı asla etkilemez. |
+| `modelCosts?` | `Record` | Sağlayıcının tam yukarı akış model kimliğine göre anahtarlanan model başına görüntüleme fiyatları (1M token başına USD) — bir sağlayıcı tanımlayıcısı veya yönlendirilen `provider/model` etiketi değil, örn. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Herhangi bir model kimliği geçerli bir anahtardır — özel sağlayıcılar `openai-chat` adaptörü aracılığıyla herhangi bir OpenAI uyumlu uç noktayı hedefleyebilir ve yerel veya dahili sağlayıcı kimlikleri yerleşik kataloglarda bulunmasalar bile çalışır. Kullanıcı tarafından yapılandırılan fiyatlar Günlükler `~$` ve Kullanım tahminlerinde yerleşik katalogları yener; geçmiş girdiler geçerli katmandan yeniden fiyatlandırılır, bu nedenle bir fiyatı düzenlemek geçmiş toplamları değiştirebilir. Geri dönüş sırası: kullanıcı `modelCosts` → jawcode kataloğu → beklenen fiyat katmanı → model düzeyinde satıcı geri dönüşü ve kullanıcının açıkça sıfır olarak belirlediği oranlar bilinen sıfır maliyetli bir tahmin üretir; otomatik fiyatlandırmaya dönmek için model girdisini silin. Tamamen sıfır katalog fiyatları bir sonraki kaynağa geçmeye devam eder. Her oran en fazla 1.000.000 (1M token başına USD) olan negatif olmayan sonlu bir sayı olmalıdır; aralık dışı satırlar yönetim sınırı tarafından reddedilir ve yükleme sırasında bırakılır. Yalnızca görüntüleme zamanı tahmini: katmanlar yönlendirmeyi, hesap seçimini, kotaları veya faturalandırmayı asla etkilemez. |
| `headers?` | `Record` | Ek yukarı akış başlıkları. Yetkilendirme, çerezler, API anahtarı başlıkları, gömülü yeni satırlar ve geçersiz adlar reddedilir. |
| `openRouterRouting?` | `OpenRouterProviderRouting` | Varsayılan OpenRouter `order`, `only` ve `allowFallbacks` tercihleri; yalnızca `openai-chat` ile kurallı OpenRouter için geçerlidir. |
| `modelOpenRouterRouting?` | `Record` | Sağlayıcı genelindeki OpenRouter tercihinin yerini alan tam model kimliği geçersiz kılmaları. |
@@ -679,3 +679,8 @@ geçerli adı kontrol edin.
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes`, modeli kayıtlı Anthropic OAuth hesap kimliklerine bağlar. Havuz açıkken büyük/küçük harfe duyarlı `match` kalıbıyla ilk eşleşen kural ilk seçimi ve 429 yeniden denemesini sınırlar. `fallback: true` yalnızca kuralda uygun hesap kalmadığında normal havuza döner.
+
+
+### `promptLengthPricing`
+
+İsteğe bağlı `promptLengthPricing` alanı dört temel ücreti genişletir. `mode: "automatic"` (veya alanın olmaması) katalog bağlam kurallarını korur; `mode: "flat"` bunları devre dışı bırakır. `mode: "custom"` için pozitif güvenli tamsayı `thresholdTokens`, `"gt"` (>) veya `"gte"` (≥) değerli `comparison` ve dört mutlak alternatif ücret içeren `rates` kullanılır. Eşik, önbellek okuma ve yazma dahil tüm istem tokenlarını sayar; çıktıyı saymaz. Seçilen ücretler çıktı dahil isteğin tamamına uygulanır ve eşiğin her iki tarafında katalog kuralının yerini alır. Fast/Priority ayarlamaları ücretler seçildikten sonra bir kez uygulanır ve katalogda bu model için yayımlanan Priority ilişkisini izler; tahmin yalnızca katalog bu ilişkiyi `lower-bound` olarak bildirdiğinde (şu anda xAI Grok) alt sınır olarak işaretlenir; katalog kuralı olmayan bir modelde Priority çarpanı bir kez uygulanır. **Models → Price** üzerinden yapılandırın. **Reset to automatic**, temel ücretler ve kural dahil tüm geçersiz kılmayı siler. Eski yapılandırmalar uyumludur; bu ayarlar gösterilen tahminleri ilgilendirir.
diff --git a/docs-site/src/content/docs/tr/reference/proxy-formats.md b/docs-site/src/content/docs/tr/reference/proxy-formats.md
index 6ca40e9fd11..5d14cc1288b 100644
--- a/docs-site/src/content/docs/tr/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/tr/reference/proxy-formats.md
@@ -44,6 +44,8 @@ Chat Completions ve Messages uç noktaları ertelenmiş araç kataloglarını de
Ayrıca, her gelen bağlantıda sağlayıcının `default.` ad alanlı çağrısı, isteğin bildirdiği yalın araç adına normalleştirilir (`default.Read` → `Read`).
+Native Messages, `tools: []` ile birlikte sistem mesajlarındaki satır içi araç tanımlarını da tanır. Bu adlar kalıcı olarak kaldırılana kadar çağrılabilir; ilgisiz istemci araçlarına izin vermez. Barındırılan MCP çağrıları ve sunucu araçlarının argüman akışları native biçimlerini korur.
+
## Uç nokta genel bakışı
| İstemci yüzeyi | Uç nokta | Başarılı akışsız sonuç | Başarılı akış veya soket sonucu |
diff --git a/docs-site/src/content/docs/troubleshooting/codex-cannot-sign-in.md b/docs-site/src/content/docs/troubleshooting/codex-cannot-sign-in.md
index 9bff3814fdb..e1f84d88b7c 100644
--- a/docs-site/src/content/docs/troubleshooting/codex-cannot-sign-in.md
+++ b/docs-site/src/content/docs/troubleshooting/codex-cannot-sign-in.md
@@ -114,3 +114,17 @@ ocx account login openai --device
See [Codex Integration](/guides/codex-integration/) for what the injection
writes and how routing is chosen.
+
+## OAuth refresh capacity reached
+
+If a Responses request reports `OAuth token refresh capacity reached`, wait at
+least one second before retrying. The proxy limits pending OAuth refresh work
+to 256 consumers per process, including requests waiting on the same refresh.
+A disconnected request keeps its refresh reservation until that work settles;
+requests using fresh cached credentials can still proceed.
+
+Before a response starts, this temporary refusal returns HTTP 503 with
+`Retry-After: 1`. A stream that has already started instead ends with a server
+error. Capacity refusal does not mark the account as needing reauthentication,
+so signing in again is unnecessary for this error. A refresh that never settles
+can continue occupying capacity; the limit does not impose a refresh deadline.
diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
index 9d0c51ebf7b..6849f78b604 100644
--- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md
@@ -18,7 +18,7 @@ ocx init
`ocx init` 会引导你完成:
-1. **选择 provider** — 从内置 registry 的 104 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。
+1. **选择 provider** — 从内置 registry 的 105 个预设中选择一个,或选择 `custom` 手动输入 base URL 和 adapter。
2. **API key** — 粘贴一个 key,或引用一个环境变量,例如 `${ANTHROPIC_API_KEY}`。
3. **默认模型** — 对于 key、本地和 custom provider,接受预设值或输入模型 id。
4. **代理端口** — 默认为 `10100`。
diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md
index 5a28a1ac65b..67e176bebb9 100644
--- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md
+++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md
@@ -158,18 +158,18 @@ unknown 表示 opencodex 无法确定设置是否仍指向自己的代理。外
### Picker 模式:在第一方 Code 标签页中显示 opencodex 模型
-Picker 模式是第一方模式的一部分。在 macOS 上选择第一方时默认开启;设置
+Picker 模式是第一方模式的一部分。在 macOS 和 Windows 上选择第一方时默认开启;设置
`claudeCode.intercept.picker: false` 后会保持关闭。它会修改第一方 Desktop 的 Code 标签页模型选择器,
-按名称列出可用的 opencodex 模型。首次开启时,macOS 可能会要求你在登录钥匙串中信任本地证书颁发机构。
+按名称列出可用的 opencodex 模型。首次开启时,macOS 可能会要求你在登录钥匙串中、Windows 可能会要求你在当前用户根存储中信任本地证书颁发机构。
该颁发机构限制为 `claude.ai` 及其子域名。可导出的签名身份由 OS 凭据存储保护,普通重启会复用同一证书和密钥。
-OpenCodex 配置目录中不保存明文 Picker 签名密钥。受限 CA 的完整验证和 OS 信任检查仍然适用。
-已批准的身份不变且凭据存储可用时,重启不会添加或删除证书信任设置。启动恢复绝不会安装信任:
+OpenCodex 配置目录中不保存明文 Picker 签名密钥。受限 CA 的完整验证和 OS 信任检查仍然适用(Windows 按精确指纹与已验证叶子验证)。
+已批准的身份不变且凭据存储可用时,重启不会添加或删除 macOS 证书信任设置。启动恢复与状态检查绝不会安装信任:
若信任缺失、被撤销或无法确认,Picker 会保持待处理状态。请明确运行 `ocx claude desktop picker on`
-或 `ocx claude desktop picker trust` 来授予信任。
+或 `ocx claude desktop picker trust` 来授予信任(Windows 需要时含确认)。
从旧身份进行一次性迁移时,清除原有信任可能需要同意。清理未完成时,Picker 不可用,已应用的配置档案
-会使用不解密的中继。macOS 还可能单独要求解锁钥匙串或批准应用访问凭据,重启或升级时也可能出现这些提示。
-Windows 和 Linux 仍不支持 Picker,不会启动 Picker CA、凭据存储或代理工作。主要 Claude 拦截功能仍可用,
+会使用不解密的中继。macOS 还可能单独要求解锁钥匙串或批准应用访问凭据,重启或升级时也可能出现这些提示。Windows 上显式 `on` 或 `trust` 可能显示添加根的确认。
+Linux 仍不支持 Picker,不会启动 Picker CA、凭据存储或代理工作。主要 Claude 拦截功能仍可用,
其本地 CA 文件受到所有者、符号链接、文件权限及 Windows ACL 检查保护。
Picker 模式开启期间,Claude Desktop 通过 OpenCodex 访问网络。如果 OpenCodex 停止,Desktop 会处于离线状态,
@@ -497,7 +497,7 @@ Search 和图像描述沿用仓库已有的 Claude Code OAuth fingerprint 先例
| Assistant `tool_use` | `function_call`(`input` → JSON 字符串化的 `arguments`) |
| 用户 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 前缀) |
| 重放 `thinking` / `redacted_thinking` | `reasoning` 项;签名和脱敏载荷保存在有界 `ocxr1` 信封中 |
-| Function 工具 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`) |
+| Function 工具 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`). 在到 `api.anthropic.com` 的转换路由上,带类型的 `web_search_*` 和 `web_fetch_*` 声明也会发送到 Anthropic 并在服务端执行;如果网页搜索边车提供了 `web_search`,则优先使用边车。搜索和抓取结果以 `server_tool_use` 及对应的 `web_search_tool_result` / `web_fetch_tool_result` 块返回客户端 |
| `tool_choice` | `auto`→`auto`,`none`→`none`,`any`→`required`,指定函数→`{type:"function",name}`,托管 WebSearch/web_search→`{type:"web_search"}` |
| `max_tokens` | `max_output_tokens` |
| `stop_sequences` | `stop` |
diff --git a/docs-site/src/content/docs/zh-cn/guides/combos.md b/docs-site/src/content/docs/zh-cn/guides/combos.md
index d0513216b58..53e918bd56e 100644
--- a/docs-site/src/content/docs/zh-cn/guides/combos.md
+++ b/docs-site/src/content/docs/zh-cn/guides/combos.md
@@ -152,7 +152,7 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。
| 被分类为认证、订阅、配额、速率限制、过载或上游服务器错误 | 即使仅凭状态码不足以判断,也会使该目标进入冷却并跳转。 |
| 客户端取消(499)、`origin_rejected`、cyber-policy 拒绝、上下文溢出,或其他无效请求 | 停止并返回错误;换其他目标也无法让请求变得有效。 |
| HTTP 400 明确表示使用 ChatGPT 账户运行 Codex 时不支持该模型 | 在输出提交前尝试下一个已配置目标。完整拒绝句必须位于 `detail`、`error.message` 或纯文本中;同时包含 `detail` 与 `error` 的混合封装仍会终止请求。此原因本身不会将账户标为需要重新登录,也不会让供应商的所有模型进入冷却。 |
-| 结构化 HTTP 400,明确拒绝 `user`、对 `reasoning.effort`/`reasoning_effort` 返回不支持值,或返回模型特定图像输入拒绝(`param: input`) | 在输出开始前跳转到下一个符合条件的目标,且不记录冷却时间;参见下方可选参数兼容性。 |
+| 结构化 HTTP 400,明确拒绝 `user`、对 `reasoning.effort`/`reasoning_effort` 返回不支持值、返回模型特定图像输入拒绝(`param: input`),或拒绝该路由无法读取的输入媒体(`unsupported_input_modality`) | 在输出开始前跳转到下一个符合条件的目标,且不记录冷却时间;参见下方可选参数兼容性。 |
| 由进程内适配器(`runTurn`)执行的 Responses 回合中,当前请求未声明的第一个工具调用(在任何输出和不可重放的副作用之前) | 让该目标进入冷却,并以相同的工具目录跳转到下一个目标。出现可见输出或不可重放的副作用之后,拒绝即为最终结果。Chat Completions 和 Anthropic Messages 请求不受影响。 |
| 任何其他未分类错误 | 停止并返回错误。 |
@@ -214,7 +214,7 @@ combo 失败分为 **跳转** 失败和 **终止** 失败。
### Dashboard
-打开本地 dashboard 并选择 **Models → Combos**。该工作区可以创建、编辑、重命名和删除 combo,其目标选择器会排除已禁用的模型和嵌套 combo。
+打开本地 dashboard 并选择 **Models → Combos**。该工作区可以创建、编辑、重命名和删除 combo。模型选择器使用包含隐藏模型的完整目录:显示/隐藏设置只影响对外模型列表,不影响 Combo 内部选择,选中隐藏模型也不会将其对外显示。目标选择器仍排除已禁用的提供方、嵌套 combo 和仅用于决策服务的 JEV 提供方。
每个目标还会显示实时额度徽章:**可用**、**额度已用尽**或**额度未知**。只有当每个可用目标均有当前有效的服务器确认,表明其所配置凭据的推理限额已耗尽时,编辑器才会因额度而禁止保存和创建。仅供显示的账户、模型、搜索和 MCP 额度,以及缺失或已过期的路由依据,都不会触发此限制。此限制会在适用的重置时间或数据有效期结束时解除,并在页面变为活动或可见状态时重新检查;刷新会同时重新加载 Combo 数据和额度。dashboard 编辑器目前还不能设置 `cooldownMs` 或 `waitForCooldownMs`;在后续 UI 完成前,请使用配置文件或管理 API。
@@ -299,6 +299,6 @@ combo id 不存在。响应是 HTTP 404,类型为 `invalid_request_error`。
## 可选参数兼容性
-一般 400 错误仍会终止请求,但明确拒绝 `user`、对 `reasoning.effort`/`reasoning_effort` 返回不支持值,或返回模型特定图像输入拒绝(`param: input`)的结构化错误,可让 combo 在输出开始前尝试下一个符合条件的目标,而不记录冷却时间。安全策略拒绝、取消以及已经开始的输出仍不可重放。
+一般 400 错误仍会终止请求,但明确拒绝 `user`、对 `reasoning.effort`/`reasoning_effort` 返回不支持值、返回模型特定图像输入拒绝(`param: input`),或拒绝该路由无法读取的输入媒体(`unsupported_input_modality`)的结构化错误,可让 combo 在输出开始前尝试下一个符合条件的目标,而不记录冷却时间。安全策略拒绝、取消以及已经开始的输出仍不可重放。
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/zh-cn/guides/providers.md b/docs-site/src/content/docs/zh-cn/guides/providers.md
index 4d0e03d4f35..dbc1a20d9b9 100644
--- a/docs-site/src/content/docs/zh-cn/guides/providers.md
+++ b/docs-site/src/content/docs/zh-cn/guides/providers.md
@@ -92,6 +92,7 @@ ocx login cursor # 独立的 Cursor PKCE 登录
ocx login command-code # Command Code 浏览器 OAuth(或导入 ~/.commandcode/auth.json)
ocx login orcarouter-oauth # OrcaRouter 浏览器授权 + PKCE
ocx login devin # Cognition/Devin:优先导入 Devin CLI 凭据,否则走 Auth0 浏览器登录
+ocx login dsh-account # DeepSeek Harness Desktop(导入 ~/.dsh/.credentials.yaml 授权)
ocx login github-copilot # GitHub 设备流 → Copilot 令牌(Copilot 账户)
ocx login codex # Codex 账号池(别名:chatgpt、openai;需要代理正在运行)
ocx logout
@@ -109,6 +110,7 @@ ocx logout
| `cursor` | `cursor` | `https://api2.cursor.sh` | 实验性 PKCE 登录、带可选 HTTP/1.1 兼容路径的 HTTP/2 传输,以及按账号筛选的模型发现。 |
| `orcarouter-oauth` | `openai-chat` | `https://api.orcarouter.ai/v1` | 浏览器授权与密钥交换走 `https://www.orcarouter.ai` + S256 PKCE。交换结果是用户自己的普通 `sk-orca-…` API key,保存在现有凭据库中并持续复用,直到被撤销。 |
| `devin` | `devin` | `https://server.codeium.com` | 实验性的非官方 Cognition/Devin 桥接。登录会先导入已安装 Devin CLI 已持有的凭据(`devin auth login` 会把 `devin-session-token` 写入它自己的 `credentials.toml`);没有则打开 Auth0 浏览器页面,再用 `RegisterUser` 把粘贴的令牌换成长期 API 密钥。`ocx login devin-cli` 仍作为已弃用别名可用。模型列表按账号通过 `GetCascadeModelConfigs` 实时获取,流式仅走 Connect-RPC 上的 `runTurn` 路径。默认不在仪表盘预设中,需要手动启用。 |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | 实验性优先导入账户提供商,复用已安装并登录的 DeepSeek Harness (DSH) Desktop 账户。在用户显式导入时严格以只读方式读取 `~/.dsh/.credentials.yaml` 中的凭据。独立于 DeepSeek API Key 计费。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 实验性。GitHub 设备流 + `copilot_internal` 交换(VS Code OAuth 客户端)。需要有效的 Copilot 订阅;不是官方第三方 API。 |
Google Antigravity 账户和提供方的配额查询(包括模型列表回退)使用固定的 Google 计量端点。这些目标支持透明 Fake-IP DNS,同时保留 TLS 验证、重定向拒绝和私有地址检查。自定义 base URL 仅改变模型请求,不改变配额目标;`NO_PROXY` 仍使用直连策略。
@@ -184,9 +186,20 @@ Kiro 登录需要 Kiro CLI:Unix 使用 `curl -fsSL https://cli.kiro.dev/instal
由于回滚依赖快照,当会话存储已存在但无法捕获时(文件不可读、架构不匹配、令牌选择有歧义),当 `KIROCLI_DB_PATH` / `KIRO_CLI_DB_FILE` 将导入路径指向与活动 CLI 存储不同的位置时,或当主 CLI 数据库没有可识别的令牌行时,**添加账户**会拒绝将 `kiro-cli` 登出。请修复或删除常规 `kiro-cli` 数据路径下的损坏数据库,并取消仅用于导入的选择器后重试。对于完全没有现有 `kiro-cli` 会话的机器,不受影响。
+### DeepSeek Account (DSH)
+
+`dsh-account` 是一个实验性的、优先导入已有凭据的账户提供商,用于复用已安装并登录的 DeepSeek Harness (DSH) Desktop 应用程序的认证凭据。
+
+- **独立于 DeepSeek API Key**:`dsh-account` 使用用户的 DeepSeek 认证会话配额(普通/赠送钱包余额与总消费),而 `deepseek` 提供商按平台的 API Key 余额扣费。这两个凭据体系和路由严格隔离,互不回退。
+- **仅显式导入**:OpenCodeX 会检测是否已安装 DeepSeek Harness 且已登录(`~/.dsh/.credentials.yaml`),但绝不会在没有用户明确操作(`ocx login dsh-account` 或在 Web 仪表盘点击导入)的情况下自动复制、导入或使用该令牌。
+- **对 DSH 凭据库只读**:OpenCodeX 将 `~/.dsh/.credentials.yaml` 视为严格只读。OpenCodeX 绝不写入、修改、删除或更改任何属于 DeepSeek Harness 的文件权限。
+- **隔离的凭据副本与登出**:导入后,OpenCodeX 会通过 DeepSeek 权威身份端点(`/auth-api/v0/users/current`)校验令牌,并在 OpenCodeX 认证库中保存独立副本。在 OpenCodeX 中登出或删除该账户仅会删除 OpenCodeX 本地副本,绝不会触发上游远程会话登出,也不会退出 DeepSeek Harness Desktop。
+- **无后台令牌自动刷新与 401 重新导入**:DSH 会话授权不支持自动后台刷新端点。若上游返回 HTTP 401(`ACCOUNT_TOKEN_INVALID`),OpenCodeX 将该账户标记为 `needsReauth` 并停止路由。要恢复访问,用户必须通过 `ocx login dsh-account` 显式重新导入。
+- **第三方免责声明**:OpenCodeX 不主张 DeepSeek 官方对于第三方客户端复用会话授权提供背书或支持。
+
## 3. API 密钥目录
-opencodex 内置 104 个预设:85 个密钥预设、15 个 OAuth 预设、3 个本地预设,以及 1 个默认的
+opencodex 内置 105 个预设:85 个密钥预设、16 个 OAuth 预设、3 个本地预设,以及 1 个默认的
ChatGPT 转发预设。仪表盘的 **Add provider** 选择器会打开密钥提供商的控制台,验证并保存密钥。
验证因提供商而异。主要条目包括:
diff --git a/docs-site/src/content/docs/zh-cn/guides/sidecars.md b/docs-site/src/content/docs/zh-cn/guides/sidecars.md
index 15605d2dcf9..b959040db09 100644
--- a/docs-site/src/content/docs/zh-cn/guides/sidecars.md
+++ b/docs-site/src/content/docs/zh-cn/guides/sidecars.md
@@ -66,8 +66,10 @@ OAuth 账户时使用 `anthropic`,否则使用 `openai`。显式选择 `anthro
当路由模型列在其 provider 的 `noVisionModels` 中,或该模型在 `modelInputModalities` 中被声明为仅文本,
并且请求包含图像时,只要有可用的 vision sidecar plan,opencodex 就会在主调用**之前**描述每张图像并用文字替换图像。
-如果没有可用 plan,原始图像会被移除,而不会继续转发给纯文本后端。模型目录会为每个由 sidecar 覆盖的模型声明图像输入。
-只有当每个 combo 成员都能原生或通过 sidecar 接受图像、且 combo 的 `imageInput` 设置未禁用时,combo 才会声明图像输入;
+如果没有可用 plan,原始图像会被移除,而不会继续转发给纯文本后端。当 `visionSidecar.enabled: false` 时,
+模型目录不会为纯文本模型添加由 sidecar 提供的图像输入,包括自定义模型和 combo 成员。原生图像支持和显式配置的
+图像输入会保留。省略 `enabled` 时,默认仍会声明 sidecar 图像支持;目录中的声明不保证 sidecar 凭据或容量可用。
+只有当每个 combo 成员都能原生或通过已启用的 sidecar 接受图像、且 combo 的 `imageInput` 设置未禁用时,combo 才会声明图像输入;
这样 Codex 应用等客户端会允许附件,而不会在 sidecar 运行前阻止它们。当 `visionSidecar.model` 缺失或为空时,OpenAI 执行路径、
Dashboard 和管理 API 都使用 `gpt-5.6-luna` 作为回退。启动时仍会把明确保存的旧
`gpt-5.4-mini` 值迁移到 `gpt-5.6-luna`;该迁移只作用于已保存值,不适用于缺失的 model 字段。
@@ -126,6 +128,10 @@ Dashboard 和管理 API 都使用 `gpt-5.6-luna` 作为回退。启动时仍会
`PUT /api/sidecar-settings` 接受相同字段。部分更新会保留未提交的键。`timeoutMs` 使用运行时整数边界(1–2147483647 毫秒)。
+Vision 的实际启用状态改变时,保存后也会刷新受管理的 Codex 目录和模型缓存。
+若刷新未完成,响应会返回 `catalogRefreshPending: true`,已保存的开关设置仍然保留。
+目录发布成功并不表示运行中的客户端已重新加载目录。
+
Web 搜索 sidecar 卡片采用相同的控件形态:模型选择器的第一行是 **关闭 (Off)**。关闭会停止
OpenCodex 对 `web_search` 的拦截,同时 Codex 集成会把 `web_search = "disabled"`
写入 `~/.codex/config.toml`;因为 Codex 在自身模式如此声明前会一直声明其原生托管的
diff --git a/docs-site/src/content/docs/zh-cn/guides/subagent-v1-default.md b/docs-site/src/content/docs/zh-cn/guides/subagent-v1-default.md
index bbe261970d2..58fa0890419 100644
--- a/docs-site/src/content/docs/zh-cn/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/zh-cn/guides/subagent-v1-default.md
@@ -94,3 +94,9 @@ ocx v2 mode v1
```
CLI 不会提示。这是同一设置,因此请了解本页描述的行为后再选择。
+
+## 子代理并发上限
+
+在 **Models → Model settings** 中,子代理上限控件可设置同时运行的子代理数量上限,不包括主代理。v1、base 和 v2 均可使用,包括全局 v2 标志关闭的混合模式。界面模式与此上限分别设置。
+
+提高上限可允许更多子代理,但不会自动创建子代理,也不保证执行更快。更改后请启动新的 Codex 会话。如果长时间运行的 App 主机仍使用旧上限,请重新启动该主机。
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 044fd71f388..81909ec088b 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
@@ -43,6 +43,7 @@ bun run dev:gui
| 区域 | 作用 |
| --- | --- |
| **Dashboard 摘要** | 显示 multi-agent 模式、在线状态、版本、运行时间、provider 数量、30 天 token 总量、活动 provider 和可用的原生/路由模型。 |
+| **原生推理密文保留** | 独立开启原生模型/账号切换时的推理密文转发;默认均关闭,跨账号保留属于实验性选项。见 [配置范围](/zh-cn/reference/configuration/#原生推理密文保留)。 |
| **Sub-agent delegation** | 选择供 OpenCodex 委派指引与可选的 Codex 原生子代理默认值共用的原生/路由模型和可选 reasoning 强度。它不是逐次生成的路由器,详见下文。 |
| **Sidecar** | 选择 web-search 模型及强度,以及图像描述模型;更改从下一次请求开始生效。 |
| **Maintenance** | 重新同步 Codex 模型目录,查看项目级配置绕过警告,检查 latest/preview 版本,并可在更新后重启代理。 在桌面 shell 中,该更新入口会打开原生应用更新页面,而不是运行包更新器。 |
diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md
index 87731f641e7..c6e89a3b4fa 100644
--- a/docs-site/src/content/docs/zh-cn/reference/cli.md
+++ b/docs-site/src/content/docs/zh-cn/reference/cli.md
@@ -17,7 +17,7 @@ opencodex 的 CLI 是 `ocx`。它会根据第一个命令名进行分发;文
管理命令会通过实时代理的管理 API 往返调用,使用记录下来的运行时端口和身份检查,而不是维护第二条配置路径。已停止或不可达的代理会被表示为 HTTP 503,并导致 CLI 以非零状态退出。明确标注为离线配置操作的命令,则可以在没有实时代理的情况下验证并编辑配置文件。
-`ocx system codex-cli-update check` 不需要实时代理,也不会向软件包注册表发起请求。它只会在限定范围内检查已配置候选项的来源元数据,包括经过脱敏的可执行文件位置和所有权证据。受信任的已发布启动器上下文只能验证该候选项快照,并不证明 Codex 已成功运行。由于这条一次性检查命令绝不会运行 Codex,来自环境变量和持久化记录的候选项仅用于报告(`managed: false`,通常为 `selection_unattested`);JSON 输出包含 `candidateAvailable`、`candidateVersion` 和 `candidateSource`,且 `selectionAttested` 始终为 `false`。检查已配置候选项需要受信任的已发布启动器上下文;直接使用 Bun 启动或从源码运行时没有这项证明,因此会忽略环境变量和持久化记录中的候选项状态,并可能报告 POSIX 下的 `candidate_unavailable` 或 Windows 下的 `windows_inspection_deferred`。在 Windows 上,这个首个切片不会对候选路径或配置路径执行任何文件系统 I/O。只有由受信任启动器捕获的绝对环境候选项可以获得应用捆绑或版本管理器的纯词法标签;其他所有 Windows 候选项都会以失败关闭方式处理。该命令不会安装或修复软件,不会运行 Codex 或 npm,不会控制正在运行的进程,也不会写入配置或缓存状态。
+`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows 不读取候选项或配置文件,而是推迟检查。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。
有关 Windows x64 安装观察,请参阅 [`attest` 命令](/zh-cn/reference/cli/agents/)。未提供显式路径时,该命令观察由受证明约束的启动器快照识别出的已选候选项;它不授予更新权限,也不证明运行时选择。
diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md
index d2c231de196..f3b95ff0f58 100644
--- a/docs-site/src/content/docs/zh-cn/reference/cli/agents.md
+++ b/docs-site/src/content/docs/zh-cn/reference/cli/agents.md
@@ -208,7 +208,9 @@ opencode 会插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。opencodex 生成的 Pi
管理无头运行时设置、启动、同步、诊断和更新。
-`ocx system codex-restart --yes` 通过与 `ocx sync --restart-codex` 相同的模块重启 Codex app-server,并完全退出再重新启动 Codex 桌面应用。若代理本身运行在 Codex 应用内部,该命令会给出可执行提示并拒绝,而不是承诺无法完成的移交。
+`ocx system codex-restart --yes` 通过与 `ocx sync --restart-codex` 相同的模块重启 Codex app-server。模型选择器在应用启动时构建,因此当桌面应用在当前模型目录写入之前启动,或本次请求更改了 `model_catalog_json` 绑定时,会完全退出再重新启动它。否则应用保持打开,自行重启其 app-server,并保留未发送的草稿和待处理的审批。若无法读取应用或目录状态,则执行完整重启。若代理本身运行在 Codex 应用内部,桌面重启会给出可执行提示并拒绝,而不是承诺无法完成的移交。
+
+如果本次请求更改了 `model_catalog_json` 绑定,即使桌面进程启动于目录文件写入之后,也会触发桌面重启。
```bash
ocx system settings --stream-mode eager-relay
@@ -220,7 +222,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` 不会向软件包注册表发起请求,只会在限定范围内检查已配置候选项的来源证据,包括经过脱敏的可执行文件位置和所有权证据。受信任的已发布启动器上下文只能验证该候选项快照,并不证明 Codex 已成功运行。由于这条一次性命令绝不会运行 Codex,来自环境变量和持久化记录的候选项仅用于报告(`managed: false`,通常为 `selection_unattested`);JSON 输出包含 `candidateAvailable`、`candidateVersion` 和 `candidateSource`,且 `selectionAttested` 始终为 `false`。检查已配置候选项需要受信任的已发布启动器上下文;直接使用 Bun 启动或从源码运行时没有这项证明,因此会忽略环境变量和持久化记录中的候选项状态,并可能在 POSIX 系统上报告 `candidate_unavailable`。在 Windows 上,这个首个切片不会对候选路径或配置路径执行任何文件系统 I/O。只有由受信任启动器捕获的绝对环境候选项可以获得应用捆绑或版本管理器的纯词法标签;其他所有 Windows 候选项都会以失败关闭方式处理。由于这个切片完全不读取持久化的选择状态,在未捕获任何环境候选项的 Windows 运行中会报告 `windows_inspection_deferred` 而非 `candidate_unavailable`:该命令无法观测 Codex CLI 是否已安装,因此报告检查被推迟,而不是断言不存在候选项。该命令不会运行 Codex 或软件包管理器,不会修复 shim,不会写入配置或缓存,不会停止进程,也不会安装任何内容。随应用捆绑的候选项、位于已识别版本管理器路径中的候选项、未经验证的独立候选项以及 shim 状态不明确的候选项,都会报告为 `unmanaged` 或 `unknown`,绝不会归类为 `managed`。
+`check` 不查询注册表,也不运行 Codex。在 POSIX 上,当已发布启动器捕获的环境候选项与运行时解析器选择的规范路径一致时,`selectionAttested` 可以为 `true`;如果关联的 npm-global 软件包清单证明了所有权,`managed` 可以为 `true`。持久化候选项仍不获认证。Windows 不读取候选项或配置文件,而是推迟检查。直接通过 Bun 或源码运行时没有启动器证明。`plan` 使用隔离的临时 npm 配置、缓存和日志查询官方注册表,并拒绝活动会话或不完整的进程观测。它不安装软件,也不向进程发送信号。请参阅[完整命令和拒绝原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。
在 Windows 上,如果捕获到 `CODEX_CLI_PATH=codex` 这样的裸命令、远程路径或设备路径,则报告 `candidate_path_unavailable`。这些情况下候选项已被捕获,但其路径不适用于此检查。
diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
index c13b8aa9797..62b44ac873d 100644
--- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
@@ -62,7 +62,7 @@ ocx eject back
### `ocx recover-history --ocx-compaction --yes`
-在通过原生 Codex 恢复某个曾由路由提供方压缩的任务前,修复该任务的历史记录。此命令按 UUID 精确选择一个任务,先保存私有的逐字节备份,然后仅将 OpenCodeX 自有的 `ocx1:` 压缩状态转换为原生 Codex 可重放的普通摘要。原生加密内容和其他任务保持不变。运行前请关闭所选任务;如果 rollout 在处理期间发生变化,恢复会停止且不会替换原文件。
+在通过原生 Codex 恢复某个曾由路由提供方压缩的任务前,修复该任务的历史记录。此命令按 UUID 精确选择一个任务,先保存私有的逐字节备份,然后仅将 OpenCodeX 自有的 `ocx1:`/`ocx2:` 压缩状态转换为原生 Codex 可重放的普通摘要。原生加密内容和其他任务保持不变。运行前请关闭所选任务;如果 rollout 在处理期间发生变化,恢复会停止且不会替换原文件。
### `ocx uninstall` · `ocx remove`
@@ -152,6 +152,8 @@ ocx status --json
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+显式指定重启标志后,即使目录和缓存的字节未变,成功提交 `config.toml` 更改也会触发重启。仅处理配置的操作若没有更改、被拒绝、仅作预检或写入已回滚,则不会触发重启。未指定重启标志时,sync 不会自动重启 Codex。
+
从每个已配置的提供方获取实时模型列表,并将合并后的目录重新注入 Codex。在添加提供方后运行,或用于刷新可用模型。
如果仍有长期运行的 Codex `app-server` 进程,`ocx sync` 会警告它们可能继续提供旧的内存模型列表,即使 `opencodex-catalog.json` / `models_cache.json` 已更新。传入 `--restart-codex` 会重启匹配的 `codex … app-server` 和 `codex-code-mode-host` 进程,并在 macOS、Linux 和 Windows 上完全退出再重新启动 Codex 桌面应用,让模型选择器重新读取目录。进行中的对话会结束。故意避免使用宽泛的 `pkill -f codex` 匹配。
diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md
index 684040f94e8..204e22636e0 100644
--- a/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md
+++ b/docs-site/src/content/docs/zh-cn/reference/cli/providers-accounts.md
@@ -5,6 +5,13 @@ description: 提供方配置、凭据、配额,以及模型目录命令。
这些命令用于配置上游提供方、认证账号、管理凭据池,并控制暴露给 Codex 的模型目录。
+使用剩余套餐额度可通过 `ocx account use-remaining openai main on` 开启,再用
+`ocx account use openai main` 选择主账号;`status --json` 查看状态,`off` 关闭。
+将 `main` 替换为唯一的池账号 ID 或别名即可控制该账号。开启后关闭该账号按用量提前切换;
+主账号的短、长窗口硬锁阈值延后到 100%。开关跨重启保留,关闭后恢复原有阈值。
+暂停、重新认证、上游冷却和付费 credits 权限仍然生效;此开关不会开启付费额度。
+详见[英文说明](/reference/cli/providers-accounts/#use-remaining-included-quota)。
+
## 提供方
### `ocx provider `
diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration.md b/docs-site/src/content/docs/zh-cn/reference/configuration.md
index 66327ddc41c..c8538ee8168 100644
--- a/docs-site/src/content/docs/zh-cn/reference/configuration.md
+++ b/docs-site/src/content/docs/zh-cn/reference/configuration.md
@@ -34,6 +34,25 @@ opencodex 会把持久化配置存放在 `$OPENCODEX_HOME/config.json`,通常
路由有自己独立的顺序化解析规则;见 [Routing](/reference/configuration/routing/)。
+## 原生推理密文保留
+
+仪表盘概览提供“切换模型时保留”和“切换账号时保留”两个独立开关,默认均关闭。也可以在 `config.json` 中配置:
+
+```jsonc
+{
+ "nativeReasoningRetention": {
+ "modelSwitch": true,
+ "accountSwitch": false
+ }
+}
+```
+
+上面的例子允许在同一原生 ChatGPT 目标和凭据下,发生已知模型切换时转发推理 `encrypted_content`。跨账号保留属于实验性选项;模型和账号同时变化时必须同时开启两个开关。策略按实际选中的原生 Combo 成员或账号池重试目标判断,不适用于第三方或 API-key provider。
+
+允许转发不代表上游一定接受或复用推理。已检测到的解密拒绝仍执行现有的有限清理恢复,拒绝缓存优先于开关。旧响应和会话引用、外部推理项 ID、上传文件的账号限制及缓存隔离保持各自的归属规则。原生压缩块继续沿用现有清理行为,原生 `/v1/responses/compact` 的直接转发清理也不改变。
+
+仪表盘和 `PUT /api/native-reasoning-retention` 的修改对后续请求生效。PUT 接受部分布尔字段,JSON `null` 恢复默认值;未知字段或类型错误会被拒绝。使用本地 `ocx config set/unset` 修改后需重启代理。完整说明见 [Native reasoning retention](/reference/configuration/server/#native-reasoning-retention)。
+
## 配置域
- [Providers](/reference/configuration/providers/) — provider 条目、认证、端点、目录、allowlist、上下文限制、配额和 provider 特定选项。
diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md b/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md
index 3ef528c6135..33d2ef714ab 100644
--- a/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/zh-cn/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: 顾问
-description: OpenCodex 自有的专家咨询 sidecar — 配置的专家模型为路由 Worker 提供建议,支持 manual 与 preflight 两种策略。
+description: 独立专家咨询,支持 manual、preflight 与基于证据的 adaptive 策略。
---
顾问是一个独立的专家模型,审阅 Worker 的任务并返回建议。OpenCodex 端到端地拥有整个咨询过程:代理向 Worker 的回合注入合成的 `advisor` 工具,自己通过正常路由权威执行咨询,并回注建议使原 Worker 继续。Worker 无需委托、无需 spawn 任何东西、也不携带 provider 凭据。
@@ -9,6 +9,8 @@ description: OpenCodex 自有的专家咨询 sidecar — 配置的专家模型
## 配置
+在仪表板 **Advisor** 页面从目录选择专家模型。选择器原样保存原生、提供商、指定账户、自定义和组合模型的标识符。已禁用和等待初始选择的模型不会作为新选项。已保存但不在可选目录中的标识符会保留并显示警告,直到您选择其他模型或清除。目录加载失败时保留当前选择并提供重试。请选择能返回文本建议的模型;目录不保证咨询兼容性。CLI 和 API 仍接受路由器支持的模型字符串。
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ description: OpenCodex 自有的专家咨询 sidecar — 配置的专家模型
| `enabled?` | `boolean` | `false` | 总开关。关闭时请求路径上没有任何 advisor 行为。 |
| `model?` | `string` | — | 专家模型。任何路由权威接受的模型字符串:裸原生模型(`gpt-6-astra`)、显式 `provider/model`(`anthropic/claude-sonnet-4-6`、`xai/grok-...`)或账户限定的原生模型。完整支持跨 provider:Worker 与 Advisor 无需同属一个 provider。 |
| `effort?` | `string` | `"max"` | Advisor 调用的推理强度(`low` 至 `ultra`)。 |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | 何时咨询顾问。 |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | 何时咨询顾问。 |
+| `maxConsultationsPerTask?` | `integer` | `3` | 自适应任务咨询上限(1–3 次尝试),包含手动调用。 |
| `timeoutMs?` | `number` | `120000` | 回环咨询超时。 |
| `contextSharingConsent?` | `"v1"` | 缺省 | 操作者同意把任务上下文发给所配置的顾问 provider。只有 `"v1"` 是当前版本。缺省、过期或其他值都表示不发送任务内容。`enabled: true` 本身不是同意。 |
-通过仪表盘的 **Advisor** 页面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。
+通过仪表盘的 **Advisor** 页面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。
`ocx advisor on` 在没有当前同意时不会开启跨 provider 发送:它会打印披露并停止。`ocx advisor on --ack-context-sharing` 与 `ocx advisor consent` 记录 `v1`。`ocx advisor consent --revoke` 会移除同意并立即停止发送。`ocx advisor set` 不授予同意。仪表盘上的同意复选框默认不勾选。
@@ -78,5 +81,13 @@ Advisor 失败是 fail-open 的:已经发出的咨询若失败(模型不可
## PR1 限制
- 原生 OpenAI passthrough 回合(ChatGPT 池 Worker)不会获得合成工具;advisor 支持覆盖路由(translated)provider。preflight 咨询适用于 run-turn 适配器;工具不适用。
-- 无自适应触发:没有卡住检测、重复失败分析、升级分层、多 Advisor 或投票。`manual` 与 `preflight` 是仅有的策略。
+- 不提供语义卡住检测、任务完成复核、升级分层、多专家或投票;自适应触发只使用已观察到的工具证据。
- preflight 去重账本是进程内的;代理重启后,进行中的任务可能再收到一次 preflight 咨询。
+
+## 基于证据的自适应咨询与活动记录
+
+`adaptive` 在初始咨询外,只有在同一工作目录中的同一验证命令失败、后续文件编辑成功、重新执行验证仍失败时才咨询。成功验证会清除对应失败;编辑前已启动的验证不算修复后的重试。初始阶段与失败阶段分别去重,自动咨询需要稳定的会话标识。工具须提供明确退出码和配对的调用/结果;支持常见测试、类型检查、lint、构建命令及已完成的文件编辑,轮询命令和单一静态 Code Mode helper 也可识别。复杂脚本、批量调用、shell 修改及模糊输出不触发。
+
+用 `ocx advisor set --policy adaptive --max-consultations 2` 设置预算。自适应模式中,手动与自动尝试共用每任务 1–3 次预算,服务端在发出调用前写入 `OPENCODEX_HOME/advisor-budget.sqlite`,重启或切换 Worker 不会清零。仅保存任务摘要、次数和过期时间,首次调用后 24 小时过期;最多保留 512 项任务。预算不可用或耗尽时 Worker 继续执行。已发出的失败或取消调用仍可能计费,因此不退回额度。没有稳定会话标识时仅保留请求级的手动调用。
+
+Advisor 页面和 `ocx advisor activity --json`(`GET /api/advisor/activity`)显示本进程最近 24 小时内最多 100 条调用元数据。页面可见时每 5 秒更新。包括原因、状态、模型、耗时、已报告的输入/输出 Token、请求档位与适配器映射档位;不存任务正文、命令、建议或会话标识,重启即清空。映射是适配器发出的参数,不代表已验证模型内部推理;未报告保持未知,不推测精确费用或建议采纳情况。
diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
index 4b738b35b26..e3695f386d6 100644
--- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md
@@ -89,9 +89,12 @@ selector,而不是分配一个新名称。
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`、`openai-responses`、`anthropic`、`google`、`kiro`、`cursor`、`ollama-native`、`azure-openai`(或别名 `azure`)之一。 |
| `baseUrl` | `string` | 上游 API 基础 URL。大多数内置固定端点会忽略不匹配的值;具备冲突安全键的预设会保留一个更早、同名的自定义目标。 |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | 可选的客户端出站请求启动节流,与上游用量、计费和限流指标相互独立。`maxConcurrentRequests` 是限制进行中请求数的正整数,提供商或模型规则都可以只配置此项。提供商限制适用于所有模型,`models` 按上游模型精确 ID 匹配,并可增加延迟或收紧并发限制。排队等待不计入响应头超时。覆盖 HTTP 以及显式适配器 `fetchResponse`/`runTurn` 调用。配置并发上限时,标准 Responses WebSocket 请求改用 HTTP/SSE,以便在响应体完成、出错或取消时释放并发名额。 对包括 Cursor 在内的 `runTurn` 适配器,并发上限计算进行中的回合数,而非物理发送数:同一回合内的 RunSSE 和 BidiAppend 可以重叠,其他回合仍须等待。后续发送仍遵守启动间隔。 |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 可选的客户端出站请求启动节流,与上游用量、计费和限流指标相互独立。`maxConcurrentRequests` 是限制进行中请求数的正整数,提供商或模型规则都可以只配置此项。提供商限制适用于所有模型,`models` 按上游模型精确 ID 匹配,并可增加延迟或收紧并发限制。排队等待不计入响应头超时。覆盖 HTTP 以及显式适配器 `fetchResponse`/`runTurn` 调用。配置并发上限时,标准 Responses WebSocket 请求改用 HTTP/SSE,以便在响应体完成、出错或取消时释放并发名额。 对包括 Cursor 在内的 `runTurn` 适配器,并发上限计算进行中的回合数,而非物理发送数:同一回合内的 RunSSE 和 BidiAppend 可以重叠,其他回合仍须等待。后续发送仍遵守启动间隔。 |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 正常请求的密钥选择:默认 `active` 保留已保存密钥和现有错误切换;`round-robin` 按顺序分配可用池密钥,不修改已保存的选择。均衡的是分配次数,不是令牌或费用。 |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | 按密钥引用设置统一 RPM、最小启动间隔和并发上限,跨模型共享,并同时受提供商和模型上限约束。相同引用共享容量;同一上游项目的不同引用不代表独立配额。不管理 TPM 或每日配额。 |
| `responsesPath?` | `string` | 用于 key-auth `openai-responses` 请求的相对资源路径。必须以 `/` 开头,且不能包含 scheme、query 或 fragment。 |
| `chatCompletionsPath?` | `string` | 用于 `openai-chat` 请求的相对资源路径,是 `responsesPath` 的对应项,适用相同的路径规则。当同一上游以不同前缀提供 Chat Completions 和 Responses 时需要此配置:按模型的 wire override 只更换适配器而不改动 `baseUrl`,否则已启用的 Chat 请求会被发送到 Responses base。随附示例为 Z.AI。 |
+| `projectResponsesLiteHostedSearchHistory?` | `boolean` | 默认关闭。仅实际布尔值 `true` 可为此 provider 显式启用有损 hosted-search 历史投影,且只作用于最终目的地精确为标准 ChatGPT Codex `/responses`、明确选择 Responses Lite 的普通请求。省略或设为 `false` 时保留结构化历史;字符串和数字无效。不会启用 Lite、改变传输,也不影响 native compact、公开 OpenAI API/其他目的地或既有 portable/routed compaction 投影。详见[Lite 搜索历史](/zh-cn/reference/proxy-formats/#codex-原生普通-lite-请求的搜索历史)。 |
| `upstreamWebsocket?` | `boolean` | 为 `openai-responses` 请求选择性启用上游 Responses WebSocket 传输(默认 `false`)。仅对第一方 `https://api.openai.com/v1` 上游生效;自定义提供者端点始终使用有界 HTTP/SSE,因为 Bun 无法在分配完整消息之前对入站 WebSocket 消息实施大小限制。对于规范 ChatGPT `openai` 提供商,省略该字段会在符合条件的轮次使用上游 WebSocket,`false` 通过 HTTP/SSE 发送流式轮次,`true` 会被拒绝;设为 `false` 时,原生轮次中操控与注入不可用。该字段独立于客户端侧的 `websockets` 设置,且不改变端点或凭据。普通 HTTP 仍使用 SSE;非 Responses 路径和 `openai-chat` 请求仍使用 HTTP。 |
| `supportsServiceTier?` | `boolean` | `service_tier` 能力的三态。`true`:fast 模式可以注入,调用方提供的值也会被保留。`false`:剥离该字段且绝不注入(已明确不支持的上游不会收到它)。未设置:未分类——调用方提供的值原样保留,fast 模式绝不注入。注册表已对官方 OpenAI(`true`)、DeepSeek 和 Volcengine Ark(`false`)分类;仅对真正支持分层的自定义网关显式设置。 |
| `responseTierAuthoritative?` | `boolean` | 响应等级是否足以确认或否定 Fast。对响应元数据不具权威性的链路显式设为 `false`;未设置时保持原有行为。不会启用 Fast 或修改出站参数。详见[响应服务等级的可信度](#响应服务等级的可信度)。 |
@@ -113,7 +116,7 @@ selector,而不是分配一个新名称。
| `modelAutoCompactTokenLimits?` | `Record` | 按模型设置的正安全整数软自动压缩预算。该值只能降低“上下文或最大输入的 90%”这一有效上限;无法解析出有效上下文窗口时不会输出。对于规范 `openai`,键必须是受支持的精确原生模型 ID,且不得包含提供者或账户选择器前缀。提供者 PATCH 会合并条目;将某个键设为 `null` 会删除该键,将整个字段设为 `null` 会清空映射。这些 `null` 删除标记仅适用于 PATCH。 |
| `defaultMaxOutputTokens?` | `number` | 当客户端省略 `max_output_tokens` 时,`openai-chat` 的提供者级回退值。 |
| `modelMaxOutputTokens?` | `Record` | 正数型、按模型设置的 `openai-chat` 回退预算;精确/模式匹配优先于提供者默认值。 |
-| `modelCosts?` | `Record` | 按模型设置的显示价格(每 100 万 token 的美元数),以该提供者的精确上游模型 ID 为键(不是提供者标识符或路由后的 `provider/model` 标签),值为四个字段:`input`、`output`、`cacheRead`、`cacheWrite`(示例:`{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。任何模型 ID 都是有效键——自定义提供者可以通过 `openai-chat` 适配器指向任意 OpenAI 兼容端点,即使不存在于内置目录中,本地 OpenAI 兼容和内部提供者的 ID 同样有效。用户配置的价格在 Logs 的 `~$` 和 Usage 估算中优先于内置目录;历史条目也会按当前覆盖项重新计价,因此修改价格可能改变过去的总额(回退顺序:用户配置 → jawcode 目录 → expected-price 覆盖 → 模型级厂商价格);用户明确将所有费率设为零时,会得到已知的零费用估算;删除该模型的覆盖项即可恢复自动定价。目录中的全零价格仍会回退到下一个来源。每个费率必须是大于等于 0 的有限数字,且不超过 1,000,000(每 100 万 token 的美元数);超出范围的条目会在管理边界被拒绝,并在加载时被丢弃。仅用于显示的估算:覆盖项不影响路由、账户选择、配额或计费。 |
+| `modelCosts?` | `Record` | 按模型设置的显示价格(每 100 万 token 的美元数),以该提供者的精确上游模型 ID 为键(不是提供者标识符或路由后的 `provider/model` 标签),值为四个字段:`input`、`output`、`cacheRead`、`cacheWrite`(示例:`{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`)。任何模型 ID 都是有效键——自定义提供者可以通过 `openai-chat` 适配器指向任意 OpenAI 兼容端点,即使不存在于内置目录中,本地 OpenAI 兼容和内部提供者的 ID 同样有效。用户配置的价格在 Logs 的 `~$` 和 Usage 估算中优先于内置目录;历史条目也会按当前覆盖项重新计价,因此修改价格可能改变过去的总额(回退顺序:用户配置 → jawcode 目录 → expected-price 覆盖 → 模型级厂商价格);用户明确将所有费率设为零时,会得到已知的零费用估算;删除该模型的覆盖项即可恢复自动定价。目录中的全零价格仍会回退到下一个来源。每个费率必须是大于等于 0 的有限数字,且不超过 1,000,000(每 100 万 token 的美元数);超出范围的条目会在管理边界被拒绝,并在加载时被丢弃。仅用于显示的估算:覆盖项不影响路由、账户选择、配额或计费。 |
| `headers?` | `Record` | 额外的上游请求头。会拒绝 Authorization、cookie、API key 头、嵌入换行符以及无效名称。 |
| `openRouterRouting?` | `OpenRouterProviderRouting` | 默认的 OpenRouter `order`、`only` 和 `allowFallbacks` 偏好;仅对使用 `openai-chat` 的规范 OpenRouter 有效。 |
| `modelOpenRouterRouting?` | `Record` | 精确模型 id 级别的覆盖项,会替换提供者级 OpenRouter 偏好。 |
@@ -587,3 +590,8 @@ Vercel AI Gateway 可以在多个底层推理提供者之间路由一个模型
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes` 将模型绑定到已保存的 Anthropic OAuth 账户 ID。启用账户池后,区分大小写的 `match` 通配模式按顺序取第一个匹配规则,限制首次选择和 429 重试。仅当该规则没有可用账户时,`fallback: true` 才回退到普通账户池。
+
+
+### `promptLengthPricing`
+
+可选字段 `promptLengthPricing` 扩展四项基础费率。`mode: "automatic"`(或省略该字段)保留目录的上下文计价规则;`mode: "flat"` 禁用这些规则。`mode: "custom"` 使用正安全整数 `thresholdTokens`、值为 `"gt"`(>)或 `"gte"`(≥)的 `comparison`,以及包含四项绝对替代费率的 `rates`。阈值统计所有提示词 token,包括缓存读取和写入,不包括输出。选中的费率适用于包括输出在内的整个请求,并在阈值两侧都取代目录上下文规则。Fast/Priority 调整在选定费率后只应用一次,并遵循目录为该模型公布的 Priority 关系;仅当目录将该关系声明为 `lower-bound`(目前为 xAI Grok)时,估算才会标记为下限;没有目录规则的模型只应用一次 Priority 倍率。可在 **Models → Price** 中配置。**Reset to automatic** 删除包括基础费率和规则在内的整个覆盖配置。旧配置保持兼容;这些设置只涉及显示的费用估算。
diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md
index d28d3e573d1..3679c8c5390 100644
--- a/docs-site/src/content/docs/zh-cn/reference/management-api.md
+++ b/docs-site/src/content/docs/zh-cn/reference/management-api.md
@@ -164,6 +164,7 @@ Aside 配置档的变更在这种情况下仍会保存一件事:确认之后
| `GET /api/config` | 返回已脱敏、对管理安全的配置 DTO | — |
| `PUT /api/config` | 禁用的完整配置替换保护 | 405;请改用聚焦端点 |
| `GET, PUT /api/settings` | 读取运行时/启动设置,或更新自动启动、流模式、应用拥有的内存预算和 `codexAccountPickerEnabled` | 400 无效、非对象或空更新 |
+| `GET, PUT /api/native-reasoning-retention` | 读取模型/账号推理保留开关;部分 PUT 保留未提交字段,JSON `null` 恢复默认值。见 [原生推理密文保留](/zh-cn/reference/configuration/#原生推理密文保留)。 | 400 未知字段、无效类型或非对象/非 null 输入 |
| `GET /api/startup-health` | 读取缓存的服务/shim 启动健康状态 | — |
| `POST /api/startup-action` | 安装或修复服务或 Codex shim | 400 无效动作;500 动作失败 |
| `GET, POST /api/windows-tray` | 读取 Windows 托盘状态,或安装、启动、停止、卸载它 | 400 不支持的平台/动作;500 操作失败 |
diff --git a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md
index 2b8778764f2..daf34a63bc4 100644
--- a/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/zh-cn/reference/proxy-formats.md
@@ -40,6 +40,8 @@ Chat Completions 和 Messages 端点支持延迟提供的工具目录:客户
此外,在所有入站协议中,提供方返回的 `default.` 命名空间调用都会规范化为客户端声明的裸名称,即使延迟目录规则关闭了上述拒绝检查。
+原生 Messages 也会识别与 `tools: []` 一同提供的系统消息内联工具定义。这些名称在被永久移除前仍可调用,但不会授权其他客户端工具。托管 MCP 调用和服务器工具的参数流保留原生格式。
+
## 端点总览
| 客户端表面 | 端点 | 成功的非流式结果 | 成功的流式或套接字结果 |
@@ -260,8 +262,14 @@ Compaction 会为需要缩短长 Responses 会话的客户端返回替换历史
| 路由类型 | 行为 |
| --- | --- |
| Canonical ChatGPT 或官方 OpenAI 路由 | 将请求转发到原生 `/responses/compact` 端点,并使用已解析的账号和模型认证 |
+| Explicitly opted-in custom `openai-responses` gateway | `supportsNativeCompactEndpoint: true` tries the native v1 endpoint for key/local/forward auth using the provider's normal Responses transport; HTTP 404 uses portable fallback; OAuth retains routed compaction and credential resolution |
| 其他路由模型 | 运行一次内部的、非流式、无工具的 compaction 轮次,并带 `compaction_trigger`;要求且只允许一个 synthetic `compaction` 项,其 `encrypted_content` 是一个 `ocx1:` 封装;把该摘要解码为 v1 替换历史 |
+The custom v1 capability is independent of v2 `supportsNativeCompactionTrigger` and
+`decodesNativeCompactionBlobs` for replay. None grants canonical identity or caller credential
+forwarding; cross-provider overrides remain portable. See the canonical
+[provider configuration](/reference/configuration/providers/) before opting in.
+
原生 compact 响应会被缓冲,最大 32 MiB,即使其声明的 `Content-Length` 已经超过该限制也是如此。compact 专用失败包括:
| 状态 | 类型或 code | 含义 |
@@ -326,3 +334,11 @@ Anthropic 来源的失败会以 Anthropic 的错误封装呈现,因此该方
重放的推理项携带的 `encrypted_content` 只有生成它的 provider 和凭据才能读取。如果 opencodex 知道该对话上一次由另一个 provider 处理,它会在发送前移除这个 blob,并保留该项的摘要。如果那个 provider 还使用了不同的 endpoint 或凭据,该项的 `rs_…` id 也会被移除,因为它指向新目标无法查到的项。如果 opencodex 无从得知,例如代理重启之后,新目标会拒绝这个 blob:OpenAI 和 Azure OpenAI 返回 `400 invalid_encrypted_content`。此时 opencodex 会去掉上一个 provider 的推理状态(blob 和 `rs_…` id)后只重发一次请求;保留 id 会导致 `Item with id 'rs_…' not found`。
这项恢复适用于所有使用 Responses 协议的 adapter,因此 `openai-responses` 和 `azure-openai` 的行为相同。恢复成功后,该对话在同一目标上的后续轮次会在接下来五分钟内于首次发送前移除这些状态。重发计入请求的常规发送预算。普通的 400 和 429 不会以这种方式重发,5xx 也不会,只有一个狭窄的例外:对于携带加密工具输出的请求,响应体恰好是该解密拒绝的 502 会得到同样的一次重发。第二次拒绝会原样返回给客户端。遇到这种情况,请在目标 provider 上开始新的对话。
+
+## Codex 原生普通 Lite 请求的搜索历史
+
+普通请求默认保留结构化 hosted-search 历史,Responses Lite 请求也一样。若要选择有损参考说明,请在 `config.json` 中设置 `providers.openai.projectResponsesLiteHostedSearchHistory: true`(必须是布尔值,不能是字符串 `"true"`)。该设置按 provider 配置且默认关闭;省略或设为 `false` 时保留结构化历史。即使已启用,也只有最终目的地精确为标准 ChatGPT Codex `/responses` 且当前有效意图明确选择 Responses Lite 的普通请求,才会把未被同输入引用的剩余 `web_search_call` 历史转为带标签的 assistant 参考说明。最终 `x-openai-internal-codex-responses-lite` header 值精确为 `true` 时选择 Lite;明确的 `false` 优先于 frame metadata。没有可识别 header 值时,采用有效字符串 map 中 `client_metadata.ws_request_header_x_openai_internal_codex_responses_lite` 的 `true` 值,沿用既有 WS metadata 语义。最终有效意图为非 Lite 或无法识别时保留结构化历史;格式无效的 metadata 不会覆盖明确的 HTTP `true`。模型名、订阅鉴权、hosted 声明缺失或仅有 `additional_tools` 都不能选择这项策略;不会为触发转换而改写 header 或 metadata。启用后适用于新 thread、fork、跨模型续聊、缓存未命中或重启,不依赖诊断设置。只有有效同 scope bridge memo 能证明并先恢复原函数调用及结果;ID 前缀、模型名或 memo 缺失不能判断来源。若同输入的 `item_reference.id` 精确指向某 hosted cell,该 cell 保留;`store:false` 会先移除引用,剩余 hosted 历史可投影。工具声明、Lite `additional_tools`、选择、call/result 配对、推理/原生密文、非 hosted 顺序及调用方 raw 历史保持原契约。
+
+这是仅在上述显式启用的 Lite 边界内采用的**有损历史策略**,不是 hosted schema 非法或所有上游错误已修复的断言。说明标为不可信参考数据,复制 status/action 及 action 或顶层 sources 中允许的 URL/title/type,不是指令或网页正文;每字符串最多 2,048 个 UTF-16 单元、每列表 20 项、全部已投影说明共 65,536 字节,超出的 action 用省略说明替代,转换项不复制 ID 或未知字段。HTTP/SSE 与 WS 共用普通请求的最终 builder。
+
+公开 OpenAI API 及其他目的地不新增普通或 native compact 投影。原生 `/responses/compact` 保持既有端点、账户清理及官方 opaque 输出路径,保留结构化 hosted 历史。portable/routed 摘要仍沿用原有触发条件与默认引用行为;共享 renderer 在这些既有路径中也补充顶层 sources 白名单保留。本次不增加重试、来源分类或新的 compaction summarizer。
diff --git a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
index 8a28107b8bc..5d120c5d723 100644
--- a/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/zh-tw/getting-started/quickstart.md
@@ -13,7 +13,7 @@ ocx init
`ocx init` 會引導你完成:
-1. **選擇 provider** —— 從內建 registry 的 104 個預設中選擇一個,或選擇 `custom` 手動輸入
+1. **選擇 provider** —— 從內建 registry 的 105 個預設中選擇一個,或選擇 `custom` 手動輸入
base URL 和 adapter。
2. **API key** —— 貼上一個 key,或引用一個環境變數,例如 `${ANTHROPIC_API_KEY}`。
3. **預設模型** —— 對於 API key、本機和 custom provider,可接受預設值或輸入模型 id。
diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md
index c91637c60ae..dee9b6374a1 100644
--- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md
+++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md
@@ -146,18 +146,18 @@ unknown 表示 opencodex 無法確定設定是否仍指向自己的代理。外
### Picker 模式:在第一方 Code 分頁顯示 opencodex 模型
-Picker 模式是第一方模式的一部分。在 macOS 上選擇第一方時預設開啟;設定
+Picker 模式是第一方模式的一部分。在 macOS 和 Windows 上選擇第一方時預設開啟;設定
`claudeCode.intercept.picker: false` 後會保持關閉。它會修改第一方 Desktop 的 Code 分頁模型選擇器,
-依名稱列出可用的 opencodex 模型。首次開啟時,macOS 可能會要求你在登入鑰匙圈中信任本機憑證授權單位。
+依名稱列出可用的 opencodex 模型。首次開啟時,macOS 可能會要求你在登入鑰匙圈中、Windows 可能會要求你在目前使用者根存放區中信任本機憑證授權單位。
該授權單位限制為 `claude.ai` 及其子網域。可匯出的簽章身分由 OS 認證資料儲存區保護,一般重新啟動會重用相同的憑證與金鑰。
-OpenCodex 設定目錄不會儲存明文 Picker 簽章金鑰。受限 CA 的完整驗證與 OS 信任檢查仍然適用。
-已核准的身分不變且認證資料儲存區可用時,重新啟動不會新增或移除憑證信任設定。啟動復原絕不會安裝信任:
+OpenCodex 設定目錄不會儲存明文 Picker 簽章金鑰。受限 CA 的完整驗證與 OS 信任檢查仍然適用(Windows 按精確指紋與已驗證葉片驗證)。
+已核准的身分不變且認證資料儲存區可用時,重新啟動不會新增或移除 macOS 憑證信任設定。啟動復原與狀態檢查絕不會安裝信任:
若信任缺失、遭撤銷或無法確認,Picker 會維持待處理狀態。請明確執行 `ocx claude desktop picker on`
-或 `ocx claude desktop picker trust` 來授予信任。
+或 `ocx claude desktop picker trust` 來授予信任(Windows 需要時含確認)。
從舊身分進行一次性移轉時,清除原有信任可能需要同意。清理未完成時,Picker 無法使用,已套用的設定檔
-會使用不解密的中繼。macOS 也可能另外要求解鎖鑰匙圈或核准應用程式存取憑證,重新啟動或升級時也可能出現這些提示。
-Windows 與 Linux 仍不支援 Picker,不會啟動 Picker CA、認證資料儲存區或代理作業。主要 Claude 攔截功能仍可用,
+會使用不解密的中繼。macOS 也可能另外要求解鎖鑰匙圈或核准應用程式存取憑證,重新啟動或升級時也可能出現這些提示。Windows 上明確執行 `on` 或 `trust` 可能顯示新增根的確認。
+Linux 仍不支援 Picker,不會啟動 Picker CA、認證資料儲存區或代理作業。主要 Claude 攔截功能仍可用,
其本機 CA 檔案受到擁有者、符號連結、檔案權限及 Windows ACL 檢查保護。
Picker 模式開啟期間,Claude Desktop 會透過 OpenCodex 存取網路。如果 OpenCodex 停止,Desktop 會離線,
diff --git a/docs-site/src/content/docs/zh-tw/guides/combos.md b/docs-site/src/content/docs/zh-tw/guides/combos.md
index 3fdb413a417..d95c8409370 100644
--- a/docs-site/src/content/docs/zh-tw/guides/combos.md
+++ b/docs-site/src/content/docs/zh-tw/guides/combos.md
@@ -165,7 +165,7 @@ Combo 失敗分為**跳轉**失敗與**終端**失敗。
| HTTP 401、403、404、408、429 或任何 5xx | 冷卻目標並跳到下一個合格目標。 |
| 分類為認證、訂閱、配額、限流、過載或上游伺服器錯誤 | 冷卻目標並跳轉,即使單靠狀態碼不足。 |
| 客戶端取消(499)、`origin_rejected`、cyber-policy 拒絕、上下文溢出或其他無效請求 | 停止並回傳錯誤;另一個目標不會讓請求變為有效。 |
-| 結構化 HTTP 400,明確拒絕 `user`、對 `reasoning.effort`/`reasoning_effort` 回傳不支援值,或回傳模型特定影像輸入拒絕(`param: input`) | 在輸出開始前跳轉到下一個符合條件的目標,且不記錄冷卻時間;參見下方選用參數相容性。 |
+| 結構化 HTTP 400,明確拒絕 `user`、對 `reasoning.effort`/`reasoning_effort` 回傳不支援值、回傳模型特定影像輸入拒絕(`param: input`),或拒絕該路由無法讀取的輸入媒體(`unsupported_input_modality`) | 在輸出開始前跳轉到下一個符合條件的目標,且不記錄冷卻時間;參見下方選用參數相容性。 |
| 由行程內轉接器(`runTurn`)執行的 Responses 回合中,目前請求未宣告的第一個工具呼叫(在任何輸出與不可重播的副作用之前) | 讓該目標進入冷卻,並以相同的工具目錄跳轉到下一個目標。出現可見輸出或不可重播的副作用之後,拒絕即為最終結果。Chat Completions 與 Anthropic Messages 請求不受影響。 |
| 任何其他未分類錯誤 | 停止並回傳錯誤。 |
@@ -221,7 +221,7 @@ Codex v2 子代理有一個重要限制([issue #92](https://github.com/lidge-j
### 儀表板
-開啟本機儀表板並選擇 **Combos**。該工作區可建立、編輯、重新命名與移除 combo,且其目標 picker 會排除已停用的模型與巢狀 combo。
+開啟本機儀表板並選擇 **Combos**。該工作區可建立、編輯、重新命名與移除 combo。模型選擇器包含隱藏模型;顯示/隱藏設定僅影響對外模型清單,不影響 Combo 內部選擇,選取隱藏模型也不會將其對外顯示。目標選擇器仍排除已停用的供應商、巢狀 combo 與僅提供決策服務的 JEV 供應商。
每個目標也會顯示即時額度徽章:**可用**、**額度已用盡**或**額度未知**。只有當每個可用目標均有目前有效的伺服器確認,顯示其所設定憑證的推論限額已耗盡時,編輯器才會因配額而停用儲存與建立。僅供顯示的帳戶、模型、搜尋與 MCP 配額,以及缺失或已過期的路由依據,都不會觸發此限制。此限制會在適用的重設時間或資料有效期限結束時解除,並在頁面變為作用中或可見狀態時重新檢查;重新整理會同時重新載入 Combo 資料與配額。
@@ -296,6 +296,6 @@ Combo id 未知。回應為 HTTP 404 並帶 type `invalid_request_error`。執
## 選用參數相容性
-一般 400 錯誤仍會終止請求,但明確拒絕 `user`、對 `reasoning.effort`/`reasoning_effort` 回傳不支援值,或回傳模型特定影像輸入拒絕(`param: input`)的結構化錯誤,可讓 combo 在輸出開始前嘗試下一個符合條件的目標,而不記錄冷卻時間。安全政策拒絕、取消及已開始的輸出仍不可重播。
+一般 400 錯誤仍會終止請求,但明確拒絕 `user`、對 `reasoning.effort`/`reasoning_effort` 回傳不支援值、回傳模型特定影像輸入拒絕(`param: input`),或拒絕該路由無法讀取的輸入媒體(`unsupported_input_modality`)的結構化錯誤,可讓 combo 在輸出開始前嘗試下一個符合條件的目標,而不記錄冷卻時間。安全政策拒絕、取消及已開始的輸出仍不可重播。
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
diff --git a/docs-site/src/content/docs/zh-tw/guides/providers.md b/docs-site/src/content/docs/zh-tw/guides/providers.md
index 38a7b1a8060..c0d81d33176 100644
--- a/docs-site/src/content/docs/zh-tw/guides/providers.md
+++ b/docs-site/src/content/docs/zh-tw/guides/providers.md
@@ -98,6 +98,7 @@ ocx login google-antigravity
ocx login cursor # 獨立 Cursor PKCE 登入
ocx login command-code # Command Code browser OAuth(或匯入 ~/.commandcode/auth.json)
ocx login devin # Cognition/Devin:優先匯入 Devin CLI 憑證,否則走 Auth0 瀏覽器登入
+ocx login dsh-account # DeepSeek Harness Desktop(匯入 ~/.dsh/.credentials.yaml 授權)
ocx login github-copilot # GitHub device flow → Copilot token(Copilot Pro/Business)
ocx login codex # Codex 帳號池(別名:chatgpt、openai;需要 proxy 正在執行)
ocx logout
@@ -114,6 +115,7 @@ ocx logout
| `google-antigravity` | `google` | `https://daily-cloudcode-pa.googleapis.com` | 透過 Cloud Code Assist wire 使用 Google OAuth。即時探索使用 CCA 經認證的 `v1internal:fetchAvailableModels` 端點,發布目前登入帳號可用的 agent 模型;維護中的 catalog 作為 fallback。 |
| `cursor` | `cursor` | `https://api2.cursor.sh` | 實驗性 PKCE 登入、即時 HTTP/2 transport 與按帳號篩選的模型探索。 |
| `devin` | `devin` | `https://server.codeium.com` | 實驗性的非官方 Cognition/Devin 橋接。登入會先匯入已安裝 Devin CLI 已持有的憑證(`devin auth login` 會把 `devin-session-token` 寫入它自己的 `credentials.toml`);沒有則開啟 Auth0 瀏覽器頁面,再以 `RegisterUser` 將貼上的權杖換成長期 API 金鑰。`ocx login devin-cli` 仍作為已棄用別名可用。模型清單依帳號透過 `GetCascadeModelConfigs` 即時取得,串流僅走 Connect-RPC 上的 `runTurn` 路徑。預設不在儀表板預設集內,需手動啟用。 |
+| `dsh-account` | `dsh-account` | `https://api.deepseek.com/anthropic` | 實驗性優先匯入帳號提供者,複用已安裝並登入的 DeepSeek Harness (DSH) Desktop 帳號。在使用者明確匯入時嚴格以唯讀方式讀取 `~/.dsh/.credentials.yaml` 憑據。獨立於 DeepSeek API Key 計費。 |
| `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | 實驗性。GitHub device flow + `copilot_internal` exchange(VS Code OAuth client)。需要有效 Copilot 訂閱;不是官方第三方 API。 |
Google Antigravity 帳戶與供應商的配額查詢(包括模型清單備援)使用固定的 Google 計量端點。這些目標支援透明 Fake-IP DNS,同時保留 TLS 驗證、重新導向拒絕與私有位址檢查。自訂 base URL 只改變模型請求,不改變配額目標;`NO_PROXY` 仍使用直連政策。
@@ -247,9 +249,20 @@ database 並移除目前的 WAL、SHM 與 journal sidecar,再發布先前的 s
`kiro-cli` data path 修復或移除不可讀 database、取消這些 import selector 後重試。沒有既有
`kiro-cli` session 的新機器登入不受影響。
+### DeepSeek Account (DSH)
+
+`dsh-account` 是一個實驗性的、優先匯入既有憑據的帳號提供者,用於複用已安裝且登入的 DeepSeek Harness (DSH) Desktop 應用程式之認證憑據。
+
+- **獨立於 DeepSeek API Key**:`dsh-account` 使用使用者的 DeepSeek 認證工作階段額度(一般/贈送錢包餘額與總支出),而 `deepseek` 提供者則依平台 API Key 餘額扣款。這兩個憑據體系與路由嚴格隔離,互不 fallback。
+- **僅明確匯入**:OpenCodeX 會偵測是否已安裝 DeepSeek Harness 且已登入(`~/.dsh/.credentials.yaml`),但絕不會在未經使用者明確操作(`ocx login dsh-account` 或在 Web 儀表板點選匯入)的情況下自動複製、匯入或使用該權杖。
+- **對 DSH 憑據存放區唯讀**:OpenCodeX 將 `~/.dsh/.credentials.yaml` 視為嚴格唯讀。OpenCodeX 絕不寫入、修改、刪除或變更任何屬於 DeepSeek Harness 的檔案權限。
+- **隔離的憑據複本與登出**:匯入後,OpenCodeX 會透過 DeepSeek 權威身分端點(`/auth-api/v0/users/current`)驗證權杖,並在 OpenCodeX 認證存放區中保存獨立複本。在 OpenCodeX 中登出或移除該帳號僅會刪除 OpenCodeX 本地複本,絕不觸發上游遠端工作階段登出,也不會使 DeepSeek Harness Desktop 登出。
+- **無背景權杖自動輪替與 401 重新匯入**:DSH 工作階段授權不提供自動背景重新整理端點。若上游傳回 HTTP 401(`ACCOUNT_TOKEN_INVALID`),OpenCodeX 會將該帳號標記為 `needsReauth` 並停止路由。若要恢復存取,使用者必須透過 `ocx login dsh-account` 明確重新匯入。
+- **第三方免責聲明**:OpenCodeX 不主張 DeepSeek 官方對第三方客戶端複用工作階段授權提供背書或支援。
+
## 3. API 金鑰目錄
-opencodex 內建 104 個 preset:85 個 key-based、15 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward
+opencodex 內建 105 個 preset:85 個 key-based、16 個 OAuth、3 個 local,以及 1 個預設 ChatGPT-forward
preset。儀表板的 **Add provider** picker 會開啟 key provider 的 dashboard、驗證金鑰並儲存;驗證方式
依 provider 而異。主要條目如下。
diff --git a/docs-site/src/content/docs/zh-tw/guides/sidecars.md b/docs-site/src/content/docs/zh-tw/guides/sidecars.md
index fae3319f226..bc200704d04 100644
--- a/docs-site/src/content/docs/zh-tw/guides/sidecars.md
+++ b/docs-site/src/content/docs/zh-tw/guides/sidecars.md
@@ -66,8 +66,10 @@ OAuth 帳號時使用 `anthropic`,否則使用 `openai`。明確選擇 `anthro
當路由模型列在其 provider 的 `noVisionModels` 中,或該模型在 `modelInputModalities` 中被宣告為僅文字,
且請求包含圖像時,只要有可用的 vision sidecar plan,opencodex 就會在主呼叫**之前**描述每張圖像並用文字替換圖像。
-若沒有可用 plan,原始圖像會被移除,不會繼續轉送給純文字後端。模型目錄會為每個由 sidecar 處理的模型宣告圖像輸入。
-只有當每個 combo 成員都能原生或透過 sidecar 接受圖像,且 combo 的 `imageInput` 設定未停用時,combo 才會宣告圖像輸入;
+若沒有可用 plan,原始圖像會被移除,不會繼續轉送給純文字後端。當 `visionSidecar.enabled: false` 時,
+模型目錄不會為純文字模型新增由 sidecar 提供的圖像輸入,包括自訂項目與 combo 成員。原生圖像支援及明確設定的圖像模態會保留。
+省略 `enabled` 時,仍會宣告 sidecar 提供的圖像輸入;目錄中顯示這項能力不代表已確認 sidecar 的憑證或處理容量可用。
+只有當每個 combo 成員都能原生或透過已啟用的 sidecar 接受圖像,且 combo 的 `imageInput` 設定未停用時,combo 才會宣告圖像輸入;
如此 Codex 應用程式等用戶端會允許附件,而不會在 sidecar 執行前阻擋它們。Dashboard 和管理 API 目前顯示的預設值是
`gpt-5.6-luna`,啟動時也會把明確儲存的舊 `gpt-5.4-mini` 值遷移到 Luna。只有在
`visionSidecar.model` 欄位不存在或為空字串時,vision 執行路徑才會使用程式碼中的 `gpt-5.6-luna` 回退值。
@@ -121,6 +123,8 @@ OAuth 帳號時使用 `anthropic`,否則使用 `openai`。明確選擇 `anthro
`PUT /api/sidecar-settings` 接受相同欄位。部分更新會保留未提交的鍵。`timeoutMs` 使用執行時整數邊界(1–2147483647 毫秒)。
+透過 `PUT /api/sidecar-settings` 變更 Vision 開關的實際生效狀態後,儲存完成便會重新整理受管理的目錄與快取;`catalogRefreshPending=true` 表示重新整理未完成,並非用戶端的確認回覆。
+
Web 搜尋 sidecar 卡片採用相同的控制項形態:模型選擇器的第一列是 **關閉 (Off)**。關閉會停止
OpenCodex 對 `web_search` 的攔截,同時 Codex 整合會把 `web_search = "disabled"`
寫入 `~/.codex/config.toml`;因為 Codex 在自身模式如此宣告前會一直宣告其原生託管的
diff --git a/docs-site/src/content/docs/zh-tw/guides/subagent-v1-default.md b/docs-site/src/content/docs/zh-tw/guides/subagent-v1-default.md
index 4215a418350..bdab9f109f1 100644
--- a/docs-site/src/content/docs/zh-tw/guides/subagent-v1-default.md
+++ b/docs-site/src/content/docs/zh-tw/guides/subagent-v1-default.md
@@ -94,3 +94,9 @@ ocx v2 mode v1
```
CLI 不會提示。這是同一項設定;選擇前請理解本頁所述的影響。
+
+## 子代理並行上限
+
+在 **Models → Model settings** 中,子代理上限控制項可設定同時執行的子代理數量上限,不包括主代理。v1、base 和 v2 均可使用,包括全域 v2 旗標關閉的混合模式。介面模式與此上限分別設定。
+
+提高上限可允許更多子代理,但不會自動建立子代理,也不保證執行更快。變更後請啟動新的 Codex 工作階段。如果長時間執行的 App 主機仍使用舊上限,請重新啟動該主機。
diff --git a/docs-site/src/content/docs/zh-tw/reference/cli.md b/docs-site/src/content/docs/zh-tw/reference/cli.md
index 37f9f01d563..acfdece2371 100644
--- a/docs-site/src/content/docs/zh-tw/reference/cli.md
+++ b/docs-site/src/content/docs/zh-tw/reference/cli.md
@@ -28,7 +28,7 @@ opencodex 的命令列工具是 `ocx`。它依第一個命令名稱分派,有
設定路徑。停止或無法連線的代理以 HTTP 503 呈現,並產生非零的 CLI 離開碼。明確記載為
離線設定操作的命令,可以在沒有執行中代理的情況下驗證與編輯設定檔。
-`ocx system codex-cli-update check` 不需要執行中的代理,也不會向套件 registry 發出請求。它只會在限定範圍內檢查設定中的安裝候選項來源中繼資料,包括經過遮罩的可執行檔位置與所有權證據。正式發布的 launcher 所提供的可信內容只會驗證該候選項快照,並不證明 Codex 已成功執行。由於這個單次檢查命令絕不會執行 Codex,來自環境變數與持久化記錄的候選項只供報告(`managed: false`,通常為 `selection_unattested`);JSON 輸出包含 `candidateAvailable`、`candidateVersion` 與 `candidateSource`,而 `selectionAttested` 維持 `false`。檢查設定中的安裝候選項時,必須有正式發布的 launcher 所提供的可信內容;直接使用 Bun 啟動或從原始碼執行時不具備這項證明,因此會忽略來自環境與持久化記錄的候選項狀態,並可能報告 POSIX 下的 `candidate_unavailable` 或 Windows 下的 `windows_inspection_deferred`。在 Windows 上,這個首個切片不會對候選路徑或設定路徑執行任何檔案系統 I/O。只有由可信 launcher 擷取的絕對環境候選項可以取得應用程式封裝或版本管理工具的純詞彙標籤;其他所有 Windows 候選項都會以失敗關閉方式處理。此命令不會安裝或修復軟體、不會執行 Codex 或 npm、不會控制執行中的程序,也不會寫入設定或快取狀態。
+`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows 不讀取候選項或設定檔,而是延後檢查。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。
觀測 Windows x64 安裝的方式請參閱 [`attest` 命令](/zh-tw/reference/cli/agents/)。未提供明確路徑時,該命令會觀測由受證明約束的啟動器快照識別出的已選候選項;它不授予更新權限,也不證明執行階段選擇。
diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md
index 92bbc9fde20..4728a5f131b 100644
--- a/docs-site/src/content/docs/zh-tw/reference/cli/agents.md
+++ b/docs-site/src/content/docs/zh-tw/reference/cli/agents.md
@@ -209,7 +209,9 @@ opencode 會插值 `{env:OPENCODEX_OPENCODE_API_KEY}`。Pi 與 OMP 的匯出不
管理無頭執行階段設定、啟動、同步、診斷與更新。
-`ocx system codex-restart --yes` 透過與 `ocx sync --restart-codex` 相同的模組重啟 Codex app-server,並完全結束再重新啟動 Codex 桌面應用程式。若代理本身在 Codex 應用程式內部執行,此命令會給出可執行提示並拒絕,而不是承諾無法完成的移交。
+`ocx system codex-restart --yes` 透過與 `ocx sync --restart-codex` 相同的模組重啟 Codex app-server。模型選擇器在應用程式啟動時建立,因此當桌面應用程式於目前模型目錄寫入之前啟動,或本次請求變更了 `model_catalog_json` 綁定時,會完全結束再重新啟動它。否則應用程式保持開啟,自行重啟其 app-server,並保留未送出的草稿與待處理的核准。若無法讀取應用程式或目錄狀態,則執行完整重啟。若代理本身在 Codex 應用程式內部執行,桌面重啟會給出可執行提示並拒絕,而不是承諾無法完成的移交。
+
+如果本次請求變更了 `model_catalog_json` 綁定,即使桌面程序在目錄檔案寫入後才啟動,也會觸發桌面重新啟動。
```bash
ocx system settings --stream-mode eager-relay
@@ -221,7 +223,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` 不會向套件 registry 發出請求,只會在限定範圍內檢查設定中的安裝候選項來源證據,包括經過遮罩的可執行檔位置與所有權證據。正式發布的 launcher 所提供的可信內容只會驗證該候選項快照,並不證明 Codex 已成功執行。由於這個單次命令絕不會執行 Codex,來自環境變數與持久化記錄的候選項只供報告(`managed: false`,通常為 `selection_unattested`);JSON 輸出包含 `candidateAvailable`、`candidateVersion` 與 `candidateSource`,而 `selectionAttested` 維持 `false`。檢查設定中的安裝候選項時,必須有正式發布的 launcher 所提供的可信內容;直接使用 Bun 啟動或從原始碼執行時不具備這項證明,因此會忽略來自環境與持久化記錄的候選項狀態,並可能在 POSIX 系統上報告 `candidate_unavailable`。在 Windows 上,這個首個切片不會對候選路徑或設定路徑執行任何檔案系統 I/O。只有由可信 launcher 擷取的絕對環境候選項可以取得應用程式封裝或版本管理工具的純詞彙標籤;其他所有 Windows 候選項都會以失敗關閉方式處理。由於這個切片完全不會讀取持久化的選擇狀態,在未擷取任何環境候選項的 Windows 執行中會報告 `windows_inspection_deferred` 而非 `candidate_unavailable`:該命令無法觀測 Codex CLI 是否已安裝,因此會報告檢查被延後,而不是斷言候選項不存在。此命令不會執行 Codex 或套件管理工具、不會修復 shim、不會寫入設定或快取、不會停止程序,也不會安裝任何內容。隨應用程式封裝的候選項、位於已識別版本管理工具路徑中的候選項、未經驗證的獨立候選項,以及 shim 狀態不明確的候選項,都會報告為 `unmanaged` 或 `unknown`,絕不會歸類為 `managed`。
+`check` 不查詢註冊表,也不執行 Codex。在 POSIX 上,當已發布啟動器擷取的環境候選項與執行階段解析器選擇的正規路徑相同時,`selectionAttested` 可以為 `true`;若關聯的 npm-global 套件資訊清單證明了所有權,`managed` 可以為 `true`。持久化候選項仍不獲認證。Windows 不讀取候選項或設定檔,而是延後檢查。直接透過 Bun 或原始碼執行時沒有啟動器證明。`plan` 使用隔離的暫存 npm 設定、快取和日誌查詢官方註冊表,並拒絕活動工作階段或不完整的程序觀測。它不安裝軟體,也不向程序傳送訊號。請參閱[完整命令與拒絕原因(英文)](/reference/cli/agents/#read-only-codex-cli-update-plan)。
在 Windows 上,如果擷取到 `CODEX_CLI_PATH=codex` 這類單純命令名稱、遠端路徑或裝置路徑,則回報 `candidate_path_unavailable`。這些情況已有擷取的候選項,但其路徑不適用於此檢查。
diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md
index c67785a0f30..d65ca711fb1 100644
--- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md
@@ -61,7 +61,7 @@ ocx eject back
### `ocx recover-history --ocx-compaction --yes`
-在透過原生 Codex 恢復曾由路由提供方壓縮的工作前,修復該工作的歷史記錄。此命令依 UUID 精確選取一個工作,先儲存私有的逐位元組備份,然後只把 OpenCodeX 自有的 `ocx1:` 壓縮狀態轉換成原生 Codex 可重播的普通摘要。原生加密內容與其他工作不會變更。執行前請關閉所選工作;若 rollout 在處理期間發生變化,復原會停止且不會取代原始檔案。
+在透過原生 Codex 恢復曾由路由提供方壓縮的工作前,修復該工作的歷史記錄。此命令依 UUID 精確選取一個工作,先儲存私有的逐位元組備份,然後只把 OpenCodeX 自有的 `ocx1:`/`ocx2:` 壓縮狀態轉換成原生 Codex 可重播的普通摘要。原生加密內容與其他工作不會變更。執行前請關閉所選工作;若 rollout 在處理期間發生變化,復原會停止且不會取代原始檔案。
### `ocx uninstall` · `ocx remove`
@@ -146,6 +146,8 @@ ocx status --json
### `ocx sync [--restart-codex] [--restart-app-server-only]`
+明確指定重新啟動旗標後,即使目錄和快取的位元組未變,成功提交 `config.toml` 變更也會觸發重新啟動。僅處理設定的操作若沒有變更、遭拒、只做預先檢查或寫入已回復,則不會觸發重新啟動。未指定重新啟動旗標時,sync 不會自動重新啟動 Codex。
+
從每個已設定的供應商擷取即時模型清單,並將合併後的目錄重新注入 Codex。在新增供應商後或要重新整理可用模型時執行它。
若長壽的 Codex `app-server` 仍在執行,`ocx sync` 會警告它們可能繼續提供先前的記憶體內模型清單,即使 `opencodex-catalog.json` / `models_cache.json` 已更新。傳入 `--restart-codex` 會重啟相符的 `codex … app-server` 與 `codex-code-mode-host` 進程,並在 macOS、Linux 與 Windows 上完全結束再重新啟動 Codex 桌面應用程式,讓模型選擇器重新讀取目錄。進行中的對話會結束。刻意避免廣泛的 `pkill -f codex` 比對。
diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md b/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md
index 655328acc95..05a2936e8f5 100644
--- a/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md
+++ b/docs-site/src/content/docs/zh-tw/reference/configuration/advisor.md
@@ -1,6 +1,6 @@
---
title: 顧問
-description: OpenCodex 自有的專家諮詢 sidecar — 設定的專家模型為路由 Worker 提供建議,支援 manual 與 preflight 兩種策略。
+description: 獨立專家諮詢,支援 manual、preflight 與依據證據的 adaptive 策略。
---
顧問是一個獨立的專家模型,審閱 Worker 的任務並返回建議。OpenCodex 端到端地擁有整個諮詢過程:代理向 Worker 的回合注入合成的 `advisor` 工具,自己透過正常路由權威執行諮詢,並回注建議使原 Worker 繼續。Worker 無需委託、無需 spawn 任何東西、也不攜帶 provider 憑證。
@@ -9,6 +9,8 @@ description: OpenCodex 自有的專家諮詢 sidecar — 設定的專家模型
## 設定
+在儀表板 **Advisor** 頁面從目錄選擇專家模型。選擇器原樣儲存原生、供應商、指定帳戶、自訂和組合模型的識別碼。已停用和等待初始選擇的模型不會作為新選項。已儲存但不在可選目錄中的識別碼會保留並顯示警告,直到您選擇其他模型或清除。目錄載入失敗時保留目前選擇並提供重試。請選擇能回傳文字建議的模型;目錄不保證諮詢相容性。CLI 和 API 仍接受路由器支援的模型字串。
+
```json
{
"advisor": {
@@ -26,11 +28,12 @@ description: OpenCodex 自有的專家諮詢 sidecar — 設定的專家模型
| `enabled?` | `boolean` | `false` | 總開關。關閉時請求路徑上沒有任何 advisor 行為。 |
| `model?` | `string` | — | 專家模型。任何路由權威接受的模型字串:裸原生模型(`gpt-6-astra`)、明確 `provider/model`(`anthropic/claude-sonnet-4-6`、`xai/grok-...`)或帳戶限定的原生模型。完整支援跨 provider:Worker 與 Advisor 無需同屬一個 provider。 |
| `effort?` | `string` | `"max"` | Advisor 呼叫的推理強度(`low` 至 `ultra`)。 |
-| `policy?` | `"manual" \| "preflight"` | `"manual"` | 何時諮詢顧問。 |
+| `policy?` | `"manual" \| "preflight" \| "adaptive"` | `"manual"` | 何時諮詢顧問。 |
+| `maxConsultationsPerTask?` | `integer` | `3` | 自適應任務諮詢上限(1–3 次嘗試),包含手動呼叫。 |
| `timeoutMs?` | `number` | `120000` | 回環諮詢逾時。 |
| `contextSharingConsent?` | `"v1"` | 缺省 | 操作者同意把任務上下文傳送給所設定的顧問 provider。只有 `"v1"` 是目前版本。缺省、過期或其他值都表示不傳送任務內容。`enabled: true` 本身不是同意。 |
-透過儀表板的 **Advisor** 頁面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。
+透過儀表板的 **Advisor** 頁面或 `ocx advisor status|on|off|set --model --effort --policy ` 管理。
沒有目前同意時,`ocx advisor on` 不會開啟跨 provider 傳送:它會印出揭露並停止。`ocx advisor on --ack-context-sharing` 與 `ocx advisor consent` 記錄 `v1`。`ocx advisor consent --revoke` 會移除同意並立即停止傳送。`ocx advisor set` 不授予同意。儀表板上的同意核取方塊預設不勾選。
@@ -78,5 +81,13 @@ Advisor 失敗是 fail-open 的:已經發出的諮詢若失敗(模型不可
## PR1 限制
- 原生 OpenAI passthrough 回合(ChatGPT 池 Worker)不會獲得合成工具;advisor 支援覆蓋路由(translated)provider。preflight 諮詢適用於 run-turn 介面卡;工具不適用。
-- 無自適應觸發:沒有卡住偵測、重複失敗分析、升級分層、多 Advisor 或投票。`manual` 與 `preflight` 是僅有的策略。
+- 不提供語義卡住偵測、任務完成複核、升級分層、多專家或投票;自適應觸發只使用已觀察到的工具證據。
- preflight 去重帳本是行程內的;代理重啟後,進行中的任務可能再收到一次 preflight 諮詢。
+
+## 依據證據的自適應諮詢與活動記錄
+
+`adaptive` 在初始諮詢之外,只有同一工作目錄中的同一驗證命令失敗、後續檔案編輯成功、重新執行驗證仍失敗時才諮詢。成功驗證會清除對應失敗;編輯前已啟動的驗證不算修復後重試。初始與失敗階段分別去重,自動諮詢需要穩定的對話識別碼。工具須提供明確退出碼及配對的呼叫/結果;複雜腳本、批次呼叫、shell 修改及模糊輸出不觸發。
+
+以 `ocx advisor set --policy adaptive --max-consultations 2` 設定預算。自適應模式中,手動與自動嘗試共用每項任務 1–3 次預算,服務端在發出呼叫前寫入 `OPENCODEX_HOME/advisor-budget.sqlite`,重啟或切換 Worker 不會歸零。僅儲存任務摘要、次數及到期時間,首次呼叫後 24 小時到期;最多保留 512 項任務。預算無法使用或耗盡時 Worker 繼續執行。已發出的失敗或取消呼叫仍可能計費,因此不退還額度。缺少穩定識別碼時僅保留請求級手動呼叫。
+
+Advisor 頁面與 `ocx advisor activity --json`(`GET /api/advisor/activity`)顯示目前程序最近 24 小時內最多 100 筆呼叫中繼資料,可見時每 5 秒更新。包括原因、狀態、模型、耗時、已回報的輸入/輸出 Token、要求及介接器映射的推理等級;不存任務內文、命令、建議或對話識別碼,重啟即清空。映射僅代表介接器發出的參數;未回報保持未知,不推測精確費用或建議採納情況。
diff --git a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md
index 4e805cf5d69..073d5653574 100644
--- a/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/zh-tw/reference/configuration/providers.md
@@ -69,9 +69,12 @@ ocx models provider openrouter on
| --- | --- | --- |
| `adapter` | `string` | `openai-chat`、`openai-responses`、`anthropic`、`google`、`kiro`、`cursor`、`ollama-native`、`azure-openai`(或別名 `azure`)之一。 |
| `baseUrl` | `string` | 上游 API base URL。多數內建固定端點忽略不符;碰撞安全的金鑰預設保留較舊的同名自訂目的地。 |
-| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | 選用的用戶端出站請求啟動節流,與上游用量、計費及限流指標彼此獨立。`maxConcurrentRequests` 是限制進行中請求數的正整數,供應商或模型規則都可單獨設定此項。供應商限制適用於所有模型,`models` 依上游模型精確 ID 比對,並可增加延遲或收緊並發限制。排隊等待不計入回應標頭逾時。涵蓋 HTTP 及明確的適配器 `fetchResponse`/`runTurn` 呼叫。設定並行上限時,標準 Responses WebSocket 請求改用 HTTP/SSE,以便在回應本文完成、出錯或取消時釋放並行名額。 對包含 Cursor 的 `runTurn` 適配器,並行上限計算進行中的回合數,而非實體傳送數:同一回合內的 RunSSE 與 BidiAppend 可重疊,其他回合仍須等待。後續傳送仍遵守啟動間隔。 |
+| `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models?, keySelection?, keys? }` | 選用的用戶端出站請求啟動節流,與上游用量、計費及限流指標彼此獨立。`maxConcurrentRequests` 是限制進行中請求數的正整數,供應商或模型規則都可單獨設定此項。供應商限制適用於所有模型,`models` 依上游模型精確 ID 比對,並可增加延遲或收緊並發限制。排隊等待不計入回應標頭逾時。涵蓋 HTTP 及明確的適配器 `fetchResponse`/`runTurn` 呼叫。設定並行上限時,標準 Responses WebSocket 請求改用 HTTP/SSE,以便在回應本文完成、出錯或取消時釋放並行名額。 對包含 Cursor 的 `runTurn` 適配器,並行上限計算進行中的回合數,而非實體傳送數:同一回合內的 RunSSE 與 BidiAppend 可重疊,其他回合仍須等待。後續傳送仍遵守啟動間隔。 |
+| `requestPacing.keySelection?` | `"active"`, `"round-robin"` | 正常請求的金鑰選擇:預設 `active` 保留已儲存金鑰及既有錯誤切換;`round-robin` 依序分配可用池金鑰,不修改已儲存選擇。均衡的是分配次數,而非權杖或費用。 |
+| `requestPacing.keys?` | `{ requestsPerMinute?, minIntervalMs?, maxConcurrentRequests? }` | 依金鑰參照設定統一 RPM、最小啟動間隔及並行上限,跨模型共享,並同時受供應商及模型上限約束。相同參照共享容量;同一上游專案的不同參照不代表獨立配額。不管理 TPM 或每日配額。 |
| `responsesPath?` | `string` | Key-auth `openai-responses` 請求的相對資源路徑。必須以 `/` 開頭且不含 scheme、query 或 fragment。 |
| `chatCompletionsPath?` | `string` | `openai-chat` 請求的相對資源路徑,為 `responsesPath` 的對應項,適用相同的路徑規則。當同一上游以不同前綴提供 Chat Completions 與 Responses 時需要此設定:按模型的 wire override 只更換適配器而不改動 `baseUrl`,否則已啟用的 Chat 請求會送往 Responses base。隨附範例為 Z.AI。 |
+| `projectResponsesLiteHostedSearchHistory?` | `boolean` | 預設關閉。僅實際布林值 `true` 可為此供應商明確啟用有損 hosted-search 歷史投影,且只作用於最終目的地精確為標準 ChatGPT Codex `/responses`、明確選擇 Responses Lite 的一般請求。省略或設為 `false` 時保留結構化歷史;字串和數字無效。不會啟用 Lite、變更傳輸,也不影響 native compact、公開 OpenAI API/其他目的地或既有 portable/routed compaction 投影。詳見[Lite 搜尋歷史](/zh-tw/reference/proxy-formats/#codex-原生一般-lite-請求的搜尋歷史)。 |
| `upstreamWebsocket?` | `boolean` | 為 `openai-responses` 請求選用上游 Responses WebSocket 傳輸(預設 `false`)。僅對第一方 `https://api.openai.com/v1` 上游生效;自訂供應商端點一律使用有界 HTTP/SSE,因為 Bun 無法在配置完整訊息之前對傳入 WebSocket 訊息套用大小限制。對於規範 ChatGPT `openai` 供應商,省略此欄位會在符合條件的回合使用上游 WebSocket,`false` 會以 HTTP/SSE 傳送串流回合,`true` 會被拒絕;設為 `false` 時,原生回合中操控與注入無法使用。此欄位獨立於用戶端 `websockets` 設定,且不會變更端點或認證資料。一般 HTTP 仍使用 SSE;非 Responses 路徑與 `openai-chat` 請求仍使用 HTTP。 |
| `disabled?` | `boolean` | 將供應商保留在磁碟上但排除於路由與模型/目錄清單。 |
| `apiKey?` | `string` | API 金鑰,或在請求時解析的 `${ENV_VAR}` / `$ENV_VAR` 參考。 |
@@ -510,3 +513,8 @@ Vercel AI Gateway 可在多個底層推論供應商之間路由一個模型。`v
### `anthropicAccountPool.routes`
`anthropicAccountPool.routes` 將模型綁定至已儲存的 Anthropic OAuth 帳戶 ID。啟用帳戶池後,區分大小寫的 `match` 萬用模式依順序採用第一個符合的規則,限制首次選擇與 429 重試。僅當該規則沒有可用帳戶時,`fallback: true` 才會回退到一般帳戶池。
+
+
+### `promptLengthPricing`
+
+選用欄位 `promptLengthPricing` 擴充四項基礎費率。`mode: "automatic"`(或省略欄位)保留目錄的上下文計價規則;`mode: "flat"` 停用這些規則。`mode: "custom"` 使用正安全整數 `thresholdTokens`、值為 `"gt"`(>)或 `"gte"`(≥)的 `comparison`,以及含四項絕對替代費率的 `rates`。門檻計算所有提示詞 token,包括快取讀取與寫入,不包括輸出。所選費率適用於包含輸出的整個請求,並在門檻兩側皆取代目錄上下文規則。Fast/Priority 調整在選定費率後只套用一次,並遵循目錄為該模型公布的 Priority 關係;僅當目錄將該關係宣告為 `lower-bound`(目前為 xAI Grok)時,估算才會標示為下限;沒有目錄規則的模型只套用一次 Priority 倍率。可在 **Models → Price** 中設定。**Reset to automatic** 刪除包含基礎費率與規則的整個覆寫設定。舊設定保持相容;這些設定僅涉及顯示的費用估算。
diff --git a/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md b/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md
index f45782de4b3..6f4ecb50e78 100644
--- a/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md
+++ b/docs-site/src/content/docs/zh-tw/reference/proxy-formats.md
@@ -35,6 +35,8 @@ Chat Completions 與 Messages 端點支援延後提供的工具目錄:用戶
此外,在所有輸入協定中,供應商回傳的 `default.` 命名空間呼叫都會正規化為用戶端宣告的裸名稱,即使延後目錄規則停用了上述拒絕檢查。
+原生 Messages 也會辨識與 `tools: []` 一同提供的系統訊息行內工具定義。這些名稱在被永久移除前仍可呼叫,但不會授權其他用戶端工具。託管 MCP 呼叫和伺服器工具的參數串流保留原生格式。
+
## 端點概覽
| 客戶端介面 | 端點 | 成功的非串流結果 | 成功的串流或 socket 結果 |
@@ -237,8 +239,14 @@ Compaction 為需要縮短長 Responses 對話的客戶端回傳取代歷史。
| 路由型別 | 行為 |
| --- | --- |
| 規範 ChatGPT 或官方 OpenAI 路由 | 以解析的帳號與模型認證將請求轉發到原生 `/responses/compact` 端點 |
+| Explicitly opted-in custom `openai-responses` gateway | `supportsNativeCompactEndpoint: true` tries the native v1 endpoint for key/local/forward auth using the provider's normal Responses transport; HTTP 404 uses portable fallback; OAuth retains routed compaction and credential resolution |
| 其他路由模型 | 執行一個內部、非串流、無工具的 compaction 回合,帶有 `compaction_trigger`;需要恰好一個合成的 `compaction` 項目,其 `encrypted_content` 為 `ocx1:` 封裝;將該摘要解碼為 v1 取代歷史 |
+The custom v1 capability is independent of v2 `supportsNativeCompactionTrigger` and
+`decodesNativeCompactionBlobs` for replay. None grants canonical identity or caller credential
+forwarding; cross-provider overrides remain portable. See the canonical
+[provider configuration](/reference/configuration/providers/) before opting in.
+
原生 compact 回應以 32 MiB 上限緩衝,包含其宣告的 `Content-Length` 已超過限制的回應。Compact 專屬失敗包含:
| 狀態 | 型別或代碼 | 意義 |
@@ -300,3 +308,11 @@ Anthropic 來源的失敗以 Anthropic 的錯誤封裝渲染,因此該方言
重放的推理項攜帶的 `encrypted_content` 只有產生它的供應商與憑證才能讀取。若 opencodex 知道該對話上一次由另一個供應商處理,它會在送出前移除這個 blob,並保留該項的摘要。若那個供應商還使用了不同的 endpoint 或憑證,該項的 `rs_…` id 也會被移除,因為它指向新目標查不到的項目。若 opencodex 無從得知,例如代理重新啟動之後,新目標會拒絕這個 blob:OpenAI 與 Azure OpenAI 回傳 `400 invalid_encrypted_content`。此時 opencodex 會去掉上一個供應商的推理狀態(blob 與 `rs_…` id)後只重送一次請求;保留 id 會導致 `Item with id 'rs_…' not found`。
這項復原適用於所有使用 Responses 協定的 adapter,因此 `openai-responses` 與 `azure-openai` 的行為相同。復原成功後,該對話在同一目標上的後續輪次會在接下來五分鐘內於首次送出前移除這些狀態。重送計入請求的一般傳送預算。一般的 400 與 429 不會以這種方式重送,5xx 也不會,只有一個狹窄的例外:對於攜帶加密工具輸出的請求,回應本文恰好是該解密拒絕的 502 會得到同樣的一次重送。第二次拒絕會原樣回傳給客戶端。遇到這種情況,請在目標供應商上開始新的對話。
+
+## Codex 原生一般 Lite 請求的搜尋歷史
+
+一般請求預設保留結構化 hosted-search 歷史,Responses Lite 請求也一樣。若要選用有損參考說明,請在 `config.json` 中設定 `providers.openai.projectResponsesLiteHostedSearchHistory: true`(必須是布林值,不能是字串 `"true"`)。此設定按供應商配置且預設關閉;省略或設為 `false` 時保留結構化歷史。即使已啟用,也只有最終目的地精確為標準 ChatGPT Codex `/responses` 且目前有效意圖明確選擇 Responses Lite 的一般請求,才會將未被同一輸入引用的剩餘 `web_search_call` 歷史轉為附標籤的 assistant 參考說明。最終 `x-openai-internal-codex-responses-lite` 標頭值精確為 `true` 時選擇 Lite;明確的 `false` 優先於訊框中繼資料。沒有可辨識的標頭值時,採用有效字串 map 中 `client_metadata.ws_request_header_x_openai_internal_codex_responses_lite` 的 `true` 值,沿用既有 WS 中繼資料語意。非 Lite 或無法辨識的有效意圖保留結構化歷史;格式無效的中繼資料不會覆寫明確的 HTTP `true`。模型名稱、訂閱認證、缺少 hosted 宣告或僅有 `additional_tools` 都不會選擇此策略;不會為觸發轉換而改寫標頭或中繼資料。啟用後適用於新對話、分支、跨模型接續、快取未命中或重新啟動,且不依賴診斷設定。只有有效、同範圍的 bridge memo 能證明並先還原原始函式呼叫及結果;ID 前綴、模型名稱或缺少 memo 不能判定來源。同一輸入的 `item_reference.id` 精確指向某 hosted 項目時,該項目會保留;`store:false` 會先移除引用,剩餘 hosted 歷史可被投影。工具宣告、Lite `additional_tools`、選擇、用戶端呼叫/結果配對、推理/原生密文、非 hosted 順序及呼叫端原始歷史均維持既有契約。
+
+這是只在上述明確啟用的 Lite 邊界內採用的**有損歷史策略**,不是 hosted schema 無效或所有上游錯誤都已修復的斷言。說明是非可信的 status/action 參考中繼資料,只複製 action 與頂層 sources 中允許的 URL/title/type,並非指令或網頁內文。每個字串最多 2,048 個 UTF-16 單位、每份清單 20 項、所有投影說明合計 65,536 個序列化位元組,超出後改用省略說明。已轉換項目的 ID 和未知欄位會被捨棄。HTTP/SSE 與 WS 共用一般請求的最終 builder。
+
+公開 OpenAI API 與其他目的地不新增一般或 native-compact 投影。原生 `/responses/compact` 保留既有端點、帳號清理、官方不透明輸出路徑及結構化 hosted 歷史。Portable/routed 摘要沿用既有投影觸發條件與預設引用行為;共用 renderer 在這些既有路徑也保留允許的頂層 sources。不會新增重試、來源分類或 compaction summarizer。
diff --git a/gui/src/advisor-models.ts b/gui/src/advisor-models.ts
new file mode 100644
index 00000000000..7ff4ca2f2e8
--- /dev/null
+++ b/gui/src/advisor-models.ts
@@ -0,0 +1,11 @@
+export interface AdvisorModelRow {
+ namespaced: string;
+ disabled?: boolean;
+ initialSelectionPending?: boolean;
+}
+
+/** Keep routing selectors opaque: labels and model display names are not model IDs. */
+export function advisorModelValues(rows: readonly AdvisorModelRow[]): string[] {
+ return [...new Set(rows.filter(row => !row.disabled && !row.initialSelectionPending)
+ .map(row => row.namespaced))];
+}
diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts
index 195504dd223..51ec534ee3c 100644
--- a/gui/src/app-routing.ts
+++ b/gui/src/app-routing.ts
@@ -131,6 +131,7 @@ export const INTEGRATION_TAB_HASHES = [
"integrations/cursor",
"integrations/opencode",
"integrations/pi",
+ "integrations/gentle-shell",
"integrations/omp",
"integrations/hermes",
"integrations/openclaw",
diff --git a/gui/src/codex-quota-utils.ts b/gui/src/codex-quota-utils.ts
index 57feab153f0..a19247d8580 100644
--- a/gui/src/codex-quota-utils.ts
+++ b/gui/src/codex-quota-utils.ts
@@ -13,7 +13,7 @@ export interface AccountQuota {
shortObservedAt?: number;
shortWindowSeconds?: number;
monthlyResetAt?: number;
- customWindows?: { label: string; percent: number; resetAt?: number }[];
+ customWindows?: { label: string; percent: number; resetAt?: number; modelId?: string }[];
creditsUsd?: {
used: number;
limit: number;
diff --git a/gui/src/combo-capabilities.ts b/gui/src/combo-capabilities.ts
index 32f91f66b98..587a4d631e5 100644
--- a/gui/src/combo-capabilities.ts
+++ b/gui/src/combo-capabilities.ts
@@ -1,14 +1,74 @@
import type { ComboTarget } from "./combo-workspace-data";
import type { ModelOption } from "./components/combo-workspace-types";
-/** Whether every selected target advertises image input (incomplete rows fail closed). */
+type ComboImageMemberKind = "vision" | "sidecar" | "blocked" | "missing";
+
+/**
+ * One combo member's image story. Classification must survive a reload, so it
+ * reads `visionSidecarConsumer` — the runtime predicate's verdict served on the
+ * /api/models row — before falling back to the DECLARED modalities
+ * (`inputModalitiesDeclared`). A model covered only by legacy `noVisionModels`
+ * has no declaration and its advertised modalities are widened to image, so the
+ * flag is what keeps it a sidecar member after reload. Rows with no known
+ * modalities, or modalities without text, cannot be covered and block the combo.
+ */
+function imageMemberKind(target: ComboTarget, models: ModelOption[]): ComboImageMemberKind {
+ const provider = target.provider.trim();
+ const modelId = target.model.trim();
+ if (!provider || !modelId) return "missing";
+ const model = models.find((row) => row.provider === provider && row.id === modelId);
+ if (!model) return "missing";
+ if (model.visionSidecarConsumer === true) return "sidecar";
+ const declared = model.inputModalitiesDeclared ?? model.inputModalities;
+ if (!declared || declared.length === 0) return "blocked";
+ if (declared.includes("image")) return "vision";
+ return declared.includes("text") ? "sidecar" : "blocked";
+}
+
+/** Whether images can be enabled: every target is known and either images natively or can be declared text-only. */
export function comboImagesSupported(targets: ComboTarget[], models: ModelOption[]): boolean {
if (targets.length === 0) return false;
return targets.every((target) => {
- const provider = target.provider.trim();
- const modelId = target.model.trim();
- if (!provider || !modelId) return false;
- const model = models.find((row) => row.provider === provider && row.id === modelId);
- return !!model?.inputModalities?.includes("image");
+ const kind = imageMemberKind(target, models);
+ return kind === "vision" || kind === "sidecar";
});
}
+
+/**
+ * Exact targets that need a text-only declaration so the Vision Sidecar covers
+ * them when the combo accepts images. Deduplicated in submission order. Empty
+ * while any member blocks images: the switch is forced off then, so a save must
+ * not declare the remaining members text-only behind the operator's back.
+ */
+export function comboVisionSidecarTargets(
+ targets: ComboTarget[],
+ models: ModelOption[],
+): Array<{ provider: string; model: string }> {
+ if (!comboImagesSupported(targets, models)) return [];
+ const seen = new Set();
+ const out: Array<{ provider: string; model: string }> = [];
+ for (const target of targets) {
+ if (imageMemberKind(target, models) !== "sidecar") continue;
+ const provider = target.provider.trim();
+ const model = target.model.trim();
+ const key = `${provider}/${model}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push({ provider, model });
+ }
+ return out;
+}
+
+/**
+ * Exact targets that cannot be covered by the Vision Sidecar: their row is
+ * known but its modalities are unknown or have no text input. Named in the
+ * hint so the operator knows which member blocks the image switch.
+ */
+export function comboImageBlockedTargets(
+ targets: ComboTarget[],
+ models: ModelOption[],
+): Array<{ provider: string; model: string }> {
+ return targets
+ .filter((target) => imageMemberKind(target, models) === "blocked")
+ .map((target) => ({ provider: target.provider.trim(), model: target.model.trim() }));
+}
diff --git a/gui/src/combo-workspace-data.ts b/gui/src/combo-workspace-data.ts
index f579a6f2b28..88c9b9b0528 100644
--- a/gui/src/combo-workspace-data.ts
+++ b/gui/src/combo-workspace-data.ts
@@ -504,9 +504,13 @@ export function draftEquals(a: ComboItem, b: ComboItem): boolean {
});
}
-export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {}): {
+export function toPutBody(
+ item: ComboItem,
+ options: { renameFrom?: string; visionSidecarTargets?: Array<{ provider: string; model: string }> } = {},
+): {
id: string;
renameFrom?: string;
+ visionSidecarTargets?: Array<{ provider: string; model: string }>;
combo: {
targets: ComboTarget[];
strategy: ComboStrategy;
@@ -526,6 +530,7 @@ export function toPutBody(item: ComboItem, options: { renameFrom?: string } = {}
return {
id: item.id.trim(),
...(options.renameFrom ? { renameFrom: options.renameFrom } : {}),
+ ...(options.visionSidecarTargets?.length ? { visionSidecarTargets: options.visionSidecarTargets } : {}),
combo: {
targets: item.targets.map((target) => ({
provider: target.provider.trim(),
diff --git a/gui/src/components/AdvisorModelSelect.tsx b/gui/src/components/AdvisorModelSelect.tsx
new file mode 100644
index 00000000000..75d7a32f635
--- /dev/null
+++ b/gui/src/components/AdvisorModelSelect.tsx
@@ -0,0 +1,59 @@
+import { useDataSurface } from "../data-surface";
+import { useT } from "../i18n/shared";
+import { Notice, Select } from "../ui";
+import { advisorModelValues, type AdvisorModelRow } from "../advisor-models";
+
+export function AdvisorModelSelect({ apiBase, value, disabled, onChange }: {
+ apiBase: string;
+ value: string;
+ disabled: boolean;
+ onChange: (value: string) => void;
+}) {
+ const t = useT();
+ const resource = useDataSurface(
+ `advisor-models:${apiBase}`,
+ [apiBase],
+ async signal => {
+ const response = await fetch(`${apiBase}/api/models`, { signal });
+ if (!response.ok) throw new Error(String(response.status));
+ const rows: unknown = await response.json();
+ if (!Array.isArray(rows) || !rows.every(row => row && typeof row.namespaced === "string" && row.namespaced.length > 0)) {
+ throw new Error("Invalid model catalog");
+ }
+ return rows as AdvisorModelRow[];
+ },
+ { isEmpty: rows => rows.length === 0 },
+ );
+ const { state } = resource;
+ const values = advisorModelValues(state.data ?? []);
+ const unavailable = value !== "" && !values.includes(value);
+ const options = [
+ { value: "", label: t("advisor.modelClear") },
+ ...(unavailable ? [{ value, label: value }] : []),
+ ...values.map(model => ({ value: model, label: model })),
+ ];
+ return (
+