@bitkyc08/opencodex 2.67.0-preview.20260926 → 2.68.0-preview.20260927

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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-B5-cAEd2.js +51 -0
  3. package/gui/dist/assets/App-DCBismRi.css +1 -0
  4. package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-BcXblvem.js +86 -0
  7. package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-B3VRRYSj.js +0 -51
  254. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  255. package/gui/dist/assets/Tray-TYAA0EQY.js +0 -1
  256. package/gui/dist/assets/index-9Ju7nHsu.js +0 -86
  257. package/gui/dist/assets/tray-data-xKfS2gSG.js +0 -1
@@ -16,6 +16,12 @@
16
16
  * Strip, do not translate. The `turnNviewN` ids are turn-scoped and opaque, and the
17
17
  * response carries no mapping from them to a URL, so there is nothing to convert them
18
18
  * into. Structured `url_citation` annotations are a separate path and are untouched.
19
+ *
20
+ * Only citation spans are stripped. The same delimiters carry other inline directives
21
+ * that the Codex App renders, such as `\uE200visualize\uE202{"path":…}\uE201` from the
22
+ * bundled visualize plugin (#6039). A span is a citation only when the keyword between
23
+ * START and its first SEPARATOR or END is one of `CITATION_KEYWORDS`; any other span
24
+ * passes through byte for byte.
19
25
  */
20
26
 
21
27
  /** Opens a citation span. */
@@ -25,6 +31,12 @@ export const CITATION_MARKER_SEPARATOR = "\uE202";
25
31
  /** Closes a citation span. */
26
32
  export const CITATION_MARKER_END = "\uE201";
27
33
 
34
+ /**
35
+ * Keywords that mark a span as a ChatGPT citation. `filecite` is the uploaded-file variant
36
+ * of `cite` and uses the same `turnN…` reference grammar.
37
+ */
38
+ const CITATION_KEYWORDS: readonly string[] = ["cite", "filecite"];
39
+
28
40
  /** True when the text contains any of the three delimiters. Cheap pre-check. */
29
41
  export function hasCitationMarker(text: string): boolean {
30
42
  return text.includes(CITATION_MARKER_START)
@@ -33,34 +45,15 @@ export function hasCitationMarker(text: string): boolean {
33
45
  }
34
46
 
35
47
  /**
36
- * Remove every complete `START … END` span from a whole string.
48
+ * Upper bound on the length of a span (START through END inclusive).
37
49
  *
38
- * A START with no END is left alone rather than truncating the remainder: an unterminated
39
- * marker is malformed input, and dropping everything after it would delete real answer
40
- * text. A stray SEPARATOR or END outside a span is also left alone for the same reason —
41
- * this function only removes what it can prove is a citation span.
50
+ * A real citation is `cite` plus a few turn-scoped ids, so it is far under this. Without a
51
+ * bound, a backend that emits a START and never terminates it makes the streaming filter
52
+ * withhold the rest of the response. A non-citation directive keeps later STARTs literal
53
+ * only within the same bound, so one unterminated directive cannot hide a real citation for
54
+ * the rest of the message.
42
55
  */
43
- export function stripCitationMarkers(text: string): string {
44
- if (!text.includes(CITATION_MARKER_START)) return text;
45
- // Walk START-delimited segments exactly like the streaming filter below: a START whose
46
- // own segment (up to the next START) contains an END within the span bound is a span and
47
- // is removed; a START that is superseded by another START before any END, or whose span
48
- // exceeds MAX_CITATION_SPAN_LENGTH, is malformed text and stays verbatim. Pairing an
49
- // earlier malformed START with a later span's END would delete real answer text and,
50
- // worse, disagree with what the streaming deltas already emitted (#3843). The bound is
51
- // shared with the streaming filter for the same reason: a span it has already released
52
- // as over-bound must not be swallowed here when the END finally arrives.
53
- let start = text.indexOf(CITATION_MARKER_START);
54
- let out = text.slice(0, start);
55
- while (start !== -1) {
56
- const nextStart = text.indexOf(CITATION_MARKER_START, start + 1);
57
- const segment = text.slice(start, nextStart === -1 ? text.length : nextStart);
58
- const end = segment.indexOf(CITATION_MARKER_END, 1);
59
- out += end === -1 || end + 1 > MAX_CITATION_SPAN_LENGTH ? segment : segment.slice(end + 1);
60
- start = nextStart;
61
- }
62
- return out;
63
- }
56
+ const MAX_CITATION_SPAN_LENGTH = 4_096;
64
57
 
65
58
  export interface CitationMarkerFilter {
66
59
  /** Feed one streaming delta; returns the portion safe to emit now. */
@@ -70,63 +63,137 @@ export interface CitationMarkerFilter {
70
63
  }
71
64
 
72
65
  /**
73
- * Upper bound on the length of a citation span (START through END inclusive), and therefore
74
- * on the text the streaming filter withholds for one unterminated START.
66
+ * Remove every complete citation span from a whole string.
75
67
  *
76
- * A real span is `cite` plus a few turn-scoped ids, so it is far under this. Without a
77
- * bound, a backend that emits a START and never terminates it makes `held` grow for the
78
- * whole response, and every later delta re-scans that accumulated prefix. The whole-string
79
- * strip applies the same bound so both paths classify a span identically regardless of how
80
- * the text was chunked.
68
+ * This runs the streaming filter over the whole text, so the bridge's re-stripped
69
+ * `output_text.done` always equals the concatenated deltas it already emitted (#3843).
70
+ * Anything that is not a complete citation span stays verbatim: an unterminated START, a
71
+ * stray SEPARATOR or END, and every non-citation directive.
81
72
  */
82
- const MAX_CITATION_SPAN_LENGTH = 4_096;
73
+ export function stripCitationMarkers(text: string): string {
74
+ if (!text.includes(CITATION_MARKER_START)) return text;
75
+ const filter = createCitationMarkerFilter();
76
+ return filter.push(text) + filter.flush();
77
+ }
83
78
 
84
79
  /**
85
- * Streaming filter.
80
+ * Streaming filter, as a per-character state machine so the result never depends on how the
81
+ * text was split into deltas.
86
82
  *
87
- * A marker can straddle a delta boundary — `\uE200cite` in one chunk and the rest in the
88
- * next — so a stateless per-delta strip would emit the tail of a span it never recognized.
89
- * This holds back the text from an unterminated START and releases it once the END arrives
90
- * (removed) or the stream ends (verbatim, so nothing the model actually said is lost).
83
+ * - `text`: ordinary text, emitted at once. A START opens `keyword`.
84
+ * - `keyword`: the START and keyword so far are withheld while the keyword can still become
85
+ * a citation keyword. A SEPARATOR after a citation keyword enters `citation`; an END right
86
+ * after one removes the span. Any other keyword releases the text and enters `other`.
87
+ * - `citation`: withheld until its END, then removed. A new START means this one was
88
+ * malformed, so it is released verbatim; so is a span that reaches the length bound.
89
+ * - `other`: the keyword of a non-citation span, emitted at once. A START here means the
90
+ * earlier one was malformed and opens a new span; a SEPARATOR enters `opaque`.
91
+ * - `opaque`: the body of a non-citation directive such as `visualize` (#6039), emitted at
92
+ * once. STARTs inside it are payload, not new spans, until its END or the length bound.
91
93
  *
92
- * A span that grows past `MAX_CITATION_SPAN_LENGTH` is malformed ordinary text, so
93
- * it is released verbatim instead of withheld; a later START can still open a valid span.
94
+ * A stream that ends while text is withheld releases it verbatim, so nothing the model
95
+ * actually said is lost.
94
96
  */
95
97
  export function createCitationMarkerFilter(): CitationMarkerFilter {
96
- // Text from an open START that has not been terminated yet.
98
+ let mode: "text" | "keyword" | "citation" | "other" | "opaque" = "text";
99
+ // The withheld START, keyword, and body in `keyword` and `citation` modes.
97
100
  let held = "";
101
+ // Characters from START so far in `other` and `opaque` modes.
102
+ let spanLength = 0;
103
+ const isKeywordPrefix = (keyword: string): boolean => CITATION_KEYWORDS.some(k => k.startsWith(keyword));
104
+
98
105
  return {
99
106
  push(delta: string): string {
100
- const combined = held + delta;
101
- held = "";
102
- let start = combined.indexOf(CITATION_MARKER_START);
103
- if (start === -1) return combined;
104
- let out = combined.slice(0, start);
105
- // Walk START-delimited segments independently so an earlier malformed START is never
106
- // paired with a later span's END (the whole-string strip would do exactly that).
107
- while (start !== -1) {
108
- const nextStart = combined.indexOf(CITATION_MARKER_START, start + 1);
109
- const segment = combined.slice(start, nextStart === -1 ? combined.length : nextStart);
110
- const end = segment.indexOf(CITATION_MARKER_END, 1);
111
- if (end !== -1 && end + 1 <= MAX_CITATION_SPAN_LENGTH) {
112
- // A complete span: drop it, keep whatever trails it inside this segment.
113
- out += segment.slice(end + 1);
114
- } else if (end === -1 && nextStart === -1 && segment.length <= MAX_CITATION_SPAN_LENGTH) {
115
- // Only a bounded trailing span can still be completed by a later delta.
116
- held = segment;
117
- } else {
118
- // Superseded by a later START, or over the bound (with or without a late END):
119
- // ordinary text, emitted verbatim so neither the retained text nor the per-delta
120
- // rescan grows without limit.
121
- out += segment;
107
+ let out = "";
108
+ let i = 0;
109
+ while (i < delta.length) {
110
+ if (mode === "text") {
111
+ const next = delta.indexOf(CITATION_MARKER_START, i);
112
+ if (next === -1) {
113
+ out += delta.slice(i);
114
+ break;
115
+ }
116
+ out += delta.slice(i, next);
117
+ held = CITATION_MARKER_START;
118
+ mode = "keyword";
119
+ i = next + 1;
120
+ continue;
121
+ }
122
+ const ch = delta[i]!;
123
+ if (mode === "other" || mode === "opaque") {
124
+ if (spanLength + 1 > MAX_CITATION_SPAN_LENGTH) {
125
+ // Past the bound this is ordinary text again; handle the character in `text`.
126
+ mode = "text";
127
+ continue;
128
+ }
129
+ if (ch === CITATION_MARKER_START && mode === "other") {
130
+ held = CITATION_MARKER_START;
131
+ mode = "keyword";
132
+ } else {
133
+ out += ch;
134
+ spanLength += 1;
135
+ if (ch === CITATION_MARKER_END) mode = "text";
136
+ else if (ch === CITATION_MARKER_SEPARATOR) mode = "opaque";
137
+ }
138
+ i += 1;
139
+ continue;
140
+ }
141
+ i += 1;
142
+ if (ch === CITATION_MARKER_START) {
143
+ // The withheld START was malformed: release it and open a new span here.
144
+ out += held;
145
+ held = CITATION_MARKER_START;
146
+ mode = "keyword";
147
+ continue;
148
+ }
149
+ if (mode === "citation") {
150
+ if (ch === CITATION_MARKER_END) {
151
+ held = "";
152
+ mode = "text";
153
+ continue;
154
+ }
155
+ held += ch;
156
+ // One more character plus an END would exceed the bound, so this is not a span.
157
+ if (held.length >= MAX_CITATION_SPAN_LENGTH) {
158
+ out += held;
159
+ held = "";
160
+ mode = "text";
161
+ }
162
+ continue;
163
+ }
164
+ // mode === "keyword"
165
+ if (ch === CITATION_MARKER_SEPARATOR || ch === CITATION_MARKER_END) {
166
+ if (CITATION_KEYWORDS.includes(held.slice(1))) {
167
+ if (ch === CITATION_MARKER_END) {
168
+ held = "";
169
+ mode = "text";
170
+ } else {
171
+ held += ch;
172
+ mode = "citation";
173
+ }
174
+ } else {
175
+ out += held + ch;
176
+ spanLength = held.length + 1;
177
+ held = "";
178
+ mode = ch === CITATION_MARKER_END ? "text" : "opaque";
179
+ }
180
+ continue;
181
+ }
182
+ held += ch;
183
+ if (!isKeywordPrefix(held.slice(1))) {
184
+ out += held;
185
+ spanLength = held.length;
186
+ held = "";
187
+ mode = "other";
122
188
  }
123
- start = nextStart;
124
189
  }
125
190
  return out;
126
191
  },
127
192
  flush(): string {
128
193
  const rest = held;
129
194
  held = "";
195
+ spanLength = 0;
196
+ mode = "text";
130
197
  return rest;
131
198
  },
132
199
  };
@@ -1,11 +1,22 @@
1
- /** Hosted tools rejected by specific native model slugs or exact provider destinations. */
1
+ /**
2
+ * Hosted tools rejected by specific native model slugs or exact provider destinations.
3
+ *
4
+ * Add a row only for a destination that refuses the tool itself.
5
+ */
2
6
  const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
3
7
  match: (model: string, baseUrl?: string) => boolean;
4
8
  tools: ReadonlySet<string>;
5
9
  }> = [
6
10
  {
7
- match: (model, baseUrl) => model === "grok-4.6"
8
- && baseUrl?.replace(/\/+$/, "") === "https://opencode.ai/zen/go/v1",
11
+ // MiMo rejects hosted search with "tool type 'web_search' is not supported by this gateway phase" (#5501).
12
+ match: (_model, baseUrl) => {
13
+ if (!baseUrl) return false;
14
+ try {
15
+ return /(?:^|\.)xiaomimimo\.com$/i.test(new URL(baseUrl).hostname);
16
+ } catch {
17
+ return false;
18
+ }
19
+ },
9
20
  tools: new Set(["web_search", "web_search_preview"]),
10
21
  },
11
22
  ];
@@ -9,7 +9,7 @@ type InputBlock =
9
9
  | { type: "input_text"; text: string }
10
10
  | { type: "text"; text: string }
11
11
  | { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
12
- | { type: "input_video"; video_url?: string }
12
+ | { type: "input_video"; video_url?: string; processing?: string }
13
13
  // codex-rs protocol/src/models.rs sends audio as input_audio with an audio_url.
14
14
  | { type: "input_audio"; audio_url?: string; format?: string }
15
15
  | { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
@@ -62,7 +62,8 @@ export function inputContentParts(blocks: unknown): string | OcxContentPart[] {
62
62
  // the request never carried, which is worse than dropping malformed input.
63
63
  } else if (block.type === "input_video") {
64
64
  const videoUrl = nonEmptyString(block.video_url);
65
- if (videoUrl) parts.push({ type: "video", videoUrl });
65
+ const processing = nonEmptyString((block as { processing?: string }).processing);
66
+ if (videoUrl) parts.push({ type: "video", videoUrl, ...(processing ? { processing } : {}) });
66
67
  } else if (block.type === "input_audio") {
67
68
  // Upstream Codex sends input_audio with an audio_url (codex-rs
68
69
  // protocol/src/models.rs). The IR has no audio carrier and no adapter consumes
@@ -27,6 +27,7 @@ import { isObj, inputContentParts, outputTextOf, outputToToolResultContent, tool
27
27
  import { mapToolChoice, buildTools, customToolNamespaces } from "./parser-tools";
28
28
  import { parseTextFormat } from "./parser-text-format";
29
29
  import { externalTaskInputContent } from "./task-input";
30
+ import { normalizeVisualizationContext } from "./visualization-directives";
30
31
 
31
32
  /**
32
33
  * Wrap a remembered proxy-side signature as provider metadata for a replayed tool call.
@@ -644,7 +645,9 @@ export function parseRequest(
644
645
  return {
645
646
  modelId: data.model,
646
647
  ...(data.previous_response_id ? { previousResponseId: data.previous_response_id } : {}),
647
- context,
648
+ // Codex App visualization references in the private-use form are invisible to some models;
649
+ // hand every model the app's ASCII directive instead (visualization-directives.ts).
650
+ context: normalizeVisualizationContext(context),
648
651
  stream: data.stream === true,
649
652
  options,
650
653
  _rawBody: body,
@@ -14,6 +14,10 @@ const inputImageBlockSchema = z.object({
14
14
  const inputVideoBlockSchema = z.object({
15
15
  type: z.literal("input_video"),
16
16
  video_url: z.string().min(1),
17
+ // Gemini agentic video understanding (#3271). z.object() strips unknown keys,
18
+ // so without declaring it here the mode is dropped before any adapter sees it
19
+ // and the request silently degrades to frame-by-frame decoding.
20
+ processing: z.string().min(1).optional(),
17
21
  });
18
22
  const inputFileBlockSchema = z.object({
19
23
  type: z.literal("input_file"),
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Codex App visualization references for models that cannot see private-use characters.
3
+ *
4
+ * The bundled Visualize skill teaches the model to answer with
5
+ *
6
+ * \uE200visualize\uE202{"path":"/abs/chart.html"}\uE201
7
+ *
8
+ * and the Codex App renders that span as an inline visualization. Some providers drop Basic
9
+ * Multilingual Plane private-use characters before the model sees them (every Claude route checked
10
+ * on 2026-09-27, direct and through Cursor), so the model reads and writes a bare
11
+ * `visualize{"path":...}` that the app shows verbatim.
12
+ *
13
+ * The app does not need the private-use form. Its own renderer rewrites each span into the plain
14
+ * directive `::codex-inline-vis{path="/abs/chart.html"}` before parsing, and it renders that
15
+ * directive when it appears directly. Rewriting the span the same way in model-visible text gives
16
+ * every model an ASCII instruction it can read and repeat.
17
+ *
18
+ * The rules mirror the app (26.924.22138, `f2` in app-shared) with two deliberate differences: code
19
+ * blocks are rewritten too, because the skill's example sits in a fenced block, and the skill's
20
+ * `<absolute-path>/<title>.html` placeholder is accepted so the model still sees the template.
21
+ * Matching follows the app's regex `/\uE200visualize\uE202([^\uE201]+)\uE201/g` exactly, scanned in
22
+ * linear time. Anything the app would keep as-is is kept as-is here.
23
+ *
24
+ * Only the parsed context is rewritten. The raw request body, which native passthrough serializes
25
+ * and which is stored for `previous_response_id` replay, is never touched.
26
+ */
27
+ import { posix } from "node:path";
28
+ import type { OcxContentPart, OcxContext, OcxMessage, OcxTextContent } from "../types";
29
+
30
+ const START = "\uE200";
31
+ const END = "\uE201";
32
+ const PREFIX = `${START}visualize\uE202`;
33
+ const INLINE_DIRECTIVE = "codex-inline-vis";
34
+ const LIVE_DIRECTIVE = "codex-live-vis";
35
+ /** The placeholder path in the Visualize skill's own template. */
36
+ const TEMPLATE_PATH = "<absolute-path>/<title>.html";
37
+ const HTML_BASENAME = /^[a-z0-9]+(?:-[a-z0-9]+)*\.html$/;
38
+ const PARENT_SEGMENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
39
+ const UNSAFE_ATTRIBUTE = /["\n\r]/;
40
+
41
+ /** The app's absolute-path test: POSIX (not `//`), drive letter, UNC with either slash. */
42
+ function isAbsoluteAppPath(path: string): boolean {
43
+ return (path.startsWith("/") && !path.startsWith("//"))
44
+ || /^[A-Za-z]:[\\/]/.test(path)
45
+ || /^\\\\[^\\]+\\[^\\]+/.test(path)
46
+ || /^\/\/[^/]+\/[^/]+/.test(path);
47
+ }
48
+
49
+ interface VisualizationReference {
50
+ path: string | null;
51
+ title?: string;
52
+ type?: "inline" | "live";
53
+ mode?: "wide";
54
+ }
55
+
56
+ /** The app's payload schema: `path` is required but nullable; the rest are optional and typed. */
57
+ function parseReference(value: unknown): VisualizationReference | undefined {
58
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
59
+ const record = value as Record<string, unknown>;
60
+ if (!("path" in record) || (record.path !== null && typeof record.path !== "string")) return undefined;
61
+ if (record.title !== undefined && typeof record.title !== "string") return undefined;
62
+ if (record.type !== undefined && record.type !== "inline" && record.type !== "live") return undefined;
63
+ if (record.mode !== undefined && record.mode !== "wide") return undefined;
64
+ if (record.wide !== undefined && typeof record.wide !== "boolean") return undefined;
65
+ return {
66
+ path: record.path as string | null,
67
+ ...(record.title !== undefined ? { title: record.title as string } : {}),
68
+ ...(record.type !== undefined ? { type: record.type as "inline" | "live" } : {}),
69
+ ...(record.mode !== undefined ? { mode: "wide" as const } : {}),
70
+ };
71
+ }
72
+
73
+ /** The ASCII directive for one span payload, or undefined when the app would keep the span. */
74
+ function toAsciiDirective(payload: string): string | undefined {
75
+ const isJson = payload.startsWith("{");
76
+ let raw: unknown = { path: payload };
77
+ if (isJson) {
78
+ try {
79
+ raw = JSON.parse(payload);
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ const reference = parseReference(raw);
85
+ if (!reference) return undefined;
86
+ const live = reference.type === "live";
87
+ const { path } = reference;
88
+ if (path === null) return live ? `::${LIVE_DIRECTIVE}{}` : undefined;
89
+
90
+ const template = isJson && path === TEMPLATE_PATH;
91
+ if (!template) {
92
+ const basename = posix.basename(path.replaceAll("\\", "/"));
93
+ if (PARENT_SEGMENT.test(path) || UNSAFE_ATTRIBUTE.test(path) || !HTML_BASENAME.test(basename)) return undefined;
94
+ if (!isAbsoluteAppPath(path) && (isJson || path !== basename)) return undefined;
95
+ }
96
+ const attribute = template || isAbsoluteAppPath(path) ? "path" : "file";
97
+ const title = reference.title !== undefined && !UNSAFE_ATTRIBUTE.test(reference.title)
98
+ ? ` title="${reference.title}"`
99
+ : "";
100
+ const mode = !live && reference.mode === "wide" ? ` mode="wide"` : "";
101
+ return `::${live ? LIVE_DIRECTIVE : INLINE_DIRECTIVE}{${attribute}="${path}"${title}${mode}}`;
102
+ }
103
+
104
+ /**
105
+ * Rewrite every visualization span in `text` to the app's ASCII directive.
106
+ *
107
+ * Returns the same string when nothing changes. The scan finds the same matches as the app's regex:
108
+ * a prefix, one or more characters that are not END, then END. When no END follows a prefix, no
109
+ * later prefix can match either, so the scan stops instead of rescanning the tail for every prefix.
110
+ */
111
+ export function normalizeVisualizationText(text: string): string {
112
+ let prefixAt = text.indexOf(PREFIX);
113
+ if (prefixAt === -1) return text;
114
+ let out = "";
115
+ let copiedTo = 0;
116
+ while (prefixAt !== -1) {
117
+ const payloadStart = prefixAt + PREFIX.length;
118
+ const endAt = text.indexOf(END, payloadStart);
119
+ if (endAt === -1) break;
120
+ if (endAt === payloadStart) {
121
+ // Empty payload: the regex moves on one character and tries again.
122
+ prefixAt = text.indexOf(PREFIX, prefixAt + 1);
123
+ continue;
124
+ }
125
+ const directive = toAsciiDirective(text.slice(payloadStart, endAt));
126
+ if (directive !== undefined) {
127
+ out += text.slice(copiedTo, prefixAt) + directive;
128
+ copiedTo = endAt + 1;
129
+ }
130
+ prefixAt = text.indexOf(PREFIX, endAt + 1);
131
+ }
132
+ return copiedTo === 0 ? text : out + text.slice(copiedTo);
133
+ }
134
+
135
+ function normalizeParts<P extends { type: string }>(parts: P[]): P[] {
136
+ let changed = false;
137
+ const next = parts.map((part) => {
138
+ if (part.type !== "text") return part;
139
+ const text = (part as unknown as OcxTextContent).text;
140
+ if (typeof text !== "string") return part;
141
+ const normalized = normalizeVisualizationText(text);
142
+ if (normalized === text) return part;
143
+ changed = true;
144
+ return { ...part, text: normalized };
145
+ });
146
+ return changed ? next : parts;
147
+ }
148
+
149
+ function normalizeContent<P extends { type: string }>(content: string | P[]): string | P[] {
150
+ return typeof content === "string" ? normalizeVisualizationText(content) : normalizeParts(content);
151
+ }
152
+
153
+ function normalizeMessage(message: OcxMessage): OcxMessage {
154
+ if (message.role === "assistant") {
155
+ const content = normalizeParts(message.content);
156
+ return content === message.content ? message : { ...message, content };
157
+ }
158
+ const content = normalizeContent<OcxContentPart>(message.content);
159
+ return content === message.content ? message : { ...message, content } as OcxMessage;
160
+ }
161
+
162
+ /**
163
+ * Rewrite visualization spans in the conversation text a routed model reads: the system prompt,
164
+ * string message content, and text parts of every role. Other fields, including images, tool calls,
165
+ * reasoning parts, and tool definitions, are returned by reference. The context itself is returned
166
+ * unchanged when no text changed.
167
+ */
168
+ export function normalizeVisualizationContext(context: OcxContext): OcxContext {
169
+ let changed = false;
170
+ const systemPrompt = context.systemPrompt?.map((text) => {
171
+ const normalized = normalizeVisualizationText(text);
172
+ if (normalized !== text) changed = true;
173
+ return normalized;
174
+ });
175
+ const messages = context.messages.map((message) => {
176
+ const normalized = normalizeMessage(message);
177
+ if (normalized !== message) changed = true;
178
+ return normalized;
179
+ });
180
+ if (!changed) return context;
181
+ return { ...context, ...(systemPrompt ? { systemPrompt } : {}), messages };
182
+ }
@@ -129,6 +129,11 @@ interface NativeChatSseOptions {
129
129
  translatorBudget: TranslatorBudget;
130
130
  signal: AbortSignal;
131
131
  stallTimeoutSec?: number;
132
+ /**
133
+ * The upstream is local infrastructure (loopback / private / `.local` / `.lan`); an unset
134
+ * `stallTimeoutSec` then resolves to disabled so a slow local model is not cut mid-turn.
135
+ */
136
+ localUpstream?: boolean;
132
137
  onFirstOutput?: () => void;
133
138
  onUsage: (usage: OcxUsage) => void;
134
139
  onTerminal?: (status: number, message?: string) => void;
@@ -157,7 +162,12 @@ export function nativeChatSse(
157
162
  let settled = false;
158
163
  let cancelled = false;
159
164
  let cancelledBySignal = false;
160
- const stallMs = resolveStallTimeoutSec(options.stallTimeoutSec) * 1_000;
165
+ const stallSec = resolveStallTimeoutSec(options.stallTimeoutSec, { localUpstream: options.localUpstream });
166
+ // 0 is an explicit disabled budget (local upstream, or the operator's stallTimeoutSec: 0). Map
167
+ // it to an Infinity deadline so every `now >= pullDeadline` / `remaining <= 0` check stays
168
+ // false and the read race is skipped, instead of arming an immediate kill on the first pull.
169
+ const stallEnabled = stallSec > 0;
170
+ const stallMs = stallEnabled ? stallSec * 1_000 : Number.POSITIVE_INFINITY;
161
171
  // Count only active upstream consumption; downstream backpressure must not
162
172
  // consume the provider's inactivity allowance. Comments and empty deltas do.
163
173
  let remainingStallMs = stallMs;
@@ -332,17 +342,19 @@ export function nativeChatSse(
332
342
  let timeout: ReturnType<typeof setTimeout> | undefined;
333
343
  let read: Awaited<ReturnType<typeof reader.read>>;
334
344
  try {
335
- read = await Promise.race([
336
- reader.read(),
337
- new Promise<never>((_, reject) => {
338
- const check = () => {
339
- const remaining = pullDeadline - performance.now();
340
- if (remaining <= 0) reject(stalled);
341
- else timeout = setTimeout(check, Math.min(remaining, 2_147_483_647));
342
- };
343
- check();
344
- }),
345
- ]);
345
+ read = stallEnabled
346
+ ? await Promise.race([
347
+ reader.read(),
348
+ new Promise<never>((_, reject) => {
349
+ const check = () => {
350
+ const remaining = pullDeadline - performance.now();
351
+ if (remaining <= 0) reject(stalled);
352
+ else timeout = setTimeout(check, Math.min(remaining, 2_147_483_647));
353
+ };
354
+ check();
355
+ }),
356
+ ])
357
+ : await reader.read();
346
358
  } finally {
347
359
  if (timeout !== undefined) clearTimeout(timeout);
348
360
  }