@bitkyc08/opencodex 2.81.0 → 2.82.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
|
@@ -36,11 +36,12 @@ import {
|
|
|
36
36
|
buildAnthropicMessagesPassthroughRequest,
|
|
37
37
|
type AnthropicMessagesPassthroughRequest,
|
|
38
38
|
} from "../adapters/anthropic/passthrough";
|
|
39
|
-
import { resolveInboundModel } from "../claude/inbound";
|
|
40
|
-
import { anthropicErrorBody, anthropicErrorResponse, collectAnthropicMessage } from "../claude/outbound";
|
|
39
|
+
import { effortFromOutputConfig, resolveInboundModel } from "../claude/inbound";
|
|
40
|
+
import { anthropicErrorBody, anthropicErrorResponse, claudeOverflowSsePayload, claudePromptTooLongMessage, collectAnthropicMessage, isContextOverflowText, isThroughputLimitText } from "../claude/outbound";
|
|
41
41
|
import type { AdmissionLease } from "../lib/admission";
|
|
42
42
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
43
43
|
import { classifyError } from "../lib/errors";
|
|
44
|
+
import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
|
|
44
45
|
import { redactSecretString } from "../lib/redact";
|
|
45
46
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
46
47
|
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
@@ -107,6 +108,7 @@ import {
|
|
|
107
108
|
import {
|
|
108
109
|
noteProviderAttemptSend,
|
|
109
110
|
recordAttemptCredentialSource,
|
|
111
|
+
recordAttemptRequestedEffort,
|
|
110
112
|
recordFirstOutput,
|
|
111
113
|
recordKeyAttemptFailure,
|
|
112
114
|
recordKeyWireAttemptUsage,
|
|
@@ -119,6 +121,7 @@ import { workflowRefusalResponse } from "./workflow-refusal";
|
|
|
119
121
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
120
122
|
import { admissionModelDeniedResponse, type AdmissionModelScope } from "./admission-model-scope";
|
|
121
123
|
import { nativeMessagesToolScopeDenial } from "./messages-native-scope";
|
|
124
|
+
import { retainUpstreamMessagesRequestId } from "./messages-response-headers";
|
|
122
125
|
|
|
123
126
|
export {
|
|
124
127
|
isNativeMessagesRouteEligible,
|
|
@@ -317,6 +320,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
317
320
|
logCtx.requestedModel = requestedModel;
|
|
318
321
|
if (route.routeReason === "model-alias" || route.modelId !== requestedModel) logCtx.requestedAlias = requestedModel;
|
|
319
322
|
logCtx.requestedServiceTier = typeof body.service_tier === "string" ? body.service_tier : undefined;
|
|
323
|
+
if (logCtx.requestedEffort === undefined) {
|
|
324
|
+
const effort = effortFromOutputConfig(body.output_config);
|
|
325
|
+
const thinking = isRec(body.thinking) ? body.thinking : undefined;
|
|
326
|
+
const budget = thinking?.budget_tokens;
|
|
327
|
+
if (effort !== undefined) logCtx.requestedEffort = effort;
|
|
328
|
+
else if (thinking?.type === "disabled") logCtx.requestedEffort = "none";
|
|
329
|
+
else if (thinking?.type === "enabled" && typeof budget === "number"
|
|
330
|
+
&& Number.isSafeInteger(budget) && budget > 0) logCtx.requestedEffort = `budget:${budget}`;
|
|
331
|
+
}
|
|
320
332
|
// Reserve spend the way native Chat does: an input estimate that never enters usage, and the
|
|
321
333
|
// caller's own output ceiling.
|
|
322
334
|
if (logCtx.usageLogInputTokens === undefined) {
|
|
@@ -332,6 +344,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
332
344
|
adapter: "anthropic",
|
|
333
345
|
});
|
|
334
346
|
attemptHandle.seal(logCtx.accountLogLabel);
|
|
347
|
+
recordAttemptRequestedEffort(logCtx);
|
|
335
348
|
const { attempt } = attemptHandle;
|
|
336
349
|
const finalLog = createFinalRequestLog(logIds, logCtx);
|
|
337
350
|
const finishLog: FinishLog = (status, message, meta = { closeReason: "non_stream" }) => {
|
|
@@ -500,7 +513,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
500
513
|
&& nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
|
|
501
514
|
};
|
|
502
515
|
|
|
503
|
-
const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
516
|
+
const send = async (recovery?: "rate-limit-429" | "oauth-401" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
|
|
504
517
|
const remaining = remainingTransientSends();
|
|
505
518
|
if (requestTransientPolicy && remaining <= 0) {
|
|
506
519
|
throw new Error("native Messages transient send budget exhausted before recovery dispatch");
|
|
@@ -672,7 +685,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
672
685
|
const oauthRetryKey = {};
|
|
673
686
|
const triedAccountIds = new Set<string>();
|
|
674
687
|
let oauthFailovers = 0;
|
|
675
|
-
while (oauthBinding && (response.status === 429 || response.status === 403)) {
|
|
688
|
+
while (oauthBinding && (response.status === 429 || response.status === 403 || response.status === 401)) {
|
|
676
689
|
const sendingBinding = oauthBinding;
|
|
677
690
|
const expectedRecoverySelection = sendingBinding.selection;
|
|
678
691
|
triedAccountIds.add(sendingBinding.snapshot.accountId);
|
|
@@ -690,7 +703,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
690
703
|
canRetry: transientSendAvailable() && oauthFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
|
|
691
704
|
});
|
|
692
705
|
if (!nextAccountId) break;
|
|
693
|
-
const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
|
|
706
|
+
const recovery = response.status === 401 ? "oauth-401" : response.status === 403 ? "oauth-account-403" : "rate-limit-429";
|
|
694
707
|
discard(response);
|
|
695
708
|
oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget,
|
|
696
709
|
sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
|
|
@@ -739,7 +752,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
739
752
|
upstream.abort();
|
|
740
753
|
return fail(499, "Client cancelled request", "api_error");
|
|
741
754
|
}
|
|
742
|
-
return nativeMessagesErrorResponse(response, bodyText, finishLog);
|
|
755
|
+
return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog), response.headers);
|
|
743
756
|
}
|
|
744
757
|
|
|
745
758
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -764,10 +777,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
764
777
|
releaseStreamTurn();
|
|
765
778
|
}
|
|
766
779
|
}, bodyGuard);
|
|
767
|
-
|
|
780
|
+
// A configured Messages provider words an oversized-input refusal its own way; Claude Code
|
|
781
|
+
// compacts only on Anthropic's wording (devlog/_plan/261009_claude_1m_default/010). The
|
|
782
|
+
// rewrite buffers whole frames, so it sits after the tap: stall detection keeps timing raw
|
|
783
|
+
// upstream bytes. Real Anthropic pools refuse pre-stream in that wording and keep one relay.
|
|
784
|
+
const clientStream = nativeInstance ? relayed : relaySseWithPayloadRewrite(relayed, claudeOverflowSsePayload, translatorBudget);
|
|
785
|
+
return retainUpstreamMessagesRequestId(new Response(clientStream, {
|
|
768
786
|
status: 200,
|
|
769
787
|
headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
|
770
|
-
});
|
|
788
|
+
}), response.headers);
|
|
771
789
|
}
|
|
772
790
|
// A non-streaming caller whose upstream streamed anyway: fold the stream into one message.
|
|
773
791
|
const tapState: { closeReason?: FinalRequestLogMeta["closeReason"]; meta?: FinalRequestLogMeta } = {};
|
|
@@ -789,10 +807,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
789
807
|
// as the streaming lane's row; a plain upstream error event keeps the non_stream row.
|
|
790
808
|
const tapMeta = tapState.meta;
|
|
791
809
|
if (tapMeta && (tapMeta.terminalStatus || tapMeta.closeReason !== "terminal")) finishLog(502, text, tapMeta);
|
|
810
|
+
if (isNativeOverflowError(error, text)) return fail(400, claudePromptTooLongMessage(text), "invalid_request_error");
|
|
792
811
|
return fail(502, text, "api_error");
|
|
793
812
|
}
|
|
794
813
|
finishLog(200);
|
|
795
|
-
return Response.json(message);
|
|
814
|
+
return retainUpstreamMessagesRequestId(Response.json(message), response.headers);
|
|
796
815
|
} catch (error) {
|
|
797
816
|
cleanupAbort();
|
|
798
817
|
upstream.abort();
|
|
@@ -850,12 +869,19 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
850
869
|
const serialized = JSON.stringify(message);
|
|
851
870
|
finishLog(200);
|
|
852
871
|
if (requestedStream) {
|
|
853
|
-
return new Response(messageAsSse(message), {
|
|
872
|
+
return retainUpstreamMessagesRequestId(new Response(messageAsSse(message), {
|
|
854
873
|
status: 200,
|
|
855
874
|
headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
|
|
856
|
-
});
|
|
875
|
+
}), response.headers);
|
|
857
876
|
}
|
|
858
|
-
return new Response(serialized, { status: 200, headers: { "Content-Type": "application/json" } });
|
|
877
|
+
return retainUpstreamMessagesRequestId(new Response(serialized, { status: 200, headers: { "Content-Type": "application/json" } }), response.headers);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** A folded stream error that refuses an oversized input (same gate as the SSE rewrite). */
|
|
881
|
+
function isNativeOverflowError(error: Rec, message: string): boolean {
|
|
882
|
+
const sized = error.type === "invalid_request_error" || error.type === "request_too_large";
|
|
883
|
+
return sized && !isThroughputLimitText(message)
|
|
884
|
+
&& (error.code === "context_length_exceeded" || isContextOverflowText(message));
|
|
859
885
|
}
|
|
860
886
|
|
|
861
887
|
/**
|
|
@@ -865,11 +891,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
865
891
|
*/
|
|
866
892
|
function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
|
|
867
893
|
let upstreamType: string | undefined;
|
|
894
|
+
let upstreamCode: string | undefined;
|
|
868
895
|
let upstreamMessage: string | undefined;
|
|
869
896
|
try {
|
|
870
897
|
const parsed = JSON.parse(bodyText) as Rec;
|
|
871
898
|
const details = isRec(parsed.error) ? parsed.error : parsed;
|
|
872
899
|
if (typeof details.type === "string") upstreamType = details.type;
|
|
900
|
+
if (typeof details.code === "string") upstreamCode = details.code;
|
|
873
901
|
if (typeof details.message === "string" && details.message.trim()) {
|
|
874
902
|
upstreamMessage = redactSecretString(details.message.trim());
|
|
875
903
|
}
|
|
@@ -898,9 +926,17 @@ function nativeMessagesErrorResponse(response: Response, bodyText: string, finis
|
|
|
898
926
|
if (retryAfter) headers.set("Retry-After", retryAfter);
|
|
899
927
|
else if (transient) headers.set("Retry-After", "2");
|
|
900
928
|
if (replayRefusal) applyReplayRefusalClientHeaders(headers);
|
|
929
|
+
// The native envelope keeps its shape (no added code); only an overflow's wording changes, so
|
|
930
|
+
// Claude Code recognizes it and compacts (010).
|
|
931
|
+
// Only a 400/413 refusal: the shared classifier also files a 429 "too many tokens per minute"
|
|
932
|
+
// under context_length_exceeded, and that one must stay a rate limit.
|
|
933
|
+
const overflow = !replayRefusal && (response.status === 400 || response.status === 413)
|
|
934
|
+
&& !isThroughputLimitText(safeMessage)
|
|
935
|
+
&& (classified.code === "context_length_exceeded" || upstreamCode === "context_length_exceeded"
|
|
936
|
+
|| isContextOverflowText(safeMessage));
|
|
901
937
|
const out = new Response(JSON.stringify(anthropicErrorBody(
|
|
902
938
|
status,
|
|
903
|
-
safeMessage,
|
|
939
|
+
overflow ? claudePromptTooLongMessage(safeMessage) : safeMessage,
|
|
904
940
|
transient ? "overloaded_error" : upstreamType,
|
|
905
941
|
replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : undefined,
|
|
906
942
|
)), { status, headers });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Only an opaque Anthropic request id is carried; arbitrary upstream headers stay private. */
|
|
2
|
+
export function upstreamMessagesRequestIdHeaders(upstream: Headers): Record<string, string> {
|
|
3
|
+
const requestId = upstream.get("request-id");
|
|
4
|
+
return requestId && /^req_[A-Za-z0-9_-]{1,128}$/.test(requestId)
|
|
5
|
+
? { "request-id": requestId }
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The response is newly constructed by the Messages handler; preserve its body and markers. */
|
|
10
|
+
export function retainUpstreamMessagesRequestId(response: Response, upstream: Headers): Response {
|
|
11
|
+
const requestId = upstreamMessagesRequestIdHeaders(upstream)["request-id"];
|
|
12
|
+
if (requestId) response.headers.set("request-id", requestId);
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
@@ -37,6 +37,47 @@ export function normalizeLogConversationId(raw: string | undefined | null): stri
|
|
|
37
37
|
return createHash("sha256").update(trimmed).digest("hex").slice(0, LOG_CONVERSATION_ID_LEN);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const CODEX_THREAD_LINK_PREFIX = "codex://threads/";
|
|
41
|
+
/** Real thread links stay well under this bound; larger pastes skip unwrapping so per-row matching stays cheap. */
|
|
42
|
+
const LOG_CONVERSATION_LINK_MAX = 512;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Unwrap a pasted `codex://threads/<id>` deep link to the bare thread id. Codex's
|
|
46
|
+
* "copy session link" clipboard carries that URI form while clients send the bare id
|
|
47
|
+
* on the wire, so Logs search accepts either paste without changing what is persisted.
|
|
48
|
+
* Query/fragment metadata (e.g. `?hostId=…`) belongs to the link, not the id, and is
|
|
49
|
+
* dropped. The scan is a bounded linear walk — no ambiguous regex — because this runs
|
|
50
|
+
* per log row on every filter evaluation.
|
|
51
|
+
*/
|
|
52
|
+
export function unwrapLogConversationQuery(query: string): string {
|
|
53
|
+
const trimmed = query.trim();
|
|
54
|
+
if (
|
|
55
|
+
trimmed.length > LOG_CONVERSATION_LINK_MAX ||
|
|
56
|
+
trimmed.length <= CODEX_THREAD_LINK_PREFIX.length ||
|
|
57
|
+
!trimmed.toLowerCase().startsWith(CODEX_THREAD_LINK_PREFIX)
|
|
58
|
+
) {
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
const rest = trimmed.slice(CODEX_THREAD_LINK_PREFIX.length);
|
|
62
|
+
const metaIndex = rest.search(/[?#]/);
|
|
63
|
+
const segment = (metaIndex === -1 ? rest : rest.slice(0, metaIndex)).replace(/\/+$/, "");
|
|
64
|
+
// A thread id is a single path segment; deeper paths or embedded whitespace keep the
|
|
65
|
+
// whole paste as the query, so foreign `codex://` URIs still fail to match.
|
|
66
|
+
return segment !== "" && !/[/\s]/.test(segment) ? segment : trimmed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The candidate ids a conversation query may legitimately match. Unwrapping a pasted
|
|
71
|
+
* `codex://threads/<id>` link must add a match path, not replace one: a client can
|
|
72
|
+
* literally send a `codex://threads/…` session id, which is hashed whole for storage,
|
|
73
|
+
* so the untouched paste stays a candidate too.
|
|
74
|
+
*/
|
|
75
|
+
export function logConversationQueryCandidates(query: string): string[] {
|
|
76
|
+
const trimmed = query.trim();
|
|
77
|
+
const unwrapped = unwrapLogConversationQuery(trimmed);
|
|
78
|
+
return unwrapped === trimmed ? [unwrapped] : [unwrapped, trimmed];
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
/**
|
|
41
82
|
* Filter match: accept either the persisted digest or the original preimage
|
|
42
83
|
* (so pasting from Logs detail or the client-facing session id both work).
|
|
@@ -46,11 +87,13 @@ export function matchesLogConversationId(
|
|
|
46
87
|
query: string | undefined | null,
|
|
47
88
|
): boolean {
|
|
48
89
|
if (!stored) return false;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
for (const candidate of logConversationQueryCandidates(typeof query === "string" ? query : "")) {
|
|
91
|
+
if (!candidate) continue;
|
|
92
|
+
if (stored === candidate) return true;
|
|
93
|
+
const hashed = normalizeLogConversationId(candidate);
|
|
94
|
+
if (hashed !== undefined && stored === hashed) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
54
97
|
}
|
|
55
98
|
|
|
56
99
|
/**
|
|
@@ -232,6 +232,8 @@ export interface RequestLogContext {
|
|
|
232
232
|
* message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
|
|
233
233
|
* request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
|
|
234
234
|
upstreamError?: string;
|
|
235
|
+
upstreamErrorCode?: string;
|
|
236
|
+
upstreamRequestId?: string;
|
|
235
237
|
/** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
|
|
236
238
|
terminalHttpStatus?: number;
|
|
237
239
|
/** Recognized structured terminal code whose exact identity must survive status mapping. */
|
|
@@ -342,6 +344,8 @@ export interface RequestLogEntry {
|
|
|
342
344
|
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
343
345
|
/** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
|
|
344
346
|
upstreamError?: string;
|
|
347
|
+
upstreamErrorCode?: string;
|
|
348
|
+
upstreamRequestId?: string;
|
|
345
349
|
usageStatus: UsageStatus;
|
|
346
350
|
usage?: OcxUsage;
|
|
347
351
|
totalTokens?: number;
|
|
@@ -1095,6 +1099,24 @@ export function inspectResponseLogSsePayloadParsed(
|
|
|
1095
1099
|
* a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
|
|
1096
1100
|
* run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
|
|
1097
1101
|
*/
|
|
1102
|
+
const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
1103
|
+
|
|
1104
|
+
function noteBoundedDiagnostic(
|
|
1105
|
+
logCtx: RequestLogContext,
|
|
1106
|
+
field: "upstreamErrorCode" | "upstreamRequestId",
|
|
1107
|
+
value: unknown,
|
|
1108
|
+
): void {
|
|
1109
|
+
if (logCtx[field] !== undefined) return;
|
|
1110
|
+
if (typeof value !== "string" || !UPSTREAM_DIAGNOSTIC_TOKEN.test(value)) return;
|
|
1111
|
+
logCtx[field] = value;
|
|
1112
|
+
const status = logCtx.terminalHttpStatus;
|
|
1113
|
+
if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
export function noteUpstreamRequestId(logCtx: RequestLogContext, headers: Headers): void {
|
|
1117
|
+
noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1098
1120
|
function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
|
|
1099
1121
|
if (!text) return;
|
|
1100
1122
|
let parsed: unknown | undefined;
|
|
@@ -1114,14 +1136,19 @@ function captureUpstreamErrorParsed(
|
|
|
1114
1136
|
if (parsed !== undefined && parsed !== null) {
|
|
1115
1137
|
const json = parsed as {
|
|
1116
1138
|
type?: unknown;
|
|
1117
|
-
error?: { message?: unknown };
|
|
1118
|
-
last_error?: { message?: unknown };
|
|
1139
|
+
error?: { message?: unknown; code?: unknown };
|
|
1140
|
+
last_error?: { message?: unknown; code?: unknown };
|
|
1119
1141
|
response?: {
|
|
1120
1142
|
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
1121
1143
|
incomplete_details?: { reason?: unknown; message?: unknown };
|
|
1122
1144
|
};
|
|
1123
1145
|
};
|
|
1124
1146
|
captureTerminalHttpStatus(logCtx, json);
|
|
1147
|
+
noteBoundedDiagnostic(
|
|
1148
|
+
logCtx,
|
|
1149
|
+
"upstreamErrorCode",
|
|
1150
|
+
json.error?.code ?? json.response?.error?.code ?? json.last_error?.code,
|
|
1151
|
+
);
|
|
1125
1152
|
const reason = json?.response?.incomplete_details?.reason;
|
|
1126
1153
|
if (json.type === "response.incomplete"
|
|
1127
1154
|
&& logCtx.terminalIncompleteReason === undefined
|
|
@@ -1593,6 +1620,8 @@ export function addFinalRequestLog(
|
|
|
1593
1620
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1594
1621
|
...(closeReason ? { closeReason } : {}),
|
|
1595
1622
|
...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
|
|
1623
|
+
...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
|
|
1624
|
+
...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
|
|
1596
1625
|
usageStatus,
|
|
1597
1626
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
1598
1627
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
@@ -401,7 +401,7 @@ export function createAdapterContinuations(
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
if (
|
|
404
|
-
(response.status === 429 || response.status === 403)
|
|
404
|
+
(response.status === 429 || response.status === 403 || response.status === 401)
|
|
405
405
|
&& anthropicInstance
|
|
406
406
|
&& transportState.anthropicPoolAccountId
|
|
407
407
|
&& !isNonReplayableResponse(response)
|
|
@@ -428,7 +428,7 @@ export function createAdapterContinuations(
|
|
|
428
428
|
);
|
|
429
429
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
430
430
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
431
|
-
nextContinuationRecoveryKind = "anthropic-oauth-429";
|
|
431
|
+
nextContinuationRecoveryKind = response.status === 401 ? "oauth-401" : "anthropic-oauth-429";
|
|
432
432
|
continue;
|
|
433
433
|
} catch {
|
|
434
434
|
// fall through to emit continuation error below
|
|
@@ -170,6 +170,9 @@ export async function prepareAdapterExchange(
|
|
|
170
170
|
| "workflowRootId"
|
|
171
171
|
| "claimAmbiguousResend"
|
|
172
172
|
| "sendsUsed"
|
|
173
|
+
| "targetSendsUsed"
|
|
174
|
+
| "initialSendAllowance"
|
|
175
|
+
| "noteInitialDispatch"
|
|
173
176
|
>,
|
|
174
177
|
) {
|
|
175
178
|
const { options, config, logCtx, req } = requestContext;
|
|
@@ -374,24 +377,33 @@ export async function prepareAdapterExchange(
|
|
|
374
377
|
const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
|
|
375
378
|
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
376
379
|
let compactPrepaidUsed = false;
|
|
380
|
+
// A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
|
|
381
|
+
// when spend enforcement is off. Enforced spend and direct requests keep the shared
|
|
382
|
+
// physical-send reporter, which already owns the permit lifecycle there.
|
|
383
|
+
// Stable per request: a Combo or compaction permit comes from `reserveDispatch`, which starts
|
|
384
|
+
// (and freezes) the spend policy first, so `spendEnforced` cannot change before dispatch.
|
|
385
|
+
const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced
|
|
386
|
+
&& Boolean(options.comboInitialSend || compactPrepaid);
|
|
377
387
|
// Combo and emergency compaction admission already book this target's first send.
|
|
378
388
|
// Its configured initial ceiling is target-local; the shared remainder below still
|
|
379
389
|
// accounts for earlier targets without deducting their sends from this target twice.
|
|
380
390
|
const initialSendCap = transientPolicy || resetPolicy
|
|
391
|
+
|| (options.comboAttempt && route.provider.adapter === "google")
|
|
381
392
|
? transientSendCapFor(transientPolicy?.attempts,
|
|
382
393
|
(options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
|
|
383
394
|
: 1;
|
|
384
395
|
const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
|
|
385
396
|
? fetchWithTransientRetry
|
|
386
397
|
: fetchWithResetRetry;
|
|
387
|
-
upstreamResponse = await fetchWithRetryPolicy(
|
|
398
|
+
try { upstreamResponse = await fetchWithRetryPolicy(
|
|
388
399
|
recovery => {
|
|
389
400
|
// Unconfigured generic sends may omit the counting reporter. Capture policy
|
|
390
401
|
// independently so a later recovery still belongs to this request's start.
|
|
391
402
|
sendBudgetState.adapterSendBudget?.startRequest?.({
|
|
392
403
|
poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
|
|
393
404
|
});
|
|
394
|
-
|
|
405
|
+
// Receipt mode books the physical send at executor admission instead of here.
|
|
406
|
+
if (!receiptMode && compactPrepaid && !compactPrepaidUsed) {
|
|
395
407
|
if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
396
408
|
compactPrepaidUsed = true;
|
|
397
409
|
}
|
|
@@ -405,6 +417,14 @@ export async function prepareAdapterExchange(
|
|
|
405
417
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
406
418
|
providerName: route.providerName,
|
|
407
419
|
modelId: route.modelId,
|
|
420
|
+
onPhysicalDispatch: () => {
|
|
421
|
+
// Preserve the unconfigured direct-adapter accounting contract; Combo admission
|
|
422
|
+
// and explicit retry/compaction budgets require a receipt for their booked send.
|
|
423
|
+
if (receiptMode) {
|
|
424
|
+
sendBudgetState.noteInitialDispatch(compactPrepaid && !compactPrepaidUsed ? compactPrepaid : undefined);
|
|
425
|
+
compactPrepaidUsed = true;
|
|
426
|
+
}
|
|
427
|
+
},
|
|
408
428
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
409
429
|
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
410
430
|
}));
|
|
@@ -413,22 +433,24 @@ export async function prepareAdapterExchange(
|
|
|
413
433
|
abortSignal: upstream.signal,
|
|
414
434
|
label: safeHostLabel(builtInitialRequest.url),
|
|
415
435
|
claimAmbiguousResend: claimPreHeaderResend,
|
|
416
|
-
...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid
|
|
436
|
+
...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
417
437
|
? {
|
|
418
438
|
// A pending compaction permit already booked this leg's first physical send.
|
|
419
|
-
...(transientPolicy || resetPolicy || compactPrepaid
|
|
439
|
+
...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
|
|
420
440
|
? {
|
|
421
441
|
attempts: Math.min(initialSendCap,
|
|
422
|
-
|
|
442
|
+
sendBudgetState.initialSendAllowance(initialSendCap) + (compactPrepaid ? 1 : 0)),
|
|
423
443
|
}
|
|
424
444
|
: {}),
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
445
|
+
// Reporter path: direct requests and enforced spend. Receipt-mode sends are
|
|
446
|
+
// settled by `onPhysicalDispatch`, so a reporter here would count them twice.
|
|
447
|
+
...(receiptMode
|
|
448
|
+
? {}
|
|
449
|
+
: { onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit) }),
|
|
428
450
|
}
|
|
429
451
|
: {}),
|
|
430
452
|
},
|
|
431
|
-
);
|
|
453
|
+
); } finally { compactPrepaid?.release(); }
|
|
432
454
|
}
|
|
433
455
|
} catch (err) {
|
|
434
456
|
cleanupUpstreamAbort();
|
|
@@ -609,8 +631,7 @@ export async function prepareAdapterExchange(
|
|
|
609
631
|
const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
|
|
610
632
|
const prepaid = sendBudgetState.pendingHopPermit;
|
|
611
633
|
const configuredTotal = refetchTransientPolicy?.attempts;
|
|
612
|
-
const refetchCap = transientSendCapFor(configuredTotal,
|
|
613
|
-
sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
|
|
634
|
+
const refetchCap = transientSendCapFor(configuredTotal, sendBudgetState.targetSendsUsed);
|
|
614
635
|
// An exact total includes a booked hop, even when it consumed the last base slot.
|
|
615
636
|
// Keep that funded send while forbidding the final reserve from widening the total.
|
|
616
637
|
const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
|
|
@@ -1076,7 +1097,7 @@ export async function prepareAdapterExchange(
|
|
|
1076
1097
|
// Anthropic OAuth: recover a rate limit or proven account entitlement refusal
|
|
1077
1098
|
// before output, within the shared request and account rotation limits.
|
|
1078
1099
|
while (
|
|
1079
|
-
(upstreamResponse.status === 429 || upstreamResponse.status === 403)
|
|
1100
|
+
(upstreamResponse.status === 429 || upstreamResponse.status === 403 || upstreamResponse.status === 401)
|
|
1080
1101
|
&& anthropicInstance
|
|
1081
1102
|
&& transportState.anthropicPoolAccountId
|
|
1082
1103
|
) {
|
|
@@ -1101,7 +1122,7 @@ export async function prepareAdapterExchange(
|
|
|
1101
1122
|
);
|
|
1102
1123
|
sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
|
|
1103
1124
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
|
|
1104
|
-
const result = await rebuildAndRefetch("anthropic-oauth-429");
|
|
1125
|
+
const result = await rebuildAndRefetch(upstreamResponse.status === 401 ? "oauth-401" : "anthropic-oauth-429");
|
|
1105
1126
|
if ("failed" in result) return result.failed;
|
|
1106
1127
|
upstreamResponse = result;
|
|
1107
1128
|
if (isNonReplayableResponse(upstreamResponse)) continue recovery;
|
|
@@ -9,7 +9,7 @@ import { CODEX_RESPONSES_HTTP_URL, type PreparedCodexWsRequest } from "./codex-w
|
|
|
9
9
|
import { CodexWsCorrelation } from "./codex-ws-correlation";
|
|
10
10
|
import type { CodexWsSession } from "./codex-ws-session";
|
|
11
11
|
import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
|
|
12
|
-
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
12
|
+
MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, markCodexWsPreludeProjection, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
|
|
13
13
|
codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
|
|
14
14
|
markCodexWsSocketDeath, type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
|
|
15
15
|
|
|
@@ -23,6 +23,7 @@ interface ExchangeOptions {
|
|
|
23
23
|
sseFallback: typeof globalThis.fetch;
|
|
24
24
|
onQuota?: CodexWsQuotaObserver;
|
|
25
25
|
beforeDispatch?: (headers: Headers) => void;
|
|
26
|
+
onPhysicalDispatch?: () => void;
|
|
26
27
|
/** Bun version string the caller gated on; stamped onto the stage record. */
|
|
27
28
|
bunVersion?: string;
|
|
28
29
|
}
|
|
@@ -44,7 +45,8 @@ function rejectionHeaders(source: Record<string, unknown>, prelude: Headers): He
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
// Reuse the metadata owner's count/value/family budgets and window freshness
|
|
47
|
-
// rules, without publishing quota twice.
|
|
48
|
+
// rules, without publishing quota twice. Pool bookkeeping consumes the HTTP
|
|
49
|
+
// projection; plain-main publication belongs only to the WS observer.
|
|
48
50
|
const projected = new CodexWsMetadata();
|
|
49
51
|
try {
|
|
50
52
|
for (const values of [Object.fromEntries(prelude), source]) {
|
|
@@ -240,6 +242,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
240
242
|
// are left out: their channel may already have sent continuation frames on this socket, so
|
|
241
243
|
// the create frame alone no longer describes the turn.
|
|
242
244
|
if (socketDied && !nativeControl) markCodexWsSocketDeath(failureResponse, stage);
|
|
245
|
+
markCodexWsPreludeProjection(failureResponse);
|
|
243
246
|
resolve(failureResponse);
|
|
244
247
|
return;
|
|
245
248
|
}
|
|
@@ -411,6 +414,17 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
411
414
|
failStream(error);
|
|
412
415
|
return;
|
|
413
416
|
}
|
|
417
|
+
try { options.onPhysicalDispatch?.(); } catch (error) {
|
|
418
|
+
// Admission refused before the frame left. This is a pre-open rejection,
|
|
419
|
+
// not a transport failure and not an authorization to replay over SSE.
|
|
420
|
+
settledPreOpen = true;
|
|
421
|
+
sent = false;
|
|
422
|
+
terminal = true;
|
|
423
|
+
cleanup();
|
|
424
|
+
session.dispose();
|
|
425
|
+
reject(error);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
414
428
|
try {
|
|
415
429
|
ws.send(frameText);
|
|
416
430
|
sentAt = Date.now();
|
|
@@ -501,6 +515,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
|
|
|
501
515
|
cleanup();
|
|
502
516
|
try { controller.close(); } catch { /* unused stream already closed */ }
|
|
503
517
|
session.dispose();
|
|
518
|
+
markCodexWsPreludeProjection(rejection);
|
|
504
519
|
resolve(rejection);
|
|
505
520
|
return;
|
|
506
521
|
}
|