@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -225,7 +225,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
225
225
 
226
226
  // Let the base adapter build the wire body (handles reasoning, tools, etc.)
227
227
  // but override the URL and headers after.
228
- const baseReq = base.buildRequest(parsed, incoming) as AdapterRequest;
228
+ const baseReq = await base.buildRequest(parsed, incoming);
229
229
  const baseBody = JSON.parse(baseReq.body as string) as unknown;
230
230
  const markedBody = injectMimoSystemMarker(baseBody);
231
231
 
@@ -0,0 +1,101 @@
1
+ import { parseDataUrl } from "./image";
2
+ import {
3
+ normalizeImageTargets,
4
+ type NormalizeOptions,
5
+ type NormalizeTarget,
6
+ } from "./anthropic-image-normalize";
7
+
8
+ /**
9
+ * Best-effort base64 image budget for translated Chat requests. This leaves room for
10
+ * other request fields but is not a guarantee that the complete body fits an upstream
11
+ * limit. Remote URLs are never fetched by request construction.
12
+ */
13
+ export const OPENAI_CHAT_IMAGE_BASE64_BUDGET = 3_670_016; // 3.5MiB
14
+
15
+ export interface NormalizeOpenAIChatImagesOptions
16
+ extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {}
17
+
18
+ /** Whether `value` is a plain object, so message and part shapes can be walked safely. */
19
+ function isRecord(value: unknown): value is Record<string, unknown> {
20
+ return value !== null && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ /**
24
+ * Walk every well-formed `image_url` part in a Chat Completions message array, ignoring
25
+ * malformed shapes rather than throwing on them. Returning false from `visit` stops the walk.
26
+ */
27
+ function forEachImagePart(
28
+ messages: unknown,
29
+ visit: (imageUrl: Record<string, unknown>, url: string) => boolean | void,
30
+ ): void {
31
+ if (!Array.isArray(messages)) return;
32
+ for (const message of messages) {
33
+ if (!isRecord(message) || !Array.isArray(message.content)) continue;
34
+ for (const part of message.content) {
35
+ if (!isRecord(part) || part.type !== "image_url" || !isRecord(part.image_url)) continue;
36
+ const imageUrl = part.image_url;
37
+ if (typeof imageUrl.url !== "string") continue;
38
+ if (visit(imageUrl, imageUrl.url) === false) return;
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Whether this turn carries inline image bytes worth normalizing. The adapter uses this
45
+ * to stay synchronous for text-only turns, which is every turn on most providers.
46
+ */
47
+ export function hasShrinkableOpenAIChatImages(messages: unknown): boolean {
48
+ let total = 0;
49
+ let found = false;
50
+ forEachImagePart(messages, (_imageUrl, url) => {
51
+ const source = parseDataUrl(url);
52
+ if (!source) return;
53
+ total += source.base64.length;
54
+ if (total > OPENAI_CHAT_IMAGE_BASE64_BUDGET) {
55
+ found = true;
56
+ return false;
57
+ }
58
+ });
59
+ return found;
60
+ }
61
+
62
+ /**
63
+ * Normalize image_url parts in already-built Chat Completions messages, in place.
64
+ *
65
+ * The drop callback deliberately keeps the original URL. The shared normalizer calls
66
+ * drop for corrupt or decode-bomb inputs, and this wire has no downstream guard that
67
+ * would re-attach a dropped image, so dropping here would silently lose a user's
68
+ * screenshot. Terminal-size overflow uses overflowAction "none" for the same reason:
69
+ * an image floored at 320px stays attached rather than being removed.
70
+ */
71
+ export async function normalizeOpenAIChatImages(
72
+ messages: unknown,
73
+ options: NormalizeOpenAIChatImagesOptions = {},
74
+ ): Promise<void> {
75
+ const targets: NormalizeTarget[] = [];
76
+ forEachImagePart(messages, (imageUrl, url) => {
77
+ const source = parseDataUrl(url);
78
+ if (!source) return;
79
+ targets.push({
80
+ base64: source.base64,
81
+ mediaType: source.mediaType,
82
+ replace: (data: string, mediaType: string) => {
83
+ imageUrl.url = `data:${mediaType};base64,${data}`;
84
+ },
85
+ drop: () => {
86
+ // Preserve the original image URL when it cannot be normalized.
87
+ },
88
+ // The drop above is a no-op, so these bytes are still on the wire and must keep
89
+ // counting against the budget. Without this the core would stop counting them and
90
+ // the demotion loop could stop early, shipping a body that is still oversized.
91
+ retainsBytesOnDrop: true,
92
+ });
93
+ });
94
+ if (targets.length === 0) return;
95
+
96
+ await normalizeImageTargets(targets, {
97
+ budget: OPENAI_CHAT_IMAGE_BASE64_BUDGET,
98
+ overflowAction: "none",
99
+ ...options,
100
+ });
101
+ }
@@ -1,7 +1,9 @@
1
- import type { AdapterRequest, ProviderAdapter } from "./base";
1
+ import { hasShrinkableOpenAIChatImages, normalizeOpenAIChatImages } from "./openai-chat-images";
2
+ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
2
3
  import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall, OcxUsage } from "../types";
3
4
  import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
4
5
  import { mapReasoningEffort, modelRecordValue } from "../reasoning-effort";
6
+ import { registryEntryForProviderDestination } from "../providers/registry";
5
7
  import { debugProviderDiagnostic } from "../lib/debug";
6
8
  import { sseFieldValue } from "../lib/sse-decoder";
7
9
  import { isDebugEnabled } from "../lib/debug-settings";
@@ -130,6 +132,10 @@ export function buildOpenAIChatPassthroughRequest(
130
132
  for (const field of CHAT_PASSTHROUGH_FIELDS) {
131
133
  if (rawBody[field] !== undefined) body[field] = rawBody[field];
132
134
  }
135
+ const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
136
+ if (modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0) {
137
+ delete body.reasoning_effort;
138
+ }
133
139
 
134
140
  const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
135
141
  if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
@@ -204,7 +210,7 @@ export function buildOpenAIChatPassthroughRequest(
204
210
  messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
205
211
  toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
206
212
  hasCredential,
207
- bodyBytes: new TextEncoder().encode(bodyJson).length,
213
+ bodyBytes: Buffer.byteLength(bodyJson, "utf8"),
208
214
  });
209
215
  }
210
216
 
@@ -733,10 +739,14 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
733
739
  };
734
740
 
735
741
  const nativeOpenAI = isNativeOpenAIChatTarget(provider);
742
+ // Hoisting a newly appended reminder rewrites the reusable prompt prefix.
743
+ // Keep this compatibility exception on the destination/model tested with OCG.
744
+ const chronologicalSystem = parsed.modelId === "deepseek-v4.1-flash"
745
+ && registryEntryForProviderDestination(provider)?.id === "opencode-go";
736
746
  const toolCatalogNudge = shouldInjectNonOpenAIToolCatalogNudge(provider)
737
747
  ? buildNonOpenAIToolCatalogNudgeForTools(context.tools, options.toolChoice)
738
748
  : undefined;
739
- const developerSystemParts = nativeOpenAI
749
+ const developerSystemParts = nativeOpenAI || chronologicalSystem
740
750
  ? []
741
751
  : context.messages
742
752
  .map(developerSystemText)
@@ -762,11 +772,15 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
762
772
  const hasImages = parts?.some(p => p.type === "image") ?? false;
763
773
  let chatMsg: Record<string, unknown>;
764
774
  if (msg.role === "developer" && !hasImages) {
765
- if (!nativeOpenAI) break;
775
+ if (!nativeOpenAI && !chronologicalSystem) break;
766
776
  const text = typeof msg.content === "string"
767
777
  ? msg.content
768
778
  : parts!.map(p => (p as OcxTextContent).text).join("");
769
- chatMsg = { role: "developer", content: text };
779
+ // A non-text timeline part (video, for example) serializes to nothing here.
780
+ // The generic path drops such a message; the chronological exception must not
781
+ // turn it into an empty system message that some upstreams reject.
782
+ if (!nativeOpenAI && text.length === 0) break;
783
+ chatMsg = { role: nativeOpenAI ? "developer" : "system", content: text };
770
784
  } else if (typeof msg.content === "string") {
771
785
  chatMsg = { role: "user", content: msg.content };
772
786
  } else if (!hasImages) {
@@ -1462,206 +1476,212 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
1462
1476
 
1463
1477
  formatErrorBody: formatOpenAIChatErrorBody,
1464
1478
 
1465
- buildRequest(parsed: OcxParsedRequest) {
1479
+ buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta) {
1466
1480
  lastRequestedModelId = parsed.modelId;
1467
1481
  const { url, headers, hasCredential } = openAIChatTransport(provider);
1468
1482
  const messages = frameAgentRouterMessages(provider.baseUrl, messagesToChatFormat(parsed, provider));
1469
- const tools = toolsToChatFormatForProvider(parsed, provider);
1470
- const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
1483
+ const finish = (): AdapterRequest => {
1484
+ const tools = toolsToChatFormatForProvider(parsed, provider);
1485
+ const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools, provider);
1471
1486
 
1472
- const body: Record<string, unknown> = {
1473
- model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
1474
- messages,
1475
- stream: parsed.stream,
1476
- };
1477
- // A policy-produced canonical decision has already passed capability validation. Without
1478
- // that decision, a canonical caller value still requires an explicit true capability;
1479
- // unclassified Chat routes remain behind the caller-forwarding opt-in.
1480
- const serviceTier = parsed.options.serviceTier;
1481
- const tierDecision = parsed.options.tierDecision;
1482
- const canSerializeServiceTier = canSerializeOpenAIChatServiceTier(
1483
- provider,
1484
- parsed.modelId,
1485
- serviceTier,
1486
- tierDecision,
1487
- );
1488
- if (canSerializeServiceTier && serviceTier !== undefined) {
1489
- body.service_tier = serviceTier;
1490
- }
1491
- if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
1492
- const maxTokens = resolveMaxTokens(provider, parsed);
1493
- const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
1494
- if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
1495
- const vercelRouting = resolveVercelGatewayRouting(provider, parsed.modelId);
1496
- if (vercelRouting) body.provider = vercelGatewayProviderPayload(vercelRouting);
1497
- if (tools) body.tools = tools;
1498
- if (tools && toolChoice !== undefined) {
1499
- body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
1500
- ? (toolChoice === "none" ? "none" : "auto")
1501
- : toolChoice;
1502
- }
1503
- if (maxTokens !== undefined) body.max_tokens = maxTokens;
1504
- if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
1505
- body.temperature = parsed.options.temperature;
1506
- }
1507
- if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
1508
- body.top_p = parsed.options.topP;
1509
- }
1510
- if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
1511
- const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
1512
- // Some gateways accept a reasoning-effort field on a plain turn but reject the
1513
- // effort + tools combination. `noReasoningModels` would fix that only by
1514
- // stripping reasoning everywhere, costing the model its whole picker. This keeps
1515
- // the ladder advertised and drops the wire field for tool-bearing requests only.
1516
- const omitReasoningEffortWithTools = !!tools
1517
- && modelInList(provider.omitReasoningEffortWithToolsModels, parsed.modelId);
1518
- const reasoningEffort = omitReasoningEffortWithTools
1519
- ? undefined
1520
- : mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
1521
- const nativeOpenAI = isNativeOpenAIChatTarget(provider);
1522
- let reasoningLog: AdapterRequest["reasoningLog"];
1523
- if (!reasoningDisabled && !omitReasoningEffortWithTools && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
1524
- if (nativeOpenAI) {
1525
- body.reasoning_effort = "none";
1526
- reasoningLog = {
1527
- effectiveEffort: "none",
1528
- wireField: "reasoning_effort",
1529
- wireValue: "none",
1530
- };
1531
- } else {
1532
- body.reasoning = { enabled: false };
1533
- reasoningLog = {
1534
- effectiveEffort: "none",
1535
- wireField: "reasoning.enabled",
1536
- wireValue: false,
1537
- };
1487
+ const body: Record<string, unknown> = {
1488
+ model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(parsed.modelId) : parsed.modelId,
1489
+ messages,
1490
+ stream: parsed.stream,
1491
+ };
1492
+ // A policy-produced canonical decision has already passed capability validation. Without
1493
+ // that decision, a canonical caller value still requires an explicit true capability;
1494
+ // unclassified Chat routes remain behind the caller-forwarding opt-in.
1495
+ const serviceTier = parsed.options.serviceTier;
1496
+ const tierDecision = parsed.options.tierDecision;
1497
+ const canSerializeServiceTier = canSerializeOpenAIChatServiceTier(
1498
+ provider,
1499
+ parsed.modelId,
1500
+ serviceTier,
1501
+ tierDecision,
1502
+ );
1503
+ if (canSerializeServiceTier && serviceTier !== undefined) {
1504
+ body.service_tier = serviceTier;
1505
+ }
1506
+ if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
1507
+ const maxTokens = resolveMaxTokens(provider, parsed);
1508
+ const openRouterRouting = resolveOpenRouterRouting(provider, parsed.modelId);
1509
+ if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
1510
+ const vercelRouting = resolveVercelGatewayRouting(provider, parsed.modelId);
1511
+ if (vercelRouting) body.provider = vercelGatewayProviderPayload(vercelRouting);
1512
+ if (tools) body.tools = tools;
1513
+ if (tools && toolChoice !== undefined) {
1514
+ body.tool_choice = modelInList(provider.autoToolChoiceOnlyModels, parsed.modelId)
1515
+ ? (toolChoice === "none" ? "none" : "auto")
1516
+ : toolChoice;
1517
+ }
1518
+ if (maxTokens !== undefined) body.max_tokens = maxTokens;
1519
+ if (parsed.options.temperature !== undefined && !modelInList(provider.noTemperatureModels, parsed.modelId)) {
1520
+ body.temperature = parsed.options.temperature;
1538
1521
  }
1539
- } else if (reasoningEffort !== undefined) {
1540
- if (provider.reasoningWireFormat === "gateway-object") {
1522
+ if (parsed.options.topP !== undefined && !modelInList(provider.noTopPModels, parsed.modelId)) {
1523
+ body.top_p = parsed.options.topP;
1524
+ }
1525
+ if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
1526
+ const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
1527
+ // Some gateways accept a reasoning-effort field on a plain turn but reject the
1528
+ // effort + tools combination. `noReasoningModels` would fix that only by
1529
+ // stripping reasoning everywhere, costing the model its whole picker. This keeps
1530
+ // the ladder advertised and drops the wire field for tool-bearing requests only.
1531
+ const omitReasoningEffortWithTools = !!tools
1532
+ && modelInList(provider.omitReasoningEffortWithToolsModels, parsed.modelId);
1533
+ const reasoningEffort = omitReasoningEffortWithTools
1534
+ ? undefined
1535
+ : mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
1536
+ const nativeOpenAI = isNativeOpenAIChatTarget(provider);
1537
+ let reasoningLog: AdapterRequest["reasoningLog"];
1538
+ if (!reasoningDisabled && !omitReasoningEffortWithTools && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
1541
1539
  if (nativeOpenAI) {
1542
- body.reasoning_effort = reasoningEffort;
1540
+ body.reasoning_effort = "none";
1543
1541
  reasoningLog = {
1544
- effectiveEffort: reasoningEffort,
1542
+ effectiveEffort: "none",
1545
1543
  wireField: "reasoning_effort",
1546
- wireValue: reasoningEffort,
1544
+ wireValue: "none",
1547
1545
  };
1548
1546
  } else {
1549
- body.reasoning = { enabled: true, effort: reasoningEffort };
1550
- reasoningLog = {
1551
- effectiveEffort: reasoningEffort,
1552
- wireField: "reasoning.effort",
1553
- wireValue: reasoningEffort,
1554
- };
1555
- }
1556
- } else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
1557
- const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
1558
- if (budget !== undefined) {
1559
- body.thinking_budget = budget;
1547
+ body.reasoning = { enabled: false };
1560
1548
  reasoningLog = {
1561
- effectiveEffort: parsed.options.reasoning === "minimal" ? "minimal" : reasoningEffort,
1562
- wireField: "thinking_budget",
1563
- wireValue: budget,
1549
+ effectiveEffort: "none",
1550
+ wireField: "reasoning.enabled",
1551
+ wireValue: false,
1564
1552
  };
1565
1553
  }
1566
- } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
1567
- if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
1568
- body.thinking = { type: reasoningEffort };
1554
+ } else if (reasoningEffort !== undefined) {
1555
+ if (provider.reasoningWireFormat === "gateway-object") {
1556
+ if (nativeOpenAI) {
1557
+ body.reasoning_effort = reasoningEffort;
1558
+ reasoningLog = {
1559
+ effectiveEffort: reasoningEffort,
1560
+ wireField: "reasoning_effort",
1561
+ wireValue: reasoningEffort,
1562
+ };
1563
+ } else {
1564
+ body.reasoning = { enabled: true, effort: reasoningEffort };
1565
+ reasoningLog = {
1566
+ effectiveEffort: reasoningEffort,
1567
+ wireField: "reasoning.effort",
1568
+ wireValue: reasoningEffort,
1569
+ };
1570
+ }
1571
+ } else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
1572
+ const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
1573
+ if (budget !== undefined) {
1574
+ body.thinking_budget = budget;
1575
+ reasoningLog = {
1576
+ effectiveEffort: parsed.options.reasoning === "minimal" ? "minimal" : reasoningEffort,
1577
+ wireField: "thinking_budget",
1578
+ wireValue: budget,
1579
+ };
1580
+ }
1581
+ } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
1582
+ if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
1583
+ body.thinking = { type: reasoningEffort };
1584
+ reasoningLog = {
1585
+ effectiveEffort: reasoningEffort,
1586
+ wireField: "thinking.type",
1587
+ wireValue: reasoningEffort,
1588
+ };
1589
+ }
1590
+ } else {
1591
+ body.reasoning_effort = reasoningEffort;
1569
1592
  reasoningLog = {
1570
1593
  effectiveEffort: reasoningEffort,
1571
- wireField: "thinking.type",
1594
+ wireField: "reasoning_effort",
1572
1595
  wireValue: reasoningEffort,
1573
1596
  };
1574
1597
  }
