@genex-ai/cli-demo 1.36.10 → 1.36.11-dev.968
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/dist/{blender-mcp-QVEGHE5L.js → blender-mcp-WSURPZET.js} +2 -2
- package/dist/{blender-serve-66OHC4ML.js → blender-serve-EH2MOW3U.js} +1 -1
- package/dist/{chunk-4BBTPRJW.js → chunk-EABIVAGT.js} +2 -2
- package/dist/{chunk-QI7FIYBY.js → chunk-K4EHXOZK.js} +1 -1
- package/dist/index.js +1325 -295
- package/package.json +1 -1
- package/templates/skills/genex-game-director/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-llm-in-games/SKILL.md +209 -72
- package/templates/skills/genex-llm-in-games/references/pricing.md +126 -85
- package/templates/skills/genex-monetization/SKILL.md +1 -9
- package/templates/skills/genex-tool-llm/SKILL.md +11 -9
- package/templates/skills/genex-updates/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
writeSecretFile,
|
|
58
58
|
writeUserToken,
|
|
59
59
|
writeWorkspace
|
|
60
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-EABIVAGT.js";
|
|
61
61
|
import {
|
|
62
62
|
CLI_CHANNEL,
|
|
63
63
|
DEFAULT_API_URL,
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
normalizeApiOrigin,
|
|
79
79
|
originKey,
|
|
80
80
|
resolveAgentTargets
|
|
81
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-K4EHXOZK.js";
|
|
82
82
|
|
|
83
83
|
// src/instrument.ts
|
|
84
84
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -219,8 +219,8 @@ var pending = /* @__PURE__ */ new Set();
|
|
|
219
219
|
function setSentryTag(key, value) {
|
|
220
220
|
tags[key] = value;
|
|
221
221
|
}
|
|
222
|
-
function setSentryContext(name,
|
|
223
|
-
contexts[name] =
|
|
222
|
+
function setSentryContext(name, context4) {
|
|
223
|
+
contexts[name] = context4;
|
|
224
224
|
}
|
|
225
225
|
function beforeSend(event, hint) {
|
|
226
226
|
const e = event;
|
|
@@ -1107,7 +1107,7 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
1107
1107
|
21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
|
|
1108
1108
|
22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Start with one reusable implementation per distinct required gameplay or visual role. Background populations default to reusable procedural bodies and motion or compatible existing rigged assets. Catalog clips need a compatible skeleton and may be paid. Reserve paid custom bodies for the player and characters examined or interacted with closely, while honoring explicit user requests. Add variants only for an unmet requirement; unspent allowance is not unfinished work. Allowance questions use timeoutPolicy no-consent: silence does not raise the allowance. Record role, reuse and completion criteria in the existing Assets table. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
|
|
1109
1109
|
23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18). One file in \`.genex/scratch/\` IS read, by the platform: \`.genex/scratch/cover.png\` is the game's cover \u2014 the frame you would put on its poster: real gameplay at its signature moment, well lit, no menus, popups or busy HUD, landscape 16:9, and never any text (the page sets the title beside it). The next \`genex preview\` makes it the cover; save a better moment over it whenever one comes along, and if your browser tool can set the cover itself, use that instead. Before the first publish, and whenever the game's look changes, load \`$genex-cover\`: it chooses, stages, lights and judges that frame.
|
|
1110
|
-
24. **Benchmark before declaring an in-game LLM
|
|
1110
|
+
24. **Benchmark before declaring an in-game LLM ceiling.** If the game calls a language model while the player plays \u2014 an NPC answering in its own words, a quest written for this save, a judge reading what the player typed \u2014 the PLAYER pays for it from their Genex credits, billed as used, and the game declares no price, only a per-call CEILING: \`maxCredits\` on \`generate()\`, \`perCallMaxCredits\` on a standing budget. That ceiling bounds what one call may cost AND funds how long its answer may be, so never pick it by judgement or from a vendor's rate card. Run the real prompt on your own credits \u2014 \`npx genex llm bench "<the prompt>" --schema <file> --samples 3 --max-credits <n> --user-approved\` \u2014 and declare the figures it prints, recorded in \`DESIGN.md\` with its model and date; re-benchmark whenever the prompt, the schema or the model changes, because a prompt edit changes the ceiling. Check the lane first with \`npx genex llm models\`: where it is off the routes 404, so build the feature behind a graceful unavailable state and say so in the handoff. A repeated call is a standing budget the player approves once (\`requestSpendGrant()\`, then \`generate({ grantId })\`), never a popup per turn, and its disclosed call rate and credits per period are computed from the game's own loop and the bench's measured average. The coin spellings (\`estimateCoins\`, \`perCallMaxCoins\`) are deprecated \u2014 write the credit names. Model output is data the game validates against its own expectation \u2014 never executed, and never authority over coin, credits, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
|
|
1111
1111
|
${CONTRACT_END}
|
|
1112
1112
|
`;
|
|
1113
1113
|
var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
|
|
@@ -1121,7 +1121,7 @@ var TOOLS_CONTRACT_OFFER = `5. **When the game is built and playable \u2014 neve
|
|
|
1121
1121
|
var TOOLS_CONTRACT_HOSTED_LAWS = `5. **This game is hosted on Genex now, and publishing is glue \u2014 never a rebuild.** The game ships exactly as the user built it: \`initEmbed()\` first in the boot code (the \`genex-threejs-embed-auth\` card has the call; the renderer draws before any \`await waitForPlayer()\`), a static build into \`dist/\` with relative asset paths, then \`npx genex preview\`. Never restructure, reformat or "improve" the game to ship it, never scaffold a new app around it, and never start a design document or a build plan for it \u2014 the user's own process is theirs. Load the \`genex-tool-publish\` card before the first preview; it owns the vocabulary, the links and the limits.
|
|
1122
1122
|
6. **Ship first, then report.** \`preview\` prints a preflight \u2014 phone memory, a missing volume slider, an asset nobody wired, a viewport line. It never blocks a deploy, so push the build as it is, hand over the link, THEN relay each preflight line to the user in one plain sentence with an offer to fix it, and fix one only on their yes. The preflight is a report for the user, not a to-do list for you.
|
|
1123
1123
|
7. **The link is the game's page, and there are two versions.** After every preview give the user \`<dashboard>/draft/<slug>\` \u2014 \`dashboardOrigins[0]\` and \`slug\` from \`.genex/project.json\` \u2014 never localhost, a file path or the bare play origin. \`preview\` updates the draft and never touches what players are on; the first release is \`npx genex publish\` (it lists the game); after that "publish it", "update it" and "yes" all mean \`npx genex promote\` \u2014 the exact draft build, no rebuild. Ask once per round of work, in one line, and keep working while you wait.`;
|
|
1124
|
-
var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with Genex
|
|
1124
|
+
var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with their Genex credits or their own Claude/ChatGPT subscription, and approves it on a Genex sheet; your game just calls \`generate()\`. Want it that way?`;
|
|
1125
1125
|
function toolsContractLlmLaw(n, hosted) {
|
|
1126
1126
|
const onYes = hosted ? "On a yes, load `$genex-tool-llm` and then `$genex-llm-in-games`, which owns the build." : "On a yes, load `$genex-tool-llm`, which owns the rest: it checks the lane with `npx genex llm models` first, then runs `npx genex init --convert` (a hosted game is a static browser build \u2014 a local server holding a key can never ship).";
|
|
1127
1127
|
return `${n}. **A model running while people PLAY is a platform feature \u2014 never something you wire onto the user's own meter.** When a request implies one (NPCs that talk or decide in their own words, content written from what the player types, a prompt box in the game, "let the player pick a model"), recognise it, say in ONE line: "${TOOLS_CONTRACT_LLM_OFFER}" \u2014 then ASK and wait. ${onYes} On a no, build the authored version instead. Either way, never ship a game that calls a model on a key or an account of the user's own: every visitor would spend their money with nobody approving it, and the credential is readable in the bundle. Player-funded generation is text and JSON only \u2014 3D, images, video and audio stay the asset lanes above, on the user's meter.`;
|
|
@@ -1924,8 +1924,8 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
|
|
|
1924
1924
|
const spec = `${CLI_PACKAGE}@${CLI_CHANNEL}`;
|
|
1925
1925
|
const { command, args } = installArgs(pm, spec);
|
|
1926
1926
|
log.step(`Adding ${c.cyan(CLI_PACKAGE)} as a devDependency (${pm}) so npx genex resolves here\u2026`);
|
|
1927
|
-
const
|
|
1928
|
-
const result = await
|
|
1927
|
+
const run5 = opts.runner ?? spawnInstall;
|
|
1928
|
+
const result = await run5(command, args, cwd);
|
|
1929
1929
|
if (result.ok) {
|
|
1930
1930
|
log.success(`${c.cyan("npx genex")} now resolves in this folder.`);
|
|
1931
1931
|
} else {
|
|
@@ -2255,13 +2255,13 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
2255
2255
|
}
|
|
2256
2256
|
async function trailingFailures(kind, cwd = process.cwd()) {
|
|
2257
2257
|
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
2258
|
-
let
|
|
2258
|
+
let run5 = 0;
|
|
2259
2259
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
2260
2260
|
const status2 = rows[i].status;
|
|
2261
2261
|
if (status2 === "completed") break;
|
|
2262
|
-
if (status2 === "failed")
|
|
2262
|
+
if (status2 === "failed") run5 += 1;
|
|
2263
2263
|
}
|
|
2264
|
-
return
|
|
2264
|
+
return run5;
|
|
2265
2265
|
}
|
|
2266
2266
|
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
2267
2267
|
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
@@ -3076,15 +3076,15 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
3076
3076
|
detail: `repeat.set(${m[1]}, ${m[2]})`
|
|
3077
3077
|
});
|
|
3078
3078
|
}
|
|
3079
|
-
for (const
|
|
3080
|
-
if (/\b(map|alphaMap)\s*:/.test(
|
|
3079
|
+
for (const call2 of ctorArgs(content, "PointsMaterial")) {
|
|
3080
|
+
if (/\b(map|alphaMap)\s*:/.test(call2.text)) continue;
|
|
3081
3081
|
found.squarePoints.push({
|
|
3082
|
-
where: `${rel}:${lineOf(content,
|
|
3082
|
+
where: `${rel}:${lineOf(content, call2.index)}`,
|
|
3083
3083
|
detail: "PointsMaterial with no map/alphaMap"
|
|
3084
3084
|
});
|
|
3085
3085
|
}
|
|
3086
|
-
for (const
|
|
3087
|
-
const args = splitArgs(
|
|
3086
|
+
for (const call2 of ctorArgs(content, "PerspectiveCamera")) {
|
|
3087
|
+
const args = splitArgs(call2.text);
|
|
3088
3088
|
if (args.length < 4) continue;
|
|
3089
3089
|
const near = Number(args[2]);
|
|
3090
3090
|
const far = Number(args[3]);
|
|
@@ -3092,7 +3092,7 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
3092
3092
|
const ratio = far / near;
|
|
3093
3093
|
if (ratio <= DEPTH_RATIO_LIMIT) continue;
|
|
3094
3094
|
found.depthRange.push({
|
|
3095
|
-
where: `${rel}:${lineOf(content,
|
|
3095
|
+
where: `${rel}:${lineOf(content, call2.index)}`,
|
|
3096
3096
|
detail: `near ${near} / far ${far}`,
|
|
3097
3097
|
ratio
|
|
3098
3098
|
});
|
|
@@ -3387,7 +3387,7 @@ async function detectShellApis(cwd = process.cwd()) {
|
|
|
3387
3387
|
for (const m of content.matchAll(DOWNLOAD_ASSIGN)) add(found.download, m.index, "a.download");
|
|
3388
3388
|
}
|
|
3389
3389
|
for (const m of content.matchAll(DOWNLOAD_ATTR)) add(found.download, m.index, "a download link");
|
|
3390
|
-
for (const
|
|
3390
|
+
for (const call2 of dialogCalls(content)) add(found.dialogs, call2.index, `${call2.name}()`);
|
|
3391
3391
|
for (const m of content.matchAll(SERVICE_WORKER)) {
|
|
3392
3392
|
add(found.serviceWorker, m.index, "serviceWorker.register");
|
|
3393
3393
|
}
|
|
@@ -4235,32 +4235,32 @@ async function uiExtract(opts, log) {
|
|
|
4235
4235
|
if (dilatePx > 0) {
|
|
4236
4236
|
const tmp = new Uint8Array(W * H);
|
|
4237
4237
|
for (let y = 0; y < H; y++) {
|
|
4238
|
-
let
|
|
4238
|
+
let run5 = 0;
|
|
4239
4239
|
for (let x = 0; x < W; x++) {
|
|
4240
|
-
if (subject[y * W + x])
|
|
4241
|
-
else if (
|
|
4242
|
-
tmp[y * W + x] =
|
|
4240
|
+
if (subject[y * W + x]) run5 = dilatePx;
|
|
4241
|
+
else if (run5 > 0) run5--;
|
|
4242
|
+
tmp[y * W + x] = run5 > 0 || subject[y * W + x] ? 1 : 0;
|
|
4243
4243
|
}
|
|
4244
|
-
|
|
4244
|
+
run5 = 0;
|
|
4245
4245
|
for (let x = W - 1; x >= 0; x--) {
|
|
4246
|
-
if (subject[y * W + x])
|
|
4247
|
-
else if (
|
|
4248
|
-
if (
|
|
4246
|
+
if (subject[y * W + x]) run5 = dilatePx;
|
|
4247
|
+
else if (run5 > 0) run5--;
|
|
4248
|
+
if (run5 > 0) tmp[y * W + x] = 1;
|
|
4249
4249
|
}
|
|
4250
4250
|
}
|
|
4251
4251
|
const tmp2 = new Uint8Array(W * H);
|
|
4252
4252
|
for (let x = 0; x < W; x++) {
|
|
4253
|
-
let
|
|
4253
|
+
let run5 = 0;
|
|
4254
4254
|
for (let y = 0; y < H; y++) {
|
|
4255
|
-
if (tmp[y * W + x])
|
|
4256
|
-
else if (
|
|
4257
|
-
tmp2[y * W + x] =
|
|
4255
|
+
if (tmp[y * W + x]) run5 = dilatePx;
|
|
4256
|
+
else if (run5 > 0) run5--;
|
|
4257
|
+
tmp2[y * W + x] = run5 > 0 || tmp[y * W + x] ? 1 : 0;
|
|
4258
4258
|
}
|
|
4259
|
-
|
|
4259
|
+
run5 = 0;
|
|
4260
4260
|
for (let y = H - 1; y >= 0; y--) {
|
|
4261
|
-
if (tmp[y * W + x])
|
|
4262
|
-
else if (
|
|
4263
|
-
if (
|
|
4261
|
+
if (tmp[y * W + x]) run5 = dilatePx;
|
|
4262
|
+
else if (run5 > 0) run5--;
|
|
4263
|
+
if (run5 > 0) tmp2[y * W + x] = 1;
|
|
4264
4264
|
}
|
|
4265
4265
|
}
|
|
4266
4266
|
connect = tmp2;
|
|
@@ -8358,14 +8358,14 @@ async function runDomain(opts) {
|
|
|
8358
8358
|
}
|
|
8359
8359
|
const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
|
|
8360
8360
|
const auth = { Authorization: `Bearer ${token}` };
|
|
8361
|
-
const
|
|
8361
|
+
const call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, "Content-Type": "application/json", ...init?.headers ?? {} } });
|
|
8362
8362
|
let res;
|
|
8363
8363
|
try {
|
|
8364
|
-
if (sub === "list") res = await
|
|
8365
|
-
else if (sub === "add") res = await
|
|
8364
|
+
if (sub === "list") res = await call2(base);
|
|
8365
|
+
else if (sub === "add") res = await call2(base, { method: "POST", body: JSON.stringify({ hostname }) });
|
|
8366
8366
|
else if (sub === "verify")
|
|
8367
|
-
res = await
|
|
8368
|
-
else res = await
|
|
8367
|
+
res = await call2(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
|
|
8368
|
+
else res = await call2(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
|
|
8369
8369
|
} catch (err) {
|
|
8370
8370
|
log.error(`Couldn't reach the API at ${apiUrl}.`);
|
|
8371
8371
|
log.dim(` ${String(err)}`);
|
|
@@ -8506,7 +8506,7 @@ async function runShop(opts) {
|
|
|
8506
8506
|
await writeUserToken(token, opts.envPath, apiUrl);
|
|
8507
8507
|
}
|
|
8508
8508
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
8509
|
-
const
|
|
8509
|
+
const call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
|
|
8510
8510
|
const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
|
|
8511
8511
|
async function fail5(res2, what) {
|
|
8512
8512
|
if (printedStructuredError(res2)) {
|
|
@@ -8528,7 +8528,7 @@ async function runShop(opts) {
|
|
|
8528
8528
|
}
|
|
8529
8529
|
}
|
|
8530
8530
|
if (sub === "list") {
|
|
8531
|
-
const res2 = await
|
|
8531
|
+
const res2 = await call2(projectSkus);
|
|
8532
8532
|
if (!res2.ok) return fail5(res2, "read your shop");
|
|
8533
8533
|
const body = await res2.json();
|
|
8534
8534
|
if (opts.json) {
|
|
@@ -8564,7 +8564,7 @@ async function runShop(opts) {
|
|
|
8564
8564
|
process.exitCode = 1;
|
|
8565
8565
|
return;
|
|
8566
8566
|
}
|
|
8567
|
-
const res2 = await
|
|
8567
|
+
const res2 = await call2(projectSkus, {
|
|
8568
8568
|
method: "POST",
|
|
8569
8569
|
body: JSON.stringify({
|
|
8570
8570
|
name,
|
|
@@ -8594,7 +8594,7 @@ async function runShop(opts) {
|
|
|
8594
8594
|
}
|
|
8595
8595
|
const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
|
|
8596
8596
|
if (sub === "test") {
|
|
8597
|
-
const res2 = await
|
|
8597
|
+
const res2 = await call2(`${skuUrl}/test-grant`, { method: "POST" });
|
|
8598
8598
|
if (!res2.ok) return fail5(res2, "grant yourself a test copy");
|
|
8599
8599
|
const got = await res2.json();
|
|
8600
8600
|
log.success(`You now own ${c.cyan(got.name)} in this game.`);
|
|
@@ -8617,14 +8617,14 @@ async function runShop(opts) {
|
|
|
8617
8617
|
process.exitCode = 1;
|
|
8618
8618
|
return;
|
|
8619
8619
|
}
|
|
8620
|
-
const res2 = await
|
|
8620
|
+
const res2 = await call2(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
|
|
8621
8621
|
if (!res2.ok) return fail5(res2, "update that item");
|
|
8622
8622
|
const sku = await res2.json();
|
|
8623
8623
|
log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
|
|
8624
8624
|
if (opts.resellable === false) log.dim(" Listings already on the market stay up.");
|
|
8625
8625
|
return;
|
|
8626
8626
|
}
|
|
8627
|
-
const res = await
|
|
8627
|
+
const res = await call2(skuUrl, { method: "DELETE" });
|
|
8628
8628
|
if (!res.ok) return fail5(res, "remove that item");
|
|
8629
8629
|
log.success("Removed from the shop.");
|
|
8630
8630
|
log.dim(" Players who already bought it keep it.");
|
|
@@ -10599,10 +10599,10 @@ function reportLocalFiles(result, log, json) {
|
|
|
10599
10599
|
}
|
|
10600
10600
|
}
|
|
10601
10601
|
function laneFailureAdvice(kind, outcomes) {
|
|
10602
|
-
const
|
|
10603
|
-
if (
|
|
10602
|
+
const run5 = outcomes.trailingFailed ?? (outcomes.completed > 0 ? 0 : outcomes.failed);
|
|
10603
|
+
if (run5 < 2) return null;
|
|
10604
10604
|
const everSucceeded = outcomes.completed > 0;
|
|
10605
|
-
const opening = everSucceeded ? `\u270B the last ${
|
|
10605
|
+
const opening = everSucceeded ? `\u270B the last ${run5} ${kind} generations in this project all failed.` : `\u270B ${run5} ${kind} generations attempted in this project, ${run5} failed, none succeeded.`;
|
|
10606
10606
|
return ` ${c.bold(opening)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
|
|
10607
10607
|
}
|
|
10608
10608
|
function videoFailureAdvice(failures) {
|
|
@@ -21453,7 +21453,7 @@ async function runWorkflow(args) {
|
|
|
21453
21453
|
created.id,
|
|
21454
21454
|
args.kind,
|
|
21455
21455
|
args.opts.json ? () => {
|
|
21456
|
-
} : (
|
|
21456
|
+
} : (progress2) => log.dim(` ${progress2}%`)
|
|
21457
21457
|
);
|
|
21458
21458
|
if (!view) {
|
|
21459
21459
|
fail3(args.opts, "Timed out waiting for the character workflow.", args.kind);
|
|
@@ -21849,22 +21849,22 @@ async function context2(opts) {
|
|
|
21849
21849
|
if (!token) return null;
|
|
21850
21850
|
return { token, apiUrl };
|
|
21851
21851
|
}
|
|
21852
|
-
async function readVideo(
|
|
21852
|
+
async function readVideo(path47, log) {
|
|
21853
21853
|
let bytes;
|
|
21854
21854
|
try {
|
|
21855
|
-
bytes = await readFile(
|
|
21855
|
+
bytes = await readFile(path47);
|
|
21856
21856
|
} catch {
|
|
21857
|
-
log.error(`Can't read ${
|
|
21857
|
+
log.error(`Can't read ${path47}.`);
|
|
21858
21858
|
return null;
|
|
21859
21859
|
}
|
|
21860
21860
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
21861
|
-
log.error(`${basename(
|
|
21861
|
+
log.error(`${basename(path47)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
21862
21862
|
return null;
|
|
21863
21863
|
}
|
|
21864
21864
|
return bytes;
|
|
21865
21865
|
}
|
|
21866
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
21867
|
-
const contentType = /\.mov$/i.test(
|
|
21866
|
+
async function uploadVideo(apiUrl, token, characterId, path47, bytes, log) {
|
|
21867
|
+
const contentType = /\.mov$/i.test(path47) ? "video/quicktime" : "video/mp4";
|
|
21868
21868
|
const minted = await apiFetch(
|
|
21869
21869
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
21870
21870
|
{
|
|
@@ -21879,7 +21879,7 @@ async function uploadVideo(apiUrl, token, characterId, path46, bytes, log) {
|
|
|
21879
21879
|
return null;
|
|
21880
21880
|
}
|
|
21881
21881
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
21882
|
-
log.dim(` uploading ${basename(
|
|
21882
|
+
log.dim(` uploading ${basename(path47)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
21883
21883
|
const put = await fetch(uploadUrl, {
|
|
21884
21884
|
method: "PUT",
|
|
21885
21885
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -22879,11 +22879,11 @@ function mAimhold(C, w) {
|
|
|
22879
22879
|
const r = yawMed * Math.PI / 180;
|
|
22880
22880
|
const axis = [Math.sin(r), 0, Math.cos(r)];
|
|
22881
22881
|
const fwdF = fwdSrc.map(({ v, L }) => (v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]) / (L + 1e-9));
|
|
22882
|
-
let
|
|
22882
|
+
let run5 = 0, best = 0;
|
|
22883
22883
|
for (let i = 0; i < fwdF.length; i++) {
|
|
22884
22884
|
const ok = dev[i] < 15 && dyF[i] < -0.22 && gapF[i] > 0.18 && gapF[i] < 0.46 && fwdF[i] > 0.75 && hunchF[i] < 16;
|
|
22885
|
-
|
|
22886
|
-
best = Math.max(best,
|
|
22885
|
+
run5 = ok ? run5 + 1 : 0;
|
|
22886
|
+
best = Math.max(best, run5);
|
|
22887
22887
|
}
|
|
22888
22888
|
let holdBest = 0, holdBestScore = -Infinity;
|
|
22889
22889
|
for (let i = 0; i < fwdF.length; i++) {
|
|
@@ -23237,15 +23237,15 @@ function cutOneshot(t, cfg) {
|
|
|
23237
23237
|
const cc = folded(t);
|
|
23238
23238
|
const air = cc.map((c2) => c2[0] === 0 && c2[1] === 0);
|
|
23239
23239
|
let best = null;
|
|
23240
|
-
let
|
|
23240
|
+
let run5 = null;
|
|
23241
23241
|
for (let f = 0; f < air.length; f++) {
|
|
23242
|
-
if (air[f])
|
|
23242
|
+
if (air[f]) run5 = run5 ? [run5[0], f] : [f, f];
|
|
23243
23243
|
else {
|
|
23244
|
-
if (
|
|
23245
|
-
|
|
23244
|
+
if (run5 && (!best || run5[1] - run5[0] > best[1] - best[0])) best = run5;
|
|
23245
|
+
run5 = null;
|
|
23246
23246
|
}
|
|
23247
23247
|
}
|
|
23248
|
-
if (
|
|
23248
|
+
if (run5 && (!best || run5[1] - run5[0] > best[1] - best[0])) best = run5;
|
|
23249
23249
|
let s, e;
|
|
23250
23250
|
if (best) {
|
|
23251
23251
|
s = best[0];
|
|
@@ -23778,7 +23778,7 @@ async function runBlender(opts) {
|
|
|
23778
23778
|
return 1;
|
|
23779
23779
|
}
|
|
23780
23780
|
if (sub === "serve") {
|
|
23781
|
-
const { serveLocalBlender } = await import("./blender-serve-
|
|
23781
|
+
const { serveLocalBlender } = await import("./blender-serve-EH2MOW3U.js");
|
|
23782
23782
|
const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
|
|
23783
23783
|
log.step(`Starting a local Blender service on port ${port}`);
|
|
23784
23784
|
log.plain(
|
|
@@ -23787,7 +23787,7 @@ async function runBlender(opts) {
|
|
|
23787
23787
|
return serveLocalBlender({ port, log });
|
|
23788
23788
|
}
|
|
23789
23789
|
if (sub === "mcp") {
|
|
23790
|
-
const { runBlenderMcp } = await import("./blender-mcp-
|
|
23790
|
+
const { runBlenderMcp } = await import("./blender-mcp-WSURPZET.js");
|
|
23791
23791
|
return runBlenderMcp();
|
|
23792
23792
|
}
|
|
23793
23793
|
if (sub === "seat") {
|
|
@@ -24295,7 +24295,12 @@ import fs35 from "fs/promises";
|
|
|
24295
24295
|
import path36 from "path";
|
|
24296
24296
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
24297
24297
|
var SUBS3 = ["models", "bench", "price", "status", "cancel"];
|
|
24298
|
-
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN
|
|
24298
|
+
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN credits \u2014 your balance, not a player's, and a spend this build's asset allowance does not count. Re-run with --max-credits <n> --user-approved only after the person at the keyboard agreed to the number.";
|
|
24299
|
+
var LLM_MAX_COINS_DEPRECATED = "--max-coins is deprecated: it is read as --max-credits, the same number \u2014 a bench spends credits now.";
|
|
24300
|
+
function benchBalanceShortLine(spendable, maxCredits) {
|
|
24301
|
+
return `Your spendable credits (${spendable}) cannot cover one attempt at --max-credits ${maxCredits} \u2014 nothing was sent, nothing was spent. Add credits, or re-run with a lower --max-credits once the person at the keyboard agrees to it.`;
|
|
24302
|
+
}
|
|
24303
|
+
var LLM_BENCH_UNVERIFIED_LINE = "Credits pay only for a verified email, and this account's is not verified yet (credits_unverified) \u2014 nothing was sent, nothing was spent. Verify it, then re-run.";
|
|
24299
24304
|
var LLM_LANE_OFF_LINE = "The runtime LLM lane is off on this stand \u2014 in-game generate() answers 404 here, and nothing can be benchmarked.";
|
|
24300
24305
|
var LLM_TOOLS_CONVERT_HINT = [
|
|
24301
24306
|
"In-game model calls need a hosted game. This folder is a Genex Tools workspace, so nothing here",
|
|
@@ -24308,7 +24313,7 @@ function serverSlotCounts(body) {
|
|
|
24308
24313
|
return { held: body.slotsHeld, limit: body.slotLimit };
|
|
24309
24314
|
}
|
|
24310
24315
|
var PENDING_SLOT_RELEASE_MINUTES = 10;
|
|
24311
|
-
var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so
|
|
24316
|
+
var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so what it reserved stays held until the bill resolves; such a row stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`;
|
|
24312
24317
|
var LLM_STATUS_NO_COUNT_LINE = "This stand does not report how many slots are held across your projects, so no count is shown.";
|
|
24313
24318
|
var LLM_CANCEL_AWAITING_BILL = "this call already stopped; its bill is awaiting the provider";
|
|
24314
24319
|
var LLM_CANCEL_ALREADY_FINAL = "this call is already final";
|
|
@@ -24322,11 +24327,33 @@ var DEFAULT_SAMPLE_TIMEOUT_SEC = 180;
|
|
|
24322
24327
|
function parseProviderKey(value) {
|
|
24323
24328
|
return value === "ok" || value === "rejected" || value === "unknown" ? value : null;
|
|
24324
24329
|
}
|
|
24330
|
+
function bpsOrNull(value) {
|
|
24331
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
24332
|
+
}
|
|
24333
|
+
function standingCreditGrantOrNull(value) {
|
|
24334
|
+
if (!value || typeof value !== "object") return null;
|
|
24335
|
+
const v = value;
|
|
24336
|
+
const whole = (x) => typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
|
|
24337
|
+
if (!whole(v.minCredits) || !whole(v.maxCredits) || !whole(v.stepCredits) || !whole(v.defaultCredits)) return null;
|
|
24338
|
+
return {
|
|
24339
|
+
minCredits: v.minCredits,
|
|
24340
|
+
maxCredits: v.maxCredits,
|
|
24341
|
+
stepCredits: v.stepCredits,
|
|
24342
|
+
defaultCredits: v.defaultCredits,
|
|
24343
|
+
...whole(v.minUsdCents) ? { minUsdCents: v.minUsdCents } : {},
|
|
24344
|
+
...whole(v.maxUsdCents) ? { maxUsdCents: v.maxUsdCents } : {},
|
|
24345
|
+
...whole(v.stepUsdCents) ? { stepUsdCents: v.stepUsdCents } : {}
|
|
24346
|
+
};
|
|
24347
|
+
}
|
|
24325
24348
|
async function readRuntimeLane(apiUrl, token) {
|
|
24326
24349
|
const empty = (state) => ({
|
|
24327
24350
|
state,
|
|
24328
24351
|
models: null,
|
|
24329
24352
|
recommendedDeclaredHeadroomBps: null,
|
|
24353
|
+
currency: null,
|
|
24354
|
+
feeBps: null,
|
|
24355
|
+
usdCentsPerCredit: null,
|
|
24356
|
+
standingCreditGrant: null,
|
|
24330
24357
|
externalProviders: null,
|
|
24331
24358
|
total: null,
|
|
24332
24359
|
featuredCount: null,
|
|
@@ -24345,7 +24372,11 @@ async function readRuntimeLane(apiUrl, token) {
|
|
|
24345
24372
|
return {
|
|
24346
24373
|
state: "live",
|
|
24347
24374
|
models: body.models,
|
|
24348
|
-
recommendedDeclaredHeadroomBps:
|
|
24375
|
+
recommendedDeclaredHeadroomBps: bpsOrNull(body.recommendedDeclaredHeadroomBps),
|
|
24376
|
+
currency: body.currency === "credits" || body.currency === "coins" ? body.currency : null,
|
|
24377
|
+
feeBps: bpsOrNull(body.feeBps),
|
|
24378
|
+
usdCentsPerCredit: typeof body.usdCentsPerCredit === "number" && Number.isSafeInteger(body.usdCentsPerCredit) && body.usdCentsPerCredit >= 1 ? body.usdCentsPerCredit : null,
|
|
24379
|
+
standingCreditGrant: standingCreditGrantOrNull(body.standingCreditGrant),
|
|
24349
24380
|
externalProviders: Array.isArray(body.externalProviders) ? body.externalProviders : null,
|
|
24350
24381
|
total: typeof body.total === "number" && Number.isFinite(body.total) ? body.total : null,
|
|
24351
24382
|
featuredCount: typeof body.featuredCount === "number" && Number.isFinite(body.featuredCount) ? body.featuredCount : null,
|
|
@@ -24361,18 +24392,45 @@ function percentile2(values, p) {
|
|
|
24361
24392
|
const rank2 = Math.ceil(p / 100 * sorted.length);
|
|
24362
24393
|
return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
|
|
24363
24394
|
}
|
|
24364
|
-
function
|
|
24365
|
-
|
|
24395
|
+
function percentilePicos(values, p) {
|
|
24396
|
+
if (values.length === 0) return null;
|
|
24397
|
+
const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
24398
|
+
const rank2 = Math.ceil(p / 100 * sorted.length);
|
|
24399
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
|
|
24366
24400
|
}
|
|
24367
|
-
|
|
24368
|
-
|
|
24401
|
+
var PICOS_PER_CENT = 10000000000n;
|
|
24402
|
+
function ceilingCreditsFor(costPicos, terms) {
|
|
24403
|
+
if (costPicos <= 0n) return 1;
|
|
24404
|
+
const num = costPicos * BigInt(1e4 + terms.feeBps) * BigInt(1e4 + terms.headroomBps);
|
|
24405
|
+
const den = 100000000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
|
|
24406
|
+
return Math.max(1, Number((num + den - 1n) / den));
|
|
24407
|
+
}
|
|
24408
|
+
function averageCreditsPerCall(costsPicos, terms) {
|
|
24409
|
+
if (costsPicos.length === 0) return null;
|
|
24410
|
+
const sum = costsPicos.reduce((a, b) => a + b, 0n);
|
|
24411
|
+
return Number(sum * BigInt(1e4 + terms.feeBps)) / Number(BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT);
|
|
24412
|
+
}
|
|
24413
|
+
function perCallEstimateCreditsFor(costsPicos, terms) {
|
|
24414
|
+
if (costsPicos.length === 0) return null;
|
|
24415
|
+
const sum = costsPicos.reduce((a, b) => a + b, 0n);
|
|
24416
|
+
const num = sum * BigInt(1e4 + terms.feeBps);
|
|
24417
|
+
const den = BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
|
|
24418
|
+
return Math.max(1, Number((num + den - 1n) / den));
|
|
24419
|
+
}
|
|
24420
|
+
function formatCredits(value) {
|
|
24421
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
24422
|
+
if (value >= 100) return String(Math.round(value));
|
|
24423
|
+
return String(Number(value.toPrecision(value < 1 ? 2 : 3)));
|
|
24424
|
+
}
|
|
24425
|
+
function usdFromPicos(picos) {
|
|
24426
|
+
if (picos === null || picos === void 0) return "unknown";
|
|
24427
|
+
const value = typeof picos === "bigint" ? picos : /^\d+$/.test(picos) ? BigInt(picos) : null;
|
|
24428
|
+
if (value === null) return "unknown";
|
|
24429
|
+
return `$${(Number(value) / 1e12).toFixed(6)}`;
|
|
24369
24430
|
}
|
|
24370
24431
|
function usdPerMillion(value) {
|
|
24371
24432
|
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
24372
24433
|
}
|
|
24373
|
-
function usd(value) {
|
|
24374
|
-
return value === null ? "unknown" : `$${value.toFixed(6)}`;
|
|
24375
|
-
}
|
|
24376
24434
|
async function runLlm(opts = {}) {
|
|
24377
24435
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
24378
24436
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -24398,16 +24456,23 @@ async function runLlm(opts = {}) {
|
|
|
24398
24456
|
return;
|
|
24399
24457
|
}
|
|
24400
24458
|
if (sub === "bench") {
|
|
24401
|
-
if (opts.maxCoins
|
|
24459
|
+
if (opts.maxCoins !== void 0) process.stderr.write(`${c.yellow("!")} ${LLM_MAX_COINS_DEPRECATED}
|
|
24460
|
+
`);
|
|
24461
|
+
if (opts.maxCoins !== void 0 && opts.maxCredits !== void 0 && opts.maxCoins !== opts.maxCredits) {
|
|
24462
|
+
fail5("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
|
|
24463
|
+
return;
|
|
24464
|
+
}
|
|
24465
|
+
const approvedCredits = opts.maxCredits ?? opts.maxCoins;
|
|
24466
|
+
if (approvedCredits === void 0 || opts.userApproved !== true) {
|
|
24402
24467
|
fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
|
|
24403
24468
|
return;
|
|
24404
24469
|
}
|
|
24405
|
-
if (!Number.isInteger(
|
|
24406
|
-
fail5("llm bench", `--max-
|
|
24470
|
+
if (!Number.isInteger(approvedCredits) || approvedCredits < 1) {
|
|
24471
|
+
fail5("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
|
|
24407
24472
|
return;
|
|
24408
24473
|
}
|
|
24409
24474
|
if (!opts.benchPrompt?.trim()) {
|
|
24410
|
-
fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-
|
|
24475
|
+
fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-credits <n> --user-approved.');
|
|
24411
24476
|
return;
|
|
24412
24477
|
}
|
|
24413
24478
|
if (opts.jsonOutput && opts.textOutput) {
|
|
@@ -24499,6 +24564,10 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
24499
24564
|
models: null,
|
|
24500
24565
|
externalProviders: null,
|
|
24501
24566
|
recommendedDeclaredHeadroomBps: null,
|
|
24567
|
+
currency: null,
|
|
24568
|
+
feeBps: null,
|
|
24569
|
+
usdCentsPerCredit: null,
|
|
24570
|
+
standingCreditGrant: null,
|
|
24502
24571
|
total: null,
|
|
24503
24572
|
featuredCount: null,
|
|
24504
24573
|
// The stand was never asked, so the verdict is unknown — but the key is
|
|
@@ -24524,6 +24593,12 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
24524
24593
|
models: lane.models,
|
|
24525
24594
|
externalProviders: lane.externalProviders,
|
|
24526
24595
|
recommendedDeclaredHeadroomBps: lane.recommendedDeclaredHeadroomBps,
|
|
24596
|
+
// The credit terms a call is billed on, the server's — null on a stand
|
|
24597
|
+
// that predates credit billing, never a number the CLI filled in.
|
|
24598
|
+
currency: lane.currency,
|
|
24599
|
+
feeBps: lane.feeBps,
|
|
24600
|
+
usdCentsPerCredit: lane.usdCentsPerCredit,
|
|
24601
|
+
standingCreditGrant: lane.standingCreditGrant,
|
|
24527
24602
|
total: lane.total ?? lane.models?.length ?? null,
|
|
24528
24603
|
featuredCount: lane.featuredCount ?? (lane.models ? lane.models.filter((m) => m.featured).length : null),
|
|
24529
24604
|
// `null` when the lane is not live or the server predates the probe.
|
|
@@ -24570,8 +24645,9 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
24570
24645
|
);
|
|
24571
24646
|
for (const m of shown) {
|
|
24572
24647
|
const plan = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
|
|
24648
|
+
const kind = m.kind === "typesafe" ? " \xB7 judge (classifier): judge() under a budget, not generate() or bench" : "";
|
|
24573
24649
|
log.plain(
|
|
24574
|
-
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}`
|
|
24650
|
+
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}${kind}`
|
|
24575
24651
|
);
|
|
24576
24652
|
}
|
|
24577
24653
|
if (hiddenCount > 0) {
|
|
@@ -24587,74 +24663,104 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
24587
24663
|
}
|
|
24588
24664
|
log.plain("");
|
|
24589
24665
|
if (lane.recommendedDeclaredHeadroomBps === null) {
|
|
24590
|
-
log.dim(" This stand serves no recommended headroom, so no
|
|
24666
|
+
log.dim(" This stand serves no recommended headroom, so no ceiling can be recommended from a bench.");
|
|
24667
|
+
} else {
|
|
24668
|
+
log.dim(` Recommended headroom over a benchmarked call's cost on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
|
|
24669
|
+
}
|
|
24670
|
+
if (lane.feeBps === null) {
|
|
24671
|
+
log.dim(" This stand serves no platform fee \u2014 it predates credit billing, so no ceiling can be recommended from a bench.");
|
|
24591
24672
|
} else {
|
|
24592
|
-
log.dim(`
|
|
24673
|
+
log.dim(` In-game calls are billed in the player's credits, as used: the provider's cost plus this stand's platform fee (${lane.feeBps} bps).`);
|
|
24593
24674
|
}
|
|
24594
24675
|
if (toolsOnly) {
|
|
24595
24676
|
printConvertHint();
|
|
24596
24677
|
return;
|
|
24597
24678
|
}
|
|
24598
|
-
log.dim(" Those per-million rates are the PROVIDER's; what
|
|
24599
|
-
log.dim(`
|
|
24679
|
+
log.dim(" Those per-million rates are the PROVIDER's list; what one call of YOUR prompt really costs,");
|
|
24680
|
+
log.dim(` and the per-call ceiling to declare, only a real call measures: ${c.cyan('genex llm bench "<prompt>" --max-credits <n> --user-approved')}`);
|
|
24600
24681
|
}
|
|
24601
24682
|
function wholeOrNull(value) {
|
|
24602
24683
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
24603
24684
|
}
|
|
24685
|
+
function costPicosOf(usage) {
|
|
24686
|
+
if (typeof usage?.costUsdPicos === "string" && /^\d+$/.test(usage.costUsdPicos)) return usage.costUsdPicos;
|
|
24687
|
+
if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) {
|
|
24688
|
+
return String(BigInt(Math.round(usage.costUsd * 1e12)));
|
|
24689
|
+
}
|
|
24690
|
+
return null;
|
|
24691
|
+
}
|
|
24604
24692
|
function sampleFrom(id, settled) {
|
|
24605
24693
|
const usage = settled?.usage;
|
|
24694
|
+
const picos = costPicosOf(usage);
|
|
24606
24695
|
return {
|
|
24607
24696
|
id,
|
|
24608
24697
|
status: settled?.status ?? null,
|
|
24609
24698
|
billingStatus: settled?.billingStatus ?? null,
|
|
24610
|
-
|
|
24611
|
-
costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : null,
|
|
24699
|
+
chargedCredits: wholeOrNull(settled?.chargedCredits),
|
|
24700
|
+
costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : picos !== null ? Number(picos) / 1e12 : null,
|
|
24701
|
+
costUsdPicos: picos,
|
|
24612
24702
|
error: settled?.error ?? (settled ? null : "timed_out"),
|
|
24613
24703
|
providerMessage: typeof settled?.providerMessage === "string" && settled.providerMessage.trim() ? settled.providerMessage.trim() : null,
|
|
24614
24704
|
inputTokens: wholeOrNull(usage?.inputTokens),
|
|
24615
24705
|
outputTokens: wholeOrNull(usage?.outputTokens),
|
|
24616
24706
|
maxOutputTokens: wholeOrNull(usage?.maxOutputTokens),
|
|
24617
|
-
|
|
24707
|
+
fitCredits: typeof usage?.fitCredits === "number" && Number.isSafeInteger(usage.fitCredits) && usage.fitCredits >= 1 ? usage.fitCredits : null,
|
|
24618
24708
|
fitExceedsCap: typeof usage?.fitExceedsCap === "boolean" ? usage.fitExceedsCap : null,
|
|
24619
24709
|
truncated: typeof usage?.truncated === "boolean" ? usage.truncated : settled?.error === "provider_token_limit" ? true : null
|
|
24620
24710
|
};
|
|
24621
24711
|
}
|
|
24622
24712
|
var PROVIDER_TOKEN_LIMIT = "provider_token_limit";
|
|
24623
|
-
function benchRecommendation(results,
|
|
24624
|
-
const settled = results.filter(
|
|
24625
|
-
|
|
24626
|
-
|
|
24627
|
-
const
|
|
24628
|
-
const
|
|
24713
|
+
function benchRecommendation(results, terms) {
|
|
24714
|
+
const settled = results.filter(
|
|
24715
|
+
(r) => r.status === "succeeded" && r.billingStatus === "final" && r.chargedCredits !== null && r.costUsdPicos !== null
|
|
24716
|
+
);
|
|
24717
|
+
const costs = settled.map((r) => BigInt(r.costUsdPicos));
|
|
24718
|
+
const charged = settled.map((r) => r.chargedCredits);
|
|
24719
|
+
const fits = settled.map((r) => r.fitCredits).filter((v) => typeof v === "number");
|
|
24720
|
+
const costP95 = percentilePicos(costs, 95);
|
|
24721
|
+
const costMax = costs.length ? costs.reduce((a, b) => b > a ? b : a) : null;
|
|
24629
24722
|
const fitP95 = percentile2(fits, 95);
|
|
24630
24723
|
const fitMax = fits.length ? Math.max(...fits) : null;
|
|
24631
24724
|
const exceedsStandCap = results.some((r) => r.fitExceedsCap === true);
|
|
24632
24725
|
const cutOffSamples = results.filter((r) => r.error === PROVIDER_TOKEN_LIMIT && r.fitExceedsCap !== true).length;
|
|
24633
24726
|
const noRecommendationReason = exceedsStandCap ? "answer_exceeds_stand_cap" : cutOffSamples > 0 ? "answer_cut_off" : null;
|
|
24634
|
-
const
|
|
24635
|
-
const
|
|
24636
|
-
const
|
|
24727
|
+
const { headroomBps, feeBps, usdCentsPerCredit } = terms;
|
|
24728
|
+
const missingTerms = headroomBps === null ? "headroom" : feeBps === null || usdCentsPerCredit === null ? "credit_terms" : null;
|
|
24729
|
+
const full = headroomBps !== null && feeBps !== null && usdCentsPerCredit !== null ? { headroomBps, feeBps, usdCentsPerCredit } : null;
|
|
24730
|
+
const billing = feeBps !== null && usdCentsPerCredit !== null ? { feeBps, usdCentsPerCredit } : null;
|
|
24731
|
+
const costBased = full !== null && costP95 !== null ? ceilingCreditsFor(costP95, full) : null;
|
|
24732
|
+
const recommended = costBased === null || noRecommendationReason !== null ? null : Math.max(costBased, fitP95 ?? 0);
|
|
24733
|
+
const ceiling = recommended === null || costMax === null || full === null ? null : Math.max(ceilingCreditsFor(costMax, full), recommended, fitMax ?? 0);
|
|
24734
|
+
const average = billing !== null ? averageCreditsPerCall(costs, billing) : null;
|
|
24735
|
+
const estimate = ceiling === null || billing === null ? null : Math.min(ceiling, perCallEstimateCreditsFor(costs, billing) ?? 1);
|
|
24637
24736
|
return {
|
|
24737
|
+
costs,
|
|
24638
24738
|
charged,
|
|
24639
|
-
|
|
24640
|
-
|
|
24641
|
-
|
|
24739
|
+
costP50: percentilePicos(costs, 50),
|
|
24740
|
+
costP95,
|
|
24741
|
+
costMax,
|
|
24742
|
+
chargedP50: percentile2(charged, 50),
|
|
24743
|
+
chargedP95: percentile2(charged, 95),
|
|
24744
|
+
chargedMax: charged.length ? Math.max(...charged) : null,
|
|
24642
24745
|
fits,
|
|
24643
24746
|
fitP95,
|
|
24644
24747
|
fitMax,
|
|
24645
|
-
|
|
24646
|
-
|
|
24647
|
-
|
|
24648
|
-
|
|
24748
|
+
costBasedMaxCredits: costBased,
|
|
24749
|
+
recommendedMaxCredits: recommended,
|
|
24750
|
+
recommendedPerCallMaxCredits: ceiling,
|
|
24751
|
+
averageCreditsPerCall: average,
|
|
24752
|
+
recommendedPerCallEstimateCredits: estimate,
|
|
24753
|
+
lengthRaisedCeiling: recommended !== null && costBased !== null && recommended > costBased,
|
|
24649
24754
|
cutOffSamples,
|
|
24650
24755
|
exceedsStandCap,
|
|
24651
|
-
noRecommendationReason
|
|
24756
|
+
noRecommendationReason,
|
|
24757
|
+
missingTerms
|
|
24652
24758
|
};
|
|
24653
24759
|
}
|
|
24654
|
-
var LLM_LENGTH_RAISED_LINE = "The
|
|
24655
|
-
var LLM_EXCEEDS_STAND_CAP_LINE = "No
|
|
24656
|
-
function benchCutOffLine(cutOff,
|
|
24657
|
-
return `No
|
|
24760
|
+
var LLM_LENGTH_RAISED_LINE = "The ceiling also decides how long the answer may be: at the cost-based ceiling the answer would be cut off, so declare this one.";
|
|
24761
|
+
var LLM_EXCEEDS_STAND_CAP_LINE = "No ceiling recommended: this answer is longer than one call on this stand may produce. Ask for a shorter answer \u2014 fewer fields, shorter strings, a length the prompt states \u2014 and benchmark again.";
|
|
24762
|
+
function benchCutOffLine(cutOff, maxCredits) {
|
|
24763
|
+
return `No ceiling recommended: ${cutOff} sample${cutOff === 1 ? " was" : "s were"} cut off at this ceiling (--max-credits ${maxCredits}) before the answer was finished, so its real length is unknown. Re-run with a higher --max-credits.`;
|
|
24658
24764
|
}
|
|
24659
24765
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["requires_confirmation", "queued", "dispatching", "awaiting_external"]);
|
|
24660
24766
|
function rowState(row) {
|
|
@@ -24665,7 +24771,22 @@ function rowState(row) {
|
|
|
24665
24771
|
function rowHoldsSlot(row) {
|
|
24666
24772
|
if (typeof row.slotHeld === "boolean") return row.slotHeld;
|
|
24667
24773
|
const state = rowState(row);
|
|
24668
|
-
return state === "active" || state === "awaiting bill" && (row.reservedCoins ?? 0) > 0;
|
|
24774
|
+
return state === "active" || state === "awaiting bill" && ((row.reservedCoins ?? 0) > 0 || (row.reservedCredits ?? 0) > 0);
|
|
24775
|
+
}
|
|
24776
|
+
function rowMoney(row) {
|
|
24777
|
+
const credits = row.currency === "credits" || row.currency === void 0 && typeof row.reservedCredits === "number" && typeof row.reservedCoins !== "number";
|
|
24778
|
+
if (credits) {
|
|
24779
|
+
return {
|
|
24780
|
+
unit: "credits",
|
|
24781
|
+
reserved: typeof row.reservedCredits === "number" ? row.reservedCredits : null,
|
|
24782
|
+
charged: typeof row.chargedCredits === "number" ? row.chargedCredits : null
|
|
24783
|
+
};
|
|
24784
|
+
}
|
|
24785
|
+
return {
|
|
24786
|
+
unit: "coin",
|
|
24787
|
+
reserved: typeof row.reservedCoins === "number" ? row.reservedCoins : null,
|
|
24788
|
+
charged: typeof row.chargedCoins === "number" ? row.chargedCoins : null
|
|
24789
|
+
};
|
|
24669
24790
|
}
|
|
24670
24791
|
function providerRefusalStatus(error) {
|
|
24671
24792
|
const m = /^provider_http_(\d{3})$/.exec(error ?? "");
|
|
@@ -24684,11 +24805,11 @@ function formatAge(iso, now = Date.now()) {
|
|
|
24684
24805
|
}
|
|
24685
24806
|
async function runBench(args) {
|
|
24686
24807
|
const { apiUrl, token, projectId, cwd, opts, log } = args;
|
|
24687
|
-
const
|
|
24808
|
+
const maxCredits = opts.maxCredits ?? opts.maxCoins;
|
|
24688
24809
|
const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
|
|
24689
24810
|
const prompt = opts.benchPrompt.trim();
|
|
24690
24811
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
24691
|
-
const
|
|
24812
|
+
const call2 = (url, init) => apiFetch(url, { ...init, headers: { ...auth, ...init?.headers ?? {} } });
|
|
24692
24813
|
let schema;
|
|
24693
24814
|
if (opts.schemaPath) {
|
|
24694
24815
|
try {
|
|
@@ -24706,7 +24827,7 @@ async function runBench(args) {
|
|
|
24706
24827
|
const lane = await readRuntimeLane(apiUrl, token);
|
|
24707
24828
|
if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
|
|
24708
24829
|
if (lane.state === "off") {
|
|
24709
|
-
if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples,
|
|
24830
|
+
if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts), status: "off", error: null });
|
|
24710
24831
|
else {
|
|
24711
24832
|
log.plain(LLM_LANE_OFF_LINE);
|
|
24712
24833
|
log.dim(" Nothing was spent.");
|
|
@@ -24733,17 +24854,27 @@ async function runBench(args) {
|
|
|
24733
24854
|
);
|
|
24734
24855
|
return;
|
|
24735
24856
|
}
|
|
24736
|
-
const
|
|
24857
|
+
const walletBefore = await fetchCreditsSnapshot(apiUrl, token);
|
|
24858
|
+
if (walletBefore?.emailVerified === false) {
|
|
24859
|
+
benchFailed(opts, log, LLM_BENCH_UNVERIFIED_LINE);
|
|
24860
|
+
return;
|
|
24861
|
+
}
|
|
24862
|
+
if (walletBefore !== null && walletBefore.spendable < maxCredits) {
|
|
24863
|
+
benchFailed(opts, log, benchBalanceShortLine(walletBefore.spendable, maxCredits));
|
|
24864
|
+
return;
|
|
24865
|
+
}
|
|
24866
|
+
const balanceBefore = walletBefore?.spendable ?? null;
|
|
24737
24867
|
if (!opts.json) {
|
|
24738
24868
|
log.plain(c.bold("genex llm bench"));
|
|
24739
24869
|
log.dim(` ${apiUrl}`);
|
|
24740
24870
|
log.plain("");
|
|
24741
24871
|
log.plain(` Model ${c.cyan(modelId)}`);
|
|
24742
24872
|
log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
|
|
24743
|
-
log.plain(` Approved ${
|
|
24873
|
+
log.plain(` Approved ${maxCredits} credit${maxCredits === 1 ? "" : "s"} per attempt \u2014 at worst ${maxCredits * samples} credits for this run, from your own balance`);
|
|
24744
24874
|
log.plain(
|
|
24745
|
-
` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore}
|
|
24875
|
+
` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} credits spendable`}`
|
|
24746
24876
|
);
|
|
24877
|
+
log.dim(" Billed as used: each attempt is charged its real cost plus the platform fee, rounded up to a whole credit.");
|
|
24747
24878
|
log.plain("");
|
|
24748
24879
|
}
|
|
24749
24880
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
@@ -24757,7 +24888,7 @@ async function runBench(args) {
|
|
|
24757
24888
|
if (!opts.json) log.warn("Stopping \u2014 cancelling the attempt in flight.");
|
|
24758
24889
|
void (async () => {
|
|
24759
24890
|
if (target) {
|
|
24760
|
-
await
|
|
24891
|
+
await call2(`${base}/${encodeURIComponent(target)}/cancel`, { method: "POST" }).catch(() => {
|
|
24761
24892
|
});
|
|
24762
24893
|
}
|
|
24763
24894
|
process.exit(130);
|
|
@@ -24766,10 +24897,10 @@ async function runBench(args) {
|
|
|
24766
24897
|
process.once("SIGINT", onSigint);
|
|
24767
24898
|
try {
|
|
24768
24899
|
for (let i = 0; i < samples; i++) {
|
|
24769
|
-
const res = await
|
|
24900
|
+
const res = await call2(base, {
|
|
24770
24901
|
method: "POST",
|
|
24771
24902
|
body: JSON.stringify({
|
|
24772
|
-
|
|
24903
|
+
maxCredits,
|
|
24773
24904
|
request: {
|
|
24774
24905
|
idempotencyKey: `bench-${randomUUID2()}`,
|
|
24775
24906
|
modelId,
|
|
@@ -24780,7 +24911,7 @@ async function runBench(args) {
|
|
|
24780
24911
|
})
|
|
24781
24912
|
});
|
|
24782
24913
|
if (!res.ok) {
|
|
24783
|
-
const verdict = await explainBenchRefusal(res,
|
|
24914
|
+
const verdict = await explainBenchRefusal(res, call2, base, log, opts, i, results.length);
|
|
24784
24915
|
if (verdict.refusal) refusal2 = verdict.refusal;
|
|
24785
24916
|
if (verdict.stop) break;
|
|
24786
24917
|
continue;
|
|
@@ -24791,7 +24922,7 @@ async function runBench(args) {
|
|
|
24791
24922
|
continue;
|
|
24792
24923
|
}
|
|
24793
24924
|
inflight = started.id;
|
|
24794
|
-
const settled = await pollSettled(
|
|
24925
|
+
const settled = await pollSettled(call2, `${base}/${encodeURIComponent(started.id)}`, opts.timeoutSec ?? DEFAULT_SAMPLE_TIMEOUT_SEC);
|
|
24795
24926
|
inflight = null;
|
|
24796
24927
|
const row = sampleFrom(started.id, settled);
|
|
24797
24928
|
results.push(row);
|
|
@@ -24799,13 +24930,14 @@ async function runBench(args) {
|
|
|
24799
24930
|
if (refusedAt !== null) providerRefused++;
|
|
24800
24931
|
if (!opts.json) {
|
|
24801
24932
|
const length = row.outputTokens !== null ? ` \xB7 ${row.outputTokens}${row.maxOutputTokens !== null ? ` of ${row.maxOutputTokens}` : ""} tokens out` : "";
|
|
24933
|
+
const charged = row.chargedCredits === null ? "\u2014" : String(row.chargedCredits);
|
|
24802
24934
|
log.plain(
|
|
24803
|
-
` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${
|
|
24935
|
+
` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${charged.padStart(4)} credit${row.chargedCredits === 1 ? "" : "s"} charged \xB7 cost ${usdFromPicos(row.costUsdPicos)}${length}${row.error ? ` \xB7 ${row.error}` : ""}`
|
|
24804
24936
|
);
|
|
24805
24937
|
if (refusedAt !== null) printProviderRefusal(log, refusedAt, row.providerMessage, row.status === "unknown");
|
|
24806
24938
|
if (row.error === PROVIDER_TOKEN_LIMIT) {
|
|
24807
24939
|
log.dim(
|
|
24808
|
-
row.fitExceedsCap === true ? " The answer was cut off at this stand's own output limit \u2014 no
|
|
24940
|
+
row.fitExceedsCap === true ? " The answer was cut off at this stand's own output limit \u2014 no ceiling makes room for it; it is not a sample." : ` The answer was cut off at this ceiling (--max-credits ${maxCredits}) before it was finished \u2014 it was charged, and it is not a sample.`
|
|
24809
24941
|
);
|
|
24810
24942
|
}
|
|
24811
24943
|
}
|
|
@@ -24813,16 +24945,19 @@ async function runBench(args) {
|
|
|
24813
24945
|
} finally {
|
|
24814
24946
|
process.removeListener("SIGINT", onSigint);
|
|
24815
24947
|
}
|
|
24816
|
-
const
|
|
24817
|
-
|
|
24818
|
-
|
|
24819
|
-
|
|
24820
|
-
|
|
24821
|
-
const
|
|
24948
|
+
const terms = {
|
|
24949
|
+
headroomBps: lane.recommendedDeclaredHeadroomBps,
|
|
24950
|
+
feeBps: lane.feeBps,
|
|
24951
|
+
usdCentsPerCredit: lane.usdCentsPerCredit
|
|
24952
|
+
};
|
|
24953
|
+
const rec = benchRecommendation(results, terms);
|
|
24954
|
+
const settledCount = rec.charged.length;
|
|
24822
24955
|
const lengthBlocked = rec.noRecommendationReason !== null;
|
|
24823
|
-
const balanceAfter = await
|
|
24956
|
+
const balanceAfter = (await fetchCreditsSnapshot(apiUrl, token))?.spendable ?? null;
|
|
24957
|
+
const picosOrNull = (v) => v === null ? null : String(v);
|
|
24824
24958
|
const record = {
|
|
24825
|
-
v:
|
|
24959
|
+
v: 2,
|
|
24960
|
+
currency: "credits",
|
|
24826
24961
|
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24827
24962
|
apiUrl,
|
|
24828
24963
|
projectId,
|
|
@@ -24831,28 +24966,34 @@ async function runBench(args) {
|
|
|
24831
24966
|
outputFormat,
|
|
24832
24967
|
samples,
|
|
24833
24968
|
completed: settledCount,
|
|
24834
|
-
|
|
24835
|
-
|
|
24836
|
-
|
|
24837
|
-
max,
|
|
24838
|
-
|
|
24839
|
-
|
|
24840
|
-
recommendedPerCallMaxCoins: ceiling,
|
|
24841
|
-
maxCoinsPerSample: maxCoins,
|
|
24842
|
-
fitCoins: rec.fits,
|
|
24969
|
+
maxCreditsPerSample: maxCredits,
|
|
24970
|
+
costUsdPicos: rec.costs.map(String),
|
|
24971
|
+
chargedCredits: rec.charged,
|
|
24972
|
+
cost: { p50: picosOrNull(rec.costP50), p95: picosOrNull(rec.costP95), max: picosOrNull(rec.costMax) },
|
|
24973
|
+
charged: { p50: rec.chargedP50, p95: rec.chargedP95, max: rec.chargedMax },
|
|
24974
|
+
fitCredits: rec.fits,
|
|
24843
24975
|
fitP95: rec.fitP95,
|
|
24844
|
-
|
|
24845
|
-
|
|
24976
|
+
fitMax: rec.fitMax,
|
|
24977
|
+
recommendedDeclaredHeadroomBps: terms.headroomBps,
|
|
24978
|
+
feeBps: terms.feeBps,
|
|
24979
|
+
usdCentsPerCredit: terms.usdCentsPerCredit,
|
|
24980
|
+
costBasedMaxCredits: rec.costBasedMaxCredits,
|
|
24981
|
+
recommendedMaxCredits: rec.recommendedMaxCredits,
|
|
24982
|
+
recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
|
|
24983
|
+
averageCreditsPerCall: rec.averageCreditsPerCall,
|
|
24984
|
+
recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
|
|
24985
|
+
lengthRaisedCeiling: rec.lengthRaisedCeiling,
|
|
24846
24986
|
cutOffSamples: rec.cutOffSamples,
|
|
24847
24987
|
noRecommendationReason: rec.noRecommendationReason
|
|
24848
24988
|
};
|
|
24849
24989
|
const savedTo = await saveBench(cwd, record);
|
|
24850
24990
|
if (opts.json) {
|
|
24851
|
-
const ok =
|
|
24991
|
+
const ok = settledCount > 0 && !lengthBlocked;
|
|
24852
24992
|
writeJsonLine({
|
|
24853
24993
|
command: "llm bench",
|
|
24854
24994
|
status: ok ? "ok" : "failed",
|
|
24855
24995
|
error: ok ? null : rec.noRecommendationReason ?? "no_sample_settled",
|
|
24996
|
+
currency: "credits",
|
|
24856
24997
|
apiUrl,
|
|
24857
24998
|
projectId,
|
|
24858
24999
|
modelId,
|
|
@@ -24861,35 +25002,54 @@ async function runBench(args) {
|
|
|
24861
25002
|
schemaPath: opts.schemaPath ?? null,
|
|
24862
25003
|
samples,
|
|
24863
25004
|
completed: settledCount,
|
|
24864
|
-
|
|
25005
|
+
maxCreditsPerSample: maxCredits,
|
|
25006
|
+
// Spendable credits before and after the run; null when unreadable.
|
|
24865
25007
|
balanceBefore,
|
|
24866
25008
|
balanceAfter,
|
|
24867
25009
|
results,
|
|
24868
25010
|
// The create-time refusal that ended the run, or null. Beside the
|
|
24869
25011
|
// samples, never among them: no attempt existed and nothing was spent.
|
|
24870
25012
|
refusal: refusal2,
|
|
24871
|
-
|
|
24872
|
-
//
|
|
25013
|
+
// Over the settled samples: the real provider cost (pico-dollar strings
|
|
25014
|
+
// and USD) and the whole credits charged.
|
|
25015
|
+
costUsdPicos: record.cost,
|
|
25016
|
+
costUsd: {
|
|
25017
|
+
p50: rec.costP50 === null ? null : Number(rec.costP50) / 1e12,
|
|
25018
|
+
p95: rec.costP95 === null ? null : Number(rec.costP95) / 1e12,
|
|
25019
|
+
max: rec.costMax === null ? null : Number(rec.costMax) / 1e12
|
|
25020
|
+
},
|
|
25021
|
+
chargedCredits: record.charged,
|
|
25022
|
+
// The answer's LENGTH, priced by the server per sample (`fitCredits`,
|
|
24873
25023
|
// headroom already on the tokens), over the settled samples.
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
|
|
25024
|
+
fitCredits: { p95: rec.fitP95, max: rec.fitMax },
|
|
25025
|
+
recommendedDeclaredHeadroomBps: terms.headroomBps,
|
|
25026
|
+
feeBps: terms.feeBps,
|
|
25027
|
+
usdCentsPerCredit: terms.usdCentsPerCredit,
|
|
25028
|
+
missingTerms: rec.missingTerms,
|
|
25029
|
+
costBasedMaxCredits: rec.costBasedMaxCredits,
|
|
25030
|
+
lengthRaisedCeiling: rec.lengthRaisedCeiling,
|
|
24877
25031
|
cutOffSamples: rec.cutOffSamples,
|
|
24878
25032
|
exceedsStandCap: rec.exceedsStandCap,
|
|
24879
25033
|
noRecommendationReason: rec.noRecommendationReason,
|
|
24880
|
-
|
|
24881
|
-
|
|
24882
|
-
|
|
25034
|
+
recommendedMaxCredits: rec.recommendedMaxCredits,
|
|
25035
|
+
recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
|
|
25036
|
+
averageCreditsPerCall: rec.averageCreditsPerCall,
|
|
25037
|
+
recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
|
|
24883
25038
|
savedTo
|
|
24884
25039
|
});
|
|
24885
25040
|
if (!ok) process.exitCode = 1;
|
|
24886
25041
|
return;
|
|
24887
25042
|
}
|
|
24888
25043
|
log.plain("");
|
|
25044
|
+
const printMeasured = () => {
|
|
25045
|
+
log.plain(c.bold(" Real cost per call"));
|
|
25046
|
+
log.plain(` p50 ${usdFromPicos(rec.costP50)} p95 ${usdFromPicos(rec.costP95)} max ${usdFromPicos(rec.costMax)} (${settledCount} of ${samples} settled)`);
|
|
25047
|
+
log.plain(c.bold(" Charged credits"));
|
|
25048
|
+
log.plain(` p50 ${rec.chargedP50} p95 ${rec.chargedP95} max ${rec.chargedMax} (whole credits, rounded up per call)`);
|
|
25049
|
+
};
|
|
24889
25050
|
if (lengthBlocked) {
|
|
24890
|
-
if (
|
|
24891
|
-
|
|
24892
|
-
log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
|
|
25051
|
+
if (settledCount > 0) {
|
|
25052
|
+
printMeasured();
|
|
24893
25053
|
log.plain("");
|
|
24894
25054
|
}
|
|
24895
25055
|
printRecommendation(log, record);
|
|
@@ -24897,17 +25057,16 @@ async function runBench(args) {
|
|
|
24897
25057
|
process.exitCode = 1;
|
|
24898
25058
|
return;
|
|
24899
25059
|
}
|
|
24900
|
-
if (
|
|
25060
|
+
if (settledCount === 0) {
|
|
24901
25061
|
log.error(
|
|
24902
|
-
providerRefused > 0 && providerRefused === results.length ? " No sample ran \u2014 the provider refused every attempt at its door \u2014 so there is nothing to
|
|
25062
|
+
providerRefused > 0 && providerRefused === results.length ? " No sample ran \u2014 the provider refused every attempt at its door \u2014 so there is nothing to measure from." : " No sample settled, so there is nothing to measure from."
|
|
24903
25063
|
);
|
|
24904
25064
|
process.exitCode = 1;
|
|
24905
25065
|
return;
|
|
24906
25066
|
}
|
|
24907
|
-
|
|
24908
|
-
log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
|
|
25067
|
+
printMeasured();
|
|
24909
25068
|
if (balanceBefore !== null && balanceAfter !== null) {
|
|
24910
|
-
log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter}
|
|
25069
|
+
log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} credits spendable`);
|
|
24911
25070
|
}
|
|
24912
25071
|
log.plain("");
|
|
24913
25072
|
printRecommendation(log, record);
|
|
@@ -24919,52 +25078,71 @@ function printRecommendation(log, record) {
|
|
|
24919
25078
|
return;
|
|
24920
25079
|
}
|
|
24921
25080
|
if (record.noRecommendationReason === "answer_cut_off") {
|
|
24922
|
-
log.warn(` ${benchCutOffLine(record.cutOffSamples
|
|
25081
|
+
log.warn(` ${benchCutOffLine(record.cutOffSamples || 1, record.maxCreditsPerSample)}`);
|
|
24923
25082
|
return;
|
|
24924
25083
|
}
|
|
24925
|
-
|
|
24926
|
-
|
|
25084
|
+
const attempts = `${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}`;
|
|
25085
|
+
if (record.recommendedMaxCredits === null || record.cost.p95 === null) {
|
|
25086
|
+
log.warn(" No ceiling recommended.");
|
|
24927
25087
|
log.dim(
|
|
24928
|
-
record.recommendedDeclaredHeadroomBps === null ? " This stand served no recommended headroom, and the multiplier is the server's to set \u2014" : "
|
|
25088
|
+
record.cost.p95 === null ? " No sample settled, so there is no p95 to build a ceiling on \u2014" : record.recommendedDeclaredHeadroomBps === null ? " This stand served no recommended headroom, and the multiplier is the server's to set \u2014" : " This stand served no platform fee or credit value, and those are the server's to set \u2014"
|
|
24929
25089
|
);
|
|
24930
25090
|
log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
|
|
25091
|
+
printAverage(log, record, attempts);
|
|
24931
25092
|
return;
|
|
24932
25093
|
}
|
|
24933
|
-
log.plain(c.bold(` Declare
|
|
24934
|
-
if (record.
|
|
25094
|
+
log.plain(c.bold(` Declare maxCredits: ${record.recommendedMaxCredits}`));
|
|
25095
|
+
if (record.lengthRaisedCeiling) {
|
|
24935
25096
|
log.plain(` ${LLM_LENGTH_RAISED_LINE}`);
|
|
24936
25097
|
log.dim(
|
|
24937
|
-
` = the smallest
|
|
25098
|
+
` = the smallest ceiling whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
|
|
24938
25099
|
);
|
|
24939
25100
|
log.dim(
|
|
24940
|
-
`
|
|
25101
|
+
` cost-based ${record.costBasedMaxCredits ?? "\u2014"} (p95 real cost ${usdFromPicos(record.cost.p95)} with the platform fee, ${record.feeBps} bps, and ${record.recommendedDeclaredHeadroomBps} bps of headroom), over ${attempts}.`
|
|
24941
25102
|
);
|
|
24942
25103
|
} else {
|
|
24943
25104
|
log.dim(
|
|
24944
|
-
` = p95
|
|
25105
|
+
` = p95 real cost (${usdFromPicos(record.cost.p95)}) with this stand's platform fee (${record.feeBps} bps) and recommended headroom (${record.recommendedDeclaredHeadroomBps} bps), in whole credits,`
|
|
24945
25106
|
);
|
|
24946
|
-
log.dim(` measured over ${
|
|
25107
|
+
log.dim(` measured over ${attempts}.`);
|
|
24947
25108
|
}
|
|
24948
|
-
log.dim(" That number is a
|
|
24949
|
-
|
|
24950
|
-
|
|
25109
|
+
log.dim(" That number is a CEILING, not a price: a call is billed its real cost as used, never more than it \u2014");
|
|
25110
|
+
log.dim(" and it also sizes how long the answer may be.");
|
|
25111
|
+
if (record.recommendedPerCallMaxCredits !== null) {
|
|
25112
|
+
log.plain(c.bold(` Grant perCallMaxCredits: ${record.recommendedPerCallMaxCredits}`));
|
|
24951
25113
|
log.dim(
|
|
24952
|
-
record.
|
|
25114
|
+
record.lengthRaisedCeiling ? ` = the same over the worst sample (${usdFromPicos(record.cost.max)}) or the longest answer's ceiling, whichever is larger, and never below the ceiling above it.` : ` = the same over the worst sample (${usdFromPicos(record.cost.max)}), and never below the ceiling above it.`
|
|
24953
25115
|
);
|
|
24954
|
-
log.dim(" The ceiling is the price's room to be wrong; declaring one under the price is refused.");
|
|
24955
25116
|
}
|
|
25117
|
+
printAverage(log, record, attempts);
|
|
24956
25118
|
}
|
|
24957
|
-
|
|
24958
|
-
if (
|
|
25119
|
+
function printAverage(log, record, attempts) {
|
|
25120
|
+
if (record.averageCreditsPerCall === null) return;
|
|
25121
|
+
log.plain(` Per-call estimate: about ${formatCredits(record.averageCreditsPerCall)} credits per call`);
|
|
25122
|
+
log.dim(` = the average real cost with the platform fee, no headroom, over ${attempts}.`);
|
|
25123
|
+
if (record.recommendedPerCallEstimateCredits !== null) {
|
|
25124
|
+
log.plain(c.bold(` Grant perCallEstimateCredits: ${record.recommendedPerCallEstimateCredits}`));
|
|
25125
|
+
log.dim(` = that average rounded up to a whole credit. disclosure.estimatedCreditsPerPeriod = your calls per period \xD7 ${formatCredits(record.averageCreditsPerCall)}, rounded up.`);
|
|
25126
|
+
}
|
|
25127
|
+
}
|
|
25128
|
+
async function explainBenchRefusal(res, call2, base, log, opts, index, settledSoFar) {
|
|
25129
|
+
if (printedStructuredError(res)) {
|
|
25130
|
+
const printed = await res.json().catch(() => ({}));
|
|
25131
|
+
return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
|
|
25132
|
+
}
|
|
24959
25133
|
const body = await res.json().catch(() => ({}));
|
|
24960
25134
|
const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
|
|
24961
25135
|
if (res.status === 429 && body.error === "generation_limit") {
|
|
24962
|
-
const counts = serverSlotCounts(body) ?? await readSlotCounts(
|
|
25136
|
+
const counts = serverSlotCounts(body) ?? await readSlotCounts(call2, base);
|
|
24963
25137
|
refusal2.slotsHeld = counts?.held ?? null;
|
|
24964
25138
|
refusal2.slotLimit = counts?.limit ?? null;
|
|
24965
25139
|
if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
|
|
24966
25140
|
return { stop: true, refusal: refusal2 };
|
|
24967
25141
|
}
|
|
25142
|
+
if (res.status === 403 && body.error === "credits_unverified") {
|
|
25143
|
+
if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
|
|
25144
|
+
return { stop: true, refusal: refusal2 };
|
|
25145
|
+
}
|
|
24968
25146
|
if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
|
|
24969
25147
|
if (res.status === 404 && body.error === "not_found") {
|
|
24970
25148
|
log.error(` ${LLM_LANE_OFF_LINE}`);
|
|
@@ -24980,7 +25158,7 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
|
|
|
24980
25158
|
return { stop: true, refusal: refusal2 };
|
|
24981
25159
|
}
|
|
24982
25160
|
if (res.status === 402) {
|
|
24983
|
-
log.error(" Not enough
|
|
25161
|
+
log.error(" Not enough credits to start the attempt \u2014 a bench is paid from your own balance.");
|
|
24984
25162
|
return { stop: true, refusal: refusal2 };
|
|
24985
25163
|
}
|
|
24986
25164
|
log.error(
|
|
@@ -24989,24 +25167,24 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
|
|
|
24989
25167
|
return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
|
|
24990
25168
|
}
|
|
24991
25169
|
function printProviderRefusal(log, status2, message, awaitingBill = false) {
|
|
24992
|
-
if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so this call
|
|
25170
|
+
if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so what this call reserved stays held until it resolves (see \`genex llm status\`); it is not a sample.`);
|
|
24993
25171
|
else log.dim(" The provider refused this call at its door \u2014 no inference ran, it cost nothing, and it is not a sample.");
|
|
24994
25172
|
if (message) log.dim(` Provider said: ${message}`);
|
|
24995
25173
|
if (status2 === 401 || status2 === 403) {
|
|
24996
25174
|
log.dim(` A ${status2} here is this stand's provider configuration refusing the model \u2014 a message for the operator, not something to fix in the game.`);
|
|
24997
25175
|
}
|
|
24998
25176
|
}
|
|
24999
|
-
async function readSlotCounts(
|
|
25000
|
-
const res = await
|
|
25177
|
+
async function readSlotCounts(call2, base) {
|
|
25178
|
+
const res = await call2(`${base}?scope=open`).catch(() => null);
|
|
25001
25179
|
if (!res?.ok) return null;
|
|
25002
25180
|
const body = await res.json().catch(() => null);
|
|
25003
25181
|
return serverSlotCounts(body);
|
|
25004
25182
|
}
|
|
25005
|
-
async function pollSettled(
|
|
25183
|
+
async function pollSettled(call2, url, timeoutSec) {
|
|
25006
25184
|
const deadline = Date.now() + timeoutSec * 1e3;
|
|
25007
25185
|
let last = null;
|
|
25008
25186
|
while (Date.now() < deadline) {
|
|
25009
|
-
const res = await
|
|
25187
|
+
const res = await call2(url).catch(() => null);
|
|
25010
25188
|
if (res?.ok) {
|
|
25011
25189
|
last = await res.json().catch(() => null);
|
|
25012
25190
|
if (last?.billingStatus === "final") return last;
|
|
@@ -25015,19 +25193,6 @@ async function pollSettled(call, url, timeoutSec) {
|
|
|
25015
25193
|
}
|
|
25016
25194
|
return last;
|
|
25017
25195
|
}
|
|
25018
|
-
async function readCoinBalance(apiUrl, token) {
|
|
25019
|
-
try {
|
|
25020
|
-
const res = await apiFetch(`${apiUrl}/api/coin/balance`, {
|
|
25021
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
25022
|
-
signal: AbortSignal.timeout(6e3)
|
|
25023
|
-
});
|
|
25024
|
-
if (!res.ok) return null;
|
|
25025
|
-
const body = await res.json().catch(() => null);
|
|
25026
|
-
return typeof body?.spendable === "number" ? body.spendable : null;
|
|
25027
|
-
} catch {
|
|
25028
|
-
return null;
|
|
25029
|
-
}
|
|
25030
|
-
}
|
|
25031
25196
|
async function saveBench(cwd, record) {
|
|
25032
25197
|
try {
|
|
25033
25198
|
const file = path36.join(cwd, LLM_BENCH_FILE);
|
|
@@ -25043,9 +25208,10 @@ function benchFailed(opts, log, message) {
|
|
|
25043
25208
|
else log.error(message);
|
|
25044
25209
|
process.exitCode = 1;
|
|
25045
25210
|
}
|
|
25046
|
-
function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples,
|
|
25211
|
+
function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts) {
|
|
25047
25212
|
return {
|
|
25048
25213
|
command: "llm bench",
|
|
25214
|
+
currency: "credits",
|
|
25049
25215
|
apiUrl,
|
|
25050
25216
|
projectId,
|
|
25051
25217
|
modelId: opts.modelId ?? null,
|
|
@@ -25054,29 +25220,36 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoi
|
|
|
25054
25220
|
schemaPath: opts.schemaPath ?? null,
|
|
25055
25221
|
samples,
|
|
25056
25222
|
completed: 0,
|
|
25057
|
-
|
|
25223
|
+
maxCreditsPerSample: maxCredits,
|
|
25058
25224
|
balanceBefore: null,
|
|
25059
25225
|
balanceAfter: null,
|
|
25060
25226
|
results: [],
|
|
25061
25227
|
refusal: null,
|
|
25062
|
-
|
|
25063
|
-
|
|
25064
|
-
|
|
25065
|
-
|
|
25228
|
+
costUsdPicos: { p50: null, p95: null, max: null },
|
|
25229
|
+
costUsd: { p50: null, p95: null, max: null },
|
|
25230
|
+
chargedCredits: { p50: null, p95: null, max: null },
|
|
25231
|
+
fitCredits: { p95: null, max: null },
|
|
25232
|
+
recommendedDeclaredHeadroomBps: null,
|
|
25233
|
+
feeBps: null,
|
|
25234
|
+
usdCentsPerCredit: null,
|
|
25235
|
+
missingTerms: null,
|
|
25236
|
+
costBasedMaxCredits: null,
|
|
25237
|
+
lengthRaisedCeiling: false,
|
|
25066
25238
|
cutOffSamples: 0,
|
|
25067
25239
|
exceedsStandCap: false,
|
|
25068
25240
|
noRecommendationReason: null,
|
|
25069
|
-
|
|
25070
|
-
|
|
25071
|
-
|
|
25241
|
+
recommendedMaxCredits: null,
|
|
25242
|
+
recommendedPerCallMaxCredits: null,
|
|
25243
|
+
averageCreditsPerCall: null,
|
|
25244
|
+
recommendedPerCallEstimateCredits: null,
|
|
25072
25245
|
savedTo: null
|
|
25073
25246
|
};
|
|
25074
25247
|
}
|
|
25075
25248
|
async function reportStatus(args) {
|
|
25076
25249
|
const { apiUrl, token, projectId, opts, log } = args;
|
|
25077
|
-
const
|
|
25250
|
+
const call2 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
|
|
25078
25251
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
25079
|
-
const res = await
|
|
25252
|
+
const res = await call2(`${base}?scope=open`).catch(() => null);
|
|
25080
25253
|
if (!res || !res.ok) {
|
|
25081
25254
|
if (res && printedStructuredError(res)) {
|
|
25082
25255
|
if (opts.json) writeJsonLine(statusJson(apiUrl, projectId, "failed", `http_${res.status}`, [], null, null));
|
|
@@ -25131,7 +25304,8 @@ async function reportStatus(args) {
|
|
|
25131
25304
|
for (const row of rows) {
|
|
25132
25305
|
const state = rowState(row);
|
|
25133
25306
|
const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
|
|
25134
|
-
const
|
|
25307
|
+
const money2 = rowMoney(row);
|
|
25308
|
+
const reserved = money2.reserved === null ? "reserved \u2014" : `${money2.reserved} ${money2.unit} reserved`;
|
|
25135
25309
|
log.plain(
|
|
25136
25310
|
` ${mark} ${row.id ?? "?"} ${row.modelId ?? "?"} ${state.padEnd(13)} ${formatAge(row.createdAt, now).padStart(4)} old ${reserved}${row.error ? ` ${row.error}` : ""}${rowHoldsSlot(row) ? "" : c.dim(" (no slot)")}`
|
|
25137
25311
|
);
|
|
@@ -25166,14 +25340,14 @@ function statusJson(apiUrl, projectId, status2, error, rows, slotsHeld, slotLimi
|
|
|
25166
25340
|
}
|
|
25167
25341
|
async function cancelCall(args) {
|
|
25168
25342
|
const { apiUrl, token, projectId, id, opts, log } = args;
|
|
25169
|
-
const
|
|
25343
|
+
const call2 = (url, init) => apiFetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...init?.headers ?? {} } });
|
|
25170
25344
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
25171
25345
|
const rowUrl = `${base}/${encodeURIComponent(id)}`;
|
|
25172
25346
|
const done = (status2, error, reason, row2) => {
|
|
25173
25347
|
if (opts.json) writeJsonLine({ command: "llm cancel", status: status2, error, id, cancelled: reason === "cancelled", reason, generation: row2 });
|
|
25174
25348
|
if (status2 === "failed") process.exitCode = 1;
|
|
25175
25349
|
};
|
|
25176
|
-
const read = await
|
|
25350
|
+
const read = await call2(rowUrl).catch(() => null);
|
|
25177
25351
|
if (!read || !read.ok) {
|
|
25178
25352
|
if (read && printedStructuredError(read)) return done("failed", `http_${read.status}`, null, null);
|
|
25179
25353
|
const body = read ? await read.json().catch(() => ({})) : {};
|
|
@@ -25203,15 +25377,16 @@ async function cancelCall(args) {
|
|
|
25203
25377
|
if (!opts.json) {
|
|
25204
25378
|
log.warn(` ${row.id} (${row.status ?? "?"}) \u2014 ${sentence}.`);
|
|
25205
25379
|
if (state === "awaiting bill") {
|
|
25206
|
-
log.dim(` Nothing to cancel:
|
|
25380
|
+
log.dim(` Nothing to cancel: what it reserved releases when the bill resolves, and it stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`);
|
|
25207
25381
|
if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
|
|
25208
25382
|
} else {
|
|
25209
|
-
|
|
25383
|
+
const money2 = rowMoney(row);
|
|
25384
|
+
log.dim(` ${money2.charged === null ? "charge unknown" : `${money2.charged} ${money2.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
|
|
25210
25385
|
}
|
|
25211
25386
|
}
|
|
25212
25387
|
return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
|
|
25213
25388
|
}
|
|
25214
|
-
const res = await
|
|
25389
|
+
const res = await call2(`${rowUrl}/cancel`, { method: "POST" }).catch(() => null);
|
|
25215
25390
|
if (!res || !res.ok) {
|
|
25216
25391
|
if (res && printedStructuredError(res)) return done("failed", `http_${res.status}`, null, row);
|
|
25217
25392
|
const body = res ? await res.json().catch(() => ({})) : {};
|
|
@@ -25228,78 +25403,126 @@ async function cancelCall(args) {
|
|
|
25228
25403
|
const after = await res.json().catch(() => null) ?? row;
|
|
25229
25404
|
if (!opts.json) {
|
|
25230
25405
|
log.success(` Cancelled ${row.id} (was ${row.status ?? "active"}).`);
|
|
25231
|
-
log.dim(`
|
|
25406
|
+
log.dim(` What it reserved releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
|
|
25232
25407
|
}
|
|
25233
25408
|
return done("ok", null, "cancelled", after);
|
|
25234
25409
|
}
|
|
25235
|
-
async function
|
|
25236
|
-
let
|
|
25410
|
+
async function readSavedBench(cwd) {
|
|
25411
|
+
let parsed;
|
|
25237
25412
|
try {
|
|
25238
|
-
|
|
25413
|
+
parsed = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
|
|
25239
25414
|
} catch {
|
|
25240
|
-
|
|
25415
|
+
return null;
|
|
25241
25416
|
}
|
|
25242
|
-
if (!
|
|
25417
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
25418
|
+
const v = parsed;
|
|
25419
|
+
if (v.v === 2 && v.currency === "credits" && v.cost && v.charged) return { kind: "credits", record: parsed };
|
|
25420
|
+
return { kind: "coins", record: parsed };
|
|
25421
|
+
}
|
|
25422
|
+
var LLM_PRICE_COIN_TERMS_LINE = "This bench was measured under the old COIN terms, when a game declared a fixed price per call. In-game calls are now billed in the player's credits, as used, and a game declares a per-call CEILING (maxCredits) instead \u2014 a coin price is not that number. Re-run the bench for the figures to declare.";
|
|
25423
|
+
async function reportSavedPrice(cwd, opts, log) {
|
|
25424
|
+
const saved = await readSavedBench(cwd);
|
|
25425
|
+
const rerun = 'genex llm bench "<prompt>" --max-credits <n> --user-approved';
|
|
25426
|
+
const creditKeys = (record2) => ({
|
|
25427
|
+
costUsdPicos: record2?.cost ?? { p50: null, p95: null, max: null },
|
|
25428
|
+
chargedCredits: record2?.charged ?? { p50: null, p95: null, max: null },
|
|
25429
|
+
fitP95: record2?.fitP95 ?? null,
|
|
25430
|
+
fitMax: record2?.fitMax ?? null,
|
|
25431
|
+
recommendedDeclaredHeadroomBps: record2?.recommendedDeclaredHeadroomBps ?? null,
|
|
25432
|
+
feeBps: record2?.feeBps ?? null,
|
|
25433
|
+
usdCentsPerCredit: record2?.usdCentsPerCredit ?? null,
|
|
25434
|
+
costBasedMaxCredits: record2?.costBasedMaxCredits ?? null,
|
|
25435
|
+
lengthRaisedCeiling: record2?.lengthRaisedCeiling ?? false,
|
|
25436
|
+
cutOffSamples: record2?.cutOffSamples ?? 0,
|
|
25437
|
+
noRecommendationReason: record2?.noRecommendationReason ?? null,
|
|
25438
|
+
recommendedMaxCredits: record2?.recommendedMaxCredits ?? null,
|
|
25439
|
+
recommendedPerCallMaxCredits: record2?.recommendedPerCallMaxCredits ?? null,
|
|
25440
|
+
averageCreditsPerCall: record2?.averageCreditsPerCall ?? null,
|
|
25441
|
+
recommendedPerCallEstimateCredits: record2?.recommendedPerCallEstimateCredits ?? null
|
|
25442
|
+
});
|
|
25443
|
+
if (!saved) {
|
|
25243
25444
|
if (opts.json) {
|
|
25244
25445
|
writeJsonLine({
|
|
25245
25446
|
command: "llm price",
|
|
25246
25447
|
status: "failed",
|
|
25247
25448
|
error: "no_bench",
|
|
25449
|
+
currency: null,
|
|
25248
25450
|
ranAt: null,
|
|
25249
25451
|
modelId: null,
|
|
25250
25452
|
samples: null,
|
|
25251
25453
|
completed: null,
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
max: null,
|
|
25255
|
-
fitP95: null,
|
|
25256
|
-
chargedBasedEstimateCoins: null,
|
|
25257
|
-
lengthRaisedPrice: false,
|
|
25258
|
-
cutOffSamples: 0,
|
|
25259
|
-
noRecommendationReason: null,
|
|
25260
|
-
recommendedDeclaredHeadroomBps: null,
|
|
25261
|
-
recommendedEstimateCoins: null,
|
|
25262
|
-
recommendedPerCallMaxCoins: null,
|
|
25454
|
+
...creditKeys(null),
|
|
25455
|
+
legacyCoinTerms: null,
|
|
25263
25456
|
savedTo: null
|
|
25264
25457
|
});
|
|
25265
25458
|
} else {
|
|
25266
25459
|
log.error("Nothing has been benchmarked in this folder yet.");
|
|
25267
|
-
log.dim(` Run ${c.cyan(
|
|
25460
|
+
log.dim(` Run ${c.cyan(rerun)} first.`);
|
|
25461
|
+
}
|
|
25462
|
+
process.exitCode = 1;
|
|
25463
|
+
return;
|
|
25464
|
+
}
|
|
25465
|
+
if (saved.kind === "coins") {
|
|
25466
|
+
const old = saved.record;
|
|
25467
|
+
const legacy = {
|
|
25468
|
+
p50: old.p50 ?? null,
|
|
25469
|
+
p95: old.p95 ?? null,
|
|
25470
|
+
max: old.max ?? null,
|
|
25471
|
+
recommendedEstimateCoins: old.recommendedEstimateCoins ?? null,
|
|
25472
|
+
recommendedPerCallMaxCoins: old.recommendedPerCallMaxCoins ?? null,
|
|
25473
|
+
maxCoinsPerSample: old.maxCoinsPerSample ?? null,
|
|
25474
|
+
noRecommendationReason: old.noRecommendationReason ?? null
|
|
25475
|
+
};
|
|
25476
|
+
if (opts.json) {
|
|
25477
|
+
writeJsonLine({
|
|
25478
|
+
command: "llm price",
|
|
25479
|
+
status: "failed",
|
|
25480
|
+
error: "coin_terms",
|
|
25481
|
+
currency: "coins",
|
|
25482
|
+
ranAt: old.ranAt ?? null,
|
|
25483
|
+
modelId: old.modelId ?? null,
|
|
25484
|
+
samples: old.samples ?? null,
|
|
25485
|
+
completed: old.completed ?? null,
|
|
25486
|
+
...creditKeys(null),
|
|
25487
|
+
legacyCoinTerms: legacy,
|
|
25488
|
+
savedTo: LLM_BENCH_FILE
|
|
25489
|
+
});
|
|
25490
|
+
} else {
|
|
25491
|
+
log.plain(c.bold("genex llm price"));
|
|
25492
|
+
log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${old.ranAt ?? "at an unknown time"}${old.modelId ? ` on ${old.modelId}` : ""}`);
|
|
25493
|
+
log.plain("");
|
|
25494
|
+
log.warn(` ${LLM_PRICE_COIN_TERMS_LINE}`);
|
|
25495
|
+
const measured = legacy.p95 === null ? "no settled sample" : `charged coins p50 ${legacy.p50} \xB7 p95 ${legacy.p95} \xB7 max ${legacy.max}`;
|
|
25496
|
+
const recommended = legacy.recommendedEstimateCoins === null ? "no price recommended" : `it recommended estimateCoins ${legacy.recommendedEstimateCoins}${legacy.recommendedPerCallMaxCoins != null ? ` and perCallMaxCoins ${legacy.recommendedPerCallMaxCoins}` : ""}`;
|
|
25497
|
+
log.dim(` What it measured then: ${measured}; ${recommended}.`);
|
|
25498
|
+
log.dim(` Re-run: ${c.cyan(rerun)}`);
|
|
25268
25499
|
}
|
|
25269
25500
|
process.exitCode = 1;
|
|
25270
25501
|
return;
|
|
25271
25502
|
}
|
|
25503
|
+
const record = saved.record;
|
|
25272
25504
|
if (opts.json) {
|
|
25273
25505
|
writeJsonLine({
|
|
25274
25506
|
command: "llm price",
|
|
25275
|
-
status: record.
|
|
25276
|
-
error: record.
|
|
25507
|
+
status: record.recommendedMaxCredits === null ? "failed" : "ok",
|
|
25508
|
+
error: record.recommendedMaxCredits === null ? record.noRecommendationReason ?? "no_recommendation" : null,
|
|
25509
|
+
currency: "credits",
|
|
25277
25510
|
ranAt: record.ranAt ?? null,
|
|
25278
25511
|
modelId: record.modelId ?? null,
|
|
25279
25512
|
samples: record.samples ?? null,
|
|
25280
25513
|
completed: record.completed ?? null,
|
|
25281
|
-
|
|
25282
|
-
|
|
25283
|
-
max: record.max ?? null,
|
|
25284
|
-
// Absent from a file an older CLI wrote; null / false / 0 then, never missing.
|
|
25285
|
-
fitP95: record.fitP95 ?? null,
|
|
25286
|
-
chargedBasedEstimateCoins: record.chargedBasedEstimateCoins ?? null,
|
|
25287
|
-
lengthRaisedPrice: record.lengthRaisedPrice ?? false,
|
|
25288
|
-
cutOffSamples: record.cutOffSamples ?? 0,
|
|
25289
|
-
noRecommendationReason: record.noRecommendationReason ?? null,
|
|
25290
|
-
recommendedDeclaredHeadroomBps: record.recommendedDeclaredHeadroomBps ?? null,
|
|
25291
|
-
recommendedEstimateCoins: record.recommendedEstimateCoins ?? null,
|
|
25292
|
-
recommendedPerCallMaxCoins: record.recommendedPerCallMaxCoins ?? null,
|
|
25514
|
+
...creditKeys(record),
|
|
25515
|
+
legacyCoinTerms: null,
|
|
25293
25516
|
savedTo: LLM_BENCH_FILE
|
|
25294
25517
|
});
|
|
25295
|
-
if (record.
|
|
25518
|
+
if (record.recommendedMaxCredits === null) process.exitCode = 1;
|
|
25296
25519
|
return;
|
|
25297
25520
|
}
|
|
25298
25521
|
log.plain(c.bold("genex llm price"));
|
|
25299
25522
|
log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
|
|
25300
25523
|
log.plain("");
|
|
25301
25524
|
printRecommendation(log, record);
|
|
25302
|
-
if (record.
|
|
25525
|
+
if (record.recommendedMaxCredits === null) process.exitCode = 1;
|
|
25303
25526
|
}
|
|
25304
25527
|
function writeJsonLine(value) {
|
|
25305
25528
|
process.stdout.write(`${JSON.stringify(value)}
|
|
@@ -25559,7 +25782,7 @@ function runtimeRow(state, lane, toolsOnly = false) {
|
|
|
25559
25782
|
return {
|
|
25560
25783
|
label: "Runtime",
|
|
25561
25784
|
value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}${keyRefused ? ` \xB7 ${c.yellow("provider key refused")}` : ""}`,
|
|
25562
|
-
fix: keyRefused ? LLM_PROVIDER_KEY_REJECTED_LINE : toolsOnly ? convertFix : '
|
|
25785
|
+
fix: keyRefused ? LLM_PROVIDER_KEY_REJECTED_LINE : toolsOnly ? convertFix : 'Measure a call before a game declares its per-call ceiling: `npx genex llm bench "<prompt>" --max-credits <n> --user-approved`.'
|
|
25563
25786
|
};
|
|
25564
25787
|
}
|
|
25565
25788
|
case "off":
|
|
@@ -26119,14 +26342,14 @@ var PlayerClient = class {
|
|
|
26119
26342
|
this.version = options.version ?? getCliVersion();
|
|
26120
26343
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
26121
26344
|
}
|
|
26122
|
-
async call(
|
|
26345
|
+
async call(path47, init = {}, provider) {
|
|
26123
26346
|
const headers = {
|
|
26124
26347
|
Authorization: `Bearer ${this.token}`,
|
|
26125
26348
|
[PLAYER_VERSION_HEADER]: this.version,
|
|
26126
26349
|
...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
|
|
26127
26350
|
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
26128
26351
|
};
|
|
26129
|
-
const res = await apiFetch(`${this.apiUrl}${
|
|
26352
|
+
const res = await apiFetch(`${this.apiUrl}${path47}`, {
|
|
26130
26353
|
...init,
|
|
26131
26354
|
headers,
|
|
26132
26355
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
@@ -27450,6 +27673,727 @@ function writeJsonLine2(value) {
|
|
|
27450
27673
|
`);
|
|
27451
27674
|
}
|
|
27452
27675
|
|
|
27676
|
+
// src/lib/native-build.ts
|
|
27677
|
+
import { createHash as createHash3 } from "crypto";
|
|
27678
|
+
import { createReadStream } from "fs";
|
|
27679
|
+
import fs43 from "fs/promises";
|
|
27680
|
+
import os13 from "os";
|
|
27681
|
+
import path46 from "path";
|
|
27682
|
+
import { execFile as execFile2 } from "child_process";
|
|
27683
|
+
import { promisify as promisify2 } from "util";
|
|
27684
|
+
|
|
27685
|
+
// src/lib/native-zip.ts
|
|
27686
|
+
import fs42 from "fs/promises";
|
|
27687
|
+
import zlib2 from "zlib";
|
|
27688
|
+
var EOCD_SIGNATURE = 101010256;
|
|
27689
|
+
var EOCD64_LOCATOR_SIGNATURE = 117853008;
|
|
27690
|
+
var EOCD64_SIGNATURE = 101075792;
|
|
27691
|
+
var CENTRAL_SIGNATURE = 33639248;
|
|
27692
|
+
var LOCAL_SIGNATURE = 67324752;
|
|
27693
|
+
var EOCD_MIN = 22;
|
|
27694
|
+
var COMMENT_MAX = 65535;
|
|
27695
|
+
var ZIP64_EXTRA = 1;
|
|
27696
|
+
var U32_MAX = 4294967295;
|
|
27697
|
+
var U16_MAX = 65535;
|
|
27698
|
+
var DIRECTORY_MAX_BYTES = 256 * 1024 * 1024;
|
|
27699
|
+
var UNIX_HOST = 3;
|
|
27700
|
+
var INPUT_MIN = 64 * 1024;
|
|
27701
|
+
var INPUT_MAX = 1024 * 1024;
|
|
27702
|
+
var S_IFMT = 61440;
|
|
27703
|
+
var S_IFLNK = 40960;
|
|
27704
|
+
var isDirectory = (entry) => entry.name.endsWith("/");
|
|
27705
|
+
var isSymlink = (entry) => entry.mode !== null && (entry.mode & S_IFMT) === S_IFLNK;
|
|
27706
|
+
async function readAt2(file, position, length) {
|
|
27707
|
+
const buffer = Buffer.alloc(length);
|
|
27708
|
+
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
27709
|
+
return buffer.subarray(0, bytesRead);
|
|
27710
|
+
}
|
|
27711
|
+
async function findDirectory(file, size) {
|
|
27712
|
+
const tailLength = Math.min(size, EOCD_MIN + COMMENT_MAX);
|
|
27713
|
+
const tail = await readAt2(file, size - tailLength, tailLength);
|
|
27714
|
+
let at = -1;
|
|
27715
|
+
for (let i = tail.length - EOCD_MIN; i >= 0; i--) {
|
|
27716
|
+
if (tail.readUInt32LE(i) === EOCD_SIGNATURE) {
|
|
27717
|
+
at = i;
|
|
27718
|
+
break;
|
|
27719
|
+
}
|
|
27720
|
+
}
|
|
27721
|
+
if (at < 0) throw new Error("not a zip archive (no end-of-directory record)");
|
|
27722
|
+
let count = tail.readUInt16LE(at + 10);
|
|
27723
|
+
let bytes = tail.readUInt32LE(at + 12);
|
|
27724
|
+
let offset = tail.readUInt32LE(at + 16);
|
|
27725
|
+
const zip64 = count === U16_MAX || bytes === U32_MAX || offset === U32_MAX;
|
|
27726
|
+
const locatorAt = at - 20;
|
|
27727
|
+
if (zip64 && locatorAt >= 0 && tail.readUInt32LE(locatorAt) === EOCD64_LOCATOR_SIGNATURE) {
|
|
27728
|
+
const recordOffset = Number(tail.readBigUInt64LE(locatorAt + 8));
|
|
27729
|
+
const record = await readAt2(file, recordOffset, 56);
|
|
27730
|
+
if (record.length < 56 || record.readUInt32LE(0) !== EOCD64_SIGNATURE) throw new Error("broken zip64 directory record");
|
|
27731
|
+
count = Number(record.readBigUInt64LE(32));
|
|
27732
|
+
bytes = Number(record.readBigUInt64LE(40));
|
|
27733
|
+
offset = Number(record.readBigUInt64LE(48));
|
|
27734
|
+
}
|
|
27735
|
+
if (bytes > DIRECTORY_MAX_BYTES || offset + bytes > size) throw new Error("zip directory is out of bounds");
|
|
27736
|
+
return { offset, bytes, count };
|
|
27737
|
+
}
|
|
27738
|
+
function applyZip64(extra, entry, overflowed) {
|
|
27739
|
+
for (let at = 0; at + 4 <= extra.length; ) {
|
|
27740
|
+
const id = extra.readUInt16LE(at);
|
|
27741
|
+
const length = extra.readUInt16LE(at + 2);
|
|
27742
|
+
if (id === ZIP64_EXTRA) {
|
|
27743
|
+
let field = at + 4;
|
|
27744
|
+
const next = () => {
|
|
27745
|
+
const value = Number(extra.readBigUInt64LE(field));
|
|
27746
|
+
field += 8;
|
|
27747
|
+
return value;
|
|
27748
|
+
};
|
|
27749
|
+
if (overflowed.uncompressed) entry.uncompressedSize = next();
|
|
27750
|
+
if (overflowed.compressed) entry.compressedSize = next();
|
|
27751
|
+
if (overflowed.offset) entry.localOffset = next();
|
|
27752
|
+
return;
|
|
27753
|
+
}
|
|
27754
|
+
at += 4 + length;
|
|
27755
|
+
}
|
|
27756
|
+
}
|
|
27757
|
+
async function readZipEntries(filePath) {
|
|
27758
|
+
const file = await fs42.open(filePath, "r");
|
|
27759
|
+
try {
|
|
27760
|
+
const { size } = await file.stat();
|
|
27761
|
+
const directory = await findDirectory(file, size);
|
|
27762
|
+
const data = await readAt2(file, directory.offset, directory.bytes);
|
|
27763
|
+
const entries = [];
|
|
27764
|
+
let at = 0;
|
|
27765
|
+
while (entries.length < directory.count && at + 46 <= data.length) {
|
|
27766
|
+
if (data.readUInt32LE(at) !== CENTRAL_SIGNATURE) throw new Error("broken zip directory entry");
|
|
27767
|
+
const madeBy = data.readUInt16LE(at + 4);
|
|
27768
|
+
const nameLength = data.readUInt16LE(at + 28);
|
|
27769
|
+
const extraLength = data.readUInt16LE(at + 30);
|
|
27770
|
+
const commentLength = data.readUInt16LE(at + 32);
|
|
27771
|
+
const external = data.readUInt32LE(at + 38);
|
|
27772
|
+
const entry = {
|
|
27773
|
+
name: data.toString("utf8", at + 46, at + 46 + nameLength),
|
|
27774
|
+
method: data.readUInt16LE(at + 10),
|
|
27775
|
+
compressedSize: data.readUInt32LE(at + 20),
|
|
27776
|
+
uncompressedSize: data.readUInt32LE(at + 24),
|
|
27777
|
+
localOffset: data.readUInt32LE(at + 42),
|
|
27778
|
+
mode: madeBy >> 8 === UNIX_HOST ? external >>> 16 : null
|
|
27779
|
+
};
|
|
27780
|
+
applyZip64(data.subarray(at + 46 + nameLength, at + 46 + nameLength + extraLength), entry, {
|
|
27781
|
+
uncompressed: entry.uncompressedSize === U32_MAX,
|
|
27782
|
+
compressed: entry.compressedSize === U32_MAX,
|
|
27783
|
+
offset: entry.localOffset === U32_MAX
|
|
27784
|
+
});
|
|
27785
|
+
entries.push(entry);
|
|
27786
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
27787
|
+
}
|
|
27788
|
+
if (entries.length !== directory.count) throw new Error("zip directory ends early");
|
|
27789
|
+
return entries;
|
|
27790
|
+
} finally {
|
|
27791
|
+
await file.close();
|
|
27792
|
+
}
|
|
27793
|
+
}
|
|
27794
|
+
async function readZipEntryHead(filePath, entry, maxBytes) {
|
|
27795
|
+
const file = await fs42.open(filePath, "r");
|
|
27796
|
+
try {
|
|
27797
|
+
const local = await readAt2(file, entry.localOffset, 30);
|
|
27798
|
+
if (local.length < 30 || local.readUInt32LE(0) !== LOCAL_SIGNATURE) throw new Error(`broken zip entry ${entry.name}`);
|
|
27799
|
+
const start = entry.localOffset + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
|
|
27800
|
+
if (entry.method === 0) return await readAt2(file, start, Math.min(maxBytes, entry.uncompressedSize));
|
|
27801
|
+
if (entry.method !== 8) throw new Error(`${entry.name} uses a compression this command cannot read`);
|
|
27802
|
+
const input = await readAt2(file, start, Math.min(entry.compressedSize, INPUT_MAX, Math.max(maxBytes, INPUT_MIN)));
|
|
27803
|
+
return await inflateHead(input, maxBytes);
|
|
27804
|
+
} finally {
|
|
27805
|
+
await file.close();
|
|
27806
|
+
}
|
|
27807
|
+
}
|
|
27808
|
+
function inflateHead(input, maxBytes) {
|
|
27809
|
+
return new Promise((resolve) => {
|
|
27810
|
+
const inflater = zlib2.createInflateRaw();
|
|
27811
|
+
const chunks = [];
|
|
27812
|
+
let total = 0;
|
|
27813
|
+
const finish2 = () => resolve(Buffer.concat(chunks).subarray(0, maxBytes));
|
|
27814
|
+
inflater.on("data", (chunk) => {
|
|
27815
|
+
chunks.push(chunk);
|
|
27816
|
+
total += chunk.length;
|
|
27817
|
+
if (total >= maxBytes) inflater.destroy();
|
|
27818
|
+
});
|
|
27819
|
+
inflater.on("error", finish2);
|
|
27820
|
+
inflater.on("close", finish2);
|
|
27821
|
+
inflater.end(input);
|
|
27822
|
+
});
|
|
27823
|
+
}
|
|
27824
|
+
|
|
27825
|
+
// src/lib/native-build.ts
|
|
27826
|
+
var run4 = promisify2(execFile2);
|
|
27827
|
+
var NATIVE_PLATFORMS = ["mac", "windows", "linux"];
|
|
27828
|
+
var NATIVE_ARCHES = ["arm64", "x64", "universal"];
|
|
27829
|
+
var VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
27830
|
+
var VERSION_MAX = 64;
|
|
27831
|
+
var OS_VERSION = /^\d{1,4}(\.\d{1,6}){0,3}$/;
|
|
27832
|
+
var ENGINE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
27833
|
+
var HEADER_BYTES = 4096;
|
|
27834
|
+
var PLIST_BYTES = 1024 * 1024;
|
|
27835
|
+
var VERSION_SHA_CHARS = 12;
|
|
27836
|
+
var ENTRY_DEPTH_MAX = 4;
|
|
27837
|
+
var MACHO_64_LE = 4277009103;
|
|
27838
|
+
var FAT_MAGIC = 3405691582;
|
|
27839
|
+
var FAT_MAGIC_64 = 3405691583;
|
|
27840
|
+
var CPU_X86_64 = 16777223;
|
|
27841
|
+
var CPU_ARM64 = 16777228;
|
|
27842
|
+
var PE_X64 = 34404;
|
|
27843
|
+
var PE_ARM64 = 43620;
|
|
27844
|
+
var ELF_X64 = 62;
|
|
27845
|
+
var ELF_ARM64 = 183;
|
|
27846
|
+
var HELPER_EXE = /crash|prereq|redist|setup|unins/i;
|
|
27847
|
+
var NativeBuildError = class extends Error {
|
|
27848
|
+
};
|
|
27849
|
+
var isPlatform = (value) => NATIVE_PLATFORMS.includes(value);
|
|
27850
|
+
var isArch = (value) => NATIVE_ARCHES.includes(value);
|
|
27851
|
+
function rootNames(entries) {
|
|
27852
|
+
return new Set(entries.map((e) => e.name.split("/")[0]).filter(Boolean));
|
|
27853
|
+
}
|
|
27854
|
+
function rootApps(entries) {
|
|
27855
|
+
return [...rootNames(entries)].filter((name) => name.toLowerCase().endsWith(".app") && entries.some((e) => e.name.startsWith(`${name}/`)));
|
|
27856
|
+
}
|
|
27857
|
+
function launchableExes(entries) {
|
|
27858
|
+
const exes = entries.filter((e) => !isDirectory(e) && e.name.toLowerCase().endsWith(".exe"));
|
|
27859
|
+
const games = exes.filter((e) => e.name.split("/").length <= ENTRY_DEPTH_MAX && !HELPER_EXE.test(path46.posix.basename(e.name)));
|
|
27860
|
+
const depth = (name) => name.split("/").length;
|
|
27861
|
+
return games.map((e) => e.name).sort((a, b) => depth(a) - depth(b) || a.localeCompare(b));
|
|
27862
|
+
}
|
|
27863
|
+
function platformOf(entries, asked) {
|
|
27864
|
+
if (asked !== void 0) {
|
|
27865
|
+
if (!isPlatform(asked)) throw new NativeBuildError(`--platform must be one of ${NATIVE_PLATFORMS.join(", ")}.`);
|
|
27866
|
+
return asked;
|
|
27867
|
+
}
|
|
27868
|
+
if (rootApps(entries).length > 0) return "mac";
|
|
27869
|
+
if (launchableExes(entries).length > 0) return "windows";
|
|
27870
|
+
const nested = entries.find((e) => /^[^/]+\/[^/]+\.app\/$/i.test(e.name));
|
|
27871
|
+
if (nested) {
|
|
27872
|
+
throw new NativeBuildError(
|
|
27873
|
+
`The Mac app sits inside a folder (${nested.name.slice(0, -1)}). Pass the .app itself, or zip it so it is at the archive's root.`
|
|
27874
|
+
);
|
|
27875
|
+
}
|
|
27876
|
+
throw new NativeBuildError("Couldn't tell which system this build is for. Pass --platform mac|windows|linux.");
|
|
27877
|
+
}
|
|
27878
|
+
function entryOf(entries, platform, asked) {
|
|
27879
|
+
const files = new Set(entries.map((e) => isDirectory(e) ? e.name.slice(0, -1) : e.name));
|
|
27880
|
+
if (asked !== void 0) {
|
|
27881
|
+
const entry = asked.replace(/\/+$/, "");
|
|
27882
|
+
if (!files.has(entry)) throw new NativeBuildError(`--entry ${asked} is not in the archive.`);
|
|
27883
|
+
return entry;
|
|
27884
|
+
}
|
|
27885
|
+
if (platform === "mac") {
|
|
27886
|
+
const apps = rootApps(entries);
|
|
27887
|
+
if (apps.length === 1) return apps[0];
|
|
27888
|
+
throw new NativeBuildError(
|
|
27889
|
+
apps.length === 0 ? "No .app at the archive's root. Pass the Mac app itself, or zip it so it is at the root." : `More than one app at the archive's root (${apps.join(", ")}). Pass --entry "<name>.app".`
|
|
27890
|
+
);
|
|
27891
|
+
}
|
|
27892
|
+
if (platform === "windows") {
|
|
27893
|
+
const exes = launchableExes(entries);
|
|
27894
|
+
const shallowest = exes.filter((name) => name.split("/").length === exes[0]?.split("/").length);
|
|
27895
|
+
if (shallowest.length === 1) return shallowest[0];
|
|
27896
|
+
throw new NativeBuildError(
|
|
27897
|
+
exes.length === 0 ? "No .exe in the archive. Pass --entry with the program to run." : `More than one program could be the game (${shallowest.join(", ")}). Pass --entry "<path>.exe".`
|
|
27898
|
+
);
|
|
27899
|
+
}
|
|
27900
|
+
throw new NativeBuildError("A Linux build needs --entry: the path of the program to run, from the archive's root.");
|
|
27901
|
+
}
|
|
27902
|
+
function plistString(plist, key) {
|
|
27903
|
+
const match = new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(plist);
|
|
27904
|
+
return match?.[1]?.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/&/g, "&").trim();
|
|
27905
|
+
}
|
|
27906
|
+
function machOArches(head) {
|
|
27907
|
+
const found = /* @__PURE__ */ new Set();
|
|
27908
|
+
const add = (cpu) => {
|
|
27909
|
+
if (cpu === CPU_ARM64) found.add("arm64");
|
|
27910
|
+
if (cpu === CPU_X86_64) found.add("x64");
|
|
27911
|
+
};
|
|
27912
|
+
if (head.length < 8) return found;
|
|
27913
|
+
const magic = head.readUInt32BE(0);
|
|
27914
|
+
if (magic === FAT_MAGIC || magic === FAT_MAGIC_64) {
|
|
27915
|
+
const stride = magic === FAT_MAGIC ? 20 : 32;
|
|
27916
|
+
const count = head.readUInt32BE(4);
|
|
27917
|
+
for (let i = 0; i < count && 8 + i * stride + 4 <= head.length; i++) add(head.readUInt32BE(8 + i * stride));
|
|
27918
|
+
return found;
|
|
27919
|
+
}
|
|
27920
|
+
if (head.readUInt32LE(0) === MACHO_64_LE) add(head.readUInt32LE(4));
|
|
27921
|
+
return found;
|
|
27922
|
+
}
|
|
27923
|
+
function exeArch(head) {
|
|
27924
|
+
if (head.length >= 64 && head.toString("latin1", 0, 2) === "MZ") {
|
|
27925
|
+
const pe = head.readUInt32LE(60);
|
|
27926
|
+
if (pe + 6 > head.length || head.toString("latin1", pe, pe + 4) !== "PE\0\0") return null;
|
|
27927
|
+
const machine = head.readUInt16LE(pe + 4);
|
|
27928
|
+
if (machine === PE_X64) return "x64";
|
|
27929
|
+
if (machine === PE_ARM64) return "arm64";
|
|
27930
|
+
return machine === 332 ? "x86" : null;
|
|
27931
|
+
}
|
|
27932
|
+
if (head.length >= 20 && head.readUInt32BE(0) === 2135247942) {
|
|
27933
|
+
const machine = head.readUInt16LE(18);
|
|
27934
|
+
if (machine === ELF_X64) return "x64";
|
|
27935
|
+
if (machine === ELF_ARM64) return "arm64";
|
|
27936
|
+
}
|
|
27937
|
+
return null;
|
|
27938
|
+
}
|
|
27939
|
+
function engineOf(entries) {
|
|
27940
|
+
const names2 = entries.map((e) => e.name.toLowerCase());
|
|
27941
|
+
const base = (name) => path46.posix.basename(name.replace(/\/$/, ""));
|
|
27942
|
+
if (names2.some((n) => /^unityplayer\.(dll|dylib|so)$/.test(base(n)) || base(n) === "globalgamemanagers")) return "unity";
|
|
27943
|
+
if (names2.some((n) => n.startsWith("engine/binaries/") || /\.app\/contents\/ue[45]?\//.test(n) || n.includes("/engine/binaries/")))
|
|
27944
|
+
return "unreal";
|
|
27945
|
+
if (names2.some((n) => n.endsWith(".pck"))) return "godot";
|
|
27946
|
+
return void 0;
|
|
27947
|
+
}
|
|
27948
|
+
async function describeMacApp(zipPath, entries, app) {
|
|
27949
|
+
const plistEntry = entries.find((e) => e.name === `${app}/Contents/Info.plist`);
|
|
27950
|
+
if (!plistEntry) throw new NativeBuildError(`${app} has no Contents/Info.plist \u2014 is it a complete app?`);
|
|
27951
|
+
const plist = (await readZipEntryHead(zipPath, plistEntry, PLIST_BYTES)).toString("utf8");
|
|
27952
|
+
const readable = plist.includes("<plist");
|
|
27953
|
+
const executable = readable ? plistString(plist, "CFBundleExecutable") : void 0;
|
|
27954
|
+
const binary = executable ? entries.find((e) => e.name === `${app}/Contents/MacOS/${executable}`) : void 0;
|
|
27955
|
+
const arches = binary && !isSymlink(binary) ? machOArches(await readZipEntryHead(zipPath, binary, HEADER_BYTES)) : /* @__PURE__ */ new Set();
|
|
27956
|
+
const arch = arches.size === 2 ? "universal" : [...arches][0];
|
|
27957
|
+
return {
|
|
27958
|
+
arch,
|
|
27959
|
+
minOs: readable ? plistString(plist, "LSMinimumSystemVersion") : void 0,
|
|
27960
|
+
appVersion: readable ? plistString(plist, "CFBundleShortVersionString") : void 0
|
|
27961
|
+
};
|
|
27962
|
+
}
|
|
27963
|
+
async function describeProgram(zipPath, entries, entry) {
|
|
27964
|
+
const file = entries.find((e) => e.name === entry);
|
|
27965
|
+
if (!file || isSymlink(file)) return void 0;
|
|
27966
|
+
const arch = exeArch(await readZipEntryHead(zipPath, file, HEADER_BYTES));
|
|
27967
|
+
if (arch === "x86") throw new NativeBuildError(`${entry} is a 32-bit program. Genex installs 64-bit builds only (x64 or arm64).`);
|
|
27968
|
+
return arch ?? void 0;
|
|
27969
|
+
}
|
|
27970
|
+
function settleArch(read, asked, platform) {
|
|
27971
|
+
if (asked !== void 0 && !isArch(asked)) throw new NativeBuildError(`--arch must be one of ${NATIVE_ARCHES.join(", ")}.`);
|
|
27972
|
+
const arch = asked ?? read;
|
|
27973
|
+
if (!arch) throw new NativeBuildError("Couldn't read which processor this build is for. Pass --arch arm64|x64|universal.");
|
|
27974
|
+
if (arch === "universal" && platform !== "mac") throw new NativeBuildError("Only a Mac app can be universal.");
|
|
27975
|
+
if (read && asked && read !== asked) {
|
|
27976
|
+
throw new NativeBuildError(`--arch ${asked} does not match the build, which is ${read}.`);
|
|
27977
|
+
}
|
|
27978
|
+
return arch;
|
|
27979
|
+
}
|
|
27980
|
+
async function describeNativeArchive(zipPath, asked = {}) {
|
|
27981
|
+
let entries;
|
|
27982
|
+
try {
|
|
27983
|
+
entries = await readZipEntries(zipPath);
|
|
27984
|
+
} catch (err) {
|
|
27985
|
+
throw new NativeBuildError(`Couldn't read ${path46.basename(zipPath)} as a zip: ${err.message}`);
|
|
27986
|
+
}
|
|
27987
|
+
if (entries.length === 0) throw new NativeBuildError("The archive is empty.");
|
|
27988
|
+
const platform = platformOf(entries, asked.platform);
|
|
27989
|
+
const entry = entryOf(entries, platform, asked.entry);
|
|
27990
|
+
const mac = platform === "mac" ? await describeMacApp(zipPath, entries, entry) : void 0;
|
|
27991
|
+
const arch = settleArch(mac ? mac.arch : await describeProgram(zipPath, entries, entry), asked.arch, platform);
|
|
27992
|
+
const minOs = asked.minOs ?? mac?.minOs;
|
|
27993
|
+
if (minOs !== void 0 && !OS_VERSION.test(minOs)) throw new NativeBuildError(`Minimum system "${minOs}" is not a dotted version such as 13.0.`);
|
|
27994
|
+
const engine = (asked.engine ?? engineOf(entries))?.toLowerCase();
|
|
27995
|
+
if (engine !== void 0 && !ENGINE.test(engine)) throw new NativeBuildError(`--engine ${engine} is not an engine id such as unity, godot or unreal.`);
|
|
27996
|
+
const installedBytes = entries.reduce((sum, e) => isDirectory(e) ? sum : sum + e.uncompressedSize, 0);
|
|
27997
|
+
return {
|
|
27998
|
+
platform,
|
|
27999
|
+
arch,
|
|
28000
|
+
entry,
|
|
28001
|
+
installedBytes: Math.max(1, installedBytes),
|
|
28002
|
+
...minOs ? { minOs } : {},
|
|
28003
|
+
...engine ? { engine } : {},
|
|
28004
|
+
...mac?.appVersion ? { appVersion: mac.appVersion } : {}
|
|
28005
|
+
};
|
|
28006
|
+
}
|
|
28007
|
+
function buildVersion(asked, appVersion, sha2562) {
|
|
28008
|
+
if (asked !== void 0) {
|
|
28009
|
+
if (asked.length > VERSION_MAX || !VERSION.test(asked)) {
|
|
28010
|
+
throw new NativeBuildError(`--build-version "${asked}" may hold only letters, digits and . _ + - (at most ${VERSION_MAX}).`);
|
|
28011
|
+
}
|
|
28012
|
+
return asked;
|
|
28013
|
+
}
|
|
28014
|
+
const hash = sha2562.slice(0, VERSION_SHA_CHARS);
|
|
28015
|
+
const app = (appVersion ?? "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, VERSION_MAX - hash.length - 1);
|
|
28016
|
+
return app ? `${app}+${hash}` : hash;
|
|
28017
|
+
}
|
|
28018
|
+
async function hashFile(filePath, onProgress) {
|
|
28019
|
+
const hash = createHash3("sha256");
|
|
28020
|
+
let sizeBytes = 0;
|
|
28021
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
28022
|
+
hash.update(chunk);
|
|
28023
|
+
sizeBytes += chunk.length;
|
|
28024
|
+
onProgress?.(sizeBytes);
|
|
28025
|
+
}
|
|
28026
|
+
return { sha256: hash.digest("hex"), sizeBytes };
|
|
28027
|
+
}
|
|
28028
|
+
async function packTarget(dir) {
|
|
28029
|
+
if (dir.toLowerCase().endsWith(".app")) return { target: dir, keepParent: true };
|
|
28030
|
+
const children = await fs43.readdir(dir, { withFileTypes: true });
|
|
28031
|
+
const apps = children.filter((d) => d.isDirectory() && d.name.toLowerCase().endsWith(".app"));
|
|
28032
|
+
if (apps.length === 1) return { target: path46.join(dir, apps[0].name), keepParent: true };
|
|
28033
|
+
return { target: dir, keepParent: false };
|
|
28034
|
+
}
|
|
28035
|
+
async function packNativeBuild(input) {
|
|
28036
|
+
const stat = await fs43.stat(input).catch(() => null);
|
|
28037
|
+
if (!stat) throw new NativeBuildError(`${input} does not exist.`);
|
|
28038
|
+
if (stat.isFile()) {
|
|
28039
|
+
if (!input.toLowerCase().endsWith(".zip")) throw new NativeBuildError("Pass a .zip, a .app, or the build's folder.");
|
|
28040
|
+
return { zipPath: input, cleanup: async () => {
|
|
28041
|
+
} };
|
|
28042
|
+
}
|
|
28043
|
+
const { target, keepParent } = await packTarget(input);
|
|
28044
|
+
const dir = await fs43.mkdtemp(path46.join(os13.tmpdir(), "genex-native-"));
|
|
28045
|
+
const zipPath = path46.join(dir, `${path46.basename(target).replace(/[^A-Za-z0-9._-]+/g, "-") || "build"}.zip`);
|
|
28046
|
+
const cleanup = () => fs43.rm(dir, { recursive: true, force: true });
|
|
28047
|
+
try {
|
|
28048
|
+
if (process.platform === "darwin") {
|
|
28049
|
+
await run4("ditto", ["-c", "-k", "--sequesterRsrc", ...keepParent ? ["--keepParent"] : [], target, zipPath]);
|
|
28050
|
+
} else if (keepParent) {
|
|
28051
|
+
await run4("zip", ["-q", "-r", "-y", zipPath, path46.basename(target)], { cwd: path46.dirname(target) });
|
|
28052
|
+
} else {
|
|
28053
|
+
await run4("zip", ["-q", "-r", "-y", zipPath, "."], { cwd: target });
|
|
28054
|
+
}
|
|
28055
|
+
} catch (err) {
|
|
28056
|
+
await cleanup();
|
|
28057
|
+
const missing = err.code === "ENOENT";
|
|
28058
|
+
throw new NativeBuildError(
|
|
28059
|
+
missing ? "Couldn't zip the build: no `zip` on this machine. Zip it yourself (keeping links) and pass the .zip." : `Couldn't zip the build: ${err.message}`
|
|
28060
|
+
);
|
|
28061
|
+
}
|
|
28062
|
+
return { zipPath, cleanup };
|
|
28063
|
+
}
|
|
28064
|
+
|
|
28065
|
+
// src/lib/native-upload.ts
|
|
28066
|
+
import fs44 from "fs/promises";
|
|
28067
|
+
var PART_CONCURRENCY = 2;
|
|
28068
|
+
async function call(ctx, route, init, what, log) {
|
|
28069
|
+
let res;
|
|
28070
|
+
try {
|
|
28071
|
+
res = await apiFetch(`${ctx.apiUrl}/api/games/${ctx.projectId}${route}`, {
|
|
28072
|
+
...init,
|
|
28073
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}`, ...init.headers ?? {} }
|
|
28074
|
+
});
|
|
28075
|
+
} catch (err) {
|
|
28076
|
+
log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
|
|
28077
|
+
return null;
|
|
28078
|
+
}
|
|
28079
|
+
if (res.ok) return res;
|
|
28080
|
+
if (res.status === 401) {
|
|
28081
|
+
log.error("Not authorized (HTTP 401) \u2014 your sign-in expired or was revoked.");
|
|
28082
|
+
log.plain(" Fix: run `genex link <your-game>` and sign in as the game's owner, then re-run this command.");
|
|
28083
|
+
return null;
|
|
28084
|
+
}
|
|
28085
|
+
if (res.status === 404 && route.startsWith("/native-upload-token")) {
|
|
28086
|
+
log.error("This account doesn't own this game (HTTP 404), or the API predates native builds.");
|
|
28087
|
+
return null;
|
|
28088
|
+
}
|
|
28089
|
+
if (res.status === 429) {
|
|
28090
|
+
log.error(`Rate limited ${what} (HTTP 429).${retryAfterHint(res)}`);
|
|
28091
|
+
return null;
|
|
28092
|
+
}
|
|
28093
|
+
const reason = await serverReason(res);
|
|
28094
|
+
log.error(`Couldn't ${what} (HTTP ${res.status}).`);
|
|
28095
|
+
if (reason) log.dim(` ${reason}`);
|
|
28096
|
+
return null;
|
|
28097
|
+
}
|
|
28098
|
+
async function requestNativeUpload(ctx, body, log) {
|
|
28099
|
+
const res = await call(ctx, "/native-upload-token", { method: "POST", body: JSON.stringify(body) }, "get an upload token", log);
|
|
28100
|
+
return res ? await res.json() : null;
|
|
28101
|
+
}
|
|
28102
|
+
async function readPart(file, position, length) {
|
|
28103
|
+
const buffer = Buffer.alloc(length);
|
|
28104
|
+
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
28105
|
+
if (bytesRead !== length) throw new Error("the archive changed while it was being uploaded");
|
|
28106
|
+
return buffer;
|
|
28107
|
+
}
|
|
28108
|
+
async function uploadNativeArchive(grant, zipPath, sizeBytes, log, onProgress) {
|
|
28109
|
+
const base = grant.uploadUrl.replace(/\/+$/, "");
|
|
28110
|
+
const encoded = grant.uploadPath.split("/").map(encodeURIComponent).join("/");
|
|
28111
|
+
const auth = { Authorization: `Bearer ${grant.token}` };
|
|
28112
|
+
const file = await fs44.open(zipPath, "r");
|
|
28113
|
+
try {
|
|
28114
|
+
if (sizeBytes <= grant.limits.singlePutMaxBytes) {
|
|
28115
|
+
const body = await readPart(file, 0, sizeBytes);
|
|
28116
|
+
const res = await fetch(`${base}/${encoded}`, { method: "PUT", headers: auth, body }).catch(() => null);
|
|
28117
|
+
if (!res?.ok) {
|
|
28118
|
+
log.error(`Upload failed${res ? ` (HTTP ${res.status})` : " (network error)"}.`);
|
|
28119
|
+
return false;
|
|
28120
|
+
}
|
|
28121
|
+
onProgress?.(sizeBytes);
|
|
28122
|
+
return true;
|
|
28123
|
+
}
|
|
28124
|
+
return await uploadParts(base, encoded, auth, file, sizeBytes, grant.limits.partSizeBytes, log, onProgress);
|
|
28125
|
+
} finally {
|
|
28126
|
+
await file.close();
|
|
28127
|
+
}
|
|
28128
|
+
}
|
|
28129
|
+
async function uploadParts(base, encoded, auth, file, sizeBytes, partSize, log, onProgress) {
|
|
28130
|
+
const created = await fetch(`${base}/${encoded}?action=mpu-create`, { method: "POST", headers: auth }).catch(() => null);
|
|
28131
|
+
if (!created?.ok) {
|
|
28132
|
+
log.error(`Upload failed${created ? ` (HTTP ${created.status} starting multipart)` : " (network error starting multipart)"}.`);
|
|
28133
|
+
return false;
|
|
28134
|
+
}
|
|
28135
|
+
const { uploadId } = await created.json();
|
|
28136
|
+
const totalParts = Math.ceil(sizeBytes / partSize);
|
|
28137
|
+
const parts = [];
|
|
28138
|
+
let sent = 0;
|
|
28139
|
+
let next = 0;
|
|
28140
|
+
let failed = false;
|
|
28141
|
+
const worker = async () => {
|
|
28142
|
+
while (!failed) {
|
|
28143
|
+
const index = next++;
|
|
28144
|
+
if (index >= totalParts) return;
|
|
28145
|
+
const start = index * partSize;
|
|
28146
|
+
const length = Math.min(partSize, sizeBytes - start);
|
|
28147
|
+
const bytes = await readPart(file, start, length);
|
|
28148
|
+
const part = await uploadPartWithRetry(base, encoded, auth, uploadId, index + 1, bytes);
|
|
28149
|
+
if (!part) {
|
|
28150
|
+
failed = true;
|
|
28151
|
+
log.error(`Upload failed (part ${index + 1}/${totalParts}).`);
|
|
28152
|
+
return;
|
|
28153
|
+
}
|
|
28154
|
+
parts.push(part);
|
|
28155
|
+
sent += length;
|
|
28156
|
+
onProgress?.(sent);
|
|
28157
|
+
}
|
|
28158
|
+
};
|
|
28159
|
+
await Promise.all(Array.from({ length: Math.min(PART_CONCURRENCY, totalParts) }, () => worker()));
|
|
28160
|
+
const query = `uploadId=${encodeURIComponent(uploadId)}`;
|
|
28161
|
+
if (!failed) {
|
|
28162
|
+
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
28163
|
+
const done = await fetch(`${base}/${encoded}?action=mpu-complete&${query}`, {
|
|
28164
|
+
method: "POST",
|
|
28165
|
+
headers: { ...auth, "Content-Type": "application/json" },
|
|
28166
|
+
body: JSON.stringify({ parts })
|
|
28167
|
+
}).catch(() => null);
|
|
28168
|
+
if (done?.ok) return true;
|
|
28169
|
+
log.error(`Upload failed${done ? ` (HTTP ${done.status} completing multipart)` : " (network error completing multipart)"}.`);
|
|
28170
|
+
}
|
|
28171
|
+
await fetch(`${base}/${encoded}?action=mpu-abort&${query}`, { method: "DELETE", headers: auth }).catch(() => null);
|
|
28172
|
+
return false;
|
|
28173
|
+
}
|
|
28174
|
+
async function publishNativeBuild(ctx, body, log) {
|
|
28175
|
+
const res = await call(ctx, "/native-builds", { method: "POST", body: JSON.stringify(body) }, "publish the build", log);
|
|
28176
|
+
if (!res) return null;
|
|
28177
|
+
return (await res.json()).build;
|
|
28178
|
+
}
|
|
28179
|
+
async function listNativeBuilds(ctx, log) {
|
|
28180
|
+
const res = await call(ctx, "/native-builds", { method: "GET" }, "list the builds", log);
|
|
28181
|
+
if (!res) return null;
|
|
28182
|
+
return (await res.json()).builds;
|
|
28183
|
+
}
|
|
28184
|
+
async function removeNativeBuild(ctx, platform, arch, log) {
|
|
28185
|
+
const route = `/native-builds/${encodeURIComponent(platform)}/${encodeURIComponent(arch)}`;
|
|
28186
|
+
return await call(ctx, route, { method: "DELETE" }, `remove the ${platform} ${arch} build`, log) !== null;
|
|
28187
|
+
}
|
|
28188
|
+
async function listInGallery(ctx, body, log) {
|
|
28189
|
+
let res;
|
|
28190
|
+
try {
|
|
28191
|
+
res = await apiFetch(`${ctx.apiUrl}/api/projects/${ctx.projectId}/publish`, {
|
|
28192
|
+
method: "POST",
|
|
28193
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ctx.token}` },
|
|
28194
|
+
body: JSON.stringify(body)
|
|
28195
|
+
});
|
|
28196
|
+
} catch (err) {
|
|
28197
|
+
log.error(`Couldn't reach the API at ${ctx.apiUrl}: ${String(err)}`);
|
|
28198
|
+
return null;
|
|
28199
|
+
}
|
|
28200
|
+
const json = await res.json().catch(() => null);
|
|
28201
|
+
if (res.status === 202) {
|
|
28202
|
+
return { state: "held", category: json?.moderation?.category ?? null, detail: json?.moderation?.detail ?? null };
|
|
28203
|
+
}
|
|
28204
|
+
if (res.status === 403 && json?.error === "moderation_refused") {
|
|
28205
|
+
return { state: "refused", category: json.category ?? null, detail: json.detail ?? null };
|
|
28206
|
+
}
|
|
28207
|
+
if (res.ok) return { state: "published", slug: json?.project?.slug ?? null };
|
|
28208
|
+
log.error(`Couldn't list the game in the gallery (HTTP ${res.status}).`);
|
|
28209
|
+
if (json?.error) log.dim(` ${json.error}`);
|
|
28210
|
+
return null;
|
|
28211
|
+
}
|
|
28212
|
+
|
|
28213
|
+
// src/commands/native.ts
|
|
28214
|
+
var PROGRESS_STEPS = 10;
|
|
28215
|
+
var MB2 = 1024 * 1024;
|
|
28216
|
+
function loggerFor2(opts) {
|
|
28217
|
+
if (!opts.json) return createLogger({ quiet: opts.quiet });
|
|
28218
|
+
const err = (s) => {
|
|
28219
|
+
if (!opts.quiet) writeSafely(process.stderr, `${s}
|
|
28220
|
+
`);
|
|
28221
|
+
};
|
|
28222
|
+
return {
|
|
28223
|
+
info: (m) => err(`i ${m}`),
|
|
28224
|
+
success: (m) => err(`\u2713 ${m}`),
|
|
28225
|
+
warn: (m) => err(`! ${m}`),
|
|
28226
|
+
error: (m) => writeSafely(process.stderr, `\u2717 ${m}
|
|
28227
|
+
`),
|
|
28228
|
+
step: (m) => err(`\u203A ${m}`),
|
|
28229
|
+
dim: err,
|
|
28230
|
+
plain: err
|
|
28231
|
+
};
|
|
28232
|
+
}
|
|
28233
|
+
function progress(log, label, total) {
|
|
28234
|
+
let shown = 0;
|
|
28235
|
+
return (done) => {
|
|
28236
|
+
const step = Math.floor(done / Math.max(total, 1) * PROGRESS_STEPS);
|
|
28237
|
+
if (step <= shown) return;
|
|
28238
|
+
shown = step;
|
|
28239
|
+
log.dim(` ${label} ${step * PROGRESS_STEPS}%`);
|
|
28240
|
+
};
|
|
28241
|
+
}
|
|
28242
|
+
async function context3(opts, log) {
|
|
28243
|
+
const meta = await readProject();
|
|
28244
|
+
if (!meta) {
|
|
28245
|
+
log.error("No genex project here. Run `genex init` or `genex link <game>` in this directory first.");
|
|
28246
|
+
return null;
|
|
28247
|
+
}
|
|
28248
|
+
const apiUrl = opts.apiUrl ?? getApiUrl(meta.apiUrl);
|
|
28249
|
+
const token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
|
|
28250
|
+
if (!token) {
|
|
28251
|
+
log.error("Not authorized. Run `genex link <your-game>` to sign in.");
|
|
28252
|
+
return null;
|
|
28253
|
+
}
|
|
28254
|
+
return { apiUrl, projectId: meta.id, token };
|
|
28255
|
+
}
|
|
28256
|
+
function refusedInHostedSession(log) {
|
|
28257
|
+
if (!inHostedSession()) return false;
|
|
28258
|
+
log.error("Native builds are published from the creator's own computer, not from a hosted session.");
|
|
28259
|
+
return true;
|
|
28260
|
+
}
|
|
28261
|
+
function sizeLine(bytes) {
|
|
28262
|
+
return `${(bytes / MB2).toFixed(1)} MB`;
|
|
28263
|
+
}
|
|
28264
|
+
function describeLine(d) {
|
|
28265
|
+
const parts = [`${d.platform} ${d.arch}`, d.entry, `${sizeLine(d.installedBytes)} unpacked`];
|
|
28266
|
+
if (d.minOs) parts.push(`${d.platform === "mac" ? "macOS" : "system"} ${d.minOs}+`);
|
|
28267
|
+
if (d.engine) parts.push(d.engine);
|
|
28268
|
+
return parts.join(" \xB7 ");
|
|
28269
|
+
}
|
|
28270
|
+
async function runPublish2(opts, log) {
|
|
28271
|
+
if (refusedInHostedSession(log)) return false;
|
|
28272
|
+
if (!opts.nativePath) {
|
|
28273
|
+
log.error("Which build? `genex native publish <path>` \u2014 a .zip, a .app, or the build's folder.");
|
|
28274
|
+
return false;
|
|
28275
|
+
}
|
|
28276
|
+
const ctx = await context3(opts, log);
|
|
28277
|
+
if (!ctx) return false;
|
|
28278
|
+
let packed;
|
|
28279
|
+
try {
|
|
28280
|
+
if (!opts.nativePath.toLowerCase().endsWith(".zip")) log.step(`Zipping ${opts.nativePath}\u2026`);
|
|
28281
|
+
packed = await packNativeBuild(opts.nativePath);
|
|
28282
|
+
} catch (err) {
|
|
28283
|
+
if (!(err instanceof NativeBuildError)) throw err;
|
|
28284
|
+
log.error(err.message);
|
|
28285
|
+
return false;
|
|
28286
|
+
}
|
|
28287
|
+
try {
|
|
28288
|
+
const description = await describeNativeArchive(packed.zipPath, opts);
|
|
28289
|
+
log.step(describeLine(description));
|
|
28290
|
+
log.step("Hashing the archive\u2026");
|
|
28291
|
+
const { sha256: sha2562, sizeBytes } = await hashFile(packed.zipPath);
|
|
28292
|
+
const version = buildVersion(opts.buildVersion, description.appVersion, sha2562);
|
|
28293
|
+
const { platform, arch, entry, installedBytes, minOs, engine } = description;
|
|
28294
|
+
const grant = await requestNativeUpload(ctx, { platform, arch, sha256: sha2562, sizeBytes }, log);
|
|
28295
|
+
if (!grant) return false;
|
|
28296
|
+
if (grant.uploaded) {
|
|
28297
|
+
log.step(`Already uploaded (${sizeLine(sizeBytes)}).`);
|
|
28298
|
+
} else {
|
|
28299
|
+
log.step(`Uploading ${sizeLine(sizeBytes)}\u2026`);
|
|
28300
|
+
const sent = await uploadNativeArchive(grant, packed.zipPath, sizeBytes, log, progress(log, "uploaded", sizeBytes));
|
|
28301
|
+
if (!sent) return false;
|
|
28302
|
+
}
|
|
28303
|
+
const build = await publishNativeBuild(
|
|
28304
|
+
ctx,
|
|
28305
|
+
{ platform, arch, sha256: sha2562, sizeBytes, installedBytes, version, entry, ...minOs ? { minOs } : {}, ...engine ? { engine } : {} },
|
|
28306
|
+
log
|
|
28307
|
+
);
|
|
28308
|
+
if (!build) return false;
|
|
28309
|
+
log.success(`Live \u2014 Genex desktop players on ${platform} ${arch} now get version ${build.version}.`);
|
|
28310
|
+
log.dim(` ${build.url}`);
|
|
28311
|
+
const listing = opts.gallery ? await listGame(ctx, opts, log) : void 0;
|
|
28312
|
+
if (opts.json) {
|
|
28313
|
+
console.log(JSON.stringify({ build, engine: engine ?? null, ...listing === void 0 ? {} : { listing } }));
|
|
28314
|
+
}
|
|
28315
|
+
return listing === void 0 || listing?.state === "published" || listing?.state === "held";
|
|
28316
|
+
} catch (err) {
|
|
28317
|
+
if (!(err instanceof NativeBuildError)) throw err;
|
|
28318
|
+
log.error(err.message);
|
|
28319
|
+
return false;
|
|
28320
|
+
} finally {
|
|
28321
|
+
await packed.cleanup();
|
|
28322
|
+
}
|
|
28323
|
+
}
|
|
28324
|
+
async function listGame(ctx, opts, log) {
|
|
28325
|
+
log.step("Listing it in the gallery\u2026");
|
|
28326
|
+
const listing = await listInGallery(
|
|
28327
|
+
ctx,
|
|
28328
|
+
{
|
|
28329
|
+
...opts.title ? { title: opts.title } : {},
|
|
28330
|
+
...opts.description ? { description: opts.description } : {},
|
|
28331
|
+
...opts.categories?.length ? { categories: opts.categories } : {}
|
|
28332
|
+
},
|
|
28333
|
+
log
|
|
28334
|
+
);
|
|
28335
|
+
if (!listing) return null;
|
|
28336
|
+
if (listing.state === "published") {
|
|
28337
|
+
log.success("Listed in the gallery.");
|
|
28338
|
+
return listing;
|
|
28339
|
+
}
|
|
28340
|
+
const why = listing.detail ? `: ${listing.detail}` : ".";
|
|
28341
|
+
if (listing.state === "held") log.warn(`Held for review before it is listed${why}`);
|
|
28342
|
+
else log.error(`The listing was refused${why}`);
|
|
28343
|
+
return listing;
|
|
28344
|
+
}
|
|
28345
|
+
function buildLine(b) {
|
|
28346
|
+
return `${c.bold(`${b.platform} ${b.arch}`)} ${b.version} ${b.entry} ${sizeLine(b.sizeBytes)} ${b.publishedAt}`;
|
|
28347
|
+
}
|
|
28348
|
+
async function runList2(opts, log) {
|
|
28349
|
+
const ctx = await context3(opts, log);
|
|
28350
|
+
if (!ctx) return false;
|
|
28351
|
+
const builds = await listNativeBuilds(ctx, log);
|
|
28352
|
+
if (!builds) return false;
|
|
28353
|
+
if (opts.json) {
|
|
28354
|
+
console.log(JSON.stringify({ builds }));
|
|
28355
|
+
return true;
|
|
28356
|
+
}
|
|
28357
|
+
if (builds.length === 0) {
|
|
28358
|
+
log.plain("No native builds. Publish one with `genex native publish <path>`.");
|
|
28359
|
+
return true;
|
|
28360
|
+
}
|
|
28361
|
+
for (const b of builds) log.plain(buildLine(b));
|
|
28362
|
+
return true;
|
|
28363
|
+
}
|
|
28364
|
+
async function runRemove(opts, log) {
|
|
28365
|
+
if (refusedInHostedSession(log)) return false;
|
|
28366
|
+
if (!opts.platform || !opts.arch) {
|
|
28367
|
+
log.error("Which build? `genex native remove --platform <mac|windows|linux> --arch <arm64|x64|universal> --yes`.");
|
|
28368
|
+
return false;
|
|
28369
|
+
}
|
|
28370
|
+
if (!opts.yes) {
|
|
28371
|
+
log.error(`This stops offering the ${opts.platform} ${opts.arch} build to players. Re-run with --yes to remove it.`);
|
|
28372
|
+
return false;
|
|
28373
|
+
}
|
|
28374
|
+
const ctx = await context3(opts, log);
|
|
28375
|
+
if (!ctx) return false;
|
|
28376
|
+
if (!await removeNativeBuild(ctx, opts.platform, opts.arch, log)) return false;
|
|
28377
|
+
if (opts.json) console.log(JSON.stringify({ removed: { platform: opts.platform, arch: opts.arch } }));
|
|
28378
|
+
log.success(`Removed the ${opts.platform} ${opts.arch} build. Copies already installed keep working.`);
|
|
28379
|
+
return true;
|
|
28380
|
+
}
|
|
28381
|
+
var SUBCOMMANDS2 = {
|
|
28382
|
+
publish: runPublish2,
|
|
28383
|
+
list: runList2,
|
|
28384
|
+
remove: runRemove
|
|
28385
|
+
};
|
|
28386
|
+
async function runNative(opts) {
|
|
28387
|
+
const log = loggerFor2(opts);
|
|
28388
|
+
const run5 = opts.name ? SUBCOMMANDS2[opts.name] : void 0;
|
|
28389
|
+
if (!run5) {
|
|
28390
|
+
log.error("Unknown native command. Use `genex native publish <path>`, `genex native list` or `genex native remove`.");
|
|
28391
|
+
process.exitCode = 1;
|
|
28392
|
+
return;
|
|
28393
|
+
}
|
|
28394
|
+
if (!await run5(opts, log)) process.exitCode = 1;
|
|
28395
|
+
}
|
|
28396
|
+
|
|
27453
28397
|
// src/lib/costs.ts
|
|
27454
28398
|
var TYPICAL_CREDITS = {
|
|
27455
28399
|
model: 35,
|
|
@@ -27515,16 +28459,16 @@ ${c.bold("Usage")}
|
|
|
27515
28459
|
status | cancel. "models" says whether this
|
|
27516
28460
|
stand serves the runtime LLM lane at all, and
|
|
27517
28461
|
at what provider rates. "bench" runs the real
|
|
27518
|
-
model on YOUR OWN
|
|
27519
|
-
attempt
|
|
28462
|
+
model on YOUR OWN credits and reports what each
|
|
28463
|
+
attempt really cost; it needs --max-credits <n>
|
|
27520
28464
|
--user-approved, like any spend the player has
|
|
27521
28465
|
to agree to. "price" reprints the last run's
|
|
27522
|
-
recommended
|
|
28466
|
+
recommended maxCredits. "status" lists this
|
|
27523
28467
|
project's open calls and the slots they hold;
|
|
27524
28468
|
"cancel <id>" stops an active one. Declare a
|
|
27525
|
-
game's
|
|
27526
|
-
a
|
|
27527
|
-
|
|
28469
|
+
game's per-call ceiling from a bench, never
|
|
28470
|
+
from a guess: the player is billed as used, and
|
|
28471
|
+
the ceiling also sizes how long an answer may be.
|
|
27528
28472
|
genex player <sub> [options] Answer the in-game model requests you approve, on your
|
|
27529
28473
|
OWN Claude or ChatGPT subscription: install | run |
|
|
27530
28474
|
status | stop | uninstall. "install" signs this machine
|
|
@@ -27569,6 +28513,11 @@ ${c.bold("Usage")}
|
|
|
27569
28513
|
genex rollback [--yes] Put players back on the previous version. One
|
|
27570
28514
|
version back, instantly \u2014 your code is not
|
|
27571
28515
|
changed, only what the play URL serves.
|
|
28516
|
+
genex native <sub> [options] This game's Mac/Windows/Linux builds for the Genex
|
|
28517
|
+
desktop app: publish <path> | list | remove.
|
|
28518
|
+
"publish" takes a .zip, a .app or the build's
|
|
28519
|
+
folder and makes it what desktop players install
|
|
28520
|
+
for its platform and processor, live at once.
|
|
27572
28521
|
genex cover [<file>] Show this game's cover and where it came from, or
|
|
27573
28522
|
make <file> (PNG/JPEG/WebP, \u22648 MB) the cover: one
|
|
27574
28523
|
real frame of the game \u2014 its signature moment,
|
|
@@ -27986,7 +28935,7 @@ ${c.bold("Examples")}
|
|
|
27986
28935
|
genex shop remove sku_123
|
|
27987
28936
|
genex llm models
|
|
27988
28937
|
genex llm models --all
|
|
27989
|
-
genex llm bench "Reply with one short taunt." --samples 5 --max-
|
|
28938
|
+
genex llm bench "Reply with one short taunt." --samples 5 --max-credits 20 --user-approved
|
|
27990
28939
|
genex llm price
|
|
27991
28940
|
genex llm status
|
|
27992
28941
|
genex llm cancel <id>
|
|
@@ -28223,22 +29172,24 @@ ${c.bold("How the allowance works")}
|
|
|
28223
29172
|
|
|
28224
29173
|
Every number printed is live from your account; prices can change without a deploy.
|
|
28225
29174
|
`,
|
|
28226
|
-
llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what should the game declare?
|
|
29175
|
+
llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what ceiling should the game declare?
|
|
28227
29176
|
|
|
28228
29177
|
${c.bold("Usage")}
|
|
28229
29178
|
genex llm models [--all] [--json] Which models this stand serves, their provider rates per
|
|
28230
|
-
million tokens,
|
|
28231
|
-
|
|
28232
|
-
|
|
28233
|
-
|
|
28234
|
-
on this stand: it says so
|
|
28235
|
-
|
|
28236
|
-
|
|
28237
|
-
|
|
28238
|
-
|
|
28239
|
-
|
|
28240
|
-
|
|
28241
|
-
|
|
29179
|
+
million tokens, the platform fee a call is billed at, and
|
|
29180
|
+
the headroom it recommends over a benchmark. The catalog
|
|
29181
|
+
is synced from OpenRouter, so the FEATURED short list is
|
|
29182
|
+
printed by default and --all prints every row (--json
|
|
29183
|
+
always carries every row). Off on this stand: it says so
|
|
29184
|
+
and exits 0 \u2014 build the feature without a model rather
|
|
29185
|
+
than promising a 404.
|
|
29186
|
+
genex llm bench "<prompt>" --max-credits <n> --user-approved [options]
|
|
29187
|
+
Run the real model through the development lane and
|
|
29188
|
+
report what each attempt really COST and was charged.
|
|
29189
|
+
Refused without both flags, before any network call:
|
|
29190
|
+
this spends YOUR OWN CREDITS, a spend the build's asset
|
|
29191
|
+
allowance does not count. Needs a folder linked to a
|
|
29192
|
+
game you own, and a balance that covers one attempt.
|
|
28242
29193
|
genex llm price [--json] The recommendation from the last bench in this folder.
|
|
28243
29194
|
Reads a file; spends nothing.
|
|
28244
29195
|
genex llm status [--json] This project's open development calls: active, or
|
|
@@ -28255,29 +29206,32 @@ ${c.bold("Bench options")}
|
|
|
28255
29206
|
--samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
|
|
28256
29207
|
--schema <file> A JSON file holding the output schema; implies JSON output.
|
|
28257
29208
|
--json-output Ask for JSON without a schema. --text Ask for text (the default).
|
|
28258
|
-
--max-
|
|
29209
|
+
--max-credits <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own credits,
|
|
28259
29210
|
never a player's. The run's worst case is that number times --samples,
|
|
28260
|
-
and it is printed before anything starts.
|
|
29211
|
+
and it is printed before anything starts. (--max-coins is its deprecated
|
|
29212
|
+
name, read as the same number.)
|
|
28261
29213
|
--timeout <seconds> Bounds the wait for each attempt.
|
|
28262
29214
|
--json One machine-readable object; every key is always present, null when
|
|
28263
29215
|
the CLI could not learn it.
|
|
28264
29216
|
|
|
28265
29217
|
${c.bold("Why a benchmark and not an estimate")}
|
|
28266
|
-
|
|
28267
|
-
|
|
28268
|
-
|
|
28269
|
-
|
|
28270
|
-
|
|
28271
|
-
|
|
28272
|
-
|
|
28273
|
-
|
|
28274
|
-
|
|
29218
|
+
An in-game call is paid from the PLAYER's credits and billed as used: its real provider cost
|
|
29219
|
+
plus the platform fee, never more than the per-call CEILING the game declares (\`maxCredits\`,
|
|
29220
|
+
or \`perCallMaxCredits\` on a standing budget). The ceiling is not a price, but it is still a
|
|
29221
|
+
number with two jobs: it bounds what one call may cost, and it funds how LONG the answer may
|
|
29222
|
+
be. Three layers sit between the provider's rate card and it \u2014 the provider's cost for YOUR
|
|
29223
|
+
prompt, the platform fee, and your headroom \u2014 and only a real call measures the first. The
|
|
29224
|
+
bench is billed to YOU, on the development lane; the player-funded lane is what the
|
|
29225
|
+
published game uses. The recommendation is the p95 real cost with the server's fee and
|
|
29226
|
+
headroom applied, in whole credits; a stand that serves no headroom or no fee gets no
|
|
29227
|
+
recommendation, because those multipliers are not the CLI's to invent.
|
|
28275
29228
|
|
|
28276
|
-
|
|
28277
|
-
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
29229
|
+
Because the ceiling sizes each call's output allowance, the recommendation is never below
|
|
29230
|
+
the smallest ceiling that leaves room for the benchmarked answer, as the server computes it.
|
|
29231
|
+
A sample cut off at your --max-credits (provider_token_limit) is not a sample \u2014 re-run with a
|
|
29232
|
+
higher --max-credits; an answer longer than one call on this stand may produce gets no
|
|
29233
|
+
ceiling at all \u2014 ask for a shorter one. The run also prints the measured average per call,
|
|
29234
|
+
fee included, which is the honest estimate a budget's disclosure is built on.
|
|
28281
29235
|
|
|
28282
29236
|
Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
|
|
28283
29237
|
`,
|
|
@@ -28366,6 +29320,39 @@ ${c.bold("Content filter")}
|
|
|
28366
29320
|
a body) as body horror \u2014 describe the object or machine instead: "a
|
|
28367
29321
|
statue-like figure threaded into the wall" passes where "cables entering her
|
|
28368
29322
|
spine" fails. Rejections are never retryable; reword and re-run.
|
|
29323
|
+
`,
|
|
29324
|
+
native: `${c.bold("genex native")} \u2014 a Unity, Godot or Unreal build players install from the Genex desktop app.
|
|
29325
|
+
|
|
29326
|
+
${c.bold("Usage")}
|
|
29327
|
+
genex native publish <path> Make this build what desktop players on its platform
|
|
29328
|
+
and processor install. <path> is a .zip, a .app, or
|
|
29329
|
+
the build's folder (zipped for you, links and
|
|
29330
|
+
permissions kept). Live as soon as it returns; the
|
|
29331
|
+
build it replaces stays downloadable one more version.
|
|
29332
|
+
genex native list This game's builds: platform, processor, version.
|
|
29333
|
+
genex native remove --platform <p> --arch <a> --yes
|
|
29334
|
+
Stop offering one build. Installed copies keep working.
|
|
29335
|
+
|
|
29336
|
+
${c.bold("Read from the build, or set")}
|
|
29337
|
+
--platform mac|windows|linux A .app at the archive's root is mac, an .exe windows.
|
|
29338
|
+
--arch arm64|x64|universal From the app's executable header.
|
|
29339
|
+
--entry <path> What runs: the root .app, or the game's .exe.
|
|
29340
|
+
--build-version <id> Default: the app's own version + the archive's hash,
|
|
29341
|
+
so every rebuild is an update.
|
|
29342
|
+
--min-os <version> Default: a Mac app's LSMinimumSystemVersion.
|
|
29343
|
+
--engine unity|godot|unreal From the files the engine ships.
|
|
29344
|
+
--json One JSON object on stdout ({"build": \u2026}).
|
|
29345
|
+
|
|
29346
|
+
${c.bold("List it in the gallery")}
|
|
29347
|
+
--gallery After the build is live, list the game in the public
|
|
29348
|
+
gallery, the same moderated listing \`genex publish\`
|
|
29349
|
+
does, without building or deploying a web bundle.
|
|
29350
|
+
--title <t> --description <d> The listing's words (kept as they are when omitted).
|
|
29351
|
+
--categories <list> 1-3, comma-separated, e.g. games.
|
|
29352
|
+
|
|
29353
|
+
${c.bold("Notes")}
|
|
29354
|
+
An archive is at most 4 GB. A Mac app must be signed, at least ad hoc (Unity, Godot
|
|
29355
|
+
and Unreal do this when they build) \u2014 the desktop app refuses an unsigned one.
|
|
28369
29356
|
`,
|
|
28370
29357
|
wait: `${c.bold("genex wait")} \u2014 attach to a generation already running, or just ask how it is doing.
|
|
28371
29358
|
|
|
@@ -28492,11 +29479,13 @@ function parseArgs(argv) {
|
|
|
28492
29479
|
"--limit",
|
|
28493
29480
|
// `genex budget --assets <credits>` — the allowance the player approved.
|
|
28494
29481
|
"--assets",
|
|
28495
|
-
// `genex llm bench` value flags. `--max-
|
|
28496
|
-
//
|
|
29482
|
+
// `genex llm bench` value flags. `--max-credits` is the same approval
|
|
29483
|
+
// shape as `--assets`, but its own gate: the asset allowance does not
|
|
29484
|
+
// count a bench. `--max-coins` is its deprecated name (same number).
|
|
28497
29485
|
"--model",
|
|
28498
29486
|
"--samples",
|
|
28499
29487
|
"--schema",
|
|
29488
|
+
"--max-credits",
|
|
28500
29489
|
"--max-coins",
|
|
28501
29490
|
// `genex player install --label <name>` — what this machine is called in
|
|
28502
29491
|
// the dashboard. Its own key rather than the generic 2nd positional,
|
|
@@ -28519,7 +29508,15 @@ function parseArgs(argv) {
|
|
|
28519
29508
|
"--tags",
|
|
28520
29509
|
// `genex cover --clip <file> --commit <sha>`: the clip and the build it shows.
|
|
28521
29510
|
"--clip",
|
|
28522
|
-
"--commit"
|
|
29511
|
+
"--commit",
|
|
29512
|
+
// `genex native publish|remove` value flags. `--build-version`, never
|
|
29513
|
+
// `--version`, which prints the CLI's own.
|
|
29514
|
+
"--platform",
|
|
29515
|
+
"--arch",
|
|
29516
|
+
"--entry",
|
|
29517
|
+
"--build-version",
|
|
29518
|
+
"--min-os",
|
|
29519
|
+
"--engine"
|
|
28523
29520
|
]);
|
|
28524
29521
|
let i = 0;
|
|
28525
29522
|
while (i < argv.length) {
|
|
@@ -28669,6 +29666,9 @@ function parseArgs(argv) {
|
|
|
28669
29666
|
case "--yes":
|
|
28670
29667
|
parsed.options.yes = true;
|
|
28671
29668
|
break;
|
|
29669
|
+
case "--gallery":
|
|
29670
|
+
parsed.options.gallery = true;
|
|
29671
|
+
break;
|
|
28672
29672
|
// A pair rather than one flag taking a value: `--resellable false` reads
|
|
28673
29673
|
// as a filename to every shell user who has ever mistyped it, and the two
|
|
28674
29674
|
// states have to be distinguishable from ABSENT so `shop set` can leave
|
|
@@ -28793,6 +29793,13 @@ function parseArgs(argv) {
|
|
|
28793
29793
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
28794
29794
|
return parsed;
|
|
28795
29795
|
}
|
|
29796
|
+
} else if (parsed.command === "native") {
|
|
29797
|
+
if (parsed.options.name === "publish" && parsed.options.nativePath === void 0) {
|
|
29798
|
+
parsed.options.nativePath = arg;
|
|
29799
|
+
} else {
|
|
29800
|
+
parsed.error = `Unexpected argument: ${arg}`;
|
|
29801
|
+
return parsed;
|
|
29802
|
+
}
|
|
28796
29803
|
} else if (parsed.command === "blender") {
|
|
28797
29804
|
if (parsed.options.name === "import" && parsed.options.importUrl === void 0) {
|
|
28798
29805
|
parsed.options.importUrl = arg;
|
|
@@ -28840,6 +29847,24 @@ function applyValueFlag(options, flag, value) {
|
|
|
28840
29847
|
case "--dir":
|
|
28841
29848
|
options.dir = value;
|
|
28842
29849
|
break;
|
|
29850
|
+
case "--platform":
|
|
29851
|
+
options.platform = value.toLowerCase();
|
|
29852
|
+
break;
|
|
29853
|
+
case "--arch":
|
|
29854
|
+
options.arch = value.toLowerCase();
|
|
29855
|
+
break;
|
|
29856
|
+
case "--entry":
|
|
29857
|
+
options.entry = value;
|
|
29858
|
+
break;
|
|
29859
|
+
case "--build-version":
|
|
29860
|
+
options.buildVersion = value;
|
|
29861
|
+
break;
|
|
29862
|
+
case "--min-os":
|
|
29863
|
+
options.minOs = value;
|
|
29864
|
+
break;
|
|
29865
|
+
case "--engine":
|
|
29866
|
+
options.engine = value.toLowerCase();
|
|
29867
|
+
break;
|
|
28843
29868
|
case "--env":
|
|
28844
29869
|
options.envPath = value;
|
|
28845
29870
|
break;
|
|
@@ -29014,12 +30039,14 @@ function applyValueFlag(options, flag, value) {
|
|
|
29014
30039
|
options.samples = n;
|
|
29015
30040
|
break;
|
|
29016
30041
|
}
|
|
30042
|
+
case "--max-credits":
|
|
29017
30043
|
case "--max-coins": {
|
|
29018
30044
|
const n = Number(value);
|
|
29019
30045
|
if (!Number.isInteger(n) || n < 1) {
|
|
29020
|
-
throw new Error(`Invalid
|
|
30046
|
+
throw new Error(`Invalid ${flag} value: ${value} (whole credits, 1 or more)`);
|
|
29021
30047
|
}
|
|
29022
|
-
options.
|
|
30048
|
+
if (flag === "--max-credits") options.maxCredits = n;
|
|
30049
|
+
else options.maxCoins = n;
|
|
29023
30050
|
break;
|
|
29024
30051
|
}
|
|
29025
30052
|
case "--timeout": {
|
|
@@ -29317,9 +30344,9 @@ async function main() {
|
|
|
29317
30344
|
await runBudget(parsed.options);
|
|
29318
30345
|
break;
|
|
29319
30346
|
// The in-game model lane: is it live here, what does one call really
|
|
29320
|
-
// cost, what should the game declare. `bench`
|
|
29321
|
-
//
|
|
29322
|
-
//
|
|
30347
|
+
// cost, what ceiling should the game declare. `bench` spends the
|
|
30348
|
+
// builder's own credits OUTSIDE the asset allowance, so it carries its
|
|
30349
|
+
// own --max-credits/--user-approved gate.
|
|
29323
30350
|
case "llm":
|
|
29324
30351
|
await runLlm(parsed.options);
|
|
29325
30352
|
break;
|
|
@@ -29429,6 +30456,9 @@ async function main() {
|
|
|
29429
30456
|
case "rollback":
|
|
29430
30457
|
await runRollback(parsed.options);
|
|
29431
30458
|
break;
|
|
30459
|
+
case "native":
|
|
30460
|
+
await runNative(parsed.options);
|
|
30461
|
+
break;
|
|
29432
30462
|
case "cover":
|
|
29433
30463
|
await runCover({ ...parsed.options, imagePath: parsed.options.name });
|
|
29434
30464
|
break;
|