@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -7,7 +7,8 @@ import { isDebugEnabled } from "../lib/debug-settings";
7
7
  import { isCyberPolicyCode } from "../lib/errors";
8
8
  import { redactSecretString } from "../lib/redact";
9
9
  import { contentPartsToText } from "./image";
10
- import { neutralizeIdentity } from "./identity";
10
+ import { identifyRoutedModel } from "./identity";
11
+ import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
11
12
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
12
13
  import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
13
14
  import {
@@ -70,6 +71,103 @@ function extractErrorDetail(parsed: unknown): string | undefined {
70
71
  return undefined;
71
72
  }
72
73
 
74
+ // ClinePass live responses observed 2026-08-02 wrap non-stream Chat Completions in
75
+ // `{ success, error, data }`; its public Chat Completions docs do not currently describe that
76
+ // envelope. Keep ordinary OpenAI-shaped responses on the direct path.
77
+ function unwrapChatCompletionPayload(json: Record<string, unknown>): Record<string, unknown> {
78
+ if ((json.error !== undefined && json.error !== null) || Array.isArray(json.choices)) return json;
79
+ const data = json.data;
80
+ return data !== null && typeof data === "object" && !Array.isArray(data)
81
+ ? data as Record<string, unknown>
82
+ : json;
83
+ }
84
+
85
+ interface OpenAIChatError {
86
+ message?: unknown;
87
+ code?: unknown;
88
+ type?: unknown;
89
+ status?: unknown;
90
+ metadata?: unknown;
91
+ }
92
+
93
+ function safeUpstreamRequestId(metadata: unknown): string | undefined {
94
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
95
+ const record = metadata as Record<string, unknown>;
96
+ const value = record.request_id ?? record.requestId;
97
+ if (typeof value !== "string") return undefined;
98
+ const requestId = value.trim();
99
+ return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)
100
+ && redactSecretString(requestId) === requestId
101
+ ? requestId
102
+ : undefined;
103
+ }
104
+
105
+ function upstreamErrorEvent(
106
+ error: unknown,
107
+ usage?: OcxUsage,
108
+ ): Extract<AdapterEvent, { type: "error" }> {
109
+ const details = error !== null && typeof error === "object" && !Array.isArray(error)
110
+ ? error as OpenAIChatError
111
+ : undefined;
112
+ const rawMessage = typeof error === "string"
113
+ ? error.trim() || "upstream error"
114
+ : typeof details?.message === "string" ? details.message : "upstream error";
115
+ const safeMessage = redactSecretString(rawMessage);
116
+ const requestId = safeUpstreamRequestId(details?.metadata);
117
+ const message = requestId !== undefined && !safeMessage.includes(requestId)
118
+ ? `${safeMessage} (request ID: ${requestId})`
119
+ : safeMessage;
120
+ const code = typeof details?.code === "string"
121
+ ? details.code
122
+ : typeof details?.code === "number" && Number.isFinite(details.code) && Number.isInteger(details.code)
123
+ ? String(details.code)
124
+ : undefined;
125
+ const errorType = typeof details?.type === "string" ? details.type : undefined;
126
+ const codeStatus = typeof details?.code === "number"
127
+ && Number.isInteger(details.code)
128
+ && details.code >= 100
129
+ && details.code <= 599
130
+ ? details.code
131
+ : undefined;
132
+ const status = isCyberPolicyCode(code)
133
+ ? 400
134
+ : typeof details?.status === "number" && Number.isInteger(details.status)
135
+ ? details.status
136
+ : codeStatus;
137
+ return {
138
+ type: "error",
139
+ message,
140
+ ...(usage !== undefined ? { usage } : {}),
141
+ ...(code !== undefined ? { code } : {}),
142
+ ...(errorType !== undefined ? { errorType } : {}),
143
+ ...(status !== undefined ? { status } : {}),
144
+ };
145
+ }
146
+
147
+ function stopReasonFor(finishReason: unknown): "max_tokens" | "content_filter" | undefined {
148
+ return finishReason === "length"
149
+ ? "max_tokens"
150
+ : finishReason === "content_filter"
151
+ ? "content_filter"
152
+ : undefined;
153
+ }
154
+
155
+ function reasoningTextFrom(record: Record<string, unknown>): string | undefined {
156
+ return typeof record.reasoning_content === "string" && record.reasoning_content.length > 0
157
+ ? record.reasoning_content
158
+ : typeof record.reasoning === "string" && record.reasoning.length > 0
159
+ ? record.reasoning
160
+ : undefined;
161
+ }
162
+
163
+ function invalidChoicesEvent(usage?: OcxUsage): Extract<AdapterEvent, { type: "error" }> {
164
+ return {
165
+ type: "error",
166
+ message: "upstream response contained invalid choices",
167
+ ...(usage !== undefined ? { usage } : {}),
168
+ };
169
+ }
170
+
73
171
  function developerSystemText(message: OcxMessage): string | undefined {
74
172
  if (message.role !== "developer") return undefined;
75
173
  if (typeof message.content === "string") return message.content;
@@ -77,9 +175,30 @@ function developerSystemText(message: OcxMessage): string | undefined {
77
175
  return message.content.map(part => (part as OcxTextContent).text).join("");
78
176
  }
79
177
 
178
+ /**
179
+ * Chat-completions image_url parts for images carried inside a tool result (issue #888). role:"tool"
180
+ * content is text-only on every chat provider, so these ride in a follow-up user message instead of
181
+ * being flattened to the "[image]" marker the model can't actually see. Data URLs and remote https
182
+ * URLs are both valid in image_url.url, unlike Gemini inline_data which needs base64.
183
+ */
184
+ function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[] {
185
+ if (typeof content === "string") return [];
186
+ const parts: unknown[] = [];
187
+ for (const p of content) {
188
+ // Skip parts without a usable URL (the tool-output parser accepts the empty file_id shape):
189
+ // a {"url":""} part would fail the whole request where the "[image]" marker degrades safely.
190
+ if (p.type !== "image" || !p.imageUrl) continue;
191
+ parts.push({ type: "image_url", image_url: { url: p.imageUrl, ...(p.detail ? { detail: p.detail } : {}) } });
192
+ }
193
+ return parts;
194
+ }
195
+
80
196
  function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
81
197
  const out: unknown[] = [];
82
198
  const { context, options } = parsed;
199
+ // Mirror the bridge's replay-cache scope (issue #950): provider call ids are
200
+ // not globally unique, so reasoning must not cross conversation boundaries.
201
+ const replayCacheScope = parsed._clientThreadId ?? "global";
83
202
 
84
203
  // 260718 dangling tool_calls hardening (devlog/_plan/260718_dangling_toolcall_hardening):
85
204
  // strict chat providers (Kimi/Moonshot) 400 when an assistant tool_call is not answered
@@ -91,6 +210,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
91
210
  interface PendingToolCall { id: string; name: string }
92
211
  let pendingToolCalls: PendingToolCall[] = [];
93
212
  let deferredBarrierMessages: unknown[] = [];
213
+ let pendingToolResultImageParts: unknown[] = [];
94
214
  let mintedIdSeq = 0;
95
215
  const seenWireCallIds = new Set<string>();
96
216
 
@@ -109,6 +229,22 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
109
229
  deferredBarrierMessages = [];
110
230
  };
