litevision 0.1.1 → 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.
- package/README.md +27 -12
- package/dist/cli.js +2643 -742
- package/package.json +4 -2
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",
|
|
@@ -14154,6 +14162,7 @@ function defaultDirs(home = homedir()) {
|
|
|
14154
14162
|
var fetchTransport = (input, init) => fetch(input, init);
|
|
14155
14163
|
|
|
14156
14164
|
// src/server.ts
|
|
14165
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
14157
14166
|
import { resolve as resolve2 } from "node:path";
|
|
14158
14167
|
|
|
14159
14168
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -21697,6 +21706,19 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
21697
21706
|
// src/config.ts
|
|
21698
21707
|
import { readFile } from "node:fs/promises";
|
|
21699
21708
|
import { join as join2 } from "node:path";
|
|
21709
|
+
|
|
21710
|
+
// src/protocols/protocol.ts
|
|
21711
|
+
var modalities = ["image", "video"];
|
|
21712
|
+
|
|
21713
|
+
class ProviderError extends Error {
|
|
21714
|
+
kind;
|
|
21715
|
+
constructor(kind, message) {
|
|
21716
|
+
super(message);
|
|
21717
|
+
this.kind = kind;
|
|
21718
|
+
}
|
|
21719
|
+
}
|
|
21720
|
+
|
|
21721
|
+
// src/config.ts
|
|
21700
21722
|
var providerSchema = object({
|
|
21701
21723
|
id: string2().min(1),
|
|
21702
21724
|
baseUrl: string2().url()
|
|
@@ -21713,13 +21735,16 @@ var modelSchema = object({
|
|
|
21713
21735
|
provider: string2().min(1),
|
|
21714
21736
|
modelId: string2().min(1),
|
|
21715
21737
|
protocol: string2().min(1),
|
|
21738
|
+
modality: _enum(modalities).default("image"),
|
|
21716
21739
|
notes: string2().optional(),
|
|
21717
|
-
notesHistory: notesHistorySchema.optional()
|
|
21740
|
+
notesHistory: notesHistorySchema.optional(),
|
|
21741
|
+
standingOptions: record(string2(), unknown()).optional()
|
|
21718
21742
|
});
|
|
21719
21743
|
var configSchema = object({
|
|
21720
21744
|
providers: array(providerSchema).default([]),
|
|
21721
21745
|
models: array(modelSchema).default([]),
|
|
21722
|
-
defaultImageModel: string2().optional()
|
|
21746
|
+
defaultImageModel: string2().optional(),
|
|
21747
|
+
defaultVideoModel: string2().optional()
|
|
21723
21748
|
});
|
|
21724
21749
|
var credentialsSchema = record(string2(), object({ apiKey: string2().min(1) }));
|
|
21725
21750
|
|
|
@@ -21760,23 +21785,31 @@ async function requireCredential(dirs, provider) {
|
|
|
21760
21785
|
throw needsSettings(`Provider "${provider.id}" has no Credential saved.`);
|
|
21761
21786
|
return credential;
|
|
21762
21787
|
}
|
|
21763
|
-
|
|
21764
|
-
|
|
21765
|
-
|
|
21788
|
+
var defaultKey = { image: "defaultImageModel", video: "defaultVideoModel" };
|
|
21789
|
+
function defaultModelAlias(config, modality = "image") {
|
|
21790
|
+
const wanted = config[defaultKey[modality]];
|
|
21791
|
+
return config.models.some((model) => model.alias === wanted && model.modality === modality) ? wanted : undefined;
|
|
21766
21792
|
}
|
|
21767
|
-
function resolveModel(config, alias) {
|
|
21768
|
-
|
|
21769
|
-
|
|
21770
|
-
const
|
|
21771
|
-
|
|
21793
|
+
function resolveModel(config, alias, modality = "image") {
|
|
21794
|
+
const ofModality = config.models.filter((model) => model.modality === modality);
|
|
21795
|
+
const wanted = alias ?? config[defaultKey[modality]];
|
|
21796
|
+
const model = config.models.find((candidate) => candidate.alias === wanted);
|
|
21797
|
+
if (ofModality.length === 0 && (alias === undefined || model === undefined))
|
|
21798
|
+
throw needsSettings(`litevision has no ${modality} Model configured yet.`);
|
|
21799
|
+
const known = ofModality.map((candidate) => candidate.alias).join(", ");
|
|
21772
21800
|
if (wanted === undefined) {
|
|
21773
|
-
throw needsSettings(`No default
|
|
21801
|
+
throw needsSettings(`No default ${modality} Model is set. Pass \`model\` with one of: ${known}. Or:`);
|
|
21774
21802
|
}
|
|
21775
|
-
const model = config.models.find((candidate) => candidate.alias === wanted);
|
|
21776
21803
|
if (model === undefined) {
|
|
21777
21804
|
if (alias === undefined)
|
|
21778
|
-
throw needsSettings(`The default
|
|
21779
|
-
throw new ConfigError(`Unknown Model Alias "${wanted}". Known Aliases: ${known}.`);
|
|
21805
|
+
throw needsSettings(`The default ${modality} Model "${wanted}" does not exist. Known Aliases: ${known}.`);
|
|
21806
|
+
throw new ConfigError(`Unknown Model Alias "${wanted}". Known ${modality} Model Aliases: ${known}.`);
|
|
21807
|
+
}
|
|
21808
|
+
if (model.modality !== modality) {
|
|
21809
|
+
if (alias === undefined)
|
|
21810
|
+
throw needsSettings(`The default ${modality} Model "${wanted}" makes ${model.modality}s. Pass \`model\` with one of: ${known}. Or:`);
|
|
21811
|
+
const tool = model.modality === "video" ? "generate_video" : "generate_image";
|
|
21812
|
+
throw new ConfigError(`Model "${model.alias}" generates ${model.modality}s, not ${modality}s. Use \`${tool}\` for it${ofModality.length === 0 ? "" : `, or one of these ${modality} Models: ${known}`}.`);
|
|
21780
21813
|
}
|
|
21781
21814
|
const provider = config.providers.find((candidate) => candidate.id === model.provider);
|
|
21782
21815
|
if (provider === undefined) {
|
|
@@ -21790,6 +21823,9 @@ import { appendFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
|
21790
21823
|
import { join as join3 } from "node:path";
|
|
21791
21824
|
var verdicts = ["good", "bad"];
|
|
21792
21825
|
var ratingReasonMaxLength = 200;
|
|
21826
|
+
function ratingOf({ ratings }, result) {
|
|
21827
|
+
return ratings.find((rating) => rating.result === result);
|
|
21828
|
+
}
|
|
21793
21829
|
|
|
21794
21830
|
class RatingError extends Error {
|
|
21795
21831
|
kind;
|
|
@@ -21799,6 +21835,15 @@ class RatingError extends Error {
|
|
|
21799
21835
|
this.name = "RatingError";
|
|
21800
21836
|
}
|
|
21801
21837
|
}
|
|
21838
|
+
|
|
21839
|
+
class PickError extends Error {
|
|
21840
|
+
kind;
|
|
21841
|
+
constructor(kind, message) {
|
|
21842
|
+
super(message);
|
|
21843
|
+
this.kind = kind;
|
|
21844
|
+
this.name = "PickError";
|
|
21845
|
+
}
|
|
21846
|
+
}
|
|
21802
21847
|
var historyPath = (dirs) => join3(dirs.data, "jobs.jsonl");
|
|
21803
21848
|
async function appendHistory(dirs, record) {
|
|
21804
21849
|
await mkdir(dirs.data, { recursive: true });
|
|
@@ -21812,8 +21857,11 @@ async function readJob(dirs, id) {
|
|
|
21812
21857
|
return (await readJobs(dirs)).find((job) => job.id === id);
|
|
21813
21858
|
}
|
|
21814
21859
|
async function readRatedJobs(dirs) {
|
|
21815
|
-
const { jobs, ratings } = await readRecords(dirs);
|
|
21816
|
-
return jobs.map((job) =>
|
|
21860
|
+
const { jobs, ratings, picks } = await readRecords(dirs);
|
|
21861
|
+
return jobs.map((job) => {
|
|
21862
|
+
const pick = job.comparison === undefined ? undefined : picks.get(job.comparison);
|
|
21863
|
+
return { job, ratings: [...ratings.get(job.id)?.values() ?? []], ...pick === undefined ? {} : { pick } };
|
|
21864
|
+
}).sort((a, b) => Date.parse(b.job.createdAt) - Date.parse(a.job.createdAt));
|
|
21817
21865
|
}
|
|
21818
21866
|
async function rateJob(dirs, rating, now) {
|
|
21819
21867
|
const reason = rating.reason.replace(/\s+/g, " ").trim();
|
|
@@ -21825,15 +21873,56 @@ async function rateJob(dirs, rating, now) {
|
|
|
21825
21873
|
const job = await readJob(dirs, rating.jobId);
|
|
21826
21874
|
if (job === undefined)
|
|
21827
21875
|
throw new RatingError("unknown-job", `litevision has no Job "${rating.jobId}".`);
|
|
21876
|
+
const { result } = rating;
|
|
21877
|
+
const problem = whyNotChosen(job, result, "rate");
|
|
21878
|
+
if (problem !== undefined)
|
|
21879
|
+
throw new RatingError(problem.kind, problem.message);
|
|
21880
|
+
const record = {
|
|
21881
|
+
type: "rating",
|
|
21882
|
+
jobId: job.id,
|
|
21883
|
+
...result === undefined ? {} : { result },
|
|
21884
|
+
verdict: rating.verdict,
|
|
21885
|
+
reason,
|
|
21886
|
+
ratedAt: now.toISOString()
|
|
21887
|
+
};
|
|
21888
|
+
await appendHistory(dirs, record);
|
|
21889
|
+
return record;
|
|
21890
|
+
}
|
|
21891
|
+
async function pickResult(dirs, pick, now) {
|
|
21892
|
+
const ofComparison = (await readJobs(dirs)).filter((job) => job.comparison === pick.comparison);
|
|
21893
|
+
if (ofComparison.length === 0)
|
|
21894
|
+
throw new PickError("unknown-comparison", `litevision has no Comparison "${pick.comparison}".`);
|
|
21895
|
+
const job = ofComparison.find((job) => job.id === pick.jobId);
|
|
21896
|
+
if (job === undefined) {
|
|
21897
|
+
const ids = ofComparison.map((job) => job.id).join(", ");
|
|
21898
|
+
throw new PickError("not-in-comparison", `Job "${pick.jobId}" is not part of Comparison ${pick.comparison}, whose Jobs are ${ids}.`);
|
|
21899
|
+
}
|
|
21900
|
+
const { result } = pick;
|
|
21901
|
+
const problem = whyNotChosen(job, result, "pick");
|
|
21902
|
+
if (problem !== undefined)
|
|
21903
|
+
throw new PickError(problem.kind, problem.message);
|
|
21904
|
+
const record = {
|
|
21905
|
+
type: "pick",
|
|
21906
|
+
comparison: pick.comparison,
|
|
21907
|
+
jobId: job.id,
|
|
21908
|
+
...result === undefined ? {} : { result },
|
|
21909
|
+
pickedAt: now.toISOString()
|
|
21910
|
+
};
|
|
21911
|
+
await appendHistory(dirs, record);
|
|
21912
|
+
return record;
|
|
21913
|
+
}
|
|
21914
|
+
function whyNotChosen(job, result, act) {
|
|
21828
21915
|
if (job.outcome === "running") {
|
|
21829
|
-
|
|
21916
|
+
return { kind: "not-completed", message: `Job ${job.id} is still running; it can be ${act === "rate" ? "rated" : "picked"} once it has completed.` };
|
|
21830
21917
|
}
|
|
21831
21918
|
if (job.outcome === "failed") {
|
|
21832
|
-
|
|
21919
|
+
return { kind: "not-completed", message: `Job ${job.id} failed and saved no images, so there is nothing to ${act}.` };
|
|
21833
21920
|
}
|
|
21834
|
-
|
|
21835
|
-
|
|
21836
|
-
|
|
21921
|
+
if (result !== undefined && !(Number.isInteger(result) && result >= 1 && result <= job.files.length)) {
|
|
21922
|
+
const count = job.files.length === 1 ? "1 result" : `${job.files.length} results`;
|
|
21923
|
+
return { kind: "invalid", message: `Job ${job.id} has ${count}, numbered from 1 in the order of its files.` };
|
|
21924
|
+
}
|
|
21925
|
+
return;
|
|
21837
21926
|
}
|
|
21838
21927
|
async function readRecords(dirs) {
|
|
21839
21928
|
let text;
|
|
@@ -21841,11 +21930,12 @@ async function readRecords(dirs) {
|
|
|
21841
21930
|
text = await readFile2(historyPath(dirs), "utf8");
|
|
21842
21931
|
} catch (error) {
|
|
21843
21932
|
if (error.code === "ENOENT")
|
|
21844
|
-
return { jobs: [], ratings: new Map };
|
|
21933
|
+
return { jobs: [], ratings: new Map, picks: new Map };
|
|
21845
21934
|
throw error;
|
|
21846
21935
|
}
|
|
21847
21936
|
const jobs = new Map;
|
|
21848
21937
|
const ratings = new Map;
|
|
21938
|
+
const picks = new Map;
|
|
21849
21939
|
for (const line of text.split(`
|
|
21850
21940
|
`)) {
|
|
21851
21941
|
if (line.trim() === "")
|
|
@@ -21857,7 +21947,14 @@ async function readRecords(dirs) {
|
|
|
21857
21947
|
continue;
|
|
21858
21948
|
}
|
|
21859
21949
|
if (isRatingRecord(record)) {
|
|
21860
|
-
ratings.
|
|
21950
|
+
const ofJob = ratings.get(record.jobId) ?? new Map;
|
|
21951
|
+
ratings.set(record.jobId, ofJob);
|
|
21952
|
+
ofJob.delete(record.result ?? 0);
|
|
21953
|
+
ofJob.set(record.result ?? 0, record);
|
|
21954
|
+
continue;
|
|
21955
|
+
}
|
|
21956
|
+
if (isPickRecord(record)) {
|
|
21957
|
+
picks.set(record.comparison, record);
|
|
21861
21958
|
continue;
|
|
21862
21959
|
}
|
|
21863
21960
|
if (!isJobRecord(record))
|
|
@@ -21865,7 +21962,7 @@ async function readRecords(dirs) {
|
|
|
21865
21962
|
jobs.delete(record.id);
|
|
21866
21963
|
jobs.set(record.id, record);
|
|
21867
21964
|
}
|
|
21868
|
-
return { jobs: [...jobs.values()], ratings };
|
|
21965
|
+
return { jobs: [...jobs.values()], ratings, picks };
|
|
21869
21966
|
}
|
|
21870
21967
|
function isJobRecord(value) {
|
|
21871
21968
|
const record = value;
|
|
@@ -21873,18 +21970,73 @@ function isJobRecord(value) {
|
|
|
21873
21970
|
}
|
|
21874
21971
|
function isRatingRecord(value) {
|
|
21875
21972
|
const record = value;
|
|
21876
|
-
return typeof record === "object" && record !== null && record.type === "rating" && typeof record.jobId === "string" && verdicts.includes(record.verdict) && typeof record.reason === "string";
|
|
21973
|
+
return typeof record === "object" && record !== null && record.type === "rating" && typeof record.jobId === "string" && verdicts.includes(record.verdict) && typeof record.reason === "string" && (record.result === undefined || Number.isInteger(record.result) && record.result >= 1);
|
|
21974
|
+
}
|
|
21975
|
+
function isPickRecord(value) {
|
|
21976
|
+
const record = value;
|
|
21977
|
+
return typeof record === "object" && record !== null && record.type === "pick" && typeof record.comparison === "string" && typeof record.jobId === "string" && typeof record.pickedAt === "string" && (record.result === undefined || Number.isInteger(record.result) && record.result >= 1);
|
|
21978
|
+
}
|
|
21979
|
+
function ranOn(job, model) {
|
|
21980
|
+
return job.provider === model.provider && job.protocol === model.protocol && job.modelId === model.modelId;
|
|
21981
|
+
}
|
|
21982
|
+
function recentRatings(jobs, model, limit) {
|
|
21983
|
+
return jobs.filter(({ job }) => ranOn(job, model)).flatMap(({ job, ratings }) => ratings.map((rating) => ({ rating, job }))).sort((a, b) => Date.parse(b.rating.ratedAt) - Date.parse(a.rating.ratedAt)).slice(0, limit);
|
|
21984
|
+
}
|
|
21985
|
+
function recentPicks(jobs, model, limit) {
|
|
21986
|
+
const byId = new Map(jobs.map(({ job }) => [job.id, job]));
|
|
21987
|
+
const entries = jobs.flatMap(({ job, pick }) => {
|
|
21988
|
+
if (pick === undefined || !ranOn(job, model))
|
|
21989
|
+
return [];
|
|
21990
|
+
const picked = byId.get(pick.jobId);
|
|
21991
|
+
return [{ pick, job, ...picked === undefined ? {} : { picked } }];
|
|
21992
|
+
}).sort((a, b) => Date.parse(b.pick.pickedAt) - Date.parse(a.pick.pickedAt));
|
|
21993
|
+
const comparisons = new Set;
|
|
21994
|
+
return entries.filter(({ pick }) => {
|
|
21995
|
+
if (comparisons.size === limit && !comparisons.has(pick.comparison))
|
|
21996
|
+
return false;
|
|
21997
|
+
comparisons.add(pick.comparison);
|
|
21998
|
+
return true;
|
|
21999
|
+
});
|
|
22000
|
+
}
|
|
22001
|
+
function optionsRating({ job, ratings }) {
|
|
22002
|
+
return ratings.findLast((rating) => rating.result === undefined || rating.result === 1 && job.files.length === 1);
|
|
21877
22003
|
}
|
|
21878
|
-
function
|
|
21879
|
-
const
|
|
21880
|
-
|
|
22004
|
+
function optionsRecord(jobs, model) {
|
|
22005
|
+
const state = new Map;
|
|
22006
|
+
const ratedBad = new Map;
|
|
22007
|
+
const ordered = [...jobs].sort((a, b) => Date.parse(a.job.createdAt) - Date.parse(b.job.createdAt));
|
|
22008
|
+
for (const ratedJob of ordered) {
|
|
22009
|
+
const { job } = ratedJob;
|
|
21881
22010
|
if (job.outcome !== "completed")
|
|
21882
22011
|
continue;
|
|
21883
|
-
|
|
22012
|
+
const rating = optionsRating(ratedJob);
|
|
22013
|
+
if (!ranOn(job, model))
|
|
21884
22014
|
continue;
|
|
21885
|
-
Object.
|
|
22015
|
+
for (const [key, value] of Object.entries(job.parameters?.providerOptions ?? {})) {
|
|
22016
|
+
const asJson = JSON.stringify(value);
|
|
22017
|
+
const badValues = ratedBad.get(key) ?? new Map;
|
|
22018
|
+
ratedBad.set(key, badValues);
|
|
22019
|
+
if (rating?.verdict === "bad")
|
|
22020
|
+
badValues.set(asJson, { value, cause: "rating", reason: rating.reason, jobId: job.id });
|
|
22021
|
+
if (rating?.verdict === "good")
|
|
22022
|
+
badValues.delete(asJson);
|
|
22023
|
+
const verdict = badValues.get(asJson);
|
|
22024
|
+
if (verdict !== undefined)
|
|
22025
|
+
state.set(key, { value, suspect: verdict });
|
|
22026
|
+
else if (job.mismatch !== undefined && rating?.verdict !== "good") {
|
|
22027
|
+
state.set(key, { value, suspect: { value, cause: "mismatch", reason: job.mismatch, jobId: job.id } });
|
|
22028
|
+
} else
|
|
22029
|
+
state.set(key, { value });
|
|
22030
|
+
}
|
|
21886
22031
|
}
|
|
21887
|
-
|
|
22032
|
+
const record = { knownGood: {}, suspect: {} };
|
|
22033
|
+
for (const [key, { value, suspect }] of state) {
|
|
22034
|
+
if (suspect === undefined)
|
|
22035
|
+
record.knownGood[key] = value;
|
|
22036
|
+
else
|
|
22037
|
+
record.suspect[key] = suspect;
|
|
22038
|
+
}
|
|
22039
|
+
return record;
|
|
21888
22040
|
}
|
|
21889
22041
|
|
|
21890
22042
|
// src/jobs.ts
|
|
@@ -21929,18 +22081,223 @@ async function readReferences(paths, cwd) {
|
|
|
21929
22081
|
}));
|
|
21930
22082
|
}
|
|
21931
22083
|
|
|
21932
|
-
// src/
|
|
21933
|
-
|
|
21934
|
-
|
|
21935
|
-
|
|
22084
|
+
// src/media.ts
|
|
22085
|
+
function fileTypeOf(bytes) {
|
|
22086
|
+
const image = imageTypeOf(bytes);
|
|
22087
|
+
if (image !== undefined)
|
|
22088
|
+
return image;
|
|
22089
|
+
if (ascii(bytes, 4, 8) === "ftyp") {
|
|
22090
|
+
const brand = ascii(bytes, 8, 12);
|
|
22091
|
+
if (brand === "avif" || brand === "avis")
|
|
22092
|
+
return { extension: "avif", mimeType: "image/avif" };
|
|
22093
|
+
if (["heic", "heix", "heim", "heis", "mif1", "msf1"].includes(brand))
|
|
22094
|
+
return { extension: "heic", mimeType: "image/heic" };
|
|
22095
|
+
return brand.startsWith("qt") ? { extension: "mov", mimeType: "video/quicktime" } : { extension: "mp4", mimeType: "video/mp4" };
|
|
22096
|
+
}
|
|
22097
|
+
if (bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163)
|
|
22098
|
+
return { extension: "webm", mimeType: "video/webm" };
|
|
22099
|
+
return;
|
|
22100
|
+
}
|
|
22101
|
+
function mediaFactsOf(bytes) {
|
|
22102
|
+
try {
|
|
22103
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
22104
|
+
switch (fileTypeOf(bytes)?.extension) {
|
|
22105
|
+
case "png":
|
|
22106
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
22107
|
+
case "gif":
|
|
22108
|
+
return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
|
|
22109
|
+
case "jpg":
|
|
22110
|
+
return jpegSize(bytes, view);
|
|
22111
|
+
case "webp":
|
|
22112
|
+
return webpSize(bytes, view);
|
|
22113
|
+
case "mp4":
|
|
22114
|
+
case "mov":
|
|
22115
|
+
return mp4Facts(bytes, view);
|
|
22116
|
+
default:
|
|
22117
|
+
return;
|
|
22118
|
+
}
|
|
22119
|
+
} catch {
|
|
22120
|
+
return;
|
|
22121
|
+
}
|
|
22122
|
+
}
|
|
22123
|
+
function ascii(bytes, start, end) {
|
|
22124
|
+
return String.fromCharCode(...bytes.subarray(start, end));
|
|
22125
|
+
}
|
|
22126
|
+
function jpegSize(bytes, view) {
|
|
22127
|
+
let offset = 2;
|
|
22128
|
+
let turned = false;
|
|
22129
|
+
while (offset + 9 < bytes.length) {
|
|
22130
|
+
if (bytes[offset] !== 255)
|
|
22131
|
+
return;
|
|
22132
|
+
const marker = bytes[offset + 1];
|
|
22133
|
+
if (marker === 225 && ascii(bytes, offset + 4, offset + 10) === "Exif\x00\x00")
|
|
22134
|
+
turned = exifTurnsQuarter(view, offset + 10);
|
|
22135
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
22136
|
+
const width = view.getUint16(offset + 7);
|
|
22137
|
+
const height = view.getUint16(offset + 5);
|
|
22138
|
+
return turned ? { width: height, height: width } : { width, height };
|
|
22139
|
+
}
|
|
22140
|
+
offset += 2 + view.getUint16(offset + 2);
|
|
22141
|
+
}
|
|
22142
|
+
return;
|
|
22143
|
+
}
|
|
22144
|
+
function exifTurnsQuarter(view, tiff) {
|
|
22145
|
+
try {
|
|
22146
|
+
const little = view.getUint16(tiff) === 18761;
|
|
22147
|
+
const ifd = tiff + view.getUint32(tiff + 4, little);
|
|
22148
|
+
const entries = view.getUint16(ifd, little);
|
|
22149
|
+
for (let index = 0;index < entries; index++) {
|
|
22150
|
+
const entry = ifd + 2 + index * 12;
|
|
22151
|
+
if (view.getUint16(entry, little) !== 274)
|
|
22152
|
+
continue;
|
|
22153
|
+
const orientation = view.getUint16(entry + 8, little);
|
|
22154
|
+
return orientation >= 5 && orientation <= 8;
|
|
22155
|
+
}
|
|
22156
|
+
} catch {}
|
|
22157
|
+
return false;
|
|
22158
|
+
}
|
|
22159
|
+
function webpSize(bytes, view) {
|
|
22160
|
+
const chunk = ascii(bytes, 12, 16);
|
|
22161
|
+
if (chunk === "VP8X") {
|
|
22162
|
+
const read24 = (at) => bytes[at] | bytes[at + 1] << 8 | bytes[at + 2] << 16;
|
|
22163
|
+
return { width: read24(24) + 1, height: read24(27) + 1 };
|
|
22164
|
+
}
|
|
22165
|
+
if (chunk === "VP8 ")
|
|
22166
|
+
return { width: view.getUint16(26, true) & 16383, height: view.getUint16(28, true) & 16383 };
|
|
22167
|
+
if (chunk === "VP8L") {
|
|
22168
|
+
const bits = view.getUint32(21, true);
|
|
22169
|
+
return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
|
|
22170
|
+
}
|
|
22171
|
+
return;
|
|
22172
|
+
}
|
|
22173
|
+
function mp4Facts(bytes, view) {
|
|
22174
|
+
let seconds;
|
|
22175
|
+
let size;
|
|
22176
|
+
const walk = (start, end) => {
|
|
22177
|
+
let offset = start;
|
|
22178
|
+
while (offset + 8 <= end) {
|
|
22179
|
+
let length = view.getUint32(offset);
|
|
22180
|
+
const type = ascii(bytes, offset + 4, offset + 8);
|
|
22181
|
+
let header = 8;
|
|
22182
|
+
if (length === 1) {
|
|
22183
|
+
length = Number(view.getBigUint64(offset + 8));
|
|
22184
|
+
header = 16;
|
|
22185
|
+
} else if (length === 0) {
|
|
22186
|
+
length = end - offset;
|
|
22187
|
+
}
|
|
22188
|
+
if (length < header)
|
|
22189
|
+
return;
|
|
22190
|
+
const body = offset + header;
|
|
22191
|
+
if (type === "moov" || type === "trak")
|
|
22192
|
+
walk(body, Math.min(offset + length, end));
|
|
22193
|
+
if (type === "mvhd") {
|
|
22194
|
+
const version = bytes[body];
|
|
22195
|
+
const timescale = view.getUint32(body + (version === 1 ? 20 : 12));
|
|
22196
|
+
const duration = version === 1 ? Number(view.getBigUint64(body + 24)) : view.getUint32(body + 16);
|
|
22197
|
+
if (timescale > 0 && duration > 0)
|
|
22198
|
+
seconds = duration / timescale;
|
|
22199
|
+
}
|
|
22200
|
+
if (type === "tkhd" && size === undefined) {
|
|
22201
|
+
const at = body + (bytes[body] === 1 ? 88 : 76);
|
|
22202
|
+
const width = view.getUint32(at) / 65536;
|
|
22203
|
+
const height = view.getUint32(at + 4) / 65536;
|
|
22204
|
+
if (width >= 1 && height >= 1)
|
|
22205
|
+
size = { width: Math.round(width), height: Math.round(height) };
|
|
22206
|
+
}
|
|
22207
|
+
offset += length;
|
|
22208
|
+
}
|
|
22209
|
+
};
|
|
22210
|
+
walk(0, bytes.length);
|
|
22211
|
+
if (size === undefined)
|
|
22212
|
+
return;
|
|
22213
|
+
return { ...size, ...seconds === undefined ? {} : { seconds: Math.round(seconds * 100) / 100 } };
|
|
21936
22214
|
}
|
|
21937
22215
|
|
|
21938
|
-
|
|
21939
|
-
|
|
21940
|
-
|
|
21941
|
-
|
|
21942
|
-
|
|
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(", ")}.`;
|
|
22219
|
+
}
|
|
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
|
+
`);
|
|
22228
|
+
}
|
|
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
|
+
};
|
|
22237
|
+
}
|
|
22238
|
+
function guideFor(entries, modelId) {
|
|
22239
|
+
const bare = modelId.replace(/^.*\//, "");
|
|
22240
|
+
return entries.find((entry) => entry.matches.test(modelId) || entry.matches.test(bare))?.guide;
|
|
22241
|
+
}
|
|
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;
|
|
22247
|
+
return {
|
|
22248
|
+
shape: guide.shapeFields,
|
|
22249
|
+
...guide.durationField === undefined ? {} : { duration: guide.durationField },
|
|
22250
|
+
...fallback.resolution === undefined ? {} : { resolution: fallback.resolution }
|
|
22251
|
+
};
|
|
22252
|
+
}
|
|
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}`;
|
|
21943
22285
|
}
|
|
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) {
|
|
22296
|
+
return {
|
|
22297
|
+
shape: readShape(fields.shape, body),
|
|
22298
|
+
resolution: readResolution(fields.resolution, body),
|
|
22299
|
+
...modality === "video" ? { seconds: readSeconds(fields.duration, body) } : {}
|
|
22300
|
+
};
|
|
21944
22301
|
}
|
|
21945
22302
|
|
|
21946
22303
|
// src/protocols/errors.ts
|
|
@@ -21953,6 +22310,7 @@ var explanations = {
|
|
|
21953
22310
|
refusal: "refused the request on content grounds. Rephrase the prompt, change the reference images, or choose another Model.",
|
|
21954
22311
|
rate_limit: "is rate limiting requests. Wait a little and try again, or choose another Model.",
|
|
21955
22312
|
unsupported: "does not support this request. Choose another Model or change the request.",
|
|
22313
|
+
rejected_option: "rejected an option in the request. Fix it using what the Provider said and the Model's docs, and try again; if it names the prompt or the number of images, change that instead.",
|
|
21956
22314
|
other: "failed to complete the Job."
|
|
21957
22315
|
};
|
|
21958
22316
|
function providerError(kind, providerId, detail) {
|
|
@@ -21977,25 +22335,328 @@ function kindOfStatus(status) {
|
|
|
21977
22335
|
return "rate_limit";
|
|
21978
22336
|
return;
|
|
21979
22337
|
}
|
|
22338
|
+
function namedParameters(response) {
|
|
22339
|
+
if (response.status !== 400 && response.status !== 422)
|
|
22340
|
+
return [];
|
|
22341
|
+
const message = response.message ?? "";
|
|
22342
|
+
const mentioned = (response.options ?? []).filter((name) => new RegExp(`(^|[^\\w])${escapeRegExp(name)}($|[^\\w])`).test(message));
|
|
22343
|
+
return [...response.param ? [response.param] : [], ...mentioned];
|
|
22344
|
+
}
|
|
22345
|
+
function escapeRegExp(text) {
|
|
22346
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22347
|
+
}
|
|
21980
22348
|
function classifyProviderError(response) {
|
|
21981
22349
|
const codes = (response.codes ?? []).filter((code) => typeof code === "string");
|
|
21982
|
-
|
|
22350
|
+
const named = namedParameters(response);
|
|
22351
|
+
const wording = named.reduce((text, name) => text.split(name).join(""), response.message ?? "");
|
|
22352
|
+
return matchKind(codes) ?? kindOfStatus(response.status) ?? matchKind([wording]) ?? (named.length > 0 ? "rejected_option" : "other");
|
|
21983
22353
|
}
|
|
21984
22354
|
function httpProviderError(providerId, response) {
|
|
21985
22355
|
const detail = response.message === undefined ? `HTTP ${response.status}` : `HTTP ${response.status}: ${response.message}`;
|
|
21986
22356
|
return providerError(classifyProviderError(response), providerId, detail);
|
|
21987
22357
|
}
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
21991
|
-
|
|
21992
|
-
|
|
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
|
+
`));
|
|
21993
22590
|
}
|
|
22591
|
+
parts.push(encoder.encode(`--${boundary}--\r
|
|
22592
|
+
`));
|
|
22593
|
+
return { body: Buffer.concat(parts), contentType: `multipart/form-data; boundary=${boundary}` };
|
|
22594
|
+
}
|
|
22595
|
+
|
|
22596
|
+
// src/protocols/send.ts
|
|
22597
|
+
var maxRetries = 2;
|
|
22598
|
+
async function sendToProvider(http, providerId, url, init, { context, poll = false }) {
|
|
22599
|
+
for (let attempt = 1;; attempt++) {
|
|
22600
|
+
const last = attempt > maxRetries;
|
|
22601
|
+
try {
|
|
22602
|
+
const response = await http(url, init);
|
|
22603
|
+
if (last || !retryableStatus(response.status, poll))
|
|
22604
|
+
return response;
|
|
22605
|
+
await response.body?.cancel().catch(() => {
|
|
22606
|
+
return;
|
|
22607
|
+
});
|
|
22608
|
+
} catch (error) {
|
|
22609
|
+
const stage = stageOf(error);
|
|
22610
|
+
if (last || !poll && stage !== "before-send")
|
|
22611
|
+
throw networkError(providerId, error, stage, poll, attempt);
|
|
22612
|
+
}
|
|
22613
|
+
await context.backoff(attempt);
|
|
22614
|
+
}
|
|
22615
|
+
}
|
|
22616
|
+
function retryableStatus(status, poll) {
|
|
22617
|
+
if (poll)
|
|
22618
|
+
return status >= 500 || status === 429;
|
|
22619
|
+
return status === 502 || status === 503 || status === 504;
|
|
22620
|
+
}
|
|
22621
|
+
var beforeSendCodes = new Set([
|
|
22622
|
+
"ECONNREFUSED",
|
|
22623
|
+
"ENOTFOUND",
|
|
22624
|
+
"EAI_AGAIN",
|
|
22625
|
+
"ENETUNREACH",
|
|
22626
|
+
"ENETDOWN",
|
|
22627
|
+
"EHOSTUNREACH",
|
|
22628
|
+
"EHOSTDOWN",
|
|
22629
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
22630
|
+
]);
|
|
22631
|
+
var timeoutCodes = new Set(["UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", "ETIMEDOUT", "TimeoutError", "Timeout"]);
|
|
22632
|
+
function stageOf(error) {
|
|
22633
|
+
const layers = [error, error?.cause].filter((layer) => typeof layer === "object" && layer !== null);
|
|
22634
|
+
const names = layers.flatMap((layer) => [layer.code, layer.name]).filter((name) => typeof name === "string");
|
|
22635
|
+
if (layers.some((layer) => layer.syscall === "connect" || layer.syscall === "getaddrinfo"))
|
|
22636
|
+
return "before-send";
|
|
22637
|
+
if (names.some((name) => beforeSendCodes.has(name)))
|
|
22638
|
+
return "before-send";
|
|
22639
|
+
if (names.some((name) => timeoutCodes.has(name)))
|
|
22640
|
+
return "timeout";
|
|
22641
|
+
return "unknown";
|
|
22642
|
+
}
|
|
22643
|
+
function describe2(error) {
|
|
22644
|
+
const message = error?.message ?? String(error);
|
|
22645
|
+
const cause = error?.cause;
|
|
22646
|
+
const reason = cause instanceof Error ? cause.message : undefined;
|
|
22647
|
+
return reason === undefined || reason === message ? message : `${message}: ${reason}`;
|
|
22648
|
+
}
|
|
22649
|
+
function networkError(providerId, error, stage, poll, attempts) {
|
|
22650
|
+
const label = providerLabel(providerId);
|
|
22651
|
+
const tries = attempts > 1 ? ` (tried ${attempts} times)` : "";
|
|
22652
|
+
if (poll || stage === "before-send")
|
|
22653
|
+
return new ProviderError("other", `Could not reach ${label}${tries}: ${describe2(error)}`);
|
|
22654
|
+
const what = stage === "timeout" ? `${label} did not answer in time` : `The connection to ${label} failed`;
|
|
22655
|
+
return new ProviderError("other", `${what} after the request was sent: ${describe2(error)}. The Provider may have processed the request and charged for it, so litevision did not send it again. Tell the User before trying again.`);
|
|
21994
22656
|
}
|
|
21995
22657
|
|
|
21996
22658
|
// src/protocols/apimart.ts
|
|
21997
22659
|
var docsBase = "https://docs.apimart.ai/en/api-reference/images";
|
|
21998
|
-
var maxImagesPerJob = 4;
|
|
21999
22660
|
var errorSchema = object({
|
|
22000
22661
|
error: object({
|
|
22001
22662
|
code: union([number2(), string2()]).nullish(),
|
|
@@ -22017,86 +22678,23 @@ var taskSchema = object({
|
|
|
22017
22678
|
})
|
|
22018
22679
|
});
|
|
22019
22680
|
var resumeSchema = object({ taskIds: array(string2()).min(1) });
|
|
22020
|
-
var sizes = (entries) => entries.map(([aspectRatio, width, height]) => ({ aspectRatio, width, height }));
|
|
22021
22681
|
var knownModels = [
|
|
22022
|
-
{
|
|
22023
|
-
|
|
22024
|
-
|
|
22025
|
-
|
|
22026
|
-
|
|
22027
|
-
|
|
22028
|
-
|
|
22029
|
-
|
|
22030
|
-
|
|
22031
|
-
|
|
22032
|
-
|
|
22033
|
-
["9:16", 864, 1536]
|
|
22034
|
-
]),
|
|
22035
|
-
maxReferences: 15,
|
|
22036
|
-
maxImages: maxImagesPerJob
|
|
22037
|
-
},
|
|
22038
|
-
batches: false
|
|
22039
|
-
},
|
|
22040
|
-
{
|
|
22041
|
-
matches: /^gpt-image-1(\.5)?(-official)?$/,
|
|
22042
|
-
docsPage: "gpt-image-1/generation",
|
|
22043
|
-
capabilities: {
|
|
22044
|
-
sizes: sizes([
|
|
22045
|
-
["1:1", 1024, 1024],
|
|
22046
|
-
["3:2", 1536, 1024],
|
|
22047
|
-
["2:3", 1024, 1536]
|
|
22048
|
-
]),
|
|
22049
|
-
maxReferences: 0,
|
|
22050
|
-
maxImages: maxImagesPerJob
|
|
22051
|
-
},
|
|
22052
|
-
batches: true
|
|
22053
|
-
},
|
|
22054
|
-
{
|
|
22055
|
-
matches: /^(gemini-3-pro-image-preview(-official)?|nano-banana-pro(-ext)?)$/,
|
|
22056
|
-
docsPage: "gemini-3-pro/generation",
|
|
22057
|
-
capabilities: {
|
|
22058
|
-
sizes: sizes([
|
|
22059
|
-
["1:1", 1024, 1024],
|
|
22060
|
-
["3:2", 1264, 848],
|
|
22061
|
-
["2:3", 848, 1264],
|
|
22062
|
-
["4:3", 1200, 896],
|
|
22063
|
-
["3:4", 896, 1200],
|
|
22064
|
-
["16:9", 1376, 768],
|
|
22065
|
-
["9:16", 768, 1376]
|
|
22066
|
-
]),
|
|
22067
|
-
maxReferences: 14,
|
|
22068
|
-
maxImages: maxImagesPerJob
|
|
22069
|
-
},
|
|
22070
|
-
batches: false
|
|
22071
|
-
},
|
|
22072
|
-
{
|
|
22073
|
-
matches: /^seedream-4[.-]5$/i,
|
|
22074
|
-
docsPage: "seedream-4.5/generation",
|
|
22075
|
-
capabilities: {
|
|
22076
|
-
sizes: sizes([
|
|
22077
|
-
["1:1", 2048, 2048],
|
|
22078
|
-
["3:2", 2496, 1664],
|
|
22079
|
-
["2:3", 1664, 2496],
|
|
22080
|
-
["4:3", 2304, 1728],
|
|
22081
|
-
["3:4", 1728, 2304],
|
|
22082
|
-
["16:9", 2560, 1440],
|
|
22083
|
-
["9:16", 1440, 2560]
|
|
22084
|
-
]),
|
|
22085
|
-
maxReferences: 14,
|
|
22086
|
-
maxImages: maxImagesPerJob
|
|
22087
|
-
},
|
|
22088
|
-
batches: false
|
|
22089
|
-
}
|
|
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 }
|
|
22090
22693
|
];
|
|
22091
|
-
var unknownModel = {
|
|
22092
|
-
sizes: sizes([["1:1", 1024, 1024]]),
|
|
22093
|
-
maxReferences: 0,
|
|
22094
|
-
maxImages: maxImagesPerJob
|
|
22095
|
-
};
|
|
22096
22694
|
var known = (modelId) => knownModels.find((model) => model.matches.test(modelId));
|
|
22097
22695
|
var headers = (credential) => ({ authorization: `Bearer ${credential}` });
|
|
22098
22696
|
var baseUrlOf = (target) => target.baseUrl.replace(/\/+$/, "");
|
|
22099
|
-
async function errorOf(response, providerId) {
|
|
22697
|
+
async function errorOf(response, providerId, options = []) {
|
|
22100
22698
|
const parsed = errorSchema.safeParse(await response.json().catch(() => {
|
|
22101
22699
|
return;
|
|
22102
22700
|
}));
|
|
@@ -22104,17 +22702,39 @@ async function errorOf(response, providerId) {
|
|
|
22104
22702
|
return httpProviderError(providerId, {
|
|
22105
22703
|
status: response.status,
|
|
22106
22704
|
codes: [error?.type],
|
|
22107
|
-
message: error?.message ?? (response.statusText || undefined)
|
|
22705
|
+
message: error?.message ?? (response.statusText || undefined),
|
|
22706
|
+
options
|
|
22108
22707
|
});
|
|
22109
22708
|
}
|
|
22110
|
-
|
|
22111
|
-
|
|
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
|
+
}
|
|
22729
|
+
async function submitTask(body, options, target, http, context) {
|
|
22730
|
+
const init = {
|
|
22112
22731
|
method: "POST",
|
|
22113
22732
|
headers: { ...headers(target.credential), "content-type": "application/json" },
|
|
22114
22733
|
body: JSON.stringify(body)
|
|
22115
|
-
}
|
|
22734
|
+
};
|
|
22735
|
+
const response = await sendToProvider(http, target.providerId, `${baseUrlOf(target)}/images/generations`, init, { context });
|
|
22116
22736
|
if (!response.ok)
|
|
22117
|
-
throw await errorOf(response, target.providerId);
|
|
22737
|
+
throw await errorOf(response, target.providerId, options);
|
|
22118
22738
|
const parsed = submittedSchema.safeParse(await response.json().catch(() => {
|
|
22119
22739
|
return;
|
|
22120
22740
|
}));
|
|
@@ -22122,50 +22742,35 @@ async function submitTask(body, target, http) {
|
|
|
22122
22742
|
throw providerError("other", target.providerId, "its answer to the submission had no task id.");
|
|
22123
22743
|
return parsed.data.data[0].task_id;
|
|
22124
22744
|
}
|
|
22125
|
-
var toleratedPollFailures = 5;
|
|
22126
22745
|
async function pollTask(taskId, target, http, context) {
|
|
22127
22746
|
const url = `${baseUrlOf(target)}/tasks/${encodeURIComponent(taskId)}?language=en`;
|
|
22128
|
-
let failures = 0;
|
|
22129
22747
|
for (;; ) {
|
|
22130
|
-
|
|
22131
|
-
|
|
22132
|
-
|
|
22133
|
-
|
|
22134
|
-
|
|
22135
|
-
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22145
|
-
|
|
22146
|
-
|
|
22147
|
-
|
|
22148
|
-
|
|
22149
|
-
|
|
22150
|
-
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
if (images.length === 0)
|
|
22154
|
-
throw providerError("other", target.providerId, `task ${taskId} completed without images.`);
|
|
22155
|
-
return { images, ...task.cost === undefined ? {} : { cost: task.cost } };
|
|
22156
|
-
}
|
|
22157
|
-
if (task.status === "failed") {
|
|
22158
|
-
const error = task.error ?? undefined;
|
|
22159
|
-
const kind = classifyProviderError({
|
|
22160
|
-
status: typeof error?.code === "number" ? error.code : 0,
|
|
22161
|
-
codes: [error?.type],
|
|
22162
|
-
message: error?.message
|
|
22163
|
-
});
|
|
22164
|
-
throw providerError(kind, target.providerId, error?.message ?? `task ${taskId} failed.`);
|
|
22165
|
-
}
|
|
22166
|
-
if (task.status === "cancelled")
|
|
22167
|
-
throw providerError("other", target.providerId, `task ${taskId} was cancelled.`);
|
|
22748
|
+
const response = await sendToProvider(http, target.providerId, url, { headers: headers(target.credential) }, { context, poll: true });
|
|
22749
|
+
if (!response.ok)
|
|
22750
|
+
throw await errorOf(response, target.providerId);
|
|
22751
|
+
const parsed = taskSchema.safeParse(await response.json().catch(() => {
|
|
22752
|
+
return;
|
|
22753
|
+
}));
|
|
22754
|
+
if (!parsed.success)
|
|
22755
|
+
throw providerError("other", target.providerId, `its status for task ${taskId} could not be read.`);
|
|
22756
|
+
const task = parsed.data.data;
|
|
22757
|
+
if (task.status === "completed") {
|
|
22758
|
+
const images = (task.result?.images ?? []).flatMap((image) => Array.isArray(image.url) ? image.url : [image.url]).map((url) => ({ url }));
|
|
22759
|
+
if (images.length === 0)
|
|
22760
|
+
throw providerError("other", target.providerId, `task ${taskId} completed without images.`);
|
|
22761
|
+
return { images, ...task.cost === undefined ? {} : { cost: task.cost } };
|
|
22762
|
+
}
|
|
22763
|
+
if (task.status === "failed") {
|
|
22764
|
+
const error = task.error ?? undefined;
|
|
22765
|
+
const kind = classifyProviderError({
|
|
22766
|
+
status: typeof error?.code === "number" ? error.code : 0,
|
|
22767
|
+
codes: [error?.type],
|
|
22768
|
+
message: error?.message
|
|
22769
|
+
});
|
|
22770
|
+
throw providerError(kind, target.providerId, error?.message ?? `task ${taskId} failed.`);
|
|
22168
22771
|
}
|
|
22772
|
+
if (task.status === "cancelled")
|
|
22773
|
+
throw providerError("other", target.providerId, `task ${taskId} was cancelled.`);
|
|
22169
22774
|
await context.pause();
|
|
22170
22775
|
}
|
|
22171
22776
|
}
|
|
@@ -22192,32 +22797,6 @@ function waitForTasks(taskIds, target, http, context, submissionFailures = []) {
|
|
|
22192
22797
|
}
|
|
22193
22798
|
};
|
|
22194
22799
|
}
|
|
22195
|
-
function capabilitiesFromSchema(schema, modelId) {
|
|
22196
|
-
const properties = object({
|
|
22197
|
-
properties: object({
|
|
22198
|
-
size: object({ enum: array(unknown()) }).optional(),
|
|
22199
|
-
image_urls: object({ maxItems: number2().int().nonnegative().optional() }).optional()
|
|
22200
|
-
})
|
|
22201
|
-
}).safeParse(schema);
|
|
22202
|
-
if (!properties.success)
|
|
22203
|
-
return;
|
|
22204
|
-
const { size, image_urls } = properties.data.properties;
|
|
22205
|
-
const ratios = aspectRatios.filter((ratio) => size?.enum.includes(ratio));
|
|
22206
|
-
if (ratios.length === 0)
|
|
22207
|
-
return;
|
|
22208
|
-
const builtIn = known(modelId)?.capabilities ?? unknownModel;
|
|
22209
|
-
return {
|
|
22210
|
-
sizes: ratios.map((ratio) => builtIn.sizes.find((candidate) => candidate.aspectRatio === ratio) ?? nominalSize(ratio)),
|
|
22211
|
-
maxReferences: image_urls === undefined ? 0 : image_urls.maxItems ?? Math.max(builtIn.maxReferences, 1),
|
|
22212
|
-
maxImages: maxImagesPerJob
|
|
22213
|
-
};
|
|
22214
|
-
}
|
|
22215
|
-
function nominalSize(aspectRatio) {
|
|
22216
|
-
const [width, height] = aspectRatio.split(":").map(Number);
|
|
22217
|
-
const scale = Math.sqrt(1024 * 1024 / (width * height));
|
|
22218
|
-
const round = (value) => Math.round(value * scale / 16) * 16;
|
|
22219
|
-
return { aspectRatio, width: round(width), height: round(height) };
|
|
22220
|
-
}
|
|
22221
22800
|
var schemaResponse = object({ parameters: object({ input_schema: unknown() }).optional() });
|
|
22222
22801
|
var modelListSchema = object({
|
|
22223
22802
|
data: array(object({ id: string2(), category: string2().optional() }))
|
|
@@ -22246,7 +22825,6 @@ async function fetchModelIds(target, http) {
|
|
|
22246
22825
|
return { ok: true, modelIds: ids.sort() };
|
|
22247
22826
|
}
|
|
22248
22827
|
var apimart = {
|
|
22249
|
-
builtInCapabilities: (modelId) => known(modelId)?.capabilities ?? unknownModel,
|
|
22250
22828
|
async fetchModelInfo(target, http) {
|
|
22251
22829
|
const response = await http(`${baseUrlOf(target)}/models/${encodeURIComponent(target.modelId)}/schema`, {
|
|
22252
22830
|
headers: headers(target.credential)
|
|
@@ -22258,27 +22836,26 @@ var apimart = {
|
|
|
22258
22836
|
const body = await response.json();
|
|
22259
22837
|
const item = schemaResponse.safeParse(body?.data ?? body);
|
|
22260
22838
|
const parameterSchema = item.success ? item.data.parameters?.input_schema : undefined;
|
|
22261
|
-
|
|
22262
|
-
return;
|
|
22263
|
-
const capabilities = capabilitiesFromSchema(parameterSchema, target.modelId);
|
|
22264
|
-
return { parameterSchema, ...capabilities === undefined ? {} : { capabilities } };
|
|
22839
|
+
return parameterSchema === undefined ? undefined : { parameterSchema };
|
|
22265
22840
|
},
|
|
22266
22841
|
docsUrl: (modelId) => {
|
|
22267
22842
|
const model = known(modelId);
|
|
22268
22843
|
return model === undefined ? "https://docs.apimart.ai/en" : `${docsBase}/${model.docsPage}`;
|
|
22269
22844
|
},
|
|
22270
|
-
|
|
22845
|
+
parameterGuide: (modelId, modality) => modality === "image" ? apimartGuide(modelId) : undefined,
|
|
22846
|
+
requestFields: { image: { shape: ["size"], resolution: "resolution" } },
|
|
22847
|
+
reservedOptions: ["model", "prompt", "image_urls"],
|
|
22271
22848
|
async submit(request, target, http, context) {
|
|
22272
22849
|
const batches = known(target.modelId)?.batches ?? false;
|
|
22850
|
+
const imageUrls = await Promise.all(request.references.map((reference) => uploadReference(reference, target, http, context)));
|
|
22273
22851
|
const body = {
|
|
22274
22852
|
model: target.modelId,
|
|
22275
22853
|
prompt: request.prompt,
|
|
22276
|
-
size: request.size.aspectRatio,
|
|
22277
22854
|
n: batches ? request.n : 1,
|
|
22278
|
-
...
|
|
22855
|
+
...imageUrls.length === 0 ? {} : { image_urls: imageUrls },
|
|
22279
22856
|
...request.providerOptions
|
|
22280
22857
|
};
|
|
22281
|
-
const settled = await Promise.allSettled(Array.from({ length: batches ? 1 : request.n }, () => submitTask(body, target, http)));
|
|
22858
|
+
const settled = await Promise.allSettled(Array.from({ length: batches ? 1 : request.n }, () => submitTask(body, Object.keys(request.providerOptions), target, http, context)));
|
|
22282
22859
|
const taskIds = settled.flatMap((outcome) => outcome.status === "fulfilled" ? [outcome.value] : []);
|
|
22283
22860
|
const failures = settled.flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
|
|
22284
22861
|
if (taskIds.length === 0)
|
|
@@ -22300,15 +22877,105 @@ var apimart = {
|
|
|
22300
22877
|
listModelIds: fetchModelIds
|
|
22301
22878
|
};
|
|
22302
22879
|
|
|
22303
|
-
// src/protocols/
|
|
22304
|
-
|
|
22305
|
-
|
|
22306
|
-
|
|
22307
|
-
|
|
22308
|
-
|
|
22309
|
-
|
|
22310
|
-
|
|
22311
|
-
|
|
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
|
+
|
|
22970
|
+
// src/protocols/parallel.ts
|
|
22971
|
+
async function inParallel(n, providerId, send) {
|
|
22972
|
+
const settled = await Promise.allSettled(Array.from({ length: n }, () => send()));
|
|
22973
|
+
const images = settled.flatMap((outcome) => outcome.status === "fulfilled" ? outcome.value : []);
|
|
22974
|
+
const failures = settled.flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
|
|
22975
|
+
if (images.length === 0)
|
|
22976
|
+
throw failures[0] ?? providerError("other", providerId, "it returned no images.");
|
|
22977
|
+
if (failures.length === 0)
|
|
22978
|
+
return { images };
|
|
22312
22979
|
return {
|
|
22313
22980
|
images,
|
|
22314
22981
|
shortfall: `${failures.length} of ${n} requests to ${providerLabel(providerId)} failed: ${failures[0].message}`
|
|
@@ -22348,50 +23015,8 @@ var blockedCodes = new Set([
|
|
|
22348
23015
|
"image_other",
|
|
22349
23016
|
"content_blocked"
|
|
22350
23017
|
]);
|
|
22351
|
-
var sizes2 = (entries) => entries.map(([aspectRatio, width, height]) => ({ aspectRatio, width, height }));
|
|
22352
|
-
var gemini3 = {
|
|
22353
|
-
capabilities: {
|
|
22354
|
-
sizes: sizes2([
|
|
22355
|
-
["1:1", 1024, 1024],
|
|
22356
|
-
["16:9", 1376, 768],
|
|
22357
|
-
["9:16", 768, 1376],
|
|
22358
|
-
["4:3", 1200, 896],
|
|
22359
|
-
["3:4", 896, 1200],
|
|
22360
|
-
["3:2", 1264, 848],
|
|
22361
|
-
["2:3", 848, 1264]
|
|
22362
|
-
]),
|
|
22363
|
-
maxReferences: 14,
|
|
22364
|
-
maxImages: 4
|
|
22365
|
-
},
|
|
22366
|
-
imageSize: "1K"
|
|
22367
|
-
};
|
|
22368
|
-
var gemini25 = {
|
|
22369
|
-
capabilities: {
|
|
22370
|
-
sizes: sizes2([
|
|
22371
|
-
["1:1", 1024, 1024],
|
|
22372
|
-
["16:9", 1344, 768],
|
|
22373
|
-
["9:16", 768, 1344],
|
|
22374
|
-
["4:3", 1184, 864],
|
|
22375
|
-
["3:4", 864, 1184],
|
|
22376
|
-
["3:2", 1248, 832],
|
|
22377
|
-
["2:3", 832, 1248]
|
|
22378
|
-
]),
|
|
22379
|
-
maxReferences: 3,
|
|
22380
|
-
maxImages: 4
|
|
22381
|
-
}
|
|
22382
|
-
};
|
|
22383
|
-
var unknownModel2 = { capabilities: { ...gemini3.capabilities, maxReferences: 3 } };
|
|
22384
|
-
var gemini3Ids = ["gemini-nano-banana-2.1", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image"];
|
|
22385
|
-
function geminiModel(modelId) {
|
|
22386
|
-
if (modelId.includes("gemini-2.5-flash-image"))
|
|
22387
|
-
return gemini25;
|
|
22388
|
-
if (gemini3Ids.some((id) => modelId.includes(id)))
|
|
22389
|
-
return gemini3;
|
|
22390
|
-
return unknownModel2;
|
|
22391
|
-
}
|
|
22392
23018
|
function interactionBody(request, target) {
|
|
22393
23019
|
const { response_format: extraFormat, ...options } = request.providerOptions;
|
|
22394
|
-
const { imageSize } = geminiModel(target.modelId);
|
|
22395
23020
|
return {
|
|
22396
23021
|
model: target.modelId,
|
|
22397
23022
|
input: [
|
|
@@ -22403,29 +23028,25 @@ function interactionBody(request, target) {
|
|
|
22403
23028
|
}))
|
|
22404
23029
|
],
|
|
22405
23030
|
...options,
|
|
22406
|
-
response_format: {
|
|
22407
|
-
...imageSize === undefined ? {} : { image_size: imageSize },
|
|
22408
|
-
...isObject2(extraFormat) ? extraFormat : {},
|
|
22409
|
-
type: "image",
|
|
22410
|
-
aspect_ratio: request.size.aspectRatio
|
|
22411
|
-
}
|
|
23031
|
+
response_format: { ...isObject2(extraFormat) ? extraFormat : {}, type: "image" }
|
|
22412
23032
|
};
|
|
22413
23033
|
}
|
|
22414
23034
|
function isObject2(value) {
|
|
22415
23035
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22416
23036
|
}
|
|
22417
|
-
function kindOf(status, codes, message) {
|
|
23037
|
+
function kindOf(status, codes, message, options = []) {
|
|
22418
23038
|
const bare = codes.filter((code) => code !== undefined).map((code) => code.split(/[/#]/).pop().toLowerCase());
|
|
22419
23039
|
if (bare.some((code) => blockedCodes.has(code)))
|
|
22420
23040
|
return "refusal";
|
|
22421
|
-
return classifyProviderError({ status, codes, message });
|
|
23041
|
+
return classifyProviderError({ status, codes, message, options });
|
|
22422
23042
|
}
|
|
22423
|
-
async function sendInteraction(request, target, http) {
|
|
22424
|
-
const
|
|
23043
|
+
async function sendInteraction(request, target, http, context) {
|
|
23044
|
+
const init = {
|
|
22425
23045
|
method: "POST",
|
|
22426
23046
|
headers: { "x-goog-api-key": target.credential, "content-type": "application/json" },
|
|
22427
23047
|
body: JSON.stringify(interactionBody(request, target))
|
|
22428
|
-
}
|
|
23048
|
+
};
|
|
23049
|
+
const response = await sendToProvider(http, target.providerId, `${trimmed(target.baseUrl)}/interactions`, init, { context });
|
|
22429
23050
|
const body = await response.json().catch(() => {
|
|
22430
23051
|
return;
|
|
22431
23052
|
});
|
|
@@ -22435,7 +23056,7 @@ async function sendInteraction(request, target, http) {
|
|
|
22435
23056
|
const message = error?.message ?? (response.statusText || undefined);
|
|
22436
23057
|
const codes = [typeof error?.code === "string" ? error.code : undefined, error?.status, ...(error?.details ?? []).map((detail) => detail.reason)];
|
|
22437
23058
|
const detail = message === undefined ? `HTTP ${response.status}` : `HTTP ${response.status}: ${message}`;
|
|
22438
|
-
throw providerError(kindOf(response.status, codes, message), target.providerId, detail);
|
|
23059
|
+
throw providerError(kindOf(response.status, codes, message, Object.keys(request.providerOptions)), target.providerId, detail);
|
|
22439
23060
|
}
|
|
22440
23061
|
const parsed = interactionSchema.safeParse(body);
|
|
22441
23062
|
if (!parsed.success) {
|
|
@@ -22455,12 +23076,13 @@ async function sendInteraction(request, target, http) {
|
|
|
22455
23076
|
const said = content.flatMap((block) => block.text === undefined ? [] : [block.text]).join(" ");
|
|
22456
23077
|
throw providerError("other", target.providerId, `the Interaction ended "${interaction.status}" with no image.${said === "" ? "" : ` The model wrote: ${said}`}`);
|
|
22457
23078
|
}
|
|
22458
|
-
var
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22463
|
-
|
|
23079
|
+
var gemini2 = {
|
|
23080
|
+
docsUrl: () => "https://ai.google.dev/gemini-api/docs/image-generation",
|
|
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"],
|
|
23084
|
+
async submit(request, target, http, context) {
|
|
23085
|
+
return { wait: () => inParallel(request.n, target.providerId, () => sendInteraction(request, target, http, context)) };
|
|
22464
23086
|
},
|
|
22465
23087
|
async testConnection(target, http) {
|
|
22466
23088
|
const list = await fetchModelIds2(target, http);
|
|
@@ -22511,32 +23133,121 @@ function trimmed(baseUrl) {
|
|
|
22511
23133
|
return baseUrl.replace(/\/+$/, "");
|
|
22512
23134
|
}
|
|
22513
23135
|
|
|
22514
|
-
// src/protocols/
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
22519
|
-
|
|
22520
|
-
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
22524
|
-
|
|
22525
|
-
|
|
22526
|
-
|
|
22527
|
-
}
|
|
22528
|
-
|
|
22529
|
-
|
|
22530
|
-
|
|
22531
|
-
|
|
22532
|
-
|
|
22533
|
-
|
|
22534
|
-
|
|
22535
|
-
|
|
22536
|
-
|
|
22537
|
-
|
|
22538
|
-
|
|
22539
|
-
}
|
|
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);
|
|
22540
23251
|
|
|
22541
23252
|
// src/protocols/openai-images.ts
|
|
22542
23253
|
var successSchema = object({
|
|
@@ -22546,32 +23257,10 @@ var errorSchema3 = object({
|
|
|
22546
23257
|
error: object({
|
|
22547
23258
|
message: string2().optional(),
|
|
22548
23259
|
code: string2().nullish(),
|
|
22549
|
-
type: string2().nullish()
|
|
23260
|
+
type: string2().nullish(),
|
|
23261
|
+
param: string2().nullish()
|
|
22550
23262
|
})
|
|
22551
23263
|
});
|
|
22552
|
-
var gptImage = {
|
|
22553
|
-
sizes: [
|
|
22554
|
-
{ aspectRatio: "1:1", width: 1024, height: 1024 },
|
|
22555
|
-
{ aspectRatio: "3:2", width: 1536, height: 1024 },
|
|
22556
|
-
{ aspectRatio: "2:3", width: 1024, height: 1536 }
|
|
22557
|
-
],
|
|
22558
|
-
maxReferences: 16,
|
|
22559
|
-
maxImages: 10
|
|
22560
|
-
};
|
|
22561
|
-
var dalle3 = {
|
|
22562
|
-
sizes: [
|
|
22563
|
-
{ aspectRatio: "1:1", width: 1024, height: 1024 },
|
|
22564
|
-
{ aspectRatio: "16:9", width: 1792, height: 1024 },
|
|
22565
|
-
{ aspectRatio: "9:16", width: 1024, height: 1792 }
|
|
22566
|
-
],
|
|
22567
|
-
maxReferences: 0,
|
|
22568
|
-
maxImages: 1
|
|
22569
|
-
};
|
|
22570
|
-
var dalle2 = {
|
|
22571
|
-
sizes: [{ aspectRatio: "1:1", width: 1024, height: 1024 }],
|
|
22572
|
-
maxReferences: 1,
|
|
22573
|
-
maxImages: 10
|
|
22574
|
-
};
|
|
22575
23264
|
function buildRequest(request, target) {
|
|
22576
23265
|
const baseUrl = target.baseUrl.replace(/\/+$/, "");
|
|
22577
23266
|
const headers = { authorization: `Bearer ${target.credential}` };
|
|
@@ -22579,7 +23268,6 @@ function buildRequest(request, target) {
|
|
|
22579
23268
|
model: target.modelId,
|
|
22580
23269
|
prompt: request.prompt,
|
|
22581
23270
|
n: request.n,
|
|
22582
|
-
size: formatSize(request.size),
|
|
22583
23271
|
...request.providerOptions
|
|
22584
23272
|
};
|
|
22585
23273
|
if (request.references.length === 0) {
|
|
@@ -22593,18 +23281,13 @@ function buildRequest(request, target) {
|
|
|
22593
23281
|
return { url: `${baseUrl}/images/edits`, init: { method: "POST", headers: { ...headers, "content-type": contentType }, body } };
|
|
22594
23282
|
}
|
|
22595
23283
|
var openaiImages = {
|
|
22596
|
-
builtInCapabilities(modelId) {
|
|
22597
|
-
if (modelId.startsWith("dall-e-3"))
|
|
22598
|
-
return dalle3;
|
|
22599
|
-
if (modelId.startsWith("dall-e-2"))
|
|
22600
|
-
return dalle2;
|
|
22601
|
-
return gptImage;
|
|
22602
|
-
},
|
|
22603
23284
|
docsUrl: () => "https://platform.openai.com/docs/api-reference/images/create",
|
|
22604
|
-
|
|
22605
|
-
|
|
23285
|
+
parameterGuide: (modelId, modality) => modality === "image" ? openaiImagesGuide(modelId) : undefined,
|
|
23286
|
+
requestFields: { image: { shape: ["size"] } },
|
|
23287
|
+
reservedOptions: ["model", "prompt", "image", "image[]"],
|
|
23288
|
+
async submit(request, target, http, context) {
|
|
22606
23289
|
const { url, init } = buildRequest(request, target);
|
|
22607
|
-
const response = await sendToProvider(http, target.providerId, url, init);
|
|
23290
|
+
const response = await sendToProvider(http, target.providerId, url, init, { context });
|
|
22608
23291
|
const body = await response.json().catch(() => {
|
|
22609
23292
|
return;
|
|
22610
23293
|
});
|
|
@@ -22614,7 +23297,9 @@ var openaiImages = {
|
|
|
22614
23297
|
throw httpProviderError(target.providerId, {
|
|
22615
23298
|
status: response.status,
|
|
22616
23299
|
codes: [error?.code, error?.type],
|
|
22617
|
-
message: error?.message ?? (response.statusText || undefined)
|
|
23300
|
+
message: error?.message ?? (response.statusText || undefined),
|
|
23301
|
+
param: error?.param,
|
|
23302
|
+
options: Object.keys(request.providerOptions)
|
|
22618
23303
|
});
|
|
22619
23304
|
}
|
|
22620
23305
|
const parsed = successSchema.safeParse(body);
|
|
@@ -22664,106 +23349,557 @@ async function fetchModelIds3(target, http) {
|
|
|
22664
23349
|
return { ok: true, modelIds: parsed.data.data.map((model) => model.id).sort() };
|
|
22665
23350
|
}
|
|
22666
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
|
+
|
|
23528
|
+
// src/protocols/together.ts
|
|
23529
|
+
var docs = {
|
|
23530
|
+
flux: "https://docs.together.ai/docs/quickstart-flux",
|
|
23531
|
+
imageParameters: "https://docs.together.ai/docs/inference/images/parameters",
|
|
23532
|
+
referenceImages: "https://docs.together.ai/docs/inference/images/reference-images",
|
|
23533
|
+
wan: "https://docs.together.ai/docs/wan2.7-quickstart",
|
|
23534
|
+
seedance: "https://docs.together.ai/docs/seedance2.5-quickstart",
|
|
23535
|
+
videoParameters: "https://docs.together.ai/docs/inference/videos/parameters"
|
|
23536
|
+
};
|
|
23537
|
+
var imageFamilies = [
|
|
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 }
|
|
23541
|
+
];
|
|
23542
|
+
var otherImages = { matches: /.*/, references: "reference_images", docsUrl: docs.imageParameters };
|
|
23543
|
+
var imageFamily = (modelId) => imageFamilies.find((family) => family.matches.test(modelId)) ?? otherImages;
|
|
23544
|
+
var videoFamilies = [
|
|
23545
|
+
{ matches: /^Wan-AI\/wan2\.7-/i, docsUrl: docs.wan },
|
|
23546
|
+
{ matches: /^ByteDance\/Seedance-2\.5$/i, docsUrl: docs.seedance }
|
|
23547
|
+
];
|
|
23548
|
+
var videoDocsUrl = (modelId) => videoFamilies.find((family) => family.matches.test(modelId))?.docsUrl ?? docs.videoParameters;
|
|
23549
|
+
var errorSchema4 = object({
|
|
23550
|
+
error: object({
|
|
23551
|
+
message: string2().optional(),
|
|
23552
|
+
code: string2().nullish(),
|
|
23553
|
+
type: string2().nullish(),
|
|
23554
|
+
param: string2().nullish()
|
|
23555
|
+
})
|
|
23556
|
+
});
|
|
23557
|
+
var imagesSchema = object({
|
|
23558
|
+
data: array(object({ b64_json: string2().nullish(), url: string2().nullish() }))
|
|
23559
|
+
});
|
|
23560
|
+
var videoSchema = object({
|
|
23561
|
+
id: string2(),
|
|
23562
|
+
status: string2(),
|
|
23563
|
+
outputs: object({ video_url: string2().optional(), cost: number2().optional() }).nullish(),
|
|
23564
|
+
error: object({ message: string2().optional(), code: string2().nullish() }).nullish()
|
|
23565
|
+
});
|
|
23566
|
+
var resumeSchema2 = object({ videoId: string2().min(1) });
|
|
23567
|
+
var modelListSchema4 = array(object({ id: string2(), type: string2().optional() }));
|
|
23568
|
+
var rootOf = (target) => target.baseUrl.replace(/\/+$/, "").replace(/\/v[12]$/, "");
|
|
23569
|
+
var authorization = (credential) => ({ authorization: `Bearer ${credential}` });
|
|
23570
|
+
var dataUrl = (image) => `data:${image.mimeType};base64,${Buffer.from(image.bytes).toString("base64")}`;
|
|
23571
|
+
var passthroughBlocked = "third_party_data_sharing_blocked";
|
|
23572
|
+
function passthroughError(providerId) {
|
|
23573
|
+
return new ProviderError("unsupported", `${providerLabel(providerId)} runs this model through its maker, which the User's Together organization does not allow yet. ` + `Only the User can change that: in Together's dashboard (https://api.together.ai/settings), Organization → General → Privacy → "Passthrough models", which sends prompts for such models to their makers. ` + "Tell the User, or choose a model Together serves itself.");
|
|
23574
|
+
}
|
|
23575
|
+
async function errorOf2(response, providerId, options = []) {
|
|
23576
|
+
const text = await response.text().catch(() => "");
|
|
23577
|
+
let body;
|
|
23578
|
+
try {
|
|
23579
|
+
body = JSON.parse(text);
|
|
23580
|
+
} catch {
|
|
23581
|
+
body = undefined;
|
|
23582
|
+
}
|
|
23583
|
+
const parsed = errorSchema4.safeParse(body);
|
|
23584
|
+
const error = parsed.success ? parsed.data.error : undefined;
|
|
23585
|
+
if (error?.code === passthroughBlocked)
|
|
23586
|
+
return passthroughError(providerId);
|
|
23587
|
+
const plain = body === undefined ? text.trim().slice(0, 300) : "";
|
|
23588
|
+
return httpProviderError(providerId, {
|
|
23589
|
+
status: response.status,
|
|
23590
|
+
codes: [error?.code, error?.type],
|
|
23591
|
+
message: error?.message ?? (plain || response.statusText || undefined),
|
|
23592
|
+
param: error?.param,
|
|
23593
|
+
options
|
|
23594
|
+
});
|
|
23595
|
+
}
|
|
23596
|
+
async function postJson(url, body, providerOptions, target, http, context) {
|
|
23597
|
+
const init = {
|
|
23598
|
+
method: "POST",
|
|
23599
|
+
headers: { ...authorization(target.credential), "content-type": "application/json" },
|
|
23600
|
+
body: JSON.stringify(body)
|
|
23601
|
+
};
|
|
23602
|
+
const response = await sendToProvider(http, target.providerId, url, init, { context });
|
|
23603
|
+
if (!response.ok)
|
|
23604
|
+
throw await errorOf2(response, target.providerId, Object.keys(providerOptions));
|
|
23605
|
+
return response.json().catch(() => {
|
|
23606
|
+
return;
|
|
23607
|
+
});
|
|
23608
|
+
}
|
|
23609
|
+
function waitForVideo(videoId, target, http, context) {
|
|
23610
|
+
const url = `${rootOf(target)}/v2/videos/${encodeURIComponent(videoId)}`;
|
|
23611
|
+
return {
|
|
23612
|
+
resumeHandle: { videoId },
|
|
23613
|
+
async wait() {
|
|
23614
|
+
for (;; ) {
|
|
23615
|
+
const response = await sendToProvider(http, target.providerId, url, { headers: authorization(target.credential) }, { context, poll: true });
|
|
23616
|
+
if (!response.ok)
|
|
23617
|
+
throw await errorOf2(response, target.providerId);
|
|
23618
|
+
const parsed = videoSchema.safeParse(await response.json().catch(() => {
|
|
23619
|
+
return;
|
|
23620
|
+
}));
|
|
23621
|
+
if (!parsed.success)
|
|
23622
|
+
throw providerError("other", target.providerId, `its status for video ${videoId} could not be read.`);
|
|
23623
|
+
const job = parsed.data;
|
|
23624
|
+
if (job.status === "completed") {
|
|
23625
|
+
const link = job.outputs?.video_url;
|
|
23626
|
+
if (link === undefined)
|
|
23627
|
+
throw providerError("other", target.providerId, `video ${videoId} completed without a link to it.`);
|
|
23628
|
+
return { video: { url: link }, ...job.outputs?.cost === undefined ? {} : { cost: job.outputs.cost } };
|
|
23629
|
+
}
|
|
23630
|
+
if (job.status === "failed")
|
|
23631
|
+
throw videoFailure(job.error ?? undefined, target.providerId, videoId);
|
|
23632
|
+
if (job.status === "cancelled")
|
|
23633
|
+
throw providerError("other", target.providerId, `video ${videoId} was cancelled.`);
|
|
23634
|
+
await context.pause();
|
|
23635
|
+
}
|
|
23636
|
+
}
|
|
23637
|
+
};
|
|
23638
|
+
}
|
|
23639
|
+
function videoFailure(error, providerId, videoId) {
|
|
23640
|
+
if (error?.code === passthroughBlocked)
|
|
23641
|
+
return passthroughError(providerId);
|
|
23642
|
+
const rejected = /^(unsupported|invalid)/i.test(error?.code ?? "");
|
|
23643
|
+
const kind = rejected ? "unsupported" : classifyProviderError({ status: 0, codes: [error?.code], message: error?.message });
|
|
23644
|
+
const detail = [error?.code, error?.message].filter(Boolean).join(": ");
|
|
23645
|
+
return providerError(kind, providerId, detail || `video ${videoId} failed.`);
|
|
23646
|
+
}
|
|
23647
|
+
async function fetchModelIds4(target, http) {
|
|
23648
|
+
const label = providerLabel(target.providerId);
|
|
23649
|
+
let response;
|
|
23650
|
+
try {
|
|
23651
|
+
response = await http(`${rootOf(target)}/v1/models`, { headers: authorization(target.credential) });
|
|
23652
|
+
} catch (error) {
|
|
23653
|
+
return { ok: false, message: `Could not reach ${label}: ${error.message}` };
|
|
23654
|
+
}
|
|
23655
|
+
if (!response.ok)
|
|
23656
|
+
return { ok: false, message: (await errorOf2(response, target.providerId)).message };
|
|
23657
|
+
const parsed = modelListSchema4.safeParse(await response.json().catch(() => {
|
|
23658
|
+
return;
|
|
23659
|
+
}));
|
|
23660
|
+
if (!parsed.success)
|
|
23661
|
+
return { ok: false, message: `${label} answered, but not with a Together model list. Check the base URL.` };
|
|
23662
|
+
const modalities = {};
|
|
23663
|
+
for (const model of parsed.data) {
|
|
23664
|
+
if (model.type === "image" || model.type === "video")
|
|
23665
|
+
modalities[model.id] = model.type;
|
|
23666
|
+
}
|
|
23667
|
+
return { ok: true, modelIds: Object.keys(modalities).sort(), modalities };
|
|
23668
|
+
}
|
|
23669
|
+
var together = {
|
|
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"],
|
|
23678
|
+
async submit(request, target, http, context) {
|
|
23679
|
+
const family = imageFamily(target.modelId);
|
|
23680
|
+
const { references } = request;
|
|
23681
|
+
const body = {
|
|
23682
|
+
model: target.modelId,
|
|
23683
|
+
prompt: request.prompt,
|
|
23684
|
+
n: request.n,
|
|
23685
|
+
response_format: "base64",
|
|
23686
|
+
...references.length === 0 ? {} : family.references === "image_url" ? { image_url: dataUrl(references[0]) } : { reference_images: references.map(dataUrl) },
|
|
23687
|
+
...request.providerOptions
|
|
23688
|
+
};
|
|
23689
|
+
const parsed = imagesSchema.safeParse(await postJson(`${rootOf(target)}/v1/images/generations`, body, request.providerOptions, target, http, context));
|
|
23690
|
+
if (!parsed.success)
|
|
23691
|
+
throw providerError("other", target.providerId, "its response could not be read as Together images.");
|
|
23692
|
+
const images = parsed.data.data.map((item) => {
|
|
23693
|
+
if (item.b64_json)
|
|
23694
|
+
return { bytes: Buffer.from(item.b64_json, "base64") };
|
|
23695
|
+
if (item.url)
|
|
23696
|
+
return { url: item.url };
|
|
23697
|
+
throw providerError("other", target.providerId, "it returned an image with neither data nor a link.");
|
|
23698
|
+
});
|
|
23699
|
+
if (images.length === 0)
|
|
23700
|
+
throw providerError("other", target.providerId, "it returned no images.");
|
|
23701
|
+
const shortfall = images.length < request.n ? `${providerLabel(target.providerId)} returned ${images.length} of ${request.n} images without saying why.` : undefined;
|
|
23702
|
+
return { wait: async () => ({ images, ...shortfall === undefined ? {} : { shortfall } }) };
|
|
23703
|
+
},
|
|
23704
|
+
async submitVideo(request, target, http, context) {
|
|
23705
|
+
const frames = [
|
|
23706
|
+
...request.firstFrame === undefined ? [] : [{ input_image: dataUrl(request.firstFrame), frame: "first" }],
|
|
23707
|
+
...request.lastFrame === undefined ? [] : [{ input_image: dataUrl(request.lastFrame), frame: "last" }]
|
|
23708
|
+
];
|
|
23709
|
+
const media = {
|
|
23710
|
+
...frames.length === 0 ? {} : { frame_images: frames },
|
|
23711
|
+
...request.references.length === 0 ? {} : { reference_images: request.references.map(dataUrl) }
|
|
23712
|
+
};
|
|
23713
|
+
const body = {
|
|
23714
|
+
model: target.modelId,
|
|
23715
|
+
prompt: request.prompt,
|
|
23716
|
+
...Object.keys(media).length === 0 ? {} : { media },
|
|
23717
|
+
...request.providerOptions
|
|
23718
|
+
};
|
|
23719
|
+
const parsed = videoSchema.safeParse(await postJson(`${rootOf(target)}/v2/videos`, body, request.providerOptions, target, http, context));
|
|
23720
|
+
if (!parsed.success)
|
|
23721
|
+
throw providerError("other", target.providerId, "its answer to the submission had no video id.");
|
|
23722
|
+
if (parsed.data.status === "failed")
|
|
23723
|
+
throw videoFailure(parsed.data.error ?? undefined, target.providerId, parsed.data.id);
|
|
23724
|
+
return waitForVideo(parsed.data.id, target, http, context);
|
|
23725
|
+
},
|
|
23726
|
+
resumeVideo(handle, target, http, context) {
|
|
23727
|
+
const parsed = resumeSchema2.safeParse(handle);
|
|
23728
|
+
if (!parsed.success)
|
|
23729
|
+
throw providerError("other", target.providerId, "the Job history holds no video id to pick the Job up from.");
|
|
23730
|
+
return waitForVideo(parsed.data.videoId, target, http, context);
|
|
23731
|
+
},
|
|
23732
|
+
async testConnection(target, http) {
|
|
23733
|
+
const list = await fetchModelIds4(target, http);
|
|
23734
|
+
if (!list.ok)
|
|
23735
|
+
return list;
|
|
23736
|
+
const count = (modality) => Object.values(list.modalities ?? {}).filter((value) => value === modality).length;
|
|
23737
|
+
return {
|
|
23738
|
+
ok: true,
|
|
23739
|
+
message: `Connected to Provider "${target.providerId}". It offers ${count("image")} image and ${count("video")} video model id(s).`
|
|
23740
|
+
};
|
|
23741
|
+
},
|
|
23742
|
+
listModelIds: fetchModelIds4
|
|
23743
|
+
};
|
|
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
|
+
|
|
22667
23873
|
// src/protocols/venice.ts
|
|
22668
23874
|
var generateDocs = "https://docs.venice.ai/api-reference/endpoint/image/generate";
|
|
22669
23875
|
var editDocs = "https://docs.venice.ai/api-reference/endpoint/image/edit";
|
|
22670
|
-
var
|
|
22671
|
-
var constraintsSchema = object({
|
|
22672
|
-
aspectRatios: array(string2()).optional(),
|
|
22673
|
-
widthHeightDivisor: number2().positive().optional(),
|
|
22674
|
-
combineImages: boolean2().optional(),
|
|
22675
|
-
maxInputImages: number2().int().positive().optional()
|
|
22676
|
-
});
|
|
22677
|
-
var sizingOf = (constraints) => (constraints.widthHeightDivisor ?? 1) > 1 ? "pixels" : "ratio";
|
|
22678
|
-
var modelListSchema4 = object({
|
|
23876
|
+
var modelListSchema5 = object({
|
|
22679
23877
|
data: array(object({
|
|
22680
23878
|
id: string2(),
|
|
22681
23879
|
type: string2().optional(),
|
|
22682
|
-
model_spec: object({
|
|
22683
|
-
constraints: record(string2(), unknown()).optional(),
|
|
22684
|
-
capabilities: object({ maxInputImages: number2().int().positive().optional() }).passthrough().optional()
|
|
22685
|
-
}).passthrough().optional()
|
|
23880
|
+
model_spec: object({ constraints: record(string2(), unknown()).optional() }).passthrough().optional()
|
|
22686
23881
|
}))
|
|
22687
23882
|
});
|
|
22688
|
-
var
|
|
23883
|
+
var errorSchema5 = object({
|
|
22689
23884
|
error: union([string2(), object({ message: string2().optional(), code: string2().nullish() })]),
|
|
22690
23885
|
code: string2().optional(),
|
|
22691
|
-
suggested_prompt: string2().optional()
|
|
23886
|
+
suggested_prompt: string2().optional(),
|
|
23887
|
+
details: record(string2(), unknown()).optional()
|
|
22692
23888
|
});
|
|
22693
23889
|
var generateSchema = object({
|
|
22694
23890
|
images: array(string2()),
|
|
22695
23891
|
content_policy_violation_count: number2().int().nonnegative().optional()
|
|
22696
23892
|
});
|
|
22697
|
-
function sizeFor(aspectRatio, divisor, maxSide = Infinity) {
|
|
22698
|
-
const [w, h] = aspectRatio.split(":").map(Number);
|
|
22699
|
-
const area = 1024 * 1024;
|
|
22700
|
-
let width = Math.sqrt(area * w / h);
|
|
22701
|
-
let height = Math.sqrt(area * h / w);
|
|
22702
|
-
const scale = Math.min(1, maxSide / width, maxSide / height);
|
|
22703
|
-
width *= scale;
|
|
22704
|
-
height *= scale;
|
|
22705
|
-
const snap = (value) => Math.max(divisor, Math.round(value / divisor) * divisor);
|
|
22706
|
-
return { aspectRatio, width: snap(width), height: snap(height) };
|
|
22707
|
-
}
|
|
22708
|
-
function pixelSizes(divisor) {
|
|
22709
|
-
return aspectRatios.map((ratio) => sizeFor(ratio, divisor, 1280));
|
|
22710
|
-
}
|
|
22711
|
-
function ratioSizes(ratios) {
|
|
22712
|
-
return aspectRatios.filter((ratio) => ratios.includes(ratio)).map((ratio) => sizeFor(ratio, 16));
|
|
22713
|
-
}
|
|
22714
23893
|
function looksLikeEdit(modelId) {
|
|
22715
23894
|
return /(^|-)edit(-|$)/.test(modelId);
|
|
22716
23895
|
}
|
|
22717
|
-
var pixelModels = ["venice-sd35", "hidream", "lustify-sdxl", "flux-dev", "flux-dev-uncensored", "pony-realism"];
|
|
22718
|
-
function builtInModel(modelId) {
|
|
22719
|
-
if (looksLikeEdit(modelId)) {
|
|
22720
|
-
return { kind: "edit", sizing: "ratio", capabilities: { sizes: ratioSizes(aspectRatios), maxReferences: 3, maxImages } };
|
|
22721
|
-
}
|
|
22722
|
-
if (pixelModels.includes(modelId)) {
|
|
22723
|
-
return { kind: "generate", sizing: "pixels", capabilities: { sizes: pixelSizes(16), maxReferences: 0, maxImages } };
|
|
22724
|
-
}
|
|
22725
|
-
return {
|
|
22726
|
-
kind: "generate",
|
|
22727
|
-
sizing: "ratio",
|
|
22728
|
-
capabilities: { sizes: ratioSizes(["1:1", "16:9", "9:16", "3:2", "2:3"]), maxReferences: 0, maxImages }
|
|
22729
|
-
};
|
|
22730
|
-
}
|
|
22731
|
-
function describeModel(listed, listType) {
|
|
22732
|
-
const raw = listed.model_spec?.constraints;
|
|
22733
|
-
const parsed = constraintsSchema.safeParse(raw ?? {});
|
|
22734
|
-
const constraints = parsed.success ? parsed.data : {};
|
|
22735
|
-
const nonEmpty = (capabilities) => capabilities.sizes.length === 0 ? undefined : capabilities;
|
|
22736
|
-
if ((listed.type ?? listType) === "inpaint") {
|
|
22737
|
-
const maxReferences = constraints.maxInputImages ?? listed.model_spec?.capabilities?.maxInputImages ?? (constraints.combineImages === false ? 1 : 3);
|
|
22738
|
-
return {
|
|
22739
|
-
kind: "edit",
|
|
22740
|
-
sizing: "ratio",
|
|
22741
|
-
capabilities: nonEmpty({ sizes: ratioSizes(constraints.aspectRatios ?? []), maxReferences, maxImages }),
|
|
22742
|
-
constraints: raw
|
|
22743
|
-
};
|
|
22744
|
-
}
|
|
22745
|
-
if (sizingOf(constraints) === "pixels") {
|
|
22746
|
-
return {
|
|
22747
|
-
kind: "generate",
|
|
22748
|
-
sizing: "pixels",
|
|
22749
|
-
capabilities: { sizes: pixelSizes(constraints.widthHeightDivisor), maxReferences: 0, maxImages },
|
|
22750
|
-
constraints: raw
|
|
22751
|
-
};
|
|
22752
|
-
}
|
|
22753
|
-
return {
|
|
22754
|
-
kind: "generate",
|
|
22755
|
-
sizing: "ratio",
|
|
22756
|
-
capabilities: nonEmpty({ sizes: ratioSizes(constraints.aspectRatios ?? []), maxReferences: 0, maxImages }),
|
|
22757
|
-
constraints: raw
|
|
22758
|
-
};
|
|
22759
|
-
}
|
|
22760
23896
|
var apiUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
22761
|
-
var
|
|
23897
|
+
var authorization2 = (credential) => ({ authorization: `Bearer ${credential}` });
|
|
22762
23898
|
async function listModels(target, type, http) {
|
|
22763
23899
|
const label = providerLabel(target.providerId);
|
|
22764
23900
|
let response;
|
|
22765
23901
|
try {
|
|
22766
|
-
response = await http(apiUrl(target.baseUrl, `/models?type=${type}`), { headers:
|
|
23902
|
+
response = await http(apiUrl(target.baseUrl, `/models?type=${type}`), { headers: authorization2(target.credential) });
|
|
22767
23903
|
} catch (error) {
|
|
22768
23904
|
return { ok: false, message: `Could not reach ${label}: ${error.message}` };
|
|
22769
23905
|
}
|
|
@@ -22773,19 +23909,12 @@ async function listModels(target, type, http) {
|
|
|
22773
23909
|
if (!response.ok) {
|
|
22774
23910
|
return { ok: false, message: `${label} returned HTTP ${response.status}: ${errorMessage(body) ?? response.statusText}` };
|
|
22775
23911
|
}
|
|
22776
|
-
const parsed =
|
|
23912
|
+
const parsed = modelListSchema5.safeParse(body);
|
|
22777
23913
|
if (!parsed.success) {
|
|
22778
23914
|
return { ok: false, message: `${label} answered, but not with a Venice model list. Check the base URL.` };
|
|
22779
23915
|
}
|
|
22780
23916
|
return { ok: true, models: parsed.data.data };
|
|
22781
23917
|
}
|
|
22782
|
-
function describedModel(described, modelId) {
|
|
22783
|
-
const kind = described.capabilities.maxReferences > 0 ? "edit" : "generate";
|
|
22784
|
-
const constraints = constraintsSchema.safeParse(described.parameterSchema);
|
|
22785
|
-
if (described.parameterSchema === undefined || !constraints.success)
|
|
22786
|
-
return { kind, sizing: builtInModel(modelId).sizing };
|
|
22787
|
-
return { kind, sizing: sizingOf(constraints.data) };
|
|
22788
|
-
}
|
|
22789
23918
|
async function lookUpModel(target, http) {
|
|
22790
23919
|
const types = looksLikeEdit(target.modelId) ? ["inpaint", "image"] : ["image", "inpaint"];
|
|
22791
23920
|
for (const type of types) {
|
|
@@ -22794,41 +23923,48 @@ async function lookUpModel(target, http) {
|
|
|
22794
23923
|
throw new Error(list.message);
|
|
22795
23924
|
const listed = list.models.find((model) => model.id === target.modelId);
|
|
22796
23925
|
if (listed !== undefined)
|
|
22797
|
-
return
|
|
23926
|
+
return listed.model_spec?.constraints === undefined ? {} : { constraints: listed.model_spec.constraints };
|
|
22798
23927
|
}
|
|
22799
23928
|
return;
|
|
22800
23929
|
}
|
|
22801
23930
|
function errorMessage(body) {
|
|
22802
|
-
const parsed =
|
|
23931
|
+
const parsed = errorSchema5.safeParse(body);
|
|
22803
23932
|
if (!parsed.success)
|
|
22804
23933
|
return;
|
|
22805
23934
|
return typeof parsed.data.error === "string" ? parsed.data.error : parsed.data.error.message;
|
|
22806
23935
|
}
|
|
22807
|
-
|
|
23936
|
+
var fieldErrorsSchema = object({ _errors: array(string2()) });
|
|
23937
|
+
function fieldProblems(details) {
|
|
23938
|
+
return Object.entries(details ?? {}).flatMap(([field, value]) => {
|
|
23939
|
+
const parsed = fieldErrorsSchema.safeParse(value);
|
|
23940
|
+
return field === "_errors" || !parsed.success || parsed.data._errors.length === 0 ? [] : [`${field}: ${parsed.data._errors.join(", ")}`];
|
|
23941
|
+
});
|
|
23942
|
+
}
|
|
23943
|
+
async function veniceError(response, providerId, options) {
|
|
22808
23944
|
const body = await response.json().catch(() => {
|
|
22809
23945
|
return;
|
|
22810
23946
|
});
|
|
22811
|
-
const parsed =
|
|
23947
|
+
const parsed = errorSchema5.safeParse(body);
|
|
22812
23948
|
const error = parsed.success ? parsed.data : undefined;
|
|
22813
23949
|
const nested = typeof error?.error === "object" ? error.error : undefined;
|
|
22814
|
-
const
|
|
23950
|
+
const problems = fieldProblems(error?.details);
|
|
23951
|
+
const said = errorMessage(body) ?? (response.statusText || undefined);
|
|
23952
|
+
const message = said === undefined ? undefined : problems.length === 0 ? said : `${said}: ${problems.join("; ")}`;
|
|
22815
23953
|
const suggestion = error?.suggested_prompt === undefined ? "" : ` Venice suggests this prompt instead: ${error.suggested_prompt}`;
|
|
22816
23954
|
return httpProviderError(providerId, {
|
|
22817
23955
|
status: response.status,
|
|
22818
23956
|
codes: [error?.code, nested?.code],
|
|
22819
|
-
message: message === undefined ? undefined : `${message}${suggestion}
|
|
23957
|
+
message: message === undefined ? undefined : `${message}${suggestion}`,
|
|
23958
|
+
options
|
|
22820
23959
|
});
|
|
22821
23960
|
}
|
|
22822
23961
|
var sharedDefaults = { safe_mode: false, enhance_prompt: false };
|
|
22823
|
-
async function generate(request, target,
|
|
22824
|
-
const { size } = request;
|
|
22825
|
-
const sizing = model.sizing === "pixels" ? { width: size.width, height: size.height } : { aspect_ratio: size.aspectRatio };
|
|
23962
|
+
async function generate(request, target, http, context) {
|
|
22826
23963
|
const body = {
|
|
22827
23964
|
model: target.modelId,
|
|
22828
23965
|
prompt: request.prompt,
|
|
22829
23966
|
variants: request.n,
|
|
22830
23967
|
return_binary: false,
|
|
22831
|
-
...sizing,
|
|
22832
23968
|
...sharedDefaults,
|
|
22833
23969
|
hide_watermark: true,
|
|
22834
23970
|
format: "png",
|
|
@@ -22836,11 +23972,11 @@ async function generate(request, target, model, http) {
|
|
|
22836
23972
|
};
|
|
22837
23973
|
const response = await sendToProvider(http, target.providerId, apiUrl(target.baseUrl, "/image/generate"), {
|
|
22838
23974
|
method: "POST",
|
|
22839
|
-
headers: { ...
|
|
23975
|
+
headers: { ...authorization2(target.credential), "content-type": "application/json" },
|
|
22840
23976
|
body: JSON.stringify(body)
|
|
22841
|
-
});
|
|
23977
|
+
}, { context });
|
|
22842
23978
|
if (!response.ok)
|
|
22843
|
-
throw await veniceError(response, target.providerId);
|
|
23979
|
+
throw await veniceError(response, target.providerId, Object.keys(request.providerOptions));
|
|
22844
23980
|
const parsed = generateSchema.safeParse(await response.json().catch(() => {
|
|
22845
23981
|
return;
|
|
22846
23982
|
}));
|
|
@@ -22858,51 +23994,44 @@ async function generate(request, target, model, http) {
|
|
|
22858
23994
|
}
|
|
22859
23995
|
return result;
|
|
22860
23996
|
}
|
|
22861
|
-
async function edit(request, target, http) {
|
|
23997
|
+
async function edit(request, target, http, context) {
|
|
22862
23998
|
const encoded = request.references.map((reference) => Buffer.from(reference.bytes).toString("base64"));
|
|
22863
23999
|
const [path, inputs] = encoded.length === 1 ? ["/image/edit", { model: target.modelId, image: encoded[0] }] : ["/image/multi-edit", { modelId: target.modelId, images: encoded }];
|
|
22864
24000
|
const body = {
|
|
22865
24001
|
...inputs,
|
|
22866
24002
|
prompt: request.prompt,
|
|
22867
|
-
aspect_ratio: request.size.aspectRatio,
|
|
22868
24003
|
...sharedDefaults,
|
|
22869
24004
|
output_format: "png",
|
|
22870
24005
|
...request.providerOptions
|
|
22871
24006
|
};
|
|
22872
24007
|
const response = await sendToProvider(http, target.providerId, apiUrl(target.baseUrl, path), {
|
|
22873
24008
|
method: "POST",
|
|
22874
|
-
headers: { ...
|
|
24009
|
+
headers: { ...authorization2(target.credential), "content-type": "application/json" },
|
|
22875
24010
|
body: JSON.stringify(body)
|
|
22876
|
-
});
|
|
24011
|
+
}, { context });
|
|
22877
24012
|
if (!response.ok)
|
|
22878
|
-
throw await veniceError(response, target.providerId);
|
|
24013
|
+
throw await veniceError(response, target.providerId, Object.keys(request.providerOptions));
|
|
22879
24014
|
if (response.headers.get("content-type")?.includes("json")) {
|
|
22880
24015
|
throw providerError("other", target.providerId, "it answered an edit with JSON rather than an image.");
|
|
22881
24016
|
}
|
|
22882
24017
|
return [{ bytes: new Uint8Array(await response.arrayBuffer()) }];
|
|
22883
24018
|
}
|
|
22884
24019
|
var venice = {
|
|
22885
|
-
builtInCapabilities: (modelId) => builtInModel(modelId).capabilities,
|
|
22886
24020
|
async fetchModelInfo(target, http) {
|
|
22887
24021
|
const model = await lookUpModel(target, http);
|
|
22888
24022
|
if (model === undefined)
|
|
22889
24023
|
return;
|
|
22890
|
-
return {
|
|
22891
|
-
...model.capabilities === undefined ? {} : { capabilities: model.capabilities },
|
|
22892
|
-
...model.constraints === undefined ? {} : { parameterSchema: model.constraints }
|
|
22893
|
-
};
|
|
24024
|
+
return model.constraints === undefined ? {} : { parameterSchema: model.constraints };
|
|
22894
24025
|
},
|
|
22895
24026
|
docsUrl: (modelId) => looksLikeEdit(modelId) ? editDocs : generateDocs,
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
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"],
|
|
24030
|
+
async submit(request, target, http, context) {
|
|
22899
24031
|
if (request.references.length > 0) {
|
|
22900
|
-
return { wait: () => inParallel(request.n, target.providerId, () => edit(request, target, http)) };
|
|
22901
|
-
}
|
|
22902
|
-
if (model.kind === "edit") {
|
|
22903
|
-
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.`);
|
|
24032
|
+
return { wait: () => inParallel(request.n, target.providerId, () => edit(request, target, http, context)) };
|
|
22904
24033
|
}
|
|
22905
|
-
const images = await generate(request, target,
|
|
24034
|
+
const images = await generate(request, target, http, context);
|
|
22906
24035
|
return { wait: async () => images };
|
|
22907
24036
|
},
|
|
22908
24037
|
async testConnection(target, http) {
|
|
@@ -22924,9 +24053,10 @@ var venice = {
|
|
|
22924
24053
|
// src/protocols/index.ts
|
|
22925
24054
|
var adapters = {
|
|
22926
24055
|
"openai-images": openaiImages,
|
|
22927
|
-
gemini,
|
|
24056
|
+
gemini: gemini2,
|
|
22928
24057
|
apimart,
|
|
22929
|
-
venice
|
|
24058
|
+
venice,
|
|
24059
|
+
together
|
|
22930
24060
|
};
|
|
22931
24061
|
function supportedProtocols() {
|
|
22932
24062
|
return Object.keys(adapters);
|
|
@@ -22945,6 +24075,7 @@ function adapterFor(model) {
|
|
|
22945
24075
|
// src/jobs.ts
|
|
22946
24076
|
var defaultWaitLimitMs = 120000;
|
|
22947
24077
|
var defaultPollIntervalMs = 3000;
|
|
24078
|
+
var defaultRetryDelayMs = 2000;
|
|
22948
24079
|
|
|
22949
24080
|
class JobFailedError extends Error {
|
|
22950
24081
|
jobId;
|
|
@@ -22961,7 +24092,7 @@ class JobAbandoned extends Error {
|
|
|
22961
24092
|
super("litevision shut down while the Job was running.");
|
|
22962
24093
|
}
|
|
22963
24094
|
}
|
|
22964
|
-
var lostMessage = "litevision stopped before this Job finished, so its result is lost;
|
|
24095
|
+
var lostMessage = "litevision stopped before this Job finished, so its result is lost; start it again.";
|
|
22965
24096
|
|
|
22966
24097
|
class JobRunner {
|
|
22967
24098
|
deps;
|
|
@@ -22970,37 +24101,23 @@ class JobRunner {
|
|
|
22970
24101
|
closed = false;
|
|
22971
24102
|
waitLimitMs;
|
|
22972
24103
|
pollIntervalMs;
|
|
24104
|
+
retryDelayMs;
|
|
22973
24105
|
constructor(deps) {
|
|
22974
24106
|
this.deps = deps;
|
|
22975
24107
|
this.waitLimitMs = deps.waitLimitMs ?? defaultWaitLimitMs;
|
|
22976
24108
|
this.pollIntervalMs = deps.pollIntervalMs ?? defaultPollIntervalMs;
|
|
24109
|
+
this.retryDelayMs = deps.retryDelayMs ?? defaultRetryDelayMs;
|
|
22977
24110
|
}
|
|
22978
24111
|
async start(spec) {
|
|
22979
24112
|
const { request } = spec.normalised;
|
|
22980
|
-
const base = {
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
prompt: request.prompt,
|
|
22989
|
-
parameters: {
|
|
22990
|
-
n: request.n,
|
|
22991
|
-
aspectRatio: spec.normalised.requestedAspectRatio,
|
|
22992
|
-
size: request.size,
|
|
22993
|
-
references: request.references.map((reference) => reference.path),
|
|
22994
|
-
...spec.outputDir === undefined ? {} : { outputPath: spec.outputDir },
|
|
22995
|
-
providerOptions: request.providerOptions
|
|
22996
|
-
}
|
|
22997
|
-
};
|
|
22998
|
-
const target = {
|
|
22999
|
-
providerId: spec.provider.id,
|
|
23000
|
-
baseUrl: spec.provider.baseUrl,
|
|
23001
|
-
modelId: spec.model.modelId,
|
|
23002
|
-
credential: spec.credential
|
|
23003
|
-
};
|
|
24113
|
+
const base = this.baseFor(spec, "image", request.prompt, {
|
|
24114
|
+
n: request.n,
|
|
24115
|
+
requested: spec.normalised.requested,
|
|
24116
|
+
references: request.references.map((reference) => reference.path),
|
|
24117
|
+
...spec.outputDir === undefined ? {} : { outputPath: spec.outputDir },
|
|
24118
|
+
providerOptions: request.providerOptions
|
|
24119
|
+
});
|
|
24120
|
+
const target = targetOf(spec);
|
|
23004
24121
|
await this.append(base, "running");
|
|
23005
24122
|
const job = this.track(base, spec.credential, async (context) => {
|
|
23006
24123
|
const submitted = await spec.adapter.submit(request, target, this.deps.http, context);
|
|
@@ -23010,6 +24127,45 @@ class JobRunner {
|
|
|
23010
24127
|
});
|
|
23011
24128
|
return this.waitFor(base.model, base.id, this.register(base.id, job));
|
|
23012
24129
|
}
|
|
24130
|
+
async startVideo(spec) {
|
|
24131
|
+
const { request } = spec.normalised;
|
|
24132
|
+
const submitVideo = spec.adapter.submitVideo;
|
|
24133
|
+
if (submitVideo === undefined) {
|
|
24134
|
+
throw new ConfigError(`Model "${spec.model.alias}" uses Protocol "${spec.model.protocol}", which litevision cannot make videos with.`);
|
|
24135
|
+
}
|
|
24136
|
+
const base = this.baseFor(spec, "video", request.prompt, {
|
|
24137
|
+
n: 1,
|
|
24138
|
+
requested: spec.normalised.requested,
|
|
24139
|
+
references: request.references.map((reference) => reference.path),
|
|
24140
|
+
...request.firstFrame === undefined ? {} : { firstFrame: request.firstFrame.path },
|
|
24141
|
+
...request.lastFrame === undefined ? {} : { lastFrame: request.lastFrame.path },
|
|
24142
|
+
...spec.outputDir === undefined ? {} : { outputPath: spec.outputDir },
|
|
24143
|
+
providerOptions: request.providerOptions
|
|
24144
|
+
});
|
|
24145
|
+
const target = targetOf(spec);
|
|
24146
|
+
await this.append(base, "running");
|
|
24147
|
+
const job = this.track(base, spec.credential, async (context) => {
|
|
24148
|
+
const submitted = await submitVideo.call(spec.adapter, request, target, this.deps.http, context);
|
|
24149
|
+
await this.append(base, "running", { resume: submitted.resumeHandle });
|
|
24150
|
+
return videoAsImages(await submitted.wait());
|
|
24151
|
+
});
|
|
24152
|
+
return this.waitFor(base.model, base.id, this.register(base.id, job));
|
|
24153
|
+
}
|
|
24154
|
+
baseFor(spec, modality, prompt, parameters) {
|
|
24155
|
+
return {
|
|
24156
|
+
type: "job",
|
|
24157
|
+
id: randomUUID2(),
|
|
24158
|
+
createdAt: this.deps.now().toISOString(),
|
|
24159
|
+
model: spec.model.alias,
|
|
24160
|
+
provider: spec.provider.id,
|
|
24161
|
+
protocol: spec.model.protocol,
|
|
24162
|
+
modelId: spec.model.modelId,
|
|
24163
|
+
modality,
|
|
24164
|
+
prompt,
|
|
24165
|
+
parameters,
|
|
24166
|
+
...spec.comparison === undefined ? {} : { comparison: spec.comparison }
|
|
24167
|
+
};
|
|
24168
|
+
}
|
|
23013
24169
|
async get(id) {
|
|
23014
24170
|
const record = await readJob(this.deps.dirs, id);
|
|
23015
24171
|
if (record === undefined) {
|
|
@@ -23023,7 +24179,10 @@ class JobRunner {
|
|
|
23023
24179
|
id,
|
|
23024
24180
|
model: record.model,
|
|
23025
24181
|
files: record.files,
|
|
23026
|
-
|
|
24182
|
+
observed: record.observed ?? record.files.map(() => null),
|
|
24183
|
+
...record.shortfall === undefined ? {} : { shortfall: record.shortfall },
|
|
24184
|
+
...record.mismatch === undefined ? {} : { mismatch: record.mismatch },
|
|
24185
|
+
...record.parameters.requested === undefined ? {} : { requested: record.parameters.requested }
|
|
23027
24186
|
};
|
|
23028
24187
|
}
|
|
23029
24188
|
if (record.outcome === "failed") {
|
|
@@ -23040,7 +24199,11 @@ class JobRunner {
|
|
|
23040
24199
|
this.abandons.clear();
|
|
23041
24200
|
}
|
|
23042
24201
|
context = {
|
|
23043
|
-
pause: () =>
|
|
24202
|
+
pause: () => this.sleep(this.pollIntervalMs),
|
|
24203
|
+
backoff: (retry) => this.sleep(this.retryDelayMs * 2 ** (retry - 1))
|
|
24204
|
+
};
|
|
24205
|
+
sleep(ms) {
|
|
24206
|
+
return new Promise((resolve, reject) => {
|
|
23044
24207
|
if (this.closed)
|
|
23045
24208
|
return reject(new JobAbandoned);
|
|
23046
24209
|
const abandon = () => {
|
|
@@ -23050,10 +24213,10 @@ class JobRunner {
|
|
|
23050
24213
|
const timer = setTimeout(() => {
|
|
23051
24214
|
this.abandons.delete(abandon);
|
|
23052
24215
|
resolve();
|
|
23053
|
-
},
|
|
24216
|
+
}, ms);
|
|
23054
24217
|
this.abandons.add(abandon);
|
|
23055
|
-
})
|
|
23056
|
-
}
|
|
24218
|
+
});
|
|
24219
|
+
}
|
|
23057
24220
|
register(id, job) {
|
|
23058
24221
|
this.jobs.set(id, job);
|
|
23059
24222
|
job.catch((error) => {
|
|
@@ -23082,7 +24245,8 @@ class JobRunner {
|
|
|
23082
24245
|
const handle = record.resume;
|
|
23083
24246
|
const base = baseOf(record);
|
|
23084
24247
|
const adapter = adapterForProtocol(record.protocol);
|
|
23085
|
-
const
|
|
24248
|
+
const video = record.modality === "video";
|
|
24249
|
+
const resume = video ? adapter?.resumeVideo : adapter?.resume;
|
|
23086
24250
|
if (handle === undefined || adapter === undefined || resume === undefined) {
|
|
23087
24251
|
await this.append(base, "failed", { error: { kind: "other", message: lostMessage } });
|
|
23088
24252
|
throw new JobFailedError(record.id, "other", lostMessage);
|
|
@@ -23093,7 +24257,12 @@ class JobRunner {
|
|
|
23093
24257
|
}
|
|
23094
24258
|
const credential = await requireCredential(this.deps.dirs, provider);
|
|
23095
24259
|
const target = { providerId: provider.id, baseUrl: provider.baseUrl, modelId: record.modelId, credential };
|
|
23096
|
-
|
|
24260
|
+
if (video) {
|
|
24261
|
+
const resumeVideo = adapter.resumeVideo;
|
|
24262
|
+
return this.track(base, credential, async (context) => videoAsImages(await resumeVideo.call(adapter, handle, target, this.deps.http, context).wait()));
|
|
24263
|
+
}
|
|
24264
|
+
const resumeImages = adapter.resume;
|
|
24265
|
+
return this.track(base, credential, (context) => resumeImages.call(adapter, handle, target, this.deps.http, context).wait());
|
|
23097
24266
|
}
|
|
23098
24267
|
async track(base, credential, run) {
|
|
23099
24268
|
const safe = (text) => redact(text, credential);
|
|
@@ -23104,14 +24273,23 @@ class JobRunner {
|
|
|
23104
24273
|
throw new JobAbandoned;
|
|
23105
24274
|
const startedAt = new Date(base.createdAt);
|
|
23106
24275
|
const folder = base.parameters.outputPath ?? join4(this.deps.dirs.output, localDate(startedAt));
|
|
23107
|
-
const saved = await
|
|
24276
|
+
const saved = await saveFiles(this.deps.http, folder, base.id, startedAt, result.images, base.provider);
|
|
23108
24277
|
const reasons = [result.shortfall, ...saved.failures.map((failure) => failure.message)].filter((reason) => reason !== undefined);
|
|
23109
24278
|
if (saved.files.length === 0) {
|
|
23110
|
-
throw saved.failures[0] ?? providerError("other", base.provider, reasons[0] ?? "it returned
|
|
24279
|
+
throw saved.failures[0] ?? providerError("other", base.provider, reasons[0] ?? "it returned nothing.");
|
|
23111
24280
|
}
|
|
24281
|
+
const observed = saved.facts;
|
|
24282
|
+
const mismatch = mismatchOf(base.parameters, observed);
|
|
23112
24283
|
const shortfall = saved.files.length >= n ? undefined : safe(`Got ${saved.files.length} of ${n} images from ${providerLabel(base.provider)}.${reasons.length === 0 ? "" : ` ${reasons.join(" ")}`}`);
|
|
23113
|
-
await this.append(base, "completed", { files: saved.files, shortfall, cost: result.cost });
|
|
23114
|
-
return {
|
|
24284
|
+
await this.append(base, "completed", { files: saved.files, shortfall, cost: result.cost, observed, mismatch });
|
|
24285
|
+
return {
|
|
24286
|
+
id: base.id,
|
|
24287
|
+
files: saved.files,
|
|
24288
|
+
observed,
|
|
24289
|
+
...shortfall === undefined ? {} : { shortfall },
|
|
24290
|
+
...mismatch === undefined ? {} : { mismatch },
|
|
24291
|
+
...base.parameters.requested === undefined ? {} : { requested: base.parameters.requested }
|
|
24292
|
+
};
|
|
23115
24293
|
} catch (error) {
|
|
23116
24294
|
if (error instanceof JobAbandoned || this.closed)
|
|
23117
24295
|
throw new JobAbandoned;
|
|
@@ -23135,18 +24313,57 @@ class JobRunner {
|
|
|
23135
24313
|
}
|
|
23136
24314
|
}
|
|
23137
24315
|
function baseOf(record) {
|
|
23138
|
-
const { type, id, createdAt, model, provider, protocol, modelId, prompt, parameters } = record;
|
|
23139
|
-
return { type, id, createdAt, model, provider, protocol, modelId, prompt, parameters };
|
|
24316
|
+
const { type, id, createdAt, model, provider, protocol, modelId, modality, prompt, parameters, comparison } = record;
|
|
24317
|
+
return { type, id, createdAt, model, provider, protocol, modelId, ...modality === undefined ? {} : { modality }, prompt, parameters, ...comparison === undefined ? {} : { comparison } };
|
|
24318
|
+
}
|
|
24319
|
+
function targetOf(spec) {
|
|
24320
|
+
return { providerId: spec.provider.id, baseUrl: spec.provider.baseUrl, modelId: spec.model.modelId, credential: spec.credential };
|
|
24321
|
+
}
|
|
24322
|
+
function videoAsImages(result) {
|
|
24323
|
+
return { images: [result.video], ...result.cost === undefined ? {} : { cost: result.cost } };
|
|
24324
|
+
}
|
|
24325
|
+
var aspectTolerance = Math.log(1.05);
|
|
24326
|
+
var durationToleranceSeconds = 0.5;
|
|
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
|
+
}
|
|
24332
|
+
const { width, height } = parameters.providerOptions;
|
|
24333
|
+
if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
|
|
24334
|
+
return { ratio: width / height, asked: `${width}x${height} in provider_options` };
|
|
24335
|
+
}
|
|
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})` };
|
|
24339
|
+
}
|
|
24340
|
+
function mismatchOf(parameters, observed) {
|
|
24341
|
+
const { followsFirstFrame } = parameters;
|
|
24342
|
+
const seconds = parameters.requested !== undefined ? parameters.requested.seconds ?? undefined : parameters.seconds;
|
|
24343
|
+
const expected = expectedShape(parameters);
|
|
24344
|
+
const problems = new Set;
|
|
24345
|
+
for (const facts of observed) {
|
|
24346
|
+
if (facts === null)
|
|
24347
|
+
continue;
|
|
24348
|
+
if (!followsFirstFrame && expected !== undefined && Math.abs(Math.log(facts.width / facts.height / expected.ratio)) > aspectTolerance) {
|
|
24349
|
+
problems.add(`asked for ${expected.asked}, got ${facts.width}x${facts.height}`);
|
|
24350
|
+
}
|
|
24351
|
+
if (seconds !== undefined && facts.seconds !== undefined && Math.abs(facts.seconds - seconds) > durationToleranceSeconds) {
|
|
24352
|
+
problems.add(`asked for ${seconds} s, got ${facts.seconds} s`);
|
|
24353
|
+
}
|
|
24354
|
+
}
|
|
24355
|
+
return problems.size === 0 ? undefined : [...problems].join("; ");
|
|
23140
24356
|
}
|
|
23141
24357
|
function redact(text, credential) {
|
|
23142
24358
|
return text.split(credential).join("[redacted]");
|
|
23143
24359
|
}
|
|
23144
|
-
async function
|
|
24360
|
+
async function saveFiles(http, folder, jobId, startedAt, results, providerId) {
|
|
23145
24361
|
await mkdir2(folder, { recursive: true });
|
|
23146
24362
|
const base = `${localTime(startedAt)}-${jobId.slice(0, 8)}`;
|
|
23147
24363
|
const files = [];
|
|
24364
|
+
const facts = [];
|
|
23148
24365
|
const failures = [];
|
|
23149
|
-
for (const [index, image] of
|
|
24366
|
+
for (const [index, image] of results.entries()) {
|
|
23150
24367
|
let bytes;
|
|
23151
24368
|
try {
|
|
23152
24369
|
bytes = "bytes" in image ? image.bytes : await download(http, image.url, providerId);
|
|
@@ -23156,12 +24373,13 @@ async function saveImages(http, folder, jobId, startedAt, images, providerId) {
|
|
|
23156
24373
|
failures.push(error);
|
|
23157
24374
|
continue;
|
|
23158
24375
|
}
|
|
23159
|
-
const suffix =
|
|
23160
|
-
const file = join4(folder, `${base}${suffix}.${
|
|
24376
|
+
const suffix = results.length === 1 ? "" : `-${index + 1}`;
|
|
24377
|
+
const file = join4(folder, `${base}${suffix}.${fileTypeOf(bytes)?.extension ?? "bin"}`);
|
|
23161
24378
|
await writeFile(file, bytes);
|
|
23162
24379
|
files.push(file);
|
|
24380
|
+
facts.push(mediaFactsOf(bytes) ?? null);
|
|
23163
24381
|
}
|
|
23164
|
-
return { files, failures };
|
|
24382
|
+
return { files, facts, failures };
|
|
23165
24383
|
}
|
|
23166
24384
|
async function download(http, url, providerId) {
|
|
23167
24385
|
const response = await http(url).catch((error) => {
|
|
@@ -23271,21 +24489,21 @@ function isPlainObject3(value) {
|
|
|
23271
24489
|
}
|
|
23272
24490
|
|
|
23273
24491
|
// src/model-info.ts
|
|
24492
|
+
function videoModelInfo({ model }) {
|
|
24493
|
+
const adapter = adapterFor(model);
|
|
24494
|
+
if (adapter.submitVideo === undefined) {
|
|
24495
|
+
throw new ConfigError(`Model "${model.alias}" is a video Model, but its Protocol "${model.protocol}" makes no videos in litevision.`);
|
|
24496
|
+
}
|
|
24497
|
+
const guide = adapter.parameterGuide?.(model.modelId, "video");
|
|
24498
|
+
return {
|
|
24499
|
+
docsUrl: adapter.docsUrl(model.modelId, "video"),
|
|
24500
|
+
...guide === undefined ? {} : { guide }
|
|
24501
|
+
};
|
|
24502
|
+
}
|
|
23274
24503
|
var modelInfoTtlMs = 24 * 60 * 60 * 1000;
|
|
23275
24504
|
var defaultModelInfoTimeoutMs = 5000;
|
|
23276
|
-
var sizeSchema = object({
|
|
23277
|
-
aspectRatio: _enum(aspectRatios),
|
|
23278
|
-
width: number2().int().positive(),
|
|
23279
|
-
height: number2().int().positive()
|
|
23280
|
-
});
|
|
23281
|
-
var capabilitiesSchema = object({
|
|
23282
|
-
sizes: array(sizeSchema).min(1),
|
|
23283
|
-
maxReferences: number2().int().nonnegative(),
|
|
23284
|
-
maxImages: number2().int().positive()
|
|
23285
|
-
});
|
|
23286
24505
|
var entrySchema = object({
|
|
23287
24506
|
fetchedAt: string2(),
|
|
23288
|
-
capabilities: capabilitiesSchema.nullable(),
|
|
23289
24507
|
parameterSchema: unknown().optional()
|
|
23290
24508
|
});
|
|
23291
24509
|
var cachePath = (dirs) => join6(dirs.data, "capabilities.json");
|
|
@@ -23294,10 +24512,10 @@ function cacheKey({ model, provider }) {
|
|
|
23294
24512
|
}
|
|
23295
24513
|
async function modelInfo(deps, resolved, options = {}) {
|
|
23296
24514
|
const adapter = adapterFor(resolved.model);
|
|
24515
|
+
const guide = adapter.parameterGuide?.(resolved.model.modelId, "image");
|
|
23297
24516
|
const builtIn = {
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
docsUrl: adapter.docsUrl(resolved.model.modelId)
|
|
24517
|
+
docsUrl: adapter.docsUrl(resolved.model.modelId, "image"),
|
|
24518
|
+
...guide === undefined ? {} : { guide }
|
|
23301
24519
|
};
|
|
23302
24520
|
const fetchModelInfo = adapter.fetchModelInfo;
|
|
23303
24521
|
if (fetchModelInfo === undefined)
|
|
@@ -23322,13 +24540,9 @@ async function modelInfo(deps, resolved, options = {}) {
|
|
|
23322
24540
|
entry = cached;
|
|
23323
24541
|
}
|
|
23324
24542
|
}
|
|
23325
|
-
if (entry === undefined)
|
|
24543
|
+
if (entry?.parameterSchema === undefined)
|
|
23326
24544
|
return builtIn;
|
|
23327
|
-
return {
|
|
23328
|
-
...builtIn,
|
|
23329
|
-
...entry.capabilities === null ? {} : { capabilities: entry.capabilities, source: "provider" },
|
|
23330
|
-
...entry.parameterSchema === undefined ? {} : { parameterSchema: entry.parameterSchema }
|
|
23331
|
-
};
|
|
24545
|
+
return { ...builtIn, parameterSchema: entry.parameterSchema };
|
|
23332
24546
|
}
|
|
23333
24547
|
async function refreshModelInfo(deps, alias) {
|
|
23334
24548
|
await refreshMatching(deps, (resolved) => resolved.model.alias === alias);
|
|
@@ -23349,12 +24563,7 @@ async function refreshMatching(deps, wanted) {
|
|
|
23349
24563
|
} catch {}
|
|
23350
24564
|
}
|
|
23351
24565
|
function toEntry(info, now) {
|
|
23352
|
-
|
|
23353
|
-
return {
|
|
23354
|
-
fetchedAt: now.toISOString(),
|
|
23355
|
-
capabilities: capabilities.success ? capabilities.data : null,
|
|
23356
|
-
...info?.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema }
|
|
23357
|
-
};
|
|
24566
|
+
return { fetchedAt: now.toISOString(), ...info?.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema } };
|
|
23358
24567
|
}
|
|
23359
24568
|
async function readCache(dirs) {
|
|
23360
24569
|
let value;
|
|
@@ -23427,7 +24636,7 @@ function updateNotesAsAgent(dirs, alias, text, at) {
|
|
|
23427
24636
|
throw needsSettings(`The Models in config.json are invalid: ${prettifyError(parsed.error)}`);
|
|
23428
24637
|
const stored = models;
|
|
23429
24638
|
if (stored.length === 0)
|
|
23430
|
-
throw needsSettings("litevision has no
|
|
24639
|
+
throw needsSettings("litevision has no Model configured yet.");
|
|
23431
24640
|
const model = stored.find((candidate) => candidate.alias === alias);
|
|
23432
24641
|
if (model === undefined) {
|
|
23433
24642
|
throw new ConfigError(`Unknown Model Alias "${alias}". Known Aliases: ${stored.map((candidate) => candidate.alias).join(", ")}.`);
|
|
@@ -23438,60 +24647,85 @@ function updateNotesAsAgent(dirs, alias, text, at) {
|
|
|
23438
24647
|
});
|
|
23439
24648
|
}
|
|
23440
24649
|
|
|
23441
|
-
// src/protocols/
|
|
23442
|
-
function
|
|
23443
|
-
const
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
}
|
|
23463
|
-
async function normaliseImageRequest(parameters, model, adapter,
|
|
23464
|
-
|
|
23465
|
-
const unsupported = (problem) => new ConfigError(`${problem} Model "${model.alias}" supports: ${describeCapabilities(capabilities)} Choose another Model or change the request.`);
|
|
23466
|
-
if (parameters.n > capabilities.maxImages) {
|
|
23467
|
-
throw unsupported(`Model "${model.alias}" cannot generate ${parameters.n} images in one Job.`);
|
|
23468
|
-
}
|
|
23469
|
-
if (parameters.references.length > 0 && capabilities.maxReferences === 0) {
|
|
23470
|
-
throw unsupported(`Model "${model.alias}" cannot take reference images.`);
|
|
23471
|
-
}
|
|
23472
|
-
if (parameters.references.length > capabilities.maxReferences) {
|
|
23473
|
-
throw unsupported(`Model "${model.alias}" cannot take ${parameters.references.length} reference images.`);
|
|
23474
|
-
}
|
|
23475
|
-
const reserved = Object.keys(parameters.providerOptions).filter((key) => adapter.reservedOptions.includes(key));
|
|
23476
|
-
if (reserved.length > 0) {
|
|
23477
|
-
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.`);
|
|
23478
|
-
}
|
|
23479
|
-
const { size, adjusted } = closestSize(capabilities.sizes, parameters.aspectRatio);
|
|
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`");
|
|
23480
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
|
+
]);
|
|
23481
24688
|
return {
|
|
23482
24689
|
request: {
|
|
23483
24690
|
prompt: parameters.prompt,
|
|
23484
|
-
|
|
23485
|
-
|
|
24691
|
+
...firstFrame === undefined ? {} : { firstFrame },
|
|
24692
|
+
...lastFrame === undefined ? {} : { lastFrame },
|
|
23486
24693
|
references,
|
|
23487
|
-
providerOptions: parameters.providerOptions
|
|
23488
|
-
model: { capabilities, ...described.parameterSchema === undefined ? {} : { parameterSchema: described.parameterSchema } }
|
|
24694
|
+
providerOptions: parameters.providerOptions
|
|
23489
24695
|
},
|
|
23490
|
-
|
|
23491
|
-
aspectRatioAdjusted: adjusted
|
|
24696
|
+
requested: readRequested(fieldsToRead(adapter, model.modelId, "video"), parameters.providerOptions, "video")
|
|
23492
24697
|
};
|
|
23493
24698
|
}
|
|
23494
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
|
+
|
|
23495
24729
|
// src/settings/browser.ts
|
|
23496
24730
|
import { spawn } from "node:child_process";
|
|
23497
24731
|
var openInBrowser = (url) => {
|
|
@@ -23522,7 +24756,8 @@ var providerPresets = [
|
|
|
23522
24756
|
{ id: "openai", label: "OpenAI", baseUrl: "https://api.openai.com/v1", protocol: "openai-images" },
|
|
23523
24757
|
{ id: "gemini", label: "Gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", protocol: "gemini" },
|
|
23524
24758
|
{ id: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", protocol: "venice" },
|
|
23525
|
-
{ id: "apimart", label: "APIMart", baseUrl: "https://api.apimart.ai/v1", protocol: "apimart" }
|
|
24759
|
+
{ id: "apimart", label: "APIMart", baseUrl: "https://api.apimart.ai/v1", protocol: "apimart" },
|
|
24760
|
+
{ id: "together", label: "Together AI", baseUrl: "https://api.together.ai", protocol: "together" }
|
|
23526
24761
|
];
|
|
23527
24762
|
function shortName(what) {
|
|
23528
24763
|
return string2().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/, `${what} is 1–64 letters, digits, '.', '_' or '-', starting with a letter or digit.`);
|
|
@@ -23632,7 +24867,7 @@ async function testConnection(dirs, http, providerId, protocol) {
|
|
|
23632
24867
|
}
|
|
23633
24868
|
return { result: outcome.ok ? "success" : "failure", message: call.redact(outcome.message) };
|
|
23634
24869
|
}
|
|
23635
|
-
async function
|
|
24870
|
+
async function fetchModelIds5(dirs, http, providerId, protocol) {
|
|
23636
24871
|
const typeInstead = "You can still type the model id.";
|
|
23637
24872
|
const call = await prepare(dirs, providerId, protocol, (adapter) => adapter.listModelIds);
|
|
23638
24873
|
if (call === "unsupported") {
|
|
@@ -23644,7 +24879,7 @@ async function fetchModelIds4(dirs, http, providerId, protocol) {
|
|
|
23644
24879
|
try {
|
|
23645
24880
|
const list = await call.run(http);
|
|
23646
24881
|
if (list.ok)
|
|
23647
|
-
return { result: "success", modelIds: list.modelIds };
|
|
24882
|
+
return { result: "success", modelIds: list.modelIds, ...list.modalities === undefined ? {} : { modalities: list.modalities } };
|
|
23648
24883
|
message = list.message;
|
|
23649
24884
|
} catch (error) {
|
|
23650
24885
|
message = unreachable(providerId, error);
|
|
@@ -23675,24 +24910,69 @@ function unreachable(providerId, error) {
|
|
|
23675
24910
|
// src/settings/job-history.ts
|
|
23676
24911
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
23677
24912
|
var rateJobInput = object({
|
|
24913
|
+
result: number2({ message: "Choose a result by its number." }).int("Choose a result by its number.").positive("Choose a result by its number.").optional(),
|
|
23678
24914
|
verdict: _enum(verdicts, { message: "Choose good or bad." }),
|
|
23679
24915
|
reason: string2().max(ratingReasonMaxLength, `Keep the reason to at most ${ratingReasonMaxLength} characters.`)
|
|
23680
24916
|
});
|
|
24917
|
+
var pickInput = object({
|
|
24918
|
+
jobId: string2({ message: "Choose one of the Comparison's Jobs." }).min(1, "Choose one of the Comparison's Jobs."),
|
|
24919
|
+
result: number2({ message: "Choose a result by its number." }).int("Choose a result by its number.").positive("Choose a result by its number.").optional()
|
|
24920
|
+
});
|
|
23681
24921
|
async function listJobHistory(dirs) {
|
|
23682
|
-
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
|
|
23695
|
-
|
|
24922
|
+
const listed = (await readRatedJobs(dirs)).map((rated) => {
|
|
24923
|
+
const { job, pick } = rated;
|
|
24924
|
+
const listedRatingOf = (result) => {
|
|
24925
|
+
const current = ratingOf(rated, result);
|
|
24926
|
+
return current === undefined ? null : listedRating(current);
|
|
24927
|
+
};
|
|
24928
|
+
return {
|
|
24929
|
+
id: job.id,
|
|
24930
|
+
createdAt: job.createdAt,
|
|
24931
|
+
model: job.model,
|
|
24932
|
+
modelId: job.modelId,
|
|
24933
|
+
provider: job.provider,
|
|
24934
|
+
prompt: job.prompt,
|
|
24935
|
+
durationMs: job.durationMs,
|
|
24936
|
+
outcome: job.outcome,
|
|
24937
|
+
modality: job.modality ?? "image",
|
|
24938
|
+
files: job.files.map((path, index) => ({
|
|
24939
|
+
path,
|
|
24940
|
+
thumbnail: `/api/jobs/${encodeURIComponent(job.id)}/files/${index}`,
|
|
24941
|
+
rating: listedRatingOf(index + 1)
|
|
24942
|
+
})),
|
|
24943
|
+
rating: listedRatingOf(),
|
|
24944
|
+
...job.error === undefined ? {} : { error: job.error.message },
|
|
24945
|
+
...job.shortfall === undefined ? {} : { shortfall: job.shortfall },
|
|
24946
|
+
...job.mismatch === undefined ? {} : { mismatch: `The result does not match the request: ${job.mismatch}.` },
|
|
24947
|
+
...job.comparison === undefined ? {} : { comparison: { id: job.comparison, pick: pick === undefined ? null : listedPick(pick) } }
|
|
24948
|
+
};
|
|
24949
|
+
});
|
|
24950
|
+
const byComparison = new Map;
|
|
24951
|
+
for (const job of listed) {
|
|
24952
|
+
if (job.comparison === undefined)
|
|
24953
|
+
continue;
|
|
24954
|
+
const ofComparison = byComparison.get(job.comparison.id) ?? [];
|
|
24955
|
+
byComparison.set(job.comparison.id, ofComparison);
|
|
24956
|
+
ofComparison.unshift(job);
|
|
24957
|
+
}
|
|
24958
|
+
return listed.flatMap((job) => {
|
|
24959
|
+
if (job.comparison === undefined)
|
|
24960
|
+
return [job];
|
|
24961
|
+
const ofComparison = byComparison.get(job.comparison.id);
|
|
24962
|
+
if (ofComparison === undefined)
|
|
24963
|
+
return [];
|
|
24964
|
+
byComparison.delete(job.comparison.id);
|
|
24965
|
+
return ofComparison;
|
|
24966
|
+
});
|
|
24967
|
+
}
|
|
24968
|
+
async function pickFromPage(dirs, comparison, input, now) {
|
|
24969
|
+
try {
|
|
24970
|
+
return listedPick(await pickResult(dirs, { comparison, ...input }, now));
|
|
24971
|
+
} catch (error) {
|
|
24972
|
+
if (!(error instanceof PickError))
|
|
24973
|
+
throw error;
|
|
24974
|
+
throw new SettingsError({ "unknown-comparison": 404, "not-in-comparison": 400, "not-completed": 409, invalid: 400 }[error.kind], error.message);
|
|
24975
|
+
}
|
|
23696
24976
|
}
|
|
23697
24977
|
async function rateJobFromPage(dirs, jobId, input, now) {
|
|
23698
24978
|
try {
|
|
@@ -23707,18 +24987,21 @@ async function jobImage(dirs, jobId, index) {
|
|
|
23707
24987
|
const job = await readJob(dirs, jobId);
|
|
23708
24988
|
const path = /^\d+$/.test(index) ? job?.files[Number(index)] : undefined;
|
|
23709
24989
|
if (path === undefined)
|
|
23710
|
-
throw new SettingsError(404, "No such
|
|
24990
|
+
throw new SettingsError(404, "No such file in the Job history.");
|
|
23711
24991
|
let bytes;
|
|
23712
24992
|
try {
|
|
23713
24993
|
bytes = await readFile6(path);
|
|
23714
24994
|
} catch {
|
|
23715
|
-
throw new SettingsError(404, "The
|
|
24995
|
+
throw new SettingsError(404, "The file is no longer on disk.");
|
|
23716
24996
|
}
|
|
23717
|
-
const type =
|
|
24997
|
+
const type = fileTypeOf(bytes);
|
|
23718
24998
|
if (type === undefined)
|
|
23719
|
-
throw new SettingsError(404, "The file is no longer an image.");
|
|
24999
|
+
throw new SettingsError(404, "The file is no longer an image or a video.");
|
|
23720
25000
|
return { bytes, contentType: type.mimeType };
|
|
23721
25001
|
}
|
|
25002
|
+
function listedPick(pick) {
|
|
25003
|
+
return { jobId: pick.jobId, ...pick.result === undefined ? {} : { result: pick.result }, pickedAt: pick.pickedAt };
|
|
25004
|
+
}
|
|
23722
25005
|
function listedRating(rating) {
|
|
23723
25006
|
return { verdict: rating.verdict, reason: rating.reason, ratedAt: rating.ratedAt };
|
|
23724
25007
|
}
|
|
@@ -23731,14 +25014,19 @@ var addModelInput = object({
|
|
|
23731
25014
|
provider: string2().min(1, "Choose a Provider."),
|
|
23732
25015
|
modelId: string2().trim().min(1, "Enter a model id.").max(200, "A model id is at most 200 characters."),
|
|
23733
25016
|
protocol: string2().refine((value) => supportedProtocols().includes(value), { error: () => `Choose a supported Protocol: ${supportedProtocols().join(", ")}.` }),
|
|
23734
|
-
|
|
25017
|
+
modality: _enum(modalities, { error: () => "Choose whether the Model makes images or videos." }).default("image"),
|
|
25018
|
+
notes: notes.optional(),
|
|
25019
|
+
standingOptions: unknown().optional()
|
|
23735
25020
|
});
|
|
23736
|
-
var updateModelInput = addModelInput.partial();
|
|
25021
|
+
var updateModelInput = addModelInput.omit({ modality: true }).partial().extend({ modality: addModelInput.shape.modality.unwrap().optional() });
|
|
23737
25022
|
var storedModels = array(modelSchema);
|
|
23738
25023
|
async function listModels2(dirs) {
|
|
23739
25024
|
const config = await readConfigObject(dirs);
|
|
23740
|
-
const
|
|
23741
|
-
|
|
25025
|
+
const defaults = {
|
|
25026
|
+
defaultImageModel: typeof config.defaultImageModel === "string" ? config.defaultImageModel : null,
|
|
25027
|
+
defaultVideoModel: typeof config.defaultVideoModel === "string" ? config.defaultVideoModel : null
|
|
25028
|
+
};
|
|
25029
|
+
return { models: modelsOf(config).map((model) => view2(model, defaults)), ...defaults };
|
|
23742
25030
|
}
|
|
23743
25031
|
async function getModel(dirs, wanted) {
|
|
23744
25032
|
const model = (await listModels2(dirs)).models.find((candidate) => candidate.alias === wanted);
|
|
@@ -23751,11 +25039,19 @@ async function addModel(dirs, input, at) {
|
|
|
23751
25039
|
const models = modelsOf(config);
|
|
23752
25040
|
requireFreeAlias(models, input.alias);
|
|
23753
25041
|
requireProvider(config, input.provider);
|
|
23754
|
-
const stored = {
|
|
25042
|
+
const stored = {
|
|
25043
|
+
alias: input.alias,
|
|
25044
|
+
provider: input.provider,
|
|
25045
|
+
modelId: input.modelId,
|
|
25046
|
+
protocol: input.protocol,
|
|
25047
|
+
modality: input.modality
|
|
25048
|
+
};
|
|
23755
25049
|
if (input.notes) {
|
|
23756
25050
|
stored.notes = input.notes;
|
|
23757
25051
|
stored.notesHistory = { changedBy: "User", changedAt: at.toISOString() };
|
|
23758
25052
|
}
|
|
25053
|
+
if (input.standingOptions !== undefined)
|
|
25054
|
+
setStandingOptions(stored, input.standingOptions);
|
|
23759
25055
|
config.models = [...models, stored];
|
|
23760
25056
|
});
|
|
23761
25057
|
return getModel(dirs, input.alias);
|
|
@@ -23769,8 +25065,14 @@ async function updateModel(dirs, wanted, input, at) {
|
|
|
23769
25065
|
if (input.alias !== undefined && input.alias !== wanted) {
|
|
23770
25066
|
requireFreeAlias(models, input.alias);
|
|
23771
25067
|
model.alias = input.alias;
|
|
23772
|
-
|
|
23773
|
-
config
|
|
25068
|
+
for (const key of Object.values(defaultKey))
|
|
25069
|
+
if (config[key] === wanted)
|
|
25070
|
+
config[key] = input.alias;
|
|
25071
|
+
}
|
|
25072
|
+
if (input.modality !== undefined && input.modality !== modalityOf(model)) {
|
|
25073
|
+
if (config[defaultKey[modalityOf(model)]] === model.alias)
|
|
25074
|
+
delete config[defaultKey[modalityOf(model)]];
|
|
25075
|
+
model.modality = input.modality;
|
|
23774
25076
|
}
|
|
23775
25077
|
if (input.provider !== undefined) {
|
|
23776
25078
|
requireProvider(config, input.provider);
|
|
@@ -23782,6 +25084,8 @@ async function updateModel(dirs, wanted, input, at) {
|
|
|
23782
25084
|
model.protocol = input.protocol;
|
|
23783
25085
|
if (input.notes !== undefined)
|
|
23784
25086
|
replaceNotes(model, input.notes, "User", at);
|
|
25087
|
+
if (input.standingOptions !== undefined)
|
|
25088
|
+
setStandingOptions(model, input.standingOptions);
|
|
23785
25089
|
});
|
|
23786
25090
|
return getModel(dirs, input.alias ?? wanted);
|
|
23787
25091
|
}
|
|
@@ -23801,22 +25105,38 @@ async function removeModel(dirs, wanted) {
|
|
|
23801
25105
|
if (!models.some((model) => model.alias === wanted))
|
|
23802
25106
|
throw notFound2(wanted);
|
|
23803
25107
|
config.models = models.filter((model) => model.alias !== wanted);
|
|
23804
|
-
|
|
23805
|
-
|
|
25108
|
+
for (const key of Object.values(defaultKey))
|
|
25109
|
+
if (config[key] === wanted)
|
|
25110
|
+
delete config[key];
|
|
23806
25111
|
});
|
|
23807
25112
|
}
|
|
23808
25113
|
var setDefaultInput = object({ alias: string2().nullable() });
|
|
23809
|
-
async function
|
|
25114
|
+
async function setDefaultModel(dirs, modality, wanted) {
|
|
25115
|
+
const key = defaultKey[modality];
|
|
23810
25116
|
await updateConfig(dirs, (config) => {
|
|
23811
25117
|
if (wanted === null) {
|
|
23812
|
-
delete config
|
|
25118
|
+
delete config[key];
|
|
23813
25119
|
return;
|
|
23814
25120
|
}
|
|
23815
|
-
|
|
25121
|
+
const model = modelsOf(config).find((candidate) => candidate.alias === wanted);
|
|
25122
|
+
if (model === undefined)
|
|
23816
25123
|
throw notFound2(wanted);
|
|
23817
|
-
|
|
25124
|
+
if (modalityOf(model) !== modality) {
|
|
25125
|
+
throw new SettingsError(400, `"${wanted}" is ${modalityOf(model) === "image" ? "an image" : "a video"} Model, so it cannot be the default ${modality} Model.`);
|
|
25126
|
+
}
|
|
25127
|
+
config[key] = wanted;
|
|
23818
25128
|
});
|
|
23819
|
-
return {
|
|
25129
|
+
return { alias: wanted };
|
|
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;
|
|
23820
25140
|
}
|
|
23821
25141
|
function requireFreeAlias(models, wanted) {
|
|
23822
25142
|
if (models.some((model) => model.alias === wanted)) {
|
|
@@ -23837,15 +25157,21 @@ function modelsOf(config) {
|
|
|
23837
25157
|
throw new SettingsError(409, `The Models in config.json are invalid: ${prettifyError(parsed.error)}`);
|
|
23838
25158
|
return config.models;
|
|
23839
25159
|
}
|
|
23840
|
-
function
|
|
25160
|
+
function modalityOf(model) {
|
|
25161
|
+
return model.modality ?? "image";
|
|
25162
|
+
}
|
|
25163
|
+
function view2(model, defaults) {
|
|
25164
|
+
const modality = modalityOf(model);
|
|
23841
25165
|
return {
|
|
23842
25166
|
alias: model.alias,
|
|
23843
25167
|
provider: model.provider,
|
|
23844
25168
|
modelId: model.modelId,
|
|
23845
25169
|
protocol: model.protocol,
|
|
25170
|
+
modality,
|
|
23846
25171
|
notes: model.notes ?? "",
|
|
23847
25172
|
...model.notesHistory && { notesHistory: model.notesHistory },
|
|
23848
|
-
|
|
25173
|
+
...model.standingOptions && { standingOptions: model.standingOptions },
|
|
25174
|
+
default: model.alias === (modality === "video" ? defaults.defaultVideoModel : defaults.defaultImageModel)
|
|
23849
25175
|
};
|
|
23850
25176
|
}
|
|
23851
25177
|
function notFound2(wanted) {
|
|
@@ -23910,7 +25236,7 @@ var routes = [
|
|
|
23910
25236
|
path: "/api/providers/:id/model-ids",
|
|
23911
25237
|
handler: async ({ dirs, http, params, body }) => {
|
|
23912
25238
|
const { protocol } = parse6(protocolInput, body);
|
|
23913
|
-
return { body: await
|
|
25239
|
+
return { body: await fetchModelIds5(dirs, http, params.id, protocol) };
|
|
23914
25240
|
}
|
|
23915
25241
|
},
|
|
23916
25242
|
{
|
|
@@ -23957,7 +25283,16 @@ var routes = [
|
|
|
23957
25283
|
{
|
|
23958
25284
|
method: "PUT",
|
|
23959
25285
|
path: "/api/default-image-model",
|
|
23960
|
-
handler: async ({ dirs, body }) => ({
|
|
25286
|
+
handler: async ({ dirs, body }) => ({
|
|
25287
|
+
body: { defaultImageModel: (await setDefaultModel(dirs, "image", parse6(setDefaultInput, body).alias)).alias }
|
|
25288
|
+
})
|
|
25289
|
+
},
|
|
25290
|
+
{
|
|
25291
|
+
method: "PUT",
|
|
25292
|
+
path: "/api/default-video-model",
|
|
25293
|
+
handler: async ({ dirs, body }) => ({
|
|
25294
|
+
body: { defaultVideoModel: (await setDefaultModel(dirs, "video", parse6(setDefaultInput, body).alias)).alias }
|
|
25295
|
+
})
|
|
23961
25296
|
},
|
|
23962
25297
|
{
|
|
23963
25298
|
method: "GET",
|
|
@@ -23971,6 +25306,13 @@ var routes = [
|
|
|
23971
25306
|
body: { rating: await rateJobFromPage(dirs, params.id, parse6(rateJobInput, body), now()) }
|
|
23972
25307
|
})
|
|
23973
25308
|
},
|
|
25309
|
+
{
|
|
25310
|
+
method: "POST",
|
|
25311
|
+
path: "/api/comparisons/:id/pick",
|
|
25312
|
+
handler: async ({ dirs, now, params, body }) => ({
|
|
25313
|
+
body: { pick: await pickFromPage(dirs, params.id, parse6(pickInput, body), now()) }
|
|
25314
|
+
})
|
|
25315
|
+
},
|
|
23974
25316
|
{
|
|
23975
25317
|
method: "GET",
|
|
23976
25318
|
path: "/api/jobs/:id/files/:index",
|
|
@@ -24119,11 +25461,20 @@ function renderPage(nonce) {
|
|
|
24119
25461
|
.prompt { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
24120
25462
|
.thumbs { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
24121
25463
|
.thumb { width: 96px; height: 96px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); }
|
|
25464
|
+
.thumb.video { width: 192px; height: 108px; }
|
|
24122
25465
|
.thumb.placeholder { display: flex; align-items: center; justify-content: center; text-align: center; padding: 6px;
|
|
24123
25466
|
font-size: 11px; color: var(--muted); border-style: dashed; }
|
|
25467
|
+
.result-thumb { margin: 0; display: grid; gap: 4px; width: min-content; }
|
|
25468
|
+
.result-thumb figcaption { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
|
|
24124
25469
|
.actions.rate { display: grid; gap: 10px; }
|
|
25470
|
+
.inline > select.rate-target { flex: 0 0 auto; }
|
|
24125
25471
|
.rating { font-size: 13px; }
|
|
24126
25472
|
.rating.good { color: var(--ok); } .rating.bad { color: var(--danger); }
|
|
25473
|
+
.comparison { display: grid; gap: 12px; border: 1px solid var(--border); border-left: 3px solid var(--accent);
|
|
25474
|
+
border-radius: 12px; padding: 16px; margin-bottom: 12px; }
|
|
25475
|
+
.comparison > .card { margin-bottom: 0; }
|
|
25476
|
+
.comparison-head p { font-size: 13px; color: var(--muted); margin-top: 2px; }
|
|
25477
|
+
.picked { color: var(--accent); font-weight: 500; }
|
|
24127
25478
|
|
|
24128
25479
|
#status { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); max-width: calc(100% - 32px);
|
|
24129
25480
|
background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 8px; font-size: 13px; display: none;
|
|
@@ -24166,6 +25517,7 @@ function renderPage(nonce) {
|
|
|
24166
25517
|
<div class="fields">
|
|
24167
25518
|
<div class="field"><label for="model-provider">Provider</label><select id="model-provider" required></select></div>
|
|
24168
25519
|
<div class="field"><label for="model-protocol">Protocol</label><select id="model-protocol" required></select></div>
|
|
25520
|
+
<div class="field"><label for="model-modality">Makes</label><select id="model-modality" required><option value="image">Images</option><option value="video">Videos</option></select></div>
|
|
24169
25521
|
<div class="field wide">
|
|
24170
25522
|
<label for="model-id">Model id</label>
|
|
24171
25523
|
<div class="inline"><input id="model-id" required spellcheck="false" list="model-ids" placeholder="gpt-image-1"><button id="fetch-models" type="button">Fetch model list</button></div>
|
|
@@ -24182,7 +25534,7 @@ function renderPage(nonce) {
|
|
|
24182
25534
|
<section id="history">
|
|
24183
25535
|
<div class="section-head">
|
|
24184
25536
|
<h2>Job history</h2>
|
|
24185
|
-
<p>Newest first. Rate a Job to keep your verdict on it; rating it again replaces the earlier Rating.</p>
|
|
25537
|
+
<p>Newest first. Rate a Job, or one of its results, to keep your verdict on it; rating it again replaces the earlier Rating. In a Comparison, pick the result you prefer; picking again replaces the earlier Pick.</p>
|
|
24186
25538
|
</div>
|
|
24187
25539
|
<div id="job-list"><p class="muted">Loading…</p></div>
|
|
24188
25540
|
</section>
|
|
@@ -24390,25 +25742,52 @@ function notesHistoryRow(model, path) {
|
|
|
24390
25742
|
return [el("div", { className: "note" }, ...parts)];
|
|
24391
25743
|
}
|
|
24392
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
|
+
|
|
24393
25770
|
function modelCard(model) {
|
|
24394
25771
|
const alias = el("input", { value: model.alias, spellcheck: false });
|
|
24395
25772
|
const provider = el("select", {}, ...options(providerIds.includes(model.provider) ? providerIds : [model.provider, ...providerIds], model.provider));
|
|
24396
25773
|
const modelId = el("input", { value: model.modelId, spellcheck: false });
|
|
24397
25774
|
const protocol = el("select", {}, ...options(protocols.includes(model.protocol) ? protocols : [model.protocol, ...protocols], model.protocol));
|
|
25775
|
+
const modality = el("select", {}, ...options(["image", "video"], model.modality));
|
|
24398
25776
|
const notes = notesField(model.notes);
|
|
25777
|
+
const standing = standingOptionsField(model.standingOptions);
|
|
24399
25778
|
const path = "/api/models/" + encodeURIComponent(model.alias);
|
|
24400
25779
|
|
|
24401
25780
|
const save = el("button", { type: "button", className: "primary", textContent: "Save" });
|
|
24402
25781
|
save.onclick = () => act(save, async () => {
|
|
24403
|
-
await api("PATCH", path, { alias: alias.value, provider: provider.value, modelId: modelId.value, protocol: protocol.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() });
|
|
24404
25783
|
await loadModels();
|
|
24405
25784
|
}, "Model saved.");
|
|
24406
25785
|
|
|
24407
25786
|
const makeDefault = el("button", { type: "button", className: "small", textContent: "Make default" });
|
|
24408
25787
|
makeDefault.onclick = () => act(makeDefault, async () => {
|
|
24409
|
-
await api("PUT", "/api/default-
|
|
25788
|
+
await api("PUT", "/api/default-" + model.modality + "-model", { alias: model.alias });
|
|
24410
25789
|
await loadModels();
|
|
24411
|
-
}, model.alias + " is now the default
|
|
25790
|
+
}, model.alias + " is now the default " + model.modality + " Model.");
|
|
24412
25791
|
|
|
24413
25792
|
const remove = el("button", { type: "button", className: "danger", textContent: "Remove Model" });
|
|
24414
25793
|
remove.onclick = () => {
|
|
@@ -24423,16 +25802,23 @@ function modelCard(model) {
|
|
|
24423
25802
|
|
|
24424
25803
|
return el("div", { className: "card" },
|
|
24425
25804
|
el("div", { className: "card-head" }, el("h3", { textContent: model.alias }),
|
|
24426
|
-
model.default ? el("span", { className: "chip accent", textContent: "Default" }) : makeDefault),
|
|
25805
|
+
model.default ? el("span", { className: "chip accent", textContent: "Default " + model.modality }) : makeDefault),
|
|
24427
25806
|
el("div", { className: "fields" },
|
|
24428
|
-
field("Alias", alias), field("Provider", provider), field("Model id", modelId), field("Protocol", protocol), notes.wrapper),
|
|
25807
|
+
field("Alias", alias), field("Provider", provider), field("Model id", modelId), field("Protocol", protocol), field("Makes", modality), notes.wrapper, standing.wrapper),
|
|
24429
25808
|
el("div", { className: "actions" }, save, el("span", { className: "spacer" }), remove),
|
|
24430
25809
|
);
|
|
24431
25810
|
}
|
|
24432
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
|
+
|
|
24433
25817
|
async function loadModels() {
|
|
24434
25818
|
const list = document.getElementById("model-list");
|
|
24435
25819
|
const { models } = await api("GET", "/api/models");
|
|
25820
|
+
savedModels = models;
|
|
25821
|
+
showModelIds();
|
|
24436
25822
|
list.replaceChildren(...(models.length === 0
|
|
24437
25823
|
? [emptyState(providerIds.length === 0 ? "Add a Provider first, then add its Models below." : "No Models yet. Add one below.")]
|
|
24438
25824
|
: models.map(modelCard)));
|
|
@@ -24447,27 +25833,57 @@ function setUpAddModelForm() {
|
|
|
24447
25833
|
const fetchButton = document.getElementById("fetch-models");
|
|
24448
25834
|
const fetchResult = document.getElementById("fetch-result");
|
|
24449
25835
|
const ids = document.getElementById("model-ids");
|
|
25836
|
+
const modality = document.getElementById("model-modality");
|
|
25837
|
+
// The model ids the last fetch found, and what each one makes when the Provider says.
|
|
25838
|
+
let fetchedIds = [];
|
|
25839
|
+
let knownModalities = {};
|
|
24450
25840
|
protocol.replaceChildren(...options(protocols));
|
|
24451
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
|
+
|
|
24452
25864
|
// Suggest the Protocol a preset Provider's Models usually speak; the User can still change it.
|
|
24453
25865
|
provider.onchange = () => {
|
|
24454
25866
|
const suggested = presets.find((p) => p.id === provider.value);
|
|
24455
25867
|
if (suggested && protocols.includes(suggested.protocol)) protocol.value = suggested.protocol;
|
|
24456
|
-
|
|
25868
|
+
forgetModelIds();
|
|
24457
25869
|
fetchResult.textContent = "";
|
|
24458
25870
|
};
|
|
24459
|
-
protocol.onchange =
|
|
25871
|
+
protocol.onchange = forgetModelIds;
|
|
25872
|
+
modality.onchange = () => showModelIds();
|
|
24460
25873
|
|
|
24461
25874
|
fetchButton.onclick = () => act(fetchButton, async () => {
|
|
24462
25875
|
fetchResult.textContent = "Fetching…";
|
|
24463
25876
|
fetchResult.className = "result";
|
|
24464
25877
|
const outcome = await api("POST", "/api/providers/" + encodeURIComponent(provider.value) + "/model-ids", { protocol: protocol.value });
|
|
24465
25878
|
if (outcome.result === "success") {
|
|
24466
|
-
|
|
24467
|
-
|
|
25879
|
+
fetchedIds = outcome.modelIds;
|
|
25880
|
+
knownModalities = outcome.modalities || {};
|
|
24468
25881
|
fetchResult.className = "result success";
|
|
25882
|
+
showModelIds();
|
|
25883
|
+
if (fetchedIds.length === 0) fetchResult.textContent = "No model ids found. Type the model id.";
|
|
24469
25884
|
modelId.focus();
|
|
24470
25885
|
} else {
|
|
25886
|
+
forgetModelIds();
|
|
24471
25887
|
fetchResult.textContent = outcome.message;
|
|
24472
25888
|
fetchResult.className = "result " + outcome.result;
|
|
24473
25889
|
}
|
|
@@ -24477,13 +25893,18 @@ function setUpAddModelForm() {
|
|
|
24477
25893
|
let aliasEdited = false;
|
|
24478
25894
|
alias.oninput = () => (aliasEdited = alias.value !== "");
|
|
24479
25895
|
modelId.oninput = () => {
|
|
25896
|
+
const known = knownModalities[modelId.value.trim()];
|
|
25897
|
+
if (known && known !== modality.value) {
|
|
25898
|
+
modality.value = known;
|
|
25899
|
+
showModelIds();
|
|
25900
|
+
}
|
|
24480
25901
|
if (!aliasEdited) alias.value = modelId.value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
|
|
24481
25902
|
};
|
|
24482
25903
|
|
|
24483
25904
|
const form = document.getElementById("add-model");
|
|
24484
25905
|
form.onsubmit = (event) => {
|
|
24485
25906
|
event.preventDefault();
|
|
24486
|
-
const body = { alias: alias.value, provider: provider.value, modelId: modelId.value, protocol: protocol.value };
|
|
25907
|
+
const body = { alias: alias.value, provider: provider.value, modelId: modelId.value, protocol: protocol.value, modality: modality.value };
|
|
24487
25908
|
if (notes.value.trim() !== "") body.notes = notes.value;
|
|
24488
25909
|
act(form.querySelector("button[type=submit]"), async () => {
|
|
24489
25910
|
await api("POST", "/api/models", body);
|
|
@@ -24524,61 +25945,147 @@ function setUpAddForm() {
|
|
|
24524
25945
|
};
|
|
24525
25946
|
}
|
|
24526
25947
|
|
|
24527
|
-
/** An image of a Job, or a placeholder once the file is gone. */
|
|
24528
|
-
function thumbnail(file) {
|
|
24529
|
-
const
|
|
24530
|
-
|
|
24531
|
-
|
|
25948
|
+
/** An image or video of a Job, or a placeholder once the file is gone. */
|
|
25949
|
+
function thumbnail(job, file) {
|
|
25950
|
+
const src = file.thumbnail + "?token=" + encodeURIComponent(token);
|
|
25951
|
+
const media = job.modality === "video"
|
|
25952
|
+
? el("video", { className: "thumb video", src, title: file.path, controls: true, muted: true, preload: "metadata" })
|
|
25953
|
+
: el("img", { className: "thumb", src, alt: file.path, title: file.path, loading: "lazy" });
|
|
25954
|
+
media.onerror = () => media.replaceWith(el("div", { className: "thumb placeholder", title: file.path, textContent: "File no longer on disk" }));
|
|
25955
|
+
return media;
|
|
24532
25956
|
}
|
|
24533
25957
|
|
|
24534
25958
|
function formatDuration(ms) {
|
|
24535
25959
|
return ms < 1000 ? ms + " ms" : (ms / 1000).toFixed(1) + " s";
|
|
24536
25960
|
}
|
|
24537
25961
|
|
|
24538
|
-
|
|
25962
|
+
function ratingText(rating) {
|
|
25963
|
+
return (rating.verdict === "good" ? "Good" : "Bad") + ": " + rating.reason;
|
|
25964
|
+
}
|
|
25965
|
+
|
|
25966
|
+
/** Whether this Job is its Comparison's Pick: as a whole, or with \`result\` that one result of it. */
|
|
25967
|
+
function isPicked(job, result) {
|
|
25968
|
+
const pick = job.comparison && job.comparison.pick;
|
|
25969
|
+
return !!pick && pick.jobId === job.id && (result === undefined || pick.result === result);
|
|
25970
|
+
}
|
|
25971
|
+
|
|
25972
|
+
/**
|
|
25973
|
+
* A thumbnail, captioned with its number, its own Rating and whether it is its Comparison's Pick, when the Job has
|
|
25974
|
+
* several results or this one is rated or picked.
|
|
25975
|
+
*/
|
|
25976
|
+
function resultThumbnail(job, file, index) {
|
|
25977
|
+
const media = thumbnail(job, file);
|
|
25978
|
+
const picked = job.files.length > 1 && isPicked(job, index + 1);
|
|
25979
|
+
if (job.files.length === 1 && !file.rating) return media;
|
|
25980
|
+
const caption = el("figcaption", { textContent: "Result " + (index + 1) + (picked ? " · Picked" : "") + (file.rating ? " · " + ratingText(file.rating) : "") });
|
|
25981
|
+
if (file.rating) caption.className = "rating " + file.rating.verdict;
|
|
25982
|
+
else if (picked) caption.className = "picked";
|
|
25983
|
+
return el("figure", { className: "result-thumb" }, media, caption);
|
|
25984
|
+
}
|
|
25985
|
+
|
|
25986
|
+
/**
|
|
25987
|
+
* For a completed Job of a Comparison: a Pick button for the Job or, when it has several results, the one selected.
|
|
25988
|
+
* A Pick says which of the Comparison's results the User prefers; it is not a Rating.
|
|
25989
|
+
*/
|
|
25990
|
+
function pickRow(job) {
|
|
25991
|
+
if (!job.comparison || job.outcome !== "completed") return [];
|
|
25992
|
+
const target = el("select", { className: "rate-target", title: "What you pick" },
|
|
25993
|
+
el("option", { value: "", textContent: "Whole Job" }),
|
|
25994
|
+
...job.files.map((_, index) => el("option", { value: String(index + 1), textContent: "Result " + (index + 1) })));
|
|
25995
|
+
const button = el("button", { type: "button", textContent: "Pick" });
|
|
25996
|
+
button.onclick = () => act(button, async () => {
|
|
25997
|
+
const body = { jobId: job.id };
|
|
25998
|
+
if (target.value !== "") body.result = Number(target.value);
|
|
25999
|
+
await api("POST", "/api/comparisons/" + encodeURIComponent(job.comparison.id) + "/pick", body);
|
|
26000
|
+
await loadJobs();
|
|
26001
|
+
}, "Pick saved.");
|
|
26002
|
+
const controls = job.files.length > 1 ? [target, button] : [button];
|
|
26003
|
+
return [el("div", { className: "actions" }, el("span", { className: "spacer muted", textContent: "Prefer this one?" }), ...controls)];
|
|
26004
|
+
}
|
|
26005
|
+
|
|
26006
|
+
/**
|
|
26007
|
+
* The whole Job's Rating, and a one-line reason with Good and Bad buttons to rate the Job or, when it has several
|
|
26008
|
+
* results, one of them. Only completed Jobs can be rated.
|
|
26009
|
+
*/
|
|
24539
26010
|
function ratingRow(job) {
|
|
24540
26011
|
if (job.outcome !== "completed") return [];
|
|
26012
|
+
const whole = job.files.length > 1 ? "Whole Job · " : "";
|
|
24541
26013
|
const current = job.rating
|
|
24542
|
-
? el("span", { className: "rating " + job.rating.verdict, textContent:
|
|
24543
|
-
: el("span", { className: "rating muted", textContent: "Not rated yet." });
|
|
26014
|
+
? el("span", { className: "rating " + job.rating.verdict, textContent: whole + ratingText(job.rating) })
|
|
26015
|
+
: el("span", { className: "rating muted", textContent: whole + "Not rated yet." });
|
|
24544
26016
|
const reason = el("input", { maxLength: reasonMax, placeholder: "One-line reason, e.g. the text is misspelled" });
|
|
26017
|
+
const target = el("select", { className: "rate-target", title: "What the Rating is about" },
|
|
26018
|
+
el("option", { value: "", textContent: "Whole Job" }),
|
|
26019
|
+
...job.files.map((_, index) => el("option", { value: String(index + 1), textContent: "Result " + (index + 1) })));
|
|
24545
26020
|
const rateAs = (verdict, label) => {
|
|
24546
26021
|
const button = el("button", { type: "button", textContent: label });
|
|
24547
26022
|
button.onclick = () => act(button, async () => {
|
|
24548
|
-
|
|
26023
|
+
const body = { verdict, reason: reason.value };
|
|
26024
|
+
if (target.value !== "") body.result = Number(target.value);
|
|
26025
|
+
await api("POST", "/api/jobs/" + encodeURIComponent(job.id) + "/rating", body);
|
|
24549
26026
|
await loadJobs();
|
|
24550
26027
|
}, "Rating saved.");
|
|
24551
26028
|
return button;
|
|
24552
26029
|
};
|
|
24553
|
-
|
|
26030
|
+
const controls = job.files.length > 1 ? [target, reason] : [reason];
|
|
26031
|
+
return [el("div", { className: "actions rate" }, current, el("div", { className: "inline" }, ...controls, rateAs("good", "Good"), rateAs("bad", "Bad")))];
|
|
24554
26032
|
}
|
|
24555
26033
|
|
|
24556
26034
|
function jobCard(job) {
|
|
24557
26035
|
const when = new Date(job.createdAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
24558
26036
|
const outcome = job.outcome === "completed"
|
|
24559
|
-
? job.files.length + " image(s)"
|
|
26037
|
+
? (job.modality === "video" ? "Video" : job.files.length + " image(s)")
|
|
24560
26038
|
: job.outcome === "running" ? "Running…" : "Failed";
|
|
24561
26039
|
const details = [];
|
|
24562
26040
|
if (job.error) details.push(el("p", { className: "result failure", textContent: job.error }));
|
|
24563
26041
|
if (job.shortfall) details.push(el("p", { className: "result unsupported", textContent: job.shortfall }));
|
|
26042
|
+
if (job.mismatch) details.push(el("p", { className: "result unsupported", textContent: job.mismatch }));
|
|
24564
26043
|
return el("div", { className: "card" },
|
|
24565
26044
|
el("div", {},
|
|
24566
26045
|
el("div", { className: "card-head" }, el("h3", { textContent: job.model }),
|
|
26046
|
+
...(isPicked(job) ? [el("span", { className: "chip accent", textContent: "Picked" })] : []),
|
|
24567
26047
|
el("span", { className: "chip " + (job.outcome === "completed" ? "ok" : job.outcome), textContent: outcome })),
|
|
24568
26048
|
el("p", { className: "job-meta", textContent: job.modelId + " on " + job.provider + " · " + when + " · " + formatDuration(job.durationMs) })),
|
|
24569
26049
|
el("p", { className: "prompt", textContent: job.prompt }),
|
|
24570
|
-
...(job.files.length === 0 ? [] : [el("div", { className: "thumbs" }, ...job.files.map(
|
|
26050
|
+
...(job.files.length === 0 ? [] : [el("div", { className: "thumbs" }, ...job.files.map((file, index) => resultThumbnail(job, file, index)))]),
|
|
24571
26051
|
...details,
|
|
24572
26052
|
...ratingRow(job),
|
|
26053
|
+
...pickRow(job),
|
|
24573
26054
|
);
|
|
24574
26055
|
}
|
|
24575
26056
|
|
|
26057
|
+
/** A Comparison's Jobs together, under what the User picked among them. */
|
|
26058
|
+
function comparisonCard(jobs) {
|
|
26059
|
+
const pick = jobs[0].comparison.pick;
|
|
26060
|
+
const picked = pick && jobs.find((job) => job.id === pick.jobId);
|
|
26061
|
+
const status = !pick ? "Not picked yet."
|
|
26062
|
+
: "Picked: " + (pick.result === undefined ? "" : "result " + pick.result + " of ") + (picked ? picked.model + "'s Job" : "Job " + pick.jobId) + ".";
|
|
26063
|
+
return el("div", { className: "comparison" },
|
|
26064
|
+
el("div", { className: "comparison-head" }, el("h3", { textContent: "Comparison · " + jobs.length + " Jobs" }),
|
|
26065
|
+
el("p", { textContent: status + " A Pick says which you prefer; it is not a Rating." })),
|
|
26066
|
+
...jobs.map(jobCard));
|
|
26067
|
+
}
|
|
26068
|
+
|
|
26069
|
+
/** Job cards, each Comparison's Jobs (which the listing keeps together) in one Comparison card. */
|
|
26070
|
+
function jobEntries(jobs) {
|
|
26071
|
+
const entries = [];
|
|
26072
|
+
for (let i = 0; i < jobs.length; ) {
|
|
26073
|
+
const comparison = jobs[i].comparison;
|
|
26074
|
+
if (!comparison) { entries.push(jobCard(jobs[i])); i++; continue; }
|
|
26075
|
+
let end = i;
|
|
26076
|
+
while (end < jobs.length && jobs[end].comparison && jobs[end].comparison.id === comparison.id) end++;
|
|
26077
|
+
entries.push(comparisonCard(jobs.slice(i, end)));
|
|
26078
|
+
i = end;
|
|
26079
|
+
}
|
|
26080
|
+
return entries;
|
|
26081
|
+
}
|
|
26082
|
+
|
|
24576
26083
|
async function loadJobs() {
|
|
24577
26084
|
const list = document.getElementById("job-list");
|
|
24578
26085
|
const { jobs } = await api("GET", "/api/jobs");
|
|
24579
26086
|
list.replaceChildren(...(jobs.length === 0
|
|
24580
26087
|
? [emptyState("No Jobs yet. Images your Agent generates will appear here.")]
|
|
24581
|
-
: jobs
|
|
26088
|
+
: jobEntries(jobs)));
|
|
24582
26089
|
}
|
|
24583
26090
|
|
|
24584
26091
|
(async () => {
|
|
@@ -24654,7 +26161,7 @@ async function startSettingsServer(options) {
|
|
|
24654
26161
|
const nonce = randomBytes2(16).toString("base64");
|
|
24655
26162
|
response.writeHead(200, {
|
|
24656
26163
|
"content-type": "text/html; charset=utf-8",
|
|
24657
|
-
"content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; form-action 'none'; base-uri 'none'; frame-ancestors 'none'`,
|
|
26164
|
+
"content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; media-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'`,
|
|
24658
26165
|
...commonHeaders
|
|
24659
26166
|
});
|
|
24660
26167
|
response.end(renderPage(nonce));
|
|
@@ -24747,91 +26254,298 @@ function settingsOpener(options) {
|
|
|
24747
26254
|
};
|
|
24748
26255
|
}
|
|
24749
26256
|
// package.json
|
|
24750
|
-
var version2 = "0.
|
|
26257
|
+
var version2 = "0.3.0";
|
|
24751
26258
|
|
|
24752
26259
|
// src/server.ts
|
|
24753
|
-
var instructions = `litevision generates images through Models the User has configured with their own Providers.
|
|
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\`.
|
|
24754
26261
|
|
|
24755
|
-
- Use the Model the User names (pass its Alias as \`model\`).
|
|
24756
|
-
- When the User names none, omit \`model\` to use the default image Model, unless the request needs something the default lacks (e.g. legible Chinese text, reference images, an aspect ratio). Then call \`list_models\` and choose by each Model's capabilities and Model Notes.
|
|
24757
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.
|
|
24758
|
-
- \`
|
|
24759
|
-
-
|
|
24760
|
-
-
|
|
24761
|
-
-
|
|
24762
|
-
-
|
|
24763
|
-
-
|
|
24764
|
-
-
|
|
24765
|
-
-
|
|
24766
|
-
|
|
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\`.
|
|
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.
|
|
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.
|
|
26266
|
+
- Tell the User where the files are, and pass on whatever a result says did not match the request or came back short.
|
|
26267
|
+
- \`generate_comparison\` costs as much as all its Jobs. Start a Comparison only when the User asks for one or agrees to it.
|
|
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.
|
|
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.
|
|
26270
|
+
- Before calling \`update_model_notes\`, show the User the old and new Model Notes and get their agreement; never change them without it.`;
|
|
26271
|
+
var recentRatingsShown = 5;
|
|
26272
|
+
var recentPicksShown = 5;
|
|
26273
|
+
var promptStartLength = 80;
|
|
24767
26274
|
var listedModelSchema = object({
|
|
24768
26275
|
alias: string2(),
|
|
24769
26276
|
provider: string2(),
|
|
24770
26277
|
protocol: string2(),
|
|
24771
26278
|
model_id: string2().describe("The Provider's own name for the model."),
|
|
24772
|
-
|
|
26279
|
+
modality: _enum(modalities).describe("What the Model generates: images with `generate_image`, videos with `generate_video`."),
|
|
26280
|
+
default: boolean2().describe("True for the default Model of its Modality."),
|
|
24773
26281
|
notes: string2().describe("Model Notes: what the Model is good or bad at, agreed with the User."),
|
|
24774
|
-
|
|
24775
|
-
aspect_ratios: array(object({ aspect_ratio: _enum(aspectRatios), size: string2() })),
|
|
24776
|
-
reference_images: boolean2(),
|
|
24777
|
-
max_reference_images: number2(),
|
|
24778
|
-
max_images_per_job: number2()
|
|
24779
|
-
}).nullable().describe("Null when the Model cannot be used; `problem` then says why."),
|
|
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."),
|
|
24780
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`)."),
|
|
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."),
|
|
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.'),
|
|
24781
26299
|
problem: string2().optional(),
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
26300
|
+
recent_ratings: array(object({
|
|
26301
|
+
job_id: string2(),
|
|
26302
|
+
result: number2().optional().describe("Which result of the Job, from 1; absent when the Rating is about the whole Job."),
|
|
26303
|
+
verdict: _enum(verdicts),
|
|
26304
|
+
reason: string2().describe("The User's reason, in their words."),
|
|
26305
|
+
rated_at: string2(),
|
|
26306
|
+
prompt: string2().describe("The start of the Job's prompt.")
|
|
26307
|
+
})).optional().describe(`Only with \`model\`: the Model's ${recentRatingsShown} most recent Ratings, newest first. When they say something the Model Notes do not, propose new Model Notes to the User.`),
|
|
26308
|
+
recent_picks: array(object({
|
|
26309
|
+
comparison_id: string2(),
|
|
26310
|
+
job_id: string2().describe("This Model's Job in the Comparison."),
|
|
26311
|
+
picked: boolean2().describe("Whether the User picked this Model's Job."),
|
|
26312
|
+
result: number2().optional().describe("When picked: which result of the Job the User picked, from 1; absent when they picked the whole Job."),
|
|
26313
|
+
picked_model: string2().optional().describe("Alias of the Model whose Job the User picked."),
|
|
26314
|
+
picked_at: string2(),
|
|
26315
|
+
prompt: string2().describe("The start of this Model's Job's prompt.")
|
|
26316
|
+
})).optional().describe(`Only with \`model\`: the current Picks of up to ${recentPicksShown} Comparisons this Model took part in, most recently picked first, one entry per Job of this Model in them. A Pick is a relative preference among the Comparison's results, not a Rating.`),
|
|
26317
|
+
parameter_schema: unknown().optional().describe("Only with `model`: the Provider's own description of the Model's parameters, when it publishes one.")
|
|
26318
|
+
});
|
|
26319
|
+
async function listedModel(deps, config, model, isDefault, detailed, ratedJobs) {
|
|
24786
26320
|
const base = {
|
|
24787
26321
|
alias: model.alias,
|
|
24788
26322
|
provider: model.provider,
|
|
24789
26323
|
protocol: model.protocol,
|
|
24790
26324
|
model_id: model.modelId,
|
|
26325
|
+
modality: model.modality,
|
|
24791
26326
|
default: isDefault,
|
|
24792
|
-
notes: model.notes ?? ""
|
|
26327
|
+
notes: model.notes ?? "",
|
|
26328
|
+
standing_options: model.standingOptions ?? {}
|
|
24793
26329
|
};
|
|
24794
|
-
const heading = `${model.alias}${isDefault ?
|
|
24795
|
-
const notes = model.notes ? ` Notes: ${model.notes}` : ""
|
|
24796
|
-
let
|
|
26330
|
+
const heading = `${model.alias}${isDefault ? ` (default ${model.modality})` : ""}: ${model.modality} Model ${model.modelId} on ${model.provider} via ${model.protocol}.`;
|
|
26331
|
+
const notes = `${model.notes ? ` Notes: ${model.notes}` : ""}${model.standingOptions ? ` Standing Options: ${JSON.stringify(model.standingOptions)}.` : ""}`;
|
|
26332
|
+
let described;
|
|
24797
26333
|
try {
|
|
24798
26334
|
const provider = config.providers.find((candidate) => candidate.id === model.provider);
|
|
24799
26335
|
if (provider === undefined)
|
|
24800
26336
|
throw needsSettings(`Model "${model.alias}" refers to Provider "${model.provider}", which is not configured.`);
|
|
24801
|
-
|
|
26337
|
+
if (model.modality === "video") {
|
|
26338
|
+
const info = videoModelInfo({ model, provider });
|
|
26339
|
+
described = {
|
|
26340
|
+
docsUrl: info.docsUrl,
|
|
26341
|
+
...info.guide === undefined ? {} : { guide: info.guide }
|
|
26342
|
+
};
|
|
26343
|
+
} else {
|
|
26344
|
+
const info = await modelInfo(deps, { model, provider });
|
|
26345
|
+
described = {
|
|
26346
|
+
docsUrl: info.docsUrl,
|
|
26347
|
+
...info.parameterSchema === undefined ? {} : { parameterSchema: info.parameterSchema },
|
|
26348
|
+
...info.guide === undefined ? {} : { guide: info.guide }
|
|
26349
|
+
};
|
|
26350
|
+
}
|
|
24802
26351
|
} catch (error) {
|
|
24803
26352
|
if (!(error instanceof ConfigError))
|
|
24804
26353
|
throw error;
|
|
24805
26354
|
return {
|
|
24806
|
-
output: { ...base,
|
|
26355
|
+
output: { ...base, docs_url: null, problem: error.message },
|
|
24807
26356
|
text: `${heading} Cannot be used: ${error.message}${notes}`
|
|
24808
26357
|
};
|
|
24809
26358
|
}
|
|
24810
|
-
const
|
|
26359
|
+
const record2 = optionsOf(ratedJobs, model);
|
|
24811
26360
|
const output = {
|
|
24812
26361
|
...base,
|
|
24813
|
-
|
|
24814
|
-
|
|
24815
|
-
|
|
24816
|
-
|
|
24817
|
-
max_images_per_job: capabilities.maxImages
|
|
24818
|
-
},
|
|
24819
|
-
docs_url: info.docsUrl
|
|
26362
|
+
docs_url: described.docsUrl,
|
|
26363
|
+
parameters: parametersSummary(described, model.provider),
|
|
26364
|
+
known_good_options: record2.knownGood,
|
|
26365
|
+
suspect_options: suspectOutput(record2)
|
|
24820
26366
|
};
|
|
24821
|
-
let text = `${heading}
|
|
26367
|
+
let text = `${heading}${notes} provider_options docs: ${described.docsUrl}`;
|
|
26368
|
+
if (!detailed)
|
|
26369
|
+
text += ` ${output.parameters}`;
|
|
26370
|
+
const goodKeys = Object.keys(record2.knownGood);
|
|
26371
|
+
const suspectKeys = Object.keys(record2.suspect);
|
|
24822
26372
|
if (detailed) {
|
|
24823
|
-
const knownGood = knownGoodOptions(await readJobs(deps.dirs), { provider: model.provider, protocol: model.protocol, modelId: model.modelId });
|
|
24824
|
-
output.known_good_options = knownGood;
|
|
24825
26373
|
text += `
|
|
24826
|
-
Known-good
|
|
24827
|
-
|
|
24828
|
-
output.parameter_schema = info.parameterSchema;
|
|
26374
|
+
Known-good Options: ${goodKeys.length === 0 ? "none recorded yet" : JSON.stringify(record2.knownGood)}`;
|
|
26375
|
+
for (const [key, suspect] of Object.entries(record2.suspect)) {
|
|
24829
26376
|
text += `
|
|
24830
|
-
|
|
24831
|
-
}
|
|
26377
|
+
Suspect: ${key}=${JSON.stringify(suspect.value)}, used by Job ${suspect.jobId}, ${suspectWhy(suspect)}`;
|
|
26378
|
+
}
|
|
26379
|
+
const ratings = recentRatings(ratedJobs, model, recentRatingsShown);
|
|
26380
|
+
output.recent_ratings = ratings.map(recentRatingOutput);
|
|
26381
|
+
text += ratings.length === 0 ? `
|
|
26382
|
+
Recent Ratings: none recorded yet.` : `
|
|
26383
|
+
Recent Ratings, newest first:`;
|
|
26384
|
+
for (const rated of ratings)
|
|
26385
|
+
text += `
|
|
26386
|
+
- ${describeRecentRating(rated)}`;
|
|
26387
|
+
const picks = recentPicks(ratedJobs, model, recentPicksShown);
|
|
26388
|
+
output.recent_picks = picks.map(recentPickOutput);
|
|
26389
|
+
text += picks.length === 0 ? `
|
|
26390
|
+
Recent Picks: none recorded yet.` : `
|
|
26391
|
+
Recent Picks in Comparisons, newest first:`;
|
|
26392
|
+
for (const picked of picks)
|
|
26393
|
+
text += `
|
|
26394
|
+
- ${describeRecentPick(picked)}`;
|
|
26395
|
+
output.parameter_guide = described.parameterSchema === undefined && described.guide !== undefined ? guideOutput(described.guide) : null;
|
|
26396
|
+
if (described.parameterSchema !== undefined)
|
|
26397
|
+
output.parameter_schema = described.parameterSchema;
|
|
26398
|
+
text += `
|
|
26399
|
+
${parameterHelp(described, model.provider)}`;
|
|
26400
|
+
} else {
|
|
26401
|
+
if (goodKeys.length > 0)
|
|
26402
|
+
text += ` Known-good Options: ${goodKeys.join(", ")}.`;
|
|
26403
|
+
if (suspectKeys.length > 0)
|
|
26404
|
+
text += ` Suspect options: ${suspectKeys.join(", ")}.`;
|
|
24832
26405
|
}
|
|
24833
26406
|
return { output, text };
|
|
24834
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
|
+
}
|
|
26427
|
+
function ratingOutput(rating) {
|
|
26428
|
+
return {
|
|
26429
|
+
job_id: rating.jobId,
|
|
26430
|
+
...rating.result === undefined ? {} : { result: rating.result },
|
|
26431
|
+
verdict: rating.verdict,
|
|
26432
|
+
reason: rating.reason,
|
|
26433
|
+
rated_at: rating.ratedAt
|
|
26434
|
+
};
|
|
26435
|
+
}
|
|
26436
|
+
function ratingSubject(rating) {
|
|
26437
|
+
return rating.result === undefined ? `Job ${rating.jobId}` : `result ${rating.result} of Job ${rating.jobId}`;
|
|
26438
|
+
}
|
|
26439
|
+
function recentRatingOutput({ rating, job }) {
|
|
26440
|
+
return { ...ratingOutput(rating), prompt: promptStart(job.prompt) };
|
|
26441
|
+
}
|
|
26442
|
+
function describeRecentRating({ rating, job }) {
|
|
26443
|
+
return `${rating.verdict} on ${ratingSubject(rating)} (${JSON.stringify(promptStart(job.prompt))}): ${rating.reason}`;
|
|
26444
|
+
}
|
|
26445
|
+
function pickOutput(pick) {
|
|
26446
|
+
return {
|
|
26447
|
+
comparison_id: pick.comparison,
|
|
26448
|
+
job_id: pick.jobId,
|
|
26449
|
+
...pick.result === undefined ? {} : { result: pick.result },
|
|
26450
|
+
picked_at: pick.pickedAt
|
|
26451
|
+
};
|
|
26452
|
+
}
|
|
26453
|
+
function recentPickOutput({ pick, job, picked }) {
|
|
26454
|
+
const isPicked = pick.jobId === job.id;
|
|
26455
|
+
return {
|
|
26456
|
+
comparison_id: pick.comparison,
|
|
26457
|
+
job_id: job.id,
|
|
26458
|
+
picked: isPicked,
|
|
26459
|
+
...isPicked && pick.result !== undefined ? { result: pick.result } : {},
|
|
26460
|
+
...picked === undefined ? {} : { picked_model: picked.model },
|
|
26461
|
+
picked_at: pick.pickedAt,
|
|
26462
|
+
prompt: promptStart(job.prompt)
|
|
26463
|
+
};
|
|
26464
|
+
}
|
|
26465
|
+
function describeRecentPick({ pick, job, picked }) {
|
|
26466
|
+
const subject = `Job ${job.id} in Comparison ${pick.comparison} (${JSON.stringify(promptStart(job.prompt))})`;
|
|
26467
|
+
if (pick.jobId === job.id)
|
|
26468
|
+
return `${subject}: picked${pick.result === undefined ? "" : `, result ${pick.result}`}`;
|
|
26469
|
+
return `${subject}: not picked; the User picked ${picked === undefined ? `Job ${pick.jobId}` : `a Job on ${picked.model}`}`;
|
|
26470
|
+
}
|
|
26471
|
+
function promptStart(prompt) {
|
|
26472
|
+
const line = prompt.replace(/\s+/g, " ").trim();
|
|
26473
|
+
return line.length <= promptStartLength ? line : `${line.slice(0, promptStartLength).trimEnd()}…`;
|
|
26474
|
+
}
|
|
26475
|
+
function suspectOutput(record2) {
|
|
26476
|
+
return Object.fromEntries(Object.entries(record2.suspect).map(([key, suspect]) => [
|
|
26477
|
+
key,
|
|
26478
|
+
{ value: suspect.value, reason: suspect.reason, job_id: suspect.jobId, cause: suspect.cause }
|
|
26479
|
+
]));
|
|
26480
|
+
}
|
|
26481
|
+
async function modelOptions(deps, model) {
|
|
26482
|
+
return optionsOf(await readRatedJobs(deps.dirs), model);
|
|
26483
|
+
}
|
|
26484
|
+
function optionsOf(ratedJobs, model) {
|
|
26485
|
+
return optionsRecord(ratedJobs, { provider: model.provider, protocol: model.protocol, modelId: model.modelId });
|
|
26486
|
+
}
|
|
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(() => {
|
|
26496
|
+
return;
|
|
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) };
|
|
26503
|
+
}
|
|
26504
|
+
function suspectWhy(suspect) {
|
|
26505
|
+
return suspect.cause === "rating" ? `which the User rated bad: "${suspect.reason}"` : `whose result did not match the request: ${suspect.reason}`;
|
|
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
|
+
}
|
|
26512
|
+
function optionsHint(docsUrl, record2) {
|
|
26513
|
+
const good = Object.keys(record2.knownGood).length === 0 ? "none recorded yet" : JSON.stringify(record2.knownGood);
|
|
26514
|
+
const suspect = Object.entries(record2.suspect).map(([key, entry]) => ` Suspect: ${key}=${JSON.stringify(entry.value)}, used by a Job ${suspectWhy(entry)}.`).join("");
|
|
26515
|
+
return ` Docs for this Model's provider_options: ${docsUrl}. Known-good Options: ${good}.${suspect}`;
|
|
26516
|
+
}
|
|
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
|
+
}
|
|
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.");
|
|
26529
|
+
function observedText(observed) {
|
|
26530
|
+
const known = [...new Set(observed.flatMap((facts) => facts === null ? [] : [describeFacts(facts)]))];
|
|
26531
|
+
return known.length === 0 ? "" : ` Got ${known.join(", ")}.`;
|
|
26532
|
+
}
|
|
26533
|
+
function describeFacts(facts) {
|
|
26534
|
+
return `${facts.width}x${facts.height}${facts.seconds === undefined ? "" : `, ${facts.seconds} s`}`;
|
|
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
|
+
}
|
|
26544
|
+
function mismatchText(mismatch, docsUrl) {
|
|
26545
|
+
if (mismatch === undefined)
|
|
26546
|
+
return "";
|
|
26547
|
+
return ` The result does not match the request: ${mismatch}. A provider_option may not have taken effect, so these options are not recorded as Known-good; check ${docsUrl} before trying again.`;
|
|
26548
|
+
}
|
|
24835
26549
|
var jobStatusSchema = _enum(["completed", "running"]).describe('"running" when the Job outlasted the wait limit and continues in the background; call `get_job` with its `job_id`.');
|
|
24836
26550
|
function jobOutput(job) {
|
|
24837
26551
|
return {
|
|
@@ -24839,7 +26553,10 @@ function jobOutput(job) {
|
|
|
24839
26553
|
status: job.status,
|
|
24840
26554
|
model: job.model,
|
|
24841
26555
|
files: job.status === "completed" ? job.files : [],
|
|
24842
|
-
|
|
26556
|
+
observed: job.status === "completed" ? job.observed : [],
|
|
26557
|
+
...job.status === "completed" && job.shortfall !== undefined ? { shortfall: job.shortfall } : {},
|
|
26558
|
+
...job.status === "completed" && job.mismatch !== undefined ? { mismatch: job.mismatch } : {},
|
|
26559
|
+
...job.status === "completed" && job.requested !== undefined ? requestedOutput(job.requested) : {}
|
|
24843
26560
|
};
|
|
24844
26561
|
}
|
|
24845
26562
|
function jobText(job) {
|
|
@@ -24848,111 +26565,265 @@ function jobText(job) {
|
|
|
24848
26565
|
function toolError(text) {
|
|
24849
26566
|
return { content: [{ type: "text", text }], isError: true };
|
|
24850
26567
|
}
|
|
26568
|
+
async function failure2(error, hint) {
|
|
26569
|
+
const reason = await failureReason(error, hint);
|
|
26570
|
+
return toolError(error instanceof JobFailedError ? `Job ${error.jobId} failed: ${reason}` : reason);
|
|
26571
|
+
}
|
|
26572
|
+
async function failureReason(error, hint) {
|
|
26573
|
+
if (error instanceof ConfigError)
|
|
26574
|
+
return error.message;
|
|
26575
|
+
if (error instanceof JobFailedError) {
|
|
26576
|
+
const helpful = hint !== undefined && (error.kind === "unsupported" || error.kind === "rejected_option" || error.kind === "other");
|
|
26577
|
+
return `${error.message}${helpful ? await hint(error.kind) : ""}`;
|
|
26578
|
+
}
|
|
26579
|
+
throw error;
|
|
26580
|
+
}
|
|
26581
|
+
var imageJobShape = {
|
|
26582
|
+
job_id: string2(),
|
|
26583
|
+
status: jobStatusSchema,
|
|
26584
|
+
model: string2().describe("Alias of the Model that ran the Job."),
|
|
26585
|
+
files: array(string2()).describe("Paths of the saved images; empty while the Job is running."),
|
|
26586
|
+
requested_shape: requestedShapeSchema,
|
|
26587
|
+
requested_resolution: requestedResolutionSchema,
|
|
26588
|
+
shortfall: string2().optional().describe("Present when fewer images than `n` were saved: how many, and why."),
|
|
26589
|
+
observed: observedSchema,
|
|
26590
|
+
mismatch: mismatchSchema
|
|
26591
|
+
};
|
|
26592
|
+
var videoJobShape = {
|
|
26593
|
+
job_id: string2(),
|
|
26594
|
+
status: jobStatusSchema,
|
|
26595
|
+
model: string2().describe("Alias of the Model that ran the Job."),
|
|
26596
|
+
files: array(string2()).describe("Path of the saved video; empty while the Job is running."),
|
|
26597
|
+
requested_shape: requestedShapeSchema,
|
|
26598
|
+
requested_duration: requestedDurationSchema,
|
|
26599
|
+
requested_resolution: requestedResolutionSchema,
|
|
26600
|
+
observed: observedSchema,
|
|
26601
|
+
mismatch: mismatchSchema
|
|
26602
|
+
};
|
|
26603
|
+
var failedJobSchema = object({
|
|
26604
|
+
job_id: string2().nullable().describe("Null when the Job could not start."),
|
|
26605
|
+
status: literal("failed"),
|
|
26606
|
+
model: string2().describe("Alias of the Model the Job ran on."),
|
|
26607
|
+
files: array(string2()).describe("Always empty."),
|
|
26608
|
+
error: string2().describe("Why the Job failed.")
|
|
26609
|
+
});
|
|
26610
|
+
var maxComparisonVariants = 4;
|
|
26611
|
+
function comparisonModality(config, variants, wanted) {
|
|
26612
|
+
const named = variants.flatMap((variant) => config.models.filter((model) => model.alias === variant.model));
|
|
26613
|
+
const found = new Set([...wanted === undefined ? [] : [wanted], ...named.map((model) => model.modality)]);
|
|
26614
|
+
if (found.size > 1) {
|
|
26615
|
+
const makes = [...new Map(named.map((model) => [model.alias, `${model.alias} makes ${model.modality}s`])).values()].join(", ");
|
|
26616
|
+
const asked = wanted === undefined ? "" : ` and \`modality\` is ${wanted}`;
|
|
26617
|
+
throw new ConfigError(`A Comparison's Models must share one Modality, but ${makes}${asked}. Compare image Models or video Models, not both.`);
|
|
26618
|
+
}
|
|
26619
|
+
return [...found][0] ?? "image";
|
|
26620
|
+
}
|
|
24851
26621
|
function createServer2(deps) {
|
|
24852
26622
|
const server = new McpServer({ name: "litevision", version: version2 }, { instructions });
|
|
24853
26623
|
const now = deps.now ?? (() => new Date);
|
|
24854
26624
|
const cwd = deps.cwd ?? process.cwd();
|
|
24855
26625
|
const infoDeps = { dirs: deps.dirs, http: deps.http, now };
|
|
24856
|
-
const jobs = new JobRunner({ dirs: deps.dirs, http: deps.http, now, waitLimitMs: deps.waitLimitMs, pollIntervalMs: deps.pollIntervalMs });
|
|
26626
|
+
const jobs = new JobRunner({ dirs: deps.dirs, http: deps.http, now, waitLimitMs: deps.waitLimitMs, pollIntervalMs: deps.pollIntervalMs, retryDelayMs: deps.retryDelayMs });
|
|
26627
|
+
const prepareImage = async (resolved, args) => {
|
|
26628
|
+
const { model } = resolved;
|
|
26629
|
+
const adapter = adapterFor(model);
|
|
26630
|
+
const credential = await requireCredential(deps.dirs, resolved.provider);
|
|
26631
|
+
const info = await modelInfo(infoDeps, resolved);
|
|
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);
|
|
26638
|
+
const outputDir = args.output_path === undefined ? undefined : resolve2(cwd, args.output_path);
|
|
26639
|
+
return {
|
|
26640
|
+
model,
|
|
26641
|
+
start: (comparison) => jobs.start({ ...resolved, adapter, credential, normalised, outputDir, ...comparison === undefined ? {} : { comparison } }),
|
|
26642
|
+
hint: async (kind) => failureHint(info, model.provider, await modelOptions(deps, model), kind),
|
|
26643
|
+
report: (job) => {
|
|
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}`}
|
|
26647
|
+
${job.files.join(`
|
|
26648
|
+
`)}`;
|
|
26649
|
+
return { output, text };
|
|
26650
|
+
}
|
|
26651
|
+
};
|
|
26652
|
+
};
|
|
26653
|
+
const prepareVideo = async (resolved, args) => {
|
|
26654
|
+
const { model } = resolved;
|
|
26655
|
+
const adapter = adapterFor(model);
|
|
26656
|
+
const info = videoModelInfo(resolved);
|
|
26657
|
+
const credential = await requireCredential(deps.dirs, resolved.provider);
|
|
26658
|
+
const normalised = await normaliseVideoRequest({
|
|
26659
|
+
prompt: args.prompt,
|
|
26660
|
+
...args.first_frame === undefined ? {} : { firstFrame: args.first_frame },
|
|
26661
|
+
...args.last_frame === undefined ? {} : { lastFrame: args.last_frame },
|
|
26662
|
+
references: args.references,
|
|
26663
|
+
providerOptions: mergeOptions(model.standingOptions ?? {}, args.provider_options)
|
|
26664
|
+
}, model, adapter, cwd);
|
|
26665
|
+
const outputDir = args.output_path === undefined ? undefined : resolve2(cwd, args.output_path);
|
|
26666
|
+
return {
|
|
26667
|
+
model,
|
|
26668
|
+
start: (comparison) => jobs.startVideo({ ...resolved, adapter, credential, normalised, outputDir, ...comparison === undefined ? {} : { comparison } }),
|
|
26669
|
+
hint: async (kind) => failureHint(info, model.provider, await modelOptions(deps, model), kind),
|
|
26670
|
+
report: (job) => {
|
|
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)}
|
|
26674
|
+
${job.files.join(`
|
|
26675
|
+
`)}`;
|
|
26676
|
+
return { output, text };
|
|
26677
|
+
}
|
|
26678
|
+
};
|
|
26679
|
+
};
|
|
26680
|
+
const runSingle = async (prepare) => {
|
|
26681
|
+
let prepared;
|
|
26682
|
+
try {
|
|
26683
|
+
prepared = await prepare();
|
|
26684
|
+
const { output, text } = prepared.report(await prepared.start());
|
|
26685
|
+
return { content: [{ type: "text", text }], structuredContent: output };
|
|
26686
|
+
} catch (error) {
|
|
26687
|
+
return failure2(error, prepared?.hint);
|
|
26688
|
+
}
|
|
26689
|
+
};
|
|
24857
26690
|
server.registerTool("generate_image", {
|
|
24858
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.",
|
|
24859
|
-
inputSchema: {
|
|
26692
|
+
inputSchema: strictObject({
|
|
24860
26693
|
prompt: string2().min(1).describe("Sent to the Model verbatim."),
|
|
24861
26694
|
model: string2().optional().describe("Alias of the Model to use. Omit to use the default image Model."),
|
|
24862
|
-
n: number2().int().positive().default(1).describe("How many images
|
|
24863
|
-
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."),
|
|
24864
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."),
|
|
24865
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."),
|
|
24866
|
-
provider_options: record(string2(), unknown()).default({}).describe('
|
|
24867
|
-
},
|
|
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
|
+
}),
|
|
26700
|
+
outputSchema: imageJobShape
|
|
26701
|
+
}, async ({ model: alias, ...args }) => runSingle(async () => prepareImage(resolveModel(await loadConfig(deps.dirs), alias), args)));
|
|
26702
|
+
server.registerTool("generate_video", {
|
|
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.",
|
|
26704
|
+
inputSchema: strictObject({
|
|
26705
|
+
prompt: string2().min(1).describe("Sent to the Model verbatim. Describe motion and camera movement, not only the scene."),
|
|
26706
|
+
model: string2().optional().describe("Alias of the video Model to use. Omit to use the default video Model."),
|
|
26707
|
+
first_frame: string2().min(1).optional().describe("Local path of an image the video starts from."),
|
|
26708
|
+
last_frame: string2().min(1).optional().describe("Local path of an image the video ends on."),
|
|
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."),
|
|
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."),
|
|
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
|
+
}),
|
|
26713
|
+
outputSchema: videoJobShape
|
|
26714
|
+
}, async ({ model: alias, ...args }) => runSingle(async () => prepareVideo(resolveModel(await loadConfig(deps.dirs), alias, "video"), args)));
|
|
26715
|
+
server.registerTool("generate_comparison", {
|
|
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.`,
|
|
26717
|
+
inputSchema: strictObject({
|
|
26718
|
+
variants: array(strictObject({
|
|
26719
|
+
prompt: string2().min(1).describe("Sent to this variant's Model verbatim."),
|
|
26720
|
+
model: string2().optional().describe("Alias of this variant's Model. Omit to use the default Model of the Comparison's Modality."),
|
|
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.")
|
|
26723
|
+
})).min(2).max(maxComparisonVariants).describe(`The Jobs to start, 2 to ${maxComparisonVariants}, one per variant.`),
|
|
26724
|
+
modality: _enum(modalities).optional().describe("image or video. Omit to take it from the variants' Models; when no variant names one, image."),
|
|
26725
|
+
references: array(string2().min(1)).default([]).describe("Local paths of reference images every Job gets. Relative paths resolve against litevision's working directory."),
|
|
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
|
+
}),
|
|
24868
26728
|
outputSchema: {
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
aspect_ratio_adjusted: boolean2().describe("True when the Model does not support the requested aspect ratio and the closest one was used."),
|
|
24877
|
-
shortfall: string2().optional().describe("Present when fewer images than `n` were saved: how many, and why.")
|
|
24878
|
-
}
|
|
24879
|
-
}, async ({ prompt, model: alias, n, aspect_ratio, references, output_path, provider_options }) => {
|
|
26729
|
+
comparison_id: string2(),
|
|
26730
|
+
modality: _enum(modalities),
|
|
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`.')
|
|
26732
|
+
}
|
|
26733
|
+
}, async ({ variants, modality: wanted, references, output_path }) => {
|
|
26734
|
+
let prepared;
|
|
26735
|
+
let modality;
|
|
24880
26736
|
try {
|
|
24881
|
-
const
|
|
24882
|
-
|
|
24883
|
-
|
|
24884
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
24889
|
-
|
|
24890
|
-
|
|
24891
|
-
aspect_ratio: size.aspectRatio,
|
|
24892
|
-
size: formatSize(size),
|
|
24893
|
-
requested_aspect_ratio: normalised.requestedAspectRatio,
|
|
24894
|
-
aspect_ratio_adjusted: normalised.aspectRatioAdjusted
|
|
24895
|
-
};
|
|
24896
|
-
const adjustment = normalised.aspectRatioAdjusted ? ` ${model.alias} does not support ${normalised.requestedAspectRatio}, so the closest supported aspect ratio was used.` : "";
|
|
24897
|
-
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}${job.shortfall === undefined ? "" : ` ${job.shortfall}`}
|
|
24898
|
-
${job.files.join(`
|
|
24899
|
-
`)}`;
|
|
24900
|
-
return { content: [{ type: "text", text }], structuredContent: output };
|
|
26737
|
+
const config = await loadConfig(deps.dirs);
|
|
26738
|
+
modality = comparisonModality(config, variants, wanted);
|
|
26739
|
+
if (modality === "video" && variants.some((variant) => (variant.n ?? 1) > 1)) {
|
|
26740
|
+
throw new ConfigError("A video Job makes one video, so leave `n` out of a video Comparison's variants.");
|
|
26741
|
+
}
|
|
26742
|
+
prepared = await Promise.all(variants.map((variant) => {
|
|
26743
|
+
const resolved = resolveModel(config, variant.model, modality);
|
|
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 });
|
|
26746
|
+
}));
|
|
24901
26747
|
} catch (error) {
|
|
24902
|
-
|
|
24903
|
-
|
|
24904
|
-
|
|
24905
|
-
|
|
24906
|
-
|
|
24907
|
-
|
|
26748
|
+
return failure2(error, undefined);
|
|
26749
|
+
}
|
|
26750
|
+
const comparisonId = randomUUID3();
|
|
26751
|
+
const settled = await Promise.allSettled(prepared.map((job) => job.start(comparisonId)));
|
|
26752
|
+
const results = await Promise.all(settled.map(async (outcome, index) => {
|
|
26753
|
+
const job = prepared[index];
|
|
26754
|
+
if (outcome.status === "fulfilled")
|
|
26755
|
+
return job.report(outcome.value);
|
|
26756
|
+
const reason = await failureReason(outcome.reason, job.hint).catch(() => `litevision could not run this Job: ${outcome.reason.message}`);
|
|
26757
|
+
const jobId = outcome.reason instanceof JobFailedError ? outcome.reason.jobId : null;
|
|
26758
|
+
return {
|
|
26759
|
+
output: { job_id: jobId, status: "failed", model: job.model.alias, files: [], error: reason },
|
|
26760
|
+
text: `Job${jobId === null ? "" : ` ${jobId}`} on ${job.model.alias} failed: ${reason}`
|
|
26761
|
+
};
|
|
26762
|
+
}));
|
|
26763
|
+
const text = [
|
|
26764
|
+
`Comparison ${comparisonId}: ${results.length} ${modality} Jobs. Show the User every result so they can choose; when they do, call \`pick_result\`.`,
|
|
26765
|
+
...results.map((result, index) => `Variant ${index + 1}: ${result.text}`)
|
|
26766
|
+
].join(`
|
|
26767
|
+
|
|
26768
|
+
`);
|
|
26769
|
+
return {
|
|
26770
|
+
content: [{ type: "text", text }],
|
|
26771
|
+
structuredContent: { comparison_id: comparisonId, modality, jobs: results.map((result) => result.output) }
|
|
26772
|
+
};
|
|
24908
26773
|
});
|
|
24909
26774
|
server.registerTool("get_job", {
|
|
24910
|
-
description: "Get a Job's status, waiting up to two minutes while it runs; once it has completed, the paths of its saved images. Use it when `generate_image` returned a Job that is still running.",
|
|
26775
|
+
description: "Get a Job's status, waiting up to two minutes while it runs; once it has completed, the paths of its saved images or video. Use it when `generate_image`, `generate_video` or `generate_comparison` returned a Job that is still running.",
|
|
24911
26776
|
inputSchema: {
|
|
24912
|
-
job_id: string2().min(1).describe("The `job_id` that `generate_image` returned.")
|
|
26777
|
+
job_id: string2().min(1).describe("The `job_id` that `generate_image`, `generate_video` or `generate_comparison` returned.")
|
|
24913
26778
|
},
|
|
24914
26779
|
outputSchema: {
|
|
24915
26780
|
job_id: string2(),
|
|
24916
26781
|
status: jobStatusSchema,
|
|
24917
26782
|
model: string2().describe("Alias of the Model that ran the Job."),
|
|
24918
|
-
files: array(string2()).describe("Paths of the saved images; empty while the Job is running."),
|
|
24919
|
-
shortfall: string2().optional().describe("Present when fewer images than asked for were saved: how many, and why.")
|
|
26783
|
+
files: array(string2()).describe("Paths of the saved images or video; empty while the Job is running."),
|
|
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(),
|
|
26788
|
+
observed: observedSchema,
|
|
26789
|
+
mismatch: mismatchSchema
|
|
24920
26790
|
}
|
|
24921
26791
|
}, async ({ job_id }) => {
|
|
24922
26792
|
try {
|
|
24923
26793
|
const job = await jobs.get(job_id);
|
|
24924
|
-
const
|
|
26794
|
+
const docsUrl = job.status === "completed" && job.mismatch !== undefined ? (await jobDocs(infoDeps, job_id))?.known.docsUrl ?? "the Model's docs link in `list_models`" : "";
|
|
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}`}
|
|
24925
26796
|
${job.files.join(`
|
|
24926
26797
|
`)}`;
|
|
24927
26798
|
return { content: [{ type: "text", text }], structuredContent: jobOutput(job) };
|
|
24928
26799
|
} catch (error) {
|
|
24929
|
-
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
throw error;
|
|
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);
|
|
26803
|
+
});
|
|
24934
26804
|
}
|
|
24935
26805
|
});
|
|
24936
26806
|
server.registerTool("rate_job", {
|
|
24937
|
-
description: "Record the User's Rating of a completed Job: good or bad, plus a one-line reason. Call it only when the User has actually said what they think of the result; never invent a verdict. A later Rating of the same Job replaces the earlier one.",
|
|
26807
|
+
description: "Record the User's Rating of a completed Job, or of one result of it: good or bad, plus a one-line reason. Call it only when the User has actually said what they think of the result; never invent a verdict. A later Rating of the same Job and result replaces the earlier one; a Rating of the whole Job and one of a single result stand side by side.",
|
|
24938
26808
|
inputSchema: {
|
|
24939
|
-
job_id: string2().min(1).describe("The `job_id` that `generate_image` or `get_job` returned."),
|
|
26809
|
+
job_id: string2().min(1).describe("The `job_id` that `generate_image`, `generate_video` or `get_job` returned."),
|
|
26810
|
+
result: number2().int().positive().optional().describe("Which result the verdict is about, from 1 in the order of the Job's files. Omit when it is about the whole Job."),
|
|
24940
26811
|
verdict: _enum(verdicts).describe("The User's verdict."),
|
|
24941
26812
|
reason: string2().max(ratingReasonMaxLength).describe(`Why, in one line and in the User's words, e.g. "the text is misspelled". At most ${ratingReasonMaxLength} characters.`)
|
|
24942
26813
|
},
|
|
24943
26814
|
outputSchema: {
|
|
24944
26815
|
job_id: string2(),
|
|
26816
|
+
result: number2().optional().describe("The result rated; absent for a Rating of the whole Job."),
|
|
24945
26817
|
verdict: _enum(verdicts),
|
|
24946
26818
|
reason: string2().describe("The reason as recorded, on one line."),
|
|
24947
26819
|
rated_at: string2().describe("When the Rating was recorded, ISO 8601.")
|
|
24948
26820
|
}
|
|
24949
|
-
}, async ({ job_id, verdict, reason }) => {
|
|
26821
|
+
}, async ({ job_id, result, verdict, reason }) => {
|
|
24950
26822
|
try {
|
|
24951
|
-
const rating = await rateJob(deps.dirs, { jobId: job_id, verdict, reason }, now());
|
|
24952
|
-
const output = { job_id: rating.jobId, verdict: rating.verdict, reason: rating.reason, rated_at: rating.ratedAt };
|
|
26823
|
+
const rating = await rateJob(deps.dirs, { jobId: job_id, result, verdict, reason }, now());
|
|
24953
26824
|
return {
|
|
24954
|
-
content: [{ type: "text", text: `Recorded a ${rating.verdict} Rating on
|
|
24955
|
-
structuredContent:
|
|
26825
|
+
content: [{ type: "text", text: `Recorded a ${rating.verdict} Rating on ${ratingSubject(rating)}: ${rating.reason}` }],
|
|
26826
|
+
structuredContent: ratingOutput(rating)
|
|
24956
26827
|
};
|
|
24957
26828
|
} catch (error) {
|
|
24958
26829
|
if (error instanceof RatingError)
|
|
@@ -24960,21 +26831,50 @@ ${job.files.join(`
|
|
|
24960
26831
|
throw error;
|
|
24961
26832
|
}
|
|
24962
26833
|
});
|
|
26834
|
+
server.registerTool("pick_result", {
|
|
26835
|
+
description: "Record the User's Pick within a Comparison: the Job, or the one result of it, the User chose among the Comparison's results. Call it only when the User actually chooses; never pick for them. A later Pick for the same Comparison replaces the earlier one. A Pick is a relative preference, not a Rating: it does not mark the result good or the others bad.",
|
|
26836
|
+
inputSchema: {
|
|
26837
|
+
comparison_id: string2().min(1).describe("The `comparison_id` that `generate_comparison` returned."),
|
|
26838
|
+
job_id: string2().min(1).describe("The `job_id` of the chosen Job, one of the Comparison's Jobs."),
|
|
26839
|
+
result: number2().int().positive().optional().describe("Which result of that Job the User chose, from 1 in the order of its files. Omit when the Job made one result or the User chose the Job as a whole.")
|
|
26840
|
+
},
|
|
26841
|
+
outputSchema: {
|
|
26842
|
+
comparison_id: string2(),
|
|
26843
|
+
job_id: string2(),
|
|
26844
|
+
result: number2().optional().describe("The result picked; absent when the Job was picked as a whole."),
|
|
26845
|
+
picked_at: string2().describe("When the Pick was recorded, ISO 8601.")
|
|
26846
|
+
}
|
|
26847
|
+
}, async ({ comparison_id, job_id, result }) => {
|
|
26848
|
+
try {
|
|
26849
|
+
const pick = await pickResult(deps.dirs, { comparison: comparison_id, jobId: job_id, result }, now());
|
|
26850
|
+
const subject = pick.result === undefined ? `Job ${pick.jobId}` : `result ${pick.result} of Job ${pick.jobId}`;
|
|
26851
|
+
return {
|
|
26852
|
+
content: [{ type: "text", text: `Recorded the User's Pick in Comparison ${pick.comparison}: ${subject}.` }],
|
|
26853
|
+
structuredContent: pickOutput(pick)
|
|
26854
|
+
};
|
|
26855
|
+
} catch (error) {
|
|
26856
|
+
if (error instanceof PickError)
|
|
26857
|
+
return toolError(error.message);
|
|
26858
|
+
throw error;
|
|
26859
|
+
}
|
|
26860
|
+
});
|
|
24963
26861
|
server.registerTool("list_models", {
|
|
24964
|
-
description: "List the User's configured
|
|
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).",
|
|
24965
26863
|
inputSchema: {
|
|
24966
26864
|
model: string2().optional().describe("Alias of one Model to describe in detail. Omit to list every Model.")
|
|
24967
26865
|
},
|
|
24968
26866
|
outputSchema: {
|
|
24969
26867
|
default_model: string2().nullable().describe("Alias of the default image Model, used when `generate_image` gets no `model`."),
|
|
26868
|
+
default_video_model: string2().nullable().describe("Alias of the default video Model, used when `generate_video` gets no `model`."),
|
|
24970
26869
|
models: array(listedModelSchema)
|
|
24971
26870
|
}
|
|
24972
26871
|
}, async ({ model: alias }) => {
|
|
24973
26872
|
try {
|
|
24974
26873
|
const config = await loadConfig(deps.dirs);
|
|
24975
26874
|
if (config.models.length === 0)
|
|
24976
|
-
throw needsSettings("litevision has no
|
|
24977
|
-
const defaultModel = defaultModelAlias(config) ?? null;
|
|
26875
|
+
throw needsSettings("litevision has no Model configured yet.");
|
|
26876
|
+
const defaultModel = defaultModelAlias(config, "image") ?? null;
|
|
26877
|
+
const defaultVideoModel = defaultModelAlias(config, "video") ?? null;
|
|
24978
26878
|
let models = config.models;
|
|
24979
26879
|
if (alias !== undefined) {
|
|
24980
26880
|
models = models.filter((model) => model.alias === alias);
|
|
@@ -24982,12 +26882,13 @@ ${job.files.join(`
|
|
|
24982
26882
|
throw new ConfigError(`Unknown Model Alias "${alias}". Known Aliases: ${config.models.map((model) => model.alias).join(", ")}.`);
|
|
24983
26883
|
}
|
|
24984
26884
|
}
|
|
24985
|
-
const
|
|
26885
|
+
const ratedJobs = await readRatedJobs(deps.dirs);
|
|
26886
|
+
const entries = await Promise.all(models.map((model) => listedModel(infoDeps, config, model, model.alias === (model.modality === "video" ? defaultVideoModel : defaultModel), alias !== undefined, ratedJobs)));
|
|
24986
26887
|
const text = entries.map((entry) => entry.text).join(`
|
|
24987
26888
|
`);
|
|
24988
26889
|
return {
|
|
24989
26890
|
content: [{ type: "text", text }],
|
|
24990
|
-
structuredContent: { default_model: defaultModel, models: entries.map((entry) => entry.output) }
|
|
26891
|
+
structuredContent: { default_model: defaultModel, default_video_model: defaultVideoModel, models: entries.map((entry) => entry.output) }
|
|
24991
26892
|
};
|
|
24992
26893
|
} catch (error) {
|
|
24993
26894
|
if (error instanceof ConfigError)
|