@bitkyc08/opencodex 2.67.0 → 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-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -380,7 +380,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
380
380
  sawPartialText: false,
381
381
  sawPartialThinking: false,
382
382
  sawTerminalResult: false,
383
- openToolCallId: undefined,
383
+ openToolBlocks: new Map(),
384
384
  partialToolCallIds: toolBridge ? new Set<string>() : undefined,
385
385
  };
386
386
 
@@ -422,6 +422,29 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
422
422
  break;
423
423
  }
424
424
  if (message.type === "system" && message.subtype === "init") initValidated = true;
425
+ const rawEvent = message.type === "stream_event" && message.event !== null && typeof message.event === "object"
426
+ ? message.event as Record<string, unknown>
427
+ : undefined;
428
+ const rawBlock = rawEvent?.content_block;
429
+ if (
430
+ !initValidated
431
+ && rawEvent?.type === "content_block_start"
432
+ && rawBlock !== null
433
+ && typeof rawBlock === "object"
434
+ && !Array.isArray(rawBlock)
435
+ && (rawBlock as Record<string, unknown>).type === "tool_use"
436
+ ) {
437
+ emitOnce({
438
+ type: "error",
439
+ message: "Coding-agent CLI called a tool before the tool bridge init handshake completed.",
440
+ status: 502,
441
+ errorType: "upstream_error",
442
+ code: "tool_bridge_init_missing",
443
+ retryable: false,
444
+ });
445
+ kill();
446
+ break;
447
+ }
425
448
  }
426
449
  const mappedEvents = mapStreamMessageToEvents(message, state);
427
450
  if (toolBridge && state.uncapturedToolUse) {
@@ -451,24 +474,6 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
451
474
  break;
452
475
  }
