@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -1,11 +1,38 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sanitizeGeminiToolParameters } from "./google-tool-schema";
2
+ import {
3
+ addGoogleToolSchemaLoss,
4
+ createGoogleToolSchemaLossReport,
5
+ GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT,
6
+ mergeGoogleToolSchemaLossReport,
7
+ sanitizeGeminiToolParametersWithReport,
8
+ type GoogleToolSchemaLossReport,
9
+ type GoogleToolSchemaPolicy,
10
+ type GoogleToolSchemaProfile,
11
+ } from "./google-tool-schema";
3
12
 
4
13
  type JsonObject = Record<string, unknown>;
5
14
 
6
15
  const GOOGLE_TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
7
16
  const GOOGLE_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
8
17
 
18
+ export class GoogleToolSchemaPolicyError extends Error {
19
+ readonly phase = "initial" as const;
20
+ readonly endpointClass: GoogleToolSchemaProfile["endpointClass"];
21
+ readonly categories: GoogleToolSchemaLossReport["categories"];
22
+ /** True when refusal is due to exhausted bounded comparison rather than proven loss. */
23
+ readonly indeterminate: boolean;
24
+
25
+ constructor(report: GoogleToolSchemaLossReport) {
26
+ const categories = { ...report.categories };
27
+ const indeterminate = report.uncertainComparisons > 0;
28
+ super(`google tool schema policy rejected ${report.lossy ? "lossy" : "indeterminate"} initial compilation (${report.endpointClass}; categories=${JSON.stringify(categories)}${indeterminate ? `; uncertainComparisons=${report.uncertainComparisons}` : ""})`);
29
+ this.name = "GoogleToolSchemaPolicyError";
30
+ this.endpointClass = report.endpointClass;
31
+ this.categories = categories;
32
+ this.indeterminate = indeterminate;
33
+ }
34
+ }
35
+
9
36
  function isObject(value: unknown): value is JsonObject {
10
37
  return !!value && typeof value === "object" && !Array.isArray(value);
11
38
  }
@@ -94,16 +121,23 @@ function compileContents(value: unknown, toWireName: (name: string) => string):
94
121
  });
95
122
  }
96
123
 
