@bitkyc08/opencodex 2.63.0 → 2.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-CpuDF3ci.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
  10. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  11. package/gui/dist/assets/index-SggB6t3z.js +86 -0
  12. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-BqrsSrIR.js +0 -50
  259. package/gui/dist/assets/index-C6SJrh0N.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -209,8 +209,9 @@ export function cursorRequestEmitsFastVariant(parsed: OcxParsedRequest): boolean
209
209
 
210
210
  /**
211
211
  * Resolve a `cursor/<model>` selection + Codex reasoning effort to Cursor's requested model shape.
212
- * Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok Fast is parameterized
213
- * instead: current Cursor clients send the matching Grok base id plus `effort` and `fast` parameters.
212
+ * Most models encode effort in a flat id (`claude-4.6-opus-high`). Grok 4.5/4.6 Fast is
213
+ * parameterized: current Cursor clients send the matching base id plus `effort` and `fast` parameters;
214
+ * Grok 4.7 (no wirePrefix) instead uses the flattened effort-fast id.
214
215
  * A fully-qualified id (one that is not a known effort base) passes through unchanged.
215
216
  */
216
217
  function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: boolean, liveRosterScope?: string): {
@@ -226,8 +227,8 @@ function normalizeCursorModelId(modelId: string, reasoning?: string, fast?: bool
226
227
  // resolver owns effort composition, variant dimensions, the synthetic -1m
227
228
  // marker (ultra -> Max Mode, evidence-gated), and the cursor- wire prefix.
228
229
  const id = selection.modelId;
229
- // Grok Fast stays parameterized: current Cursor clients send the base id
230
- // plus effort/fast parameters instead of the flattened -fast id.
230
+ // Grok 4.5/4.6 Fast stays parameterized: current Cursor clients send the base id
231
+ // plus effort/fast parameters; 4.7 (no wirePrefix) uses the flattened effort-fast id.
231
232
  const grokFast = cursorGrokFastSelection(id, reasoning, fast);
232
233
  if (grokFast) {
233
234
  return {
@@ -185,7 +185,7 @@ export function buildCursorToolGuidanceSystemNote(
185
185
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
186
186
  // model probes for a top-level shell tool that is not there.
187
187
  codeMode
188
- ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.`
188
+ ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`text(await tools.exec_command({cmd: \"ls\"}))\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched.`
189
189
  : undefined,
190
190
  codeMode
191
191
  ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers. " + CODE_MODE_HOST_CONTRACT_SENTENCE
@@ -30,6 +30,7 @@ export const DEVIN_STATIC_MODELS = [
30
30
  "glm-5-2",
31
31
  "kimi-k2-7",
32
32
  "grok-4-5",
33
+ "grok-4-7",
33
34
  ] as const;
34
35
 
35
36
  /**
@@ -73,6 +74,9 @@ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
73
74
  "gemini-3-8-flash": 1_048_576,
74
75
  "grok-4-5": 500_000,
75
76
  "grok-4-6": 500_000,
77
+ // Live Devin catalog context_length, 2026-09-23:
78
+ // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
79
+ "grok-4-7": 500_000,
76
80
  };
77
81
 
78
82
  /**
@@ -135,6 +139,9 @@ export function sortDevinRungs(rungs: Iterable<string>): string[] {
135
139
  */
136
140
  export const DEVIN_MODEL_EFFORTS: Record<string, string[]> = {
137
141
  "swe-2": ["medium", "high", "max"],
142
+ // Live Devin catalog, 2026-09-23:
143
+ // devlog/_plan/260923_grok47_parity/010_probe-evidence.md.
144
+ "grok-4-7": ["low", "medium", "high", "xhigh", "max"],
138
145
  };
139
146
 
140
147
  /**
@@ -0,0 +1,251 @@
1
+ import type { AdapterEvent } from "../types";
2
+ import { modelInList } from "../types";
3
+ import type { TranslatorBudget } from "../lib/translator-budget";
4
+
5
+ type ThinkingTag = "<thinking>" | "<think>" | "<reasoning>";
6
+ type ParserState = "pre" | "thinking" | "scanning" | "streaming";
7
+
8
+ const OPEN_TAGS: ThinkingTag[] = ["<thinking>", "<think>", "<reasoning>"];
9
+ const MAX_OPEN_TAG = Math.max(...OPEN_TAGS.map(t => t.length));
10
+ const MAX_CLOSE_TAG = Math.max(...OPEN_TAGS.map(t => `</${t.slice(1)}`.length));
11
+
12
+ function closeTagFor(openTag: ThinkingTag): string {
13
+ return `</${openTag.slice(1)}`;
14
+ }
15
+
16
+ function isPossibleOpenTagPrefix(text: string): boolean {
17
+ return OPEN_TAGS.some(tag => tag.startsWith(text) && text.length < tag.length);
18
+ }
19
+
20
+ /** Move a send boundary back one unit rather than splitting a surrogate pair into U+FFFD. */
21
+ function surrogateSafeCut(text: string, cut: number): number {
22
+ if (cut <= 0 || cut >= text.length) return Math.max(0, Math.min(cut, text.length));
23
+ const atCut = text.charCodeAt(cut - 1);
24
+ return atCut >= 0xd800 && atCut <= 0xdbff ? cut - 1 : cut;
25
+ }
26
+
27
+ export interface InlineThinkTagOptions {
28
+ /**
29
+ * Keep scanning for further think blocks after the first one closes. Kiro emits a single
30
+ * leading block, so it leaves this off and streams the rest verbatim. MiniMax M-series
31
+ * interleaves several blocks with answer segments, so a reusing adapter opts in.
32
+ */
33
+ interleaved?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Recovers thinking that a gateway left inline in visible content as `<think>` blocks instead of
38
+ * a separate `reasoning_content` / `reasoning_details` field. Shared by the Kiro adapter and by
39
+ * the openai-chat adapter's opt-in `inlineThinkTagModels`.
40
+ */
41
+ export class InlineThinkTagParser {
42
+ private state: ParserState = "pre";
43
+ private preWhitespaceChunks: string[] = [];
44
+ private preWhitespaceLength = 0;
45
+ private preWhitespaceBytes = 0;
46
+ private preBuffer = "";
47
+ private thinkingBuffer = "";
48
+ private closeTag = "";
49
+
50
+ private readonly interleaved: boolean;
51
+
52
+ constructor(private readonly budget?: TranslatorBudget, options?: InlineThinkTagOptions) {
53
+ this.interleaved = options?.interleaved === true;
54
+ }
55
+
56
+ private replaceCarry(field: "preBuffer" | "thinkingBuffer", next: string): void {
57
+ const previous = this[field];
58
+ if (previous === next) return;
59
+ const previousBytes = Buffer.byteLength(previous);
60
+ const nextBytes = Buffer.byteLength(next);
61
+ const reservation = this.budget?.reserveTransient(nextBytes, { kind: "reasoning" });
62
+ this[field] = next;
63
+ reservation?.commitRetained();
64
+ this.budget?.releaseRetained(previousBytes, { kind: "reasoning" });
65
+ }
66
+
67
+ private appendPreWhitespace(text: string): void {
68
+ if (!text) return;
69
+ const bytes = Buffer.byteLength(text);
70
+ const reservation = this.budget?.reserveTransient(bytes, { kind: "reasoning" });
71
+ this.preWhitespaceChunks.push(text);
72
+ this.preWhitespaceLength += text.length;
73
+ this.preWhitespaceBytes += bytes;
74
+ reservation?.commitRetained();
75
+ }
76
+
77
+ private finishPreWhitespace(emit: boolean): string {
78
+ if (this.preWhitespaceLength === 0) return "";
79
+ const reservation = emit ? this.budget?.reserveTransient(this.preWhitespaceBytes, { kind: "reasoning" }) : undefined;
80
+ try {
81
+ return emit ? this.preWhitespaceChunks.join("") : "";
82
+ } finally {
83
+ this.preWhitespaceChunks.length = 0;
84
+ this.preWhitespaceLength = 0;
85
+ this.budget?.releaseRetained(this.preWhitespaceBytes, { kind: "reasoning" });
86
+ this.preWhitespaceBytes = 0;
87
+ reservation?.release();
88
+ }
89
+ }
90
+
91
+ feed(text: string): AdapterEvent[] {
92
+ if (!text) return [];
93
+ if (this.state === "streaming") return [{ type: "text_delta", text }];
94
+ let input = text;
95
+ if (this.state === "pre") {
96
+ if (this.preBuffer) {
97
+ input = this.preBuffer + text;
98
+ this.replaceCarry("preBuffer", "");
99
+ } else {
100
+ const stripped = text.trimStart();
101
+ const leadingLength = text.length - stripped.length;
102
+ if (leadingLength > 0) this.appendPreWhitespace(text.slice(0, leadingLength));
103
+ if (!stripped) return [];
104
+ input = stripped;
105
+ }
106
+ const openTag = OPEN_TAGS.find(tag => input.startsWith(tag));
107
+ if (openTag) {
108
+ const leading = this.finishPreWhitespace(this.interleaved);
109
+ this.state = "thinking";
110
+ this.closeTag = closeTagFor(openTag);
111
+ const events: AdapterEvent[] = leading ? [{ type: "text_delta", text: leading }] : [];
112
+ return this.drainChunk(input, openTag.length, events);
113
+ }
114
+ if (input.length <= MAX_OPEN_TAG && isPossibleOpenTagPrefix(input)) {
115
+ this.replaceCarry("preBuffer", input);
116
+ return [];
117
+ }
118
+ this.state = "streaming";
119
+ return [{ type: "text_delta", text: this.finishPreWhitespace(true) + input }];
120
+ }
121
+ if (this.state === "thinking") {
122
+ input = this.thinkingBuffer + text;
123
+ this.replaceCarry("thinkingBuffer", "");
124
+ } else {
125
+ input = this.preBuffer + text;
126
+ this.replaceCarry("preBuffer", "");
127
+ }
128
+ return this.drainChunk(input, 0, []);
129
+ }
130
+
131
+ flush(): AdapterEvent[] {
132
+ if (this.state === "thinking") {
133
+ const out = this.thinkingBuffer;
134
+ this.replaceCarry("thinkingBuffer", "");
135
+ this.state = "streaming";
136
+ return out ? [{ type: "reasoning_raw_delta", text: out }] : [];
137
+ }
138
+ if (this.preWhitespaceLength > 0 || this.preBuffer) {
139
+ const out = this.finishPreWhitespace(true) + this.preBuffer;
140
+ this.replaceCarry("preBuffer", "");
141
+ this.state = "streaming";
142
+ return [{ type: "text_delta", text: out }];
143
+ }
144
+ return [];
145
+ }
146
+
147
+ /** Release any partial tag/content carry when the owning stream stops early. */
148
+ dispose(): void {
149
+ this.finishPreWhitespace(false);
150
+ this.replaceCarry("preBuffer", "");
151
+ this.replaceCarry("thinkingBuffer", "");
152
+ this.closeTag = "";
153
+ this.state = "streaming";
154
+ }
155
+
156
+ private drainChunk(input: string, start: number, events: AdapterEvent[]): AdapterEvent[] {
157
+ let offset = start;
158
+ for (;;) {
159
+ if (this.state === "thinking") {
160
+ const idx = input.indexOf(this.closeTag, offset);
161
+ if (idx >= 0) {
162
+ if (idx > offset) events.push({ type: "reasoning_raw_delta", text: input.slice(offset, idx) });
163
+ offset = idx + this.closeTag.length;
164
+ if (!this.interleaved) {
165
+ // Opt-in Chat answers are byte-preserving; keep Kiro's legacy normalization.
166
+ const after = input.slice(offset).trimStart();
167
+ this.state = "streaming";
168
+ if (after) events.push({ type: "text_delta", text: after });
169
+ return events;
170
+ }
171
+ this.state = "scanning";
172
+ continue;
173
+ }
174
+ // Keep only a possible close tag, and do not split a surrogate pair.
175
+ const cut = Math.max(offset, surrogateSafeCut(input, input.length - MAX_CLOSE_TAG));
176
+ if (cut > offset) events.push({ type: "reasoning_raw_delta", text: input.slice(offset, cut) });
177
+ this.replaceCarry("thinkingBuffer", input.slice(cut));
178
+ return events;
179
+ }
180
+
181
+ // Interleaved mode has already seen a leading tag; later tags delimit anywhere.
182
+ let openIndex = input.indexOf("<", offset);
183
+ let openTag: ThinkingTag | undefined;
184
+ while (openIndex >= 0) {
185
+ openTag = OPEN_TAGS.find(tag => input.startsWith(tag, openIndex));
186
+ if (openTag) break;
187
+ openIndex = input.indexOf("<", openIndex + 1);
188
+ }
189
+ if (openIndex >= 0 && openTag) {
190
+ if (openIndex > offset) events.push({ type: "text_delta", text: input.slice(offset, openIndex) });
191
+ offset = openIndex + openTag.length;
192
+ this.state = "thinking";
193
+ this.closeTag = closeTagFor(openTag);
194
+ continue;
195
+ }
196
+ // Hold back only a possible open-tag prefix, with a surrogate-safe boundary.
197
+ const cut = Math.max(offset, surrogateSafeCut(input, input.length - (MAX_OPEN_TAG - 1)));
198
+ if (cut > offset) events.push({ type: "text_delta", text: input.slice(offset, cut) });
199
+ this.replaceCarry("preBuffer", input.slice(cut));
200
+ return events;
201
+ }
202
+ }
203
+ }
204
+
205
+ /** Visible-content splitter the openai-chat adapter holds for the life of one response. */
206
+ export interface InlineThinkContentSplitter {
207
+ feed(text: string): AdapterEvent[];
208
+ flush(): AdapterEvent[];
209
+ dispose(): void;
210
+ }
211
+
212
+ const PASSTHROUGH: InlineThinkContentSplitter = {
213
+ feed: text => [{ type: "text_delta", text }],
214
+ flush: () => [],
215
+ dispose: () => { /* nothing carried */ },
216
+ };
217
+
218
+ /**
219
+ * Opt-in recovery for `inlineThinkTagModels`. A model that is not listed gets a passthrough that
220
+ * never inspects or rewrites visible content, so the 66 registry providers sharing the openai-chat
221
+ * adapter keep byte-exact behavior.
222
+ */
223
+ export function createInlineThinkContentSplitter(
224
+ models: string[] | undefined,
225
+ modelId: string | undefined,
226
+ budget?: TranslatorBudget,
227
+ ): InlineThinkContentSplitter {
228
+ if (!modelInList(models, modelId ?? "")) return PASSTHROUGH;
229
+ const parser = new InlineThinkTagParser(budget, { interleaved: true });
230
+ return {
231
+ // An empty content delta stays an empty delta: it is a wire signal, not thinking.
232
+ feed: text => (text.length === 0 ? [{ type: "text_delta", text }] : parser.feed(text)),
233
+ flush: () => parser.flush(),
234
+ dispose: () => parser.dispose(),
235
+ };
236
+ }
237
+
238
+ /** One-shot form for a non-streaming response body. */
239
+ export function splitInlineThinkContent(
240
+ models: string[] | undefined,
241
+ modelId: string | undefined,
242
+ budget: TranslatorBudget | undefined,
243
+ content: string,
244
+ ): AdapterEvent[] {
245
+ const splitter = createInlineThinkContentSplitter(models, modelId, budget);
246
+ try {
247
+ return [...splitter.feed(content), ...splitter.flush()];
248
+ } finally {
249
+ splitter.dispose();
250
+ }
251
+ }
@@ -250,6 +250,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
250
250
  });
251
251
  return {
252
252
  response,
253
+ abortSignal: requestAbortSignal,
253
254
  inputTokens: retry.inputTokens,
254
255
  contextInputEstimate: retry.contextInputEstimate,
255
256
  nameMap: retry.nameMap,
@@ -16,9 +16,10 @@ import {
16
16
  } from "../kiro-errors";
17
17
  import { parseKiroEvent } from "../kiro-events";
18
18
  import { noteKiroTransientThrottle } from "../kiro-retry";
19
- import { KiroThinkingParser } from "../kiro-thinking";
19
+ import { InlineThinkTagParser } from "../inline-think-tags";
20
20
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation";
21
21
  import { isValidKiroConversationId } from "../kiro-wire";
22
+ import { readDisplaySafeErrorPayloadText } from "../upstream-http-error";
22
23
  import { tagKiroReasoningBlob } from "./reasoning";
23
24
  import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage";
24
25
 
@@ -74,6 +75,7 @@ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetent
74
75
 
75
76
  interface KiroFallbackAttempt {
76
77
  response: Response;
78
+ abortSignal?: AbortSignal;
77
79
  inputTokens: number;
78
80
  contextInputEstimate: number;
79
81
  nameMap: Map<string, string>;
@@ -319,7 +321,7 @@ async function* parseKiroAttemptEvents(
319
321
  let authoritativeUsage: OcxUsage | undefined;
320
322
  let stopReason: string | undefined;
321
323
  const fallbackEvents: AdapterEvent[] = [];
322
- const thinking = new KiroThinkingParser(budget);
324
+ const thinking = new InlineThinkTagParser(budget);
323
325
 
324
326
  const retainedEventBytes = (event: AdapterEvent): number => Buffer.byteLength(JSON.stringify(event));
325
327
  const retainEvent = (event: AdapterEvent): void => {
@@ -1092,7 +1094,7 @@ export async function* parseKiroStream(
1092
1094
  firstResult.releaseRetained();
1093
1095
  fallback.releaseRequestBody?.();
1094
1096
  if (!fallback.response.ok) {
1095
- const payload = await fallback.response.text().catch(() => "");
1097
+ const payload = await readDisplaySafeErrorPayloadText(fallback.response, fallback.abortSignal);
1096
1098
  const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
1097
1099
  yield {
1098
1100
  type: "error",
@@ -1,4 +1,4 @@
1
- import { estimateTokens } from "../../lib/token-estimate";
1
+ import { estimateTokens, estimateTokensFromCharacterCounts } from "../../lib/token-estimate";
2
2
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../../providers/kiro-models";
3
3
  import { modelRecordValue } from "../../reasoning-effort";
4
4
  import { sniffImageDimensions } from "../anthropic-image-guard";
@@ -98,8 +98,9 @@ export function estimateKiroWireTokens(text: string, modelId: string): number {
98
98
  if (!text) return 0;
99
99
  const cjk = kiroCjkCount(text);
100
100
  if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
101
- const latinTokens = estimateKiroTokens("x".repeat(text.length - cjk), modelId);
102
- const cjkTokens = estimateKiroTokens("\uac00".repeat(cjk), modelId);
101
+ const prefixedModelId = `kiro/${modelId}`;
102
+ const latinTokens = estimateTokensFromCharacterCounts(text.length - cjk, 0, prefixedModelId);
103
+ const cjkTokens = estimateTokensFromCharacterCounts(0, cjk, prefixedModelId);
103
104
  return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
104
105
  }
105
106
 
@@ -105,11 +105,10 @@ export function resetMimoJwtCache(): void {
105
105
  inFlightJwt = null;
106
106
  }
107
107
 
108
- async function fetchJwt(signal?: AbortSignal): Promise<string> {
109
- // Bounded bootstrap: request-abort propagates, and a stalled bootstrap can never
110
- // hang past BOOTSTRAP_TIMEOUT_MS.
108
+ async function fetchJwt(): Promise<string> {
109
+ // Bounded bootstrap: a stalled bootstrap can never hang past BOOTSTRAP_TIMEOUT_MS. It carries
110
+ // no caller signal because concurrent requests share it; each caller aborts its own wait.
111
111
  const timeout = AbortSignal.timeout(BOOTSTRAP_TIMEOUT_MS);
112
- const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
113
112
  const response = await fetch(BOOTSTRAP_URL, {
114
113
  method: "POST",
115
114
  redirect: "manual",
@@ -118,7 +117,7 @@ async function fetchJwt(signal?: AbortSignal): Promise<string> {
118
117
  "User-Agent": randomUserAgent(),
119
118
  },
120
119
  body: JSON.stringify({ client: getMimoClientId() }),
121
- signal: combined,
120
+ signal: timeout,
122
121
  });
123
122
  if (!response.ok) {
124
123
  try { await response.body?.cancel(); } catch { /* already consumed */ }
@@ -159,25 +158,45 @@ async function fetchJwt(signal?: AbortSignal): Promise<string> {
159
158
  return data.jwt;
160
159
  }
161
160
 
161
+ function abortReason(signal: AbortSignal): unknown {
162
+ return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
163
+ }
164
+
165
+ /** Wait for the shared bootstrap, or stop waiting when this caller aborts; the bootstrap keeps running. */
166
+ function awaitForCaller(shared: Promise<string>, signal?: AbortSignal): Promise<string> {
167
+ if (!signal) return shared;
168
+ if (signal.aborted) return Promise.reject(abortReason(signal));
169
+ return new Promise<string>((resolve, reject) => {
170
+ const onAbort = () => reject(abortReason(signal));
171
+ signal.addEventListener("abort", onAbort, { once: true });
172
+ shared.then(
173
+ jwt => { signal.removeEventListener("abort", onAbort); resolve(jwt); },
174
+ error => { signal.removeEventListener("abort", onAbort); reject(error); },
175
+ );
176
+ });
177
+ }
178
+
162
179
  export async function getMimoJwt(signal?: AbortSignal): Promise<string> {
163
180
  if (cachedJwt && Date.now() < jwtExpiresAt - JWT_EXPIRY_BUFFER_MS) {
164
181
  return cachedJwt;
165
182
  }
183
+ if (signal?.aborted) throw abortReason(signal);
166
184
  // Single-flight: concurrent callers await the same bootstrap instead of issuing
167
- // parallel bootstraps.
185
+ // parallel bootstraps. One caller aborting must not fail the others, so the shared
186
+ // bootstrap is bound only to its timeout and each caller races it against its own signal.
168
187
  if (!inFlightJwt) {
169
- inFlightJwt = (async () => {
170
- try {
171
- const jwt = await fetchJwt(signal);
172
- cachedJwt = jwt;
173
- jwtExpiresAt = parseJwtExp(jwt);
174
- return jwt;
175
- } finally {
176
- inFlightJwt = null;
177
- }
178
- })();
188
+ const shared = fetchJwt().then(jwt => {
189
+ cachedJwt = jwt;
190
+ jwtExpiresAt = parseJwtExp(jwt);
191
+ return jwt;
192
+ });
193
+ inFlightJwt = shared;
194
+ // Registered before any waiter, so the slot is cleared first; it also handles a failure that
195
+ // arrives after every waiter has left. A reset during the flight owns the slot and is kept.
196
+ const release = () => { if (inFlightJwt === shared) inFlightJwt = null; };
197
+ shared.then(release, release);
179
198
  }
180
- return inFlightJwt;
199
+ return awaitForCaller(inFlightJwt, signal);
181
200
  }
182
201
 
183
202
  /**
@@ -223,7 +242,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
223
242
  name: "mimo-free",
224
243
 
225
244
  async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): Promise<AdapterRequest> {
226
- const jwt = await getMimoJwt();
245
+ const jwt = await getMimoJwt(incoming?.abortSignal);
227
246
 
228
247
  // Let the base adapter build the wire body (handles reasoning, tools, etc.)
229
248
  // but override the URL and headers after.
@@ -224,7 +224,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
224
224
  let reasoningContent = thinkingParts.map(p => p.thinking).join("");
225
225
  if (
226
226
  reasoningContent.length === 0
227
- && toolCalls.length > 0
227
+ && (toolCalls.length > 0 || thinkingParts.length > 0)
228
228
  && modelInList(provider.preserveReasoningContentModels, parsed.modelId)
229
229
  ) {
230
230
  const cached = toolCalls
@@ -235,11 +235,11 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
235
235
  if (cached.length > 0) {
236
236
  reasoningContent = [...new Set(cached)].join("\n");
237
237
  } else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
238
- // Fallback (extends #950, closes #1193): the replay cache is
238
+ // Fallback (extends #950 and #1193; fixes #5421): the replay cache is
239
239
  // bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
240
- // long sessions, and some tool rounds carry no recorded reasoning
241
- // at all. DeepSeek thinking mode rejects ANY tool_call assistant
242
- // message missing reasoning_content with HTTP 400, so inject a
240
+ // long sessions, and some thinking/tool rounds carry no recorded
241
+ // reasoning at all. DeepSeek thinking mode rejects replay without
242
+ // reasoning_content with HTTP 400, so inject a
243
243
  // minimal placeholder rather than emit a bare continuation the
244
244
  // upstream will reject. Scoped to requiresReasoningPlaceholderModels
245
245
  // (defaulting to the preserve list): preserve-listed providers with