@genex-ai/cli-demo 1.36.17-dev.1046 → 1.36.17

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/index.js CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  writeSecretFile,
58
58
  writeUserToken,
59
59
  writeWorkspace
60
- } from "./chunk-EABIVAGT.js";
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-K4EHXOZK.js";
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 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 Nex, credits, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
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 Nex \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 Nex, 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 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?`;
1151
+ var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with Nex 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)} Nex`;
8742
- const usd = (displayUsdCents / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
8743
- return `${formatCoins(coins)} Nex ($${usd})`;
8742
+ const usd2 = (displayUsdCents / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
8743
+ return `${formatCoins(coins)} Nex ($${usd2})`;
8744
8744
  }
8745
8745
  function priceGridLine(grid, freeAllowed) {
8746
8746
  return `${[...freeAllowed ? ["Free"] : [], ...grid.map(formatCoins)].join(" \xB7 ")} Nex`;
@@ -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-EH2MOW3U.js");
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-WSURPZET.js");
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 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.";
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 Nex \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 what it reserved stays held until the bill resolves; such a row stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`;
26544
+ var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so its Nex 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: bpsOrNull(body.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 percentilePicos(values, p) {
26629
- if (values.length === 0) return null;
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
- var PICOS_PER_CENT = 10000000000n;
26635
- function ceilingCreditsFor(costPicos, terms) {
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 !== void 0) process.stderr.write(`${c.yellow("!")} ${LLM_MAX_COINS_DEPRECATED}
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(approvedCredits) || approvedCredits < 1) {
26704
- fail5("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
26638
+ if (!Number.isInteger(opts.maxCoins) || opts.maxCoins < 1) {
26639
+ fail5("llm bench", `--max-coins takes a whole number of Nex, 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-credits <n> --user-approved.');
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}${kind}`
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 ceiling can be recommended from a bench.");
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(` 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).`);
26825
+ log.dim(` Recommended headroom over benchmarked Nex 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 list; what one call of YOUR prompt really costs,");
26913
- 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')}`);
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
- chargedCredits: wholeOrNull(settled?.chargedCredits),
26933
- costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : picos !== null ? Number(picos) / 1e12 : null,
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
- fitCredits: typeof usage?.fitCredits === "number" && Number.isSafeInteger(usage.fitCredits) && usage.fitCredits >= 1 ? usage.fitCredits : null,
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, terms) {
26947
- const settled = results.filter(
26948
- (r) => r.status === "succeeded" && r.billingStatus === "final" && r.chargedCredits !== null && r.costUsdPicos !== null
26949
- );
26950
- const costs = settled.map((r) => BigInt(r.costUsdPicos));
26951
- const charged = settled.map((r) => r.chargedCredits);
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 { headroomBps, feeBps, usdCentsPerCredit } = terms;
26961
- const missingTerms = headroomBps === null ? "headroom" : feeBps === null || usdCentsPerCredit === null ? "credit_terms" : null;
26962
- const full = headroomBps !== null && feeBps !== null && usdCentsPerCredit !== null ? { headroomBps, feeBps, usdCentsPerCredit } : null;
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
- costP50: percentilePicos(costs, 50),
26973
- costP95,
26974
- costMax,
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
- costBasedMaxCredits: costBased,
26982
- recommendedMaxCredits: recommended,
26983
- recommendedPerCallMaxCredits: ceiling,
26984
- averageCreditsPerCall: average,
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 ceiling also decides how long the answer may be: at the cost-based ceiling the answer would be cut off, so declare this one.";
26994
- 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.";
26995
- function benchCutOffLine(cutOff, maxCredits) {
26996
- 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.`;
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,23 +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" && ((row.reservedCoins ?? 0) > 0 || (row.reservedCredits ?? 0) > 0);
27008
- }
27009
- var UNIT_WORD = { credits: "credits", coin: "Nex" };
27010
- function rowMoney(row) {
27011
- const credits = row.currency === "credits" || row.currency === void 0 && typeof row.reservedCredits === "number" && typeof row.reservedCoins !== "number";
27012
- if (credits) {
27013
- return {
27014
- unit: "credits",
27015
- reserved: typeof row.reservedCredits === "number" ? row.reservedCredits : null,
27016
- charged: typeof row.chargedCredits === "number" ? row.chargedCredits : null
27017
- };
27018
- }
27019
- return {
27020
- unit: "coin",
27021
- reserved: typeof row.reservedCoins === "number" ? row.reservedCoins : null,
27022
- charged: typeof row.chargedCoins === "number" ? row.chargedCoins : null
27023
- };
26901
+ return state === "active" || state === "awaiting bill" && (row.reservedCoins ?? 0) > 0;
27024
26902
  }
27025
26903
  function providerRefusalStatus(error) {
27026
26904
  const m = /^provider_http_(\d{3})$/.exec(error ?? "");
@@ -27039,7 +26917,7 @@ function formatAge(iso, now = Date.now()) {
27039
26917
  }
27040
26918
  async function runBench(args) {
27041
26919
  const { apiUrl, token, projectId, cwd, opts, log } = args;
27042
- const maxCredits = opts.maxCredits ?? opts.maxCoins;
26920
+ const maxCoins = opts.maxCoins;
27043
26921
  const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
27044
26922
  const prompt = opts.benchPrompt.trim();
27045
26923
  const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
@@ -27061,7 +26939,7 @@ async function runBench(args) {
27061
26939
  const lane = await readRuntimeLane(apiUrl, token);
27062
26940
  if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
27063
26941
  if (lane.state === "off") {
27064
- if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts), status: "off", error: null });
26942
+ if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts), status: "off", error: null });
27065
26943
  else {
27066
26944
  log.plain(LLM_LANE_OFF_LINE);
27067
26945
  log.dim(" Nothing was spent.");
@@ -27088,27 +26966,17 @@ async function runBench(args) {
27088
26966
  );
27089
26967
  return;
27090
26968
  }
27091
- const walletBefore = await fetchCreditsSnapshot(apiUrl, token);
27092
- if (walletBefore?.emailVerified === false) {
27093
- benchFailed(opts, log, LLM_BENCH_UNVERIFIED_LINE);
27094
- return;
27095
- }
27096
- if (walletBefore !== null && walletBefore.spendable < maxCredits) {
27097
- benchFailed(opts, log, benchBalanceShortLine(walletBefore.spendable, maxCredits));
27098
- return;
27099
- }
27100
- const balanceBefore = walletBefore?.spendable ?? null;
26969
+ const balanceBefore = await readCoinBalance(apiUrl, token);
27101
26970
  if (!opts.json) {
27102
26971
  log.plain(c.bold("genex llm bench"));
27103
26972
  log.dim(` ${apiUrl}`);
27104
26973
  log.plain("");
27105
26974
  log.plain(` Model ${c.cyan(modelId)}`);
27106
26975
  log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
27107
- log.plain(` Approved ${maxCredits} credit${maxCredits === 1 ? "" : "s"} per attempt \u2014 at worst ${maxCredits * samples} credits for this run, from your own balance`);
26976
+ log.plain(` Approved ${maxCoins} Nex per attempt \u2014 at worst ${maxCoins * samples} Nex for this run`);
27108
26977
  log.plain(
27109
- ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} credits spendable`}`
26978
+ ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} Nex spendable`}`
27110
26979
  );
27111
- log.dim(" Billed as used: each attempt is charged its real cost plus the platform fee, rounded up to a whole credit.");
27112
26980
  log.plain("");
27113
26981
  }
27114
26982
  const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
@@ -27134,7 +27002,7 @@ async function runBench(args) {
27134
27002
  const res = await call2(base, {
27135
27003
  method: "POST",
27136
27004
  body: JSON.stringify({
27137
- maxCredits,
27005
+ maxCoins,
27138
27006
  request: {
27139
27007
  idempotencyKey: `bench-${randomUUID2()}`,
27140
27008
  modelId,
@@ -27164,14 +27032,13 @@ async function runBench(args) {
27164
27032
  if (refusedAt !== null) providerRefused++;
27165
27033
  if (!opts.json) {
27166
27034
  const length = row.outputTokens !== null ? ` \xB7 ${row.outputTokens}${row.maxOutputTokens !== null ? ` of ${row.maxOutputTokens}` : ""} tokens out` : "";
27167
- const charged = row.chargedCredits === null ? "\u2014" : String(row.chargedCredits);
27168
27035
  log.plain(
27169
- ` ${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}` : ""}`
27036
+ ` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${String(row.chargedCoins ?? "\u2014").padStart(4)} Nex charged \xB7 provider ${usd(row.costUsd)}${length}${row.error ? ` \xB7 ${row.error}` : ""}`
27170
27037
  );
27171
27038
  if (refusedAt !== null) printProviderRefusal(log, refusedAt, row.providerMessage, row.status === "unknown");
27172
27039
  if (row.error === PROVIDER_TOKEN_LIMIT) {
27173
27040
  log.dim(
27174
- 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.`
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.`
27175
27042
  );
27176
27043
  }
27177
27044
  }
@@ -27179,19 +27046,16 @@ async function runBench(args) {
27179
27046
  } finally {
27180
27047
  process.removeListener("SIGINT", onSigint);
27181
27048
  }
27182
- const terms = {
27183
- headroomBps: lane.recommendedDeclaredHeadroomBps,
27184
- feeBps: lane.feeBps,
27185
- usdCentsPerCredit: lane.usdCentsPerCredit
27186
- };
27187
- const rec = benchRecommendation(results, terms);
27188
- 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;
27189
27055
  const lengthBlocked = rec.noRecommendationReason !== null;
27190
- const balanceAfter = (await fetchCreditsSnapshot(apiUrl, token))?.spendable ?? null;
27191
- const picosOrNull = (v) => v === null ? null : String(v);
27056
+ const balanceAfter = await readCoinBalance(apiUrl, token);
27192
27057
  const record = {
27193
- v: 2,
27194
- currency: "credits",
27058
+ v: 1,
27195
27059
  ranAt: (/* @__PURE__ */ new Date()).toISOString(),
27196
27060
  apiUrl,
27197
27061
  projectId,
@@ -27200,34 +27064,28 @@ async function runBench(args) {
27200
27064
  outputFormat,
27201
27065
  samples,
27202
27066
  completed: settledCount,
27203
- maxCreditsPerSample: maxCredits,
27204
- costUsdPicos: rec.costs.map(String),
27205
- chargedCredits: rec.charged,
27206
- cost: { p50: picosOrNull(rec.costP50), p95: picosOrNull(rec.costP95), max: picosOrNull(rec.costMax) },
27207
- charged: { p50: rec.chargedP50, p95: rec.chargedP95, max: rec.chargedMax },
27208
- fitCredits: rec.fits,
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,
27209
27076
  fitP95: rec.fitP95,
27210
- fitMax: rec.fitMax,
27211
- recommendedDeclaredHeadroomBps: terms.headroomBps,
27212
- feeBps: terms.feeBps,
27213
- usdCentsPerCredit: terms.usdCentsPerCredit,
27214
- costBasedMaxCredits: rec.costBasedMaxCredits,
27215
- recommendedMaxCredits: rec.recommendedMaxCredits,
27216
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
27217
- averageCreditsPerCall: rec.averageCreditsPerCall,
27218
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
27219
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
27077
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
27078
+ lengthRaisedPrice: rec.lengthRaisedPrice,
27220
27079
  cutOffSamples: rec.cutOffSamples,
27221
27080
  noRecommendationReason: rec.noRecommendationReason
27222
27081
  };
27223
27082
  const savedTo = await saveBench(cwd, record);
27224
27083
  if (opts.json) {
27225
- const ok = settledCount > 0 && !lengthBlocked;
27084
+ const ok = charged.length > 0 && !lengthBlocked;
27226
27085
  writeJsonLine({
27227
27086
  command: "llm bench",
27228
27087
  status: ok ? "ok" : "failed",
27229
27088
  error: ok ? null : rec.noRecommendationReason ?? "no_sample_settled",
27230
- currency: "credits",
27231
27089
  apiUrl,
27232
27090
  projectId,
27233
27091
  modelId,
@@ -27236,54 +27094,35 @@ async function runBench(args) {
27236
27094
  schemaPath: opts.schemaPath ?? null,
27237
27095
  samples,
27238
27096
  completed: settledCount,
27239
- maxCreditsPerSample: maxCredits,
27240
- // Spendable credits before and after the run; null when unreadable.
27097
+ maxCoinsPerSample: maxCoins,
27241
27098
  balanceBefore,
27242
27099
  balanceAfter,
27243
27100
  results,
27244
27101
  // The create-time refusal that ended the run, or null. Beside the
27245
27102
  // samples, never among them: no attempt existed and nothing was spent.
27246
27103
  refusal: refusal2,
27247
- // Over the settled samples: the real provider cost (pico-dollar strings
27248
- // and USD) and the whole credits charged.
27249
- costUsdPicos: record.cost,
27250
- costUsd: {
27251
- p50: rec.costP50 === null ? null : Number(rec.costP50) / 1e12,
27252
- p95: rec.costP95 === null ? null : Number(rec.costP95) / 1e12,
27253
- max: rec.costMax === null ? null : Number(rec.costMax) / 1e12
27254
- },
27255
- chargedCredits: record.charged,
27256
- // 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`,
27257
27106
  // headroom already on the tokens), over the settled samples.
27258
- fitCredits: { p95: rec.fitP95, max: rec.fitMax },
27259
- recommendedDeclaredHeadroomBps: terms.headroomBps,
27260
- feeBps: terms.feeBps,
27261
- usdCentsPerCredit: terms.usdCentsPerCredit,
27262
- missingTerms: rec.missingTerms,
27263
- costBasedMaxCredits: rec.costBasedMaxCredits,
27264
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
27107
+ fitCoins: { p95: rec.fitP95, max: rec.fitMax },
27108
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
27109
+ lengthRaisedPrice: rec.lengthRaisedPrice,
27265
27110
  cutOffSamples: rec.cutOffSamples,
27266
27111
  exceedsStandCap: rec.exceedsStandCap,
27267
27112
  noRecommendationReason: rec.noRecommendationReason,
27268
- recommendedMaxCredits: rec.recommendedMaxCredits,
27269
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
27270
- averageCreditsPerCall: rec.averageCreditsPerCall,
27271
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
27113
+ recommendedDeclaredHeadroomBps: headroomBps,
27114
+ recommendedEstimateCoins: recommended,
27115
+ recommendedPerCallMaxCoins: ceiling,
27272
27116
  savedTo
27273
27117
  });
27274
27118
  if (!ok) process.exitCode = 1;
27275
27119
  return;
27276
27120
  }
27277
27121
  log.plain("");
27278
- const printMeasured = () => {
27279
- log.plain(c.bold(" Real cost per call"));
27280
- log.plain(` p50 ${usdFromPicos(rec.costP50)} p95 ${usdFromPicos(rec.costP95)} max ${usdFromPicos(rec.costMax)} (${settledCount} of ${samples} settled)`);
27281
- log.plain(c.bold(" Charged credits"));
27282
- log.plain(` p50 ${rec.chargedP50} p95 ${rec.chargedP95} max ${rec.chargedMax} (whole credits, rounded up per call)`);
27283
- };
27284
27122
  if (lengthBlocked) {
27285
- if (settledCount > 0) {
27286
- printMeasured();
27123
+ if (charged.length > 0) {
27124
+ log.plain(c.bold(" Charged Nex"));
27125
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
27287
27126
  log.plain("");
27288
27127
  }
27289
27128
  printRecommendation(log, record);
@@ -27291,16 +27130,17 @@ async function runBench(args) {
27291
27130
  process.exitCode = 1;
27292
27131
  return;
27293
27132
  }
27294
- if (settledCount === 0) {
27133
+ if (charged.length === 0) {
27295
27134
  log.error(
27296
- 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."
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."
27297
27136
  );
27298
27137
  process.exitCode = 1;
27299
27138
  return;
27300
27139
  }
27301
- printMeasured();
27140
+ log.plain(c.bold(" Charged Nex"));
27141
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
27302
27142
  if (balanceBefore !== null && balanceAfter !== null) {
27303
- log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} credits spendable`);
27143
+ log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} Nex spendable`);
27304
27144
  }
27305
27145
  log.plain("");
27306
27146
  printRecommendation(log, record);
@@ -27312,58 +27152,43 @@ function printRecommendation(log, record) {
27312
27152
  return;
27313
27153
  }
27314
27154
  if (record.noRecommendationReason === "answer_cut_off") {
27315
- log.warn(` ${benchCutOffLine(record.cutOffSamples || 1, record.maxCreditsPerSample)}`);
27155
+ log.warn(` ${benchCutOffLine(record.cutOffSamples ?? 1, record.maxCoinsPerSample ?? 0)}`);
27316
27156
  return;
27317
27157
  }
27318
- const attempts = `${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}`;
27319
- if (record.recommendedMaxCredits === null || record.cost.p95 === null) {
27320
- log.warn(" No ceiling recommended.");
27158
+ if (record.recommendedEstimateCoins === null || record.p95 === null) {
27159
+ log.warn(" No price recommended.");
27321
27160
  log.dim(
27322
- 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"
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"
27323
27162
  );
27324
27163
  log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
27325
- printAverage(log, record, attempts);
27326
27164
  return;
27327
27165
  }
27328
- log.plain(c.bold(` Declare maxCredits: ${record.recommendedMaxCredits}`));
27329
- if (record.lengthRaisedCeiling) {
27166
+ log.plain(c.bold(` Declare estimateCoins: ${record.recommendedEstimateCoins}`));
27167
+ if (record.lengthRaisedPrice) {
27330
27168
  log.plain(` ${LLM_LENGTH_RAISED_LINE}`);
27331
27169
  log.dim(
27332
- ` = the smallest ceiling whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
27170
+ ` = the smallest price whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
27333
27171
  );
27334
27172
  log.dim(
27335
- ` 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}.`
27173
+ ` charged-based ${record.chargedBasedEstimateCoins ?? "\u2014"} (p95 of charged Nex ${record.p95} plus ${record.recommendedDeclaredHeadroomBps} bps), over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`
27336
27174
  );
27337
27175
  } else {
27338
27176
  log.dim(
27339
- ` = 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,`
27177
+ ` = p95 of charged Nex (${record.p95}) plus this stand's recommended headroom (${record.recommendedDeclaredHeadroomBps} bps),`
27340
27178
  );
27341
- log.dim(` measured over ${attempts}.`);
27179
+ log.dim(` measured over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`);
27342
27180
  }
27343
- log.dim(" That number is a CEILING, not a price: a call is billed its real cost as used, never more than it \u2014");
27344
- log.dim(" and it also sizes how long the answer may be.");
27345
- if (record.recommendedPerCallMaxCredits !== null) {
27346
- 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}`));
27347
27184
  log.dim(
27348
- 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.`
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.`
27349
27186
  );
27350
- }
27351
- printAverage(log, record, attempts);
27352
- }
27353
- function printAverage(log, record, attempts) {
27354
- if (record.averageCreditsPerCall === null) return;
27355
- log.plain(` Per-call estimate: about ${formatCredits(record.averageCreditsPerCall)} credits per call`);
27356
- log.dim(` = the average real cost with the platform fee, no headroom, over ${attempts}.`);
27357
- if (record.recommendedPerCallEstimateCredits !== null) {
27358
- log.plain(c.bold(` Grant perCallEstimateCredits: ${record.recommendedPerCallEstimateCredits}`));
27359
- 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.");
27360
27188
  }
27361
27189
  }
27362
27190
  async function explainBenchRefusal(res, call2, base, log, opts, index, settledSoFar) {
27363
- if (printedStructuredError(res)) {
27364
- const printed = await res.json().catch(() => ({}));
27365
- return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
27366
- }
27191
+ if (printedStructuredError(res)) return { stop: true, refusal: { status: res.status, error: null, slotsHeld: null, slotLimit: null } };
27367
27192
  const body = await res.json().catch(() => ({}));
27368
27193
  const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
27369
27194
  if (res.status === 429 && body.error === "generation_limit") {
@@ -27373,10 +27198,6 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
27373
27198
  if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
27374
27199
  return { stop: true, refusal: refusal2 };
27375
27200
  }
27376
- if (res.status === 403 && body.error === "credits_unverified") {
27377
- if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
27378
- return { stop: true, refusal: refusal2 };
27379
- }
27380
27201
  if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
27381
27202
  if (res.status === 404 && body.error === "not_found") {
27382
27203
  log.error(` ${LLM_LANE_OFF_LINE}`);
@@ -27392,7 +27213,7 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
27392
27213
  return { stop: true, refusal: refusal2 };
27393
27214
  }
27394
27215
  if (res.status === 402) {
27395
- log.error(" Not enough credits to start the attempt \u2014 a bench is paid from your own balance.");
27216
+ log.error(" Not enough Nex to start the attempt.");
27396
27217
  return { stop: true, refusal: refusal2 };
27397
27218
  }
27398
27219
  log.error(
@@ -27401,7 +27222,7 @@ async function explainBenchRefusal(res, call2, base, log, opts, index, settledSo
27401
27222
  return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
27402
27223
  }
27403
27224
  function printProviderRefusal(log, status2, message, awaitingBill = false) {
27404
- 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.`);
27225
+ if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so this call's Nex stays held until it resolves (see \`genex llm status\`); it is not a sample.`);
27405
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.");
27406
27227
  if (message) log.dim(` Provider said: ${message}`);
27407
27228
  if (status2 === 401 || status2 === 403) {
@@ -27427,6 +27248,19 @@ async function pollSettled(call2, url, timeoutSec) {
27427
27248
  }
27428
27249
  return last;
27429
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
+ }
27430
27264
  async function saveBench(cwd, record) {
27431
27265
  try {
27432
27266
  const file = path39.join(cwd, LLM_BENCH_FILE);
@@ -27442,10 +27276,9 @@ function benchFailed(opts, log, message) {
27442
27276
  else log.error(message);
27443
27277
  process.exitCode = 1;
27444
27278
  }
27445
- function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts) {
27279
+ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts) {
27446
27280
  return {
27447
27281
  command: "llm bench",
27448
- currency: "credits",
27449
27282
  apiUrl,
27450
27283
  projectId,
27451
27284
  modelId: opts.modelId ?? null,
@@ -27454,28 +27287,21 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCre
27454
27287
  schemaPath: opts.schemaPath ?? null,
27455
27288
  samples,
27456
27289
  completed: 0,
27457
- maxCreditsPerSample: maxCredits,
27290
+ maxCoinsPerSample: maxCoins,
27458
27291
  balanceBefore: null,
27459
27292
  balanceAfter: null,
27460
27293
  results: [],
27461
27294
  refusal: null,
27462
- costUsdPicos: { p50: null, p95: null, max: null },
27463
- costUsd: { p50: null, p95: null, max: null },
27464
- chargedCredits: { p50: null, p95: null, max: null },
27465
- fitCredits: { p95: null, max: null },
27466
- recommendedDeclaredHeadroomBps: null,
27467
- feeBps: null,
27468
- usdCentsPerCredit: null,
27469
- missingTerms: null,
27470
- costBasedMaxCredits: null,
27471
- lengthRaisedCeiling: false,
27295
+ chargedCoins: { p50: null, p95: null, max: null },
27296
+ fitCoins: { p95: null, max: null },
27297
+ chargedBasedEstimateCoins: null,
27298
+ lengthRaisedPrice: false,
27472
27299
  cutOffSamples: 0,
27473
27300
  exceedsStandCap: false,
27474
27301
  noRecommendationReason: null,
27475
- recommendedMaxCredits: null,
27476
- recommendedPerCallMaxCredits: null,
27477
- averageCreditsPerCall: null,
27478
- recommendedPerCallEstimateCredits: null,
27302
+ recommendedDeclaredHeadroomBps: null,
27303
+ recommendedEstimateCoins: null,
27304
+ recommendedPerCallMaxCoins: null,
27479
27305
  savedTo: null
27480
27306
  };
27481
27307
  }
@@ -27538,8 +27364,7 @@ async function reportStatus(args) {
27538
27364
  for (const row of rows) {
27539
27365
  const state = rowState(row);
27540
27366
  const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
27541
- const money = rowMoney(row);
27542
- const reserved = money.reserved === null ? "reserved \u2014" : `${money.reserved} ${UNIT_WORD[money.unit]} reserved`;
27367
+ const reserved = typeof row.reservedCoins === "number" ? `${row.reservedCoins} Nex reserved` : "reserved \u2014";
27543
27368
  log.plain(
27544
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)")}`
27545
27370
  );
@@ -27611,11 +27436,10 @@ async function cancelCall(args) {
27611
27436
  if (!opts.json) {
27612
27437
  log.warn(` ${row.id} (${row.status ?? "?"}) \u2014 ${sentence}.`);
27613
27438
  if (state === "awaiting bill") {
27614
- 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.`);
27439
+ log.dim(` Nothing to cancel: its Nex releases when the bill resolves, and it stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`);
27615
27440
  if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
27616
27441
  } else {
27617
- const money = rowMoney(row);
27618
- log.dim(` ${money.charged === null ? "charge unknown" : `${money.charged} ${UNIT_WORD[money.unit]} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
27442
+ log.dim(` ${typeof row.chargedCoins === "number" ? `${row.chargedCoins} Nex charged` : "charge unknown"}${row.error ? ` \xB7 ${row.error}` : ""}.`);
27619
27443
  }
27620
27444
  }
27621
27445
  return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
@@ -27637,126 +27461,78 @@ async function cancelCall(args) {
27637
27461
  const after = await res.json().catch(() => null) ?? row;
27638
27462
  if (!opts.json) {
27639
27463
  log.success(` Cancelled ${row.id} (was ${row.status ?? "active"}).`);
27640
- log.dim(` What it reserved releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
27464
+ log.dim(` Its Nex releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
27641
27465
  }
27642
27466
  return done("ok", null, "cancelled", after);
27643
27467
  }
27644
- async function readSavedBench(cwd) {
27645
- let parsed;
27468
+ async function reportSavedPrice(cwd, opts, log) {
27469
+ let record = null;
27646
27470
  try {
27647
- parsed = JSON.parse(await fs39.readFile(path39.join(cwd, LLM_BENCH_FILE), "utf8"));
27471
+ record = JSON.parse(await fs39.readFile(path39.join(cwd, LLM_BENCH_FILE), "utf8"));
27648
27472
  } catch {
27649
- return null;
27473
+ record = null;
27650
27474
  }
27651
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
27652
- const v = parsed;
27653
- if (v.v === 2 && v.currency === "credits" && v.cost && v.charged) return { kind: "credits", record: parsed };
27654
- return { kind: "coins", record: parsed };
27655
- }
27656
- var LLM_PRICE_COIN_TERMS_LINE = "This bench was measured under the old Nex terms, when a game declared a fixed Nex 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 Nex price is not that number. Re-run the bench for the figures to declare.";
27657
- async function reportSavedPrice(cwd, opts, log) {
27658
- const saved = await readSavedBench(cwd);
27659
- const rerun = 'genex llm bench "<prompt>" --max-credits <n> --user-approved';
27660
- const creditKeys = (record2) => ({
27661
- costUsdPicos: record2?.cost ?? { p50: null, p95: null, max: null },
27662
- chargedCredits: record2?.charged ?? { p50: null, p95: null, max: null },
27663
- fitP95: record2?.fitP95 ?? null,
27664
- fitMax: record2?.fitMax ?? null,
27665
- recommendedDeclaredHeadroomBps: record2?.recommendedDeclaredHeadroomBps ?? null,
27666
- feeBps: record2?.feeBps ?? null,
27667
- usdCentsPerCredit: record2?.usdCentsPerCredit ?? null,
27668
- costBasedMaxCredits: record2?.costBasedMaxCredits ?? null,
27669
- lengthRaisedCeiling: record2?.lengthRaisedCeiling ?? false,
27670
- cutOffSamples: record2?.cutOffSamples ?? 0,
27671
- noRecommendationReason: record2?.noRecommendationReason ?? null,
27672
- recommendedMaxCredits: record2?.recommendedMaxCredits ?? null,
27673
- recommendedPerCallMaxCredits: record2?.recommendedPerCallMaxCredits ?? null,
27674
- averageCreditsPerCall: record2?.averageCreditsPerCall ?? null,
27675
- recommendedPerCallEstimateCredits: record2?.recommendedPerCallEstimateCredits ?? null
27676
- });
27677
- if (!saved) {
27475
+ if (!record) {
27678
27476
  if (opts.json) {
27679
27477
  writeJsonLine({
27680
27478
  command: "llm price",
27681
27479
  status: "failed",
27682
27480
  error: "no_bench",
27683
- currency: null,
27684
27481
  ranAt: null,
27685
27482
  modelId: null,
27686
27483
  samples: null,
27687
27484
  completed: null,
27688
- ...creditKeys(null),
27689
- legacyCoinTerms: null,
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,
27690
27496
  savedTo: null
27691
27497
  });
27692
27498
  } else {
27693
27499
  log.error("Nothing has been benchmarked in this folder yet.");
27694
- log.dim(` Run ${c.cyan(rerun)} first.`);
27695
- }
27696
- process.exitCode = 1;
27697
- return;
27698
- }
27699
- if (saved.kind === "coins") {
27700
- const old = saved.record;
27701
- const legacy = {
27702
- p50: old.p50 ?? null,
27703
- p95: old.p95 ?? null,
27704
- max: old.max ?? null,
27705
- recommendedEstimateCoins: old.recommendedEstimateCoins ?? null,
27706
- recommendedPerCallMaxCoins: old.recommendedPerCallMaxCoins ?? null,
27707
- maxCoinsPerSample: old.maxCoinsPerSample ?? null,
27708
- noRecommendationReason: old.noRecommendationReason ?? null
27709
- };
27710
- if (opts.json) {
27711
- writeJsonLine({
27712
- command: "llm price",
27713
- status: "failed",
27714
- error: "coin_terms",
27715
- currency: "coins",
27716
- ranAt: old.ranAt ?? null,
27717
- modelId: old.modelId ?? null,
27718
- samples: old.samples ?? null,
27719
- completed: old.completed ?? null,
27720
- ...creditKeys(null),
27721
- legacyCoinTerms: legacy,
27722
- savedTo: LLM_BENCH_FILE
27723
- });
27724
- } else {
27725
- log.plain(c.bold("genex llm price"));
27726
- log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${old.ranAt ?? "at an unknown time"}${old.modelId ? ` on ${old.modelId}` : ""}`);
27727
- log.plain("");
27728
- log.warn(` ${LLM_PRICE_COIN_TERMS_LINE}`);
27729
- const measured = legacy.p95 === null ? "no settled sample" : `charged Nex p50 ${legacy.p50} \xB7 p95 ${legacy.p95} \xB7 max ${legacy.max}`;
27730
- const recommended = legacy.recommendedEstimateCoins === null ? "no price recommended" : `it recommended estimateCoins ${legacy.recommendedEstimateCoins}${legacy.recommendedPerCallMaxCoins != null ? ` and perCallMaxCoins ${legacy.recommendedPerCallMaxCoins}` : ""}`;
27731
- log.dim(` What it measured then: ${measured}; ${recommended}.`);
27732
- log.dim(` Re-run: ${c.cyan(rerun)}`);
27500
+ log.dim(` Run ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')} first.`);
27733
27501
  }
27734
27502
  process.exitCode = 1;
27735
27503
  return;
27736
27504
  }
27737
- const record = saved.record;
27738
27505
  if (opts.json) {
27739
27506
  writeJsonLine({
27740
27507
  command: "llm price",
27741
- status: record.recommendedMaxCredits === null ? "failed" : "ok",
27742
- error: record.recommendedMaxCredits === null ? record.noRecommendationReason ?? "no_recommendation" : null,
27743
- currency: "credits",
27508
+ status: record.recommendedEstimateCoins === null ? "failed" : "ok",
27509
+ error: record.recommendedEstimateCoins === null ? record.noRecommendationReason ?? "no_recommendation" : null,
27744
27510
  ranAt: record.ranAt ?? null,
27745
27511
  modelId: record.modelId ?? null,
27746
27512
  samples: record.samples ?? null,
27747
27513
  completed: record.completed ?? null,
27748
- ...creditKeys(record),
27749
- legacyCoinTerms: null,
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,
27750
27526
  savedTo: LLM_BENCH_FILE
27751
27527
  });
27752
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
27528
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
27753
27529
  return;
27754
27530
  }
27755
27531
  log.plain(c.bold("genex llm price"));
27756
27532
  log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
27757
27533
  log.plain("");
27758
27534
  printRecommendation(log, record);
27759
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
27535
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
27760
27536
  }
27761
27537
  function writeJsonLine(value) {
27762
27538
  process.stdout.write(`${JSON.stringify(value)}
@@ -28016,7 +27792,7 @@ function runtimeRow(state, lane, toolsOnly = false) {
28016
27792
  return {
28017
27793
  label: "Runtime",
28018
27794
  value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}${keyRefused ? ` \xB7 ${c.yellow("provider key refused")}` : ""}`,
28019
- 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`.'
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`.'
28020
27796
  };
28021
27797
  }
28022
27798
  case "off":
@@ -30413,8 +30189,7 @@ async function publishNativeBuild(ctx, body, log) {
30413
30189
  async function listNativeBuilds(ctx, log) {
30414
30190
  const res = await call(ctx, "/native-builds", { method: "GET" }, "list the builds", log);
30415
30191
  if (!res) return null;
30416
- const body = await res.json();
30417
- return { builds: body.builds, submissions: body.submissions ?? [] };
30192
+ return (await res.json()).builds;
30418
30193
  }
30419
30194
  async function removeNativeBuild(ctx, platform, arch, log) {
30420
30195
  const route = `/native-builds/${encodeURIComponent(platform)}/${encodeURIComponent(arch)}`;
@@ -30502,17 +30277,6 @@ function describeLine(d) {
30502
30277
  if (d.engine) parts.push(d.engine);
30503
30278
  return parts.join(" \xB7 ");
30504
30279
  }
30505
- function reportPublished(build, log) {
30506
- const pair = `${build.platform} ${build.arch}`;
30507
- if (build.review === "pending") {
30508
- log.success(`Sent for review \u2014 Genex desktop players on ${pair} get version ${build.version} once Genex approves it.`);
30509
- } else if (build.review === "rejected") {
30510
- const why = build.reviewReason ? `: ${build.reviewReason}` : ".";
30511
- log.warn(`Version ${build.version} for ${pair} was turned down in review${why} Change the build and publish it again.`);
30512
- } else {
30513
- log.success(`Live \u2014 Genex desktop players on ${pair} now get version ${build.version}.`);
30514
- }
30515
- }
30516
30280
  async function runPublish2(opts, log) {
30517
30281
  if (refusedInHostedSession(log)) return false;
30518
30282
  if (!opts.nativePath) {
@@ -30552,7 +30316,7 @@ async function runPublish2(opts, log) {
30552
30316
  log
30553
30317
  );
30554
30318
  if (!build) return false;
30555
- reportPublished(build, log);
30319
+ log.success(`Live \u2014 Genex desktop players on ${platform} ${arch} now get version ${build.version}.`);
30556
30320
  log.dim(` ${build.url}`);
30557
30321
  const listing = opts.gallery ? await listGame(ctx, opts, log) : void 0;
30558
30322
  if (opts.json) {
@@ -30591,25 +30355,20 @@ async function listGame(ctx, opts, log) {
30591
30355
  function buildLine(b) {
30592
30356
  return `${c.bold(`${b.platform} ${b.arch}`)} ${b.version} ${b.entry} ${sizeLine(b.sizeBytes)} ${b.publishedAt}`;
30593
30357
  }
30594
- function heldLine(b) {
30595
- const state = b.review === "rejected" ? `turned down${b.reviewReason ? `: ${b.reviewReason}` : ""}` : "in review";
30596
- return `${buildLine(b)} ${c.dim(state)}`;
30597
- }
30598
30358
  async function runList2(opts, log) {
30599
30359
  const ctx = await context3(opts, log);
30600
30360
  if (!ctx) return false;
30601
- const listed = await listNativeBuilds(ctx, log);
30602
- if (!listed) return false;
30361
+ const builds = await listNativeBuilds(ctx, log);
30362
+ if (!builds) return false;
30603
30363
  if (opts.json) {
30604
- console.log(JSON.stringify(listed));
30364
+ console.log(JSON.stringify({ builds }));
30605
30365
  return true;
30606
30366
  }
30607
- if (listed.builds.length === 0 && listed.submissions.length === 0) {
30367
+ if (builds.length === 0) {
30608
30368
  log.plain("No native builds. Publish one with `genex native publish <path>`.");
30609
30369
  return true;
30610
30370
  }
30611
- for (const b of listed.builds) log.plain(buildLine(b));
30612
- for (const b of listed.submissions) log.plain(heldLine(b));
30371
+ for (const b of builds) log.plain(buildLine(b));
30613
30372
  return true;
30614
30373
  }
30615
30374
  async function runRemove(opts, log) {
@@ -30710,16 +30469,16 @@ ${c.bold("Usage")}
30710
30469
  status | cancel. "models" says whether this
30711
30470
  stand serves the runtime LLM lane at all, and
30712
30471
  at what provider rates. "bench" runs the real
30713
- model on YOUR OWN credits and reports what each
30714
- attempt really cost; it needs --max-credits <n>
30472
+ model on YOUR OWN Nex and reports what each
30473
+ attempt was charged; it needs --max-coins <n>
30715
30474
  --user-approved, like any spend the player has
30716
30475
  to agree to. "price" reprints the last run's
30717
- recommended maxCredits. "status" lists this
30476
+ recommended estimateCoins. "status" lists this
30718
30477
  project's open calls and the slots they hold;
30719
30478
  "cancel <id>" stops an active one. Declare a
30720
- game's per-call ceiling from a bench, never
30721
- from a guess: the player is billed as used, and
30722
- the ceiling also sizes how long an answer may be.
30479
+ game's price from a bench, never from a guess:
30480
+ a started attempt is charged in full, including
30481
+ one that fails.
30723
30482
  genex player <sub> [options] Answer the in-game model requests you approve, on your
30724
30483
  OWN Claude or ChatGPT subscription: install | run |
30725
30484
  status | stop | uninstall. "install" signs this machine
@@ -31222,7 +30981,7 @@ ${c.bold("Examples")}
31222
30981
  genex items add sku_456
31223
30982
  genex llm models
31224
30983
  genex llm models --all
31225
- genex llm bench "Reply with one short taunt." --samples 5 --max-credits 20 --user-approved
30984
+ genex llm bench "Reply with one short taunt." --samples 5 --max-coins 20 --user-approved
31226
30985
  genex llm price
31227
30986
  genex llm status
31228
30987
  genex llm cancel <id>
@@ -31459,24 +31218,22 @@ ${c.bold("How the allowance works")}
31459
31218
 
31460
31219
  Every number printed is live from your account; prices can change without a deploy.
31461
31220
  `,
31462
- 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?
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?
31463
31222
 
31464
31223
  ${c.bold("Usage")}
31465
31224
  genex llm models [--all] [--json] Which models this stand serves, their provider rates per
31466
- million tokens, the platform fee a call is billed at, and
31467
- the headroom it recommends over a benchmark. The catalog
31468
- is synced from OpenRouter, so the FEATURED short list is
31469
- printed by default and --all prints every row (--json
31470
- always carries every row). Off on this stand: it says so
31471
- and exits 0 \u2014 build the feature without a model rather
31472
- than promising a 404.
31473
- genex llm bench "<prompt>" --max-credits <n> --user-approved [options]
31474
- Run the real model through the development lane and
31475
- report what each attempt really COST and was charged.
31476
- Refused without both flags, before any network call:
31477
- this spends YOUR OWN CREDITS, a spend the build's asset
31478
- allowance does not count. Needs a folder linked to a
31479
- 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.
31480
31237
  genex llm price [--json] The recommendation from the last bench in this folder.
31481
31238
  Reads a file; spends nothing.
31482
31239
  genex llm status [--json] This project's open development calls: active, or
@@ -31493,32 +31250,29 @@ ${c.bold("Bench options")}
31493
31250
  --samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
31494
31251
  --schema <file> A JSON file holding the output schema; implies JSON output.
31495
31252
  --json-output Ask for JSON without a schema. --text Ask for text (the default).
31496
- --max-credits <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own credits,
31253
+ --max-coins <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own Nex,
31497
31254
  never a player's. The run's worst case is that number times --samples,
31498
- and it is printed before anything starts. (--max-coins is its deprecated
31499
- name, read as the same number.)
31255
+ and it is printed before anything starts.
31500
31256
  --timeout <seconds> Bounds the wait for each attempt.
31501
31257
  --json One machine-readable object; every key is always present, null when
31502
31258
  the CLI could not learn it.
31503
31259
 
31504
31260
  ${c.bold("Why a benchmark and not an estimate")}
31505
- An in-game call is paid from the PLAYER's credits and billed as used: its real provider cost
31506
- plus the platform fee, never more than the per-call CEILING the game declares (\`maxCredits\`,
31507
- or \`perCallMaxCredits\` on a standing budget). The ceiling is not a price, but it is still a
31508
- number with two jobs: it bounds what one call may cost, and it funds how LONG the answer may
31509
- be. Three layers sit between the provider's rate card and it \u2014 the provider's cost for YOUR
31510
- prompt, the platform fee, and your headroom \u2014 and only a real call measures the first. The
31511
- bench is billed to YOU, on the development lane; the player-funded lane is what the
31512
- published game uses. The recommendation is the p95 real cost with the server's fee and
31513
- headroom applied, in whole credits; a stand that serves no headroom or no fee gets no
31514
- 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 Nex), 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.
31515
31270
 
31516
- Because the ceiling sizes each call's output allowance, the recommendation is never below
31517
- the smallest ceiling that leaves room for the benchmarked answer, as the server computes it.
31518
- A sample cut off at your --max-credits (provider_token_limit) is not a sample \u2014 re-run with a
31519
- higher --max-credits; an answer longer than one call on this stand may produce gets no
31520
- ceiling at all \u2014 ask for a shorter one. The run also prints the measured average per call,
31521
- 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.
31522
31276
 
31523
31277
  Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
31524
31278
  `,
@@ -31773,13 +31527,11 @@ function parseArgs(argv) {
31773
31527
  "--limit",
31774
31528
  // `genex budget --assets <credits>` — the allowance the player approved.
31775
31529
  "--assets",
31776
- // `genex llm bench` value flags. `--max-credits` is the same approval
31777
- // shape as `--assets`, but its own gate: the asset allowance does not
31778
- // 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).
31779
31532
  "--model",
31780
31533
  "--samples",
31781
31534
  "--schema",
31782
- "--max-credits",
31783
31535
  "--max-coins",
31784
31536
  // `genex player install --label <name>` — what this machine is called in
31785
31537
  // the dashboard. Its own key rather than the generic 2nd positional,
@@ -32350,14 +32102,12 @@ function applyValueFlag(options, flag, value) {
32350
32102
  options.samples = n;
32351
32103
  break;
32352
32104
  }
32353
- case "--max-credits":
32354
32105
  case "--max-coins": {
32355
32106
  const n = Number(value);
32356
32107
  if (!Number.isInteger(n) || n < 1) {
32357
- throw new Error(`Invalid ${flag} value: ${value} (whole credits, 1 or more)`);
32108
+ throw new Error(`Invalid --max-coins value: ${value} (whole coin, 1 or more)`);
32358
32109
  }
32359
- if (flag === "--max-credits") options.maxCredits = n;
32360
- else options.maxCoins = n;
32110
+ options.maxCoins = n;
32361
32111
  break;
32362
32112
  }
32363
32113
  case "--timeout": {
@@ -32668,9 +32418,9 @@ async function main() {
32668
32418
  await runBudget(parsed.options);
32669
32419
  break;
32670
32420
  // The in-game model lane: is it live here, what does one call really
32671
- // cost, what ceiling should the game declare. `bench` spends the
32672
- // builder's own credits OUTSIDE the asset allowance, so it carries its
32673
- // own --max-credits/--user-approved gate.
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.
32674
32424
  case "llm":
32675
32425
  await runLlm(parsed.options);
32676
32426
  break;