@tanstack/ai-fal 0.13.4 → 0.14.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.
@@ -0,0 +1,66 @@
1
+ import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters';
2
+ import { LiveVideoGenerationOptions, LiveVideoGenerationResult } from '@tanstack/ai';
3
+ import { FalClientConfig } from '../utils/client.js';
4
+ export declare const FAL_LIVE_VIDEO_MODELS: readonly ["minimax/h3-max/director"];
5
+ export type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number];
6
+ /**
7
+ * WMA / token app id for each public live model. Director's AsyncAPI lives at
8
+ * `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
9
+ * `minimax/h3-max/director` parses as alias `h3-max`.
10
+ */
11
+ export declare const FAL_LIVE_VIDEO_APP: {
12
+ readonly 'minimax/h3-max/director': "fal-ai/minimax-h3-max-director";
13
+ };
14
+ export declare function isFalLiveVideoModel(model: string): model is FalLiveVideoModel;
15
+ /**
16
+ * URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
17
+ * (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
18
+ * `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
19
+ * rejected so the proxy cannot run arbitrary fal apps.
20
+ */
21
+ export declare function allowedFalLiveVideoProxyTarget(raw: string): URL | null;
22
+ /**
23
+ * Provider options for fal live sessions. The browser applies resolution and
24
+ * aspect ratio on the WMA `configure` message. `tokenDuration` is the only
25
+ * server-side field: it sets how long the minted JWT lasts.
26
+ */
27
+ export interface FalLiveVideoProviderOptions {
28
+ /** JWT lifetime in seconds. Defaults to 300. */
29
+ tokenDuration?: number;
30
+ }
31
+ export type FalLiveVideoModelProviderOptionsByName = {
32
+ [M in FalLiveVideoModel]: FalLiveVideoProviderOptions;
33
+ };
34
+ /**
35
+ * fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
36
+ * `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
37
+ * server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
38
+ * credentials.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { generateLiveVideo } from '@tanstack/ai'
43
+ * import { falLiveVideo } from '@tanstack/ai-fal'
44
+ *
45
+ * const live = await generateLiveVideo({
46
+ * adapter: falLiveVideo('minimax/h3-max/director'),
47
+ * prompt: 'A chef tosses noodles in a steel wok, flames leaping',
48
+ * })
49
+ * // live.model is 'fal-ai/minimax-h3-max-director'
50
+ * ```
51
+ */
52
+ export declare class FalLiveVideoAdapter<TModel extends FalLiveVideoModel> extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {
53
+ readonly name: "fal";
54
+ private readonly clientConfig;
55
+ private readonly apiKey;
56
+ constructor(model: TModel, config?: FalClientConfig);
57
+ createLiveVideo(options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>): Promise<LiveVideoGenerationResult>;
58
+ }
59
+ /**
60
+ * Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
61
+ */
62
+ export declare function falLiveVideo<TModel extends FalLiveVideoModel>(model: TModel, config?: FalClientConfig): FalLiveVideoAdapter<TModel>;
63
+ /**
64
+ * Create a fal live-video adapter with an explicit API key.
65
+ */
66
+ export declare function createFalLiveVideo<TModel extends FalLiveVideoModel>(model: TModel, apiKey: string, config?: Omit<FalClientConfig, 'apiKey'>): FalLiveVideoAdapter<TModel>;
@@ -0,0 +1,149 @@
1
+ import { getFalApiKeyFromEnv } from "../utils/client.js";
2
+ import { BaseLiveVideoAdapter } from "@tanstack/ai/adapters";
3
+ import { generateId } from "@tanstack/ai-utils";
4
+ //#region src/adapters/live.ts
5
+ var DEFAULT_FAL_TOKEN_URL = "https://rest.fal.ai/tokens/";
6
+ var DEFAULT_TOKEN_DURATION_SECONDS = 300;
7
+ var FAL_LIVE_VIDEO_MODELS = ["minimax/h3-max/director"];
8
+ /**
9
+ * WMA / token app id for each public live model. Director's AsyncAPI lives at
10
+ * `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
11
+ * `minimax/h3-max/director` parses as alias `h3-max`.
12
+ */
13
+ var FAL_LIVE_VIDEO_APP = { "minimax/h3-max/director": "fal-ai/minimax-h3-max-director" };
14
+ function isFalLiveVideoModel(model) {
15
+ return FAL_LIVE_VIDEO_MODELS.includes(model);
16
+ }
17
+ var WMA_PROXY_PATHS = /* @__PURE__ */ new Set([
18
+ "/ice",
19
+ "/session",
20
+ "/session/heartbeat"
21
+ ]);
22
+ /**
23
+ * URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
24
+ * (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
25
+ * `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
26
+ * rejected so the proxy cannot run arbitrary fal apps.
27
+ */
28
+ function allowedFalLiveVideoProxyTarget(raw) {
29
+ let url;
30
+ try {
31
+ url = new URL(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+ if (url.protocol !== "https:") return null;
36
+ if (url.username || url.password || url.port || url.search || url.hash) return null;
37
+ const path = url.pathname.replace(/\/$/, "") || "/";
38
+ if (url.hostname === "wma.fal.run") return WMA_PROXY_PATHS.has(path) ? url : null;
39
+ if (url.hostname === "fal.run") {
40
+ for (const app of Object.values(FAL_LIVE_VIDEO_APP)) if (path === `/${app}/ice`) return url;
41
+ }
42
+ return null;
43
+ }
44
+ function resolveFalLiveApiKey(config) {
45
+ return config?.apiKey ?? getFalApiKeyFromEnv();
46
+ }
47
+ function readFalToken(body) {
48
+ if (typeof body === "string" && body.length > 0) return body;
49
+ if (typeof body === "object" && body !== null && "token" in body) {
50
+ const token = body.token;
51
+ if (typeof token === "string" && token.length > 0) return token;
52
+ }
53
+ throw new Error("fal token response did not include a token.");
54
+ }
55
+ /**
56
+ * fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
57
+ * `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
58
+ * server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
59
+ * credentials.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * import { generateLiveVideo } from '@tanstack/ai'
64
+ * import { falLiveVideo } from '@tanstack/ai-fal'
65
+ *
66
+ * const live = await generateLiveVideo({
67
+ * adapter: falLiveVideo('minimax/h3-max/director'),
68
+ * prompt: 'A chef tosses noodles in a steel wok, flames leaping',
69
+ * })
70
+ * // live.model is 'fal-ai/minimax-h3-max-director'
71
+ * ```
72
+ */
73
+ var FalLiveVideoAdapter = class extends BaseLiveVideoAdapter {
74
+ name = "fal";
75
+ clientConfig;
76
+ apiKey;
77
+ constructor(model, config) {
78
+ super(model, {});
79
+ this.apiKey = resolveFalLiveApiKey(config);
80
+ this.clientConfig = config;
81
+ }
82
+ async createLiveVideo(options) {
83
+ const { logger, prompt, abortSignal, modelOptions } = options;
84
+ const tokenDuration = modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS;
85
+ const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch;
86
+ logger.request(`activity=generateLiveVideo provider=fal model=${this.model}`, {
87
+ provider: "fal",
88
+ model: this.model
89
+ });
90
+ if (typeof fetchImpl !== "function") throw new Error("global fetch is not available. Pass `fetch` in the fal adapter config.");
91
+ try {
92
+ const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {
93
+ method: "POST",
94
+ headers: {
95
+ Authorization: `Key ${this.apiKey}`,
96
+ "Content-Type": "application/json"
97
+ },
98
+ body: JSON.stringify({
99
+ allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],
100
+ token_expiration: tokenDuration
101
+ }),
102
+ ...abortSignal ? { signal: abortSignal } : {}
103
+ });
104
+ if (!response.ok) {
105
+ const detail = await response.text().catch(() => "");
106
+ throw new Error(`fal token request failed (${response.status}${response.statusText ? ` ${response.statusText}` : ""})${detail ? `: ${detail}` : ""}`);
107
+ }
108
+ const token = readFalToken(await response.json());
109
+ const result = {
110
+ id: generateId("liveVideo"),
111
+ model: FAL_LIVE_VIDEO_APP[this.model],
112
+ token,
113
+ expiresAt: Date.now() + tokenDuration * 1e3,
114
+ prompt,
115
+ status: "ready"
116
+ };
117
+ logger.output(`activity=generateLiveVideo provider=fal model=${this.model}`, {
118
+ model: this.model,
119
+ status: result.status
120
+ });
121
+ return result;
122
+ } catch (error) {
123
+ logger.errors("fal.createLiveVideo failed", {
124
+ error,
125
+ source: "fal.createLiveVideo"
126
+ });
127
+ throw error;
128
+ }
129
+ }
130
+ };
131
+ /**
132
+ * Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
133
+ */
134
+ function falLiveVideo(model, config) {
135
+ return new FalLiveVideoAdapter(model, config);
136
+ }
137
+ /**
138
+ * Create a fal live-video adapter with an explicit API key.
139
+ */
140
+ function createFalLiveVideo(model, apiKey, config) {
141
+ return new FalLiveVideoAdapter(model, {
142
+ ...config,
143
+ apiKey
144
+ });
145
+ }
146
+ //#endregion
147
+ export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalLiveVideoAdapter, allowedFalLiveVideoProxyTarget, createFalLiveVideo, falLiveVideo, isFalLiveVideoModel };
148
+
149
+ //# sourceMappingURL=live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","names":[],"sources":["../../../src/adapters/live.ts"],"sourcesContent":["import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters'\nimport { generateId } from '@tanstack/ai-utils'\nimport { getFalApiKeyFromEnv } from '../utils/client'\nimport type {\n LiveVideoGenerationOptions,\n LiveVideoGenerationResult,\n} from '@tanstack/ai'\nimport type { FalClientConfig } from '../utils/client'\n\nconst DEFAULT_FAL_TOKEN_URL = 'https://rest.fal.ai/tokens/'\nconst DEFAULT_TOKEN_DURATION_SECONDS = 300\n\nexport const FAL_LIVE_VIDEO_MODELS = ['minimax/h3-max/director'] as const\n\nexport type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number]\n\n/**\n * WMA / token app id for each public live model. Director's AsyncAPI lives at\n * `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:\n * `minimax/h3-max/director` parses as alias `h3-max`.\n */\nexport const FAL_LIVE_VIDEO_APP = {\n 'minimax/h3-max/director': 'fal-ai/minimax-h3-max-director',\n} as const satisfies Record<FalLiveVideoModel, string>\n\nexport function isFalLiveVideoModel(model: string): model is FalLiveVideoModel {\n return (FAL_LIVE_VIDEO_MODELS as ReadonlyArray<string>).includes(model)\n}\n\nconst WMA_PROXY_PATHS = new Set(['/ice', '/session', '/session/heartbeat'])\n\n/**\n * URLs a Director live proxy may forward. WMA talks to `wma.fal.run`\n * (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to\n * `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is\n * rejected so the proxy cannot run arbitrary fal apps.\n */\nexport function allowedFalLiveVideoProxyTarget(raw: string): URL | null {\n let url: URL\n try {\n url = new URL(raw)\n } catch {\n return null\n }\n if (url.protocol !== 'https:') return null\n if (url.username || url.password || url.port || url.search || url.hash) {\n return null\n }\n const path = url.pathname.replace(/\\/$/, '') || '/'\n if (url.hostname === 'wma.fal.run') {\n return WMA_PROXY_PATHS.has(path) ? url : null\n }\n if (url.hostname === 'fal.run') {\n for (const app of Object.values(FAL_LIVE_VIDEO_APP)) {\n if (path === `/${app}/ice`) return url\n }\n }\n return null\n}\n\n/**\n * Provider options for fal live sessions. The browser applies resolution and\n * aspect ratio on the WMA `configure` message. `tokenDuration` is the only\n * server-side field: it sets how long the minted JWT lasts.\n */\nexport interface FalLiveVideoProviderOptions {\n /** JWT lifetime in seconds. Defaults to 300. */\n tokenDuration?: number\n}\n\nexport type FalLiveVideoModelProviderOptionsByName = {\n [M in FalLiveVideoModel]: FalLiveVideoProviderOptions\n}\n\nfunction resolveFalLiveApiKey(config?: FalClientConfig): string {\n return config?.apiKey ?? getFalApiKeyFromEnv()\n}\n\nfunction readFalToken(body: unknown): string {\n if (typeof body === 'string' && body.length > 0) return body\n if (typeof body === 'object' && body !== null && 'token' in body) {\n const token = body.token\n if (typeof token === 'string' && token.length > 0) return token\n }\n throw new Error('fal token response did not include a token.')\n}\n\n/**\n * fal live-video adapter. Mints a scoped JWT and returns the WMA app id on\n * `result.model`. Open with `fal.realtime.open(wma(live.model))` through a\n * server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key\n * credentials.\n *\n * @example\n * ```ts\n * import { generateLiveVideo } from '@tanstack/ai'\n * import { falLiveVideo } from '@tanstack/ai-fal'\n *\n * const live = await generateLiveVideo({\n * adapter: falLiveVideo('minimax/h3-max/director'),\n * prompt: 'A chef tosses noodles in a steel wok, flames leaping',\n * })\n * // live.model is 'fal-ai/minimax-h3-max-director'\n * ```\n */\nexport class FalLiveVideoAdapter<\n TModel extends FalLiveVideoModel,\n> extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {\n readonly name = 'fal' as const\n private readonly clientConfig: FalClientConfig | undefined\n private readonly apiKey: string\n\n constructor(model: TModel, config?: FalClientConfig) {\n super(model, {})\n this.apiKey = resolveFalLiveApiKey(config)\n this.clientConfig = config\n }\n\n async createLiveVideo(\n options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>,\n ): Promise<LiveVideoGenerationResult> {\n const { logger, prompt, abortSignal, modelOptions } = options\n const tokenDuration =\n modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS\n const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch\n\n logger.request(\n `activity=generateLiveVideo provider=fal model=${this.model}`,\n {\n provider: 'fal',\n model: this.model,\n },\n )\n\n if (typeof fetchImpl !== 'function') {\n throw new Error(\n 'global fetch is not available. Pass `fetch` in the fal adapter config.',\n )\n }\n\n try {\n const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {\n method: 'POST',\n headers: {\n Authorization: `Key ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],\n token_expiration: tokenDuration,\n }),\n ...(abortSignal ? { signal: abortSignal } : {}),\n })\n\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `fal token request failed (${response.status}${\n response.statusText ? ` ${response.statusText}` : ''\n })${detail ? `: ${detail}` : ''}`,\n )\n }\n\n const token = readFalToken(await response.json())\n const result: LiveVideoGenerationResult = {\n id: generateId('liveVideo'),\n model: FAL_LIVE_VIDEO_APP[this.model],\n token,\n expiresAt: Date.now() + tokenDuration * 1000,\n prompt,\n status: 'ready',\n }\n\n logger.output(\n `activity=generateLiveVideo provider=fal model=${this.model}`,\n {\n model: this.model,\n status: result.status,\n },\n )\n\n return result\n } catch (error) {\n logger.errors('fal.createLiveVideo failed', {\n error,\n source: 'fal.createLiveVideo',\n })\n throw error\n }\n }\n}\n\n/**\n * Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.\n */\nexport function falLiveVideo<TModel extends FalLiveVideoModel>(\n model: TModel,\n config?: FalClientConfig,\n): FalLiveVideoAdapter<TModel> {\n return new FalLiveVideoAdapter(model, config)\n}\n\n/**\n * Create a fal live-video adapter with an explicit API key.\n */\nexport function createFalLiveVideo<TModel extends FalLiveVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<FalClientConfig, 'apiKey'>,\n): FalLiveVideoAdapter<TModel> {\n return new FalLiveVideoAdapter(model, { ...config, apiKey })\n}\n"],"mappings":";;;;AASA,IAAM,wBAAwB;AAC9B,IAAM,iCAAiC;AAEvC,IAAa,wBAAwB,CAAC,yBAAyB;;;;;;AAS/D,IAAa,qBAAqB,EAChC,2BAA2B,iCAC7B;AAEA,SAAgB,oBAAoB,OAA2C;CAC7E,OAAQ,sBAAgD,SAAS,KAAK;AACxE;AAEA,IAAM,kCAAkB,IAAI,IAAI;CAAC;CAAQ;CAAY;AAAoB,CAAC;;;;;;;AAQ1E,SAAgB,+BAA+B,KAAyB;CACtE,IAAI;CACJ,IAAI;EACF,MAAM,IAAI,IAAI,GAAG;CACnB,QAAQ;EACN,OAAO;CACT;CACA,IAAI,IAAI,aAAa,UAAU,OAAO;CACtC,IAAI,IAAI,YAAY,IAAI,YAAY,IAAI,QAAQ,IAAI,UAAU,IAAI,MAChE,OAAO;CAET,MAAM,OAAO,IAAI,SAAS,QAAQ,OAAO,EAAE,KAAK;CAChD,IAAI,IAAI,aAAa,eACnB,OAAO,gBAAgB,IAAI,IAAI,IAAI,MAAM;CAE3C,IAAI,IAAI,aAAa,WACd;OAAA,MAAM,OAAO,OAAO,OAAO,kBAAkB,GAChD,IAAI,SAAS,IAAI,IAAI,OAAO,OAAO;CAAA;CAGvC,OAAO;AACT;AAgBA,SAAS,qBAAqB,QAAkC;CAC9D,OAAO,QAAQ,UAAU,oBAAoB;AAC/C;AAEA,SAAS,aAAa,MAAuB;CAC3C,IAAI,OAAO,SAAS,YAAY,KAAK,SAAS,GAAG,OAAO;CACxD,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,WAAW,MAAM;EAChE,MAAM,QAAQ,KAAK;EACnB,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG,OAAO;CAC5D;CACA,MAAM,IAAI,MAAM,6CAA6C;AAC/D;;;;;;;;;;;;;;;;;;;AAoBA,IAAa,sBAAb,cAEU,qBAA0D;CAClE,OAAgB;CAChB;CACA;CAEA,YAAY,OAAe,QAA0B;EACnD,MAAM,OAAO,CAAC,CAAC;EACf,KAAK,SAAS,qBAAqB,MAAM;EACzC,KAAK,eAAe;CACtB;CAEA,MAAM,gBACJ,SACoC;EACpC,MAAM,EAAE,QAAQ,QAAQ,aAAa,iBAAiB;EACtD,MAAM,gBACJ,cAAc,iBAAiB;EACjC,MAAM,YAAY,KAAK,cAAc,SAAS,WAAW;EAEzD,OAAO,QACL,iDAAiD,KAAK,SACtD;GACE,UAAU;GACV,OAAO,KAAK;EACd,CACF;EAEA,IAAI,OAAO,cAAc,YACvB,MAAM,IAAI,MACR,wEACF;EAGF,IAAI;GACF,MAAM,WAAW,MAAM,UAAU,uBAAuB;IACtD,QAAQ;IACR,SAAS;KACP,eAAe,OAAO,KAAK;KAC3B,gBAAgB;IAClB;IACA,MAAM,KAAK,UAAU;KACnB,cAAc,CAAC,mBAAmB,KAAK,MAAM;KAC7C,kBAAkB;IACpB,CAAC;IACD,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;GAC/C,CAAC;GAED,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,SAAS,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,EAAE;IACnD,MAAM,IAAI,MACR,6BAA6B,SAAS,SACpC,SAAS,aAAa,IAAI,SAAS,eAAe,GACnD,GAAG,SAAS,KAAK,WAAW,IAC/B;GACF;GAEA,MAAM,QAAQ,aAAa,MAAM,SAAS,KAAK,CAAC;GAChD,MAAM,SAAoC;IACxC,IAAI,WAAW,WAAW;IAC1B,OAAO,mBAAmB,KAAK;IAC/B;IACA,WAAW,KAAK,IAAI,IAAI,gBAAgB;IACxC;IACA,QAAQ;GACV;GAEA,OAAO,OACL,iDAAiD,KAAK,SACtD;IACE,OAAO,KAAK;IACZ,QAAQ,OAAO;GACjB,CACF;GAEA,OAAO;EACT,SAAS,OAAO;GACd,OAAO,OAAO,8BAA8B;IAC1C;IACA,QAAQ;GACV,CAAC;GACD,MAAM;EACR;CACF;AACF;;;;AAKA,SAAgB,aACd,OACA,QAC6B;CAC7B,OAAO,IAAI,oBAAoB,OAAO,MAAM;AAC9C;;;;AAKA,SAAgB,mBACd,OACA,QACA,QAC6B;CAC7B,OAAO,IAAI,oBAAoB,OAAO;EAAE,GAAG;EAAQ;CAAO,CAAC;AAC7D"}
@@ -1,5 +1,7 @@
1
1
  export { FalImageAdapter, falImage } from './adapters/image.js';
