@bitkyc08/opencodex 2.11.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -21,6 +21,7 @@ import {
21
21
  import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
22
22
  import { formatCodexProviderForLog } from "../codex/routing";
23
23
  import { signalWithTimeout } from "../lib/abort";
24
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
24
25
  import { sidecarEnter } from "../lib/sidecar-tracker";
25
26
  import type { OcxConfig } from "../types";
26
27
  import {
@@ -44,7 +45,7 @@ import { codexAccountSelectionForTurn } from "./lifecycle";
44
45
  * long-running search).
45
46
  */
46
47
  const SEARCH_UPSTREAM_TIMEOUT_MS = 200_000;
47
- const SEARCH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
48
+ export const SEARCH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
48
49
 
49
50
  export async function handleSearch(
50
51
  req: Request,
@@ -144,27 +145,36 @@ export async function handleSearch(
144
145
  const timeoutMs = config.search?.timeoutMs ?? SEARCH_UPSTREAM_TIMEOUT_MS;
145
146
  const linkedSignal = signalWithTimeout(timeoutMs, req.signal);
146
147
  const sidecarExit = sidecarEnter("search");
148
+ let upstreamResponse: Response | undefined;
147
149
  try {
148
- const upstreamResponse = await fetch(url, {
150
+ upstreamResponse = await fetch(url, {
149
151
  method: "POST",
150
152
  headers,
151
153
  body: JSON.stringify(relayBody),
152
154
  signal: linkedSignal.signal,
153
155
  });
154
- const payload = await upstreamResponse.arrayBuffer();
155
- if (payload.byteLength > SEARCH_RESPONSE_MAX_BYTES) {
156
- return formatErrorResponse(502, "upstream_error", `search response too large (${payload.byteLength} bytes)`);
156
+ const observed = await readBoundedResponseBytes(upstreamResponse, {
157
+ maxBytes: SEARCH_RESPONSE_MAX_BYTES,
158
+ signal: linkedSignal.signal,
159
+ });
160
+ if (observed.oversized) {
161
+ upstream.recordOutcome?.(upstreamResponse.status);
162
+ return formatErrorResponse(
163
+ 502,
164
+ "upstream_error",
165
+ `search response too large (exceeded ${SEARCH_RESPONSE_MAX_BYTES} bytes)`,
166
+ );
157
167
  }
158
168
  upstream.recordOutcome?.(upstreamResponse.status);
159
169
  const relayHeaders: Record<string, string> = {};
160
170
  const contentType = upstreamResponse.headers.get("content-type");
161
171
  if (contentType) relayHeaders["content-type"] = contentType;
162
- return new Response(payload, { status: upstreamResponse.status, headers: relayHeaders });
172
+ return new Response(observed.bytes, { status: upstreamResponse.status, headers: relayHeaders });
163
173
  } catch (err) {
164
174
  if (req.signal.aborted) {
165
175
  return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
166
176
  }
167
- if (err instanceof Error && err.name === "TimeoutError") {
177
+ if (linkedSignal.signal.aborted || (err instanceof Error && err.name === "TimeoutError")) {
168
178
  upstream.recordOutcome?.("timeout");
169
179
  return formatErrorResponse(504, "upstream_error", "search upstream timed out");
170
180
  }
@@ -177,5 +187,11 @@ export async function handleSearch(
177
187
  } finally {
178
188
  sidecarExit();
179
189
  linkedSignal.cleanup();
190
+ // A response that aborted before the reader attached still owns its upstream socket.
191
+ // Consumed/cancelled bodies are already closed; locked bodies remain owned by the reader.
192
+ const pendingBody = upstreamResponse?.body;
193
+ if (pendingBody && !pendingBody.locked) {
194
+ try { void pendingBody.cancel().catch(() => undefined); } catch { /* already closed */ }
195
+ }
180
196
  }
181
197
  }
@@ -40,6 +40,8 @@ export interface WsData {
40
40
  liveUpstreamUrl?: string;
41
41
  liveUpstreamHeaders?: Record<string, string>;
42
42
  livePending?: Array<string | Buffer>;
43
+ /** Total encoded bytes retained in livePending while the upstream connects. */
44
+ livePendingBytes?: number;
43
45
  liveOpened?: boolean;
44
46
  /** Once teardown starts, ignore new client frames until the upstream closes. */
45
47
  liveClosing?: boolean;
@@ -467,4 +469,4 @@ export async function readBoundedPrefix(
467
469
  export function looksLikeSse(prefix: Uint8Array): boolean {
468
470
  const text = new TextDecoder().decode(prefix);
469
471
  return /^\s*(event:|data:)/.test(text);
470
- }
472
+ }
package/src/service.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  type ElevatedSchtasksCreateAndRunExecution,
34
34
  type ElevatedSchtasksCreateAndRunResult,
35
35
  } from "./lib/windows-elevation";
36
- import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, winswXmlPath, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION } from "./lib/winsw";
36
+ import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, winswXmlPath, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION, type WinswStatus } from "./lib/winsw";
37
37
  import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
38
38
  import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
39
39
  import { recordOwnedConfigPath } from "./lib/config-ownership";
@@ -2452,33 +2452,53 @@ function removeServiceInstallState(): void {
2452
2452
  }
2453
2453
  }
2454
2454
 
2455
+ type UninstallServiceHooksForTests = {
2456
+ platform: typeof process.platform;
2457
+ assertEnvironment: () => void;
2458
+ probeWindowsTask: () => WindowsSchedulerTaskProbe;
2459
+ uninstallWindowsTask: () => void;
2460
+ nativeStatus: () => WinswStatus;
2461
+ uninstallNative: () => void;
2462
+ removeInstallState: () => void;
2463
+ };
2464
+
2465
+ let uninstallServiceHooksForTests: UninstallServiceHooksForTests | null = null;
2466
+
2467
+ /** Test-only hooks for full-uninstall service removal. */
2468
+ export function setUninstallServiceHooksForTests(hooks: UninstallServiceHooksForTests | null): void {
2469
+ uninstallServiceHooksForTests = hooks;
2470
+ }
2471
+
2455
2472
  /**
2456
2473
  * Best-effort service removal for full uninstall. Unlike `ocx service uninstall`, this is quiet
2457
- * when no service exists and never exits the process just because the platform has no service
2458
- * manager.
2474
+ * when no service exists or the platform has no service manager. An installed native Windows
2475
+ * service or scheduler task that cannot be removed throws so the caller cannot erase state and
2476
+ * report success.
2459
2477
  */
2460
2478
  export function uninstallServiceIfInstalled(): boolean {
2461
- assertServiceEnvironmentMatchesInstall();
2462
- if (process.platform === "darwin") {
2479
+ const hooks = uninstallServiceHooksForTests;
2480
+ (hooks?.assertEnvironment ?? assertServiceEnvironmentMatchesInstall)();
2481
+ const platform = hooks?.platform ?? process.platform;
2482
+ if (platform === "darwin") {
2463
2483
  if (existsSync(plistPath())) {
2464
2484
  try { uninstallLaunchd(); removeServiceInstallState(); return true; } catch { return false; }
2465
2485
  }
2466
- } else if (process.platform === "win32") {
2486
+ } else if (platform === "win32") {
2467
2487
  let removed = false;
2468
- try {
2469
- const q = schtasks(["/query", "/tn", TASK]);
2470
- if (q.includes(TASK)) { uninstallWindows(); removed = true; }
2471
- } catch { /* task not found */ }
2472
- if (statusWinswRaw() !== "nonexistent") {
2473
- try {
2474
- uninstallWinswService();
2475
- removed = true;
2476
- } catch (err) {
2477
- console.warn(`⚠️ Failed to remove native service: ${err instanceof Error ? err.message : String(err)}. Check 'sc.exe query ${WINSW_SERVICE_ID}'.`);
2478
- }
2488
+ const scheduler = (hooks?.probeWindowsTask ?? probeWindowsSchedulerTask)();
2489
+ if (scheduler.status === "unknown") {
2490
+ throw new Error(`Could not determine Task Scheduler state: ${scheduler.detail}`);
2491
+ }
2492
+ if (scheduler.status === "present") {
2493
+ (hooks?.uninstallWindowsTask ?? uninstallWindows)();
2494
+ removed = true;
2479
2495
  }
2480
- if (removed) { removeServiceInstallState(); return true; }
2481
- } else if (process.platform === "linux" && existsSync(unitPath())) {
2496
+ if ((hooks?.nativeStatus ?? statusWinswRaw)() !== "nonexistent") {
2497
+ (hooks?.uninstallNative ?? uninstallWinswService)();
2498
+ removed = true;
2499
+ }
2500
+ if (removed) { (hooks?.removeInstallState ?? removeServiceInstallState)(); return true; }
2501
+ } else if (platform === "linux" && existsSync(unitPath())) {
2482
2502
  try { uninstallSystemd(); removeServiceInstallState(); return true; } catch {
2483
2503
  try { unlinkSync(unitPath()); removeServiceInstallState(); return true; } catch { return false; }
2484
2504
  }
@@ -2873,4 +2893,4 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2873
2893
  console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
2874
2894
  process.exit(1);
2875
2895
  }
2876
- }
2896
+ }
package/src/types.ts CHANGED
@@ -223,6 +223,19 @@ export function isAllowedToolChoice(value: OcxToolChoice | undefined): value is
223
223
  return typeof value === "object" && value !== null && "allowedTools" in value;
224
224
  }
225
225
 
226
+ /** Compile the request's tool-choice policy into a reusable advertisement/restoration predicate. */
227
+ export function toolChoiceToolPredicate(
228
+ choice: OcxToolChoice | undefined,
229
+ ): (tool: Pick<OcxTool, "namespace" | "name">) => boolean {
230
+ if (!choice || choice === "auto" || choice === "required") return () => true;
231
+ if (choice === "none") return () => false;
232
+ if (isAllowedToolChoice(choice)) {
233
+ const allowed = new Set(choice.allowedTools);
234
+ return tool => toolAllowedByChoice(tool, allowed);
235
+ }
236
+ return tool => toolChoiceAliases(tool).includes(choice.name);
237
+ }
238
+
226
239
  export interface OcxRequestOptions {
227
240
  maxOutputTokens?: number;
228
241
  temperature?: number;
@@ -605,6 +618,17 @@ export interface OcxConfig {
605
618
  * turn to the next available entry before routing.
606
619
  */
607
620
  subagentModelFallback?: string[];
621
+ /**
622
+ * Per-primary-model fallback chains for spawned sub-agents, keyed by the
623
+ * requested primary model id (bare native or "provider/model"). Entries for
624
+ * the matching key are consulted after the requested model and before the
625
+ * global `subagentModelFallback` list.
626
+ *
627
+ * This is the supported home for per-role fallback metadata: storing it as
628
+ * `model_fallback` inside `$CODEX_HOME/agents/*.toml` makes Codex >= 0.146
629
+ * reject the whole role file as an unknown field (#1190).
630
+ */
631
+ subagentModelFallbackByModel?: Record<string, string[]>;
608
632
  /**
609
633
  * TTL (ms) for cached sub-agent model availability probes. Default 60_000.
610
634
  */
@@ -944,9 +968,24 @@ export interface OcxRoutingProfileOptimize {
944
968
  quota?: number;
945
969
  }
946
970
 
971
+ /**
972
+ * Policy for the hard cost ceiling when a candidate has no finite cost
973
+ * estimate. `"allow"` (default) preserves the documented dry-run contract:
974
+ * the cap only excludes evidence known to exceed it, and the candidate's
975
+ * `cost.capOutcome` is `"unknown-allowed"`. `"exclude"` makes the ceiling
976
+ * fail-closed (`cost-limit-unknown` + `capOutcome: "unknown-excluded"`).
977
+ */
978
+ export type OcxRoutingUnknownCostCapMode = "allow" | "exclude";
979
+
947
980
  export interface OcxRoutingProfileLimits {
948
981
  /** Hard per-request estimated-cost ceiling in USD. */
949
982
  maxEstimatedCostUsd?: number;
983
+ /**
984
+ * How `maxEstimatedCostUsd` behaves when the estimate is unknown.
985
+ * Defaults to `"allow"` (eligible + `cost.capOutcome: "unknown-allowed"`);
986
+ * opt in to `"exclude"` for a true hard ceiling.
987
+ */
988
+ onUnknownCost?: OcxRoutingUnknownCostCapMode;
950
989
  }
951
990
 
952
991
  export interface OcxRoutingProfileUnknownEvidence {
@@ -1117,6 +1156,18 @@ export interface RateLimitRetryPolicy {
1117
1156
  respectRetryAfter?: boolean;
1118
1157
  }
1119
1158
 
1159
+ /**
1160
+ * User-configured display price for one model (USD per 1M tokens).
1161
+ * Mirrors the `Cost4` shape used by the usage cost estimator; structurally
1162
+ * compatible so config rows can be lifted directly into price overlays.
1163
+ */
1164
+ export interface ProviderCostOverlay {
1165
+ input: number;
1166
+ output: number;
1167
+ cacheRead: number;
1168
+ cacheWrite: number;
1169
+ }
1170
+
1120
1171
  /**
1121
1172
  * One configured provider entry. `authMode` (default `"key"`) decides whether same-target 429
1122
1173
  * retries are allowed; OAuth/forward credentials and local runtimes are never replayed.
@@ -1158,6 +1209,12 @@ export interface OcxProviderConfig {
1158
1209
  * forwarded to an upstream that cannot resolve their pair.
1159
1210
  */
1160
1211
  statelessResponses?: boolean;
1212
+ /**
1213
+ * Responses upstream whose parser requires each tool result to immediately follow
1214
+ * its matching call. When enabled, only unambiguous matched pairs are reordered;
1215
+ * intervening messages are preserved after the result instead of being dropped.
1216
+ */
1217
+ requiresAdjacentResponsesToolResults?: boolean;
1161
1218
  /**
1162
1219
  * Whether this provider's Responses route honours the OpenAI `service_tier`
1163
1220
  * parameter. Tri-state: `true` lets fast mode inject/remove the field (an unset
@@ -1233,6 +1290,15 @@ export interface OcxProviderConfig {
1233
1290
  defaultMaxOutputTokens?: number;
1234
1291
  /** Model-specific fallback output token budgets. Exact/model-pattern entries beat the provider default. */
1235
1292
  modelMaxOutputTokens?: Record<string, number>;
1293
+ /**
1294
+ * Per-model display prices (USD per 1M tokens) keyed by exact model id —
1295
+ * opencode-style per-model pricing in ocx's flat `modelXxx` convention:
1296
+ * `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`.
1297
+ * User-configured prices win over the built-in jawcode/expected catalogs in
1298
+ * the Logs `~$` estimate. Display-time estimation only; never billing. An
1299
+ * all-zero entry means "not billable here" and falls through to the catalogs.
1300
+ */
1301
+ modelCosts?: Record<string, ProviderCostOverlay>;
1236
1302
  headers?: Record<string, string>;
1237
1303
  /** Default provider-routing preferences for models sent through the canonical OpenRouter API. */
1238
1304
  openRouterRouting?: OpenRouterProviderRouting;
@@ -1336,6 +1402,14 @@ export interface OcxProviderConfig {
1336
1402
  autoToolChoiceOnlyModels?: string[];
1337
1403
  /** Model ids that expect prior assistant `reasoning_content` to be preserved in chat history. */
1338
1404
  preserveReasoningContentModels?: string[];
1405
+ /**
1406
+ * Model ids whose upstream hard-rejects a tool_call continuation missing
1407
+ * `reasoning_content` (DeepSeek thinking mode: HTTP 400). When the replay
1408
+ * cache misses, the adapter injects a minimal placeholder for these models.
1409
+ * Defaults to `preserveReasoningContentModels` when unset; set `[]` to opt
1410
+ * out explicitly (e.g. MiniMax, where low effort disables thinking).
1411
+ */
1412
+ requiresReasoningPlaceholderModels?: string[];
1339
1413
  /**
1340
1414
  * Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only,
1341
1415
  * openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays
package/src/update/job.ts CHANGED
@@ -1134,8 +1134,16 @@ async function restartAfterUpdate(
1134
1134
  `Port ${port} still busy after ${Math.trunc(RESTART_PORT_RECLAIM_MS / 1000)}s; refusing to hop — reinstall may fail until the port is free.`
1135
1135
  + ` ${formatPortHolders(port, listPids, verifyOcx, preServiceAllow)}`,
1136
1136
  );
1137
- const liveAfter = listPids(port).filter(pid => pid !== process.pid && aliveFn(pid));
1138
- if (liveAfter.length === 0) {
1137
+ const liveScan: ListenPidScan = io.scanListenPidsFn
1138
+ ? io.scanListenPidsFn(port)
1139
+ : io.listListenPidsFn
1140
+ // Test seam: an injected list represents a successful scan.
1141
+ ? { ok: true, pids: io.listListenPidsFn(port) }
1142
+ : scanListenPids(port);
1143
+ const liveAfter = liveScan.ok
1144
+ ? liveScan.pids.filter(pid => pid !== process.pid && aliveFn(pid))
1145
+ : null;
1146
+ if (liveAfter !== null && liveAfter.length === 0) {
1139
1147
  // Non-elevated `service install` will UAC-fail anyway; skip straight to
1140
1148
  // the direct-start fallthrough instead of burning another minute on it.
1141
1149
  updateJob(job, {}, "Skipping service reinstall after reclaim timeout with no live holders; falling back to a direct proxy start.");
package/src/usage/cost.ts CHANGED
Binary file
@@ -18,6 +18,15 @@ export interface Cost4 {
18
18
  cacheWrite: number;
19
19
  }
20
20
 
21
+ /**
22
+ * Upper bound for a user-configured USD-per-1M-token rate. Real prices are far
23
+ * below this (the most expensive published models cost a few hundred USD/M);
24
+ * the bound keeps `rate * tokens / 1e6` finite for any token count a usage log
25
+ * can plausibly hold, so an overlay cannot overflow the estimate to Infinity
26
+ * and serialize cost fields as null.
27
+ */
28
+ export const MAX_COST4_RATE = 1_000_000;
29
+
21
30
  export type ExpectedPriceStatus = "verified" | "verified-derived" | "unverified";
22
31
 
23
32
  export interface ExpectedPriceOverlay {
package/src/usage/log.ts CHANGED
@@ -420,6 +420,7 @@ export type UsageLogRevision = {
420
420
 
421
421
  let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
422
422
  const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
423
+ const RECENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
423
424
  const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
424
425
  const MANAGEMENT_USAGE_MAX_ENTRIES = 500_000;
425
426
  const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
@@ -655,10 +656,10 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
655
656
  fd = openSync(path, "r");
656
657
  const size = fstatSync(fd).size;
657
658
  if (size <= 0) return [];
658
- // Trace-sized rows (up to MAX_TRACE_BYTES, RI-01) need a larger per-row
659
- // budget than the pre-trace ledger; keep expanding until the window covers
660
- // the file start or the whole file so a restart never hydrates nothing.
661
- let windowBytes = Math.min(size, Math.max(64 * 1024, Math.ceil(limit) * 20 * 1024));
659
+ // Trace-sized rows need a larger per-row budget than the pre-trace ledger,
660
+ // but startup hydration must never grow into an unbounded whole-file read.
661
+ const maxWindowBytes = Math.min(size, RECENT_USAGE_MAX_READ_BYTES);
662
+ let windowBytes = Math.min(maxWindowBytes, Math.max(64 * 1024, Math.ceil(limit) * 20 * 1024));
662
663
  while (true) {
663
664
  const start = Math.max(0, size - windowBytes);
664
665
  const buf = Buffer.alloc(size - start);
@@ -667,8 +668,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
667
668
  if (start > 0) {
668
669
  const nl = text.indexOf("\n");
669
670
  if (nl < 0) {
670
- if (start === 0) break;
671
- windowBytes = Math.min(size, windowBytes * 4);
671
+ if (start === 0 || windowBytes >= maxWindowBytes) break;
672
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
672
673
  continue;
673
674
  }
674
675
  text = text.slice(nl + 1);
@@ -678,9 +679,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
678
679
  // or partial lines are filtered out during parsing and we always return the
679
680
  // most recent N valid rows (not N physical lines minus corrupt ones).
680
681
  const entries = parseUsageLines(lines);
681
- if (entries.length >= limit || start === 0 || windowBytes >= size) return entries.slice(-limit);
682
- if (windowBytes >= size) break;
683
- windowBytes = Math.min(size, windowBytes * 4);
682
+ if (entries.length >= limit || start === 0 || windowBytes >= maxWindowBytes) return entries.slice(-limit);
683
+ windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
684
684
  }
685
685
  return [];
686
686
  } catch {