@nodetool-ai/runtime 0.7.0-rc.23 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/agent-memory.d.ts +118 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +186 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/async-local-storage.d.ts +20 -0
- package/dist/async-local-storage.d.ts.map +1 -0
- package/dist/async-local-storage.js +28 -0
- package/dist/async-local-storage.js.map +1 -0
- package/dist/blender-executor.d.ts +93 -0
- package/dist/blender-executor.d.ts.map +1 -0
- package/dist/blender-executor.js +294 -0
- package/dist/blender-executor.js.map +1 -0
- package/dist/comfy-executor.d.ts +23 -1
- package/dist/comfy-executor.d.ts.map +1 -1
- package/dist/comfy-executor.js +234 -55
- package/dist/comfy-executor.js.map +1 -1
- package/dist/context-packer.d.ts +1 -1
- package/dist/context-packer.d.ts.map +1 -1
- package/dist/context-packer.js +23 -25
- package/dist/context-packer.js.map +1 -1
- package/dist/context.d.ts +954 -127
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2424 -513
- package/dist/context.js.map +1 -1
- package/dist/cost-reconciler.d.ts +32 -0
- package/dist/cost-reconciler.d.ts.map +1 -0
- package/dist/cost-reconciler.js +21 -0
- package/dist/cost-reconciler.js.map +1 -0
- package/dist/entities.d.ts +33 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +43 -0
- package/dist/entities.js.map +1 -0
- package/dist/external-media-fetch.d.ts +29 -0
- package/dist/external-media-fetch.d.ts.map +1 -0
- package/dist/external-media-fetch.js +38 -0
- package/dist/external-media-fetch.js.map +1 -0
- package/dist/generation-receipt.d.ts +86 -0
- package/dist/generation-receipt.d.ts.map +1 -0
- package/dist/generation-receipt.js +137 -0
- package/dist/generation-receipt.js.map +1 -0
- package/dist/generation-registry.d.ts +67 -0
- package/dist/generation-registry.d.ts.map +1 -0
- package/dist/generation-registry.js +126 -0
- package/dist/generation-registry.js.map +1 -0
- package/dist/google/client.d.ts +196 -0
- package/dist/google/client.d.ts.map +1 -0
- package/dist/google/client.js +396 -0
- package/dist/google/client.js.map +1 -0
- package/dist/google/index.d.ts +3 -0
- package/dist/google/index.d.ts.map +1 -0
- package/dist/google/index.js +3 -0
- package/dist/google/index.js.map +1 -0
- package/dist/google/token.d.ts +15 -0
- package/dist/google/token.d.ts.map +1 -0
- package/dist/google/token.js +24 -0
- package/dist/google/token.js.map +1 -0
- package/dist/host-binaries.d.ts +106 -0
- package/dist/host-binaries.d.ts.map +1 -0
- package/dist/host-binaries.js +344 -0
- package/dist/host-binaries.js.map +1 -0
- package/dist/image-codec.d.ts +74 -0
- package/dist/image-codec.d.ts.map +1 -0
- package/dist/image-codec.js +273 -0
- package/dist/image-codec.js.map +1 -0
- package/dist/index.d.ts +56 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -4
- package/dist/index.js.map +1 -1
- package/dist/invocation-account.d.ts +43 -0
- package/dist/invocation-account.d.ts.map +1 -0
- package/dist/invocation-account.js +60 -0
- package/dist/invocation-account.js.map +1 -0
- package/dist/media-ref-bytes.d.ts +19 -0
- package/dist/media-ref-bytes.d.ts.map +1 -0
- package/dist/media-ref-bytes.js +181 -0
- package/dist/media-ref-bytes.js.map +1 -0
- package/dist/node-executor.d.ts +113 -2
- package/dist/node-executor.d.ts.map +1 -1
- package/dist/permission-gate.d.ts +152 -0
- package/dist/permission-gate.d.ts.map +1 -0
- package/dist/permission-gate.js +87 -0
- package/dist/permission-gate.js.map +1 -0
- package/dist/prefixed-storage-adapter.d.ts +33 -0
- package/dist/prefixed-storage-adapter.d.ts.map +1 -0
- package/dist/prefixed-storage-adapter.js +70 -0
- package/dist/prefixed-storage-adapter.js.map +1 -0
- package/dist/prompt-asset-refs.d.ts +222 -0
- package/dist/prompt-asset-refs.d.ts.map +1 -0
- package/dist/prompt-asset-refs.js +751 -0
- package/dist/prompt-asset-refs.js.map +1 -0
- package/dist/provider-cache.d.ts +3 -0
- package/dist/provider-cache.d.ts.map +1 -0
- package/dist/provider-cache.js +8 -0
- package/dist/provider-cache.js.map +1 -0
- package/dist/providers/aki-provider.d.ts +33 -10
- package/dist/providers/aki-provider.d.ts.map +1 -1
- package/dist/providers/aki-provider.js +84 -77
- package/dist/providers/aki-provider.js.map +1 -1
- package/dist/providers/alibaba-provider.d.ts +39 -0
- package/dist/providers/alibaba-provider.d.ts.map +1 -0
- package/dist/providers/alibaba-provider.js +86 -0
- package/dist/providers/alibaba-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +52 -3
- package/dist/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/providers/anthropic-provider.js +1020 -338
- package/dist/providers/anthropic-provider.js.map +1 -1
- package/dist/providers/atlascloud-provider.d.ts +101 -0
- package/dist/providers/atlascloud-provider.d.ts.map +1 -0
- package/dist/providers/atlascloud-provider.js +1009 -0
- package/dist/providers/atlascloud-provider.js.map +1 -0
- package/dist/providers/atlascloud-transport.d.ts +100 -0
- package/dist/providers/atlascloud-transport.d.ts.map +1 -0
- package/dist/providers/atlascloud-transport.js +364 -0
- package/dist/providers/atlascloud-transport.js.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts +35 -0
- package/dist/providers/atlascloud-webhook-registry.d.ts.map +1 -0
- package/dist/providers/atlascloud-webhook-registry.js +104 -0
- package/dist/providers/atlascloud-webhook-registry.js.map +1 -0
- package/dist/providers/audio-mime.d.ts +11 -0
- package/dist/providers/audio-mime.d.ts.map +1 -0
- package/dist/providers/audio-mime.js +46 -0
- package/dist/providers/audio-mime.js.map +1 -0
- package/dist/providers/base-provider.d.ts +481 -24
- package/dist/providers/base-provider.d.ts.map +1 -1
- package/dist/providers/base-provider.js +1259 -42
- package/dist/providers/base-provider.js.map +1 -1
- package/dist/providers/cassette-provider.d.ts +217 -0
- package/dist/providers/cassette-provider.d.ts.map +1 -0
- package/dist/providers/cassette-provider.js +502 -0
- package/dist/providers/cassette-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +6 -12
- package/dist/providers/cerebras-provider.d.ts.map +1 -1
- package/dist/providers/cerebras-provider.js +8 -33
- package/dist/providers/cerebras-provider.js.map +1 -1
- package/dist/providers/claude-agent-provider.d.ts +131 -68
- package/dist/providers/claude-agent-provider.d.ts.map +1 -1
- package/dist/providers/claude-agent-provider.js +1095 -515
- package/dist/providers/claude-agent-provider.js.map +1 -1
- package/dist/providers/codex-provider.d.ts +82 -0
- package/dist/providers/codex-provider.d.ts.map +1 -0
- package/dist/providers/codex-provider.js +492 -0
- package/dist/providers/codex-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +5 -2
- package/dist/providers/cohere-provider.d.ts.map +1 -1
- package/dist/providers/cohere-provider.js +9 -2
- package/dist/providers/cohere-provider.js.map +1 -1
- package/dist/providers/comfy-cloud-provider.d.ts +30 -0
- package/dist/providers/comfy-cloud-provider.d.ts.map +1 -0
- package/dist/providers/comfy-cloud-provider.js +42 -0
- package/dist/providers/comfy-cloud-provider.js.map +1 -0
- package/dist/providers/content-filter.d.ts +46 -0
- package/dist/providers/content-filter.d.ts.map +1 -0
- package/dist/providers/content-filter.js +92 -0
- package/dist/providers/content-filter.js.map +1 -0
- package/dist/providers/context-exceeded.d.ts +37 -0
- package/dist/providers/context-exceeded.d.ts.map +1 -0
- package/dist/providers/context-exceeded.js +114 -0
- package/dist/providers/context-exceeded.js.map +1 -0
- package/dist/providers/context-window.d.ts +44 -0
- package/dist/providers/context-window.d.ts.map +1 -0
- package/dist/providers/context-window.js +145 -0
- package/dist/providers/context-window.js.map +1 -0
- package/dist/providers/contract/index.d.ts +4 -0
- package/dist/providers/contract/index.d.ts.map +1 -0
- package/dist/providers/contract/index.js +4 -0
- package/dist/providers/contract/index.js.map +1 -0
- package/dist/providers/contract/probe-manifest.d.ts +75 -0
- package/dist/providers/contract/probe-manifest.d.ts.map +1 -0
- package/dist/providers/contract/probe-manifest.js +379 -0
- package/dist/providers/contract/probe-manifest.js.map +1 -0
- package/dist/providers/contract/probe-runner.d.ts +57 -0
- package/dist/providers/contract/probe-runner.d.ts.map +1 -0
- package/dist/providers/contract/probe-runner.js +180 -0
- package/dist/providers/contract/probe-runner.js.map +1 -0
- package/dist/providers/contract/redact.d.ts +29 -0
- package/dist/providers/contract/redact.d.ts.map +1 -0
- package/dist/providers/contract/redact.js +113 -0
- package/dist/providers/contract/redact.js.map +1 -0
- package/dist/providers/core-tools.d.ts +74 -0
- package/dist/providers/core-tools.d.ts.map +1 -0
- package/dist/providers/core-tools.js +127 -0
- package/dist/providers/core-tools.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +69 -50
- package/dist/providers/cost-calculator.d.ts.map +1 -1
- package/dist/providers/cost-calculator.js +207 -397
- package/dist/providers/cost-calculator.js.map +1 -1
- package/dist/providers/credential-check.d.ts +45 -0
- package/dist/providers/credential-check.d.ts.map +1 -0
- package/dist/providers/credential-check.js +159 -0
- package/dist/providers/credential-check.js.map +1 -0
- package/dist/providers/custom-model-kinds.d.ts +33 -0
- package/dist/providers/custom-model-kinds.d.ts.map +1 -0
- package/dist/providers/custom-model-kinds.js +96 -0
- package/dist/providers/custom-model-kinds.js.map +1 -0
- package/dist/providers/custom-openai-provider.d.ts +81 -0
- package/dist/providers/custom-openai-provider.d.ts.map +1 -0
- package/dist/providers/custom-openai-provider.js +182 -0
- package/dist/providers/custom-openai-provider.js.map +1 -0
- package/dist/providers/custom-provider-registry.d.ts +19 -0
- package/dist/providers/custom-provider-registry.d.ts.map +1 -0
- package/dist/providers/custom-provider-registry.js +52 -0
- package/dist/providers/custom-provider-registry.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +8 -14
- package/dist/providers/deepseek-provider.d.ts.map +1 -1
- package/dist/providers/deepseek-provider.js +6 -35
- package/dist/providers/deepseek-provider.js.map +1 -1
- package/dist/providers/defaults.d.ts +12 -0
- package/dist/providers/defaults.d.ts.map +1 -0
- package/dist/providers/defaults.js +12 -0
- package/dist/providers/defaults.js.map +1 -0
- package/dist/providers/elevenlabs-provider.d.ts +49 -0
- package/dist/providers/elevenlabs-provider.d.ts.map +1 -0
- package/dist/providers/elevenlabs-provider.js +142 -0
- package/dist/providers/elevenlabs-provider.js.map +1 -0
- package/dist/providers/entity-references.d.ts +31 -0
- package/dist/providers/entity-references.d.ts.map +1 -0
- package/dist/providers/entity-references.js +113 -0
- package/dist/providers/entity-references.js.map +1 -0
- package/dist/providers/evolink-provider.d.ts +38 -0
- package/dist/providers/evolink-provider.d.ts.map +1 -0
- package/dist/providers/evolink-provider.js +304 -0
- package/dist/providers/evolink-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +72 -1
- package/dist/providers/fake-provider.d.ts.map +1 -1
- package/dist/providers/fake-provider.js +203 -1
- package/dist/providers/fake-provider.js.map +1 -1
- package/dist/providers/fal-generations.d.ts +106 -0
- package/dist/providers/fal-generations.d.ts.map +1 -0
- package/dist/providers/fal-generations.js +286 -0
- package/dist/providers/fal-generations.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +171 -7
- package/dist/providers/fal-provider.d.ts.map +1 -1
- package/dist/providers/fal-provider.js +1369 -64
- package/dist/providers/fal-provider.js.map +1 -1
- package/dist/providers/fal-queue.d.ts +47 -0
- package/dist/providers/fal-queue.d.ts.map +1 -0
- package/dist/providers/fal-queue.js +231 -0
- package/dist/providers/fal-queue.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +148 -8
- package/dist/providers/gemini-provider.d.ts.map +1 -1
- package/dist/providers/gemini-provider.js +1500 -316
- package/dist/providers/gemini-provider.js.map +1 -1
- package/dist/providers/gmi-provider.d.ts +21 -0
- package/dist/providers/gmi-provider.d.ts.map +1 -0
- package/dist/providers/gmi-provider.js +34 -0
- package/dist/providers/gmi-provider.js.map +1 -0
- package/dist/providers/gpt-image-size.d.ts +3 -0
- package/dist/providers/gpt-image-size.d.ts.map +1 -0
- package/dist/providers/gpt-image-size.js +37 -0
- package/dist/providers/gpt-image-size.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +9 -12
- package/dist/providers/groq-provider.d.ts.map +1 -1
- package/dist/providers/groq-provider.js +19 -33
- package/dist/providers/groq-provider.js.map +1 -1
- package/dist/providers/groq-request.d.ts +38 -0
- package/dist/providers/groq-request.d.ts.map +1 -0
- package/dist/providers/groq-request.js +138 -0
- package/dist/providers/groq-request.js.map +1 -0
- package/dist/providers/higgsfield-provider.d.ts +30 -0
- package/dist/providers/higgsfield-provider.d.ts.map +1 -0
- package/dist/providers/higgsfield-provider.js +131 -0
- package/dist/providers/higgsfield-provider.js.map +1 -0
- package/dist/providers/higgsfield-transport.d.ts +106 -0
- package/dist/providers/higgsfield-transport.d.ts.map +1 -0
- package/dist/providers/higgsfield-transport.js +182 -0
- package/dist/providers/higgsfield-transport.js.map +1 -0
- package/dist/providers/http-transport.d.ts +74 -0
- package/dist/providers/http-transport.d.ts.map +1 -0
- package/dist/providers/http-transport.js +152 -0
- package/dist/providers/http-transport.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +71 -3
- package/dist/providers/huggingface-provider.d.ts.map +1 -1
- package/dist/providers/huggingface-provider.js +230 -34
- package/dist/providers/huggingface-provider.js.map +1 -1
- package/dist/providers/image-mime.d.ts +39 -0
- package/dist/providers/image-mime.d.ts.map +1 -0
- package/dist/providers/image-mime.js +111 -0
- package/dist/providers/image-mime.js.map +1 -0
- package/dist/providers/image-ref.d.ts +21 -0
- package/dist/providers/image-ref.d.ts.map +1 -0
- package/dist/providers/image-ref.js +44 -0
- package/dist/providers/image-ref.js.map +1 -0
- package/dist/providers/index.d.ts +84 -8
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +239 -56
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/jina-provider.d.ts +10 -2
- package/dist/providers/jina-provider.d.ts.map +1 -1
- package/dist/providers/jina-provider.js +16 -7
- package/dist/providers/jina-provider.js.map +1 -1
- package/dist/providers/kie-provider.d.ts +170 -9
- package/dist/providers/kie-provider.d.ts.map +1 -1
- package/dist/providers/kie-provider.js +1176 -228
- package/dist/providers/kie-provider.js.map +1 -1
- package/dist/providers/kie-webhook-registry.d.ts +28 -0
- package/dist/providers/kie-webhook-registry.d.ts.map +1 -0
- package/dist/providers/kie-webhook-registry.js +91 -0
- package/dist/providers/kie-webhook-registry.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +21 -37
- package/dist/providers/llama-provider.d.ts.map +1 -1
- package/dist/providers/llama-provider.js +43 -354
- package/dist/providers/llama-provider.js.map +1 -1
- package/dist/providers/lmstudio-provider.d.ts +4 -7
- package/dist/providers/lmstudio-provider.d.ts.map +1 -1
- package/dist/providers/lmstudio-provider.js +24 -15
- package/dist/providers/lmstudio-provider.js.map +1 -1
- package/dist/providers/manifest-models.d.ts +341 -1
- package/dist/providers/manifest-models.d.ts.map +1 -1
- package/dist/providers/manifest-models.js +1106 -22
- package/dist/providers/manifest-models.js.map +1 -1
- package/dist/providers/media-mime.d.ts +14 -0
- package/dist/providers/media-mime.d.ts.map +1 -0
- package/dist/providers/media-mime.js +35 -0
- package/dist/providers/media-mime.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +14 -1
- package/dist/providers/meshy-provider.d.ts.map +1 -1
- package/dist/providers/meshy-provider.js +76 -46
- package/dist/providers/meshy-provider.js.map +1 -1
- package/dist/providers/meta-provider.d.ts +22 -0
- package/dist/providers/meta-provider.d.ts.map +1 -0
- package/dist/providers/meta-provider.js +54 -0
- package/dist/providers/meta-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +25 -18
- package/dist/providers/minimax-provider.d.ts.map +1 -1
- package/dist/providers/minimax-provider.js +211 -72
- package/dist/providers/minimax-provider.js.map +1 -1
- package/dist/providers/mistral-provider.d.ts +6 -12
- package/dist/providers/mistral-provider.d.ts.map +1 -1
- package/dist/providers/mistral-provider.js +10 -34
- package/dist/providers/mistral-provider.js.map +1 -1
- package/dist/providers/moonshot-provider.d.ts +11 -15
- package/dist/providers/moonshot-provider.d.ts.map +1 -1
- package/dist/providers/moonshot-provider.js +33 -23
- package/dist/providers/moonshot-provider.js.map +1 -1
- package/dist/providers/node-llama-cpp-provider.d.ts +98 -0
- package/dist/providers/node-llama-cpp-provider.d.ts.map +1 -0
- package/dist/providers/node-llama-cpp-provider.js +479 -0
- package/dist/providers/node-llama-cpp-provider.js.map +1 -0
- package/dist/providers/nodetool-provider.d.ts +49 -0
- package/dist/providers/nodetool-provider.d.ts.map +1 -0
- package/dist/providers/nodetool-provider.js +224 -0
- package/dist/providers/nodetool-provider.js.map +1 -0
- package/dist/providers/oauth/browser-launcher.d.ts +23 -0
- package/dist/providers/oauth/browser-launcher.d.ts.map +1 -0
- package/dist/providers/oauth/browser-launcher.js +45 -0
- package/dist/providers/oauth/browser-launcher.js.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts +74 -0
- package/dist/providers/oauth/claude-code-credentials.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-credentials.js +133 -0
- package/dist/providers/oauth/claude-code-credentials.js.map +1 -0
- package/dist/providers/oauth/claude-code-login.d.ts +124 -0
- package/dist/providers/oauth/claude-code-login.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-login.js +230 -0
- package/dist/providers/oauth/claude-code-login.js.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts +111 -0
- package/dist/providers/oauth/claude-code-oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/claude-code-oauth-client.js +207 -0
- package/dist/providers/oauth/claude-code-oauth-client.js.map +1 -0
- package/dist/providers/oauth/errors.d.ts +54 -0
- package/dist/providers/oauth/errors.d.ts.map +1 -0
- package/dist/providers/oauth/errors.js +79 -0
- package/dist/providers/oauth/errors.js.map +1 -0
- package/dist/providers/oauth/index.d.ts +105 -0
- package/dist/providers/oauth/index.d.ts.map +1 -0
- package/dist/providers/oauth/index.js +167 -0
- package/dist/providers/oauth/index.js.map +1 -0
- package/dist/providers/oauth/jwt-claims.d.ts +23 -0
- package/dist/providers/oauth/jwt-claims.d.ts.map +1 -0
- package/dist/providers/oauth/jwt-claims.js +67 -0
- package/dist/providers/oauth/jwt-claims.js.map +1 -0
- package/dist/providers/oauth/local-callback-server.d.ts +60 -0
- package/dist/providers/oauth/local-callback-server.d.ts.map +1 -0
- package/dist/providers/oauth/local-callback-server.js +148 -0
- package/dist/providers/oauth/local-callback-server.js.map +1 -0
- package/dist/providers/oauth/oauth-client.d.ts +55 -0
- package/dist/providers/oauth/oauth-client.d.ts.map +1 -0
- package/dist/providers/oauth/oauth-client.js +161 -0
- package/dist/providers/oauth/oauth-client.js.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts +129 -0
- package/dist/providers/oauth/openai-oauth-provider.d.ts.map +1 -0
- package/dist/providers/oauth/openai-oauth-provider.js +265 -0
- package/dist/providers/oauth/openai-oauth-provider.js.map +1 -0
- package/dist/providers/oauth/pkce-helper.d.ts +43 -0
- package/dist/providers/oauth/pkce-helper.d.ts.map +1 -0
- package/dist/providers/oauth/pkce-helper.js +57 -0
- package/dist/providers/oauth/pkce-helper.js.map +1 -0
- package/dist/providers/oauth/redaction.d.ts +22 -0
- package/dist/providers/oauth/redaction.d.ts.map +1 -0
- package/dist/providers/oauth/redaction.js +59 -0
- package/dist/providers/oauth/redaction.js.map +1 -0
- package/dist/providers/oauth/secure-credential-store.d.ts +67 -0
- package/dist/providers/oauth/secure-credential-store.d.ts.map +1 -0
- package/dist/providers/oauth/secure-credential-store.js +73 -0
- package/dist/providers/oauth/secure-credential-store.js.map +1 -0
- package/dist/providers/oauth/token-store.d.ts +53 -0
- package/dist/providers/oauth/token-store.d.ts.map +1 -0
- package/dist/providers/oauth/token-store.js +78 -0
- package/dist/providers/oauth/token-store.js.map +1 -0
- package/dist/providers/oauth/types.d.ts +64 -0
- package/dist/providers/oauth/types.d.ts.map +1 -0
- package/dist/providers/oauth/types.js +12 -0
- package/dist/providers/oauth/types.js.map +1 -0
- package/dist/providers/object-tracking.d.ts +47 -0
- package/dist/providers/object-tracking.d.ts.map +1 -0
- package/dist/providers/object-tracking.js +46 -0
- package/dist/providers/object-tracking.js.map +1 -0
- package/dist/providers/offline-model-index.d.ts +20 -0
- package/dist/providers/offline-model-index.d.ts.map +1 -0
- package/dist/providers/offline-model-index.js +112 -0
- package/dist/providers/offline-model-index.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +28 -1
- package/dist/providers/ollama-provider.d.ts.map +1 -1
- package/dist/providers/ollama-provider.js +193 -43
- package/dist/providers/ollama-provider.js.map +1 -1
- package/dist/providers/openai-compat/client.d.ts +57 -0
- package/dist/providers/openai-compat/client.d.ts.map +1 -0
- package/dist/providers/openai-compat/client.js +157 -0
- package/dist/providers/openai-compat/client.js.map +1 -0
- package/dist/providers/openai-compat/decode.d.ts +36 -0
- package/dist/providers/openai-compat/decode.d.ts.map +1 -0
- package/dist/providers/openai-compat/decode.js +55 -0
- package/dist/providers/openai-compat/decode.js.map +1 -0
- package/dist/providers/openai-compat/errors.d.ts +54 -0
- package/dist/providers/openai-compat/errors.d.ts.map +1 -0
- package/dist/providers/openai-compat/errors.js +144 -0
- package/dist/providers/openai-compat/errors.js.map +1 -0
- package/dist/providers/openai-compat/index.d.ts +8 -0
- package/dist/providers/openai-compat/index.d.ts.map +1 -0
- package/dist/providers/openai-compat/index.js +5 -0
- package/dist/providers/openai-compat/index.js.map +1 -0
- package/dist/providers/openai-compat/sse.d.ts +14 -0
- package/dist/providers/openai-compat/sse.d.ts.map +1 -0
- package/dist/providers/openai-compat/sse.js +69 -0
- package/dist/providers/openai-compat/sse.js.map +1 -0
- package/dist/providers/openai-compat/types.d.ts +75 -0
- package/dist/providers/openai-compat/types.d.ts.map +1 -0
- package/dist/providers/openai-compat/types.js +9 -0
- package/dist/providers/openai-compat/types.js.map +1 -0
- package/dist/providers/openai-compat-provider.d.ts +90 -0
- package/dist/providers/openai-compat-provider.d.ts.map +1 -0
- package/dist/providers/openai-compat-provider.js +282 -0
- package/dist/providers/openai-compat-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +89 -26
- package/dist/providers/openai-provider.d.ts.map +1 -1
- package/dist/providers/openai-provider.js +990 -105
- package/dist/providers/openai-provider.js.map +1 -1
- package/dist/providers/openrouter-provider.d.ts +6 -12
- package/dist/providers/openrouter-provider.d.ts.map +1 -1
- package/dist/providers/openrouter-provider.js +24 -53
- package/dist/providers/openrouter-provider.js.map +1 -1
- package/dist/providers/provider-call-failure.d.ts +39 -0
- package/dist/providers/provider-call-failure.d.ts.map +1 -0
- package/dist/providers/provider-call-failure.js +132 -0
- package/dist/providers/provider-call-failure.js.map +1 -0
- package/dist/providers/provider-error.d.ts +59 -0
- package/dist/providers/provider-error.d.ts.map +1 -0
- package/dist/providers/provider-error.js +165 -0
- package/dist/providers/provider-error.js.map +1 -0
- package/dist/providers/provider-generations.d.ts +99 -0
- package/dist/providers/provider-generations.d.ts.map +1 -0
- package/dist/providers/provider-generations.js +71 -0
- package/dist/providers/provider-generations.js.map +1 -0
- package/dist/providers/provider-queue.d.ts +62 -0
- package/dist/providers/provider-queue.d.ts.map +1 -0
- package/dist/providers/provider-queue.js +28 -0
- package/dist/providers/provider-queue.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +33 -14
- package/dist/providers/provider-registry.d.ts.map +1 -1
- package/dist/providers/provider-registry.js +85 -68
- package/dist/providers/provider-registry.js.map +1 -1
- package/dist/providers/provider-request-log.d.ts +53 -0
- package/dist/providers/provider-request-log.d.ts.map +1 -0
- package/dist/providers/provider-request-log.js +157 -0
- package/dist/providers/provider-request-log.js.map +1 -0
- package/dist/providers/provider-session.d.ts +3 -0
- package/dist/providers/provider-session.d.ts.map +1 -0
- package/dist/providers/provider-session.js +10 -0
- package/dist/providers/provider-session.js.map +1 -0
- package/dist/providers/provider-transport.d.ts +11 -0
- package/dist/providers/provider-transport.d.ts.map +1 -0
- package/dist/providers/provider-transport.js +11 -0
- package/dist/providers/provider-transport.js.map +1 -0
- package/dist/providers/python-provider.d.ts +20 -12
- package/dist/providers/python-provider.d.ts.map +1 -1
- package/dist/providers/python-provider.js +185 -25
- package/dist/providers/python-provider.js.map +1 -1
- package/dist/providers/replicate-provider.d.ts +89 -7
- package/dist/providers/replicate-provider.d.ts.map +1 -1
- package/dist/providers/replicate-provider.js +801 -65
- package/dist/providers/replicate-provider.js.map +1 -1
- package/dist/providers/replicate-retry.d.ts +30 -0
- package/dist/providers/replicate-retry.d.ts.map +1 -0
- package/dist/providers/replicate-retry.js +97 -0
- package/dist/providers/replicate-retry.js.map +1 -0
- package/dist/providers/responses-api.d.ts +27 -0
- package/dist/providers/responses-api.d.ts.map +1 -0
- package/dist/providers/responses-api.js +297 -0
- package/dist/providers/responses-api.js.map +1 -0
- package/dist/providers/reve-provider.d.ts +45 -0
- package/dist/providers/reve-provider.d.ts.map +1 -0
- package/dist/providers/reve-provider.js +159 -0
- package/dist/providers/reve-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +23 -1
- package/dist/providers/rodin-provider.d.ts.map +1 -1
- package/dist/providers/rodin-provider.js +89 -51
- package/dist/providers/rodin-provider.js.map +1 -1
- package/dist/providers/safe-url.d.ts +70 -0
- package/dist/providers/safe-url.d.ts.map +1 -0
- package/dist/providers/safe-url.js +277 -0
- package/dist/providers/safe-url.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +9 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -1
- package/dist/providers/scripted-provider.js +86 -10
- package/dist/providers/scripted-provider.js.map +1 -1
- package/dist/providers/structured-output.d.ts +42 -0
- package/dist/providers/structured-output.d.ts.map +1 -0
- package/dist/providers/structured-output.js +83 -0
- package/dist/providers/structured-output.js.map +1 -0
- package/dist/providers/together-provider.d.ts +19 -16
- package/dist/providers/together-provider.d.ts.map +1 -1
- package/dist/providers/together-provider.js +103 -225
- package/dist/providers/together-provider.js.map +1 -1
- package/dist/providers/topaz-provider.d.ts +44 -0
- package/dist/providers/topaz-provider.d.ts.map +1 -0
- package/dist/providers/topaz-provider.js +239 -0
- package/dist/providers/topaz-provider.js.map +1 -0
- package/dist/providers/types.d.ts +603 -4
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js +81 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/providers/useapi-provider.d.ts +27 -0
- package/dist/providers/useapi-provider.d.ts.map +1 -0
- package/dist/providers/useapi-provider.js +266 -0
- package/dist/providers/useapi-provider.js.map +1 -0
- package/dist/providers/video-duration.d.ts +6 -0
- package/dist/providers/video-duration.d.ts.map +1 -0
- package/dist/providers/video-duration.js +63 -0
- package/dist/providers/video-duration.js.map +1 -0
- package/dist/providers/video-frame-fallback.d.ts +39 -0
- package/dist/providers/video-frame-fallback.d.ts.map +1 -0
- package/dist/providers/video-frame-fallback.js +155 -0
- package/dist/providers/video-frame-fallback.js.map +1 -0
- package/dist/providers/video-frames.d.ts +61 -0
- package/dist/providers/video-frames.d.ts.map +1 -0
- package/dist/providers/video-frames.js +208 -0
- package/dist/providers/video-frames.js.map +1 -0
- package/dist/providers/video-mime.d.ts +14 -0
- package/dist/providers/video-mime.d.ts.map +1 -0
- package/dist/providers/video-mime.js +44 -0
- package/dist/providers/video-mime.js.map +1 -0
- package/dist/providers/video-to-audio.d.ts +34 -0
- package/dist/providers/video-to-audio.d.ts.map +1 -0
- package/dist/providers/video-to-audio.js +62 -0
- package/dist/providers/video-to-audio.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -8
- package/dist/providers/vllm-provider.d.ts.map +1 -1
- package/dist/providers/vllm-provider.js +48 -23
- package/dist/providers/vllm-provider.js.map +1 -1
- package/dist/providers/voyage-provider.d.ts +10 -2
- package/dist/providers/voyage-provider.d.ts.map +1 -1
- package/dist/providers/voyage-provider.js +16 -7
- package/dist/providers/voyage-provider.js.map +1 -1
- package/dist/providers/xai-provider.d.ts +54 -13
- package/dist/providers/xai-provider.d.ts.map +1 -1
- package/dist/providers/xai-provider.js +299 -22
- package/dist/providers/xai-provider.js.map +1 -1
- package/dist/python-bridge-base.d.ts +358 -0
- package/dist/python-bridge-base.d.ts.map +1 -0
- package/dist/python-bridge-base.js +1557 -0
- package/dist/python-bridge-base.js.map +1 -0
- package/dist/python-bridge-codec.d.ts +28 -0
- package/dist/python-bridge-codec.d.ts.map +1 -0
- package/dist/python-bridge-codec.js +39 -0
- package/dist/python-bridge-codec.js.map +1 -0
- package/dist/python-bridge-factory.d.ts +52 -0
- package/dist/python-bridge-factory.d.ts.map +1 -0
- package/dist/python-bridge-factory.js +69 -0
- package/dist/python-bridge-factory.js.map +1 -0
- package/dist/python-bridge-framing.d.ts +68 -0
- package/dist/python-bridge-framing.d.ts.map +1 -0
- package/dist/python-bridge-framing.js +91 -0
- package/dist/python-bridge-framing.js.map +1 -0
- package/dist/python-bridge-types.d.ts +545 -2
- package/dist/python-bridge-types.d.ts.map +1 -1
- package/dist/python-bridge-types.js +49 -1
- package/dist/python-bridge-types.js.map +1 -1
- package/dist/python-graph-resolver.d.ts +57 -0
- package/dist/python-graph-resolver.d.ts.map +1 -0
- package/dist/python-graph-resolver.js +79 -0
- package/dist/python-graph-resolver.js.map +1 -0
- package/dist/python-node-executor.d.ts +36 -4
- package/dist/python-node-executor.d.ts.map +1 -1
- package/dist/python-node-executor.js +177 -86
- package/dist/python-node-executor.js.map +1 -1
- package/dist/python-stdio-bridge.d.ts +18 -37
- package/dist/python-stdio-bridge.d.ts.map +1 -1
- package/dist/python-stdio-bridge.js +148 -475
- package/dist/python-stdio-bridge.js.map +1 -1
- package/dist/python-websocket-bridge.d.ts +140 -0
- package/dist/python-websocket-bridge.d.ts.map +1 -0
- package/dist/python-websocket-bridge.js +561 -0
- package/dist/python-websocket-bridge.js.map +1 -0
- package/dist/python-worker-stderr.d.ts +4 -0
- package/dist/python-worker-stderr.d.ts.map +1 -0
- package/dist/python-worker-stderr.js +24 -0
- package/dist/python-worker-stderr.js.map +1 -0
- package/dist/recommended-models.d.ts +2 -2
- package/dist/recommended-models.d.ts.map +1 -1
- package/dist/recommended-models.js +24 -4
- package/dist/recommended-models.js.map +1 -1
- package/dist/recoverable-node-error.d.ts +31 -0
- package/dist/recoverable-node-error.d.ts.map +1 -0
- package/dist/recoverable-node-error.js +37 -0
- package/dist/recoverable-node-error.js.map +1 -0
- package/dist/redact-params.d.ts +13 -0
- package/dist/redact-params.d.ts.map +1 -0
- package/dist/redact-params.js +57 -0
- package/dist/redact-params.js.map +1 -0
- package/dist/sandbox-module-catalog.d.ts +43 -0
- package/dist/sandbox-module-catalog.d.ts.map +1 -0
- package/dist/sandbox-module-catalog.js +25 -0
- package/dist/sandbox-module-catalog.js.map +1 -0
- package/dist/storage-workspace.d.ts +59 -0
- package/dist/storage-workspace.d.ts.map +1 -0
- package/dist/storage-workspace.js +426 -0
- package/dist/storage-workspace.js.map +1 -0
- package/dist/swappable-python-bridge.d.ts +83 -0
- package/dist/swappable-python-bridge.d.ts.map +1 -0
- package/dist/swappable-python-bridge.js +246 -0
- package/dist/swappable-python-bridge.js.map +1 -0
- package/dist/telemetry.d.ts +7 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +23 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/testing.d.ts +81 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +130 -0
- package/dist/testing.js.map +1 -0
- package/dist/token-counter.d.ts +30 -0
- package/dist/token-counter.d.ts.map +1 -0
- package/dist/token-counter.js +156 -0
- package/dist/token-counter.js.map +1 -0
- package/dist/trace-exporters.d.ts +10 -0
- package/dist/trace-exporters.d.ts.map +1 -1
- package/dist/trace-exporters.js +59 -20
- package/dist/trace-exporters.js.map +1 -1
- package/dist/tracing-helpers.d.ts +45 -4
- package/dist/tracing-helpers.d.ts.map +1 -1
- package/dist/tracing-helpers.js +62 -19
- package/dist/tracing-helpers.js.map +1 -1
- package/dist/turn-budget.d.ts +278 -0
- package/dist/turn-budget.d.ts.map +1 -0
- package/dist/turn-budget.js +365 -0
- package/dist/turn-budget.js.map +1 -0
- package/dist/variable-channel.d.ts +48 -0
- package/dist/variable-channel.d.ts.map +1 -0
- package/dist/variable-channel.js +110 -0
- package/dist/variable-channel.js.map +1 -0
- package/dist/workspace.d.ts +119 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +112 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +107 -19
|
@@ -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,54 @@ 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;
|
|
194
539
|
static requiredSecrets() {
|
|
195
540
|
return ["FAL_API_KEY"];
|
|
196
541
|
}
|
|
197
|
-
constructor(secrets = {}) {
|
|
542
|
+
constructor(secrets = {}, options = {}) {
|
|
198
543
|
super("fal_ai");
|
|
199
544
|
this.apiKey = secrets["FAL_API_KEY"] ?? "";
|
|
545
|
+
this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
546
|
+
this._webhookUrl = options.webhookUrl;
|
|
547
|
+
this._onQueueAccepted = options.onQueueAccepted;
|
|
548
|
+
this._onQueueBound = options.onQueueBound;
|
|
200
549
|
}
|
|
201
550
|
/** Build an onQueueUpdate callback that forwards progress via emitMessage. */
|
|
202
551
|
makeQueueUpdateHandler() {
|
|
@@ -233,27 +582,324 @@ export class FalProvider extends BaseProvider {
|
|
|
233
582
|
if (this._client)
|
|
234
583
|
return this._client;
|
|
235
584
|
const { createFalClient } = await import("@fal-ai/client");
|
|
236
|
-
|
|
585
|
+
const client = createFalClient({
|
|
237
586
|
credentials: this.apiKey
|
|
238
587
|
});
|
|
588
|
+
// The SDK's subscribe() hides submit/bind/wait and enables automatic POST
|
|
589
|
+
// retries. Use the explicit adapter when this is a queue-capable SDK. The
|
|
590
|
+
// fallback keeps injected legacy clients and older hosts compatible.
|
|
591
|
+
const queueCapable = typeof client === "object" &&
|
|
592
|
+
client !== null &&
|
|
593
|
+
"queue" in client &&
|
|
594
|
+
typeof client.queue === "object" &&
|
|
595
|
+
client.queue !== null;
|
|
596
|
+
this._client = {
|
|
597
|
+
subscribe: async (endpoint, opts) => {
|
|
598
|
+
if (!queueCapable) {
|
|
599
|
+
const result = await client.subscribe(endpoint, opts);
|
|
600
|
+
recordGenerationProviderResult(result.data ?? result);
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
const operations = createFalQueueOperations({
|
|
604
|
+
apiKey: this.apiKey,
|
|
605
|
+
fetchFn: this._fetch
|
|
606
|
+
});
|
|
607
|
+
const request = {
|
|
608
|
+
endpoint,
|
|
609
|
+
input: opts.input
|
|
610
|
+
};
|
|
611
|
+
const providerRequestOptions = currentGenerationProviderRequestOptions();
|
|
612
|
+
if (opts.abortSignal)
|
|
613
|
+
request.signal = opts.abortSignal;
|
|
614
|
+
const webhookUrl = this._webhookUrl ?? providerRequestOptions?.webhookUrl;
|
|
615
|
+
if (webhookUrl)
|
|
616
|
+
request.webhookUrl = webhookUrl;
|
|
617
|
+
const result = await falSubmitAndWait(operations, request, {
|
|
618
|
+
signal: opts.abortSignal,
|
|
619
|
+
onAccepted: (submission) => {
|
|
620
|
+
// The id is recorded immediately after the non-retried POST and
|
|
621
|
+
// before any status/result wait, so a later wait timeout preserves
|
|
622
|
+
// the provider identity for a host lifecycle adapter.
|
|
623
|
+
return (async () => {
|
|
624
|
+
await recordGenerationReceiptAsync({ provider_request_id: submission.providerRequestId }, submission);
|
|
625
|
+
await this._onQueueAccepted?.(submission);
|
|
626
|
+
})();
|
|
627
|
+
},
|
|
628
|
+
onBound: async (binding) => {
|
|
629
|
+
await recordGenerationBindingAsync(binding);
|
|
630
|
+
await this._onQueueBound?.(binding);
|
|
631
|
+
},
|
|
632
|
+
onUpdate: (observation) => {
|
|
633
|
+
const status = typeof observation.rawStatus === "object" &&
|
|
634
|
+
observation.rawStatus !== null &&
|
|
635
|
+
"status" in observation.rawStatus &&
|
|
636
|
+
typeof observation.rawStatus.status === "string"
|
|
637
|
+
? observation.rawStatus.status
|
|
638
|
+
: observation.state === "queued"
|
|
639
|
+
? "IN_QUEUE"
|
|
640
|
+
: observation.state === "running"
|
|
641
|
+
? "IN_PROGRESS"
|
|
642
|
+
: "COMPLETED";
|
|
643
|
+
const logs = typeof observation.rawStatus === "object" &&
|
|
644
|
+
observation.rawStatus !== null &&
|
|
645
|
+
"logs" in observation.rawStatus &&
|
|
646
|
+
Array.isArray(observation.rawStatus.logs)
|
|
647
|
+
? observation.rawStatus.logs
|
|
648
|
+
: undefined;
|
|
649
|
+
opts.onQueueUpdate?.({
|
|
650
|
+
status,
|
|
651
|
+
logs: logs?.filter((entry) => typeof entry === "object" &&
|
|
652
|
+
entry !== null &&
|
|
653
|
+
"message" in entry &&
|
|
654
|
+
typeof entry.message === "string")
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
recordGenerationProviderResult(result.data);
|
|
659
|
+
return { data: result.data, requestId: result.requestId };
|
|
660
|
+
},
|
|
661
|
+
storage: client.storage
|
|
662
|
+
};
|
|
239
663
|
return this._client;
|
|
240
664
|
}
|
|
665
|
+
/** How the platform-API client reaches FAL: this provider's fetch, its signal. */
|
|
666
|
+
generationsOptions(signal) {
|
|
667
|
+
const options = { fetchFn: this._fetch };
|
|
668
|
+
if (signal)
|
|
669
|
+
options.signal = signal;
|
|
670
|
+
return options;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* FAL's own record of this account's requests, through its Platform APIs —
|
|
674
|
+
* generations this installation never ran included, at the cost FAL billed.
|
|
675
|
+
* See `fal-generations.ts` for which endpoint answers what.
|
|
676
|
+
*/
|
|
677
|
+
async listGenerations(query = {}) {
|
|
678
|
+
return falListGenerations(this.apiKey, query, this.generationsOptions(query.signal));
|
|
679
|
+
}
|
|
680
|
+
/** One FAL request by its request id — the id the local row keeps. */
|
|
681
|
+
async getGeneration(requestId, options = {}) {
|
|
682
|
+
return falGetGeneration(this.apiKey, requestId, options, this.generationsOptions(options.signal));
|
|
683
|
+
}
|
|
241
684
|
async getAvailableImageModels() {
|
|
242
|
-
return loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
685
|
+
return expandTopazVariants(loadImageModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
|
|
243
686
|
}
|
|
244
687
|
async getAvailableVideoModels() {
|
|
245
|
-
return loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
688
|
+
return expandTopazVariants(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai"));
|
|
246
689
|
}
|
|
247
|
-
async
|
|
248
|
-
|
|
690
|
+
async getAvailableTTSModels() {
|
|
691
|
+
return loadTTSModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
249
692
|
}
|
|
250
|
-
async
|
|
251
|
-
|
|
693
|
+
async getAvailableMusicModels() {
|
|
694
|
+
return loadMusicModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
695
|
+
}
|
|
696
|
+
async getAvailableAudioToAudioModels() {
|
|
697
|
+
return loadAudioToAudioModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai");
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Rewrite a recording. The source audio is uploaded and attached to whatever
|
|
701
|
+
* audio field the endpoint declares; the optional direction fields are only
|
|
702
|
+
* written where the manifest declares them, so a voice changer that takes a
|
|
703
|
+
* `voice` and a separator that takes nothing both work off one call.
|
|
704
|
+
*/
|
|
705
|
+
async audioToAudio(audio, params) {
|
|
706
|
+
const modelId = params.model.id;
|
|
707
|
+
const url = await this.upload(audio, "audio/mpeg");
|
|
708
|
+
const b = new FalArgsBuilder(modelId);
|
|
709
|
+
b.attachAsset("audio", url)
|
|
710
|
+
.set("prompt", params.prompt)
|
|
711
|
+
.set("voice", params.voice)
|
|
712
|
+
.set("strength", params.strength);
|
|
713
|
+
if (params.seed != null && params.seed !== -1)
|
|
714
|
+
b.set("seed", params.seed);
|
|
715
|
+
this.recordRequestPayload(b.args);
|
|
716
|
+
log.debug("FAL audioToAudio", { model: modelId });
|
|
717
|
+
const client = await this.getClient();
|
|
718
|
+
const result = await client.subscribe(modelId, {
|
|
719
|
+
input: b.args,
|
|
720
|
+
logs: true,
|
|
721
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
722
|
+
});
|
|
723
|
+
const data = (result.data ?? result);
|
|
724
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
725
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Generate music as an encoded audio file. FAL music endpoints (Stable Audio,
|
|
729
|
+
* MiniMax Music, ACE-Step, DiffRhythm, …) return a file URL, so this mirrors
|
|
730
|
+
* {@link textToSpeechEncoded}: shape the request from the manifest, run it,
|
|
731
|
+
* and download the resulting audio bytes.
|
|
732
|
+
*/
|
|
733
|
+
async textToMusic(params) {
|
|
734
|
+
if (!params.prompt)
|
|
735
|
+
throw new Error("prompt must not be empty");
|
|
736
|
+
const client = await this.getClient();
|
|
737
|
+
const modelId = params.model.id;
|
|
738
|
+
const input = this.buildTextToMusicArgs(modelId, params);
|
|
739
|
+
this.recordRequestPayload(input);
|
|
740
|
+
log.debug("FAL textToMusic", { model: modelId });
|
|
741
|
+
const result = await client.subscribe(modelId, {
|
|
742
|
+
input,
|
|
743
|
+
logs: true,
|
|
744
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
745
|
+
});
|
|
746
|
+
const data = (result.data ?? result);
|
|
747
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
748
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Shape music inputs for whatever the endpoint declares. The text prompt
|
|
752
|
+
* lands in `prompt`; lyrics (for vocal models) and duration land in whichever
|
|
753
|
+
* of the heterogeneous duration/lyrics fields the endpoint accepts.
|
|
754
|
+
* {@link FalArgsBuilder.set} drops keys the endpoint doesn't declare.
|
|
755
|
+
*/
|
|
756
|
+
buildTextToMusicArgs(modelId, params) {
|
|
757
|
+
const b = new FalArgsBuilder(modelId);
|
|
758
|
+
b.force("prompt", params.prompt)
|
|
759
|
+
.set("style_prompt", params.prompt)
|
|
760
|
+
.set("lyrics", params.lyrics)
|
|
761
|
+
.set("lyrics_prompt", params.lyrics);
|
|
762
|
+
if (params.durationSeconds != null) {
|
|
763
|
+
b.set("duration", params.durationSeconds)
|
|
764
|
+
.set("seconds_total", params.durationSeconds)
|
|
765
|
+
.set("music_duration", params.durationSeconds);
|
|
766
|
+
if (b.declares("music_length_ms")) {
|
|
767
|
+
// ElevenLabs expects an integer, despite the optional field's string
|
|
768
|
+
// type in the generated manifest: https://fal.ai/models/fal-ai/elevenlabs/music/api
|
|
769
|
+
b.force("music_length_ms", Math.round(params.durationSeconds * 1000));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (params.seed != null && params.seed !== -1)
|
|
773
|
+
b.set("seed", params.seed);
|
|
774
|
+
return b.args;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Generate speech as an encoded audio file (mp3/wav, depending on the
|
|
778
|
+
* endpoint). FAL TTS endpoints return a file URL rather than raw PCM, so the
|
|
779
|
+
* unified TTS node consumes this encoded path instead of streaming samples.
|
|
780
|
+
*/
|
|
781
|
+
async textToSpeechEncoded(args) {
|
|
782
|
+
if (!args.text)
|
|
783
|
+
throw new Error("text must not be empty");
|
|
784
|
+
const client = await this.getClient();
|
|
785
|
+
const input = this.buildTextToSpeechArgs(args.model, args);
|
|
786
|
+
log.debug("FAL textToSpeech", { model: args.model });
|
|
787
|
+
const result = await client.subscribe(args.model, {
|
|
788
|
+
input,
|
|
789
|
+
logs: true,
|
|
790
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
791
|
+
});
|
|
792
|
+
const data = (result.data ?? result);
|
|
793
|
+
const bytes = await downloadBytes(extractAudioUrl(data));
|
|
794
|
+
return { data: bytes, mimeType: sniffAudioMime(bytes) };
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Shape TTS inputs for whatever the endpoint declares. The spoken text lands
|
|
798
|
+
* in the first text-like field the endpoint accepts; the voice lands in
|
|
799
|
+
* `voice` / `speaker` when enumerated. {@link FalArgsBuilder.set} drops keys
|
|
800
|
+
* the endpoint doesn't declare, so this works across the heterogeneous TTS
|
|
801
|
+
* catalog.
|
|
802
|
+
*/
|
|
803
|
+
buildTextToSpeechArgs(modelId, params) {
|
|
804
|
+
const b = new FalArgsBuilder(modelId);
|
|
805
|
+
if (b.has("text")) {
|
|
806
|
+
b.set("text", params.text);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
b.set("prompt", params.text)
|
|
810
|
+
.set("script", params.text)
|
|
811
|
+
.set("gen_text", params.text)
|
|
812
|
+
.set("input", params.text);
|
|
813
|
+
}
|
|
814
|
+
b.set("voice", params.voice).set("speaker", params.voice);
|
|
815
|
+
if (params.speed != null)
|
|
816
|
+
b.set("speed", params.speed);
|
|
817
|
+
b.set("language", params.language).set("instructions", params.instructions);
|
|
818
|
+
return b.args;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* fal's chat surface is an OpenAI-compatible route
|
|
822
|
+
* ({@link FAL_LLM_BASE_URL}) backed by OpenRouter, so it speaks the same
|
|
823
|
+
* dialect every other gateway provider here does — with one difference: fal
|
|
824
|
+
* authenticates with `Key <FAL_KEY>`, not `Bearer`, and answers 401 to the
|
|
825
|
+
* Bearer form. The compat client spreads `defaultHeaders` after its own
|
|
826
|
+
* Authorization, so this overrides it.
|
|
827
|
+
*/
|
|
828
|
+
chatProvider() {
|
|
829
|
+
if (this._chat)
|
|
830
|
+
return this._chat;
|
|
831
|
+
if (!this.apiKey) {
|
|
832
|
+
throw new Error("FAL_API_KEY is required for chat generation");
|
|
833
|
+
}
|
|
834
|
+
const chat = new OpenAICompatProvider({
|
|
835
|
+
providerId: "fal_ai",
|
|
836
|
+
apiKey: this.apiKey,
|
|
837
|
+
baseURL: FAL_LLM_BASE_URL,
|
|
838
|
+
defaultHeaders: { Authorization: `Key ${this.apiKey}` }
|
|
839
|
+
}, { fetchFn: this._fetch });
|
|
840
|
+
chat.setMessageEmitter((msg) => this.emitMessage(msg));
|
|
841
|
+
this._chat = chat;
|
|
842
|
+
return chat;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Language models reachable through fal's OpenAI-compatible route. The route
|
|
846
|
+
* is OpenRouter's router, so the ids it accepts are OpenRouter's ids and its
|
|
847
|
+
* public catalog is the model list — fal itself publishes no `/models` for
|
|
848
|
+
* this endpoint. Unreachable catalog means no language models rather than a
|
|
849
|
+
* failed provider, matching how the other enumerating providers fail.
|
|
850
|
+
*/
|
|
851
|
+
async getAvailableLanguageModels() {
|
|
852
|
+
let rows;
|
|
853
|
+
try {
|
|
854
|
+
const response = await this._fetch(OPENROUTER_CATALOG_URL);
|
|
855
|
+
if (!response.ok)
|
|
856
|
+
return [];
|
|
857
|
+
rows = decodeFalLanguageCatalog(await response.json());
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
log.warn(`Could not load the fal language-model catalog: ${err}`);
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
const toolCapable = new Set();
|
|
864
|
+
const models = [];
|
|
865
|
+
for (const row of rows) {
|
|
866
|
+
if (!isNonEmptyString(row.id))
|
|
867
|
+
continue;
|
|
868
|
+
if (row.supported_parameters?.includes("tools"))
|
|
869
|
+
toolCapable.add(row.id);
|
|
870
|
+
models.push({ id: row.id, name: row.name ?? row.id, provider: "fal_ai" });
|
|
871
|
+
}
|
|
872
|
+
this._toolCapableModels = toolCapable;
|
|
873
|
+
return models;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Tool support varies model by model across a 300-model catalog, so answer
|
|
877
|
+
* from what the catalog declared. Before any listing there is nothing to
|
|
878
|
+
* answer from — say yes, as the base class does, rather than hiding tools
|
|
879
|
+
* from a model that has them.
|
|
880
|
+
*/
|
|
881
|
+
async hasToolSupport(model) {
|
|
882
|
+
return this._toolCapableModels?.has(model) ?? true;
|
|
883
|
+
}
|
|
884
|
+
async generateMessage(args) {
|
|
885
|
+
return this.chatProvider().generateMessage(args);
|
|
886
|
+
}
|
|
887
|
+
async *generateMessages(args) {
|
|
888
|
+
yield* this.chatProvider().generateMessages(args);
|
|
889
|
+
}
|
|
890
|
+
/** Chat spend accrues on the delegate; report it as this provider's own. */
|
|
891
|
+
get cost() {
|
|
892
|
+
return super.cost + (this._chat?.cost ?? 0);
|
|
893
|
+
}
|
|
894
|
+
resetCost() {
|
|
895
|
+
super.resetCost();
|
|
896
|
+
this._chat?.resetCost();
|
|
252
897
|
}
|
|
253
898
|
buildTextToImageArgs(modelId, params) {
|
|
254
899
|
const b = new FalArgsBuilder(modelId);
|
|
255
900
|
b.force("prompt", params.prompt)
|
|
256
901
|
.set("output_format", "png")
|
|
902
|
+
.set("quality", params.quality)
|
|
257
903
|
.set("negative_prompt", params.negativePrompt)
|
|
258
904
|
.set("guidance_scale", params.guidanceScale)
|
|
259
905
|
.set("num_inference_steps", params.numInferenceSteps)
|
|
@@ -263,14 +909,17 @@ export class FalProvider extends BaseProvider {
|
|
|
263
909
|
b.set("seed", params.seed);
|
|
264
910
|
return b.args;
|
|
265
911
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
912
|
+
/**
|
|
913
|
+
* Configure a {@link FalArgsBuilder} with the shared image-edit inputs
|
|
914
|
+
* (source images + prompt + sampling knobs). Used by both image-to-image and
|
|
915
|
+
* inpaint; the latter additionally attaches a mask.
|
|
916
|
+
*/
|
|
917
|
+
buildEditBuilder(modelId, imageUrls, params) {
|
|
270
918
|
const b = new FalArgsBuilder(modelId);
|
|
271
|
-
b.
|
|
919
|
+
b.attachAssets("image", imageUrls)
|
|
272
920
|
.force("prompt", params.prompt)
|
|
273
921
|
.set("output_format", "png")
|
|
922
|
+
.set("quality", params.quality)
|
|
274
923
|
.set("negative_prompt", params.negativePrompt)
|
|
275
924
|
.set("guidance_scale", params.guidanceScale)
|
|
276
925
|
.set("num_inference_steps", params.numInferenceSteps)
|
|
@@ -279,6 +928,23 @@ export class FalProvider extends BaseProvider {
|
|
|
279
928
|
.setSize(params.aspectRatio, params.resolution);
|
|
280
929
|
if (params.seed != null && params.seed !== -1)
|
|
281
930
|
b.set("seed", params.seed);
|
|
931
|
+
return b;
|
|
932
|
+
}
|
|
933
|
+
async buildImageToImageArgs(modelId, images, params) {
|
|
934
|
+
const urls = await this.uploadImages(images);
|
|
935
|
+
return this.buildEditBuilder(modelId, urls, params).args;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Build image-to-image args plus the inpaint mask. The mask is uploaded and
|
|
939
|
+
* attached to whatever mask field the endpoint declares (`mask_url`,
|
|
940
|
+
* `mask_image_url`, …) via {@link FalArgsBuilder.attachMask}.
|
|
941
|
+
*/
|
|
942
|
+
async buildInpaintArgs(modelId, images, params) {
|
|
943
|
+
const urls = await this.uploadImages(images);
|
|
944
|
+
const b = this.buildEditBuilder(modelId, urls, params);
|
|
945
|
+
const [maskUrl] = await this.uploadImages([params.mask]);
|
|
946
|
+
if (maskUrl)
|
|
947
|
+
b.attachMask(maskUrl);
|
|
282
948
|
return b.args;
|
|
283
949
|
}
|
|
284
950
|
buildTextToVideoArgs(modelId, params) {
|
|
@@ -293,12 +959,10 @@ export class FalProvider extends BaseProvider {
|
|
|
293
959
|
b.set("seed", params.seed);
|
|
294
960
|
return b.args;
|
|
295
961
|
}
|
|
296
|
-
async buildImageToVideoArgs(modelId,
|
|
297
|
-
const
|
|
298
|
-
const blob = new Blob([image], { type: "image/png" });
|
|
299
|
-
const uploadedUrl = await client.storage.upload(blob);
|
|
962
|
+
async buildImageToVideoArgs(modelId, images, params) {
|
|
963
|
+
const urls = await this.uploadImages(images);
|
|
300
964
|
const b = new FalArgsBuilder(modelId);
|
|
301
|
-
b.
|
|
965
|
+
b.attachAssets("image", urls)
|
|
302
966
|
.set("prompt", params.prompt)
|
|
303
967
|
.set("negative_prompt", params.negativePrompt)
|
|
304
968
|
.set("num_frames", params.numFrames)
|
|
@@ -313,20 +977,38 @@ export class FalProvider extends BaseProvider {
|
|
|
313
977
|
const client = await this.getClient();
|
|
314
978
|
const modelId = params.model.id;
|
|
315
979
|
const args = this.buildTextToImageArgs(modelId, params);
|
|
980
|
+
this.recordRequestPayload(args);
|
|
316
981
|
log.debug("FAL textToImage", { model: modelId });
|
|
317
982
|
const result = await client.subscribe(modelId, {
|
|
318
983
|
input: args,
|
|
319
984
|
logs: true,
|
|
320
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
985
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
986
|
+
abortSignal: params.signal
|
|
321
987
|
});
|
|
322
988
|
const data = (result.data ?? result);
|
|
323
989
|
return downloadBytes(extractImageUrl(data));
|
|
324
990
|
}
|
|
325
|
-
async imageToImage(
|
|
991
|
+
async imageToImage(images, params) {
|
|
326
992
|
const client = await this.getClient();
|
|
327
993
|
const modelId = params.model.id;
|
|
328
|
-
const args = await this.buildImageToImageArgs(modelId,
|
|
994
|
+
const args = await this.buildImageToImageArgs(modelId, images, params);
|
|
995
|
+
this.recordRequestPayload(args);
|
|
329
996
|
log.debug("FAL imageToImage", { model: modelId });
|
|
997
|
+
const result = await client.subscribe(modelId, {
|
|
998
|
+
input: args,
|
|
999
|
+
logs: true,
|
|
1000
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1001
|
+
abortSignal: params.signal
|
|
1002
|
+
});
|
|
1003
|
+
const data = (result.data ?? result);
|
|
1004
|
+
return downloadBytes(extractImageUrl(data));
|
|
1005
|
+
}
|
|
1006
|
+
async inpaint(images, params) {
|
|
1007
|
+
const client = await this.getClient();
|
|
1008
|
+
const modelId = params.model.id;
|
|
1009
|
+
const args = await this.buildInpaintArgs(modelId, images, params);
|
|
1010
|
+
this.recordRequestPayload(args);
|
|
1011
|
+
log.debug("FAL inpaint", { model: modelId });
|
|
330
1012
|
const result = await client.subscribe(modelId, {
|
|
331
1013
|
input: args,
|
|
332
1014
|
logs: true,
|
|
@@ -335,38 +1017,77 @@ export class FalProvider extends BaseProvider {
|
|
|
335
1017
|
const data = (result.data ?? result);
|
|
336
1018
|
return downloadBytes(extractImageUrl(data));
|
|
337
1019
|
}
|
|
1020
|
+
async inpaintImages(images, params, numImages) {
|
|
1021
|
+
if (numImages <= 1) {
|
|
1022
|
+
return [await this.inpaint(images, params)];
|
|
1023
|
+
}
|
|
1024
|
+
const client = await this.getClient();
|
|
1025
|
+
const modelId = params.model.id;
|
|
1026
|
+
const args = await this.buildInpaintArgs(modelId, images, params);
|
|
1027
|
+
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
1028
|
+
args.num_images = numImages;
|
|
1029
|
+
}
|
|
1030
|
+
this.recordRequestPayload(args);
|
|
1031
|
+
log.debug("FAL inpaintImages", { model: modelId, numImages });
|
|
1032
|
+
const result = await client.subscribe(modelId, {
|
|
1033
|
+
input: args,
|
|
1034
|
+
logs: true,
|
|
1035
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1036
|
+
});
|
|
1037
|
+
const data = (result.data ?? result);
|
|
1038
|
+
const urls = extractImageUrls(data);
|
|
1039
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
1040
|
+
}
|
|
1041
|
+
async outpaintImage(images, params) {
|
|
1042
|
+
const modelId = params.model.id;
|
|
1043
|
+
const urls = await this.uploadImages(images);
|
|
1044
|
+
const b = new FalArgsBuilder(modelId);
|
|
1045
|
+
b.attachAssets("image", urls)
|
|
1046
|
+
.set("prompt", params.prompt)
|
|
1047
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1048
|
+
.setSize(params.aspectRatio, null);
|
|
1049
|
+
setOutpaintPadding(b, params.padding);
|
|
1050
|
+
if (params.seed != null && params.seed !== -1)
|
|
1051
|
+
b.set("seed", params.seed);
|
|
1052
|
+
log.debug("FAL outpaintImage", { model: modelId });
|
|
1053
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1054
|
+
}
|
|
338
1055
|
async textToVideo(params) {
|
|
339
1056
|
const client = await this.getClient();
|
|
340
1057
|
const modelId = params.model.id;
|
|
341
1058
|
const args = this.buildTextToVideoArgs(modelId, params);
|
|
1059
|
+
this.recordRequestPayload(args);
|
|
342
1060
|
log.debug("FAL textToVideo", { model: modelId });
|
|
343
1061
|
const result = await client.subscribe(modelId, {
|
|
344
1062
|
input: args,
|
|
345
1063
|
logs: true,
|
|
346
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1064
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1065
|
+
abortSignal: params.signal
|
|
347
1066
|
});
|
|
348
1067
|
const data = (result.data ?? result);
|
|
349
1068
|
return downloadBytes(extractVideoUrl(data));
|
|
350
1069
|
}
|
|
351
|
-
async imageToImages(
|
|
1070
|
+
async imageToImages(images, params, numImages) {
|
|
352
1071
|
if (numImages <= 1) {
|
|
353
|
-
return [await this.imageToImage(
|
|
1072
|
+
return [await this.imageToImage(images, params)];
|
|
354
1073
|
}
|
|
355
1074
|
const client = await this.getClient();
|
|
356
1075
|
const modelId = params.model.id;
|
|
357
|
-
const args = await this.buildImageToImageArgs(modelId,
|
|
1076
|
+
const args = await this.buildImageToImageArgs(modelId, images, params);
|
|
358
1077
|
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
359
1078
|
args.num_images = numImages;
|
|
360
1079
|
}
|
|
1080
|
+
this.recordRequestPayload(args);
|
|
361
1081
|
log.debug("FAL imageToImages", { model: modelId, numImages });
|
|
362
1082
|
const result = await client.subscribe(modelId, {
|
|
363
1083
|
input: args,
|
|
364
1084
|
logs: true,
|
|
365
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1085
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1086
|
+
abortSignal: params.signal
|
|
366
1087
|
});
|
|
367
1088
|
const data = (result.data ?? result);
|
|
368
1089
|
const urls = extractImageUrls(data);
|
|
369
|
-
return Promise.all(urls.map(downloadBytes));
|
|
1090
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
370
1091
|
}
|
|
371
1092
|
async textToImages(params, numImages) {
|
|
372
1093
|
if (numImages <= 1) {
|
|
@@ -378,31 +1099,359 @@ export class FalProvider extends BaseProvider {
|
|
|
378
1099
|
if (new FalArgsBuilder(modelId).has("num_images")) {
|
|
379
1100
|
args.num_images = numImages;
|
|
380
1101
|
}
|
|
1102
|
+
this.recordRequestPayload(args);
|
|
381
1103
|
log.debug("FAL textToImages", { model: modelId, numImages });
|
|
382
1104
|
const result = await client.subscribe(modelId, {
|
|
383
1105
|
input: args,
|
|
384
1106
|
logs: true,
|
|
385
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1107
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1108
|
+
abortSignal: params.signal
|
|
386
1109
|
});
|
|
387
1110
|
const data = (result.data ?? result);
|
|
388
1111
|
const urls = extractImageUrls(data);
|
|
389
|
-
return Promise.all(urls.map(downloadBytes));
|
|
1112
|
+
return Promise.all(urls.map((url) => downloadBytes(url)));
|
|
390
1113
|
}
|
|
391
1114
|
async imageToVideo(image, params) {
|
|
1115
|
+
if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
|
|
1116
|
+
.find((m) => m.id === params.model.id)
|
|
1117
|
+
?.supportedTasks?.includes("image_to_video") ?? false))
|
|
1118
|
+
throw new Error(`FAL model ${params.model.id} does not support image_to_video`);
|
|
392
1119
|
const client = await this.getClient();
|
|
393
1120
|
const modelId = params.model.id;
|
|
394
|
-
const args = await this.buildImageToVideoArgs(modelId, image, params);
|
|
1121
|
+
const args = await this.buildImageToVideoArgs(modelId, [image], params);
|
|
1122
|
+
this.recordRequestPayload(args);
|
|
395
1123
|
log.debug("FAL imageToVideo", { model: modelId });
|
|
396
1124
|
const result = await client.subscribe(modelId, {
|
|
397
1125
|
input: args,
|
|
398
1126
|
logs: true,
|
|
399
|
-
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1127
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1128
|
+
abortSignal: params.signal
|
|
400
1129
|
});
|
|
401
1130
|
const data = (result.data ?? result);
|
|
402
1131
|
return downloadBytes(extractVideoUrl(data));
|
|
403
1132
|
}
|
|
1133
|
+
async referenceToVideo(inputs, params) {
|
|
1134
|
+
const images = inputs.images.filter((b) => b.length > 0);
|
|
1135
|
+
const videos = inputs.videos.filter((b) => b.length > 0);
|
|
1136
|
+
if (images.length === 0 && videos.length === 0) {
|
|
1137
|
+
throw new Error("reference_to_video requires at least one reference image or video");
|
|
1138
|
+
}
|
|
1139
|
+
const modelId = params.model.id;
|
|
1140
|
+
const builder = new FalArgsBuilder(modelId);
|
|
1141
|
+
if (!(loadVideoModels(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, "fal_ai")
|
|
1142
|
+
.find((m) => m.id === modelId)
|
|
1143
|
+
?.supportedTasks?.includes("reference_to_video") ?? false))
|
|
1144
|
+
throw new Error(`FAL model ${modelId} does not support reference_to_video`);
|
|
1145
|
+
validateReferenceInputs("FAL", modelId, inputs, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, modelId));
|
|
1146
|
+
const imageFields = builder.referenceAssetInputs("image");
|
|
1147
|
+
const videoFields = builder.referenceAssetInputs("video");
|
|
1148
|
+
if (images.length > 0 && imageFields.length === 0)
|
|
1149
|
+
throw new Error(`FAL model ${modelId} does not support reference images`);
|
|
1150
|
+
if (videos.length > 0 && videoFields.length === 0)
|
|
1151
|
+
throw new Error(`FAL model ${modelId} does not support reference videos`);
|
|
1152
|
+
if (params.useReferenceVideoAudio === true && videos.length === 0)
|
|
1153
|
+
throw new Error(`FAL model ${modelId} requires a reference video when audio is enabled`);
|
|
1154
|
+
if (params.useReferenceVideoAudio === true &&
|
|
1155
|
+
!builder.has("use_reference_video_audio"))
|
|
1156
|
+
throw new Error(`FAL model ${modelId} does not support reference video audio`);
|
|
1157
|
+
const signal = combineSignals(params.signal, params.timeoutSeconds);
|
|
1158
|
+
signal?.throwIfAborted();
|
|
1159
|
+
const imageUrls = await Promise.all(images.map((b) => this.upload(b, detectImageMime(b))));
|
|
1160
|
+
const videoUrls = await Promise.all(videos.map((b) => this.upload(b, sniffMediaMime(b, "video/mp4"))));
|
|
1161
|
+
signal?.throwIfAborted();
|
|
1162
|
+
builder
|
|
1163
|
+
.attachReferenceAssets("image", imageUrls)
|
|
1164
|
+
.attachReferenceAssets("video", videoUrls)
|
|
1165
|
+
.set("prompt", params.prompt)
|
|
1166
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1167
|
+
.set("duration", params.durationSeconds)
|
|
1168
|
+
.setSize(params.aspectRatio, params.resolution)
|
|
1169
|
+
.set("use_reference_video_audio", params.useReferenceVideoAudio);
|
|
1170
|
+
this.recordRequestPayload(builder.args);
|
|
1171
|
+
const client = await this.getClient();
|
|
1172
|
+
const result = await client.subscribe(modelId, {
|
|
1173
|
+
input: builder.args,
|
|
1174
|
+
logs: true,
|
|
1175
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1176
|
+
abortSignal: signal
|
|
1177
|
+
});
|
|
1178
|
+
return downloadBytes(extractVideoUrl((result.data ?? result)), signal);
|
|
1179
|
+
}
|
|
1180
|
+
/** Upload raw bytes to FAL storage and return the hosted URL. */
|
|
1181
|
+
async upload(bytes, mimeType) {
|
|
1182
|
+
const client = await this.getClient();
|
|
1183
|
+
const blob = new Blob([new Uint8Array(bytes)], {
|
|
1184
|
+
type: mimeType
|
|
1185
|
+
});
|
|
1186
|
+
return client.storage.upload(blob);
|
|
1187
|
+
}
|
|
1188
|
+
/** Upload every non-empty image to FAL storage, returning the hosted URLs. */
|
|
1189
|
+
async uploadImages(images) {
|
|
1190
|
+
const valid = images.filter((b) => b && b.length > 0);
|
|
1191
|
+
return Promise.all(valid.map((b) => this.upload(b, detectImageMime(b))));
|
|
1192
|
+
}
|
|
1193
|
+
/** Run an endpoint that takes a single uploaded image and returns an image. */
|
|
1194
|
+
async runImageEndpoint(modelId, args) {
|
|
1195
|
+
const client = await this.getClient();
|
|
1196
|
+
this.recordRequestPayload(args);
|
|
1197
|
+
const result = await client.subscribe(modelId, {
|
|
1198
|
+
input: args,
|
|
1199
|
+
logs: true,
|
|
1200
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1201
|
+
});
|
|
1202
|
+
const data = (result.data ?? result);
|
|
1203
|
+
return downloadBytes(extractImageUrl(data));
|
|
1204
|
+
}
|
|
1205
|
+
/** Run an endpoint that returns a video and download the bytes. */
|
|
1206
|
+
async runVideoEndpoint(modelId, args, signal) {
|
|
1207
|
+
const client = await this.getClient();
|
|
1208
|
+
this.recordRequestPayload(args);
|
|
1209
|
+
const result = await client.subscribe(modelId, {
|
|
1210
|
+
input: args,
|
|
1211
|
+
logs: true,
|
|
1212
|
+
abortSignal: signal,
|
|
1213
|
+
onQueueUpdate: this.makeQueueUpdateHandler()
|
|
1214
|
+
});
|
|
1215
|
+
const data = (result.data ?? result);
|
|
1216
|
+
return downloadBytes(extractVideoUrl(data), signal);
|
|
1217
|
+
}
|
|
1218
|
+
async upscaleImage(image, params) {
|
|
1219
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1220
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1221
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1222
|
+
b.attachAsset("image", url)
|
|
1223
|
+
.set("model", variant)
|
|
1224
|
+
.set("prompt", params.prompt)
|
|
1225
|
+
.set("upscale_factor", params.scale)
|
|
1226
|
+
.set("scale", params.scale)
|
|
1227
|
+
.set("creativity", topazCreativity(endpointId, params.creativity));
|
|
1228
|
+
if (params.seed != null && params.seed !== -1)
|
|
1229
|
+
b.set("seed", params.seed);
|
|
1230
|
+
log.debug("FAL upscaleImage", { model: endpointId, variant });
|
|
1231
|
+
return this.runImageEndpoint(endpointId, b.args);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Segment an image into object masks. fal's SAM endpoints answer with mask
|
|
1235
|
+
* images plus loose side-channels — `scores`, `boxes` and `metadata` arrive
|
|
1236
|
+
* as JSON-encoded strings, and boxes are normalized `[cx, cy, w, h]` — so
|
|
1237
|
+
* everything past the mask list is parsed defensively and left absent when
|
|
1238
|
+
* it does not decode.
|
|
1239
|
+
*/
|
|
1240
|
+
async segmentImage(image, params) {
|
|
1241
|
+
if (image.length === 0) {
|
|
1242
|
+
throw new Error("segmentImage requires a non-empty image");
|
|
1243
|
+
}
|
|
1244
|
+
const modelId = params.model.id;
|
|
1245
|
+
assertSegmentPromptIsUsable(modelId, params);
|
|
1246
|
+
const client = await this.getClient();
|
|
1247
|
+
const [imageUrl] = await this.uploadImages([image]);
|
|
1248
|
+
const args = buildSegmentImageArgs(modelId, imageUrl, params);
|
|
1249
|
+
this.recordRequestPayload(args);
|
|
1250
|
+
log.debug("FAL segmentImage", { model: modelId });
|
|
1251
|
+
const result = await client.subscribe(modelId, {
|
|
1252
|
+
input: args,
|
|
1253
|
+
logs: true,
|
|
1254
|
+
onQueueUpdate: this.makeQueueUpdateHandler(),
|
|
1255
|
+
abortSignal: params.signal
|
|
1256
|
+
});
|
|
1257
|
+
const data = (result.data ?? result);
|
|
1258
|
+
const { masks, decoded, bestScore } = await collectSegmentationMasks(data, params.minConfidence);
|
|
1259
|
+
if (masks.length === 0) {
|
|
1260
|
+
// Three different failures end here and look alike from the caller: the
|
|
1261
|
+
// model found nothing, the response carries a shape this decoder does
|
|
1262
|
+
// not read, or every mask it did read fell under `minConfidence`. Name
|
|
1263
|
+
// the fields the response carried and what was decoded so they separate.
|
|
1264
|
+
log.warn("FAL segmentImage returned no masks", {
|
|
1265
|
+
model: modelId,
|
|
1266
|
+
responseFields: Object.keys(data),
|
|
1267
|
+
decodedMasks: decoded,
|
|
1268
|
+
minConfidence: params.minConfidence ?? null,
|
|
1269
|
+
bestScore
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
return masks;
|
|
1273
|
+
}
|
|
1274
|
+
async removeBackground(image, params) {
|
|
1275
|
+
const modelId = params.model.id;
|
|
1276
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1277
|
+
const b = new FalArgsBuilder(modelId);
|
|
1278
|
+
b.attachAsset("image", url).set("output_format", "png");
|
|
1279
|
+
log.debug("FAL removeBackground", { model: modelId });
|
|
1280
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1281
|
+
}
|
|
1282
|
+
async relightImage(image, params) {
|
|
1283
|
+
const modelId = params.model.id;
|
|
1284
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1285
|
+
const b = new FalArgsBuilder(modelId);
|
|
1286
|
+
b.attachAsset("image", url)
|
|
1287
|
+
.force("prompt", params.prompt)
|
|
1288
|
+
.set("output_format", "png")
|
|
1289
|
+
.set("negative_prompt", params.negativePrompt);
|
|
1290
|
+
if (params.seed != null && params.seed !== -1)
|
|
1291
|
+
b.set("seed", params.seed);
|
|
1292
|
+
log.debug("FAL relightImage", { model: modelId });
|
|
1293
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1294
|
+
}
|
|
1295
|
+
async vectorizeImage(image, params) {
|
|
1296
|
+
const modelId = params.model.id;
|
|
1297
|
+
const url = await this.upload(image, detectImageMime(image));
|
|
1298
|
+
const b = new FalArgsBuilder(modelId);
|
|
1299
|
+
b.attachAsset("image", url);
|
|
1300
|
+
log.debug("FAL vectorizeImage", { model: modelId });
|
|
1301
|
+
return this.runImageEndpoint(modelId, b.args);
|
|
1302
|
+
}
|
|
1303
|
+
async videoToVideo(video, params) {
|
|
1304
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1305
|
+
const images = params.referenceImages ?? [];
|
|
1306
|
+
if (images.length) {
|
|
1307
|
+
validateReferenceInputs("FAL", endpointId, { images, videos: [] }, getModelReferenceInputs(FAL_MANIFEST_PKG, FAL_MANIFEST_PATH, endpointId));
|
|
1308
|
+
}
|
|
1309
|
+
params.signal?.throwIfAborted();
|
|
1310
|
+
const imageUrls = await Promise.all(images.map((image) => this.upload(image, detectImageMime(image))));
|
|
1311
|
+
const url = await this.upload(video, "video/mp4");
|
|
1312
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1313
|
+
b.attachReferenceAssets("image", imageUrls);
|
|
1314
|
+
b.attachAsset("video", url)
|
|
1315
|
+
.set("model", variant)
|
|
1316
|
+
.set("prompt", params.prompt)
|
|
1317
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1318
|
+
.set("strength", params.strength)
|
|
1319
|
+
.set("duration", params.durationSeconds)
|
|
1320
|
+
.setSize(null, params.resolution);
|
|
1321
|
+
if (params.seed != null && params.seed !== -1)
|
|
1322
|
+
b.set("seed", params.seed);
|
|
1323
|
+
log.debug("FAL videoToVideo", { model: endpointId, variant });
|
|
1324
|
+
return this.runVideoEndpoint(endpointId, b.args, params.signal);
|
|
1325
|
+
}
|
|
1326
|
+
async extendVideo(video, params) {
|
|
1327
|
+
const model = (await this.getAvailableVideoModels()).find((candidate) => candidate.id === params.model.id);
|
|
1328
|
+
if (!model?.supportedTasks?.includes("extend_video")) {
|
|
1329
|
+
throw new Error(`Model ${params.model.id} does not support extend_video`);
|
|
1330
|
+
}
|
|
1331
|
+
const directionTasks = model.supportedTasks.filter((task) => task.startsWith("extend_video_"));
|
|
1332
|
+
if (directionTasks.length > 0 &&
|
|
1333
|
+
!directionTasks.includes(`extend_video_${params.mode}`)) {
|
|
1334
|
+
throw new Error(`Model ${params.model.id} does not support ${params.mode} extension`);
|
|
1335
|
+
}
|
|
1336
|
+
if ((params.mode !== "start" && params.mode !== "end") ||
|
|
1337
|
+
!Number.isFinite(params.durationSeconds) ||
|
|
1338
|
+
params.durationSeconds <= 0 ||
|
|
1339
|
+
(model.durations?.length &&
|
|
1340
|
+
!model.durations.includes(params.durationSeconds))) {
|
|
1341
|
+
throw new Error("Choose a supported extension mode and duration.");
|
|
1342
|
+
}
|
|
1343
|
+
const entry = getFalManifestEntry(params.model.id);
|
|
1344
|
+
const durationField = entry?.inputFields?.find((field) => (field.apiParamName ?? field.name) === "duration");
|
|
1345
|
+
if ((durationField?.min !== undefined &&
|
|
1346
|
+
params.durationSeconds < durationField.min) ||
|
|
1347
|
+
(durationField?.max !== undefined &&
|
|
1348
|
+
params.durationSeconds > durationField.max)) {
|
|
1349
|
+
throw new Error("Choose an extension duration within the model's supported range.");
|
|
1350
|
+
}
|
|
1351
|
+
const duration = durationField?.propType === "str" &&
|
|
1352
|
+
typeof durationField.default === "string" &&
|
|
1353
|
+
/s$/i.test(durationField.default.trim())
|
|
1354
|
+
? `${params.durationSeconds}s`
|
|
1355
|
+
: params.durationSeconds;
|
|
1356
|
+
const url = await this.upload(video, "video/mp4");
|
|
1357
|
+
const builder = new FalArgsBuilder(params.model.id);
|
|
1358
|
+
builder
|
|
1359
|
+
.attachAsset("video", url)
|
|
1360
|
+
.set("prompt", params.prompt)
|
|
1361
|
+
.set("mode", params.mode)
|
|
1362
|
+
.set("duration", duration);
|
|
1363
|
+
return this.runVideoEndpoint(params.model.id, builder.args);
|
|
1364
|
+
}
|
|
1365
|
+
async upscaleVideo(video, params) {
|
|
1366
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1367
|
+
const url = await this.upload(video, "video/mp4");
|
|
1368
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1369
|
+
b.attachAsset("video", url)
|
|
1370
|
+
.set("model", variant)
|
|
1371
|
+
.set("prompt", params.prompt)
|
|
1372
|
+
.set("creativity", params.creativity)
|
|
1373
|
+
// `upscale_factor` and `scale` name the same thing across endpoints, and
|
|
1374
|
+
// `target_resolution` is the rung-shaped alternative; each is written
|
|
1375
|
+
// only where the endpoint declares it.
|
|
1376
|
+
.set("upscale_factor", params.scale)
|
|
1377
|
+
.set("scale", params.scale)
|
|
1378
|
+
.set("target_resolution", params.targetResolution)
|
|
1379
|
+
.setSize(null, params.targetResolution);
|
|
1380
|
+
if (params.seed != null && params.seed !== -1)
|
|
1381
|
+
b.set("seed", params.seed);
|
|
1382
|
+
log.debug("FAL upscaleVideo", { model: endpointId, variant });
|
|
1383
|
+
return this.runVideoEndpoint(endpointId, b.args);
|
|
1384
|
+
}
|
|
1385
|
+
async interpolateVideo(video, params) {
|
|
1386
|
+
const { endpointId, variant } = splitTopazVariant(params.model.id);
|
|
1387
|
+
const url = await this.upload(video, "video/mp4");
|
|
1388
|
+
const b = new FalArgsBuilder(endpointId);
|
|
1389
|
+
b.attachAsset("video", url)
|
|
1390
|
+
.set("model", variant)
|
|
1391
|
+
.set("output_fps", params.targetFps)
|
|
1392
|
+
.set("target_fps", params.targetFps)
|
|
1393
|
+
// AMT doubles the frame count per pass, so a factor is log2 passes;
|
|
1394
|
+
// Topaz takes the factor itself as a slowdown.
|
|
1395
|
+
.set("slowdown_factor", params.factor)
|
|
1396
|
+
.set("recursive_interpolation_passes", params.factor != null && params.factor > 1
|
|
1397
|
+
? Math.max(1, Math.round(Math.log2(params.factor)))
|
|
1398
|
+
: null);
|
|
1399
|
+
if (params.seed != null && params.seed !== -1)
|
|
1400
|
+
b.set("seed", params.seed);
|
|
1401
|
+
log.debug("FAL interpolateVideo", { model: endpointId, variant });
|
|
1402
|
+
return this.runVideoEndpoint(endpointId, b.args);
|
|
1403
|
+
}
|
|
1404
|
+
async outpaintVideo(video, params) {
|
|
1405
|
+
const modelId = params.model.id;
|
|
1406
|
+
const url = await this.upload(video, "video/mp4");
|
|
1407
|
+
const b = new FalArgsBuilder(modelId);
|
|
1408
|
+
b.attachAsset("video", url)
|
|
1409
|
+
.set("prompt", params.prompt)
|
|
1410
|
+
.set("negative_prompt", params.negativePrompt)
|
|
1411
|
+
.set("expand_ratio", params.expandRatio)
|
|
1412
|
+
.setSize(params.aspectRatio, params.resolution);
|
|
1413
|
+
setOutpaintPadding(b, params.padding);
|
|
1414
|
+
if (params.seed != null && params.seed !== -1)
|
|
1415
|
+
b.set("seed", params.seed);
|
|
1416
|
+
log.debug("FAL outpaintVideo", { model: modelId });
|
|
1417
|
+
return this.runVideoEndpoint(modelId, b.args);
|
|
1418
|
+
}
|
|
1419
|
+
async lipSync(video, params) {
|
|
1420
|
+
const modelId = params.model.id;
|
|
1421
|
+
const [videoUrl, audioUrl] = await Promise.all([
|
|
1422
|
+
this.upload(video, "video/mp4"),
|
|
1423
|
+
this.upload(params.audio, "audio/mpeg")
|
|
1424
|
+
]);
|
|
1425
|
+
const b = new FalArgsBuilder(modelId);
|
|
1426
|
+
b.attachAsset("video", videoUrl).attachAsset("audio", audioUrl);
|
|
1427
|
+
if (params.seed != null && params.seed !== -1)
|
|
1428
|
+
b.set("seed", params.seed);
|
|
1429
|
+
log.debug("FAL lipSync", { model: modelId });
|
|
1430
|
+
return this.runVideoEndpoint(modelId, b.args);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Decode the OpenRouter catalog fal's chat route is backed by. Throws when the
|
|
1435
|
+
* body carries no `data` array; {@link FalProvider.getAvailableLanguageModels}
|
|
1436
|
+
* treats that the way it treats an unreachable catalog.
|
|
1437
|
+
*/
|
|
1438
|
+
export function decodeFalLanguageCatalog(payload) {
|
|
1439
|
+
if (!isObjectLike(payload)) {
|
|
1440
|
+
throw new Error("fal language catalog is not an object");
|
|
1441
|
+
}
|
|
1442
|
+
const rows = payload.data;
|
|
1443
|
+
if (!Array.isArray(rows)) {
|
|
1444
|
+
throw new Error("fal language catalog has no `data` array");
|
|
1445
|
+
}
|
|
1446
|
+
return rows;
|
|
404
1447
|
}
|
|
405
|
-
|
|
1448
|
+
/**
|
|
1449
|
+
* The result-URL extractors below are the fal response decoders: every media
|
|
1450
|
+
* surface hands its endpoint's payload to one of them. The checked-in raw
|
|
1451
|
+
* fixtures in `packages/runtime/tests/fixtures/provider-contract/` run these
|
|
1452
|
+
* same functions, so an endpoint that renames its result field fails there.
|
|
1453
|
+
*/
|
|
1454
|
+
export function extractImageUrls(result) {
|
|
406
1455
|
const images = result.images;
|
|
407
1456
|
if (images && images.length > 0) {
|
|
408
1457
|
const urls = images.map((img) => img.url).filter(Boolean);
|
|
@@ -414,17 +1463,258 @@ function extractImageUrls(result) {
|
|
|
414
1463
|
return [image.url];
|
|
415
1464
|
throw new Error(`Unexpected FAL image response: ${JSON.stringify(result)}`);
|
|
416
1465
|
}
|
|
417
|
-
|
|
1466
|
+
/**
|
|
1467
|
+
* Shape the inputs a SAM endpoint declares. The prompt kinds are optional and
|
|
1468
|
+
* an endpoint that does not declare one simply drops it — {@link
|
|
1469
|
+
* FalArgsBuilder.set} writes only fields the manifest knows.
|
|
1470
|
+
*/
|
|
1471
|
+
function buildSegmentImageArgs(modelId, imageUrl, params) {
|
|
1472
|
+
const b = new FalArgsBuilder(modelId);
|
|
1473
|
+
const maxMasks = params.maxMasks != null ? Math.max(1, params.maxMasks) : null;
|
|
1474
|
+
b.attachAsset("image", imageUrl)
|
|
1475
|
+
// The mask images are the answer; a masked copy of the source is not.
|
|
1476
|
+
.set("apply_mask", false)
|
|
1477
|
+
.set("output_format", "png")
|
|
1478
|
+
.set("include_scores", true)
|
|
1479
|
+
.set("include_boxes", true);
|
|
1480
|
+
if (maxMasks != null) {
|
|
1481
|
+
b.set("max_masks", maxMasks).set("return_multiple_masks", maxMasks > 1);
|
|
1482
|
+
}
|
|
1483
|
+
// Each prompt kind is spelled differently per endpoint, and a name the
|
|
1484
|
+
// endpoint does not declare is dropped rather than sent — so a prompt written
|
|
1485
|
+
// to one name only would be silently discarded on half the SAM catalog.
|
|
1486
|
+
const prompt = params.prompt?.trim();
|
|
1487
|
+
const hasVisualPrompt = Boolean(params.points?.length || params.box);
|
|
1488
|
+
if (prompt) {
|
|
1489
|
+
b.setFirst(SEGMENT_TEXT_PROMPT_FIELDS, prompt);
|
|
1490
|
+
}
|
|
1491
|
+
else if (hasVisualPrompt) {
|
|
1492
|
+
// An absent field takes the endpoint's own default, and sam-3.1 defaults
|
|
1493
|
+
// `prompt` to "wheel" — so omitting the text prompt runs a concept search
|
|
1494
|
+
// for a wheel and the click is ignored. Measured against the live API: a
|
|
1495
|
+
// point alone returns `masks: []`, the same point with `prompt: ""`
|
|
1496
|
+
// returns the mask at score 0.51.
|
|
1497
|
+
b.forceFirst(SEGMENT_TEXT_PROMPT_FIELDS, "");
|
|
1498
|
+
}
|
|
1499
|
+
if (params.points?.length) {
|
|
1500
|
+
b.setFirst(SEGMENT_POINT_PROMPT_FIELDS, params.points.map((p) => ({
|
|
1501
|
+
x: Math.round(p.x),
|
|
1502
|
+
y: Math.round(p.y),
|
|
1503
|
+
label: p.include ? 1 : 0
|
|
1504
|
+
})));
|
|
1505
|
+
}
|
|
1506
|
+
if (params.box) {
|
|
1507
|
+
// fal takes box corners, not an origin and a size.
|
|
1508
|
+
b.setFirst(SEGMENT_BOX_PROMPT_FIELDS, [
|
|
1509
|
+
{
|
|
1510
|
+
x_min: Math.round(params.box.x),
|
|
1511
|
+
y_min: Math.round(params.box.y),
|
|
1512
|
+
x_max: Math.round(params.box.x + params.box.width),
|
|
1513
|
+
y_max: Math.round(params.box.y + params.box.height)
|
|
1514
|
+
}
|
|
1515
|
+
]);
|
|
1516
|
+
}
|
|
1517
|
+
return b.args;
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Refuse a call whose target nothing names.
|
|
1521
|
+
*
|
|
1522
|
+
* An endpoint that declares a prompt field segments only what a prompt points
|
|
1523
|
+
* at, and answers an unprompted call with either a 422 (`fal-ai/sam2/image`:
|
|
1524
|
+
* "Please provide at least one of prompts or box_prompts") or a silent empty
|
|
1525
|
+
* mask list (`fal-ai/sam-3-1/image`, which fills the absent `prompt` with its
|
|
1526
|
+
* own default). Both cost a request and neither says what to do about it.
|
|
1527
|
+
* Endpoints that declare no prompt field at all — sam2/auto-segment, birefnet
|
|
1528
|
+
* — find objects on their own and run unprompted.
|
|
1529
|
+
*/
|
|
1530
|
+
function assertSegmentPromptIsUsable(modelId, params) {
|
|
1531
|
+
if (params.prompt?.trim() || params.points?.length || params.box) {
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const b = new FalArgsBuilder(modelId);
|
|
1535
|
+
const takesAPrompt = [
|
|
1536
|
+
...SEGMENT_TEXT_PROMPT_FIELDS,
|
|
1537
|
+
...SEGMENT_POINT_PROMPT_FIELDS,
|
|
1538
|
+
...SEGMENT_BOX_PROMPT_FIELDS
|
|
1539
|
+
].some((field) => b.declares(field));
|
|
1540
|
+
if (!takesAPrompt) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
throw new Error(`${modelId} needs a prompt: name what to find (for example "the dog"), ` +
|
|
1544
|
+
"click a point, or drag a box. To split a layer with no prompt, pick a " +
|
|
1545
|
+
"model that finds objects on its own, such as fal-ai/sam2/auto-segment.");
|
|
1546
|
+
}
|
|
1547
|
+
/** Text-prompt field names across fal's segmentation endpoints, in priority order. */
|
|
1548
|
+
const SEGMENT_TEXT_PROMPT_FIELDS = [
|
|
1549
|
+
"prompt",
|
|
1550
|
+
"text_prompt",
|
|
1551
|
+
"text_input",
|
|
1552
|
+
"object"
|
|
1553
|
+
];
|
|
1554
|
+
const SEGMENT_POINT_PROMPT_FIELDS = ["point_prompts", "prompts"];
|
|
1555
|
+
const SEGMENT_BOX_PROMPT_FIELDS = ["box_prompts"];
|
|
1556
|
+
/**
|
|
1557
|
+
* Read one mask image.
|
|
1558
|
+
*
|
|
1559
|
+
* fal is inconsistent here: a `list[Image]` field usually holds `{url, width,
|
|
1560
|
+
* height}` objects, but the string-typed mask fields (`combined_mask`,
|
|
1561
|
+
* `mask_image`) hold a bare URL, and some endpoints nest the image one level
|
|
1562
|
+
* down. Accepting only the object form dropped whole responses on the floor.
|
|
1563
|
+
*/
|
|
1564
|
+
function asFalMaskImage(value) {
|
|
1565
|
+
if (isNonEmptyString(value)) {
|
|
1566
|
+
return { url: value, width: null, height: null };
|
|
1567
|
+
}
|
|
1568
|
+
if (!isRecord(value)) {
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
if (isNonEmptyString(value.url)) {
|
|
1572
|
+
return {
|
|
1573
|
+
url: value.url,
|
|
1574
|
+
width: finiteNumberOrNull(value.width),
|
|
1575
|
+
height: finiteNumberOrNull(value.height)
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
return asFalMaskImage(value.image);
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* The mask images in a fal segmentation response, in the order the endpoints
|
|
1582
|
+
* prefer: every list field first, then the single-mask fields.
|
|
1583
|
+
*
|
|
1584
|
+
* Each endpoint names this differently — sam-3/3-1 answer with `masks`,
|
|
1585
|
+
* sam2/auto-segment with `individual_masks` plus a `combined_mask` URL, and
|
|
1586
|
+
* the single-mask endpoints (evf-sam, sam2/image, birefnet, the florence
|
|
1587
|
+
* variants) with one `image` or `mask_image`.
|
|
1588
|
+
*/
|
|
1589
|
+
function collectFalMaskImages(data) {
|
|
1590
|
+
for (const field of ["masks", "individual_masks", "images"]) {
|
|
1591
|
+
const value = data[field];
|
|
1592
|
+
if (Array.isArray(value)) {
|
|
1593
|
+
const masks = value
|
|
1594
|
+
.map(asFalMaskImage)
|
|
1595
|
+
.filter((mask) => mask !== null);
|
|
1596
|
+
if (masks.length > 0) {
|
|
1597
|
+
return masks;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
for (const field of ["image", "mask_image", "combined_mask", "mask"]) {
|
|
1602
|
+
const mask = asFalMaskImage(data[field]);
|
|
1603
|
+
if (mask) {
|
|
1604
|
+
return [mask];
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1609
|
+
/** A side-channel field fal sends as JSON text on some endpoints and as a value on others. */
|
|
1610
|
+
function parseFalJsonField(value) {
|
|
1611
|
+
if (!isString(value)) {
|
|
1612
|
+
return value;
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
return JSON.parse(value);
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
// Not JSON — the field carries an opaque string this parser has no use for.
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
function finiteNumberOrNull(value) {
|
|
1623
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Convert one entry of fal's `boxes` output into source-image pixels. fal sends
|
|
1627
|
+
* normalized `[cx, cy, w, h]` (center and size, 0–1), which needs the mask's
|
|
1628
|
+
* own size to become a rectangle.
|
|
1629
|
+
*/
|
|
1630
|
+
function normalizeFalBox(value, width, height) {
|
|
1631
|
+
if (Array.isArray(value) && value.length >= 4) {
|
|
1632
|
+
const [cx, cy, w, h] = value.map(finiteNumberOrNull);
|
|
1633
|
+
if (cx == null || cy == null || w == null || h == null) {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
if (width == null || height == null) {
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
return {
|
|
1640
|
+
x: Math.round((cx - w / 2) * width),
|
|
1641
|
+
y: Math.round((cy - h / 2) * height),
|
|
1642
|
+
width: Math.round(w * width),
|
|
1643
|
+
height: Math.round(h * height)
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (isRecord(value)) {
|
|
1647
|
+
const x = finiteNumberOrNull(value.x);
|
|
1648
|
+
const y = finiteNumberOrNull(value.y);
|
|
1649
|
+
const w = finiteNumberOrNull(value.width);
|
|
1650
|
+
const h = finiteNumberOrNull(value.height);
|
|
1651
|
+
if (x != null && y != null && w != null && h != null) {
|
|
1652
|
+
return { x, y, width: w, height: h };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return null;
|
|
1656
|
+
}
|
|
1657
|
+
/** The label and score fal reports per mask under `metadata`. */
|
|
1658
|
+
function falMaskMetadata(entries, index) {
|
|
1659
|
+
const list = parseFalJsonField(entries);
|
|
1660
|
+
const entry = Array.isArray(list) ? list[index] : index === 0 ? list : null;
|
|
1661
|
+
if (!isRecord(entry)) {
|
|
1662
|
+
return { label: null, score: null, box: null };
|
|
1663
|
+
}
|
|
1664
|
+
const label = isNonEmptyString(entry.label)
|
|
1665
|
+
? entry.label
|
|
1666
|
+
: isNonEmptyString(entry.name)
|
|
1667
|
+
? entry.name
|
|
1668
|
+
: null;
|
|
1669
|
+
return {
|
|
1670
|
+
label,
|
|
1671
|
+
score: finiteNumberOrNull(entry.score),
|
|
1672
|
+
box: entry.box ?? entry.bbox ?? entry.bounds ?? null
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
async function collectSegmentationMasks(data, minConfidence) {
|
|
1676
|
+
const rawMasks = collectFalMaskImages(data);
|
|
1677
|
+
const scores = parseFalJsonField(data.scores);
|
|
1678
|
+
const boxes = parseFalJsonField(data.boxes);
|
|
1679
|
+
const threshold = minConfidence ?? null;
|
|
1680
|
+
const masks = [];
|
|
1681
|
+
let bestScore = null;
|
|
1682
|
+
for (let i = 0; i < rawMasks.length; i++) {
|
|
1683
|
+
const { url, width, height } = rawMasks[i];
|
|
1684
|
+
const meta = falMaskMetadata(data.metadata, i);
|
|
1685
|
+
const confidence = meta.score ??
|
|
1686
|
+
(Array.isArray(scores) ? finiteNumberOrNull(scores[i]) : null);
|
|
1687
|
+
if (confidence != null && (bestScore == null || confidence > bestScore)) {
|
|
1688
|
+
bestScore = confidence;
|
|
1689
|
+
}
|
|
1690
|
+
if (threshold != null && confidence != null && confidence < threshold) {
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const box = normalizeFalBox(meta.box, width, height) ??
|
|
1694
|
+
(Array.isArray(boxes) ? normalizeFalBox(boxes[i], width, height) : null);
|
|
1695
|
+
masks.push({
|
|
1696
|
+
mask: await downloadBytes(url),
|
|
1697
|
+
mimeType: "image/png",
|
|
1698
|
+
width,
|
|
1699
|
+
height,
|
|
1700
|
+
label: meta.label,
|
|
1701
|
+
confidence,
|
|
1702
|
+
box
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return { masks, decoded: rawMasks.length, bestScore };
|
|
1706
|
+
}
|
|
1707
|
+
export function extractImageUrl(result) {
|
|
418
1708
|
return extractImageUrls(result)[0];
|
|
419
1709
|
}
|
|
420
|
-
function extractVideoUrl(result) {
|
|
1710
|
+
export function extractVideoUrl(result) {
|
|
421
1711
|
// FAL video endpoints return { video: { url } } or { video_url } or { url }
|
|
422
1712
|
const video = result.video;
|
|
423
1713
|
if (video?.url)
|
|
424
1714
|
return video.url;
|
|
425
|
-
if (
|
|
1715
|
+
if (isString(result.video_url))
|
|
426
1716
|
return result.video_url;
|
|
427
|
-
if (
|
|
1717
|
+
if (isString(result.url))
|
|
428
1718
|
return result.url;
|
|
429
1719
|
// Some endpoints return an array of videos
|
|
430
1720
|
const videos = result.videos;
|
|
@@ -435,8 +1725,23 @@ function extractVideoUrl(result) {
|
|
|
435
1725
|
}
|
|
436
1726
|
throw new Error(`Unexpected FAL video response: ${JSON.stringify(result)}`);
|
|
437
1727
|
}
|
|
438
|
-
|
|
439
|
-
|
|
1728
|
+
export function extractAudioUrl(result) {
|
|
1729
|
+
// FAL TTS endpoints return { audio: { url } } or { audio: "url" } or
|
|
1730
|
+
// { audio_url } or a bare { url }.
|
|
1731
|
+
const audio = result.audio;
|
|
1732
|
+
if (isObjectLike(audio) && isString(audio.url)) {
|
|
1733
|
+
return audio.url;
|
|
1734
|
+
}
|
|
1735
|
+
if (isString(audio))
|
|
1736
|
+
return audio;
|
|
1737
|
+
if (isString(result.audio_url))
|
|
1738
|
+
return result.audio_url;
|
|
1739
|
+
if (isString(result.url))
|
|
1740
|
+
return result.url;
|
|
1741
|
+
throw new Error(`Unexpected FAL audio response: ${JSON.stringify(result)}`);
|
|
1742
|
+
}
|
|
1743
|
+
async function downloadBytes(url, signal) {
|
|
1744
|
+
const res = await safeFetch(url, signal ? { signal } : undefined);
|
|
440
1745
|
if (!res.ok)
|
|
441
1746
|
throw new Error(`Failed to download FAL result: ${res.status}`);
|
|
442
1747
|
const buf = await res.arrayBuffer();
|