luxin-cli 0.2.2 → 0.2.4
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 +129 -0
- package/bin/luxin.mjs +775 -193
- package/cli.md +160 -50
- package/commands.json +2 -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 +160 -50
- package/skills/luxin/references/commands.json +2 -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.4";
|
|
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,
|
|
@@ -552,6 +709,8 @@ function commandHelpByKey(key) {
|
|
|
552
709
|
docs_url: "https://luxin.sh/cli.md#luxin-create",
|
|
553
710
|
optional_flags: [
|
|
554
711
|
"--guide",
|
|
712
|
+
"--no-spend",
|
|
713
|
+
"--explain",
|
|
555
714
|
"--dry-run",
|
|
556
715
|
"--model",
|
|
557
716
|
"--aspect-ratio",
|
|
@@ -1752,16 +1911,61 @@ async function createGuide(args, options = {}) {
|
|
|
1752
1911
|
return token.result;
|
|
1753
1912
|
}
|
|
1754
1913
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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, {
|
|
1758
1929
|
operation: guideOperation,
|
|
1759
1930
|
prompt: trimmedPrompt,
|
|
1760
1931
|
intent: requestedIntent,
|
|
1761
1932
|
maxEstimatedUsdPerImage,
|
|
1933
|
+
requestedAspectRatio: briefAspectRatio,
|
|
1934
|
+
});
|
|
1935
|
+
let selection = selectModel(listedModels);
|
|
1936
|
+
if (briefAspectRatio !== null && listedModels !== null) {
|
|
1937
|
+
// Two-pass: the first pass names the models worth asking about, then one
|
|
1938
|
+
// bounded round of detail lookups makes the second pass actually
|
|
1939
|
+
// capability-aware instead of guessing.
|
|
1940
|
+
const capabilityModels = await guideModelsWithAspectRatioCapabilities({
|
|
1941
|
+
models: listedModels,
|
|
1942
|
+
modelIds: guideAspectRatioProbeModelIds({
|
|
1943
|
+
models: listedModels,
|
|
1944
|
+
selected: selection?.model ?? null,
|
|
1945
|
+
intent: requestedIntent,
|
|
1946
|
+
}),
|
|
1947
|
+
apiBaseUrl,
|
|
1948
|
+
});
|
|
1949
|
+
if (capabilityModels !== listedModels) {
|
|
1950
|
+
selection = selectModel(capabilityModels);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
const selected = selection?.model ?? null;
|
|
1954
|
+
const selectedAspectRatio = createGuideSuggestedAspectRatio({
|
|
1955
|
+
model: selected,
|
|
1956
|
+
requestedAspectRatio: briefAspectRatio,
|
|
1957
|
+
});
|
|
1958
|
+
// The brief asked for a ratio and the selected model cannot express it, so no
|
|
1959
|
+
// model could. Say so instead of dropping it silently.
|
|
1960
|
+
const unsupportedAspectRatioWarning =
|
|
1961
|
+
briefAspectRatio !== null &&
|
|
1962
|
+
selectedAspectRatio === null &&
|
|
1963
|
+
selected !== null
|
|
1964
|
+
? aspectRatioUnsupportedWarning({
|
|
1965
|
+
aspectRatio: briefAspectRatio,
|
|
1966
|
+
modelId: selected.id,
|
|
1762
1967
|
})
|
|
1763
1968
|
: null;
|
|
1764
|
-
const selectedAspectRatio = createGuideSuggestedAspectRatio(selected);
|
|
1765
1969
|
const pricingContext = {
|
|
1766
1970
|
aspectRatio: selectedAspectRatio ?? "1:1",
|
|
1767
1971
|
outputCount: 1,
|
|
@@ -1795,25 +1999,16 @@ async function createGuide(args, options = {}) {
|
|
|
1795
1999
|
pricingContext,
|
|
1796
2000
|
)
|
|
1797
2001
|
: createGuideModelCreditPricing(selected);
|
|
1798
|
-
const providerCostEstimate =
|
|
1799
|
-
selected === null || !shouldPriceModelParameters
|
|
1800
|
-
? null
|
|
1801
|
-
: createGuideProviderCostEstimateForModel(
|
|
1802
|
-
selected,
|
|
1803
|
-
defaultedModelParameters.modelParameters,
|
|
1804
|
-
pricingContext,
|
|
1805
|
-
);
|
|
1806
2002
|
const estimatedCredits = pricing?.credits_required ?? null;
|
|
1807
|
-
|
|
1808
|
-
|
|
2003
|
+
// The guide publishes what the caller will be charged, never our provider
|
|
2004
|
+
// cost. `economics.estimated_usd_per_image` is the hosted charge per image.
|
|
2005
|
+
const estimatedDebitUsdPerImage =
|
|
2006
|
+
pricing?.estimated_charge_usd ??
|
|
1809
2007
|
selected?.economics?.estimated_usd_per_image ??
|
|
1810
|
-
pricing?.fallback_provider_cost_usd ??
|
|
1811
2008
|
(typeof selected?.estimated_usd_per_image === "number"
|
|
1812
2009
|
? selected.estimated_usd_per_image
|
|
1813
2010
|
: null) ??
|
|
1814
2011
|
null;
|
|
1815
|
-
const estimatedDebitUsdPerImage =
|
|
1816
|
-
pricing?.estimated_revenue_usd ?? estimatedProviderUsdPerImage;
|
|
1817
2012
|
const budgetGuard =
|
|
1818
2013
|
maxEstimatedUsdPerImage ??
|
|
1819
2014
|
estimatedDebitUsdPerImage ??
|
|
@@ -1866,6 +2061,14 @@ async function createGuide(args, options = {}) {
|
|
|
1866
2061
|
quota,
|
|
1867
2062
|
estimatedCredits,
|
|
1868
2063
|
});
|
|
2064
|
+
// #2204-#2216: `--explain` opts back into the complete payload in every
|
|
2065
|
+
// stage; the compact default is for the agent that just wants the decision.
|
|
2066
|
+
// The payment/self-fund/recovery apparatus is correct when the guide is
|
|
2067
|
+
// blocked and noise when it is not, so shape on that state rather than on a
|
|
2068
|
+
// flag the caller has to know about.
|
|
2069
|
+
const explainOutputMode = flagBool(args, "explain");
|
|
2070
|
+
const readyCompact =
|
|
2071
|
+
stage === "ready_to_create" && blocker === null && !explainOutputMode;
|
|
1869
2072
|
const nextCommand = createGuideNextCommand(stage, {
|
|
1870
2073
|
prompt: trimmedPrompt,
|
|
1871
2074
|
selected,
|
|
@@ -1924,11 +2127,21 @@ async function createGuide(args, options = {}) {
|
|
|
1924
2127
|
noSpendNextCommandLabel,
|
|
1925
2128
|
noSpendNextCommandEffect,
|
|
1926
2129
|
});
|
|
1927
|
-
const
|
|
2130
|
+
const baseGuideWarning = createGuideWarning(stage, {
|
|
1928
2131
|
nextCommandEffect,
|
|
1929
2132
|
paymentSummary,
|
|
1930
2133
|
nextCommandCopyRunnable,
|
|
2134
|
+
readyCompact,
|
|
1931
2135
|
});
|
|
2136
|
+
// Append, never replace: the stage warning carries the spend-safety contract
|
|
2137
|
+
// agents rely on, and the ratio note is additive context.
|
|
2138
|
+
const guideWarning =
|
|
2139
|
+
unsupportedAspectRatioWarning === null
|
|
2140
|
+
? baseGuideWarning
|
|
2141
|
+
: {
|
|
2142
|
+
...baseGuideWarning,
|
|
2143
|
+
warning: `${baseGuideWarning.warning} ${unsupportedAspectRatioWarning}`,
|
|
2144
|
+
};
|
|
1932
2145
|
const quotaTopUp = createGuideQuotaTopUp(
|
|
1933
2146
|
quota?.envelope.data?.top_up ?? null,
|
|
1934
2147
|
);
|
|
@@ -1984,15 +2197,117 @@ async function createGuide(args, options = {}) {
|
|
|
1984
2197
|
paymentSummary,
|
|
1985
2198
|
selfFundPreparation,
|
|
1986
2199
|
);
|
|
2200
|
+
// #2194: no-spend output mode. Omit the live self-fund and payment command
|
|
2201
|
+
// templates when the agent opts into a no-spend/dry-run study so the safe
|
|
2202
|
+
// dry-run is not buried under quote/buy/payment workflow commands. Retain the
|
|
2203
|
+
// dry-run command and the mutation proof, and keep the self-fund path
|
|
2204
|
+
// discoverable via self_fund_discovery instead of dominating the response.
|
|
2205
|
+
const noSpendOutputMode = flagBool(args, "no-spend");
|
|
2206
|
+
// Keep the pre-wall self-fund nudge whenever it is actually recommended.
|
|
2207
|
+
const selfFundRecommended = selfFundPreparation?.recommended === true;
|
|
2208
|
+
const paymentSummaryOutput =
|
|
2209
|
+
noSpendOutputMode || readyCompact
|
|
2210
|
+
? {
|
|
2211
|
+
...paymentSummary,
|
|
2212
|
+
suggested_commands: [],
|
|
2213
|
+
...(readyCompact ? { preferred_method_summary: null } : {}),
|
|
2214
|
+
}
|
|
2215
|
+
: paymentSummary;
|
|
2216
|
+
const selfFundHandoffOutput = noSpendOutputMode ? null : selfFundHandoff;
|
|
2217
|
+
const selfFundPreparationOutput = noSpendOutputMode
|
|
2218
|
+
? null
|
|
2219
|
+
: selfFundPreparation;
|
|
2220
|
+
const selfFundNextCommandOutput = noSpendOutputMode
|
|
2221
|
+
? null
|
|
2222
|
+
: selfFundNextCommand;
|
|
2223
|
+
const selfFundNextCommandLabelOutput = noSpendOutputMode
|
|
2224
|
+
? null
|
|
2225
|
+
: selfFundNextCommandLabel;
|
|
2226
|
+
const noSpendOutputModeData = noSpendOutputMode
|
|
2227
|
+
? {
|
|
2228
|
+
enabled: true,
|
|
2229
|
+
suppressed_fields: [
|
|
2230
|
+
"self_fund_next_command",
|
|
2231
|
+
"self_fund_next_command_label",
|
|
2232
|
+
"self_fund_handoff",
|
|
2233
|
+
"self_fund_preparation",
|
|
2234
|
+
"checks.payments.suggested_commands",
|
|
2235
|
+
],
|
|
2236
|
+
self_fund_discovery: {
|
|
2237
|
+
rerun_with_funding_command: renderGuideCommand(
|
|
2238
|
+
trimmedPrompt.length > 0 ? trimmedPrompt : "PROMPT",
|
|
2239
|
+
explicitApiBaseUrl(args),
|
|
2240
|
+
guideCommandPrefix,
|
|
2241
|
+
{
|
|
2242
|
+
operation: guideOperation,
|
|
2243
|
+
inputReference: options.inputReference,
|
|
2244
|
+
modelId: requestedModelId,
|
|
2245
|
+
providerId: requestedProviderId,
|
|
2246
|
+
intent: requestedIntentFlag,
|
|
2247
|
+
maxEstimatedUsdPerImage,
|
|
2248
|
+
modelParametersJson: requestedModelParametersJson,
|
|
2249
|
+
},
|
|
2250
|
+
),
|
|
2251
|
+
inspect_methods_command: escapeHatches.payment_methods,
|
|
2252
|
+
note: noSpendDiscoveryNote(readyCompact),
|
|
2253
|
+
},
|
|
2254
|
+
}
|
|
2255
|
+
: {
|
|
2256
|
+
enabled: false,
|
|
2257
|
+
suppressed_fields: [],
|
|
2258
|
+
self_fund_discovery: null,
|
|
2259
|
+
};
|
|
1987
2260
|
const guideRecovery = createGuideRecovery(stage, {
|
|
1988
2261
|
blocker,
|
|
1989
2262
|
nextCommand,
|
|
1990
2263
|
noSpendNextCommand,
|
|
1991
2264
|
afterNext,
|
|
1992
2265
|
escapeHatches,
|
|
1993
|
-
selfFundNextCommand,
|
|
2266
|
+
selfFundNextCommand: selfFundNextCommandOutput,
|
|
1994
2267
|
});
|
|
1995
|
-
|
|
2268
|
+
// #2204-#2216: fields the compact ready payload drops. `self_fund_*` only
|
|
2269
|
+
// joins the list when the pre-wall top-up is not recommended.
|
|
2270
|
+
const compactOmittedFields = readyCompact
|
|
2271
|
+
? COMPACT_OMITTABLE_GUIDE_FIELDS.filter(
|
|
2272
|
+
(field) =>
|
|
2273
|
+
selfFundRecommended === false ||
|
|
2274
|
+
!COMPACT_SELF_FUND_GUIDE_FIELDS.has(field),
|
|
2275
|
+
)
|
|
2276
|
+
: [];
|
|
2277
|
+
const compactCompactedFields = readyCompact
|
|
2278
|
+
? [
|
|
2279
|
+
"checks.payments.preferred_method_summary",
|
|
2280
|
+
"checks.payments.suggested_commands",
|
|
2281
|
+
"checks.quota.top_up",
|
|
2282
|
+
]
|
|
2283
|
+
: [];
|
|
2284
|
+
const outputModeData = {
|
|
2285
|
+
schema: "image-skill.guide-output-mode.v1",
|
|
2286
|
+
mode: readyCompact ? "ready_compact" : "full",
|
|
2287
|
+
reason: readyCompact
|
|
2288
|
+
? "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."
|
|
2289
|
+
: explainOutputMode
|
|
2290
|
+
? "--explain returns every guide field in every stage."
|
|
2291
|
+
: "the guide is not at an unblocked ready_to_create stage, so every field is returned.",
|
|
2292
|
+
omitted_fields: compactOmittedFields,
|
|
2293
|
+
compacted_fields: compactCompactedFields,
|
|
2294
|
+
full_output_command: renderGuideCommand(
|
|
2295
|
+
trimmedPrompt.length > 0 ? trimmedPrompt : "PROMPT",
|
|
2296
|
+
explicitApiBaseUrl(args),
|
|
2297
|
+
guideCommandPrefix,
|
|
2298
|
+
{
|
|
2299
|
+
operation: guideOperation,
|
|
2300
|
+
inputReference: options.inputReference,
|
|
2301
|
+
modelId: requestedModelId,
|
|
2302
|
+
providerId: requestedProviderId,
|
|
2303
|
+
intent: requestedIntentFlag,
|
|
2304
|
+
maxEstimatedUsdPerImage,
|
|
2305
|
+
modelParametersJson: requestedModelParametersJson,
|
|
2306
|
+
explain: true,
|
|
2307
|
+
},
|
|
2308
|
+
),
|
|
2309
|
+
};
|
|
2310
|
+
const guideData = {
|
|
1996
2311
|
schema:
|
|
1997
2312
|
guideOperation === "edit"
|
|
1998
2313
|
? "image-skill.edit-guide.v1"
|
|
@@ -2032,7 +2347,7 @@ async function createGuide(args, options = {}) {
|
|
|
2032
2347
|
required_credits: estimatedCredits,
|
|
2033
2348
|
daily_jobs_remaining:
|
|
2034
2349
|
quota?.envelope.data?.daily_jobs?.remaining ?? null,
|
|
2035
|
-
top_up: quotaTopUp,
|
|
2350
|
+
top_up: readyCompact ? null : quotaTopUp,
|
|
2036
2351
|
reason:
|
|
2037
2352
|
quota === null
|
|
2038
2353
|
? "auth_required"
|
|
@@ -2040,7 +2355,7 @@ async function createGuide(args, options = {}) {
|
|
|
2040
2355
|
? null
|
|
2041
2356
|
: (quota.envelope.error?.code ?? "quota_unavailable"),
|
|
2042
2357
|
},
|
|
2043
|
-
payments:
|
|
2358
|
+
payments: paymentSummaryOutput,
|
|
2044
2359
|
},
|
|
2045
2360
|
selection:
|
|
2046
2361
|
selected === null
|
|
@@ -2055,20 +2370,28 @@ async function createGuide(args, options = {}) {
|
|
|
2055
2370
|
modality: selected.modality ?? null,
|
|
2056
2371
|
suggested_aspect_ratio: selectedAspectRatio,
|
|
2057
2372
|
reason:
|
|
2058
|
-
requestedModelId
|
|
2059
|
-
?
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2373
|
+
requestedModelId !== null
|
|
2374
|
+
? createGuideRequestedSelectionReason(selected)
|
|
2375
|
+
: selection?.aspectRatioRoutedFrom != null &&
|
|
2376
|
+
briefAspectRatio !== null
|
|
2377
|
+
? aspectRatioRoutedCreateSelectionReason({
|
|
2378
|
+
aspectRatio: briefAspectRatio,
|
|
2379
|
+
skippedModelId: selection.aspectRatioRoutedFrom,
|
|
2380
|
+
})
|
|
2381
|
+
: selection?.aspectRatioCapableFallback === true &&
|
|
2382
|
+
briefAspectRatio !== null
|
|
2383
|
+
? `guide selected an available model that supports the requested ${briefAspectRatio} aspect ratio`
|
|
2384
|
+
: createGuideSelectionReason(
|
|
2385
|
+
selected,
|
|
2386
|
+
trimmedPrompt,
|
|
2387
|
+
requestedIntent,
|
|
2388
|
+
guideOperation,
|
|
2389
|
+
),
|
|
2066
2390
|
},
|
|
2067
2391
|
cost: {
|
|
2068
2392
|
estimated_credits: estimatedCredits,
|
|
2069
2393
|
estimated_usd_per_image: estimatedDebitUsdPerImage,
|
|
2070
2394
|
estimated_debit_usd_per_image: estimatedDebitUsdPerImage,
|
|
2071
|
-
estimated_provider_usd_per_image: estimatedProviderUsdPerImage,
|
|
2072
2395
|
credit_unit_usd: pricing?.credit_unit_usd ?? CREDIT_UNIT_USD,
|
|
2073
2396
|
pricing_confidence: pricing?.pricing_confidence ?? null,
|
|
2074
2397
|
pricing_source: pricing?.pricing_source ?? null,
|
|
@@ -2090,13 +2413,15 @@ async function createGuide(args, options = {}) {
|
|
|
2090
2413
|
recommended_no_spend_command: noSpendNextCommand,
|
|
2091
2414
|
recommended_no_spend_command_label: noSpendNextCommandLabel,
|
|
2092
2415
|
recommended_no_spend_command_effect: noSpendNextCommandEffect,
|
|
2093
|
-
self_fund_next_command:
|
|
2094
|
-
self_fund_next_command_label:
|
|
2095
|
-
self_fund_handoff:
|
|
2096
|
-
self_fund_preparation:
|
|
2416
|
+
self_fund_next_command: selfFundNextCommandOutput,
|
|
2417
|
+
self_fund_next_command_label: selfFundNextCommandLabelOutput,
|
|
2418
|
+
self_fund_handoff: selfFundHandoffOutput,
|
|
2419
|
+
self_fund_preparation: selfFundPreparationOutput,
|
|
2097
2420
|
after_next: afterNext,
|
|
2098
2421
|
auth_handoff: authHandoff,
|
|
2099
2422
|
escape_hatches: escapeHatches,
|
|
2423
|
+
no_spend_output_mode: noSpendOutputModeData,
|
|
2424
|
+
output_mode: outputModeData,
|
|
2100
2425
|
mutation: {
|
|
2101
2426
|
provider_call: false,
|
|
2102
2427
|
hosted_create: false,
|
|
@@ -2105,7 +2430,11 @@ async function createGuide(args, options = {}) {
|
|
|
2105
2430
|
credit_debit: false,
|
|
2106
2431
|
media_write: false,
|
|
2107
2432
|
},
|
|
2108
|
-
}
|
|
2433
|
+
};
|
|
2434
|
+
for (const field of compactOmittedFields) {
|
|
2435
|
+
delete guideData[field];
|
|
2436
|
+
}
|
|
2437
|
+
return createGuideSuccess(command, quota?.envelope.actor ?? null, guideData);
|
|
2109
2438
|
}
|
|
2110
2439
|
|
|
2111
2440
|
function createGuideSuccess(command, actor, data) {
|
|
@@ -2122,6 +2451,12 @@ function selectCreateGuideModel(
|
|
|
2122
2451
|
prompt = "",
|
|
2123
2452
|
intent = undefined,
|
|
2124
2453
|
maxEstimatedUsdPerImage = null,
|
|
2454
|
+
/**
|
|
2455
|
+
* Aspect ratio explicitly requested by the brief, treated as a REQUIRED
|
|
2456
|
+
* capability during selection (#2213/#2203/#2209) the same way a
|
|
2457
|
+
* reproducibility need is. Null when the brief states no ratio.
|
|
2458
|
+
*/
|
|
2459
|
+
requestedAspectRatio = null,
|
|
2125
2460
|
} = {},
|
|
2126
2461
|
) {
|
|
2127
2462
|
const isExecutableCreate = (model) =>
|
|
@@ -2139,10 +2474,18 @@ function selectCreateGuideModel(
|
|
|
2139
2474
|
operation === "edit"
|
|
2140
2475
|
? isExecutableInputImageEdit(model)
|
|
2141
2476
|
: isExecutableCreate(model) || isExecutableInputImageEdit(model);
|
|
2477
|
+
const requestedRatio = requestedAspectRatio ?? null;
|
|
2478
|
+
const chosen = (
|
|
2479
|
+
model,
|
|
2480
|
+
{ aspectRatioRoutedFrom = null, aspectRatioCapableFallback = false } = {},
|
|
2481
|
+
) =>
|
|
2482
|
+
model === undefined || model === null
|
|
2483
|
+
? null
|
|
2484
|
+
: { model, aspectRatioRoutedFrom, aspectRatioCapableFallback };
|
|
2142
2485
|
if (requestedModelId !== null) {
|
|
2143
2486
|
const requested = resolveModelLookup(requestedModelId, models);
|
|
2144
2487
|
return requested !== undefined && isExecutableGuideModel(requested)
|
|
2145
|
-
? requested
|
|
2488
|
+
? chosen(requested)
|
|
2146
2489
|
: null;
|
|
2147
2490
|
}
|
|
2148
2491
|
const candidates =
|
|
@@ -2159,7 +2502,7 @@ function selectCreateGuideModel(
|
|
|
2159
2502
|
});
|
|
2160
2503
|
const threeDimensional = eligible3d[0];
|
|
2161
2504
|
if (threeDimensional !== undefined) {
|
|
2162
|
-
return threeDimensional;
|
|
2505
|
+
return chosen(threeDimensional);
|
|
2163
2506
|
}
|
|
2164
2507
|
}
|
|
2165
2508
|
const eligible = guideCandidatesWithinBudget({
|
|
@@ -2169,23 +2512,151 @@ function selectCreateGuideModel(
|
|
|
2169
2512
|
if (createGuideImpliesAudio({ prompt, intent })) {
|
|
2170
2513
|
const audio = eligible.find((model) => model?.modality === "audio");
|
|
2171
2514
|
if (audio !== undefined) {
|
|
2172
|
-
return audio;
|
|
2515
|
+
return chosen(audio);
|
|
2173
2516
|
}
|
|
2174
2517
|
}
|
|
2175
2518
|
if (createGuideImpliesVideo({ prompt, intent })) {
|
|
2176
|
-
const
|
|
2519
|
+
const videoCandidates = eligible.filter(
|
|
2520
|
+
(model) => model?.modality === "video",
|
|
2521
|
+
);
|
|
2522
|
+
const video =
|
|
2523
|
+
videoCandidates.find(
|
|
2524
|
+
(model) => !modelCannotExpressAspectRatio(model, requestedRatio),
|
|
2525
|
+
) ?? videoCandidates[0];
|
|
2177
2526
|
if (video !== undefined) {
|
|
2178
|
-
return video;
|
|
2527
|
+
return chosen(video);
|
|
2179
2528
|
}
|
|
2180
2529
|
}
|
|
2181
2530
|
const intentClass = createGuideIntentClass(intent);
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2531
|
+
const preferredCandidates = preferredCreateGuideModelIds(intentClass)
|
|
2532
|
+
.map((modelId) => eligible.find((model) => model?.id === modelId))
|
|
2533
|
+
.filter((model) => model !== undefined);
|
|
2534
|
+
const [topPreferred] = preferredCandidates;
|
|
2535
|
+
|
|
2536
|
+
// An explicitly requested aspect ratio is a REQUIRED capability, so walk the
|
|
2537
|
+
// curated preference list in strength order but skip models that are KNOWN
|
|
2538
|
+
// not to express the ratio through the normalized `--aspect-ratio` control
|
|
2539
|
+
// (#2213/#2203/#2209). Unknown capability is not a denial: see
|
|
2540
|
+
// `modelAspectRatioSupport`.
|
|
2541
|
+
const preferredSupportingRatio = preferredCandidates.find(
|
|
2542
|
+
(model) => !modelCannotExpressAspectRatio(model, requestedRatio),
|
|
2543
|
+
);
|
|
2544
|
+
if (preferredSupportingRatio !== undefined) {
|
|
2545
|
+
const routedForAspectRatio =
|
|
2546
|
+
requestedRatio !== null &&
|
|
2547
|
+
topPreferred !== undefined &&
|
|
2548
|
+
topPreferred.id !== preferredSupportingRatio.id;
|
|
2549
|
+
return chosen(preferredSupportingRatio, {
|
|
2550
|
+
aspectRatioRoutedFrom: routedForAspectRatio ? topPreferred.id : null,
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// No curated model can express the ratio: fall out to the strongest
|
|
2555
|
+
// remaining executable candidate that provably can, before degrading to the
|
|
2556
|
+
// ratio-blind fallback.
|
|
2557
|
+
if (requestedRatio !== null) {
|
|
2558
|
+
const capable = eligible.find(
|
|
2559
|
+
(model) => modelAspectRatioSupport(model, requestedRatio) === true,
|
|
2560
|
+
);
|
|
2561
|
+
if (capable !== undefined) {
|
|
2562
|
+
return chosen(capable, {
|
|
2563
|
+
aspectRatioRoutedFrom: topPreferred?.id ?? null,
|
|
2564
|
+
aspectRatioCapableFallback: topPreferred === undefined,
|
|
2565
|
+
});
|
|
2186
2566
|
}
|
|
2187
2567
|
}
|
|
2188
|
-
|
|
2568
|
+
|
|
2569
|
+
// Nothing can express the requested ratio (or none was requested): keep the
|
|
2570
|
+
// ordinary selection exactly. The caller emits an honest guide_warning rather
|
|
2571
|
+
// than letting the ratio disappear silently.
|
|
2572
|
+
if (topPreferred !== undefined) {
|
|
2573
|
+
return chosen(topPreferred);
|
|
2574
|
+
}
|
|
2575
|
+
return chosen(eligible[0]);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// The hosted `/v1/models` list is a compact summary and does not publish
|
|
2579
|
+
// `media.input.aspect_ratios.values`, so a stated ratio cannot be checked
|
|
2580
|
+
// against it. Probe the detail endpoint for the handful of models the guide
|
|
2581
|
+
// would actually route between -- never the whole catalog, whose
|
|
2582
|
+
// `?details=true` payload is multiple megabytes.
|
|
2583
|
+
//
|
|
2584
|
+
// Fail-open: an unreachable, errored, or malformed detail response leaves the
|
|
2585
|
+
// capability UNKNOWN, which never disqualifies a model and never triggers the
|
|
2586
|
+
// unsupported-ratio warning.
|
|
2587
|
+
|
|
2588
|
+
function guideAspectRatioProbeModelIds({ models, selected, intent }) {
|
|
2589
|
+
const ids = [];
|
|
2590
|
+
if (typeof selected?.id === "string") {
|
|
2591
|
+
ids.push(selected.id);
|
|
2592
|
+
}
|
|
2593
|
+
for (const modelId of preferredCreateGuideModelIds(
|
|
2594
|
+
createGuideIntentClass(intent),
|
|
2595
|
+
)) {
|
|
2596
|
+
if (models.some((model) => model?.id === modelId)) {
|
|
2597
|
+
ids.push(modelId);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return [...new Set(ids)].slice(0, GUIDE_ASPECT_RATIO_PROBE_LIMIT);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
async function guideModelsWithAspectRatioCapabilities({
|
|
2604
|
+
models,
|
|
2605
|
+
modelIds,
|
|
2606
|
+
apiBaseUrl,
|
|
2607
|
+
}) {
|
|
2608
|
+
const missing = modelIds.filter((modelId) => {
|
|
2609
|
+
const model = models.find((candidate) => candidate?.id === modelId);
|
|
2610
|
+
return model !== undefined && guideModelAspectRatioValues(model) === null;
|
|
2611
|
+
});
|
|
2612
|
+
if (missing.length === 0) {
|
|
2613
|
+
return models;
|
|
2614
|
+
}
|
|
2615
|
+
const probes = await Promise.all(
|
|
2616
|
+
missing.map(async (modelId) => {
|
|
2617
|
+
try {
|
|
2618
|
+
const detail = await apiRequest({
|
|
2619
|
+
command: "luxin create --guide",
|
|
2620
|
+
method: "GET",
|
|
2621
|
+
apiBaseUrl,
|
|
2622
|
+
path: `/v1/models/${encodeURIComponent(modelId)}`,
|
|
2623
|
+
});
|
|
2624
|
+
const values = guideModelAspectRatioValues(
|
|
2625
|
+
detail?.envelope?.data?.model ?? null,
|
|
2626
|
+
);
|
|
2627
|
+
return values === null ? null : { modelId, values };
|
|
2628
|
+
} catch {
|
|
2629
|
+
return null;
|
|
2630
|
+
}
|
|
2631
|
+
}),
|
|
2632
|
+
);
|
|
2633
|
+
const resolved = new Map(
|
|
2634
|
+
probes
|
|
2635
|
+
.filter((probe) => probe !== null)
|
|
2636
|
+
.map((probe) => [probe.modelId, probe.values]),
|
|
2637
|
+
);
|
|
2638
|
+
if (resolved.size === 0) {
|
|
2639
|
+
return models;
|
|
2640
|
+
}
|
|
2641
|
+
return models.map((model) => {
|
|
2642
|
+
const values = resolved.get(model?.id);
|
|
2643
|
+
if (values === undefined) {
|
|
2644
|
+
return model;
|
|
2645
|
+
}
|
|
2646
|
+
return {
|
|
2647
|
+
...model,
|
|
2648
|
+
media: {
|
|
2649
|
+
...model?.media,
|
|
2650
|
+
input: {
|
|
2651
|
+
...model?.media?.input,
|
|
2652
|
+
aspect_ratios: {
|
|
2653
|
+
...model?.media?.input?.aspect_ratios,
|
|
2654
|
+
values,
|
|
2655
|
+
},
|
|
2656
|
+
},
|
|
2657
|
+
},
|
|
2658
|
+
};
|
|
2659
|
+
});
|
|
2189
2660
|
}
|
|
2190
2661
|
|
|
2191
2662
|
const MODEL_ALIAS_RULES = [
|
|
@@ -2357,29 +2828,47 @@ function createGuideIntentClass(intent) {
|
|
|
2357
2828
|
return "general";
|
|
2358
2829
|
}
|
|
2359
2830
|
|
|
2831
|
+
// Mirrors src/hosted-create-policy.ts (#2231). budget_draft is a superset of
|
|
2832
|
+
// general and both are ordered ascending by charged credits, so a
|
|
2833
|
+
// cost-minimizing intent can never select a model priced above the general
|
|
2834
|
+
// default. final keeps the quality-premium lead.
|
|
2360
2835
|
function preferredCreateGuideModelIds(intentClass) {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2836
|
+
if (intentClass === "budget_draft") {
|
|
2837
|
+
return [
|
|
2838
|
+
"fal.flux-1-schnell",
|
|
2839
|
+
"fal.flux-1-dev",
|
|
2840
|
+
"xai.grok-imagine-image",
|
|
2841
|
+
"fal.bria-text-to-image-fast",
|
|
2842
|
+
"xai.grok-imagine-image-quality",
|
|
2843
|
+
"openai.gpt-image-2",
|
|
2844
|
+
];
|
|
2845
|
+
}
|
|
2846
|
+
if (intentClass === "final") {
|
|
2847
|
+
return [
|
|
2848
|
+
"xai.grok-imagine-image-quality",
|
|
2849
|
+
"openai.gpt-image-2",
|
|
2850
|
+
"fal.flux-1-dev",
|
|
2851
|
+
"xai.grok-imagine-image",
|
|
2852
|
+
"fal.flux-1-schnell",
|
|
2853
|
+
"fal.bria-text-to-image-fast",
|
|
2854
|
+
];
|
|
2855
|
+
}
|
|
2856
|
+
return [
|
|
2857
|
+
"fal.flux-1-dev",
|
|
2858
|
+
"xai.grok-imagine-image",
|
|
2859
|
+
"xai.grok-imagine-image-quality",
|
|
2860
|
+
"openai.gpt-image-2",
|
|
2861
|
+
"fal.flux-1-schnell",
|
|
2862
|
+
"fal.bria-text-to-image-fast",
|
|
2863
|
+
];
|
|
2374
2864
|
}
|
|
2375
2865
|
|
|
2376
2866
|
function guideBudgetUsdForModel(model) {
|
|
2867
|
+
// Budget caps compare the credit debit -- what the caller pays.
|
|
2377
2868
|
const pricing = createGuideModelCreditPricing(model);
|
|
2378
2869
|
return (
|
|
2379
|
-
pricing?.
|
|
2870
|
+
pricing?.estimated_charge_usd ??
|
|
2380
2871
|
model?.economics?.estimated_usd_per_image ??
|
|
2381
|
-
pricing?.estimated_provider_cost_usd ??
|
|
2382
|
-
pricing?.fallback_provider_cost_usd ??
|
|
2383
2872
|
(typeof model?.estimated_usd_per_image === "number"
|
|
2384
2873
|
? model.estimated_usd_per_image
|
|
2385
2874
|
: null) ??
|
|
@@ -2395,7 +2884,7 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2395
2884
|
input.model?.id === "xai.grok-imagine-image-quality" &&
|
|
2396
2885
|
modelParameters.resolution === undefined
|
|
2397
2886
|
) {
|
|
2398
|
-
// Budget defaults must compare the credit debit (
|
|
2887
|
+
// Budget defaults must compare the credit debit (estimated_charge_usd),
|
|
2399
2888
|
// the same basis the hosted budget guard enforces, not the provider cost.
|
|
2400
2889
|
const twoKPricing = createGuidePricingForModel(
|
|
2401
2890
|
input.model,
|
|
@@ -2404,8 +2893,8 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2404
2893
|
);
|
|
2405
2894
|
const twoKAllowedByBudget =
|
|
2406
2895
|
input.maxEstimatedUsdPerImage === null ||
|
|
2407
|
-
typeof twoKPricing?.
|
|
2408
|
-
twoKPricing.
|
|
2896
|
+
typeof twoKPricing?.estimated_charge_usd !== "number" ||
|
|
2897
|
+
twoKPricing.estimated_charge_usd <= input.maxEstimatedUsdPerImage;
|
|
2409
2898
|
const intentClass = createGuideIntentClass(input.intent);
|
|
2410
2899
|
const resolution =
|
|
2411
2900
|
intentClass !== "budget_draft" && twoKAllowedByBudget ? "2k" : "1k";
|
|
@@ -2435,8 +2924,8 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2435
2924
|
);
|
|
2436
2925
|
const mediumAllowedByBudget =
|
|
2437
2926
|
input.maxEstimatedUsdPerImage === null ||
|
|
2438
|
-
typeof mediumPricing?.
|
|
2439
|
-
mediumPricing.
|
|
2927
|
+
typeof mediumPricing?.estimated_charge_usd !== "number" ||
|
|
2928
|
+
mediumPricing.estimated_charge_usd <= input.maxEstimatedUsdPerImage;
|
|
2440
2929
|
if (mediumAllowedByBudget) {
|
|
2441
2930
|
modelParameters.quality = "medium";
|
|
2442
2931
|
defaultsApplied.push("quality=medium");
|
|
@@ -2447,45 +2936,17 @@ function createGuideDefaultModelParameters(input) {
|
|
|
2447
2936
|
}
|
|
2448
2937
|
|
|
2449
2938
|
function createGuidePricingForModel(model, modelParameters, context = {}) {
|
|
2450
|
-
|
|
2451
|
-
model,
|
|
2452
|
-
modelParameters,
|
|
2453
|
-
context,
|
|
2454
|
-
);
|
|
2455
|
-
if (estimate.estimated_provider_cost_usd === null) {
|
|
2456
|
-
return createGuideModelCreditPricing(model);
|
|
2939
|
+
if (createGuideCanPriceModelParameters(model)) {
|
|
2940
|
+
return createGuideXaiImagePricing(model, modelParameters, context);
|
|
2457
2941
|
}
|
|
2458
|
-
return
|
|
2459
|
-
providerCostUsd: estimate.estimated_provider_cost_usd,
|
|
2460
|
-
pricingConfidence: estimate.pricing_confidence,
|
|
2461
|
-
pricingSource: estimate.pricing_source,
|
|
2462
|
-
});
|
|
2942
|
+
return createGuideModelCreditPricing(model);
|
|
2463
2943
|
}
|
|
2464
2944
|
|
|
2465
2945
|
function createGuideCanPriceModelParameters(model) {
|
|
2466
2946
|
return String(model?.id ?? "").startsWith("xai.grok-imagine-image");
|
|
2467
2947
|
}
|
|
2468
2948
|
|
|
2469
|
-
function
|
|
2470
|
-
model,
|
|
2471
|
-
modelParameters = {},
|
|
2472
|
-
context = {},
|
|
2473
|
-
) {
|
|
2474
|
-
if (String(model?.id ?? "").startsWith("xai.grok-imagine-image")) {
|
|
2475
|
-
return createGuideXaiImageCostEstimate(model, modelParameters, context);
|
|
2476
|
-
}
|
|
2477
|
-
return {
|
|
2478
|
-
estimated_provider_cost_usd:
|
|
2479
|
-
typeof model?.economics?.estimated_usd_per_image === "number"
|
|
2480
|
-
? model.economics.estimated_usd_per_image
|
|
2481
|
-
: (createGuideModelCreditPricing(model)?.estimated_provider_cost_usd ??
|
|
2482
|
-
null),
|
|
2483
|
-
pricing_source: "model_registry",
|
|
2484
|
-
pricing_confidence: "known",
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
2949
|
+
function createGuideXaiImagePricing(model, modelParameters, context) {
|
|
2489
2950
|
const modelId = String(model?.id ?? "");
|
|
2490
2951
|
const quality = modelId.includes("-quality");
|
|
2491
2952
|
const edit = modelId.endsWith("-edit");
|
|
@@ -2500,11 +2961,20 @@ function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
|
2500
2961
|
? context.referenceAssetCount
|
|
2501
2962
|
: 0;
|
|
2502
2963
|
const sourceImageCount = edit ? 1 + referenceAssetCount : 0;
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2964
|
+
const sourceCredits = quality
|
|
2965
|
+
? XAI_IMAGE_CREDIT_PRICES.source_image.quality
|
|
2966
|
+
: XAI_IMAGE_CREDIT_PRICES.source_image.standard;
|
|
2967
|
+
const outputCredits = quality
|
|
2968
|
+
? XAI_IMAGE_CREDIT_PRICES.output_image.quality[resolution]
|
|
2969
|
+
: XAI_IMAGE_CREDIT_PRICES.output_image.standard;
|
|
2970
|
+
const creditsRequired = Math.max(
|
|
2971
|
+
1,
|
|
2972
|
+
Math.ceil(
|
|
2973
|
+
roundUsdMicro(
|
|
2974
|
+
sourceCredits * sourceImageCount + outputCredits * outputImageCount,
|
|
2975
|
+
),
|
|
2976
|
+
),
|
|
2977
|
+
);
|
|
2508
2978
|
const defaultResolution =
|
|
2509
2979
|
modelParameters?.resolution === undefined ||
|
|
2510
2980
|
modelParameters?.resolution === null ||
|
|
@@ -2513,67 +2983,12 @@ function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
|
2513
2983
|
defaultResolution &&
|
|
2514
2984
|
outputImageCount === 1 &&
|
|
2515
2985
|
sourceImageCount === (edit ? 1 : 0);
|
|
2516
|
-
return {
|
|
2517
|
-
estimated_provider_cost_usd: roundUsdMicro(
|
|
2518
|
-
inputUsdPerImage * sourceImageCount +
|
|
2519
|
-
outputUsdPerImage * outputImageCount,
|
|
2520
|
-
),
|
|
2521
|
-
pricing_source: defaultShape ? "model_registry" : "model_parameters",
|
|
2522
|
-
pricing_confidence: "known",
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
function createGuideCreditPricingForProviderCost(input) {
|
|
2527
|
-
const providerCostUsd = roundUsdMicro(input.providerCostUsd);
|
|
2528
|
-
const creditsRequired = Math.max(
|
|
2529
|
-
1,
|
|
2530
|
-
Math.ceil(
|
|
2531
|
-
roundUsdMicro(
|
|
2532
|
-
providerCostUsd / (1 - TARGET_GROSS_MARGIN) / CREDIT_UNIT_USD,
|
|
2533
|
-
),
|
|
2534
|
-
),
|
|
2535
|
-
);
|
|
2536
|
-
const estimatedRevenueUsd = roundUsd(creditsRequired * CREDIT_UNIT_USD);
|
|
2537
|
-
const estimatedPaymentFeeUsd = roundUsdMicro(
|
|
2538
|
-
estimatedRevenueUsd * PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2539
|
-
);
|
|
2540
|
-
const estimatedNetRevenueUsd = roundUsdMicro(
|
|
2541
|
-
estimatedRevenueUsd - estimatedPaymentFeeUsd,
|
|
2542
|
-
);
|
|
2543
|
-
const estimatedGrossMargin =
|
|
2544
|
-
estimatedRevenueUsd > 0
|
|
2545
|
-
? roundRatio(
|
|
2546
|
-
(estimatedRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2547
|
-
)
|
|
2548
|
-
: null;
|
|
2549
|
-
const estimatedFeeAdjustedMargin =
|
|
2550
|
-
estimatedRevenueUsd > 0
|
|
2551
|
-
? roundRatio(
|
|
2552
|
-
(estimatedNetRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2553
|
-
)
|
|
2554
|
-
: null;
|
|
2555
|
-
const selfFundBlockReason =
|
|
2556
|
-
estimatedNetRevenueUsd + 1e-9 < providerCostUsd
|
|
2557
|
-
? "payment_fee_margin_negative"
|
|
2558
|
-
: null;
|
|
2559
2986
|
return {
|
|
2560
2987
|
credits_required: creditsRequired,
|
|
2561
2988
|
credit_unit_usd: CREDIT_UNIT_USD,
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
estimated_gross_margin: estimatedGrossMargin,
|
|
2566
|
-
payment_fee_rate: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2567
|
-
payment_fee_model: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL,
|
|
2568
|
-
estimated_payment_fee_usd: estimatedPaymentFeeUsd,
|
|
2569
|
-
estimated_net_revenue_usd: estimatedNetRevenueUsd,
|
|
2570
|
-
estimated_fee_adjusted_margin: estimatedFeeAdjustedMargin,
|
|
2571
|
-
self_fundable: selfFundBlockReason === null,
|
|
2572
|
-
self_fund_block_reason: selfFundBlockReason,
|
|
2573
|
-
target_gross_margin: TARGET_GROSS_MARGIN,
|
|
2574
|
-
pricing_confidence: input.pricingConfidence,
|
|
2575
|
-
pricing_source: input.pricingSource,
|
|
2576
|
-
margin_model: "provider_cost_plus_margin",
|
|
2989
|
+
estimated_charge_usd: roundUsd(creditsRequired * CREDIT_UNIT_USD),
|
|
2990
|
+
pricing_confidence: "known",
|
|
2991
|
+
pricing_source: defaultShape ? "model_registry" : "model_parameters",
|
|
2577
2992
|
};
|
|
2578
2993
|
}
|
|
2579
2994
|
|
|
@@ -2598,10 +3013,6 @@ function roundUsd(value) {
|
|
|
2598
3013
|
return Math.round(value * 100) / 100;
|
|
2599
3014
|
}
|
|
2600
3015
|
|
|
2601
|
-
function roundRatio(value) {
|
|
2602
|
-
return Math.round(value * 1000) / 1000;
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
3016
|
function roundUsdMicro(value) {
|
|
2606
3017
|
return Math.round(value * 1_000_000) / 1_000_000;
|
|
2607
3018
|
}
|
|
@@ -2627,16 +3038,8 @@ function createGuideModelCreditPricing(model) {
|
|
|
2627
3038
|
}
|
|
2628
3039
|
return {
|
|
2629
3040
|
credits_required: model.credits_required,
|
|
2630
|
-
estimated_revenue_usd:
|
|
2631
|
-
typeof model.credits_required === "number"
|
|
2632
|
-
? model.credits_required * CREDIT_UNIT_USD
|
|
2633
|
-
: null,
|
|
2634
|
-
estimated_provider_cost_usd:
|
|
2635
|
-
typeof model?.estimated_usd_per_image === "number"
|
|
2636
|
-
? model.estimated_usd_per_image
|
|
2637
|
-
: null,
|
|
2638
|
-
fallback_provider_cost_usd: null,
|
|
2639
3041
|
credit_unit_usd: CREDIT_UNIT_USD,
|
|
3042
|
+
estimated_charge_usd: roundUsd(model.credits_required * CREDIT_UNIT_USD),
|
|
2640
3043
|
pricing_confidence:
|
|
2641
3044
|
typeof model?.pricing_confidence === "string"
|
|
2642
3045
|
? model.pricing_confidence
|
|
@@ -2671,14 +3074,124 @@ function createGuideImpliesVideo(input) {
|
|
|
2671
3074
|
);
|
|
2672
3075
|
}
|
|
2673
3076
|
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
3077
|
+
// Extract an explicitly requested aspect ratio from a create/edit brief.
|
|
3078
|
+
//
|
|
3079
|
+
// Precedence:
|
|
3080
|
+
// 1. An explicit `W:H` token from EXPLICIT_ASPECT_RATIOS wins over any
|
|
3081
|
+
// orientation word -- it is the most specific statement of intent.
|
|
3082
|
+
// 2. Otherwise a bound orientation phrase is used.
|
|
3083
|
+
//
|
|
3084
|
+
// Conflicts resolve to null (no request) rather than to a guess: two different
|
|
3085
|
+
// explicit ratios, or two orientation phrases implying different shapes, mean
|
|
3086
|
+
// the brief is ambiguous and the guide should not pick a side.
|
|
3087
|
+
function classifyRequestedAspectRatio(input) {
|
|
3088
|
+
const text = `${input?.intent ?? ""} ${input?.prompt ?? ""}`.trim();
|
|
3089
|
+
if (text.length === 0) {
|
|
3090
|
+
return { aspectRatio: null, signal: null };
|
|
2677
3091
|
}
|
|
3092
|
+
|
|
3093
|
+
const ratioTokenIsSuppressed = RATIO_SUPPRESSING_CONTEXT.some((re) =>
|
|
3094
|
+
re.test(text),
|
|
3095
|
+
);
|
|
3096
|
+
|
|
3097
|
+
const explicit = new Set();
|
|
3098
|
+
if (!ratioTokenIsSuppressed) {
|
|
3099
|
+
for (const match of text.matchAll(ASPECT_RATIO_TOKEN_RE)) {
|
|
3100
|
+
const token = `${match[1]}:${match[2]}`;
|
|
3101
|
+
if (EXPLICIT_ASPECT_RATIOS.includes(token)) {
|
|
3102
|
+
explicit.add(token);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
if (explicit.size === 1) {
|
|
3107
|
+
const [only] = [...explicit];
|
|
3108
|
+
return { aspectRatio: only ?? null, signal: "explicit_ratio_token" };
|
|
3109
|
+
}
|
|
3110
|
+
if (explicit.size > 1) {
|
|
3111
|
+
// Contradictory explicit ratios: refuse to guess.
|
|
3112
|
+
return { aspectRatio: null, signal: null };
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
const implied = new Set();
|
|
3116
|
+
for (const rule of ORIENTATION_RULES) {
|
|
3117
|
+
if (rule.re.test(text)) {
|
|
3118
|
+
implied.add(rule.ratio);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
if (implied.size === 1) {
|
|
3122
|
+
const [only] = [...implied];
|
|
3123
|
+
return { aspectRatio: only ?? null, signal: "orientation_phrase" };
|
|
3124
|
+
}
|
|
3125
|
+
return { aspectRatio: null, signal: null };
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function requestedAspectRatioFromBrief(input) {
|
|
3129
|
+
return classifyRequestedAspectRatio(input).aspectRatio;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
// The normalized ratios a model publishes, or null when the payload does not
|
|
3133
|
+
// say. The hosted `/v1/models` list is a compact summary and omits the `media`
|
|
3134
|
+
// block entirely, so "not published" is the common case here and must be kept
|
|
3135
|
+
// distinct from "published and does not include this ratio".
|
|
3136
|
+
function guideModelAspectRatioValues(model) {
|
|
2678
3137
|
const values = Array.isArray(model?.media?.input?.aspect_ratios?.values)
|
|
2679
3138
|
? model.media.input.aspect_ratios.values
|
|
2680
3139
|
: model?.aspect_ratios;
|
|
2681
|
-
|
|
3140
|
+
return Array.isArray(values) ? values : null;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// Tri-state on purpose: true (published and supported), false (published and
|
|
3144
|
+
// NOT supported), null (capability unknown). Unknown must never be read as a
|
|
3145
|
+
// denial -- doing so would make every ratio request in production claim "no
|
|
3146
|
+
// available model supports this", which is worse than the bug being fixed.
|
|
3147
|
+
function modelAspectRatioSupport(model, aspectRatio) {
|
|
3148
|
+
if (aspectRatio === null || aspectRatio === undefined) {
|
|
3149
|
+
return true;
|
|
3150
|
+
}
|
|
3151
|
+
const values = guideModelAspectRatioValues(model);
|
|
3152
|
+
return values === null ? null : values.includes(aspectRatio);
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function modelCannotExpressAspectRatio(model, aspectRatio) {
|
|
3156
|
+
return modelAspectRatioSupport(model, aspectRatio) === false;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// Honest capability note for the case where an explicitly requested aspect
|
|
3160
|
+
// ratio made the guide skip its normal top pick. Name the capability that
|
|
3161
|
+
// forced the hop so agents learn which model could not express the shape.
|
|
3162
|
+
function aspectRatioRoutedCreateSelectionReason(input) {
|
|
3163
|
+
return `guide selected a create model that supports the requested ${input.aspectRatio} aspect ratio because ${input.skippedModelId} does not expose ${input.aspectRatio}`;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Fail-honestly note for the case where NO available model can express the
|
|
3167
|
+
// requested ratio. The guide keeps its normal selection and says so rather than
|
|
3168
|
+
// silently dropping the ratio, which is the exact failure this fixes.
|
|
3169
|
+
function aspectRatioUnsupportedWarning(input) {
|
|
3170
|
+
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.`;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function createGuideSuggestedAspectRatio(input) {
|
|
3174
|
+
const model = input?.model ?? null;
|
|
3175
|
+
const requestedAspectRatio = input?.requestedAspectRatio ?? null;
|
|
3176
|
+
if (model === null) {
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
// An explicit request wins for every modality, but not when the selected
|
|
3180
|
+
// model is KNOWN not to express it. In that case return null so
|
|
3181
|
+
// `next_command` stays runnable (`--aspect-ratio` would fail model
|
|
3182
|
+
// validation) and let the caller warn instead of silently pretending the
|
|
3183
|
+
// ratio was honored.
|
|
3184
|
+
if (requestedAspectRatio !== null) {
|
|
3185
|
+
return modelCannotExpressAspectRatio(model, requestedAspectRatio)
|
|
3186
|
+
? null
|
|
3187
|
+
: requestedAspectRatio;
|
|
3188
|
+
}
|
|
3189
|
+
// No explicit request: preserve the existing video-only default.
|
|
3190
|
+
if (model?.modality !== "video") {
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
const values = guideModelAspectRatioValues(model);
|
|
3194
|
+
if (values === null) {
|
|
2682
3195
|
return null;
|
|
2683
3196
|
}
|
|
2684
3197
|
return values.includes("16:9") ? "16:9" : (values[0] ?? null);
|
|
@@ -2732,9 +3245,14 @@ function createGuideSelectionReason(
|
|
|
2732
3245
|
model?.id,
|
|
2733
3246
|
)
|
|
2734
3247
|
) {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3248
|
+
const intentClass = createGuideIntentClass(intent);
|
|
3249
|
+
if (intentClass === "budget_draft") {
|
|
3250
|
+
return "guide selected the lowest-credit available create model for a draft/budget intent";
|
|
3251
|
+
}
|
|
3252
|
+
if (intentClass === "general") {
|
|
3253
|
+
return "guide selected a cost-efficient general-purpose create model for iteration; pass --intent final for the quality-first model";
|
|
3254
|
+
}
|
|
3255
|
+
return "guide selected the strongest currently available quality-first create model for this intent";
|
|
2738
3256
|
}
|
|
2739
3257
|
return operation === "edit"
|
|
2740
3258
|
? "guide selected the first available executable input-image edit model"
|
|
@@ -3389,7 +3907,24 @@ function createGuideBlocker(stage, input) {
|
|
|
3389
3907
|
};
|
|
3390
3908
|
}
|
|
3391
3909
|
if (stage === "quota_required") {
|
|
3392
|
-
const
|
|
3910
|
+
const quotaData = input.quota?.envelope.data ?? null;
|
|
3911
|
+
const remaining = quotaRemainingCredits(quotaData);
|
|
3912
|
+
// Mirror createGuideStage's credit-first ordering: credits only outrank the
|
|
3913
|
+
// daily-job cap when they are genuinely short. Agents that hold spare
|
|
3914
|
+
// credits but hit the daily allowance (issues #2103, #2148) must be told the
|
|
3915
|
+
// honest trigger, not a credits-shortfall line that reads as wrong to them
|
|
3916
|
+
// (same confusion class as #1193).
|
|
3917
|
+
const creditsShort =
|
|
3918
|
+
input.estimatedCredits !== null &&
|
|
3919
|
+
remaining !== null &&
|
|
3920
|
+
remaining < input.estimatedCredits;
|
|
3921
|
+
const dailyJobs = quotaData?.daily_jobs;
|
|
3922
|
+
if (!creditsShort && dailyJobs !== undefined && dailyJobs.remaining <= 0) {
|
|
3923
|
+
return {
|
|
3924
|
+
code: "quota_required",
|
|
3925
|
+
message: `You've reached today's job allowance (${dailyJobs.cap}/day); a top-up raises your daily allowance so you can keep creating now.`,
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3393
3928
|
return {
|
|
3394
3929
|
code: "quota_required",
|
|
3395
3930
|
message: `Selected first image requires ${input.estimatedCredits ?? "unknown"} credits; current remaining credits are ${remaining ?? "unknown"}.`,
|
|
@@ -4000,14 +4535,22 @@ function createGuideWarning(stage, input) {
|
|
|
4000
4535
|
warning,
|
|
4001
4536
|
};
|
|
4002
4537
|
}
|
|
4538
|
+
// #2204-#2216: name the field this response actually carries. The compact
|
|
4539
|
+
// ready payload drops the `recommended_no_spend_command` alias, so it points
|
|
4540
|
+
// at the base field instead; blocked and `--explain` payloads keep the alias
|
|
4541
|
+
// and keep naming it. Either way
|
|
4542
|
+
// `data[data.guide_warning.recommended_command_field]` resolves.
|
|
4543
|
+
const noSpendField =
|
|
4544
|
+
input.readyCompact === true
|
|
4545
|
+
? "no_spend_next_command"
|
|
4546
|
+
: "recommended_no_spend_command";
|
|
4003
4547
|
return {
|
|
4004
4548
|
...base,
|
|
4005
4549
|
next_command_safety: "live_media_create_credit_debit",
|
|
4006
4550
|
no_spend_safe: false,
|
|
4007
4551
|
spend_required: true,
|
|
4008
|
-
recommended_command_field:
|
|
4009
|
-
warning:
|
|
4010
|
-
"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.",
|
|
4552
|
+
recommended_command_field: noSpendField,
|
|
4553
|
+
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}.`,
|
|
4011
4554
|
};
|
|
4012
4555
|
}
|
|
4013
4556
|
|
|
@@ -4097,6 +4640,7 @@ function createGuideNextCommand(stage, input) {
|
|
|
4097
4640
|
prompt: input.prompt,
|
|
4098
4641
|
inputReference: input.inputReference,
|
|
4099
4642
|
budgetGuard: input.budgetGuard,
|
|
4643
|
+
aspectRatio: input.aspectRatio,
|
|
4100
4644
|
modelParametersJson: input.modelParametersJson,
|
|
4101
4645
|
dryRun: false,
|
|
4102
4646
|
idempotencyKey: `edit-guide-${Date.now()}-${randomBytes(4).toString("hex")}`,
|
|
@@ -4156,6 +4700,7 @@ function createGuideEscapeHatches(input) {
|
|
|
4156
4700
|
prompt: input.prompt,
|
|
4157
4701
|
inputReference: input.inputReference,
|
|
4158
4702
|
budgetGuard: input.budgetGuard,
|
|
4703
|
+
aspectRatio: input.aspectRatio,
|
|
4159
4704
|
modelParametersJson: input.modelParametersJson,
|
|
4160
4705
|
dryRun: true,
|
|
4161
4706
|
apiBaseUrl: input.apiBaseUrl,
|
|
@@ -4185,7 +4730,7 @@ function renderGuideCommand(
|
|
|
4185
4730
|
const operation = options.operation ?? "create";
|
|
4186
4731
|
return [
|
|
4187
4732
|
commandPrefix,
|
|
4188
|
-
`${operation} --guide --prompt`,
|
|
4733
|
+
`${operation} --guide${options.explain === true ? " --explain" : ""} --prompt`,
|
|
4189
4734
|
shellQuote(prompt),
|
|
4190
4735
|
...(operation === "edit" &&
|
|
4191
4736
|
typeof options.inputReference === "string" &&
|
|
@@ -4291,6 +4836,9 @@ function renderInputImageGuideCommand(input) {
|
|
|
4291
4836
|
"--model",
|
|
4292
4837
|
shellQuote(input.modelId),
|
|
4293
4838
|
...(promptless ? [] : ["--prompt", shellQuote(input.prompt)]),
|
|
4839
|
+
...(input.aspectRatio === null || input.aspectRatio === undefined
|
|
4840
|
+
? []
|
|
4841
|
+
: ["--aspect-ratio", shellQuote(input.aspectRatio)]),
|
|
4294
4842
|
"--max-estimated-usd-per-image",
|
|
4295
4843
|
shellQuote(formatUsd(input.budgetGuard)),
|
|
4296
4844
|
...(input.modelParametersJson === null ||
|
|
@@ -4439,6 +4987,18 @@ async function create(argv) {
|
|
|
4439
4987
|
if (flagBool(args, "guide")) {
|
|
4440
4988
|
return createGuide(args);
|
|
4441
4989
|
}
|
|
4990
|
+
if (flagBool(args, "no-spend")) {
|
|
4991
|
+
return invalid(
|
|
4992
|
+
"luxin create",
|
|
4993
|
+
"create --no-spend is a guide output mode; combine it with --guide, or use create --dry-run for a no-spend planned job",
|
|
4994
|
+
);
|
|
4995
|
+
}
|
|
4996
|
+
if (flagBool(args, "explain")) {
|
|
4997
|
+
return invalid(
|
|
4998
|
+
"luxin create",
|
|
4999
|
+
"create --explain is a guide output mode; combine it with --guide to return every guide field",
|
|
5000
|
+
);
|
|
5001
|
+
}
|
|
4442
5002
|
const prompt = await promptValue(args);
|
|
4443
5003
|
if (!prompt.ok) {
|
|
4444
5004
|
return prompt.result;
|
|
@@ -4586,6 +5146,18 @@ async function edit(argv) {
|
|
|
4586
5146
|
inputReference: input,
|
|
4587
5147
|
});
|
|
4588
5148
|
}
|
|
5149
|
+
if (flagBool(args, "no-spend")) {
|
|
5150
|
+
return invalid(
|
|
5151
|
+
"luxin edit",
|
|
5152
|
+
"edit --no-spend is a guide output mode; combine it with --guide, or use edit --dry-run for a no-spend planned job",
|
|
5153
|
+
);
|
|
5154
|
+
}
|
|
5155
|
+
if (flagBool(args, "explain")) {
|
|
5156
|
+
return invalid(
|
|
5157
|
+
"luxin edit",
|
|
5158
|
+
"edit --explain is a guide output mode; combine it with --guide to return every guide field",
|
|
5159
|
+
);
|
|
5160
|
+
}
|
|
4589
5161
|
if (input === undefined) {
|
|
4590
5162
|
return invalid(
|
|
4591
5163
|
"luxin edit",
|
|
@@ -4705,7 +5277,7 @@ async function assets(argv) {
|
|
|
4705
5277
|
if (assetId === null) {
|
|
4706
5278
|
return invalid(
|
|
4707
5279
|
"luxin assets",
|
|
4708
|
-
|
|
5280
|
+
`assets currently supports Luxin asset ids and ${ACCEPTED_PUBLIC_MEDIA_HOSTS.join(" or ")} URLs`,
|
|
4709
5281
|
);
|
|
4710
5282
|
}
|
|
4711
5283
|
const token = await resolveToken(args);
|
|
@@ -6967,6 +7539,16 @@ function quotaRecoveryWithCopyRunnableCommands(recovery, commandPrefix) {
|
|
|
6967
7539
|
),
|
|
6968
7540
|
}
|
|
6969
7541
|
: {}),
|
|
7542
|
+
...(recovery.human_handoff &&
|
|
7543
|
+
typeof recovery.human_handoff === "object" &&
|
|
7544
|
+
typeof recovery.human_handoff.command === "string"
|
|
7545
|
+
? {
|
|
7546
|
+
human_handoff: {
|
|
7547
|
+
...recovery.human_handoff,
|
|
7548
|
+
command: render(recovery.human_handoff.command),
|
|
7549
|
+
},
|
|
7550
|
+
}
|
|
7551
|
+
: {}),
|
|
6970
7552
|
top_up: quotaTopUpWithCopyRunnableCommands(recovery.top_up, commandPrefix),
|
|
6971
7553
|
};
|
|
6972
7554
|
}
|
|
@@ -7130,7 +7712,7 @@ function assetIdFromReference(reference) {
|
|
|
7130
7712
|
const url = new URL(reference);
|
|
7131
7713
|
if (
|
|
7132
7714
|
url.protocol !== "https:" ||
|
|
7133
|
-
url.hostname
|
|
7715
|
+
!ACCEPTED_PUBLIC_MEDIA_HOSTS.includes(url.hostname.toLowerCase()) ||
|
|
7134
7716
|
!url.pathname.startsWith("/a/")
|
|
7135
7717
|
) {
|
|
7136
7718
|
return null;
|