1575
- } else {
1576
- body.reasoning_effort = reasoningEffort;
1577
- reasoningLog = {
1578
- effectiveEffort: reasoningEffort,
1579
- wireField: "reasoning_effort",
1580
- wireValue: reasoningEffort,
1581
- };
1582
1598
  }
1583
- }
1584
- if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
1585
- body.presence_penalty = parsed.options.presencePenalty;
1586
- }
1587
- if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
1588
- body.frequency_penalty = parsed.options.frequencyPenalty;
1589
- }
1590
- if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
1591
- body.prompt_cache_key = parsed.options.promptCacheKey;
1592
- }
1593
- // Structured-output support varies by the physical upstream model even when one
1594
- // gateway exposes a uniform OpenAI-compatible endpoint. Keep the #1137 translation
1595
- // as the default, but let an exact model opt out instead of forcing a provider-wide
1596
- // rollback that would silently return prose for siblings that support JSON Schema.
1597
- if (!provider.noStructuredOutputModels?.includes(parsed.modelId)) {
1598
- const textFormat = parsed.options.textFormat;
1599
- if (textFormat?.type === "json_object") {
1600
- body.response_format = { type: "json_object" };
1601
- } else if (textFormat?.type === "json_schema") {
1602
- // Same downgrade as the passthrough path: the schema is dropped because the
1603
- // upstream rejects it, but the JSON-mode request itself survives.
1604
- body.response_format = provider.noJsonSchemaModels?.includes(parsed.modelId)
1605
- ? { type: "json_object" }
1606
- : {
1607
- type: "json_schema",
1608
- json_schema: {
1609
- name: textFormat.name ?? "response",
1610
- ...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
1611
- ...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
1612
- ...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
1613
- },
1614
- };
1599
+ if (parsed.options.presencePenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
1600
+ body.presence_penalty = parsed.options.presencePenalty;
1601
+ }
1602
+ if (parsed.options.frequencyPenalty !== undefined && !modelInList(provider.noPenaltyModels, parsed.modelId)) {
1603
+ body.frequency_penalty = parsed.options.frequencyPenalty;
1604
+ }
1605
+ if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
1606
+ body.prompt_cache_key = parsed.options.promptCacheKey;
1607
+ }
1608
+ // Structured-output support varies by the physical upstream model even when one
1609
+ // gateway exposes a uniform OpenAI-compatible endpoint. Keep the #1137 translation
1610
+ // as the default, but let an exact model opt out instead of forcing a provider-wide
1611
+ // rollback that would silently return prose for siblings that support JSON Schema.
1612
+ if (!provider.noStructuredOutputModels?.includes(parsed.modelId)) {
1613
+ const textFormat = parsed.options.textFormat;
1614
+ if (textFormat?.type === "json_object") {
1615
+ body.response_format = { type: "json_object" };
1616
+ } else if (textFormat?.type === "json_schema") {
1617
+ // Same downgrade as the passthrough path: the schema is dropped because the
1618
+ // upstream rejects it, but the JSON-mode request itself survives.
1619
+ body.response_format = provider.noJsonSchemaModels?.includes(parsed.modelId)
1620
+ ? { type: "json_object" }
1621
+ : {
1622
+ type: "json_schema",
1623
+ json_schema: {
1624
+ name: textFormat.name ?? "response",
1625
+ ...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
1626
+ ...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
1627
+ ...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
1628
+ },
1629
+ };
1630
+ }
1615
1631
  }
1616
- }
1617
1632
 
