@bitkyc08/opencodex 2.10.0 → 2.10.1
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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/codex/runtime.ts
CHANGED
|
@@ -13,32 +13,38 @@ export type CodexRuntimeSource =
|
|
|
13
13
|
| "path"
|
|
14
14
|
| "fallback";
|
|
15
15
|
|
|
16
|
+
export type DeepReadonly<T> =
|
|
17
|
+
T extends (...args: never[]) => unknown ? T
|
|
18
|
+
: T extends readonly (infer U)[] ? readonly DeepReadonly<U>[]
|
|
19
|
+
: T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
|
20
|
+
: T;
|
|
21
|
+
|
|
16
22
|
export interface ResolvedCodexRuntime {
|
|
17
|
-
command: string;
|
|
18
|
-
version: string | null;
|
|
19
|
-
source: CodexRuntimeSource;
|
|
23
|
+
readonly command: string;
|
|
24
|
+
readonly version: string | null;
|
|
25
|
+
readonly source: CodexRuntimeSource;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface RuntimeProbeFailure {
|
|
23
|
-
command: string;
|
|
24
|
-
source: CodexRuntimeSource;
|
|
25
|
-
reason: string;
|
|
29
|
+
readonly command: string;
|
|
30
|
+
readonly source: CodexRuntimeSource;
|
|
31
|
+
readonly reason: string;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export interface EffortClampDiagnostic {
|
|
29
|
-
runtimePath: string;
|
|
30
|
-
runtimeVersion: string | null;
|
|
31
|
-
removedEfforts: string[];
|
|
32
|
-
affectedModels: string[];
|
|
35
|
+
readonly runtimePath: string;
|
|
36
|
+
readonly runtimeVersion: string | null;
|
|
37
|
+
readonly removedEfforts: readonly string[];
|
|
38
|
+
readonly affectedModels: readonly string[];
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export interface ResolveCodexRuntimeResult {
|
|
36
|
-
runtime: ResolvedCodexRuntime;
|
|
37
|
-
failures: RuntimeProbeFailure[];
|
|
38
|
-
replacedConfigured?: { from: ResolvedCodexRuntime; reason: string }
|
|
39
|
-
newerAvailable?: ResolvedCodexRuntime;
|
|
42
|
+
readonly runtime: ResolvedCodexRuntime;
|
|
43
|
+
readonly failures: readonly RuntimeProbeFailure[];
|
|
44
|
+
readonly replacedConfigured?: Readonly<{ from: ResolvedCodexRuntime; reason: string }>;
|
|
45
|
+
readonly newerAvailable?: ResolvedCodexRuntime;
|
|
40
46
|
/** Set when the selected runtime could not be written to codex-runtime.json. */
|
|
41
|
-
persistError?: string;
|
|
47
|
+
readonly persistError?: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export type RuntimeExecFile = (
|
|
@@ -70,17 +76,43 @@ export interface ResolveCodexRuntimeDeps {
|
|
|
70
76
|
discoverAlternatives?: boolean;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
interface
|
|
74
|
-
version: 1;
|
|
75
|
-
command: string;
|
|
76
|
-
source: CodexRuntimeSource;
|
|
77
|
-
selectedVersion
|
|
78
|
-
updatedAt: string;
|
|
79
|
+
export interface PersistedCodexRuntimeState {
|
|
80
|
+
readonly version: 1;
|
|
81
|
+
readonly command: string;
|
|
82
|
+
readonly source: CodexRuntimeSource;
|
|
83
|
+
readonly selectedVersion?: string | null;
|
|
84
|
+
readonly updatedAt: string;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
const PERSIST_FILE = "codex-runtime.json";
|
|
82
88
|
const CLAMP_PERSIST_FILE = "codex-runtime-clamp.json";
|
|
83
89
|
|
|
90
|
+
function cloneAndDeepFreeze<T>(value: T): DeepReadonly<T> {
|
|
91
|
+
const clone = (current: unknown): unknown => {
|
|
92
|
+
if (Array.isArray(current)) return current.map(clone);
|
|
93
|
+
if (current && typeof current === "object") {
|
|
94
|
+
const out: Record<string, unknown> = {};
|
|
95
|
+
for (const [key, child] of Object.entries(current)) out[key] = clone(child);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
return current;
|
|
99
|
+
};
|
|
100
|
+
const freeze = (current: unknown): unknown => {
|
|
101
|
+
if (!current || typeof current !== "object" || Object.isFrozen(current)) return current;
|
|
102
|
+
for (const child of Object.values(current)) freeze(child);
|
|
103
|
+
return Object.freeze(current);
|
|
104
|
+
};
|
|
105
|
+
return freeze(clone(value)) as DeepReadonly<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isCodexRuntimeSource(value: unknown): value is CodexRuntimeSource {
|
|
109
|
+
return value === "environment"
|
|
110
|
+
|| value === "configured"
|
|
111
|
+
|| value === "shim"
|
|
112
|
+
|| value === "path"
|
|
113
|
+
|| value === "fallback";
|
|
114
|
+
}
|
|
115
|
+
|
|
84
116
|
export function codexRuntimeStatePath(configDir: string = getConfigDir()): string {
|
|
85
117
|
return join(configDir, PERSIST_FILE);
|
|
86
118
|
}
|
|
@@ -94,9 +126,16 @@ interface PersistedClampState extends EffortClampDiagnostic {
|
|
|
94
126
|
updatedAt: string;
|
|
95
127
|
}
|
|
96
128
|
|
|
129
|
+
interface LoadedEffortClampDiagnostic {
|
|
130
|
+
runtimePath: string;
|
|
131
|
+
runtimeVersion: string | null;
|
|
132
|
+
removedEfforts: string[];
|
|
133
|
+
affectedModels: string[];
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
export function loadLastEffortClamp(
|
|
98
137
|
deps: ResolveCodexRuntimeDeps = {},
|
|
99
|
-
):
|
|
138
|
+
): LoadedEffortClampDiagnostic | null {
|
|
100
139
|
const configDir = deps.configDir ?? getConfigDir();
|
|
101
140
|
const read = deps.readFileSync ?? ((path, encoding) => readFileSync(path, encoding));
|
|
102
141
|
try {
|
|
@@ -196,15 +235,31 @@ export function compareCodexVersions(a: string | null, b: string | null): number
|
|
|
196
235
|
return 0;
|
|
197
236
|
}
|
|
198
237
|
|
|
238
|
+
/** Parse already-observed runtime-selection bytes without consulting the filesystem. */
|
|
239
|
+
export function parsePersistedCodexRuntime(
|
|
240
|
+
bytes: string | Uint8Array,
|
|
241
|
+
): DeepReadonly<PersistedCodexRuntimeState> | null {
|
|
242
|
+
try {
|
|
243
|
+
const text = typeof bytes === "string" ? bytes : Buffer.from(bytes).toString("utf8");
|
|
244
|
+
const raw = JSON.parse(text) as Partial<PersistedCodexRuntimeState>;
|
|
245
|
+
if (raw.version !== 1 || typeof raw.command !== "string" || !raw.command.trim()) return null;
|
|
246
|
+
if (!isCodexRuntimeSource(raw.source) || typeof raw.updatedAt !== "string") return null;
|
|
247
|
+
if (raw.selectedVersion !== undefined
|
|
248
|
+
&& raw.selectedVersion !== null
|
|
249
|
+
&& typeof raw.selectedVersion !== "string") return null;
|
|
250
|
+
return cloneAndDeepFreeze(raw as PersistedCodexRuntimeState);
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
199
256
|
export function loadPersistedCodexRuntime(
|
|
200
257
|
deps: ResolveCodexRuntimeDeps = {},
|
|
201
|
-
):
|
|
258
|
+
): DeepReadonly<PersistedCodexRuntimeState> | null {
|
|
202
259
|
const configDir = deps.configDir ?? getConfigDir();
|
|
203
260
|
const read = deps.readFileSync ?? ((path, encoding) => readFileSync(path, encoding));
|
|
204
261
|
try {
|
|
205
|
-
|
|
206
|
-
if (raw?.version !== 1 || typeof raw.command !== "string" || !raw.command.trim()) return null;
|
|
207
|
-
return raw;
|
|
262
|
+
return parsePersistedCodexRuntime(read(codexRuntimeStatePath(configDir), "utf8"));
|
|
208
263
|
} catch {
|
|
209
264
|
return null;
|
|
210
265
|
}
|
|
@@ -216,16 +271,16 @@ export function persistCodexRuntime(
|
|
|
216
271
|
): void {
|
|
217
272
|
const configDir = deps.configDir ?? getConfigDir();
|
|
218
273
|
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
219
|
-
const payload:
|
|
274
|
+
const payload: PersistedCodexRuntimeState = {
|
|
220
275
|
version: 1,
|
|
221
276
|
command: runtime.command,
|
|
222
277
|
source: runtime.source,
|
|
223
278
|
selectedVersion: runtime.version,
|
|
224
279
|
updatedAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
225
280
|
};
|
|
281
|
+
// Invalidate process authority before the persisted replacement is visible.
|
|
282
|
+
clearCodexRuntimeResolveCache();
|
|
226
283
|
atomicWriteFile(codexRuntimeStatePath(configDir), `${JSON.stringify(payload, null, 2)}\n`);
|
|
227
|
-
// Same-process consumers must re-resolve; catalog cache is keyed by runtime identity.
|
|
228
|
-
resolveCache = null;
|
|
229
284
|
}
|
|
230
285
|
|
|
231
286
|
function probeVersion(
|
|
@@ -360,15 +415,68 @@ export function effortClampAppliesToRuntime(
|
|
|
360
415
|
}
|
|
361
416
|
|
|
362
417
|
const RESOLVE_CACHE_MS = 15_000;
|
|
363
|
-
|
|
418
|
+
interface ResolveCacheMemo {
|
|
419
|
+
readonly key: string;
|
|
420
|
+
readonly at: number;
|
|
421
|
+
readonly epoch: number;
|
|
422
|
+
readonly valueIdentity: string;
|
|
423
|
+
readonly value: DeepReadonly<ResolveCodexRuntimeResult>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export type CodexRuntimeProcessCachePeek =
|
|
427
|
+
| Readonly<{
|
|
428
|
+
kind: "available";
|
|
429
|
+
epoch: number;
|
|
430
|
+
valueIdentity: string;
|
|
431
|
+
value: DeepReadonly<ResolveCodexRuntimeResult>;
|
|
432
|
+
}>
|
|
433
|
+
| Readonly<{ kind: "unavailable"; epoch: number }>;
|
|
434
|
+
|
|
435
|
+
let resolveCacheEpoch = 0;
|
|
436
|
+
let resolveCache: ResolveCacheMemo | null = null;
|
|
437
|
+
|
|
438
|
+
function publishResolveCache(key: string, at: number, value: ResolveCodexRuntimeResult): void {
|
|
439
|
+
const epoch = ++resolveCacheEpoch;
|
|
440
|
+
resolveCache = {
|
|
441
|
+
key,
|
|
442
|
+
at,
|
|
443
|
+
epoch,
|
|
444
|
+
valueIdentity: `runtime:${epoch}`,
|
|
445
|
+
value: cloneAndDeepFreeze(value),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function clearResolveCache(): void {
|
|
450
|
+
resolveCacheEpoch += 1;
|
|
451
|
+
resolveCache = null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Clear process-local runtime authority without resolving a replacement. */
|
|
455
|
+
export function clearCodexRuntimeResolveCache(): void {
|
|
456
|
+
clearResolveCache();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Observe only an unexpired successful process memo; never resolves or probes. */
|
|
460
|
+
export function peekCodexRuntimeProcessCache(): CodexRuntimeProcessCachePeek {
|
|
461
|
+
const memo = resolveCache;
|
|
462
|
+
if (!memo || Date.now() - memo.at >= RESOLVE_CACHE_MS) {
|
|
463
|
+
return Object.freeze({ kind: "unavailable" as const, epoch: resolveCacheEpoch });
|
|
464
|
+
}
|
|
465
|
+
return Object.freeze({
|
|
466
|
+
kind: "available" as const,
|
|
467
|
+
epoch: memo.epoch,
|
|
468
|
+
valueIdentity: memo.valueIdentity,
|
|
469
|
+
value: cloneAndDeepFreeze(memo.value),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
364
472
|
|
|
365
473
|
function persistedRuntimeCacheStamp(deps: ResolveCodexRuntimeDeps): string {
|
|
366
474
|
// Include on-disk selection so doctor --fix in another process busts this memo.
|
|
367
475
|
const configDir = deps.configDir ?? getConfigDir();
|
|
368
476
|
const read = deps.readFileSync ?? ((path, encoding) => readFileSync(path, encoding));
|
|
369
477
|
try {
|
|
370
|
-
const raw =
|
|
371
|
-
if (raw
|
|
478
|
+
const raw = parsePersistedCodexRuntime(read(codexRuntimeStatePath(configDir), "utf8"));
|
|
479
|
+
if (!raw) return "";
|
|
372
480
|
return `${raw.command}|${raw.selectedVersion ?? ""}|${raw.updatedAt ?? ""}`;
|
|
373
481
|
} catch {
|
|
374
482
|
return "";
|
|
@@ -397,17 +505,30 @@ function resolveCacheKey(deps: ResolveCodexRuntimeDeps): string | null {
|
|
|
397
505
|
export function resolveCodexRuntime(deps: ResolveCodexRuntimeDeps = {}): ResolveCodexRuntimeResult {
|
|
398
506
|
const cacheKey = resolveCacheKey(deps);
|
|
399
507
|
if (cacheKey && resolveCache && resolveCache.key === cacheKey && Date.now() - resolveCache.at < RESOLVE_CACHE_MS) {
|
|
400
|
-
return resolveCache.value;
|
|
508
|
+
return cloneAndDeepFreeze(resolveCache.value);
|
|
401
509
|
}
|
|
402
510
|
|
|
403
511
|
const result = resolveCodexRuntimeUncached(deps);
|
|
404
|
-
if (cacheKey)
|
|
405
|
-
|
|
512
|
+
if (cacheKey) {
|
|
513
|
+
publishResolveCache(cacheKey, Date.now(), result);
|
|
514
|
+
return cloneAndDeepFreeze(resolveCache!.value);
|
|
515
|
+
}
|
|
516
|
+
return cloneAndDeepFreeze(result);
|
|
406
517
|
}
|
|
407
518
|
|
|
408
519
|
/** Test-only: drop the short-lived process resolve cache. */
|
|
409
520
|
export function resetCodexRuntimeResolveCacheForTests(): void {
|
|
410
|
-
|
|
521
|
+
clearCodexRuntimeResolveCache();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Test-only owner mutation seam; input is cloned and frozen before publication. */
|
|
525
|
+
export function setCodexRuntimeResolveCacheForTests(
|
|
526
|
+
value: ResolveCodexRuntimeResult,
|
|
527
|
+
deps: ResolveCodexRuntimeDeps = {},
|
|
528
|
+
): void {
|
|
529
|
+
const key = resolveCacheKey(deps);
|
|
530
|
+
if (!key) throw new TypeError("Injected runtime dependencies cannot populate the process memo.");
|
|
531
|
+
publishResolveCache(key, Date.now(), value);
|
|
411
532
|
}
|
|
412
533
|
|
|
413
534
|
function resolveCodexRuntimeUncached(deps: ResolveCodexRuntimeDeps = {}): ResolveCodexRuntimeResult {
|
|
@@ -468,7 +589,7 @@ function resolveCodexRuntimeUncached(deps: ResolveCodexRuntimeDeps = {}): Resolv
|
|
|
468
589
|
replacedConfigured = {
|
|
469
590
|
from: {
|
|
470
591
|
command: persisted.command,
|
|
471
|
-
version: persisted.selectedVersion,
|
|
592
|
+
version: persisted.selectedVersion ?? null,
|
|
472
593
|
source: "configured",
|
|
473
594
|
},
|
|
474
595
|
reason: failures.find(item => sameRuntimeCommand(item.command, persisted.command))?.reason
|
|
@@ -518,7 +639,7 @@ export function resolveAndPersistCodexRuntime(
|
|
|
518
639
|
const message = error instanceof Error ? error.message : String(error);
|
|
519
640
|
const persistError = redactUserPath(redactSecretString(message)).slice(0, 200);
|
|
520
641
|
console.warn(`[opencodex] Failed to persist Codex runtime selection: ${persistError}`);
|
|
521
|
-
return { ...result, persistError };
|
|
642
|
+
return cloneAndDeepFreeze({ ...result, persistError });
|
|
522
643
|
}
|
|
523
644
|
}
|
|
524
645
|
return result;
|
package/src/codex/shim.ts
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
writeFileSync,
|
|
20
20
|
} from "node:fs";
|
|
21
21
|
import { getConfigDir } from "../config";
|
|
22
|
-
import {
|
|
22
|
+
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "../lib/bun-runtime";
|
|
23
|
+
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
23
24
|
import { isProcessAlive } from "../lib/process-control";
|
|
24
25
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
25
26
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
@@ -120,11 +121,13 @@ export type CodexShimAutoRestoreResult =
|
|
|
120
121
|
| { status: "ineligible" | "deferred"; message?: string }
|
|
121
122
|
| { status: "restored"; message: string };
|
|
122
123
|
|
|
123
|
-
function cliEntry(): { bun: string; cli: string } {
|
|
124
|
+
function cliEntry(): { bun: string; bunRuntimeSource: BunRuntimeSource; cli: string } {
|
|
124
125
|
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
125
126
|
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
126
127
|
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
127
|
-
|
|
128
|
+
// Path and provenance resolve together so the marker always describes this binary.
|
|
129
|
+
const runtime = durableBunRuntime();
|
|
130
|
+
return { bun: runtime.path, bunRuntimeSource: runtime.source, cli: join(import.meta.dir, "..", "cli", "index.ts") };
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
function commandNames(name: string): string[] {
|
|
@@ -366,7 +369,16 @@ function shQuote(value: string): string {
|
|
|
366
369
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
367
370
|
}
|
|
368
371
|
|
|
369
|
-
|
|
372
|
+
// Provenance is required rather than defaulted: a default would let a caller pass an
|
|
373
|
+
// override binary and silently label it something else, which is precisely the
|
|
374
|
+
// path/marker disagreement this feature exists to prevent.
|
|
375
|
+
//
|
|
376
|
+
// The marker is scoped to the `ensure` invocation in every flavor below and is never
|
|
377
|
+
// exported into the shim's own environment. A shim wraps the real `codex`, so an
|
|
378
|
+
// exported marker would be inherited by Codex and everything it spawns — a shell that
|
|
379
|
+
// then ran a *different* Bun directly would carry a provenance describing a binary it
|
|
380
|
+
// is not executing.
|
|
381
|
+
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource, tokenFile = serviceApiTokenFilePath()): string {
|
|
370
382
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
371
383
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
372
384
|
return `#!/usr/bin/env sh
|
|
@@ -406,7 +418,7 @@ case "$ocx_subcommand" in
|
|
|
406
418
|
;;
|
|
407
419
|
*)
|
|
408
420
|
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
409
|
-
${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
421
|
+
${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
410
422
|
fi
|
|
411
423
|
;;
|
|
412
424
|
esac
|
|
@@ -431,7 +443,7 @@ function windowsBatchSet(name: string, value: string): string {
|
|
|
431
443
|
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
432
444
|
}
|
|
433
445
|
|
|
434
|
-
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
446
|
+
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
435
447
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
436
448
|
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
437
449
|
return `@echo off\r
|
|
@@ -462,7 +474,11 @@ if "%~1"=="" goto ensure_ocx\r
|
|
|
462
474
|
shift\r
|
|
463
475
|
goto scan_codex_args\r
|
|
464
476
|
:ensure_ocx\r
|
|
477
|
+
setlocal\r
|
|
478
|
+
${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
|
|
479
|
+
${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
|
|
465
480
|
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
481
|
+
endlocal\r
|
|
466
482
|
:run_codex\r
|
|
467
483
|
"%OCX_REAL_CODEX%" %*\r
|
|
468
484
|
`;
|
|
@@ -472,7 +488,7 @@ function psString(value: string): string {
|
|
|
472
488
|
return `'${value.replace(/'/g, "''")}'`;
|
|
473
489
|
}
|
|
474
490
|
|
|
475
|
-
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
491
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string, bunRuntimeSource: BunRuntimeSource): string {
|
|
476
492
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
477
493
|
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
478
494
|
const tokenFile = serviceApiTokenFilePath();
|
|
@@ -497,7 +513,17 @@ foreach ($argValue in $args) {
|
|
|
497
513
|
}
|
|
498
514
|
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
499
515
|
if (-not $skipEnsure) {
|
|
500
|
-
|
|
516
|
+
$priorRuntimeSource = $env:${BUN_RUNTIME_SOURCE_ENV}
|
|
517
|
+
$priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
|
|
518
|
+
$env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
|
|
519
|
+
$env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
|
|
520
|
+
try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
|
|
521
|
+
finally {
|
|
522
|
+
if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
|
|
523
|
+
else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
|
|
524
|
+
if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
|
|
525
|
+
else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
|
|
526
|
+
}
|
|
501
527
|
}
|
|
502
528
|
& ${psString(realCodexPath)} @args
|
|
503
529
|
exit $LASTEXITCODE
|
|
@@ -601,25 +627,25 @@ function gitBashPath(path: string): string {
|
|
|
601
627
|
}
|
|
602
628
|
|
|
603
629
|
function writeShim(wrapperPath: string, realCodexPath: string): void {
|
|
604
|
-
const { bun, cli } = cliEntry();
|
|
630
|
+
const { bun, bunRuntimeSource, cli } = cliEntry();
|
|
605
631
|
if (process.platform === "win32") {
|
|
606
632
|
const lower = wrapperPath.toLowerCase();
|
|
607
633
|
if (lower.endsWith(".ps1")) {
|
|
608
634
|
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
|
|
609
635
|
// codepage, which mangles non-ASCII paths embedded in the shim.
|
|
610
|
-
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
|
|
636
|
+
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli, bunRuntimeSource)}`, "utf8");
|
|
611
637
|
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
612
|
-
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
638
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
|
|
613
639
|
} else {
|
|
614
640
|
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
|
|
615
641
|
writeFileSync(
|
|
616
642
|
wrapperPath,
|
|
617
|
-
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
|
|
643
|
+
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), bunRuntimeSource, gitBashPath(serviceApiTokenFilePath())),
|
|
618
644
|
"utf8",
|
|
619
645
|
);
|
|
620
646
|
}
|
|
621
647
|
} else {
|
|
622
|
-
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
|
|
648
|
+
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli, bunRuntimeSource), "utf8");
|
|
623
649
|
chmodSync(wrapperPath, 0o755);
|
|
624
650
|
}
|
|
625
651
|
}
|
|
@@ -15,11 +15,16 @@ import { CODEX_HOME, getCodexHome } from "./paths";
|
|
|
15
15
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
16
16
|
import {
|
|
17
17
|
canAcquireCodexQuotaProbeLease,
|
|
18
|
+
codexQuotaScopeForModel,
|
|
18
19
|
computeCodexUsageScore,
|
|
20
|
+
getCodexQuotaHealthSnapshot,
|
|
19
21
|
getPoolAccountPlan,
|
|
20
22
|
isCodexAccountInCooldown,
|
|
21
23
|
} from "./routing";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
isCodexAccountUsable,
|
|
26
|
+
type CodexAccountUsabilityOptions,
|
|
27
|
+
} from "./account-usability";
|
|
23
28
|
import { isCodexAccountPaused } from "./account-pause";
|
|
24
29
|
import { slugEquals } from "../providers/slug-codec";
|
|
25
30
|
import { isThreadSpawnRequest } from "../server/effort-policy";
|
|
@@ -27,6 +32,7 @@ import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
|
27
32
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
28
33
|
import { routeModel, type RouteResult } from "../router";
|
|
29
34
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
35
|
+
import { codexAccountNamespaceForModel } from "./account-namespace-match";
|
|
30
36
|
export const DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS = 60_000;
|
|
31
37
|
|
|
32
38
|
type SubagentQuotaPrimeFn = (config: OcxConfig, reason: string) => Promise<void>;
|
|
@@ -68,6 +74,13 @@ function isDisabledFallbackModel(model: string, config: OcxConfig): boolean {
|
|
|
68
74
|
const slash = model.indexOf("/");
|
|
69
75
|
const provider = model.slice(0, slash);
|
|
70
76
|
const modelId = model.slice(slash + 1);
|
|
77
|
+
if (codexAccountNamespaceForModel(config.codexAccountNamespaces, model)) {
|
|
78
|
+
return disabled.some(stored =>
|
|
79
|
+
stored === model
|
|
80
|
+
|| stored === modelId
|
|
81
|
+
|| slugEquals(stored, "openai", modelId)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
71
84
|
return disabled.some(stored => stored === model || slugEquals(stored, provider, modelId));
|
|
72
85
|
}
|
|
73
86
|
|
|
@@ -79,13 +92,22 @@ function pollIntervalMs(config: OcxConfig): number {
|
|
|
79
92
|
return configured;
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
function fallbackChainKey(model: string, namespaces: unknown): string {
|
|
96
|
+
const selector = codexAccountNamespaceForModel(namespaces, model);
|
|
97
|
+
if (!selector) return JSON.stringify(["model", model.toLowerCase()]);
|
|
98
|
+
const slash = model.indexOf("/");
|
|
99
|
+
// Selector keys are exact-case account boundaries. Keep that segment distinct while
|
|
100
|
+
// retaining legacy case-insensitive de-duplication for the native model suffix.
|
|
101
|
+
return JSON.stringify(["account", selector, model.slice(slash + 1).toLowerCase()]);
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
function normalizedChain(primary: string, config: OcxConfig, extra: readonly string[] = []): string[] {
|
|
83
105
|
const chain: string[] = [];
|
|
84
106
|
const seen = new Set<string>();
|
|
85
107
|
const push = (model: string | undefined) => {
|
|
86
108
|
if (!model || model.trim() === "") return;
|
|
87
109
|
const trimmed = model.trim();
|
|
88
|
-
const key = trimmed.
|
|
110
|
+
const key = fallbackChainKey(trimmed, config.codexAccountNamespaces);
|
|
89
111
|
if (seen.has(key)) return;
|
|
90
112
|
seen.add(key);
|
|
91
113
|
chain.push(trimmed);
|
|
@@ -127,9 +149,18 @@ function resolvePoolFallbackAccountId(
|
|
|
127
149
|
return activeCodexAccountId(config);
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
function resolveRouteFallbackAccountId(
|
|
153
|
+
route: RouteResult | null,
|
|
154
|
+
config: OcxConfig,
|
|
155
|
+
accountId?: string | null,
|
|
156
|
+
): string | null {
|
|
157
|
+
return route?.codexAccountId ?? resolvePoolFallbackAccountId(config, accountId);
|
|
158
|
+
}
|
|
159
|
+
|
|
130
160
|
function isRoutableFallbackModel(model: string, config: OcxConfig): boolean {
|
|
131
161
|
const slash = model.indexOf("/");
|
|
132
162
|
if (slash > 0) {
|
|
163
|
+
if (codexAccountNamespaceForModel(config.codexAccountNamespaces, model)) return true;
|
|
133
164
|
const providerName = model.slice(0, slash);
|
|
134
165
|
if (!hasOwnProvider(config.providers, providerName)) {
|
|
135
166
|
// Allow well-known "vendor/model" ids (e.g. anthropic/claude-*) to flow as
|
|
@@ -150,7 +181,7 @@ export function isNativeModelQuotaExhausted(
|
|
|
150
181
|
): boolean {
|
|
151
182
|
const route = tryRouteFallbackModel(config, model);
|
|
152
183
|
if (!route || !isPoolCodexRoute(route)) return false;
|
|
153
|
-
const resolvedAccountId =
|
|
184
|
+
const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
|
|
154
185
|
if (!resolvedAccountId) return false;
|
|
155
186
|
const quota = getAccountQuota(resolvedAccountId);
|
|
156
187
|
const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId));
|
|
@@ -167,7 +198,11 @@ export function isModelHealthBlocked(
|
|
|
167
198
|
const route = tryRouteFallbackModel(config, model);
|
|
168
199
|
const poolScoped = !!route && isPoolCodexRoute(route);
|
|
169
200
|
const health = modelHealth.get(
|
|
170
|
-
healthKey(
|
|
201
|
+
healthKey(
|
|
202
|
+
model,
|
|
203
|
+
resolveRouteFallbackAccountId(route, config, accountId),
|
|
204
|
+
poolScoped,
|
|
205
|
+
),
|
|
171
206
|
);
|
|
172
207
|
return !!health && health.unavailableUntil > now;
|
|
173
208
|
}
|
|
@@ -177,6 +212,7 @@ export function isSubagentModelUnavailable(
|
|
|
177
212
|
config: OcxConfig,
|
|
178
213
|
accountId?: string | null,
|
|
179
214
|
now = Date.now(),
|
|
215
|
+
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
180
216
|
): boolean {
|
|
181
217
|
if (isDisabledFallbackModel(model, config)) return true;
|
|
182
218
|
if (!isRoutableFallbackModel(model, config)) return true;
|
|
@@ -187,11 +223,17 @@ export function isSubagentModelUnavailable(
|
|
|
187
223
|
|
|
188
224
|
// Pool candidates need a usable account. Derive requirement from the resolved
|
|
189
225
|
// route (canonical openai defaults to pool even when codexAccountMode is omitted).
|
|
190
|
-
const resolvedAccountId =
|
|
226
|
+
const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
|
|
191
227
|
if (!resolvedAccountId) return true;
|
|
192
228
|
if (isCodexAccountPaused(config, resolvedAccountId)) return true;
|
|
193
|
-
if (!isCodexAccountUsable(config, resolvedAccountId)) return true;
|
|
194
|
-
if (
|
|
229
|
+
if (!isCodexAccountUsable(config, resolvedAccountId, accountUsabilityOptions)) return true;
|
|
230
|
+
if (route.codexAccountId !== undefined) {
|
|
231
|
+
// An account-qualified route is pinned and cannot consume Pool's recovery-probe
|
|
232
|
+
// escape hatch. Honor both account-wide and model-scoped cooldowns so fallback
|
|
233
|
+
// advances instead of selecting a candidate that exact auth will reject.
|
|
234
|
+
const quotaScope = codexQuotaScopeForModel(route.modelId);
|
|
235
|
+
if (getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now) !== null) return true;
|
|
236
|
+
} else if (
|
|
195
237
|
isCodexAccountInCooldown(resolvedAccountId, now)
|
|
196
238
|
&& !canAcquireCodexQuotaProbeLease(resolvedAccountId, now)
|
|
197
239
|
) {
|
|
@@ -207,6 +249,7 @@ export function selectAvailableSubagentModel(
|
|
|
207
249
|
accountId?: string | null,
|
|
208
250
|
now = Date.now(),
|
|
209
251
|
nativeFallbackOnly = false,
|
|
252
|
+
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
210
253
|
): { model: string; rewritten: boolean; skipped: string[] } {
|
|
211
254
|
const chain = normalizedChain(primary, config, extraFallback);
|
|
212
255
|
const skipped: string[] = [];
|
|
@@ -218,7 +261,7 @@ export function selectAvailableSubagentModel(
|
|
|
218
261
|
continue;
|
|
219
262
|
}
|
|
220
263
|
}
|
|
221
|
-
if (isSubagentModelUnavailable(candidate, config, accountId, now)) {
|
|
264
|
+
if (isSubagentModelUnavailable(candidate, config, accountId, now, accountUsabilityOptions)) {
|
|
222
265
|
skipped.push(candidate);
|
|
223
266
|
continue;
|
|
224
267
|
}
|
|
@@ -240,7 +283,11 @@ export function noteSubagentModelFailure(
|
|
|
240
283
|
const route = tryRouteFallbackModel(config, model);
|
|
241
284
|
const poolScoped = !!route && isPoolCodexRoute(route);
|
|
242
285
|
modelHealth.set(
|
|
243
|
-
healthKey(
|
|
286
|
+
healthKey(
|
|
287
|
+
model,
|
|
288
|
+
resolveRouteFallbackAccountId(route, config, accountId),
|
|
289
|
+
poolScoped,
|
|
290
|
+
),
|
|
244
291
|
{
|
|
245
292
|
unavailableUntil: now + interval,
|
|
246
293
|
reason: "quota_exhausted",
|
|
@@ -324,13 +371,16 @@ export function readCodexAgentModel(role: string, codexHome = CODEX_HOME): strin
|
|
|
324
371
|
export function resolveAgentModelFallbackForPrimary(
|
|
325
372
|
primary: string,
|
|
326
373
|
codexHome = CODEX_HOME,
|
|
374
|
+
namespaces?: unknown,
|
|
327
375
|
): string[] {
|
|
328
376
|
const merged: string[] = [];
|
|
329
377
|
const seen = new Set<string>();
|
|
330
378
|
const push = (model: string | null | undefined) => {
|
|
331
379
|
if (!model || model.trim() === "") return;
|
|
332
380
|
const trimmed = model.trim();
|
|
333
|
-
|
|
381
|
+
// Match global-chain de-dupe: keep account-selector prefixes case-sensitive when
|
|
382
|
+
// those selectors are configured, while ordinary provider/model ids stay case-insensitive.
|
|
383
|
+
const key = fallbackChainKey(trimmed, namespaces);
|
|
334
384
|
if (seen.has(key)) return;
|
|
335
385
|
seen.add(key);
|
|
336
386
|
merged.push(trimmed);
|
|
@@ -349,7 +399,12 @@ export function resolveAgentModelFallbackForPrimary(
|
|
|
349
399
|
* after a successful refresh so failures remain retryable. Errors are swallowed so
|
|
350
400
|
* spawn routing can continue.
|
|
351
401
|
*/
|
|
352
|
-
export function maybePrimeSubagentQuota(
|
|
402
|
+
export function maybePrimeSubagentQuota(
|
|
403
|
+
config: OcxConfig,
|
|
404
|
+
now = Date.now(),
|
|
405
|
+
options: { nativeMainReadsForbidden?: boolean } = {},
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
if (options.nativeMainReadsForbidden) return Promise.resolve();
|
|
353
408
|
if (quotaPrimeInFlight) return quotaPrimeInFlight;
|
|
354
409
|
if (!shouldPrimeSubagentQuota(config, now)) return Promise.resolve();
|
|
355
410
|
|
|
@@ -391,9 +446,14 @@ export function applySubagentModelFallback(
|
|
|
391
446
|
accountId?: string | null,
|
|
392
447
|
now = Date.now(),
|
|
393
448
|
nativeFallbackOnly = false,
|
|
449
|
+
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
394
450
|
): { from?: string; to?: string; skipped?: string[] } | null {
|
|
395
451
|
if (!isThreadSpawnRequest(headers)) return null;
|
|
396
|
-
const roleFallback = resolveAgentModelFallbackForPrimary(
|
|
452
|
+
const roleFallback = resolveAgentModelFallbackForPrimary(
|
|
453
|
+
parsed.modelId,
|
|
454
|
+
getCodexHome(),
|
|
455
|
+
config.codexAccountNamespaces,
|
|
456
|
+
);
|
|
397
457
|
const globalFallback = config.subagentModelFallback ?? [];
|
|
398
458
|
if (globalFallback.length === 0 && roleFallback.length === 0) return null;
|
|
399
459
|
const selection = selectAvailableSubagentModel(
|
|
@@ -403,6 +463,7 @@ export function applySubagentModelFallback(
|
|
|
403
463
|
accountId,
|
|
404
464
|
now,
|
|
405
465
|
nativeFallbackOnly,
|
|
466
|
+
accountUsabilityOptions,
|
|
406
467
|
);
|
|
407
468
|
if (!selection.rewritten) return selection.skipped.length > 0
|
|
408
469
|
? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
|