2
2
  export { FalVideoAdapter, falVideo } from './adapters/video.js';
3
+ export { FalLiveVideoAdapter, falLiveVideo, createFalLiveVideo, FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, isFalLiveVideoModel, allowedFalLiveVideoProxyTarget, } from './adapters/live.js';
4
+ export type { FalLiveVideoModel, FalLiveVideoProviderOptions, FalLiveVideoModelProviderOptionsByName, } from './adapters/live.js';
3
5
  export { FalSpeechAdapter, falSpeech } from './adapters/speech.js';
4
6
  export { FalTranscriptionAdapter, falTranscription, } from './adapters/transcription.js';
5
7
  export { FalAudioAdapter, falAudio } from './adapters/audio.js';
package/dist/esm/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { configureFalClient, generateId, getFalApiKeyFromEnv } from "./utils/client.js";
2
2
  import { FalImageAdapter, falImage } from "./adapters/image.js";
3
3
  import { FalVideoAdapter, falVideo } from "./adapters/video.js";
4
+ import { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalLiveVideoAdapter, allowedFalLiveVideoProxyTarget, createFalLiveVideo, falLiveVideo, isFalLiveVideoModel } from "./adapters/live.js";
4
5
  import { FalSpeechAdapter, falSpeech } from "./adapters/speech.js";