111
231
 
232
+ // Tool-result images collected during the open round land in ONE user vision message once the
233
+ // round closes — never inside it, where strict providers (Kimi/Moonshot) 400 on interleaved
234
+ // user messages. Released before deferred barriers so the images stay adjacent to the results
235
+ // they came from (mirrors google.ts sibling inline_data parts and the Kiro carrier images).
236
+ const flushToolResultImages = (): void => {
237
+ if (pendingToolResultImageParts.length === 0) return;
238
+ out.push({
239
+ role: "user",
240
+ content: [
241
+ { type: "text", text: "[ocx] image output from the preceding tool result(s):" },
242
+ ...pendingToolResultImageParts,
243
+ ],
244
+ });
245
+ pendingToolResultImageParts = [];
246
+ };
247
+
112
248
  // Close an unresolved tool round with explicit unavailable-result messages. The wording
113
249
  // must not claim interruption, success, failure, or user intent: execution status is
114
250
  // UNKNOWN, and for user-input tools this must not read as an answer.
@@ -122,6 +258,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
122
258
  });
123
259
  }
124
260
  pendingToolCalls = [];
261
+ flushToolResultImages();
125
262
  releaseDeferredBarriers();
126
263
  };
127
264
 
@@ -147,7 +284,10 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
147
284
  // base_instructions is ignored at request time). Neutralize that one identity line
