@bitkyc08/opencodex 2.55.0 → 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.
Files changed (167) hide show
  1. package/gui/dist/assets/{index-VuoiWj9J.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,611 @@
1
+ import { normalizeRoutedAgentMessages } from "../routed-agent-messages";
2
+ import { stripBracketedModelSuffix } from "../openai-chat";
3
+ import { normalizeOpenCodeGoAdditionalTools } from "../opencode-go-additional-tools";
4
+ import { isXaiResponsesDestination } from "../../providers/xai-transport";
5
+ import { Buffer } from "node:buffer";
6
+ import type { IncomingMeta, ProviderAdapter } from "../base";
7
+ import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../../types";
8
+ import { applyCodexRoutingHint, CODEX_RESPONSES_LITE_HEADER, CODEX_ROUTING_HINT_HEADER } from "../../codex/forward-transport-headers";
9
+ import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../../responses/compaction";
10
+ import { decodeServerSentEvents } from "../../lib/sse-decoder";
11
+ import {
12
+ CODEX_FORWARD_BASE_URL,
13
+ destinationDecodesNativeCompactionBlob,
14
+ isCanonicalOpenAiForwardProvider,
15
+ isOpenAiOperatedResponsesDestination,
16
+ } from "../../providers/openai-tiers";
17
+ import type { TranslatorBudget } from "../../lib/translator-budget";
18
+ import { rewriteRoutedCustomToolsForUpstream } from "../../responses/custom-tool-compat";
19
+ import { rewriteRoutedToolSearchForUpstream } from "../../responses/tool-search-compat";
20
+ import { rewriteRoutedNamespaceToolsForUpstream } from "../../responses/namespace-tool-compat";
21
+ import { preparePlaintextV2AgentMessages } from "../../responses/plaintext-v2-agent-messages";
22
+ import { isMetaAiResponsesDestination, rewriteMuseToolNamesForUpstream } from "../../responses/muse-tool-name-alias";
23
+ import { openaiResponsesUrl } from "../openai-responses-url";
24
+ import { normalizeResponsesCodeMode } from "../responses-code-mode";
25
+ import { injectXaiResponsesXSearch, normalizeXaiResponsesWebSearch } from "../xai-web-search";
26
+ import {
27
+ isXaiSchemaTarget,
28
+ normalizeXaiToolParameters,
29
+ XaiToolSchemaCompatibilityError,
30
+ } from "../xai-tool-schema";
31
+ import {
32
+ createAdapterTierMetadata,
33
+ } from "../../providers/fastwire";
34
+ import { mapRoutedResponsesReasoningEffort, normalizeConfiguredReasoningSummaryDelivery, sanitizeReasoningInputContent, stripDisabledReasoningSummaries, stripDisabledVerbosity, stripUnsupportedReasoningSummaryDelivery } from "./reasoning";
35
+ import { scrubOcxCompactionItems, stripCanonicalOnlyToolFields, stripInternalChatMessageMetadataPassthrough, stripInvalidItemIds, stripItemIdsWhenUnstored } from "./request-strips";
36
+ import { stripCanonicalForwardPromptCacheOptions, stripDeprecatedPromptCacheRetention } from "./prompt-cache";
37
+ import { isPlainObject } from "./internal";
38
+ import { normalizeToolSchemas, promoteClientLoadedTools, stripUnsupportedHostedTools } from "./tool-schema";
39
+ import { annotateEmptyResponsesToolOutputs, backfillWebSearchQueries, normalizeResponsesToolResultAdjacency, repairOrphanedInputItems, repairOversizedReplayCallIds, repairUnidentifiedToolOutputItems } from "./tool-output-recovery";
40
+ import { applyTierDecisionToResponsesBody, normalizeCanonicalForwardContinuationEnvelope, normalizeCanonicalForwardPromptEnvelope, stripCanonicalForwardSamplingParams, stripPreviousResponseId, stripStatefulResponsesParams, stripUnsupportedForwardParams } from "./canonical-forward";
41
+ import { normalizeImageGenClientTools, preferConfiguredHostedTools } from "./image-gen";
42
+ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchFields } from "./web-search";
43
+
44
+ // Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
45
+ // Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
46
+ export const FORWARD_HEADERS = [
47
+ "authorization",
48
+ "chatgpt-account-id",
49
+ "openai-beta",
50
+ "originator",
51
+ "session_id",
52
+ "session-id",
53
+ "thread-id",
54
+ "x-client-request-id",
55
+ "x-codex-beta-features",
56
+ "x-codex-installation-id",
57
+ "x-codex-parent-thread-id",
58
+ "x-codex-turn-metadata",
59
+ "x-codex-turn-state",
60
+ "x-codex-window-id",
61
+ "x-oai-attestation",
62
+ "x-openai-subagent",
63
+ "x-responsesapi-include-timing-metrics",
64
+ CODEX_RESPONSES_LITE_HEADER,
65
+ ];
66
+
67
+ /** Replace every `input_image` part under a routed-compaction body with a short marker. */
68
+ function stripInputImagesDeep(value: unknown): unknown {
69
+ if (Array.isArray(value)) return value.map(stripInputImagesDeep);
70
+ if (!isPlainObject(value)) return value;
71
+ if (value.type === "input_image") {
72
+ return { type: "input_text", text: "[image omitted for compaction]" };
73
+ }
74
+ const out: Record<string, unknown> = {};
75
+ for (const [key, entry] of Object.entries(value)) out[key] = stripInputImagesDeep(entry);
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * Rewrite a compaction turn for an upstream that does not speak Codex's private
81
+ * `compaction_trigger` item: drop the trigger and the whole tool surface, and ask
82
+ * for the handoff summary in plain terms instead (#422).
83
+ *
84
+ * The adapter builds from `parsed._rawBody`, so the summarizer prompt that
85
+ * handleResponses() pushed onto `parsed.context` never reaches the wire — it has to
86
+ * be applied here. Images go too: a summary needs no pixels, and a text-only
87
+ * gateway would reject them.
88
+ */
89
+ function buildRoutedCompactionBody(body: unknown): unknown {
90
+ if (!isPlainObject(body)) return body;
91
+ // `text` goes with the tool fields: the summary must be prose, not schema-constrained JSON.
92
+ const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, text: _text, ...rest } = body;
93
+ const input = Array.isArray(body.input) ? body.input : [];
94
+ const kept = input.filter(item => !isPlainObject(item)
95
+ // `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
96
+ // leaving it in would break the no-tools invariant even with `tools` removed.
97
+ || (item.type !== "compaction_trigger" && item.type !== "additional_tools"));
98
+ return {
99
+ ...rest,
100
+ input: [
101
+ ...(stripInputImagesDeep(kept) as unknown[]),
102
+ { type: "message", role: "user", content: [{ type: "input_text", text: COMPACT_PROMPT }] },
103
+ ],
104
+ };
105
+ }
106
+
107
+ /** Read the Responses `usage` block, if the gateway sent one. */
108
+ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
109
+ if (!isPlainObject(payload) || !isPlainObject(payload.usage)) return undefined;
110
+ const usage = payload.usage;
111
+ const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
112
+ const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
113
+ // openai/codex#41980: the raw usage object is wire data a rebuilt response.completed must keep —
114
+ // unknown keys (subscription metadata, future counters) ride along even when the token counts
115
+ // themselves are zero or absent (metadata-only usage).
116
+ const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
117
+ const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
118
+ || (isPlainObject(usage.input_tokens_details)
119
+ && Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"))
120
+ || (isPlainObject(usage.output_tokens_details)
121
+ && Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
122
+ if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
123
+ const inputDetails = isPlainObject(usage.input_tokens_details) ? usage.input_tokens_details : undefined;
124
+ const outputDetails = isPlainObject(usage.output_tokens_details) ? usage.output_tokens_details : undefined;
125
+ return {
126
+ inputTokens,
127
+ outputTokens,
128
+ ...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
129
+ ...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
130
+ ...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
131
+ ...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
132
+ ...(hasExtras ? { rawUsage: { ...usage } } : {}),
133
+ };
134
+ }
135
+
136
+ function responsesPayloadText(response: unknown): string {
137
+ if (!isPlainObject(response) || !Array.isArray(response.output)) return "";
138
+ return response.output
139
+ .filter(item => isPlainObject(item) && item.type === "message")
140
+ .flatMap(item => (Array.isArray((item as Record<string, unknown>).content)
141
+ ? (item as { content: unknown[] }).content
142
+ : []))
143
+ .filter(part => isPlainObject(part) && part.type === "output_text")
144
+ .map(part => String((part as { text?: unknown }).text ?? ""))
145
+ .join("");
146
+ }
147
+
148
+ function responsesErrorMessage(payload: unknown): string {
149
+ if (!isPlainObject(payload)) return "upstream compaction failed";
150
+ const err = payload.error;
151
+ if (typeof err === "string") return err;
152
+ if (isPlainObject(err) && typeof err.message === "string") return err.message;
153
+ const incomplete = payload.incomplete_details;
154
+ if (isPlainObject(incomplete) && typeof incomplete.reason === "string") return incomplete.reason;
155
+ return "upstream compaction failed";
156
+ }
157
+
158
+ /** Count an append without rescanning accumulated text, including split surrogate pairs. */
159
+ function appendedUtf8Bytes(previousBytes: number, lastCodeUnit: number, fragment: string): number {
160
+ const first = fragment.charCodeAt(0);
161
+ // Separate lone surrogates each count as a three-byte replacement character; together
162
+ // they encode as one four-byte scalar. Empty fragments produce NaN and never pair.
163
+ const joinsSurrogatePair = lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff && first >= 0xdc00 && first <= 0xdfff;
164
+ return previousBytes + Buffer.byteLength(fragment, "utf8") - (joinsSurrogatePair ? 2 : 0);
165
+ }
166
+
167
+ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } {
168
+ return {
169
+ name: "openai-responses",
170
+ passthrough: true as const,
171
+
172
+ buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) {
173
+ const translatorBudget = incoming.translatorBudget;
174
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
175
+ let url: string;
176
+
177
+ if (provider.authMode === "forward") {
178
+ const mayForwardCallerCredentials = isCanonicalOpenAiForwardProvider(provider);
179
+ // OAuth passthrough: ChatGPT backend path is `${baseUrl}/responses` (no /v1).
180
+ const baseUrl = mayForwardCallerCredentials
181
+ ? CODEX_FORWARD_BASE_URL
182
+ : provider.baseUrl.replace(/\/+$/, "");
183
+ url = `${baseUrl}/responses`;
184
+ if (provider.headers) Object.assign(headers, provider.headers); // static headers first…
185
+ const runtimeProvider = provider as {
186
+ _codexAccountOverride?: { accessToken: string; chatgptAccountId: string };
187
+ _codexAccountRequired?: boolean;
188
+ };
189
+ if (
190
+ mayForwardCallerCredentials
191
+ && runtimeProvider._codexAccountRequired
192
+ && !runtimeProvider._codexAccountOverride
193
+ ) {
194
+ throw new Error("Codex pool account auth is required but unavailable");
195
+ }
196
+ if (mayForwardCallerCredentials) {
197
+ for (const h of FORWARD_HEADERS) {
198
+ const v = incoming?.headers.get(h);
199
+ if (v) {
200
+ if (h === CODEX_RESPONSES_LITE_HEADER) {
201
+ for (const name of Object.keys(headers)) {
202
+ if (name.toLowerCase() === h) delete headers[name];
203
+ }
204
+ }
205
+ headers[h] = v; // …so genuine forwarded fields win.
206
+ }
207
+ }
208
+ }
209
+ const override = runtimeProvider._codexAccountOverride;
210
+ if (override && mayForwardCallerCredentials) {
211
+ headers["authorization"] = `Bearer ${override.accessToken}`;
212
+ headers["chatgpt-account-id"] = override.chatgptAccountId;
213
+ }
214
+ } else {
215
+ if (provider.responsesPath === undefined) {
216
+ url = openaiResponsesUrl(provider.baseUrl);
217
+ } else {
218
+ const base = provider.baseUrl.replace(/\/$/, "");
219
+ url = `${base}${provider.responsesPath}`;
220
+ }
221
+ if (provider.apiKey) headers["Authorization"] = `Bearer ${provider.apiKey}`;
222
+ if (provider.headers) Object.assign(headers, provider.headers);
223
+ }
224
+
225
+ const forward = provider.authMode === "forward";
226
+ let convertedRoutedCustomToolNames: Set<string> | undefined;
227
+ let routedCustomToolRepairNames: Set<string> | undefined;
228
+ let convertedRoutedToolSearchNames: Set<string> | undefined;
229
+ let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
230
+ let plaintextV2AgentMessageToolNames: ReadonlySet<string> | undefined;
231
+ let plaintextV2AgentMessageAliasedToolNames: ReadonlySet<string> | undefined;
232
+ let convertedMuseToolNameAliases: Map<string, string> | undefined;
233
+ const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
234
+ let outBody = stripPreviousResponseId(
235
+ parsed._rawBody,
236
+ forward || parsed._previousResponseInputExpanded === true,
237
+ );
238
+ if (!forward) outBody = normalizeRoutedAgentMessages(outBody, {
239
+ allowStringContent: isXaiResponsesDestination(provider),
240
+ });
241
+ outBody = mapRoutedResponsesReasoningEffort(outBody, provider, parsed.modelId);
242
+ // stripPreviousResponseId() intentionally returns its input on a no-op. Detach before the
243
+ // tier write so a force-fast/default decision can never mutate parsed._rawBody.
244
+ outBody = applyTierDecisionToResponsesBody(outBody, parsed.options?.tierDecision);
245
+ const stateless = provider.statelessResponses === true;
246
+ if (stateless) outBody = stripStatefulResponsesParams(outBody);
247
+ // A replay miss can leave a function_call_output whose paired function_call sat
248
+ // in the prefix that was never expanded. A stateless upstream cannot resolve the
249
+ // pair from its own storage either, so it needs the same repair the forward
250
+ // backend gets — dropping previous_response_id is not much use if the body that
251
+ // reaches the wire is unparseable.
252
+ if (provider.annotateEmptyToolOutputs === true) {
253
+ outBody = annotateEmptyResponsesToolOutputs(outBody, true);
254
+ }
255
+ if (forward || stateless) {
256
+ outBody = repairOrphanedInputItems(outBody, unexpandedMiss, stateless && !forward);
257
+ }
258
+ if (provider.requiresAdjacentResponsesToolResults === true) {
259
+ outBody = normalizeResponsesToolResultAdjacency(outBody);
260
+ }
261
+ if (forward) {
262
+ outBody = stripUnsupportedForwardParams(outBody);
263
+ // Only the canonical ChatGPT backend rejects the retired field; a self-hosted or
264
+ // third-party forward gateway may still accept it, so this must not be widened.
265
+ if (isCanonicalOpenAiForwardProvider(provider)) {
266
+ outBody = stripCanonicalForwardSamplingParams(outBody);
267
+ outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
268
+ outBody = stripCanonicalForwardPromptCacheOptions(outBody);
269
+ outBody = normalizeCanonicalForwardPromptEnvelope(outBody);
270
+ outBody = normalizeCanonicalForwardContinuationEnvelope(outBody);
271
+ }
272
+ } else {
273
+ outBody = preferConfiguredHostedTools(
274
+ outBody,
275
+ provider,
276
+ parsed.modelId,
277
+ parsed._openAiVirtualSelectedModelId,
278
+ );
279
+ outBody = normalizeImageGenClientTools(outBody);
280
+ }
281
+ if (forward || parsed._previousResponseInputExpanded === true) {
282
+ outBody = repairOversizedReplayCallIds(outBody);
283
+ }
284
+ outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId);
285
+ // Repair stored history from before the bridge emitted both keys, in either
286
+ // direction: a conversation that already recorded a web_search_call replays it
287
+ // every turn, and a strict parser rejects the whole request over the missing key —
288
+ // `queries` for DeepSeek (#930), `query` for Console Go (#3071).
289
+ outBody = backfillWebSearchQueries(outBody);
290
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
291
+ outBody = stripInternalChatMessageMetadataPassthrough(outBody);
292
+ outBody = promoteClientLoadedTools(outBody);
293
+ }
294
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
295
+ const rewritten = rewriteRoutedCustomToolsForUpstream(
296
+ outBody,
297
+ provider.supportsResponsesCustomTools,
298
+ );
299
+ outBody = rewritten.body;
300
+ convertedRoutedCustomToolNames = rewritten.names;
301
+ routedCustomToolRepairNames = rewritten.repairNames;
302
+ }
303
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
304
+ // Run after custom-tool lowering so the search compatibility layer can choose a
305
+ // collision-free public function name against the final routed function catalog.
306
+ const rewritten = rewriteRoutedToolSearchForUpstream(outBody);
307
+ outBody = rewritten.body;
308
+ convertedRoutedToolSearchNames = rewritten.names;
309
+ }
310
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
311
+ // Codex 0.147 emits private namespace tool groups, while public/third-party Responses
312
+ // gateways accept only flat tool variants. Run after custom/tool-search lowering so
313
+ // namespace children already carry their final public kind before they are promoted.
314
+ const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody, convertedRoutedCustomToolNames);
315
+ outBody = rewritten.body;
316
+ convertedRoutedNamespaceToolAliases = rewritten.aliases;
317
+ // Preserve xAI's cached-only fail-closed semantics and image-search mapping before the
318
+ // generic capability fallback removes the private OpenAI fields.
319
+ outBody = normalizeXaiResponsesWebSearch(outBody, provider);
320
+ outBody = injectXaiResponsesXSearch(outBody, provider, parsed._replayPrefixLen);
321
+ // xAI and explicitly classified compatible gateways reject these OpenAI web_search
322
+ // extensions. Keep them for OpenAI API-key traffic and unclassified gateways.
323
+ if (provider.supportsOpenAiWebSearchToolFields === false) {
324
+ outBody = stripOpenAiOnlyWebSearchFields(outBody);
325
+ }
326
+ outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url);
327
+ // Host-only: api.meta.ai rejects function names over 64 chars on every Muse model,
328
+ // including default muse-spark-1.3. Do not reuse the contributor/Zen web_search
329
+ // predicates. Namespace flattening has already produced the public wire names.
330
+ if (isMetaAiResponsesDestination(url)) {
331
+ const rewritten = rewriteMuseToolNamesForUpstream(outBody);
332
+ outBody = rewritten.body;
333
+ convertedMuseToolNameAliases = rewritten.aliases;
334
+ }
335
+ // Last, so promoted namespace children are also cleared of Codex-private fields.
336
+ outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
337
+ }
338
+ if (!forward) outBody = normalizeOpenCodeGoAdditionalTools(outBody, url);
339
+ // Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
340
+ // let a noncanonical custom forward provider skip this rewrite while the server still routes
341
+ // it as a summarizer turn (#422). The compaction body build removes the tool surface and must
342
+ // therefore be the last routed transform that may depend on those declarations. Structural
343
+ // sanitizers below can still run after it.
344
+ outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
345
+ if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
346
+ outBody = buildRoutedCompactionBody(outBody);
347
+ }
348
+ // Run after routed compaction so nested input_image parts are replaced before a malformed
349
+ // tool output is flattened to text and can no longer be inspected structurally.
350
+ outBody = repairUnidentifiedToolOutputItems(outBody);
351
+ if (parsed._plaintextV2AgentMessages === true && isCanonicalOpenAiForwardProvider(provider)) {
352
+ const prepared = preparePlaintextV2AgentMessages(outBody);
353
+ outBody = prepared.body;
354
+ if (prepared.namespaceAliased) {
355
+ plaintextV2AgentMessageToolNames = prepared.toolNames;
356
+ plaintextV2AgentMessageAliasedToolNames = prepared.aliasedAgentMessageToolNames;
357
+ }
358
+ }
359
+ const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
360
+ const sanitizedBody = normalizeToolSchemas(
361
+ stripItemIdsWhenUnstored(
362
+ stripInvalidItemIds(
363
+ stripUnsupportedHostedTools(
364
+ sanitizeReasoningInputContent(
365
+ scrubOcxCompactionItems(
366
+ outBody,
367
+ destinationDecodesNativeCompactionBlob(provider),
368
+ threadServingIdentityChanged,
369
+ ),
370
+ {
371
+ preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
372
+ dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
373
+ stripEncryptedContent: threadServingIdentityChanged,
374
+ },
375
+ ),
376
+ provider,
377
+ ),
378
+ ),
379
+ ),
380
+ isXaiSchemaTarget(provider),
381
+ );
382
+ const unnormalizedBody = stripDisabledVerbosity(
383
+ stripDisabledReasoningSummaries(
384
+ normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
385
+ provider,
386
+ parsed.modelId,
387
+ ),
388
+ provider,
389
+ parsed.modelId,
390
+ );
391
+ // Normalize the wire model before deriving model-dependent transport metadata.
392
+ const finalBody =
393
+ provider.modelSuffixBracketStrip
394
+ && unnormalizedBody !== null
395
+ && typeof unnormalizedBody === "object"
396
+ && !Array.isArray(unnormalizedBody)
397
+ && typeof (unnormalizedBody as { model?: unknown }).model === "string"
398
+ ? { ...(unnormalizedBody as Record<string, unknown>), model: stripBracketedModelSuffix((unnormalizedBody as { model: string }).model) }
399
+ : unnormalizedBody;
400
+ if (isCanonicalOpenAiForwardProvider(provider)) {
401
+ const routingHeaders = new Headers(headers);
402
+ applyCodexRoutingHint(routingHeaders, finalBody);
403
+ // Static headers may use mixed casing. Remove every stale spelling
404
+ // without normalizing unrelated headers returned by this adapter.
405
+ for (const name of Object.keys(headers)) {
406
+ if (name.toLowerCase() === CODEX_ROUTING_HINT_HEADER) delete headers[name];
407
+ }
408
+ const hint = routingHeaders.get(CODEX_ROUTING_HINT_HEADER);
409
+ if (hint !== null) headers[CODEX_ROUTING_HINT_HEADER] = hint;
410
+ }
411
+ const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
412
+ ? finalBody.service_tier
413
+ : null;
414
+ const tierLog = createAdapterTierMetadata(
415
+ parsed.options?.tierObservation,
416
+ parsed.options?.tierDecision,
417
+ actualServiceTier === null ? null : "service-tier",
418
+ actualServiceTier,
419
+ );
420
+ // The Responses adapter is passthrough: it forwards `parsed._rawBody` rather than
421
+ // rebuilding the body from `parsed.modelId`, and the router writes the routed id into
422
+ // that raw body. So a provider whose upstream rejects bracketed ids has to be honoured
423
+ // here, on the serialized body, not on the parsed selector. One place covers both the
424
+ // HTTP and the WebSocket outbound, because the WS path transports this same request
425
+ // instead of rebuilding it.
426
+ const body = JSON.stringify(finalBody);
427
+ const releaseBodyObservation = translatorBudget.observeExternallyCapped(
428
+ "passthrough_serialization",
429
+ Buffer.byteLength(body, "utf8"),
430
+ );
431
+ return {
432
+ url,
433
+ method: "POST",
434
+ headers,
435
+ body,
436
+ releaseBodyObservation,
437
+ ...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
438
+ ...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
439
+ ...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
440
+ ...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
441
+ ...(plaintextV2AgentMessageToolNames ? { plaintextV2AgentMessageToolNames } : {}),
442
+ ...(plaintextV2AgentMessageAliasedToolNames ? { plaintextV2AgentMessageAliasedToolNames } : {}),
443
+ ...(convertedMuseToolNameAliases ? { convertedMuseToolNameAliases } : {}),
444
+ ...(tierLog ? { tierLog } : {}),
445
+ };
446
+ },
447
+
448
+ // The passthrough normally relays the upstream stream verbatim and never parses.
449
+ // The exception is a routed compaction turn: the server drives this adapter like
450
+ // an ordinary one so the bridge can build the single compaction item (#422).
451
+ async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
452
+ if (!response.body) {
453
+ yield { type: "error", message: "passthrough adapter received no response body" };
454
+ return;
455
+ }
456
+ let deltas = "";
457
+ let deltasBytes = 0;
458
+ let deltasLastCodeUnit = 0;
459
+ let doneText = "";
460
+ let doneTextBytes = 0;
461
+ let doneTextLastCodeUnit = 0;
462
+ let snapshot = "";
463
+ let snapshotBytes = 0;
464
+ let usage: OcxUsage | undefined;
465
+ let usageRawBytes = 0;
466
+ let compactionEncryptedContent: string | undefined;
467
+ let compactionEncryptedContentBytes = 0;
468
+ let completedSeen = false;
469
+ for await (const event of decodeServerSentEvents(response.body, { translatorBudget: budget })) {
470
+ let payload: unknown;
471
+ try { payload = JSON.parse(event.data); } catch { continue; }
472
+ if (!isPlainObject(payload)) continue;
473
+ switch (payload.type) {
474
+ case "response.output_text.delta":
475
+ if (typeof payload.delta === "string") {
476
+ const next = deltas + payload.delta;
477
+ const nextBytes = appendedUtf8Bytes(deltasBytes, deltasLastCodeUnit, payload.delta);
478
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
479
+ deltas = next;
480
+ reservation.commitRetained();
481
+ budget.releaseRetained(deltasBytes, { kind: "retained_collectors" });
482
+ deltasBytes = nextBytes;
483
+ if (payload.delta.length > 0) deltasLastCodeUnit = payload.delta.charCodeAt(payload.delta.length - 1);
484
+ }
485
+ break;
486
+ case "response.output_text.done":
487
+ if (typeof payload.text === "string") {
488
+ const next = doneText + payload.text;
489
+ const nextBytes = appendedUtf8Bytes(doneTextBytes, doneTextLastCodeUnit, payload.text);
490
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
491
+ doneText = next;
492
+ reservation.commitRetained();
493
+ budget.releaseRetained(doneTextBytes, { kind: "retained_collectors" });
494
+ doneTextBytes = nextBytes;
495
+ if (payload.text.length > 0) doneTextLastCodeUnit = payload.text.charCodeAt(payload.text.length - 1);
496
+ }
497
+ break;
498
+ case "response.failed":
499
+ case "error":
500
+ yield { type: "error", message: responsesErrorMessage(payload.response ?? payload) };
501
+ return;
502
+ case "response.incomplete":
503
+ yield { type: "incomplete", reason: responsesErrorMessage(payload.response ?? payload) };
504
+ return;
505
+ case "response.completed":
506
+ {
507
+ completedSeen = true;
508
+ const responsePayload = isPlainObject(payload.response) ? payload.response : undefined;
509
+ const output = Array.isArray(responsePayload?.output) ? responsePayload.output : [];
510
+ const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
511
+ if (isPlainObject(compaction) && typeof compaction.encrypted_content === "string") {
512
+ const nextEncryptedContent = compaction.encrypted_content;
513
+ const nextEncryptedContentBytes = Buffer.byteLength(nextEncryptedContent, "utf8");
514
+ const reservation = budget.reserveTransient(nextEncryptedContentBytes, { kind: "retained_collectors" });
515
+ compactionEncryptedContent = nextEncryptedContent;
516
+ reservation.commitRetained();
517
+ budget.releaseRetained(compactionEncryptedContentBytes, { kind: "retained_collectors" });
518
+ compactionEncryptedContentBytes = nextEncryptedContentBytes;
519
+ }
520
+ const next = responsesPayloadText(payload.response);
521
+ const nextBytes = Buffer.byteLength(next, "utf8");
522
+ const reservation = budget.reserveTransient(nextBytes, { kind: "retained_collectors" });
523
+ snapshot = next;
524
+ reservation.commitRetained();
525
+ budget.releaseRetained(snapshotBytes, { kind: "retained_collectors" });
526
+ snapshotBytes = nextBytes;
527
+ }
528
+ {
529
+ const nextUsage = usageFromResponsesPayload(payload.response);
530
+ // The attached raw usage object can be event-sized (unknown keys carry arbitrary
531
+ // values); it stays reachable until the terminal yields, so charge it like the
532
+ // adjacent retained collectors or it would defeat the per-request memory cap.
533
+ const nextRawBytes = nextUsage?.rawUsage === undefined ? 0
534
+ : Buffer.byteLength(JSON.stringify(nextUsage.rawUsage), "utf8");
535
+ if (nextRawBytes > 0) {
536
+ const reservation = budget.reserveTransient(nextRawBytes, { kind: "retained_collectors" });
537
+ usage = nextUsage;
538
+ reservation.commitRetained();
539
+ } else {
540
+ usage = nextUsage;
541
+ }
542
+ if (usageRawBytes > 0) {
543
+ budget.releaseRetained(usageRawBytes, { kind: "retained_collectors" });
544
+ }
545
+ usageRawBytes = nextRawBytes;
546
+ }
547
+ break;
548
+ }
549
+ // Buffered text is still upstream progress, but gateway keepalives are not.
550
+ // Yield after accounting, directly to the consumer: no progress queue or content leak.
551
+ if (
552
+ !completedSeen
553
+ && (payload.type === "response.output_text.delta"
554
+ || payload.type === "response.reasoning_summary_text.delta"
555
+ || payload.type === "response.reasoning_text.delta")
556
+ && typeof payload.delta === "string"
557
+ && payload.delta.length > 0
558
+ ) {
559
+ yield { type: "heartbeat" };
560
+ }
561
+ }
562
+ // Gateways differ in which of these they emit; prefer the authoritative
563
+ // completed snapshot so text is never double-counted.
564
+ const text = snapshot || doneText || deltas;
565
+ if (text) yield { type: "text_delta", text };
566
+ budget.releaseRetained(
567
+ deltasBytes + doneTextBytes + snapshotBytes + usageRawBytes,
568
+ { kind: "retained_collectors" },
569
+ );
570
+ yield {
571
+ type: "done",
572
+ ...(usage ? { usage } : {}),
573
+ ...(compactionEncryptedContent ? { compactionEncryptedContent } : {}),
574
+ };
575
+ },
576
+
577
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
578
+ let payload: unknown;
579
+ try { payload = await response.json(); } catch {
580
+ return [{ type: "error", message: "malformed upstream compaction response" }];
581
+ }
582
+ budget.chargeRetained(Buffer.byteLength(JSON.stringify(payload), "utf8"), { kind: "retained_collectors" });
583
+ if (!isPlainObject(payload)) {
584
+ return [{ type: "error", message: "malformed upstream compaction response" }];
585
+ }
586
+ if (payload.error || payload.status === "failed") {
587
+ return [{ type: "error", message: responsesErrorMessage(payload) }];
588
+ }
589
+ if (payload.status === "incomplete") {
590
+ return [{ type: "incomplete", reason: responsesErrorMessage(payload) }];
591
+ }
592
+ const usage = usageFromResponsesPayload(payload);
593
+ const output = Array.isArray(payload.output) ? payload.output : [];
594
+ const compaction = output.find(item => isPlainObject(item) && item.type === "compaction");
595
+ const compactionEncryptedContent = isPlainObject(compaction) && typeof compaction.encrypted_content === "string"
596
+ ? compaction.encrypted_content
597
+ : undefined;
598
+ const text = responsesPayloadText(payload);
599
+ if (!text && !compactionEncryptedContent) {
600
+ // A completed turn with neither text nor a native compaction blob cannot become a
601
+ // replacement-history item. A ciphertext-only native completion is valid, though.
602
+ return [{ type: "error", message: "upstream compaction returned no summary text" }];
603
+ }
604
+ return [...(text ? [{ type: "text_delta" as const, text }] : []), {
605
+ type: "done",
606
+ ...(usage ? { usage } : {}),
607
+ ...(compactionEncryptedContent ? { compactionEncryptedContent } : {}),
608
+ }];
609
+ },
610
+ };
611
+ }