@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.
- package/README.md +1 -1
- package/bin/ocx.mjs +4 -0
- package/gui/dist/assets/index-BNVYzdn0.css +1 -0
- package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +94 -5
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google.ts +24 -7
- package/src/adapters/openai-chat.ts +233 -305
- package/src/adapters/openai-responses.ts +62 -0
- package/src/adapters/tool-catalog-nudge.ts +2 -11
- package/src/claude/outbound.ts +42 -14
- package/src/cli/claude.ts +61 -9
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +8 -1
- package/src/cli/config-command.ts +4 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +15 -0
- package/src/cli/index.ts +27 -11
- package/src/cli/lab.ts +367 -0
- package/src/cli/provider.ts +9 -1
- package/src/clients/config-export.ts +100 -3
- package/src/codex/app-server-processes.ts +11 -8
- package/src/codex/auth-api.ts +92 -28
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/catalog/metadata.ts +7 -3
- package/src/codex/catalog/provider-fetch.ts +191 -47
- package/src/codex/catalog.ts +2 -2
- package/src/codex/history-job.ts +83 -18
- package/src/codex/history-migration-guardian.ts +9 -16
- package/src/codex/history-provider.ts +178 -1
- package/src/codex/history-transition.ts +5 -5
- package/src/codex/history-worker.ts +22 -2
- package/src/codex/native-profile-store.ts +1 -1
- package/src/codex/plan.ts +15 -0
- package/src/codex/quota.ts +6 -6
- package/src/codex/routing.ts +3 -3
- package/src/codex/subagent-model-fallback.ts +246 -22
- package/src/codex/user-identity.ts +63 -12
- package/src/config.ts +265 -32
- package/src/images/loop.ts +3 -1
- package/src/images/plan.ts +23 -13
- package/src/integrations/omp-yaml-source.ts +225 -0
- package/src/integrations/ownership.ts +5 -5
- package/src/integrations/state.ts +45 -6
- package/src/integrations/writer.ts +38 -3
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +24 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +160 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +57 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +191 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +228 -0
- package/src/lab/events/validate.ts +780 -0
- package/src/lab/index.ts +36 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +241 -0
- package/src/lab/ledger/store.ts +263 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +311 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +116 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +344 -0
- package/src/lab/observe/from-live.ts +103 -0
- package/src/lab/paths.ts +65 -0
- package/src/lab/projection/rebuild.ts +477 -0
- package/src/lab/projection/schema.ts +122 -0
- package/src/lab/projection/verdicts.ts +438 -0
- package/src/lab/projection/verification.ts +332 -0
- package/src/lab/query/catalog.ts +72 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/index.ts +34 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +81 -0
- package/src/lab/subject/route-subject.ts +73 -0
- package/src/lib/bounded-body.ts +104 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +45 -0
- package/src/lib/pinned-http.ts +36 -7
- package/src/oauth/index.ts +20 -1
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/login-cli.ts +38 -4
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/codex-capacity.ts +7 -4
- package/src/providers/command-code-efforts.ts +5 -2
- package/src/providers/derive.ts +105 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/openai-tiers.ts +59 -3
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/quota.ts +67 -21
- package/src/providers/registry.ts +118 -5
- package/src/router.ts +15 -1
- package/src/routing/analytics.ts +8 -7
- package/src/routing/evaluator.ts +51 -5
- package/src/routing/profile.ts +10 -1
- package/src/routing/trace.ts +42 -6
- package/src/server/auth-cors.ts +15 -0
- package/src/server/background-lifecycle.ts +169 -0
- package/src/server/chat-completions.ts +18 -3
- package/src/server/claude-messages.ts +1 -0
- package/src/server/images.ts +78 -11
- package/src/server/index.ts +72 -18
- package/src/server/lifecycle.ts +6 -0
- package/src/server/live.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +41 -24
- package/src/server/management/body.ts +6 -0
- package/src/server/management/config-routes.ts +51 -8
- package/src/server/management/lab-routes.ts +388 -0
- package/src/server/management/logs-usage-routes.ts +19 -1
- package/src/server/management/model-rows.ts +25 -11
- package/src/server/management/native-integration-routes.ts +3 -3
- package/src/server/management/provider-routes.ts +5 -0
- package/src/server/management/shared.ts +8 -1
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management/vision-sidecar-options.ts +125 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/request-decompress.ts +106 -5
- package/src/server/responses/collaboration.ts +4 -1
- package/src/server/responses/core.ts +117 -31
- package/src/server/responses-json-events.ts +50 -12
- package/src/server/search.ts +23 -7
- package/src/server/ws-bridge.ts +3 -1
- package/src/service.ts +40 -20
- package/src/types.ts +74 -0
- package/src/update/job.ts +10 -2
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +9 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/describe.ts +2 -2
- package/src/vision/eligibility.ts +229 -0
- package/src/vision/index.ts +20 -2
- package/src/web-search/executor.ts +2 -2
- package/src/web-search/index.ts +4 -2
- package/src/web-search/loop.ts +3 -1
- package/src/web-search/parse.ts +72 -3
- package/gui/dist/assets/index-BA1dgo4q.css +0 -1
- package/gui/dist/assets/index-DDZpgzKk.js +0 -70
package/src/server/search.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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(
|
|
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
|
}
|
package/src/server/ws-bridge.ts
CHANGED
|
@@ -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
|
|
2458
|
-
*
|
|
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
|
-
|
|
2462
|
-
|
|
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 (
|
|
2486
|
+
} else if (platform === "win32") {
|
|
2467
2487
|
let removed = false;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
}
|
|
2472
|
-
if (
|
|
2473
|
-
|
|
2474
|
-
|
|
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 (
|
|
2481
|
-
|
|
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
|
|
1138
|
-
|
|
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
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
let windowBytes = Math.min(
|
|
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(
|
|
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 >=
|
|
682
|
-
|
|
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 {
|