@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
package/src/service.ts
CHANGED
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, execSync, spawnSync } from "node:child_process";
|
|
9
9
|
import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
|
|
10
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { accessSync, chmodSync, constants as fsConstants, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir, tmpdir } from "node:os";
|
|
12
|
-
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
12
|
+
import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
13
13
|
import { expandUserPath, getConfigDir, loadConfig } from "./config";
|
|
14
14
|
import { readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config/process-state";
|
|
15
15
|
import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
|
|
16
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
17
17
|
import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
|
|
18
18
|
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime";
|
|
19
|
-
import type { BunRuntimeSource } from "./lib/bun-runtime";
|
|
19
|
+
import type { BunRuntimeSource, DurableBunRuntime } from "./lib/bun-runtime";
|
|
20
20
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
21
21
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
22
22
|
import { tokenCollidesWithAdmin } from "./lib/admin-secrets";
|
|
@@ -49,23 +49,66 @@ import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from ".
|
|
|
49
49
|
import { recordOwnedConfigPath } from "./lib/config-ownership";
|
|
50
50
|
import { killWindowsSchedulerWrappers } from "./lib/windows-service-wrappers";
|
|
51
51
|
import { maybeShowStarPrompt } from "./cli/star-prompt";
|
|
52
|
+
import { systemdProperty } from "./service-manager-probe";
|
|
52
53
|
|
|
53
54
|
const LABEL = "com.opencodex.proxy";
|
|
54
55
|
const TASK = "opencodex-proxy";
|
|
55
56
|
|
|
56
57
|
export type ServiceBackend = "scheduler" | "native";
|
|
57
58
|
|
|
58
|
-
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
59
|
+
function cliEntry(runtime: DurableBunRuntime = durableBunRuntime()): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
59
60
|
// Bake the bundled Bun (npm global prefix, survives `ocx update`) rather than
|
|
60
61
|
// a transient system Bun, so launchd/systemd/schtasks keep resolving even if a
|
|
61
62
|
// standalone Bun is later removed. The CLI entry lives at src/cli/index.ts.
|
|
62
63
|
//
|
|
63
64
|
// Path and provenance come from ONE resolution so the marker can never describe a
|
|
64
65
|
// different binary than the one actually baked.
|
|
65
|
-
const runtime = durableBunRuntime();
|
|
66
66
|
return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "cli", "index.ts") };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The stable `ocx` launcher to bake into a systemd unit, or null to fall back to the
|
|
71
|
+
* Bun + CLI pair.
|
|
72
|
+
*
|
|
73
|
+
* `cliEntry()` resolves both of its paths from `import.meta.dir`, so they point INSIDE
|
|
74
|
+
* the installed package tree. Under a version manager that tree is a versioned directory:
|
|
75
|
+
* `~/.local/share/mise/installs/npm-opencodex/2.35.0/...`. An upgrade installs 2.36.0 and
|
|
76
|
+
* deletes 2.35.0, after which the unit's `exec <old-bun> <old-cli>` cannot resolve, and
|
|
77
|
+
* `Restart=on-failure` turns that into a restart loop (#2898). The shim in
|
|
78
|
+
* `~/.local/share/mise/shims/ocx` survives the upgrade and dispatches to whatever version
|
|
79
|
+
* is current, so it is the durable thing to name.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately LEXICAL. Resolving the symlink would write the versioned target back into
|
|
82
|
+
* the unit and reintroduce the bug — the indirection is the entire point.
|
|
83
|
+
*
|
|
84
|
+
* Only an absolute path is accepted. A bare `ocx` would be re-resolved through `PATH` on
|
|
85
|
+
* every restart, which turns a service definition into a PATH-hijacking surface; naming
|
|
86
|
+
* one validated absolute file keeps the target fixed at install time.
|
|
87
|
+
*/
|
|
88
|
+
export function stableLauncherEntry(deps: {
|
|
89
|
+
env?: NodeJS.ProcessEnv;
|
|
90
|
+
isExecutableFile?: (path: string) => boolean;
|
|
91
|
+
pathDelimiter?: string;
|
|
92
|
+
} = {}): string | null {
|
|
93
|
+
const env = deps.env ?? process.env;
|
|
94
|
+
const isExecutableFile = deps.isExecutableFile ?? ((path: string): boolean => {
|
|
95
|
+
try {
|
|
96
|
+
if (!statSync(path).isFile()) return false;
|
|
97
|
+
accessSync(path, fsConstants.X_OK);
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const entries = (env.PATH ?? "").split(deps.pathDelimiter ?? delimiter);
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry || !isAbsolute(entry)) continue;
|
|
106
|
+
const candidate = join(entry, "ocx");
|
|
107
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
69
112
|
function plistPath(): string {
|
|
70
113
|
return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
71
114
|
}
|
|
@@ -154,6 +197,14 @@ export interface ServiceInstallState {
|
|
|
154
197
|
/** Baked at install; lets status flag paths gone stale after npm prefix/nvm moves. */
|
|
155
198
|
bunPath?: string;
|
|
156
199
|
cliPath?: string;
|
|
200
|
+
/**
|
|
201
|
+
* Linux only. The stable `ocx` launcher the unit actually invokes, when one was found.
|
|
202
|
+
* Present means `bunPath`/`cliPath` are provenance for the install, NOT what systemd
|
|
203
|
+
* runs — so staleness must be judged against THIS path instead. A version-manager
|
|
204
|
+
* upgrade replaces the directory those two point into while the launcher survives, and
|
|
205
|
+
* checking the old pair would report a stale service that is in fact healthy.
|
|
206
|
+
*/
|
|
207
|
+
launcherPath?: string;
|
|
157
208
|
/** v2: which Windows backend was chosen at install; absent (v1/legacy) means scheduler. */
|
|
158
209
|
backend?: ServiceBackend;
|
|
159
210
|
winswVersion?: string;
|
|
@@ -166,7 +217,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
166
217
|
if (state.version !== 1 && state.version !== 2) return null;
|
|
167
218
|
if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null;
|
|
168
219
|
if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null;
|
|
169
|
-
for (const key of ["bunPath", "cliPath", "winswVersion", "winswSha256"] as const) {
|
|
220
|
+
for (const key of ["bunPath", "cliPath", "launcherPath", "winswVersion", "winswSha256"] as const) {
|
|
170
221
|
if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null;
|
|
171
222
|
}
|
|
172
223
|
if (state.version === 1) {
|
|
@@ -177,7 +228,7 @@ export function parseServiceInstallState(value: unknown): ServiceInstallState |
|
|
|
177
228
|
return state as unknown as ServiceInstallState;
|
|
178
229
|
}
|
|
179
230
|
|
|
180
|
-
function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
|
|
231
|
+
function writeServiceInstallState(backend: ServiceBackend = "scheduler", launcherPath?: string | null): void {
|
|
181
232
|
const { bun, cli } = cliEntry();
|
|
182
233
|
const state: ServiceInstallState = {
|
|
183
234
|
version: 2,
|
|
@@ -185,6 +236,7 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void {
|
|
|
185
236
|
opencodexHome: currentOpenCodexHome(),
|
|
186
237
|
bunPath: bun,
|
|
187
238
|
cliPath: cli,
|
|
239
|
+
...(launcherPath ? { launcherPath } : {}),
|
|
188
240
|
backend,
|
|
189
241
|
...(backend === "native" ? { winswVersion: WINSW_VERSION, winswSha256: WINSW_SHA256 } : {}),
|
|
190
242
|
};
|
|
@@ -500,6 +552,17 @@ function buildServiceShellCommand(bun: string, cli: string, port = resolveServic
|
|
|
500
552
|
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${shellQuote(cli)} start --port ${port}`;
|
|
501
553
|
}
|
|
502
554
|
|
|
555
|
+
/**
|
|
556
|
+
* The same command shape, launched through a stable `ocx` executable instead of an
|
|
557
|
+
* explicit Bun + CLI pair. The token-file preamble is identical and deliberately shared
|
|
558
|
+
* in form: the service still reads the token from disk at start and never carries it in
|
|
559
|
+
* the unit.
|
|
560
|
+
*/
|
|
561
|
+
function buildServiceLauncherShellCommand(launcher: string, port = resolveServiceListenPort()): string {
|
|
562
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
563
|
+
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(launcher)} start --port ${port}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
503
566
|
/**
|
|
504
567
|
* The `--port <n>` actually baked into the installed launchd plist, or null when it
|
|
505
568
|
* cannot be read. macOS only — named for launchd rather than "service" so no caller
|
|
@@ -706,12 +769,6 @@ export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name:
|
|
|
706
769
|
return resolved;
|
|
707
770
|
}
|
|
708
771
|
|
|
709
|
-
function systemdOutputTarget(value: string): string {
|
|
710
|
-
// StandardOutput/StandardError use output specifiers such as append:/path.
|
|
711
|
-
// Quoting the full specifier makes systemd reject it as an invalid output target.
|
|
712
|
-
return value.replace(/%/g, "%%").replace(/\n/g, "\\n");
|
|
713
|
-
}
|
|
714
|
-
|
|
715
772
|
function sh(cmd: string): string {
|
|
716
773
|
return execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
717
774
|
}
|
|
@@ -2512,6 +2569,14 @@ function uninstallWindows(): void {
|
|
|
2512
2569
|
*/
|
|
2513
2570
|
export function bakedServicePathsDiagnostic(): string | null {
|
|
2514
2571
|
const state = readServiceInstallState();
|
|
2572
|
+
// A launcher install runs the launcher, not the baked pair, so the pair's existence says
|
|
2573
|
+
// nothing about whether the service can start. Judging the recorded launcher is both
|
|
2574
|
+
// necessary (a deleted launcher IS stale) and sufficient (a replaced version directory
|
|
2575
|
+
// is not, which is exactly what #2898 made routine).
|
|
2576
|
+
if (state?.launcherPath) {
|
|
2577
|
+
if (existsSync(state.launcherPath)) return null;
|
|
2578
|
+
return `STALE baked paths (missing: ${state.launcherPath}) — run 'ocx service repair' to re-bake`;
|
|
2579
|
+
}
|
|
2515
2580
|
if (!state?.bunPath || !state?.cliPath) return null;
|
|
2516
2581
|
const missing = [state.bunPath, state.cliPath].filter(path => !existsSync(path));
|
|
2517
2582
|
if (missing.length === 0) return null;
|
|
@@ -2532,8 +2597,16 @@ function unitPath(): string {
|
|
|
2532
2597
|
return join(unitDir(), `${TASK}.service`);
|
|
2533
2598
|
}
|
|
2534
2599
|
|
|
2535
|
-
export function buildUnit(
|
|
2536
|
-
|
|
2600
|
+
export function buildUnit(
|
|
2601
|
+
proxyEnv: { name: string; value: string }[] = resolvedProxyEnv(),
|
|
2602
|
+
deps: { launcher?: string | null; runtime?: DurableBunRuntime } = {},
|
|
2603
|
+
): string {
|
|
2604
|
+
const runtime = deps.runtime ?? durableBunRuntime();
|
|
2605
|
+
const { bun, bunRuntimeSource, cli } = cliEntry(runtime);
|
|
2606
|
+
// Discovery belongs to installSystemd(), which resolves once and passes the same value to
|
|
2607
|
+
// both the unit and install state. Keeping this builder explicit makes tests and diagnostics
|
|
2608
|
+
// independent of the host PATH.
|
|
2609
|
+
const launcher = deps.launcher ?? null;
|
|
2537
2610
|
const log = logPath();
|
|
2538
2611
|
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
2539
2612
|
const codexHome = systemdEnvironmentAssignment("CODEX_HOME", process.env.CODEX_HOME?.trim());
|
|
@@ -2541,14 +2614,23 @@ export function buildUnit(proxyEnv: { name: string; value: string }[] = resolved
|
|
|
2541
2614
|
const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
|
|
2542
2615
|
const envLines = [
|
|
2543
2616
|
systemdEnvironmentAssignment("OCX_SERVICE", "1"),
|
|
2544
|
-
|
|
2545
|
-
|
|
2617
|
+
...(launcher ? [] : [
|
|
2618
|
+
systemdEnvironmentAssignment(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource),
|
|
2619
|
+
systemdEnvironmentAssignment(BUN_RUNTIME_PATH_ENV, bun),
|
|
2620
|
+
]),
|
|
2621
|
+
// A launcher normally resolves the current package's bundled Bun after every upgrade.
|
|
2622
|
+
// Preserve only a proof-bound shell override; otherwise writing a package-local path here
|
|
2623
|
+
// would recreate the version-manager pin that the launcher mode exists to remove.
|
|
2624
|
+
launcher && runtime.source === "override"
|
|
2625
|
+
? systemdEnvironmentAssignment(runtime.overrideEnv, runtime.path)
|
|
2626
|
+
: null,
|
|
2546
2627
|
systemdEnvironmentAssignment("PATH", path),
|
|
2547
2628
|
codexHome,
|
|
2548
2629
|
codexSqliteHome,
|
|
2549
2630
|
opencodexHome,
|
|
2550
2631
|
...proxyEnv.map(({ name, value }) => systemdEnvironmentAssignment(name, value)),
|
|
2551
2632
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
2633
|
+
const command = `${launcher ? buildServiceLauncherShellCommand(launcher) : buildServiceShellCommand(bun, cli)} >> ${shellQuote(log)} 2>&1`;
|
|
2552
2634
|
return `[Unit]
|
|
2553
2635
|
Description=OpenCodex Proxy Server
|
|
2554
2636
|
After=network-online.target
|
|
@@ -2556,12 +2638,10 @@ Wants=network-online.target
|
|
|
2556
2638
|
|
|
2557
2639
|
[Service]
|
|
2558
2640
|
Type=simple
|
|
2559
|
-
ExecStart=${systemdQuote("/bin/sh")} -lc ${systemdQuote(
|
|
2641
|
+
ExecStart=${systemdQuote("/bin/sh")} -lc ${systemdQuote(command)}
|
|
2560
2642
|
Restart=on-failure
|
|
2561
2643
|
RestartSec=5
|
|
2562
2644
|
${envLines}
|
|
2563
|
-
StandardOutput=${systemdOutputTarget(`append:${log}`)}
|
|
2564
|
-
StandardError=${systemdOutputTarget(`append:${log}`)}
|
|
2565
2645
|
|
|
2566
2646
|
[Install]
|
|
2567
2647
|
WantedBy=default.target
|
|
@@ -2608,11 +2688,14 @@ function installSystemd(): void {
|
|
|
2608
2688
|
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
2609
2689
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
2610
2690
|
writeServiceApiTokenFile();
|
|
2611
|
-
|
|
2691
|
+
// Resolve ONCE and reuse: the unit and the install state must agree about what is
|
|
2692
|
+
// launched, or the staleness check would validate a path the unit does not run.
|
|
2693
|
+
const launcher = stableLauncherEntry();
|
|
2694
|
+
writeServiceDefinitionFile(unitPath(), buildUnit(resolvedProxyEnv(), { launcher }), "utf8");
|
|
2612
2695
|
sh("systemctl --user daemon-reload");
|
|
2613
2696
|
sh(`systemctl --user enable ${TASK}`);
|
|
2614
2697
|
sh(`systemctl --user restart ${TASK}`);
|
|
2615
|
-
writeServiceInstallState();
|
|
2698
|
+
writeServiceInstallState("scheduler", launcher);
|
|
2616
2699
|
}
|
|
2617
2700
|
/**
|
|
2618
2701
|
* Whether systemd's in-memory unit differs from the file on disk.
|
|
@@ -2660,10 +2743,18 @@ function startSystemd(): void {
|
|
|
2660
2743
|
}
|
|
2661
2744
|
function stopSystemd(): void { try { sh(`systemctl --user stop ${TASK}`); } catch { /* not running */ } }
|
|
2662
2745
|
function statusSystemd(): string { try { return sh(`systemctl --user status ${TASK}`); } catch { return ""; } }
|
|
2663
|
-
function uninstallSystemd(
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2746
|
+
export function uninstallSystemd(deps: {
|
|
2747
|
+
run?: (command: string) => string;
|
|
2748
|
+
unitExists?: () => boolean;
|
|
2749
|
+
removeUnit?: () => void;
|
|
2750
|
+
} = {}): void {
|
|
2751
|
+
const run = deps.run ?? sh;
|
|
2752
|
+
try { run(`systemctl --user stop ${TASK}`); } catch { /* not running */ }
|
|
2753
|
+
try { run(`systemctl --user disable ${TASK}`); } catch { /* absent */ }
|
|
2754
|
+
if ((deps.unitExists ?? (() => existsSync(unitPath())))()) {
|
|
2755
|
+
(deps.removeUnit ?? (() => unlinkSync(unitPath())))();
|
|
2756
|
+
}
|
|
2757
|
+
try { run("systemctl --user daemon-reload"); } catch { /* best-effort */ }
|
|
2667
2758
|
}
|
|
2668
2759
|
|
|
2669
2760
|
type ServiceOps = {
|
|
@@ -2676,6 +2767,21 @@ type ServiceInstallCleanupOps = {
|
|
|
2676
2767
|
stop: () => void;
|
|
2677
2768
|
};
|
|
2678
2769
|
|
|
2770
|
+
export function systemdServiceInstallCleanupOps(deps: {
|
|
2771
|
+
run?: (command: string) => string;
|
|
2772
|
+
} = {}): ServiceInstallCleanupOps {
|
|
2773
|
+
const run = deps.run ?? sh;
|
|
2774
|
+
return {
|
|
2775
|
+
status: () => {
|
|
2776
|
+
const output = run(`systemctl --user show -p LoadState ${TASK}`);
|
|
2777
|
+
const loadState = systemdProperty(output, "LoadState")?.toLowerCase();
|
|
2778
|
+
if (!loadState) throw new Error("systemd service status could not be verified.");
|
|
2779
|
+
return loadState === "not-found" ? null : loadState;
|
|
2780
|
+
},
|
|
2781
|
+
stop: () => { run(`systemctl --user stop ${TASK}`); },
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2679
2785
|
function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
2680
2786
|
if (process.platform === "darwin")
|
|
2681
2787
|
return { install: installLaunchd, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
|
|
@@ -2744,20 +2850,13 @@ function platformServiceInstallCleanupOps(backend: ServiceBackend): ServiceInsta
|
|
|
2744
2850
|
};
|
|
2745
2851
|
}
|
|
2746
2852
|
if (process.platform === "linux") {
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
// caller therefore fails closed.
|
|
2755
|
-
const loadState = sh(`systemctl --user show ${TASK} --property=LoadState --value`).trim().toLowerCase();
|
|
2756
|
-
if (!loadState) throw new Error("systemd service status could not be verified.");
|
|
2757
|
-
return loadState === "not-found" ? null : loadState;
|
|
2758
|
-
},
|
|
2759
|
-
stop: () => { sh(`systemctl --user stop ${TASK}`); },
|
|
2760
|
-
};
|
|
2853
|
+
// `list-unit-files <name>` exits non-zero when the unit has never been
|
|
2854
|
+
// installed, which made a clean first install look like an unknown manager
|
|
2855
|
+
// failure. `show LoadState` gives us the tri-state we actually need: a
|
|
2856
|
+
// healthy user manager returns `not-found` for a missing unit, while an
|
|
2857
|
+
// unreachable/permission-denied manager still makes `sh()` throw and the
|
|
2858
|
+
// caller therefore fails closed.
|
|
2859
|
+
return systemdServiceInstallCleanupOps();
|
|
2761
2860
|
}
|
|
2762
2861
|
return null;
|
|
2763
2862
|
}
|
package/src/storage/cleanup.ts
CHANGED
|
@@ -2578,6 +2578,11 @@ export interface RestoreTestHooks {
|
|
|
2578
2578
|
* reconcile, so cleanup can race an in-flight restore.
|
|
2579
2579
|
*/
|
|
2580
2580
|
holdAfterFileMovesMs?: number;
|
|
2581
|
+
/**
|
|
2582
|
+
* Test-only: publish a ready file after rollout moves, then wait until the
|
|
2583
|
+
* release file exists. This makes cross-thread race tests phase-driven.
|
|
2584
|
+
*/
|
|
2585
|
+
pauseAfterFileMoves?: { readyPath: string; releasePath: string };
|
|
2581
2586
|
}
|
|
2582
2587
|
|
|
2583
2588
|
/**
|
|
@@ -2917,6 +2922,11 @@ export function restoreTrashEntry(
|
|
|
2917
2922
|
return { ok: false, trashDir: id, ...partialCounts, error };
|
|
2918
2923
|
};
|
|
2919
2924
|
|
|
2925
|
+
if (hooks?.pauseAfterFileMoves) {
|
|
2926
|
+
writeFileSync(hooks.pauseAfterFileMoves.readyPath, "ready\n");
|
|
2927
|
+
while (!existsSync(hooks.pauseAfterFileMoves.releasePath)) Bun.sleepSync(10);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2920
2930
|
if (hooks?.holdAfterFileMovesMs !== undefined) {
|
|
2921
2931
|
const holdMs = Math.max(0, Math.floor(hooks.holdAfterFileMovesMs));
|
|
2922
2932
|
if (holdMs > 0) {
|
|
@@ -16,6 +16,12 @@ export type StorageMutationBusyError = "storage_mutation_busy";
|
|
|
16
16
|
export interface StorageMutationCoordinatorTestHooks {
|
|
17
17
|
/** Block after acquiring the slot, before mutation work (race tests). */
|
|
18
18
|
blockMs?: number;
|
|
19
|
+
/** Test-only cross-thread handshake after one mutation kind acquires its slot. */
|
|
20
|
+
pauseAfterAcquire?: {
|
|
21
|
+
kind: StorageMutationKind;
|
|
22
|
+
readyPath: string;
|
|
23
|
+
releasePath: string;
|
|
24
|
+
};
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
interface ActiveSlot {
|
|
@@ -93,7 +99,12 @@ export function endStorageMutation(codexHome?: string): void {
|
|
|
93
99
|
slot.lease.release();
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
async function applyCoordinatorBlock(): Promise<void> {
|
|
102
|
+
async function applyCoordinatorBlock(kind: StorageMutationKind): Promise<void> {
|
|
103
|
+
const pause = testHooks?.pauseAfterAcquire;
|
|
104
|
+
if (pause?.kind === kind) {
|
|
105
|
+
await Bun.write(pause.readyPath, "ready\n");
|
|
106
|
+
while (!Bun.file(pause.releasePath).size) await Bun.sleep(10);
|
|
107
|
+
}
|
|
97
108
|
const blockMs = testHooks?.blockMs;
|
|
98
109
|
if (typeof blockMs === "number" && Number.isFinite(blockMs) && blockMs > 0) {
|
|
99
110
|
await Bun.sleep(Math.floor(blockMs));
|
|
@@ -114,7 +125,7 @@ export async function runPolicyStorageMutation<T>(
|
|
|
114
125
|
return { ok: false, error: "storage_mutation_busy" };
|
|
115
126
|
}
|
|
116
127
|
try {
|
|
117
|
-
await applyCoordinatorBlock();
|
|
128
|
+
await applyCoordinatorBlock("policy");
|
|
118
129
|
return await work();
|
|
119
130
|
} finally {
|
|
120
131
|
gate.lease.release();
|
|
@@ -131,7 +142,7 @@ export async function withStorageMutationSlot<T>(
|
|
|
131
142
|
return { ok: false, error: "storage_mutation_busy" };
|
|
132
143
|
}
|
|
133
144
|
try {
|
|
134
|
-
await applyCoordinatorBlock();
|
|
145
|
+
await applyCoordinatorBlock(kind);
|
|
135
146
|
return await work();
|
|
136
147
|
} finally {
|
|
137
148
|
gate.lease.release();
|
|
@@ -13,6 +13,7 @@ param(
|
|
|
13
13
|
$ErrorActionPreference = "Stop"
|
|
14
14
|
Add-Type -AssemblyName System.Windows.Forms
|
|
15
15
|
Add-Type -AssemblyName System.Drawing
|
|
16
|
+
try { [System.Windows.Forms.Application]::EnableVisualStyles() } catch { $null = $_ }
|
|
16
17
|
|
|
17
18
|
# Normalize aliases before deriving singleton/event names. Without this,
|
|
18
19
|
# C:\path and C:\path\. create separate tray instances for the same home.
|
|
@@ -68,6 +69,7 @@ if (-not $createdNew) {
|
|
|
68
69
|
$mutex.Dispose()
|
|
69
70
|
exit 0
|
|
70
71
|
}
|
|
72
|
+
[void]$stopEvent.Reset()
|
|
71
73
|
|
|
72
74
|
$heartbeatPath = Join-Path $OpenCodexHome "tray-heartbeat.json"
|
|
73
75
|
$actionLogPath = Join-Path $OpenCodexHome "tray-actions.log"
|
|
@@ -332,11 +334,15 @@ $notify.add_DoubleClick({ Start-OcxCommand @("gui") })
|
|
|
332
334
|
$timer = New-Object System.Windows.Forms.Timer
|
|
333
335
|
$timer.Interval = 3000
|
|
334
336
|
$timer.add_Tick({
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
337
|
+
try {
|
|
338
|
+
if ($stopEvent.WaitOne(0)) {
|
|
339
|
+
[System.Windows.Forms.Application]::Exit()
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
Update-TrayState
|
|
343
|
+
} catch {
|
|
344
|
+
try { Write-ActionLog "timer tick failed: $($_.Exception.GetType().Name)" } catch { $null = $_ }
|
|
338
345
|
}
|
|
339
|
-
Update-TrayState
|
|
340
346
|
})
|
|
341
347
|
$notify.ContextMenuStrip = $menu
|
|
342
348
|
$notify.Icon = $offlineIcon
|
package/src/tray/windows.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile, execFileSync, spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { join, resolve, win32 as win32Path } from "node:path";
|
|
6
6
|
import { expandUserPath, getConfigDir } from "../config";
|
|
7
7
|
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
8
8
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
@@ -46,6 +46,12 @@ export interface WindowsTrayStatus {
|
|
|
46
46
|
summary: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type WindowsTrayLaunchRunner = (
|
|
50
|
+
file: string,
|
|
51
|
+
args: readonly string[],
|
|
52
|
+
options: { stdio: "ignore"; windowsHide: true; timeout: number },
|
|
53
|
+
) => void;
|
|
54
|
+
|
|
49
55
|
function trayStatePath(): string {
|
|
50
56
|
return join(getConfigDir(), "tray-state.json");
|
|
51
57
|
}
|
|
@@ -506,8 +512,10 @@ const DETACHED_TRAY_HOST_LAUNCHER = [
|
|
|
506
512
|
"$startInfo = New-Object System.Diagnostics.ProcessStartInfo",
|
|
507
513
|
"$startInfo.FileName = $env:OCX_TRAY_HOST_BUN",
|
|
508
514
|
"$startInfo.Arguments = $env:OCX_TRAY_HOST_ARGS",
|
|
509
|
-
"$startInfo.UseShellExecute = $
|
|
515
|
+
"$startInfo.UseShellExecute = $false",
|
|
516
|
+
"$startInfo.CreateNoWindow = $true",
|
|
510
517
|
"$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden",
|
|
518
|
+
"$startInfo.EnvironmentVariables['OCX_TRAY_ENTRY_B64'] = $env:OCX_TRAY_ENTRY_B64",
|
|
511
519
|
"$child = [System.Diagnostics.Process]::Start($startInfo)",
|
|
512
520
|
"if ($null -eq $child) { throw 'Windows tray host did not start.' }",
|
|
513
521
|
"$child.Dispose()",
|
|
@@ -537,7 +545,27 @@ export function launchWindowsTrayHost(state: WindowsTrayEntry): void {
|
|
|
537
545
|
});
|
|
538
546
|
}
|
|
539
547
|
|
|
548
|
+
export function launchInstalledWindowsTray(
|
|
549
|
+
launcherPath: string,
|
|
550
|
+
deps: { systemRoot?: string; run?: WindowsTrayLaunchRunner } = {},
|
|
551
|
+
): void {
|
|
552
|
+
const wscript = win32Path.join(deps.systemRoot ?? process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
|
|
553
|
+
const run = deps.run ?? ((file, args, options) => {
|
|
554
|
+
execFileSync(file, [...args], options);
|
|
555
|
+
});
|
|
556
|
+
run(wscript, ["//B", "//NoLogo", safePath(launcherPath)], {
|
|
557
|
+
stdio: "ignore",
|
|
558
|
+
windowsHide: true,
|
|
559
|
+
timeout: 15_000,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
540
563
|
function spawnTray(state: WindowsTrayEntry): void {
|
|
564
|
+
const launcher = installedTrayLauncherPath();
|
|
565
|
+
if (existsSync(launcher)) {
|
|
566
|
+
launchInstalledWindowsTray(launcher);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
541
569
|
launchWindowsTrayHost(state);
|
|
542
570
|
}
|
|
543
571
|
|
package/src/types/config.ts
CHANGED
|
@@ -435,17 +435,26 @@ export interface OcxConfig {
|
|
|
435
435
|
* Shadow call intercept: redirect Codex's hard-coded helper calls (title generation,
|
|
436
436
|
* commit messages, skill orchestration) to a user-chosen model. Default intercepted
|
|
437
437
|
* source models: gpt-5.4-mini (older clients) and gpt-5.6-luna (Codex 0.145.0+).
|
|
438
|
-
* Opt-in; disabled by default. Matching
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
438
|
+
* Opt-in; disabled by default. Matching requests preserve their configured reasoning effort.
|
|
439
|
+
* All requests for configured shadow source models are intercepted regardless of request kind,
|
|
440
|
+
* except when the replacement intersects the same provider+model source set.
|
|
441
|
+
*/
|
|
442
|
+
shadowCallIntercept?: {
|
|
443
|
+
/** When true, requests for known shadow/helper source models are rewritten to the configured model. */
|
|
444
|
+
enabled?: boolean;
|
|
445
|
+
/** Replacement model id (e.g. "gpt-5.5"). */
|
|
446
|
+
model?: string;
|
|
447
|
+
/** Optional override of intercepted source-model prefixes (default: gpt-5.4-mini, gpt-5.6-luna). */
|
|
448
|
+
sourceModels?: string[];
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* Optional map of blocked model IDs to their replacement model IDs.
|
|
452
|
+
* When configured, incoming requests targeting a blocked model (including
|
|
453
|
+
* account-namespaced and concrete routes) are redirected to the replacement
|
|
454
|
+
* model at the shared routing layer with routeReason "blocked-model-redirect".
|
|
455
|
+
* Unset or omitted by default.
|
|
456
|
+
*/
|
|
457
|
+
blockedModelRedirects?: Record<string, string>;
|
|
449
458
|
/**
|
|
450
459
|
* 3-state multi-agent surface override:
|
|
451
460
|
* - "v1": force ALL models to v1 surface (override upstream pins)
|
|
@@ -619,6 +628,8 @@ export interface OcxConfig {
|
|
|
619
628
|
strategy?: OcxAccountPoolRotationStrategy;
|
|
620
629
|
/** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
|
|
621
630
|
stickyLimit?: number;
|
|
631
|
+
/** Usage window for quota-based scoring. Default "five-hour" (today's behaviour). */
|
|
632
|
+
quotaWindow?: OcxAccountPoolQuotaWindow;
|
|
622
633
|
};
|
|
623
634
|
/**
|
|
624
635
|
* Generic OAuth multi-account 429 failover (#2568). Presence-driven by default.
|
|
@@ -653,19 +664,21 @@ export interface OcxConfig {
|
|
|
653
664
|
|
|
654
665
|
export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-first";
|
|
655
666
|
|
|
656
|
-
export type
|
|
667
|
+
export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilization";
|
|
668
|
+
|
|
669
|
+
export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
|
|
657
670
|
export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
658
671
|
|
|
659
672
|
export interface OcxComboTarget {
|
|
660
673
|
provider: string;
|
|
661
674
|
model: string;
|
|
662
|
-
/** Relative
|
|
675
|
+
/** Relative target weight for round-robin batches and random selection. Default 1; valid range 1..10000. */
|
|
663
676
|
weight?: number;
|
|
664
677
|
}
|
|
665
678
|
|
|
666
679
|
export interface OcxComboConfig {
|
|
667
680
|
targets: OcxComboTarget[];
|
|
668
|
-
/** Ordered failover (default)
|
|
681
|
+
/** Ordered failover (default), round-robin, weighted random, least-used, or quota reset-window selection. */
|
|
669
682
|
strategy?: OcxComboStrategy;
|
|
670
683
|
/** Successful requests retained on one RR selection batch. Default 1; range 1..100. */
|
|
671
684
|
stickyLimit?: number;
|
package/src/types/provider.ts
CHANGED
|
@@ -17,6 +17,15 @@ export interface OpenRouterProviderRouting {
|
|
|
17
17
|
allowFallbacks?: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface VercelGatewayRouting {
|
|
21
|
+
/** Vercel AI Gateway provider slugs to try first, in priority order. */
|
|
22
|
+
order?: string[];
|
|
23
|
+
/** Restrict routing to these Vercel AI Gateway provider slugs. */
|
|
24
|
+
only?: string[];
|
|
25
|
+
/** Sort providers by "cost", "ttft", or "tps". */
|
|
26
|
+
sort?: "cost" | "ttft" | "tps";
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export interface ResponsesItemIdRepairConfig {
|
|
21
30
|
/** Exact `message` item ids that the proxy should rewrite to request-local canonical ids. */
|
|
22
31
|
message?: string[];
|
|
@@ -32,6 +41,25 @@ export interface ResponsesItemIdRepairConfig {
|
|
|
32
41
|
repairInvalidIds?: boolean;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Opt-in retry for pre-stream transient upstream statuses (500/502/503/504/520/521/522) on
|
|
46
|
+
* `providers.<name>.transientRetryOn5xx`.
|
|
47
|
+
*
|
|
48
|
+
* Disabled unless the object is present; a bare `{}` opts in with defaults. Separate from
|
|
49
|
+
* `retryOn429`, which handles rate limiting with its own waits.
|
|
50
|
+
*/
|
|
51
|
+
export interface TransientRetryPolicy {
|
|
52
|
+
/** Master switch. Presence of the object also enables the policy (default true). */
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* TOTAL upstream sends allowed for one request, including the first (1..10, default 3).
|
|
56
|
+
*
|
|
57
|
+
* Not a per-layer retry count: the connection-reset and transient-status recovery layers
|
|
58
|
+
* share this single budget, so `3` means at most three real requests reach the provider.
|
|
59
|
+
*/
|
|
60
|
+
attempts?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
/**
|
|
36
64
|
* Same-target 429 wait-and-retry policy (`providers.<name>.retryOn429`). When present and not
|
|
37
65
|
* explicitly disabled, the proxy waits and replays the identical request on the same key before
|
|
@@ -197,6 +225,16 @@ export interface OcxProviderConfig {
|
|
|
197
225
|
* preserved after it, and parallel calls stay together with the reasoning turn that produced them.
|
|
198
226
|
*/
|
|
199
227
|
requiresAdjacentResponsesToolResults?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* When enabled, a tool result that is present but empty (no usable text or content
|
|
230
|
+
* part) is rewritten to an explicit annotation before it reaches the upstream wire,
|
|
231
|
+
* so models do not silently accept an empty result or re-issue the same call.
|
|
232
|
+
* Non-empty results and missing-result placeholders stay byte-identical.
|
|
233
|
+
* Seeded true for DeepSeek; absent keeps legacy behavior for every other provider.
|
|
234
|
+
* Only the OpenAI-family adapters (openai-chat / openai-responses) read this option;
|
|
235
|
+
* other adapters ignore it.
|
|
236
|
+
*/
|
|
237
|
+
annotateEmptyToolOutputs?: boolean;
|
|
200
238
|
/**
|
|
201
239
|
* Provider fallback for canonical Fast capability over an OpenAI `service_tier` wire.
|
|
202
240
|
* This pure tri-state feeds catalog publication, routing eligibility, compatibility
|
|
@@ -342,6 +380,10 @@ export interface OcxProviderConfig {
|
|
|
342
380
|
openRouterRouting?: OpenRouterProviderRouting;
|
|
343
381
|
/** Exact model-id overrides for `openRouterRouting`. Each matching entry replaces the default. */
|
|
344
382
|
modelOpenRouterRouting?: Record<string, OpenRouterProviderRouting>;
|
|
383
|
+
/** Default provider-routing preferences for models sent through Vercel AI Gateway (issue #1406). */
|
|
384
|
+
vercelGatewayRouting?: VercelGatewayRouting;
|
|
385
|
+
/** Exact model-id overrides for `vercelGatewayRouting`. Each matching entry replaces the default. */
|
|
386
|
+
modelVercelGatewayRouting?: Record<string, VercelGatewayRouting>;
|
|
345
387
|
/**
|
|
346
388
|
* "key" (default): authenticate upstream with `apiKey`.
|
|
347
389
|
* "forward": relay the caller's incoming auth headers verbatim (OAuth passthrough; gpt only).
|
|
@@ -421,6 +463,12 @@ export interface OcxProviderConfig {
|
|
|
421
463
|
* passthrough compatibility for OpenAI and unclassified gateways.
|
|
422
464
|
*/
|
|
423
465
|
supportsOpenAiWebSearchToolFields?: boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Opt xAI Responses destinations into the provider-hosted `x_search` declaration when a live
|
|
468
|
+
* `web_search` tool survives final request normalization. Disabled by default. This is separate
|
|
469
|
+
* from the web-search sidecar's `search.xSearch` options and never widens caller tool selectors.
|
|
470
|
+
*/
|
|
471
|
+
xaiResponsesXSearch?: boolean;
|
|
424
472
|
/**
|
|
425
473
|
* Whether the Responses upstream accepts native custom tools and custom_tool_call items.
|
|
426
474
|
* Set false only for a provider whose native contract rejects them; absence preserves
|
|
@@ -537,6 +585,12 @@ export interface OcxProviderConfig {
|
|
|
537
585
|
* before any response bytes are relayed, so the replay is lossless.
|
|
538
586
|
*/
|
|
539
587
|
retryOn429?: RateLimitRetryPolicy;
|
|
588
|
+
/**
|
|
589
|
+
* Opt-in retry for pre-stream transient upstream statuses
|
|
590
|
+
* (`providers.<name>.transientRetryOn5xx`). Disabled unless present; a bare `{}` opts in
|
|
591
|
+
* with defaults. Key-auth `openai-chat` only.
|
|
592
|
+
*/
|
|
593
|
+
transientRetryOn5xx?: TransientRetryPolicy;
|
|
540
594
|
/**
|
|
541
595
|
* Model ids whose OpenAI-compatible chat endpoint accepts `reasoning_split: true` and returns
|
|
542
596
|
* thinking separately in `reasoning_content` / `reasoning_details` instead of visible content.
|