@bitkyc08/opencodex 2.33.0 → 2.34.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-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.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 +40 -3
- 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 +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- 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 +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -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 +32 -4
- package/src/cli/account-extended.ts +1 -1
- 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/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/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 +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- 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 +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- 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/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- 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/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- 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 +28 -0
- 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 +65 -7
- package/src/providers/registry.ts +160 -68
- 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 +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -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/index.ts +77 -11
- 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/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- 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/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
* Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §1.
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
closeSync,
|
|
16
|
+
fsyncSync,
|
|
17
|
+
linkSync,
|
|
18
|
+
lstatSync,
|
|
19
|
+
openSync,
|
|
20
|
+
realpathSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { basename, dirname, join } from "node:path";
|
|
14
24
|
|
|
15
25
|
import { Database } from "bun:sqlite";
|
|
16
26
|
|
|
@@ -38,7 +48,7 @@ import {
|
|
|
38
48
|
} from "./user-identity";
|
|
39
49
|
|
|
40
50
|
export const CODEX_COORDINATOR_SCHEMA_VERSION = 1;
|
|
41
|
-
const DURABLE_HISTORY_STATUSES = new Set(["converged", "pending", "running", "blocked", "unknown"]);
|
|
51
|
+
const DURABLE_HISTORY_STATUSES = new Set(["adoption-pending", "converged", "pending", "running", "blocked", "unknown"]);
|
|
42
52
|
const DURABLE_HISTORY_REASONS = new Set([
|
|
43
53
|
"db-busy",
|
|
44
54
|
"permission",
|
|
@@ -66,7 +76,7 @@ const CREATE_TRANSITION_TABLE = `
|
|
|
66
76
|
history_pending_rows INTEGER,
|
|
67
77
|
history_backup_entries INTEGER,
|
|
68
78
|
updated_at TEXT NOT NULL,
|
|
69
|
-
CHECK (history_status IN ('converged', 'pending', 'running', 'blocked', 'unknown')),
|
|
79
|
+
CHECK (history_status IN ('adoption-pending', 'converged', 'pending', 'running', 'blocked', 'unknown')),
|
|
70
80
|
CHECK (history_reason IS NULL OR history_reason IN
|
|
71
81
|
('db-busy', 'permission', 'unreadable', 'schema', 'timeout',
|
|
72
82
|
'shutdown-cancelled', 'worker-died', 'overtaken', 'record-write-failed')),
|
|
@@ -75,14 +85,20 @@ const CREATE_TRANSITION_TABLE = `
|
|
|
75
85
|
CHECK ((native_generation = 0 AND current_tx_id IS NULL)
|
|
76
86
|
OR (native_generation > 0 AND length(trim(current_tx_id)) > 0)),
|
|
77
87
|
CHECK ((native_generation = 0
|
|
88
|
+
AND history_status = 'unknown'
|
|
78
89
|
AND history_tx_id IS NULL
|
|
79
90
|
AND history_direction IS NULL
|
|
80
91
|
AND history_authority_snapshot_id IS NULL)
|
|
92
|
+
OR (native_generation = 0
|
|
93
|
+
AND history_status = 'adoption-pending'
|
|
94
|
+
AND length(trim(history_tx_id)) > 0
|
|
95
|
+
AND history_direction IS NOT NULL
|
|
96
|
+
AND length(trim(history_authority_snapshot_id)) > 0)
|
|
81
97
|
OR (native_generation > 0
|
|
82
98
|
AND history_tx_id = current_tx_id
|
|
83
99
|
AND history_direction IS NOT NULL
|
|
84
100
|
AND length(trim(history_authority_snapshot_id)) > 0)),
|
|
85
|
-
CHECK (native_generation > 0 OR
|
|
101
|
+
CHECK (native_generation > 0 OR history_status = 'adoption-pending' OR
|
|
86
102
|
(history_status = 'unknown'
|
|
87
103
|
AND history_reason IS NULL
|
|
88
104
|
AND history_attempts = 0
|
|
@@ -100,6 +116,15 @@ const INITIALIZE_TRANSITION_ROW = `
|
|
|
100
116
|
history_backup_entries, updated_at
|
|
101
117
|
) VALUES (1, 0, NULL, 'unknown', NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, ?)`;
|
|
102
118
|
|
|
119
|
+
const INITIALIZE_ADOPTION_ROW = `
|
|
120
|
+
INSERT INTO codex_transition_state (
|
|
121
|
+
singleton, native_generation, current_tx_id,
|
|
122
|
+
history_status, history_reason, history_attempts,
|
|
123
|
+
history_next_retry_at, history_tx_id, history_direction,
|
|
124
|
+
history_authority_snapshot_id, history_pending_rows,
|
|
125
|
+
history_backup_entries, updated_at
|
|
126
|
+
) VALUES (1, 0, NULL, 'adoption-pending', NULL, 0, NULL, ?, ?, ?, NULL, NULL, ?)`;
|
|
127
|
+
|
|
103
128
|
const SELECT_TRANSITION_ROW = `
|
|
104
129
|
SELECT native_generation, current_tx_id,
|
|
105
130
|
history_status, history_reason, history_attempts,
|
|
@@ -210,8 +235,15 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
|
|
|
210
235
|
|
|
211
236
|
const generation = row.native_generation;
|
|
212
237
|
if (generation === 0) {
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
const ordinaryInitial = row.history_status === "unknown"
|
|
239
|
+
&& row.history_tx_id === null
|
|
240
|
+
&& row.history_direction === null
|
|
241
|
+
&& row.history_authority_snapshot_id === null;
|
|
242
|
+
const adoptionInitial = row.history_status === "adoption-pending"
|
|
243
|
+
&& Boolean(row.history_tx_id?.trim())
|
|
244
|
+
&& (row.history_direction === "apply" || row.history_direction === "remove")
|
|
245
|
+
&& Boolean(row.history_authority_snapshot_id?.trim());
|
|
246
|
+
if (row.current_tx_id !== null || (!ordinaryInitial && !adoptionInitial)) {
|
|
215
247
|
throw new CodexCoordinatorTransactionError("The initial coordinator row contains transition metadata.");
|
|
216
248
|
}
|
|
217
249
|
} else if (!row.current_tx_id?.trim()
|
|
@@ -234,13 +266,73 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
|
|
|
234
266
|
nativeGeneration: generation,
|
|
235
267
|
currentTxId: row.current_tx_id,
|
|
236
268
|
history,
|
|
237
|
-
historySchedule: generation === 0 ? null : {
|
|
269
|
+
historySchedule: generation === 0 && row.history_status !== "adoption-pending" ? null : {
|
|
238
270
|
direction: row.history_direction as "apply" | "remove",
|
|
239
271
|
authoritySnapshotId: row.history_authority_snapshot_id as string,
|
|
240
272
|
},
|
|
241
273
|
};
|
|
242
274
|
}
|
|
243
275
|
|
|
276
|
+
export type CodexCoordinatorAdoptionCheckpoint = "temp-created" | "temp-committed" | "published";
|
|
277
|
+
|
|
278
|
+
export interface CodexCoordinatorAdoptionOptions {
|
|
279
|
+
readonly direction: "apply" | "remove";
|
|
280
|
+
readonly onCheckpoint?: (checkpoint: CodexCoordinatorAdoptionCheckpoint) => void;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fsyncPath(path: string): void {
|
|
284
|
+
const fd = openSync(path, "r");
|
|
285
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function publishAdoptionDatabase(
|
|
289
|
+
finalDatabasePath: string,
|
|
290
|
+
options: CodexCoordinatorAdoptionOptions,
|
|
291
|
+
): void {
|
|
292
|
+
const parent = dirname(finalDatabasePath);
|
|
293
|
+
const tempPath = join(parent, `.${basename(finalDatabasePath)}.adopt-${process.pid}-${randomUUID()}.tmp`);
|
|
294
|
+
let tempPresent = false;
|
|
295
|
+
try {
|
|
296
|
+
const fd = openSync(tempPath, "wx", 0o600);
|
|
297
|
+
closeSync(fd);
|
|
298
|
+
tempPresent = true;
|
|
299
|
+
options.onCheckpoint?.("temp-created");
|
|
300
|
+
|
|
301
|
+
const temp = new Database(tempPath, { readwrite: true, create: false });
|
|
302
|
+
try {
|
|
303
|
+
temp.exec("PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
|
|
304
|
+
temp.exec(CREATE_TRANSITION_TABLE);
|
|
305
|
+
temp.query(INITIALIZE_ADOPTION_ROW).run(
|
|
306
|
+
randomUUID(),
|
|
307
|
+
options.direction,
|
|
308
|
+
randomUUID(),
|
|
309
|
+
new Date().toISOString(),
|
|
310
|
+
);
|
|
311
|
+
temp.exec(`PRAGMA user_version = ${CODEX_COORDINATOR_SCHEMA_VERSION}; COMMIT`);
|
|
312
|
+
readCodexCoordinatorState(temp);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
try { temp.exec("ROLLBACK"); } catch { /* commit may already have completed */ }
|
|
315
|
+
throw error;
|
|
316
|
+
} finally {
|
|
317
|
+
temp.close();
|
|
318
|
+
}
|
|
319
|
+
fsyncPath(tempPath);
|
|
320
|
+
options.onCheckpoint?.("temp-committed");
|
|
321
|
+
|
|
322
|
+
linkSync(tempPath, finalDatabasePath);
|
|
323
|
+
options.onCheckpoint?.("published");
|
|
324
|
+
if (process.platform !== "win32") fsyncPath(parent);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
327
|
+
} finally {
|
|
328
|
+
if (tempPresent) {
|
|
329
|
+
try { unlinkSync(tempPath); } catch (error) {
|
|
330
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
export function readCodexCoordinatorState(database: Database): CodexTransitionState {
|
|
245
337
|
const row = database.query<TransitionRow, []>(SELECT_TRANSITION_ROW).get();
|
|
246
338
|
return rowToState(row);
|
|
@@ -346,7 +438,10 @@ function createCapability(
|
|
|
346
438
|
};
|
|
347
439
|
}
|
|
348
440
|
|
|
349
|
-
export function openCodexCoordinatorTransaction(
|
|
441
|
+
export function openCodexCoordinatorTransaction(
|
|
442
|
+
finalDatabasePath: string,
|
|
443
|
+
adoption?: CodexCoordinatorAdoptionOptions,
|
|
444
|
+
): CodexCoordinatorTransactionController {
|
|
350
445
|
let database: Database | undefined;
|
|
351
446
|
let transactionOpen = false;
|
|
352
447
|
let closed = false;
|
|
@@ -389,6 +484,10 @@ export function openCodexCoordinatorTransaction(finalDatabasePath: string): Code
|
|
|
389
484
|
if (errorCode(cause) !== "ENOENT") throw cause;
|
|
390
485
|
databaseWasAbsent = true;
|
|
391
486
|
}
|
|
487
|
+
if (databaseWasAbsent && adoption) {
|
|
488
|
+
publishAdoptionDatabase(finalDatabasePath, adoption);
|
|
489
|
+
databaseWasAbsent = false;
|
|
490
|
+
}
|
|
392
491
|
database = new Database(finalDatabasePath, { create: true });
|
|
393
492
|
if (databaseWasAbsent) {
|
|
394
493
|
try { chmodSync(finalDatabasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
|
package/src/combos/types.ts
CHANGED
|
@@ -203,6 +203,16 @@ export function comboConfigIssues(
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
const body = raw as Record<string, unknown>;
|
|
206
|
+
if (typeof body.alias === "string") {
|
|
207
|
+
const alias = body.alias.toLowerCase();
|
|
208
|
+
for (const [providerName, provider] of Object.entries(providers)) {
|
|
209
|
+
if (provider.alias?.toLowerCase() === alias) {
|
|
210
|
+
issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by provider "${providerName}"` });
|
|
211
|
+
}
|
|
212
|
+
const model = Object.entries(provider.modelAliases ?? {}).find(([, value]) => value.toLowerCase() === alias);
|
|
213
|
+
if (model) issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by model "${providerName}/${model[0]}"` });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
206
216
|
if (body.strategy !== undefined
|
|
207
217
|
&& body.strategy !== "failover"
|
|
208
218
|
&& body.strategy !== "round-robin") {
|
|
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
|
|
|
9
9
|
export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
|
|
10
10
|
schemaVersion: 1,
|
|
11
11
|
id: "openai.codex-forward.gpt-5-6-sol.responses",
|
|
12
|
-
version: "1.
|
|
12
|
+
version: "1.2.0",
|
|
13
13
|
subject: {
|
|
14
14
|
providerId: "openai",
|
|
15
15
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
@@ -69,6 +69,38 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
|
|
|
69
69
|
limitation: "The field is removed before dispatch.",
|
|
70
70
|
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }],
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
id: "system-input-messages",
|
|
74
|
+
feature: "request.input.system_messages",
|
|
75
|
+
disposition: "translated",
|
|
76
|
+
summary: "Text-only input system messages are folded into top-level instructions.",
|
|
77
|
+
limitation: "The destination does not accept system-role input items; multimodal system messages stay unchanged rather than being dropped.",
|
|
78
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["system-message-folded", "system-message-removed"] }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "truncation",
|
|
82
|
+
feature: "request.truncation",
|
|
83
|
+
disposition: "unsupported",
|
|
84
|
+
summary: "The ChatGPT Codex forward route does not receive truncation.",
|
|
85
|
+
limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
|
|
86
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "prompt-cache-breakpoint",
|
|
90
|
+
feature: "request.prompt_cache_breakpoint",
|
|
91
|
+
disposition: "unsupported",
|
|
92
|
+
summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.",
|
|
93
|
+
limitation: "Markers are removed recursively from input within bounded depth and node budgets.",
|
|
94
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "unstored-item-reference",
|
|
98
|
+
feature: "continuation.item_reference",
|
|
99
|
+
disposition: "degraded",
|
|
100
|
+
summary: "Unpersisted item references are omitted from store-false continuations.",
|
|
101
|
+
limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.",
|
|
102
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }],
|
|
103
|
+
},
|
|
72
104
|
{
|
|
73
105
|
id: "prompt-cache-retention",
|
|
74
106
|
feature: "request.prompt_cache_retention",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
const pendingTopLevelDeletions = new WeakMap<OcxConfig, Set<string>>();
|
|
4
|
+
export const CONFIG_REBASE_PROVENANCE_KEY = "configRebaseProvenance";
|
|
5
|
+
|
|
6
|
+
export function parsedConfigRebaseDeletionKeys(config: OcxConfig): Set<string> | null {
|
|
7
|
+
const value = config.configRebaseProvenance;
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
9
|
+
const record = value as Record<string, unknown>;
|
|
10
|
+
if (record.version !== 1 || !Array.isArray(record.deletedTopLevelKeys)) return null;
|
|
11
|
+
if (!record.deletedTopLevelKeys.every(key => typeof key === "string" && key !== CONFIG_REBASE_PROVENANCE_KEY)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return new Set(record.deletedTopLevelKeys as string[]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function configRebaseDeletionKeys(config: OcxConfig): Set<string> {
|
|
18
|
+
const deleted = new Set([
|
|
19
|
+
...(parsedConfigRebaseDeletionKeys(config) ?? []),
|
|
20
|
+
...(pendingTopLevelDeletions.get(config) ?? []),
|
|
21
|
+
]);
|
|
22
|
+
const record = config as unknown as Record<string, unknown>;
|
|
23
|
+
for (const key of [...deleted]) {
|
|
24
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
25
|
+
}
|
|
26
|
+
return deleted;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function configHasRebaseProvenance(config: OcxConfig): boolean {
|
|
30
|
+
return parsedConfigRebaseDeletionKeys(config) !== null || pendingTopLevelDeletions.has(config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function projectConfigRebaseProvenance(config: OcxConfig): OcxConfig {
|
|
34
|
+
const pending = pendingTopLevelDeletions.get(config);
|
|
35
|
+
const parsed = parsedConfigRebaseDeletionKeys(config);
|
|
36
|
+
// Preserve unknown future metadata byte-for-value. It carries no authority here.
|
|
37
|
+
if (config.configRebaseProvenance !== undefined && parsed === null) return config;
|
|
38
|
+
const deleted = new Set([...(parsed ?? []), ...(pending ?? [])]);
|
|
39
|
+
const record = config as unknown as Record<string, unknown>;
|
|
40
|
+
for (const key of [...deleted]) {
|
|
41
|
+
if (Object.hasOwn(record, key) && record[key] !== undefined) deleted.delete(key);
|
|
42
|
+
}
|
|
43
|
+
// A SHALLOW copy, deliberately. A provider entry may carry a non-cloneable value — a test
|
|
44
|
+
// fixture injects its own `fetch`, and `structuredClone` throws DataCloneError on a function,
|
|
45
|
+
// which took every provider-probe and CLI-parity suite down. Only the provenance key is
|
|
46
|
+
// rewritten here, so nothing below the top level needs to be copied at all; the deep clone
|
|
47
|
+
// was doing work this projection never asked for.
|
|
48
|
+
const projected = { ...config };
|
|
49
|
+
if (deleted.size === 0) delete projected.configRebaseProvenance;
|
|
50
|
+
else projected.configRebaseProvenance = {
|
|
51
|
+
version: 1,
|
|
52
|
+
deletedTopLevelKeys: [...deleted].sort(),
|
|
53
|
+
};
|
|
54
|
+
return projected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Delete one top-level config key and retain the writer's explicit intent for rebasing. */
|
|
58
|
+
export function deleteConfigTopLevelKey<K extends keyof OcxConfig>(config: OcxConfig, key: K): void {
|
|
59
|
+
delete config[key];
|
|
60
|
+
if (key === CONFIG_REBASE_PROVENANCE_KEY) return;
|
|
61
|
+
const deleted = pendingTopLevelDeletions.get(config) ?? new Set<string>();
|
|
62
|
+
deleted.add(key);
|
|
63
|
+
pendingTopLevelDeletions.set(config, deleted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clearPendingConfigTopLevelDeletions(config: OcxConfig): void {
|
|
67
|
+
pendingTopLevelDeletions.delete(config);
|
|
68
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -60,6 +60,7 @@ import { assertNotRealHomeUnderTest } from "./lib/test-home-guard";
|
|
|
60
60
|
import { providerDestinationConfigError } from "./lib/destination-policy";
|
|
61
61
|
import { redactSecretString } from "./lib/redact";
|
|
62
62
|
import { openRouterRoutingConfigError } from "./providers/openrouter-routing";
|
|
63
|
+
import { MODEL_ALIAS_PATTERN } from "./providers/default-aliases";
|
|
63
64
|
import {
|
|
64
65
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
65
66
|
OPENAI_PROVIDER_TIER_VERSION,
|
|
@@ -141,6 +142,15 @@ export {
|
|
|
141
142
|
writeRuntimePort,
|
|
142
143
|
type RuntimePortState,
|
|
143
144
|
} from "./config/process-state";
|
|
145
|
+
import {
|
|
146
|
+
clearPendingConfigTopLevelDeletions,
|
|
147
|
+
configHasRebaseProvenance,
|
|
148
|
+
configRebaseDeletionKeys,
|
|
149
|
+
CONFIG_REBASE_PROVENANCE_KEY,
|
|
150
|
+
deleteConfigTopLevelKey,
|
|
151
|
+
projectConfigRebaseProvenance,
|
|
152
|
+
} from "./config/rebase-provenance";
|
|
153
|
+
export { deleteConfigTopLevelKey } from "./config/rebase-provenance";
|
|
144
154
|
|
|
145
155
|
export class OpenAiTierBackupCleanupError extends Error {
|
|
146
156
|
constructor() { super("OpenAI tier backup temporary cleanup failed"); this.name = "OpenAiTierBackupCleanupError"; }
|
|
@@ -479,6 +489,9 @@ const fastWireSchema = z.object({
|
|
|
479
489
|
const providerConfigSchema = z.object({
|
|
480
490
|
adapter: z.string().min(1),
|
|
481
491
|
baseUrl: z.string().min(1),
|
|
492
|
+
alias: z.string().optional(),
|
|
493
|
+
modelAliases: z.record(z.string(), z.string()).optional(),
|
|
494
|
+
defaultAliases: z.boolean().optional(),
|
|
482
495
|
requestPacing: requestPacingSchema.optional().catch(undefined),
|
|
483
496
|
mcpMaxTools: z.number().int().positive().optional(),
|
|
484
497
|
mcpMaxSchemaBytes: z.number().int().positive().optional(),
|
|
@@ -865,6 +878,10 @@ const configSchema = z.object({
|
|
|
865
878
|
]).optional().catch(undefined),
|
|
866
879
|
providers: z.record(z.string(), providerConfigSchema),
|
|
867
880
|
defaultProvider: z.string().min(1).default("openai"),
|
|
881
|
+
defaultModelAliases: z.boolean().optional(),
|
|
882
|
+
// Future versions remain opaque through passthrough-compatible whole-config saves.
|
|
883
|
+
// Only version 1 grants deletion authority in the rebase path.
|
|
884
|
+
configRebaseProvenance: z.unknown().optional(),
|
|
868
885
|
// A retry can be billable, so absence and malformed hand edits both stay off.
|
|
869
886
|
emptyCompletionRetry: z.boolean().optional().catch(false),
|
|
870
887
|
// A malformed hand edit must not silently stop opening the browser: fall back
|
|
@@ -904,6 +921,9 @@ const configSchema = z.object({
|
|
|
904
921
|
// A malformed hand edit must degrade to false without discarding providers, accounts,
|
|
905
922
|
// or the exact selector map. Live writes remain strict.
|
|
906
923
|
codexAccountPickerEnabled: z.boolean().optional().catch(false),
|
|
924
|
+
// Same degrade-not-reject rule: a malformed hand edit hides Spark rather than discarding the
|
|
925
|
+
// whole config. Hidden is also the default, so `catch(false)` and the default agree.
|
|
926
|
+
showCodexSparkQuota: z.boolean().optional().catch(false),
|
|
907
927
|
// Model ids excluded from the Grok Build managed block (dashboard switches).
|
|
908
928
|
grokExcludedModels: z.array(z.string()).optional(),
|
|
909
929
|
// Invalid values degrade to undefined ("auto") instead of failing the whole
|
|
@@ -1127,6 +1147,17 @@ const configSchema = z.object({
|
|
|
1127
1147
|
message: reasoningSummariesError,
|
|
1128
1148
|
});
|
|
1129
1149
|
}
|
|
1150
|
+
const verbositySupportError = booleanRecordConfigError(
|
|
1151
|
+
(provider as { modelSupportsVerbosity?: unknown }).modelSupportsVerbosity,
|
|
1152
|
+
"modelSupportsVerbosity",
|
|
1153
|
+
);
|
|
1154
|
+
if (verbositySupportError) {
|
|
1155
|
+
ctx.addIssue({
|
|
1156
|
+
code: "custom",
|
|
1157
|
+
path: ["providers", redactSecretString(name), "modelSupportsVerbosity"],
|
|
1158
|
+
message: verbositySupportError,
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1130
1161
|
const serviceTierModelsError = booleanRecordConfigError(
|
|
1131
1162
|
(provider as { modelSupportsServiceTier?: unknown }).modelSupportsServiceTier,
|
|
1132
1163
|
"modelSupportsServiceTier",
|
|
@@ -1783,6 +1814,7 @@ export function loadConfig(): OcxConfig {
|
|
|
1783
1814
|
try {
|
|
1784
1815
|
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
|
|
1785
1816
|
const parsed = JSON.parse(raw);
|
|
1817
|
+
sanitizeAliasesForLoad(parsed);
|
|
1786
1818
|
sanitizeRetryOn429ForLoad(parsed);
|
|
1787
1819
|
sanitizeModelCostsForLoad(parsed);
|
|
1788
1820
|
const result = configSchema.safeParse(parsed);
|
|
@@ -1854,6 +1886,43 @@ export function loadConfig(): OcxConfig {
|
|
|
1854
1886
|
}
|
|
1855
1887
|
}
|
|
1856
1888
|
|
|
1889
|
+
/** Hand-edited alias mistakes disable only the bad alias; providers and routing survive. */
|
|
1890
|
+
function sanitizeAliasesForLoad(raw: unknown): void {
|
|
1891
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
1892
|
+
const root = raw as Record<string, unknown>;
|
|
1893
|
+
if (!root.providers || typeof root.providers !== "object" || Array.isArray(root.providers)) return;
|
|
1894
|
+
const providers = root.providers as Record<string, Record<string, unknown>>;
|
|
1895
|
+
const providerNames = new Set(Object.keys(providers).map(name => name.toLowerCase()));
|
|
1896
|
+
const claimedProviders = new Set<string>();
|
|
1897
|
+
const comboAliases = new Set(Object.values((root.combos as Record<string, { alias?: unknown }> | undefined) ?? {})
|
|
1898
|
+
.map(combo => typeof combo?.alias === "string" ? combo.alias.toLowerCase() : "").filter(Boolean));
|
|
1899
|
+
const accountNamespaces = new Set(Object.keys((root.codexAccountNamespaces as Record<string, unknown> | undefined) ?? {}).map(name => name.toLowerCase()));
|
|
1900
|
+
for (const provider of Object.values(providers)) {
|
|
1901
|
+
const alias = provider.alias;
|
|
1902
|
+
if (typeof alias !== "string" || !isValidProviderName(alias)
|
|
1903
|
+
|| providerNames.has(alias.toLowerCase()) || claimedProviders.has(alias.toLowerCase())
|
|
1904
|
+
|| comboAliases.has(alias.toLowerCase()) || accountNamespaces.has(alias.toLowerCase())) {
|
|
1905
|
+
if (alias !== undefined) console.warn("Ignoring invalid or colliding provider alias in config.json");
|
|
1906
|
+
delete provider.alias;
|
|
1907
|
+
} else claimedProviders.add(alias.toLowerCase());
|
|
1908
|
+
if (!provider.modelAliases || typeof provider.modelAliases !== "object" || Array.isArray(provider.modelAliases)) {
|
|
1909
|
+
if (provider.modelAliases !== undefined) delete provider.modelAliases;
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
const aliases = provider.modelAliases as Record<string, unknown>;
|
|
1913
|
+
const nativeIds = new Set((Array.isArray(provider.models) ? provider.models : []).filter((id): id is string => typeof id === "string").map(id => id.toLowerCase()));
|
|
1914
|
+
const claimed = new Set<string>();
|
|
1915
|
+
for (const [id, value] of Object.entries(aliases)) {
|
|
1916
|
+
const lower = typeof value === "string" ? value.toLowerCase() : "";
|
|
1917
|
+
if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value) || claimed.has(lower)
|
|
1918
|
+
|| nativeIds.has(lower) || comboAliases.has(lower) || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) {
|
|
1919
|
+
console.warn(`Ignoring invalid or colliding model alias for ${id} in config.json`);
|
|
1920
|
+
delete aliases[id];
|
|
1921
|
+
} else claimed.add(lower);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1857
1926
|
/** Refresh the user cost-overlay registry from `config` and return it unchanged. */
|
|
1858
1927
|
function withRefreshedCostOverlays(config: OcxConfig): OcxConfig {
|
|
1859
1928
|
refreshUserCostOverlays(config);
|
|
@@ -2445,7 +2514,12 @@ function persistConfigUnlocked(config: OcxConfig): boolean {
|
|
|
2445
2514
|
// External editors can add provider rows the live config deliberately does
|
|
2446
2515
|
// not route with yet; merge them at the serialization boundary so an
|
|
2447
2516
|
// unrelated in-process save cannot erase the provider or its overlay.
|
|
2517
|
+
// Provider preservation reads symbol-keyed live-owner state, which structuredClone
|
|
2518
|
+
// intentionally drops. Resolve that ownership before projecting JSON provenance.
|
|
2519
|
+
const provenanceProjection = projectConfigRebaseProvenance(config);
|
|
2448
2520
|
const persisted = withPreservedDiskOnlyProviders(config);
|
|
2521
|
+
if (provenanceProjection.configRebaseProvenance === undefined) delete persisted.configRebaseProvenance;
|
|
2522
|
+
else persisted.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
2449
2523
|
const bytes = JSON.stringify(persisted, null, 2) + "\n";
|
|
2450
2524
|
let unchanged = false;
|
|
2451
2525
|
try {
|
|
@@ -2473,9 +2547,15 @@ export function saveConfig(config: OcxConfig): void {
|
|
|
2473
2547
|
// Keep the real-home assertion ahead of even lock-directory preparation.
|
|
2474
2548
|
assertNotRealHomeUnderTest(getConfigDir());
|
|
2475
2549
|
withConfigMutationLockSync(() => {
|
|
2476
|
-
const
|
|
2477
|
-
|
|
2478
|
-
|
|
2550
|
+
const withProvenance = projectCustomModelCatalogMigration(
|
|
2551
|
+
readRawConfigJson(),
|
|
2552
|
+
projectConfigRebaseProvenance(config),
|
|
2553
|
+
);
|
|
2554
|
+
if (persistConfigUnlocked(withProvenance)) bumpGenerationForCooperatingConfigWrite();
|
|
2555
|
+
adoptCustomModelCatalogMigration(config, withProvenance);
|
|
2556
|
+
if (withProvenance.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
2557
|
+
else config.configRebaseProvenance = structuredClone(withProvenance.configRebaseProvenance);
|
|
2558
|
+
clearPendingConfigTopLevelDeletions(config);
|
|
2479
2559
|
});
|
|
2480
2560
|
}
|
|
2481
2561
|
|
|
@@ -2594,7 +2674,6 @@ const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
|
|
|
2594
2674
|
* reconciliation paths below.
|
|
2595
2675
|
*/
|
|
2596
2676
|
const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
|
|
2597
|
-
|
|
2598
2677
|
/**
|
|
2599
2678
|
* The live config retains the address of the socket Bun actually opened, while
|
|
2600
2679
|
* this map retains the operator's desired address for the next process start.
|
|
@@ -2898,6 +2977,8 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2898
2977
|
if (baseline && onDisk !== undefined) {
|
|
2899
2978
|
const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
|
|
2900
2979
|
if (persistedDiagnostics.source === "file") {
|
|
2980
|
+
const deletedKeys = configRebaseDeletionKeys(config);
|
|
2981
|
+
const provenanceExists = configHasRebaseProvenance(config);
|
|
2901
2982
|
// Only keys this live config is actually known to have diverged on may be
|
|
2902
2983
|
// rebased. The baseline is captured once when the server arms it, so any key
|
|
2903
2984
|
// that appeared on disk afterwards — through saveConfig(), a hand edit, or
|
|
@@ -2911,8 +2992,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2911
2992
|
const rebaseableKeys = new Set([
|
|
2912
2993
|
...Object.keys(baseline as unknown as Record<string, unknown>),
|
|
2913
2994
|
...Object.keys(config as unknown as Record<string, unknown>),
|
|
2995
|
+
...(provenanceExists
|
|
2996
|
+
? Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)
|
|
2997
|
+
: []),
|
|
2914
2998
|
]);
|
|
2915
|
-
const skipped = new Set(["hostname", "port", "claudeCode"]);
|
|
2999
|
+
const skipped = new Set(["hostname", "port", "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
|
|
2916
3000
|
for (const key of Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)) {
|
|
2917
3001
|
if (!rebaseableKeys.has(key)) skipped.add(key);
|
|
2918
3002
|
}
|
|
@@ -2922,6 +3006,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2922
3006
|
persistedDiagnostics.config as unknown as Record<string, unknown>,
|
|
2923
3007
|
skipped,
|
|
2924
3008
|
);
|
|
3009
|
+
for (const key of deletedKeys) delete (config as unknown as Record<string, unknown>)[key];
|
|
2925
3010
|
}
|
|
2926
3011
|
}
|
|
2927
3012
|
if (claudeCodeBaseline.has(config)) {
|
|
@@ -2935,10 +3020,13 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2935
3020
|
}
|
|
2936
3021
|
}
|
|
2937
3022
|
}
|
|
3023
|
+
const provenanceProjection = projectConfigRebaseProvenance(config);
|
|
2938
3024
|
const projectedConfig = projectCustomModelCatalogMigration(
|
|
2939
3025
|
onDisk,
|
|
2940
3026
|
config,
|
|
2941
3027
|
);
|
|
3028
|
+
if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
|
|
3029
|
+
else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
2942
3030
|
const persistedBinding = bindingBaseline && onDisk
|
|
2943
3031
|
? readPersistedServerBinding(onDisk, bindingBaseline)
|
|
2944
3032
|
: bindingBaseline;
|
|
@@ -2956,8 +3044,11 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2956
3044
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
2957
3045
|
}
|
|
2958
3046
|
if (liveConfigBaseline.has(config)) {
|
|
2959
|
-
|
|
3047
|
+
if (projectedConfig.configRebaseProvenance === undefined) delete config.configRebaseProvenance;
|
|
3048
|
+
else config.configRebaseProvenance = structuredClone(projectedConfig.configRebaseProvenance);
|
|
3049
|
+
liveConfigBaseline.set(config, structuredClone(projectedConfig));
|
|
2960
3050
|
}
|
|
3051
|
+
clearPendingConfigTopLevelDeletions(config);
|
|
2961
3052
|
});
|
|
2962
3053
|
}
|
|
2963
3054
|
|
|
@@ -3033,10 +3124,17 @@ export function applyProxyEnv(config: OcxConfig): void {
|
|
|
3033
3124
|
const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
3034
3125
|
const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
|
|
3035
3126
|
const seen = new Set(entries.map(e => e.toLowerCase()));
|
|
3036
|
-
|
|
3037
|
-
|
|
3127
|
+
// Configured entries first, then loopback: loopback is unconditional, so appending it last
|
|
3128
|
+
// keeps it present even when the operator lists a loopback host themselves.
|
|
3129
|
+
const raw = config.noProxy;
|
|
3130
|
+
const configured = (Array.isArray(raw) ? raw : (resolveEnvValue(raw) ?? "").split(","))
|
|
3131
|
+
.map(entry => entry.trim())
|
|
3132
|
+
.filter(Boolean);
|
|
3133
|
+
for (const host of [...configured, "localhost", "127.0.0.1", "::1", "[::1]"]) {
|
|
3134
|
+
const key = host.toLowerCase();
|
|
3135
|
+
if (!seen.has(key)) {
|
|
3038
3136
|
entries.push(host);
|
|
3039
|
-
seen.add(
|
|
3137
|
+
seen.add(key);
|
|
3040
3138
|
}
|
|
3041
3139
|
}
|
|
3042
3140
|
process.env.NO_PROXY = entries.join(",");
|