@broberg/ai-sdk 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ interface TierSpec {
13
13
  transport: Transport;
14
14
  }
15
15
  /** High-level capability a call exercises. Mirrors the capability layer (F5). */
16
- type Capability = "chat" | "vision" | "video" | "translate" | "image" | "embedding" | "transcribe" | "ocr" | "moderation" | "podcast" | "tts" | "mockup" | "design" | "extract" | "classify" | "rerank";
16
+ type Capability = "chat" | "vision" | "video" | "translate" | "image" | "embedding" | "transcribe" | "ocr" | "moderation" | "podcast" | "tts" | "trainStyle" | "mockup" | "design" | "extract" | "classify" | "rerank";
17
17
  type Role = "system" | "user" | "assistant" | "tool";
18
18
  /** A piece of message content. Text everywhere; image parts feed vision. */
19
19
  type ContentPart = {
@@ -150,16 +150,44 @@ type ChatStreamEvent = {
150
150
  message: string;
151
151
  status?: number;
152
152
  };
153
+ /** A trained LoRA to merge at inference time (F021). */
154
+ interface LoraWeight {
155
+ /** URL (or fal path) to the LoRA weights. */
156
+ path: string;
157
+ /** Scales the LoRA before merging (default 1). */
158
+ scale?: number;
159
+ }
153
160
  interface ImageRequest {
154
161
  prompt: string;
155
162
  spec: TierSpec;
156
163
  width?: number;
157
164
  height?: number;
165
+ /** LoRAs to merge at inference (F021) — e.g. a trained brand/style LoRA. */
166
+ loras?: LoraWeight[];
158
167
  }
159
168
  interface ImageResult {
160
169
  url: string;
161
170
  usage: Usage;
162
171
  }
172
+ /** Style/brand LoRA training (F021) — fal fal-ai/flux-lora-fast-training. */
173
+ interface TrainStyleRequest {
174
+ /** A hosted archive URL, or an array of image URLs the SDK zips in-memory. */
175
+ images: string | string[];
176
+ spec: TierSpec;
177
+ /** Style LoRA (disables captioning/masks). Default true. */
178
+ isStyle?: boolean;
179
+ triggerWord?: string;
180
+ /** Training steps (~1000 typical). */
181
+ steps?: number;
182
+ createMasks?: boolean;
183
+ }
184
+ interface TrainStyleResult {
185
+ /** URL to the trained LoRA weights — pass to ai.image({ lora }). */
186
+ loraUrl: string;
187
+ /** URL to the training config file. */
188
+ configUrl: string;
189
+ usage: Usage;
190
+ }
163
191
  interface EmbeddingRequest {
164
192
  input: string[];
165
193
  spec: TierSpec;
@@ -257,6 +285,8 @@ interface ProviderAdapter {
257
285
  chatStream?(req: ChatRequest): AsyncIterable<ChatStreamEvent>;
258
286
  vision?(req: ChatRequest): Promise<ChatResult>;
259
287
  image?(req: ImageRequest): Promise<ImageResult>;
288
+ /** Train a style/brand LoRA from images (F021). fal. */
289
+ trainStyle?(req: TrainStyleRequest): Promise<TrainStyleResult>;
260
290
  embedding?(req: EmbeddingRequest): Promise<EmbeddingResult>;
261
291
  transcribe?(req: TranscribeRequest): Promise<TranscribeResult>;
262
292
  ocr?(req: OcrRequest): Promise<OcrResult>;
@@ -886,9 +916,27 @@ declare const imageInputSchema: z.ZodObject<{
886
916
  prompt: z.ZodString;
887
917
  width: z.ZodOptional<z.ZodNumber>;
888
918
  height: z.ZodOptional<z.ZodNumber>;
919
+ /** LoRAs to merge at inference (F021). */
920
+ loras: z.ZodOptional<z.ZodArray<z.ZodObject<{
921
+ path: z.ZodString;
922
+ scale: z.ZodOptional<z.ZodNumber>;
923
+ }, "strip", z.ZodTypeAny, {
924
+ path: string;
925
+ scale?: number | undefined;
926
+ }, {
927
+ path: string;
928
+ scale?: number | undefined;
929
+ }>, "many">>;
930
+ /** Shorthand for a single LoRA — normalized to loras:[{path, scale:1}]. */
931
+ lora: z.ZodOptional<z.ZodString>;
889
932
  }, "strip", z.ZodTypeAny, {
890
933
  prompt: string;
891
934
  purpose?: string | undefined;
935
+ loras?: {
936
+ path: string;
937
+ scale?: number | undefined;
938
+ }[] | undefined;
939
+ lora?: string | undefined;
892
940
  tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
893
941
  override?: {
894
942
  provider?: string | undefined;
@@ -906,6 +954,11 @@ declare const imageInputSchema: z.ZodObject<{
906
954
  }, {
907
955
  prompt: string;
908
956
  purpose?: string | undefined;
957
+ loras?: {
958
+ path: string;
959
+ scale?: number | undefined;
960
+ }[] | undefined;
961
+ lora?: string | undefined;
909
962
  tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
910
963
  override?: {
911
964
  provider?: string | undefined;
@@ -921,6 +974,82 @@ declare const imageInputSchema: z.ZodObject<{
921
974
  width?: number | undefined;
922
975
  height?: number | undefined;
923
976
  }>;
977
+ declare const trainStyleInputSchema: z.ZodObject<{
978
+ tier: z.ZodOptional<z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>>;
979
+ override: z.ZodOptional<z.ZodObject<{
980
+ provider: z.ZodOptional<z.ZodString>;
981
+ model: z.ZodOptional<z.ZodString>;
982
+ transport: z.ZodOptional<z.ZodEnum<["http", "subprocess"]>>;
983
+ }, "strip", z.ZodTypeAny, {
984
+ provider?: string | undefined;
985
+ model?: string | undefined;
986
+ transport?: "http" | "subprocess" | undefined;
987
+ }, {
988
+ provider?: string | undefined;
989
+ model?: string | undefined;
990
+ transport?: "http" | "subprocess" | undefined;
991
+ }>>;
992
+ fallback: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>, z.ZodObject<{
993
+ provider: z.ZodString;
994
+ model: z.ZodString;
995
+ transport: z.ZodEnum<["http", "subprocess"]>;
996
+ }, "strip", z.ZodTypeAny, {
997
+ provider: string;
998
+ model: string;
999
+ transport: "http" | "subprocess";
1000
+ }, {
1001
+ provider: string;
1002
+ model: string;
1003
+ transport: "http" | "subprocess";
1004
+ }>]>, "many">>;
1005
+ purpose: z.ZodOptional<z.ZodString>;
1006
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1007
+ /** A hosted archive URL, or an array of image URLs the SDK zips in-memory. */
1008
+ images: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
1009
+ /** Style LoRA (disables captioning/masks). Default true. */
1010
+ isStyle: z.ZodOptional<z.ZodBoolean>;
1011
+ triggerWord: z.ZodOptional<z.ZodString>;
1012
+ steps: z.ZodOptional<z.ZodNumber>;
1013
+ createMasks: z.ZodOptional<z.ZodBoolean>;
1014
+ }, "strip", z.ZodTypeAny, {
1015
+ images: string | string[];
1016
+ purpose?: string | undefined;
1017
+ steps?: number | undefined;
1018
+ tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
1019
+ override?: {
1020
+ provider?: string | undefined;
1021
+ model?: string | undefined;
1022
+ transport?: "http" | "subprocess" | undefined;
1023
+ } | undefined;
1024
+ fallback?: ("fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | {
1025
+ provider: string;
1026
+ model: string;
1027
+ transport: "http" | "subprocess";
1028
+ })[] | undefined;
1029
+ labels?: Record<string, string> | undefined;
1030
+ isStyle?: boolean | undefined;
1031
+ triggerWord?: string | undefined;
1032
+ createMasks?: boolean | undefined;
1033
+ }, {
1034
+ images: string | string[];
1035
+ purpose?: string | undefined;
1036
+ steps?: number | undefined;
1037
+ tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
1038
+ override?: {
1039
+ provider?: string | undefined;
1040
+ model?: string | undefined;
1041
+ transport?: "http" | "subprocess" | undefined;
1042
+ } | undefined;
1043
+ fallback?: ("fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | {
1044
+ provider: string;
1045
+ model: string;
1046
+ transport: "http" | "subprocess";
1047
+ })[] | undefined;
1048
+ labels?: Record<string, string> | undefined;
1049
+ isStyle?: boolean | undefined;
1050
+ triggerWord?: string | undefined;
1051
+ createMasks?: boolean | undefined;
1052
+ }>;
924
1053
  declare const embeddingInputSchema: z.ZodObject<{
925
1054
  tier: z.ZodOptional<z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>>;
926
1055
  override: z.ZodOptional<z.ZodObject<{
@@ -1386,6 +1515,7 @@ type VisionInput = z.infer<typeof visionInputSchema>;
1386
1515
  type VideoInput = z.infer<typeof videoInputSchema>;
1387
1516
  type TranslateInput = z.infer<typeof translateInputSchema>;
1388
1517
  type ImageInput = z.infer<typeof imageInputSchema>;
1518
+ type TrainStyleInput = z.infer<typeof trainStyleInputSchema>;
1389
1519
  type EmbeddingInput = z.infer<typeof embeddingInputSchema>;
1390
1520
  type TranscribeInput = z.infer<typeof transcribeInputSchema>;
1391
1521
  type OcrInput = z.infer<typeof ocrInputSchema>;
@@ -1404,6 +1534,8 @@ interface AiClient {
1404
1534
  video(input: VideoInput): Promise<ChatResult>;
1405
1535
  translate(input: TranslateInput): Promise<TranslateResult>;
1406
1536
  image(input: ImageInput): Promise<ImageResult>;
1537
+ /** Train a style/brand LoRA from images (F021) → { loraUrl, configUrl }. fal. */
1538
+ trainStyle(input: TrainStyleInput): Promise<TrainStyleResult>;
1407
1539
  embedding(input: EmbeddingInput): Promise<EmbeddingResult>;
1408
1540
  transcribe(input: TranscribeInput): Promise<TranscribeResult>;
1409
1541
  /** OCR (F016.2) — document/image → structured markdown, billed per page. Mistral. */
@@ -1505,9 +1637,13 @@ interface FalAdapterConfig {
1505
1637
  queueBaseUrl?: string;
1506
1638
  pollIntervalMs?: number;
1507
1639
  timeoutMs?: number;
1640
+ /** Deadline for training jobs — they take minutes (default 600000 = 10 min). */
1641
+ trainTimeoutMs?: number;
1508
1642
  fetch?: typeof fetch;
1509
1643
  /** Override the per-image USD price (else a built-in estimate per model, 0 if unknown). */
1510
1644
  pricePerImage?: number;
1645
+ /** Override the flat per-training USD price (else ~$2 estimate). */
1646
+ pricePerTraining?: number;
1511
1647
  }
1512
1648
  declare function falAdapter(config?: FalAdapterConfig): ProviderAdapter;
1513
1649
 
@@ -1544,8 +1680,8 @@ declare const falStubAdapter: ProviderAdapter;
1544
1680
  * wires the live adapters. */
1545
1681
  declare const stubProviders: Record<string, ProviderAdapter>;
1546
1682
 
1547
- declare const VERSION: "0.9.1";
1548
- declare const SDK_TAG: "@broberg/ai-sdk@0.9.1";
1683
+ declare const VERSION: "0.10.1";
1684
+ declare const SDK_TAG: "@broberg/ai-sdk@0.10.1";
1549
1685
 
1550
1686
  /** Built-in defaults. Every entry is overridable via AiConfig.defaults or a
1551
1687
  * per-call override. Model IDs are current at scaffold time; callers pin their
@@ -1738,4 +1874,4 @@ interface StreamTransportRequest extends TransportRequest {
1738
1874
  */
1739
1875
  declare function streamTransport(req: StreamTransportRequest): AsyncIterable<string>;
1740
1876
 
1741
- export { type AiClient, type AiConfig, type BatchJob, type BatchRequestItem, type BatchResultItem, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostSink, type CostSummary, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PricingEntry, type ProviderAdapter, type RerankInput, type RerankResult, type Role, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsSinkConfig, type Usage, VERSION, type VideoInput, type VisionInput, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, chatInputSchema, computeCost, createAI, deepinfraAdapter, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, makeContracts, makeOpenAICompatibleAdapter, messageSchema, mistralAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, upmetricsSink, visionInputSchema };
1877
+ export { type AiClient, type AiConfig, type BatchJob, type BatchRequestItem, type BatchResultItem, type BudgetConfig, BudgetExceededError, BudgetGuard, type BudgetStore, type CallOptions, type Capability, type ChatInput, type ChatRequest, type ChatResult, type ChatStreamEvent, type ClassifyInput, type ClassifyResult, type ContentPart, type Contracts, type CostSink, type CostSummary, DEFAULT_TIER_MAP, type DesignInput, type DesignResult, type DialogueRequest, type DialogueTurn, type DiscordSinkConfig, ELEVENLABS_DANISH_VOICES, type EmbeddingInput, type EmbeddingRequest, type EmbeddingResult, type ExtractInput, type ExtractResult, type FalAdapterConfig, type HttpResponse, type ImageInput, type ImageRequest, type ImageResult, type LoraWeight, type Message, type MockupInput, type MockupResult, type ModerationInput, type ModerationItem, type ModerationRequest, type ModerationResult, type OcrInput, type OcrPage, type OcrRequest, type OcrResult, type OpenAICompatibleConfig, type PodcastInput, type PodcastResult, type PricingEntry, type ProviderAdapter, type RerankInput, type RerankResult, type Role, SDK_TAG, type SqliteBudgetStoreConfig, type SqliteSinkConfig, StreamHttpError, type SubprocessResponse, type Tier, type TierSpec, type Tool, type ToolCall, type TrainStyleInput, type TrainStyleRequest, type TrainStyleResult, type TranscribeInput, type TranscribeRequest, type TranscribeResult, type TranslateInput, type TranslateResult, type Transport, type TransportRequest, type TransportResponse, type TtsInput, type TtsRequest, type UpmetricsSinkConfig, type Usage, VERSION, type VideoInput, type VisionInput, aiConfigSchema, anthropicAdapter, anthropicApiAdapter, anthropicSubprocessAdapter, chatInputSchema, computeCost, createAI, deepinfraAdapter, defaultProviders, discordSink, elevenlabsAdapter, embeddingInputSchema, falAdapter, falStubAdapter, freshUsage, fromProviderToolCall, geminiAdapter, getCostSummary, getPrice, httpTransport, imageInputSchema, makeContracts, makeOpenAICompatibleAdapter, messageSchema, mistralAdapter, multiSink, noopSink, openaiAdapter, openaiStubAdapter, openrouterAdapter, parseClaudeCliJson, parseJsonLoose, resolveTier, resolveVoice, sqliteBudgetStore, sqliteSink, streamTransport, stubProviders, subprocessTransport, tierSpecSchema, toProviderTools, toolSchema, translateInputSchema, upmetricsSink, visionInputSchema };
package/dist/index.js CHANGED
@@ -1284,12 +1284,15 @@ function elevenlabsAdapter(config = {}) {
1284
1284
  }
1285
1285
 
1286
1286
  // src/providers/fal.ts
1287
+ import { deflateRawSync, crc32 } from "zlib";
1287
1288
  var FAL_IMAGE_PRICE_ESTIMATE = {
1288
1289
  "fal-ai/flux/schnell": 3e-3,
1289
1290
  "fal-ai/flux/dev": 0.025,
1291
+ "fal-ai/flux-lora": 0.025,
1290
1292
  "fal-ai/flux-pro": 0.05,
1291
1293
  "fal-ai/flux-pro/v1.1": 0.04
1292
1294
  };
1295
+ var FAL_TRAIN_PRICE_ESTIMATE = 2;
1293
1296
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1294
1297
  function falAdapter(config = {}) {
1295
1298
  const doFetch = config.fetch ?? fetch;
@@ -1297,14 +1300,22 @@ function falAdapter(config = {}) {
1297
1300
  const queueBase = config.queueBaseUrl ?? "https://queue.fal.run";
1298
1301
  const pollIntervalMs = config.pollIntervalMs ?? 2e3;
1299
1302
  const timeoutMs = config.timeoutMs ?? 6e4;
1303
+ const resolveKey = () => config.apiKey ?? process.env.FAL_KEY ?? process.env.FAL_API_KEY;
1304
+ const authHeaders = (apiKey) => ({
1305
+ "content-type": "application/json",
1306
+ Authorization: `Key ${apiKey}`
1307
+ });
1300
1308
  async function image(req) {
1301
- const apiKey = config.apiKey ?? process.env.FAL_KEY;
1309
+ const apiKey = resolveKey();
1302
1310
  if (!apiKey) throw new Error("fal adapter: FAL_KEY not set");
1303
- const headers = { "content-type": "application/json", Authorization: `Key ${apiKey}` };
1311
+ const headers = authHeaders(apiKey);
1304
1312
  const body = { prompt: req.prompt };
1305
1313
  if (req.width !== void 0 && req.height !== void 0) {
1306
1314
  body.image_size = { width: req.width, height: req.height };
1307
1315
  }
1316
+ if (req.loras && req.loras.length > 0) {
1317
+ body.loras = req.loras.map((l) => ({ path: l.path, scale: l.scale ?? 1 }));
1318
+ }
1308
1319
  const mode = config.mode ?? "sync";
1309
1320
  const url = await (mode === "sync" ? runSync(req.spec.model, headers, body) : runQueue(req.spec.model, headers, body));
1310
1321
  const usage = freshUsage({
@@ -1318,6 +1329,54 @@ function falAdapter(config = {}) {
1318
1329
  usage.costUsd = config.pricePerImage ?? FAL_IMAGE_PRICE_ESTIMATE[req.spec.model] ?? 0;
1319
1330
  return { url, usage };
1320
1331
  }
1332
+ async function trainStyle(req) {
1333
+ const apiKey = resolveKey();
1334
+ if (!apiKey) throw new Error("fal adapter: FAL_KEY not set");
1335
+ const headers = authHeaders(apiKey);
1336
+ const body = {
1337
+ images_data_url: await resolveImagesDataUrl(req.images),
1338
+ is_style: req.isStyle ?? true
1339
+ };
1340
+ if (req.triggerWord !== void 0) body.trigger_word = req.triggerWord;
1341
+ if (req.steps !== void 0) body.steps = req.steps;
1342
+ if (req.createMasks !== void 0) body.create_masks = req.createMasks;
1343
+ const result = await queueResult(
1344
+ req.spec.model,
1345
+ headers,
1346
+ body,
1347
+ config.trainTimeoutMs ?? 6e5
1348
+ );
1349
+ const { loraUrl, configUrl } = extractTrainedFiles(result);
1350
+ if (!loraUrl) {
1351
+ throw new Error(
1352
+ `fal trainStyle: no LoRA file url in result \u2014 fal returned keys [${Object.keys(
1353
+ result ?? {}
1354
+ ).join(", ")}]: ${JSON.stringify(result).slice(0, 800)}`
1355
+ );
1356
+ }
1357
+ const usage = freshUsage({
1358
+ provider: "fal",
1359
+ model: req.spec.model,
1360
+ transport: "http",
1361
+ capability: "trainStyle",
1362
+ inputTokens: 0,
1363
+ outputTokens: 0
1364
+ });
1365
+ usage.costUsd = config.pricePerTraining ?? FAL_TRAIN_PRICE_ESTIMATE;
1366
+ return { loraUrl, configUrl: configUrl ?? "", usage };
1367
+ }
1368
+ async function resolveImagesDataUrl(images) {
1369
+ if (typeof images === "string") return images;
1370
+ const files = await Promise.all(
1371
+ images.map(async (url, i) => {
1372
+ const res = await doFetch(url);
1373
+ if (!res.ok) throw new Error(`fal trainStyle: failed to fetch image ${url} (${res.status})`);
1374
+ return { name: fileNameFromUrl(url, i), data: new Uint8Array(await res.arrayBuffer()) };
1375
+ })
1376
+ );
1377
+ const zip = buildZip(files);
1378
+ return `data:application/zip;base64,${Buffer.from(zip).toString("base64")}`;
1379
+ }
1321
1380
  async function runSync(model, headers, body) {
1322
1381
  const res = await doFetch(`${syncBase}/${model}`, {
1323
1382
  method: "POST",
@@ -1333,34 +1392,133 @@ function falAdapter(config = {}) {
1333
1392
  return out;
1334
1393
  }
1335
1394
  async function runQueue(model, headers, body) {
1395
+ const result = await queueResult(model, headers, body, timeoutMs);
1396
+ const out = result.images?.[0]?.url;
1397
+ if (!out) throw new Error("fal queue: no image url in result");
1398
+ return out;
1399
+ }
1400
+ async function queueResult(model, headers, body, deadlineMs) {
1336
1401
  const submitRes = await doFetch(`${queueBase}/${model}`, {
1337
1402
  method: "POST",
1338
1403
  headers,
1339
1404
  body: JSON.stringify(body)
1340
1405
  });
1341
1406
  if (!submitRes.ok) {
1342
- throw new Error(`fal queue submit ${submitRes.status}`);
1407
+ throw new Error(
1408
+ `fal queue submit ${submitRes.status}: ${(await submitRes.text().catch(() => "")).slice(0, 200)}`
1409
+ );
1343
1410
  }
1344
1411
  const submit = await submitRes.json();
1345
1412
  const statusUrl = submit.status_url;
1346
1413
  const responseUrl = submit.response_url;
1347
1414
  if (!statusUrl || !responseUrl) throw new Error("fal queue: missing status/response url");
1348
- const deadline = Date.now() + timeoutMs;
1415
+ const deadline = Date.now() + deadlineMs;
1349
1416
  for (; ; ) {
1350
1417
  const statusRes = await doFetch(statusUrl, { headers });
1351
1418
  const status = await statusRes.json();
1352
1419
  if (status.status === "COMPLETED") break;
1353
- if (status.status === "FAILED") throw new Error("fal queue: generation FAILED");
1354
- if (Date.now() >= deadline) throw new Error(`fal queue: timed out after ${timeoutMs}ms`);
1420
+ if (status.status === "FAILED") throw new Error("fal queue: job FAILED");
1421
+ if (Date.now() >= deadline) throw new Error(`fal queue: timed out after ${deadlineMs}ms`);
1355
1422
  await sleep(pollIntervalMs);
1356
1423
  }
1357
1424
  const resultRes = await doFetch(responseUrl, { headers });
1358
- const result = await resultRes.json();
1359
- const out = result.images?.[0]?.url;
1360
- if (!out) throw new Error("fal queue: no image url in result");
1361
- return out;
1425
+ if (!resultRes.ok) {
1426
+ throw new Error(
1427
+ `fal queue result ${resultRes.status}: ${(await resultRes.text().catch(() => "")).slice(0, 300)}`
1428
+ );
1429
+ }
1430
+ return resultRes.json();
1431
+ }
1432
+ return { name: "fal", image, trainStyle };
1433
+ }
1434
+ function urlOf(v) {
1435
+ if (typeof v === "string") return v;
1436
+ if (v && typeof v === "object" && typeof v.url === "string") {
1437
+ return v.url;
1362
1438
  }
1363
- return { name: "fal", image };
1439
+ return void 0;
1440
+ }
1441
+ function deepFindUrl(obj, match) {
1442
+ const stack = [obj];
1443
+ while (stack.length) {
1444
+ const cur = stack.pop();
1445
+ if (typeof cur === "string") {
1446
+ if (match(cur)) return cur;
1447
+ continue;
1448
+ }
1449
+ if (cur && typeof cur === "object") {
1450
+ const u = cur.url;
1451
+ if (typeof u === "string" && match(u)) return u;
1452
+ for (const v of Object.values(cur)) stack.push(v);
1453
+ }
1454
+ }
1455
+ return void 0;
1456
+ }
1457
+ function extractTrainedFiles(result) {
1458
+ const r = result;
1459
+ const root = r?.data ?? r?.response ?? r?.output ?? r;
1460
+ const loraUrl = urlOf(root?.diffusers_lora_file) ?? urlOf(root?.lora_file) ?? urlOf(root?.safetensors) ?? urlOf(root?.lora) ?? deepFindUrl(root, (u) => /\.safetensors(\?|$)/i.test(u));
1461
+ const configUrl = urlOf(root?.config_file) ?? urlOf(root?.config) ?? deepFindUrl(root, (u) => /config[^/]*\.json(\?|$)/i.test(u));
1462
+ return { loraUrl, configUrl };
1463
+ }
1464
+ function fileNameFromUrl(url, i) {
1465
+ const base = url.split("?")[0].split("/").pop() || "";
1466
+ return /\.[a-z0-9]+$/i.test(base) ? base : `image_${i}.png`;
1467
+ }
1468
+ function buildZip(files) {
1469
+ const parts = [];
1470
+ const central = [];
1471
+ let offset = 0;
1472
+ for (const f of files) {
1473
+ const nameBuf = Buffer.from(f.name, "utf8");
1474
+ const data = Buffer.from(f.data);
1475
+ const comp = deflateRawSync(data);
1476
+ const crc = crc32(data) >>> 0;
1477
+ const lfh = Buffer.alloc(30);
1478
+ lfh.writeUInt32LE(67324752, 0);
1479
+ lfh.writeUInt16LE(20, 4);
1480
+ lfh.writeUInt16LE(0, 6);
1481
+ lfh.writeUInt16LE(8, 8);
1482
+ lfh.writeUInt16LE(0, 10);
1483
+ lfh.writeUInt16LE(33, 12);
1484
+ lfh.writeUInt32LE(crc, 14);
1485
+ lfh.writeUInt32LE(comp.length, 18);
1486
+ lfh.writeUInt32LE(data.length, 22);
1487
+ lfh.writeUInt16LE(nameBuf.length, 26);
1488
+ lfh.writeUInt16LE(0, 28);
1489
+ parts.push(lfh, nameBuf, comp);
1490
+ const cdh = Buffer.alloc(46);
1491
+ cdh.writeUInt32LE(33639248, 0);
1492
+ cdh.writeUInt16LE(20, 4);
1493
+ cdh.writeUInt16LE(20, 6);
1494
+ cdh.writeUInt16LE(0, 8);
1495
+ cdh.writeUInt16LE(8, 10);
1496
+ cdh.writeUInt16LE(0, 12);
1497
+ cdh.writeUInt16LE(33, 14);
1498
+ cdh.writeUInt32LE(crc, 16);
1499
+ cdh.writeUInt32LE(comp.length, 20);
1500
+ cdh.writeUInt32LE(data.length, 24);
1501
+ cdh.writeUInt16LE(nameBuf.length, 28);
1502
+ cdh.writeUInt16LE(0, 30);
1503
+ cdh.writeUInt16LE(0, 32);
1504
+ cdh.writeUInt16LE(0, 34);
1505
+ cdh.writeUInt16LE(0, 36);
1506
+ cdh.writeUInt32LE(0, 38);
1507
+ cdh.writeUInt32LE(offset, 42);
1508
+ central.push(cdh, nameBuf);
1509
+ offset += lfh.length + nameBuf.length + comp.length;
1510
+ }
1511
+ const cd = Buffer.concat(central);
1512
+ const eocd = Buffer.alloc(22);
1513
+ eocd.writeUInt32LE(101010256, 0);
1514
+ eocd.writeUInt16LE(0, 4);
1515
+ eocd.writeUInt16LE(0, 6);
1516
+ eocd.writeUInt16LE(files.length, 8);
1517
+ eocd.writeUInt16LE(files.length, 10);
1518
+ eocd.writeUInt32LE(cd.length, 12);
1519
+ eocd.writeUInt32LE(offset, 16);
1520
+ eocd.writeUInt16LE(0, 20);
1521
+ return Buffer.concat([...parts, cd, eocd]);
1364
1522
  }
1365
1523
 
1366
1524
  // src/providers/registry.ts
@@ -1665,10 +1823,28 @@ var translateInputSchema = z.object({
1665
1823
  from: z.string().optional(),
1666
1824
  ...callOptions
1667
1825
  });
1826
+ var loraWeightSchema = z.object({
1827
+ path: z.string(),
1828
+ scale: z.number().optional()
1829
+ });
1668
1830
  var imageInputSchema = z.object({
1669
1831
  prompt: z.string(),
1670
1832
  width: z.number().int().positive().optional(),
1671
1833
  height: z.number().int().positive().optional(),
1834
+ /** LoRAs to merge at inference (F021). */
1835
+ loras: z.array(loraWeightSchema).optional(),
1836
+ /** Shorthand for a single LoRA — normalized to loras:[{path, scale:1}]. */
1837
+ lora: z.string().optional(),
1838
+ ...callOptions
1839
+ });
1840
+ var trainStyleInputSchema = z.object({
1841
+ /** A hosted archive URL, or an array of image URLs the SDK zips in-memory. */
1842
+ images: z.union([z.string(), z.array(z.string())]),
1843
+ /** Style LoRA (disables captioning/masks). Default true. */
1844
+ isStyle: z.boolean().optional(),
1845
+ triggerWord: z.string().optional(),
1846
+ steps: z.number().int().positive().optional(),
1847
+ createMasks: z.boolean().optional(),
1672
1848
  ...callOptions
1673
1849
  });
1674
1850
  var embeddingInputSchema = z.object({
@@ -1724,6 +1900,16 @@ var DEFAULT_IMAGE_SPEC = {
1724
1900
  model: "fal-ai/flux/schnell",
1725
1901
  transport: "http"
1726
1902
  };
1903
+ var DEFAULT_LORA_IMAGE_SPEC = {
1904
+ provider: "fal",
1905
+ model: "fal-ai/flux-lora",
1906
+ transport: "http"
1907
+ };
1908
+ var DEFAULT_TRAINSTYLE_SPEC = {
1909
+ provider: "fal",
1910
+ model: "fal-ai/flux-lora-fast-training",
1911
+ transport: "http"
1912
+ };
1727
1913
  var DEFAULT_OCR_SPEC = { provider: "mistral", model: "mistral-ocr-latest", transport: "http" };
1728
1914
  var DEFAULT_MODERATION_SPEC = { provider: "mistral", model: "mistral-moderation-latest", transport: "http" };
1729
1915
  var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
@@ -1956,8 +2142,13 @@ function createAI(config = {}) {
1956
2142
  },
1957
2143
  async image(input) {
1958
2144
  input = imageInputSchema.parse(input);
2145
+ const loras = [
2146
+ ...input.loras ?? [],
2147
+ ...input.lora ? [{ path: input.lora }] : []
2148
+ ];
2149
+ const base = loras.length > 0 ? DEFAULT_LORA_IMAGE_SPEC : DEFAULT_IMAGE_SPEC;
1959
2150
  return runCapability({
1960
- primary: { ...DEFAULT_IMAGE_SPEC, ...input.override },
2151
+ primary: { ...base, ...input.override },
1961
2152
  fallback: input.fallback,
1962
2153
  capability: "image",
1963
2154
  purpose: input.purpose,
@@ -1968,7 +2159,39 @@ function createAI(config = {}) {
1968
2159
  invoke: async (spec) => {
1969
2160
  const adapter = pickProvider(spec.provider);
1970
2161
  if (!adapter.image) throw new Error(`createAI: provider "${spec.provider}" does not support image`);
1971
- return adapter.image({ prompt: input.prompt, spec, width: input.width, height: input.height });
2162
+ return adapter.image({
2163
+ prompt: input.prompt,
2164
+ spec,
2165
+ width: input.width,
2166
+ height: input.height,
2167
+ loras: loras.length ? loras : void 0
2168
+ });
2169
+ }
2170
+ });
2171
+ },
2172
+ async trainStyle(input) {
2173
+ input = trainStyleInputSchema.parse(input);
2174
+ return runCapability({
2175
+ primary: { ...DEFAULT_TRAINSTYLE_SPEC, ...input.override },
2176
+ fallback: input.fallback,
2177
+ capability: "trainStyle",
2178
+ purpose: input.purpose,
2179
+ labels: input.labels,
2180
+ estIn: 0,
2181
+ // training is priced flat by fal, not token-based
2182
+ estOut: 0,
2183
+ invoke: async (spec) => {
2184
+ const adapter = pickProvider(spec.provider);
2185
+ if (!adapter.trainStyle)
2186
+ throw new Error(`createAI: provider "${spec.provider}" does not support trainStyle`);
2187
+ return adapter.trainStyle({
2188
+ images: input.images,
2189
+ spec,
2190
+ isStyle: input.isStyle,
2191
+ triggerWord: input.triggerWord,
2192
+ steps: input.steps,
2193
+ createMasks: input.createMasks
2194
+ });
1972
2195
  }
1973
2196
  });
1974
2197
  },
@@ -2196,8 +2419,8 @@ var stubProviders = {
2196
2419
  };
2197
2420
 
2198
2421
  // src/version.ts
2199
- var VERSION = "0.9.1";
2200
- var SDK_TAG = "@broberg/ai-sdk@0.9.1";
2422
+ var VERSION = "0.10.1";
2423
+ var SDK_TAG = "@broberg/ai-sdk@0.10.1";
2201
2424
 
2202
2425
  // src/cost/budget-store.ts
2203
2426
  function sqliteBudgetStore(config) {