diff --git a/CHANGELOG.md b/CHANGELOG.md index e9571aca..f3f246e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Support Gemini native TTS generateContent endpoint — nguyenha935 - Add missing zh-CN endpoint key label (i18n) — weimaozhen - CodeBuddy: only send reasoning params when client requests reasoning (#2071) — Rex +- CodeBuddy CN: show one-shot bonus packs as expiring, not monthly-replenishing - Show custom provider models in combo picker — Sapto - Docker: add docker-compose.yml with headroom enabled by default — nitsuahlabs - Clarify token diagnostics vs provider billing (headroom, #1998) — Sutarto Jordan Chrisfivo diff --git a/open-sse/services/usage/codebuddy-cn.js b/open-sse/services/usage/codebuddy-cn.js index 32d15c2c..d355c729 100644 --- a/open-sse/services/usage/codebuddy-cn.js +++ b/open-sse/services/usage/codebuddy-cn.js @@ -109,15 +109,22 @@ export async function getCodeBuddyCnUsage(accessToken, apiKey, providerSpecificD total: num(acc.CycleCapacitySizePrecise, acc.CycleCapacitySize), resetAt: parseResetTime(acc.CycleEndTime), unlimited: false, + // Recurring allowance: the CycleEndTime is the next refresh, not the + // final expiry. The UI must show "Resets in", not "Expires in". + recurring: true, }; }); // Bonus packs: use the lifetime Capacity balance; resetAt is the expiry. + // These are one-shot credits (CycleEndTime == DeductionEndTime), so they + // never replenish — mark recurring:false so the UI shows "Expires in" + // instead of implying a monthly refill. bonuses.forEach((acc, i) => { quotas[`Bonus Pack ${i + 1}`] = { used: num(acc.CapacityUsedPrecise, acc.CapacityUsed), total: num(acc.CapacitySizePrecise, acc.CapacitySize), resetAt: parseResetTime(acc.CycleEndTime), unlimited: false, + recurring: false, }; }); diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js index eb145263..541cc9f0 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/ProviderLimitCard.js @@ -165,6 +165,7 @@ export default function ProviderLimitCard({ percentage={percentage} unlimited={unlimited} resetTime={quota.resetAt} + recurring={quota.recurring !== false} /> ); })} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js index fc0bdc1d..1a13acd2 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaProgressBar.js @@ -69,12 +69,17 @@ export default function QuotaProgressBar({ used = 0, total = 0, unlimited = false, - resetTime = null + resetTime = null, + recurring = true, }) { const colors = getColorClasses(percentage); const countdown = formatResetTime(resetTime); const resetDisplay = formatResetTimeDisplay(resetTime); - + + // recurring defaults true. One-shot packs (e.g. CodeBuddy CN bonus packs) + // set recurring:false: resetTime is a hard expiry, so word it as "expires". + const resetWord = recurring ? "Reset" : "Expires"; + // percentage is already remaining percentage (from ProviderLimitCard) const remaining = percentage; @@ -111,7 +116,7 @@ export default function QuotaProgressBar({ {countdown !== "-" && (
- Reset in {countdown} + {resetWord} in {countdown}
)} @@ -119,7 +124,7 @@ export default function QuotaProgressBar({ {/* Reset time display */} {resetDisplay && (
- Reset at {resetDisplay} + {resetWord} at {resetDisplay}
)} diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js index 765d7e91..8f2a1bc3 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaTable.js @@ -153,6 +153,11 @@ export default function QuotaTable({ const colors = getColorClasses(quota.remaining); const countdown = formatResetTime(quota.resetAt); const resetDisplay = formatResetTimeDisplay(quota.resetAt); + // recurring defaults true: a missing flag means the quota + // refreshes at resetAt. Bonus/one-shot packs set recurring:false + // and their resetAt is a hard expiry, so word it as "expires". + const recurring = quota.recurring !== false; + const countdownLabel = recurring ? `in ${countdown}` : `expires in ${countdown}`; return ( - {countdown !== "-" ? `in ${countdown}` : resetDisplay} + {countdown !== "-" ? countdownLabel : resetDisplay} ) : (
{countdown !== "-" && (
- in {countdown} + {countdownLabel}
)} {resetDisplay && ( diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 089670e2..688f0ab7 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -433,6 +433,24 @@ export function parseQuotaData(provider, data) { } break; + case "codebuddy-cn": + // CodeBuddy CN mixes recurring refill packs ("Monthly"/"Weekly"/...) + // with one-shot bonus packs ("Bonus Pack N"). Forward `recurring` + // so the UI can show "Expires in" for bonus packs (whose resetAt is + // a hard expiry, not a refresh) instead of "Reset in". + if (data.quotas) { + Object.entries(data.quotas).forEach(([name, quota]) => { + normalizedQuotas.push({ + name, + used: quota.used || 0, + total: quota.total || 0, + resetAt: quota.resetAt || null, + recurring: quota.recurring !== false, + }); + }); + } + break; + default: // Generic fallback for unknown providers if (data.quotas) { diff --git a/tests/unit/codebuddy-cn-bonus-recurring.test.js b/tests/unit/codebuddy-cn-bonus-recurring.test.js new file mode 100644 index 00000000..7d75942f --- /dev/null +++ b/tests/unit/codebuddy-cn-bonus-recurring.test.js @@ -0,0 +1,30 @@ +// CodeBuddy CN mixes recurring refill packs with one-shot bonus packs. +// Bonus packs ("Bonus Pack N") must surface recurring:false so the dashboard +// shows "Expires in" instead of implying a monthly refill. The usage handler +// tags the flag and parseQuotaData must forward it. +import { describe, it, expect } from "vitest"; +import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js"; + +describe("parseQuotaData codebuddy-cn recurring flag", () => { + it("forwards recurring:false for bonus packs and true for refill packs", () => { + const data = { + plan: "CodeBuddy CN", + quotas: { + Monthly: { used: 6.54, total: 500, resetAt: "2026-07-31T00:00:00Z", recurring: true }, + "Bonus Pack 1": { used: 12, total: 100, resetAt: "2026-07-15T00:00:00Z", recurring: false }, + }, + }; + + const out = parseQuotaData("codebuddy-cn", data); + const byName = Object.fromEntries(out.map((q) => [q.name, q])); + + expect(byName["Monthly"].recurring).toBe(true); + expect(byName["Bonus Pack 1"].recurring).toBe(false); + }); + + it("defaults recurring to true when the flag is absent (back-compat)", () => { + const data = { quotas: { Monthly: { used: 0, total: 100, resetAt: null } } }; + const out = parseQuotaData("codebuddy-cn", data); + expect(out[0].recurring).toBe(true); + }); +});