@bitkyc08/opencodex 2.78.0 → 2.79.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/App-BOUa-ek3.js +54 -0
- package/gui/dist/assets/App-CSJvmjNs.css +1 -0
- package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-DUtzB4zF.js} +1 -1
- package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
- package/gui/dist/assets/index-DwHNDF4s.js +94 -0
- package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-BuL4WIBo.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/devin.ts +22 -1
- package/src/claude/desktop-picker.ts +11 -9
- package/src/claude/inbound-content-options.ts +29 -2
- package/src/claude/intercept/local-ca-files.ts +265 -0
- package/src/claude/intercept/local-ca-windows.ts +73 -0
- package/src/claude/intercept/local-ca.ts +12 -24
- package/src/claude/intercept/picker-ca-persistence.ts +106 -0
- package/src/claude/intercept/picker-ca-startup.ts +16 -0
- package/src/claude/intercept/picker-ca-store.ts +70 -0
- package/src/claude/intercept/picker-ca.ts +43 -9
- package/src/claude/intercept/picker-runtime.ts +14 -4
- package/src/claude/intercept/runtime.ts +32 -23
- package/src/cli/ensure-readiness.ts +48 -0
- package/src/cli/gui-pair-client.ts +15 -7
- package/src/cli/gui.ts +4 -1
- package/src/cli/hub.ts +1 -0
- package/src/cli/index.ts +10 -11
- package/src/cli/status.ts +2 -2
- package/src/cli/update-restart-child.ts +4 -1
- package/src/codex/account-credit-use.ts +8 -2
- package/src/codex/auth-api/account-pause-group.ts +113 -0
- package/src/codex/auth-api/routes.ts +18 -12
- package/src/codex/auth-context.ts +45 -3
- package/src/codex/autostart-health.ts +10 -0
- package/src/codex/pool-credit-policy.ts +67 -0
- package/src/generated/compatibility-version.json +106 -46
- package/src/lib/gui-pair-capability.ts +1 -1
- package/src/lib/gui-pair-intent.ts +170 -0
- package/src/lib/windows-owner-acl.ts +143 -0
- package/src/providers/openai-sidecar-credit.ts +26 -0
- package/src/providers/openai-sidecar.ts +35 -0
- package/src/server/claude-messages.ts +1 -0
- package/src/server/gui-pair-delivery.ts +23 -0
- package/src/server/index/serve-options.ts +5 -8
- package/src/server/live-native-calls.ts +94 -0
- package/src/server/live.ts +28 -5
- package/src/server/local-management-read-client.ts +15 -11
- package/src/server/management/oauth-account-routes.ts +15 -0
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/shared.ts +9 -3
- package/src/server/messages-native-scope.ts +57 -0
- package/src/server/messages-native.ts +9 -0
- package/src/server/responses/adapter-dispatch.ts +31 -2
- package/src/server/responses/compact.ts +17 -2
- package/src/server/responses/core-auth.ts +18 -1
- package/src/server/responses/core-codex-account.ts +18 -8
- package/src/server/responses/non-replayable-error.ts +48 -0
- package/src/server/responses/passthrough-delivery.ts +5 -1
- package/src/server/responses/passthrough-dispatch.ts +21 -19
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/sidecar-execution.ts +1 -0
- package/src/server/search.ts +4 -0
- package/src/server/startup-health-cache.ts +2 -6
- package/src/vision/describe.ts +17 -10
- package/src/vision/index.ts +1 -0
- package/src/web-search/executor.ts +17 -10
- package/src/web-search/format-result.ts +304 -20
- package/src/web-search/loop.ts +2 -1
- package/src/web-search/passthrough-bridge.ts +1 -0
- package/src/web-search/run-turn-loop.ts +1 -1
- package/gui/dist/assets/App-BtfG5id-.js +0 -54
- package/gui/dist/assets/App-lCIslZ1L.css +0 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
|
@@ -110,6 +110,8 @@ import { linkAbortSignal } from "./responses/core-lifetime";
|
|
|
110
110
|
import { attachRequestSpendTracker } from "./responses/request-spend";
|
|
111
111
|
import { workflowRefusalResponse } from "./workflow-refusal";
|
|
112
112
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
113
|
+
import { admissionModelDeniedResponse, type AdmissionModelScope } from "./admission-model-scope";
|
|
114
|
+
import { nativeMessagesToolScopeDenial } from "./messages-native-scope";
|
|
113
115
|
|
|
114
116
|
export {
|
|
115
117
|
isNativeMessagesRouteEligible,
|
|
@@ -159,6 +161,7 @@ export interface HandleNativeMessagesOptions {
|
|
|
159
161
|
callerAnthropicBeta?: string | null;
|
|
160
162
|
clientIdentity?: AnthropicClientIdentity;
|
|
161
163
|
sessionKey?: string | null;
|
|
164
|
+
modelScope?: AdmissionModelScope;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
|
|
@@ -350,6 +353,12 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
350
353
|
return null;
|
|
351
354
|
};
|
|
352
355
|
|
|
356
|
+
const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
|
|
357
|
+
if (toolScopeDenial) {
|
|
358
|
+
finishLog(403, toolScopeDenial.message);
|
|
359
|
+
return admissionModelDeniedResponse(toolScopeDenial);
|
|
360
|
+
}
|
|
361
|
+
|
|
353
362
|
try {
|
|
354
363
|
await prepareNativeBody(body, req.signal);
|
|
355
364
|
} catch (error) {
|
|
@@ -4,6 +4,7 @@ import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
|
4
4
|
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
5
5
|
import { transientSendCapFor } from "./request-send-budget";
|
|
6
6
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
7
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
7
8
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
8
9
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
9
10
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
@@ -100,6 +101,9 @@ import {
|
|
|
100
101
|
} from "../../lib/errors";
|
|
101
102
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
102
103
|
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
104
|
+
import { createCodexAuthDispatchGuard, releaseCodexAuthContextProbeLease, unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
|
|
105
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
106
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
103
107
|
import { chargeWorkflowSends } from "../../lib/workflow-budget";
|
|
104
108
|
import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
|
|
105
109
|
|
|
@@ -344,6 +348,8 @@ export async function prepareAdapterExchange(
|
|
|
344
348
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
345
349
|
providerName: route.providerName,
|
|
346
350
|
modelId: route.modelId,
|
|
351
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
352
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
347
353
|
}),
|
|
348
354
|
}));
|
|
349
355
|
} else {
|
|
@@ -384,6 +390,8 @@ export async function prepareAdapterExchange(
|
|
|
384
390
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
385
391
|
providerName: route.providerName,
|
|
386
392
|
modelId: route.modelId,
|
|
393
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
394
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
387
395
|
}));
|
|
388
396
|
},
|
|
389
397
|
{
|
|
@@ -406,6 +414,13 @@ export async function prepareAdapterExchange(
|
|
|
406
414
|
upstream.abort();
|
|
407
415
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
408
416
|
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
417
|
+
const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
418
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
419
|
+
});
|
|
420
|
+
if (codexRefusal) {
|
|
421
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
422
|
+
return codexRefusal;
|
|
423
|
+
}
|
|
409
424
|
// A pause committed during pacing is local admission policy, not a failed upstream.
|
|
410
425
|
if (refusal instanceof OAuthAccountPausedError) {
|
|
411
426
|
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
@@ -550,6 +565,8 @@ export async function prepareAdapterExchange(
|
|
|
550
565
|
dispatchOverride: oauthDispatch(retryRequest),
|
|
551
566
|
providerName: route.providerName,
|
|
552
567
|
modelId: route.modelId,
|
|
568
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
569
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
553
570
|
}),
|
|
554
571
|
});
|
|
555
572
|
});
|
|
@@ -601,6 +618,8 @@ export async function prepareAdapterExchange(
|
|
|
601
618
|
dispatchOverride: oauthDispatch(retryRequest),
|
|
602
619
|
providerName: route.providerName,
|
|
603
620
|
modelId: route.modelId,
|
|
621
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
622
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
604
623
|
}));
|
|
605
624
|
},
|
|
606
625
|
{
|
|
@@ -624,6 +643,15 @@ export async function prepareAdapterExchange(
|
|
|
624
643
|
retryRequest.releaseBodyObservation?.();
|
|
625
644
|
}
|
|
626
645
|
} catch (err) {
|
|
646
|
+
const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
647
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
648
|
+
});
|
|
649
|
+
if (codexRefusal) {
|
|
650
|
+
cleanupUpstreamAbort();
|
|
651
|
+
upstream.abort();
|
|
652
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
653
|
+
return { failed: codexRefusal };
|
|
654
|
+
}
|
|
627
655
|
if (preserveFailureResponse && !replacementAdmitted && !options.abortSignal?.aborted)
|
|
628
656
|
return { failed: preserveFailureResponse };
|
|
629
657
|
cleanupUpstreamAbort();
|
|
@@ -704,8 +732,9 @@ export async function prepareAdapterExchange(
|
|
|
704
732
|
// Preserve the terminal verdict through adapter and combo error formatting.
|
|
705
733
|
// This also covers a reset reached by a 401/429/413 recovery refetch.
|
|
706
734
|
if (isNonReplayableResponse(upstreamResponse)) {
|
|
707
|
-
|
|
708
|
-
|
|
735
|
+
try {
|
|
736
|
+
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
737
|
+
} finally { cleanupUpstreamAbort(); }
|
|
709
738
|
}
|
|
710
739
|
if (
|
|
711
740
|
upstreamResponse.status === 401
|
|
@@ -53,7 +53,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
|
|
|
53
53
|
import {
|
|
54
54
|
applyCodexAuthContextToProvider,
|
|
55
55
|
callerCodexWorkspaceAccountId,
|
|
56
|
-
|
|
56
|
+
createCodexAuthDispatchGuard,
|
|
57
57
|
unwrapUpstreamRetryEvidenceError,
|
|
58
58
|
CodexMainProfileDrainingError,
|
|
59
59
|
headersForCodexAuthContext,
|
|
@@ -182,6 +182,8 @@ import {
|
|
|
182
182
|
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
|
|
183
183
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
184
184
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
185
|
+
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
186
|
+
import { clientCancelledResponse } from "./core-errors";
|
|
185
187
|
|
|
186
188
|
import {
|
|
187
189
|
codexAccountGatedCanonicalWireModel,
|
|
@@ -435,6 +437,9 @@ async function refreshPoolCompactContext(args: {
|
|
|
435
437
|
generation: refreshed.generation,
|
|
436
438
|
poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
|
|
437
439
|
};
|
|
440
|
+
// See the core counterpart: the spread dropped the WeakMap-bound credit policy, so a hold
|
|
441
|
+
// or opt-out landing during the refresh await would go unchecked on the copy.
|
|
442
|
+
rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
|
|
438
443
|
const refreshedProvider = applyCodexAuthContextToProvider(
|
|
439
444
|
stripCodexRuntimeProviderFields(provider),
|
|
440
445
|
refreshedAuthCtx,
|
|
@@ -463,6 +468,16 @@ async function refreshPoolCompactContext(args: {
|
|
|
463
468
|
if (isTerminalCompactPoolRefreshFailure(error)) {
|
|
464
469
|
return { ok: false, quarantine: true, response: reauthResponse() };
|
|
465
470
|
}
|
|
471
|
+
if (req.signal.aborted) {
|
|
472
|
+
return { ok: false, quarantine: false, response: clientCancelledResponse() };
|
|
473
|
+
}
|
|
474
|
+
// See the core counterpart: a policy refusal raised after a successful refresh maps like
|
|
475
|
+
// the admission path — a reset-bound 429, never a 503 mislabeled as a refresh failure.
|
|
476
|
+
const policyResponse = mapCodexAuthContextErrorToResponse(error, {
|
|
477
|
+
now: Date.now(),
|
|
478
|
+
accountSelector: args.codexAccountNamespace,
|
|
479
|
+
});
|
|
480
|
+
if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
|
|
466
481
|
return {
|
|
467
482
|
ok: false,
|
|
468
483
|
quarantine: false,
|
|
@@ -1042,7 +1057,7 @@ export async function handleResponsesCompact(
|
|
|
1042
1057
|
providerName: route.providerName,
|
|
1043
1058
|
modelId: route.modelId,
|
|
1044
1059
|
beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
|
|
1045
|
-
?
|
|
1060
|
+
? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
|
|
1046
1061
|
}),
|
|
1047
1062
|
// Every credential-bearing forward send gets manual redirects, not only
|
|
1048
1063
|
// pool sends: direct mode carries the caller's credential too (#914).
|
|
@@ -41,6 +41,7 @@ import { markLocalRequestLogRefusal } from "../request-log";
|
|
|
41
41
|
import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
|
|
42
42
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
43
43
|
import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
44
|
+
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
44
45
|
import { safeCallerSessionId } from "../caller-session-identity";
|
|
45
46
|
|
|
46
47
|
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
@@ -200,6 +201,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
200
201
|
credentialDomainWasRewritten = false,
|
|
201
202
|
retainAccountForUploadedFiles = false,
|
|
202
203
|
): Promise<ResponsesAuthResolution> {
|
|
204
|
+
let authCtx: CodexAuthContext | undefined;
|
|
203
205
|
try {
|
|
204
206
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
205
207
|
req,
|
|
@@ -260,7 +262,6 @@ export async function resolveResponsesCodexAuth(
|
|
|
260
262
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
261
263
|
validateForwardAdmissionCredential(authInputHeaders, config);
|
|
262
264
|
}
|
|
263
|
-
let authCtx: CodexAuthContext;
|
|
264
265
|
if (route.codexAccountMode) {
|
|
265
266
|
authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
|
|
266
267
|
admission: options.admission,
|
|
@@ -342,6 +343,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
342
343
|
substituteMainCredential,
|
|
343
344
|
};
|
|
344
345
|
} catch (err) {
|
|
346
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
345
347
|
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
346
348
|
return { ok: false, response: clientCancelledResponse() };
|
|
347
349
|
}
|
|
@@ -480,6 +482,9 @@ export async function refreshPoolForwardAuth(args: {
|
|
|
480
482
|
generation: refreshed.generation,
|
|
481
483
|
poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
|
|
482
484
|
};
|
|
485
|
+
// The spread dropped the WeakMap-bound credit policy: without the live binding a hold or
|
|
486
|
+
// opt-out landing during the refresh await would go unchecked on every no-override path.
|
|
487
|
+
rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
|
|
483
488
|
const provider = applyCodexAuthContextToProvider(
|
|
484
489
|
stripCodexRuntimeProviderFields(route.provider),
|
|
485
490
|
refreshedAuthCtx,
|
|
@@ -502,6 +507,18 @@ export async function refreshPoolForwardAuth(args: {
|
|
|
502
507
|
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
503
508
|
};
|
|
504
509
|
}
|
|
510
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
511
|
+
return { ok: false, quarantine: false, response: clientCancelledResponse() };
|
|
512
|
+
}
|
|
513
|
+
// The credential itself refreshed; a policy refusal raised afterwards (a credit hold or an
|
|
514
|
+
// opt-out that landed during the await) is not a refresh failure. Map it the same way the
|
|
515
|
+
// admission path does — a reset-bound 429, never a 503 telling the caller to sign in — and
|
|
516
|
+
// never quarantine a valid account for a policy refusal.
|
|
517
|
+
const policyResponse = mapCodexAuthContextErrorToResponse(error, {
|
|
518
|
+
accountSelector: route.codexAccountNamespace,
|
|
519
|
+
now: Date.now(),
|
|
520
|
+
});
|
|
521
|
+
if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
|
|
505
522
|
return {
|
|
506
523
|
ok: false,
|
|
507
524
|
quarantine: false,
|
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
headersForCodexAuthContext,
|
|
55
55
|
applyCodexAuthContextToProvider,
|
|
56
56
|
stripCodexRuntimeProviderFields,
|
|
57
|
-
|
|
57
|
+
createCodexAuthDispatchGuard,
|
|
58
|
+
CodexPoolAccountCreditsOffError,
|
|
58
59
|
} from "../../codex/auth-context";
|
|
59
60
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
60
61
|
import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
@@ -745,12 +746,21 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
745
746
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
746
747
|
// ordinary requests must block the first account before the alternate send.
|
|
747
748
|
if (!deferFirstOutcome) recordFirstOutcome();
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
retryAuthCtx,
|
|
752
|
-
"pool"
|
|
753
|
-
)
|
|
749
|
+
let retryHeaders: Headers;
|
|
750
|
+
let retryProvider: ReturnType<typeof applyCodexAuthContextToProvider>;
|
|
751
|
+
try {
|
|
752
|
+
retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
|
|
753
|
+
retryProvider = applyCodexAuthContextToProvider(stripCodexRuntimeProviderFields(route.provider), retryAuthCtx, "pool");
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (!(error instanceof CodexPoolAccountCreditsOffError)) throw error;
|
|
756
|
+
// Body/entitlement reads above can outlive credit consent. No alternate will send:
|
|
757
|
+
// release its reservation and probes, then let the owner map the policy refusal.
|
|
758
|
+
accountMovePermit?.release();
|
|
759
|
+
releaseCodexAuthContextProbeLease(firstAuthCtx);
|
|
760
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
761
|
+
await firstResponse.body?.cancel().catch(() => undefined);
|
|
762
|
+
return { kind: "transport", error, authCtx: retryAuthCtx };
|
|
763
|
+
}
|
|
754
764
|
const retryAdapter = resolveAdapter(
|
|
755
765
|
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
|
|
756
766
|
config.cacheRetention,
|
|
@@ -889,7 +899,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
889
899
|
modelId: route.modelId,
|
|
890
900
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
891
901
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
892
|
-
?
|
|
902
|
+
? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
893
903
|
}),
|
|
894
904
|
// Credential-bearing forward send: never follow a redirect into a
|
|
895
905
|
// dead-host rejection after the credential was seen (#914).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isNonReplayableResponse, isReplayRefusalResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
2
|
+
import { clientCancelledResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
3
|
+
|
|
4
|
+
const UPSTREAM_ERROR_TYPES = new Set([
|
|
5
|
+
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
6
|
+
"rate_limit_error", "request_too_large", "overloaded_error", "api_error", "server_error", "insufficient_quota",
|
|
7
|
+
]);
|
|
8
|
+
const UPSTREAM_ERROR_CODES = new Set([
|
|
9
|
+
"context_length_exceeded", "rate_limit_exceeded", "insufficient_quota", "invalid_api_key",
|
|
10
|
+
"model_not_found", "invalid_prompt", "content_policy_violation", "server_error",
|
|
11
|
+
"unsupported_parameter", "invalid_value", "string_above_max_length",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/** Client projection never grants another send or turns a real upstream error into our refusal. */
|
|
15
|
+
export async function sanitizeNonReplayableUpstreamError(
|
|
16
|
+
response: Response,
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
if (response.ok || !isNonReplayableResponse(response) || isReplayRefusalResponse(response)) return response;
|
|
20
|
+
const text = await readDisplaySafeErrorText(response, signal, "");
|
|
21
|
+
if (signal.aborted) {
|
|
22
|
+
const cancelled = clientCancelledResponse();
|
|
23
|
+
markResponseNonReplayable(cancelled);
|
|
24
|
+
return cancelled;
|
|
25
|
+
}
|
|
26
|
+
let type = "upstream_error";
|
|
27
|
+
let code: string | undefined;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(text);
|
|
30
|
+
const upstreamType = parsed?.error?.type;
|
|
31
|
+
const upstreamCode = parsed?.error?.code;
|
|
32
|
+
if (typeof upstreamType === "string" && UPSTREAM_ERROR_TYPES.has(upstreamType)) type = upstreamType;
|
|
33
|
+
if (typeof upstreamCode === "string" && UPSTREAM_ERROR_CODES.has(upstreamCode)) code = upstreamCode;
|
|
34
|
+
} catch {
|
|
35
|
+
// Incomplete or non-JSON bodies retain the generic error type.
|
|
36
|
+
}
|
|
37
|
+
// Upstream text is not forwarded here: no bounded redaction can be proven complete against a client's decoders.
|
|
38
|
+
const body = JSON.stringify({ error: { type, ...(code === undefined ? {} : { code }),
|
|
39
|
+
message: `Provider error ${response.status}: upstream diagnostic withheld for a non-replayable failure`,
|
|
40
|
+
} });
|
|
41
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
42
|
+
if (response.headers.get("x-should-retry") === "false") headers.set("x-should-retry", "false");
|
|
43
|
+
const safe = new Response(response.status === 304 ? null : body, {
|
|
44
|
+
status: response.status, headers,
|
|
45
|
+
});
|
|
46
|
+
markResponseNonReplayable(safe);
|
|
47
|
+
return safe;
|
|
48
|
+
}
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
relayWithAbort,
|
|
19
19
|
} from "../relay";
|
|
20
20
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
21
|
-
import { isReplayRefusalResponse } from "../../lib/upstream-retry";
|
|
21
|
+
import { isNonReplayableResponse, isReplayRefusalResponse } from "../../lib/upstream-retry";
|
|
22
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
22
23
|
import { teeWithBoundedInspection } from "../inspection-tee";
|
|
23
24
|
import {
|
|
24
25
|
codexForwardTerminalOutcomeRecorder,
|
|
@@ -543,6 +544,9 @@ export async function deliverPassthroughResponse(
|
|
|
543
544
|
// through sanitizePassthroughHeaders) so a redirect to a dead host can never
|
|
544
545
|
// masquerade as a pre-connection failure after the credential was seen.
|
|
545
546
|
// The numeric outcome above already classified it neutral — no streak.
|
|
547
|
+
if (isNonReplayableResponse(upstreamResponse) && !isReplayRefusalResponse(upstreamResponse)) {
|
|
548
|
+
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
549
|
+
}
|
|
546
550
|
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
547
551
|
return new Response(upstreamResponse.body, {
|
|
548
552
|
status: upstreamResponse.status,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
codexProbeLeaseId,
|
|
38
38
|
codexProbeQuotaScope,
|
|
39
39
|
codexTransientProbeGrant,
|
|
40
|
-
|
|
40
|
+
createCodexAuthDispatchGuard,
|
|
41
41
|
} from "../../codex/auth-context";
|
|
42
42
|
import {
|
|
43
43
|
NamespaceToolCollisionError,
|
|
@@ -87,7 +87,6 @@ import {
|
|
|
87
87
|
fetchWithHeaderTimeout,
|
|
88
88
|
providerFetch,
|
|
89
89
|
safeHostLabel,
|
|
90
|
-
storedPoolReplayDispatchNotifier,
|
|
91
90
|
} from "./fetch-helpers";
|
|
92
91
|
import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
93
92
|
import { clientCancelledResponse } from "./core-errors";
|
|
@@ -816,7 +815,7 @@ export async function preparePassthroughExchange(
|
|
|
816
815
|
throw new Error("Credential selection changed before pre-output stream recovery");
|
|
817
816
|
}
|
|
818
817
|
if (isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
819
|
-
|
|
818
|
+
createCodexAuthDispatchGuard(
|
|
820
819
|
admissionState.authCtx,
|
|
821
820
|
options.codexAuthPolicy ?? config,
|
|
822
821
|
route.modelId,
|
|
@@ -984,7 +983,7 @@ export async function preparePassthroughExchange(
|
|
|
984
983
|
modelId: route.modelId,
|
|
985
984
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
986
985
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
987
|
-
?
|
|
986
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
988
987
|
}),
|
|
989
988
|
route.provider.authMode === "forward")
|
|
990
989
|
// Every real attempt response — including an intermediate 5xx the
|
|
@@ -1089,7 +1088,7 @@ export async function preparePassthroughExchange(
|
|
|
1089
1088
|
modelId: route.modelId,
|
|
1090
1089
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1091
1090
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1092
|
-
?
|
|
1091
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1093
1092
|
}),
|
|
1094
1093
|
route.provider.authMode === "forward")
|
|
1095
1094
|
.then(adoptObservedResponse);
|
|
@@ -1180,13 +1179,13 @@ export async function preparePassthroughExchange(
|
|
|
1180
1179
|
// The replay-dispatched signal is what bounds the rest of this logical request, so it
|
|
1181
1180
|
// has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
|
|
1182
1181
|
// admission BEFORE calling the executor, so signalling at the call site would spend the
|
|
1183
|
-
// budget even when a rejected pacing wait means nothing reaches the network.
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1182
|
+
// budget even when a rejected pacing wait means nothing reaches the network. Signal
|
|
1183
|
+
// only after the physical-dispatch policy check succeeds; a local refusal is no send.
|
|
1184
|
+
// A replacement is another send of the same replay, rather than a second announcement.
|
|
1185
|
+
const replayGuard = isCanonicalOpenAiForwardProvider(route.provider)
|
|
1186
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined;
|
|
1187
|
+
let replayNotified = false;
|
|
1188
|
+
const oauthReplayExecutor = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1190
1189
|
httpOnly: canonicalBufferedJson,
|
|
1191
1190
|
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1192
1191
|
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
@@ -1195,11 +1194,14 @@ export async function preparePassthroughExchange(
|
|
|
1195
1194
|
providerName: route.providerName,
|
|
1196
1195
|
modelId: route.modelId,
|
|
1197
1196
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1198
|
-
beforeDispatch:
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1197
|
+
beforeDispatch: headers => {
|
|
1198
|
+
replayGuard?.(headers);
|
|
1199
|
+
if (codex401ReplayKind === "stored" && !replayNotified) {
|
|
1200
|
+
replayNotified = true;
|
|
1201
|
+
options.onStoredPool401ReplayDispatched?.();
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1203
1205
|
// Routed through the shared helper so an ambiguous reset on THIS leg answers with the
|
|
1204
1206
|
// same refusal every other leg gives. A bare fetch here rejected instead, and the
|
|
1205
1207
|
// caller's transport-failure path turns a rejection into a client-retryable 502 --
|
|
@@ -1346,7 +1348,7 @@ export async function preparePassthroughExchange(
|
|
|
1346
1348
|
modelId: route.modelId,
|
|
1347
1349
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1348
1350
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1349
|
-
?
|
|
1351
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1350
1352
|
}),
|
|
1351
1353
|
route.provider.authMode === "forward")
|
|
1352
1354
|
.then(adoptObservedResponse);
|
|
@@ -1481,7 +1483,7 @@ export async function preparePassthroughExchange(
|
|
|
1481
1483
|
modelId: route.modelId,
|
|
1482
1484
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1483
1485
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1484
|
-
?
|
|
1486
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1485
1487
|
}),
|
|
1486
1488
|
route.provider.authMode === "forward")
|
|
1487
1489
|
.then(adoptObservedResponse);
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
codexPoolAffinityKey,
|
|
47
47
|
previewCodexPoolLineage,
|
|
48
48
|
applyCodexAuthContextToProvider,
|
|
49
|
+
releaseCodexAuthContextProbeLease,
|
|
49
50
|
hasCallerCodexBearer,
|
|
50
51
|
requestOwnedMainPinState,
|
|
51
52
|
requestOwnedMainCredentialIsLive,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
previousResponseProviderState,
|
|
59
60
|
} from "../../responses/state";
|
|
60
61
|
import { formatErrorResponse } from "../../bridge";
|
|
62
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
61
63
|
import type { OcxParsedRequest } from "../../types";
|
|
62
64
|
import { buildToolBridgeMaps } from "./collaboration";
|
|
63
65
|
import { parseRequest } from "../../responses/parser";
|
|
@@ -1352,7 +1354,15 @@ export async function prepareResponsesRequest(
|
|
|
1352
1354
|
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
1353
1355
|
}
|
|
1354
1356
|
|
|
1355
|
-
|
|
1357
|
+
try {
|
|
1358
|
+
route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
1361
|
+
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
|
|
1362
|
+
const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now(), accountSelector: route.codexAccountNamespace });
|
|
1363
|
+
if (mapped) return mapped;
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1356
1366
|
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
1357
1367
|
try {
|
|
1358
1368
|
recordPolicyPreparedDestination(policyScope, route.providerName, parsed._wireModelOverride ?? parsed.modelId);
|
|
@@ -510,6 +510,7 @@ export async function executeResponsesSidecars(
|
|
|
510
510
|
transportState.bindKeyUsageFromBridge(usage);
|
|
511
511
|
},
|
|
512
512
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
513
|
+
beforeSidecarDispatch: wsPlan.forwardSidecar?.beforeDispatch,
|
|
513
514
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
514
515
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
515
516
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
package/src/server/search.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* paid backend than the one the operator named.
|
|
12
12
|
*/
|
|
13
13
|
import { formatErrorResponse } from "../bridge";
|
|
14
|
+
import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
|
|
14
15
|
import {
|
|
15
16
|
CodexAccountCooldownError,
|
|
16
17
|
codexMainProfileDrainingResponse,
|
|
@@ -202,6 +203,7 @@ export async function handleSearch(
|
|
|
202
203
|
const sidecarExit = sidecarEnter("search");
|
|
203
204
|
let upstreamResponse: Response | undefined;
|
|
204
205
|
try {
|
|
206
|
+
upstream.beforeDispatch?.();
|
|
205
207
|
upstreamResponse = await fetch(url, {
|
|
206
208
|
method: "POST",
|
|
207
209
|
headers,
|
|
@@ -233,6 +235,8 @@ export async function handleSearch(
|
|
|
233
235
|
if (req.signal.aborted) {
|
|
234
236
|
return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
|
|
235
237
|
}
|
|
238
|
+
const policyRefusal = openAiSidecarCreditRefusal(err);
|
|
239
|
+
if (policyRefusal) return cooldownErrorResponse(policyRefusal, Date.now(), accountNamespace);
|
|
236
240
|
if (linkedSignal.signal.aborted || (err instanceof Error && err.name === "TimeoutError")) {
|
|
237
241
|
upstream.recordOutcome?.("timeout");
|
|
238
242
|
return formatErrorResponse(504, "upstream_error", "search upstream timed out");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { codexAutoStartEnabled } from "../config";
|
|
4
|
-
import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health";
|
|
4
|
+
import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
|
|
5
5
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
6
6
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
7
7
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
@@ -11,7 +11,7 @@ import type { OcxConfig } from "../types";
|
|
|
11
11
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
12
12
|
|
|
13
13
|
const CACHE_TTL_MS = 30_000;
|
|
14
|
-
const PROBE_TIMEOUT_MS =
|
|
14
|
+
const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
|
|
15
15
|
const INITIAL_PROBE_WAIT_MS = PROBE_TIMEOUT_MS + 500;
|
|
16
16
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
17
17
|
|
|
@@ -31,10 +31,6 @@ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
|
31
31
|
* bound in both cases: a wedged probe is still abandoned, and the caller still
|
|
32
32
|
* receives the previous reading rather than waiting on it.
|
|
33
33
|
*/
|
|
34
|
-
function probeTimeoutMs(): number {
|
|
35
|
-
return process.platform === "win32" ? 15_000 : 5_000;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
/** The probe bound, so a test's own budget cannot fall below what it must wait for. */
|
|
39
35
|
export function startupHealthProbeTimeoutMs(): number {
|
|
40
36
|
return PROBE_TIMEOUT_MS;
|
package/src/vision/describe.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-
|
|
|
8
8
|
import { parseSidecarSSE } from "../web-search/parse";
|
|
9
9
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
10
10
|
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
11
|
+
import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
|
|
11
12
|
|
|
12
13
|
export interface VisionSettings {
|
|
13
14
|
model: string;
|
|
@@ -60,6 +61,7 @@ export async function describeImage(
|
|
|
60
61
|
settings: VisionSettings,
|
|
61
62
|
abortSignal?: AbortSignal,
|
|
62
63
|
recordOutcome?: SidecarOutcomeRecorder,
|
|
64
|
+
beforeDispatch?: () => void,
|
|
63
65
|
): Promise<DescribeOutcome> {
|
|
64
66
|
if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
|
|
65
67
|
return { text: "", error: "Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision helper model." };
|
|
@@ -98,16 +100,19 @@ export async function describeImage(
|
|
|
98
100
|
const res = await fetchWithResetRetry(
|
|
99
101
|
// The replay needs `keepalive: false` to abandon the half-closed pooled socket; Bun has
|
|
100
102
|
// ignored a bare `Connection: close` (oven-sh/bun#20492).
|
|
101
|
-
recovery =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
recovery => {
|
|
104
|
+
beforeDispatch?.();
|
|
105
|
+
return fetch(`${forwardProvider.baseUrl}/responses`, applyUpstreamRecoveryInit({
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers,
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: linkedSignal.signal,
|
|
110
|
+
// Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
|
|
111
|
+
// across origins but forwards nonstandard headers such as `chatgpt-account-id`,
|
|
112
|
+
// `session_id`, and `x-codex-turn-metadata` to the redirect target.
|
|
113
|
+
redirect: "manual",
|
|
114
|
+
}, recovery));
|
|
115
|
+
},
|
|
111
116
|
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar" },
|
|
112
117
|
);
|
|
113
118
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
@@ -129,6 +134,8 @@ export async function describeImage(
|
|
|
129
134
|
detachBodyGuard();
|
|
130
135
|
}
|
|
131
136
|
} catch (e) {
|
|
137
|
+
const policyRefusal = openAiSidecarCreditRefusal(e);
|
|
138
|
+
if (policyRefusal) return { text: "", error: policyRefusal.message };
|
|
132
139
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
133
140
|
const callerAborted = abortSignal?.aborted === true
|
|
134
141
|
&& linkedSignal.signal.aborted
|