@bitkyc08/opencodex 2.6.22 → 2.6.23
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/gui/dist/assets/{index-DDcEW0Cm.css → index-BcHhxo1I.css} +1 -1
- package/gui/dist/assets/index-Yuh5eZiD.js +9 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +1 -1
- package/src/cli-help.ts +27 -0
- package/src/cli-models.ts +138 -0
- package/src/cli-provider.ts +414 -0
- package/src/cli.ts +27 -1
- package/src/codex-auth-api.ts +35 -20
- package/src/provider-quota.ts +330 -0
- package/src/server.ts +6 -0
- package/gui/dist/assets/index-C0YNrCNA.js +0 -9
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx provider` subcommand — non-interactive provider management.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* list List configured and available registry providers
|
|
6
|
+
* add <name> Add a provider from the registry or with custom flags
|
|
7
|
+
* remove <name> Remove a configured provider
|
|
8
|
+
* show <name> Show provider config details (secrets masked)
|
|
9
|
+
* set-default <name> Change the default provider
|
|
10
|
+
*/
|
|
11
|
+
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "./config";
|
|
12
|
+
import { hasHelpFlag } from "./cli-help";
|
|
13
|
+
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "./providers/registry";
|
|
14
|
+
import { providerConfigSeed } from "./providers/derive";
|
|
15
|
+
import type { OcxProviderConfig } from "./types";
|
|
16
|
+
import { findLiveProxy } from "./proxy-liveness";
|
|
17
|
+
import { syncModelsToCodex } from "./codex-sync";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Arg helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function consumeFlag(args: string[], flag: string): boolean {
|
|
24
|
+
const idx = args.indexOf(flag);
|
|
25
|
+
if (idx === -1) return false;
|
|
26
|
+
args.splice(idx, 1);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function consumeFlagValue(args: string[], flag: string): string | undefined {
|
|
31
|
+
const idx = args.indexOf(flag);
|
|
32
|
+
if (idx === -1 || idx + 1 >= args.length) return undefined;
|
|
33
|
+
const value = args[idx + 1];
|
|
34
|
+
args.splice(idx, 2);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reject any leftover args (unknown flags or trailing values). */
|
|
39
|
+
function rejectUnknownArgs(args: string[], usage: string): void {
|
|
40
|
+
if (args.length === 0) return;
|
|
41
|
+
const unknown = args.filter(a => a.startsWith("-"));
|
|
42
|
+
if (unknown.length > 0) {
|
|
43
|
+
console.error(`Unknown flag(s): ${unknown.join(", ")}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.error(`Unexpected argument(s): ${args.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
console.error(usage);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function maskSecret(value: string): string {
|
|
52
|
+
if (value.length <= 8) return "****";
|
|
53
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Validation helper (F1 fix: validate before saveConfig)
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function validateAndSave(config: ReturnType<typeof loadConfig>): void {
|
|
61
|
+
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
62
|
+
console.error("Error: config would have no providers. Aborting.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
66
|
+
console.error(`Error: defaultProvider "${config.defaultProvider}" does not exist in providers. Aborting.`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
saveConfig(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// provider list
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function handleList(args: string[]): void {
|
|
77
|
+
const wantsJson = consumeFlag(args, "--json");
|
|
78
|
+
rejectUnknownArgs(args, "Usage: ocx provider list [--json]");
|
|
79
|
+
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
const configured = Object.keys(config.providers);
|
|
82
|
+
|
|
83
|
+
if (wantsJson) {
|
|
84
|
+
const entries = configured.map(name => {
|
|
85
|
+
const prov = config.providers[name];
|
|
86
|
+
const registryEntry = getProviderRegistryEntry(name);
|
|
87
|
+
return {
|
|
88
|
+
name,
|
|
89
|
+
adapter: prov.adapter,
|
|
90
|
+
baseUrl: prov.baseUrl,
|
|
91
|
+
authMode: prov.authMode ?? "key",
|
|
92
|
+
defaultModel: prov.defaultModel ?? null,
|
|
93
|
+
isDefault: name === config.defaultProvider,
|
|
94
|
+
source: registryEntry ? "registry" : "custom",
|
|
95
|
+
models: prov.models ?? [],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
console.log(JSON.stringify({ configured: entries, registryCount: PROVIDER_REGISTRY.length }, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("Configured providers:\n");
|
|
103
|
+
for (const name of configured) {
|
|
104
|
+
const prov = config.providers[name];
|
|
105
|
+
const isDefault = name === config.defaultProvider ? " (default)" : "";
|
|
106
|
+
const registryEntry = getProviderRegistryEntry(name);
|
|
107
|
+
const source = registryEntry ? "" : " [custom]";
|
|
108
|
+
const model = prov.defaultModel ? ` model=${prov.defaultModel}` : "";
|
|
109
|
+
console.log(` ${name}${isDefault}${source} adapter=${prov.adapter}${model}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const available = PROVIDER_REGISTRY.filter(e => !configured.includes(e.id));
|
|
113
|
+
if (available.length > 0) {
|
|
114
|
+
console.log(`\nAvailable from registry (${available.length}):\n`);
|
|
115
|
+
for (const entry of available) {
|
|
116
|
+
const auth = entry.authKind === "forward" ? "chatgpt-login" : entry.authKind;
|
|
117
|
+
console.log(` ${entry.id.padEnd(24)} ${entry.label} (${auth})`);
|
|
118
|
+
}
|
|
119
|
+
console.log(`\nAdd with: ocx provider add <name> [--api-key <key>]`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// provider add
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const ADD_USAGE = "Usage: ocx provider add <name> [--adapter <adapter>] [--base-url <url>] [--api-key <key>] [--default-model <model>] [--set-default] [--force] [--json] [--sync]";
|
|
128
|
+
|
|
129
|
+
async function handleAdd(args: string[]): Promise<void> {
|
|
130
|
+
const name = args[0];
|
|
131
|
+
if (!name || name.startsWith("-")) {
|
|
132
|
+
console.error(ADD_USAGE);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!isValidProviderName(name)) {
|
|
137
|
+
console.error(`Invalid provider name: "${name}". Use letters, numbers, dots, underscores, or hyphens.`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const restArgs = args.slice(1);
|
|
142
|
+
const force = consumeFlag(restArgs, "--force");
|
|
143
|
+
const setDefault = consumeFlag(restArgs, "--set-default");
|
|
144
|
+
const wantsJson = consumeFlag(restArgs, "--json");
|
|
145
|
+
const wantsSync = consumeFlag(restArgs, "--sync");
|
|
146
|
+
const apiKey = consumeFlagValue(restArgs, "--api-key");
|
|
147
|
+
const adapter = consumeFlagValue(restArgs, "--adapter");
|
|
148
|
+
const baseUrl = consumeFlagValue(restArgs, "--base-url");
|
|
149
|
+
const defaultModel = consumeFlagValue(restArgs, "--default-model");
|
|
150
|
+
rejectUnknownArgs(restArgs, ADD_USAGE);
|
|
151
|
+
|
|
152
|
+
const config = loadConfig();
|
|
153
|
+
|
|
154
|
+
if (hasOwnProvider(config.providers, name) && !force) {
|
|
155
|
+
console.error(`Provider "${name}" already exists. Use --force to overwrite.`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let provConfig: OcxProviderConfig;
|
|
160
|
+
const registryEntry = getProviderRegistryEntry(name);
|
|
161
|
+
|
|
162
|
+
if (registryEntry) {
|
|
163
|
+
provConfig = providerConfigSeed(registryEntry);
|
|
164
|
+
if (apiKey) {
|
|
165
|
+
if (registryEntry.authKind === "forward") {
|
|
166
|
+
console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`);
|
|
167
|
+
} else if (registryEntry.authKind === "oauth") {
|
|
168
|
+
console.warn(`Warning: provider "${name}" uses OAuth auth; --api-key is ignored. Run: ocx login ${name}`);
|
|
169
|
+
} else {
|
|
170
|
+
provConfig.apiKey = apiKey;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (defaultModel) provConfig.defaultModel = defaultModel;
|
|
174
|
+
if (adapter) provConfig.adapter = adapter;
|
|
175
|
+
if (baseUrl) provConfig.baseUrl = baseUrl;
|
|
176
|
+
} else {
|
|
177
|
+
if (!adapter || !baseUrl) {
|
|
178
|
+
console.error(`Provider "${name}" is not in the registry. --adapter and --base-url are required.`);
|
|
179
|
+
console.error("Usage: ocx provider add <name> --adapter <adapter> --base-url <url> [--api-key <key>]");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
provConfig = {
|
|
183
|
+
adapter,
|
|
184
|
+
baseUrl,
|
|
185
|
+
...(apiKey ? { apiKey } : {}),
|
|
186
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
config.providers[name] = provConfig;
|
|
191
|
+
if (setDefault) config.defaultProvider = name;
|
|
192
|
+
|
|
193
|
+
validateAndSave(config);
|
|
194
|
+
|
|
195
|
+
if (wantsJson) {
|
|
196
|
+
console.log(JSON.stringify({
|
|
197
|
+
action: "added",
|
|
198
|
+
provider: name,
|
|
199
|
+
adapter: provConfig.adapter,
|
|
200
|
+
baseUrl: provConfig.baseUrl,
|
|
201
|
+
defaultModel: provConfig.defaultModel ?? null,
|
|
202
|
+
isDefault: config.defaultProvider === name,
|
|
203
|
+
source: registryEntry ? "registry" : "custom",
|
|
204
|
+
needsSync: true,
|
|
205
|
+
}, null, 2));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (wantsSync) {
|
|
210
|
+
const live = await findLiveProxy();
|
|
211
|
+
if (live) {
|
|
212
|
+
await syncModelsToCodex(live.port).catch(e => {
|
|
213
|
+
console.error(`Warning: sync failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const registryLabel = registryEntry ? ` (${registryEntry.label})` : "";
|
|
219
|
+
console.log(`✅ Provider "${name}"${registryLabel} added.`);
|
|
220
|
+
if (setDefault) console.log(` Set as default provider.`);
|
|
221
|
+
if (registryEntry?.authKind === "oauth") {
|
|
222
|
+
console.log(` Authenticate with: ocx login ${name}`);
|
|
223
|
+
}
|
|
224
|
+
if (registryEntry?.authKind === "key" && !apiKey) {
|
|
225
|
+
const envKey = `${name.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
226
|
+
console.log(` Set API key with: ocx provider add ${name} --api-key <key> --force`);
|
|
227
|
+
console.log(` Or set env var: ${envKey}`);
|
|
228
|
+
}
|
|
229
|
+
if (wantsSync) {
|
|
230
|
+
console.log(` Models synced to Codex.`);
|
|
231
|
+
} else {
|
|
232
|
+
console.log(` Apply to Codex: ocx sync`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// provider remove
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
function handleRemove(args: string[]): void {
|
|
241
|
+
const restArgs = [...args];
|
|
242
|
+
const wantsJson = consumeFlag(restArgs, "--json");
|
|
243
|
+
const name = restArgs[0];
|
|
244
|
+
if (!name || name.startsWith("-")) {
|
|
245
|
+
console.error("Usage: ocx provider remove <name> [--json]");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider remove <name> [--json]");
|
|
249
|
+
|
|
250
|
+
const config = loadConfig();
|
|
251
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
252
|
+
console.error(`Provider "${name}" is not configured.`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (name === config.defaultProvider) {
|
|
257
|
+
console.error(`Cannot remove "${name}" — it is the default provider. Change the default first: ocx provider set-default <other>`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Object.keys(config.providers).length <= 1) {
|
|
262
|
+
console.error("Cannot remove the last provider.");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
delete config.providers[name];
|
|
267
|
+
validateAndSave(config);
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if (wantsJson) {
|
|
271
|
+
console.log(JSON.stringify({
|
|
272
|
+
action: "removed",
|
|
273
|
+
provider: name,
|
|
274
|
+
remainingProviders: Object.keys(config.providers),
|
|
275
|
+
defaultProvider: config.defaultProvider,
|
|
276
|
+
needsSync: true,
|
|
277
|
+
}, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`✅ Provider "${name}" removed.`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// provider show
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function handleShow(args: string[]): void {
|
|
289
|
+
const restArgs = [...args];
|
|
290
|
+
const wantsJson = consumeFlag(restArgs, "--json");
|
|
291
|
+
const name = restArgs[0];
|
|
292
|
+
if (!name || name.startsWith("-")) {
|
|
293
|
+
console.error("Usage: ocx provider show <name> [--json]");
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider show <name> [--json]");
|
|
297
|
+
|
|
298
|
+
const config = loadConfig();
|
|
299
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
300
|
+
console.error(`Provider "${name}" is not configured.`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const prov = config.providers[name];
|
|
305
|
+
const display = { ...prov, ...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}) };
|
|
306
|
+
|
|
307
|
+
if (wantsJson) {
|
|
308
|
+
console.log(JSON.stringify({ name, isDefault: name === config.defaultProvider, ...display }, null, 2));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log(`Provider: ${name}${name === config.defaultProvider ? " (default)" : ""}`);
|
|
313
|
+
console.log(` adapter: ${display.adapter}`);
|
|
314
|
+
console.log(` baseUrl: ${display.baseUrl}`);
|
|
315
|
+
if (display.authMode) console.log(` authMode: ${display.authMode}`);
|
|
316
|
+
if (display.apiKey) console.log(` apiKey: ${display.apiKey}`);
|
|
317
|
+
if (display.defaultModel) console.log(` defaultModel: ${display.defaultModel}`);
|
|
318
|
+
if (display.models?.length) console.log(` models: ${display.models.join(", ")}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// provider set-default
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
function handleSetDefault(args: string[]): void {
|
|
326
|
+
const restArgs = [...args];
|
|
327
|
+
const wantsJson = consumeFlag(restArgs, "--json");
|
|
328
|
+
const name = restArgs[0];
|
|
329
|
+
if (!name || name.startsWith("-")) {
|
|
330
|
+
console.error("Usage: ocx provider set-default <name> [--json]");
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
rejectUnknownArgs(restArgs.slice(1), "Usage: ocx provider set-default <name> [--json]");
|
|
334
|
+
|
|
335
|
+
const config = loadConfig();
|
|
336
|
+
if (!hasOwnProvider(config.providers, name)) {
|
|
337
|
+
console.error(`Provider "${name}" is not configured. Add it first: ocx provider add ${name}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (config.defaultProvider === name) {
|
|
342
|
+
if (wantsJson) {
|
|
343
|
+
console.log(JSON.stringify({ action: "noop", provider: name, defaultProvider: name, needsSync: false }, null, 2));
|
|
344
|
+
} else {
|
|
345
|
+
console.log(`"${name}" is already the default provider.`);
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
config.defaultProvider = name;
|
|
351
|
+
validateAndSave(config);
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if (wantsJson) {
|
|
355
|
+
console.log(JSON.stringify({ action: "set-default", provider: name, defaultProvider: name, needsSync: true }, null, 2));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(`✅ Default provider set to "${name}".`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Router (F2 fix: handle help flags internally, like service/codex-shim)
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
const PROVIDER_USAGE = `Usage: ocx provider <subcommand>
|
|
367
|
+
|
|
368
|
+
Subcommands:
|
|
369
|
+
list List configured and available providers
|
|
370
|
+
add <name> Add a provider (registry or custom)
|
|
371
|
+
remove <name> Remove a configured provider
|
|
372
|
+
show <name> Show provider config details
|
|
373
|
+
set-default <name> Change the default provider
|
|
374
|
+
|
|
375
|
+
Examples:
|
|
376
|
+
ocx provider list
|
|
377
|
+
ocx provider add anthropic --api-key sk-ant-...
|
|
378
|
+
ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1
|
|
379
|
+
ocx provider show anthropic --json
|
|
380
|
+
ocx provider set-default anthropic
|
|
381
|
+
ocx provider remove my-ollama`;
|
|
382
|
+
|
|
383
|
+
export async function handleProviderCommand(args: string[]): Promise<void> {
|
|
384
|
+
const sub = args[0];
|
|
385
|
+
|
|
386
|
+
if (!sub || sub === "help" || hasHelpFlag(args)) {
|
|
387
|
+
console.log(PROVIDER_USAGE);
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const subArgs = args.slice(1);
|
|
392
|
+
|
|
393
|
+
switch (sub) {
|
|
394
|
+
case "list":
|
|
395
|
+
handleList(subArgs);
|
|
396
|
+
break;
|
|
397
|
+
case "add":
|
|
398
|
+
await handleAdd(subArgs);
|
|
399
|
+
break;
|
|
400
|
+
case "remove":
|
|
401
|
+
handleRemove(subArgs);
|
|
402
|
+
break;
|
|
403
|
+
case "show":
|
|
404
|
+
handleShow(subArgs);
|
|
405
|
+
break;
|
|
406
|
+
case "set-default":
|
|
407
|
+
handleSetDefault(subArgs);
|
|
408
|
+
break;
|
|
409
|
+
default:
|
|
410
|
+
console.error(`Unknown provider subcommand: ${sub}`);
|
|
411
|
+
console.error(PROVIDER_USAGE);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -537,7 +537,33 @@ switch (command) {
|
|
|
537
537
|
runGuiUpdateWorker(jobId, channel, args[3] === "restart");
|
|
538
538
|
break;
|
|
539
539
|
}
|
|
540
|
-
case "
|
|
540
|
+
case "restart": {
|
|
541
|
+
await handleStop();
|
|
542
|
+
await handleEnsure();
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "health": {
|
|
546
|
+
const healthArgs = args.slice(1);
|
|
547
|
+
const wantsHealthJson = healthArgs.includes("--json");
|
|
548
|
+
const live = await findLiveProxy();
|
|
549
|
+
if (wantsHealthJson) {
|
|
550
|
+
console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
|
|
551
|
+
} else {
|
|
552
|
+
console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
|
|
553
|
+
}
|
|
554
|
+
process.exit(live ? 0 : 1);
|
|
555
|
+
}
|
|
556
|
+
case "provider": {
|
|
557
|
+
const { handleProviderCommand } = await import("./cli-provider");
|
|
558
|
+
await handleProviderCommand(args.slice(1));
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case "models": {
|
|
562
|
+
const { handleModels } = await import("./cli-models");
|
|
563
|
+
handleModels(args.slice(1));
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "help":
|
|
541
567
|
case "--help":
|
|
542
568
|
case "-h":
|
|
543
569
|
case undefined:
|
package/src/codex-auth-api.ts
CHANGED
|
@@ -68,7 +68,7 @@ function poolAccountDto(
|
|
|
68
68
|
account: CodexAccount,
|
|
69
69
|
quotaResult: PoolQuotaResult,
|
|
70
70
|
hasCredential: boolean,
|
|
71
|
-
):
|
|
71
|
+
): CodexAuthAccountDto {
|
|
72
72
|
const quota = quotaForPlan(quotaResult.quota, account.plan);
|
|
73
73
|
return {
|
|
74
74
|
id: account.id,
|
|
@@ -229,6 +229,17 @@ interface PoolQuotaResult {
|
|
|
229
229
|
needsReauth: boolean;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
export interface CodexAuthAccountDto {
|
|
233
|
+
id: string;
|
|
234
|
+
email: string;
|
|
235
|
+
plan?: string | null;
|
|
236
|
+
logLabel?: string;
|
|
237
|
+
isMain: boolean;
|
|
238
|
+
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
239
|
+
needsReauth?: boolean;
|
|
240
|
+
hasCredential: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
232
243
|
async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, configuredPlan?: string): Promise<PoolQuotaResult> {
|
|
233
244
|
const existing = getAccountQuota(accountId);
|
|
234
245
|
if (!forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
|
|
@@ -313,6 +324,28 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
313
324
|
primeInFlight = null;
|
|
314
325
|
}
|
|
315
326
|
|
|
327
|
+
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
328
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
329
|
+
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(a => !a.isMain);
|
|
330
|
+
const mainInfo = await fetchMainAccountInfo(forceRefresh);
|
|
331
|
+
const withQuota = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
332
|
+
const cred = getCodexAccountCredential(a.id);
|
|
333
|
+
const quotaResult = cred
|
|
334
|
+
? await fetchPoolAccountQuota(a.id, forceRefresh, a.plan)
|
|
335
|
+
: { quota: null, needsReauth: true };
|
|
336
|
+
return poolAccountDto(a, quotaResult, !!cred);
|
|
337
|
+
});
|
|
338
|
+
const main: CodexAuthAccountDto = {
|
|
339
|
+
id: MAIN_CODEX_ACCOUNT_ID,
|
|
340
|
+
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
341
|
+
plan: mainInfo.plan,
|
|
342
|
+
isMain: true,
|
|
343
|
+
hasCredential: true,
|
|
344
|
+
quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
|
|
345
|
+
};
|
|
346
|
+
return [main, ...withQuota];
|
|
347
|
+
}
|
|
348
|
+
|
|
316
349
|
export async function handleCodexAuthAPI(
|
|
317
350
|
req: Request,
|
|
318
351
|
url: URL,
|
|
@@ -321,25 +354,7 @@ export async function handleCodexAuthAPI(
|
|
|
321
354
|
|
|
322
355
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
|
|
323
356
|
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
|
|
324
|
-
|
|
325
|
-
const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(a => !a.isMain);
|
|
326
|
-
const mainInfo = await fetchMainAccountInfo(forceRefresh);
|
|
327
|
-
const withQuota = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
|
|
328
|
-
const cred = getCodexAccountCredential(a.id);
|
|
329
|
-
const quotaResult = cred
|
|
330
|
-
? await fetchPoolAccountQuota(a.id, forceRefresh, a.plan)
|
|
331
|
-
: { quota: null, needsReauth: true };
|
|
332
|
-
return poolAccountDto(a, quotaResult, !!cred);
|
|
333
|
-
});
|
|
334
|
-
const main = {
|
|
335
|
-
id: MAIN_CODEX_ACCOUNT_ID,
|
|
336
|
-
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
337
|
-
plan: mainInfo.plan,
|
|
338
|
-
isMain: true,
|
|
339
|
-
hasCredential: true,
|
|
340
|
-
quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
|
|
341
|
-
};
|
|
342
|
-
return jsonResponse({ accounts: [main, ...withQuota] });
|
|
357
|
+
return jsonResponse({ accounts: await listCodexAuthAccounts(config, forceRefresh) });
|
|
343
358
|
}
|
|
344
359
|
|
|
345
360
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|