luxin-cli 0.2.3 → 0.2.5
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/CHANGELOG.md +61 -0
- package/bin/luxin.mjs +830 -206
- package/cli.md +142 -52
- package/commands.json +1 -0
- package/llms.txt +6 -6
- package/package.json +2 -3
- package/skill.md +14 -10
- package/skills/agent-image-generation/SKILL.md +1 -1
- package/skills/ai-audio-generation/SKILL.md +1 -1
- package/skills/ai-image-generation/SKILL.md +1 -1
- package/skills/ai-video-generation/SKILL.md +1 -1
- package/skills/creative-media/SKILL.md +1 -1
- package/skills/image-edit/SKILL.md +1 -1
- package/skills/image-generation/SKILL.md +2 -2
- package/skills/image-skill/SKILL.md +78 -0
- package/skills/image-to-3d/SKILL.md +2 -2
- package/skills/luxin/SKILL.md +14 -10
- package/skills/luxin/references/cli.md +142 -52
- package/skills/luxin/references/commands.json +1 -0
- package/skills/luxin/references/llms.txt +6 -6
- package/SKILL.md +0 -436
package/bin/luxin.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { Readable } from "node:stream";
|
|
|
15
15
|
import { pipeline } from "node:stream/promises";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
|
|
18
|
-
const VERSION = "0.2.
|
|
18
|
+
const VERSION = "0.2.5";
|
|
19
19
|
const PACKAGE_NAME = "luxin";
|
|
20
20
|
const DEFAULT_API_BASE_URL = "https://api.luxin.sh";
|
|
21
21
|
const DEFAULT_DOCS_BASE_URL = "https://luxin.sh";
|
|
@@ -32,6 +32,39 @@ const PROMPTLESS_EDIT_MODEL_IDS = new Set([
|
|
|
32
32
|
// provider rejects any prompt, so the public CLI must not require/send one.
|
|
33
33
|
"fal.trellis-image-to-3d",
|
|
34
34
|
]);
|
|
35
|
+
// #2204-#2216: the guide fields the `ready_compact` output mode drops. Kept as
|
|
36
|
+
// one list so the delete and `data.output_mode.omitted_fields` cannot drift.
|
|
37
|
+
const COMPACT_OMITTABLE_GUIDE_FIELDS = [
|
|
38
|
+
"auth_handoff",
|
|
39
|
+
"auth_ready",
|
|
40
|
+
"escape_hatches",
|
|
41
|
+
"guide_recovery",
|
|
42
|
+
"no_spend_evaluation",
|
|
43
|
+
"recommended_no_spend_command",
|
|
44
|
+
"recommended_no_spend_command_effect",
|
|
45
|
+
"recommended_no_spend_command_label",
|
|
46
|
+
"self_fund_handoff",
|
|
47
|
+
"self_fund_next_command",
|
|
48
|
+
"self_fund_next_command_label",
|
|
49
|
+
"self_fund_preparation",
|
|
50
|
+
];
|
|
51
|
+
// Kept whenever the pre-wall top-up is recommended: dropping the funding nudge
|
|
52
|
+
// at ready_to_create would regress the self-fund wall work.
|
|
53
|
+
const COMPACT_SELF_FUND_GUIDE_FIELDS = new Set([
|
|
54
|
+
"self_fund_handoff",
|
|
55
|
+
"self_fund_next_command",
|
|
56
|
+
"self_fund_next_command_label",
|
|
57
|
+
"self_fund_preparation",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// #2194 + #2204-#2216: in `ready_compact` mode `escape_hatches` is omitted, so
|
|
61
|
+
// the no-spend discovery note points at the literal command instead of a field
|
|
62
|
+
// that is not in the response.
|
|
63
|
+
function noSpendDiscoveryNote(readyCompact) {
|
|
64
|
+
return readyCompact
|
|
65
|
+
? "No-spend output mode omits live self-fund and payment command templates. Re-run this guide without --no-spend to surface them; self_fund_discovery.inspect_methods_command stays available for read-only, no-spend payment-method inspection. provider_call, credit_debit, and media_write remain false (see data.mutation). data.output_mode lists the fields this ready_to_create response also omits."
|
|
66
|
+
: "No-spend output mode omits live self-fund and payment command templates. Re-run this guide without --no-spend to surface them; data.escape_hatches.payment_methods stays available for read-only, no-spend payment-method inspection. provider_call, credit_debit, and media_write remain false (see data.mutation).";
|
|
67
|
+
}
|
|
35
68
|
const DEFAULT_CONFIG_PATH = join(
|
|
36
69
|
process.env.XDG_CONFIG_HOME ?? join(os.homedir(), ".config"),
|
|
37
70
|
"luxin",
|
|
@@ -52,9 +85,28 @@ const AGENT_SKILL_URL = "https://luxin.sh/skill.md";
|
|
|
52
85
|
const AGENT_LLMS_URL = "https://luxin.sh/llms.txt";
|
|
53
86
|
const AGENT_CLI_DOCS_URL = "https://luxin.sh/cli.md";
|
|
54
87
|
const CREDIT_UNIT_USD = 0.01;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
88
|
+
const CANONICAL_PUBLIC_MEDIA_HOST = "media.luxin.sh";
|
|
89
|
+
const LEGACY_PUBLIC_MEDIA_HOST = "media.image-skill.com";
|
|
90
|
+
// media.luxin.sh is the canonical media host. media.image-skill.com is the
|
|
91
|
+
// pre-rename host; it still serves the identical objects, so asset URLs handed
|
|
92
|
+
// out under it stay valid inputs forever.
|
|
93
|
+
const ACCEPTED_PUBLIC_MEDIA_HOSTS = [
|
|
94
|
+
CANONICAL_PUBLIC_MEDIA_HOST,
|
|
95
|
+
LEGACY_PUBLIC_MEDIA_HOST,
|
|
96
|
+
];
|
|
97
|
+
// Credit prices for the xAI Grok Imagine image variants, in credits
|
|
98
|
+
// (1 credit = CREDIT_UNIT_USD). The guide prices model parameters locally so it
|
|
99
|
+
// can pick a resolution without a second round trip; the hosted API remains the
|
|
100
|
+
// authority. Exact fractions keep the local estimate identical to the hosted
|
|
101
|
+
// price. These are prices, not costs: the CLI never computes or publishes
|
|
102
|
+
// provider cost or margin.
|
|
103
|
+
const XAI_IMAGE_CREDIT_PRICES = {
|
|
104
|
+
source_image: { standard: 1 / 3, quality: 5 / 3 },
|
|
105
|
+
output_image: {
|
|
106
|
+
standard: 10 / 3,
|
|
107
|
+
quality: { "1k": 25 / 3, "2k": 35 / 3 },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
58
110
|
const MODALITY_COMMAND_ALIASES = new Map([
|
|
59
111
|
["image", { command: "create", intent: null }],
|
|
60
112
|
["video", { command: "create", intent: "video" }],
|
|
@@ -156,6 +208,111 @@ const GUIDE_NEXT_COMMAND_PLACEHOLDERS = [
|
|
|
156
208
|
},
|
|
157
209
|
];
|
|
158
210
|
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Explicit aspect-ratio intent (#2213/#2203/#2209, hardened by #2243/#2252).
|
|
213
|
+
//
|
|
214
|
+
// An agent that briefs "a 16:9 cinematic widescreen poster" is stating a
|
|
215
|
+
// REQUIRED output shape, not decorating the prompt. This CLI used to ignore it
|
|
216
|
+
// entirely: `createGuideSuggestedAspectRatio` only ever suggested a ratio for
|
|
217
|
+
// video models, so `selection.suggested_aspect_ratio` was null for every image
|
|
218
|
+
// create and the emitted `next_command` carried no `--aspect-ratio`. The agent
|
|
219
|
+
// then got the model default with no signal that the ratio it asked for had
|
|
220
|
+
// been dropped.
|
|
221
|
+
//
|
|
222
|
+
// Aspect ratio is a TOP-LEVEL NORMALIZED control (`--aspect-ratio`, validated
|
|
223
|
+
// against `media.input.aspect_ratios.values`), NOT a `model_parameters` field.
|
|
224
|
+
//
|
|
225
|
+
// Conservative on purpose: a false negative degrades to the old behavior (no
|
|
226
|
+
// suggested ratio), while a false positive silently overrides the shape an
|
|
227
|
+
// agent actually wanted. So this fires ONLY on an explicit ratio token from a
|
|
228
|
+
// known allowlist, or on an orientation word BOUND to an orientation noun.
|
|
229
|
+
// Bare "portrait" and bare "landscape" are deliberately excluded -- "a portrait
|
|
230
|
+
// of a woman" and "a landscape at dusk" are subject/genre words.
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
// Ratio tokens accepted verbatim from a brief. Deliberately limited to the
|
|
234
|
+
// unambiguous photographic/display ratios. `2:1` and `1:2` are NOT here: they
|
|
235
|
+
// collide with odds and mixing ratios far more often than they express a shape.
|
|
236
|
+
const EXPLICIT_ASPECT_RATIOS = [
|
|
237
|
+
"1:1",
|
|
238
|
+
"4:3",
|
|
239
|
+
"3:4",
|
|
240
|
+
"3:2",
|
|
241
|
+
"2:3",
|
|
242
|
+
"16:9",
|
|
243
|
+
"9:16",
|
|
244
|
+
"21:9",
|
|
245
|
+
"9:21",
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
// A `W:H` token that is not part of a longer numeric run (so "16:9:30" and
|
|
249
|
+
// "1920:1080:2" do not match) and is not a clock time ("9:16 am").
|
|
250
|
+
const ASPECT_RATIO_TOKEN_RE =
|
|
251
|
+
/(?<![\d.:])(\d{1,2}):(\d{1,2})(?![\d.:])(?!\s*[ap]\.?m\.?\b)/gi;
|
|
252
|
+
|
|
253
|
+
// A `W:H` token can appear in a brief without being a shape request:
|
|
254
|
+
//
|
|
255
|
+
// "the ratio of sugar to flour is 3:2 in this recipe" -> must not fire
|
|
256
|
+
// "a train departing at 16:9" -> must not fire
|
|
257
|
+
//
|
|
258
|
+
// Deliberately narrow. These match the immediate context of the token only;
|
|
259
|
+
// they are not an attempt at natural-language understanding, and #2252 had to
|
|
260
|
+
// walk back a first cut that over-matched.
|
|
261
|
+
const RATIO_SUPPRESSING_CONTEXT = [
|
|
262
|
+
// Quantity ratios and mixes: "ratio of sugar to flour is 3:2", "a 2:3 mix",
|
|
263
|
+
// "dilute 1:1", "3:2 by weight/volume".
|
|
264
|
+
//
|
|
265
|
+
// Requires the "ratio of X TO Y" quantities construction, and exempts the
|
|
266
|
+
// shape vocabulary. A first cut matched any "ratio of ... N:M" and silently
|
|
267
|
+
// killed "an image with an aspect ratio of 16:9" -- the single most standard
|
|
268
|
+
// way to state a ratio in English, and a far worse failure than the recipe
|
|
269
|
+
// case it was written for (#2252).
|
|
270
|
+
/\bratios?\s+of\b(?![^.!?]*?\b(?:width|height|aspect)\b)[^.!?]*?\bto\b[^.!?]*?\d{1,2}:\d{1,2}/i,
|
|
271
|
+
/\d{1,2}:\d{1,2}\s*(?:dilution|mix(?:ture)?|blend|ratio\s+by|by\s+(?:weight|volume)|odds)\b/i,
|
|
272
|
+
/\b(?:dilut\w*|mix(?:ed|ing)?|blend\w*|steep\w*)\s+(?:it\s+|them\s+)?(?:at\s+|to\s+)?\d{1,2}:\d{1,2}/i,
|
|
273
|
+
// Clock times written without a leading zero: "a train departing at 16:9".
|
|
274
|
+
// `ASPECT_RATIO_TOKEN_RE` already rejects the "9:16 am" form; this covers the
|
|
275
|
+
// bare 24-hour reading.
|
|
276
|
+
//
|
|
277
|
+
// Keyed on a SCHEDULE VERB, not on a bare preposition. "at" alone is
|
|
278
|
+
// ambiguous and suppressing it broke a real request -- "render at 4:3 for the
|
|
279
|
+
// retro TV bumper" is a shape request and must keep firing.
|
|
280
|
+
//
|
|
281
|
+
// `leav` and `board` are deliberately NOT in this list: they collide with
|
|
282
|
+
// ordinary image nouns (autumn leaves, boardwalk, surfboard, keyboard,
|
|
283
|
+
// billboard), so "a photo of autumn leaves at 16:9" would lose its ratio.
|
|
284
|
+
/\b(?:depart|arriv|due|scheduled|timetabled)\w*\s+(?:at|by|around)?\s*\d{1,2}:\d{1,2}\b/i,
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
// Orientation nouns that turn an ambiguous orientation adjective into an
|
|
288
|
+
// unambiguous shape request. "portrait orientation" / "vertical video" /
|
|
289
|
+
// "square canvas" are shape requests; "portrait of a woman" is not.
|
|
290
|
+
const ORIENTATION_NOUN =
|
|
291
|
+
"orientation|format|mode|aspect(?:\\s+ratio)?|ratio|crop|canvas|image|photo|picture|poster|banner|video|frame|composition|layout|thumbnail|cover";
|
|
292
|
+
|
|
293
|
+
function boundOrientationRe(adjective) {
|
|
294
|
+
// "<adj> <noun>" (e.g. "vertical video") or "in <adj>" / "<adj> orientation".
|
|
295
|
+
return new RegExp(
|
|
296
|
+
`\\b(?:in\\s+${adjective}\\b|${adjective}[\\s-]+(?:${ORIENTATION_NOUN})\\b)`,
|
|
297
|
+
"i",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const ORIENTATION_RULES = [
|
|
302
|
+
// "widescreen" and "ultrawide" are unambiguous on their own -- they have no
|
|
303
|
+
// competing subject/genre meaning.
|
|
304
|
+
{ re: /\bultra[\s-]?wide(?:screen)?\b/i, ratio: "21:9" },
|
|
305
|
+
{ re: /\bwide[\s-]?screen\b/i, ratio: "16:9" },
|
|
306
|
+
// These four need an orientation noun to bind to.
|
|
307
|
+
{ re: boundOrientationRe("portrait"), ratio: "9:16" },
|
|
308
|
+
{ re: boundOrientationRe("vertical"), ratio: "9:16" },
|
|
309
|
+
{ re: boundOrientationRe("landscape"), ratio: "16:9" },
|
|
310
|
+
{ re: boundOrientationRe("horizontal"), ratio: "16:9" },
|
|
311
|
+
{ re: boundOrientationRe("square"), ratio: "1:1" },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const GUIDE_ASPECT_RATIO_PROBE_LIMIT = 8;
|
|
315
|
+
|
|
159
316
|
function agentSkillHint(commandPrefix = "luxin") {
|
|
160
317
|
return {
|
|
161
318
|
url: AGENT_SKILL_URL,
|
|
@@ -553,6 +710,7 @@ function commandHelpByKey(key) {
|
|
|
553
710
|
optional_flags: [
|
|
554
711
|
"--guide",
|
|
555
712
|
"--no-spend",
|
|
713
|
+
"--explain",
|
|
556
714
|
"--dry-run",
|
|
557
715
|
"--model",
|
|
558
716
|
"--aspect-ratio",
|
|
@@ -1753,16 +1911,71 @@ async function createGuide(args, options = {}) {
|
|
|
1753
1911
|
return token.result;
|
|
1754
1912
|
}
|
|
1755
1913
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1914
|
+
// Explicit aspect ratio stated in the brief, treated as a required capability
|
|
1915
|
+
// during model selection and propagated into the emitted commands
|
|
1916
|
+
// (#2213/#2203/#2209, guards corrected by #2252).
|
|
1917
|
+
const briefAspectRatio = requestedAspectRatioFromBrief({
|
|
1918
|
+
prompt: trimmedPrompt,
|
|
1919
|
+
intent: requestedIntentFlag,
|
|
1920
|
+
});
|
|
1921
|
+
const listedModels =
|
|
1922
|
+
models.envelope.ok && Array.isArray(models.envelope.data?.models)
|
|
1923
|
+
? models.envelope.data.models
|
|
1924
|
+
: null;
|
|
1925
|
+
const selectModel = (candidateModels) =>
|
|
1926
|
+
candidateModels === null
|
|
1927
|
+
? null
|
|
1928
|
+
: selectCreateGuideModel(candidateModels, requestedModelId, {
|
|
1759
1929
|
operation: guideOperation,
|
|
1760
1930
|
prompt: trimmedPrompt,
|
|
1761
1931
|
intent: requestedIntent,
|
|
1762
1932
|
maxEstimatedUsdPerImage,
|
|
1933
|
+
requestedAspectRatio: briefAspectRatio,
|
|
1934
|
+
});
|
|
1935
|
+
let selection = selectModel(listedModels);
|
|
1936
|
+
// The compact hosted list omits `media.input.aspect_ratios` entirely, so the
|
|
1937
|
+
// ratio capability can only come from the detail endpoint. Probe when the
|
|
1938
|
+
// brief named a ratio (selection itself has to be capability-aware) and also
|
|
1939
|
+
// when the pick is a video model: `selection.suggested_aspect_ratio` is
|
|
1940
|
+
// video-only, and without the probe it reported null on every real video
|
|
1941
|
+
// brief that did not state a ratio -- the same
|
|
1942
|
+
// published-payload-omits-the-field defect as #2265, one field over.
|
|
1943
|
+
const needsAspectRatioCapabilities =
|
|
1944
|
+
briefAspectRatio !== null || selection?.model?.modality === "video";
|
|
1945
|
+
if (needsAspectRatioCapabilities && listedModels !== null) {
|
|
1946
|
+
// Two-pass: the first pass names the models worth asking about, then one
|
|
1947
|
+
// bounded round of detail lookups makes the second pass actually
|
|
1948
|
+
// capability-aware instead of guessing.
|
|
1949
|
+
const capabilityModels = await guideModelsWithAspectRatioCapabilities({
|
|
1950
|
+
models: listedModels,
|
|
1951
|
+
modelIds: guideAspectRatioProbeModelIds({
|
|
1952
|
+
models: listedModels,
|
|
1953
|
+
selected: selection?.model ?? null,
|
|
1954
|
+
intent: requestedIntent,
|
|
1955
|
+
routing: briefAspectRatio !== null,
|
|
1956
|
+
}),
|
|
1957
|
+
apiBaseUrl,
|
|
1958
|
+
});
|
|
1959
|
+
if (capabilityModels !== listedModels) {
|
|
1960
|
+
selection = selectModel(capabilityModels);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const selected = selection?.model ?? null;
|
|
1964
|
+
const selectedAspectRatio = createGuideSuggestedAspectRatio({
|
|
1965
|
+
model: selected,
|
|
1966
|
+
requestedAspectRatio: briefAspectRatio,
|
|
1967
|
+
});
|
|
1968
|
+
// The brief asked for a ratio and the selected model cannot express it, so no
|
|
1969
|
+
// model could. Say so instead of dropping it silently.
|
|
1970
|
+
const unsupportedAspectRatioWarning =
|
|
1971
|
+
briefAspectRatio !== null &&
|
|
1972
|
+
selectedAspectRatio === null &&
|
|
1973
|
+
selected !== null
|
|
1974
|
+
? aspectRatioUnsupportedWarning({
|
|
1975
|
+
aspectRatio: briefAspectRatio,
|
|
1976
|
+
modelId: selected.id,
|
|
1763
1977
|
})
|
|
1764
1978
|
: null;
|
|
1765
|
-
const selectedAspectRatio = createGuideSuggestedAspectRatio(selected);
|
|
1766
1979
|
const pricingContext = {
|
|
1767
1980
|
aspectRatio: selectedAspectRatio ?? "1:1",
|
|
1768
1981
|
outputCount: 1,
|
|
@@ -1780,41 +1993,30 @@ async function createGuide(args, options = {}) {
|
|
|
1780
1993
|
modelParameters: modelParameters.value ?? {},
|
|
1781
1994
|
maxEstimatedUsdPerImage,
|
|
1782
1995
|
});
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1996
|
+
// Price the request the guide is about to emit, at the ratio it will emit it
|
|
1997
|
+
// with -- not the registry's default-shape quote, which describes a request
|
|
1998
|
+
// `create` never makes (#2259). Models priced from input-image dimensions stay
|
|
1999
|
+
// on the quote: the guide uploads nothing, so there are no dimensions to price
|
|
2000
|
+
// from. `tests/hosted-create-estimate-parity.test.ts` pins this against the
|
|
2001
|
+
// source dry-run so the two implementations cannot drift apart again.
|
|
1789
2002
|
const pricing =
|
|
1790
|
-
selected === null
|
|
1791
|
-
?
|
|
1792
|
-
:
|
|
1793
|
-
? createGuidePricingForModel(
|
|
1794
|
-
selected,
|
|
1795
|
-
defaultedModelParameters.modelParameters,
|
|
1796
|
-
pricingContext,
|
|
1797
|
-
)
|
|
1798
|
-
: createGuideModelCreditPricing(selected);
|
|
1799
|
-
const providerCostEstimate =
|
|
1800
|
-
selected === null || !shouldPriceModelParameters
|
|
1801
|
-
? null
|
|
1802
|
-
: createGuideProviderCostEstimateForModel(
|
|
2003
|
+
selected === null || createGuideSelectedModelRequiresInputImage(selected)
|
|
2004
|
+
? createGuideModelCreditPricing(selected)
|
|
2005
|
+
: createGuidePricingForModel(
|
|
1803
2006
|
selected,
|
|
1804
2007
|
defaultedModelParameters.modelParameters,
|
|
1805
2008
|
pricingContext,
|
|
1806
2009
|
);
|
|
1807
2010
|
const estimatedCredits = pricing?.credits_required ?? null;
|
|
1808
|
-
|
|
1809
|
-
|
|
2011
|
+
// The guide publishes what the caller will be charged, never our provider
|
|
2012
|
+
// cost. `economics.estimated_usd_per_image` is the hosted charge per image.
|
|
2013
|
+
const estimatedDebitUsdPerImage =
|
|
2014
|
+
pricing?.estimated_charge_usd ??
|
|
1810
2015
|
selected?.economics?.estimated_usd_per_image ??
|
|
1811
|
-
pricing?.fallback_provider_cost_usd ??
|
|
1812
2016
|
(typeof selected?.estimated_usd_per_image === "number"
|
|
1813
2017
|
? selected.estimated_usd_per_image
|
|
1814
2018
|
: null) ??
|
|
1815
2019
|
null;
|
|
1816
|
-
const estimatedDebitUsdPerImage =
|
|
1817
|
-
pricing?.estimated_revenue_usd ?? estimatedProviderUsdPerImage;
|
|
1818
2020
|
const budgetGuard =
|
|
1819
2021
|
maxEstimatedUsdPerImage ??
|
|
1820
2022
|
estimatedDebitUsdPerImage ??
|
|
@@ -1863,10 +2065,19 @@ async function createGuide(args, options = {}) {
|
|
|
1863
2065
|
guideCommandPrefix,
|
|
1864
2066
|
);
|
|
1865
2067
|
const blocker = createGuideBlocker(stage, {
|
|
2068
|
+
operation: guideOperation,
|
|
1866
2069
|
requestedModelId,
|
|
1867
2070
|
quota,
|
|
1868
2071
|
estimatedCredits,
|
|
1869
2072
|
});
|
|
2073
|
+
// #2204-#2216: `--explain` opts back into the complete payload in every
|
|
2074
|
+
// stage; the compact default is for the agent that just wants the decision.
|
|
2075
|
+
// The payment/self-fund/recovery apparatus is correct when the guide is
|
|
2076
|
+
// blocked and noise when it is not, so shape on that state rather than on a
|
|
2077
|
+
// flag the caller has to know about.
|
|
2078
|
+
const explainOutputMode = flagBool(args, "explain");
|
|
2079
|
+
const readyCompact =
|
|
2080
|
+
stage === "ready_to_create" && blocker === null && !explainOutputMode;
|
|
1870
2081
|
const nextCommand = createGuideNextCommand(stage, {
|
|
1871
2082
|
prompt: trimmedPrompt,
|
|
1872
2083
|
selected,
|
|
@@ -1925,11 +2136,21 @@ async function createGuide(args, options = {}) {
|
|
|
1925
2136
|
noSpendNextCommandLabel,
|
|
1926
2137
|
noSpendNextCommandEffect,
|
|
1927
2138
|
});
|
|
1928
|
-
const
|
|
2139
|
+
const baseGuideWarning = createGuideWarning(stage, {
|
|
1929
2140
|
nextCommandEffect,
|
|
1930
2141
|
paymentSummary,
|
|
1931
2142
|
nextCommandCopyRunnable,
|
|
2143
|
+
readyCompact,
|
|
1932
2144
|
});
|
|
2145
|
+
// Append, never replace: the stage warning carries the spend-safety contract
|
|
2146
|
+
// agents rely on, and the ratio note is additive context.
|
|
2147
|
+
const guideWarning =
|
|
2148
|
+
unsupportedAspectRatioWarning === null
|
|
2149
|
+
? baseGuideWarning
|
|
2150
|
+
: {
|
|
2151
|
+
...baseGuideWarning,
|
|
2152
|
+
warning: `${baseGuideWarning.warning} ${unsupportedAspectRatioWarning}`,
|
|
2153
|
+
};
|
|
1933
2154
|
const quotaTopUp = createGuideQuotaTopUp(
|
|
1934
2155
|
quota?.envelope.data?.top_up ?? null,
|
|
1935
2156
|
);
|
|
@@ -1991,9 +2212,16 @@ async function createGuide(args, options = {}) {
|
|
|
1991
2212
|
// dry-run command and the mutation proof, and keep the self-fund path
|
|
1992
2213
|
// discoverable via self_fund_discovery instead of dominating the response.
|
|
1993
2214
|
const noSpendOutputMode = flagBool(args, "no-spend");
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2215
|
+
// Keep the pre-wall self-fund nudge whenever it is actually recommended.
|
|
2216
|
+
const selfFundRecommended = selfFundPreparation?.recommended === true;
|
|
2217
|
+
const paymentSummaryOutput =
|
|
2218
|
+
noSpendOutputMode || readyCompact
|
|
2219
|
+
? {
|
|
2220
|
+
...paymentSummary,
|
|
2221
|
+
suggested_commands: [],
|
|
2222
|
+
...(readyCompact ? { preferred_method_summary: null } : {}),
|
|
2223
|
+
}
|
|
2224
|
+
: paymentSummary;
|
|
1997
2225
|
const selfFundHandoffOutput = noSpendOutputMode ? null : selfFundHandoff;
|
|
1998
2226
|
const selfFundPreparationOutput = noSpendOutputMode
|
|
1999
2227
|
? null
|
|
@@ -2030,7 +2258,7 @@ async function createGuide(args, options = {}) {
|
|
|
2030
2258
|
},
|
|
2031
2259
|
),
|
|
2032
2260
|
inspect_methods_command: escapeHatches.payment_methods,
|
|
2033
|
-
note:
|
|
2261
|
+
note: noSpendDiscoveryNote(readyCompact),
|
|
2034
2262
|
},
|
|
2035
2263
|
}
|
|
2036
2264
|
: {
|
|
@@ -2046,7 +2274,49 @@ async function createGuide(args, options = {}) {
|
|
|
2046
2274
|
escapeHatches,
|
|
2047
2275
|
selfFundNextCommand: selfFundNextCommandOutput,
|
|
2048
2276
|
});
|
|
2049
|
-
|
|
2277
|
+
// #2204-#2216: fields the compact ready payload drops. `self_fund_*` only
|
|
2278
|
+
// joins the list when the pre-wall top-up is not recommended.
|
|
2279
|
+
const compactOmittedFields = readyCompact
|
|
2280
|
+
? COMPACT_OMITTABLE_GUIDE_FIELDS.filter(
|
|
2281
|
+
(field) =>
|
|
2282
|
+
selfFundRecommended === false ||
|
|
2283
|
+
!COMPACT_SELF_FUND_GUIDE_FIELDS.has(field),
|
|
2284
|
+
)
|
|
2285
|
+
: [];
|
|
2286
|
+
const compactCompactedFields = readyCompact
|
|
2287
|
+
? [
|
|
2288
|
+
"checks.payments.preferred_method_summary",
|
|
2289
|
+
"checks.payments.suggested_commands",
|
|
2290
|
+
"checks.quota.top_up",
|
|
2291
|
+
]
|
|
2292
|
+
: [];
|
|
2293
|
+
const outputModeData = {
|
|
2294
|
+
schema: "image-skill.guide-output-mode.v1",
|
|
2295
|
+
mode: readyCompact ? "ready_compact" : "full",
|
|
2296
|
+
reason: readyCompact
|
|
2297
|
+
? "stage is ready_to_create with no blocker, so the guide returns the decision, the cost, and the no-spend proof. Payment, self-fund, auth-handoff, and recovery detail belong to blocked stages and are omitted here."
|
|
2298
|
+
: explainOutputMode
|
|
2299
|
+
? "--explain returns every guide field in every stage."
|
|
2300
|
+
: "the guide is not at an unblocked ready_to_create stage, so every field is returned.",
|
|
2301
|
+
omitted_fields: compactOmittedFields,
|
|
2302
|
+
compacted_fields: compactCompactedFields,
|
|
2303
|
+
full_output_command: renderGuideCommand(
|
|
2304
|
+
trimmedPrompt.length > 0 ? trimmedPrompt : "PROMPT",
|
|
2305
|
+
explicitApiBaseUrl(args),
|
|
2306
|
+
guideCommandPrefix,
|
|
2307
|
+
{
|
|
2308
|
+
operation: guideOperation,
|
|
2309
|
+
inputReference: options.inputReference,
|
|
2310
|
+
modelId: requestedModelId,
|
|
2311
|
+
providerId: requestedProviderId,
|
|
2312
|
+
intent: requestedIntentFlag,
|
|
2313
|
+
maxEstimatedUsdPerImage,
|
|
2314
|
+
modelParametersJson: requestedModelParametersJson,
|
|
2315
|
+
explain: true,
|
|
2316
|
+
},
|
|
2317
|
+
),
|
|
2318
|
+
};
|
|
2319
|
+
const guideData = {
|
|
2050
2320
|
schema:
|
|
2051
2321
|
guideOperation === "edit"
|
|
2052
2322
|
? "image-skill.edit-guide.v1"
|
|
@@ -2086,7 +2356,7 @@ async function createGuide(args, options = {}) {
|
|
|
2086
2356
|
required_credits: estimatedCredits,
|
|
2087
2357
|
daily_jobs_remaining:
|
|
2088
2358
|
quota?.envelope.data?.daily_jobs?.remaining ?? null,
|
|
2089
|
-
top_up: quotaTopUp,
|
|
2359
|
+
top_up: readyCompact ? null : quotaTopUp,
|
|
2090
2360
|
reason:
|
|
2091
2361
|
quota === null
|
|
2092
2362
|
? "auth_required"
|
|
@@ -2109,20 +2379,28 @@ async function createGuide(args, options = {}) {
|
|
|
2109
2379
|
modality: selected.modality ?? null,
|
|
2110
2380
|
suggested_aspect_ratio: selectedAspectRatio,
|
|
2111
2381
|
reason:
|
|
2112
|
-
requestedModelId
|
|
2113
|
-
?
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2382
|
+
requestedModelId !== null
|
|
2383
|
+
? createGuideRequestedSelectionReason(selected)
|
|
2384
|
+
: selection?.aspectRatioRoutedFrom != null &&
|
|
2385
|
+
briefAspectRatio !== null
|
|
2386
|
+
? aspectRatioRoutedCreateSelectionReason({
|
|
2387
|
+
aspectRatio: briefAspectRatio,
|
|
2388
|
+
skippedModelId: selection.aspectRatioRoutedFrom,
|
|
2389
|
+
})
|
|
2390
|
+
: selection?.aspectRatioCapableFallback === true &&
|
|
2391
|
+
briefAspectRatio !== null
|
|
2392
|
+
? `guide selected an available model that supports the requested ${briefAspectRatio} aspect ratio`
|
|
2393
|
+
: createGuideSelectionReason(
|
|
2394
|
+
selected,
|
|
2395
|
+
trimmedPrompt,
|
|
2396
|
+
requestedIntent,
|
|
2397
|
+
guideOperation,
|
|
2398
|
+
),
|
|
2120
2399
|
},
|
|
2121
2400
|
cost: {
|
|
2122
2401
|
estimated_credits: estimatedCredits,
|
|
2123
2402
|
estimated_usd_per_image: estimatedDebitUsdPerImage,
|
|
2124
2403
|
estimated_debit_usd_per_image: estimatedDebitUsdPerImage,
|
|
2125
|
-
estimated_provider_usd_per_image: estimatedProviderUsdPerImage,
|
|
2126
2404
|
credit_unit_usd: pricing?.credit_unit_usd ?? CREDIT_UNIT_USD,
|
|
2127
2405
|
pricing_confidence: pricing?.pricing_confidence ?? null,
|
|
2128
2406
|
pricing_source: pricing?.pricing_source ?? null,
|
|
@@ -2152,6 +2430,7 @@ async function createGuide(args, options = {}) {
|
|
|
2152
2430
|
auth_handoff: authHandoff,
|
|
2153
2431
|
escape_hatches: escapeHatches,
|
|
2154
2432
|
no_spend_output_mode: noSpendOutputModeData,
|
|
2433
|
+
output_mode: outputModeData,
|
|
2155
2434
|
mutation: {
|
|
2156
2435
|
provider_call: false,
|
|
2157
2436
|
hosted_create: false,
|
|
@@ -2160,7 +2439,11 @@ async function createGuide(args, options = {}) {
|
|
|
2160
2439
|
credit_debit: false,
|
|
2161
2440
|
media_write: false,
|
|
2162
2441
|
},
|
|
2163
|
-
}
|
|
2442
|
+
};
|
|
2443
|
+
for (const field of compactOmittedFields) {
|
|
2444
|
+
delete guideData[field];
|
|
2445
|
+
}
|
|
2446
|
+
return createGuideSuccess(command, quota?.envelope.actor ?? null, guideData);
|
|
2164
2447
|
}
|
|
2165
2448
|
|
|
2166
2449
|
function createGuideSuccess(command, actor, data) {
|
|
@@ -2177,6 +2460,12 @@ function selectCreateGuideModel(
|
|
|
2177
2460
|
prompt = "",
|
|
2178
2461
|
intent = undefined,
|
|
2179
2462
|
maxEstimatedUsdPerImage = null,
|
|
2463
|
+
/**
|
|
2464
|
+
* Aspect ratio explicitly requested by the brief, treated as a REQUIRED
|
|
2465
|
+
* capability during selection (#2213/#2203/#2209) the same way a
|
|
2466
|
+
* reproducibility need is. Null when the brief states no ratio.
|
|
2467
|
+
*/
|
|
2468
|
+
requestedAspectRatio = null,
|
|
2180
2469
|
} = {},
|
|
2181
2470
|
) {
|
|
2182
2471
|
const isExecutableCreate = (model) =>
|
|
@@ -2187,17 +2476,23 @@ function selectCreateGuideModel(
|
|
|
2187
2476
|
const isExecutableInputImageEdit = (model) =>
|
|
2188
2477
|
model?.status === "available" &&
|
|
2189
2478
|
guideModelExecutionStatus(model) === "executable" &&
|
|
2190
|
-
Array.isArray(model?.supports) &&
|
|
2191
|
-
(model.supports.includes("edit") || model.supports.includes("variation")) &&
|
|
2192
2479
|
createGuideSelectedModelRequiresInputImage(model);
|
|
2193
2480
|
const isExecutableGuideModel = (model) =>
|
|
2194
2481
|
operation === "edit"
|
|
2195
2482
|
? isExecutableInputImageEdit(model)
|
|
2196
2483
|
: isExecutableCreate(model) || isExecutableInputImageEdit(model);
|
|
2484
|
+
const requestedRatio = requestedAspectRatio ?? null;
|
|
2485
|
+
const chosen = (
|
|
2486
|
+
model,
|
|
2487
|
+
{ aspectRatioRoutedFrom = null, aspectRatioCapableFallback = false } = {},
|
|
2488
|
+
) =>
|
|
2489
|
+
model === undefined || model === null
|
|
2490
|
+
? null
|
|
2491
|
+
: { model, aspectRatioRoutedFrom, aspectRatioCapableFallback };
|
|
2197
2492
|
if (requestedModelId !== null) {
|
|
2198
2493
|
const requested = resolveModelLookup(requestedModelId, models);
|
|
2199
2494
|
return requested !== undefined && isExecutableGuideModel(requested)
|
|
2200
|
-
? requested
|
|
2495
|
+
? chosen(requested)
|
|
2201
2496
|
: null;
|
|
2202
2497
|
}
|
|
2203
2498
|
const candidates =
|
|
@@ -2214,7 +2509,7 @@ function selectCreateGuideModel(
|
|
|
2214
2509
|
});
|
|
2215
2510
|
const threeDimensional = eligible3d[0];
|
|
2216
2511
|
if (threeDimensional !== undefined) {
|
|
2217
|
-
return threeDimensional;
|
|
2512
|
+
return chosen(threeDimensional);
|
|
2218
2513
|
}
|
|
2219
2514
|
}
|
|
2220
2515
|
const eligible = guideCandidatesWithinBudget({
|
|
@@ -2224,23 +2519,156 @@ function selectCreateGuideModel(
|
|
|
2224
2519
|
if (createGuideImpliesAudio({ prompt, intent })) {
|
|
2225
2520
|
const audio = eligible.find((model) => model?.modality === "audio");
|
|
2226
2521
|
if (audio !== undefined) {
|
|
2227
|
-
return audio;
|
|
2522
|
+
return chosen(audio);
|
|
2228
2523
|
}
|
|
2229
2524
|
}
|
|
2230
2525
|
if (createGuideImpliesVideo({ prompt, intent })) {
|
|
2231
|
-
const
|
|
2526
|
+
const videoCandidates = eligible.filter(
|
|
2527
|
+
(model) => model?.modality === "video",
|
|
2528
|
+
);
|
|
2529
|
+
const video =
|
|
2530
|
+
videoCandidates.find(
|
|
2531
|
+
(model) => !modelCannotExpressAspectRatio(model, requestedRatio),
|
|
2532
|
+
) ?? videoCandidates[0];
|
|
2232
2533
|
if (video !== undefined) {
|
|
2233
|
-
return video;
|
|
2534
|
+
return chosen(video);
|
|
2234
2535
|
}
|
|
2235
2536
|
}
|
|
2236
2537
|
const intentClass = createGuideIntentClass(intent);
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2538
|
+
const preferredCandidates = preferredCreateGuideModelIds(intentClass)
|
|
2539
|
+
.map((modelId) => eligible.find((model) => model?.id === modelId))
|
|
2540
|
+
.filter((model) => model !== undefined);
|
|
2541
|
+
const [topPreferred] = preferredCandidates;
|
|
2542
|
+
|
|
2543
|
+
// An explicitly requested aspect ratio is a REQUIRED capability, so walk the
|
|
2544
|
+
// curated preference list in strength order but skip models that are KNOWN
|
|
2545
|
+
// not to express the ratio through the normalized `--aspect-ratio` control
|
|
2546
|
+
// (#2213/#2203/#2209). Unknown capability is not a denial: see
|
|
2547
|
+
// `modelAspectRatioSupport`.
|
|
2548
|
+
const preferredSupportingRatio = preferredCandidates.find(
|
|
2549
|
+
(model) => !modelCannotExpressAspectRatio(model, requestedRatio),
|
|
2550
|
+
);
|
|
2551
|
+
if (preferredSupportingRatio !== undefined) {
|
|
2552
|
+
const routedForAspectRatio =
|
|
2553
|
+
requestedRatio !== null &&
|
|
2554
|
+
topPreferred !== undefined &&
|
|
2555
|
+
topPreferred.id !== preferredSupportingRatio.id;
|
|
2556
|
+
return chosen(preferredSupportingRatio, {
|
|
2557
|
+
aspectRatioRoutedFrom: routedForAspectRatio ? topPreferred.id : null,
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// No curated model can express the ratio: fall out to the strongest
|
|
2562
|
+
// remaining executable candidate that provably can, before degrading to the
|
|
2563
|
+
// ratio-blind fallback.
|
|
2564
|
+
if (requestedRatio !== null) {
|
|
2565
|
+
const capable = eligible.find(
|
|
2566
|
+
(model) => modelAspectRatioSupport(model, requestedRatio) === true,
|
|
2567
|
+
);
|
|
2568
|
+
if (capable !== undefined) {
|
|
2569
|
+
return chosen(capable, {
|
|
2570
|
+
aspectRatioRoutedFrom: topPreferred?.id ?? null,
|
|
2571
|
+
aspectRatioCapableFallback: topPreferred === undefined,
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// Nothing can express the requested ratio (or none was requested): keep the
|
|
2577
|
+
// ordinary selection exactly. The caller emits an honest guide_warning rather
|
|
2578
|
+
// than letting the ratio disappear silently.
|
|
2579
|
+
if (topPreferred !== undefined) {
|
|
2580
|
+
return chosen(topPreferred);
|
|
2581
|
+
}
|
|
2582
|
+
return chosen(eligible[0]);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// The hosted `/v1/models` list is a compact summary and does not publish
|
|
2586
|
+
// `media.input.aspect_ratios.values`, so a stated ratio cannot be checked
|
|
2587
|
+
// against it. Probe the detail endpoint for the handful of models the guide
|
|
2588
|
+
// would actually route between -- never the whole catalog, whose
|
|
2589
|
+
// `?details=true` payload is multiple megabytes.
|
|
2590
|
+
//
|
|
2591
|
+
// Fail-open: an unreachable, errored, or malformed detail response leaves the
|
|
2592
|
+
// capability UNKNOWN, which never disqualifies a model and never triggers the
|
|
2593
|
+
// unsupported-ratio warning.
|
|
2594
|
+
|
|
2595
|
+
function guideAspectRatioProbeModelIds({ models, selected, intent, routing }) {
|
|
2596
|
+
const ids = [];
|
|
2597
|
+
if (typeof selected?.id === "string") {
|
|
2598
|
+
ids.push(selected.id);
|
|
2599
|
+
}
|
|
2600
|
+
// Only a stated ratio can make the guide hop between models. Without one the
|
|
2601
|
+
// sole open question is what the current pick can express, so asking about
|
|
2602
|
+
// the alternatives would be a round trip spent on an answer nothing reads.
|
|
2603
|
+
if (routing) {
|
|
2604
|
+
for (const modelId of preferredCreateGuideModelIds(
|
|
2605
|
+
createGuideIntentClass(intent),
|
|
2606
|
+
)) {
|
|
2607
|
+
if (models.some((model) => model?.id === modelId)) {
|
|
2608
|
+
ids.push(modelId);
|
|
2609
|
+
}
|
|
2241
2610
|
}
|
|
2242
2611
|
}
|
|
2243
|
-
return
|
|
2612
|
+
return [...new Set(ids)].slice(0, GUIDE_ASPECT_RATIO_PROBE_LIMIT);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
async function guideModelsWithAspectRatioCapabilities({
|
|
2616
|
+
models,
|
|
2617
|
+
modelIds,
|
|
2618
|
+
apiBaseUrl,
|
|
2619
|
+
}) {
|
|
2620
|
+
const missing = modelIds.filter((modelId) => {
|
|
2621
|
+
const model = models.find((candidate) => candidate?.id === modelId);
|
|
2622
|
+
return model !== undefined && guideModelAspectRatioValues(model) === null;
|
|
2623
|
+
});
|
|
2624
|
+
if (missing.length === 0) {
|
|
2625
|
+
return models;
|
|
2626
|
+
}
|
|
2627
|
+
const probes = await Promise.all(
|
|
2628
|
+
missing.map(async (modelId) => {
|
|
2629
|
+
try {
|
|
2630
|
+
const detail = await apiRequest({
|
|
2631
|
+
command: "luxin create --guide",
|
|
2632
|
+
method: "GET",
|
|
2633
|
+
apiBaseUrl,
|
|
2634
|
+
path: `/v1/models/${encodeURIComponent(modelId)}`,
|
|
2635
|
+
});
|
|
2636
|
+
const values = guideModelAspectRatioValues(
|
|
2637
|
+
detail?.envelope?.data?.model ?? null,
|
|
2638
|
+
);
|
|
2639
|
+
return values === null ? null : { modelId, values };
|
|
2640
|
+
} catch {
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
}),
|
|
2644
|
+
);
|
|
2645
|
+
const resolved = new Map(
|
|
2646
|
+
probes
|
|
2647
|
+
.filter((probe) => probe !== null)
|
|
2648
|
+
.map((probe) => [probe.modelId, probe.values]),
|
|
2649
|
+
);
|
|
2650
|
+
if (resolved.size === 0) {
|
|
2651
|
+
return models;
|
|
2652
|
+
}
|
|
2653
|
+
return models.map((model) => {
|
|
2654
|
+
const values = resolved.get(model?.id);
|
|
2655
|
+
if (values === undefined) {
|
|
2656
|
+
return model;
|
|
2657
|
+
}
|
|
2658
|
+
return {
|
|
2659
|
+
...model,
|
|
2660
|
+
media: {
|
|
2661
|
+
...model?.media,
|
|
2662
|
+
input: {
|
|
2663
|
+
...model?.media?.input,
|
|
2664
|
+
aspect_ratios: {
|
|
2665
|
+
...model?.media?.input?.aspect_ratios,
|
|
2666
|
+
values,
|
|
2667
|
+
},
|
|
2668
|
+
},
|
|
2669
|
+
},
|
|
2670
|
+
};
|
|
2671
|
+
});
|
|
2244
2672
|
}
|
|
2245
2673
|
|
|
2246
2674
|
const MODEL_ALIAS_RULES = [
|
|
@@ -2412,29 +2840,47 @@ function createGuideIntentClass(intent) {
|
|
|
2412
2840
|
return "general";
|
|
2413
2841
|
}
|
|
2414
2842
|
|
|
2843
|
+
// Mirrors src/hosted-create-policy.ts (#2231). budget_draft is a superset of
|
|
2844
|
+
// general and both are ordered ascending by charged credits, so a
|
|
2845
|
+
// cost-minimizing intent can never select a model priced above the general
|
|
2846
|
+
// default. final keeps the quality-premium lead.
|
|
2415
2847
|
function preferredCreateGuideModelIds(intentClass) {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2848
|
+
if (intentClass === "budget_draft") {
|
|
2849
|
+
return [
|
|
2850
|
+
"fal.flux-1-schnell",
|
|
2851
|
+
"fal.flux-1-dev",
|
|
2852
|
+
"xai.grok-imagine-image",
|
|
2853
|
+
"fal.bria-text-to-image-fast",
|
|
2854
|
+
"xai.grok-imagine-image-quality",
|
|
2855
|
+
"openai.gpt-image-2",
|
|
2856
|
+
];
|
|
2857
|
+
}
|
|
2858
|
+
if (intentClass === "final") {
|
|
2859
|
+
return [
|
|
2860
|
+
"xai.grok-imagine-image-quality",
|
|
2861
|
+
"openai.gpt-image-2",
|
|
2862
|
+
"fal.flux-1-dev",
|
|
2863
|
+
"xai.grok-imagine-image",
|
|
2864
|
+
"fal.flux-1-schnell",
|
|
2865
|
+
"fal.bria-text-to-image-fast",
|
|
2866
|
+
];
|
|
2867
|
+
}
|
|
2868
|
+
return [
|
|
2869
|
+
"fal.flux-1-dev",
|
|
2870
|
+
"xai.grok-imagine-image",
|
|
2871
|
+
"xai.grok-imagine-image-quality",
|
|
2872
|
+
"openai.gpt-image-2",
|
|
2873
|
+
"fal.flux-1-schnell",
|
|
2874
|
+
"fal.bria-text-to-image-fast",
|
|
2875
|
+
];
|
|
2429
2876
|
}
|
|
2430
2877
|
|
|
2431
2878
|
function guideBudgetUsdForModel(model) {
|
|
2879
|
+
// Budget caps compare the credit debit -- what the caller pays.
|
|
2432
2880
|
const pricing = createGuideModelCreditPricing(model);
|
|
2433
2881
|
return (
|
|
2434
|
-
pricing?.
|
|
2882
|
+
pricing?.estimated_charge_usd ??
|
|
2435
2883
|
model?.economics?.estimated_usd_per_image ??
|
|
2436
|
-
pricing?.estimated_provider_cost_usd ??
|
|
2437
|
-
pricing?.fallback_provider_cost_usd ??
|
|
2438
2884
|
(typeof model?.estimated_usd_per_image === "number"
|
|
2439
2885
|
? model.estimated_usd_per_image
|
|
2440
2886
|
: null) ??
|
|
@@ -2450,7 +2896,7 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2450
2896
|
input.model?.id === "xai.grok-imagine-image-quality" &&
|
|
2451
2897
|
modelParameters.resolution === undefined
|
|
2452
2898
|
) {
|
|
2453
|
-
// Budget defaults must compare the credit debit (
|
|
2899
|
+
// Budget defaults must compare the credit debit (estimated_charge_usd),
|
|
2454
2900
|
// the same basis the hosted budget guard enforces, not the provider cost.
|
|
2455
2901
|
const twoKPricing = createGuidePricingForModel(
|
|
2456
2902
|
input.model,
|
|
@@ -2459,8 +2905,8 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2459
2905
|
);
|
|
2460
2906
|
const twoKAllowedByBudget =
|
|
2461
2907
|
input.maxEstimatedUsdPerImage === null ||
|
|
2462
|
-
typeof twoKPricing?.
|
|
2463
|
-
twoKPricing.
|
|
2908
|
+
typeof twoKPricing?.estimated_charge_usd !== "number" ||
|
|
2909
|
+
twoKPricing.estimated_charge_usd <= input.maxEstimatedUsdPerImage;
|
|
2464
2910
|
const intentClass = createGuideIntentClass(input.intent);
|
|
2465
2911
|
const resolution =
|
|
2466
2912
|
intentClass !== "budget_draft" && twoKAllowedByBudget ? "2k" : "1k";
|
|
@@ -2490,8 +2936,8 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2490
2936
|
);
|
|
2491
2937
|
const mediumAllowedByBudget =
|
|
2492
2938
|
input.maxEstimatedUsdPerImage === null ||
|
|
2493
|
-
typeof mediumPricing?.
|
|
2494
|
-
mediumPricing.
|
|
2939
|
+
typeof mediumPricing?.estimated_charge_usd !== "number" ||
|
|
2940
|
+
mediumPricing.estimated_charge_usd <= input.maxEstimatedUsdPerImage;
|
|
2495
2941
|
if (mediumAllowedByBudget) {
|
|
2496
2942
|
modelParameters.quality = "medium";
|
|
2497
2943
|
defaultsApplied.push("quality=medium");
|
|
@@ -2502,45 +2948,59 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2502
2948
|
}
|
|
2503
2949
|
|
|
2504
2950
|
function createGuidePricingForModel(model, modelParameters, context = {}) {
|
|
2505
|
-
|
|
2506
|
-
model,
|
|
2507
|
-
modelParameters,
|
|
2508
|
-
context,
|
|
2509
|
-
);
|
|
2510
|
-
if (estimate.estimated_provider_cost_usd === null) {
|
|
2511
|
-
return createGuideModelCreditPricing(model);
|
|
2951
|
+
if (createGuideCanPriceModelParameters(model)) {
|
|
2952
|
+
return createGuideXaiImagePricing(model, modelParameters, context);
|
|
2512
2953
|
}
|
|
2513
|
-
return
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
function createGuideCanPriceModelParameters(model) {
|
|
2521
|
-
return String(model?.id ?? "").startsWith("xai.grok-imagine-image");
|
|
2954
|
+
return (
|
|
2955
|
+
createGuideAspectRatioCreditPricing(model, context) ??
|
|
2956
|
+
createGuideModelCreditPricing(model)
|
|
2957
|
+
);
|
|
2522
2958
|
}
|
|
2523
2959
|
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2960
|
+
// The debit `create` will actually make at the ratio the guide is about to
|
|
2961
|
+
// emit, read from the model registry (#2259).
|
|
2962
|
+
//
|
|
2963
|
+
// `economics.credit_pricing` is the quote at the model's own default shape --
|
|
2964
|
+
// no aspect ratio, no policy model-parameter defaults. `create` never sends
|
|
2965
|
+
// that request: it always sends a concrete ratio (`1:1` unless the brief asked
|
|
2966
|
+
// otherwise) plus the hosted defaults. For any model priced from its output
|
|
2967
|
+
// shape or from a defaulted parameter the quote is therefore not the debit, and
|
|
2968
|
+
// this CLI has no pricing engine to recompute it -- so the registry publishes
|
|
2969
|
+
// the answer and this reads it. Absent field means the quote already is the
|
|
2970
|
+
// debit.
|
|
2971
|
+
function createGuideAspectRatioCreditPricing(model, context) {
|
|
2972
|
+
// Both shapes: `GET /v1/models` serves the COMPACT summary, which flattens
|
|
2973
|
+
// economics onto the row, and only `?details=true` (or a fixture built from
|
|
2974
|
+
// the full catalog) nests it. Reading only the nested one is how a fix passes
|
|
2975
|
+
// its own test against a payload production never sends.
|
|
2976
|
+
const byAspectRatio = isRecord(
|
|
2977
|
+
model?.economics?.create_credit_debit_by_aspect_ratio,
|
|
2978
|
+
)
|
|
2979
|
+
? model.economics.create_credit_debit_by_aspect_ratio
|
|
2980
|
+
: model?.create_credit_debit_by_aspect_ratio;
|
|
2981
|
+
const aspectRatio = context?.aspectRatio;
|
|
2982
|
+
if (!isRecord(byAspectRatio) || typeof aspectRatio !== "string") {
|
|
2983
|
+
return null;
|
|
2984
|
+
}
|
|
2985
|
+
const credits = byAspectRatio[aspectRatio];
|
|
2986
|
+
if (!Number.isFinite(credits)) {
|
|
2987
|
+
return null;
|
|
2531
2988
|
}
|
|
2989
|
+
const quoted = createGuideModelCreditPricing(model);
|
|
2532
2990
|
return {
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
pricing_source: "model_registry",
|
|
2539
|
-
pricing_confidence: "known",
|
|
2991
|
+
...(quoted ?? {}),
|
|
2992
|
+
credits_required: credits,
|
|
2993
|
+
credit_unit_usd: CREDIT_UNIT_USD,
|
|
2994
|
+
estimated_charge_usd: roundUsd(credits * CREDIT_UNIT_USD),
|
|
2995
|
+
pricing_confidence: quoted?.pricing_confidence ?? "known",
|
|
2540
2996
|
};
|
|
2541
2997
|
}
|
|
2542
2998
|
|
|
2543
|
-
function
|
|
2999
|
+
function createGuideCanPriceModelParameters(model) {
|
|
3000
|
+
return String(model?.id ?? "").startsWith("xai.grok-imagine-image");
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
function createGuideXaiImagePricing(model, modelParameters, context) {
|
|
2544
3004
|
const modelId = String(model?.id ?? "");
|
|
2545
3005
|
const quality = modelId.includes("-quality");
|
|
2546
3006
|
const edit = modelId.endsWith("-edit");
|
|
@@ -2555,11 +3015,20 @@ function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
|
2555
3015
|
? context.referenceAssetCount
|
|
2556
3016
|
: 0;
|
|
2557
3017
|
const sourceImageCount = edit ? 1 + referenceAssetCount : 0;
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
3018
|
+
const sourceCredits = quality
|
|
3019
|
+
? XAI_IMAGE_CREDIT_PRICES.source_image.quality
|
|
3020
|
+
: XAI_IMAGE_CREDIT_PRICES.source_image.standard;
|
|
3021
|
+
const outputCredits = quality
|
|
3022
|
+
? XAI_IMAGE_CREDIT_PRICES.output_image.quality[resolution]
|
|
3023
|
+
: XAI_IMAGE_CREDIT_PRICES.output_image.standard;
|
|
3024
|
+
const creditsRequired = Math.max(
|
|
3025
|
+
1,
|
|
3026
|
+
Math.ceil(
|
|
3027
|
+
roundUsdMicro(
|
|
3028
|
+
sourceCredits * sourceImageCount + outputCredits * outputImageCount,
|
|
3029
|
+
),
|
|
3030
|
+
),
|
|
3031
|
+
);
|
|
2563
3032
|
const defaultResolution =
|
|
2564
3033
|
modelParameters?.resolution === undefined ||
|
|
2565
3034
|
modelParameters?.resolution === null ||
|
|
@@ -2568,67 +3037,12 @@ function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
|
2568
3037
|
defaultResolution &&
|
|
2569
3038
|
outputImageCount === 1 &&
|
|
2570
3039
|
sourceImageCount === (edit ? 1 : 0);
|
|
2571
|
-
return {
|
|
2572
|
-
estimated_provider_cost_usd: roundUsdMicro(
|
|
2573
|
-
inputUsdPerImage * sourceImageCount +
|
|
2574
|
-
outputUsdPerImage * outputImageCount,
|
|
2575
|
-
),
|
|
2576
|
-
pricing_source: defaultShape ? "model_registry" : "model_parameters",
|
|
2577
|
-
pricing_confidence: "known",
|
|
2578
|
-
};
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
function createGuideCreditPricingForProviderCost(input) {
|
|
2582
|
-
const providerCostUsd = roundUsdMicro(input.providerCostUsd);
|
|
2583
|
-
const creditsRequired = Math.max(
|
|
2584
|
-
1,
|
|
2585
|
-
Math.ceil(
|
|
2586
|
-
roundUsdMicro(
|
|
2587
|
-
providerCostUsd / (1 - TARGET_GROSS_MARGIN) / CREDIT_UNIT_USD,
|
|
2588
|
-
),
|
|
2589
|
-
),
|
|
2590
|
-
);
|
|
2591
|
-
const estimatedRevenueUsd = roundUsd(creditsRequired * CREDIT_UNIT_USD);
|
|
2592
|
-
const estimatedPaymentFeeUsd = roundUsdMicro(
|
|
2593
|
-
estimatedRevenueUsd * PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2594
|
-
);
|
|
2595
|
-
const estimatedNetRevenueUsd = roundUsdMicro(
|
|
2596
|
-
estimatedRevenueUsd - estimatedPaymentFeeUsd,
|
|
2597
|
-
);
|
|
2598
|
-
const estimatedGrossMargin =
|
|
2599
|
-
estimatedRevenueUsd > 0
|
|
2600
|
-
? roundRatio(
|
|
2601
|
-
(estimatedRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2602
|
-
)
|
|
2603
|
-
: null;
|
|
2604
|
-
const estimatedFeeAdjustedMargin =
|
|
2605
|
-
estimatedRevenueUsd > 0
|
|
2606
|
-
? roundRatio(
|
|
2607
|
-
(estimatedNetRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2608
|
-
)
|
|
2609
|
-
: null;
|
|
2610
|
-
const selfFundBlockReason =
|
|
2611
|
-
estimatedNetRevenueUsd + 1e-9 < providerCostUsd
|
|
2612
|
-
? "payment_fee_margin_negative"
|
|
2613
|
-
: null;
|
|
2614
3040
|
return {
|
|
2615
3041
|
credits_required: creditsRequired,
|
|
2616
3042
|
credit_unit_usd: CREDIT_UNIT_USD,
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
estimated_gross_margin: estimatedGrossMargin,
|
|
2621
|
-
payment_fee_rate: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2622
|
-
payment_fee_model: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL,
|
|
2623
|
-
estimated_payment_fee_usd: estimatedPaymentFeeUsd,
|
|
2624
|
-
estimated_net_revenue_usd: estimatedNetRevenueUsd,
|
|
2625
|
-
estimated_fee_adjusted_margin: estimatedFeeAdjustedMargin,
|
|
2626
|
-
self_fundable: selfFundBlockReason === null,
|
|
2627
|
-
self_fund_block_reason: selfFundBlockReason,
|
|
2628
|
-
target_gross_margin: TARGET_GROSS_MARGIN,
|
|
2629
|
-
pricing_confidence: input.pricingConfidence,
|
|
2630
|
-
pricing_source: input.pricingSource,
|
|
2631
|
-
margin_model: "provider_cost_plus_margin",
|
|
3043
|
+
estimated_charge_usd: roundUsd(creditsRequired * CREDIT_UNIT_USD),
|
|
3044
|
+
pricing_confidence: "known",
|
|
3045
|
+
pricing_source: defaultShape ? "model_registry" : "model_parameters",
|
|
2632
3046
|
};
|
|
2633
3047
|
}
|
|
2634
3048
|
|
|
@@ -2653,14 +3067,47 @@ function roundUsd(value) {
|
|
|
2653
3067
|
return Math.round(value * 100) / 100;
|
|
2654
3068
|
}
|
|
2655
3069
|
|
|
2656
|
-
function roundRatio(value) {
|
|
2657
|
-
return Math.round(value * 1000) / 1000;
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
3070
|
function roundUsdMicro(value) {
|
|
2661
3071
|
return Math.round(value * 1_000_000) / 1_000_000;
|
|
2662
3072
|
}
|
|
2663
3073
|
|
|
3074
|
+
// ---------------------------------------------------------------------------
|
|
3075
|
+
// How the guide reads a model field. Read this before adding an accessor.
|
|
3076
|
+
//
|
|
3077
|
+
// There is NO normalizer. `createGuide` hands the rows from `GET /v1/models`
|
|
3078
|
+
// to these accessors verbatim -- `withModelSummary` is the `models list`
|
|
3079
|
+
// command's compactor and never runs on this path. So an accessor here has to
|
|
3080
|
+
// cope with BOTH published shapes itself:
|
|
3081
|
+
//
|
|
3082
|
+
// GET /v1/models (compact list) flat: model_execution_status,
|
|
3083
|
+
// credits_required, estimated_usd_per_image,
|
|
3084
|
+
// max_outputs_per_request, artifact_storage.
|
|
3085
|
+
// NO `media`, `execution`, or `economics`.
|
|
3086
|
+
// GET /v1/models/:id (detail) nested under execution/economics/media.
|
|
3087
|
+
//
|
|
3088
|
+
// The convention is therefore NESTED-OR-FLAT: try the detail shape, fall back
|
|
3089
|
+
// to the flat field the list actually publishes. That convention is the whole
|
|
3090
|
+
// safety property, and #2265 was one accessor quietly breaking it.
|
|
3091
|
+
// `createGuideSelectedModelRequiresInputImage` read
|
|
3092
|
+
// `media.input.images.required` with a "fallback" of `accepts_input_images` --
|
|
3093
|
+
// a field the API has never published -- so against a real list row it had no
|
|
3094
|
+
// fallback at all, and it failed closed.
|
|
3095
|
+
//
|
|
3096
|
+
// guideModelExecutionStatus execution.model_execution_status -> flat
|
|
3097
|
+
// createGuideModelCreditPricing economics.credit_pricing -> flat
|
|
3098
|
+
// guideBudgetUsdForModel economics.estimated_usd_per_image -> flat
|
|
3099
|
+
// guideModelAspectRatioValues media.input.aspect_ratios.values -> none;
|
|
3100
|
+
// fails open and is hydrated from the detail
|
|
3101
|
+
// endpoint on demand (see the note there)
|
|
3102
|
+
// ...SelectedModelRequiresInputImage reads `supports`, published on the list
|
|
3103
|
+
//
|
|
3104
|
+
// `max_outputs_per_request` and `artifact_storage` are published flat as well,
|
|
3105
|
+
// but nothing on the guide path reads them. Their only nested reads live in
|
|
3106
|
+
// `modelSummaryRow`/`modelSummaryTaskTags`, which `withModelSummary` skips
|
|
3107
|
+
// outright for a payload already marked `compact_model_summary` -- which the
|
|
3108
|
+
// hosted list is. Those eight `media.*` reads therefore never see a list row.
|
|
3109
|
+
// ---------------------------------------------------------------------------
|
|
3110
|
+
|
|
2664
3111
|
function guideModelExecutionStatus(model) {
|
|
2665
3112
|
if (
|
|
2666
3113
|
isRecord(model?.execution) &&
|
|
@@ -2682,16 +3129,8 @@ function createGuideModelCreditPricing(model) {
|
|
|
2682
3129
|
}
|
|
2683
3130
|
return {
|
|
2684
3131
|
credits_required: model.credits_required,
|
|
2685
|
-
estimated_revenue_usd:
|
|
2686
|
-
typeof model.credits_required === "number"
|
|
2687
|
-
? model.credits_required * CREDIT_UNIT_USD
|
|
2688
|
-
: null,
|
|
2689
|
-
estimated_provider_cost_usd:
|
|
2690
|
-
typeof model?.estimated_usd_per_image === "number"
|
|
2691
|
-
? model.estimated_usd_per_image
|
|
2692
|
-
: null,
|
|
2693
|
-
fallback_provider_cost_usd: null,
|
|
2694
3132
|
credit_unit_usd: CREDIT_UNIT_USD,
|
|
3133
|
+
estimated_charge_usd: roundUsd(model.credits_required * CREDIT_UNIT_USD),
|
|
2695
3134
|
pricing_confidence:
|
|
2696
3135
|
typeof model?.pricing_confidence === "string"
|
|
2697
3136
|
? model.pricing_confidence
|
|
@@ -2726,23 +3165,174 @@ function createGuideImpliesVideo(input) {
|
|
|
2726
3165
|
);
|
|
2727
3166
|
}
|
|
2728
3167
|
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
3168
|
+
// Extract an explicitly requested aspect ratio from a create/edit brief.
|
|
3169
|
+
//
|
|
3170
|
+
// Precedence:
|
|
3171
|
+
// 1. An explicit `W:H` token from EXPLICIT_ASPECT_RATIOS wins over any
|
|
3172
|
+
// orientation word -- it is the most specific statement of intent.
|
|
3173
|
+
// 2. Otherwise a bound orientation phrase is used.
|
|
3174
|
+
//
|
|
3175
|
+
// Conflicts resolve to null (no request) rather than to a guess: two different
|
|
3176
|
+
// explicit ratios, or two orientation phrases implying different shapes, mean
|
|
3177
|
+
// the brief is ambiguous and the guide should not pick a side.
|
|
3178
|
+
function classifyRequestedAspectRatio(input) {
|
|
3179
|
+
const text = `${input?.intent ?? ""} ${input?.prompt ?? ""}`.trim();
|
|
3180
|
+
if (text.length === 0) {
|
|
3181
|
+
return { aspectRatio: null, signal: null };
|
|
2732
3182
|
}
|
|
3183
|
+
|
|
3184
|
+
const ratioTokenIsSuppressed = RATIO_SUPPRESSING_CONTEXT.some((re) =>
|
|
3185
|
+
re.test(text),
|
|
3186
|
+
);
|
|
3187
|
+
|
|
3188
|
+
const explicit = new Set();
|
|
3189
|
+
if (!ratioTokenIsSuppressed) {
|
|
3190
|
+
for (const match of text.matchAll(ASPECT_RATIO_TOKEN_RE)) {
|
|
3191
|
+
const token = `${match[1]}:${match[2]}`;
|
|
3192
|
+
if (EXPLICIT_ASPECT_RATIOS.includes(token)) {
|
|
3193
|
+
explicit.add(token);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
if (explicit.size === 1) {
|
|
3198
|
+
const [only] = [...explicit];
|
|
3199
|
+
return { aspectRatio: only ?? null, signal: "explicit_ratio_token" };
|
|
3200
|
+
}
|
|
3201
|
+
if (explicit.size > 1) {
|
|
3202
|
+
// Contradictory explicit ratios: refuse to guess.
|
|
3203
|
+
return { aspectRatio: null, signal: null };
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
const implied = new Set();
|
|
3207
|
+
for (const rule of ORIENTATION_RULES) {
|
|
3208
|
+
if (rule.re.test(text)) {
|
|
3209
|
+
implied.add(rule.ratio);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (implied.size === 1) {
|
|
3213
|
+
const [only] = [...implied];
|
|
3214
|
+
return { aspectRatio: only ?? null, signal: "orientation_phrase" };
|
|
3215
|
+
}
|
|
3216
|
+
return { aspectRatio: null, signal: null };
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
function requestedAspectRatioFromBrief(input) {
|
|
3220
|
+
return classifyRequestedAspectRatio(input).aspectRatio;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// The normalized ratios a model publishes, or null when the payload does not
|
|
3224
|
+
// say. The hosted `/v1/models` list is a compact summary and omits the `media`
|
|
3225
|
+
// block entirely, so "not published" is the common case here and must be kept
|
|
3226
|
+
// distinct from "published and does not include this ratio".
|
|
3227
|
+
//
|
|
3228
|
+
// #2265, worth stating because the next reader will assume otherwise: this is
|
|
3229
|
+
// ONE OF EXACTLY TWO `media.*` reads that run over the LIST payload, and the
|
|
3230
|
+
// two behave OPPOSITELY. `GET /v1/models/:id` does publish `media`; the compact
|
|
3231
|
+
// list does not. So a `media.*` read is only safe here if it either fails open
|
|
3232
|
+
// or arranges to be hydrated from the detail endpoint first.
|
|
3233
|
+
//
|
|
3234
|
+
// - This one FAILS OPEN. Unknown never disqualifies a model, so an unhydrated
|
|
3235
|
+
// read costs a hint, not a decision. That is why capability routing kept
|
|
3236
|
+
// working in production while `edit --guide` was dead.
|
|
3237
|
+
// - `createGuideSelectedModelRequiresInputImage` used to FAIL CLOSED: no
|
|
3238
|
+
// `media` meant no model could prove it accepts an input image, so `edit
|
|
3239
|
+
// --guide` selected nothing on every real call. It no longer reads `media`
|
|
3240
|
+
// at all -- see the note there.
|
|
3241
|
+
//
|
|
3242
|
+
// Failing open is not the same as being correct. `guideModelSuggestedAspectRatio`
|
|
3243
|
+
// consumes this to REPORT a value rather than to gate one, so before the probe
|
|
3244
|
+
// was extended to video picks it published `suggested_aspect_ratio: null` on
|
|
3245
|
+
// every video brief that did not state a ratio -- silently, since no blocker
|
|
3246
|
+
// fires. Fail-open degrades quietly; that is its failure mode, not its absence.
|
|
3247
|
+
function guideModelAspectRatioValues(model) {
|
|
2733
3248
|
const values = Array.isArray(model?.media?.input?.aspect_ratios?.values)
|
|
2734
3249
|
? model.media.input.aspect_ratios.values
|
|
2735
3250
|
: model?.aspect_ratios;
|
|
2736
|
-
|
|
3251
|
+
return Array.isArray(values) ? values : null;
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
// Tri-state on purpose: true (published and supported), false (published and
|
|
3255
|
+
// NOT supported), null (capability unknown). Unknown must never be read as a
|
|
3256
|
+
// denial -- doing so would make every ratio request in production claim "no
|
|
3257
|
+
// available model supports this", which is worse than the bug being fixed.
|
|
3258
|
+
function modelAspectRatioSupport(model, aspectRatio) {
|
|
3259
|
+
if (aspectRatio === null || aspectRatio === undefined) {
|
|
3260
|
+
return true;
|
|
3261
|
+
}
|
|
3262
|
+
const values = guideModelAspectRatioValues(model);
|
|
3263
|
+
return values === null ? null : values.includes(aspectRatio);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
function modelCannotExpressAspectRatio(model, aspectRatio) {
|
|
3267
|
+
return modelAspectRatioSupport(model, aspectRatio) === false;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// Honest capability note for the case where an explicitly requested aspect
|
|
3271
|
+
// ratio made the guide skip its normal top pick. Name the capability that
|
|
3272
|
+
// forced the hop so agents learn which model could not express the shape.
|
|
3273
|
+
function aspectRatioRoutedCreateSelectionReason(input) {
|
|
3274
|
+
return `guide selected a create model that supports the requested ${input.aspectRatio} aspect ratio because ${input.skippedModelId} does not expose ${input.aspectRatio}`;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// Fail-honestly note for the case where NO available model can express the
|
|
3278
|
+
// requested ratio. The guide keeps its normal selection and says so rather than
|
|
3279
|
+
// silently dropping the ratio, which is the exact failure this fixes.
|
|
3280
|
+
function aspectRatioUnsupportedWarning(input) {
|
|
3281
|
+
return `The requested ${input.aspectRatio} aspect ratio is not supported by any available model, so data.next_command omits --aspect-ratio and ${input.modelId} will use its own default shape. Run the model_inspection_command to see the ratios it does support.`;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function createGuideSuggestedAspectRatio(input) {
|
|
3285
|
+
const model = input?.model ?? null;
|
|
3286
|
+
const requestedAspectRatio = input?.requestedAspectRatio ?? null;
|
|
3287
|
+
if (model === null) {
|
|
3288
|
+
return null;
|
|
3289
|
+
}
|
|
3290
|
+
// An explicit request wins for every modality, but not when the selected
|
|
3291
|
+
// model is KNOWN not to express it. In that case return null so
|
|
3292
|
+
// `next_command` stays runnable (`--aspect-ratio` would fail model
|
|
3293
|
+
// validation) and let the caller warn instead of silently pretending the
|
|
3294
|
+
// ratio was honored.
|
|
3295
|
+
if (requestedAspectRatio !== null) {
|
|
3296
|
+
return modelCannotExpressAspectRatio(model, requestedAspectRatio)
|
|
3297
|
+
? null
|
|
3298
|
+
: requestedAspectRatio;
|
|
3299
|
+
}
|
|
3300
|
+
// No explicit request: preserve the existing video-only default.
|
|
3301
|
+
if (model?.modality !== "video") {
|
|
3302
|
+
return null;
|
|
3303
|
+
}
|
|
3304
|
+
const values = guideModelAspectRatioValues(model);
|
|
3305
|
+
if (values === null) {
|
|
2737
3306
|
return null;
|
|
2738
3307
|
}
|
|
2739
3308
|
return values.includes("16:9") ? "16:9" : (values[0] ?? null);
|
|
2740
3309
|
}
|
|
2741
3310
|
|
|
3311
|
+
// #2265: this must decide from what the hosted payload actually publishes.
|
|
3312
|
+
// `GET /v1/models` returns the compact summary, which omits the `media` block
|
|
3313
|
+
// entirely (and has never published `accepts_input_images`), so gating on
|
|
3314
|
+
// `media.input.images.required` disqualified every model on every real call and
|
|
3315
|
+
// `edit --guide` could not select one, ever.
|
|
3316
|
+
//
|
|
3317
|
+
// This read FAILED CLOSED, which is why it was catastrophic rather than merely
|
|
3318
|
+
// lossy: an absent field could not prove any model accepts an input image, so
|
|
3319
|
+
// the guide selected nothing and reported `no_executable_model`. The other
|
|
3320
|
+
// list-payload `media.*` read (`guideModelAspectRatioValues`) fails open and
|
|
3321
|
+
// only loses a hint. Do not assume the two are interchangeable -- see the note
|
|
3322
|
+
// there before adding a third.
|
|
3323
|
+
//
|
|
3324
|
+
// `supports` is published and carries the same fact: it is derived from the
|
|
3325
|
+
// capability's operations (`image.edit` -> "edit", `image.variation` ->
|
|
3326
|
+
// "variation"), and both are input-image operations by construction. On the
|
|
3327
|
+
// live 220-model catalog the old conjunction and this predicate select the
|
|
3328
|
+
// identical 79 models, so the dropped conjunct was doing no work -- while
|
|
3329
|
+
// costing the mirror the ability to decide at all.
|
|
3330
|
+
//
|
|
3331
|
+
// Keep this reading only summary fields. `src/` holds strictly more model data
|
|
3332
|
+
// than the mirror can ever see; every extra field the guide reads there is a
|
|
3333
|
+
// fresh chance for the two surfaces to decide differently.
|
|
2742
3334
|
function createGuideSelectedModelRequiresInputImage(model) {
|
|
2743
3335
|
return (
|
|
2744
|
-
(model?.media?.input?.images?.required === true ||
|
|
2745
|
-
model?.accepts_input_images === true) &&
|
|
2746
3336
|
Array.isArray(model?.supports) &&
|
|
2747
3337
|
(model.supports.includes("edit") || model.supports.includes("variation"))
|
|
2748
3338
|
);
|
|
@@ -2787,9 +3377,14 @@ function createGuideSelectionReason(
|
|
|
2787
3377
|
model?.id,
|
|
2788
3378
|
)
|
|
2789
3379
|
) {
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
3380
|
+
const intentClass = createGuideIntentClass(intent);
|
|
3381
|
+
if (intentClass === "budget_draft") {
|
|
3382
|
+
return "guide selected the lowest-credit available create model for a draft/budget intent";
|
|
3383
|
+
}
|
|
3384
|
+
if (intentClass === "general") {
|
|
3385
|
+
return "guide selected a cost-efficient general-purpose create model for iteration; pass --intent final for the quality-first model";
|
|
3386
|
+
}
|
|
3387
|
+
return "guide selected the strongest currently available quality-first create model for this intent";
|
|
2793
3388
|
}
|
|
2794
3389
|
return operation === "edit"
|
|
2795
3390
|
? "guide selected the first available executable input-image edit model"
|
|
@@ -3428,12 +4023,16 @@ function createGuideBlocker(stage, input) {
|
|
|
3428
4023
|
};
|
|
3429
4024
|
}
|
|
3430
4025
|
if (stage === "no_executable_model") {
|
|
4026
|
+
// #2265: name the operation that actually failed. Saying "create" when the
|
|
4027
|
+
// caller ran `edit --guide` made the one diagnostic an agent gets point at
|
|
4028
|
+
// the wrong half of the product.
|
|
4029
|
+
const operation = input.operation === "edit" ? "edit" : "create";
|
|
3431
4030
|
return {
|
|
3432
4031
|
code: "no_executable_model",
|
|
3433
4032
|
message:
|
|
3434
4033
|
input.requestedModelId === null
|
|
3435
|
-
?
|
|
3436
|
-
: `Requested model is not currently an available executable
|
|
4034
|
+
? `No available executable ${operation} model was found in the public registry.`
|
|
4035
|
+
: `Requested model is not currently an available executable ${operation} model: ${input.requestedModelId}`,
|
|
3437
4036
|
};
|
|
3438
4037
|
}
|
|
3439
4038
|
if (stage === "auth_required") {
|
|
@@ -4072,14 +4671,22 @@ function createGuideWarning(stage, input) {
|
|
|
4072
4671
|
warning,
|
|
4073
4672
|
};
|
|
4074
4673
|
}
|
|
4674
|
+
// #2204-#2216: name the field this response actually carries. The compact
|
|
4675
|
+
// ready payload drops the `recommended_no_spend_command` alias, so it points
|
|
4676
|
+
// at the base field instead; blocked and `--explain` payloads keep the alias
|
|
4677
|
+
// and keep naming it. Either way
|
|
4678
|
+
// `data[data.guide_warning.recommended_command_field]` resolves.
|
|
4679
|
+
const noSpendField =
|
|
4680
|
+
input.readyCompact === true
|
|
4681
|
+
? "no_spend_next_command"
|
|
4682
|
+
: "recommended_no_spend_command";
|
|
4075
4683
|
return {
|
|
4076
4684
|
...base,
|
|
4077
4685
|
next_command_safety: "live_media_create_credit_debit",
|
|
4078
4686
|
no_spend_safe: false,
|
|
4079
4687
|
spend_required: true,
|
|
4080
|
-
recommended_command_field:
|
|
4081
|
-
warning:
|
|
4082
|
-
"data.next_command is a live media create that can call a provider, debit credits, and create media. Run it only when media spend is allowed; otherwise run data.recommended_no_spend_command.",
|
|
4688
|
+
recommended_command_field: noSpendField,
|
|
4689
|
+
warning: `data.next_command is a live media create that can call a provider, debit credits, and create media. Run it only when media spend is allowed; otherwise run data.${noSpendField}.`,
|
|
4083
4690
|
};
|
|
4084
4691
|
}
|
|
4085
4692
|
|
|
@@ -4169,6 +4776,7 @@ function createGuideNextCommand(stage, input) {
|
|
|
4169
4776
|
prompt: input.prompt,
|
|
4170
4777
|
inputReference: input.inputReference,
|
|
4171
4778
|
budgetGuard: input.budgetGuard,
|
|
4779
|
+
aspectRatio: input.aspectRatio,
|
|
4172
4780
|
modelParametersJson: input.modelParametersJson,
|
|
4173
4781
|
dryRun: false,
|
|
4174
4782
|
idempotencyKey: `edit-guide-${Date.now()}-${randomBytes(4).toString("hex")}`,
|
|
@@ -4228,6 +4836,7 @@ function createGuideEscapeHatches(input) {
|
|
|
4228
4836
|
prompt: input.prompt,
|
|
4229
4837
|
inputReference: input.inputReference,
|
|
4230
4838
|
budgetGuard: input.budgetGuard,
|
|
4839
|
+
aspectRatio: input.aspectRatio,
|
|
4231
4840
|
modelParametersJson: input.modelParametersJson,
|
|
4232
4841
|
dryRun: true,
|
|
4233
4842
|
apiBaseUrl: input.apiBaseUrl,
|
|
@@ -4257,7 +4866,7 @@ function renderGuideCommand(
|
|
|
4257
4866
|
const operation = options.operation ?? "create";
|
|
4258
4867
|
return [
|
|
4259
4868
|
commandPrefix,
|
|
4260
|
-
`${operation} --guide --prompt`,
|
|
4869
|
+
`${operation} --guide${options.explain === true ? " --explain" : ""} --prompt`,
|
|
4261
4870
|
shellQuote(prompt),
|
|
4262
4871
|
...(operation === "edit" &&
|
|
4263
4872
|
typeof options.inputReference === "string" &&
|
|
@@ -4363,6 +4972,9 @@ function renderInputImageGuideCommand(input) {
|
|
|
4363
4972
|
"--model",
|
|
4364
4973
|
shellQuote(input.modelId),
|
|
4365
4974
|
...(promptless ? [] : ["--prompt", shellQuote(input.prompt)]),
|
|
4975
|
+
...(input.aspectRatio === null || input.aspectRatio === undefined
|
|
4976
|
+
? []
|
|
4977
|
+
: ["--aspect-ratio", shellQuote(input.aspectRatio)]),
|
|
4366
4978
|
"--max-estimated-usd-per-image",
|
|
4367
4979
|
shellQuote(formatUsd(input.budgetGuard)),
|
|
4368
4980
|
...(input.modelParametersJson === null ||
|
|
@@ -4517,6 +5129,12 @@ async function create(argv) {
|
|
|
4517
5129
|
"create --no-spend is a guide output mode; combine it with --guide, or use create --dry-run for a no-spend planned job",
|
|
4518
5130
|
);
|
|
4519
5131
|
}
|
|
5132
|
+
if (flagBool(args, "explain")) {
|
|
5133
|
+
return invalid(
|
|
5134
|
+
"luxin create",
|
|
5135
|
+
"create --explain is a guide output mode; combine it with --guide to return every guide field",
|
|
5136
|
+
);
|
|
5137
|
+
}
|
|
4520
5138
|
const prompt = await promptValue(args);
|
|
4521
5139
|
if (!prompt.ok) {
|
|
4522
5140
|
return prompt.result;
|
|
@@ -4670,6 +5288,12 @@ async function edit(argv) {
|
|
|
4670
5288
|
"edit --no-spend is a guide output mode; combine it with --guide, or use edit --dry-run for a no-spend planned job",
|
|
4671
5289
|
);
|
|
4672
5290
|
}
|
|
5291
|
+
if (flagBool(args, "explain")) {
|
|
5292
|
+
return invalid(
|
|
5293
|
+
"luxin edit",
|
|
5294
|
+
"edit --explain is a guide output mode; combine it with --guide to return every guide field",
|
|
5295
|
+
);
|
|
5296
|
+
}
|
|
4673
5297
|
if (input === undefined) {
|
|
4674
5298
|
return invalid(
|
|
4675
5299
|
"luxin edit",
|
|
@@ -4789,7 +5413,7 @@ async function assets(argv) {
|
|
|
4789
5413
|
if (assetId === null) {
|
|
4790
5414
|
return invalid(
|
|
4791
5415
|
"luxin assets",
|
|
4792
|
-
|
|
5416
|
+
`assets currently supports Luxin asset ids and ${ACCEPTED_PUBLIC_MEDIA_HOSTS.join(" or ")} URLs`,
|
|
4793
5417
|
);
|
|
4794
5418
|
}
|
|
4795
5419
|
const token = await resolveToken(args);
|
|
@@ -7224,7 +7848,7 @@ function assetIdFromReference(reference) {
|
|
|
7224
7848
|
const url = new URL(reference);
|
|
7225
7849
|
if (
|
|
7226
7850
|
url.protocol !== "https:" ||
|
|
7227
|
-
url.hostname
|
|
7851
|
+
!ACCEPTED_PUBLIC_MEDIA_HOSTS.includes(url.hostname.toLowerCase()) ||
|
|
7228
7852
|
!url.pathname.startsWith("/a/")
|
|
7229
7853
|
) {
|
|
7230
7854
|
return null;
|