@genex-ai/cli-demo 1.36.10-dev.959 → 1.36.10

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";
@@ -1107,7 +1107,7 @@ Important note: put soul into your creations, with many details and love. Aim to
1107
1107
  21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
1108
1108
  22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Start with one reusable implementation per distinct required gameplay or visual role. Background populations default to reusable procedural bodies and motion or compatible existing rigged assets. Catalog clips need a compatible skeleton and may be paid. Reserve paid custom bodies for the player and characters examined or interacted with closely, while honoring explicit user requests. Add variants only for an unmet requirement; unspent allowance is not unfinished work. Allowance questions use timeoutPolicy no-consent: silence does not raise the allowance. Record role, reuse and completion criteria in the existing Assets table. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
1109
1109
  23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18). One file in \`.genex/scratch/\` IS read, by the platform: \`.genex/scratch/cover.png\` is the game's cover \u2014 the frame you would put on its poster: real gameplay at its signature moment, well lit, no menus, popups or busy HUD, landscape 16:9, and never any text (the page sets the title beside it). The next \`genex preview\` makes it the cover; save a better moment over it whenever one comes along, and if your browser tool can set the cover itself, use that instead. Before the first publish, and whenever the game's look changes, load \`$genex-cover\`: it chooses, stages, lights and judges that frame.
1110
- 24. **Benchmark before declaring an in-game LLM 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.
1110
+ 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.
1111
1111
  ${CONTRACT_END}
1112
1112
  `;
1113
1113
  var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
@@ -1121,7 +1121,7 @@ var TOOLS_CONTRACT_OFFER = `5. **When the game is built and playable \u2014 neve
1121
1121
  var TOOLS_CONTRACT_HOSTED_LAWS = `5. **This game is hosted on Genex now, and publishing is glue \u2014 never a rebuild.** The game ships exactly as the user built it: \`initEmbed()\` first in the boot code (the \`genex-threejs-embed-auth\` card has the call; the renderer draws before any \`await waitForPlayer()\`), a static build into \`dist/\` with relative asset paths, then \`npx genex preview\`. Never restructure, reformat or "improve" the game to ship it, never scaffold a new app around it, and never start a design document or a build plan for it \u2014 the user's own process is theirs. Load the \`genex-tool-publish\` card before the first preview; it owns the vocabulary, the links and the limits.
1122
1122
  6. **Ship first, then report.** \`preview\` prints a preflight \u2014 phone memory, a missing volume slider, an asset nobody wired, a viewport line. It never blocks a deploy, so push the build as it is, hand over the link, THEN relay each preflight line to the user in one plain sentence with an offer to fix it, and fix one only on their yes. The preflight is a report for the user, not a to-do list for you.
1123
1123
  7. **The link is the game's page, and there are two versions.** After every preview give the user \`<dashboard>/draft/<slug>\` \u2014 \`dashboardOrigins[0]\` and \`slug\` from \`.genex/project.json\` \u2014 never localhost, a file path or the bare play origin. \`preview\` updates the draft and never touches what players are on; the first release is \`npx genex publish\` (it lists the game); after that "publish it", "update it" and "yes" all mean \`npx genex promote\` \u2014 the exact draft build, no rebuild. Ask once per round of work, in one line, and keep working while you wait.`;
1124
- var TOOLS_CONTRACT_LLM_OFFER = `A model running while people play is built into the Genex platform \u2014 the player pays, with 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?`;
1124
+ 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?`;
1125
1125
  function toolsContractLlmLaw(n, hosted) {
1126
1126
  const onYes = hosted ? "On a yes, load `$genex-tool-llm` and then `$genex-llm-in-games`, which owns the build." : "On a yes, load `$genex-tool-llm`, which owns the rest: it checks the lane with `npx genex llm models` first, then runs `npx genex init --convert` (a hosted game is a static browser build \u2014 a local server holding a key can never ship).";
1127
1127
  return `${n}. **A model running while people PLAY is a platform feature \u2014 never something you wire onto the user's own meter.** When a request implies one (NPCs that talk or decide in their own words, content written from what the player types, a prompt box in the game, "let the player pick a model"), recognise it, say in ONE line: "${TOOLS_CONTRACT_LLM_OFFER}" \u2014 then ASK and wait. ${onYes} On a no, build the authored version instead. Either way, never ship a game that calls a model on a key or an account of the user's own: every visitor would spend their money with nobody approving it, and the credential is readable in the bundle. Player-funded generation is text and JSON only \u2014 3D, images, video and audio stay the asset lanes above, on the user's meter.`;
@@ -23778,7 +23778,7 @@ async function runBlender(opts) {
23778
23778
  return 1;
23779
23779
  }
