@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-VuoiWj9J.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BBOZWGB6.css">
19
+ <script type="module" crossorigin src="/assets/index-Cz7CLdif.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-C5-RdDmD.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.55.0",
3
+ "version": "2.57.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -53,6 +53,7 @@
53
53
  "skill:surface:check": "bun scripts/generate-ocx-skill-surface.ts --check",
54
54
  "structure:index": "bun scripts/structure-ssot.ts --fix",
55
55
  "structure:check": "bun scripts/structure-ssot.ts",
56
+ "ratchet:update": "bun scripts/file-size-ratchet.ts --update",
56
57
  "generate:model-metadata": "bun scripts/generate-model-metadata.ts",
57
58
  "build:gui": "cd gui && bun install --frozen-lockfile && bun run build && cd .. && bun run prepare:package",
58
59
  "build:remote-workspace-helper": "cargo build --release --locked --manifest-path native/remote-workspace-helper/Cargo.toml",
@@ -75,11 +76,11 @@
75
76
  "@bufbuild/protobuf": "^2.14.0",
76
77
  "@modelcontextprotocol/sdk": "^1.30.0",
77
78
  "@napi-rs/keyring": "1.3.0",
78
- "bun": "1.4.2",
79
+ "bun": "1.4.0",
79
80
  "zod": "4.4.3"
80
81
  },
81
82
  "devDependencies": {
82
- "@types/bun": "1.4.2",
83
+ "@types/bun": "1.4.0",
83
84
  "typescript": "7.0.2"
84
85
  },
