@bitkyc08/opencodex 2.39.0 → 2.40.0
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.md +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
package/src/codex/inject.ts
CHANGED
|
@@ -139,6 +139,70 @@ export interface InjectCodexOptions {
|
|
|
139
139
|
* provider discovery so a deterministic config refusal cannot degrade an existing catalog.
|
|
140
140
|
*/
|
|
141
141
|
validateOnly?: boolean;
|
|
142
|
+
/** Explicit remote routing target. Absence preserves byte-compatible standalone output. */
|
|
143
|
+
routingTarget?: CodexRoutingTarget;
|
|
144
|
+
journalOwner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface CodexRoutingTarget {
|
|
148
|
+
baseUrl: string;
|
|
149
|
+
requiresAdmissionToken: boolean;
|
|
150
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN";
|
|
151
|
+
/**
|
|
152
|
+
* Opt-in authless Codex Desktop mode (#1107): inject the dedicated provider table with
|
|
153
|
+
* `requires_openai_auth = false` so Desktop skips the ChatGPT login gate. Only ever true for
|
|
154
|
+
* loopback targets that need no admission token; non-loopback admission is a separate layer
|
|
155
|
+
* and is never weakened by this flag.
|
|
156
|
+
*/
|
|
157
|
+
desktopAuthless?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTarget {
|
|
161
|
+
let parsed: URL;
|
|
162
|
+
try {
|
|
163
|
+
parsed = new URL(target.baseUrl);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new TypeError("Codex routing target must be an absolute HTTP(S) /v1 URL");
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
169
|
+
|| parsed.username
|
|
170
|
+
|| parsed.password
|
|
171
|
+
|| parsed.pathname !== "/v1"
|
|
172
|
+
|| parsed.search
|
|
173
|
+
|| parsed.hash
|
|
174
|
+
|| target.tokenEnv !== "OPENCODEX_API_AUTH_TOKEN"
|
|
175
|
+
) {
|
|
176
|
+
throw new TypeError("Codex routing target must be a canonical HTTP(S) /v1 URL without credentials, query, or fragment");
|
|
177
|
+
}
|
|
178
|
+
return { ...target, baseUrl: `${parsed.origin}/v1` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Provider-table form is used for non-loopback admission and for the authless Desktop opt-in. */
|
|
182
|
+
function usesProviderTable(target: CodexRoutingTarget): boolean {
|
|
183
|
+
return target.requiresAdmissionToken || target.desktopAuthless === true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function standaloneCodexRoutingTarget(
|
|
187
|
+
port: number,
|
|
188
|
+
config?: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless">,
|
|
189
|
+
): CodexRoutingTarget {
|
|
190
|
+
const loopback = config?.unauthenticatedLoopbackListener;
|
|
191
|
+
const effectivePort = loopback?.enabled ? loopback.port : port;
|
|
192
|
+
const hostname = loopback?.enabled ? undefined : config?.hostname;
|
|
193
|
+
const requiresAdmissionToken = loopback?.enabled ? false : shouldInjectApiAuthHeader(config);
|
|
194
|
+
return {
|
|
195
|
+
baseUrl: `http://${providerBaseHost(hostname)}:${effectivePort}/v1`,
|
|
196
|
+
requiresAdmissionToken,
|
|
197
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
198
|
+
...(config?.codexDesktopAuthless === true && !requiresAdmissionToken
|
|
199
|
+
? { desktopAuthless: true }
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function routingTargetOrigin(target: CodexRoutingTarget): string {
|
|
205
|
+
return target.baseUrl.slice(0, -3);
|
|
142
206
|
}
|
|
143
207
|
|
|
144
208
|
function configuredManagedSubagentDefaults(
|
|
@@ -213,28 +277,52 @@ export function shouldInjectApiAuthHeader(
|
|
|
213
277
|
|
|
214
278
|
export function buildProviderTableBlock(
|
|
215
279
|
port: number,
|
|
280
|
+
supportsWebsockets?: boolean,
|
|
281
|
+
includeApiAuthHeader?: boolean,
|
|
282
|
+
hostname?: string,
|
|
283
|
+
): string;
|
|
284
|
+
export function buildProviderTableBlock(
|
|
285
|
+
target: CodexRoutingTarget,
|
|
286
|
+
supportsWebsockets?: boolean,
|
|
287
|
+
): string;
|
|
288
|
+
export function buildProviderTableBlock(
|
|
289
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
216
290
|
supportsWebsockets = false,
|
|
217
291
|
includeApiAuthHeader = false,
|
|
218
292
|
hostname?: string,
|
|
219
293
|
): string {
|
|
220
|
-
const
|
|
294
|
+
const target = typeof portOrTarget === "number"
|
|
295
|
+
? validateCodexRoutingTarget({
|
|
296
|
+
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
297
|
+
requiresAdmissionToken: includeApiAuthHeader,
|
|
298
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
299
|
+
})
|
|
300
|
+
: validateCodexRoutingTarget(portOrTarget);
|
|
301
|
+
return buildProviderTableBlockForTarget(target, supportsWebsockets);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function buildProviderTableBlockForTarget(
|
|
305
|
+
target: CodexRoutingTarget,
|
|
306
|
+
supportsWebsockets = false,
|
|
307
|
+
): string {
|
|
221
308
|
const lines = [
|
|
222
309
|
"",
|
|
223
310
|
OCX_SECTION_MARKER,
|
|
224
311
|
"[model_providers.opencodex]",
|
|
225
312
|
'name = "OpenCodex Proxy"',
|
|
226
|
-
`base_url =
|
|
313
|
+
`base_url = ${tomlString(target.baseUrl)}`,
|
|
227
314
|
'wire_api = "responses"',
|
|
228
|
-
|
|
315
|
+
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
316
|
+
`requires_openai_auth = ${target.desktopAuthless === true ? "false" : "true"}`,
|
|
229
317
|
];
|
|
230
|
-
if (
|
|
318
|
+
if (target.requiresAdmissionToken) {
|
|
231
319
|
// codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
|
|
232
320
|
// hard-errors on a missing/empty variable instead of silently omitting auth. It
|
|
233
321
|
// coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
|
|
234
322
|
// login/account UX), and the server substitutes stored main auth for our admission
|
|
235
323
|
// bearer (#1686), so the modern form is strictly better than the legacy
|
|
236
324
|
// env_http_headers table this line used to emit.
|
|
237
|
-
lines.push(
|
|
325
|
+
lines.push(`env_key = ${tomlString(target.tokenEnv)}`);
|
|
238
326
|
}
|
|
239
327
|
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
240
328
|
return lines.join("\n") + "\n";
|
|
@@ -243,8 +331,19 @@ export function buildProviderTableBlock(
|
|
|
243
331
|
export function buildOpenaiBaseUrlLine(
|
|
244
332
|
port: number,
|
|
245
333
|
hostname?: string,
|
|
334
|
+
): string;
|
|
335
|
+
export function buildOpenaiBaseUrlLine(target: CodexRoutingTarget): string;
|
|
336
|
+
export function buildOpenaiBaseUrlLine(
|
|
337
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
338
|
+
hostname?: string,
|
|
246
339
|
): string {
|
|
247
|
-
return
|
|
340
|
+
return typeof portOrTarget === "number"
|
|
341
|
+
? `openai_base_url = "http://${providerBaseHost(hostname)}:${portOrTarget}/v1"`
|
|
342
|
+
: buildOpenaiBaseUrlLineForTarget(validateCodexRoutingTarget(portOrTarget));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildOpenaiBaseUrlLineForTarget(target: CodexRoutingTarget): string {
|
|
346
|
+
return `openai_base_url = ${tomlString(target.baseUrl)}`;
|
|
248
347
|
}
|
|
249
348
|
|
|
250
349
|
/**
|
|
@@ -257,11 +356,23 @@ export function setRootOpenaiBaseUrl(
|
|
|
257
356
|
content: string,
|
|
258
357
|
port: number,
|
|
259
358
|
hostname?: string,
|
|
359
|
+
): { content: string; keptUserBaseUrl: boolean };
|
|
360
|
+
export function setRootOpenaiBaseUrl(
|
|
361
|
+
content: string,
|
|
362
|
+
target: CodexRoutingTarget,
|
|
363
|
+
): { content: string; keptUserBaseUrl: boolean };
|
|
364
|
+
export function setRootOpenaiBaseUrl(
|
|
365
|
+
content: string,
|
|
366
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
367
|
+
hostname?: string,
|
|
260
368
|
): { content: string; keptUserBaseUrl: boolean } {
|
|
369
|
+
if (typeof portOrTarget !== "number") {
|
|
370
|
+
return setRootOpenaiBaseUrlForTarget(content, validateCodexRoutingTarget(portOrTarget));
|
|
371
|
+
}
|
|
261
372
|
const lines = content.split("\n");
|
|
262
373
|
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
263
374
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
264
|
-
const key = buildOpenaiBaseUrlLine(
|
|
375
|
+
const key = buildOpenaiBaseUrlLine(portOrTarget, hostname);
|
|
265
376
|
|
|
266
377
|
for (let i = 0; i < rootEnd; i++) {
|
|
267
378
|
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
@@ -289,6 +400,33 @@ export function setRootOpenaiBaseUrl(
|
|
|
289
400
|
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
290
401
|
}
|
|
291
402
|
|
|
403
|
+
function setRootOpenaiBaseUrlForTarget(
|
|
404
|
+
content: string,
|
|
405
|
+
target: CodexRoutingTarget,
|
|
406
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
407
|
+
const lines = content.split("\n");
|
|
408
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
409
|
+
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
410
|
+
const key = buildOpenaiBaseUrlLineForTarget(target);
|
|
411
|
+
for (let index = 0; index < rootEnd; index += 1) {
|
|
412
|
+
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
413
|
+
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
414
|
+
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
415
|
+
lines[index] = key;
|
|
416
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
417
|
+
}
|
|
418
|
+
if (firstTable === -1) {
|
|
419
|
+
return {
|
|
420
|
+
content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
|
|
421
|
+
keptUserBaseUrl: false,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
let insertAt = firstTable;
|
|
425
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
|
|
426
|
+
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
427
|
+
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
428
|
+
}
|
|
429
|
+
|
|
292
430
|
/**
|
|
293
431
|
* Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
|
|
294
432
|
* A user's own root override (no marker) survives; an orphaned marker with no key line after
|
|
@@ -619,17 +757,48 @@ function stripOpencodexCatalogPath(content: string): string {
|
|
|
619
757
|
.join("\n");
|
|
620
758
|
}
|
|
621
759
|
|
|
622
|
-
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets
|
|
623
|
-
|
|
760
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets?: boolean, includeApiAuthHeader?: boolean, hostname?: string, fastMode?: boolean): string;
|
|
761
|
+
export function buildProfileFile(target: CodexRoutingTarget, catalogPath?: string | null, supportsWebsockets?: boolean, fastMode?: boolean): string;
|
|
762
|
+
export function buildProfileFile(
|
|
763
|
+
portOrTarget: number | CodexRoutingTarget,
|
|
764
|
+
catalogPath?: string | null,
|
|
765
|
+
supportsWebsockets = false,
|
|
766
|
+
includeApiAuthHeaderOrFastMode?: boolean,
|
|
767
|
+
hostname?: string,
|
|
768
|
+
fastMode?: boolean,
|
|
769
|
+
): string {
|
|
770
|
+
const target = typeof portOrTarget === "number"
|
|
771
|
+
? validateCodexRoutingTarget({
|
|
772
|
+
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
773
|
+
requiresAdmissionToken: includeApiAuthHeaderOrFastMode === true,
|
|
774
|
+
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
775
|
+
})
|
|
776
|
+
: validateCodexRoutingTarget(portOrTarget);
|
|
777
|
+
return buildProfileFileForTarget(
|
|
778
|
+
target,
|
|
779
|
+
catalogPath,
|
|
780
|
+
supportsWebsockets,
|
|
781
|
+
typeof portOrTarget === "number" ? fastMode : includeApiAuthHeaderOrFastMode,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function buildProfileFileForTarget(
|
|
786
|
+
target: CodexRoutingTarget,
|
|
787
|
+
catalogPath?: string | null,
|
|
788
|
+
supportsWebsockets = false,
|
|
789
|
+
fastMode?: boolean,
|
|
790
|
+
): string {
|
|
791
|
+
const origin = routingTargetOrigin(target);
|
|
792
|
+
const host = new URL(origin).host;
|
|
624
793
|
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
625
794
|
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
626
|
-
// the x-opencodex-api-key env header).
|
|
627
|
-
if (!
|
|
795
|
+
// the x-opencodex-api-key env header); the authless Desktop opt-in shares that shape.
|
|
796
|
+
if (!usesProviderTable(target)) {
|
|
628
797
|
const lines = [
|
|
629
798
|
"# OpenCodex proxy fallback config (Design B)",
|
|
630
|
-
`# Root override that points Codex's built-in openai provider at the proxy on ${host}
|
|
799
|
+
`# Root override that points Codex's built-in openai provider at the proxy on ${host}.`,
|
|
631
800
|
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
632
|
-
|
|
801
|
+
buildOpenaiBaseUrlLineForTarget(target),
|
|
633
802
|
];
|
|
634
803
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
635
804
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
@@ -637,12 +806,12 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
637
806
|
}
|
|
638
807
|
const lines = [
|
|
639
808
|
"# OpenCodex proxy profile — use with: codex --profile opencodex",
|
|
640
|
-
`# Routes all model requests through the opencodex proxy at ${host}
|
|
809
|
+
`# Routes all model requests through the opencodex proxy at ${host}`,
|
|
641
810
|
'model_provider = "opencodex"',
|
|
642
811
|
];
|
|
643
812
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
644
813
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
645
|
-
lines.push(
|
|
814
|
+
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
646
815
|
return lines.join("\n");
|
|
647
816
|
}
|
|
648
817
|
|
|
@@ -684,8 +853,14 @@ export async function injectCodexConfig(
|
|
|
684
853
|
//
|
|
685
854
|
// The listener port is fixed in config, never OS-assigned, so this value survives restarts
|
|
686
855
|
// and matches what an already-running app-server read at startup.
|
|
687
|
-
|
|
688
|
-
|
|
856
|
+
let routingTarget: CodexRoutingTarget;
|
|
857
|
+
try {
|
|
858
|
+
routingTarget = options.routingTarget
|
|
859
|
+
? validateCodexRoutingTarget(options.routingTarget)
|
|
860
|
+
: standaloneCodexRoutingTarget(port, config);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
return { success: false, message: error instanceof Error ? error.message : "Invalid Codex routing target" };
|
|
863
|
+
}
|
|
689
864
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
690
865
|
return {
|
|
691
866
|
success: false,
|
|
@@ -712,8 +887,8 @@ export async function injectCodexConfig(
|
|
|
712
887
|
message:
|
|
713
888
|
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
714
889
|
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
715
|
-
` Configure that provider for Responses passthrough at
|
|
716
|
-
`${
|
|
890
|
+
` Configure that provider for Responses passthrough at ${routingTarget.baseUrl}` +
|
|
891
|
+
`${routingTarget.requiresAdmissionToken ? ` with x-opencodex-api-key from ${routingTarget.tokenEnv}` : ""}.\n` +
|
|
717
892
|
` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
|
|
718
893
|
};
|
|
719
894
|
}
|
|
@@ -781,28 +956,26 @@ export async function injectCodexConfig(
|
|
|
781
956
|
? setRootModelCatalogPath(content, catalogPath)
|
|
782
957
|
: stripOpencodexCatalogPath(content);
|
|
783
958
|
|
|
784
|
-
|
|
959
|
+
// Provider-table form: non-loopback admission (legacy) or the authless Desktop opt-in (#1107).
|
|
960
|
+
const legacyMode = usesProviderTable(routingTarget);
|
|
785
961
|
let keptUserBaseUrl = false;
|
|
786
962
|
if (legacyMode) {
|
|
787
963
|
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
788
964
|
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
965
|
+
// The authless opt-in needs the same table because only a dedicated provider can carry
|
|
966
|
+
// requires_openai_auth = false.
|
|
789
967
|
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
790
968
|
content = setRootModelProvider(content);
|
|
791
969
|
// 2) Provider table appended at EOF (position-independent).
|
|
792
970
|
content =
|
|
793
971
|
content.trimEnd() +
|
|
794
972
|
"\n" +
|
|
795
|
-
|
|
796
|
-
port,
|
|
797
|
-
websocketsEnabled(config ?? {}),
|
|
798
|
-
true,
|
|
799
|
-
config?.hostname,
|
|
800
|
-
);
|
|
973
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
|
|
801
974
|
} else {
|
|
802
975
|
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
803
976
|
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
804
977
|
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
805
|
-
const result =
|
|
978
|
+
const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
806
979
|
content = result.content;
|
|
807
980
|
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
808
981
|
}
|
|
@@ -838,7 +1011,12 @@ export async function injectCodexConfig(
|
|
|
838
1011
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
839
1012
|
}
|
|
840
1013
|
|
|
841
|
-
const profileContent =
|
|
1014
|
+
const profileContent = buildProfileFileForTarget(
|
|
1015
|
+
routingTarget,
|
|
1016
|
+
catalogPath,
|
|
1017
|
+
websocketsEnabled(config ?? {}),
|
|
1018
|
+
config?.fastMode,
|
|
1019
|
+
);
|
|
842
1020
|
content = applyEol(content, eol);
|
|
843
1021
|
|
|
844
1022
|
/*
|
|
@@ -916,6 +1094,7 @@ export async function injectCodexConfig(
|
|
|
916
1094
|
writeJournal({
|
|
917
1095
|
currentStateIsNative: !hasInjectedCodexRouting(rawContent),
|
|
918
1096
|
configContent: baselineContent,
|
|
1097
|
+
owner: options.journalOwner,
|
|
919
1098
|
});
|
|
920
1099
|
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
921
1100
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
@@ -1118,9 +1297,11 @@ export async function injectCodexConfig(
|
|
|
1118
1297
|
` Reference config: ${CODEX_PROFILE_PATH}`,
|
|
1119
1298
|
};
|
|
1120
1299
|
}
|
|
1121
|
-
const headline =
|
|
1122
|
-
? `Injected opencodex as default provider into Codex config.\n`
|
|
1123
|
-
:
|
|
1300
|
+
const headline = routingTarget.desktopAuthless === true
|
|
1301
|
+
? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n`
|
|
1302
|
+
: legacyMode
|
|
1303
|
+
? `Injected opencodex as default provider into Codex config.\n`
|
|
1304
|
+
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
|
|
1124
1305
|
return {
|
|
1125
1306
|
success: true,
|
|
1126
1307
|
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
package/src/codex/journal.ts
CHANGED
|
@@ -16,6 +16,10 @@ import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
|
|
|
16
16
|
*/
|
|
17
17
|
export const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
|
|
18
18
|
|
|
19
|
+
export type JournalOwner =
|
|
20
|
+
| { kind: "process"; pid: number }
|
|
21
|
+
| { kind: "client"; apiKeyId: string };
|
|
22
|
+
|
|
19
23
|
interface Journal {
|
|
20
24
|
version: 1;
|
|
21
25
|
originalConfig: string;
|
|
@@ -41,10 +45,11 @@ interface Journal {
|
|
|
41
45
|
*/
|
|
42
46
|
injectedCatalogPath?: string | null;
|
|
43
47
|
pid: number;
|
|
48
|
+
owner?: JournalOwner;
|
|
44
49
|
timestamp: string;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
interface RestoreJournalResult {
|
|
52
|
+
export interface RestoreJournalResult {
|
|
48
53
|
configRestored: boolean;
|
|
49
54
|
profileRestored: boolean;
|
|
50
55
|
configChanged: boolean;
|
|
@@ -71,6 +76,7 @@ export interface WriteJournalOptions {
|
|
|
71
76
|
* another process rewrites config.toml mid-flight.
|
|
72
77
|
*/
|
|
73
78
|
configContent?: string;
|
|
79
|
+
owner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/**
|
|
@@ -103,6 +109,9 @@ export function writeJournal(options: WriteJournalOptions = {}): void {
|
|
|
103
109
|
originalConfig: Buffer.from(config).toString("base64"),
|
|
104
110
|
originalProfile: profile ? Buffer.from(profile).toString("base64") : null,
|
|
105
111
|
pid: process.pid,
|
|
112
|
+
owner: options.owner?.kind === "client"
|
|
113
|
+
? { kind: "client", apiKeyId: options.owner.apiKeyId }
|
|
114
|
+
: { kind: "process", pid: process.pid },
|
|
106
115
|
timestamp: new Date().toISOString(),
|
|
107
116
|
};
|
|
108
117
|
atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
|
|
@@ -168,6 +177,20 @@ function readJournal(): Journal | null {
|
|
|
168
177
|
}
|
|
169
178
|
}
|
|
170
179
|
|
|
180
|
+
export function journalOwner(): JournalOwner | null {
|
|
181
|
+
const journal = readJournal();
|
|
182
|
+
if (!journal) return null;
|
|
183
|
+
if (journal.owner?.kind === "client" && typeof journal.owner.apiKeyId === "string" && journal.owner.apiKeyId) {
|
|
184
|
+
return { kind: "client", apiKeyId: journal.owner.apiKeyId };
|
|
185
|
+
}
|
|
186
|
+
if (journal.owner?.kind === "process" && Number.isSafeInteger(journal.owner.pid) && journal.owner.pid > 0) {
|
|
187
|
+
return { kind: "process", pid: journal.owner.pid };
|
|
188
|
+
}
|
|
189
|
+
return Number.isSafeInteger(journal.pid) && journal.pid > 0
|
|
190
|
+
? { kind: "process", pid: journal.pid }
|
|
191
|
+
: null;
|
|
192
|
+
}
|
|
193
|
+
|
|
171
194
|
export function restoreJournalState(): RestoreJournalResult {
|
|
172
195
|
const journal = readJournal();
|
|
173
196
|
if (!journal) {
|
|
@@ -187,10 +210,22 @@ export function restoreJournalState(): RestoreJournalResult {
|
|
|
187
210
|
if (profileUnchanged) {
|
|
188
211
|
if (journal.originalProfile !== null) {
|
|
189
212
|
atomicWriteFile(CODEX_PROFILE_PATH, Buffer.from(journal.originalProfile, "base64").toString("utf-8"));
|
|
213
|
+
profileRestored = true;
|
|
190
214
|
} else if (existsSync(CODEX_PROFILE_PATH)) {
|
|
191
|
-
|
|
215
|
+
// "There was no profile before, so remove the one we generated." Claiming success
|
|
216
|
+
// without checking is how a caller ends up deleting the journal, reporting a clean
|
|
217
|
+
// restore, and leaving our profile on disk with nothing left that records it should
|
|
218
|
+
// not be there. ENOENT is the one benign outcome: the file is already gone, which is
|
|
219
|
+
// the state we wanted.
|
|
220
|
+
try {
|
|
221
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
222
|
+
profileRestored = true;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
profileRestored = (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
profileRestored = true;
|
|
192
228
|
}
|
|
193
|
-
profileRestored = true;
|
|
194
229
|
}
|
|
195
230
|
const complete = configRestored && profileRestored;
|
|
196
231
|
if (complete) removeJournal();
|
|
@@ -207,11 +242,24 @@ export function restoreJournal(): boolean {
|
|
|
207
242
|
return restoreJournalState().complete;
|
|
208
243
|
}
|
|
209
244
|
|
|
210
|
-
export
|
|
245
|
+
export interface ReconcileJournalOptions {
|
|
246
|
+
activeClientApiKeyId?: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean {
|
|
211
250
|
const journal = readJournal();
|
|
212
251
|
if (!journal) return false;
|
|
252
|
+
const owner = journalOwner();
|
|
253
|
+
if (owner?.kind === "client") {
|
|
254
|
+
if (options.activeClientApiKeyId === owner.apiKeyId) return false;
|
|
255
|
+
const restored = restoreJournalState();
|
|
256
|
+
if (!restored.configRestored && !restored.profileRestored) return false;
|
|
257
|
+
console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const pid = owner?.kind === "process" ? owner.pid : journal.pid;
|
|
213
261
|
try {
|
|
214
|
-
process.kill(
|
|
262
|
+
process.kill(pid, 0);
|
|
215
263
|
return false;
|
|
216
264
|
} catch (e: unknown) {
|
|
217
265
|
if ((e as NodeJS.ErrnoException).code === "EPERM") {
|
|
@@ -220,6 +268,6 @@ export function reconcileJournal(): boolean {
|
|
|
220
268
|
}
|
|
221
269
|
const restored = restoreJournalState();
|
|
222
270
|
if (!restored.configRestored && !restored.profileRestored) return false;
|
|
223
|
-
console.error(`⚠️ Previous session (PID ${
|
|
271
|
+
console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
|
|
224
272
|
return true;
|
|
225
273
|
}
|