diff --git a/packages/agent-chat/src/creditDisplay.ts b/packages/agent-chat/src/creditDisplay.ts index c266c97..6e0a443 100644 --- a/packages/agent-chat/src/creditDisplay.ts +++ b/packages/agent-chat/src/creditDisplay.ts @@ -28,6 +28,20 @@ export function formatWholeCredits(amount: number): string { return `${sign}${grouped(String(Math.abs(n)))}`; } +/** Local currency per some quantity → credit text at 2 decimals. + +One credit is ``rate`` of that currency (0.01 in production: one fen or one cent). +A short amount stays a fraction. It is not rounded up to 1 credit. +*/ +export function formatCreditsFromLocal(localAmount: number, rate = 0.01): string { + if (!Number.isFinite(localAmount) || localAmount <= 0 || !(rate > 0)) { + return formatFromMicros(0); + } + const millionths = Math.round(localAmount * 1_000_000); + const microsPerMillionth = Math.round(MICROS_PER_CREDIT / (rate * 1_000_000)); + return formatFromMicros(millionths * microsPerMillionth); +} + /** Chat hops are stored in micros. Top-ups and withdrawals stay whole credits. */ export function formatLedgerAmount(amount: number, unit?: string | null): string { if (unit === "micros") return formatFromMicros(Math.trunc(amount)); diff --git a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx index 84e88a3..242494f 100644 --- a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx +++ b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx @@ -24,6 +24,7 @@ import { copyText } from "./connectPrompt"; import type { RanchMessages } from "./i18n"; import { officialShelfAllows } from "./officialV0"; import { formatLocalPerMillion } from "../composerPrice"; +import { formatCreditsFromLocal } from "../creditDisplay"; import { chatSettleQuote, DEFAULT_CHAT_FEE_RATE } from "./settleQuote"; import { btnGhost, btnPrimary, colors, inputStyle } from "./styles"; import { useModalA11y } from "./useModalA11y"; @@ -95,11 +96,6 @@ function skuFromRow(row: PieceSku): Omit { const FALLBACK_MODEL_ID = "openai/gpt-4o-mini"; const DEFAULT_MARKUP_PERCENT = 50; -function roundUsdPerMillion(n: number): number { - // Keep enough precision for cheap models without noisy floats. - return Math.round(n * 1e6) / 1e6; -} - function sameModelId(left: string, right: string): boolean { return left.trim().toLowerCase() === right.trim().toLowerCase(); } @@ -215,14 +211,6 @@ function resolvePricingModelId( return FALLBACK_MODEL_ID; } -/** 1 Credit = $0.01 — $1 = 100 Credits, same as wallet recharge. */ -const CREDIT_TO_USD = 0.01; - -function usdToCredits(usd: number): number { - if (!Number.isFinite(usd) || usd <= 0) return 0; - return Math.max(0, Math.ceil(roundUsdPerMillion(usd) / CREDIT_TO_USD - 1e-12)); -} - function uniqModelIds(...groups: Array | undefined>): string[] { const out: string[] = []; const seen = new Set(); @@ -2622,10 +2610,15 @@ export function AgentOwnerSettings({ }} > {fillTemplate(t.myAgentsPricingCreditsLine, { - in: String(usdToCredits(inputParsed)), - out: String(usdToCredits(outputParsed)), + in: formatCreditsFromLocal(inputParsed), + out: formatCreditsFromLocal(outputParsed), })} - + ) : null} diff --git a/packages/agent-chat/src/ranch-shell/i18n.ts b/packages/agent-chat/src/ranch-shell/i18n.ts index 573dd74..c386d0b 100644 --- a/packages/agent-chat/src/ranch-shell/i18n.ts +++ b/packages/agent-chat/src/ranch-shell/i18n.ts @@ -1193,8 +1193,8 @@ const en: RanchMessages = { myAgentsPricingModelSearch: "Search", myAgentsPricingOfficialEmpty: "No matching OpenRouter models in Host Catalog.", myAgentsPricingOptionLine: "${name} · in ${in} · out ${out}", - myAgentsPricingCreditsLine: "in ${in} · out ${out} Credits / 1M", - myAgentsPricingCreditsNote: "100 Credits = $1, same as wallet recharge. Each hop settles in whole Credits.", + myAgentsPricingCreditsLine: "in ${in} · out ${out} credits / 1M", + myAgentsPricingCreditsNote: "100 credits = ${price}. Usage is shown to 2 decimals. Top-up stays a whole number.", myAgentsPricingRuntimeHint: "Runtime reported: ${model} (heartbeat model only — Host cannot verify the key).", myAgentsPricingSelfReportNote: @@ -1952,8 +1952,8 @@ const zh: RanchMessages = { myAgentsPricingModelSearch: "搜索", myAgentsPricingOfficialEmpty: "Host Catalog 里没有匹配的 OpenRouter 模型。", myAgentsPricingOptionLine: "${name} · 入 ${in} · 出 ${out}", - myAgentsPricingCreditsLine: "入 ${in} · 出 ${out} Credits /百万", - myAgentsPricingCreditsNote: "与钱包充值相同:100 Credits = $1。每跳按整数 Credits 结算。", + myAgentsPricingCreditsLine: "入 ${in} · 出 ${out} 星币 /百万", + myAgentsPricingCreditsNote: "100 星币 = ${price}。用量显示两位小数。充值仍是整数星币。", myAgentsPricingRuntimeHint: "运行环境上报:${model}(只认心跳型号,Host 看不见钥匙)。", myAgentsPricingSelfReportNote: "Owner 只设默认模型 + 上浮;底价由 Host 维护,智能体不能改写全网结算价。", diff --git a/tests/credit-display.test.cjs b/tests/credit-display.test.cjs index 40620ec..f00cac0 100644 --- a/tests/credit-display.test.cjs +++ b/tests/credit-display.test.cjs @@ -13,7 +13,7 @@ const js = ts.transpileModule(source, { }).outputText; const moduleExports = {}; new Function("exports", js)(moduleExports); -const { formatCreditBalance, formatLedgerAmount, formatWholeCredits } = moduleExports; +const { formatCreditBalance, formatCreditsFromLocal, formatLedgerAmount, formatWholeCredits } = moduleExports; test("a balance keeps the micro remainder to two decimals", () => { assert.equal(formatCreditBalance(10, 14000), "10.01"); @@ -30,6 +30,14 @@ test("a chat hop in micros is shown in credits", () => { assert.equal(formatCreditBalance(1, 4999), "1.00"); }); +test("a local price becomes credits at two decimals", () => { + assert.equal(formatCreditsFromLocal(1.15), "115.00"); + assert.equal(formatCreditsFromLocal(2.3), "230.00"); + assert.equal(formatCreditsFromLocal(0.766667), "76.67"); + assert.equal(formatCreditsFromLocal(0.004), "0.40"); + assert.equal(formatCreditsFromLocal(0), "0.00"); +}); + test("a top-up stays a whole number", () => { assert.equal(formatLedgerAmount(100, "credits"), "100"); assert.equal(formatLedgerAmount(100), "100");