@genex-ai/cli-demo 1.36.15-dev.1035 → 1.36.15
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-WSURPZET.js → blender-mcp-QVEGHE5L.js} +2 -2
- package/dist/{blender-serve-EH2MOW3U.js → blender-serve-66OHC4ML.js} +1 -1
- package/dist/{chunk-EABIVAGT.js → chunk-4BBTPRJW.js} +2 -2
- package/dist/{chunk-K4EHXOZK.js → chunk-QI7FIYBY.js} +1 -1
- package/dist/index.js +225 -474
- 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 +72 -209
- package/templates/skills/genex-llm-in-games/references/pricing.md +85 -126
- package/templates/skills/genex-monetization/SKILL.md +9 -1
- package/templates/skills/genex-tool-llm/SKILL.md +9 -11
- 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-4BBTPRJW.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-QI7FIYBY.js";
|
|
82
82
|
|
|
83
83
|
// src/instrument.ts
|
|
84
84
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -1134,7 +1134,7 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
1134
1134
|
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.
|
|
1135
1135
|
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.
|
|
1136
1136
|
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.
|
|
1137
|
-
24. **Benchmark before declaring an in-game LLM
|
|
1137
|
+
24. **Benchmark before declaring an in-game LLM price.** 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, and \`estimateCoins\` is the FIXED price of a started attempt: charged whether it succeeds, fails, is canceled, or stops at its own budget. Never pick that number by judgement or from a vendor's rate card. Run the real prompt on your own coins \u2014 \`npx genex llm bench "<the prompt>" --schema <file> --samples 3 --max-coins <n> --user-approved\` \u2014 and declare the figure it recommends, recorded in \`DESIGN.md\` with its model and date; re-benchmark whenever the prompt, the schema or the model changes, because a prompt edit is a price change. 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 is computed from the game's own loop. Model output is data the game validates against its own expectation \u2014 never executed, and never authority over coin, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
|
|
1138
1138
|
${CONTRACT_END}
|
|
1139
1139
|
`;
|
|
1140
1140
|
var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
|
|
@@ -1148,7 +1148,7 @@ var TOOLS_CONTRACT_OFFER = `5. **When the game is built and playable \u2014 neve
|
|
|
1148
1148
|
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.
|
|
1149
1149
|
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.
|
|
1150
1150
|
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.`;
|
|
1151
|
-
var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with
|
|
1151
|
+
var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with Genex coins or their own Claude/ChatGPT subscription, and approves it on a Genex sheet; your game just calls \`generate()\`. Want it that way?`;
|
|
1152
1152
|
function toolsContractLlmLaw(n, hosted) {
|
|
1153
1153
|
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).";
|
|
1154
1154
|
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.`;
|
|
@@ -8739,8 +8739,8 @@ function formatCoins(coins) {
|
|
|
8739
8739
|
function coinPrice(coins, displayUsdCents) {
|
|
8740
8740
|
if (coins === 0) return "Free";
|
|
8741
8741
|
if (displayUsdCents === void 0) return `${formatCoins(coins)} coin`;
|
|
8742
|
-
const
|
|
8743
|
-
return `${formatCoins(coins)} coin ($${
|
|
8742
|
+
const usd2 = (displayUsdCents / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
8743
|
+
return `${formatCoins(coins)} coin ($${usd2})`;
|
|
8744
8744
|
}
|
|
8745
8745
|
function priceGridLine(grid, freeAllowed) {
|
|
8746
8746
|
return `${[...freeAllowed ? ["Free"] : [], ...grid.map(formatCoins)].join(" \xB7 ")} coin`;
|
|
@@ -26011,7 +26011,7 @@ async function runBlender(opts) {
|
|
|
26011
26011
|
return 1;
|
|
26012
26012
|
}
|
|
26013
26013
|
if (sub === "serve") {
|
|
26014
|
-
const { serveLocalBlender } = await import("./blender-serve-
|
|
26014
|
+
const { serveLocalBlender } = await import("./blender-serve-66OHC4ML.js");
|
|
26015
26015
|
const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
|
|
26016
26016
|
log.step(`Starting a local Blender service on port ${port}`);
|
|
26017
26017
|
log.plain(
|
|
@@ -26020,7 +26020,7 @@ async function runBlender(opts) {
|
|
|
26020
26020
|
return serveLocalBlender({ port, log });
|
|
26021
26021
|
}
|
|
26022
26022
|
if (sub === "mcp") {
|
|
26023
|
-
const { runBlenderMcp } = await import("./blender-mcp-
|
|
26023
|
+
const { runBlenderMcp } = await import("./blender-mcp-QVEGHE5L.js");
|
|
26024
26024
|
return runBlenderMcp();
|
|
26025
26025
|
}
|
|
26026
26026
|
if (sub === "seat") {
|
|
@@ -26528,12 +26528,7 @@ import fs39 from "fs/promises";
|
|
|
26528
26528
|
import path39 from "path";
|
|
26529
26529
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
26530
26530
|
var SUBS4 = ["models", "bench", "price", "status", "cancel"];
|
|
26531
|
-
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN
|
|
26532
|
-
var LLM_MAX_COINS_DEPRECATED = "--max-coins is deprecated: it is read as --max-credits, the same number \u2014 a bench spends credits now.";
|
|
26533
|
-
function benchBalanceShortLine(spendable, maxCredits) {
|
|
26534
|
-
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.`;
|
|
26535
|
-
}
|
|
26536
|
-
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.";
|
|
26531
|
+
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN coin \u2014 your wallet, not a player's, and one this build's asset allowance cannot see. Re-run with --max-coins <n> --user-approved only after the person at the keyboard agreed to the number.";
|
|
26537
26532
|
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.";
|
|
26538
26533
|
var LLM_TOOLS_CONVERT_HINT = [
|
|
26539
26534
|
"In-game model calls need a hosted game. This folder is a Genex Tools workspace, so nothing here",
|
|
@@ -26546,7 +26541,7 @@ function serverSlotCounts(body) {
|
|
|
26546
26541
|
return { held: body.slotsHeld, limit: body.slotLimit };
|
|
26547
26542
|
}
|
|
26548
26543
|
var PENDING_SLOT_RELEASE_MINUTES = 10;
|
|
26549
|
-
var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so
|
|
26544
|
+
var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so its coin stays held until the bill resolves; such a row stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`;
|
|
26550
26545
|
var LLM_STATUS_NO_COUNT_LINE = "This stand does not report how many slots are held across your projects, so no count is shown.";
|
|
26551
26546
|
var LLM_CANCEL_AWAITING_BILL = "this call already stopped; its bill is awaiting the provider";
|
|
26552
26547
|
var LLM_CANCEL_ALREADY_FINAL = "this call is already final";
|
|
@@ -26560,33 +26555,11 @@ var DEFAULT_SAMPLE_TIMEOUT_SEC = 180;
|
|
|
26560
26555
|
function parseProviderKey(value) {
|
|
26561
26556
|
return value === "ok" || value === "rejected" || value === "unknown" ? value : null;
|
|
26562
26557
|
}
|
|
26563
|
-
function bpsOrNull(value) {
|
|
26564
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
26565
|
-
}
|
|
26566
|
-
function standingCreditGrantOrNull(value) {
|
|
26567
|
-
if (!value || typeof value !== "object") return null;
|
|
26568
|
-
const v = value;
|
|
26569
|
-
const whole = (x) => typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
|
|
26570
|
-
if (!whole(v.minCredits) || !whole(v.maxCredits) || !whole(v.stepCredits) || !whole(v.defaultCredits)) return null;
|
|
26571
|
-
return {
|
|
26572
|
-
minCredits: v.minCredits,
|
|
26573
|
-
maxCredits: v.maxCredits,
|
|
26574
|
-
stepCredits: v.stepCredits,
|
|
26575
|
-
defaultCredits: v.defaultCredits,
|
|
26576
|
-
...whole(v.minUsdCents) ? { minUsdCents: v.minUsdCents } : {},
|
|
26577
|
-
...whole(v.maxUsdCents) ? { maxUsdCents: v.maxUsdCents } : {},
|
|
26578
|
-
...whole(v.stepUsdCents) ? { stepUsdCents: v.stepUsdCents } : {}
|
|
26579
|
-
};
|
|
26580
|
-
}
|
|
26581
26558
|
async function readRuntimeLane(apiUrl, token) {
|
|
26582
26559
|
const empty = (state) => ({
|
|
26583
26560
|
state,
|
|
26584
26561
|
models: null,
|
|
26585
26562
|
recommendedDeclaredHeadroomBps: null,
|
|
26586
|
-
currency: null,
|
|
26587
|
-
feeBps: null,
|
|
26588
|
-
usdCentsPerCredit: null,
|
|
26589
|
-
standingCreditGrant: null,
|
|
26590
26563
|
externalProviders: null,
|
|
26591
26564
|
total: null,
|
|
26592
26565
|
featuredCount: null,
|
|
@@ -26605,11 +26578,7 @@ async function readRuntimeLane(apiUrl, token) {
|
|
|
26605
26578
|
return {
|
|
26606
26579
|
state: "live",
|
|
26607
26580
|
models: body.models,
|
|
26608
|
-
recommendedDeclaredHeadroomBps:
|
|
26609
|
-
currency: body.currency === "credits" || body.currency === "coins" ? body.currency : null,
|
|
26610
|
-
feeBps: bpsOrNull(body.feeBps),
|
|
26611
|
-
usdCentsPerCredit: typeof body.usdCentsPerCredit === "number" && Number.isSafeInteger(body.usdCentsPerCredit) && body.usdCentsPerCredit >= 1 ? body.usdCentsPerCredit : null,
|
|
26612
|
-
standingCreditGrant: standingCreditGrantOrNull(body.standingCreditGrant),
|
|
26581
|
+
recommendedDeclaredHeadroomBps: typeof body.recommendedDeclaredHeadroomBps === "number" && Number.isFinite(body.recommendedDeclaredHeadroomBps) ? body.recommendedDeclaredHeadroomBps : null,
|
|
26613
26582
|
externalProviders: Array.isArray(body.externalProviders) ? body.externalProviders : null,
|
|
26614
26583
|
total: typeof body.total === "number" && Number.isFinite(body.total) ? body.total : null,
|
|
26615
26584
|
featuredCount: typeof body.featuredCount === "number" && Number.isFinite(body.featuredCount) ? body.featuredCount : null,
|
|
@@ -26625,45 +26594,18 @@ function percentile2(values, p) {
|
|
|
26625
26594
|
const rank2 = Math.ceil(p / 100 * sorted.length);
|
|
26626
26595
|
return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
|
|
26627
26596
|
}
|
|
26628
|
-
function
|
|
26629
|
-
|
|
26630
|
-
const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
26631
|
-
const rank2 = Math.ceil(p / 100 * sorted.length);
|
|
26632
|
-
return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
|
|
26597
|
+
function recommendEstimateCoins(p95, headroomBps) {
|
|
26598
|
+
return Math.max(1, Math.ceil(p95 * (1e4 + headroomBps) / 1e4));
|
|
26633
26599
|
}
|
|
26634
|
-
|
|
26635
|
-
|
|
26636
|
-
if (costPicos <= 0n) return 1;
|
|
26637
|
-
const num = costPicos * BigInt(1e4 + terms.feeBps) * BigInt(1e4 + terms.headroomBps);
|
|
26638
|
-
const den = 100000000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
|
|
26639
|
-
return Math.max(1, Number((num + den - 1n) / den));
|
|
26640
|
-
}
|
|
26641
|
-
function averageCreditsPerCall(costsPicos, terms) {
|
|
26642
|
-
if (costsPicos.length === 0) return null;
|
|
26643
|
-
const sum = costsPicos.reduce((a, b) => a + b, 0n);
|
|
26644
|
-
return Number(sum * BigInt(1e4 + terms.feeBps)) / Number(BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT);
|
|
26645
|
-
}
|
|
26646
|
-
function perCallEstimateCreditsFor(costsPicos, terms) {
|
|
26647
|
-
if (costsPicos.length === 0) return null;
|
|
26648
|
-
const sum = costsPicos.reduce((a, b) => a + b, 0n);
|
|
26649
|
-
const num = sum * BigInt(1e4 + terms.feeBps);
|
|
26650
|
-
const den = BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
|
|
26651
|
-
return Math.max(1, Number((num + den - 1n) / den));
|
|
26652
|
-
}
|
|
26653
|
-
function formatCredits(value) {
|
|
26654
|
-
if (!Number.isFinite(value)) return "\u2014";
|
|
26655
|
-
if (value >= 100) return String(Math.round(value));
|
|
26656
|
-
return String(Number(value.toPrecision(value < 1 ? 2 : 3)));
|
|
26657
|
-
}
|
|
26658
|
-
function usdFromPicos(picos) {
|
|
26659
|
-
if (picos === null || picos === void 0) return "unknown";
|
|
26660
|
-
const value = typeof picos === "bigint" ? picos : /^\d+$/.test(picos) ? BigInt(picos) : null;
|
|
26661
|
-
if (value === null) return "unknown";
|
|
26662
|
-
return `$${(Number(value) / 1e12).toFixed(6)}`;
|
|
26600
|
+
function recommendPerCallMaxCoins(p95, max, headroomBps) {
|
|
26601
|
+
return Math.max(recommendEstimateCoins(p95, headroomBps), recommendEstimateCoins(max, headroomBps));
|
|
26663
26602
|
}
|
|
26664
26603
|
function usdPerMillion(value) {
|
|
26665
26604
|
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
26666
26605
|
}
|
|
26606
|
+
function usd(value) {
|
|
26607
|
+
return value === null ? "unknown" : `$${value.toFixed(6)}`;
|
|
26608
|
+
}
|
|
26667
26609
|
async function runLlm(opts = {}) {
|
|
26668
26610
|
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
26669
26611
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -26689,23 +26631,16 @@ async function runLlm(opts = {}) {
|
|
|
26689
26631
|
return;
|
|
26690
26632
|
}
|
|
26691
26633
|
if (sub === "bench") {
|
|
26692
|
-
if (opts.maxCoins
|
|
26693
|
-
`);
|
|
26694
|
-
if (opts.maxCoins !== void 0 && opts.maxCredits !== void 0 && opts.maxCoins !== opts.maxCredits) {
|
|
26695
|
-
fail5("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
|
|
26696
|
-
return;
|
|
26697
|
-
}
|
|
26698
|
-
const approvedCredits = opts.maxCredits ?? opts.maxCoins;
|
|
26699
|
-
if (approvedCredits === void 0 || opts.userApproved !== true) {
|
|
26634
|
+
if (opts.maxCoins === void 0 || opts.userApproved !== true) {
|
|
26700
26635
|
fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
|
|
26701
26636
|
return;
|
|
26702
26637
|
}
|
|
26703
|
-
if (!Number.isInteger(
|
|
26704
|
-
fail5("llm bench", `--max-
|
|
26638
|
+
if (!Number.isInteger(opts.maxCoins) || opts.maxCoins < 1) {
|
|
26639
|
+
fail5("llm bench", `--max-coins takes a whole number of coin, 1 or more (got ${String(opts.maxCoins)}).`);
|
|
26705
26640
|
return;
|
|
26706
26641
|
}
|
|
26707
26642
|
if (!opts.benchPrompt?.trim()) {
|
|
26708
|
-
fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-
|
|
26643
|
+
fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-coins <n> --user-approved.');
|
|
26709
26644
|
return;
|
|
26710
26645
|
}
|
|
26711
26646
|
if (opts.jsonOutput && opts.textOutput) {
|
|
@@ -26797,10 +26732,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
26797
26732
|
models: null,
|
|
26798
26733
|
externalProviders: null,
|
|
26799
26734
|
recommendedDeclaredHeadroomBps: null,
|
|
26800
|
-
currency: null,
|
|
26801
|
-
feeBps: null,
|
|
26802
|
-
usdCentsPerCredit: null,
|
|
26803
|
-
standingCreditGrant: null,
|
|
26804
26735
|
total: null,
|
|
26805
26736
|
featuredCount: null,
|
|
26806
26737
|
// The stand was never asked, so the verdict is unknown — but the key is
|
|
@@ -26826,12 +26757,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
26826
26757
|
models: lane.models,
|
|
26827
26758
|
externalProviders: lane.externalProviders,
|
|
26828
26759
|
recommendedDeclaredHeadroomBps: lane.recommendedDeclaredHeadroomBps,
|
|
26829
|
-
// The credit terms a call is billed on, the server's — null on a stand
|
|
26830
|
-
// that predates credit billing, never a number the CLI filled in.
|
|
26831
|
-
currency: lane.currency,
|
|
26832
|
-
feeBps: lane.feeBps,
|
|
26833
|
-
usdCentsPerCredit: lane.usdCentsPerCredit,
|
|
26834
|
-
standingCreditGrant: lane.standingCreditGrant,
|
|
26835
26760
|
total: lane.total ?? lane.models?.length ?? null,
|
|
26836
26761
|
featuredCount: lane.featuredCount ?? (lane.models ? lane.models.filter((m) => m.featured).length : null),
|
|
26837
26762
|
// `null` when the lane is not live or the server predates the probe.
|
|
@@ -26878,9 +26803,8 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
26878
26803
|
);
|
|
26879
26804
|
for (const m of shown) {
|
|
26880
26805
|
const plan = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
|
|
26881
|
-
const kind = m.kind === "typesafe" ? " \xB7 judge (classifier): judge() under a budget, not generate() or bench" : "";
|
|
26882
26806
|
log.plain(
|
|
26883
|
-
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}
|
|
26807
|
+
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}`
|
|
26884
26808
|
);
|
|
26885
26809
|
}
|
|
26886
26810
|
if (hiddenCount > 0) {
|
|
@@ -26896,104 +26820,74 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
|
|
|
26896
26820
|
}
|
|
26897
26821
|
log.plain("");
|
|
26898
26822
|
if (lane.recommendedDeclaredHeadroomBps === null) {
|
|
26899
|
-
log.dim(" This stand serves no recommended headroom, so no
|
|
26900
|
-
} else {
|
|
26901
|
-
log.dim(` Recommended headroom over a benchmarked call's cost on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
|
|
26902
|
-
}
|
|
26903
|
-
if (lane.feeBps === null) {
|
|
26904
|
-
log.dim(" This stand serves no platform fee \u2014 it predates credit billing, so no ceiling can be recommended from a bench.");
|
|
26823
|
+
log.dim(" This stand serves no recommended headroom, so no price can be recommended from a bench.");
|
|
26905
26824
|
} else {
|
|
26906
|
-
log.dim(`
|
|
26825
|
+
log.dim(` Recommended headroom over benchmarked coins on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
|
|
26907
26826
|
}
|
|
26908
26827
|
if (toolsOnly) {
|
|
26909
26828
|
printConvertHint();
|
|
26910
26829
|
return;
|
|
26911
26830
|
}
|
|
26912
|
-
log.dim(" Those per-million rates are the PROVIDER's
|
|
26913
|
-
log.dim(`
|
|
26831
|
+
log.dim(" Those per-million rates are the PROVIDER's; what your game is charged is the platform");
|
|
26832
|
+
log.dim(` tariff on top, which only a real call reveals: ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')}`);
|
|
26914
26833
|
}
|
|
26915
26834
|
function wholeOrNull(value) {
|
|
26916
26835
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
26917
26836
|
}
|
|
26918
|
-
function costPicosOf(usage) {
|
|
26919
|
-
if (typeof usage?.costUsdPicos === "string" && /^\d+$/.test(usage.costUsdPicos)) return usage.costUsdPicos;
|
|
26920
|
-
if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) {
|
|
26921
|
-
return String(BigInt(Math.round(usage.costUsd * 1e12)));
|
|
26922
|
-
}
|
|
26923
|
-
return null;
|
|
26924
|
-
}
|
|
26925
26837
|
function sampleFrom(id, settled) {
|
|
26926
26838
|
const usage = settled?.usage;
|
|
26927
|
-
const picos = costPicosOf(usage);
|
|
26928
26839
|
return {
|
|
26929
26840
|
id,
|
|
26930
26841
|
status: settled?.status ?? null,
|
|
26931
26842
|
billingStatus: settled?.billingStatus ?? null,
|
|
26932
|
-
|
|
26933
|
-
costUsd: typeof usage?.costUsd === "number" ? usage.costUsd :
|
|
26934
|
-
costUsdPicos: picos,
|
|
26843
|
+
chargedCoins: typeof settled?.chargedCoins === "number" ? settled.chargedCoins : null,
|
|
26844
|
+
costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : null,
|
|
26935
26845
|
error: settled?.error ?? (settled ? null : "timed_out"),
|
|
26936
26846
|
providerMessage: typeof settled?.providerMessage === "string" && settled.providerMessage.trim() ? settled.providerMessage.trim() : null,
|
|
26937
26847
|
inputTokens: wholeOrNull(usage?.inputTokens),
|
|
26938
26848
|
outputTokens: wholeOrNull(usage?.outputTokens),
|
|
26939
26849
|
maxOutputTokens: wholeOrNull(usage?.maxOutputTokens),
|
|
26940
|
-
|
|
26850
|
+
fitCoins: typeof usage?.fitCoins === "number" && Number.isSafeInteger(usage.fitCoins) && usage.fitCoins >= 1 ? usage.fitCoins : null,
|
|
26941
26851
|
fitExceedsCap: typeof usage?.fitExceedsCap === "boolean" ? usage.fitExceedsCap : null,
|
|
26942
26852
|
truncated: typeof usage?.truncated === "boolean" ? usage.truncated : settled?.error === "provider_token_limit" ? true : null
|
|
26943
26853
|
};
|
|
26944
26854
|
}
|
|
26945
26855
|
var PROVIDER_TOKEN_LIMIT = "provider_token_limit";
|
|
26946
|
-
function benchRecommendation(results,
|
|
26947
|
-
const settled = results.filter(
|
|
26948
|
-
|
|
26949
|
-
);
|
|
26950
|
-
const
|
|
26951
|
-
const
|
|
26952
|
-
const fits = settled.map((r) => r.fitCredits).filter((v) => typeof v === "number");
|
|
26953
|
-
const costP95 = percentilePicos(costs, 95);
|
|
26954
|
-
const costMax = costs.length ? costs.reduce((a, b) => b > a ? b : a) : null;
|
|
26856
|
+
function benchRecommendation(results, headroomBps) {
|
|
26857
|
+
const settled = results.filter((r) => r.status === "succeeded" && r.billingStatus === "final" && typeof r.chargedCoins === "number");
|
|
26858
|
+
const charged = settled.map((r) => r.chargedCoins);
|
|
26859
|
+
const fits = settled.map((r) => r.fitCoins).filter((v) => typeof v === "number");
|
|
26860
|
+
const p95 = percentile2(charged, 95);
|
|
26861
|
+
const max = charged.length ? Math.max(...charged) : null;
|
|
26955
26862
|
const fitP95 = percentile2(fits, 95);
|
|
26956
26863
|
const fitMax = fits.length ? Math.max(...fits) : null;
|
|
26957
26864
|
const exceedsStandCap = results.some((r) => r.fitExceedsCap === true);
|
|
26958
26865
|
const cutOffSamples = results.filter((r) => r.error === PROVIDER_TOKEN_LIMIT && r.fitExceedsCap !== true).length;
|
|
26959
26866
|
const noRecommendationReason = exceedsStandCap ? "answer_exceeds_stand_cap" : cutOffSamples > 0 ? "answer_cut_off" : null;
|
|
26960
|
-
const
|
|
26961
|
-
const
|
|
26962
|
-
const
|
|
26963
|
-
const billing = feeBps !== null && usdCentsPerCredit !== null ? { feeBps, usdCentsPerCredit } : null;
|
|
26964
|
-
const costBased = full !== null && costP95 !== null ? ceilingCreditsFor(costP95, full) : null;
|
|
26965
|
-
const recommended = costBased === null || noRecommendationReason !== null ? null : Math.max(costBased, fitP95 ?? 0);
|
|
26966
|
-
const ceiling = recommended === null || costMax === null || full === null ? null : Math.max(ceilingCreditsFor(costMax, full), recommended, fitMax ?? 0);
|
|
26967
|
-
const average = billing !== null ? averageCreditsPerCall(costs, billing) : null;
|
|
26968
|
-
const estimate = ceiling === null || billing === null ? null : Math.min(ceiling, perCallEstimateCreditsFor(costs, billing) ?? 1);
|
|
26867
|
+
const chargedBased = p95 !== null && headroomBps !== null ? recommendEstimateCoins(p95, headroomBps) : null;
|
|
26868
|
+
const recommended = chargedBased === null || noRecommendationReason !== null ? null : Math.max(chargedBased, fitP95 ?? 0);
|
|
26869
|
+
const ceiling = recommended === null || p95 === null || max === null || headroomBps === null ? null : Math.max(recommendPerCallMaxCoins(p95, max, headroomBps), recommended, fitMax ?? 0);
|
|
26969
26870
|
return {
|
|
26970
|
-
costs,
|
|
26971
26871
|
charged,
|
|
26972
|
-
|
|
26973
|
-
|
|
26974
|
-
|
|
26975
|
-
chargedP50: percentile2(charged, 50),
|
|
26976
|
-
chargedP95: percentile2(charged, 95),
|
|
26977
|
-
chargedMax: charged.length ? Math.max(...charged) : null,
|
|
26872
|
+
p50: percentile2(charged, 50),
|
|
26873
|
+
p95,
|
|
26874
|
+
max,
|
|
26978
26875
|
fits,
|
|
26979
26876
|
fitP95,
|
|
26980
26877
|
fitMax,
|
|
26981
|
-
|
|
26982
|
-
|
|
26983
|
-
|
|
26984
|
-
|
|
26985
|
-
recommendedPerCallEstimateCredits: estimate,
|
|
26986
|
-
lengthRaisedCeiling: recommended !== null && costBased !== null && recommended > costBased,
|
|
26878
|
+
chargedBasedEstimateCoins: chargedBased,
|
|
26879
|
+
recommendedEstimateCoins: recommended,
|
|
26880
|
+
recommendedPerCallMaxCoins: ceiling,
|
|
26881
|
+
lengthRaisedPrice: recommended !== null && chargedBased !== null && recommended > chargedBased,
|
|
26987
26882
|
cutOffSamples,
|
|
26988
26883
|
exceedsStandCap,
|
|
26989
|
-
noRecommendationReason
|
|
26990
|
-
missingTerms
|
|
26884
|
+
noRecommendationReason
|
|
26991
26885
|
};
|
|
26992
26886
|
}
|
|
26993
|
-
var LLM_LENGTH_RAISED_LINE = "The
|
|
26994
|
-
var LLM_EXCEEDS_STAND_CAP_LINE = "No
|
|
26995
|
-
function benchCutOffLine(cutOff,
|
|
26996
|
-
return `No
|
|
26887
|
+
var LLM_LENGTH_RAISED_LINE = "The declared price also pays for how long the answer may be: at the charged-based price the answer would be cut off, so declare this one.";
|
|
26888
|
+
var LLM_EXCEEDS_STAND_CAP_LINE = "No price 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.";
|
|
26889
|
+
function benchCutOffLine(cutOff, maxCoins) {
|
|
26890
|
+
return `No price recommended: ${cutOff} sample${cutOff === 1 ? " was" : "s were"} cut off at this ceiling (--max-coins ${maxCoins}) before the answer was finished, so its real length is unknown. Re-run with a higher --max-coins.`;
|
|
26997
26891
|
}
|
|
26998
26892
|
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["requires_confirmation", "queued", "dispatching", "awaiting_external"]);
|
|
26999
26893
|
function rowState(row) {
|
|
@@ -27004,22 +26898,7 @@ function rowState(row) {
|
|
|
27004
26898
|
function rowHoldsSlot(row) {
|
|
27005
26899
|
if (typeof row.slotHeld === "boolean") return row.slotHeld;
|
|
27006
26900
|
const state = rowState(row);
|
|
27007
|
-
return state === "active" || state === "awaiting bill" && (
|
|
27008
|
-
}
|
|
27009
|
-
function rowMoney(row) {
|
|
27010
|
-
const credits = row.currency === "credits" || row.currency === void 0 && typeof row.reservedCredits === "number" && typeof row.reservedCoins !== "number";
|
|
27011
|
-
if (credits) {
|
|
27012
|
-
return {
|
|
27013
|
-
unit: "credits",
|
|
27014
|
-
reserved: typeof row.reservedCredits === "number" ? row.reservedCredits : null,
|
|
27015
|
-
charged: typeof row.chargedCredits === "number" ? row.chargedCredits : null
|
|
27016
|
-
};
|
|
27017
|
-
}
|
|
27018
|
-
return {
|
|
27019
|
-
unit: "coin",
|
|
27020
|
-
reserved: typeof row.reservedCoins === "number" ? row.reservedCoins : null,
|
|
27021
|
-
charged: typeof row.chargedCoins === "number" ? row.chargedCoins : null
|
|
27022
|
-
};
|
|
26901
|
+
return state === "active" || state === "awaiting bill" && (row.reservedCoins ?? 0) > 0;
|
|
27023
26902
|
}
|
|
27024
26903
|
function providerRefusalStatus(error) {
|
|
27025
26904
|
const m = /^provider_http_(\d{3})$/.exec(error ?? "");
|
|
@@ -27038,7 +26917,7 @@ function formatAge(iso, now = Date.now()) {
|
|
|
27038
26917
|
}
|
|
27039
26918
|
async function runBench(args) {
|
|
27040
26919
|
const { apiUrl, token, projectId, cwd, opts, log } = args;
|
|
27041
|
-
const
|
|
26920
|
+
const maxCoins = opts.maxCoins;
|
|
27042
26921
|
const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
|
|
27043
26922
|
const prompt = opts.benchPrompt.trim();
|
|
27044
26923
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
@@ -27060,7 +26939,7 @@ async function runBench(args) {
|
|
|
27060
26939
|
const lane = await readRuntimeLane(apiUrl, token);
|
|
27061
26940
|
if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
|
|
27062
26941
|
if (lane.state === "off") {
|
|
27063
|
-
if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples,
|
|
26942
|
+
if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts), status: "off", error: null });
|
|
27064
26943
|
else {
|
|
27065
26944
|
log.plain(LLM_LANE_OFF_LINE);
|
|
27066
26945
|
log.dim(" Nothing was spent.");
|
|
@@ -27087,27 +26966,17 @@ async function runBench(args) {
|
|
|
27087
26966
|
);
|
|
27088
26967
|
return;
|
|
27089
26968
|
}
|
|
27090
|
-
const
|
|
27091
|
-
if (walletBefore?.emailVerified === false) {
|
|
27092
|
-
benchFailed(opts, log, LLM_BENCH_UNVERIFIED_LINE);
|
|
27093
|
-
return;
|
|
27094
|
-
}
|
|
27095
|
-
if (walletBefore !== null && walletBefore.spendable < maxCredits) {
|
|
27096
|
-
benchFailed(opts, log, benchBalanceShortLine(walletBefore.spendable, maxCredits));
|
|
27097
|
-
return;
|
|
27098
|
-
}
|
|
27099
|
-
const balanceBefore = walletBefore?.spendable ?? null;
|
|
26969
|
+
const balanceBefore = await readCoinBalance(apiUrl, token);
|
|
27100
26970
|
if (!opts.json) {
|
|
27101
26971
|
log.plain(c.bold("genex llm bench"));
|
|
27102
26972
|
log.dim(` ${apiUrl}`);
|
|
27103
26973
|
log.plain("");
|
|
27104
26974
|
log.plain(` Model ${c.cyan(modelId)}`);
|
|
27105
26975
|
log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
|
|
27106
|
-
log.plain(` Approved ${
|
|
26976
|
+
log.plain(` Approved ${maxCoins} coin per attempt \u2014 at worst ${maxCoins * samples} coin for this run`);
|
|
27107
26977
|
log.plain(
|
|
27108
|
-
` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore}
|
|
26978
|
+
` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} coin spendable`}`
|
|
27109
26979
|
);
|
|
27110
|
-
log.dim(" Billed as used: each attempt is charged its real cost plus the platform fee, rounded up to a whole credit.");
|
|
27111
26980
|
log.plain("");
|
|
27112
26981
|
}
|
|
27113
26982
|
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
@@ -27133,7 +27002,7 @@ async function runBench(args) {
|
|
|
27133
27002
|
const res = await call2(base, {
|
|
27134
27003
|
method: "POST",
|
|
27135
27004
|
body: JSON.stringify({
|
|
27136
|
-
|
|
27005
|
+
maxCoins,
|
|
27137
27006
|
request: {
|
|
27138
27007
|
idempotencyKey: `bench-${randomUUID2()}`,
|
|
27139
27008
|
modelId,
|
|
@@ -27163,14 +27032,13 @@ async function runBench(args) {
|
|
|
27163
27032
|
if (refusedAt !== null) providerRefused++;
|
|
27164
27033
|
if (!opts.json) {
|
|
27165
27034
|
const length = row.outputTokens !== null ? ` \xB7 ${row.outputTokens}${row.maxOutputTokens !== null ? ` of ${row.maxOutputTokens}` : ""} tokens out` : "";
|
|
27166
|
-
const charged = row.chargedCredits === null ? "\u2014" : String(row.chargedCredits);
|
|
27167
27035
|
log.plain(
|
|
27168
|
-
` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${
|
|
27036
|
+
` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${String(row.chargedCoins ?? "\u2014").padStart(4)} coin charged \xB7 provider ${usd(row.costUsd)}${length}${row.error ? ` \xB7 ${row.error}` : ""}`
|
|
27169
27037
|
);
|
|
27170
27038
|
if (refusedAt !== null) printProviderRefusal(log, refusedAt, row.providerMessage, row.status === "unknown");
|
|
27171
27039
|
if (row.error === PROVIDER_TOKEN_LIMIT) {
|
|
27172
27040
|
log.dim(
|
|
27173
|
-
row.fitExceedsCap === true ? " The answer was cut off at this stand's own output limit \u2014 no
|
|
27041
|
+
row.fitExceedsCap === true ? " The answer was cut off at this stand's own output limit \u2014 no price makes room for it; it is not a sample." : ` The answer was cut off at this ceiling (--max-coins ${maxCoins}) before it was finished \u2014 it was charged, and it is not a sample.`
|
|
27174
27042
|
);
|
|
27175
27043
|
}
|
|
27176
27044
|
}
|
|
@@ -27178,19 +27046,16 @@ async function runBench(args) {
|
|
|
27178
27046
|
} finally {
|
|
27179
27047
|
process.removeListener("SIGINT", onSigint);
|
|
27180
27048
|
}
|
|
27181
|
-
const
|
|
27182
|
-
|
|
27183
|
-
|
|
27184
|
-
|
|
27185
|
-
|
|
27186
|
-
const
|
|
27187
|
-
const settledCount = rec.charged.length;
|
|
27049
|
+
const headroomBps = lane.recommendedDeclaredHeadroomBps;
|
|
27050
|
+
const rec = benchRecommendation(results, headroomBps);
|
|
27051
|
+
const { charged, p50, p95, max } = rec;
|
|
27052
|
+
const settledCount = charged.length;
|
|
27053
|
+
const recommended = rec.recommendedEstimateCoins;
|
|
27054
|
+
const ceiling = rec.recommendedPerCallMaxCoins;
|
|
27188
27055
|
const lengthBlocked = rec.noRecommendationReason !== null;
|
|
27189
|
-
const balanceAfter =
|
|
27190
|
-
const picosOrNull = (v) => v === null ? null : String(v);
|
|
27056
|
+
const balanceAfter = await readCoinBalance(apiUrl, token);
|
|
27191
27057
|
const record = {
|
|
27192
|
-
v:
|
|
27193
|
-
currency: "credits",
|
|
27058
|
+
v: 1,
|
|
27194
27059
|
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27195
27060
|
apiUrl,
|
|
27196
27061
|
projectId,
|
|
@@ -27199,34 +27064,28 @@ async function runBench(args) {
|
|
|
27199
27064
|
outputFormat,
|
|
27200
27065
|
samples,
|
|
27201
27066
|
completed: settledCount,
|
|
27202
|
-
|
|
27203
|
-
|
|
27204
|
-
|
|
27205
|
-
|
|
27206
|
-
|
|
27207
|
-
|
|
27067
|
+
chargedCoins: charged,
|
|
27068
|
+
p50,
|
|
27069
|
+
p95,
|
|
27070
|
+
max,
|
|
27071
|
+
recommendedDeclaredHeadroomBps: headroomBps,
|
|
27072
|
+
recommendedEstimateCoins: recommended,
|
|
27073
|
+
recommendedPerCallMaxCoins: ceiling,
|
|
27074
|
+
maxCoinsPerSample: maxCoins,
|
|
27075
|
+
fitCoins: rec.fits,
|
|
27208
27076
|
fitP95: rec.fitP95,
|
|
27209
|
-
|
|
27210
|
-
|
|
27211
|
-
feeBps: terms.feeBps,
|
|
27212
|
-
usdCentsPerCredit: terms.usdCentsPerCredit,
|
|
27213
|
-
costBasedMaxCredits: rec.costBasedMaxCredits,
|
|
27214
|
-
recommendedMaxCredits: rec.recommendedMaxCredits,
|
|
27215
|
-
recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
|
|
27216
|
-
averageCreditsPerCall: rec.averageCreditsPerCall,
|
|
27217
|
-
recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
|
|
27218
|
-
lengthRaisedCeiling: rec.lengthRaisedCeiling,
|
|
27077
|
+
chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
|
|
27078
|
+
lengthRaisedPrice: rec.lengthRaisedPrice,
|
|
27219
27079
|
cutOffSamples: rec.cutOffSamples,
|
|
27220
27080
|
noRecommendationReason: rec.noRecommendationReason
|
|
27221
27081
|
};
|
|
27222
27082
|
const savedTo = await saveBench(cwd, record);
|
|
27223
27083
|
if (opts.json) {
|
|
27224
|
-
const ok =
|
|
27084
|
+
const ok = charged.length > 0 && !lengthBlocked;
|
|
27225
27085
|
writeJsonLine({
|
|
27226
27086
|
command: "llm bench",
|
|
27227
27087
|
status: ok ? "ok" : "failed",
|
|
27228
27088
|
error: ok ? null : rec.noRecommendationReason ?? "no_sample_settled",
|
|
27229
|
-
currency: "credits",
|
|
27230
27089
|
apiUrl,
|
|
27231
27090
|
projectId,
|
|
27232
27091
|
modelId,
|
|
@@ -27235,54 +27094,35 @@ async function runBench(args) {
|
|
|
27235
27094
|
schemaPath: opts.schemaPath ?? null,
|
|
27236
27095
|
samples,
|
|
27237
27096
|
completed: settledCount,
|
|
27238
|
-
|
|
27239
|
-
// Spendable credits before and after the run; null when unreadable.
|
|
27097
|
+
maxCoinsPerSample: maxCoins,
|
|
27240
27098
|
balanceBefore,
|
|
27241
27099
|
balanceAfter,
|
|
27242
27100
|
results,
|
|
27243
27101
|
// The create-time refusal that ended the run, or null. Beside the
|
|
27244
27102
|
// samples, never among them: no attempt existed and nothing was spent.
|
|
27245
27103
|
refusal: refusal2,
|
|
27246
|
-
|
|
27247
|
-
//
|
|
27248
|
-
costUsdPicos: record.cost,
|
|
27249
|
-
costUsd: {
|
|
27250
|
-
p50: rec.costP50 === null ? null : Number(rec.costP50) / 1e12,
|
|
27251
|
-
p95: rec.costP95 === null ? null : Number(rec.costP95) / 1e12,
|
|
27252
|
-
max: rec.costMax === null ? null : Number(rec.costMax) / 1e12
|
|
27253
|
-
},
|
|
27254
|
-
chargedCredits: record.charged,
|
|
27255
|
-
// The answer's LENGTH, priced by the server per sample (`fitCredits`,
|
|
27104
|
+
chargedCoins: { p50, p95, max },
|
|
27105
|
+
// The answer's LENGTH, priced by the server per sample (`fitCoins`,
|
|
27256
27106
|
// headroom already on the tokens), over the settled samples.
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
27260
|
-
usdCentsPerCredit: terms.usdCentsPerCredit,
|
|
27261
|
-
missingTerms: rec.missingTerms,
|
|
27262
|
-
costBasedMaxCredits: rec.costBasedMaxCredits,
|
|
27263
|
-
lengthRaisedCeiling: rec.lengthRaisedCeiling,
|
|
27107
|
+
fitCoins: { p95: rec.fitP95, max: rec.fitMax },
|
|
27108
|
+
chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
|
|
27109
|
+
lengthRaisedPrice: rec.lengthRaisedPrice,
|
|
27264
27110
|
cutOffSamples: rec.cutOffSamples,
|
|
27265
27111
|
exceedsStandCap: rec.exceedsStandCap,
|
|
27266
27112
|
noRecommendationReason: rec.noRecommendationReason,
|
|
27267
|
-
|
|
27268
|
-
|
|
27269
|
-
|
|
27270
|
-
recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
|
|
27113
|
+
recommendedDeclaredHeadroomBps: headroomBps,
|
|
27114
|
+
recommendedEstimateCoins: recommended,
|
|
27115
|
+
recommendedPerCallMaxCoins: ceiling,
|
|
27271
27116
|
savedTo
|
|
27272
27117
|
});
|
|
27273
27118
|
if (!ok) process.exitCode = 1;
|
|
27274
27119
|
return;
|
|
27275
27120
|
}
|
|
27276
27121
|
log.plain("");
|
|
27277
|
-
const printMeasured = () => {
|
|
27278
|
-
log.plain(c.bold(" Real cost per call"));
|
|
27279
|
-
log.plain(` p50 ${usdFromPicos(rec.costP50)} p95 ${usdFromPicos(rec.costP95)} max ${usdFromPicos(rec.costMax)} (${settledCount} of ${samples} settled)`);
|
|
27280
|
-
log.plain(c.bold(" Charged credits"));
|
|
27281
|
-
log.plain(` p50 ${rec.chargedP50} p95 ${rec.chargedP95} max ${rec.chargedMax} (whole credits, rounded up per call)`);
|
|
27282
|
-
};
|
|
27283
27122
|
if (lengthBlocked) {
|
|
27284
|
-
if (
|
|
27285
|
-
|
|
27123
|
+
if (charged.length > 0) {
|
|
27124
|
+
log.plain(c.bold(" Charged coins"));
|
|
27125
|
+
log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
|
|
27286
27126
|
log.plain("");
|
|
27287
27127
|
}
|
|
27288
27128
|
printRecommendation(log, record);
|
|
@@ -27290,16 +27130,17 @@ async function runBench(args) {
|
|
|
27290
27130
|
process.exitCode = 1;
|
|
27291
27131
|
return;
|
|
27292
27132
|
}
|
|
27293
|
-
if (
|
|
27133
|
+
if (charged.length === 0) {
|
|
27294
27134
|
log.error(
|
|
27295
|
-
providerRefused > 0 && providerRefused === results.length ? " No sample ran \u2014 the provider refused every attempt at its door \u2014 so there is nothing to
|
|
27135
|
+
providerRefused > 0 && providerRefused === results.length ? " No sample ran \u2014 the provider refused every attempt at its door \u2014 so there is nothing to price from." : " No sample settled, so there is nothing to price from."
|
|
27296
27136
|
);
|
|
27297
27137
|
process.exitCode = 1;
|
|
27298
27138
|
return;
|
|
27299
27139
|
}
|
|
27300
|
-
|
|
27140
|
+
log.plain(c.bold(" Charged coins"));
|
|
27141
|
+
log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
|
|
27301
27142
|
if (balanceBefore !== null && balanceAfter !== null) {
|
|
27302
|
-
log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter}
|
|
27143
|
+
log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} coin spendable`);
|
|
27303
27144
|
}
|
|
27304
27145
|
log.plain("");
|
|
27305
27146
|
printRecommendation(log, record);
|
|
@@ -27311,58 +27152,43 @@ function printRecommendation(log, record) {
|
|
|
27311
27152
|
return;
|
|
27312
27153
|
}
|
|
27313
27154
|
if (record.noRecommendationReason === "answer_cut_off") {
|
|
27314
|
-
log.warn(` ${benchCutOffLine(record.cutOffSamples
|
|
27155
|
+
log.warn(` ${benchCutOffLine(record.cutOffSamples ?? 1, record.maxCoinsPerSample ?? 0)}`);
|
|
27315
27156
|
return;
|
|
27316
27157
|
}
|
|
27317
|
-
|
|
27318
|
-
|
|
27319
|
-
log.warn(" No ceiling recommended.");
|
|
27158
|
+
if (record.recommendedEstimateCoins === null || record.p95 === null) {
|
|
27159
|
+
log.warn(" No price recommended.");
|
|
27320
27160
|
log.dim(
|
|
27321
|
-
record.
|
|
27161
|
+
record.recommendedDeclaredHeadroomBps === null ? " This stand served no recommended headroom, and the multiplier is the server's to set \u2014" : " No sample settled, so there is no p95 to build a price on \u2014"
|
|
27322
27162
|
);
|
|
27323
27163
|
log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
|
|
27324
|
-
printAverage(log, record, attempts);
|
|
27325
27164
|
return;
|
|
27326
27165
|
}
|
|
27327
|
-
log.plain(c.bold(` Declare
|
|
27328
|
-
if (record.
|
|
27166
|
+
log.plain(c.bold(` Declare estimateCoins: ${record.recommendedEstimateCoins}`));
|
|
27167
|
+
if (record.lengthRaisedPrice) {
|
|
27329
27168
|
log.plain(` ${LLM_LENGTH_RAISED_LINE}`);
|
|
27330
27169
|
log.dim(
|
|
27331
|
-
` = the smallest
|
|
27170
|
+
` = the smallest price whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
|
|
27332
27171
|
);
|
|
27333
27172
|
log.dim(
|
|
27334
|
-
`
|
|
27173
|
+
` charged-based ${record.chargedBasedEstimateCoins ?? "\u2014"} (p95 of charged coins ${record.p95} plus ${record.recommendedDeclaredHeadroomBps} bps), over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`
|
|
27335
27174
|
);
|
|
27336
27175
|
} else {
|
|
27337
27176
|
log.dim(
|
|
27338
|
-
` = p95
|
|
27177
|
+
` = p95 of charged coins (${record.p95}) plus this stand's recommended headroom (${record.recommendedDeclaredHeadroomBps} bps),`
|
|
27339
27178
|
);
|
|
27340
|
-
log.dim(` measured over ${
|
|
27179
|
+
log.dim(` measured over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`);
|
|
27341
27180
|
}
|
|
27342
|
-
log.dim(" That number is a
|
|
27343
|
-
|
|
27344
|
-
|
|
27345
|
-
log.plain(c.bold(` Grant perCallMaxCredits: ${record.recommendedPerCallMaxCredits}`));
|
|
27181
|
+
log.dim(" That number is a PRICE: a started attempt is charged in full, including one that fails.");
|
|
27182
|
+
if (record.recommendedPerCallMaxCoins != null) {
|
|
27183
|
+
log.plain(c.bold(` Grant perCallMaxCoins: ${record.recommendedPerCallMaxCoins}`));
|
|
27346
27184
|
log.dim(
|
|
27347
|
-
record.
|
|
27185
|
+
record.lengthRaisedPrice ? ` = the same headroom over the worst sample (${record.max}) or the longest answer's price, whichever is larger, and never below the price above it.` : ` = the same headroom over the worst sample (${record.max}), and never below the price above it.`
|
|
27348
27186
|
);
|
|
27349
|
-
|
|
27350
|
-
printAverage(log, record, attempts);
|
|
27351
|
-
}
|
|
27352
|
-
function printAverage(log, record, attempts) {
|
|
27353
|
-
if (record.averageCreditsPerCall === null) return;
|
|
27354
|
-
log.plain(` Per-call estimate: about ${formatCredits(record.averageCreditsPerCall)} credits per call`);
|
|
27355
|
-
log.dim(` = the average real cost with the platform fee, no headroom, over ${attempts}.`);
|
|
27356
|
-
if (record.recommendedPerCallEstimateCredits !== null) {
|
|
27357
|
-
log.plain(c.bold(` Grant perCallEstimateCredits: ${record.recommendedPerCallEstimateCredits}`));
|
|
27358
|
-
log.dim(` = that average rounded up to a whole credit. disclosure.estimatedCreditsPerPeriod = your calls per period \xD7 ${formatCredits(record.averageCreditsPerCall)}, rounded up.`);
|
|
27187
|
+
log.dim(" The ceiling is the price's room to be wrong; declaring one under the price is refused.");
|
|
27359
27188
|
}
|
|
27360
27189
|
}
|
|
27361
27190
|
async function explainBenchRefusal(res, call2, base, log, opts, index, settledSoFar) {
|
|
27362
|
-
if (printedStructuredError(res)) {
|
|
27363
|
-
const printed = await res.json().catch(() => ({}));
|
|
27364
|
-
return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
|
|
27365
|
-
}
|
|
27191
|
+
if (printedStructuredError(res)) return { stop: true, refusal: { status: res.status, error: null, slotsHeld: null, slotLimit: null } };
|
|
27366
27192
|
const body = await res.json().catch(() => ({}));
|
|
27367
27193
|
const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
|
|
27368
27194
|
if (res.status === 429 && body.error === "generation_limit") {
|
|
@@ -27372,10 +27198,6 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
|
|
|
27372
27198
|
if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
|
|
27373
27199
|
return { stop: true, refusal: refusal2 };
|
|
27374
27200
|
}
|
|
27375
|
-
if (res.status === 403 && body.error === "credits_unverified") {
|
|
27376
|
-
if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
|
|
27377
|
-
return { stop: true, refusal: refusal2 };
|
|
27378
|
-
}
|
|
27379
27201
|
if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
|
|
27380
27202
|
if (res.status === 404 && body.error === "not_found") {
|
|
27381
27203
|
log.error(` ${LLM_LANE_OFF_LINE}`);
|
|
@@ -27391,7 +27213,7 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
|
|
|
27391
27213
|
return { stop: true, refusal: refusal2 };
|
|
27392
27214
|
}
|
|
27393
27215
|
if (res.status === 402) {
|
|
27394
|
-
log.error(" Not enough
|
|
27216
|
+
log.error(" Not enough coin to start the attempt.");
|
|
27395
27217
|
return { stop: true, refusal: refusal2 };
|
|
27396
27218
|
}
|
|
27397
27219
|
log.error(
|
|
@@ -27400,7 +27222,7 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
|
|
|
27400
27222
|
return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
|
|
27401
27223
|
}
|
|
27402
27224
|
function printProviderRefusal(log, status2, message, awaitingBill = false) {
|
|
27403
|
-
if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so
|
|
27225
|
+
if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so this call's coin stays held until it resolves (see \`genex llm status\`); it is not a sample.`);
|
|
27404
27226
|
else log.dim(" The provider refused this call at its door \u2014 no inference ran, it cost nothing, and it is not a sample.");
|
|
27405
27227
|
if (message) log.dim(` Provider said: ${message}`);
|
|
27406
27228
|
if (status2 === 401 || status2 === 403) {
|
|
@@ -27426,6 +27248,19 @@ async function pollSettled(call2, url, timeoutSec) {
|
|
|
27426
27248
|
}
|
|
27427
27249
|
return last;
|
|
27428
27250
|
}
|
|
27251
|
+
async function readCoinBalance(apiUrl, token) {
|
|
27252
|
+
try {
|
|
27253
|
+
const res = await apiFetch(`${apiUrl}/api/coin/balance`, {
|
|
27254
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
27255
|
+
signal: AbortSignal.timeout(6e3)
|
|
27256
|
+
});
|
|
27257
|
+
if (!res.ok) return null;
|
|
27258
|
+
const body = await res.json().catch(() => null);
|
|
27259
|
+
return typeof body?.spendable === "number" ? body.spendable : null;
|
|
27260
|
+
} catch {
|
|
27261
|
+
return null;
|
|
27262
|
+
}
|
|
27263
|
+
}
|
|
27429
27264
|
async function saveBench(cwd, record) {
|
|
27430
27265
|
try {
|
|
27431
27266
|
const file = path39.join(cwd, LLM_BENCH_FILE);
|
|
@@ -27441,10 +27276,9 @@ function benchFailed(opts, log, message) {
|
|
|
27441
27276
|
else log.error(message);
|
|
27442
27277
|
process.exitCode = 1;
|
|
27443
27278
|
}
|
|
27444
|
-
function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples,
|
|
27279
|
+
function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts) {
|
|
27445
27280
|
return {
|
|
27446
27281
|
command: "llm bench",
|
|
27447
|
-
currency: "credits",
|
|
27448
27282
|
apiUrl,
|
|
27449
27283
|
projectId,
|
|
27450
27284
|
modelId: opts.modelId ?? null,
|
|
@@ -27453,28 +27287,21 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCre
|
|
|
27453
27287
|
schemaPath: opts.schemaPath ?? null,
|
|
27454
27288
|
samples,
|
|
27455
27289
|
completed: 0,
|
|
27456
|
-
|
|
27290
|
+
maxCoinsPerSample: maxCoins,
|
|
27457
27291
|
balanceBefore: null,
|
|
27458
27292
|
balanceAfter: null,
|
|
27459
27293
|
results: [],
|
|
27460
27294
|
refusal: null,
|
|
27461
|
-
|
|
27462
|
-
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
recommendedDeclaredHeadroomBps: null,
|
|
27466
|
-
feeBps: null,
|
|
27467
|
-
usdCentsPerCredit: null,
|
|
27468
|
-
missingTerms: null,
|
|
27469
|
-
costBasedMaxCredits: null,
|
|
27470
|
-
lengthRaisedCeiling: false,
|
|
27295
|
+
chargedCoins: { p50: null, p95: null, max: null },
|
|
27296
|
+
fitCoins: { p95: null, max: null },
|
|
27297
|
+
chargedBasedEstimateCoins: null,
|
|
27298
|
+
lengthRaisedPrice: false,
|
|
27471
27299
|
cutOffSamples: 0,
|
|
27472
27300
|
exceedsStandCap: false,
|
|
27473
27301
|
noRecommendationReason: null,
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
recommendedPerCallEstimateCredits: null,
|
|
27302
|
+
recommendedDeclaredHeadroomBps: null,
|
|
27303
|
+
recommendedEstimateCoins: null,
|
|
27304
|
+
recommendedPerCallMaxCoins: null,
|
|
27478
27305
|
savedTo: null
|
|
27479
27306
|
};
|
|
27480
27307
|
}
|
|
@@ -27537,8 +27364,7 @@ async function reportStatus(args) {
|
|
|
27537
27364
|
for (const row of rows) {
|
|
27538
27365
|
const state = rowState(row);
|
|
27539
27366
|
const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
|
|
27540
|
-
const
|
|
27541
|
-
const reserved = money.reserved === null ? "reserved \u2014" : `${money.reserved} ${money.unit} reserved`;
|
|
27367
|
+
const reserved = typeof row.reservedCoins === "number" ? `${row.reservedCoins} coin reserved` : "reserved \u2014";
|
|
27542
27368
|
log.plain(
|
|
27543
27369
|
` ${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)")}`
|
|
27544
27370
|
);
|
|
@@ -27610,11 +27436,10 @@ async function cancelCall(args) {
|
|
|
27610
27436
|
if (!opts.json) {
|
|
27611
27437
|
log.warn(` ${row.id} (${row.status ?? "?"}) \u2014 ${sentence}.`);
|
|
27612
27438
|
if (state === "awaiting bill") {
|
|
27613
|
-
log.dim(` Nothing to cancel:
|
|
27439
|
+
log.dim(` Nothing to cancel: its coin releases when the bill resolves, and it stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`);
|
|
27614
27440
|
if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
|
|
27615
27441
|
} else {
|
|
27616
|
-
|
|
27617
|
-
log.dim(` ${money.charged === null ? "charge unknown" : `${money.charged} ${money.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
|
|
27442
|
+
log.dim(` ${typeof row.chargedCoins === "number" ? `${row.chargedCoins} coin charged` : "charge unknown"}${row.error ? ` \xB7 ${row.error}` : ""}.`);
|
|
27618
27443
|
}
|
|
27619
27444
|
}
|
|
27620
27445
|
return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
|
|
@@ -27636,126 +27461,78 @@ async function cancelCall(args) {
|
|
|
27636
27461
|
const after = await res.json().catch(() => null) ?? row;
|
|
27637
27462
|
if (!opts.json) {
|
|
27638
27463
|
log.success(` Cancelled ${row.id} (was ${row.status ?? "active"}).`);
|
|
27639
|
-
log.dim(`
|
|
27464
|
+
log.dim(` Its coin releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
|
|
27640
27465
|
}
|
|
27641
27466
|
return done("ok", null, "cancelled", after);
|
|
27642
27467
|
}
|
|
27643
|
-
async function
|
|
27644
|
-
let
|
|
27468
|
+
async function reportSavedPrice(cwd, opts, log) {
|
|
27469
|
+
let record = null;
|
|
27645
27470
|
try {
|
|
27646
|
-
|
|
27471
|
+
record = JSON.parse(await fs39.readFile(path39.join(cwd, LLM_BENCH_FILE), "utf8"));
|
|
27647
27472
|
} catch {
|
|
27648
|
-
|
|
27473
|
+
record = null;
|
|
27649
27474
|
}
|
|
27650
|
-
if (!
|
|
27651
|
-
const v = parsed;
|
|
27652
|
-
if (v.v === 2 && v.currency === "credits" && v.cost && v.charged) return { kind: "credits", record: parsed };
|
|
27653
|
-
return { kind: "coins", record: parsed };
|
|
27654
|
-
}
|
|
27655
|
-
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.";
|
|
27656
|
-
async function reportSavedPrice(cwd, opts, log) {
|
|
27657
|
-
const saved = await readSavedBench(cwd);
|
|
27658
|
-
const rerun = 'genex llm bench "<prompt>" --max-credits <n> --user-approved';
|
|
27659
|
-
const creditKeys = (record2) => ({
|
|
27660
|
-
costUsdPicos: record2?.cost ?? { p50: null, p95: null, max: null },
|
|
27661
|
-
chargedCredits: record2?.charged ?? { p50: null, p95: null, max: null },
|
|
27662
|
-
fitP95: record2?.fitP95 ?? null,
|
|
27663
|
-
fitMax: record2?.fitMax ?? null,
|
|
27664
|
-
recommendedDeclaredHeadroomBps: record2?.recommendedDeclaredHeadroomBps ?? null,
|
|
27665
|
-
feeBps: record2?.feeBps ?? null,
|
|
27666
|
-
usdCentsPerCredit: record2?.usdCentsPerCredit ?? null,
|
|
27667
|
-
costBasedMaxCredits: record2?.costBasedMaxCredits ?? null,
|
|
27668
|
-
lengthRaisedCeiling: record2?.lengthRaisedCeiling ?? false,
|
|
27669
|
-
cutOffSamples: record2?.cutOffSamples ?? 0,
|
|
27670
|
-
noRecommendationReason: record2?.noRecommendationReason ?? null,
|
|
27671
|
-
recommendedMaxCredits: record2?.recommendedMaxCredits ?? null,
|
|
27672
|
-
recommendedPerCallMaxCredits: record2?.recommendedPerCallMaxCredits ?? null,
|
|
27673
|
-
averageCreditsPerCall: record2?.averageCreditsPerCall ?? null,
|
|
27674
|
-
recommendedPerCallEstimateCredits: record2?.recommendedPerCallEstimateCredits ?? null
|
|
27675
|
-
});
|
|
27676
|
-
if (!saved) {
|
|
27475
|
+
if (!record) {
|
|
27677
27476
|
if (opts.json) {
|
|
27678
27477
|
writeJsonLine({
|
|
27679
27478
|
command: "llm price",
|
|
27680
27479
|
status: "failed",
|
|
27681
27480
|
error: "no_bench",
|
|
27682
|
-
currency: null,
|
|
27683
27481
|
ranAt: null,
|
|
27684
27482
|
modelId: null,
|
|
27685
27483
|
samples: null,
|
|
27686
27484
|
completed: null,
|
|
27687
|
-
|
|
27688
|
-
|
|
27485
|
+
p50: null,
|
|
27486
|
+
p95: null,
|
|
27487
|
+
max: null,
|
|
27488
|
+
fitP95: null,
|
|
27489
|
+
chargedBasedEstimateCoins: null,
|
|
27490
|
+
lengthRaisedPrice: false,
|
|
27491
|
+
cutOffSamples: 0,
|
|
27492
|
+
noRecommendationReason: null,
|
|
27493
|
+
recommendedDeclaredHeadroomBps: null,
|
|
27494
|
+
recommendedEstimateCoins: null,
|
|
27495
|
+
recommendedPerCallMaxCoins: null,
|
|
27689
27496
|
savedTo: null
|
|
27690
27497
|
});
|
|
27691
27498
|
} else {
|
|
27692
27499
|
log.error("Nothing has been benchmarked in this folder yet.");
|
|
27693
|
-
log.dim(` Run ${c.cyan(
|
|
27694
|
-
}
|
|
27695
|
-
process.exitCode = 1;
|
|
27696
|
-
return;
|
|
27697
|
-
}
|
|
27698
|
-
if (saved.kind === "coins") {
|
|
27699
|
-
const old = saved.record;
|
|
27700
|
-
const legacy = {
|
|
27701
|
-
p50: old.p50 ?? null,
|
|
27702
|
-
p95: old.p95 ?? null,
|
|
27703
|
-
max: old.max ?? null,
|
|
27704
|
-
recommendedEstimateCoins: old.recommendedEstimateCoins ?? null,
|
|
27705
|
-
recommendedPerCallMaxCoins: old.recommendedPerCallMaxCoins ?? null,
|
|
27706
|
-
maxCoinsPerSample: old.maxCoinsPerSample ?? null,
|
|
27707
|
-
noRecommendationReason: old.noRecommendationReason ?? null
|
|
27708
|
-
};
|
|
27709
|
-
if (opts.json) {
|
|
27710
|
-
writeJsonLine({
|
|
27711
|
-
command: "llm price",
|
|
27712
|
-
status: "failed",
|
|
27713
|
-
error: "coin_terms",
|
|
27714
|
-
currency: "coins",
|
|
27715
|
-
ranAt: old.ranAt ?? null,
|
|
27716
|
-
modelId: old.modelId ?? null,
|
|
27717
|
-
samples: old.samples ?? null,
|
|
27718
|
-
completed: old.completed ?? null,
|
|
27719
|
-
...creditKeys(null),
|
|
27720
|
-
legacyCoinTerms: legacy,
|
|
27721
|
-
savedTo: LLM_BENCH_FILE
|
|
27722
|
-
});
|
|
27723
|
-
} else {
|
|
27724
|
-
log.plain(c.bold("genex llm price"));
|
|
27725
|
-
log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${old.ranAt ?? "at an unknown time"}${old.modelId ? ` on ${old.modelId}` : ""}`);
|
|
27726
|
-
log.plain("");
|
|
27727
|
-
log.warn(` ${LLM_PRICE_COIN_TERMS_LINE}`);
|
|
27728
|
-
const measured = legacy.p95 === null ? "no settled sample" : `charged coins p50 ${legacy.p50} \xB7 p95 ${legacy.p95} \xB7 max ${legacy.max}`;
|
|
27729
|
-
const recommended = legacy.recommendedEstimateCoins === null ? "no price recommended" : `it recommended estimateCoins ${legacy.recommendedEstimateCoins}${legacy.recommendedPerCallMaxCoins != null ? ` and perCallMaxCoins ${legacy.recommendedPerCallMaxCoins}` : ""}`;
|
|
27730
|
-
log.dim(` What it measured then: ${measured}; ${recommended}.`);
|
|
27731
|
-
log.dim(` Re-run: ${c.cyan(rerun)}`);
|
|
27500
|
+
log.dim(` Run ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')} first.`);
|
|
27732
27501
|
}
|
|
27733
27502
|
process.exitCode = 1;
|
|
27734
27503
|
return;
|
|
27735
27504
|
}
|
|
27736
|
-
const record = saved.record;
|
|
27737
27505
|
if (opts.json) {
|
|
27738
27506
|
writeJsonLine({
|
|
27739
27507
|
command: "llm price",
|
|
27740
|
-
status: record.
|
|
27741
|
-
error: record.
|
|
27742
|
-
currency: "credits",
|
|
27508
|
+
status: record.recommendedEstimateCoins === null ? "failed" : "ok",
|
|
27509
|
+
error: record.recommendedEstimateCoins === null ? record.noRecommendationReason ?? "no_recommendation" : null,
|
|
27743
27510
|
ranAt: record.ranAt ?? null,
|
|
27744
27511
|
modelId: record.modelId ?? null,
|
|
27745
27512
|
samples: record.samples ?? null,
|
|
27746
27513
|
completed: record.completed ?? null,
|
|
27747
|
-
|
|
27748
|
-
|
|
27514
|
+
p50: record.p50 ?? null,
|
|
27515
|
+
p95: record.p95 ?? null,
|
|
27516
|
+
max: record.max ?? null,
|
|
27517
|
+
// Absent from a file an older CLI wrote; null / false / 0 then, never missing.
|
|
27518
|
+
fitP95: record.fitP95 ?? null,
|
|
27519
|
+
chargedBasedEstimateCoins: record.chargedBasedEstimateCoins ?? null,
|
|
27520
|
+
lengthRaisedPrice: record.lengthRaisedPrice ?? false,
|
|
27521
|
+
cutOffSamples: record.cutOffSamples ?? 0,
|
|
27522
|
+
noRecommendationReason: record.noRecommendationReason ?? null,
|
|
27523
|
+
recommendedDeclaredHeadroomBps: record.recommendedDeclaredHeadroomBps ?? null,
|
|
27524
|
+
recommendedEstimateCoins: record.recommendedEstimateCoins ?? null,
|
|
27525
|
+
recommendedPerCallMaxCoins: record.recommendedPerCallMaxCoins ?? null,
|
|
27749
27526
|
savedTo: LLM_BENCH_FILE
|
|
27750
27527
|
});
|
|
27751
|
-
if (record.
|
|
27528
|
+
if (record.recommendedEstimateCoins === null) process.exitCode = 1;
|
|
27752
27529
|
return;
|
|
27753
27530
|
}
|
|
27754
27531
|
log.plain(c.bold("genex llm price"));
|
|
27755
27532
|
log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
|
|
27756
27533
|
log.plain("");
|
|
27757
27534
|
printRecommendation(log, record);
|
|
27758
|
-
if (record.
|
|
27535
|
+
if (record.recommendedEstimateCoins === null) process.exitCode = 1;
|
|
27759
27536
|
}
|
|
27760
27537
|
function writeJsonLine(value) {
|
|
27761
27538
|
process.stdout.write(`${JSON.stringify(value)}
|
|
@@ -28015,7 +27792,7 @@ function runtimeRow(state, lane, toolsOnly = false) {
|
|
|
28015
27792
|
return {
|
|
28016
27793
|
label: "Runtime",
|
|
28017
27794
|
value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}${keyRefused ? ` \xB7 ${c.yellow("provider key refused")}` : ""}`,
|
|
28018
|
-
fix: keyRefused ? LLM_PROVIDER_KEY_REJECTED_LINE : toolsOnly ? convertFix : '
|
|
27795
|
+
fix: keyRefused ? LLM_PROVIDER_KEY_REJECTED_LINE : toolsOnly ? convertFix : 'Price a call before a game declares one: `npx genex llm bench "<prompt>" --max-coins <n> --user-approved`.'
|
|
28019
27796
|
};
|
|
28020
27797
|
}
|
|
28021
27798
|
case "off":
|
|
@@ -30412,8 +30189,7 @@ async function publishNativeBuild(ctx, body, log) {
|
|
|
30412
30189
|
async function listNativeBuilds(ctx, log) {
|
|
30413
30190
|
const res = await call(ctx, "/native-builds", { method: "GET" }, "list the builds", log);
|
|
30414
30191
|
if (!res) return null;
|
|
30415
|
-
|
|
30416
|
-
return { builds: body.builds, submissions: body.submissions ?? [] };
|
|
30192
|
+
return (await res.json()).builds;
|
|
30417
30193
|
}
|
|
30418
30194
|
async function removeNativeBuild(ctx, platform, arch, log) {
|
|
30419
30195
|
const route = `/native-builds/${encodeURIComponent(platform)}/${encodeURIComponent(arch)}`;
|
|
@@ -30501,17 +30277,6 @@ function describeLine(d) {
|
|
|
30501
30277
|
if (d.engine) parts.push(d.engine);
|
|
30502
30278
|
return parts.join(" \xB7 ");
|
|
30503
30279
|
}
|
|
30504
|
-
function reportPublished(build, log) {
|
|
30505
|
-
const pair = `${build.platform} ${build.arch}`;
|
|
30506
|
-
if (build.review === "pending") {
|
|
30507
|
-
log.success(`Sent for review \u2014 Genex desktop players on ${pair} get version ${build.version} once Genex approves it.`);
|
|
30508
|
-
} else if (build.review === "rejected") {
|
|
30509
|
-
const why = build.reviewReason ? `: ${build.reviewReason}` : ".";
|
|
30510
|
-
log.warn(`Version ${build.version} for ${pair} was turned down in review${why} Change the build and publish it again.`);
|
|
30511
|
-
} else {
|
|
30512
|
-
log.success(`Live \u2014 Genex desktop players on ${pair} now get version ${build.version}.`);
|
|
30513
|
-
}
|
|
30514
|
-
}
|
|
30515
30280
|
async function runPublish2(opts, log) {
|
|
30516
30281
|
if (refusedInHostedSession(log)) return false;
|
|
30517
30282
|
if (!opts.nativePath) {
|
|
@@ -30551,7 +30316,7 @@ async function runPublish2(opts, log) {
|
|
|
30551
30316
|
log
|
|
30552
30317
|
);
|
|
30553
30318
|
if (!build) return false;
|
|
30554
|
-
|
|
30319
|
+
log.success(`Live \u2014 Genex desktop players on ${platform} ${arch} now get version ${build.version}.`);
|
|
30555
30320
|
log.dim(` ${build.url}`);
|
|
30556
30321
|
const listing = opts.gallery ? await listGame(ctx, opts, log) : void 0;
|
|
30557
30322
|
if (opts.json) {
|
|
@@ -30590,25 +30355,20 @@ async function listGame(ctx, opts, log) {
|
|
|
30590
30355
|
function buildLine(b) {
|
|
30591
30356
|
return `${c.bold(`${b.platform} ${b.arch}`)} ${b.version} ${b.entry} ${sizeLine(b.sizeBytes)} ${b.publishedAt}`;
|
|
30592
30357
|
}
|
|
30593
|
-
function heldLine(b) {
|
|
30594
|
-
const state = b.review === "rejected" ? `turned down${b.reviewReason ? `: ${b.reviewReason}` : ""}` : "in review";
|
|
30595
|
-
return `${buildLine(b)} ${c.dim(state)}`;
|
|
30596
|
-
}
|
|
30597
30358
|
async function runList2(opts, log) {
|
|
30598
30359
|
const ctx = await context3(opts, log);
|
|
30599
30360
|
if (!ctx) return false;
|
|
30600
|
-
const
|
|
30601
|
-
if (!
|
|
30361
|
+
const builds = await listNativeBuilds(ctx, log);
|
|
30362
|
+
if (!builds) return false;
|
|
30602
30363
|
if (opts.json) {
|
|
30603
|
-
console.log(JSON.stringify(
|
|
30364
|
+
console.log(JSON.stringify({ builds }));
|
|
30604
30365
|
return true;
|
|
30605
30366
|
}
|
|
30606
|
-
if (
|
|
30367
|
+
if (builds.length === 0) {
|
|
30607
30368
|
log.plain("No native builds. Publish one with `genex native publish <path>`.");
|
|
30608
30369
|
return true;
|
|
30609
30370
|
}
|
|
30610
|
-
for (const b of
|
|
30611
|
-
for (const b of listed.submissions) log.plain(heldLine(b));
|
|
30371
|
+
for (const b of builds) log.plain(buildLine(b));
|
|
30612
30372
|
return true;
|
|
30613
30373
|
}
|
|
30614
30374
|
async function runRemove(opts, log) {
|
|
@@ -30709,16 +30469,16 @@ ${c.bold("Usage")}
|
|
|
30709
30469
|
status | cancel. "models" says whether this
|
|
30710
30470
|
stand serves the runtime LLM lane at all, and
|
|
30711
30471
|
at what provider rates. "bench" runs the real
|
|
30712
|
-
model on YOUR OWN
|
|
30713
|
-
attempt
|
|
30472
|
+
model on YOUR OWN coin and reports what each
|
|
30473
|
+
attempt was charged; it needs --max-coins <n>
|
|
30714
30474
|
--user-approved, like any spend the player has
|
|
30715
30475
|
to agree to. "price" reprints the last run's
|
|
30716
|
-
recommended
|
|
30476
|
+
recommended estimateCoins. "status" lists this
|
|
30717
30477
|
project's open calls and the slots they hold;
|
|
30718
30478
|
"cancel <id>" stops an active one. Declare a
|
|
30719
|
-
game's
|
|
30720
|
-
|
|
30721
|
-
|
|
30479
|
+
game's price from a bench, never from a guess:
|
|
30480
|
+
a started attempt is charged in full, including
|
|
30481
|
+
one that fails.
|
|
30722
30482
|
genex player <sub> [options] Answer the in-game model requests you approve, on your
|
|
30723
30483
|
OWN Claude or ChatGPT subscription: install | run |
|
|
30724
30484
|
status | stop | uninstall. "install" signs this machine
|
|
@@ -31221,7 +30981,7 @@ ${c.bold("Examples")}
|
|
|
31221
30981
|
genex items add sku_456
|
|
31222
30982
|
genex llm models
|
|
31223
30983
|
genex llm models --all
|
|
31224
|
-
genex llm bench "Reply with one short taunt." --samples 5 --max-
|
|
30984
|
+
genex llm bench "Reply with one short taunt." --samples 5 --max-coins 20 --user-approved
|
|
31225
30985
|
genex llm price
|
|
31226
30986
|
genex llm status
|
|
31227
30987
|
genex llm cancel <id>
|
|
@@ -31458,24 +31218,22 @@ ${c.bold("How the allowance works")}
|
|
|
31458
31218
|
|
|
31459
31219
|
Every number printed is live from your account; prices can change without a deploy.
|
|
31460
31220
|
`,
|
|
31461
|
-
llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what
|
|
31221
|
+
llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what should the game declare?
|
|
31462
31222
|
|
|
31463
31223
|
${c.bold("Usage")}
|
|
31464
31224
|
genex llm models [--all] [--json] Which models this stand serves, their provider rates per
|
|
31465
|
-
million tokens, the
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
allowance does not count. Needs a folder linked to a
|
|
31478
|
-
game you own, and a balance that covers one attempt.
|
|
31225
|
+
million tokens, and the headroom it recommends over a
|
|
31226
|
+
benchmark. The catalog is synced from OpenRouter, so the
|
|
31227
|
+
FEATURED short list is printed by default and --all
|
|
31228
|
+
prints every row (--json always carries every row). Off
|
|
31229
|
+
on this stand: it says so and exits 0 \u2014 build the feature
|
|
31230
|
+
without a model rather than promising a 404.
|
|
31231
|
+
genex llm bench "<prompt>" --max-coins <n> --user-approved [options]
|
|
31232
|
+
Run the real model <n> times through the development
|
|
31233
|
+
lane and report what each attempt was CHARGED. Refused
|
|
31234
|
+
without both flags, before any network call: this spends
|
|
31235
|
+
YOUR OWN COIN, a wallet the build's asset allowance
|
|
31236
|
+
cannot see. Needs a folder linked to a game you own.
|
|
31479
31237
|
genex llm price [--json] The recommendation from the last bench in this folder.
|
|
31480
31238
|
Reads a file; spends nothing.
|
|
31481
31239
|
genex llm status [--json] This project's open development calls: active, or
|
|
@@ -31492,32 +31250,29 @@ ${c.bold("Bench options")}
|
|
|
31492
31250
|
--samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
|
|
31493
31251
|
--schema <file> A JSON file holding the output schema; implies JSON output.
|
|
31494
31252
|
--json-output Ask for JSON without a schema. --text Ask for text (the default).
|
|
31495
|
-
--max-
|
|
31253
|
+
--max-coins <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own coin,
|
|
31496
31254
|
never a player's. The run's worst case is that number times --samples,
|
|
31497
|
-
and it is printed before anything starts.
|
|
31498
|
-
name, read as the same number.)
|
|
31255
|
+
and it is printed before anything starts.
|
|
31499
31256
|
--timeout <seconds> Bounds the wait for each attempt.
|
|
31500
31257
|
--json One machine-readable object; every key is always present, null when
|
|
31501
31258
|
the CLI could not learn it.
|
|
31502
31259
|
|
|
31503
31260
|
${c.bold("Why a benchmark and not an estimate")}
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
31512
|
-
|
|
31513
|
-
recommendation, because those multipliers are not the CLI's to invent.
|
|
31261
|
+
\`estimateCoins\` is a PRICE, not a guess: declaring it charges it, in full, even when the
|
|
31262
|
+
attempt fails, is cancelled, or stops at its budget. And three layers sit between the
|
|
31263
|
+
provider's rate card and that number \u2014 the provider's cost, the platform's tariff (already
|
|
31264
|
+
inside every charged coin), and your headroom. The bench itself is billed to YOU, on the
|
|
31265
|
+
development lane; the player-funded lane is what the published game uses. Adding a margin
|
|
31266
|
+
to the provider's rate silently drops the tariff and under-prices every call, so the
|
|
31267
|
+
recommendation is built from CHARGED COINS and the headroom comes from the server. A stand
|
|
31268
|
+
that serves no headroom gets no recommendation, because the multiplier is not the CLI's to
|
|
31269
|
+
invent.
|
|
31514
31270
|
|
|
31515
|
-
|
|
31516
|
-
|
|
31517
|
-
A sample cut off at your --max-
|
|
31518
|
-
|
|
31519
|
-
|
|
31520
|
-
fee included, which is the honest estimate a budget's disclosure is built on.
|
|
31271
|
+
The declared price also decides how LONG the answer may be: it sizes each call's output
|
|
31272
|
+
allowance. So the recommendation is never below the smallest price that leaves room for
|
|
31273
|
+
the benchmarked answer, as the server computes it. A sample cut off at your --max-coins
|
|
31274
|
+
(provider_token_limit) is not a sample \u2014 re-run with a higher --max-coins; an answer longer
|
|
31275
|
+
than one call on this stand may produce gets no price at all \u2014 ask for a shorter one.
|
|
31521
31276
|
|
|
31522
31277
|
Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
|
|
31523
31278
|
`,
|
|
@@ -31772,13 +31527,11 @@ function parseArgs(argv) {
|
|
|
31772
31527
|
"--limit",
|
|
31773
31528
|
// `genex budget --assets <credits>` — the allowance the player approved.
|
|
31774
31529
|
"--assets",
|
|
31775
|
-
// `genex llm bench` value flags. `--max-
|
|
31776
|
-
// shape as `--assets
|
|
31777
|
-
// count a bench. `--max-coins` is its deprecated name (same number).
|
|
31530
|
+
// `genex llm bench` value flags. `--max-coins` is the COIN half of the
|
|
31531
|
+
// same approval shape as `--assets` (a different economy, its own gate).
|
|
31778
31532
|
"--model",
|
|
31779
31533
|
"--samples",
|
|
31780
31534
|
"--schema",
|
|
31781
|
-
"--max-credits",
|
|
31782
31535
|
"--max-coins",
|
|
31783
31536
|
// `genex player install --label <name>` — what this machine is called in
|
|
31784
31537
|
// the dashboard. Its own key rather than the generic 2nd positional,
|
|
@@ -32349,14 +32102,12 @@ function applyValueFlag(options, flag, value) {
|
|
|
32349
32102
|
options.samples = n;
|
|
32350
32103
|
break;
|
|
32351
32104
|
}
|
|
32352
|
-
case "--max-credits":
|
|
32353
32105
|
case "--max-coins": {
|
|
32354
32106
|
const n = Number(value);
|
|
32355
32107
|
if (!Number.isInteger(n) || n < 1) {
|
|
32356
|
-
throw new Error(`Invalid
|
|
32108
|
+
throw new Error(`Invalid --max-coins value: ${value} (whole coin, 1 or more)`);
|
|
32357
32109
|
}
|
|
32358
|
-
|
|
32359
|
-
else options.maxCoins = n;
|
|
32110
|
+
options.maxCoins = n;
|
|
32360
32111
|
break;
|
|
32361
32112
|
}
|
|
32362
32113
|
case "--timeout": {
|
|
@@ -32667,9 +32418,9 @@ async function main() {
|
|
|
32667
32418
|
await runBudget(parsed.options);
|
|
32668
32419
|
break;
|
|
32669
32420
|
// The in-game model lane: is it live here, what does one call really
|
|
32670
|
-
// cost, what
|
|
32671
|
-
//
|
|
32672
|
-
//
|
|
32421
|
+
// cost, what should the game declare. `bench` is the only CLI command
|
|
32422
|
+
// that spends COIN, so it carries its own --max-coins/--user-approved
|
|
32423
|
+
// gate — the credits allowance cannot see that wallet.
|
|
32673
32424
|
case "llm":
|
|
32674
32425
|
await runLlm(parsed.options);
|
|
32675
32426
|
break;
|