97
- function compileTools(value: unknown, toWireName: (name: string) => string): unknown[] | undefined {
124
+ function compileTools(
125
+ value: unknown,
126
+ toWireName: (name: string) => string,
127
+ profile: GoogleToolSchemaProfile,
128
+ lossReport: GoogleToolSchemaLossReport,
129
+ ): unknown[] | undefined {
98
130
  if (!Array.isArray(value)) return undefined;
99
131
  const tools = value.flatMap(rawTool => {
100
132
  if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return [];
101
133
  const functionDeclarations = rawTool.functionDeclarations.flatMap(rawDeclaration => {
102
134
  if (!isObject(rawDeclaration) || typeof rawDeclaration.name !== "string") return [];
135
+ const sanitized = sanitizeGeminiToolParametersWithReport(rawDeclaration.parameters, profile);
136
+ mergeGoogleToolSchemaLossReport(lossReport, sanitized.lossReport);
103
137
  return [{
104
138
  name: toWireName(rawDeclaration.name),
105
139
  ...(typeof rawDeclaration.description === "string" ? { description: rawDeclaration.description } : {}),
106
- parameters: sanitizeGeminiToolParameters(rawDeclaration.parameters),
140
+ parameters: sanitized.parameters,
107
141
  }];
108
142
  });
109
143
  return functionDeclarations.length > 0 ? [{ functionDeclarations }] : [];
@@ -180,24 +214,35 @@ function compileToolConfig(value: unknown, toWireName: (name: string) => string)
180
214
  * Final trust boundary for every Google-family request. The adapter may build a convenient
181
215
  * Gemini-shaped object; only this compiler is allowed to decide what reaches the wire.
182
216
  */
183
- export function compileGoogleWireBody(input: unknown): {
217
+ export function compileGoogleWireBody(
218
+ input: unknown,
219
+ profile: GoogleToolSchemaProfile = { endpointClass: "ai-studio" },
220
+ policy: GoogleToolSchemaPolicy = "compatible",
221
+ ): {
184
222
  body: JsonObject;
185
223
  restoreToolName: (name: string) => string;
224
+ toolSchemaLossReport: GoogleToolSchemaLossReport;
186
225
  } {
187
226
  const source = isObject(input) ? input : {};
188
227
  const names = toolNameCodec(collectToolNames(source));
189
228
  const body: JsonObject = {};
229
+ const toolSchemaLossReport = createGoogleToolSchemaLossReport(profile);
190
230
  const contents = compileContents(source.contents, names.toWire);
191
231
  if (contents) body.contents = contents;
192
232
  if (isObject(source.systemInstruction)) body.systemInstruction = source.systemInstruction;
193
- const tools = compileTools(source.tools, names.toWire);
233
+ const tools = compileTools(source.tools, names.toWire, profile, toolSchemaLossReport);
194
234
  if (tools) body.tools = tools;
235
+ // Strict mode refuses proven loss and bounded-comparison indeterminacy alike: an exhausted
236
+ // comparison can hide a changed constraint, so unknown is not admitted as lossless.
237
+ if (policy === "reject-lossy" && (toolSchemaLossReport.lossy || toolSchemaLossReport.uncertainComparisons > 0)) {
238
+ throw new GoogleToolSchemaPolicyError(toolSchemaLossReport);
239
+ }
195
240
  const generationConfig = compileGenerationConfig(source.generationConfig);
196
241
  if (generationConfig) body.generationConfig = generationConfig;
197
242
  const toolConfig = compileToolConfig(source.toolConfig, names.toWire);
198
243
  if (toolConfig) body.toolConfig = toolConfig;
199
244
  if (typeof source.sessionId === "string" && source.sessionId.length > 0) body.sessionId = source.sessionId;
200
- return { body, restoreToolName: names.fromWire };
245
+ return { body, restoreToolName: names.fromWire, toolSchemaLossReport };
201
246
  }
202
247
 
203
248
  function functionDeclarations(root: JsonObject): JsonObject[] {
@@ -208,10 +253,35 @@ function functionDeclarations(root: JsonObject): JsonObject[] {
208
253
  });
209
254
  }
210
255
 
256
+ /**
257
+ * Upstream INVALID_ARGUMENT text blaming a tool or function declaration schema.
258
+ *
259
+ * Shared with the diagnostic error classifier so the repair path and the description of why a
260
+ * request was rejected cannot drift into disagreeing about the same payload.
261
+ */
262
+ export function isGoogleToolSchemaErrorText(errorPayload: string): boolean {
263
+ return /(?:input[_ ]schema|json schema|function[_ ]declarations?|x-mcp-header)/i.test(errorPayload);
264
+ }
265
+
266
+ /** Upstream INVALID_ARGUMENT text blaming the thinking configuration. */
267
+ export function isGoogleThinkingConfigErrorText(errorPayload: string): boolean {
268
+ return /thinking[_ ]?(?:config|level)/i.test(errorPayload);
269
+ }
270
+
271
+ export interface GoogleInvalidRequestRepair {
272
+ body: string;
273
+ toolSchemaLoss?: GoogleToolSchemaLossReport;
274
+ toolSchemaDeclarationCount?: number;
275
+ }
276
+
211
277
  /** Build a changed request for one known-safe replay of an INVALID_ARGUMENT response. */
212
- export function repairGoogleInvalidRequestBody(body: string, errorPayload: string): string | undefined {
213
- const schemaError = /(?:input[_ ]schema|json schema|function[_ ]declarations?|x-mcp-header)/i.test(errorPayload);
214
- const thinkingError = /thinking[_ ]?(?:config|level)/i.test(errorPayload);
278
+ export function repairGoogleInvalidRequestBodyWithReport(
279
+ body: string,
280
+ errorPayload: string,
281
+ profile: GoogleToolSchemaProfile = { endpointClass: "ai-studio" },
282
+ ): GoogleInvalidRequestRepair | undefined {
283
+ const schemaError = isGoogleToolSchemaErrorText(errorPayload);
284
+ const thinkingError = isGoogleThinkingConfigErrorText(errorPayload);
215
285
  if (!schemaError && !thinkingError) return undefined;
216
286
  let parsed: unknown;
217
287
  try {
@@ -222,6 +292,8 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
222
292
  if (!isObject(parsed)) return undefined;
223
293
  const root = isObject(parsed.request) ? parsed.request : parsed;
224
294
  let changed = false;
295
+ let toolSchemaLoss: GoogleToolSchemaLossReport | undefined;
296
+ let toolSchemaDeclarationCount: number | undefined;
225
297
 
226
298
  if (thinkingError && isObject(root.generationConfig) && "thinkingConfig" in root.generationConfig) {
227
299
  delete root.generationConfig.thinkingConfig;
@@ -237,6 +309,9 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
237
309
  const index = indexed === undefined ? -1 : Number.parseInt(indexed, 10);
238
310
  const rejected = declarations[index];
239
311
  const targets = rejected ? [rejected] : declarations;
312
+ toolSchemaLoss = createGoogleToolSchemaLossReport(profile);
313
+ addGoogleToolSchemaLoss(toolSchemaLoss, "repair-opened-schema", targets.length);
314
+ toolSchemaDeclarationCount = Math.min(targets.length, GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT);
240
315
  for (const declaration of targets) {
241
316
  declaration.parameters = { type: "object", properties: {} };
242
317
  delete declaration.parametersJsonSchema;
@@ -244,5 +319,13 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
244
319
  changed = true;
245
320
  }
246
321
  }
247
- return changed ? JSON.stringify(parsed) : undefined;
322
+ return changed ? {
323
+ body: JSON.stringify(parsed),
324
+ ...(toolSchemaLoss ? { toolSchemaLoss, toolSchemaDeclarationCount } : {}),
325
+ } : undefined;
326
+ }
327
+
328
+ /** Compatibility facade for callers that need only the changed body. */
329
+ export function repairGoogleInvalidRequestBody(body: string, errorPayload: string): string | undefined {
330
+ return repairGoogleInvalidRequestBodyWithReport(body, errorPayload)?.body;
248
331
  }
@@ -0,0 +1,461 @@
1
+ import { MAX_DEBUG_LINE_BYTES } from "../lib/debug-log-buffer";
2
+ import { THOUGHT_SIGNATURE_BYPASS } from "./google-antigravity-replay";
3
+ import type { AntigravitySessionAnchor } from "./google-antigravity-wire";
4
+ import { isGoogleThinkingConfigErrorText, isGoogleToolSchemaErrorText } from "./google-wire-compiler";
5
+
6
+ /**
7
+ * Content-free structural projection of a compiled Google-family wire request (#5008).
8
+ *
9
+ * Why this exists. The report behind #5008 is an intermittent HTTP 400 on the FIRST upstream
10
+ * send of a long, tool-heavy Antigravity session: "Please ensure that function call turn comes
11
+ * immediately after a user turn or after a function response turn." The reporter's own static
12
+ * matrix (A-F on the issue) exercised every adjacency hazard the translator can produce and all
13
+ * of them returned 200, so the failing wire cannot be reconstructed from the repro shapes. What
14
+ * is missing is a description of the request that actually failed, and the only description a
15
+ * user can attach to a public bug report is one that carries no conversation in it.
16
+ *
17
+ * This module is that description. It reads the request object AFTER compileGoogleWireBody and
18
+ * after Antigravity replay/signature adjustment — the exact object the envelope carries — and
19
+ * returns counts, positions, kinds and booleans.
20
+ *
21
+ * What it never retains, by construction rather than by redaction: prompt or system text, tool
22
+ * arguments, tool results, tool or function names, original or wire tool-call ids, thought
23
+ * signature text or any hash of it, inline file bytes, project or account identifiers, the
24
+ * request id, the Cloud Code Assist session id, the Codex thread or session ids, and the first
25
+ * user message. Tool-call identity survives only as a request-internal ordinal assigned in
26
+ * first-appearance order, which is enough to show pairing structure and is meaningless outside
27
+ * the single request that produced it.
28
+ *
29
+ * Totals are computed over the whole request; per-turn detail is cut at
30
+ * GOOGLE_WIRE_SHAPE_TURN_CEILING and the summary is marked truncated. A 440-message session
31
+ * therefore still reports its true turn and call counts.
32
+ *
33
+ * This is a projection, not a validator. It does not decide whether a request is acceptable to
34
+ * the upstream, and nothing in the request path consults its output.
35
+ *
36
+ * What it costs. With provider debug off the projection never runs: the call site passes a
37
+ * builder to debugProviderDiagnosticLazy, which returns before invoking it. With provider debug
38
+ * ON it runs synchronously on the dispatch path, before the request is sent, and it walks every
39
+ * turn, every part and every call id, then serializes, redacts and writes one line. The cost is
40
+ * linear in history length, which is largest for exactly the long sessions this exists to
41
+ * describe. That is the price of observing the real outbound body rather than a reconstruction,
42
+ * and an operator who turns provider debug on for a 440-message session is paying it on every
43
+ * turn.
44
+ */
45
+
46
+ /** Per-turn detail beyond this many turns is dropped; totals are unaffected. */
47
+ export const GOOGLE_WIRE_SHAPE_TURN_CEILING = 64;
48
+
49
+ /** Per-turn ordinal lists are cut here; the turn's own counts are unaffected. */
50
+ export const GOOGLE_WIRE_SHAPE_ORDINAL_CEILING = 16;
51
+
52
+ /** Longest error text inspected by classifyGoogleWireUpstreamError. */
53
+ const UPSTREAM_ERROR_SCAN_LIMIT = 2048;
54
+
55
+ /**
56
+ * Serialized ceiling for one summary, held at half the debug buffer's per-line cap so the event
57
+ * prefix and any redaction copy still fit under it.
58
+ *
59
+ * The item ceilings above are not sufficient on their own: 64 retained turns carrying 16 ordinals
60
+ * each serialize past the buffer's limit, and the buffer truncates at a byte boundary. That
61
+ * leaves a consumer with JSON it cannot parse whose retained prefix still says truncated is
62
+ * false — a cut that hides itself. Trimming here instead keeps the line parseable and keeps the
63
+ * flag honest.
64
+ */
65
+ export const GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES = Math.floor(MAX_DEBUG_LINE_BYTES / 2);
66
+
67
+ export type GoogleWireTurnRole = "user" | "model" | "other";
68
+
69
+ export type GoogleWirePartKind =
70
+ | "text"
71
+ | "thought"
72
+ | "functionCall"
73
+ | "functionResponse"
74
+ | "inlineData"
75
+ | "other";
76
+
77
+ /**
78
+ * Ordering classes transcribed from the upstream error text, not inferred from it.
79
+ *
80
+ * The message names one relationship — a function-call turn must follow a user turn or a
81
+ * function-response turn — so call-turn-opens-request and call-turn-after-model-turn are the two
82
+ * ways a well-formed request can contradict it, call-turn-after-unknown-turn covers a predecessor
83
+ * that is neither, and response-turn-without-call-turn is its converse.
84
+ * request-ends-with-model-turn is a different upstream 400 that messagesToGeminiFormat already
85
+ * guards with its "(continue)" nudge; it is projected so a defeated guard is visible rather than
86
+ * silent.
87
+ *
88
+ * A flag is not a verdict. The reporter's case A produces call-turn-after-model-turn and
89
+ * returned 200, which is itself worth being able to observe in a real failing request.
90
+ */
91
+ export type GoogleWireOrderingViolation =
92
+ | "call-turn-opens-request"
93
+ | "call-turn-after-model-turn"
94
+ | "call-turn-after-unknown-turn"
95
+ | "response-turn-without-call-turn"
96
+ | "request-ends-with-model-turn";
97
+
98
+ /** Bounded classes for an upstream rejection. The error text itself is never retained. */
99
+ export type GoogleWireUpstreamErrorClass =
100
+ | "turn-adjacency"
101
+ | "thought-signature"
102
+ | "tool-schema"
103
+ | "thinking-config"
104
+ | "other";
105
+
106
+ export interface GoogleWireTurnShape {
107
+ /** Position in contents. */
108
+ index: number;
109
+ role: GoogleWireTurnRole;
110
+ /** Number of parts in this turn. */
111
+ parts: number;
112
+ /** Distinct part kinds, in first-appearance order. */
113
+ kinds: GoogleWirePartKind[];
114
+ /** Request-internal ordinals of the function calls in this turn. */
115
+ calls: number[];
116
+ /** Request-internal ordinals of the function responses in this turn. */
117
+ responses: number[];
118
+ /** Function calls carrying any thought signature. */
119
+ signedCalls: number;
120
+ /** Of those, the ones carrying only the validator-bypass sentinel. */
121
+ sentinelCalls: number;
122
+ violation?: GoogleWireOrderingViolation;
123
+ }
124
+
125
+ /** Facts the caller knows and the wire body does not carry. */
126
+ export interface GoogleWireShapeFacts {
127
+ /** Which anchor class produced the Antigravity session id. Never the id itself. */
128
+ sessionAnchor?: AntigravitySessionAnchor;
129
+ /**
130
+ * Function calls the translator signed from client history or the durable replay store,
131
+ * counted before the Antigravity session cache runs.
132
+ */
133
+ historySignedCalls?: number;
134
+ /** A durable replay scope was attached to this request. Its completeness is NOT asserted. */
135
+ replayScopeBound?: boolean;
136
+ /** The real physical send this body belongs to, 1 for the first send. */
137
+ sendOrdinal?: number;
138
+ /** Bounded class of the upstream rejection, when this body was rejected. */
139
+ errorClass?: GoogleWireUpstreamErrorClass;
140
+ }
141
+
142
+ export type GoogleWireShapeSummary = {
143
+ version: 1;
144
+ /** Per-turn detail was cut by a ceiling. Totals below remain exact. */
145
+ truncated: boolean;
146
+ turns: number;
147
+ roles: Record<GoogleWireTurnRole, number>;
148
+ functionCalls: number;
149
+ functionResponses: number;
150
+ /** Distinct tool-call ids seen anywhere in the request. */
151
+ distinctCallIds: number;
152
+ /** Function calls that carried no id at all. */
153
+ callsWithoutId: number;
154
+ /** Function responses that carried no id at all. */
155
+ responsesWithoutId: number;
156
+ /** Call ordinals no response answers. */
157
+ unansweredCalls: number;
158
+ /** Response ordinals no call introduced. */
159
+ unmatchedResponses: number;
160
+ toolDeclarations: number;
161
+ hasSystemInstruction: boolean;
162
+ hasSessionId: boolean;
163
+ sessionAnchor: AntigravitySessionAnchor | "unknown";
164
+ signature: {
165
+ /** At least one function call reaches the wire with a signature. */
166
+ present: boolean;
167
+ /** Every signed call carries only the validator-bypass sentinel. */
168
+ sentinelOnly: boolean;
169
+ signedCalls: number;
170
+ sentinelCalls: number;
171
+ /** Signed at translation time, from client history or the durable replay store. */
172
+ historySignedCalls: number;
173
+ /**
174
+ * The remainder the Antigravity session cache supplied, clamped at zero. The cache never
175
+ * overwrites an existing signature, so the three counts partition the signed calls.
176
+ */
177
+ sessionCacheSignedCalls: number;
178
+ /**
179
+ * A durable replay scope was attached. Whether it was COMPLETE enough to key a lookup is
180
+ * decided inside lookupReplayThoughtSignature and is deliberately not claimed here.
181
+ */
182
+ replayScopeBound: boolean;
183
+ };
184
+ orderingViolations: number;
185
+ firstOrderingViolation: { index: number; kind: GoogleWireOrderingViolation } | null;
186
+ /** Present only when the caller supplied send facts. */
187
+ send: { ordinal: number; errorClass: GoogleWireUpstreamErrorClass | null } | null;
188
+ turnShapes: GoogleWireTurnShape[];
189
+ };
190
+
191
+ function isRecord(value: unknown): value is Record<string, unknown> {
192
+ return !!value && typeof value === "object" && !Array.isArray(value);
193
+ }
194
+
195
+ function turnRole(content: Record<string, unknown>): GoogleWireTurnRole {
196
+ const role = content.role;
197
+ return role === "user" || role === "model" ? role : "other";
198
+ }
199
+
200
+ function partKind(part: Record<string, unknown>): GoogleWirePartKind {
201
+ if (part.thought === true) return "thought";
202
+ if (isRecord(part.functionCall)) return "functionCall";
203
+ if (isRecord(part.functionResponse)) return "functionResponse";
204
+ if (isRecord(part.inline_data) || isRecord(part.inlineData)) return "inlineData";
205
+ if (typeof part.text === "string") return "text";
206
+ return "other";
207
+ }
208
+
209
+ /**
210
+ * A function call or response id, read only to be replaced by an ordinal. The value never
211
+ * leaves this function.
212
+ */
213
+ function callIdOf(call: Record<string, unknown>): string | undefined {
214
+ const id = call.id;
215
+ return typeof id === "string" && id.length > 0 ? id : undefined;
216
+ }
217
+
218
+ function signatureOf(part: Record<string, unknown>): string | undefined {
219
+ const camel = part.thoughtSignature;
220
+ if (typeof camel === "string" && camel.length > 0) return camel;
221
+ const snake = part.thought_signature;
222
+ if (typeof snake === "string" && snake.length > 0) return snake;
223
+ return undefined;
224
+ }
225
+
226
+ /** Request-internal renumbering: first appearance wins, and the mapping dies with the call. */
227
+ function ordinalAllocator(): (rawId: string) => number {
228
+ const ordinals = new Map<string, number>();
229
+ return rawId => {
230
+ const existing = ordinals.get(rawId);
231
+ if (existing !== undefined) return existing;
232
+ const next = ordinals.size + 1;
233
+ ordinals.set(rawId, next);
234
+ return next;
235
+ };
236
+ }
237
+
238
+ function countToolDeclarations(tools: unknown): number {
239
+ if (!Array.isArray(tools)) return 0;
240
+ let total = 0;
241
+ for (const tool of tools) {
242
+ if (!isRecord(tool) || !Array.isArray(tool.functionDeclarations)) continue;
243
+ total += tool.functionDeclarations.length;
244
+ }
245
+ return total;
246
+ }
247
+
248
+ interface TurnFacts {
249
+ role: GoogleWireTurnRole;
250
+ hasFunctionCall: boolean;
251
+ hasFunctionResponse: boolean;
252
+ }
253
+
254
+ /**
255
+ * The first ordering class this turn contradicts, judged against the turn before it.
256
+ *
257
+ * "Function response turn" is read as a user turn carrying at least one functionResponse, which
258
+ * is the shape messagesToGeminiFormat emits for a tool-result batch. A plain user turn satisfies
259
+ * the rule on its own, so a call turn after either one is unflagged.
260
+ */
261
+ function violationFor(turn: TurnFacts, previous: TurnFacts | undefined, isLast: boolean): GoogleWireOrderingViolation | undefined {
262
+ if (turn.role === "model" && turn.hasFunctionCall) {
263
+ if (!previous) return "call-turn-opens-request";
264
+ if (previous.role === "model") return "call-turn-after-model-turn";
265
+ if (previous.role !== "user") return "call-turn-after-unknown-turn";
266
+ }
267
+ if (turn.role === "user" && turn.hasFunctionResponse) {
268
+ if (!previous || previous.role !== "model" || !previous.hasFunctionCall) {
269
+ return "response-turn-without-call-turn";
270
+ }
271
+ }
272
+ if (isLast && turn.role === "model") return "request-ends-with-model-turn";
273
+ return undefined;
274
+ }
275
+
276
+ function serializedBytes(summary: GoogleWireShapeSummary): number {
277
+ return new TextEncoder().encode(JSON.stringify(summary)).length;
278
+ }
279
+
280
+ /**
281
+ * Drop per-turn detail from the tail until the summary serializes under its byte budget.
282
+ *
283
+ * Trimming from the tail rather than the head is deliberate: the opening turns of a request are
284
+ * where a first-send ordering violation lives, and they are what a reader needs most.
285
+ */
286
+ function fitToSerializedBudget(summary: GoogleWireShapeSummary): GoogleWireShapeSummary {
287
+ if (serializedBytes(summary) <= GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES) return summary;
288
+ let kept = summary.turnShapes.length;
289
+ let fitted: GoogleWireShapeSummary = { ...summary, truncated: true, turnShapes: summary.turnShapes };
290
+ while (kept > 0 && serializedBytes(fitted) > GOOGLE_WIRE_SHAPE_MAX_SERIALIZED_BYTES) {
291
+ kept -= 1;
292
+ fitted = { ...summary, truncated: true, turnShapes: summary.turnShapes.slice(0, kept) };
293
+ }
294
+ return fitted;
295
+ }
296
+
297
+ /**
298
+ * Project the structure of a compiled Google wire request body.
299
+ *
300
+ * Pure and non-mutating: every field is read, nothing is written back, and the returned object
301
+ * shares no reference with the input.
302
+ */
303
+ export function summarizeGoogleWireShape(
304
+ body: unknown,
305
+ facts: GoogleWireShapeFacts = {},
306
+ ): GoogleWireShapeSummary {
307
+ const root = isRecord(body) ? body : {};
308
+ const contents: unknown[] = Array.isArray(root.contents) ? root.contents : [];
309
+ const ordinalFor = ordinalAllocator();
310
+ const historySignedCalls = Math.max(0, facts.historySignedCalls ?? 0);
311
+
312
+ const roles: Record<GoogleWireTurnRole, number> = { user: 0, model: 0, other: 0 };
313
+ const callOrdinals = new Set<number>();
314
+ const responseOrdinals = new Set<number>();
315
+ const turnShapes: GoogleWireTurnShape[] = [];
316
+
317
+ let functionCalls = 0;
318
+ let functionResponses = 0;
319
+ let callsWithoutId = 0;
320
+ let responsesWithoutId = 0;
321
+ let signedCalls = 0;
322
+ let sentinelCalls = 0;
323
+ let orderingViolations = 0;
324
+ let truncated = false;
325
+ let firstOrderingViolation: GoogleWireShapeSummary["firstOrderingViolation"] = null;
326
+ let previous: TurnFacts | undefined;
327
+
328
+ for (let index = 0; index < contents.length; index++) {
329
+ const rawContent: unknown = contents[index];
330
+ const content = isRecord(rawContent) ? rawContent : {};
331
+ const role = turnRole(content);
332
+ roles[role] += 1;
333
+ const parts: unknown[] = Array.isArray(content.parts) ? content.parts : [];
334
+ const kinds: GoogleWirePartKind[] = [];
335
+ const calls: number[] = [];
336
+ const responses: number[] = [];
337
+ let turnSignedCalls = 0;
338
+ let turnSentinelCalls = 0;
339
+
340
+ for (const rawPart of parts) {
341
+ const part = isRecord(rawPart) ? rawPart : {};
342
+ const kind = partKind(part);
343
+ if (!kinds.includes(kind)) kinds.push(kind);
344
+ if (kind === "functionCall") {
345
+ functionCalls += 1;
346
+ const id = callIdOf(part.functionCall as Record<string, unknown>);
347
+ if (id === undefined) {
348
+ callsWithoutId += 1;
349
+ } else {
350
+ const ordinal = ordinalFor(id);
351
+ callOrdinals.add(ordinal);
352
+ if (calls.length < GOOGLE_WIRE_SHAPE_ORDINAL_CEILING) calls.push(ordinal);
353
+ else truncated = true;
354
+ }
355
+ const signature = signatureOf(part);
356
+ if (signature !== undefined) {
357
+ turnSignedCalls += 1;
358
+ if (signature === THOUGHT_SIGNATURE_BYPASS) turnSentinelCalls += 1;
359
+ }
360
+ } else if (kind === "functionResponse") {
361
+ functionResponses += 1;
362
+ const id = callIdOf(part.functionResponse as Record<string, unknown>);
363
+ if (id === undefined) {
364
+ responsesWithoutId += 1;
365
+ } else {
366
+ const ordinal = ordinalFor(id);
367
+ responseOrdinals.add(ordinal);
368
+ if (responses.length < GOOGLE_WIRE_SHAPE_ORDINAL_CEILING) responses.push(ordinal);
369
+ else truncated = true;
370
+ }
371
+ }
372
+ }
373
+
374
+ signedCalls += turnSignedCalls;
375
+ sentinelCalls += turnSentinelCalls;
376
+ const current: TurnFacts = {
377
+ role,
378
+ hasFunctionCall: kinds.includes("functionCall"),
379
+ hasFunctionResponse: kinds.includes("functionResponse"),
380
+ };
381
+ const violation = violationFor(current, previous, index === contents.length - 1);
382
+ if (violation) {
383
+ orderingViolations += 1;
384
+ if (!firstOrderingViolation) firstOrderingViolation = { index, kind: violation };
385
+ }
386
+ if (turnShapes.length < GOOGLE_WIRE_SHAPE_TURN_CEILING) {
387
+ turnShapes.push({
388
+ index,
389
+ role,
390
+ parts: parts.length,
391
+ kinds,
392
+ calls,
393
+ responses,
394
+ signedCalls: turnSignedCalls,
395
+ sentinelCalls: turnSentinelCalls,
396
+ ...(violation ? { violation } : {}),
397
+ });
398
+ } else {
399
+ truncated = true;
400
+ }
401
+ previous = current;
402
+ }
403
+
404
+ let unansweredCalls = 0;
405
+ for (const ordinal of callOrdinals) if (!responseOrdinals.has(ordinal)) unansweredCalls += 1;
406
+ let unmatchedResponses = 0;
407
+ for (const ordinal of responseOrdinals) if (!callOrdinals.has(ordinal)) unmatchedResponses += 1;
408
+
409
+ return fitToSerializedBudget({
410
+ version: 1,
411
+ truncated,
412
+ turns: contents.length,
413
+ roles,
414
+ functionCalls,
415
+ functionResponses,
416
+ distinctCallIds: new Set([...callOrdinals, ...responseOrdinals]).size,
417
+ callsWithoutId,
418
+ responsesWithoutId,
419
+ unansweredCalls,
420
+ unmatchedResponses,
421
+ toolDeclarations: countToolDeclarations(root.tools),
422
+ hasSystemInstruction: isRecord(root.systemInstruction),
423
+ hasSessionId: typeof root.sessionId === "string" && root.sessionId.length > 0,
424
+ sessionAnchor: facts.sessionAnchor ?? "unknown",
425
+ signature: {
426
+ present: signedCalls > 0,
427
+ sentinelOnly: signedCalls > 0 && signedCalls === sentinelCalls,
428
+ signedCalls,
429
+ sentinelCalls,
430
+ historySignedCalls,
431
+ sessionCacheSignedCalls: Math.max(0, signedCalls - sentinelCalls - historySignedCalls),
432
+ replayScopeBound: facts.replayScopeBound === true,
433
+ },
434
+ orderingViolations,
435
+ firstOrderingViolation,
436
+ send: facts.sendOrdinal === undefined
437
+ ? null
438
+ : { ordinal: facts.sendOrdinal, errorClass: facts.errorClass ?? null },
439
+ turnShapes,
440
+ });
441
+ }
442
+
443
+ /**
444
+ * Classify an upstream rejection into one of a fixed set of classes.
445
+ *
446
+ * Bounded in both directions: at most UPSTREAM_ERROR_SCAN_LIMIT characters are inspected, and
447
+ * only the class is returned, so no upstream text reaches a caller through this function.
448
+ */
449
+ export function classifyGoogleWireUpstreamError(message: unknown): GoogleWireUpstreamErrorClass {
450
+ if (typeof message !== "string" || message.length === 0) return "other";
451
+ const scanned = message.slice(0, UPSTREAM_ERROR_SCAN_LIMIT);
452
+ if (/function\s+call\s+turn|function\s+response\s+turn|model\s+turn\s+are\s+not\s+supported|assistant\s+message\s+prefill/i.test(scanned)) {
453
+ return "turn-adjacency";
454
+ }
455
+ if (/thought[_\s-]?signature|TYPE_BYTES/i.test(scanned)) return "thought-signature";
456
+ // Shared with the wire compiler's repair path so a payload cannot be repaired as one class
457
+ // and described as another.
458
+ if (isGoogleToolSchemaErrorText(scanned)) return "tool-schema";
459
+ if (isGoogleThinkingConfigErrorText(scanned)) return "thinking-config";
460
+ return "other";
461
+ }