1618
- if (tools) {
1619
- if (provider.parallelToolCalls === false) {
1620
- // NIM documents the Boolean defaulting to false and kimi rejects true; pin the
1621
- // wire bit so Codex cannot opt in via request.options. Other opted-out providers
1622
- // omit the field by default so strict OpenAI-compatible hosts never see an
1623
- // unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
1624
- // emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
1625
- if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
1626
- || provider.pinParallelToolCallsFalse === true) {
1627
- body.parallel_tool_calls = false;
1633
+ if (tools) {
1634
+ if (provider.parallelToolCalls === false) {
1635
+ // NIM documents the Boolean defaulting to false and kimi rejects true; pin the
1636
+ // wire bit so Codex cannot opt in via request.options. Other opted-out providers
1637
+ // omit the field by default so strict OpenAI-compatible hosts never see an
1638
+ // unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
1639
+ // emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
1640
+ if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
1641
+ || provider.pinParallelToolCallsFalse === true) {
1642
+ body.parallel_tool_calls = false;
1643
+ }
1644
+ } else if (provider.parallelToolCalls === true) {
1645
+ body.parallel_tool_calls = parsed.options.parallelToolCalls !== false;
1628
1646
  }
1629
- } else if (provider.parallelToolCalls === true) {
1630
- body.parallel_tool_calls = parsed.options.parallelToolCalls !== false;
1631
1647
  }
1632
- }
1633
- if (parsed.stream) body.stream_options = { include_usage: true };
1634
-
1635
- const bodyJson = JSON.stringify(body);
1636
- const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
1637
- const tierLog = createAdapterTierMetadata(
1638
- parsed.options.tierObservation,
1639
- parsed.options.tierDecision,
1640
- actualServiceTier === null ? null : "service-tier",
1641
- actualServiceTier,
1642
- );
1643
- if (isDebugEnabled()) {
1644
- let host = "upstream";
1645
- try { host = new URL(url).host; } catch { /* keep fallback */ }
1646
- debugProviderDiagnostic("openai-chat", "request", {
1647
- host,
1648
- model: body.model,
1649
- stream: parsed.stream,
1650
- messageCount: Array.isArray(messages) ? messages.length : 0,
1651
- toolCount: Array.isArray(tools) ? tools.length : 0,
1652
- hasCredential,
1653
- bodyBytes: new TextEncoder().encode(bodyJson).length,
1654
- });
1655
- }
1648
+ if (parsed.stream) body.stream_options = { include_usage: true };
1649
+
1650
+ const bodyJson = JSON.stringify(body);
1651
+ const actualServiceTier = typeof body.service_tier === "string" ? body.service_tier : null;
1652
+ const tierLog = createAdapterTierMetadata(
1653
+ parsed.options.tierObservation,
1654
+ parsed.options.tierDecision,
1655
+ actualServiceTier === null ? null : "service-tier",
1656
+ actualServiceTier,
1657
+ );
1658
+ if (isDebugEnabled()) {
1659
+ let host = "upstream";
1660
+ try { host = new URL(url).host; } catch { /* keep fallback */ }
1661
+ debugProviderDiagnostic("openai-chat", "request", {
1662
+ host,
1663
+ model: body.model,
1664
+ stream: parsed.stream,
1665
+ messageCount: Array.isArray(messages) ? messages.length : 0,
1666
+ toolCount: Array.isArray(tools) ? tools.length : 0,
1667
+ hasCredential,
1668
+ bodyBytes: Buffer.byteLength(bodyJson, "utf8"),
1669
+ });
1670
+ }
1656
1671
 
