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);
+ });
+});