@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
|
@@ -2,33 +2,154 @@
|
|
|
2
2
|
* FAL AI Provider — wraps the @fal-ai/client SDK to provide image generation
|
|
3
3
|
* through the standard BaseProvider interface.
|
|
4
4
|
*
|
|
5
|
-
* Supports: textToImage, imageToImage, getAvailableImageModels
|
|
5
|
+
* Supports: textToImage, imageToImage, getAvailableImageModels, and chat
|
|
6
|
+
* against fal's OpenAI-compatible LLM route.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { currentGenerationProviderRequestOptions, recordGenerationBindingAsync, recordGenerationProviderResult, recordGenerationReceiptAsync } from "../generation-receipt.js";
|
|
8
9
|
import { BaseProvider } from "./base-provider.js";
|
|
10
|
+
import { falGetGeneration, falListGenerations } from "./fal-generations.js";
|
|
9
11
|
import { createLogger } from "@nodetool-ai/config";
|
|
10
|
-
import {
|
|
12
|
+
import { OpenAICompatProvider } from "./openai-compat-provider.js";
|
|
13
|
+
import { loadImageModels, loadManifest, loadMusicModels, loadAudioToAudioModels, loadTTSModels, loadVideoModels, selectPrimaryImageInput, sizeEnumToAspect } from "./manifest-models.js";
|
|
14
|
+
import { validateReferenceInputs, getModelReferenceInputs } from "./manifest-models.js";
|
|
15
|
+
import { sniffAudioMime } from "./audio-mime.js";
|
|
16
|
+
import { snapToGptImage2Size } from "./gpt-image-size.js";
|
|
17
|
+
import { detectImageMime } from "./image-mime.js";
|
|
18
|
+
import { sniffMediaMime } from "./media-mime.js";
|
|
19
|
+
import { safeFetch } from "./safe-url.js";
|
|
20
|
+
import { createFalQueueOperations, falSubmitAndWait } from "./fal-queue.js";
|
|
21
|
+
import { isNonEmptyString, isObjectLike, isRecord, isString } from "@nodetool-ai/protocol";
|
|
11
22
|
const log = createLogger("nodetool.runtime.providers.fal");
|
|
23
|
+
function combineSignals(signal, timeoutSeconds) {
|
|
24
|
+
const timeout = timeoutSeconds && timeoutSeconds > 0
|
|
25
|
+
? AbortSignal.timeout(timeoutSeconds * 1000)
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!signal)
|
|
28
|
+
return timeout;
|
|
29
|
+
if (!timeout)
|
|
30
|
+
return signal;
|
|
31
|
+
return AbortSignal.any([signal, timeout]);
|
|
32
|
+
}
|
|
12
33
|
const FAL_MANIFEST_PKG = "@nodetool-ai/fal-nodes";
|
|
13
34
|
const FAL_MANIFEST_PATH = "fal-manifest.json";
|
|
35
|
+
/** fal's OpenAI-compatible chat route (`openrouter/router`). */
|
|
36
|
+
const FAL_LLM_BASE_URL = "https://fal.run/openrouter/router/openai/v1";
|
|
37
|
+
/** Public, keyless catalog of the ids that route accepts. */
|
|
38
|
+
const OPENROUTER_CATALOG_URL = "https://openrouter.ai/api/v1/models";
|
|
14
39
|
let _falManifestByEndpoint = null;
|
|
15
40
|
function getFalManifestEntry(modelId) {
|
|
16
41
|
if (!_falManifestByEndpoint) {
|
|
17
42
|
_falManifestByEndpoint = new Map();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (entry.endpointId)
|
|
23
|
-
_falManifestByEndpoint.set(entry.endpointId, entry);
|
|
43
|
+
const data = loadManifest(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH);
|
|
44
|
+
for (const entry of data) {
|
|
45
|
+
if (entry.endpointId) {
|
|
46
|
+
_falManifestByEndpoint.set(entry.endpointId, entry);
|
|
24
47
|
}
|
|
25
48
|
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
log.warn(`Could not load fal manifest for arg shaping: ${err}`);
|
|
28
|
-
}
|
|
29
49
|
}
|
|
30
50
|
return _falManifestByEndpoint.get(modelId);
|
|
31
51
|
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Topaz model variants
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
/**
|
|
56
|
+
* fal ships Topaz as one endpoint per media kind and selects the actual Topaz
|
|
57
|
+
* model — Standard V2, Redefine, Wonder 3 for images; Proteus, Artemis, Gaia,
|
|
58
|
+
* Nyx, Starlight for video — through a `model` enum on that endpoint. A single
|
|
59
|
+
* catalog entry per endpoint therefore offers no way to pick one, and every
|
|
60
|
+
* run silently gets fal's default (Standard V2 / Proteus), which is the wrong
|
|
61
|
+
* model for compressed sources, CGI, animation, or generative restoration.
|
|
62
|
+
*
|
|
63
|
+
* So each endpoint is expanded into one catalog entry per variant, id
|
|
64
|
+
* `<endpointId>/<variant>`, and the variant is split back off and sent as
|
|
65
|
+
* `model` when the request is built — the same shape TopazProvider uses for
|
|
66
|
+
* Topaz's own API.
|
|
67
|
+
*/
|
|
68
|
+
const TOPAZ_UPSCALE_IMAGE = "fal-ai/topaz/upscale/image";
|
|
69
|
+
const TOPAZ_UPSCALE_VIDEO = "fal-ai/topaz/upscale/video";
|
|
70
|
+
const TOPAZ_VARIANT_ENDPOINTS = [TOPAZ_UPSCALE_IMAGE, TOPAZ_UPSCALE_VIDEO];
|
|
71
|
+
/** The `model` enum a Topaz endpoint declares, or [] when it isn't shipped. */
|
|
72
|
+
function topazVariants(endpointId) {
|
|
73
|
+
const field = getFalManifestEntry(endpointId)?.inputFields?.find((f) => (f.apiParamName ?? f.name) === "model");
|
|
74
|
+
return field?.enumValues ?? [];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Replace each Topaz entry with one entry per model variant. Entries whose
|
|
78
|
+
* endpoint declares no `model` enum are left as they are, so a manifest that
|
|
79
|
+
* drops the field degrades to today's single entry rather than to none.
|
|
80
|
+
*/
|
|
81
|
+
function expandTopazVariants(models) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const m of models) {
|
|
84
|
+
const variants = TOPAZ_VARIANT_ENDPOINTS.includes(m.id)
|
|
85
|
+
? topazVariants(m.id)
|
|
86
|
+
: [];
|
|
87
|
+
if (variants.length === 0) {
|
|
88
|
+
out.push(m);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const variant of variants) {
|
|
92
|
+
out.push({
|
|
93
|
+
...m,
|
|
94
|
+
id: `${m.id}/${variant}`,
|
|
95
|
+
name: `${m.name} — ${variant}`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Split a catalog id back into the fal endpoint and the Topaz `model` value.
|
|
103
|
+
* Bare endpoint ids (saved before variants existed, or typed by hand) resolve
|
|
104
|
+
* to no variant, which leaves fal's own default in place.
|
|
105
|
+
*/
|
|
106
|
+
function splitTopazVariant(modelId) {
|
|
107
|
+
for (const endpointId of TOPAZ_VARIANT_ENDPOINTS) {
|
|
108
|
+
const prefix = `${endpointId}/`;
|
|
109
|
+
if (modelId.startsWith(prefix)) {
|
|
110
|
+
return { endpointId, variant: modelId.slice(prefix.length) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { endpointId: modelId };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* `UpscaleImageParams.creativity` is 0–1; the Topaz image endpoint takes a
|
|
117
|
+
* 1–6 level (Redefine only) and rejects anything else, so it is rescaled for
|
|
118
|
+
* that endpoint and passed through untouched everywhere else.
|
|
119
|
+
*/
|
|
120
|
+
function topazCreativity(endpointId, creativity) {
|
|
121
|
+
if (endpointId !== TOPAZ_UPSCALE_IMAGE || creativity == null) {
|
|
122
|
+
return creativity;
|
|
123
|
+
}
|
|
124
|
+
const level = Math.round(Math.min(1, Math.max(0, creativity)) * 5) + 1;
|
|
125
|
+
return String(level);
|
|
126
|
+
}
|
|
127
|
+
/** The four per-side outpaint API names, in the order fal declares them. */
|
|
128
|
+
const EXPAND_SIDES = [
|
|
129
|
+
["left", "expand_left"],
|
|
130
|
+
["right", "expand_right"],
|
|
131
|
+
["top", "expand_top"],
|
|
132
|
+
["bottom", "expand_bottom"]
|
|
133
|
+
];
|
|
134
|
+
/**
|
|
135
|
+
* Write the padding onto whatever `expand_*` fields the endpoint declares.
|
|
136
|
+
*
|
|
137
|
+
* The two families spell the same request differently: image endpoints take
|
|
138
|
+
* pixel counts (`expand_top: 256`), while the wan-vace video ones take a
|
|
139
|
+
* boolean per side plus one shared `expand_ratio`. A pixel count sent to the
|
|
140
|
+
* boolean field is a 422, so the declared type decides — a positive count
|
|
141
|
+
* becomes `true` there, and the ratio rides separately.
|
|
142
|
+
*/
|
|
143
|
+
function setOutpaintPadding(b, padding) {
|
|
144
|
+
if (!padding)
|
|
145
|
+
return;
|
|
146
|
+
for (const [side, apiName] of EXPAND_SIDES) {
|
|
147
|
+
const pixels = padding[side];
|
|
148
|
+
if (pixels == null)
|
|
149
|
+
continue;
|
|
150
|
+
b.set(apiName, b.propType(apiName) === "bool" ? pixels > 0 : pixels);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
32
153
|
/**
|
|
33
154
|
* Helper bound to one endpoint that adds keys only when the manifest declares
|
|
34
155
|
* them, and attaches asset URLs to whatever field name/shape the endpoint
|
|
@@ -37,10 +158,12 @@ function getFalManifestEntry(modelId) {
|
|
|
37
158
|
*/
|
|
38
159
|
class FalArgsBuilder {
|
|
39
160
|
args = {};
|
|
161
|
+
modelId;
|
|
40
162
|
fields;
|
|
41
163
|
accepted;
|
|
42
164
|
known;
|
|
43
165
|
constructor(modelId) {
|
|
166
|
+
this.modelId = modelId;
|
|
44
167
|
const entry = getFalManifestEntry(modelId);
|
|
45
168
|
this.known = entry != null;
|
|
46
169
|
this.fields = entry?.inputFields ?? [];
|
|
@@ -50,35 +173,201 @@ class FalArgsBuilder {
|
|
|
50
173
|
has(apiName) {
|
|
51
174
|
return this.known ? this.accepted.has(apiName) : true;
|
|
52
175
|
}
|
|
176
|
+
/** True only when the manifest describes this endpoint and lists `apiName`. */
|
|
177
|
+
declares(apiName) {
|
|
178
|
+
return this.known && this.accepted.has(apiName);
|
|
179
|
+
}
|
|
53
180
|
/** propType for a manifest field by API name, lowercased. */
|
|
54
181
|
propType(apiName) {
|
|
55
182
|
return this.accepted.get(apiName)?.propType.toLowerCase();
|
|
56
183
|
}
|
|
57
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Set `apiName` to `value` only if the endpoint accepts it (or unknown),
|
|
186
|
+
* coercing to the manifest's declared type. fal returns 422 when a value's
|
|
187
|
+
* type doesn't match the field — notably numeric `duration`/`seed`/
|
|
188
|
+
* `num_frames` sent to fields the endpoint declares as `enum`/`str` (e.g.
|
|
189
|
+
* `duration` enums are `"5"`/`"10"`, not `5`). Enum values that aren't in
|
|
190
|
+
* the field's vocabulary are dropped rather than sent.
|
|
191
|
+
*/
|
|
58
192
|
set(apiName, value) {
|
|
59
193
|
if (value == null)
|
|
60
194
|
return this;
|
|
61
|
-
if (
|
|
195
|
+
if (isString(value) && value === "")
|
|
62
196
|
return this;
|
|
63
|
-
if (this.has(apiName))
|
|
64
|
-
this
|
|
197
|
+
if (!this.has(apiName))
|
|
198
|
+
return this;
|
|
199
|
+
const t = this.propType(apiName);
|
|
200
|
+
if (t === "enum") {
|
|
201
|
+
const v = this.acceptedEnumValue(apiName, String(value));
|
|
202
|
+
if (v !== undefined)
|
|
203
|
+
this.args[apiName] = v;
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
if (t === "str" && !isString(value)) {
|
|
207
|
+
this.args[apiName] = String(value);
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
if (t === "int" || t === "float") {
|
|
211
|
+
const n = this.numericValue(apiName, value, t === "int");
|
|
212
|
+
if (n !== undefined)
|
|
213
|
+
this.args[apiName] = n;
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
this.args[apiName] = value;
|
|
65
217
|
return this;
|
|
66
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Coerce a numeric field to what the endpoint declares: integers are
|
|
221
|
+
* rounded, and both kinds are clamped to the field's `min`/`max`. fal
|
|
222
|
+
* returns 422 for a value outside the declared range or a float sent to an
|
|
223
|
+
* `int` field — a 1.5 s storyboard shot sent as `duration` to an endpoint
|
|
224
|
+
* declaring `int` 5..15 fails the whole render. Clamping produces the
|
|
225
|
+
* nearest clip the model can make instead. Non-numeric values are dropped.
|
|
226
|
+
*/
|
|
227
|
+
numericValue(apiName, value, isInt) {
|
|
228
|
+
const raw = isString(value) ? Number(value) : value;
|
|
229
|
+
if (typeof raw !== "number" || !Number.isFinite(raw))
|
|
230
|
+
return undefined;
|
|
231
|
+
const field = this.accepted.get(apiName);
|
|
232
|
+
let n = isInt ? Math.round(raw) : raw;
|
|
233
|
+
if (field) {
|
|
234
|
+
if (typeof field.min === "number" && n < field.min)
|
|
235
|
+
n = field.min;
|
|
236
|
+
if (typeof field.max === "number" && n > field.max)
|
|
237
|
+
n = field.max;
|
|
238
|
+
if (isInt)
|
|
239
|
+
n = Math.round(n);
|
|
240
|
+
}
|
|
241
|
+
return n;
|
|
242
|
+
}
|
|
67
243
|
/** Same as set() but always writes (used for required canonical keys like prompt). */
|
|
68
244
|
force(apiName, value) {
|
|
69
245
|
if (value != null)
|
|
70
246
|
this.args[apiName] = value;
|
|
71
247
|
return this;
|
|
72
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Write `value` to the first name in `apiNames` the endpoint declares. Used
|
|
251
|
+
* where one concept has many spellings across endpoints (a segmentation text
|
|
252
|
+
* prompt is `prompt`, `text_prompt`, `text_input` or `object`), so writing a
|
|
253
|
+
* single name would drop the value on every endpoint using another.
|
|
254
|
+
*/
|
|
255
|
+
setFirst(apiNames, value) {
|
|
256
|
+
for (const apiName of apiNames) {
|
|
257
|
+
if (this.has(apiName)) {
|
|
258
|
+
return this.set(apiName, value);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// The endpoint declares none of them — an unknown key would 422 the call,
|
|
262
|
+
// and the model simply does not take this prompt kind.
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Write `value` to the first name in `apiNames` the endpoint declares, even
|
|
267
|
+
* when {@link set} would drop it. An empty string is a meaningful value on a
|
|
268
|
+
* segmentation prompt field: fal fills an absent field with the endpoint's
|
|
269
|
+
* own default, and sam-3.1 defaults `prompt` to `"wheel"`.
|
|
270
|
+
*/
|
|
271
|
+
forceFirst(apiNames, value) {
|
|
272
|
+
for (const apiName of apiNames) {
|
|
273
|
+
if (this.has(apiName)) {
|
|
274
|
+
return this.force(apiName, value);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The `kind`-typed asset inputs the endpoint declares, as {@link
|
|
281
|
+
* ModelImageInput}s (apiName / isList / name), in manifest order.
|
|
282
|
+
*/
|
|
283
|
+
assetInputs(kind) {
|
|
284
|
+
return this.fields
|
|
285
|
+
.filter((f) => {
|
|
286
|
+
const t = f.propType.toLowerCase();
|
|
287
|
+
return t === kind || t === `list[${kind}]`;
|
|
288
|
+
})
|
|
289
|
+
.map((f) => ({
|
|
290
|
+
apiName: f.apiParamName ?? f.name,
|
|
291
|
+
isList: f.propType.toLowerCase().startsWith("list["),
|
|
292
|
+
name: f.name
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pick the field a source asset should attach to. For images this is the
|
|
297
|
+
* primary source field, skipping auxiliary slots (mask/control/reference/
|
|
298
|
+
* style/pose/end-frame, …) that must never receive the source image — the
|
|
299
|
+
* same {@link selectPrimaryImageInput} the model picker uses. For video/audio
|
|
300
|
+
* (endpoints with a single such field) it's the first declared field.
|
|
301
|
+
*/
|
|
302
|
+
selectAssetField(kind, count) {
|
|
303
|
+
const inputs = this.assetInputs(kind);
|
|
304
|
+
if (inputs.length === 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
if (kind === "image")
|
|
307
|
+
return selectPrimaryImageInput(inputs, count);
|
|
308
|
+
return inputs[0];
|
|
309
|
+
}
|
|
73
310
|
/**
|
|
74
311
|
* Attach an uploaded asset URL to whatever field (image/video/audio) the
|
|
75
312
|
* endpoint declares. Handles list-typed fields (`list[image]` -> array).
|
|
76
313
|
* If the endpoint isn't in the manifest, falls back to `${kind}_url`.
|
|
77
314
|
*/
|
|
78
315
|
attachAsset(kind, url, fallbackApiName) {
|
|
316
|
+
const field = this.selectAssetField(kind, 1);
|
|
317
|
+
if (field) {
|
|
318
|
+
this.args[field.apiName] = field.isList ? [url] : url;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
this.args[fallbackApiName ?? `${kind}_url`] = url;
|
|
322
|
+
}
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Attach one or more uploaded asset URLs to whatever field (image/video/
|
|
327
|
+
* audio) the endpoint declares. When the endpoint declares a list-typed field
|
|
328
|
+
* (e.g. `image_urls`), every URL is sent; otherwise only the first URL is
|
|
329
|
+
* attached to the single-valued field. Auxiliary image slots (mask, control,
|
|
330
|
+
* reference/style/end-frame, …) are skipped so the source never lands there.
|
|
331
|
+
* Falls back to `${kind}_url` (first URL) for endpoints not in the manifest.
|
|
332
|
+
*/
|
|
333
|
+
attachAssets(kind, urls, fallbackApiName) {
|
|
334
|
+
if (urls.length === 0)
|
|
335
|
+
return this;
|
|
336
|
+
const field = this.selectAssetField(kind, urls.length);
|
|
337
|
+
if (field) {
|
|
338
|
+
this.args[field.apiName] = field.isList ? urls : urls[0];
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.args[fallbackApiName ?? `${kind}_url`] = urls[0];
|
|
342
|
+
}
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
referenceAssetInputs(kind) {
|
|
346
|
+
return getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, this.modelId)
|
|
347
|
+
.filter((field) => field.kind === kind)
|
|
348
|
+
.map(({ kind: _kind, ...field }) => field);
|
|
349
|
+
}
|
|
350
|
+
attachReferenceAssets(kind, urls) {
|
|
351
|
+
if (urls.length === 0)
|
|
352
|
+
return this;
|
|
353
|
+
const fields = this.referenceAssetInputs(kind);
|
|
354
|
+
const field = fields[0];
|
|
355
|
+
if (!field)
|
|
356
|
+
return this;
|
|
357
|
+
this.args[field.apiName] = field.isList ? urls : urls[0];
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Attach an inpaint mask URL to whatever mask field the endpoint declares.
|
|
362
|
+
* Endpoints name it `mask_url`, `mask_image_url`, `static_mask_url`, etc. —
|
|
363
|
+
* all `image`-typed fields whose name contains "mask". Falls back to
|
|
364
|
+
* `mask_url` for endpoints not in the manifest.
|
|
365
|
+
*/
|
|
366
|
+
attachMask(url) {
|
|
79
367
|
const field = this.fields.find((f) => {
|
|
80
368
|
const t = f.propType.toLowerCase();
|
|
81
|
-
|
|
369
|
+
const apiName = (f.apiParamName ?? f.name).toLowerCase();
|
|
370
|
+
return (t === "image" || t === "list[image]") && apiName.includes("mask");
|
|
82
371
|
});
|
|
83
372
|
if (field) {
|
|
84
373
|
const apiName = field.apiParamName ?? field.name;
|
|
@@ -87,7 +376,7 @@ class FalArgsBuilder {
|
|
|
87
376
|
: url;
|
|
88
377
|
}
|
|
89
378
|
else {
|
|
90
|
-
this.args
|
|
379
|
+
this.args.mask_url = url;
|
|
91
380
|
}
|
|
92
381
|
return this;
|
|
93
382
|
}
|
|
@@ -99,11 +388,18 @@ class FalArgsBuilder {
|
|
|
99
388
|
setImageSize(width, height) {
|
|
100
389
|
if (!width || !height)
|
|
101
390
|
return this;
|
|
391
|
+
if (this.modelId.startsWith("openai/gpt-image-2.5/")) {
|
|
392
|
+
const size = snapToGptImage2Size(width, height);
|
|
393
|
+
if (size && this.field("image_size")?.acceptsObject) {
|
|
394
|
+
this.args.image_size = { width: size[0], height: size[1] };
|
|
395
|
+
}
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
102
398
|
const t = this.propType("image_size");
|
|
103
399
|
if (!this.known) {
|
|
104
400
|
this.args.image_size = { width, height };
|
|
105
401
|
}
|
|
106
|
-
else if (t && t !== "enum") {
|
|
402
|
+
else if (t && (t !== "enum" || this.field("image_size")?.acceptsObject)) {
|
|
107
403
|
this.args.image_size = { width, height };
|
|
108
404
|
}
|
|
109
405
|
return this;
|
|
@@ -124,12 +420,21 @@ class FalArgsBuilder {
|
|
|
124
420
|
return undefined;
|
|
125
421
|
if (field.propType.toLowerCase() !== "enum")
|
|
126
422
|
return value;
|
|
127
|
-
const enumValues = field
|
|
128
|
-
.enumValues;
|
|
423
|
+
const enumValues = field.enumValues;
|
|
129
424
|
if (!enumValues || enumValues.length === 0)
|
|
130
425
|
return value;
|
|
131
426
|
return enumValues.includes(value) ? value : undefined;
|
|
132
427
|
}
|
|
428
|
+
field(apiName) {
|
|
429
|
+
return this.accepted.get(apiName);
|
|
430
|
+
}
|
|
431
|
+
/** Declared enum vocabulary for an enum-typed field, or undefined. */
|
|
432
|
+
enumValuesOf(apiName) {
|
|
433
|
+
const field = this.accepted.get(apiName);
|
|
434
|
+
if (!field || field.propType.toLowerCase() !== "enum")
|
|
435
|
+
return undefined;
|
|
436
|
+
return field.enumValues;
|
|
437
|
+
}
|
|
133
438
|
/**
|
|
134
439
|
* Route an aspect ratio + resolution pair to whichever of the endpoint's
|
|
135
440
|
* size-shaping enum fields it actually declares: `aspect_ratio`,
|
|
@@ -146,16 +451,21 @@ class FalArgsBuilder {
|
|
|
146
451
|
const res = this.acceptedEnumValue("resolution", resolution);
|
|
147
452
|
if (res)
|
|
148
453
|
this.args.resolution = res;
|
|
149
|
-
// `video_size` / `image_size` enums
|
|
150
|
-
// (square_hd,
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
454
|
+
// `video_size` / `image_size` are enums whose vocabulary varies per endpoint
|
|
455
|
+
// (square_hd, landscape_16_9, landscape_3_2, 1536x1024, ...). Pick the value
|
|
456
|
+
// the endpoint ITSELF declares whose ratio matches `aspectRatio`, using the
|
|
457
|
+
// same size→aspect map the model picker derives its options from — so any
|
|
458
|
+
// ratio the picker offers round-trips to a value fal accepts. Unknown
|
|
459
|
+
// endpoints (no declared enum) fall back to the canonical short list.
|
|
154
460
|
for (const apiName of ["video_size", "image_size"]) {
|
|
155
461
|
// Only set if not already set (e.g. setImageSize() may have written
|
|
156
462
|
// image_size as a dict for endpoints that accept that shape).
|
|
157
463
|
if (apiName in this.args)
|
|
158
464
|
continue;
|
|
465
|
+
const declared = this.enumValuesOf(apiName);
|
|
466
|
+
const sized = declared
|
|
467
|
+
? pickSizeEnumForAspect(declared, aspectRatio)
|
|
468
|
+
: aspectRatioToSizeEnum(aspectRatio);
|
|
159
469
|
const v = this.acceptedEnumValue(apiName, sized);
|
|
160
470
|
if (v)
|
|
161
471
|
this.args[apiName] = v;
|
|
@@ -188,15 +498,81 @@ function aspectRatioToSizeEnum(aspectRatio) {
|
|
|
188
498
|
return undefined;
|
|
189
499
|
}
|
|
190
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Given the size-enum values an endpoint declares, pick the one whose aspect
|
|
503
|
+
* ratio matches `aspectRatio` (via {@link sizeEnumToAspect} — the same map the
|
|
504
|
+
* model picker uses to build its option list, so extraction and request-shaping
|
|
505
|
+
* agree). When several declared values share the ratio (e.g. `square` and
|
|
506
|
+
* `square_hd`), prefer the higher-resolution variant. Returns undefined when
|
|
507
|
+
* the endpoint declares no value for that ratio.
|
|
508
|
+
*/
|
|
509
|
+
function pickSizeEnumForAspect(declared, aspectRatio) {
|
|
510
|
+
if (!aspectRatio)
|
|
511
|
+
return undefined;
|
|
512
|
+
const matches = declared.filter((v) => sizeEnumToAspect(v) === aspectRatio);
|
|
513
|
+
if (matches.length === 0)
|
|
514
|
+
return undefined;
|
|
515
|
+
return matches.sort((a, b) => sizeEnumRank(b) - sizeEnumRank(a))[0];
|
|
516
|
+
}
|
|
517
|
+
/** Rough quality ordering so "square_hd" beats "square", "…_uhd" beats both. */
|
|
518
|
+
function sizeEnumRank(value) {
|
|
519
|
+
const v = value.toLowerCase();
|
|
520
|
+
if (v.includes("uhd"))
|
|
521
|
+
return 3;
|
|
522
|
+
if (v.includes("hd"))
|
|
523
|
+
return 2;
|
|
524
|
+
const dims = v.match(/(\d+)x(\d+)/);
|
|
525
|
+
if (dims)
|
|
526
|
+
return 1 + (Number(dims[1]) * Number(dims[2])) / 1e9;
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
191
529
|
export class FalProvider extends BaseProvider {
|
|
192
530
|
apiKey;
|
|
193
531
|
_client = null;
|
|
532
|
+
_chat = null;
|
|
533
|
+
_fetch;
|
|
534
|
+
_webhookUrl;
|
|
535
|
+
_onQueueAccepted;
|
|
536
|
+
_onQueueBound;
|
|
537
|
+
/** Model ids the catalog says accept `tools`. Filled by the model listing. */
|
|
538
|
+
_toolCapableModels = null;
|
|
539
|
+
declaredCapabilities() {
|
|
540
|
+
return [
|
|
541
|
+
"text_to_image",
|
|
542
|
+
"image_to_image",
|
|
543
|
+
"text_to_video",
|
|
544
|
+
"image_to_video",
|
|
545
|
+
"reference_to_video",
|
|
546
|
+
"inpainting",
|
|
547
|
+
"outpaint_image",
|
|
548
|
+
"upscale_image",
|
|
549
|
+
"remove_background",
|
|
550
|
+
"relight_image",
|
|
551
|
+
"segment_image",
|
|
552
|
+
"vectorize_image",
|
|
553
|
+
"video_to_video",
|
|
554
|
+
"extend_video",
|
|
555
|
+
"upscale_video",
|
|
556
|
+
"interpolate_video",
|
|
557
|
+
"outpaint_video",
|
|
558
|
+
"lip_sync",
|
|
559
|
+
"text_to_speech",
|
|
560
|
+
"text_to_music",
|
|
561
|
+
"audio_to_audio",
|
|
562
|
+
"list_generations",
|
|
563
|
+
"get_generation"
|
|
564
|
+
];
|
|
565
|
+
}
|
|
194
566
|
static requiredSecrets() {
|
|
195
567
|
return ["FAL_API_KEY"];
|
|
196
568
|
}
|
|
197
|
-
constructor(secrets = {}) {
|
|
569
|
+
constructor(secrets = {}, options = {}) {
|
|
198
570
|
super("fal_ai");
|
|
199
571
|
this.apiKey = secrets["FAL_API_KEY"] ?? "";
|
|
572
|
+
this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
573
|
+
this._webhookUrl = options.webhookUrl;
|
|
574
|
+
this._onQueueAccepted = options.onQueueAccepted;
|
|
575
|
+
this._onQueueBound = options.onQueueBound;
|
|
200
576
|
}
|
|
201
577
|
/** Build an onQueueUpdate callback that forwards progress via emitMessage. */
|
|
202
578
|
makeQueueUpdateHandler() {
|
|
@@ -233,27 +609,324 @@ export class FalProvider extends BaseProvider {
|
|
|
233
609
|
if (this._client)
|
|
234
610
|
return this._client;
|
|
235
611
|
const { createFalClient } = await import("@fal-ai/client");
|
|
236
|
-
|
|
612
|
+
const client = createFalClient({
|
|
237
613
|
credentials: this.apiKey
|
|
238
614
|
});
|
|
615
|
+
// The SDK's subscribe() hides submit/bind/wait and enables automatic POST
|
|
616
|
+
// retries. Use the explicit adapter when this is a queue-capable SDK. The
|
|
617
|
+
// fallback keeps injected legacy clients and older hosts compatible.
|
|
618
|
+
const queueCapable = typeof client === "object" &&
|
|
619
|
+
client !== null &&
|
|
620
|
+
"queue" in client &&
|
|
621
|
+
typeof client.queue === "object" &&
|
|
622
|
+
client.queue !== null;
|
|
623
|
+
this._client = {
|
|
624
|
+
subscribe: async (endpoint, opts) => {
|
|
625
|
+
if (!queueCapable) {
|
|
626
|
+
const result = await client.subscribe(endpoint, opts);
|
|
627
|
+
recordGenerationProviderResult(result.data ?? result);
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
const operations = createFalQueueOperations({
|
|
631
|
+
apiKey: this.apiKey,
|
|
632
|
+
fetchFn: this._fetch
|
|
633
|
+
});
|
|
634
|
+
const request = {
|
|
635
|
+
endpoint,
|
|
636
|
+
input: opts.input
|
|
637
|
+
};
|
|
638
|
+
const providerRequestOptions = currentGenerationProviderRequestOptions();
|
|
639
|
+
if (opts.abortSignal)
|
|
640
|
+
request.signal = opts.abortSignal;
|
|
641
|
+
const webhookUrl = this._webhookUrl ?? providerRequestOptions?.webhookUrl;
|
|
642
|
+
if (webhookUrl)
|
|
643
|
+
request.webhookUrl = webhookUrl;
|
|
644
|
+
const result = await falSubmitAndWait(operations, request, {
|
|
645
|
+
signal: opts.abortSignal,
|
|
646
|
+
onAccepted: (submission) => {
|
|
647
|
+
// The id is recorded immediately after the non-retried POST and
|
|
648
|
+
// before any status/result wait, so a later wait timeout preserves
|
|
649
|
+
// the provider identity for a host lifecycle adapter.
|
|
650
|
+
return (async () => {
|
|
651
|
+
await recordGenerationReceiptAsync({ provider_request_id: submission.providerRequestId }, submission);
|
|
652
|
+
await this._onQueueAccepted?.(submission);
|
|
653
|
+
})();
|
|
654
|
+
},
|
|
655
|
+
onBound: async (binding) => {
|
|
656
|
+
await recordGenerationBindingAsync(binding);
|
|
657
|
+
await this._onQueueBound?.(binding);
|
|
658
|
+
},
|
|
659
|
+
onUpdate: (observation) => {
|
|
660
|
+
const status = typeof observation.rawStatus === "object" &&
|
|
661
|
+
observation.rawStatus !== null &&
|
|
662
|
+
"status" in observation.rawStatus &&
|
|
663
|
+
typeof observation.rawStatus.status === "string"
|
|
664
|
+
? observation.rawStatus.status
|
|
665
|
+
: observation.state === "queued"
|
|
666
|
+
? "IN_QUEUE"
|
|
667
|
+
: observation.state === "running"
|
|
668
|
+
? "IN_PROGRESS"
|
|
669
|
+
: "COMPLETED";
|
|
670
|
+
const logs = typeof observation.rawStatus === "object" &&
|
|
671
|
+
observation.rawStatus !== null &&
|
|
672
|
+
"logs" in observation.rawStatus &&
|
|
673
|
+
Array.isArray(observation.rawStatus.logs)
|
|
674
|
+
? observation.rawStatus.logs
|
|
675
|
+
: undefined;
|
|
676
|
+
opts.onQueueUpdate?.({
|
|
677
|
+
status,
|
|
678
|
+
logs: logs?.filter((entry) => typeof entry === "object" &&
|
|
679
|
+
entry !== null &&
|
|
680
|
+
"message" in entry &&
|
|
681
|
+
typeof entry.message === "string")
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
recordGenerationProviderResult(result.data);
|
|
686
|
+
return { data: result.data, requestId: result.requestId };
|
|
687
|
+
},
|
|
688
|
+
storage: client.storage
|
|
689
|
+
};
|
|
239
690
|
return this._client;
|
|
240
691
|
}
|
|
692
|
+
/** How the platform-API client reaches FAL: this provider's fetch, its signal. */
|
|
693
|
+
generationsOptions(signal) {
|
|
694
|
+
const options = { fetchFn: this._fetch };
|
|
695
|
+
if (signal)
|
|
696
|
+
options.signal = signal;
|
|
697
|
+
return options;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* FAL's own record of this account's requests, through its Platform APIs —
|
|
701
|
+
* generations this installation never ran included, at the cost FAL billed.
|
|
702
|
+
* See `fal-generations.ts` for which endpoint answers what.
|
|
703
|
+
*/
|
|
704
|
+
async listGenerations(query = {}) {
|
|
705
|
+
return falListGenerations(this.apiKey, query, this.generationsOptions(query.signal));
|
|
706
|
+
}
|
|
707
|
+
/** One FAL request by its request id — the id the local row keeps. */
|
|
708
|
+
async getGeneration(requestId, options = {}) {
|
|
709
|
+
return falGetGeneration(this.apiKey, requestId, options, this.generationsOptions(options.signal));
|
|
710
|
+
}
|
|
241
711
|
async getAvailableImageModels() {
|
|
242
|
-
return loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
712
|
+
return expandTopazVariants(loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
|
|
243
713
|
}
|
|
244
714
|
async getAvailableVideoModels() {
|
|
245
|
-
return loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
715
|
+
return expandTopazVariants(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
|
|
716
|
+
}
|
|
717
|
+
async getAvailableTTSModels() {
|
|
718
|
+
return loadTTSModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
246
719
|
}
|
|
247
|
-
async
|
|
248
|
-
|
|
720
|
+
async getAvailableMusicModels() {
|
|
721
|
+
return loadMusicModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
249
722
|
}
|
|
250
|
-
async
|
|
251
|
-
|
|
723
|
+
async getAvailableAudioToAudioModels() {
|
|
724
|
+
return loadAudioToAudioModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Rewrite a recording. The source audio is uploaded and attached to whatever
|
|
728
|
+
* audio field the endpoint declares; the optional direction fields are only
|
|
729
|
+
* written where the manifest declares them, so a voice changer that takes a
|
|
730
|
+
* `voice` and a separator that takes nothing both work off one call.
|
|
731
|
+
*/
|
|
732
|
+
async audioToAudio(audio, params) {
|
|
733
|
+
const modelId = params.model.id;
|
|
734
|
+
const url = await this.upload(audio, "audio/mpeg");
|
|
735
|
+
const b = new FalArgsBuilder(modelId);
|
|
736
|
+
b.attachAsset("audio", url)
|
|
737
|
+
.set("prompt", params.prompt)
|
|
738
|
+
.set("voice", params.voice)
|
|
739
|
+
.set("strength", params.strength);
|
|
740
|
+
if (params.seed != null && params.seed !== -1)
|
|
741
|
+
b.set("seed", params.seed);
|
|
742
|
+
this.recordRequestPayload(b.args);
|
|
743
|
+
log.debug("FAL audioToAudio", { model: modelId });
|
|
744
|
+
const client = await this.getClient();
|
|
745
|
+
const result = await client.subscribe(modelId, {
|
|
746
|
+
input: b.args,
|
|
747
|
+
logs: true,
|
|
748
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
749
|
+
});
|
|
750
|
+
const data = (result.data ?? result);
|
|
751
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
752
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Generate music as an encoded audio file. FAL music endpoints (Stable Audio,
|
|
756
|
+
* MiniMax Music, ACE-Step, DiffRhythm, …) return a file URL, so this mirrors
|
|
757
|
+
* {@link textToSpeechEncoded}: shape the request from the manifest, run it,
|
|
758
|
+
* and download the resulting audio bytes.
|
|
759
|
+
*/
|
|
760
|
+
async textToMusic(params) {
|
|
761
|
+
if (!params.prompt)
|
|
762
|
+
throw new Error("prompt must not be empty");
|
|
763
|
+
const client = await this.getClient();
|
|
764
|
+
const modelId = params.model.id;
|
|
765
|
+
const input = this.buildTextToMusicArgs(modelId, params);
|
|
766
|
+
this.recordRequestPayload(input);
|
|
767
|
+
log.debug("FAL textToMusic", { model: modelId });
|
|
768
|
+
const result = await client.subscribe(modelId, {
|
|
769
|
+
input,
|
|
770
|
+
logs: true,
|
|
771
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
772
|
+
});
|
|
773
|
+
const data = (result.data ?? result);
|
|
774
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
775
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Shape music inputs for whatever the endpoint declares. The text prompt
|
|
779
|
+
* lands in `prompt`; lyrics (for vocal models) and duration land in whichever
|
|
780
|
+
* of the heterogeneous duration/lyrics fields the endpoint accepts.
|
|
781
|
+
* {@link FalArgsBuilder.set} drops keys the endpoint doesn't declare.
|
|
782
|
+
*/
|
|
783
|
+
buildTextToMusicArgs(modelId, params) {
|
|
784
|
+
const b = new FalArgsBuilder(modelId);
|
|
785
|
+
b.force("prompt", params.prompt)
|
|
786
|
+
.set("style_prompt", params.prompt)
|
|
787
|
+
.set("lyrics", params.lyrics)
|
|
788
|
+
.set("lyrics_prompt", params.lyrics);
|
|
789
|
+
if (params.durationSeconds != null) {
|
|
790
|
+
b.set("duration", params.durationSeconds)
|
|
791
|
+
.set("seconds_total", params.durationSeconds)
|
|
792
|
+
.set("music_duration", params.durationSeconds);
|
|
793
|
+
if (b.declares("music_length_ms")) {
|
|
794
|
+
// ElevenLabs expects an integer, despite the optional field's string
|
|
795
|
+
// type in the generated manifest: https://fal.ai/models/fal-ai/elevenlabs/music/api
|
|
796
|
+
b.force("music_length_ms", Math.round(params.durationSeconds * 1000));
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (params.seed != null && params.seed !== -1)
|
|
800
|
+
b.set("seed", params.seed);
|
|
801
|
+
return b.args;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Generate speech as an encoded audio file (mp3/wav, depending on the
|
|
805
|
+
* endpoint). FAL TTS endpoints return a file URL rather than raw PCM, so the
|
|
806
|
+
* unified TTS node consumes this encoded path instead of streaming samples.
|
|
807
|
+
*/
|
|
808
|
+
async textToSpeechEncoded(args) {
|
|
809
|
+
if (!args.text)
|
|
810
|
+
throw new Error("text must not be empty");
|
|
811
|
+
const client = await this.getClient();
|
|
812
|
+
const input = this.buildTextToSpeechArgs(args.model, args);
|
|
813
|
+
log.debug("FAL textToSpeech", { model: args.model });
|
|
814
|
+
const result = await client.subscribe(args.model, {
|
|
815
|
+
input,
|
|
816
|
+
logs: true,
|
|
817
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
818
|
+
});
|
|
819
|
+
const data = (result.data ?? result);
|
|
820
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
821
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Shape TTS inputs for whatever the endpoint declares. The spoken text lands
|
|
825
|
+
* in the first text-like field the endpoint accepts; the voice lands in
|
|
826
|
+
* `voice` / `speaker` when enumerated. {@link FalArgsBuilder.set} drops keys
|
|
827
|
+
* the endpoint doesn't declare, so this works across the heterogeneous TTS
|
|
828
|
+
* catalog.
|
|
829
|
+
*/
|
|
830
|
+
buildTextToSpeechArgs(modelId, params) {
|
|
831
|
+
const b = new FalArgsBuilder(modelId);
|
|
832
|
+
if (b.has("text")) {
|
|
833
|
+
b.set("text", params.text);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
b.set("prompt", params.text)
|
|
837
|
+
.set("script", params.text)
|
|
838
|
+
.set("gen_text", params.text)
|
|
839
|
+
.set("input", params.text);
|
|
840
|
+
}
|
|
841
|
+
b.set("voice", params.voice).set("speaker", params.voice);
|
|
842
|
+
if (params.speed != null)
|
|
843
|
+
b.set("speed", params.speed);
|
|
844
|
+
b.set("language", params.language).set("instructions", params.instructions);
|
|
845
|
+
return b.args;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* fal's chat surface is an OpenAI-compatible route
|
|
849
|
+
* ({@link FAL_LLM_BASE_URL}) backed by OpenRouter, so it speaks the same
|
|
850
|
+
* dialect every other gateway provider here does — with one difference: fal
|
|
851
|
+
* authenticates with `Key <FAL_KEY>`, not `Bearer`, and answers 401 to the
|
|
852
|
+
* Bearer form. The compat client spreads `defaultHeaders` after its own
|
|
853
|
+
* Authorization, so this overrides it.
|
|
854
|
+
*/
|
|
855
|
+
chatProvider() {
|
|
856
|
+
if (this._chat)
|
|
857
|
+
return this._chat;
|
|
858
|
+
if (!this.apiKey) {
|
|
859
|
+
throw new Error("FAL_API_KEY is required for chat generation");
|
|
860
|
+
}
|
|
861
|
+
const chat = new OpenAICompatProvider({
|
|
862
|
+
providerId: "fal_ai",
|
|
863
|
+
apiKey: this.apiKey,
|
|
864
|
+
baseURL: FAL_LLM_BASE_URL,
|
|
865
|
+
defaultHeaders: { Authorization: `Key ${this.apiKey}` }
|
|
866
|
+
}, { fetchFn: this._fetch });
|
|
867
|
+
chat.setMessageEmitter((msg) => this.emitMessage(msg));
|
|
868
|
+
this._chat = chat;
|
|
869
|
+
return chat;
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Language models reachable through fal's OpenAI-compatible route. The route
|
|
873
|
+
* is OpenRouter's router, so the ids it accepts are OpenRouter's ids and its
|
|
874
|
+
* public catalog is the model list — fal itself publishes no `/models` for
|
|
875
|
+
* this endpoint. Unreachable catalog means no language models rather than a
|
|
876
|
+
* failed provider, matching how the other enumerating providers fail.
|
|
877
|
+
*/
|
|
878
|
+
async getAvailableLanguageModels() {
|
|
879
|
+
let rows;
|
|
880
|
+
try {
|
|
881
|
+
const response = await this._fetch(OPENROUTER_CATALOG_URL);
|
|
882
|
+
if (!response.ok)
|
|
883
|
+
return [];
|
|
884
|
+
rows = decodeFalLanguageCatalog(await response.json());
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
log.warn(`Could not load the fal language-model catalog: ${err}`);
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
const toolCapable = new Set();
|
|
891
|
+
const models = [];
|
|
892
|
+
for (const row of rows) {
|
|
893
|
+
if (!isNonEmptyString(row.id))
|
|
894
|
+
continue;
|
|
895
|
+
if (row.supported_parameters?.includes("tools"))
|
|
896
|
+
toolCapable.add(row.id);
|
|
897
|
+
models.push({ id: row.id, name: row.name ?? row.id, provider: "fal_ai" });
|
|
898
|
+
}
|
|
899
|
+
this._toolCapableModels = toolCapable;
|
|
900
|
+
return models;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Tool support varies model by model across a 300-model catalog, so answer
|
|
904
|
+
* from what the catalog declared. Before any listing there is nothing to
|
|
905
|
+
* answer from — say yes, as the base class does, rather than hiding tools
|
|
906
|
+
* from a model that has them.
|
|
907
|
+
*/
|
|
908
|
+
async hasToolSupport(model) {
|
|
909
|
+
return this._toolCapableModels?.has(model) ?? true;
|
|
910
|
+
}
|
|
911
|
+
async generateMessage(args) {
|
|
912
|
+
return this.chatProvider().generateMessage(args);
|
|
913
|
+
}
|
|
914
|
+
async *generateMessages(args) {
|
|
915
|
+
yield* this.chatProvider().generateMessages(args);
|
|
916
|
+
}
|
|
917
|
+
/** Chat spend accrues on the delegate; report it as this provider's own. */
|
|
918
|
+
get cost() {
|
|
919
|
+
return super.cost + (this._chat?.cost ?? 0);
|
|
920
|
+
}
|
|
921
|
+
resetCost() {
|
|
922
|
+
super.resetCost();
|
|
923
|
+
this._chat?.resetCost();
|
|
252
924
|
}
|
|
253
925
|
buildTextToImageArgs(modelId, params) {
|
|
254
926
|
const b = new FalArgsBuilder(modelId);
|
|
255
927
|
b.force("prompt", params.prompt)
|
|
256
928
|
.set("output_format", "png")
|
|
929
|
+
.set("quality", params.quality)
|
|
257
930
|
.set("negative_prompt", params.negativePrompt)
|
|
258
931
|
.set("guidance_scale", params.guidanceScale)
|
|
259
932
|
.set("num_inference_steps", params.numInferenceSteps)
|
|
@@ -263,14 +936,17 @@ export class FalProvider extends BaseProvider {
|
|
|
263
936
|
b.set("seed", params.seed);
|
|
264
937
|
return b.args;
|
|
265
938
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
939
|
+
/**
|
|
940
|
+
* Configure a {@link FalArgsBuilder} with the shared image-edit inputs
|
|
941
|
+
* (source images + prompt + sampling knobs). Used by both image-to-image and
|
|
942
|
+
* inpaint; the latter additionally attaches a mask.
|
|
943
|
+
*/
|
|
944
|
+
buildEditBuilder(modelId, imageUrls, params) {
|
|
270
945
|
const b = new FalArgsBuilder(modelId);
|
|
271
|
-
b.
|
|
946
|
+
b.attachAssets("image", imageUrls)
|
|
272
947
|
.force("prompt", params.prompt)
|
|
273
948
|
.set("output_format", "png")
|
|
949
|
+
.set("quality", params.quality)
|
|
274
950
|
.set("negative_prompt", params.negativePrompt)
|
|
275
951
|
.set("guidance_scale", params.guidanceScale)
|
|
276
952
|
.set("num_inference_steps", params.numInferenceSteps)
|
|
@@ -279,6 +955,23 @@ export class FalProvider extends BaseProvider {
|
|
|
279
955
|
.setSize(params.aspectRatio, params.resolution);
|
|
280
956
|
if (params.seed != null && params.seed !== -1)
|
|
281
957
|
b.set("seed", params.seed);
|
|
958
|
+
return b;
|
|
959
|
+
}
|
|
960
|
+
async buildImageToImageArgs(modelId, images, params) {
|
|
961
|
+
const urls = await this.uploadImages(images);
|
|
962
|
+
return this.buildEditBuilder(modelId, urls, params).args;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Build image-to-image args plus the inpaint mask. The mask is uploaded and
|
|
966
|
+
* attached to whatever mask field the endpoint declares (`mask_url`,
|
|
967
|
+
* `mask_image_url`, …) via {@link FalArgsBuilder.attachMask}.
|
|
968
|
+
*/
|
|
969
|
+
async buildInpaintArgs(modelId, images, params) {
|
|
970
|
+
const urls = await this.uploadImages(images);
|
|
971
|
+
const b = this.buildEditBuilder(modelId, urls, params);
|
|
972
|
+
const [maskUrl] = await this.uploadImages([params.mask]);
|
|
973
|
+
if (maskUrl)
|
|
974
|
+
b.attachMask(maskUrl);
|
|
282
975
|
return b.args;
|
|
283
976
|
}
|
|
284
977
|
buildTextToVideoArgs(modelId, params) {
|
|
@@ -293,12 +986,10 @@ export class FalProvider extends BaseProvider {
|
|
|
293
986
|
b.set("seed", params.seed);
|
|
294
987
|
return b.args;
|
|
295
988
|
}
|
|
296
|
-
async buildImageToVideoArgs(modelId,
|
|
297
|
-
const
|
|
298
|
-
const blob = new Blob([image], { type: "image/png" });
|
|
299
|
-
const uploadedUrl = await client.storage.upload(blob);
|
|
989
|
+
async buildImageToVideoArgs(modelId, images, params) {
|
|
990
|
+
const urls = await this.uploadImages(images);
|
|
300
991
|
const b = new FalArgsBuilder(modelId);
|
|
301
|
-
b.
|
|
992
|
+
b.attachAssets("image", urls)
|
|
302
993
|
.set("prompt", params.prompt)
|
|
303
994
|
.set("negative_prompt", params.negativePrompt)
|
|
304
995
|
.set("num_frames", params.numFrames)
|
|
@@ -313,20 +1004,38 @@ export class FalProvider extends BaseProvider {
|
|
|
313
1004
|
const client = await this.getClient();
|
|
314
1005
|
const modelId = params.model.id;
|
|
315
1006
|
const args = this.buildTextToImageArgs(modelId, params);
|
|
1007
|
+
this.recordRequestPayload(args);
|
|
316
1008
|
log.debug("FAL textToImage", { model: modelId });
|
|
317
1009
|
const result = await client.subscribe(modelId, {
|
|
318
1010
|
input: args,
|
|
319
1011
|
logs: true,
|
|
320
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1012
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1013
|
+
abortSignal: params.signal
|
|
321
1014
|
});
|
|
322
1015
|
const data = (result.data ?? result);
|
|
323
1016
|
return downloadBytes(extractImageUrl(data));
|
|
324
1017
|
}
|
|
325
|
-
async imageToImage(
|
|
1018
|
+
async imageToImage(images, params) {
|
|
326
1019
|
const client = await this.getClient();
|
|
327
1020
|
const modelId = params.model.id;
|
|
328
|
-
const args = await this.buildImageToImageArgs(modelId,
|
|
1021
|
+
const args = await this.buildImageToImageArgs(modelId, images, params);
|
|
1022
|
+
this.recordRequestPayload(args);
|
|
329
1023
|
log.debug("FAL imageToImage", { model: modelId });
|
|
1024
|
+
const result = await client.subscribe(modelId, {
|
|
1025
|
+
input: args,
|
|
1026
|
+
logs: true,
|
|
1027
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1028
|
+
abortSignal: params.signal
|
|
1029
|
+
});
|
|
1030
|
+
const data = (result.data ?? result);
|
|
1031
|
+
return downloadBytes(extractImageUrl(data));
|
|
1032
|
+
}
|
|
1033
|
+
async inpaint(images, params) {
|
|
1034
|
+
const client = await this.getClient();
|
|
1035
|
+
const modelId = params.model.id;
|
|
1036
|
+
const args = await this.buildInpaintArgs(modelId, images, params);
|
|
1037
|
+
this.recordRequestPayload(args);
|
|
1038
|
+
log.debug("FAL inpaint", { model: modelId });
|
|
330
1039
|
const result = await client.subscribe(modelId, {
|
|
331
1040
|
input: args,
|
|
332
1041
|
logs: true,
|
|
@@ -335,38 +1044,77 @@ export class FalProvider extends BaseProvider {
|
|
|
335
1044
|
const data = (result.data ?? result);
|
|
336
1045
|
return downloadBytes(extractImageUrl(data));
|
|
337
1046
|
}
|
|
1047
|
+
async inpaintImages(images, params, numImages) {
|
|
1048
|
+
if (numImages <= 1) {
|
|
1049
|
+
return [await this.inpaint(images, params)];
|
|
1050
|
+
}
|
|
1051
|
+
const client = await this.getClient();
|
|
1052
|
+
const modelId = params.model.id;
|
|
1053
|
+
const args = await this.buildInpaintArgs(modelId, images, params);
|
|
1054
|
+
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
1055
|
+
args.num_images = numImages;
|
|
1056
|
+
}
|
|
1057
|
+
this.recordRequestPayload(args);
|
|
1058
|
+
log.debug("FAL inpaintImages", { model: modelId, numImages });
|
|
1059
|
+
const result = await client.subscribe(modelId, {
|
|
1060
|
+
input: args,
|
|
1061
|
+
logs: true,
|
|
1062
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1063
|
+
});
|
|
1064
|
+
const data = (result.data ?? result);
|
|
1065
|
+
const urls = extractImageUrls(data);
|
|
1066
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
1067
|
+
}
|
|
1068
|
+
async outpaintImage(images, params) {
|
|
1069
|
+
const modelId = params.model.id;
|
|
1070
|
+
const urls = await this.uploadImages(images);
|
|
1071
|
+
const b = new FalArgsBuilder(modelId);
|
|
1072
|
+
b.attachAssets("image", urls)
|
|
1073
|
+
.set("prompt", params.prompt)
|
|
1074
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1075
|
+
.setSize(params.aspectRatio, null);
|
|
1076
|
+
setOutpaintPadding(b, params.padding);
|
|
1077
|
+
if (params.seed != null && params.seed !== -1)
|
|
1078
|
+
b.set("seed", params.seed);
|
|
1079
|
+
log.debug("FAL outpaintImage", { model: modelId });
|
|
1080
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1081
|
+
}
|
|
338
1082
|
async textToVideo(params) {
|
|
339
1083
|
const client = await this.getClient();
|
|
340
1084
|
const modelId = params.model.id;
|
|
341
1085
|
const args = this.buildTextToVideoArgs(modelId, params);
|
|
1086
|
+
this.recordRequestPayload(args);
|
|
342
1087
|
log.debug("FAL textToVideo", { model: modelId });
|
|
343
1088
|
const result = await client.subscribe(modelId, {
|
|
344
1089
|
input: args,
|
|
345
1090
|
logs: true,
|
|
346
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1091
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1092
|
+
abortSignal: params.signal
|
|
347
1093
|
});
|
|
348
1094
|
const data = (result.data ?? result);
|
|
349
1095
|
return downloadBytes(extractVideoUrl(data));
|
|
350
1096
|
}
|
|
351
|
-
async imageToImages(
|
|
1097
|
+
async imageToImages(images, params, numImages) {
|
|
352
1098
|
if (numImages <= 1) {
|
|
353
|
-
return [await this.imageToImage(
|
|
1099
|
+
return [await this.imageToImage(images, params)];
|
|
354
1100
|
}
|
|
355
1101
|
const client = await this.getClient();
|
|
356
1102
|
const modelId = params.model.id;
|
|
357
|
-
const args = await this.buildImageToImageArgs(modelId,
|
|
1103
|
+
const args = await this.buildImageToImageArgs(modelId, images, params);
|
|
358
1104
|
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
359
1105
|
args.num_images = numImages;
|
|
360
1106
|
}
|
|
1107
|
+
this.recordRequestPayload(args);
|
|
361
1108
|
log.debug("FAL imageToImages", { model: modelId, numImages });
|
|
362
1109
|
const result = await client.subscribe(modelId, {
|
|
363
1110
|
input: args,
|
|
364
1111
|
logs: true,
|
|
365
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1112
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1113
|
+
abortSignal: params.signal
|
|
366
1114
|
});
|
|
367
1115
|
const data = (result.data ?? result);
|
|
368
1116
|
const urls = extractImageUrls(data);
|
|
369
|
-
return Promise.all(urls.map(downloadBytes));
|
|
1117
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
370
1118
|
}
|
|
371
1119
|
async textToImages(params, numImages) {
|
|
372
1120
|
if (numImages <= 1) {
|
|
@@ -378,31 +1126,359 @@ export class FalProvider extends BaseProvider {
|
|
|
378
1126
|
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
379
1127
|
args.num_images = numImages;
|
|
380
1128
|
}
|
|
1129
|
+
this.recordRequestPayload(args);
|
|
381
1130
|
log.debug("FAL textToImages", { model: modelId, numImages });
|
|
382
1131
|
const result = await client.subscribe(modelId, {
|
|
383
1132
|
input: args,
|
|
384
1133
|
logs: true,
|
|
385
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1134
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1135
|
+
abortSignal: params.signal
|
|
386
1136
|
});
|
|
387
1137
|
const data = (result.data ?? result);
|
|
388
1138
|
const urls = extractImageUrls(data);
|
|
389
|
-
return Promise.all(urls.map(downloadBytes));
|
|
1139
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
390
1140
|
}
|
|
391
1141
|
async imageToVideo(image, params) {
|
|
1142
|
+
if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
|
|
1143
|
+
.find((m) => m.id === params.model.id)
|
|
1144
|
+
?.supportedTasks?.includes("image_to_video") ?? false))
|
|
1145
|
+
throw new Error(`FAL model ${params.model.id} does not support image_to_video`);
|
|
392
1146
|
const client = await this.getClient();
|
|
393
1147
|
const modelId = params.model.id;
|
|
394
|
-
const args = await this.buildImageToVideoArgs(modelId, image, params);
|
|
1148
|
+
const args = await this.buildImageToVideoArgs(modelId, [image], params);
|
|
1149
|
+
this.recordRequestPayload(args);
|
|
395
1150
|
log.debug("FAL imageToVideo", { model: modelId });
|
|
396
1151
|
const result = await client.subscribe(modelId, {
|
|
397
1152
|
input: args,
|
|
398
1153
|
logs: true,
|
|
399
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1154
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1155
|
+
abortSignal: params.signal
|
|
400
1156
|
});
|
|
401
1157
|
const data = (result.data ?? result);
|
|
402
1158
|
return downloadBytes(extractVideoUrl(data));
|
|
403
1159
|
}
|
|
1160
|
+
async referenceToVideo(inputs, params) {
|
|
1161
|
+
const images = inputs.images.filter((b) => b.length > 0);
|
|
1162
|
+
const videos = inputs.videos.filter((b) => b.length > 0);
|
|
1163
|
+
if (images.length === 0 && videos.length === 0) {
|
|
1164
|
+
throw new Error("reference_to_video requires at least one reference image or video");
|
|
1165
|
+
}
|
|
1166
|
+
const modelId = params.model.id;
|
|
1167
|
+
const builder = new FalArgsBuilder(modelId);
|
|
1168
|
+
if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
|
|
1169
|
+
.find((m) => m.id === modelId)
|
|
1170
|
+
?.supportedTasks?.includes("reference_to_video") ?? false))
|
|
1171
|
+
throw new Error(`FAL model ${modelId} does not support reference_to_video`);
|
|
1172
|
+
validateReferenceInputs("FAL", modelId, inputs, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, modelId));
|
|
1173
|
+
const imageFields = builder.referenceAssetInputs("image");
|
|
1174
|
+
const videoFields = builder.referenceAssetInputs("video");
|
|
1175
|
+
if (images.length > 0 && imageFields.length === 0)
|
|
1176
|
+
throw new Error(`FAL model ${modelId} does not support reference images`);
|
|
1177
|
+
if (videos.length > 0 && videoFields.length === 0)
|
|
1178
|
+
throw new Error(`FAL model ${modelId} does not support reference videos`);
|
|
1179
|
+
if (params.useReferenceVideoAudio === true && videos.length === 0)
|
|
1180
|
+
throw new Error(`FAL model ${modelId} requires a reference video when audio is enabled`);
|
|
1181
|
+
if (params.useReferenceVideoAudio === true &&
|
|
1182
|
+
!builder.has("use_reference_video_audio"))
|
|
1183
|
+
throw new Error(`FAL model ${modelId} does not support reference video audio`);
|
|
1184
|
+
const signal = combineSignals(params.signal, params.timeoutSeconds);
|
|
1185
|
+
signal?.throwIfAborted();
|
|
1186
|
+
const imageUrls = await Promise.all(images.map((b) => this.upload(b, detectImageMime(b))));
|
|
1187
|
+
const videoUrls = await Promise.all(videos.map((b) => this.upload(b, sniffMediaMime(b, "video/mp4"))));
|
|
1188
|
+
signal?.throwIfAborted();
|
|
1189
|
+
builder
|
|
1190
|
+
.attachReferenceAssets("image", imageUrls)
|
|
1191
|
+
.attachReferenceAssets("video", videoUrls)
|
|
1192
|
+
.set("prompt", params.prompt)
|
|
1193
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1194
|
+
.set("duration", params.durationSeconds)
|
|
1195
|
+
.setSize(params.aspectRatio, params.resolution)
|
|
1196
|
+
.set("use_reference_video_audio", params.useReferenceVideoAudio);
|
|
1197
|
+
this.recordRequestPayload(builder.args);
|
|
1198
|
+
const client = await this.getClient();
|
|
1199
|
+
const result = await client.subscribe(modelId, {
|
|
1200
|
+
input: builder.args,
|
|
1201
|
+
logs: true,
|
|
1202
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1203
|
+
abortSignal: signal
|
|
1204
|
+
});
|
|
1205
|
+
return downloadBytes(extractVideoUrl((result.data ?? result)), signal);
|
|
1206
|
+
}
|
|
1207
|
+
/** Upload raw bytes to FAL storage and return the hosted URL. */
|
|
1208
|
+
async upload(bytes, mimeType) {
|
|
1209
|
+
const client = await this.getClient();
|
|
1210
|
+
const blob = new Blob([new Uint8Array(bytes)], {
|
|
1211
|
+
type: mimeType
|
|
1212
|
+
});
|
|
1213
|
+
return client.storage.upload(blob);
|
|
1214
|
+
}
|
|
1215
|
+
/** Upload every non-empty image to FAL storage, returning the hosted URLs. */
|
|
1216
|
+
async uploadImages(images) {
|
|
1217
|
+
const valid = images.filter((b) => b && b.length > 0);
|
|
1218
|
+
return Promise.all(valid.map((b) => this.upload(b, detectImageMime(b))));
|
|
1219
|
+
}
|
|
1220
|
+
/** Run an endpoint that takes a single uploaded image and returns an image. */
|
|
1221
|
+
async runImageEndpoint(modelId, args) {
|
|
1222
|
+
const client = await this.getClient();
|
|
1223
|
+
this.recordRequestPayload(args);
|
|
1224
|
+
const result = await client.subscribe(modelId, {
|
|
1225
|
+
input: args,
|
|
1226
|
+
logs: true,
|
|
1227
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1228
|
+
});
|
|
1229
|
+
const data = (result.data ?? result);
|
|
1230
|
+
return downloadBytes(extractImageUrl(data));
|
|
1231
|
+
}
|
|
1232
|
+
/** Run an endpoint that returns a video and download the bytes. */
|
|
1233
|
+
async runVideoEndpoint(modelId, args, signal) {
|
|
1234
|
+
const client = await this.getClient();
|
|
1235
|
+
this.recordRequestPayload(args);
|
|
1236
|
+
const result = await client.subscribe(modelId, {
|
|
1237
|
+
input: args,
|
|
1238
|
+
logs: true,
|
|
1239
|
+
abortSignal: signal,
|
|
1240
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1241
|
+
});
|
|
1242
|
+
const data = (result.data ?? result);
|
|
1243
|
+
return downloadBytes(extractVideoUrl(data), signal);
|
|
1244
|
+
}
|
|
1245
|
+
async upscaleImage(image, params) {
|
|
1246
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1247
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1248
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1249
|
+
b.attachAsset("image", url)
|
|
1250
|
+
.set("model", variant)
|
|
1251
|
+
.set("prompt", params.prompt)
|
|
1252
|
+
.set("upscale_factor", params.scale)
|
|
1253
|
+
.set("scale", params.scale)
|
|
1254
|
+
.set("creativity", topazCreativity(endpointId, params.creativity));
|
|
1255
|
+
if (params.seed != null && params.seed !== -1)
|
|
1256
|
+
b.set("seed", params.seed);
|
|
1257
|
+
log.debug("FAL upscaleImage", { model: endpointId, variant });
|
|
1258
|
+
return this.runImageEndpoint(endpointId, b.args);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Segment an image into object masks. fal's SAM endpoints answer with mask
|
|
1262
|
+
* images plus loose side-channels — `scores`, `boxes` and `metadata` arrive
|
|
1263
|
+
* as JSON-encoded strings, and boxes are normalized `[cx, cy, w, h]` — so
|
|
1264
|
+
* everything past the mask list is parsed defensively and left absent when
|
|
1265
|
+
* it does not decode.
|
|
1266
|
+
*/
|
|
1267
|
+
async segmentImage(image, params) {
|
|
1268
|
+
if (image.length === 0) {
|
|
1269
|
+
throw new Error("segmentImage requires a non-empty image");
|
|
1270
|
+
}
|
|
1271
|
+
const modelId = params.model.id;
|
|
1272
|
+
assertSegmentPromptIsUsable(modelId, params);
|
|
1273
|
+
const client = await this.getClient();
|
|
1274
|
+
const [imageUrl] = await this.uploadImages([image]);
|
|
1275
|
+
const args = buildSegmentImageArgs(modelId, imageUrl, params);
|
|
1276
|
+
this.recordRequestPayload(args);
|
|
1277
|
+
log.debug("FAL segmentImage", { model: modelId });
|
|
1278
|
+
const result = await client.subscribe(modelId, {
|
|
1279
|
+
input: args,
|
|
1280
|
+
logs: true,
|
|
1281
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1282
|
+
abortSignal: params.signal
|
|
1283
|
+
});
|
|
1284
|
+
const data = (result.data ?? result);
|
|
1285
|
+
const { masks, decoded, bestScore } = await collectSegmentationMasks(data, params.minConfidence);
|
|
1286
|
+
if (masks.length === 0) {
|
|
1287
|
+
// Three different failures end here and look alike from the caller: the
|
|
1288
|
+
// model found nothing, the response carries a shape this decoder does
|
|
1289
|
+
// not read, or every mask it did read fell under `minConfidence`. Name
|
|
1290
|
+
// the fields the response carried and what was decoded so they separate.
|
|
1291
|
+
log.warn("FAL segmentImage returned no masks", {
|
|
1292
|
+
model: modelId,
|
|
1293
|
+
responseFields: Object.keys(data),
|
|
1294
|
+
decodedMasks: decoded,
|
|
1295
|
+
minConfidence: params.minConfidence ?? null,
|
|
1296
|
+
bestScore
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return masks;
|
|
1300
|
+
}
|
|
1301
|
+
async removeBackground(image, params) {
|
|
1302
|
+
const modelId = params.model.id;
|
|
1303
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1304
|
+
const b = new FalArgsBuilder(modelId);
|
|
1305
|
+
b.attachAsset("image", url).set("output_format", "png");
|
|
1306
|
+
log.debug("FAL removeBackground", { model: modelId });
|
|
1307
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1308
|
+
}
|
|
1309
|
+
async relightImage(image, params) {
|
|
1310
|
+
const modelId = params.model.id;
|
|
1311
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1312
|
+
const b = new FalArgsBuilder(modelId);
|
|
1313
|
+
b.attachAsset("image", url)
|
|
1314
|
+
.force("prompt", params.prompt)
|
|
1315
|
+
.set("output_format", "png")
|
|
1316
|
+
.set("negative_prompt", params.negativePrompt);
|
|
1317
|
+
if (params.seed != null && params.seed !== -1)
|
|
1318
|
+
b.set("seed", params.seed);
|
|
1319
|
+
log.debug("FAL relightImage", { model: modelId });
|
|
1320
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1321
|
+
}
|
|
1322
|
+
async vectorizeImage(image, params) {
|
|
1323
|
+
const modelId = params.model.id;
|
|
1324
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1325
|
+
const b = new FalArgsBuilder(modelId);
|
|
1326
|
+
b.attachAsset("image", url);
|
|
1327
|
+
log.debug("FAL vectorizeImage", { model: modelId });
|
|
1328
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1329
|
+
}
|
|
1330
|
+
async videoToVideo(video, params) {
|
|
1331
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1332
|
+
const images = params.referenceImages ?? [];
|
|
1333
|
+
if (images.length) {
|
|
1334
|
+
validateReferenceInputs("FAL", endpointId, { images, videos: [] }, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, endpointId));
|
|
1335
|
+
}
|
|
1336
|
+
params.signal?.throwIfAborted();
|
|
1337
|
+
const imageUrls = await Promise.all(images.map((image) => this.upload(image, detectImageMime(image))));
|
|
1338
|
+
const url = await this.upload(video, "video/mp4");
|
|
1339
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1340
|
+
b.attachReferenceAssets("image", imageUrls);
|
|
1341
|
+
b.attachAsset("video", url)
|
|
1342
|
+
.set("model", variant)
|
|
1343
|
+
.set("prompt", params.prompt)
|
|
1344
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1345
|
+
.set("strength", params.strength)
|
|
1346
|
+
.set("duration", params.durationSeconds)
|
|
1347
|
+
.setSize(null, params.resolution);
|
|
1348
|
+
if (params.seed != null && params.seed !== -1)
|
|
1349
|
+
b.set("seed", params.seed);
|
|
1350
|
+
log.debug("FAL videoToVideo", { model: endpointId, variant });
|
|
1351
|
+
return this.runVideoEndpoint(endpointId, b.args, params.signal);
|
|
1352
|
+
}
|
|
1353
|
+
async extendVideo(video, params) {
|
|
1354
|
+
const model = (await this.getAvailableVideoModels()).find((candidate) => candidate.id === params.model.id);
|
|
1355
|
+
if (!model?.supportedTasks?.includes("extend_video")) {
|
|
1356
|
+
throw new Error(`Model ${params.model.id} does not support extend_video`);
|
|
1357
|
+
}
|
|
1358
|
+
const directionTasks = model.supportedTasks.filter((task) => task.startsWith("extend_video_"));
|
|
1359
|
+
if (directionTasks.length > 0 &&
|
|
1360
|
+
!directionTasks.includes(`extend_video_${params.mode}`)) {
|
|
1361
|
+
throw new Error(`Model ${params.model.id} does not support ${params.mode} extension`);
|
|
1362
|
+
}
|
|
1363
|
+
if ((params.mode !== "start" && params.mode !== "end") ||
|
|
1364
|
+
!Number.isFinite(params.durationSeconds) ||
|
|
1365
|
+
params.durationSeconds <= 0 ||
|
|
1366
|
+
(model.durations?.length &&
|
|
1367
|
+
!model.durations.includes(params.durationSeconds))) {
|
|
1368
|
+
throw new Error("Choose a supported extension mode and duration.");
|
|
1369
|
+
}
|
|
1370
|
+
const entry = getFalManifestEntry(params.model.id);
|
|
1371
|
+
const durationField = entry?.inputFields?.find((field) => (field.apiParamName ?? field.name) === "duration");
|
|
1372
|
+
if ((durationField?.min !== undefined &&
|
|
1373
|
+
params.durationSeconds < durationField.min) ||
|
|
1374
|
+
(durationField?.max !== undefined &&
|
|
1375
|
+
params.durationSeconds > durationField.max)) {
|
|
1376
|
+
throw new Error("Choose an extension duration within the model's supported range.");
|
|
1377
|
+
}
|
|
1378
|
+
const duration = durationField?.propType === "str" &&
|
|
1379
|
+
typeof durationField.default === "string" &&
|
|
1380
|
+
/s$/i.test(durationField.default.trim())
|
|
1381
|
+
? `${params.durationSeconds}s`
|
|
1382
|
+
: params.durationSeconds;
|
|
1383
|
+
const url = await this.upload(video, "video/mp4");
|
|
1384
|
+
const builder = new FalArgsBuilder(params.model.id);
|
|
1385
|
+
builder
|
|
1386
|
+
.attachAsset("video", url)
|
|
1387
|
+
.set("prompt", params.prompt)
|
|
1388
|
+
.set("mode", params.mode)
|
|
1389
|
+
.set("duration", duration);
|
|
1390
|
+
return this.runVideoEndpoint(params.model.id, builder.args);
|
|
1391
|
+
}
|
|
1392
|
+
async upscaleVideo(video, params) {
|
|
1393
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1394
|
+
const url = await this.upload(video, "video/mp4");
|
|
1395
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1396
|
+
b.attachAsset("video", url)
|
|
1397
|
+
.set("model", variant)
|
|
1398
|
+
.set("prompt", params.prompt)
|
|
1399
|
+
.set("creativity", params.creativity)
|
|
1400
|
+
// `upscale_factor` and `scale` name the same thing across endpoints, and
|
|
1401
|
+
// `target_resolution` is the rung-shaped alternative; each is written
|
|
1402
|
+
// only where the endpoint declares it.
|
|
1403
|
+
.set("upscale_factor", params.scale)
|
|
1404
|
+
.set("scale", params.scale)
|
|
1405
|
+
.set("target_resolution", params.targetResolution)
|
|
1406
|
+
.setSize(null, params.targetResolution);
|
|
1407
|
+
if (params.seed != null && params.seed !== -1)
|
|
1408
|
+
b.set("seed", params.seed);
|
|
1409
|
+
log.debug("FAL upscaleVideo", { model: endpointId, variant });
|
|
1410
|
+
return this.runVideoEndpoint(endpointId, b.args);
|
|
1411
|
+
}
|
|
1412
|
+
async interpolateVideo(video, params) {
|
|
1413
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1414
|
+
const url = await this.upload(video, "video/mp4");
|
|
1415
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1416
|
+
b.attachAsset("video", url)
|
|
1417
|
+
.set("model", variant)
|
|
1418
|
+
.set("output_fps", params.targetFps)
|
|
1419
|
+
.set("target_fps", params.targetFps)
|
|
1420
|
+
// AMT doubles the frame count per pass, so a factor is log2 passes;
|
|
1421
|
+
// Topaz takes the factor itself as a slowdown.
|
|
1422
|
+
.set("slowdown_factor", params.factor)
|
|
1423
|
+
.set("recursive_interpolation_passes", params.factor != null && params.factor > 1
|
|
1424
|
+
? Math.max(1, Math.round(Math.log2(params.factor)))
|
|
1425
|
+
: null);
|
|
1426
|
+
if (params.seed != null && params.seed !== -1)
|
|
1427
|
+
b.set("seed", params.seed);
|
|
1428
|
+
log.debug("FAL interpolateVideo", { model: endpointId, variant });
|
|
1429
|
+
return this.runVideoEndpoint(endpointId, b.args);
|
|
1430
|
+
}
|
|
1431
|
+
async outpaintVideo(video, params) {
|
|
1432
|
+
const modelId = params.model.id;
|
|
1433
|
+
const url = await this.upload(video, "video/mp4");
|
|
1434
|
+
const b = new FalArgsBuilder(modelId);
|
|
1435
|
+
b.attachAsset("video", url)
|
|
1436
|
+
.set("prompt", params.prompt)
|
|
1437
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1438
|
+
.set("expand_ratio", params.expandRatio)
|
|
1439
|
+
.setSize(params.aspectRatio, params.resolution);
|
|
1440
|
+
setOutpaintPadding(b, params.padding);
|
|
1441
|
+
if (params.seed != null && params.seed !== -1)
|
|
1442
|
+
b.set("seed", params.seed);
|
|
1443
|
+
log.debug("FAL outpaintVideo", { model: modelId });
|
|
1444
|
+
return this.runVideoEndpoint(modelId, b.args);
|
|
1445
|
+
}
|
|
1446
|
+
async lipSync(video, params) {
|
|
1447
|
+
const modelId = params.model.id;
|
|
1448
|
+
const [videoUrl, audioUrl] = await Promise.all([
|
|
1449
|
+
this.upload(video, "video/mp4"),
|
|
1450
|
+
this.upload(params.audio, "audio/mpeg")
|
|
1451
|
+
]);
|
|
1452
|
+
const b = new FalArgsBuilder(modelId);
|
|
1453
|
+
b.attachAsset("video", videoUrl).attachAsset("audio", audioUrl);
|
|
1454
|
+
if (params.seed != null && params.seed !== -1)
|
|
1455
|
+
b.set("seed", params.seed);
|
|
1456
|
+
log.debug("FAL lipSync", { model: modelId });
|
|
1457
|
+
return this.runVideoEndpoint(modelId, b.args);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Decode the OpenRouter catalog fal's chat route is backed by. Throws when the
|
|
1462
|
+
* body carries no `data` array; {@link FalProvider.getAvailableLanguageModels}
|
|
1463
|
+
* treats that the way it treats an unreachable catalog.
|
|
1464
|
+
*/
|
|
1465
|
+
export function decodeFalLanguageCatalog(payload) {
|
|
1466
|
+
if (!isObjectLike(payload)) {
|
|
1467
|
+
throw new Error("fal language catalog is not an object");
|
|
1468
|
+
}
|
|
1469
|
+
const rows = payload.data;
|
|
1470
|
+
if (!Array.isArray(rows)) {
|
|
1471
|
+
throw new Error("fal language catalog has no `data` array");
|
|
1472
|
+
}
|
|
1473
|
+
return rows;
|
|
404
1474
|
}
|
|
405
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The result-URL extractors below are the fal response decoders: every media
|
|
1477
|
+
* surface hands its endpoint's payload to one of them. The checked-in raw
|
|
1478
|
+
* fixtures in `packages/runtime/tests/fixtures/provider-contract/` run these
|
|
1479
|
+
* same functions, so an endpoint that renames its result field fails there.
|
|
1480
|
+
*/
|
|
1481
|
+
export function extractImageUrls(result) {
|
|
406
1482
|
const images = result.images;
|
|
407
1483
|
if (images && images.length > 0) {
|
|
408
1484
|
const urls = images.map((img) => img.url).filter(Boolean);
|
|
@@ -414,17 +1490,258 @@ function extractImageUrls(result) {
|
|
|
414
1490
|
return [image.url];
|
|
415
1491
|
throw new Error(`Unexpected FAL image response: ${JSON.stringify(result)}`);
|
|
416
1492
|
}
|
|
417
|
-
|
|
1493
|
+
/**
|
|
1494
|
+
* Shape the inputs a SAM endpoint declares. The prompt kinds are optional and
|
|
1495
|
+
* an endpoint that does not declare one simply drops it — {@link
|
|
1496
|
+
* FalArgsBuilder.set} writes only fields the manifest knows.
|
|
1497
|
+
*/
|
|
1498
|
+
function buildSegmentImageArgs(modelId, imageUrl, params) {
|
|
1499
|
+
const b = new FalArgsBuilder(modelId);
|
|
1500
|
+
const maxMasks = params.maxMasks != null ? Math.max(1, params.maxMasks) : null;
|
|
1501
|
+
b.attachAsset("image", imageUrl)
|
|
1502
|
+
// The mask images are the answer; a masked copy of the source is not.
|
|
1503
|
+
.set("apply_mask", false)
|
|
1504
|
+
.set("output_format", "png")
|
|
1505
|
+
.set("include_scores", true)
|
|
1506
|
+
.set("include_boxes", true);
|
|
1507
|
+
if (maxMasks != null) {
|
|
1508
|
+
b.set("max_masks", maxMasks).set("return_multiple_masks", maxMasks > 1);
|
|
1509
|
+
}
|
|
1510
|
+
// Each prompt kind is spelled differently per endpoint, and a name the
|
|
1511
|
+
// endpoint does not declare is dropped rather than sent — so a prompt written
|
|
1512
|
+
// to one name only would be silently discarded on half the SAM catalog.
|
|
1513
|
+
const prompt = params.prompt?.trim();
|
|
1514
|
+
const hasVisualPrompt = Boolean(params.points?.length || params.box);
|
|
1515
|
+
if (prompt) {
|
|
1516
|
+
b.setFirst(SEGMENT_TEXT_PROMPT_FIELDS, prompt);
|
|
1517
|
+
}
|
|
1518
|
+
else if (hasVisualPrompt) {
|
|
1519
|
+
// An absent field takes the endpoint's own default, and sam-3.1 defaults
|
|
1520
|
+
// `prompt` to "wheel" — so omitting the text prompt runs a concept search
|
|
1521
|
+
// for a wheel and the click is ignored. Measured against the live API: a
|
|
1522
|
+
// point alone returns `masks: []`, the same point with `prompt: ""`
|
|
1523
|
+
// returns the mask at score 0.51.
|
|
1524
|
+
b.forceFirst(SEGMENT_TEXT_PROMPT_FIELDS, "");
|
|
1525
|
+
}
|
|
1526
|
+
if (params.points?.length) {
|
|
1527
|
+
b.setFirst(SEGMENT_POINT_PROMPT_FIELDS, params.points.map((p) => ({
|
|
1528
|
+
x: Math.round(p.x),
|
|
1529
|
+
y: Math.round(p.y),
|
|
1530
|
+
label: p.include ? 1 : 0
|
|
1531
|
+
})));
|
|
1532
|
+
}
|
|
1533
|
+
if (params.box) {
|
|
1534
|
+
// fal takes box corners, not an origin and a size.
|
|
1535
|
+
b.setFirst(SEGMENT_BOX_PROMPT_FIELDS, [
|
|
1536
|
+
{
|
|
1537
|
+
x_min: Math.round(params.box.x),
|
|
1538
|
+
y_min: Math.round(params.box.y),
|
|
1539
|
+
x_max: Math.round(params.box.x + params.box.width),
|
|
1540
|
+
y_max: Math.round(params.box.y + params.box.height)
|
|
1541
|
+
}
|
|
1542
|
+
]);
|
|
1543
|
+
}
|
|
1544
|
+
return b.args;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Refuse a call whose target nothing names.
|
|
1548
|
+
*
|
|
1549
|
+
* An endpoint that declares a prompt field segments only what a prompt points
|
|
1550
|
+
* at, and answers an unprompted call with either a 422 (`fal-ai/sam2/image`:
|
|
1551
|
+
* "Please provide at least one of prompts or box_prompts") or a silent empty
|
|
1552
|
+
* mask list (`fal-ai/sam-3-1/image`, which fills the absent `prompt` with its
|
|
1553
|
+
* own default). Both cost a request and neither says what to do about it.
|
|
1554
|
+
* Endpoints that declare no prompt field at all — sam2/auto-segment, birefnet
|
|
1555
|
+
* — find objects on their own and run unprompted.
|
|
1556
|
+
*/
|
|
1557
|
+
function assertSegmentPromptIsUsable(modelId, params) {
|
|
1558
|
+
if (params.prompt?.trim() || params.points?.length || params.box) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const b = new FalArgsBuilder(modelId);
|
|
1562
|
+
const takesAPrompt = [
|
|
1563
|
+
...SEGMENT_TEXT_PROMPT_FIELDS,
|
|
1564
|
+
...SEGMENT_POINT_PROMPT_FIELDS,
|
|
1565
|
+
...SEGMENT_BOX_PROMPT_FIELDS
|
|
1566
|
+
].some((field) => b.declares(field));
|
|
1567
|
+
if (!takesAPrompt) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
throw new Error(`${modelId} needs a prompt: name what to find (for example "the dog"), ` +
|
|
1571
|
+
"click a point, or drag a box. To split a layer with no prompt, pick a " +
|
|
1572
|
+
"model that finds objects on its own, such as fal-ai/sam2/auto-segment.");
|
|
1573
|
+
}
|
|
1574
|
+
/** Text-prompt field names across fal's segmentation endpoints, in priority order. */
|
|
1575
|
+
const SEGMENT_TEXT_PROMPT_FIELDS = [
|
|
1576
|
+
"prompt",
|
|
1577
|
+
"text_prompt",
|
|
1578
|
+
"text_input",
|
|
1579
|
+
"object"
|
|
1580
|
+
];
|
|
1581
|
+
const SEGMENT_POINT_PROMPT_FIELDS = ["point_prompts", "prompts"];
|
|
1582
|
+
const SEGMENT_BOX_PROMPT_FIELDS = ["box_prompts"];
|
|
1583
|
+
/**
|
|
1584
|
+
* Read one mask image.
|
|
1585
|
+
*
|
|
1586
|
+
* fal is inconsistent here: a `list[Image]` field usually holds `{url, width,
|
|
1587
|
+
* height}` objects, but the string-typed mask fields (`combined_mask`,
|
|
1588
|
+
* `mask_image`) hold a bare URL, and some endpoints nest the image one level
|
|
1589
|
+
* down. Accepting only the object form dropped whole responses on the floor.
|
|
1590
|
+
*/
|
|
1591
|
+
function asFalMaskImage(value) {
|
|
1592
|
+
if (isNonEmptyString(value)) {
|
|
1593
|
+
return { url: value, width: null, height: null };
|
|
1594
|
+
}
|
|
1595
|
+
if (!isRecord(value)) {
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
if (isNonEmptyString(value.url)) {
|
|
1599
|
+
return {
|
|
1600
|
+
url: value.url,
|
|
1601
|
+
width: finiteNumberOrNull(value.width),
|
|
1602
|
+
height: finiteNumberOrNull(value.height)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
return asFalMaskImage(value.image);
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* The mask images in a fal segmentation response, in the order the endpoints
|
|
1609
|
+
* prefer: every list field first, then the single-mask fields.
|
|
1610
|
+
*
|
|
1611
|
+
* Each endpoint names this differently — sam-3/3-1 answer with `masks`,
|
|
1612
|
+
* sam2/auto-segment with `individual_masks` plus a `combined_mask` URL, and
|
|
1613
|
+
* the single-mask endpoints (evf-sam, sam2/image, birefnet, the florence
|
|
1614
|
+
* variants) with one `image` or `mask_image`.
|
|
1615
|
+
*/
|
|
1616
|
+
function collectFalMaskImages(data) {
|
|
1617
|
+
for (const field of ["masks", "individual_masks", "images"]) {
|
|
1618
|
+
const value = data[field];
|
|
1619
|
+
if (Array.isArray(value)) {
|
|
1620
|
+
const masks = value
|
|
1621
|
+
.map(asFalMaskImage)
|
|
1622
|
+
.filter((mask) => mask !== null);
|
|
1623
|
+
if (masks.length > 0) {
|
|
1624
|
+
return masks;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
for (const field of ["image", "mask_image", "combined_mask", "mask"]) {
|
|
1629
|
+
const mask = asFalMaskImage(data[field]);
|
|
1630
|
+
if (mask) {
|
|
1631
|
+
return [mask];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return [];
|
|
1635
|
+
}
|
|
1636
|
+
/** A side-channel field fal sends as JSON text on some endpoints and as a value on others. */
|
|
1637
|
+
function parseFalJsonField(value) {
|
|
1638
|
+
if (!isString(value)) {
|
|
1639
|
+
return value;
|
|
1640
|
+
}
|
|
1641
|
+
try {
|
|
1642
|
+
return JSON.parse(value);
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
// Not JSON — the field carries an opaque string this parser has no use for.
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function finiteNumberOrNull(value) {
|
|
1650
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Convert one entry of fal's `boxes` output into source-image pixels. fal sends
|
|
1654
|
+
* normalized `[cx, cy, w, h]` (center and size, 0–1), which needs the mask's
|
|
1655
|
+
* own size to become a rectangle.
|
|
1656
|
+
*/
|
|
1657
|
+
function normalizeFalBox(value, width, height) {
|
|
1658
|
+
if (Array.isArray(value) && value.length >= 4) {
|
|
1659
|
+
const [cx, cy, w, h] = value.map(finiteNumberOrNull);
|
|
1660
|
+
if (cx == null || cy == null || w == null || h == null) {
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
if (width == null || height == null) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
return {
|
|
1667
|
+
x: Math.round((cx - w / 2) * width),
|
|
1668
|
+
y: Math.round((cy - h / 2) * height),
|
|
1669
|
+
width: Math.round(w * width),
|
|
1670
|
+
height: Math.round(h * height)
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
if (isRecord(value)) {
|
|
1674
|
+
const x = finiteNumberOrNull(value.x);
|
|
1675
|
+
const y = finiteNumberOrNull(value.y);
|
|
1676
|
+
const w = finiteNumberOrNull(value.width);
|
|
1677
|
+
const h = finiteNumberOrNull(value.height);
|
|
1678
|
+
if (x != null && y != null && w != null && h != null) {
|
|
1679
|
+
return { x, y, width: w, height: h };
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
/** The label and score fal reports per mask under `metadata`. */
|
|
1685
|
+
function falMaskMetadata(entries, index) {
|
|
1686
|
+
const list = parseFalJsonField(entries);
|
|
1687
|
+
const entry = Array.isArray(list) ? list[index] : index === 0 ? list : null;
|
|
1688
|
+
if (!isRecord(entry)) {
|
|
1689
|
+
return { label: null, score: null, box: null };
|
|
1690
|
+
}
|
|
1691
|
+
const label = isNonEmptyString(entry.label)
|
|
1692
|
+
? entry.label
|
|
1693
|
+
: isNonEmptyString(entry.name)
|
|
1694
|
+
? entry.name
|
|
1695
|
+
: null;
|
|
1696
|
+
return {
|
|
1697
|
+
label,
|
|
1698
|
+
score: finiteNumberOrNull(entry.score),
|
|
1699
|
+
box: entry.box ?? entry.bbox ?? entry.bounds ?? null
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
async function collectSegmentationMasks(data, minConfidence) {
|
|
1703
|
+
const rawMasks = collectFalMaskImages(data);
|
|
1704
|
+
const scores = parseFalJsonField(data.scores);
|
|
1705
|
+
const boxes = parseFalJsonField(data.boxes);
|
|
1706
|
+
const threshold = minConfidence ?? null;
|
|
1707
|
+
const masks = [];
|
|
1708
|
+
let bestScore = null;
|
|
1709
|
+
for (let i = 0; i < rawMasks.length; i++) {
|
|
1710
|
+
const { url, width, height } = rawMasks[i];
|
|
1711
|
+
const meta = falMaskMetadata(data.metadata, i);
|
|
1712
|
+
const confidence = meta.score ??
|
|
1713
|
+
(Array.isArray(scores) ? finiteNumberOrNull(scores[i]) : null);
|
|
1714
|
+
if (confidence != null && (bestScore == null || confidence > bestScore)) {
|
|
1715
|
+
bestScore = confidence;
|
|
1716
|
+
}
|
|
1717
|
+
if (threshold != null && confidence != null && confidence < threshold) {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
const box = normalizeFalBox(meta.box, width, height) ??
|
|
1721
|
+
(Array.isArray(boxes) ? normalizeFalBox(boxes[i], width, height) : null);
|
|
1722
|
+
masks.push({
|
|
1723
|
+
mask: await downloadBytes(url),
|
|
1724
|
+
mimeType: "image/png",
|
|
1725
|
+
width,
|
|
1726
|
+
height,
|
|
1727
|
+
label: meta.label,
|
|
1728
|
+
confidence,
|
|
1729
|
+
box
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
return { masks, decoded: rawMasks.length, bestScore };
|
|
1733
|
+
}
|
|
1734
|
+
export function extractImageUrl(result) {
|
|
418
1735
|
return extractImageUrls(result)[0];
|
|
419
1736
|
}
|
|
420
|
-
function extractVideoUrl(result) {
|
|
1737
|
+
export function extractVideoUrl(result) {
|
|
421
1738
|
// FAL video endpoints return { video: { url } } or { video_url } or { url }
|
|
422
1739
|
const video = result.video;
|
|
423
1740
|
if (video?.url)
|
|
424
1741
|
return video.url;
|
|
425
|
-
if (
|
|
1742
|
+
if (isString(result.video_url))
|
|
426
1743
|
return result.video_url;
|
|
427
|
-
if (
|
|
1744
|
+
if (isString(result.url))
|
|
428
1745
|
return result.url;
|
|
429
1746
|
// Some endpoints return an array of videos
|
|
430
1747
|
const videos = result.videos;
|
|
@@ -435,8 +1752,23 @@ function extractVideoUrl(result) {
|
|
|
435
1752
|
}
|
|
436
1753
|
throw new Error(`Unexpected FAL video response: ${JSON.stringify(result)}`);
|
|
437
1754
|
}
|
|
438
|
-
|
|
439
|
-
|
|
1755
|
+
export function extractAudioUrl(result) {
|
|
1756
|
+
// FAL TTS endpoints return { audio: { url } } or { audio: "url" } or
|
|
1757
|
+
// { audio_url } or a bare { url }.
|
|
1758
|
+
const audio = result.audio;
|
|
1759
|
+
if (isObjectLike(audio) && isString(audio.url)) {
|
|
1760
|
+
return audio.url;
|
|
1761
|
+
}
|
|
1762
|
+
if (isString(audio))
|
|
1763
|
+
return audio;
|
|
1764
|
+
if (isString(result.audio_url))
|
|
1765
|
+
return result.audio_url;
|
|
1766
|
+
if (isString(result.url))
|
|
1767
|
+
return result.url;
|
|
1768
|
+
throw new Error(`Unexpected FAL audio response: ${JSON.stringify(result)}`);
|
|
1769
|
+
}
|
|
1770
|
+
async function downloadBytes(url, signal) {
|
|
1771
|
+
const res = await safeFetch(url, signal ? { signal } : undefined);
|
|
440
1772
|
if (!res.ok)
|
|
441
1773
|
throw new Error(`Failed to download FAL result: ${res.status}`);
|
|
442
1774
|
const buf = await res.arrayBuffer();
|