From 9292a403a92d51066336f5ddffea75866a5d390b Mon Sep 17 00:00:00 2001 From: NeilJo-GY <43027886+NeilJo-GY@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:12:06 +0800 Subject: [PATCH] Show the agent settings price in yuan or dollars, with the fee outside the total. Co-authored-by: Cursor --- packages/agent-chat/src/composerPrice.ts | 4 ++ packages/agent-chat/src/gateway.ts | 46 +++++++++++++------ .../src/ranch-shell/AgentOwnerSettings.tsx | 35 ++++++++------ .../src/ranch-shell/RanchChatShell.tsx | 16 ++++--- .../agent-chat/src/ranch-shell/settleQuote.ts | 4 +- tests/composer-price.test.cjs | 7 ++- tests/settle-quote.test.cjs | 12 ++--- 7 files changed, 80 insertions(+), 44 deletions(-) diff --git a/packages/agent-chat/src/composerPrice.ts b/packages/agent-chat/src/composerPrice.ts index b561d7d..4efc768 100644 --- a/packages/agent-chat/src/composerPrice.ts +++ b/packages/agent-chat/src/composerPrice.ts @@ -8,6 +8,10 @@ export function formatCnyPerMillion(n: number): string { return `¥${Number(n.toPrecision(6))}`; } +export function formatLocalPerMillion(n: number, currency?: string | null): string { + return (currency || "").toUpperCase() === "CNY" ? formatCnyPerMillion(n) : formatUsdPerMillion(n); +} + export type MenuRate = { input_price_per_million?: number; output_price_per_million?: number; diff --git a/packages/agent-chat/src/gateway.ts b/packages/agent-chat/src/gateway.ts index d77b53b..a9f6412 100644 --- a/packages/agent-chat/src/gateway.ts +++ b/packages/agent-chat/src/gateway.ts @@ -467,9 +467,12 @@ export type ModelCatalogItem = { display_name?: string | null; input_price_per_million: number; output_price_per_million: number; - /** Official Catalog = Host sync × 1.15. Absent on older Hosts. */ + /** Official list in `currency`: book × 1.15. Absent on older Hosts. */ published_input_price_per_million?: number | null; published_output_price_per_million?: number | null; + /** Yuan sync when `currency` is CNY. Dollars otherwise. Not × 1.15. */ + local_input_price_per_million?: number | null; + local_output_price_per_million?: number | null; currency?: string; source?: string | null; modality?: string | null; @@ -480,38 +483,51 @@ export type ModelCatalogItem = { export const OFFICIAL_PUBLISH_FACTOR = 1.15; +function catalogCurrency(row: { currency?: string | null }): "CNY" | "USD" { + return (row.currency || "").toUpperCase() === "CNY" ? "CNY" : "USD"; +} + export function officialCatalogRates(row: { input_price_per_million: number; output_price_per_million: number; published_input_price_per_million?: number | null; published_output_price_per_million?: number | null; -}): { input: number; output: number } | null { - const syncIn = Number(row.input_price_per_million); - const syncOut = Number(row.output_price_per_million); - if (!Number.isFinite(syncIn) || !Number.isFinite(syncOut)) return null; + currency?: string | null; +}): { input: number; output: number; currency: "CNY" | "USD" } | null { const publishedIn = Number(row.published_input_price_per_million); const publishedOut = Number(row.published_output_price_per_million); const round = (n: number) => Math.round(n * 1e6) / 1e6; + if (Number.isFinite(publishedIn) && Number.isFinite(publishedOut)) { + return { input: publishedIn, output: publishedOut, currency: catalogCurrency(row) }; + } + const syncIn = Number(row.input_price_per_million); + const syncOut = Number(row.output_price_per_million); + if (!Number.isFinite(syncIn) || !Number.isFinite(syncOut)) return null; return { - input: Number.isFinite(publishedIn) - ? publishedIn - : round(syncIn * OFFICIAL_PUBLISH_FACTOR), - output: Number.isFinite(publishedOut) - ? publishedOut - : round(syncOut * OFFICIAL_PUBLISH_FACTOR), + input: round(syncIn * OFFICIAL_PUBLISH_FACTOR), + output: round(syncOut * OFFICIAL_PUBLISH_FACTOR), + currency: "USD", }; } -/** BYO / Store OpenRouter listing uses Host sync, not official Catalog × 1.15. */ +/** BYO listing uses the book rate in the market currency, not official × 1.15. */ export function syncCatalogRates(row: { input_price_per_million: number; output_price_per_million: number; -}): { input: number; output: number } | null { + local_input_price_per_million?: number | null; + local_output_price_per_million?: number | null; + currency?: string | null; +}): { input: number; output: number; currency: "CNY" | "USD" } | null { + const round = (n: number) => Math.round(n * 1e6) / 1e6; + const localIn = Number(row.local_input_price_per_million); + const localOut = Number(row.local_output_price_per_million); + if (Number.isFinite(localIn) && Number.isFinite(localOut)) { + return { input: round(localIn), output: round(localOut), currency: catalogCurrency(row) }; + } const syncIn = Number(row.input_price_per_million); const syncOut = Number(row.output_price_per_million); if (!Number.isFinite(syncIn) || !Number.isFinite(syncOut)) return null; - const round = (n: number) => Math.round(n * 1e6) / 1e6; - return { input: round(syncIn), output: round(syncOut) }; + return { input: round(syncIn), output: round(syncOut), currency: "USD" }; } export type ModelCatalogList = { diff --git a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx index c1ad531..84e88a3 100644 --- a/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx +++ b/packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx @@ -23,6 +23,7 @@ import { import { copyText } from "./connectPrompt"; import type { RanchMessages } from "./i18n"; import { officialShelfAllows } from "./officialV0"; +import { formatLocalPerMillion } from "../composerPrice"; import { chatSettleQuote, DEFAULT_CHAT_FEE_RATE } from "./settleQuote"; import { btnGhost, btnPrimary, colors, inputStyle } from "./styles"; import { useModalA11y } from "./useModalA11y"; @@ -238,9 +239,9 @@ function uniqModelIds(...groups: Array | undefi return out; } -function fmtUsd(n: number): string { +function fmtLocal(n: number, currency?: string): string { if (!Number.isFinite(n)) return "—"; - return roundUsdPerMillion(n).toFixed(6); + return formatLocalPerMillion(n, currency); } function shortModelLabel(modelId: string): string { @@ -249,7 +250,7 @@ function shortModelLabel(modelId: string): string { return slash >= 0 ? s.slice(slash + 1) : s; } -type CatalogPair = { in: number; out: number; source?: string }; +type CatalogPair = { in: number; out: number; source?: string; currency?: string }; const TOKENHUB_PRICE_URL = "https://cloud.tencent.com/document/product/1823/130055"; @@ -560,12 +561,16 @@ function OfficialModelPicker({ function catalogOptionLabel( id: string, - pair: { in: number; out: number } | undefined, + pair: { in: number; out: number; currency?: string } | undefined, tmpl: string, ): string { const name = shortModelLabel(id) || id; if (!pair) return name; - return fillTemplate(tmpl, { name, in: fmtUsd(pair.in), out: fmtUsd(pair.out) }); + return fillTemplate(tmpl, { + name, + in: fmtLocal(pair.in, pair.currency), + out: fmtLocal(pair.out, pair.currency), + }); } function fillTemplate( @@ -1167,11 +1172,10 @@ export function AgentOwnerSettings({ supportedModels.map((id) => client.getModelCatalogItem(id).then( (row) => { - const cin = Number(row.input_price_per_million); - const cout = Number(row.output_price_per_million); - if (!Number.isFinite(cin) || !Number.isFinite(cout)) return [id, null] as const; + const quote = syncCatalogRates(row); + if (!quote) return [id, null] as const; const source = (row.source || "").trim() || undefined; - return [id, { in: cin, out: cout, source }] as const; + return [id, { in: quote.input, out: quote.output, source, currency: quote.currency }] as const; }, () => [id, null] as const, ), @@ -1224,6 +1228,7 @@ export function AgentOwnerSettings({ in: quote.input, out: quote.output, source: src || book, + currency: quote.currency, }); } if (!data.items.length) break; @@ -1385,11 +1390,12 @@ export function AgentOwnerSettings({ sameModelId(row.id, displayedModelId), ); const selectedCatalog = officialSelected - ? officialRow + ? officialRow ? { in: officialRow.in, out: officialRow.out, source: officialRow.source || "openrouter", + currency: officialRow.currency, } : null : openRouterByoSelected @@ -1398,6 +1404,7 @@ export function AgentOwnerSettings({ in: byoOpenRouterRow.in, out: byoOpenRouterRow.out, source: byoOpenRouterRow.source || "openrouter", + currency: byoOpenRouterRow.currency, } : null : catalogById[displayedModelId] ?? @@ -2594,15 +2601,15 @@ export function AgentOwnerSettings({
{fillTemplate(t.myAgentsPricingListingLine, { - in: fmtUsd(inputParsed), - out: fmtUsd(outputParsed), + in: fmtLocal(inputParsed, selectedCatalog?.currency), + out: fmtLocal(outputParsed, selectedCatalog?.currency), })}
{ownerInParsed != null && ownerOutParsed != null ? (
{fillTemplate(t.myAgentsPricingKeepLine, { - in: fmtUsd(ownerInParsed), - out: fmtUsd(ownerOutParsed), + in: fmtLocal(ownerInParsed, selectedCatalog?.currency), + out: fmtLocal(ownerOutParsed, selectedCatalog?.currency), })}
) : null} diff --git a/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx b/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx index 02e0fbf..c5cfdb9 100644 --- a/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx +++ b/packages/agent-chat/src/ranch-shell/RanchChatShell.tsx @@ -34,7 +34,7 @@ import { } from "../attachmentListedPrice"; import { ChatComputerControl } from "../ChatComputerControl"; import { computerName, extraIndex, type ComputerNameRow } from "../computerLabel"; -import { formatMenuSide, formatUsdPerMillion } from "../composerPrice"; +import { formatLocalPerMillion, formatMenuSide } from "../composerPrice"; import { placeForDefaultComputer, screenAsksBeforeCharge, @@ -3178,7 +3178,9 @@ export function RanchChatShell(props: RanchChatShellProps) { const [droppedModelIds, setDroppedModelIds] = useState([]); const [composerMenuOpen, setComposerMenuOpen] = useState(false); const [composerMenuQuery, setComposerMenuQuery] = useState(""); - const [composerCatalog, setComposerCatalog] = useState>( + const [composerCatalog, setComposerCatalog] = useState< + Array<{ id: string; in: number; out: number; currency?: string }> + >( [], ); const [composerCatalogLoading, setComposerCatalogLoading] = useState(false); @@ -5164,7 +5166,7 @@ export function RanchChatShell(props: RanchChatShellProps) { setComposerCatalogLoading(true); void (async () => { const page = 500; - const acc: Array<{ id: string; in: number; out: number }> = []; + const acc: Array<{ id: string; in: number; out: number; currency?: string }> = []; let offset = 0; let total = Number.POSITIVE_INFINITY; try { @@ -5187,7 +5189,7 @@ export function RanchChatShell(props: RanchChatShellProps) { if (!quote) continue; if (quote.input < 0 || quote.output < 0) continue; if (acc.some((item) => sameModelId(item.id, id))) continue; - acc.push({ id, in: quote.input, out: quote.output }); + acc.push({ id, in: quote.input, out: quote.output, currency: quote.currency }); } if (!cancelled) setComposerCatalog([...acc]); if (!data.items.length) break; @@ -7821,11 +7823,13 @@ export function RanchChatShell(props: RanchChatShellProps) { } if (cat) { return t.composerModelPrice( - formatUsdPerMillion( + formatLocalPerMillion( applyMarkupUsd(cat.in, composerModel.markup_percent), + cat.currency, ), - formatUsdPerMillion( + formatLocalPerMillion( applyMarkupUsd(cat.out, composerModel.markup_percent), + cat.currency, ), ); } diff --git a/packages/agent-chat/src/ranch-shell/settleQuote.ts b/packages/agent-chat/src/ranch-shell/settleQuote.ts index d01cbd3..26a1a9b 100644 --- a/packages/agent-chat/src/ranch-shell/settleQuote.ts +++ b/packages/agent-chat/src/ranch-shell/settleQuote.ts @@ -23,8 +23,8 @@ export function chatSettleQuote( return { ownerIn: roundUsd(ownerIn), ownerOut: roundUsd(ownerOut), - hunterIn: roundUsd(baseIn + gross(ownerIn)), - hunterOut: roundUsd(baseOut + gross(ownerOut)), + hunterIn: roundUsd(gross(baseIn + ownerIn)), + hunterOut: roundUsd(gross(baseOut + ownerOut)), }; } const ownerIn = baseIn * (1 + markupPercent / 100); diff --git a/tests/composer-price.test.cjs b/tests/composer-price.test.cjs index 5227aec..9d4d1d7 100644 --- a/tests/composer-price.test.cjs +++ b/tests/composer-price.test.cjs @@ -13,7 +13,12 @@ const js = ts.transpileModule(source, { }).outputText; const moduleExports = {}; new Function("exports", js)(moduleExports); -const { formatCnyPerMillion, formatMenuSide, formatUsdPerMillion } = moduleExports; +const { formatCnyPerMillion, formatLocalPerMillion, formatMenuSide, formatUsdPerMillion } = moduleExports; + +test("a local rate prints yuan only when the book says CNY", () => { + assert.equal(formatLocalPerMillion(0.69, "CNY"), "¥0.69"); + assert.equal(formatLocalPerMillion(0.1725, "USD"), formatUsdPerMillion(0.1725)); +}); test("yuan labels keep the stored list rate", () => { assert.equal(formatCnyPerMillion(0.6), "¥0.6"); diff --git a/tests/settle-quote.test.cjs b/tests/settle-quote.test.cjs index 258dd4b..7f60f8c 100644 --- a/tests/settle-quote.test.cjs +++ b/tests/settle-quote.test.cjs @@ -23,18 +23,18 @@ test("byo hunter price grosses the fee outside the marked-up book", () => { assert.ok(Math.abs(quote.hunterOut - 3 / 0.9) < 1e-6); }); -test("official keeps list times 1.15 and grosses only the markup", () => { +test("official fee sits outside list plus markup", () => { const quote = chatSettleQuote(1.15, 2.3, 50, true, 0.1); assert.ok(Math.abs(quote.ownerIn - 1.15 * 0.5) < 1e-6); assert.ok(Math.abs(quote.ownerOut - 2.3 * 0.5) < 1e-6); - assert.ok(Math.abs(quote.hunterIn - (1.15 + (1.15 * 0.5) / 0.9)) < 1e-6); - assert.ok(Math.abs(quote.hunterOut - (2.3 + (2.3 * 0.5) / 0.9)) < 1e-6); + assert.ok(Math.abs(quote.hunterIn - (1.15 * 1.5) / 0.9) < 1e-6); + assert.ok(Math.abs(quote.hunterOut - (2.3 * 1.5) / 0.9) < 1e-6); }); -test("official markup zero pays the baseline and the owner keeps nothing", () => { +test("official markup zero still pays the fee outside the list", () => { const quote = chatSettleQuote(1.15, 2.3, 0, true, 0.1); - assert.equal(quote.hunterIn, 1.15); - assert.equal(quote.hunterOut, 2.3); + assert.ok(Math.abs(quote.hunterIn - 1.15 / 0.9) < 1e-6); + assert.ok(Math.abs(quote.hunterOut - 2.3 / 0.9) < 1e-6); assert.equal(quote.ownerIn, 0); assert.equal(quote.ownerOut, 0); });