@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { ENCRYPTED_FUNCTION_OUTPUT_REJECTION, upstreamErrorMessageFromPayload } from "../../lib/errors";
|
|
2
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
3
|
+
import { isReasoningEffortRejection } from "../../providers/reasoning-metadata";
|
|
4
|
+
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
5
|
+
import type { OcxParsedRequest } from "../../types";
|
|
6
|
+
import type { RequestLogContext } from "../request-log";
|
|
7
|
+
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
8
|
+
import { rememberReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
|
|
9
|
+
|
|
10
|
+
export const OPAQUE_RESPONSES_INPUT_TYPES = new Set([
|
|
11
|
+
"reasoning",
|
|
12
|
+
"compaction",
|
|
13
|
+
"compaction_summary",
|
|
14
|
+
"context_compaction",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const FUNCTION_OUTPUT_TYPES = new Set(["function_call_output", "custom_tool_call_output"]);
|
|
18
|
+
|
|
19
|
+
// codex-app subagent results replay as agent_message items whose content parts may carry
|
|
20
|
+
// backend-minted encrypted_content; the ChatGPT backend decrypts them in its function-output
|
|
21
|
+
// path, so a cross-identity replay of those parts produces ENCRYPTED_FUNCTION_OUTPUT_REJECTION.
|
|
22
|
+
export const AGENT_MESSAGE_TYPE = "agent_message";
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export function encryptedFunctionOutputParts(output: unknown): boolean {
|
|
26
|
+
return Array.isArray(output) && output.some(part => (
|
|
27
|
+
part !== null
|
|
28
|
+
&& typeof part === "object"
|
|
29
|
+
&& !Array.isArray(part)
|
|
30
|
+
&& (part as { type?: unknown }).type === "encrypted_content"
|
|
31
|
+
&& typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
32
|
+
&& (part as { encrypted_content: string }).encrypted_content.length > 0
|
|
33
|
+
));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function outboundResponsesInput(bodyText: string | undefined): unknown[] | undefined {
|
|
38
|
+
if (!bodyText) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
const body = JSON.parse(bodyText) as unknown;
|
|
41
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
|
|
42
|
+
const input = (body as { input?: unknown }).input;
|
|
43
|
+
return Array.isArray(input) ? input : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
export function outboundResponsesBodyCarriesEncryptedFunctionOutput(bodyText: string | undefined): boolean {
|
|
51
|
+
const input = outboundResponsesInput(bodyText);
|
|
52
|
+
if (!input) return false;
|
|
53
|
+
return input.some(item => {
|
|
54
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) return false;
|
|
55
|
+
const candidate = item as { type?: unknown; output?: unknown; content?: unknown };
|
|
56
|
+
const type = String(candidate.type ?? "");
|
|
57
|
+
if (FUNCTION_OUTPUT_TYPES.has(type) && encryptedFunctionOutputParts(candidate.output)) return true;
|
|
58
|
+
return type === AGENT_MESSAGE_TYPE && encryptedFunctionOutputParts(candidate.content);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
export function outboundResponsesBodyCarriesOpaqueBlob(bodyText: string | undefined): boolean {
|
|
64
|
+
const input = outboundResponsesInput(bodyText);
|
|
65
|
+
if (!input) return false;
|
|
66
|
+
return input.some(item => {
|
|
67
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
68
|
+
const candidate = item as { type?: unknown; encrypted_content?: unknown; output?: unknown };
|
|
69
|
+
if (
|
|
70
|
+
typeof candidate.type === "string"
|
|
71
|
+
&& OPAQUE_RESPONSES_INPUT_TYPES.has(candidate.type)
|
|
72
|
+
&& typeof candidate.encrypted_content === "string"
|
|
73
|
+
&& candidate.encrypted_content.length > 0
|
|
74
|
+
) return true;
|
|
75
|
+
if (
|
|
76
|
+
typeof candidate.type === "string"
|
|
77
|
+
&& FUNCTION_OUTPUT_TYPES.has(candidate.type)
|
|
78
|
+
&& encryptedFunctionOutputParts(candidate.output)
|
|
79
|
+
) return true;
|
|
80
|
+
return candidate.type === AGENT_MESSAGE_TYPE
|
|
81
|
+
&& encryptedFunctionOutputParts((candidate as { content?: unknown }).content);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
export function isEncryptedFunctionOutputRejection(bodyText: string): boolean {
|
|
87
|
+
if (bodyText.trim() === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
88
|
+
try {
|
|
89
|
+
const payload = JSON.parse(bodyText) as unknown;
|
|
90
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
91
|
+
const record = payload as { detail?: unknown; message?: unknown; error?: unknown };
|
|
92
|
+
if (record.detail === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
93
|
+
if (record.message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
94
|
+
if (record.error === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) return true;
|
|
95
|
+
return record.error !== null
|
|
96
|
+
&& typeof record.error === "object"
|
|
97
|
+
&& !Array.isArray(record.error)
|
|
98
|
+
&& (record.error as { message?: unknown }).message === ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* #4469: reasoning encrypted_content is minted per caller identity, so replaying it under a
|
|
107
|
+
* different caller is rejected with "reasoning `encrypted_content` was not issued to this
|
|
108
|
+
* caller". Substring checks tolerate the optional backticks and a leading or trailing
|
|
109
|
+
* sentence, while the "was not issued to this caller" anchor plus an encrypted-content or
|
|
110
|
+
* reasoning subject keep unrelated invalid_request_error prose from gaining a hidden resend.
|
|
111
|
+
*/
|
|
112
|
+
export function isReasoningBlobCallerMismatchMessage(message: string): boolean {
|
|
113
|
+
if (!message.includes("was not issued to this caller")) return false;
|
|
114
|
+
return message.includes("encrypted_content") || message.includes("reasoning");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
export function isSelfIdentifiedOpaqueBlobRejection(bodyText: string): boolean {
|
|
119
|
+
if (isEncryptedFunctionOutputRejection(bodyText)) return true;
|
|
120
|
+
try {
|
|
121
|
+
if (upstreamErrorMessageFromPayload(JSON.parse(bodyText) as unknown) === ENCRYPTED_FUNCTION_OUTPUT_REJECTION) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
/* invalid JSON bodies fall through to the exact nested envelope checks */
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const payload = JSON.parse(bodyText) as unknown;
|
|
129
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
130
|
+
const record = payload as { code?: unknown; type?: unknown; message?: unknown; error?: unknown };
|
|
131
|
+
|
|
132
|
+
if (record.error && typeof record.error === "object" && !Array.isArray(record.error)) {
|
|
133
|
+
const error = record.error as { type?: unknown; code?: unknown; message?: unknown };
|
|
134
|
+
if (error.type === "invalid_request_error") {
|
|
135
|
+
if (error.code === "invalid_encrypted_content") return true;
|
|
136
|
+
if (
|
|
137
|
+
(error.code === null || error.code === undefined)
|
|
138
|
+
&& typeof error.message === "string"
|
|
139
|
+
&& error.message.startsWith("The encrypted content ")
|
|
140
|
+
&& error.message.endsWith(
|
|
141
|
+
" could not be verified. Reason: Encrypted content could not be decrypted or parsed.",
|
|
142
|
+
)
|
|
143
|
+
) return true;
|
|
144
|
+
// #4469: the caller-mismatch wording arrives without a dedicated code, so the
|
|
145
|
+
// message itself is the identity. It is not gated on code being null — the upstream
|
|
146
|
+
// may attach a generic code — because the anchored phrase is already specific.
|
|
147
|
+
if (typeof error.message === "string" && isReasoningBlobCallerMismatchMessage(error.message)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// The flat stream-error envelope carries type/message at the top level rather than under
|
|
154
|
+
// an error object; the same anchored identity applies there.
|
|
155
|
+
if (
|
|
156
|
+
record.type === "invalid_request_error"
|
|
157
|
+
&& typeof record.message === "string"
|
|
158
|
+
&& isReasoningBlobCallerMismatchMessage(record.message)
|
|
159
|
+
) return true;
|
|
160
|
+
|
|
161
|
+
if (record.code !== "invalid-argument" || typeof record.error !== "string") return false;
|
|
162
|
+
return record.error.startsWith("Could not decode the compaction blob")
|
|
163
|
+
|| record.error.startsWith("Could not decrypt the provided encrypted_content");
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether an upstream Responses 4xx authoritatively rejected opaque replay state.
|
|
172
|
+
*
|
|
173
|
+
* The outbound-body check is intentional: the inbound transcript may contain a proxy envelope or
|
|
174
|
+
* compaction blob that the adapter already lowered, in which case a replay would be byte-identical.
|
|
175
|
+
* OpenAI usually exposes a dedicated nested code; ChatGPT also emits one exact code-less
|
|
176
|
+
* unverifiable-ciphertext message, and #4469 added the anchored caller-mismatch wording for
|
|
177
|
+
* reasoning blobs minted under a different caller. xAI's code is generic, so its two concrete
|
|
178
|
+
* decoder error identities are also required. Unrelated error prose must never gain a hidden resend.
|
|
179
|
+
*/
|
|
180
|
+
export function shouldAttemptOpaqueBlobRecovery(args: {
|
|
181
|
+
status: number;
|
|
182
|
+
adapterName: string;
|
|
183
|
+
outboundBody?: string;
|
|
184
|
+
errorBody: string;
|
|
185
|
+
alreadyAttempted: boolean;
|
|
186
|
+
}): boolean {
|
|
187
|
+
const acceptedStatus = (args.status >= 400 && args.status < 500)
|
|
188
|
+
|| (
|
|
189
|
+
args.status === 502
|
|
190
|
+
&& outboundResponsesBodyCarriesEncryptedFunctionOutput(args.outboundBody)
|
|
191
|
+
&& isEncryptedFunctionOutputRejection(args.errorBody)
|
|
192
|
+
);
|
|
193
|
+
return acceptedStatus
|
|
194
|
+
&& args.adapterName === "openai-responses"
|
|
195
|
+
&& !args.alreadyAttempted
|
|
196
|
+
&& outboundResponsesBodyCarriesOpaqueBlob(args.outboundBody)
|
|
197
|
+
&& isSelfIdentifiedOpaqueBlobRejection(args.errorBody);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Peek the upstream error body for the reasoning-effort downgrade. Only 400/403 are considered
|
|
203
|
+
* and the body must be complete and display-safe, the same contract the other rejection peeks
|
|
204
|
+
* use. The match is deliberately narrow: the upstream has to name reasoning effort, so an
|
|
205
|
+
* unrelated 400 never triggers a replay.
|
|
206
|
+
*/
|
|
207
|
+
export async function reasoningEffortRejectionText(
|
|
208
|
+
response: Response,
|
|
209
|
+
alreadyAttempted: boolean,
|
|
210
|
+
signal: AbortSignal,
|
|
211
|
+
): Promise<string | undefined> {
|
|
212
|
+
if (alreadyAttempted) return undefined;
|
|
213
|
+
if (response.status !== 400 && response.status !== 403) return undefined;
|
|
214
|
+
try {
|
|
215
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
216
|
+
if (!body.displaySafe || body.truncated) return undefined;
|
|
217
|
+
return isReasoningEffortRejection(body.text) ? body.text : undefined;
|
|
218
|
+
} catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
export async function opaqueBlobRejectionBodyForRecovery(
|
|
225
|
+
response: Response,
|
|
226
|
+
outboundBody: string | undefined,
|
|
227
|
+
adapterName: string,
|
|
228
|
+
alreadyAttempted: boolean,
|
|
229
|
+
signal: AbortSignal,
|
|
230
|
+
): Promise<string | undefined> {
|
|
231
|
+
if (
|
|
232
|
+
isNonReplayableResponse(response)
|
|
233
|
+
|| response.status < 400
|
|
234
|
+
|| (response.status >= 500 && response.status !== 502)
|
|
235
|
+
|| adapterName !== "openai-responses"
|
|
236
|
+
|| alreadyAttempted
|
|
237
|
+
|| !outboundResponsesBodyCarriesOpaqueBlob(outboundBody)
|
|
238
|
+
) return undefined;
|
|
239
|
+
try {
|
|
240
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
241
|
+
return body.displaySafe && !body.truncated ? body.text : undefined;
|
|
242
|
+
} catch {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Backoff for the single exact-request replay after a canonical Console upload rejection.
|
|
250
|
+
*/
|
|
251
|
+
export const CONSOLE_GO_UPLOAD_RETRY_DELAY_MS = 800;
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Peek the upstream error body for the Console Go transient-400 recovery. Only a complete,
|
|
256
|
+
* display-safe body may drive a retry decision (same contract as
|
|
257
|
+
* opaqueBlobRejectionBodyForRecovery), and reading a clone leaves the original response intact
|
|
258
|
+
* for the caller's own error surface when no retry is taken.
|
|
259
|
+
*/
|
|
260
|
+
export async function consoleGoUploadRejectionBody(
|
|
261
|
+
response: Response,
|
|
262
|
+
alreadyAttempted: boolean,
|
|
263
|
+
signal: AbortSignal,
|
|
264
|
+
): Promise<string | undefined> {
|
|
265
|
+
if (isNonReplayableResponse(response) || response.status !== 400 || alreadyAttempted) return undefined;
|
|
266
|
+
try {
|
|
267
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
268
|
+
return body.displaySafe && !body.truncated ? body.text : undefined;
|
|
269
|
+
} catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
export function prepareOpaqueBlobRecovery(parsed: OcxParsedRequest): void {
|
|
276
|
+
parsed._stripReasoningEncryptedContent = true;
|
|
277
|
+
const rawBody = parsed._rawBody;
|
|
278
|
+
if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) return;
|
|
279
|
+
const input = (rawBody as { input?: unknown }).input;
|
|
280
|
+
if (!Array.isArray(input)) return;
|
|
281
|
+
const stripEncryptedParts = (parts: unknown[]): unknown[] => {
|
|
282
|
+
let changed = false;
|
|
283
|
+
const stripped = parts.map(part => {
|
|
284
|
+
if (
|
|
285
|
+
part !== null
|
|
286
|
+
&& typeof part === "object"
|
|
287
|
+
&& !Array.isArray(part)
|
|
288
|
+
&& (part as { type?: unknown }).type === "encrypted_content"
|
|
289
|
+
&& typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
|
|
290
|
+
&& (part as { encrypted_content: string }).encrypted_content.length > 0
|
|
291
|
+
) {
|
|
292
|
+
changed = true;
|
|
293
|
+
return { type: "input_text", text: "[encrypted content omitted]" };
|
|
294
|
+
}
|
|
295
|
+
return part;
|
|
296
|
+
});
|
|
297
|
+
return changed ? stripped : parts;
|
|
298
|
+
};
|
|
299
|
+
const strippedInput = input.map(item => {
|
|
300
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
301
|
+
const record = item as Record<string, unknown>;
|
|
302
|
+
const type = String(record.type ?? "");
|
|
303
|
+
if (FUNCTION_OUTPUT_TYPES.has(type) && Array.isArray(record.output)) {
|
|
304
|
+
const output = stripEncryptedParts(record.output);
|
|
305
|
+
return output !== record.output ? { ...record, output } : item;
|
|
306
|
+
}
|
|
307
|
+
if (type === AGENT_MESSAGE_TYPE && Array.isArray(record.content)) {
|
|
308
|
+
const content = stripEncryptedParts(record.content);
|
|
309
|
+
return content !== record.content ? { ...record, content } : item;
|
|
310
|
+
}
|
|
311
|
+
return item;
|
|
312
|
+
});
|
|
313
|
+
Object.assign(rawBody, { input: strippedInput });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
export function resetStreamedOpaqueBlobLogContext(logCtx: RequestLogContext): void {
|
|
318
|
+
delete logCtx.upstreamError;
|
|
319
|
+
delete logCtx.terminalHttpStatus;
|
|
320
|
+
delete logCtx.terminalErrorCode;
|
|
321
|
+
delete logCtx.terminalIncompleteReason;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
export type OpaqueBlobRecoveryGuard = { attempted: boolean };
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
export type OpaqueBlobRecoveryResult =
|
|
329
|
+
| { kind: "skipped" }
|
|
330
|
+
| { kind: "recovered"; response: Response }
|
|
331
|
+
| { kind: "failed"; response: Response };
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
export async function attemptOpaqueBlobRecovery(
|
|
335
|
+
args: {
|
|
336
|
+
response: Response;
|
|
337
|
+
outboundBody?: string;
|
|
338
|
+
adapterName: string;
|
|
339
|
+
parsed: OcxParsedRequest;
|
|
340
|
+
guard: OpaqueBlobRecoveryGuard;
|
|
341
|
+
signal: AbortSignal;
|
|
342
|
+
},
|
|
343
|
+
rebuild: (kind: AttemptRecoveryKind) => Promise<Response | { failed: Response }>,
|
|
344
|
+
): Promise<OpaqueBlobRecoveryResult> {
|
|
345
|
+
const errorBody = await opaqueBlobRejectionBodyForRecovery(
|
|
346
|
+
args.response,
|
|
347
|
+
args.outboundBody,
|
|
348
|
+
args.adapterName,
|
|
349
|
+
args.guard.attempted,
|
|
350
|
+
args.signal,
|
|
351
|
+
);
|
|
352
|
+
if (errorBody === undefined || !shouldAttemptOpaqueBlobRecovery({
|
|
353
|
+
status: args.response.status,
|
|
354
|
+
adapterName: args.adapterName,
|
|
355
|
+
outboundBody: args.outboundBody,
|
|
356
|
+
errorBody,
|
|
357
|
+
alreadyAttempted: args.guard.attempted,
|
|
358
|
+
})) {
|
|
359
|
+
return { kind: "skipped" };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
args.guard.attempted = true;
|
|
363
|
+
const rejectedScope = args.parsed._reasoningReplayScope
|
|
364
|
+
? {
|
|
365
|
+
clientThreadId: args.parsed._reasoningReplayScope.clientThreadId,
|
|
366
|
+
...(args.parsed._reasoningReplayScope.current
|
|
367
|
+
? { current: { ...args.parsed._reasoningReplayScope.current } }
|
|
368
|
+
: {}),
|
|
369
|
+
}
|
|
370
|
+
: undefined;
|
|
371
|
+
prepareOpaqueBlobRecovery(args.parsed);
|
|
372
|
+
try { void args.response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
373
|
+
const result = await rebuild("opaque-blob-rejection");
|
|
374
|
+
if (!("failed" in result) && result.ok) {
|
|
375
|
+
rememberReasoningReplayOpaqueBlobRejection(rejectedScope);
|
|
376
|
+
}
|
|
377
|
+
return "failed" in result
|
|
378
|
+
? { kind: "failed", response: result.failed }
|
|
379
|
+
: { kind: "recovered", response: result };
|
|
380
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
|
|
2
|
+
import type { CodexAuthPolicyConfig, CodexAuthContext } from "../../codex/auth-context";
|
|
3
|
+
import type { AdmissionLease } from "../../lib/admission";
|
|
4
|
+
import type { DataPlaneAdmission } from "../auth-cors";
|
|
5
|
+
import { resolveCodexModelEntitlements } from "../../codex/model-entitlements";
|
|
6
|
+
import type { ResponsesTerminalStatus } from "../../bridge";
|
|
7
|
+
import type { ResponsesTerminalRepairScheduler } from "../responses-terminal-repair";
|
|
8
|
+
import type { BunRuntimeGateInput } from "./ws-upstream";
|
|
9
|
+
import type { NativeMainRefreshDependencies } from "../../codex/main-account";
|
|
10
|
+
import type { InboundWire } from "../../providers/registry";
|
|
11
|
+
import type { ExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
|
|
12
|
+
import type { CallerDirectAuth } from "../../providers/caller-authorization";
|
|
13
|
+
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
14
|
+
import type { TransientSendBudget } from "../../lib/upstream-retry";
|
|
15
|
+
import type { RequestLogContext } from "../request-log";
|
|
16
|
+
import type { UpstreamHostAdmissionLease } from "../../codex/upstream-host-health";
|
|
17
|
+
|
|
18
|
+
export interface ConsumedComboFailure {
|
|
19
|
+
response: Response;
|
|
20
|
+
classificationText: string;
|
|
21
|
+
/** Structured upstream `error.code` when present in the failure body. */
|
|
22
|
+
upstreamCode?: string;
|
|
23
|
+
/** Valid numeric/date value used only for cooldown calculation. */
|
|
24
|
+
retryAfter?: string;
|
|
25
|
+
/** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
|
|
26
|
+
resetAt?: string[];
|
|
27
|
+
/** Reserved for 040 usage attribution without adding another body read. */
|
|
28
|
+
usage?: OcxUsage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
export interface HandleResponsesOptions {
|
|
35
|
+
/** Internal Claude replay identity; consumed only by the final canonical Go transport. */
|
|
36
|
+
claudeGoAffinity?: { sessionLane?: string };
|
|
37
|
+
/** Validated Claude metadata identity; projected only into final canonical attempt headers. */
|
|
38
|
+
claudeNativeSessionId?: string;
|
|
39
|
+
/** Original live policy owner; separate from caller-specific routing/sidecar snapshots. */
|
|
40
|
+
codexAuthPolicy?: CodexAuthPolicyConfig;
|
|
41
|
+
turnAdmissionLease?: AdmissionLease;
|
|
42
|
+
/**
|
|
43
|
+
* How the caller proved data-plane admission (#1686).
|
|
44
|
+
*
|
|
45
|
+
* A bearer-presented admission secret is one of OUR OWN secrets, so a Direct turn must
|
|
46
|
+
* SUBSTITUTE the stored main credential rather than forward it. Without this fact at the
|
|
47
|
+
* decision point, Direct cannot tell an admission bearer from the user own ChatGPT bearer,
|
|
48
|
+
* which is why it refused the whole env_key flow instead of serving it.
|
|
49
|
+
*/
|
|
50
|
+
admission?: DataPlaneAdmission;
|
|
51
|
+
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
52
|
+
onRequestBodyRead?: () => void;
|
|
53
|
+
forceEmptyResponseId?: boolean;
|
|
54
|
+
abortSignal?: AbortSignal;
|
|
55
|
+
/** One-shot TTFT callback: first non-empty model output observed (WP4). */
|
|
56
|
+
onFirstOutput?: () => void;
|
|
57
|
+
onCodexAuthContextResolved?: (context: CodexAuthContext | undefined) => void;
|
|
58
|
+
/** Internal deterministic seam for account-gated native fallback tests. */
|
|
59
|
+
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
60
|
+
/** Internal: validated final client-visible model, after completed terminal success only. */
|
|
61
|
+
onResponseComplete?: (model: string) => void;
|
|
62
|
+
recordTerminalOutcomes?: boolean;
|
|
63
|
+
setTerminalOutcomeRecorder?: (recorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined) => void;
|
|
64
|
+
onNativePassthroughTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
65
|
+
onNativePassthroughCancel?: () => void;
|
|
66
|
+
/** Internal deterministic clock/timer seam for provider terminal repair. */
|
|
67
|
+
responsesTerminalRepairScheduler?: ResponsesTerminalRepairScheduler;
|
|
68
|
+
/** Internal deterministic runtime-identity seam for Codex upstream WS selection tests. */
|
|
69
|
+
codexWsRuntimeIdentity?: BunRuntimeGateInput;
|
|
70
|
+
/** Test seam for native main refresh without live OAuth traffic. */
|
|
71
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
72
|
+
/**
|
|
73
|
+
* When true, body `prompt_cache_key` is a Claude Desktop shared cache cohort
|
|
74
|
+
* (system/tools hash), not a per-session id — do not use it for Anthropic pool affinity.
|
|
75
|
+
*/
|
|
76
|
+
promptCacheKeyIsSharedCohort?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Wire protocol the ORIGINAL client spoke. The Chat and Anthropic surfaces translate
|
|
79
|
+
* their body into a Responses shape and replay through this function, so without an
|
|
80
|
+
* explicit value the replay would look like a native Responses request and an
|
|
81
|
+
* inbound-scoped registry wire default would fire for a client that never asked for
|
|
82
|
+
* it. Omitted means a genuine Responses inbound.
|
|
83
|
+
*/
|
|
84
|
+
inboundWire?: InboundWire;
|
|
85
|
+
/** Internal transport identity for route-scoped upstream compatibility policy. */
|
|
86
|
+
inboundTransport?: "websocket";
|
|
87
|
+
/**
|
|
88
|
+
* Claude replay may add native-main auth so OpenAI sidecars remain available.
|
|
89
|
+
* Strip only that internal credential when the final route is a noncanonical
|
|
90
|
+
* forward/caller-auth destination; final routing can differ from Claude's preflight route.
|
|
91
|
+
*/
|
|
92
|
+
stripClaudeMainAuthForNoncanonicalForward?: boolean;
|
|
93
|
+
/** In-memory credential proven by Claude's native-main turn claim; never persist or log. */
|
|
94
|
+
trustedClaudeMainAuth?: { authorization: string; chatgptAccountId?: string };
|
|
95
|
+
/** Sidecar-only auth captured before route changes; null means no usable original pair. */
|
|
96
|
+
openAiSidecarAuth?: ExplicitOpenAiCallerAuth | null;
|
|
97
|
+
/** Internal Chat bridge permission to obtain claimed stored auth only for a final Direct sidecar. */
|
|
98
|
+
allowStoredOpenAiSidecarAuth?: boolean;
|
|
99
|
+
/** Original caller-owned native pair; separate from any claimed sidecar enrichment. */
|
|
100
|
+
nativeCallerAuth?: ExplicitOpenAiCallerAuth | null;
|
|
101
|
+
/** Caller Direct credential under Direct\'s own predicate; restored only for the canonical OpenAI final route. */
|
|
102
|
+
callerDirectAuth?: CallerDirectAuth | null;
|
|
103
|
+
/** Internal recursion guard; callers outside this module must not set it. */
|
|
104
|
+
comboAttempt?: boolean;
|
|
105
|
+
/** Internal combo handoff for one parent-validated continuation snapshot. */
|
|
106
|
+
comboReplaySnapshot?: {
|
|
107
|
+
sourceBody: unknown;
|
|
108
|
+
previousResponseInputExpanded: boolean;
|
|
109
|
+
providerContinuation: OcxProviderContinuationState | undefined;
|
|
110
|
+
recoveredPlaintext: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** Internal combo handoff: allow a later same-provider model after a reset-derived 429/402. */
|
|
113
|
+
deferCodexResetDerivedCooldown?: boolean;
|
|
114
|
+
/** 030-owned handoff when a child consumed the original failure under bounds. */
|
|
115
|
+
onConsumedComboFailure?: (failure: ConsumedComboFailure) => void;
|
|
116
|
+
/** A stored Pool credential was refreshed and its one allowed same-account replay was sent. */
|
|
117
|
+
onStoredPool401ReplayDispatched?: () => void;
|
|
118
|
+
/** Caller-owned for Chat/Claude replay; omitted only at genuine Responses ingress. */
|
|
119
|
+
translatorBudget?: TranslatorBudget;
|
|
120
|
+
/**
|
|
121
|
+
* Transient sends already spent by this logical request. Combo children inherit the parent's
|
|
122
|
+
* holder through the options spread, so a fan-out shares one allowance instead of taking a
|
|
123
|
+
* fresh one per target (#4546).
|
|
124
|
+
*/
|
|
125
|
+
sendBudget?: TransientSendBudget;
|
|
126
|
+
/**
|
|
127
|
+
* Terminal vision-describe marker (roadmap 180): true when the inbound
|
|
128
|
+
* request IS the vision sidecar's own loopback describe call. The plan site
|
|
129
|
+
* then STRIPS images instead of planning another describe — a depth cap of 1
|
|
130
|
+
* that holds under predicate drift and combo re-resolution. The Chat surface
|
|
131
|
+
* detects the raw `x-opencodex-vision-describe` header before its bridge
|
|
132
|
+
* rebuilds headers and carries the fact through this flag.
|
|
133
|
+
*/
|
|
134
|
+
visionDescribeTerminal?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Values shared by the call, not a bag of mutable pipeline state. */
|
|
138
|
+
export interface ResponsesRequestContext {
|
|
139
|
+
req: Request;
|
|
140
|
+
config: OcxConfig;
|
|
141
|
+
logCtx: RequestLogContext;
|
|
142
|
+
options: HandleResponsesOptions & { translatorBudget: TranslatorBudget };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Admission leases remain owned by the outer finally until explicitly transferred. */
|
|
146
|
+
export interface ResponsesAdmissionState {
|
|
147
|
+
pendingHostAdmissionLease: UpstreamHostAdmissionLease | null;
|
|
148
|
+
authCtx: CodexAuthContext;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface PassthroughAdmissionState {
|
|
152
|
+
lease: UpstreamHostAdmissionLease | null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Recursive combo children enter the same ingress without a runtime import cycle. */
|
|
156
|
+
export interface ResponsesDispatchers {
|
|
157
|
+
handleResponses(req: Request, config: OcxConfig, logCtx: RequestLogContext, options?: HandleResponsesOptions): Promise<Response>;
|
|
158
|
+
handleComboResponses(req: Request, body: unknown, comboId: string, config: OcxConfig, logCtx: RequestLogContext, options: HandleResponsesOptions & { translatorBudget: TranslatorBudget }): Promise<Response>;
|
|
159
|
+
}
|