@wenathlan/devthink 2.0.14 → 2.0.19
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.
- package/CHANGELOG.md +60 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/request.ts
DELETED
|
@@ -1,4482 +0,0 @@
|
|
|
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;
|