@bitkyc08/opencodex 2.7.30 → 2.7.33-preview.20260722
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.ja.md +438 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +480 -0
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +18 -1
- package/gui/dist/assets/index-B79f-04T.js +52 -0
- package/gui/dist/assets/index-D6Fcl4yM.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +17 -2
- package/src/adapters/cursor/discovery.ts +2 -2
- package/src/adapters/google-tool-schema.ts +4 -0
- package/src/adapters/google.ts +17 -2
- package/src/adapters/openai-chat.ts +36 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +12 -4
- package/src/cli/account-api.ts +4 -2
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude.ts +6 -1
- package/src/cli/help.ts +25 -4
- package/src/cli/init.ts +38 -3
- package/src/cli/models.ts +206 -7
- package/src/codex/auth-api.ts +45 -3
- package/src/codex/catalog.ts +105 -12
- package/src/codex/routing.ts +85 -4
- package/src/combos/index.ts +1 -1
- package/src/combos/request.ts +26 -4
- package/src/combos/types.ts +4 -4
- package/src/config.ts +60 -3
- package/src/lib/upstream-retry.ts +21 -0
- package/src/lib/winsw.ts +7 -1
- package/src/oauth/anthropic.ts +23 -1
- package/src/oauth/github-copilot.ts +1 -0
- package/src/oauth/index.ts +88 -8
- package/src/oauth/kiro.ts +12 -1
- package/src/oauth/local-token-detect.ts +3 -0
- package/src/oauth/store.ts +43 -0
- package/src/oauth/types.ts +3 -1
- package/src/providers/antigravity-models.ts +123 -14
- package/src/providers/api-keys.ts +12 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/registry.ts +41 -17
- package/src/router.ts +2 -1
- package/src/server/auth-cors.ts +5 -0
- package/src/server/index.ts +3 -3
- package/src/server/management-api.ts +143 -6
- package/src/server/relay.ts +31 -5
- package/src/server/request-log.ts +12 -0
- package/src/server/responses.ts +127 -31
- package/src/service.ts +12 -3
- package/src/types.ts +44 -3
- package/src/update/index.ts +19 -2
- package/src/update/job.ts +13 -3
- package/src/usage/expected-prices.ts +22 -9
- package/src/usage/summary.ts +43 -0
- package/gui/dist/assets/index-B-UauL1p.css +0 -1
- package/gui/dist/assets/index-avcinRsG.js +0 -40
package/src/cli/help.ts
CHANGED
|
@@ -94,9 +94,18 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
94
94
|
],
|
|
95
95
|
},
|
|
96
96
|
models: {
|
|
97
|
-
usage: "ocx models [--provider <name>] [--json]",
|
|
98
|
-
summary: "List
|
|
99
|
-
details: [
|
|
97
|
+
usage: "ocx models [list] [--provider <name>] [--json] | add <provider> <modelId> [opts] | remove <id|provider/modelId> [--yes] | list-custom [--json]",
|
|
98
|
+
summary: "List models and manage custom (manually registered) models.",
|
|
99
|
+
details: [
|
|
100
|
+
"List available models from static config with no subcommand (liveModels may add more at runtime).",
|
|
101
|
+
"add: register a model the provider catalog does not advertise yet.",
|
|
102
|
+
" --display-name <name> Human label (no slashes).",
|
|
103
|
+
" --context-window <tokens> e.g. 200000.",
|
|
104
|
+
" --modalities text,image Comma-separated (text|image|audio).",
|
|
105
|
+
"remove: delete a custom model by UUID or <provider>/<modelId>.",
|
|
106
|
+
"list-custom: show all custom models.",
|
|
107
|
+
"Changes apply immediately to a running proxy (catalog sync).",
|
|
108
|
+
],
|
|
100
109
|
},
|
|
101
110
|
claude: {
|
|
102
111
|
usage: "ocx claude [claude args...]",
|
|
@@ -113,6 +122,17 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
113
122
|
usage: "ocx restart",
|
|
114
123
|
summary: "Stop the proxy and restart it (background). Equivalent to stop + ensure.",
|
|
115
124
|
},
|
|
125
|
+
v2: {
|
|
126
|
+
usage: "ocx v2 <status|on|off|mode <v1|default|v2>|threads <n>>",
|
|
127
|
+
summary: "Toggle the Codex multi_agent_v2 feature (multi-agent surface).",
|
|
128
|
+
details: [
|
|
129
|
+
"status Show flag, multi-agent mode, and thread limit.",
|
|
130
|
+
"on | off Enable/disable multi_agent_v2 (catalog resyncs).",
|
|
131
|
+
"mode <v1|default|v2> Force all models to one surface, or respect upstream pins.",
|
|
132
|
+
"threads <n> Set max_concurrent_threads_per_session (integer >= 1).",
|
|
133
|
+
"Flips preserve the active thread limit while moving between v1/v2 modes.",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
116
136
|
health: {
|
|
117
137
|
usage: "ocx health [--json]",
|
|
118
138
|
summary: "Check proxy health. Exits 0 if healthy, 1 otherwise.",
|
|
@@ -156,10 +176,11 @@ Usage:
|
|
|
156
176
|
ocx gui Open the opencodex dashboard
|
|
157
177
|
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
|
|
158
178
|
ocx restart Stop and restart the proxy
|
|
179
|
+
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|threads)
|
|
159
180
|
ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
|
|
160
181
|
ocx provider <sub> Manage providers (list|add|remove|show|set-default)
|
|
161
182
|
ocx account <sub> Accounts/keys (list|current|use|refresh|auto-switch|remove|add-key)
|
|
162
|
-
ocx models
|
|
183
|
+
ocx models <sub> List models; manage custom models (add|remove|list-custom)
|
|
163
184
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
164
185
|
ocx help [command] Show help
|
|
165
186
|
ocx --version | -v Print version
|
package/src/cli/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
+
import { constants as fsConstants, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
3
|
import { injectCodexConfig } from "../codex/inject";
|
|
3
|
-
import { getDefaultConfig, isValidProviderName, saveConfig } from "../config";
|
|
4
|
+
import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, saveConfig } from "../config";
|
|
4
5
|
import { enrichProviderFromCatalog } from "../oauth/key-providers";
|
|
5
6
|
import { deriveInitProviders } from "../providers/derive";
|
|
6
7
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -42,7 +43,7 @@ const KIND_HEADING: Record<InitKind, string> = {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
function printMenu(providers: InitProvider[]): void {
|
|
45
|
-
console.log("
|
|
46
|
+
console.log("Choose your default provider (you can add more later):");
|
|
46
47
|
let lastKind: InitKind | null = null;
|
|
47
48
|
providers.forEach((p, i) => {
|
|
48
49
|
if (p.kind !== lastKind) { console.log(`\n ${KIND_HEADING[p.kind]}:`); lastKind = p.kind; }
|
|
@@ -53,6 +54,33 @@ function printMenu(providers: InitProvider[]): void {
|
|
|
53
54
|
|
|
54
55
|
const envKeyFor = (id: string) => `${id.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
55
56
|
|
|
57
|
+
/** Post-init cleanup of `.pre-openai-tiers-v2.bak` with rollback preservation (issue #257). */
|
|
58
|
+
export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()): void {
|
|
59
|
+
const backup = `${configPath}.pre-openai-tiers-v2.bak`;
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(backup)) return;
|
|
62
|
+
if (classifyOpenAiTierBackup(readFileSync(backup)) === "stale") {
|
|
63
|
+
unlinkSync(backup);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Publish the preserved snapshot with a no-replace copy (COPYFILE_EXCL) so a
|
|
67
|
+
// destination collision (frozen/rolled-back clock, pre-created file) can never
|
|
68
|
+
// silently overwrite another rollback snapshot; retry with a sequence suffix.
|
|
69
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
70
|
+
const preserved = `${configPath}.pre-openai-tiers-v1-rollback.${Date.now()}${attempt ? `-${attempt}` : ""}.bak`;
|
|
71
|
+
try {
|
|
72
|
+
copyFileSync(backup, preserved, fsConstants.COPYFILE_EXCL);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
unlinkSync(backup);
|
|
78
|
+
console.warn(`⚠️ Kept your pre-migration config rollback snapshot at ${preserved}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
} catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
export async function runInit(): Promise<void> {
|
|
57
85
|
const prompt = createPrompt();
|
|
58
86
|
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
@@ -60,7 +88,7 @@ export async function runInit(): Promise<void> {
|
|
|
60
88
|
const providers = buildInitProviders();
|
|
61
89
|
printMenu(providers);
|
|
62
90
|
|
|
63
|
-
const choice = await prompt.ask("\nSelect provider (number): ");
|
|
91
|
+
const choice = await prompt.ask("\nSelect default provider (number): ");
|
|
64
92
|
const idx = parseInt(choice, 10) - 1;
|
|
65
93
|
|
|
66
94
|
let providerName: string;
|
|
@@ -136,6 +164,13 @@ export async function runInit(): Promise<void> {
|
|
|
136
164
|
};
|
|
137
165
|
|
|
138
166
|
saveConfig(config);
|
|
167
|
+
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
168
|
+
// installation would make the next `ocx start` crash on a stale-backup
|
|
169
|
+
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
170
|
+
// post-migration v2 snapshot) may be deleted; a backup that still parses as a
|
|
171
|
+
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
172
|
+
// preserved by renaming it out of the collision path (sol review 260722).
|
|
173
|
+
cleanupOpenAiTierBackupAfterInit();
|
|
139
174
|
console.log(`\n✅ Config saved to ~/.opencodex/config.json`);
|
|
140
175
|
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
141
176
|
|
package/src/cli/models.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ocx models` subcommand — list
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* ocx models [--provider <name>] [--json]
|
|
2
|
+
* `ocx models` subcommand — list configured models and manage custom models.
|
|
6
3
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { syncModelsToCodex } from "../codex/sync";
|
|
7
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
|
|
8
|
+
import { routedSlug } from "../providers/slug-codec";
|
|
9
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
10
|
+
import type { OcxConfig, OcxCustomModel } from "../types";
|
|
11
|
+
|
|
12
|
+
const ADD_USAGE = "Usage: ocx models add <provider> <modelId> [--display-name <name>] [--context-window <tokens>] [--modalities text,image,audio]";
|
|
13
|
+
const REMOVE_USAGE = "Usage: ocx models remove <customId|provider/modelId> [--yes]";
|
|
14
|
+
const LIST_CUSTOM_USAGE = "Usage: ocx models list-custom [--json]";
|
|
15
|
+
const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
|
|
9
16
|
|
|
10
17
|
interface ModelEntry {
|
|
11
18
|
provider: string;
|
|
@@ -75,7 +82,175 @@ function consumeFlagValue(args: string[], flag: string): string | undefined {
|
|
|
75
82
|
return value;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
function fail(message: string, usage?: string): never {
|
|
86
|
+
console.error(`Error: ${message}`);
|
|
87
|
+
if (usage) console.error(usage);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rejectUnexpectedArgs(args: string[], usage: string): void {
|
|
92
|
+
if (args.length === 0) return;
|
|
93
|
+
const unknown = args.filter(arg => arg.startsWith("-"));
|
|
94
|
+
fail(
|
|
95
|
+
unknown.length > 0
|
|
96
|
+
? `Unknown flag(s): ${unknown.join(", ")}`
|
|
97
|
+
: `Unexpected argument(s): ${args.join(", ")}`,
|
|
98
|
+
usage,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function syncCustomModelsIfLive(): Promise<void> {
|
|
103
|
+
const live = await findLiveProxy();
|
|
104
|
+
if (!live) return;
|
|
105
|
+
await syncModelsToCodex(live.port).catch(error => {
|
|
106
|
+
console.error(`Warning: custom model saved, but catalog sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function handleCustomAdd(args: string[]): Promise<void> {
|
|
111
|
+
const rest = [...args];
|
|
112
|
+
const provider = rest.shift()?.trim() ?? "";
|
|
113
|
+
const modelId = rest.shift()?.trim() ?? "";
|
|
114
|
+
const displayNameValue = consumeFlagValue(rest, "--display-name");
|
|
115
|
+
const contextWindowValue = consumeFlagValue(rest, "--context-window");
|
|
116
|
+
const modalitiesValue = consumeFlagValue(rest, "--modalities");
|
|
117
|
+
rejectUnexpectedArgs(rest, ADD_USAGE);
|
|
118
|
+
|
|
119
|
+
if (!provider || !modelId) fail("provider and modelId are required", ADD_USAGE);
|
|
120
|
+
if (!isValidProviderName(provider)) fail(`invalid provider name "${provider}"`);
|
|
121
|
+
if (modelId.includes("/")) fail("modelId must not contain /");
|
|
122
|
+
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
if (!hasOwnProvider(config.providers, provider)) {
|
|
125
|
+
fail(`provider "${provider}" is not configured. See: ocx provider list`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const displayName = displayNameValue?.trim() || undefined;
|
|
129
|
+
if (displayName?.includes("/")) fail("displayName must not contain /");
|
|
130
|
+
|
|
131
|
+
let contextWindow: number | undefined;
|
|
132
|
+
if (contextWindowValue !== undefined) {
|
|
133
|
+
contextWindow = Number(contextWindowValue);
|
|
134
|
+
if (!Number.isInteger(contextWindow) || contextWindow <= 0) {
|
|
135
|
+
fail("context window must be a positive integer");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let inputModalities: string[] | undefined;
|
|
140
|
+
if (modalitiesValue !== undefined) {
|
|
141
|
+
inputModalities = modalitiesValue.split(",").map(value => value.trim());
|
|
142
|
+
const invalid = inputModalities.filter(value => !ALLOWED_MODALITIES.has(value));
|
|
143
|
+
if (inputModalities.length === 0 || invalid.length > 0) {
|
|
144
|
+
fail("modalities must be comma-separated values from text|image|audio");
|
|
145
|
+
}
|
|
146
|
+
inputModalities = [...new Set(inputModalities)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const existing = config.customModels ?? [];
|
|
150
|
+
const slug = routedSlug(provider, modelId);
|
|
151
|
+
if (existing.some(model => routedSlug(model.provider, model.modelId) === slug)) {
|
|
152
|
+
fail(`custom model "${slug}" already exists`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entry: OcxCustomModel = {
|
|
156
|
+
id: randomUUID(),
|
|
157
|
+
provider,
|
|
158
|
+
modelId,
|
|
159
|
+
...(displayName ? { displayName } : {}),
|
|
160
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
161
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
162
|
+
addedAt: new Date().toISOString(),
|
|
163
|
+
};
|
|
164
|
+
config.customModels = [...existing, entry];
|
|
165
|
+
saveConfig(config);
|
|
166
|
+
await syncCustomModelsIfLive();
|
|
167
|
+
console.log(`Added custom model ${slug} (${entry.id}).`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function confirmCustomRemoval(model: OcxCustomModel): Promise<boolean> {
|
|
171
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
172
|
+
fail("remove requires --yes in non-interactive mode");
|
|
173
|
+
}
|
|
174
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
175
|
+
try {
|
|
176
|
+
const answer = (await rl.question(`Remove custom model ${routedSlug(model.provider, model.modelId)}? [y/N] `)).trim().toLowerCase();
|
|
177
|
+
return answer === "y" || answer === "yes";
|
|
178
|
+
} finally {
|
|
179
|
+
rl.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function handleCustomRemove(args: string[]): Promise<void> {
|
|
184
|
+
const rest = [...args];
|
|
185
|
+
const confirmed = consumeFlag(rest, "--yes");
|
|
186
|
+
const target = rest.shift()?.trim() ?? "";
|
|
187
|
+
rejectUnexpectedArgs(rest, REMOVE_USAGE);
|
|
188
|
+
if (!target) fail("custom model id or provider/modelId is required", REMOVE_USAGE);
|
|
189
|
+
|
|
190
|
+
const config = loadConfig();
|
|
191
|
+
const existing = config.customModels ?? [];
|
|
192
|
+
const index = target.includes("/")
|
|
193
|
+
? existing.findIndex(model => routedSlug(model.provider, model.modelId) === target)
|
|
194
|
+
: existing.findIndex(model => model.id === target);
|
|
195
|
+
if (index === -1) fail(`custom model "${target}" not found`);
|
|
196
|
+
|
|
197
|
+
const model = existing[index];
|
|
198
|
+
if (!confirmed && !(await confirmCustomRemoval(model))) {
|
|
199
|
+
console.log("Cancelled.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const next = existing.filter((_, modelIndex) => modelIndex !== index);
|
|
204
|
+
config.customModels = next.length > 0 ? next : undefined;
|
|
205
|
+
saveConfig(config);
|
|
206
|
+
await syncCustomModelsIfLive();
|
|
207
|
+
console.log(`Removed custom model ${routedSlug(model.provider, model.modelId)}.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function customModelCells(model: OcxCustomModel): string[] {
|
|
211
|
+
return [
|
|
212
|
+
model.id.slice(0, 8),
|
|
213
|
+
model.modelId,
|
|
214
|
+
model.displayName ?? "-",
|
|
215
|
+
model.contextWindow ? `${Math.round(model.contextWindow / 1000)}k` : "-",
|
|
216
|
+
model.inputModalities?.join(",") ?? "-",
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printCustomModelGroup(provider: string, models: OcxCustomModel[]): void {
|
|
221
|
+
const rows = models.map(customModelCells);
|
|
222
|
+
const headers = ["ID", "MODEL", "DISPLAY NAME", "CONTEXT", "MODALITIES"];
|
|
223
|
+
const widths = headers.map((header, column) => Math.max(header.length, ...rows.map(row => row[column].length)));
|
|
224
|
+
const line = (cells: string[]) => cells.map((cell, column) => cell.padEnd(widths[column])).join(" ");
|
|
225
|
+
console.log(`${provider}:`);
|
|
226
|
+
console.log(` ${line(headers)}`);
|
|
227
|
+
for (const row of rows) console.log(` ${line(row)}`);
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleCustomList(args: string[]): void {
|
|
232
|
+
const rest = [...args];
|
|
233
|
+
const wantsJson = consumeFlag(rest, "--json");
|
|
234
|
+
rejectUnexpectedArgs(rest, LIST_CUSTOM_USAGE);
|
|
235
|
+
const models = loadConfig().customModels ?? [];
|
|
236
|
+
if (wantsJson) {
|
|
237
|
+
console.log(JSON.stringify(models, null, 2));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (models.length === 0) {
|
|
241
|
+
console.log("No custom models registered.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const byProvider = new Map<string, OcxCustomModel[]>();
|
|
245
|
+
for (const model of models) {
|
|
246
|
+
const group = byProvider.get(model.provider) ?? [];
|
|
247
|
+
group.push(model);
|
|
248
|
+
byProvider.set(model.provider, group);
|
|
249
|
+
}
|
|
250
|
+
for (const [provider, providerModels] of byProvider) printCustomModelGroup(provider, providerModels);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function handleConfiguredModels(args: string[]): void {
|
|
79
254
|
const restArgs = [...args];
|
|
80
255
|
const wantsJson = consumeFlag(restArgs, "--json");
|
|
81
256
|
const providerFilter = consumeFlagValue(restArgs, "--provider");
|
|
@@ -136,3 +311,27 @@ export function handleModels(args: string[]): void {
|
|
|
136
311
|
console.log("* = default model for provider");
|
|
137
312
|
console.log("Note: providers with liveModels may have additional models at runtime.");
|
|
138
313
|
}
|
|
314
|
+
|
|
315
|
+
function runCustomCommand(command: Promise<void>): void {
|
|
316
|
+
command.catch(error => {
|
|
317
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
process.exitCode = 1;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function handleModels(args: string[]): void {
|
|
323
|
+
const [subcommand, ...rest] = args;
|
|
324
|
+
if (subcommand === "add") {
|
|
325
|
+
runCustomCommand(handleCustomAdd(rest));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (subcommand === "remove") {
|
|
329
|
+
runCustomCommand(handleCustomRemove(rest));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (subcommand === "list-custom") {
|
|
333
|
+
handleCustomList(rest);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
handleConfiguredModels(subcommand === "list" ? rest : args);
|
|
337
|
+
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -77,6 +77,7 @@ function poolAccountDto(
|
|
|
77
77
|
return {
|
|
78
78
|
id: account.id,
|
|
79
79
|
email: maskEmail(account.email) ?? account.email,
|
|
80
|
+
...(account.alias !== undefined ? { alias: account.alias } : {}),
|
|
80
81
|
...(account.plan !== undefined ? { plan: account.plan } : {}),
|
|
81
82
|
...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}),
|
|
82
83
|
isMain: false,
|
|
@@ -168,9 +169,13 @@ async function verifyCodexAccountWarmup(
|
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
function expireCodexAuthFlow(flowId: string | null, error = "Login cancelled"): void {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
const ids = flowId
|
|
173
|
+
? [flowId]
|
|
174
|
+
: [...codexAuthLoginState].filter(([, state]) => state.status === "pending").map(([id]) => id);
|
|
175
|
+
for (const id of ids) {
|
|
176
|
+
codexAuthLoginState.set(id, { status: "error", error, doneAt: Date.now() });
|
|
177
|
+
setTimeout(() => codexAuthLoginState.delete(id), 30_000);
|
|
178
|
+
}
|
|
174
179
|
}
|
|
175
180
|
|
|
176
181
|
let mainAccountCache: { email: string | null; plan: string | null; quota: Omit<StoredAccountQuota, "updatedAt"> | null; ts: number } | null = null;
|
|
@@ -258,6 +263,7 @@ interface PoolQuotaResult {
|
|
|
258
263
|
|
|
259
264
|
export interface CodexAuthAccountDto {
|
|
260
265
|
id: string;
|
|
266
|
+
alias?: string;
|
|
261
267
|
email: string;
|
|
262
268
|
plan?: string | null;
|
|
263
269
|
logLabel?: string;
|
|
@@ -442,6 +448,24 @@ export async function handleCodexAuthAPI(
|
|
|
442
448
|
return jsonResponse({ ok: true });
|
|
443
449
|
}
|
|
444
450
|
|
|
451
|
+
if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
|
|
452
|
+
const body = await req.json().catch(() => ({})) as { id?: unknown; alias?: unknown };
|
|
453
|
+
const id = typeof body.id === "string" ? body.id.trim() : "";
|
|
454
|
+
const alias = typeof body.alias === "string" ? body.alias.trim() : "";
|
|
455
|
+
if (!id || !ACCOUNT_ID_RE.test(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
456
|
+
if (id === MAIN_CODEX_ACCOUNT_ID) return jsonResponse({ error: "Main Codex account alias is not configurable" }, 400);
|
|
457
|
+
if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
|
|
458
|
+
return jsonResponse({ error: "Alias must be a string of at most 80 printable characters" }, 400);
|
|
459
|
+
}
|
|
460
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
461
|
+
const account = (runtimeConfig.codexAccounts ?? []).find(candidate => candidate.id === id && !candidate.isMain);
|
|
462
|
+
if (!account) return jsonResponse({ error: "Account not found" }, 404);
|
|
463
|
+
if (alias) account.alias = alias;
|
|
464
|
+
else delete account.alias;
|
|
465
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
466
|
+
return jsonResponse({ ok: true, id, alias: alias || null });
|
|
467
|
+
}
|
|
468
|
+
|
|
445
469
|
if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
|
|
446
470
|
let body: { accountId: string | null };
|
|
447
471
|
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
@@ -763,6 +787,24 @@ export async function handleCodexAuthAPI(
|
|
|
763
787
|
}
|
|
764
788
|
}
|
|
765
789
|
|
|
790
|
+
if (url.pathname === "/api/codex-auth/login/code" && req.method === "POST") {
|
|
791
|
+
const body = (await req.json().catch(() => ({}))) as { flowId?: unknown; input?: unknown };
|
|
792
|
+
const flowId = typeof body.flowId === "string" ? body.flowId.trim() : "";
|
|
793
|
+
const input = typeof body.input === "string" ? body.input : "";
|
|
794
|
+
if (!flowId) return jsonResponse({ error: "flowId required" }, 400);
|
|
795
|
+
if (input.length > 4096) return jsonResponse({ error: "input too long" }, 400);
|
|
796
|
+
|
|
797
|
+
// Import may yield; validate afterwards so cancel/replace cannot race a stale flow through.
|
|
798
|
+
const { submitManualLoginCode } = await import("../oauth");
|
|
799
|
+
const flow = codexAuthLoginState.get(flowId);
|
|
800
|
+
if (!flow) return jsonResponse({ error: "login flow expired or unknown" }, 400);
|
|
801
|
+
if (flow.status !== "pending") return jsonResponse({ error: "login flow is not pending" }, 400);
|
|
802
|
+
|
|
803
|
+
const result = submitManualLoginCode("chatgpt", input);
|
|
804
|
+
if (!result.ok) return jsonResponse({ error: result.error }, 400);
|
|
805
|
+
return jsonResponse({ ok: true }, 202);
|
|
806
|
+
}
|
|
807
|
+
|
|
766
808
|
if (url.pathname === "/api/codex-auth/login/cancel" && req.method === "POST") {
|
|
767
809
|
const body = (await req.json().catch(() => ({}))) as { flowId?: string };
|
|
768
810
|
const { cancelLoginFlow } = await import("../oauth");
|
package/src/codex/catalog.ts
CHANGED
|
@@ -690,7 +690,7 @@ export function materializeBundledCodexCatalog(path: string, deps: BundledCatalo
|
|
|
690
690
|
|
|
691
691
|
function loadCatalogForSync(path: string): RawCatalog | null {
|
|
692
692
|
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
693
|
-
if (bundled) return bundled;
|
|
693
|
+
if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
|
|
694
694
|
const catalog = readCatalog(path);
|
|
695
695
|
if (catalog && findNativeTemplate(catalog)) return catalog;
|
|
696
696
|
return readCatalog(catalogBackupPathFor(path))
|
|
@@ -829,6 +829,66 @@ function ensureUltraReasoningLevel(entry: RawEntry): void {
|
|
|
829
829
|
entry.supported_reasoning_levels = levels;
|
|
830
830
|
}
|
|
831
831
|
|
|
832
|
+
/** Reasoning-effort labels accepted by the installed Codex binary's bundled catalog. */
|
|
833
|
+
export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): Set<string> | null {
|
|
834
|
+
const bundled = loadBundledCodexCatalog(deps);
|
|
835
|
+
if (!bundled) return null;
|
|
836
|
+
const efforts = new Set<string>();
|
|
837
|
+
for (const model of bundled.models ?? []) {
|
|
838
|
+
if (typeof model.slug !== "string" || model.slug.includes("/")) continue;
|
|
839
|
+
const levels = Array.isArray(model.supported_reasoning_levels) ? model.supported_reasoning_levels : [];
|
|
840
|
+
for (const level of levels) {
|
|
841
|
+
const effort = (level as { effort?: unknown })?.effort;
|
|
842
|
+
if (typeof effort === "string") efforts.add(effort);
|
|
843
|
+
}
|
|
844
|
+
if (typeof model.default_reasoning_level === "string") efforts.add(model.default_reasoning_level);
|
|
845
|
+
}
|
|
846
|
+
return efforts.size > 0 ? efforts : null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Highest surviving rung at or below the original default, with a conservative empty fallback. */
|
|
850
|
+
export function clampedDefaultEffort(original: string, surviving: readonly string[]): string {
|
|
851
|
+
if (surviving.length === 0) return "medium";
|
|
852
|
+
const ranked = [...surviving]
|
|
853
|
+
.map(effort => ({ effort, rank: codexEffortRank(effort) }))
|
|
854
|
+
.sort((a, b) => a.rank - b.rank);
|
|
855
|
+
const originalRank = codexEffortRank(original);
|
|
856
|
+
const atOrBelow = ranked.filter(item => item.rank >= 0 && item.rank <= originalRank);
|
|
857
|
+
return (atOrBelow.at(-1) ?? ranked[0]!).effort;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Remove reasoning efforts the installed Codex binary cannot deserialize from one entry. */
|
|
861
|
+
export function clampEntryToCodexSupportedEfforts(entry: RawEntry, supported: Set<string> | null): void {
|
|
862
|
+
if (!supported) return;
|
|
863
|
+
const levels = Array.isArray(entry.supported_reasoning_levels)
|
|
864
|
+
? entry.supported_reasoning_levels as Array<{ effort?: string }>
|
|
865
|
+
: null;
|
|
866
|
+
if (levels && levels.length > 0) {
|
|
867
|
+
const kept = levels.filter(level => typeof level?.effort === "string" && supported.has(level.effort));
|
|
868
|
+
entry.supported_reasoning_levels = kept.length > 0
|
|
869
|
+
? kept
|
|
870
|
+
: CODEX_REASONING_LEVELS
|
|
871
|
+
.filter(level => level.effort === "low" || level.effort === "medium" || level.effort === "high")
|
|
872
|
+
.map(level => ({ ...level }));
|
|
873
|
+
}
|
|
874
|
+
const currentDefault = entry.default_reasoning_level;
|
|
875
|
+
if (typeof currentDefault === "string" && !supported.has(currentDefault)) {
|
|
876
|
+
const surviving = (Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
|
|
877
|
+
.flatMap(level => typeof (level as { effort?: string })?.effort === "string"
|
|
878
|
+
? [(level as { effort: string }).effort]
|
|
879
|
+
: []);
|
|
880
|
+
entry.default_reasoning_level = clampedDefaultEffort(currentDefault, surviving);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Clamp every catalog entry to the reasoning ladder accepted by the installed Codex binary. */
|
|
885
|
+
export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: BundledCatalogDeps = {}): RawEntry[] {
|
|
886
|
+
const supported = codexSupportedReasoningEfforts(deps);
|
|
887
|
+
if (!supported) return models;
|
|
888
|
+
for (const entry of models) clampEntryToCodexSupportedEfforts(entry, supported);
|
|
889
|
+
return models;
|
|
890
|
+
}
|
|
891
|
+
|
|
832
892
|
/**
|
|
833
893
|
* Native entry from the pinned upstream snapshot, finished for emission. Keeps the entry's
|
|
834
894
|
* OWN identity (display_name, description, priority, availability_nux — it is the model's own
|
|
@@ -1240,11 +1300,22 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
|
|
|
1240
1300
|
async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
|
|
1241
1301
|
if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
|
|
1242
1302
|
const apiKey = await resolveModelsAuthToken(name, prov);
|
|
1243
|
-
const
|
|
1303
|
+
const seedVertexDefault = prov.adapter === "google"
|
|
1304
|
+
&& prov.googleMode === "vertex"
|
|
1305
|
+
&& (prov.models?.length ?? 0) === 0
|
|
1306
|
+
&& Boolean(prov.defaultModel);
|
|
1307
|
+
const configuredIds = seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : (prov.models ?? []);
|
|
1308
|
+
const configured: CatalogModel[] = configuredIds.map(id => ({
|
|
1244
1309
|
id,
|
|
1245
1310
|
provider: name,
|
|
1246
1311
|
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
|
|
1247
1312
|
}));
|
|
1313
|
+
const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
|
|
1314
|
+
const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
|
|
1315
|
+
vertexDefaultSeed && !models.some(model => model.id === vertexDefaultSeed.id)
|
|
1316
|
+
? [...models, vertexDefaultSeed]
|
|
1317
|
+
: models
|
|
1318
|
+
);
|
|
1248
1319
|
if (prov.adapter === "cursor") {
|
|
1249
1320
|
if (prov.liveModels === false || !apiKey) return configured;
|
|
1250
1321
|
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
@@ -1276,20 +1347,27 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1276
1347
|
return configured;
|
|
1277
1348
|
}
|
|
1278
1349
|
const fresh = getFreshCached(name, ttlMs);
|
|
1279
|
-
if (fresh) return applyConfigHintsToCachedModels(name, prov, fresh, contextCap); // dedups Codex's frequent /v1/models polling within the TTL
|
|
1350
|
+
if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
|
|
1280
1351
|
if (isModelsFetchCoolingDown(name)) {
|
|
1281
1352
|
// A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
|
|
1282
1353
|
// fetch timeout on every catalog poll — the dashboard polls this path per page load.
|
|
1283
1354
|
const stale = getStaleCached(name);
|
|
1284
|
-
return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
|
|
1355
|
+
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : configured;
|
|
1285
1356
|
}
|
|
1286
1357
|
const { url, headers } = buildModelsRequest(prov, apiKey, name);
|
|
1358
|
+
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
1359
|
+
? "vertex-aiplatform"
|
|
1360
|
+
: "provider-models";
|
|
1287
1361
|
try {
|
|
1288
1362
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
1289
1363
|
if (!res.ok) {
|
|
1290
1364
|
markModelsFetchFailure(name);
|
|
1291
1365
|
const stale = getStaleCached(name);
|
|
1292
|
-
|
|
1366
|
+
const fallback = stale ? "stale" : "configured";
|
|
1367
|
+
console.warn(
|
|
1368
|
+
`[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1369
|
+
);
|
|
1370
|
+
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : configured;
|
|
1293
1371
|
}
|
|
1294
1372
|
const json = await res.json() as unknown;
|
|
1295
1373
|
const data = json !== null && typeof json === "object" && !Array.isArray(json)
|
|
@@ -1301,7 +1379,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1301
1379
|
`[opencodex] Provider model discovery for "${name}" returned malformed 2xx data; using stale/static catalog degradation.`,
|
|
1302
1380
|
);
|
|
1303
1381
|
const stale = getStaleCached(name);
|
|
1304
|
-
return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
|
|
1382
|
+
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : configured;
|
|
1305
1383
|
}
|
|
1306
1384
|
const items = data;
|
|
1307
1385
|
const live = items.map(m => applyProviderConfigHints(name, prov, {
|
|
@@ -1324,7 +1402,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1324
1402
|
if (dated) {
|
|
1325
1403
|
// Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
|
|
1326
1404
|
live.push(applyProviderConfigHints(name, prov, { ...dated, id: m.id }, contextCap));
|
|
1327
|
-
} else if (shouldRetainConfiguredProviderModel(name, m.id)) {
|
|
1405
|
+
} else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
|
|
1328
1406
|
live.push(m);
|
|
1329
1407
|
} else {
|
|
1330
1408
|
droppedConfiguredIds.push(m.id);
|
|
@@ -1341,10 +1419,14 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1341
1419
|
}
|
|
1342
1420
|
setCached(name, live);
|
|
1343
1421
|
return live;
|
|
1344
|
-
} catch {
|
|
1422
|
+
} catch (error) {
|
|
1345
1423
|
markModelsFetchFailure(name);
|
|
1346
1424
|
const stale = getStaleCached(name);
|
|
1347
|
-
|
|
1425
|
+
const fallback = stale ? "stale" : "configured";
|
|
1426
|
+
console.warn(
|
|
1427
|
+
`[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1428
|
+
);
|
|
1429
|
+
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : configured;
|
|
1348
1430
|
}
|
|
1349
1431
|
}
|
|
1350
1432
|
|
|
@@ -1428,7 +1510,16 @@ export async function gatherRoutedModels(config: OcxConfig): Promise<CatalogMode
|
|
|
1428
1510
|
else warnUncataloguedComboOnce(id, combo, members);
|
|
1429
1511
|
}
|
|
1430
1512
|
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
1431
|
-
|
|
1513
|
+
const customModels = (config.customModels ?? []).map(cm => ({
|
|
1514
|
+
id: cm.modelId,
|
|
1515
|
+
provider: cm.provider,
|
|
1516
|
+
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
1517
|
+
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
1518
|
+
}));
|
|
1519
|
+
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
1520
|
+
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
1521
|
+
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
1522
|
+
return [...deduped, ...customModels];
|
|
1432
1523
|
}
|
|
1433
1524
|
|
|
1434
1525
|
const openAiApiCollisionWarnings = new Set<string>();
|
|
@@ -1441,11 +1532,12 @@ function intersectStrings(values: readonly string[][]): string[] {
|
|
|
1441
1532
|
}
|
|
1442
1533
|
|
|
1443
1534
|
function effectiveComboDefault(
|
|
1444
|
-
configured: string | undefined,
|
|
1535
|
+
configured: string | null | undefined,
|
|
1445
1536
|
common: readonly string[],
|
|
1446
1537
|
): string | undefined {
|
|
1538
|
+
if (!configured) return undefined;
|
|
1447
1539
|
if (configured && common.includes(configured)) return configured;
|
|
1448
|
-
const requestedRank = codexEffortRank(configured
|
|
1540
|
+
const requestedRank = codexEffortRank(configured);
|
|
1449
1541
|
const ranked = common
|
|
1450
1542
|
.map(effort => ({ effort, rank: codexEffortRank(effort) }))
|
|
1451
1543
|
.filter(item => item.rank >= 0)
|
|
@@ -1903,6 +1995,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
1903
1995
|
// native template can never leak supports_websockets while the flag is off.
|
|
1904
1996
|
const wsEnabled = websocketsEnabled(config);
|
|
1905
1997
|
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider);
|
|
1998
|
+
clampCatalogModelsToCodexSupport(catalog.models);
|
|
1906
1999
|
|
|
1907
2000
|
atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
|
|
1908
2001
|
return { added: goEntries.length, path: catalogPath };
|