mixdog 1.3.3 → 1.3.5

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 (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -9,11 +9,14 @@ import {
9
9
  providerAccountExhaustedForModelOnly,
10
10
  providerAccountPolicy,
11
11
  providerAccountsResetAt,
12
+ providerAccountWithBlocks,
13
+ providerAccountBlockedUntil,
14
+ staleQuotaWindows,
12
15
  } from '../../../shared/provider-accounts.mjs';
13
16
  import { withProviderAccount, hasExplicitProviderAuthBinding } from '../../../shared/provider-auth-binding.mjs';
14
17
  import { isExplicitUserAbortError, markProviderRecoveryExhausted } from './retry-classifier.mjs';
15
18
  import { sleepWithAbort } from './retry-backoff.mjs';
16
- import { QUOTA_KINDS, providerErrorKind } from './provider-error-kind.mjs';
19
+ import { QUOTA_KINDS, boundedCauseChain, providerErrorKind } from './provider-error-kind.mjs';
17
20
  import { fetchOAuthUsageSnapshot, readCachedOAuthUsageSnapshot } from './oauth-usage.mjs';
18
21
 
19
22
  // Only an explicit quota / plan / billing refusal advances the roster as
@@ -124,15 +127,18 @@ function accountsExhaustedError(useCredits) {
124
127
  // evidence): a rate limit, advising the earliest cooldown end as the wait.
125
128
  // The pool already spent its cooldown wait, so this is a terminal verdict for
126
129
  // the loop, not an unclassified failure.
127
- function earliestCooldownMs(rows, now) {
128
- const ends = rows.map((row) => Number(row.blockedUntil)).filter((until) => Number.isFinite(until) && until > now);
130
+ // A row's cooldown for `model` ends when every block holding that model lifts.
131
+ const coolingUntil = (row, model) => providerAccountBlockedUntil(row, model) ?? 0;
132
+
133
+ function earliestCooldownMs(rows, now, model) {
134
+ const ends = rows.map((row) => coolingUntil(row, model)).filter((until) => until > now);
129
135
  return ends.length ? Math.min(...ends) - now : null;
130
136
  }
131
137
 
132
- function accountsCoolingDownError(rows, now) {
138
+ function accountsCoolingDownError(rows, now, model) {
133
139
  const error = new Error('Every connected account is cooling down after a rate limit. Try again shortly.');
134
140
  error.code = 'rate_limited';
135
- const wait = earliestCooldownMs(rows, now);
141
+ const wait = earliestCooldownMs(rows, now, model);
136
142
  if (wait != null) error.retryAfterMs = wait;
137
143
  return markProviderRecoveryExhausted(error, { owner: 'account-pool', terminal: true });
138
144
  }
@@ -258,6 +264,36 @@ function recordServingAccount(providerName, result, id, bindingId) {
258
264
  };
259
265
  }
260
266
 
267
+ // A block covers the quota bucket of the model that failed, which only a usage
268
+ // reading names. An account without a recorded reading is measured once before
269
+ // it is blocked — once per account for this process, whatever the outcome,
270
+ // through the ordinary cache (a fresh failed reading is not repeated) — and
271
+ // again only after a reading has recorded windows and they were dropped.
272
+ // Without a reading the block stays account-wide.
273
+ const measureAttempted = new Set();
274
+
275
+ async function measureOnce(providerName, row, account) {
276
+ const key = `${providerName}:${row.id}`;
277
+ if (row.usage?.windows?.length) {
278
+ measureAttempted.delete(key);
279
+ return;
280
+ }
281
+ if (measureAttempted.has(key)) return;
282
+ measureAttempted.add(key);
283
+ await fetchOAuthUsageSnapshot({ provider: providerName, accountId: row.id }, account(row.id), () => {}).catch(
284
+ () => null
285
+ );
286
+ }
287
+
288
+ // The server's id of the limit a refusal names (a Codex model-specific limit),
289
+ // stamped on the error by the provider boundary.
290
+ function quotaLimitOf(error) {
291
+ for (const item of boundedCauseChain(error)) {
292
+ if (typeof item?.quotaLimit === 'string' && item.quotaLimit) return item.quotaLimit;
293
+ }
294
+ return '';
295
+ }
296
+
261
297
  // Whether the failed account's quota window is exhausted, plus the server's
262
298
  // retry delay. Anthropic and other subscriptions may use the same 429 code
263
299
  // for burst throttling and exhausted subscription windows, so the native
@@ -272,7 +308,7 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
272
308
  const policy = cooldownPolicyOf(account(row.id));
273
309
  const floorMs = policy ? finiteMs(policy.floorMs, 0) : 0;
274
310
  const blockUntil = (withReset, reason = 'unknown') => {
275
- if (withReset && resetAt > now) return { until: resetAt, source: 'retry-after' };
311
+ if (withReset && resetAt > now) return { until: resetAt, source: 'retry-after', reset: true };
276
312
  if (delay > 0) return { until: now + Math.max(delay, floorMs), source: 'retry-after' };
277
313
  if (policy) {
278
314
  const ms = policyCooldownMs(policy, `${providerName}:${row.id}`, reason);
@@ -298,10 +334,14 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
298
334
  };
299
335
  }
300
336
  const typed = isAccountQuotaError(error);
301
- if (typed) return { exhausted: true, delay, evidence: true, ...blockUntil(true, 'quota') };
337
+ if (typed) {
338
+ await measureOnce(providerName, row, account);
339
+ return { exhausted: true, delay, evidence: true, ...blockUntil(true, 'quota') };
340
+ }
302
341
  // A policy with rotateOnRateLimit rotates on a plain rate limit too (the
303
342
  // advice as its cooldown, else rateLimitedMs); never quota evidence.
304
343
  if (policy?.rotateOnRateLimit === true && kind === 'rate_limited' && status === 429) {
344
+ await measureOnce(providerName, row, account);
305
345
  return { exhausted: true, delay, evidence: false, ...blockUntil(false, 'rate_limited') };
306
346
  }
307
347
  let exhausted = status === 429 && delay > QUOTA_WINDOW_RETRY_AFTER_MS;
@@ -315,7 +355,7 @@ async function accountQuotaExhausted(providerName, row, model, error, account) {
315
355
  const pool = readProviderAccountPool(providerName);
316
356
  const entry = pool.accounts.find((item) => item.id === row.id);
317
357
  const options = { useCredits: pool.useCredits === true };
318
- evidence = providerAccountExhausted({ ...entry, blockedUntil: 0 }, Date.now(), model, options);
358
+ evidence = providerAccountExhausted(providerAccountWithBlocks(entry), Date.now(), model, options);
319
359
  exhausted = exhausted || evidence || providerAccountExhausted(entry, Date.now(), model, options);
320
360
  }
321
361
  return { exhausted, delay, evidence, ...(exhausted ? blockUntil(evidence, evidence ? 'quota' : 'unknown') : {}) };
@@ -350,7 +390,7 @@ function poolSend(providerName, account) {
350
390
  rows.length > 0 &&
351
391
  rows.every((entry) =>
352
392
  providerAccountExhausted(
353
- evidenced.has(entry.id) || entry.blockedQuota === true ? entry : { ...entry, blockedUntil: 0 },
393
+ evidenced.has(entry.id) ? entry : providerAccountWithBlocks(entry, (block) => block.blockedQuota === true),
354
394
  now,
355
395
  model,
356
396
  opts
@@ -363,7 +403,7 @@ function poolSend(providerName, account) {
363
403
  if (all) {
364
404
  return quotaExhausted(error || accountsExhaustedError(opts.useCredits), providerName, model, rows, opts);
365
405
  }
366
- return error || accountsCoolingDownError(rows, now);
406
+ return error || accountsCoolingDownError(rows, now, model);
367
407
  };
368
408
  // Accounts this send blocked on a rate-limit cooldown, the time it spent
369
409
  // waiting for cooldowns, and the provider's wait bound.
@@ -381,8 +421,9 @@ function poolSend(providerName, account) {
381
421
  const bound = waitBoundMs(ids);
382
422
  // Persisting a short cooldown can outlast its deadline. Release only
383
423
  // attempts cooled by this send, and only under an enabled wait policy.
384
- const expired = bound > 0 && rows.some((row) => cooled.has(row.id) && !(Number(row.blockedUntil) > now));
385
- const wait = all ? null : expired ? 0 : earliestCooldownMs(rows, now);
424
+ const expired = bound > 0 && rows.some((row) => cooled.has(row.id) && !(coolingUntil(row, model) > now));
425
+ let wait = null;
426
+ if (!all) wait = expired ? 0 : earliestCooldownMs(rows, now, model);
386
427
  if (wait == null || waitedMs + wait > bound) throw markExhausted(error, ids);
387
428
  options.onStageChange?.('reconnecting', {
388
429
  message: `Rate limited — every account is cooling down; retrying in ${Math.ceil(wait / 1000)}s`,
@@ -397,7 +438,7 @@ function poolSend(providerName, account) {
397
438
  waitedMs += wait;
398
439
  const lifted = Date.now();
399
440
  for (const row of readProviderAccountPool(providerName).accounts) {
400
- if (cooled.has(row.id) && !(Number(row.blockedUntil) > lifted)) {
441
+ if (cooled.has(row.id) && !(coolingUntil(row, model) > lifted)) {
401
442
  cooled.delete(row.id);
402
443
  attempted.delete(row.id);
403
444
  }
@@ -409,7 +450,8 @@ function poolSend(providerName, account) {
409
450
  if (!pool.accounts.length) {
410
451
  if (pool.useCredits !== true) {
411
452
  const stored = readCachedOAuthUsageSnapshot({ provider: providerName, model: '' });
412
- if (stored?.quotaWindows?.length) {
453
+ // A snapshot cached before per-model gating is stale evidence.
454
+ if (stored?.quotaWindows?.length && !staleQuotaWindows(providerName, stored.quotaWindows)) {
413
455
  const row = { usage: { windows: stored.quotaWindows, checkedAt: stored.cachedAt } };
414
456
  if (providerAccountExhausted(row, Date.now(), model)) {
415
457
  throw quotaExhausted(accountsExhaustedError(false), providerName, model, [row]);
@@ -480,7 +522,7 @@ function poolSend(providerName, account) {
480
522
  options.onStageChange?.('reconnecting', { message: 'Sign-in expired — switching to the next account' });
481
523
  continue;
482
524
  }
483
- const { exhausted, evidence, until, source, accountBlocked } = await accountQuotaExhausted(
525
+ const { exhausted, evidence, until, source, reset, accountBlocked } = await accountQuotaExhausted(
484
526
  providerName,
485
527
  row,
486
528
  model,
@@ -490,7 +532,13 @@ function poolSend(providerName, account) {
490
532
  if (options.signal?.aborted || !exhausted) throw error;
491
533
  if (evidence) evidenced.add(row.id);
492
534
  else cooled.add(row.id);
493
- await blockProviderAccount(providerName, row.id, until, model, source, { quota: evidence === true });
535
+ // An account-level block holds every model, so it carries no model or
536
+ // limit scope.
537
+ await blockProviderAccount(providerName, row.id, until, accountBlocked ? '' : model, source, {
538
+ quota: evidence === true,
539
+ reset: reset === true,
540
+ limit: accountBlocked ? '' : quotaLimitOf(error),
541
+ });
494
542
  // An account block is unsafe to re-issue on THAT account only; with no
495
543
  // output emitted, the next account may serve the same request.
496
544
  if (
@@ -509,13 +557,10 @@ function poolSend(providerName, account) {
509
557
  lastError = error;
510
558
  switched = true;
511
559
  refusalScoped = accountExhaustedForModelOnly(providerName, row.id, model);
512
- options.onStageChange?.('reconnecting', {
513
- message: evidence
514
- ? 'Quota exhausted — switching to the next account'
515
- : accountBlocked
516
- ? 'Account blocked — switching to the next account'
517
- : 'Rate limited (cooldown) — switching to the next account',
518
- });
560
+ let reason = 'Rate limited (cooldown)';
561
+ if (evidence) reason = 'Quota exhausted';
562
+ else if (accountBlocked) reason = 'Account blocked';
563
+ options.onStageChange?.('reconnecting', { message: `${reason} — switching to the next account` });
519
564
  }
520
565
  }
521
566
  throw markExhausted(lastError || new Error('Provider account attempts exhausted.'));
@@ -2,6 +2,13 @@ import { createHash } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { PRIORITY_RANK, normalizePriority, resourceAdmission } from '../../../shared/resource-admission.mjs';
4
4
  import { accountProviderSend } from '../../../shared/llm/usage-accounting.mjs';
5
+ import { sanitizeSurrogates } from '../../../shared/llm/well-formed.mjs';
6
+ import { providerAvailable, providerUnavailableError } from './provider-availability.mjs';
7
+ import {
8
+ finalizeProviderRequestTools,
9
+ isFinalizedProviderRequestTools,
10
+ providerNativeToolPrefixCount,
11
+ } from '../runtime-core/provider-request-tools.mjs';
5
12
 
6
13
  // Normal provider traffic is not concurrency-gated. Independent sessions and
7
14
  // accounts start immediately; a finite limit exists only when an operator
@@ -518,7 +525,18 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
518
525
  if (!provider || typeof provider.send !== 'function' || provider[WRAPPED]) return provider;
519
526
  const originalSend = provider.send;
520
527
  Object.defineProperty(provider, WRAPPED, { value: true });
521
- provider.send = function admittedProviderSend(messages, model, tools, sendOpts) {
528
+ provider.send = function admittedProviderSend(rawMessages, model, rawTools, sendOpts) {
529
+ // Every registered provider (and its account-pool / proxy / custom variants)
530
+ // is wrapped here, so this is the one point all model requests pass before
531
+ // serialization. A lone surrogate would make the API reject the body on
532
+ // every send; only the outgoing request is rewritten, never the transcript.
533
+ const messages = sanitizeSurrogates(rawMessages);
534
+ let tools = sanitizeSurrogates(rawTools);
535
+ // A finalized tool list is recognised by identity; its sanitized copy must
536
+ // stay finalized with the same native prefix (the model proxy reads both).
537
+ if (tools !== rawTools && isFinalizedProviderRequestTools(rawTools)) {
538
+ tools = finalizeProviderRequestTools(tools, providerNativeToolPrefixCount(rawTools));
539
+ }
522
540
  const opts = sendOpts || {};
523
541
  const signal = opts.signal || null;
524
542
  const key = providerAdmissionKey(providerName, this);
@@ -530,6 +548,9 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
530
548
  scheduler.run(
531
549
  key,
532
550
  (admissionSignal, admissionMetrics) => {
551
+ // The Developer option may have been switched off after lookup or
552
+ // while this request waited: the gate is rechecked right before send.
553
+ if (!providerAvailable(providerName)) throw providerUnavailableError(providerName);
533
554
  // Admission is the common request-clock boundary for WS/SSE/HTTP.
534
555
  // Queue wait therefore cannot consume first-byte or agent-watchdog
535
556
  // time. Provider-local retry remains the sole retry owner.
@@ -18,13 +18,17 @@ import { applyAnthropicEffortToBody, shouldIncludeEffortBeta } from './anthropic
18
18
  import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
19
19
  import {
20
20
  anthropicThinkingActive,
21
+ anthropicTurnScopedReminders,
21
22
  appendAnthropicTailReminder,
23
+ appendAnthropicTurnReminder,
22
24
  applyAnthropicCacheMarkers,
23
25
  clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
26
+ hasAnthropicTurnScopedMessages,
24
27
  resolveAnthropicCacheTtls as resolveCacheTtls,
25
28
  resolveAnthropicMessageCacheSlots,
26
29
  sharedCachePrefixBoundary,
27
30
  toAnthropicToolChoice,
31
+ withAnthropicTurnReminders,
28
32
  } from './lib/anthropic-request-utils.mjs';
29
33
  import { splitTailReminder } from './lib/tail-reminder-split.mjs';
30
34
  import {
@@ -97,12 +101,17 @@ export async function buildAnthropicApiRequest({
97
101
  // are applied to the FINAL sanitized array by invariant, so block
98
102
  // drops / inserts / reorders performed by the sanitizer can never move
99
103
  // or delete a marked block. NEVER sanitize again after this.
100
- const lowerHistory = (history, projection) =>
101
- lowerAnthropicEffortHistory(
104
+ // Turn-scoped tail reminders (tail-reminder-split.mjs): stored reminders are
105
+ // re-sent before their assistant turns, the current one ends the request.
106
+ const turnScoped = anthropicTurnScopedReminders(name, useModel, config);
107
+ const lowerHistory = (history, projection) => {
108
+ const lowered = lowerAnthropicEffortHistory(
102
109
  history.filter((m) => m.role !== 'system'),
103
110
  (segment) => toAnthropicMessages(segment, requestTools, name),
104
111
  projection
105
112
  );
113
+ return turnScoped ? withAnthropicTurnReminders(lowered) : lowered;
114
+ };
106
115
  const anthropicMessages = applyAnthropicCacheMarkers(
107
116
  lowerHistory(chatMsgs, effortProjection),
108
117
  messageCacheSlots,
@@ -115,9 +124,12 @@ export async function buildAnthropicApiRequest({
115
124
  model: useModel,
116
125
  max_tokens: maxTokens,
117
126
  system: systemBlocks.length ? systemBlocks : undefined,
118
- messages: await prepareAnthropicImages(appendAnthropicTailReminder(anthropicMessages, reminder), {
119
- signal: opts.signal,
120
- }),
127
+ messages: await prepareAnthropicImages(
128
+ turnScoped
129
+ ? appendAnthropicTurnReminder(anthropicMessages, reminder)
130
+ : appendAnthropicTailReminder(anthropicMessages, reminder),
131
+ { signal: opts.signal }
132
+ ),
121
133
  };
122
134
  // Server-side fallback and fast mode are decided from Claude model ids:
123
135
  // first-party knowledge, never applied to a custom endpoint.
@@ -255,6 +267,7 @@ function requestHeadersFor(
255
267
  effort: isCustom ? Boolean(params.output_config?.effort) : shouldIncludeEffortBeta(capabilityModel, opts),
256
268
  serverFallback: params.fallbacks === 'default',
257
269
  thinkingDisplayUpdates: params.thinking?.display === 'updates',
270
+ turnScopedSystem: hasAnthropicTurnScopedMessages(params.messages),
258
271
  }),
259
272
  ...(effortProjection ? [EFFORT_CONFIGURATION_BETA] : []),
260
273
  ].filter(Boolean);
@@ -10,7 +10,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
10
10
  // source release just to advance the user-agent version.
11
11
  // Claude Opus 5.5 requires 2.1.280 or newer. The release workflow raises this
12
12
  // to the published CLI version (scripts/sync-client-version-floors.mjs).
13
- export const DEFAULT_CLI_VERSION = '2.1.308';
13
+ export const DEFAULT_CLI_VERSION = '2.1.310';
14
14
  export const CLAUDE_CLI_NPM_PACKAGE = '@anthropic-ai/claude-code';
15
15
 
16
16
  const CACHE_SCHEMA_VERSION = 1;
@@ -61,10 +61,13 @@ import { ANTHROPIC_EFFORT_LEVELS, withEffortRejectionFallback } from './effort-r
61
61
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
62
62
  import {
63
63
  anthropicThinkingActive,
64
+ anthropicTurnScopedReminders,
64
65
  appendAnthropicTailReminder,
66
+ appendAnthropicTurnReminder,
65
67
  applyAnthropicCacheMarkers,
66
68
  clampAnthropicThinkingBudget as clampThinkingBudgetTokens,
67
69
  deferredAnthropicTools as sharedDeferredAnthropicTools,
70
+ hasAnthropicTurnScopedMessages,
68
71
  requestAnthropicTools as sharedRequestAnthropicTools,
69
72
  resolveAnthropicCacheTtls as resolveCacheTtls,
70
73
  resolveAnthropicMessageCacheSlots,
@@ -72,6 +75,7 @@ import {
72
75
  sharedCachePrefixBoundary,
73
76
  toAnthropicMessages,
74
77
  toAnthropicToolChoice,
78
+ withAnthropicTurnReminders,
75
79
  } from './lib/anthropic-request-utils.mjs';
76
80
  import { splitTailReminder } from './lib/tail-reminder-split.mjs';
77
81
  import {
@@ -111,6 +115,7 @@ function buildOAuthBetaHeaders(body, { fastMode = false, toolSearch = false, mod
111
115
  effort: shouldIncludeEffortBeta(model, opts),
112
116
  serverFallback: body?.fallbacks === 'default',
113
117
  thinkingDisplayUpdates: body?.thinking?.display === 'updates',
118
+ turnScopedSystem: hasAnthropicTurnScopedMessages(body?.messages),
114
119
  });
115
120
  }
116
121
 
@@ -223,12 +228,17 @@ function buildRequestBody(requestMessages, model, tools, sendOpts) {
223
228
  // applied to the FINAL sanitized array by invariant, so block drops /
224
229
  // inserts / reorders performed by the sanitizer can never move or delete a
225
230
  // marked block. NEVER sanitize again after this (see send path).
226
- const lowerHistory = (history, projection) =>
227
- lowerAnthropicEffortHistory(
231
+ // Turn-scoped tail reminders (tail-reminder-split.mjs): stored reminders are
232
+ // re-sent before their assistant turns, the current one ends the request.
233
+ const turnScoped = anthropicTurnScopedReminders('anthropic-oauth', model);
234
+ const lowerHistory = (history, projection) => {
235
+ const lowered = lowerAnthropicEffortHistory(
228
236
  history.filter((m) => m.role !== 'system'),
229
237
  (segment) => toAnthropicMessages(segment, requestTools),
230
238
  projection
231
239
  );
240
+ return turnScoped ? withAnthropicTurnReminders(lowered) : lowered;
241
+ };
232
242
  const anthropicMessages = applyAnthropicCacheMarkers(
233
243
  lowerHistory(chatMsgs, effortProjection),
234
244
  messageCacheSlots,
@@ -240,7 +250,9 @@ function buildRequestBody(requestMessages, model, tools, sendOpts) {
240
250
  const body = {
241
251
  model,
242
252
  max_tokens: maxTokens,
243
- messages: appendAnthropicTailReminder(anthropicMessages, reminder),
253
+ messages: turnScoped
254
+ ? appendAnthropicTurnReminder(anthropicMessages, reminder)
255
+ : appendAnthropicTailReminder(anthropicMessages, reminder),
244
256
  stream: true,
245
257
  };
246
258
  applyAnthropicServerFallback(body, model, {
@@ -23,7 +23,8 @@ import {
23
23
  } from './antigravity-oauth-tokens.mjs';
24
24
 
25
25
  const ANTIGRAVITY_MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
26
- const ANTIGRAVITY_MODEL_CACHE_VERSION = 1;
26
+ // v2: rows keep the gateway-declared per-model `maxOutputTokens`.
27
+ const ANTIGRAVITY_MODEL_CACHE_VERSION = 2;
27
28
  const FETCH_MODELS_TIMEOUT_MS = 15_000;
28
29
  // Tier labels the gateway puts in parentheses, in effort order.
29
30
  const TIER_EFFORTS = { 'extra low': 'minimal', minimal: 'minimal', low: 'low', medium: 'medium', high: 'high' };
@@ -145,10 +146,21 @@ export function normalizeAntigravityCatalog(rawModels) {
145
146
  const key = familyId(base);
146
147
  let family = families.get(key);
147
148
  if (!family) {
148
- family = { base, contextWindow: 0, supportsVision: false, supportsReasoning: false, wires: new Map() };
149
+ family = {
150
+ base,
151
+ contextWindow: 0,
152
+ maxOutputTokens: 0,
153
+ supportsVision: false,
154
+ supportsReasoning: false,
155
+ wires: new Map(),
156
+ };
149
157
  families.set(key, family);
150
158
  }
151
159
  family.contextWindow = Math.max(family.contextWindow, contextWindow);
160
+ // Wire ids sharing one record keep the smallest declared output limit, so
161
+ // the ceiling holds for every id the record can resolve to.
162
+ const declaredOutput = Math.floor(Number(entry.maxOutputTokens));
163
+ if (declaredOutput > 0) family.maxOutputTokens = Math.min(family.maxOutputTokens || declaredOutput, declaredOutput);
152
164
  family.supportsVision ||= entry.supportsImages === true;
153
165
  family.supportsReasoning ||= entry.supportsThinking === true;
154
166
  const slot = effort || '';
@@ -177,6 +189,7 @@ export function normalizeAntigravityCatalog(rawModels) {
177
189
  provider: 'antigravity-oauth',
178
190
  ...(kind ? { family: kind } : {}),
179
191
  contextWindow: family.contextWindow,
192
+ ...(family.maxOutputTokens ? { maxOutputTokens: family.maxOutputTokens } : {}),
180
193
  supportsVision: family.supportsVision,
181
194
  supportsReasoning: family.supportsReasoning,
182
195
  supportsFunctionCalling: true,
@@ -210,6 +223,19 @@ export function antigravityModelFamily(model, models = antigravityCatalogModels(
210
223
  return typeof record?.family === 'string' && record.family ? record.family : null;
211
224
  }
212
225
 
226
+ /**
227
+ * The output-token limit the gateway declared for a picker id or one of its
228
+ * wire ids, or null when the catalog row carries none (the curated fallback
229
+ * list and caches written before the field existed declare none).
230
+ */
231
+ export function antigravityOutputLimit(model, models = antigravityCatalogModels()) {
232
+ const id = String(model || '');
233
+ const wires = (entry) => (entry?.wire && typeof entry.wire === 'object' ? Object.values(entry.wire) : [entry?.wire]);
234
+ const record = models.find((entry) => entry?.id === id) || models.find((entry) => wires(entry).includes(id));
235
+ const limit = Math.floor(Number(record?.maxOutputTokens));
236
+ return limit > 0 ? limit : null;
237
+ }
238
+
213
239
  /**
214
240
  * Turn a picker model + effort into the wire id the gateway accepts. Tiered
215
241
  * families consume the effort (the id encodes it); a bare id whose record
@@ -232,7 +258,7 @@ export function resolveAntigravityWireModel(model, effort, models = antigravityC
232
258
  return { model: wire, effort: record.reasoningLevels?.length ? effort : null };
233
259
  }
234
260
 
235
- function quotaRow(label, bucket) {
261
+ function quotaRow(label, bucket, scope) {
236
262
  if (!bucket || typeof bucket !== 'object') return null;
237
263
  if (typeof bucket.remainingFraction !== 'number' || !Number.isFinite(bucket.remainingFraction)) return null;
238
264
  const usedPct = Math.round(Math.max(0, Math.min(100, (1 - bucket.remainingFraction) * 100)) * 100) / 100;
@@ -242,6 +268,7 @@ function quotaRow(label, bucket) {
242
268
  usedPct,
243
269
  ...(Number.isFinite(resetAt) && resetAt > 0 ? { resetAt } : {}),
244
270
  source: 'antigravity-quota-summary',
271
+ ...scope,
245
272
  };
246
273
  }
247
274
 
@@ -254,11 +281,21 @@ const ARGON_WEEKLY_BUCKET_ID = 'gemini-argon-wtu-weekly';
254
281
  const THIRD_PARTY_5H_BUCKET_ID = '3p-5h';
255
282
  const THIRD_PARTY_WEEKLY_BUCKET_ID = '3p-weekly';
256
283
 
284
+ // The model ids each separate bucket meters: the "Claude and GPT models"
285
+ // group (Claude Opus, Claude Sonnet, GPT-OSS) and the Gemini 4 Argon group.
286
+ const THIRD_PARTY_MODELS = Object.freeze(['claude', 'gpt']);
287
+ const ARGON_MODELS = Object.freeze(['argon']);
288
+
257
289
  /**
258
290
  * Gemini 5H/7D rows from retrieveUserQuotaSummary, the separate weekly limit
259
291
  * of the Gemini 4 Argon group (its own group with only a weekly bucket), and
260
292
  * the Claude/GPT-OSS 5H/7D pair. Rows are selected by bucket id and window.
261
293
  * Missing fields stay omitted.
294
+ *
295
+ * Each group's buckets gate its own models only: the 3p pair gates Claude and
296
+ * GPT-OSS, the Argon bucket gates Argon, and the shared Gemini pair gates every
297
+ * model no other bucket in this summary claims, so a full Claude/GPT window
298
+ * leaves Gemini serving and the reverse.
262
299
  */
263
300
  export function antigravityQuotaWindows(summary) {
264
301
  const groups = (Array.isArray(summary?.groups) ? summary.groups : []).filter(
@@ -268,11 +305,18 @@ export function antigravityQuotaWindows(summary) {
268
305
  const bucket = (id, window) =>
269
306
  buckets.find((entry) => entry && typeof entry === 'object' && entry.bucketId === id && entry.window === window) ??
270
307
  null;
308
+ const reported = (...ids) => buckets.some((entry) => ids.includes(entry?.bucketId));
309
+ const claimed = [
310
+ ...(reported(THIRD_PARTY_5H_BUCKET_ID, THIRD_PARTY_WEEKLY_BUCKET_ID) ? THIRD_PARTY_MODELS : []),
311
+ ...(reported(ARGON_WEEKLY_BUCKET_ID) ? ARGON_MODELS : []),
312
+ ];
313
+ const gemini = claimed.length ? { except: claimed } : {};
314
+ const thirdParty = { models: [...THIRD_PARTY_MODELS] };
271
315
  return [
272
- quotaRow('5H', bucket(GEMINI_5H_BUCKET_ID, '5h')),
273
- quotaRow('7D', bucket(GEMINI_WEEKLY_BUCKET_ID, 'weekly')),
274
- quotaRow('7D Argon', bucket(ARGON_WEEKLY_BUCKET_ID, 'weekly')),
275
- quotaRow('5H Claude/GPT', bucket(THIRD_PARTY_5H_BUCKET_ID, '5h')),
276
- quotaRow('7D Claude/GPT', bucket(THIRD_PARTY_WEEKLY_BUCKET_ID, 'weekly')),
316
+ quotaRow('5H', bucket(GEMINI_5H_BUCKET_ID, '5h'), gemini),
317
+ quotaRow('7D', bucket(GEMINI_WEEKLY_BUCKET_ID, 'weekly'), gemini),
318
+ quotaRow('7D Argon', bucket(ARGON_WEEKLY_BUCKET_ID, 'weekly'), { models: [...ARGON_MODELS] }),
319
+ quotaRow('5H Claude/GPT', bucket(THIRD_PARTY_5H_BUCKET_ID, '5h'), thirdParty),
320
+ quotaRow('7D Claude/GPT', bucket(THIRD_PARTY_WEEKLY_BUCKET_ID, 'weekly'), thirdParty),
277
321
  ].filter(Boolean);
278
322
  }
@@ -18,6 +18,8 @@ import { buildAntigravityRequest, isAntigravityClaude as isClaudeModel } from '.
18
18
  import { createPassthroughSignal } from '../stall-policy.mjs';
19
19
  import { preconnect } from '../../../shared/llm/http-agent.mjs';
20
20
  import { createAntigravityStreamCollector } from './antigravity-stream.mjs';
21
+ import { createGeminiReasoningRelay } from './gemini-reasoning-relay.mjs';
22
+ import { splitTailReminder } from './lib/tail-reminder-split.mjs';
21
23
  import { createAntigravityRequest } from './antigravity-transport.mjs';
22
24
  import { finalizeAntigravityTurn } from './antigravity-response.mjs';
23
25
  import {
@@ -41,6 +43,7 @@ import {
41
43
  fetchAvailableModels,
42
44
  fetchUserQuotaSummary,
43
45
  normalizeAntigravityCatalog,
46
+ antigravityOutputLimit,
44
47
  resolveAntigravityWireModel,
45
48
  } from './antigravity-oauth-catalog.mjs';
46
49
 
@@ -109,7 +112,7 @@ export class AntigravityOAuthProvider {
109
112
  }
110
113
 
111
114
  _buildBody(messages, model, tools, opts) {
112
- const body = buildAntigravityRequest(messages, model, tools, opts, this._projectId);
115
+ const body = buildAntigravityRequest(messages, model, tools, opts, this._projectId, antigravityOutputLimit(model));
113
116
  dumpAntigravityRequest(body);
114
117
  return body;
115
118
  }
@@ -131,6 +134,7 @@ export class AntigravityOAuthProvider {
131
134
  // Relayed through opts so the callback keeps its original receiver.
132
135
  const onTextDelta = typeof opts.onTextDelta === 'function' ? (text) => opts.onTextDelta(text) : null;
133
136
  const onStreamDelta = typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null;
137
+ const reasoning = createGeminiReasoningRelay(opts);
134
138
  // Per-turn stream record: live text mirror, ordered parts, native/leaked
135
139
  // tool calls and the exposure marks a failed attempt carries
136
140
  // (antigravity-stream.mjs).
@@ -140,6 +144,8 @@ export class AntigravityOAuthProvider {
140
144
  onToolCall,
141
145
  onTextDelta,
142
146
  onStreamDelta,
147
+ onReasoningDelta: reasoning.onReasoningDelta,
148
+ retractReasoning: reasoning.retract,
143
149
  });
144
150
  if (signal?.aborted) {
145
151
  const reason = signal.reason;
@@ -181,6 +187,9 @@ export class AntigravityOAuthProvider {
181
187
  collector,
182
188
  sleepFn: this.config.sleepFn,
183
189
  firstByte: antigravityFirstBytePolicy(useModel),
190
+ // The tail reminder is not history: an empty answer right after a tool
191
+ // result is the model's answer, whatever reminder rides behind it.
192
+ afterToolResult: splitTailReminder(messages).messages.at(-1)?.role === 'tool',
184
193
  });
185
194
  try {
186
195
  try {
@@ -3,10 +3,17 @@ import { createProviderReplay, providerReplayItems } from './lib/provider-replay
3
3
  import { toGeminiContents, toGeminiRequestTools, toGeminiToolConfig } from './gemini-schema.mjs';
4
4
  import { geminiThinkingConfig } from './gemini-thinking.mjs';
5
5
  import { isGeminiFamilyModel } from './gemini-family.mjs';
6
+ import { geminiMaxOutputTokens } from './gemini-output-limit.mjs';
6
7
  import { splitTailReminder } from './lib/tail-reminder-split.mjs';
7
8
 
8
9
  const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator';
10
+ // Used only when the gateway declared no output limit for a Claude id (the
11
+ // gateway enforces Claude's limit per model: one token over it is a 400).
9
12
  const CLAUDE_MAX_OUTPUT_TOKENS = 64000;
13
+ const claudeMaxOutputTokens = (declared) => {
14
+ const limit = Math.floor(Number(declared));
15
+ return limit > 0 ? limit : CLAUDE_MAX_OUTPUT_TOKENS;
16
+ };
10
17
 
11
18
  // Claude models behind the gateway speak a different thinking/replay dialect
12
19
  // (token budgets, anthropic-beta header, their own signatures). The roster
@@ -190,7 +197,7 @@ function claudeGatewayLevel(value) {
190
197
 
191
198
  // An explicit token budget is sent as-is; otherwise the chosen effort rides
192
199
  // on thinkingLevel with the wire id unchanged.
193
- function claudeThinkingConfig(opts) {
200
+ function claudeThinkingConfig(opts, cap) {
194
201
  const budget = opts.thinkingBudget ?? opts.thinkingBudgetTokens;
195
202
  const thinkingConfig = { includeThoughts: true };
196
203
  if (budget == null) {
@@ -199,8 +206,8 @@ function claudeThinkingConfig(opts) {
199
206
  return thinkingConfig;
200
207
  }
201
208
  const value = Number(budget);
202
- if (!Number.isInteger(value) || value < 1024 || value >= CLAUDE_MAX_OUTPUT_TOKENS) {
203
- throw new TypeError('Antigravity Claude thinkingBudget must be an integer from 1024 to 63999.');
209
+ if (!Number.isInteger(value) || value < 1024 || value >= cap) {
210
+ throw new TypeError(`Antigravity Claude thinkingBudget must be an integer from 1024 to ${cap - 1}.`);
204
211
  }
205
212
  thinkingConfig.thinkingBudget = value;
206
213
  return thinkingConfig;
@@ -220,7 +227,9 @@ function antigravityGeminiThinkingConfig(model, opts) {
220
227
  });
221
228
  }
222
229
 
223
- export function buildAntigravityRequest(requestMessages, model, tools, opts = {}, projectId) {
230
+ // `outputLimit` is the output-token limit the gateway declared for this wire id
231
+ // (the catalog row's `maxOutputTokens`), or null when it declared none.
232
+ export function buildAntigravityRequest(requestMessages, model, tools, opts = {}, projectId, outputLimit = null) {
224
233
  // The tail reminder is not history: the tool-loop closure below must see the
225
234
  // real tail, so the reminder is appended to the wire contents last.
226
235
  const { messages, reminder } = splitTailReminder(requestMessages);
@@ -232,10 +241,17 @@ export function buildAntigravityRequest(requestMessages, model, tools, opts = {}
232
241
  const history = toGeminiContents(signatureSafeMessages(chatMessages, model), model);
233
242
  if (!history.length) throw new Error('No messages to send');
234
243
  const claude = isAntigravityClaude(model);
235
- const thinkingConfig = claude ? claudeThinkingConfig(opts) : antigravityGeminiThinkingConfig(model, opts);
244
+ const claudeCap = claudeMaxOutputTokens(outputLimit);
245
+ const thinkingConfig = claude ? claudeThinkingConfig(opts, claudeCap) : antigravityGeminiThinkingConfig(model, opts);
236
246
  const contents = claude && claudeThinkingEnabled(model, thinkingConfig) ? closeUnthoughtToolLoop(history) : history;
247
+ // Gemini wire ids carry the model's output ceiling (thoughts count against
248
+ // it); Claude is always sent a cap (the declared limit, else a fixed one);
249
+ // GPT-OSS keeps the plain config its gateway accepts.
250
+ let maxOutputTokens;
251
+ if (claude) maxOutputTokens = claudeCap;
252
+ else if (isGeminiFamilyModel(model)) maxOutputTokens = geminiMaxOutputTokens(model, opts, null, outputLimit);
237
253
  const generationConfig = {
238
- ...(claude ? { maxOutputTokens: CLAUDE_MAX_OUTPUT_TOKENS } : {}),
254
+ ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
239
255
  ...(thinkingConfig ? { thinkingConfig } : {}),
240
256
  };
241
257
  const request = { contents: reminder ? [...contents, { role: 'user', parts: [{ text: reminder }] }] : contents };
@@ -23,10 +23,21 @@ export function normalizeAntigravityUsage(um) {
23
23
  (um.promptTokenCount || um.prompt_token_count || 0) +
24
24
  (um.toolUsePromptTokenCount || um.tool_use_prompt_token_count || 0);
25
25
  const cachedTokens = um.cachedContentTokenCount || um.cached_content_token_count || 0;
26
- const outputTokens =
27
- (um.candidatesTokenCount || um.candidates_token_count || 0) +
28
- (um.thoughtsTokenCount || um.thoughts_token_count || 0);
29
- return { inputTokens, outputTokens, cachedTokens, promptTokens: inputTokens, raw: um };
26
+ const thoughtTokens = um.thoughtsTokenCount ?? um.thoughts_token_count;
27
+ // Thought tokens are billed and capped as output.
28
+ const outputTokens = (um.candidatesTokenCount || um.candidates_token_count || 0) + (Number(thoughtTokens) || 0);
29
+ const reportedTotal = Number(um.totalTokenCount ?? um.total_token_count);
30
+ return {
31
+ inputTokens,
32
+ outputTokens,
33
+ cachedTokens,
34
+ promptTokens: inputTokens,
35
+ // Reasoning is reported only when the gateway reported thoughts (a missing
36
+ // count is not zero).
37
+ ...(typeof thoughtTokens === 'number' ? { reasoningTokens: thoughtTokens } : {}),
38
+ totalTokens: reportedTotal > 0 ? reportedTotal : inputTokens + outputTokens,
39
+ raw: um,
40
+ };
30
41
  }
31
42
 
32
43
  /**