@genex-ai/cli-demo 1.36.4-dev.807 → 1.36.4

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 * as Sentry from "@sentry/node";
@@ -981,7 +981,7 @@ Important note: put soul into your creations, with many details and love. Aim to
981
981
  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.
982
982
  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.
983
983
  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.
984
- 24. **Benchmark before declaring an in-game LLM ceiling.** If the game calls a language model while the player plays \u2014 an NPC answering in its own words, a quest written for this save, a judge reading what the player typed \u2014 the PLAYER pays for it from their Genex credits, billed as used, and the game declares no price, only a per-call CEILING: \`maxCredits\` on \`generate()\`, \`perCallMaxCredits\` on a standing budget. That ceiling bounds what one call may cost AND funds how long its answer may be, so never pick it by judgement or from a vendor's rate card. Run the real prompt on your own credits \u2014 \`npx genex llm bench "<the prompt>" --schema <file> --samples 3 --max-credits <n> --user-approved\` \u2014 and declare the figures it prints, recorded in \`DESIGN.md\` with its model and date; re-benchmark whenever the prompt, the schema or the model changes, because a prompt edit changes the ceiling. Check the lane first with \`npx genex llm models\`: where it is off the routes 404, so build the feature behind a graceful unavailable state and say so in the handoff. A repeated call is a standing budget the player approves once (\`requestSpendGrant()\`, then \`generate({ grantId })\`), never a popup per turn, and its disclosed call rate and credits per period are computed from the game's own loop and the bench's measured average. The coin spellings (\`estimateCoins\`, \`perCallMaxCoins\`) are deprecated \u2014 write the credit names. Model output is data the game validates against its own expectation \u2014 never executed, and never authority over coin, credits, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
984
+ 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.
985
985
  ${CONTRACT_END}
986
986
  `;
987
987
  var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
@@ -995,7 +995,7 @@ var TOOLS_CONTRACT_OFFER = `5. **When the game is built and playable \u2014 neve
995
995
  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.
996
996
  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.
997
997
  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.`;
998
- 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?`;
998
+ 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?`;
999
999
  function toolsContractLlmLaw(n, hosted) {
1000
1000
  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).";
1001
1001
  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.`;
@@ -22946,7 +22946,7 @@ async function runBlender(opts) {
22946
22946
  return 1;
22947
22947
  }
22948
22948
  if (sub === "serve") {
22949
- const { serveLocalBlender } = await import("./blender-serve-EH2MOW3U.js");
22949
+ const { serveLocalBlender } = await import("./blender-serve-66OHC4ML.js");
22950
22950
  const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
22951
22951
  log.step(`Starting a local Blender service on port ${port}`);
22952
22952
  log.plain(
@@ -22955,7 +22955,7 @@ async function runBlender(opts) {
22955
22955
  return serveLocalBlender({ port, log });
22956
22956
  }
22957
22957
  if (sub === "mcp") {
22958
- const { runBlenderMcp } = await import("./blender-mcp-WSURPZET.js");
22958
+ const { runBlenderMcp } = await import("./blender-mcp-QVEGHE5L.js");
22959
22959
  return runBlenderMcp();
22960
22960
  }
22961
22961
  if (sub === "seat") {
@@ -23463,12 +23463,7 @@ import fs35 from "fs/promises";
23463
23463
  import path36 from "path";
23464
23464
  import { randomUUID as randomUUID2 } from "crypto";
23465
23465
  var SUBS3 = ["models", "bench", "price", "status", "cancel"];
23466
- 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.";
23467
- var LLM_MAX_COINS_DEPRECATED = "--max-coins is deprecated: it is read as --max-credits, the same number \u2014 a bench spends credits now.";
23468
- function benchBalanceShortLine(spendable, maxCredits) {
23469
- 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.`;
23470
- }
23471
- 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.";
23466
+ 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.";
23472
23467
  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.";