148
285
  // so routed, non-OpenAI models don't misreport themselves as GPT-5 / OpenAI — without
149
286
  // leaking the proxy identity into the payload.
150
- const sys = neutralizeIdentity(systemParts.join("\n\n"));
287
+ const wireModelId = provider.modelSuffixBracketStrip
288
+ ? stripBracketedModelSuffix(parsed.modelId)
289
+ : parsed.modelId;
290
+ const sys = identifyRoutedModel(systemParts.join("\n\n"), wireModelId);
151
291
  out.push({ role: "system", content: sys });
152
292
  }
153
293
 
@@ -188,7 +328,26 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
188
328
  if (textParts.length > 0) {
189
329
  chatMsg.content = textParts.map(p => p.text).join("");
190
330
  }
191
- const reasoningContent = thinkingParts.map(p => p.thinking).join("");
331
+ let reasoningContent = thinkingParts.map(p => p.thinking).join("");
332
+ // History transformations (compaction, lost assistant turn, resumed
333
+ // threads) can strip the reasoning item while the tool round survives.
334
+ // Re-attach the reasoning the bridge recorded for these call ids so
335
+ // preserveReasoningContentModels providers (DeepSeek thinking mode)
336
+ // never receive a bare tool-call continuation (issue #950).
337
+ if (
338
+ reasoningContent.length === 0
339
+ && toolCalls.length > 0
340
+ && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
341
+ ) {
342
+ const cached = toolCalls
343
+ .map(tc => (tc.id ? peekReasoningForCall(tc.id, replayCacheScope) : undefined))
344
+ .filter((text): text is string => typeof text === "string" && text.length > 0);
345
+ // Parallel calls share one preceding reasoning block, which is
346
+ // recorded under every call id — join unique texts only.
347
+ if (cached.length > 0) {
348
+ reasoningContent = [...new Set(cached)].join("\n");
349
+ }
350
+ }
192
351
  if (reasoningContent.length > 0 && modelInList(provider.preserveReasoningContentModels, parsed.modelId)) {
193
352
  chatMsg.reasoning_content = reasoningContent;
194
353
  }
@@ -232,8 +391,12 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
232
391
  tool_call_id: toolCallId,
233
392
  content: contentPartsToText(msg.content),
234
393
  });
394
+ pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
235
395
  pendingToolCalls.splice(matchIdx, 1);
