@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
@@ -18,7 +18,8 @@ import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPr
18
18
  import { parseDataUrl } from "./image";
19
19
  import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
20
20
  import { normalizeAnthropicImages } from "./anthropic-image-normalize";
21
- import { neutralizeIdentity } from "./identity";
21
+ import { identifyRoutedModel } from "./identity";
22
+ import { redactSecretString } from "../lib/redact";
22
23
  import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
23
24
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
24
25
  import { decodeServerSentEvents } from "../lib/sse-decoder";
@@ -243,6 +244,43 @@ function isLikelyRealAnthropicThinkingSignature(signature: string | undefined):
243
244
  return /^[A-Za-z0-9+/_=-]+$/.test(signature);
244
245
  }
245
246
 
247
+ /**
248
+ * Bridge error fidelity (web-search/images loops): extract a display-safe summary from an
249
+ * Anthropic JSON error envelope so `Provider error <status>` carries the upstream reason.
250
+ * JSON-only extraction — HTML/non-JSON bodies yield "" so raw markup is never echoed.
251
+ */
252
+ export function formatAnthropicErrorBody(status: number, _headers: Headers, payloadText: string): string {
253
+ let parsed: unknown;
254
+ try {
255
+ parsed = JSON.parse(payloadText);
256
+ } catch {
257
+ return "";
258
+ }
259
+ const detail = extractAnthropicErrorDetail(parsed);
260
+ if (!detail) return "";
261
+ return redactSecretString(detail).slice(0, 400);
262
+ }
263
+
264
+ function extractAnthropicErrorDetail(parsed: unknown): string | undefined {
265
+ if (typeof parsed === "string") return parsed.trim() || undefined;
266
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
267
+ const obj = parsed as Record<string, unknown>;
268
+ // Anthropic envelope: { type: "error", error: { type, message } }; tolerate a bare
269
+ // { error: { message } } and a string error field.
270
+ const err = obj.error;
271
+ if (typeof err === "string" && err.trim()) return err.trim();
272
+ if (err !== null && typeof err === "object" && !Array.isArray(err)) {
273
+ const e = err as Record<string, unknown>;
274
+ const msg = e.message;
275
+ if (typeof msg !== "string" || !msg.trim()) return undefined;
276
+ const type = e.type;
277
+ return typeof type === "string" && type.trim()
278
+ ? `${type.trim()}: ${msg.trim()}`
279
+ : msg.trim();
280
+ }
281
+ return undefined;
282
+ }
283
+
246
284
  function usesNativeAnthropicEndpoint(provider: OcxProviderConfig): boolean {
247
285
  try {
248
286
  return new URL(provider.baseUrl).hostname === "api.anthropic.com";
@@ -277,7 +315,69 @@ function usableToolUseId(id: unknown): string {
277
315
  return typeof id === "string" && id.trim() ? id : synthesizeToolUseId();
278
316
  }
279
317
 
280
- function toolUseArguments(input: unknown): string {
318
+ /**
319
+ * Bound repair for a malformed tool-arguments string under the compatibility profile (#658):
320
+ * a gateway such as AgentRouter can concatenate JSON objects (`{}{"value":42}`). Find the
321
+ * last parseable JSON object by scanning suffixes from each object-open brace and prefixes
322
+ * ending at each object-close brace. Both scans walk backwards from the end trying at most
323
+ * `maxCandidates` positions, so no offset index is ever materialized: a brace-dense hostile
324
+ * input costs at most 2 × maxCandidates bounded JSON.parse attempts and no extra storage.
325
+ * Inputs above MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES are not repaired at all.
326
+ */
327
+ const MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES = 1024 * 1024;
328
+
329
+ /**
330
+ * Whether `input` encodes to more than `max` UTF-8 bytes, with an early exit so the check
331
+ * itself never allocates a copy of a hostile string. `string.length` counts UTF-16 code
332
+ * units, which undercounts astral text by 2x against a byte budget.
333
+ */
334
+ function utf8BytesExceed(input: string, max: number): boolean {
335
+ let bytes = 0;
336
+ for (let i = 0; i < input.length; i++) {
337
+ const code = input.charCodeAt(i);
338
+ if (code < 0x80) bytes += 1;
339
+ else if (code < 0x800) bytes += 2;
340
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length
341
+ && input.charCodeAt(i + 1) >= 0xdc00 && input.charCodeAt(i + 1) <= 0xdfff) {
342
+ // A complete surrogate pair is one 4-byte scalar. Anything else — a high surrogate
343
+ // followed by another high surrogate or a non-surrogate — encodes as two separate
344
+ // U+FFFD replacements, so the next unit must NOT be skipped.
345
+ bytes += 4;
346
+ i++;
347
+ } else bytes += 3; // lone surrogates encode as U+FFFD (3 bytes)
348
+ if (bytes > max) return true;
349
+ }
350
+ return false;
351
+ }
352
+
353
+ function lastValidJsonObject(input: string, maxCandidates: number): string | undefined {
354
+ const tryParseObject = (candidate: string): string | undefined => {
355
+ try {
356
+ const parsed = JSON.parse(candidate) as unknown;
357
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return candidate;
358
+ } catch { /* keep scanning */ }
359
+ return undefined;
360
+ };
361
+ let scanFrom = input.length - 1;
362
+ for (let tried = 0; tried < maxCandidates && scanFrom >= 0; tried++) {
363
+ const open = input.lastIndexOf("{", scanFrom);
364
+ if (open === -1) break;
365
+ const repaired = tryParseObject(input.slice(open));
366
+ if (repaired !== undefined) return repaired;
367
+ scanFrom = open - 1;
368
+ }
369
+ scanFrom = input.length - 1;
370
+ for (let tried = 0; tried < maxCandidates && scanFrom >= 0; tried++) {
371
+ const close = input.lastIndexOf("}", scanFrom);
372
+ if (close === -1) break;
373
+ const repaired = tryParseObject(input.slice(0, close + 1));
374
+ if (repaired !== undefined) return repaired;
375
+ scanFrom = close - 1;
376
+ }
377
+ return undefined;
378
+ }
379
+
380
+ function toolUseArguments(input: unknown, lenient = false): string {
281
381
  if (typeof input === "string") {
282
382
  const trimmed = input.trim();
283
383
  if (!trimmed) return "{}";
@@ -285,6 +385,10 @@ function toolUseArguments(input: unknown): string {
285
385
  JSON.parse(trimmed);
286
386
  return trimmed;
287
387
  } catch {
388
+ if (lenient && !utf8BytesExceed(trimmed, MAX_REPAIRABLE_TOOL_ARGUMENT_BYTES)) {
389
+ const repaired = lastValidJsonObject(trimmed, 32);
390
+ if (repaired !== undefined) return repaired;
391
+ }
288
392
  // A tool call's arguments must be a JSON object. Re-encoding an unparseable string as a
289
393
  // JSON *string* is the double-encoding #765 reports: the caller then receives
290
394
  // `"get weather"` where an object was required and the tool call is unusable either way.
@@ -342,16 +446,68 @@ const ADAPTIVE_THINKING_FAMILY_MINIMUMS: Record<string, readonly [major: number,
342
446
  fable: [0, 0],
343
447
  };
344
448
 
345
- function usesAdaptiveThinking(modelId: string): boolean {
346
- // Minor is 1-2 digits with a non-digit lookahead so date-pinned ids ("claude-opus-4-20250514")
347
- // parse as minor 0 instead of minor 20250514; suffixed ids ("claude-opus-4-8[1m]") still match.
348
- const match = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId);
349
- if (!match) return false;
350
- const minimum = ADAPTIVE_THINKING_FAMILY_MINIMUMS[match[1]];
449
+ /**
450
+ * Family/version parse for a Claude model id, tolerant of a routing prefix.
451
+ *
452
+ * `parsed.modelId` is not always bare, and the slash can fall on either side.
453
+ * A `modelMap` entry may point at a routed destination such as
454
+ * `anthropic/claude-sonnet-5` (prefix), while a custom provider may expose a
455
+ * native id such as `claude-sonnet-5/variant` (suffix); both survive routing's
456
+ * known-id decoding. So this matches the segment that actually begins with
457
+ * `claude-` rather than assuming it is the first or the last one. A capability
458
+ * predicate that quietly returns false is worse than one that throws — the
459
+ * request just goes out wrong.
460
+ *
461
+ * Minor is 1-2 digits with a non-digit lookahead so date-pinned ids
462
+ * ("claude-opus-4-20250514") parse as minor 0 instead of minor 20250514;
463
+ * suffixed ids ("claude-opus-4-8[1m]") still match.
464
+ */
465
+ function claudeFamilyVersion(modelId: string): { family: string; major: number; minor: number } | undefined {
466
+ // Find the segment that actually starts with `claude-`, rather than assuming it is either
467
+ // the first (breaks `anthropic/claude-sonnet-5`) or the last (breaks `claude-sonnet-5/variant`,
468
+ // where the slash carries a vendor suffix rather than a routing prefix).
469
+ const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId);
470
+ if (!match) return undefined;
471
+ return {
472
+ family: match[1]!,
473
+ major: Number(match[2]),
474
+ minor: match[3] === undefined ? 0 : Number(match[3]),
475
+ };
476
+ }
477
+
478
+ function meetsFamilyMinimum(
479
+ modelId: string,
480
+ minimums: Record<string, readonly [major: number, minor: number]>,
481
+ ): boolean {
482
+ const parsed = claudeFamilyVersion(modelId);
483
+ if (!parsed) return false;
484
+ const minimum = minimums[parsed.family];
351
485
  if (!minimum) return false;
352
- const major = Number(match[2]);
353
- const minor = match[3] === undefined ? 0 : Number(match[3]);
354
- return major > minimum[0] || (major === minimum[0] && minor >= minimum[1]);
486
+ return parsed.major > minimum[0] || (parsed.major === minimum[0] && parsed.minor >= minimum[1]);
487
+ }
488
+
489
+ function usesAdaptiveThinking(modelId: string): boolean {
490
+ return meetsFamilyMinimum(modelId, ADAPTIVE_THINKING_FAMILY_MINIMUMS);
491
+ }
492
+
493
+ /**
494
+ * Claude families that (a) think by DEFAULT when the request omits `thinking`,
495
+ * and (b) accept an explicit `thinking: {type: "disabled"}` to turn it off.
496
+ *
497
+ * Deliberately NOT `usesAdaptiveThinking()`, which answers a different question
498
+ * (which wire shape a family accepts). The two sets differ in both directions:
499
+ * Fable always thinks and REJECTS an explicit disable, while Opus 4.7/4.8 use
500
+ * the adaptive wire but leave thinking off when the field is omitted, so they
501
+ * need no disable at all. Seeded with the family where the defect reproduces
502
+ * (#545); widen only with vendor evidence, since a wrong entry here turns a
503
+ * silent truncation into a 400.
504
+ */
505
+ const EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS: Record<string, readonly [major: number, minor: number]> = {
506
+ sonnet: [5, 0],
507
+ };
508
+
509
+ function supportsExplicitThinkingDisable(modelId: string): boolean {
510
+ return meetsFamilyMinimum(modelId, EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS);
355
511
  }
356
512
 
357
513
  /** `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" is rejected with a 400. */
@@ -441,7 +597,7 @@ function messagesToAnthropicFormat(
441
597
  );
442
598
  const systemParts = [...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : [])];
443
599
  const system = systemParts.length
444
- ? neutralizeIdentity(systemParts.join("\n\n")) || undefined
600
+ ? identifyRoutedModel(systemParts.join("\n\n"), parsed.modelId) || undefined
445
601
  : undefined;
446
602
  const messages: unknown[] = [];
447
603
 
@@ -659,6 +815,8 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
659
815
  return {
660
816
  name: "anthropic",
661
817
 
818
+ formatErrorBody: formatAnthropicErrorBody,
819
+
662
820
  async buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta) {
663
821
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
664
822
  if (isOAuth) {
@@ -700,7 +858,14 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
700
858
  // `reasoning` is a Codex effort string; "none" is the disable sentinel (see parser.ts
701
859
  // REASONING_EFFORTS). A bare truthy check would treat "none" as truthy and wrongly enable
702
860
  // extended thinking (and strip temperature/top_p), so gate on a real, non-disable effort.
703
- if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
861
+ //
862
+ // "none" is not the same as absent. Omitting `thinking` lets a default-on model think
863
+ // anyway, and thinking shares the caller's `max_tokens` — which truncates a small-budget
864
+ // request before it can emit its stop sequence (#545). Say "disabled" out loud where the
865
+ // model both defaults to thinking and accepts being told not to.
866
+ if (parsed.options.reasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
867
+ body.thinking = { type: "disabled" };
868
+ } else if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") {
704
869
  if (usesAdaptiveThinking(parsed.modelId)) {
705
870
  // Adaptive-thinking models replace the token budget with an effort knob and reject
706
871
  // `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
@@ -798,6 +963,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
798
963
  let pendingUsage: Record<string, number> | undefined;
799
964
  let pendingStopReason: string | undefined;
800
965
  let emittedDone = false;
966
+ let sawVisibleText = false;
801
967
 
802
968
  const emitDone = function* (): Generator<AdapterEvent> {
803
969
  if (emittedDone) return;
@@ -853,6 +1019,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
853
1019
  const delta = data.delta as Record<string, unknown> | undefined;
854
1020
  if (!delta) break;
855
1021
  if (delta.type === "text_delta" && typeof delta.text === "string") {
1022
+ // Only non-empty text proves the upstream produced usable output; an empty
1023
+ // delta followed by EOF must stay a truncation error even on the tolerant
1024
+ // profile, or a cut-off turn would surface as a successful empty answer.
1025
+ if (delta.text.length > 0) sawVisibleText = true;
856
1026
  yield { type: "text_delta", text: delta.text };
857
1027
  } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
858
1028
  yield { type: "thinking_delta", thinking: delta.thinking };
@@ -934,6 +1104,10 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
934
1104
  code: "translation_buffer_limit",
935
1105
  message: "upstream translation buffer exceeded the safe limit",
936
1106
  };
1107
+ // The budget error IS the terminal event for this stream. Falling through to the
1108
+ // EOF handling below could append tool_call_end/done after it, violating the
1109
+ // one-terminal-event contract for consumers that keep draining the generator.
1110
+ return;
937
1111
  } finally {
938
1112
  if (currentToolCallId) budget.closeCall(currentToolCallId);
939
1113
  }
@@ -951,6 +1125,26 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
951
1125
  usage: usageFromAnthropic(pendingUsage),
952
1126
  ...(stopReason ? { stopReason } : {}),
953
1127
  };
1128
+ } else if (provider.anthropicEofTolerance === true) {
1129
+ // AgentRouter-style compatibility profile (#658): the upstream can close the stream
1130
+ // after valid content without terminal frames. Complete only when visible text was
1131
+ // received or an open tool call has complete JSON-object arguments; everything else
1132
+ // (incomplete tool JSON, no usable content, transport failure) stays a truncation
1133
+ // error, matching the strict default.
1134
+ if (currentToolCallId) {
1135
+ if (streamedToolArgumentsParse(currentToolCallJson)) {
1136
+ budget.closeCall(currentToolCallId);
1137
+ currentToolCallId = "";
1138
+ yield { type: "tool_call_end" };
1139
+ yield* emitDone();
1140
+ } else {
1141
+ yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
1142
+ }
1143
+ } else if (sawVisibleText) {
1144
+ yield* emitDone();
1145
+ } else {
1146
+ yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
1147
+ }
954
1148
  } else {
955
1149
  yield { type: "error", message: "upstream stream ended before message_stop — possible truncation" };
956
1150
  }
@@ -980,7 +1174,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
980
1174
  } else if (block.type === "tool_use") {
981
1175
  const id = usableToolUseId(block.id);
982
1176
  events.push({ type: "tool_call_start", id, name: toolNames.fromWire(block.name ?? "") });
983
- events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input) });
1177
+ events.push({ type: "tool_call_delta", arguments: toolUseArguments(block.input, provider.anthropicEofTolerance === true) });
984
1178
  events.push({ type: "tool_call_end" });