23473
23468
  var LLM_TOOLS_CONVERT_HINT = [
23474
23469
  "In-game model calls need a hosted game. This folder is a Genex Tools workspace, so nothing here",
@@ -23481,7 +23476,7 @@ function serverSlotCounts(body) {
23481
23476
  return { held: body.slotsHeld, limit: body.slotLimit };
23482
23477
  }
23483
23478
  var PENDING_SLOT_RELEASE_MINUTES = 10;
23484
- 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.`;
23479
+ 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.`;
23485
23480
  var LLM_STATUS_NO_COUNT_LINE = "This stand does not report how many slots are held across your projects, so no count is shown.";
23486
23481
  var LLM_CANCEL_AWAITING_BILL = "this call already stopped; its bill is awaiting the provider";
23487
23482
  var LLM_CANCEL_ALREADY_FINAL = "this call is already final";
@@ -23495,33 +23490,11 @@ var DEFAULT_SAMPLE_TIMEOUT_SEC = 180;
23495
23490
  function parseProviderKey(value) {
23496
23491
  return value === "ok" || value === "rejected" || value === "unknown" ? value : null;
23497
23492
  }
23498
- function bpsOrNull(value) {
23499
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
23500
- }
23501
- function standingCreditGrantOrNull(value) {
23502
- if (!value || typeof value !== "object") return null;
23503
- const v = value;
23504
- const whole = (x) => typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
23505
- if (!whole(v.minCredits) || !whole(v.maxCredits) || !whole(v.stepCredits) || !whole(v.defaultCredits)) return null;
23506
- return {
23507
- minCredits: v.minCredits,
23508
- maxCredits: v.maxCredits,
23509
- stepCredits: v.stepCredits,
23510
- defaultCredits: v.defaultCredits,
23511
- ...whole(v.minUsdCents) ? { minUsdCents: v.minUsdCents } : {},
23512
- ...whole(v.maxUsdCents) ? { maxUsdCents: v.maxUsdCents } : {},
23513
- ...whole(v.stepUsdCents) ? { stepUsdCents: v.stepUsdCents } : {}
23514
- };
23515
- }
23516
23493
  async function readRuntimeLane(apiUrl, token) {
23517
23494
  const empty = (state) => ({
23518
23495
  state,
23519
23496
  models: null,
23520
23497
  recommendedDeclaredHeadroomBps: null,
23521
- currency: null,
23522
- feeBps: null,
23523
- usdCentsPerCredit: null,
23524
- standingCreditGrant: null,
23525
23498
  externalProviders: null,
23526
23499
  total: null,
23527
23500
  featuredCount: null,
@@ -23540,11 +23513,7 @@ async function readRuntimeLane(apiUrl, token) {
23540
23513
  return {
23541
23514
  state: "live",
23542
23515
  models: body.models,
23543
- recommendedDeclaredHeadroomBps: bpsOrNull(body.recommendedDeclaredHeadroomBps),
23544
- currency: body.currency === "credits" || body.currency === "coins" ? body.currency : null,
23545
- feeBps: bpsOrNull(body.feeBps),
23546
- usdCentsPerCredit: typeof body.usdCentsPerCredit === "number" && Number.isSafeInteger(body.usdCentsPerCredit) && body.usdCentsPerCredit >= 1 ? body.usdCentsPerCredit : null,
23547
- standingCreditGrant: standingCreditGrantOrNull(body.standingCreditGrant),
23516
+ recommendedDeclaredHeadroomBps: typeof body.recommendedDeclaredHeadroomBps === "number" && Number.isFinite(body.recommendedDeclaredHeadroomBps) ? body.recommendedDeclaredHeadroomBps : null,
23548
23517
  externalProviders: Array.isArray(body.externalProviders) ? body.externalProviders : null,
23549
23518
  total: typeof body.total === "number" && Number.isFinite(body.total) ? body.total : null,
23550
23519
  featuredCount: typeof body.featuredCount === "number" && Number.isFinite(body.featuredCount) ? body.featuredCount : null,
@@ -23560,45 +23529,18 @@ function percentile2(values, p) {
23560
23529
  const rank2 = Math.ceil(p / 100 * sorted.length);
23561
23530
  return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
23562
23531
  }
23563
- function percentilePicos(values, p) {
23564
- if (values.length === 0) return null;
23565
- const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
23566
- const rank2 = Math.ceil(p / 100 * sorted.length);
23567
- return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
23532
+ function recommendEstimateCoins(p95, headroomBps) {
23533
+ return Math.max(1, Math.ceil(p95 * (1e4 + headroomBps) / 1e4));
23568
23534
  }
23569
- var PICOS_PER_CENT = 10000000000n;
23570
- function ceilingCreditsFor(costPicos, terms) {
23571
- if (costPicos <= 0n) return 1;
23572
- const num = costPicos * BigInt(1e4 + terms.feeBps) * BigInt(1e4 + terms.headroomBps);
23573
- const den = 100000000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
23574
- return Math.max(1, Number((num + den - 1n) / den));
23575
- }
23576
- function averageCreditsPerCall(costsPicos, terms) {
23577
- if (costsPicos.length === 0) return null;
23578
- const sum = costsPicos.reduce((a, b) => a + b, 0n);
23579
- return Number(sum * BigInt(1e4 + terms.feeBps)) / Number(BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT);
23580
- }
23581
- function perCallEstimateCreditsFor(costsPicos, terms) {
23582
- if (costsPicos.length === 0) return null;
23583
- const sum = costsPicos.reduce((a, b) => a + b, 0n);
23584
- const num = sum * BigInt(1e4 + terms.feeBps);
23585
- const den = BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
23586
- return Math.max(1, Number((num + den - 1n) / den));
23587
- }
23588
- function formatCredits(value) {
23589
- if (!Number.isFinite(value)) return "\u2014";
23590
- if (value >= 100) return String(Math.round(value));
23591
- return String(Number(value.toPrecision(value < 1 ? 2 : 3)));
23592
- }
23593
- function usdFromPicos(picos) {
23594
- if (picos === null || picos === void 0) return "unknown";
23595
- const value = typeof picos === "bigint" ? picos : /^\d+$/.test(picos) ? BigInt(picos) : null;
23596
- if (value === null) return "unknown";
23597
- return `$${(Number(value) / 1e12).toFixed(6)}`;
23535
+ function recommendPerCallMaxCoins(p95, max, headroomBps) {
23536
+ return Math.max(recommendEstimateCoins(p95, headroomBps), recommendEstimateCoins(max, headroomBps));
23598
23537
  }
23599
23538
  function usdPerMillion(value) {
23600
23539
  return `$${value.toFixed(value < 1 ? 4 : 2)}`;
23601
23540
  }
23541
+ function usd(value) {
23542
+ return value === null ? "unknown" : `$${value.toFixed(6)}`;
23543
+ }
23602
23544
  async function runLlm(opts = {}) {
23603
23545
  const log = createLogger({ quiet: opts.quiet || opts.json });
23604
23546
  const cwd = opts.cwd ?? process.cwd();
@@ -23624,23 +23566,16 @@ async function runLlm(opts = {}) {
23624
23566
  return;
23625
23567
  }
23626
23568
  if (sub === "bench") {
23627
- if (opts.maxCoins !== void 0) process.stderr.write(`${c.yellow("!")} ${LLM_MAX_COINS_DEPRECATED}
23628
- `);
23629
- if (opts.maxCoins !== void 0 && opts.maxCredits !== void 0 && opts.maxCoins !== opts.maxCredits) {
23630
- fail5("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
23631
- return;
23632
- }
23633
- const approvedCredits = opts.maxCredits ?? opts.maxCoins;
23634
- if (approvedCredits === void 0 || opts.userApproved !== true) {
23569
+ if (opts.maxCoins === void 0 || opts.userApproved !== true) {
23635
23570
  fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
23636
23571
  return;
23637
23572
  }
23638
- if (!Number.isInteger(approvedCredits) || approvedCredits < 1) {
23639
- fail5("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
23573
+ if (!Number.isInteger(opts.maxCoins) || opts.maxCoins < 1) {
23574
+ fail5("llm bench", `--max-coins takes a whole number of coin, 1 or more (got ${String(opts.maxCoins)}).`);
23640
23575
  return;
23641
23576
  }
23642
23577
  if (!opts.benchPrompt?.trim()) {
23643
- fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-credits <n> --user-approved.');
23578
+ fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-coins <n> --user-approved.');
23644
23579
  return;
23645
23580
  }
23646
23581
  if (opts.jsonOutput && opts.textOutput) {
@@ -23732,10 +23667,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
23732
23667
  models: null,
23733
23668
  externalProviders: null,
23734
23669
  recommendedDeclaredHeadroomBps: null,
23735
- currency: null,
23736
- feeBps: null,
23737
- usdCentsPerCredit: null,
23738
- standingCreditGrant: null,
23739
23670
  total: null,
23740
23671
  featuredCount: null,
23741
23672
  // The stand was never asked, so the verdict is unknown — but the key is
@@ -23761,12 +23692,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
23761
23692
  models: lane.models,
23762
23693
  externalProviders: lane.externalProviders,
23763
23694
  recommendedDeclaredHeadroomBps: lane.recommendedDeclaredHeadroomBps,
23764
- // The credit terms a call is billed on, the server's — null on a stand
23765
- // that predates credit billing, never a number the CLI filled in.
23766
- currency: lane.currency,
23767
- feeBps: lane.feeBps,
23768
- usdCentsPerCredit: lane.usdCentsPerCredit,
23769
- standingCreditGrant: lane.standingCreditGrant,
23770
23695
  total: lane.total ?? lane.models?.length ?? null,
23771
23696
  featuredCount: lane.featuredCount ?? (lane.models ? lane.models.filter((m) => m.featured).length : null),
23772
23697
  // `null` when the lane is not live or the server predates the probe.
@@ -23813,9 +23738,8 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
23813
23738
  );
23814
23739
  for (const m of shown) {
23815
23740
  const plan = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
23816
- const kind = m.kind === "typesafe" ? " \xB7 judge (classifier): judge() under a budget, not generate() or bench" : "";
23817
23741
  log.plain(
23818
- ` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}${kind}`
23742
+ ` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}`
23819
23743
  );
23820
23744
  }
23821
23745
  if (hiddenCount > 0) {
@@ -23831,104 +23755,74 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
23831
23755
  }
23832
23756
  log.plain("");
23833
23757
  if (lane.recommendedDeclaredHeadroomBps === null) {
23834
- log.dim(" This stand serves no recommended headroom, so no ceiling can be recommended from a bench.");
23758
+ log.dim(" This stand serves no recommended headroom, so no price can be recommended from a bench.");
23835
23759
  } else {
23836
- log.dim(` Recommended headroom over a benchmarked call's cost on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
23837
- }
23838
- if (lane.feeBps === null) {
23839
- log.dim(" This stand serves no platform fee \u2014 it predates credit billing, so no ceiling can be recommended from a bench.");
23840
- } else {
23841
- 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).`);
23760
+ log.dim(` Recommended headroom over benchmarked coins on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
23842
23761
  }
23843
23762
  if (toolsOnly) {
23844
23763
  printConvertHint();
23845
23764
  return;
23846
23765
  }
23847
- log.dim(" Those per-million rates are the PROVIDER's list; what one call of YOUR prompt really costs,");
23848
- 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')}`);
23766
+ log.dim(" Those per-million rates are the PROVIDER's; what your game is charged is the platform");
23767
+ log.dim(` tariff on top, which only a real call reveals: ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')}`);
23849
23768
  }
23850
23769
  function wholeOrNull(value) {
23851
23770
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
23852
23771
  }
23853
- function costPicosOf(usage) {
23854
- if (typeof usage?.costUsdPicos === "string" && /^\d+$/.test(usage.costUsdPicos)) return usage.costUsdPicos;
23855
- if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) {
23856
- return String(BigInt(Math.round(usage.costUsd * 1e12)));
23857
- }
23858
- return null;
23859
- }
23860
23772
  function sampleFrom(id, settled) {
23861
23773
  const usage = settled?.usage;
23862
- const picos = costPicosOf(usage);
23863
23774
  return {
23864
23775
  id,
23865
23776
  status: settled?.status ?? null,
23866
23777
  billingStatus: settled?.billingStatus ?? null,
23867
- chargedCredits: wholeOrNull(settled?.chargedCredits),
23868
- costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : picos !== null ? Number(picos) / 1e12 : null,
23869
- costUsdPicos: picos,
23778
+ chargedCoins: typeof settled?.chargedCoins === "number" ? settled.chargedCoins : null,
23779
+ costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : null,
23870
23780
  error: settled?.error ?? (settled ? null : "timed_out"),
23871
23781
  providerMessage: typeof settled?.providerMessage === "string" && settled.providerMessage.trim() ? settled.providerMessage.trim() : null,
23872
23782
  inputTokens: wholeOrNull(usage?.inputTokens),
23873
23783
  outputTokens: wholeOrNull(usage?.outputTokens),
23874
23784
  maxOutputTokens: wholeOrNull(usage?.maxOutputTokens),
23875
- fitCredits: typeof usage?.fitCredits === "number" && Number.isSafeInteger(usage.fitCredits) && usage.fitCredits >= 1 ? usage.fitCredits : null,
23785
+ fitCoins: typeof usage?.fitCoins === "number" && Number.isSafeInteger(usage.fitCoins) && usage.fitCoins >= 1 ? usage.fitCoins : null,
23876
23786
  fitExceedsCap: typeof usage?.fitExceedsCap === "boolean" ? usage.fitExceedsCap : null,
23877
23787
  truncated: typeof usage?.truncated === "boolean" ? usage.truncated : settled?.error === "provider_token_limit" ? true : null
23878
23788
  };
23879
23789
  }
23880
23790
  var PROVIDER_TOKEN_LIMIT = "provider_token_limit";
23881
- function benchRecommendation(results, terms) {
23882
- const settled = results.filter(
23883
- (r) => r.status === "succeeded" && r.billingStatus === "final" && r.chargedCredits !== null && r.costUsdPicos !== null
23884
- );
23885
- const costs = settled.map((r) => BigInt(r.costUsdPicos));
23886
- const charged = settled.map((r) => r.chargedCredits);
23887
- const fits = settled.map((r) => r.fitCredits).filter((v) => typeof v === "number");
23888
- const costP95 = percentilePicos(costs, 95);
23889
- const costMax = costs.length ? costs.reduce((a, b) => b > a ? b : a) : null;
23791
+ function benchRecommendation(results, headroomBps) {
23792
+ const settled = results.filter((r) => r.status === "succeeded" && r.billingStatus === "final" && typeof r.chargedCoins === "number");
23793
+ const charged = settled.map((r) => r.chargedCoins);
23794
+ const fits = settled.map((r) => r.fitCoins).filter((v) => typeof v === "number");
23795
+ const p95 = percentile2(charged, 95);
23796
+ const max = charged.length ? Math.max(...charged) : null;
23890
23797
  const fitP95 = percentile2(fits, 95);
23891
23798
  const fitMax = fits.length ? Math.max(...fits) : null;
23892
23799
  const exceedsStandCap = results.some((r) => r.fitExceedsCap === true);
23893
23800
  const cutOffSamples = results.filter((r) => r.error === PROVIDER_TOKEN_LIMIT && r.fitExceedsCap !== true).length;
23894
23801
  const noRecommendationReason = exceedsStandCap ? "answer_exceeds_stand_cap" : cutOffSamples > 0 ? "answer_cut_off" : null;
23895
- const { headroomBps, feeBps, usdCentsPerCredit } = terms;
23896
- const missingTerms = headroomBps === null ? "headroom" : feeBps === null || usdCentsPerCredit === null ? "credit_terms" : null;
23897
- const full = headroomBps !== null && feeBps !== null && usdCentsPerCredit !== null ? { headroomBps, feeBps, usdCentsPerCredit } : null;
23898
- const billing = feeBps !== null && usdCentsPerCredit !== null ? { feeBps, usdCentsPerCredit } : null;
23899
- const costBased = full !== null && costP95 !== null ? ceilingCreditsFor(costP95, full) : null;
23900
- const recommended = costBased === null || noRecommendationReason !== null ? null : Math.max(costBased, fitP95 ?? 0);
23901
- const ceiling = recommended === null || costMax === null || full === null ? null : Math.max(ceilingCreditsFor(costMax, full), recommended, fitMax ?? 0);
23902
- const average = billing !== null ? averageCreditsPerCall(costs, billing) : null;
23903
- const estimate = ceiling === null || billing === null ? null : Math.min(ceiling, perCallEstimateCreditsFor(costs, billing) ?? 1);
23802
+ const chargedBased = p95 !== null && headroomBps !== null ? recommendEstimateCoins(p95, headroomBps) : null;
23803
+ const recommended = chargedBased === null || noRecommendationReason !== null ? null : Math.max(chargedBased, fitP95 ?? 0);
23804
+ const ceiling = recommended === null || p95 === null || max === null || headroomBps === null ? null : Math.max(recommendPerCallMaxCoins(p95, max, headroomBps), recommended, fitMax ?? 0);
23904
23805
  return {
23905
- costs,
23906
23806
  charged,
23907
- costP50: percentilePicos(costs, 50),
23908
- costP95,
23909
- costMax,
23910
- chargedP50: percentile2(charged, 50),
23911
- chargedP95: percentile2(charged, 95),
23912
- chargedMax: charged.length ? Math.max(...charged) : null,
23807
+ p50: percentile2(charged, 50),
23808
+ p95,
23809
+ max,
23913
23810
  fits,
23914
23811
  fitP95,
23915
23812
  fitMax,
23916
- costBasedMaxCredits: costBased,
23917
- recommendedMaxCredits: recommended,
23918
- recommendedPerCallMaxCredits: ceiling,
23919
- averageCreditsPerCall: average,
23920
- recommendedPerCallEstimateCredits: estimate,
23921
- lengthRaisedCeiling: recommended !== null && costBased !== null && recommended > costBased,
23813
+ chargedBasedEstimateCoins: chargedBased,
23814
+ recommendedEstimateCoins: recommended,
23815
+ recommendedPerCallMaxCoins: ceiling,
23816
+ lengthRaisedPrice: recommended !== null && chargedBased !== null && recommended > chargedBased,
23922
23817
  cutOffSamples,
23923
23818
  exceedsStandCap,
23924
- noRecommendationReason,
23925
- missingTerms
23819
+ noRecommendationReason
23926
23820
  };
23927
23821
  }
23928
- 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.";
23929
- 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.";
23930
- function benchCutOffLine(cutOff, maxCredits) {
23931
- 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.`;
23822
+ 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.";
23823
+ 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.";
23824
+ function benchCutOffLine(cutOff, maxCoins) {
23825
+ 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.`;
23932
23826
  }
23933
23827
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["requires_confirmation", "queued", "dispatching", "awaiting_external"]);
23934
23828
  function rowState(row) {
@@ -23939,22 +23833,7 @@ function rowState(row) {
23939
23833
  function rowHoldsSlot(row) {
23940
23834
  if (typeof row.slotHeld === "boolean") return row.slotHeld;
23941
23835
  const state = rowState(row);
23942
- return state === "active" || state === "awaiting bill" && ((row.reservedCoins ?? 0) > 0 || (row.reservedCredits ?? 0) > 0);
23943
- }
23944
- function rowMoney(row) {
23945
- const credits = row.currency === "credits" || row.currency === void 0 && typeof row.reservedCredits === "number" && typeof row.reservedCoins !== "number";
23946
- if (credits) {
23947
- return {
23948
- unit: "credits",
23949
- reserved: typeof row.reservedCredits === "number" ? row.reservedCredits : null,
23950
- charged: typeof row.chargedCredits === "number" ? row.chargedCredits : null
23951
- };
23952
- }
23953
- return {
23954
- unit: "coin",
23955
- reserved: typeof row.reservedCoins === "number" ? row.reservedCoins : null,
23956
- charged: typeof row.chargedCoins === "number" ? row.chargedCoins : null
23957
- };
23836
+ return state === "active" || state === "awaiting bill" && (row.reservedCoins ?? 0) > 0;
23958
23837
  }
23959
23838
  function providerRefusalStatus(error) {
23960
23839
  const m = /^provider_http_(\d{3})$/.exec(error ?? "");
@@ -23973,7 +23852,7 @@ function formatAge(iso, now = Date.now()) {
23973
23852
  }
23974
23853
  async function runBench(args) {
23975
23854
  const { apiUrl, token, projectId, cwd, opts, log } = args;
23976
- const maxCredits = opts.maxCredits ?? opts.maxCoins;
23855
+ const maxCoins = opts.maxCoins;
23977
23856
  const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
23978
23857
  const prompt = opts.benchPrompt.trim();
23979
23858
  const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
@@ -23995,7 +23874,7 @@ async function runBench(args) {
23995
23874
  const lane = await readRuntimeLane(apiUrl, token);
23996
23875
  if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
23997
23876
  if (lane.state === "off") {
23998
- if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts), status: "off", error: null });
23877
+ if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts), status: "off", error: null });
23999
23878
  else {
24000
23879
  log.plain(LLM_LANE_OFF_LINE);
24001
23880
  log.dim(" Nothing was spent.");
@@ -24022,27 +23901,17 @@ async function runBench(args) {
24022
23901
  );
24023
23902
  return;
24024
23903
  }
24025
- const walletBefore = await fetchCreditsSnapshot(apiUrl, token);
24026
- if (walletBefore?.emailVerified === false) {
24027
- benchFailed(opts, log, LLM_BENCH_UNVERIFIED_LINE);
24028
- return;
24029
- }
24030
- if (walletBefore !== null && walletBefore.spendable < maxCredits) {
24031
- benchFailed(opts, log, benchBalanceShortLine(walletBefore.spendable, maxCredits));
24032
- return;
24033
- }
24034
- const balanceBefore = walletBefore?.spendable ?? null;
23904
+ const balanceBefore = await readCoinBalance(apiUrl, token);
24035
23905
  if (!opts.json) {
24036
23906
  log.plain(c.bold("genex llm bench"));
24037
23907
  log.dim(` ${apiUrl}`);
24038
23908
  log.plain("");
24039
23909
  log.plain(` Model ${c.cyan(modelId)}`);
24040
23910
  log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
24041
- log.plain(` Approved ${maxCredits} credit${maxCredits === 1 ? "" : "s"} per attempt \u2014 at worst ${maxCredits * samples} credits for this run, from your own balance`);
23911
+ log.plain(` Approved ${maxCoins} coin per attempt \u2014 at worst ${maxCoins * samples} coin for this run`);
24042
23912
  log.plain(
24043
- ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} credits spendable`}`
23913
+ ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} coin spendable`}`
24044
23914
  );
24045
- log.dim(" Billed as used: each attempt is charged its real cost plus the platform fee, rounded up to a whole credit.");
24046
23915
  log.plain("");
24047
23916
  }
24048
23917
  const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
@@ -24068,7 +23937,7 @@ async function runBench(args) {
24068
23937
  const res = await call(base, {
24069
23938
  method: "POST",
24070
23939
  body: JSON.stringify({
24071
- maxCredits,
23940
+ maxCoins,
24072
23941
  request: {
24073
23942
  idempotencyKey: `bench-${randomUUID2()}`,
24074
23943
  modelId,
@@ -24098,14 +23967,13 @@ async function runBench(args) {
24098
23967
  if (refusedAt !== null) providerRefused++;
24099
23968
  if (!opts.json) {
24100
23969
  const length = row.outputTokens !== null ? ` \xB7 ${row.outputTokens}${row.maxOutputTokens !== null ? ` of ${row.maxOutputTokens}` : ""} tokens out` : "";
24101
- const charged = row.chargedCredits === null ? "\u2014" : String(row.chargedCredits);
24102
23970
  log.plain(
24103
- ` ${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}` : ""}`
23971
+ ` ${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}` : ""}`
24104
23972
  );
24105
23973
  if (refusedAt !== null) printProviderRefusal(log, refusedAt, row.providerMessage, row.status === "unknown");
24106
23974
  if (row.error === PROVIDER_TOKEN_LIMIT) {
24107
23975
  log.dim(
24108
- 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.`
23976
+ 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.`
24109
23977
  );
24110
23978
  }
24111
23979
  }
@@ -24113,19 +23981,16 @@ async function runBench(args) {
24113
23981
  } finally {
24114
23982
  process.removeListener("SIGINT", onSigint);
24115
23983
  }
24116
- const terms = {
24117
- headroomBps: lane.recommendedDeclaredHeadroomBps,
24118
- feeBps: lane.feeBps,
24119
- usdCentsPerCredit: lane.usdCentsPerCredit
24120
- };
24121
- const rec = benchRecommendation(results, terms);
24122
- const settledCount = rec.charged.length;
23984
+ const headroomBps = lane.recommendedDeclaredHeadroomBps;
23985
+ const rec = benchRecommendation(results, headroomBps);
23986
+ const { charged, p50, p95, max } = rec;
23987
+ const settledCount = charged.length;
23988
+ const recommended = rec.recommendedEstimateCoins;
23989
+ const ceiling = rec.recommendedPerCallMaxCoins;
24123
23990
  const lengthBlocked = rec.noRecommendationReason !== null;
24124
- const balanceAfter = (await fetchCreditsSnapshot(apiUrl, token))?.spendable ?? null;
24125
- const picosOrNull = (v) => v === null ? null : String(v);
23991
+ const balanceAfter = await readCoinBalance(apiUrl, token);
24126
23992
  const record = {
24127
- v: 2,
24128
- currency: "credits",
23993
+ v: 1,
24129
23994
  ranAt: (/* @__PURE__ */ new Date()).toISOString(),
24130
23995
  apiUrl,
24131
23996
  projectId,
@@ -24134,34 +23999,28 @@ async function runBench(args) {
24134
23999
  outputFormat,
24135
24000
  samples,
24136
24001
  completed: settledCount,
24137
- maxCreditsPerSample: maxCredits,
24138
- costUsdPicos: rec.costs.map(String),
24139
- chargedCredits: rec.charged,
24140
- cost: { p50: picosOrNull(rec.costP50), p95: picosOrNull(rec.costP95), max: picosOrNull(rec.costMax) },
24141
- charged: { p50: rec.chargedP50, p95: rec.chargedP95, max: rec.chargedMax },
24142
- fitCredits: rec.fits,
24002
+ chargedCoins: charged,
24003
+ p50,
24004
+ p95,
24005
+ max,
24006
+ recommendedDeclaredHeadroomBps: headroomBps,
24007
+ recommendedEstimateCoins: recommended,
24008
+ recommendedPerCallMaxCoins: ceiling,
24009
+ maxCoinsPerSample: maxCoins,
24010
+ fitCoins: rec.fits,
24143
24011
  fitP95: rec.fitP95,
24144
- fitMax: rec.fitMax,
24145
- recommendedDeclaredHeadroomBps: terms.headroomBps,
24146
- feeBps: terms.feeBps,
24147
- usdCentsPerCredit: terms.usdCentsPerCredit,
24148
- costBasedMaxCredits: rec.costBasedMaxCredits,
24149
- recommendedMaxCredits: rec.recommendedMaxCredits,
24150
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
24151
- averageCreditsPerCall: rec.averageCreditsPerCall,
24152
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
24153
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
24012
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
24013
+ lengthRaisedPrice: rec.lengthRaisedPrice,
24154
24014
  cutOffSamples: rec.cutOffSamples,
24155
24015
  noRecommendationReason: rec.noRecommendationReason
24156
24016
  };
24157
24017
  const savedTo = await saveBench(cwd, record);
24158
24018
  if (opts.json) {
24159
- const ok = settledCount > 0 && !lengthBlocked;
24019
+ const ok = charged.length > 0 && !lengthBlocked;
24160
24020
  writeJsonLine({
24161
24021
  command: "llm bench",
24162
24022
  status: ok ? "ok" : "failed",
24163
24023
  error: ok ? null : rec.noRecommendationReason ?? "no_sample_settled",
24164
- currency: "credits",
24165
24024
  apiUrl,
24166
24025
  projectId,
24167
24026
  modelId,
@@ -24170,54 +24029,35 @@ async function runBench(args) {
24170
24029
  schemaPath: opts.schemaPath ?? null,
24171
24030
  samples,
24172
24031
  completed: settledCount,
24173
- maxCreditsPerSample: maxCredits,
24174
- // Spendable credits before and after the run; null when unreadable.
24032
+ maxCoinsPerSample: maxCoins,
24175
24033
  balanceBefore,
24176
24034
  balanceAfter,
24177
24035
  results,
24178
24036
  // The create-time refusal that ended the run, or null. Beside the
24179
24037
  // samples, never among them: no attempt existed and nothing was spent.
24180
24038
  refusal: refusal2,
24181
- // Over the settled samples: the real provider cost (pico-dollar strings
24182
- // and USD) and the whole credits charged.
24183
- costUsdPicos: record.cost,
24184
- costUsd: {
24185
- p50: rec.costP50 === null ? null : Number(rec.costP50) / 1e12,
24186
- p95: rec.costP95 === null ? null : Number(rec.costP95) / 1e12,
24187
- max: rec.costMax === null ? null : Number(rec.costMax) / 1e12
24188
- },
24189
- chargedCredits: record.charged,
24190
- // The answer's LENGTH, priced by the server per sample (`fitCredits`,
24039
+ chargedCoins: { p50, p95, max },
24040
+ // The answer's LENGTH, priced by the server per sample (`fitCoins`,
24191
24041
  // headroom already on the tokens), over the settled samples.
24192
- fitCredits: { p95: rec.fitP95, max: rec.fitMax },
24193
- recommendedDeclaredHeadroomBps: terms.headroomBps,
24194
- feeBps: terms.feeBps,
24195
- usdCentsPerCredit: terms.usdCentsPerCredit,
24196
- missingTerms: rec.missingTerms,
24197
- costBasedMaxCredits: rec.costBasedMaxCredits,
24198
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
24042
+ fitCoins: { p95: rec.fitP95, max: rec.fitMax },
24043
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
24044
+ lengthRaisedPrice: rec.lengthRaisedPrice,
24199
24045
  cutOffSamples: rec.cutOffSamples,
24200
24046
  exceedsStandCap: rec.exceedsStandCap,
24201
24047
  noRecommendationReason: rec.noRecommendationReason,
24202
- recommendedMaxCredits: rec.recommendedMaxCredits,
24203
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
24204
- averageCreditsPerCall: rec.averageCreditsPerCall,
24205
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
24048
+ recommendedDeclaredHeadroomBps: headroomBps,
24049
+ recommendedEstimateCoins: recommended,
24050
+ recommendedPerCallMaxCoins: ceiling,
24206
24051
  savedTo
24207
24052
  });
24208
24053
  if (!ok) process.exitCode = 1;
24209
24054
  return;
24210
24055
  }
24211
24056
  log.plain("");
24212
- const printMeasured = () => {
24213
- log.plain(c.bold(" Real cost per call"));
24214
- log.plain(` p50 ${usdFromPicos(rec.costP50)} p95 ${usdFromPicos(rec.costP95)} max ${usdFromPicos(rec.costMax)} (${settledCount} of ${samples} settled)`);
24215
- log.plain(c.bold(" Charged credits"));
24216
- log.plain(` p50 ${rec.chargedP50} p95 ${rec.chargedP95} max ${rec.chargedMax} (whole credits, rounded up per call)`);
24217
- };
24218
24057
  if (lengthBlocked) {
24219
- if (settledCount > 0) {
24220
- printMeasured();
24058
+ if (charged.length > 0) {
24059
+ log.plain(c.bold(" Charged coins"));
24060
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
24221
24061
  log.plain("");
24222
24062
  }
24223
24063
  printRecommendation(log, record);
@@ -24225,16 +24065,17 @@ async function runBench(args) {
24225
24065
  process.exitCode = 1;
24226
24066
  return;
24227
24067
  }
24228
- if (settledCount === 0) {
24068
+ if (charged.length === 0) {
24229
24069
  log.error(
24230
- 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."
24070
+ 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."
24231
24071
  );
24232
24072
  process.exitCode = 1;
24233
24073
  return;
24234
24074
  }
24235
- printMeasured();
24075
+ log.plain(c.bold(" Charged coins"));
24076
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
24236
24077
  if (balanceBefore !== null && balanceAfter !== null) {
24237
- log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} credits spendable`);
24078
+ log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} coin spendable`);
24238
24079
  }
24239
24080
  log.plain("");
24240
24081
  printRecommendation(log, record);
@@ -24246,58 +24087,43 @@ function printRecommendation(log, record) {
24246
24087
  return;
24247
24088
  }
24248
24089
  if (record.noRecommendationReason === "answer_cut_off") {
24249
- log.warn(` ${benchCutOffLine(record.cutOffSamples || 1, record.maxCreditsPerSample)}`);
24090
+ log.warn(` ${benchCutOffLine(record.cutOffSamples ?? 1, record.maxCoinsPerSample ?? 0)}`);
24250
24091
  return;
24251
24092
  }
24252
- const attempts = `${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}`;
24253
- if (record.recommendedMaxCredits === null || record.cost.p95 === null) {
24254
- log.warn(" No ceiling recommended.");
24093
+ if (record.recommendedEstimateCoins === null || record.p95 === null) {
24094
+ log.warn(" No price recommended.");
24255
24095
  log.dim(
24256
- 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"
24096
+ 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"
24257
24097
  );
24258
24098
  log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
24259
- printAverage(log, record, attempts);
24260
24099
  return;
24261
24100
  }
24262
- log.plain(c.bold(` Declare maxCredits: ${record.recommendedMaxCredits}`));
24263
- if (record.lengthRaisedCeiling) {
24101
+ log.plain(c.bold(` Declare estimateCoins: ${record.recommendedEstimateCoins}`));
24102
+ if (record.lengthRaisedPrice) {
24264
24103
  log.plain(` ${LLM_LENGTH_RAISED_LINE}`);
24265
24104
  log.dim(
24266
- ` = the smallest ceiling whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
24105
+ ` = the smallest price whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
24267
24106
  );
24268
24107
  log.dim(
24269
- ` 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}.`
24108
+ ` 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}.`
24270
24109
  );
24271
24110
  } else {
24272
24111
  log.dim(
24273
- ` = 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,`
24112
+ ` = p95 of charged coins (${record.p95}) plus this stand's recommended headroom (${record.recommendedDeclaredHeadroomBps} bps),`
24274
24113
  );
24275
- log.dim(` measured over ${attempts}.`);
24114
+ log.dim(` measured over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`);
24276
24115
  }
24277
- log.dim(" That number is a CEILING, not a price: a call is billed its real cost as used, never more than it \u2014");
24278
- log.dim(" and it also sizes how long the answer may be.");
24279
- if (record.recommendedPerCallMaxCredits !== null) {
24280
- log.plain(c.bold(` Grant perCallMaxCredits: ${record.recommendedPerCallMaxCredits}`));
24116
+ log.dim(" That number is a PRICE: a started attempt is charged in full, including one that fails.");
24117
+ if (record.recommendedPerCallMaxCoins != null) {
24118
+ log.plain(c.bold(` Grant perCallMaxCoins: ${record.recommendedPerCallMaxCoins}`));
24281
24119
  log.dim(
24282
- 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.`
24120
+ 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.`
24283
24121
  );
24284
- }
24285
- printAverage(log, record, attempts);
24286
- }
24287
- function printAverage(log, record, attempts) {
24288
- if (record.averageCreditsPerCall === null) return;
24289
- log.plain(` Per-call estimate: about ${formatCredits(record.averageCreditsPerCall)} credits per call`);
24290
- log.dim(` = the average real cost with the platform fee, no headroom, over ${attempts}.`);
24291
- if (record.recommendedPerCallEstimateCredits !== null) {
24292
- log.plain(c.bold(` Grant perCallEstimateCredits: ${record.recommendedPerCallEstimateCredits}`));
24293
- log.dim(` = that average rounded up to a whole credit. disclosure.estimatedCreditsPerPeriod = your calls per period \xD7 ${formatCredits(record.averageCreditsPerCall)}, rounded up.`);
24122
+ log.dim(" The ceiling is the price's room to be wrong; declaring one under the price is refused.");
24294
24123
  }
24295
24124
  }
24296
24125
  async function explainBenchRefusal(res, call, base, log, opts, index, settledSoFar) {
24297
- if (printedStructuredError(res)) {
24298
- const printed = await res.json().catch(() => ({}));
24299
- return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
24300
- }
24126
+ if (printedStructuredError(res)) return { stop: true, refusal: { status: res.status, error: null, slotsHeld: null, slotLimit: null } };
24301
24127
  const body = await res.json().catch(() => ({}));
24302
24128
  const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
24303
24129
  if (res.status === 429 && body.error === "generation_limit") {
@@ -24307,10 +24133,6 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
24307
24133
  if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
24308
24134
  return { stop: true, refusal: refusal2 };
24309
24135
  }
24310
- if (res.status === 403 && body.error === "credits_unverified") {
24311
- if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
24312
- return { stop: true, refusal: refusal2 };
24313
- }
24314
24136
  if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
24315
24137
  if (res.status === 404 && body.error === "not_found") {
24316
24138
  log.error(` ${LLM_LANE_OFF_LINE}`);
@@ -24326,7 +24148,7 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
24326
24148
  return { stop: true, refusal: refusal2 };
24327
24149
  }
24328
24150
  if (res.status === 402) {
24329
- log.error(" Not enough credits to start the attempt \u2014 a bench is paid from your own balance.");
24151
+ log.error(" Not enough coin to start the attempt.");
24330
24152
  return { stop: true, refusal: refusal2 };
24331
24153
  }
24332
24154
  log.error(
@@ -24335,7 +24157,7 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
24335
24157
  return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
24336
24158
  }
24337
24159
  function printProviderRefusal(log, status2, message, awaitingBill = false) {
24338
- 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.`);
24160
+ 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.`);
24339
24161
  else log.dim(" The provider refused this call at its door \u2014 no inference ran, it cost nothing, and it is not a sample.");
24340
24162
  if (message) log.dim(` Provider said: ${message}`);
24341
24163
  if (status2 === 401 || status2 === 403) {
@@ -24361,6 +24183,19 @@ async function pollSettled(call, url, timeoutSec) {
24361
24183
  }
24362
24184
  return last;
24363
24185
  }
24186
+ async function readCoinBalance(apiUrl, token) {
24187
+ try {
24188
+ const res = await apiFetch(`${apiUrl}/api/coin/balance`, {
24189
+ headers: { Authorization: `Bearer ${token}` },
24190
+ signal: AbortSignal.timeout(6e3)
24191
+ });
24192
+ if (!res.ok) return null;
24193
+ const body = await res.json().catch(() => null);
24194
+ return typeof body?.spendable === "number" ? body.spendable : null;
24195
+ } catch {
24196
+ return null;
24197
+ }
24198
+ }
24364
24199
  async function saveBench(cwd, record) {
24365
24200
  try {
24366
24201
  const file = path36.join(cwd, LLM_BENCH_FILE);
@@ -24376,10 +24211,9 @@ function benchFailed(opts, log, message) {
24376
24211
  else log.error(message);
24377
24212
  process.exitCode = 1;
24378
24213
  }
24379
- function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts) {
24214
+ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts) {
24380
24215
  return {
24381
24216
  command: "llm bench",
24382
- currency: "credits",
24383
24217
  apiUrl,
24384
24218
  projectId,
24385
24219
  modelId: opts.modelId ?? null,
@@ -24388,28 +24222,21 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCre
24388
24222
  schemaPath: opts.schemaPath ?? null,
24389
24223
  samples,
24390
24224
  completed: 0,
24391
- maxCreditsPerSample: maxCredits,
24225
+ maxCoinsPerSample: maxCoins,
24392
24226
  balanceBefore: null,
24393
24227
  balanceAfter: null,
24394
24228
  results: [],
24395
24229
  refusal: null,
24396
- costUsdPicos: { p50: null, p95: null, max: null },
24397
- costUsd: { p50: null, p95: null, max: null },
24398
- chargedCredits: { p50: null, p95: null, max: null },
24399
- fitCredits: { p95: null, max: null },
24400
- recommendedDeclaredHeadroomBps: null,
24401
- feeBps: null,
24402
- usdCentsPerCredit: null,
24403
- missingTerms: null,
24404
- costBasedMaxCredits: null,
24405
- lengthRaisedCeiling: false,
24230
+ chargedCoins: { p50: null, p95: null, max: null },
24231
+ fitCoins: { p95: null, max: null },
24232
+ chargedBasedEstimateCoins: null,
24233
+ lengthRaisedPrice: false,
24406
24234
  cutOffSamples: 0,
24407
24235
  exceedsStandCap: false,
24408
24236
  noRecommendationReason: null,
24409
- recommendedMaxCredits: null,
24410
- recommendedPerCallMaxCredits: null,
24411
- averageCreditsPerCall: null,
24412
- recommendedPerCallEstimateCredits: null,
24237
+ recommendedDeclaredHeadroomBps: null,
24238
+ recommendedEstimateCoins: null,
24239
+ recommendedPerCallMaxCoins: null,
24413
24240
  savedTo: null
24414
24241
  };
24415
24242
  }
@@ -24472,8 +24299,7 @@ async function reportStatus(args) {
24472
24299
  for (const row of rows) {
24473
24300
  const state = rowState(row);
24474
24301
  const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
24475
- const money2 = rowMoney(row);
24476
- const reserved = money2.reserved === null ? "reserved \u2014" : `${money2.reserved} ${money2.unit} reserved`;
24302
+ const reserved = typeof row.reservedCoins === "number" ? `${row.reservedCoins} coin reserved` : "reserved \u2014";
24477
24303
  log.plain(
24478
24304
  ` ${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)")}`
24479
24305
  );
@@ -24545,11 +24371,10 @@ async function cancelCall(args) {
24545
24371
  if (!opts.json) {
24546
24372
  log.warn(` ${row.id} (${row.status ?? "?"}) \u2014 ${sentence}.`);
24547
24373
  if (state === "awaiting bill") {
24548
- 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.`);
24374
+ 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.`);
24549
24375
  if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
24550
24376
  } else {
24551
- const money2 = rowMoney(row);
24552
- log.dim(` ${money2.charged === null ? "charge unknown" : `${money2.charged} ${money2.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
24377
+ log.dim(` ${typeof row.chargedCoins === "number" ? `${row.chargedCoins} coin charged` : "charge unknown"}${row.error ? ` \xB7 ${row.error}` : ""}.`);
24553
24378
  }
24554
24379
  }
24555
24380
  return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
@@ -24571,126 +24396,78 @@ async function cancelCall(args) {
24571
24396
  const after = await res.json().catch(() => null) ?? row;
24572
24397
  if (!opts.json) {
24573
24398
  log.success(` Cancelled ${row.id} (was ${row.status ?? "active"}).`);
24574
- log.dim(` What it reserved releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
24399
+ log.dim(` Its coin releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
24575
24400
  }
24576
24401
  return done("ok", null, "cancelled", after);
24577
24402
  }
24578
- async function readSavedBench(cwd) {
24579
- let parsed;
24403
+ async function reportSavedPrice(cwd, opts, log) {
24404
+ let record = null;
24580
24405
  try {
24581
- parsed = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
24406
+ record = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
24582
24407
  } catch {
24583
- return null;
24408
+ record = null;
24584
24409
  }
24585
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
24586
- const v = parsed;
24587
- if (v.v === 2 && v.currency === "credits" && v.cost && v.charged) return { kind: "credits", record: parsed };
24588
- return { kind: "coins", record: parsed };
24589
- }
24590
- 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.";
24591
- async function reportSavedPrice(cwd, opts, log) {
24592
- const saved = await readSavedBench(cwd);
24593
- const rerun = 'genex llm bench "<prompt>" --max-credits <n> --user-approved';
24594
- const creditKeys = (record2) => ({
24595
- costUsdPicos: record2?.cost ?? { p50: null, p95: null, max: null },
24596
- chargedCredits: record2?.charged ?? { p50: null, p95: null, max: null },
24597
- fitP95: record2?.fitP95 ?? null,
24598
- fitMax: record2?.fitMax ?? null,
24599
- recommendedDeclaredHeadroomBps: record2?.recommendedDeclaredHeadroomBps ?? null,
24600
- feeBps: record2?.feeBps ?? null,
24601
- usdCentsPerCredit: record2?.usdCentsPerCredit ?? null,
24602
- costBasedMaxCredits: record2?.costBasedMaxCredits ?? null,
24603
- lengthRaisedCeiling: record2?.lengthRaisedCeiling ?? false,
24604
- cutOffSamples: record2?.cutOffSamples ?? 0,
24605
- noRecommendationReason: record2?.noRecommendationReason ?? null,
24606
- recommendedMaxCredits: record2?.recommendedMaxCredits ?? null,
24607
- recommendedPerCallMaxCredits: record2?.recommendedPerCallMaxCredits ?? null,
24608
- averageCreditsPerCall: record2?.averageCreditsPerCall ?? null,
24609
- recommendedPerCallEstimateCredits: record2?.recommendedPerCallEstimateCredits ?? null
24610
- });
24611
- if (!saved) {
24410
+ if (!record) {
24612
24411
  if (opts.json) {
24613
24412
  writeJsonLine({
24614
24413
  command: "llm price",
24615
24414
  status: "failed",
24616
24415
  error: "no_bench",
24617
- currency: null,
24618
24416
  ranAt: null,
24619
24417
  modelId: null,
24620
24418
  samples: null,
24621
24419
  completed: null,
24622
- ...creditKeys(null),
24623
- legacyCoinTerms: null,
24420
+ p50: null,
24421
+ p95: null,
24422
+ max: null,
24423
+ fitP95: null,
24424
+ chargedBasedEstimateCoins: null,
24425
+ lengthRaisedPrice: false,
24426
+ cutOffSamples: 0,
24427
+ noRecommendationReason: null,
24428
+ recommendedDeclaredHeadroomBps: null,
24429
+ recommendedEstimateCoins: null,
24430
+ recommendedPerCallMaxCoins: null,
24624
24431
  savedTo: null
24625
24432
  });
24626
24433
  } else {
24627
24434
  log.error("Nothing has been benchmarked in this folder yet.");
24628
- log.dim(` Run ${c.cyan(rerun)} first.`);
24629
- }
24630
- process.exitCode = 1;
24631
- return;
24632
- }
24633
- if (saved.kind === "coins") {
24634
- const old = saved.record;
24635
- const legacy = {
24636
- p50: old.p50 ?? null,
24637
- p95: old.p95 ?? null,
24638
- max: old.max ?? null,
24639
- recommendedEstimateCoins: old.recommendedEstimateCoins ?? null,
24640
- recommendedPerCallMaxCoins: old.recommendedPerCallMaxCoins ?? null,
24641
- maxCoinsPerSample: old.maxCoinsPerSample ?? null,
24642
- noRecommendationReason: old.noRecommendationReason ?? null
24643
- };
24644
- if (opts.json) {
24645
- writeJsonLine({
24646
- command: "llm price",
24647
- status: "failed",
24648
- error: "coin_terms",
24649
- currency: "coins",
24650
- ranAt: old.ranAt ?? null,
24651
- modelId: old.modelId ?? null,
24652
- samples: old.samples ?? null,
24653
- completed: old.completed ?? null,
24654
- ...creditKeys(null),
24655
- legacyCoinTerms: legacy,
24656
- savedTo: LLM_BENCH_FILE
24657
- });
24658
- } else {
24659
- log.plain(c.bold("genex llm price"));
24660
- log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${old.ranAt ?? "at an unknown time"}${old.modelId ? ` on ${old.modelId}` : ""}`);
24661
- log.plain("");
24662
- log.warn(` ${LLM_PRICE_COIN_TERMS_LINE}`);
24663
- const measured = legacy.p95 === null ? "no settled sample" : `charged coins p50 ${legacy.p50} \xB7 p95 ${legacy.p95} \xB7 max ${legacy.max}`;
24664
- const recommended = legacy.recommendedEstimateCoins === null ? "no price recommended" : `it recommended estimateCoins ${legacy.recommendedEstimateCoins}${legacy.recommendedPerCallMaxCoins != null ? ` and perCallMaxCoins ${legacy.recommendedPerCallMaxCoins}` : ""}`;
24665
- log.dim(` What it measured then: ${measured}; ${recommended}.`);
24666
- log.dim(` Re-run: ${c.cyan(rerun)}`);
24435
+ log.dim(` Run ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')} first.`);
24667
24436
  }
24668
24437
  process.exitCode = 1;
24669
24438
  return;
24670
24439
  }
24671
- const record = saved.record;
24672
24440
  if (opts.json) {
24673
24441
  writeJsonLine({
24674
24442
  command: "llm price",
24675
- status: record.recommendedMaxCredits === null ? "failed" : "ok",
24676
- error: record.recommendedMaxCredits === null ? record.noRecommendationReason ?? "no_recommendation" : null,
24677
- currency: "credits",
24443
+ status: record.recommendedEstimateCoins === null ? "failed" : "ok",
24444
+ error: record.recommendedEstimateCoins === null ? record.noRecommendationReason ?? "no_recommendation" : null,
24678
24445
  ranAt: record.ranAt ?? null,
24679
24446
  modelId: record.modelId ?? null,
24680
24447
  samples: record.samples ?? null,
24681
24448
  completed: record.completed ?? null,
24682
- ...creditKeys(record),
24683
- legacyCoinTerms: null,
24449
+ p50: record.p50 ?? null,
24450
+ p95: record.p95 ?? null,
24451
+ max: record.max ?? null,
24452
+ // Absent from a file an older CLI wrote; null / false / 0 then, never missing.
24453
+ fitP95: record.fitP95 ?? null,
24454
+ chargedBasedEstimateCoins: record.chargedBasedEstimateCoins ?? null,
24455
+ lengthRaisedPrice: record.lengthRaisedPrice ?? false,
24456
+ cutOffSamples: record.cutOffSamples ?? 0,
24457
+ noRecommendationReason: record.noRecommendationReason ?? null,
24458
+ recommendedDeclaredHeadroomBps: record.recommendedDeclaredHeadroomBps ?? null,
24459
+ recommendedEstimateCoins: record.recommendedEstimateCoins ?? null,
24460
+ recommendedPerCallMaxCoins: record.recommendedPerCallMaxCoins ?? null,
24684
24461
  savedTo: LLM_BENCH_FILE
24685
24462
  });
24686
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
24463
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
24687
24464
  return;
24688
24465
  }
24689
24466
  log.plain(c.bold("genex llm price"));
24690
24467
  log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
24691
24468
  log.plain("");
24692
24469
  printRecommendation(log, record);
24693
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
24470
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
24694
24471
  }
24695
24472
  function writeJsonLine(value) {
24696
24473
  process.stdout.write(`${JSON.stringify(value)}
@@ -24950,7 +24727,7 @@ function runtimeRow(state, lane, toolsOnly = false) {
24950
24727
  return {
24951
24728
  label: "Runtime",
24952
24729
  value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}${keyRefused ? ` \xB7 ${c.yellow("provider key refused")}` : ""}`,
24953
- 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`.'
24730
+ 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`.'
24954
24731
  };
24955
24732
  }
24956
24733
  case "off":
@@ -26906,16 +26683,16 @@ ${c.bold("Usage")}
26906
26683
  status | cancel. "models" says whether this
26907
26684
  stand serves the runtime LLM lane at all, and
26908
26685
  at what provider rates. "bench" runs the real
26909
- model on YOUR OWN credits and reports what each
26910
- attempt really cost; it needs --max-credits <n>
26686
+ model on YOUR OWN coin and reports what each
26687
+ attempt was charged; it needs --max-coins <n>
26911
26688
  --user-approved, like any spend the player has
26912
26689
  to agree to. "price" reprints the last run's
26913
- recommended maxCredits. "status" lists this
26690
+ recommended estimateCoins. "status" lists this
26914
26691
  project's open calls and the slots they hold;
26915
26692
  "cancel <id>" stops an active one. Declare a
26916
- game's per-call ceiling from a bench, never
26917
- from a guess: the player is billed as used, and
26918
- the ceiling also sizes how long an answer may be.
26693
+ game's price from a bench, never from a guess:
26694
+ a started attempt is charged in full, including
26695
+ one that fails.
26919
26696
  genex player <sub> [options] Answer the in-game model requests you approve, on your
26920
26697
  OWN Claude or ChatGPT subscription: install | run |
26921
26698
  status | stop | uninstall. "install" signs this machine
@@ -27370,7 +27147,7 @@ ${c.bold("Examples")}
27370
27147
  genex shop remove sku_123
27371
27148
  genex llm models
27372
27149
  genex llm models --all
27373
- genex llm bench "Reply with one short taunt." --samples 5 --max-credits 20 --user-approved
27150
+ genex llm bench "Reply with one short taunt." --samples 5 --max-coins 20 --user-approved
27374
27151
  genex llm price
27375
27152
  genex llm status
27376
27153
  genex llm cancel <id>
@@ -27607,24 +27384,22 @@ ${c.bold("How the allowance works")}
27607
27384
 
27608
27385
  Every number printed is live from your account; prices can change without a deploy.
27609
27386
  `,
27610
- 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?
27387
+ llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what should the game declare?
27611
27388
 
27612
27389
  ${c.bold("Usage")}
27613
27390
  genex llm models [--all] [--json] Which models this stand serves, their provider rates per
27614
- million tokens, the platform fee a call is billed at, and
27615
- the headroom it recommends over a benchmark. The catalog
27616
- is synced from OpenRouter, so the FEATURED short list is
27617
- printed by default and --all prints every row (--json
27618
- always carries every row). Off on this stand: it says so
27619
- and exits 0 \u2014 build the feature without a model rather
27620
- than promising a 404.
27621
- genex llm bench "<prompt>" --max-credits <n> --user-approved [options]
27622
- Run the real model through the development lane and
27623
- report what each attempt really COST and was charged.
27624
- Refused without both flags, before any network call:
27625
- this spends YOUR OWN CREDITS, a spend the build's asset
27626
- allowance does not count. Needs a folder linked to a
27627
- game you own, and a balance that covers one attempt.
27391
+ million tokens, and the headroom it recommends over a
27392
+ benchmark. The catalog is synced from OpenRouter, so the
27393
+ FEATURED short list is printed by default and --all
27394
+ prints every row (--json always carries every row). Off
27395
+ on this stand: it says so and exits 0 \u2014 build the feature
27396
+ without a model rather than promising a 404.
27397
+ genex llm bench "<prompt>" --max-coins <n> --user-approved [options]
27398
+ Run the real model <n> times through the development
27399
+ lane and report what each attempt was CHARGED. Refused
27400
+ without both flags, before any network call: this spends
27401
+ YOUR OWN COIN, a wallet the build's asset allowance
27402
+ cannot see. Needs a folder linked to a game you own.
27628
27403
  genex llm price [--json] The recommendation from the last bench in this folder.
27629
27404
  Reads a file; spends nothing.
27630
27405
  genex llm status [--json] This project's open development calls: active, or
@@ -27641,32 +27416,29 @@ ${c.bold("Bench options")}
27641
27416
  --samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
27642
27417
  --schema <file> A JSON file holding the output schema; implies JSON output.
27643
27418
  --json-output Ask for JSON without a schema. --text Ask for text (the default).
27644
- --max-credits <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own credits,
27419
+ --max-coins <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own coin,
27645
27420
  never a player's. The run's worst case is that number times --samples,
27646
- and it is printed before anything starts. (--max-coins is its deprecated
27647
- name, read as the same number.)
27421
+ and it is printed before anything starts.
27648
27422
  --timeout <seconds> Bounds the wait for each attempt.
27649
27423
  --json One machine-readable object; every key is always present, null when
27650
27424
  the CLI could not learn it.
27651
27425
 
27652
27426
  ${c.bold("Why a benchmark and not an estimate")}
27653
- An in-game call is paid from the PLAYER's credits and billed as used: its real provider cost
27654
- plus the platform fee, never more than the per-call CEILING the game declares (\`maxCredits\`,
27655
- or \`perCallMaxCredits\` on a standing budget). The ceiling is not a price, but it is still a
27656
- number with two jobs: it bounds what one call may cost, and it funds how LONG the answer may
27657
- be. Three layers sit between the provider's rate card and it \u2014 the provider's cost for YOUR
27658
- prompt, the platform fee, and your headroom \u2014 and only a real call measures the first. The
27659
- bench is billed to YOU, on the development lane; the player-funded lane is what the
27660
- published game uses. The recommendation is the p95 real cost with the server's fee and
27661
- headroom applied, in whole credits; a stand that serves no headroom or no fee gets no
27662
- recommendation, because those multipliers are not the CLI's to invent.
27427
+ \`estimateCoins\` is a PRICE, not a guess: declaring it charges it, in full, even when the
27428
+ attempt fails, is cancelled, or stops at its budget. And three layers sit between the
27429
+ provider's rate card and that number \u2014 the provider's cost, the platform's tariff (already
27430
+ inside every charged coin), and your headroom. The bench itself is billed to YOU, on the
27431
+ development lane; the player-funded lane is what the published game uses. Adding a margin
27432
+ to the provider's rate silently drops the tariff and under-prices every call, so the
27433
+ recommendation is built from CHARGED COINS and the headroom comes from the server. A stand
27434
+ that serves no headroom gets no recommendation, because the multiplier is not the CLI's to
27435
+ invent.
27663
27436
 
27664
- Because the ceiling sizes each call's output allowance, the recommendation is never below
27665
- the smallest ceiling that leaves room for the benchmarked answer, as the server computes it.
27666
- A sample cut off at your --max-credits (provider_token_limit) is not a sample \u2014 re-run with a
27667
- higher --max-credits; an answer longer than one call on this stand may produce gets no
27668
- ceiling at all \u2014 ask for a shorter one. The run also prints the measured average per call,
27669
- fee included, which is the honest estimate a budget's disclosure is built on.
27437
+ The declared price also decides how LONG the answer may be: it sizes each call's output
27438
+ allowance. So the recommendation is never below the smallest price that leaves room for
27439
+ the benchmarked answer, as the server computes it. A sample cut off at your --max-coins
27440
+ (provider_token_limit) is not a sample \u2014 re-run with a higher --max-coins; an answer longer
27441
+ than one call on this stand may produce gets no price at all \u2014 ask for a shorter one.
27670
27442
 
27671
27443
  Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
27672
27444
  `,
@@ -27881,13 +27653,11 @@ function parseArgs(argv) {
27881
27653
  "--limit",
27882
27654
  // `genex budget --assets <credits>` — the allowance the player approved.
27883
27655
  "--assets",
27884
- // `genex llm bench` value flags. `--max-credits` is the same approval
27885
- // shape as `--assets`, but its own gate: the asset allowance does not
27886
- // count a bench. `--max-coins` is its deprecated name (same number).
27656
+ // `genex llm bench` value flags. `--max-coins` is the COIN half of the
27657
+ // same approval shape as `--assets` (a different economy, its own gate).
27887
27658
  "--model",
27888
27659
  "--samples",
27889
27660
  "--schema",
27890
- "--max-credits",
27891
27661
  "--max-coins",
27892
27662
  // `genex player install --label <name>` — what this machine is called in
27893
27663
  // the dashboard. Its own key rather than the generic 2nd positional,
@@ -28402,14 +28172,12 @@ function applyValueFlag(options, flag, value) {
28402
28172
  options.samples = n;
28403
28173
  break;
28404
28174
  }
28405
- case "--max-credits":
28406
28175
  case "--max-coins": {
28407
28176
  const n = Number(value);
28408
28177
  if (!Number.isInteger(n) || n < 1) {
28409
- throw new Error(`Invalid ${flag} value: ${value} (whole credits, 1 or more)`);
28178
+ throw new Error(`Invalid --max-coins value: ${value} (whole coin, 1 or more)`);
28410
28179
  }
28411
- if (flag === "--max-credits") options.maxCredits = n;
28412
- else options.maxCoins = n;
28180
+ options.maxCoins = n;
28413
28181
  break;
28414
28182
  }
28415
28183
  case "--timeout": {
@@ -28701,9 +28469,9 @@ async function main() {
28701
28469
  await runBudget(parsed.options);
28702
28470
  break;
28703
28471
  // The in-game model lane: is it live here, what does one call really
28704
- // cost, what ceiling should the game declare. `bench` spends the
28705
- // builder's own credits OUTSIDE the asset allowance, so it carries its
28706
- // own --max-credits/--user-approved gate.
28472
+ // cost, what should the game declare. `bench` is the only CLI command
28473
+ // that spends COIN, so it carries its own --max-coins/--user-approved
28474
+ // gate — the credits allowance cannot see that wallet.
28707
28475
  case "llm":
28708
28476
  await runLlm(parsed.options);
28709
28477
  break;