236
- if (pendingToolCalls.length === 0) releaseDeferredBarriers();
396
+ if (pendingToolCalls.length === 0) {
397
+ flushToolResultImages();
398
+ releaseDeferredBarriers();
399
+ }
237
400
  } else {
238
401
  if (!toolCallId) toolCallId = `call_orphan_${out.length}`;
239
402
  // No matching call in the open round. Close any unresolved round first so the
@@ -242,9 +405,17 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
242
405
  // role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
243
406
  flushPendingToolCalls();
244
407
  const name = safeToolName(msg.toolName);
408
+ // The orphan repair synthesizes an assistant tool call for a result
409
+ // whose assistant turn was lost; carry the recorded reasoning so the
410
+ // replayed round stays valid for thinking-mode providers (#950).
411
+ const cachedReasoning =
412
+ toolCallId && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
413
+ ? peekReasoningForCall(toolCallId, replayCacheScope)
414
+ : undefined;
245
415
  out.push({
246
416
  role: "assistant",
247
417
  content: emptyAssistantContent(provider),
418
+ ...(cachedReasoning ? { reasoning_content: cachedReasoning } : {}),
248
419
  tool_calls: [{
249
420
  id: toolCallId,
250
421
  type: "function",
@@ -257,6 +428,8 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
257
428
  tool_call_id: toolCallId,
258
429
  content: contentPartsToText(msg.content),
259
430
  });
431
+ pendingToolResultImageParts.push(...toolResultImageChatParts(msg.content));
432
+ flushToolResultImages();
260
433
  }
261
434
  break;
262
435
  }
@@ -370,14 +543,6 @@ function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
370
543
  }
371
544
  }
372
545
 
373
- function isKimiSchemaTarget(provider: OcxProviderConfig): boolean {
374
- try {
375
- return new URL(provider.baseUrl).hostname === "api.kimi.com";
376
- } catch {
377
- return false;
378
- }
379
- }
380
-
381
546
  // Volcengine Ark regional endpoints. Ark validates an assistant message's text field as a
382
547
  // REQUIRED parameter and treats "" as absent, so a tool-call-only assistant in history 400s with
383
548
  // `MissingParameter: input.content.text` (#796). Every other OpenAI-compatible provider accepts
@@ -416,11 +581,16 @@ function emptyAssistantContent(provider: OcxProviderConfig): string | { type: "t
416
581
  }
417
582
 
418
583
  /**
419
- * Kimi requires function.parameters.type to be exactly "object" at the root.
420
- * Codex tools with oneOf/anyOf schemas omit the root type, causing 400 errors.
421
- * Add type: "object" at the root while preserving oneOf, $defs, and other schema keys.
584
+ * Providers like Kimi and DeepSeek reject function parameter schemas whose root
585
+ * `type` is missing or `null` JSON Schema requires `"object"` at the root of
586
+ * function parameters. Add `type: "object"` at the root while preserving
587
+ * `oneOf`, `$defs`, and every other schema key.
588
+ *
589
+ * This mirrors `normalizeFunctionToolSchema` in openai-responses.ts, which
590
+ * applies the same root-only normalization unconditionally on the responses
591
+ * path. Nested schema content is intentionally left untouched.
422
592
  */
423
- function ensureKimiRootObjectType(parameters: unknown): Record<string, unknown> {
593
+ function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
424
594
  if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
425
595
  return { type: "object", properties: {} };
426
596
  }
@@ -471,13 +641,11 @@ function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig
471
641
  : parsed.context.tools;
472
642
  if (tools.length === 0) return undefined;
473
643
  const xaiTarget = isXaiSchemaTarget(provider);
