@bitkyc08/opencodex 2.23.0 → 2.24.1
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-rFWrIE11.js → index-C3FiAveG.js} +13 -13
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/claude/context-windows.ts +15 -3
- package/src/claude/model-info.ts +1 -1
- package/src/codex/catalog/metadata.ts +100 -24
- package/src/codex/catalog/parsing.ts +45 -11
- package/src/codex/catalog/provider-fetch.ts +4 -2
- package/src/codex/catalog/sync.ts +9 -7
- package/src/codex/catalog-admission.ts +5 -3
- package/src/codex/catalog.ts +1 -1
- package/src/generated/compatibility-version.json +16 -16
- package/src/lib/pinned-http.ts +7 -2
- package/src/lib/windows-elevation.ts +3 -3
- package/src/server/auth-cors.ts +40 -0
- package/src/server/chat-native.ts +32 -6
- package/src/server/management/config-routes.ts +79 -2
- package/src/service.ts +8 -0
- package/src/types.ts +1 -1
package/src/server/auth-cors.ts
CHANGED
|
@@ -493,6 +493,37 @@ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: Oc
|
|
|
493
493
|
return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
function positiveWindowValue(value: unknown): boolean {
|
|
497
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Shape-check the two context overlays before the canonical seed comparison drops them.
|
|
502
|
+
*
|
|
503
|
+
* `null` means "clear this" and is normalized by the PATCH field mask, but this validator
|
|
504
|
+
* also runs for POST and reload, where a whole provider object lands on disk verbatim. A
|
|
505
|
+
* `null` surviving there would be a value no reader expects, so full objects must carry a
|
|
506
|
+
* real number or omit the field.
|
|
507
|
+
*/
|
|
508
|
+
function nativeContextOverlayError(raw: Record<string, unknown>): string | null {
|
|
509
|
+
if (Object.hasOwn(raw, "contextWindow") && !positiveWindowValue(raw.contextWindow)) {
|
|
510
|
+
return "provider openai contextWindow must be a positive safe integer";
|
|
511
|
+
}
|
|
512
|
+
if (Object.hasOwn(raw, "modelContextWindows")) {
|
|
513
|
+
const windows = raw.modelContextWindows;
|
|
514
|
+
if (typeof windows !== "object" || windows === null || Array.isArray(windows)) {
|
|
515
|
+
return "provider openai modelContextWindows must be a plain object";
|
|
516
|
+
}
|
|
517
|
+
for (const [model, value] of Object.entries(windows as Record<string, unknown>)) {
|
|
518
|
+
if (model.trim() === "") return "provider openai modelContextWindows keys must be nonblank model ids";
|
|
519
|
+
if (!positiveWindowValue(value)) {
|
|
520
|
+
return "provider openai modelContextWindows values must be positive safe integers";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
496
527
|
/**
|
|
497
528
|
* Validate a provider object arriving at the management write boundary. Returns an error
|
|
498
529
|
* string, or null when the provider may be persisted. Caller-controlled names/fields are
|
|
@@ -522,6 +553,15 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
522
553
|
delete canonicalCandidate.modelCosts;
|
|
523
554
|
// requestPacing is a user-owned transport overlay, not part of the canonical seed.
|
|
524
555
|
delete canonicalCandidate.requestPacing;
|
|
556
|
+
// Context windows are the same kind of user-owned overlay as requestPacing: the operator
|
|
557
|
+
// narrowing what their own native rows advertise. They can only ever LOWER the measured
|
|
558
|
+
// window (see nativeOpenAiContextWindow), so admitting them cannot widen what the proxy
|
|
559
|
+
// claims. Validated first — this function also guards POST/reload, where nothing
|
|
560
|
+
// normalizes the shape afterwards, so a bad value would reach disk.
|
|
561
|
+
const contextOverlayError = nativeContextOverlayError(raw);
|
|
562
|
+
if (contextOverlayError) return contextOverlayError;
|
|
563
|
+
delete canonicalCandidate.contextWindow;
|
|
564
|
+
delete canonicalCandidate.modelContextWindows;
|
|
525
565
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
526
566
|
if (!canonical) {
|
|
527
567
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isChatCompletionsStreamError,
|
|
8
8
|
} from "../chat/outbound";
|
|
9
9
|
import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
|
|
10
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
10
11
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
11
12
|
import { redactSecretString } from "../lib/redact";
|
|
12
13
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type RequestLogContext,
|
|
40
41
|
} from "./request-log";
|
|
41
42
|
import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
|
|
43
|
+
import { registerTurn, unregisterTurn } from "./lifecycle";
|
|
42
44
|
|
|
43
45
|
type Rec = Record<string, unknown>;
|
|
44
46
|
|
|
@@ -78,7 +80,7 @@ interface HandleNativeChatOptions {
|
|
|
78
80
|
req: Request;
|
|
79
81
|
config: OcxConfig;
|
|
80
82
|
logCtx: RequestLogContext;
|
|
81
|
-
logIds?: { requestId: string; start: number };
|
|
83
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease };
|
|
82
84
|
route: RouteResult;
|
|
83
85
|
chatBody: Rec;
|
|
84
86
|
requestedModel: string;
|
|
@@ -122,6 +124,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
122
124
|
|
|
123
125
|
const upstream = new AbortController();
|
|
124
126
|
const cleanupAbort = linkAbortSignal(upstream, req.signal);
|
|
127
|
+
// nativeChatSse already owns the translated stream's async pull/cancel path.
|
|
128
|
+
// Bind the lease to that same controller and its terminal callbacks instead
|
|
129
|
+
// of adding another trackStreamLifetime wrapper (unsafe on bundled Bun#32111).
|
|
130
|
+
let streamTurnRegistered = false;
|
|
131
|
+
const transferTurnToStream = () => {
|
|
132
|
+
const lease = logIds?.turnAdmissionLease;
|
|
133
|
+
if (!lease || typeof (lease as { bindAbortController?: unknown }).bindAbortController !== "function") return;
|
|
134
|
+
registerTurn(upstream, lease);
|
|
135
|
+
streamTurnRegistered = true;
|
|
136
|
+
};
|
|
137
|
+
const releaseStreamTurn = () => {
|
|
138
|
+
if (!streamTurnRegistered) return;
|
|
139
|
+
streamTurnRegistered = false;
|
|
140
|
+
unregisterTurn(upstream);
|
|
141
|
+
};
|
|
125
142
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
126
143
|
let activeProvider: OcxProviderConfig = route.provider;
|
|
127
144
|
let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
|
|
@@ -276,6 +293,7 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
276
293
|
|
|
277
294
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
278
295
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
296
|
+
if (requestedStream) transferTurnToStream();
|
|
279
297
|
const stream = nativeChatSse(response.body, {
|
|
280
298
|
requestedModel,
|
|
281
299
|
translatorBudget,
|
|
@@ -287,13 +305,21 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
287
305
|
},
|
|
288
306
|
...(requestedStream ? {
|
|
289
307
|
onTerminal: (status: number, message?: string) => {
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
try {
|
|
309
|
+
cleanupAbort();
|
|
310
|
+
finishLog(status, message, "terminal");
|
|
311
|
+
} finally {
|
|
312
|
+
releaseStreamTurn();
|
|
313
|
+
}
|
|
292
314
|
},
|
|
293
315
|
onCancel: () => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
316
|
+
try {
|
|
317
|
+
cleanupAbort();
|
|
318
|
+
upstream.abort();
|
|
319
|
+
finishLog(499, undefined, "client_cancel");
|
|
320
|
+
} finally {
|
|
321
|
+
releaseStreamTurn();
|
|
322
|
+
}
|
|
297
323
|
},
|
|
298
324
|
} : {}),
|
|
299
325
|
});
|
|
@@ -37,7 +37,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
37
37
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
38
38
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
39
39
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
40
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
40
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
41
41
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
42
42
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
43
43
|
import {
|
|
@@ -118,6 +118,74 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
|
118
118
|
return { model, reasoning, models };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** One client's outcome from a fan-out sync. Absent from the list means "left alone". */
|
|
122
|
+
interface ClientIntegrationSyncOutcome {
|
|
123
|
+
readonly client: "grok" | "claude-desktop";
|
|
124
|
+
readonly ok: boolean;
|
|
125
|
+
readonly changed?: boolean;
|
|
126
|
+
readonly reason?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Re-inject every client integration the operator has switched ON.
|
|
131
|
+
*
|
|
132
|
+
* Only Codex used to run here, so a catalog change reached Codex and nothing else: a Grok
|
|
133
|
+
* fence or a written Desktop profile kept the context windows it was created with until the
|
|
134
|
+
* next `ocx start`. The startup path already gates each client on its own toggle
|
|
135
|
+
* (`src/cli/index.ts`), and this is that same fan-out for the on-demand command.
|
|
136
|
+
*
|
|
137
|
+
* A client that is OFF is omitted from the result rather than reported as skipped — the
|
|
138
|
+
* caller has to be able to tell "not touched" from "tried and failed". A client that fails
|
|
139
|
+
* does not fail the sync: Codex is the one that matters for routing, and a broken Grok file
|
|
140
|
+
* should surface as a warning, not as a 500 on a command that did its main job.
|
|
141
|
+
*/
|
|
142
|
+
async function syncEnabledClientIntegrations(
|
|
143
|
+
port: number | undefined,
|
|
144
|
+
config: OcxConfig,
|
|
145
|
+
): Promise<ClientIntegrationSyncOutcome[]> {
|
|
146
|
+
if (port === undefined) return [];
|
|
147
|
+
const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
|
|
148
|
+
const out: ClientIntegrationSyncOutcome[] = [];
|
|
149
|
+
|
|
150
|
+
if (grokIntegrationEnabled(config)) {
|
|
151
|
+
try {
|
|
152
|
+
const { syncGrokConfig } = await import("../../grok/sync");
|
|
153
|
+
const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
|
|
154
|
+
out.push(r.ok
|
|
155
|
+
? { client: "grok", ok: true, changed: r.changed === true }
|
|
156
|
+
: { client: "grok", ok: false, reason: r.message });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
out.push({ client: "grok", ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (claudeDesktopIntegrationEnabled(config)) {
|
|
163
|
+
try {
|
|
164
|
+
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
165
|
+
const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
|
|
166
|
+
const { fetchAllModels } = await import("../management-api");
|
|
167
|
+
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config)
|
|
168
|
+
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
|
|
169
|
+
const r = writeDesktop3pConfig(
|
|
170
|
+
port,
|
|
171
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
172
|
+
routed,
|
|
173
|
+
config.apiKeys?.[0]?.key,
|
|
174
|
+
"static",
|
|
175
|
+
config.claudeCode?.desktopProfile,
|
|
176
|
+
providerContextCap(config, OPENAI_CODEX_PROVIDER_ID),
|
|
177
|
+
);
|
|
178
|
+
out.push(r.written
|
|
179
|
+
? { client: "claude-desktop", ok: true, changed: true }
|
|
180
|
+
: { client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
|
|
181
|
+
} catch (error) {
|
|
182
|
+
out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
function publicVisionSidecarSettings(
|
|
122
190
|
config: OcxConfig,
|
|
123
191
|
vision: Awaited<ReturnType<typeof sidecarVisionResponseSettings>>,
|
|
@@ -387,10 +455,19 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
387
455
|
// Never use the server-captured startup object for a durable integration
|
|
388
456
|
// decision. A toggle may have persisted while this process was gathering.
|
|
389
457
|
const runtime = readRuntimePort(process.pid);
|
|
390
|
-
const
|
|
458
|
+
const config = loadConfig();
|
|
459
|
+
const result = await syncModelsToCodex(runtime?.port, config, null);
|
|
460
|
+
// A sync used to stop here, so a Grok fence or a Desktop profile kept whatever
|
|
461
|
+
// context windows it was written with while the Codex catalog moved on. The
|
|
462
|
+
// startup path already fans out to every enabled client; this is the same fan-out
|
|
463
|
+
// for the on-demand command. Codex goes first because the others read its catalog.
|
|
464
|
+
const integrations = result.status === "refused"
|
|
465
|
+
? []
|
|
466
|
+
: await syncEnabledClientIntegrations(runtime?.port, config);
|
|
391
467
|
const status = result.status === "refused" ? 409 : (result.status === "skipped" || result.ok ? 200 : 500);
|
|
392
468
|
return jsonResponse({
|
|
393
469
|
...attachStaleAppServerHint(result),
|
|
470
|
+
...(integrations.length > 0 ? { integrations } : {}),
|
|
394
471
|
...(result.ok ? {} : { error: result.message }),
|
|
395
472
|
}, status);
|
|
396
473
|
}
|
package/src/service.ts
CHANGED
|
@@ -1545,6 +1545,14 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
|
|
|
1545
1545
|
'>>"%OCX_SERVICE_LOG%" echo opencodex_home="%OPENCODEX_HOME%"',
|
|
1546
1546
|
'>>"%OCX_SERVICE_LOG%" echo codex_home="%CODEX_HOME%"',
|
|
1547
1547
|
'>>"%OCX_SERVICE_LOG%" echo token_file="%OCX_API_TOKEN_FILE%"',
|
|
1548
|
+
'if not exist "%OCX_BUN%" (',
|
|
1549
|
+
' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: bundled Bun is missing; reinstall opencodex, then run ocx service repair',
|
|
1550
|
+
" exit /b 3",
|
|
1551
|
+
")",
|
|
1552
|
+
'if not exist "%OCX_CLI%" (',
|
|
1553
|
+
' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair',
|
|
1554
|
+
" exit /b 3",
|
|
1555
|
+
")",
|
|
1548
1556
|
`"%OCX_BUN%" "%OCX_CLI%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1`,
|
|
1549
1557
|
"if %ERRORLEVEL% NEQ 0 (",
|
|
1550
1558
|
' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] child exited with code %ERRORLEVEL%; restarting in 5s',
|
package/src/types.ts
CHANGED
|
@@ -525,7 +525,7 @@ export interface OcxClaudeCodeConfig {
|
|
|
525
525
|
* (the legacy DISABLE_COMPACT pair takes rule-1 precedence in the CLI).
|
|
526
526
|
*/
|
|
527
527
|
autoContext?: boolean;
|
|
528
|
-
/** Compact-window tokens for auto-context. Default
|
|
528
|
+
/** Compact-window tokens for auto-context. Default 829_800 (AUTO_COMPACT_WINDOW_DEFAULT). */
|
|
529
529
|
autoCompactWindow?: number;
|
|
530
530
|
/**
|
|
531
531
|
* Bundled-skill content elision for ROUTED (non-Anthropic) models (devlog 260712
|