23780
23780
  if (sub === "serve") {
23781
- const { serveLocalBlender } = await import("./blender-serve-EH2MOW3U.js");
23781
+ const { serveLocalBlender } = await import("./blender-serve-66OHC4ML.js");
23782
23782
  const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
23783
23783
  log.step(`Starting a local Blender service on port ${port}`);
23784
23784
  log.plain(
@@ -23787,7 +23787,7 @@ async function runBlender(opts) {
23787
23787
  return serveLocalBlender({ port, log });
23788
23788
  }
23789
23789
  if (sub === "mcp") {
23790
- const { runBlenderMcp } = await import("./blender-mcp-WSURPZET.js");
23790
+ const { runBlenderMcp } = await import("./blender-mcp-QVEGHE5L.js");
23791
23791
  return runBlenderMcp();
23792
23792
  }
23793
23793
  if (sub === "seat") {
@@ -24295,12 +24295,7 @@ import fs35 from "fs/promises";
24295
24295
  import path36 from "path";
24296
24296
  import { randomUUID as randomUUID2 } from "crypto";
24297
24297
  var SUBS3 = ["models", "bench", "price", "status", "cancel"];
24298
- var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends YOUR OWN credits \u2014 your balance, not a player's, and a spend this build's asset allowance does not count. Re-run with --max-credits <n> --user-approved only after the person at the keyboard agreed to the number.";
24299
- var LLM_MAX_COINS_DEPRECATED = "--max-coins is deprecated: it is read as --max-credits, the same number \u2014 a bench spends credits now.";
24300
- function benchBalanceShortLine(spendable, maxCredits) {
24301
- return `Your spendable credits (${spendable}) cannot cover one attempt at --max-credits ${maxCredits} \u2014 nothing was sent, nothing was spent. Add credits, or re-run with a lower --max-credits once the person at the keyboard agrees to it.`;
24302
- }
24303
- var LLM_BENCH_UNVERIFIED_LINE = "Credits pay only for a verified email, and this account's is not verified yet (credits_unverified) \u2014 nothing was sent, nothing was spent. Verify it, then re-run.";
24298
+ 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.";
24304
24299
  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.";
24305
24300
  var LLM_TOOLS_CONVERT_HINT = [
24306
24301
  "In-game model calls need a hosted game. This folder is a Genex Tools workspace, so nothing here",
@@ -24313,7 +24308,7 @@ function serverSlotCounts(body) {
24313
24308
  return { held: body.slotsHeld, limit: body.slotLimit };
24314
24309
  }
24315
24310
  var PENDING_SLOT_RELEASE_MINUTES = 10;
24316
- var LLM_STATUS_LEGEND = `"awaiting bill" = the call has stopped but the provider's bill is not final, so what it reserved stays held until the bill resolves; such a row stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`;
24311
+ 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.`;
24317
24312
  var LLM_STATUS_NO_COUNT_LINE = "This stand does not report how many slots are held across your projects, so no count is shown.";
24318
24313
  var LLM_CANCEL_AWAITING_BILL = "this call already stopped; its bill is awaiting the provider";
24319
24314
  var LLM_CANCEL_ALREADY_FINAL = "this call is already final";
@@ -24327,33 +24322,11 @@ var DEFAULT_SAMPLE_TIMEOUT_SEC = 180;
24327
24322
  function parseProviderKey(value) {
24328
24323
  return value === "ok" || value === "rejected" || value === "unknown" ? value : null;
24329
24324
  }
24330
- function bpsOrNull(value) {
24331
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
24332
- }
24333
- function standingCreditGrantOrNull(value) {
24334
- if (!value || typeof value !== "object") return null;
24335
- const v = value;
24336
- const whole = (x) => typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
24337
- if (!whole(v.minCredits) || !whole(v.maxCredits) || !whole(v.stepCredits) || !whole(v.defaultCredits)) return null;
24338
- return {
24339
- minCredits: v.minCredits,
24340
- maxCredits: v.maxCredits,
24341
- stepCredits: v.stepCredits,
24342
- defaultCredits: v.defaultCredits,
24343
- ...whole(v.minUsdCents) ? { minUsdCents: v.minUsdCents } : {},
24344
- ...whole(v.maxUsdCents) ? { maxUsdCents: v.maxUsdCents } : {},
24345
- ...whole(v.stepUsdCents) ? { stepUsdCents: v.stepUsdCents } : {}
24346
- };
24347
- }
24348
24325
  async function readRuntimeLane(apiUrl, token) {
24349
24326
  const empty = (state) => ({
24350
24327
  state,
24351
24328
  models: null,
24352
24329
  recommendedDeclaredHeadroomBps: null,
24353
- currency: null,
24354
- feeBps: null,
24355
- usdCentsPerCredit: null,
24356
- standingCreditGrant: null,
24357
24330
  externalProviders: null,
24358
24331
  total: null,
24359
24332
  featuredCount: null,
@@ -24372,11 +24345,7 @@ async function readRuntimeLane(apiUrl, token) {
24372
24345
  return {
24373
24346
  state: "live",
24374
24347
  models: body.models,
24375
- recommendedDeclaredHeadroomBps: bpsOrNull(body.recommendedDeclaredHeadroomBps),
24376
- currency: body.currency === "credits" || body.currency === "coins" ? body.currency : null,
24377
- feeBps: bpsOrNull(body.feeBps),
24378
- usdCentsPerCredit: typeof body.usdCentsPerCredit === "number" && Number.isSafeInteger(body.usdCentsPerCredit) && body.usdCentsPerCredit >= 1 ? body.usdCentsPerCredit : null,
24379
- standingCreditGrant: standingCreditGrantOrNull(body.standingCreditGrant),
24348
+ recommendedDeclaredHeadroomBps: typeof body.recommendedDeclaredHeadroomBps === "number" && Number.isFinite(body.recommendedDeclaredHeadroomBps) ? body.recommendedDeclaredHeadroomBps : null,
24380
24349
  externalProviders: Array.isArray(body.externalProviders) ? body.externalProviders : null,
24381
24350
  total: typeof body.total === "number" && Number.isFinite(body.total) ? body.total : null,
24382
24351
  featuredCount: typeof body.featuredCount === "number" && Number.isFinite(body.featuredCount) ? body.featuredCount : null,
@@ -24392,45 +24361,18 @@ function percentile2(values, p) {
24392
24361
  const rank2 = Math.ceil(p / 100 * sorted.length);
24393
24362
  return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
24394
24363
  }
24395
- function percentilePicos(values, p) {
24396
- if (values.length === 0) return null;
24397
- const sorted = [...values].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
24398
- const rank2 = Math.ceil(p / 100 * sorted.length);
24399
- return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
24364
+ function recommendEstimateCoins(p95, headroomBps) {
24365
+ return Math.max(1, Math.ceil(p95 * (1e4 + headroomBps) / 1e4));
24400
24366
  }
24401
- var PICOS_PER_CENT = 10000000000n;
24402
- function ceilingCreditsFor(costPicos, terms) {
24403
- if (costPicos <= 0n) return 1;
24404
- const num = costPicos * BigInt(1e4 + terms.feeBps) * BigInt(1e4 + terms.headroomBps);
24405
- const den = 100000000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
24406
- return Math.max(1, Number((num + den - 1n) / den));
24407
- }
24408
- function averageCreditsPerCall(costsPicos, terms) {
24409
- if (costsPicos.length === 0) return null;
24410
- const sum = costsPicos.reduce((a, b) => a + b, 0n);
24411
- return Number(sum * BigInt(1e4 + terms.feeBps)) / Number(BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT);
24412
- }
24413
- function perCallEstimateCreditsFor(costsPicos, terms) {
24414
- if (costsPicos.length === 0) return null;
24415
- const sum = costsPicos.reduce((a, b) => a + b, 0n);
24416
- const num = sum * BigInt(1e4 + terms.feeBps);
24417
- const den = BigInt(costsPicos.length) * 10000n * BigInt(terms.usdCentsPerCredit) * PICOS_PER_CENT;
24418
- return Math.max(1, Number((num + den - 1n) / den));
24419
- }
24420
- function formatCredits(value) {
24421
- if (!Number.isFinite(value)) return "\u2014";
24422
- if (value >= 100) return String(Math.round(value));
24423
- return String(Number(value.toPrecision(value < 1 ? 2 : 3)));
24424
- }
24425
- function usdFromPicos(picos) {
24426
- if (picos === null || picos === void 0) return "unknown";
24427
- const value = typeof picos === "bigint" ? picos : /^\d+$/.test(picos) ? BigInt(picos) : null;
24428
- if (value === null) return "unknown";
24429
- return `$${(Number(value) / 1e12).toFixed(6)}`;
24367
+ function recommendPerCallMaxCoins(p95, max, headroomBps) {
24368
+ return Math.max(recommendEstimateCoins(p95, headroomBps), recommendEstimateCoins(max, headroomBps));
24430
24369
  }
24431
24370
  function usdPerMillion(value) {
24432
24371
  return `$${value.toFixed(value < 1 ? 4 : 2)}`;
24433
24372
  }
24373
+ function usd(value) {
24374
+ return value === null ? "unknown" : `$${value.toFixed(6)}`;
24375
+ }
24434
24376
  async function runLlm(opts = {}) {
24435
24377
  const log = createLogger({ quiet: opts.quiet || opts.json });
24436
24378
  const cwd = opts.cwd ?? process.cwd();
@@ -24456,23 +24398,16 @@ async function runLlm(opts = {}) {
24456
24398
  return;
24457
24399
  }
24458
24400
  if (sub === "bench") {
24459
- if (opts.maxCoins !== void 0) process.stderr.write(`${c.yellow("!")} ${LLM_MAX_COINS_DEPRECATED}
24460
- `);
24461
- if (opts.maxCoins !== void 0 && opts.maxCredits !== void 0 && opts.maxCoins !== opts.maxCredits) {
24462
- fail5("llm bench", `Pass --max-credits alone \u2014 --max-coins is its deprecated name, and the two disagree (${opts.maxCredits} vs ${opts.maxCoins}).`);
24463
- return;
24464
- }
24465
- const approvedCredits = opts.maxCredits ?? opts.maxCoins;
24466
- if (approvedCredits === void 0 || opts.userApproved !== true) {
24401
+ if (opts.maxCoins === void 0 || opts.userApproved !== true) {
24467
24402
  fail5("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
24468
24403
  return;
24469
24404
  }
24470
- if (!Number.isInteger(approvedCredits) || approvedCredits < 1) {
24471
- fail5("llm bench", `--max-credits takes a whole number of credits, 1 or more (got ${String(approvedCredits)}).`);
24405
+ if (!Number.isInteger(opts.maxCoins) || opts.maxCoins < 1) {
24406
+ fail5("llm bench", `--max-coins takes a whole number of coin, 1 or more (got ${String(opts.maxCoins)}).`);
24472
24407
  return;
24473
24408
  }
24474
24409
  if (!opts.benchPrompt?.trim()) {
24475
- fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-credits <n> --user-approved.');
24410
+ fail5("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-coins <n> --user-approved.');
24476
24411
  return;
24477
24412
  }
24478
24413
  if (opts.jsonOutput && opts.textOutput) {
@@ -24564,10 +24499,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
24564
24499
  models: null,
24565
24500
  externalProviders: null,
24566
24501
  recommendedDeclaredHeadroomBps: null,
24567
- currency: null,
24568
- feeBps: null,
24569
- usdCentsPerCredit: null,
24570
- standingCreditGrant: null,
24571
24502
  total: null,
24572
24503
  featuredCount: null,
24573
24504
  // The stand was never asked, so the verdict is unknown — but the key is
@@ -24593,12 +24524,6 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
24593
24524
  models: lane.models,
24594
24525
  externalProviders: lane.externalProviders,
24595
24526
  recommendedDeclaredHeadroomBps: lane.recommendedDeclaredHeadroomBps,
24596
- // The credit terms a call is billed on, the server's — null on a stand
24597
- // that predates credit billing, never a number the CLI filled in.
24598
- currency: lane.currency,
24599
- feeBps: lane.feeBps,
24600
- usdCentsPerCredit: lane.usdCentsPerCredit,
24601
- standingCreditGrant: lane.standingCreditGrant,
24602
24527
  total: lane.total ?? lane.models?.length ?? null,
24603
24528
  featuredCount: lane.featuredCount ?? (lane.models ? lane.models.filter((m) => m.featured).length : null),
24604
24529
  // `null` when the lane is not live or the server predates the probe.
@@ -24645,9 +24570,8 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
24645
24570
  );
24646
24571
  for (const m of shown) {
24647
24572
  const plan = m.personalPlan ? ` \xB7 personal plan: ${m.personalPlan}` : "";
24648
- const kind = m.kind === "typesafe" ? " \xB7 judge (classifier): judge() under a budget, not generate() or bench" : "";
24649
24573
  log.plain(
24650
- ` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}${kind}`
24574
+ ` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens${plan}`
24651
24575
  );
24652
24576
  }
24653
24577
  if (hiddenCount > 0) {
@@ -24663,104 +24587,74 @@ async function reportModels(apiUrl, token, opts, log, toolsOnly = false) {
24663
24587
  }
24664
24588
  log.plain("");
24665
24589
  if (lane.recommendedDeclaredHeadroomBps === null) {
24666
- log.dim(" This stand serves no recommended headroom, so no ceiling can be recommended from a bench.");
24590
+ log.dim(" This stand serves no recommended headroom, so no price can be recommended from a bench.");
24667
24591
  } else {
24668
- log.dim(` Recommended headroom over a benchmarked call's cost on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
24669
- }
24670
- if (lane.feeBps === null) {
24671
- log.dim(" This stand serves no platform fee \u2014 it predates credit billing, so no ceiling can be recommended from a bench.");
24672
- } else {
24673
- log.dim(` In-game calls are billed in the player's credits, as used: the provider's cost plus this stand's platform fee (${lane.feeBps} bps).`);
24592
+ log.dim(` Recommended headroom over benchmarked coins on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
24674
24593
  }
24675
24594
  if (toolsOnly) {
24676
24595
  printConvertHint();
24677
24596
  return;
24678
24597
  }
24679
- log.dim(" Those per-million rates are the PROVIDER's list; what one call of YOUR prompt really costs,");
24680
- log.dim(` and the per-call ceiling to declare, only a real call measures: ${c.cyan('genex llm bench "<prompt>" --max-credits <n> --user-approved')}`);
24598
+ log.dim(" Those per-million rates are the PROVIDER's; what your game is charged is the platform");
24599
+ log.dim(` tariff on top, which only a real call reveals: ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')}`);
24681
24600
  }
24682
24601
  function wholeOrNull(value) {
24683
24602
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
24684
24603
  }
24685
- function costPicosOf(usage) {
24686
- if (typeof usage?.costUsdPicos === "string" && /^\d+$/.test(usage.costUsdPicos)) return usage.costUsdPicos;
24687
- if (typeof usage?.costUsd === "number" && Number.isFinite(usage.costUsd) && usage.costUsd >= 0) {
24688
- return String(BigInt(Math.round(usage.costUsd * 1e12)));
24689
- }
24690
- return null;
24691
- }
24692
24604
  function sampleFrom(id, settled) {
24693
24605
  const usage = settled?.usage;
24694
- const picos = costPicosOf(usage);
24695
24606
  return {
24696
24607
  id,
24697
24608
  status: settled?.status ?? null,
24698
24609
  billingStatus: settled?.billingStatus ?? null,
24699
- chargedCredits: wholeOrNull(settled?.chargedCredits),
24700
- costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : picos !== null ? Number(picos) / 1e12 : null,
24701
- costUsdPicos: picos,
24610
+ chargedCoins: typeof settled?.chargedCoins === "number" ? settled.chargedCoins : null,
24611
+ costUsd: typeof usage?.costUsd === "number" ? usage.costUsd : null,
24702
24612
  error: settled?.error ?? (settled ? null : "timed_out"),
24703
24613
  providerMessage: typeof settled?.providerMessage === "string" && settled.providerMessage.trim() ? settled.providerMessage.trim() : null,
24704
24614
  inputTokens: wholeOrNull(usage?.inputTokens),
24705
24615
  outputTokens: wholeOrNull(usage?.outputTokens),
24706
24616
  maxOutputTokens: wholeOrNull(usage?.maxOutputTokens),
24707
- fitCredits: typeof usage?.fitCredits === "number" && Number.isSafeInteger(usage.fitCredits) && usage.fitCredits >= 1 ? usage.fitCredits : null,
24617
+ fitCoins: typeof usage?.fitCoins === "number" && Number.isSafeInteger(usage.fitCoins) && usage.fitCoins >= 1 ? usage.fitCoins : null,
24708
24618
  fitExceedsCap: typeof usage?.fitExceedsCap === "boolean" ? usage.fitExceedsCap : null,
24709
24619
  truncated: typeof usage?.truncated === "boolean" ? usage.truncated : settled?.error === "provider_token_limit" ? true : null
24710
24620
  };
24711
24621
  }
24712
24622
  var PROVIDER_TOKEN_LIMIT = "provider_token_limit";
24713
- function benchRecommendation(results, terms) {
24714
- const settled = results.filter(
24715
- (r) => r.status === "succeeded" && r.billingStatus === "final" && r.chargedCredits !== null && r.costUsdPicos !== null
24716
- );
24717
- const costs = settled.map((r) => BigInt(r.costUsdPicos));
24718
- const charged = settled.map((r) => r.chargedCredits);
24719
- const fits = settled.map((r) => r.fitCredits).filter((v) => typeof v === "number");
24720
- const costP95 = percentilePicos(costs, 95);
24721
- const costMax = costs.length ? costs.reduce((a, b) => b > a ? b : a) : null;
24623
+ function benchRecommendation(results, headroomBps) {
24624
+ const settled = results.filter((r) => r.status === "succeeded" && r.billingStatus === "final" && typeof r.chargedCoins === "number");
24625
+ const charged = settled.map((r) => r.chargedCoins);
24626
+ const fits = settled.map((r) => r.fitCoins).filter((v) => typeof v === "number");
24627
+ const p95 = percentile2(charged, 95);
24628
+ const max = charged.length ? Math.max(...charged) : null;
24722
24629
  const fitP95 = percentile2(fits, 95);
24723
24630
  const fitMax = fits.length ? Math.max(...fits) : null;
24724
24631
  const exceedsStandCap = results.some((r) => r.fitExceedsCap === true);
24725
24632
  const cutOffSamples = results.filter((r) => r.error === PROVIDER_TOKEN_LIMIT && r.fitExceedsCap !== true).length;
24726
24633
  const noRecommendationReason = exceedsStandCap ? "answer_exceeds_stand_cap" : cutOffSamples > 0 ? "answer_cut_off" : null;
24727
- const { headroomBps, feeBps, usdCentsPerCredit } = terms;
24728
- const missingTerms = headroomBps === null ? "headroom" : feeBps === null || usdCentsPerCredit === null ? "credit_terms" : null;
24729
- const full = headroomBps !== null && feeBps !== null && usdCentsPerCredit !== null ? { headroomBps, feeBps, usdCentsPerCredit } : null;
24730
- const billing = feeBps !== null && usdCentsPerCredit !== null ? { feeBps, usdCentsPerCredit } : null;
24731
- const costBased = full !== null && costP95 !== null ? ceilingCreditsFor(costP95, full) : null;
24732
- const recommended = costBased === null || noRecommendationReason !== null ? null : Math.max(costBased, fitP95 ?? 0);
24733
- const ceiling = recommended === null || costMax === null || full === null ? null : Math.max(ceilingCreditsFor(costMax, full), recommended, fitMax ?? 0);
24734
- const average = billing !== null ? averageCreditsPerCall(costs, billing) : null;
24735
- const estimate = ceiling === null || billing === null ? null : Math.min(ceiling, perCallEstimateCreditsFor(costs, billing) ?? 1);
24634
+ const chargedBased = p95 !== null && headroomBps !== null ? recommendEstimateCoins(p95, headroomBps) : null;
24635
+ const recommended = chargedBased === null || noRecommendationReason !== null ? null : Math.max(chargedBased, fitP95 ?? 0);
24636
+ const ceiling = recommended === null || p95 === null || max === null || headroomBps === null ? null : Math.max(recommendPerCallMaxCoins(p95, max, headroomBps), recommended, fitMax ?? 0);
24736
24637
  return {
24737
- costs,
24738
24638
  charged,
24739
- costP50: percentilePicos(costs, 50),
24740
- costP95,
24741
- costMax,
24742
- chargedP50: percentile2(charged, 50),
24743
- chargedP95: percentile2(charged, 95),
24744
- chargedMax: charged.length ? Math.max(...charged) : null,
24639
+ p50: percentile2(charged, 50),
24640
+ p95,
24641
+ max,
24745
24642
  fits,
24746
24643
  fitP95,
24747
24644
  fitMax,
24748
- costBasedMaxCredits: costBased,
24749
- recommendedMaxCredits: recommended,
24750
- recommendedPerCallMaxCredits: ceiling,
24751
- averageCreditsPerCall: average,
24752
- recommendedPerCallEstimateCredits: estimate,
24753
- lengthRaisedCeiling: recommended !== null && costBased !== null && recommended > costBased,
24645
+ chargedBasedEstimateCoins: chargedBased,
24646
+ recommendedEstimateCoins: recommended,
24647
+ recommendedPerCallMaxCoins: ceiling,
24648
+ lengthRaisedPrice: recommended !== null && chargedBased !== null && recommended > chargedBased,
24754
24649
  cutOffSamples,
24755
24650
  exceedsStandCap,
24756
- noRecommendationReason,
24757
- missingTerms
24651
+ noRecommendationReason
24758
24652
  };
24759
24653
  }
24760
- var LLM_LENGTH_RAISED_LINE = "The ceiling also decides how long the answer may be: at the cost-based ceiling the answer would be cut off, so declare this one.";
24761
- var LLM_EXCEEDS_STAND_CAP_LINE = "No ceiling recommended: this answer is longer than one call on this stand may produce. Ask for a shorter answer \u2014 fewer fields, shorter strings, a length the prompt states \u2014 and benchmark again.";
24762
- function benchCutOffLine(cutOff, maxCredits) {
24763
- return `No ceiling recommended: ${cutOff} sample${cutOff === 1 ? " was" : "s were"} cut off at this ceiling (--max-credits ${maxCredits}) before the answer was finished, so its real length is unknown. Re-run with a higher --max-credits.`;
24654
+ 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.";
24655
+ 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.";
24656
+ function benchCutOffLine(cutOff, maxCoins) {
24657
+ 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.`;
24764
24658
  }
24765
24659
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["requires_confirmation", "queued", "dispatching", "awaiting_external"]);
24766
24660
  function rowState(row) {
@@ -24771,22 +24665,7 @@ function rowState(row) {
24771
24665
  function rowHoldsSlot(row) {
24772
24666
  if (typeof row.slotHeld === "boolean") return row.slotHeld;
24773
24667
  const state = rowState(row);
24774
- return state === "active" || state === "awaiting bill" && ((row.reservedCoins ?? 0) > 0 || (row.reservedCredits ?? 0) > 0);
24775
- }
24776
- function rowMoney(row) {
24777
- const credits = row.currency === "credits" || row.currency === void 0 && typeof row.reservedCredits === "number" && typeof row.reservedCoins !== "number";
24778
- if (credits) {
24779
- return {
24780
- unit: "credits",
24781
- reserved: typeof row.reservedCredits === "number" ? row.reservedCredits : null,
24782
- charged: typeof row.chargedCredits === "number" ? row.chargedCredits : null
24783
- };
24784
- }
24785
- return {
24786
- unit: "coin",
24787
- reserved: typeof row.reservedCoins === "number" ? row.reservedCoins : null,
24788
- charged: typeof row.chargedCoins === "number" ? row.chargedCoins : null
24789
- };
24668
+ return state === "active" || state === "awaiting bill" && (row.reservedCoins ?? 0) > 0;
24790
24669
  }
24791
24670
  function providerRefusalStatus(error) {
24792
24671
  const m = /^provider_http_(\d{3})$/.exec(error ?? "");
@@ -24805,7 +24684,7 @@ function formatAge(iso, now = Date.now()) {
24805
24684
  }
24806
24685
  async function runBench(args) {
24807
24686
  const { apiUrl, token, projectId, cwd, opts, log } = args;
24808
- const maxCredits = opts.maxCredits ?? opts.maxCoins;
24687
+ const maxCoins = opts.maxCoins;
24809
24688
  const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
24810
24689
  const prompt = opts.benchPrompt.trim();
24811
24690
  const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
@@ -24827,7 +24706,7 @@ async function runBench(args) {
24827
24706
  const lane = await readRuntimeLane(apiUrl, token);
24828
24707
  if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
24829
24708
  if (lane.state === "off") {
24830
- if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts), status: "off", error: null });
24709
+ if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts), status: "off", error: null });
24831
24710
  else {
24832
24711
  log.plain(LLM_LANE_OFF_LINE);
24833
24712
  log.dim(" Nothing was spent.");
@@ -24854,27 +24733,17 @@ async function runBench(args) {
24854
24733
  );
24855
24734
  return;
24856
24735
  }
24857
- const walletBefore = await fetchCreditsSnapshot(apiUrl, token);
24858
- if (walletBefore?.emailVerified === false) {
24859
- benchFailed(opts, log, LLM_BENCH_UNVERIFIED_LINE);
24860
- return;
24861
- }
24862
- if (walletBefore !== null && walletBefore.spendable < maxCredits) {
24863
- benchFailed(opts, log, benchBalanceShortLine(walletBefore.spendable, maxCredits));
24864
- return;
24865
- }
24866
- const balanceBefore = walletBefore?.spendable ?? null;
24736
+ const balanceBefore = await readCoinBalance(apiUrl, token);
24867
24737
  if (!opts.json) {
24868
24738
  log.plain(c.bold("genex llm bench"));
24869
24739
  log.dim(` ${apiUrl}`);
24870
24740
  log.plain("");
24871
24741
  log.plain(` Model ${c.cyan(modelId)}`);
24872
24742
  log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
24873
- log.plain(` Approved ${maxCredits} credit${maxCredits === 1 ? "" : "s"} per attempt \u2014 at worst ${maxCredits * samples} credits for this run, from your own balance`);
24743
+ log.plain(` Approved ${maxCoins} coin per attempt \u2014 at worst ${maxCoins * samples} coin for this run`);
24874
24744
  log.plain(
24875
- ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} credits spendable`}`
24745
+ ` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} coin spendable`}`
24876
24746
  );
24877
- log.dim(" Billed as used: each attempt is charged its real cost plus the platform fee, rounded up to a whole credit.");
24878
24747
  log.plain("");
24879
24748
  }
24880
24749
  const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
@@ -24900,7 +24769,7 @@ async function runBench(args) {
24900
24769
  const res = await call(base, {
24901
24770
  method: "POST",
24902
24771
  body: JSON.stringify({
24903
- maxCredits,
24772
+ maxCoins,
24904
24773
  request: {
24905
24774
  idempotencyKey: `bench-${randomUUID2()}`,
24906
24775
  modelId,
@@ -24930,14 +24799,13 @@ async function runBench(args) {
24930
24799
  if (refusedAt !== null) providerRefused++;
24931
24800
  if (!opts.json) {
24932
24801
  const length = row.outputTokens !== null ? ` \xB7 ${row.outputTokens}${row.maxOutputTokens !== null ? ` of ${row.maxOutputTokens}` : ""} tokens out` : "";
24933
- const charged = row.chargedCredits === null ? "\u2014" : String(row.chargedCredits);
24934
24802
  log.plain(
24935
- ` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${charged.padStart(4)} credit${row.chargedCredits === 1 ? "" : "s"} charged \xB7 cost ${usdFromPicos(row.costUsdPicos)}${length}${row.error ? ` \xB7 ${row.error}` : ""}`
24803
+ ` ${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}` : ""}`
24936
24804
  );
24937
24805
  if (refusedAt !== null) printProviderRefusal(log, refusedAt, row.providerMessage, row.status === "unknown");
24938
24806
  if (row.error === PROVIDER_TOKEN_LIMIT) {
24939
24807
  log.dim(
24940
- row.fitExceedsCap === true ? " The answer was cut off at this stand's own output limit \u2014 no ceiling makes room for it; it is not a sample." : ` The answer was cut off at this ceiling (--max-credits ${maxCredits}) before it was finished \u2014 it was charged, and it is not a sample.`
24808
+ 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.`
24941
24809
  );
24942
24810
  }
24943
24811
  }
@@ -24945,19 +24813,16 @@ async function runBench(args) {
24945
24813
  } finally {
24946
24814
  process.removeListener("SIGINT", onSigint);
24947
24815
  }
24948
- const terms = {
24949
- headroomBps: lane.recommendedDeclaredHeadroomBps,
24950
- feeBps: lane.feeBps,
24951
- usdCentsPerCredit: lane.usdCentsPerCredit
24952
- };
24953
- const rec = benchRecommendation(results, terms);
24954
- const settledCount = rec.charged.length;
24816
+ const headroomBps = lane.recommendedDeclaredHeadroomBps;
24817
+ const rec = benchRecommendation(results, headroomBps);
24818
+ const { charged, p50, p95, max } = rec;
24819
+ const settledCount = charged.length;
24820
+ const recommended = rec.recommendedEstimateCoins;
24821
+ const ceiling = rec.recommendedPerCallMaxCoins;
24955
24822
  const lengthBlocked = rec.noRecommendationReason !== null;
24956
- const balanceAfter = (await fetchCreditsSnapshot(apiUrl, token))?.spendable ?? null;
24957
- const picosOrNull = (v) => v === null ? null : String(v);
24823
+ const balanceAfter = await readCoinBalance(apiUrl, token);
24958
24824
  const record = {
24959
- v: 2,
24960
- currency: "credits",
24825
+ v: 1,
24961
24826
  ranAt: (/* @__PURE__ */ new Date()).toISOString(),
24962
24827
  apiUrl,
24963
24828
  projectId,
@@ -24966,34 +24831,28 @@ async function runBench(args) {
24966
24831
  outputFormat,
24967
24832
  samples,
24968
24833
  completed: settledCount,
24969
- maxCreditsPerSample: maxCredits,
24970
- costUsdPicos: rec.costs.map(String),
24971
- chargedCredits: rec.charged,
24972
- cost: { p50: picosOrNull(rec.costP50), p95: picosOrNull(rec.costP95), max: picosOrNull(rec.costMax) },
24973
- charged: { p50: rec.chargedP50, p95: rec.chargedP95, max: rec.chargedMax },
24974
- fitCredits: rec.fits,
24834
+ chargedCoins: charged,
24835
+ p50,
24836
+ p95,
24837
+ max,
24838
+ recommendedDeclaredHeadroomBps: headroomBps,
24839
+ recommendedEstimateCoins: recommended,
24840
+ recommendedPerCallMaxCoins: ceiling,
24841
+ maxCoinsPerSample: maxCoins,
24842
+ fitCoins: rec.fits,
24975
24843
  fitP95: rec.fitP95,
24976
- fitMax: rec.fitMax,
24977
- recommendedDeclaredHeadroomBps: terms.headroomBps,
24978
- feeBps: terms.feeBps,
24979
- usdCentsPerCredit: terms.usdCentsPerCredit,
24980
- costBasedMaxCredits: rec.costBasedMaxCredits,
24981
- recommendedMaxCredits: rec.recommendedMaxCredits,
24982
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
24983
- averageCreditsPerCall: rec.averageCreditsPerCall,
24984
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
24985
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
24844
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
24845
+ lengthRaisedPrice: rec.lengthRaisedPrice,
24986
24846
  cutOffSamples: rec.cutOffSamples,
24987
24847
  noRecommendationReason: rec.noRecommendationReason
24988
24848
  };
24989
24849
  const savedTo = await saveBench(cwd, record);
24990
24850
  if (opts.json) {
24991
- const ok = settledCount > 0 && !lengthBlocked;
24851
+ const ok = charged.length > 0 && !lengthBlocked;
24992
24852
  writeJsonLine({
24993
24853
  command: "llm bench",
24994
24854
  status: ok ? "ok" : "failed",
24995
24855
  error: ok ? null : rec.noRecommendationReason ?? "no_sample_settled",
24996
- currency: "credits",
24997
24856
  apiUrl,
24998
24857
  projectId,
24999
24858
  modelId,
@@ -25002,54 +24861,35 @@ async function runBench(args) {
25002
24861
  schemaPath: opts.schemaPath ?? null,
25003
24862
  samples,
25004
24863
  completed: settledCount,
25005
- maxCreditsPerSample: maxCredits,
25006
- // Spendable credits before and after the run; null when unreadable.
24864
+ maxCoinsPerSample: maxCoins,
25007
24865
  balanceBefore,
25008
24866
  balanceAfter,
25009
24867
  results,
25010
24868
  // The create-time refusal that ended the run, or null. Beside the
25011
24869
  // samples, never among them: no attempt existed and nothing was spent.
25012
24870
  refusal: refusal2,
25013
- // Over the settled samples: the real provider cost (pico-dollar strings
25014
- // and USD) and the whole credits charged.
25015
- costUsdPicos: record.cost,
25016
- costUsd: {
25017
- p50: rec.costP50 === null ? null : Number(rec.costP50) / 1e12,
25018
- p95: rec.costP95 === null ? null : Number(rec.costP95) / 1e12,
25019
- max: rec.costMax === null ? null : Number(rec.costMax) / 1e12
25020
- },
25021
- chargedCredits: record.charged,
25022
- // The answer's LENGTH, priced by the server per sample (`fitCredits`,
24871
+ chargedCoins: { p50, p95, max },
24872
+ // The answer's LENGTH, priced by the server per sample (`fitCoins`,
25023
24873
  // headroom already on the tokens), over the settled samples.
25024
- fitCredits: { p95: rec.fitP95, max: rec.fitMax },
25025
- recommendedDeclaredHeadroomBps: terms.headroomBps,
25026
- feeBps: terms.feeBps,
25027
- usdCentsPerCredit: terms.usdCentsPerCredit,
25028
- missingTerms: rec.missingTerms,
25029
- costBasedMaxCredits: rec.costBasedMaxCredits,
25030
- lengthRaisedCeiling: rec.lengthRaisedCeiling,
24874
+ fitCoins: { p95: rec.fitP95, max: rec.fitMax },
24875
+ chargedBasedEstimateCoins: rec.chargedBasedEstimateCoins,
24876
+ lengthRaisedPrice: rec.lengthRaisedPrice,
25031
24877
  cutOffSamples: rec.cutOffSamples,
25032
24878
  exceedsStandCap: rec.exceedsStandCap,
25033
24879
  noRecommendationReason: rec.noRecommendationReason,
25034
- recommendedMaxCredits: rec.recommendedMaxCredits,
25035
- recommendedPerCallMaxCredits: rec.recommendedPerCallMaxCredits,
25036
- averageCreditsPerCall: rec.averageCreditsPerCall,
25037
- recommendedPerCallEstimateCredits: rec.recommendedPerCallEstimateCredits,
24880
+ recommendedDeclaredHeadroomBps: headroomBps,
24881
+ recommendedEstimateCoins: recommended,
24882
+ recommendedPerCallMaxCoins: ceiling,
25038
24883
  savedTo
25039
24884
  });
25040
24885
  if (!ok) process.exitCode = 1;
25041
24886
  return;
25042
24887
  }
25043
24888
  log.plain("");
25044
- const printMeasured = () => {
25045
- log.plain(c.bold(" Real cost per call"));
25046
- log.plain(` p50 ${usdFromPicos(rec.costP50)} p95 ${usdFromPicos(rec.costP95)} max ${usdFromPicos(rec.costMax)} (${settledCount} of ${samples} settled)`);
25047
- log.plain(c.bold(" Charged credits"));
25048
- log.plain(` p50 ${rec.chargedP50} p95 ${rec.chargedP95} max ${rec.chargedMax} (whole credits, rounded up per call)`);
25049
- };
25050
24889
  if (lengthBlocked) {
25051
- if (settledCount > 0) {
25052
- printMeasured();
24890
+ if (charged.length > 0) {
24891
+ log.plain(c.bold(" Charged coins"));
24892
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
25053
24893
  log.plain("");
25054
24894
  }
25055
24895
  printRecommendation(log, record);
@@ -25057,16 +24897,17 @@ async function runBench(args) {
25057
24897
  process.exitCode = 1;
25058
24898
  return;
25059
24899
  }
25060
- if (settledCount === 0) {
24900
+ if (charged.length === 0) {
25061
24901
  log.error(
25062
- providerRefused > 0 && providerRefused === results.length ? " No sample ran \u2014 the provider refused every attempt at its door \u2014 so there is nothing to measure from." : " No sample settled, so there is nothing to measure from."
24902
+ 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."
25063
24903
  );
25064
24904
  process.exitCode = 1;
25065
24905
  return;
25066
24906
  }
25067
- printMeasured();
24907
+ log.plain(c.bold(" Charged coins"));
24908
+ log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
25068
24909
  if (balanceBefore !== null && balanceAfter !== null) {
25069
- log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} credits spendable`);
24910
+ log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} coin spendable`);
25070
24911
  }
25071
24912
  log.plain("");
25072
24913
  printRecommendation(log, record);
@@ -25078,58 +24919,43 @@ function printRecommendation(log, record) {
25078
24919
  return;
25079
24920
  }
25080
24921
  if (record.noRecommendationReason === "answer_cut_off") {
25081
- log.warn(` ${benchCutOffLine(record.cutOffSamples || 1, record.maxCreditsPerSample)}`);
24922
+ log.warn(` ${benchCutOffLine(record.cutOffSamples ?? 1, record.maxCoinsPerSample ?? 0)}`);
25082
24923
  return;
25083
24924
  }
25084
- const attempts = `${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}`;
25085
- if (record.recommendedMaxCredits === null || record.cost.p95 === null) {
25086
- log.warn(" No ceiling recommended.");
24925
+ if (record.recommendedEstimateCoins === null || record.p95 === null) {
24926
+ log.warn(" No price recommended.");
25087
24927
  log.dim(
25088
- record.cost.p95 === null ? " No sample settled, so there is no p95 to build a ceiling on \u2014" : record.recommendedDeclaredHeadroomBps === null ? " This stand served no recommended headroom, and the multiplier is the server's to set \u2014" : " This stand served no platform fee or credit value, and those are the server's to set \u2014"
24928
+ 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"
25089
24929
  );
25090
24930
  log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
25091
- printAverage(log, record, attempts);
25092
24931
  return;
25093
24932
  }
25094
- log.plain(c.bold(` Declare maxCredits: ${record.recommendedMaxCredits}`));
25095
- if (record.lengthRaisedCeiling) {
24933
+ log.plain(c.bold(` Declare estimateCoins: ${record.recommendedEstimateCoins}`));
24934
+ if (record.lengthRaisedPrice) {
25096
24935
  log.plain(` ${LLM_LENGTH_RAISED_LINE}`);
25097
24936
  log.dim(
25098
- ` = the smallest ceiling whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
24937
+ ` = the smallest price whose answer allowance holds the answer plus this stand's headroom (p95 ${record.fitP95 ?? "\u2014"}), above the`
25099
24938
  );
25100
24939
  log.dim(
25101
- ` cost-based ${record.costBasedMaxCredits ?? "\u2014"} (p95 real cost ${usdFromPicos(record.cost.p95)} with the platform fee, ${record.feeBps} bps, and ${record.recommendedDeclaredHeadroomBps} bps of headroom), over ${attempts}.`
24940
+ ` 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}.`
25102
24941
  );
25103
24942
  } else {
25104
24943
  log.dim(
25105
- ` = p95 real cost (${usdFromPicos(record.cost.p95)}) with this stand's platform fee (${record.feeBps} bps) and recommended headroom (${record.recommendedDeclaredHeadroomBps} bps), in whole credits,`
24944
+ ` = p95 of charged coins (${record.p95}) plus this stand's recommended headroom (${record.recommendedDeclaredHeadroomBps} bps),`
25106
24945
  );
25107
- log.dim(` measured over ${attempts}.`);
24946
+ log.dim(` measured over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`);
25108
24947
  }
25109
- log.dim(" That number is a CEILING, not a price: a call is billed its real cost as used, never more than it \u2014");
25110
- log.dim(" and it also sizes how long the answer may be.");
25111
- if (record.recommendedPerCallMaxCredits !== null) {
25112
- log.plain(c.bold(` Grant perCallMaxCredits: ${record.recommendedPerCallMaxCredits}`));
24948
+ log.dim(" That number is a PRICE: a started attempt is charged in full, including one that fails.");
24949
+ if (record.recommendedPerCallMaxCoins != null) {
24950
+ log.plain(c.bold(` Grant perCallMaxCoins: ${record.recommendedPerCallMaxCoins}`));
25113
24951
  log.dim(
25114
- record.lengthRaisedCeiling ? ` = the same over the worst sample (${usdFromPicos(record.cost.max)}) or the longest answer's ceiling, whichever is larger, and never below the ceiling above it.` : ` = the same over the worst sample (${usdFromPicos(record.cost.max)}), and never below the ceiling above it.`
24952
+ 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.`
25115
24953
  );
25116
- }
25117
- printAverage(log, record, attempts);
25118
- }
25119
- function printAverage(log, record, attempts) {
25120
- if (record.averageCreditsPerCall === null) return;
25121
- log.plain(` Per-call estimate: about ${formatCredits(record.averageCreditsPerCall)} credits per call`);
25122
- log.dim(` = the average real cost with the platform fee, no headroom, over ${attempts}.`);
25123
- if (record.recommendedPerCallEstimateCredits !== null) {
25124
- log.plain(c.bold(` Grant perCallEstimateCredits: ${record.recommendedPerCallEstimateCredits}`));
25125
- log.dim(` = that average rounded up to a whole credit. disclosure.estimatedCreditsPerPeriod = your calls per period \xD7 ${formatCredits(record.averageCreditsPerCall)}, rounded up.`);
24954
+ log.dim(" The ceiling is the price's room to be wrong; declaring one under the price is refused.");
25126
24955
  }
25127
24956
  }
25128
24957
  async function explainBenchRefusal(res, call, base, log, opts, index, settledSoFar) {
25129
- if (printedStructuredError(res)) {
25130
- const printed = await res.json().catch(() => ({}));
25131
- return { stop: true, refusal: { status: res.status, error: typeof printed.error === "string" ? printed.error : null, slotsHeld: null, slotLimit: null } };
25132
- }
24958
+ if (printedStructuredError(res)) return { stop: true, refusal: { status: res.status, error: null, slotsHeld: null, slotLimit: null } };
25133
24959
  const body = await res.json().catch(() => ({}));
25134
24960
  const refusal2 = { status: res.status, error: body.error ?? null, slotsHeld: null, slotLimit: null };
25135
24961
  if (res.status === 429 && body.error === "generation_limit") {
@@ -25139,10 +24965,6 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
25139
24965
  if (!opts.json) log.error(` Sample ${index + 1} ${benchSlotsHeldSentence(counts)}`);
25140
24966
  return { stop: true, refusal: refusal2 };
25141
24967
  }
25142
- if (res.status === 403 && body.error === "credits_unverified") {
25143
- if (!opts.json) log.error(` ${LLM_BENCH_UNVERIFIED_LINE}`);
25144
- return { stop: true, refusal: refusal2 };
25145
- }
25146
24968
  if (opts.json) return { stop: res.status !== 429, refusal: refusal2 };
25147
24969
  if (res.status === 404 && body.error === "not_found") {
25148
24970
  log.error(` ${LLM_LANE_OFF_LINE}`);
@@ -25158,7 +24980,7 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
25158
24980
  return { stop: true, refusal: refusal2 };
25159
24981
  }
25160
24982
  if (res.status === 402) {
25161
- log.error(" Not enough credits to start the attempt \u2014 a bench is paid from your own balance.");
24983
+ log.error(" Not enough coin to start the attempt.");
25162
24984
  return { stop: true, refusal: refusal2 };
25163
24985
  }
25164
24986
  log.error(
@@ -25167,7 +24989,7 @@ async function explainBenchRefusal(res, call, base, log, opts, index, settledSoF
25167
24989
  return { stop: res.status >= 500 || res.status === 401 || res.status === 403, refusal: refusal2 };
25168
24990
  }
25169
24991
  function printProviderRefusal(log, status2, message, awaitingBill = false) {
25170
- if (awaitingBill) log.dim(` The provider answered ${status2} after routing \u2014 its bill is not final, so what this call reserved stays held until it resolves (see \`genex llm status\`); it is not a sample.`);
24992
+ 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.`);
25171
24993
  else log.dim(" The provider refused this call at its door \u2014 no inference ran, it cost nothing, and it is not a sample.");
25172
24994
  if (message) log.dim(` Provider said: ${message}`);
25173
24995
  if (status2 === 401 || status2 === 403) {
@@ -25193,6 +25015,19 @@ async function pollSettled(call, url, timeoutSec) {
25193
25015
  }
25194
25016
  return last;
25195
25017
  }
25018
+ async function readCoinBalance(apiUrl, token) {
25019
+ try {
25020
+ const res = await apiFetch(`${apiUrl}/api/coin/balance`, {
25021
+ headers: { Authorization: `Bearer ${token}` },
25022
+ signal: AbortSignal.timeout(6e3)
25023
+ });
25024
+ if (!res.ok) return null;
25025
+ const body = await res.json().catch(() => null);
25026
+ return typeof body?.spendable === "number" ? body.spendable : null;
25027
+ } catch {
25028
+ return null;
25029
+ }
25030
+ }
25196
25031
  async function saveBench(cwd, record) {
25197
25032
  try {
25198
25033
  const file = path36.join(cwd, LLM_BENCH_FILE);
@@ -25208,10 +25043,9 @@ function benchFailed(opts, log, message) {
25208
25043
  else log.error(message);
25209
25044
  process.exitCode = 1;
25210
25045
  }
25211
- function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCredits, opts) {
25046
+ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts) {
25212
25047
  return {
25213
25048
  command: "llm bench",
25214
- currency: "credits",
25215
25049
  apiUrl,
25216
25050
  projectId,
25217
25051
  modelId: opts.modelId ?? null,
@@ -25220,28 +25054,21 @@ function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCre
25220
25054
  schemaPath: opts.schemaPath ?? null,
25221
25055
  samples,
25222
25056
  completed: 0,
25223
- maxCreditsPerSample: maxCredits,
25057
+ maxCoinsPerSample: maxCoins,
25224
25058
  balanceBefore: null,
25225
25059
  balanceAfter: null,
25226
25060
  results: [],
25227
25061
  refusal: null,
25228
- costUsdPicos: { p50: null, p95: null, max: null },
25229
- costUsd: { p50: null, p95: null, max: null },
25230
- chargedCredits: { p50: null, p95: null, max: null },
25231
- fitCredits: { p95: null, max: null },
25232
- recommendedDeclaredHeadroomBps: null,
25233
- feeBps: null,
25234
- usdCentsPerCredit: null,
25235
- missingTerms: null,
25236
- costBasedMaxCredits: null,
25237
- lengthRaisedCeiling: false,
25062
+ chargedCoins: { p50: null, p95: null, max: null },
25063
+ fitCoins: { p95: null, max: null },
25064
+ chargedBasedEstimateCoins: null,
25065
+ lengthRaisedPrice: false,
25238
25066
  cutOffSamples: 0,
25239
25067
  exceedsStandCap: false,
25240
25068
  noRecommendationReason: null,
25241
- recommendedMaxCredits: null,
25242
- recommendedPerCallMaxCredits: null,
25243
- averageCreditsPerCall: null,
25244
- recommendedPerCallEstimateCredits: null,
25069
+ recommendedDeclaredHeadroomBps: null,
25070
+ recommendedEstimateCoins: null,
25071
+ recommendedPerCallMaxCoins: null,
25245
25072
  savedTo: null
25246
25073
  };
25247
25074
  }
@@ -25304,8 +25131,7 @@ async function reportStatus(args) {
25304
25131
  for (const row of rows) {
25305
25132
  const state = rowState(row);
25306
25133
  const mark = state === "active" ? c.cyan("\u25CF") : state === "awaiting bill" ? c.yellow("\u25D0") : c.green("\u25CB");
25307
- const money2 = rowMoney(row);
25308
- const reserved = money2.reserved === null ? "reserved \u2014" : `${money2.reserved} ${money2.unit} reserved`;
25134
+ const reserved = typeof row.reservedCoins === "number" ? `${row.reservedCoins} coin reserved` : "reserved \u2014";
25309
25135
  log.plain(
25310
25136
  ` ${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)")}`
25311
25137
  );
@@ -25377,11 +25203,10 @@ async function cancelCall(args) {
25377
25203
  if (!opts.json) {
25378
25204
  log.warn(` ${row.id} (${row.status ?? "?"}) \u2014 ${sentence}.`);
25379
25205
  if (state === "awaiting bill") {
25380
- log.dim(` Nothing to cancel: what it reserved releases when the bill resolves, and it stops holding a slot ${PENDING_SLOT_RELEASE_MINUTES} minutes after dispatch.`);
25206
+ 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.`);
25381
25207
  if (row.providerMessage?.trim()) log.dim(` Provider said: ${row.providerMessage.trim()}`);
25382
25208
  } else {
25383
- const money2 = rowMoney(row);
25384
- log.dim(` ${money2.charged === null ? "charge unknown" : `${money2.charged} ${money2.unit} charged`}${row.error ? ` \xB7 ${row.error}` : ""}.`);
25209
+ log.dim(` ${typeof row.chargedCoins === "number" ? `${row.chargedCoins} coin charged` : "charge unknown"}${row.error ? ` \xB7 ${row.error}` : ""}.`);
25385
25210
  }
25386
25211
  }
25387
25212
  return done("ok", null, state === "awaiting bill" ? "awaiting_bill" : "already_final", row);
@@ -25403,126 +25228,78 @@ async function cancelCall(args) {
25403
25228
  const after = await res.json().catch(() => null) ?? row;
25404
25229
  if (!opts.json) {
25405
25230
  log.success(` Cancelled ${row.id} (was ${row.status ?? "active"}).`);
25406
- log.dim(` What it reserved releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
25231
+ log.dim(` Its coin releases once the bill settles; ${c.cyan("genex llm status")} shows it until then.`);
25407
25232
  }
25408
25233
  return done("ok", null, "cancelled", after);
25409
25234
  }
25410
- async function readSavedBench(cwd) {
25411
- let parsed;
25235
+ async function reportSavedPrice(cwd, opts, log) {
25236
+ let record = null;
25412
25237
  try {
25413
- parsed = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
25238
+ record = JSON.parse(await fs35.readFile(path36.join(cwd, LLM_BENCH_FILE), "utf8"));
25414
25239
  } catch {
25415
- return null;
25240
+ record = null;
25416
25241
  }
25417
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
25418
- const v = parsed;
25419
- if (v.v === 2 && v.currency === "credits" && v.cost && v.charged) return { kind: "credits", record: parsed };
25420
- return { kind: "coins", record: parsed };
25421
- }
25422
- var LLM_PRICE_COIN_TERMS_LINE = "This bench was measured under the old COIN terms, when a game declared a fixed price per call. In-game calls are now billed in the player's credits, as used, and a game declares a per-call CEILING (maxCredits) instead \u2014 a coin price is not that number. Re-run the bench for the figures to declare.";
25423
- async function reportSavedPrice(cwd, opts, log) {
25424
- const saved = await readSavedBench(cwd);
25425
- const rerun = 'genex llm bench "<prompt>" --max-credits <n> --user-approved';
25426
- const creditKeys = (record2) => ({
25427
- costUsdPicos: record2?.cost ?? { p50: null, p95: null, max: null },
25428
- chargedCredits: record2?.charged ?? { p50: null, p95: null, max: null },
25429
- fitP95: record2?.fitP95 ?? null,
25430
- fitMax: record2?.fitMax ?? null,
25431
- recommendedDeclaredHeadroomBps: record2?.recommendedDeclaredHeadroomBps ?? null,
25432
- feeBps: record2?.feeBps ?? null,
25433
- usdCentsPerCredit: record2?.usdCentsPerCredit ?? null,
25434
- costBasedMaxCredits: record2?.costBasedMaxCredits ?? null,
25435
- lengthRaisedCeiling: record2?.lengthRaisedCeiling ?? false,
25436
- cutOffSamples: record2?.cutOffSamples ?? 0,
25437
- noRecommendationReason: record2?.noRecommendationReason ?? null,
25438
- recommendedMaxCredits: record2?.recommendedMaxCredits ?? null,
25439
- recommendedPerCallMaxCredits: record2?.recommendedPerCallMaxCredits ?? null,
25440
- averageCreditsPerCall: record2?.averageCreditsPerCall ?? null,
25441
- recommendedPerCallEstimateCredits: record2?.recommendedPerCallEstimateCredits ?? null
25442
- });
25443
- if (!saved) {
25242
+ if (!record) {
25444
25243
  if (opts.json) {
25445
25244
  writeJsonLine({
25446
25245
  command: "llm price",
25447
25246
  status: "failed",
25448
25247
  error: "no_bench",
25449
- currency: null,
25450
25248
  ranAt: null,
25451
25249
  modelId: null,
25452
25250
  samples: null,
25453
25251
  completed: null,
25454
- ...creditKeys(null),
25455
- legacyCoinTerms: null,
25252
+ p50: null,
25253
+ p95: null,
25254
+ max: null,
25255
+ fitP95: null,
25256
+ chargedBasedEstimateCoins: null,
25257
+ lengthRaisedPrice: false,
25258
+ cutOffSamples: 0,
25259
+ noRecommendationReason: null,
25260
+ recommendedDeclaredHeadroomBps: null,
25261
+ recommendedEstimateCoins: null,
25262
+ recommendedPerCallMaxCoins: null,
25456
25263
  savedTo: null
25457
25264
  });
25458
25265
  } else {
25459
25266
  log.error("Nothing has been benchmarked in this folder yet.");
25460
- log.dim(` Run ${c.cyan(rerun)} first.`);
25461
- }
25462
- process.exitCode = 1;
25463
- return;
25464
- }
25465
- if (saved.kind === "coins") {
25466
- const old = saved.record;
25467
- const legacy = {
25468
- p50: old.p50 ?? null,
25469
- p95: old.p95 ?? null,
25470
- max: old.max ?? null,
25471
- recommendedEstimateCoins: old.recommendedEstimateCoins ?? null,
25472
- recommendedPerCallMaxCoins: old.recommendedPerCallMaxCoins ?? null,
25473
- maxCoinsPerSample: old.maxCoinsPerSample ?? null,
25474
- noRecommendationReason: old.noRecommendationReason ?? null
25475
- };
25476
- if (opts.json) {
25477
- writeJsonLine({
25478
- command: "llm price",
25479
- status: "failed",
25480
- error: "coin_terms",
25481
- currency: "coins",
25482
- ranAt: old.ranAt ?? null,
25483
- modelId: old.modelId ?? null,
25484
- samples: old.samples ?? null,
25485
- completed: old.completed ?? null,
25486
- ...creditKeys(null),
25487
- legacyCoinTerms: legacy,
25488
- savedTo: LLM_BENCH_FILE
25489
- });
25490
- } else {
25491
- log.plain(c.bold("genex llm price"));
25492
- log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${old.ranAt ?? "at an unknown time"}${old.modelId ? ` on ${old.modelId}` : ""}`);
25493
- log.plain("");
25494
- log.warn(` ${LLM_PRICE_COIN_TERMS_LINE}`);
25495
- const measured = legacy.p95 === null ? "no settled sample" : `charged coins p50 ${legacy.p50} \xB7 p95 ${legacy.p95} \xB7 max ${legacy.max}`;
25496
- const recommended = legacy.recommendedEstimateCoins === null ? "no price recommended" : `it recommended estimateCoins ${legacy.recommendedEstimateCoins}${legacy.recommendedPerCallMaxCoins != null ? ` and perCallMaxCoins ${legacy.recommendedPerCallMaxCoins}` : ""}`;
25497
- log.dim(` What it measured then: ${measured}; ${recommended}.`);
25498
- log.dim(` Re-run: ${c.cyan(rerun)}`);
25267
+ log.dim(` Run ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')} first.`);
25499
25268
  }
25500
25269
  process.exitCode = 1;
25501
25270
  return;
25502
25271
  }
25503
- const record = saved.record;
25504
25272
  if (opts.json) {
25505
25273
  writeJsonLine({
25506
25274
  command: "llm price",
25507
- status: record.recommendedMaxCredits === null ? "failed" : "ok",
25508
- error: record.recommendedMaxCredits === null ? record.noRecommendationReason ?? "no_recommendation" : null,
25509
- currency: "credits",
25275
+ status: record.recommendedEstimateCoins === null ? "failed" : "ok",
25276
+ error: record.recommendedEstimateCoins === null ? record.noRecommendationReason ?? "no_recommendation" : null,
25510
25277
  ranAt: record.ranAt ?? null,
25511
25278
  modelId: record.modelId ?? null,
25512
25279
  samples: record.samples ?? null,
25513
25280
  completed: record.completed ?? null,
25514
- ...creditKeys(record),
25515
- legacyCoinTerms: null,
25281
+ p50: record.p50 ?? null,
25282
+ p95: record.p95 ?? null,
25283
+ max: record.max ?? null,
25284
+ // Absent from a file an older CLI wrote; null / false / 0 then, never missing.
25285
+ fitP95: record.fitP95 ?? null,
25286
+ chargedBasedEstimateCoins: record.chargedBasedEstimateCoins ?? null,
25287
+ lengthRaisedPrice: record.lengthRaisedPrice ?? false,
25288
+ cutOffSamples: record.cutOffSamples ?? 0,
25289
+ noRecommendationReason: record.noRecommendationReason ?? null,
25290
+ recommendedDeclaredHeadroomBps: record.recommendedDeclaredHeadroomBps ?? null,
25291
+ recommendedEstimateCoins: record.recommendedEstimateCoins ?? null,
25292
+ recommendedPerCallMaxCoins: record.recommendedPerCallMaxCoins ?? null,
25516
25293
  savedTo: LLM_BENCH_FILE
25517
25294
  });
25518
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
25295
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
25519
25296
  return;
25520
25297
  }
25521
25298
  log.plain(c.bold("genex llm price"));
25522
25299
  log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
25523
25300
  log.plain("");
25524
25301
  printRecommendation(log, record);
25525
- if (record.recommendedMaxCredits === null) process.exitCode = 1;
25302
+ if (record.recommendedEstimateCoins === null) process.exitCode = 1;
25526
25303
  }
25527
25304
  function writeJsonLine(value) {
25528
25305
  process.stdout.write(`${JSON.stringify(value)}
@@ -25782,7 +25559,7 @@ function runtimeRow(state, lane, toolsOnly = false) {
25782
25559
  return {
25783
25560
  label: "Runtime",
25784
25561
  value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}${keyRefused ? ` \xB7 ${c.yellow("provider key refused")}` : ""}`,
25785
- fix: keyRefused ? LLM_PROVIDER_KEY_REJECTED_LINE : toolsOnly ? convertFix : 'Measure a call before a game declares its per-call ceiling: `npx genex llm bench "<prompt>" --max-credits <n> --user-approved`.'
25562
+ 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`.'
25786
25563
  };
25787
25564
  }
25788
25565
  case "off":
@@ -27738,16 +27515,16 @@ ${c.bold("Usage")}
27738
27515
  status | cancel. "models" says whether this
27739
27516
  stand serves the runtime LLM lane at all, and
27740
27517
  at what provider rates. "bench" runs the real
27741
- model on YOUR OWN credits and reports what each
27742
- attempt really cost; it needs --max-credits <n>
27518
+ model on YOUR OWN coin and reports what each
27519
+ attempt was charged; it needs --max-coins <n>
27743
27520
  --user-approved, like any spend the player has
27744
27521
  to agree to. "price" reprints the last run's
27745
- recommended maxCredits. "status" lists this
27522
+ recommended estimateCoins. "status" lists this
27746
27523
  project's open calls and the slots they hold;
27747
27524
  "cancel <id>" stops an active one. Declare a
27748
- game's per-call ceiling from a bench, never
27749
- from a guess: the player is billed as used, and
27750
- the ceiling also sizes how long an answer may be.
27525
+ game's price from a bench, never from a guess:
27526
+ a started attempt is charged in full, including
27527
+ one that fails.
27751
27528
  genex player <sub> [options] Answer the in-game model requests you approve, on your
27752
27529
  OWN Claude or ChatGPT subscription: install | run |
27753
27530
  status | stop | uninstall. "install" signs this machine
@@ -28209,7 +27986,7 @@ ${c.bold("Examples")}
28209
27986
  genex shop remove sku_123
28210
27987
  genex llm models
28211
27988
  genex llm models --all
28212
- genex llm bench "Reply with one short taunt." --samples 5 --max-credits 20 --user-approved
27989
+ genex llm bench "Reply with one short taunt." --samples 5 --max-coins 20 --user-approved
28213
27990
  genex llm price
28214
27991
  genex llm status
28215
27992
  genex llm cancel <id>
@@ -28446,24 +28223,22 @@ ${c.bold("How the allowance works")}
28446
28223
 
28447
28224
  Every number printed is live from your account; prices can change without a deploy.
28448
28225
  `,
28449
- 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?
28226
+ llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what should the game declare?
28450
28227
 
28451
28228
  ${c.bold("Usage")}
28452
28229
  genex llm models [--all] [--json] Which models this stand serves, their provider rates per
28453
- million tokens, the platform fee a call is billed at, and
28454
- the headroom it recommends over a benchmark. The catalog
28455
- is synced from OpenRouter, so the FEATURED short list is
28456
- printed by default and --all prints every row (--json
28457
- always carries every row). Off on this stand: it says so
28458
- and exits 0 \u2014 build the feature without a model rather
28459
- than promising a 404.
28460
- genex llm bench "<prompt>" --max-credits <n> --user-approved [options]
28461
- Run the real model through the development lane and
28462
- report what each attempt really COST and was charged.
28463
- Refused without both flags, before any network call:
28464
- this spends YOUR OWN CREDITS, a spend the build's asset
28465
- allowance does not count. Needs a folder linked to a
28466
- game you own, and a balance that covers one attempt.
28230
+ million tokens, and the headroom it recommends over a
28231
+ benchmark. The catalog is synced from OpenRouter, so the
28232
+ FEATURED short list is printed by default and --all
28233
+ prints every row (--json always carries every row). Off
28234
+ on this stand: it says so and exits 0 \u2014 build the feature
28235
+ without a model rather than promising a 404.
28236
+ genex llm bench "<prompt>" --max-coins <n> --user-approved [options]
28237
+ Run the real model <n> times through the development
28238
+ lane and report what each attempt was CHARGED. Refused
28239
+ without both flags, before any network call: this spends
28240
+ YOUR OWN COIN, a wallet the build's asset allowance
28241
+ cannot see. Needs a folder linked to a game you own.
28467
28242
  genex llm price [--json] The recommendation from the last bench in this folder.
28468
28243
  Reads a file; spends nothing.
28469
28244
  genex llm status [--json] This project's open development calls: active, or
@@ -28480,32 +28255,29 @@ ${c.bold("Bench options")}
28480
28255
  --samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
28481
28256
  --schema <file> A JSON file holding the output schema; implies JSON output.
28482
28257
  --json-output Ask for JSON without a schema. --text Ask for text (the default).
28483
- --max-credits <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own credits,
28258
+ --max-coins <n> The per-attempt ceiling YOU approved \u2014 a bench runs on your own coin,
28484
28259
  never a player's. The run's worst case is that number times --samples,
28485
- and it is printed before anything starts. (--max-coins is its deprecated
28486
- name, read as the same number.)
28260
+ and it is printed before anything starts.
28487
28261
  --timeout <seconds> Bounds the wait for each attempt.
28488
28262
  --json One machine-readable object; every key is always present, null when
28489
28263
  the CLI could not learn it.
28490
28264
 
28491
28265
  ${c.bold("Why a benchmark and not an estimate")}
28492
- An in-game call is paid from the PLAYER's credits and billed as used: its real provider cost
28493
- plus the platform fee, never more than the per-call CEILING the game declares (\`maxCredits\`,
28494
- or \`perCallMaxCredits\` on a standing budget). The ceiling is not a price, but it is still a
28495
- number with two jobs: it bounds what one call may cost, and it funds how LONG the answer may
28496
- be. Three layers sit between the provider's rate card and it \u2014 the provider's cost for YOUR
28497
- prompt, the platform fee, and your headroom \u2014 and only a real call measures the first. The
28498
- bench is billed to YOU, on the development lane; the player-funded lane is what the
28499
- published game uses. The recommendation is the p95 real cost with the server's fee and
28500
- headroom applied, in whole credits; a stand that serves no headroom or no fee gets no
28501
- recommendation, because those multipliers are not the CLI's to invent.
28266
+ \`estimateCoins\` is a PRICE, not a guess: declaring it charges it, in full, even when the
28267
+ attempt fails, is cancelled, or stops at its budget. And three layers sit between the
28268
+ provider's rate card and that number \u2014 the provider's cost, the platform's tariff (already
28269
+ inside every charged coin), and your headroom. The bench itself is billed to YOU, on the
28270
+ development lane; the player-funded lane is what the published game uses. Adding a margin
28271
+ to the provider's rate silently drops the tariff and under-prices every call, so the
28272
+ recommendation is built from CHARGED COINS and the headroom comes from the server. A stand
28273
+ that serves no headroom gets no recommendation, because the multiplier is not the CLI's to
28274
+ invent.
28502
28275
 
28503
- Because the ceiling sizes each call's output allowance, the recommendation is never below
28504
- the smallest ceiling that leaves room for the benchmarked answer, as the server computes it.
28505
- A sample cut off at your --max-credits (provider_token_limit) is not a sample \u2014 re-run with a
28506
- higher --max-credits; an answer longer than one call on this stand may produce gets no
28507
- ceiling at all \u2014 ask for a shorter one. The run also prints the measured average per call,
28508
- fee included, which is the honest estimate a budget's disclosure is built on.
28276
+ The declared price also decides how LONG the answer may be: it sizes each call's output
28277
+ allowance. So the recommendation is never below the smallest price that leaves room for
28278
+ the benchmarked answer, as the server computes it. A sample cut off at your --max-coins
28279
+ (provider_token_limit) is not a sample \u2014 re-run with a higher --max-coins; an answer longer
28280
+ than one call on this stand may produce gets no price at all \u2014 ask for a shorter one.
28509
28281
 
28510
28282
  Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
28511
28283
  `,
@@ -28720,13 +28492,11 @@ function parseArgs(argv) {
28720
28492
  "--limit",
28721
28493
  // `genex budget --assets <credits>` — the allowance the player approved.
28722
28494
  "--assets",
28723
- // `genex llm bench` value flags. `--max-credits` is the same approval
28724
- // shape as `--assets`, but its own gate: the asset allowance does not
28725
- // count a bench. `--max-coins` is its deprecated name (same number).
28495
+ // `genex llm bench` value flags. `--max-coins` is the COIN half of the
28496
+ // same approval shape as `--assets` (a different economy, its own gate).
28726
28497
  "--model",
28727
28498
  "--samples",
28728
28499
  "--schema",
28729
- "--max-credits",
28730
28500
  "--max-coins",
28731
28501
  // `genex player install --label <name>` — what this machine is called in
28732
28502
  // the dashboard. Its own key rather than the generic 2nd positional,
@@ -29244,14 +29014,12 @@ function applyValueFlag(options, flag, value) {
29244
29014
  options.samples = n;
29245
29015
  break;
29246
29016
  }
29247
- case "--max-credits":
29248
29017
  case "--max-coins": {
29249
29018
  const n = Number(value);
29250
29019
  if (!Number.isInteger(n) || n < 1) {
29251
- throw new Error(`Invalid ${flag} value: ${value} (whole credits, 1 or more)`);
29020
+ throw new Error(`Invalid --max-coins value: ${value} (whole coin, 1 or more)`);
29252
29021
  }
29253
- if (flag === "--max-credits") options.maxCredits = n;
29254
- else options.maxCoins = n;
29022
+ options.maxCoins = n;
29255
29023
  break;
29256
29024
  }
29257
29025
  case "--timeout": {
@@ -29549,9 +29317,9 @@ async function main() {
29549
29317
  await runBudget(parsed.options);
29550
29318
  break;
29551
29319
  // The in-game model lane: is it live here, what does one call really
29552
- // cost, what ceiling should the game declare. `bench` spends the
29553
- // builder's own credits OUTSIDE the asset allowance, so it carries its
29554
- // own --max-credits/--user-approved gate.
29320
+ // cost, what should the game declare. `bench` is the only CLI command
29321
+ // that spends COIN, so it carries its own --max-coins/--user-approved
29322
+ // gate — the credits allowance cannot see that wallet.
29555
29323
  case "llm":
29556
29324
  await runLlm(parsed.options);
29557
29325
  break;