@howells/motif-sdk 0.7.0 → 0.9.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 +49 -0
- package/dist/image.d.ts +198 -19
- package/dist/image.js +195 -46
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -123,6 +123,55 @@ The package exports public types for generation, processing, queue, metadata, an
|
|
|
123
123
|
- `ModelConfig`, `AspectRatio`, `Resolution`, `ImageSize`, `ImageQuality`, `BackgroundMode`, `ThinkingLevel`
|
|
124
124
|
- `FalToolConfig`, `FalToolId`, `FalToolRequest`, `FalToolRunOptions`
|
|
125
125
|
|
|
126
|
+
## Image Layer (`@howells/motif-sdk/image`)
|
|
127
|
+
|
|
128
|
+
A provider-agnostic image generation + editing layer, additive to the fal-specific `MotifServer` surface above. It is an ESM-only subpath export, built on the Vercel AI SDK image interface (`ai`'s `generateImage`).
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm install @howells/motif-sdk
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { createMotifImage } from "@howells/motif-sdk/image";
|
|
136
|
+
|
|
137
|
+
const img = createMotifImage({ defaultProvider: "google" });
|
|
138
|
+
|
|
139
|
+
// text -> image
|
|
140
|
+
const generated = await img.generate({
|
|
141
|
+
tier: "fast",
|
|
142
|
+
prompt: "a plain room, bare concrete wall",
|
|
143
|
+
aspectRatio: "1:1",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// multi-image edit (images + instruction, optional mask -> image out)
|
|
147
|
+
const edited = await img.edit({
|
|
148
|
+
tier: "balanced",
|
|
149
|
+
images: [roomBytes, tileBytes],
|
|
150
|
+
instruction: "Apply the oak texture from image 2 onto the wall in image 1.",
|
|
151
|
+
mask: surfaceMaskBytes,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (edited.isOk()) {
|
|
155
|
+
edited.value.images; // MotifImageFile[]
|
|
156
|
+
edited.value.cost; // { usd, source: "provider-metadata" | "table" | "unknown" }
|
|
157
|
+
edited.value.provider; // resolved ImageProviderId
|
|
158
|
+
edited.value.model; // resolved model id
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Both `generate()` and `edit()` return `Result<MotifImageResult, MotifError>`, matching the rest of the SDK — no throws, check `isOk()` / `isErr()`.
|
|
163
|
+
|
|
164
|
+
Four providers are implemented, each reading its own API key from the environment (or a `MotifImageConfig` override):
|
|
165
|
+
|
|
166
|
+
| Provider | Env var | Notes |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| `google` | `GOOGLE_GENERATIVE_AI_API_KEY` | Default provider; Gemini gen + edit |
|
|
169
|
+
| `openai` | `OPENAI_API_KEY` | gpt-image-1 |
|
|
170
|
+
| `replicate` | `REPLICATE_API_TOKEN` | flux-1.1-pro-ultra |
|
|
171
|
+
| `fal` | `FAL_KEY` | fal-hosted adapter |
|
|
172
|
+
|
|
173
|
+
`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 }`.
|
|
174
|
+
|
|
126
175
|
## Testing
|
|
127
176
|
|
|
128
177
|
```bash
|
package/dist/image.d.ts
CHANGED
|
@@ -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.
|
|
29
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
|
@@ -58,25 +78,53 @@ interface GenerateImageOptions {
|
|
|
58
78
|
model?: string;
|
|
59
79
|
/** Provider override for this call. */
|
|
60
80
|
provider?: ImageProviderId;
|
|
61
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Aspect ratio, e.g. `"1:1"`. An alternative to {@link size}; pass one or the
|
|
83
|
+
* other. If both are passed the provider decides which to honor and typically
|
|
84
|
+
* surfaces a warning (see {@link MotifImageResult.warnings}).
|
|
85
|
+
*/
|
|
62
86
|
aspectRatio?: `${number}:${number}`;
|
|
63
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Explicit pixel size, e.g. `"1024x1024"`. An alternative to
|
|
89
|
+
* {@link aspectRatio}; pass one or the other. If both are passed the provider
|
|
90
|
+
* decides which to honor and typically surfaces a warning (see
|
|
91
|
+
* {@link MotifImageResult.warnings}).
|
|
92
|
+
*/
|
|
64
93
|
size?: `${number}x${number}`;
|
|
65
94
|
/** Number of images to generate. */
|
|
66
95
|
n?: number;
|
|
96
|
+
/** Seed for reproducible generation, where the provider supports it. */
|
|
97
|
+
seed?: number;
|
|
98
|
+
/** Abort signal to cancel the in-flight request. */
|
|
99
|
+
signal?: AbortSignal;
|
|
67
100
|
/**
|
|
68
101
|
* Provider-specific options, passed straight through to the underlying model
|
|
69
102
|
* as body parameters. Outer key = provider name, inner key = option name.
|
|
103
|
+
* Values must be JSON-representable; a non-JSON value (undefined, function,
|
|
104
|
+
* bigint, symbol) makes the call fail with a `MotifError`.
|
|
70
105
|
*/
|
|
71
106
|
providerOptions?: Record<string, Record<string, unknown>>;
|
|
72
107
|
}
|
|
73
108
|
/** Options for a multi-image edit (images in → image out), with an optional mask. */
|
|
74
109
|
interface EditImageOptions {
|
|
75
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Input images. Each entry is raw bytes (`Uint8Array`) or a string. A string
|
|
112
|
+
* may be base64, a `data:` URL, OR a remote `http(s)://` URL. Remote URLs are
|
|
113
|
+
* FETCHED by the provider — and on some providers (e.g. OpenAI) that fetch
|
|
114
|
+
* happens from the local process running this SDK. Callers that accept
|
|
115
|
+
* untrusted URLs should fetch and validate the bytes themselves before
|
|
116
|
+
* passing them here (SSRF / local-network exposure otherwise).
|
|
117
|
+
*/
|
|
76
118
|
images: (Uint8Array | string)[];
|
|
77
119
|
/** Natural-language edit instruction. */
|
|
78
120
|
instruction: string;
|
|
79
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Optional mask constraining the edited region. Same accepted forms as
|
|
123
|
+
* {@link EditImageOptions.images} (bytes, base64, `data:` URL, or a remote
|
|
124
|
+
* `http(s)://` URL that the provider fetches — see the images note on
|
|
125
|
+
* untrusted URLs). When multiple images are passed, the mask applies to
|
|
126
|
+
* `images[0]`.
|
|
127
|
+
*/
|
|
80
128
|
mask?: Uint8Array | string;
|
|
81
129
|
/** Quality/latency tier. Ignored when `model` is set. */
|
|
82
130
|
tier?: ImageTier;
|
|
@@ -86,6 +134,10 @@ interface EditImageOptions {
|
|
|
86
134
|
provider?: ImageProviderId;
|
|
87
135
|
/** Number of images to generate. */
|
|
88
136
|
n?: number;
|
|
137
|
+
/** Seed for reproducible generation, where the provider supports it. */
|
|
138
|
+
seed?: number;
|
|
139
|
+
/** Abort signal to cancel the in-flight request. */
|
|
140
|
+
signal?: AbortSignal;
|
|
89
141
|
/** Provider-specific options (see {@link GenerateImageOptions.providerOptions}). */
|
|
90
142
|
providerOptions?: Record<string, Record<string, unknown>>;
|
|
91
143
|
}
|
|
@@ -107,6 +159,12 @@ interface MotifImageResult {
|
|
|
107
159
|
model: string;
|
|
108
160
|
/** Provider correlation id, where the provider surfaces one. */
|
|
109
161
|
requestId?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Degraded-success warnings from the provider (a requested setting was
|
|
164
|
+
* ignored or adjusted — e.g. passing both `size` and `aspectRatio`). Each is a
|
|
165
|
+
* readable string. Omitted entirely when the provider returned none.
|
|
166
|
+
*/
|
|
167
|
+
warnings?: readonly string[];
|
|
110
168
|
}
|
|
111
169
|
/** The provider-agnostic image client. Every method returns a Result — no throws. */
|
|
112
170
|
interface MotifImageClient {
|
|
@@ -116,6 +174,78 @@ interface MotifImageClient {
|
|
|
116
174
|
edit: (opts: EditImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
|
|
117
175
|
}
|
|
118
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Internal dependency-injection seam for the image layer.
|
|
179
|
+
*
|
|
180
|
+
* These types are INTERNAL: they are consumed by `createMotifImage`'s `deps`
|
|
181
|
+
* parameter and by the offline tests, but they are deliberately NOT re-exported
|
|
182
|
+
* from the public `@howells/motif-sdk/image` subpath, so they never appear in
|
|
183
|
+
* `dist/image.d.ts`. Import them from `./deps` inside the package (and from
|
|
184
|
+
* `../src/image/deps` in tests).
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
/** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
|
|
188
|
+
type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
|
|
189
|
+
/** Internal dependency-injection seam (default: real `generateImage` + adapters). */
|
|
190
|
+
interface MotifImageDeps {
|
|
191
|
+
generateImage?: typeof generateImage;
|
|
192
|
+
resolveModel?: ResolveImageModel;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Provider registry for the image layer.
|
|
197
|
+
*
|
|
198
|
+
* Each provider (google, openai, replicate, fal) contributes exactly one
|
|
199
|
+
* {@link ImageProviderAdapter}. The dispatch functions in `index.ts` and the
|
|
200
|
+
* cost lookup in `cost.ts` read the registry by id, so adding a provider is a
|
|
201
|
+
* single registry entry — not new branches spread across generate/edit/cost.
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A single image provider. A thin wrapper over the provider's `@ai-sdk/*` image
|
|
206
|
+
* model, plus the metadata the layer needs to route by tier, resolve keys, and
|
|
207
|
+
* meter spend.
|
|
208
|
+
*/
|
|
209
|
+
interface ImageProviderAdapter {
|
|
210
|
+
/** Provider id, matching the key it is registered under in {@link PROVIDERS}. */
|
|
211
|
+
readonly id: ImageProviderId;
|
|
212
|
+
/** Tier → model id map, used when a call does not pass an explicit `model`. */
|
|
213
|
+
readonly tierModels: Readonly<Record<ImageTier, string>>;
|
|
214
|
+
/** Env var read for the API key when no key is supplied in config. */
|
|
215
|
+
readonly apiKeyEnv: string;
|
|
216
|
+
/**
|
|
217
|
+
* Build the AI SDK `ImageModel` for a model id. Prefers the passed `apiKey`,
|
|
218
|
+
* else the adapter's `apiKeyEnv`. Throws `MotifError` when neither is present
|
|
219
|
+
* (callers translate this into a `Result.err`). Building a model performs no
|
|
220
|
+
* network I/O.
|
|
221
|
+
*/
|
|
222
|
+
readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
|
|
223
|
+
/**
|
|
224
|
+
* Static per-model USD/**image** table (best-effort; cited per adapter). This
|
|
225
|
+
* is multiplied by the returned image count to form the call total.
|
|
226
|
+
*
|
|
227
|
+
* Cost contract: if an adapter instead surfaces a cost on
|
|
228
|
+
* `result.providerMetadata` (the preferred source; see
|
|
229
|
+
* {@link costFromProviderMetadata}), that value MUST already be the **call
|
|
230
|
+
* total** across all `n` images — NOT a per-image figure. `priceUsdByModel`
|
|
231
|
+
* is per-image; `providerMetadata.cost` is the whole call. These two paths
|
|
232
|
+
* intentionally differ, so an adapter must not populate a per-image number
|
|
233
|
+
* into `providerMetadata.cost`.
|
|
234
|
+
*/
|
|
235
|
+
readonly priceUsdByModel: Readonly<Record<string, number>>;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The provider registry. Every id in {@link ImageProviderId}'s closed part maps
|
|
239
|
+
* to its adapter; the open union tail means a lookup can still miss, so access
|
|
240
|
+
* goes through {@link getProviderAdapter}.
|
|
241
|
+
*/
|
|
242
|
+
declare const PROVIDERS: Record<ImageProviderId, ImageProviderAdapter>;
|
|
243
|
+
/**
|
|
244
|
+
* Look up an adapter by provider id, throwing a `MotifError` for an unknown
|
|
245
|
+
* provider (callers catch this into a `Result.err`).
|
|
246
|
+
*/
|
|
247
|
+
declare function getProviderAdapter(provider: ImageProviderId): ImageProviderAdapter;
|
|
248
|
+
|
|
119
249
|
/**
|
|
120
250
|
* Google (Gemini) provider adapter.
|
|
121
251
|
*
|
|
@@ -137,14 +267,70 @@ declare const GOOGLE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
|
|
|
137
267
|
/** Env var read for the Google API key when `apiKey` is not supplied in config. */
|
|
138
268
|
declare const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
139
269
|
|
|
270
|
+
/**
|
|
271
|
+
* OpenAI (gpt-image) provider adapter.
|
|
272
|
+
*
|
|
273
|
+
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/openai`. Building a model
|
|
274
|
+
* performs no network I/O — the request only happens when `generateImage`
|
|
275
|
+
* invokes `model.doGenerate`.
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
/** Tier → OpenAI image model id (all tiers → the single gpt-image model). */
|
|
279
|
+
declare const OPENAI_TIER_MODELS: Readonly<Record<ImageTier, string>>;
|
|
280
|
+
/** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
|
|
281
|
+
declare const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Replicate provider adapter.
|
|
285
|
+
*
|
|
286
|
+
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/replicate`. Building a model
|
|
287
|
+
* performs no network I/O — the request only happens when `generateImage`
|
|
288
|
+
* invokes `model.doGenerate`.
|
|
289
|
+
*
|
|
290
|
+
* NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
|
|
291
|
+
* and reads `REPLICATE_API_TOKEN` from the environment.
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
/** Tier → Replicate model id (all tiers → FLUX 1.1 Pro Ultra for now). */
|
|
295
|
+
declare const REPLICATE_TIER_MODELS: Readonly<Record<ImageTier, string>>;
|
|
296
|
+
/** Env var read for the Replicate API token when `apiToken` is not in config. */
|
|
297
|
+
declare const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* fal provider adapter.
|
|
301
|
+
*
|
|
302
|
+
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/fal`. Building a model
|
|
303
|
+
* performs no network I/O — the request only happens when `generateImage`
|
|
304
|
+
* invokes `model.doGenerate`. This is the lightweight fal *image* adapter; the
|
|
305
|
+
* richer `MotifServer` fal client (queue, upload, upscale, rmbg, video, tools)
|
|
306
|
+
* is a separate, later fold (design doc §8, phase 1d).
|
|
307
|
+
*
|
|
308
|
+
* ENDPOINT QUIRK (Phase 0): fal's gpt-image endpoint wants `image_size` as a
|
|
309
|
+
* STRING enum (e.g. "1024x1024"), passed at generate time via
|
|
310
|
+
* `providerOptions.fal.image_size` — NOT the AI SDK's generic `size` object.
|
|
311
|
+
* This adapter does NOT auto-inject it; callers pass `providerOptions` when they
|
|
312
|
+
* need a specific size. Example:
|
|
313
|
+
* img.generate({
|
|
314
|
+
* provider: "fal",
|
|
315
|
+
* tier: "balanced",
|
|
316
|
+
* prompt: "...",
|
|
317
|
+
* providerOptions: { fal: { image_size: "1024x1024" } },
|
|
318
|
+
* });
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
/** Tier → fal model id. */
|
|
322
|
+
declare const FAL_TIER_MODELS: Readonly<Record<ImageTier, string>>;
|
|
323
|
+
/** Env var read for the fal key when `apiKey` is not supplied in config. */
|
|
324
|
+
declare const FAL_API_KEY_ENV = "FAL_KEY";
|
|
325
|
+
|
|
140
326
|
/**
|
|
141
327
|
* Per-call cost tracking for the image layer.
|
|
142
328
|
*
|
|
143
329
|
* Preference order:
|
|
144
330
|
* 1. A cost surfaced by the provider on `result.providerMetadata` (most image
|
|
145
331
|
* providers do NOT surface one today, so this is usually absent).
|
|
146
|
-
* 2. A static per-model table
|
|
147
|
-
*
|
|
332
|
+
* 2. A static per-model table, owned per-adapter (`priceUsdByModel`) and read
|
|
333
|
+
* from the provider registry (see sources in each adapter).
|
|
148
334
|
* 3. Unknown → `{ usd: 0, source: "unknown" }`.
|
|
149
335
|
*/
|
|
150
336
|
|
|
@@ -178,13 +364,6 @@ declare function costForImages(provider: ImageProviderId, modelId: string, provi
|
|
|
178
364
|
* ```
|
|
179
365
|
*/
|
|
180
366
|
|
|
181
|
-
/** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
|
|
182
|
-
type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
|
|
183
|
-
/** Internal dependency-injection seam (default: real `generateImage` + adapters). */
|
|
184
|
-
interface MotifImageDeps {
|
|
185
|
-
generateImage?: typeof generateImage;
|
|
186
|
-
resolveModel?: ResolveImageModel;
|
|
187
|
-
}
|
|
188
367
|
/**
|
|
189
368
|
* Create a provider-agnostic image client.
|
|
190
369
|
*
|
|
@@ -194,4 +373,4 @@ interface MotifImageDeps {
|
|
|
194
373
|
*/
|
|
195
374
|
declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
|
|
196
375
|
|
|
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
|
|
376
|
+
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 MotifImageFile, type MotifImageResult, OPENAI_API_KEY_ENV, OPENAI_TIER_MODELS, PROVIDERS, REPLICATE_API_KEY_ENV, REPLICATE_TIER_MODELS, 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/
|
|
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
|
-
|
|
1307
|
-
|
|
1443
|
+
const adapter = PROVIDERS[provider];
|
|
1444
|
+
if (adapter === void 0) {
|
|
1445
|
+
return void 0;
|
|
1308
1446
|
}
|
|
1309
|
-
return
|
|
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
|
-
|
|
1363
|
-
|
|
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);
|
|
@@ -1375,6 +1503,8 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1375
1503
|
...opts.n === void 0 ? {} : { n: opts.n },
|
|
1376
1504
|
...opts.size === void 0 ? {} : { size: opts.size },
|
|
1377
1505
|
...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
|
|
1506
|
+
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
1507
|
+
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
1378
1508
|
...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
|
|
1379
1509
|
});
|
|
1380
1510
|
return ok2(toMotifImageResult(result, provider, modelId));
|
|
@@ -1395,6 +1525,8 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1395
1525
|
...opts.mask === void 0 ? {} : { mask: opts.mask }
|
|
1396
1526
|
},
|
|
1397
1527
|
...opts.n === void 0 ? {} : { n: opts.n },
|
|
1528
|
+
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
1529
|
+
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
1398
1530
|
...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
|
|
1399
1531
|
});
|
|
1400
1532
|
return ok2(toMotifImageResult(result, provider, modelId));
|
|
@@ -1405,20 +1537,14 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1405
1537
|
return { generate, edit };
|
|
1406
1538
|
}
|
|
1407
1539
|
function defaultResolveModel(provider, modelId, apiKey) {
|
|
1408
|
-
|
|
1409
|
-
return resolveModel(modelId, apiKey);
|
|
1410
|
-
}
|
|
1411
|
-
throw new MotifError(`Unsupported image provider: ${provider}`, 0);
|
|
1540
|
+
return getProviderAdapter(provider).resolveModel(modelId, apiKey);
|
|
1412
1541
|
}
|
|
1413
1542
|
function resolveModelId(provider, model, tier) {
|
|
1414
1543
|
if (model !== void 0 && model !== "") {
|
|
1415
1544
|
return model;
|
|
1416
1545
|
}
|
|
1417
1546
|
const resolvedTier = tier ?? DEFAULT_TIER;
|
|
1418
|
-
|
|
1419
|
-
return googleModelForTier(resolvedTier);
|
|
1420
|
-
}
|
|
1421
|
-
throw new MotifError(`Unsupported image provider: ${provider}`, 0);
|
|
1547
|
+
return getProviderAdapter(provider).tierModels[resolvedTier];
|
|
1422
1548
|
}
|
|
1423
1549
|
function toMotifImageResult(result, provider, model) {
|
|
1424
1550
|
const images = result.images.map((file) => ({
|
|
@@ -1433,14 +1559,26 @@ function toMotifImageResult(result, provider, model) {
|
|
|
1433
1559
|
images.length
|
|
1434
1560
|
);
|
|
1435
1561
|
const requestId = extractRequestId(result);
|
|
1562
|
+
const warnings = result.warnings.map(renderWarning);
|
|
1436
1563
|
return {
|
|
1437
1564
|
images,
|
|
1438
1565
|
cost,
|
|
1439
1566
|
provider,
|
|
1440
1567
|
model,
|
|
1441
|
-
...requestId === void 0 ? {} : { requestId }
|
|
1568
|
+
...requestId === void 0 ? {} : { requestId },
|
|
1569
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
1442
1570
|
};
|
|
1443
1571
|
}
|
|
1572
|
+
function renderWarning(warning) {
|
|
1573
|
+
if (warning.type === "unsupported" || warning.type === "compatibility") {
|
|
1574
|
+
const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
|
|
1575
|
+
return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
|
|
1576
|
+
}
|
|
1577
|
+
if (warning.type === "deprecated") {
|
|
1578
|
+
return `deprecated setting "${warning.setting}": ${warning.message}`;
|
|
1579
|
+
}
|
|
1580
|
+
return warning.message;
|
|
1581
|
+
}
|
|
1444
1582
|
function isRecord3(value) {
|
|
1445
1583
|
return typeof value === "object" && value !== null;
|
|
1446
1584
|
}
|
|
@@ -1479,13 +1617,13 @@ function toProviderOptions(input) {
|
|
|
1479
1617
|
for (const [namespace, options] of Object.entries(input)) {
|
|
1480
1618
|
const inner = {};
|
|
1481
1619
|
for (const [key, value] of Object.entries(options)) {
|
|
1482
|
-
inner[key] = toJsonValue(value);
|
|
1620
|
+
inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
|
|
1483
1621
|
}
|
|
1484
1622
|
out[namespace] = inner;
|
|
1485
1623
|
}
|
|
1486
1624
|
return out;
|
|
1487
1625
|
}
|
|
1488
|
-
function toJsonValue(value) {
|
|
1626
|
+
function toJsonValue(value, path) {
|
|
1489
1627
|
if (value === null) {
|
|
1490
1628
|
return null;
|
|
1491
1629
|
}
|
|
@@ -1495,18 +1633,21 @@ function toJsonValue(value) {
|
|
|
1495
1633
|
if (typeof value === "object") {
|
|
1496
1634
|
if (Array.isArray(value)) {
|
|
1497
1635
|
const arr = [];
|
|
1498
|
-
for (const item of value) {
|
|
1499
|
-
arr.push(toJsonValue(item));
|
|
1636
|
+
for (const [index, item] of value.entries()) {
|
|
1637
|
+
arr.push(toJsonValue(item, `${path}[${index}]`));
|
|
1500
1638
|
}
|
|
1501
1639
|
return arr;
|
|
1502
1640
|
}
|
|
1503
1641
|
const obj = {};
|
|
1504
1642
|
for (const [key, entry] of Object.entries(value)) {
|
|
1505
|
-
obj[key] = toJsonValue(entry);
|
|
1643
|
+
obj[key] = toJsonValue(entry, `${path}.${key}`);
|
|
1506
1644
|
}
|
|
1507
1645
|
return obj;
|
|
1508
1646
|
}
|
|
1509
|
-
|
|
1647
|
+
throw new MotifError(
|
|
1648
|
+
`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`,
|
|
1649
|
+
0
|
|
1650
|
+
);
|
|
1510
1651
|
}
|
|
1511
1652
|
function toMotifError(error) {
|
|
1512
1653
|
if (error instanceof MotifError) {
|
|
@@ -1517,9 +1658,17 @@ function toMotifError(error) {
|
|
|
1517
1658
|
return new MotifError(message, 0, code);
|
|
1518
1659
|
}
|
|
1519
1660
|
export {
|
|
1661
|
+
FAL_API_KEY_ENV,
|
|
1662
|
+
FAL_TIER_MODELS,
|
|
1520
1663
|
GOOGLE_API_KEY_ENV,
|
|
1521
1664
|
GOOGLE_TIER_MODELS,
|
|
1665
|
+
OPENAI_API_KEY_ENV,
|
|
1666
|
+
OPENAI_TIER_MODELS,
|
|
1667
|
+
PROVIDERS,
|
|
1668
|
+
REPLICATE_API_KEY_ENV,
|
|
1669
|
+
REPLICATE_TIER_MODELS,
|
|
1522
1670
|
costForImages,
|
|
1523
1671
|
costFromProviderMetadata,
|
|
1524
|
-
createMotifImage
|
|
1672
|
+
createMotifImage,
|
|
1673
|
+
getProviderAdapter
|
|
1525
1674
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howells/motif-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -58,8 +61,8 @@
|
|
|
58
61
|
"vitest": "^4.1.10"
|
|
59
62
|
},
|
|
60
63
|
"scripts": {
|
|
61
|
-
"build": "tsup",
|
|
62
|
-
"dev": "tsup --watch",
|
|
64
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
65
|
+
"dev": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup --watch",
|
|
63
66
|
"lint": "howells-check .",
|
|
64
67
|
"lint:fix": "howells-fix .",
|
|
65
68
|
"typecheck": "tsc --noEmit",
|