474
- const kimiTarget = isKimiSchemaTarget(provider);
475
644
  const formatted = tools.flatMap(t => {
476
645
  const parameters = xaiTarget
477
646
  ? normalizeXaiToolParameters(t.parameters)
478
- : kimiTarget
479
- ? ensureKimiRootObjectType(t.parameters)
480
- : t.parameters;
647
+ : ensureRootObjectType(t.parameters);
648
+
481
649
  if (parameters === undefined) return [];
482
650
  return [{
483
651
  type: "function",
@@ -588,10 +756,27 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
588
756
  body.top_p = parsed.options.topP;
589
757
  }
590
758
  if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
759
+ const reasoningDisabled = modelInList(provider.noReasoningModels, parsed.modelId);
591
760
  const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
592
761
  let reasoningLog: AdapterRequest["reasoningLog"];
593
- if (reasoningEffort !== undefined) {
594
- if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
762
+ // ClinePass live requests observed 2026-08-02 require this gateway-specific object; the
763
+ // public API docs do not currently specify its request shape.
764
+ if (!reasoningDisabled && provider.reasoningWireFormat === "gateway-object" && parsed.options.reasoning === "none") {
765
+ body.reasoning = { enabled: false };
766
+ reasoningLog = {
767
+ effectiveEffort: "none",
768
+ wireField: "reasoning.enabled",
769
+ wireValue: false,
770
+ };
771
+ } else if (reasoningEffort !== undefined) {
772
+ if (provider.reasoningWireFormat === "gateway-object") {
773
+ body.reasoning = { enabled: true, effort: reasoningEffort };
774
+ reasoningLog = {
775
+ effectiveEffort: reasoningEffort,
776
+ wireField: "reasoning.effort",
777
+ wireValue: reasoningEffort,
778
+ };
779
+ } else if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
595
780
  const budget = thinkingBudgetForEffort(parsed, reasoningEffort, maxTokens);
596
781
  if (budget !== undefined) {
597
782
  body.thinking_budget = budget;
@@ -704,17 +889,28 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
704
889
  interface PendingToolCall { key: string; id: string; name: string; args: string; argsBytes: number }
705
890
  const pendingToolCalls: PendingToolCall[] = [];
706
891
  let toolCallSeq = 0;
892
+ const closeToolCalls = (): PendingToolCall[] => {
893
+ const calls = [...pendingToolCalls];
894
+ for (const call of calls) budget.closeCall(call.key);
895
+ pendingToolCalls.length = 0;
896
+ return calls;
897
+ };
707
898
  const flushToolCalls = function* (): Generator<AdapterEvent> {
708
899
  // Do not treat flushed tool calls as user-facing output for the finish-less EOF
709
900
  // fallback — incomplete tool args must stay on the truncation path.
710
- for (const call of pendingToolCalls) {
901
+ for (const call of closeToolCalls()) {
711
902
  if (!call.id) call.id = `call_${++toolCallSeq}`;
712
903
  yield { type: "tool_call_start", id: call.id, name: call.name };
713
904
  if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
714
905
  yield { type: "tool_call_end" };
715
- budget.closeCall(call.key);
716
906
  }
717
- pendingToolCalls.length = 0;
907
+ };
908
+ const terminateWithError = function* (
909
+ event: Extract<AdapterEvent, { type: "error" }>,
910
+ ): Generator<AdapterEvent, "terminate"> {
911
+ closeToolCalls();
912
+ yield event;
913
+ return "terminate";
718
914
  };
719
915
  let pendingUsage: OcxUsage | undefined;
720
916
  // Track terminal signals so a socket EOF without any terminator can fail closed instead of
@@ -735,11 +931,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
735
931
  const payload = line.slice(6).trim();
736
932
  if (payload === "[DONE]") {
737
933
  yield* flushToolCalls();
738
- const stopReason = finishReason === "length"
739
- ? "max_tokens"
740
- : finishReason === "content_filter"
741
- ? "content_filter"
742
- : undefined;
934
+ const stopReason = stopReasonFor(finishReason);
743
935
  yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
744
936
  return "terminate";
745
937
  }
@@ -755,23 +947,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
755
947
  // A 200/OK chat-completions stream may carry an inline provider error envelope
756
948
  // instead of a clean [DONE]. Surface it as a terminal error so the bridge emits a
757
949
  // classified response.failed (bridge case "error") — never a truncated completion.
758
- if (chunk.error) {
759
- const err = chunk.error as { message?: string; code?: string; type?: string; status?: number } | undefined;
760
- const message = err?.message ?? "upstream error";
761
- debugProviderDiagnostic("openai-chat", "stream-error", { message });
762
- yield* flushToolCalls();
763
- yield {
764
- type: "error",
765
- message,
766
- ...(typeof err?.code === "string" ? { code: err.code } : {}),
767
- ...(typeof err?.type === "string" ? { errorType: err.type } : {}),
768
- ...(isCyberPolicyCode(err?.code)
769
- ? { status: 400 }
770
- : typeof err?.status === "number" && Number.isInteger(err.status)
771
- ? { status: err.status }
772
- : {}),
773
- };
774
- return "terminate";
950
+ if (chunk.error !== undefined && chunk.error !== null) {
951
+ const event = upstreamErrorEvent(chunk.error, pendingUsage);
952
+ debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
953
+ return yield* terminateWithError(event);
775
954
  }
776
955
 
777
956
  if (chunk.usage) {
@@ -781,17 +960,36 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
781
960
  pendingUsage = usageFromOpenAIChat(chunk.usage as Record<string, unknown>);
782
961
  }
783
962
 
784
- const choices = chunk.choices as { delta?: Record<string, unknown>; finish_reason?: string }[] | undefined;
785
- if (!choices || choices.length === 0) return "continue";
963
+ const choices = chunk.choices;
964
+ if (choices === undefined) return "continue";
965
+ if (!Array.isArray(choices)) {
966
+ return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
967
+ }
968
+ if (choices.length === 0) return "continue";
969
+ const rawChoice = choices[0];
970
+ if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
971
+ return yield* terminateWithError(invalidChoicesEvent(pendingUsage));
972
+ }
973
+ const choice = rawChoice as {
974
+ delta?: Record<string, unknown>;
975
+ finish_reason?: string;
976
+ error?: unknown;
977
+ };
978
+ if (choice.finish_reason === "error") {
979
+ const event = upstreamErrorEvent(choice.error, pendingUsage);
980
+ debugProviderDiagnostic("openai-chat", "stream-error", { message: event.message });
981
+ return yield* terminateWithError(event);
982
+ }
786
983
  // Observe the terminator BEFORE the delta guard: a finish-only chunk (finish_reason set,
787
984
  // no delta) is a graceful close and must record finishReason even though we skip it below.
788
- if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
789
- finishReason = choices[0].finish_reason;
985
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
986
+ finishReason = choice.finish_reason;
790
987
  }
791
- const delta = choices[0].delta;
988
+ const delta = choice.delta;
792
989
  if (delta) {
793
- if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
794
- yield { type: "reasoning_raw_delta", text: delta.reasoning_content };
990
+ const reasoningText = reasoningTextFrom(delta);
991
+ if (reasoningText !== undefined) {
992
+ yield { type: "reasoning_raw_delta", text: reasoningText };
795
993
  }
796
994
  if (typeof delta.content === "string" && delta.content.length > 0) {
797
995
  sawUserFacingOutput = true;
@@ -839,7 +1037,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
839
1037
 
840
1038
  // Any non-empty finish_reason ends the generation: flush assembled tool calls as
841
1039
  // atomic sequences (covers "tool_calls" AND providers that close tool turns with "stop").
842
- if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
1040
+ if (typeof choice.finish_reason === "string" && choice.finish_reason) {
843
1041
  yield* flushToolCalls();
844
1042
  }
845
1043
  return "continue";
@@ -918,11 +1116,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
918
1116
  }
919
1117
  yield* flushToolCalls();
920
1118
  // Graceful close that omitted [DONE] but delivered finish_reason and/or answer text.
921
- const stopReason = finishReason === "length"
922
- ? "max_tokens"
923
- : finishReason === "content_filter"
924
- ? "content_filter"
925
- : undefined;
1119
+ const stopReason = stopReasonFor(finishReason);
926
1120
  yield { type: "done", usage: pendingUsage, ...(stopReason ? { stopReason } : {}) };
927
1121
  } catch (error) {
928
1122
  if (isTranslatorBudgetExceededError(error)
@@ -940,7 +1134,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
940
1134
  throw error;
941
1135
  } finally {
942
1136
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
943
- for (const call of pendingToolCalls) budget.closeCall(call.key);
1137
+ closeToolCalls();
944
1138
  reader.releaseLock();
945
1139
  }
946
1140
  },
@@ -950,34 +1144,40 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
950
1144
  const responseBytes = new TextEncoder().encode(JSON.stringify(json)).byteLength;
951
1145
  budget.chargeRetained(responseBytes, { kind: "retained_collectors" });
952
1146
  try {
953
- if (json.error) {
954
- const upstreamError = json.error as { message?: unknown; code?: unknown; type?: unknown; status?: unknown };
955
- const message = typeof upstreamError.message === "string" ? upstreamError.message : "upstream error";
956
- const code = typeof upstreamError.code === "string" ? upstreamError.code : undefined;
957
- const errorType = typeof upstreamError.type === "string" ? upstreamError.type : undefined;
958
- const status = isCyberPolicyCode(code)
959
- ? 400
960
- : typeof upstreamError.status === "number" && Number.isInteger(upstreamError.status)
961
- ? upstreamError.status
962
- : undefined;
1147
+ const payload = unwrapChatCompletionPayload(json);
1148
+ const usage = usageFromOpenAIChat(payload.usage as Record<string, unknown> | undefined);
1149
+ if (json.success === false && payload.error === undefined) {
963
1150
  return [{
964
1151
  type: "error",
965
- message,
966
- ...(code !== undefined ? { code } : {}),
967
- ...(errorType !== undefined ? { errorType } : {}),
968
- ...(status !== undefined ? { status } : {}),
1152
+ message: "upstream reported failure without an error payload",
1153
+ ...(usage ? { usage } : {}),
969
1154
  }];
970
1155
  }
1156
+ if (payload.error !== undefined && payload.error !== null) {
1157
+ return [upstreamErrorEvent(payload.error, usage)];
1158
+ }
971
1159
 
972
1160
  const events: AdapterEvent[] = [];
973
- const choices = json.choices as { message?: Record<string, unknown> }[] | undefined;
974
- if (!Array.isArray(choices) || choices.length === 0 || !choices[0].message) {
975
- return [{ type: "error", message: "upstream response contained no choices" }];
1161
+ const choices = payload.choices as {
1162
+ message?: Record<string, unknown>;
1163
+ finish_reason?: unknown;
1164
+ error?: OpenAIChatError;
1165
+ }[] | undefined;
1166
+ if (!Array.isArray(choices) || choices.length === 0) {
1167
+ return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
1168
+ }
1169
+ const rawChoice = choices[0];
1170
+ if (rawChoice === null || typeof rawChoice !== "object" || Array.isArray(rawChoice)) {
1171
+ return [invalidChoicesEvent(usage)];
976
1172
  }
1173
+ const choice = rawChoice;
1174
+ if (choice.finish_reason === "error") return [upstreamErrorEvent(choice.error, usage)];
1175
+ if (!choice.message) return [{ type: "error", message: "upstream response contained no choices", ...(usage ? { usage } : {}) }];
977
1176
 
978
- const msg = choices[0].message;
979
- if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
980
- events.push({ type: "reasoning_raw_delta", text: msg.reasoning_content });
1177
+ const msg = choice.message;
1178
+ const reasoningText = reasoningTextFrom(msg);
1179
+ if (reasoningText !== undefined) {
1180
+ events.push({ type: "reasoning_raw_delta", text: reasoningText });
981
1181
  }
982
1182
  if (typeof msg.content === "string") {
983
1183
  events.push({ type: "text_delta", text: msg.content });
@@ -990,10 +1190,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
990
1190
  events.push({ type: "tool_call_end" });
991
1191
  }
992
1192
  }
993
- const usage = json.usage as Record<string, unknown> | undefined;
1193
+ const stopReason = stopReasonFor(choice.finish_reason);
994
1194
  events.push({
995
1195
  type: "done",
996
- usage: usageFromOpenAIChat(usage),
1196
+ usage,
1197
+ ...(stopReason ? { stopReason } : {}),
997
1198
  });
998
1199
  retainTranslatedEventBatch(events, budget);
999
1200
  return events;