@howells/motif-sdk 0.7.0 → 0.8.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/image.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { generateImage, ImageModel } from 'ai';
1
+ import { ImageModel, generateImage } from 'ai';
2
2
  import { Result } from 'neverthrow';
3
3
 
4
4
  declare class MotifError extends Error {
@@ -25,11 +25,11 @@ declare class MotifError extends Error {
25
25
  */
26
26
  type ImageTier = "fast" | "balanced" | "quality" | "hero";
27
27
  /**
28
- * Image provider id. Only `google` is implemented in Phase 1a; the type is kept
29
- * an open union so later adapters (fal, openai, replicate) can slot in without a
30
- * breaking type change.
28
+ * Image provider id. All four Phase 1b adapters are implemented
29
+ * (`google`, `openai`, `replicate`, `fal`); the type keeps an open union tail so
30
+ * further adapters can slot in without a breaking type change.
31
31
  */
32
- type ImageProviderId = "google" | (string & Record<never, never>);
32
+ type ImageProviderId = "google" | "openai" | "replicate" | "fal" | (string & Record<never, never>);
33
33
  /** Source that produced a normalized per-call cost. */
34
34
  type ImageCostSource = "provider-metadata" | "table" | "unknown";
35
35
  /** Normalized per-call spend attached to every result. */
@@ -39,7 +39,12 @@ interface ImageCost {
39
39
  /** Where the figure came from: the provider's metadata, the static table, or unknown. */
40
40
  source: ImageCostSource;
41
41
  }
42
- /** Client configuration. Provider keys fall back to environment variables. */
42
+ /**
43
+ * Client configuration. Every provider key is optional and falls back to that
44
+ * provider's environment variable (see each adapter's `apiKeyEnv`). The config
45
+ * field name mirrors each SDK's own option name — notably Replicate calls its
46
+ * credential `apiToken`, not `apiKey`.
47
+ */
43
48
  interface MotifImageConfig {
44
49
  /** Provider used when a call does not specify one. Defaults to `google`. */
45
50
  defaultProvider?: ImageProviderId;
@@ -47,6 +52,21 @@ interface MotifImageConfig {
47
52
  google?: {
48
53
  apiKey?: string;
49
54
  };
55
+ /** OpenAI provider overrides. `apiKey` falls back to `OPENAI_API_KEY`. */
56
+ openai?: {
57
+ apiKey?: string;
58
+ };
59
+ /**
60
+ * Replicate provider overrides. Replicate's SDK names the credential
61
+ * `apiToken` (not `apiKey`); it falls back to `REPLICATE_API_TOKEN`.
62
+ */
63
+ replicate?: {
64
+ apiToken?: string;
65
+ };
66
+ /** fal provider overrides. `apiKey` falls back to `FAL_KEY`. */
67
+ fal?: {
68
+ apiKey?: string;
69
+ };
50
70
  }
51
71
  /** Options for a text→image generation. */
52
72
  interface GenerateImageOptions {
@@ -116,6 +136,49 @@ interface MotifImageClient {
116
136
  edit: (opts: EditImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
117
137
  }
118
138
 
139
+ /**
140
+ * Provider registry for the image layer.
141
+ *
142
+ * Each provider (google, openai, replicate, fal) contributes exactly one
143
+ * {@link ImageProviderAdapter}. The dispatch functions in `index.ts` and the
144
+ * cost lookup in `cost.ts` read the registry by id, so adding a provider is a
145
+ * single registry entry — not new branches spread across generate/edit/cost.
146
+ */
147
+
148
+ /**
149
+ * A single image provider. A thin wrapper over the provider's `@ai-sdk/*` image
150
+ * model, plus the metadata the layer needs to route by tier, resolve keys, and
151
+ * meter spend.
152
+ */
153
+ interface ImageProviderAdapter {
154
+ /** Provider id, matching the key it is registered under in {@link PROVIDERS}. */
155
+ readonly id: ImageProviderId;
156
+ /** Tier → model id map, used when a call does not pass an explicit `model`. */
157
+ readonly tierModels: Readonly<Record<ImageTier, string>>;
158
+ /** Env var read for the API key when no key is supplied in config. */
159
+ readonly apiKeyEnv: string;
160
+ /**
161
+ * Build the AI SDK `ImageModel` for a model id. Prefers the passed `apiKey`,
162
+ * else the adapter's `apiKeyEnv`. Throws `MotifError` when neither is present
163
+ * (callers translate this into a `Result.err`). Building a model performs no
164
+ * network I/O.
165
+ */
166
+ readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
167
+ /** Static per-model USD/image table (best-effort; cited per adapter). */
168
+ readonly priceUsdByModel: Readonly<Record<string, number>>;
169
+ }
170
+ /**
171
+ * The provider registry. Every id in {@link ImageProviderId}'s closed part maps
172
+ * to its adapter; the open union tail means a lookup can still miss, so access
173
+ * goes through {@link getProviderAdapter}.
174
+ */
175
+ declare const PROVIDERS: Record<ImageProviderId, ImageProviderAdapter>;
176
+ /**
177
+ * Look up an adapter by provider id, throwing a `MotifError` for an unknown
178
+ * provider (callers catch this into a `Result.err`).
179
+ */
180
+ declare function getProviderAdapter(provider: ImageProviderId): ImageProviderAdapter;
181
+
119
182
  /**
120
183
  * Google (Gemini) provider adapter.
121
184
  *
@@ -137,14 +200,70 @@ declare const GOOGLE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
137
200
  /** Env var read for the Google API key when `apiKey` is not supplied in config. */
138
201
  declare const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
139
202
 
203
+ /**
204
+ * OpenAI (gpt-image) provider adapter.
205
+ *
206
+ * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/openai`. Building a model
207
+ * performs no network I/O — the request only happens when `generateImage`
208
+ * invokes `model.doGenerate`.
209
+ */
210
+
211
+ /** Tier → OpenAI image model id (all tiers → the single gpt-image model). */
212
+ declare const OPENAI_TIER_MODELS: Readonly<Record<ImageTier, string>>;
213
+ /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
214
+ declare const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
215
+
216
+ /**
217
+ * Replicate provider adapter.
218
+ *
219
+ * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/replicate`. Building a model
220
+ * performs no network I/O — the request only happens when `generateImage`
221
+ * invokes `model.doGenerate`.
222
+ *
223
+ * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
224
+ * and reads `REPLICATE_API_TOKEN` from the environment.
225
+ */
226
+
227
+ /** Tier → Replicate model id (all tiers → FLUX 1.1 Pro Ultra for now). */
228
+ declare const REPLICATE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
229
+ /** Env var read for the Replicate API token when `apiToken` is not in config. */
230
+ declare const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
231
+
232
+ /**
233
+ * fal provider adapter.
234
+ *
235
+ * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/fal`. Building a model
236
+ * performs no network I/O — the request only happens when `generateImage`
237
+ * invokes `model.doGenerate`. This is the lightweight fal *image* adapter; the
238
+ * richer `MotifServer` fal client (queue, upload, upscale, rmbg, video, tools)
239
+ * is a separate, later fold (design doc §8, phase 1d).
240
+ *
241
+ * ENDPOINT QUIRK (Phase 0): fal's gpt-image endpoint wants `image_size` as a
242
+ * STRING enum (e.g. "1024x1024"), passed at generate time via
243
+ * `providerOptions.fal.image_size` — NOT the AI SDK's generic `size` object.
244
+ * This adapter does NOT auto-inject it; callers pass `providerOptions` when they
245
+ * need a specific size. Example:
246
+ * img.generate({
247
+ * provider: "fal",
248
+ * tier: "balanced",
249
+ * prompt: "...",
250
+ * providerOptions: { fal: { image_size: "1024x1024" } },
251
+ * });
252
+ */
253
+
254
+ /** Tier → fal model id. */
255
+ declare const FAL_TIER_MODELS: Readonly<Record<ImageTier, string>>;
256
+ /** Env var read for the fal key when `apiKey` is not supplied in config. */
257
+ declare const FAL_API_KEY_ENV = "FAL_KEY";
258
+
140
259
  /**
141
260
  * Per-call cost tracking for the image layer.
142
261
  *
143
262
  * Preference order:
144
263
  * 1. A cost surfaced by the provider on `result.providerMetadata` (most image
145
264
  * providers do NOT surface one today, so this is usually absent).
146
- * 2. A static per-model table seeded from Google's published Gemini image
147
- * pricing (see sources below).
265
+ * 2. A static per-model table, owned per-adapter (`priceUsdByModel`) and read
266
+ * from the provider registry (see sources in each adapter).
148
267
  * 3. Unknown → `{ usd: 0, source: "unknown" }`.
149
268
  */
150
269
 
@@ -194,4 +313,4 @@ interface MotifImageDeps {
194
313
  */
195
314
  declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
196
315
 
197
- export { type EditImageOptions, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, type GenerateImageOptions, type ImageCost, type ImageCostSource, type ImageProviderId, type ImageTier, type MotifImageClient, type MotifImageConfig, type MotifImageDeps, type MotifImageFile, type MotifImageResult, type ResolveImageModel, costForImages, costFromProviderMetadata, createMotifImage };
316
+ export { type EditImageOptions, FAL_API_KEY_ENV, FAL_TIER_MODELS, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, type GenerateImageOptions, type ImageCost, type ImageCostSource, type ImageProviderAdapter, type ImageProviderId, type ImageTier, type MotifImageClient, type MotifImageConfig, type MotifImageDeps, type MotifImageFile, type MotifImageResult, OPENAI_API_KEY_ENV, OPENAI_TIER_MODELS, PROVIDERS, REPLICATE_API_KEY_ENV, REPLICATE_TIER_MODELS, type ResolveImageModel, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter };
package/dist/image.js CHANGED
@@ -1279,12 +1279,149 @@ var MotifError = class extends Error {
1279
1279
  }
1280
1280
  };
1281
1281
 
1282
- // src/image/cost.ts
1282
+ // src/image/fal.ts
1283
+ import { createFal } from "@ai-sdk/fal";
1284
+ var FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
1285
+ var FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
1286
+ var FAL_TIER_MODELS = {
1287
+ fast: FAL_FLUX_MODEL,
1288
+ balanced: FAL_GPT_IMAGE_MODEL,
1289
+ quality: FAL_GPT_IMAGE_MODEL,
1290
+ hero: FAL_GPT_IMAGE_MODEL
1291
+ };
1292
+ var FAL_API_KEY_ENV = "FAL_KEY";
1293
+ var FAL_IMAGE_PRICE_USD = {
1294
+ [FAL_FLUX_MODEL]: MODELS.flux?.pricePerImageUsd ?? 0.06,
1295
+ [FAL_GPT_IMAGE_MODEL]: MODELS.gpt?.pricePerImageUsd ?? 0.133
1296
+ };
1297
+ function resolveModel(modelId, apiKey) {
1298
+ const key = apiKey ?? process.env[FAL_API_KEY_ENV];
1299
+ if (key === void 0 || key === "") {
1300
+ throw new MotifError(
1301
+ `fal image generation requires an API key (config.fal.apiKey or ${FAL_API_KEY_ENV})`,
1302
+ 0
1303
+ );
1304
+ }
1305
+ return createFal({ apiKey: key }).image(modelId);
1306
+ }
1307
+ var falAdapter = {
1308
+ id: "fal",
1309
+ tierModels: FAL_TIER_MODELS,
1310
+ apiKeyEnv: FAL_API_KEY_ENV,
1311
+ resolveModel,
1312
+ priceUsdByModel: FAL_IMAGE_PRICE_USD
1313
+ };
1314
+
1315
+ // src/image/google.ts
1316
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
1317
+ var GOOGLE_TIER_MODELS = {
1318
+ fast: "gemini-2.5-flash-image",
1319
+ balanced: "gemini-3.1-flash-image-preview",
1320
+ quality: "gemini-3-pro-image-preview",
1321
+ hero: "gemini-3-pro-image-preview"
1322
+ };
1323
+ var GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
1283
1324
  var GOOGLE_IMAGE_PRICE_USD = {
1284
1325
  "gemini-2.5-flash-image": 0.039,
1285
1326
  "gemini-3.1-flash-image-preview": 0.039,
1286
1327
  "gemini-3-pro-image-preview": 0.134
1287
1328
  };
1329
+ function resolveModel2(modelId, apiKey) {
1330
+ const key = apiKey ?? process.env[GOOGLE_API_KEY_ENV];
1331
+ if (key === void 0 || key === "") {
1332
+ throw new MotifError(
1333
+ `Google image generation requires an API key (config.google.apiKey or ${GOOGLE_API_KEY_ENV})`,
1334
+ 0
1335
+ );
1336
+ }
1337
+ return createGoogleGenerativeAI({ apiKey: key }).image(modelId);
1338
+ }
1339
+ var googleAdapter = {
1340
+ id: "google",
1341
+ tierModels: GOOGLE_TIER_MODELS,
1342
+ apiKeyEnv: GOOGLE_API_KEY_ENV,
1343
+ resolveModel: resolveModel2,
1344
+ priceUsdByModel: GOOGLE_IMAGE_PRICE_USD
1345
+ };
1346
+
1347
+ // src/image/openai.ts
1348
+ import { createOpenAI } from "@ai-sdk/openai";
1349
+ var OPENAI_MODEL = "gpt-image-1";
1350
+ var OPENAI_TIER_MODELS = {
1351
+ fast: OPENAI_MODEL,
1352
+ balanced: OPENAI_MODEL,
1353
+ quality: OPENAI_MODEL,
1354
+ hero: OPENAI_MODEL
1355
+ };
1356
+ var OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
1357
+ var OPENAI_IMAGE_PRICE_USD = {
1358
+ "gpt-image-1": 0.042
1359
+ };
1360
+ function resolveModel3(modelId, apiKey) {
1361
+ const key = apiKey ?? process.env[OPENAI_API_KEY_ENV];
1362
+ if (key === void 0 || key === "") {
1363
+ throw new MotifError(
1364
+ `OpenAI image generation requires an API key (config.openai.apiKey or ${OPENAI_API_KEY_ENV})`,
1365
+ 0
1366
+ );
1367
+ }
1368
+ return createOpenAI({ apiKey: key }).image(modelId);
1369
+ }
1370
+ var openaiAdapter = {
1371
+ id: "openai",
1372
+ tierModels: OPENAI_TIER_MODELS,
1373
+ apiKeyEnv: OPENAI_API_KEY_ENV,
1374
+ resolveModel: resolveModel3,
1375
+ priceUsdByModel: OPENAI_IMAGE_PRICE_USD
1376
+ };
1377
+
1378
+ // src/image/replicate.ts
1379
+ import { createReplicate } from "@ai-sdk/replicate";
1380
+ var REPLICATE_MODEL = "black-forest-labs/flux-1.1-pro-ultra";
1381
+ var REPLICATE_TIER_MODELS = {
1382
+ fast: REPLICATE_MODEL,
1383
+ balanced: REPLICATE_MODEL,
1384
+ quality: REPLICATE_MODEL,
1385
+ hero: REPLICATE_MODEL
1386
+ };
1387
+ var REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
1388
+ var REPLICATE_IMAGE_PRICE_USD = {
1389
+ "black-forest-labs/flux-1.1-pro-ultra": 0.06
1390
+ };
1391
+ function resolveModel4(modelId, apiKey) {
1392
+ const token = apiKey ?? process.env[REPLICATE_API_KEY_ENV];
1393
+ if (token === void 0 || token === "") {
1394
+ throw new MotifError(
1395
+ `Replicate image generation requires an API token (config.replicate.apiToken or ${REPLICATE_API_KEY_ENV})`,
1396
+ 0
1397
+ );
1398
+ }
1399
+ return createReplicate({ apiToken: token }).image(modelId);
1400
+ }
1401
+ var replicateAdapter = {
1402
+ id: "replicate",
1403
+ tierModels: REPLICATE_TIER_MODELS,
1404
+ apiKeyEnv: REPLICATE_API_KEY_ENV,
1405
+ resolveModel: resolveModel4,
1406
+ priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
1407
+ };
1408
+
1409
+ // src/image/provider.ts
1410
+ var PROVIDERS = {
1411
+ google: googleAdapter,
1412
+ openai: openaiAdapter,
1413
+ replicate: replicateAdapter,
1414
+ fal: falAdapter
1415
+ };
1416
+ function getProviderAdapter(provider) {
1417
+ const adapter = PROVIDERS[provider];
1418
+ if (adapter === void 0) {
1419
+ throw new MotifError(`Unsupported image provider: ${provider}`, 0);
1420
+ }
1421
+ return adapter;
1422
+ }
1423
+
1424
+ // src/image/cost.ts
1288
1425
  function isRecord2(value) {
1289
1426
  return typeof value === "object" && value !== null;
1290
1427
  }
@@ -1303,10 +1440,11 @@ function costFromProviderMetadata(providerMetadata) {
1303
1440
  return void 0;
1304
1441
  }
1305
1442
  function tablePricePerImage(provider, modelId) {
1306
- if (provider === "google") {
1307
- return GOOGLE_IMAGE_PRICE_USD[modelId];
1443
+ const adapter = PROVIDERS[provider];
1444
+ if (adapter === void 0) {
1445
+ return void 0;
1308
1446
  }
1309
- return void 0;
1447
+ return adapter.priceUsdByModel[modelId];
1310
1448
  }
1311
1449
  function roundUsd(value) {
1312
1450
  return Number(value.toFixed(6));
@@ -1326,29 +1464,6 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
1326
1464
  return { usd: 0, source: "unknown" };
1327
1465
  }
1328
1466
 
1329
- // src/image/google.ts
1330
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
1331
- var GOOGLE_TIER_MODELS = {
1332
- fast: "gemini-2.5-flash-image",
1333
- balanced: "gemini-3.1-flash-image-preview",
1334
- quality: "gemini-3-pro-image-preview",
1335
- hero: "gemini-3-pro-image-preview"
1336
- };
1337
- var GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
1338
- function googleModelForTier(tier) {
1339
- return GOOGLE_TIER_MODELS[tier];
1340
- }
1341
- function resolveModel(modelId, apiKey) {
1342
- const key = apiKey ?? process.env[GOOGLE_API_KEY_ENV];
1343
- if (key === void 0 || key === "") {
1344
- throw new MotifError(
1345
- `Google image generation requires an API key (config.google.apiKey or ${GOOGLE_API_KEY_ENV})`,
1346
- 0
1347
- );
1348
- }
1349
- return createGoogleGenerativeAI({ apiKey: key }).image(modelId);
1350
- }
1351
-
1352
1467
  // src/image/index.ts
1353
1468
  var DEFAULT_TIER = "balanced";
1354
1469
  var DEFAULT_PROVIDER = "google";
@@ -1359,10 +1474,23 @@ function createMotifImage(config = {}, deps = {}) {
1359
1474
  return provider ?? config.defaultProvider ?? DEFAULT_PROVIDER;
1360
1475
  }
1361
1476
  function apiKeyFor(provider) {
1362
- if (provider === "google") {
1363
- return config.google?.apiKey;
1477
+ switch (provider) {
1478
+ case "google": {
1479
+ return config.google?.apiKey;
1480
+ }
1481
+ case "openai": {
1482
+ return config.openai?.apiKey;
1483
+ }
1484
+ case "replicate": {
1485
+ return config.replicate?.apiToken;
1486
+ }
1487
+ case "fal": {
1488
+ return config.fal?.apiKey;
1489
+ }
1490
+ default: {
1491
+ return void 0;
1492
+ }
1364
1493
  }
1365
- return void 0;
1366
1494
  }
1367
1495
  async function generate(opts) {
1368
1496
  const provider = resolveProvider(opts.provider);
@@ -1405,20 +1533,14 @@ function createMotifImage(config = {}, deps = {}) {
1405
1533
  return { generate, edit };
1406
1534
  }
1407
1535
  function defaultResolveModel(provider, modelId, apiKey) {
1408
- if (provider === "google") {
1409
- return resolveModel(modelId, apiKey);
1410
- }
1411
- throw new MotifError(`Unsupported image provider: ${provider}`, 0);
1536
+ return getProviderAdapter(provider).resolveModel(modelId, apiKey);
1412
1537
  }
1413
1538
  function resolveModelId(provider, model, tier) {
1414
1539
  if (model !== void 0 && model !== "") {
1415
1540
  return model;
1416
1541
  }
1417
1542
  const resolvedTier = tier ?? DEFAULT_TIER;
1418
- if (provider === "google") {
1419
- return googleModelForTier(resolvedTier);
1420
- }
1421
- throw new MotifError(`Unsupported image provider: ${provider}`, 0);
1543
+ return getProviderAdapter(provider).tierModels[resolvedTier];
1422
1544
  }
1423
1545
  function toMotifImageResult(result, provider, model) {
1424
1546
  const images = result.images.map((file) => ({
@@ -1517,9 +1639,17 @@ function toMotifError(error) {
1517
1639
  return new MotifError(message, 0, code);
1518
1640
  }
1519
1641
  export {
1642
+ FAL_API_KEY_ENV,
1643
+ FAL_TIER_MODELS,
1520
1644
  GOOGLE_API_KEY_ENV,
1521
1645
  GOOGLE_TIER_MODELS,
1646
+ OPENAI_API_KEY_ENV,
1647
+ OPENAI_TIER_MODELS,
1648
+ PROVIDERS,
1649
+ REPLICATE_API_KEY_ENV,
1650
+ REPLICATE_TIER_MODELS,
1522
1651
  costForImages,
1523
1652
  costFromProviderMetadata,
1524
- createMotifImage
1653
+ createMotifImage,
1654
+ getProviderAdapter
1525
1655
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howells/motif-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Public Node SDK for Motif fal.ai generation, editing, utility tools, and model metadata.",
5
5
  "keywords": [
6
6
  "fal",
@@ -42,7 +42,10 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
+ "@ai-sdk/fal": "^3.0.8",
45
46
  "@ai-sdk/google": "^4.0.12",
47
+ "@ai-sdk/openai": "^4.0.11",
48
+ "@ai-sdk/replicate": "^3.0.8",
46
49
  "@howells/envy": "^0.3.7",
47
50
  "ai": "^7.0.22",
48
51
  "neverthrow": "^8.2.0",