@yansigit/opencodex 2.33.0 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -22,6 +22,10 @@ const USAGE = `Usage:
|
|
|
22
22
|
ocx models <enable|disable> <provider/model|native-model> [--native] [--json]
|
|
23
23
|
ocx models provider <name> <on|off> [--json]
|
|
24
24
|
ocx models selected <provider> [--set <id,id...>|--clear] [--json]
|
|
25
|
+
ocx models preset show [--provider <name>] [--json]
|
|
26
|
+
ocx models preset apply <provider> [--all] [--json]
|
|
27
|
+
ocx models new-policy [on|off] [--provider <name>] [--json]
|
|
28
|
+
ocx models new-arrivals [--json]
|
|
25
29
|
ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]
|
|
26
30
|
ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]`;
|
|
27
31
|
|
|
@@ -162,6 +166,94 @@ async function selected(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
162
166
|
printData(result, wantsJson, [`${provider}: ${models.length ? models.join(", ") : "all models"}`]);
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
|
|
170
|
+
interface ModelPresetView {
|
|
171
|
+
mode: string;
|
|
172
|
+
appliedVersion?: number;
|
|
173
|
+
availableVersion: number;
|
|
174
|
+
presetIds: string[];
|
|
175
|
+
presetCount: number;
|
|
176
|
+
totalCount: number;
|
|
177
|
+
fallback?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function presetLine(name: string, view: ModelPresetView): string {
|
|
181
|
+
const parts = [`${name}: mode=${view.mode}`];
|
|
182
|
+
if (view.appliedVersion !== undefined && view.appliedVersion !== view.availableVersion) {
|
|
183
|
+
parts.push(`applied v${view.appliedVersion}, available v${view.availableVersion}`);
|
|
184
|
+
} else {
|
|
185
|
+
parts.push(`preset v${view.availableVersion}`);
|
|
186
|
+
}
|
|
187
|
+
parts.push(`(${view.presetCount} of ${view.totalCount} models)`);
|
|
188
|
+
if (view.fallback) parts.push(`fallback=${view.fallback}`);
|
|
189
|
+
return parts.join(" ");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function preset(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
193
|
+
const args = [...argv];
|
|
194
|
+
const action = (args.shift() ?? "show").toLowerCase();
|
|
195
|
+
const wantsJson = takeFlag(args, "--json");
|
|
196
|
+
if (action === "show") {
|
|
197
|
+
const only = takeOption(args, "--provider")?.trim();
|
|
198
|
+
rejectArgs(args, USAGE);
|
|
199
|
+
const result = await runtimeRequest<{ providers?: Record<string, ModelPresetView> }>("/api/model-presets", {}, deps);
|
|
200
|
+
const providers = result.providers ?? {};
|
|
201
|
+
const entries = Object.entries(providers).filter(([name]) => !only || name === only);
|
|
202
|
+
const lines = entries.length > 0
|
|
203
|
+
? entries.map(([name, view]) => presetLine(name, view))
|
|
204
|
+
// A provider with no shipped preset is not an error: it simply has nothing to curate.
|
|
205
|
+
: [only ? `${only}: no model preset is shipped for this provider` : "no providers have a shipped model preset"];
|
|
206
|
+
printData(only ? providers[only] ?? {} : result, wantsJson, lines);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (action !== "apply") throw new CliUsageError(`unknown preset action '${action}'`, USAGE);
|
|
210
|
+
const provider = args.shift()?.trim();
|
|
211
|
+
const all = takeFlag(args, "--all");
|
|
212
|
+
if (!provider) throw new CliUsageError("provider is required", USAGE);
|
|
213
|
+
rejectArgs(args, USAGE);
|
|
214
|
+
const mode = all ? "all" : "preset";
|
|
215
|
+
const result = await runtimeRequest<{ selected?: string[]; fallback?: string; appliedVersion?: number }>(
|
|
216
|
+
"/api/model-presets",
|
|
217
|
+
{ method: "PUT", body: JSON.stringify({ provider, mode }) },
|
|
218
|
+
deps,
|
|
219
|
+
);
|
|
220
|
+
const selectedIds = result.selected ?? [];
|
|
221
|
+
const line = result.fallback === "preset-empty"
|
|
222
|
+
// Never silently narrow to nothing: empty means ALL, so a zero-match preset keeps what was
|
|
223
|
+
// there and says so.
|
|
224
|
+
? `${provider}: preset matched no models — selection unchanged (fallback to all)`
|
|
225
|
+
: all
|
|
226
|
+
? `${provider}: showing all models (allowlist cleared)`
|
|
227
|
+
: `${provider}: preset v${result.appliedVersion ?? "?"} applied — ${selectedIds.length} models selected`;
|
|
228
|
+
printData(result, wantsJson, [line]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function newPolicy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
232
|
+
const args = [...argv];
|
|
233
|
+
const state = args[0] && !args[0].startsWith("--") ? args.shift()!.toLowerCase() : undefined;
|
|
234
|
+
const provider = takeOption(args, "--provider")?.trim();
|
|
235
|
+
const wantsJson = takeFlag(args, "--json");
|
|
236
|
+
if (state !== undefined && state !== "on" && state !== "off") throw new CliUsageError("new policy must be on or off", USAGE);
|
|
237
|
+
rejectArgs(args, USAGE);
|
|
238
|
+
if (!state) {
|
|
239
|
+
const result = await runtimeRequest<{ policy: string; providers: Record<string, string> }>("/api/model-discovery", {}, deps);
|
|
240
|
+
const value = provider ? result.providers[provider] ?? "inherit" : result.policy;
|
|
241
|
+
printData(provider ? { provider, policy: value } : result, wantsJson, [`${provider ?? "global"}: ${value}`]);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const result = await runtimeRequest<{ baselineBootstrapped?: boolean }>("/api/model-discovery", {
|
|
245
|
+
method: "PUT", body: JSON.stringify({ policy: state, provider: provider ?? null }),
|
|
246
|
+
}, deps);
|
|
247
|
+
printData(result, wantsJson, [`${provider ?? "global"}: ${state}${result.baselineBootstrapped ? " (current models recorded as known)" : ""}`]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function newArrivals(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
251
|
+
const args = [...argv]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
|
|
252
|
+
const result = await runtimeRequest<{ recentArrivals: Record<string, Array<{ id: string; at: string; state: string }>> }>("/api/model-discovery", {}, deps);
|
|
253
|
+
const lines = Object.entries(result.recentArrivals).flatMap(([provider, rows]) => rows.map(row => `${provider}/${row.id} [${row.state}] ${row.at}`));
|
|
254
|
+
printData(result.recentArrivals, wantsJson, lines.length ? lines : ["no recent model arrivals"]);
|
|
255
|
+
}
|
|
256
|
+
|
|
165
257
|
async function context(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
166
258
|
const args = [...argv];
|
|
167
259
|
const action = (args.shift() ?? "status").toLowerCase();
|
|
@@ -236,6 +328,9 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
|
|
|
236
328
|
else if (sub === "disable") action = () => visibility(false, argv, deps);
|
|
237
329
|
else if (sub === "provider") action = () => providerVisibility(argv, deps);
|
|
238
330
|
else if (sub === "selected") action = () => selected(argv, deps);
|
|
331
|
+
else if (sub === "preset") action = () => preset(argv, deps);
|
|
332
|
+
else if (sub === "new-policy") action = () => newPolicy(argv, deps);
|
|
333
|
+
else if (sub === "new-arrivals") action = () => newArrivals(argv, deps);
|
|
239
334
|
else if (sub === "context") action = () => context(argv, deps);
|
|
240
335
|
else if (sub === "shadow") action = () => shadow(argv, deps);
|
|
241
336
|
if (!action) return null;
|
package/src/cli/models.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
isDeclaredReasoningEffort,
|
|
12
12
|
modelRecordValue,
|
|
13
13
|
} from "../reasoning-effort";
|
|
14
|
-
import { encodedModelIdCollides,
|
|
14
|
+
import { encodedModelIdCollides, resolveSlugSelection, routedSlug } from "../providers/slug-codec";
|
|
15
15
|
import { knownModelIdsForProvider } from "../router";
|
|
16
16
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
17
17
|
import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
|
|
@@ -277,11 +277,17 @@ async function handleCustomRemove(args: string[]): Promise<void> {
|
|
|
277
277
|
|
|
278
278
|
const config = loadConfig();
|
|
279
279
|
const existing = config.customModels ?? [];
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
// Slug matching goes through the shared resolver so this command sees the same collision
|
|
281
|
+
// class catalog filtering and persisted sync see (#2491). `slugEquals` compares the raw and
|
|
282
|
+
// encoded spellings of ONE id, so a selector written in the native slash form matched only
|
|
283
|
+
// that row while the dash form matched both — the two relations disagreed on the same
|
|
284
|
+
// config. Removal stays exact-or-refuse: an ambiguous selector still aborts below, which is
|
|
285
|
+
// the right default for a destructive command.
|
|
286
|
+
const matchingIndexes = existing.flatMap((model, index) => {
|
|
287
|
+
if (!target.includes("/")) return model.id === target ? [index] : [];
|
|
288
|
+
const resolved = resolveSlugSelection(model.provider, target, [model.modelId]);
|
|
289
|
+
return resolved.matched.length > 0 ? [index] : [];
|
|
290
|
+
});
|
|
285
291
|
if (matchingIndexes.length === 0) fail(`custom model "${target}" not found`);
|
|
286
292
|
if (matchingIndexes.length > 1) {
|
|
287
293
|
fail(`custom model selector "${target}" is ambiguous; use the custom model id`);
|
|
@@ -422,7 +428,7 @@ export async function handleModels(args: string[]): Promise<void> {
|
|
|
422
428
|
handleCustomList(rest);
|
|
423
429
|
return;
|
|
424
430
|
}
|
|
425
|
-
if (["live", "edit", "enable", "disable", "provider", "selected", "context", "shadow"].includes(subcommand ?? "")) {
|
|
431
|
+
if (["live", "edit", "enable", "disable", "provider", "selected", "preset", "context", "shadow"].includes(subcommand ?? "")) {
|
|
426
432
|
const { handleModelsRuntimeCommand } = await import("./models-runtime");
|
|
427
433
|
const code = await handleModelsRuntimeCommand(subcommand!, rest);
|
|
428
434
|
if (code !== null) process.exitCode = code;
|
|
@@ -11,6 +11,13 @@ import {
|
|
|
11
11
|
takeOption,
|
|
12
12
|
type RuntimeApiDeps,
|
|
13
13
|
} from "./runtime-api";
|
|
14
|
+
import { providerQuotaLine } from "./account-extended";
|
|
15
|
+
import type { ProviderQuotaReportDto } from "./account-api";
|
|
16
|
+
|
|
17
|
+
interface ProviderQuotasDto {
|
|
18
|
+
generatedAt?: number;
|
|
19
|
+
reports?: ProviderQuotaReportDto[];
|
|
20
|
+
}
|
|
14
21
|
|
|
15
22
|
const USAGE = `Usage:
|
|
16
23
|
ocx provider edit <name> [--adapter <id>] [--base-url <url>] [--default-model <id|->]
|
|
@@ -107,8 +114,15 @@ async function quota(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
107
114
|
const wantsJson = takeFlag(args, "--json");
|
|
108
115
|
const refresh = takeFlag(args, "--refresh");
|
|
109
116
|
rejectArgs(args, USAGE);
|
|
110
|
-
const result = await runtimeRequest(`/api/provider-quotas${refresh ? "?refresh=1" : ""}`, {}, deps);
|
|
111
|
-
|
|
117
|
+
const result = await runtimeRequest<ProviderQuotasDto>(`/api/provider-quotas${refresh ? "?refresh=1" : ""}`, {}, deps);
|
|
118
|
+
// `summaryLines` is a depth-1 flattener: it renders a non-scalar array as "N item(s)", which
|
|
119
|
+
// collapsed the whole report to a count and made the command useless for its stated purpose
|
|
120
|
+
// (#2565). Render one line per report with the same formatter `ocx account refresh` uses.
|
|
121
|
+
const reports = Array.isArray(result?.reports) ? result.reports : [];
|
|
122
|
+
const lines = reports.length > 0
|
|
123
|
+
? reports.map(report => providerQuotaLine(report.provider, report))
|
|
124
|
+
: ["no quota reports available"];
|
|
125
|
+
printData(result, wantsJson, lines);
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
async function presets(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
package/src/cli/registry.ts
CHANGED
|
@@ -165,10 +165,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
165
165
|
"A selection-order change applies from the next unbound request and never moves a bound thread.",
|
|
166
166
|
],
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
name: "alias",
|
|
170
|
+
usage: "ocx alias <list|set|rm|defaults> ...",
|
|
171
|
+
summary: "Manage short provider and model names.",
|
|
172
|
+
},
|
|
168
173
|
{
|
|
169
174
|
name: "models",
|
|
170
175
|
aliases: ["model"],
|
|
171
|
-
usage: "ocx models <list|live|add|edit|remove|enable|disable|provider|selected|context|shadow> ...",
|
|
176
|
+
usage: "ocx models <list|live|add|edit|remove|enable|disable|provider|selected|preset|context|shadow> ...",
|
|
172
177
|
summary: "List models and manage custom (manually registered) models.",
|
|
173
178
|
details: [
|
|
174
179
|
"List available models from static config with no subcommand (liveModels may add more at runtime).",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TelemetryLedger } from "../telemetry/ledger";
|
|
2
|
+
import type { OcxConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
export function runTelemetryCommand(args: string[], config: OcxConfig): number {
|
|
5
|
+
if (args[0] !== "status") {
|
|
6
|
+
console.error("Usage: ocx telemetry status");
|
|
7
|
+
return 1;
|
|
8
|
+
}
|
|
9
|
+
const ledger = new TelemetryLedger();
|
|
10
|
+
try {
|
|
11
|
+
const records = ledger.listRecords();
|
|
12
|
+
console.log(`Telemetry failures: ${records.length}`);
|
|
13
|
+
for (const record of records) {
|
|
14
|
+
console.log(`${record.fingerprint} ${record.status} count=${record.count} lastSeen=${new Date(record.lastSeen).toISOString()}`);
|
|
15
|
+
}
|
|
16
|
+
if ((config as OcxConfig & { autonomousRemediation?: { enabled?: boolean; instanceId?: string } }).autonomousRemediation?.enabled === true) {
|
|
17
|
+
console.log("Autonomous remediation: enabled");
|
|
18
|
+
} else {
|
|
19
|
+
console.log("Autonomous remediation: disabled");
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
} finally {
|
|
23
|
+
ledger.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/cli/v2.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { dirname } from "node:path";
|
|
|
15
15
|
import { activeCodexConfigPath, getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
|
|
16
16
|
|
|
17
17
|
import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
|
|
18
|
-
import { loadConfig, saveConfig } from "../config";
|
|
18
|
+
import { deleteConfigTopLevelKey, loadConfig, saveConfig } from "../config";
|
|
19
19
|
import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
|
|
20
20
|
|
|
21
21
|
export interface V2CliDeps {
|
|
@@ -88,18 +88,24 @@ function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
|
|
|
88
88
|
|
|
89
89
|
export function v2StatusLine(enabled: boolean): string {
|
|
90
90
|
return enabled
|
|
91
|
-
? "multi_agent_v2: ON —
|
|
92
|
-
: "multi_agent_v2: OFF —
|
|
91
|
+
? "multi_agent_v2: ON — global V2 override active"
|
|
92
|
+
: "multi_agent_v2: OFF — model catalog pins and defaults decide the surface";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export function multiAgentModeLine(mode: string): string {
|
|
95
|
+
export function multiAgentModeLine(mode: string, keepNativeChatGptOnV1 = false): string {
|
|
96
96
|
switch (mode) {
|
|
97
97
|
case "v1": return "multi_agent_mode: v1 — ALL models forced to v1 surface (upstream pins overridden)";
|
|
98
|
-
case "v2": return
|
|
98
|
+
case "v2": return keepNativeChatGptOnV1
|
|
99
|
+
? "multi_agent_mode: v2 hybrid — ChatGPT-native models use v1; routed models use v2"
|
|
100
|
+
: "multi_agent_mode: v2 — ALL models forced to v2 surface (upstream pins overridden)";
|
|
99
101
|
default: return "multi_agent_mode: default — upstream model pins respected (sol/terra=v2, luna=v1, rest=codex flag)";
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
function requiresGlobalV2Disabled(multiAgentMode: string | undefined, keepNativeChatGptOnV1: boolean): boolean {
|
|
106
|
+
return multiAgentMode === "v2" && keepNativeChatGptOnV1;
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
|
|
104
110
|
const log = deps.log ?? console;
|
|
105
111
|
const isEnabled = deps.isEnabled ?? isMultiAgentV2Enabled;
|
|
@@ -109,9 +115,13 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
109
115
|
if (verb === "status") {
|
|
110
116
|
log.log(v2StatusLine(isEnabled()));
|
|
111
117
|
const cfg = loadConfig();
|
|
112
|
-
|
|
118
|
+
const mode = cfg.multiAgentMode ?? "default";
|
|
119
|
+
const keepNativeV1 = cfg.keepNativeChatGptOnV1 === true;
|
|
120
|
+
log.log(multiAgentModeLine(mode, keepNativeV1));
|
|
113
121
|
log.log(cfg.keepNativeChatGptOnV1 === true
|
|
114
|
-
?
|
|
122
|
+
? requiresGlobalV2Disabled(mode, keepNativeV1) && isEnabled()
|
|
123
|
+
? "keep_native_chatgpt_on_v1: CONFLICT — global multi_agent_v2 overrides the native v1 catalog pin; run 'ocx v2 keep-native-v1 on' to reconcile"
|
|
124
|
+
: "keep_native_chatgpt_on_v1: ON — global V2 override is off; ChatGPT-native rows use v1 and routed rows use v2 when mode is v2"
|
|
115
125
|
: "keep_native_chatgpt_on_v1: OFF");
|
|
116
126
|
const threads = getLogicalMaxThreads();
|
|
117
127
|
log.log(`max_threads: ${threads ?? "(unset — codex default)"}`);
|
|
@@ -186,14 +196,14 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
186
196
|
}
|
|
187
197
|
const cfg = loadConfig();
|
|
188
198
|
if (modeArg !== "default") {
|
|
189
|
-
const target = modeArg === "v2";
|
|
199
|
+
const target = modeArg === "v2" && cfg.keepNativeChatGptOnV1 !== true;
|
|
190
200
|
const transition = transitionMultiAgentV2(target, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
|
|
191
201
|
if (!transition.ok) {
|
|
192
202
|
log.error(`multi-agent mode transition failed: ${transition.error}`);
|
|
193
203
|
return 1;
|
|
194
204
|
}
|
|
195
205
|
}
|
|
196
|
-
if (modeArg === "default")
|
|
206
|
+
if (modeArg === "default") deleteConfigTopLevelKey(cfg, "multiAgentMode");
|
|
197
207
|
else cfg.multiAgentMode = modeArg as "v1" | "v2";
|
|
198
208
|
saveConfig(cfg);
|
|
199
209
|
try {
|
|
@@ -216,8 +226,15 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
216
226
|
const cfg = loadConfig();
|
|
217
227
|
const next = flag === "on";
|
|
218
228
|
const already = cfg.keepNativeChatGptOnV1 === true === next;
|
|
229
|
+
if (next && requiresGlobalV2Disabled(cfg.multiAgentMode, true)) {
|
|
230
|
+
const transition = transitionMultiAgentV2(false, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
|
|
231
|
+
if (!transition.ok) {
|
|
232
|
+
log.error(`keep-native-v1 transition failed: ${transition.error}`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
219
236
|
if (next) cfg.keepNativeChatGptOnV1 = true;
|
|
220
|
-
else
|
|
237
|
+
else deleteConfigTopLevelKey(cfg, "keepNativeChatGptOnV1");
|
|
221
238
|
saveConfig(cfg);
|
|
222
239
|
try {
|
|
223
240
|
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
|
|
@@ -243,6 +260,13 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
243
260
|
}
|
|
244
261
|
|
|
245
262
|
const want = verb === "on";
|
|
263
|
+
if (want) {
|
|
264
|
+
const cfg = loadConfig();
|
|
265
|
+
if (requiresGlobalV2Disabled(cfg.multiAgentMode, cfg.keepNativeChatGptOnV1 === true)) {
|
|
266
|
+
log.error("v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first.");
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
246
270
|
const transition = transitionMultiAgentV2(want, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
|
|
247
271
|
if (!transition.ok) {
|
|
248
272
|
log.error(`codex features ${want ? "enable" : "disable"} multi_agent_v2 failed: ${transition.error}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
2
3
|
|
|
3
4
|
/** Whether an account is administratively excluded from future pool selection. */
|
|
4
5
|
export function isCodexAccountPaused(config: OcxConfig, accountId: string): boolean {
|
|
@@ -12,7 +13,7 @@ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paus
|
|
|
12
13
|
else pausedIds.delete(accountId);
|
|
13
14
|
|
|
14
15
|
if (pausedIds.size > 0) config.pausedCodexAccountIds = [...pausedIds];
|
|
15
|
-
else
|
|
16
|
+
else deleteConfigTopLevelKey(config, "pausedCodexAccountIds");
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function forgetCodexAccountPause(config: OcxConfig, accountId: string): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
2
2
|
import { DEFAULT_ACCOUNT_PRIORITY, normalizeAccountPriority } from "./pool-rotation";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Which ids may carry a selection order: any pool account, plus the synthetic
|
|
@@ -34,7 +35,7 @@ export function setCodexAccountPriority(config: OcxConfig, accountId: string, pr
|
|
|
34
35
|
else entries.set(accountId, priority);
|
|
35
36
|
|
|
36
37
|
if (entries.size > 0) config.codexAccountPriorities = Object.fromEntries(entries);
|
|
37
|
-
else
|
|
38
|
+
else deleteConfigTopLevelKey(config, "codexAccountPriorities");
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function forgetCodexAccountPriority(config: OcxConfig, accountId: string): void {
|
|
@@ -78,6 +79,6 @@ export function setCodexAccountPin(config: OcxConfig, accountId: string): void {
|
|
|
78
79
|
/** Release the pin. With `accountId` given, only when it is the pinned account. */
|
|
79
80
|
export function clearCodexAccountPin(config: OcxConfig, accountId?: string): void {
|
|
80
81
|
if (accountId === undefined || config.activeCodexAccountPinned === accountId) {
|
|
81
|
-
|
|
82
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -260,6 +260,23 @@ export function isCodexAppServerCommandLine(commandLine: string, executable?: st
|
|
|
260
260
|
}
|
|
261
261
|
if (tokens.length === 0) return false;
|
|
262
262
|
if (isCodeModeHostProcess(tokens)) return true;
|
|
263
|
+
// An npm-installed Codex runs as a PAIR: `node /usr/local/bin/codex app-server` and the
|
|
264
|
+
// vendored native binary that wrapper spawns. Only the child used to match, so
|
|
265
|
+
// `--restart-codex` signalled the child while its supervisor kept holding the socket -
|
|
266
|
+
// which is what "PID(s) still running after SIGTERM" was reporting on Linux.
|
|
267
|
+
//
|
|
268
|
+
// The codex-shaped token must be IMMEDIATELY next. Skipping interpreter flags to reach
|
|
269
|
+
// it looks tempting and is wrong: interpreter options take values, so a generic skip
|
|
270
|
+
// reads the value of `node --require codex app-server worker.js` as the entrypoint.
|
|
271
|
+
// Supporting flags needs a real Node/Bun/Deno entrypoint parser, not a loop over
|
|
272
|
+
// hyphens; the observed wrappers put the path first, so this stays narrow on purpose.
|
|
273
|
+
//
|
|
274
|
+
// Dropping only the interpreter and re-running the ordinary scan is what preserves the
|
|
275
|
+
// subcommand discipline below: `node <codex> exec 'hi'` stays unmatched exactly like
|
|
276
|
+
// `codex exec 'hi'` does.
|
|
277
|
+
if (isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodexExecutableToken(tokens[1]!)) {
|
|
278
|
+
tokens = tokens.slice(1);
|
|
279
|
+
}
|
|
263
280
|
if (!isCodexExecutableToken(tokens[0]!)) return false;
|
|
264
281
|
|
|
265
282
|
let i = 1;
|
|
@@ -740,6 +757,28 @@ const CATALOG_STATE_TTL_MS = 5_000;
|
|
|
740
757
|
*/
|
|
741
758
|
const CATALOG_STATE_UNKNOWN_TTL_MS = 250;
|
|
742
759
|
|
|
760
|
+
/**
|
|
761
|
+
* How long a real observation may still be SERVED after it expires, while a refresh
|
|
762
|
+
* runs behind it (#2499).
|
|
763
|
+
*
|
|
764
|
+
* The probe is advisory and, on Windows, slow: `Invoke-CimMethod GetOwner` costs
|
|
765
|
+
* ~0.4s per candidate process, so a cold probe routinely outlives the 5s TTL and
|
|
766
|
+
* every turn that misses the cache pays for it on the request path. Serving the
|
|
767
|
+
* previous reading immediately keeps that cost off the turn without pretending it is
|
|
768
|
+
* fresh -- the refresh it triggers is what makes the next reading current.
|
|
769
|
+
*
|
|
770
|
+
* Measured from expiry rather than from when the reading was taken, so a `fresh`
|
|
771
|
+
* entry stays servable for its own TTL plus this bound. Anchoring it to expiry keeps
|
|
772
|
+
* the stale window independent of the TTL -- a cap on total age would quietly turn
|
|
773
|
+
* this path off if the TTL were ever raised past it.
|
|
774
|
+
*
|
|
775
|
+
* Bounded rather than unlimited: if the refresh keeps failing, an observation this
|
|
776
|
+
* old stops being evidence about the machine and it is better to wait for a real one.
|
|
777
|
+
* `unknown` is never served this way -- it is a failure to observe, not an
|
|
778
|
+
* observation, and it already has its own short window for exactly that reason.
|
|
779
|
+
*/
|
|
780
|
+
export const CATALOG_STATE_MAX_STALE_MS = 60_000;
|
|
781
|
+
|
|
743
782
|
export function catalogStateTtlMs(state: CodexAppServerCatalogState): number {
|
|
744
783
|
return state === "unknown" ? CATALOG_STATE_UNKNOWN_TTL_MS : CATALOG_STATE_TTL_MS;
|
|
745
784
|
}
|
|
@@ -835,6 +874,14 @@ export function collectCodexAppServerCatalogState(
|
|
|
835
874
|
* - 선택한 방식: retain the synchronous API and use async PowerShell plus an identity-scoped in-flight refresh, short cache, and invalidation generation only for Windows requests.
|
|
836
875
|
* - 다른 대안 대신 이 방식을 선택한 이유: it fixes unrelated `/healthz` starvation without widening the process-matching or restart contract.
|
|
837
876
|
* - 장점, 단점 및 영향: concurrent turns share one CIM walk, invalidated pre-write results cannot repopulate the cache, and the event loop stays responsive; a cold v2 turn can still await the bounded advisory probe.
|
|
877
|
+
*
|
|
878
|
+
* [Decision Log · #2499]
|
|
879
|
+
* - 목적과 의도: a cold probe outlives its own 5s TTL on Windows (~435ms per candidate process for `Invoke-CimMethod GetOwner`), so the cache expires before it can serve and the miss lands on a turn.
|
|
880
|
+
* - 기존 구현 및 제약 조건: the reading is advisory, and only `fresh` authorizes positive guidance (`src/server/responses/collaboration.ts`); `unknown` is a failure to observe rather than an observation.
|
|
881
|
+
* - 검토한 주요 대안: drop the per-process GetOwner fan-out (issue suggestion 1), or widen the TTL past the probe duration (suggestion 3).
|
|
882
|
+
* - 선택한 방식: serve an expired reading immediately when its generation still matches, bounded by `CATALOG_STATE_MAX_STALE_MS`, never for `unknown`, and refresh behind it; a failed refresh no longer evicts the reading it was refreshing.
|
|
883
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: the fan-out change alters what "could not verify the owner" means for the current-user scoping contract and needs its own ground-truth comparison; a wider TTL still pays the probe on every human-paced turn.
|
|
884
|
+
* - 장점, 단점 및 영향: after the first probe the request path never waits; a server that stopped between readings can be described as `fresh` for up to the stale bound; a catalog write still invalidates immediately through the generation, and the cold path is unchanged.
|
|
838
885
|
*/
|
|
839
886
|
export async function collectCodexAppServerCatalogStateForRequest(
|
|
840
887
|
io: CodexAppServerProcessIo = {},
|
|
@@ -853,16 +900,30 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
853
900
|
catalogMtimeMs: io.catalogMtimeMs,
|
|
854
901
|
now: io.now,
|
|
855
902
|
};
|
|
856
|
-
|
|
903
|
+
const cached = requestCatalogStateCache
|
|
857
904
|
&& requestCatalogStateCache.generation === generation
|
|
858
905
|
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity)
|
|
859
|
-
|
|
860
|
-
|
|
906
|
+
? requestCatalogStateCache
|
|
907
|
+
: null;
|
|
908
|
+
if (cached && now - cached.atMs < catalogStateTtlMs(cached.status.state)) {
|
|
909
|
+
return cached.status;
|
|
861
910
|
}
|
|
911
|
+
// An expired real reading is still worth handing back while the refresh runs. It
|
|
912
|
+
// cannot have been invalidated by an ocx catalog write: every such write calls
|
|
913
|
+
// `resetCodexAppServerCatalogStateCache`, which advances the generation and drops
|
|
914
|
+
// this entry, so a generation match means no write has landed since it was taken.
|
|
915
|
+
// What it can miss is an app-server that started or stopped meanwhile -- and a
|
|
916
|
+
// server started after the reading is newer than the catalog, which is the `fresh`
|
|
917
|
+
// this entry already says.
|
|
918
|
+
const servableStale = cached
|
|
919
|
+
&& cached.status.state !== "unknown"
|
|
920
|
+
&& now - cached.atMs < catalogStateTtlMs(cached.status.state) + CATALOG_STATE_MAX_STALE_MS
|
|
921
|
+
? cached.status
|
|
922
|
+
: null;
|
|
862
923
|
if (requestCatalogStateFlight
|
|
863
924
|
&& requestCatalogStateFlight.generation === generation
|
|
864
925
|
&& sameRequestCatalogStateIdentity(requestCatalogStateFlight.identity, identity)) {
|
|
865
|
-
return requestCatalogStateFlight.promise;
|
|
926
|
+
return servableStale ?? requestCatalogStateFlight.promise;
|
|
866
927
|
}
|
|
867
928
|
|
|
868
929
|
const refresh = async (): Promise<CodexAppServerCatalogStatus> => {
|
|
@@ -920,7 +981,18 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
920
981
|
if (requestCatalogStateGeneration !== generation) {
|
|
921
982
|
return { state: "unknown" as const, processes: [], catalogMtimeMs: null };
|
|
922
983
|
}
|
|
923
|
-
|
|
984
|
+
// A refresh that failed must not evict a real reading. Before this function
|
|
985
|
+
// served stale entries, caching `unknown` cost at most the 250ms that state
|
|
986
|
+
// is allowed to live. Now that an expired observation is what callers are
|
|
987
|
+
// handed, overwriting one with `unknown` would take the answer AWAY from
|
|
988
|
+
// them on a transient failure -- `unknown` is not servable, so the next
|
|
989
|
+
// caller waits for a probe instead of getting the reading it would have had.
|
|
990
|
+
// Keep the observation and let its own age retire it.
|
|
991
|
+
const wouldEvictAnObservation = status.state === "unknown"
|
|
992
|
+
&& requestCatalogStateCache?.generation === generation
|
|
993
|
+
&& requestCatalogStateCache.status.state !== "unknown"
|
|
994
|
+
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity);
|
|
995
|
+
if (requestCatalogStateFlight === flight && !wouldEvictAnObservation) {
|
|
924
996
|
requestCatalogStateCache = {
|
|
925
997
|
generation,
|
|
926
998
|
identity,
|
|
@@ -934,7 +1006,9 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
934
1006
|
});
|
|
935
1007
|
flight = { generation, identity, promise };
|
|
936
1008
|
requestCatalogStateFlight = flight;
|
|
937
|
-
|
|
1009
|
+
// `promise` already absorbs its own failures, so leaving it unawaited here cannot
|
|
1010
|
+
// surface as an unhandled rejection; the next caller picks up whatever it stored.
|
|
1011
|
+
return servableStale ?? flight.promise;
|
|
938
1012
|
}
|
|
939
1013
|
|
|
940
1014
|
/**
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -209,21 +209,61 @@ function codexAccountPersistenceConflict(
|
|
|
209
209
|
: undefined;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The exact label `parseUsageQuota` emits for the Codex Spark window (quota.ts).
|
|
214
|
+
* Matching on the label rather than on "is a custom window" is load-bearing: the same array
|
|
215
|
+
* carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet,
|
|
216
|
+
* Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters.
|
|
217
|
+
*/
|
|
218
|
+
const CODEX_SPARK_WINDOW_LABEL = "GPT-5.3-Codex-Spark Weekly";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Drop the Spark window unless the operator asked for it (default hidden).
|
|
222
|
+
*
|
|
223
|
+
* Applied at the DTO boundary, never at parse or cache time: custom windows participate in
|
|
224
|
+
* quota-presence checks, snapshot reconciliation and capacity aggregation, so removing Spark
|
|
225
|
+
* upstream of this point would change routing state rather than display.
|
|
226
|
+
*
|
|
227
|
+
* Both GUI surfaces funnel through here — the Codex Auth rows directly, and /api/provider-quotas
|
|
228
|
+
* via listCodexAuthAccountsSnapshot — so one filter covers both. Filtering only one would leave
|
|
229
|
+
* the other still rendering the row the operator switched off.
|
|
230
|
+
*/
|
|
231
|
+
export function withSparkVisibility<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
232
|
+
quota: T,
|
|
233
|
+
): T {
|
|
234
|
+
if (!quota?.customWindows?.length) return quota;
|
|
235
|
+
if (loadConfig().showCodexSparkQuota === true) return quota;
|
|
236
|
+
const kept = quota.customWindows.filter(window => window.label !== CODEX_SPARK_WINDOW_LABEL);
|
|
237
|
+
if (kept.length === quota.customWindows.length) return quota;
|
|
238
|
+
// An empty list is dropped rather than serialized: an absent field and an empty array should
|
|
239
|
+
// not be two different ways of saying "no custom windows" on the wire.
|
|
240
|
+
const next = { ...quota } as Record<string, unknown>;
|
|
241
|
+
if (kept.length > 0) next.customWindows = kept;
|
|
242
|
+
else delete next.customWindows;
|
|
243
|
+
return next as T;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
212
247
|
function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
213
248
|
quota: T,
|
|
214
249
|
plan: unknown,
|
|
215
250
|
): T {
|
|
216
|
-
|
|
251
|
+
const visible = withSparkVisibility(quota);
|
|
252
|
+
if (!visible || !isThirtyDayOnlyCodexPlan(plan)) return visible;
|
|
253
|
+
const quotaWindows = visible;
|
|
217
254
|
return {
|
|
218
|
-
...(
|
|
219
|
-
...(
|
|
255
|
+
...(quotaWindows.monthlyPercent !== undefined ? { monthlyPercent: quotaWindows.monthlyPercent } : {}),
|
|
256
|
+
...(quotaWindows.monthlyResetAt !== undefined ? { monthlyResetAt: quotaWindows.monthlyResetAt } : {}),
|
|
220
257
|
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
221
258
|
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
222
|
-
...(
|
|
223
|
-
...(
|
|
224
|
-
...(
|
|
225
|
-
...(
|
|
226
|
-
...(
|
|
259
|
+
...(quotaWindows.fiveHourPercent !== undefined ? { fiveHourPercent: quotaWindows.fiveHourPercent } : {}),
|
|
260
|
+
...(quotaWindows.fiveHourResetAt !== undefined ? { fiveHourResetAt: quotaWindows.fiveHourResetAt } : {}),
|
|
261
|
+
...(quotaWindows.shortPercent !== undefined ? { shortPercent: quotaWindows.shortPercent } : {}),
|
|
262
|
+
...(quotaWindows.shortResetAt !== undefined ? { shortResetAt: quotaWindows.shortResetAt } : {}),
|
|
263
|
+
...(quotaWindows.shortWindowSeconds !== undefined ? { shortWindowSeconds: quotaWindows.shortWindowSeconds } : {}),
|
|
264
|
+
...(quotaWindows.customWindows !== undefined ? { customWindows: quotaWindows.customWindows } : {}),
|
|
265
|
+
...(quotaWindows.resetCredits !== undefined ? { resetCredits: quotaWindows.resetCredits } : {}),
|
|
266
|
+
...("updatedAt" in quotaWindows ? { updatedAt: quotaWindows.updatedAt } : {}),
|
|
227
267
|
} as T;
|
|
228
268
|
}
|
|
229
269
|
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
entitledCodexAccountIdsForModel,
|
|
30
30
|
isDirectCallerEntitledToCodexModel,
|
|
31
31
|
resolveCodexModelEntitlements,
|
|
32
|
-
type CodexModelEntitlementSnapshot,
|
|
33
32
|
} from "./model-entitlements";
|
|
34
33
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
35
34
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
@@ -334,9 +333,7 @@ export interface ResolveCodexAuthContextOptions {
|
|
|
334
333
|
getMainAccountToken?: typeof getMainAccountToken;
|
|
335
334
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
336
335
|
/** Test seam for account-gated native model discovery. */
|
|
337
|
-
resolveCodexModelEntitlements?:
|
|
338
|
-
config: Pick<OcxConfig, "codexAccounts">,
|
|
339
|
-
) => Promise<CodexModelEntitlementSnapshot>;
|
|
336
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
340
337
|
/** Direct requests admitted with a proxy bearer substitute the stored native-main credential. */
|
|
341
338
|
substituteMainCredentialForDirect?: boolean;
|
|
342
339
|
/** Test seam for a Direct request's own forwarded ChatGPT credential. */
|
|
@@ -381,28 +378,34 @@ export async function resolveCodexAuthContext(
|
|
|
381
378
|
return { kind: "main", accountId: null };
|
|
382
379
|
}
|
|
383
380
|
const affinityKey = fixedAccountId === undefined ? codexPoolAffinityKey(headers) : undefined;
|
|
384
|
-
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
385
|
-
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config)
|
|
386
|
-
: undefined;
|
|
387
|
-
const modelEligibleAccountIds = entitlementSnapshot
|
|
388
|
-
? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
|
|
389
|
-
: undefined;
|
|
390
381
|
// Retained startup recovery makes the physical main identity ineligible. Routing
|
|
391
382
|
// can still preserve service by selecting a healthy configured pool account.
|
|
392
383
|
const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
|
|
393
384
|
const selectionAdmission = options.beginCodexAccountSelection?.();
|
|
394
385
|
const nativeMainReadsForbidden = nativeMainTrafficBlocked || selectionAdmission?.mainProfileDraining === true;
|
|
395
|
-
const selectionOptions = {
|
|
396
|
-
// Temporary switch drain keeps the candidate until the atomic claim rejects
|
|
397
|
-
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
398
|
-
nativeMainSelectionOnly: !nativeMainTrafficBlocked
|
|
399
|
-
&& selectionAdmission?.mainProfileDraining === true,
|
|
400
|
-
isMainAccountTokenLive: options.isMainAccountTokenLive,
|
|
401
|
-
modelEligibleAccountIds,
|
|
402
|
-
};
|
|
403
386
|
let accountId: string;
|
|
404
387
|
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
405
388
|
try {
|
|
389
|
+
const excludeAccountIds = nativeMainReadsForbidden
|
|
390
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
391
|
+
: undefined;
|
|
392
|
+
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
393
|
+
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, { excludeAccountIds })
|
|
394
|
+
: undefined;
|
|
395
|
+
const entitledAccountIds = entitlementSnapshot
|
|
396
|
+
? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
|
|
397
|
+
: undefined;
|
|
398
|
+
const modelEligibleAccountIds = entitledAccountIds
|
|
399
|
+
? new Set([...entitledAccountIds].filter(candidate => !excludeAccountIds?.has(candidate)))
|
|
400
|
+
: undefined;
|
|
401
|
+
const selectionOptions = {
|
|
402
|
+
// Temporary switch drain keeps the candidate until the atomic claim rejects
|
|
403
|
+
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
404
|
+
nativeMainSelectionOnly: !nativeMainTrafficBlocked
|
|
405
|
+
&& selectionAdmission?.mainProfileDraining === true,
|
|
406
|
+
isMainAccountTokenLive: options.isMainAccountTokenLive,
|
|
407
|
+
modelEligibleAccountIds,
|
|
408
|
+
};
|
|
406
409
|
// A pre-drain selector reserves the native identity while reconciliation and
|
|
407
410
|
// routing inspect it. Selectors arriving after the fence skip reconciliation
|
|
408
411
|
// and may still route to non-main pool accounts without touching switch state.
|