5
6
  import { FalTranscriptionAdapter, falTranscription } from "./adapters/transcription.js";
6
7
  import { FalAudioAdapter, falAudio } from "./adapters/audio.js";
7
- export { FalAudioAdapter, FalImageAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, configureFalClient, falAudio, falImage, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv };
8
+ export { FAL_LIVE_VIDEO_APP, FAL_LIVE_VIDEO_MODELS, FalAudioAdapter, FalImageAdapter, FalLiveVideoAdapter, FalSpeechAdapter, FalTranscriptionAdapter, FalVideoAdapter, allowedFalLiveVideoProxyTarget, configureFalClient, createFalLiveVideo, falAudio, falImage, falLiveVideo, falSpeech, falTranscription, falVideo, generateId, getFalApiKeyFromEnv, isFalLiveVideoModel };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.13.4",
4
- "description": "fal.ai adapter for TanStack AI image, video, audio, speech, and transcription generation.",
3
+ "version": "0.14.1",
4
+ "description": "fal.ai adapter for TanStack AI image, video, live, audio, speech, and transcription generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
7
7
  "homepage": "https://tanstack.com/ai",
@@ -44,6 +44,7 @@
44
44
  "adapter",
45
45
  "image-generation",
46
46
  "video-generation",
47
+ "live-generation",
47
48
  "audio-generation",
