@howells/motif-sdk 0.8.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 +74 -14
- package/dist/image.js +26 -7
- package/package.json +3 -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateImage, ImageModel } from 'ai';
|
|
2
2
|
import { Result } from 'neverthrow';
|
|
3
3
|
|
|
4
4
|
declare class MotifError extends Error {
|
|
@@ -78,25 +78,53 @@ interface GenerateImageOptions {
|
|
|
78
78
|
model?: string;
|
|
79
79
|
/** Provider override for this call. */
|
|
80
80
|
provider?: ImageProviderId;
|
|
81
|
-
/**
|
|
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
|
+
*/
|
|
82
86
|
aspectRatio?: `${number}:${number}`;
|
|
83
|
-
/**
|
|
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
|
+
*/
|
|
84
93
|
size?: `${number}x${number}`;
|
|
85
94
|
/** Number of images to generate. */
|
|
86
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;
|
|
87
100
|
/**
|
|
88
101
|
* Provider-specific options, passed straight through to the underlying model
|
|
89
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`.
|
|
90
105
|
*/
|
|
91
106
|
providerOptions?: Record<string, Record<string, unknown>>;
|
|
92
107
|
}
|
|
93
108
|
/** Options for a multi-image edit (images in → image out), with an optional mask. */
|
|
94
109
|
interface EditImageOptions {
|
|
95
|
-
/**
|
|
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
|
+
*/
|
|
96
118
|
images: (Uint8Array | string)[];
|
|
97
119
|
/** Natural-language edit instruction. */
|
|
98
120
|
instruction: string;
|
|
99
|
-
/**
|
|
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
|
+
*/
|
|
100
128
|
mask?: Uint8Array | string;
|
|
101
129
|
/** Quality/latency tier. Ignored when `model` is set. */
|
|
102
130
|
tier?: ImageTier;
|
|
@@ -106,6 +134,10 @@ interface EditImageOptions {
|
|
|
106
134
|
provider?: ImageProviderId;
|
|
107
135
|
/** Number of images to generate. */
|
|
108
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;
|
|
109
141
|
/** Provider-specific options (see {@link GenerateImageOptions.providerOptions}). */
|
|
110
142
|
providerOptions?: Record<string, Record<string, unknown>>;
|
|
111
143
|
}
|
|
@@ -127,6 +159,12 @@ interface MotifImageResult {
|
|
|
127
159
|
model: string;
|
|
128
160
|
/** Provider correlation id, where the provider surfaces one. */
|
|
129
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[];
|
|
130
168
|
}
|
|
131
169
|
/** The provider-agnostic image client. Every method returns a Result — no throws. */
|
|
132
170
|
interface MotifImageClient {
|
|
@@ -136,6 +174,24 @@ interface MotifImageClient {
|
|
|
136
174
|
edit: (opts: EditImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
|
|
137
175
|
}
|
|
138
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
|
+
|
|
139
195
|
/**
|
|
140
196
|
* Provider registry for the image layer.
|
|
141
197
|
*
|
|
@@ -164,7 +220,18 @@ interface ImageProviderAdapter {
|
|
|
164
220
|
* network I/O.
|
|
165
221
|
*/
|
|
166
222
|
readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
|
|
167
|
-
/**
|
|
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
|
+
*/
|
|
168
235
|
readonly priceUsdByModel: Readonly<Record<string, number>>;
|
|
169
236
|
}
|
|
170
237
|
/**
|
|
@@ -297,13 +364,6 @@ declare function costForImages(provider: ImageProviderId, modelId: string, provi
|
|
|
297
364
|
* ```
|
|
298
365
|
*/
|
|
299
366
|
|
|
300
|
-
/** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
|
|
301
|
-
type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
|
|
302
|
-
/** Internal dependency-injection seam (default: real `generateImage` + adapters). */
|
|
303
|
-
interface MotifImageDeps {
|
|
304
|
-
generateImage?: typeof generateImage;
|
|
305
|
-
resolveModel?: ResolveImageModel;
|
|
306
|
-
}
|
|
307
367
|
/**
|
|
308
368
|
* Create a provider-agnostic image client.
|
|
309
369
|
*
|
|
@@ -313,4 +373,4 @@ interface MotifImageDeps {
|
|
|
313
373
|
*/
|
|
314
374
|
declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
|
|
315
375
|
|
|
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
|
|
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
|
@@ -1503,6 +1503,8 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1503
1503
|
...opts.n === void 0 ? {} : { n: opts.n },
|
|
1504
1504
|
...opts.size === void 0 ? {} : { size: opts.size },
|
|
1505
1505
|
...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
|
|
1506
|
+
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
1507
|
+
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
1506
1508
|
...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
|
|
1507
1509
|
});
|
|
1508
1510
|
return ok2(toMotifImageResult(result, provider, modelId));
|
|
@@ -1523,6 +1525,8 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1523
1525
|
...opts.mask === void 0 ? {} : { mask: opts.mask }
|
|
1524
1526
|
},
|
|
1525
1527
|
...opts.n === void 0 ? {} : { n: opts.n },
|
|
1528
|
+
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
1529
|
+
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
1526
1530
|
...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
|
|
1527
1531
|
});
|
|
1528
1532
|
return ok2(toMotifImageResult(result, provider, modelId));
|
|
@@ -1555,14 +1559,26 @@ function toMotifImageResult(result, provider, model) {
|
|
|
1555
1559
|
images.length
|
|
1556
1560
|
);
|
|
1557
1561
|
const requestId = extractRequestId(result);
|
|
1562
|
+
const warnings = result.warnings.map(renderWarning);
|
|
1558
1563
|
return {
|
|
1559
1564
|
images,
|
|
1560
1565
|
cost,
|
|
1561
1566
|
provider,
|
|
1562
1567
|
model,
|
|
1563
|
-
...requestId === void 0 ? {} : { requestId }
|
|
1568
|
+
...requestId === void 0 ? {} : { requestId },
|
|
1569
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
1564
1570
|
};
|
|
1565
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
|
+
}
|
|
1566
1582
|
function isRecord3(value) {
|
|
1567
1583
|
return typeof value === "object" && value !== null;
|
|
1568
1584
|
}
|
|
@@ -1601,13 +1617,13 @@ function toProviderOptions(input) {
|
|
|
1601
1617
|
for (const [namespace, options] of Object.entries(input)) {
|
|
1602
1618
|
const inner = {};
|
|
1603
1619
|
for (const [key, value] of Object.entries(options)) {
|
|
1604
|
-
inner[key] = toJsonValue(value);
|
|
1620
|
+
inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
|
|
1605
1621
|
}
|
|
1606
1622
|
out[namespace] = inner;
|
|
1607
1623
|
}
|
|
1608
1624
|
return out;
|
|
1609
1625
|
}
|
|
1610
|
-
function toJsonValue(value) {
|
|
1626
|
+
function toJsonValue(value, path) {
|
|
1611
1627
|
if (value === null) {
|
|
1612
1628
|
return null;
|
|
1613
1629
|
}
|
|
@@ -1617,18 +1633,21 @@ function toJsonValue(value) {
|
|
|
1617
1633
|
if (typeof value === "object") {
|
|
1618
1634
|
if (Array.isArray(value)) {
|
|
1619
1635
|
const arr = [];
|
|
1620
|
-
for (const item of value) {
|
|
1621
|
-
arr.push(toJsonValue(item));
|
|
1636
|
+
for (const [index, item] of value.entries()) {
|
|
1637
|
+
arr.push(toJsonValue(item, `${path}[${index}]`));
|
|
1622
1638
|
}
|
|
1623
1639
|
return arr;
|
|
1624
1640
|
}
|
|
1625
1641
|
const obj = {};
|
|
1626
1642
|
for (const [key, entry] of Object.entries(value)) {
|
|
1627
|
-
obj[key] = toJsonValue(entry);
|
|
1643
|
+
obj[key] = toJsonValue(entry, `${path}.${key}`);
|
|
1628
1644
|
}
|
|
1629
1645
|
return obj;
|
|
1630
1646
|
}
|
|
1631
|
-
|
|
1647
|
+
throw new MotifError(
|
|
1648
|
+
`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`,
|
|
1649
|
+
0
|
|
1650
|
+
);
|
|
1632
1651
|
}
|
|
1633
1652
|
function toMotifError(error) {
|
|
1634
1653
|
if (error instanceof MotifError) {
|
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",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"vitest": "^4.1.10"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
|
-
"build": "tsup",
|
|
65
|
-
"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",
|
|
66
66
|
"lint": "howells-check .",
|
|
67
67
|
"lint:fix": "howells-fix .",
|
|
68
68
|
"typecheck": "tsc --noEmit",
|