@yansigit/opencodex 2.36.1 → 2.36.2-dev.20260829.52
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 +9 -4
- package/gui/dist/assets/{ApiKeys-CsmNyf4I.js → ApiKeys-GpvAosG-.js} +1 -1
- package/gui/dist/assets/{Claude-B4sHsGTD.js → Claude-Biju6rfh.js} +1 -1
- package/gui/dist/assets/{CodexSet-BuAQ4YQn.js → CodexSet-_EkE9hNC.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-BC65sEaS.js → FileIntegrationPage-BL55KJCW.js} +1 -1
- package/gui/dist/assets/{Grok-DNJWlISf.js → Grok-Dp3sOS5B.js} +1 -1
- package/gui/dist/assets/{Integrations-vGJshFZK.js → Integrations-Bx0V4Dx1.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-DYMkDFHE.js → IntegrationsOverview-CT41lCh4.js} +1 -1
- package/gui/dist/assets/{Logs-Coc7_RPP.js → Logs-BxS_u7Hr.js} +1 -1
- package/gui/dist/assets/{Models-CKVT8Meb.js → Models-DvaBTkUM.js} +1 -1
- package/gui/dist/assets/{NumberStepper-Dug4wR8n.js → NumberStepper-97eq4OKR.js} +1 -1
- package/gui/dist/assets/Providers-CB9I0Vv1.js +1 -0
- package/gui/dist/assets/{RestoreDialog-BpuvVU2w.js → RestoreDialog-C9sltYZD.js} +1 -1
- package/gui/dist/assets/{Startup-DAUQqNEN.js → Startup-BvFv8yC2.js} +1 -1
- package/gui/dist/assets/{Storage-DfP95SlJ.js → Storage-CKa2mqSI.js} +1 -1
- package/gui/dist/assets/{Subagents-CMNsZLfk.js → Subagents-B4oMNOVh.js} +1 -1
- package/gui/dist/assets/{Usage-ESFRfBMO.js → Usage-gaSKtbMw.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-W4FhikN7.js → codex-stale-banner-B5VQPO3t.js} +1 -1
- package/gui/dist/assets/{data-surface-B8PX8rjm.js → data-surface-B0APtUxm.js} +1 -1
- package/gui/dist/assets/{data-surface-CETFXqiA.js → data-surface-CH5yKPY1.js} +1 -1
- package/gui/dist/assets/index-C8flH881.css +1 -0
- package/gui/dist/assets/{index-DHQHBRhb.js → index-DWxg-UlU.js} +3 -3
- package/gui/dist/assets/{model-display-OBTJhQwT.js → model-display-vDl3mo07.js} +1 -1
- package/gui/dist/assets/{provider-payload-CJRJUn7w.js → provider-payload-C3Xp5N0y.js} +1 -1
- package/gui/dist/assets/{section-tabs-B8sLMiz6.js → section-tabs-Bpm5lFcZ.js} +1 -1
- package/gui/dist/assets/shared-BHpPx-nZ.js +69 -0
- package/gui/dist/index.html +3 -3
- package/package.json +1 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/google-http.ts +34 -14
- package/src/adapters/google.ts +4 -1
- package/src/cli/index.ts +9 -1
- package/src/codex/inject.ts +14 -9
- package/src/config/process-state.ts +15 -0
- package/src/config.ts +17 -0
- package/src/generated/compatibility-version.json +61 -41
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/pinned-http.ts +7 -1
- package/src/lib/process-control.ts +3 -2
- package/src/lib/provider-outbound.ts +77 -27
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +97 -11
- package/src/lib/server-tls.ts +47 -0
- package/src/lib/test-provider-fetch.ts +6 -0
- package/src/lib/test-server-start.ts +5 -0
- package/src/lib/upstream-retry.ts +39 -25
- package/src/oauth/anthropic.ts +4 -3
- package/src/oauth/chatgpt.ts +7 -3
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/cursor.ts +3 -2
- package/src/oauth/github-copilot.ts +7 -5
- package/src/oauth/google-antigravity.ts +5 -4
- package/src/oauth/key-providers.ts +11 -13
- package/src/oauth/kimi.ts +7 -6
- package/src/oauth/kiro.ts +3 -2
- package/src/oauth/nous.ts +5 -5
- package/src/oauth/transport.ts +77 -0
- package/src/oauth/xai.ts +5 -4
- package/src/providers/replit/probe.ts +8 -3
- package/src/providers/xai-transport.ts +16 -2
- package/src/server/auth-cors.ts +15 -1
- package/src/server/chat-native.ts +9 -3
- package/src/server/index.ts +63 -23
- package/src/server/management/config-routes.ts +117 -15
- package/src/server/management/context.ts +4 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/responses/compact.ts +5 -2
- package/src/server/responses/core.ts +16 -19
- package/src/server/responses/fetch-helpers.ts +15 -7
- package/src/server/ws-bridge.ts +33 -4
- package/src/types/config.ts +9 -0
- package/src/types/provider.ts +2 -0
- package/src/types.ts +1 -0
- package/gui/dist/assets/Providers-DyaCHRWL.js +0 -1
- package/gui/dist/assets/index-pLho_LBF.css +0 -1
- package/gui/dist/assets/shared-DI18-uF7.js +0 -69
package/gui/dist/index.html
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="modulepreload" crossorigin href="/assets/shared-
|
|
21
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DWxg-UlU.js"></script>
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/shared-BHpPx-nZ.js">
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C8flH881.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
24
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yansigit/opencodex",
|
|
3
|
-
"version": "2.36.
|
|
3
|
+
"version": "2.36.2-dev.20260829.52",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
package/src/adapters/base.ts
CHANGED
|
@@ -143,6 +143,8 @@ export interface AdapterFetchContext {
|
|
|
143
143
|
stream?: boolean;
|
|
144
144
|
/** Custom fetch executor to use for physical upstream network requests (defaults to globalThis.fetch). */
|
|
145
145
|
executor?: typeof globalThis.fetch;
|
|
146
|
+
/** Shared transient replay allowance for one logical generation. */
|
|
147
|
+
replayBudget?: { remaining: number };
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -14,6 +14,7 @@ import { identifyRoutedModel } from "./identity";
|
|
|
14
14
|
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
15
15
|
import { parseDataUrl } from "./image";
|
|
16
16
|
import { redactSecretString } from "../lib/redact";
|
|
17
|
+
import { testProviderFetch } from "../lib/test-provider-fetch";
|
|
17
18
|
|
|
18
19
|
// Retain the short ids emitted by the first local integration. New requests use the live catalog's
|
|
19
20
|
// provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
|
|
@@ -600,7 +601,7 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
|
|
|
600
601
|
}
|
|
601
602
|
|
|
602
603
|
export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
603
|
-
const executor = (provider
|
|
604
|
+
const executor = testProviderFetch(provider) ?? globalThis.fetch;
|
|
604
605
|
return {
|
|
605
606
|
name: "command-code",
|
|
606
607
|
formatErrorBody: formatCommandCodeErrorBody,
|
|
@@ -281,6 +281,8 @@ export interface GoogleRetryOptions {
|
|
|
281
281
|
repairInvalid400?: boolean;
|
|
282
282
|
/** Let the Responses layer own Antigravity's single same-account 429 replay. */
|
|
283
283
|
retry429?: boolean;
|
|
284
|
+
replayTransientFailures?: boolean;
|
|
285
|
+
replayBudget?: { remaining: number };
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
|
|
@@ -358,7 +360,13 @@ async function fetchGoogleWithRetryInternal(
|
|
|
358
360
|
let antigravityHostIndex = 0;
|
|
359
361
|
let lastError: unknown;
|
|
360
362
|
let compatibilityReplayUsed = false;
|
|
361
|
-
|
|
363
|
+
const maxAttempts = opts.replayTransientFailures ? GOOGLE_RETRY_ATTEMPTS : 1;
|
|
364
|
+
const consumeReplay = (): boolean => {
|
|
365
|
+
if (!opts.replayTransientFailures || (opts.replayBudget && opts.replayBudget.remaining <= 0)) return false;
|
|
366
|
+
if (opts.replayBudget) opts.replayBudget.remaining -= 1;
|
|
367
|
+
return true;
|
|
368
|
+
};
|
|
369
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
362
370
|
if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
|
|
363
371
|
try {
|
|
364
372
|
const res = await fetchWithAttemptDeadline(activeRequest.url, {
|
|
@@ -374,7 +382,7 @@ async function fetchGoogleWithRetryInternal(
|
|
|
374
382
|
}
|
|
375
383
|
if (antigravityHosts.length > 1 && antigravityHostIndex === 0) {
|
|
376
384
|
const shouldTryPeer = res.status === 404 || isUnavailableResponse(res);
|
|
377
|
-
if (shouldTryPeer) {
|
|
385
|
+
if (shouldTryPeer && consumeReplay()) {
|
|
378
386
|
cancelResponseBodyBestEffort(res);
|
|
379
387
|
antigravityHostIndex = 1;
|
|
380
388
|
activeRequest = requestForHost(activeRequest, antigravityHosts[antigravityHostIndex]!);
|
|
@@ -382,14 +390,16 @@ async function fetchGoogleWithRetryInternal(
|
|
|
382
390
|
}
|
|
383
391
|
}
|
|
384
392
|
if (label === "Antigravity" && isAntigravitySseRequest(activeRequest) && res.ok) {
|
|
385
|
-
const fetchPeer = antigravityHosts.length > 1 && antigravityHostIndex === 0
|
|
386
|
-
? () =>
|
|
393
|
+
const fetchPeer = antigravityHosts.length > 1 && antigravityHostIndex === 0 && opts.replayTransientFailures
|
|
394
|
+
? () => consumeReplay()
|
|
395
|
+
? fetchGoogleWithRetryInternal(
|
|
387
396
|
label,
|
|
388
397
|
requestForHost(activeRequest, antigravityHosts[1]!),
|
|
389
398
|
ctx,
|
|
390
399
|
false,
|
|
391
400
|
opts,
|
|
392
|
-
|
|
401
|
+
)
|
|
402
|
+
: Promise.reject(new Error("Antigravity transient replay budget exhausted"))
|
|
393
403
|
: undefined;
|
|
394
404
|
return prepareCcaSseResponse(res, fetchPeer, ctx.accountId);
|
|
395
405
|
}
|
|
@@ -422,7 +432,8 @@ async function fetchGoogleWithRetryInternal(
|
|
|
422
432
|
if (res.status === 429 && !retry429) {
|
|
423
433
|
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
424
434
|
}
|
|
425
|
-
if (!retryableGoogleStatus(res.status) || attempt ===
|
|
435
|
+
if (!retryableGoogleStatus(res.status) || attempt === maxAttempts - 1
|
|
436
|
+
|| (opts.replayBudget && opts.replayBudget.remaining <= 0)) {
|
|
426
437
|
return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
427
438
|
}
|
|
428
439
|
// A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
|
|
@@ -443,19 +454,21 @@ async function fetchGoogleWithRetryInternal(
|
|
|
443
454
|
maxDelayMs: GOOGLE_RETRY_MAX_MS,
|
|
444
455
|
headers: res.headers,
|
|
445
456
|
}), ctx.abortSignal);
|
|
457
|
+
if (!consumeReplay()) return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
|
|
446
458
|
} catch (err) {
|
|
447
459
|
if (ctx.abortSignal?.aborted) throw err;
|
|
448
460
|
lastError = err;
|
|
449
|
-
if (antigravityHosts.length > 1 && antigravityHostIndex === 0) {
|
|
461
|
+
if (antigravityHosts.length > 1 && antigravityHostIndex === 0 && consumeReplay()) {
|
|
450
462
|
antigravityHostIndex = 1;
|
|
451
463
|
activeRequest = requestForHost(activeRequest, antigravityHosts[antigravityHostIndex]!);
|
|
452
464
|
continue;
|
|
453
465
|
}
|
|
454
|
-
if (attempt ===
|
|
466
|
+
if (attempt === maxAttempts - 1 || (opts.replayBudget && opts.replayBudget.remaining <= 0)) throw err;
|
|
455
467
|
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
456
468
|
baseDelayMs: GOOGLE_RETRY_BASE_MS,
|
|
457
469
|
maxDelayMs: GOOGLE_RETRY_MAX_MS,
|
|
458
470
|
}), ctx.abortSignal);
|
|
471
|
+
if (!consumeReplay()) throw err;
|
|
459
472
|
}
|
|
460
473
|
}
|
|
461
474
|
throw lastError ?? new Error(`${label} fetch failed`);
|
|
@@ -480,16 +493,23 @@ export function fetchGoogleWithRetry(
|
|
|
480
493
|
* demand" spikes, plus plain rate-limit 429s, both of which previously failed immediately
|
|
481
494
|
* because the default server fetch path only retries connection resets.
|
|
482
495
|
*/
|
|
483
|
-
export function fetchDirectGeminiWithRetry(
|
|
484
|
-
|
|
496
|
+
export function fetchDirectGeminiWithRetry(
|
|
497
|
+
request: AdapterRequest,
|
|
498
|
+
ctx: AdapterFetchContext = {},
|
|
499
|
+
opts: GoogleRetryOptions = {},
|
|
500
|
+
): Promise<Response> {
|
|
501
|
+
return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, {
|
|
502
|
+
...opts,
|
|
503
|
+
repairInvalid400: false,
|
|
504
|
+
});
|
|
485
505
|
}
|
|
486
506
|
|
|
487
507
|
/** Vertex AI retry wrapper. */
|
|
488
|
-
export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
489
|
-
return fetchGoogleWithRetry("Vertex AI", request, ctx);
|
|
508
|
+
export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}, opts: GoogleRetryOptions = {}): Promise<Response> {
|
|
509
|
+
return fetchGoogleWithRetry("Vertex AI", request, ctx, opts);
|
|
490
510
|
}
|
|
491
511
|
|
|
492
512
|
/** Antigravity (Cloud Code Assist) retry wrapper. */
|
|
493
|
-
export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
|
|
494
|
-
return fetchGoogleWithRetry("Antigravity", request, ctx, { retry429: false });
|
|
513
|
+
export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}, opts: GoogleRetryOptions = {}): Promise<Response> {
|
|
514
|
+
return fetchGoogleWithRetry("Antigravity", request, ctx, { ...opts, retry429: false });
|
|
495
515
|
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -860,7 +860,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
860
860
|
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
|
|
861
861
|
? {
|
|
862
862
|
fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
|
|
863
|
-
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx
|
|
863
|
+
(provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx, {
|
|
864
|
+
replayTransientFailures: provider.replayTransientFailures,
|
|
865
|
+
replayBudget: ctx?.replayBudget,
|
|
866
|
+
}),
|
|
864
867
|
formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
|
|
865
868
|
(provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
|
|
866
869
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./tray-proxy";
|
|
39
39
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
40
40
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
41
|
+
import { canonicalServerOrigin } from "../lib/server-tls";
|
|
41
42
|
import { dispatchCommand } from "./dispatch";
|
|
42
43
|
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
43
44
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
@@ -299,7 +300,14 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
299
300
|
writePid(process.pid);
|
|
300
301
|
|
|
301
302
|
const config = loadConfig();
|
|
302
|
-
|
|
303
|
+
const boundPort = server.port ?? port;
|
|
304
|
+
writeRuntimePort({
|
|
305
|
+
pid: process.pid,
|
|
306
|
+
port: boundPort,
|
|
307
|
+
hostname: config.hostname,
|
|
308
|
+
origin: canonicalServerOrigin(config, boundPort),
|
|
309
|
+
attestationSecret: localAttestationSecret,
|
|
310
|
+
});
|
|
303
311
|
// No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
|
|
304
312
|
// is about to transform; snapshotting earlier only captured a baseline that could
|
|
305
313
|
// already be stale by the time injection ran (#477).
|
package/src/codex/inject.ts
CHANGED
|
@@ -218,14 +218,16 @@ export function buildProviderTableBlock(
|
|
|
218
218
|
supportsWebsockets = false,
|
|
219
219
|
includeApiAuthHeader = false,
|
|
220
220
|
hostname?: string,
|
|
221
|
+
publicOrigin?: string,
|
|
221
222
|
): string {
|
|
222
223
|
const host = providerBaseHost(hostname);
|
|
224
|
+
const baseUrl = publicOrigin ?? `http://${host}:${port}`;
|
|
223
225
|
const lines = [
|
|
224
226
|
"",
|
|
225
227
|
OCX_SECTION_MARKER,
|
|
226
228
|
"[model_providers.opencodex]",
|
|
227
229
|
'name = "OpenCodex Proxy"',
|
|
228
|
-
`base_url = "
|
|
230
|
+
`base_url = "${baseUrl}/v1"`,
|
|
229
231
|
'wire_api = "responses"',
|
|
230
232
|
"requires_openai_auth = true",
|
|
231
233
|
];
|
|
@@ -245,8 +247,9 @@ export function buildProviderTableBlock(
|
|
|
245
247
|
export function buildOpenaiBaseUrlLine(
|
|
246
248
|
port: number,
|
|
247
249
|
hostname?: string,
|
|
250
|
+
publicOrigin?: string,
|
|
248
251
|
): string {
|
|
249
|
-
return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
|
|
252
|
+
return `openai_base_url = "${publicOrigin ?? `http://${providerBaseHost(hostname)}:${port}`}/v1"`;
|
|
250
253
|
}
|
|
251
254
|
|
|
252
255
|
/**
|
|
@@ -259,11 +262,12 @@ export function setRootOpenaiBaseUrl(
|
|
|
259
262
|
content: string,
|
|
260
263
|
port: number,
|
|
261
264
|
hostname?: string,
|
|
265
|
+
publicOrigin?: string,
|
|
262
266
|
): { content: string; keptUserBaseUrl: boolean } {
|
|
263
267
|
const lines = content.split("\n");
|
|
264
268
|
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
265
269
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
266
|
-
const key = buildOpenaiBaseUrlLine(port, hostname);
|
|
270
|
+
const key = buildOpenaiBaseUrlLine(port, hostname, publicOrigin);
|
|
267
271
|
|
|
268
272
|
for (let i = 0; i < rootEnd; i++) {
|
|
269
273
|
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
@@ -621,7 +625,7 @@ function stripOpencodexCatalogPath(content: string): string {
|
|
|
621
625
|
.join("\n");
|
|
622
626
|
}
|
|
623
627
|
|
|
624
|
-
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
|
|
628
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean, publicOrigin?: string): string {
|
|
625
629
|
const host = providerBaseHost(hostname);
|
|
626
630
|
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
627
631
|
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
@@ -631,7 +635,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
631
635
|
"# OpenCodex proxy fallback config (Design B)",
|
|
632
636
|
`# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
|
|
633
637
|
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
634
|
-
buildOpenaiBaseUrlLine(port, hostname),
|
|
638
|
+
buildOpenaiBaseUrlLine(port, hostname, publicOrigin),
|
|
635
639
|
];
|
|
636
640
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
637
641
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
@@ -644,7 +648,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
644
648
|
];
|
|
645
649
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
646
650
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
647
|
-
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
|
|
651
|
+
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname, publicOrigin).trimEnd(), "");
|
|
648
652
|
return lines.join("\n");
|
|
649
653
|
}
|
|
650
654
|
|
|
@@ -726,7 +730,7 @@ export async function injectCodexConfig(
|
|
|
726
730
|
message:
|
|
727
731
|
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
728
732
|
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
729
|
-
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
733
|
+
` Configure that provider for Responses passthrough at ${config?.tls?.publicOrigin ?? `http://${providerBaseHost(config?.hostname)}:${port}`}/v1` +
|
|
730
734
|
`${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
|
|
731
735
|
` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
|
|
732
736
|
});
|
|
@@ -811,12 +815,13 @@ export async function injectCodexConfig(
|
|
|
811
815
|
websocketsEnabled(config ?? {}),
|
|
812
816
|
true,
|
|
813
817
|
config?.hostname,
|
|
818
|
+
config?.tls?.publicOrigin,
|
|
814
819
|
);
|
|
815
820
|
} else {
|
|
816
821
|
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
817
822
|
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
818
823
|
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
819
|
-
const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
|
|
824
|
+
const result = setRootOpenaiBaseUrl(content, port, config?.hostname, config?.tls?.publicOrigin);
|
|
820
825
|
content = result.content;
|
|
821
826
|
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
822
827
|
}
|
|
@@ -852,7 +857,7 @@ export async function injectCodexConfig(
|
|
|
852
857
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
853
858
|
}
|
|
854
859
|
|
|
855
|
-
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
|
|
860
|
+
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode, config?.tls?.publicOrigin);
|
|
856
861
|
content = applyEol(content, eol);
|
|
857
862
|
|
|
858
863
|
/*
|
|
@@ -34,6 +34,8 @@ export type RuntimePortState = {
|
|
|
34
34
|
pid: number;
|
|
35
35
|
port: number;
|
|
36
36
|
hostname?: string;
|
|
37
|
+
/** Canonical listener origin. New clients prefer this; legacy records omit it. */
|
|
38
|
+
origin?: string;
|
|
37
39
|
/** Per-process proof key; protected by the config directory and never served. */
|
|
38
40
|
attestationSecret?: string;
|
|
39
41
|
};
|
|
@@ -42,6 +44,18 @@ function isValidRuntimePortState(value: unknown): value is RuntimePortState {
|
|
|
42
44
|
if (!value || typeof value !== "object") return false;
|
|
43
45
|
const state = value as Record<string, unknown>;
|
|
44
46
|
const hostnameOk = state.hostname === undefined || typeof state.hostname === "string";
|
|
47
|
+
const originOk = state.origin === undefined || (() => {
|
|
48
|
+
if (typeof state.origin !== "string") return false;
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(state.origin);
|
|
51
|
+
return (url.protocol === "http:" || url.protocol === "https:")
|
|
52
|
+
&& !url.username && !url.password
|
|
53
|
+
&& url.pathname === "/" && !url.search && !url.hash
|
|
54
|
+
&& url.origin === state.origin;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
45
59
|
const attestationOk = state.attestationSecret === undefined || isLocalAttestationSecret(state.attestationSecret);
|
|
46
60
|
return Number.isSafeInteger(state.pid)
|
|
47
61
|
&& Number(state.pid) > 0
|
|
@@ -49,6 +63,7 @@ function isValidRuntimePortState(value: unknown): value is RuntimePortState {
|
|
|
49
63
|
&& Number(state.port) > 0
|
|
50
64
|
&& Number(state.port) <= 65535
|
|
51
65
|
&& hostnameOk
|
|
66
|
+
&& originOk
|
|
52
67
|
&& attestationOk;
|
|
53
68
|
}
|
|
54
69
|
|
package/src/config.ts
CHANGED
|
@@ -150,6 +150,7 @@ export {
|
|
|
150
150
|
writeRuntimePort,
|
|
151
151
|
type RuntimePortState,
|
|
152
152
|
} from "./config/process-state";
|
|
153
|
+
import { serverTlsConfigError } from "./lib/server-tls";
|
|
153
154
|
import {
|
|
154
155
|
clearPendingConfigTopLevelDeletions,
|
|
155
156
|
configHasRebaseProvenance,
|
|
@@ -536,6 +537,7 @@ const providerConfigSchema = z.object({
|
|
|
536
537
|
.transform(normalizeNonBlankStringArray)
|
|
537
538
|
.optional(),
|
|
538
539
|
retryOn429: retryOn429PolicySchema.optional(),
|
|
540
|
+
replayTransientFailures: z.boolean().optional(),
|
|
539
541
|
codexAccountMode: z.enum(["pool", "direct"]).optional(),
|
|
540
542
|
// Validated rather than passed through: this schema ends in `.passthrough()`, so an
|
|
541
543
|
// undeclared key survives verbatim. A misspelled `codexToolMode` therefore used to be
|
|
@@ -904,6 +906,11 @@ const configSchema = z.object({
|
|
|
904
906
|
// is safe: startServer() already falls back to 127.0.0.1 for a missing hostname. Write-time
|
|
905
907
|
// rejection lives in validateConfigCandidate() so bad values still surface to the caller.
|
|
906
908
|
hostname: z.string().trim().min(1).optional().catch(undefined),
|
|
909
|
+
tls: z.object({
|
|
910
|
+
certFile: z.string().trim().min(1),
|
|
911
|
+
keyFile: z.string().trim().min(1),
|
|
912
|
+
publicOrigin: z.string().trim().min(1),
|
|
913
|
+
}).strict().optional().catch(undefined),
|
|
907
914
|
// Discriminated on `enabled` so a disabled entry cannot be forced to carry a port, and an
|
|
908
915
|
// enabled one cannot omit it (#1102). A malformed value degrades to undefined rather than
|
|
909
916
|
// failing the whole parse: this is an opt-in convenience surface, and a hand-edit typo here
|
|
@@ -1418,6 +1425,11 @@ function sanitizeRetryOn429ForLoad(parsed: unknown): void {
|
|
|
1418
1425
|
const safeProviderName = JSON.stringify(redactSecretString(name));
|
|
1419
1426
|
if (!provider || typeof provider !== "object" || Array.isArray(provider)) continue;
|
|
1420
1427
|
const p = provider as Record<string, unknown>;
|
|
1428
|
+
if (p.replayTransientFailures !== undefined && typeof p.replayTransientFailures !== "boolean") {
|
|
1429
|
+
const receivedType = typeof p.replayTransientFailures;
|
|
1430
|
+
delete p.replayTransientFailures;
|
|
1431
|
+
console.warn(`⚠️ config.json providers.${safeProviderName}.replayTransientFailures (${receivedType}) is invalid — ignoring the field`);
|
|
1432
|
+
}
|
|
1421
1433
|
const policy = p.retryOn429;
|
|
1422
1434
|
if (policy === undefined) continue;
|
|
1423
1435
|
if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
|
|
@@ -2385,6 +2397,11 @@ function loopbackListenerPortError(value: unknown): string | null {
|
|
|
2385
2397
|
|
|
2386
2398
|
export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
|
|
2387
2399
|
const boundaryError = blankHostnameError(value)
|
|
2400
|
+
?? (() => {
|
|
2401
|
+
const raw = rawConfigRecord(value);
|
|
2402
|
+
const error = raw ? serverTlsConfigError(raw.tls) : null;
|
|
2403
|
+
return error ? `schema_invalid: ${error}` : null;
|
|
2404
|
+
})()
|
|
2388
2405
|
?? claudeSubagentEffortError(value)
|
|
2389
2406
|
?? subagentRolesError(value)
|
|
2390
2407
|
?? appOwnedMemoryBudgetError(value)
|