@howells/motif-sdk 0.9.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 CHANGED
@@ -172,6 +172,34 @@ Four providers are implemented, each reading its own API key from the environmen
172
172
 
173
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
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
+
175
203
  ## Testing
176
204
 
177
205
  ```bash
package/dist/image.d.ts CHANGED
@@ -166,12 +166,62 @@ interface MotifImageResult {
166
166
  */
167
167
  warnings?: readonly string[];
168
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;
212
+ }
169
213
  /** The provider-agnostic image client. Every method returns a Result — no throws. */
170
214
  interface MotifImageClient {
171
215
  /** Text→image generation. */
172
216
  generate: (opts: GenerateImageOptions) => Promise<Result<MotifImageResult, MotifError>>;
173
217
  /** Multi-image edit (with optional mask). */
174
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>>;
175
225
  }
176
226
 
177
227
  /**
@@ -373,4 +423,4 @@ declare function costForImages(provider: ImageProviderId, modelId: string, provi
373
423
  */
374
424
  declare function createMotifImage(config?: MotifImageConfig, deps?: MotifImageDeps): MotifImageClient;
375
425
 
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 };
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
@@ -1534,7 +1534,91 @@ function createMotifImage(config = {}, deps = {}) {
1534
1534
  return err2(toMotifError(error));
1535
1535
  }
1536
1536
  }
1537
- return { generate, edit };
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
+ }
1538
1622
  }
1539
1623
  function defaultResolveModel(provider, modelId, apiKey) {
1540
1624
  return getProviderAdapter(provider).resolveModel(modelId, apiKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howells/motif-sdk",
3
- "version": "0.9.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",