@bitkyc08/opencodex 2.22.0 → 2.23.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/gui/dist/assets/{index-ClEcVlFO.js → index-rFWrIE11.js} +19 -19
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +39 -7
- package/src/adapters/cursor/tool-definitions.ts +48 -0
- package/src/adapters/google.ts +18 -12
- package/src/adapters/openai-chat.ts +106 -13
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +3 -0
- package/src/bridge.ts +16 -5
- package/src/chat/inbound.ts +5 -11
- package/src/claude/context-windows.ts +5 -1
- package/src/claude/desktop-3p.ts +11 -6
- package/src/claude/inbound.ts +39 -1
- package/src/claude/model-info.ts +28 -8
- package/src/cli/account-api.ts +5 -1
- package/src/cli/claude-desktop.ts +3 -0
- package/src/cli/config-command.ts +37 -14
- package/src/codex/app-server-restart-service.ts +1 -1
- package/src/codex/auth-api.ts +5 -0
- package/src/codex/auth-context.ts +43 -2
- package/src/codex/catalog/metadata.ts +55 -8
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +21 -7
- package/src/codex/catalog/provider-fetch.ts +35 -7
- package/src/codex/catalog/sync.ts +40 -18
- package/src/codex/catalog-refresh-status.ts +21 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +23 -2
- package/src/codex/desired-state.ts +1 -1
- package/src/codex/inject.ts +38 -7
- package/src/codex/injected-marker.ts +28 -0
- package/src/codex/journal.ts +40 -1
- package/src/codex/management-convergence.ts +55 -2
- package/src/codex/quota-rejection.ts +61 -1
- package/src/codex/quota.ts +60 -6
- package/src/codex/routing.ts +30 -3
- package/src/combos/failover.ts +20 -0
- package/src/config.ts +271 -4
- package/src/generated/compatibility-version.json +86 -74
- package/src/grok/sync.ts +3 -1
- package/src/lab/artifacts/sanitize.ts +1 -1
- package/src/lab/live/manifest.ts +1 -1
- package/src/lib/codex-restart-contract.ts +1 -1
- package/src/lib/config-ownership.ts +1 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/lab-activation.ts +1 -1
- package/src/lib/optional-shutdown-hooks.ts +1 -1
- package/src/providers/quota.ts +10 -4
- package/src/providers/registry.ts +2 -2
- package/src/responses/parser.ts +42 -7
- package/src/responses/provider-opaque-metadata.ts +1 -1
- package/src/responses/thought-signature-replay.ts +261 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/provider-slot.ts +1 -1
- package/src/routing/evaluator.ts +12 -2
- package/src/routing/health.ts +16 -5
- package/src/routing/history/schema.ts +1 -1
- package/src/routing/trace.ts +1 -1
- package/src/server/auth-cors.ts +56 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/provider-routes.ts +19 -0
- package/src/server/management/shared.ts +3 -3
- package/src/server/management-api.ts +13 -6
- package/src/server/passive-route-linker.ts +1 -1
- package/src/server/relay.ts +16 -0
- package/src/server/responses/compact.ts +10 -3
- package/src/server/responses/core.ts +160 -33
- package/src/server/responses/fetch-helpers.ts +34 -2
- package/src/server/responses/input-admission.ts +17 -9
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/system-env.ts +4 -2
- package/src/service.ts +14 -7
- package/src/types.ts +34 -0
package/src/server/auth-cors.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
retryOn429PolicyConfigError,
|
|
17
17
|
requestPacingConfigError,
|
|
18
18
|
sanitizeModelCostsForDisplay,
|
|
19
|
+
upstreamHttpVersionConfigError,
|
|
19
20
|
} from "../config";
|
|
20
21
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
21
22
|
import { redactSecretString } from "../lib/redact";
|
|
@@ -310,10 +311,20 @@ function secretEquals(actual: string, expected: string | undefined): boolean {
|
|
|
310
311
|
* point at, and a sentinel string in the id would collide with a hand-edited
|
|
311
312
|
* entry that happens to be named `loopback`.
|
|
312
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* HOW an admission credential was presented.
|
|
316
|
+
*
|
|
317
|
+
* The credential IDENTITY (which key matched) and its PRESENTATION (which header carried it)
|
|
318
|
+
* are different facts, and #1686 needs both: a proxy secret arriving as a bearer on the
|
|
319
|
+
* Responses transport is admissible, but only if the upstream credential is then guaranteed to
|
|
320
|
+
* be substituted. Collapsing the two is what made that flow unexpressible.
|
|
321
|
+
*/
|
|
322
|
+
export type DataPlaneAdmissionSource = "loopback" | "dedicated" | "bearer" | "x-api-key";
|
|
323
|
+
|
|
313
324
|
export type DataPlaneAdmission =
|
|
314
|
-
| { kind: "configured"; keyId: string }
|
|
315
|
-
| { kind: "environment" }
|
|
316
|
-
| { kind: "loopback" };
|
|
325
|
+
| { kind: "configured"; keyId: string; source: DataPlaneAdmissionSource }
|
|
326
|
+
| { kind: "environment"; source: DataPlaneAdmissionSource }
|
|
327
|
+
| { kind: "loopback"; source: "loopback" };
|
|
317
328
|
|
|
318
329
|
/**
|
|
319
330
|
* Which admission secret `token` is, or null when it is none of them.
|
|
@@ -324,12 +335,16 @@ export type DataPlaneAdmission =
|
|
|
324
335
|
* discarded, which is what makes per-key attribution possible without touching
|
|
325
336
|
* the admission decision itself.
|
|
326
337
|
*/
|
|
327
|
-
export function resolveDataPlaneAdmissionSecret(
|
|
338
|
+
export function resolveDataPlaneAdmissionSecret(
|
|
339
|
+
token: string,
|
|
340
|
+
config: Pick<OcxConfig, "apiKeys">,
|
|
341
|
+
source: DataPlaneAdmissionSource = "dedicated",
|
|
342
|
+
): DataPlaneAdmission | null {
|
|
328
343
|
const actual = token.trim();
|
|
329
344
|
if (!actual) return null;
|
|
330
|
-
if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment" };
|
|
345
|
+
if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment", source };
|
|
331
346
|
for (const k of config.apiKeys ?? []) {
|
|
332
|
-
if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id };
|
|
347
|
+
if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id, source };
|
|
333
348
|
}
|
|
334
349
|
return null;
|
|
335
350
|
}
|
|
@@ -376,8 +391,12 @@ export interface ApiAuthMatrixRow {
|
|
|
376
391
|
* against every cell rather than reading the table back to itself.
|
|
377
392
|
*/
|
|
378
393
|
export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
379
|
-
|
|
380
|
-
|
|
394
|
+
// #1686: a bearer that is one of OUR admission secrets is now accepted here. It is safe
|
|
395
|
+
// because materializeCodexUpstreamAuth substitutes the stored main credential rather than
|
|
396
|
+
// forwarding it; a bearer that is NOT our secret stays unadmitted and remains Codex Direct
|
|
397
|
+
// passthrough, so the two bearer domains still never mix. `x-api-key` is still rejected.
|
|
398
|
+
{ endpoint: "/v1/responses", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
399
|
+
{ endpoint: "/v1/chat/completions", bearer: "accepted", dedicated: "accepted", xApiKey: "rejected" },
|
|
381
400
|
{ endpoint: "/v1/messages", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
382
401
|
{ endpoint: "/v1/models", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
383
402
|
];
|
|
@@ -414,13 +433,15 @@ export function validateForwardAdmissionCredential(headers: Headers, config: Ocx
|
|
|
414
433
|
*/
|
|
415
434
|
export function resolveApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
416
435
|
// A loopback bind never reads a token at all, so there is no key to name.
|
|
417
|
-
if (!isApiAuthRequired(config)) return { kind: "loopback" };
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
436
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
437
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
438
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
439
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
440
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
441
|
+
// Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
|
|
442
|
+
const apiKey = req.headers.get("x-api-key")?.trim();
|
|
443
|
+
if (apiKey) return resolveDataPlaneAdmissionSecret(apiKey, config, "x-api-key");
|
|
444
|
+
return null;
|
|
424
445
|
}
|
|
425
446
|
|
|
426
447
|
export function hasValidApiAuth(req: Request, config: RequestPolicyView): boolean {
|
|
@@ -438,14 +459,23 @@ export function requireApiAuth(req: Request, config: RequestPolicyView, _kind: "
|
|
|
438
459
|
* domains can never be confused.
|
|
439
460
|
*/
|
|
440
461
|
export function resolveResponsesApiAuth(req: Request, config: RequestPolicyView): DataPlaneAdmission | null {
|
|
441
|
-
if (!isApiAuthRequired(config)) return { kind: "loopback" };
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
462
|
+
if (!isApiAuthRequired(config)) return { kind: "loopback", source: "loopback" };
|
|
463
|
+
// The dedicated header still WINS, because it is unambiguous.
|
|
464
|
+
const dedicated = req.headers.get("x-opencodex-api-key")?.trim();
|
|
465
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
466
|
+
// #1686: a bearer may also be one of OUR admission secrets. Rejecting it outright meant a
|
|
467
|
+
// Codex client configured with `env_key` could not reach Direct at all. Admitting it is only
|
|
468
|
+
// safe because the upstream credential is then SUBSTITUTED rather than forwarded -- see
|
|
469
|
+
// materializeCodexUpstreamAuth. A bearer that is NOT our secret stays unadmitted here and
|
|
470
|
+
// remains Codex Direct passthrough, so the two bearer domains still never mix.
|
|
471
|
+
const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
472
|
+
if (bearer) return resolveDataPlaneAdmissionSecret(bearer, config, "bearer");
|
|
473
|
+
// `x-api-key` is deliberately NOT accepted on this transport.
|
|
474
|
+
return null;
|
|
447
475
|
}
|
|
448
476
|
|
|
477
|
+
|
|
478
|
+
|
|
449
479
|
export function requireResponsesApiAuth(req: Request, config: RequestPolicyView): Response | null {
|
|
450
480
|
if (resolveResponsesApiAuth(req, config)) return null;
|
|
451
481
|
return formatErrorResponse(401, "authentication_error", "opencodex API key required");
|
|
@@ -520,6 +550,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
520
550
|
if (requestPacingError) {
|
|
521
551
|
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
522
552
|
}
|
|
553
|
+
const upstreamHttpVersionError = upstreamHttpVersionConfigError(raw.upstreamHttpVersion);
|
|
554
|
+
if (upstreamHttpVersionError) {
|
|
555
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
|
|
556
|
+
}
|
|
523
557
|
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
524
558
|
if (modelCostsError) {
|
|
525
559
|
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
@@ -640,6 +674,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
640
674
|
"noTopPModels",
|
|
641
675
|
"noPenaltyModels",
|
|
642
676
|
"noStructuredOutputModels",
|
|
677
|
+
"upstreamHttpVersion",
|
|
643
678
|
"autoToolChoiceOnlyModels",
|
|
644
679
|
"preserveReasoningContentModels",
|
|
645
680
|
"requiresReasoningPlaceholderModels",
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { responseWithDeferredRequestLog } from "./relay";
|
|
38
38
|
import { handleResponses } from "./responses";
|
|
39
39
|
import type { AdmissionLease } from "../lib/admission";
|
|
40
|
+
import type { DataPlaneAdmission } from "./auth-cors";
|
|
40
41
|
import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
|
|
41
42
|
import {
|
|
42
43
|
createTranslatorBudget,
|
|
@@ -64,7 +65,7 @@ export async function handleChatCompletions(
|
|
|
64
65
|
req: Request,
|
|
65
66
|
config: OcxConfig,
|
|
66
67
|
logCtx: RequestLogContext,
|
|
67
|
-
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
68
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
68
69
|
): Promise<Response> {
|
|
69
70
|
const translatorBudget = createTranslatorBudget();
|
|
70
71
|
try {
|
|
@@ -83,7 +84,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
83
84
|
config: OcxConfig,
|
|
84
85
|
logCtx: RequestLogContext,
|
|
85
86
|
translatorBudget: TranslatorBudget,
|
|
86
|
-
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
87
|
+
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
87
88
|
): Promise<Response> {
|
|
88
89
|
let chatBody: Rec;
|
|
89
90
|
try {
|
|
@@ -251,6 +252,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
251
252
|
};
|
|
252
253
|
const upstream = await handleResponses(internalReq, config, logCtx, {
|
|
253
254
|
...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
|
|
255
|
+
// #1686: the Chat surface translates its body and replays here, so the admission fact has
|
|
256
|
+
// to ride along or a bearer-admitted Chat caller would still be refused by Direct.
|
|
257
|
+
...(logIds?.admission ? { admission: logIds.admission } : {}),
|
|
254
258
|
abortSignal: req.signal,
|
|
255
259
|
// Body is Responses-shaped by now, but the client spoke Chat Completions.
|
|
256
260
|
inboundWire: "chat",
|
package/src/server/index.ts
CHANGED
|
@@ -958,7 +958,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
958
958
|
: idsParam === "desktop"
|
|
959
959
|
? "desktop3p" as const
|
|
960
960
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
961
|
-
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
961
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
962
962
|
return jsonResponse({ data }, 200, req, policy);
|
|
963
963
|
}
|
|
964
964
|
if (url.searchParams.has("client_version")) {
|
|
@@ -1092,7 +1092,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1092
1092
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1093
1093
|
let response: Response;
|
|
1094
1094
|
try {
|
|
1095
|
-
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease);
|
|
1095
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
|
|
1096
1096
|
} catch {
|
|
1097
1097
|
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
1098
1098
|
}
|
|
@@ -1214,6 +1214,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1214
1214
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1215
1215
|
const response = await handleResponses(req, config, logCtx, {
|
|
1216
1216
|
turnAdmissionLease,
|
|
1217
|
+
admission,
|
|
1217
1218
|
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1218
1219
|
abortSignal: req.signal,
|
|
1219
1220
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
@@ -1302,7 +1303,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1302
1303
|
inboundProtocol: "chat",
|
|
1303
1304
|
};
|
|
1304
1305
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1305
|
-
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
1306
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
1306
1307
|
req,
|
|
1307
1308
|
config,
|
|
1308
1309
|
));
|
|
@@ -1569,6 +1570,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1569
1570
|
try {
|
|
1570
1571
|
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1571
1572
|
const response = await handleResponses(req, config, logCtx, {
|
|
1573
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
1572
1574
|
forceEmptyResponseId: true,
|
|
1573
1575
|
inboundTransport: "websocket",
|
|
1574
1576
|
abortSignal: turnAbort.signal,
|
|
@@ -31,7 +31,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
31
31
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
32
32
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
33
33
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
34
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
34
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
35
35
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
36
36
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
37
37
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
@@ -207,6 +207,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
207
207
|
current.apiKeys?.[0]?.key,
|
|
208
208
|
"static",
|
|
209
209
|
current.claudeCode.desktopProfile,
|
|
210
|
+
providerContextCap(current, OPENAI_CODEX_PROVIDER_ID),
|
|
210
211
|
);
|
|
211
212
|
if (result.written && result.fingerprint) {
|
|
212
213
|
current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
|
|
@@ -885,6 +886,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
885
886
|
latest.apiKeys?.[0]?.key,
|
|
886
887
|
mode,
|
|
887
888
|
state.profile,
|
|
889
|
+
providerContextCap(latest, OPENAI_CODEX_PROVIDER_ID),
|
|
888
890
|
);
|
|
889
891
|
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
890
892
|
// Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
|
|
@@ -976,7 +978,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
976
978
|
if (isDisabled(m.provider, m.id)) continue;
|
|
977
979
|
aliases.push({ id: claudeCodeAlias(m.provider, m.id), display_name: `${m.id} (${m.provider})` });
|
|
978
980
|
}
|
|
979
|
-
const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models);
|
|
981
|
+
const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
980
982
|
const webSearchOverride = config.claudeCode?.webSearchSidecar;
|
|
981
983
|
const visionOverride = config.claudeCode?.visionSidecar;
|
|
982
984
|
// Auto is a RESOLUTION, recomputed per request — never stored state. Detection is
|
|
@@ -1005,6 +1007,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1005
1007
|
smallFastModel: config.claudeCode?.smallFastModel ?? "",
|
|
1006
1008
|
tierModels: config.claudeCode?.tierModels ?? {},
|
|
1007
1009
|
modelMap: config.claudeCode?.modelMap ?? {},
|
|
1010
|
+
classifierModel: config.claudeCode?.classifierModel ?? "",
|
|
1011
|
+
classifierFallbacks: config.claudeCode?.classifierFallbacks ?? [],
|
|
1008
1012
|
systemEnv: config.claudeCode?.systemEnv === true,
|
|
1009
1013
|
autoConnectSupported: process.platform === "darwin",
|
|
1010
1014
|
maxContextTokens: config.claudeCode?.maxContextTokens ?? null,
|
|
@@ -1042,7 +1046,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1042
1046
|
return prototype === Object.prototype || prototype === null;
|
|
1043
1047
|
};
|
|
1044
1048
|
if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
1045
|
-
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1049
|
+
const body = parsedBody as { enabled?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
|
|
1046
1050
|
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1047
1051
|
const section = body[field];
|
|
1048
1052
|
if (section === undefined || section === null) continue;
|
|
@@ -1182,13 +1186,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1182
1186
|
}
|
|
1183
1187
|
nextFastMode = body.fastMode === null ? undefined : body.fastMode;
|
|
1184
1188
|
}
|
|
1185
|
-
for (const field of ["model", "smallFastModel"] as const) {
|
|
1189
|
+
for (const field of ["model", "smallFastModel", "classifierModel"] as const) {
|
|
1186
1190
|
const value = body[field];
|
|
1187
1191
|
if (value === undefined) continue;
|
|
1188
1192
|
if (typeof value !== "string") return jsonResponse({ error: `${field} must be a string` }, 400);
|
|
1189
1193
|
if (value.trim() === "") delete next[field];
|
|
1190
1194
|
else next[field] = value.trim();
|
|
1191
1195
|
}
|
|
1196
|
+
if (body.classifierFallbacks !== undefined) {
|
|
1197
|
+
if (body.classifierFallbacks === null) {
|
|
1198
|
+
delete next.classifierFallbacks;
|
|
1199
|
+
} else {
|
|
1200
|
+
if (!Array.isArray(body.classifierFallbacks)) {
|
|
1201
|
+
return jsonResponse({ error: "classifierFallbacks must be an array of strings, or null" }, 400);
|
|
1202
|
+
}
|
|
1203
|
+
const list: string[] = [];
|
|
1204
|
+
for (const entry of body.classifierFallbacks) {
|
|
1205
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
1206
|
+
return jsonResponse({ error: "classifierFallbacks entries must be non-empty strings" }, 400);
|
|
1207
|
+
}
|
|
1208
|
+
list.push(entry.trim());
|
|
1209
|
+
}
|
|
1210
|
+
if (list.length > 0) next.classifierFallbacks = list;
|
|
1211
|
+
else delete next.classifierFallbacks;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1192
1214
|
if (body.modelMap !== undefined) {
|
|
1193
1215
|
if (body.modelMap === null) {
|
|
1194
1216
|
delete next.modelMap;
|
|
@@ -62,7 +62,7 @@ export interface ManagementApiDeps {
|
|
|
62
62
|
* leaves this unset, so the route creates its normal NativeProfileManager.
|
|
63
63
|
*/
|
|
64
64
|
/**
|
|
65
|
-
* Codex app-server restart seam (devlog/
|
|
65
|
+
* Codex app-server restart seam (devlog/_fin/260815_gui_codex_restart).
|
|
66
66
|
* Grouped rather than three separate fields: the route is an adapter over one
|
|
67
67
|
* service, and a route test that could not stub it would really terminate the
|
|
68
68
|
* developer's own Codex app-servers.
|
|
@@ -62,6 +62,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
62
62
|
metadataSlug: slug,
|
|
63
63
|
disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
|
|
64
64
|
contextWindow: undefined,
|
|
65
|
+
maxInputTokens: undefined,
|
|
65
66
|
})))
|
|
66
67
|
: [];
|
|
67
68
|
const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
|
|
@@ -77,6 +78,10 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
|
|
|
77
78
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
78
79
|
inputModalities: nativeInputModalities(row.slug),
|
|
79
80
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
81
|
+
// The input ceiling is a separate number from the window for GPT-5.6 (922k under
|
|
82
|
+
// 1.05M). Dropping it here made /api/models describe a native row as if the whole
|
|
83
|
+
// window were usable as input, which is the claim the measurement disproved.
|
|
84
|
+
...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
|
|
80
85
|
};
|
|
81
86
|
});
|
|
82
87
|
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
21
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
22
|
+
import { providerContextCap } from "../../providers/context-cap";
|
|
23
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
22
24
|
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
23
25
|
import { injectGrokConfig, stripGrokConfig, type GrokInjectModel } from "../../grok/inject";
|
|
24
26
|
import { inspectGrokConfig } from "../../grok/inspect";
|
|
@@ -506,7 +508,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
|
|
|
506
508
|
// Native slugs carry their context window: without it Grok falls back
|
|
507
509
|
// to its own 200k default and understates a 372k model.
|
|
508
510
|
...visibleNativeSlugs(config).map(id => {
|
|
509
|
-
const contextWindow = nativeOpenAiContextWindow(id);
|
|
511
|
+
const contextWindow = nativeOpenAiContextWindow(id, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
510
512
|
return { id, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
511
513
|
}),
|
|
512
514
|
...routed.map(m => ({
|
|
@@ -661,6 +663,7 @@ async function handleClaudeDesktopToggle(ctx: ManagementContext): Promise<Respon
|
|
|
661
663
|
latest.apiKeys?.[0]?.key,
|
|
662
664
|
"static",
|
|
663
665
|
latest.claudeCode?.desktopProfile,
|
|
666
|
+
providerContextCap(latest, OPENAI_CODEX_PROVIDER_ID),
|
|
664
667
|
);
|
|
665
668
|
if (!result.written) return postCommitRefusal(500, "claude-desktop", "write_failed", "Claude Desktop apply failed.", { desiredEnabled: latestDesiredEnabled });
|
|
666
669
|
return jsonResponse({
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
requestPacingConfigError,
|
|
18
18
|
readConfigAdmissionSnapshot,
|
|
19
19
|
saveConfigPreservingClaudeCode,
|
|
20
|
+
upstreamHttpVersionConfigError,
|
|
20
21
|
withConfigMutationLockSync,
|
|
21
22
|
} from "../../config";
|
|
22
23
|
import {
|
|
@@ -194,6 +195,19 @@ function applyProviderPatchFields(
|
|
|
194
195
|
}
|
|
195
196
|
touched = true;
|
|
196
197
|
}
|
|
198
|
+
if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
|
|
199
|
+
const value = rawBody.upstreamHttpVersion;
|
|
200
|
+
if (value === null || value === "") {
|
|
201
|
+
delete next.upstreamHttpVersion;
|
|
202
|
+
} else {
|
|
203
|
+
const versionError = upstreamHttpVersionConfigError(value);
|
|
204
|
+
if (versionError) return { error: versionError };
|
|
205
|
+
// `upstreamHttpVersionConfigError` is the shared write boundary; the assertion is
|
|
206
|
+
// explicit because the incoming value is an unknown JSON scalar.
|
|
207
|
+
next.upstreamHttpVersion = value as OcxProviderConfig["upstreamHttpVersion"];
|
|
208
|
+
}
|
|
209
|
+
touched = true;
|
|
210
|
+
}
|
|
197
211
|
// The Models page edits the catalog hints in place; keep them on the existing
|
|
198
212
|
// provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
|
|
199
213
|
if (Object.hasOwn(rawBody, "contextWindow")) {
|
|
@@ -374,6 +388,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
374
388
|
modelContextWindows: p.modelContextWindows,
|
|
375
389
|
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
376
390
|
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
391
|
+
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
377
392
|
authMode: p.authMode,
|
|
378
393
|
apiKeyTransport: p.apiKeyTransport,
|
|
379
394
|
disabled: p.disabled === true,
|
|
@@ -466,6 +481,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
466
481
|
const serviceTierError = providerServiceTierConfigError(name, body.provider);
|
|
467
482
|
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
468
483
|
const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
|
|
484
|
+
// PATCH already clears on null; POST persisted the body as submitted, so a `null` here
|
|
485
|
+
// reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
|
|
486
|
+
// "clear" means everywhere else.
|
|
487
|
+
if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
|
|
469
488
|
if (!name || !prov?.adapter || !prov?.baseUrl) {
|
|
470
489
|
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
|
|
471
490
|
}
|
|
@@ -28,7 +28,7 @@ import { deriveProviderPresets } from "../../providers/derive";
|
|
|
28
28
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
29
29
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
30
30
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
31
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
32
32
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
33
33
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
@@ -196,7 +196,7 @@ export async function fetchGrokCandidateModels(config: OcxConfig): Promise<GrokC
|
|
|
196
196
|
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
197
197
|
return [
|
|
198
198
|
...visibleNativeSlugs(config).map(id => {
|
|
199
|
-
const contextWindow = nativeOpenAiContextWindow(id);
|
|
199
|
+
const contextWindow = nativeOpenAiContextWindow(id, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
200
200
|
return { id, native: true, ...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
201
201
|
}),
|
|
202
202
|
...routed.map(m => ({
|
|
@@ -229,7 +229,7 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
229
229
|
// Native rows carry their real context window from the same accessor the Grok sync
|
|
230
230
|
// uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop.
|
|
231
231
|
...desktopVisibleNativeSlugs(config).map(id => {
|
|
232
|
-
const contextWindow = nativeOpenAiContextWindow(id);
|
|
232
|
+
const contextWindow = nativeOpenAiContextWindow(id, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
233
233
|
return { route: `native/${id}`, label: `${id} (native)`,
|
|
234
234
|
...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
235
235
|
}),
|
|
@@ -28,7 +28,7 @@ import { deriveProviderPresets } from "../providers/derive";
|
|
|
28
28
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
29
29
|
import { routedSlug, slugEquals } from "../providers/slug-codec";
|
|
30
30
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../providers/quota";
|
|
31
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
31
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
32
32
|
import { clearThreadAccountMap } from "../codex/routing";
|
|
33
33
|
import { primeCodexPoolQuotas } from "../codex/auth-api";
|
|
34
34
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../providers/context-cap";
|
|
@@ -109,7 +109,7 @@ function pathInManagementNamespace(pathname: string, prefix: string): boolean {
|
|
|
109
109
|
* keeps `management-api.ts` on the same footing as the three protected core files.
|
|
110
110
|
*
|
|
111
111
|
* Cherry-picked from @Wibias's PR #1676, which solved this before the boundary work
|
|
112
|
-
* reached it. See devlog/
|
|
112
|
+
* reached it. See devlog/_fin/260814_lab_core_decoupling/.
|
|
113
113
|
*/
|
|
114
114
|
async function handleRoutingProfileRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
115
115
|
if (!pathInManagementNamespace(ctx.url.pathname, "/api/routing-profiles")) return null;
|
|
@@ -174,13 +174,20 @@ export async function handleManagementAPI(
|
|
|
174
174
|
throw new TypeError("Catalog convergence returned an invalid outcome.");
|
|
175
175
|
}
|
|
176
176
|
return catalogRefresh;
|
|
177
|
-
} catch {
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// #1784: this used to manufacture `reason: "disk"` for every escaping error, so a
|
|
179
|
+
// programming fault and a full filesystem were indistinguishable and both reported
|
|
180
|
+
// non-retryable. Classify honestly and keep the cause allowlisted.
|
|
181
|
+
const invalidRequest = error instanceof TypeError
|
|
182
|
+
|| error instanceof RangeError
|
|
183
|
+
|| error instanceof SyntaxError;
|
|
178
184
|
return {
|
|
179
185
|
status: "failed",
|
|
180
|
-
reason: "
|
|
186
|
+
reason: invalidRequest ? "request-invalid" : "internal",
|
|
181
187
|
phase: convergenceInvoked ? "commit" : "gather",
|
|
182
188
|
retryable: false,
|
|
183
189
|
partialWrite: convergenceInvoked,
|
|
190
|
+
cause: { kind: invalidRequest ? "invalid-request" : "unknown" },
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
193
|
}
|
|
@@ -198,7 +205,7 @@ export async function handleManagementAPI(
|
|
|
198
205
|
import("../claude/context-windows"),
|
|
199
206
|
import("../codex/catalog"),
|
|
200
207
|
]);
|
|
201
|
-
injectClaudeAgentDefs(config, buildClaudeContextWindows([...visibleNativeSlugs(config)], models));
|
|
208
|
+
injectClaudeAgentDefs(config, buildClaudeContextWindows([...visibleNativeSlugs(config)], models, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)));
|
|
202
209
|
} catch {
|
|
203
210
|
// Keep routes available through a provider-discovery blip. A later
|
|
204
211
|
// launch-time sync restores any context markers missing from this pass.
|
|
@@ -302,4 +309,4 @@ export async function handleManagementAPI(
|
|
|
302
309
|
}
|
|
303
310
|
|
|
304
311
|
|
|
305
|
-
export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
|
|
312
|
+
export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* call site so the guarantee belongs to the mechanism instead of being restated by every
|
|
13
13
|
* caller.
|
|
14
14
|
*
|
|
15
|
-
* See devlog/
|
|
15
|
+
* See devlog/_fin/260814_lab_core_decoupling/020_request_path_gate.md
|
|
16
16
|
*/
|
|
17
17
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
18
|
import type { InboundWire } from "../providers/registry";
|
package/src/server/relay.ts
CHANGED
|
@@ -603,6 +603,13 @@ export type SseInspectorHandlers = {
|
|
|
603
603
|
onTerminal?: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void;
|
|
604
604
|
logCtx?: RequestLogContext;
|
|
605
605
|
onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
|
|
606
|
+
/**
|
|
607
|
+
* Every parsed SSE payload, delivered BEFORE any onCompletedResponse derived from that same
|
|
608
|
+
* payload. A caller that must decide on the whole turn -- not just its terminal snapshot --
|
|
609
|
+
* needs to see the incremental events, because a stream can announce an item and then close
|
|
610
|
+
* with an empty `output`.
|
|
611
|
+
*/
|
|
612
|
+
onParsedPayload?: (payload: unknown) => void;
|
|
606
613
|
onFirstOutput?: () => void;
|
|
607
614
|
/**
|
|
608
615
|
* Provider-scoped compatibility: persist the completed snapshot under the
|
|
@@ -786,6 +793,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
786
793
|
if (!reported && handlers.logCtx) {
|
|
787
794
|
inspectResponseLogSsePayloadParsed(handlers.logCtx, payload, parsed);
|
|
788
795
|
}
|
|
796
|
+
// Before any terminal handling: a consumer deciding on the whole turn must observe this
|
|
797
|
+
// payload even when the terminal snapshot that follows no longer mentions it.
|
|
798
|
+
if (handlers.onParsedPayload && parsed !== undefined) {
|
|
799
|
+
try { handlers.onParsedPayload(parsed); } catch { /* inspection must never throw into the pump */ }
|
|
800
|
+
}
|
|
789
801
|
reportFirstOutput.parsed(parsed);
|
|
790
802
|
const status = terminalStatusFromParsed(parsed);
|
|
791
803
|
if (status) sawTerminal = true;
|
|
@@ -953,6 +965,8 @@ export type InspectionConsumerOptions = {
|
|
|
953
965
|
now?: () => number;
|
|
954
966
|
/** Forward provider-scoped response-id pinning to the owned inspector. */
|
|
955
967
|
pinCompletedResponseIdToFirstSeen?: boolean;
|
|
968
|
+
/** Observe every parsed SSE payload on the inspection side; see SseInspectorHandlers. */
|
|
969
|
+
onParsedPayload?: (payload: unknown) => void;
|
|
956
970
|
/** Test seam for proving both public consumers dispose their owned inspector. */
|
|
957
971
|
inspectorFactory?: (handlers: SseInspectorHandlers) => SseInspector;
|
|
958
972
|
};
|
|
@@ -1108,6 +1122,7 @@ export function consumeForInspection(
|
|
|
1108
1122
|
onTerminal,
|
|
1109
1123
|
logCtx,
|
|
1110
1124
|
onCompletedResponse,
|
|
1125
|
+
onParsedPayload: options?.onParsedPayload,
|
|
1111
1126
|
onFirstOutput,
|
|
1112
1127
|
pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
|
|
1113
1128
|
});
|
|
@@ -1158,6 +1173,7 @@ export function consumeForResponseLogMetadata(
|
|
|
1158
1173
|
const inspector = (options?.inspectorFactory ?? createSseInspector)({
|
|
1159
1174
|
logCtx,
|
|
1160
1175
|
onCompletedResponse,
|
|
1176
|
+
onParsedPayload: options?.onParsedPayload,
|
|
1161
1177
|
onFirstOutput,
|
|
1162
1178
|
pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
|
|
1163
1179
|
});
|
|
@@ -51,6 +51,8 @@ import {
|
|
|
51
51
|
CodexPoolAuthenticationError,
|
|
52
52
|
CodexThreadAffinityExpiredError,
|
|
53
53
|
headersForCodexAuthContext,
|
|
54
|
+
materializeCodexUpstreamAuth,
|
|
55
|
+
CodexMainSubstitutionUnavailableError,
|
|
54
56
|
isCodexAuthContextUsable,
|
|
55
57
|
resolveCodexAuthContext,
|
|
56
58
|
codexProbeLeaseId,
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
type UpstreamHostAdmissionLease,
|
|
82
84
|
} from "../../codex/upstream-host-health";
|
|
83
85
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
86
|
+
import type { DataPlaneAdmission } from "../auth-cors";
|
|
84
87
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
85
88
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
|
|
86
89
|
import { slugsEquivalent } from "../../providers/slug-codec";
|
|
@@ -269,6 +272,7 @@ export async function handleResponsesCompact(
|
|
|
269
272
|
config: OcxConfig,
|
|
270
273
|
logCtx: RequestLogContext,
|
|
271
274
|
turnAdmissionLease?: AdmissionLease,
|
|
275
|
+
admission?: DataPlaneAdmission,
|
|
272
276
|
): Promise<Response> {
|
|
273
277
|
let body: unknown;
|
|
274
278
|
try {
|
|
@@ -316,7 +320,10 @@ export async function handleResponsesCompact(
|
|
|
316
320
|
logCtx.resolvedModel = route.modelId;
|
|
317
321
|
}
|
|
318
322
|
|
|
319
|
-
|
|
323
|
+
// #1686: a bearer-presented admission secret is one of ours, so the stored main credential
|
|
324
|
+
// is substituted below instead of the caller bearer being forwarded.
|
|
325
|
+
const substituteMainCredential = admission?.source === "bearer";
|
|
326
|
+
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
320
327
|
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
321
328
|
catch (err) {
|
|
322
329
|
if (err instanceof ForwardAdmissionCredentialError) return formatErrorResponse(401, "authentication_error", err.message);
|
|
@@ -364,7 +371,7 @@ export async function handleResponsesCompact(
|
|
|
364
371
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
365
372
|
});
|
|
366
373
|
logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
|
|
367
|
-
const selected =
|
|
374
|
+
const selected = materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential });
|
|
368
375
|
compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
369
376
|
for (const name of FORWARD_HEADERS) {
|
|
370
377
|
const value = selected.get(name);
|
|
@@ -671,7 +678,7 @@ export async function handleResponsesCompact(
|
|
|
671
678
|
headers: internalHeaders,
|
|
672
679
|
body: JSON.stringify(internalBody),
|
|
673
680
|
});
|
|
674
|
-
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease });
|
|
681
|
+
const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, ...(admission ? { admission } : {}) });
|
|
675
682
|
if (!response.ok) return response;
|
|
676
683
|
let json: { output?: unknown[]; status?: unknown; error?: unknown };
|
|
677
684
|
try {
|