luxin-cli 0.2.4 → 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 +20 -0
- package/bin/luxin.mjs +166 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ provenance; this file is the human- and agent-readable release map.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.2.5 - 2026-08-09
|
|
10
|
+
|
|
11
|
+
- Release (correctness): `edit --guide` selects a model again. It gated on
|
|
12
|
+
`media.input.images.required`, which the hosted `/v1/models` summary does not
|
|
13
|
+
publish, so on 0.2.4 every edit brief returned `no_executable_model` and the
|
|
14
|
+
blocker named `create`. The predicate now reads `supports`, which the summary
|
|
15
|
+
does publish; measured on the live 220-model catalog the two express the same
|
|
16
|
+
79-model set, so this drops a redundant conjunct rather than a guarantee. The
|
|
17
|
+
blocker also names the operation it actually failed on.
|
|
18
|
+
- Release (correctness): a plain video brief reports its aspect ratio again.
|
|
19
|
+
`selection.suggested_aspect_ratio` read the same unpublished block and
|
|
20
|
+
returned null on every real video call unless the brief happened to name a
|
|
21
|
+
ratio.
|
|
22
|
+
- Known issue (#2259): `create --guide` can quote a different credit figure than
|
|
23
|
+
the `--dry-run` it hands you, because it prices the model's catalog default
|
|
24
|
+
shape rather than the ratio the command actually sends. Measured against the
|
|
25
|
+
live API: 4 vs 5 with no ratio stated, 4 vs 3 at 16:9. The dry-run figure is
|
|
26
|
+
the authoritative one; budget from it, not from the guide. Unchanged from
|
|
27
|
+
0.2.4; the fix landed server-side but not in this package.
|
|
28
|
+
|
|
9
29
|
## 0.2.4 - 2026-08-09
|
|
10
30
|
|
|
11
31
|
- Release (correctness): the guide honors an aspect ratio stated in the brief
|
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";
|
|
@@ -1933,7 +1933,16 @@ async function createGuide(args, options = {}) {
|
|
|
1933
1933
|
requestedAspectRatio: briefAspectRatio,
|
|
1934
1934
|
});
|
|
1935
1935
|
let selection = selectModel(listedModels);
|
|
1936
|
-
|
|
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) {
|
|
1937
1946
|
// Two-pass: the first pass names the models worth asking about, then one
|
|
1938
1947
|
// bounded round of detail lookups makes the second pass actually
|
|
1939
1948
|
// capability-aware instead of guessing.
|
|
@@ -1943,6 +1952,7 @@ async function createGuide(args, options = {}) {
|
|
|
1943
1952
|
models: listedModels,
|
|
1944
1953
|
selected: selection?.model ?? null,
|
|
1945
1954
|
intent: requestedIntent,
|
|
1955
|
+
routing: briefAspectRatio !== null,
|
|
1946
1956
|
}),
|
|
1947
1957
|
apiBaseUrl,
|
|
1948
1958
|
});
|
|
@@ -1983,22 +1993,20 @@ async function createGuide(args, options = {}) {
|
|
|
1983
1993
|
modelParameters: modelParameters.value ?? {},
|
|
1984
1994
|
maxEstimatedUsdPerImage,
|
|
1985
1995
|
});
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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.
|
|
1992
2002
|
const pricing =
|
|
1993
|
-
selected === null
|
|
1994
|
-
?
|
|
1995
|
-
:
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
)
|
|
2001
|
-
: createGuideModelCreditPricing(selected);
|
|
2003
|
+
selected === null || createGuideSelectedModelRequiresInputImage(selected)
|
|
2004
|
+
? createGuideModelCreditPricing(selected)
|
|
2005
|
+
: createGuidePricingForModel(
|
|
2006
|
+
selected,
|
|
2007
|
+
defaultedModelParameters.modelParameters,
|
|
2008
|
+
pricingContext,
|
|
2009
|
+
);
|
|
2002
2010
|
const estimatedCredits = pricing?.credits_required ?? null;
|
|
2003
2011
|
// The guide publishes what the caller will be charged, never our provider
|
|
2004
2012
|
// cost. `economics.estimated_usd_per_image` is the hosted charge per image.
|
|
@@ -2057,6 +2065,7 @@ async function createGuide(args, options = {}) {
|
|
|
2057
2065
|
guideCommandPrefix,
|
|
2058
2066
|
);
|
|
2059
2067
|
const blocker = createGuideBlocker(stage, {
|
|
2068
|
+
operation: guideOperation,
|
|
2060
2069
|
requestedModelId,
|
|
2061
2070
|
quota,
|
|
2062
2071
|
estimatedCredits,
|
|
@@ -2467,8 +2476,6 @@ function selectCreateGuideModel(
|
|
|
2467
2476
|
const isExecutableInputImageEdit = (model) =>
|
|
2468
2477
|
model?.status === "available" &&
|
|
2469
2478
|
guideModelExecutionStatus(model) === "executable" &&
|
|
2470
|
-
Array.isArray(model?.supports) &&
|
|
2471
|
-
(model.supports.includes("edit") || model.supports.includes("variation")) &&
|
|
2472
2479
|
createGuideSelectedModelRequiresInputImage(model);
|
|
2473
2480
|
const isExecutableGuideModel = (model) =>
|
|
2474
2481
|
operation === "edit"
|
|
@@ -2585,16 +2592,21 @@ function selectCreateGuideModel(
|
|
|
2585
2592
|
// capability UNKNOWN, which never disqualifies a model and never triggers the
|
|
2586
2593
|
// unsupported-ratio warning.
|
|
2587
2594
|
|
|
2588
|
-
function guideAspectRatioProbeModelIds({ models, selected, intent }) {
|
|
2595
|
+
function guideAspectRatioProbeModelIds({ models, selected, intent, routing }) {
|
|
2589
2596
|
const ids = [];
|
|
2590
2597
|
if (typeof selected?.id === "string") {
|
|
2591
2598
|
ids.push(selected.id);
|
|
2592
2599
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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
|
+
}
|
|
2598
2610
|
}
|
|
2599
2611
|
}
|
|
2600
2612
|
return [...new Set(ids)].slice(0, GUIDE_ASPECT_RATIO_PROBE_LIMIT);
|
|
@@ -2939,7 +2951,49 @@ function createGuidePricingForModel(model, modelParameters, context = {}) {
|
|
|
2939
2951
|
if (createGuideCanPriceModelParameters(model)) {
|
|
2940
2952
|
return createGuideXaiImagePricing(model, modelParameters, context);
|
|
2941
2953
|
}
|
|
2942
|
-
return
|
|
2954
|
+
return (
|
|
2955
|
+
createGuideAspectRatioCreditPricing(model, context) ??
|
|
2956
|
+
createGuideModelCreditPricing(model)
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
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;
|
|
2988
|
+
}
|
|
2989
|
+
const quoted = createGuideModelCreditPricing(model);
|
|
2990
|
+
return {
|
|
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",
|
|
2996
|
+
};
|
|
2943
2997
|
}
|
|
2944
2998
|
|
|
2945
2999
|
function createGuideCanPriceModelParameters(model) {
|
|
@@ -3017,6 +3071,43 @@ function roundUsdMicro(value) {
|
|
|
3017
3071
|
return Math.round(value * 1_000_000) / 1_000_000;
|
|
3018
3072
|
}
|
|
3019
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
|
+
|
|
3020
3111
|
function guideModelExecutionStatus(model) {
|
|
3021
3112
|
if (
|
|
3022
3113
|
isRecord(model?.execution) &&
|
|
@@ -3133,6 +3224,26 @@ function requestedAspectRatioFromBrief(input) {
|
|
|
3133
3224
|
// say. The hosted `/v1/models` list is a compact summary and omits the `media`
|
|
3134
3225
|
// block entirely, so "not published" is the common case here and must be kept
|
|
3135
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.
|
|
3136
3247
|
function guideModelAspectRatioValues(model) {
|
|
3137
3248
|
const values = Array.isArray(model?.media?.input?.aspect_ratios?.values)
|
|
3138
3249
|
? model.media.input.aspect_ratios.values
|
|
@@ -3197,10 +3308,31 @@ function createGuideSuggestedAspectRatio(input) {
|
|
|
3197
3308
|
return values.includes("16:9") ? "16:9" : (values[0] ?? null);
|
|
3198
3309
|
}
|
|
3199
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.
|
|
3200
3334
|
function createGuideSelectedModelRequiresInputImage(model) {
|
|
3201
3335
|
return (
|
|
3202
|
-
(model?.media?.input?.images?.required === true ||
|
|
3203
|
-
model?.accepts_input_images === true) &&
|
|
3204
3336
|
Array.isArray(model?.supports) &&
|
|
3205
3337
|
(model.supports.includes("edit") || model.supports.includes("variation"))
|
|
3206
3338
|
);
|
|
@@ -3891,12 +4023,16 @@ function createGuideBlocker(stage, input) {
|
|
|
3891
4023
|
};
|
|
3892
4024
|
}
|
|
3893
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";
|
|
3894
4030
|
return {
|
|
3895
4031
|
code: "no_executable_model",
|
|
3896
4032
|
message:
|
|
3897
4033
|
input.requestedModelId === null
|
|
3898
|
-
?
|
|
3899
|
-
: `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}`,
|
|
3900
4036
|
};
|
|
3901
4037
|
}
|
|
3902
4038
|
if (stage === "auth_required") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luxin-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Image generation, image editing, video, audio, and image-to-3D for agents, with no API key to obtain first. The agent signs itself up, starts on free preview credits, and gets hosted media URLs, recoverable jobs, and cost receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|