@tanstack/ai-fal 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,7 @@
1
1
  import { fal } from "@fal-ai/client";
2
- function getEnvironment() {
3
- if (typeof globalThis !== "undefined") {
4
- const win = globalThis.window;
5
- if (win?.env) {
6
- return win.env;
7
- }
8
- }
9
- if (typeof process !== "undefined") {
10
- return process.env;
11
- }
12
- return void 0;
13
- }
2
+ import { getApiKeyFromEnv, generateId as generateId$1 } from "@tanstack/ai-utils";
14
3
  function getFalApiKeyFromEnv() {
15
- const env = getEnvironment();
16
- const key = env?.FAL_KEY;
17
- if (!key) {
18
- throw new Error(
19
- "FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key."
20
- );
21
- }
22
- return key;
4
+ return getApiKeyFromEnv("FAL_KEY");
23
5
  }
24
6
  function configureFalClient(config) {
25
7
  const apiKey = config?.apiKey ?? getFalApiKeyFromEnv();
@@ -29,7 +11,7 @@ function configureFalClient(config) {
29
11
  });
30
12
  }
31
13
  function generateId(prefix) {
32
- return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2)}`;
14
+ return generateId$1(prefix);
33
15
  }
34
16
  function extractUrlExtension(url) {
35
17
  let pathname;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { fal } from '@fal-ai/client'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\n}\n\ninterface EnvObject {\n FAL_KEY?: string\n}\n\ninterface WindowWithEnv {\n env?: EnvObject\n}\n\nfunction getEnvironment(): EnvObject | undefined {\n if (typeof globalThis !== 'undefined') {\n const win = (globalThis as { window?: WindowWithEnv }).window\n if (win?.env) {\n return win.env\n }\n }\n if (typeof process !== 'undefined') {\n return process.env as EnvObject\n }\n return undefined\n}\n\nexport function getFalApiKeyFromEnv(): string {\n const env = getEnvironment()\n const key = env?.FAL_KEY\n\n if (!key) {\n throw new Error(\n 'FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',\n )\n }\n\n return key\n}\n\nexport function configureFalClient(config?: FalClientConfig): void {\n const apiKey = config?.apiKey ?? getFalApiKeyFromEnv()\n fal.config({\n credentials: apiKey,\n ...(config?.proxyUrl ? { proxyUrl: config.proxyUrl } : {}),\n })\n}\n\nexport function generateId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2)}`\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]!\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 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 (let i = 0; i < view.byteLength; i += 1) {\n binary += String.fromCharCode(view[i]!)\n }\n return btoa(binary)\n}\n"],"names":[],"mappings":"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"}
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'\n\nexport interface FalClientConfig {\n apiKey: string\n proxyUrl?: string\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 ...(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]!\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 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 (let i = 0; i < view.byteLength; i += 1) {\n binary += String.fromCharCode(view[i]!)\n }\n return btoa(binary)\n}\n"],"names":["_generateId"],"mappings":";;AAQO,SAAS,sBAA8B;AAC5C,SAAO,iBAAiB,SAAS;AACnC;AAEO,SAAS,mBAAmB,QAAgC;AACjE,QAAM,SAAS,QAAQ,UAAU,oBAAA;AACjC,MAAI,OAAO;AAAA,IACT,aAAa;AAAA,IACb,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,EAAG,MAAM,GAAG,EAAE,CAAC;AAAA,EAC5C;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;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,WAAS,IAAI,GAAG,IAAI,KAAK,YAAY,KAAK,GAAG;AAC3C,cAAU,OAAO,aAAa,KAAK,CAAC,CAAE;AAAA,EACxC;AACA,SAAO,KAAK,MAAM;AACpB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-fal",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "fal.ai adapter for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -36,7 +36,8 @@
36
36
  "transcription"
37
37
  ],
38
38
  "dependencies": {
39
- "@fal-ai/client": "^1.9.4"
39
+ "@fal-ai/client": "^1.9.4",
40
+ "@tanstack/ai-utils": "0.2.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@vitest/coverage-v8": "4.0.14",
@@ -1,42 +1,13 @@
1
1
  import { fal } from '@fal-ai/client'
2
+ import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'
2
3
 
3
4
  export interface FalClientConfig {
4
5
  apiKey: string
5
6
  proxyUrl?: string
6
7
  }
7
8
 
8
- interface EnvObject {
9
- FAL_KEY?: string
10
- }
11
-
12
- interface WindowWithEnv {
13
- env?: EnvObject
14
- }
15
-
16
- function getEnvironment(): EnvObject | undefined {
17
- if (typeof globalThis !== 'undefined') {
18
- const win = (globalThis as { window?: WindowWithEnv }).window
19
- if (win?.env) {
20
- return win.env
21
- }
22
- }
23
- if (typeof process !== 'undefined') {
24
- return process.env as EnvObject
25
- }
26
- return undefined
27
- }
28
-
29
9
  export function getFalApiKeyFromEnv(): string {
30
- const env = getEnvironment()
31
- const key = env?.FAL_KEY
32
-
33
- if (!key) {
34
- throw new Error(
35
- 'FAL_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
36
- )
37
- }
38
-
39
- return key
10
+ return getApiKeyFromEnv('FAL_KEY')
40
11
  }
41
12
 
42
13
  export function configureFalClient(config?: FalClientConfig): void {
@@ -48,7 +19,7 @@ export function configureFalClient(config?: FalClientConfig): void {
48
19
  }
49
20
 
50
21
  export function generateId(prefix: string): string {
51
- return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2)}`
22
+ return _generateId(prefix)
52
23
  }
53
24
 
54
25
  /**