@nodetool-ai/runtime 0.7.0-rc.23 → 0.8.2
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 +982 -127
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2649 -530
- 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 +75 -0
- package/dist/generation-registry.d.ts.map +1 -0
- package/dist/generation-registry.js +144 -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 +352 -0
- package/dist/host-binaries.js.map +1 -0
- package/dist/image-codec.d.ts +76 -0
- package/dist/image-codec.d.ts.map +1 -0
- package/dist/image-codec.js +293 -0
- package/dist/image-codec.js.map +1 -0
- package/dist/image-model-tasks.d.ts +15 -0
- package/dist/image-model-tasks.d.ts.map +1 -0
- package/dist/image-model-tasks.js +24 -0
- package/dist/image-model-tasks.js.map +1 -0
- package/dist/index.d.ts +67 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -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 +35 -0
- package/dist/media-ref-bytes.d.ts.map +1 -0
- package/dist/media-ref-bytes.js +191 -0
- package/dist/media-ref-bytes.js.map +1 -0
- package/dist/node-executor.d.ts +119 -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 +35 -10
- package/dist/providers/aki-provider.d.ts.map +1 -1
- package/dist/providers/aki-provider.js +90 -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 +103 -0
- package/dist/providers/atlascloud-provider.d.ts.map +1 -0
- package/dist/providers/atlascloud-provider.js +1031 -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 +501 -32
- package/dist/providers/base-provider.d.ts.map +1 -1
- package/dist/providers/base-provider.js +1280 -87
- 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 +107 -74
- package/dist/providers/claude-agent-provider.d.ts.map +1 -1
- package/dist/providers/claude-agent-provider.js +1279 -517
- package/dist/providers/claude-agent-provider.js.map +1 -1
- package/dist/providers/codex-provider.d.ts +84 -0
- package/dist/providers/codex-provider.d.ts.map +1 -0
- package/dist/providers/codex-provider.js +505 -0
- package/dist/providers/codex-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +7 -3
- package/dist/providers/cohere-provider.d.ts.map +1 -1
- package/dist/providers/cohere-provider.js +14 -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 +75 -0
- package/dist/providers/core-tools.d.ts.map +1 -0
- package/dist/providers/core-tools.js +128 -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 +164 -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 +83 -0
- package/dist/providers/custom-openai-provider.d.ts.map +1 -0
- package/dist/providers/custom-openai-provider.js +190 -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/dreamina-models.d.ts +5 -0
- package/dist/providers/dreamina-models.d.ts.map +1 -0
- package/dist/providers/dreamina-models.js +57 -0
- package/dist/providers/dreamina-models.js.map +1 -0
- package/dist/providers/dreamina-music.d.ts +52 -0
- package/dist/providers/dreamina-music.d.ts.map +1 -0
- package/dist/providers/dreamina-music.js +115 -0
- package/dist/providers/dreamina-music.js.map +1 -0
- package/dist/providers/dreamina-provider.d.ts +62 -0
- package/dist/providers/dreamina-provider.d.ts.map +1 -0
- package/dist/providers/dreamina-provider.js +790 -0
- package/dist/providers/dreamina-provider.js.map +1 -0
- package/dist/providers/elevenlabs-provider.d.ts +50 -0
- package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
- package/dist/providers/elevenlabs-provider.js +147 -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 +40 -0
- package/dist/providers/evolink-provider.d.ts.map +1 -0
- package/dist/providers/evolink-provider.js +312 -0
- package/dist/providers/evolink-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +74 -2
- package/dist/providers/fake-provider.d.ts.map +1 -1
- package/dist/providers/fake-provider.js +213 -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 +173 -8
- package/dist/providers/fal-provider.d.ts.map +1 -1
- package/dist/providers/fal-provider.js +1396 -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 +150 -9
- package/dist/providers/gemini-provider.d.ts.map +1 -1
- package/dist/providers/gemini-provider.js +1515 -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 +31 -0
- package/dist/providers/higgsfield-provider.d.ts.map +1 -0
- package/dist/providers/higgsfield-provider.js +143 -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 +73 -4
- package/dist/providers/huggingface-provider.d.ts.map +1 -1
- package/dist/providers/huggingface-provider.js +240 -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 +87 -8
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +250 -56
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/jina-provider.d.ts +12 -3
- package/dist/providers/jina-provider.d.ts.map +1 -1
- package/dist/providers/jina-provider.js +21 -7
- package/dist/providers/jina-provider.js.map +1 -1
- package/dist/providers/kie-provider.d.ts +172 -10
- package/dist/providers/kie-provider.d.ts.map +1 -1
- package/dist/providers/kie-provider.js +1190 -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 +23 -36
- package/dist/providers/llama-provider.d.ts.map +1 -1
- package/dist/providers/llama-provider.js +50 -355
- 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/loop-tracing.d.ts +17 -0
- package/dist/providers/loop-tracing.d.ts.map +1 -0
- package/dist/providers/loop-tracing.js +132 -0
- package/dist/providers/loop-tracing.js.map +1 -0
- 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 +16 -2
- package/dist/providers/meshy-provider.d.ts.map +1 -1
- package/dist/providers/meshy-provider.js +82 -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 +27 -18
- package/dist/providers/minimax-provider.d.ts.map +1 -1
- package/dist/providers/minimax-provider.js +222 -72
- package/dist/providers/minimax-provider.js.map +1 -1
- package/dist/providers/mistral-provider.d.ts +8 -12
- package/dist/providers/mistral-provider.d.ts.map +1 -1
- package/dist/providers/mistral-provider.js +15 -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 +99 -0
- package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
- package/dist/providers/node-llama-cpp-provider.js +484 -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 +222 -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 +30 -2
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +198 -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 +98 -27
- package/dist/providers/openai-provider.d.ts.map +1 -1
- package/dist/providers/openai-provider.js +1037 -105
- package/dist/providers/openai-provider.js.map +1 -1
- package/dist/providers/openrouter-provider.d.ts +58 -13
- package/dist/providers/openrouter-provider.d.ts.map +1 -1
- package/dist/providers/openrouter-provider.js +383 -51
- 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 +171 -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 +168 -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 +26 -13
- package/dist/providers/python-provider.d.ts.map +1 -1
- package/dist/providers/python-provider.js +204 -25
- package/dist/providers/python-provider.js.map +1 -1
- package/dist/providers/replicate-provider.d.ts +91 -8
- package/dist/providers/replicate-provider.d.ts.map +1 -1
- package/dist/providers/replicate-provider.js +823 -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/requesty-provider.d.ts +30 -0
- package/dist/providers/requesty-provider.d.ts.map +1 -0
- package/dist/providers/requesty-provider.js +87 -0
- package/dist/providers/requesty-provider.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 +46 -0
- package/dist/providers/reve-provider.d.ts.map +1 -0
- package/dist/providers/reve-provider.js +165 -0
- package/dist/providers/reve-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +25 -2
- package/dist/providers/rodin-provider.d.ts.map +1 -1
- package/dist/providers/rodin-provider.js +95 -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-operations.d.ts +27 -0
- package/dist/providers/together-operations.d.ts.map +1 -0
- package/dist/providers/together-operations.js +95 -0
- package/dist/providers/together-operations.js.map +1 -0
- package/dist/providers/together-provider.d.ts +18 -18
- package/dist/providers/together-provider.d.ts.map +1 -1
- package/dist/providers/together-provider.js +140 -367
- package/dist/providers/together-provider.js.map +1 -1
- package/dist/providers/topaz-provider.d.ts +45 -0
- package/dist/providers/topaz-provider.d.ts.map +1 -0
- package/dist/providers/topaz-provider.js +244 -0
- package/dist/providers/topaz-provider.js.map +1 -0
- package/dist/providers/types.d.ts +641 -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 +28 -0
- package/dist/providers/useapi-provider.d.ts.map +1 -0
- package/dist/providers/useapi-provider.js +274 -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 +20 -8
- package/dist/providers/vllm-provider.d.ts.map +1 -1
- package/dist/providers/vllm-provider.js +44 -23
- package/dist/providers/vllm-provider.js.map +1 -1
- package/dist/providers/voyage-provider.d.ts +12 -3
- package/dist/providers/voyage-provider.d.ts.map +1 -1
- package/dist/providers/voyage-provider.js +21 -7
- package/dist/providers/voyage-provider.js.map +1 -1
- package/dist/providers/xai-provider.d.ts +56 -13
- package/dist/providers/xai-provider.d.ts.map +1 -1
- package/dist/providers/xai-provider.js +307 -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 +44 -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/run-context.d.ts +20 -0
- package/dist/run-context.d.ts.map +1 -0
- package/dist/run-context.js +18 -0
- package/dist/run-context.js.map +1 -0
- package/dist/run-trace-context.d.ts +30 -0
- package/dist/run-trace-context.d.ts.map +1 -0
- package/dist/run-trace-context.js +80 -0
- package/dist/run-trace-context.js.map +1 -0
- package/dist/run-trace-processor.d.ts +65 -0
- package/dist/run-trace-processor.d.ts.map +1 -0
- package/dist/run-trace-processor.js +417 -0
- package/dist/run-trace-processor.js.map +1 -0
- package/dist/run-trace-serialization.d.ts +5 -0
- package/dist/run-trace-serialization.d.ts.map +1 -0
- package/dist/run-trace-serialization.js +113 -0
- package/dist/run-trace-serialization.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 +62 -0
- package/dist/storage-workspace.d.ts.map +1 -0
- package/dist/storage-workspace.js +441 -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 +16 -3
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +76 -20
- 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 +11 -28
- package/dist/trace-exporters.d.ts.map +1 -1
- package/dist/trace-exporters.js +69 -22
- package/dist/trace-exporters.js.map +1 -1
- package/dist/tracing-helpers.d.ts +60 -26
- package/dist/tracing-helpers.d.ts.map +1 -1
- package/dist/tracing-helpers.js +100 -25
- package/dist/tracing-helpers.js.map +1 -1
- package/dist/trim-video-window.d.ts +8 -0
- package/dist/trim-video-window.d.ts.map +1 -0
- package/dist/trim-video-window.js +50 -0
- package/dist/trim-video-window.js.map +1 -0
- 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 +121 -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,189 @@ 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 {
|
|
555
|
+
declaredCapabilities() {
|
|
556
|
+
return [
|
|
557
|
+
"text_to_image",
|
|
558
|
+
"image_to_image",
|
|
559
|
+
"text_to_video",
|
|
560
|
+
"image_to_video",
|
|
561
|
+
"reference_to_video",
|
|
562
|
+
"video_to_video",
|
|
563
|
+
"extend_video",
|
|
564
|
+
"text_to_speech",
|
|
565
|
+
"text_to_music",
|
|
566
|
+
"automatic_speech_recognition",
|
|
567
|
+
"generate_embedding"
|
|
568
|
+
];
|
|
569
|
+
}
|
|
51
570
|
static requiredSecrets() {
|
|
52
571
|
return ["GEMINI_API_KEY"];
|
|
53
572
|
}
|
|
54
573
|
apiKey;
|
|
55
574
|
_fetch;
|
|
575
|
+
_sleep;
|
|
56
576
|
constructor(secrets, options = {}) {
|
|
57
577
|
super("gemini");
|
|
58
578
|
const apiKey = secrets.GEMINI_API_KEY;
|
|
@@ -61,6 +581,9 @@ export class GeminiProvider extends BaseProvider {
|
|
|
61
581
|
}
|
|
62
582
|
this.apiKey = apiKey;
|
|
63
583
|
this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
584
|
+
this._sleep =
|
|
585
|
+
options.sleepFn ??
|
|
586
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
64
587
|
}
|
|
65
588
|
getContainerEnv() {
|
|
66
589
|
return { GEMINI_API_KEY: this.apiKey };
|
|
@@ -68,33 +591,55 @@ export class GeminiProvider extends BaseProvider {
|
|
|
68
591
|
async hasToolSupport(_model) {
|
|
69
592
|
return true;
|
|
70
593
|
}
|
|
594
|
+
get supportsNativeWebSearch() {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
/** Gemini reads a whole clip: see {@link videoContentToGeminiPart}. */
|
|
598
|
+
get supportsVideoInput() {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
71
601
|
// ---------------------------------------------------------------------------
|
|
72
602
|
// Model listing
|
|
73
603
|
// ---------------------------------------------------------------------------
|
|
74
604
|
async getAvailableLanguageModels() {
|
|
75
|
-
const
|
|
76
|
-
let
|
|
605
|
+
const items = [];
|
|
606
|
+
let pageToken;
|
|
77
607
|
try {
|
|
78
|
-
|
|
608
|
+
do {
|
|
609
|
+
const query = new URLSearchParams({
|
|
610
|
+
key: this.apiKey,
|
|
611
|
+
pageSize: "1000"
|
|
612
|
+
});
|
|
613
|
+
if (pageToken)
|
|
614
|
+
query.set("pageToken", pageToken);
|
|
615
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/models?${query}`);
|
|
616
|
+
if (!response.ok)
|
|
617
|
+
return [];
|
|
618
|
+
const payload = decodeGeminiModelsPage(await response.json());
|
|
619
|
+
items.push(...(payload.models ?? []));
|
|
620
|
+
pageToken = payload.nextPageToken;
|
|
621
|
+
} while (pageToken);
|
|
79
622
|
}
|
|
80
623
|
catch {
|
|
81
624
|
return [];
|
|
82
625
|
}
|
|
83
|
-
|
|
84
|
-
return [];
|
|
85
|
-
const payload = (await response.json());
|
|
86
|
-
const items = payload.models ?? [];
|
|
626
|
+
const seen = new Set();
|
|
87
627
|
return items
|
|
88
628
|
.filter((m) => (m.supportedGenerationMethods ?? []).includes("generateContent"))
|
|
89
629
|
.filter((m) => !!m.name)
|
|
630
|
+
.filter((m) => !/(embedding|aqa|imagen|veo|image|tts)/i.test(m.name ?? ""))
|
|
90
631
|
.map((m) => {
|
|
91
632
|
const id = m.name.split("/").pop();
|
|
633
|
+
if (seen.has(id))
|
|
634
|
+
return null;
|
|
635
|
+
seen.add(id);
|
|
92
636
|
return {
|
|
93
637
|
id,
|
|
94
638
|
name: m.displayName ?? id,
|
|
95
639
|
provider: "gemini"
|
|
96
640
|
};
|
|
97
|
-
})
|
|
641
|
+
})
|
|
642
|
+
.filter((model) => model !== null);
|
|
98
643
|
}
|
|
99
644
|
// ---------------------------------------------------------------------------
|
|
100
645
|
// Message conversion helpers
|
|
@@ -107,26 +652,43 @@ export class GeminiProvider extends BaseProvider {
|
|
|
107
652
|
const img = content.image;
|
|
108
653
|
let base64Data;
|
|
109
654
|
let mimeType = img.mimeType ?? "image/jpeg";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
655
|
+
const parseImageDataUri = (uri) => {
|
|
656
|
+
const idx = uri.indexOf(",");
|
|
657
|
+
if (idx < 0)
|
|
658
|
+
throw new Error("Invalid image data URI");
|
|
659
|
+
const header = uri.slice(5, idx);
|
|
660
|
+
mimeType = header.split(";")[0] || mimeType;
|
|
661
|
+
return uri.slice(idx + 1);
|
|
662
|
+
};
|
|
663
|
+
if (isNonEmptyString(img.data) ||
|
|
664
|
+
(img.data instanceof Uint8Array && img.data.length > 0)) {
|
|
665
|
+
if (isString(img.data)) {
|
|
666
|
+
// Inline data may itself be a data: URI — strip the prefix and take
|
|
667
|
+
// the real mime type from it rather than shipping the header as
|
|
668
|
+
// base64 payload.
|
|
669
|
+
base64Data = img.data.startsWith("data:")
|
|
670
|
+
? parseImageDataUri(img.data)
|
|
671
|
+
: img.data;
|
|
113
672
|
}
|
|
114
673
|
else {
|
|
115
674
|
base64Data = Buffer.from(img.data).toString("base64");
|
|
116
675
|
}
|
|
117
676
|
}
|
|
118
677
|
else if (img.uri) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
678
|
+
// resolveUri turns asset file:// URIs (what the chat pipeline produces)
|
|
679
|
+
// into data: URIs; http(s) URIs pass through to safeFetch.
|
|
680
|
+
const resolvedUri = img.uri.startsWith("data:")
|
|
681
|
+
? img.uri
|
|
682
|
+
: await this.resolveUri(img.uri);
|
|
683
|
+
if (resolvedUri.startsWith("data:")) {
|
|
684
|
+
base64Data = parseImageDataUri(resolvedUri);
|
|
124
685
|
}
|
|
125
686
|
else {
|
|
126
|
-
const resp = await this._fetch
|
|
687
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
127
688
|
if (!resp.ok)
|
|
128
689
|
throw new Error(`Failed to fetch image: ${resp.status}`);
|
|
129
|
-
mimeType =
|
|
690
|
+
mimeType =
|
|
691
|
+
stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
|
|
130
692
|
base64Data = Buffer.from(await resp.arrayBuffer()).toString("base64");
|
|
131
693
|
}
|
|
132
694
|
}
|
|
@@ -138,87 +700,271 @@ export class GeminiProvider extends BaseProvider {
|
|
|
138
700
|
if (content.type === "audio") {
|
|
139
701
|
const aud = content.audio;
|
|
140
702
|
let base64Data;
|
|
141
|
-
let mimeType = aud.mimeType
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
703
|
+
let mimeType = aud.mimeType;
|
|
704
|
+
const parseAudioDataUri = (uri) => {
|
|
705
|
+
const idx = uri.indexOf(",");
|
|
706
|
+
if (idx < 0)
|
|
707
|
+
throw new Error("Invalid audio data URI");
|
|
708
|
+
const header = uri.slice(5, idx);
|
|
709
|
+
mimeType = mimeType ?? header.split(";")[0];
|
|
710
|
+
return uri.slice(idx + 1);
|
|
711
|
+
};
|
|
712
|
+
if (isNonEmptyString(aud.data) ||
|
|
713
|
+
(aud.data instanceof Uint8Array && aud.data.length > 0)) {
|
|
714
|
+
if (isString(aud.data)) {
|
|
715
|
+
base64Data = aud.data.startsWith("data:")
|
|
716
|
+
? parseAudioDataUri(aud.data)
|
|
717
|
+
: aud.data;
|
|
718
|
+
mimeType =
|
|
719
|
+
mimeType ?? sniffAudioMime(Buffer.from(base64Data, "base64"));
|
|
145
720
|
}
|
|
146
721
|
else {
|
|
147
|
-
|
|
722
|
+
const bytes = Buffer.from(aud.data);
|
|
723
|
+
base64Data = bytes.toString("base64");
|
|
724
|
+
mimeType = mimeType ?? sniffAudioMime(bytes);
|
|
148
725
|
}
|
|
149
726
|
}
|
|
150
727
|
else if (aud.uri) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
base64Data =
|
|
728
|
+
const resolvedUri = aud.uri.startsWith("data:")
|
|
729
|
+
? aud.uri
|
|
730
|
+
: await this.resolveUri(aud.uri);
|
|
731
|
+
if (resolvedUri.startsWith("data:")) {
|
|
732
|
+
base64Data = parseAudioDataUri(resolvedUri);
|
|
156
733
|
}
|
|
157
734
|
else {
|
|
158
|
-
const resp = await this._fetch
|
|
735
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
159
736
|
if (!resp.ok)
|
|
160
737
|
throw new Error(`Failed to fetch audio: ${resp.status}`);
|
|
161
|
-
|
|
162
|
-
|
|
738
|
+
const bytes = Buffer.from(await resp.arrayBuffer());
|
|
739
|
+
mimeType =
|
|
740
|
+
stripMimeParams(resp.headers.get("content-type")) ??
|
|
741
|
+
mimeType ??
|
|
742
|
+
sniffAudioMime(bytes);
|
|
743
|
+
base64Data = bytes.toString("base64");
|
|
163
744
|
}
|
|
164
745
|
}
|
|
165
746
|
else {
|
|
166
747
|
base64Data = "";
|
|
167
748
|
}
|
|
168
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
inlineData: { mimeType: geminiAudioMime(mimeType), data: base64Data }
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (content.type === "video") {
|
|
754
|
+
return await this.videoContentToGeminiPart(content.video);
|
|
169
755
|
}
|
|
170
756
|
return { text: "[unsupported content type]" };
|
|
171
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Source a video's bytes the way the image and audio branches do, then send
|
|
760
|
+
* them inline when small enough and through the Files API when not.
|
|
761
|
+
*/
|
|
762
|
+
async videoContentToGeminiPart(video) {
|
|
763
|
+
let bytes;
|
|
764
|
+
let mimeType = video.mimeType;
|
|
765
|
+
const parseVideoDataUri = (uri) => {
|
|
766
|
+
const idx = uri.indexOf(",");
|
|
767
|
+
if (idx < 0)
|
|
768
|
+
throw new Error("Invalid video data URI");
|
|
769
|
+
const header = uri.slice(5, idx);
|
|
770
|
+
mimeType = mimeType ?? stripMimeParams(header.split(";base64")[0]);
|
|
771
|
+
return Buffer.from(uri.slice(idx + 1), "base64");
|
|
772
|
+
};
|
|
773
|
+
if (isNonEmptyString(video.data) ||
|
|
774
|
+
(video.data instanceof Uint8Array && video.data.length > 0)) {
|
|
775
|
+
if (isString(video.data)) {
|
|
776
|
+
bytes = video.data.startsWith("data:")
|
|
777
|
+
? parseVideoDataUri(video.data)
|
|
778
|
+
: Buffer.from(video.data, "base64");
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
bytes = Buffer.from(video.data);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
else if (video.uri) {
|
|
785
|
+
const resolvedUri = video.uri.startsWith("data:")
|
|
786
|
+
? video.uri
|
|
787
|
+
: await this.resolveUri(video.uri);
|
|
788
|
+
if (resolvedUri.startsWith("data:")) {
|
|
789
|
+
bytes = parseVideoDataUri(resolvedUri);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const resp = await safeFetch(resolvedUri, undefined, 5, this._fetch);
|
|
793
|
+
if (!resp.ok)
|
|
794
|
+
throw new Error(`Failed to fetch video: ${resp.status}`);
|
|
795
|
+
mimeType =
|
|
796
|
+
stripMimeParams(resp.headers.get("content-type")) ?? mimeType;
|
|
797
|
+
bytes = Buffer.from(await resp.arrayBuffer());
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
bytes = Buffer.alloc(0);
|
|
802
|
+
}
|
|
803
|
+
const resolvedMime = mimeType ?? sniffVideoMime(bytes);
|
|
804
|
+
if (bytes.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
|
|
805
|
+
const fileUri = await this.uploadFileToGemini(bytes, resolvedMime);
|
|
806
|
+
return { fileData: { mimeType: resolvedMime, fileUri } };
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
inlineData: { mimeType: resolvedMime, data: bytes.toString("base64") }
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Upload bytes with the Files API resumable protocol and wait until the file
|
|
814
|
+
* is ACTIVE — Gemini rejects a `fileData` part that references a file still
|
|
815
|
+
* being processed. Returns the file's uri.
|
|
816
|
+
*/
|
|
817
|
+
async uploadFileToGemini(bytes, mimeType) {
|
|
818
|
+
const startResp = await this._fetch(`${GEMINI_UPLOAD_BASE}?key=${this.apiKey}`, {
|
|
819
|
+
method: "POST",
|
|
820
|
+
headers: {
|
|
821
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
822
|
+
"X-Goog-Upload-Command": "start",
|
|
823
|
+
"X-Goog-Upload-Header-Content-Length": String(bytes.length),
|
|
824
|
+
"X-Goog-Upload-Header-Content-Type": mimeType,
|
|
825
|
+
"Content-Type": "application/json"
|
|
826
|
+
},
|
|
827
|
+
body: JSON.stringify({
|
|
828
|
+
file: { display_name: `nodetool-upload.${mimeType.split("/")[1]}` }
|
|
829
|
+
})
|
|
830
|
+
});
|
|
831
|
+
if (!startResp.ok) {
|
|
832
|
+
const errText = await startResp.text();
|
|
833
|
+
throw new Error(`Gemini file upload failed to start ${startResp.status}: ${errText}`);
|
|
834
|
+
}
|
|
835
|
+
const uploadUrl = startResp.headers.get("x-goog-upload-url");
|
|
836
|
+
if (!uploadUrl) {
|
|
837
|
+
throw new Error("Gemini file upload returned no X-Goog-Upload-URL");
|
|
838
|
+
}
|
|
839
|
+
const uploadResp = await this._fetch(uploadUrl, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
headers: {
|
|
842
|
+
"Content-Length": String(bytes.length),
|
|
843
|
+
"X-Goog-Upload-Offset": "0",
|
|
844
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
845
|
+
},
|
|
846
|
+
body: new Blob([new Uint8Array(bytes)], {
|
|
847
|
+
type: mimeType
|
|
848
|
+
})
|
|
849
|
+
});
|
|
850
|
+
if (!uploadResp.ok) {
|
|
851
|
+
const errText = await uploadResp.text();
|
|
852
|
+
throw new Error(`Gemini file upload failed ${uploadResp.status}: ${errText}`);
|
|
853
|
+
}
|
|
854
|
+
const uploaded = (await uploadResp.json());
|
|
855
|
+
let file = uploaded.file;
|
|
856
|
+
if (!file?.name || !file.uri) {
|
|
857
|
+
throw new Error("Gemini file upload returned no file record");
|
|
858
|
+
}
|
|
859
|
+
const fileUri = file.uri;
|
|
860
|
+
const fileName = file.name;
|
|
861
|
+
for (let poll = 0; poll < GEMINI_FILE_MAX_POLLS; poll++) {
|
|
862
|
+
if (file?.state === "ACTIVE")
|
|
863
|
+
return fileUri;
|
|
864
|
+
if (file?.state === "FAILED") {
|
|
865
|
+
throw new Error(`Gemini file processing failed: ${file.error?.message ?? "unknown error"}`);
|
|
866
|
+
}
|
|
867
|
+
await this._sleep(GEMINI_FILE_POLL_INTERVAL_MS);
|
|
868
|
+
const pollResp = await this._fetch(`${GEMINI_API_BASE}/${fileName}`, {
|
|
869
|
+
headers: { "x-goog-api-key": this.apiKey }
|
|
870
|
+
});
|
|
871
|
+
if (!pollResp.ok) {
|
|
872
|
+
const errText = await pollResp.text();
|
|
873
|
+
throw new Error(`Gemini file poll failed ${pollResp.status}: ${errText}`);
|
|
874
|
+
}
|
|
875
|
+
file = (await pollResp.json());
|
|
876
|
+
}
|
|
877
|
+
if (file?.state === "ACTIVE")
|
|
878
|
+
return fileUri;
|
|
879
|
+
throw new Error(`Gemini file ${fileName} did not become ACTIVE in time`);
|
|
880
|
+
}
|
|
172
881
|
/**
|
|
173
882
|
* Convert our Message array into Gemini contents + optional system instruction.
|
|
174
883
|
*/
|
|
175
|
-
async convertMessages(messages) {
|
|
884
|
+
async convertMessages(messages, nameMap = new Map()) {
|
|
176
885
|
let systemInstruction;
|
|
177
886
|
const contents = [];
|
|
887
|
+
// Gemini correlates a tool result to its call by the function *name*, not
|
|
888
|
+
// by id (and our tool-call ids are synthesized — never valid Gemini
|
|
889
|
+
// function names). Map each tool-call id back to its function name so the
|
|
890
|
+
// `functionResponse.name` below matches the earlier `functionCall.name`.
|
|
891
|
+
const toolCallNames = new Map();
|
|
892
|
+
for (const m of messages) {
|
|
893
|
+
if (m.role === "assistant" && m.toolCalls) {
|
|
894
|
+
for (const tc of m.toolCalls) {
|
|
895
|
+
if (tc.id)
|
|
896
|
+
toolCallNames.set(tc.id, nameMap.get(tc.name) ?? tc.name);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
178
900
|
for (const msg of messages) {
|
|
179
901
|
if (msg.role === "system") {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
902
|
+
const instruction = isString(msg.content)
|
|
903
|
+
? msg.content
|
|
904
|
+
: (msg.content ?? [])
|
|
905
|
+
.filter((c) => c.type === "text")
|
|
906
|
+
.map((c) => c.text)
|
|
907
|
+
.join(" ");
|
|
908
|
+
systemInstruction = systemInstruction
|
|
909
|
+
? `${systemInstruction}\n${instruction}`
|
|
910
|
+
: instruction;
|
|
187
911
|
continue;
|
|
188
912
|
}
|
|
189
913
|
if (msg.role === "tool") {
|
|
190
|
-
// Tool result →
|
|
191
|
-
|
|
914
|
+
// Tool result → user role with functionResponse part. The name must
|
|
915
|
+
// match the originating functionCall's name, resolved from the call id.
|
|
916
|
+
const responseText = isString(msg.content)
|
|
192
917
|
? msg.content
|
|
193
918
|
: JSON.stringify(msg.content);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
919
|
+
const functionName = (msg.toolCallId ? toolCallNames.get(msg.toolCallId) : undefined) ??
|
|
920
|
+
msg.toolCallId ??
|
|
921
|
+
"unknown";
|
|
922
|
+
const responsePart = {
|
|
923
|
+
functionResponse: {
|
|
924
|
+
name: functionName,
|
|
925
|
+
id: msg.toolCallId ?? undefined,
|
|
926
|
+
response: { result: responseText }
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
// Merge parallel tool results into a single user turn so the request
|
|
930
|
+
// keeps alternating user/model roles.
|
|
931
|
+
const prev = contents[contents.length - 1];
|
|
932
|
+
if (prev &&
|
|
933
|
+
prev.role === "user" &&
|
|
934
|
+
prev.parts.length > 0 &&
|
|
935
|
+
prev.parts.every((p) => p.functionResponse !== undefined)) {
|
|
936
|
+
prev.parts.push(responsePart);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
appendGeminiContent(contents, {
|
|
940
|
+
role: "user",
|
|
941
|
+
parts: [responsePart]
|
|
942
|
+
});
|
|
943
|
+
}
|
|
205
944
|
continue;
|
|
206
945
|
}
|
|
207
946
|
if (msg.role === "assistant") {
|
|
208
947
|
// If we have raw Gemini parts (with thought content), replay them exactly
|
|
209
948
|
if (msg._rawGeminiParts && Array.isArray(msg._rawGeminiParts)) {
|
|
210
|
-
contents
|
|
949
|
+
// Copy: the contents we build get merged and stamped below, and the
|
|
950
|
+
// message's own parts must survive that untouched for the next turn.
|
|
951
|
+
appendGeminiContent(contents, {
|
|
211
952
|
role: "model",
|
|
212
|
-
parts: msg._rawGeminiParts
|
|
953
|
+
parts: msg._rawGeminiParts.map((part) => ({
|
|
954
|
+
...part
|
|
955
|
+
}))
|
|
213
956
|
});
|
|
214
957
|
continue;
|
|
215
958
|
}
|
|
216
959
|
const parts = [];
|
|
217
|
-
// Tool calls
|
|
218
960
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
219
961
|
for (const tc of msg.toolCalls) {
|
|
220
962
|
const part = {
|
|
221
|
-
functionCall: {
|
|
963
|
+
functionCall: {
|
|
964
|
+
id: tc.id,
|
|
965
|
+
name: nameMap.get(tc.name) ?? tc.name,
|
|
966
|
+
args: tc.args
|
|
967
|
+
}
|
|
222
968
|
};
|
|
223
969
|
if (tc.thought_signature) {
|
|
224
970
|
part.thoughtSignature = tc.thought_signature;
|
|
@@ -226,8 +972,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
226
972
|
parts.push(part);
|
|
227
973
|
}
|
|
228
974
|
}
|
|
229
|
-
|
|
230
|
-
if (typeof msg.content === "string" && msg.content) {
|
|
975
|
+
if (isNonEmptyString(msg.content)) {
|
|
231
976
|
parts.push({ text: msg.content });
|
|
232
977
|
}
|
|
233
978
|
else if (Array.isArray(msg.content)) {
|
|
@@ -236,13 +981,12 @@ export class GeminiProvider extends BaseProvider {
|
|
|
236
981
|
}
|
|
237
982
|
}
|
|
238
983
|
if (parts.length > 0) {
|
|
239
|
-
contents
|
|
984
|
+
appendGeminiContent(contents, { role: "model", parts });
|
|
240
985
|
}
|
|
241
986
|
continue;
|
|
242
987
|
}
|
|
243
|
-
// user
|
|
244
988
|
const parts = [];
|
|
245
|
-
if (
|
|
989
|
+
if (isString(msg.content)) {
|
|
246
990
|
parts.push({ text: msg.content });
|
|
247
991
|
}
|
|
248
992
|
else if (Array.isArray(msg.content)) {
|
|
@@ -251,17 +995,26 @@ export class GeminiProvider extends BaseProvider {
|
|
|
251
995
|
}
|
|
252
996
|
}
|
|
253
997
|
if (parts.length > 0) {
|
|
254
|
-
contents
|
|
998
|
+
appendGeminiContent(contents, { role: "user", parts });
|
|
255
999
|
}
|
|
256
1000
|
}
|
|
1001
|
+
signUnsignedFunctionCalls(contents);
|
|
257
1002
|
return { contents, systemInstruction };
|
|
258
1003
|
}
|
|
259
1004
|
formatTools(tools) {
|
|
1005
|
+
return this.formatToolsForModel(tools, "gemini-3");
|
|
1006
|
+
}
|
|
1007
|
+
formatToolsForModel(tools, model) {
|
|
260
1008
|
const nameMap = new Map();
|
|
261
1009
|
const reverseMap = new Map();
|
|
262
1010
|
const usedNames = new Set();
|
|
263
1011
|
const declarations = [];
|
|
264
1012
|
for (const tool of tools) {
|
|
1013
|
+
if ((tool.name === WEB_SEARCH_TOOL_NAME &&
|
|
1014
|
+
this.usesNativeWebSearch(model, tools)) ||
|
|
1015
|
+
tool.type === "code_interpreter") {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
265
1018
|
const original = tool.name;
|
|
266
1019
|
let unique = sanitizeToolName(original);
|
|
267
1020
|
let suffix = 2;
|
|
@@ -273,7 +1026,10 @@ export class GeminiProvider extends BaseProvider {
|
|
|
273
1026
|
usedNames.add(unique);
|
|
274
1027
|
nameMap.set(original, unique);
|
|
275
1028
|
reverseMap.set(unique, original);
|
|
276
|
-
const rawParameters = tool.inputSchema ?? {
|
|
1029
|
+
const rawParameters = tool.inputSchema ?? {
|
|
1030
|
+
type: "object",
|
|
1031
|
+
properties: {}
|
|
1032
|
+
};
|
|
277
1033
|
declarations.push({
|
|
278
1034
|
name: unique,
|
|
279
1035
|
description: tool.description ?? "",
|
|
@@ -286,20 +1042,82 @@ export class GeminiProvider extends BaseProvider {
|
|
|
286
1042
|
reverseMap
|
|
287
1043
|
};
|
|
288
1044
|
}
|
|
1045
|
+
usesNativeWebSearch(model, tools) {
|
|
1046
|
+
const hasWebSearch = tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
|
|
1047
|
+
if (!hasWebSearch)
|
|
1048
|
+
return false;
|
|
1049
|
+
const hasCustomTools = this.hasCustomFunctionTools(tools);
|
|
1050
|
+
return !hasCustomTools || isGemini3Model(model);
|
|
1051
|
+
}
|
|
1052
|
+
hasCustomFunctionTools(tools) {
|
|
1053
|
+
return tools.some((tool) => tool.name !== WEB_SEARCH_TOOL_NAME &&
|
|
1054
|
+
tool.type !== "code_interpreter");
|
|
1055
|
+
}
|
|
1056
|
+
validateToolCombination(model, tools) {
|
|
1057
|
+
if (isGemini3Model(model))
|
|
1058
|
+
return;
|
|
1059
|
+
const hasCodeExecution = tools.some((tool) => tool.type === "code_interpreter");
|
|
1060
|
+
if (hasCodeExecution && this.hasCustomFunctionTools(tools)) {
|
|
1061
|
+
throw new Error(`Gemini code execution cannot be combined with other tools on ${model}; ` +
|
|
1062
|
+
"only Gemini 3 models support this combination");
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Fill in `tools` and `toolConfig` on a request body.
|
|
1067
|
+
*
|
|
1068
|
+
* Built-in tools (googleSearch, codeExecution) run server side. When they are
|
|
1069
|
+
* combined with functionDeclarations, Gemini rejects the request unless
|
|
1070
|
+
* `toolConfig.includeServerSideToolInvocations` is set — the built-in calls
|
|
1071
|
+
* and their results have to be echoed back into the conversation for the
|
|
1072
|
+
* function-calling loop to stay coherent.
|
|
1073
|
+
*/
|
|
1074
|
+
applyTools(body, model, tools, geminiTools, nameMap, toolChoice) {
|
|
1075
|
+
if (geminiTools.length > 0) {
|
|
1076
|
+
body.tools = geminiTools;
|
|
1077
|
+
}
|
|
1078
|
+
let hasBuiltIn = false;
|
|
1079
|
+
if (this.usesNativeWebSearch(model, tools)) {
|
|
1080
|
+
body.tools = [...(body.tools ?? []), { googleSearch: {} }];
|
|
1081
|
+
hasBuiltIn = true;
|
|
1082
|
+
}
|
|
1083
|
+
if (tools.some((tool) => tool.type === "code_interpreter")) {
|
|
1084
|
+
body.tools = [...(body.tools ?? []), { codeExecution: {} }];
|
|
1085
|
+
hasBuiltIn = true;
|
|
1086
|
+
}
|
|
1087
|
+
const toolConfig = {};
|
|
1088
|
+
if (hasBuiltIn && geminiTools.length > 0) {
|
|
1089
|
+
toolConfig.includeServerSideToolInvocations = true;
|
|
1090
|
+
}
|
|
1091
|
+
if (toolChoice &&
|
|
1092
|
+
(toolChoice === "any" ? geminiTools.length > 0 : nameMap.has(toolChoice))) {
|
|
1093
|
+
const selected = toolChoice === "any"
|
|
1094
|
+
? undefined
|
|
1095
|
+
: (nameMap.get(toolChoice) ?? sanitizeToolName(toolChoice));
|
|
1096
|
+
const functionCallingConfig = {
|
|
1097
|
+
mode: "ANY"
|
|
1098
|
+
};
|
|
1099
|
+
if (selected) {
|
|
1100
|
+
functionCallingConfig.allowedFunctionNames = [selected];
|
|
1101
|
+
}
|
|
1102
|
+
toolConfig.functionCallingConfig = functionCallingConfig;
|
|
1103
|
+
}
|
|
1104
|
+
if (Object.keys(toolConfig).length > 0) {
|
|
1105
|
+
body.toolConfig = toolConfig;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
289
1108
|
// ---------------------------------------------------------------------------
|
|
290
1109
|
// Non-streaming generation
|
|
291
1110
|
// ---------------------------------------------------------------------------
|
|
292
1111
|
async generateMessage(args) {
|
|
293
|
-
const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
|
|
294
|
-
|
|
295
|
-
const { geminiTools, reverseMap } = this.
|
|
1112
|
+
const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
|
|
1113
|
+
this.validateToolCombination(model, tools);
|
|
1114
|
+
const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
|
|
1115
|
+
const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
|
|
296
1116
|
const body = { contents };
|
|
297
1117
|
if (systemInstruction) {
|
|
298
1118
|
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
299
1119
|
}
|
|
300
|
-
|
|
301
|
-
body.tools = geminiTools;
|
|
302
|
-
}
|
|
1120
|
+
this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
|
|
303
1121
|
const generationConfig = {
|
|
304
1122
|
maxOutputTokens: maxTokens
|
|
305
1123
|
};
|
|
@@ -307,13 +1125,19 @@ export class GeminiProvider extends BaseProvider {
|
|
|
307
1125
|
generationConfig.temperature = temperature;
|
|
308
1126
|
if (topP != null)
|
|
309
1127
|
generationConfig.topP = topP;
|
|
1128
|
+
if (presencePenalty != null)
|
|
1129
|
+
generationConfig.presencePenalty = presencePenalty;
|
|
1130
|
+
if (frequencyPenalty != null)
|
|
1131
|
+
generationConfig.frequencyPenalty = frequencyPenalty;
|
|
310
1132
|
body.generationConfig = generationConfig;
|
|
311
1133
|
log.debug("Gemini request", { model });
|
|
312
1134
|
const url = `${GEMINI_API_BASE}/models/${model}:generateContent?key=${this.apiKey}`;
|
|
1135
|
+
this.recordRequestPayload(body);
|
|
313
1136
|
const response = await this._fetch(url, {
|
|
314
1137
|
method: "POST",
|
|
315
1138
|
headers: { "Content-Type": "application/json" },
|
|
316
|
-
body: JSON.stringify(body)
|
|
1139
|
+
body: JSON.stringify(body),
|
|
1140
|
+
signal: args.signal
|
|
317
1141
|
});
|
|
318
1142
|
if (!response.ok) {
|
|
319
1143
|
const text = await response.text();
|
|
@@ -323,30 +1147,33 @@ export class GeminiProvider extends BaseProvider {
|
|
|
323
1147
|
});
|
|
324
1148
|
throw new Error(`Gemini API error ${response.status}: ${text}`);
|
|
325
1149
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1150
|
+
const decoded = decodeGeminiGenerateContent(await response.json());
|
|
1151
|
+
this.trackGeminiUsage(model, decoded.usage);
|
|
1152
|
+
return this.extractMessage(decoded.parts, reverseMap);
|
|
1153
|
+
}
|
|
1154
|
+
/** Record token usage from a Gemini usageMetadata block (if present). */
|
|
1155
|
+
trackGeminiUsage(model, usage) {
|
|
1156
|
+
if (!usage)
|
|
1157
|
+
return;
|
|
1158
|
+
this.trackUsage(model, {
|
|
1159
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
1160
|
+
outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0),
|
|
1161
|
+
cachedTokens: usage.cachedContentTokenCount ?? 0
|
|
1162
|
+
});
|
|
335
1163
|
}
|
|
336
1164
|
// ---------------------------------------------------------------------------
|
|
337
1165
|
// Streaming generation
|
|
338
1166
|
// ---------------------------------------------------------------------------
|
|
339
1167
|
async *generateMessages(args) {
|
|
340
|
-
const { model, tools = [], maxTokens = 16384, temperature, topP } = args;
|
|
341
|
-
|
|
342
|
-
const { geminiTools, reverseMap } = this.
|
|
1168
|
+
const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
|
|
1169
|
+
this.validateToolCombination(model, tools);
|
|
1170
|
+
const { geminiTools, nameMap, reverseMap } = this.formatToolsForModel(tools, model);
|
|
1171
|
+
const { contents, systemInstruction } = await this.convertMessages(args.messages, nameMap);
|
|
343
1172
|
const body = { contents };
|
|
344
1173
|
if (systemInstruction) {
|
|
345
1174
|
body.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
346
1175
|
}
|
|
347
|
-
|
|
348
|
-
body.tools = geminiTools;
|
|
349
|
-
}
|
|
1176
|
+
this.applyTools(body, model, tools, geminiTools, nameMap, args.toolChoice);
|
|
350
1177
|
const generationConfig = {
|
|
351
1178
|
maxOutputTokens: maxTokens
|
|
352
1179
|
};
|
|
@@ -354,13 +1181,19 @@ export class GeminiProvider extends BaseProvider {
|
|
|
354
1181
|
generationConfig.temperature = temperature;
|
|
355
1182
|
if (topP != null)
|
|
356
1183
|
generationConfig.topP = topP;
|
|
1184
|
+
if (presencePenalty != null)
|
|
1185
|
+
generationConfig.presencePenalty = presencePenalty;
|
|
1186
|
+
if (frequencyPenalty != null)
|
|
1187
|
+
generationConfig.frequencyPenalty = frequencyPenalty;
|
|
357
1188
|
body.generationConfig = generationConfig;
|
|
358
1189
|
log.debug("Gemini request", { model });
|
|
359
1190
|
const url = `${GEMINI_API_BASE}/models/${model}:streamGenerateContent?alt=sse&key=${this.apiKey}`;
|
|
1191
|
+
this.recordRequestPayload(body);
|
|
360
1192
|
const response = await this._fetch(url, {
|
|
361
1193
|
method: "POST",
|
|
362
1194
|
headers: { "Content-Type": "application/json" },
|
|
363
|
-
body: JSON.stringify(body)
|
|
1195
|
+
body: JSON.stringify(body),
|
|
1196
|
+
signal: args.signal
|
|
364
1197
|
});
|
|
365
1198
|
if (!response.ok) {
|
|
366
1199
|
const text = await response.text();
|
|
@@ -373,71 +1206,51 @@ export class GeminiProvider extends BaseProvider {
|
|
|
373
1206
|
if (!response.body) {
|
|
374
1207
|
throw new Error("Gemini streaming response has no body");
|
|
375
1208
|
}
|
|
376
|
-
const reader = response.body.getReader();
|
|
377
|
-
const decoder = new TextDecoder();
|
|
378
|
-
let buffer = "";
|
|
379
1209
|
// Accumulate all parts across SSE events for raw replay.
|
|
380
1210
|
// Gemini thinking models emit thought parts and function calls across
|
|
381
1211
|
// separate SSE events, but they must all be sent back together.
|
|
382
1212
|
const allParts = [];
|
|
383
1213
|
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
|
|
1214
|
+
// Gemini SSE reports CUMULATIVE usageMetadata; keep the last one seen and
|
|
1215
|
+
// record it once after the stream (accumulating each event would over-count).
|
|
1216
|
+
let lastUsage;
|
|
1217
|
+
for await (const event of decodeGeminiSse(response.body, args.signal)) {
|
|
1218
|
+
const eventError = geminiResponseError(event);
|
|
1219
|
+
if (eventError)
|
|
1220
|
+
throw eventError;
|
|
1221
|
+
if (event.usageMetadata)
|
|
1222
|
+
lastUsage = event.usageMetadata;
|
|
1223
|
+
const parts = event.candidates?.[0]?.content?.parts;
|
|
1224
|
+
if (!parts)
|
|
1225
|
+
continue;
|
|
1226
|
+
for (const part of parts) {
|
|
1227
|
+
allParts.push(part);
|
|
1228
|
+
if (part.text !== undefined && !part.thought) {
|
|
1229
|
+
const chunk = {
|
|
1230
|
+
type: "chunk",
|
|
1231
|
+
content: part.text,
|
|
1232
|
+
done: false
|
|
1233
|
+
};
|
|
1234
|
+
yield chunk;
|
|
1235
|
+
}
|
|
1236
|
+
else if (part.functionCall) {
|
|
1237
|
+
const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
|
|
1238
|
+
const toolCall = {
|
|
1239
|
+
id: part.functionCall.id ??
|
|
1240
|
+
`call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
1241
|
+
name: originalName,
|
|
1242
|
+
args: part.functionCall.args ?? {}
|
|
1243
|
+
};
|
|
1244
|
+
if (part.thoughtSignature) {
|
|
1245
|
+
toolCall.thought_signature = part.thoughtSignature;
|
|
432
1246
|
}
|
|
1247
|
+
pendingToolCalls.push(toolCall);
|
|
433
1248
|
}
|
|
434
1249
|
}
|
|
435
1250
|
}
|
|
436
|
-
|
|
437
|
-
reader.releaseLock();
|
|
438
|
-
}
|
|
1251
|
+
this.trackGeminiUsage(model, lastUsage);
|
|
439
1252
|
// Attach accumulated raw parts to tool calls for thought replay
|
|
440
|
-
const hasThoughts = allParts.some((p) => p.thought);
|
|
1253
|
+
const hasThoughts = allParts.some((p) => p.thought || p.thoughtSignature);
|
|
441
1254
|
for (const tc of pendingToolCalls) {
|
|
442
1255
|
if (hasThoughts) {
|
|
443
1256
|
tc._rawGeminiParts = allParts;
|
|
@@ -459,13 +1272,14 @@ export class GeminiProvider extends BaseProvider {
|
|
|
459
1272
|
const textParts = [];
|
|
460
1273
|
const toolCalls = [];
|
|
461
1274
|
for (const part of parts) {
|
|
462
|
-
if (part.text !== undefined) {
|
|
1275
|
+
if (part.text !== undefined && !part.thought) {
|
|
463
1276
|
textParts.push(part.text);
|
|
464
1277
|
}
|
|
465
1278
|
else if (part.functionCall) {
|
|
466
1279
|
const originalName = reverseMap.get(part.functionCall.name) ?? part.functionCall.name;
|
|
467
1280
|
const tc = {
|
|
468
|
-
id:
|
|
1281
|
+
id: part.functionCall.id ??
|
|
1282
|
+
`call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
469
1283
|
name: originalName,
|
|
470
1284
|
args: part.functionCall.args ?? {}
|
|
471
1285
|
};
|
|
@@ -475,7 +1289,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
475
1289
|
toolCalls.push(tc);
|
|
476
1290
|
}
|
|
477
1291
|
}
|
|
478
|
-
const hasThoughts = parts.some((p) => p.thought);
|
|
1292
|
+
const hasThoughts = parts.some((p) => p.thought || p.thoughtSignature);
|
|
479
1293
|
const msg = {
|
|
480
1294
|
role: "assistant",
|
|
481
1295
|
content: textParts.join("") || null,
|
|
@@ -492,11 +1306,50 @@ export class GeminiProvider extends BaseProvider {
|
|
|
492
1306
|
async getAvailableImageModels() {
|
|
493
1307
|
return [
|
|
494
1308
|
{
|
|
495
|
-
id: "gemini-
|
|
496
|
-
name: "Gemini
|
|
497
|
-
provider: "gemini"
|
|
1309
|
+
id: "gemini-3.1-flash-image",
|
|
1310
|
+
name: "Gemini 3.1 Flash Image",
|
|
1311
|
+
provider: "gemini",
|
|
1312
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1313
|
+
aspectRatios: [
|
|
1314
|
+
"1:1",
|
|
1315
|
+
"2:3",
|
|
1316
|
+
"3:2",
|
|
1317
|
+
"3:4",
|
|
1318
|
+
"4:3",
|
|
1319
|
+
"4:5",
|
|
1320
|
+
"5:4",
|
|
1321
|
+
"9:16",
|
|
1322
|
+
"16:9",
|
|
1323
|
+
"21:9"
|
|
1324
|
+
],
|
|
1325
|
+
resolutions: ["1K", "2K", "4K"]
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
id: "gemini-3.1-flash-lite-image",
|
|
1329
|
+
name: "Gemini 3.1 Flash-Lite Image",
|
|
1330
|
+
provider: "gemini",
|
|
1331
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1332
|
+
aspectRatios: ["1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"]
|
|
498
1333
|
},
|
|
499
|
-
{
|
|
1334
|
+
{
|
|
1335
|
+
id: "gemini-3-pro-image",
|
|
1336
|
+
name: "Gemini 3 Pro Image",
|
|
1337
|
+
provider: "gemini",
|
|
1338
|
+
supportedTasks: ["text_to_image", "image_to_image"],
|
|
1339
|
+
aspectRatios: [
|
|
1340
|
+
"1:1",
|
|
1341
|
+
"2:3",
|
|
1342
|
+
"3:2",
|
|
1343
|
+
"3:4",
|
|
1344
|
+
"4:3",
|
|
1345
|
+
"4:5",
|
|
1346
|
+
"5:4",
|
|
1347
|
+
"9:16",
|
|
1348
|
+
"16:9",
|
|
1349
|
+
"21:9"
|
|
1350
|
+
],
|
|
1351
|
+
resolutions: ["1K", "2K", "4K"]
|
|
1352
|
+
}
|
|
500
1353
|
];
|
|
501
1354
|
}
|
|
502
1355
|
async getAvailableTTSModels() {
|
|
@@ -533,6 +1386,30 @@ export class GeminiProvider extends BaseProvider {
|
|
|
533
1386
|
"Sulafat"
|
|
534
1387
|
];
|
|
535
1388
|
return [
|
|
1389
|
+
{
|
|
1390
|
+
id: "gemini-3.8-flash-tts",
|
|
1391
|
+
name: "Gemini 3.8 Flash TTS",
|
|
1392
|
+
provider: "gemini",
|
|
1393
|
+
voices
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
id: "gemini-3.8-flash-lite-tts",
|
|
1397
|
+
name: "Gemini 3.8 Flash-Lite TTS",
|
|
1398
|
+
provider: "gemini",
|
|
1399
|
+
voices
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
id: "gemini-3.1-flash-tts-preview",
|
|
1403
|
+
name: "Gemini 3.1 Flash TTS Preview",
|
|
1404
|
+
provider: "gemini",
|
|
1405
|
+
voices
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
id: "gemini-2.5-flash-preview-tts",
|
|
1409
|
+
name: "Gemini 2.5 Flash TTS",
|
|
1410
|
+
provider: "gemini",
|
|
1411
|
+
voices
|
|
1412
|
+
},
|
|
536
1413
|
{
|
|
537
1414
|
id: "gemini-2.5-pro-preview-tts",
|
|
538
1415
|
name: "Gemini 2.5 Pro TTS",
|
|
@@ -543,27 +1420,51 @@ export class GeminiProvider extends BaseProvider {
|
|
|
543
1420
|
}
|
|
544
1421
|
async getAvailableASRModels() {
|
|
545
1422
|
return [
|
|
546
|
-
{
|
|
547
|
-
|
|
1423
|
+
{
|
|
1424
|
+
// The transcription-specific model. It answers `generateContent`, so
|
|
1425
|
+
// it runs through the same path as the general models below.
|
|
1426
|
+
id: "gemini-3.5-transcribe",
|
|
1427
|
+
name: "Gemini 3.5 Transcribe",
|
|
1428
|
+
provider: "gemini"
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
id: "gemini-3.5-flash",
|
|
1432
|
+
name: "Gemini 3.5 Flash",
|
|
1433
|
+
provider: "gemini"
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
id: "gemini-3.1-flash-lite",
|
|
1437
|
+
name: "Gemini 3.1 Flash-Lite",
|
|
1438
|
+
provider: "gemini"
|
|
1439
|
+
}
|
|
548
1440
|
];
|
|
549
1441
|
}
|
|
550
1442
|
async getAvailableVideoModels() {
|
|
1443
|
+
return [...GEMINI_VIDEO_MODELS];
|
|
1444
|
+
}
|
|
1445
|
+
async getAvailableMusicModels() {
|
|
551
1446
|
return [
|
|
552
1447
|
{
|
|
553
|
-
id: "
|
|
554
|
-
name: "
|
|
555
|
-
provider: "gemini"
|
|
1448
|
+
id: "lyria-3.5",
|
|
1449
|
+
name: "Lyria 3.5",
|
|
1450
|
+
provider: "gemini",
|
|
1451
|
+
supportedTasks: ["text_to_music"]
|
|
556
1452
|
},
|
|
557
|
-
{
|
|
1453
|
+
{
|
|
1454
|
+
id: "lyria-3-clip-preview",
|
|
1455
|
+
name: "Lyria 3 Clip Preview",
|
|
1456
|
+
provider: "gemini",
|
|
1457
|
+
supportedTasks: ["text_to_music"]
|
|
1458
|
+
}
|
|
558
1459
|
];
|
|
559
1460
|
}
|
|
560
1461
|
async getAvailableEmbeddingModels() {
|
|
561
1462
|
return [
|
|
562
1463
|
{
|
|
563
|
-
id: "
|
|
564
|
-
name: "
|
|
1464
|
+
id: "gemini-embedding-2",
|
|
1465
|
+
name: "Gemini Embedding 2",
|
|
565
1466
|
provider: "gemini",
|
|
566
|
-
dimensions:
|
|
1467
|
+
dimensions: 3072
|
|
567
1468
|
},
|
|
568
1469
|
{
|
|
569
1470
|
id: "gemini-embedding-001",
|
|
@@ -581,7 +1482,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
581
1482
|
if (!text || (Array.isArray(text) && text.length === 0)) {
|
|
582
1483
|
throw new Error("text must not be empty");
|
|
583
1484
|
}
|
|
584
|
-
const texts =
|
|
1485
|
+
const texts = isString(text) ? [text] : text;
|
|
585
1486
|
// Gemini embedContent supports a single content; batch by calling per text
|
|
586
1487
|
const embeddings = [];
|
|
587
1488
|
for (const t of texts) {
|
|
@@ -605,7 +1506,9 @@ export class GeminiProvider extends BaseProvider {
|
|
|
605
1506
|
if (!data.embedding?.values) {
|
|
606
1507
|
throw new Error("No embedding returned from Gemini API");
|
|
607
1508
|
}
|
|
608
|
-
embeddings.push(
|
|
1509
|
+
embeddings.push(dimensions && dimensions < 3072
|
|
1510
|
+
? normalizeEmbedding(data.embedding.values)
|
|
1511
|
+
: data.embedding.values);
|
|
609
1512
|
}
|
|
610
1513
|
return embeddings;
|
|
611
1514
|
}
|
|
@@ -619,11 +1522,20 @@ export class GeminiProvider extends BaseProvider {
|
|
|
619
1522
|
const modelId = params.model.id;
|
|
620
1523
|
if (modelId.startsWith("gemini-")) {
|
|
621
1524
|
// Use generateContent with IMAGE response modality
|
|
1525
|
+
const imageConfig = {};
|
|
1526
|
+
if (params.aspectRatio)
|
|
1527
|
+
imageConfig.aspectRatio = params.aspectRatio;
|
|
1528
|
+
if (params.resolution)
|
|
1529
|
+
imageConfig.imageSize = params.resolution;
|
|
1530
|
+
const generationConfig = {
|
|
1531
|
+
responseModalities: ["IMAGE", "TEXT"]
|
|
1532
|
+
};
|
|
1533
|
+
if (Object.keys(imageConfig).length > 0) {
|
|
1534
|
+
generationConfig.imageConfig = imageConfig;
|
|
1535
|
+
}
|
|
622
1536
|
const body = {
|
|
623
1537
|
contents: [{ role: "user", parts: [{ text: params.prompt }] }],
|
|
624
|
-
generationConfig
|
|
625
|
-
responseModalities: ["IMAGE", "TEXT"]
|
|
626
|
-
}
|
|
1538
|
+
generationConfig
|
|
627
1539
|
};
|
|
628
1540
|
const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
|
|
629
1541
|
const response = await this._fetch(url, {
|
|
@@ -635,7 +1547,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
635
1547
|
const errText = await response.text();
|
|
636
1548
|
throw new Error(`Gemini text-to-image failed ${response.status}: ${errText}`);
|
|
637
1549
|
}
|
|
638
|
-
const data = (await response.json());
|
|
1550
|
+
const data = parseGeminiResponse(await response.json());
|
|
639
1551
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
640
1552
|
if (!parts)
|
|
641
1553
|
throw new Error("No candidates in response");
|
|
@@ -646,33 +1558,13 @@ export class GeminiProvider extends BaseProvider {
|
|
|
646
1558
|
}
|
|
647
1559
|
throw new Error("No image data returned in response");
|
|
648
1560
|
}
|
|
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"));
|
|
1561
|
+
// Google shut down Imagen in the Gemini API; `predict` now answers 404.
|
|
1562
|
+
throw new Error(`Model ${modelId} is not available. Use a gemini-*-image model for text-to-image.`);
|
|
671
1563
|
}
|
|
672
1564
|
// ---------------------------------------------------------------------------
|
|
673
1565
|
// Image-to-image
|
|
674
1566
|
// ---------------------------------------------------------------------------
|
|
675
|
-
async imageToImage(
|
|
1567
|
+
async imageToImage(images, params) {
|
|
676
1568
|
if (!params.prompt) {
|
|
677
1569
|
throw new Error("The input prompt cannot be empty.");
|
|
678
1570
|
}
|
|
@@ -680,20 +1572,36 @@ export class GeminiProvider extends BaseProvider {
|
|
|
680
1572
|
if (!modelId.startsWith("gemini-")) {
|
|
681
1573
|
throw new Error(`Model ${modelId} does not support image-to-image. Only gemini-* models supported.`);
|
|
682
1574
|
}
|
|
683
|
-
const
|
|
1575
|
+
const imageParts = images
|
|
1576
|
+
.filter((b) => b && b.length > 0)
|
|
1577
|
+
.map((b) => ({
|
|
1578
|
+
inlineData: {
|
|
1579
|
+
mimeType: "image/png",
|
|
1580
|
+
data: Buffer.from(b).toString("base64")
|
|
1581
|
+
}
|
|
1582
|
+
}));
|
|
1583
|
+
if (imageParts.length === 0) {
|
|
1584
|
+
throw new Error("At least one input image is required");
|
|
1585
|
+
}
|
|
1586
|
+
const imageConfig = {};
|
|
1587
|
+
if (params.aspectRatio)
|
|
1588
|
+
imageConfig.aspectRatio = params.aspectRatio;
|
|
1589
|
+
if (params.resolution)
|
|
1590
|
+
imageConfig.imageSize = params.resolution;
|
|
1591
|
+
const generationConfig = {
|
|
1592
|
+
responseModalities: ["IMAGE", "TEXT"]
|
|
1593
|
+
};
|
|
1594
|
+
if (Object.keys(imageConfig).length > 0) {
|
|
1595
|
+
generationConfig.imageConfig = imageConfig;
|
|
1596
|
+
}
|
|
684
1597
|
const body = {
|
|
685
1598
|
contents: [
|
|
686
1599
|
{
|
|
687
1600
|
role: "user",
|
|
688
|
-
parts: [
|
|
689
|
-
{ text: params.prompt },
|
|
690
|
-
{ inlineData: { mimeType: "image/png", data: imageBase64 } }
|
|
691
|
-
]
|
|
1601
|
+
parts: [{ text: params.prompt }, ...imageParts]
|
|
692
1602
|
}
|
|
693
1603
|
],
|
|
694
|
-
generationConfig
|
|
695
|
-
responseModalities: ["IMAGE", "TEXT"]
|
|
696
|
-
}
|
|
1604
|
+
generationConfig
|
|
697
1605
|
};
|
|
698
1606
|
const url = `${GEMINI_API_BASE}/models/${modelId}:generateContent?key=${this.apiKey}`;
|
|
699
1607
|
const response = await this._fetch(url, {
|
|
@@ -705,7 +1613,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
705
1613
|
const errText = await response.text();
|
|
706
1614
|
throw new Error(`Gemini image-to-image failed ${response.status}: ${errText}`);
|
|
707
1615
|
}
|
|
708
|
-
const data = (await response.json());
|
|
1616
|
+
const data = parseGeminiResponse(await response.json());
|
|
709
1617
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
710
1618
|
if (!parts)
|
|
711
1619
|
throw new Error("No candidates in response");
|
|
@@ -720,7 +1628,14 @@ export class GeminiProvider extends BaseProvider {
|
|
|
720
1628
|
// Text-to-speech
|
|
721
1629
|
// ---------------------------------------------------------------------------
|
|
722
1630
|
async *textToSpeech(args) {
|
|
723
|
-
const {
|
|
1631
|
+
const { model, voice = "Puck" } = args;
|
|
1632
|
+
const text = [
|
|
1633
|
+
args.language ? `Speak in ${args.language}.` : undefined,
|
|
1634
|
+
args.instructions,
|
|
1635
|
+
args.text
|
|
1636
|
+
]
|
|
1637
|
+
.filter((value) => value !== undefined)
|
|
1638
|
+
.join(" ");
|
|
724
1639
|
const body = {
|
|
725
1640
|
contents: [{ role: "user", parts: [{ text }] }],
|
|
726
1641
|
generationConfig: {
|
|
@@ -742,7 +1657,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
742
1657
|
const errText = await response.text();
|
|
743
1658
|
throw new Error(`Gemini TTS failed ${response.status}: ${errText}`);
|
|
744
1659
|
}
|
|
745
|
-
const data = (await response.json());
|
|
1660
|
+
const data = parseGeminiResponse(await response.json());
|
|
746
1661
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
747
1662
|
if (!parts)
|
|
748
1663
|
throw new Error("No audio in response");
|
|
@@ -763,31 +1678,34 @@ export class GeminiProvider extends BaseProvider {
|
|
|
763
1678
|
if (!audio || audio.length === 0) {
|
|
764
1679
|
throw new Error("audio must not be empty");
|
|
765
1680
|
}
|
|
766
|
-
|
|
767
|
-
|
|
1681
|
+
if (audio.length > 20 * 1024 * 1024) {
|
|
1682
|
+
throw new Error("Gemini inline audio is limited to 20 MB; upload the audio with the File API first");
|
|
1683
|
+
}
|
|
1684
|
+
// Detect MIME type from the audio header.
|
|
1685
|
+
let mimeType = geminiAudioMime(sniffAudioMime(audio));
|
|
768
1686
|
if (audio[0] === 0x52 &&
|
|
769
1687
|
audio[1] === 0x49 &&
|
|
770
1688
|
audio[2] === 0x46 &&
|
|
771
1689
|
audio[3] === 0x46) {
|
|
772
|
-
mimeType = "audio/wav";
|
|
1690
|
+
mimeType = "audio/wav";
|
|
773
1691
|
}
|
|
774
1692
|
else if (audio[0] === 0x49 && audio[1] === 0x44 && audio[2] === 0x33) {
|
|
775
|
-
mimeType = "audio/mp3";
|
|
1693
|
+
mimeType = "audio/mp3";
|
|
776
1694
|
}
|
|
777
1695
|
else if (audio[0] === 0xff && (audio[1] === 0xfb || audio[1] === 0xf3)) {
|
|
778
|
-
mimeType = "audio/mp3";
|
|
1696
|
+
mimeType = "audio/mp3";
|
|
779
1697
|
}
|
|
780
1698
|
else if (audio[0] === 0x66 &&
|
|
781
1699
|
audio[1] === 0x4c &&
|
|
782
1700
|
audio[2] === 0x61 &&
|
|
783
1701
|
audio[3] === 0x43) {
|
|
784
|
-
mimeType = "audio/flac";
|
|
1702
|
+
mimeType = "audio/flac";
|
|
785
1703
|
}
|
|
786
1704
|
else if (audio[0] === 0x4f &&
|
|
787
1705
|
audio[1] === 0x67 &&
|
|
788
1706
|
audio[2] === 0x67 &&
|
|
789
1707
|
audio[3] === 0x53) {
|
|
790
|
-
mimeType = "audio/ogg";
|
|
1708
|
+
mimeType = "audio/ogg";
|
|
791
1709
|
}
|
|
792
1710
|
let promptText = args.prompt ?? "Transcribe this audio to text.";
|
|
793
1711
|
if (language) {
|
|
@@ -818,7 +1736,7 @@ export class GeminiProvider extends BaseProvider {
|
|
|
818
1736
|
const errText = await response.text();
|
|
819
1737
|
throw new Error(`Gemini ASR failed ${response.status}: ${errText}`);
|
|
820
1738
|
}
|
|
821
|
-
const data = (await response.json());
|
|
1739
|
+
const data = parseGeminiResponse(await response.json());
|
|
822
1740
|
const parts = data.candidates?.[0]?.content?.parts;
|
|
823
1741
|
if (!parts)
|
|
824
1742
|
return { text: "" };
|
|
@@ -828,142 +1746,423 @@ export class GeminiProvider extends BaseProvider {
|
|
|
828
1746
|
.join("");
|
|
829
1747
|
return { text };
|
|
830
1748
|
}
|
|
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
|
-
};
|
|
1749
|
+
buildVideoParameters(params) {
|
|
845
1750
|
const parameters = {};
|
|
846
|
-
if (params.negativePrompt)
|
|
1751
|
+
if (params.negativePrompt) {
|
|
847
1752
|
parameters.negativePrompt = params.negativePrompt;
|
|
848
|
-
|
|
1753
|
+
}
|
|
1754
|
+
if (params.aspectRatio) {
|
|
849
1755
|
parameters.aspectRatio = params.aspectRatio;
|
|
850
|
-
|
|
1756
|
+
}
|
|
1757
|
+
if (params.resolution) {
|
|
1758
|
+
parameters.resolution = params.resolution;
|
|
1759
|
+
}
|
|
1760
|
+
if (params.durationSeconds != null) {
|
|
1761
|
+
parameters.durationSeconds = params.durationSeconds;
|
|
1762
|
+
}
|
|
1763
|
+
if (params.seed != null) {
|
|
851
1764
|
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
1765
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1766
|
+
return parameters;
|
|
1767
|
+
}
|
|
1768
|
+
getVideoUri(operation) {
|
|
1769
|
+
return (operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video
|
|
1770
|
+
?.uri ?? operation.response?.generatedVideos?.[0]?.video?.uri);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* The refusal to raise when an operation came back with no video, or null
|
|
1774
|
+
* when nothing says the filter is why. A filtered take leaves the operation
|
|
1775
|
+
* successful and the samples empty, so without this the caller sees only
|
|
1776
|
+
* "No video URI in response" — indistinguishable from a broken response, and
|
|
1777
|
+
* fatal to a run that has already paid for its other shots.
|
|
1778
|
+
*/
|
|
1779
|
+
videoContentFilterRefusal(operation, modelId) {
|
|
1780
|
+
const rai = operation.response?.generateVideoResponse ?? operation.response;
|
|
1781
|
+
const reasons = rai?.raiMediaFilteredReasons ?? [];
|
|
1782
|
+
const filtered = rai?.raiMediaFilteredCount ?? 0;
|
|
1783
|
+
if (filtered <= 0 && reasons.length === 0)
|
|
1784
|
+
return null;
|
|
1785
|
+
const detail = reasons.length
|
|
1786
|
+
? reasons.join("; ")
|
|
1787
|
+
: "no reason given by the provider";
|
|
1788
|
+
return new ContentFilterRefusal(`Veo filtered every generated video for this prompt: ${detail}`, { provider: "gemini", model: modelId, reasons });
|
|
1789
|
+
}
|
|
1790
|
+
async waitForVideoOperation(operation, timeoutSeconds, signal) {
|
|
1791
|
+
const maxWait = timeoutSeconds && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 600_000;
|
|
868
1792
|
const pollInterval = 10_000;
|
|
869
1793
|
let elapsed = 0;
|
|
870
1794
|
let current = operation;
|
|
871
1795
|
while (!current.done && elapsed < maxWait) {
|
|
872
|
-
await new Promise((
|
|
1796
|
+
await new Promise((resolve, reject) => {
|
|
1797
|
+
const onAbort = () => {
|
|
1798
|
+
clearTimeout(timer);
|
|
1799
|
+
reject(abortError(signal));
|
|
1800
|
+
};
|
|
1801
|
+
const timer = setTimeout(() => {
|
|
1802
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1803
|
+
resolve();
|
|
1804
|
+
}, pollInterval);
|
|
1805
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1806
|
+
});
|
|
873
1807
|
elapsed += pollInterval;
|
|
874
|
-
if (!current.name)
|
|
1808
|
+
if (!current.name) {
|
|
875
1809
|
throw new Error("No operation name for polling");
|
|
876
|
-
|
|
877
|
-
const pollResp = await this._fetch(
|
|
1810
|
+
}
|
|
1811
|
+
const pollResp = await this._fetch(`${GEMINI_API_BASE}/${current.name}`, {
|
|
1812
|
+
headers: { "x-goog-api-key": this.apiKey },
|
|
1813
|
+
signal
|
|
1814
|
+
});
|
|
878
1815
|
if (!pollResp.ok) {
|
|
879
1816
|
const errText = await pollResp.text();
|
|
880
1817
|
throw new Error(`Poll failed ${pollResp.status}: ${errText}`);
|
|
881
1818
|
}
|
|
882
1819
|
current = (await pollResp.json());
|
|
883
1820
|
}
|
|
884
|
-
if (!current.done)
|
|
1821
|
+
if (!current.done) {
|
|
885
1822
|
throw new Error("Video generation timed out");
|
|
886
|
-
|
|
887
|
-
if (
|
|
1823
|
+
}
|
|
1824
|
+
if (current.error?.message) {
|
|
1825
|
+
// Vertex answers a filtered prompt through the operation's error slot
|
|
1826
|
+
// ("videos were filtered out because they violated ... usage
|
|
1827
|
+
// guidelines"), so the refusal has to be recognized here too.
|
|
1828
|
+
if (isContentFilterRefusal(current.error.message)) {
|
|
1829
|
+
throw new ContentFilterRefusal(current.error.message, {
|
|
1830
|
+
provider: "gemini"
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
throw new Error(`Gemini video generation failed: ${current.error.message}`);
|
|
1834
|
+
}
|
|
1835
|
+
return current;
|
|
1836
|
+
}
|
|
1837
|
+
async downloadGeminiMedia(uri, signal) {
|
|
1838
|
+
const hostname = new URL(uri).hostname;
|
|
1839
|
+
const headers = hostname === "generativelanguage.googleapis.com"
|
|
1840
|
+
? { "x-goog-api-key": this.apiKey }
|
|
1841
|
+
: undefined;
|
|
1842
|
+
const response = await safeFetch(uri, { headers, signal }, 5, this._fetch);
|
|
1843
|
+
if (!response.ok) {
|
|
1844
|
+
throw new Error(`Gemini media download failed: ${response.status}`);
|
|
1845
|
+
}
|
|
1846
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Submit a Veo `predictLongRunning` request, wait for the operation, and
|
|
1850
|
+
* download the first generated video.
|
|
1851
|
+
*/
|
|
1852
|
+
async runVeoOperation(modelId, body, label, options) {
|
|
1853
|
+
const signal = combineSignals(options.signal, options.timeoutSeconds);
|
|
1854
|
+
this.recordRequestPayload(body);
|
|
1855
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/models/${modelId}:predictLongRunning`, {
|
|
1856
|
+
method: "POST",
|
|
1857
|
+
headers: {
|
|
1858
|
+
"Content-Type": "application/json",
|
|
1859
|
+
"x-goog-api-key": this.apiKey
|
|
1860
|
+
},
|
|
1861
|
+
body: JSON.stringify(body),
|
|
1862
|
+
signal
|
|
1863
|
+
});
|
|
1864
|
+
if (!response.ok) {
|
|
1865
|
+
const errText = await response.text();
|
|
1866
|
+
throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
|
|
1867
|
+
}
|
|
1868
|
+
const operation = await this.waitForVideoOperation((await response.json()), options.timeoutSeconds, signal);
|
|
1869
|
+
const videoUri = this.getVideoUri(operation);
|
|
1870
|
+
if (!videoUri) {
|
|
1871
|
+
const refusal = this.videoContentFilterRefusal(operation, modelId);
|
|
1872
|
+
if (refusal)
|
|
1873
|
+
throw refusal;
|
|
888
1874
|
throw new Error("No video URI in response");
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1875
|
+
}
|
|
1876
|
+
return this.downloadGeminiMedia(videoUri, signal);
|
|
1877
|
+
}
|
|
1878
|
+
/** Send one request to the Interactions API and return the finished interaction. */
|
|
1879
|
+
async createInteraction(body, label, modelId, signal) {
|
|
1880
|
+
this.recordRequestPayload(body);
|
|
1881
|
+
const response = await this._fetch(`${GEMINI_API_BASE}/interactions`, {
|
|
1882
|
+
method: "POST",
|
|
1883
|
+
headers: {
|
|
1884
|
+
"Content-Type": "application/json",
|
|
1885
|
+
"x-goog-api-key": this.apiKey
|
|
1886
|
+
},
|
|
1887
|
+
body: JSON.stringify(body),
|
|
1888
|
+
signal
|
|
1889
|
+
});
|
|
1890
|
+
if (!response.ok) {
|
|
1891
|
+
const errText = await response.text();
|
|
1892
|
+
if (isContentFilterRefusal(errText)) {
|
|
1893
|
+
throw new ContentFilterRefusal(errText, {
|
|
1894
|
+
provider: "gemini",
|
|
1895
|
+
model: modelId
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
throw new Error(`Gemini ${label} failed ${response.status}: ${errText}`);
|
|
1899
|
+
}
|
|
1900
|
+
const interaction = (await response.json());
|
|
1901
|
+
if (interaction.status && interaction.status !== "completed") {
|
|
1902
|
+
throw new Error(`Gemini ${label} ended with status ${interaction.status}`);
|
|
1903
|
+
}
|
|
1904
|
+
return interaction;
|
|
1905
|
+
}
|
|
1906
|
+
/** The first `type` output of an interaction, inline or behind a file uri. */
|
|
1907
|
+
async interactionMedia(interaction, type, signal) {
|
|
1908
|
+
for (const step of interaction.steps ?? []) {
|
|
1909
|
+
for (const content of step.content ?? []) {
|
|
1910
|
+
if (content.type !== type)
|
|
1911
|
+
continue;
|
|
1912
|
+
if (content.data) {
|
|
1913
|
+
return {
|
|
1914
|
+
data: new Uint8Array(Buffer.from(content.data, "base64")),
|
|
1915
|
+
mimeType: content.mime_type
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
if (content.uri) {
|
|
1919
|
+
return {
|
|
1920
|
+
data: await this.downloadGeminiMedia(content.uri, signal),
|
|
1921
|
+
mimeType: content.mime_type
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
throw new Error(`Gemini returned no ${type} output`);
|
|
1927
|
+
}
|
|
1928
|
+
/** An Interactions API video block: inline, or uploaded when too large. */
|
|
1929
|
+
async omniVideoInput(video) {
|
|
1930
|
+
const mimeType = sniffVideoMime(video);
|
|
1931
|
+
if (video.length > GEMINI_INLINE_VIDEO_MAX_BYTES) {
|
|
1932
|
+
const uri = await this.uploadFileToGemini(video, mimeType);
|
|
1933
|
+
return { type: "video", uri, mime_type: mimeType };
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
type: "video",
|
|
1937
|
+
data: Buffer.from(video).toString("base64"),
|
|
1938
|
+
mime_type: mimeType
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/** Generate or edit a video with Gemini Omni through the Interactions API. */
|
|
1942
|
+
async runOmniVideo(modelId, input, options) {
|
|
1943
|
+
const responseFormat = { type: "video" };
|
|
1944
|
+
if (options.aspectRatio)
|
|
1945
|
+
responseFormat.aspect_ratio = options.aspectRatio;
|
|
1946
|
+
if (options.resolution)
|
|
1947
|
+
responseFormat.resolution = options.resolution;
|
|
1948
|
+
const signal = combineSignals(options.signal, options.timeoutSeconds);
|
|
1949
|
+
const interaction = await this.createInteraction({ model: modelId, input, response_format: responseFormat }, "video generation", modelId, signal);
|
|
1950
|
+
return (await this.interactionMedia(interaction, "video", signal)).data;
|
|
894
1951
|
}
|
|
895
1952
|
// ---------------------------------------------------------------------------
|
|
896
|
-
//
|
|
1953
|
+
// Text-to-video (Veo: long-running operation; Omni: Interactions API)
|
|
1954
|
+
// ---------------------------------------------------------------------------
|
|
1955
|
+
async textToVideo(params) {
|
|
1956
|
+
if (!params.prompt) {
|
|
1957
|
+
throw new Error("The input prompt cannot be empty.");
|
|
1958
|
+
}
|
|
1959
|
+
const modelId = params.model.id;
|
|
1960
|
+
if (isOmniVideoModel(modelId)) {
|
|
1961
|
+
return this.runOmniVideo(modelId, [{ type: "text", text: params.prompt }], params);
|
|
1962
|
+
}
|
|
1963
|
+
if (!modelId.startsWith("veo-")) {
|
|
1964
|
+
throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for text-to-video.`);
|
|
1965
|
+
}
|
|
1966
|
+
const body = {
|
|
1967
|
+
instances: [{ prompt: params.prompt }]
|
|
1968
|
+
};
|
|
1969
|
+
const parameters = this.buildVideoParameters(params);
|
|
1970
|
+
if (Object.keys(parameters).length > 0) {
|
|
1971
|
+
body.parameters = parameters;
|
|
1972
|
+
}
|
|
1973
|
+
return this.runVeoOperation(modelId, body, "video generation", params);
|
|
1974
|
+
}
|
|
1975
|
+
// ---------------------------------------------------------------------------
|
|
1976
|
+
// Image-to-video
|
|
897
1977
|
// ---------------------------------------------------------------------------
|
|
898
1978
|
async imageToVideo(image, params) {
|
|
899
1979
|
if (!image || image.length === 0) {
|
|
900
1980
|
throw new Error("Input image cannot be empty.");
|
|
901
1981
|
}
|
|
902
1982
|
const modelId = params.model.id;
|
|
1983
|
+
const prompt = params.prompt || "Animate this image";
|
|
1984
|
+
if (isOmniVideoModel(modelId)) {
|
|
1985
|
+
if (params.endImage?.length) {
|
|
1986
|
+
throw new Error(`Gemini model ${modelId} does not accept a last frame`);
|
|
1987
|
+
}
|
|
1988
|
+
return this.runOmniVideo(modelId, [
|
|
1989
|
+
{
|
|
1990
|
+
type: "image",
|
|
1991
|
+
data: Buffer.from(image).toString("base64"),
|
|
1992
|
+
mime_type: detectImageMime(image)
|
|
1993
|
+
},
|
|
1994
|
+
{ type: "text", text: prompt }
|
|
1995
|
+
], params);
|
|
1996
|
+
}
|
|
903
1997
|
if (!modelId.startsWith("veo-")) {
|
|
904
|
-
throw new Error(`Model ${modelId} is not a
|
|
1998
|
+
throw new Error(`Model ${modelId} is not a Gemini video model. Use a Veo or Gemini Omni model for image-to-video.`);
|
|
1999
|
+
}
|
|
2000
|
+
const instance = {
|
|
2001
|
+
prompt,
|
|
2002
|
+
image: veoImage(image)
|
|
2003
|
+
};
|
|
2004
|
+
const parameters = this.buildVideoParameters(params);
|
|
2005
|
+
if (params.endImage?.length) {
|
|
2006
|
+
// Veo interpolates between the two frames only in an 8-second clip.
|
|
2007
|
+
instance.lastFrame = veoImage(params.endImage);
|
|
2008
|
+
parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
|
|
2009
|
+
}
|
|
2010
|
+
const body = { instances: [instance] };
|
|
2011
|
+
if (Object.keys(parameters).length > 0) {
|
|
2012
|
+
body.parameters = parameters;
|
|
2013
|
+
}
|
|
2014
|
+
return this.runVeoOperation(modelId, body, "image-to-video", params);
|
|
2015
|
+
}
|
|
2016
|
+
// ---------------------------------------------------------------------------
|
|
2017
|
+
// Reference-to-video
|
|
2018
|
+
// ---------------------------------------------------------------------------
|
|
2019
|
+
async referenceToVideo(inputs, params) {
|
|
2020
|
+
if (!params.prompt) {
|
|
2021
|
+
throw new Error("The input prompt cannot be empty.");
|
|
2022
|
+
}
|
|
2023
|
+
const modelId = params.model.id;
|
|
2024
|
+
requireVideoTask(modelId, "reference_to_video");
|
|
2025
|
+
const images = inputs.images.filter((bytes) => bytes.length > 0);
|
|
2026
|
+
const videos = inputs.videos.filter((bytes) => bytes.length > 0);
|
|
2027
|
+
if ((inputs.audios ?? []).some((bytes) => bytes.length > 0)) {
|
|
2028
|
+
throw new Error(`Gemini model ${modelId} does not accept reference audio`);
|
|
2029
|
+
}
|
|
2030
|
+
if (images.length === 0 && videos.length === 0) {
|
|
2031
|
+
throw new Error("At least one reference image or video is required");
|
|
2032
|
+
}
|
|
2033
|
+
if (isOmniVideoModel(modelId)) {
|
|
2034
|
+
if (videos.length > OMNI_MAX_REFERENCE_VIDEOS) {
|
|
2035
|
+
throw new Error(`Gemini model ${modelId} accepts at most ${OMNI_MAX_REFERENCE_VIDEOS} reference videos`);
|
|
2036
|
+
}
|
|
2037
|
+
const input = images.map((bytes) => ({
|
|
2038
|
+
type: "image",
|
|
2039
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
2040
|
+
mime_type: detectImageMime(bytes)
|
|
2041
|
+
}));
|
|
2042
|
+
for (const video of videos) {
|
|
2043
|
+
input.push(await this.omniVideoInput(video));
|
|
2044
|
+
}
|
|
2045
|
+
input.push({ type: "text", text: params.prompt });
|
|
2046
|
+
return this.runOmniVideo(modelId, input, params);
|
|
2047
|
+
}
|
|
2048
|
+
if (videos.length > 0) {
|
|
2049
|
+
throw new Error(`Gemini model ${modelId} accepts reference images only`);
|
|
2050
|
+
}
|
|
2051
|
+
if (images.length > VEO_MAX_REFERENCE_IMAGES) {
|
|
2052
|
+
throw new Error(`Gemini model ${modelId} accepts at most ${VEO_MAX_REFERENCE_IMAGES} reference images`);
|
|
905
2053
|
}
|
|
906
|
-
const
|
|
907
|
-
|
|
2054
|
+
const parameters = this.buildVideoParameters(params);
|
|
2055
|
+
parameters.durationSeconds ??= VEO_CONSTRAINED_DURATION_SECONDS;
|
|
908
2056
|
const body = {
|
|
909
2057
|
instances: [
|
|
910
2058
|
{
|
|
911
|
-
prompt,
|
|
912
|
-
|
|
2059
|
+
prompt: params.prompt,
|
|
2060
|
+
referenceImages: images.map((bytes) => ({
|
|
2061
|
+
image: veoImage(bytes),
|
|
2062
|
+
referenceType: "asset"
|
|
2063
|
+
}))
|
|
913
2064
|
}
|
|
914
|
-
]
|
|
2065
|
+
],
|
|
2066
|
+
parameters
|
|
915
2067
|
};
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
2068
|
+
return this.runVeoOperation(modelId, body, "reference-to-video", params);
|
|
2069
|
+
}
|
|
2070
|
+
// ---------------------------------------------------------------------------
|
|
2071
|
+
// Video extension (Veo) and editing (Omni)
|
|
2072
|
+
// ---------------------------------------------------------------------------
|
|
2073
|
+
async extendVideo(video, params) {
|
|
2074
|
+
if (video.length === 0) {
|
|
2075
|
+
throw new Error("The input video is empty.");
|
|
2076
|
+
}
|
|
2077
|
+
const modelId = params.model.id;
|
|
2078
|
+
requireVideoTask(modelId, "extend_video");
|
|
2079
|
+
if (params.mode !== "end") {
|
|
2080
|
+
throw new Error(`Gemini model ${modelId} extends videos at the end only`);
|
|
2081
|
+
}
|
|
2082
|
+
if (params.durationSeconds !== VEO_EXTENSION_SECONDS) {
|
|
2083
|
+
throw new Error(`Gemini model ${modelId} extends a video by exactly ${VEO_EXTENSION_SECONDS} seconds`);
|
|
2084
|
+
}
|
|
2085
|
+
const body = {
|
|
2086
|
+
instances: [
|
|
2087
|
+
{
|
|
2088
|
+
prompt: params.prompt,
|
|
2089
|
+
video: {
|
|
2090
|
+
inlineData: {
|
|
2091
|
+
mimeType: sniffVideoMime(video),
|
|
2092
|
+
data: Buffer.from(video).toString("base64")
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
],
|
|
2097
|
+
// Veo extends 720p input only and returns the input plus the new seconds.
|
|
2098
|
+
parameters: { numberOfVideos: 1, resolution: "720p" }
|
|
2099
|
+
};
|
|
2100
|
+
return this.runVeoOperation(modelId, body, "video extension", {
|
|
2101
|
+
signal: params.signal
|
|
930
2102
|
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
2103
|
+
}
|
|
2104
|
+
async videoToVideo(video, params) {
|
|
2105
|
+
if (video.length === 0) {
|
|
2106
|
+
throw new Error("The input video is empty.");
|
|
934
2107
|
}
|
|
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());
|
|
2108
|
+
if (!params.prompt) {
|
|
2109
|
+
throw new Error("The edit instruction cannot be empty.");
|
|
953
2110
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
2111
|
+
const modelId = params.model.id;
|
|
2112
|
+
requireVideoTask(modelId, "video_to_video");
|
|
2113
|
+
const input = [await this.omniVideoInput(video)];
|
|
2114
|
+
for (const bytes of params.referenceImages ?? []) {
|
|
2115
|
+
if (bytes.length === 0)
|
|
2116
|
+
continue;
|
|
2117
|
+
input.push({
|
|
2118
|
+
type: "image",
|
|
2119
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
2120
|
+
mime_type: detectImageMime(bytes)
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
input.push({ type: "text", text: params.prompt });
|
|
2124
|
+
return this.runOmniVideo(modelId, input, params);
|
|
2125
|
+
}
|
|
2126
|
+
// ---------------------------------------------------------------------------
|
|
2127
|
+
// Text-to-music (Lyria, Interactions API)
|
|
2128
|
+
// ---------------------------------------------------------------------------
|
|
2129
|
+
async textToMusic(params) {
|
|
2130
|
+
if (!params.prompt) {
|
|
2131
|
+
throw new Error("The input prompt cannot be empty.");
|
|
2132
|
+
}
|
|
2133
|
+
const modelId = params.model.id;
|
|
2134
|
+
if (!modelId.startsWith("lyria-")) {
|
|
2135
|
+
throw new Error(`Model ${modelId} is not a Lyria model. Use a Lyria model for text-to-music.`);
|
|
2136
|
+
}
|
|
2137
|
+
// Lyria takes length and lyrics from the prompt; it has no fields for them.
|
|
2138
|
+
const input = [
|
|
2139
|
+
params.prompt,
|
|
2140
|
+
params.durationSeconds
|
|
2141
|
+
? `Length: about ${Math.round(params.durationSeconds)} seconds.`
|
|
2142
|
+
: undefined,
|
|
2143
|
+
params.lyrics ? `Lyrics:\n${params.lyrics}` : undefined
|
|
2144
|
+
]
|
|
2145
|
+
.filter((part) => part !== undefined)
|
|
2146
|
+
.join("\n\n");
|
|
2147
|
+
const body = { model: modelId, input };
|
|
2148
|
+
// Lyria answers MP3 by default. Only Lyria 3.5 returns WAV, on request.
|
|
2149
|
+
if (params.audioFormat === "wav" && modelId === "lyria-3.5") {
|
|
2150
|
+
body.response_format = { type: "audio" };
|
|
2151
|
+
}
|
|
2152
|
+
const signal = combineSignals(undefined, params.timeoutSeconds);
|
|
2153
|
+
const interaction = await this.createInteraction(body, "music generation", modelId, signal);
|
|
2154
|
+
const audio = await this.interactionMedia(interaction, "audio", signal);
|
|
2155
|
+
return {
|
|
2156
|
+
data: audio.data,
|
|
2157
|
+
mimeType: audio.mimeType ?? sniffAudioMime(audio.data)
|
|
2158
|
+
};
|
|
963
2159
|
}
|
|
964
2160
|
// ---------------------------------------------------------------------------
|
|
965
2161
|
// Error detection
|
|
966
2162
|
// ---------------------------------------------------------------------------
|
|
2163
|
+
isContextExceededError(error) {
|
|
2164
|
+
return geminiContextExceeded(error) !== null;
|
|
2165
|
+
}
|
|
967
2166
|
isContextLengthError(error) {
|
|
968
2167
|
const msg = String(error).toLowerCase();
|
|
969
2168
|
return (msg.includes("context length") ||
|