1657
- return {
1658
- url,
1659
- method: "POST",
1660
- headers,
1661
- body: bodyJson,
1662
- ...(reasoningLog ? { reasoningLog } : {}),
1663
- ...(tierLog ? { tierLog } : {}),
1672
+ return {
1673
+ url,
1674
+ method: "POST",
1675
+ headers,
1676
+ body: bodyJson,
1677
+ ...(reasoningLog ? { reasoningLog } : {}),
1678
+ ...(tierLog ? { tierLog } : {}),
1679
+ };
1664
1680
  };
1681
+ if (hasShrinkableOpenAIChatImages(messages)) {
1682
+ return normalizeOpenAIChatImages(messages, { tierBias: incoming?.imageTierBias }).then(finish, finish);
1683
+ }
1684
+ return finish();
1665
1685
  },
1666
1686
 
1667
1687
  async *parseStream(
@@ -2080,7 +2100,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
2080
2100
  if (Object.hasOwn(json, "service_tier")) {
2081
2101
  tierMetadata?.observeResponseServiceTier(json.service_tier);
2082
2102
  }
2083
- const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
2103
+ const responseBytes = Buffer.byteLength(JSON.stringify(json), "utf8");
2084
2104
  budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
2085
2105
  try {
2086
2106
  const payload = unwrapChatCompletionPayload(json);