85
86
  "overrides": {
@@ -1,6 +1,7 @@
1
1
  import type { AdapterEvent, OcxParsedRequest } from "../types";
2
2
  import type { TranslatorBudget } from "../lib/translator-budget";
3
3
  import type { RequestExecutionBudget } from "../lib/request-execution-budget";
4
+ import type { AttemptRecoveryKind } from "../usage/log";
4
5
  import type { AdapterTierMetadata } from "../providers/fastwire";
5
6
 
6
7
  /** Metadata about the caller's incoming request, for auth-forwarding adapters. */
@@ -20,6 +21,16 @@ export interface IncomingMeta {
20
21
  * the anthropic and openai-chat adapters; others ignore it.
21
22
  */
22
23
  imageTierBias?: number;
24
+ /**
25
+ * The enclosing request's send budget, for adapters that own their upstream transport.
26
+ *
27
+ * A `runTurn` adapter never receives an `AdapterFetchContext`, so the budget that bounds every
28
+ * other leg could not reach it: Cursor re-sends a whole turn up to three times inside one
29
+ * adapter call, and the request cap counted that as one send. Optional, and absent means
30
+ * unlimited, because adapter unit tests build a meta with neither a budget nor a request
31
+ * behind it (#4546).
32
+ */
33
+ sendBudget?: RequestExecutionBudget;
23
34
  }
24
35
 
25
36
  export interface ProviderAdapter {
@@ -147,6 +158,16 @@ export interface AdapterFetchContext {
147
158
  * adapter entry as one send is how a nested 3x3 ladder stayed invisible to a request cap.
148
159
  */
149
160
  sendBudget?: RequestExecutionBudget;
161
+ /**
162
+ * Observes every physical upstream send this adapter makes, including its own inner retries.
163
+ *
164
+ * `ordinal` counts from 1 within this fetch call, so a caller that already recorded the entry
165
+ * send records only ordinals above 1 and an adapter that never retries internally logs exactly
166
+ * what it logs today. Kiro and Cursor were unpinnable without this: they report one send per
167
+ * adapter call however many requests they actually made, so their inner ladders were invisible
168
+ * to `sendCount` and no regression could assert a count for them (#4546).
169
+ */
170
+ onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
150
171
  }
151
172
 
152
173
  /**
@@ -4,6 +4,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
4
4
  import { buildSystemPrompt } from "../coding-agent/protocol";
5
5
  import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type SpawnFn } from "../coding-agent/turn";
6
6
  import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "./profiles";
7
+ import { guardCodeBuddyScaffolding } from "./scaffold-guard";
7
8
 
8
9
  export type { SpawnFn } from "../coding-agent/turn";
9
10
  export type CodeBuddyAdapterDeps = CodingAgentDeps;
@@ -75,7 +76,7 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu
75
76
  provider,
76
77
  parsed,
77
78
  incoming,
78
- emit,
79
+ emit: guardCodeBuddyScaffolding(emit),
79
80
  buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov),
80
81
  buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
81
82
  deps,
@@ -0,0 +1,248 @@
1
+ import type { AdapterEvent } from "../../types";
2
+
3
+ /** Error code for a CodeBuddy turn whose output contains vendor agent scaffolding. */
4
+ export const CODEBUDDY_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";
5
+
6
+ // The observed control protocol uses FULLWIDTH VERTICAL LINE (U+FF5C). Detection stays
7
+ // deliberately narrower than the marker spelling: a calls control line must be followed by an
8
+ // invoke line for a functions.* tool. That distinguishes an agent scaffold from prose quoting or
9
+ // discussing one tag.
10
+ const DSML_CALLS_LINE = "<||dsml|| calls>";
11
+ const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"functions.";
12
+
13
+ export interface CodeBuddyScaffoldFilterResult {
14
+ /** Bytes released from a suffix withheld by an earlier event on this channel. */
15
+ releasedPending: string;
16
+ /** Safe bytes belonging to the event currently being processed. */
17
+ text: string;
18
+ /** The earlier pending event still owns the extended candidate. */
19
+ pendingContinues: boolean;
20
+ fail: boolean;
21
+ }
22
+
23
+ interface ScanResult {
24
+ safe: string;
25
+ held: string;
26
+ fail: boolean;
27
+ fence: "`" | "~" | null;
28
+ lineStart: boolean;
29
+ }
30
+
31
+ function prefixAtEnd(text: string, at: number, expected: string): boolean {
32
+ const rest = text.slice(at).toLowerCase();
33
+ return rest.length < expected.length && expected.startsWith(rest);
34
+ }
35
+
36
+ /**
37
+ * Scan complete bytes and retain only a bounded suffix that can still become a control sequence.
38
+ *
39
+ * Control tags are recognized only at column zero and outside fenced Markdown. Inline code,
40
+ * quoted strings, blockquotes, indented source, and prose all add syntax before the tag and are
41
+ * therefore forwarded unchanged. A calls line alone is harmless; refusal requires the observed
42
+ * two-line calls-plus-functions-invoke grammar.
43
+ */
44
+ function scan(
45
+ text: string,
46
+ initialFence: "`" | "~" | null,
47
+ initialLineStart: boolean,
48
+ ): ScanResult {
49
+ let fence = initialFence;
50
+ let lineStart = initialLineStart;
51
+ let index = 0;
52
+
53
+ while (index < text.length) {
54
+ if (lineStart) {
55
+ const fenceMarkers = fence ? [fence.repeat(3)] : ["```", "~~~"];
56
+ const completeFence = fenceMarkers.find(marker => text.startsWith(marker, index));
57
+ if (completeFence) {
58
+ fence = fence ? null : (completeFence[0] as "`" | "~");
59
+ index += completeFence.length;
60
+ lineStart = false;
61
+ continue;
62
+ }
63
+ if (fenceMarkers.some(marker => prefixAtEnd(text, index, marker))) {
64
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
65
+ }
66
+
67
+ if (!fence) {
68
+ const lowered = text.slice(index).toLowerCase();
69
+ if (lowered.startsWith(DSML_CALLS_LINE)) {
70
+ const afterCalls = index + DSML_CALLS_LINE.length;
71
+ let invokeAt = -1;
72
+ if (text[afterCalls] === "\n") invokeAt = afterCalls + 1;
73
+ else if (text[afterCalls] === "\r" && text[afterCalls + 1] === "\n") invokeAt = afterCalls + 2;
74
+ else if (afterCalls === text.length || (text[afterCalls] === "\r" && afterCalls + 1 === text.length)) {
75
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
76
+ }
77
+
78
+ if (invokeAt >= 0) {
79
+ const invokeRest = text.slice(invokeAt).toLowerCase();
80
+ if (invokeRest.startsWith(DSML_INVOKE_PREFIX)) {
81
+ return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
82
+ }
83
+ if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
84
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
85
+ }
86
+ }
87
+ } else if (prefixAtEnd(text, index, DSML_CALLS_LINE)) {
88
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
89
+ }
90
+ }
91
+ }
92
+
93
+ const char = text[index]!;
94
+ index += 1;
95
+ lineStart = char === "\n";
96
+ }
97
+
98
+ return { safe: text, held: "", fail: false, fence, lineStart };
99
+ }
100
+
101
+ /** Streaming DSML control-sequence filter for one text or reasoning channel. */
102
+ export class CodeBuddyScaffoldFilter {
103
+ private pending = "";
104
+ private failed = false;
105
+ private fence: "`" | "~" | null = null;
106
+ private lineStart = true;
107
+
108
+ /** True while an earlier event owns an unresolved marker or fence prefix. */
109
+ hasPending(): boolean {
110
+ return this.pending.length > 0;
111
+ }
112
+
113
+ push(chunk: string): CodeBuddyScaffoldFilterResult {
114
+ if (this.failed) {
115
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
116
+ }
117
+ if (!chunk) {
118
+ return {
119
+ releasedPending: "",
120
+ text: "",
121
+ pendingContinues: this.hasPending(),
122
+ fail: false,
123
+ };
124
+ }
125
+
126
+ const priorPending = this.pending;
127
+ const result = scan(priorPending + chunk, this.fence, this.lineStart);
128
+ this.pending = result.held;
129
+ this.fence = result.fence;
130
+ this.lineStart = result.lineStart;
131
+ this.failed = result.fail;
132
+
133
+ const releasedLength = Math.min(priorPending.length, result.safe.length);
134
+ return {
135
+ releasedPending: result.safe.slice(0, releasedLength),
136
+ text: result.safe.slice(releasedLength),
137
+ pendingContinues: priorPending.length > 0 && result.safe.length === 0 && result.held.length > 0,
138
+ fail: result.fail,
139
+ };
140
+ }
141
+
142
+ /** Release a suffix that never completed the two-line control grammar. */
143
+ flush(): CodeBuddyScaffoldFilterResult {
144
+ if (this.failed) {
145
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
146
+ }
147
+ const text = this.pending;
148
+ this.pending = "";
149
+ return { releasedPending: text, text: "", pendingContinues: false, fail: false };
150
+ }
151
+ }
152
+
153
+ function codeBuddyScaffoldErrorMessage(): string {
154
+ return "CodeBuddy CLI emitted vendor tool-call markup in an assistant output channel. This route"
155
+ + " runs the CLI with its own tools and MCP servers disabled and Codex owns tool control, so"
156
+ + " the turn was refused rather than forwarding or executing vendor agent scaffolding.";
157
+ }
158
+
159
+ /** Guard both streamed channels while preserving event order around withheld marker prefixes. */
160
+ export function guardCodeBuddyScaffolding(emit: (event: AdapterEvent) => void): (event: AdapterEvent) => void {
161
+ const textFilter = new CodeBuddyScaffoldFilter();
162
+ const thinkingFilter = new CodeBuddyScaffoldFilter();
163
+ type PendingChannel = "text" | "thinking";
164
+ type EventSlot = { resolved: boolean; event?: AdapterEvent };
165
+ const eventQueue: EventSlot[] = [];
166
+ const pendingSlots = new Map<PendingChannel, EventSlot>();
167
+ let closed = false;
168
+
169
+ const channelEvent = (channel: PendingChannel, text: string): AdapterEvent => channel === "text"
170
+ ? { type: "text_delta", text }
171
+ : { type: "thinking_delta", thinking: text };
172
+
173
+ const drainResolved = (): void => {
174
+ while (eventQueue[0]?.resolved) {
175
+ const slot = eventQueue.shift()!;
176
+ if (slot.event) emit(slot.event);
177
+ }
178
+ };
179
+
180
+ const enqueueResolved = (event: AdapterEvent): void => {
181
+ eventQueue.push({ resolved: true, event });
182
+ drainResolved();
183
+ };
184
+
185
+ const resolvePendingSlot = (channel: PendingChannel, text: string): void => {
186
+ const slot = pendingSlots.get(channel);
187
+ if (!slot) return;
188
+ slot.resolved = true;
189
+ if (text) slot.event = channelEvent(channel, text);
190
+ pendingSlots.delete(channel);
191
+ drainResolved();
192
+ };
193
+
194
+ const enqueuePendingSlot = (channel: PendingChannel): void => {
195
+ const slot: EventSlot = { resolved: false };
196
+ eventQueue.push(slot);
197
+ pendingSlots.set(channel, slot);
198
+ };
199
+
200
+ const flushAllPending = (): void => {
201
+ for (const channel of ["text", "thinking"] as const) {
202
+ if (!pendingSlots.has(channel)) continue;
203
+ const filter = channel === "text" ? textFilter : thinkingFilter;
204
+ resolvePendingSlot(channel, filter.flush().releasedPending);
205
+ }
206
+ drainResolved();
207
+ };
208
+
209
+ const refuse = (): void => {
210
+ if (closed) return;
211
+ flushAllPending();
212
+ closed = true;
213
+ emit({
214
+ type: "error",
215
+ message: codeBuddyScaffoldErrorMessage(),
216
+ status: 502,
217
+ errorType: "upstream_error",
218
+ code: CODEBUDDY_SCAFFOLD_ERROR_CODE,
219
+ retryable: false,
220
+ });
221
+ };
222
+
223
+ return (event: AdapterEvent): void => {
224
+ if (closed) return;
225
+ if (event.type === "text_delta" || event.type === "thinking_delta") {
226
+ const channel: PendingChannel = event.type === "text_delta" ? "text" : "thinking";
227
+ const filter = channel === "text" ? textFilter : thinkingFilter;
228
+ const hadPending = filter.hasPending();
229
+ const cleaned = filter.push(event.type === "text_delta" ? event.text : event.thinking);
230
+ if (hadPending && !cleaned.pendingContinues) resolvePendingSlot(channel, cleaned.releasedPending);
231
+ if (cleaned.text) {
232
+ enqueueResolved(event.type === "text_delta"
233
+ ? { ...event, text: cleaned.text }
234
+ : { ...event, thinking: cleaned.text });
235
+ }
236
+ if (filter.hasPending() && !cleaned.pendingContinues) enqueuePendingSlot(channel);
237
+ if (cleaned.fail) refuse();
238
+ return;
239
+ }
240
+ if (event.type === "done" || event.type === "error" || event.type === "incomplete") {
241
+ flushAllPending();
242
+ closed = true;
243
+ emit(event);
244
+ return;
245
+ }
246
+ enqueueResolved(event);
247
+ };
248
+ }
@@ -469,7 +469,7 @@ async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContex
469
469
  const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000);
