litevision 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +7 -6
  2. package/dist/cli.js +1233 -824
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12866,6 +12866,14 @@ function object(shape, params) {
12866
12866
  };
12867
12867
  return new ZodObject(def);
12868
12868
  }
12869
+ function strictObject(shape, params) {
12870
+ return new ZodObject({
12871
+ type: "object",
12872
+ shape,
12873
+ catchall: never(),
12874
+ ...normalizeParams(params)
12875
+ });
12876
+ }
12869
12877
  function looseObject(shape, params) {
12870
12878
  return new ZodObject({
12871
12879
  type: "object",
@@ -21700,11 +21708,7 @@ import { readFile } from "node:fs/promises";
21700
21708
  import { join as join2 } from "node:path";
21701
21709
 
21702
21710
  // src/protocols/protocol.ts
21703
- var aspectRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"];
21704
21711
  var modalities = ["image", "video"];
21705
- function formatSize(size) {
21706
- return `${size.width}x${size.height}`;
21707
- }
21708
21712
 
21709
21713
  class ProviderError extends Error {
21710
21714
  kind;
@@ -21733,7 +21737,8 @@ var modelSchema = object({
21733
21737
  protocol: string2().min(1),
21734
21738
  modality: _enum(modalities).default("image"),
21735
21739
  notes: string2().optional(),
21736
- notesHistory: notesHistorySchema.optional()
21740
+ notesHistory: notesHistorySchema.optional(),
21741
+ standingOptions: record(string2(), unknown()).optional()
21737
21742
  });
21738
21743
  var configSchema = object({
21739
21744
  providers: array(providerSchema).default([]),
@@ -22208,128 +22213,90 @@ function mp4Facts(bytes, view) {
22208
22213
  return { ...size, ...seconds === undefined ? {} : { seconds: Math.round(seconds * 100) / 100 } };
22209
22214
  }
22210
22215
 
22211
- // src/protocols/capabilities.ts
22212
- function describeCapabilities(capabilities) {
22213
- const ratios = capabilities.sizes.map((size) => `${size.aspectRatio} (${formatSize(size)})`).join(", ");
22214
- const references = capabilities.maxReferences === 0 ? "no reference images" : `up to ${capabilities.maxReferences} reference image${capabilities.maxReferences === 1 ? "" : "s"}`;
22215
- return `aspect ratios ${ratios}; up to ${capabilities.maxImages} image${capabilities.maxImages === 1 ? "" : "s"} per Job; ${references}.`;
22216
+ // src/protocols/parameter-guide.ts
22217
+ function guideSummary(guide) {
22218
+ return `Parameter Guide for ${guide.family} (checked ${guide.checked}): ${guide.parameters.map((parameter) => parameter.name).join(", ")}.`;
22216
22219
  }
22217
- function closestSize(sizes, wanted) {
22218
- const exact = sizes.find((size) => size.aspectRatio === wanted);
22219
- if (exact !== undefined)
22220
- return { size: exact, adjusted: false };
22221
- return { size: sizeClosestTo(sizes, ratioOf(wanted)), adjusted: true };
22220
+ function guideText(guide) {
22221
+ const lines = [
22222
+ `Parameter Guide for ${guide.family}, checked ${guide.checked} (sources: ${guide.sources.join(", ")}):`,
22223
+ ...guide.parameters.map((parameter) => `- ${parameter.name}: ${parameter.values} Default: ${parameter.default}. Price: ${parameter.price}`),
22224
+ ...(guide.notes ?? []).map((note) => `Note: ${note}`)
22225
+ ];
22226
+ return lines.join(`
22227
+ `);
22222
22228
  }
22223
- function sizeClosestTo(sizes, ratio) {
22224
- const target = Math.log(ratio);
22225
- const distance = (size) => Math.abs(Math.log(size.width / size.height) - target);
22226
- const [first, ...rest] = sizes;
22227
- if (first === undefined)
22228
- throw new Error("A Protocol adapter declared no supported sizes.");
22229
- return rest.reduce((best, size) => distance(size) < distance(best) ? size : best, first);
22229
+ function guideOutput(guide) {
22230
+ return {
22231
+ family: guide.family,
22232
+ checked: guide.checked,
22233
+ sources: guide.sources,
22234
+ parameters: guide.parameters.map((parameter) => ({ ...parameter })),
22235
+ notes: guide.notes ?? []
22236
+ };
22230
22237
  }
22231
- function ratioOf(aspectRatio) {
22232
- const [width, height] = aspectRatio.split(":").map(Number);
22233
- return width / height;
22238
+ function guideFor(entries, modelId) {
22239
+ const bare = modelId.replace(/^.*\//, "");
22240
+ return entries.find((entry) => entry.matches.test(modelId) || entry.matches.test(bare))?.guide;
22234
22241
  }
22235
- async function normaliseImageRequest(parameters, model, adapter, described, cwd) {
22236
- const { capabilities } = described;
22237
- const unsupported = (problem) => new ConfigError(`${problem} Model "${model.alias}" supports: ${describeCapabilities(capabilities)} Choose another Model or change the request.`);
22238
- if (parameters.n > capabilities.maxImages) {
22239
- throw unsupported(`Model "${model.alias}" cannot generate ${parameters.n} images in one Job.`);
22240
- }
22241
- if (parameters.references.length > 0 && capabilities.maxReferences === 0) {
22242
- throw unsupported(`Model "${model.alias}" cannot take reference images.`);
22243
- }
22244
- if (parameters.references.length > capabilities.maxReferences) {
22245
- throw unsupported(`Model "${model.alias}" cannot take ${parameters.references.length} reference images.`);
22246
- }
22247
- const reserved = Object.keys(parameters.providerOptions).filter((key) => adapter.reservedOptions.includes(key));
22248
- if (reserved.length > 0) {
22249
- throw new ConfigError(`provider_options may not set ${reserved.map((key) => `\`${key}\``).join(", ")}: litevision sets ${reserved.length === 1 ? "it" : "them"} from the \`generate_image\` parameters. Use those instead.`);
22250
- }
22251
- const { size, adjusted } = closestSize(capabilities.sizes, parameters.aspectRatio);
22252
- const references = await readReferences(parameters.references, cwd);
22242
+ function fieldsToRead(adapter, modelId, modality) {
22243
+ const guide = adapter.parameterGuide?.(modelId, modality);
22244
+ const fallback = (modality === "video" ? adapter.requestFields.video : adapter.requestFields.image) ?? { shape: [] };
22245
+ if (guide === undefined)
22246
+ return fallback;
22253
22247
  return {
22254
- request: {
22255
- prompt: parameters.prompt,
22256
- n: parameters.n,
22257
- size,
22258
- references,
22259
- providerOptions: parameters.providerOptions,
22260
- model: { capabilities, ...described.parameterSchema === undefined ? {} : { parameterSchema: described.parameterSchema } }
22261
- },
22262
- requestedAspectRatio: parameters.aspectRatio,
22263
- aspectRatioAdjusted: adjusted
22248
+ shape: guide.shapeFields,
22249
+ ...guide.durationField === undefined ? {} : { duration: guide.durationField },
22250
+ ...fallback.resolution === undefined ? {} : { resolution: fallback.resolution }
22264
22251
  };
22265
22252
  }
22266
- function describeVideoCapabilities(capabilities) {
22267
- const tiers = capabilities.resolutions.map((tier) => {
22268
- const ratios = tier.sizes.map((size) => `${size.aspectRatio} (${formatSize(size)})`).join(", ");
22269
- return tier.name === undefined ? `aspect ratios ${ratios}` : `${tier.name}: ${ratios}`;
22270
- }).join("; ");
22271
- const { durations } = capabilities;
22272
- const lengths = isRun(durations) && durations.length > 2 ? `${durations[0]}–${durations.at(-1)} s` : `${durations.join(", ")} s`;
22273
- const frames = [capabilities.firstFrame && "a first frame", capabilities.lastFrame && "a last frame"].filter(Boolean).join(" and ");
22274
- const references = capabilities.maxReferences === 0 ? "no reference images" : `up to ${capabilities.maxReferences} reference image${capabilities.maxReferences === 1 ? "" : "s"}`;
22275
- const named = capabilities.resolutions.some((tier) => tier.name !== undefined);
22276
- return `${named ? "resolutions " : ""}${tiers}; ${lengths} (default ${capabilities.defaultDuration} s); ${frames || "no first or last frame"}; ${references}.`;
22277
- }
22278
- function isRun(values) {
22279
- return values.every((value, index) => index === 0 || value === values[index - 1] + 1);
22280
- }
22281
- async function normaliseVideoRequest(parameters, model, adapter, capabilities, cwd) {
22282
- const unsupported = (problem) => new ConfigError(`${problem} Model "${model.alias}" supports: ${describeVideoCapabilities(capabilities)} Choose another Model or change the request.`);
22283
- const [defaultTier] = capabilities.resolutions;
22284
- if (defaultTier === undefined)
22285
- throw new Error("A Protocol adapter declared no video resolutions.");
22286
- let tier = defaultTier;
22287
- if (parameters.resolution !== undefined) {
22288
- const wanted = parameters.resolution.toLowerCase();
22289
- const named = capabilities.resolutions.find((candidate) => candidate.name?.toLowerCase() === wanted);
22290
- if (named === undefined)
22291
- throw unsupported(`Model "${model.alias}" has no resolution "${parameters.resolution}".`);
22292
- tier = named;
22293
- }
22294
- if (parameters.firstFrame !== undefined && !capabilities.firstFrame)
22295
- throw unsupported(`Model "${model.alias}" cannot start from a given first frame.`);
22296
- if (parameters.lastFrame !== undefined && !capabilities.lastFrame)
22297
- throw unsupported(`Model "${model.alias}" cannot end on a given last frame.`);
22298
- if (parameters.references.length > 0 && capabilities.maxReferences === 0)
22299
- throw unsupported(`Model "${model.alias}" cannot take reference images.`);
22300
- if (parameters.references.length > capabilities.maxReferences) {
22301
- throw unsupported(`Model "${model.alias}" cannot take ${parameters.references.length} reference images.`);
22302
- }
22303
- const reservedOptions = adapter.reservedVideoOptions ?? [];
22304
- const reserved = Object.keys(parameters.providerOptions).filter((key) => reservedOptions.includes(key));
22305
- if (reserved.length > 0) {
22306
- throw new ConfigError(`provider_options may not set ${reserved.map((key) => `\`${key}\``).join(", ")}: litevision sets ${reserved.length === 1 ? "it" : "them"} from the \`generate_video\` parameters. Use those instead.`);
22253
+ function valueAt(body, path) {
22254
+ return path.split(".").reduce((value, key) => typeof value === "object" && value !== null ? value[key] : undefined, body);
22255
+ }
22256
+ var positive = (value) => {
22257
+ const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
22258
+ return typeof number === "number" && Number.isFinite(number) && number > 0 ? number : undefined;
22259
+ };
22260
+ function parseShape(text) {
22261
+ const match = /^\s*(\d+(?:\.\d+)?)\s*([x×:])\s*(\d+(?:\.\d+)?)\s*$/i.exec(text);
22262
+ if (match === null)
22263
+ return;
22264
+ const width = Number(match[1]);
22265
+ const height = Number(match[3]);
22266
+ if (!(width > 0 && height > 0))
22267
+ return;
22268
+ return { label: `${match[1]}${match[2] === ":" ? ":" : "x"}${match[3]}`, ratio: width / height };
22269
+ }
22270
+ function readShape(fields, body) {
22271
+ for (const field of fields) {
22272
+ if (typeof field === "string") {
22273
+ const value = valueAt(body, field);
22274
+ if (value === undefined)
22275
+ continue;
22276
+ return typeof value === "string" ? parseShape(value)?.label ?? null : null;
22277
+ }
22278
+ const width = valueAt(body, field.width);
22279
+ const height = valueAt(body, field.height);
22280
+ if (width === undefined && height === undefined)
22281
+ continue;
22282
+ const w = positive(width);
22283
+ const h = positive(height);
22284
+ return w === undefined || h === undefined ? null : `${w}x${h}`;
22307
22285
  }
22308
- const frame = async (path) => path === undefined ? undefined : (await readReferences([path], cwd))[0];
22309
- const [firstFrame, lastFrame, references] = await Promise.all([
22310
- frame(parameters.firstFrame),
22311
- frame(parameters.lastFrame),
22312
- readReferences(parameters.references, cwd)
22313
- ]);
22314
- const frameFacts = parameters.aspectRatio === undefined && firstFrame !== undefined ? mediaFactsOf(firstFrame.bytes) : undefined;
22315
- const { size, adjusted } = frameFacts === undefined ? closestSize(tier.sizes, parameters.aspectRatio ?? "16:9") : { size: sizeClosestTo(tier.sizes, frameFacts.width / frameFacts.height), adjusted: false };
22316
- const seconds = parameters.duration === undefined ? capabilities.defaultDuration : capabilities.durations.reduce((best, candidate) => Math.abs(candidate - parameters.duration) < Math.abs(best - parameters.duration) ? candidate : best);
22286
+ return null;
22287
+ }
22288
+ function readResolution(field, body) {
22289
+ const value = field === undefined ? undefined : valueAt(body, field);
22290
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
22291
+ }
22292
+ function readSeconds(field, body) {
22293
+ return field === undefined ? null : positive(valueAt(body, field)) ?? null;
22294
+ }
22295
+ function readRequested(fields, body, modality) {
22317
22296
  return {
22318
- request: {
22319
- prompt: parameters.prompt,
22320
- size,
22321
- ...tier.name === undefined ? {} : { resolution: tier.name },
22322
- seconds,
22323
- ...firstFrame === undefined ? {} : { firstFrame },
22324
- ...lastFrame === undefined ? {} : { lastFrame },
22325
- references,
22326
- providerOptions: parameters.providerOptions
22327
- },
22328
- requestedAspectRatio: frameFacts === undefined ? parameters.aspectRatio ?? "16:9" : size.aspectRatio,
22329
- aspectRatioAdjusted: adjusted,
22330
- followsFirstFrame: frameFacts !== undefined || firstFrame !== undefined && adjusted,
22331
- ...parameters.duration === undefined ? {} : { requestedDuration: parameters.duration },
22332
- durationAdjusted: parameters.duration !== undefined && seconds !== parameters.duration
22297
+ shape: readShape(fields.shape, body),
22298
+ resolution: readResolution(fields.resolution, body),
22299
+ ...modality === "video" ? { seconds: readSeconds(fields.duration, body) } : {}
22333
22300
  };
22334
22301
  }
22335
22302
 
@@ -22389,6 +22356,243 @@ function httpProviderError(providerId, response) {
22389
22356
  return providerError(classifyProviderError(response), providerId, detail);
22390
22357
  }
22391
22358
 
22359
+ // src/protocols/guides/apimart.ts
22360
+ var checked = "2026-11-05";
22361
+ var page = (path) => `https://docs.apimart.ai/en/api-reference/images/${path}`;
22362
+ var pricing = "https://apimart.ai/pricing";
22363
+ var pricingNote = "APIMart publishes prices only on its pricing page, which depends on the account tier; figures here are approximate.";
22364
+ var references = (count) => ({
22365
+ name: "image_urls",
22366
+ values: `${count}; litevision uploads \`references\` through APIMart's upload endpoint and fills this with the links.`,
22367
+ default: "none",
22368
+ price: "not documented."
22369
+ });
22370
+ var count = (range) => ({
22371
+ name: "n",
22372
+ values: `${range} images per task; litevision sends \`n\` here, or one task per image where the model makes one at a time.`,
22373
+ default: "1",
22374
+ price: "each image is billed."
22375
+ });
22376
+ var fifteenRatios = "1:1, 3:2, 2:3, 4:3, 3:4, 5:4, 4:5, 16:9, 9:16, 2:1, 1:2, 3:1, 1:3, 21:9, 9:21";
22377
+ var exactPixels = "or exact WIDTHxHEIGHT under OpenAI's rule (multiples of 16, at most 3840 a side and 3:1, 655,360–8,294,400 pixels)";
22378
+ var gptTiers = "1k, 2k, 4k (16:9 is 1536x864, 2048x1152 or 3840x2160).";
22379
+ var gptImage25 = {
22380
+ family: "GPT Image 2.5 / 2.6 on APIMart",
22381
+ parameters: [
22382
+ { name: "size", values: `auto, ${fifteenRatios}, ${exactPixels}.`, default: "auto", price: "set by resolution." },
22383
+ { name: "resolution", values: `${gptTiers} Ignored for exact pixels.`, default: "1k", price: "billed by output tokens, which grow with the tier and the quality." },
22384
+ { name: "quality", values: "auto, low, medium, high, xhigh, max.", default: "auto", price: "at 1024x1024 about $0.006 low to $0.21 max ($30 per 1M output tokens)." },
22385
+ { name: "background", values: "auto, opaque, transparent.", default: "auto", price: "no effect." },
22386
+ { name: "moderation", values: "auto, low.", default: "low", price: "no effect." },
22387
+ references("up to 16 public links"),
22388
+ count("1–4")
22389
+ ],
22390
+ sources: [page("gpt-image-2.5/generation"), page("gpt-image-2.6/generation"), pricing],
22391
+ checked,
22392
+ notes: ["GPT Image 2.6 adds complexity, markup_url, GIF and SVG output.", pricingNote],
22393
+ shapeFields: ["size"]
22394
+ };
22395
+ var gptImage2 = {
22396
+ family: "GPT Image 2 on APIMart",
22397
+ parameters: [
22398
+ { name: "size", values: `auto (= 1:1), ${fifteenRatios}, ${exactPixels}.`, default: "1:1", price: "set by resolution." },
22399
+ { name: "resolution", values: gptTiers, default: "1k", price: "about $0.0085 at 1K, $0.015 at 2K, $0.022 at 4K per image." },
22400
+ references("up to 15 links or data URIs (gpt-image-2-official: up to 16 public links)"),
22401
+ count("1 (gpt-image-2-official: 1–4)")
22402
+ ],
22403
+ sources: [page("gpt-image-2/generation"), page("gpt-image-2/official"), pricing],
22404
+ checked,
22405
+ notes: [
22406
+ "Without size, the output takes the input image's resolution.",
22407
+ "The gpt-image-2 page gives two pixel sizes for many 1k ratios (1:1 is 1024x1024 or 1254x1254), so the exact 1k output is uncertain.",
22408
+ pricingNote
22409
+ ],
22410
+ shapeFields: ["size"]
22411
+ };
22412
+ var gptImageExt = {
22413
+ family: "GPT Image 2.5 / 2.6 Ext on APIMart",
22414
+ parameters: [
22415
+ { name: "size", values: "auto, 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 21:9; no exact pixels.", default: "auto", price: "no effect." },
22416
+ { name: "resolution", values: "1K, 2K, 4K (gpt-image-2.6-ext: only 1k today).", default: "1K", price: "about $0.0085 at 1K, $0.03 at 2K, $0.04 at 4K per image." },
22417
+ { name: "version", values: "flare, sunburst.", default: "the Provider picks", price: "billed by version × resolution × images delivered." },
22418
+ references("up to 16 links or data URIs"),
22419
+ count("1–4 (gpt-image-2.6-ext: 1)")
22420
+ ],
22421
+ sources: [page("gpt-image-2.5-ext/generation"), page("gpt-image-2.6-ext/generation"), pricing],
22422
+ checked,
22423
+ notes: [pricingNote],
22424
+ shapeFields: ["size"]
22425
+ };
22426
+ var gptImage1 = {
22427
+ family: "GPT Image 1 / 1.5 on APIMart",
22428
+ parameters: [
22429
+ { name: "size", values: "1:1, 3:2, 2:3 (1024x1024, 1536x1024, 1024x1536).", default: "1:1", price: "not documented." },
22430
+ { name: "quality", values: "auto, low, medium, high.", default: "auto", price: "not documented." },
22431
+ { name: "background", values: "auto, opaque, transparent.", default: "auto", price: "no effect." },
22432
+ references("up to 15 public links"),
22433
+ count("1–4")
22434
+ ],
22435
+ sources: [page("gpt-image-1/generation")],
22436
+ checked,
22437
+ notes: ["OpenAI shut gpt-image-1 down on 2026-10-23."],
22438
+ shapeFields: ["size"]
22439
+ };
22440
+ var geminiRatios10 = "auto, 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9";
22441
+ var gemini = (family, path, ratios, tiers, refs, n, notes) => ({
22442
+ family,
22443
+ parameters: [
22444
+ { name: "size", values: `${ratios}.`, default: "auto: the model decides, and image-to-image follows the reference", price: "no effect." },
22445
+ { name: "resolution", values: tiers, default: "1K", price: "higher tiers cost more." },
22446
+ references(refs),
22447
+ count(n)
22448
+ ],
22449
+ sources: [page(path), pricing],
22450
+ checked,
22451
+ notes: [...notes, pricingNote],
22452
+ shapeFields: ["size"]
22453
+ });
22454
+ var seedream = (family, path, ratios, tiers, defaultTier, refs, n, notes, defaultSize = "1:1") => ({
22455
+ family,
22456
+ parameters: [
22457
+ { name: "size", values: `${ratios}.`, default: defaultSize, price: "no effect." },
22458
+ { name: "resolution", values: tiers, default: defaultTier, price: "higher tiers cost more." },
22459
+ references(refs),
22460
+ count(n)
22461
+ ],
22462
+ sources: [page(path), pricing],
22463
+ checked,
22464
+ notes: [...notes, pricingNote],
22465
+ shapeFields: ["size"]
22466
+ });
22467
+ var seedreamRatios = "1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 2:1, 1:2, 21:9, 9:21, auto (needs reference images)";
22468
+ var flux2 = {
22469
+ family: "FLUX.2 on APIMart",
22470
+ parameters: [
22471
+ { name: "size", values: "1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 21:9, 9:21, auto, or WIDTHxHEIGHT.", default: "1:1", price: "set by resolution." },
22472
+ { name: "resolution", values: "1MP, 2MP, 3MP, 4MP (1K, 2K, 4K are taken as 2MP, 3MP, 4MP; 16:9 is 1344x752 at 1MP, 2720x1536 at 4MP).", default: "2MP", price: "flux-2-pro about $0.024, $0.036, $0.048, $0.06." },
22473
+ { name: "width", values: "with height, in place of size: at least 64 each, at most 4,194,304 pixels.", default: "none", price: "by megapixels." },
22474
+ { name: "height", values: "with width, as above.", default: "none", price: "by megapixels." },
22475
+ { name: "safety_tolerance", values: "0–5.", default: "the Provider picks", price: "no effect." },
22476
+ { name: "seed", values: "any integer.", default: "random", price: "no effect." },
22477
+ references("up to 8"),
22478
+ count("1")
22479
+ ],
22480
+ sources: [page("flux-2/generation"), pricing],
22481
+ checked,
22482
+ notes: ["steps (1–50) and guidance (1.5–10) apply to flux-2-flex only.", pricingNote],
22483
+ shapeFields: [{ width: "width", height: "height" }, "size"]
22484
+ };
22485
+ var fluxKontext = {
22486
+ family: "FLUX.1 Kontext on APIMart",
22487
+ parameters: [
22488
+ { name: "size", values: "1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 21:9, 9:21, auto; about a megapixel (16:9 is 1392x752). Pixels are mapped to the nearest ratio; width and height fail.", default: "1:1", price: "not documented." },
22489
+ references("up to 4"),
22490
+ count("1")
22491
+ ],
22492
+ sources: [page("flux-kontext/generation")],
22493
+ checked,
22494
+ shapeFields: ["size"]
22495
+ };
22496
+ var flux3 = {
22497
+ family: "FLUX 3 Image on APIMart",
22498
+ parameters: [
22499
+ { name: "aspect_ratio", values: "21:9, 2:1, 16:9, 3:2, 7:5, 4:3, 5:4, 1:1, 4:5, 3:4, 5:7, 2:3, 9:16, 1:2, 9:21, auto; also taken as size. Pixels are rejected.", default: "auto", price: "no effect." },
22500
+ { name: "resolution", values: "768sq, 1k, 1.5k, 2k, 4k (about 0.6, 1, 2, 4 and 16 MP).", default: "1k", price: "about $0.033, $0.038, $0.056, $0.08, $0.49." },
22501
+ { name: "safety_tolerance", values: "0–4.", default: "the Provider picks", price: "no effect." },
22502
+ references("up to 10"),
22503
+ count("1")
22504
+ ],
22505
+ sources: [page("flux-3-image/generation"), pricing],
22506
+ checked,
22507
+ notes: ["seed, steps, guidance and negative_prompt are rejected.", pricingNote],
22508
+ shapeFields: ["aspect_ratio", "size"]
22509
+ };
22510
+ var entries = [
22511
+ { matches: /^gpt-image-2\.[56]-ext$/, guide: gptImageExt },
22512
+ { matches: /^gpt-image-2\.[56]-(flare|sunburst)$/, guide: gptImage25 },
22513
+ { matches: /^gpt-image-2(-ext|-official)?$/, guide: gptImage2 },
22514
+ { matches: /^gpt-image-1(\.5)?(-official)?$/, guide: gptImage1 },
22515
+ {
22516
+ matches: /^gemini-nano-banana-2\.1(-ext)?$/,
22517
+ guide: gemini("Nano Banana 2.1 on APIMart", "gemini-nano-banana-2.1/generation", `${geminiRatios10}, 1:4, 4:1, 1:8, 8:1`, "1K, 2K, 4K; 0.5K and 512 are rejected, an unknown tier is taken as 1K.", "no fixed count, links or data URIs, up to 20 MB each", "1–4 (Ext: 1)", [])
22518
+ },
22519
+ {
22520
+ matches: /^(gemini-3\.1-flash-image-preview(-official)?|nano-banana-2(-ext)?)$/,
22521
+ guide: gemini("Nano Banana 2 (Gemini 3.1 Flash Image) on APIMart", "gemini-3.1-flash/generation", `${geminiRatios10}, 1:4, 4:1, 1:8, 8:1`, "0.5K, 1K, 2K, 4K.", "up to 14", "1", ["APIMart calls the small tier 0.5K where Google's own API says 512."])
22522
+ },
22523
+ {
22524
+ matches: /^(gemini-3\.1-flash-lite-image(-ext)?|nano-banana-2-lite)$/,
22525
+ guide: gemini("Nano Banana 2 Lite on APIMart", "gemini-3.1-flash/generation-lite", geminiRatios10, "1K only; 2K, 4K and 0.5K are silently taken as 1K.", "up to 14", "1–4", [])
22526
+ },
22527
+ {
22528
+ matches: /^(gemini-3-pro-image-preview(-official)?|nano-banana-pro(-ext)?)$/,
22529
+ guide: gemini("Nano Banana Pro on APIMart", "gemini-3-pro/generation", geminiRatios10, "1K, 2K, 4K.", "up to 14", "1", [
22530
+ "With size auto and no reference, the docs say the result is 1:1 or 16:9."
22531
+ ])
22532
+ },
22533
+ {
22534
+ matches: /^(gemini-2\.5-flash-image-preview(-official)?|nano-banana(-ext)?)$/,
22535
+ guide: gemini("Nano Banana on APIMart", "gemini-2.5-flash/generation", geminiRatios10, "1K only.", "up to 14", "1", [
22536
+ "APIMart allows 14 reference images; Google says the model works best with up to 3.",
22537
+ "Prompts of at most 1000 characters."
22538
+ ])
22539
+ },
22540
+ {
22541
+ matches: /^seedream-4[.-]0$/i,
22542
+ guide: seedream("Seedream 4.0 on APIMart", "seedream-4/generation", seedreamRatios, "1K, 2K, 4K.", "2K", "references and n together at most 15", "1–15", [])
22543
+ },
22544
+ {
22545
+ matches: /^seedream-4[.-]5$/i,
22546
+ guide: seedream("Seedream 4.5 on APIMart", "seedream-4.5/generation", seedreamRatios, "2K, 4K; no 1K (16:9 is 2560x1440 at 2K, 5404x3040 at 4K).", "2K", "references and n together at most 15", "1–15", [
22547
+ "More than one image switches to sequential mode; text-to-image alone cannot make several."
22548
+ ])
22549
+ },
22550
+ {
22551
+ matches: /^seedream-5[.-]0-lite$/i,
22552
+ guide: seedream("Seedream 5.0 Lite on APIMart", "seedream-5-lite/generation", seedreamRatios.replace(", 9:21", ""), "2K, 3K, 4K; no 1K.", "2K", "references and n together at most 15", "1–15", [
22553
+ 'The page header says "2K / 3K" while the field lists 2K, 3K and 4K.'
22554
+ ])
22555
+ },
22556
+ {
22557
+ matches: /^seedream-5[.-]0-(pro|flash)$/i,
22558
+ guide: {
22559
+ ...seedream("Seedream 5.0 Pro / Flash on APIMart", "seedream-5-0-pro/generation", "auto, 1:1, 4:3, 3:4, 16:9, 9:16, 3:2, 2:3, 2:1, 1:2, 21:9, or exact pixels (921,600–4,624,220 pixels, ratio 1/16–16)", "1K, 1.5K, 2K; 3K and 4K are rejected.", "1K", "up to 10, the first free and each further one a fixed surcharge", "1", ['1.5K costs the same as 1K. The Flash page says "also accepted: seedream-5.0-pro", probably a copy error.'], "auto"),
22560
+ sources: [page("seedream-5-0-pro/generation"), page("seedream-5-0-flash/generation"), pricing]
22561
+ }
22562
+ },
22563
+ { matches: /^flux-2-(flex|pro|max)$/, guide: flux2 },
22564
+ { matches: /^flux-kontext-(pro|max)$/, guide: fluxKontext },
22565
+ { matches: /^flux-3-image$/, guide: flux3 }
22566
+ ];
22567
+ var apimartGuide = (modelId) => guideFor(entries, modelId);
22568
+
22569
+ // src/protocols/multipart.ts
22570
+ import { randomUUID } from "node:crypto";
22571
+ function multipartBody(fields, files) {
22572
+ const boundary = `litevision-${randomUUID()}`;
22573
+ const encoder = new TextEncoder;
22574
+ const quote = (value) => value.replace(/["\r\n\\]/g, (char) => encodeURIComponent(char));
22575
+ const parts = [];
22576
+ for (const [name, value] of fields) {
22577
+ parts.push(encoder.encode(`--${boundary}\r
22578
+ Content-Disposition: form-data; name="${quote(name)}"\r
22579
+ \r
22580
+ ${value}\r
22581
+ `));
22582
+ }
22583
+ for (const file of files) {
22584
+ parts.push(encoder.encode(`--${boundary}\r
22585
+ Content-Disposition: form-data; name="${quote(file.name)}"; filename="${quote(file.fileName)}"\r
22586
+ Content-Type: ${file.mimeType}\r
22587
+ \r
22588
+ `), file.bytes, encoder.encode(`\r
22589
+ `));
22590
+ }
22591
+ parts.push(encoder.encode(`--${boundary}--\r
22592
+ `));
22593
+ return { body: Buffer.concat(parts), contentType: `multipart/form-data; boundary=${boundary}` };
22594
+ }
22595
+
22392
22596
  // src/protocols/send.ts
22393
22597
  var maxRetries = 2;
22394
22598
  async function sendToProvider(http, providerId, url, init, { context, poll = false }) {
@@ -22453,7 +22657,6 @@ function networkError(providerId, error, stage, poll, attempts) {
22453
22657
 
22454
22658
  // src/protocols/apimart.ts
22455
22659
  var docsBase = "https://docs.apimart.ai/en/api-reference/images";
22456
- var maxImagesPerJob = 4;
22457
22660
  var errorSchema = object({
22458
22661
  error: object({
22459
22662
  code: union([number2(), string2()]).nullish(),
@@ -22475,82 +22678,19 @@ var taskSchema = object({
22475
22678
  })
22476
22679
  });
22477
22680
  var resumeSchema = object({ taskIds: array(string2()).min(1) });
22478
- var sizes = (entries) => entries.map(([aspectRatio, width, height]) => ({ aspectRatio, width, height }));
22479
22681
  var knownModels = [
22480
- {
22481
- matches: /^gpt-image-2(-ext|-official)?$/,
22482
- docsPage: "gpt-image-2/generation",
22483
- capabilities: {
22484
- sizes: sizes([
22485
- ["1:1", 1024, 1024],
22486
- ["3:2", 1536, 1024],
22487
- ["2:3", 1024, 1536],
22488
- ["4:3", 1024, 768],
22489
- ["3:4", 768, 1024],
22490
- ["16:9", 1536, 864],
22491
- ["9:16", 864, 1536]
22492
- ]),
22493
- maxReferences: 15,
22494
- maxImages: maxImagesPerJob
22495
- },
22496
- batches: false
22497
- },
22498
- {
22499
- matches: /^gpt-image-1(\.5)?(-official)?$/,
22500
- docsPage: "gpt-image-1/generation",
22501
- capabilities: {
22502
- sizes: sizes([
22503
- ["1:1", 1024, 1024],
22504
- ["3:2", 1536, 1024],
22505
- ["2:3", 1024, 1536]
22506
- ]),
22507
- maxReferences: 0,
22508
- maxImages: maxImagesPerJob
22509
- },
22510
- batches: true
22511
- },
22512
- {
22513
- matches: /^(gemini-3-pro-image-preview(-official)?|nano-banana-pro(-ext)?)$/,
22514
- docsPage: "gemini-3-pro/generation",
22515
- capabilities: {
22516
- sizes: sizes([
22517
- ["1:1", 1024, 1024],
22518
- ["3:2", 1264, 848],
22519
- ["2:3", 848, 1264],
22520
- ["4:3", 1200, 896],
22521
- ["3:4", 896, 1200],
22522
- ["16:9", 1376, 768],
22523
- ["9:16", 768, 1376]
22524
- ]),
22525
- maxReferences: 14,
22526
- maxImages: maxImagesPerJob
22527
- },
22528
- batches: false
22529
- },
22530
- {
22531
- matches: /^seedream-4[.-]5$/i,
22532
- docsPage: "seedream-4.5/generation",
22533
- capabilities: {
22534
- sizes: sizes([
22535
- ["1:1", 2048, 2048],
22536
- ["3:2", 2496, 1664],
22537
- ["2:3", 1664, 2496],
22538
- ["4:3", 2304, 1728],
22539
- ["3:4", 1728, 2304],
22540
- ["16:9", 2560, 1440],
22541
- ["9:16", 1440, 2560]
22542
- ]),
22543
- maxReferences: 14,
22544
- maxImages: maxImagesPerJob
22545
- },
22546
- batches: false
22547
- }
22682
+ { matches: /^gpt-image-2-official$/, docsPage: "gpt-image-2/official", batches: true },
22683
+ { matches: /^gpt-image-2(-ext)?$/, docsPage: "gpt-image-2/generation", batches: false },
22684
+ { matches: /^gpt-image-2\.5-(flare|sunburst)$/, docsPage: "gpt-image-2.5/generation", batches: true },
22685
+ { matches: /^gpt-image-2\.6-(flare|sunburst)$/, docsPage: "gpt-image-2.6/generation", batches: true },
22686
+ { matches: /^gpt-image-2\.5-ext$/, docsPage: "gpt-image-2.5-ext/generation", batches: true },
22687
+ { matches: /^gpt-image-2\.6-ext$/, docsPage: "gpt-image-2.6-ext/generation", batches: false },
22688
+ { matches: /^gpt-image-1(\.5)?(-official)?$/, docsPage: "gpt-image-1/generation", batches: true },
22689
+ { matches: /^gemini-nano-banana-2\.1$/, docsPage: "gemini-nano-banana-2.1/generation", batches: true },
22690
+ { matches: /^(gemini-3\.1-flash-lite-image(-ext)?|nano-banana-2-lite)$/, docsPage: "gemini-3.1-flash/generation-lite", batches: true },
22691
+ { matches: /^(gemini-3-pro-image-preview(-official)?|nano-banana-pro(-ext)?)$/, docsPage: "gemini-3-pro/generation", batches: false },
22692
+ { matches: /^seedream-4[.-]5$/i, docsPage: "seedream-4.5/generation", batches: false }
22548
22693
  ];
22549
- var unknownModel = {
22550
- sizes: sizes([["1:1", 1024, 1024]]),
22551
- maxReferences: 0,
22552
- maxImages: maxImagesPerJob
22553
- };
22554
22694
  var known = (modelId) => knownModels.find((model) => model.matches.test(modelId));
22555
22695
  var headers = (credential) => ({ authorization: `Bearer ${credential}` });
22556
22696
  var baseUrlOf = (target) => target.baseUrl.replace(/\/+$/, "");
@@ -22566,6 +22706,26 @@ async function errorOf(response, providerId, options = []) {
22566
22706
  options
22567
22707
  });
22568
22708
  }
22709
+ var uploadedSchema = object({ url: string2().min(1) });
22710
+ async function uploadReference(reference, target, http, context) {
22711
+ const { body, contentType } = multipartBody([], [{ name: "file", ...reference }]);
22712
+ const init = { method: "POST", headers: { ...headers(target.credential), "content-type": contentType }, body };
22713
+ const failed = (error) => new ProviderError(error instanceof ProviderError ? error.kind : "other", `Could not upload the reference image ${reference.fileName} to APIMart. ${error.message}`);
22714
+ let response;
22715
+ try {
22716
+ response = await sendToProvider(http, target.providerId, `${baseUrlOf(target)}/uploads/images`, init, { context });
22717
+ } catch (error) {
22718
+ throw error instanceof ProviderError ? failed(error) : error;
22719
+ }
22720
+ if (!response.ok)
22721
+ throw failed(await errorOf(response, target.providerId));
22722
+ const parsed = uploadedSchema.safeParse(await response.json().catch(() => {
22723
+ return;
22724
+ }));
22725
+ if (!parsed.success)
22726
+ throw failed(providerError("other", target.providerId, "its answer to the upload had no link."));
22727
+ return parsed.data.url;
22728
+ }
22569
22729
  async function submitTask(body, options, target, http, context) {
22570
22730
  const init = {
22571
22731
  method: "POST",
@@ -22637,32 +22797,6 @@ function waitForTasks(taskIds, target, http, context, submissionFailures = []) {
22637
22797
  }
22638
22798
  };
22639
22799
  }
22640
- function capabilitiesFromSchema(schema, modelId) {
22641
- const properties = object({
22642
- properties: object({
22643
- size: object({ enum: array(unknown()) }).optional(),
22644
- image_urls: object({ maxItems: number2().int().nonnegative().optional() }).optional()
22645
- })
22646
- }).safeParse(schema);
22647
- if (!properties.success)
22648
- return;
22649
- const { size, image_urls } = properties.data.properties;
22650
- const ratios = aspectRatios.filter((ratio) => size?.enum.includes(ratio));
22651
- if (ratios.length === 0)
22652
- return;
22653
- const builtIn = known(modelId)?.capabilities ?? unknownModel;
22654
- return {
22655
- sizes: ratios.map((ratio) => builtIn.sizes.find((candidate) => candidate.aspectRatio === ratio) ?? nominalSize(ratio)),
22656
- maxReferences: image_urls === undefined ? 0 : image_urls.maxItems ?? Math.max(builtIn.maxReferences, 1),
22657
- maxImages: maxImagesPerJob
22658
- };
22659
- }
22660
- function nominalSize(aspectRatio) {
22661
- const [width, height] = aspectRatio.split(":").map(Number);
22662
- const scale = Math.sqrt(1024 * 1024 / (width * height));
22663
- const round = (value) => Math.round(value * scale / 16) * 16;
22664
- return { aspectRatio, width: round(width), height: round(height) };
22665
- }
22666
22800
  var schemaResponse = object({ parameters: object({ input_schema: unknown() }).optional() });
22667
22801
  var modelListSchema = object({
22668
22802
  data: array(object({ id: string2(), category: string2().optional() }))
@@ -22691,7 +22825,6 @@ async function fetchModelIds(target, http) {
22691
22825
  return { ok: true, modelIds: ids.sort() };
22692
22826
  }
22693
22827
  var apimart = {
22694
- builtInCapabilities: (modelId) => known(modelId)?.capabilities ?? unknownModel,
22695
22828
  async fetchModelInfo(target, http) {
22696
22829
  const response = await http(`${baseUrlOf(target)}/models/${encodeURIComponent(target.modelId)}/schema`, {
22697
22830
  headers: headers(target.credential)
@@ -22703,24 +22836,23 @@ var apimart = {
22703
22836
  const body = await response.json();
22704
22837
  const item = schemaResponse.safeParse(body?.data ?? body);
22705
22838
  const parameterSchema = item.success ? item.data.parameters?.input_schema : undefined;
22706
- if (parameterSchema === undefined)
22707
- return;
22708
- const capabilities = capabilitiesFromSchema(parameterSchema, target.modelId);
22709
- return { parameterSchema, ...capabilities === undefined ? {} : { capabilities } };
22839
+ return parameterSchema === undefined ? undefined : { parameterSchema };
22710
22840
  },
22711
22841
  docsUrl: (modelId) => {
22712
22842
  const model = known(modelId);
22713
22843
  return model === undefined ? "https://docs.apimart.ai/en" : `${docsBase}/${model.docsPage}`;
22714
22844
  },
22715
- reservedOptions: ["model", "prompt", "size", "n", "image_urls"],
22845
+ parameterGuide: (modelId, modality) => modality === "image" ? apimartGuide(modelId) : undefined,
22846
+ requestFields: { image: { shape: ["size"], resolution: "resolution" } },
22847
+ reservedOptions: ["model", "prompt", "image_urls"],
22716
22848
  async submit(request, target, http, context) {
22717
22849
  const batches = known(target.modelId)?.batches ?? false;
22850
+ const imageUrls = await Promise.all(request.references.map((reference) => uploadReference(reference, target, http, context)));
22718
22851
  const body = {
22719
22852
  model: target.modelId,
22720
22853
  prompt: request.prompt,
22721
- size: request.size.aspectRatio,
22722
22854
  n: batches ? request.n : 1,
22723
- ...request.references.length === 0 ? {} : { image_urls: request.references.map((reference) => `data:${reference.mimeType};base64,${Buffer.from(reference.bytes).toString("base64")}`) },
22855
+ ...imageUrls.length === 0 ? {} : { image_urls: imageUrls },
22724
22856
  ...request.providerOptions
22725
22857
  };
22726
22858
  const settled = await Promise.allSettled(Array.from({ length: batches ? 1 : request.n }, () => submitTask(body, Object.keys(request.providerOptions), target, http, context)));
@@ -22745,6 +22877,96 @@ var apimart = {
22745
22877
  listModelIds: fetchModelIds
22746
22878
  };
22747
22879
 
22880
+ // src/protocols/guides/gemini.ts
22881
+ var checked2 = "2026-11-05";
22882
+ var sources = [
22883
+ "https://ai.google.dev/gemini-api/docs/image-generation",
22884
+ "https://aistudio.google.com/api-reference/interactions-api/create-interaction",
22885
+ "https://ai.google.dev/gemini-api/docs/pricing"
22886
+ ];
22887
+ var tenRatios = "1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9";
22888
+ var fourteenRatios = `${tenRatios}, 1:4, 4:1, 1:8, 8:1`;
22889
+ var ratio = (ratios) => ({
22890
+ name: "response_format.aspect_ratio",
22891
+ values: `${ratios}.`,
22892
+ default: "the shape of the input image, else 1:1",
22893
+ price: "no effect: every ratio of a tier costs the same."
22894
+ });
22895
+ var mimeType = { name: "response_format.mime_type", values: "image/jpeg.", default: "the Provider picks", price: "no effect." };
22896
+ var imageSize = (values, price) => ({
22897
+ name: "response_format.image_size",
22898
+ values: `${values} Uppercase K: a lowercase 1k is rejected.`,
22899
+ default: "1K",
22900
+ price
22901
+ });
22902
+ var shape = ["response_format.aspect_ratio"];
22903
+ var nanoBanana21 = {
22904
+ family: "Nano Banana 2.1 (gemini-nano-banana-2.1)",
22905
+ parameters: [
22906
+ ratio(fourteenRatios),
22907
+ imageSize("1K, 2K, 4K (16:9: 1376x768, 2752x1536, 5504x3072).", "about $0.034 at 1K, $0.050 at 2K, $0.113 at 4K ($30 per 1M image tokens)."),
22908
+ mimeType
22909
+ ],
22910
+ sources: [...sources, "https://ai.google.dev/gemini-api/docs/models/gemini-nano-banana-2.1"],
22911
+ checked: checked2,
22912
+ notes: ["4K tokens: the image guide says 2520, the pricing page 3780 ($0.113); the price above follows the pricing page."],
22913
+ shapeFields: shape
22914
+ };
22915
+ var flash31 = {
22916
+ family: "Nano Banana 2 (gemini-3.1-flash-image)",
22917
+ parameters: [
22918
+ ratio(fourteenRatios),
22919
+ imageSize("512, 1K, 2K, 4K (16:9: 688x384, 1376x768, 2752x1536, 5504x3072).", "about $0.045 at 512, $0.067 at 1K, $0.101 at 2K, $0.151 at 4K."),
22920
+ mimeType
22921
+ ],
22922
+ sources: [...sources, "https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-image"],
22923
+ checked: checked2,
22924
+ notes: [
22925
+ `The small tier's value is "512" (the guide also calls it 0.5K).`,
22926
+ "The guide prints 21:9 at 512 as 792x168, which is not 21:9; probably 792x336."
22927
+ ],
22928
+ shapeFields: shape
22929
+ };
22930
+ var flashLite31 = {
22931
+ family: "Nano Banana 2 Lite (gemini-3.1-flash-lite-image)",
22932
+ parameters: [ratio(tenRatios), imageSize("1K only; 2K and 4K are not supported.", "about $0.034 per image."), mimeType],
22933
+ sources: [...sources, "https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-lite-image"],
22934
+ checked: checked2,
22935
+ notes: ['The model page speaks of "14 aspect ratios" but lists the 10 above.', "Not optimized for several reference images."],
22936
+ shapeFields: shape
22937
+ };
22938
+ var pro3 = {
22939
+ family: "Nano Banana Pro (gemini-3-pro-image)",
22940
+ parameters: [
22941
+ ratio(tenRatios),
22942
+ imageSize("1K, 2K, 4K (16:9: 1376x768, 2752x1536, 5504x3072).", "about $0.134 at 1K, the same at 2K, $0.24 at 4K."),
22943
+ mimeType
22944
+ ],
22945
+ sources: [...sources, "https://ai.google.dev/gemini-api/docs/models/gemini-3-pro-image"],
22946
+ checked: checked2,
22947
+ notes: ["Up to 14 reference images, 5 of them with high fidelity."],
22948
+ shapeFields: shape
22949
+ };
22950
+ var flash25 = {
22951
+ family: "Nano Banana (gemini-2.5-flash-image)",
22952
+ parameters: [
22953
+ { ...ratio(tenRatios), values: `${tenRatios}; one size each (16:9 is 1344x768).` },
22954
+ mimeType
22955
+ ],
22956
+ sources: [...sources, "https://ai.google.dev/gemini-api/docs/models/gemini-2.5-flash-image"],
22957
+ checked: checked2,
22958
+ notes: ["Takes no image_size. Works best with up to 3 reference images.", "Costs $0.039 per image."],
22959
+ shapeFields: shape
22960
+ };
22961
+ var entries2 = [
22962
+ { matches: /gemini-nano-banana-2\.1/, guide: nanoBanana21 },
22963
+ { matches: /gemini-3\.1-flash-lite-image/, guide: flashLite31 },
22964
+ { matches: /gemini-3\.1-flash-image/, guide: flash31 },
22965
+ { matches: /gemini-3-pro-image/, guide: pro3 },
22966
+ { matches: /gemini-2\.5-flash-image/, guide: flash25 }
22967
+ ];
22968
+ var geminiGuide = (modelId) => guideFor(entries2, modelId);
22969
+
22748
22970
  // src/protocols/parallel.ts
22749
22971
  async function inParallel(n, providerId, send) {
22750
22972
  const settled = await Promise.allSettled(Array.from({ length: n }, () => send()));
@@ -22793,50 +23015,8 @@ var blockedCodes = new Set([
22793
23015
  "image_other",
22794
23016
  "content_blocked"
22795
23017
  ]);
22796
- var sizes2 = (entries) => entries.map(([aspectRatio, width, height]) => ({ aspectRatio, width, height }));
22797
- var gemini3 = {
22798
- capabilities: {
22799
- sizes: sizes2([
22800
- ["1:1", 1024, 1024],
22801
- ["16:9", 1376, 768],
22802
- ["9:16", 768, 1376],
22803
- ["4:3", 1200, 896],
22804
- ["3:4", 896, 1200],
22805
- ["3:2", 1264, 848],
22806
- ["2:3", 848, 1264]
22807
- ]),
22808
- maxReferences: 14,
22809
- maxImages: 4
22810
- },
22811
- imageSize: "1K"
22812
- };
22813
- var gemini25 = {
22814
- capabilities: {
22815
- sizes: sizes2([
22816
- ["1:1", 1024, 1024],
22817
- ["16:9", 1344, 768],
22818
- ["9:16", 768, 1344],
22819
- ["4:3", 1184, 864],
22820
- ["3:4", 864, 1184],
22821
- ["3:2", 1248, 832],
22822
- ["2:3", 832, 1248]
22823
- ]),
22824
- maxReferences: 3,
22825
- maxImages: 4
22826
- }
22827
- };
22828
- var unknownModel2 = { capabilities: { ...gemini3.capabilities, maxReferences: 3 } };
22829
- var gemini3Ids = ["gemini-nano-banana-2.1", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image"];
22830
- function geminiModel(modelId) {
22831
- if (modelId.includes("gemini-2.5-flash-image"))
22832
- return gemini25;
22833
- if (gemini3Ids.some((id) => modelId.includes(id)))
22834
- return gemini3;
22835
- return unknownModel2;
22836
- }
22837
23018
  function interactionBody(request, target) {
22838
23019
  const { response_format: extraFormat, ...options } = request.providerOptions;
22839
- const { imageSize } = geminiModel(target.modelId);
22840
23020
  return {
22841
23021
  model: target.modelId,
22842
23022
  input: [
@@ -22848,12 +23028,7 @@ function interactionBody(request, target) {
22848
23028
  }))
22849
23029
  ],
22850
23030
  ...options,
22851
- response_format: {
22852
- ...imageSize === undefined ? {} : { image_size: imageSize },
22853
- ...isObject2(extraFormat) ? extraFormat : {},
22854
- type: "image",
22855
- aspect_ratio: request.size.aspectRatio
22856
- }
23031
+ response_format: { ...isObject2(extraFormat) ? extraFormat : {}, type: "image" }
22857
23032
  };
22858
23033
  }
22859
23034
  function isObject2(value) {
@@ -22901,10 +23076,11 @@ async function sendInteraction(request, target, http, context) {
22901
23076
  const said = content.flatMap((block) => block.text === undefined ? [] : [block.text]).join(" ");
22902
23077
  throw providerError("other", target.providerId, `the Interaction ended "${interaction.status}" with no image.${said === "" ? "" : ` The model wrote: ${said}`}`);
22903
23078
  }
22904
- var gemini = {
22905
- builtInCapabilities: (modelId) => geminiModel(modelId).capabilities,
23079
+ var gemini2 = {
22906
23080
  docsUrl: () => "https://ai.google.dev/gemini-api/docs/image-generation",
22907
- reservedOptions: ["model", "input"],
23081
+ parameterGuide: (modelId, modality) => modality === "image" ? geminiGuide(modelId) : undefined,
23082
+ requestFields: { image: { shape: ["response_format.aspect_ratio"], resolution: "response_format.image_size" } },
23083
+ reservedOptions: ["model", "input", "response_format.type"],
22908
23084
  async submit(request, target, http, context) {
22909
23085
  return { wait: () => inParallel(request.n, target.providerId, () => sendInteraction(request, target, http, context)) };
22910
23086
  },
@@ -22957,32 +23133,121 @@ function trimmed(baseUrl) {
22957
23133
  return baseUrl.replace(/\/+$/, "");
22958
23134
  }
22959
23135
 
22960
- // src/protocols/multipart.ts
22961
- import { randomUUID } from "node:crypto";
22962
- function multipartBody(fields, files) {
22963
- const boundary = `litevision-${randomUUID()}`;
22964
- const encoder = new TextEncoder;
22965
- const quote = (value) => value.replace(/["\r\n\\]/g, (char) => encodeURIComponent(char));
22966
- const parts = [];
22967
- for (const [name, value] of fields) {
22968
- parts.push(encoder.encode(`--${boundary}\r
22969
- Content-Disposition: form-data; name="${quote(name)}"\r
22970
- \r
22971
- ${value}\r
22972
- `));
22973
- }
22974
- for (const file of files) {
22975
- parts.push(encoder.encode(`--${boundary}\r
22976
- Content-Disposition: form-data; name="${quote(file.name)}"; filename="${quote(file.fileName)}"\r
22977
- Content-Type: ${file.mimeType}\r
22978
- \r
22979
- `), file.bytes, encoder.encode(`\r
22980
- `));
22981
- }
22982
- parts.push(encoder.encode(`--${boundary}--\r
22983
- `));
22984
- return { body: Buffer.concat(parts), contentType: `multipart/form-data; boundary=${boundary}` };
22985
- }
23136
+ // src/protocols/guides/openai-images.ts
23137
+ var checked3 = "2026-11-05";
23138
+ var sources2 = [
23139
+ "https://developers.openai.com/api/reference/resources/images/methods/generate",
23140
+ "https://developers.openai.com/api/reference/resources/images/methods/edit",
23141
+ "https://developers.openai.com/api/docs/guides/image-generation"
23142
+ ];
23143
+ var shared = {
23144
+ background: {
23145
+ name: "background",
23146
+ values: "auto, opaque, transparent (transparent needs output_format png or webp).",
23147
+ default: "auto",
23148
+ price: "no effect."
23149
+ },
23150
+ outputFormat: {
23151
+ name: "output_format",
23152
+ values: "png, jpeg, webp.",
23153
+ default: "png",
23154
+ price: "no effect."
23155
+ },
23156
+ outputCompression: {
23157
+ name: "output_compression",
23158
+ values: "0–100, for jpeg and webp.",
23159
+ default: "100",
23160
+ price: "no effect."
23161
+ },
23162
+ moderation: { name: "moderation", values: "auto, low.", default: "auto", price: "no effect." },
23163
+ n: { name: "n", values: "1–10 images per request; litevision sends `n` here.", default: "1", price: "each image is billed." }
23164
+ };
23165
+ var arbitrarySize = "auto, or any WIDTHxHEIGHT: both sides multiples of 16, the long side at most 3:1 to the short, at most 3840 a side, 655,360–8,294,400 pixels in all (above 2560x1440 is experimental). Popular: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 2048x1152, 3840x2160, 2160x3840.";
23166
+ var sizePrice = "billed by output tokens, which grow with the size and the quality; a larger non-square size can cost fewer tokens than a square.";
23167
+ var gptImage252 = {
23168
+ family: "GPT Image 2.5",
23169
+ parameters: [
23170
+ { name: "size", values: arbitrarySize, default: "auto", price: sizePrice },
23171
+ {
23172
+ name: "quality",
23173
+ values: "auto, low, medium, high, xhigh, max.",
23174
+ default: "auto",
23175
+ price: "the main price lever: at 1024x1024 about $0.006 low, $0.013 medium, $0.053 high, up to $0.21 max ($30 per 1M image output tokens)."
23176
+ },
23177
+ shared.background,
23178
+ shared.outputFormat,
23179
+ shared.outputCompression,
23180
+ shared.moderation,
23181
+ shared.n
23182
+ ],
23183
+ sources: [...sources2, "https://developers.openai.com/api/docs/models/gpt-image-2.5-flare"],
23184
+ checked: checked3,
23185
+ shapeFields: ["size"]
23186
+ };
23187
+ var gptImage26 = {
23188
+ ...gptImage252,
23189
+ family: "GPT Image 2.6",
23190
+ parameters: [
23191
+ ...gptImage252.parameters,
23192
+ { name: "complexity", values: "low, medium, high.", default: "high", price: "no separate fee." }
23193
+ ],
23194
+ sources: [...sources2, "https://developers.openai.com/api/docs/models/gpt-image-2.6-sunburst"],
23195
+ notes: ["The size rule above is GPT Image 2.5's: OpenAI does not say outright that 2.6 still images take arbitrary sizes."]
23196
+ };
23197
+ var gptImage22 = {
23198
+ family: "GPT Image 2",
23199
+ parameters: [
23200
+ { name: "size", values: arbitrarySize, default: "auto", price: sizePrice },
23201
+ {
23202
+ name: "quality",
23203
+ values: "auto, low, medium, high.",
23204
+ default: "auto",
23205
+ price: "1024x1024 costs $0.006 low, $0.053 medium, $0.211 high; 1536x1024 and 1024x1536 cost $0.005, $0.041, $0.165."
23206
+ },
23207
+ { ...shared.background, values: "auto, opaque, transparent (transparent is a preview on gpt-image-2)." },
23208
+ shared.outputFormat,
23209
+ shared.outputCompression,
23210
+ shared.moderation,
23211
+ shared.n
23212
+ ],
23213
+ sources: [...sources2, "https://developers.openai.com/api/docs/models/gpt-image-2"],
23214
+ checked: checked3,
23215
+ shapeFields: ["size"]
23216
+ };
23217
+ var gptImage12 = {
23218
+ family: "GPT Image 1.x",
23219
+ parameters: [
23220
+ {
23221
+ name: "size",
23222
+ values: "auto, 1024x1024, 1536x1024, 1024x1536.",
23223
+ default: "auto",
23224
+ price: "gpt-image-1.5: portrait and landscape cost about 1.5 times the square."
23225
+ },
23226
+ {
23227
+ name: "quality",
23228
+ values: "auto, low, medium, high.",
23229
+ default: "auto",
23230
+ price: "gpt-image-1.5 at 1024x1024: $0.009 low, $0.034 medium, $0.133 high; gpt-image-1-mini: $0.005, $0.011, $0.036."
23231
+ },
23232
+ { name: "input_fidelity", values: "high, low (gpt-image-1-mini takes low only).", default: "low", price: "not documented." },
23233
+ shared.background,
23234
+ shared.outputFormat,
23235
+ shared.outputCompression,
23236
+ shared.moderation,
23237
+ shared.n
23238
+ ],
23239
+ sources: [...sources2, "https://developers.openai.com/api/docs/models/gpt-image-1.5"],
23240
+ checked: checked3,
23241
+ notes: ["OpenAI marks gpt-image-1.5 deprecated and shut gpt-image-1 down on 2026-10-23; a relay may still serve them."],
23242
+ shapeFields: ["size"]
23243
+ };
23244
+ var entries3 = [
23245
+ { matches: /^gpt-image-2\.6(-|$)/, guide: gptImage26 },
23246
+ { matches: /^gpt-image-2\.5(-|$)/, guide: gptImage252 },
23247
+ { matches: /^gpt-image-2(-\d{4}-\d{2}-\d{2})?$/, guide: gptImage22 },
23248
+ { matches: /^(gpt-image-1(\.5|-mini)?|chatgpt-image-latest)$/, guide: gptImage12 }
23249
+ ];
23250
+ var openaiImagesGuide = (modelId) => guideFor(entries3, modelId);
22986
23251
 
22987
23252
  // src/protocols/openai-images.ts
22988
23253
  var successSchema = object({
@@ -22996,29 +23261,6 @@ var errorSchema3 = object({
22996
23261
  param: string2().nullish()
22997
23262
  })
22998
23263
  });
22999
- var gptImage = {
23000
- sizes: [
23001
- { aspectRatio: "1:1", width: 1024, height: 1024 },
23002
- { aspectRatio: "3:2", width: 1536, height: 1024 },
23003
- { aspectRatio: "2:3", width: 1024, height: 1536 }
23004
- ],
23005
- maxReferences: 16,
23006
- maxImages: 10
23007
- };
23008
- var dalle3 = {
23009
- sizes: [
23010
- { aspectRatio: "1:1", width: 1024, height: 1024 },
23011
- { aspectRatio: "16:9", width: 1792, height: 1024 },
23012
- { aspectRatio: "9:16", width: 1024, height: 1792 }
23013
- ],
23014
- maxReferences: 0,
23015
- maxImages: 1
23016
- };
23017
- var dalle2 = {
23018
- sizes: [{ aspectRatio: "1:1", width: 1024, height: 1024 }],
23019
- maxReferences: 1,
23020
- maxImages: 10
23021
- };
23022
23264
  function buildRequest(request, target) {
23023
23265
  const baseUrl = target.baseUrl.replace(/\/+$/, "");
23024
23266
  const headers = { authorization: `Bearer ${target.credential}` };
@@ -23026,7 +23268,6 @@ function buildRequest(request, target) {
23026
23268
  model: target.modelId,
23027
23269
  prompt: request.prompt,
23028
23270
  n: request.n,
23029
- size: formatSize(request.size),
23030
23271
  ...request.providerOptions
23031
23272
  };
23032
23273
  if (request.references.length === 0) {
@@ -23040,15 +23281,10 @@ function buildRequest(request, target) {
23040
23281
  return { url: `${baseUrl}/images/edits`, init: { method: "POST", headers: { ...headers, "content-type": contentType }, body } };
23041
23282
  }
23042
23283
  var openaiImages = {
23043
- builtInCapabilities(modelId) {
23044
- if (modelId.startsWith("dall-e-3"))
23045
- return dalle3;
23046
- if (modelId.startsWith("dall-e-2"))
23047
- return dalle2;
23048
- return gptImage;
23049
- },
23050
23284
  docsUrl: () => "https://platform.openai.com/docs/api-reference/images/create",
23051
- reservedOptions: ["model", "prompt", "n", "size", "image", "image[]"],
23285
+ parameterGuide: (modelId, modality) => modality === "image" ? openaiImagesGuide(modelId) : undefined,
23286
+ requestFields: { image: { shape: ["size"] } },
23287
+ reservedOptions: ["model", "prompt", "image", "image[]"],
23052
23288
  async submit(request, target, http, context) {
23053
23289
  const { url, init } = buildRequest(request, target);
23054
23290
  const response = await sendToProvider(http, target.providerId, url, init, { context });
@@ -23113,6 +23349,182 @@ async function fetchModelIds3(target, http) {
23113
23349
  return { ok: true, modelIds: parsed.data.data.map((model) => model.id).sort() };
23114
23350
  }
23115
23351
 
23352
+ // src/protocols/guides/together.ts
23353
+ var checked4 = "2026-11-05";
23354
+ var imageSources = [
23355
+ "https://docs.together.ai/docs/inference/images/parameters",
23356
+ "https://docs.together.ai/docs/inference/images/reference-images",
23357
+ "https://docs.together.ai/docs/serverless/models"
23358
+ ];
23359
+ var videoSources = [
23360
+ "https://docs.together.ai/docs/inference/videos/parameters",
23361
+ "https://docs.together.ai/docs/inference/videos/reference-and-keyframes",
23362
+ "https://docs.together.ai/docs/serverless/models#video-models"
23363
+ ];
23364
+ var pixels = (range, price) => [
23365
+ { name: "width", values: `${range}, a multiple of 8.`, default: "1024", price },
23366
+ { name: "height", values: `${range}, a multiple of 8.`, default: "1024", price }
23367
+ ];
23368
+ var common = [
23369
+ { name: "n", values: "1–4 images per request; litevision sends `n` here.", default: "1", price: "each image is billed." },
23370
+ { name: "seed", values: "any integer.", default: "random", price: "no effect." },
23371
+ { name: "output_format", values: "jpeg, png.", default: "jpeg", price: "no effect." }
23372
+ ];
23373
+ var imageShape = [{ width: "width", height: "height" }, "aspect_ratio"];
23374
+ var flux22 = {
23375
+ family: "FLUX.2 [pro], [flex], [dev]",
23376
+ parameters: [
23377
+ ...pixels("256–1920", "the [pro] and [flex] estimate is $0.03+ per image, [dev] $0.0154+; larger images cost more."),
23378
+ {
23379
+ name: "reference_images",
23380
+ values: "an array of image links; litevision fills it from `references`. Together gives no count; BFL says up to 10.",
23381
+ default: "none",
23382
+ price: "not documented."
23383
+ },
23384
+ { name: "prompt_upsampling", values: "true, false.", default: "true", price: "no effect." },
23385
+ { name: "steps", values: "[dev] and [flex] only.", default: "the Provider picks", price: "not documented." },
23386
+ { name: "guidance", values: "[dev] and [flex] only.", default: "the Provider picks", price: "not documented." },
23387
+ ...common
23388
+ ],
23389
+ sources: [...imageSources, "https://docs.together.ai/docs/quickstart-flux", "https://docs.bfl.ml/flux_2/flux2_overview"],
23390
+ checked: checked4,
23391
+ notes: [
23392
+ "Default size: the quickstart says 1024x768, the API reference 1024x1024.",
23393
+ "Most output: Together's quickstart says up to 9 MP for [pro], BFL says up to 4 MP."
23394
+ ],
23395
+ shapeFields: imageShape
23396
+ };
23397
+ var flux2Max = {
23398
+ family: "FLUX.2 [max]",
23399
+ parameters: [...pixels("assumed 256–1920 as for the other FLUX.2 models", "$0.07 per megapixel at 50 steps: doubling the pixels doubles the price."), ...common],
23400
+ sources: [...imageSources, "https://docs.together.ai/docs/quickstart-flux"],
23401
+ checked: checked4,
23402
+ notes: ["Together documents neither its size range nor reference images."],
23403
+ shapeFields: imageShape
23404
+ };
23405
+ var kontext = {
23406
+ family: "FLUX.1 Kontext [pro], [max]",
23407
+ parameters: [
23408
+ {
23409
+ name: "aspect_ratio",
23410
+ values: 'a ratio such as "1:1", "16:9", "9:16", "4:3", "3:2" (examples Together gives).',
23411
+ default: "the Provider picks",
23412
+ price: "[pro] $0.04 and [max] $0.08 per megapixel."
23413
+ },
23414
+ ...pixels("multiples of 8, in place of aspect_ratio", "billed per megapixel."),
23415
+ { name: "image_url", values: "one image link; litevision fills it from the first of `references`.", default: "none", price: "not documented." },
23416
+ { name: "steps", values: "an integer; more than the default costs proportionally more.", default: "28", price: "price × steps ÷ 28 above the default." },
23417
+ ...common
23418
+ ],
23419
+ sources: [...imageSources, "https://docs.together.ai/docs/quickstart-flux"],
23420
+ checked: checked4,
23421
+ notes: ["`aspect_ratio` is in Together's docs but not in its OpenAPI schema."],
23422
+ shapeFields: imageShape
23423
+ };
23424
+ var flux11 = {
23425
+ family: "FLUX1.1 [pro]",
23426
+ parameters: [...pixels("multiples of 8", "$0.04 per megapixel: doubling the pixels doubles the price."), ...common],
23427
+ sources: imageSources,
23428
+ checked: checked4,
23429
+ notes: ["Takes no reference images."],
23430
+ shapeFields: imageShape
23431
+ };
23432
+ var gemini3Pro = {
23433
+ family: "Gemini 3 Pro Image on Together",
23434
+ parameters: [
23435
+ {
23436
+ name: "width",
23437
+ values: "with height, one of Together's sizes: 1K 1024x1024, 1264x848, 848x1264, 1200x896, 896x1200, 928x1152, 1152x928, 768x1376, 1376x768, 1584x672; 2K 2048x2048, 2528x1696, 2752x1536 and the like; 4K 4096x4096, 5504x3072 and the like.",
23438
+ default: "1024",
23439
+ price: "$0.134 at 1K or 2K (the same), about $0.24 at 4K."
23440
+ },
23441
+ { name: "height", values: "with width, as above.", default: "1024", price: "as width." },
23442
+ { name: "reference_images", values: "an array of image links; litevision fills it from `references`. Count not documented by Together (Google: up to 14).", default: "none", price: "not documented." },
23443
+ ...common
23444
+ ],
23445
+ sources: imageSources,
23446
+ checked: checked4,
23447
+ notes: ['Together gives some sizes as alternatives ("1548x672 or 1584x672", "5096x3392 or 5056x3392"); Google says 1584x672 and 5056x3392.'],
23448
+ shapeFields: imageShape
23449
+ };
23450
+ var flash252 = {
23451
+ family: "Gemini 2.5 Flash Image on Together",
23452
+ parameters: [
23453
+ ...pixels("not documented by Together", "estimated at $0.039+ per image."),
23454
+ { name: "reference_images", values: "an array of image links; litevision fills it from `references`. Count not documented.", default: "none", price: "not documented." },
23455
+ ...common
23456
+ ],
23457
+ sources: imageSources,
23458
+ checked: checked4,
23459
+ notes: ["Together documents no sizes for this model; Google's own sizes are about a megapixel at each ratio."],
23460
+ shapeFields: imageShape
23461
+ };
23462
+ var imageEntries = [
23463
+ { matches: /^black-forest-labs\/FLUX\.2-max$/i, guide: flux2Max },
23464
+ { matches: /^black-forest-labs\/FLUX\.2-(pro|flex|dev)$/i, guide: flux22 },
23465
+ { matches: /^black-forest-labs\/FLUX\.1-kontext-(pro|max)$/i, guide: kontext },
23466
+ { matches: /^black-forest-labs\/FLUX\.1\.1-pro$/i, guide: flux11 },
23467
+ { matches: /^google\/gemini-3-pro-image$/i, guide: gemini3Pro },
23468
+ { matches: /^google\/flash-image-2\.5$/i, guide: flash252 }
23469
+ ];
23470
+ var videoCommon = [
23471
+ { name: "seed", values: "0–2,147,483,647.", default: "random", price: "no effect." },
23472
+ { name: "negative_prompt", values: "text, at most 500 characters.", default: "none", price: "no effect." },
23473
+ {
23474
+ name: "media.frame_images",
23475
+ values: 'litevision fills it from `first_frame` and `last_frame` as { input_image, frame: "first" | "last" }.',
23476
+ default: "none",
23477
+ price: "not documented."
23478
+ }
23479
+ ];
23480
+ var wan = (variant, seconds, media) => ({
23481
+ family: `Wan 2.7 ${variant}`,
23482
+ parameters: [
23483
+ {
23484
+ name: "resolution",
23485
+ values: "720P, 1080P (16:9 is 1280x720 or 1920x1080; 4:3 is 1104x832 or 1648x1248).",
23486
+ default: "1080P",
23487
+ price: "$0.10 per video in Together's catalog, which gives no resolution or length."
23488
+ },
23489
+ { name: "ratio", values: "16:9, 9:16, 1:1, 4:3, 3:4.", default: "16:9", price: "no effect." },
23490
+ { name: "seconds", values: `${seconds}, as a string.`, default: '"5"', price: "not documented." },
23491
+ ...videoCommon
23492
+ ],
23493
+ sources: [...videoSources, "https://docs.together.ai/docs/wan2.7-quickstart"],
23494
+ checked: checked4,
23495
+ notes: [media, "30 fps MP4."],
23496
+ shapeFields: ["ratio"],
23497
+ durationField: "seconds"
23498
+ });
23499
+ var seedance25 = {
23500
+ family: "Seedance 2.5",
23501
+ parameters: [
23502
+ { name: "resolution", values: "480p, 720p.", default: "720p", price: "480p $0.115 per second; 720p from $0.249 per second." },
23503
+ { name: "seconds", values: "4–30 whole seconds, as a string.", default: '"5"', price: "billed per second of output." },
23504
+ { name: "media.reference_images", values: "up to 30 image links; litevision fills it from `references`.", default: "none", price: "not documented." },
23505
+ ...videoCommon
23506
+ ],
23507
+ sources: [...videoSources, "https://docs.together.ai/docs/seedance2.5-quickstart"],
23508
+ checked: checked4,
23509
+ notes: [
23510
+ "Takes no ratio, width or height: the shape follows the first frame, else the model picks. Audio is always on.",
23511
+ "Frames and references cannot be combined.",
23512
+ 'The catalog lists $0.115 under "Price per video"; the quickstart says per second, which is what is given here.'
23513
+ ],
23514
+ shapeFields: [],
23515
+ durationField: "seconds"
23516
+ };
23517
+ var videoEntries = [
23518
+ { matches: /^Wan-AI\/wan2\.7-t2v$/i, guide: wan("text-to-video", "2–15", "Takes no frames or reference images.") },
23519
+ { matches: /^Wan-AI\/wan2\.7-i2v$/i, guide: wan("image-to-video", "2–15", "Takes a first and/or last frame.") },
23520
+ {
23521
+ matches: /^Wan-AI\/wan2\.7-r2v$/i,
23522
+ guide: wan("reference-to-video", "2–10", "Takes several reference images in media.reference_images; Together gives no count.")
23523
+ },
23524
+ { matches: /^ByteDance\/Seedance-2\.5$/i, guide: seedance25 }
23525
+ ];
23526
+ var togetherGuide = (modelId, modality) => guideFor(modality === "video" ? videoEntries : imageEntries, modelId);
23527
+
23116
23528
  // src/protocols/together.ts
23117
23529
  var docs = {
23118
23530
  flux: "https://docs.together.ai/docs/quickstart-flux",
@@ -23122,185 +23534,18 @@ var docs = {
23122
23534
  seedance: "https://docs.together.ai/docs/seedance2.5-quickstart",
23123
23535
  videoParameters: "https://docs.together.ai/docs/inference/videos/parameters"
23124
23536
  };
23125
- var sizes3 = (entries) => entries.map(([aspectRatio, width, height]) => ({ aspectRatio, width, height }));
23126
- var megapixelSizes = sizes3([
23127
- ["1:1", 1024, 1024],
23128
- ["16:9", 1344, 768],
23129
- ["9:16", 768, 1344],
23130
- ["4:3", 1152, 896],
23131
- ["3:4", 896, 1152],
23132
- ["3:2", 1216, 832],
23133
- ["2:3", 832, 1216]
23134
- ]);
23135
- var cappedSizes = sizes3([
23136
- ["1:1", 1024, 1024],
23137
- ["16:9", 1024, 576],
23138
- ["9:16", 576, 1024],
23139
- ["4:3", 1024, 768],
23140
- ["3:4", 768, 1024],
23141
- ["3:2", 1024, 680],
23142
- ["2:3", 680, 1024]
23143
- ]);
23144
- var maxImagesPerJob2 = 4;
23145
23537
  var imageFamilies = [
23146
- {
23147
- matches: /^black-forest-labs\/FLUX\.1-kontext-/i,
23148
- capabilities: { sizes: megapixelSizes, maxReferences: 1, maxImages: maxImagesPerJob2 },
23149
- sizing: "ratio",
23150
- references: "image_url",
23151
- docsUrl: docs.flux
23152
- },
23153
- {
23154
- matches: /^black-forest-labs\/FLUX\.2-(pro|flex|dev)$/i,
23155
- capabilities: { sizes: megapixelSizes, maxReferences: 4, maxImages: maxImagesPerJob2 },
23156
- sizing: "pixels",
23157
- references: "reference_images",
23158
- docsUrl: docs.flux
23159
- },
23160
- {
23161
- matches: /^black-forest-labs\//i,
23162
- capabilities: { sizes: megapixelSizes, maxReferences: 0, maxImages: maxImagesPerJob2 },
23163
- sizing: "pixels",
23164
- references: "reference_images",
23165
- docsUrl: docs.flux
23166
- },
23167
- {
23168
- matches: /^google\/gemini-3-pro-image$/i,
23169
- capabilities: {
23170
- sizes: sizes3([
23171
- ["1:1", 1024, 1024],
23172
- ["16:9", 1376, 768],
23173
- ["9:16", 768, 1376],
23174
- ["4:3", 1200, 896],
23175
- ["3:4", 896, 1200],
23176
- ["3:2", 1264, 848],
23177
- ["2:3", 848, 1264]
23178
- ]),
23179
- maxReferences: 14,
23180
- maxImages: maxImagesPerJob2
23181
- },
23182
- sizing: "pixels",
23183
- references: "reference_images",
23184
- docsUrl: docs.referenceImages
23185
- },
23186
- {
23187
- matches: /^google\/flash-image-2\.5$/i,
23188
- capabilities: {
23189
- sizes: sizes3([
23190
- ["1:1", 1024, 1024],
23191
- ["16:9", 1344, 768],
23192
- ["9:16", 768, 1344],
23193
- ["4:3", 1184, 864],
23194
- ["3:4", 864, 1184],
23195
- ["3:2", 1248, 832],
23196
- ["2:3", 832, 1248]
23197
- ]),
23198
- maxReferences: 3,
23199
- maxImages: maxImagesPerJob2
23200
- },
23201
- sizing: "pixels",
23202
- references: "reference_images",
23203
- docsUrl: docs.referenceImages
23204
- }
23538
+ { matches: /^black-forest-labs\/FLUX\.1-kontext-/i, references: "image_url", docsUrl: docs.flux },
23539
+ { matches: /^black-forest-labs\//i, references: "reference_images", docsUrl: docs.flux },
23540
+ { matches: /^google\/(gemini-3-pro-image|flash-image-2\.5)$/i, references: "reference_images", docsUrl: docs.referenceImages }
23205
23541
  ];
23206
- var otherImages = {
23207
- matches: /.*/,
23208
- capabilities: { sizes: cappedSizes, maxReferences: 0, maxImages: maxImagesPerJob2 },
23209
- sizing: "pixels",
23210
- references: "reference_images",
23211
- docsUrl: docs.imageParameters
23212
- };
23542
+ var otherImages = { matches: /.*/, references: "reference_images", docsUrl: docs.imageParameters };
23213
23543
  var imageFamily = (modelId) => imageFamilies.find((family) => family.matches.test(modelId)) ?? otherImages;
23214
- var range = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => from + index);
23215
- var wanResolutions = [
23216
- {
23217
- name: "1080P",
23218
- sizes: sizes3([
23219
- ["16:9", 1920, 1080],
23220
- ["9:16", 1080, 1920],
23221
- ["1:1", 1440, 1440],
23222
- ["4:3", 1632, 1248],
23223
- ["3:4", 1248, 1632]
23224
- ])
23225
- },
23226
- {
23227
- name: "720P",
23228
- sizes: sizes3([
23229
- ["16:9", 1280, 720],
23230
- ["9:16", 720, 1280],
23231
- ["1:1", 960, 960],
23232
- ["4:3", 1088, 832],
23233
- ["3:4", 832, 1088]
23234
- ])
23235
- }
23236
- ];
23237
- var wan = (capabilities) => ({
23238
- resolutions: wanResolutions,
23239
- defaultDuration: 5,
23240
- ...capabilities
23241
- });
23242
23544
  var videoFamilies = [
23243
- {
23244
- matches: /^Wan-AI\/wan2\.7-t2v$/i,
23245
- capabilities: wan({ durations: range(2, 15), firstFrame: false, lastFrame: false, maxReferences: 0 }),
23246
- sizing: "tier+ratio",
23247
- docsUrl: docs.wan
23248
- },
23249
- {
23250
- matches: /^Wan-AI\/wan2\.7-i2v$/i,
23251
- capabilities: wan({ durations: range(2, 15), firstFrame: true, lastFrame: true, maxReferences: 0 }),
23252
- sizing: "tier+ratio",
23253
- docsUrl: docs.wan
23254
- },
23255
- {
23256
- matches: /^Wan-AI\/wan2\.7-r2v$/i,
23257
- capabilities: wan({ durations: range(2, 10), firstFrame: false, lastFrame: false, maxReferences: 5 }),
23258
- sizing: "tier+ratio",
23259
- docsUrl: docs.wan
23260
- },
23261
- {
23262
- matches: /^ByteDance\/Seedance-2\.5$/i,
23263
- capabilities: {
23264
- resolutions: [
23265
- { name: "720p", sizes: sizes3([["16:9", 1280, 720]]) },
23266
- { name: "480p", sizes: sizes3([["16:9", 854, 480]]) }
23267
- ],
23268
- durations: range(4, 30),
23269
- defaultDuration: 5,
23270
- firstFrame: true,
23271
- lastFrame: true,
23272
- maxReferences: 30
23273
- },
23274
- sizing: "tier",
23275
- docsUrl: docs.seedance
23276
- }
23545
+ { matches: /^Wan-AI\/wan2\.7-/i, docsUrl: docs.wan },
23546
+ { matches: /^ByteDance\/Seedance-2\.5$/i, docsUrl: docs.seedance }
23277
23547
  ];
23278
- var otherVideos = {
23279
- matches: /.*/,
23280
- capabilities: {
23281
- resolutions: [
23282
- {
23283
- sizes: sizes3([
23284
- ["16:9", 1280, 720],
23285
- ["9:16", 720, 1280],
23286
- ["1:1", 960, 960],
23287
- ["4:3", 1088, 816],
23288
- ["3:4", 816, 1088],
23289
- ["3:2", 1152, 768],
23290
- ["2:3", 768, 1152]
23291
- ])
23292
- }
23293
- ],
23294
- durations: range(1, 10),
23295
- defaultDuration: 5,
23296
- firstFrame: true,
23297
- lastFrame: true,
23298
- maxReferences: 4
23299
- },
23300
- sizing: "pixels",
23301
- docsUrl: docs.videoParameters
23302
- };
23303
- var videoFamily = (modelId) => videoFamilies.find((family) => family.matches.test(modelId)) ?? otherVideos;
23548
+ var videoDocsUrl = (modelId) => videoFamilies.find((family) => family.matches.test(modelId))?.docsUrl ?? docs.videoParameters;
23304
23549
  var errorSchema4 = object({
23305
23550
  error: object({
23306
23551
  message: string2().optional(),
@@ -23422,19 +23667,21 @@ async function fetchModelIds4(target, http) {
23422
23667
  return { ok: true, modelIds: Object.keys(modalities).sort(), modalities };
23423
23668
  }
23424
23669
  var together = {
23425
- builtInCapabilities: (modelId) => imageFamily(modelId).capabilities,
23426
- builtInVideoCapabilities: (modelId) => videoFamily(modelId).capabilities,
23427
- docsUrl: (modelId, modality = "image") => (modality === "video" ? videoFamily(modelId) : imageFamily(modelId)).docsUrl,
23428
- reservedOptions: ["model", "prompt", "n", "width", "height", "aspect_ratio", "response_format", "reference_images", "image_url"],
23429
- reservedVideoOptions: ["model", "prompt", "seconds", "resolution", "ratio", "media"],
23670
+ parameterGuide: (modelId, modality) => togetherGuide(modelId, modality),
23671
+ docsUrl: (modelId, modality = "image") => modality === "video" ? videoDocsUrl(modelId) : imageFamily(modelId).docsUrl,
23672
+ requestFields: {
23673
+ image: { shape: [{ width: "width", height: "height" }, "aspect_ratio"] },
23674
+ video: { shape: [{ width: "width", height: "height" }, "ratio"], duration: "seconds", resolution: "resolution" }
23675
+ },
23676
+ reservedOptions: ["model", "prompt", "response_format", "reference_images", "image_url"],
23677
+ reservedVideoOptions: ["model", "prompt", "media"],
23430
23678
  async submit(request, target, http, context) {
23431
23679
  const family = imageFamily(target.modelId);
23432
- const { size, references } = request;
23680
+ const { references } = request;
23433
23681
  const body = {
23434
23682
  model: target.modelId,
23435
23683
  prompt: request.prompt,
23436
23684
  n: request.n,
23437
- ...family.sizing === "ratio" ? { aspect_ratio: size.aspectRatio } : { width: size.width, height: size.height },
23438
23685
  response_format: "base64",
23439
23686
  ...references.length === 0 ? {} : family.references === "image_url" ? { image_url: dataUrl(references[0]) } : { reference_images: references.map(dataUrl) },
23440
23687
  ...request.providerOptions
@@ -23455,8 +23702,6 @@ var together = {
23455
23702
  return { wait: async () => ({ images, ...shortfall === undefined ? {} : { shortfall } }) };
23456
23703
  },
23457
23704
  async submitVideo(request, target, http, context) {
23458
- const family = videoFamily(target.modelId);
23459
- const { size } = request;
23460
23705
  const frames = [
23461
23706
  ...request.firstFrame === undefined ? [] : [{ input_image: dataUrl(request.firstFrame), frame: "first" }],
23462
23707
  ...request.lastFrame === undefined ? [] : [{ input_image: dataUrl(request.lastFrame), frame: "last" }]
@@ -23465,12 +23710,9 @@ var together = {
23465
23710
  ...frames.length === 0 ? {} : { frame_images: frames },
23466
23711
  ...request.references.length === 0 ? {} : { reference_images: request.references.map(dataUrl) }
23467
23712
  };
23468
- const sizing = family.sizing === "pixels" ? { width: size.width, height: size.height } : { resolution: request.resolution, ...family.sizing === "tier+ratio" ? { ratio: size.aspectRatio } : {} };
23469
23713
  const body = {
23470
23714
  model: target.modelId,
23471
23715
  prompt: request.prompt,
23472
- seconds: String(request.seconds),
23473
- ...sizing,
23474
23716
  ...Object.keys(media).length === 0 ? {} : { media },
23475
23717
  ...request.providerOptions
23476
23718
  };
@@ -23500,25 +23742,142 @@ var together = {
23500
23742
  listModelIds: fetchModelIds4
23501
23743
  };
23502
23744
 
23745
+ // src/protocols/guides/venice.ts
23746
+ var checked5 = "2026-11-05";
23747
+ var generateSources = [
23748
+ "https://docs.venice.ai/api-reference/endpoint/image/generate",
23749
+ "https://docs.venice.ai/models/image",
23750
+ "https://api.venice.ai/api/v1/models?type=image"
23751
+ ];
23752
+ var editSources = [
23753
+ "https://docs.venice.ai/api-reference/endpoint/image/edit",
23754
+ "https://docs.venice.ai/api-reference/endpoint/image/multi-edit",
23755
+ "https://api.venice.ai/api/v1/models?type=inpaint"
23756
+ ];
23757
+ var generateCommon = [
23758
+ { name: "variants", values: "1–4 images per request; litevision sends `n` here.", default: "1", price: "each image is billed." },
23759
+ { name: "format", values: "jpeg, png, webp.", default: "png (litevision's choice; Venice's own is webp)", price: "no effect." },
23760
+ { name: "negative_prompt", values: "text.", default: "none", price: "no effect." },
23761
+ { name: "seed", values: "−999,999,999 to 999,999,999.", default: "random", price: "no effect." },
23762
+ { name: "safe_mode", values: "true, false.", default: "false (litevision's choice; Venice's own is true)", price: "no effect." },
23763
+ { name: "hide_watermark", values: "true, false.", default: "true (litevision's choice)", price: "no effect." }
23764
+ ];
23765
+ var ratioShape = [{ width: "width", height: "height" }, "aspect_ratio"];
23766
+ var pixelModels = {
23767
+ family: "Venice pixel-sized models (venice-sd35, wai-Illustrious, lustify, z-image-turbo, chroma)",
23768
+ parameters: [
23769
+ {
23770
+ name: "width",
23771
+ values: "1–1280, divisible by the model's widthHeightDivisor (16 for venice-sd35 and wai-Illustrious, 8 for the rest).",
23772
+ default: "1024",
23773
+ price: "$0.01 per image whatever the size."
23774
+ },
23775
+ { name: "height", values: "as width.", default: "1024", price: "as width." },
23776
+ { name: "cfg_scale", values: "0–20.", default: "the Provider picks", price: "no effect." },
23777
+ { name: "steps", values: "per model (venice-sd35: up to 30).", default: "per model (venice-sd35: 25)", price: "no effect." },
23778
+ ...generateCommon
23779
+ ],
23780
+ sources: generateSources,
23781
+ checked: checked5,
23782
+ shapeFields: ratioShape
23783
+ };
23784
+ var tierModels = {
23785
+ family: "Venice models sized by aspect ratio and resolution tier",
23786
+ parameters: [
23787
+ {
23788
+ name: "aspect_ratio",
23789
+ values: "per model, most of 1:1, 3:2, 16:9, 21:9, 9:16, 2:3, 3:4, 4:5 (grok-imagine-* lack 21:9 and 4:5 in part; 4:3 only on seedream-v5-*).",
23790
+ default: "1:1",
23791
+ price: "no effect."
23792
+ },
23793
+ {
23794
+ name: "resolution",
23795
+ values: "1K, 2K, and on some models 4K; per model.",
23796
+ default: "1K",
23797
+ price: "per model at 1K / 2K / 4K: gpt-image-2-5-flare and -sunburst $0.07 / $0.15 / $0.23; gpt-image-2 $0.27 / $0.51 / $0.84; nano-banana-2 and -2-1 $0.10 / $0.14 / $0.19; nano-banana-pro $0.18 / $0.23 / $0.35; flux-3-image $0.12 / $0.29 / $0.98; krea-2-turbo $0.04 / $0.06; grok-imagine-image $0.03 / $0.04; seedream-v5-pro $0.06 / $0.11; qwen-image-3-pro $0.05 / $0.09; the same at 1K and 2K on ideogram-v4-5 ($0.26), qwen-image-3 ($0.04), qwen-image-2-1-turbo ($0.02) and qwen-image-2-1-pro ($0.05)."
23798
+ },
23799
+ {
23800
+ name: "quality",
23801
+ values: "low, medium, high on models that list qualities (gpt-image-2*, ideogram-v4-5; grok-imagine-image-2-0 low, medium).",
23802
+ default: "high",
23803
+ price: "gpt-image-2 at 1K: $0.02 low, $0.07 medium, $0.26 high; gpt-image-2.5 at 1K: $0.03, $0.04, $0.07."
23804
+ },
23805
+ ...generateCommon
23806
+ ],
23807
+ sources: generateSources,
23808
+ checked: checked5,
23809
+ notes: ["Venice publishes no pixel sizes for these models.", "gpt-image-2's headline prices are $0.01 above its per-quality high prices; both are in Venice's model list."],
23810
+ shapeFields: ratioShape
23811
+ };
23812
+ var ratioModels = {
23813
+ family: "Venice models sized by aspect ratio",
23814
+ parameters: [
23815
+ {
23816
+ name: "aspect_ratio",
23817
+ values: "per model, most of 1:1, 3:2, 16:9, 21:9, 9:16, 2:3, 3:4, 4:5 (gpt-image-1-5: 1:1, 3:2, 2:3; flux-2-max also auto).",
23818
+ default: "1:1 (flux-2-max: auto)",
23819
+ price: "no effect; per image: flux-2-pro $0.03, flux-2-max $0.09, gpt-image-1-5 $0.26, hunyuan-image-v3 $0.09, ideogram-v4 $0.06, krea-v2-large $0.07, krea-v2-medium $0.04, luma-uni-1 $0.05, luma-uni-1-max $0.12, muse-image $0.02, nano-banana-2-lite $0.06, recraft-v4 $0.05, recraft-v4-pro $0.29, seedream-v4 $0.05, seedream-v5-lite $0.05, qwen-image $0.03, qwen-image-2 $0.05, qwen-image-2-pro $0.10, wan-2-7-text-to-image $0.0375, wan-2-7-pro-text-to-image $0.094."
23820
+ },
23821
+ ...generateCommon
23822
+ ],
23823
+ sources: generateSources,
23824
+ checked: checked5,
23825
+ notes: ["The Qwen Image models reject width and height with a 400.", "krea-v2-* and luma-uni-1* take up to 3 style_references."],
23826
+ shapeFields: ratioShape
23827
+ };
23828
+ var editModels = {
23829
+ family: "Venice edit models (`-edit`)",
23830
+ parameters: [
23831
+ {
23832
+ name: "aspect_ratio",
23833
+ values: "auto, 1:1, 3:2, 16:9, 21:9, 9:16, 2:3, 3:4, 4:3, 4:5; a subset per model.",
23834
+ default: "auto",
23835
+ price: "no effect."
23836
+ },
23837
+ {
23838
+ name: "resolution",
23839
+ values: "1K, 2K, and on some models 4K; per model.",
23840
+ default: "1K",
23841
+ price: "per model, e.g. nano-banana-pro-edit $0.18 / $0.23 / $0.35, gpt-image-2-5-flare-edit $0.08 / $0.15 / $0.24."
23842
+ },
23843
+ {
23844
+ name: "quality",
23845
+ values: "low, medium, high; only on /image/multi-edit (several references), where the model lists qualities. /image/edit rejects it.",
23846
+ default: "the model's default tier",
23847
+ price: "per model."
23848
+ },
23849
+ { name: "output_format", values: "jpeg, png, webp.", default: "png (litevision's choice)", price: "no effect." }
23850
+ ],
23851
+ sources: editSources,
23852
+ checked: checked5,
23853
+ notes: [
23854
+ "Reference images per Job: the model list's maxInputImages, else 3, or 1 when combineImages is false. The multi-edit docs name capabilities.maxInputImages, the model list puts it in constraints.",
23855
+ "One reference image goes to /image/edit, several to /image/multi-edit; each image of a Job is its own request."
23856
+ ],
23857
+ shapeFields: ["aspect_ratio"]
23858
+ };
23859
+ var entries4 = [
23860
+ { matches: /(^|-)edit(-|$)/, guide: editModels },
23861
+ { matches: /^(venice-sd35|wai-Illustrious|lustify-(sdxl|v7|v8)|z-image-turbo|chroma)$/, guide: pixelModels },
23862
+ {
23863
+ matches: /^(gpt-image-2(-5-(flare|sunburst))?|nano-banana-(2|2-1|pro)|flux-3-image|krea-2-turbo|grok-imagine-image(-2-0)?|ideogram-v4-5|seedream-v5-pro|qwen-image-3(-pro)?|qwen-image-2-1-(turbo|pro))$/,
23864
+ guide: tierModels
23865
+ },
23866
+ {
23867
+ matches: /^(flux-2-(pro|max)|gpt-image-1-5|hunyuan-image-v3|ideogram-v4|krea-v2-(large|medium)|luma-uni-1(-max)?|muse-image|nano-banana-2-lite|recraft-v4(-pro)?|seedream-v4|seedream-v5-lite|qwen-image(-2(-pro)?)?|wan-2-7(-pro)?-text-to-image)$/,
23868
+ guide: ratioModels
23869
+ }
23870
+ ];
23871
+ var veniceGuide = (modelId) => guideFor(entries4, modelId);
23872
+
23503
23873
  // src/protocols/venice.ts
23504
23874
  var generateDocs = "https://docs.venice.ai/api-reference/endpoint/image/generate";
23505
23875
  var editDocs = "https://docs.venice.ai/api-reference/endpoint/image/edit";
23506
- var maxImages = 4;
23507
- var constraintsSchema = object({
23508
- aspectRatios: array(string2()).optional(),
23509
- widthHeightDivisor: number2().positive().optional(),
23510
- combineImages: boolean2().optional(),
23511
- maxInputImages: number2().int().positive().optional()
23512
- });
23513
- var sizingOf = (constraints) => (constraints.widthHeightDivisor ?? 1) > 1 ? "pixels" : "ratio";
23514
23876
  var modelListSchema5 = object({
23515
23877
  data: array(object({
23516
23878
  id: string2(),
23517
23879
  type: string2().optional(),
23518
- model_spec: object({
23519
- constraints: record(string2(), unknown()).optional(),
23520
- capabilities: object({ maxInputImages: number2().int().positive().optional() }).passthrough().optional()
23521
- }).passthrough().optional()
23880
+ model_spec: object({ constraints: record(string2(), unknown()).optional() }).passthrough().optional()
23522
23881
  }))
23523
23882
  });
23524
23883
  var errorSchema5 = object({
@@ -23531,69 +23890,9 @@ var generateSchema = object({
23531
23890
  images: array(string2()),
23532
23891
  content_policy_violation_count: number2().int().nonnegative().optional()
23533
23892
  });
23534
- function sizeFor(aspectRatio, divisor, maxSide = Infinity) {
23535
- const [w, h] = aspectRatio.split(":").map(Number);
23536
- const area = 1024 * 1024;
23537
- let width = Math.sqrt(area * w / h);
23538
- let height = Math.sqrt(area * h / w);
23539
- const scale = Math.min(1, maxSide / width, maxSide / height);
23540
- width *= scale;
23541
- height *= scale;
23542
- const snap = (value) => Math.max(divisor, Math.round(value / divisor) * divisor);
23543
- return { aspectRatio, width: snap(width), height: snap(height) };
23544
- }
23545
- function pixelSizes(divisor) {
23546
- return aspectRatios.map((ratio) => sizeFor(ratio, divisor, 1280));
23547
- }
23548
- function ratioSizes(ratios) {
23549
- return aspectRatios.filter((ratio) => ratios.includes(ratio)).map((ratio) => sizeFor(ratio, 16));
23550
- }
23551
23893
  function looksLikeEdit(modelId) {
23552
23894
  return /(^|-)edit(-|$)/.test(modelId);
23553
23895
  }
23554
- var pixelModels = ["venice-sd35", "hidream", "lustify-sdxl", "flux-dev", "flux-dev-uncensored", "pony-realism"];
23555
- function builtInModel(modelId) {
23556
- if (looksLikeEdit(modelId)) {
23557
- return { kind: "edit", sizing: "ratio", capabilities: { sizes: ratioSizes(aspectRatios), maxReferences: 3, maxImages } };
23558
- }
23559
- if (pixelModels.includes(modelId)) {
23560
- return { kind: "generate", sizing: "pixels", capabilities: { sizes: pixelSizes(16), maxReferences: 0, maxImages } };
23561
- }
23562
- return {
23563
- kind: "generate",
23564
- sizing: "ratio",
23565
- capabilities: { sizes: ratioSizes(["1:1", "16:9", "9:16", "3:2", "2:3"]), maxReferences: 0, maxImages }
23566
- };
23567
- }
23568
- function describeModel(listed, listType) {
23569
- const raw = listed.model_spec?.constraints;
23570
- const parsed = constraintsSchema.safeParse(raw ?? {});
23571
- const constraints = parsed.success ? parsed.data : {};
23572
- const nonEmpty = (capabilities) => capabilities.sizes.length === 0 ? undefined : capabilities;
23573
- if ((listed.type ?? listType) === "inpaint") {
23574
- const maxReferences = constraints.maxInputImages ?? listed.model_spec?.capabilities?.maxInputImages ?? (constraints.combineImages === false ? 1 : 3);
23575
- return {
23576
- kind: "edit",
23577
- sizing: "ratio",
23578
- capabilities: nonEmpty({ sizes: ratioSizes(constraints.aspectRatios ?? []), maxReferences, maxImages }),
23579
- constraints: raw
23580
- };
23581
- }
23582
- if (sizingOf(constraints) === "pixels") {
23583
- return {
23584
- kind: "generate",
23585
- sizing: "pixels",
23586
- capabilities: { sizes: pixelSizes(constraints.widthHeightDivisor), maxReferences: 0, maxImages },
23587
- constraints: raw
23588
- };
23589
- }
23590
- return {
23591
- kind: "generate",
23592
- sizing: "ratio",
23593
- capabilities: nonEmpty({ sizes: ratioSizes(constraints.aspectRatios ?? []), maxReferences: 0, maxImages }),
23594
- constraints: raw
23595
- };
23596
- }
23597
23896
  var apiUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}${path}`;
23598
23897
  var authorization2 = (credential) => ({ authorization: `Bearer ${credential}` });
23599
23898
  async function listModels(target, type, http) {
@@ -23616,13 +23915,6 @@ async function listModels(target, type, http) {
23616
23915
  }
23617
23916
  return { ok: true, models: parsed.data.data };
23618
23917
  }
23619
- function describedModel(described, modelId) {
23620
- const kind = described.capabilities.maxReferences > 0 ? "edit" : "generate";
23621
- const constraints = constraintsSchema.safeParse(described.parameterSchema);
23622
- if (described.parameterSchema === undefined || !constraints.success)
23623
- return { kind, sizing: builtInModel(modelId).sizing };
23624
- return { kind, sizing: sizingOf(constraints.data) };
23625
- }
23626
23918
  async function lookUpModel(target, http) {
23627
23919
  const types = looksLikeEdit(target.modelId) ? ["inpaint", "image"] : ["image", "inpaint"];
23628
23920
  for (const type of types) {
@@ -23631,7 +23923,7 @@ async function lookUpModel(target, http) {
23631
23923
  throw new Error(list.message);
23632
23924
  const listed = list.models.find((model) => model.id === target.modelId);
23633
23925
  if (listed !== undefined)
23634
- return describeModel(listed, type);
23926
+ return listed.model_spec?.constraints === undefined ? {} : { constraints: listed.model_spec.constraints };
23635
23927
  }
23636
23928
  return;
23637
23929
  }
@@ -23667,15 +23959,12 @@ async function veniceError(response, providerId, options) {
23667
23959
  });
23668
23960
  }
23669
23961
  var sharedDefaults = { safe_mode: false, enhance_prompt: false };
23670
- async function generate(request, target, model, http, context) {
23671
- const { size } = request;
23672
- const sizing = model.sizing === "pixels" ? { width: size.width, height: size.height } : { aspect_ratio: size.aspectRatio };
23962
+ async function generate(request, target, http, context) {
23673
23963
  const body = {
23674
23964
  model: target.modelId,
23675
23965
  prompt: request.prompt,
23676
23966
  variants: request.n,
23677
23967
  return_binary: false,
23678
- ...sizing,
23679
23968
  ...sharedDefaults,
23680
23969
  hide_watermark: true,
23681
23970
  format: "png",
@@ -23711,7 +24000,6 @@ async function edit(request, target, http, context) {
23711
24000
  const body = {
23712
24001
  ...inputs,
23713
24002
  prompt: request.prompt,
23714
- aspect_ratio: request.size.aspectRatio,
23715
24003
  ...sharedDefaults,
23716
24004
  output_format: "png",
23717
24005
  ...request.providerOptions
@@ -23729,27 +24017,21 @@ async function edit(request, target, http, context) {
23729
24017
  return [{ bytes: new Uint8Array(await response.arrayBuffer()) }];
23730
24018
  }
23731
24019
  var venice = {
23732
- builtInCapabilities: (modelId) => builtInModel(modelId).capabilities,
23733
24020
  async fetchModelInfo(target, http) {
23734
24021
  const model = await lookUpModel(target, http);
23735
24022
  if (model === undefined)
23736
24023
  return;
23737
- return {
23738
- ...model.capabilities === undefined ? {} : { capabilities: model.capabilities },
23739
- ...model.constraints === undefined ? {} : { parameterSchema: model.constraints }
23740
- };
24024
+ return model.constraints === undefined ? {} : { parameterSchema: model.constraints };
23741
24025
  },
23742
24026
  docsUrl: (modelId) => looksLikeEdit(modelId) ? editDocs : generateDocs,
23743
- reservedOptions: ["model", "modelId", "prompt", "variants", "return_binary", "width", "height", "aspect_ratio", "image", "images"],
24027
+ parameterGuide: (modelId, modality) => modality === "image" ? veniceGuide(modelId) : undefined,
24028
+ requestFields: { image: { shape: [{ width: "width", height: "height" }, "aspect_ratio"], resolution: "resolution" } },
24029
+ reservedOptions: ["model", "modelId", "prompt", "return_binary", "image", "images"],
23744
24030
  async submit(request, target, http, context) {
23745
- const model = describedModel(request.model, target.modelId);
23746
24031
  if (request.references.length > 0) {
23747
24032
  return { wait: () => inParallel(request.n, target.providerId, () => edit(request, target, http, context)) };
23748
24033
  }
23749
- if (model.kind === "edit") {
23750
- throw providerError("unsupported", target.providerId, `"${target.modelId}" is an edit model and needs at least one reference image. Pass \`references\`, or choose a text-to-image Model.`);
23751
- }
23752
- const images = await generate(request, target, model, http, context);
24034
+ const images = await generate(request, target, http, context);
23753
24035
  return { wait: async () => images };
23754
24036
  },
23755
24037
  async testConnection(target, http) {
@@ -23771,7 +24053,7 @@ var venice = {
23771
24053
  // src/protocols/index.ts
23772
24054
  var adapters = {
23773
24055
  "openai-images": openaiImages,
23774
- gemini,
24056
+ gemini: gemini2,
23775
24057
  apimart,
23776
24058
  venice,
23777
24059
  together
@@ -23830,8 +24112,7 @@ class JobRunner {
23830
24112
  const { request } = spec.normalised;
23831
24113
  const base = this.baseFor(spec, "image", request.prompt, {
23832
24114
  n: request.n,
23833
- aspectRatio: spec.normalised.requestedAspectRatio,
23834
- size: request.size,
24115
+ requested: spec.normalised.requested,
23835
24116
  references: request.references.map((reference) => reference.path),
23836
24117
  ...spec.outputDir === undefined ? {} : { outputPath: spec.outputDir },
23837
24118
  providerOptions: request.providerOptions
@@ -23854,12 +24135,8 @@ class JobRunner {
23854
24135
  }
23855
24136
  const base = this.baseFor(spec, "video", request.prompt, {
23856
24137
  n: 1,
23857
- aspectRatio: spec.normalised.requestedAspectRatio,
23858
- size: request.size,
24138
+ requested: spec.normalised.requested,
23859
24139
  references: request.references.map((reference) => reference.path),
23860
- seconds: request.seconds,
23861
- ...request.resolution === undefined ? {} : { resolution: request.resolution },
23862
- ...spec.normalised.followsFirstFrame ? { followsFirstFrame: true } : {},
23863
24140
  ...request.firstFrame === undefined ? {} : { firstFrame: request.firstFrame.path },
23864
24141
  ...request.lastFrame === undefined ? {} : { lastFrame: request.lastFrame.path },
23865
24142
  ...spec.outputDir === undefined ? {} : { outputPath: spec.outputDir },
@@ -23904,7 +24181,8 @@ class JobRunner {
23904
24181
  files: record.files,
23905
24182
  observed: record.observed ?? record.files.map(() => null),
23906
24183
  ...record.shortfall === undefined ? {} : { shortfall: record.shortfall },
23907
- ...record.mismatch === undefined ? {} : { mismatch: record.mismatch }
24184
+ ...record.mismatch === undefined ? {} : { mismatch: record.mismatch },
24185
+ ...record.parameters.requested === undefined ? {} : { requested: record.parameters.requested }
23908
24186
  };
23909
24187
  }
23910
24188
  if (record.outcome === "failed") {
@@ -24009,7 +24287,8 @@ class JobRunner {
24009
24287
  files: saved.files,
24010
24288
  observed,
24011
24289
  ...shortfall === undefined ? {} : { shortfall },
24012
- ...mismatch === undefined ? {} : { mismatch }
24290
+ ...mismatch === undefined ? {} : { mismatch },
24291
+ ...base.parameters.requested === undefined ? {} : { requested: base.parameters.requested }
24013
24292
  };
24014
24293
  } catch (error) {
24015
24294
  if (error instanceof JobAbandoned || this.closed)
@@ -24046,20 +24325,27 @@ function videoAsImages(result) {
24046
24325
  var aspectTolerance = Math.log(1.05);
24047
24326
  var durationToleranceSeconds = 0.5;
24048
24327
  function expectedShape(parameters) {
24328
+ if (parameters.requested !== undefined) {
24329
+ const parsed = parameters.requested.shape === null ? undefined : parseShape(parameters.requested.shape);
24330
+ return parsed === undefined ? undefined : { ratio: parsed.ratio, asked: parsed.label };
24331
+ }
24049
24332
  const { width, height } = parameters.providerOptions;
24050
24333
  if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
24051
24334
  return { ratio: width / height, asked: `${width}x${height} in provider_options` };
24052
24335
  }
24053
- return { ratio: ratioOf(parameters.size.aspectRatio), asked: `${parameters.size.aspectRatio} (${formatSize(parameters.size)})` };
24336
+ const { size } = parameters;
24337
+ const ratio = size === undefined ? undefined : parseShape(size.aspectRatio);
24338
+ return size === undefined || ratio === undefined ? undefined : { ratio: ratio.ratio, asked: `${size.aspectRatio} (${size.width}x${size.height})` };
24054
24339
  }
24055
24340
  function mismatchOf(parameters, observed) {
24056
- const { seconds, followsFirstFrame } = parameters;
24341
+ const { followsFirstFrame } = parameters;
24342
+ const seconds = parameters.requested !== undefined ? parameters.requested.seconds ?? undefined : parameters.seconds;
24057
24343
  const expected = expectedShape(parameters);
24058
24344
  const problems = new Set;
24059
24345
  for (const facts of observed) {
24060
24346
  if (facts === null)
24061
24347
  continue;
24062
- if (!followsFirstFrame && Math.abs(Math.log(facts.width / facts.height / expected.ratio)) > aspectTolerance) {
24348
+ if (!followsFirstFrame && expected !== undefined && Math.abs(Math.log(facts.width / facts.height / expected.ratio)) > aspectTolerance) {
24063
24349
  problems.add(`asked for ${expected.asked}, got ${facts.width}x${facts.height}`);
24064
24350
  }
24065
24351
  if (seconds !== undefined && facts.seconds !== undefined && Math.abs(facts.seconds - seconds) > durationToleranceSeconds) {
@@ -24205,26 +24491,19 @@ function isPlainObject3(value) {
24205
24491
  // src/model-info.ts
24206
24492
  function videoModelInfo({ model }) {
24207
24493
  const adapter = adapterFor(model);
24208
- if (adapter.builtInVideoCapabilities === undefined || adapter.submitVideo === undefined) {
24494
+ if (adapter.submitVideo === undefined) {
24209
24495
  throw new ConfigError(`Model "${model.alias}" is a video Model, but its Protocol "${model.protocol}" makes no videos in litevision.`);
24210
24496
  }
24211
- return { capabilities: adapter.builtInVideoCapabilities(model.modelId), docsUrl: adapter.docsUrl(model.modelId, "video") };
24497
+ const guide = adapter.parameterGuide?.(model.modelId, "video");
24498
+ return {
24499
+ docsUrl: adapter.docsUrl(model.modelId, "video"),
24500
+ ...guide === undefined ? {} : { guide }
24501
+ };
24212
24502
  }
24213
24503
  var modelInfoTtlMs = 24 * 60 * 60 * 1000;
24214
24504
  var defaultModelInfoTimeoutMs = 5000;
24215
- var sizeSchema = object({
24216
- aspectRatio: _enum(aspectRatios),
24217
- width: number2().int().positive(),
24218
- height: number2().int().positive()
24219
- });
24220
- var capabilitiesSchema = object({
24221
- sizes: array(sizeSchema).min(1),
24222
- maxReferences: number2().int().nonnegative(),
24223
- maxImages: number2().int().positive()
24224
- });
24225
24505
  var entrySchema = object({
24226
24506
  fetchedAt: string2(),
24227
- capabilities: capabilitiesSchema.nullable(),
24228
24507
  parameterSchema: unknown().optional()
24229
24508
  });
24230
24509
  var cachePath = (dirs) => join6(dirs.data, "capabilities.json");
@@ -24233,10 +24512,10 @@ function cacheKey({ model, provider }) {
24233
24512
  }
24234
24513
  async function modelInfo(deps, resolved, options = {}) {
24235
24514
  const adapter = adapterFor(resolved.model);
24515
+ const guide = adapter.parameterGuide?.(resolved.model.modelId, "image");
24236
24516
  const builtIn = {
24237
- capabilities: adapter.builtInCapabilities(resolved.model.modelId),
24238
- source: "built-in",
24239
- docsUrl: adapter.docsUrl(resolved.model.modelId, "image")
24517
+ docsUrl: adapter.docsUrl(resolved.model.modelId, "image"),
24518
+ ...guide === undefined ? {} : { guide }
24240
24519
  };
24241
24520
  const fetchModelInfo = adapter.fetchModelInfo;
24242
24521
  if (fetchModelInfo === undefined)
@@ -24261,13 +24540,9 @@ async function modelInfo(deps, resolved, options = {}) {
24261
24540
  entry = cached;
24262
24541
  }
24263
24542
  }
24264
- if (entry === undefined)
24543
+ if (entry?.parameterSchema === undefined)
24265
24544
  return builtIn;
24266
- return {
24267
- ...builtIn,
24268
- ...entry.capabilities === null ? {} : { capabilities: entry.capabilities, source: "provider" },
24269
- ...entry.parameterSchema === undefined ? {} : { parameterSchema: entry.parameterSchema }
24270
- };
24545
+ return { ...builtIn, parameterSchema: entry.parameterSchema };
24271
24546
  }
24272
24547
  async function refreshModelInfo(deps, alias) {
24273
24548
  await refreshMatching(deps, (resolved) => resolved.model.alias === alias);
@@ -24288,12 +24563,7 @@ async function refreshMatching(deps, wanted) {
24288
24563
  } catch {}
24289
24564
  }
24290
24565
  function toEntry(info, now) {
24291
- const capabilities = capabilitiesSchema.safeParse(info?.capabilities);
24292
- return {
24293
- fetchedAt: now.toISOString(),
24294
- capabilities: capabilities.success ? capabilities.data : null,
24295
- ...info?.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema }
24296
- };
24566
+ return { fetchedAt: now.toISOString(), ...info?.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema } };
24297
24567
  }
24298
24568
  async function readCache(dirs) {
24299
24569
  let value;
@@ -24377,6 +24647,85 @@ function updateNotesAsAgent(dirs, alias, text, at) {
24377
24647
  });
24378
24648
  }
24379
24649
 
24650
+ // src/protocols/requests.ts
24651
+ function reservedConflicts(options, reservedFields) {
24652
+ const conflictOf = (field) => {
24653
+ const [parent, child] = field.split(".");
24654
+ if (!Object.hasOwn(options, parent))
24655
+ return;
24656
+ const value = options[parent];
24657
+ if (child === undefined)
24658
+ return `\`${field}\``;
24659
+ if (!isPlainObject3(value))
24660
+ return `\`${parent}\` as anything but an object (litevision sets \`${field}\` in it)`;
24661
+ return Object.hasOwn(value, child) ? `\`${field}\`` : undefined;
24662
+ };
24663
+ const order = Object.keys(options);
24664
+ return [...reservedFields].sort((a, b) => order.indexOf(a.split(".")[0]) - order.indexOf(b.split(".")[0])).map(conflictOf).filter((conflict) => conflict !== undefined);
24665
+ }
24666
+ function refuseReserved(providerOptions, reservedFields, fromWhat) {
24667
+ const conflicts = reservedConflicts(providerOptions, reservedFields);
24668
+ if (conflicts.length === 0)
24669
+ return;
24670
+ throw new ConfigError(`provider_options, and the Model's Standing Options they are merged over, may not set ${conflicts.join(", ")}: litevision fills ${conflicts.length === 1 ? "that field" : "those fields"} itself, from ${fromWhat}. Use those instead.`);
24671
+ }
24672
+ async function normaliseImageRequest(parameters, model, adapter, cwd) {
24673
+ refuseReserved(parameters.providerOptions, adapter.reservedOptions, "the prompt, `references` and `model`");
24674
+ const references = await readReferences(parameters.references, cwd);
24675
+ return {
24676
+ request: { prompt: parameters.prompt, n: parameters.n, references, providerOptions: parameters.providerOptions },
24677
+ requested: readRequested(fieldsToRead(adapter, model.modelId, "image"), parameters.providerOptions, "image")
24678
+ };
24679
+ }
24680
+ async function normaliseVideoRequest(parameters, model, adapter, cwd) {
24681
+ refuseReserved(parameters.providerOptions, adapter.reservedVideoOptions ?? [], "the prompt, `first_frame`, `last_frame`, `references` and `model`");
24682
+ const frame = async (path) => path === undefined ? undefined : (await readReferences([path], cwd))[0];
24683
+ const [firstFrame, lastFrame, references] = await Promise.all([
24684
+ frame(parameters.firstFrame),
24685
+ frame(parameters.lastFrame),
24686
+ readReferences(parameters.references, cwd)
24687
+ ]);
24688
+ return {
24689
+ request: {
24690
+ prompt: parameters.prompt,
24691
+ ...firstFrame === undefined ? {} : { firstFrame },
24692
+ ...lastFrame === undefined ? {} : { lastFrame },
24693
+ references,
24694
+ providerOptions: parameters.providerOptions
24695
+ },
24696
+ requested: readRequested(fieldsToRead(adapter, model.modelId, "video"), parameters.providerOptions, "video")
24697
+ };
24698
+ }
24699
+
24700
+ // src/standing-options.ts
24701
+ function mergeOptions(standing, agent) {
24702
+ const merged = { ...standing };
24703
+ for (const [key, value] of Object.entries(agent)) {
24704
+ if (value === null)
24705
+ delete merged[key];
24706
+ else if (isPlainObject3(value))
24707
+ merged[key] = mergeOptions(isPlainObject3(merged[key]) ? merged[key] : {}, value);
24708
+ else
24709
+ merged[key] = value;
24710
+ }
24711
+ return merged;
24712
+ }
24713
+ function reservedFields(protocol, modality) {
24714
+ const adapter = adapterForProtocol(protocol);
24715
+ if (adapter === undefined)
24716
+ return [];
24717
+ return modality === "video" ? adapter.reservedVideoOptions ?? [] : adapter.reservedOptions;
24718
+ }
24719
+ function standingOptionsProblem(value, protocol, modality) {
24720
+ if (!isPlainObject3(value)) {
24721
+ return 'Standing Options must be a JSON object of request fields, such as { "quality": "high" }.';
24722
+ }
24723
+ const conflicts = reservedConflicts(value, reservedFields(protocol, modality));
24724
+ if (conflicts.length === 0)
24725
+ return;
24726
+ return `Standing Options may not set ${conflicts.join(", ")}: litevision fills ${conflicts.length === 1 ? "that field" : "those fields"} itself for Protocol "${protocol}".`;
24727
+ }
24728
+
24380
24729
  // src/settings/browser.ts
24381
24730
  import { spawn } from "node:child_process";
24382
24731
  var openInBrowser = (url) => {
@@ -24666,7 +25015,8 @@ var addModelInput = object({
24666
25015
  modelId: string2().trim().min(1, "Enter a model id.").max(200, "A model id is at most 200 characters."),
24667
25016
  protocol: string2().refine((value) => supportedProtocols().includes(value), { error: () => `Choose a supported Protocol: ${supportedProtocols().join(", ")}.` }),
24668
25017
  modality: _enum(modalities, { error: () => "Choose whether the Model makes images or videos." }).default("image"),
24669
- notes: notes.optional()
25018
+ notes: notes.optional(),
25019
+ standingOptions: unknown().optional()
24670
25020
  });
24671
25021
  var updateModelInput = addModelInput.omit({ modality: true }).partial().extend({ modality: addModelInput.shape.modality.unwrap().optional() });
24672
25022
  var storedModels = array(modelSchema);
@@ -24700,6 +25050,8 @@ async function addModel(dirs, input, at) {
24700
25050
  stored.notes = input.notes;
24701
25051
  stored.notesHistory = { changedBy: "User", changedAt: at.toISOString() };
24702
25052
  }
25053
+ if (input.standingOptions !== undefined)
25054
+ setStandingOptions(stored, input.standingOptions);
24703
25055
  config.models = [...models, stored];
24704
25056
  });
24705
25057
  return getModel(dirs, input.alias);
@@ -24732,6 +25084,8 @@ async function updateModel(dirs, wanted, input, at) {
24732
25084
  model.protocol = input.protocol;
24733
25085
  if (input.notes !== undefined)
24734
25086
  replaceNotes(model, input.notes, "User", at);
25087
+ if (input.standingOptions !== undefined)
25088
+ setStandingOptions(model, input.standingOptions);
24735
25089
  });
24736
25090
  return getModel(dirs, input.alias ?? wanted);
24737
25091
  }
@@ -24774,6 +25128,16 @@ async function setDefaultModel(dirs, modality, wanted) {
24774
25128
  });
24775
25129
  return { alias: wanted };
24776
25130
  }
25131
+ function setStandingOptions(model, value) {
25132
+ if (value === null || isPlainObject3(value) && Object.keys(value).length === 0) {
25133
+ delete model.standingOptions;
25134
+ return;
25135
+ }
25136
+ const problem = standingOptionsProblem(value, model.protocol, modalityOf(model));
25137
+ if (problem !== undefined)
25138
+ throw new SettingsError(400, problem);
25139
+ model.standingOptions = value;
25140
+ }
24777
25141
  function requireFreeAlias(models, wanted) {
24778
25142
  if (models.some((model) => model.alias === wanted)) {
24779
25143
  throw new SettingsError(409, `A Model with the Alias "${wanted}" already exists. Choose another Alias.`);
@@ -24806,6 +25170,7 @@ function view2(model, defaults) {
24806
25170
  modality,
24807
25171
  notes: model.notes ?? "",
24808
25172
  ...model.notesHistory && { notesHistory: model.notesHistory },
25173
+ ...model.standingOptions && { standingOptions: model.standingOptions },
24809
25174
  default: model.alias === (modality === "video" ? defaults.defaultVideoModel : defaults.defaultImageModel)
24810
25175
  };
24811
25176
  }
@@ -25377,6 +25742,31 @@ function notesHistoryRow(model, path) {
25377
25742
  return [el("div", { className: "note" }, ...parts)];
25378
25743
  }
25379
25744
 
25745
+ /** A JSON textarea for a Model's Standing Options: request fields every Job on it sends unless the Agent overrides them. */
25746
+ function standingOptionsField(value) {
25747
+ const textarea = el("textarea", {
25748
+ value: value ? JSON.stringify(value, null, 2) : "",
25749
+ spellcheck: false,
25750
+ placeholder: '{ "quality": "high" }',
25751
+ });
25752
+ const wrapper = field("Standing Options", textarea);
25753
+ wrapper.classList.add("wide");
25754
+ wrapper.append(el("div", { className: "note", textContent: "A JSON object of the Provider's own request fields, sent with every Job on this Model. Your Agent's provider_options override them for a Job. Leave empty for none." }));
25755
+ /** The value to save: null when empty; throws when the text is not a JSON object. */
25756
+ const read = () => {
25757
+ const text = textarea.value.trim();
25758
+ if (text === "") return null;
25759
+ let parsed;
25760
+ try {
25761
+ parsed = JSON.parse(text);
25762
+ } catch {
25763
+ throw new Error('Standing Options must be a JSON object, such as { "quality": "high" }. This is not valid JSON: ' + text.slice(0, 40));
25764
+ }
25765
+ return parsed;
25766
+ };
25767
+ return { wrapper, read };
25768
+ }
25769
+
25380
25770
  function modelCard(model) {
25381
25771
  const alias = el("input", { value: model.alias, spellcheck: false });
25382
25772
  const provider = el("select", {}, ...options(providerIds.includes(model.provider) ? providerIds : [model.provider, ...providerIds], model.provider));
@@ -25384,11 +25774,12 @@ function modelCard(model) {
25384
25774
  const protocol = el("select", {}, ...options(protocols.includes(model.protocol) ? protocols : [model.protocol, ...protocols], model.protocol));
25385
25775
  const modality = el("select", {}, ...options(["image", "video"], model.modality));
25386
25776
  const notes = notesField(model.notes);
25777
+ const standing = standingOptionsField(model.standingOptions);
25387
25778
  const path = "/api/models/" + encodeURIComponent(model.alias);
25388
25779
 
25389
25780
  const save = el("button", { type: "button", className: "primary", textContent: "Save" });
25390
25781
  save.onclick = () => act(save, async () => {
25391
- await api("PATCH", path, { alias: alias.value, provider: provider.value, modelId: modelId.value, protocol: protocol.value, modality: modality.value, notes: notes.textarea.value });
25782
+ await api("PATCH", path, { alias: alias.value, provider: provider.value, modelId: modelId.value, protocol: protocol.value, modality: modality.value, notes: notes.textarea.value, standingOptions: standing.read() });
25392
25783
  await loadModels();
25393
25784
  }, "Model saved.");
25394
25785
 
@@ -25413,14 +25804,21 @@ function modelCard(model) {
25413
25804
  el("div", { className: "card-head" }, el("h3", { textContent: model.alias }),
25414
25805
  model.default ? el("span", { className: "chip accent", textContent: "Default " + model.modality }) : makeDefault),
25415
25806
  el("div", { className: "fields" },
25416
- field("Alias", alias), field("Provider", provider), field("Model id", modelId), field("Protocol", protocol), field("Makes", modality), notes.wrapper),
25807
+ field("Alias", alias), field("Provider", provider), field("Model id", modelId), field("Protocol", protocol), field("Makes", modality), notes.wrapper, standing.wrapper),
25417
25808
  el("div", { className: "actions" }, save, el("span", { className: "spacer" }), remove),
25418
25809
  );
25419
25810
  }
25420
25811
 
25812
+ // The saved Models, so the fetched model list can say which model ids are already added.
25813
+ let savedModels = [];
25814
+ // Redraws the fetched model list; set by setUpAddModelForm.
25815
+ let showModelIds = () => {};
25816
+
25421
25817
  async function loadModels() {
25422
25818
  const list = document.getElementById("model-list");
25423
25819
  const { models } = await api("GET", "/api/models");
25820
+ savedModels = models;
25821
+ showModelIds();
25424
25822
  list.replaceChildren(...(models.length === 0
25425
25823
  ? [emptyState(providerIds.length === 0 ? "Add a Provider first, then add its Models below." : "No Models yet. Add one below.")]
25426
25824
  : models.map(modelCard)));
@@ -25436,34 +25834,56 @@ function setUpAddModelForm() {
25436
25834
  const fetchResult = document.getElementById("fetch-result");
25437
25835
  const ids = document.getElementById("model-ids");
25438
25836
  const modality = document.getElementById("model-modality");
25439
- // What the fetched model list says each model id makes, when the Provider says.
25837
+ // The model ids the last fetch found, and what each one makes when the Provider says.
25838
+ let fetchedIds = [];
25440
25839
  let knownModalities = {};
25441
25840
  protocol.replaceChildren(...options(protocols));
25442
25841
 
25842
+ const forgetModelIds = () => {
25843
+ fetchedIds = [];
25844
+ knownModalities = {};
25845
+ showModelIds();
25846
+ };
25847
+
25848
+ // Lists the fetched model ids that make what Makes says (and those the Provider says nothing about),
25849
+ // marking the ones already added as a Model on this Provider.
25850
+ showModelIds = () => {
25851
+ const shown = fetchedIds.filter((id) => (knownModalities[id] ?? modality.value) === modality.value);
25852
+ ids.replaceChildren(...shown.map((id) => {
25853
+ const aliases = savedModels.filter((m) => m.provider === provider.value && m.modelId === id).map((m) => m.alias);
25854
+ return el("option", aliases.length === 0 ? { value: id } : { value: id, label: "Already added as " + aliases.join(", ") });
25855
+ }));
25856
+ if (fetchedIds.length === 0) return;
25857
+ const hidden = fetchedIds.length - shown.length;
25858
+ const other = modality.value === "image" ? "video" : "image";
25859
+ fetchResult.textContent = shown.length + (hidden === 0 ? "" : " " + modality.value) + " model id(s) found"
25860
+ + (hidden === 0 ? "" : ", and " + hidden + " " + other + " model id(s) hidden until Makes says " + (other === "image" ? "Images" : "Videos"))
25861
+ + ". Pick one in the Model id field, or type your own.";
25862
+ };
25863
+
25443
25864
  // Suggest the Protocol a preset Provider's Models usually speak; the User can still change it.
25444
25865
  provider.onchange = () => {
25445
25866
  const suggested = presets.find((p) => p.id === provider.value);
25446
25867
  if (suggested && protocols.includes(suggested.protocol)) protocol.value = suggested.protocol;
25447
- ids.replaceChildren();
25448
- knownModalities = {};
25868
+ forgetModelIds();
25449
25869
  fetchResult.textContent = "";
25450
25870
  };
25451
- protocol.onchange = () => {
25452
- ids.replaceChildren();
25453
- knownModalities = {};
25454
- };
25871
+ protocol.onchange = forgetModelIds;
25872
+ modality.onchange = () => showModelIds();
25455
25873
 
25456
25874
  fetchButton.onclick = () => act(fetchButton, async () => {
25457
25875
  fetchResult.textContent = "Fetching…";
25458
25876
  fetchResult.className = "result";
25459
25877
  const outcome = await api("POST", "/api/providers/" + encodeURIComponent(provider.value) + "/model-ids", { protocol: protocol.value });
25460
25878
  if (outcome.result === "success") {
25879
+ fetchedIds = outcome.modelIds;
25461
25880
  knownModalities = outcome.modalities || {};
25462
- ids.replaceChildren(...outcome.modelIds.map((id) => el("option", { value: id, label: knownModalities[id] ? id + " (" + knownModalities[id] + ")" : id })));
25463
- fetchResult.textContent = outcome.modelIds.length + " model id(s) found. Pick one in the Model id field, or type your own.";
25464
25881
  fetchResult.className = "result success";
25882
+ showModelIds();
25883
+ if (fetchedIds.length === 0) fetchResult.textContent = "No model ids found. Type the model id.";
25465
25884
  modelId.focus();
25466
25885
  } else {
25886
+ forgetModelIds();
25467
25887
  fetchResult.textContent = outcome.message;
25468
25888
  fetchResult.className = "result " + outcome.result;
25469
25889
  }
@@ -25474,7 +25894,10 @@ function setUpAddModelForm() {
25474
25894
  alias.oninput = () => (aliasEdited = alias.value !== "");
25475
25895
  modelId.oninput = () => {
25476
25896
  const known = knownModalities[modelId.value.trim()];
25477
- if (known) modality.value = known;
25897
+ if (known && known !== modality.value) {
25898
+ modality.value = known;
25899
+ showModelIds();
25900
+ }
25478
25901
  if (!aliasEdited) alias.value = modelId.value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
25479
25902
  };
25480
25903
 
@@ -25831,16 +26254,16 @@ function settingsOpener(options) {
25831
26254
  };
25832
26255
  }
25833
26256
  // package.json
25834
- var version2 = "0.2.0";
26257
+ var version2 = "0.3.0";
25835
26258
 
25836
26259
  // src/server.ts
25837
26260
  var instructions = `litevision generates images and videos through Models the User has configured with their own Providers. Before writing a prompt, use the litevision Skill. If you do not have it, mention once that the User can install it with \`npx skills add levyxdev/litevision\`.
25838
26261
 
25839
26262
  - Never ask the User for an API key or any other Credential. If litevision reports that nothing is configured, call \`open_settings\` so the User can set it up in their browser.
25840
26263
  - Use the Model the User names. When they name none, omit \`model\` for the default, unless the request needs something the default lacks; then choose with \`list_models\`.
25841
- - For \`provider_options\`, reuse the Model's Known-good Options. For any other option, or one marked suspect, read the Model's docs and parameter schema first (\`list_models\` with \`model\`).
26264
+ - litevision passes \`provider_options\` to the Provider as they are and translates nothing: write each Model's own fields, such as the size, resolution or length, from its Parameter Guide (\`list_models\` with \`model\`). Without them the Provider's defaults apply. Reuse Known-good Options; for anything else, or an option marked suspect, read the Parameter Guide and docs first.
25842
26265
  - If the Provider rejects an option (the error says it "rejected an option"), fix it from the Provider's message and the docs and try again, at most twice. If it is still rejected, stop and tell the User what the Provider said.
25843
- - Tell the User where the files are, and pass on whatever a result says was adjusted, did not match the request, or came back short.
26266
+ - Tell the User where the files are, and pass on whatever a result says did not match the request or came back short.
25844
26267
  - \`generate_comparison\` costs as much as all its Jobs. Start a Comparison only when the User asks for one or agrees to it.
25845
26268
  - When the User says what they think of a result, call \`rate_job\` with their verdict and a one-line reason in their words; when it is about one result of a Job with several, pass \`result\`. Never invent a verdict, and never rate a result yourself.
25846
26269
  - When the User chooses among a Comparison's results, call \`pick_result\`, only when the User actually chooses. A Pick is not a Rating: if they also give a verdict, call \`rate_job\` too.
@@ -25856,24 +26279,21 @@ var listedModelSchema = object({
25856
26279
  modality: _enum(modalities).describe("What the Model generates: images with `generate_image`, videos with `generate_video`."),
25857
26280
  default: boolean2().describe("True for the default Model of its Modality."),
25858
26281
  notes: string2().describe("Model Notes: what the Model is good or bad at, agreed with the User."),
25859
- capabilities: object({
25860
- aspect_ratios: array(object({ aspect_ratio: _enum(aspectRatios), size: string2() })),
25861
- reference_images: boolean2(),
25862
- max_reference_images: number2(),
25863
- max_images_per_job: number2()
25864
- }).nullable().describe("An image Model's capabilities; null for a video Model, or when the Model cannot be used (`problem` then says why)."),
25865
- video_capabilities: object({
25866
- resolutions: array(object({
25867
- resolution: string2().nullable().describe("The tier's name to pass as `resolution`; null when the Model takes none."),
25868
- aspect_ratios: array(object({ aspect_ratio: _enum(aspectRatios), size: string2() }))
25869
- })),
25870
- durations: array(number2()).describe("Every clip length in seconds the Model can make."),
25871
- default_duration: number2(),
25872
- first_frame: boolean2(),
25873
- last_frame: boolean2(),
25874
- max_reference_images: number2()
25875
- }).optional().describe("A video Model's capabilities; the first resolution is the default."),
26282
+ standing_options: record(string2(), unknown()).describe("Standing Options the User set: request fields every Job on this Model sends. Your `provider_options` are merged over them: your values win, objects merge key by key, and `null` removes a key for that Job."),
25876
26283
  docs_url: string2().nullable().describe("Where to read which `provider_options` this Model accepts."),
26284
+ parameters: string2().optional().describe("In brief, what litevision knows about the Model's request parameters: its Parameter Guide, the Provider's own description, or that it knows neither."),
26285
+ parameter_guide: object({
26286
+ family: string2(),
26287
+ checked: string2().describe("When the sources were last checked; read the docs first when this is old."),
26288
+ sources: array(string2()),
26289
+ parameters: array(object({
26290
+ name: string2().describe("The Provider's own field name for `provider_options`, dotted for nested fields."),
26291
+ values: string2(),
26292
+ default: string2(),
26293
+ price: string2().describe("How the value changes the price.")
26294
+ })),
26295
+ notes: array(string2())
26296
+ }).nullable().optional().describe("Only with `model`: the built-in Parameter Guide for the Model's family. Null when litevision has none, or when the Provider describes the Model itself (see `parameter_schema`)."),
25877
26297
  known_good_options: record(string2(), unknown()).optional().describe("Known-good Options: `provider_options` whose results matched the request and that the User has not rated bad. Reuse them."),
25878
26298
  suspect_options: record(string2(), object({ value: unknown(), reason: string2(), job_id: string2(), cause: _enum(["rating", "mismatch"]) })).optional().describe('`provider_options` last used by a Job the User rated bad (cause "rating", with the User\'s reason) or whose result did not match the request (cause "mismatch"). Check the docs before sending them again.'),
25879
26299
  problem: string2().optional(),
@@ -25904,10 +26324,11 @@ async function listedModel(deps, config, model, isDefault, detailed, ratedJobs)
25904
26324
  model_id: model.modelId,
25905
26325
  modality: model.modality,
25906
26326
  default: isDefault,
25907
- notes: model.notes ?? ""
26327
+ notes: model.notes ?? "",
26328
+ standing_options: model.standingOptions ?? {}
25908
26329
  };
25909
26330
  const heading = `${model.alias}${isDefault ? ` (default ${model.modality})` : ""}: ${model.modality} Model ${model.modelId} on ${model.provider} via ${model.protocol}.`;
25910
- const notes = model.notes ? ` Notes: ${model.notes}` : "";
26331
+ const notes = `${model.notes ? ` Notes: ${model.notes}` : ""}${model.standingOptions ? ` Standing Options: ${JSON.stringify(model.standingOptions)}.` : ""}`;
25911
26332
  let described;
25912
26333
  try {
25913
26334
  const provider = config.providers.find((candidate) => candidate.id === model.provider);
@@ -25916,45 +26337,36 @@ async function listedModel(deps, config, model, isDefault, detailed, ratedJobs)
25916
26337
  if (model.modality === "video") {
25917
26338
  const info = videoModelInfo({ model, provider });
25918
26339
  described = {
25919
- output: { capabilities: null, video_capabilities: videoCapabilitiesOutput(info.capabilities) },
25920
- summary: describeVideoCapabilities(info.capabilities),
25921
- docsUrl: info.docsUrl
26340
+ docsUrl: info.docsUrl,
26341
+ ...info.guide === undefined ? {} : { guide: info.guide }
25922
26342
  };
25923
26343
  } else {
25924
26344
  const info = await modelInfo(deps, { model, provider });
25925
- const { capabilities } = info;
25926
26345
  described = {
25927
- output: {
25928
- capabilities: {
25929
- aspect_ratios: capabilities.sizes.map((size) => ({ aspect_ratio: size.aspectRatio, size: formatSize(size) })),
25930
- reference_images: capabilities.maxReferences > 0,
25931
- max_reference_images: capabilities.maxReferences,
25932
- max_images_per_job: capabilities.maxImages
25933
- }
25934
- },
25935
- summary: describeCapabilities(capabilities),
25936
26346
  docsUrl: info.docsUrl,
25937
- ...info.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema }
26347
+ ...info.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema },
26348
+ ...info.guide === undefined ? {} : { guide: info.guide }
25938
26349
  };
25939
26350
  }
25940
26351
  } catch (error) {
25941
26352
  if (!(error instanceof ConfigError))
25942
26353
  throw error;
25943
26354
  return {
25944
- output: { ...base, capabilities: null, docs_url: null, problem: error.message },
26355
+ output: { ...base, docs_url: null, problem: error.message },
25945
26356
  text: `${heading} Cannot be used: ${error.message}${notes}`
25946
26357
  };
25947
26358
  }
25948
26359
  const record2 = optionsOf(ratedJobs, model);
25949
26360
  const output = {
25950
26361
  ...base,
25951
- capabilities: null,
25952
- ...described.output,
25953
26362
  docs_url: described.docsUrl,
26363
+ parameters: parametersSummary(described, model.provider),
25954
26364
  known_good_options: record2.knownGood,
25955
26365
  suspect_options: suspectOutput(record2)
25956
26366
  };
25957
- let text = `${heading} Supports ${described.summary}${notes} provider_options docs: ${described.docsUrl}`;
26367
+ let text = `${heading}${notes} provider_options docs: ${described.docsUrl}`;
26368
+ if (!detailed)
26369
+ text += ` ${output.parameters}`;
25958
26370
  const goodKeys = Object.keys(record2.knownGood);
25959
26371
  const suspectKeys = Object.keys(record2.suspect);
25960
26372
  if (detailed) {
@@ -25980,11 +26392,11 @@ Recent Picks in Comparisons, newest first:`;
25980
26392
  for (const picked of picks)
25981
26393
  text += `
25982
26394
  - ${describeRecentPick(picked)}`;
25983
- if (described.parameterSchema !== undefined) {
26395
+ output.parameter_guide = described.parameterSchema === undefined && described.guide !== undefined ? guideOutput(described.guide) : null;
26396
+ if (described.parameterSchema !== undefined)
25984
26397
  output.parameter_schema = described.parameterSchema;
25985
- text += `
25986
- Parameter schema from the Provider: ${JSON.stringify(described.parameterSchema)}`;
25987
- }
26398
+ text += `
26399
+ ${parameterHelp(described, model.provider)}`;
25988
26400
  } else {
25989
26401
  if (goodKeys.length > 0)
25990
26402
  text += ` Known-good Options: ${goodKeys.join(", ")}.`;
@@ -25993,6 +26405,25 @@ Parameter schema from the Provider: ${JSON.stringify(described.parameterSchema)}
25993
26405
  }
25994
26406
  return { output, text };
25995
26407
  }
26408
+ function parametersSummary(known, providerId) {
26409
+ if (known.parameterSchema !== undefined) {
26410
+ return `Provider "${providerId}" describes this Model's parameters itself; \`list_models\` with \`model\` shows its description.`;
26411
+ }
26412
+ if (known.guide !== undefined)
26413
+ return guideSummary(known.guide);
26414
+ return noGuide(known.docsUrl);
26415
+ }
26416
+ function parameterHelp(known, providerId) {
26417
+ if (known.parameterSchema !== undefined) {
26418
+ return `Parameter schema from Provider "${providerId}", which wins over litevision's own Parameter Guide: ${JSON.stringify(known.parameterSchema)}`;
26419
+ }
26420
+ if (known.guide !== undefined)
26421
+ return guideText(known.guide);
26422
+ return noGuide(known.docsUrl);
26423
+ }
26424
+ function noGuide(docsUrl) {
26425
+ return `litevision has no Parameter Guide for this model id; read ${docsUrl} before setting provider_options.`;
26426
+ }
25996
26427
  function ratingOutput(rating) {
25997
26428
  return {
25998
26429
  job_id: rating.jobId,
@@ -26041,19 +26472,6 @@ function promptStart(prompt) {
26041
26472
  const line = prompt.replace(/\s+/g, " ").trim();
26042
26473
  return line.length <= promptStartLength ? line : `${line.slice(0, promptStartLength).trimEnd()}…`;
26043
26474
  }
26044
- function videoCapabilitiesOutput(capabilities) {
26045
- return {
26046
- resolutions: capabilities.resolutions.map((tier) => ({
26047
- resolution: tier.name ?? null,
26048
- aspect_ratios: tier.sizes.map((size) => ({ aspect_ratio: size.aspectRatio, size: formatSize(size) }))
26049
- })),
26050
- durations: capabilities.durations,
26051
- default_duration: capabilities.defaultDuration,
26052
- first_frame: capabilities.firstFrame,
26053
- last_frame: capabilities.lastFrame,
26054
- max_reference_images: capabilities.maxReferences
26055
- };
26056
- }
26057
26475
  function suspectOutput(record2) {
26058
26476
  return Object.fromEntries(Object.entries(record2.suspect).map(([key, suspect]) => [
26059
26477
  key,
@@ -26066,22 +26484,47 @@ async function modelOptions(deps, model) {
26066
26484
  function optionsOf(ratedJobs, model) {
26067
26485
  return optionsRecord(ratedJobs, { provider: model.provider, protocol: model.protocol, modelId: model.modelId });
26068
26486
  }
26069
- async function jobDocs(dirs, jobId) {
26070
- const job = await readJob(dirs, jobId);
26071
- const docsUrl = job === undefined ? undefined : adapterForProtocol(job.protocol)?.docsUrl(job.modelId, job.modality ?? "image");
26072
- if (job === undefined || docsUrl === undefined)
26487
+ async function jobDocs(deps, jobId) {
26488
+ const job = await readJob(deps.dirs, jobId);
26489
+ const adapter = job === undefined ? undefined : adapterForProtocol(job.protocol);
26490
+ if (job === undefined || adapter === undefined)
26491
+ return;
26492
+ const modality = job.modality ?? "image";
26493
+ const guide = adapter.parameterGuide?.(job.modelId, modality);
26494
+ let known = { docsUrl: adapter.docsUrl(job.modelId, modality), ...guide === undefined ? {} : { guide } };
26495
+ const provider = modality === "image" ? (await loadConfig(deps.dirs).catch(() => {
26073
26496
  return;
26074
- return { docsUrl, options: optionsRecord(await readRatedJobs(dirs), job) };
26497
+ }))?.providers.find((candidate) => candidate.id === job.provider) : undefined;
26498
+ if (provider !== undefined) {
26499
+ const model = { alias: job.model, provider: job.provider, modelId: job.modelId, protocol: job.protocol, modality };
26500
+ known = await modelInfo(deps, { model, provider });
26501
+ }
26502
+ return { known, provider: job.provider, options: optionsRecord(await readRatedJobs(deps.dirs), job) };
26075
26503
  }
26076
26504
  function suspectWhy(suspect) {
26077
26505
  return suspect.cause === "rating" ? `which the User rated bad: "${suspect.reason}"` : `whose result did not match the request: ${suspect.reason}`;
26078
26506
  }
26507
+ function failureHint(known, providerId, record2, kind) {
26508
+ const help = kind === "rejected_option" || kind === "unsupported" ? `
26509
+ ${parameterHelp(known, providerId)}` : "";
26510
+ return `${optionsHint(known.docsUrl, record2)}${help}`;
26511
+ }
26079
26512
  function optionsHint(docsUrl, record2) {
26080
26513
  const good = Object.keys(record2.knownGood).length === 0 ? "none recorded yet" : JSON.stringify(record2.knownGood);
26081
26514
  const suspect = Object.entries(record2.suspect).map(([key, entry]) => ` Suspect: ${key}=${JSON.stringify(entry.value)}, used by a Job ${suspectWhy(entry)}.`).join("");
26082
26515
  return ` Docs for this Model's provider_options: ${docsUrl}. Known-good Options: ${good}.${suspect}`;
26083
26516
  }
26084
26517
  var observedSchema = array(object({ width: number2(), height: number2(), seconds: number2().optional() }).nullable()).describe("What each saved file turned out to be, in the order of `files`, read from the file itself; null where litevision could not tell.");
26518
+ var requestedShapeSchema = string2().nullable().describe("The shape the request sent asks for, as litevision read it from the Model's own fields, e.g. 16:9 or 3840x2160; null when it asks for none litevision can read, so the result's shape is not checked. Compare it with `observed`.");
26519
+ var requestedResolutionSchema = string2().nullable().describe("The resolution tier the request sent asks for, as written in its Protocol's tier field, e.g. 4K or 1080P; null when it sets none. Not checked against the result.");
26520
+ var requestedDurationSchema = number2().nullable().describe("Video Jobs: the length in seconds the request sent asks for, as litevision read it; null when unreadable, so the length is not checked.");
26521
+ function requestedOutput(requested) {
26522
+ return {
26523
+ requested_shape: requested.shape,
26524
+ requested_resolution: requested.resolution,
26525
+ ...requested.seconds === undefined ? {} : { requested_duration: requested.seconds }
26526
+ };
26527
+ }
26085
26528
  var mismatchSchema = string2().optional().describe("Present when the result's shape or length differs from the request: an option may not have taken effect. Tell the User.");
26086
26529
  function observedText(observed) {
26087
26530
  const known = [...new Set(observed.flatMap((facts) => facts === null ? [] : [describeFacts(facts)]))];
@@ -26090,6 +26533,14 @@ function observedText(observed) {
26090
26533
  function describeFacts(facts) {
26091
26534
  return `${facts.width}x${facts.height}${facts.seconds === undefined ? "" : `, ${facts.seconds} s`}`;
26092
26535
  }
26536
+ function requestedText({ shape, resolution, seconds }) {
26537
+ const size = shape === null ? resolution : resolution === null ? shape : `${shape} at ${resolution}`;
26538
+ const read = [size, typeof seconds === "number" ? `${seconds} s` : null].filter((part) => part !== null);
26539
+ const unread = [shape === null ? "shape" : null, seconds === null ? "length" : null].filter((part) => part !== null);
26540
+ const asked = read.length === 0 ? "" : `The request asks for ${read.join(", ")}.`;
26541
+ const unchecked = unread.length === 0 ? "" : `The request asks for no ${unread.join(" or ")} litevision can read, so the result's ${unread.join(" and ")} ${unread.length === 1 ? "was" : "were"} not checked.`;
26542
+ return [asked, unchecked].filter(Boolean).join(" ");
26543
+ }
26093
26544
  function mismatchText(mismatch, docsUrl) {
26094
26545
  if (mismatch === undefined)
26095
26546
  return "";
@@ -26104,7 +26555,8 @@ function jobOutput(job) {
26104
26555
  files: job.status === "completed" ? job.files : [],
26105
26556
  observed: job.status === "completed" ? job.observed : [],
26106
26557
  ...job.status === "completed" && job.shortfall !== undefined ? { shortfall: job.shortfall } : {},
26107
- ...job.status === "completed" && job.mismatch !== undefined ? { mismatch: job.mismatch } : {}
26558
+ ...job.status === "completed" && job.mismatch !== undefined ? { mismatch: job.mismatch } : {},
26559
+ ...job.status === "completed" && job.requested !== undefined ? requestedOutput(job.requested) : {}
26108
26560
  };
26109
26561
  }
26110
26562
  function jobText(job) {
@@ -26122,7 +26574,7 @@ async function failureReason(error, hint) {
26122
26574
  return error.message;
26123
26575
  if (error instanceof JobFailedError) {
26124
26576
  const helpful = hint !== undefined && (error.kind === "unsupported" || error.kind === "rejected_option" || error.kind === "other");
26125
- return `${error.message}${helpful ? await hint() : ""}`;
26577
+ return `${error.message}${helpful ? await hint(error.kind) : ""}`;
26126
26578
  }
26127
26579
  throw error;
26128
26580
  }
@@ -26131,10 +26583,8 @@ var imageJobShape = {
26131
26583
  status: jobStatusSchema,
26132
26584
  model: string2().describe("Alias of the Model that ran the Job."),
26133
26585
  files: array(string2()).describe("Paths of the saved images; empty while the Job is running."),
26134
- aspect_ratio: _enum(aspectRatios).describe("Aspect ratio actually used."),
26135
- size: string2().describe("Size actually requested from the Provider, e.g. 1536x1024."),
26136
- requested_aspect_ratio: _enum(aspectRatios),
26137
- aspect_ratio_adjusted: boolean2().describe("True when the Model does not support the requested aspect ratio and the closest one was used."),
26586
+ requested_shape: requestedShapeSchema,
26587
+ requested_resolution: requestedResolutionSchema,
26138
26588
  shortfall: string2().optional().describe("Present when fewer images than `n` were saved: how many, and why."),
26139
26589
  observed: observedSchema,
26140
26590
  mismatch: mismatchSchema
@@ -26144,13 +26594,9 @@ var videoJobShape = {
26144
26594
  status: jobStatusSchema,
26145
26595
  model: string2().describe("Alias of the Model that ran the Job."),
26146
26596
  files: array(string2()).describe("Path of the saved video; empty while the Job is running."),
26147
- aspect_ratio: _enum(aspectRatios).describe("Aspect ratio actually used."),
26148
- size: string2().describe("Size the request implies, e.g. 1280x720."),
26149
- resolution: string2().nullable().describe("Resolution tier actually requested; null when the Model takes none."),
26150
- duration: number2().describe("Length in seconds actually requested."),
26151
- requested_aspect_ratio: _enum(aspectRatios),
26152
- aspect_ratio_adjusted: boolean2(),
26153
- duration_adjusted: boolean2().describe("True when the Model cannot make the requested length and the closest one was used."),
26597
+ requested_shape: requestedShapeSchema,
26598
+ requested_duration: requestedDurationSchema,
26599
+ requested_resolution: requestedResolutionSchema,
26154
26600
  observed: observedSchema,
26155
26601
  mismatch: mismatchSchema
26156
26602
  };
@@ -26183,23 +26629,21 @@ function createServer2(deps) {
26183
26629
  const adapter = adapterFor(model);
26184
26630
  const credential = await requireCredential(deps.dirs, resolved.provider);
26185
26631
  const info = await modelInfo(infoDeps, resolved);
26186
- const normalised = await normaliseImageRequest({ prompt: args.prompt, n: args.n, aspectRatio: args.aspect_ratio, references: args.references, providerOptions: args.provider_options }, model, adapter, info, cwd);
26632
+ const normalised = await normaliseImageRequest({
26633
+ prompt: args.prompt,
26634
+ n: args.n,
26635
+ references: args.references,
26636
+ providerOptions: mergeOptions(model.standingOptions ?? {}, args.provider_options)
26637
+ }, model, adapter, cwd);
26187
26638
  const outputDir = args.output_path === undefined ? undefined : resolve2(cwd, args.output_path);
26188
26639
  return {
26189
26640
  model,
26190
26641
  start: (comparison) => jobs.start({ ...resolved, adapter, credential, normalised, outputDir, ...comparison === undefined ? {} : { comparison } }),
26191
- hint: async () => optionsHint(info.docsUrl, await modelOptions(deps, model)),
26642
+ hint: async (kind) => failureHint(info, model.provider, await modelOptions(deps, model), kind),
26192
26643
  report: (job) => {
26193
- const { size } = normalised.request;
26194
- const output = {
26195
- ...jobOutput(job),
26196
- aspect_ratio: size.aspectRatio,
26197
- size: formatSize(size),
26198
- requested_aspect_ratio: normalised.requestedAspectRatio,
26199
- aspect_ratio_adjusted: normalised.aspectRatioAdjusted
26200
- };
26201
- const adjustment = normalised.aspectRatioAdjusted ? ` ${model.alias} does not support ${normalised.requestedAspectRatio}, so the closest supported aspect ratio was used.` : "";
26202
- const text = job.status === "running" ? `${jobText(job)} It asks ${model.alias} for ${size.aspectRatio} (${formatSize(size)}).${adjustment}` : `Saved ${job.files.length} image(s) from ${model.alias} at ${size.aspectRatio} (${formatSize(size)}), Job ${job.id}.${adjustment}${observedText(job.observed)}${mismatchText(job.mismatch, info.docsUrl)}${job.shortfall === undefined ? "" : ` ${job.shortfall}`}
26644
+ const output = { ...jobOutput(job), ...requestedOutput(normalised.requested) };
26645
+ const asked = requestedText(normalised.requested);
26646
+ const text = job.status === "running" ? `${jobText(job)} ${asked}` : `Saved ${job.files.length} image(s) from ${model.alias}, Job ${job.id}. ${asked}${observedText(job.observed)}${mismatchText(job.mismatch, info.docsUrl)}${job.shortfall === undefined ? "" : ` ${job.shortfall}`}
26203
26647
  ${job.files.join(`
26204
26648
  `)}`;
26205
26649
  return { output, text };
@@ -26213,37 +26657,20 @@ ${job.files.join(`
26213
26657
  const credential = await requireCredential(deps.dirs, resolved.provider);
26214
26658
  const normalised = await normaliseVideoRequest({
26215
26659
  prompt: args.prompt,
26216
- ...args.aspect_ratio === undefined ? {} : { aspectRatio: args.aspect_ratio },
26217
- ...args.duration === undefined ? {} : { duration: args.duration },
26218
- ...args.resolution === undefined ? {} : { resolution: args.resolution },
26219
26660
  ...args.first_frame === undefined ? {} : { firstFrame: args.first_frame },
26220
26661
  ...args.last_frame === undefined ? {} : { lastFrame: args.last_frame },
26221
26662
  references: args.references,
26222
- providerOptions: args.provider_options
26223
- }, model, adapter, info.capabilities, cwd);
26663
+ providerOptions: mergeOptions(model.standingOptions ?? {}, args.provider_options)
26664
+ }, model, adapter, cwd);
26224
26665
  const outputDir = args.output_path === undefined ? undefined : resolve2(cwd, args.output_path);
26225
26666
  return {
26226
26667
  model,
26227
26668
  start: (comparison) => jobs.startVideo({ ...resolved, adapter, credential, normalised, outputDir, ...comparison === undefined ? {} : { comparison } }),
26228
- hint: async () => optionsHint(info.docsUrl, await modelOptions(deps, model)),
26669
+ hint: async (kind) => failureHint(info, model.provider, await modelOptions(deps, model), kind),
26229
26670
  report: (job) => {
26230
- const { size, seconds } = normalised.request;
26231
- const output = {
26232
- ...jobOutput(job),
26233
- aspect_ratio: size.aspectRatio,
26234
- size: formatSize(size),
26235
- resolution: normalised.request.resolution ?? null,
26236
- duration: seconds,
26237
- requested_aspect_ratio: normalised.requestedAspectRatio,
26238
- aspect_ratio_adjusted: normalised.aspectRatioAdjusted,
26239
- duration_adjusted: normalised.durationAdjusted
26240
- };
26241
- const asked = `${seconds} s at ${size.aspectRatio}${normalised.request.resolution === undefined ? "" : ` ${normalised.request.resolution}`} (${formatSize(size)})`;
26242
- const adjustments = [
26243
- normalised.aspectRatioAdjusted ? `${model.alias} does not support ${normalised.requestedAspectRatio}, so the closest supported aspect ratio was used.` : "",
26244
- normalised.durationAdjusted ? `${model.alias} cannot make ${normalised.requestedDuration} s, so the closest length it can was used.` : ""
26245
- ].filter(Boolean).map((sentence) => ` ${sentence}`).join("");
26246
- const text = job.status === "running" ? `${jobText(job)} It asks ${model.alias} for ${asked}.${adjustments}` : `Saved a video from ${model.alias}, ${asked}, Job ${job.id}.${adjustments}${observedText(job.observed)}${mismatchText(job.mismatch, info.docsUrl)}
26671
+ const output = { ...jobOutput(job), ...requestedOutput(normalised.requested) };
26672
+ const asked = requestedText(normalised.requested);
26673
+ const text = job.status === "running" ? `${jobText(job)} ${asked}` : `Saved a video from ${model.alias}, Job ${job.id}. ${asked}${observedText(job.observed)}${mismatchText(job.mismatch, info.docsUrl)}
26247
26674
  ${job.files.join(`
26248
26675
  `)}`;
26249
26676
  return { output, text };
@@ -26262,68 +26689,60 @@ ${job.files.join(`
26262
26689
  };
26263
26690
  server.registerTool("generate_image", {
26264
26691
  description: "Generate images from a text prompt with one of the User's configured Models. Saves the images locally and returns their file paths.",
26265
- inputSchema: {
26692
+ inputSchema: strictObject({
26266
26693
  prompt: string2().min(1).describe("Sent to the Model verbatim."),
26267
26694
  model: string2().optional().describe("Alias of the Model to use. Omit to use the default image Model."),
26268
- n: number2().int().positive().default(1).describe("How many images to generate in this Job, up to the Model's maximum."),
26269
- aspect_ratio: _enum(aspectRatios).default("1:1").describe("Wanted aspect ratio. If the Model does not support it, the closest supported one is used and reported."),
26695
+ n: number2().int().positive().default(1).describe("How many images this Job brings back: sent as the Model's own count field, or as that many requests where it has none."),
26270
26696
  references: array(string2().min(1)).default([]).describe("Local paths of reference images to edit or draw on. Relative paths resolve against litevision's working directory."),
26271
26697
  output_path: string2().min(1).optional().describe("Folder to save the images in, e.g. the project's assets folder; created if missing. Relative paths resolve against litevision's working directory. Omit to save under a dated folder in the User's home directory."),
26272
- provider_options: record(string2(), unknown()).default({}).describe('Provider-specific request fields passed through untouched, e.g. { "quality": "high" } for OpenAI. Cannot override fields litevision sets from the other parameters.')
26273
- },
26698
+ provider_options: record(string2(), unknown()).default({}).describe(`The Model's own request fields, passed through untouched, e.g. { "size": "2048x1152", "quality": "high" } for OpenAI: see its Parameter Guide in \`list_models\`. Merged over the Model's Standing Options. Only the model id, prompt, reference image fields and response format are litevision's.`)
26699
+ }),
26274
26700
  outputSchema: imageJobShape
26275
26701
  }, async ({ model: alias, ...args }) => runSingle(async () => prepareImage(resolveModel(await loadConfig(deps.dirs), alias), args)));
26276
26702
  server.registerTool("generate_video", {
26277
26703
  description: "Generate one video from a text prompt with one of the User's configured video Models, optionally from a first and last frame or reference images. Saves the video locally and returns its file path. Videos often take minutes: the Job then keeps running in the background.",
26278
- inputSchema: {
26704
+ inputSchema: strictObject({
26279
26705
  prompt: string2().min(1).describe("Sent to the Model verbatim. Describe motion and camera movement, not only the scene."),
26280
26706
  model: string2().optional().describe("Alias of the video Model to use. Omit to use the default video Model."),
26281
- aspect_ratio: _enum(aspectRatios).optional().describe("Wanted aspect ratio. Omit for 16:9, or for the first frame's shape when `first_frame` is given. If the Model does not support it, the closest supported one is used and reported."),
26282
- duration: number2().int().positive().optional().describe("Wanted length in seconds. Omit for the Model's default. If the Model cannot make it, the closest length it can is used and reported."),
26283
- resolution: string2().min(1).optional().describe("A resolution tier the Model lists in `list_models`, e.g. 720P. Omit for the Model's default."),
26284
26707
  first_frame: string2().min(1).optional().describe("Local path of an image the video starts from."),
26285
26708
  last_frame: string2().min(1).optional().describe("Local path of an image the video ends on."),
26286
26709
  references: array(string2().min(1)).default([]).describe("Local paths of reference images for characters, objects or style. Relative paths resolve against litevision's working directory."),
26287
26710
  output_path: string2().min(1).optional().describe("Folder to save the video in; created if missing. Omit to save under a dated folder in the User's home directory."),
26288
- provider_options: record(string2(), unknown()).default({}).describe('Provider-specific request fields passed through untouched, e.g. { "generate_audio": true }. Reuse the Model\'s Known-good Options from `list_models`.')
26289
- },
26711
+ provider_options: record(string2(), unknown()).default({}).describe(`The Model's own request fields, passed through untouched, e.g. { "resolution": "1080P", "ratio": "9:16", "seconds": "10" } for Wan 2.7: see its Parameter Guide in \`list_models\`. Merged over the Model's Standing Options. Only the model id, prompt and media fields are litevision's.`)
26712
+ }),
26290
26713
  outputSchema: videoJobShape
26291
26714
  }, async ({ model: alias, ...args }) => runSingle(async () => prepareVideo(resolveModel(await loadConfig(deps.dirs), alias, "video"), args)));
26292
26715
  server.registerTool("generate_comparison", {
26293
26716
  description: `Start a Comparison: 2 to ${maxComparisonVariants} Jobs together for one request, differing in prompt, in Model, or both, so the User can choose among their results. All variants' Models must share one Modality. Start one only when the User asks for a Comparison or agrees to one: it costs as much as all its Jobs. Saves each Job's files locally and returns them under one Comparison id.`,
26294
- inputSchema: {
26295
- variants: array(object({
26717
+ inputSchema: strictObject({
26718
+ variants: array(strictObject({
26296
26719
  prompt: string2().min(1).describe("Sent to this variant's Model verbatim."),
26297
26720
  model: string2().optional().describe("Alias of this variant's Model. Omit to use the default Model of the Comparison's Modality."),
26298
- n: number2().int().positive().optional().describe("Image Comparisons only: how many images this variant's Job makes, up to its Model's maximum. Defaults to 1.")
26721
+ n: number2().int().positive().optional().describe("Image Comparisons only: how many images this variant's Job makes. Defaults to 1."),
26722
+ provider_options: record(string2(), unknown()).default({}).describe("This variant's own request fields, in its Model's own names (see its Parameter Guide), merged over its Standing Options.")
26299
26723
  })).min(2).max(maxComparisonVariants).describe(`The Jobs to start, 2 to ${maxComparisonVariants}, one per variant.`),
26300
26724
  modality: _enum(modalities).optional().describe("image or video. Omit to take it from the variants' Models; when no variant names one, image."),
26301
- aspect_ratio: _enum(aspectRatios).optional().describe("Wanted aspect ratio for every Job. Omit for 1:1 images or 16:9 videos. Each Model that does not support it uses its closest one."),
26302
26725
  references: array(string2().min(1)).default([]).describe("Local paths of reference images every Job gets. Relative paths resolve against litevision's working directory."),
26303
- output_path: string2().min(1).optional().describe("Folder to save every Job's files in; created if missing. Omit to save under a dated folder in the User's home directory."),
26304
- duration: number2().int().positive().optional().describe("Video Comparisons only: wanted length in seconds. Omit for each Model's default.")
26305
- },
26726
+ output_path: string2().min(1).optional().describe("Folder to save every Job's files in; created if missing. Omit to save under a dated folder in the User's home directory.")
26727
+ }),
26306
26728
  outputSchema: {
26307
26729
  comparison_id: string2(),
26308
26730
  modality: _enum(modalities),
26309
26731
  jobs: array(union([object(imageJobShape), object(videoJobShape), failedJobSchema])).describe('One per variant, in the order given: each as `generate_image` or `generate_video` returns it, or status "failed" with why. A "running" Job continues in the background; call `get_job` with its `job_id`.')
26310
26732
  }
26311
- }, async ({ variants, modality: wanted, aspect_ratio, references, output_path, duration }) => {
26733
+ }, async ({ variants, modality: wanted, references, output_path }) => {
26312
26734
  let prepared;
26313
26735
  let modality;
26314
26736
  try {
26315
26737
  const config = await loadConfig(deps.dirs);
26316
26738
  modality = comparisonModality(config, variants, wanted);
26317
- if (modality === "image" && duration !== undefined) {
26318
- throw new ConfigError("`duration` applies only to a video Comparison; leave it out for images.");
26319
- }
26320
26739
  if (modality === "video" && variants.some((variant) => (variant.n ?? 1) > 1)) {
26321
26740
  throw new ConfigError("A video Job makes one video, so leave `n` out of a video Comparison's variants.");
26322
26741
  }
26323
26742
  prepared = await Promise.all(variants.map((variant) => {
26324
26743
  const resolved = resolveModel(config, variant.model, modality);
26325
- const shared = { prompt: variant.prompt, references, provider_options: {}, ...output_path === undefined ? {} : { output_path } };
26326
- return modality === "video" ? prepareVideo(resolved, { ...shared, ...aspect_ratio === undefined ? {} : { aspect_ratio }, ...duration === undefined ? {} : { duration } }) : prepareImage(resolved, { ...shared, n: variant.n ?? 1, aspect_ratio: aspect_ratio ?? "1:1" });
26744
+ const shared = { prompt: variant.prompt, references, provider_options: variant.provider_options, ...output_path === undefined ? {} : { output_path } };
26745
+ return modality === "video" ? prepareVideo(resolved, shared) : prepareImage(resolved, { ...shared, n: variant.n ?? 1 });
26327
26746
  }));
26328
26747
  } catch (error) {
26329
26748
  return failure2(error, undefined);
@@ -26363,21 +26782,24 @@ ${job.files.join(`
26363
26782
  model: string2().describe("Alias of the Model that ran the Job."),
26364
26783
  files: array(string2()).describe("Paths of the saved images or video; empty while the Job is running."),
26365
26784
  shortfall: string2().optional().describe("Present when fewer images than asked for were saved: how many, and why."),
26785
+ requested_shape: requestedShapeSchema.optional(),
26786
+ requested_duration: requestedDurationSchema.optional(),
26787
+ requested_resolution: requestedResolutionSchema.optional(),
26366
26788
  observed: observedSchema,
26367
26789
  mismatch: mismatchSchema
26368
26790
  }
26369
26791
  }, async ({ job_id }) => {
26370
26792
  try {
26371
26793
  const job = await jobs.get(job_id);
26372
- const docsUrl = job.status === "completed" && job.mismatch !== undefined ? (await jobDocs(deps.dirs, job_id))?.docsUrl ?? "the Model's docs link in `list_models`" : "";
26794
+ const docsUrl = job.status === "completed" && job.mismatch !== undefined ? (await jobDocs(infoDeps, job_id))?.known.docsUrl ?? "the Model's docs link in `list_models`" : "";
26373
26795
  const text = job.status === "running" ? jobText(job) : `Job ${job.id} completed: saved ${job.files.length} file(s) from ${job.model}.${observedText(job.observed)}${mismatchText(job.mismatch, docsUrl)}${job.shortfall === undefined ? "" : ` ${job.shortfall}`}
26374
26796
  ${job.files.join(`
26375
26797
  `)}`;
26376
26798
  return { content: [{ type: "text", text }], structuredContent: jobOutput(job) };
26377
26799
  } catch (error) {
26378
- return failure2(error, async () => {
26379
- const docs = await jobDocs(deps.dirs, job_id);
26380
- return docs === undefined ? "" : optionsHint(docs.docsUrl, docs.options);
26800
+ return failure2(error, async (kind) => {
26801
+ const docs = await jobDocs(infoDeps, job_id);
26802
+ return docs === undefined ? "" : failureHint(docs.known, docs.provider, docs.options, kind);
26381
26803
  });
26382
26804
  }
26383
26805
  });
@@ -26437,7 +26859,7 @@ ${job.files.join(`
26437
26859
  }
26438
26860
  });
26439
26861
  server.registerTool("list_models", {
26440
- description: "List the User's configured Models: Alias, Modality (image or video), Provider, Protocol, capabilities, Model Notes, the Known-good Options for `provider_options` and a docs link, and which Models are the defaults. Use it to choose a Model when the request needs something the default may lack. Pass `model` for one Model in detail, with the values of its Known-good and suspect options, its five most recent Ratings, the Picks of up to five Comparisons it took part in (most recently picked first), and the Provider's parameter schema.",
26862
+ description: "List the User's configured Models: Alias, Modality (image or video), Provider, Protocol, Model Notes, Standing Options, a summary of each Model's Parameter Guide, the Known-good Options for `provider_options` and a docs link, and which Models are the defaults. Use it to choose a Model by what it accepts and costs. Pass `model` for one Model in detail: its full Parameter Guide (or the Provider's own parameter schema, which wins), the values of its Known-good and suspect options, its five most recent Ratings, and the Picks of up to five Comparisons it took part in (most recently picked first).",
26441
26863
  inputSchema: {
26442
26864
  model: string2().optional().describe("Alias of one Model to describe in detail. Omit to list every Model.")
26443
26865
  },
@@ -26533,19 +26955,6 @@ Now: ${change.notes || "(none)"}`;
26533
26955
  const text = "Opened the litevision settings page in the User's browser. Ask the User to finish setting up there and tell you when they are done. Never ask the User for a Credential in the conversation.";
26534
26956
  return { content: [{ type: "text", text }] };
26535
26957
  });
26536
- server.registerPrompt("settings", {
26537
- title: "litevision settings",
26538
- description: "Open litevision's settings page to add Providers, enter Credentials and set up Models."
26539
- }, async () => {
26540
- let text;
26541
- try {
26542
- await settings.open();
26543
- text = "I have already opened litevision's settings page in my browser. Do not call `open_settings`. Wait until I say I am done there. Never ask me for a Credential in the conversation.";
26544
- } catch {
26545
- text = "I tried to open litevision's settings page, but it could not open my browser. Tell me to run `litevision settings` in a terminal, which opens the page and prints its address.";
26546
- }
26547
- return { messages: [{ role: "user", content: { type: "text", text } }] };
26548
- });
26549
26958
  return server;
26550
26959
  }
26551
26960