@bitkyc08/opencodex 2.7.40 → 2.7.41
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 +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
|
+
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
3
4
|
import {
|
|
4
5
|
getConfigPath,
|
|
5
6
|
multiAgentGuidanceEnabled,
|
|
@@ -9,7 +10,7 @@ import { parseRequest } from "../../responses/parser";
|
|
|
9
10
|
import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
|
|
10
11
|
import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
|
|
11
12
|
import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
|
|
12
|
-
import { routeModel } from "../../router";
|
|
13
|
+
import { routeModel, type RouteResult } from "../../router";
|
|
13
14
|
import {
|
|
14
15
|
advanceComboAfterFailure,
|
|
15
16
|
comboDefaultEffort,
|
|
@@ -42,6 +43,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
|
|
|
42
43
|
import {
|
|
43
44
|
applyCodexAuthContextToProvider,
|
|
44
45
|
CodexAccountCooldownError,
|
|
46
|
+
cooldownErrorResponse,
|
|
45
47
|
CodexAuthContextError,
|
|
46
48
|
CodexDirectAuthenticationError,
|
|
47
49
|
CodexPoolAuthenticationError,
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
} from "../../codex/auth-context";
|
|
57
59
|
import {
|
|
58
60
|
formatCodexProviderForLog,
|
|
61
|
+
previewCodexAccountForRequest,
|
|
59
62
|
recordCodexUpstreamOutcome,
|
|
60
63
|
type CodexUpstreamOutcome,
|
|
61
64
|
} from "../../codex/routing";
|
|
@@ -76,6 +79,12 @@ import { registerTurn, trackStreamLifetime, unregisterTurn } from "../lifecycle"
|
|
|
76
79
|
import { redactSecretString } from "../../lib/redact";
|
|
77
80
|
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
78
81
|
import { supportedLadderFor } from "../effort-policy";
|
|
82
|
+
import { isThreadSpawnRequest } from "../effort-policy";
|
|
83
|
+
import {
|
|
84
|
+
applySubagentModelFallback,
|
|
85
|
+
maybePrimeSubagentQuota,
|
|
86
|
+
recordSubagentQuotaFailureForThreadSpawn,
|
|
87
|
+
} from "../../codex/subagent-model-fallback";
|
|
79
88
|
import {
|
|
80
89
|
beginRequestAttempt,
|
|
81
90
|
catalogModelSupportsServiceTier,
|
|
@@ -266,6 +275,8 @@ export function comboUnavailableResponse(message: string): Response {
|
|
|
266
275
|
export interface ConsumedComboFailure {
|
|
267
276
|
response: Response;
|
|
268
277
|
classificationText: string;
|
|
278
|
+
/** Structured upstream `error.code` when present in the failure body. */
|
|
279
|
+
upstreamCode?: string;
|
|
269
280
|
/** Valid numeric/date value used only for cooldown calculation. */
|
|
270
281
|
retryAfter?: string;
|
|
271
282
|
/** Reserved for 040 usage attribution without adding another body read. */
|
|
@@ -314,12 +325,20 @@ export async function consumeComboFailure(
|
|
|
314
325
|
const fallback = `Provider error ${response.status}`;
|
|
315
326
|
let classificationText = fallback;
|
|
316
327
|
let usage: OcxUsage | undefined;
|
|
328
|
+
let upstreamCode: string | undefined;
|
|
317
329
|
try {
|
|
318
330
|
const body = await readBoundedResponseBody(response, { signal });
|
|
319
331
|
usage = usageFromComboFailureText(body.text);
|
|
320
332
|
if (body.displaySafe) {
|
|
321
333
|
const safeText = redactSecretString(body.text).slice(0, 500);
|
|
322
334
|
if (safeText) classificationText = safeText;
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(body.text) as { error?: { code?: unknown } | string };
|
|
337
|
+
const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.code : undefined;
|
|
338
|
+
if (typeof nested === "string" && nested.length > 0) upstreamCode = nested;
|
|
339
|
+
} catch {
|
|
340
|
+
/* non-JSON upstream body — message-only classification */
|
|
341
|
+
}
|
|
323
342
|
}
|
|
324
343
|
} catch (error) {
|
|
325
344
|
if (signal?.aborted) throw error;
|
|
@@ -330,8 +349,11 @@ export async function consumeComboFailure(
|
|
|
330
349
|
: `${fallback}: ${classificationText}`;
|
|
331
350
|
const retryAfter = sanitizedRetryAfter(response.headers.get("retry-after"), now);
|
|
332
351
|
return {
|
|
333
|
-
response: formatErrorResponse(response.status, "upstream_error", message
|
|
352
|
+
response: formatErrorResponse(response.status, "upstream_error", message, {
|
|
353
|
+
...(upstreamCode !== undefined ? { code: upstreamCode } : {}),
|
|
354
|
+
}),
|
|
334
355
|
classificationText,
|
|
356
|
+
...(upstreamCode !== undefined ? { upstreamCode } : {}),
|
|
335
357
|
...(retryAfter !== undefined ? { retryAfter } : {}),
|
|
336
358
|
...(usage ? { usage } : {}),
|
|
337
359
|
};
|
|
@@ -414,6 +436,169 @@ function unreadableEncryptedAgentTaskResponse(): Response {
|
|
|
414
436
|
);
|
|
415
437
|
}
|
|
416
438
|
|
|
439
|
+
type ResponsesAuthResolution =
|
|
440
|
+
| { ok: true; authCtx: CodexAuthContext; headers: Headers }
|
|
441
|
+
| { ok: false; response: Response };
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Resolve Codex auth for a route. On unusable contexts, releases any probe lease
|
|
445
|
+
* before returning the 401 (nothing reaches upstream).
|
|
446
|
+
*/
|
|
447
|
+
async function resolveResponsesCodexAuth(
|
|
448
|
+
req: Request,
|
|
449
|
+
config: OcxConfig,
|
|
450
|
+
route: RouteResult,
|
|
451
|
+
options: HandleResponsesOptions,
|
|
452
|
+
): Promise<ResponsesAuthResolution> {
|
|
453
|
+
try {
|
|
454
|
+
if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
|
|
455
|
+
let authCtx: CodexAuthContext;
|
|
456
|
+
if (route.codexAccountMode) {
|
|
457
|
+
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
|
|
458
|
+
options.onCodexAuthContextResolved?.(authCtx);
|
|
459
|
+
} else {
|
|
460
|
+
authCtx = { kind: "main", accountId: null };
|
|
461
|
+
options.onCodexAuthContextResolved?.(undefined);
|
|
462
|
+
}
|
|
463
|
+
if (!isCodexAuthContextUsable(authCtx, config)) {
|
|
464
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
ok: true,
|
|
472
|
+
authCtx,
|
|
473
|
+
headers: headersForCodexAuthContext(req.headers, authCtx),
|
|
474
|
+
};
|
|
475
|
+
} catch (err) {
|
|
476
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
477
|
+
return { ok: false, response: cooldownErrorResponse(err) };
|
|
478
|
+
}
|
|
479
|
+
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
response: formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session"),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (err instanceof CodexAuthContextError) {
|
|
486
|
+
const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
487
|
+
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (err instanceof CodexPoolAuthenticationError) {
|
|
494
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
495
|
+
}
|
|
496
|
+
if (err instanceof CodexDirectAuthenticationError) {
|
|
497
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
498
|
+
}
|
|
499
|
+
if (err instanceof ForwardAdmissionCredentialError) {
|
|
500
|
+
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
501
|
+
}
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Apply every route-dependent request mutation against the final selected route.
|
|
508
|
+
* Must run only after subagent fallback has settled the model/provider.
|
|
509
|
+
*/
|
|
510
|
+
async function applyFinalRouteRequestNormalization(args: {
|
|
511
|
+
parsed: OcxParsedRequest;
|
|
512
|
+
route: RouteResult;
|
|
513
|
+
config: OcxConfig;
|
|
514
|
+
req: Request;
|
|
515
|
+
logCtx: RequestLogContext;
|
|
516
|
+
}): Promise<void> {
|
|
517
|
+
const { parsed, route, config, req, logCtx } = args;
|
|
518
|
+
|
|
519
|
+
// Apply the routed model id upstream: routing may strip a "<provider>/" namespace.
|
|
520
|
+
if (route.modelId !== parsed.modelId) {
|
|
521
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
522
|
+
(parsed._rawBody as { model?: string }).model = route.modelId;
|
|
523
|
+
}
|
|
524
|
+
parsed.modelId = route.modelId;
|
|
525
|
+
}
|
|
526
|
+
// Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
|
|
527
|
+
// this request will actually use (#404).
|
|
528
|
+
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
|
|
529
|
+
logCtx.model = route.modelId;
|
|
530
|
+
logCtx.provider = route.providerName;
|
|
531
|
+
logCtx.providerAdapter = route.provider.adapter;
|
|
532
|
+
|
|
533
|
+
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
534
|
+
const finalSelectedModelId = route.modelId;
|
|
535
|
+
|
|
536
|
+
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
537
|
+
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
538
|
+
|
|
539
|
+
// Fast mode override for OpenAI-routed models.
|
|
540
|
+
if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
|
|
541
|
+
const tier = config.fastMode ? "priority" : undefined;
|
|
542
|
+
if (parsed._rawBody && typeof parsed._rawBody === "object") {
|
|
543
|
+
if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
|
|
544
|
+
else delete (parsed._rawBody as Record<string, unknown>).service_tier;
|
|
545
|
+
}
|
|
546
|
+
parsed.options.serviceTier = tier;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
{
|
|
550
|
+
const guidance = await multiAgentGuidanceText(parsed, {
|
|
551
|
+
multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
|
|
552
|
+
injectionModel: config.injectionModel,
|
|
553
|
+
injectionEffort: config.injectionEffort,
|
|
554
|
+
subagentModels: config.subagentModels,
|
|
555
|
+
subagentModelFallback: config.subagentModelFallback,
|
|
556
|
+
injectionPrompt: config.injectionPrompt,
|
|
557
|
+
});
|
|
558
|
+
if (guidance) {
|
|
559
|
+
injectDeveloperMessage(parsed, guidance);
|
|
560
|
+
if (isInjectionDebugEnabled()) {
|
|
561
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
|
|
562
|
+
}
|
|
563
|
+
} else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
|
|
564
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
{
|
|
569
|
+
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
|
|
570
|
+
const surface = collabSurface(parsed);
|
|
571
|
+
if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
|
|
572
|
+
const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
|
|
573
|
+
if (capped) {
|
|
574
|
+
logCtx.requestedEffort = `${capped.from}->${capped.to}`;
|
|
575
|
+
if (isInjectionDebugEnabled()) {
|
|
576
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
|
|
580
|
+
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
{
|
|
585
|
+
const { nativeEffortClamp, shouldApplyNativeEffortClamp } = await import("../../codex/catalog");
|
|
586
|
+
const clamped = shouldApplyNativeEffortClamp(route.providerName, route.provider, finalSelectedModelId)
|
|
587
|
+
? nativeEffortClamp(route.modelId, parsed.options.reasoning)
|
|
588
|
+
: null;
|
|
589
|
+
if (clamped) {
|
|
590
|
+
parsed.options.reasoning = clamped;
|
|
591
|
+
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
592
|
+
if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
|
|
593
|
+
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
|
|
597
|
+
route.modelId,
|
|
598
|
+
logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
417
602
|
|
|
418
603
|
|
|
419
604
|
export async function handleComboResponses(
|
|
@@ -604,7 +789,9 @@ export async function handleComboResponses(
|
|
|
604
789
|
(logCtx.attempts ??= []).push(attempt);
|
|
605
790
|
attemptRetained = true;
|
|
606
791
|
lastFailure = failure.response;
|
|
607
|
-
if (comboFailureDecision(response.status, failure.classificationText
|
|
792
|
+
if (comboFailureDecision(failure.response.status, failure.classificationText, {
|
|
793
|
+
code: failure.upstreamCode,
|
|
794
|
+
}) === "stop") {
|
|
608
795
|
Object.assign(logCtx, childLog, {
|
|
609
796
|
requestedModel,
|
|
610
797
|
model: requestedModel,
|
|
@@ -706,7 +893,11 @@ export async function handleResponses(
|
|
|
706
893
|
}
|
|
707
894
|
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
708
895
|
|
|
709
|
-
|
|
896
|
+
if (isThreadSpawnRequest(req.headers)) {
|
|
897
|
+
await maybePrimeSubagentQuota(config);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
let route: RouteResult;
|
|
710
901
|
try {
|
|
711
902
|
route = routeModel(config, parsed.modelId);
|
|
712
903
|
} catch (err) {
|
|
@@ -716,167 +907,72 @@ export async function handleResponses(
|
|
|
716
907
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
717
908
|
}
|
|
718
909
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
910
|
+
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
911
|
+
let selectedForwardHeaders = req.headers;
|
|
912
|
+
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
913
|
+
let subagentQuotaFailureModel = parsed.modelId;
|
|
914
|
+
|
|
915
|
+
// Subagent fallback must settle the final model/provider BEFORE route-dependent
|
|
916
|
+
// normalization (virtual models, effort caps, service tier, wire protocol).
|
|
917
|
+
// Preview the preferred Codex account without acquiring a probe lease or refreshing
|
|
918
|
+
// tokens — auth is resolved only after the final route is selected.
|
|
919
|
+
if (isThreadSpawnRequest(req.headers) && !options.comboAttempt) {
|
|
920
|
+
const threadId = req.headers.get("x-codex-parent-thread-id");
|
|
921
|
+
const previewAccountId = previewCodexAccountForRequest(threadId, config);
|
|
922
|
+
subagentFallbackAccountId = previewAccountId ?? config.activeCodexAccountId ?? null;
|
|
923
|
+
const fallback = applySubagentModelFallback(
|
|
924
|
+
parsed,
|
|
925
|
+
req.headers,
|
|
926
|
+
config,
|
|
927
|
+
previewAccountId,
|
|
928
|
+
Date.now(),
|
|
929
|
+
unreadableEncryptedAgentTask,
|
|
930
|
+
);
|
|
931
|
+
if (fallback) {
|
|
932
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackFrom = fallback.from;
|
|
933
|
+
(logCtx as unknown as Record<string, unknown>).subagentModelFallbackTo = fallback.to;
|
|
934
|
+
if (isInjectionDebugEnabled()) {
|
|
935
|
+
injectionDebugLog(`[opencodex] subagent model fallback ${fallback.from} -> ${fallback.to}`);
|
|
936
|
+
}
|
|
732
937
|
}
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
// Settle the wire once, right after the native model id is known, so logging,
|
|
736
|
-
// fast-mode injection, auth, and sidecar decisions all read the adapter this
|
|
737
|
-
// request will actually use rather than the provider-wide default (#404).
|
|
738
|
-
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider);
|
|
739
|
-
logCtx.model = route.modelId;
|
|
740
|
-
logCtx.provider = route.providerName;
|
|
741
|
-
logCtx.providerAdapter = route.provider.adapter;
|
|
742
|
-
|
|
743
|
-
// Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
|
|
744
|
-
// Must run before effort caps/native clamps so the base model gets correct limits.
|
|
745
|
-
applyOpenAiVirtualModel(parsed, route, logCtx);
|
|
938
|
+
subagentQuotaFailureModel = fallback?.to ?? parsed.modelId;
|
|
746
939
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
940
|
+
if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
|
|
941
|
+
try {
|
|
942
|
+
route = routeModel(config, fallback.to);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
if (err instanceof NoAvailableComboTargetsError) {
|
|
945
|
+
return comboUnavailableResponse(err.message);
|
|
946
|
+
}
|
|
947
|
+
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
948
|
+
}
|
|
754
949
|
}
|
|
755
|
-
parsed.options.serviceTier = tier;
|
|
756
950
|
}
|
|
757
951
|
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// The surface is judged from the request's own tool list. Runs BEFORE the
|
|
763
|
-
// mock-max clamp below so the synthetic top tier (ultra arrives as max on the
|
|
764
|
-
// codex wire) is still visible. Both request shapes are rewritten.
|
|
765
|
-
{
|
|
766
|
-
const guidance = await multiAgentGuidanceText(parsed, {
|
|
767
|
-
multiAgentGuidanceEnabled: config.multiAgentGuidanceEnabled,
|
|
768
|
-
injectionModel: config.injectionModel,
|
|
769
|
-
injectionEffort: config.injectionEffort,
|
|
770
|
-
subagentModels: config.subagentModels,
|
|
771
|
-
injectionPrompt: config.injectionPrompt,
|
|
772
|
-
});
|
|
773
|
-
if (guidance) {
|
|
774
|
-
injectDeveloperMessage(parsed, guidance);
|
|
775
|
-
if (isInjectionDebugEnabled()) injectionDebugLog(`[opencodex] ${route.modelId}: multi-agent guidance injected (surface=${collabSurface(parsed)}, guidanceEnabled=${multiAgentGuidanceEnabled(config)}, ${guidance.length} chars)`);
|
|
776
|
-
} else if (isInjectionDebugEnabled() && collabSurface(parsed) !== null) {
|
|
777
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: collab surface=${collabSurface(parsed)}, guidance silent (effort=${parsed.options.reasoning ?? "unset"}, injectionModel=${config.injectionModel ?? "unset"})`);
|
|
778
|
-
}
|
|
952
|
+
// Encrypted child tasks may only reach the canonical native backend. This check
|
|
953
|
+
// runs against the FINAL route so native-only fallback can rescue a routed primary.
|
|
954
|
+
if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) {
|
|
955
|
+
return unreadableEncryptedAgentTaskResponse();
|
|
779
956
|
}
|
|
780
957
|
|
|
781
|
-
|
|
782
|
-
// injection above — spawn-arg prompting cannot stop codex-rs from inheriting the parent's
|
|
783
|
-
// ultra-tier default on bare spawns (see src/server/effort-policy.ts). Runs BEFORE the
|
|
784
|
-
// mock-max clamp so a capped effort is what nativeness clamping then validates; rewrites
|
|
785
|
-
// both request shapes (same dual-write contract as the clamp below).
|
|
786
|
-
// GATE: v2 feature only (effortCapAppliesTo) — v2-surface main turns plus header-marked
|
|
787
|
-
// child turns admitted regardless of tool surface (depth-limited leaves carry no collab
|
|
788
|
-
// tools while shallower children do, so tool sniffing alone would cap siblings
|
|
789
|
-
// inconsistently); multiAgentMode "v1" disables caps entirely; compaction turns bypass
|
|
790
|
-
// caps so routed compaction matches native /v1/responses/compact (which never enters
|
|
791
|
-
// handleResponses).
|
|
792
|
-
{
|
|
793
|
-
const { applyEffortCap, effortCapAppliesTo, supportedLadderFor } = await import("../effort-policy");
|
|
794
|
-
const surface = collabSurface(parsed);
|
|
795
|
-
if (effortCapAppliesTo(surface, req.headers, config, parsed._compactionRequest === true)) {
|
|
796
|
-
const capped = applyEffortCap(parsed, req.headers, config, supportedLadderFor(route));
|
|
797
|
-
if (capped) {
|
|
798
|
-
logCtx.requestedEffort = `${capped.from}->${capped.to}`;
|
|
799
|
-
if (isInjectionDebugEnabled()) {
|
|
800
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap applied (${capped.from} -> ${capped.to}, ${capped.subagent ? "sub-agent" : "main"} turn)`);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
} else if (isInjectionDebugEnabled() && (config.effortCap || config.subagentEffortCap)) {
|
|
804
|
-
injectionDebugLog(`[opencodex] ${route.modelId}: effort cap skipped (surface=${surface ?? "none"}, v2 feature only)`);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
958
|
+
await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx });
|
|
807
959
|
|
|
808
|
-
// Mock-max clamp: native models whose real ladder stops below max (gpt-5.5/5.4/…)
|
|
809
|
-
// receive `max` when the user picks Ultra (codex converts ultra->max client-side).
|
|
810
|
-
// Clamp to the model's highest real effort BEFORE any adapter — the ChatGPT
|
|
811
|
-
// passthrough serializes _rawBody verbatim, so both shapes must be rewritten.
|
|
812
|
-
// GUARD: judge nativeness by BOTH the originally requested id (logCtx.requestedModel)
|
|
813
|
-
// and the resolved provider identity. Routing strips the "<provider>/" namespace, and
|
|
814
|
-
// some third-party providers expose bare `defaultModel` selectors, so route.modelId
|
|
815
|
-
// alone can make a routed model masquerade as an off-snapshot native. Only the
|
|
816
|
-
// canonical built-in ChatGPT forward provider should receive the native clamp.
|
|
817
960
|
{
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
: null;
|
|
823
|
-
if (clamped) {
|
|
824
|
-
parsed.options.reasoning = clamped;
|
|
825
|
-
const raw = parsed._rawBody as { reasoning?: { effort?: string } } | undefined;
|
|
826
|
-
if (raw?.reasoning && typeof raw.reasoning === "object") raw.reasoning.effort = clamped;
|
|
827
|
-
logCtx.requestedEffort = `${logCtx.requestedEffort ?? "max"}->${clamped}`;
|
|
828
|
-
}
|
|
961
|
+
const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
|
|
962
|
+
if (!finalAuth.ok) return finalAuth.response;
|
|
963
|
+
authCtx = finalAuth.authCtx;
|
|
964
|
+
selectedForwardHeaders = finalAuth.headers;
|
|
829
965
|
}
|
|
830
|
-
logCtx.modelSupportsServiceTier = catalogModelSupportsServiceTier(
|
|
831
|
-
route.modelId,
|
|
832
|
-
logCtx.requestedServiceTier ?? logCtx.configuredServiceTier,
|
|
833
|
-
);
|
|
834
966
|
|
|
835
|
-
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
836
|
-
let selectedForwardHeaders: Headers;
|
|
837
|
-
try {
|
|
838
|
-
if (route.codexAccountMode === "direct") validateForwardAdmissionCredential(req.headers, config);
|
|
839
|
-
if (route.codexAccountMode) {
|
|
840
|
-
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
|
|
841
|
-
options.onCodexAuthContextResolved?.(authCtx);
|
|
842
|
-
} else {
|
|
843
|
-
options.onCodexAuthContextResolved?.(undefined);
|
|
844
|
-
}
|
|
845
|
-
selectedForwardHeaders = headersForCodexAuthContext(req.headers, authCtx);
|
|
846
|
-
} catch (err) {
|
|
847
|
-
if (err instanceof CodexAccountCooldownError) {
|
|
848
|
-
return formatErrorResponse(429, "rate_limit_error", "Selected Codex account is cooling down");
|
|
849
|
-
}
|
|
850
|
-
if (err instanceof CodexThreadAffinityExpiredError) {
|
|
851
|
-
return formatErrorResponse(409, "invalid_request_error", "Codex thread account affinity expired; start a new session");
|
|
852
|
-
}
|
|
853
|
-
if (err instanceof CodexAuthContextError) {
|
|
854
|
-
const safeAccountLabel = formatCodexProviderForLog(route.providerName, err.accountId, config);
|
|
855
|
-
console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
|
|
856
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
857
|
-
}
|
|
858
|
-
if (err instanceof CodexPoolAuthenticationError) {
|
|
859
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
860
|
-
}
|
|
861
|
-
if (err instanceof CodexDirectAuthenticationError) {
|
|
862
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
863
|
-
}
|
|
864
|
-
if (err instanceof ForwardAdmissionCredentialError) {
|
|
865
|
-
return formatErrorResponse(401, "authentication_error", err.message);
|
|
866
|
-
}
|
|
867
|
-
throw err;
|
|
868
|
-
}
|
|
869
|
-
if (!isCodexAuthContextUsable(authCtx, config)) {
|
|
870
|
-
// Nothing reaches upstream on this path, so give the probe back.
|
|
871
|
-
releaseCodexAuthContextProbeLease(authCtx);
|
|
872
|
-
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
873
|
-
}
|
|
874
967
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
|
|
875
968
|
logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
876
969
|
// Prefer Codex pool account as the Cursor thread namespace when present. Cursor routes without
|
|
877
970
|
// codexAccountMode still get a credential-derived scope inside the Cursor adapter.
|
|
878
971
|
const identityScope = codexLogAccountId(authCtx);
|
|
879
972
|
if (identityScope) parsed._cursorIdentityScope = identityScope;
|
|
973
|
+
subagentFallbackAccountId = authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
974
|
+
? authCtx.accountId
|
|
975
|
+
: config.activeCodexAccountId ?? null;
|
|
880
976
|
|
|
881
977
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
882
978
|
// existing openai-chat / anthropic adapters authenticate with no change.
|
|
@@ -1178,6 +1274,22 @@ export async function handleResponses(
|
|
|
1178
1274
|
if (terminalBodyWillRecord) {
|
|
1179
1275
|
options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
|
|
1180
1276
|
terminalRecorder(status, httpStatusOverride);
|
|
1277
|
+
if (status === "failed") {
|
|
1278
|
+
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|
|
1279
|
+
|| logCtx.terminalHttpStatus === 429
|
|
1280
|
+
|| logCtx.terminalHttpStatus === 402
|
|
1281
|
+
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
1282
|
+
: undefined;
|
|
1283
|
+
if (quotaFailureMessage !== undefined) {
|
|
1284
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
1285
|
+
req.headers,
|
|
1286
|
+
subagentQuotaFailureModel,
|
|
1287
|
+
quotaFailureMessage,
|
|
1288
|
+
config,
|
|
1289
|
+
subagentFallbackAccountId,
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1181
1293
|
options.onNativePassthroughTerminal?.(status);
|
|
1182
1294
|
});
|
|
1183
1295
|
} else {
|
|
@@ -1194,6 +1306,23 @@ export async function handleResponses(
|
|
|
1194
1306
|
}
|
|
1195
1307
|
}
|
|
1196
1308
|
|
|
1309
|
+
// Non-2xx passthrough failures must never reach Codex as an empty body —
|
|
1310
|
+
// Codex renders that as the opaque "Unknown error" (#452). Combo attempts
|
|
1311
|
+
// keep their typed failure envelope. Non-empty bodies are relayed verbatim
|
|
1312
|
+
// (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
|
|
1313
|
+
if (!upstreamResponse.ok) {
|
|
1314
|
+
if (options.comboAttempt) {
|
|
1315
|
+
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
|
|
1316
|
+
options.onConsumedComboFailure?.(failure);
|
|
1317
|
+
return failure.response;
|
|
1318
|
+
}
|
|
1319
|
+
const errorText = await upstreamResponse.text().catch(() => "");
|
|
1320
|
+
return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
|
|
1321
|
+
statusText: upstreamResponse.statusText,
|
|
1322
|
+
headers,
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1197
1326
|
// Bun#32111 workaround: passthrough SSE uses tee()+native relay to avoid the
|
|
1198
1327
|
// async-pull segfault on Windows. Branch[0] goes directly to the Response (Bun
|
|
1199
1328
|
// native relay, never enters JS Sink.write); branch[1] is consumed in the
|
|
@@ -1204,7 +1333,7 @@ export async function handleResponses(
|
|
|
1204
1333
|
// bounded reader with inline inspection (see src/server/relay-eager.ts and
|
|
1205
1334
|
// devlog/_plan/260723_win_mem_safestream/020). Default on the bundled
|
|
1206
1335
|
// known-bad runtime remains the tee path below.
|
|
1207
|
-
if (
|
|
1336
|
+
if (isEventStream && upstreamResponse.body) {
|
|
1208
1337
|
const repairConfig = route.provider.responsesItemIdRepair;
|
|
1209
1338
|
const winNoRepair = process.platform === "win32" && !hasResponsesItemIdRepair(repairConfig);
|
|
1210
1339
|
const eagerDecision = winNoRepair ? decideEagerRelay(config.streamMode ?? "auto") : null;
|
|
@@ -1215,6 +1344,22 @@ export async function handleResponses(
|
|
|
1215
1344
|
const reportNativeTerminal = recordTerminalOutcomes
|
|
1216
1345
|
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
1217
1346
|
terminalRecorder?.(status, httpStatusOverride);
|
|
1347
|
+
if (status === "failed") {
|
|
1348
|
+
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|
|
1349
|
+
|| logCtx.terminalHttpStatus === 429
|
|
1350
|
+
|| logCtx.terminalHttpStatus === 402
|
|
1351
|
+
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
1352
|
+
: undefined;
|
|
1353
|
+
if (quotaFailureMessage !== undefined) {
|
|
1354
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
1355
|
+
req.headers,
|
|
1356
|
+
subagentQuotaFailureModel,
|
|
1357
|
+
quotaFailureMessage,
|
|
1358
|
+
config,
|
|
1359
|
+
subagentFallbackAccountId,
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1218
1363
|
options.onNativePassthroughTerminal?.(status);
|
|
1219
1364
|
}
|
|
1220
1365
|
: undefined;
|
|
@@ -1259,6 +1404,22 @@ export async function handleResponses(
|
|
|
1259
1404
|
// client-cancel (no terminal seen) is finalized separately via consumeForInspection's onCancel.
|
|
1260
1405
|
const reportNativeTerminal = (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
1261
1406
|
terminalRecorder?.(status, httpStatusOverride);
|
|
1407
|
+
if (status === "failed") {
|
|
1408
|
+
const quotaFailureMessage = httpStatusOverride === 429 || httpStatusOverride === 402
|
|
1409
|
+
|| logCtx.terminalHttpStatus === 429
|
|
1410
|
+
|| logCtx.terminalHttpStatus === 402
|
|
1411
|
+
? (httpStatusOverride ?? logCtx.terminalHttpStatus)
|
|
1412
|
+
: undefined;
|
|
1413
|
+
if (quotaFailureMessage !== undefined) {
|
|
1414
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
1415
|
+
req.headers,
|
|
1416
|
+
subagentQuotaFailureModel,
|
|
1417
|
+
quotaFailureMessage,
|
|
1418
|
+
config,
|
|
1419
|
+
subagentFallbackAccountId,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1262
1423
|
options.onNativePassthroughTerminal?.(status);
|
|
1263
1424
|
};
|
|
1264
1425
|
consumeForInspection(
|
|
@@ -1297,14 +1458,9 @@ export async function handleResponses(
|
|
|
1297
1458
|
}));
|
|
1298
1459
|
}
|
|
1299
1460
|
if (headers.get("content-type")?.toLowerCase().includes("application/json")) {
|
|
1300
|
-
if (!upstreamResponse.ok && options.comboAttempt) {
|
|
1301
|
-
const failure = await consumeComboFailure(upstreamResponse, options.abortSignal);
|
|
1302
|
-
options.onConsumedComboFailure?.(failure);
|
|
1303
|
-
return failure.response;
|
|
1304
|
-
}
|
|
1305
1461
|
const text = await upstreamResponse.text();
|
|
1306
1462
|
inspectResponseLogJson(logCtx, text);
|
|
1307
|
-
if (
|
|
1463
|
+
if (rememberPassthroughResponse) {
|
|
1308
1464
|
try {
|
|
1309
1465
|
rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
|
|
1310
1466
|
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
@@ -1374,6 +1530,15 @@ export async function handleResponses(
|
|
|
1374
1530
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1375
1531
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1376
1532
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1533
|
+
onUsage: usage => {
|
|
1534
|
+
// Raw adapter usage, pre wire-normalization: the bridged SSE now always carries
|
|
1535
|
+
// zero-default detail objects, so provenance must come from here (cache_detail_missing).
|
|
1536
|
+
logCtx.usageFromBridge = true;
|
|
1537
|
+
if (usage) {
|
|
1538
|
+
logCtx.usage = usage;
|
|
1539
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
1540
|
+
}
|
|
1541
|
+
},
|
|
1377
1542
|
...(routedCompaction ? {} : {
|
|
1378
1543
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
|
|
1379
1544
|
rememberResponseState(
|
|
@@ -1411,6 +1576,13 @@ export async function handleResponses(
|
|
|
1411
1576
|
toolSearchToolNames,
|
|
1412
1577
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1413
1578
|
onProviderState: state => { providerState = state; },
|
|
1579
|
+
onUsage: usage => {
|
|
1580
|
+
logCtx.usageFromBridge = true;
|
|
1581
|
+
if (usage) {
|
|
1582
|
+
logCtx.usage = usage;
|
|
1583
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
1584
|
+
}
|
|
1585
|
+
},
|
|
1414
1586
|
});
|
|
1415
1587
|
if (!routedCompaction) {
|
|
1416
1588
|
rememberResponseState(
|
|
@@ -1442,6 +1614,13 @@ export async function handleResponses(
|
|
|
1442
1614
|
forceEmptyResponseId: true,
|
|
1443
1615
|
abortSignal: options.abortSignal,
|
|
1444
1616
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1617
|
+
onUsage: usage => {
|
|
1618
|
+
logCtx.usageFromBridge = true;
|
|
1619
|
+
if (usage) {
|
|
1620
|
+
logCtx.usage = usage;
|
|
1621
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1445
1624
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
1446
1625
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
1447
1626
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
@@ -1640,6 +1819,15 @@ export async function handleResponses(
|
|
|
1640
1819
|
}
|
|
1641
1820
|
const errorText = await upstreamResponse.text().catch(() => "unknown error");
|
|
1642
1821
|
cleanupUpstreamAbort();
|
|
1822
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
1823
|
+
req.headers,
|
|
1824
|
+
subagentQuotaFailureModel,
|
|
1825
|
+
upstreamResponse.status === 429 || upstreamResponse.status === 402
|
|
1826
|
+
? upstreamResponse.status
|
|
1827
|
+
: `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`,
|
|
1828
|
+
config,
|
|
1829
|
+
subagentFallbackAccountId,
|
|
1830
|
+
);
|
|
1643
1831
|
// Upstreams occasionally echo request details in error bodies — scrub token-shaped
|
|
1644
1832
|
// material before it reaches the client-facing error surface.
|
|
1645
1833
|
return formatErrorResponse(upstreamResponse.status, "upstream_error", `Provider error ${upstreamResponse.status}: ${redactSecretString(errorText.slice(0, 500))}`);
|
|
@@ -1788,6 +1976,14 @@ export async function handleResponses(
|
|
|
1788
1976
|
hideThinkingSummary: parsed.options.hideThinkingSummary,
|
|
1789
1977
|
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
1790
1978
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1979
|
+
onUsage: usage => {
|
|
1980
|
+
// Raw adapter usage, pre wire-normalization (see the runTurn branch above).
|
|
1981
|
+
logCtx.usageFromBridge = true;
|
|
1982
|
+
if (usage) {
|
|
1983
|
+
logCtx.usage = usage;
|
|
1984
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1791
1987
|
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
1792
1988
|
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
1793
1989
|
// giant stale chain Codex just replaced.
|
|
@@ -1837,6 +2033,13 @@ export async function handleResponses(
|
|
|
1837
2033
|
toolSearchToolNames,
|
|
1838
2034
|
...(routedCompaction ? { compaction: true } : {}),
|
|
1839
2035
|
onProviderState: state => { providerState = state; },
|
|
2036
|
+
onUsage: usage => {
|
|
2037
|
+
logCtx.usageFromBridge = true;
|
|
2038
|
+
if (usage) {
|
|
2039
|
+
logCtx.usage = usage;
|
|
2040
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
1840
2043
|
});
|
|
1841
2044
|
// See the streaming branch: compaction turns skip the continuation cache.
|
|
1842
2045
|
if (!routedCompaction) {
|