@howells/motif-sdk 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/image.d.ts +124 -14
- package/dist/image.js +111 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -123,6 +123,83 @@ 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
|
+
|
|
175
|
+
### Best-of-N with an injectable judge
|
|
176
|
+
|
|
177
|
+
`bestOfN()` generates `n` candidates in parallel and picks a winner. It reuses the same options as `generate()` (text→image) or `edit()` (pass `images` for the edit path), plus `n` and an optional `judge`. When a `seed` is given each candidate uses `seed + index`, so the N vary. The judge is a caller-provided function — the layer takes no text-client dependency, so it pairs well with `@howells/ai`'s vision client but does not require it. Omit the judge and candidate 0 wins.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const best = await img.bestOfN({
|
|
181
|
+
prompt: "a plain room, bare concrete wall",
|
|
182
|
+
n: 4,
|
|
183
|
+
seed: 100, // candidates get seeds 100, 101, 102, 103
|
|
184
|
+
// Caller-provided judge: receives the successful candidates + context,
|
|
185
|
+
// returns the winning index. Wire in @howells/ai here if you want a vision judge.
|
|
186
|
+
judge: async (candidates, context) => {
|
|
187
|
+
// ...score candidates[i].images[0] against context.prompt...
|
|
188
|
+
return { index: 0, reason: "sharpest wall texture" };
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (best.isOk()) {
|
|
193
|
+
best.value.best; // the winning MotifImageResult
|
|
194
|
+
best.value.chosenIndex; // its index within candidates
|
|
195
|
+
best.value.reason; // the judge's rationale, if any
|
|
196
|
+
best.value.candidates; // every successful candidate (generation order)
|
|
197
|
+
best.value.totalCostUsd; // summed USD across all candidates generated
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If some candidates fail, the judge sees only the survivors; if all `n` fail, `bestOfN()` returns `Result.err`.
|
|
202
|
+
|
|
126
203
|
## Testing
|
|
127
204
|
|
|
128
205
|
```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,56 @@ 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[];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Picks the winning candidate for a {@link MotifImageClient.bestOfN} call.
|
|
171
|
+
*
|
|
172
|
+
* Receives the successful candidates (index-aligned to
|
|
173
|
+
* {@link BestOfNResult.candidates}) and the request context, and returns the
|
|
174
|
+
* chosen index (plus an optional human-readable reason). May be sync or async.
|
|
175
|
+
* The judge is caller-provided so the image layer stays decoupled from any text
|
|
176
|
+
* client — it pairs well with `@howells/ai`'s vision client, but that dependency
|
|
177
|
+
* is not required.
|
|
178
|
+
*/
|
|
179
|
+
type ImageJudge = (candidates: readonly MotifImageResult[], context: {
|
|
180
|
+
readonly prompt?: string;
|
|
181
|
+
readonly instruction?: string;
|
|
182
|
+
}) => Promise<{
|
|
183
|
+
index: number;
|
|
184
|
+
reason?: string;
|
|
185
|
+
}> | {
|
|
186
|
+
index: number;
|
|
187
|
+
reason?: string;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Options for a best-of-N generation. Extends either {@link GenerateImageOptions}
|
|
191
|
+
* (text→image) or {@link EditImageOptions} (multi-image edit) — the presence of
|
|
192
|
+
* `images` selects the edit path — with the candidate count and an optional judge.
|
|
193
|
+
*/
|
|
194
|
+
type BestOfNOptions = (GenerateImageOptions | EditImageOptions) & {
|
|
195
|
+
/** How many candidates to generate (>= 1). */
|
|
196
|
+
n: number;
|
|
197
|
+
/** Chooses the winner. Omit → candidate 0 wins. */
|
|
198
|
+
judge?: ImageJudge;
|
|
199
|
+
};
|
|
200
|
+
/** Result of a {@link MotifImageClient.bestOfN} call. */
|
|
201
|
+
interface BestOfNResult {
|
|
202
|
+
/** The winning candidate (its single-image {@link MotifImageResult}). */
|
|
203
|
+
best: MotifImageResult;
|
|
204
|
+
/** The index of `best` within `candidates`. */
|
|
205
|
+
chosenIndex: number;
|
|
206
|
+
/** Judge's rationale, if it returned one. */
|
|
207
|
+
reason?: string;
|
|
208
|
+
/** All successful candidates, in generation order. */
|
|
209
|
+
candidates: readonly MotifImageResult[];
|
|
210
|
+
/** Total USD across all candidates that were generated (successes only). */
|
|
211
|
+
totalCostUsd: number;
|
|
130
212
|
}
|
|
131
213
|
/** The provider-agnostic image client. Every method returns a Result — no throws. */
|
|
132
214
|
interface MotifImageClient {
|
|
@@ -134,6 +216,30 @@ interface MotifImageClient {
|
|
|
134
216
|
generate: (opts: GenerateImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
|
|
135
217
|
/** Multi-image edit (with optional mask). */
|
|
136
218
|
edit: (opts: EditImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
|
|
219
|
+
/**
|
|
220
|
+
* Generate N candidates (in parallel) and pick the best via an optional
|
|
221
|
+
* injectable judge. Discriminates generate vs edit by the presence of
|
|
222
|
+
* `images`. Returns the winner plus all successful candidates and total spend.
|
|
223
|
+
*/
|
|
224
|
+
bestOfN: (opts: BestOfNOptions) => Promise<Result<BestOfNResult, MotifError>>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Internal dependency-injection seam for the image layer.
|
|
229
|
+
*
|
|
230
|
+
* These types are INTERNAL: they are consumed by `createMotifImage`'s `deps`
|
|
231
|
+
* parameter and by the offline tests, but they are deliberately NOT re-exported
|
|
232
|
+
* from the public `@howells/motif-sdk/image` subpath, so they never appear in
|
|
233
|
+
* `dist/image.d.ts`. Import them from `./deps` inside the package (and from
|
|
234
|
+
* `../src/image/deps` in tests).
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/** A model resolver: builds an AI SDK `ImageModel` for a (provider, model, key). */
|
|
238
|
+
type ResolveImageModel = (provider: ImageProviderId, modelId: string, apiKey?: string) => ImageModel;
|
|
239
|
+
/** Internal dependency-injection seam (default: real `generateImage` + adapters). */
|
|
240
|
+
interface MotifImageDeps {
|
|
241
|
+
generateImage?: typeof generateImage;
|
|
242
|
+
resolveModel?: ResolveImageModel;
|
|
137
243
|
}
|
|
138
244
|
|
|
139
245
|
/**
|
|
@@ -164,7 +270,18 @@ interface ImageProviderAdapter {
|
|
|
164
270
|
* network I/O.
|
|
165
271
|
*/
|
|
166
272
|
readonly resolveModel: (modelId: string, apiKey?: string) => ImageModel;
|
|
167
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Static per-model USD/**image** table (best-effort; cited per adapter). This
|
|
275
|
+
* is multiplied by the returned image count to form the call total.
|
|
276
|
+
*
|
|
277
|
+
* Cost contract: if an adapter instead surfaces a cost on
|
|
278
|
+
* `result.providerMetadata` (the preferred source; see
|
|
279
|
+
* {@link costFromProviderMetadata}), that value MUST already be the **call
|
|
280
|
+
* total** across all `n` images — NOT a per-image figure. `priceUsdByModel`
|
|
281
|
+
* is per-image; `providerMetadata.cost` is the whole call. These two paths
|
|
282
|
+
* intentionally differ, so an adapter must not populate a per-image number
|
|
283
|
+
* into `providerMetadata.cost`.
|
|
284
|
+
*/
|
|
168
285
|
readonly priceUsdByModel: Readonly<Record<string, number>>;
|
|
169
286
|
}
|
|
170
287
|
/**
|
|
@@ -297,13 +414,6 @@ declare function costForImages(provider: ImageProviderId, modelId: string, provi
|
|
|
297
414
|
* ```
|
|
298
415
|
*/
|
|
299
416
|
|
|
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
417
|
/**
|
|
308
418
|
* Create a provider-agnostic image client.
|
|
309
419
|
*
|
|
@@ -313,4 +423,4 @@ interface MotifImageDeps {
|
|
|
313
423
|
*/
|
|
314
424
|
declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
|
|
315
425
|
|
|
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
|
|
426
|
+
export { type BestOfNOptions, type BestOfNResult, type EditImageOptions, FAL_API_KEY_ENV, FAL_TIER_MODELS, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, type GenerateImageOptions, type ImageCost, type ImageCostSource, type ImageJudge, 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));
|
|
@@ -1530,7 +1534,91 @@ function createMotifImage(config = {}, deps = {}) {
|
|
|
1530
1534
|
return err2(toMotifError(error));
|
|
1531
1535
|
}
|
|
1532
1536
|
}
|
|
1533
|
-
|
|
1537
|
+
async function bestOfN(opts) {
|
|
1538
|
+
return await runBestOfN(opts, generate, edit);
|
|
1539
|
+
}
|
|
1540
|
+
return { generate, edit, bestOfN };
|
|
1541
|
+
}
|
|
1542
|
+
async function runCandidate(opts, index, generate, edit) {
|
|
1543
|
+
const candidateSeed = opts.seed === void 0 ? void 0 : opts.seed + index;
|
|
1544
|
+
const overrides = {
|
|
1545
|
+
n: 1,
|
|
1546
|
+
...candidateSeed === void 0 ? {} : { seed: candidateSeed }
|
|
1547
|
+
};
|
|
1548
|
+
if ("images" in opts) {
|
|
1549
|
+
return await edit({ ...opts, ...overrides });
|
|
1550
|
+
}
|
|
1551
|
+
return await generate({ ...opts, ...overrides });
|
|
1552
|
+
}
|
|
1553
|
+
async function selectWinner(opts, candidates) {
|
|
1554
|
+
const { judge } = opts;
|
|
1555
|
+
if (judge === void 0) {
|
|
1556
|
+
return ok2({ chosenIndex: 0 });
|
|
1557
|
+
}
|
|
1558
|
+
const context = "images" in opts ? { instruction: opts.instruction } : { prompt: opts.prompt };
|
|
1559
|
+
const decision = await judge(candidates, context);
|
|
1560
|
+
if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= candidates.length) {
|
|
1561
|
+
return err2(
|
|
1562
|
+
new MotifError(
|
|
1563
|
+
`bestOfN judge returned an out-of-range index ${decision.index} (expected 0..${candidates.length - 1})`,
|
|
1564
|
+
0
|
|
1565
|
+
)
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
return ok2({
|
|
1569
|
+
chosenIndex: decision.index,
|
|
1570
|
+
...decision.reason === void 0 ? {} : { reason: decision.reason }
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async function runBestOfN(opts, generate, edit) {
|
|
1574
|
+
try {
|
|
1575
|
+
const { n } = opts;
|
|
1576
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1577
|
+
return err2(
|
|
1578
|
+
new MotifError(`bestOfN requires an integer n >= 1 (got ${n})`, 0)
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
const results = await Promise.all(
|
|
1582
|
+
Array.from({ length: n }, (_unused, index) => index).map(
|
|
1583
|
+
async (index) => await runCandidate(opts, index, generate, edit)
|
|
1584
|
+
)
|
|
1585
|
+
);
|
|
1586
|
+
const candidates = [];
|
|
1587
|
+
let firstError;
|
|
1588
|
+
for (const result of results) {
|
|
1589
|
+
if (result.isOk()) {
|
|
1590
|
+
candidates.push(result.value);
|
|
1591
|
+
} else {
|
|
1592
|
+
firstError ??= result.error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (candidates.length === 0) {
|
|
1596
|
+
return err2(
|
|
1597
|
+
firstError ?? new MotifError(`all ${n} bestOfN candidates failed`, 0)
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
const winner = await selectWinner(opts, candidates);
|
|
1601
|
+
if (winner.isErr()) {
|
|
1602
|
+
return err2(winner.error);
|
|
1603
|
+
}
|
|
1604
|
+
const { chosenIndex, reason } = winner.value;
|
|
1605
|
+
const best = candidates[chosenIndex];
|
|
1606
|
+
if (best === void 0) {
|
|
1607
|
+
return err2(new MotifError("bestOfN failed to resolve a winner", 0));
|
|
1608
|
+
}
|
|
1609
|
+
const totalCostUsd = Number(
|
|
1610
|
+
candidates.reduce((sum, candidate) => sum + candidate.cost.usd, 0).toFixed(6)
|
|
1611
|
+
);
|
|
1612
|
+
return ok2({
|
|
1613
|
+
best,
|
|
1614
|
+
chosenIndex,
|
|
1615
|
+
...reason === void 0 ? {} : { reason },
|
|
1616
|
+
candidates,
|
|
1617
|
+
totalCostUsd
|
|
1618
|
+
});
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
return err2(toMotifError(error));
|
|
1621
|
+
}
|
|
1534
1622
|
}
|
|
1535
1623
|
function defaultResolveModel(provider, modelId, apiKey) {
|
|
1536
1624
|
return getProviderAdapter(provider).resolveModel(modelId, apiKey);
|
|
@@ -1555,14 +1643,26 @@ function toMotifImageResult(result, provider, model) {
|
|
|
1555
1643
|
images.length
|
|
1556
1644
|
);
|
|
1557
1645
|
const requestId = extractRequestId(result);
|
|
1646
|
+
const warnings = result.warnings.map(renderWarning);
|
|
1558
1647
|
return {
|
|
1559
1648
|
images,
|
|
1560
1649
|
cost,
|
|
1561
1650
|
provider,
|
|
1562
1651
|
model,
|
|
1563
|
-
...requestId === void 0 ? {} : { requestId }
|
|
1652
|
+
...requestId === void 0 ? {} : { requestId },
|
|
1653
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
1564
1654
|
};
|
|
1565
1655
|
}
|
|
1656
|
+
function renderWarning(warning) {
|
|
1657
|
+
if (warning.type === "unsupported" || warning.type === "compatibility") {
|
|
1658
|
+
const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
|
|
1659
|
+
return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
|
|
1660
|
+
}
|
|
1661
|
+
if (warning.type === "deprecated") {
|
|
1662
|
+
return `deprecated setting "${warning.setting}": ${warning.message}`;
|
|
1663
|
+
}
|
|
1664
|
+
return warning.message;
|
|
1665
|
+
}
|
|
1566
1666
|
function isRecord3(value) {
|
|
1567
1667
|
return typeof value === "object" && value !== null;
|
|
1568
1668
|
}
|
|
@@ -1601,13 +1701,13 @@ function toProviderOptions(input) {
|
|
|
1601
1701
|
for (const [namespace, options] of Object.entries(input)) {
|
|
1602
1702
|
const inner = {};
|
|
1603
1703
|
for (const [key, value] of Object.entries(options)) {
|
|
1604
|
-
inner[key] = toJsonValue(value);
|
|
1704
|
+
inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
|
|
1605
1705
|
}
|
|
1606
1706
|
out[namespace] = inner;
|
|
1607
1707
|
}
|
|
1608
1708
|
return out;
|
|
1609
1709
|
}
|
|
1610
|
-
function toJsonValue(value) {
|
|
1710
|
+
function toJsonValue(value, path) {
|
|
1611
1711
|
if (value === null) {
|
|
1612
1712
|
return null;
|
|
1613
1713
|
}
|
|
@@ -1617,18 +1717,21 @@ function toJsonValue(value) {
|
|
|
1617
1717
|
if (typeof value === "object") {
|
|
1618
1718
|
if (Array.isArray(value)) {
|
|
1619
1719
|
const arr = [];
|
|
1620
|
-
for (const item of value) {
|
|
1621
|
-
arr.push(toJsonValue(item));
|
|
1720
|
+
for (const [index, item] of value.entries()) {
|
|
1721
|
+
arr.push(toJsonValue(item, `${path}[${index}]`));
|
|
1622
1722
|
}
|
|
1623
1723
|
return arr;
|
|
1624
1724
|
}
|
|
1625
1725
|
const obj = {};
|
|
1626
1726
|
for (const [key, entry] of Object.entries(value)) {
|
|
1627
|
-
obj[key] = toJsonValue(entry);
|
|
1727
|
+
obj[key] = toJsonValue(entry, `${path}.${key}`);
|
|
1628
1728
|
}
|
|
1629
1729
|
return obj;
|
|
1630
1730
|
}
|
|
1631
|
-
|
|
1731
|
+
throw new MotifError(
|
|
1732
|
+
`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`,
|
|
1733
|
+
0
|
|
1734
|
+
);
|
|
1632
1735
|
}
|
|
1633
1736
|
function toMotifError(error) {
|
|
1634
1737
|
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.10.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",
|