453
476
  if (toolBridge && event.type === "tool_call_start") {
454
- // The catalog is only advertised once the CLI has acknowledged the bridge server in
455
- // its init handshake. A tool call that arrives before that acknowledgement means the
456
- // model acted on a catalog this bridge never validated, so fail closed before the
457
- // call is counted or renamed. Checking at arrival matters: a later init frame used to
458
- // set initValidated and let an early call finish as a successful done(tool_use).
459
- if (!initValidated) {
460
- emitOnce({
461
- type: "error",
462
- message: "Coding-agent CLI called a tool before the tool bridge init handshake completed.",
463
- status: 502,
464
- errorType: "upstream_error",
465
- code: "tool_bridge_init_missing",
466
- retryable: false,
467
- });
468
- failClosed = true;
469
- kill();
470
- break;
471
- }
472
477
  toolCallStarts += 1;
473
478
  if (toolCallStarts > toolBridge.maxTurnToolCalls) {
474
479
  emitOnce({
@@ -527,8 +532,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
527
532
  toolBridge
528
533
  && !terminalEmitted
529
534
  && event.type === "done"
530
- && toolCallStarts > 0
531
- && (state.completedToolCalls ?? 0) !== toolCallStarts
535
+ && (state.toolBlockStarts ?? 0) > 0
536
+ && (state.completedToolCalls ?? 0) !== (state.toolBlockStarts ?? 0)
532
537
  ) {
533
538
  // A terminal result that arrives while a captured tool call is still open must not
534
539
  // become a successful completion the client can accept. The message_stop check after
@@ -552,8 +557,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
552
557
  toolBridge
553
558
  && !terminalEmitted
554
559
  && event.type === "done"
555
- && toolCallStarts > 0
556
- && (state.completedToolCalls ?? 0) === toolCallStarts
560
+ && (state.toolBlockStarts ?? 0) > 0
561
+ && (state.completedToolCalls ?? 0) === (state.toolBlockStarts ?? 0)
557
562
  ) {
558
563
  // Every captured call completed and the CLI settled with a successful result before
559
564
  // message_stop (instead of parking on the never-answering capture server). Emitting
@@ -573,8 +578,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
573
578
  toolBridge
574
579
  && !terminalEmitted
575
580
  && state.sawMessageStop
576
- && toolCallStarts > 0
577
- && (state.completedToolCalls ?? 0) !== toolCallStarts
581
+ && (state.toolBlockStarts ?? 0) > 0
582
+ && (state.completedToolCalls ?? 0) !== (state.toolBlockStarts ?? 0)
578
583
  ) {
579
584
  emitOnce({
580
585
  type: "error",
@@ -588,8 +593,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
588
593
  break;
589
594
  }
590
595
  if (toolBridge && !terminalEmitted && state.sawMessageStop && (state.completedToolCalls ?? 0) > 0) {
591
- // No init re-check here: a completed call implies a tool_call_start was mapped, and the
592
- // arrival-time gate above already refuses any start that lands before the handshake.
596
+ // Raw tool_use starts are gated before buffering, so every completed call was admitted
597
+ // after the bridge init handshake.
593
598
  // The capture-only MCP handler never answers, so the CLI parks after message_stop.
594
599
  // The completed tool_use blocks are this turn's structured output: end the leg here
595
600
  // and terminate the tree; the client executes, and the next request continues.
@@ -51,6 +51,7 @@ export {
51
51
 
52
52
  export {
53
53
  streamChatEventsWithResetRetry,
54
+ devinStatedResetWaitMs,
54
55
  STATED_RESET_MAX_REPLAYS,
55
56
  STATED_RESET_MAX_WAIT_MS,
56
57
  type StatedResetRetryOptions,
@@ -17,21 +17,26 @@ export const STATED_RESET_MAX_WAIT_MS = 1_800_000;
17
17
  /** Absolute maximum cumulative allowance, including explicit overrides. */
18
18
  export const STATED_RESET_WAIT_CEILING_MS = 3_600_000;
19
19
 
20
- function statedResetMaxWaitMs(): number {
20
+ function statedResetMaxWaitMs(defaultMs = STATED_RESET_MAX_WAIT_MS): number {
21
21
  const raw = process.env.OPENCODEX_DEVIN_STATED_RESET_WAIT_MS?.trim();
22
- if (!raw) return STATED_RESET_MAX_WAIT_MS;
22
+ if (!raw) return defaultMs;
23
23
  const parsed = Number(raw);
24
- if (!Number.isFinite(parsed) || parsed < 0) return STATED_RESET_MAX_WAIT_MS;
24
+ if (!Number.isFinite(parsed) || parsed < 0) return defaultMs;
25
25
  // Zero explicitly disables local waiting. Values above one hour are capped.
26
26
  return Math.min(Math.floor(parsed), STATED_RESET_WAIT_CEILING_MS);
27
27
  }
28
28
  export const statedResetMaxWaitMsForTests = statedResetMaxWaitMs;
29
29
 
30
+ export function devinStatedResetWaitMs(): number {
31
+ return statedResetMaxWaitMs(0);
32
+ }
33
+
30
34
  export interface StatedResetRetryOptions {
31
35
  /** Test seam: defaults to the real cloud stream. */
32
36
  stream?: (req: CloudChatRequest) => AsyncGenerator<CloudChatEvent>;
33
37
  /** Test seam: must either honour the whole delay or reject on cancellation. */
34
38
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
39
+ onWaitHeartbeat?: () => void;
35
40
  maxReplays?: number;
36
41
  /** CUMULATIVE wait allowance, not a fresh allowance on every failure. */
37
42
  maxWaitMs?: number;
@@ -136,7 +141,16 @@ export async function* streamChatEventsWithResetRetry(
136
141
  // scheduling: waking a few milliseconds late must not reject an already
137
142
  // approved one-hour retry. No later wait can spend this allowance again.
138
143
  waitedMs += waitMs;
139
- await sleep(waitMs, req.signal);
144
+ const heartbeat = options?.onWaitHeartbeat;
145
+ if (waitMs > 0) heartbeat?.();
146
+ const beat = heartbeat && waitMs > 0
147
+ ? setInterval(heartbeat, Math.min(500, Math.max(100, Math.floor(waitMs / 2))))
148
+ : undefined;
149
+ try {
150
+ await sleep(waitMs, req.signal);
151
+ } finally {
152
+ if (beat !== undefined) clearInterval(beat);
153
+ }
140
154
  if (req.signal?.aborted) throw abortError(req.signal);
141
155
  }
142
156
  }
@@ -9,7 +9,7 @@
9
9
  import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTool, OcxToolCall, OcxToolResultMessage, OcxUsage } from "../types";
10
10
  import { namespacedToolName } from "../types";
11
11
  import type { IncomingMeta, ProviderAdapter } from "./base";
12
- import { streamChatEventsWithResetRetry, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
12
+ import { streamChatEventsWithResetRetry, devinStatedResetWaitMs, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
13
13
  import type { ContentPart } from "./devin/cloud-direct/chat";
14
14
  import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog";
15
15
  import { collapseDevinModelUid } from "./devin/live-models";
@@ -642,11 +642,12 @@ export function createDevinAdapter(
642
642
  const maxOutputTokens = resolveDevinMaxOutputTokens(
643
643
  provider, modelUid, parsed.options.maxOutputTokens,
644
644
  );
645
+ // A combo child has not committed an outer response yet. Holding its preflight through
646
+ // a reset wait would also hold the next-target fallback with no client keepalive.
647
+ const resetWaitMs = incoming.comboAttempt ? 0 : devinStatedResetWaitMs();
645
648
  // An admitted HTTP turn owns globally shared capacity until this call
646
- // emits. Never retain that capacity while waiting out a provider 429:
647
- // preserve the typed reset delay in generated diagnostic wording,
648
- // never the raw trailer text that may reflect a credential. The
649
- // refusal returns immediately so the caller can release its slot.
649
+ // emits. Without an explicit wait allowance, preserve the typed reset
650
+ // delay in generated diagnostic wording and return immediately.
650
651
  for await (const event of streamChatEventsWithResetRetry({
651
652
  apiKey,
652
653
  apiServerUrl: host,
@@ -665,7 +666,9 @@ export function createDevinAdapter(
665
666
  },
666
667
  signal: incoming.abortSignal,
667
668
  }, {
668
- maxWaitMs: 0,
669
+ maxWaitMs: resetWaitMs,
670
+ onWaitHeartbeat: resetWaitMs > 0 && parsed.stream
671
+ ? () => emit({ type: "heartbeat", preflightReady: true }) : undefined,
669
672
  execution: {
670
673
  executor: incoming.providerFetch,
671
674
  sendBudget: incoming.sendBudget,
@@ -14,10 +14,10 @@ import {
14
14
  retryBackoffDelayMs,
15
15
  sleepWithAbort,
16
16
  SendBudgetExhaustedError,
17
+ TRANSIENT_RETRY_MAX_ATTEMPTS,
17
18
  isConnectionResetError,
18
19
  } from "../lib/upstream-retry";
19
20
 
20
- const GOOGLE_RETRY_ATTEMPTS = 3;
21
21
  const GOOGLE_RETRY_BASE_MS = 250;
22
22
  const GOOGLE_RETRY_MAX_MS = 2_000;
23
23
 
@@ -60,7 +60,7 @@ export async function fetchGoogleWithRetry(
60
60
  let retryDelayMs = 0;
61
61
  let sendClass: SendClass = "transient";
62
62
  let recovery: AttemptRecoveryKind | undefined;
63
- for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
63
+ for (let attempt = 0; attempt < TRANSIENT_RETRY_MAX_ATTEMPTS; attempt++) {
64
64
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
65
65
  try {
66
66
  const res = await send({ url: activeRequest.url, sendClass, recovery,
@@ -105,7 +105,7 @@ export async function fetchGoogleWithRetry(
105
105
  continue;
106
106
  }
107
107
  }
108
- if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
108
+ if (!retryableGoogleStatus(res.status) || attempt === TRANSIENT_RETRY_MAX_ATTEMPTS - 1) {
109
109
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
110
110
  }
111
111
  // A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
@@ -141,7 +141,7 @@ export async function fetchGoogleWithRetry(
141
141
  throw err;
142
142
  }
143
143
  lastError = err;
144
- if (attempt === GOOGLE_RETRY_ATTEMPTS - 1) throw err;
144
+ if (attempt === TRANSIENT_RETRY_MAX_ATTEMPTS - 1) throw err;
145
145
  sendClass = "transient";
146
146
  recovery = isConnectionResetError(err) ? "connection-reset" : undefined;
147
147
  retryDelayMs = retryBackoffDelayMs(attempt, {
@@ -231,6 +231,72 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
231
231
  * surfaced on Claude-on-Antigravity; the guard lives here because this is where the parts are
232
232
  * built. Mirrors the Anthropic adapter's own empty-block guard (src/adapters/anthropic.ts).
233
233
  */
234
+ /**
235
+ * A video URI Gemini fetches on its own behalf, as a `file_data` reference.
236
+ *
237
+ * Deliberately an allowlist of the forms Google documents, not "anything that is
238
+ * not a data: URL". `file_data` tells Gemini to go and get the bytes; pointing it
239
+ * at an arbitrary host would either fail upstream or make the proxy the reason a
240
+ * caller's private URL got dereferenced by Google. Anything not matched here
241
+ * keeps the existing `[video: …]` text marker.
242
+ *
243
+ * Returns the uri alone: the documented REST example for a YouTube part carries
244
+ * `file_data.file_uri` and nothing else, and the Files API knows the type of what
245
+ * it stored. An invented `mime_type` would be a guess on both paths.
246
+ *
247
+ * https://ai.google.dev/gemini-api/docs/generate-content/video-understanding
248
+ */
249
+ function geminiFetchableVideoUri(url: string): string | null {
250
+ let parsed: URL;
251
+ try {
252
+ parsed = new URL(url);
253
+ } catch {
254
+ return null;
255
+ }
256
+ if (parsed.protocol !== "https:") return null;
257
+
258
+ const host = parsed.hostname.toLowerCase();
259
+ const youtubeHosts = new Set([
260
+ "youtube.com",
261
+ "www.youtube.com",
262
+ "m.youtube.com",
263
+ "music.youtube.com",
264
+ "youtu.be",
265
+ "www.youtube-nocookie.com",
266
+ "youtube-nocookie.com",
267
+ ]);
268
+ if (youtubeHosts.has(host)) return url;
269
+
270
+ // The Files API resource form, https://generativelanguage.googleapis.com/v1beta/files/<id>.
271
+ // Anchored at the start so the resumable-upload endpoint (/upload/v1beta/files/<id>) does
272
+ // not match: that URL is not a readable resource, and passing it as `file_data.file_uri`
273
+ // would have Gemini dereference something it cannot read. The version segment stays loose
274
+ // because this service is reachable as v1, v1beta and v1alpha.
275
+ if (host === "generativelanguage.googleapis.com" && /^\/v1[a-z0-9]*\/files\/[^/]+$/.test(parsed.pathname)) {
276
+ return url;
277
+ }
278
+
279
+ return null;
280
+ }
281
+
282
+ /**
283
+ * The caller's requested video mode as GenerateContent spells it.
284
+ *
285
+ * `media_processing` sits on the part beside `inline_data`/`file_data` and takes
286
+ * `STATIC` (the default) or `AGENTIC`. `processing: "agentic"` — the spelling in
287
+ * the original request and in Google's Interactions API — is a different API and
288
+ * is ignored here, so forwarding it verbatim would have looked like a
289
+ * pass-through while agentic mode never actually engaged.
290
+ *
291
+ * Upper-cased and forwarded rather than checked against our own copy of the enum:
292
+ * that list is Google's to extend, and a stale allowlist here would silently
293
+ * downgrade a caller using a newer mode. An unrecognized value fails upstream
294
+ * naming the field, which is a better failure than us dropping it.
295
+ */
296
+ function geminiMediaProcessing(processing: string | undefined): string | undefined {
297
+ return processing ? processing.toUpperCase() : undefined;
298
+ }
299
+
234
300
  const GEMINI_EMPTY_PLACEHOLDER = "(empty)";
235
301
  const GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER = "(empty tool output)";
236
302
  const GEMINI_MISSING_TOOL_RESULT = "[missing tool_result for this tool_use in history]";
@@ -341,10 +407,34 @@ function messagesToGeminiFormat(
341
407
  continue;
342
408
  }
343
409
  if (p.type === "video") {
410
+ // `media_processing` rides on the PART, so it applies to inline bytes
411
+ // exactly as it does to a fetched uri — emitting it on only one of the
412
+ // two would silently drop the mode for data: URLs.
413
+ const mediaProcessing = geminiMediaProcessing(p.processing);
414
+ const processingPart = mediaProcessing ? { media_processing: mediaProcessing } : {};
415
+
416
+ // Gemini accepts inline video bytes in the same Part union as images.
344
417
  const data = parseDataUrl(p.videoUrl);
345
- // Gemini accepts inline video bytes in the same Part union as images. Arbitrary
346
- // remote URLs are not valid fileData references, so retain only a short marker.
347
- parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
418
+ if (data) {
419
+ parts.push({
420
+ inline_data: { mime_type: data.mediaType, data: data.base64 },
421
+ ...processingPart,
422
+ });
423
+ continue;
424
+ }
425
+ // Two URI forms Gemini fetches itself: a YouTube watch URL and a Files API
426
+ // uri. Those ARE valid file_data references (#3271), and flattening them to
427
+ // a text marker was the whole reason agentic video could not be reached —
428
+ // the video never arrived as a video. Every other remote URL keeps the
429
+ // marker: we have no mime type for it and no evidence Gemini can fetch it.
430
+ const fileUri = geminiFetchableVideoUri(p.videoUrl);
431
+ if (fileUri) {
432
+ // Emitted only when the caller asked for a mode, so no existing
433
+ // request gains a field it did not have.
434
+ parts.push({ file_data: { file_uri: fileUri }, ...processingPart });
435
+ continue;
436
+ }
437
+ parts.push({ text: `[video: ${p.videoUrl}]` });
348
438
  continue;
349
439
  }
350
440
  if (p.type === "document") {
@@ -63,6 +63,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
63
63
  let requestSnapshot: OcxParsedRequest | undefined;
64
64
  let firstRequestBodyBytes = 0;
65
65
  let requestAbortSignal: AbortSignal | undefined;
66
+ let requestExecutor: typeof globalThis.fetch | undefined;
66
67
  // Captured the same way as the abort signal, because the text-fallback rebuild below runs
67
68
  // outside the fetchResponse frame and used to construct a context without either (#4546).
68
69
  let requestSendBudget: RequestExecutionBudget | undefined;
@@ -236,6 +237,8 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
236
237
  const fallbackOrdinalBase = physicalSendsObserved;
237
238
  const response = await fetchKiroWithRetry(retry.request, {
238
239
  abortSignal: requestAbortSignal,
240
+ ...(requestExecutor ? { executor: requestExecutor } : {}),
241
+ allowGatewayRotation: !_sawReasoning && !assistantText.trim(),
239
242
  returnRawErrors: true,
240
243
  stream: true,
241
244
  // The text-fallback rebuild used to construct a fresh context and drop the budget,
@@ -318,6 +321,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
318
321
  // Keep it for the adapter-owned bounded continuation so cancelling the client turn aborts
319
322
  // both the first Kiro request and its one allowed completion retry.
320
323
  if (ctx?.abortSignal) requestAbortSignal = ctx.abortSignal;
324
+ requestExecutor = ctx?.executor;
321
325
  if (ctx?.sendBudget) requestSendBudget = ctx.sendBudget;
322
326
  if (ctx?.onPhysicalSend) requestOnPhysicalSend = ctx.onPhysicalSend;
323
327
  // Reset per fetch call, because `ordinal` is defined within one call and the caller records
@@ -327,8 +331,8 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
327
331
  // Routed through the same forwarder as the fallback so both legs share one ordinal
328
332
  // sequence; a context without an observer is passed through untouched.
329
333
  return fetchKiroWithRetry(request, requestOnPhysicalSend
330
- ? { ...ctx, onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, 0) }
331
- : ctx);
334
+ ? { ...ctx, returnRawErrors: true, onPhysicalSend: (send: KiroPhysicalSend) => forwardPhysicalSend(send, 0) }
335
+ : { ...ctx, returnRawErrors: true });
332
336
  },
333
337
 
334
338
  formatErrorBody(status: number, headers: Headers, payloadText: string): string {
@@ -12,6 +12,7 @@ import {
12
12
  classifyKiroHttpError,
13
13
  classifyKiroStreamError,
14
14
  safeKiroErrorMessage,
15
+ safeKiroHttpErrorMessage,
15
16
  type KiroErrorClassification,
16
17
  } from "../kiro-errors";
17
18
  import { parseKiroEvent } from "../kiro-events";
@@ -175,6 +176,7 @@ function mergeKiroUsage(
175
176
  ...(sumOptional("cachedInputTokens") !== undefined ? { cachedInputTokens: sumOptional("cachedInputTokens") } : {}),
176
177
  ...(sumOptional("cacheReadInputTokens") !== undefined ? { cacheReadInputTokens: sumOptional("cacheReadInputTokens") } : {}),
177
178
  ...(sumOptional("cacheCreationInputTokens") !== undefined ? { cacheCreationInputTokens: sumOptional("cacheCreationInputTokens") } : {}),
179
+ ...(sumOptional("providerCredits") !== undefined ? { providerCredits: sumOptional("providerCredits") } : {}),
178
180
  ...(sumOptional("reasoningOutputTokens") !== undefined ? { reasoningOutputTokens: sumOptional("reasoningOutputTokens") } : {}),
179
181
  ...(first.estimated || second.estimated ? { estimated: true } : {}),
180
182
  };
@@ -319,6 +321,7 @@ async function* parseKiroAttemptEvents(
319
321
  let completionAnswer: string | undefined;
320
322
  let completionCalls = 0;
321
323
  let authoritativeUsage: OcxUsage | undefined;
324
+ let providerCredits: number | undefined;
322
325
  let stopReason: string | undefined;
323
326
  const fallbackEvents: AdapterEvent[] = [];
324
327
  const thinking = new InlineThinkTagParser(budget);
@@ -379,7 +382,11 @@ async function* parseKiroAttemptEvents(
379
382
  contextUsageTotalFloor() ?? 0,
380
383
  authoritativeTurnTotal,
381
384
  );
382
- return contextTotal > 0 ? { ...base, contextTotalTokens: contextTotal } : base;
385
+ return {
386
+ ...base,
387
+ ...(contextTotal > 0 ? { contextTotalTokens: contextTotal } : {}),
388
+ ...(providerCredits !== undefined ? { providerCredits } : {}),
389
+ };
383
390
  };
384
391
 
385
392
  const classifiedTerminal = (failure: KiroErrorClassification): AdapterEvent => {
@@ -599,6 +606,9 @@ async function* parseKiroAttemptEvents(
599
606
  const ev = parseKiroEvent(eventType, msg.payload);
600
607
  if (!ev) continue;
601
608
  switch (ev.type) {
609
+ case "metering":
610
+ if (ev.unit === "credit" || ev.unit === "credits") providerCredits = ev.usage;
611
+ break;
602
612
  case "metadata":
603
613
  if (ev.usage) authoritativeUsage = ev.usage;
604
614
  if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
@@ -1098,7 +1108,7 @@ export async function* parseKiroStream(
1098
1108
  const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
1099
1109
  yield {
1100
1110
  type: "error",
1101
- message: failure.message,
1111
+ message: safeKiroHttpErrorMessage(fallback.response.status, fallback.response.headers, payload),
1102
1112
  status: failure.status,
1103
1113
  errorType: failure.errorType,
1104
1114
  code: failure.code,
@@ -1,5 +1,6 @@
1
1
  import { estimateTokens, estimateTokensFromCharacterCounts } from "../../lib/token-estimate";
2
2
  import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../../providers/kiro-models";
3
+ import { kiroObservedContextWindow } from "../../providers/kiro-model-catalog";
3
4
  import { modelRecordValue } from "../../reasoning-effort";
4
5
  import { sniffImageDimensions } from "../anthropic-image-guard";
5
6
  import type { KiroImage } from "../kiro-images";
@@ -219,7 +220,8 @@ export function kiroUpstreamContextWindow(modelId: string | undefined): number |
219
220
  if (!modelId) return undefined;
220
221
  const normalizedModelId = normalizeKiroModelId(modelId);
221
222
  if (normalizedModelId === "auto") return undefined;
222
- const window = modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
223
+ const window = kiroObservedContextWindow(modelId)
224
+ ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
223
225
  ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalizedModelId);
224
226
  return typeof window === "number" && Number.isFinite(window) && window > 0 ? window : undefined;
225
227
  }
@@ -1,5 +1,7 @@
1
1
  import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
2
+ import { debugProviderDiagnostic } from "../lib/debug";
2
3
  const DETAIL_KEYS = ["__type", "code", "error", "name", "reason", "message", "Message", "errorMessage"];
4
+ const KIRO_5XX_DIAGNOSTIC_STATUSES = new Set([500, 502, 503, 504]);
3
5
 
4
6
  export interface KiroErrorClassification {
5
7
  message: string;
@@ -204,5 +206,13 @@ export function classifyKiroEventError(reason: string | undefined, message: stri
204
206
  }
205
207
 
206
208
  export function safeKiroHttpErrorMessage(status: number, headers: Headers | Record<string, unknown>, payloadText: string): string {
207
- return classifyKiroFailure(headers, payloadText, status).message;
209
+ const failure = classifyKiroFailure(headers, payloadText, status);
210
+ if (status >= 500) {
211
+ debugProviderDiagnostic("kiro", "http_error", {
212
+ status: KIRO_5XX_DIAGNOSTIC_STATUSES.has(status) ? status : "other_5xx",
213
+ code: failure.code === "server_is_overloaded" ? "server_is_overloaded" : "upstream_server_error",
214
+ });
215
+ return status === 504 ? "Kiro upstream gateway timeout" : "Kiro upstream service unavailable";
216
+ }
217
+ return failure.message;
208
218
  }
@@ -1,10 +1,12 @@
1
1
  import type { OcxUsage } from "../types";
2
+ import { debugProviderDiagnostic } from "../lib/debug";
2
3
  import { kiroTruncationReason } from "./kiro-truncation";
3
4
 
4
5
  export type ParsedKiroEvent =
5
6
  | { type: "content"; data?: string; modelId?: string }
6
7
  | { type: "reasoning"; data?: string; signature?: string; redactedContent?: string }
7
8
  | { type: "context_usage"; contextUsagePercentage: number }
9
+ | { type: "metering"; unit: string; usage: number; unitPlural?: string }
8
10
  | { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean }
9
11
  | { type: "truncation"; data: string }
10
12
  | { type: "metadata"; usage?: OcxUsage; contextUsagePercentage?: number; stopReason?: string }
@@ -17,10 +19,12 @@ const KNOWN_EVENT_TYPES = new Set([
17
19
  "reasoningContentEvent",
18
20
  "toolUseEvent",
19
21
  "messageMetadataEvent",
22
+ "initial-response",
20
23
  "metadataEvent",
21
24
  // Authoritative context pressure. Every capture (kiro-cli 2.14.1 and 2.16.0) put the percentage
22
25
  // HERE and left `metadataEvent` carrying only `stopReason`; metadataEvent's own
23
26
  // contextUsagePercentage stays supported as a fallback rather than being dropped.
27
+ "meteringEvent",
24
28
  "contextUsageEvent",
25
29
  "invalidStateEvent",
26
30
  "error",
@@ -111,7 +115,11 @@ function parseTokenUsage(eventType: string, value: unknown): OcxUsage | undefine
111
115
  /** Decode a known Kiro event using its Smithy `:event-type` header. */
112
116
  export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKiroEvent | null {
113
117
  // Unknown event types are intentionally ignored without parsing or logging their payload.
114
- if (!KNOWN_EVENT_TYPES.has(eventType)) return null;
118
+ if (!KNOWN_EVENT_TYPES.has(eventType)) {
119
+ // The Smithy header is upstream-controlled too; a raw value can contain private data.
120
+ debugProviderDiagnostic("kiro", "unknown_event", { eventTypeLength: eventType.length });
121
+ return null;
122
+ }
115
123
  const parsed = parseObject(eventType, payload);
116
124
  // A metadataEvent's `stopReason` is Kiro's own terminal verdict and must reach the parser
117
125
  // intact. The generic truncation sniffer matches substrings ("max_tokens", "length",
@@ -174,7 +182,25 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi
174
182
  ? { stop: optionalBoolean(eventType, parsed, "stop") }
175
183
  : {}),
176
184
  };
185
+ case "meteringEvent": {
186
+ const unit = optionalString(eventType, parsed, "unit");
187
+ if (unit === undefined) {
188
+ return malformed(eventType, "unit must be a string");
189
+ }
190
+ const unitPlural = optionalString(eventType, parsed, "unitPlural");
191
+ const rawUsage = parsed.usage !== undefined ? parsed.usage : parsed.amount;
192
+ if (typeof rawUsage !== "number" || !Number.isFinite(rawUsage) || rawUsage < 0) {
193
+ return malformed(eventType, "usage must be a finite non-negative number");
194
+ }
195
+ return {
196
+ type: "metering",
197
+ unit,
198
+ usage: rawUsage,
199
+ ...(unitPlural !== undefined ? { unitPlural } : {}),
200
+ };
201
+ }
177
202
  case "messageMetadataEvent":
203
+ case "initial-response":
178
204
  return {
179
205
  type: "message_metadata",
180
206
  conversationId:
@@ -0,0 +1,30 @@
1
+ /** Account-scoped Kiro refusals. Unknown data never convicts an account. */
2
+ import { BOUNDED_BODY_MAX_BYTES } from "../lib/bounded-body";
3
+ export type KiroRefusalKind = "rate" | "monthly_quota" | "suspended" | "other";
4
+ export interface KiroRefusal { kind: KiroRefusalKind; resetAt?: number }
5
+
6
+ const SUSPENSION_WORDS = [
7
+ "temporarily suspended", "temporarily is suspended",
8
+ "locked your account", "locked it as a",
9
+ ] as const;
10
+
11
+ export function classifyKiroRefusal(status: number, bodyText: string): KiroRefusal {
12
+ if (Buffer.byteLength(bodyText, "utf8") > BOUNDED_BODY_MAX_BYTES)
13
+ return { kind: status === 429 ? "rate" : "other" };
14
+ let reason: unknown;
15
+ let message: unknown;
16
+ try {
17
+ const row: unknown = JSON.parse(bodyText);
18
+ if (row && typeof row === "object" && !Array.isArray(row)) {
19
+ reason = (row as Record<string, unknown>).reason;
20
+ message = (row as Record<string, unknown>).message;
21
+ }
22
+ } catch { /* Unrecognised bodies cannot establish an account verdict. */ }
23
+ if ((status === 400 || status === 429) && reason === "MONTHLY_REQUEST_COUNT")
24
+ return { kind: "monthly_quota" };
25
+ if (status === 403 && (reason === "TEMPORARILY_SUSPENDED"
26
+ || (typeof message === "string" && SUSPENSION_WORDS.some(word => message.toLowerCase().includes(word)))))
27
+ return { kind: "suspended" };
28
+ if (status === 429) return { kind: "rate" };
29
+ return { kind: "other" };
30
+ }