@broberg/ai-sdk 0.9.0 → 0.10.0

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,26 @@ 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;
892
939
  tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
893
940
  override?: {
894
941
  provider?: string | undefined;
@@ -903,9 +950,14 @@ declare const imageInputSchema: z.ZodObject<{
903
950
  labels?: Record<string, string> | undefined;
904
951
  width?: number | undefined;
905
952
  height?: number | undefined;
953
+ lora?: string | undefined;
906
954
  }, {
907
955
  prompt: string;
908
956
  purpose?: string | undefined;
957
+ loras?: {
958
+ path: string;
959
+ scale?: number | undefined;
960
+ }[] | undefined;
909
961
  tier?: "fast" | "smart" | "powerful" | "cheap" | "vision" | "video" | "embedding" | undefined;
910
962
  override?: {
911
963
  provider?: string | undefined;
@@ -920,6 +972,83 @@ declare const imageInputSchema: z.ZodObject<{
920
972
  labels?: Record<string, string> | undefined;
921
973
  width?: number | undefined;
922
974
  height?: number | undefined;
975
+ lora?: string | undefined;
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;
923
1052
  }>;
924
1053
  declare const embeddingInputSchema: z.ZodObject<{
925
1054
  tier: z.ZodOptional<z.ZodEnum<["fast", "smart", "powerful", "cheap", "vision", "video", "embedding"]>>;
@@ -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.0";
1548
- declare const SDK_TAG: "@broberg/ai-sdk@0.9.0";
1683
+ declare const VERSION: "0.10.0";
1684
+ declare const SDK_TAG: "@broberg/ai-sdk@0.10.0";
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
@@ -253,6 +253,13 @@ var PRICING = {
253
253
  outputPer1M: 1.2,
254
254
  version: "2026-06-04"
255
255
  },
256
+ // DeepSeek V4 (CN-hosted — NOT GDPR-safe; non-personal-data workloads only).
257
+ // On 2026-05-22 DeepSeek made the "75% off" promo the permanent official price.
258
+ // V4-Pro $0.435/$0.87 is ~34x cheaper than GPT-5.5 on output; flash is cheaper
259
+ // still. Numbers match OpenRouter /api/v1/models 1:1 (no router markup). A strong
260
+ // cheap route for fleet background work once `claude -p` is API-billed (15 Jun).
261
+ "openrouter:deepseek/deepseek-v4-pro": { inputPer1M: 0.435, outputPer1M: 0.87, version: "2026-05-22-deepseek-official" },
262
+ "openrouter:deepseek/deepseek-v4-flash": { inputPer1M: 0.0983, outputPer1M: 0.1966, version: "2026-05-22-deepseek-official" },
256
263
  // Google Gemini (direct). Provider key is "gemini" — matches the adapter's
257
264
  // usage.provider + the override.provider callers pass. (Image-gen models are
258
265
  // priced per-image in the adapter, not here.)
@@ -1277,12 +1284,15 @@ function elevenlabsAdapter(config = {}) {
1277
1284
  }
1278
1285
 
1279
1286
  // src/providers/fal.ts
1287
+ import { deflateRawSync, crc32 } from "zlib";
1280
1288
  var FAL_IMAGE_PRICE_ESTIMATE = {
1281
1289
  "fal-ai/flux/schnell": 3e-3,
1282
1290
  "fal-ai/flux/dev": 0.025,
1291
+ "fal-ai/flux-lora": 0.025,
1283
1292
  "fal-ai/flux-pro": 0.05,
1284
1293
  "fal-ai/flux-pro/v1.1": 0.04
1285
1294
  };
1295
+ var FAL_TRAIN_PRICE_ESTIMATE = 2;
1286
1296
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1287
1297
  function falAdapter(config = {}) {
1288
1298
  const doFetch = config.fetch ?? fetch;
@@ -1290,14 +1300,22 @@ function falAdapter(config = {}) {
1290
1300
  const queueBase = config.queueBaseUrl ?? "https://queue.fal.run";
1291
1301
  const pollIntervalMs = config.pollIntervalMs ?? 2e3;
1292
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
+ });
1293
1308
  async function image(req) {
1294
- const apiKey = config.apiKey ?? process.env.FAL_KEY;
1309
+ const apiKey = resolveKey();
1295
1310
  if (!apiKey) throw new Error("fal adapter: FAL_KEY not set");
1296
- const headers = { "content-type": "application/json", Authorization: `Key ${apiKey}` };
1311
+ const headers = authHeaders(apiKey);
1297
1312
  const body = { prompt: req.prompt };
1298
1313
  if (req.width !== void 0 && req.height !== void 0) {
1299
1314
  body.image_size = { width: req.width, height: req.height };
1300
1315
  }
1316
+ if (req.loras && req.loras.length > 0) {
1317
+ body.loras = req.loras.map((l) => ({ path: l.path, scale: l.scale ?? 1 }));
1318
+ }
1301
1319
  const mode = config.mode ?? "sync";
1302
1320
  const url = await (mode === "sync" ? runSync(req.spec.model, headers, body) : runQueue(req.spec.model, headers, body));
1303
1321
  const usage = freshUsage({
@@ -1311,6 +1329,48 @@ function falAdapter(config = {}) {
1311
1329
  usage.costUsd = config.pricePerImage ?? FAL_IMAGE_PRICE_ESTIMATE[req.spec.model] ?? 0;
1312
1330
  return { url, usage };
1313
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 = result.diffusers_lora_file?.url;
1350
+ if (!loraUrl) throw new Error("fal trainStyle: no diffusers_lora_file.url in result");
1351
+ const usage = freshUsage({
1352
+ provider: "fal",
1353
+ model: req.spec.model,
1354
+ transport: "http",
1355
+ capability: "trainStyle",
1356
+ inputTokens: 0,
1357
+ outputTokens: 0
1358
+ });
1359
+ usage.costUsd = config.pricePerTraining ?? FAL_TRAIN_PRICE_ESTIMATE;
1360
+ return { loraUrl, configUrl: result.config_file?.url ?? "", usage };
1361
+ }
1362
+ async function resolveImagesDataUrl(images) {
1363
+ if (typeof images === "string") return images;
1364
+ const files = await Promise.all(
1365
+ images.map(async (url, i) => {
1366
+ const res = await doFetch(url);
1367
+ if (!res.ok) throw new Error(`fal trainStyle: failed to fetch image ${url} (${res.status})`);
1368
+ return { name: fileNameFromUrl(url, i), data: new Uint8Array(await res.arrayBuffer()) };
1369
+ })
1370
+ );
1371
+ const zip = buildZip(files);
1372
+ return `data:application/zip;base64,${Buffer.from(zip).toString("base64")}`;
1373
+ }
1314
1374
  async function runSync(model, headers, body) {
1315
1375
  const res = await doFetch(`${syncBase}/${model}`, {
1316
1376
  method: "POST",
@@ -1326,34 +1386,98 @@ function falAdapter(config = {}) {
1326
1386
  return out;
1327
1387
  }
1328
1388
  async function runQueue(model, headers, body) {
1389
+ const result = await queueResult(model, headers, body, timeoutMs);
1390
+ const out = result.images?.[0]?.url;
1391
+ if (!out) throw new Error("fal queue: no image url in result");
1392
+ return out;
1393
+ }
1394
+ async function queueResult(model, headers, body, deadlineMs) {
1329
1395
  const submitRes = await doFetch(`${queueBase}/${model}`, {
1330
1396
  method: "POST",
1331
1397
  headers,
1332
1398
  body: JSON.stringify(body)
1333
1399
  });
1334
1400
  if (!submitRes.ok) {
1335
- throw new Error(`fal queue submit ${submitRes.status}`);
1401
+ throw new Error(
1402
+ `fal queue submit ${submitRes.status}: ${(await submitRes.text().catch(() => "")).slice(0, 200)}`
1403
+ );
1336
1404
  }
1337
1405
  const submit = await submitRes.json();
1338
1406
  const statusUrl = submit.status_url;
1339
1407
  const responseUrl = submit.response_url;
1340
1408
  if (!statusUrl || !responseUrl) throw new Error("fal queue: missing status/response url");
1341
- const deadline = Date.now() + timeoutMs;
1409
+ const deadline = Date.now() + deadlineMs;
1342
1410
  for (; ; ) {
1343
1411
  const statusRes = await doFetch(statusUrl, { headers });
1344
1412
  const status = await statusRes.json();
1345
1413
  if (status.status === "COMPLETED") break;
1346
- if (status.status === "FAILED") throw new Error("fal queue: generation FAILED");
1347
- if (Date.now() >= deadline) throw new Error(`fal queue: timed out after ${timeoutMs}ms`);
1414
+ if (status.status === "FAILED") throw new Error("fal queue: job FAILED");
1415
+ if (Date.now() >= deadline) throw new Error(`fal queue: timed out after ${deadlineMs}ms`);
1348
1416
  await sleep(pollIntervalMs);
1349
1417
  }
1350
1418
  const resultRes = await doFetch(responseUrl, { headers });
1351
- const result = await resultRes.json();
1352
- const out = result.images?.[0]?.url;
1353
- if (!out) throw new Error("fal queue: no image url in result");
1354
- return out;
1419
+ return resultRes.json();
1355
1420
  }
1356
- return { name: "fal", image };
1421
+ return { name: "fal", image, trainStyle };
1422
+ }
1423
+ function fileNameFromUrl(url, i) {
1424
+ const base = url.split("?")[0].split("/").pop() || "";
1425
+ return /\.[a-z0-9]+$/i.test(base) ? base : `image_${i}.png`;
1426
+ }
1427
+ function buildZip(files) {
1428
+ const parts = [];
1429
+ const central = [];
1430
+ let offset = 0;
1431
+ for (const f of files) {
1432
+ const nameBuf = Buffer.from(f.name, "utf8");
1433
+ const data = Buffer.from(f.data);
1434
+ const comp = deflateRawSync(data);
1435
+ const crc = crc32(data) >>> 0;
1436
+ const lfh = Buffer.alloc(30);
1437
+ lfh.writeUInt32LE(67324752, 0);
1438
+ lfh.writeUInt16LE(20, 4);
1439
+ lfh.writeUInt16LE(0, 6);
1440
+ lfh.writeUInt16LE(8, 8);
1441
+ lfh.writeUInt16LE(0, 10);
1442
+ lfh.writeUInt16LE(33, 12);
1443
+ lfh.writeUInt32LE(crc, 14);
1444
+ lfh.writeUInt32LE(comp.length, 18);
1445
+ lfh.writeUInt32LE(data.length, 22);
1446
+ lfh.writeUInt16LE(nameBuf.length, 26);
1447
+ lfh.writeUInt16LE(0, 28);
1448
+ parts.push(lfh, nameBuf, comp);
1449
+ const cdh = Buffer.alloc(46);
1450
+ cdh.writeUInt32LE(33639248, 0);
1451
+ cdh.writeUInt16LE(20, 4);
1452
+ cdh.writeUInt16LE(20, 6);
1453
+ cdh.writeUInt16LE(0, 8);
1454
+ cdh.writeUInt16LE(8, 10);
1455
+ cdh.writeUInt16LE(0, 12);
1456
+ cdh.writeUInt16LE(33, 14);
1457
+ cdh.writeUInt32LE(crc, 16);
1458
+ cdh.writeUInt32LE(comp.length, 20);
1459
+ cdh.writeUInt32LE(data.length, 24);
1460
+ cdh.writeUInt16LE(nameBuf.length, 28);
1461
+ cdh.writeUInt16LE(0, 30);
1462
+ cdh.writeUInt16LE(0, 32);
1463
+ cdh.writeUInt16LE(0, 34);
1464
+ cdh.writeUInt16LE(0, 36);
1465
+ cdh.writeUInt32LE(0, 38);
1466
+ cdh.writeUInt32LE(offset, 42);
1467
+ central.push(cdh, nameBuf);
1468
+ offset += lfh.length + nameBuf.length + comp.length;
1469
+ }
1470
+ const cd = Buffer.concat(central);
1471
+ const eocd = Buffer.alloc(22);
1472
+ eocd.writeUInt32LE(101010256, 0);
1473
+ eocd.writeUInt16LE(0, 4);
1474
+ eocd.writeUInt16LE(0, 6);
1475
+ eocd.writeUInt16LE(files.length, 8);
1476
+ eocd.writeUInt16LE(files.length, 10);
1477
+ eocd.writeUInt32LE(cd.length, 12);
1478
+ eocd.writeUInt32LE(offset, 16);
1479
+ eocd.writeUInt16LE(0, 20);
1480
+ return Buffer.concat([...parts, cd, eocd]);
1357
1481
  }
1358
1482
 
1359
1483
  // src/providers/registry.ts
@@ -1658,10 +1782,28 @@ var translateInputSchema = z.object({
1658
1782
  from: z.string().optional(),
1659
1783
  ...callOptions
1660
1784
  });
1785
+ var loraWeightSchema = z.object({
1786
+ path: z.string(),
1787
+ scale: z.number().optional()
1788
+ });
1661
1789
  var imageInputSchema = z.object({
1662
1790
  prompt: z.string(),
1663
1791
  width: z.number().int().positive().optional(),
1664
1792
  height: z.number().int().positive().optional(),
1793
+ /** LoRAs to merge at inference (F021). */
1794
+ loras: z.array(loraWeightSchema).optional(),
1795
+ /** Shorthand for a single LoRA — normalized to loras:[{path, scale:1}]. */
1796
+ lora: z.string().optional(),
1797
+ ...callOptions
1798
+ });
1799
+ var trainStyleInputSchema = z.object({
1800
+ /** A hosted archive URL, or an array of image URLs the SDK zips in-memory. */
1801
+ images: z.union([z.string(), z.array(z.string())]),
1802
+ /** Style LoRA (disables captioning/masks). Default true. */
1803
+ isStyle: z.boolean().optional(),
1804
+ triggerWord: z.string().optional(),
1805
+ steps: z.number().int().positive().optional(),
1806
+ createMasks: z.boolean().optional(),
1665
1807
  ...callOptions
1666
1808
  });
1667
1809
  var embeddingInputSchema = z.object({
@@ -1717,6 +1859,16 @@ var DEFAULT_IMAGE_SPEC = {
1717
1859
  model: "fal-ai/flux/schnell",
1718
1860
  transport: "http"
1719
1861
  };
1862
+ var DEFAULT_LORA_IMAGE_SPEC = {
1863
+ provider: "fal",
1864
+ model: "fal-ai/flux-lora",
1865
+ transport: "http"
1866
+ };
1867
+ var DEFAULT_TRAINSTYLE_SPEC = {
1868
+ provider: "fal",
1869
+ model: "fal-ai/flux-lora-fast-training",
1870
+ transport: "http"
1871
+ };
1720
1872
  var DEFAULT_OCR_SPEC = { provider: "mistral", model: "mistral-ocr-latest", transport: "http" };
1721
1873
  var DEFAULT_MODERATION_SPEC = { provider: "mistral", model: "mistral-moderation-latest", transport: "http" };
1722
1874
  var DEFAULT_PODCAST_SPEC = { provider: "elevenlabs", model: "eleven_v3", transport: "http" };
@@ -1949,8 +2101,13 @@ function createAI(config = {}) {
1949
2101
  },
1950
2102
  async image(input) {
1951
2103
  input = imageInputSchema.parse(input);
2104
+ const loras = [
2105
+ ...input.loras ?? [],
2106
+ ...input.lora ? [{ path: input.lora }] : []
2107
+ ];
2108
+ const base = loras.length > 0 ? DEFAULT_LORA_IMAGE_SPEC : DEFAULT_IMAGE_SPEC;
1952
2109
  return runCapability({
1953
- primary: { ...DEFAULT_IMAGE_SPEC, ...input.override },
2110
+ primary: { ...base, ...input.override },
1954
2111
  fallback: input.fallback,
1955
2112
  capability: "image",
1956
2113
  purpose: input.purpose,
@@ -1961,7 +2118,39 @@ function createAI(config = {}) {
1961
2118
  invoke: async (spec) => {
1962
2119
  const adapter = pickProvider(spec.provider);
1963
2120
  if (!adapter.image) throw new Error(`createAI: provider "${spec.provider}" does not support image`);
1964
- return adapter.image({ prompt: input.prompt, spec, width: input.width, height: input.height });
2121
+ return adapter.image({
2122
+ prompt: input.prompt,
2123
+ spec,
2124
+ width: input.width,
2125
+ height: input.height,
2126
+ loras: loras.length ? loras : void 0
2127
+ });
2128
+ }
2129
+ });
2130
+ },
2131
+ async trainStyle(input) {
2132
+ input = trainStyleInputSchema.parse(input);
2133
+ return runCapability({
2134
+ primary: { ...DEFAULT_TRAINSTYLE_SPEC, ...input.override },
2135
+ fallback: input.fallback,
2136
+ capability: "trainStyle",
2137
+ purpose: input.purpose,
2138
+ labels: input.labels,
2139
+ estIn: 0,
2140
+ // training is priced flat by fal, not token-based
2141
+ estOut: 0,
2142
+ invoke: async (spec) => {
2143
+ const adapter = pickProvider(spec.provider);
2144
+ if (!adapter.trainStyle)
2145
+ throw new Error(`createAI: provider "${spec.provider}" does not support trainStyle`);
2146
+ return adapter.trainStyle({
2147
+ images: input.images,
2148
+ spec,
2149
+ isStyle: input.isStyle,
2150
+ triggerWord: input.triggerWord,
2151
+ steps: input.steps,
2152
+ createMasks: input.createMasks
2153
+ });
1965
2154
  }
1966
2155
  });
1967
2156
  },
@@ -2189,8 +2378,8 @@ var stubProviders = {
2189
2378
  };
2190
2379
 
2191
2380
  // src/version.ts
2192
- var VERSION = "0.9.0";
2193
- var SDK_TAG = "@broberg/ai-sdk@0.9.0";
2381
+ var VERSION = "0.10.0";
2382
+ var SDK_TAG = "@broberg/ai-sdk@0.10.0";
2194
2383
 
2195
2384
  // src/cost/budget-store.ts
2196
2385
  function sqliteBudgetStore(config) {