@tanstack/ai-fal 0.9.5 → 0.9.6

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.
Files changed (39) hide show
  1. package/dist/esm/adapters/audio.d.ts +32 -0
  2. package/dist/esm/adapters/audio.js +83 -0
  3. package/dist/esm/adapters/audio.js.map +1 -0
  4. package/dist/esm/adapters/image.d.ts +36 -0
  5. package/dist/esm/adapters/image.js +115 -0
  6. package/dist/esm/adapters/image.js.map +1 -0
  7. package/dist/esm/adapters/speech.d.ts +32 -0
  8. package/dist/esm/adapters/speech.js +87 -0
  9. package/dist/esm/adapters/speech.js.map +1 -0
  10. package/dist/esm/adapters/transcription.d.ts +32 -0
  11. package/dist/esm/adapters/transcription.js +93 -0
  12. package/dist/esm/adapters/transcription.js.map +1 -0
  13. package/dist/esm/adapters/video.d.ts +34 -0
  14. package/dist/esm/adapters/video.js +165 -0
  15. package/dist/esm/adapters/video.js.map +1 -0
  16. package/dist/esm/image/generated/image-field-overrides.d.ts +1279 -0
  17. package/dist/esm/image/generated/image-field-overrides.js +368 -0
  18. package/dist/esm/image/generated/image-field-overrides.js.map +1 -0
  19. package/dist/esm/image/image-inputs.d.ts +50 -0
  20. package/dist/esm/image/image-inputs.js +119 -0
  21. package/dist/esm/image/image-inputs.js.map +1 -0
  22. package/dist/esm/image/image-provider-options.d.ts +2 -0
  23. package/dist/esm/image/image-provider-options.js +23 -0
  24. package/dist/esm/image/image-provider-options.js.map +1 -0
  25. package/dist/esm/index.d.ts +7 -0
  26. package/dist/esm/index.js +22 -0
  27. package/dist/esm/index.js.map +1 -0
  28. package/dist/esm/model-meta.d.ts +129 -0
  29. package/dist/esm/utils/billing.d.ts +39 -0
  30. package/dist/esm/utils/billing.js +55 -0
  31. package/dist/esm/utils/billing.js.map +1 -0
  32. package/dist/esm/utils/client.d.ts +45 -0
  33. package/dist/esm/utils/client.js +101 -0
  34. package/dist/esm/utils/client.js.map +1 -0
  35. package/dist/esm/utils/index.d.ts +2 -0
  36. package/dist/esm/video/video-provider-options.d.ts +2 -0
  37. package/dist/esm/video/video-provider-options.js +18 -0
  38. package/dist/esm/video/video-provider-options.js.map +1 -0
  39. package/package.json +4 -4