470
470
  const callerSignal = ctx?.abortSignal ?? new AbortController().signal;
471
471
  try {
472
- return await executor(request.url, {
472
+ return await (ctx?.executor ?? executor)(request.url, {
473
473
  method: request.method,
474
474
  headers: request.headers,
475
475
  body: request.body,
@@ -217,7 +217,13 @@ export type RoutingCommentaryDecision =
217
217
  | { kind: "flush" }
218
218
  | { kind: "hallucination" };
219
219
 
220
- const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
220
+ // The Korean alternative is deliberately asymmetric: it has a left boundary and no right one.
221
+ // Korean attaches particles directly to the noun, so the real sentences this detector exists to
222
+ // catch read "네이티브 셸이 차단되어..." and "네이티브 셸과 Read가...". A mirrored
223
+ // (?![\p{L}\p{M}\p{N}_]) lookahead would see the 이/과 particle as a letter and stop matching
224
+ // every one of them, which is why the negative cases below only probe the left side. Adding the
225
+ // right boundary looks like an obvious fix and disables the check; do not.
226
+ const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(?<![\p{L}\p{M}\p{N}_])네이티브\s*(?:셸|쉘)/giu;
221
227
  const ROUTING_TOOL_HINT =
222
228
  /(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
223
229
  const ROUTING_FAILURE_CLAIM =
@@ -276,7 +282,7 @@ export class CursorRoutingCommentarySniffer {
276
282
  private matchesHallucination(): boolean {
277
283
  if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
278
284
  const nativeTools = new Set(
279
- [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
285
+ [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase() ?? "shell"),
280
286
  );
281
287
  if (nativeTools.size === 0) return false;
282
288
  return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
@@ -1,6 +1,8 @@
1
1
  import type { CursorRunRequest, CursorServerMessage } from "./types";
2
2
  import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
3
- import { abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
3
+ import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
4
+ import type { AttemptRecoveryKind } from "../../usage/log";
5
+ import { SendBudgetExhaustedError, abortError, retryBackoffDelayMs, sleepWithAbort } from "../../lib/upstream-retry";
4
6
  import { debugProviderDiagnostic } from "../../lib/debug";
5
7
  import { isCursorRootEnvelopeError, safeCursorErrorMessage } from "./cursor-errors";
6
8
 
@@ -11,6 +13,27 @@ export const CURSOR_RETRY_ATTEMPTS = 3;
11
13
  export const CURSOR_RETRY_BASE_MS = 250;
12
14
  export const CURSOR_RETRY_MAX_MS = 2_000;
13
15
 
16
+ /**
17
+ * Fixed identity for the Cursor upstream in the request budget's target ledger. A literal, not
18
+ * anything derived from the turn: the ledger is read back in diagnostics, so it must not become
19
+ * a place where a session or credential identity leaks.
20
+ */
21
+ export const CURSOR_BUDGET_TARGET_KEY = "cursor";
22
+
23
+ /**
24
+ * How one Cursor turn participates in the enclosing logical request (#4546).
25
+ *
26
+ * Both fields are optional and the whole object defaults to empty, which is what keeps a
27
+ * context-free unit call unlimited: this transport is exercised directly by tests that build no
28
+ * request at all, and a mandatory budget would have made every one of them a budget test.
29
+ */
30
+ export interface CursorTurnExecutionOptions {
31
+ /** Absent means unlimited; present means every retry is a physical send the request pays for. */
32
+ sendBudget?: RequestExecutionBudget;
33
+ /** Observes each physical run request; `ordinal` counts from 1 within this turn. */
34
+ onPhysicalSend?: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) => void;
35
+ }
36
+
14
37
  /**
15
38
  * True only for clearly transient failures that occur BEFORE the run request is committed to the
16
39
  * wire (connection refused/reset/timeout, immediate HTTP/2 GOAWAY, gRPC/Connect "unavailable").
@@ -66,6 +89,11 @@ function requestUncommitted(transport: CursorTransport): boolean {
66
89
  * - the failing transport reports the run request was not committed to the wire,
67
90
  * - the error is a transient pre-commit failure.
68
91
  * Otherwise the error propagates (the adapter maps it to a user-facing message).
92
+ *
93
+ * `execution` carries the enclosing request's send budget. Each attempt here is a real re-send
94
+ * of the whole turn, so an outer cap that counted one adapter entry counted at most a third of
95
+ * what went upstream; when a budget is present every attempt is admitted against it and an
96
+ * exhausted request stops before opening another transport (#4546).
69
97
  */
70
98
  export async function runCursorTurnWithRetry(
71
99
  makeTransport: (input: CursorTransportFactoryInput) => CursorTransport,
@@ -73,9 +101,26 @@ export async function runCursorTurnWithRetry(
73
101
  request: CursorRunRequest,
74
102
  signal: AbortSignal | undefined,
75
103
  onEvent: (message: CursorServerMessage, transport: CursorTransport) => void,
104
+ execution: CursorTurnExecutionOptions = {},
76
105
  ): Promise<void> {
77
106
  for (let attempt = 0; ; attempt++) {
78
107
  if (signal?.aborted) throw abortError(signal);
108
+ // Admitted before the transport is built: a refused send must not open a connection, and
109
+ // the refusal must reach the adapter as the typed exhaustion rather than as a run failure
110
+ // that the retry predicate below could read as transient.
111
+ const decision = execution.sendBudget?.reserveDispatch({
112
+ sendClass: "transient",
113
+ targetKey: CURSOR_BUDGET_TARGET_KEY,
114
+ });
115
+ if (decision && (!decision.allowed || !decision.permit.use())) {
116
+ throw new SendBudgetExhaustedError(CURSOR_BUDGET_TARGET_KEY);
117
+ }
118
+ execution.onPhysicalSend?.({
119
+ ordinal: attempt + 1,
120
+ // Cursor retries only pre-commit transport failures, so every retry send is the
121
+ // connection-reset class; there is no re-send of a turn the server may have accepted.
122
+ ...(attempt > 0 ? { recovery: "connection-reset" as const } : {}),
123
+ });
79
124
  const transport = makeTransport(input);
80
125
  let emittedAny = false;
81
126
  let closed = false;
@@ -403,6 +403,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
403
403
  }
404
404
  }
405
405
  },
406
+ // Cursor's retry ladder re-sends the WHOLE turn, so each attempt is a physical send
407
+ // the enclosing request pays for. A meta without a budget -- every adapter unit test,
408
+ // and any caller predating this -- keeps the adapter's own three attempts (#4546).
409
+ incoming.sendBudget ? { sendBudget: incoming.sendBudget } : {},
406
410
  );
407
411
  };
408
412
 
@@ -796,14 +796,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
796
796
  // body, URL or credential.
797
797
  const requestedTextFormat = parsed.options.textFormat;
798
798
  if (requestedTextFormat) {
799
- if (provider.googleMode === "cloud-code-assist") {
800
- // Not implemented or verified by opencodex for the Cloud Code Assist envelope,
801
- // including Claude models served through it. This is not a claim that the
802
- // upstream cannot do it — silence would return unconstrained prose as success,
803
- // which is the failure this fix exists to remove.
799
+ if (provider.googleMode === "cloud-code-assist" && !parsed.modelId.startsWith("gemini-")) {
800
+ // Not implemented by opencodex for non-Gemini models (including Claude)
801
+ // served through the Cloud Code Assist envelope. This is not a claim that
802
+ // the upstream cannot do it — silence would return unconstrained prose as success,
803
+ // which is the failure this refusal exists to prevent.
804
804
  throw new Error(
805
- "google cloud-code-assist structured output is not implemented by opencodex — "
806
- + "remove response_format or route this model through AI Studio or Vertex",
805
+ "google cloud-code-assist structured output is not implemented by opencodex for non-Gemini models — "
806
+ + "remove response_format or route this model through a direct provider",
807
807
  );
808
808
  }
809
809
  if (isImageCapableModel(parsed.modelId)) {
@@ -45,6 +45,10 @@ import {
45
45
  type KiroWireClient,
46
46
  } from "./wire";
47
47
 
48
+ /** The physical-send observer an `AdapterFetchContext` may carry, and the record it receives. */
49
+ type KiroPhysicalSendObserver = NonNullable<AdapterFetchContext["onPhysicalSend"]>;
50
+ type KiroPhysicalSend = Parameters<KiroPhysicalSendObserver>[0];
51
+
48
52
  // Adapter
49
53
  export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter {
50
54
  // Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
@@ -62,6 +66,25 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
62
66
  // Captured the same way as the abort signal, because the text-fallback rebuild below runs
63
67
  // outside the fetchResponse frame and used to construct a context without either (#4546).
64
68
  let requestSendBudget: RequestExecutionBudget | undefined;
69
+ // Captured for the same reason, and needed for the same leg to be COUNTABLE rather than merely
70
+ // bounded: the rebuild's sends were paid for out of the request budget but reported by nobody,
71
+ // so no regression could pin how many requests one Kiro turn actually makes.
72
+ let requestOnPhysicalSend: KiroPhysicalSendObserver | undefined;
73
+ // One ordinal sequence across the whole turn. `fetchKiroWithRetry` numbers from 1 inside each
74
+ // call, and the caller reads ordinal 1 as the send it already recorded itself; forwarding the
75
+ // rebuild's raw ordinals would therefore drop its first send — the very send that makes the
76
+ // fallback a second request rather than a continuation of the first.
77
+ let physicalSendsObserved = 0;
78
+ const forwardPhysicalSend = (
79
+ send: KiroPhysicalSend,
80
+ ordinalBase: number,
81
+ defaultRecovery?: KiroPhysicalSend["recovery"],
82
+ ): void => {
83
+ const ordinal = ordinalBase + send.ordinal;
84
+ if (ordinal > physicalSendsObserved) physicalSendsObserved = ordinal;
85
+ const recovery = send.recovery ?? defaultRecovery;
86
+ requestOnPhysicalSend?.({ ordinal, ...(recovery ? { recovery } : {}) });
87
+ };
65
88
 
66
89
  const build = async (
67
90
  parsed: OcxParsedRequest,
@@ -208,6 +231,9 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
208
231
  retryBodyReservation.commitRetained();
209
232
  retryBodyRetained = true;
210
233
  budget.releaseRetained(retryBodyUpperBound - retryBodyBytes, { kind: "request_copies" });
234
+ // Fixed before the rebuild dispatches, so the leg's ordinals continue the first attempt's
235
+ // sequence even though this call's own counter restarts at 1.
236
+ const fallbackOrdinalBase = physicalSendsObserved;
211
237
  const response = await fetchKiroWithRetry(retry.request, {
212
238
  abortSignal: requestAbortSignal,
213
239
  returnRawErrors: true,
@@ -215,6 +241,12 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
215
241
  // The text-fallback rebuild used to construct a fresh context and drop the budget,
216
242
  // so everything after the first send escaped the per-request cap.
217
243
  ...(requestSendBudget ? { sendBudget: requestSendBudget } : {}),
244
+ // And reported nothing, so the sends it paid for were invisible. Its own first send is
245
+ // the completion retry itself: the first attempt produced progress without a final
246
+ // answer, which is the same recovery class the generic empty-completion guard records.
247
+ ...(requestOnPhysicalSend
248
+ ? { onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, fallbackOrdinalBase, "empty-completion") }
249
+ : {}),
218
250
  });
219
251
  return {
220
252
  response,
@@ -286,7 +318,16 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
286
318
  // both the first Kiro request and its one allowed completion retry.
287
319
  if (ctx?.abortSignal) requestAbortSignal = ctx.abortSignal;
288
320
  if (ctx?.sendBudget) requestSendBudget = ctx.sendBudget;
289
- return fetchKiroWithRetry(request, ctx);
321
+ if (ctx?.onPhysicalSend) requestOnPhysicalSend = ctx.onPhysicalSend;
322
+ // Reset per fetch call, because `ordinal` is defined within one call and the caller records
323
+ // ordinal 1 of each new attempt itself. The text fallback that follows this attempt then
324
+ // continues THIS attempt's sequence rather than an earlier one's.
325
+ physicalSendsObserved = 0;
326
+ // Routed through the same forwarder as the fallback so both legs share one ordinal
327
+ // sequence; a context without an observer is passed through untouched.
328
+ return fetchKiroWithRetry(request, requestOnPhysicalSend
329
+ ? { ...ctx, onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, 0) }
330
+ : ctx);
290
331
  },
291
332
 
292
333
  formatErrorBody(status: number, headers: Headers, payloadText: string): string {
@@ -38,7 +38,12 @@ import {
38
38
  validateKiroConversationState,
39
39
  type KiroTurn,
40
40
  } from "./conversation";
41
- import { injectKiroThinkingTags, kiroNativeEffortField, KIRO_NATIVE_EFFORTS } from "./reasoning";
41
+ import {
42
+ injectKiroThinkingTags,
43
+ kiroNativeEffortField,
44
+ kiroReasoningContent,
45
+ KIRO_NATIVE_EFFORTS,
46
+ } from "./reasoning";
42
47
  import { kiroPayloadMessages, userContentText } from "./usage";
43
48
  import {
44
49
  kiroToolWireNames,
@@ -388,7 +393,11 @@ export function buildKiroPayload(
388
393
  assistantResponseMessage: {
389
394
  content: turn.content,
390
395
  ...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}),
391
- ...(turn.redactedReasoning ? { reasoningContent: { redactedContent: turn.redactedReasoning } } : {}),
396
+ // Replayed on the field it was received on: the GPT-5.6 signature is not base64 and is
397
+ // rejected when sent as `redactedContent`.
398
+ ...(turn.redactedReasoning
399
+ ? { reasoningContent: kiroReasoningContent(turn.redactedReasoning) }
400
+ : {}),
392
401
  },
393
402
  }
394
403
  : {
@@ -447,7 +456,12 @@ export function buildKiroPayload(
447
456
  if (!KIRO_NATIVE_EFFORTS.includes(effort)) {
448
457
  throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`);
449
458
  }
450
- payload.additionalModelRequestFields = { [effortField]: { effort } };
459
+ // Model eligibility still owns unsupported-effort validation above; wire eligibility
460
+ // is narrower for luna/terra, whose unverified rungs retain the thinking-tag path.
461
+ const verifiedEffortField = kiroNativeEffortField(parsed.modelId, effort);
462
+ if (verifiedEffortField) {
463
+ payload.additionalModelRequestFields = { [verifiedEffortField]: { effort } };
464
+ }
451
465
  }
452
466
  if (profileArn) payload.profileArn = profileArn;
453
467
  return { payload, nameMap, conversationId, completionMode };