feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits (#12754)

* feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits

z.ai sells Reset Cards that clear an exhausted GLM coding-plan window (5-hour or
weekly) ahead of its natural rollover, but OmniRoute only ever read the passive
nextResetTime, so redeeming one meant leaving the dashboard.

Add the wire layer for z.ai's two reset endpoints
(/api/biz/customer-package-reset/list and /use), which authenticate with the same
Bearer API key as /api/monitor/usage/quota/limit and report failures inside an
HTTP-200 envelope, so callers must inspect success/code rather than the status line.

The banked count rides along with the quota poll - only for keys that actually
report a resettable window, and strictly best-effort so a card-less account or a
transient failure still renders its quotas. The existing reset-credit card, picker
and confirmation flow, until now gated to Codex, now also drive glm/glm-cn/glmt/zai
through the new /api/usage/glm-reset-card route, reusing z.ai's requestId as the
idempotency key so a retry cannot burn two cards.

* test(usage): cover GLM reset-card edge cases

* test(dashboard): require GLM reset-card copy

* fix(usage): harden GLM reset-card redemption

* fix(usage): treat missing GLM key as empty

* fix(usage): fence GLM reset-card operations and coalesce lease-window duplicates

- Acquire a synthetic 60s exclusive-connection lease around each list/use
  wire operation; release in finally so a competing lease can acquire
  immediately after success or failure.
- Coalesce same-key duplicates that arrive after lease acquisition by
  checking the in-flight attempt before loading the connection.
- Run the post-commit quota refresh outside the lease (redemption is
  already committed; the refresh is auxiliary and failure-tolerant).
- Do not discard a retained ambiguous attempt on a lease-conflict 409.
- Harden transport error mapping: static messages for proxy transport
  failures, keep explicit direct routing for unproxied connections
  through list, use, and refresh.

* fix(i18n): sync GLM reset-card keys to pt-BR and vi locales

---------

Co-authored-by: insoln <is@careerum.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Innokentiy Solntsev
2026-09-17 23:38:09 +02:00
committed by GitHub
parent 4d9c4d3d8f
commit 241e63bfea
22 changed files with 2266 additions and 62 deletions

View File

@@ -13,10 +13,60 @@ interface Props {
availableCount: number;
isOpen: boolean;
loading: boolean;
provider: string;
onClose: () => void;
onRedeem: (selectionToken: string) => Promise<void>;
}
function isGlmResetProvider(provider: string): boolean {
return provider !== "codex";
}
export function getResetCreditWindowTitle(
provider: string,
credit: CodexResetCreditView,
tr: (key: string, fallback: string, values?: UsageTranslationValues) => string
): string {
if (!isGlmResetProvider(provider)) {
return credit.title || tr("resetCreditDefaultTitle", "Full reset");
}
const windowTitle =
credit.resetType === "WEEK"
? tr("glmResetCreditWeekTitle", "Weekly window reset")
: tr("glmResetCreditFiveHourTitle", "5-hour window reset");
return credit.title ? `${windowTitle} · ${credit.title}` : windowTitle;
}
export function getResetCreditConfirmation(
provider: string,
resetType: string | undefined,
tr: (key: string, fallback: string, values?: UsageTranslationValues) => string
): string {
if (!isGlmResetProvider(provider)) {
return tr(
"confirmRedeemResetCredit",
"Redeeming immediately resets the eligible Codex usage windows and permanently consumes this credit."
);
}
if (resetType === "WEEK") {
return tr(
"glmConfirmRedeemWeekResetCredit",
"Redeeming immediately resets the weekly usage window and permanently consumes this card."
);
}
if (resetType === "FIVE_HOUR") {
return tr(
"glmConfirmRedeemFiveHourResetCredit",
"Redeeming immediately resets the 5-hour usage window and permanently consumes this card."
);
}
return tr(
"glmConfirmRedeemResetCredit",
"Redeeming immediately resets the selected usage window and permanently consumes this card."
);
}
function formatRelativeExpiry(expiresAt: string | null | undefined): string | null {
if (!expiresAt) return null;
const diffMs = new Date(expiresAt).getTime() - Date.now();
@@ -80,9 +130,11 @@ function ResetCreditModalFooter({
function ResetCreditConfirmation({
credit,
provider,
tr,
}: {
credit: CodexResetCreditView;
provider: string;
tr: (key: string, fallback: string, values?: UsageTranslationValues) => string;
}) {
return (
@@ -95,15 +147,12 @@ function ResetCreditConfirmation({
{tr("confirmRedeemResetCreditTitle", "Redeem this reset credit?")}
</p>
<p className="mt-1 text-sm text-text-muted">
{tr(
"confirmRedeemResetCredit",
"Redeeming immediately resets the eligible Codex usage windows and permanently consumes this credit."
)}
{getResetCreditConfirmation(provider, credit.resetType, tr)}
</p>
</div>
</div>
</div>
<CreditSummary credit={credit} tr={tr} />
<CreditSummary credit={credit} provider={provider} tr={tr} />
</div>
);
}
@@ -113,12 +162,14 @@ function ResetCreditList({
credits,
loading,
onSelect,
provider,
tr,
}: {
availableCount: number;
credits: CodexResetCreditView[];
loading: boolean;
onSelect: (selectionToken: string) => void;
provider: string;
tr: (key: string, fallback: string, values?: UsageTranslationValues) => string;
}) {
if (credits.length === 0) {
@@ -141,7 +192,7 @@ function ResetCreditList({
key={credit.selectionToken}
className="flex flex-col gap-3 rounded-lg border border-border bg-bg-subtle/40 p-3 sm:flex-row sm:items-center"
>
<CreditSummary credit={credit} tr={tr} recommended={index === 0} />
<CreditSummary credit={credit} provider={provider} tr={tr} recommended={index === 0} />
<Button
size="sm"
className="shrink-0"
@@ -161,6 +212,7 @@ export default function CodexResetCreditsModal({
availableCount,
isOpen,
loading,
provider,
onClose,
onRedeem,
}: Props) {
@@ -196,7 +248,11 @@ export default function CodexResetCreditsModal({
<Modal
isOpen={isOpen}
onClose={close}
title={tr("resetCreditsModalTitle", "Codex reset credits")}
title={
isGlmResetProvider(provider)
? tr("glmResetCreditsModalTitle", "GLM Coding Plan reset cards")
: tr("resetCreditsModalTitle", "Codex reset credits")
}
size="lg"
closeOnOverlay={!loading}
footer={
@@ -211,20 +267,26 @@ export default function CodexResetCreditsModal({
}
>
{confirming && selectedCredit ? (
<ResetCreditConfirmation credit={selectedCredit} tr={tr} />
<ResetCreditConfirmation credit={selectedCredit} provider={provider} tr={tr} />
) : (
<div className="space-y-4">
<p className="text-sm text-text-muted">
{tr(
"resetCreditsModalExplainer",
"Credits are ordered by expiration. Automatic redemption always uses the credit that expires first."
)}
{isGlmResetProvider(provider)
? tr(
"glmResetCreditsModalExplainer",
"Reset cards are ordered by expiration. Choose the 5-hour or weekly window you want to reset."
)
: tr(
"resetCreditsModalExplainer",
"Credits are ordered by expiration. Automatic redemption always uses the credit that expires first."
)}
</p>
<ResetCreditList
availableCount={availableCount}
credits={credits}
loading={loading}
onSelect={beginRedeem}
provider={provider}
tr={tr}
/>
</div>
@@ -235,10 +297,12 @@ export default function CodexResetCreditsModal({
function CreditSummary({
credit,
provider,
recommended = false,
tr,
}: {
credit: CodexResetCreditView;
provider: string;
recommended?: boolean;
tr: (key: string, fallback: string, values?: UsageTranslationValues) => string;
}) {
@@ -250,7 +314,7 @@ function CreditSummary({
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium text-text-main">
{credit.title || tr("resetCreditDefaultTitle", "Full reset")}
{getResetCreditWindowTitle(provider, credit, tr)}
</span>
{recommended && (
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold text-primary">

View File

@@ -17,7 +17,10 @@ import {
import Card from "@/shared/components/Card";
import { CardSkeleton } from "@/shared/components/Loading";
import { pickDisplayValue } from "@/shared/utils/maskEmail";
import { supportsProviderQuota, isProviderQuotaVisible } from "@/shared/utils/providerQuotaVisibility";
import {
supportsProviderQuota,
isProviderQuotaVisible,
} from "@/shared/utils/providerQuotaVisibility";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import { useNotificationStore } from "@/store/notificationStore";
@@ -1110,6 +1113,7 @@ export default function ProviderLimits({
credits={resetCreditRedemption.resetCreditPicker.credits}
availableCount={resetCreditRedemption.resetCreditPicker.availableCount}
loading={resetCreditRedemption.redeemingResetCreditId !== null}
provider={resetCreditRedemption.resetCreditPicker.provider}
onClose={resetCreditRedemption.closeResetCreditPicker}
onRedeem={resetCreditRedemption.redeemCodexResetCredit}
/>

View File

@@ -21,10 +21,10 @@ const PROVIDER_COLUMNS: Record<string, string[]> = {
"banked_reset_credits",
],
claude: ["session", "weekly"],
glm: ["session", "weekly", "mcp_monthly"],
"glm-cn": ["session", "weekly", "mcp_monthly"],
glmt: ["session", "weekly", "mcp_monthly"],
zai: ["session", "weekly", "mcp_monthly"],
glm: ["session", "weekly", "mcp_monthly", "banked_reset_credits"],
"glm-cn": ["session", "weekly", "mcp_monthly", "banked_reset_credits"],
glmt: ["session", "weekly", "mcp_monthly", "banked_reset_credits"],
zai: ["session", "weekly", "mcp_monthly", "banked_reset_credits"],
github: ["chat", "completions", "premium_interactions"],
minimax: ["session"],
"minimax-cn": ["session"],

View File

@@ -10,7 +10,7 @@ const CODEX_QUOTA_ORDER: Record<string, number> = {
gpt_5_3_codex_spark_weekly: 3,
banked_reset_credits: 4,
};
const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "opencode-go"];
const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "zai", "opencode-go"];
const KIMI_CODING_PROVIDERS: readonly string[] = getProviderConnectionFamilyIds("kimi-coding");
/**
@@ -163,7 +163,7 @@ function parseGithub(data: any) {
}
function parseGlmFamily(data: any) {
return quotaEntries(data).map(([name, quota]) =>
const quotas = quotaEntries(data).map(([name, quota]) =>
normalizeQuotaEntry(name, quota, {
displayName: quota?.displayName,
details: Array.isArray(quota?.details) ? quota.details : undefined,
@@ -171,6 +171,14 @@ function parseGlmFamily(data: any) {
Number(quota?.total || 0) === 100 && quota?.remainingPercentage !== undefined,
})
);
// GLM Coding Plan Reset Cards, surfaced by getGlmUsage alongside the windows.
const bankedResetCredits = Number(data?.bankedResetCredits);
if (Number.isFinite(bankedResetCredits) && bankedResetCredits > 0) {
quotas.push(buildBankedResetCreditsQuota(bankedResetCredits));
}
return quotas;
}
function buildCreditsQuota(
@@ -467,7 +475,7 @@ function looksLikeMoonshotBalance(data: any): boolean {
function parseProviderQuotas(providerId: string, data: any) {
if (looksLikeMoonshotBalance(data)) return parseMoonshotBalance(data);
if (providerId === "github") return parseGithub(data);
if (["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return parseGlmFamily(data);
if (GLM_FAMILY_PROVIDERS.includes(providerId)) return parseGlmFamily(data);
if (providerId === "antigravity" || providerId === "agy") return parseAntigravity(data);
if (providerId === "codex") return parseCodex(data);
if (providerId === "claude") return parseClaude(data);

View File

@@ -3,7 +3,7 @@
import { useCallback, useRef, useState } from "react";
import { useNotificationStore } from "@/store/notificationStore";
import { parseQuotaData } from "./utils";
import { canProviderRedeemResetCredit, getResetCreditEndpoint, parseQuotaData } from "./utils";
import type { UsageTranslationValues } from "./i18nFallback";
type TranslateUsage = (key: string, fallback: string, values?: UsageTranslationValues) => string;
@@ -43,18 +43,28 @@ interface ResetCreditRequestState {
// Module-level so the ref-store mutation stays outside any hook body — the
// immutability rule bars in-callback writes to `state.idempotencyKeysRef.current`.
function resetIdempotencyKeys(keys: React.MutableRefObject<Record<string, string>>): void {
keys.current = {};
function resetIdempotencyKey(
keys: React.MutableRefObject<Record<string, string>>,
connectionId: string,
selectionToken: string
): void {
delete keys.current[`${connectionId}:${selectionToken}`];
}
function ensureIdempotencyKey(
keys: React.MutableRefObject<Record<string, string>>,
connectionId: string,
selectionToken: string
): string {
const existing = keys.current[selectionToken];
const key = `${connectionId}:${selectionToken}`;
const existing = keys.current[key];
if (existing) return existing;
for (const storedKey of Object.keys(keys.current)) {
if (storedKey.startsWith(`${connectionId}:`)) delete keys.current[storedKey];
}
const created = createIdempotencyKey();
keys.current[selectionToken] = created;
keys.current[key] = created;
return created;
}
@@ -68,20 +78,38 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error && error.message ? error.message : fallback;
}
export function applyCommittedResetCreditFallback(entry: any): any {
if (!entry) return entry;
const quotas = Array.isArray(entry.quotas)
? entry.quotas.flatMap((quota: any) => {
if (!quota?.isResetCredits) return [quota];
const count = Math.max(0, Number(quota.creditCount ?? quota.remaining ?? 0) - 1);
return count > 0 ? [{ ...quota, creditCount: count, remaining: count }] : [];
})
: entry.quotas;
const raw =
entry.raw && typeof entry.raw === "object"
? {
...entry.raw,
bankedResetCredits: Math.max(0, Number(entry.raw.bankedResetCredits ?? 0) - 1),
}
: entry.raw;
return { ...entry, quotas, raw };
}
function useOpenCodexResetCredits(
loadingResetCreditsId: string | null,
redeemingResetCreditId: string | null,
setErrors: SetErrors,
setLoadingResetCreditsId: React.Dispatch<React.SetStateAction<string | null>>,
setResetCreditPicker: React.Dispatch<React.SetStateAction<ResetCreditPickerState | null>>,
idempotencyKeysRef: React.MutableRefObject<Record<string, string>>,
tr: TranslateUsage
) {
const notify = useNotificationStore();
return useCallback(
async (connectionId: string, provider: string) => {
if (
(provider !== "codex" && provider !== "grok-cli") ||
!canProviderRedeemResetCredit(provider) ||
loadingResetCreditsId ||
redeemingResetCreditId
)
@@ -89,13 +117,14 @@ function useOpenCodexResetCredits(
setLoadingResetCreditsId(connectionId);
setErrors((prev) => ({ ...prev, [connectionId]: null }));
try {
const endpoint = getResetCreditEndpoint(provider);
if (!endpoint) return;
const response = await fetch(
`/api/usage/codex-reset-credit?connectionId=${encodeURIComponent(connectionId)}`,
`${endpoint}?connectionId=${encodeURIComponent(connectionId)}`,
{ cache: "no-store" }
);
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || response.statusText);
idempotencyKeysRef.current = {};
setResetCreditPicker({
connectionId,
provider,
@@ -116,7 +145,6 @@ function useOpenCodexResetCredits(
}
},
[
idempotencyKeysRef,
loadingResetCreditsId,
notify,
redeemingResetCreditId,
@@ -134,11 +162,17 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
async (selectionToken: string) => {
const picker = state.resetCreditPicker;
if (!picker || state.redeemingResetCreditId || !selectionToken) return;
const idempotencyKey = ensureIdempotencyKey(state.idempotencyKeysRef, selectionToken);
const idempotencyKey = ensureIdempotencyKey(
state.idempotencyKeysRef,
picker.connectionId,
selectionToken
);
state.setRedeemingResetCreditId(picker.connectionId);
state.setErrors((prev) => ({ ...prev, [picker.connectionId]: null }));
try {
const response = await fetch("/api/usage/codex-reset-credit", {
const endpoint = getResetCreditEndpoint(picker.provider);
if (!endpoint) return;
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -149,23 +183,36 @@ function useRedeemCodexResetCredit(state: ResetCreditRequestState) {
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || response.statusText);
const usage = data.usage || {};
state.setQuotaData((prev) => ({
...prev,
[picker.connectionId]: {
quotas: parseQuotaData(picker.provider, usage),
plan: usage.plan || null,
message: usage.message || null,
raw: usage,
stale: usage._stale ? { since: usage._staleSince, reason: usage._staleReason } : null,
},
}));
const refreshPending = data.refreshPending === true && !data.usage;
if (refreshPending) {
// The upstream redemption already committed but the post-commit quota
// refresh failed: keep the existing windows and only decrement the
// consumed reset-credit count so success is not reported as failure.
state.setQuotaData((prev) => {
const entry = prev[picker.connectionId];
return entry
? { ...prev, [picker.connectionId]: applyCommittedResetCreditFallback(entry) }
: prev;
});
} else {
const usage = data.usage || {};
state.setQuotaData((prev) => ({
...prev,
[picker.connectionId]: {
quotas: parseQuotaData(picker.provider, usage),
plan: usage.plan || null,
message: usage.message || null,
raw: usage,
stale: usage._stale ? { since: usage._staleSince, reason: usage._staleReason } : null,
},
}));
}
state.setLastRefreshedAt((prev) => ({
...prev,
[picker.connectionId]: new Date().toISOString(),
}));
state.setResetCreditPicker(null);
resetIdempotencyKeys(state.idempotencyKeysRef);
resetIdempotencyKey(state.idempotencyKeysRef, picker.connectionId, selectionToken);
notify.success(state.tr("resetCreditRedeemed", "Reset redeemed"));
} catch (error) {
const message = getRequestErrorMessage(
@@ -199,7 +246,6 @@ export function useCodexResetCreditRedemption(
setErrors,
setLoadingResetCreditsId,
setResetCreditPicker,
idempotencyKeysRef,
tr
);
const redeemCodexResetCredit = useRedeemCodexResetCredit({

View File

@@ -337,9 +337,37 @@ export function computeCanEditCutoff(quotas: any[]): boolean {
return quotas.some((q: any) => q && typeof q.name === "string" && !q.isCredits);
}
/**
* Providers that can redeem a banked reset credit: Codex reset credits and the GLM
* Coding Plan Reset Cards, which share the list/redeem contract behind different routes.
*/
export const RESET_CREDIT_PROVIDERS = [
"codex",
// grok-cli redeems through the same codex-reset-credit endpoint (tip of
// release/v3.8.51); kept here so the generalized gate below covers it.
"grok-cli",
"glm",
"glm-cn",
"glmt",
"zai",
] as const;
/** The redemption API backing a provider's reset credits, if supported. */
export function getResetCreditEndpoint(provider: string): string | null {
if (provider === "codex" || provider === "grok-cli") return "/api/usage/codex-reset-credit";
if (["glm", "glm-cn", "glmt", "zai"].includes(provider)) {
return "/api/usage/glm-reset-card";
}
return null;
}
export function canProviderRedeemResetCredit(provider: string): boolean {
return (RESET_CREDIT_PROVIDERS as readonly string[]).includes(provider);
}
export function computeCanRedeemResetCredit(provider: string, quotas: any[]): boolean {
return (
(provider === "codex" || provider === "grok-cli") &&
canProviderRedeemResetCredit(provider) &&
quotas.some((q: any) => q?.isResetCredits && Number(q.creditCount ?? q.remaining ?? 0) > 0)
);
}

View File

@@ -0,0 +1,78 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import {
GlmResetCardError,
consumeGlmResetCard,
listGlmResetCards,
} from "@/lib/usage/glmResetCards";
const ConnectionIdSchema = z.string().trim().min(1).max(256);
const GlmResetCardBodySchema = z.object({
connectionId: ConnectionIdSchema,
idempotencyKey: z.string().trim().min(1).max(256),
creditId: z.string().trim().min(1).max(512).optional(),
});
function buildErrorResponse(error: unknown) {
const status = error instanceof GlmResetCardError ? error.status : 500;
const code = error instanceof GlmResetCardError ? error.code : "glm_reset_card_failed";
const message =
error instanceof GlmResetCardError
? sanitizeErrorMessage(error.message) || "GLM reset-card request failed."
: "GLM reset-card request failed.";
console.error("[API] GLM reset-card request failed", {
status,
code,
message,
});
return NextResponse.json({ ok: false, code, error: message }, { status });
}
export async function GET(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const parsed = ConnectionIdSchema.safeParse(
new URL(request.url).searchParams.get("connectionId")
);
if (!parsed.success) {
return NextResponse.json(
{ ok: false, code: "invalid_connection_id", error: "Invalid connectionId." },
{ status: 400 }
);
}
const result = await listGlmResetCards(parsed.data);
return NextResponse.json({ ok: true, ...result });
} catch (error) {
return buildErrorResponse(error);
}
}
export async function POST(request: Request) {
const authError = await requireManagementAuth(request);
if (authError) return authError;
try {
const raw = await request.json().catch(() => ({}));
const parsed = GlmResetCardBodySchema.safeParse(raw);
if (!parsed.success) {
return NextResponse.json(
{ ok: false, code: "invalid_request_body", error: "Invalid request body." },
{ status: 400 }
);
}
const result = await consumeGlmResetCard(
parsed.data.connectionId,
parsed.data.idempotencyKey,
parsed.data.creditId
);
return NextResponse.json({ ok: true, ...result });
} catch (error) {
return buildErrorResponse(error);
}
}

View File

@@ -9503,7 +9503,7 @@
"redeemResetCredit": "Redeem reset",
"manageResetCredits": "View credits",
"viewResetCredits": "View reset credits",
"resetCreditsModalTitle": "Codex reset credits",
"resetCreditsModalTitle": "Reset credits",
"resetCreditsModalExplainer": "Credits are ordered by expiration. Automatic redemption always uses the credit that expires first.",
"resetCreditsLoadFailed": "Failed to load reset credits",
"resetCreditsDetailsUnavailable": "Credit details are currently unavailable. Refresh and try again.",
@@ -9514,10 +9514,17 @@
"resetCreditNoExpiry": "No expiration date",
"redeemThisResetCredit": "Redeem",
"confirmRedeemResetCreditTitle": "Redeem this reset credit?",
"confirmRedeemResetCredit": "Redeeming immediately resets the eligible Codex usage windows and permanently consumes this credit.",
"confirmRedeemResetCredit": "Redeeming immediately resets the eligible usage windows and permanently consumes this credit.",
"confirmRedeemResetCreditButton": "Redeem credit",
"resetCreditRedeemed": "Reset redeemed",
"resetCreditRedeemFailed": "Failed to redeem reset credit",
"glmResetCreditsModalTitle": "GLM Coding Plan reset cards",
"glmResetCreditsModalExplainer": "Reset cards are ordered by expiration. Choose the 5-hour or weekly window you want to reset.",
"glmResetCreditFiveHourTitle": "5-hour window reset",
"glmResetCreditWeekTitle": "Weekly window reset",
"glmConfirmRedeemResetCredit": "Redeeming immediately resets the selected usage window and permanently consumes this card.",
"glmConfirmRedeemFiveHourResetCredit": "Redeeming immediately resets the 5-hour usage window and permanently consumes this card.",
"glmConfirmRedeemWeekResetCredit": "Redeeming immediately resets the weekly usage window and permanently consumes this card.",
"suiteBuilderSaveFailed": "Failed to save suite",
"clone": "Clone",
"exportSuite": "Export",

View File

@@ -9516,6 +9516,13 @@
"confirmRedeemResetCreditTitle": "Resgatar este crédito de redefinição?",
"confirmRedeemResetCredit": "O resgate redefine imediatamente as janelas de uso elegíveis do Codex e consome este crédito de forma permanente.",
"confirmRedeemResetCreditButton": "Resgatar crédito",
"glmResetCreditsModalTitle": "Cartões de redefinição do GLM Coding Plan",
"glmResetCreditsModalExplainer": "Os cartões de redefinição estão ordenados por expiração. Escolha a janela de 5 horas ou semanal que deseja redefinir.",
"glmResetCreditFiveHourTitle": "Redefinição da janela de 5 horas",
"glmResetCreditWeekTitle": "Redefinição da janela semanal",
"glmConfirmRedeemResetCredit": "O resgate redefine imediatamente a janela de uso selecionada e consome este cartão de forma permanente.",
"glmConfirmRedeemFiveHourResetCredit": "O resgate redefine imediatamente a janela de uso de 5 horas e consome este cartão de forma permanente.",
"glmConfirmRedeemWeekResetCredit": "O resgate redefine imediatamente a janela de uso semanal e consome este cartão de forma permanente.",
"resetCreditRedeemed": "Redefinição resgatada",
"resetCreditRedeemFailed": "Falha ao resgatar crédito de redefinição",
"suiteBuilderSaveFailed": "Falha ao salvar a suíte customizada",

View File

@@ -9516,6 +9516,13 @@
"confirmRedeemResetCreditTitle": "Đổi tín dụng đặt lại này?",
"confirmRedeemResetCredit": "Đổi một tín dụng đặt lại Codex cho tài khoản này? Thao tác này sẽ tiêu tốn một tín dụng đặt lại.",
"confirmRedeemResetCreditButton": "Đổi tín dụng",
"glmResetCreditsModalTitle": "Thẻ đặt lại của GLM Coding Plan",
"glmResetCreditsModalExplainer": "Thẻ đặt lại được sắp xếp theo thời gian hết hạn. Chọn cửa sổ 5 giờ hoặc hàng tuần bạn muốn đặt lại.",
"glmResetCreditFiveHourTitle": "Đặt lại cửa sổ 5 giờ",
"glmResetCreditWeekTitle": "Đặt lại cửa sổ hàng tuần",
"glmConfirmRedeemResetCredit": "Việc đổi sẽ đặt lại ngay cửa sổ sử dụng đã chọn và tiêu tốn vĩnh viễn thẻ này.",
"glmConfirmRedeemFiveHourResetCredit": "Việc đổi sẽ đặt lại ngay cửa sổ sử dụng 5 giờ và tiêu tốn vĩnh viễn thẻ này.",
"glmConfirmRedeemWeekResetCredit": "Việc đổi sẽ đặt lại ngay cửa sổ sử dụng hàng tuần và tiêu tốn vĩnh viễn thẻ này.",
"resetCreditRedeemed": "Đã đổi lượt đặt lại",
"resetCreditRedeemFailed": "Không thể đổi tín dụng đặt lại",
"suiteBuilderSaveFailed": "Không thể lưu bộ đánh giá",

View File

@@ -0,0 +1,442 @@
import { randomBytes } from "node:crypto";
import {
acquireExclusiveConnectionLease,
releaseExclusiveConnectionLease,
} from "@/lib/db/exclusiveConnectionLeases";
import { getProviderConnectionById } from "@/lib/db/providers";
import { resolveProxyForConnection } from "@/lib/db/settings";
import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation";
import { fetchAndPersistProviderLimits } from "@/lib/usage/providerLimits";
import {
fetchGlmResetCardList,
getGlmResetCardEnvelopeMessage,
getGlmResetCardEnvelopeStatus,
isGlmResetCardEnvelopeOk,
isGlmResetCardListEnvelopeOk,
parseGlmResetCards,
redeemGlmResetCard,
type GlmResetCard,
} from "@omniroute/open-sse/services/usage/glmResetCards.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import {
runWithDirectFetchContext,
runWithProxyContext,
} from "@omniroute/open-sse/utils/proxyFetch.ts";
type JsonRecord = Record<string, unknown>;
export const GLM_RESET_CARD_PROVIDERS = ["glm", "glm-cn", "glmt", "zai"] as const;
const ATTEMPT_TTL_MS = 10 * 60_000;
const MAX_ATTEMPTS = 500;
// Short-lived synthetic operation lease: `exclusive_connection_leases` has no FK on
// api_key_id, so a namespaced synthetic id is schema-legal. The partial unique index
// on (connection_id) WHERE state = 'ACTIVE' gives an atomic fence around the wire
// operations that no check-then-act availability probe can provide.
const OPERATION_LEASE_TTL_MS = 60_000;
const OPERATION_LEASE_API_KEY_ID = "omniroute:glm-reset-card-operation";
const OPERATION_LEASE_OWNER_PREFIX = "vlo_";
type ProxyLike = Parameters<typeof runWithProxyContext>[0];
/** A connection with no proxy must go explicitly direct — passing null to
* runWithProxyContext would inherit an ambient context instead. */
function runWithConnectionFetch<T>(proxy: ProxyLike, fn: () => T): T {
return proxy ? runWithProxyContext(proxy, fn) : runWithDirectFetchContext(fn);
}
/** Map transport-layer proxy failures to safe typed errors. Public messages are
* static: the underlying error text can embed proxy host:port topology. */
function toSafeTransportError(error: unknown, fallback: GlmResetCardError): GlmResetCardError {
const err = error as { code?: unknown; errorCode?: unknown };
const code = typeof err?.code === "string" ? err.code : null;
const errorCode = typeof err?.errorCode === "string" ? err.errorCode : null;
if (code === "PROXY_UNREACHABLE" || errorCode === "proxy_unreachable") {
return new GlmResetCardError(503, "proxy_unreachable", "The connection proxy is unreachable.");
}
if (code === "PROXY_FAMILY_UNAVAILABLE" || errorCode === "proxy_family_unavailable") {
return new GlmResetCardError(
502,
"proxy_family_unavailable",
"No proxy is available for this request."
);
}
if (code === "PROXY_REQUEST_FAILED" || errorCode === "proxy_request_failed") {
return new GlmResetCardError(502, "proxy_request_failed", "The proxy request failed.");
}
if (code === "RELAY_TIMEOUT" || errorCode === "relay_timeout") {
return new GlmResetCardError(504, "relay_timeout", "The proxied request timed out.");
}
return fallback;
}
/** Atomically claim the connection for one list/use operation. */
async function withOperationLease<T>(
connection: GlmConnectionLike,
operation: () => Promise<T>
): Promise<T> {
const leaseOwnerId = OPERATION_LEASE_OWNER_PREFIX + randomBytes(32).toString("base64url");
const acquired = acquireExclusiveConnectionLease({
leaseOwnerId,
apiKeyId: OPERATION_LEASE_API_KEY_ID,
provider: connection.provider,
connectionId: connection.id,
ttlMs: OPERATION_LEASE_TTL_MS,
});
if (acquired.kind === "CONNECTION_BUSY" || acquired.kind === "OWNER_ALREADY_ACTIVE") {
throw new GlmResetCardError(
409,
"exclusive_lease_active",
"Reset-card operations are deferred while an exclusive lease is active."
);
}
try {
return await operation();
} finally {
releaseExclusiveConnectionLease({
leaseOwnerId,
generation: acquired.lease.generation,
apiKeyId: OPERATION_LEASE_API_KEY_ID,
});
}
}
type GlmConnectionLike = JsonRecord & {
id: string;
provider: string;
apiKey?: string;
providerSpecificData?: JsonRecord;
};
export type PublicGlmResetCard = Omit<GlmResetCard, "id"> & {
selectionToken: string;
};
export interface GlmResetCardListResult {
credits: PublicGlmResetCard[];
availableCount: number;
lastFiveHourResetAt: string | null;
lastWeekResetAt: string | null;
}
export interface GlmResetCardConsumeResult {
outcome: "reset";
usage?: JsonRecord;
refreshPending?: true;
}
interface RedemptionAttempt {
requestedSelection: string | null;
card?: GlmResetCard;
inFlight?: Promise<GlmResetCardConsumeResult>;
committed?: GlmResetCardConsumeResult;
expiresAt: number;
}
const redemptionAttempts = new Map<string, RedemptionAttempt>();
export class GlmResetCardError extends Error {
status: number;
code: string;
constructor(status: number, code: string, message: string) {
super(message);
this.name = "GlmResetCardError";
this.status = status;
this.code = code;
}
}
export function isGlmResetCardProvider(provider: unknown): boolean {
return (
typeof provider === "string" &&
(GLM_RESET_CARD_PROVIDERS as readonly string[]).includes(provider)
);
}
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toPublicCard(card: GlmResetCard): PublicGlmResetCard {
const { id, ...metadata } = card;
return { ...metadata, selectionToken: id };
}
function getProviderSpecificData(connection: GlmConnectionLike): JsonRecord {
return {
...toRecord(connection.providerSpecificData),
...(connection.provider === "glm-cn" ? { apiRegion: "china" } : {}),
};
}
async function loadGlmConnection(connectionId: string): Promise<GlmConnectionLike> {
if (await isConnectionUnavailableToAuxiliaryActivity(connectionId)) {
throw new GlmResetCardError(
409,
"exclusive_lease_active",
"Reset-card operations are deferred while an exclusive lease is active."
);
}
const connection = (await getProviderConnectionById(
connectionId
)) as unknown as GlmConnectionLike | null;
if (!connection) {
throw new GlmResetCardError(404, "connection_not_found", "Connection not found.");
}
if (!isGlmResetCardProvider(connection.provider)) {
throw new GlmResetCardError(
400,
"glm_provider_required",
"Reset cards can only be redeemed for GLM coding-plan accounts."
);
}
if (!connection.apiKey) {
throw new GlmResetCardError(
401,
"glm_api_key_missing",
"GLM coding-plan API key is missing on this connection."
);
}
return connection;
}
function assertEnvelopeOk(
payload: unknown,
httpStatus: number,
fallbackMessage: string,
requireListData = false
): void {
const envelopeOk = requireListData
? isGlmResetCardListEnvelopeOk(payload)
: isGlmResetCardEnvelopeOk(payload);
if (envelopeOk && httpStatus < 400) return;
const status = getGlmResetCardEnvelopeStatus(payload, httpStatus);
if (status === 401 || status === 403) {
throw new GlmResetCardError(
401,
"glm_reset_card_unauthorized",
"The GLM API key was rejected by the reset-card API."
);
}
const upstreamMessage = getGlmResetCardEnvelopeMessage(payload);
throw new GlmResetCardError(
status >= 400 ? status : 502,
"glm_reset_card_upstream_error",
sanitizeErrorMessage(upstreamMessage) || fallbackMessage
);
}
function pruneAttempts(now = Date.now()): void {
for (const [key, attempt] of redemptionAttempts) {
if (!attempt.inFlight && attempt.expiresAt <= now) redemptionAttempts.delete(key);
}
if (redemptionAttempts.size <= MAX_ATTEMPTS) return;
for (const [key, attempt] of redemptionAttempts) {
if (!attempt.inFlight) redemptionAttempts.delete(key);
if (redemptionAttempts.size <= MAX_ATTEMPTS) break;
}
}
function attemptKey(connectionId: string, idempotencyKey: string): string {
return `${connectionId}:${idempotencyKey}`;
}
function normalizeSelection(selectionToken?: string): string | null {
return typeof selectionToken === "string" && selectionToken.trim() ? selectionToken.trim() : null;
}
function assertCompatibleAttempt(
attempt: RedemptionAttempt,
requestedSelection: string | null
): void {
if (attempt.requestedSelection !== requestedSelection) {
throw new GlmResetCardError(
409,
"idempotency_key_conflict",
"This idempotency key is already bound to a different reset-card selection."
);
}
}
async function refreshAfterCommit(connectionId: string): Promise<GlmResetCardConsumeResult> {
try {
const refreshed = await fetchAndPersistProviderLimits(connectionId, "manual", {
allowRotatingRefresh: true,
});
return { outcome: "reset", usage: refreshed.usage };
} catch {
// Redemption is already irreversible. The caller can preserve its current
// quotas and refresh later instead of turning committed success into a 500.
return { outcome: "reset", refreshPending: true };
}
}
export async function listGlmResetCards(connectionId: string): Promise<GlmResetCardListResult> {
if (!connectionId || typeof connectionId !== "string") {
throw new GlmResetCardError(400, "connection_id_required", "connectionId is required.");
}
try {
const connection = await loadGlmConnection(connectionId);
const proxyInfo = await resolveProxyForConnection(connection.id);
const { response, payload } = await withOperationLease(connection, () =>
runWithConnectionFetch(proxyInfo?.proxy ?? null, () =>
fetchGlmResetCardList(connection.apiKey as string, getProviderSpecificData(connection))
)
);
assertEnvelopeOk(payload, response.status, "The GLM reset-card API returned an error.", true);
const parsed = parseGlmResetCards(payload);
return {
credits: parsed.cards.map(toPublicCard),
availableCount: parsed.availableCount,
lastFiveHourResetAt: parsed.lastFiveHourResetAt,
lastWeekResetAt: parsed.lastWeekResetAt,
};
} catch (error) {
if (error instanceof GlmResetCardError) throw error;
throw toSafeTransportError(
error,
new GlmResetCardError(500, "glm_reset_card_list_failed", "Failed to load GLM reset cards.")
);
}
}
async function executeRedemption(
attempt: RedemptionAttempt,
connection: GlmConnectionLike,
requestId: string,
proxyConfig: ProxyLike
): Promise<void> {
const providerSpecificData = getProviderSpecificData(connection);
const apiKey = connection.apiKey as string;
if (!attempt.card) {
const listed = await runWithConnectionFetch(proxyConfig, () =>
fetchGlmResetCardList(apiKey, providerSpecificData)
);
assertEnvelopeOk(
listed.payload,
listed.response.status,
"The GLM reset-card API returned an error.",
true
);
const { cards } = parseGlmResetCards(listed.payload);
const card = attempt.requestedSelection
? cards.find((entry) => entry.id === attempt.requestedSelection)
: cards[0];
if (!card) {
throw new GlmResetCardError(
409,
attempt.requestedSelection ? "selected_card_unavailable" : "no_reset_card",
attempt.requestedSelection
? "The selected GLM reset card is no longer available."
: "No GLM reset cards are available."
);
}
attempt.card = card;
}
// A transport rejection is ambiguous: z.ai may have committed the card before
// the response was lost. Keep `attempt.card`, and the next call will retry the
// exact same body/requestId without relisting.
const redeemed = await runWithConnectionFetch(proxyConfig, () =>
redeemGlmResetCard(apiKey, providerSpecificData, attempt.card as GlmResetCard, requestId)
);
assertEnvelopeOk(
redeemed.payload,
redeemed.response.status,
"The GLM reset-card API rejected the redemption."
);
}
export async function consumeGlmResetCard(
connectionId: string,
idempotencyKey: string,
selectionToken?: string
): Promise<GlmResetCardConsumeResult> {
if (!connectionId || typeof connectionId !== "string") {
throw new GlmResetCardError(400, "connection_id_required", "connectionId is required.");
}
if (!idempotencyKey || typeof idempotencyKey !== "string" || !idempotencyKey.trim()) {
throw new GlmResetCardError(400, "idempotency_key_required", "idempotencyKey is required.");
}
const requestId = idempotencyKey.trim();
const requestedSelection = normalizeSelection(selectionToken);
const key = attemptKey(connectionId, requestId);
pruneAttempts();
const activeAttempt = redemptionAttempts.get(key);
if (activeAttempt?.inFlight) {
assertCompatibleAttempt(activeAttempt, requestedSelection);
activeAttempt.expiresAt = Date.now() + ATTEMPT_TTL_MS;
return activeAttempt.inFlight;
}
try {
const connection = await loadGlmConnection(connectionId);
const proxyInfo = await resolveProxyForConnection(connection.id);
const existing = redemptionAttempts.get(key);
if (existing) {
assertCompatibleAttempt(existing, requestedSelection);
existing.expiresAt = Date.now() + ATTEMPT_TTL_MS;
if (existing.committed) return existing.committed;
if (existing.inFlight) return existing.inFlight;
}
const attempt: RedemptionAttempt = existing ?? {
requestedSelection,
expiresAt: Date.now() + ATTEMPT_TTL_MS,
};
redemptionAttempts.set(key, attempt);
const operation = (async () => {
await withOperationLease(connection, () =>
executeRedemption(attempt, connection, requestId, proxyInfo?.proxy ?? null)
);
return runWithConnectionFetch(proxyInfo?.proxy ?? null, () =>
refreshAfterCommit(connection.id)
);
})();
attempt.inFlight = operation;
try {
const result = await operation;
attempt.committed = result;
attempt.expiresAt = Date.now() + ATTEMPT_TTL_MS;
return result;
} catch (error) {
// Keep ambiguous transport failures after a card has been selected. A lease
// conflict is also a retryable local deferral, so it must not discard the
// selected card/requestId binding from a previous ambiguous attempt.
const retryableLeaseConflict =
error instanceof GlmResetCardError && error.code === "exclusive_lease_active";
if ((error instanceof GlmResetCardError && !retryableLeaseConflict) || !attempt.card) {
redemptionAttempts.delete(key);
} else {
attempt.expiresAt = Date.now() + ATTEMPT_TTL_MS;
}
throw error;
} finally {
attempt.inFlight = undefined;
}
} catch (error) {
if (error instanceof GlmResetCardError) throw error;
throw toSafeTransportError(
error,
new GlmResetCardError(500, "glm_reset_card_failed", "Failed to redeem GLM reset card.")
);
}
}

View File

@@ -3,6 +3,7 @@ import { sanitizeProviderBillingStatus } from "@/shared/utils/providerBilling";
import { GROK_BUILD_ADDITIONAL_CREDITS_URL } from "@/shared/utils/grokBilling";
const GROK_CLI_PROVIDER = "grok-cli";
const GLM_RESET_CARD_PROVIDERS = new Set(["glm", "glm-cn", "glmt", "zai"]);
type JsonRecord = Record<string, unknown>;
@@ -43,6 +44,18 @@ export function mergeProviderLimitsCacheEntry(
return previous;
}
// A GLM quota refresh omits `bankedResetCredits` when the auxiliary reset-card
// list request failed (fetchGlmResetCardCount → null). That "unknown" must not
// erase a previously known count; an explicit number (including an
// authoritative 0) always wins.
if (
GLM_RESET_CARD_PROVIDERS.has(provider) &&
next.bankedResetCredits === undefined &&
previous.bankedResetCredits !== undefined
) {
return { ...next, bankedResetCredits: previous.bankedResetCredits };
}
if (provider !== GROK_CLI_PROVIDER) return next;
const nextBilling = next.billing;