@@ -0,0 +1,119 @@
1
+ import { FAL_IMAGE_FIELD_OVERRIDES } from "./generated/image-field-overrides.js";
2
+ const DEFAULT_FIELDS = {
3
+ single: "image_url",
4
+ multi: "image_urls",
5
+ mask: "mask_url",
6
+ control: "control_image_url",
7
+ reference: "reference_image_urls",
8
+ start: "start_image_url",
9
+ end: "end_image_url"
10
+ };
11
+ const LIST_FIELDS = /* @__PURE__ */ new Set([
12
+ "image_urls",
13
+ "input_image_urls",
14
+ "ref_image_urls",
15
+ "reference_image_urls"
16
+ ]);
17
+ function fieldSpecFor(model) {
18
+ const overrides = FAL_IMAGE_FIELD_OVERRIDES[model];
19
+ return { ...DEFAULT_FIELDS, ...overrides };
20
+ }
21
+ function assignField(fields, field, urls, model, what) {
22
+ if (urls.length === 0) return;
23
+ const existing = fields[field];
24
+ if (LIST_FIELDS.has(field)) {
25
+ fields[field] = Array.isArray(existing) ? [...existing, ...urls] : urls;
26
+ } else if (existing !== void 0) {
27
+ throw new Error(
28
+ `fal: multiple inputs map to '${field}' on model ${model}. Drop one of the conflicting inputs or pass the field explicitly via modelOptions.`
29
+ );
30
+ } else if (urls.length === 1) {
31
+ fields[field] = urls[0];
32
+ } else {
33
+ throw new Error(
34
+ `fal: model ${model} accepts a single ${what} image via '${field}' (received ${urls.length}).`
35
+ );
36
+ }
37
+ }
38
+ function bucketByRole(imageInputs) {
39
+ const buckets = {
40
+ sources: [],
41
+ masks: [],
42
+ controls: [],
43
+ references: [],
44
+ starts: [],
45
+ ends: []
46
+ };
47
+ for (const part of imageInputs) {
48
+ const url = imagePartToUrl(part);
49
+ const role = part.metadata?.role;
50
+ if (role === "mask") buckets.masks.push(url);
51
+ else if (role === "control") buckets.controls.push(url);
52
+ else if (role === "reference" || role === "character")
53
+ buckets.references.push(url);
54
+ else if (role === "start_frame") buckets.starts.push(url);
55
+ else if (role === "end_frame") buckets.ends.push(url);
56
+ else buckets.sources.push(url);
57
+ }
58
+ return buckets;
59
+ }
60
+ function mapImageInputsToFalFields(model, imageInputs) {
61
+ if (!imageInputs || imageInputs.length === 0) return {};
62
+ const spec = fieldSpecFor(model);
63
+ const { sources, masks, controls, references, starts, ends } = bucketByRole(imageInputs);
64
+ const allSources = [...sources, ...starts, ...ends];
65
+ if (masks.length > 1) {
66
+ throw new Error(
67
+ `fal: only one input with metadata.role === 'mask' is supported per request (received ${masks.length}).`
68
+ );
69
+ }
70
+ if (controls.length > 1) {
71
+ throw new Error(
72
+ `fal: only one input with metadata.role === 'control' is supported per request (received ${controls.length}).`
73
+ );
74
+ }
75
+ const fields = {};
76
+ const sourceField = allSources.length > 1 ? spec.multi : spec.single;
77
+ assignField(fields, sourceField, allSources, model, "source");
78
+ assignField(fields, spec.reference, references, model, "reference");
79
+ assignField(fields, spec.mask, masks, model, "mask");
80
+ assignField(fields, spec.control, controls, model, "control");
81
+ return fields;
82
+ }
83
+ function mapImageInputsToFalVideoFields(model, imageInputs) {
84
+ if (!imageInputs || imageInputs.length === 0) return {};
85
+ const spec = fieldSpecFor(model);
86
+ const { sources, masks, controls, references, starts, ends } = bucketByRole(imageInputs);
87
+ if (masks.length > 0 || controls.length > 0) {
88
+ const role = masks.length > 0 ? "mask" : "control";
89
+ throw new Error(
90
+ `fal: metadata.role === '${role}' is not supported for video generation on model ${model}. Remove the role or pass the field explicitly via modelOptions.`
91
+ );
92
+ }
93
+ if (starts.length > 1) {
94
+ throw new Error(
95
+ `fal: only one input with metadata.role === 'start_frame' is supported (received ${starts.length}).`
96
+ );
97
+ }
98
+ if (ends.length > 1) {
99
+ throw new Error(
100
+ `fal: only one input with metadata.role === 'end_frame' is supported (received ${ends.length}).`
101
+ );
102
+ }
103
+ const fields = {};
104
+ const sourceField = sources.length > 1 ? spec.multi : spec.single;
105
+ assignField(fields, sourceField, sources, model, "source");
106
+ assignField(fields, spec.reference, references, model, "reference");
107
+ assignField(fields, spec.start, starts, model, "start frame");
108
+ assignField(fields, spec.end, ends, model, "end frame");
109
+ return fields;
110
+ }
111
+ function imagePartToUrl(part) {
112
+ if (part.source.type === "url") return part.source.value;
113
+ return `data:${part.source.mimeType};base64,${part.source.value}`;
114
+ }
115
+ export {
116
+ mapImageInputsToFalFields,
117
+ mapImageInputsToFalVideoFields
118
+ };
119
+ //# sourceMappingURL=image-inputs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-inputs.js","sources":["../../../src/image/image-inputs.ts"],"sourcesContent":["import { FAL_IMAGE_FIELD_OVERRIDES } from './generated/image-field-overrides'\nimport type {\n FalImageFieldName,\n FalImageFieldOverride,\n} from './generated/image-field-overrides'\nimport type { ImagePart, MediaInputMetadata } from '@tanstack/ai'\nimport type { FalModel, FalModelInput } from '../model-meta'\n\n/**\n * The image-conditioning fields the mappers may set, narrowed to the ones\n * that actually exist on the given endpoint's input type. For endpoints\n * unknown to the installed `@fal-ai/client` this widens to all known field\n * names.\n */\nexport type FalImageInputFields<TModel extends string> = Partial<\n Pick<\n FalModelInput<TModel>,\n Extract<keyof FalModelInput<TModel>, FalImageFieldName>\n >\n>\n\n/**\n * Default field per routing role. Endpoint-specific deviations live in the\n * generated `FAL_IMAGE_FIELD_OVERRIDES` map (regenerate with\n * `pnpm generate:fal-image-fields`); these defaults must stay in sync with\n * `DEFAULTS` in scripts/generate-fal-image-field-map.ts.\n */\nconst DEFAULT_FIELDS = {\n single: 'image_url',\n multi: 'image_urls',\n mask: 'mask_url',\n control: 'control_image_url',\n reference: 'reference_image_urls',\n start: 'start_image_url',\n end: 'end_image_url',\n} satisfies Required<FalImageFieldOverride>\n\n/**\n * Field names that accept an array of images. The generator asserts the\n * SDK types agree with this set, so wrap-vs-scalar decisions stay correct.\n */\nconst LIST_FIELDS = new Set<string>([\n 'image_urls',\n 'input_image_urls',\n 'ref_image_urls',\n 'reference_image_urls',\n])\n\n/** Resolve the per-role field names for a model: defaults + generated overrides. */\nfunction fieldSpecFor(model: string): Required<FalImageFieldOverride> {\n const overrides = (\n FAL_IMAGE_FIELD_OVERRIDES as Record<string, FalImageFieldOverride>\n )[model]\n return { ...DEFAULT_FIELDS, ...overrides }\n}\n\n/**\n * Assign URLs to a field, wrapping or unwrapping based on whether the field\n * takes an array. When two roles resolve to the same list field (e.g.\n * sources and references both land on `image_urls` for nano-banana edit)\n * the values are merged in assignment order; two roles resolving to the\n * same scalar field is ambiguous and throws. Throws when multiple images\n * target a scalar field.\n */\nfunction assignField(\n fields: Record<string, unknown>,\n field: string,\n urls: Array<string>,\n model: string,\n what: string,\n): void {\n if (urls.length === 0) return\n const existing = fields[field]\n if (LIST_FIELDS.has(field)) {\n fields[field] = Array.isArray(existing) ? [...existing, ...urls] : urls\n } else if (existing !== undefined) {\n throw new Error(\n `fal: multiple inputs map to '${field}' on model ${model}. Drop one of the conflicting inputs or pass the field explicitly via modelOptions.`,\n )\n } else if (urls.length === 1) {\n fields[field] = urls[0]\n } else {\n throw new Error(\n `fal: model ${model} accepts a single ${what} image via '${field}' (received ${urls.length}).`,\n )\n }\n}\n\ninterface RoleBuckets {\n sources: Array<string>\n masks: Array<string>\n controls: Array<string>\n references: Array<string>\n starts: Array<string>\n ends: Array<string>\n}\n\nfunction bucketByRole(\n imageInputs: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): RoleBuckets {\n const buckets: RoleBuckets = {\n sources: [],\n masks: [],\n controls: [],\n references: [],\n starts: [],\n ends: [],\n }\n for (const part of imageInputs) {\n const url = imagePartToUrl(part)\n const role = part.metadata?.role\n if (role === 'mask') buckets.masks.push(url)\n else if (role === 'control') buckets.controls.push(url)\n else if (role === 'reference' || role === 'character')\n buckets.references.push(url)\n else if (role === 'start_frame') buckets.starts.push(url)\n else if (role === 'end_frame') buckets.ends.push(url)\n else buckets.sources.push(url)\n }\n return buckets\n}\n\n/**\n * Map the prompt's image parts onto fal.ai image-endpoint fields.\n *\n * fal endpoints use different field names for image-conditioned generation\n * (~80% use `image_url` for single; the rest use `image_urls`,\n * `reference_image_urls`, `mask_url`, `control_image_url`, etc.). Field\n * names are resolved per endpoint from the generated\n * `FAL_IMAGE_FIELD_OVERRIDES` map (derived from the fal SDK's endpoint\n * types), falling back to the defaults above for endpoints the installed\n * SDK doesn't know:\n *\n * - parts with `metadata.role === 'mask'` → spec.mask (single)\n * - parts with `metadata.role === 'control'` → spec.control (single)\n * - `role === 'reference' | 'character'` → spec.reference\n * - `role === 'start_frame' | 'end_frame'` → treated as sources (frame\n * roles only apply to video generation)\n * - remaining parts → spec.single / spec.multi\n *\n * Users can always override the resulting field shape via `modelOptions`\n * (spread before these fields), or pass everything through `modelOptions`\n * directly when the mapping doesn't match an obscure endpoint.\n */\nexport function mapImageInputsToFalFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Frame roles aren't meaningful for image generation; treat as the\n // primary source. The video mapper handles start/end framing.\n const allSources = [...sources, ...starts, ...ends]\n\n if (masks.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'mask' is supported per request (received ${masks.length}).`,\n )\n }\n if (controls.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'control' is supported per request (received ${controls.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = allSources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, allSources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n assignField(fields, spec.mask, masks, model, 'mask')\n assignField(fields, spec.control, controls, model, 'control')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Map the prompt's image parts onto fal.ai video-endpoint fields.\n *\n * Video endpoints often expose a start frame as `image_url` (76% of i2v\n * models) plus an optional `end_image_url`. Multi-reference video models\n * (Kling O3, Seedance reference-to-video) use `reference_image_urls` or\n * `image_urls`. Field names resolve through the same generated override\n * map as the image mapper — e.g. `role: 'start_frame'` lands on `image_url`\n * for Kling/Veo image-to-video and `first_frame_url` for Pixverse. Mapping:\n *\n * - `metadata.role === 'start_frame'` → spec.start\n * - `metadata.role === 'end_frame'` → spec.end\n * - `metadata.role === 'reference' | 'character'` → spec.reference\n * - `metadata.role === 'mask' | 'control'` → throws (no video routing)\n * - remaining parts (no role) → spec.single / spec.multi\n */\nexport function mapImageInputsToFalVideoFields<TModel extends FalModel>(\n model: TModel,\n imageInputs?: ReadonlyArray<ImagePart<MediaInputMetadata>>,\n): FalImageInputFields<TModel> {\n if (!imageInputs || imageInputs.length === 0) return {}\n\n const spec = fieldSpecFor(model)\n const { sources, masks, controls, references, starts, ends } =\n bucketByRole(imageInputs)\n // Mask / control roles have no video-specific routing; silently repurposing\n // them as source frames would hide the problem, so reject them instead.\n if (masks.length > 0 || controls.length > 0) {\n const role = masks.length > 0 ? 'mask' : 'control'\n throw new Error(\n `fal: metadata.role === '${role}' is not supported for video generation on model ${model}. ` +\n `Remove the role or pass the field explicitly via modelOptions.`,\n )\n }\n\n if (starts.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'start_frame' is supported (received ${starts.length}).`,\n )\n }\n if (ends.length > 1) {\n throw new Error(\n `fal: only one input with metadata.role === 'end_frame' is supported (received ${ends.length}).`,\n )\n }\n\n const fields: Record<string, unknown> = {}\n const sourceField = sources.length > 1 ? spec.multi : spec.single\n assignField(fields, sourceField, sources, model, 'source')\n assignField(fields, spec.reference, references, model, 'reference')\n // Frame roles assign last: when an endpoint routes the start frame to its\n // generic source field (e.g. Kling image-to-video) and an unroled source\n // was also provided, assignField rejects the ambiguous combination.\n assignField(fields, spec.start, starts, model, 'start frame')\n assignField(fields, spec.end, ends, model, 'end frame')\n\n return fields as FalImageInputFields<TModel>\n}\n\n/**\n * Convert a TanStack ImagePart into a string suitable for fal's URL-based\n * input fields. URL sources pass through; data sources are emitted as a\n * `data:<mime>;base64,<value>` URI which fal endpoints accept on the wire.\n */\nfunction imagePartToUrl(part: ImagePart<MediaInputMetadata>): string {\n if (part.source.type === 'url') return part.source.value\n return `data:${part.source.mimeType};base64,${part.source.value}`\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { FalModelImageSize, FalModelImageSizeInput } from '../model-meta.js';
2
+ export declare function mapSizeToFalFormat<TModel extends string>(size: FalModelImageSize<TModel> | undefined): FalModelImageSizeInput<TModel> | undefined;
@@ -0,0 +1,23 @@
1
+ function mapSizeToFalFormat(size) {
2
+ if (!size) return void 0;
3
+ if (typeof size === "string") {
4
+ if (size.includes("_")) {
5
+ const [first, second] = size.split("_");
6
+ if (first && first.includes(":")) {
7
+ return {
8
+ aspect_ratio: first,
9
+ resolution: second
10
+ };
11
+ }
12
+ } else if (size.includes(":")) {
13
+ return { aspect_ratio: size };
14
+ }
15
+ }
16
+ return {
17
+ image_size: size
18
+ };
19
+ }
20
+ export {
21
+ mapSizeToFalFormat
22
+ };
23
+ //# sourceMappingURL=image-provider-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-provider-options.js","sources":["../../../src/image/image-provider-options.ts"],"sourcesContent":["import type { FalModelImageSize, FalModelImageSizeInput } from '../model-meta'\n\nexport function mapSizeToFalFormat<TModel extends string>(\n size: FalModelImageSize<TModel> | undefined,\n): FalModelImageSizeInput<TModel> | undefined {\n if (!size) return undefined\n\n // \"16:9_4K\" → { aspect_ratio, resolution }\n // \"16:9\" → { aspect_ratio }\n // \"4K\" → { resolution } (no colon, no underscore, model has `resolution`)\n // \"square_hd\" → { image_size } (no colon, no resolution field on model)\n if (typeof size === 'string') {\n if (size.includes('_')) {\n const [first, second] = size.split('_')\n if (first && first.includes(':')) {\n return {\n aspect_ratio: first,\n resolution: second,\n } as FalModelImageSizeInput<TModel>\n }\n } else if (size.includes(':')) {\n return { aspect_ratio: size } as FalModelImageSizeInput<TModel>\n }\n }\n\n return {\n image_size: size,\n } as FalModelImageSizeInput<TModel>\n}\n"],"names":[],"mappings":"AAEO,SAAS,mBACd,MAC4C;AAC5C,MAAI,CAAC,KAAM,QAAO;AAMlB,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,KAAK,SAAS,GAAG,GAAG;AACtB,YAAM,CAAC,OAAO,MAAM,IAAI,KAAK,MAAM,GAAG;AACtC,UAAI,SAAS,MAAM,SAAS,GAAG,GAAG;AAChC,eAAO;AAAA,UACL,cAAc;AAAA,UACd,YAAY;AAAA,QAAA;AAAA,MAEhB;AAAA,IACF,WAAW,KAAK,SAAS,GAAG,GAAG;AAC7B,aAAO,EAAE,cAAc,KAAA;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAY;AAAA,EAAA;AAEhB;"}
@@ -0,0 +1,7 @@
1
+ export { FalImageAdapter, falImage } from './adapters/image.js';
2
+ export { FalVideoAdapter, falVideo } from './adapters/video.js';
3
+ export { FalSpeechAdapter, falSpeech } from './adapters/speech.js';
4
+ export { FalTranscriptionAdapter, falTranscription, } from './adapters/transcription.js';
5
+ export { FalAudioAdapter, falAudio } from './adapters/audio.js';
6
+ export { type FalImageProviderOptions, type FalVideoProviderOptions, type FalSpeechProviderOptions, type FalTranscriptionProviderOptions, type FalAudioProviderOptions, type FalModel, type FalModelInput, type FalModelOutput, type FalModelImageSize, type FalModelVideoSize, } from './model-meta.js';
7
+ export { getFalApiKeyFromEnv, configureFalClient, generateId, type FalClientConfig, } from './utils.js';
@@ -0,0 +1,22 @@
1
+ import { FalImageAdapter, falImage } from "./adapters/image.js";
2
+ import { FalVideoAdapter, falVideo } from "./adapters/video.js";
3
+ import { FalSpeechAdapter, falSpeech } from "./adapters/speech.js";
4
+ import { FalTranscriptionAdapter, falTranscription } from "./adapters/transcription.js";
5
+ import { FalAudioAdapter, falAudio } from "./adapters/audio.js";
6
+ import { configureFalClient, generateId, getFalApiKeyFromEnv } from "./utils/client.js";
7
+ export {
8
+ FalAudioAdapter,
9
+ FalImageAdapter,
10
+ FalSpeechAdapter,
11
+ FalTranscriptionAdapter,
12
+ FalVideoAdapter,
13
+ configureFalClient,
14
+ falAudio,
15
+ falImage,
16
+ falSpeech,
17
+ falTranscription,
18
+ falVideo,
19
+ generateId,
20
+ getFalApiKeyFromEnv
21
+ };
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,129 @@
1
+ import { EndpointTypeMap } from '@fal-ai/client/endpoints';
2
+ import { MediaPromptModality } from '@tanstack/ai';
3
+ import { FalImageFieldName } from './image/generated/image-field-overrides.js';
4
+ export type { EndpointTypeMap } from '@fal-ai/client/endpoints';
5
+ /**
6
+ * All known fal.ai model IDs with autocomplete support.
7
+ * Also accepts any string for custom/new models.
8
+ */
9
+ export type FalModel = keyof EndpointTypeMap | (string & {});
10
+ /**
11
+ * Utility type to extract the input type for a specific fal model.
12
+ *
13
+ * @example
14
+ * type FluxInput = FalModelInput<'fal-ai/flux/dev'>
15
+ * // { prompt: string; num_inference_steps?: number; ... }
16
+ */
17
+ export type FalModelInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['input'] : Record<string, unknown>;
18
+ /**
19
+ * Utility type to extract the output type for a specific fal model.
20
+ *
21
+ * @example
22
+ * type FluxOutput = FalModelOutput<'fal-ai/flux/dev'>
23
+ * // { images: Array<Image>; seed: number; ... }
24
+ */
25
+ export type FalModelOutput<TModel extends string> = TModel extends keyof EndpointTypeMap ? EndpointTypeMap[TModel]['output'] : unknown;
26
+ /**
27
+ * Extract the image_size type supported by a specific fal model.
28
+ * Returns never if the model doesn't support image_size.
29
+ *
30
+ * @example
31
+ * type FluxSize = FalModelImageSize<'fal-ai/flux/dev'>
32
+ * // "square_hd" | "square" | "portrait_4_3" | "portrait_16_9" | ...
33
+ */
34
+ export type FalModelImageSize<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'image_size' extends keyof EndpointTypeMap[TModel]['input'] ? NonNullable<Exclude<FalModelInput<TModel>['image_size'], object>> : 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}` : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string> : undefined : string;
35
+ export type FalModelImageSizeInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
36
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
37
+ resolution: FalModelInput<TModel>['resolution'];
38
+ } : {
39
+ aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
40
+ } : 'image_size' extends keyof EndpointTypeMap[TModel]['input'] ? {
41
+ image_size: FalModelImageSize<TModel>;
42
+ } : never : {
43
+ image_size: string;
44
+ };
45
+ /**
46
+ * Input fields the prompt-part mappers can populate: image conditioning via
47
+ * the generated `FalImageFieldName` set, video conditioning via
48
+ * `video_url` / `video_urls` / `reference_video_urls`, audio via `audio_url`.
49
+ */
50
+ type FalMediaInputFieldName = FalImageFieldName | 'video_url' | 'video_urls' | 'reference_video_urls' | 'audio_url';
51
+ /**
52
+ * Demote an endpoint input's media-conditioning fields from required to
53
+ * optional. Image-to-video endpoints declare e.g. `image_url` as a required
54
+ * input, but with a multimodal `prompt` the start frame usually arrives as a
55
+ * prompt part — requiring it in `modelOptions` too would force redundancy.
56
+ * The fields stay passable via `modelOptions` as the documented escape hatch
57
+ * (and override-wise the mapped prompt-part fields win on conflict).
58
+ */
59
+ type WithOptionalMediaInputFields<TInput> = Omit<TInput, Extract<keyof TInput, FalMediaInputFieldName>> & Partial<Pick<TInput, Extract<keyof TInput, FalMediaInputFieldName>>>;
60
+ /**
61
+ * Provider options for image generation, excluding fields TanStack AI handles.
62
+ * Use this for the `modelOptions` parameter in image generation.
63
+ *
64
+ * @example
65
+ * type FluxOptions = FalImageProviderOptions<'fal-ai/flux/dev'>
66
+ * // { num_inference_steps?: number; guidance_scale?: number; seed?: number; ... }
67
+ */
68
+ export type FalImageProviderOptions<TModel extends string> = WithOptionalMediaInputFields<Omit<FalModelInput<TModel>, 'prompt'>>;
69
+ /**
70
+ * Extract the video size type supported by a specific fal model.
71
+ * Video models typically use aspect_ratio and/or resolution fields.
72
+ *
73
+ * - aspect_ratio + resolution → "16:9_720p"
74
+ * - aspect_ratio only → "16:9"
75
+ * - resolution only → "720p"
76
+ * - neither → undefined (the model takes no size param)
77
+ * - unknown models → string
78
+ */
79
+ export type FalModelVideoSize<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? `${Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string>}_${Extract<NonNullable<FalModelInput<TModel>['resolution']>, string>}` : Extract<NonNullable<FalModelInput<TModel>['aspect_ratio']>, string> : 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? Extract<NonNullable<FalModelInput<TModel>['resolution']>, string> : undefined : string;
80
+ export type FalModelVideoSizeInput<TModel extends string> = TModel extends keyof EndpointTypeMap ? 'aspect_ratio' extends keyof EndpointTypeMap[TModel]['input'] ? 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
81
+ aspect_ratio: FalModelInput<TModel>['aspect_ratio'];
82
+ resolution: FalModelInput<TModel>['resolution'];
83
+ } : {
84
+ aspect_ratio: NonNullable<FalModelInput<TModel>['aspect_ratio']>;
85
+ } : 'resolution' extends keyof EndpointTypeMap[TModel]['input'] ? {
86
+ resolution: NonNullable<FalModelInput<TModel>['resolution']>;
87
+ } : never : {
88
+ aspect_ratio?: string;
89
+ resolution?: string;
90
+ };
91
+ /**
92
+ * Prompt input modalities for a fal image endpoint, derived from the SDK's
93
+ * endpoint input type: an endpoint accepts image prompt parts exactly when
94
+ * its input declares one of the known image-conditioning fields
95
+ * (`image_url`, `image_urls`, `mask_url`, …). Endpoints unknown to the
96
+ * installed SDK are unconstrained.
97
+ */
98
+ export type FalImagePromptModalitiesFor<TModel extends string> = TModel extends keyof EndpointTypeMap ? ReadonlyArray<Extract<keyof FalModelInput<TModel>, FalImageFieldName> extends never ? never : 'image'> : ReadonlyArray<MediaPromptModality>;
99
+ /**
100
+ * Prompt input modalities for a fal video endpoint. Image conditioning is
101
+ * detected via the same field set as image endpoints; video conditioning via
102
+ * `video_url` / `video_urls` / `reference_video_urls`; audio conditioning
103
+ * via `audio_url`. Endpoints unknown to the installed SDK are unconstrained.
104
+ */
105
+ export type FalVideoPromptModalitiesFor<TModel extends string> = TModel extends keyof EndpointTypeMap ? ReadonlyArray<(Extract<keyof FalModelInput<TModel>, FalImageFieldName> extends never ? never : 'image') | (Extract<keyof FalModelInput<TModel>, 'video_url' | 'video_urls' | 'reference_video_urls'> extends never ? never : 'video') | (Extract<keyof FalModelInput<TModel>, 'audio_url'> extends never ? never : 'audio')> : ReadonlyArray<MediaPromptModality>;
106
+ /**
107
+ * Provider options for video generation, excluding fields TanStack AI handles.
108
+ * Use this for the `modelOptions` parameter in video generation.
109
+ *
110
+ * Media-conditioning fields (start/end frame, reference images, source
111
+ * video/audio) are optional here even when the endpoint requires them —
112
+ * they're usually supplied as prompt parts instead.
113
+ */
114
+ export type FalVideoProviderOptions<TModel extends string> = TModel extends keyof EndpointTypeMap ? WithOptionalMediaInputFields<Omit<FalModelInput<TModel>, 'prompt'>> : Record<string, unknown>;
115
+ /**
116
+ * Provider options for TTS, excluding fields TanStack AI handles.
117
+ * Use this for the `modelOptions` parameter in speech generation.
118
+ */
119
+ export type FalSpeechProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'prompt' | 'text'>;
120
+ /**
121
+ * Provider options for transcription, excluding fields TanStack AI handles.
122
+ * Use this for the `modelOptions` parameter in transcription.
123
+ */
124
+ export type FalTranscriptionProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'audio_url'>;
125
+ /**
126
+ * Provider options for audio generation, excluding fields TanStack AI handles.
127
+ * Use this for the `modelOptions` parameter in audio generation.
128
+ */
129
+ export type FalAudioProviderOptions<TModel extends string> = Omit<FalModelInput<TModel>, 'prompt'>;
@@ -0,0 +1,39 @@
1
+ import { TokenUsage } from '@tanstack/ai';
2
+ /**
3
+ * Parse the `x-fal-billable-units` header value into a finite number. Returns
4
+ * `undefined` for a missing or non-numeric value so callers can skip attaching
5
+ * usage rather than surfacing `NaN`.
6
+ */
7
+ export declare function parseBillableUnits(value: string | null): number | undefined;
8
+ /**
9
+ * Record the billable units carried by a fal result response, keyed by the
10
+ * request id from the same response. Reading headers does not consume the body,
11
+ * so the response can be returned to the fal client untouched.
12
+ */
13
+ export declare function recordBillableUnitsFromResponse(response: Response): void;
14
+ /**
15
+ * Read and remove the billable units recorded for a request id. Removing on read
16
+ * keeps the registry from growing across the lifetime of the process.
17
+ */
18
+ export declare function takeBillableUnits(requestId: string | undefined): number | undefined;
19
+ /**
20
+ * Build a {@link TokenUsage} carrying fal's billed quantity. Media generation has
21
+ * no tokens, so the token fields are zero and the real billing signal rides on
22
+ * `unitsBilled` — mirroring how the duration-billed transcription adapters
23
+ * surface `durationSeconds`. Returns `undefined` when no units were captured so
24
+ * callers can omit `usage` entirely.
25
+ */
26
+ export declare function buildFalUsage(unitsBilled: number | undefined): TokenUsage | undefined;
27
+ /**
28
+ * Wrap a fetch so every fal request's response is inspected for the
29
+ * `x-fal-billable-units` header before being returned untouched. Installed as
30
+ * fal's `config.fetch`, which (unlike a global `responseHandler`) is honoured for
31
+ * every request — the fal client forces `resultResponseHandler` per queue
32
+ * operation, clobbering any configured response handler.
33
+ *
34
+ * `baseFetch` is the underlying implementation to delegate to (defaults to the
35
+ * global `fetch`). Injecting it keeps usage capture working when a caller
36
+ * supplies a custom fetch — a proxy, instrumentation, or a test mock — without
37
+ * mutating any global.
38
+ */
39
+ export declare function createBillingFetch(baseFetch?: typeof fetch): typeof fetch;
@@ -0,0 +1,55 @@
1
+ const FAL_BILLABLE_UNITS_HEADER = "x-fal-billable-units";
2
+ const FAL_REQUEST_ID_HEADER = "x-fal-request-id";
3
+ const MAX_PENDING_ENTRIES = 256;
4
+ const billableUnitsByRequestId = /* @__PURE__ */ new Map();
5
+ function parseBillableUnits(value) {
6
+ if (value == null || value === "") return void 0;
7
+ const parsed = Number(value);
8
+ return Number.isFinite(parsed) ? parsed : void 0;
9
+ }
10
+ function recordBillableUnitsFromResponse(response) {
11
+ const units = parseBillableUnits(
12
+ response.headers.get(FAL_BILLABLE_UNITS_HEADER)
13
+ );
14
+ if (units == null) return;
15
+ const requestId = response.headers.get(FAL_REQUEST_ID_HEADER);
16
+ if (!requestId) return;
17
+ if (billableUnitsByRequestId.size >= MAX_PENDING_ENTRIES && !billableUnitsByRequestId.has(requestId)) {
18
+ const oldest = billableUnitsByRequestId.keys().next().value;
19
+ if (oldest !== void 0) billableUnitsByRequestId.delete(oldest);
20
+ }
21
+ billableUnitsByRequestId.set(requestId, units);
22
+ }
23
+ function takeBillableUnits(requestId) {
24
+ if (!requestId) return void 0;
25
+ const units = billableUnitsByRequestId.get(requestId);
26
+ if (units !== void 0) billableUnitsByRequestId.delete(requestId);
27
+ return units;
28
+ }
29
+ function buildFalUsage(unitsBilled) {
30
+ if (unitsBilled == null) return void 0;
31
+ return {
32
+ promptTokens: 0,
33
+ completionTokens: 0,
34
+ totalTokens: 0,
35
+ unitsBilled
36
+ };
37
+ }
38
+ function createBillingFetch(baseFetch = globalThis.fetch) {
39
+ return async (input, init) => {
40
+ const response = await baseFetch(input, init);
41
+ try {
42
+ recordBillableUnitsFromResponse(response);
43
+ } catch {
44
+ }
45
+ return response;
46
+ };
47
+ }
48
+ export {
49
+ buildFalUsage,
50
+ createBillingFetch,
51
+ parseBillableUnits,
52
+ recordBillableUnitsFromResponse,
53
+ takeBillableUnits
54
+ };
55
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sources":["../../../src/utils/billing.ts"],"sourcesContent":["import type { TokenUsage } from '@tanstack/ai'\n\n/**\n * Response header fal sets on a queue *result* fetch carrying the real billed\n * quantity for the generation, denominated in the endpoint's priced unit.\n */\nconst FAL_BILLABLE_UNITS_HEADER = 'x-fal-billable-units'\n\n/**\n * Response header fal sets carrying the request id. The fal client surfaces this\n * same value as `Result.requestId`, so keying captured billable units by it\n * guarantees the adapter's lookup matches the fetch the units came from — no URL\n * parsing or global correlation registry of our own design needed.\n */\nconst FAL_REQUEST_ID_HEADER = 'x-fal-request-id'\n\n/**\n * Upper bound on retained, not-yet-consumed billable-unit entries. Each\n * successful generation reads-and-deletes its entry (see {@link takeBillableUnits}),\n * so this only guards against an unbounded leak when a result fetch records units\n * but the adapter never resolves (e.g. it throws before reading). When the cap is\n * exceeded the oldest entry is evicted (Map preserves insertion order).\n */\nconst MAX_PENDING_ENTRIES = 256\n\nconst billableUnitsByRequestId = new Map<string, number>()\n\n/**\n * Parse the `x-fal-billable-units` header value into a finite number. Returns\n * `undefined` for a missing or non-numeric value so callers can skip attaching\n * usage rather than surfacing `NaN`.\n */\nexport function parseBillableUnits(value: string | null): number | undefined {\n if (value == null || value === '') return undefined\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : undefined\n}\n\n/**\n * Record the billable units carried by a fal result response, keyed by the\n * request id from the same response. Reading headers does not consume the body,\n * so the response can be returned to the fal client untouched.\n */\nexport function recordBillableUnitsFromResponse(response: Response): void {\n const units = parseBillableUnits(\n response.headers.get(FAL_BILLABLE_UNITS_HEADER),\n )\n if (units == null) return\n const requestId = response.headers.get(FAL_REQUEST_ID_HEADER)\n if (!requestId) return\n if (\n billableUnitsByRequestId.size >= MAX_PENDING_ENTRIES &&\n !billableUnitsByRequestId.has(requestId)\n ) {\n const oldest = billableUnitsByRequestId.keys().next().value\n if (oldest !== undefined) billableUnitsByRequestId.delete(oldest)\n }\n billableUnitsByRequestId.set(requestId, units)\n}\n\n/**\n * Read and remove the billable units recorded for a request id. Removing on read\n * keeps the registry from growing across the lifetime of the process.\n */\nexport function takeBillableUnits(\n requestId: string | undefined,\n): number | undefined {\n if (!requestId) return undefined\n const units = billableUnitsByRequestId.get(requestId)\n if (units !== undefined) billableUnitsByRequestId.delete(requestId)\n return units\n}\n\n/**\n * Build a {@link TokenUsage} carrying fal's billed quantity. Media generation has\n * no tokens, so the token fields are zero and the real billing signal rides on\n * `unitsBilled` — mirroring how the duration-billed transcription adapters\n * surface `durationSeconds`. Returns `undefined` when no units were captured so\n * callers can omit `usage` entirely.\n */\nexport function buildFalUsage(\n unitsBilled: number | undefined,\n): TokenUsage | undefined {\n if (unitsBilled == null) return undefined\n return {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n unitsBilled,\n }\n}\n\n/**\n * Wrap a fetch so every fal request's response is inspected for the\n * `x-fal-billable-units` header before being returned untouched. Installed as\n * fal's `config.fetch`, which (unlike a global `responseHandler`) is honoured for\n * every request — the fal client forces `resultResponseHandler` per queue\n * operation, clobbering any configured response handler.\n *\n * `baseFetch` is the underlying implementation to delegate to (defaults to the\n * global `fetch`). Injecting it keeps usage capture working when a caller\n * supplies a custom fetch — a proxy, instrumentation, or a test mock — without\n * mutating any global.\n */\nexport function createBillingFetch(\n baseFetch: typeof fetch = globalThis.fetch,\n): typeof fetch {\n return async (input, init) => {\n const response = await baseFetch(input, init)\n try {\n recordBillableUnitsFromResponse(response)\n } catch {\n // Capturing usage must never break the underlying request.\n }\n return response\n }\n}\n"],"names":[],"mappings":"AAMA,MAAM,4BAA4B;AAQlC,MAAM,wBAAwB;AAS9B,MAAM,sBAAsB;AAE5B,MAAM,+CAA+B,IAAA;AAO9B,SAAS,mBAAmB,OAA0C;AAC3E,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAC1C,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAOO,SAAS,gCAAgC,UAA0B;AACxE,QAAM,QAAQ;AAAA,IACZ,SAAS,QAAQ,IAAI,yBAAyB;AAAA,EAAA;AAEhD,MAAI,SAAS,KAAM;AACnB,QAAM,YAAY,SAAS,QAAQ,IAAI,qBAAqB;AAC5D,MAAI,CAAC,UAAW;AAChB,MACE,yBAAyB,QAAQ,uBACjC,CAAC,yBAAyB,IAAI,SAAS,GACvC;AACA,UAAM,SAAS,yBAAyB,KAAA,EAAO,OAAO;AACtD,QAAI,WAAW,OAAW,0BAAyB,OAAO,MAAM;AAAA,EAClE;AACA,2BAAyB,IAAI,WAAW,KAAK;AAC/C;AAMO,SAAS,kBACd,WACoB;AACpB,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,yBAAyB,IAAI,SAAS;AACpD,MAAI,UAAU,OAAW,0BAAyB,OAAO,SAAS;AAClE,SAAO;AACT;AASO,SAAS,cACd,aACwB;AACxB,MAAI,eAAe,KAAM,QAAO;AAChC,SAAO;AAAA,IACL,cAAc;AAAA,IACd,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb;AAAA,EAAA;AAEJ;AAcO,SAAS,mBACd,YAA0B,WAAW,OACvB;AACd,SAAO,OAAO,OAAO,SAAS;AAC5B,UAAM,WAAW,MAAM,UAAU,OAAO,IAAI;AAC5C,QAAI;AACF,sCAAgC,QAAQ;AAAA,IAC1C,QAAQ;AAAA,IAER;AACA,WAAO;AAAA,EACT;AACF;"}
@@ -0,0 +1,45 @@
1
+ export interface FalClientConfig {
2
+ apiKey: string;
3
+ proxyUrl?: string;
4
+ /**
5
+ * Override the underlying fetch used for fal requests. The adapter wraps it to
6
+ * read the `x-fal-billable-units` header (see ./billing.ts), so usage capture
7
+ * still works. Defaults to the global `fetch`. Useful for proxying,
8
+ * instrumentation, or pointing requests at a mock in tests.
9
+ */
10
+ fetch?: typeof fetch;
11
+ }
12
+ export declare function getFalApiKeyFromEnv(): string;
13
+ export declare function configureFalClient(config?: FalClientConfig): void;
14
+ export declare function generateId(prefix: string): string;
15
+ /**
16
+ * Extract a safe file extension from a URL. Strips query strings, URL
17
+ * fragments, and any trailing slashes, and only returns the extension when
18
+ * it looks like a real one (2-5 alphanumeric chars). Returns undefined
19
+ * otherwise so callers can fall back to a default.
20
+ */
21
+ export declare function extractUrlExtension(url: string): string | undefined;
22
+ /**
23
+ * Derive a reasonable audio content-type. Prefers the explicit MIME (stripped
24
+ * of parameters), then an extension-based lookup for common audio formats,
25
+ * otherwise falls back to audio/mpeg — fal URLs virtually always serve mp3.
26
+ */
27
+ export declare function deriveAudioContentType(explicitContentType: string | undefined, url: string): string;
28
+ /**
29
+ * Decode a `data:` URL into a Blob so fal-client can auto-upload it via
30
+ * `fal.storage.upload`. fal's inference API rejects data URLs with a 422
31
+ * "Unsupported data URL", so we convert them before handing them off.
32
+ *
33
+ * Supports both base64 and URL-encoded data URLs. Returns `undefined` for
34
+ * anything that isn't a data URL, so callers can fall through to other
35
+ * handling (http URLs are passed to fal as-is).
36
+ */
37
+ export declare function dataUrlToBlob(value: string): Blob | undefined;
38
+ /**
39
+ * Convert an ArrayBuffer to base64 in a cross-runtime way.
40
+ *
41
+ * The naive `btoa(String.fromCharCode(...bytes))` form blows up V8's argument
42
+ * limit (~65k) on realistic audio payloads, so we either use `Buffer` (Node /
43
+ * Bun) or walk the byte array in a single loop (browser).
44
+ */
45
+ export declare function arrayBufferToBase64(bytes: ArrayBuffer): string;
@@ -0,0 +1,101 @@
1
+ import { fal } from "@fal-ai/client";
2
+ import { generateId as generateId$1, getApiKeyFromEnv } from "@tanstack/ai-utils";
3
+ import { createBillingFetch } from "./billing.js";
4
+ function getFalApiKeyFromEnv() {
5
+ return getApiKeyFromEnv("FAL_KEY");
6
+ }
7
+ function configureFalClient(config) {
8
+ const apiKey = config?.apiKey ?? getFalApiKeyFromEnv();
9
+ fal.config({
10
+ credentials: apiKey,
11
+ // Wrap the (optionally overridden) fetch to read fal's
12
+ // `x-fal-billable-units` header off every response so adapters can surface
13
+ // the billed quantity as `result.usage`. See ./billing.ts.
14
+ fetch: createBillingFetch(config?.fetch),
15
+ ...config?.proxyUrl ? { proxyUrl: config.proxyUrl } : {}
16
+ });
17
+ }
18
+ function generateId(prefix) {
19
+ return generateId$1(prefix);
20
+ }
21
+ function extractUrlExtension(url) {
22
+ let pathname;
23
+ try {
24
+ const parsed = new URL(url);
25
+ pathname = parsed.pathname;
26
+ } catch {
27
+ pathname = url.split("?")[0]?.split("#")[0] ?? url;
28
+ }
29
+ const normalized = pathname.replace(/\/+$/, "");
30
+ if (!normalized.includes("/")) return void 0;
31
+ const lastSegment = normalized.split("/").pop();
32
+ if (!lastSegment) return void 0;
33
+ const extension = lastSegment.split(".").pop();
34
+ if (!extension || extension === lastSegment) return void 0;
35
+ return /^[a-z0-9]{2,5}$/i.test(extension) ? extension : void 0;
36
+ }
37
+ function deriveAudioContentType(explicitContentType, url) {
38
+ const stripped = explicitContentType?.split(";")[0]?.trim();
39
+ if (stripped) return stripped;
40
+ const ext = extractUrlExtension(url)?.toLowerCase();
41
+ switch (ext) {
42
+ case "mp3":
43
+ return "audio/mpeg";
44
+ case "wav":
45
+ return "audio/wav";
46
+ case "ogg":
47
+ case "oga":
48
+ return "audio/ogg";
49
+ case "flac":
50
+ return "audio/flac";
51
+ case "aac":
52
+ return "audio/aac";
53
+ case "m4a":
54
+ case "mp4":
55
+ return "audio/mp4";
56
+ case "webm":
57
+ return "audio/webm";
58
+ case void 0:
59
+ default:
60
+ return "audio/mpeg";
61
+ }
62
+ }
63
+ function dataUrlToBlob(value) {
64
+ if (!value.startsWith("data:")) return void 0;
65
+ const commaIndex = value.indexOf(",");
66
+ if (commaIndex === -1) return void 0;
67
+ const header = value.slice(5, commaIndex);
68
+ const payload = value.slice(commaIndex + 1);
69
+ const isBase64 = /;base64$/i.test(header);
70
+ const mimeType = header.split(";")[0] || "application/octet-stream";
71
+ if (isBase64) {
72
+ const binary = atob(payload);
73
+ const bytes = new Uint8Array(binary.length);
74
+ for (let i = 0; i < binary.length; i += 1) {
75
+ bytes[i] = binary.charCodeAt(i);
76
+ }
77
+ return new Blob([bytes], { type: mimeType });
78
+ }
79
+ return new Blob([decodeURIComponent(payload)], { type: mimeType });
80
+ }
81
+ function arrayBufferToBase64(bytes) {
82
+ if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
83
+ return Buffer.from(bytes).toString("base64");
84
+ }
85
+ const view = new Uint8Array(bytes);
86
+ let binary = "";
87
+ for (const byte of view) {
88
+ binary += String.fromCharCode(byte);
89
+ }
90
+ return btoa(binary);
91
+ }
92
+ export {
93
+ arrayBufferToBase64,
94
+ configureFalClient,
95
+ dataUrlToBlob,
96
+ deriveAudioContentType,
97
+ extractUrlExtension,
98
+ generateId,
99
+ getFalApiKeyFromEnv
100
+ };
101
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\nimport { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'\nimport { createBillingFetch } from './billing'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\n /**\n * Override the underlying fetch used for fal requests. The adapter wraps it to\n * read the `x-fal-billable-units` header (see ./billing.ts), so usage capture\n * still works. Defaults to the global `fetch`. Useful for proxying,\n * instrumentation, or pointing requests at a mock in tests.\n */\n fetch?: typeof fetch\n}\n\nexport function getFalApiKeyFromEnv(): string {\n return getApiKeyFromEnv('FAL_KEY')\n}\n\nexport function configureFalClient(config?: FalClientConfig): void {\n const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()\n fal.config({\n credentials: apiKey,\n // Wrap the (optionally overridden) fetch to read fal's\n // `x-fal-billable-units` header off every response so adapters can surface\n // the billed quantity as `result.usage`. See ./billing.ts.\n fetch: createBillingFetch(config?.fetch),\n ...(config?.proxyUrl ? { proxyUrl: config.proxyUrl } : {}),\n })\n}\n\nexport function generateId(prefix: string): string {\n return _generateId(prefix)\n}\n\n/**\n * Extract a safe file extension from a URL. Strips query strings, URL\n * fragments, and any trailing slashes, and only returns the extension when\n * it looks like a real one (2-5 alphanumeric chars). Returns undefined\n * otherwise so callers can fall back to a default.\n */\nexport function extractUrlExtension(url: string): string | undefined {\n // Parse via URL when possible so we only look at the pathname and never\n // mistake a TLD (e.g. the `.com` in `https://x.com/`) for a file extension.\n let pathname: string\n try {\n const parsed = new URL(url)\n pathname = parsed.pathname\n } catch {\n // Fall back to treating the input as a raw path when URL parsing fails\n // (e.g. the caller passed a bare path). Still strip ?query and #fragment.\n pathname = url.split('?')[0]?.split('#')[0] ?? url\n }\n // Drop trailing slashes so `/path/audio.mp3/` still yields `mp3`.\n const normalized = pathname.replace(/\\/+$/, '')\n // Require at least one `/` — otherwise we're looking at an empty pathname\n // (bare-host URLs like `https://x.com/` land here after stripping the\n // trailing slash).\n if (!normalized.includes('/')) return undefined\n const lastSegment = normalized.split('/').pop()\n if (!lastSegment) return undefined\n const extension = lastSegment.split('.').pop()\n if (!extension || extension === lastSegment) return undefined\n return /^[a-z0-9]{2,5}$/i.test(extension) ? extension : undefined\n}\n\n/**\n * Derive a reasonable audio content-type. Prefers the explicit MIME (stripped\n * of parameters), then an extension-based lookup for common audio formats,\n * otherwise falls back to audio/mpeg — fal URLs virtually always serve mp3.\n */\nexport function deriveAudioContentType(\n explicitContentType: string | undefined,\n url: string,\n): string {\n const stripped = explicitContentType?.split(';')[0]?.trim()\n if (stripped) return stripped\n\n const ext = extractUrlExtension(url)?.toLowerCase()\n switch (ext) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'ogg':\n case 'oga':\n return 'audio/ogg'\n case 'flac':\n return 'audio/flac'\n case 'aac':\n return 'audio/aac'\n case 'm4a':\n case 'mp4':\n return 'audio/mp4'\n case 'webm':\n return 'audio/webm'\n case undefined:\n default:\n return 'audio/mpeg'\n }\n}\n\n/**\n * Decode a `data:` URL into a Blob so fal-client can auto-upload it via\n * `fal.storage.upload`. fal's inference API rejects data URLs with a 422\n * \"Unsupported data URL\", so we convert them before handing them off.\n *\n * Supports both base64 and URL-encoded data URLs. Returns `undefined` for\n * anything that isn't a data URL, so callers can fall through to other\n * handling (http URLs are passed to fal as-is).\n */\nexport function dataUrlToBlob(value: string): Blob | undefined {\n if (!value.startsWith('data:')) return undefined\n const commaIndex = value.indexOf(',')\n if (commaIndex === -1) return undefined\n\n const header = value.slice(5, commaIndex)\n const payload = value.slice(commaIndex + 1)\n const isBase64 = /;base64$/i.test(header)\n const mimeType = header.split(';')[0] || 'application/octet-stream'\n\n if (isBase64) {\n const binary = atob(payload)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) {\n bytes[i] = binary.charCodeAt(i)\n }\n return new Blob([bytes], { type: mimeType })\n }\n\n return new Blob([decodeURIComponent(payload)], { type: mimeType })\n}\n\n/**\n * Convert an ArrayBuffer to base64 in a cross-runtime way.\n *\n * The naive `btoa(String.fromCharCode(...bytes))` form blows up V8's argument\n * limit (~65k) on realistic audio payloads, so we either use `Buffer` (Node /\n * Bun) or walk the byte array in a single loop (browser).\n */\nexport function arrayBufferToBase64(bytes: ArrayBuffer): string {\n if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {\n return Buffer.from(bytes).toString('base64')\n }\n const view = new Uint8Array(bytes)\n let binary = ''\n for (const byte of view) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n"],"names":["_generateId"],"mappings":";;;AAgBO,SAAS,sBAA8B;AAC5C,SAAO,iBAAiB,SAAS;AACnC;AAEO,SAAS,mBAAmB,QAAgC;AACjE,QAAM,SAAS,QAAQ,UAAU,oBAAA;AACjC,MAAI,OAAO;AAAA,IACT,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,OAAO,mBAAmB,QAAQ,KAAK;AAAA,IACvC,GAAI,QAAQ,WAAW,EAAE,UAAU,OAAO,SAAA,IAAa,CAAA;AAAA,EAAC,CACzD;AACH;AAEO,SAAS,WAAW,QAAwB;AACjD,SAAOA,aAAY,MAAM;AAC3B;AAQO,SAAS,oBAAoB,KAAiC;AAGnE,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,eAAW,OAAO;AAAA,EACpB,QAAQ;AAGN,eAAW,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AAAA,EACjD;AAEA,QAAM,aAAa,SAAS,QAAQ,QAAQ,EAAE;AAI9C,MAAI,CAAC,WAAW,SAAS,GAAG,EAAG,QAAO;AACtC,QAAM,cAAc,WAAW,MAAM,GAAG,EAAE,IAAA;AAC1C,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,YAAY,YAAY,MAAM,GAAG,EAAE,IAAA;AACzC,MAAI,CAAC,aAAa,cAAc,YAAa,QAAO;AACpD,SAAO,mBAAmB,KAAK,SAAS,IAAI,YAAY;AAC1D;AAOO,SAAS,uBACd,qBACA,KACQ;AACR,QAAM,WAAW,qBAAqB,MAAM,GAAG,EAAE,CAAC,GAAG,KAAA;AACrD,MAAI,SAAU,QAAO;AAErB,QAAM,MAAM,oBAAoB,GAAG,GAAG,YAAA;AACtC,UAAQ,KAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EAAA;AAEb;AAWO,SAAS,cAAc,OAAiC;AAC7D,MAAI,CAAC,MAAM,WAAW,OAAO,EAAG,QAAO;AACvC,QAAM,aAAa,MAAM,QAAQ,GAAG;AACpC,MAAI,eAAe,GAAI,QAAO;AAE9B,QAAM,SAAS,MAAM,MAAM,GAAG,UAAU;AACxC,QAAM,UAAU,MAAM,MAAM,aAAa,CAAC;AAC1C,QAAM,WAAW,YAAY,KAAK,MAAM;AACxC,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK;AAEzC,MAAI,UAAU;AACZ,UAAM,SAAS,KAAK,OAAO;AAC3B,UAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AACzC,YAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AAAA,IAChC;AACA,WAAO,IAAI,KAAK,CAAC,KAAK,GAAG,EAAE,MAAM,UAAU;AAAA,EAC7C;AAEA,SAAO,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,GAAG,EAAE,MAAM,UAAU;AACnE;AASO,SAAS,oBAAoB,OAA4B;AAC9D,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,SAAS,YAAY;AACtE,WAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAAA,EAC7C;AACA,QAAM,OAAO,IAAI,WAAW,KAAK;AACjC,MAAI,SAAS;AACb,aAAW,QAAQ,MAAM;AACvB,cAAU,OAAO,aAAa,IAAI;AAAA,EACpC;AACA,SAAO,KAAK,MAAM;AACpB;"}
@@ -0,0 +1,2 @@
1
+ export { getFalApiKeyFromEnv, configureFalClient, generateId, arrayBufferToBase64, dataUrlToBlob, extractUrlExtension, deriveAudioContentType, type FalClientConfig, } from './client.js';
2
+ export { takeBillableUnits, buildFalUsage } from './billing.js';
@@ -0,0 +1,2 @@
1
+ import { FalModelVideoSize, FalModelVideoSizeInput } from '../model-meta.js';
2
+ export declare function mapVideoSizeToFalFormat<TModel extends string>(size: FalModelVideoSize<TModel> | undefined): FalModelVideoSizeInput<TModel> | undefined;