@bitkyc08/opencodex 2.7.35 → 2.7.36
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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +4 -2
- package/README.ru.md +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +52 -0
- package/gui/dist/assets/index-BpX-hoSd.css +1 -0
- package/gui/dist/assets/index-ZmFopEYw.js +52 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/cursor-errors.ts +38 -1
- package/src/adapters/cursor/discovery.ts +1 -0
- package/src/adapters/cursor/effort-map.ts +1 -0
- package/src/adapters/cursor/live-models.ts +22 -5
- package/src/adapters/cursor/live-transport.ts +82 -7
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +5 -2
- package/src/adapters/openai-responses.ts +64 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/help.ts +10 -0
- package/src/cli/index.ts +88 -9
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/status.ts +15 -4
- package/src/cli/tray-proxy.ts +52 -0
- package/src/codex/auth-api.ts +46 -5
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +268 -0
- package/src/codex/catalog/bundled.ts +188 -0
- package/src/codex/catalog/effort.ts +263 -0
- package/src/codex/catalog/metadata.ts +176 -0
- package/src/codex/catalog/parsing.ts +399 -0
- package/src/codex/catalog/provider-fetch.ts +609 -0
- package/src/codex/catalog/sync.ts +540 -0
- package/src/codex/catalog.ts +11 -2426
- package/src/codex/inject.ts +165 -3
- package/src/codex/shim.ts +141 -8
- package/src/codex/sync.ts +17 -2
- package/src/config.ts +23 -0
- package/src/lib/errors.ts +11 -0
- package/src/providers/antigravity-models.ts +33 -0
- package/src/providers/kiro-models.ts +2 -0
- package/src/providers/registry.ts +2 -2
- package/src/responses/state.ts +69 -6
- package/src/server/auth-cors.ts +3 -0
- package/src/server/management/agent-settings-routes.ts +536 -0
- package/src/server/management/combo-routes.ts +210 -0
- package/src/server/management/config-routes.ts +302 -0
- package/src/server/management/context.ts +21 -0
- package/src/server/management/logs-usage-routes.ts +176 -0
- package/src/server/management/model-routes.ts +253 -0
- package/src/server/management/oauth-account-routes.ts +301 -0
- package/src/server/management/provider-routes.ts +408 -0
- package/src/server/management/shared.ts +186 -0
- package/src/server/management-api.ts +23 -1806
- package/src/server/responses/collaboration.ts +300 -0
- package/src/server/responses/compact.ts +342 -0
- package/src/server/responses/core.ts +1498 -0
- package/src/server/responses/encrypted-payload.ts +231 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses.ts +9 -2172
- package/src/server/startup-action-control.ts +41 -0
- package/src/server/startup-health-cache.ts +100 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/service.ts +171 -19
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +628 -0
- package/src/types.ts +5 -0
- package/src/update/index.ts +43 -0
- package/src/update/job.ts +46 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +9 -2
- package/src/usage/summary.ts +42 -7
- package/gui/dist/assets/index-BunUANVE.js +0 -52
- package/gui/dist/assets/index-Sg-7L_oZ.css +0 -1
package/src/codex/inject.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { atomicWriteFile, loadConfig, websocketsEnabled } from "../config";
|
|
3
|
-
import { markJournalInjectedState, restoreJournalState, writeJournal } from "./journal";
|
|
3
|
+
import { markJournalInjectedState, removeJournal, restoreJournalState, writeJournal } from "./journal";
|
|
4
4
|
import { restoreCodexCatalog } from "./catalog";
|
|
5
5
|
import { migrateHistoryToOpenai, syncCodexHistoryProvider } from "./history-provider";
|
|
6
6
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH, DEFAULT_CATALOG_PATH, parseTomlString, readRootTomlString, resolveCodexConfigPath, tomlString } from "./paths";
|
|
7
|
+
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
7
8
|
import type { OcxConfig } from "../types";
|
|
8
9
|
|
|
9
10
|
const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
|
|
10
11
|
|
|
12
|
+
export function externalCodexModelProvider(content: string): string | null {
|
|
13
|
+
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
14
|
+
return provider && provider !== "openai" && provider !== "opencodex" ? provider : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function currentExternalCodexModelProvider(): string | null {
|
|
18
|
+
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
19
|
+
return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/**
|
|
12
23
|
* Detect the file's dominant line ending. Every transform in this module is LF-pure
|
|
13
24
|
* (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
|
|
@@ -151,7 +162,7 @@ export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
|
151
162
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
165
|
+
export function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
155
166
|
const lines = content.split("\n");
|
|
156
167
|
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
157
168
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
@@ -161,6 +172,137 @@ function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
|
161
172
|
return false;
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
176
|
+
|
|
177
|
+
function tomlStringPattern(key: string): RegExp {
|
|
178
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
const keyToken = `(?:${escaped}|\"${escaped}\"|'${escaped}')`;
|
|
180
|
+
return new RegExp(`^\\s*${keyToken}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function rootTomlString(content: string, key: string): string | null {
|
|
184
|
+
const lines = content.split("\n");
|
|
185
|
+
const firstTable = lines.findIndex(line => /^\s*\[/.test(line));
|
|
186
|
+
const rootLines = lines.slice(0, firstTable === -1 ? lines.length : firstTable);
|
|
187
|
+
const pattern = tomlStringPattern(key);
|
|
188
|
+
for (const line of rootLines) {
|
|
189
|
+
const match = pattern.exec(line);
|
|
190
|
+
if (match?.[1]) return match[1].trim();
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function providerTableStart(lines: string[], provider: string): number {
|
|
196
|
+
const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
|
+
const providerToken = `(?:${escapedProvider}|\"${escapedProvider}\"|'${escapedProvider}')`;
|
|
198
|
+
const header = new RegExp(`^\\s*\\[\\s*(?:model_providers|\"model_providers\"|'model_providers')\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`);
|
|
199
|
+
return lines.findIndex(line => header.test(line));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function providerTableString(content: string, provider: string, key: string): string | null {
|
|
203
|
+
const lines = content.split("\n");
|
|
204
|
+
const start = providerTableStart(lines, provider);
|
|
205
|
+
if (start === -1) return null;
|
|
206
|
+
const pattern = tomlStringPattern(key);
|
|
207
|
+
for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) {
|
|
208
|
+
const match = pattern.exec(lines[index]);
|
|
209
|
+
if (match?.[1]) return match[1].trim();
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
215
|
+
|
|
216
|
+
function ipv4Octets(hostname: string): number[] | null {
|
|
217
|
+
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
218
|
+
if (dotted) {
|
|
219
|
+
const octets = dotted.slice(1).map(Number);
|
|
220
|
+
return octets.some(octet => octet > 255) ? null : octets;
|
|
221
|
+
}
|
|
222
|
+
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
223
|
+
if (!mapped) return null;
|
|
224
|
+
const high = Number.parseInt(mapped[1], 16);
|
|
225
|
+
const low = Number.parseInt(mapped[2], 16);
|
|
226
|
+
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
230
|
+
try {
|
|
231
|
+
const url = new URL(value);
|
|
232
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
233
|
+
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
234
|
+
if (!hostname) return "unknown";
|
|
235
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) return "local";
|
|
236
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0") return "local";
|
|
237
|
+
const octets = ipv4Octets(hostname);
|
|
238
|
+
if (octets) {
|
|
239
|
+
if (octets.every(octet => octet === 0)) return "local";
|
|
240
|
+
if (octets[0] === 127) return "local";
|
|
241
|
+
return "remote";
|
|
242
|
+
}
|
|
243
|
+
if (/^::ffff:/i.test(hostname)) return "unknown";
|
|
244
|
+
return "remote";
|
|
245
|
+
} catch {
|
|
246
|
+
return "unknown";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Classify actual routing dependency separately from opencodex ownership. */
|
|
251
|
+
export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
252
|
+
const rootBaseUrl = rootTomlString(content, "openai_base_url");
|
|
253
|
+
if (rootBaseUrl) {
|
|
254
|
+
const endpoint = classifyRoutingEndpoint(rootBaseUrl);
|
|
255
|
+
if (endpoint === "unknown") return "unknown";
|
|
256
|
+
if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
|
|
257
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
258
|
+
}
|
|
259
|
+
const rootProvider = rootTomlString(content, "model_provider");
|
|
260
|
+
if (rootProvider) {
|
|
261
|
+
const providerTableExists = providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
262
|
+
const providerBaseUrl = providerTableString(content, rootProvider, "base_url");
|
|
263
|
+
if (providerBaseUrl) {
|
|
264
|
+
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
265
|
+
if (endpoint === "unknown") return "unknown";
|
|
266
|
+
if (rootProvider === "opencodex") return "opencodex-local";
|
|
267
|
+
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
268
|
+
}
|
|
269
|
+
if (rootProvider === "opencodex" || providerTableExists || rootProvider !== "openai") return "unknown";
|
|
270
|
+
}
|
|
271
|
+
return "native";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* True when the active Codex config is owned by opencodex routing. Covers the
|
|
276
|
+
* loopback Design B root override and the legacy/non-loopback provider table.
|
|
277
|
+
* A user-owned `openai_base_url` is intentionally not classified as injected.
|
|
278
|
+
*/
|
|
279
|
+
export function hasInjectedCodexRouting(content: string): boolean {
|
|
280
|
+
if (hasInjectedOpenaiBaseUrl(content)) return true;
|
|
281
|
+
return rootTomlString(content, "model_provider") === "opencodex"
|
|
282
|
+
&& providerTableString(content, "opencodex", "base_url") !== null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Read-only probe used by status, doctor, and the dashboard. */
|
|
286
|
+
export function isCodexRoutingInjected(): boolean {
|
|
287
|
+
const path = CODEX_CONFIG_PATH;
|
|
288
|
+
if (!existsSync(path)) return false;
|
|
289
|
+
try {
|
|
290
|
+
return hasInjectedCodexRouting(readFileSync(path, "utf8"));
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function getCodexRoutingKind(): CodexRoutingKind {
|
|
297
|
+
const path = CODEX_CONFIG_PATH;
|
|
298
|
+
if (!existsSync(path)) return "native";
|
|
299
|
+
try {
|
|
300
|
+
return classifyCodexRouting(readFileSync(path, "utf8"));
|
|
301
|
+
} catch {
|
|
302
|
+
return "unknown";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
164
306
|
/**
|
|
165
307
|
* Strip every existing `model_provider` line that we must not duplicate: any line set to
|
|
166
308
|
* "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
|
|
@@ -365,8 +507,23 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
365
507
|
return { success: false, message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?` };
|
|
366
508
|
}
|
|
367
509
|
|
|
368
|
-
writeJournal();
|
|
369
510
|
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
511
|
+
const activeProvider = externalCodexModelProvider(rawContent);
|
|
512
|
+
if (activeProvider) {
|
|
513
|
+
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
514
|
+
// replay that stale snapshot over externally managed config.
|
|
515
|
+
removeJournal();
|
|
516
|
+
return {
|
|
517
|
+
success: true,
|
|
518
|
+
message: `⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
519
|
+
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
520
|
+
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
521
|
+
`${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
|
|
522
|
+
` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
writeJournal();
|
|
370
527
|
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
371
528
|
const eol = dominantEol(rawContent);
|
|
372
529
|
let content = applyEol(rawContent, "\n");
|
|
@@ -543,6 +700,11 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
543
700
|
* handler, and `ocx restore`. Idempotent + atomic.
|
|
544
701
|
*/
|
|
545
702
|
export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
703
|
+
const activeProvider = currentExternalCodexModelProvider();
|
|
704
|
+
if (activeProvider) {
|
|
705
|
+
removeJournal();
|
|
706
|
+
return { success: true, message: `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.` };
|
|
707
|
+
}
|
|
546
708
|
const journal = restoreJournalState();
|
|
547
709
|
const cfg = journal.configRestored
|
|
548
710
|
? { success: true, message: "Codex config restored from opencodex journal." }
|
package/src/codex/shim.ts
CHANGED
|
@@ -34,6 +34,23 @@ const CODEX_INTERNAL_COMMANDS = [
|
|
|
34
34
|
"update",
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
+
// Codex accepts global options before a subcommand. The shim must skip the value belonging to
|
|
38
|
+
// these options before it decides which first positional token is the real subcommand. Keep this
|
|
39
|
+
// list aligned with `codex --help`; `--option=value` and attached short forms stay one token.
|
|
40
|
+
const CODEX_GLOBAL_OPTIONS_WITH_VALUE = [
|
|
41
|
+
"-c", "--config",
|
|
42
|
+
"--enable", "--disable",
|
|
43
|
+
"--remote", "--remote-auth-token-env",
|
|
44
|
+
"-i", "--image",
|
|
45
|
+
"-m", "--model",
|
|
46
|
+
"--local-provider",
|
|
47
|
+
"-p", "--profile",
|
|
48
|
+
"-s", "--sandbox",
|
|
49
|
+
"-C", "--cd",
|
|
50
|
+
"--add-dir",
|
|
51
|
+
"-a", "--ask-for-approval",
|
|
52
|
+
];
|
|
53
|
+
|
|
37
54
|
interface ShimState {
|
|
38
55
|
platform: NodeJS.Platform;
|
|
39
56
|
wrapperPath: string;
|
|
@@ -71,6 +88,17 @@ function isShim(path: string): boolean {
|
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
90
|
|
|
91
|
+
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
92
|
+
try {
|
|
93
|
+
const content = readFileSync(path, "utf8");
|
|
94
|
+
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
95
|
+
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* A PATH entry that reaches Windows through WSL drive interop
|
|
76
104
|
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
@@ -190,13 +218,40 @@ function shQuote(value: string): string {
|
|
|
190
218
|
|
|
191
219
|
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, tokenFile = serviceApiTokenFilePath()): string {
|
|
192
220
|
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
221
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
193
222
|
return `#!/usr/bin/env sh
|
|
194
223
|
# ${SHIM_MARKER}
|
|
195
224
|
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
196
225
|
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
197
226
|
export OPENCODEX_API_AUTH_TOKEN
|
|
198
227
|
fi
|
|
199
|
-
|
|
228
|
+
ocx_subcommand=""
|
|
229
|
+
ocx_skip_next=0
|
|
230
|
+
for ocx_arg in "$@"; do
|
|
231
|
+
if [ "$ocx_skip_next" -eq 1 ]; then
|
|
232
|
+
ocx_skip_next=0
|
|
233
|
+
continue
|
|
234
|
+
fi
|
|
235
|
+
case "$ocx_arg" in
|
|
236
|
+
--)
|
|
237
|
+
break
|
|
238
|
+
;;
|
|
239
|
+
${valueOptions})
|
|
240
|
+
ocx_skip_next=1
|
|
241
|
+
;;
|
|
242
|
+
--help|-h|--version|-V)
|
|
243
|
+
ocx_subcommand="$ocx_arg"
|
|
244
|
+
break
|
|
245
|
+
;;
|
|
246
|
+
-*)
|
|
247
|
+
;;
|
|
248
|
+
*)
|
|
249
|
+
ocx_subcommand="$ocx_arg"
|
|
250
|
+
break
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
253
|
+
done
|
|
254
|
+
case "$ocx_subcommand" in
|
|
200
255
|
${internalCommands}|--help|-h|--version|-V)
|
|
201
256
|
;;
|
|
202
257
|
*)
|
|
@@ -228,6 +283,7 @@ function windowsBatchSet(name: string, value: string): string {
|
|
|
228
283
|
|
|
229
284
|
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
230
285
|
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
286
|
+
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
231
287
|
return `@echo off\r
|
|
232
288
|
rem ${SHIM_MARKER}\r
|
|
233
289
|
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
@@ -236,11 +292,26 @@ ${windowsBatchSet("OCX_CLI", cliPath)}\r
|
|
|
236
292
|
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
237
293
|
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
238
294
|
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
295
|
+
goto scan_codex_args\r
|
|
296
|
+
:scan_codex_args\r
|
|
297
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
298
|
+
if "%~1"=="--" goto ensure_ocx\r
|
|
299
|
+
${valueOptionChecks}\r
|
|
239
300
|
${internalCommandChecks}\r
|
|
240
301
|
if /I "%~1"=="--help" goto run_codex\r
|
|
241
302
|
if /I "%~1"=="-h" goto run_codex\r
|
|
242
303
|
if /I "%~1"=="--version" goto run_codex\r
|
|
243
304
|
if /I "%~1"=="-V" goto run_codex\r
|
|
305
|
+
set "OCX_SCAN_ARG=%~1"\r
|
|
306
|
+
if "%OCX_SCAN_ARG:~0,1%"=="-" goto shift_codex_arg\r
|
|
307
|
+
goto ensure_ocx\r
|
|
308
|
+
:skip_option_value\r
|
|
309
|
+
shift\r
|
|
310
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
311
|
+
:shift_codex_arg\r
|
|
312
|
+
shift\r
|
|
313
|
+
goto scan_codex_args\r
|
|
314
|
+
:ensure_ocx\r
|
|
244
315
|
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
245
316
|
:run_codex\r
|
|
246
317
|
"%OCX_REAL_CODEX%" %*\r
|
|
@@ -253,6 +324,7 @@ function psString(value: string): string {
|
|
|
253
324
|
|
|
254
325
|
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
255
326
|
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
327
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
256
328
|
const tokenFile = serviceApiTokenFilePath();
|
|
257
329
|
return `#!/usr/bin/env pwsh
|
|
258
330
|
# ${SHIM_MARKER}
|
|
@@ -260,8 +332,20 @@ if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(t
|
|
|
260
332
|
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
|
|
261
333
|
}
|
|
262
334
|
$internalCommands = @(${internalCommands})
|
|
263
|
-
$
|
|
264
|
-
$
|
|
335
|
+
$valueOptions = @(${valueOptions})
|
|
336
|
+
$subcommand = ""
|
|
337
|
+
$skipNext = $false
|
|
338
|
+
foreach ($argValue in $args) {
|
|
339
|
+
$argText = [string]$argValue
|
|
340
|
+
if ($skipNext) { $skipNext = $false; continue }
|
|
341
|
+
if ($argText -eq "--") { break }
|
|
342
|
+
if ($valueOptions -contains $argText) { $skipNext = $true; continue }
|
|
343
|
+
if (@("--help", "-h", "--version", "-V") -contains $argText) { $subcommand = $argText; break }
|
|
344
|
+
if ($argText.StartsWith("-")) { continue }
|
|
345
|
+
$subcommand = $argText
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
265
349
|
if (-not $skipEnsure) {
|
|
266
350
|
& ${psString(bunPath)} ${psString(cliPath)} ensure *> $null
|
|
267
351
|
}
|
|
@@ -272,7 +356,25 @@ exit $LASTEXITCODE
|
|
|
272
356
|
|
|
273
357
|
function readState(): ShimState | null {
|
|
274
358
|
try {
|
|
275
|
-
|
|
359
|
+
const value = JSON.parse(readFileSync(statePath(), "utf8")) as unknown;
|
|
360
|
+
if (!value || typeof value !== "object") return null;
|
|
361
|
+
const state = value as Record<string, unknown>;
|
|
362
|
+
if (typeof state.platform !== "string") return null;
|
|
363
|
+
const validFile = (item: unknown): item is ShimFileState => {
|
|
364
|
+
if (!item || typeof item !== "object") return false;
|
|
365
|
+
const file = item as Record<string, unknown>;
|
|
366
|
+
return typeof file.wrapperPath === "string"
|
|
367
|
+
&& typeof file.originalPath === "string"
|
|
368
|
+
&& typeof file.backupPath === "string"
|
|
369
|
+
&& (file.realPath === undefined || typeof file.realPath === "string")
|
|
370
|
+
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
371
|
+
};
|
|
372
|
+
if (state.wrappers !== undefined) {
|
|
373
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return null;
|
|
374
|
+
} else if (!validFile(state)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return state as unknown as ShimState;
|
|
276
378
|
} catch {
|
|
277
379
|
return null;
|
|
278
380
|
}
|
|
@@ -431,13 +533,39 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
|
431
533
|
|
|
432
534
|
/** True if a Codex autostart shim is currently installed (state file present). */
|
|
433
535
|
export function isCodexShimInstalled(): boolean {
|
|
434
|
-
return
|
|
536
|
+
return diagnoseCodexShim().installed;
|
|
435
537
|
}
|
|
436
538
|
|
|
437
|
-
export
|
|
539
|
+
export interface CodexShimDiagnostic {
|
|
540
|
+
installed: boolean;
|
|
541
|
+
healthy: boolean;
|
|
542
|
+
summary: string;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
546
|
+
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
438
547
|
const state = readState();
|
|
439
|
-
if (!state)
|
|
440
|
-
|
|
548
|
+
if (!state) {
|
|
549
|
+
if (existsSync(statePath())) {
|
|
550
|
+
return {
|
|
551
|
+
installed: true,
|
|
552
|
+
healthy: false,
|
|
553
|
+
summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
installed: false,
|
|
558
|
+
healthy: false,
|
|
559
|
+
summary: "Codex autostart shim is not installed.",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const files = stateFiles(state);
|
|
563
|
+
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
564
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
565
|
+
: existsSync(file.wrapperPath)
|
|
566
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
567
|
+
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
568
|
+
const summary = files.map(file => {
|
|
441
569
|
const wrapper = existsSync(file.wrapperPath)
|
|
442
570
|
? isShim(file.wrapperPath)
|
|
443
571
|
? "shim present"
|
|
@@ -446,4 +574,9 @@ export function codexShimStatus(): string {
|
|
|
446
574
|
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
447
575
|
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
448
576
|
}).join("\n");
|
|
577
|
+
return { installed: true, healthy, summary };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function codexShimStatus(): string {
|
|
581
|
+
return diagnoseCodexShim().summary;
|
|
449
582
|
}
|
package/src/codex/sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { injectCodexConfig } from "./inject";
|
|
1
|
+
import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
|
|
2
2
|
import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
|
|
3
3
|
import { refreshCodexModelCatalog } from "./refresh";
|
|
4
4
|
import { applyProxyEnv, loadConfig } from "../config";
|
|
@@ -19,6 +19,7 @@ export interface CodexSyncResult {
|
|
|
19
19
|
interface CodexSyncDeps {
|
|
20
20
|
refreshCodexModelCatalog: typeof refreshCodexModelCatalog;
|
|
21
21
|
injectCodexConfig: typeof injectCodexConfig;
|
|
22
|
+
currentExternalCodexModelProvider?: typeof currentExternalCodexModelProvider;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const defaultDeps: CodexSyncDeps = {
|
|
@@ -32,8 +33,22 @@ export async function syncModelsToCodex(
|
|
|
32
33
|
log: Pick<Console, "log" | "error"> | null = console,
|
|
33
34
|
deps: CodexSyncDeps = defaultDeps,
|
|
34
35
|
): Promise<CodexSyncResult> {
|
|
35
|
-
applyProxyEnv(config); // `ocx ensure`/`ocx sync` fetch provider models outside the server process
|
|
36
36
|
const p = port ?? config.port ?? 10100;
|
|
37
|
+
const externalProvider = (deps.currentExternalCodexModelProvider ?? currentExternalCodexModelProvider)();
|
|
38
|
+
if (externalProvider) {
|
|
39
|
+
const result = await deps.injectCodexConfig(p, config, {});
|
|
40
|
+
log?.log(result.message);
|
|
41
|
+
return {
|
|
42
|
+
ok: result.success,
|
|
43
|
+
added: 0,
|
|
44
|
+
catalogPath: null,
|
|
45
|
+
catalogExists: false,
|
|
46
|
+
cacheSynced: false,
|
|
47
|
+
message: result.message,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
applyProxyEnv(config); // `ocx ensure`/`ocx sync` fetch provider models outside the server process
|
|
37
52
|
let added = 0;
|
|
38
53
|
let catalogPath: string | null = null;
|
|
39
54
|
let catalogPathForInjection: string | null | undefined;
|
package/src/config.ts
CHANGED
|
@@ -422,6 +422,18 @@ export function positiveIntegerConfigError(value: unknown, field: string): strin
|
|
|
422
422
|
return null;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
export function booleanRecordConfigError(value: unknown, field: string): string | null {
|
|
426
|
+
if (value === undefined) return null;
|
|
427
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
428
|
+
const prototype = Object.getPrototypeOf(value);
|
|
429
|
+
if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
|
|
430
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
431
|
+
if (!key.trim()) return `${field} keys must be nonblank model ids`;
|
|
432
|
+
if (typeof entry !== "boolean") return `${field}.${key} must be a boolean`;
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
const configSchema = z.object({
|
|
426
438
|
port: z.number().int().min(0).max(65535).default(10100),
|
|
427
439
|
providers: z.record(z.string(), providerConfigSchema),
|
|
@@ -505,6 +517,17 @@ const configSchema = z.object({
|
|
|
505
517
|
message: maxInputError,
|
|
506
518
|
});
|
|
507
519
|
}
|
|
520
|
+
const reasoningSummariesError = booleanRecordConfigError(
|
|
521
|
+
(provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
|
|
522
|
+
"modelSupportsReasoningSummaries",
|
|
523
|
+
);
|
|
524
|
+
if (reasoningSummariesError) {
|
|
525
|
+
ctx.addIssue({
|
|
526
|
+
code: "custom",
|
|
527
|
+
path: ["providers", name, "modelSupportsReasoningSummaries"],
|
|
528
|
+
message: reasoningSummariesError,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
508
531
|
const defaultMaxOutputError = positiveIntegerConfigError(
|
|
509
532
|
(provider as { defaultMaxOutputTokens?: unknown }).defaultMaxOutputTokens,
|
|
510
533
|
"defaultMaxOutputTokens",
|
package/src/lib/errors.ts
CHANGED
|
@@ -99,9 +99,18 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
99
99
|
) {
|
|
100
100
|
return { message, type: "invalid_request_error", code: "context_length_exceeded" };
|
|
101
101
|
}
|
|
102
|
+
// "Cursor resource limit exceeded" is emitted only for explicit request-size overflow
|
|
103
|
+
// details (isCursorRequestTooLargeDetail in cursor-errors.ts); quota-style resource
|
|
104
|
+
// exhaustion arrives as "Cursor rate limit exceeded" and falls through to 429 below.
|
|
102
105
|
if (text.includes("cursor resource limit exceeded")) {
|
|
103
106
|
return { message, type: "invalid_request_error", code: "tool_catalog_too_large" };
|
|
104
107
|
}
|
|
108
|
+
// The Cursor adapter's classified rate-limit prefix is authoritative: its DETAIL may echo
|
|
109
|
+
// quota wording ("... quota exhausted") that would otherwise hit the insufficient_quota
|
|
110
|
+
// branch below and break the planned retry-with-backoff contract (WP3 review blocker 1).
|
|
111
|
+
if (text.includes("cursor rate limit exceeded")) {
|
|
112
|
+
return { message, type: "rate_limit_error", code: "rate_limit_exceeded" };
|
|
113
|
+
}
|
|
105
114
|
if (
|
|
106
115
|
text.includes("insufficient_quota") ||
|
|
107
116
|
text.includes("exceeded your current quota") ||
|
|
@@ -202,6 +211,8 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
202
211
|
const lower = message.toLowerCase();
|
|
203
212
|
// Client aborts (e.g. mid web-search loop) must not look like upstream 502s in /api/logs.
|
|
204
213
|
if (isClientClosedMessage(lower)) return 499;
|
|
214
|
+
// See classifyError: this prefix now only means explicit request-size overflow (400);
|
|
215
|
+
// quota-style Cursor resource exhaustion carries the rate-limit prefix and maps to 429.
|
|
205
216
|
if (lower.includes("cursor resource limit exceeded")) return 400;
|
|
206
217
|
if (
|
|
207
218
|
lower.includes("resource_exhausted") ||
|
|
@@ -167,3 +167,36 @@ export function resolveAntigravityEffortWireModel(
|
|
|
167
167
|
// Rule 5: everything else.
|
|
168
168
|
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// ── Usage aggregation reverse map (picker/call base identity) ──
|
|
173
|
+
// Effort wire IDs and compatibility aliases must collapse to the same picker-visible
|
|
174
|
+
// base model that users invoke after effort routing. Historical logs store suffix IDs;
|
|
175
|
+
// summary aggregation consults this reverse map so one call model = one usage row.
|
|
176
|
+
const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
|
|
177
|
+
const rev: Record<string, string> = {};
|
|
178
|
+
for (const base of ANTIGRAVITY_MODELS) rev[base] = base;
|
|
179
|
+
for (const [base, effortMap] of Object.entries(ANTIGRAVITY_EFFORT_WIRE_MAP)) {
|
|
180
|
+
rev[base] = base;
|
|
181
|
+
for (const wire of Object.values(effortMap)) rev[wire] = base;
|
|
182
|
+
}
|
|
183
|
+
for (const [alias, wire] of Object.entries(ANTIGRAVITY_MODEL_ALIASES)) {
|
|
184
|
+
const base = rev[wire] ?? rev[alias];
|
|
185
|
+
if (base) rev[alias] = base;
|
|
186
|
+
// If alias is itself a base/wire already mapped, keep that mapping.
|
|
187
|
+
else if (rev[wire]) rev[alias] = rev[wire]!;
|
|
188
|
+
}
|
|
189
|
+
// Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
|
|
190
|
+
// included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
|
|
191
|
+
return rev;
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Canonical picker/call model id for usage aggregation.
|
|
196
|
+
* Unknown ids stay identity so future CCA models do not invent mappings.
|
|
197
|
+
*/
|
|
198
|
+
export function canonicalAntigravityUsageModel(modelId: string): string {
|
|
199
|
+
const id = modelId.trim();
|
|
200
|
+
if (!id) return modelId;
|
|
201
|
+
return ANTIGRAVITY_USAGE_BASE_BY_ID[id] ?? id;
|
|
202
|
+
}
|
|
@@ -5,6 +5,7 @@ export const KIRO_MODELS = [
|
|
|
5
5
|
"gpt-5.6-terra",
|
|
6
6
|
"gpt-5.6-luna",
|
|
7
7
|
"claude-sonnet-5",
|
|
8
|
+
"claude-opus-5",
|
|
8
9
|
"claude-opus-4.8",
|
|
9
10
|
"claude-opus-4.7",
|
|
10
11
|
"claude-opus-4.6",
|
|
@@ -28,6 +29,7 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
28
29
|
"gpt-5.6-terra": 272_000,
|
|
29
30
|
"gpt-5.6-luna": 272_000,
|
|
30
31
|
"claude-sonnet-5": 1_000_000,
|
|
32
|
+
"claude-opus-5": 1_000_000,
|
|
31
33
|
"claude-opus-4.8": 1_000_000,
|
|
32
34
|
"claude-opus-4.7": 1_000_000,
|
|
33
35
|
"claude-opus-4.6": 1_000_000,
|
|
@@ -100,8 +100,8 @@ export type ProviderConfigSeed = Pick<
|
|
|
100
100
|
// same static model seed.
|
|
101
101
|
// 260710 context refresh: Tier-2 evidence in
|
|
102
102
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
103
|
-
const ANTHROPIC_MODELS = ["claude-fable-5", "claude-sonnet-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
|
|
104
|
-
const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-haiku-4-5": 200_000 };
|
|
103
|
+
const ANTHROPIC_MODELS = ["claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
|
|
104
|
+
const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-haiku-4-5": 200_000 };
|
|
105
105
|
|
|
106
106
|
const ZAI_GLM_52_MODELS = ["glm-5.2", "glm-5.2[1m]"];
|
|
107
107
|
const ZAI_GLM_52_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|