terramend 0.2.0
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/LICENSE +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { Tool } from "fastmcp";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
isGeminiRouted,
|
|
6
|
+
sanitizeForGemini,
|
|
7
|
+
sanitizeToolForGemini,
|
|
8
|
+
wrapSchemaForGemini,
|
|
9
|
+
} from "#app/mcp/geminiSanitizer";
|
|
10
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
11
|
+
|
|
12
|
+
describe("sanitizeForGemini", () => {
|
|
13
|
+
it("passes through primitives and null untouched", () => {
|
|
14
|
+
expect(sanitizeForGemini(null)).toBeNull();
|
|
15
|
+
expect(sanitizeForGemini(undefined)).toBeUndefined();
|
|
16
|
+
expect(sanitizeForGemini("string")).toBe("string");
|
|
17
|
+
expect(sanitizeForGemini(42)).toBe(42);
|
|
18
|
+
expect(sanitizeForGemini(true)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("maps arrays element-wise", () => {
|
|
22
|
+
expect(sanitizeForGemini([{ $schema: "x", type: "object" }, 1])).toEqual([
|
|
23
|
+
{ type: "object" },
|
|
24
|
+
1,
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("adds type: string to an enum-only string union (case 1)", () => {
|
|
29
|
+
expect(sanitizeForGemini({ enum: ["A", "B"] })).toEqual({
|
|
30
|
+
type: "string",
|
|
31
|
+
enum: ["A", "B"],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps the description when typing an enum-only schema", () => {
|
|
36
|
+
expect(sanitizeForGemini({ enum: ["A"], description: "pick one" })).toEqual({
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["A"],
|
|
39
|
+
description: "pick one",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("leaves a mixed-type enum to the generic pass (no fabricated type)", () => {
|
|
44
|
+
expect(sanitizeForGemini({ enum: ["A", 1] })).toEqual({ enum: ["A", 1] });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not re-type an enum that already declares a string type", () => {
|
|
48
|
+
expect(sanitizeForGemini({ type: "string", enum: ["A"], extra: 1 })).toEqual({
|
|
49
|
+
type: "string",
|
|
50
|
+
enum: ["A"],
|
|
51
|
+
extra: 1,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("collapses an anyOf of string enums into a typed enum (case 2)", () => {
|
|
56
|
+
expect(
|
|
57
|
+
sanitizeForGemini({
|
|
58
|
+
anyOf: [{ enum: ["a"] }, { enum: ["b", "c"] }],
|
|
59
|
+
description: "choice",
|
|
60
|
+
}),
|
|
61
|
+
).toEqual({ type: "string", enum: ["a", "b", "c"], description: "choice" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("collapses const branches and dedupes repeated values", () => {
|
|
65
|
+
expect(sanitizeForGemini({ oneOf: [{ const: "x" }, { const: "y" }, { enum: ["x"] }] })).toEqual(
|
|
66
|
+
{ type: "string", enum: ["x", "y"] },
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("strips sibling fields from a non-collapsible anyOf (case 3)", () => {
|
|
71
|
+
const result = sanitizeForGemini({
|
|
72
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "dropped per gemini rule",
|
|
75
|
+
});
|
|
76
|
+
expect(result).toEqual({ anyOf: [{ type: "string" }, { type: "number" }] });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("handles a oneOf-only non-collapsible union (no anyOf key fabricated)", () => {
|
|
80
|
+
expect(
|
|
81
|
+
sanitizeForGemini({ oneOf: [{ type: "number" }, { type: "boolean" }], description: "d" }),
|
|
82
|
+
).toEqual({ oneOf: [{ type: "number" }, { type: "boolean" }] });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps both anyOf and oneOf when neither collapses, recursing into branches", () => {
|
|
86
|
+
const result = sanitizeForGemini({
|
|
87
|
+
anyOf: [{ $schema: "x", type: "string" }],
|
|
88
|
+
oneOf: [{ type: "number" }, { type: "boolean" }],
|
|
89
|
+
});
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
anyOf: [{ type: "string" }],
|
|
92
|
+
oneOf: [{ type: "number" }, { type: "boolean" }],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("treats a branch with an empty or non-string enum as non-collapsible", () => {
|
|
97
|
+
// the union is NOT collapsed (case 2 fails); branches are still sanitized
|
|
98
|
+
// individually, so the all-strings empty enum gains a string type (case 1).
|
|
99
|
+
expect(sanitizeForGemini({ anyOf: [{ enum: [] }, { enum: ["a"] }] })).toEqual({
|
|
100
|
+
anyOf: [
|
|
101
|
+
{ type: "string", enum: [] },
|
|
102
|
+
{ type: "string", enum: ["a"] },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
expect(sanitizeForGemini({ anyOf: [{ enum: [1, 2] }, "not-an-object"] })).toEqual({
|
|
106
|
+
anyOf: [{ enum: [1, 2] }, "not-an-object"],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("drops $schema and renames $defs to definitions (case 4)", () => {
|
|
111
|
+
expect(
|
|
112
|
+
sanitizeForGemini({
|
|
113
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
114
|
+
$defs: { inner: { enum: ["a"] } },
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: { mode: { enum: ["Build", "Review"] } },
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
definitions: { inner: { type: "string", enum: ["a"] } },
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: { mode: { type: "string", enum: ["Build", "Review"] } },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// structural view of the proxied schema, so tests can reach into the wrapped
|
|
127
|
+
// `~standard`/`toJsonSchema` chain without scattering `any` casts.
|
|
128
|
+
type SchemaView = {
|
|
129
|
+
"~standard": {
|
|
130
|
+
vendor: string;
|
|
131
|
+
validate: unknown;
|
|
132
|
+
jsonSchema: {
|
|
133
|
+
input: (args?: unknown) => unknown;
|
|
134
|
+
output: () => unknown;
|
|
135
|
+
version: string;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
toJsonSchema: (() => unknown) | string;
|
|
139
|
+
infer: unknown;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
function view(schema: StandardSchemaV1<unknown>): SchemaView {
|
|
143
|
+
return schema as unknown as SchemaView;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe("wrapSchemaForGemini", () => {
|
|
147
|
+
function makeSchema(overrides?: Record<string, unknown>): StandardSchemaV1<unknown> {
|
|
148
|
+
return {
|
|
149
|
+
"~standard": {
|
|
150
|
+
version: 1,
|
|
151
|
+
vendor: "arktype",
|
|
152
|
+
validate: () => ({ value: undefined }),
|
|
153
|
+
jsonSchema: {
|
|
154
|
+
input: () => ({ $schema: "x", enum: ["a", "b"] }),
|
|
155
|
+
output: () => ({ $defs: { d: {} }, type: "object" }),
|
|
156
|
+
version: "v1",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
...overrides,
|
|
160
|
+
} as unknown as StandardSchemaV1<unknown>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
it("sanitizes through the ~standard.jsonSchema.input path (xsschema path A)", () => {
|
|
164
|
+
const wrapped = view(wrapSchemaForGemini(makeSchema()));
|
|
165
|
+
const produced = wrapped["~standard"].jsonSchema.input({ target: "draft-07" });
|
|
166
|
+
expect(produced).toEqual({ type: "string", enum: ["a", "b"] });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("sanitizes the jsonSchema.output producer too", () => {
|
|
170
|
+
const wrapped = view(wrapSchemaForGemini(makeSchema()));
|
|
171
|
+
expect(wrapped["~standard"].jsonSchema.output()).toEqual({
|
|
172
|
+
definitions: { d: {} },
|
|
173
|
+
type: "object",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("passes non-function jsonSchema members through unchanged", () => {
|
|
178
|
+
const wrapped = view(wrapSchemaForGemini(makeSchema()));
|
|
179
|
+
expect(wrapped["~standard"].jsonSchema.version).toBe("v1");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("passes other ~standard members (vendor, validate) through unchanged", () => {
|
|
183
|
+
const wrapped = view(wrapSchemaForGemini(makeSchema()));
|
|
184
|
+
expect(wrapped["~standard"].vendor).toBe("arktype");
|
|
185
|
+
expect(typeof wrapped["~standard"].validate).toBe("function");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("sanitizes through the toJsonSchema path (xsschema path B)", () => {
|
|
189
|
+
const schema = makeSchema({
|
|
190
|
+
toJsonSchema: () => ({ $schema: "x", type: "object" }),
|
|
191
|
+
});
|
|
192
|
+
const wrapped = view(wrapSchemaForGemini(schema));
|
|
193
|
+
expect(typeof wrapped.toJsonSchema).toBe("function");
|
|
194
|
+
if (typeof wrapped.toJsonSchema === "function") {
|
|
195
|
+
expect(wrapped.toJsonSchema()).toEqual({ type: "object" });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns a non-function toJsonSchema property as-is", () => {
|
|
200
|
+
const schema = makeSchema({ toJsonSchema: "not callable" });
|
|
201
|
+
const wrapped = view(wrapSchemaForGemini(schema));
|
|
202
|
+
expect(wrapped.toJsonSchema).toBe("not callable");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns a non-object ~standard value as-is", () => {
|
|
206
|
+
const schema = { "~standard": null } as unknown as StandardSchemaV1<unknown>;
|
|
207
|
+
const wrapped = view(wrapSchemaForGemini(schema));
|
|
208
|
+
expect(wrapped["~standard"]).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns a non-object jsonSchema value as-is", () => {
|
|
212
|
+
const schema = {
|
|
213
|
+
"~standard": { version: 1, vendor: "arktype", jsonSchema: "nope" },
|
|
214
|
+
} as unknown as StandardSchemaV1<unknown>;
|
|
215
|
+
const wrapped = view(wrapSchemaForGemini(schema));
|
|
216
|
+
expect(wrapped["~standard"].jsonSchema).toBe("nope");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("passes unrelated schema properties through the outer proxy", () => {
|
|
220
|
+
const schema = makeSchema({ infer: "marker" });
|
|
221
|
+
const wrapped = view(wrapSchemaForGemini(schema));
|
|
222
|
+
expect(wrapped.infer).toBe("marker");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("sanitizeToolForGemini", () => {
|
|
227
|
+
type AnyTool = Tool<Record<string, never>, StandardSchemaV1<unknown>>;
|
|
228
|
+
|
|
229
|
+
it("returns the tool unchanged when it has no parameters", () => {
|
|
230
|
+
const bare = { name: "t", execute: async () => "ok" } as unknown as AnyTool;
|
|
231
|
+
expect(sanitizeToolForGemini(bare)).toBe(bare);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("wraps the parameters schema and preserves the other fields", () => {
|
|
235
|
+
const params = {
|
|
236
|
+
"~standard": {
|
|
237
|
+
version: 1,
|
|
238
|
+
vendor: "arktype",
|
|
239
|
+
jsonSchema: { input: () => ({ enum: ["x"] }) },
|
|
240
|
+
},
|
|
241
|
+
} as unknown as StandardSchemaV1<unknown>;
|
|
242
|
+
const original = {
|
|
243
|
+
name: "t",
|
|
244
|
+
description: "d",
|
|
245
|
+
parameters: params,
|
|
246
|
+
execute: async () => "ok",
|
|
247
|
+
} as unknown as AnyTool;
|
|
248
|
+
const wrapped = sanitizeToolForGemini(original);
|
|
249
|
+
expect(wrapped).not.toBe(original);
|
|
250
|
+
expect(wrapped.name).toBe("t");
|
|
251
|
+
const wrappedParams = wrapped.parameters as StandardSchemaV1<unknown> | undefined;
|
|
252
|
+
expect(wrappedParams).toBeDefined();
|
|
253
|
+
if (wrappedParams) {
|
|
254
|
+
expect(view(wrappedParams)["~standard"].jsonSchema.input()).toEqual({
|
|
255
|
+
type: "string",
|
|
256
|
+
enum: ["x"],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("isGeminiRouted", () => {
|
|
263
|
+
const ctx = (resolvedModel: string | undefined, payloadModel?: string): ToolContext =>
|
|
264
|
+
({ resolvedModel, payload: { model: payloadModel } }) as unknown as ToolContext;
|
|
265
|
+
|
|
266
|
+
it("matches any slug containing 'gemini' regardless of provider prefix", () => {
|
|
267
|
+
expect(isGeminiRouted(ctx("google/gemini-3.1-pro-preview"))).toBe(true);
|
|
268
|
+
expect(isGeminiRouted(ctx("opencode/Gemini-2.5-flash"))).toBe(true);
|
|
269
|
+
expect(isGeminiRouted(ctx("openrouter/google/gemini-2.5-pro"))).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("treats an unresolved specifier (undefined / auto / bare slug) as gemini-possible", () => {
|
|
273
|
+
expect(isGeminiRouted(ctx(undefined))).toBe(true);
|
|
274
|
+
expect(isGeminiRouted(ctx("auto"))).toBe(true);
|
|
275
|
+
expect(isGeminiRouted(ctx("some-unknown-slug"))).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("falls back to the payload model when no resolved model exists", () => {
|
|
279
|
+
expect(isGeminiRouted(ctx(undefined, "google/gemini-2.5-pro"))).toBe(true);
|
|
280
|
+
expect(isGeminiRouted(ctx(undefined, "anthropic/claude-opus-4-7"))).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("returns false for a concrete non-gemini provider/model", () => {
|
|
284
|
+
expect(isGeminiRouted(ctx("anthropic/claude-opus-4-7"))).toBe(false);
|
|
285
|
+
expect(isGeminiRouted(ctx("openai/gpt-5.2"))).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { Tool } from "fastmcp";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
|
|
5
|
+
// ── gemini schema sanitizer ────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// gemini's generateContent API expects an OpenAPI 3.0 Schema subset, not full
|
|
8
|
+
// JSON Schema. arktype 2.x emits constructs that gemini rejects with errors like:
|
|
9
|
+
// - "parameters.<field>.enum: only allowed for STRING type"
|
|
10
|
+
// - "functionDeclaration parameters.<field> schema didn't specify the schema type field"
|
|
11
|
+
// - "anyOf must be the only field in a schema node"
|
|
12
|
+
//
|
|
13
|
+
// transforms applied here:
|
|
14
|
+
// 1. add `type: "string"` to enum-only schemas. arktype emits string literal
|
|
15
|
+
// unions as `{enum: ["a","b"]}` without a `type` field — gemini requires
|
|
16
|
+
// the type declaration for any non-object schema.
|
|
17
|
+
// 2. collapse `{anyOf: [{enum:["a"]}, {enum:["b"]}]}` (older arktype form)
|
|
18
|
+
// into `{type:"string", enum:[...]}`. also handles `{const:"a"}` branches.
|
|
19
|
+
// 3. when `anyOf` / `oneOf` can't be collapsed, strip sibling fields (`type`,
|
|
20
|
+
// `description`, `items`, etc.) — gemini rejects `anyOf` alongside any
|
|
21
|
+
// peer keywords. see opencode #14659.
|
|
22
|
+
// 4. drop `$schema` metadata and rename `$defs` → `definitions` (draft-07
|
|
23
|
+
// compatibility; gemini doesn't understand either).
|
|
24
|
+
//
|
|
25
|
+
// gating: `isGeminiRouted()` detects gemini-targeted traffic so other
|
|
26
|
+
// providers continue to see the original (untransformed) schema.
|
|
27
|
+
//
|
|
28
|
+
// delivery: fastmcp (3.x) uses `xsschema.toJsonSchema()` which reads
|
|
29
|
+
// `schema["~standard"].jsonSchema.input({target:"draft-07"})` when present
|
|
30
|
+
// (arktype 2.x exposes this). we proxy the whole `~standard` chain so our
|
|
31
|
+
// transform runs regardless of which path xsschema takes.
|
|
32
|
+
|
|
33
|
+
function parseStringEnumBranch(item: unknown): { values: string[] } | null {
|
|
34
|
+
if (!item || typeof item !== "object") return null;
|
|
35
|
+
const record = item as Record<string, unknown>;
|
|
36
|
+
if (Array.isArray(record.enum)) {
|
|
37
|
+
const strings = record.enum.filter((v): v is string => typeof v === "string");
|
|
38
|
+
return strings.length === record.enum.length && strings.length > 0 ? { values: strings } : null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof record.const === "string") {
|
|
41
|
+
return { values: [record.const] };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collapseStringUnion(branches: unknown[]): { type: "string"; enum: string[] } | null {
|
|
47
|
+
const values: string[] = [];
|
|
48
|
+
for (const item of branches) {
|
|
49
|
+
const parsed = parseStringEnumBranch(item);
|
|
50
|
+
if (!parsed) return null;
|
|
51
|
+
values.push(...parsed.values);
|
|
52
|
+
}
|
|
53
|
+
if (values.length === 0) return null;
|
|
54
|
+
return { type: "string", enum: [...new Set(values)] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Recursively transform a JSON schema to gemini's stricter subset.
|
|
59
|
+
* See module header for the exact transforms applied.
|
|
60
|
+
*/
|
|
61
|
+
export function sanitizeForGemini(schema: unknown): unknown {
|
|
62
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
63
|
+
if (Array.isArray(schema)) return schema.map(sanitizeForGemini);
|
|
64
|
+
|
|
65
|
+
const source = schema as Record<string, unknown>;
|
|
66
|
+
|
|
67
|
+
// case 1: enum-only string union → add `type: "string"`.
|
|
68
|
+
// arktype emits `type: "'A' | 'B'"` as `{enum: ["A","B"]}` without a type.
|
|
69
|
+
if (Array.isArray(source.enum) && typeof source.type !== "string") {
|
|
70
|
+
const allStrings = source.enum.every((v) => typeof v === "string");
|
|
71
|
+
if (allStrings) {
|
|
72
|
+
const result: Record<string, unknown> = { type: "string", enum: source.enum };
|
|
73
|
+
if (typeof source.description === "string") result.description = source.description;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// case 2: collapsible string-enum union (older arktype form)
|
|
79
|
+
for (const unionKey of ["anyOf", "oneOf"] as const) {
|
|
80
|
+
const branches = source[unionKey];
|
|
81
|
+
if (Array.isArray(branches) && branches.length > 0) {
|
|
82
|
+
const collapsed = collapseStringUnion(branches);
|
|
83
|
+
if (collapsed) {
|
|
84
|
+
const result: Record<string, unknown> = { ...collapsed };
|
|
85
|
+
if (typeof source.description === "string") result.description = source.description;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// case 3: non-collapsible anyOf/oneOf → strip sibling fields (gemini rule)
|
|
92
|
+
if (Array.isArray(source.anyOf) || Array.isArray(source.oneOf)) {
|
|
93
|
+
const result: Record<string, unknown> = {};
|
|
94
|
+
if (Array.isArray(source.anyOf)) result.anyOf = source.anyOf.map(sanitizeForGemini);
|
|
95
|
+
if (Array.isArray(source.oneOf)) result.oneOf = source.oneOf.map(sanitizeForGemini);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// case 4: generic pass — drop $schema, rename $defs, recurse
|
|
100
|
+
const sanitized: Record<string, unknown> = {};
|
|
101
|
+
for (const [key, value] of Object.entries(source)) {
|
|
102
|
+
if (key === "$schema") continue;
|
|
103
|
+
if (key === "$defs") {
|
|
104
|
+
sanitized.definitions = sanitizeForGemini(value);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
sanitized[key] = sanitizeForGemini(value);
|
|
108
|
+
}
|
|
109
|
+
return sanitized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── delivery mechanism ─────────────────────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// fastmcp 3.x resolves the JSON schema via xsschema, which takes two paths:
|
|
115
|
+
// path A: `schema["~standard"].jsonSchema.input({target:"draft-07"})` when
|
|
116
|
+
// the StandardJSONSchemaV1 extension is present (arktype 2.x).
|
|
117
|
+
// path B: `schema.toJsonSchema()` via a vendor-dispatched function (older
|
|
118
|
+
// arktype, other vendors).
|
|
119
|
+
//
|
|
120
|
+
// we proxy both entry points so the transform runs regardless of which path
|
|
121
|
+
// xsschema picks.
|
|
122
|
+
|
|
123
|
+
function wrapJsonSchemaProducer<T extends object>(producer: T): T {
|
|
124
|
+
return new Proxy(producer, {
|
|
125
|
+
get(target, prop, receiver) {
|
|
126
|
+
const value = Reflect.get(target, prop, receiver);
|
|
127
|
+
if ((prop === "input" || prop === "output") && typeof value === "function") {
|
|
128
|
+
const fn = value as (...args: unknown[]) => unknown;
|
|
129
|
+
return (...args: unknown[]) => sanitizeForGemini(fn.apply(target, args));
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function wrapStandard<T extends object>(standard: T): T {
|
|
137
|
+
return new Proxy(standard, {
|
|
138
|
+
get(target, prop, receiver) {
|
|
139
|
+
if (prop === "jsonSchema") {
|
|
140
|
+
const value = Reflect.get(target, prop, receiver);
|
|
141
|
+
if (value && typeof value === "object") {
|
|
142
|
+
return wrapJsonSchemaProducer(value as object);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
return Reflect.get(target, prop, receiver);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function wrapSchemaForGemini(schema: StandardSchemaV1<any>): StandardSchemaV1<any> {
|
|
152
|
+
return new Proxy(schema, {
|
|
153
|
+
get(target, prop, receiver) {
|
|
154
|
+
if (prop === "~standard") {
|
|
155
|
+
const value = Reflect.get(target, prop, receiver);
|
|
156
|
+
if (value && typeof value === "object") {
|
|
157
|
+
return wrapStandard(value as object);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
if (prop === "toJsonSchema") {
|
|
162
|
+
const method = Reflect.get(target, prop, receiver);
|
|
163
|
+
if (typeof method === "function") {
|
|
164
|
+
return () => sanitizeForGemini((method as (...args: unknown[]) => unknown).call(target));
|
|
165
|
+
}
|
|
166
|
+
return method;
|
|
167
|
+
}
|
|
168
|
+
return Reflect.get(target, prop, receiver);
|
|
169
|
+
},
|
|
170
|
+
}) as StandardSchemaV1<any>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function sanitizeToolForGemini<T extends Tool<any, any>>(tool: T): T {
|
|
174
|
+
if (!tool.parameters) return tool;
|
|
175
|
+
return { ...tool, parameters: wrapSchemaForGemini(tool.parameters) } as T;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* true when the effective upstream model is — or might become — google
|
|
180
|
+
* generative language API traffic. matches:
|
|
181
|
+
* - direct `google/*`, opencode `opencode/gemini-*`, openrouter
|
|
182
|
+
* `openrouter/google/gemini-*` (slug substring "gemini" wins).
|
|
183
|
+
* - any unresolved specifier: `undefined`, `"auto"`, or a slug that
|
|
184
|
+
* didn't map through the alias registry (no `provider/` prefix).
|
|
185
|
+
* these flow through the agent's own auto-select, which may land
|
|
186
|
+
* on gemini *after* the MCP server has already registered tools —
|
|
187
|
+
* at which point sanitization is too late to apply. erring on the
|
|
188
|
+
* side of sanitizing is safe: cases 1 + 2 are universally
|
|
189
|
+
* compatible JSON-Schema normalizations (enum-only → typed string,
|
|
190
|
+
* collapsible const-unions → string enum); case 3 is gemini-
|
|
191
|
+
* specific but only fires on non-collapsible unions, which arktype
|
|
192
|
+
* does not emit for our current tool schemas. see issue #676 for
|
|
193
|
+
* the prod failure that motivated this widening.
|
|
194
|
+
*/
|
|
195
|
+
export function isGeminiRouted(ctx: ToolContext): boolean {
|
|
196
|
+
const effective = ctx.resolvedModel ?? ctx.payload.model;
|
|
197
|
+
if (!effective) return true;
|
|
198
|
+
const normalized = effective.toLowerCase();
|
|
199
|
+
if (normalized.includes("gemini")) return true;
|
|
200
|
+
// every concrete model resolved through the registry carries a
|
|
201
|
+
// `provider/` prefix (e.g. "anthropic/claude-opus-4-7"). anything
|
|
202
|
+
// without a slash is either the literal `"auto"` alias or an
|
|
203
|
+
// unrecognized slug that resolveModel logged a warning for — both
|
|
204
|
+
// route through the agent's late auto-select, which may pick gemini.
|
|
205
|
+
if (!normalized.includes("/")) return true;
|
|
206
|
+
return false;
|
|
207
|
+
}
|