48
49
  "text-to-speech",
49
50
  "speech-to-text",
@@ -58,10 +59,10 @@
58
59
  "devDependencies": {
59
60
  "@vitest/coverage-v8": "4.1.10",
60
61
  "vite": "^8.2.1",
61
- "@tanstack/ai": "0.53.0"
62
+ "@tanstack/ai": "0.55.0"
62
63
  },
63
64
  "peerDependencies": {
64
- "@tanstack/ai": "^0.53.0"
65
+ "@tanstack/ai": "^0.55.0"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "vite build",
@@ -0,0 +1,212 @@
1
+ import { BaseLiveVideoAdapter } from '@tanstack/ai/adapters'
2
+ import { generateId } from '@tanstack/ai-utils'
3
+ import { getFalApiKeyFromEnv } from '../utils/client'
4
+ import type {
5
+ LiveVideoGenerationOptions,
6
+ LiveVideoGenerationResult,
7
+ } from '@tanstack/ai'
8
+ import type { FalClientConfig } from '../utils/client'
9
+
10
+ const DEFAULT_FAL_TOKEN_URL = 'https://rest.fal.ai/tokens/'
11
+ const DEFAULT_TOKEN_DURATION_SECONDS = 300
12
+
13
+ export const FAL_LIVE_VIDEO_MODELS = ['minimax/h3-max/director'] as const
14
+
15
+ export type FalLiveVideoModel = (typeof FAL_LIVE_VIDEO_MODELS)[number]
16
+
17
+ /**
18
+ * WMA / token app id for each public live model. Director's AsyncAPI lives at
19
+ * `fal-ai/minimax-h3-max-director`. `wma()` and `/tokens/` must use this id:
20
+ * `minimax/h3-max/director` parses as alias `h3-max`.
21
+ */
22
+ export const FAL_LIVE_VIDEO_APP = {
23
+ 'minimax/h3-max/director': 'fal-ai/minimax-h3-max-director',
24
+ } as const satisfies Record<FalLiveVideoModel, string>
25
+
26
+ export function isFalLiveVideoModel(model: string): model is FalLiveVideoModel {
27
+ return (FAL_LIVE_VIDEO_MODELS as ReadonlyArray<string>).includes(model)
28
+ }
29
+
30
+ const WMA_PROXY_PATHS = new Set(['/ice', '/session', '/session/heartbeat'])
31
+
32
+ /**
33
+ * URLs a Director live proxy may forward. WMA talks to `wma.fal.run`
34
+ * (`/ice`, `/session`, `/session/heartbeat`). ICE can fall back to
35
+ * `fal.run/<app>/ice`. Anything else, including `queue.fal.run`, is
36
+ * rejected so the proxy cannot run arbitrary fal apps.
37
+ */
38
+ export function allowedFalLiveVideoProxyTarget(raw: string): URL | null {
39
+ let url: URL
40
+ try {
41
+ url = new URL(raw)
42
+ } catch {
43
+ return null
44
+ }
45
+ if (url.protocol !== 'https:') return null
46
+ if (url.username || url.password || url.port || url.search || url.hash) {
47
+ return null
48
+ }
49
+ const path = url.pathname.replace(/\/$/, '') || '/'
50
+ if (url.hostname === 'wma.fal.run') {
51
+ return WMA_PROXY_PATHS.has(path) ? url : null
52
+ }
53
+ if (url.hostname === 'fal.run') {
54
+ for (const app of Object.values(FAL_LIVE_VIDEO_APP)) {
55
+ if (path === `/${app}/ice`) return url
56
+ }
57
+ }
58
+ return null
59
+ }
60
+
61
+ /**
62
+ * Provider options for fal live sessions. The browser applies resolution and
63
+ * aspect ratio on the WMA `configure` message. `tokenDuration` is the only
64
+ * server-side field: it sets how long the minted JWT lasts.
65
+ */
66
+ export interface FalLiveVideoProviderOptions {
67
+ /** JWT lifetime in seconds. Defaults to 300. */
68
+ tokenDuration?: number
69
+ }
70
+
71
+ export type FalLiveVideoModelProviderOptionsByName = {
72
+ [M in FalLiveVideoModel]: FalLiveVideoProviderOptions
73
+ }
74
+
75
+ function resolveFalLiveApiKey(config?: FalClientConfig): string {
76
+ return config?.apiKey ?? getFalApiKeyFromEnv()
77
+ }
78
+
79
+ function readFalToken(body: unknown): string {
80
+ if (typeof body === 'string' && body.length > 0) return body
81
+ if (typeof body === 'object' && body !== null && 'token' in body) {
82
+ const token = body.token
83
+ if (typeof token === 'string' && token.length > 0) return token
84
+ }
85
+ throw new Error('fal token response did not include a token.')
86
+ }
87
+
88
+ /**
89
+ * fal live-video adapter. Mints a scoped JWT and returns the WMA app id on
90
+ * `result.model`. Open with `fal.realtime.open(wma(live.model))` through a
91
+ * server proxy that attaches `FAL_KEY`. WMA rejects the JWT as Key
92
+ * credentials.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { generateLiveVideo } from '@tanstack/ai'
97
+ * import { falLiveVideo } from '@tanstack/ai-fal'
98
+ *
99
+ * const live = await generateLiveVideo({
100
+ * adapter: falLiveVideo('minimax/h3-max/director'),
101
+ * prompt: 'A chef tosses noodles in a steel wok, flames leaping',
102
+ * })
103
+ * // live.model is 'fal-ai/minimax-h3-max-director'
104
+ * ```
105
+ */
106
+ export class FalLiveVideoAdapter<
107
+ TModel extends FalLiveVideoModel,
108
+ > extends BaseLiveVideoAdapter<TModel, FalLiveVideoProviderOptions> {
109
+ readonly name = 'fal' as const
110
+ private readonly clientConfig: FalClientConfig | undefined
111
+ private readonly apiKey: string
112
+
113
+ constructor(model: TModel, config?: FalClientConfig) {
114
+ super(model, {})
115
+ this.apiKey = resolveFalLiveApiKey(config)
116
+ this.clientConfig = config
117
+ }
118
+
119
+ async createLiveVideo(
120
+ options: LiveVideoGenerationOptions<FalLiveVideoProviderOptions>,
121
+ ): Promise<LiveVideoGenerationResult> {
122
+ const { logger, prompt, abortSignal, modelOptions } = options
123
+ const tokenDuration =
124
+ modelOptions?.tokenDuration ?? DEFAULT_TOKEN_DURATION_SECONDS
125
+ const fetchImpl = this.clientConfig?.fetch ?? globalThis.fetch
126
+
127
+ logger.request(
128
+ `activity=generateLiveVideo provider=fal model=${this.model}`,
129
+ {
130
+ provider: 'fal',
131
+ model: this.model,
132
+ },
133
+ )
134
+
135
+ if (typeof fetchImpl !== 'function') {
136
+ throw new Error(
137
+ 'global fetch is not available. Pass `fetch` in the fal adapter config.',
138
+ )
139
+ }
140
+
141
+ try {
142
+ const response = await fetchImpl(DEFAULT_FAL_TOKEN_URL, {
143
+ method: 'POST',
144
+ headers: {
145
+ Authorization: `Key ${this.apiKey}`,
146
+ 'Content-Type': 'application/json',
147
+ },
148
+ body: JSON.stringify({
149
+ allowed_apps: [FAL_LIVE_VIDEO_APP[this.model]],
150
+ token_expiration: tokenDuration,
151
+ }),
152
+ ...(abortSignal ? { signal: abortSignal } : {}),
153
+ })
154
+
155
+ if (!response.ok) {
156
+ const detail = await response.text().catch(() => '')
157
+ throw new Error(
158
+ `fal token request failed (${response.status}${
159
+ response.statusText ? ` ${response.statusText}` : ''
160
+ })${detail ? `: ${detail}` : ''}`,
161
+ )
162
+ }
163
+
164
+ const token = readFalToken(await response.json())
165
+ const result: LiveVideoGenerationResult = {
166
+ id: generateId('liveVideo'),
167
+ model: FAL_LIVE_VIDEO_APP[this.model],
168
+ token,
169
+ expiresAt: Date.now() + tokenDuration * 1000,
170
+ prompt,
171
+ status: 'ready',
172
+ }
173
+
174
+ logger.output(
175
+ `activity=generateLiveVideo provider=fal model=${this.model}`,
176
+ {
177
+ model: this.model,
178
+ status: result.status,
179
+ },
180
+ )
181
+
182
+ return result
183
+ } catch (error) {
184
+ logger.errors('fal.createLiveVideo failed', {
185
+ error,
186
+ source: 'fal.createLiveVideo',
187
+ })
188
+ throw error
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Create a fal live-video adapter. Reads `FAL_KEY` when `apiKey` is omitted.
195
+ */
196
+ export function falLiveVideo<TModel extends FalLiveVideoModel>(
197
+ model: TModel,
198
+ config?: FalClientConfig,
199
+ ): FalLiveVideoAdapter<TModel> {
200
+ return new FalLiveVideoAdapter(model, config)
201
+ }
202
+
203
+ /**
204
+ * Create a fal live-video adapter with an explicit API key.
205
+ */
206
+ export function createFalLiveVideo<TModel extends FalLiveVideoModel>(
207
+ model: TModel,
208
+ apiKey: string,
209
+ config?: Omit<FalClientConfig, 'apiKey'>,
210
+ ): FalLiveVideoAdapter<TModel> {
211
+ return new FalLiveVideoAdapter(model, { ...config, apiKey })
212
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,25 @@ export { FalImageAdapter, falImage } from './adapters/image'
10
10
 
11
11
  export { FalVideoAdapter, falVideo } from './adapters/video'
12
12
 
13
+ // ============================================================================
14
+ // Live Adapter (Experimental)
15
+ // ============================================================================
16
+
17
+ export {
18
+ FalLiveVideoAdapter,
19
+ falLiveVideo,
20
+ createFalLiveVideo,
21
+ FAL_LIVE_VIDEO_APP,
22
+ FAL_LIVE_VIDEO_MODELS,
23
+ isFalLiveVideoModel,
24
+ allowedFalLiveVideoProxyTarget,
25
+ } from './adapters/live'
26
+ export type {
27
+ FalLiveVideoModel,
28
+ FalLiveVideoProviderOptions,
29
+ FalLiveVideoModelProviderOptionsByName,
30
+ } from './adapters/live'
31
+
13
32
  // ============================================================================
14
33
  // Speech Adapter (TTS)
15
34
  // ============================================================================