mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -7,6 +7,9 @@ export const EFFORT_CONFIGURATION_BETA = 'mid-conversation-output-config-2026-07
7
7
  // first version checked on the wire (update accepted, prompt cache kept), and
8
8
  // every later version of that family. A model that turns out to reject the
9
9
  // update is switched off at runtime (markEffortConfigurationUnsupported).
10
+ // The Anthropic catalogs publish no per-model flag for it, so a measured
11
+ // floor is kept for the families already checked: below it the rejection is
12
+ // certain, and every request would pay a failed attempt first.
10
13
  const ANTHROPIC_MIN_VERSION = Object.freeze({
11
14
  opus: [5, 0],
12
15
  sonnet: [5, 5],
@@ -14,6 +17,10 @@ const ANTHROPIC_MIN_VERSION = Object.freeze({
14
17
  fable: [5, 1],
15
18
  mythos: [5, 1],
16
19
  });
20
+ // A family not measured yet starts at the earliest checked major, on the
21
+ // routes that replay a rejected update (the rest cannot recover from one).
22
+ const ANTHROPIC_FIRST_MAJOR = Math.min(...Object.values(ANTHROPIC_MIN_VERSION).map(([major]) => major));
23
+ const ANTHROPIC_REPLAYING_PROVIDERS = new Set(['anthropic-oauth']);
17
24
  // https://developers.openai.com/api/docs/guides/reasoning#change-reasoning-mid-conversation
18
25
  // — GPT-6 and later, standard single-agent mode. The public API publishes no
19
26
  // per-model flag, so the version is its whole answer; the OAuth backend's
@@ -25,14 +32,19 @@ function versionAtLeast([major, minor], [minMajor, minMinor]) {
25
32
  return major > minMajor || (major === minMajor && minor >= minMinor);
26
33
  }
27
34
 
28
- function anthropicVersionSupported(id) {
35
+ function anthropicVersionSupported(provider, id) {
29
36
  const m = id.match(/^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?$/);
30
- const floor = m && ANTHROPIC_MIN_VERSION[m[1]];
31
- return Boolean(floor) && versionAtLeast([Number(m[2]), Number(m[3] || 0)], floor);
37
+ if (!m) return false;
38
+ const version = [Number(m[2]), Number(m[3] || 0)];
39
+ const floor = ANTHROPIC_MIN_VERSION[m[1]];
40
+ if (floor) return versionAtLeast(version, floor);
41
+ return ANTHROPIC_REPLAYING_PROVIDERS.has(provider) && version[0] >= ANTHROPIC_FIRST_MAJOR;
32
42
  }
33
43
 
44
+ // Any `gpt-<major>` id, whatever follows the major (tier names, minors,
45
+ // snapshots), so a later naming scheme needs no edit.
34
46
  function openAiVersionSupported(id) {
35
- const m = id.match(/^gpt-(\d+)(?:-\d+)?(?:-[a-z]+)?$/);
47
+ const m = id.match(/^gpt-(\d+)(?:-|$)/);
36
48
  return Boolean(m) && Number(m[1]) >= OPENAI_MIN_MAJOR;
37
49
  }
38
50
 
@@ -110,11 +122,25 @@ function openAiSupportsEffortUpdates(provider, model, id) {
110
122
  return declared ?? openAiVersionSupported(id);
111
123
  }
112
124
 
125
+ // A custom route on the Responses wire (`opts.protocol`) serving GPT-6 or
126
+ // later takes the same update as the official routes. The version is read
127
+ // from the id's last `/` segment, so gateway ids like `openai/gpt-6.1-sol` count.
128
+ function customResponsesSupportsEffortUpdates(provider, opts, id) {
129
+ return (
130
+ isCustomProviderName(provider) &&
131
+ opts.protocol === 'openai-responses' &&
132
+ openAiVersionSupported(id.slice(id.lastIndexOf('/') + 1))
133
+ );
134
+ }
135
+
113
136
  export function effortConfigurationMode(provider, model, opts = {}) {
114
137
  const id = modelKey(model);
115
138
  if (opts.effortConfigurationEnabled === false || Number(opts.thinkingBudgetTokens) > 0) return null;
116
139
  if (unsupportedModels.has(`${provider}:${id}`)) return null;
117
- if (OPENAI_PROVIDERS.has(provider) && openAiSupportsEffortUpdates(provider, model, id)) {
140
+ if (
141
+ (OPENAI_PROVIDERS.has(provider) && openAiSupportsEffortUpdates(provider, model, id)) ||
142
+ customResponsesSupportsEffortUpdates(provider, opts, id)
143
+ ) {
118
144
  const parameters = opts.modelParameters || {};
119
145
  const mode = opts.reasoning?.mode ?? parameters.reasoning_mode ?? parameters.mode ?? 'standard';
120
146
  if (mode !== 'standard' || opts.multiAgent === true || parameters.multi_agent === true) return null;
@@ -122,7 +148,7 @@ export function effortConfigurationMode(provider, model, opts = {}) {
122
148
  }
123
149
  if (
124
150
  ANTHROPIC_PROVIDERS.has(provider) &&
125
- anthropicVersionSupported(id) &&
151
+ anthropicVersionSupported(provider, id) &&
126
152
  opts.disableBetaHeaders !== true &&
127
153
  (!opts.baseURL || /^https:\/\/api\.anthropic\.com(?:\/|$)/.test(opts.baseURL))
128
154
  )
@@ -132,7 +158,8 @@ export function effortConfigurationMode(provider, model, opts = {}) {
132
158
 
133
159
  // Custom providers on the Chat and Responses wires take a fresh effort with
134
160
  // every request, so auto effort sends a turn's or step's effort as that
135
- // request's own effort: no mid-conversation update, no beta header. A custom
161
+ // request's own effort: no mid-conversation update, no beta header (except a
162
+ // Responses-wire GPT-6+ model, which takes the 'responses' update). A custom
136
163
  // Anthropic-wire provider keeps auto effort off.
137
164
  const CUSTOM_AUTO_EFFORT_PROTOCOLS = new Set(['openai-chat', 'openai-responses']);
138
165
 
@@ -161,17 +188,24 @@ export function autoEffortModel(providerName, providerImpl, model) {
161
188
  }
162
189
 
163
190
  /** The effort-change mode of a session's route: 'responses' / 'anthropic'
164
- * (mid-conversation update), 'custom' (per-request effort), or null. */
191
+ * (mid-conversation update), 'custom' (per-request effort), or null. A
192
+ * custom route takes an update mode only when its model declares levels. */
165
193
  export function turnEffortConfigurationMode(session, provider) {
166
- const opts = { ...(provider?.config || {}), modelParameters: session.modelParameters || {} };
194
+ const protocol = provider?.customConfig?.protocol;
195
+ const opts = { ...(provider?.config || {}), modelParameters: session.modelParameters || {}, protocol };
196
+ const custom = customAutoEffortCapable(
197
+ session.provider,
198
+ protocol,
199
+ autoEffortModel(session.provider, provider, session.model)
200
+ );
167
201
  const mode = effortConfigurationMode(session.provider, session.model, opts);
168
- if (mode) return mode;
169
- const model = autoEffortModel(session.provider, provider, session.model);
170
- return customAutoEffortCapable(session.provider, provider?.customConfig?.protocol, model) ? 'custom' : null;
202
+ if (mode && (custom || !isCustomProviderName(session.provider))) return mode;
203
+ return custom ? 'custom' : null;
171
204
  }
172
205
 
173
206
  const CUSTOM_EFFORTS = new Set(CUSTOM_EFFORT_LEVELS);
174
- const effortsFor = (mode) => (mode === 'custom' ? CUSTOM_EFFORTS : EFFORTS);
207
+ // A custom route keeps its own levels (including `none`) in either mode.
208
+ const effortsFor = (mode, provider) => (mode === 'custom' || isCustomProviderName(provider) ? CUSTOM_EFFORTS : EFFORTS);
175
209
 
176
210
  function normalizedEffort(provider, model, effort) {
177
211
  if (ANTHROPIC_PROVIDERS.has(provider)) return normalizeAnthropicEffortInput(effort, model) || null;
@@ -187,8 +221,8 @@ function validSnapshot(value, provider, model, mode) {
187
221
  value.provider === provider &&
188
222
  value.model === modelKey(model) &&
189
223
  value.mode === mode &&
190
- effortsFor(mode).has(value.initialEffort) &&
191
- effortsFor(mode).has(value.effort)
224
+ effortsFor(mode, provider).has(value.initialEffort) &&
225
+ effortsFor(mode, provider).has(value.effort)
192
226
  );
193
227
  }
194
228
 
@@ -199,7 +233,7 @@ function validSnapshot(value, provider, model, mode) {
199
233
  export function prepareTurnEffortConfiguration(session, provider, turnEffort) {
200
234
  const mode = turnEffortConfigurationMode(session, provider);
201
235
  const effort = normalizedEffort(session.provider, session.model, turnEffort ?? session.effort);
202
- if (!mode || !effortsFor(mode).has(effort)) return null;
236
+ if (!mode || !effortsFor(mode, session.provider).has(effort)) return null;
203
237
  const first = (session.messages || []).find((message) =>
204
238
  validSnapshot(message?.meta?.[META_KEY], session.provider, session.model, mode)
205
239
  )?.meta?.[META_KEY];
@@ -221,7 +255,7 @@ export function prepareTurnEffortConfiguration(session, provider, turnEffort) {
221
255
  // step), or null when that effort is not valid for the model.
222
256
  export function stepEffortConfiguration(snapshot, provider, model, effort) {
223
257
  const value = normalizedEffort(provider, model, effort);
224
- if (!snapshot || !EFFORTS.has(value)) return null;
258
+ if (!snapshot || !effortsFor(snapshot.mode, provider).has(value)) return null;
225
259
  return { ...snapshot, effort: value };
226
260
  }
227
261
 
@@ -256,6 +290,32 @@ export function projectEffortConfiguration(messages, provider, model, opts = {})
256
290
  return { mode, initialEffort: seed.initialEffort, effort: current, updates };
257
291
  }
258
292
 
293
+ /**
294
+ * Responses-wire placement of a projection's updates while a history is
295
+ * lowered in order: `before(message)` gives the items to emit ahead of a
296
+ * message, `end()` those after the last one. An update on a tool result
297
+ * applies after its whole batch; one on a user message before it. Adjacent
298
+ * updates are rejected, so a batch update that a user message directly
299
+ * follows yields to that message's own.
300
+ */
301
+ export function responsesEffortUpdates(projection) {
302
+ let afterBatch = null;
303
+ const items = (effort) => (effort ? [{ type: 'configuration_update', reasoning: { effort } }] : []);
304
+ return {
305
+ before(message) {
306
+ const effort = projection?.updates.get(message);
307
+ if (message.role === 'tool') {
308
+ if (effort) afterBatch = effort;
309
+ return [];
310
+ }
311
+ const value = effort || afterBatch;
312
+ afterBatch = null;
313
+ return items(value);
314
+ },
315
+ end: () => items(afterBatch),
316
+ };
317
+ }
318
+
259
319
  // Split only before a user message or after a whole tool-result batch.
260
320
  // Sanitizing each segment before inserting the trusted empty system control
261
321
  // avoids losing it as empty text, and never separates a tool call from its
@@ -252,7 +252,7 @@ export function dropRejectedGeminiCache(cachedContent, opts) {
252
252
  // the model-specific minimum. Skip the POST entirely when the estimate
253
253
  // is under threshold so we don't spam 400 responses turn-after-turn.
254
254
  export function geminiPrefixBelowMinimum({ model, systemInstruction, geminiTools, contents, opts, currentIter }) {
255
- const minTokens = _geminiCacheMinTokens(model);
255
+ const minTokens = _geminiCacheMinTokens();
256
256
  const estimatedTokens = _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents);
257
257
  if (estimatedTokens >= minTokens) return false;
258
258
  traceGeminiCache(opts, currentIter, 'gemini_cache_skip', {
@@ -43,12 +43,11 @@ function summarizeTraceContents(contents) {
43
43
  return [...summaries.slice(0, 8), { omittedTurns: summaries.length - 12 }, ...summaries.slice(-4)];
44
44
  }
45
45
 
46
- // Gemini cachedContents API rejects prefixes below the model-specific minimum
47
- // (Gemini 2.0/2.5 = 2048 tokens, Gemini 3.x = 4096 tokens) with HTTP 400
48
- // "Cached content is too small". Estimating chars/4 ≈ tokens lets us skip the
49
- // roundtrip when the prefix cannot satisfy the invariant. Estimate undercount
50
- // is harmless (we attempt and get the same 400 we would have skipped); overcount
51
- // is harmless (we skip, fall back to implicit cache).
46
+ // Gemini cachedContents API rejects prefixes below a model-specific minimum
47
+ // with HTTP 400 "Cached content is too small". Estimating chars/4 ≈ tokens
48
+ // lets us skip the roundtrip when the prefix cannot satisfy the invariant.
49
+ // Estimate undercount is harmless (we attempt and get the same 400 we would
50
+ // have skipped); overcount is harmless (we skip, fall back to implicit cache).
52
51
  export function _estimateGeminiCacheTokens(systemInstruction, geminiTools, contents) {
53
52
  let chars = 0;
54
53
  if (typeof systemInstruction === 'string') chars += systemInstruction.length;
@@ -67,8 +66,15 @@ export function _estimateGeminiCacheTokens(systemInstruction, geminiTools, conte
67
66
  return Math.ceil(chars / 4);
68
67
  }
69
68
 
70
- export function _geminiCacheMinTokens(model) {
71
- return /^gemini-3/i.test(String(model || '')) ? 4096 : 2048;
69
+ // No catalog publishes the cachedContents floor and the published minimums
70
+ // differ per model and change between releases, so one floor serves every id:
71
+ // the highest published minimum. Skipping a small prefix only falls back to the
72
+ // implicit cache, whereas a floor that is too low would POST a create that the
73
+ // server rejects on every turn.
74
+ const GEMINI_CACHE_MIN_TOKENS = 4096;
75
+
76
+ export function _geminiCacheMinTokens() {
77
+ return GEMINI_CACHE_MIN_TOKENS;
72
78
  }
73
79
 
74
80
  export function _geminiCachePrefixCount(contents) {
@@ -110,6 +116,21 @@ const GEMINI_GLOBAL_CACHE_MAX_ENTRIES = 128;
110
116
  export const GEMINI_GLOBAL_CACHE_DELETE_GRACE_MS = 2 * 60 * 1000;
111
117
  const geminiGlobalCaches = new Map();
112
118
  export const geminiGlobalCacheCreates = new Map();
119
+ // Models whose cachedContents.create the server refused as unsupported (not
120
+ // a prefix or quota failure). No catalog publishes explicit-cache support, so
121
+ // the server's own answer decides, once per process.
122
+ const geminiExplicitCacheRefusedModels = new Set();
123
+
124
+ export function markGeminiExplicitCacheRefused(model, err) {
125
+ if (err?.status !== 400) return false;
126
+ if (!/explicit caching is not supported/i.test(String(err?.message || ''))) return false;
127
+ geminiExplicitCacheRefusedModels.add(String(model || ''));
128
+ return true;
129
+ }
130
+
131
+ export function geminiExplicitCacheRefused(model) {
132
+ return geminiExplicitCacheRefusedModels.has(String(model || ''));
133
+ }
113
134
 
114
135
  export function _geminiCredentialFingerprint(apiKey) {
115
136
  return traceHash(String(apiKey || ''));
@@ -0,0 +1,191 @@
1
+ /**
2
+ * gemini-error-contract.mjs — Google RPC error details → the provider error
3
+ * contract shared by Gemini (REST + SDK) and Antigravity:
4
+ * status, code (lowercased), retryAfterMs (uncapped), resetAt (epoch ms),
5
+ * quotaScope ('minute' | 'day' | undefined).
6
+ *
7
+ * Details come from `error.details` (REST body) or `errorDetails` (SDK
8
+ * GoogleGenerativeAIFetchError). Entries are google.rpc.RetryInfo,
9
+ * QuotaFailure and ErrorInfo.
10
+ */
11
+
12
+ const DURATION_RE = /^(?:\d+(?:\.\d+)?(?:ms|s|m|h))+$/;
13
+ const DURATION_PART_RE = /(\d+(?:\.\d+)?)(ms|s|m|h)/g;
14
+ const DURATION_UNIT_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
15
+ const BARE_SECONDS_RE = /^\d+(?:\.\d+)?$/;
16
+
17
+ // Google error classification: a per-minute quota with no RetryInfo is
18
+ // retried after one quota window.
19
+ export const GEMINI_PER_MINUTE_RETRY_MS = 60_000;
20
+
21
+ // "Please retry in 34.07s" (Gemini API) / "reset after 2h1m1s" (Cloud Code).
22
+ const MESSAGE_DELAY_RE = /(?:please retry in|reset after)\s+((?:\d+(?:\.\d+)?(?:ms|h|m|s))+)/i;
23
+ const LIMIT_ZERO_RE = /limit:\s*0(?!\d|\.\d)/i;
24
+ const CAPACITY_MESSAGE_RE = /exhausted your capacity|capacity exceeded|model_capacity_exhausted/i;
25
+ // ErrorInfo reasons that name a spent quota with no window of its own.
26
+ const TERMINAL_REASONS = new Set(['quota_exhausted', 'insufficient_g1_credits_balance']);
27
+ const CAPACITY_REASONS = new Set(['model_capacity_exhausted', 'model_capacity_exceeded']);
28
+
29
+ const GOOGLE_STATUS_HTTP = {
30
+ INVALID_ARGUMENT: 400,
31
+ UNAUTHENTICATED: 401,
32
+ PERMISSION_DENIED: 403,
33
+ NOT_FOUND: 404,
34
+ RESOURCE_EXHAUSTED: 429,
35
+ INTERNAL: 500,
36
+ UNAVAILABLE: 503,
37
+ DEADLINE_EXCEEDED: 504,
38
+ };
39
+
40
+ function detailType(entry) {
41
+ return typeof entry?.['@type'] === 'string' ? entry['@type'] : '';
42
+ }
43
+
44
+ function durationMs(value) {
45
+ if (typeof value === 'string') {
46
+ const text = value.trim();
47
+ if (BARE_SECONDS_RE.test(text)) return Math.ceil(Number(text) * 1000);
48
+ if (!DURATION_RE.test(text)) return null;
49
+ let total = 0;
50
+ for (const [, amount, unit] of text.matchAll(DURATION_PART_RE)) total += Number(amount) * DURATION_UNIT_MS[unit];
51
+ return Math.ceil(total);
52
+ }
53
+ if (value && typeof value === 'object') {
54
+ const ms = Number(value.seconds || 0) * 1000 + Number(value.nanos || 0) / 1_000_000;
55
+ return Number.isFinite(ms) && ms >= 0 ? Math.ceil(ms) : null;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // A zero wait is no advice: the caller's normal backoff (or the per-minute
61
+ // window) applies instead of an immediate re-issue.
62
+ function adviceMs(value) {
63
+ const ms = durationMs(value);
64
+ return ms > 0 ? ms : undefined;
65
+ }
66
+
67
+ function scopeOf(text) {
68
+ if (/per[\s_-]?day|daily/i.test(text)) return 'day';
69
+ if (/per[\s_-]?minute/i.test(text)) return 'minute';
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * `quotaScope` is 'minute' / 'day' from the typed quota ids only (never from
75
+ * the length of the server's wait); 'plan' marks a spent
76
+ * quota with no window (ErrorInfo QUOTA_EXHAUSTED, or a QuotaFailure
77
+ * violation with a zero limit; message prose alone never counts). Capacity
78
+ * (MODEL_CAPACITY_EXHAUSTED / "exhausted your capacity") is an overload of the
79
+ * shared pool: code `model_capacity_exhausted`, never a quota scope.
80
+ * @param {{ details?: unknown, errorDetails?: unknown, googleStatus?: string, message?: string }} input
81
+ * @returns {{ code?: string, retryAfterMs?: number, resetAt?: number, quotaScope?: 'minute'|'day'|'plan' }}
82
+ */
83
+ export function geminiErrorContract({ details, errorDetails, googleStatus, message } = {}) {
84
+ const list = [details, errorDetails].flatMap((entry) => (Array.isArray(entry) ? entry : []));
85
+ let reason = '';
86
+ let retryAfterMs;
87
+ let resetAt;
88
+ const scopes = new Set();
89
+ let zeroLimit = false;
90
+ for (const entry of list) {
91
+ const type = detailType(entry);
92
+ if (type.endsWith('.RetryInfo') || entry?.retryDelay !== undefined) {
93
+ retryAfterMs ??= adviceMs(entry.retryDelay);
94
+ }
95
+ if (type.endsWith('.QuotaFailure') || Array.isArray(entry?.violations)) {
96
+ for (const violation of entry.violations || []) {
97
+ const scope = scopeOf(String(violation?.quotaId || ''));
98
+ if (scope) scopes.add(scope);
99
+ // A spent allowance is named by the typed violation, not by prose.
100
+ if (String(violation?.quotaValue ?? '').trim() === '0' || LIMIT_ZERO_RE.test(String(violation?.description || ''))) {
101
+ zeroLimit = true;
102
+ }
103
+ }
104
+ }
105
+ if (type.endsWith('.ErrorInfo') || typeof entry?.reason === 'string') {
106
+ if (!reason && typeof entry.reason === 'string') reason = entry.reason.trim().toLowerCase();
107
+ const metadata = entry.metadata && typeof entry.metadata === 'object' ? entry.metadata : {};
108
+ retryAfterMs ??= adviceMs(metadata.quotaResetDelay);
109
+ const stamp = Date.parse(String(metadata.quotaResetTimeStamp || ''));
110
+ if (resetAt === undefined && Number.isFinite(stamp)) resetAt = stamp;
111
+ const scope = scopeOf(
112
+ [metadata.quotaId, metadata.quota_limit, metadata.quota_metric, entry.reason].filter(Boolean).join(' ')
113
+ );
114
+ if (scope) scopes.add(scope);
115
+ }
116
+ }
117
+ const text = typeof message === 'string' ? message : '';
118
+ // The message names capacity only when nothing typed said otherwise (a
119
+ // reason, a quota id or RetryInfo outranks it, as in classifyGoogleError).
120
+ const capacity =
121
+ CAPACITY_REASONS.has(reason) ||
122
+ (!reason && scopes.size === 0 && retryAfterMs === undefined && CAPACITY_MESSAGE_RE.test(text));
123
+ // A per-day violation outlasts any per-minute one reported with it.
124
+ let quotaScope = scopes.has('day') ? 'day' : scopes.has('minute') ? 'minute' : undefined;
125
+ if (retryAfterMs === undefined) {
126
+ const named = MESSAGE_DELAY_RE.exec(text);
127
+ if (named) retryAfterMs = adviceMs(named[1]);
128
+ }
129
+ // Per-minute quota with no RetryInfo: wait one quota window.
130
+ if (retryAfterMs === undefined && quotaScope === 'minute' && !capacity) retryAfterMs = GEMINI_PER_MINUTE_RETRY_MS;
131
+ if (resetAt === undefined && retryAfterMs !== undefined) resetAt = Date.now() + retryAfterMs;
132
+ if (!capacity && !quotaScope && (TERMINAL_REASONS.has(reason) || zeroLimit)) quotaScope = 'plan';
133
+ const status = typeof googleStatus === 'string' ? googleStatus.trim().toLowerCase() : '';
134
+ const code = capacity ? 'model_capacity_exhausted' : reason || status;
135
+ return {
136
+ ...(code ? { code } : {}),
137
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
138
+ ...(resetAt !== undefined ? { resetAt } : {}),
139
+ ...(quotaScope ? { quotaScope } : {}),
140
+ };
141
+ }
142
+
143
+ /** Copy the contract fields onto `err`; an already-typed `code` is kept. */
144
+ export function applyGeminiErrorContract(err, input) {
145
+ const { code, ...rest } = geminiErrorContract(input);
146
+ Object.assign(err, rest);
147
+ if (code && !(typeof err.code === 'string' && err.code)) err.code = code;
148
+ return err;
149
+ }
150
+
151
+ /**
152
+ * The `{ error: {...} }` envelope a Gemini/Cloud Code stream carries in-band
153
+ * instead of an HTTP error status. Returns the typed error, or null when the
154
+ * chunk is an ordinary payload.
155
+ */
156
+ export function geminiInBandError(chunk, label) {
157
+ const detail = chunk && typeof chunk === 'object' ? chunk.error : null;
158
+ if (!detail || (typeof detail !== 'object' && typeof detail !== 'string')) return null;
159
+ const body = typeof detail === 'object' ? detail : { message: detail };
160
+ const googleStatus = typeof body.status === 'string' ? body.status : '';
161
+ const status = Number.isFinite(Number(body.code)) && Number(body.code) > 0
162
+ ? Number(body.code)
163
+ : GOOGLE_STATUS_HTTP[googleStatus.toUpperCase()];
164
+ const err = new Error(`${label} in-band error${status ? ` ${status}` : ''}: ${String(body.message || googleStatus || 'unknown').slice(0, 300)}`);
165
+ err.name = 'GeminiInBandError';
166
+ if (status) {
167
+ err.status = status;
168
+ err.httpStatus = status;
169
+ }
170
+ if (googleStatus) err.geminiStatus = googleStatus.toUpperCase();
171
+ err.error = body;
172
+ const details = body.details ?? body.errorDetails;
173
+ if (Array.isArray(details)) err.details = details;
174
+ return applyGeminiErrorContract(err, { details, googleStatus, message: body.message });
175
+ }
176
+
177
+ /** Throw the typed in-band error when `chunk` is an error envelope. */
178
+ export function assertNoGeminiInBandError(chunk, label) {
179
+ const err = geminiInBandError(chunk, label);
180
+ if (err) throw err;
181
+ }
182
+
183
+ const SAFETY_FINISH = /^(?:PROMPT_)?(?:SAFETY|IMAGE_SAFETY|BLOCKLIST|PROHIBITED_CONTENT|SPII|IMAGE_PROHIBITED_CONTENT)$/;
184
+
185
+ /** Policy-refusal code for a finishReason, or null for any other reason. */
186
+ export function geminiPolicyFinishCode(finishReason) {
187
+ const reason = String(finishReason || '').replace(/^FINISH_REASON_/, '');
188
+ if (SAFETY_FINISH.test(reason)) return 'safety';
189
+ if (/^(?:PROMPT_)?RECITATION$/.test(reason)) return 'recitation';
190
+ return null;
191
+ }
@@ -0,0 +1,9 @@
1
+ // True for an id in the Gemini wire family ("gemini-4-argon", "models/gemini-3.1-pro",
2
+ // "gemini-pro-agent"). It names the family only, never a generation: it separates
3
+ // Gemini from the other model families an Antigravity gateway serves (Claude,
4
+ // GPT-OSS), which reject Gemini-only request fields outright.
5
+ const GEMINI_FAMILY = /(?:^|\/)gemini-/i;
6
+
7
+ export function isGeminiFamilyModel(model) {
8
+ return GEMINI_FAMILY.test(String(model || ''));
9
+ }
@@ -1,10 +1,15 @@
1
+ import { isGeminiFamilyModel } from './gemini-family.mjs';
2
+
1
3
  const SYNTHETIC_TOOL_SIGNATURE = 'skip_thought_signature_validator';
2
4
 
3
- // Gemini 3 validates the first functionCall in every step of the current turn.
4
- // Foreign/interrupted histories may not have a native signature for that call.
5
- // Preserve real signatures and unsigned siblings of a signed parallel batch.
5
+ // Gemini models that validate thought signatures check the first functionCall
6
+ // in every step of the current turn. Foreign/interrupted histories may not have
7
+ // a native signature for that call. Preserve real signatures and unsigned
8
+ // siblings of a signed parallel batch. The validator-bypass token is a Gemini
9
+ // wire token: it is only fabricated for the Gemini family, because the other
10
+ // families an Antigravity gateway serves reject it.
6
11
  export function ensureGeminiToolCallSignatures(contents, model) {
7
- if (!/^(?:models\/)?gemini-3(?:[.-]|$)/i.test(String(model || ''))) return contents;
12
+ if (!isGeminiFamilyModel(model)) return contents;
8
13
  let activeTurnStart = 0;
9
14
  for (let index = contents.length - 1; index >= 0; index -= 1) {
10
15
  const content = contents[index];
@@ -14,6 +14,7 @@ import { createProviderReplay } from './lib/provider-replay.mjs';
14
14
  import { collectGeminiGroundingSources, parseGeminiTextPartMetadata, parseToolCalls } from './gemini-schema.mjs';
15
15
  import { _resolveGeminiCacheUsage } from './gemini-cache.mjs';
16
16
  import { traceGeminiCache } from './gemini-cache-policy.mjs';
17
+ import { geminiPolicyFinishCode } from './gemini-error-contract.mjs';
17
18
 
18
19
  // Candidate text and tool calls after the text-leak guard: leaked
19
20
  // (text-embedded) tool calls are appended to the native ones and empty the
@@ -64,7 +65,8 @@ export function geminiIncompleteError(response, parsed, useModel, usage = null)
64
65
  if (!finishReason || normalizedFinishReason === 'STOP') return null;
65
66
  return Object.assign(new Error(`Gemini response incomplete: finishReason=${finishReason}`), {
66
67
  name: 'ProviderIncompleteError',
67
- code: 'PROVIDER_INCOMPLETE',
68
+ // Safety/recitation are policy refusals the core presents as such.
69
+ code: geminiPolicyFinishCode(finishReason) || 'PROVIDER_INCOMPLETE',
68
70
  providerIncomplete: true,
69
71
  finishReason,
70
72
  partialContent: parsed.content,