From 4977f1adcffcff5a7f07c093db32c02187dd3aeb Mon Sep 17 00:00:00 2001 From: Abhishek Sharma Date: Wed, 26 Aug 2026 13:58:35 -0700 Subject: [PATCH] fix(opencode-plugin): stop a rounded free budget rendering as 1000K instead of 1M (#11684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via /merge-batch (2026-08-26, v3.8.51). Conflito trivial no package.json (lista de testes, mesclada com #11660). Também atualizei um teste pré-existente que documentava explicitamente o bug ("characterises the 999_999 rounding wart... Not this PR's bug to fix") — este PR é exatamente esse futuro fix, então virei a asserção para o comportamento correto (1M em vez de 1000K). Validado: 16/16 testes passando. Obrigado pela contribuição. --- @omniroute/opencode-plugin/package.json | 2 +- @omniroute/opencode-plugin/src/naming.ts | 78 +++++++++---------- .../tests/free-budget-magnitude.test.ts | 78 +++++++++++++++++++ .../opencode-plugin/tests/naming.test.ts | 12 +-- .../11684-free-budget-magnitude-rounding.md | 1 + 5 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 @omniroute/opencode-plugin/tests/free-budget-magnitude.test.ts create mode 100644 changelog.d/fixes/11684-free-budget-magnitude-rounding.md diff --git a/@omniroute/opencode-plugin/package.json b/@omniroute/opencode-plugin/package.json index a77cee5b93..527ff4555e 100644 --- a/@omniroute/opencode-plugin/package.json +++ b/@omniroute/opencode-plugin/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "tsup", "clean": "rm -rf dist", - "test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts", + "test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts", "prepublishOnly": "npm run clean && npm run build && npm test" }, "keywords": [ diff --git a/@omniroute/opencode-plugin/src/naming.ts b/@omniroute/opencode-plugin/src/naming.ts index f1aa673b06..823809cf20 100644 --- a/@omniroute/opencode-plugin/src/naming.ts +++ b/@omniroute/opencode-plugin/src/naming.ts @@ -23,27 +23,11 @@ const ALIAS_UPPER_MAX_CHARS = 5; // ── Auto Combo Types ───────────────────────────────────────────────────── -export type AutoVariant = - | "coding" - | "fast" - | "cheap" - | "offline" - | "smart" - | "lkgp"; +export type AutoVariant = "coding" | "fast" | "cheap" | "offline" | "smart" | "lkgp"; -export const AUTO_VARIANTS: AutoVariant[] = [ - "coding", - "fast", - "cheap", - "offline", - "smart", - "lkgp", -]; +export const AUTO_VARIANTS: AutoVariant[] = ["coding", "fast", "cheap", "offline", "smart", "lkgp"]; -export const AUTO_VARIANT_DESCRIPTIONS: Record< - AutoVariant | "default", - string -> = { +export const AUTO_VARIANT_DESCRIPTIONS: Record = { default: "Best provider via scoring", coding: "Quality-first for code tasks", fast: "Latency-optimized routing", @@ -83,24 +67,15 @@ function titleCaseAlias(alias: string): string { * 3. Neither → undefined. */ export function shortProviderLabel( - enrichment: - | { providerDisplayName?: string; providerAlias?: string } - | undefined, + enrichment: { providerDisplayName?: string; providerAlias?: string } | undefined ): string | undefined { if (!enrichment) return undefined; const raw = - typeof enrichment.providerDisplayName === "string" - ? enrichment.providerDisplayName.trim() - : ""; + typeof enrichment.providerDisplayName === "string" ? enrichment.providerDisplayName.trim() : ""; if (raw.length > 0 && raw.length <= PROVIDER_LABEL_MAX_CHARS) return raw; - const alias = - typeof enrichment.providerAlias === "string" - ? enrichment.providerAlias.trim() - : ""; + const alias = typeof enrichment.providerAlias === "string" ? enrichment.providerAlias.trim() : ""; if (alias.length > 0) { - return alias.length <= ALIAS_UPPER_MAX_CHARS - ? alias.toUpperCase() - : titleCaseAlias(alias); + return alias.length <= ALIAS_UPPER_MAX_CHARS ? alias.toUpperCase() : titleCaseAlias(alias); } // Long displayName with no alias to fall back on: keep the long label // rather than dropping the provider prefix entirely. @@ -131,10 +106,33 @@ export function normaliseFreeLabel(name: string): string { // ── Free Budget Formatting ──────────────────────────────────────────────── +/** Scales, largest first, so the unit is chosen by descending magnitude. */ +const TOKEN_UNITS = [ + [1e9, "B"], + [1e6, "M"], + [1e3, "K"], +] as const; + +/** + * Format a token count as a short magnitude string: `25M`, `1.5K`, `999`. + * + * The unit has to be picked from the value that will actually be *printed*, + * not from the raw input. `toFixed(1)` rounds to the nearest tenth, so at the + * K scale 999_950 and above render as `1000.0` — and by then the M branch has + * already been skipped, producing `1000K` for a number that is `1M`. The same + * carry turns just under a billion into `1000M`. When the rounded value reaches + * the next scale, re-render at that scale instead. + */ function fmtTokens(n: number): string { - if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B"; - if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M"; - if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "K"; + for (let i = 0; i < TOKEN_UNITS.length; i++) { + const [scale, suffix] = TOKEN_UNITS[i]!; + if (n < scale) continue; + const value = Number((n / scale).toFixed(1)); + // `Number()` also drops a trailing `.0`, which the previous regex did. + if (value < 1000 || i === 0) return `${value}${suffix}`; + const [nextScale, nextSuffix] = TOKEN_UNITS[i - 1]!; + return `${Number((n / nextScale).toFixed(1))}${nextSuffix}`; + } return String(n); } @@ -184,15 +182,11 @@ export function formatFreeBudget(params: { */ export function formatAutoComboName( variant: AutoVariant | undefined, - candidateCount?: number, + candidateCount?: number ): string { - const label = variant - ? variant.charAt(0).toUpperCase() + variant.slice(1) - : "Default"; + const label = variant ? variant.charAt(0).toUpperCase() + variant.slice(1) : "Default"; const count = - typeof candidateCount === "number" && candidateCount > 0 - ? ` (${candidateCount}p)` - : ""; + typeof candidateCount === "number" && candidateCount > 0 ? ` (${candidateCount}p)` : ""; return `Auto: ${label}${count}`; } diff --git a/@omniroute/opencode-plugin/tests/free-budget-magnitude.test.ts b/@omniroute/opencode-plugin/tests/free-budget-magnitude.test.ts new file mode 100644 index 0000000000..bf7347cd0a --- /dev/null +++ b/@omniroute/opencode-plugin/tests/free-budget-magnitude.test.ts @@ -0,0 +1,78 @@ +/** + * Magnitude-crossover regression for the free-budget suffix + * (`formatFreeBudget` -> `fmtTokens` in @omniroute/opencode-plugin/src/naming.ts). + * + * `fmtTokens` picked its unit from the raw input and then rounded with + * `toFixed(1)`. Rounding can carry a value into the next magnitude *after* that + * branch has been skipped, so 999_950..999_999 rendered as "1000K" rather than + * "1M", and just under a billion rendered as "1000M" rather than "1B". + * + * These budgets are not always round numbers: `monthlyTokens` is derived from the + * remote Radar feed (`tokensPerMonth`) and can be replaced wholesale by a + * user-local override, so the crossover band is reachable with real data. + * + * Kept in its own file rather than added to naming.test.ts so this does not + * collide with the coverage being added for `formatFreeBudget` in #11660. + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { formatFreeBudget } from "../src/naming.js"; + +/** `recurring-daily` is the shortest path from a token count to a rendered suffix. */ +const daily = (monthlyTokens: number) => + formatFreeBudget({ freeType: "recurring-daily", monthlyTokens }).replace(" tokens/day", ""); + +test("fmtTokens: a rounded K value that reaches 1000 is promoted to M", () => { + // 999_950 is the true boundary, not 999_999: toFixed(1) rounds to the nearest + // tenth, so 999.95K is the first value that carries to "1000.0". + assert.equal(daily(999_950), "1M"); + assert.equal(daily(999_999), "1M"); +}); + +test("fmtTokens: a rounded M value that reaches 1000 is promoted to B", () => { + assert.equal(daily(999_950_000), "1B"); + assert.equal(daily(999_999_999), "1B"); +}); + +test("fmtTokens: values just below the rounding boundary keep their own unit", () => { + // The promotion must not fire early — 999.9K still rounds to 999.9, not 1000. + assert.equal(daily(999_949), "999.9K"); + assert.equal(daily(999_499), "999.5K"); + assert.equal(daily(999_499_999), "999.5M"); +}); + +test("fmtTokens: ordinary magnitudes are unchanged", () => { + assert.equal(daily(0), "0"); + assert.equal(daily(999), "999"); + assert.equal(daily(1_000), "1K"); + assert.equal(daily(1_500), "1.5K"); + assert.equal(daily(1_000_000), "1M"); + assert.equal(daily(1_500_000), "1.5M"); + assert.equal(daily(25_000_000), "25M"); + assert.equal(daily(1_234_567), "1.2M"); + assert.equal(daily(1_000_000_000), "1B"); + assert.equal(daily(2_500_000_000), "2.5B"); +}); + +test("fmtTokens: B is the top unit, so a carry there has nowhere to go", () => { + // Deliberately pinned: promoting past B would need a unit that does not exist, + // so "1000B" is the intended output rather than an oversight. + assert.equal(daily(999_999_999_999), "1000B"); +}); + +test("formatFreeBudget: the promotion applies to every token-bearing branch", () => { + assert.equal( + formatFreeBudget({ freeType: "recurring-monthly", monthlyTokens: 999_999 }), + "1M tokens/month" + ); + assert.equal( + formatFreeBudget({ freeType: "recurring-credit", creditTokens: 999_999 }), + "1M credits" + ); + assert.equal( + formatFreeBudget({ freeType: "one-time-initial", creditTokens: 999_999 }), + "1M credits (one-time)" + ); +}); diff --git a/@omniroute/opencode-plugin/tests/naming.test.ts b/@omniroute/opencode-plugin/tests/naming.test.ts index 4502bf6a19..5a3c61b442 100644 --- a/@omniroute/opencode-plugin/tests/naming.test.ts +++ b/@omniroute/opencode-plugin/tests/naming.test.ts @@ -69,13 +69,13 @@ test("formatFreeBudget: sub-1K token count is not abbreviated", () => { ); }); -test("formatFreeBudget: characterises the 999_999 rounding wart (rounds past its own magnitude)", () => { - // `toFixed(1)` rounds 999999/1e3 up to "1000.0" before the `>= 1e6` - // threshold check has a chance to apply, so this reads "1000K" instead of - // the intended "1M". Not this PR's bug to fix — pinned here as the - // documented current behaviour so a future fix has a test to flip. +test("formatFreeBudget: the 999_999 rounding wart is fixed — promotes to 1M", () => { + // `toFixed(1)` rounds 999999/1e3 up to "1000.0" before the `>= 1e6` threshold + // check has a chance to apply. fmtTokens now promotes a rounded-up "1000" in + // any unit to the next unit up, so this correctly reads "1M" instead of the + // old "1000K" wart. assert.equal( formatFreeBudget({ freeType: "recurring-daily", monthlyTokens: 999_999 }), - "1000K tokens/day" + "1M tokens/day" ); }); diff --git a/changelog.d/fixes/11684-free-budget-magnitude-rounding.md b/changelog.d/fixes/11684-free-budget-magnitude-rounding.md new file mode 100644 index 0000000000..13c20ff567 --- /dev/null +++ b/changelog.d/fixes/11684-free-budget-magnitude-rounding.md @@ -0,0 +1 @@ +- fix(opencode-plugin): stop a free-tier budget that rounds up across a magnitude from rendering as `1000K`/`1000M` in the model picker — `fmtTokens` chose its unit from the raw token count and then rounded with `toFixed(1)`, so 999,950–999,999 printed as `1000K` rather than `1M` and just under a billion printed as `1000M` rather than `1B` ([#11684](https://github.com/diegosouzapw/OmniRoute/pull/11684))