image-skill 0.1.42 → 0.1.44
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/SKILL.md +8 -8
- package/bin/image-skill.mjs +611 -24
- package/cli.md +42 -3
- package/commands.json +3 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/skill.md +8 -8
- package/skills/agent-image-generation/SKILL.md +78 -0
- package/skills/image-skill/SKILL.md +8 -8
- package/skills/image-skill/references/cli.md +42 -3
- package/skills/image-skill/references/commands.json +3 -1
- package/skills/image-skill/references/llms.txt +2 -2
package/bin/image-skill.mjs
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { createWriteStream } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmod,
|
|
6
|
+
mkdir,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
rm,
|
|
10
|
+
stat,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from "node:fs/promises";
|
|
5
13
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
14
|
import { Readable } from "node:stream";
|
|
7
15
|
import { pipeline } from "node:stream/promises";
|
|
8
16
|
import os from "node:os";
|
|
9
17
|
|
|
10
|
-
const VERSION = "0.1.
|
|
18
|
+
const VERSION = "0.1.44";
|
|
11
19
|
const PACKAGE_NAME = "image-skill";
|
|
12
20
|
const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
|
|
13
21
|
const DEFAULT_DOCS_BASE_URL = "https://image-skill.com";
|
|
14
22
|
const DEFAULT_NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
15
23
|
const PUBLIC_REPO_URL = "https://github.com/danielgwilson/image-skill-cli";
|
|
24
|
+
const IN_FLIGHT_RESERVATION_TTL_MS = 15 * 60 * 1000;
|
|
25
|
+
const IN_FLIGHT_SWEEP_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
16
26
|
const PROMPTLESS_EDIT_MODEL_IDS = new Set([
|
|
17
27
|
"fal.flux-dev-redux",
|
|
18
28
|
"fal.flux-krea-redux",
|
|
@@ -39,6 +49,9 @@ const HOSTED_SIGNUP_TOKEN_RETURNED_WARNING =
|
|
|
39
49
|
const PUBLIC_NPX_COMMAND_PREFIX =
|
|
40
50
|
"npm_config_update_notifier=false npx -y image-skill@latest";
|
|
41
51
|
const CREDIT_UNIT_USD = 0.01;
|
|
52
|
+
const TARGET_GROSS_MARGIN = 0.4;
|
|
53
|
+
const PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE = 0.015;
|
|
54
|
+
const PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL = "stripe_stablecoin_usd_percent";
|
|
42
55
|
const MODALITY_COMMAND_ALIASES = new Map([
|
|
43
56
|
["image", { command: "create", intent: null }],
|
|
44
57
|
["video", { command: "create", intent: "video" }],
|
|
@@ -327,7 +340,8 @@ function commandHelpByKey(key) {
|
|
|
327
340
|
usage: "image-skill doctor --json",
|
|
328
341
|
docs_url: "https://image-skill.com/cli.md#image-skill-doctor",
|
|
329
342
|
description:
|
|
330
|
-
"Check hosted API reachability, CLI version, auth state, and
|
|
343
|
+
"Check hosted API reachability, CLI version, auth state, health, and live-spend recovery breadcrumbs.",
|
|
344
|
+
optional_flags: ["--sweep-in-flight"],
|
|
331
345
|
},
|
|
332
346
|
trust: {
|
|
333
347
|
command: "image-skill trust help",
|
|
@@ -518,6 +532,7 @@ function commandHelpByKey(key) {
|
|
|
518
532
|
docs_url: "https://image-skill.com/cli.md#image-skill-edit",
|
|
519
533
|
required_flags: ["--input"],
|
|
520
534
|
optional_flags: [
|
|
535
|
+
"--guide",
|
|
521
536
|
"--dry-run",
|
|
522
537
|
"--prompt",
|
|
523
538
|
"--model",
|
|
@@ -606,6 +621,10 @@ async function doctor(argv) {
|
|
|
606
621
|
const args = parseArgs(argv);
|
|
607
622
|
const apiBaseUrl = apiBase(args);
|
|
608
623
|
const config = await readConfig(configPath());
|
|
624
|
+
const inFlight = await inFlightSpendDoctorReport({
|
|
625
|
+
sweep: flagBool(args, "sweep-in-flight"),
|
|
626
|
+
now: new Date(),
|
|
627
|
+
});
|
|
609
628
|
const health = await apiRequest({
|
|
610
629
|
command: "image-skill doctor",
|
|
611
630
|
method: "GET",
|
|
@@ -628,6 +647,7 @@ async function doctor(argv) {
|
|
|
628
647
|
saved_token: config.tokenPresent,
|
|
629
648
|
env_token: hasEnvToken(),
|
|
630
649
|
},
|
|
650
|
+
in_flight: inFlight,
|
|
631
651
|
docs: {
|
|
632
652
|
skill: "https://image-skill.com/skill.md",
|
|
633
653
|
llms: "https://image-skill.com/llms.txt",
|
|
@@ -1591,17 +1611,19 @@ async function capabilities(argv) {
|
|
|
1591
1611
|
});
|
|
1592
1612
|
}
|
|
1593
1613
|
|
|
1594
|
-
async function createGuide(args) {
|
|
1614
|
+
async function createGuide(args, options = {}) {
|
|
1615
|
+
const guideOperation = options.guideOperation ?? "create";
|
|
1616
|
+
const command = `image-skill ${guideOperation} --guide`;
|
|
1595
1617
|
if (flagBool(args, "dry-run")) {
|
|
1596
1618
|
return invalid(
|
|
1597
|
-
|
|
1598
|
-
|
|
1619
|
+
command,
|
|
1620
|
+
`${guideOperation} --guide cannot be combined with --dry-run; the guide returns the dry-run escape hatch separately`,
|
|
1599
1621
|
);
|
|
1600
1622
|
}
|
|
1601
1623
|
if (hasReferenceFlags(args)) {
|
|
1602
1624
|
return invalid(
|
|
1603
|
-
|
|
1604
|
-
|
|
1625
|
+
command,
|
|
1626
|
+
`${guideOperation} --guide does not upload or resolve reference images; inspect the model with models show, then run ${guideOperation} --dry-run before live referenced media calls`,
|
|
1605
1627
|
);
|
|
1606
1628
|
}
|
|
1607
1629
|
const modelParameters = jsonObjectFlag(args, "model-parameters-json");
|
|
@@ -1616,6 +1638,10 @@ async function createGuide(args) {
|
|
|
1616
1638
|
const requestedProviderId = flagString(args, "provider");
|
|
1617
1639
|
const requestedIntentFlag = flagString(args, "intent");
|
|
1618
1640
|
const requestedIntent = requestedIntentFlag ?? "explore";
|
|
1641
|
+
const requestedModelParametersJson =
|
|
1642
|
+
modelParameters.value === null
|
|
1643
|
+
? null
|
|
1644
|
+
: JSON.stringify(modelParameters.value);
|
|
1619
1645
|
const maxEstimatedUsdPerImage = flagNumber(
|
|
1620
1646
|
args,
|
|
1621
1647
|
"max-estimated-usd-per-image",
|
|
@@ -1646,17 +1672,58 @@ async function createGuide(args) {
|
|
|
1646
1672
|
const selected =
|
|
1647
1673
|
models.envelope.ok && models.envelope.data?.models
|
|
1648
1674
|
? selectCreateGuideModel(models.envelope.data.models, requestedModelId, {
|
|
1675
|
+
operation: guideOperation,
|
|
1649
1676
|
prompt: trimmedPrompt,
|
|
1650
1677
|
intent: requestedIntent,
|
|
1651
1678
|
maxEstimatedUsdPerImage,
|
|
1652
1679
|
})
|
|
1653
1680
|
: null;
|
|
1654
1681
|
const selectedAspectRatio = createGuideSuggestedAspectRatio(selected);
|
|
1655
|
-
const
|
|
1682
|
+
const pricingContext = {
|
|
1683
|
+
aspectRatio: selectedAspectRatio ?? "1:1",
|
|
1684
|
+
outputCount: 1,
|
|
1685
|
+
};
|
|
1686
|
+
const defaultedModelParameters =
|
|
1687
|
+
selected === null || createGuideSelectedModelRequiresInputImage(selected)
|
|
1688
|
+
? {
|
|
1689
|
+
modelParameters: modelParameters.value ?? {},
|
|
1690
|
+
defaultsApplied: [],
|
|
1691
|
+
}
|
|
1692
|
+
: createGuideDefaultModelParameters({
|
|
1693
|
+
model: selected,
|
|
1694
|
+
aspectRatio: pricingContext.aspectRatio,
|
|
1695
|
+
intent: requestedIntent,
|
|
1696
|
+
modelParameters: modelParameters.value ?? {},
|
|
1697
|
+
maxEstimatedUsdPerImage,
|
|
1698
|
+
});
|
|
1699
|
+
const shouldPriceModelParameters =
|
|
1700
|
+
selected !== null &&
|
|
1701
|
+
!createGuideSelectedModelRequiresInputImage(selected) &&
|
|
1702
|
+
createGuideCanPriceModelParameters(selected) &&
|
|
1703
|
+
(defaultedModelParameters.defaultsApplied.length > 0 ||
|
|
1704
|
+
Object.keys(modelParameters.value ?? {}).length > 0);
|
|
1705
|
+
const pricing =
|
|
1706
|
+
selected === null
|
|
1707
|
+
? null
|
|
1708
|
+
: shouldPriceModelParameters
|
|
1709
|
+
? createGuidePricingForModel(
|
|
1710
|
+
selected,
|
|
1711
|
+
defaultedModelParameters.modelParameters,
|
|
1712
|
+
pricingContext,
|
|
1713
|
+
)
|
|
1714
|
+
: createGuideModelCreditPricing(selected);
|
|
1715
|
+
const providerCostEstimate =
|
|
1716
|
+
selected === null || !shouldPriceModelParameters
|
|
1717
|
+
? null
|
|
1718
|
+
: createGuideProviderCostEstimateForModel(
|
|
1719
|
+
selected,
|
|
1720
|
+
defaultedModelParameters.modelParameters,
|
|
1721
|
+
pricingContext,
|
|
1722
|
+
);
|
|
1656
1723
|
const estimatedCredits = pricing?.credits_required ?? null;
|
|
1657
1724
|
const estimatedProviderUsdPerImage =
|
|
1725
|
+
providerCostEstimate?.estimated_provider_cost_usd ??
|
|
1658
1726
|
selected?.economics?.estimated_usd_per_image ??
|
|
1659
|
-
pricing?.estimated_provider_cost_usd ??
|
|
1660
1727
|
pricing?.fallback_provider_cost_usd ??
|
|
1661
1728
|
(typeof selected?.estimated_usd_per_image === "number"
|
|
1662
1729
|
? selected.estimated_usd_per_image
|
|
@@ -1683,6 +1750,9 @@ async function createGuide(args) {
|
|
|
1683
1750
|
token.source === "anonymous" ? "none" : token.source;
|
|
1684
1751
|
const stage = createGuideStage({
|
|
1685
1752
|
prompt: trimmedPrompt,
|
|
1753
|
+
promptRequired:
|
|
1754
|
+
trimmedPrompt.length === 0 &&
|
|
1755
|
+
(selected === null || !PROMPTLESS_EDIT_MODEL_IDS.has(selected.id)),
|
|
1686
1756
|
health,
|
|
1687
1757
|
models,
|
|
1688
1758
|
selected,
|
|
@@ -1714,9 +1784,12 @@ async function createGuide(args) {
|
|
|
1714
1784
|
requestedIntent,
|
|
1715
1785
|
requestedIntentFlag,
|
|
1716
1786
|
requestedModelId,
|
|
1787
|
+
guideOperation,
|
|
1788
|
+
inputReference: options.inputReference,
|
|
1717
1789
|
maxEstimatedUsdPerImage,
|
|
1718
1790
|
budgetGuard,
|
|
1719
1791
|
aspectRatio: selectedAspectRatio,
|
|
1792
|
+
modelParametersJson: requestedModelParametersJson,
|
|
1720
1793
|
apiBaseUrl: explicitApiBaseUrl(args),
|
|
1721
1794
|
paymentSummary,
|
|
1722
1795
|
commandPrefix: guideCommandPrefix,
|
|
@@ -1732,9 +1805,12 @@ async function createGuide(args) {
|
|
|
1732
1805
|
requestedIntent,
|
|
1733
1806
|
requestedIntentFlag,
|
|
1734
1807
|
requestedModelId,
|
|
1808
|
+
guideOperation,
|
|
1809
|
+
inputReference: options.inputReference,
|
|
1735
1810
|
maxEstimatedUsdPerImage,
|
|
1736
1811
|
budgetGuard,
|
|
1737
1812
|
aspectRatio: selectedAspectRatio,
|
|
1813
|
+
modelParametersJson: requestedModelParametersJson,
|
|
1738
1814
|
apiBaseUrl: explicitApiBaseUrl(args),
|
|
1739
1815
|
commandPrefix: guideCommandPrefix,
|
|
1740
1816
|
});
|
|
@@ -1776,10 +1852,13 @@ async function createGuide(args) {
|
|
|
1776
1852
|
explicitApiBaseUrl(args),
|
|
1777
1853
|
guideCommandPrefix,
|
|
1778
1854
|
{
|
|
1855
|
+
operation: guideOperation,
|
|
1856
|
+
inputReference: options.inputReference,
|
|
1779
1857
|
modelId: requestedModelId,
|
|
1780
1858
|
providerId: requestedProviderId,
|
|
1781
1859
|
intent: requestedIntentFlag,
|
|
1782
1860
|
maxEstimatedUsdPerImage,
|
|
1861
|
+
modelParametersJson: requestedModelParametersJson,
|
|
1783
1862
|
},
|
|
1784
1863
|
)
|
|
1785
1864
|
: null;
|
|
@@ -1802,8 +1881,19 @@ async function createGuide(args) {
|
|
|
1802
1881
|
tokenSource: publicTokenSource,
|
|
1803
1882
|
commandPrefix: guideCommandPrefix,
|
|
1804
1883
|
});
|
|
1805
|
-
|
|
1806
|
-
|
|
1884
|
+
const guideRecovery = createGuideRecovery(stage, {
|
|
1885
|
+
blocker,
|
|
1886
|
+
nextCommand,
|
|
1887
|
+
noSpendNextCommand,
|
|
1888
|
+
afterNext,
|
|
1889
|
+
escapeHatches,
|
|
1890
|
+
selfFundNextCommand,
|
|
1891
|
+
});
|
|
1892
|
+
return createGuideSuccess(command, quota?.envelope.actor ?? null, {
|
|
1893
|
+
schema:
|
|
1894
|
+
guideOperation === "edit"
|
|
1895
|
+
? "image-skill.edit-guide.v1"
|
|
1896
|
+
: "image-skill.create-guide.v1",
|
|
1807
1897
|
ready: stage === "ready_to_create",
|
|
1808
1898
|
stage,
|
|
1809
1899
|
checks: {
|
|
@@ -1868,6 +1958,7 @@ async function createGuide(args) {
|
|
|
1868
1958
|
selected,
|
|
1869
1959
|
trimmedPrompt,
|
|
1870
1960
|
requestedIntent,
|
|
1961
|
+
guideOperation,
|
|
1871
1962
|
)
|
|
1872
1963
|
: createGuideSelectedModelRequiresInputImage(selected)
|
|
1873
1964
|
? selected.modality === "3d"
|
|
@@ -1882,6 +1973,9 @@ async function createGuide(args) {
|
|
|
1882
1973
|
estimated_provider_usd_per_image: estimatedProviderUsdPerImage,
|
|
1883
1974
|
credit_unit_usd: pricing?.credit_unit_usd ?? CREDIT_UNIT_USD,
|
|
1884
1975
|
pricing_confidence: pricing?.pricing_confidence ?? null,
|
|
1976
|
+
pricing_source: pricing?.pricing_source ?? null,
|
|
1977
|
+
model_parameter_defaults_applied:
|
|
1978
|
+
defaultedModelParameters.defaultsApplied,
|
|
1885
1979
|
},
|
|
1886
1980
|
blocker,
|
|
1887
1981
|
guide_warning: guideWarning,
|
|
@@ -1894,6 +1988,7 @@ async function createGuide(args) {
|
|
|
1894
1988
|
no_spend_next_command_label: noSpendNextCommandLabel,
|
|
1895
1989
|
no_spend_next_command_effect: noSpendNextCommandEffect,
|
|
1896
1990
|
no_spend_evaluation: noSpendEvaluation,
|
|
1991
|
+
guide_recovery: guideRecovery,
|
|
1897
1992
|
recommended_no_spend_command: noSpendNextCommand,
|
|
1898
1993
|
recommended_no_spend_command_label: noSpendNextCommandLabel,
|
|
1899
1994
|
recommended_no_spend_command_effect: noSpendNextCommandEffect,
|
|
@@ -1914,8 +2009,8 @@ async function createGuide(args) {
|
|
|
1914
2009
|
});
|
|
1915
2010
|
}
|
|
1916
2011
|
|
|
1917
|
-
function createGuideSuccess(actor, data) {
|
|
1918
|
-
const result = success(
|
|
2012
|
+
function createGuideSuccess(command, actor, data) {
|
|
2013
|
+
const result = success(command, data);
|
|
1919
2014
|
result.envelope.actor = actor;
|
|
1920
2015
|
return result;
|
|
1921
2016
|
}
|
|
@@ -1923,7 +2018,12 @@ function createGuideSuccess(actor, data) {
|
|
|
1923
2018
|
function selectCreateGuideModel(
|
|
1924
2019
|
models,
|
|
1925
2020
|
requestedModelId,
|
|
1926
|
-
{
|
|
2021
|
+
{
|
|
2022
|
+
operation = "create",
|
|
2023
|
+
prompt = "",
|
|
2024
|
+
intent = undefined,
|
|
2025
|
+
maxEstimatedUsdPerImage = null,
|
|
2026
|
+
} = {},
|
|
1927
2027
|
) {
|
|
1928
2028
|
const isExecutableCreate = (model) =>
|
|
1929
2029
|
model?.status === "available" &&
|
|
@@ -1937,14 +2037,19 @@ function selectCreateGuideModel(
|
|
|
1937
2037
|
(model.supports.includes("edit") || model.supports.includes("variation")) &&
|
|
1938
2038
|
createGuideSelectedModelRequiresInputImage(model);
|
|
1939
2039
|
const isExecutableGuideModel = (model) =>
|
|
1940
|
-
|
|
2040
|
+
operation === "edit"
|
|
2041
|
+
? isExecutableInputImageEdit(model)
|
|
2042
|
+
: isExecutableCreate(model) || isExecutableInputImageEdit(model);
|
|
1941
2043
|
if (requestedModelId !== null) {
|
|
1942
2044
|
const requested = models.find((model) => model.id === requestedModelId);
|
|
1943
2045
|
return requested !== undefined && isExecutableGuideModel(requested)
|
|
1944
2046
|
? requested
|
|
1945
2047
|
: null;
|
|
1946
2048
|
}
|
|
1947
|
-
const candidates =
|
|
2049
|
+
const candidates =
|
|
2050
|
+
operation === "edit"
|
|
2051
|
+
? models.filter(isExecutableInputImageEdit)
|
|
2052
|
+
: models.filter(isExecutableCreate);
|
|
1948
2053
|
if (createGuideImplies3d({ prompt, intent })) {
|
|
1949
2054
|
const eligible3d = guideCandidatesWithinBudget({
|
|
1950
2055
|
candidates: models.filter(
|
|
@@ -2052,6 +2157,221 @@ function guideBudgetUsdForModel(model) {
|
|
|
2052
2157
|
);
|
|
2053
2158
|
}
|
|
2054
2159
|
|
|
2160
|
+
function createGuideDefaultModelParameters(input) {
|
|
2161
|
+
const modelParameters = { ...(input.modelParameters ?? {}) };
|
|
2162
|
+
const defaultsApplied = [];
|
|
2163
|
+
|
|
2164
|
+
if (
|
|
2165
|
+
input.model?.id === "xai.grok-imagine-image-quality" &&
|
|
2166
|
+
modelParameters.resolution === undefined
|
|
2167
|
+
) {
|
|
2168
|
+
const twoKEstimate = createGuideProviderCostEstimateForModel(
|
|
2169
|
+
input.model,
|
|
2170
|
+
{ resolution: "2k" },
|
|
2171
|
+
{ aspectRatio: input.aspectRatio },
|
|
2172
|
+
).estimated_provider_cost_usd;
|
|
2173
|
+
const twoKAllowedByBudget =
|
|
2174
|
+
input.maxEstimatedUsdPerImage === null ||
|
|
2175
|
+
twoKEstimate === null ||
|
|
2176
|
+
twoKEstimate <= input.maxEstimatedUsdPerImage;
|
|
2177
|
+
const intentClass = createGuideIntentClass(input.intent);
|
|
2178
|
+
const resolution =
|
|
2179
|
+
intentClass !== "budget_draft" && twoKAllowedByBudget ? "2k" : "1k";
|
|
2180
|
+
modelParameters.resolution = resolution;
|
|
2181
|
+
defaultsApplied.push(`resolution=${resolution}`);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
if (
|
|
2185
|
+
input.model?.id === "fal.flux-dev" &&
|
|
2186
|
+
modelParameters.image_size === undefined
|
|
2187
|
+
) {
|
|
2188
|
+
const imageSize = falDefaultImageSize(input.aspectRatio);
|
|
2189
|
+
if (imageSize !== null) {
|
|
2190
|
+
modelParameters.image_size = imageSize;
|
|
2191
|
+
defaultsApplied.push(`image_size=${imageSize}`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
if (
|
|
2196
|
+
input.model?.id === "openai.gpt-image-2" &&
|
|
2197
|
+
modelParameters.quality === undefined
|
|
2198
|
+
) {
|
|
2199
|
+
const mediumEstimate = createGuideProviderCostEstimateForModel(
|
|
2200
|
+
input.model,
|
|
2201
|
+
{ ...modelParameters, quality: "medium" },
|
|
2202
|
+
{ aspectRatio: input.aspectRatio },
|
|
2203
|
+
).estimated_provider_cost_usd;
|
|
2204
|
+
const mediumAllowedByBudget =
|
|
2205
|
+
input.maxEstimatedUsdPerImage === null ||
|
|
2206
|
+
mediumEstimate === null ||
|
|
2207
|
+
mediumEstimate <= input.maxEstimatedUsdPerImage;
|
|
2208
|
+
if (mediumAllowedByBudget) {
|
|
2209
|
+
modelParameters.quality = "medium";
|
|
2210
|
+
defaultsApplied.push("quality=medium");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
return { modelParameters, defaultsApplied };
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function createGuidePricingForModel(model, modelParameters, context = {}) {
|
|
2218
|
+
const estimate = createGuideProviderCostEstimateForModel(
|
|
2219
|
+
model,
|
|
2220
|
+
modelParameters,
|
|
2221
|
+
context,
|
|
2222
|
+
);
|
|
2223
|
+
if (estimate.estimated_provider_cost_usd === null) {
|
|
2224
|
+
return createGuideModelCreditPricing(model);
|
|
2225
|
+
}
|
|
2226
|
+
return createGuideCreditPricingForProviderCost({
|
|
2227
|
+
providerCostUsd: estimate.estimated_provider_cost_usd,
|
|
2228
|
+
pricingConfidence: estimate.pricing_confidence,
|
|
2229
|
+
pricingSource: estimate.pricing_source,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function createGuideCanPriceModelParameters(model) {
|
|
2234
|
+
return String(model?.id ?? "").startsWith("xai.grok-imagine-image");
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function createGuideProviderCostEstimateForModel(
|
|
2238
|
+
model,
|
|
2239
|
+
modelParameters = {},
|
|
2240
|
+
context = {},
|
|
2241
|
+
) {
|
|
2242
|
+
if (String(model?.id ?? "").startsWith("xai.grok-imagine-image")) {
|
|
2243
|
+
return createGuideXaiImageCostEstimate(model, modelParameters, context);
|
|
2244
|
+
}
|
|
2245
|
+
return {
|
|
2246
|
+
estimated_provider_cost_usd:
|
|
2247
|
+
typeof model?.economics?.estimated_usd_per_image === "number"
|
|
2248
|
+
? model.economics.estimated_usd_per_image
|
|
2249
|
+
: (createGuideModelCreditPricing(model)?.estimated_provider_cost_usd ??
|
|
2250
|
+
null),
|
|
2251
|
+
pricing_source: "model_registry",
|
|
2252
|
+
pricing_confidence: "known",
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function createGuideXaiImageCostEstimate(model, modelParameters, context) {
|
|
2257
|
+
const modelId = String(model?.id ?? "");
|
|
2258
|
+
const quality = modelId.includes("-quality");
|
|
2259
|
+
const edit = modelId.endsWith("-edit");
|
|
2260
|
+
const resolution = modelParameters?.resolution === "2k" ? "2k" : "1k";
|
|
2261
|
+
const outputImageCount =
|
|
2262
|
+
Number.isInteger(context?.outputCount) && context.outputCount > 0
|
|
2263
|
+
? context.outputCount
|
|
2264
|
+
: 1;
|
|
2265
|
+
const referenceAssetCount =
|
|
2266
|
+
Number.isInteger(context?.referenceAssetCount) &&
|
|
2267
|
+
context.referenceAssetCount > 0
|
|
2268
|
+
? context.referenceAssetCount
|
|
2269
|
+
: 0;
|
|
2270
|
+
const sourceImageCount = edit ? 1 + referenceAssetCount : 0;
|
|
2271
|
+
const inputUsdPerImage = quality ? 0.01 : 0.002;
|
|
2272
|
+
const outputUsdPerImage =
|
|
2273
|
+
quality && resolution === "2k" ? 0.07 : quality ? 0.05 : 0.02;
|
|
2274
|
+
const defaultResolution =
|
|
2275
|
+
modelParameters?.resolution === undefined ||
|
|
2276
|
+
modelParameters?.resolution === null ||
|
|
2277
|
+
modelParameters?.resolution === "1k";
|
|
2278
|
+
const defaultShape =
|
|
2279
|
+
defaultResolution &&
|
|
2280
|
+
outputImageCount === 1 &&
|
|
2281
|
+
sourceImageCount === (edit ? 1 : 0);
|
|
2282
|
+
return {
|
|
2283
|
+
estimated_provider_cost_usd: roundUsdMicro(
|
|
2284
|
+
inputUsdPerImage * sourceImageCount +
|
|
2285
|
+
outputUsdPerImage * outputImageCount,
|
|
2286
|
+
),
|
|
2287
|
+
pricing_source: defaultShape ? "model_registry" : "model_parameters",
|
|
2288
|
+
pricing_confidence: "known",
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function createGuideCreditPricingForProviderCost(input) {
|
|
2293
|
+
const providerCostUsd = roundUsdMicro(input.providerCostUsd);
|
|
2294
|
+
const creditsRequired = Math.max(
|
|
2295
|
+
1,
|
|
2296
|
+
Math.ceil(
|
|
2297
|
+
roundUsdMicro(
|
|
2298
|
+
providerCostUsd / (1 - TARGET_GROSS_MARGIN) / CREDIT_UNIT_USD,
|
|
2299
|
+
),
|
|
2300
|
+
),
|
|
2301
|
+
);
|
|
2302
|
+
const estimatedRevenueUsd = roundUsd(creditsRequired * CREDIT_UNIT_USD);
|
|
2303
|
+
const estimatedPaymentFeeUsd = roundUsdMicro(
|
|
2304
|
+
estimatedRevenueUsd * PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2305
|
+
);
|
|
2306
|
+
const estimatedNetRevenueUsd = roundUsdMicro(
|
|
2307
|
+
estimatedRevenueUsd - estimatedPaymentFeeUsd,
|
|
2308
|
+
);
|
|
2309
|
+
const estimatedGrossMargin =
|
|
2310
|
+
estimatedRevenueUsd > 0
|
|
2311
|
+
? roundRatio(
|
|
2312
|
+
(estimatedRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2313
|
+
)
|
|
2314
|
+
: null;
|
|
2315
|
+
const estimatedFeeAdjustedMargin =
|
|
2316
|
+
estimatedRevenueUsd > 0
|
|
2317
|
+
? roundRatio(
|
|
2318
|
+
(estimatedNetRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
|
|
2319
|
+
)
|
|
2320
|
+
: null;
|
|
2321
|
+
const selfFundBlockReason =
|
|
2322
|
+
estimatedNetRevenueUsd + 1e-9 < providerCostUsd
|
|
2323
|
+
? "payment_fee_margin_negative"
|
|
2324
|
+
: null;
|
|
2325
|
+
return {
|
|
2326
|
+
credits_required: creditsRequired,
|
|
2327
|
+
credit_unit_usd: CREDIT_UNIT_USD,
|
|
2328
|
+
estimated_provider_cost_usd: providerCostUsd,
|
|
2329
|
+
fallback_provider_cost_usd: null,
|
|
2330
|
+
estimated_revenue_usd: estimatedRevenueUsd,
|
|
2331
|
+
estimated_gross_margin: estimatedGrossMargin,
|
|
2332
|
+
payment_fee_rate: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
|
|
2333
|
+
payment_fee_model: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL,
|
|
2334
|
+
estimated_payment_fee_usd: estimatedPaymentFeeUsd,
|
|
2335
|
+
estimated_net_revenue_usd: estimatedNetRevenueUsd,
|
|
2336
|
+
estimated_fee_adjusted_margin: estimatedFeeAdjustedMargin,
|
|
2337
|
+
self_fundable: selfFundBlockReason === null,
|
|
2338
|
+
self_fund_block_reason: selfFundBlockReason,
|
|
2339
|
+
target_gross_margin: TARGET_GROSS_MARGIN,
|
|
2340
|
+
pricing_confidence: input.pricingConfidence,
|
|
2341
|
+
pricing_source: input.pricingSource,
|
|
2342
|
+
margin_model: "provider_cost_plus_margin",
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function falDefaultImageSize(aspectRatio) {
|
|
2347
|
+
switch (aspectRatio) {
|
|
2348
|
+
case "1:1":
|
|
2349
|
+
return "square_hd";
|
|
2350
|
+
case "4:3":
|
|
2351
|
+
return "landscape_4_3";
|
|
2352
|
+
case "3:4":
|
|
2353
|
+
return "portrait_4_3";
|
|
2354
|
+
case "16:9":
|
|
2355
|
+
return "landscape_16_9";
|
|
2356
|
+
case "9:16":
|
|
2357
|
+
return "portrait_16_9";
|
|
2358
|
+
default:
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function roundUsd(value) {
|
|
2364
|
+
return Math.round(value * 100) / 100;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function roundRatio(value) {
|
|
2368
|
+
return Math.round(value * 1000) / 1000;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function roundUsdMicro(value) {
|
|
2372
|
+
return Math.round(value * 1_000_000) / 1_000_000;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2055
2375
|
function guideModelExecutionStatus(model) {
|
|
2056
2376
|
if (
|
|
2057
2377
|
isRecord(model?.execution) &&
|
|
@@ -2139,7 +2459,12 @@ function createGuideSelectedModelRequiresInputImage(model) {
|
|
|
2139
2459
|
);
|
|
2140
2460
|
}
|
|
2141
2461
|
|
|
2142
|
-
function createGuideSelectionReason(
|
|
2462
|
+
function createGuideSelectionReason(
|
|
2463
|
+
model,
|
|
2464
|
+
prompt,
|
|
2465
|
+
intent,
|
|
2466
|
+
operation = "create",
|
|
2467
|
+
) {
|
|
2143
2468
|
if (
|
|
2144
2469
|
createGuideSelectedModelRequiresInputImage(model) &&
|
|
2145
2470
|
createGuideImplies3d({ prompt, intent })
|
|
@@ -2167,7 +2492,9 @@ function createGuideSelectionReason(model, prompt, intent) {
|
|
|
2167
2492
|
? "guide selected a draft/budget create model with high-definition defaults"
|
|
2168
2493
|
: "guide selected the strongest currently available quality-first create model for this intent";
|
|
2169
2494
|
}
|
|
2170
|
-
return
|
|
2495
|
+
return operation === "edit"
|
|
2496
|
+
? "guide selected the first available executable input-image edit model"
|
|
2497
|
+
: "guide selected the first available executable create model";
|
|
2171
2498
|
}
|
|
2172
2499
|
|
|
2173
2500
|
function createGuidePaymentSummary(data, commandPrefix) {
|
|
@@ -2382,7 +2709,7 @@ function renderCopyRunnablePaymentCommand(commandPrefix, command) {
|
|
|
2382
2709
|
}
|
|
2383
2710
|
|
|
2384
2711
|
function createGuideStage(input) {
|
|
2385
|
-
if (input.
|
|
2712
|
+
if (input.promptRequired) {
|
|
2386
2713
|
return "prompt_required";
|
|
2387
2714
|
}
|
|
2388
2715
|
if (!input.health.envelope.ok || !input.models.envelope.ok) {
|
|
@@ -2789,6 +3116,64 @@ function createGuideNoSpendEvaluation(stage, input) {
|
|
|
2789
3116
|
};
|
|
2790
3117
|
}
|
|
2791
3118
|
|
|
3119
|
+
function createGuideRecovery(stage, input) {
|
|
3120
|
+
let noSpendCommand = null;
|
|
3121
|
+
let noSpendCommandField = null;
|
|
3122
|
+
if (stage === "ready_to_create" && input.noSpendNextCommand !== null) {
|
|
3123
|
+
noSpendCommand = input.noSpendNextCommand;
|
|
3124
|
+
noSpendCommandField = "recommended_no_spend_command";
|
|
3125
|
+
} else if (stage === "quota_required") {
|
|
3126
|
+
noSpendCommand = input.escapeHatches.quota;
|
|
3127
|
+
noSpendCommandField = "escape_hatches.quota";
|
|
3128
|
+
} else if (
|
|
3129
|
+
stage === "no_executable_model" ||
|
|
3130
|
+
stage === "service_unreachable"
|
|
3131
|
+
) {
|
|
3132
|
+
noSpendCommand = input.nextCommand;
|
|
3133
|
+
noSpendCommandField = "next_command";
|
|
3134
|
+
} else if (stage === "auth_required" || stage === "prompt_required") {
|
|
3135
|
+
noSpendCommand = input.nextCommand;
|
|
3136
|
+
noSpendCommandField = "next_command";
|
|
3137
|
+
}
|
|
3138
|
+
const noSpendMissingInputs =
|
|
3139
|
+
noSpendCommand === null
|
|
3140
|
+
? []
|
|
3141
|
+
: createGuideNextCommandMissingInputs(noSpendCommand);
|
|
3142
|
+
const liveCreateCommandField =
|
|
3143
|
+
stage === "ready_to_create" ? "next_command" : null;
|
|
3144
|
+
const livePaymentCommandField =
|
|
3145
|
+
stage === "quota_required" && input.selfFundNextCommand !== null
|
|
3146
|
+
? "self_fund_next_command"
|
|
3147
|
+
: null;
|
|
3148
|
+
const doubleSpendGuardRequired =
|
|
3149
|
+
liveCreateCommandField !== null || livePaymentCommandField !== null;
|
|
3150
|
+
return {
|
|
3151
|
+
schema: "image-skill.guide-recovery.v1",
|
|
3152
|
+
stage,
|
|
3153
|
+
precondition_code: input.blocker?.code ?? null,
|
|
3154
|
+
precondition_message: input.blocker?.message ?? null,
|
|
3155
|
+
no_spend_command: noSpendCommand,
|
|
3156
|
+
no_spend_command_field: noSpendCommandField,
|
|
3157
|
+
no_spend_command_copy_runnable:
|
|
3158
|
+
noSpendCommand === null ? null : noSpendMissingInputs.length === 0,
|
|
3159
|
+
no_spend_command_missing_inputs: noSpendMissingInputs,
|
|
3160
|
+
after_success_command: input.afterNext,
|
|
3161
|
+
after_success_command_field: input.afterNext === null ? null : "after_next",
|
|
3162
|
+
live_create_command_field: liveCreateCommandField,
|
|
3163
|
+
live_payment_command_field: livePaymentCommandField,
|
|
3164
|
+
double_spend_guard: {
|
|
3165
|
+
required: doubleSpendGuardRequired,
|
|
3166
|
+
safe_rerun_command_field: noSpendCommandField,
|
|
3167
|
+
warning:
|
|
3168
|
+
liveCreateCommandField !== null
|
|
3169
|
+
? "Do not blindly rerun data.next_command after a partial or unknown create/edit failure; use data.guide_recovery.no_spend_command, jobs/activity, or error.recovery before any live retry."
|
|
3170
|
+
: livePaymentCommandField !== null
|
|
3171
|
+
? "Do not blindly rerun live payment commands with fresh identifiers after a partial or unknown payment failure; use data.guide_recovery.no_spend_command and payment status recovery before any new buy."
|
|
3172
|
+
: "No live payment or live media command is exposed for this stage; follow the no-spend command and rerun the guide after the precondition is satisfied.",
|
|
3173
|
+
},
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
|
|
2792
3177
|
function createGuideWarning(stage, input) {
|
|
2793
3178
|
const effect = input.nextCommandEffect;
|
|
2794
3179
|
const base = {
|
|
@@ -2909,10 +3294,13 @@ function escapeRegExp(value) {
|
|
|
2909
3294
|
function createGuideNextCommand(stage, input) {
|
|
2910
3295
|
if (stage === "prompt_required") {
|
|
2911
3296
|
return renderGuideCommand("PROMPT", input.apiBaseUrl, input.commandPrefix, {
|
|
3297
|
+
operation: input.guideOperation,
|
|
3298
|
+
inputReference: input.inputReference,
|
|
2912
3299
|
modelId: input.requestedModelId,
|
|
2913
3300
|
providerId: input.requestedProviderId,
|
|
2914
3301
|
intent: input.requestedIntentFlag,
|
|
2915
3302
|
maxEstimatedUsdPerImage: input.maxEstimatedUsdPerImage,
|
|
3303
|
+
modelParametersJson: input.modelParametersJson,
|
|
2916
3304
|
});
|
|
2917
3305
|
}
|
|
2918
3306
|
if (stage === "no_executable_model" || stage === "service_unreachable") {
|
|
@@ -2934,7 +3322,9 @@ function createGuideNextCommand(stage, input) {
|
|
|
2934
3322
|
return renderInputImageGuideCommand({
|
|
2935
3323
|
modelId: input.selected.id,
|
|
2936
3324
|
prompt: input.prompt,
|
|
3325
|
+
inputReference: input.inputReference,
|
|
2937
3326
|
budgetGuard: input.budgetGuard,
|
|
3327
|
+
modelParametersJson: input.modelParametersJson,
|
|
2938
3328
|
dryRun: false,
|
|
2939
3329
|
idempotencyKey: `edit-guide-${Date.now()}-${randomBytes(4).toString("hex")}`,
|
|
2940
3330
|
apiBaseUrl: input.apiBaseUrl,
|
|
@@ -2948,6 +3338,7 @@ function createGuideNextCommand(stage, input) {
|
|
|
2948
3338
|
intent: input.requestedIntent,
|
|
2949
3339
|
budgetGuard: input.budgetGuard,
|
|
2950
3340
|
aspectRatio: input.aspectRatio,
|
|
3341
|
+
modelParametersJson: input.modelParametersJson,
|
|
2951
3342
|
dryRun: false,
|
|
2952
3343
|
// Retry-safe by default (#1228): bake a stable idempotency key into the
|
|
2953
3344
|
// advertised create command so an agent that copies it and retries after a
|
|
@@ -2977,16 +3368,22 @@ function createGuideEscapeHatches(input) {
|
|
|
2977
3368
|
"usage quota --json",
|
|
2978
3369
|
),
|
|
2979
3370
|
dry_run:
|
|
2980
|
-
input.selected === null ||
|
|
3371
|
+
input.selected === null ||
|
|
3372
|
+
(input.prompt.length === 0 &&
|
|
3373
|
+
!PROMPTLESS_EDIT_MODEL_IDS.has(input.selected.id))
|
|
2981
3374
|
? renderGuidePrefixedCommand(
|
|
2982
3375
|
input.commandPrefix,
|
|
2983
|
-
|
|
3376
|
+
input.guideOperation === "edit"
|
|
3377
|
+
? "edit --dry-run --input image_... --prompt PROMPT --json"
|
|
3378
|
+
: "create --dry-run --prompt PROMPT --json",
|
|
2984
3379
|
)
|
|
2985
3380
|
: createGuideSelectedModelRequiresInputImage(input.selected)
|
|
2986
3381
|
? renderInputImageGuideCommand({
|
|
2987
3382
|
modelId: input.selected.id,
|
|
2988
3383
|
prompt: input.prompt,
|
|
3384
|
+
inputReference: input.inputReference,
|
|
2989
3385
|
budgetGuard: input.budgetGuard,
|
|
3386
|
+
modelParametersJson: input.modelParametersJson,
|
|
2990
3387
|
dryRun: true,
|
|
2991
3388
|
apiBaseUrl: input.apiBaseUrl,
|
|
2992
3389
|
commandPrefix: input.commandPrefix,
|
|
@@ -2998,6 +3395,7 @@ function createGuideEscapeHatches(input) {
|
|
|
2998
3395
|
intent: input.requestedIntent,
|
|
2999
3396
|
budgetGuard: input.budgetGuard,
|
|
3000
3397
|
aspectRatio: input.aspectRatio,
|
|
3398
|
+
modelParametersJson: input.modelParametersJson,
|
|
3001
3399
|
dryRun: true,
|
|
3002
3400
|
apiBaseUrl: input.apiBaseUrl,
|
|
3003
3401
|
commandPrefix: input.commandPrefix,
|
|
@@ -3011,10 +3409,16 @@ function renderGuideCommand(
|
|
|
3011
3409
|
commandPrefix = "image-skill",
|
|
3012
3410
|
options = {},
|
|
3013
3411
|
) {
|
|
3412
|
+
const operation = options.operation ?? "create";
|
|
3014
3413
|
return [
|
|
3015
3414
|
commandPrefix,
|
|
3016
|
-
|
|
3415
|
+
`${operation} --guide --prompt`,
|
|
3017
3416
|
shellQuote(prompt),
|
|
3417
|
+
...(operation === "edit" &&
|
|
3418
|
+
typeof options.inputReference === "string" &&
|
|
3419
|
+
options.inputReference.trim().length > 0
|
|
3420
|
+
? ["--input", shellQuote(options.inputReference.trim())]
|
|
3421
|
+
: []),
|
|
3018
3422
|
...(options.modelId === null ||
|
|
3019
3423
|
options.modelId === undefined ||
|
|
3020
3424
|
options.modelId === ""
|
|
@@ -3037,6 +3441,10 @@ function renderGuideCommand(
|
|
|
3037
3441
|
"--max-estimated-usd-per-image",
|
|
3038
3442
|
shellQuote(formatUsd(options.maxEstimatedUsdPerImage)),
|
|
3039
3443
|
]),
|
|
3444
|
+
...(options.modelParametersJson === null ||
|
|
3445
|
+
options.modelParametersJson === undefined
|
|
3446
|
+
? []
|
|
3447
|
+
: ["--model-parameters-json", shellQuote(options.modelParametersJson)]),
|
|
3040
3448
|
...(apiBaseUrl === null ? [] : ["--api-base-url", shellQuote(apiBaseUrl)]),
|
|
3041
3449
|
"--json",
|
|
3042
3450
|
].join(" ");
|
|
@@ -3091,12 +3499,18 @@ function renderInputImageGuideCommand(input) {
|
|
|
3091
3499
|
"edit",
|
|
3092
3500
|
...(input.dryRun ? ["--dry-run"] : []),
|
|
3093
3501
|
"--input",
|
|
3094
|
-
|
|
3502
|
+
input.inputReference?.trim()
|
|
3503
|
+
? shellQuote(input.inputReference.trim())
|
|
3504
|
+
: "image_...",
|
|
3095
3505
|
"--model",
|
|
3096
3506
|
shellQuote(input.modelId),
|
|
3097
3507
|
...(promptless ? [] : ["--prompt", shellQuote(input.prompt)]),
|
|
3098
3508
|
"--max-estimated-usd-per-image",
|
|
3099
3509
|
shellQuote(formatUsd(input.budgetGuard)),
|
|
3510
|
+
...(input.modelParametersJson === null ||
|
|
3511
|
+
input.modelParametersJson === undefined
|
|
3512
|
+
? []
|
|
3513
|
+
: ["--model-parameters-json", shellQuote(input.modelParametersJson)]),
|
|
3100
3514
|
...(input.idempotencyKey === undefined || input.idempotencyKey === null
|
|
3101
3515
|
? []
|
|
3102
3516
|
: ["--idempotency-key", shellQuote(input.idempotencyKey)]),
|
|
@@ -3126,6 +3540,10 @@ function renderCreateCommand(input) {
|
|
|
3126
3540
|
: ["--aspect-ratio", shellQuote(input.aspectRatio)]),
|
|
3127
3541
|
"--max-estimated-usd-per-image",
|
|
3128
3542
|
shellQuote(formatUsd(input.budgetGuard)),
|
|
3543
|
+
...(input.modelParametersJson === null ||
|
|
3544
|
+
input.modelParametersJson === undefined
|
|
3545
|
+
? []
|
|
3546
|
+
: ["--model-parameters-json", shellQuote(input.modelParametersJson)]),
|
|
3129
3547
|
...(input.idempotencyKey === undefined || input.idempotencyKey === null
|
|
3130
3548
|
? []
|
|
3131
3549
|
: ["--idempotency-key", shellQuote(input.idempotencyKey)]),
|
|
@@ -3361,6 +3779,12 @@ async function edit(argv) {
|
|
|
3361
3779
|
const args = parseArgs(argv);
|
|
3362
3780
|
const input = flagString(args, "input") ?? args.positionals[0];
|
|
3363
3781
|
const modelId = flagString(args, "model");
|
|
3782
|
+
if (flagBool(args, "guide")) {
|
|
3783
|
+
return createGuide(args, {
|
|
3784
|
+
guideOperation: "edit",
|
|
3785
|
+
inputReference: input,
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3364
3788
|
if (input === undefined) {
|
|
3365
3789
|
return invalid(
|
|
3366
3790
|
"image-skill edit",
|
|
@@ -4575,6 +4999,169 @@ function inFlightSpendFileName(idempotencyKey) {
|
|
|
4575
4999
|
return `${trimmed.length === 0 ? "key" : trimmed}.json`;
|
|
4576
5000
|
}
|
|
4577
5001
|
|
|
5002
|
+
async function inFlightSpendDoctorReport(input) {
|
|
5003
|
+
const dir = inFlightSpendDir();
|
|
5004
|
+
const now = input.now ?? new Date();
|
|
5005
|
+
const files = await readdir(dir).catch((error) => {
|
|
5006
|
+
if (error?.code === "ENOENT") {
|
|
5007
|
+
return [];
|
|
5008
|
+
}
|
|
5009
|
+
return null;
|
|
5010
|
+
});
|
|
5011
|
+
if (files === null) {
|
|
5012
|
+
return {
|
|
5013
|
+
schema: "image-skill.in-flight-spend-report.v1",
|
|
5014
|
+
directory: dir,
|
|
5015
|
+
count: null,
|
|
5016
|
+
recoverable_count: null,
|
|
5017
|
+
ttl_elapsed_count: null,
|
|
5018
|
+
sweep_eligible_count: null,
|
|
5019
|
+
invalid_count: null,
|
|
5020
|
+
entries: [],
|
|
5021
|
+
error: "in-flight directory could not be read",
|
|
5022
|
+
reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
|
|
5023
|
+
sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
|
|
5024
|
+
swept_count: 0,
|
|
5025
|
+
sweep_requested: input.sweep === true,
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
const entries = [];
|
|
5030
|
+
let invalidCount = 0;
|
|
5031
|
+
let sweptCount = 0;
|
|
5032
|
+
for (const file of files.sort()) {
|
|
5033
|
+
if (!file.endsWith(".json")) {
|
|
5034
|
+
continue;
|
|
5035
|
+
}
|
|
5036
|
+
const path = join(dir, file);
|
|
5037
|
+
const entry = await readInFlightSpendEntry({ path, file, now });
|
|
5038
|
+
if (entry === null) {
|
|
5039
|
+
invalidCount += 1;
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
if (input.sweep === true && entry.sweep_eligible === true) {
|
|
5043
|
+
await rm(path, { force: true }).catch(() => {});
|
|
5044
|
+
sweptCount += 1;
|
|
5045
|
+
continue;
|
|
5046
|
+
}
|
|
5047
|
+
entries.push(entry);
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5050
|
+
return {
|
|
5051
|
+
schema: "image-skill.in-flight-spend-report.v1",
|
|
5052
|
+
directory: dir,
|
|
5053
|
+
count: entries.length,
|
|
5054
|
+
recoverable_count: entries.filter((entry) => entry.state === "recoverable")
|
|
5055
|
+
.length,
|
|
5056
|
+
ttl_elapsed_count: entries.filter((entry) => entry.state === "ttl_elapsed")
|
|
5057
|
+
.length,
|
|
5058
|
+
sweep_eligible_count: entries.filter((entry) => entry.sweep_eligible)
|
|
5059
|
+
.length,
|
|
5060
|
+
invalid_count: invalidCount,
|
|
5061
|
+
swept_count: sweptCount,
|
|
5062
|
+
reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
|
|
5063
|
+
sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
|
|
5064
|
+
sweep_requested: input.sweep === true,
|
|
5065
|
+
entries,
|
|
5066
|
+
note:
|
|
5067
|
+
entries.length === 0
|
|
5068
|
+
? "no in-flight live spend breadcrumbs found"
|
|
5069
|
+
: "rerun an entry's recover_command to settle or inspect a maybe-reserved spend before sweeping it",
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
async function readInFlightSpendEntry({ path, file, now }) {
|
|
5074
|
+
let parsed;
|
|
5075
|
+
let fileStat;
|
|
5076
|
+
try {
|
|
5077
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
5078
|
+
fileStat = await stat(path);
|
|
5079
|
+
} catch {
|
|
5080
|
+
return null;
|
|
5081
|
+
}
|
|
5082
|
+
if (
|
|
5083
|
+
parsed?.schema !== "image-skill.in-flight-spend.v1" ||
|
|
5084
|
+
typeof parsed.idempotency_key !== "string" ||
|
|
5085
|
+
typeof parsed.operation !== "string"
|
|
5086
|
+
) {
|
|
5087
|
+
return null;
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5090
|
+
const startedAt =
|
|
5091
|
+
typeof parsed.started_at === "string" ? parsed.started_at : null;
|
|
5092
|
+
const startedTime =
|
|
5093
|
+
startedAt === null ? Number.NaN : new Date(startedAt).getTime();
|
|
5094
|
+
const fallbackTime = fileStat.mtime.getTime();
|
|
5095
|
+
const basisTime = Number.isFinite(startedTime) ? startedTime : fallbackTime;
|
|
5096
|
+
const ageMs = Math.max(0, now.getTime() - basisTime);
|
|
5097
|
+
const state =
|
|
5098
|
+
ageMs >= IN_FLIGHT_RESERVATION_TTL_MS ? "ttl_elapsed" : "recoverable";
|
|
5099
|
+
const sweepEligible = ageMs >= IN_FLIGHT_SWEEP_AFTER_MS;
|
|
5100
|
+
const argv = Array.isArray(parsed.argv)
|
|
5101
|
+
? parsed.argv.filter((value) => typeof value === "string")
|
|
5102
|
+
: [];
|
|
5103
|
+
const recoverCommand = renderRecoverCommand({
|
|
5104
|
+
operation: parsed.operation,
|
|
5105
|
+
argv,
|
|
5106
|
+
idempotencyKey: parsed.idempotency_key,
|
|
5107
|
+
fallback: parsed.recover_command,
|
|
5108
|
+
});
|
|
5109
|
+
|
|
5110
|
+
return {
|
|
5111
|
+
file,
|
|
5112
|
+
path,
|
|
5113
|
+
operation: parsed.operation,
|
|
5114
|
+
command:
|
|
5115
|
+
typeof parsed.command === "string"
|
|
5116
|
+
? parsed.command
|
|
5117
|
+
: `image-skill ${parsed.operation}`,
|
|
5118
|
+
idempotency_key: parsed.idempotency_key,
|
|
5119
|
+
started_at: startedAt,
|
|
5120
|
+
age_ms: ageMs,
|
|
5121
|
+
state,
|
|
5122
|
+
sweep_eligible: sweepEligible,
|
|
5123
|
+
recover_command: recoverCommand,
|
|
5124
|
+
original_recover_command:
|
|
5125
|
+
typeof parsed.recover_command === "string"
|
|
5126
|
+
? parsed.recover_command
|
|
5127
|
+
: null,
|
|
5128
|
+
warning:
|
|
5129
|
+
state === "recoverable"
|
|
5130
|
+
? "the hosted reservation TTL has not elapsed; recover before cleanup"
|
|
5131
|
+
: sweepEligible
|
|
5132
|
+
? "reservation TTL has long elapsed; recover first if the original result still matters, or run doctor --sweep-in-flight to remove this breadcrumb"
|
|
5133
|
+
: "reservation TTL has elapsed; recover if you need the result, otherwise leave it until it becomes sweep-eligible",
|
|
5134
|
+
};
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
function renderRecoverCommand(input) {
|
|
5138
|
+
const argv = withRecoveryArgs(input.argv, input.idempotencyKey);
|
|
5139
|
+
if (argv.length === 0 && typeof input.fallback === "string") {
|
|
5140
|
+
return input.fallback;
|
|
5141
|
+
}
|
|
5142
|
+
return renderImageSkillCommand(input.operation, argv);
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function withRecoveryArgs(argv, idempotencyKey) {
|
|
5146
|
+
const args = [...argv];
|
|
5147
|
+
const hasIdempotency = args.some(
|
|
5148
|
+
(arg) =>
|
|
5149
|
+
arg === "--idempotency-key" || arg.startsWith("--idempotency-key="),
|
|
5150
|
+
);
|
|
5151
|
+
if (!hasIdempotency) {
|
|
5152
|
+
args.push("--idempotency-key", idempotencyKey);
|
|
5153
|
+
}
|
|
5154
|
+
const hasJson = args.some((arg) => arg === "--json");
|
|
5155
|
+
if (!hasJson) {
|
|
5156
|
+
args.push("--json");
|
|
5157
|
+
}
|
|
5158
|
+
return args;
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
function renderImageSkillCommand(operation, argv) {
|
|
5162
|
+
return ["image-skill", operation, ...argv.map(shellQuote)].join(" ");
|
|
5163
|
+
}
|
|
5164
|
+
|
|
4578
5165
|
async function recordInFlightSpend(input) {
|
|
4579
5166
|
const { command, operation, idempotencyKey, argv } = input;
|
|
4580
5167
|
const recoverCommand = recoverCommandFor(operation, idempotencyKey);
|