@wenathlan/devthink 2.0.13 → 2.0.15

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 (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/request.ts ADDED
@@ -0,0 +1,4482 @@
1
+ /**
2
+ * @file request.ts
3
+ * @module provider/request
4
+ * @description
5
+ * THE single request-building module of the provider family of the merged library. v2.1.14
6
+ * consolidation: request.ts absorbed request-helpers.ts in full, so the
7
+ * entire request-building category — schema cleaning, tool validation,
8
+ * thinking budgets, fingerprint headers, session/prompt id builders, model
9
+ * classification, request transforms/builders and cascade fetch — lives in
10
+ * this one file.
11
+ *
12
+ * Third-person observer view: the library watches how OpenCode transforms
13
+ * user messages (OpenAI and Anthropic shapes) into Gemini/Claude payloads
14
+ * and how Antigravity Manager forwards them to cloudcode-pa.googleapis.com
15
+ * without using localhost v1 proxy.
16
+ *
17
+ * Responsibilities:
18
+ * - transformRequest: OpenCode -> Gemini normalized request (contents, system, tools, genConfig)
19
+ * - buildAntigravityRequest / buildGeminiCLIRequest: wraps normalized request into
20
+ * cloudcode-pa envelope with deterministic session_id / user_prompt_id (FNV-1a),
21
+ * endpoint cascade, fingerprint headers, and x-goog-user-project stripping
22
+ * - schema cleaning: tolerant Gemini-shaping cleanJsonSchema plus the strict
23
+ * allowlist cleanJsonSchemaStrict / cleanToolSchemaTolerant family
24
+ * - tool name validation, sanitization and deduplication (two parallel
25
+ * families: validateToolNames and validateToolNamesDetailed)
26
+ * - message normalization: normalizeMessages and normalizeIncomingMessages
27
+ * - thinking level parsing, budget maps, family-aware budgets and variant hints
28
+ * - deterministic id builders over FNV-1a 32/64-bit digests
29
+ * - model classification (isGeminiCLIOnlyModel, isLikelyAntigravityOnlyModel,
30
+ * variant extraction) and the Gemini CLI bypass constants
31
+ * - google_search injection when enabled; cascade fetch with retry
32
+ *
33
+ * Architecture governance:
34
+ * - Date: 25/08/2026 — Canto do Buriti, Piauí, Brasil
35
+ * - Library-first, root-first: file lives beside its siblings (no src/ nesting)
36
+ * - Only node:* builtins + global fetch (Node >=18) — zero external deps
37
+ * - Direct cloudcode-pa usage: https://cloudcode-pa.googleapis.com,
38
+ * https://daily-cloudcode-pa.googleapis.com,
39
+ * https://daily-cloudcode-pa.sandbox.googleapis.com
40
+ * - No localhost v1 base-url proxy (requisito explícito)
41
+ * - Deterministic IDs via FNV-1a (32/64-bit) — reproducible per conversation
42
+ * - Strip x-goog-user-project header to avoid 403 IAM (issue pi-mono #1830)
43
+ * - Skip sandbox for gemini-cli models to avoid 404/403 cascade (#233)
44
+ *
45
+ * v2.1.14 merge notes (request.ts + request-helpers.ts -> request.ts):
46
+ * - Identical duplicated symbols collapsed to a single definition.
47
+ * - Divergent duplicates resolved toward the more defensive variant; the
48
+ * losing variant survives under a parallel export whenever its behavior
49
+ * was not equivalent:
50
+ * cleanJsonSchemaStrict <- request-helpers cleanJsonSchema (strict allowlist)
51
+ * cleanToolSchemaTolerant <- request-helpers cleanToolSchema (tolerant normalizer)
52
+ * validateToolNamesDetailed <- request-helpers validateToolNames (rich validator)
53
+ * buildGenerationConfigFromRequest <- request.ts buildGenerationConfig (request-shaped)
54
+ * - Canonical fnv1a32 hashes UTF-16 code units (charCodeAt), matching the
55
+ * repo-wide implementation in core.ts; the UTF-8 byte variant that lived
56
+ * only in request-helpers.ts was dropped as the outlier.
57
+ * - Canonical TOOL_NAME_REGEX is the letter-start + 64-cap pattern shared
58
+ * with core.ts (TOOL_REGEX); the underscore-start variant was dropped.
59
+ * - Canonical generateSessionId/generateUserPromptId are the validating
60
+ * versions (throw RequestHelpersError on empty seed).
61
+ * - The RequestHelpers barrel is re-exported from this module; the default
62
+ * export remains RequestPipeline.
63
+ *
64
+ * @author devthink
65
+ * @license MIT
66
+ * @version 2.1.16
67
+ */
68
+
69
+ import * as os from "node:os";
70
+ const osPlatform = os.platform;
71
+ const osArch = os.arch;
72
+ import { randomBytes, randomInt } from "node:crypto";
73
+ // v2.1.15 Phase B: raw shared values (identity UA, client metadata family,
74
+ // project fallback, forbidden header lists, FNV constants, cloudcode bases,
75
+ // antigravity version fallback) come from constants.js; generic utilities
76
+ // (fnv hash family, retry predicate) from core.js; identity masquerade
77
+ // (gl-node api-client snapshot, client-metadata builder) from fingerprint.js.
78
+ import {
79
+ ANTIGRAVITY_VERSION_FALLBACK,
80
+ CLIENT_METADATA,
81
+ CLIENT_METADATA_RAW,
82
+ CLIENT_METADATA_STRING,
83
+ CLOUDCODE_DAILY_BASE,
84
+ CLOUDCODE_PA_BASE,
85
+ FALLBACK_PROJECT_ID,
86
+ FORBIDDEN_HEADERS,
87
+ FNV_OFFSET_BASIS,
88
+ FNV_PRIME,
89
+ GEMINI_CLI_OAUTH_CLIENT_ID,
90
+ GEMINI_CLI_OAUTH_CLIENT_SECRET,
91
+ CODE_ASSIST_BASE_URL,
92
+ CODE_ASSIST_PATH_MAP,
93
+ CONTENT_TIMEOUT_MS,
94
+ GEMINI_CLI_USER_AGENT,
95
+ HEADERS_TO_STRIP,
96
+ PLUGIN_VERSION,
97
+ PROJECT_FALLBACK,
98
+ } from "./constants.js";
99
+ import { fnv1a32, fnv1a32Hex, getJitter, isRetryableStatus } from "./core.js";
100
+ const MODELS_2026_SET = new Set<string>(FETCH_AVAILABLE_MODELS_2026 as readonly string[]);
101
+ export const TOOL_NAME_MAX_LENGTH = 64 as const;
102
+
103
+ export const TOOL_NAME_MIN_LENGTH = 1 as const;
104
+
105
+ export const THINKING_LEVEL_TO_BUDGET_GEMINI: Record<string, number> = {
106
+ none: 0,
107
+ minimal: 0, // 0 desativa thinking em Gemini nativo
108
+ low: 1024,
109
+ medium: 4096,
110
+ high: 8192,
111
+ ultra: 16384,
112
+ };
113
+
114
+ export const THINKING_LEVEL_TO_BUDGET_CLAUDE: Record<string, number> = {
115
+ none: 0,
116
+ minimal: 1024,
117
+ low: 2048,
118
+ medium: 8192,
119
+ high: 16384,
120
+ ultra: 32768,
121
+ };
122
+
123
+ /**
124
+ * Thinking budget ceiling shared by the clamp paths. v2.1.16: the value
125
+ * (64000) is byte-identical to models.ts THINKING_BUDGET_MAX_CLAUDE — the
126
+ * thinking-table owner — so this export now aliases that import instead of
127
+ * repeating the literal.
128
+ */
129
+ export const THINKING_BUDGET_MAX = THINKING_BUDGET_MAX_CLAUDE;
130
+
131
+ // candidate for models.js ownership (THINKING_BUDGET_MIN): models.ts exports
132
+ // no minimum-thinking-budget constant yet, so the 1024 floor stays local.
133
+ export const THINKING_BUDGET_MIN = 1024 as const;
134
+
135
+ // v2.1.15 Phase C: historical alias names for the gemini-cli client pair.
136
+ export { GEMINI_CLI_OAUTH_CLIENT_ID as GEMINI_CLI_CLIENT_ID } from "./constants.js";
137
+ export { GEMINI_CLI_OAUTH_CLIENT_SECRET as GEMINI_CLI_CLIENT_SECRET } from "./constants.js";
138
+ import {
139
+ buildClientMetadata,
140
+ getDynamicUserAgent,
141
+ getFingerprintHeaders,
142
+ normalizeArch,
143
+ normalizePlatform,
144
+ X_GOOG_API_CLIENT_GEMINI_CLI as X_GOOG_API_CLIENT,
145
+ } from "./fingerprint.js";
146
+ // v2.1.15 Phase A: model-identification families (the observed 2026-08-25
147
+ // fetchAvailableModels list, the Gemini-CLI-only routing list, search model
148
+ // ids, thinking tables and the endpoint routing orders/classifiers) are
149
+ // imported from models.ts — THE single owner. The historical local names are
150
+ // re-exported below so this module's public surface stays unchanged.
151
+ import {
152
+ CODE_ASSIST_ENDPOINTS,
153
+ ENDPOINT_ORDER_ANTIGRAVITY,
154
+ ENDPOINT_ORDER_GEMINI_CLI,
155
+ FETCH_AVAILABLE_MODELS_2026,
156
+ GEMINI_CLI_ONLY_MODELS,
157
+ SEARCH_MODEL,
158
+ SEARCH_MODEL_PREVIEW,
159
+ THINKING_BUDGET_MAP,
160
+ THINKING_BUDGET_MAX_CLAUDE,
161
+ THINKING_BUDGET_MAX_GEMINI,
162
+ THINKING_LEVELS,
163
+ getEndpointsForModel,
164
+ isGeminiCLIOnlyModel,
165
+ } from "./models.js";
166
+ import type { FetchAvailableModel2026, ThinkingLevel } from "./models.js";
167
+
168
+ export {
169
+ ENDPOINT_ORDER_ANTIGRAVITY,
170
+ ENDPOINT_ORDER_GEMINI_CLI,
171
+ GEMINI_CLI_ONLY_MODELS,
172
+ SEARCH_MODEL,
173
+ SEARCH_MODEL_PREVIEW,
174
+ THINKING_BUDGET_MAP,
175
+ THINKING_BUDGET_MAX_CLAUDE,
176
+ THINKING_BUDGET_MAX_GEMINI,
177
+ THINKING_LEVELS,
178
+ getEndpointsForModel,
179
+ isGeminiCLIOnlyModel,
180
+ } from "./models.js";
181
+ export type { ThinkingLevel } from "./models.js";
182
+ /** Historical request-module alias of the models.ts owner list. */
183
+ export { FETCH_AVAILABLE_MODELS_2026 as MODELS_2026 } from "./models.js";
184
+ // v2.1.15 Phase A: the ModelId2026 alias re-export was deleted — the name
185
+ // is owned by models.ts (import it from there when needed).
186
+
187
+ /**
188
+ * Linear-time trailing-slash strip (regex-free; safe on uncontrolled input —
189
+ * the former /\/+$/.replace was polynomial and flagged as ReDoS-prone).
190
+ */
191
+ function stripTrailingSlashes(value: string): string {
192
+ let end = value.length;
193
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
194
+ return end === value.length ? value : value.slice(0, end);
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Private utilities merged from request-helpers.ts
199
+ // ---------------------------------------------------------------------------
200
+
201
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
202
+ return (
203
+ typeof v === "object" && v !== null && !Array.isArray(v) && Object.prototype.toString.call(v) === "[object Object]"
204
+ );
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Constants — endpoint cascade, models 2026, budgets, headers
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Official cloudcode-pa endpoints — prod is authoritative in 2026,
213
+ * daily and sandbox are rollout / canary fallbacks.
214
+ * Third-person: observer copies order from Antigravity Manager binary.
215
+ */
216
+ // v2.1.15 Phase D: the host map ENDPOINTS lives in constants.js (raw value owner).
217
+
218
+ /** Default cascade order for Antigravity pool (re-exported from models.ts above). */
219
+
220
+ /** Stream endpoint suffix used by Antigravity */
221
+
222
+ /** Antigravity version fallback — governance requires 1.19.2 (bypass ban <1.15.8) */
223
+
224
+ /** Fallback project id */
225
+
226
+ // v2.1.15 Phase A: MODELS_2026 (the 22-id observed fetchAvailableModels
227
+ // list), GEMINI_CLI_ONLY_MODELS, SEARCH_MODEL, SEARCH_MODEL_PREVIEW,
228
+ // THINKING_LEVELS/ThinkingLevel and THINKING_BUDGET_MAP moved to models.ts
229
+ // and are imported + re-exported at the top of this file.
230
+
231
+ /** FNV-1a constants — 32-bit */
232
+
233
+ /** Forbidden headers stripped case-insensitively — observer notes AM never sends them on content */
234
+
235
+ /**
236
+ * OpenCode function tool — accepts OpenAI, Anthropic, or Gemini pre-shaped.
237
+ */
238
+ export type OpenCodeTool =
239
+ | { type: "function"; function: { name: string; description?: string; parameters?: any } }
240
+ | { name: string; description?: string; input_schema?: any; inputSchema?: any; parameters?: any; [k: string]: any }
241
+ | { functionDeclarations: Array<{ name: string; description?: string; parameters?: any }> };
242
+
243
+ export interface TransformRequestInput {
244
+ /** Model id, e.g. gemini-3-flash-preview or claude-opus-4-6-thinking-low */
245
+ model: string;
246
+ /** Message history in OpenCode format */
247
+ messages: OpenCodeMessage[];
248
+ /** Optional tools — OpenAI / Anthropic / Gemini shapes tolerated */
249
+ tools?: OpenCodeTool[] | any[];
250
+ /** Optional system instruction as string or array of texts */
251
+ system?: string | string[] | null;
252
+ /** Optional thinking level minimal|low|medium|high|max — maps to budget */
253
+ thinkingLevel?: ThinkingLevel | string;
254
+ /** Optional explicit thinking budget — overrides level map */
255
+ thinkingBudget?: number;
256
+ /** Whether to inject google_search grounding tool */
257
+ googleSearchEnabled?: boolean;
258
+ /** Optional generationConfig passthrough (temperature, maxTokens, etc) */
259
+ generationConfig?: Record<string, any>;
260
+ /** Optional seed for deterministic session_id — defaults to hash of messages+model */
261
+ sessionSeed?: string;
262
+ /** Optional seed for user_prompt_id — defaults to last user message */
263
+ userPromptSeed?: string;
264
+ /** Optional projectId — not used in transformRequest but kept for symmetry */
265
+ projectId?: string;
266
+ }
267
+
268
+ /**
269
+ * Gemini content part.
270
+ * v2.1.14 merge: unions the request.ts declaration (thought/thoughtSignature)
271
+ * with the request-helpers.ts declaration (fileData plus the explicit
272
+ * GeminiFunctionCall/GeminiFunctionResponse sub-shapes).
273
+ */
274
+ export interface GeminiPart {
275
+ text?: string;
276
+ thought?: boolean;
277
+ thoughtSignature?: string;
278
+ inlineData?: { mimeType: string; data: string };
279
+ fileData?: { mimeType: string; fileUri: string };
280
+ functionCall?: GeminiFunctionCall;
281
+ functionResponse?: GeminiFunctionResponse;
282
+ [k: string]: unknown;
283
+ }
284
+
285
+ /**
286
+ * Gemini content turn.
287
+ * v2.1.14 merge: keeps the "system" role from request.ts alongside the
288
+ * user/model pair used by both halves.
289
+ */
290
+ export interface OpenCodeMessage {
291
+ role: "system" | "user" | "assistant" | "tool" | "model" | string;
292
+ content?: string | any[] | null;
293
+ tool_calls?: Array<{ id: string; type?: string; function: { name: string; arguments: string | object } }>;
294
+ tool_call_id?: string;
295
+ name?: string;
296
+ // Anthropic extras
297
+ thinking?: string;
298
+ signature?: string;
299
+ [k: string]: any;
300
+ }
301
+
302
+ export interface GeminiContent {
303
+ role: "user" | "model" | "system";
304
+ parts: GeminiPart[];
305
+ }
306
+
307
+ /** System instruction content shape (live pipeline lineage). */
308
+ export type GeminiSystem = { parts: GeminiPart[] } | undefined;
309
+
310
+ export interface GeminiTool {
311
+ functionDeclarations?: Array<{ name: string; description?: string; parameters?: any }>;
312
+ }
313
+
314
+ export interface TransformedRequest {
315
+ /** Normalized model id stripped of antigravity- prefix for routing but original preserved */
316
+ model: string;
317
+ /** Original model string exactly as user provided */
318
+ originalModel: string;
319
+ /** Gemini contents array ready for cloudcode-pa */
320
+ contents: GeminiContent[];
321
+ /** System instruction aggregated */
322
+ systemInstruction?: { role: "system"; parts: GeminiPart[] } | null;
323
+ /** Tools normalized to Gemini functionDeclarations */
324
+ tools?: GeminiTool[] | null;
325
+ /** Generation config with thinking budget mapped if provided */
326
+ generationConfig?: Record<string, any> | null;
327
+ /** Deterministic session_id via FNV-1a */
328
+ sessionId: string;
329
+ /** Deterministic user_prompt_id via FNV-1a */
330
+ userPromptId: string;
331
+ /** Thinking budget resolved */
332
+ thinkingBudget?: number;
333
+ /** Thinking level resolved */
334
+ thinkingLevel?: ThinkingLevel;
335
+ }
336
+
337
+ export const ALLOWED_SCHEMA_KEYS = new Set([
338
+ "type",
339
+ "properties",
340
+ "required",
341
+ "description",
342
+ "enum",
343
+ "items",
344
+ "anyOf",
345
+ "oneOf",
346
+ "format",
347
+ "minimum",
348
+ "maximum",
349
+ "minLength",
350
+ "maxLength",
351
+ "pattern",
352
+ "title",
353
+ "nullable",
354
+ "propertyOrdering",
355
+ ]);
356
+
357
+ /**
358
+ * Keys kept by the tolerant cleaner {@link cleanJsonSchema}: the module
359
+ * allowlist minus propertyOrdering (the cloudcode-pa tool-schema validator
360
+ * rejects that key on parameters). Derived — never re-listed — from
361
+ * {@link ALLOWED_SCHEMA_KEYS} so the two policies cannot drift apart.
362
+ */
363
+ const CLEAN_SCHEMA_KEYS: ReadonlySet<string> = new Set(
364
+ [...ALLOWED_SCHEMA_KEYS].filter((k) => k !== "propertyOrdering"),
365
+ );
366
+
367
+ export const MAX_TOOLS_PER_REQUEST = 128 as const;
368
+
369
+ /** Allowed top-level keys of a tool definition after cleaning. */
370
+ export const ALLOWED_TOOL_TOP_KEYS = ["name", "description", "parameters"] as const;
371
+
372
+ /** Direct production cloudcode-pa endpoint — bypasses any localhost proxy. */
373
+ export const DIRECT_ENDPOINT = "https://cloudcode-pa.googleapis.com" as const;
374
+ /** Daily (canary) cloudcode-pa endpoint. */
375
+ export const DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com" as const;
376
+ /** Autopush sandbox endpoint. */
377
+ export const AUTOPUSH_ENDPOINT = "https://autopush-cloudcode-pa.sandbox.googleapis.com" as const;
378
+
379
+ /** Cascade order used by the observer when retrying endpoints. */
380
+ export const ENDPOINT_CASCADE = [DAILY_ENDPOINT, AUTOPUSH_ENDPOINT, DIRECT_ENDPOINT] as const;
381
+
382
+ /**
383
+ * Headers stripped before sending to avoid 403 IAM (pi-mono #1830); Antigravity
384
+ * Manager never sends these on content requests.
385
+ */
386
+
387
+ // v2.1.16: GEMINI_CLI_ONLY_EXACT (10-id exact list) was deleted — zero
388
+ // consumers repo-wide (only the RequestHelpers bundle listed it). Routing
389
+ // consults models.isGeminiCLIOnlyModel / getEndpointsForModel instead.
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Errors
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Thrown when transformRequest cannot normalize input.
397
+ */
398
+ export class RequestTransformationError extends Error {
399
+ public readonly code: string;
400
+ constructor(message: string, code = "REQUEST_TRANSFORM_ERROR") {
401
+ super(message);
402
+ this.name = "RequestTransformationError";
403
+ this.code = code;
404
+ }
405
+ }
406
+
407
+ // ============================================================================
408
+ // 11. MERGED OBSERVER HELPERS (from request-helpers v2 — third-person module)
409
+ // ============================================================================
410
+
411
+ /**
412
+ * Domain error for the merged observer helpers. Thrown instead of generic Error
413
+ * so callers can distinguish cleaning/normalization failures from transport.
414
+ */
415
+ export class RequestHelpersError extends Error {
416
+ public readonly code: string;
417
+
418
+ constructor(message: string, code = "REQUEST_HELPERS_ERROR", cause?: unknown) {
419
+ super(message);
420
+ this.name = "RequestHelpersError";
421
+ this.code = code;
422
+ if (cause) {
423
+ (this as { cause?: unknown }).cause = cause;
424
+ }
425
+ // Maintain proper prototype chain for instanceof
426
+ Object.setPrototypeOf(this, RequestHelpersError.prototype);
427
+ }
428
+ }
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // FNV-1a — deterministic hashing for session_id / user_prompt_id
432
+ // ---------------------------------------------------------------------------
433
+
434
+ /**
435
+ * Computes FNV-1a 32-bit hash.
436
+ * Third-person: observer notes this hash is fast, no crypto deps, deterministic.
437
+ *
438
+ * @param input - string to hash
439
+ * @returns unsigned 32-bit integer
440
+ */
441
+
442
+ /**
443
+ * Generates hex string padded to 8 chars from FNV-1a.
444
+ *
445
+ * @param input - seed
446
+ * @returns 8-char hex
447
+ */
448
+
449
+ // ---------------------------------------------------------------------------
450
+ // Merged: deterministic ids over FNV-1a hex digests
451
+ // ---------------------------------------------------------------------------
452
+
453
+ /**
454
+ * Returns the FNV-1a 32-bit digest of input padded to 8 hex chars.
455
+ *
456
+ * @param input - seed string
457
+ */
458
+ export function fnv1aHex(input: string): string {
459
+ const h = fnv1a32(input);
460
+ return h.toString(16).padStart(8, "0");
461
+ }
462
+
463
+ /**
464
+ * Returns a 16-char hex digest from two independent FNV-1a rounds, used when
465
+ * longer session ids are needed without reaching for crypto randomness.
466
+ *
467
+ * @param input - seed string
468
+ */
469
+ export function fnv1aHex16(input: string): string {
470
+ const low = fnv1a32(input);
471
+ const high = fnv1a32(`__high__:${input}`);
472
+ return `${high.toString(16).padStart(8, "0")}${low.toString(16).padStart(8, "0")}`;
473
+ }
474
+
475
+ // ---------------------------------------------------------------------------
476
+ // Merged from request-helpers.ts — validating id generators (canonical).
477
+ // v2.1.14 merge: these replace request.ts's non-validating wrappers; the ids
478
+ // stay deterministic via the canonical generateDeterministicId above.
479
+ // ---------------------------------------------------------------------------
480
+
481
+ /**
482
+ * Alias kept for fingerprint.ts compatibility.
483
+ */
484
+ export const fnv1aHash = fnv1a32;
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // Model helpers — 2026 support, variant extraction, cli-only detection
488
+ // ---------------------------------------------------------------------------
489
+
490
+ /**
491
+ * Normalizes model id — trims, lowercases for detection, preserves original case for request.
492
+ * Also strips antigravity- prefix for internal detection but keeps original model field elsewhere.
493
+ *
494
+ * @param modelId - raw model string
495
+ * @returns object with base, original, normalized
496
+ */
497
+ export function parseModelId(modelId: string): {
498
+ original: string;
499
+ normalized: string;
500
+ base: string;
501
+ variantSuffixes: string[];
502
+ } {
503
+ if (!modelId || typeof modelId !== "string") {
504
+ throw new RequestTransformationError("model id must be non-empty string", "INVALID_MODEL");
505
+ }
506
+ const original = modelId.trim();
507
+ if (!original) throw new RequestTransformationError("model id empty after trim", "INVALID_MODEL");
508
+ const lower = original.toLowerCase();
509
+ // Extract antigravity- prefix if present
510
+ const withoutAntigravity = lower.startsWith("antigravity-") ? lower.slice("antigravity-".length) : lower;
511
+ // Variant suffixes like -customtools, -thinking-low, -thinking-max
512
+ const variantSuffixes: string[] = [];
513
+ let base = withoutAntigravity;
514
+
515
+ // Known suffixes to peel
516
+ const peelSuffixes = [
517
+ "-customtools",
518
+ "-search",
519
+ "-grounding",
520
+ "-thinking-max",
521
+ "-thinking-low",
522
+ "-thinking-medium",
523
+ "-thinking-high",
524
+ "-thinking-minimal",
525
+ "-thinking",
526
+ ];
527
+ // Peel iteratively from end
528
+ let peeled = true;
529
+ while (peeled) {
530
+ peeled = false;
531
+ for (const sfx of peelSuffixes) {
532
+ if (base.endsWith(sfx)) {
533
+ variantSuffixes.unshift(sfx.replace(/^-/, ""));
534
+ base = base.slice(0, -sfx.length);
535
+ peeled = true;
536
+ break;
537
+ }
538
+ }
539
+ }
540
+
541
+ return { original, normalized: withoutAntigravity, base, variantSuffixes };
542
+ }
543
+
544
+ // v2.1.15 Phase A: isGeminiCLIOnlyModel moved to models.ts (this exact
545
+ // implementation is the canonical one) — imported + re-exported at the top
546
+ // of this file.
547
+
548
+ // ============================================================================
549
+ // 03. isLikelyAntigravityOnlyModel()
550
+ // ============================================================================
551
+
552
+ const ANTIGRAVITY_ONLY_EXACT = new Set<string>([
553
+ "antigravity-gemini-3-pro",
554
+ "antigravity-gemini-3.1-pro",
555
+ "antigravity-gemini-3-flash",
556
+ "antigravity-claude-sonnet-4-6",
557
+ "antigravity-claude-opus-4-6-thinking",
558
+ "gemini-3.1-pro-preview-customtools",
559
+ "gemini-3.1-pro-preview-customtools-thinking",
560
+ "antigravity-gemini-3-pro-thinking",
561
+ "antigravity-gemini-3.1-pro-thinking",
562
+ ]);
563
+
564
+ /**
565
+ * Detecta se modelo só funciona via endpoint Antigravity (cloudcode-pa v1internal)
566
+ * e não via genai público. Heurística baseada em prefixos e sufixos.
567
+ *
568
+ * @param modelId - ID do modelo (ex: "antigravity-claude-opus-4-6-thinking")
569
+ * @returns true se provavelmente só funciona em Antigravity
570
+ *
571
+ * Regras:
572
+ * - qualquer modelo começando com antigravity- => true
573
+ * - contém "claude" => true (Claude só via bypass Antigravity)
574
+ * - contém "customtools" => true (customtools é feature Antigravity 2026)
575
+ * - está no set ANTIGRAVITY_ONLY_EXACT
576
+ * - contém "rising-fact" ou "unwritten-orb" etc? Não, esses são projects, não modelos
577
+ */
578
+ export function isLikelyAntigravityOnlyModel(modelId: string): boolean {
579
+ if (!modelId || typeof modelId !== "string") return false;
580
+ const id = modelId.trim().toLowerCase();
581
+ if (!id) return false;
582
+
583
+ if (id.startsWith("antigravity-")) return true;
584
+ if (ANTIGRAVITY_ONLY_EXACT.has(id)) return true;
585
+ if (id.includes("claude")) return true;
586
+ if (id.includes("customtools")) return true;
587
+ if (id.includes("antigravity")) return true;
588
+ // modelos 3.1 preview com sufixo custom são Antigravity-only na prática
589
+ if (id.includes("3.1") && id.includes("custom")) return true;
590
+ // modelos com thinking + antigravity family
591
+ if (id.includes("-thinking") && id.includes("gemini-3")) {
592
+ // gemini-3 pro nativo não tem thinking nativo, só via antigravity
593
+ // mas mantém simples: se for preview thinking também é antigravity-only
594
+ if (id.startsWith("antigravity-") || id.includes("preview")) {
595
+ // evita falso positivo para gemini-3-pro-preview que é nativo, então só true se tem thinking
596
+ if (id.includes("thinking")) return true;
597
+ }
598
+ }
599
+ return false;
600
+ }
601
+
602
+ // v2.1.15 Phase A: getEndpointsForModel moved to models.ts (this exact
603
+ // implementation is the canonical one) — imported + re-exported at the top
604
+ // of this file.
605
+
606
+ // ============================================================================
607
+ // 10. extractVariants()
608
+ // ============================================================================
609
+
610
+ export interface ExtractVariantsResult {
611
+ input: string;
612
+ base: string;
613
+ isAntigravity: boolean;
614
+ isThinking: boolean;
615
+ isCustomTools: boolean;
616
+ isPreview: boolean;
617
+ family: string;
618
+ variants: string[]; // todas variantes geradas
619
+ candidatesInCatalog: string[]; // filtradas que existem em MODELS_2026
620
+ relatedModels: Array<{ id: string; inCatalog: boolean }>;
621
+ }
622
+
623
+ /**
624
+ * Extrai variantes de um modelId, removendo prefixos/sufixos e gerando candidatos.
625
+ *
626
+ * Ex: "antigravity-claude-opus-4-6-thinking" =>
627
+ * base: "claude-opus-4-6"
628
+ * variants: ["antigravity-claude-opus-4-6-thinking", "antigravity-claude-opus-4-6", "claude-opus-4-6-thinking", "claude-opus-4-6", ...]
629
+ */
630
+ export function extractVariants(modelId: string): ExtractVariantsResult {
631
+ if (!modelId || typeof modelId !== "string") {
632
+ return {
633
+ input: String(modelId),
634
+ base: String(modelId),
635
+ isAntigravity: false,
636
+ isThinking: false,
637
+ isCustomTools: false,
638
+ isPreview: false,
639
+ family: String(modelId),
640
+ variants: [],
641
+ candidatesInCatalog: [],
642
+ relatedModels: [],
643
+ };
644
+ }
645
+
646
+ const original = modelId.trim();
647
+ const lower = original.toLowerCase();
648
+
649
+ const isAntigravity = lower.startsWith("antigravity-") || lower.includes("antigravity");
650
+ const isThinking = lower.includes("thinking");
651
+ const isCustomTools =
652
+ lower.includes("customtools") || lower.includes("custom_tools") || lower.includes("custom-tools");
653
+ const isPreview = lower.includes("preview");
654
+
655
+ // Remove prefixo antigravity-
656
+ let base = lower;
657
+ if (base.startsWith("antigravity-")) base = base.slice("antigravity-".length);
658
+
659
+ // Remove sufixos iterativamente: -thinking, -customtools, -preview-customtools, -preview, -custom
660
+ const suffixes = [
661
+ "-thinking",
662
+ "-customtools",
663
+ "-custom_tools",
664
+ "-custom-tools",
665
+ "-preview-customtools",
666
+ "-preview",
667
+ "-custom",
668
+ ];
669
+ let changed = true;
670
+ while (changed) {
671
+ changed = false;
672
+ for (const suf of suffixes) {
673
+ if (base.endsWith(suf)) {
674
+ base = base.slice(0, -suf.length);
675
+ changed = true;
676
+ break;
677
+ }
678
+ }
679
+ }
680
+
681
+ // base limpo ainda pode ter -preview no meio? Remove novamente
682
+ // family é base + heurística de família (ex: gemini-3-pro)
683
+ const family = base;
684
+
685
+ // Gera variantes combinatórias
686
+ const variantSet = new Set<string>();
687
+
688
+ const prefixes = ["", "antigravity-"];
689
+ const suffixCombos = ["", "-preview", "-preview-customtools", "-customtools", "-thinking", "-preview-thinking"];
690
+
691
+ // Gera todas combinações prefixo + base + sufixo
692
+ for (const pre of prefixes) {
693
+ for (const suf of suffixCombos) {
694
+ variantSet.add(`${pre}${base}${suf}`);
695
+ }
696
+ }
697
+
698
+ // Adiciona também variações com base original + antigravity
699
+ variantSet.add(original.toLowerCase());
700
+ variantSet.add(`antigravity-${base}`);
701
+ if (isThinking) {
702
+ variantSet.add(`${base}-thinking`);
703
+ variantSet.add(`antigravity-${base}-thinking`);
704
+ }
705
+ if (isCustomTools) {
706
+ variantSet.add(`${base}-preview-customtools`);
707
+ variantSet.add(`antigravity-${base}-preview-customtools`);
708
+ variantSet.add(`${base}-customtools`);
709
+ }
710
+
711
+ // Variantes específicas Claude
712
+ if (base.includes("claude")) {
713
+ variantSet.add(`antigravity-${base}`);
714
+ variantSet.add(`antigravity-${base}-thinking`);
715
+ variantSet.add(`${base}-thinking`);
716
+ }
717
+
718
+ // Variantes gemini 2.5/3.x
719
+ if (base.startsWith("gemini-")) {
720
+ variantSet.add(base);
721
+ variantSet.add(`${base}-preview`);
722
+ variantSet.add(`antigravity-${base}`);
723
+ // gemini-3-pro => gemini-3-pro-preview
724
+ if (!base.includes("preview") && base.match(/^gemini-\d/)) {
725
+ variantSet.add(`${base}-preview`);
726
+ variantSet.add(`antigravity-${base}`);
727
+ }
728
+ }
729
+
730
+ // Normaliza e deduplica, remove vazios
731
+ const variants = Array.from(variantSet)
732
+ .filter((v) => v && v.length > 2)
733
+ .sort();
734
+
735
+ const candidatesInCatalog = variants.filter((v) => MODELS_2026_SET.has(v));
736
+ const relatedModels = variants.map((id) => ({ id, inCatalog: MODELS_2026_SET.has(id) }));
737
+
738
+ return {
739
+ input: original,
740
+ base,
741
+ isAntigravity,
742
+ isThinking,
743
+ isCustomTools,
744
+ isPreview,
745
+ family,
746
+ variants,
747
+ candidatesInCatalog,
748
+ relatedModels,
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Helper que extrai variantes de lista ou objeto
754
+ */
755
+ export function extractVariantsFromAny(input: unknown): ExtractVariantsResult[] {
756
+ if (typeof input === "string") return [extractVariants(input)];
757
+ if (Array.isArray(input)) {
758
+ const out: ExtractVariantsResult[] = [];
759
+ for (const item of input) {
760
+ if (typeof item === "string") out.push(extractVariants(item));
761
+ else if (isPlainObject(item) && typeof (item as any).id === "string") out.push(extractVariants((item as any).id));
762
+ else if (isPlainObject(item) && typeof (item as any).model === "string")
763
+ out.push(extractVariants((item as any).model));
764
+ }
765
+ return out;
766
+ }
767
+ if (isPlainObject(input)) {
768
+ const keys = Object.keys(input as object);
769
+ // se chaves parecem model ids
770
+ if (keys.every((k) => k.includes("gemini") || k.includes("claude") || k.includes("antigravity"))) {
771
+ return keys.map((k) => extractVariants(k));
772
+ }
773
+ }
774
+ return [];
775
+ }
776
+
777
+ // ---------------------------------------------------------------------------
778
+ // Merged: model detection and variant extraction
779
+ // ---------------------------------------------------------------------------
780
+
781
+ /**
782
+ * Detailed model variant extraction result used for routing and budgets.
783
+ */
784
+ export interface ModelVariantInfo {
785
+ original: string;
786
+ normalized: string;
787
+ base: string;
788
+ family: "gemini" | "claude" | "unknown";
789
+ isPreview: boolean;
790
+ isThinking: boolean;
791
+ thinkingVariant: "low" | "max" | null;
792
+ isCustomTools: boolean;
793
+ isClaude: boolean;
794
+ isGemini: boolean;
795
+ isAntigravityWrapper: boolean;
796
+ budgetHint?: number | undefined;
797
+ group: "antigravity" | "gemini-cli";
798
+ }
799
+
800
+ /**
801
+ * Extracts routing/budget metadata from a model id: family detection, preview
802
+ * and customtools flags, thinking-low/max variant, antigravity wrapper and the
803
+ * suggested quota group.
804
+ *
805
+ * @param modelId - raw model id
806
+ * @throws {RequestHelpersError} when modelId is empty
807
+ */
808
+ export function extractModelVariant(modelId: string): ModelVariantInfo {
809
+ if (typeof modelId !== "string" || modelId.trim() === "") {
810
+ throw new RequestHelpersError("extractModelVariant requires non-empty modelId", "INVALID_MODEL");
811
+ }
812
+ const original = modelId.trim();
813
+ const normalized = original.toLowerCase();
814
+
815
+ const isPreview = normalized.includes("preview");
816
+ const isThinking = normalized.includes("thinking");
817
+ const isCustomTools = normalized.includes("customtools");
818
+ const isClaude = normalized.includes("claude");
819
+ const isGemini = normalized.includes("gemini");
820
+ const isAntigravityWrapper = normalized.startsWith("antigravity-");
821
+
822
+ let thinkingVariant: "low" | "max" | null = null;
823
+ if (normalized.includes("-low") || normalized.endsWith("low")) {
824
+ if (normalized.includes("thinking-low") || normalized.match(/-low$/)) {
825
+ thinkingVariant = "low";
826
+ }
827
+ }
828
+ if (normalized.includes("-max") || normalized.endsWith("max")) {
829
+ if (normalized.includes("thinking-max") || normalized.match(/-max$/)) {
830
+ thinkingVariant = "max";
831
+ }
832
+ }
833
+ if (normalized.includes("max") && thinkingVariant !== "low") {
834
+ if (isThinking) thinkingVariant = normalized.includes("max") ? "max" : thinkingVariant;
835
+ }
836
+
837
+ let family: ModelVariantInfo["family"] = "unknown";
838
+ if (isClaude) family = "claude";
839
+ else if (isGemini) family = "gemini";
840
+
841
+ let base = normalized;
842
+ base = base.replace(/^antigravity-/, "");
843
+ base = base.replace(/-preview/g, "");
844
+ base = base.replace(/-customtools/g, "");
845
+ base = base.replace(/-thinking-max/g, "");
846
+ base = base.replace(/-thinking-low/g, "");
847
+ base = base.replace(/-thinking/g, "");
848
+ if (isClaude) {
849
+ base = base.replace(/-low$/g, "");
850
+ base = base.replace(/-max$/g, "");
851
+ }
852
+ base = base.replace(/--+/g, "-").replace(/^-|-$/g, "");
853
+
854
+ const group: ModelVariantInfo["group"] = isGeminiCLIOnlyModel(original) ? "gemini-cli" : "antigravity";
855
+
856
+ const budgetHint = isThinking
857
+ ? getThinkingBudgetForVariant(thinkingVariant)
858
+ : isGemini
859
+ ? THINKING_BUDGET_MAP.medium
860
+ : undefined;
861
+
862
+ return {
863
+ original,
864
+ normalized,
865
+ base,
866
+ family,
867
+ isPreview,
868
+ isThinking,
869
+ thinkingVariant,
870
+ isCustomTools,
871
+ isClaude,
872
+ isGemini,
873
+ isAntigravityWrapper,
874
+ budgetHint,
875
+ group,
876
+ };
877
+ }
878
+
879
+ /**
880
+ * Checklist alias of {@link extractModelVariant}.
881
+ */
882
+ export const parseModelVariant = extractModelVariant;
883
+
884
+ // ---------------------------------------------------------------------------
885
+ // Headers — fingerprint, User-Agent dynamic, strip forbidden
886
+ // ---------------------------------------------------------------------------
887
+
888
+ /**
889
+ * Builds dynamic User-Agent antigravity/{version} {os}/{arch}.
890
+ * Third-person observer copies AM behavior to bypass ban on 1.11.5.
891
+ *
892
+ * @param version - optional semver
893
+ */
894
+ export function buildDynamicUserAgent(version?: string): string {
895
+ const ver = version && /^\d+\.\d+\.\d+/.test(version) ? version.trim() : ANTIGRAVITY_VERSION_FALLBACK;
896
+ try {
897
+ const plat = normalizePlatform(os.platform());
898
+ const arch = normalizeArch(os.arch());
899
+ return `antigravity/${ver} ${plat}/${arch}`;
900
+ } catch {
901
+ return `antigravity/${ver}`;
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Builds Client-Metadata header.
907
+ *
908
+ * @param version - optional version
909
+ */
910
+
911
+ /**
912
+ * Builds X-Goog-Api-Client header — only for gemini-cli mode.
913
+ *
914
+ * @param version - optional
915
+ */
916
+ export function buildApiClient(version?: string): string {
917
+ const ver = version && /^\d+\.\d+\.\d+/.test(version) ? version.trim() : ANTIGRAVITY_VERSION_FALLBACK;
918
+ try {
919
+ const nodeVer = process.versions.node ?? "20.0.0";
920
+ return `antigravity/${ver} gl-node/${nodeVer} gax/4.9.0 grpc/1.14.0`;
921
+ } catch {
922
+ return `antigravity/${ver}`;
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Generates fingerprint headers for given style.
928
+ * Observer knows:
929
+ * - AM only sends User-Agent on content requests (not on onboard/load)
930
+ * - Never sends X-Goog-QuotaUser nor X-Client-Device-Id
931
+ * - Gemini CLI includes X-Goog-Api-Client
932
+ *
933
+ * @param style - 'antigravity' or 'gemini-cli'
934
+ * @param version - optional antigravity version
935
+ * @returns headers record
936
+ */
937
+ // v2.1.15 Phase B: getFingerprintHeaders moved to fingerprint.js (identity owner).
938
+
939
+ /**
940
+ * Alias required by checklist naming: strip x-goog-user-project header explicit.
941
+ *
942
+ * @param headers - headers map
943
+ * @returns cleaned map
944
+ */
945
+ export function stripXGoogUserProjectHeader(headers: Record<string, string>): Record<string, string> {
946
+ return stripForbiddenHeaders(headers);
947
+ }
948
+
949
+ // ---------------------------------------------------------------------------
950
+ // Merged: transport helpers — retry classification and header stripping
951
+ // ---------------------------------------------------------------------------
952
+
953
+ /**
954
+ * Checks whether an HTTP status should trigger cascade retry:
955
+ * 429, 403, 404 and the whole 5xx range.
956
+ *
957
+ * @param status - HTTP status code
958
+ */
959
+
960
+ // ---------------------------------------------------------------------------
961
+ // Tool name sanitization & validation
962
+ // ---------------------------------------------------------------------------
963
+
964
+ // ---------------------------------------------------------------------------
965
+ // JSON Schema cleaning — fixes Invalid JSON payload Unknown name 'parameters'
966
+ // ---------------------------------------------------------------------------
967
+
968
+ // ---------------------------------------------------------------------------
969
+ // Merged: strict JSON schema cleaning for tool definitions
970
+ // ---------------------------------------------------------------------------
971
+
972
+ /**
973
+ * Recursively cleans a JSON Schema node down to only the keys listed in
974
+ * {@link ALLOWED_SCHEMA_KEYS}. This removes $schema/$ref/title/
975
+ * additionalProperties/default/examples and similar unknown names that trigger
976
+ * "Invalid JSON payload Unknown name" errors upstream.
977
+ *
978
+ * v2.1.14 merge note: renamed from cleanJsonSchema (request-helpers.ts) to
979
+ * cleanJsonSchemaStrict; the tolerant Gemini-shaping normalizer from
980
+ * request.ts owns the cleanJsonSchema name. ALLOWED_SCHEMA_KEYS is now the
981
+ * full allowlist Set from request.ts; the switch below still only emits the
982
+ * six core keys, so the observable behavior of this strict cleaner is
983
+ * unchanged by the merge.
984
+ *
985
+ * @param schema - raw schema node
986
+ * @param depth - recursion guard, throws past depth 12
987
+ * @returns cleaned schema object or undefined when not salvageable
988
+ * @throws {RequestHelpersError} when depth exceeds 12 (possible circular schema)
989
+ */
990
+ export function cleanJsonSchemaStrict(schema: unknown, depth = 0): Record<string, unknown> | undefined {
991
+ if (depth > 12) {
992
+ throw new RequestHelpersError("JSON schema depth >12 — possible circular", "SCHEMA_TOO_DEEP");
993
+ }
994
+ if (!isPlainObject(schema)) {
995
+ return undefined;
996
+ }
997
+
998
+ const out: Record<string, unknown> = {};
999
+
1000
+ for (const key of Object.keys(schema)) {
1001
+ if (!ALLOWED_SCHEMA_KEYS.has(key)) {
1002
+ continue;
1003
+ }
1004
+
1005
+ const val = (schema as Record<string, unknown>)[key];
1006
+
1007
+ switch (key) {
1008
+ case "type": {
1009
+ if (typeof val === "string" && ["object", "string", "number", "integer", "boolean", "array"].includes(val)) {
1010
+ out.type = val;
1011
+ }
1012
+ break;
1013
+ }
1014
+ case "description": {
1015
+ if (typeof val === "string") out.description = val.slice(0, 1024);
1016
+ break;
1017
+ }
1018
+ case "enum": {
1019
+ if (Array.isArray(val)) {
1020
+ const filtered = val
1021
+ .filter((e) => typeof e === "string" || typeof e === "number" || typeof e === "boolean")
1022
+ .slice(0, 100);
1023
+ if (filtered.length > 0) out.enum = filtered;
1024
+ }
1025
+ break;
1026
+ }
1027
+ case "required": {
1028
+ if (Array.isArray(val)) {
1029
+ const req = val.filter((r) => typeof r === "string").slice(0, 100);
1030
+ if (req.length > 0) out.required = req;
1031
+ }
1032
+ break;
1033
+ }
1034
+ case "properties": {
1035
+ if (isPlainObject(val)) {
1036
+ const props: Record<string, unknown> = {};
1037
+ for (const propName of Object.keys(val)) {
1038
+ const propSchema = (val as Record<string, unknown>)[propName];
1039
+ const cleaned = cleanJsonSchemaStrict(propSchema, depth + 1);
1040
+ if (cleaned) {
1041
+ props[propName] = cleaned;
1042
+ }
1043
+ }
1044
+ out.properties = props;
1045
+ }
1046
+ break;
1047
+ }
1048
+ case "items": {
1049
+ const cleaned = cleanJsonSchemaStrict(val, depth + 1);
1050
+ if (cleaned) out.items = cleaned;
1051
+ break;
1052
+ }
1053
+ default:
1054
+ break;
1055
+ }
1056
+ }
1057
+
1058
+ return out;
1059
+ }
1060
+
1061
+ /**
1062
+ * Cleaned tool definition compatible with Gemini functionDeclarations.
1063
+ */
1064
+ export interface CleanedTool {
1065
+ name: string;
1066
+ description?: string | undefined;
1067
+ parameters: {
1068
+ type: "object";
1069
+ properties: Record<string, unknown>;
1070
+ required?: string[] | undefined;
1071
+ description?: string | undefined;
1072
+ };
1073
+ }
1074
+
1075
+ /**
1076
+ * Cleans a single raw tool definition (OpenAI wrapper, Anthropic input_schema
1077
+ * or plain parameters) into a {@link CleanedTool} with a sanitized name and a
1078
+ * root-object schema enforced.
1079
+ *
1080
+ * v2.1.14 merge note: keeps its name; it now builds on
1081
+ * cleanJsonSchemaStrict (the strict cleaner formerly exported as the
1082
+ * request-helpers cleanJsonSchema).
1083
+ *
1084
+ * @param raw - raw tool definition
1085
+ * @throws {RequestHelpersError} on missing/invalid objects or names
1086
+ */
1087
+ export function cleanRawToolSchema(raw: unknown): CleanedTool {
1088
+ if (!isPlainObject(raw) && !(raw as any)?.function) {
1089
+ throw new RequestHelpersError(`cleanToolSchema expects object, got ${typeof raw}`, "INVALID_TOOL_SCHEMA");
1090
+ }
1091
+
1092
+ // Unwrap OpenAI style { type:"function", function:{...} }
1093
+ let src: Record<string, unknown> = raw as Record<string, unknown>;
1094
+ if (isPlainObject((src as any).function)) {
1095
+ src = (src as any).function as Record<string, unknown>;
1096
+ }
1097
+
1098
+ const potentialSchemas = [
1099
+ (src as any).parameters,
1100
+ (src as any).input_schema,
1101
+ (src as any).schema,
1102
+ (src as any).parameters_json,
1103
+ ];
1104
+
1105
+ let parametersRaw: unknown = undefined;
1106
+ for (const cand of potentialSchemas) {
1107
+ if (cand && isPlainObject(cand)) {
1108
+ parametersRaw = cand;
1109
+ break;
1110
+ }
1111
+ }
1112
+
1113
+ if (!parametersRaw) {
1114
+ parametersRaw = { type: "object", properties: {} };
1115
+ }
1116
+
1117
+ const cleanedParams = cleanJsonSchemaStrict(parametersRaw, 0) ?? { type: "object", properties: {} };
1118
+
1119
+ // Enforce root object type required by Gemini functionDeclarations
1120
+ if ((cleanedParams as any).type !== "object") {
1121
+ (cleanedParams as any).type = "object";
1122
+ }
1123
+ if (!(cleanedParams as any).properties) {
1124
+ (cleanedParams as any).properties = {};
1125
+ }
1126
+
1127
+ const rawName = (src as any).name;
1128
+ if (typeof rawName !== "string" || rawName.trim() === "") {
1129
+ throw new RequestHelpersError("tool schema missing name", "INVALID_TOOL_NAME");
1130
+ }
1131
+ const sanitized = sanitizeToolName(rawName);
1132
+
1133
+ const descriptionRaw = (src as any).description;
1134
+ const description = typeof descriptionRaw === "string" ? descriptionRaw.slice(0, 4096) : undefined;
1135
+
1136
+ return {
1137
+ name: sanitized,
1138
+ description,
1139
+ parameters: cleanedParams as unknown as CleanedTool["parameters"],
1140
+ };
1141
+ }
1142
+
1143
+ /**
1144
+ * Cleans an array of raw tools into deduplicated {@link CleanedTool} entries
1145
+ * (last wins per sanitized name), capped at {@link MAX_TOOLS_PER_REQUEST}.
1146
+ * Invalid tools are skipped rather than failing the request.
1147
+ *
1148
+ * @param tools - array of raw tool definitions
1149
+ * @throws {RequestHelpersError} when input is not an array
1150
+ */
1151
+ export function cleanToolsArray(tools: unknown): CleanedTool[] {
1152
+ if (!Array.isArray(tools)) {
1153
+ throw new RequestHelpersError("cleanToolsArray expects array", "INVALID_TOOLS");
1154
+ }
1155
+ const map = new Map<string, CleanedTool>();
1156
+ for (const t of tools) {
1157
+ try {
1158
+ const cleaned = cleanRawToolSchema(t);
1159
+ map.set(cleaned.name, cleaned);
1160
+ if (map.size >= MAX_TOOLS_PER_REQUEST) break;
1161
+ } catch {
1162
+ continue;
1163
+ }
1164
+ }
1165
+ return Array.from(map.values());
1166
+ }
1167
+
1168
+ // ---------------------------------------------------------------------------
1169
+ // Thinking blocks mapping
1170
+ // ---------------------------------------------------------------------------
1171
+
1172
+ /**
1173
+ * Maps a thinking block to Gemini thought part.
1174
+ * For Gemini 3 and Claude via Antigravity, thought parts must have thought: true.
1175
+ * Optionally includes thoughtSignature for multi-turn preservation.
1176
+ *
1177
+ * @param text - thinking text
1178
+ * @param signature - optional signature for LRU cache in streaming.ts
1179
+ * @returns GeminiPart
1180
+ */
1181
+ export function mapThinkingBlock(text: string, signature?: string): GeminiPart {
1182
+ if (!text || typeof text !== "string") text = String(text ?? "");
1183
+ const part: GeminiPart = {
1184
+ text,
1185
+ thought: true,
1186
+ };
1187
+ if (signature && typeof signature === "string" && signature.length > 0) {
1188
+ part.thoughtSignature = signature.slice(0, 10000);
1189
+ }
1190
+ return part;
1191
+ }
1192
+
1193
+ /**
1194
+ * Sorts parts so that thought parts come first, then text, then function calls.
1195
+ * Required because Gemini/Claude require thinking block order validation.
1196
+ *
1197
+ * @param parts - parts array
1198
+ * @returns sorted parts
1199
+ */
1200
+ export function sortThoughtFirst(parts: GeminiPart[]): GeminiPart[] {
1201
+ if (!Array.isArray(parts)) return [];
1202
+ const thoughts = parts.filter((p) => p.thought === true);
1203
+ const others = parts.filter((p) => p.thought !== true);
1204
+ // Preserve relative order within groups, thoughts first
1205
+ return [...thoughts, ...others];
1206
+ }
1207
+
1208
+ /**
1209
+ * Filters and normalizes thinking content — removes empty thinking and ensures
1210
+ * signature preservation.
1211
+ *
1212
+ * @param parts - incoming parts
1213
+ * @returns filtered parts
1214
+ */
1215
+ export function filterAndMapThinkingParts(parts: any[]): GeminiPart[] {
1216
+ const out: GeminiPart[] = [];
1217
+ for (const p of parts) {
1218
+ if (!p) continue;
1219
+ // Already Gemini thought
1220
+ if (p.thought === true && p.text) {
1221
+ out.push(mapThinkingBlock(String(p.text), p.thoughtSignature));
1222
+ continue;
1223
+ }
1224
+ // Anthropic thinking type
1225
+ if (p.type === "thinking" || p.type === "reasoning") {
1226
+ const txt = p.thinking ?? p.text ?? p.reasoning ?? "";
1227
+ const sig = p.signature ?? p.thoughtSignature;
1228
+ if (txt) out.push(mapThinkingBlock(String(txt), sig ? String(sig) : undefined));
1229
+ continue;
1230
+ }
1231
+ // Direct string thinking marker
1232
+ if (typeof p === "object" && "thinking" in p && typeof p.thinking === "string") {
1233
+ out.push(mapThinkingBlock(p.thinking, p.signature));
1234
+ continue;
1235
+ }
1236
+ // Keep other parts as-is for later mapping (text, etc)
1237
+ out.push(p);
1238
+ }
1239
+ return sortThoughtFirst(out);
1240
+ }
1241
+
1242
+ /**
1243
+ * Detects thinking blocks that must be filtered before dispatch: thought flags,
1244
+ * type discriminators, reasoning_content/reasoning fields.
1245
+ *
1246
+ * @param block - any part-like object
1247
+ */
1248
+ export function isThinkingBlock(block: unknown): boolean {
1249
+ if (!block || typeof block !== "object") return false;
1250
+ const b = block as Record<string, unknown>;
1251
+
1252
+ if ((b as any).thought === true) return true;
1253
+ if ((b as any).thinking === true) return true;
1254
+
1255
+ const t = (b as any).type;
1256
+ if (typeof t === "string") {
1257
+ const lt = t.toLowerCase();
1258
+ if (lt === "thinking" || lt === "reasoning" || lt === "thought" || lt === "internal_reasoning") return true;
1259
+ }
1260
+
1261
+ if ("thinking" in b && typeof b.thinking === "string" && (b as any).type === "thinking") return true;
1262
+ if ("reasoning_content" in b) return true;
1263
+ if ("reasoning" in b && typeof b.reasoning === "string") return true;
1264
+
1265
+ return false;
1266
+ }
1267
+
1268
+ // ---------------------------------------------------------------------------
1269
+ // Merged: standalone thinking-block filters
1270
+ // ---------------------------------------------------------------------------
1271
+
1272
+ /**
1273
+ * Filters thinking blocks from a parts array.
1274
+ *
1275
+ * @param parts - array of parts in any shape
1276
+ * @throws {RequestHelpersError} when input is not an array
1277
+ */
1278
+ export function filterThinkingBlocksFromParts(parts: unknown): GeminiPart[] {
1279
+ if (!Array.isArray(parts)) {
1280
+ throw new RequestHelpersError("filterThinkingBlocksFromParts expects array", "INVALID_PARTS");
1281
+ }
1282
+ const filtered: GeminiPart[] = [];
1283
+ for (const p of parts) {
1284
+ if (isThinkingBlock(p)) continue;
1285
+ if (isPlainObject(p)) {
1286
+ const obj = p as Record<string, unknown>;
1287
+ if ((obj as any).thought === true) continue;
1288
+ }
1289
+ filtered.push(p as GeminiPart);
1290
+ }
1291
+ return filtered;
1292
+ }
1293
+
1294
+ /**
1295
+ * Filters thinking parts inside each Gemini message, dropping messages that
1296
+ * become empty (including system).
1297
+ *
1298
+ * @param messages - Gemini-shaped messages
1299
+ * @throws {RequestHelpersError} when input is not an array
1300
+ */
1301
+ export function filterThinkingBlocksFromMessages(messages: unknown): GeminiMessage[] {
1302
+ if (!Array.isArray(messages)) {
1303
+ throw new RequestHelpersError("filterThinkingBlocksFromMessages expects array", "INVALID_MESSAGES");
1304
+ }
1305
+ const out: GeminiMessage[] = [];
1306
+ for (const raw of messages) {
1307
+ if (!isPlainObject(raw)) continue;
1308
+ const msg = raw as { role?: string; parts?: unknown[] };
1309
+ const role = msg.role;
1310
+ const partsRaw = msg.parts;
1311
+ if (!Array.isArray(partsRaw)) continue;
1312
+ const filteredParts = filterThinkingBlocksFromParts(partsRaw);
1313
+ if (filteredParts.length === 0) {
1314
+ if (role === "system") continue;
1315
+ continue;
1316
+ }
1317
+ out.push({
1318
+ role: (role as GeminiMessage["role"]) ?? "user",
1319
+ parts: filteredParts,
1320
+ _rawRole: (raw as any)._rawRole,
1321
+ });
1322
+ }
1323
+ return out;
1324
+ }
1325
+
1326
+ /**
1327
+ * Auto-detecting filter: routes message arrays (role+parts items) to
1328
+ * {@link filterThinkingBlocksFromMessages} and everything else to
1329
+ * {@link filterThinkingBlocksFromParts}.
1330
+ *
1331
+ * Named `filterThinkingBlocksAuto` because the historical overloaded
1332
+ * `filterThinkingBlocks` keeps its original tag-stripping/string semantics.
1333
+ *
1334
+ * @param input - parts array or messages array
1335
+ * @throws {RequestHelpersError} when input is not an array
1336
+ */
1337
+ export function filterThinkingBlocksAuto(input: unknown): unknown[] {
1338
+ if (!Array.isArray(input)) {
1339
+ throw new RequestHelpersError("filterThinkingBlocks expects array", "INVALID_INPUT");
1340
+ }
1341
+ if (input.length === 0) return [];
1342
+
1343
+ const first = input[0] as any;
1344
+ if (isPlainObject(first) && "role" in first && "parts" in first) {
1345
+ return filterThinkingBlocksFromMessages(input);
1346
+ }
1347
+ return filterThinkingBlocksFromParts(input);
1348
+ }
1349
+
1350
+ // ============================================================================
1351
+ // 06. filterThinkingBlocks()
1352
+ // ============================================================================
1353
+
1354
+ export interface ThoughtLikePart {
1355
+ thought?: boolean;
1356
+ thinking?: boolean;
1357
+ type?: string;
1358
+ text?: string;
1359
+ content?: string;
1360
+ reasoning?: string;
1361
+ [k: string]: unknown;
1362
+ }
1363
+
1364
+ /**
1365
+ * Remove blocos de thinking/reasoning de conteúdo.
1366
+ * Suporta:
1367
+ * - string: remove tags <thinking>...</thinking>, <thought>...</thought>, <think>...</think>
1368
+ * - array de parts Gemini (com thought:true) => filtra
1369
+ * - array de blocks OpenCode-like
1370
+ */
1371
+
1372
+ const THINKING_TAG_REGEX = /<(thinking|thought|think|reasoning|Thought|Thinking)[^>]*>[\s\S]*?<\/\1\s*>/gi;
1373
+ const THINKING_SELF_CLOSED_REGEX = /<(thinking|thought|think)[^>]*\/>/gi;
1374
+ const THINKING_INLINE_PREFIX = /^(?:Thought:|Thinking:|Reasoning:)\s*/i;
1375
+
1376
+ export function filterThinkingBlocks(input: string): string;
1377
+ export function filterThinkingBlocks<T extends ThoughtLikePart>(input: T[]): T[];
1378
+ export function filterThinkingBlocks(input: unknown): unknown;
1379
+ export function filterThinkingBlocks(input: unknown): unknown {
1380
+ if (typeof input === "string") {
1381
+ let out = input;
1382
+ // remove blocos com tags
1383
+ out = out.replace(THINKING_TAG_REGEX, "");
1384
+ out = out.replace(THINKING_SELF_CLOSED_REGEX, "");
1385
+ // remove múltiplas linhas que são só prefixo Thinking:
1386
+ // também limpa artefactos "<thinking>" soltos
1387
+ out = out.replace(/<\/?thinking[^>]*>/gi, "");
1388
+ out = out.replace(/<\/?thought[^>]*>/gi, "");
1389
+ out = out.replace(/<\/?think[^>]*>/gi, "");
1390
+ out = out.replace(/<\/?reasoning[^>]*>/gi, "");
1391
+ // remove linhas que começam com Thought:
1392
+ out = out
1393
+ .split("\n")
1394
+ .filter((line) => {
1395
+ const trimmed = line.trim();
1396
+ if (!trimmed) return true;
1397
+ // se linha inteira é marcador de thinking, remove
1398
+ if (/^(Thought|Thinking|Reasoning):\s*$/i.test(trimmed)) return false;
1399
+ return true;
1400
+ })
1401
+ .join("\n");
1402
+ // trim excesso de espaços em branco deixados
1403
+ out = out.replace(/\n{3,}/g, "\n\n").trim();
1404
+ return out;
1405
+ }
1406
+
1407
+ if (Array.isArray(input)) {
1408
+ return (input as ThoughtLikePart[]).filter((part) => {
1409
+ if (!part || typeof part !== "object") return true;
1410
+ // Gemini: { thought: true }
1411
+ if ((part as any).thought === true) return false;
1412
+ if ((part as any).thinking === true) return false;
1413
+ const t = (part as any).type?.toLowerCase?.();
1414
+ if (t === "thinking" || t === "thought" || t === "reasoning") return false;
1415
+ // conteúdo textual que é apenas thinking tag
1416
+ const text = (part as any).text ?? (part as any).content ?? (part as any).reasoning;
1417
+ if (typeof text === "string") {
1418
+ const trimmed = text.trim();
1419
+ if (!trimmed) return true;
1420
+ // se conteúdo inteiro está envolto em thinking tags, filtra bloco inteiro
1421
+ if (/^<(thinking|thought|think|reasoning)[\s\S]*<\/(thinking|thought|think|reasoning)>$/i.test(trimmed))
1422
+ return false;
1423
+ // se contém apenas tag, filtra
1424
+ if (/^<\/?(thinking|thought|think|reasoning)[^>]*>$/i.test(trimmed)) return false;
1425
+ }
1426
+ return true;
1427
+ });
1428
+ }
1429
+
1430
+ // se objeto único com parts
1431
+ if (isPlainObject(input)) {
1432
+ const obj = input as Record<string, unknown>;
1433
+ if (Array.isArray(obj["parts"])) {
1434
+ return {
1435
+ ...obj,
1436
+ parts: filterThinkingBlocks(obj["parts"] as unknown[]),
1437
+ };
1438
+ }
1439
+ if (Array.isArray(obj["content"])) {
1440
+ return {
1441
+ ...obj,
1442
+ content: filterThinkingBlocks(obj["content"] as unknown[]),
1443
+ };
1444
+ }
1445
+ }
1446
+
1447
+ return input;
1448
+ }
1449
+
1450
+ // ---------------------------------------------------------------------------
1451
+ // Google Search tool injection
1452
+ // ---------------------------------------------------------------------------
1453
+
1454
+ /**
1455
+ * Definition for google_search built-in grounding tool wrapped as function declaration
1456
+ * for compatibility with OpenCode → Gemini flow.
1457
+ * Observer notes: original plugin shekohex uses wrapper to call separate search API.
1458
+ */
1459
+ export const GOOGLE_SEARCH_FUNCTION_DECLARATION = {
1460
+ name: "google_search",
1461
+ description:
1462
+ "Search the web for real-time information, factual grounding, current events, documentation, and recent knowledge. Use when user asks for latest information, news, or requires browsing to answer.",
1463
+ parameters: {
1464
+ type: "OBJECT",
1465
+ properties: {
1466
+ query: {
1467
+ type: "STRING",
1468
+ description: "Search query string, 3-10 words focused keywords",
1469
+ },
1470
+ },
1471
+ required: ["query"],
1472
+ },
1473
+ } as const;
1474
+
1475
+ export const URL_CONTEXT_FUNCTION_DECLARATION = {
1476
+ name: "url_context",
1477
+ description: "Fetch and provide context from a URL for grounding.",
1478
+ parameters: {
1479
+ type: "OBJECT",
1480
+ properties: {
1481
+ url: {
1482
+ type: "STRING",
1483
+ description: "HTTP(S) URL to fetch",
1484
+ },
1485
+ },
1486
+ required: ["url"],
1487
+ },
1488
+ } as const;
1489
+
1490
+ /**
1491
+ * Injects google_search tool into declarations if enabled and not already present.
1492
+ *
1493
+ * @param declarations - existing declarations array
1494
+ * @param enabled - whether injection enabled
1495
+ * @param includeUrlContext - whether to also inject url_context
1496
+ * @returns new declarations array with injection
1497
+ */
1498
+ export function injectGoogleSearchTool(
1499
+ declarations: Array<{ name: string; description?: string; parameters?: any; originalName: string }>,
1500
+ enabled?: boolean,
1501
+ includeUrlContext = false,
1502
+ ): Array<{ name: string; description?: string; parameters?: any; originalName: string }> {
1503
+ if (!enabled) return declarations;
1504
+ const existingNames = new Set(declarations.map((d) => d.name.toLowerCase()));
1505
+ const result = [...declarations];
1506
+
1507
+ if (!existingNames.has("google_search")) {
1508
+ result.push({
1509
+ name: "google_search",
1510
+ description: GOOGLE_SEARCH_FUNCTION_DECLARATION.description,
1511
+ parameters: GOOGLE_SEARCH_FUNCTION_DECLARATION.parameters,
1512
+ originalName: "google_search",
1513
+ });
1514
+ }
1515
+
1516
+ if (includeUrlContext && !existingNames.has("url_context")) {
1517
+ result.push({
1518
+ name: "url_context",
1519
+ description: URL_CONTEXT_FUNCTION_DECLARATION.description,
1520
+ parameters: URL_CONTEXT_FUNCTION_DECLARATION.parameters,
1521
+ originalName: "url_context",
1522
+ });
1523
+ }
1524
+
1525
+ return result;
1526
+ }
1527
+
1528
+ // ---------------------------------------------------------------------------
1529
+ // Content helpers — inlineData, data URI, function args parsing
1530
+ // ---------------------------------------------------------------------------
1531
+
1532
+ /**
1533
+ * Parses OpenAI tool arguments which may be stringified JSON.
1534
+ *
1535
+ * @param args - args string or object
1536
+ * @returns parsed object
1537
+ */
1538
+ export function parseFunctionArgs(args: string | object | any): Record<string, any> {
1539
+ if (!args) return {};
1540
+ if (typeof args === "object" && !Array.isArray(args)) return args as Record<string, any>;
1541
+ if (typeof args === "string") {
1542
+ const trimmed = args.trim();
1543
+ if (!trimmed) return {};
1544
+ try {
1545
+ const parsed = JSON.parse(trimmed);
1546
+ if (parsed && typeof parsed === "object") return parsed;
1547
+ return { value: parsed };
1548
+ } catch {
1549
+ // If not JSON, treat as single string value if schema expects string, else wrap
1550
+ return { query: trimmed, input: trimmed };
1551
+ }
1552
+ }
1553
+ return { value: args };
1554
+ }
1555
+
1556
+ // v2.1.16: dead parseDataUri was deleted — zero callers repo-wide. The live
1557
+ // data-URI parser is the private parseDataUrl below (near-twin that also
1558
+ // guards against non-string input), used by contentPartsToGeminiParts.
1559
+
1560
+ /**
1561
+ * Attempts to extract last user prompt text for deterministic user_prompt_id.
1562
+ *
1563
+ * @param messages - messages array
1564
+ * @returns last user text or fallback
1565
+ */
1566
+ export function extractLastUserPrompt(messages: OpenCodeMessage[]): string {
1567
+ if (!Array.isArray(messages) || messages.length === 0) return "empty-prompt";
1568
+ for (let i = messages.length - 1; i >= 0; i--) {
1569
+ const m = messages[i];
1570
+ if (!m) continue;
1571
+ if (String(m.role).toLowerCase() !== "user") continue;
1572
+ const c = m.content;
1573
+ if (typeof c === "string" && c.trim()) return c.trim().slice(0, 2000);
1574
+ if (Array.isArray(c)) {
1575
+ for (const part of c) {
1576
+ if (!part) continue;
1577
+ if (part.type === "text" && part.text && String(part.text).trim()) {
1578
+ return String(part.text).trim().slice(0, 2000);
1579
+ }
1580
+ if (typeof part === "object" && "text" in part && typeof (part as any).text === "string") {
1581
+ const t = String((part as any).text).trim();
1582
+ if (t) return t.slice(0, 2000);
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ // Fallback: stringify whole
1588
+ try {
1589
+ return JSON.stringify(messages).slice(0, 2000);
1590
+ } catch {
1591
+ return "fallback-prompt";
1592
+ }
1593
+ }
1594
+
1595
+ // ============================================================================
1596
+ // 07. normalizeMessages() - OpenCode -> Gemini
1597
+ // ============================================================================
1598
+
1599
+ export interface OpenCodeContentPart {
1600
+ type?: string; // "text" | "image_url" | "tool_call" etc
1601
+ text?: string;
1602
+ image_url?: { url: string; detail?: string };
1603
+ // tool
1604
+ id?: string;
1605
+ tool_call_id?: string;
1606
+ name?: string;
1607
+ // para compat
1608
+ function?: { name: string; arguments: string | object };
1609
+ [k: string]: unknown;
1610
+ }
1611
+
1612
+ export interface OpenCodeToolCall {
1613
+ id: string;
1614
+ type: "function";
1615
+ function: { name: string; arguments: string | object };
1616
+ }
1617
+
1618
+ export interface GeminiFunctionCall {
1619
+ name: string;
1620
+ args: Record<string, unknown>;
1621
+ }
1622
+
1623
+ export interface GeminiFunctionResponse {
1624
+ name: string;
1625
+ response: Record<string, unknown>;
1626
+ }
1627
+
1628
+ export interface NormalizedMessagesResult {
1629
+ systemInstruction?: GeminiSystemInstruction;
1630
+ contents: GeminiContent[];
1631
+ /** mensagens originais ignoradas por serem vazias */
1632
+ droppedCount: number;
1633
+ }
1634
+
1635
+ /**
1636
+ * Converte data URL para mime + base64
1637
+ */
1638
+ function parseDataUrl(url: string): { mimeType: string; data: string } | null {
1639
+ const m = url.match(/^data:([^;]+);base64,(.*)$/);
1640
+ if (!m) return null;
1641
+ return { mimeType: m[1]!, data: m[2]! };
1642
+ }
1643
+
1644
+ function safeJsonParse(str: string): Record<string, unknown> | null {
1645
+ try {
1646
+ const parsed = JSON.parse(str);
1647
+ if (isPlainObject(parsed)) return parsed as Record<string, unknown>;
1648
+ // se não for objeto, encapsula
1649
+ return { result: parsed } as Record<string, unknown>;
1650
+ } catch {
1651
+ return null;
1652
+ }
1653
+ }
1654
+
1655
+ function contentPartsToGeminiParts(content: string | OpenCodeContentPart[] | null | undefined): GeminiPart[] {
1656
+ if (content == null) return [];
1657
+ if (typeof content === "string") {
1658
+ if (!content.trim()) return [];
1659
+ return [{ text: content }];
1660
+ }
1661
+ if (Array.isArray(content)) {
1662
+ const parts: GeminiPart[] = [];
1663
+ for (const part of content) {
1664
+ if (!part) continue;
1665
+ if (typeof part === "string") {
1666
+ // fallback caso array contenha string
1667
+ if ((part as unknown as string).trim()) parts.push({ text: part as unknown as string });
1668
+ continue;
1669
+ }
1670
+ const p = part as OpenCodeContentPart;
1671
+ if (p.type === "text" && p.text) {
1672
+ parts.push({ text: p.text });
1673
+ } else if (p.type === "image_url" && p.image_url?.url) {
1674
+ const url = p.image_url.url;
1675
+ const data = parseDataUrl(url);
1676
+ if (data) {
1677
+ parts.push({ inlineData: data });
1678
+ } else {
1679
+ // se for https, Gemini prefere fileData, mas mantemos texto referência para evitar falha
1680
+ // poderia baixar, mas zero-dep: mantém como fileUri
1681
+ // detecta mime via extensão simples
1682
+ parts.push({ fileData: { mimeType: "image/jpeg", fileUri: url } });
1683
+ }
1684
+ } else if ((p.type === "image" || (p as any).inlineData) && (p as any).inlineData) {
1685
+ parts.push({ inlineData: (p as any).inlineData });
1686
+ } else if (p.text) {
1687
+ // fallback generico com text
1688
+ parts.push({ text: p.text });
1689
+ } else if ((p as any).content && typeof (p as any).content === "string") {
1690
+ parts.push({ text: (p as any).content });
1691
+ }
1692
+ }
1693
+ return parts;
1694
+ }
1695
+ return [];
1696
+ }
1697
+
1698
+ /**
1699
+ * Normaliza lista de mensagens OpenCode (OpenAI-like) para formato Gemini
1700
+ * Cloud Code Assist v1internal.
1701
+ *
1702
+ * - system => systemInstruction (combinado)
1703
+ * - user => role user
1704
+ * - assistant => role model, com functionCall se tiver tool_calls
1705
+ * - tool => role user com functionResponse
1706
+ * - filtra thinking blocks automaticamente
1707
+ * - merge mensagens consecutivas de mesmo role
1708
+ *
1709
+ * @param messages - mensagens OpenCode
1710
+ * @param opts - includeSystemAsUser? Se true, system vai como user prefixado
1711
+ */
1712
+ export function normalizeMessages(
1713
+ messages: OpenCodeMessage[],
1714
+ opts?: { mergeConsecutive?: boolean; includeSystemAsUser?: boolean; filterThinking?: boolean },
1715
+ ): NormalizedMessagesResult {
1716
+ const mergeConsecutive = opts?.mergeConsecutive ?? true;
1717
+ const includeSystemAsUser = opts?.includeSystemAsUser ?? false;
1718
+ const filterThinking = opts?.filterThinking ?? true;
1719
+
1720
+ if (!Array.isArray(messages)) {
1721
+ return { contents: [], droppedCount: 0 };
1722
+ }
1723
+
1724
+ const systemParts: GeminiPart[] = [];
1725
+ const contents: GeminiContent[] = [];
1726
+ let dropped = 0;
1727
+
1728
+ for (const msg of messages) {
1729
+ if (!msg || typeof msg !== "object") {
1730
+ dropped++;
1731
+ continue;
1732
+ }
1733
+ const roleRaw = (msg.role ?? "user").toLowerCase();
1734
+
1735
+ // SYSTEM
1736
+ if (roleRaw === "system") {
1737
+ const parts = contentPartsToGeminiParts(msg.content as any);
1738
+ if (parts.length === 0) {
1739
+ dropped++;
1740
+ continue;
1741
+ }
1742
+ const filtered = filterThinking ? (filterThinkingBlocks(parts) as GeminiPart[]) : parts;
1743
+ if (includeSystemAsUser) {
1744
+ // quando solicitado, system vira user com prefixo
1745
+ if (filtered.length > 0) {
1746
+ contents.push({ role: "user", parts: filtered.map((p) => (p.text ? { text: `[System] ${p.text}` } : p)) });
1747
+ }
1748
+ } else {
1749
+ systemParts.push(...filtered);
1750
+ }
1751
+ continue;
1752
+ }
1753
+
1754
+ // TOOL -> functionResponse
1755
+ if (roleRaw === "tool") {
1756
+ const toolName = msg.name ?? (msg as any).tool_name ?? msg.tool_call_id ?? "unknown_tool";
1757
+ let responseContent: string;
1758
+ if (typeof msg.content === "string") responseContent = msg.content;
1759
+ else if (Array.isArray(msg.content)) {
1760
+ responseContent = (msg.content as OpenCodeContentPart[]).map((p) => p.text ?? "").join("\n");
1761
+ } else responseContent = String(msg.content ?? "");
1762
+
1763
+ const parsed = safeJsonParse(responseContent);
1764
+ const response: Record<string, unknown> = parsed ?? { content: responseContent };
1765
+
1766
+ const part: GeminiPart = {
1767
+ functionResponse: {
1768
+ name: toolName,
1769
+ response,
1770
+ },
1771
+ };
1772
+ // Gemini exige que functionResponse venha em role user
1773
+ contents.push({ role: "user", parts: [part] });
1774
+ continue;
1775
+ }
1776
+
1777
+ // ASSISTANT -> model
1778
+ if (roleRaw === "assistant" || roleRaw === "model" || roleRaw === "bot") {
1779
+ const parts: GeminiPart[] = [];
1780
+
1781
+ // text content
1782
+ const textParts = contentPartsToGeminiParts(msg.content as any);
1783
+ parts.push(...textParts);
1784
+
1785
+ // tool_calls -> functionCall
1786
+ if (Array.isArray(msg.tool_calls)) {
1787
+ for (const tc of msg.tool_calls) {
1788
+ if (!tc || !tc.function?.name) continue;
1789
+ let args: Record<string, unknown> = {};
1790
+ const rawArgs = tc.function.arguments;
1791
+ if (typeof rawArgs === "string") {
1792
+ const parsed = safeJsonParse(rawArgs);
1793
+ args = parsed ?? { _raw: rawArgs };
1794
+ // se raw era string pura, tenta manter objeto
1795
+ if (!parsed) {
1796
+ // tenta parse novamente como objeto genérico
1797
+ try {
1798
+ args = JSON.parse(rawArgs) as Record<string, unknown>;
1799
+ } catch {
1800
+ args = { input: rawArgs };
1801
+ }
1802
+ }
1803
+ } else if (isPlainObject(rawArgs)) {
1804
+ args = rawArgs as Record<string, unknown>;
1805
+ }
1806
+ // sanitize function name via nossa validação
1807
+ const cleanName = sanitizeToolName(tc.function.name);
1808
+ parts.push({ functionCall: { name: cleanName, args } });
1809
+ }
1810
+ }
1811
+
1812
+ if (parts.length === 0) {
1813
+ dropped++;
1814
+ continue;
1815
+ }
1816
+ const filtered = filterThinking ? (filterThinkingBlocks(parts) as GeminiPart[]) : parts;
1817
+ if (filtered.length === 0) {
1818
+ dropped++;
1819
+ continue;
1820
+ }
1821
+ contents.push({ role: "model", parts: filtered });
1822
+ continue;
1823
+ }
1824
+
1825
+ // USER (default)
1826
+ {
1827
+ const parts = contentPartsToGeminiParts(msg.content as any);
1828
+ if (parts.length === 0) {
1829
+ dropped++;
1830
+ continue;
1831
+ }
1832
+ const filtered = filterThinking ? (filterThinkingBlocks(parts) as GeminiPart[]) : parts;
1833
+ if (filtered.length === 0) {
1834
+ dropped++;
1835
+ continue;
1836
+ }
1837
+ contents.push({ role: "user", parts: filtered });
1838
+ }
1839
+ }
1840
+
1841
+ // Merge consecutivo mesmo role
1842
+ let finalContents = contents;
1843
+ if (mergeConsecutive && contents.length > 1) {
1844
+ const merged: GeminiContent[] = [];
1845
+ let cur: GeminiContent | null = null;
1846
+ for (const c of contents) {
1847
+ if (cur && cur.role === c.role) {
1848
+ cur.parts.push(...c.parts);
1849
+ } else {
1850
+ if (cur) merged.push(cur);
1851
+ cur = { role: c.role, parts: [...c.parts] };
1852
+ }
1853
+ }
1854
+ if (cur) merged.push(cur);
1855
+ finalContents = merged;
1856
+ }
1857
+
1858
+ const result: NormalizedMessagesResult = {
1859
+ contents: finalContents,
1860
+ droppedCount: dropped,
1861
+ };
1862
+
1863
+ if (systemParts.length > 0) {
1864
+ result.systemInstruction = { role: "system", parts: systemParts };
1865
+ }
1866
+
1867
+ return result;
1868
+ }
1869
+
1870
+ // ---------------------------------------------------------------------------
1871
+ // Merged: message normalization (incoming shapes to Gemini messages)
1872
+ // ---------------------------------------------------------------------------
1873
+
1874
+ /**
1875
+ * Normalizes any role spelling onto the Gemini system/user/model triad.
1876
+ * Tool-ish roles map to user because Cloud Code Assist expects
1877
+ * functionResponse parts inside a user turn.
1878
+ *
1879
+ * @param rawRole - incoming role string
1880
+ */
1881
+ export function normalizeRole(rawRole: string): "system" | "user" | "model" {
1882
+ if (typeof rawRole !== "string") return "user";
1883
+ const l = rawRole.trim().toLowerCase();
1884
+ switch (l) {
1885
+ case "system":
1886
+ case "developer":
1887
+ case "system_instruction":
1888
+ case "instruction":
1889
+ case "sys":
1890
+ return "system";
1891
+ case "assistant":
1892
+ case "model":
1893
+ case "ai":
1894
+ case "bot":
1895
+ case "agent":
1896
+ case "assistant_model":
1897
+ return "model";
1898
+ case "user":
1899
+ case "human":
1900
+ case "end_user":
1901
+ return "user";
1902
+ case "tool":
1903
+ case "function":
1904
+ case "tool_result":
1905
+ case "tool_response":
1906
+ case "function_response":
1907
+ case "function_result":
1908
+ case "tool_result_content":
1909
+ return "user";
1910
+ default:
1911
+ return "user";
1912
+ }
1913
+ }
1914
+
1915
+ /**
1916
+ * Normalizes one content element (string, block array or object) into Gemini
1917
+ * parts. Skips thinking blocks, degrades images to markers and converts
1918
+ * tool_use/tool_result blocks into functionCall/functionResponse parts.
1919
+ *
1920
+ * @param content - incoming content of any shape
1921
+ */
1922
+ export function normalizeContentToParts(content: unknown): GeminiPart[] {
1923
+ const parts: GeminiPart[] = [];
1924
+
1925
+ if (content == null) return parts;
1926
+
1927
+ if (typeof content === "string") {
1928
+ if (content.trim() !== "") parts.push({ text: content });
1929
+ return parts;
1930
+ }
1931
+
1932
+ if (Array.isArray(content)) {
1933
+ for (const blk of content) {
1934
+ if (Array.isArray(blk)) {
1935
+ parts.push(...normalizeContentToParts(blk));
1936
+ continue;
1937
+ }
1938
+ if (typeof blk === "string") {
1939
+ if (blk.trim() !== "") parts.push({ text: blk });
1940
+ continue;
1941
+ }
1942
+ if (!isPlainObject(blk)) continue;
1943
+ const b = blk as Record<string, unknown>;
1944
+
1945
+ if (isThinkingBlock(b)) continue;
1946
+
1947
+ if (b.type === "text" && typeof b.text === "string") {
1948
+ parts.push({ text: b.text as string });
1949
+ continue;
1950
+ }
1951
+ if (typeof b.text === "string" && (b.type === undefined || (b as any).type === "text")) {
1952
+ if (!(b as any).thought) parts.push({ text: b.text as string });
1953
+ continue;
1954
+ }
1955
+ if (b.type === "image_url" || b.type === "image") {
1956
+ parts.push({ text: "[image]" });
1957
+ continue;
1958
+ }
1959
+ if (b.type === "tool_use") {
1960
+ const nameRaw = (b as any).name ?? (b as any).tool_name ?? "unknown_tool";
1961
+ try {
1962
+ const name = sanitizeToolName(String(nameRaw));
1963
+ const input = (b as any).input ?? (b as any).arguments ?? {};
1964
+ parts.push({
1965
+ functionCall: {
1966
+ name,
1967
+ args: isPlainObject(input) ? (input as Record<string, unknown>) : {},
1968
+ },
1969
+ });
1970
+ } catch {
1971
+ // Skip invalid tool_use blocks
1972
+ }
1973
+ continue;
1974
+ }
1975
+ if (b.type === "tool_result" || b.type === "tool_response" || b.type === "function_response") {
1976
+ const nameRaw = (b as any).name ?? (b as any).tool_name ?? (b as any).tool_use_id ?? "tool";
1977
+ try {
1978
+ const name = sanitizeToolName(String(nameRaw));
1979
+ let respContent = (b as any).content;
1980
+ if (Array.isArray(respContent)) {
1981
+ respContent = respContent
1982
+ .map((c: any) => (typeof c === "string" ? c : (c.text ?? JSON.stringify(c))))
1983
+ .join("\n");
1984
+ } else if (typeof respContent !== "string") {
1985
+ respContent = JSON.stringify(respContent ?? {});
1986
+ }
1987
+ parts.push({
1988
+ functionResponse: {
1989
+ name,
1990
+ response: { result: String(respContent).slice(0, 20000) },
1991
+ },
1992
+ });
1993
+ } catch {
1994
+ // Skip invalid tool_result blocks
1995
+ }
1996
+ continue;
1997
+ }
1998
+ if ((b as any).functionCall) {
1999
+ parts.push(b as unknown as GeminiPart);
2000
+ continue;
2001
+ }
2002
+ if ((b as any).functionResponse) {
2003
+ parts.push(b as unknown as GeminiPart);
2004
+ continue;
2005
+ }
2006
+ if (typeof b.text === "string") {
2007
+ parts.push({ text: b.text });
2008
+ }
2009
+ }
2010
+ return parts;
2011
+ }
2012
+
2013
+ if (isPlainObject(content)) {
2014
+ const c = content as Record<string, unknown>;
2015
+ if (typeof c.text === "string" && c.text.trim() !== "") {
2016
+ parts.push({ text: c.text });
2017
+ }
2018
+ return parts;
2019
+ }
2020
+
2021
+ return parts;
2022
+ }
2023
+
2024
+ /**
2025
+ * Gemini message with normalized role plus raw-role provenance.
2026
+ */
2027
+ export interface GeminiMessage {
2028
+ role: "system" | "user" | "model";
2029
+ parts: GeminiPart[];
2030
+ _rawRole?: string;
2031
+ }
2032
+
2033
+ /**
2034
+ * Generic incoming OpenCode / OpenAI / Anthropic message shape.
2035
+ */
2036
+ export interface IncomingMessage {
2037
+ role: string;
2038
+ content?: string | unknown[] | Record<string, unknown> | null;
2039
+ name?: string;
2040
+ tool_calls?: Array<{
2041
+ id?: string;
2042
+ type?: string;
2043
+ function?: { name: string; arguments?: string | Record<string, unknown> };
2044
+ }>;
2045
+ tool_call_id?: string;
2046
+ tool_use_id?: string;
2047
+ [k: string]: unknown;
2048
+ }
2049
+
2050
+ /**
2051
+ * Normalizes incoming OpenCode / OpenAI / Anthropic messages into Gemini
2052
+ * messages: role mapping via normalizeRole, content via normalizeContentToParts,
2053
+ * OpenAI tool_calls converted to functionCall parts, thinking blocks filtered,
2054
+ * consecutive same-role messages merged (except system).
2055
+ *
2056
+ * Named `normalizeIncomingMessages` because the historical `normalizeMessages`
2057
+ * keeps its original signature in this module.
2058
+ *
2059
+ * @param messages - array of incoming messages of any shape
2060
+ * @throws {RequestHelpersError} when input is not an array
2061
+ */
2062
+ export function normalizeIncomingMessages(messages: unknown): GeminiMessage[] {
2063
+ if (!Array.isArray(messages)) {
2064
+ throw new RequestHelpersError(`normalizeMessages expects array, got ${typeof messages}`, "INVALID_MESSAGES");
2065
+ }
2066
+
2067
+ const out: GeminiMessage[] = [];
2068
+
2069
+ for (const raw of messages) {
2070
+ if (!isPlainObject(raw)) continue;
2071
+ const msg = raw as IncomingMessage;
2072
+ const roleRaw = typeof msg.role === "string" ? msg.role : "user";
2073
+ const normRole = normalizeRole(roleRaw);
2074
+
2075
+ let parts = normalizeContentToParts(msg.content as unknown);
2076
+
2077
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
2078
+ for (const tc of msg.tool_calls) {
2079
+ try {
2080
+ const fn = tc.function ?? (tc as any);
2081
+ const nameRaw = (fn as any).name ?? tc.id ?? "tool";
2082
+ const name = sanitizeToolName(String(nameRaw));
2083
+ let args: Record<string, unknown> = {};
2084
+ const argsRaw = (fn as any).arguments;
2085
+ if (typeof argsRaw === "string") {
2086
+ try {
2087
+ const parsed = JSON.parse(argsRaw);
2088
+ if (isPlainObject(parsed)) args = parsed as Record<string, unknown>;
2089
+ } catch {
2090
+ // Leave empty args when not JSON
2091
+ }
2092
+ } else if (isPlainObject(argsRaw)) {
2093
+ args = argsRaw as Record<string, unknown>;
2094
+ }
2095
+ parts.push({ functionCall: { name, args } });
2096
+ } catch {
2097
+ continue;
2098
+ }
2099
+ }
2100
+ }
2101
+
2102
+ if (parts.length === 0) {
2103
+ if (normRole === "system") continue;
2104
+ continue;
2105
+ }
2106
+
2107
+ parts = parts.filter((p) => !isThinkingBlock(p));
2108
+
2109
+ if (parts.length === 0) continue;
2110
+
2111
+ out.push({
2112
+ role: normRole,
2113
+ parts,
2114
+ _rawRole: roleRaw,
2115
+ });
2116
+ }
2117
+
2118
+ const merged: GeminiMessage[] = [];
2119
+ for (const cur of out) {
2120
+ const last = merged[merged.length - 1];
2121
+ if (last && last.role === cur.role && cur.role !== "system") {
2122
+ last.parts.push(...cur.parts);
2123
+ } else {
2124
+ merged.push(cur);
2125
+ }
2126
+ }
2127
+
2128
+ return merged;
2129
+ }
2130
+
2131
+ // ============================================================================
2132
+ // 09. enhanceAgySdkErrorResponse()
2133
+ // ============================================================================
2134
+
2135
+ export interface EnhancedAgyError {
2136
+ message: string;
2137
+ code?: string | undefined;
2138
+ status?: number | undefined;
2139
+ statusText?: string | undefined;
2140
+ details?: unknown | undefined;
2141
+ isRetryable: boolean;
2142
+ isAuthError: boolean;
2143
+ isQuotaError: boolean;
2144
+ isModelError: boolean;
2145
+ isThinkingBudgetError: boolean;
2146
+ original: unknown;
2147
+ suggestion?: string | undefined;
2148
+ requestId?: string | undefined;
2149
+ modelId?: string | undefined;
2150
+ }
2151
+
2152
+ function extractJsonFromText(text: string): Record<string, unknown> | null {
2153
+ try {
2154
+ return JSON.parse(text) as Record<string, unknown>;
2155
+ } catch {
2156
+ // tenta extrair json embebido
2157
+ const m = text.match(/\{[\s\S]*\}/);
2158
+ if (m) {
2159
+ try {
2160
+ return JSON.parse(m[0]) as Record<string, unknown>;
2161
+ } catch {
2162
+ return null;
2163
+ }
2164
+ }
2165
+ return null;
2166
+ }
2167
+ }
2168
+
2169
+ /**
2170
+ * Enriquece erro vindo do Agentic SDK / Cloud Code Assist.
2171
+ * Detecta auth, quota, model not found, thinking budget etc e sugere correção.
2172
+ *
2173
+ * @param error - erro bruto (Error, Response, objeto, string)
2174
+ * @param context - contexto opcional com modelId, requestId etc
2175
+ */
2176
+ export function enhanceAgySdkErrorResponse(
2177
+ error: unknown,
2178
+ context?: { modelId?: string; requestId?: string },
2179
+ ): EnhancedAgyError {
2180
+ let message = "Unknown Agentic SDK error";
2181
+ let code: string | undefined;
2182
+ let status: number | undefined;
2183
+ let statusText: string | undefined;
2184
+ let details: unknown = undefined;
2185
+ let requestId = context?.requestId;
2186
+ let modelId = context?.modelId;
2187
+
2188
+ // Normaliza entrada
2189
+ if (typeof error === "string") {
2190
+ message = error;
2191
+ const parsed = extractJsonFromText(error);
2192
+ if (parsed) {
2193
+ details = parsed;
2194
+ if (typeof (parsed as any).message === "string") message = (parsed as any).message;
2195
+ if (typeof (parsed as any).error === "string") message = (parsed as any).error;
2196
+ if (typeof (parsed as any).code === "string") code = (parsed as any).code;
2197
+ // @ts-ignore
2198
+ if ((parsed as any).error?.message) message = (parsed as any).error.message;
2199
+ // @ts-ignore
2200
+ if ((parsed as any).error?.code) code = (parsed as any).error.code;
2201
+ // @ts-ignore
2202
+ if ((parsed as any).error?.status) code = (parsed as any).error.status;
2203
+ }
2204
+ } else if (error instanceof Error) {
2205
+ message = error.message || message;
2206
+ details = { name: error.name, stack: error.stack?.split("\n").slice(0, 5).join("\n") };
2207
+ // tenta extrair status de propriedades comuns
2208
+ const anyErr = error as any;
2209
+ if (typeof anyErr.status === "number") status = anyErr.status;
2210
+ if (typeof anyErr.statusCode === "number") status = anyErr.statusCode;
2211
+ if (typeof anyErr.code === "string") code = anyErr.code;
2212
+ if (typeof anyErr.requestId === "string") requestId = anyErr.requestId;
2213
+ } else if (isPlainObject(error)) {
2214
+ const obj = error as Record<string, unknown>;
2215
+ if (typeof obj["message"] === "string") message = obj["message"] as string;
2216
+ else if (typeof (obj["error"] as any)?.message === "string") message = (obj["error"] as any).message;
2217
+ else if (typeof obj["error"] === "string") message = obj["error"] as string;
2218
+
2219
+ if (typeof obj["code"] === "string") code = obj["code"] as string;
2220
+ else if (typeof (obj["error"] as any)?.code === "string") code = (obj["error"] as any).code;
2221
+ else if (typeof obj["status"] === "string") code = obj["status"] as string;
2222
+
2223
+ if (typeof obj["status"] === "number") status = obj["status"] as number;
2224
+ if (typeof obj["statusCode"] === "number") status = obj["statusCode"] as number;
2225
+ if (typeof obj["httpStatus"] === "number") status = obj["httpStatus"] as number;
2226
+
2227
+ if (typeof obj["statusText"] === "string") statusText = obj["statusText"] as string;
2228
+ if (typeof obj["requestId"] === "string") requestId = obj["requestId"] as string;
2229
+ if (typeof obj["modelId"] === "string") modelId = obj["modelId"] as string;
2230
+
2231
+ details = obj;
2232
+ } else {
2233
+ details = error;
2234
+ try {
2235
+ message = String(error);
2236
+ } catch {
2237
+ // keep default
2238
+ }
2239
+ }
2240
+
2241
+ const lowerMsg = message.toLowerCase();
2242
+ const lowerCode = (code ?? "").toLowerCase();
2243
+
2244
+ const isAuthError =
2245
+ status === 401 ||
2246
+ status === 403 ||
2247
+ lowerCode.includes("unauthenticated") ||
2248
+ lowerCode.includes("permission_denied") ||
2249
+ lowerCode.includes("forbidden") ||
2250
+ lowerMsg.includes("unauthenticated") ||
2251
+ lowerMsg.includes("permission denied") ||
2252
+ lowerMsg.includes("unauthorized") ||
2253
+ lowerMsg.includes("invalid authentication") ||
2254
+ lowerMsg.includes("requires authentication") ||
2255
+ (lowerMsg.includes("auth") && lowerMsg.includes("failed"));
2256
+
2257
+ const isQuotaError =
2258
+ status === 429 ||
2259
+ lowerCode.includes("resource_exhausted") ||
2260
+ lowerCode.includes("quota") ||
2261
+ lowerCode.includes("rate_limit") ||
2262
+ lowerMsg.includes("quota") ||
2263
+ lowerMsg.includes("rate limit") ||
2264
+ lowerMsg.includes("too many requests") ||
2265
+ lowerMsg.includes("resource exhausted") ||
2266
+ lowerMsg.includes("exceeded");
2267
+
2268
+ const isModelError =
2269
+ status === 404 ||
2270
+ lowerCode.includes("model_not_found") ||
2271
+ (lowerCode.includes("not_found") && lowerMsg.includes("model")) ||
2272
+ lowerMsg.includes("model not found") ||
2273
+ lowerMsg.includes("unknown model") ||
2274
+ lowerMsg.includes("model not supported") ||
2275
+ lowerMsg.includes("invalid model");
2276
+
2277
+ const isThinkingBudgetError =
2278
+ (lowerMsg.includes("thinking") && (lowerMsg.includes("budget") || lowerMsg.includes("token"))) ||
2279
+ lowerCode.includes("thinking_budget") ||
2280
+ lowerMsg.includes("thinkingBudget");
2281
+
2282
+ const isRetryable =
2283
+ status === 408 ||
2284
+ status === 429 ||
2285
+ status === 500 ||
2286
+ status === 502 ||
2287
+ status === 503 ||
2288
+ status === 504 ||
2289
+ lowerCode.includes("unavailable") ||
2290
+ lowerCode.includes("deadline_exceeded") ||
2291
+ lowerCode.includes("aborted") ||
2292
+ lowerCode.includes("internal") ||
2293
+ (isQuotaError && status === 429); // quota 429 retryable com backoff
2294
+
2295
+ let suggestion: string | undefined;
2296
+ if (isAuthError) {
2297
+ suggestion = `Reautentique com OAuth: client ${GEMINI_CLI_OAUTH_CLIENT_ID}. Verifique token expirou, escopos ${"https://www.googleapis.com/auth/cloud-platform"}. Tente limpar cache em ~/.config/opencode/auth/ e rodar auth flow novamente. Headers: User-Agent=${GEMINI_CLI_USER_AGENT}, X-Goog-Api-Client=${X_GOOG_API_CLIENT}`;
2298
+ } else if (isQuotaError) {
2299
+ suggestion = `Quota excedida. Verifique retrieveUserQuotaSummary em ${CLOUDCODE_PA_BASE}/v1internal:retrieveUserQuotaSummary. Troque para modelo fallback ${PROJECT_FALLBACK} ou aguarde reset. Considere modelo alternativo: ${FETCH_AVAILABLE_MODELS_2026.filter((m) => !m.includes("claude")).join(", ")}`;
2300
+ } else if (isModelError) {
2301
+ suggestion = `Modelo ${modelId ?? "desconhecido"} não encontrado. Modelos válidos 2026: ${FETCH_AVAILABLE_MODELS_2026.join(", ")}. Verifique se precisa de prefixo antigravity- e endpoint ${CLOUDCODE_PA_BASE}/v1internal. Para Claude use isLikelyAntigravityOnlyModel() para decidir roteamento.`;
2302
+ } else if (isThinkingBudgetError) {
2303
+ suggestion = `Thinking budget inválido. Use buildGenerationConfig() com thinkingLevel minimal/low/medium/high. Para Gemini: minimal=0, low=1024, medium=4096, high=8192. Para Claude: minimal=1024, low=2048, medium=8192, high=16384. Verifique modelo suporta thinking (claude-opus thinking ou gemini-3 com thinkingConfig).`;
2304
+ } else if (isRetryable) {
2305
+ suggestion = `Erro transitório (${status ?? code}). Tente novamente com backoff exponencial (500ms base, jitter). Endpoint: ${CLOUDCODE_PA_BASE}/v1internal. Project fallback: ${PROJECT_FALLBACK}.`;
2306
+ }
2307
+
2308
+ // Anonimiza secret em mensagem
2309
+ const safeMessage = message.replace(/GOCSPX-[A-Za-z0-9\-_]+/g, "[REDACTED_SECRET]");
2310
+
2311
+ return {
2312
+ message: safeMessage,
2313
+ code,
2314
+ status,
2315
+ statusText,
2316
+ details,
2317
+ isRetryable,
2318
+ isAuthError,
2319
+ isQuotaError,
2320
+ isModelError,
2321
+ isThinkingBudgetError,
2322
+ original: error,
2323
+ suggestion,
2324
+ requestId,
2325
+ modelId,
2326
+ };
2327
+ }
2328
+
2329
+ // ---------------------------------------------------------------------------
2330
+ // Core: transformRequest — OpenCode → Gemini normalized
2331
+ // ---------------------------------------------------------------------------
2332
+
2333
+ /**
2334
+ * Transforms OpenCode request (OpenAI/Anthropic mixed) into Gemini/Antigravity
2335
+ * normalized request ready for cloudcode-pa envelope.
2336
+ *
2337
+ * Steps performed by third-person observer:
2338
+ * - Validates model, messages
2339
+ * - Parses model variant to extract thinkingLevel/budget
2340
+ * - Aggregates system instructions
2341
+ * - Normalizes contents: user ↔ model, maps thinking blocks first,
2342
+ * handles tool_use/tool_result pairing, handles OpenAI tool_calls
2343
+ * - Cleans tool schemas, sanitizes tool names, ensures uniqueness
2344
+ * - Injects google_search tool when enabled
2345
+ * - Generates deterministic session_id and user_prompt_id via FNV-1a
2346
+ * - Builds generationConfig with thinking budget if applicable
2347
+ * - Supports 2026 models aliasing
2348
+ *
2349
+ * @param input - transform input
2350
+ * @returns transformed request
2351
+ * @throws RequestTransformationError on invalid input
2352
+ */
2353
+
2354
+ // ---------------------------------------------------------------------------
2355
+ // Build envelope for cloudcode-pa — endpoint cascade, headers, body
2356
+ // ---------------------------------------------------------------------------
2357
+
2358
+ // ---------------------------------------------------------------------------
2359
+ // Convenience: fetch with cascade (optional helper for plugin.ts integration)
2360
+ // ---------------------------------------------------------------------------
2361
+
2362
+ // ---------------------------------------------------------------------------
2363
+ // Merged: Gemini CLI bypass constants (canonical values from legacy module)
2364
+ // ---------------------------------------------------------------------------
2365
+
2366
+ // v2.1.15 Phase C: the local GEMINI_CLI_OAUTH_CLIENT_ID/SECRET assembled
2367
+ // pair was byte-identical to constants.js — deleted; constants.js is the
2368
+ // single owner of the gemini-cli client pair.
2369
+
2370
+ // v2.1.16: two orphaned bare literals (the full GeminiCLI UA string and the
2371
+ // IDE_UNSPECIFIED Client-Metadata string) were deleted — unbound top-level
2372
+ // expression statements left behind by the v2.1.15 merge, dead residue with
2373
+ // no runtime effect. The live values are the constants.js imports above
2374
+ // (GEMINI_CLI_USER_AGENT / CLIENT_METADATA_STRING).
2375
+
2376
+ /** X-Goog-Api-Client value observed on real Gemini CLI traffic. */
2377
+
2378
+ /** Client metadata object injected by Gemini CLI for bypass requests. */
2379
+
2380
+ /** Fallback project id when no cloud project is resolved. */
2381
+
2382
+ /** Production Cloud Code Assist base endpoint. */
2383
+
2384
+ /** Daily (canary) Cloud Code Assist base endpoint — default for pure Gemini CLI bypass. */
2385
+
2386
+ /** v1internal method suffixes observed on Cloud Code Assist traffic.
2387
+ * v2.1.15 Phase A: renamed from the historical local name
2388
+ * CODE_ASSIST_ENDPOINTS (a method-path map) so the host-map semantic of that
2389
+ * name resolves to the models.ts owner; re-exported below for surface
2390
+ * continuity. */
2391
+ export const CODE_ASSIST_METHOD_PATHS = {
2392
+ GENERATE: "/v1internal:generateContent",
2393
+ STREAM_GENERATE: "/v1internal:streamGenerateContent",
2394
+ LOAD: "/v1internal:loadCodeAssist",
2395
+ } as const;
2396
+
2397
+ /** Cloud Code Assist HOST map (canonical owner: models.ts).
2398
+ * @deprecated legacy alias kept for the historical request-module surface. */
2399
+ export { CODE_ASSIST_ENDPOINTS } from "./models.js";
2400
+
2401
+ /**
2402
+ * Union of model ids listed in {@link FETCH_AVAILABLE_MODELS_2026}.
2403
+ */
2404
+ // v2.1.15 Phase A: the local Model2026 alias was deleted — models.ts owns
2405
+ // the Model2026 family name.
2406
+
2407
+ // ---------------------------------------------------------------------------
2408
+ // Merged: FNV-1a 64-bit — longer deterministic digests
2409
+ // ---------------------------------------------------------------------------
2410
+
2411
+ const FNV64_OFFSET_BASIS = BigInt("14695981039346656037");
2412
+ const FNV64_PRIME = BigInt("1099511628211");
2413
+ const FNV64_MASK = BigInt("0xffffffffffffffff");
2414
+
2415
+ /**
2416
+ * Computes FNV-1a 64-bit hash over UTF-8 bytes.
2417
+ * Complements the 32-bit variant when longer deterministic digests are needed.
2418
+ *
2419
+ * @param input - string to hash
2420
+ * @returns unsigned 64-bit integer as bigint
2421
+ */
2422
+ export function fnv1a64BigInt(input: string): bigint {
2423
+ let hash: bigint = FNV64_OFFSET_BASIS;
2424
+ const buf = Buffer.from(input, "utf8");
2425
+ for (let i = 0; i < buf.length; i++) {
2426
+ hash ^= BigInt(buf[i]!);
2427
+ hash = (hash * FNV64_PRIME) & FNV64_MASK;
2428
+ }
2429
+ return hash;
2430
+ }
2431
+
2432
+ function toHex8(n: number): string {
2433
+ return (n >>> 0).toString(16).padStart(8, "0");
2434
+ }
2435
+
2436
+ function toHex16(n: bigint): string {
2437
+ return n.toString(16).padStart(16, "0");
2438
+ }
2439
+
2440
+ // ---------------------------------------------------------------------------
2441
+ // Merged: deterministic session / prompt ids keyed by directory
2442
+ // ---------------------------------------------------------------------------
2443
+
2444
+ /**
2445
+ * Options controlling {@link buildUserPromptId} output shape.
2446
+ */
2447
+ export interface BuildPromptIdOptions {
2448
+ /** Custom prefix, defaults to "agyp_". */
2449
+ prefix?: string;
2450
+ /** Extra seed for disambiguation, e.g. projectId or sessionId. */
2451
+ seed?: string;
2452
+ /** Include timestamp? Breaks determinism, discouraged but supported. */
2453
+ includeTimestamp?: boolean;
2454
+ /** Hash length: "short" yields 8 hex chars, "long" adds a 12-char 64-bit suffix. */
2455
+ length?: "short" | "long";
2456
+ }
2457
+
2458
+ /**
2459
+ * Generates a deterministic session_id via FNV-1a over the working directory.
2460
+ * Same directory implies same session_id, which keeps fingerprints stable.
2461
+ *
2462
+ * @param directory - optional directory; falls back to cwd, then home dir
2463
+ * @returns session id like `sess_1a2b3c4d5e6f7a8b9c0d`
2464
+ */
2465
+ export function buildSessionIdFromDir(directory?: string | null): string {
2466
+ let dir = directory?.trim();
2467
+ if (!dir) {
2468
+ try {
2469
+ dir = process.cwd();
2470
+ } catch {
2471
+ dir = os.homedir() || FALLBACK_PROJECT_ID;
2472
+ }
2473
+ }
2474
+ // Normalize separators so Windows and POSIX paths hash identically
2475
+ dir = stripTrailingSlashes(dir.replace(/\\/g, "/"));
2476
+ const h = fnv1a32(dir);
2477
+ return `sess_${toHex8(h)}${toHex8(fnv1a32(dir.split("").reverse().join("")))}`.slice(0, 20);
2478
+ }
2479
+
2480
+ /**
2481
+ * Generates a deterministic user_prompt_id via FNV-1a.
2482
+ * Used by the Cloud Code backend for deduplication.
2483
+ *
2484
+ * @param input - prompt content; objects are JSON.stringify-ed; omitted input
2485
+ * falls back to hostname|cwd|pid for per-workspace stability
2486
+ * @param opts - options object or bare seed string
2487
+ * @returns id like `agyp_1a2b3c4d` or `agyp_1a2b3c4d-9f8e7d6c5b4a`
2488
+ */
2489
+ export function buildUserPromptId(
2490
+ input?: string | object | null | undefined,
2491
+ opts?: BuildPromptIdOptions | string,
2492
+ ): string {
2493
+ let options: BuildPromptIdOptions = {};
2494
+ if (typeof opts === "string") options = { seed: opts };
2495
+ else if (opts && typeof opts === "object") options = opts;
2496
+
2497
+ const prefix = options.prefix ?? "agyp_";
2498
+ const length = options.length ?? "short";
2499
+
2500
+ let baseStr: string;
2501
+ if (input == null) {
2502
+ try {
2503
+ baseStr = `${os.hostname()}|${process.cwd()}|${process.pid}`;
2504
+ } catch {
2505
+ baseStr = `fallback|${FALLBACK_PROJECT_ID}`;
2506
+ }
2507
+ } else if (typeof input === "string") baseStr = input;
2508
+ else {
2509
+ try {
2510
+ baseStr = JSON.stringify(input);
2511
+ } catch {
2512
+ baseStr = String(input);
2513
+ }
2514
+ }
2515
+
2516
+ if (options.seed) baseStr = `${baseStr}|${options.seed}`;
2517
+ if (options.includeTimestamp) baseStr = `${baseStr}|${Date.now()}`;
2518
+
2519
+ const h32 = fnv1a32(baseStr);
2520
+ if (length === "short") return `${prefix}${toHex8(h32)}`;
2521
+
2522
+ const h64 = fnv1a64BigInt(baseStr);
2523
+ return `${prefix}${toHex8(h32)}-${toHex16(h64).slice(0, 12)}`;
2524
+ }
2525
+
2526
+ /**
2527
+ * Compatibility alias for fingerprint.ts: derives the session id from a directory.
2528
+ * Kept alongside {@link generateSessionId}, which hashes an arbitrary seed instead.
2529
+ *
2530
+ * @param directory - optional directory path
2531
+ * @returns deterministic session id
2532
+ */
2533
+ export function buildSessionId(directory?: string): string {
2534
+ return buildSessionIdFromDir(directory);
2535
+ }
2536
+
2537
+ // ---------------------------------------------------------------------------
2538
+ // Merged: thinking level normalization and budget mapping
2539
+ // ---------------------------------------------------------------------------
2540
+
2541
+ // v2.1.14 merge: request.ts's duplicate short THINKING_LEVEL_TO_BUDGET_* maps
2542
+ // (see constants above) and its normalizeThinkingLevel-based
2543
+ // mapThinkingLevelToBudget were dropped in favor of the richer
2544
+ // request-helpers.ts versions merged below.
2545
+
2546
+ /**
2547
+ * Normalizes free-form thinking input (string, options object, or message field)
2548
+ * into a canonical level. Unknown values collapse to "medium"; explicit
2549
+ * none/off inputs yield "none".
2550
+ *
2551
+ * @param input - raw thinking spec
2552
+ * @returns canonical level including "none"
2553
+ */
2554
+ export function normalizeThinkingLevel(input: unknown): ThinkingLevel | "none" {
2555
+ if (!input) return "medium";
2556
+ let level: string;
2557
+
2558
+ if (typeof input === "string") level = input;
2559
+ else if (typeof input === "object" && input !== null) {
2560
+ const obj = input as any;
2561
+ if (typeof obj.level === "string") level = obj.level;
2562
+ else if (typeof obj.thinkingLevel === "string") level = obj.thinkingLevel;
2563
+ else if (typeof obj.thinking_level === "string") level = obj.thinking_level;
2564
+ else level = "medium";
2565
+ } else level = "medium";
2566
+
2567
+ level = level.toLowerCase().trim();
2568
+ if (level === "low" || level === "minimal" || level === "none") return level;
2569
+ if (level === "high" || level === "medium") return level;
2570
+ // Map common variations onto canonical levels
2571
+ if (["1", "small", "light"].includes(level)) return "low";
2572
+ if (["2", "default", "balanced"].includes(level)) return "medium";
2573
+ if (["3", "large", "deep", "max"].includes(level)) return "high";
2574
+ return "medium";
2575
+ }
2576
+
2577
+ // ---------------------------------------------------------------------------
2578
+ // Merged: thinking level parsing and budget mapping
2579
+ // ---------------------------------------------------------------------------
2580
+
2581
+ /**
2582
+ * Parses free-form input into a canonical thinking level. Also maps
2583
+ * none/off onto minimal for compatibility.
2584
+ *
2585
+ * @param input - level-like value
2586
+ * @returns canonical level or undefined when not parseable
2587
+ */
2588
+ export function parseThinkingLevel(input: unknown): ThinkingLevel | undefined {
2589
+ if (typeof input !== "string") return undefined;
2590
+ const l = input.trim().toLowerCase();
2591
+ if ((THINKING_LEVELS as readonly string[]).includes(l)) {
2592
+ return l as ThinkingLevel;
2593
+ }
2594
+ if (l === "none" || l === "off") return "minimal";
2595
+ return undefined;
2596
+ }
2597
+
2598
+ /**
2599
+ * Maps a thinking level (or numeric budget) onto its token budget, clamping
2600
+ * numbers into [THINKING_BUDGET_MIN, THINKING_BUDGET_MAX]. Unknown named levels
2601
+ * safely fall back to medium.
2602
+ *
2603
+ * @param level - level name, numeric budget or numeric string
2604
+ * @throws {RequestHelpersError} on non-finite numbers or empty strings
2605
+ */
2606
+ export function mapThinkingLevelToBudget(level: string | number): number {
2607
+ if (typeof level === "number") {
2608
+ if (!Number.isFinite(level)) {
2609
+ throw new RequestHelpersError(`thinking budget number not finite: ${level}`, "INVALID_THINKING_BUDGET");
2610
+ }
2611
+ return Math.max(THINKING_BUDGET_MIN, Math.min(THINKING_BUDGET_MAX, Math.floor(level)));
2612
+ }
2613
+
2614
+ if (typeof level !== "string") {
2615
+ throw new RequestHelpersError(
2616
+ `thinking level must be string|number, got ${typeof level}`,
2617
+ "INVALID_THINKING_LEVEL",
2618
+ );
2619
+ }
2620
+
2621
+ const trimmed = level.trim().toLowerCase();
2622
+
2623
+ // Numeric strings act as direct budgets
2624
+ const asNum = Number(trimmed);
2625
+ if (trimmed !== "" && Number.isFinite(asNum) && /^\d+$/.test(trimmed)) {
2626
+ return Math.max(THINKING_BUDGET_MIN, Math.min(THINKING_BUDGET_MAX, Math.floor(asNum)));
2627
+ }
2628
+
2629
+ const parsed = parseThinkingLevel(trimmed);
2630
+ if (parsed === "minimal" || parsed === "low" || parsed === "medium" || parsed === "high" || parsed === "max") {
2631
+ return THINKING_BUDGET_MAP[parsed];
2632
+ }
2633
+
2634
+ if (trimmed === "") {
2635
+ throw new RequestHelpersError("empty thinking level", "INVALID_THINKING_LEVEL");
2636
+ }
2637
+
2638
+ return THINKING_BUDGET_MAP.medium;
2639
+ }
2640
+
2641
+ /**
2642
+ * Spec-required alias of {@link mapThinkingLevelToBudget}.
2643
+ */
2644
+ export const thinkingLevelToBudget = mapThinkingLevelToBudget;
2645
+
2646
+ /**
2647
+ * Budget hint for a parsed variant suffix.
2648
+ *
2649
+ * @param variant - "low", "max" or null
2650
+ */
2651
+ export function getThinkingBudgetForVariant(variant: "low" | "max" | null): number {
2652
+ if (variant === "low") return THINKING_BUDGET_MAP.low;
2653
+ if (variant === "max") return THINKING_BUDGET_MAP.max;
2654
+ return THINKING_BUDGET_MAP.medium;
2655
+ }
2656
+
2657
+ // ---------------------------------------------------------------------------
2658
+ // Merged: OpenCode request shapes and Cloud Code Assist payload types
2659
+ // ---------------------------------------------------------------------------
2660
+
2661
+ /**
2662
+ * Anthropic/OpenAI-style content block tolerated inside OpenCode messages.
2663
+ */
2664
+ export interface OpenCodeContentBlock {
2665
+ type?: string;
2666
+ text?: string;
2667
+ thinking?: string;
2668
+ level?: ThinkingLevel | string;
2669
+ thought?: string;
2670
+ id?: string;
2671
+ name?: string;
2672
+ input?: unknown;
2673
+ args?: unknown;
2674
+ tool_use_id?: string;
2675
+ tool_call_id?: string;
2676
+ content?: unknown;
2677
+ source?: { type?: string; media_type?: string; data?: string };
2678
+ [key: string]: unknown;
2679
+ }
2680
+
2681
+ /**
2682
+ * Full OpenCode request envelope with thinking/search/session passthrough fields.
2683
+ */
2684
+ export interface OpenCodeRequest {
2685
+ model?: string;
2686
+ messages?: OpenCodeMessage[];
2687
+ tools?: OpenCodeTool[];
2688
+ tool_choice?: unknown;
2689
+ thinking?:
2690
+ | ThinkingLevel
2691
+ | string
2692
+ | { level?: ThinkingLevel | string; budget?: number; type?: string; budget_tokens?: number };
2693
+ thinkingLevel?: ThinkingLevel | string;
2694
+ thinking_level?: ThinkingLevel | string;
2695
+ directory?: string;
2696
+ cwd?: string;
2697
+ project?: string;
2698
+ projectId?: string;
2699
+ temperature?: number;
2700
+ top_p?: number;
2701
+ topP?: number;
2702
+ top_k?: number;
2703
+ topK?: number;
2704
+ max_tokens?: number;
2705
+ maxTokens?: number;
2706
+ google_search?: boolean;
2707
+ enable_google_search?: boolean;
2708
+ enableGoogleSearch?: boolean;
2709
+ session_id?: string;
2710
+ user_prompt_id?: string;
2711
+ [key: string]: unknown;
2712
+ }
2713
+
2714
+ /**
2715
+ * System instruction shape accepted by Cloud Code Assist.
2716
+ */
2717
+ export interface GeminiSystemInstruction {
2718
+ role?: "system";
2719
+ parts: GeminiPart[];
2720
+ }
2721
+
2722
+ /**
2723
+ * Function declaration shape used in Gemini tools payloads.
2724
+ */
2725
+ export interface GeminiFunctionDeclaration {
2726
+ name: string;
2727
+ description?: string;
2728
+ parameters?: any;
2729
+ }
2730
+
2731
+ /**
2732
+ * Generation config including thinkingConfig with level/budget mapping.
2733
+ * v2.1.14 merge: unions the request.ts declaration (top-level thinking
2734
+ * compat fields) with the request-helpers.ts declaration (penalties,
2735
+ * responseMimeType and the index signature).
2736
+ */
2737
+ export interface GeminiGenerationConfig {
2738
+ temperature?: number;
2739
+ topP?: number;
2740
+ topK?: number;
2741
+ maxOutputTokens?: number;
2742
+ stopSequences?: string[];
2743
+ candidateCount?: number;
2744
+ presencePenalty?: number;
2745
+ frequencyPenalty?: number;
2746
+ thinkingConfig?: {
2747
+ thinkingLevel?: string;
2748
+ thinkingBudget?: number;
2749
+ includeThoughts?: boolean;
2750
+ };
2751
+ responseMimeType?: string;
2752
+ // Compat: some backend versions expect these at the top level too
2753
+ thinkingLevel?: string;
2754
+ thinkingBudget?: number;
2755
+ [k: string]: unknown;
2756
+ }
2757
+
2758
+ /**
2759
+ * Inner `request` field of the Cloud Code Assist v1internal envelope.
2760
+ */
2761
+ export interface CloudCodeAssistInnerRequest {
2762
+ contents: GeminiContent[];
2763
+ systemInstruction?: GeminiSystemInstruction;
2764
+ tools?: any[];
2765
+ toolConfig?: { functionCallingConfig?: { mode: string; allowedFunctionNames?: string[] } };
2766
+ generationConfig?: GeminiGenerationConfig;
2767
+ }
2768
+
2769
+ /**
2770
+ * Options bag accepted by the merged transform helpers.
2771
+ */
2772
+ export interface TransformOptions {
2773
+ projectId?: string;
2774
+ directory?: string;
2775
+ enableGoogleSearch?: boolean;
2776
+ thinkingLevel?: ThinkingLevel | string;
2777
+ model?: string;
2778
+ sessionId?: string;
2779
+ userPromptId?: string;
2780
+ temperature?: number;
2781
+ topP?: number;
2782
+ topK?: number;
2783
+ maxTokens?: number;
2784
+ }
2785
+
2786
+ /**
2787
+ * Result of {@link transformTools}: ready-made tools payload plus tool config.
2788
+ */
2789
+ export interface TransformToolsResult {
2790
+ tools: any[] | undefined;
2791
+ toolConfig?: { functionCallingConfig: { mode: string; allowedFunctionNames?: string[] } } | undefined;
2792
+ sanitizedMap: Record<string, string>;
2793
+ }
2794
+
2795
+ /**
2796
+ * Complete fetch payload for Antigravity / Cloud Code Assist endpoints.
2797
+ */
2798
+ export interface AntigravityFetchPayload {
2799
+ url: string;
2800
+ method: "POST";
2801
+ headers: Record<string, string>;
2802
+ body: {
2803
+ model: string;
2804
+ project: string;
2805
+ request: CloudCodeAssistInnerRequest;
2806
+ user_prompt_id: string;
2807
+ session_id: string;
2808
+ requestId?: string;
2809
+ };
2810
+ rawBody: string;
2811
+ model: string;
2812
+ project: string;
2813
+ user_prompt_id: string;
2814
+ session_id: string;
2815
+ isGeminiCLIOnly: boolean;
2816
+ }
2817
+
2818
+ // ---------------------------------------------------------------------------
2819
+ // Merged: generation config builder and bypass headers
2820
+ // ---------------------------------------------------------------------------
2821
+
2822
+ /**
2823
+ * Builds a GeminiGenerationConfig from an OpenCode request plus overrides.
2824
+ *
2825
+ * v2.1.14 merge note: renamed from buildGenerationConfig so the richer
2826
+ * options-object builder merged from request-helpers.ts can own that name.
2827
+ * Resolves the thinking level (override > opts > request > default) and maps
2828
+ * it onto the canonical budget map.
2829
+ *
2830
+ * @param req - OpenCode request with optional sampling/thinking fields
2831
+ * @param opts - transform-level overrides
2832
+ * @param thinkingLevelOverride - explicit level that wins over everything else
2833
+ * @returns generation config ready for CloudCodeAssistInnerRequest
2834
+ */
2835
+ export function buildGenerationConfigFromRequest(
2836
+ req: OpenCodeRequest,
2837
+ opts?: TransformOptions,
2838
+ thinkingLevelOverride?: ThinkingLevel,
2839
+ ): GeminiGenerationConfig {
2840
+ const thinkingLevel =
2841
+ thinkingLevelOverride ||
2842
+ normalizeThinkingLevel(
2843
+ opts?.thinkingLevel ??
2844
+ req.thinkingLevel ??
2845
+ req.thinking_level ??
2846
+ (req as any).thinking?.level ??
2847
+ req.thinking ??
2848
+ "medium",
2849
+ );
2850
+
2851
+ const thinkingBudget = mapThinkingLevelToBudget(thinkingLevel);
2852
+
2853
+ const temperature = opts?.temperature ?? req.temperature;
2854
+ const topP = opts?.topP ?? req.topP ?? req.top_p;
2855
+ const topK = opts?.topK ?? req.topK ?? req.top_k;
2856
+ const maxTokens = opts?.maxTokens ?? req.max_tokens ?? req.maxTokens;
2857
+
2858
+ const cfg: GeminiGenerationConfig = {};
2859
+
2860
+ if (typeof temperature === "number") cfg.temperature = temperature;
2861
+ if (typeof topP === "number") cfg.topP = topP;
2862
+ if (typeof topK === "number") cfg.topK = topK;
2863
+ if (typeof maxTokens === "number") cfg.maxOutputTokens = maxTokens;
2864
+
2865
+ cfg.thinkingConfig = {
2866
+ thinkingLevel,
2867
+ thinkingBudget,
2868
+ includeThoughts: true,
2869
+ };
2870
+ // Compat: some backend versions expect these fields at the top level too
2871
+ cfg.thinkingLevel = thinkingLevel;
2872
+ cfg.thinkingBudget = thinkingBudget;
2873
+
2874
+ return cfg;
2875
+ }
2876
+
2877
+ // ---------------------------------------------------------------------------
2878
+ // Merged from request-helpers.ts — canonical buildGenerationConfig (options
2879
+ // object with clamping, NaN guards and family-aware budget maps)
2880
+ // ---------------------------------------------------------------------------
2881
+
2882
+ export interface BuildGenerationConfigOptions {
2883
+ temperature?: number;
2884
+ topP?: number;
2885
+ topK?: number;
2886
+ maxTokens?: number; // alias para maxOutputTokens
2887
+ maxOutputTokens?: number;
2888
+ thinkingLevel?: ThinkingLevel;
2889
+ thinkingBudget?: number; // override direto
2890
+ includeThoughts?: boolean;
2891
+ modelId?: string;
2892
+ stopSequences?: string[];
2893
+ candidateCount?: number;
2894
+ presencePenalty?: number;
2895
+ frequencyPenalty?: number;
2896
+ // extras Gemini
2897
+ responseMimeType?: string;
2898
+ }
2899
+
2900
+ /**
2901
+ * Constrói GenerationConfig para cloudcode-pa v1internal.
2902
+ * Mapeia thinkingLevel (minimal/low/medium/high) para thinkingBudget.
2903
+ *
2904
+ * - Para modelos Gemini: minimal=0 (desativa), low=1024, medium=4096, high=8192
2905
+ * - Para modelos Claude (via Antigravity): budgets maiores
2906
+ * - Se thinkingBudget direto fornecido, usa direto
2907
+ * - Se thinkingLevel=none/minimal com includeThoughts=false, omite includeThoughts para economia
2908
+ *
2909
+ * @param opts - opções de geração
2910
+ * @returns config compatível com v1internal:generateContent
2911
+ */
2912
+ export function buildGenerationConfig(opts: BuildGenerationConfigOptions = {}): GeminiGenerationConfig {
2913
+ const config: GeminiGenerationConfig = {};
2914
+
2915
+ if (typeof opts.temperature === "number" && !Number.isNaN(opts.temperature)) {
2916
+ config.temperature = Math.max(0, Math.min(2, opts.temperature));
2917
+ }
2918
+ if (typeof opts.topP === "number" && !Number.isNaN(opts.topP)) {
2919
+ config.topP = Math.max(0, Math.min(1, opts.topP));
2920
+ }
2921
+ if (typeof opts.topK === "number" && !Number.isNaN(opts.topK)) {
2922
+ config.topK = Math.max(1, Math.floor(opts.topK));
2923
+ }
2924
+
2925
+ const maxT = opts.maxOutputTokens ?? opts.maxTokens;
2926
+ if (typeof maxT === "number" && !Number.isNaN(maxT)) {
2927
+ config.maxOutputTokens = Math.max(1, Math.floor(maxT));
2928
+ }
2929
+
2930
+ if (Array.isArray(opts.stopSequences) && opts.stopSequences.length > 0) {
2931
+ config.stopSequences = opts.stopSequences.filter((s) => typeof s === "string" && s.length > 0).slice(0, 5);
2932
+ }
2933
+
2934
+ if (typeof opts.candidateCount === "number") {
2935
+ config.candidateCount = Math.max(1, Math.min(8, Math.floor(opts.candidateCount)));
2936
+ }
2937
+
2938
+ if (opts.responseMimeType) config.responseMimeType = opts.responseMimeType;
2939
+
2940
+ // Pensamento / thinking
2941
+ const modelId = (opts.modelId ?? "").toLowerCase();
2942
+ const isClaude = modelId.includes("claude") || modelId.includes("opus") || modelId.includes("sonnet");
2943
+ const budgetMap = isClaude ? THINKING_LEVEL_TO_BUDGET_CLAUDE : THINKING_LEVEL_TO_BUDGET_GEMINI;
2944
+
2945
+ let budget: number | undefined;
2946
+ let levelNorm: string | undefined;
2947
+
2948
+ if (typeof opts.thinkingBudget === "number" && !Number.isNaN(opts.thinkingBudget)) {
2949
+ budget = Math.max(0, Math.floor(opts.thinkingBudget));
2950
+ levelNorm = opts.thinkingLevel?.toLowerCase?.() ?? (budget === 0 ? "none" : "custom");
2951
+ } else if (opts.thinkingLevel) {
2952
+ levelNorm = opts.thinkingLevel.toLowerCase().trim();
2953
+ if (levelNorm in budgetMap) {
2954
+ budget = budgetMap[levelNorm];
2955
+ } else {
2956
+ // fallback heurístico: tenta mapear sinônimos
2957
+ if (["off", "disable", "disabled", "zero"].includes(levelNorm)) {
2958
+ budget = 0;
2959
+ levelNorm = "none";
2960
+ } else if (["min", "minimum"].includes(levelNorm)) {
2961
+ budget = budgetMap["minimal"];
2962
+ levelNorm = "minimal";
2963
+ } else {
2964
+ // default medium se desconhecido
2965
+ budget = budgetMap["medium"];
2966
+ }
2967
+ }
2968
+ }
2969
+
2970
+ if (budget !== undefined) {
2971
+ config.thinkingConfig = {};
2972
+ if (levelNorm) config.thinkingConfig.thinkingLevel = levelNorm;
2973
+ config.thinkingConfig.thinkingBudget = budget;
2974
+
2975
+ // includeThoughts: só true se budget >0 ou explicitamente pedido
2976
+ // Para minimal/none com budget 0, força false para economizar
2977
+ let includeThoughts = opts.includeThoughts;
2978
+ if (includeThoughts === undefined) {
2979
+ includeThoughts = budget > 0;
2980
+ }
2981
+ config.thinkingConfig.includeThoughts = includeThoughts;
2982
+
2983
+ // Se for Gemini e budget 0 e includeThoughts false, podemos omitir thinkingConfig
2984
+ // mas mantemos para clareza, exceto se for none
2985
+ if (budget === 0 && !includeThoughts && (levelNorm === "none" || levelNorm === "minimal")) {
2986
+ // Para Gemini nativo, remover thinkingConfig quando desativa é válido, mas para Claude mantém 0
2987
+ if (!isClaude) {
2988
+ // mantém mesmo assim com budget 0 para explicitar desativação
2989
+ // não remove
2990
+ }
2991
+ }
2992
+ }
2993
+
2994
+ // presence/frequency - não são nativas Gemini, mas alguns proxies aceitam; mantemos se vierem
2995
+ if (typeof opts.presencePenalty === "number") config.presencePenalty = opts.presencePenalty;
2996
+ if (typeof opts.frequencyPenalty === "number") config.frequencyPenalty = opts.frequencyPenalty;
2997
+
2998
+ return config;
2999
+ }
3000
+
3001
+ /**
3002
+ * Builds the exact header set used by Gemini CLI bypass traffic:
3003
+ * User-Agent, X-Goog-Api-Client, Client-Metadata (header and mirrored
3004
+ * X-Goog-Client-Metadata), JSON content type, gzip accept-encoding, and an
3005
+ * optional Bearer authorization merged last so extras can override.
3006
+ *
3007
+ * @param token - optional OAuth token; "Bearer " prefix added when missing
3008
+ * @param extra - additional headers merged after the defaults
3009
+ * @returns headers record ready for fetch
3010
+ */
3011
+ export function buildBypassHeaders(token?: string, extra?: Record<string, string>): Record<string, string> {
3012
+ const h: Record<string, string> = {
3013
+ "User-Agent": GEMINI_CLI_USER_AGENT,
3014
+ "X-Goog-Api-Client": X_GOOG_API_CLIENT,
3015
+ "Client-Metadata": CLIENT_METADATA_STRING,
3016
+ "X-Goog-Client-Metadata": CLIENT_METADATA_STRING,
3017
+ "Content-Type": "application/json",
3018
+ Accept: "application/json",
3019
+ "Accept-Encoding": "gzip",
3020
+ };
3021
+ if (token && token.trim()) {
3022
+ const t = token.trim();
3023
+ h["Authorization"] = t.startsWith("Bearer ") ? t : `Bearer ${t}`;
3024
+ }
3025
+ if (extra) {
3026
+ for (const [k, v] of Object.entries(extra)) if (v) h[k] = v;
3027
+ }
3028
+ return h;
3029
+ }
3030
+
3031
+ // ---------------------------------------------------------------------------
3032
+ // Merged: tool schema cleaning (strict allowed-keys variant)
3033
+ // ---------------------------------------------------------------------------
3034
+
3035
+ /**
3036
+ * Cleans one JSON Schema for Gemini function declarations: drops every key not
3037
+ * present in {@link ALLOWED_SCHEMA_KEYS} (recursively), normalizes oneOf to
3038
+ * anyOf, filters required/enum arrays, and defaults type to object when
3039
+ * properties exist.
3040
+ *
3041
+ * @param schema - raw schema node of any shape
3042
+ * @returns cleaned schema (same reference type semantics as input primitives)
3043
+ */
3044
+ export function cleanToolSchema(schema: unknown): any {
3045
+ if (schema == null) return schema;
3046
+ if (typeof schema !== "object") return schema;
3047
+ if (Array.isArray(schema)) return schema.map(cleanToolSchema);
3048
+
3049
+ const src = schema as Record<string, unknown>;
3050
+ const out: Record<string, unknown> = {};
3051
+
3052
+ for (const [k, v] of Object.entries(src)) {
3053
+ if (!ALLOWED_SCHEMA_KEYS.has(k)) {
3054
+ // Allow recursion through anyOf/oneOf while dropping $schema/$ref/additionalProperties etc.
3055
+ if (k === "anyOf" || k === "oneOf") {
3056
+ // Normalize oneOf to anyOf for compatibility
3057
+ out["anyOf"] = Array.isArray(v) ? (v as unknown[]).map(cleanToolSchema) : cleanToolSchema(v);
3058
+ }
3059
+ continue;
3060
+ }
3061
+
3062
+ if (k === "properties" && v && typeof v === "object" && !Array.isArray(v)) {
3063
+ const props = v as Record<string, unknown>;
3064
+ const cleanedProps: Record<string, unknown> = {};
3065
+ for (const [propName, propSchema] of Object.entries(props)) {
3066
+ cleanedProps[propName] = cleanToolSchema(propSchema);
3067
+ }
3068
+ out[k] = cleanedProps;
3069
+ } else if (k === "items" && v && typeof v === "object") {
3070
+ out[k] = cleanToolSchema(v);
3071
+ } else if (k === "anyOf" && Array.isArray(v)) {
3072
+ out[k] = (v as unknown[]).map(cleanToolSchema);
3073
+ } else if (k === "required" && Array.isArray(v)) {
3074
+ out[k] = (v as string[]).filter((s) => typeof s === "string");
3075
+ } else if (k === "enum" && Array.isArray(v)) {
3076
+ out[k] = v;
3077
+ } else {
3078
+ out[k] = v;
3079
+ }
3080
+ }
3081
+
3082
+ // Default type to object when properties exist without a type
3083
+ if (out.properties && !out.type) out.type = "object";
3084
+
3085
+ return out;
3086
+ }
3087
+
3088
+ // ============================================================================
3089
+ // 04. cleanToolSchema() - limpeza de JSON Schema para Gemini
3090
+ // ============================================================================
3091
+
3092
+ const DROP_KEYS = new Set<string>([
3093
+ "$schema",
3094
+ "$id",
3095
+ "$comment",
3096
+ "$anchor",
3097
+ "$defs",
3098
+ "definitions",
3099
+ "examples",
3100
+ "example",
3101
+ "deprecated",
3102
+ "const", // Gemini não curte const em alguns ver, convertemos para enum
3103
+ "format", // mantém? Gemini aceita alguns formatos, mas por segurança remove se for json-specific
3104
+ ]);
3105
+
3106
+ const VALID_JSON_TYPES = new Set<string>(["string", "number", "integer", "boolean", "object", "array", "null"]);
3107
+
3108
+ /**
3109
+ * Normaliza campo `type`: se for array com null, mantém tipo principal
3110
+ * Se for array com múltiplos tipos, escolhe primeiro não-null
3111
+ */
3112
+ function normalizeTypeField(typeVal: unknown): string | undefined {
3113
+ if (typeof typeVal === "string") {
3114
+ const low = typeVal.toLowerCase();
3115
+ return VALID_JSON_TYPES.has(low) ? low : undefined;
3116
+ }
3117
+ if (Array.isArray(typeVal)) {
3118
+ const filtered = (typeVal as unknown[])
3119
+ .map((t) => (typeof t === "string" ? t.toLowerCase() : ""))
3120
+ .filter((t) => t && t !== "null" && VALID_JSON_TYPES.has(t));
3121
+ if (filtered.length > 0) return filtered[0];
3122
+ // fallback
3123
+ if ((typeVal as string[]).includes("string")) return "string";
3124
+ return undefined;
3125
+ }
3126
+ return undefined;
3127
+ }
3128
+
3129
+ export interface CleanToolSchemaOptions {
3130
+ maxDepth?: number;
3131
+ keepFormat?: boolean;
3132
+ keepDescription?: boolean;
3133
+ }
3134
+
3135
+ /**
3136
+ * Limpa e normaliza JSON Schema de tool para formato aceito por Gemini / Cloud Code Assist.
3137
+ *
3138
+ * O que faz:
3139
+ * - remove $schema, $id, examples, etc
3140
+ * - remove chaves undefined / null vazias
3141
+ * - normaliza type para lowercase e singular
3142
+ * - converte `const: X` para `enum: [X]`
3143
+ * - limpa recursivamente properties, items, anyOf/oneOf/allOf
3144
+ * - garante que object com properties tenha type=object
3145
+ * - remove propriedades vazias
3146
+ * - impede profundidade infinita (>20)
3147
+ *
3148
+ * v2.1.14 merge note: renamed from cleanToolSchema (request-helpers.ts) to
3149
+ * cleanToolSchemaTolerant; the strict allowlist cleaner from request.ts owns
3150
+ * the cleanToolSchema name.
3151
+ *
3152
+ * @param schema - schema bruto (any)
3153
+ * @param opts - opções
3154
+ * @returns schema limpo
3155
+ */
3156
+ export function cleanToolSchemaTolerant(schema: unknown, opts?: CleanToolSchemaOptions, _depth = 0): unknown {
3157
+ const maxDepth = opts?.maxDepth ?? 20;
3158
+ const depth = _depth;
3159
+
3160
+ if (depth > maxDepth) return schema;
3161
+ if (schema == null) return schema;
3162
+ if (typeof schema === "boolean") return schema;
3163
+
3164
+ if (Array.isArray(schema)) {
3165
+ return (schema as unknown[])
3166
+ .map((item) => cleanToolSchemaTolerant(item, opts, depth + 1))
3167
+ .filter((v) => v !== undefined);
3168
+ }
3169
+
3170
+ if (!isPlainObject(schema)) {
3171
+ return schema;
3172
+ }
3173
+
3174
+ const out: Record<string, unknown> = {};
3175
+
3176
+ for (const [rawKey, rawVal] of Object.entries(schema as Record<string, unknown>)) {
3177
+ if (rawVal === undefined) continue;
3178
+ if (DROP_KEYS.has(rawKey)) {
3179
+ // exceção: se keepFormat e key é format, mantém
3180
+ if (rawKey === "format" && opts?.keepFormat) {
3181
+ // mantém apenas formatos seguros para Gemini
3182
+ const fmt = String(rawVal).toLowerCase();
3183
+ if (["date-time", "date", "enum", "int32", "int64", "double", "float"].includes(fmt)) {
3184
+ out[rawKey] = fmt;
3185
+ }
3186
+ continue;
3187
+ }
3188
+ // exceção: description sempre mantém por padrão
3189
+ continue;
3190
+ }
3191
+
3192
+ if (rawKey === "type") {
3193
+ const normalized = normalizeTypeField(rawVal);
3194
+ if (normalized) out[rawKey] = normalized;
3195
+ // se type array incluía null, Gemini usa nullable via `nullable:true`? No Gemini mais novo usa type sem null, mas aceita null em array?
3196
+ // Vamos omitir null e não setar nullable para compat. Se precisar, poderia setar out['nullable']=true.
3197
+ continue;
3198
+ }
3199
+
3200
+ if (rawKey === "const") {
3201
+ // const => enum
3202
+ out["enum"] = [rawVal];
3203
+ continue;
3204
+ }
3205
+
3206
+ if (rawKey === "properties" && isPlainObject(rawVal)) {
3207
+ const cleanedProps: Record<string, unknown> = {};
3208
+ for (const [propKey, propVal] of Object.entries(rawVal as Record<string, unknown>)) {
3209
+ const cleaned = cleanToolSchemaTolerant(propVal, opts, depth + 1);
3210
+ if (cleaned !== undefined) cleanedProps[propKey] = cleaned;
3211
+ }
3212
+ if (Object.keys(cleanedProps).length > 0) out[rawKey] = cleanedProps;
3213
+ continue;
3214
+ }
3215
+
3216
+ if (rawKey === "items") {
3217
+ out[rawKey] = cleanToolSchemaTolerant(rawVal, opts, depth + 1);
3218
+ continue;
3219
+ }
3220
+
3221
+ if (rawKey === "additionalProperties") {
3222
+ if (typeof rawVal === "boolean") {
3223
+ out[rawKey] = rawVal;
3224
+ } else if (isPlainObject(rawVal)) {
3225
+ out[rawKey] = cleanToolSchemaTolerant(rawVal, opts, depth + 1);
3226
+ }
3227
+ continue;
3228
+ }
3229
+
3230
+ if (["anyOf", "oneOf", "allOf"].includes(rawKey) && Array.isArray(rawVal)) {
3231
+ const cleanedArr = (rawVal as unknown[])
3232
+ .map((v) => cleanToolSchemaTolerant(v, opts, depth + 1))
3233
+ .filter((v) => v !== undefined && v !== null) as unknown[];
3234
+ if (cleanedArr.length > 0) out[rawKey] = cleanedArr;
3235
+ continue;
3236
+ }
3237
+
3238
+ if (rawKey === "enum" && Array.isArray(rawVal)) {
3239
+ // filtra enums válidos, remove duplicados
3240
+ const uniq = Array.from(new Set((rawVal as unknown[]).filter((x) => x != null)));
3241
+ if (uniq.length > 0) out[rawKey] = uniq;
3242
+ continue;
3243
+ }
3244
+
3245
+ if (rawKey === "required" && Array.isArray(rawVal)) {
3246
+ const uniq = Array.from(
3247
+ new Set((rawVal as unknown[]).filter((x) => typeof x === "string" && (x as string).length > 0)),
3248
+ ) as string[];
3249
+ if (uniq.length > 0) out[rawKey] = uniq;
3250
+ continue;
3251
+ }
3252
+
3253
+ // Recursivo genérico para objetos
3254
+ if (isPlainObject(rawVal)) {
3255
+ const cleaned = cleanToolSchemaTolerant(rawVal, opts, depth + 1);
3256
+ if (
3257
+ cleaned !== undefined &&
3258
+ (typeof cleaned !== "object" || Object.keys(cleaned as object).length > 0 || Array.isArray(cleaned))
3259
+ ) {
3260
+ out[rawKey] = cleaned;
3261
+ }
3262
+ continue;
3263
+ }
3264
+
3265
+ if (Array.isArray(rawVal)) {
3266
+ // para arrays genéricos (ex: enum já tratado, mas outros)
3267
+ out[rawKey] = rawVal
3268
+ .map((v) => (isPlainObject(v) ? cleanToolSchemaTolerant(v, opts, depth + 1) : v))
3269
+ .filter((v) => v !== undefined);
3270
+ continue;
3271
+ }
3272
+
3273
+ // primitivos: string, number, boolean
3274
+ out[rawKey] = rawVal;
3275
+ }
3276
+
3277
+ // heurística: se tem properties mas não type, assume object
3278
+ if (out["properties"] && !out["type"]) {
3279
+ out["type"] = "object";
3280
+ }
3281
+ // se é object mas properties vazio e não tem additionalProperties, remove properties para evitar schema vazio que Gemini rejeita
3282
+ if (
3283
+ out["type"] === "object" &&
3284
+ out["properties"] &&
3285
+ isPlainObject(out["properties"]) &&
3286
+ Object.keys(out["properties"] as object).length === 0
3287
+ ) {
3288
+ delete out["properties"];
3289
+ }
3290
+
3291
+ return out;
3292
+ }
3293
+
3294
+ /**
3295
+ * Versão plural que limpa mapa de tools ou array de function declarations
3296
+ *
3297
+ * v2.1.14 merge note: canonical plural (superset of request.ts's array-only
3298
+ * variant; strict-array cleaning stays available via schemas.map(cleanToolSchema)).
3299
+ */
3300
+ export function cleanToolSchemas(tools: unknown, opts?: CleanToolSchemaOptions): unknown {
3301
+ if (Array.isArray(tools)) {
3302
+ return tools.map((t) => cleanToolSchemaTolerant(t, opts)).filter(Boolean);
3303
+ }
3304
+ if (isPlainObject(tools)) {
3305
+ // pode ser { functions: [...] } ou { name: { parameters: schema } }
3306
+ const obj = tools as Record<string, unknown>;
3307
+ if (obj["parameters"] || obj["properties"] || obj["type"]) {
3308
+ return cleanToolSchemaTolerant(obj, opts);
3309
+ }
3310
+ const cleaned: Record<string, unknown> = {};
3311
+ for (const [k, v] of Object.entries(obj)) {
3312
+ cleaned[k] = cleanToolSchemaTolerant(v, opts);
3313
+ }
3314
+ return cleaned;
3315
+ }
3316
+ return cleanToolSchemaTolerant(tools, opts);
3317
+ }
3318
+
3319
+ // ---------------------------------------------------------------------------
3320
+ // Merged: tool name validation
3321
+ // ---------------------------------------------------------------------------
3322
+
3323
+ /**
3324
+ * Checks whether a tool name already satisfies the Gemini naming rules.
3325
+ *
3326
+ * @param name - candidate name
3327
+ * @returns true when valid without sanitization
3328
+ */
3329
+ export function isValidToolName(name: string): boolean {
3330
+ return TOOL_NAME_REGEX.test(name);
3331
+ }
3332
+
3333
+ /**
3334
+ * Detailed result of {@link validateToolNames}.
3335
+ */
3336
+ export interface ValidateToolNamesResult {
3337
+ valid: string[];
3338
+ invalid: string[];
3339
+ sanitized: Record<string, string>;
3340
+ duplicates: string[];
3341
+ }
3342
+
3343
+ /**
3344
+ * Validates a list of tool names, reporting duplicates and computing sanitized
3345
+ * replacements for invalid entries.
3346
+ *
3347
+ * @param names - raw tool names
3348
+ * @returns valid/invalid partitions plus sanitized mapping
3349
+ */
3350
+ export function validateToolNames(names: string[]): ValidateToolNamesResult {
3351
+ const valid: string[] = [];
3352
+ const invalid: string[] = [];
3353
+ const sanitized: Record<string, string> = {};
3354
+ const seen = new Set<string>();
3355
+ const duplicates: string[] = [];
3356
+
3357
+ for (const raw of names) {
3358
+ if (!raw || typeof raw !== "string") {
3359
+ invalid.push(String(raw));
3360
+ continue;
3361
+ }
3362
+ if (seen.has(raw)) duplicates.push(raw);
3363
+ seen.add(raw);
3364
+
3365
+ if (isValidToolName(raw)) valid.push(raw);
3366
+ else {
3367
+ invalid.push(raw);
3368
+ sanitized[raw] = sanitizeToolName(raw);
3369
+ }
3370
+ }
3371
+ return { valid, invalid, sanitized, duplicates };
3372
+ }
3373
+
3374
+ // v2.1.14 merge: request.ts's delegating isLikelyAntigravityOnlyModel alias
3375
+ // (formerly directly below) was dropped; the richer standalone heuristic from
3376
+ // request-helpers.ts (see the model helpers section above) is canonical.
3377
+
3378
+ // ---------------------------------------------------------------------------
3379
+ // Merged: tool name validation
3380
+ // ---------------------------------------------------------------------------
3381
+
3382
+ /**
3383
+ * Validates one tool name against the Gemini naming rules.
3384
+ *
3385
+ * @param name - candidate name of any type
3386
+ */
3387
+ export function validateToolName(name: unknown): boolean {
3388
+ if (typeof name !== "string") return false;
3389
+ const t = name.trim();
3390
+ if (t.length < TOOL_NAME_MIN_LENGTH || t.length > TOOL_NAME_MAX_LENGTH) return false;
3391
+ return TOOL_NAME_REGEX.test(t);
3392
+ }
3393
+
3394
+ // ============================================================================
3395
+ // 05. validateToolNames()
3396
+ // ============================================================================
3397
+
3398
+ // Tool name validation constants (TOOL_NAME_REGEX, TOOL_NAME_MAX_LENGTH)
3399
+ // are module-wide exports defined in the constants section above.
3400
+
3401
+ export interface InvalidToolName {
3402
+ name: string;
3403
+ reason: string;
3404
+ }
3405
+
3406
+ export interface SanitizedToolName {
3407
+ original: string;
3408
+ sanitized: string;
3409
+ }
3410
+
3411
+ export interface ValidateToolNamesDetailedResult {
3412
+ valid: boolean;
3413
+ validNames: string[];
3414
+ invalid: InvalidToolName[];
3415
+ sanitized: SanitizedToolName[];
3416
+ allSanitizedValid: boolean;
3417
+ }
3418
+
3419
+ /**
3420
+ * Valida lista de nomes de tools. Gemini exige ^[A-Za-z_][A-Za-z0-9_]*$ e max 64.
3421
+ *
3422
+ * v2.1.14 merge note: renamed from validateToolNames (request-helpers.ts);
3423
+ * the canonical TOOL_NAME_REGEX (letter start + 64 cap, from request.ts)
3424
+ * now backs this validator.
3425
+ *
3426
+ * @param tools - array de string, array de {name}, ou Record<string,any>
3427
+ * @returns resultado com valid/invalid/sanitized
3428
+ */
3429
+ export function validateToolNamesDetailed(
3430
+ tools: Array<string | { name: string; [k: string]: unknown }> | Record<string, unknown>,
3431
+ ): ValidateToolNamesDetailedResult {
3432
+ let names: string[] = [];
3433
+
3434
+ if (Array.isArray(tools)) {
3435
+ for (const item of tools) {
3436
+ if (typeof item === "string") names.push(item);
3437
+ else if (isPlainObject(item) && typeof (item as any).name === "string") names.push((item as any).name);
3438
+ }
3439
+ } else if (isPlainObject(tools)) {
3440
+ // se for Record de toolName -> schema, chaves são nomes
3441
+ names = Object.keys(tools as Record<string, unknown>);
3442
+ }
3443
+
3444
+ // deduplica mantendo ordem para validação, mas mantém duplicatas para report? Vamos dedup para validNames
3445
+ const seen = new Set<string>();
3446
+ const uniqueNames: string[] = [];
3447
+ for (const n of names) {
3448
+ if (!seen.has(n)) {
3449
+ seen.add(n);
3450
+ uniqueNames.push(n);
3451
+ }
3452
+ }
3453
+
3454
+ const invalid: InvalidToolName[] = [];
3455
+ const validNames: string[] = [];
3456
+ const sanitized: SanitizedToolName[] = [];
3457
+
3458
+ for (const name of uniqueNames) {
3459
+ if (!name || typeof name !== "string") {
3460
+ invalid.push({ name: String(name), reason: "empty or not string" });
3461
+ sanitized.push({ original: String(name), sanitized: "_tool" });
3462
+ continue;
3463
+ }
3464
+ if (name.length > TOOL_NAME_MAX_LENGTH) {
3465
+ invalid.push({ name, reason: `exceeds ${TOOL_NAME_MAX_LENGTH} chars` });
3466
+ } else if (!TOOL_NAME_REGEX.test(name)) {
3467
+ let reason = "must start with letter or underscore and contain only alphanum + underscore";
3468
+ if (/^[0-9]/.test(name)) reason = "must not start with digit";
3469
+ else if (/[^A-Za-z0-9_]/.test(name)) reason = "contains invalid characters (only A-Za-z0-9_ allowed)";
3470
+ else if (!name.length) reason = "empty";
3471
+ invalid.push({ name, reason });
3472
+ } else {
3473
+ validNames.push(name);
3474
+ }
3475
+
3476
+ const s = sanitizeToolName(name);
3477
+ // se original inválido, adiciona sanitizado
3478
+ if (!TOOL_NAME_REGEX.test(name) || name.length > TOOL_NAME_MAX_LENGTH) {
3479
+ sanitized.push({ original: name, sanitized: s });
3480
+ }
3481
+ }
3482
+
3483
+ const allSanitized = sanitized.map((s) => s.sanitized).concat(validNames);
3484
+ const allSanitizedValid = allSanitized.every((n) => TOOL_NAME_REGEX.test(n) && n.length <= TOOL_NAME_MAX_LENGTH);
3485
+
3486
+ return {
3487
+ valid: invalid.length === 0,
3488
+ validNames,
3489
+ invalid,
3490
+ sanitized,
3491
+ allSanitizedValid,
3492
+ };
3493
+ }
3494
+
3495
+ // ---------------------------------------------------------------------------
3496
+ // Merged: transformTools — clean + validate + google_search injection
3497
+ // ---------------------------------------------------------------------------
3498
+
3499
+ /**
3500
+ * Converts OpenCode tool definitions into the Cloud Code Assist tools payload:
3501
+ * validates names, sanitizes invalid ones, cleans each schema, deduplicates by
3502
+ * final name, optionally injects `{ google_search: {} }`, and emits the
3503
+ * matching AUTO function-calling toolConfig.
3504
+ *
3505
+ * Kept alongside {@link validateAndSanitizeTools}, which returns flat
3506
+ * declarations instead of a wrapped payload.
3507
+ *
3508
+ * @param openTools - raw tools in OpenAI/Anthropic/Gemini shapes
3509
+ * @param enableGoogleSearch - inject the google_search built-in tool
3510
+ * @returns tools payload, tool config and sanitized-name map
3511
+ */
3512
+ export function transformTools(
3513
+ openTools: OpenCodeTool[] | undefined,
3514
+ enableGoogleSearch?: boolean,
3515
+ ): TransformToolsResult {
3516
+ const sanitizedMap: Record<string, string> = {};
3517
+ const functionDeclarations: GeminiFunctionDeclaration[] = [];
3518
+
3519
+ if (openTools && Array.isArray(openTools) && openTools.length > 0) {
3520
+ const names: string[] = (openTools as Array<{ name?: string }>).map((t) => t.name).filter(Boolean) as string[];
3521
+ const validation = validateToolNames(names);
3522
+
3523
+ for (const inv of validation.invalid) {
3524
+ sanitizedMap[inv] = validation.sanitized[inv] || sanitizeToolName(inv);
3525
+ }
3526
+
3527
+ for (const entry of openTools as any[]) {
3528
+ if (!entry || !entry.name) continue;
3529
+ const originalName: string = entry.name;
3530
+ const name = isValidToolName(originalName)
3531
+ ? originalName
3532
+ : sanitizedMap[originalName] || sanitizeToolName(originalName);
3533
+
3534
+ // Accept the schema from any known alias key
3535
+ const rawSchema = entry.input_schema ??
3536
+ entry.inputSchema ??
3537
+ entry.parameters ??
3538
+ entry.schema ?? { type: "object", properties: {} };
3539
+
3540
+ const cleaned = cleanToolSchema(rawSchema);
3541
+
3542
+ functionDeclarations.push({
3543
+ name,
3544
+ description: entry.description || `Function ${name}`,
3545
+ parameters: cleaned,
3546
+ });
3547
+ }
3548
+ }
3549
+
3550
+ const toolsPayload: any[] = [];
3551
+
3552
+ if (functionDeclarations.length > 0) {
3553
+ // Deduplicate by name — last declaration wins
3554
+ const dedup = new Map<string, GeminiFunctionDeclaration>();
3555
+ for (const fd of functionDeclarations) dedup.set(fd.name, fd);
3556
+ toolsPayload.push({ functionDeclarations: Array.from(dedup.values()) });
3557
+ }
3558
+
3559
+ if (enableGoogleSearch) {
3560
+ // Gemini 3+ uses { google_search: {} }, legacy uses { google_search_retrieval: {} }
3561
+ toolsPayload.push({ google_search: {} });
3562
+ }
3563
+
3564
+ const tools = toolsPayload.length > 0 ? toolsPayload : undefined;
3565
+
3566
+ let toolConfig: TransformToolsResult["toolConfig"];
3567
+ if (functionDeclarations.length > 0) {
3568
+ toolConfig = {
3569
+ functionCallingConfig: {
3570
+ mode: "AUTO",
3571
+ allowedFunctionNames: Array.from(new Set(functionDeclarations.map((f) => f.name))),
3572
+ },
3573
+ };
3574
+ }
3575
+
3576
+ return { tools, toolConfig, sanitizedMap };
3577
+ }
3578
+
3579
+ // ---------------------------------------------------------------------------
3580
+ // Merged: RequestTransformer barrel
3581
+ // ---------------------------------------------------------------------------
3582
+
3583
+ /**
3584
+ * Named bundle mirroring the legacy ONDA 4 module surface. The default export
3585
+ * remains {@link RequestPipeline}; both barrels stay available.
3586
+ */
3587
+ export const RequestTransformer = {
3588
+ // bypass constants
3589
+ GEMINI_CLI_OAUTH_CLIENT_ID,
3590
+ GEMINI_CLI_OAUTH_CLIENT_SECRET,
3591
+ GEMINI_CLI_USER_AGENT,
3592
+ X_GOOG_API_CLIENT,
3593
+ CLIENT_METADATA,
3594
+ CLIENT_METADATA_STRING,
3595
+ FALLBACK_PROJECT_ID,
3596
+ CLOUDCODE_PA_BASE,
3597
+ CLOUDCODE_DAILY_BASE,
3598
+ // legacy member keeps its historical method-path value (v2.1.15 Phase A
3599
+ // renamed the local map to CODE_ASSIST_METHOD_PATHS; the module-level
3600
+ // CODE_ASSIST_ENDPOINTS export now resolves to the models.ts host map)
3601
+ CODE_ASSIST_ENDPOINTS: CODE_ASSIST_METHOD_PATHS,
3602
+ CODE_ASSIST_METHOD_PATHS,
3603
+ MODELS_2026: FETCH_AVAILABLE_MODELS_2026,
3604
+ THINKING_BUDGET_MAP,
3605
+
3606
+ // core FNV
3607
+ fnv1a32,
3608
+ fnv1a64BigInt,
3609
+ buildSessionIdFromDir,
3610
+ buildSessionId,
3611
+ buildUserPromptId,
3612
+
3613
+ // tools
3614
+ sanitizeToolName,
3615
+ isValidToolName,
3616
+ validateToolNames,
3617
+ cleanToolSchema,
3618
+ cleanToolSchemas,
3619
+ transformTools,
3620
+
3621
+ // thinking
3622
+ normalizeThinkingLevel,
3623
+ mapThinkingLevelToBudget,
3624
+
3625
+ // models
3626
+ isGeminiCLIOnlyModel,
3627
+ isLikelyAntigravityOnlyModel,
3628
+
3629
+ // generation
3630
+ buildGenerationConfigFromRequest,
3631
+ buildBypassHeaders,
3632
+
3633
+ // main
3634
+ transformRequest,
3635
+ buildAntigravityRequest,
3636
+ buildGeminiCLIRequest,
3637
+ } as const;
3638
+
3639
+ // ---------------------------------------------------------------------------
3640
+ // Barrel exports — library-first
3641
+ // ---------------------------------------------------------------------------
3642
+
3643
+ export const RequestPipeline = {
3644
+ transformRequest,
3645
+ buildAntigravityRequest,
3646
+ buildGeminiCLIRequest,
3647
+ isGeminiCLIOnlyModel,
3648
+ getEndpointsForModel,
3649
+ fnv1a32,
3650
+ fnv1a32Hex,
3651
+ generateDeterministicId,
3652
+ generateSessionId,
3653
+ generateUserPromptId,
3654
+ sanitizeToolName,
3655
+ ensureUniqueToolNames,
3656
+ validateAndSanitizeTools,
3657
+ cleanJsonSchema,
3658
+ mapThinkingBlock,
3659
+ sortThoughtFirst,
3660
+ filterAndMapThinkingParts,
3661
+ injectGoogleSearchTool,
3662
+ stripForbiddenHeaders,
3663
+ stripXGoogUserProjectHeader,
3664
+ getFingerprintHeaders,
3665
+ buildDynamicUserAgent,
3666
+ fetchWithCascade,
3667
+ parseModelId,
3668
+ extractThinkingLevel,
3669
+ };
3670
+
3671
+ // ============================================================================
3672
+ // 12. DEFAULT EXPORT + NAMED EXPORTS BUNDLE
3673
+ // ============================================================================
3674
+
3675
+ export function stripForbiddenHeaders(headers: Record<string, string>): Record<string, string> {
3676
+ if (!headers || typeof headers !== "object") return {};
3677
+ const forbidden = new Set(FORBIDDEN_HEADERS.map((h) => h.toLowerCase()));
3678
+ const out: Record<string, string> = {};
3679
+ for (const [k, v] of Object.entries(headers)) {
3680
+ if (!k) continue;
3681
+ const low = k.toLowerCase().trim();
3682
+ if (forbidden.has(low)) continue;
3683
+ if (low === "x-goog-user-project") continue;
3684
+ out[k] = v;
3685
+ }
3686
+ return out;
3687
+ }
3688
+ // ===========================================================================
3689
+ /** Cloud Code streaming method path (family owner — former plugin.ts local). */
3690
+ export const STREAM_METHOD = "/v1internal:streamGenerateContent?alt=sse" as const;
3691
+ /** Cloud Code non-streaming method path. */
3692
+ export const GENERATE_METHOD = "/v1internal:generateContent" as const;
3693
+ /** Cloud Code loadCodeAssist method path (project discovery). */
3694
+ export const LOAD_METHOD = "/v1internal:loadCodeAssist" as const;
3695
+ /** Cloud Code onboardUser method path (project discovery). */
3696
+ export const ONBOARD_METHOD = "/v1internal:onboardUser" as const;
3697
+
3698
+ // v2.1.15 Phase D: LIVE request pipeline moved from plugin.ts — request
3699
+ // building lives ONLY in request.ts. This is the opencode-validated chain
3700
+ // (transformRequest + buildCloudCodeBody + buildAntigravityRequest +
3701
+ // buildGeminiCLIRequest + fetchWithCascade with their helper cluster).
3702
+ // The former RequestHelpers-lineage implementations of these families were
3703
+ // deleted; the live shapes are now the canonical public API.
3704
+ // ===========================================================================
3705
+
3706
+ export const TOOL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
3707
+
3708
+ function sanitizeToolName(name: string): string {
3709
+ if (typeof name !== "string") return `tool_${fnv1a32Hex(String(name ?? "")).slice(0, 8)}`;
3710
+ let t = name.trim();
3711
+ if (!t) return `tool_${fnv1a32Hex(name).slice(0, 8)}`;
3712
+ t = t.replace(/[^a-zA-Z0-9_]/g, "_").replace(/__+/g, "_");
3713
+ if (!/^[a-zA-Z]/.test(t)) t = `tool_${t.replace(/^[^a-zA-Z]+/, "")}`;
3714
+ t = t.slice(0, 64);
3715
+ if (!TOOL_NAME_REGEX.test(t)) t = `a${t.slice(0, 63)}`;
3716
+ return t;
3717
+ }
3718
+ function ensureUniqueToolNames(names: string[]): string[] {
3719
+ const seen = new Map<string, number>();
3720
+ const out: string[] = [];
3721
+ for (let raw of names) {
3722
+ let cand = raw;
3723
+ let cnt = seen.get(raw) ?? 0;
3724
+ if (cnt > 0 || out.includes(cand)) {
3725
+ let idx = 1;
3726
+ while (out.includes(`${raw}_${idx}`)) idx++;
3727
+ cand = `${raw}_${idx}`.slice(0, 64);
3728
+ }
3729
+ seen.set(raw, (seen.get(raw) ?? 0) + 1);
3730
+ out.push(cand);
3731
+ }
3732
+ return out;
3733
+ }
3734
+ function cleanJsonSchema(input: any, seen = new WeakSet()): any {
3735
+ if (input == null) return { type: "OBJECT", properties: {} };
3736
+ if (typeof input === "boolean")
3737
+ return input ? { type: "OBJECT", properties: {} } : { type: "OBJECT", properties: {}, description: "disallowed" };
3738
+ if (Array.isArray(input)) return input.map((i) => cleanJsonSchema(i, seen));
3739
+ if (typeof input !== "object") return { type: "STRING" };
3740
+ if (seen.has(input)) return { type: "OBJECT", properties: {} };
3741
+ seen.add(input);
3742
+ const out: any = {};
3743
+ if (input.type) {
3744
+ let rt = Array.isArray(input.type) ? (input.type.find((t: any) => t !== "null") ?? input.type[0]) : input.type;
3745
+ if (typeof rt === "string") out.type = rt.toUpperCase();
3746
+ }
3747
+ for (const k of Object.keys(input)) {
3748
+ if (!CLEAN_SCHEMA_KEYS.has(k)) continue;
3749
+ const v = (input as any)[k];
3750
+ if (k === "properties" && typeof v === "object" && v !== null) {
3751
+ out.properties = {};
3752
+ for (const [pk, pv] of Object.entries(v)) out.properties[pk] = cleanJsonSchema(pv, seen);
3753
+ } else if ((k === "items" || k === "anyOf" || k === "oneOf") && v) {
3754
+ out[k] = cleanJsonSchema(v, seen);
3755
+ } else if (v !== undefined) {
3756
+ out[k] = v;
3757
+ }
3758
+ }
3759
+ if (!out.type && out.properties) out.type = "OBJECT";
3760
+ if (!out.type) out.type = "OBJECT";
3761
+ return out;
3762
+ }
3763
+ function validateAndSanitizeTools(rawTools?: any[]): { declarations: any[] } {
3764
+ if (!rawTools || !Array.isArray(rawTools) || rawTools.length === 0) return { declarations: [] };
3765
+ const decls: any[] = [];
3766
+ for (const entry of rawTools) {
3767
+ if (!entry) continue;
3768
+ if (entry.functionDeclarations && Array.isArray(entry.functionDeclarations)) {
3769
+ for (const fd of entry.functionDeclarations) {
3770
+ if (!fd?.name) continue;
3771
+ decls.push({
3772
+ name: sanitizeToolName(String(fd.name)),
3773
+ description: fd.description ? String(fd.description).slice(0, 1024) : undefined,
3774
+ parameters: fd.parameters ? cleanJsonSchema(fd.parameters) : { type: "OBJECT", properties: {} },
3775
+ originalName: String(fd.name),
3776
+ });
3777
+ }
3778
+ continue;
3779
+ }
3780
+ if (entry.type === "function" && entry.function) {
3781
+ const fn = entry.function;
3782
+ if (!fn.name) continue;
3783
+ decls.push({
3784
+ name: sanitizeToolName(String(fn.name)),
3785
+ description: fn.description ? String(fn.description).slice(0, 1024) : undefined,
3786
+ parameters: fn.parameters ? cleanJsonSchema(fn.parameters) : { type: "OBJECT", properties: {} },
3787
+ originalName: String(fn.name),
3788
+ });
3789
+ continue;
3790
+ }
3791
+ if (entry.name) {
3792
+ const schemaRaw = entry.input_schema ?? entry.inputSchema ?? entry.parameters ?? entry.schema;
3793
+ decls.push({
3794
+ name: sanitizeToolName(String(entry.name)),
3795
+ description: entry.description ? String(entry.description).slice(0, 1024) : undefined,
3796
+ parameters: schemaRaw ? cleanJsonSchema(schemaRaw) : { type: "OBJECT", properties: {} },
3797
+ originalName: String(entry.name),
3798
+ });
3799
+ continue;
3800
+ }
3801
+ }
3802
+ const names = decls.map((d) => d.name);
3803
+ const unique = ensureUniqueToolNames(names);
3804
+ for (let i = 0; i < decls.length; i++) decls[i].name = unique[i];
3805
+ return { declarations: decls };
3806
+ }
3807
+
3808
+ // v2.1.15 Phase A: isGeminiCLIOnlyModel moved to models.ts (canonical
3809
+ // request-lineage implementation — classification superset of this local
3810
+ // variant) — imported and re-exported at the top of this file.
3811
+
3812
+ // v2.1.15 Phase D: shouldSkipSandbox lives in models.js (classifier owner) — imported.
3813
+
3814
+ // v2.1.15 Phase A: getEndpointsForModel moved to models.ts (canonical
3815
+ // request-lineage implementation; the former local shouldSkipSandbox
3816
+ // consultation is subsumed by the canonical classifier) — imported and
3817
+ // re-exported at the top of this file.
3818
+
3819
+ function buildDynamicUserAgentFromPlatform(version?: string): string {
3820
+ return getDynamicUserAgent(version);
3821
+ }
3822
+
3823
+ function buildCommonHeaders(
3824
+ opts: { accessToken: string; userAgent?: string | undefined; version?: string | undefined },
3825
+ style: "antigravity" | "gemini-cli" = "antigravity",
3826
+ ): Record<string, string> {
3827
+ const ua = opts.userAgent ?? buildDynamicUserAgentFromPlatform(opts.version);
3828
+ const headers: Record<string, string> = {
3829
+ Authorization: `Bearer ${opts.accessToken}`,
3830
+ "Content-Type": "application/json",
3831
+ "User-Agent": ua,
3832
+ };
3833
+ // gemini-cli adds X-Goog-Api-Client but AM does not — observer includes only for gemini-cli pool when cli_first
3834
+ if (style === "gemini-cli") {
3835
+ try {
3836
+ const nodeVer = (process as any)?.versions?.node ?? "20.0.0";
3837
+ headers["X-Goog-Api-Client"] =
3838
+ `antigravity/${opts.version ?? ANTIGRAVITY_VERSION_FALLBACK} gl-node/${nodeVer} gax/4.9.0 grpc/1.14.0`;
3839
+ headers["Client-Metadata"] =
3840
+ `ideType=ANTIGRAVITY,platform=${normalizePlatform(osPlatform()).toUpperCase()},ideVersion=${opts.version ?? ANTIGRAVITY_VERSION_FALLBACK},pluginVersion=${PLUGIN_VERSION}`;
3841
+ } catch {
3842
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
3843
+ }
3844
+ }
3845
+ return stripForbiddenHeaders(headers);
3846
+ }
3847
+
3848
+ // v2.1.15 Phase B: buildAntigravityClientMetadata was dead (zero callers) — deleted;
3849
+ // fingerprint.js buildClientMetadata is the canonical metadata builder.
3850
+
3851
+ // v2.1.15 Phase A: mapModelToGroup moved to models.ts (canonical merged
3852
+ // implementation) — imported and re-exported at the top of this file.
3853
+
3854
+ // deterministic ids
3855
+ export function generateDeterministicId(seed: string): string {
3856
+ return fnv1a32Hex(seed);
3857
+ }
3858
+ export function generateSessionId(seed?: string, offsetEnabled?: boolean): string {
3859
+ const base = seed ?? `${Date.now()}-${randomBytes(4).toString("hex")}`;
3860
+ const offset = offsetEnabled ? randomInt(0, 1001) : 0;
3861
+ return fnv1a32Hex(`${base}-${offset}-session`);
3862
+ }
3863
+ export function generateUserPromptId(seed?: string, offsetEnabled?: boolean): string {
3864
+ const base = seed ?? `${Date.now()}-${randomBytes(4).toString("hex")}`;
3865
+ const offset = offsetEnabled ? randomInt(0, 1001) : 0;
3866
+ // 9router pid_offset trick 0-1000 added to user_prompt_id for anti-fingerprint jitter
3867
+ return fnv1a32Hex(`${base}-${offset}-user-prompt-${offsetEnabled ? offset : 0}`);
3868
+ }
3869
+
3870
+ // ---------------------------------------------------------------------------
3871
+ // Transform OpenAI/Anthropic messages → Gemini contents
3872
+ // ---------------------------------------------------------------------------
3873
+
3874
+ // v2.1.15 Phase D: the live pipeline uses the exported superset interfaces
3875
+ // GeminiPart/GeminiContent/GeminiSystem above (one type family, one owner).
3876
+
3877
+ interface TransformRequestOptions {
3878
+ model: string;
3879
+ messages?: any[] | undefined;
3880
+ tools?: any[] | undefined;
3881
+ system?: string | any | undefined;
3882
+ thinkingLevel?: string | undefined;
3883
+ thinkingBudget?: number | undefined;
3884
+ googleSearchEnabled?: boolean | undefined;
3885
+ generationConfig?: any | undefined;
3886
+ sessionSeed?: string | undefined;
3887
+ userPromptSeed?: string | undefined;
3888
+ projectId?: string | undefined;
3889
+ pidOffsetEnabled?: boolean | undefined;
3890
+ claudeToolHardening?: boolean | undefined;
3891
+ keepThinking?: boolean | undefined;
3892
+ }
3893
+
3894
+ interface TransformResult {
3895
+ contents: GeminiContent[];
3896
+ systemInstruction?: GeminiSystem | undefined;
3897
+ tools?: any[] | undefined;
3898
+ toolConfig?: any | undefined;
3899
+ generationConfig?: any | undefined;
3900
+ sessionId: string;
3901
+ userPromptId: string;
3902
+ thinkingBudget?: number | undefined;
3903
+ thinkingLevel?: ThinkingLevel | undefined;
3904
+ }
3905
+
3906
+ function extractThinkingLevel(modelId: string, explicit?: string): { level?: ThinkingLevel; budget?: number } {
3907
+ const exp = (explicit ?? "").toLowerCase().trim();
3908
+ if (exp && (THINKING_LEVELS as readonly string[]).includes(exp))
3909
+ return { level: exp as ThinkingLevel, budget: THINKING_BUDGET_MAP[exp as ThinkingLevel] };
3910
+ const lower = (modelId ?? "").toLowerCase();
3911
+ for (const lvl of THINKING_LEVELS)
3912
+ if (lower.includes(`-${lvl}`))
3913
+ return { level: lvl as ThinkingLevel, budget: THINKING_BUDGET_MAP[lvl as ThinkingLevel] };
3914
+ if (lower.includes("thinking"))
3915
+ return {
3916
+ level: lower.includes("claude") ? "medium" : "low",
3917
+ budget: THINKING_BUDGET_MAP[lower.includes("claude") ? "medium" : "low"],
3918
+ };
3919
+ return {};
3920
+ }
3921
+
3922
+ function openAIContentToParts(content: any): GeminiPart[] {
3923
+ if (content == null) return [{ text: "" }];
3924
+ if (typeof content === "string") return [{ text: content }];
3925
+ if (Array.isArray(content)) {
3926
+ const parts: GeminiPart[] = [];
3927
+ for (const c of content) {
3928
+ if (!c) continue;
3929
+ if (typeof c === "string") parts.push({ text: c });
3930
+ else if (c.type === "text" && c.text) parts.push({ text: c.text });
3931
+ else if (c.type === "image_url" && c.image_url?.url) {
3932
+ // data url handling (linear parse, no capture-group regex on uncontrolled input)
3933
+ const url = c.image_url.url;
3934
+ const sep = url.lastIndexOf(";base64,");
3935
+ if (url.startsWith("data:") && sep > 5 && sep + 8 < url.length) {
3936
+ parts.push({ inlineData: { mimeType: url.slice(5, sep), data: url.slice(sep + 8) } });
3937
+ } else parts.push({ text: `[image: ${url.slice(0, 200)}]` });
3938
+ } else if (c.text) parts.push({ text: String(c.text) });
3939
+ }
3940
+ return parts.length ? parts : [{ text: "" }];
3941
+ }
3942
+ if (typeof content === "object" && (content as any).text) return [{ text: String((content as any).text) }];
3943
+ return [{ text: String(content) }];
3944
+ }
3945
+
3946
+ export function transformRequest(opts: TransformRequestOptions): TransformResult {
3947
+ const {
3948
+ model,
3949
+ messages = [],
3950
+ tools,
3951
+ system,
3952
+ thinkingLevel: explicitThinkingLevel,
3953
+ thinkingBudget: explicitBudget,
3954
+ googleSearchEnabled,
3955
+ generationConfig,
3956
+ sessionSeed,
3957
+ userPromptSeed,
3958
+ pidOffsetEnabled,
3959
+ claudeToolHardening,
3960
+ keepThinking,
3961
+ } = opts;
3962
+
3963
+ const { level: inferredLevel, budget: inferredBudget } = extractThinkingLevel(model, explicitThinkingLevel);
3964
+ const finalLevel = inferredLevel ?? (explicitThinkingLevel as ThinkingLevel | undefined);
3965
+ const finalBudget = explicitBudget ?? inferredBudget ?? (finalLevel ? THINKING_BUDGET_MAP[finalLevel] : undefined);
3966
+
3967
+ // system instruction extraction
3968
+ let sysText = "";
3969
+ if (typeof system === "string") sysText = system;
3970
+ else if (Array.isArray(system)) sysText = system.map((s) => (typeof s === "string" ? s : (s.text ?? ""))).join("\n");
3971
+ else if (system && typeof system === "object" && (system as any).text) sysText = String((system as any).text);
3972
+
3973
+ // also extract system role from messages
3974
+ const filteredMessages: any[] = [];
3975
+ for (const m of messages) {
3976
+ if (!m) continue;
3977
+ if (m.role === "system") {
3978
+ const t =
3979
+ typeof m.content === "string"
3980
+ ? m.content
3981
+ : Array.isArray(m.content)
3982
+ ? m.content.map((c: any) => c.text ?? "").join("\n")
3983
+ : String(m.content ?? "");
3984
+ sysText = sysText ? `${sysText}\n${t}` : t;
3985
+ } else {
3986
+ filteredMessages.push(m);
3987
+ }
3988
+ }
3989
+
3990
+ const systemInstruction: GeminiSystem = sysText ? { parts: [{ text: sysText }] } : undefined;
3991
+
3992
+ const contents: GeminiContent[] = [];
3993
+ for (const m of filteredMessages) {
3994
+ if (!m) continue;
3995
+ const roleRaw = String(m.role ?? "user").toLowerCase();
3996
+ const role: "user" | "model" = roleRaw === "assistant" || roleRaw === "model" ? "model" : "user";
3997
+
3998
+ // tool messages
3999
+ if (roleRaw === "tool" || roleRaw === "function") {
4000
+ const toolName = m.tool_call_id ?? m.name ?? m.toolName ?? "tool";
4001
+ const sanitizedName = sanitizeToolName(String(toolName));
4002
+ const contentStr = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? {});
4003
+ let parsed: any;
4004
+ try {
4005
+ parsed = JSON.parse(contentStr);
4006
+ } catch {
4007
+ parsed = { result: contentStr };
4008
+ }
4009
+ contents.push({
4010
+ role: "user",
4011
+ parts: [{ functionResponse: { name: sanitizedName, response: parsed } }],
4012
+ });
4013
+ continue;
4014
+ }
4015
+
4016
+ // assistant with tool_calls
4017
+ if (role === "model" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
4018
+ const parts: GeminiPart[] = [];
4019
+ const textParts = openAIContentToParts(m.content);
4020
+ for (const tp of textParts) if (tp.text) parts.push(tp);
4021
+ for (const tc of m.tool_calls) {
4022
+ const fn = tc.function ?? tc;
4023
+ const name = sanitizeToolName(String(fn.name ?? tc.name ?? "tool"));
4024
+ let args: any = fn.arguments ?? fn.args ?? {};
4025
+ if (typeof args === "string") {
4026
+ try {
4027
+ args = JSON.parse(args);
4028
+ } catch {
4029
+ args = { raw: args };
4030
+ }
4031
+ }
4032
+ parts.push({ functionCall: { name, args } });
4033
+ }
4034
+ if (parts.length > 0) contents.push({ role, parts });
4035
+ continue;
4036
+ }
4037
+
4038
+ // Anthropic tool_use
4039
+ if (Array.isArray(m.content) && m.content.some((c: any) => c?.type === "tool_use")) {
4040
+ const parts: GeminiPart[] = [];
4041
+ for (const c of m.content) {
4042
+ if (c.type === "text") parts.push({ text: c.text });
4043
+ else if (c.type === "tool_use")
4044
+ parts.push({ functionCall: { name: sanitizeToolName(c.name), args: c.input ?? {} } });
4045
+ }
4046
+ contents.push({ role, parts: parts.length ? parts : [{ text: "" }] });
4047
+ continue;
4048
+ }
4049
+
4050
+ // regular
4051
+ const parts = openAIContentToParts(m.content ?? "");
4052
+ // thinking blocks mapping when model is thinking variant: if parts contain thinking tag? skip for now
4053
+ contents.push({ role, parts: parts.length ? parts : [{ text: "" }] });
4054
+ }
4055
+
4056
+ // tools sanitization + google_search injection
4057
+ let geminiTools: any[] | undefined;
4058
+ if (tools && tools.length > 0) {
4059
+ const { declarations } = validateAndSanitizeTools(tools);
4060
+ if (declarations.length > 0) {
4061
+ // claude_tool_hardening: prefix descriptions so Claude models respect schema constraints
4062
+ const harden = !!claudeToolHardening && model.toLowerCase().includes("claude");
4063
+ geminiTools = [
4064
+ {
4065
+ functionDeclarations: declarations.map((d) => ({
4066
+ name: d.name,
4067
+ description: harden ? `[tool_hardening] ${d.description ?? ""}`.trim() : d.description,
4068
+ parameters: d.parameters,
4069
+ })),
4070
+ },
4071
+ ];
4072
+ }
4073
+ }
4074
+
4075
+ if (googleSearchEnabled) {
4076
+ const modelLower = model.toLowerCase();
4077
+ const isGemini = modelLower.includes("gemini");
4078
+ if (isGemini) {
4079
+ // inject google_search grounding tool as per Gemini spec
4080
+ if (!geminiTools) geminiTools = [];
4081
+ // avoid duplicate
4082
+ const hasSearch = geminiTools.some((t: any) => t.googleSearch || t.google_search);
4083
+ if (!hasSearch) geminiTools.push({ googleSearch: {} });
4084
+ }
4085
+ }
4086
+
4087
+ // generationConfig
4088
+ const genConfig: any = { ...(generationConfig ?? {}) };
4089
+ if (finalBudget) {
4090
+ // Gemini 3 thinking budget
4091
+ genConfig.thinkingConfig = { thinkingBudget: finalBudget, ...(finalLevel ? { thinkingLevel: finalLevel } : {}) };
4092
+ }
4093
+ if (keepThinking) {
4094
+ // keep_thinking: request reasoning back and preserve signatures end-to-end
4095
+ genConfig.thinkingConfig = { ...(genConfig.thinkingConfig ?? {}), includeThoughts: true, keepThinking: true };
4096
+ }
4097
+ // normalize temperature etc
4098
+ if (genConfig.temperature === undefined) genConfig.temperature = 1.0;
4099
+
4100
+ const sessionId = generateSessionId(sessionSeed ?? `${model}-${contents.length}-${Date.now()}`, pidOffsetEnabled);
4101
+ const userPromptId = generateUserPromptId(
4102
+ userPromptSeed ?? `${model}-${sysText.slice(0, 50)}-${Date.now()}`,
4103
+ pidOffsetEnabled,
4104
+ );
4105
+
4106
+ return {
4107
+ contents,
4108
+ systemInstruction,
4109
+ tools: geminiTools,
4110
+ generationConfig: genConfig,
4111
+ sessionId,
4112
+ userPromptId,
4113
+ thinkingBudget: finalBudget,
4114
+ thinkingLevel: finalLevel,
4115
+ };
4116
+ }
4117
+
4118
+ export function buildCloudCodeBody(projectId: string, inner: TransformResult): Record<string, any> {
4119
+ // Gemini CLI exact wrapping: {model, project, request: {contents, systemInstruction, tools, generationConfig, toolConfig?}}
4120
+ // NOTE: session_id / user_prompt_id are metadata ONLY (logs/fingerprint) — the live
4121
+ // cloudcode-pa v1internal:streamGenerateContent schema rejects unknown root fields
4122
+ // (verified live 2026-08-26: "Unknown name \"session_id\": Cannot find field" -> 400).
4123
+ const request: any = {
4124
+ contents: inner.contents,
4125
+ };
4126
+ if (inner.systemInstruction) request.systemInstruction = inner.systemInstruction;
4127
+ if (inner.tools && inner.tools.length > 0) request.tools = inner.tools;
4128
+ if (inner.toolConfig) request.toolConfig = inner.toolConfig;
4129
+ if (inner.generationConfig) request.generationConfig = inner.generationConfig;
4130
+
4131
+ return {
4132
+ model: undefined, // filled by caller wrapper
4133
+ project: projectId,
4134
+ request,
4135
+ };
4136
+ }
4137
+
4138
+ export interface BuildOptions {
4139
+ model: string;
4140
+ messages?: any[] | undefined;
4141
+ tools?: any[] | undefined;
4142
+ system?: any | undefined;
4143
+ thinkingLevel?: string | undefined;
4144
+ thinkingBudget?: number | undefined;
4145
+ googleSearchEnabled?: boolean | undefined;
4146
+ generationConfig?: any | undefined;
4147
+ accessToken: string;
4148
+ projectId: string;
4149
+ endpointOverrides?: string[] | undefined;
4150
+ userAgent?: string | undefined;
4151
+ antigravityVersion?: string | undefined;
4152
+ methodSuffix?: string | undefined;
4153
+ sessionSeed?: string | undefined;
4154
+ userPromptSeed?: string | undefined;
4155
+ pidOffsetEnabled?: boolean | undefined;
4156
+ claudeToolHardening?: boolean | undefined;
4157
+ keepThinking?: boolean | undefined;
4158
+ }
4159
+
4160
+ export interface BuildResult {
4161
+ url: string;
4162
+ endpoints: string[];
4163
+ baseEndpoints: string[];
4164
+ headers: Record<string, string>;
4165
+ body: Record<string, any>;
4166
+ sessionId: string;
4167
+ userPromptId: string;
4168
+ isGeminiCLIOnly: boolean;
4169
+ method: string;
4170
+ innerRequest: TransformResult;
4171
+ }
4172
+
4173
+ /**
4174
+ * Shared prologue of {@link buildAntigravityRequest} and
4175
+ * {@link buildGeminiCLIRequest}: validates the common build options, resolves
4176
+ * the fallback project id and runs the 16-field {@link transformRequest}
4177
+ * call. v2.1.16 pure refactor — the two builders previously carried a
4178
+ * byte-identical 25-line copy of this block; wire bytes and thrown error
4179
+ * messages are unchanged.
4180
+ *
4181
+ * @param options - shared build options (model, messages, tools, seeds, ...)
4182
+ * @param label - error-message suffix ("" for the antigravity variant,
4183
+ * " for Gemini CLI" for the gemini-cli variant)
4184
+ */
4185
+ function prepareBuildOptions(
4186
+ options: BuildOptions,
4187
+ label = "",
4188
+ ): { projectId: string; token: string; model: string; inner: TransformResult } {
4189
+ if (!options) throw new Error(`build options required${label}`);
4190
+ const projectId = String(options.projectId ?? "").trim() || FALLBACK_PROJECT_ID;
4191
+ const token = String(options.accessToken ?? "").trim();
4192
+ if (!token) throw new Error("accessToken required");
4193
+ const model = String(options.model ?? "").trim();
4194
+ if (!model) throw new Error(`model required${label}`);
4195
+
4196
+ const inner = transformRequest({
4197
+ model: options.model,
4198
+ messages: options.messages,
4199
+ tools: options.tools,
4200
+ system: options.system,
4201
+ thinkingLevel: options.thinkingLevel,
4202
+ thinkingBudget: options.thinkingBudget,
4203
+ googleSearchEnabled: options.googleSearchEnabled,
4204
+ generationConfig: options.generationConfig,
4205
+ sessionSeed: options.sessionSeed,
4206
+ userPromptSeed: options.userPromptSeed,
4207
+ projectId,
4208
+ pidOffsetEnabled: options.pidOffsetEnabled,
4209
+ claudeToolHardening: options.claudeToolHardening,
4210
+ keepThinking: options.keepThinking,
4211
+ });
4212
+
4213
+ return { projectId, token, model, inner };
4214
+ }
4215
+
4216
+ export function buildAntigravityRequest(options: BuildOptions): BuildResult {
4217
+ const { projectId, token, model, inner } = prepareBuildOptions(options);
4218
+
4219
+ const bases = getEndpointsForModel(model, options.endpointOverrides);
4220
+ const fullEndpoints = bases.map((b) => `${stripTrailingSlashes(b)}${options.methodSuffix ?? STREAM_METHOD}`);
4221
+
4222
+ const version = options.antigravityVersion ?? ANTIGRAVITY_VERSION_FALLBACK;
4223
+ const headers = buildCommonHeaders({ accessToken: token, userAgent: options.userAgent, version }, "antigravity");
4224
+
4225
+ const bodyWrapper = buildCloudCodeBody(projectId, inner);
4226
+ // bodyWrapper.model set here
4227
+ bodyWrapper.model = model;
4228
+ // keep request key as spec requires {model, project, request}
4229
+ // session_id / user_prompt_id stay OUT of the wire body (schema-verified) —
4230
+ // they remain available as BuildResult.sessionId / .userPromptId metadata.
4231
+ const body = {
4232
+ model: bodyWrapper.model,
4233
+ project: bodyWrapper.project,
4234
+ request: bodyWrapper.request,
4235
+ };
4236
+
4237
+ return {
4238
+ url: fullEndpoints[0]!,
4239
+ endpoints: fullEndpoints,
4240
+ baseEndpoints: bases,
4241
+ headers,
4242
+ body,
4243
+ sessionId: inner.sessionId,
4244
+ userPromptId: inner.userPromptId,
4245
+ isGeminiCLIOnly: isGeminiCLIOnlyModel(model),
4246
+ method: "POST",
4247
+ innerRequest: inner,
4248
+ };
4249
+ }
4250
+
4251
+ export function buildGeminiCLIRequest(options: BuildOptions): BuildResult {
4252
+ const { projectId, token, model, inner } = prepareBuildOptions(options, " for Gemini CLI");
4253
+
4254
+ const rawBases = getEndpointsForModel(model, options.endpointOverrides);
4255
+ const filteredBases = rawBases.filter((b) => !b.toLowerCase().includes("sandbox"));
4256
+ const bases = filteredBases.length > 0 ? filteredBases : [CODE_ASSIST_ENDPOINTS.PROD];
4257
+ const fullEndpoints = bases.map((b) => `${stripTrailingSlashes(b)}${options.methodSuffix ?? STREAM_METHOD}`);
4258
+
4259
+ const version = options.antigravityVersion ?? ANTIGRAVITY_VERSION_FALLBACK;
4260
+ const headers = buildCommonHeaders({ accessToken: token, userAgent: options.userAgent, version }, "gemini-cli");
4261
+
4262
+ const bodyWrapper = buildCloudCodeBody(projectId, inner);
4263
+ // session_id / user_prompt_id stay OUT of the wire body (schema-verified live:
4264
+ // cloudcode-pa rejects unknown root fields with 400 INVALID_ARGUMENT) —
4265
+ // they remain as BuildResult metadata for logging/fingerprinting.
4266
+ const body = {
4267
+ model,
4268
+ project: projectId,
4269
+ request: bodyWrapper.request,
4270
+ };
4271
+
4272
+ return {
4273
+ url: fullEndpoints[0]!,
4274
+ endpoints: fullEndpoints,
4275
+ baseEndpoints: bases,
4276
+ headers,
4277
+ body,
4278
+ sessionId: inner.sessionId,
4279
+ userPromptId: inner.userPromptId,
4280
+ isGeminiCLIOnly: true,
4281
+ method: "POST",
4282
+ innerRequest: inner,
4283
+ };
4284
+ }
4285
+
4286
+ export async function fetchWithCascade(
4287
+ buildResult: BuildResult,
4288
+ fetchOptions?: { signal?: AbortSignal; timeoutMs?: number },
4289
+ ): Promise<Response> {
4290
+ if (!buildResult || !Array.isArray(buildResult.endpoints) || buildResult.endpoints.length === 0)
4291
+ throw new Error("buildResult with endpoints required");
4292
+ let lastError: Error | null = null;
4293
+
4294
+ for (const endpointUrl of buildResult.endpoints) {
4295
+ const controller = new AbortController();
4296
+ const timeoutMs = fetchOptions?.timeoutMs ?? CONTENT_TIMEOUT_MS;
4297
+ const to = setTimeout(() => controller.abort(), timeoutMs);
4298
+
4299
+ const signal = fetchOptions?.signal
4300
+ ? (() => {
4301
+ const combined = new AbortController();
4302
+ const onExt = () => combined.abort();
4303
+ fetchOptions.signal!.addEventListener("abort", onExt, { once: true });
4304
+ controller.signal.addEventListener("abort", () => combined.abort());
4305
+ return combined.signal;
4306
+ })()
4307
+ : controller.signal;
4308
+
4309
+ try {
4310
+ const jitter = getJitter();
4311
+ if (jitter > 0) await new Promise((r) => setTimeout(r, jitter));
4312
+
4313
+ const headers = stripForbiddenHeaders(buildResult.headers);
4314
+
4315
+ const res = await fetch(endpointUrl, {
4316
+ method: buildResult.method,
4317
+ headers,
4318
+ body: JSON.stringify(buildResult.body),
4319
+ signal,
4320
+ });
4321
+
4322
+ if (res.status === 403 || res.status === 404 || res.status >= 500) {
4323
+ // Permanent entitlement/region denials must surface immediately with
4324
+ // Google's own message — retrying other endpoints/accounts only burns
4325
+ // minutes and hides the real cause (verified live: UNSUPPORTED_LOCATION).
4326
+ if (res.status === 403) {
4327
+ const probe = res.clone();
4328
+ const bodyTxt = await probe.text().catch(() => "");
4329
+ const low = bodyTxt.toLowerCase();
4330
+ if (low.includes("location") || low.includes("not eligible") || low.includes("unsupported_location")) {
4331
+ clearTimeout(to);
4332
+ return res;
4333
+ }
4334
+ }
4335
+ lastError = new Error(`cascade retryable status ${res.status} at ${endpointUrl}`);
4336
+ continue;
4337
+ }
4338
+ clearTimeout(to);
4339
+ return res;
4340
+ } catch (e: any) {
4341
+ clearTimeout(to);
4342
+ lastError = e instanceof Error ? e : new Error(String(e));
4343
+ if (fetchOptions?.signal?.aborted) throw lastError;
4344
+ continue;
4345
+ } finally {
4346
+ clearTimeout(to);
4347
+ }
4348
+ }
4349
+ throw lastError ?? new Error("all endpoints failed in cascade");
4350
+ }
4351
+
4352
+ export const BYPASS_CONSTANTS = {
4353
+ directEndpoint: DIRECT_ENDPOINT,
4354
+ dailyEndpoint: DAILY_ENDPOINT,
4355
+ sandboxEndpoint: AUTOPUSH_ENDPOINT,
4356
+ endpointCascade: ENDPOINT_CASCADE,
4357
+ headersToStrip: HEADERS_TO_STRIP,
4358
+ toolNamePattern: TOOL_NAME_REGEX.source,
4359
+ maxToolNameLength: TOOL_NAME_MAX_LENGTH,
4360
+ maxToolsPerRequest: MAX_TOOLS_PER_REQUEST,
4361
+ thinkingBudgetMap: THINKING_BUDGET_MAP,
4362
+ fnvOffsetBasis: FNV_OFFSET_BASIS,
4363
+ fnvPrime: FNV_PRIME,
4364
+ } as const;
4365
+
4366
+ // ===========================================================================
4367
+ // v2.1.15 Phase D: Cloud Code method-path maps moved from fingerprint.ts and
4368
+ // quota.ts — the API surface lives only in request.ts. (The bare ENDPOINTS
4369
+ // name is the constants.js host map; these method maps are API_PATHS.)
4370
+ // ===========================================================================
4371
+
4372
+ /** Full Cloud Code v1internal method URLs on the production base. */
4373
+ export const API_PATHS = {
4374
+ loadCodeAssist: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.LOAD_CODE_ASSIST}`,
4375
+ onboardUser: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.ONBOARD_USER}`,
4376
+ fetchAvailableModels: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.FETCH_AVAILABLE_MODELS}`,
4377
+ generateContent: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.GENERATE_CONTENT}`,
4378
+ streamGenerateContent: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.STREAM_GENERATE_CONTENT}`,
4379
+ retrieveUserQuotaSummary: `${CODE_ASSIST_BASE_URL}${CODE_ASSIST_PATH_MAP.RETRIEVE_USER_QUOTA_SUMMARY}`,
4380
+ } as const;
4381
+
4382
+ /** Daily-build method URLs (quota lineage: antigravity quota reads the daily base). */
4383
+ export const API_PATHS_DAILY = {
4384
+ loadCodeAssist: `${CLOUDCODE_DAILY_BASE}${CODE_ASSIST_PATH_MAP.LOAD_CODE_ASSIST}`,
4385
+ retrieveUserQuotaSummary: `${CLOUDCODE_DAILY_BASE}${CODE_ASSIST_PATH_MAP.RETRIEVE_USER_QUOTA_SUMMARY}`,
4386
+ } as const;
4387
+
4388
+ export const RequestHelpers = {
4389
+ // constants
4390
+ GEMINI_CLI_OAUTH_CLIENT_ID,
4391
+ GEMINI_CLI_OAUTH_CLIENT_SECRET,
4392
+ GEMINI_CLI_USER_AGENT,
4393
+ X_GOOG_API_CLIENT,
4394
+ CLIENT_METADATA_RAW,
4395
+ CLOUDCODE_PA_BASE,
4396
+ PROJECT_FALLBACK,
4397
+ MODELS_2026: FETCH_AVAILABLE_MODELS_2026,
4398
+
4399
+ // core
4400
+ fnv1a32,
4401
+ fnv1a64BigInt,
4402
+ buildUserPromptId,
4403
+ buildSessionIdFromDir,
4404
+ isLikelyAntigravityOnlyModel,
4405
+ cleanToolSchemaTolerant,
4406
+ cleanToolSchemas,
4407
+ validateToolNamesDetailed,
4408
+ sanitizeToolName,
4409
+ filterThinkingBlocks,
4410
+ normalizeMessages,
4411
+ buildGenerationConfig,
4412
+ enhanceAgySdkErrorResponse,
4413
+ extractVariants,
4414
+ extractVariantsFromAny,
4415
+
4416
+ // mappings
4417
+ THINKING_LEVEL_TO_BUDGET_GEMINI,
4418
+ THINKING_LEVEL_TO_BUDGET_CLAUDE,
4419
+
4420
+ // merged: FNV + deterministic ids
4421
+ FNV_OFFSET_BASIS,
4422
+ FNV_PRIME,
4423
+ fnv1aHex,
4424
+ fnv1aHex16,
4425
+ fnv1aHash,
4426
+ generateDeterministicId,
4427
+ generateSessionId,
4428
+ generateUserPromptId,
4429
+
4430
+ // merged: thinking budgets
4431
+ THINKING_BUDGET_MAP,
4432
+ THINKING_LEVELS,
4433
+ THINKING_BUDGET_MIN,
4434
+ THINKING_BUDGET_MAX,
4435
+ THINKING_BUDGET_MAX_GEMINI,
4436
+ THINKING_BUDGET_MAX_CLAUDE,
4437
+ parseThinkingLevel,
4438
+ mapThinkingLevelToBudget,
4439
+ thinkingLevelToBudget,
4440
+ getThinkingBudgetForVariant,
4441
+
4442
+ // merged: model detection
4443
+ isGeminiCLIOnlyModel,
4444
+ extractModelVariant,
4445
+ parseModelVariant,
4446
+
4447
+ // merged: tool names and schema cleaning
4448
+ TOOL_NAME_REGEX,
4449
+ TOOL_NAME_MIN_LENGTH,
4450
+ TOOL_NAME_MAX_LENGTH,
4451
+ MAX_TOOLS_PER_REQUEST,
4452
+ ALLOWED_SCHEMA_KEYS,
4453
+ ALLOWED_TOOL_TOP_KEYS,
4454
+ validateToolName,
4455
+ cleanJsonSchemaStrict,
4456
+ cleanRawToolSchema,
4457
+ cleanToolsArray,
4458
+
4459
+ // merged: message normalization and thinking filters
4460
+ normalizeRole,
4461
+ normalizeContentToParts,
4462
+ normalizeIncomingMessages,
4463
+ isThinkingBlock,
4464
+ filterThinkingBlocksAuto,
4465
+ filterThinkingBlocksFromParts,
4466
+ filterThinkingBlocksFromMessages,
4467
+
4468
+ // merged: transport helpers
4469
+ DIRECT_ENDPOINT,
4470
+ DAILY_ENDPOINT,
4471
+ AUTOPUSH_ENDPOINT,
4472
+ ENDPOINT_CASCADE,
4473
+ HEADERS_TO_STRIP,
4474
+ BYPASS_CONSTANTS,
4475
+ isRetryableStatus,
4476
+ stripForbiddenHeaders,
4477
+
4478
+ // merged: error type
4479
+ RequestHelpersError,
4480
+ } as const;
4481
+
4482
+ export default RequestPipeline;