@howells/motif-sdk 2.0.0 → 4.0.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @howells/motif-sdk
2
2
 
3
- Public Node SDK for Motif fal.ai generation, editing, utility tools, and model metadata.
3
+ Public Node SDK for Motif: Task-first image, video and utility work on fal.ai, plus a provider-agnostic image layer.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,129 +8,69 @@ Public Node SDK for Motif fal.ai generation, editing, utility tools, and model m
8
8
  npm install @howells/motif-sdk
9
9
  ```
10
10
 
11
- ## Generate Images
11
+ ## Run a Task
12
12
 
13
- The primary image API is `createMotifImage` (`@howells/motif-sdk/image`) — provider-agnostic generate/edit across google, openai, replicate, and fal. See [Image Layer](#image-layer-howellsmotif-sdkimage) below.
14
-
15
- The examples in this section use the low-level `FalClient`, the fal-native client for fal-specific capabilities (queue, upload, upscale, background removal, video, and utility tools).
13
+ The Task client, `createMotif`, is the fal surface: name a Task and, optionally, a Tier, and Motif chooses the Model, builds its request and runs it on fal. `createMotifImage` (`@howells/motif-sdk/image`) is the provider-agnostic generate/edit layer across google, openai, replicate, and fal. See [Image Layer](#image-layer-howellsmotif-sdkimage) below.
16
14
 
17
15
  ```ts
18
- import { FalClient } from "@howells/motif-sdk";
16
+ import { createMotif } from "@howells/motif-sdk";
19
17
 
20
- const motif = new FalClient({
21
- apiKey: process.env.FAL_KEY!,
22
- retries: 3,
23
- timeout: 120_000,
24
- });
18
+ const motif = createMotif(); // reads FAL_KEY
25
19
 
26
20
  const result = await motif.generate({
27
- model: "banana2",
28
21
  prompt: "editorial product photo",
29
22
  resolution: "2K",
30
- enableGoogleSearch: true,
31
- ephemeral: true,
23
+ tier: "quality",
32
24
  });
33
25
 
34
26
  if (result.isErr()) {
35
27
  throw result.error;
36
28
  }
37
29
 
38
- console.log(result.value.images[0]?.url);
30
+ console.log(result.value.model, result.value.files[0]?.url, result.value.cost);
39
31
  ```
40
32
 
41
- Every async SDK method returns `Result<T, MotifError>` from `neverthrow`. Methods do not throw for fal request failures; check `isErr()` / `isOk()`.
33
+ Every Task function returns `Result<TaskOutput, MotifError>` from `neverthrow` and does not throw for fal request failures; check `isErr()` / `isOk()`.
42
34
 
43
- ## Dry-Run Request Bodies
35
+ ## Plan Without Calling fal
44
36
 
45
- Use `buildGenerateBody` or `motif.buildRequestBody()` when you need the exact fal endpoint and request body without making an API call.
37
+ `plan(task, input, { dryRun: true })` resolves the Model and returns the endpoint, body and projected cost with no I/O and no key.
46
38
 
47
39
  ```ts
48
- import { buildGenerateBody } from "@howells/motif-sdk";
49
-
50
- const preview = buildGenerateBody({
51
- model: "gpt2",
52
- prompt: "change the wall color",
53
- editImageUrls: ["https://example.com/interior.png"],
54
- imageSize: "1536x1024",
55
- maskImageUrl: "https://example.com/wall-mask.png",
56
- quality: "auto",
57
- syncMode: true,
58
- });
59
-
60
- console.log(preview.endpoint);
61
- console.log(preview.body);
62
- ```
63
-
64
- ## Queue, Upload, and Cleanup
65
-
66
- ```ts
67
- const job = await motif.submitGeneration({
68
- model: "gpt2",
69
- prompt: "gallery poster",
70
- });
40
+ const plan = motif.plan(
41
+ "erase",
42
+ {
43
+ image: "https://example.com/room.png",
44
+ prompt: "the chair",
45
+ },
46
+ { dryRun: true }
47
+ );
71
48
 