985
1179
  }
986
1180
  }
@@ -49,11 +49,22 @@ export interface AdapterRequest {
49
49
  /** Releases observation of a serialized request body after its final fetch attempt settles. */
50
50
  releaseBodyObservation?: () => void;
51
51
  /** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
52
- reasoningLog?: {
53
- effectiveEffort: string;
54
- wireField: "reasoning_effort" | "thinking_budget" | "thinking.type";
55
- wireValue: string | number;
56
- };
52
+ reasoningLog?:
53
+ | {
54
+ effectiveEffort: string;
55
+ wireField: "reasoning.enabled";
56
+ wireValue: boolean;
57
+ }
58
+ | {
59
+ effectiveEffort: string;
60
+ wireField: "thinking_budget";
61
+ wireValue: number;
62
+ }
63
+ | {
64
+ effectiveEffort: string;
65
+ wireField: "reasoning_effort" | "reasoning.effort" | "thinking.type";
66
+ wireValue: string;
67
+ };
57
68
  usageLog?: {
58
69
  inputTokens?: number;
59
70
  estimated?: boolean;
@@ -116,8 +116,9 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
116
116
  }
117
117
 
118
118
  /**
119
- * Compose a Cursor wire id from a Codex-facing base id and effort tier.
120
- * Fast variants put the mode after the effort; other models use the ordinary `{base}-{effort}` form.
119
+ * Compose Cursor's flattened model id from a Codex-facing base id and effort tier. Discovery uses
120
+ * this for the ids returned by GetUsableModels. Parameterized Grok Fast requests bypass the flat id
121
+ * and send the base model plus requested_model parameters instead.
121
122
  */
122
123
  export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
123
124
  if (baseModelId.endsWith("-fast")) {
@@ -168,6 +168,45 @@ export function decodeAvailableConnectFrames(
168
168
  }
169
169
  }
170
170
 
171
+ /**
172
+ * Cursor-based sibling of decodeAvailableConnectFrames for callers that keep
173
+ * their own raw backlog: consumes complete frames from the FRONT of `input`
174
+ * and reports how many bytes were consumed (headers included) instead of
175
+ * materializing a remainder copy, and hands payload VIEWS (not copies) back so
176
+ * the caller can transfer the already-charged bytes to the frame lifecycle
177
+ * instead of reserving a second copy. The caller advances its own cursor by
178
+ * `consumedBytes` and never pays an O(backlog) copy per drain.
179
+ */
180
+ export function consumeConnectFrames(
181
+ input: Uint8Array,
182
+ maxPayloadBytes = MAX_CONNECT_FRAME_PAYLOAD_BYTES,
183
+ availableFrameSlots = Number.POSITIVE_INFINITY,
184
+ ): { frames: ConnectFrame[]; consumedBytes: number } {
185
+ const planned: InspectedConnectFrame[] = [];
186
+ let offset = 0;
187
+ while (offset < input.length && planned.length < availableFrameSlots) {
188
+ const inspected = inspectConnectFrame(input, offset, maxPayloadBytes);
189
+ if (!inspected) break;
190
+ planned.push(inspected);
191
+ offset += inspected.readBytes;
192
+ }
193
+ // Zero-copy handoff for large payloads: VIEWS into the caller's backlog
194
+ // buffer (safe: append-only at its end, compaction/growth replace the
195
+ // buffer, a consumed region is never mutated in place). Small payloads are
196
+ // COPIED instead — a small view would pin the whole backlog buffer (up to
197
+ // 32 MiB) alive while the frame waits in the work queue.
198
+ const COPY_PIN_THRESHOLD_BYTES = 64 * 1024;
199
+ const frames = planned.map(({ flags, length, payloadStart }) => ({
200
+ flags,
201
+ payload: length <= COPY_PIN_THRESHOLD_BYTES
202
+ ? input.slice(payloadStart, payloadStart + length)
203
+ : input.subarray(payloadStart, payloadStart + length),
204
+ compressed: isConnectFrameCompressed(flags),
205
+ endStream: isConnectFrameEndStream(flags),
206
+ }));
207
+ return { frames, consumedBytes: offset };
208
+ }
209
+
171
210
  function inspectConnectFrame(
172
211
  input: Uint8Array,
173
212
  offset: number,