@bitkyc08/opencodex 2.1.5 → 2.1.7
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.ko.md +1 -0
- package/README.md +23 -0
- package/README.zh-CN.md +1 -0
- package/gui/dist/assets/{index-DB2i6w5f.js → index-BVahEsvB.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +27 -9
- package/src/cli.ts +82 -0
- package/src/codex-catalog.ts +32 -9
- package/src/codex-history-provider.ts +228 -21
- package/src/codex-inject.ts +28 -6
- package/src/oauth/index.ts +3 -0
- package/src/oauth/key-providers.ts +32 -3
- package/src/oauth/login-cli.ts +37 -9
- package/src/providers/derive.ts +12 -0
- package/src/providers/registry.ts +68 -1
- package/src/types.ts +15 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BVahEsvB.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-dCS-lwCM.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@ const MIN_THINKING_BUDGET = 1024;
|
|
|
37
37
|
const OUTPUT_HEADROOM = 8192;
|
|
38
38
|
/** Minimum visible-output room kept below `max_tokens` (so `max_tokens > budget_tokens` always holds). */
|
|
39
39
|
const OUTPUT_FLOOR = 4096;
|
|
40
|
+
const COMPAT_TOOL_PREFIX = "ocx_";
|
|
40
41
|
|
|
41
42
|
/** Map a Responses reasoning effort to an Anthropic extended-thinking budget (tokens, >= 1024). */
|
|
42
43
|
function reasoningBudget(effort: string): number {
|
|
@@ -61,7 +62,23 @@ function usageFromAnthropic(usage: Record<string, number> | undefined): OcxUsage
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function
|
|
65
|
+
function buildToolNameTransforms(provider: OcxProviderConfig): { toWire: (name: string) => string; fromWire: (name: string) => string } {
|
|
66
|
+
if (provider.authMode === "oauth") {
|
|
67
|
+
return { toWire: applyClaudeToolPrefix, fromWire: stripClaudeToolPrefix };
|
|
68
|
+
}
|
|
69
|
+
if (provider.escapeBuiltinToolNames === true) {
|
|
70
|
+
return {
|
|
71
|
+
toWire: (name) => name.startsWith(COMPAT_TOOL_PREFIX) ? name : COMPAT_TOOL_PREFIX + name,
|
|
72
|
+
fromWire: (name) => name.startsWith(COMPAT_TOOL_PREFIX) ? name.slice(COMPAT_TOOL_PREFIX.length) : name,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { toWire: (name) => name, fromWire: (name) => name };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function messagesToAnthropicFormat(
|
|
79
|
+
parsed: OcxParsedRequest,
|
|
80
|
+
toolNames: { toWire: (name: string) => string },
|
|
81
|
+
): { system: string | undefined; messages: unknown[] } {
|
|
65
82
|
const system = parsed.context.systemPrompt?.join("\n\n") || undefined;
|
|
66
83
|
const messages: unknown[] = [];
|
|
67
84
|
|
|
@@ -87,7 +104,7 @@ function messagesToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean):
|
|
|
87
104
|
} else if (part.type === "toolCall") {
|
|
88
105
|
const tc = part as OcxToolCall;
|
|
89
106
|
const flatName = namespacedToolName(tc.namespace, tc.name);
|
|
90
|
-
content.push({ type: "tool_use", id: tc.id, name:
|
|
107
|
+
content.push({ type: "tool_use", id: tc.id, name: toolNames.toWire(flatName), input: tc.arguments });
|
|
91
108
|
}
|
|
92
109
|
}
|
|
93
110
|
messages.push({ role: "assistant", content });
|
|
@@ -115,10 +132,10 @@ function messagesToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean):
|
|
|
115
132
|
return { system, messages };
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
function toolsToAnthropicFormat(parsed: OcxParsedRequest,
|
|
135
|
+
function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (name: string) => string }): unknown[] | undefined {
|
|
119
136
|
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
120
137
|
return parsed.context.tools.map(t => ({
|
|
121
|
-
name:
|
|
138
|
+
name: toolNames.toWire(namespacedToolName(t.namespace, t.name)),
|
|
122
139
|
description: t.description,
|
|
123
140
|
input_schema: t.parameters,
|
|
124
141
|
}));
|
|
@@ -126,12 +143,13 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, isOAuth: boolean): unk
|
|
|
126
143
|
|
|
127
144
|
export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
128
145
|
const isOAuth = provider.authMode === "oauth";
|
|
146
|
+
const toolNames = buildToolNameTransforms(provider);
|
|
129
147
|
return {
|
|
130
148
|
name: "anthropic",
|
|
131
149
|
|
|
132
150
|
buildRequest(parsed: OcxParsedRequest) {
|
|
133
|
-
const { system, messages } = messagesToAnthropicFormat(parsed,
|
|
134
|
-
const tools = toolsToAnthropicFormat(parsed,
|
|
151
|
+
const { system, messages } = messagesToAnthropicFormat(parsed, toolNames);
|
|
152
|
+
const tools = toolsToAnthropicFormat(parsed, toolNames);
|
|
135
153
|
|
|
136
154
|
const body: Record<string, unknown> = {
|
|
137
155
|
model: parsed.modelId,
|
|
@@ -174,7 +192,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
174
192
|
if (tc === "auto") body.tool_choice = { type: "auto" };
|
|
175
193
|
else if (tc === "none") body.tool_choice = { type: "none" };
|
|
176
194
|
else if (tc === "required") body.tool_choice = { type: "any" };
|
|
177
|
-
else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name:
|
|
195
|
+
else if (typeof tc === "object" && "name" in tc) body.tool_choice = { type: "tool", name: toolNames.toWire(tc.name) };
|
|
178
196
|
}
|
|
179
197
|
|
|
180
198
|
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
|
|
@@ -241,7 +259,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
241
259
|
currentBlockType = block.type;
|
|
242
260
|
if (block.type === "tool_use") {
|
|
243
261
|
currentToolCallId = block.id ?? "";
|
|
244
|
-
currentToolCallName =
|
|
262
|
+
currentToolCallName = toolNames.fromWire(block.name ?? "");
|
|
245
263
|
yield { type: "tool_call_start", id: currentToolCallId, name: currentToolCallName };
|
|
246
264
|
}
|
|
247
265
|
break;
|
|
@@ -302,7 +320,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig): ProviderAda
|
|
|
302
320
|
if (block.type === "text" && block.text) {
|
|
303
321
|
events.push({ type: "text_delta", text: block.text });
|
|
304
322
|
} else if (block.type === "tool_use") {
|
|
305
|
-
events.push({ type: "tool_call_start", id: block.id ?? "", name:
|
|
323
|
+
events.push({ type: "tool_call_start", id: block.id ?? "", name: toolNames.fromWire(block.name ?? "") });
|
|
306
324
|
events.push({ type: "tool_call_delta", arguments: JSON.stringify(block.input ?? {}) });
|
|
307
325
|
events.push({ type: "tool_call_end" });
|
|
308
326
|
}
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { execFileSync, spawn } from "node:child_process";
|
|
3
3
|
import { rmSync } from "node:fs";
|
|
4
4
|
import { restoreNativeCodex } from "./codex-inject";
|
|
5
|
+
import { restoreLegacyOpenaiHistory } from "./codex-history-provider";
|
|
5
6
|
import { codexAutoStartEnabled, getConfigDir, loadConfig, readPid, removePid, saveConfig, writePid } from "./config";
|
|
6
7
|
import { findAvailablePort } from "./ports";
|
|
7
8
|
import { serviceCommand, stopServiceIfInstalled, uninstallServiceIfInstalled } from "./service";
|
|
@@ -19,6 +20,8 @@ Usage:
|
|
|
19
20
|
ocx start [--port <port>] Start the proxy server (auto-syncs models to Codex)
|
|
20
21
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
21
22
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
23
|
+
ocx recover-history --legacy-openai
|
|
24
|
+
Explicitly recover pre-backup syncResumeHistory rows
|
|
22
25
|
ocx uninstall Remove service/shim/config and restore native Codex
|
|
23
26
|
ocx service <sub> Run as a background service (install|start|stop|status|uninstall)
|
|
24
27
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall)
|
|
@@ -38,6 +41,72 @@ Examples:
|
|
|
38
41
|
ocx sync Sync available models to Codex`);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
function hasHelpFlag(values: string[]): boolean {
|
|
45
|
+
return values.some(value => value === "--help" || value === "-h" || value === "help");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printSubcommandUsage(name: string | undefined): void {
|
|
49
|
+
switch (name) {
|
|
50
|
+
case "init":
|
|
51
|
+
console.log("Usage: ocx init\n\nInteractive setup for providers and Codex config injection.");
|
|
52
|
+
break;
|
|
53
|
+
case "start":
|
|
54
|
+
console.log("Usage: ocx start [--port <port>]\n\nStart the proxy server and sync models to Codex.");
|
|
55
|
+
break;
|
|
56
|
+
case "stop":
|
|
57
|
+
console.log("Usage: ocx stop\n\nStop the proxy and restore native Codex config.");
|
|
58
|
+
break;
|
|
59
|
+
case "restore":
|
|
60
|
+
case "eject":
|
|
61
|
+
console.log(`Usage: ocx ${name}\n\nRestore native Codex config without stopping the proxy.`);
|
|
62
|
+
break;
|
|
63
|
+
case "recover-history":
|
|
64
|
+
console.log("Usage: ocx recover-history --legacy-openai\n\nExplicitly recover pre-backup syncResumeHistory rows.");
|
|
65
|
+
break;
|
|
66
|
+
case "uninstall":
|
|
67
|
+
case "remove":
|
|
68
|
+
console.log(`Usage: ocx ${name}\n\nRemove service/shim/config and restore native Codex.`);
|
|
69
|
+
break;
|
|
70
|
+
case "service":
|
|
71
|
+
console.log("Usage: ocx service <install|start|stop|status|uninstall>");
|
|
72
|
+
break;
|
|
73
|
+
case "codex-shim":
|
|
74
|
+
console.log("Usage: ocx codex-shim <install|status|uninstall>");
|
|
75
|
+
break;
|
|
76
|
+
case "ensure":
|
|
77
|
+
console.log("Usage: ocx ensure\n\nEnsure the proxy is running and Codex config/cache are current.");
|
|
78
|
+
break;
|
|
79
|
+
case "sync":
|
|
80
|
+
console.log("Usage: ocx sync\n\nFetch provider models and inject them into Codex config.");
|
|
81
|
+
break;
|
|
82
|
+
case "sync-cache":
|
|
83
|
+
console.log("Usage: ocx sync-cache\n\nRefresh Codex's model cache from the active catalog.");
|
|
84
|
+
break;
|
|
85
|
+
case "status":
|
|
86
|
+
console.log("Usage: ocx status\n\nCheck proxy server status.");
|
|
87
|
+
break;
|
|
88
|
+
case "login":
|
|
89
|
+
console.log("Usage: ocx login <provider>\n\nOAuth or API-key login for a provider.");
|
|
90
|
+
break;
|
|
91
|
+
case "logout":
|
|
92
|
+
console.log("Usage: ocx logout <provider>\n\nRemove a stored provider login.");
|
|
93
|
+
break;
|
|
94
|
+
case "gui":
|
|
95
|
+
console.log("Usage: ocx gui\n\nOpen the opencodex dashboard.");
|
|
96
|
+
break;
|
|
97
|
+
case "update":
|
|
98
|
+
console.log("Usage: ocx update\n\nUpdate opencodex to the latest published version.");
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
printUsage();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
106
|
+
printSubcommandUsage(command);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
41
110
|
async function syncModelsToCodex(port?: number) {
|
|
42
111
|
const config = loadConfig();
|
|
43
112
|
const p = port ?? config.port ?? 10100;
|
|
@@ -304,6 +373,16 @@ function handleStatus() {
|
|
|
304
373
|
}
|
|
305
374
|
}
|
|
306
375
|
|
|
376
|
+
function handleRecoverHistory() {
|
|
377
|
+
if (args[1] !== "--legacy-openai") {
|
|
378
|
+
console.error("Usage: ocx recover-history --legacy-openai");
|
|
379
|
+
console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const r = restoreLegacyOpenaiHistory();
|
|
383
|
+
console.log(`Recovered ${r.rows} legacy thread(s) to openai (${r.files} rollout file(s) updated).`);
|
|
384
|
+
}
|
|
385
|
+
|
|
307
386
|
switch (command) {
|
|
308
387
|
case "init": {
|
|
309
388
|
const { runInit } = await import("./init");
|
|
@@ -323,6 +402,9 @@ switch (command) {
|
|
|
323
402
|
console.log("Plain `codex` now runs natively (no proxy).");
|
|
324
403
|
break;
|
|
325
404
|
}
|
|
405
|
+
case "recover-history":
|
|
406
|
+
handleRecoverHistory();
|
|
407
|
+
break;
|
|
326
408
|
case "uninstall":
|
|
327
409
|
case "remove":
|
|
328
410
|
await handleUninstall();
|
package/src/codex-catalog.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readR
|
|
|
6
6
|
import { DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, setCached } from "./model-cache";
|
|
7
7
|
import { buildModelsRequest, resolveModelsAuthToken } from "./oauth/index";
|
|
8
8
|
import type { OcxConfig, OcxProviderConfig } from "./types";
|
|
9
|
-
import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, sanitizeCodexReasoningEfforts } from "./reasoning-effort";
|
|
9
|
+
import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "./reasoning-effort";
|
|
10
10
|
import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "./generated/jawcode-model-metadata";
|
|
11
11
|
import { shouldCaseFoldMetadataModelId } from "./providers/derive";
|
|
12
12
|
|
|
@@ -341,19 +341,43 @@ type ProviderModelsApiItem = {
|
|
|
341
341
|
};
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
-
function
|
|
344
|
+
function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
|
|
345
|
+
const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
|
|
346
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function configuredInputModalities(prov: OcxProviderConfig, id: string): string[] | undefined {
|
|
350
|
+
const modalities = modelRecordValue(prov.modelInputModalities, id);
|
|
351
|
+
return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel): CatalogModel {
|
|
345
355
|
void name;
|
|
346
|
-
const
|
|
356
|
+
const contextCap = configuredContextWindow(prov, model.id);
|
|
357
|
+
const inputModalities = configuredInputModalities(prov, model.id);
|
|
358
|
+
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
347
359
|
return {
|
|
360
|
+
...model,
|
|
361
|
+
...(contextCap !== undefined
|
|
362
|
+
? {
|
|
363
|
+
contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
364
|
+
? Math.min(model.contextWindow, contextCap)
|
|
365
|
+
: contextCap,
|
|
366
|
+
}
|
|
367
|
+
: {}),
|
|
368
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
348
369
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
349
370
|
};
|
|
350
371
|
}
|
|
351
372
|
|
|
373
|
+
function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string): Partial<CatalogModel> {
|
|
374
|
+
const hinted = applyProviderConfigHints(name, prov, { id, provider: name });
|
|
375
|
+
const { provider: _provider, id: _id, ...hints } = hinted;
|
|
376
|
+
return hints;
|
|
377
|
+
}
|
|
378
|
+
|
|
352
379
|
function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[]): CatalogModel[] {
|
|
353
|
-
return models.map(model => (
|
|
354
|
-
...catalogHintsFromProviderConfig(name, prov, model.id),
|
|
355
|
-
...model,
|
|
356
|
-
}));
|
|
380
|
+
return models.map(model => applyProviderConfigHints(name, prov, model));
|
|
357
381
|
}
|
|
358
382
|
|
|
359
383
|
function isGlm52ModelId(id: string): boolean {
|
|
@@ -410,11 +434,10 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
410
434
|
return stale ? applyConfigHintsToCachedModels(name, prov, stale) : configured;
|
|
411
435
|
}
|
|
412
436
|
const json = await res.json() as { data?: ProviderModelsApiItem[] };
|
|
413
|
-
const live = (json.data ?? []).map(m => ({
|
|
437
|
+
const live = (json.data ?? []).map(m => applyProviderConfigHints(name, prov, {
|
|
414
438
|
id: m.id,
|
|
415
439
|
provider: name,
|
|
416
440
|
owned_by: m.owned_by,
|
|
417
|
-
...catalogHintsFromProviderConfig(name, prov, m.id),
|
|
418
441
|
...catalogHintsFromModelsApiItem(name, m),
|
|
419
442
|
}));
|
|
420
443
|
const liveIds = new Set(live.map(m => m.id));
|
|
@@ -1,19 +1,82 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
4
|
import { CODEX_HOME } from "./codex-paths";
|
|
5
|
+
import { atomicWriteFile, getConfigDir } from "./config";
|
|
5
6
|
|
|
6
7
|
const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
|
|
8
|
+
const HISTORY_BACKUP_PATH = join(getConfigDir(), "codex-history-backup.json");
|
|
7
9
|
const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
|
|
8
10
|
|
|
9
11
|
type CodexHistoryProvider = "openai" | "opencodex";
|
|
10
12
|
|
|
13
|
+
export interface CodexHistorySyncResult {
|
|
14
|
+
rows: number;
|
|
15
|
+
files: number;
|
|
16
|
+
ejectedRows?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
interface ThreadRow {
|
|
12
20
|
id: string;
|
|
13
21
|
rollout_path: string;
|
|
22
|
+
model_provider: string;
|
|
23
|
+
source: string;
|
|
24
|
+
has_user_event: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BackupEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
rolloutPath: string;
|
|
30
|
+
modelProvider: string;
|
|
31
|
+
source: string;
|
|
32
|
+
hasUserEvent: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface BackupManifest {
|
|
36
|
+
version: 1;
|
|
37
|
+
entries: Record<string, BackupEntry>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface NativeRestoreTarget {
|
|
41
|
+
modelProvider: string;
|
|
42
|
+
source: string;
|
|
43
|
+
hasUserEvent: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBackup(path: string): BackupManifest {
|
|
47
|
+
if (!existsSync(path)) return { version: 1, entries: {} };
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<BackupManifest>;
|
|
50
|
+
if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") {
|
|
51
|
+
return { version: 1, entries: {} };
|
|
52
|
+
}
|
|
53
|
+
return { version: 1, entries: parsed.entries };
|
|
54
|
+
} catch {
|
|
55
|
+
return { version: 1, entries: {} };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeBackup(path: string, manifest: BackupManifest): void {
|
|
60
|
+
if (Object.keys(manifest.entries).length === 0) {
|
|
61
|
+
if (existsSync(path)) unlinkSync(path);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
65
|
+
atomicWriteFile(path, JSON.stringify(manifest, null, 2) + "\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rememberOriginal(manifest: BackupManifest, row: ThreadRow): void {
|
|
69
|
+
if (manifest.entries[row.id]) return;
|
|
70
|
+
manifest.entries[row.id] = {
|
|
71
|
+
id: row.id,
|
|
72
|
+
rolloutPath: row.rollout_path,
|
|
73
|
+
modelProvider: row.model_provider,
|
|
74
|
+
source: row.source,
|
|
75
|
+
hasUserEvent: Number(row.has_user_event) || 0,
|
|
76
|
+
};
|
|
14
77
|
}
|
|
15
78
|
|
|
16
|
-
function
|
|
79
|
+
function updateSessionMeta(path: string, patch: { provider?: string; source?: string }): boolean {
|
|
17
80
|
if (!path || !existsSync(path)) return false;
|
|
18
81
|
const stat = statSync(path);
|
|
19
82
|
const raw = readFileSync(path, "utf8");
|
|
@@ -29,57 +92,201 @@ function updateSessionMetaProvider(path: string, provider: CodexHistoryProvider)
|
|
|
29
92
|
}
|
|
30
93
|
|
|
31
94
|
if (!parsed || typeof parsed !== "object") return false;
|
|
32
|
-
const record = parsed as { type?: unknown; payload?: { model_provider?: unknown } };
|
|
95
|
+
const record = parsed as { type?: unknown; payload?: { model_provider?: unknown; source?: unknown } };
|
|
33
96
|
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return false;
|
|
34
|
-
if (record.payload.model_provider === provider) return false;
|
|
35
97
|
|
|
36
|
-
|
|
98
|
+
let changed = false;
|
|
99
|
+
if (patch.provider !== undefined && record.payload.model_provider !== patch.provider) {
|
|
100
|
+
record.payload.model_provider = patch.provider;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
if (patch.source !== undefined && record.payload.source !== patch.source) {
|
|
104
|
+
record.payload.source = patch.source;
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if (!changed) return false;
|
|
108
|
+
|
|
37
109
|
writeFileSync(path, `${JSON.stringify(record)}${rest}`, "utf8");
|
|
38
110
|
utimesSync(path, stat.atime, stat.mtime);
|
|
39
111
|
return true;
|
|
40
112
|
}
|
|
41
113
|
|
|
42
|
-
|
|
114
|
+
function toNativeRestoreTarget(entry: BackupEntry): NativeRestoreTarget {
|
|
115
|
+
if (entry.modelProvider !== "opencodex") {
|
|
116
|
+
return {
|
|
117
|
+
modelProvider: entry.modelProvider,
|
|
118
|
+
source: entry.source,
|
|
119
|
+
hasUserEvent: entry.hasUserEvent,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
modelProvider: "openai",
|
|
124
|
+
source: entry.source === "exec" ? "cli" : entry.source,
|
|
125
|
+
hasUserEvent: 1,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: number } {
|
|
130
|
+
const rows = db
|
|
131
|
+
.query<ThreadRow, []>(`
|
|
132
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
133
|
+
FROM threads
|
|
134
|
+
WHERE model_provider = 'opencodex'
|
|
135
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
136
|
+
`)
|
|
137
|
+
.all();
|
|
138
|
+
|
|
139
|
+
let files = 0;
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
try {
|
|
142
|
+
if (updateSessionMeta(row.rollout_path, {
|
|
143
|
+
provider: "openai",
|
|
144
|
+
source: row.source === "exec" ? "cli" : undefined,
|
|
145
|
+
})) files++;
|
|
146
|
+
} catch {
|
|
147
|
+
/* native restore should continue even if an old rollout is missing */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const restore = db.transaction(() => {
|
|
152
|
+
const update = db.query(`
|
|
153
|
+
UPDATE threads
|
|
154
|
+
SET model_provider = 'openai',
|
|
155
|
+
source = CASE WHEN source = 'exec' THEN 'cli' ELSE source END,
|
|
156
|
+
has_user_event = 1
|
|
157
|
+
WHERE id = ?
|
|
158
|
+
`);
|
|
159
|
+
for (const row of rows) update.run(row.id);
|
|
160
|
+
});
|
|
161
|
+
restore();
|
|
162
|
+
return { rows: rows.length, files };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function syncCodexHistoryProvider(provider: CodexHistoryProvider, stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): CodexHistorySyncResult {
|
|
43
166
|
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
44
|
-
|
|
167
|
+
if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
|
|
168
|
+
|
|
45
169
|
const db = new Database(stateDbPath);
|
|
46
170
|
try {
|
|
47
171
|
const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
|
|
48
|
-
const
|
|
172
|
+
const openaiRows = db
|
|
49
173
|
.query<ThreadRow, string[]>(`
|
|
50
|
-
SELECT id, rollout_path
|
|
174
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
51
175
|
FROM threads
|
|
52
|
-
WHERE model_provider =
|
|
176
|
+
WHERE model_provider = 'openai'
|
|
53
177
|
AND source IN (${placeholders})
|
|
54
178
|
`)
|
|
55
|
-
.all(
|
|
179
|
+
.all(...RESUMABLE_SOURCES);
|
|
180
|
+
const execRows = db
|
|
181
|
+
.query<ThreadRow, []>(`
|
|
182
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
183
|
+
FROM threads
|
|
184
|
+
WHERE model_provider = 'opencodex'
|
|
185
|
+
AND source = 'exec'
|
|
186
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
187
|
+
`)
|
|
188
|
+
.all();
|
|
189
|
+
|
|
190
|
+
const manifest = readBackup(backupPath);
|
|
191
|
+
for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
|
|
192
|
+
writeBackup(backupPath, manifest);
|
|
56
193
|
|
|
57
194
|
let files = 0;
|
|
58
|
-
for (const row of
|
|
195
|
+
for (const row of openaiRows) {
|
|
59
196
|
try {
|
|
60
|
-
if (
|
|
197
|
+
if (updateSessionMeta(row.rollout_path, { provider: "opencodex" })) files++;
|
|
198
|
+
} catch {
|
|
199
|
+
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const row of execRows) {
|
|
203
|
+
try {
|
|
204
|
+
if (updateSessionMeta(row.rollout_path, { source: "cli" })) files++;
|
|
61
205
|
} catch {
|
|
62
206
|
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
63
207
|
}
|
|
64
208
|
}
|
|
65
209
|
|
|
66
210
|
const update = db.transaction(() => {
|
|
67
|
-
db.query(`
|
|
211
|
+
const markUserEvent = db.query(`
|
|
68
212
|
UPDATE threads
|
|
69
213
|
SET has_user_event = 1
|
|
70
|
-
WHERE
|
|
214
|
+
WHERE id = ?
|
|
71
215
|
AND trim(coalesce(first_user_message, '')) != ''
|
|
72
|
-
`)
|
|
216
|
+
`);
|
|
217
|
+
for (const row of [...openaiRows, ...execRows]) markUserEvent.run(row.id);
|
|
73
218
|
db.query(`
|
|
74
219
|
UPDATE threads
|
|
75
|
-
SET model_provider =
|
|
76
|
-
WHERE model_provider =
|
|
220
|
+
SET model_provider = 'opencodex'
|
|
221
|
+
WHERE model_provider = 'openai'
|
|
77
222
|
AND source IN (${placeholders})
|
|
78
|
-
`).run(
|
|
223
|
+
`).run(...RESUMABLE_SOURCES);
|
|
224
|
+
db.query(`
|
|
225
|
+
UPDATE threads
|
|
226
|
+
SET source = 'cli'
|
|
227
|
+
WHERE model_provider = 'opencodex'
|
|
228
|
+
AND source = 'exec'
|
|
229
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
230
|
+
`).run();
|
|
79
231
|
});
|
|
80
232
|
update();
|
|
81
233
|
|
|
82
|
-
return { rows:
|
|
234
|
+
return { rows: openaiRows.length + execRows.length, files };
|
|
235
|
+
} finally {
|
|
236
|
+
db.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
241
|
+
const manifest = readBackup(backupPath);
|
|
242
|
+
const entries = Object.values(manifest.entries);
|
|
243
|
+
|
|
244
|
+
const db = new Database(stateDbPath);
|
|
245
|
+
try {
|
|
246
|
+
if (entries.length === 0) {
|
|
247
|
+
const ejected = ejectRemainingOpencodexHistory(db);
|
|
248
|
+
return ejected.rows > 0 ? { rows: 0, files: ejected.files, ejectedRows: ejected.rows } : { rows: 0, files: 0 };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let files = 0;
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
const target = toNativeRestoreTarget(entry);
|
|
254
|
+
try {
|
|
255
|
+
if (updateSessionMeta(entry.rolloutPath, { provider: target.modelProvider, source: target.source })) files++;
|
|
256
|
+
} catch {
|
|
257
|
+
/* best-effort; keep DB restore moving even if one rollout disappeared */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const restore = db.transaction(() => {
|
|
262
|
+
const update = db.query(`
|
|
263
|
+
UPDATE threads
|
|
264
|
+
SET model_provider = ?,
|
|
265
|
+
source = ?,
|
|
266
|
+
has_user_event = ?
|
|
267
|
+
WHERE id = ?
|
|
268
|
+
`);
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const target = toNativeRestoreTarget(entry);
|
|
271
|
+
update.run(target.modelProvider, target.source, target.hasUserEvent, entry.id);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
restore();
|
|
275
|
+
writeBackup(backupPath, { version: 1, entries: {} });
|
|
276
|
+
const ejected = ejectRemainingOpencodexHistory(db);
|
|
277
|
+
return ejected.rows > 0
|
|
278
|
+
? { rows: entries.length, files: files + ejected.files, ejectedRows: ejected.rows }
|
|
279
|
+
: { rows: entries.length, files };
|
|
280
|
+
} finally {
|
|
281
|
+
db.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function restoreLegacyOpenaiHistory(stateDbPath = STATE_DB_PATH): { rows: number; files: number } {
|
|
286
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
287
|
+
const db = new Database(stateDbPath);
|
|
288
|
+
try {
|
|
289
|
+
return ejectRemainingOpencodexHistory(db);
|
|
83
290
|
} finally {
|
|
84
291
|
db.close();
|
|
85
292
|
}
|
package/src/codex-inject.ts
CHANGED
|
@@ -71,6 +71,21 @@ function stripRootContextWindowOverrides(content: string): string {
|
|
|
71
71
|
.join("\n");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function stripRootRoutedModel(content: string): string {
|
|
75
|
+
const lines = content.split("\n");
|
|
76
|
+
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
77
|
+
return lines
|
|
78
|
+
.filter((line, i) => {
|
|
79
|
+
const isRoot = firstTable === -1 || i < firstTable;
|
|
80
|
+
if (!isRoot) return true;
|
|
81
|
+
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/);
|
|
82
|
+
if (!m) return true;
|
|
83
|
+
const model = parseTomlString(m[1]);
|
|
84
|
+
return !model?.includes("/");
|
|
85
|
+
})
|
|
86
|
+
.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
/**
|
|
75
90
|
* Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
|
|
76
91
|
* header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
|
|
@@ -229,14 +244,16 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
229
244
|
|
|
230
245
|
writeFileSync(CODEX_CONFIG_PATH, content, "utf-8");
|
|
231
246
|
writeFileSync(CODEX_PROFILE_PATH, buildProfileFile(port, catalogPath), "utf-8");
|
|
232
|
-
const history =
|
|
247
|
+
const history = config?.syncResumeHistory === true
|
|
248
|
+
? syncCodexHistoryProvider("opencodex")
|
|
249
|
+
: { rows: 0, files: 0 };
|
|
233
250
|
|
|
234
251
|
const catalogMessage = catalogPath
|
|
235
252
|
? ` Codex model catalog: ${catalogPath}\n`
|
|
236
253
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
237
|
-
const historyMessage =
|
|
238
|
-
? ` Codex resume history: ${history.rows} thread(s)
|
|
239
|
-
:
|
|
254
|
+
const historyMessage = config?.syncResumeHistory === true
|
|
255
|
+
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
256
|
+
: ` Codex resume history: left unchanged. Existing OpenAI and opencodex exec project chats may be hidden while opencodex is active; set syncResumeHistory=true to enable the reversible compatibility remap.\n`;
|
|
240
257
|
return {
|
|
241
258
|
success: true,
|
|
242
259
|
message: `Injected opencodex as default provider into Codex config.\n` +
|
|
@@ -283,6 +300,7 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
283
300
|
// must match the detection regex above, or a detected line could survive un-removed.
|
|
284
301
|
out = out.split("\n").filter(l => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l)).join("\n");
|
|
285
302
|
out = stripRootContextWindowOverrides(out);
|
|
303
|
+
out = stripRootRoutedModel(out);
|
|
286
304
|
out = stripOpencodexCatalogPath(out);
|
|
287
305
|
return out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
288
306
|
}
|
|
@@ -299,7 +317,7 @@ export function removeCodexConfig(): { success: boolean; message: string } {
|
|
|
299
317
|
const had = hasOpencodexRouting(content);
|
|
300
318
|
if (had) {
|
|
301
319
|
atomicWriteFile(CODEX_CONFIG_PATH, stripOpencodexConfig(content));
|
|
302
|
-
} else if (
|
|
320
|
+
} else if (stripOpencodexConfig(content) !== content) {
|
|
303
321
|
atomicWriteFile(CODEX_CONFIG_PATH, stripOpencodexConfig(content));
|
|
304
322
|
}
|
|
305
323
|
if (existsSync(CODEX_PROFILE_PATH)) unlinkSync(CODEX_PROFILE_PATH);
|
|
@@ -321,7 +339,11 @@ export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
|
321
339
|
const msg = cat.removed > 0
|
|
322
340
|
? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
|
|
323
341
|
: cfg.message;
|
|
324
|
-
const historyMsg = history.rows > 0
|
|
342
|
+
const historyMsg = history.rows > 0
|
|
343
|
+
? ` Resume history restored from opencodex backup (${history.rows} thread(s)).`
|
|
344
|
+
: history.ejectedRows
|
|
345
|
+
? ` ${history.ejectedRows} opencodex history thread(s) were ejected to openai so native Codex can resume them.`
|
|
346
|
+
: "";
|
|
325
347
|
return { success: cfg.success, message: `${msg}${historyMsg}` };
|
|
326
348
|
}
|
|
327
349
|
|