@nodetool-ai/runtime 0.7.0-rc.23 → 0.8.1
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/README.md +29 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/agent-memory.d.ts +118 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +186 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/async-local-storage.d.ts +20 -0
- package/dist/async-local-storage.d.ts.map +1 -0
- package/dist/async-local-storage.js +28 -0
- package/dist/async-local-storage.js.map +1 -0
- package/dist/blender-executor.d.ts +93 -0
- package/dist/blender-executor.d.ts.map +1 -0
- package/dist/blender-executor.js +294 -0
- package/dist/blender-executor.js.map +1 -0
- package/dist/comfy-executor.d.ts +23 -1
- package/dist/comfy-executor.d.ts.map +1 -1
- package/dist/comfy-executor.js +234 -55
- package/dist/comfy-executor.js.map +1 -1
- package/dist/context-packer.d.ts +1 -1
- package/dist/context-packer.d.ts.map +1 -1
- package/dist/context-packer.js +23 -25
- package/dist/context-packer.js.map +1 -1
- package/dist/context.d.ts +954 -127
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2424 -513
- package/dist/context.js.map +1 -1
- package/dist/cost-reconciler.d.ts +32 -0
- package/dist/cost-reconciler.d.ts.map +1 -0
- package/dist/cost-reconciler.js +21 -0
- package/dist/cost-reconciler.js.map +1 -0
- package/dist/entities.d.ts +33 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +43 -0
- package/dist/entities.js.map +1 -0
- package/dist/external-media-fetch.d.ts +29 -0
- package/dist/external-media-fetch.d.ts.map +1 -0
- package/dist/external-media-fetch.js +38 -0
- package/dist/external-media-fetch.js.map +1 -0
- package/dist/generation-receipt.d.ts +86 -0
- package/dist/generation-receipt.d.ts.map +1 -0
- package/dist/generation-receipt.js +137 -0
- package/dist/generation-receipt.js.map +1 -0
- package/dist/generation-registry.d.ts +67 -0
- package/dist/generation-registry.d.ts.map +1 -0
- package/dist/generation-registry.js +126 -0
- package/dist/generation-registry.js.map +1 -0
- package/dist/google/client.d.ts +196 -0
- package/dist/google/client.d.ts.map +1 -0
- package/dist/google/client.js +396 -0
- package/dist/google/client.js.map +1 -0
- package/dist/google/index.d.ts +3 -0
- package/dist/google/index.d.ts.map +1 -0
- package/dist/google/index.js +3 -0
- package/dist/google/index.js.map +1 -0
- package/dist/google/token.d.ts +15 -0
- package/dist/google/token.d.ts.map +1 -0
- package/dist/google/token.js +24 -0
- package/dist/google/token.js.map +1 -0
- package/dist/host-binaries.d.ts +106 -0
- package/dist/host-binaries.d.ts.map +1 -0
- package/dist/host-binaries.js +344 -0
- package/dist/host-binaries.js.map +1 -0
- package/dist/image-codec.d.ts +74 -0
- package/dist/image-codec.d.ts.map +1 -0
- package/dist/image-codec.js +273 -0
- package/dist/image-codec.js.map +1 -0
- package/dist/index.d.ts +56 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -4
- package/dist/index.js.map +1 -1
- package/dist/invocation-account.d.ts +43 -0
- package/dist/invocation-account.d.ts.map +1 -0
- package/dist/invocation-account.js +60 -0
- package/dist/invocation-account.js.map +1 -0
- package/dist/media-ref-bytes.d.ts +19 -0
- package/dist/media-ref-bytes.d.ts.map +1 -0
- package/dist/media-ref-bytes.js +181 -0
- package/dist/media-ref-bytes.js.map +1 -0
- package/dist/node-executor.d.ts +113 -2
- package/dist/node-executor.d.ts.map +1 -1
- package/dist/permission-gate.d.ts +152 -0
- package/dist/permission-gate.d.ts.map +1 -0
- package/dist/permission-gate.js +87 -0
- package/dist/permission-gate.js.map +1 -0
- package/dist/prefixed-storage-adapter.d.ts +33 -0
- package/dist/prefixed-storage-adapter.d.ts.map +1 -0
- package/dist/prefixed-storage-adapter.js +70 -0
- package/dist/prefixed-storage-adapter.js.map +1 -0
- package/dist/prompt-asset-refs.d.ts +222 -0
- package/dist/prompt-asset-refs.d.ts.map +1 -0
- package/dist/prompt-asset-refs.js +751 -0
- package/dist/prompt-asset-refs.js.map +1 -0
- package/dist/provider-cache.d.ts +3 -0
- package/dist/provider-cache.d.ts.map +1 -0
- package/dist/provider-cache.js +8 -0
- package/dist/provider-cache.js.map +1 -0
- package/dist/providers/aki-provider.d.ts +33 -10
- package/dist/providers/aki-provider.d.ts.map +1 -1
- package/dist/providers/aki-provider.js +84 -77
- package/dist/providers/aki-provider.js.map +1 -1
- package/dist/providers/alibaba-provider.d.ts +39 -0
- package/dist/providers/alibaba-provider.d.ts.map +1 -0
- package/dist/providers/alibaba-provider.js +86 -0
- package/dist/providers/alibaba-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +52 -3
- package/dist/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/providers/anthropic-provider.js +1020 -338
- package/dist/providers/anthropic-provider.js.map +1 -1
- package/dist/providers/atlascloud-provider.d.ts +101 -0
- package/dist/providers/atlascloud-provider.d.ts.map +1 -0
- package/dist/providers/atlascloud-provider.js +1009 -0
- package/dist/providers/atlascloud-provider.js.map +1 -0
- package/dist/providers/atlascloud-transport.d.ts +100 -0
- package/dist/providers/atlascloud-transport.d.ts.map +1 -0
- package/dist/providers/atlascloud-transport.js +364 -0
- package/dist/providers/atlascloud-transport.js.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.js +104 -0
- package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
- package/dist/providers/audio-mime.d.ts +11 -0
- package/dist/providers/audio-mime.d.ts.map +1 -0
- package/dist/providers/audio-mime.js +46 -0
- package/dist/providers/audio-mime.js.map +1 -0
- package/dist/providers/base-provider.d.ts +481 -24
- package/dist/providers/base-provider.d.ts.map +1 -1
- package/dist/providers/base-provider.js +1259 -42
- package/dist/providers/base-provider.js.map +1 -1
- package/dist/providers/cassette-provider.d.ts +217 -0
- package/dist/providers/cassette-provider.d.ts.map +1 -0
- package/dist/providers/cassette-provider.js +502 -0
- package/dist/providers/cassette-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +6 -12
- package/dist/providers/cerebras-provider.d.ts.map +1 -1
- package/dist/providers/cerebras-provider.js +8 -33
- package/dist/providers/cerebras-provider.js.map +1 -1
- package/dist/providers/claude-agent-provider.d.ts +131 -68
- package/dist/providers/claude-agent-provider.d.ts.map +1 -1
- package/dist/providers/claude-agent-provider.js +1095 -515
- package/dist/providers/claude-agent-provider.js.map +1 -1
- package/dist/providers/codex-provider.d.ts +82 -0
- package/dist/providers/codex-provider.d.ts.map +1 -0
- package/dist/providers/codex-provider.js +492 -0
- package/dist/providers/codex-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +5 -2
- package/dist/providers/cohere-provider.d.ts.map +1 -1
- package/dist/providers/cohere-provider.js +9 -2
- package/dist/providers/cohere-provider.js.map +1 -1
- package/dist/providers/comfy-cloud-provider.d.ts +30 -0
- package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
- package/dist/providers/comfy-cloud-provider.js +42 -0
- package/dist/providers/comfy-cloud-provider.js.map +1 -0
- package/dist/providers/content-filter.d.ts +46 -0
- package/dist/providers/content-filter.d.ts.map +1 -0
- package/dist/providers/content-filter.js +92 -0
- package/dist/providers/content-filter.js.map +1 -0
- package/dist/providers/context-exceeded.d.ts +37 -0
- package/dist/providers/context-exceeded.d.ts.map +1 -0
- package/dist/providers/context-exceeded.js +114 -0
- package/dist/providers/context-exceeded.js.map +1 -0
- package/dist/providers/context-window.d.ts +44 -0
- package/dist/providers/context-window.d.ts.map +1 -0
- package/dist/providers/context-window.js +145 -0
- package/dist/providers/context-window.js.map +1 -0
- package/dist/providers/contract/index.d.ts +4 -0
- package/dist/providers/contract/index.d.ts.map +1 -0
- package/dist/providers/contract/index.js +4 -0
- package/dist/providers/contract/index.js.map +1 -0
- package/dist/providers/contract/probe-manifest.d.ts +75 -0
- package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
- package/dist/providers/contract/probe-manifest.js +379 -0
- package/dist/providers/contract/probe-manifest.js.map +1 -0
- package/dist/providers/contract/probe-runner.d.ts +57 -0
- package/dist/providers/contract/probe-runner.d.ts.map +1 -0
- package/dist/providers/contract/probe-runner.js +180 -0
- package/dist/providers/contract/probe-runner.js.map +1 -0
- package/dist/providers/contract/redact.d.ts +29 -0
- package/dist/providers/contract/redact.d.ts.map +1 -0
- package/dist/providers/contract/redact.js +113 -0
- package/dist/providers/contract/redact.js.map +1 -0
- package/dist/providers/core-tools.d.ts +74 -0
- package/dist/providers/core-tools.d.ts.map +1 -0
- package/dist/providers/core-tools.js +127 -0
- package/dist/providers/core-tools.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +69 -50
- package/dist/providers/cost-calculator.d.ts.map +1 -1
- package/dist/providers/cost-calculator.js +207 -397
- package/dist/providers/cost-calculator.js.map +1 -1
- package/dist/providers/credential-check.d.ts +45 -0
- package/dist/providers/credential-check.d.ts.map +1 -0
- package/dist/providers/credential-check.js +159 -0
- package/dist/providers/credential-check.js.map +1 -0
- package/dist/providers/custom-model-kinds.d.ts +33 -0
- package/dist/providers/custom-model-kinds.d.ts.map +1 -0
- package/dist/providers/custom-model-kinds.js +96 -0
- package/dist/providers/custom-model-kinds.js.map +1 -0
- package/dist/providers/custom-openai-provider.d.ts +81 -0
- package/dist/providers/custom-openai-provider.d.ts.map +1 -0
- package/dist/providers/custom-openai-provider.js +182 -0
- package/dist/providers/custom-openai-provider.js.map +1 -0
- package/dist/providers/custom-provider-registry.d.ts +19 -0
- package/dist/providers/custom-provider-registry.d.ts.map +1 -0
- package/dist/providers/custom-provider-registry.js +52 -0
- package/dist/providers/custom-provider-registry.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +8 -14
- package/dist/providers/deepseek-provider.d.ts.map +1 -1
- package/dist/providers/deepseek-provider.js +6 -35
- package/dist/providers/deepseek-provider.js.map +1 -1
- package/dist/providers/defaults.d.ts +12 -0
- package/dist/providers/defaults.d.ts.map +1 -0
- package/dist/providers/defaults.js +12 -0
- package/dist/providers/defaults.js.map +1 -0
- package/dist/providers/elevenlabs-provider.d.ts +49 -0
- package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
- package/dist/providers/elevenlabs-provider.js +142 -0
- package/dist/providers/elevenlabs-provider.js.map +1 -0
- package/dist/providers/entity-references.d.ts +31 -0
- package/dist/providers/entity-references.d.ts.map +1 -0
- package/dist/providers/entity-references.js +113 -0
- package/dist/providers/entity-references.js.map +1 -0
- package/dist/providers/evolink-provider.d.ts +38 -0
- package/dist/providers/evolink-provider.d.ts.map +1 -0
- package/dist/providers/evolink-provider.js +304 -0
- package/dist/providers/evolink-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +72 -1
- package/dist/providers/fake-provider.d.ts.map +1 -1
- package/dist/providers/fake-provider.js +203 -1
- package/dist/providers/fake-provider.js.map +1 -1
- package/dist/providers/fal-generations.d.ts +106 -0
- package/dist/providers/fal-generations.d.ts.map +1 -0
- package/dist/providers/fal-generations.js +286 -0
- package/dist/providers/fal-generations.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +171 -7
- package/dist/providers/fal-provider.d.ts.map +1 -1
- package/dist/providers/fal-provider.js +1369 -64
- package/dist/providers/fal-provider.js.map +1 -1
- package/dist/providers/fal-queue.d.ts +47 -0
- package/dist/providers/fal-queue.d.ts.map +1 -0
- package/dist/providers/fal-queue.js +231 -0
- package/dist/providers/fal-queue.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +148 -8
- package/dist/providers/gemini-provider.d.ts.map +1 -1
- package/dist/providers/gemini-provider.js +1500 -316
- package/dist/providers/gemini-provider.js.map +1 -1
- package/dist/providers/gmi-provider.d.ts +21 -0
- package/dist/providers/gmi-provider.d.ts.map +1 -0
- package/dist/providers/gmi-provider.js +34 -0
- package/dist/providers/gmi-provider.js.map +1 -0
- package/dist/providers/gpt-image-size.d.ts +3 -0
- package/dist/providers/gpt-image-size.d.ts.map +1 -0
- package/dist/providers/gpt-image-size.js +37 -0
- package/dist/providers/gpt-image-size.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +9 -12
- package/dist/providers/groq-provider.d.ts.map +1 -1
- package/dist/providers/groq-provider.js +19 -33
- package/dist/providers/groq-provider.js.map +1 -1
- package/dist/providers/groq-request.d.ts +38 -0
- package/dist/providers/groq-request.d.ts.map +1 -0
- package/dist/providers/groq-request.js +138 -0
- package/dist/providers/groq-request.js.map +1 -0
- package/dist/providers/higgsfield-provider.d.ts +30 -0
- package/dist/providers/higgsfield-provider.d.ts.map +1 -0
- package/dist/providers/higgsfield-provider.js +131 -0
- package/dist/providers/higgsfield-provider.js.map +1 -0
- package/dist/providers/higgsfield-transport.d.ts +106 -0
- package/dist/providers/higgsfield-transport.d.ts.map +1 -0
- package/dist/providers/higgsfield-transport.js +182 -0
- package/dist/providers/higgsfield-transport.js.map +1 -0
- package/dist/providers/http-transport.d.ts +74 -0
- package/dist/providers/http-transport.d.ts.map +1 -0
- package/dist/providers/http-transport.js +152 -0
- package/dist/providers/http-transport.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +71 -3
- package/dist/providers/huggingface-provider.d.ts.map +1 -1
- package/dist/providers/huggingface-provider.js +230 -34
- package/dist/providers/huggingface-provider.js.map +1 -1
- package/dist/providers/image-mime.d.ts +39 -0
- package/dist/providers/image-mime.d.ts.map +1 -0
- package/dist/providers/image-mime.js +111 -0
- package/dist/providers/image-mime.js.map +1 -0
- package/dist/providers/image-ref.d.ts +21 -0
- package/dist/providers/image-ref.d.ts.map +1 -0
- package/dist/providers/image-ref.js +44 -0
- package/dist/providers/image-ref.js.map +1 -0
- package/dist/providers/index.d.ts +84 -8
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +239 -56
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/jina-provider.d.ts +10 -2
- package/dist/providers/jina-provider.d.ts.map +1 -1
- package/dist/providers/jina-provider.js +16 -7
- package/dist/providers/jina-provider.js.map +1 -1
- package/dist/providers/kie-provider.d.ts +170 -9
- package/dist/providers/kie-provider.d.ts.map +1 -1
- package/dist/providers/kie-provider.js +1176 -228
- package/dist/providers/kie-provider.js.map +1 -1
- package/dist/providers/kie-webhook-registry.d.ts +28 -0
- package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
- package/dist/providers/kie-webhook-registry.js +91 -0
- package/dist/providers/kie-webhook-registry.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +21 -37
- package/dist/providers/llama-provider.d.ts.map +1 -1
- package/dist/providers/llama-provider.js +43 -354
- package/dist/providers/llama-provider.js.map +1 -1
- package/dist/providers/lmstudio-provider.d.ts +4 -7
- package/dist/providers/lmstudio-provider.d.ts.map +1 -1
- package/dist/providers/lmstudio-provider.js +24 -15
- package/dist/providers/lmstudio-provider.js.map +1 -1
- package/dist/providers/manifest-models.d.ts +341 -1
- package/dist/providers/manifest-models.d.ts.map +1 -1
- package/dist/providers/manifest-models.js +1106 -22
- package/dist/providers/manifest-models.js.map +1 -1
- package/dist/providers/media-mime.d.ts +14 -0
- package/dist/providers/media-mime.d.ts.map +1 -0
- package/dist/providers/media-mime.js +35 -0
- package/dist/providers/media-mime.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +14 -1
- package/dist/providers/meshy-provider.d.ts.map +1 -1
- package/dist/providers/meshy-provider.js +76 -46
- package/dist/providers/meshy-provider.js.map +1 -1
- package/dist/providers/meta-provider.d.ts +22 -0
- package/dist/providers/meta-provider.d.ts.map +1 -0
- package/dist/providers/meta-provider.js +54 -0
- package/dist/providers/meta-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +25 -18
- package/dist/providers/minimax-provider.d.ts.map +1 -1
- package/dist/providers/minimax-provider.js +211 -72
- package/dist/providers/minimax-provider.js.map +1 -1
- package/dist/providers/mistral-provider.d.ts +6 -12
- package/dist/providers/mistral-provider.d.ts.map +1 -1
- package/dist/providers/mistral-provider.js +10 -34
- package/dist/providers/mistral-provider.js.map +1 -1
- package/dist/providers/moonshot-provider.d.ts +11 -15
- package/dist/providers/moonshot-provider.d.ts.map +1 -1
- package/dist/providers/moonshot-provider.js +33 -23
- package/dist/providers/moonshot-provider.js.map +1 -1
- package/dist/providers/node-llama-cpp-provider.d.ts +98 -0
- package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
- package/dist/providers/node-llama-cpp-provider.js +479 -0
- package/dist/providers/node-llama-cpp-provider.js.map +1 -0
- package/dist/providers/nodetool-provider.d.ts +49 -0
- package/dist/providers/nodetool-provider.d.ts.map +1 -0
- package/dist/providers/nodetool-provider.js +224 -0
- package/dist/providers/nodetool-provider.js.map +1 -0
- package/dist/providers/oauth/browser-launcher.d.ts +23 -0
- package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
- package/dist/providers/oauth/browser-launcher.js +45 -0
- package/dist/providers/oauth/browser-launcher.js.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.js +133 -0
- package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
- package/dist/providers/oauth/claude-code-login.d.ts +124 -0
- package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-login.js +230 -0
- package/dist/providers/oauth/claude-code-login.js.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
- package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
- package/dist/providers/oauth/errors.d.ts +54 -0
- package/dist/providers/oauth/errors.d.ts.map +1 -0
- package/dist/providers/oauth/errors.js +79 -0
- package/dist/providers/oauth/errors.js.map +1 -0
- package/dist/providers/oauth/index.d.ts +105 -0
- package/dist/providers/oauth/index.d.ts.map +1 -0
- package/dist/providers/oauth/index.js +167 -0
- package/dist/providers/oauth/index.js.map +1 -0
- package/dist/providers/oauth/jwt-claims.d.ts +23 -0
- package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
- package/dist/providers/oauth/jwt-claims.js +67 -0
- package/dist/providers/oauth/jwt-claims.js.map +1 -0
- package/dist/providers/oauth/local-callback-server.d.ts +60 -0
- package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
- package/dist/providers/oauth/local-callback-server.js +148 -0
- package/dist/providers/oauth/local-callback-server.js.map +1 -0
- package/dist/providers/oauth/oauth-client.d.ts +55 -0
- package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/oauth-client.js +161 -0
- package/dist/providers/oauth/oauth-client.js.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.js +265 -0
- package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
- package/dist/providers/oauth/pkce-helper.d.ts +43 -0
- package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
- package/dist/providers/oauth/pkce-helper.js +57 -0
- package/dist/providers/oauth/pkce-helper.js.map +1 -0
- package/dist/providers/oauth/redaction.d.ts +22 -0
- package/dist/providers/oauth/redaction.d.ts.map +1 -0
- package/dist/providers/oauth/redaction.js +59 -0
- package/dist/providers/oauth/redaction.js.map +1 -0
- package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
- package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
- package/dist/providers/oauth/secure-credential-store.js +73 -0
- package/dist/providers/oauth/secure-credential-store.js.map +1 -0
- package/dist/providers/oauth/token-store.d.ts +53 -0
- package/dist/providers/oauth/token-store.d.ts.map +1 -0
- package/dist/providers/oauth/token-store.js +78 -0
- package/dist/providers/oauth/token-store.js.map +1 -0
- package/dist/providers/oauth/types.d.ts +64 -0
- package/dist/providers/oauth/types.d.ts.map +1 -0
- package/dist/providers/oauth/types.js +12 -0
- package/dist/providers/oauth/types.js.map +1 -0
- package/dist/providers/object-tracking.d.ts +47 -0
- package/dist/providers/object-tracking.d.ts.map +1 -0
- package/dist/providers/object-tracking.js +46 -0
- package/dist/providers/object-tracking.js.map +1 -0
- package/dist/providers/offline-model-index.d.ts +20 -0
- package/dist/providers/offline-model-index.d.ts.map +1 -0
- package/dist/providers/offline-model-index.js +112 -0
- package/dist/providers/offline-model-index.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +28 -1
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +193 -43
- package/dist/providers/ollama-provider.js.map +1 -1
- package/dist/providers/openai-compat/client.d.ts +57 -0
- package/dist/providers/openai-compat/client.d.ts.map +1 -0
- package/dist/providers/openai-compat/client.js +157 -0
- package/dist/providers/openai-compat/client.js.map +1 -0
- package/dist/providers/openai-compat/decode.d.ts +36 -0
- package/dist/providers/openai-compat/decode.d.ts.map +1 -0
- package/dist/providers/openai-compat/decode.js +55 -0
- package/dist/providers/openai-compat/decode.js.map +1 -0
- package/dist/providers/openai-compat/errors.d.ts +54 -0
- package/dist/providers/openai-compat/errors.d.ts.map +1 -0
- package/dist/providers/openai-compat/errors.js +144 -0
- package/dist/providers/openai-compat/errors.js.map +1 -0
- package/dist/providers/openai-compat/index.d.ts +8 -0
- package/dist/providers/openai-compat/index.d.ts.map +1 -0
- package/dist/providers/openai-compat/index.js +5 -0
- package/dist/providers/openai-compat/index.js.map +1 -0
- package/dist/providers/openai-compat/sse.d.ts +14 -0
- package/dist/providers/openai-compat/sse.d.ts.map +1 -0
- package/dist/providers/openai-compat/sse.js +69 -0
- package/dist/providers/openai-compat/sse.js.map +1 -0
- package/dist/providers/openai-compat/types.d.ts +75 -0
- package/dist/providers/openai-compat/types.d.ts.map +1 -0
- package/dist/providers/openai-compat/types.js +9 -0
- package/dist/providers/openai-compat/types.js.map +1 -0
- package/dist/providers/openai-compat-provider.d.ts +90 -0
- package/dist/providers/openai-compat-provider.d.ts.map +1 -0
- package/dist/providers/openai-compat-provider.js +282 -0
- package/dist/providers/openai-compat-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +89 -26
- package/dist/providers/openai-provider.d.ts.map +1 -1
- package/dist/providers/openai-provider.js +990 -105
- package/dist/providers/openai-provider.js.map +1 -1
- package/dist/providers/openrouter-provider.d.ts +6 -12
- package/dist/providers/openrouter-provider.d.ts.map +1 -1
- package/dist/providers/openrouter-provider.js +24 -53
- package/dist/providers/openrouter-provider.js.map +1 -1
- package/dist/providers/provider-call-failure.d.ts +39 -0
- package/dist/providers/provider-call-failure.d.ts.map +1 -0
- package/dist/providers/provider-call-failure.js +132 -0
- package/dist/providers/provider-call-failure.js.map +1 -0
- package/dist/providers/provider-error.d.ts +59 -0
- package/dist/providers/provider-error.d.ts.map +1 -0
- package/dist/providers/provider-error.js +165 -0
- package/dist/providers/provider-error.js.map +1 -0
- package/dist/providers/provider-generations.d.ts +99 -0
- package/dist/providers/provider-generations.d.ts.map +1 -0
- package/dist/providers/provider-generations.js +71 -0
- package/dist/providers/provider-generations.js.map +1 -0
- package/dist/providers/provider-queue.d.ts +62 -0
- package/dist/providers/provider-queue.d.ts.map +1 -0
- package/dist/providers/provider-queue.js +28 -0
- package/dist/providers/provider-queue.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +33 -14
- package/dist/providers/provider-registry.d.ts.map +1 -1
- package/dist/providers/provider-registry.js +85 -68
- package/dist/providers/provider-registry.js.map +1 -1
- package/dist/providers/provider-request-log.d.ts +53 -0
- package/dist/providers/provider-request-log.d.ts.map +1 -0
- package/dist/providers/provider-request-log.js +157 -0
- package/dist/providers/provider-request-log.js.map +1 -0
- package/dist/providers/provider-session.d.ts +3 -0
- package/dist/providers/provider-session.d.ts.map +1 -0
- package/dist/providers/provider-session.js +10 -0
- package/dist/providers/provider-session.js.map +1 -0
- package/dist/providers/provider-transport.d.ts +11 -0
- package/dist/providers/provider-transport.d.ts.map +1 -0
- package/dist/providers/provider-transport.js +11 -0
- package/dist/providers/provider-transport.js.map +1 -0
- package/dist/providers/python-provider.d.ts +20 -12
- package/dist/providers/python-provider.d.ts.map +1 -1
- package/dist/providers/python-provider.js +185 -25
- package/dist/providers/python-provider.js.map +1 -1
- package/dist/providers/replicate-provider.d.ts +89 -7
- package/dist/providers/replicate-provider.d.ts.map +1 -1
- package/dist/providers/replicate-provider.js +801 -65
- package/dist/providers/replicate-provider.js.map +1 -1
- package/dist/providers/replicate-retry.d.ts +30 -0
- package/dist/providers/replicate-retry.d.ts.map +1 -0
- package/dist/providers/replicate-retry.js +97 -0
- package/dist/providers/replicate-retry.js.map +1 -0
- package/dist/providers/responses-api.d.ts +27 -0
- package/dist/providers/responses-api.d.ts.map +1 -0
- package/dist/providers/responses-api.js +297 -0
- package/dist/providers/responses-api.js.map +1 -0
- package/dist/providers/reve-provider.d.ts +45 -0
- package/dist/providers/reve-provider.d.ts.map +1 -0
- package/dist/providers/reve-provider.js +159 -0
- package/dist/providers/reve-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +23 -1
- package/dist/providers/rodin-provider.d.ts.map +1 -1
- package/dist/providers/rodin-provider.js +89 -51
- package/dist/providers/rodin-provider.js.map +1 -1
- package/dist/providers/safe-url.d.ts +70 -0
- package/dist/providers/safe-url.d.ts.map +1 -0
- package/dist/providers/safe-url.js +277 -0
- package/dist/providers/safe-url.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +9 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -1
- package/dist/providers/scripted-provider.js +86 -10
- package/dist/providers/scripted-provider.js.map +1 -1
- package/dist/providers/structured-output.d.ts +42 -0
- package/dist/providers/structured-output.d.ts.map +1 -0
- package/dist/providers/structured-output.js +83 -0
- package/dist/providers/structured-output.js.map +1 -0
- package/dist/providers/together-provider.d.ts +19 -16
- package/dist/providers/together-provider.d.ts.map +1 -1
- package/dist/providers/together-provider.js +103 -225
- package/dist/providers/together-provider.js.map +1 -1
- package/dist/providers/topaz-provider.d.ts +44 -0
- package/dist/providers/topaz-provider.d.ts.map +1 -0
- package/dist/providers/topaz-provider.js +239 -0
- package/dist/providers/topaz-provider.js.map +1 -0
- package/dist/providers/types.d.ts +603 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +81 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/useapi-provider.d.ts +27 -0
- package/dist/providers/useapi-provider.d.ts.map +1 -0
- package/dist/providers/useapi-provider.js +266 -0
- package/dist/providers/useapi-provider.js.map +1 -0
- package/dist/providers/video-duration.d.ts +6 -0
- package/dist/providers/video-duration.d.ts.map +1 -0
- package/dist/providers/video-duration.js +63 -0
- package/dist/providers/video-duration.js.map +1 -0
- package/dist/providers/video-frame-fallback.d.ts +39 -0
- package/dist/providers/video-frame-fallback.d.ts.map +1 -0
- package/dist/providers/video-frame-fallback.js +155 -0
- package/dist/providers/video-frame-fallback.js.map +1 -0
- package/dist/providers/video-frames.d.ts +61 -0
- package/dist/providers/video-frames.d.ts.map +1 -0
- package/dist/providers/video-frames.js +208 -0
- package/dist/providers/video-frames.js.map +1 -0
- package/dist/providers/video-mime.d.ts +14 -0
- package/dist/providers/video-mime.d.ts.map +1 -0
- package/dist/providers/video-mime.js +44 -0
- package/dist/providers/video-mime.js.map +1 -0
- package/dist/providers/video-to-audio.d.ts +34 -0
- package/dist/providers/video-to-audio.d.ts.map +1 -0
- package/dist/providers/video-to-audio.js +62 -0
- package/dist/providers/video-to-audio.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -8
- package/dist/providers/vllm-provider.d.ts.map +1 -1
- package/dist/providers/vllm-provider.js +48 -23
- package/dist/providers/vllm-provider.js.map +1 -1
- package/dist/providers/voyage-provider.d.ts +10 -2
- package/dist/providers/voyage-provider.d.ts.map +1 -1
- package/dist/providers/voyage-provider.js +16 -7
- package/dist/providers/voyage-provider.js.map +1 -1
- package/dist/providers/xai-provider.d.ts +54 -13
- package/dist/providers/xai-provider.d.ts.map +1 -1
- package/dist/providers/xai-provider.js +299 -22
- package/dist/providers/xai-provider.js.map +1 -1
- package/dist/python-bridge-base.d.ts +358 -0
- package/dist/python-bridge-base.d.ts.map +1 -0
- package/dist/python-bridge-base.js +1557 -0
- package/dist/python-bridge-base.js.map +1 -0
- package/dist/python-bridge-codec.d.ts +28 -0
- package/dist/python-bridge-codec.d.ts.map +1 -0
- package/dist/python-bridge-codec.js +39 -0
- package/dist/python-bridge-codec.js.map +1 -0
- package/dist/python-bridge-factory.d.ts +52 -0
- package/dist/python-bridge-factory.d.ts.map +1 -0
- package/dist/python-bridge-factory.js +69 -0
- package/dist/python-bridge-factory.js.map +1 -0
- package/dist/python-bridge-framing.d.ts +68 -0
- package/dist/python-bridge-framing.d.ts.map +1 -0
- package/dist/python-bridge-framing.js +91 -0
- package/dist/python-bridge-framing.js.map +1 -0
- package/dist/python-bridge-types.d.ts +545 -2
- package/dist/python-bridge-types.d.ts.map +1 -1
- package/dist/python-bridge-types.js +49 -1
- package/dist/python-bridge-types.js.map +1 -1
- package/dist/python-graph-resolver.d.ts +57 -0
- package/dist/python-graph-resolver.d.ts.map +1 -0
- package/dist/python-graph-resolver.js +79 -0
- package/dist/python-graph-resolver.js.map +1 -0
- package/dist/python-node-executor.d.ts +36 -4
- package/dist/python-node-executor.d.ts.map +1 -1
- package/dist/python-node-executor.js +177 -86
- package/dist/python-node-executor.js.map +1 -1
- package/dist/python-stdio-bridge.d.ts +18 -37
- package/dist/python-stdio-bridge.d.ts.map +1 -1
- package/dist/python-stdio-bridge.js +148 -475
- package/dist/python-stdio-bridge.js.map +1 -1
- package/dist/python-websocket-bridge.d.ts +140 -0
- package/dist/python-websocket-bridge.d.ts.map +1 -0
- package/dist/python-websocket-bridge.js +561 -0
- package/dist/python-websocket-bridge.js.map +1 -0
- package/dist/python-worker-stderr.d.ts +4 -0
- package/dist/python-worker-stderr.d.ts.map +1 -0
- package/dist/python-worker-stderr.js +24 -0
- package/dist/python-worker-stderr.js.map +1 -0
- package/dist/recommended-models.d.ts +2 -2
- package/dist/recommended-models.d.ts.map +1 -1
- package/dist/recommended-models.js +24 -4
- package/dist/recommended-models.js.map +1 -1
- package/dist/recoverable-node-error.d.ts +31 -0
- package/dist/recoverable-node-error.d.ts.map +1 -0
- package/dist/recoverable-node-error.js +37 -0
- package/dist/recoverable-node-error.js.map +1 -0
- package/dist/redact-params.d.ts +13 -0
- package/dist/redact-params.d.ts.map +1 -0
- package/dist/redact-params.js +57 -0
- package/dist/redact-params.js.map +1 -0
- package/dist/sandbox-module-catalog.d.ts +43 -0
- package/dist/sandbox-module-catalog.d.ts.map +1 -0
- package/dist/sandbox-module-catalog.js +25 -0
- package/dist/sandbox-module-catalog.js.map +1 -0
- package/dist/storage-workspace.d.ts +59 -0
- package/dist/storage-workspace.d.ts.map +1 -0
- package/dist/storage-workspace.js +426 -0
- package/dist/storage-workspace.js.map +1 -0
- package/dist/swappable-python-bridge.d.ts +83 -0
- package/dist/swappable-python-bridge.d.ts.map +1 -0
- package/dist/swappable-python-bridge.js +246 -0
- package/dist/swappable-python-bridge.js.map +1 -0
- package/dist/telemetry.d.ts +7 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +23 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/testing.d.ts +81 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +130 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-counter.d.ts +30 -0
- package/dist/token-counter.d.ts.map +1 -0
- package/dist/token-counter.js +156 -0
- package/dist/token-counter.js.map +1 -0
- package/dist/trace-exporters.d.ts +10 -0
- package/dist/trace-exporters.d.ts.map +1 -1
- package/dist/trace-exporters.js +59 -20
- package/dist/trace-exporters.js.map +1 -1
- package/dist/tracing-helpers.d.ts +45 -4
- package/dist/tracing-helpers.d.ts.map +1 -1
- package/dist/tracing-helpers.js +62 -19
- package/dist/tracing-helpers.js.map +1 -1
- package/dist/turn-budget.d.ts +278 -0
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/turn-budget.js +365 -0
- package/dist/turn-budget.js.map +1 -0
- package/dist/variable-channel.d.ts +48 -0
- package/dist/variable-channel.d.ts.map +1 -0
- package/dist/variable-channel.js +110 -0
- package/dist/variable-channel.js.map +1 -0
- package/dist/workspace.d.ts +119 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +112 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +107 -19
|
@@ -1,41 +1,384 @@
|
|
|
1
1
|
import { createLogger } from "@nodetool-ai/config";
|
|
2
2
|
import { BaseProvider } from "./base-provider.js";
|
|
3
|
+
import { geminiContextExceeded } from "./context-exceeded.js";
|
|
4
|
+
import { sniffAudioMime } from "./audio-mime.js";
|
|
5
|
+
import { sniffVideoMime } from "./video-mime.js";
|
|
6
|
+
import { detectImageMime } from "./image-mime.js";
|
|
7
|
+
import { safeFetch } from "./safe-url.js";
|
|
8
|
+
import { ContentFilterRefusal, isContentFilterRefusal } from "./content-filter.js";
|
|
9
|
+
import { isBoolean, isFiniteNumber, isNonEmptyString, isString } from "@nodetool-ai/protocol";
|
|
3
10
|
const log = createLogger("nodetool.runtime.providers.gemini");
|
|
11
|
+
import { WEB_SEARCH_TOOL_NAME } from "./types.js";
|
|
4
12
|
const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
13
|
+
const GEMINI_UPLOAD_BASE = `${GEMINI_API_BASE.replace("/v1beta", "")}/upload/v1beta/files`;
|
|
14
|
+
/**
|
|
15
|
+
* Largest payload sent as `inlineData`. Gemini caps a generateContent request
|
|
16
|
+
* at 20 MB total; anything above this goes through the Files API instead, which
|
|
17
|
+
* leaves room for the rest of the request.
|
|
18
|
+
*/
|
|
19
|
+
export const GEMINI_INLINE_VIDEO_MAX_BYTES = 19 * 1024 * 1024;
|
|
20
|
+
/** Polls of `files/{name}` before an uploaded file is declared stuck. */
|
|
21
|
+
const GEMINI_FILE_MAX_POLLS = 60;
|
|
22
|
+
const GEMINI_FILE_POLL_INTERVAL_MS = 2_000;
|
|
23
|
+
/** Veo accepts at most three `asset` reference images per request. */
|
|
24
|
+
const VEO_MAX_REFERENCE_IMAGES = 3;
|
|
25
|
+
/** Veo requires 8-second clips for reference images and first/last frames. */
|
|
26
|
+
const VEO_CONSTRAINED_DURATION_SECONDS = 8;
|
|
27
|
+
/** Veo appends a fixed 7-second continuation to the input video. */
|
|
28
|
+
const VEO_EXTENSION_SECONDS = 7;
|
|
29
|
+
/** Omni Flash accepts at most three reference clips. */
|
|
30
|
+
const OMNI_MAX_REFERENCE_VIDEOS = 3;
|
|
31
|
+
/**
|
|
32
|
+
* Video models and the tasks each accepts. Veo 3.1 Lite takes no reference
|
|
33
|
+
* images, last frame, or extension. Omni Flash runs through the Interactions
|
|
34
|
+
* API and edits existing video; its duration follows the prompt.
|
|
35
|
+
*
|
|
36
|
+
* Veo lists no `durations`: callers apply that list to extension length too,
|
|
37
|
+
* and Veo generates 4, 6, or 8 seconds but extends by 7 only.
|
|
38
|
+
*/
|
|
39
|
+
const GEMINI_VIDEO_MODELS = [
|
|
40
|
+
{
|
|
41
|
+
id: "veo-3.1-generate-preview",
|
|
42
|
+
name: "Veo 3.1 Preview",
|
|
43
|
+
provider: "gemini",
|
|
44
|
+
supportedTasks: [
|
|
45
|
+
"text_to_video",
|
|
46
|
+
"image_to_video",
|
|
47
|
+
"reference_to_video",
|
|
48
|
+
"extend_video",
|
|
49
|
+
"extend_video_end"
|
|
50
|
+
],
|
|
51
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
52
|
+
aspectRatios: ["16:9", "9:16"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "veo-3.1-fast-generate-preview",
|
|
56
|
+
name: "Veo 3.1 Fast Preview",
|
|
57
|
+
provider: "gemini",
|
|
58
|
+
supportedTasks: [
|
|
59
|
+
"text_to_video",
|
|
60
|
+
"image_to_video",
|
|
61
|
+
"reference_to_video",
|
|
62
|
+
"extend_video",
|
|
63
|
+
"extend_video_end"
|
|
64
|
+
],
|
|
65
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
66
|
+
aspectRatios: ["16:9", "9:16"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "veo-3.1-lite-generate-preview",
|
|
70
|
+
name: "Veo 3.1 Lite Preview",
|
|
71
|
+
provider: "gemini",
|
|
72
|
+
supportedTasks: ["text_to_video", "image_to_video"],
|
|
73
|
+
resolutions: ["720p", "1080p"],
|
|
74
|
+
aspectRatios: ["16:9", "9:16"]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "gemini-omni-1.1-flash",
|
|
78
|
+
name: "Gemini Omni Flash",
|
|
79
|
+
provider: "gemini",
|
|
80
|
+
supportedTasks: [
|
|
81
|
+
"text_to_video",
|
|
82
|
+
"image_to_video",
|
|
83
|
+
"reference_to_video",
|
|
84
|
+
"video_to_video"
|
|
85
|
+
],
|
|
86
|
+
resolutions: ["360p", "720p", "1080p", "4k"],
|
|
87
|
+
aspectRatios: ["16:9", "9:16"]
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
function isOmniVideoModel(modelId) {
|
|
91
|
+
return modelId.startsWith("gemini-omni-");
|
|
92
|
+
}
|
|
93
|
+
function requireVideoTask(modelId, task) {
|
|
94
|
+
const model = GEMINI_VIDEO_MODELS.find((entry) => entry.id === modelId);
|
|
95
|
+
if (!model?.supportedTasks?.includes(task)) {
|
|
96
|
+
throw new Error(`Gemini model ${modelId} does not support ${task}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function combineSignals(signal, timeoutSeconds) {
|
|
100
|
+
const timeout = timeoutSeconds && timeoutSeconds > 0
|
|
101
|
+
? AbortSignal.timeout(timeoutSeconds * 1000)
|
|
102
|
+
: undefined;
|
|
103
|
+
if (!signal)
|
|
104
|
+
return timeout;
|
|
105
|
+
if (!timeout)
|
|
106
|
+
return signal;
|
|
107
|
+
return AbortSignal.any([signal, timeout]);
|
|
108
|
+
}
|
|
109
|
+
/** The image object Veo reads for `image`, `lastFrame`, and references. */
|
|
110
|
+
function veoImage(bytes) {
|
|
111
|
+
return {
|
|
112
|
+
bytesBase64Encoded: Buffer.from(bytes).toString("base64"),
|
|
113
|
+
mimeType: detectImageMime(bytes)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Drop `; charset=…`/`; codecs=…` parameters from a Content-Type header. */
|
|
117
|
+
function stripMimeParams(value) {
|
|
118
|
+
const mime = value?.split(";")[0].trim();
|
|
119
|
+
return mime || undefined;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Normalize an audio mime to one Gemini's `inlineData` accepts. Gemini lists
|
|
123
|
+
* audio/wav, audio/mp3, audio/aiff, audio/aac, audio/ogg and audio/flac; the
|
|
124
|
+
* common `audio/mpeg` label is remapped to `audio/mp3`. Falls back to
|
|
125
|
+
* `audio/mp3` when the type is unknown.
|
|
126
|
+
*/
|
|
127
|
+
function geminiAudioMime(mime) {
|
|
128
|
+
if (!mime)
|
|
129
|
+
return "audio/mp3";
|
|
130
|
+
if (mime === "audio/mpeg" || mime === "audio/mpga")
|
|
131
|
+
return "audio/mp3";
|
|
132
|
+
return mime;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Gemini's native/custom tool combination contract is versioned by model
|
|
136
|
+
* family. Keep this check deliberately based on the model id supplied for the
|
|
137
|
+
* request: model discovery is optional and callers may use a newly released
|
|
138
|
+
* Gemini 3 id before it appears in the catalog.
|
|
139
|
+
*/
|
|
140
|
+
function isGemini3Model(model) {
|
|
141
|
+
const modelId = model.split("/").pop()?.toLowerCase() ?? model.toLowerCase();
|
|
142
|
+
return /^gemini-3(?:[.-]|$)/.test(modelId);
|
|
143
|
+
}
|
|
5
144
|
// Gemini's function-declaration schema is a strict subset of OpenAPI 3.0.
|
|
6
|
-
// It rejects JSON-Schema-only fields like `
|
|
7
|
-
// `$
|
|
145
|
+
// It rejects JSON-Schema-only fields like `const`, `additionalProperties`,
|
|
146
|
+
// `$schema`, `$ref`, `definitions`, `patternProperties`, etc. Any one of these
|
|
8
147
|
// anywhere in the tree causes a 400 that aborts the entire tool batch, so we
|
|
9
|
-
// recursively strip them before sending.
|
|
148
|
+
// recursively strip them before sending. Zod 4's `z.toJSONSchema` (draft
|
|
149
|
+
// 2020-12) emits several of them — `const` for every `z.literal()`, `$ref` +
|
|
150
|
+
// `$defs` for every reused schema — so tools defined in Zod hit this.
|
|
10
151
|
const GEMINI_UNSUPPORTED_SCHEMA_KEYS = new Set([
|
|
11
152
|
"additionalProperties",
|
|
12
153
|
"$schema",
|
|
13
154
|
"$id",
|
|
14
155
|
"$ref",
|
|
156
|
+
"$defs",
|
|
157
|
+
"$comment",
|
|
15
158
|
"definitions",
|
|
16
159
|
"patternProperties",
|
|
160
|
+
"propertyNames",
|
|
17
161
|
"unevaluatedProperties",
|
|
162
|
+
"unevaluatedItems",
|
|
18
163
|
"dependentSchemas",
|
|
19
|
-
"dependentRequired"
|
|
164
|
+
"dependentRequired",
|
|
165
|
+
"exclusiveMinimum",
|
|
166
|
+
"exclusiveMaximum",
|
|
167
|
+
"const",
|
|
168
|
+
"allOf",
|
|
169
|
+
"oneOf",
|
|
170
|
+
"not",
|
|
171
|
+
"if",
|
|
172
|
+
"then",
|
|
173
|
+
"else",
|
|
174
|
+
"prefixItems",
|
|
175
|
+
"additionalItems",
|
|
176
|
+
"contains",
|
|
177
|
+
"minContains",
|
|
178
|
+
"maxContains",
|
|
179
|
+
"uniqueItems",
|
|
180
|
+
"multipleOf",
|
|
181
|
+
"examples",
|
|
182
|
+
"readOnly",
|
|
183
|
+
"writeOnly",
|
|
184
|
+
"deprecated",
|
|
185
|
+
"contentEncoding",
|
|
186
|
+
"contentMediaType"
|
|
20
187
|
]);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
188
|
+
/** Keywords whose value is data, not a schema — never recurse into them. */
|
|
189
|
+
const GEMINI_DATA_KEYS = new Set([
|
|
190
|
+
"enum",
|
|
191
|
+
"const",
|
|
192
|
+
"default",
|
|
193
|
+
"example",
|
|
194
|
+
"examples"
|
|
195
|
+
]);
|
|
196
|
+
function isArraySchemaType(type) {
|
|
197
|
+
if (isString(type))
|
|
198
|
+
return type.toLowerCase() === "array";
|
|
199
|
+
if (Array.isArray(type)) {
|
|
200
|
+
return type.some((t) => isString(t) && t.toLowerCase() === "array");
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function isPlainObject(value) {
|
|
205
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
206
|
+
}
|
|
207
|
+
/** The JSON Schema type name for a primitive literal, if it has one. */
|
|
208
|
+
function primitiveSchemaType(value) {
|
|
209
|
+
if (isString(value))
|
|
210
|
+
return "string";
|
|
211
|
+
if (isBoolean(value))
|
|
212
|
+
return "boolean";
|
|
213
|
+
if (isFiniteNumber(value)) {
|
|
214
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
24
215
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function resolveJsonPointer(root, pointer) {
|
|
219
|
+
if (pointer === "#" || pointer === "")
|
|
220
|
+
return { found: true, value: root };
|
|
221
|
+
if (!pointer.startsWith("#/"))
|
|
222
|
+
return { found: false, value: undefined };
|
|
223
|
+
let cursor = root;
|
|
224
|
+
for (const rawSegment of pointer.slice(2).split("/")) {
|
|
225
|
+
const segment = decodeURIComponent(rawSegment)
|
|
226
|
+
.replace(/~1/g, "/")
|
|
227
|
+
.replace(/~0/g, "~");
|
|
228
|
+
if (Array.isArray(cursor)) {
|
|
229
|
+
const index = Number(segment);
|
|
230
|
+
if (!Number.isInteger(index) || index < 0 || index >= cursor.length) {
|
|
231
|
+
return { found: false, value: undefined };
|
|
232
|
+
}
|
|
233
|
+
cursor = cursor[index];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!isPlainObject(cursor) || !(segment in cursor)) {
|
|
237
|
+
return { found: false, value: undefined };
|
|
238
|
+
}
|
|
239
|
+
cursor = cursor[segment];
|
|
240
|
+
}
|
|
241
|
+
return { found: true, value: cursor };
|
|
242
|
+
}
|
|
243
|
+
function inlineGeminiRefs(node, root, seen) {
|
|
244
|
+
if (Array.isArray(node)) {
|
|
245
|
+
return node.map((item) => inlineGeminiRefs(item, root, seen));
|
|
246
|
+
}
|
|
247
|
+
// SAFETY: a schema node that is neither a list nor an object is a JSON
|
|
248
|
+
// scalar — this walker is only ever handed a decoded JSON Schema.
|
|
249
|
+
if (!isPlainObject(node))
|
|
250
|
+
return node;
|
|
251
|
+
if (isString(node.$ref)) {
|
|
252
|
+
const { $ref, ...rest } = node;
|
|
253
|
+
if (seen.has($ref))
|
|
254
|
+
return { type: "object", ...rest };
|
|
255
|
+
const { found, value } = resolveJsonPointer(root, $ref);
|
|
256
|
+
if (!found || !isPlainObject(value))
|
|
257
|
+
return { type: "object", ...rest };
|
|
258
|
+
// SAFETY: both inputs are objects, so both results are objects too.
|
|
259
|
+
const resolved = inlineGeminiRefs(value, root, new Set([...seen, $ref]));
|
|
260
|
+
// SAFETY: as above.
|
|
261
|
+
const overrides = inlineGeminiRefs(rest, root, seen);
|
|
262
|
+
return { ...resolved, ...overrides };
|
|
263
|
+
}
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [key, value] of Object.entries(node)) {
|
|
266
|
+
out[key] = GEMINI_DATA_KEYS.has(key)
|
|
267
|
+
? // SAFETY: a data key's value is carried through verbatim; it is part of
|
|
268
|
+
// the same decoded JSON Schema.
|
|
269
|
+
value
|
|
270
|
+
: inlineGeminiRefs(value, root, seen);
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
/** Fold `allOf` members into the parent schema; parent keys win. */
|
|
275
|
+
function mergeAllOf(out, members) {
|
|
276
|
+
for (const member of members) {
|
|
277
|
+
const sanitized = sanitizeSchemaNode(member);
|
|
278
|
+
if (!isPlainObject(sanitized))
|
|
279
|
+
continue;
|
|
280
|
+
for (const [key, value] of Object.entries(sanitized)) {
|
|
281
|
+
if (key === "properties" && isPlainObject(value)) {
|
|
282
|
+
// SAFETY: both sides are `properties` maps of the same schema.
|
|
283
|
+
out.properties = {
|
|
284
|
+
...value,
|
|
285
|
+
...(out.properties ?? {})
|
|
286
|
+
};
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (key === "required" && Array.isArray(value)) {
|
|
290
|
+
const existing = Array.isArray(out.required) ? out.required : [];
|
|
291
|
+
out.required = [...new Set([...existing, ...value])];
|
|
29
292
|
continue;
|
|
30
|
-
|
|
293
|
+
}
|
|
294
|
+
if (out[key] === undefined)
|
|
295
|
+
out[key] = value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function sanitizeSchemaNode(value) {
|
|
301
|
+
if (Array.isArray(value))
|
|
302
|
+
return value.map(sanitizeSchemaNode);
|
|
303
|
+
// SAFETY: as in `inlineGeminiRefs` — a non-list, non-object node is a scalar.
|
|
304
|
+
if (!isPlainObject(value))
|
|
305
|
+
return value;
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
308
|
+
if (GEMINI_UNSUPPORTED_SCHEMA_KEYS.has(key))
|
|
309
|
+
continue;
|
|
310
|
+
if (key === "properties" && isPlainObject(nested)) {
|
|
311
|
+
const properties = {};
|
|
312
|
+
// Property *names* are data — a property called "const" must survive.
|
|
313
|
+
for (const [name, sub] of Object.entries(nested)) {
|
|
314
|
+
properties[name] = sanitizeSchemaNode(sub);
|
|
315
|
+
}
|
|
316
|
+
out.properties = properties;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (key === "items") {
|
|
320
|
+
out.items = sanitizeSchemaNode(Array.isArray(nested) ? (nested[0] ?? { type: "string" }) : nested);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (key === "anyOf" && Array.isArray(nested)) {
|
|
324
|
+
out.anyOf = nested.map(sanitizeSchemaNode);
|
|
325
|
+
continue;
|
|
31
326
|
}
|
|
32
|
-
|
|
327
|
+
out[key] = GEMINI_DATA_KEYS.has(key)
|
|
328
|
+
? // SAFETY: a data key's value is carried through verbatim; it is part of
|
|
329
|
+
// the same decoded JSON Schema.
|
|
330
|
+
nested
|
|
331
|
+
: sanitizeSchemaNode(nested);
|
|
332
|
+
}
|
|
333
|
+
// `oneOf` means the same thing to a model as `anyOf`, which Gemini accepts.
|
|
334
|
+
if (out.anyOf === undefined && Array.isArray(value.oneOf)) {
|
|
335
|
+
out.anyOf = value.oneOf.map(sanitizeSchemaNode);
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(value.allOf))
|
|
338
|
+
mergeAllOf(out, value.allOf);
|
|
339
|
+
// `const` is what Zod emits for a literal. A single-value `enum` says the
|
|
340
|
+
// same thing in Gemini's dialect, but only for strings — its `enum` is a
|
|
341
|
+
// list of strings — so other literals keep the constraint in the description.
|
|
342
|
+
if (value.const !== undefined && out.enum === undefined) {
|
|
343
|
+
const literalType = primitiveSchemaType(value.const);
|
|
344
|
+
if (literalType === "string") {
|
|
345
|
+
// SAFETY: `primitiveSchemaType` proved the literal is a string.
|
|
346
|
+
out.enum = [value.const];
|
|
347
|
+
out.type ??= "string";
|
|
348
|
+
}
|
|
349
|
+
else if (literalType) {
|
|
350
|
+
out.type ??= literalType;
|
|
351
|
+
const hint = `Must be ${JSON.stringify(value.const)}.`;
|
|
352
|
+
out.description =
|
|
353
|
+
isNonEmptyString(out.description)
|
|
354
|
+
? `${out.description} ${hint}`
|
|
355
|
+
: hint;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Gemini's `type` is one string; JSON Schema allows a union. `["x","null"]`
|
|
359
|
+
// is Zod's optional/nullable shape and maps onto `nullable`.
|
|
360
|
+
if (Array.isArray(out.type)) {
|
|
361
|
+
const named = out.type.filter((t) => typeof t === "string" && t.toLowerCase() !== "null");
|
|
362
|
+
if (named.length < out.type.length)
|
|
363
|
+
out.nullable = true;
|
|
364
|
+
if (named.length > 0)
|
|
365
|
+
out.type = named[0];
|
|
366
|
+
else
|
|
367
|
+
delete out.type;
|
|
368
|
+
}
|
|
369
|
+
// Gemini rejects an array schema that omits `items` ("items: missing
|
|
370
|
+
// field"). JSON Schema allows it, so backfill a permissive default.
|
|
371
|
+
if (isArraySchemaType(out.type) && out.items === undefined) {
|
|
372
|
+
out.items = { type: "string" };
|
|
33
373
|
}
|
|
34
|
-
return
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
function sanitizeGeminiSchema(value) {
|
|
377
|
+
return sanitizeSchemaNode(inlineGeminiRefs(value, value, new Set()));
|
|
35
378
|
}
|
|
36
379
|
function sanitizeToolName(name) {
|
|
37
380
|
let sanitized = (name ?? "").trim();
|
|
38
|
-
sanitized = sanitized.replace(/[^a-zA-Z0-9_
|
|
381
|
+
sanitized = sanitized.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
39
382
|
sanitized = sanitized.replace(/_+/g, "_");
|
|
40
383
|
if (!sanitized)
|
|
41
384
|
sanitized = "_tool";
|
|
@@ -47,12 +390,174 @@ function sanitizeToolName(name) {
|
|
|
47
390
|
sanitized = "_tool";
|
|
48
391
|
return sanitized;
|
|
49
392
|
}
|
|
393
|
+
function appendGeminiContent(contents, content) {
|
|
394
|
+
const previous = contents[contents.length - 1];
|
|
395
|
+
if (previous?.role === content.role) {
|
|
396
|
+
previous.parts.push(...content.parts);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
contents.push(content);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Sentinel Gemini accepts in place of a real thought signature. Gemini 3
|
|
404
|
+
* rejects a request whose history replays a function call with no signature —
|
|
405
|
+
* which is every turn recorded before signatures were persisted, and every
|
|
406
|
+
* call a caller injected by hand. The documented escape hatch is this literal:
|
|
407
|
+
* https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures
|
|
408
|
+
* Reasoning continuity is lost for that call, so it is only ever a fallback
|
|
409
|
+
* for a signature we do not have.
|
|
410
|
+
*/
|
|
411
|
+
const GEMINI_UNSIGNED_CALL_SENTINEL = "skip_thought_signature_validator";
|
|
412
|
+
/**
|
|
413
|
+
* Gemini validates that the *first* functionCall part of each model turn
|
|
414
|
+
* carries a signature. Stamp the sentinel on any that reaches us without one
|
|
415
|
+
* so an unsigned history fails soft (degraded reasoning) instead of 400.
|
|
416
|
+
*/
|
|
417
|
+
function signUnsignedFunctionCalls(contents) {
|
|
418
|
+
for (const content of contents) {
|
|
419
|
+
if (content.role !== "model")
|
|
420
|
+
continue;
|
|
421
|
+
const first = content.parts.find((p) => p.functionCall !== undefined);
|
|
422
|
+
if (first && !first.thoughtSignature) {
|
|
423
|
+
first.thoughtSignature = GEMINI_UNSIGNED_CALL_SENTINEL;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function geminiResponseError(data) {
|
|
428
|
+
if (data.error?.message)
|
|
429
|
+
return new Error(`Gemini API error: ${data.error.message}`);
|
|
430
|
+
if (data.promptFeedback?.blockReason) {
|
|
431
|
+
const detail = data.promptFeedback.blockReasonMessage
|
|
432
|
+
? `: ${data.promptFeedback.blockReasonMessage}`
|
|
433
|
+
: "";
|
|
434
|
+
return new Error(`Gemini prompt blocked (${data.promptFeedback.blockReason})${detail}`);
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
function parseGeminiResponse(value) {
|
|
439
|
+
if (!isPlainObject(value)) {
|
|
440
|
+
throw new Error("Gemini returned an invalid response envelope");
|
|
441
|
+
}
|
|
442
|
+
const response = value;
|
|
443
|
+
if (response.candidates !== undefined &&
|
|
444
|
+
!Array.isArray(response.candidates)) {
|
|
445
|
+
throw new Error("Gemini returned invalid candidates");
|
|
446
|
+
}
|
|
447
|
+
for (const candidate of response.candidates ?? []) {
|
|
448
|
+
if (candidate.content?.parts !== undefined &&
|
|
449
|
+
!Array.isArray(candidate.content.parts)) {
|
|
450
|
+
throw new Error("Gemini returned invalid candidate parts");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return response;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Decode a `:generateContent` body: validate the envelope, raise the API's own
|
|
457
|
+
* error or block reason, and return the first candidate's parts.
|
|
458
|
+
*
|
|
459
|
+
* The live contract probe and the checked-in raw-response fixtures
|
|
460
|
+
* (`packages/runtime/tests/fixtures/provider-contract/`) run this same
|
|
461
|
+
* function, so a wire change surfaces here rather than only on a real call.
|
|
462
|
+
*/
|
|
463
|
+
export function decodeGeminiGenerateContent(value) {
|
|
464
|
+
const data = parseGeminiResponse(value);
|
|
465
|
+
const dataError = geminiResponseError(data);
|
|
466
|
+
if (dataError)
|
|
467
|
+
throw dataError;
|
|
468
|
+
const candidate = data.candidates?.[0];
|
|
469
|
+
if (!candidate?.content?.parts) {
|
|
470
|
+
throw new Error("Gemini returned no candidates");
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
parts: candidate.content.parts,
|
|
474
|
+
finishReason: candidate.finishReason ?? null,
|
|
475
|
+
usage: data.usageMetadata
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Decode one page of `GET /models`. Throws when the page carries no `models`
|
|
480
|
+
* array; {@link GeminiProvider.getAvailableLanguageModels} treats that the same
|
|
481
|
+
* way it treats an unreachable listing.
|
|
482
|
+
*/
|
|
483
|
+
export function decodeGeminiModelsPage(value) {
|
|
484
|
+
if (!isPlainObject(value)) {
|
|
485
|
+
throw new Error("Gemini model page is not an object");
|
|
486
|
+
}
|
|
487
|
+
const page = value;
|
|
488
|
+
if (!Array.isArray(page.models)) {
|
|
489
|
+
throw new Error("Gemini model page has no `models` array");
|
|
490
|
+
}
|
|
491
|
+
return page;
|
|
492
|
+
}
|
|
493
|
+
function normalizeEmbedding(values) {
|
|
494
|
+
const norm = Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
|
|
495
|
+
return norm > 0 ? values.map((value) => value / norm) : values;
|
|
496
|
+
}
|
|
497
|
+
function abortError(signal) {
|
|
498
|
+
return signal?.reason instanceof Error
|
|
499
|
+
? signal.reason
|
|
500
|
+
: new DOMException("Aborted", "AbortError");
|
|
501
|
+
}
|
|
502
|
+
async function* decodeGeminiSse(body, signal) {
|
|
503
|
+
const reader = body.getReader();
|
|
504
|
+
const decoder = new TextDecoder();
|
|
505
|
+
let buffer = "";
|
|
506
|
+
try {
|
|
507
|
+
while (true) {
|
|
508
|
+
if (signal?.aborted)
|
|
509
|
+
throw abortError(signal);
|
|
510
|
+
const { done, value } = await reader.read();
|
|
511
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
512
|
+
const events = buffer.split(/\r?\n\r?\n/);
|
|
513
|
+
buffer = events.pop() ?? "";
|
|
514
|
+
if (done && buffer.trim()) {
|
|
515
|
+
events.push(buffer);
|
|
516
|
+
buffer = "";
|
|
517
|
+
}
|
|
518
|
+
for (const eventText of events) {
|
|
519
|
+
const data = eventText
|
|
520
|
+
.split(/\r?\n/)
|
|
521
|
+
.filter((line) => line.startsWith("data:"))
|
|
522
|
+
.map((line) => line.slice(5).trimStart())
|
|
523
|
+
.join("\n")
|
|
524
|
+
.trim();
|
|
525
|
+
if (!data || data === "[DONE]")
|
|
526
|
+
continue;
|
|
527
|
+
let parsed;
|
|
528
|
+
try {
|
|
529
|
+
parsed = JSON.parse(data);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
throw new Error("Gemini returned malformed SSE JSON", {
|
|
533
|
+
cause: error
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (!isPlainObject(parsed)) {
|
|
537
|
+
throw new Error("Gemini returned an invalid SSE event");
|
|
538
|
+
}
|
|
539
|
+
yield parseGeminiResponse(parsed);
|
|
540
|
+
}
|
|
541
|
+
if (done)
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
// Stop the underlying connection whenever the consumer bails early (abort
|
|
547
|
+
// or `break`); releasing the lock alone leaves the HTTP body undrained.
|
|
548
|
+
await reader
|
|
549
|
+
.cancel(signal?.aborted ? signal.reason : undefined)
|
|
550
|
+
.catch(() => undefined);
|
|
551
|
+
reader.releaseLock();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
50
554
|
export class GeminiProvider extends BaseProvider {
|
|
51
555
|
static requiredSecrets() {
|
|
52
556
|
return ["GEMINI_API_KEY"];
|
|
53
557
|
}
|
|
54
558
|
apiKey;
|
|
55
559
|
_fetch;
|
|
560
|
+
_sleep;
|
|
56
561
|
constructor(secrets, options = {}) {
|
|
57
562
|
super("gemini");
|
|
58
563
|
const apiKey = secrets.GEMINI_API_KEY;
|
|
@@ -61,6 +566,9 @@ export class GeminiProvider extends BaseProvider {
|
|
|
61
566
|
}
|
|
62
567
|
this.apiKey = apiKey;
|
|
63
568
|
this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
569
|
+
this._sleep =
|
|
570
|
+
options.sleepFn ??
|
|
571
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
64
572
|
}
|
|
65
573
|
getContainerEnv() {
|
|
66
574
|
return { GEMINI_API_KEY: this.apiKey };
|
|
@@ -68,33 +576,55 @@ export class GeminiProvider extends BaseProvider {
|
|
|
68
576
|
async hasToolSupport(_model) {
|
|
69
577
|
return true;
|
|
70
578
|
}
|
|
579
|
+
get supportsNativeWebSearch() {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
/** Gemini reads a whole clip: see {@link videoContentToGeminiPart}. */
|
|
583
|
+
get supportsVideoInput() {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
71
586
|
// ---------------------------------------------------------------------------
|
|
72
587
|
// Model listing
|
|
73
588
|
// ---------------------------------------------------------------------------
|
|
74
589
|
async getAvailableLanguageModels() {
|
|
75
|
-
const
|
|
76
|
-
let
|
|
590
|
+
const items = [];
|
|
591
|
+
let pageToken;
|
|
77
592
|
try {
|
|
78
|
-
|
|
593
|
+
do {
|
|
594
|
+
const query = new URLSearchParams({
|
|
595
|
+
key: this.apiKey,
|
|
596
|
+
pageSize: "1000"
|
|
597
|
+
});
|
|
598
|
+
if (pageToken)
|
|
599
|
+
query.set("pageToken", pageToken);
|
|
600
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/models?${query}`);
|
|
601
|
+
if (!response.ok)
|
|
602
|
+
return [];
|
|
603
|
+
const payload = decodeGeminiModelsPage(await response.json());
|
|
604
|
+
items.push(...(payload.models ?? []));
|
|
605
|
+
pageToken = payload.nextPageToken;
|
|
606
|
+
} while (pageToken);
|
|
79
607
|
}
|
|
80
608
|
catch {
|
|
81
609
|
return [];
|
|
82
610
|
}
|
|
83
|
-
|
|
84
|
-
return [];
|
|
85
|
-
const payload = (await response.json());
|
|
86
|
-
const items = payload.models ?? [];
|
|
611
|
+
const seen = new Set();
|
|
87
612
|
return items
|
|
88
613
|
.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent"))
|
|
89
614
|
.filter((m) => !!m.name)
|
|
615
|
+
.filter((m) => !/(embedding|aqa|imagen|veo|image|tts)/i.test(m.name ?? ""))
|
|
90
616
|
.map((m) => {
|
|
91
617
|
const id = m.name.split("/").pop();
|
|
618
|
+
if (seen.has(id))
|
|
619
|
+
return null;
|
|
620
|
+
seen.add(id);
|
|
92
621
|
return {
|
|
93
622
|
id,
|
|
94
623
|
name: m.displayName ?? id,
|
|
95
624
|
provider: "gemini"
|
|
96
625
|
};
|
|
97
|
-
})
|
|
626
|
+
})
|
|
627
|
+
.filter((model) => model !== null);
|
|
98
628
|
}
|
|
99
629
|
// ---------------------------------------------------------------------------
|
|
100
630
|
// Message conversion helpers
|
|
@@ -107,26 +637,43 @@ export class GeminiProvider extends BaseProvider {
|
|
|
107
637
|
const img = content.image;
|
|
108
638
|
let base64Data;
|
|
109
639
|
let mimeType = img.mimeType ?? "image/jpeg";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
640
|
+
const parseImageDataUri = (uri) => {
|
|
641
|
+
const idx = uri.indexOf(",");
|
|
642
|
+
if (idx < 0)
|
|
643
|
+
throw new Error("Invalid image data URI");
|
|
644
|
+
const header = uri.slice(5, idx);
|
|
645
|
+
mimeType = header.split(";")[0] || mimeType;
|
|
646
|
+
return uri.slice(idx + 1);
|
|
647
|
+
};
|
|
648
|
+
if (isNonEmptyString(img.data) ||
|
|
649
|
+
(img.data instanceof Uint8Array && img.data.length > 0)) {
|
|
650
|
+
if (isString(img.data)) {
|
|
651
|
+
// Inline data may itself be a data: URI — strip the prefix and take
|
|
652
|
+
// the real mime type from it rather than shipping the header as
|
|
653
|
+
// base64 payload.
|
|
654
|
+
base64Data = img.data.startsWith("data:")
|
|
655
|
+
? parseImageDataUri(img.data)
|
|
656
|
+
: img.data;
|
|
113
657
|
}
|
|
114
658
|
else {
|
|
115
659
|
base64Data = Buffer.from(img.data).toString("base64");
|
|
116
660
|
}
|
|
117
661
|
}
|
|
118
662
|
else if (img.uri) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
663
|
+
// resolveUri turns asset file:// URIs (what the chat pipeline produces)
|
|
664
|
+
// into data: URIs; http(s) URIs pass through to safeFetch.
|
|
665
|
+
const resolvedUri = img.uri.startsWith("data:")
|
|
666
|
+
? img.uri
|
|
667
|
+
: await this.resolveUri(img.uri);
|
|
668
|
+
if (resolvedUri.startsWith("data:")) {
|
|
669
|
+
base64Data = parseImageDataUri(resolvedUri);
|
|
124
670
|
}
|
|
125
671
|
else {
|
|
126
|
-
const resp = await this._fetch
|
|
672
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
127
673
|
if (!resp.ok)
|
|
128
674
|
throw new Error(`Failed to fetch image: ${resp.status}`);
|
|
129
|
-
mimeType =
|
|
675
|
+
mimeType =
|
|
676
|
+
stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
|
|
130
677
|
base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
|
|
131
678
|
}
|
|
132
679
|
}
|
|
@@ -138,87 +685,271 @@ export class GeminiProvider extends BaseProvider {
|
|
|
138
685
|
if (content.type === "audio") {
|
|
139
686
|
const aud = content.audio;
|
|
140
687
|
let base64Data;
|
|
141
|
-
let mimeType = aud.mimeType
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
688
|
+
let mimeType = aud.mimeType;
|
|
689
|
+
const parseAudioDataUri = (uri) => {
|
|
690
|
+
const idx = uri.indexOf(",");
|
|
691
|
+
if (idx < 0)
|
|
692
|
+
throw new Error("Invalid audio data URI");
|
|
693
|
+
const header = uri.slice(5, idx);
|
|
694
|
+
mimeType = mimeType ?? header.split(";")[0];
|
|
695
|
+
return uri.slice(idx + 1);
|
|
696
|
+
};
|
|
697
|
+
if (isNonEmptyString(aud.data) ||
|
|
698
|
+
(aud.data instanceof Uint8Array && aud.data.length > 0)) {
|
|
699
|
+
if (isString(aud.data)) {
|
|
700
|
+
base64Data = aud.data.startsWith("data:")
|
|
701
|
+
? parseAudioDataUri(aud.data)
|
|
702
|
+
: aud.data;
|
|
703
|
+
mimeType =
|
|
704
|
+
mimeType ?? sniffAudioMime(Buffer.from(base64Data, "base64"));
|
|
145
705
|
}
|
|
146
706
|
else {
|
|
147
|
-
|
|
707
|
+
const bytes = Buffer.from(aud.data);
|
|
708
|
+
base64Data = bytes.toString("base64");
|
|
709
|
+
mimeType = mimeType ?? sniffAudioMime(bytes);
|
|
148
710
|
}
|
|
149
711
|
}
|
|
150
712
|
else if (aud.uri) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
base64Data =
|
|
713
|
+
const resolvedUri = aud.uri.startsWith("data:")
|
|
714
|
+
? aud.uri
|
|
715
|
+
: await this.resolveUri(aud.uri);
|
|
716
|
+
if (resolvedUri.startsWith("data:")) {
|
|
717
|
+
base64Data = parseAudioDataUri(resolvedUri);
|
|
156
718
|
}
|
|
157
719
|
else {
|
|
158
|
-
const resp = await this._fetch
|
|
720
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
159
721
|
if (!resp.ok)
|
|
160
722
|
throw new Error(`Failed to fetch audio: ${resp.status}`);
|
|
161
|
-
|
|
162
|
-
|
|
723
|
+
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
724
|
+
mimeType =
|
|
725
|
+
stripMimeParams(resp.headers.get("content-type")) ??
|
|
726
|
+
mimeType ??
|
|
727
|
+
sniffAudioMime(bytes);
|
|
728
|
+
base64Data = bytes.toString("base64");
|
|
163
729
|
}
|
|
164
730
|
}
|
|
165
731
|
else {
|
|
166
732
|
base64Data = "";
|
|
167
733
|
}
|
|
168
|
-
return {
|
|
734
|
+
return {
|
|
735
|
+
inlineData: { mimeType: geminiAudioMime(mimeType), data: base64Data }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (content.type === "video") {
|
|
739
|
+
return await this.videoContentToGeminiPart(content.video);
|
|
169
740
|
}
|
|
170
741
|
return { text: "[unsupported content type]" };
|
|
171
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Source a video's bytes the way the image and audio branches do, then send
|
|
745
|
+
* them inline when small enough and through the Files API when not.
|
|
746
|
+
*/
|
|
747
|
+
async videoContentToGeminiPart(video) {
|
|
748
|
+
let bytes;
|
|
749
|
+
let mimeType = video.mimeType;
|
|
750
|
+
const parseVideoDataUri = (uri) => {
|
|
751
|
+
const idx = uri.indexOf(",");
|
|
752
|
+
if (idx < 0)
|
|
753
|
+
throw new Error("Invalid video data URI");
|
|
754
|
+
const header = uri.slice(5, idx);
|
|
755
|
+
mimeType = mimeType ?? stripMimeParams(header.split(";base64")[0]);
|
|
756
|
+
return Buffer.from(uri.slice(idx + 1), "base64");
|
|
757
|
+
};
|
|
758
|
+
if (isNonEmptyString(video.data) ||
|
|
759
|
+
(video.data instanceof Uint8Array && video.data.length > 0)) {
|
|
760
|
+
if (isString(video.data)) {
|
|
761
|
+
bytes = video.data.startsWith("data:")
|
|
762
|
+
? parseVideoDataUri(video.data)
|
|
763
|
+
: Buffer.from(video.data, "base64");
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
bytes = Buffer.from(video.data);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else if (video.uri) {
|
|
770
|
+
const resolvedUri = video.uri.startsWith("data:")
|
|
771
|
+
? video.uri
|
|
772
|
+
: await this.resolveUri(video.uri);
|
|
773
|
+
if (resolvedUri.startsWith("data:")) {
|
|
774
|
+
bytes = parseVideoDataUri(resolvedUri);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
778
|
+
if (!resp.ok)
|
|
779
|
+
throw new Error(`Failed to fetch video: ${resp.status}`);
|
|
780
|
+
mimeType =
|
|
781
|
+
stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
|
|
782
|
+
bytes = Buffer.from(await resp.arrayBuffer());
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
bytes = Buffer.alloc(0);
|
|
787
|
+
}
|
|
788
|
+
const resolvedMime = mimeType ?? sniffVideoMime(bytes);
|
|
789
|
+
if (bytes.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
|
|
790
|
+
const fileUri = await this.uploadFileToGemini(bytes, resolvedMime);
|
|
791
|
+
return { fileData: { mimeType: resolvedMime, fileUri } };
|
|
792
|
+
}
|
|
793
|
+
return {
|
|
794
|
+
inlineData: { mimeType: resolvedMime, data: bytes.toString("base64") }
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Upload bytes with the Files API resumable protocol and wait until the file
|
|
799
|
+
* is ACTIVE — Gemini rejects a `fileData` part that references a file still
|
|
800
|
+
* being processed. Returns the file's uri.
|
|
801
|
+
*/
|
|
802
|
+
async uploadFileToGemini(bytes, mimeType) {
|
|
803
|
+
const startResp = await this._fetch(`${GEMINI_UPLOAD_BASE}?key=${this.apiKey}`, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: {
|
|
806
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
807
|
+
"X-Goog-Upload-Command": "start",
|
|
808
|
+
"X-Goog-Upload-Header-Content-Length": String(bytes.length),
|
|
809
|
+
"X-Goog-Upload-Header-Content-Type": mimeType,
|
|
810
|
+
"Content-Type": "application/json"
|
|
811
|
+
},
|
|
812
|
+
body: JSON.stringify({
|
|
813
|
+
file: { display_name: `nodetool-upload.${mimeType.split("/")[1]}` }
|
|
814
|
+
})
|
|
815
|
+
});
|
|
816
|
+
if (!startResp.ok) {
|
|
817
|
+
const errText = await startResp.text();
|
|
818
|
+
throw new Error(`Gemini file upload failed to start ${startResp.status}: ${errText}`);
|
|
819
|
+
}
|
|
820
|
+
const uploadUrl = startResp.headers.get("x-goog-upload-url");
|
|
821
|
+
if (!uploadUrl) {
|
|
822
|
+
throw new Error("Gemini file upload returned no X-Goog-Upload-URL");
|
|
823
|
+
}
|
|
824
|
+
const uploadResp = await this._fetch(uploadUrl, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
headers: {
|
|
827
|
+
"Content-Length": String(bytes.length),
|
|
828
|
+
"X-Goog-Upload-Offset": "0",
|
|
829
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
830
|
+
},
|
|
831
|
+
body: new Blob([new Uint8Array(bytes)], {
|
|
832
|
+
type: mimeType
|
|
833
|
+
})
|
|
834
|
+
});
|
|
835
|
+
if (!uploadResp.ok) {
|
|
836
|
+
const errText = await uploadResp.text();
|
|
837
|
+
throw new Error(`Gemini file upload failed ${uploadResp.status}: ${errText}`);
|
|
838
|
+
}
|
|
839
|
+
const uploaded = (await uploadResp.json());
|
|
840
|
+
let file = uploaded.file;
|
|
841
|
+
if (!file?.name || !file.uri) {
|
|
842
|
+
throw new Error("Gemini file upload returned no file record");
|
|
843
|
+
}
|
|
844
|
+
const fileUri = file.uri;
|
|
845
|
+
const fileName = file.name;
|
|
846
|
+
for (let poll = 0; poll < GEMINI_FILE_MAX_POLLS; poll++) {
|
|
847
|
+
if (file?.state === "ACTIVE")
|
|
848
|
+
return fileUri;
|
|
849
|
+
if (file?.state === "FAILED") {
|
|
850
|
+
throw new Error(`Gemini file processing failed: ${file.error?.message ?? "unknown error"}`);
|
|
851
|
+
}
|
|
852
|
+
await this._sleep(GEMINI_FILE_POLL_INTERVAL_MS);
|
|
853
|
+
const pollResp = await this._fetch(`${GEMINI_API_BASE}/${fileName}`, {
|
|
854
|
+
headers: { "x-goog-api-key": this.apiKey }
|
|
855
|
+
});
|
|
856
|
+
if (!pollResp.ok) {
|
|
857
|
+
const errText = await pollResp.text();
|
|
858
|
+
throw new Error(`Gemini file poll failed ${pollResp.status}: ${errText}`);
|
|
859
|
+
}
|
|
860
|
+
file = (await pollResp.json());
|
|
861
|
+
}
|
|
862
|
+
if (file?.state === "ACTIVE")
|
|
863
|
+
return fileUri;
|
|
864
|
+
throw new Error(`Gemini file ${fileName} did not become ACTIVE in time`);
|
|
865
|
+
}
|
|
172
866
|
/**
|
|
173
867
|
* Convert our Message array into Gemini contents + optional system instruction.
|
|
174
868
|
*/
|
|
175
|
-
async convertMessages(messages) {
|
|
869
|
+
async convertMessages(messages, nameMap = new Map()) {
|
|
176
870
|
let systemInstruction;
|
|
177
871
|
const contents = [];
|
|
872
|
+
// Gemini correlates a tool result to its call by the function *name*, not
|
|
873
|
+
// by id (and our tool-call ids are synthesized — never valid Gemini
|
|
874
|
+
// function names). Map each tool-call id back to its function name so the
|
|
875
|
+
// `functionResponse.name` below matches the earlier `functionCall.name`.
|
|
876
|
+
const toolCallNames = new Map();
|
|
877
|
+
for (const m of messages) {
|
|
878
|
+
if (m.role === "assistant" && m.toolCalls) {
|
|
879
|
+
for (const tc of m.toolCalls) {
|
|
880
|
+
if (tc.id)
|
|
881
|
+
toolCallNames.set(tc.id, nameMap.get(tc.name) ?? tc.name);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
178
885
|
for (const msg of messages) {
|
|
179
886
|
if (msg.role === "system") {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
887
|
+
const instruction = isString(msg.content)
|
|
888
|
+
? msg.content
|
|
889
|
+
: (msg.content ?? [])
|
|
890
|
+
.filter((c) => c.type === "text")
|
|
891
|
+
.map((c) => c.text)
|
|
892
|
+
.join(" ");
|
|
893
|
+
systemInstruction = systemInstruction
|
|
894
|
+
? `${systemInstruction}\n${instruction}`
|
|
895
|
+
: instruction;
|
|
187
896
|
continue;
|
|
188
897
|
}
|
|
189
898
|
if (msg.role === "tool") {
|
|
190
|
-
// Tool result →
|
|
191
|
-
|
|
899
|
+
// Tool result → user role with functionResponse part. The name must
|
|
900
|
+
// match the originating functionCall's name, resolved from the call id.
|
|
901
|
+
const responseText = isString(msg.content)
|
|
192
902
|
? msg.content
|
|
193
903
|
: JSON.stringify(msg.content);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
904
|
+
const functionName = (msg.toolCallId ? toolCallNames.get(msg.toolCallId) : undefined) ??
|
|
905
|
+
msg.toolCallId ??
|
|
906
|
+
"unknown";
|
|
907
|
+
const responsePart = {
|
|
908
|
+
functionResponse: {
|
|
909
|
+
name: functionName,
|
|
910
|
+
id: msg.toolCallId ?? undefined,
|
|
911
|
+
response: { result: responseText }
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
// Merge parallel tool results into a single user turn so the request
|
|
915
|
+
// keeps alternating user/model roles.
|
|
916
|
+
const prev = contents[contents.length - 1];
|
|
917
|
+
if (prev &&
|
|
918
|
+
prev.role === "user" &&
|
|
919
|
+
prev.parts.length > 0 &&
|
|
920
|
+
prev.parts.every((p) => p.functionResponse !== undefined)) {
|
|
921
|
+
prev.parts.push(responsePart);
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
appendGeminiContent(contents, {
|
|
925
|
+
role: "user",
|
|
926
|
+
parts: [responsePart]
|
|
927
|
+
});
|
|
928
|
+
}
|
|
205
929
|
continue;
|
|
206
930
|
}
|
|
207
931
|
if (msg.role === "assistant") {
|
|
208
932
|
// If we have raw Gemini parts (with thought content), replay them exactly
|
|
209
933
|
if (msg._rawGeminiParts && Array.isArray(msg._rawGeminiParts)) {
|
|
210
|
-
contents
|
|
934
|
+
// Copy: the contents we build get merged and stamped below, and the
|
|
935
|
+
// message's own parts must survive that untouched for the next turn.
|
|
936
|
+
appendGeminiContent(contents, {
|
|
211
937
|
role: "model",
|
|
212
|
-
parts: msg._rawGeminiParts
|
|
938
|
+
parts: msg._rawGeminiParts.map((part) => ({
|
|
939
|
+
...part
|
|
940
|
+
}))
|
|
213
941
|
});
|
|
214
942
|
continue;
|
|
215
943
|
}
|
|
216
944
|
const parts = [];
|
|
217
|
-
// Tool calls
|
|
218
945
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
219
946
|
for (const tc of msg.toolCalls) {
|
|
220
947
|
const part = {
|
|
221
|
-
functionCall: {
|
|
948
|
+
functionCall: {
|
|
949
|
+
id: tc.id,
|
|
950
|
+
name: nameMap.get(tc.name) ?? tc.name,
|
|
951
|
+
args: tc.args
|
|
952
|
+
}
|
|
222
953
|
};
|
|
223
954
|
if (tc.thought_signature) {
|
|
224
955
|
part.thoughtSignature = tc.thought_signature;
|
|
@@ -226,8 +957,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
226
957
|
parts.push(part);
|
|
227
958
|
}
|
|
228
959
|
}
|
|
229
|
-
|
|
230
|
-
if (typeof msg.content === "string" && msg.content) {
|
|
960
|
+
if (isNonEmptyString(msg.content)) {
|
|
231
961
|
parts.push({ text: msg.content });
|
|
232
962
|
}
|
|
233
963
|
else if (Array.isArray(msg.content)) {
|
|
@@ -236,13 +966,12 @@ export class GeminiProvider extends BaseProvider {
|
|
|
236
966
|
}
|
|
237
967
|
}
|
|
238
968
|
if (parts.length > 0) {
|
|
239
|
-
contents
|
|
969
|
+
appendGeminiContent(contents, { role: "model", parts });
|
|
240
970
|
}
|
|
241
971
|
continue;
|
|
242
972
|
}
|
|
243
|
-
// user
|
|
244
973
|
const parts = [];
|
|
245
|
-
if (
|
|
974
|
+
if (isString(msg.content)) {
|
|
246
975
|
parts.push({ text: msg.content });
|
|
247
976
|
}
|
|
248
977
|
else if (Array.isArray(msg.content)) {
|
|
@@ -251,17 +980,26 @@ export class GeminiProvider extends BaseProvider {
|
|
|
251
980
|
}
|
|
252
981
|
}
|
|
253
982
|
if (parts.length > 0) {
|
|
254
|
-
contents
|
|
983
|
+
appendGeminiContent(contents, { role: "user", parts });
|
|
255
984
|
}
|
|
256
985
|
}
|
|
986
|
+
signUnsignedFunctionCalls(contents);
|
|
257
987
|
return { contents, systemInstruction };
|
|
258
988
|
}
|
|
259
989
|
formatTools(tools) {
|
|
990
|
+
return this.formatToolsForModel(tools, "gemini-3");
|
|
991
|
+
}
|
|
992
|
+
formatToolsForModel(tools, model) {
|
|
260
993
|
const nameMap = new Map();
|
|
261
994
|
const reverseMap = new Map();
|
|
262
995
|
const usedNames = new Set();
|
|
263
996
|
const declarations = [];
|
|
264
997
|
for (const tool of tools) {
|
|
998
|
+
if ((tool.name === WEB_SEARCH_TOOL_NAME &&
|
|
999
|
+
this.usesNativeWebSearch(model, tools)) ||
|
|
1000
|
+
tool.type === "code_interpreter") {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
265
1003
|
const original = tool.name;
|
|
266
1004
|
let unique = sanitizeToolName(original);
|
|
267
1005
|
let suffix = 2;
|
|
@@ -273,7 +1011,10 @@ export class GeminiProvider extends BaseProvider {
|
|
|
273
1011
|
usedNames.add(unique);
|
|
274
1012
|
nameMap.set(original, unique);
|
|
275
1013
|
reverseMap.set(unique, original);
|
|
276
|
-
const rawParameters = tool.inputSchema ?? {
|
|
1014
|
+
const rawParameters = tool.inputSchema ?? {
|
|
1015
|
+
type: "object",
|
|
1016
|
+
properties: {}
|
|
1017
|
+
};
|
|
277
1018
|
declarations.push({
|
|
278
1019
|
name: unique,
|
|
279
1020
|
description: tool.description ?? "",
|
|
@@ -286,20 +1027,82 @@ export class GeminiProvider extends BaseProvider {
|
|
|
286
1027
|
reverseMap
|
|
287
1028
|
};
|
|
288
1029
|
}
|
|
1030
|
+
usesNativeWebSearch(model, tools) {
|
|
1031
|
+
const hasWebSearch = tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
|
|
1032
|
+
if (!hasWebSearch)
|
|
1033
|
+
return false;
|
|
1034
|
+
const hasCustomTools = this.hasCustomFunctionTools(tools);
|
|
1035
|
+
return !hasCustomTools || isGemini3Model(model);
|
|
1036
|
+
}
|
|
1037
|
+
hasCustomFunctionTools(tools) {
|
|
1038
|
+
return tools.some((tool) => tool.name !== WEB_SEARCH_TOOL_NAME &&
|
|
1039
|
+
tool.type !== "code_interpreter");
|
|
1040
|
+
}
|
|
1041
|
+
validateToolCombination(model, tools) {
|
|
1042
|
+
if (isGemini3Model(model))
|
|
1043
|
+
return;
|
|
1044
|
+
const hasCodeExecution = tools.some((tool) => tool.type === "code_interpreter");
|
|
1045
|
+
if (hasCodeExecution && this.hasCustomFunctionTools(tools)) {
|
|
1046
|
+
throw new Error(`Gemini code execution cannot be combined with other tools on ${model}; ` +
|
|
1047
|
+
"only Gemini 3 models support this combination");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Fill in `tools` and `toolConfig` on a request body.
|
|
1052
|
+
*
|
|
1053
|
+
* Built-in tools (googleSearch, codeExecution) run server side. When they are
|
|
1054
|
+
* combined with functionDeclarations, Gemini rejects the request unless
|
|
1055
|
+
* `toolConfig.includeServerSideToolInvocations` is set — the built-in calls
|
|
1056
|
+
* and their results have to be echoed back into the conversation for the
|
|
1057
|
+
* function-calling loop to stay coherent.
|
|
1058
|
+
*/
|
|
1059
|
+
applyTools(body, model, tools, geminiTools, nameMap, toolChoice) {
|
|
1060
|
+
if (geminiTools.length > 0) {
|
|
1061
|
+
body.tools = geminiTools;
|
|
1062
|
+
}
|
|
1063
|
+
let hasBuiltIn = false;
|
|
1064
|
+
if (this.usesNativeWebSearch(model, tools)) {
|
|
1065
|
+
body.tools = [...(body.tools ?? []), { googleSearch: {} }];
|
|
1066
|
+
hasBuiltIn = true;
|
|
1067
|
+
}
|
|
1068
|
+
if (tools.some((tool) => tool.type === "code_interpreter")) {
|
|
1069
|
+
body.tools = [...(body.tools ?? []), { codeExecution: {} }];
|
|
1070
|
+
hasBuiltIn = true;
|
|
1071
|
+
}
|
|
1072
|
+
const toolConfig = {};
|
|
1073
|
+
if (hasBuiltIn && geminiTools.length > 0) {
|
|
1074
|
+
toolConfig.includeServerSideToolInvocations = true;
|
|
1075
|
+
}
|
|
1076
|
+
if (toolChoice &&
|
|
1077
|
+
(toolChoice === "any" ? geminiTools.length > 0 : nameMap.has(toolChoice))) {
|
|
1078
|
+
const selected = toolChoice === "any"
|
|
1079
|
+
? undefined
|
|
1080
|
+
: (nameMap.get(toolChoice) ?? sanitizeToolName(toolChoice));
|
|
1081
|
+
const functionCallingConfig = {
|
|
1082
|
+
mode: "ANY"
|
|
1083
|
+
};
|
|
1084
|
+
if (selected) {
|
|
1085
|
+
functionCallingConfig.allowedFunctionNames = [selected];
|
|
1086
|
+
}
|
|
1087
|
+
toolConfig.functionCallingConfig = functionCallingConfig;
|
|
1088
|
+
}
|
|
1089
|
+
if (Object.keys(toolConfig).length > 0) {
|
|
1090
|
+
body.toolConfig = toolConfig;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
289
1093
|
// ---------------------------------------------------------------------------
|
|
290
1094
|
// Non-streaming generation
|
|
291
1095
|
// ---------------------------------------------------------------------------
|
|
292
1096
|
async generateMessage(args) {
|
|
293
|
-
const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
|
|
294
|
-
|
|
295
|
-
const { geminiTools, reverseMap } = this.
|
|
1097
|
+
const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
|
|
1098
|
+
this.validateToolCombination(model, tools);
|
|
1099
|
+
const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
|
|
1100
|
+
const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
|
|
296
1101
|
const body = { contents };
|
|
297
1102
|
if (systemInstruction) {
|
|
298
1103
|
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
299
1104
|
}
|
|
300
|
-
|
|
301
|
-
body.tools = geminiTools;
|
|
302
|
-
}
|
|
1105
|
+
this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
|
|
303
1106
|
const generationConfig = {
|
|
304
1107
|
maxOutputTokens: maxTokens
|
|
305
1108
|
};
|
|
@@ -307,13 +1110,19 @@ export class GeminiProvider extends BaseProvider {
|
|
|
307
1110
|
generationConfig.temperature = temperature;
|
|
308
1111
|
if (topP != null)
|
|
309
1112
|
generationConfig.topP = topP;
|
|
1113
|
+
if (presencePenalty != null)
|
|
1114
|
+
generationConfig.presencePenalty = presencePenalty;
|
|
1115
|
+
if (frequencyPenalty != null)
|
|
1116
|
+
generationConfig.frequencyPenalty = frequencyPenalty;
|
|
310
1117
|
body.generationConfig = generationConfig;
|
|
311
1118
|
log.debug("Gemini request", { model });
|
|
312
1119
|
const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
|
|
1120
|
+
this.recordRequestPayload(body);
|
|
313
1121
|
const response = await this._fetch(url, {
|
|
314
1122
|
method: "POST",
|
|
315
1123
|
headers: { "Content-Type": "application/json" },
|
|
316
|
-
body: JSON.stringify(body)
|
|
1124
|
+
body: JSON.stringify(body),
|
|
1125
|
+
signal: args.signal
|
|
317
1126
|
});
|
|
318
1127
|
if (!response.ok) {
|
|
319
1128
|
const text = await response.text();
|
|
@@ -323,30 +1132,33 @@ export class GeminiProvider extends BaseProvider {
|
|
|
323
1132
|
});
|
|
324
1133
|
throw new Error(`Gemini API error ${response.status}: ${text}`);
|
|
325
1134
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1135
|
+
const decoded = decodeGeminiGenerateContent(await response.json());
|
|
1136
|
+
this.trackGeminiUsage(model, decoded.usage);
|
|
1137
|
+
return this.extractMessage(decoded.parts, reverseMap);
|
|
1138
|
+
}
|
|
1139
|
+
/** Record token usage from a Gemini usageMetadata block (if present). */
|
|
1140
|
+
trackGeminiUsage(model, usage) {
|
|
1141
|
+
if (!usage)
|
|
1142
|
+
return;
|
|
1143
|
+
this.trackUsage(model, {
|
|
1144
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
1145
|
+
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
|
|
1146
|
+
cachedTokens: usage.cachedContentTokenCount ?? 0
|
|
1147
|
+
});
|
|
335
1148
|
}
|
|
336
1149
|
// ---------------------------------------------------------------------------
|
|
337
1150
|
// Streaming generation
|
|
338
1151
|
// ---------------------------------------------------------------------------
|
|
339
1152
|
async *generateMessages(args) {
|
|
340
|
-
const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
|
|
341
|
-
|
|
342
|
-
const { geminiTools, reverseMap } = this.
|
|
1153
|
+
const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
|
|
1154
|
+
this.validateToolCombination(model, tools);
|
|
1155
|
+
const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
|
|
1156
|
+
const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
|
|
343
1157
|
const body = { contents };
|
|
344
1158
|
if (systemInstruction) {
|
|
345
1159
|
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
346
1160
|
}
|
|
347
|
-
|
|
348
|
-
body.tools = geminiTools;
|
|
349
|
-
}
|
|
1161
|
+
this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
|
|
350
1162
|
const generationConfig = {
|
|
351
1163
|
maxOutputTokens: maxTokens
|
|
352
1164
|
};
|
|
@@ -354,13 +1166,19 @@ export class GeminiProvider extends BaseProvider {
|
|
|
354
1166
|
generationConfig.temperature = temperature;
|
|
355
1167
|
if (topP != null)
|
|
356
1168
|
generationConfig.topP = topP;
|
|
1169
|
+
if (presencePenalty != null)
|
|
1170
|
+
generationConfig.presencePenalty = presencePenalty;
|
|
1171
|
+
if (frequencyPenalty != null)
|
|
1172
|
+
generationConfig.frequencyPenalty = frequencyPenalty;
|
|
357
1173
|
body.generationConfig = generationConfig;
|
|
358
1174
|
log.debug("Gemini request", { model });
|
|
359
1175
|
const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
|
|
1176
|
+
this.recordRequestPayload(body);
|
|
360
1177
|
const response = await this._fetch(url, {
|
|
361
1178
|
method: "POST",
|
|
362
1179
|
headers: { "Content-Type": "application/json" },
|
|
363
|
-
body: JSON.stringify(body)
|
|
1180
|
+
body: JSON.stringify(body),
|
|
1181
|
+
signal: args.signal
|
|
364
1182
|
});
|
|
365
1183
|
if (!response.ok) {
|
|
366
1184
|
const text = await response.text();
|
|
@@ -373,71 +1191,51 @@ export class GeminiProvider extends BaseProvider {
|
|
|
373
1191
|
if (!response.body) {
|
|
374
1192
|
throw new Error("Gemini streaming response has no body");
|
|
375
1193
|
}
|
|
376
|
-
const reader = response.body.getReader();
|
|
377
|
-
const decoder = new TextDecoder();
|
|
378
|
-
let buffer = "";
|
|
379
1194
|
// Accumulate all parts across SSE events for raw replay.
|
|
380
1195
|
// Gemini thinking models emit thought parts and function calls across
|
|
381
1196
|
// separate SSE events, but they must all be sent back together.
|
|
382
1197
|
const allParts = [];
|
|
383
1198
|
const pendingToolCalls = [];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
yield chunk;
|
|
417
|
-
}
|
|
418
|
-
else if (part.functionCall) {
|
|
419
|
-
const originalName = reverseMap.get(part.functionCall.name) ??
|
|
420
|
-
part.functionCall.name;
|
|
421
|
-
const toolCall = {
|
|
422
|
-
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
423
|
-
name: originalName,
|
|
424
|
-
args: part.functionCall.args ?? {}
|
|
425
|
-
};
|
|
426
|
-
if (part.thoughtSignature) {
|
|
427
|
-
toolCall.thought_signature = part.thoughtSignature;
|
|
428
|
-
}
|
|
429
|
-
pendingToolCalls.push(toolCall);
|
|
430
|
-
}
|
|
431
|
-
// Thought text parts are silently accumulated in allParts
|
|
1199
|
+
// Gemini SSE reports CUMULATIVE usageMetadata; keep the last one seen and
|
|
1200
|
+
// record it once after the stream (accumulating each event would over-count).
|
|
1201
|
+
let lastUsage;
|
|
1202
|
+
for await (const event of decodeGeminiSse(response.body, args.signal)) {
|
|
1203
|
+
const eventError = geminiResponseError(event);
|
|
1204
|
+
if (eventError)
|
|
1205
|
+
throw eventError;
|
|
1206
|
+
if (event.usageMetadata)
|
|
1207
|
+
lastUsage = event.usageMetadata;
|
|
1208
|
+
const parts = event.candidates?.[0]?.content?.parts;
|
|
1209
|
+
if (!parts)
|
|
1210
|
+
continue;
|
|
1211
|
+
for (const part of parts) {
|
|
1212
|
+
allParts.push(part);
|
|
1213
|
+
if (part.text !== undefined && !part.thought) {
|
|
1214
|
+
const chunk = {
|
|
1215
|
+
type: "chunk",
|
|
1216
|
+
content: part.text,
|
|
1217
|
+
done: false
|
|
1218
|
+
};
|
|
1219
|
+
yield chunk;
|
|
1220
|
+
}
|
|
1221
|
+
else if (part.functionCall) {
|
|
1222
|
+
const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
|
|
1223
|
+
const toolCall = {
|
|
1224
|
+
id: part.functionCall.id ??
|
|
1225
|
+
`call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1226
|
+
name: originalName,
|
|
1227
|
+
args: part.functionCall.args ?? {}
|
|
1228
|
+
};
|
|
1229
|
+
if (part.thoughtSignature) {
|
|
1230
|
+
toolCall.thought_signature = part.thoughtSignature;
|
|
432
1231
|
}
|
|
1232
|
+
pendingToolCalls.push(toolCall);
|
|
433
1233
|
}
|
|
434
1234
|
}
|
|
435
1235
|
}
|
|
436
|
-
|
|
437
|
-
reader.releaseLock();
|
|
438
|
-
}
|
|
1236
|
+
this.trackGeminiUsage(model, lastUsage);
|
|
439
1237
|
// Attach accumulated raw parts to tool calls for thought replay
|
|
440
|
-
const hasThoughts = allParts.some((p) => p.thought);
|
|
1238
|
+
const hasThoughts = allParts.some((p) => p.thought || p.thoughtSignature);
|
|
441
1239
|
for (const tc of pendingToolCalls) {
|
|
442
1240
|
if (hasThoughts) {
|
|
443
1241
|
tc._rawGeminiParts = allParts;
|
|
@@ -459,13 +1257,14 @@ export class GeminiProvider extends BaseProvider {
|
|
|
459
1257
|
const textParts = [];
|
|
460
1258
|
const toolCalls = [];
|
|
461
1259
|
for (const part of parts) {
|
|
462
|
-
if (part.text !== undefined) {
|
|
1260
|
+
if (part.text !== undefined && !part.thought) {
|
|
463
1261
|
textParts.push(part.text);
|
|
464
1262
|
}
|
|
465
1263
|
else if (part.functionCall) {
|
|
466
1264
|
const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
|
|
467
1265
|
const tc = {
|
|
468
|
-
id:
|
|
1266
|
+
id: part.functionCall.id ??
|
|
1267
|
+
`call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
469
1268
|
name: originalName,
|
|
470
1269
|
args: part.functionCall.args ?? {}
|
|
471
1270
|
};
|
|
@@ -475,7 +1274,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
475
1274
|
toolCalls.push(tc);
|
|
476
1275
|
}
|
|
477
1276
|
}
|
|
478
|
-
const hasThoughts = parts.some((p) => p.thought);
|
|
1277
|
+
const hasThoughts = parts.some((p) => p.thought || p.thoughtSignature);
|
|
479
1278
|
const msg = {
|
|
480
1279
|
role: "assistant",
|
|
481
1280
|
content: textParts.join("") || null,
|
|
@@ -492,11 +1291,50 @@ export class GeminiProvider extends BaseProvider {
|
|
|
492
1291
|
async getAvailableImageModels() {
|
|
493
1292
|
return [
|
|
494
1293
|
{
|
|
495
|
-
id: "gemini-
|
|
496
|
-
name: "Gemini
|
|
497
|
-
provider: "gemini"
|
|
1294
|
+
id: "gemini-3.1-flash-image",
|
|
1295
|
+
name: "Gemini 3.1 Flash Image",
|
|
1296
|
+
provider: "gemini",
|
|
1297
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1298
|
+
aspectRatios: [
|
|
1299
|
+
"1:1",
|
|
1300
|
+
"2:3",
|
|
1301
|
+
"3:2",
|
|
1302
|
+
"3:4",
|
|
1303
|
+
"4:3",
|
|
1304
|
+
"4:5",
|
|
1305
|
+
"5:4",
|
|
1306
|
+
"9:16",
|
|
1307
|
+
"16:9",
|
|
1308
|
+
"21:9"
|
|
1309
|
+
],
|
|
1310
|
+
resolutions: ["1K", "2K", "4K"]
|
|
498
1311
|
},
|
|
499
|
-
{
|
|
1312
|
+
{
|
|
1313
|
+
id: "gemini-3.1-flash-lite-image",
|
|
1314
|
+
name: "Gemini 3.1 Flash-Lite Image",
|
|
1315
|
+
provider: "gemini",
|
|
1316
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1317
|
+
aspectRatios: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"]
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
id: "gemini-3-pro-image",
|
|
1321
|
+
name: "Gemini 3 Pro Image",
|
|
1322
|
+
provider: "gemini",
|
|
1323
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1324
|
+
aspectRatios: [
|
|
1325
|
+
"1:1",
|
|
1326
|
+
"2:3",
|
|
1327
|
+
"3:2",
|
|
1328
|
+
"3:4",
|
|
1329
|
+
"4:3",
|
|
1330
|
+
"4:5",
|
|
1331
|
+
"5:4",
|
|
1332
|
+
"9:16",
|
|
1333
|
+
"16:9",
|
|
1334
|
+
"21:9"
|
|
1335
|
+
],
|
|
1336
|
+
resolutions: ["1K", "2K", "4K"]
|
|
1337
|
+
}
|
|
500
1338
|
];
|
|
501
1339
|
}
|
|
502
1340
|
async getAvailableTTSModels() {
|
|
@@ -533,6 +1371,30 @@ export class GeminiProvider extends BaseProvider {
|
|
|
533
1371
|
"Sulafat"
|
|
534
1372
|
];
|
|
535
1373
|
return [
|
|
1374
|
+
{
|
|
1375
|
+
id: "gemini-3.8-flash-tts",
|
|
1376
|
+
name: "Gemini 3.8 Flash TTS",
|
|
1377
|
+
provider: "gemini",
|
|
1378
|
+
voices
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
id: "gemini-3.8-flash-lite-tts",
|
|
1382
|
+
name: "Gemini 3.8 Flash-Lite TTS",
|
|
1383
|
+
provider: "gemini",
|
|
1384
|
+
voices
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
id: "gemini-3.1-flash-tts-preview",
|
|
1388
|
+
name: "Gemini 3.1 Flash TTS Preview",
|
|
1389
|
+
provider: "gemini",
|
|
1390
|
+
voices
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
id: "gemini-2.5-flash-preview-tts",
|
|
1394
|
+
name: "Gemini 2.5 Flash TTS",
|
|
1395
|
+
provider: "gemini",
|
|
1396
|
+
voices
|
|
1397
|
+
},
|
|
536
1398
|
{
|
|
537
1399
|
id: "gemini-2.5-pro-preview-tts",
|
|
538
1400
|
name: "Gemini 2.5 Pro TTS",
|
|
@@ -543,27 +1405,51 @@ export class GeminiProvider extends BaseProvider {
|
|
|
543
1405
|
}
|
|
544
1406
|
async getAvailableASRModels() {
|
|
545
1407
|
return [
|
|
546
|
-
{
|
|
547
|
-
|
|
1408
|
+
{
|
|
1409
|
+
// The transcription-specific model. It answers `generateContent`, so
|
|
1410
|
+
// it runs through the same path as the general models below.
|
|
1411
|
+
id: "gemini-3.5-transcribe",
|
|
1412
|
+
name: "Gemini 3.5 Transcribe",
|
|
1413
|
+
provider: "gemini"
|
|
1414
|
+
},
|
|
1415
|
+
{
|
|
1416
|
+
id: "gemini-3.5-flash",
|
|
1417
|
+
name: "Gemini 3.5 Flash",
|
|
1418
|
+
provider: "gemini"
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
id: "gemini-3.1-flash-lite",
|
|
1422
|
+
name: "Gemini 3.1 Flash-Lite",
|
|
1423
|
+
provider: "gemini"
|
|
1424
|
+
}
|
|
548
1425
|
];
|
|
549
1426
|
}
|
|
550
1427
|
async getAvailableVideoModels() {
|
|
1428
|
+
return [...GEMINI_VIDEO_MODELS];
|
|
1429
|
+
}
|
|
1430
|
+
async getAvailableMusicModels() {
|
|
551
1431
|
return [
|
|
552
1432
|
{
|
|
553
|
-
id: "
|
|
554
|
-
name: "
|
|
555
|
-
provider: "gemini"
|
|
1433
|
+
id: "lyria-3.5",
|
|
1434
|
+
name: "Lyria 3.5",
|
|
1435
|
+
provider: "gemini",
|
|
1436
|
+
supportedTasks: ["text_to_music"]
|
|
556
1437
|
},
|
|
557
|
-
{
|
|
1438
|
+
{
|
|
1439
|
+
id: "lyria-3-clip-preview",
|
|
1440
|
+
name: "Lyria 3 Clip Preview",
|
|
1441
|
+
provider: "gemini",
|
|
1442
|
+
supportedTasks: ["text_to_music"]
|
|
1443
|
+
}
|
|
558
1444
|
];
|
|
559
1445
|
}
|
|
560
1446
|
async getAvailableEmbeddingModels() {
|
|
561
1447
|
return [
|
|
562
1448
|
{
|
|
563
|
-
id: "
|
|
564
|
-
name: "
|
|
1449
|
+
id: "gemini-embedding-2",
|
|
1450
|
+
name: "Gemini Embedding 2",
|
|
565
1451
|
provider: "gemini",
|
|
566
|
-
dimensions:
|
|
1452
|
+
dimensions: 3072
|
|
567
1453
|
},
|
|
568
1454
|
{
|
|
569
1455
|
id: "gemini-embedding-001",
|
|
@@ -581,7 +1467,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
581
1467
|
if (!text || (Array.isArray(text) && text.length === 0)) {
|
|
582
1468
|
throw new Error("text must not be empty");
|
|
583
1469
|
}
|
|
584
|
-
const texts =
|
|
1470
|
+
const texts = isString(text) ? [text] : text;
|
|
585
1471
|
// Gemini embedContent supports a single content; batch by calling per text
|
|
586
1472
|
const embeddings = [];
|
|
587
1473
|
for (const t of texts) {
|
|
@@ -605,7 +1491,9 @@ export class GeminiProvider extends BaseProvider {
|
|
|
605
1491
|
if (!data.embedding?.values) {
|
|
606
1492
|
throw new Error("No embedding returned from Gemini API");
|
|
607
1493
|
}
|
|
608
|
-
embeddings.push(
|
|
1494
|
+
embeddings.push(dimensions && dimensions < 3072
|
|
1495
|
+
? normalizeEmbedding(data.embedding.values)
|
|
1496
|
+
: data.embedding.values);
|
|
609
1497
|
}
|
|
610
1498
|
return embeddings;
|
|
611
1499
|
}
|
|
@@ -619,11 +1507,20 @@ export class GeminiProvider extends BaseProvider {
|
|
|
619
1507
|
const modelId = params.model.id;
|
|
620
1508
|
if (modelId.startsWith("gemini-")) {
|
|
621
1509
|
// Use generateContent with IMAGE response modality
|
|
1510
|
+
const imageConfig = {};
|
|
1511
|
+
if (params.aspectRatio)
|
|
1512
|
+
imageConfig.aspectRatio = params.aspectRatio;
|
|
1513
|
+
if (params.resolution)
|
|
1514
|
+
imageConfig.imageSize = params.resolution;
|
|
1515
|
+
const generationConfig = {
|
|
1516
|
+
responseModalities: ["IMAGE", "TEXT"]
|
|
1517
|
+
};
|
|
1518
|
+
if (Object.keys(imageConfig).length > 0) {
|
|
1519
|
+
generationConfig.imageConfig = imageConfig;
|
|
1520
|
+
}
|
|
622
1521
|
const body = {
|
|
623
1522
|
contents: [{ role: "user", parts: [{ text: params.prompt }] }],
|
|
624
|
-
generationConfig
|
|
625
|
-
responseModalities: ["IMAGE", "TEXT"]
|
|
626
|
-
}
|
|
1523
|
+
generationConfig
|
|
627
1524
|
};
|
|
628
1525
|
const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
|
|
629
1526
|
const response = await this._fetch(url, {
|
|
@@ -635,7 +1532,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
635
1532
|
const errText = await response.text();
|
|
636
1533
|
throw new Error(`Gemini text-to-image failed ${response.status}: ${errText}`);
|
|
637
1534
|
}
|
|
638
|
-
const data = (await response.json());
|
|
1535
|
+
const data = parseGeminiResponse(await response.json());
|
|
639
1536
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
640
1537
|
if (!parts)
|
|
641
1538
|
throw new Error("No candidates in response");
|
|
@@ -646,33 +1543,13 @@ export class GeminiProvider extends BaseProvider {
|
|
|
646
1543
|
}
|
|
647
1544
|
throw new Error("No image data returned in response");
|
|
648
1545
|
}
|
|
649
|
-
// Imagen
|
|
650
|
-
|
|
651
|
-
instances: [{ prompt: params.prompt }],
|
|
652
|
-
parameters: { sampleCount: 1 }
|
|
653
|
-
};
|
|
654
|
-
const url = `${GEMINI_API_BASE}/models/${modelId}:generateImages?key=${this.apiKey}`;
|
|
655
|
-
const response = await this._fetch(url, {
|
|
656
|
-
method: "POST",
|
|
657
|
-
headers: { "Content-Type": "application/json" },
|
|
658
|
-
body: JSON.stringify(body)
|
|
659
|
-
});
|
|
660
|
-
if (!response.ok) {
|
|
661
|
-
const errText = await response.text();
|
|
662
|
-
throw new Error(`Gemini image generation failed ${response.status}: ${errText}`);
|
|
663
|
-
}
|
|
664
|
-
const data = (await response.json());
|
|
665
|
-
// Try predictions format first (Vertex-style), then generatedImages
|
|
666
|
-
const b64 = data.predictions?.[0]?.bytesBase64Encoded ??
|
|
667
|
-
data.generatedImages?.[0]?.image?.imageBytes;
|
|
668
|
-
if (!b64)
|
|
669
|
-
throw new Error("No image data in response");
|
|
670
|
-
return Uint8Array.from(Buffer.from(b64, "base64"));
|
|
1546
|
+
// Google shut down Imagen in the Gemini API; `predict` now answers 404.
|
|
1547
|
+
throw new Error(`Model ${modelId} is not available. Use a gemini-*-image model for text-to-image.`);
|
|
671
1548
|
}
|
|
672
1549
|
// ---------------------------------------------------------------------------
|
|
673
1550
|
// Image-to-image
|
|
674
1551
|
// ---------------------------------------------------------------------------
|
|
675
|
-
async imageToImage(
|
|
1552
|
+
async imageToImage(images, params) {
|
|
676
1553
|
if (!params.prompt) {
|
|
677
1554
|
throw new Error("The input prompt cannot be empty.");
|
|
678
1555
|
}
|
|
@@ -680,20 +1557,36 @@ export class GeminiProvider extends BaseProvider {
|
|
|
680
1557
|
if (!modelId.startsWith("gemini-")) {
|
|
681
1558
|
throw new Error(`Model ${modelId} does not support image-to-image. Only gemini-* models supported.`);
|
|
682
1559
|
}
|
|
683
|
-
const
|
|
1560
|
+
const imageParts = images
|
|
1561
|
+
.filter((b) => b && b.length > 0)
|
|
1562
|
+
.map((b) => ({
|
|
1563
|
+
inlineData: {
|
|
1564
|
+
mimeType: "image/png",
|
|
1565
|
+
data: Buffer.from(b).toString("base64")
|
|
1566
|
+
}
|
|
1567
|
+
}));
|
|
1568
|
+
if (imageParts.length === 0) {
|
|
1569
|
+
throw new Error("At least one input image is required");
|
|
1570
|
+
}
|
|
1571
|
+
const imageConfig = {};
|
|
1572
|
+
if (params.aspectRatio)
|
|
1573
|
+
imageConfig.aspectRatio = params.aspectRatio;
|
|
1574
|
+
if (params.resolution)
|
|
1575
|
+
imageConfig.imageSize = params.resolution;
|
|
1576
|
+
const generationConfig = {
|
|
1577
|
+
responseModalities: ["IMAGE", "TEXT"]
|
|
1578
|
+
};
|
|
1579
|
+
if (Object.keys(imageConfig).length > 0) {
|
|
1580
|
+
generationConfig.imageConfig = imageConfig;
|
|
1581
|
+
}
|
|
684
1582
|
const body = {
|
|
685
1583
|
contents: [
|
|
686
1584
|
{
|
|
687
1585
|
role: "user",
|
|
688
|
-
parts: [
|
|
689
|
-
{ text: params.prompt },
|
|
690
|
-
{ inlineData: { mimeType: "image/png", data: imageBase64 } }
|
|
691
|
-
]
|
|
1586
|
+
parts: [{ text: params.prompt }, ...imageParts]
|
|
692
1587
|
}
|
|
693
1588
|
],
|
|
694
|
-
generationConfig
|
|
695
|
-
responseModalities: ["IMAGE", "TEXT"]
|
|
696
|
-
}
|
|
1589
|
+
generationConfig
|
|
697
1590
|
};
|
|
698
1591
|
const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
|
|
699
1592
|
const response = await this._fetch(url, {
|
|
@@ -705,7 +1598,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
705
1598
|
const errText = await response.text();
|
|
706
1599
|
throw new Error(`Gemini image-to-image failed ${response.status}: ${errText}`);
|
|
707
1600
|
}
|
|
708
|
-
const data = (await response.json());
|
|
1601
|
+
const data = parseGeminiResponse(await response.json());
|
|
709
1602
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
710
1603
|
if (!parts)
|
|
711
1604
|
throw new Error("No candidates in response");
|
|
@@ -720,7 +1613,14 @@ export class GeminiProvider extends BaseProvider {
|
|
|
720
1613
|
// Text-to-speech
|
|
721
1614
|
// ---------------------------------------------------------------------------
|
|
722
1615
|
async *textToSpeech(args) {
|
|
723
|
-
const {
|
|
1616
|
+
const { model, voice = "Puck" } = args;
|
|
1617
|
+
const text = [
|
|
1618
|
+
args.language ? `Speak in ${args.language}.` : undefined,
|
|
1619
|
+
args.instructions,
|
|
1620
|
+
args.text
|
|
1621
|
+
]
|
|
1622
|
+
.filter((value) => value !== undefined)
|
|
1623
|
+
.join(" ");
|
|
724
1624
|
const body = {
|
|
725
1625
|
contents: [{ role: "user", parts: [{ text }] }],
|
|
726
1626
|
generationConfig: {
|
|
@@ -742,7 +1642,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
742
1642
|
const errText = await response.text();
|
|
743
1643
|
throw new Error(`Gemini TTS failed ${response.status}: ${errText}`);
|
|
744
1644
|
}
|
|
745
|
-
const data = (await response.json());
|
|
1645
|
+
const data = parseGeminiResponse(await response.json());
|
|
746
1646
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
747
1647
|
if (!parts)
|
|
748
1648
|
throw new Error("No audio in response");
|
|
@@ -763,31 +1663,34 @@ export class GeminiProvider extends BaseProvider {
|
|
|
763
1663
|
if (!audio || audio.length === 0) {
|
|
764
1664
|
throw new Error("audio must not be empty");
|
|
765
1665
|
}
|
|
766
|
-
|
|
767
|
-
|
|
1666
|
+
if (audio.length > 20 * 1024 * 1024) {
|
|
1667
|
+
throw new Error("Gemini inline audio is limited to 20 MB; upload the audio with the File API first");
|
|
1668
|
+
}
|
|
1669
|
+
// Detect MIME type from the audio header.
|
|
1670
|
+
let mimeType = geminiAudioMime(sniffAudioMime(audio));
|
|
768
1671
|
if (audio[0] === 0x52 &&
|
|
769
1672
|
audio[1] === 0x49 &&
|
|
770
1673
|
audio[2] === 0x46 &&
|
|
771
1674
|
audio[3] === 0x46) {
|
|
772
|
-
mimeType = "audio/wav";
|
|
1675
|
+
mimeType = "audio/wav";
|
|
773
1676
|
}
|
|
774
1677
|
else if (audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) {
|
|
775
|
-
mimeType = "audio/mp3";
|
|
1678
|
+
mimeType = "audio/mp3";
|
|
776
1679
|
}
|
|
777
1680
|
else if (audio[0] === 0xff && (audio[1] === 0xfb || audio[1] === 0xf3)) {
|
|
778
|
-
mimeType = "audio/mp3";
|
|
1681
|
+
mimeType = "audio/mp3";
|
|
779
1682
|
}
|
|
780
1683
|
else if (audio[0] === 0x66 &&
|
|
781
1684
|
audio[1] === 0x4c &&
|
|
782
1685
|
audio[2] === 0x61 &&
|
|
783
1686
|
audio[3] === 0x43) {
|
|
784
|
-
mimeType = "audio/flac";
|
|
1687
|
+
mimeType = "audio/flac";
|
|
785
1688
|
}
|
|
786
1689
|
else if (audio[0] === 0x4f &&
|
|
787
1690
|
audio[1] === 0x67 &&
|
|
788
1691
|
audio[2] === 0x67 &&
|
|
789
1692
|
audio[3] === 0x53) {
|
|
790
|
-
mimeType = "audio/ogg";
|
|
1693
|
+
mimeType = "audio/ogg";
|
|
791
1694
|
}
|
|
792
1695
|
let promptText = args.prompt ?? "Transcribe this audio to text.";
|
|
793
1696
|
if (language) {
|
|
@@ -818,7 +1721,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
818
1721
|
const errText = await response.text();
|
|
819
1722
|
throw new Error(`Gemini ASR failed ${response.status}: ${errText}`);
|
|
820
1723
|
}
|
|
821
|
-
const data = (await response.json());
|
|
1724
|
+
const data = parseGeminiResponse(await response.json());
|
|
822
1725
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
823
1726
|
if (!parts)
|
|
824
1727
|
return { text: "" };
|
|
@@ -828,142 +1731,423 @@ export class GeminiProvider extends BaseProvider {
|
|
|
828
1731
|
.join("");
|
|
829
1732
|
return { text };
|
|
830
1733
|
}
|
|
831
|
-
|
|
832
|
-
// Text-to-video (Veo models — async operation with polling)
|
|
833
|
-
// ---------------------------------------------------------------------------
|
|
834
|
-
async textToVideo(params) {
|
|
835
|
-
if (!params.prompt) {
|
|
836
|
-
throw new Error("The input prompt cannot be empty.");
|
|
837
|
-
}
|
|
838
|
-
const modelId = params.model.id;
|
|
839
|
-
if (!modelId.startsWith("veo-")) {
|
|
840
|
-
throw new Error(`Model ${modelId} is not a Veo model. Only Veo models support text-to-video.`);
|
|
841
|
-
}
|
|
842
|
-
const body = {
|
|
843
|
-
instances: [{ prompt: params.prompt }]
|
|
844
|
-
};
|
|
1734
|
+
buildVideoParameters(params) {
|
|
845
1735
|
const parameters = {};
|
|
846
|
-
if (params.negativePrompt)
|
|
1736
|
+
if (params.negativePrompt) {
|
|
847
1737
|
parameters.negativePrompt = params.negativePrompt;
|
|
848
|
-
|
|
1738
|
+
}
|
|
1739
|
+
if (params.aspectRatio) {
|
|
849
1740
|
parameters.aspectRatio = params.aspectRatio;
|
|
850
|
-
|
|
1741
|
+
}
|
|
1742
|
+
if (params.resolution) {
|
|
1743
|
+
parameters.resolution = params.resolution;
|
|
1744
|
+
}
|
|
1745
|
+
if (params.durationSeconds != null) {
|
|
1746
|
+
parameters.durationSeconds = params.durationSeconds;
|
|
1747
|
+
}
|
|
1748
|
+
if (params.seed != null) {
|
|
851
1749
|
parameters.seed = params.seed;
|
|
852
|
-
if (Object.keys(parameters).length > 0)
|
|
853
|
-
body.parameters = parameters;
|
|
854
|
-
// Initiate async generation
|
|
855
|
-
const url = `${GEMINI_API_BASE}/models/${modelId}:generateVideos?key=${this.apiKey}`;
|
|
856
|
-
const response = await this._fetch(url, {
|
|
857
|
-
method: "POST",
|
|
858
|
-
headers: { "Content-Type": "application/json" },
|
|
859
|
-
body: JSON.stringify(body)
|
|
860
|
-
});
|
|
861
|
-
if (!response.ok) {
|
|
862
|
-
const errText = await response.text();
|
|
863
|
-
throw new Error(`Gemini video generation failed ${response.status}: ${errText}`);
|
|
864
1750
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1751
|
+
return parameters;
|
|
1752
|
+
}
|
|
1753
|
+
getVideoUri(operation) {
|
|
1754
|
+
return (operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video
|
|
1755
|
+
?.uri ?? operation.response?.generatedVideos?.[0]?.video?.uri);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* The refusal to raise when an operation came back with no video, or null
|
|
1759
|
+
* when nothing says the filter is why. A filtered take leaves the operation
|
|
1760
|
+
* successful and the samples empty, so without this the caller sees only
|
|
1761
|
+
* "No video URI in response" — indistinguishable from a broken response, and
|
|
1762
|
+
* fatal to a run that has already paid for its other shots.
|
|
1763
|
+
*/
|
|
1764
|
+
videoContentFilterRefusal(operation, modelId) {
|
|
1765
|
+
const rai = operation.response?.generateVideoResponse ?? operation.response;
|
|
1766
|
+
const reasons = rai?.raiMediaFilteredReasons ?? [];
|
|
1767
|
+
const filtered = rai?.raiMediaFilteredCount ?? 0;
|
|
1768
|
+
if (filtered <= 0 && reasons.length === 0)
|
|
1769
|
+
return null;
|
|
1770
|
+
const detail = reasons.length
|
|
1771
|
+
? reasons.join("; ")
|
|
1772
|
+
: "no reason given by the provider";
|
|
1773
|
+
return new ContentFilterRefusal(`Veo filtered every generated video for this prompt: ${detail}`, { provider: "gemini", model: modelId, reasons });
|
|
1774
|
+
}
|
|
1775
|
+
async waitForVideoOperation(operation, timeoutSeconds, signal) {
|
|
1776
|
+
const maxWait = timeoutSeconds && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 600_000;
|
|
868
1777
|
const pollInterval = 10_000;
|
|
869
1778
|
let elapsed = 0;
|
|
870
1779
|
let current = operation;
|
|
871
1780
|
while (!current.done && elapsed < maxWait) {
|
|
872
|
-
await new Promise((
|
|
1781
|
+
await new Promise((resolve, reject) => {
|
|
1782
|
+
const onAbort = () => {
|
|
1783
|
+
clearTimeout(timer);
|
|
1784
|
+
reject(abortError(signal));
|
|
1785
|
+
};
|
|
1786
|
+
const timer = setTimeout(() => {
|
|
1787
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1788
|
+
resolve();
|
|
1789
|
+
}, pollInterval);
|
|
1790
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1791
|
+
});
|
|
873
1792
|
elapsed += pollInterval;
|
|
874
|
-
if (!current.name)
|
|
1793
|
+
if (!current.name) {
|
|
875
1794
|
throw new Error("No operation name for polling");
|
|
876
|
-
|
|
877
|
-
const pollResp = await this._fetch(
|
|
1795
|
+
}
|
|
1796
|
+
const pollResp = await this._fetch(`${GEMINI_API_BASE}/${current.name}`, {
|
|
1797
|
+
headers: { "x-goog-api-key": this.apiKey },
|
|
1798
|
+
signal
|
|
1799
|
+
});
|
|
878
1800
|
if (!pollResp.ok) {
|
|
879
1801
|
const errText = await pollResp.text();
|
|
880
1802
|
throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
|
|
881
1803
|
}
|
|
882
1804
|
current = (await pollResp.json());
|
|
883
1805
|
}
|
|
884
|
-
if (!current.done)
|
|
1806
|
+
if (!current.done) {
|
|
885
1807
|
throw new Error("Video generation timed out");
|
|
886
|
-
|
|
887
|
-
if (
|
|
1808
|
+
}
|
|
1809
|
+
if (current.error?.message) {
|
|
1810
|
+
// Vertex answers a filtered prompt through the operation's error slot
|
|
1811
|
+
// ("videos were filtered out because they violated ... usage
|
|
1812
|
+
// guidelines"), so the refusal has to be recognized here too.
|
|
1813
|
+
if (isContentFilterRefusal(current.error.message)) {
|
|
1814
|
+
throw new ContentFilterRefusal(current.error.message, {
|
|
1815
|
+
provider: "gemini"
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
throw new Error(`Gemini video generation failed: ${current.error.message}`);
|
|
1819
|
+
}
|
|
1820
|
+
return current;
|
|
1821
|
+
}
|
|
1822
|
+
async downloadGeminiMedia(uri, signal) {
|
|
1823
|
+
const hostname = new URL(uri).hostname;
|
|
1824
|
+
const headers = hostname === "generativelanguage.googleapis.com"
|
|
1825
|
+
? { "x-goog-api-key": this.apiKey }
|
|
1826
|
+
: undefined;
|
|
1827
|
+
const response = await safeFetch(uri, { headers, signal }, 5, this._fetch);
|
|
1828
|
+
if (!response.ok) {
|
|
1829
|
+
throw new Error(`Gemini media download failed: ${response.status}`);
|
|
1830
|
+
}
|
|
1831
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Submit a Veo `predictLongRunning` request, wait for the operation, and
|
|
1835
|
+
* download the first generated video.
|
|
1836
|
+
*/
|
|
1837
|
+
async runVeoOperation(modelId, body, label, options) {
|
|
1838
|
+
const signal = combineSignals(options.signal, options.timeoutSeconds);
|
|
1839
|
+
this.recordRequestPayload(body);
|
|
1840
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/models/${modelId}:predictLongRunning`, {
|
|
1841
|
+
method: "POST",
|
|
1842
|
+
headers: {
|
|
1843
|
+
"Content-Type": "application/json",
|
|
1844
|
+
"x-goog-api-key": this.apiKey
|
|
1845
|
+
},
|
|
1846
|
+
body: JSON.stringify(body),
|
|
1847
|
+
signal
|
|
1848
|
+
});
|
|
1849
|
+
if (!response.ok) {
|
|
1850
|
+
const errText = await response.text();
|
|
1851
|
+
throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
|
|
1852
|
+
}
|
|
1853
|
+
const operation = await this.waitForVideoOperation((await response.json()), options.timeoutSeconds, signal);
|
|
1854
|
+
const videoUri = this.getVideoUri(operation);
|
|
1855
|
+
if (!videoUri) {
|
|
1856
|
+
const refusal = this.videoContentFilterRefusal(operation, modelId);
|
|
1857
|
+
if (refusal)
|
|
1858
|
+
throw refusal;
|
|
888
1859
|
throw new Error("No video URI in response");
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1860
|
+
}
|
|
1861
|
+
return this.downloadGeminiMedia(videoUri, signal);
|
|
1862
|
+
}
|
|
1863
|
+
/** Send one request to the Interactions API and return the finished interaction. */
|
|
1864
|
+
async createInteraction(body, label, modelId, signal) {
|
|
1865
|
+
this.recordRequestPayload(body);
|
|
1866
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/interactions`, {
|
|
1867
|
+
method: "POST",
|
|
1868
|
+
headers: {
|
|
1869
|
+
"Content-Type": "application/json",
|
|
1870
|
+
"x-goog-api-key": this.apiKey
|
|
1871
|
+
},
|
|
1872
|
+
body: JSON.stringify(body),
|
|
1873
|
+
signal
|
|
1874
|
+
});
|
|
1875
|
+
if (!response.ok) {
|
|
1876
|
+
const errText = await response.text();
|
|
1877
|
+
if (isContentFilterRefusal(errText)) {
|
|
1878
|
+
throw new ContentFilterRefusal(errText, {
|
|
1879
|
+
provider: "gemini",
|
|
1880
|
+
model: modelId
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
|
|
1884
|
+
}
|
|
1885
|
+
const interaction = (await response.json());
|
|
1886
|
+
if (interaction.status && interaction.status !== "completed") {
|
|
1887
|
+
throw new Error(`Gemini ${label} ended with status ${interaction.status}`);
|
|
1888
|
+
}
|
|
1889
|
+
return interaction;
|
|
1890
|
+
}
|
|
1891
|
+
/** The first `type` output of an interaction, inline or behind a file uri. */
|
|
1892
|
+
async interactionMedia(interaction, type, signal) {
|
|
1893
|
+
for (const step of interaction.steps ?? []) {
|
|
1894
|
+
for (const content of step.content ?? []) {
|
|
1895
|
+
if (content.type !== type)
|
|
1896
|
+
continue;
|
|
1897
|
+
if (content.data) {
|
|
1898
|
+
return {
|
|
1899
|
+
data: new Uint8Array(Buffer.from(content.data, "base64")),
|
|
1900
|
+
mimeType: content.mime_type
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
if (content.uri) {
|
|
1904
|
+
return {
|
|
1905
|
+
data: await this.downloadGeminiMedia(content.uri, signal),
|
|
1906
|
+
mimeType: content.mime_type
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
throw new Error(`Gemini returned no ${type} output`);
|
|
1912
|
+
}
|
|
1913
|
+
/** An Interactions API video block: inline, or uploaded when too large. */
|
|
1914
|
+
async omniVideoInput(video) {
|
|
1915
|
+
const mimeType = sniffVideoMime(video);
|
|
1916
|
+
if (video.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
|
|
1917
|
+
const uri = await this.uploadFileToGemini(video, mimeType);
|
|
1918
|
+
return { type: "video", uri, mime_type: mimeType };
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
type: "video",
|
|
1922
|
+
data: Buffer.from(video).toString("base64"),
|
|
1923
|
+
mime_type: mimeType
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
/** Generate or edit a video with Gemini Omni through the Interactions API. */
|
|
1927
|
+
async runOmniVideo(modelId, input, options) {
|
|
1928
|
+
const responseFormat = { type: "video" };
|
|
1929
|
+
if (options.aspectRatio)
|
|
1930
|
+
responseFormat.aspect_ratio = options.aspectRatio;
|
|
1931
|
+
if (options.resolution)
|
|
1932
|
+
responseFormat.resolution = options.resolution;
|
|
1933
|
+
const signal = combineSignals(options.signal, options.timeoutSeconds);
|
|
1934
|
+
const interaction = await this.createInteraction({ model: modelId, input, response_format: responseFormat }, "video generation", modelId, signal);
|
|
1935
|
+
return (await this.interactionMedia(interaction, "video", signal)).data;
|
|
1936
|
+
}
|
|
1937
|
+
// ---------------------------------------------------------------------------
|
|
1938
|
+
// Text-to-video (Veo: long-running operation; Omni: Interactions API)
|
|
1939
|
+
// ---------------------------------------------------------------------------
|
|
1940
|
+
async textToVideo(params) {
|
|
1941
|
+
if (!params.prompt) {
|
|
1942
|
+
throw new Error("The input prompt cannot be empty.");
|
|
1943
|
+
}
|
|
1944
|
+
const modelId = params.model.id;
|
|
1945
|
+
if (isOmniVideoModel(modelId)) {
|
|
1946
|
+
return this.runOmniVideo(modelId, [{ type: "text", text: params.prompt }], params);
|
|
1947
|
+
}
|
|
1948
|
+
if (!modelId.startsWith("veo-")) {
|
|
1949
|
+
throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for text-to-video.`);
|
|
1950
|
+
}
|
|
1951
|
+
const body = {
|
|
1952
|
+
instances: [{ prompt: params.prompt }]
|
|
1953
|
+
};
|
|
1954
|
+
const parameters = this.buildVideoParameters(params);
|
|
1955
|
+
if (Object.keys(parameters).length > 0) {
|
|
1956
|
+
body.parameters = parameters;
|
|
1957
|
+
}
|
|
1958
|
+
return this.runVeoOperation(modelId, body, "video generation", params);
|
|
894
1959
|
}
|
|
895
1960
|
// ---------------------------------------------------------------------------
|
|
896
|
-
// Image-to-video
|
|
1961
|
+
// Image-to-video
|
|
897
1962
|
// ---------------------------------------------------------------------------
|
|
898
1963
|
async imageToVideo(image, params) {
|
|
899
1964
|
if (!image || image.length === 0) {
|
|
900
1965
|
throw new Error("Input image cannot be empty.");
|
|
901
1966
|
}
|
|
902
1967
|
const modelId = params.model.id;
|
|
1968
|
+
const prompt = params.prompt || "Animate this image";
|
|
1969
|
+
if (isOmniVideoModel(modelId)) {
|
|
1970
|
+
if (params.endImage?.length) {
|
|
1971
|
+
throw new Error(`Gemini model ${modelId} does not accept a last frame`);
|
|
1972
|
+
}
|
|
1973
|
+
return this.runOmniVideo(modelId, [
|
|
1974
|
+
{
|
|
1975
|
+
type: "image",
|
|
1976
|
+
data: Buffer.from(image).toString("base64"),
|
|
1977
|
+
mime_type: detectImageMime(image)
|
|
1978
|
+
},
|
|
1979
|
+
{ type: "text", text: prompt }
|
|
1980
|
+
], params);
|
|
1981
|
+
}
|
|
903
1982
|
if (!modelId.startsWith("veo-")) {
|
|
904
|
-
throw new Error(`Model ${modelId} is not a
|
|
1983
|
+
throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for image-to-video.`);
|
|
1984
|
+
}
|
|
1985
|
+
const instance = {
|
|
1986
|
+
prompt,
|
|
1987
|
+
image: veoImage(image)
|
|
1988
|
+
};
|
|
1989
|
+
const parameters = this.buildVideoParameters(params);
|
|
1990
|
+
if (params.endImage?.length) {
|
|
1991
|
+
// Veo interpolates between the two frames only in an 8-second clip.
|
|
1992
|
+
instance.lastFrame = veoImage(params.endImage);
|
|
1993
|
+
parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
|
|
1994
|
+
}
|
|
1995
|
+
const body = { instances: [instance] };
|
|
1996
|
+
if (Object.keys(parameters).length > 0) {
|
|
1997
|
+
body.parameters = parameters;
|
|
1998
|
+
}
|
|
1999
|
+
return this.runVeoOperation(modelId, body, "image-to-video", params);
|
|
2000
|
+
}
|
|
2001
|
+
// ---------------------------------------------------------------------------
|
|
2002
|
+
// Reference-to-video
|
|
2003
|
+
// ---------------------------------------------------------------------------
|
|
2004
|
+
async referenceToVideo(inputs, params) {
|
|
2005
|
+
if (!params.prompt) {
|
|
2006
|
+
throw new Error("The input prompt cannot be empty.");
|
|
2007
|
+
}
|
|
2008
|
+
const modelId = params.model.id;
|
|
2009
|
+
requireVideoTask(modelId, "reference_to_video");
|
|
2010
|
+
const images = inputs.images.filter((bytes) => bytes.length > 0);
|
|
2011
|
+
const videos = inputs.videos.filter((bytes) => bytes.length > 0);
|
|
2012
|
+
if ((inputs.audios ?? []).some((bytes) => bytes.length > 0)) {
|
|
2013
|
+
throw new Error(`Gemini model ${modelId} does not accept reference audio`);
|
|
2014
|
+
}
|
|
2015
|
+
if (images.length === 0 && videos.length === 0) {
|
|
2016
|
+
throw new Error("At least one reference image or video is required");
|
|
2017
|
+
}
|
|
2018
|
+
if (isOmniVideoModel(modelId)) {
|
|
2019
|
+
if (videos.length > OMNI_MAX_REFERENCE_VIDEOS) {
|
|
2020
|
+
throw new Error(`Gemini model ${modelId} accepts at most ${OMNI_MAX_REFERENCE_VIDEOS} reference videos`);
|
|
2021
|
+
}
|
|
2022
|
+
const input = images.map((bytes) => ({
|
|
2023
|
+
type: "image",
|
|
2024
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
2025
|
+
mime_type: detectImageMime(bytes)
|
|
2026
|
+
}));
|
|
2027
|
+
for (const video of videos) {
|
|
2028
|
+
input.push(await this.omniVideoInput(video));
|
|
2029
|
+
}
|
|
2030
|
+
input.push({ type: "text", text: params.prompt });
|
|
2031
|
+
return this.runOmniVideo(modelId, input, params);
|
|
2032
|
+
}
|
|
2033
|
+
if (videos.length > 0) {
|
|
2034
|
+
throw new Error(`Gemini model ${modelId} accepts reference images only`);
|
|
2035
|
+
}
|
|
2036
|
+
if (images.length > VEO_MAX_REFERENCE_IMAGES) {
|
|
2037
|
+
throw new Error(`Gemini model ${modelId} accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images`);
|
|
905
2038
|
}
|
|
906
|
-
const
|
|
907
|
-
|
|
2039
|
+
const parameters = this.buildVideoParameters(params);
|
|
2040
|
+
parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
|
|
908
2041
|
const body = {
|
|
909
2042
|
instances: [
|
|
910
2043
|
{
|
|
911
|
-
prompt,
|
|
912
|
-
|
|
2044
|
+
prompt: params.prompt,
|
|
2045
|
+
referenceImages: images.map((bytes) => ({
|
|
2046
|
+
image: veoImage(bytes),
|
|
2047
|
+
referenceType: "asset"
|
|
2048
|
+
}))
|
|
913
2049
|
}
|
|
914
|
-
]
|
|
2050
|
+
],
|
|
2051
|
+
parameters
|
|
915
2052
|
};
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
2053
|
+
return this.runVeoOperation(modelId, body, "reference-to-video", params);
|
|
2054
|
+
}
|
|
2055
|
+
// ---------------------------------------------------------------------------
|
|
2056
|
+
// Video extension (Veo) and editing (Omni)
|
|
2057
|
+
// ---------------------------------------------------------------------------
|
|
2058
|
+
async extendVideo(video, params) {
|
|
2059
|
+
if (video.length === 0) {
|
|
2060
|
+
throw new Error("The input video is empty.");
|
|
2061
|
+
}
|
|
2062
|
+
const modelId = params.model.id;
|
|
2063
|
+
requireVideoTask(modelId, "extend_video");
|
|
2064
|
+
if (params.mode !== "end") {
|
|
2065
|
+
throw new Error(`Gemini model ${modelId} extends videos at the end only`);
|
|
2066
|
+
}
|
|
2067
|
+
if (params.durationSeconds !== VEO_EXTENSION_SECONDS) {
|
|
2068
|
+
throw new Error(`Gemini model ${modelId} extends a video by exactly ${VEO_EXTENSION_SECONDS} seconds`);
|
|
2069
|
+
}
|
|
2070
|
+
const body = {
|
|
2071
|
+
instances: [
|
|
2072
|
+
{
|
|
2073
|
+
prompt: params.prompt,
|
|
2074
|
+
video: {
|
|
2075
|
+
inlineData: {
|
|
2076
|
+
mimeType: sniffVideoMime(video),
|
|
2077
|
+
data: Buffer.from(video).toString("base64")
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
],
|
|
2082
|
+
// Veo extends 720p input only and returns the input plus the new seconds.
|
|
2083
|
+
parameters: { numberOfVideos: 1, resolution: "720p" }
|
|
2084
|
+
};
|
|
2085
|
+
return this.runVeoOperation(modelId, body, "video extension", {
|
|
2086
|
+
signal: params.signal
|
|
930
2087
|
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
2088
|
+
}
|
|
2089
|
+
async videoToVideo(video, params) {
|
|
2090
|
+
if (video.length === 0) {
|
|
2091
|
+
throw new Error("The input video is empty.");
|
|
934
2092
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const maxWait = 600_000;
|
|
938
|
-
const pollInterval = 10_000;
|
|
939
|
-
let elapsed = 0;
|
|
940
|
-
let current = operation;
|
|
941
|
-
while (!current.done && elapsed < maxWait) {
|
|
942
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
943
|
-
elapsed += pollInterval;
|
|
944
|
-
if (!current.name)
|
|
945
|
-
throw new Error("No operation name for polling");
|
|
946
|
-
const pollUrl = `${GEMINI_API_BASE}/${current.name}?key=${this.apiKey}`;
|
|
947
|
-
const pollResp = await this._fetch(pollUrl);
|
|
948
|
-
if (!pollResp.ok) {
|
|
949
|
-
const errText = await pollResp.text();
|
|
950
|
-
throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
|
|
951
|
-
}
|
|
952
|
-
current = (await pollResp.json());
|
|
2093
|
+
if (!params.prompt) {
|
|
2094
|
+
throw new Error("The edit instruction cannot be empty.");
|
|
953
2095
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
2096
|
+
const modelId = params.model.id;
|
|
2097
|
+
requireVideoTask(modelId, "video_to_video");
|
|
2098
|
+
const input = [await this.omniVideoInput(video)];
|
|
2099
|
+
for (const bytes of params.referenceImages ?? []) {
|
|
2100
|
+
if (bytes.length === 0)
|
|
2101
|
+
continue;
|
|
2102
|
+
input.push({
|
|
2103
|
+
type: "image",
|
|
2104
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
2105
|
+
mime_type: detectImageMime(bytes)
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
input.push({ type: "text", text: params.prompt });
|
|
2109
|
+
return this.runOmniVideo(modelId, input, params);
|
|
2110
|
+
}
|
|
2111
|
+
// ---------------------------------------------------------------------------
|
|
2112
|
+
// Text-to-music (Lyria, Interactions API)
|
|
2113
|
+
// ---------------------------------------------------------------------------
|
|
2114
|
+
async textToMusic(params) {
|
|
2115
|
+
if (!params.prompt) {
|
|
2116
|
+
throw new Error("The input prompt cannot be empty.");
|
|
2117
|
+
}
|
|
2118
|
+
const modelId = params.model.id;
|
|
2119
|
+
if (!modelId.startsWith("lyria-")) {
|
|
2120
|
+
throw new Error(`Model ${modelId} is not a Lyria model. Use a Lyria model for text-to-music.`);
|
|
2121
|
+
}
|
|
2122
|
+
// Lyria takes length and lyrics from the prompt; it has no fields for them.
|
|
2123
|
+
const input = [
|
|
2124
|
+
params.prompt,
|
|
2125
|
+
params.durationSeconds
|
|
2126
|
+
? `Length: about ${Math.round(params.durationSeconds)} seconds.`
|
|
2127
|
+
: undefined,
|
|
2128
|
+
params.lyrics ? `Lyrics:\n${params.lyrics}` : undefined
|
|
2129
|
+
]
|
|
2130
|
+
.filter((part) => part !== undefined)
|
|
2131
|
+
.join("\n\n");
|
|
2132
|
+
const body = { model: modelId, input };
|
|
2133
|
+
// Lyria answers MP3 by default. Only Lyria 3.5 returns WAV, on request.
|
|
2134
|
+
if (params.audioFormat === "wav" && modelId === "lyria-3.5") {
|
|
2135
|
+
body.response_format = { type: "audio" };
|
|
2136
|
+
}
|
|
2137
|
+
const signal = combineSignals(undefined, params.timeoutSeconds);
|
|
2138
|
+
const interaction = await this.createInteraction(body, "music generation", modelId, signal);
|
|
2139
|
+
const audio = await this.interactionMedia(interaction, "audio", signal);
|
|
2140
|
+
return {
|
|
2141
|
+
data: audio.data,
|
|
2142
|
+
mimeType: audio.mimeType ?? sniffAudioMime(audio.data)
|
|
2143
|
+
};
|
|
963
2144
|
}
|
|
964
2145
|
// ---------------------------------------------------------------------------
|
|
965
2146
|
// Error detection
|
|
966
2147
|
// ---------------------------------------------------------------------------
|
|
2148
|
+
isContextExceededError(error) {
|
|
2149
|
+
return geminiContextExceeded(error) !== null;
|
|
2150
|
+
}
|
|
967
2151
|
isContextLengthError(error) {
|
|
968
2152
|
const msg = String(error).toLowerCase();
|
|
969
2153
|
return (msg.includes("context length") ||
|