72
- if (job.isOk()) {
73
- const status = await motif.getJobStatus(
74
- job.value.endpoint,
75
- job.value.requestId
76
- );
77
- const completed = await motif.getJobResult(
78
- job.value.endpoint,
79
- job.value.requestId
80
- );
49
+ if (plan.isOk()) {
50
+ console.log(plan.value.model, plan.value.cost);
81
51
  }
82
-
83
- const uploaded = await motif.uploadToFalCdn(fileBytes, {
84
- contentType: "image/png",
85
- fileName: "reference.png",
86
- });
87
-
88
- const deleted = await motif.deletePayloads("fal-request-id");
89
- ```
90
-
91
- ## Utility Tools and Video
92
-
93
- ```ts
94
- const mask = await motif.runTool({
95
- tool: "sam3-image",
96
- input: "https://example.com/input.png",
97
- options: { prompt: "shoe", max_masks: 2 },
98
- });
99
-
100
- const videoJob = await motif.submitVideo({
101
- imageUrl: "https://example.com/frame.png",
102
- prompt: "slow cinematic push-in",
103
- duration: 5,
104
- generateAudio: false,
105
- });
106
52
  ```
107
53
 
108
54
  ## Main Exports
109
55
 
110
- - `createMotifImage` (`@howells/motif-sdk/image`) - the primary image API: provider-agnostic generate/edit/best-of-N across google, openai, replicate, and fal.
111
- - `FalClient` - low-level fal-native client for generation, queue jobs, upload, utility tools, and payload deletion.
112
- - `buildGenerateBody` - Pure fal request normalization for dry runs and tests.
113
- - `MODELS`, `GENERATION_MODELS`, `UTILITY_MODELS`, `VIDEO_MODELS` - Motif model aliases, fal endpoints, capabilities, pricing, and benchmarks.
114
- - `FAL_TOOLS`, `FAL_TOOL_IDS`, `buildFalToolRequest`, `isFalToolId` - Normalized fal utility endpoints such as SAM, depth, upscaling, moderation, and background removal.
115
- - `ASPECT_RATIOS`, `RESOLUTIONS`, `FORMAT_PRESETS`, `aspectToGptSize`, `aspectToFalImageSize` - Shared sizing metadata and normalization helpers.
116
- - `IMAGE_TEXT_TO_IMAGE_TOP_20`, `IMAGE_EDITING_TOP_20`, `VIDEO_TEXT_TO_VIDEO_TOP_15`, `VIDEO_IMAGE_TO_VIDEO_TOP_15` - Bundled Artificial Analysis snapshots.
117
- - `estimateCost`, `estimateVideoCost` - Local cost estimates used by CLI dry runs and SDK previews.
118
- - `getFalKeyFromEnv` - `@howells/envy` backed `FAL_KEY` parsing.
56
+ - `createMotif` - the Task client: one function per Task plus `run`, `plan`, `upload` and `deletePayloads`.
57
+ - `TASKS`, `TASK_IDS`, `TIERS`, `resolveTask`, `modelProfile`, `tierChangesChoice` - the Task registry and Model resolution.
58
+ - `createMotifImage` (`@howells/motif-sdk/image`) - provider-agnostic generate/edit/best-of-N across google, openai, replicate, and fal.
59
+ - `ASPECT_RATIOS`, `RESOLUTIONS`, `FORMAT_PRESETS` - shared sizing metadata.
60
+ - `LOOKS`, `CREATIVE_TAXONOMY`, `enrichPrompt`, `validateCreativeDirection` - house looks and moods.
61
+ - `formatCost`, `sumCosts` - cost formatting and totals.
62
+ - `getFalKeyFromEnv`, `getOpenAiKeyFromEnv` - `@howells/envy` backed key parsing.
119
63
  - Re-exported `neverthrow` helpers: `ok`, `err`, `Result`, `ResultAsync`.
120
64
 
121
65
  ## Common Types
122
66
 
123
- The package exports public types for generation, processing, queue, metadata, and utility tools:
124
-
125
- - `GenerateOptions`, `MotifResponse`, `MotifImage`
126
- - `UpscaleOptions`, `RemoveBackgroundOptions`, `VideoOptions`, `VideoResponse`
127
- - `QueuedJob`, `JobStatus`, `FalClientConfig`
128
- - `ModelConfig`, `AspectRatio`, `Resolution`, `ImageSize`, `ImageQuality`, `BackgroundMode`, `ThinkingLevel`
129
- - `FalToolConfig`, `FalToolId`, `FalToolRequest`, `FalToolRunOptions`
67
+ - `MotifClient`, `MotifClientConfig`, `TaskInput`, `TaskOutput`, `TaskPlan`, `TaskFile`
68
+ - `TaskId`, `Tier`, `TaskDefinition`, `RankedModel`, `TaskRequest`, `TaskResolution`, `ModelProfile`
69
+ - `AspectRatio`, `Resolution`, `CustomImageSize`, `ImageSizeBounds`, `MotifError`
130
70
 
131
71
  ## Image Layer (`@howells/motif-sdk/image`)
132
72
 
133
- The primary, provider-agnostic image generation + editing layer — the recommended way to generate and edit images. The fal-specific `FalClient` surface above stays for fal-native extras. It is an ESM-only subpath export, built on the Vercel AI SDK image interface (`ai`'s `generateImage`).
73
+ The provider-agnostic image generation + editing layer, for callers who choose the provider and model themselves. It is an ESM-only subpath export, built on the Vercel AI SDK image interface (`ai`'s `generateImage`).
134
74
 
135
75
  ```bash
136
76
  npm install @howells/motif-sdk
@@ -143,14 +83,14 @@ const img = createMotifImage({ defaultProvider: "google" });
143
83
 
144
84
  // text -> image
145
85
  const generated = await img.generate({
146
- tier: "fast",
86
+ model: "gemini-3.1-flash-image-preview",
147
87
  prompt: "a plain room, bare concrete wall",
148
88
  aspectRatio: "1:1",
149
89
  });
150
90
 
151
91
  // multi-image edit (images + instruction, optional mask -> image out)
152
92
  const edited = await img.edit({
153
- tier: "balanced",
93
+ model: "gemini-3.1-flash-image-preview",
154
94
  images: [roomBytes, tileBytes],
155
95
  instruction: "Apply the oak texture from image 2 onto the wall in image 1.",
156
96
  mask: surfaceMaskBytes,
@@ -171,13 +111,13 @@ Four providers are implemented, each reading its own API key from the environmen
171
111
  | Provider | Env var | Notes |
172
112
  | --- | --- | --- |
173
113
  | `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | Default provider; Gemini gen + edit |
174
- | `openai` | `OPENAI_API_KEY` | GPT Image 2.5 Flare (fast/balanced), Sunburst (quality/hero) |
114
+ | `openai` | `OPENAI_API_KEY` | GPT Image 2.5 Flare and Sunburst |
175
115
  | `replicate` | `REPLICATE_API_TOKEN` | flux-1.1-pro-ultra |
176
116
  | `fal` | `FAL_KEY` | fal-hosted adapter |
177
117
 
178
- `generate()` and `edit()` accept `tier` (`"fast" | "balanced" | "quality" | "hero"`) to resolve a model per provider, or an explicit `model` id. Every result carries a normalized per-call `cost: { usd, source }`.
118
+ `generate()` and `edit()` take a provider model id; Task resolution chooses which model to pass, not the image layer. Every result carries a normalized per-call `cost: { usd, source }`.
179
119
 
180
- For OpenAI, `fast` and `balanced` (the default tier) select `gpt-image-2.5-flare`; `quality` and `hero` select `gpt-image-2.5-sunburst`. This updates the previous OpenAI tier default of `gpt-image-1`; pass that explicit model to retain it. Both new models support generation and multi-image editing:
120
+ Both `gpt-image-2.5-flare` and `gpt-image-2.5-sunburst` support generation and multi-image editing:
181
121
 
182
122
  ```ts
183
123
  const image = createMotifImage({ defaultProvider: "openai" });
@@ -192,7 +132,7 @@ const refined = await image.edit({
192
132
  });
193
133
  ```
194
134
 
195
- These models use token-based billing. Motif has no static per-image estimate for them, so `cost` is `{ usd: 0, source: "unknown" }` unless the provider supplies a cost; this does not mean generation is free. See the official [Flare](https://developers.openai.com/api/docs/models/gpt-image-2.5-flare) and [Sunburst](https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst) model pages. The installed OpenAI adapter accepts `low`, `medium`, `high`, and `auto` quality; the new `xhigh` and `max` settings require a future adapter update. The fal-backed CLI and `FalClient` expose these models as `flare` and `sunburst`. Fal supports `xhigh` and `max`, up to 16 edit references, masks and transparent backgrounds. Fal generation estimates are `null` (metered), including `estimateCost()` and queued jobs.
135
+ These models use token-based billing. Motif has no static per-image estimate for them, so `cost` is `{ usd: 0, source: "unknown" }` unless the provider supplies a cost; this does not mean generation is free. See the official [Flare](https://developers.openai.com/api/docs/models/gpt-image-2.5-flare) and [Sunburst](https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst) model pages. The installed OpenAI adapter accepts `low`, `medium`, `high`, and `auto` quality; the new `xhigh` and `max` settings require a future adapter update. The Task client and the CLI reach these models on fal as `flare` and `sunburst`. Fal supports `xhigh` and `max`, up to 16 edit references, masks and transparent backgrounds. Fal generation estimates are `null` (metered).
196
136
 
197
137
  ### Best-of-N with an injectable judge
198
138
 
package/dist/image.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { ImageModel, generateImage } from "ai";
2
2
  import { Result } from "neverthrow";
3
+ //#region src/types.d.ts
4
+ /** ─── Configuration ──────────────────────────────────────────── */
5
+ /** The network seam: a `fetch`-shaped function. */
6
+ type FalFetch = (url: string, init: RequestInit) => Promise<Response>;
7
+ //#endregion
3
8
  //#region src/errors.d.ts
4
9
  /**
5
10
  * `MotifError` and its coercion helper.
@@ -15,15 +20,12 @@ declare class MotifError extends Error {
15
20
  /** fal's request-correlation id (from the `x-fal-request-id` header or the
16
21
  * error body). Ties a failure back to fal's dashboard/support. */
17
22
  readonly requestId?: string;
18
- constructor(message: string, status: number, code?: string, requestId?: string);
23
+ /** Structured context for the failure, e.g. the refused field. */
24
+ readonly details?: Record<string, unknown>;
25
+ constructor(message: string, status: number, code?: string, requestId?: string, details?: Record<string, unknown>);
19
26
  }
20
27
  //#endregion
21
28
  //#region src/image/types.d.ts
22
- /**
23
- * Quality/latency tier. Resolves through a provider-aware tier→model map when no
24
- * explicit `model` id is given. `balanced` is the default when a tier is omitted.
25
- */
26
- type ImageTier = "fast" | "balanced" | "quality" | "hero";
27
29
  /**
28
30
  * Image provider id. All four Phase 1b adapters are implemented
29
31
  * (`google`, `openai`, `replicate`, `fal`); the type keeps an open union tail so
@@ -63,6 +65,13 @@ interface MotifImageConfig {
63
65
  replicate?: {
64
66
  apiToken?: string;
65
67
  };
68
+ /**
69
+ * Replaces global fetch for every provider request. Remote image URLs passed
70
+ * to `edit` are still downloaded by the AI SDK with global fetch.
71
+ */
72
+ fetch?: FalFetch;
73
+ /** Retries per call for retryable provider failures. AI SDK default: 2. */
74
+ maxRetries?: number;
66
75
  /** fal provider overrides. `apiKey` falls back to `FAL_KEY`. */
67
76
  fal?: {
68
77
  apiKey?: string;
@@ -72,10 +81,8 @@ interface MotifImageConfig {
72
81
  interface GenerateImageOptions {
73
82
  /** The text prompt. */
74
83
  prompt: string;
75
- /** Quality/latency tier. Ignored when `model` is set. */
76
- tier?: ImageTier;
77
- /** Explicit provider model id. Overrides `tier`. */
78
- model?: string;
84
+ /** Provider model id. Task resolution chooses it; the image layer never does. */
85
+ model: string;
79
86
  /** Provider override for this call. */
80
87
  provider?: ImageProviderId;
81
88
  /**
@@ -132,10 +139,8 @@ interface EditImageOptions {
132
139
  * `images[0]`.
133
140
  */
134
141
  mask?: Uint8Array | string;
135
- /** Quality/latency tier. Ignored when `model` is set. */
136
- tier?: ImageTier;
137
- /** Explicit provider model id. Overrides `tier`. */
138
- model?: string;
142
+ /** Provider model id. Task resolution chooses it; the image layer never does. */
143
+ model: string;
139
144
  /** Provider override for this call. */
140
145
  provider?: ImageProviderId;
141
146
  /** Number of images to generate. */
@@ -238,7 +243,7 @@ interface MotifImageClient {
238
243
  //#endregion
239
244
  //#region src/image/deps.d.ts
240
245
  /** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
241
- type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
246
+ type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string, fetch?: FalFetch) => ImageModel;
242
247
  /** Internal dependency-injection seam (default: real `generateImage` + adapters). */
243
248
  interface MotifImageDeps {
244
249
  generateImage?: typeof generateImage;
@@ -248,14 +253,11 @@ interface MotifImageDeps {
248
253
  //#region src/image/provider.d.ts
249
254
  /**
250
255
  * A single image provider. A thin wrapper over the provider's `@ai-sdk/*` image
251
- * model, plus the metadata the layer needs to route by tier, resolve keys, and
252
- * meter spend.
256
+ * model, plus the metadata the layer needs to resolve keys and meter spend.
253
257
  */
254
258
  interface ImageProviderAdapter {
255
259
  /** Provider id, matching the key it is registered under in {@link PROVIDERS}. */
256
260
  readonly id: ImageProviderId;
257
- /** Tier → model id map, used when a call does not pass an explicit `model`. */
258
- readonly tierModels: Readonly<Record<ImageTier, string>>;
259
261
  /** Env var read for the API key when no key is supplied in config. */
260
262
  readonly apiKeyEnv: string;
261
263
  /**
@@ -264,7 +266,7 @@ interface ImageProviderAdapter {
264
266
  * (callers translate this into a `Result.err`). Building a model performs no
265
267
  * network I/O.
266
268
  */
267
- readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
269
+ readonly resolveModel: (modelId: string, apiKey?: string, fetch?: FalFetch) => ImageModel;
268
270
  /**
269
271
  * Static per-model USD/**image** table (best-effort; cited per adapter). This
270
272
  * is multiplied by the returned image count to form the call total.
@@ -292,39 +294,18 @@ export declare const PROVIDERS: Record<ImageProviderId, ImageProviderAdapter>;
292
294
  export declare function getProviderAdapter(provider: ImageProviderId): ImageProviderAdapter;
293
295
  //#endregion
294
296
  //#region src/image/google.d.ts
295
- /**
296
- * Tier → Gemini image model id.
297
- *
298
- * Seeded from Material Desk's `RENDER_IMAGE_MODEL_BY_QUALITY` (the driving
299
- * consumer, see the design doc). `gemini-2.5-flash-image` is the proven-reachable
300
- * floor; the preview ids may require allowlist/tier access.
301
- */
302
- export declare const GOOGLE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
303
297
  /** Env var read for the Google API key when `apiKey` is not supplied in config. */
304
298
  export declare const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
305
299
  //#endregion
306
300
  //#region src/image/openai.d.ts
307
- /**
308
- * Flare favors speed for everyday generation; Sunburst favors editing precision.
309
- * Explicit model ids still override tiers, including older GPT Image models.
310
- */
311
- export declare const OPENAI_TIER_MODELS: Readonly<Record<ImageTier, string>>;
312
301
  /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
313
302
  export declare const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
314
303
  //#endregion
315
304
  //#region src/image/replicate.d.ts
316
- /** Tier → Replicate model id (all tiers → FLUX 1.1 Pro Ultra for now). */
317
- export declare const REPLICATE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
318
305
  /** Env var read for the Replicate API token when `apiToken` is not in config. */
319
306
  export declare const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
320
307
  //#endregion
321
308
  //#region src/image/fal.d.ts
322
- /**
323
- * Tier → fal model id. For fal, explicit `model:` endpoint ids are the primary
324
- * path (any fal endpoint resolves via passthrough); this tier map is a
325
- * convenience covering the two most common (FLUX Pro Ultra + gpt-image).
326
- */
327
- export declare const FAL_TIER_MODELS: Readonly<Record<ImageTier, string>>;
328
309
  /** Env var read for the fal key when `apiKey` is not supplied in config. */
329
310
  export declare const FAL_API_KEY_ENV = "FAL_KEY";
330
311
  //#endregion
@@ -335,6 +316,8 @@ export declare const FAL_API_KEY_ENV = "FAL_KEY";
335
316
  * `{ [provider]: { ...; cost?: number } }`. Returns undefined otherwise.
336
317
  */
337
318
  export declare function costFromProviderMetadata(providerMetadata: unknown): number | undefined;
319
+ /** Static per-image USD for a (provider, model), or undefined if unknown. */
320
+ export declare function providerPricePerImageUsd(provider: ImageProviderId, modelId: string): number | undefined;
338
321
  /**
339
322
  * Normalized per-call cost for a generation. Prefers a provider-metadata cost,
340
323
  * then the static table (× image count), then unknown.
@@ -351,4 +334,4 @@ export declare function costForImages(provider: ImageProviderId, modelId: string
351
334
  */
352
335
  export declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
353
336
  //#endregion
354
- export type { BestOfNOptions, BestOfNResult, EditImageOptions, GenerateImageOptions, ImageCost, ImageCostSource, ImageJudge, ImageProviderAdapter, ImageProviderId, ImageTier, MotifImageClient, MotifImageConfig, MotifImageFile, MotifImageResult };
337
+ export type { BestOfNOptions, BestOfNResult, EditImageOptions, GenerateImageOptions, ImageCost, ImageCostSource, ImageJudge, ImageProviderAdapter, ImageProviderId, MotifImageClient, MotifImageConfig, MotifImageFile, MotifImageResult };