Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/agent-chat/src/composerPrice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 31 additions & 15 deletions packages/agent-chat/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = {
Expand Down
35 changes: 21 additions & 14 deletions packages/agent-chat/src/ranch-shell/AgentOwnerSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -238,9 +239,9 @@ function uniqModelIds(...groups: Array<Array<string | null | undefined> | 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 {
Expand All @@ -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";

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
),
Expand Down Expand Up @@ -1224,6 +1228,7 @@ export function AgentOwnerSettings({
in: quote.input,
out: quote.output,
source: src || book,
currency: quote.currency,
});
}
if (!data.items.length) break;
Expand Down Expand Up @@ -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
Expand All @@ -1398,6 +1404,7 @@ export function AgentOwnerSettings({
in: byoOpenRouterRow.in,
out: byoOpenRouterRow.out,
source: byoOpenRouterRow.source || "openrouter",
currency: byoOpenRouterRow.currency,
}
: null
: catalogById[displayedModelId] ??
Expand Down Expand Up @@ -2594,15 +2601,15 @@ export function AgentOwnerSettings({
<div style={{ margin: "0 0 10px", fontSize: 12, lineHeight: 1.45 }}>
<div style={{ color: colors.text }}>
{fillTemplate(t.myAgentsPricingListingLine, {
in: fmtUsd(inputParsed),
out: fmtUsd(outputParsed),
in: fmtLocal(inputParsed, selectedCatalog?.currency),
out: fmtLocal(outputParsed, selectedCatalog?.currency),
})}
</div>
{ownerInParsed != null && ownerOutParsed != null ? (
<div style={{ color: colors.muted, marginTop: 2 }}>
{fillTemplate(t.myAgentsPricingKeepLine, {
in: fmtUsd(ownerInParsed),
out: fmtUsd(ownerOutParsed),
in: fmtLocal(ownerInParsed, selectedCatalog?.currency),
out: fmtLocal(ownerOutParsed, selectedCatalog?.currency),
})}
</div>
) : null}
Expand Down
16 changes: 10 additions & 6 deletions packages/agent-chat/src/ranch-shell/RanchChatShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -3178,7 +3178,9 @@ export function RanchChatShell(props: RanchChatShellProps) {
const [droppedModelIds, setDroppedModelIds] = useState<string[]>([]);
const [composerMenuOpen, setComposerMenuOpen] = useState(false);
const [composerMenuQuery, setComposerMenuQuery] = useState("");
const [composerCatalog, setComposerCatalog] = useState<Array<{ id: string; in: number; out: number }>>(
const [composerCatalog, setComposerCatalog] = useState<
Array<{ id: string; in: number; out: number; currency?: string }>
>(
[],
);
const [composerCatalogLoading, setComposerCatalogLoading] = useState(false);
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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,
),
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-chat/src/ranch-shell/settleQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion tests/composer-price.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
12 changes: 6 additions & 6 deletions tests/settle-quote.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading