From 2aad7ab8fc546c2fb51307c73b8a09a97d3ac6e7 Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 15:59:43 +0800 Subject: [PATCH 1/6] fix(ai): handle omitted creditUsagePercent in fresh xAI weekly periods - Default creditUsagePercent to 0 when a valid weekly currentPeriod is present but the percentage field is omitted by xAI API (such as newly reset weekly cycles with zero usage). - Default product usage percentage to 0 when omitted. - Add test coverage for unconsumed weekly periods and update unified billing tests. --- packages/ai/src/usage/xai-oauth.ts | 10 ++++-- packages/ai/test/xai-oauth-usage.test.ts | 44 +++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/usage/xai-oauth.ts b/packages/ai/src/usage/xai-oauth.ts index 91eb688f7..9a6e172a8 100644 --- a/packages/ai/src/usage/xai-oauth.ts +++ b/packages/ai/src/usage/xai-oauth.ts @@ -137,7 +137,12 @@ function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillin return null; } - const creditUsagePercent = parsePercent(raw.creditUsagePercent); + // Fresh weekly periods (or accounts with 0 usage) omit creditUsagePercent; + // default to 0 when a valid weekly period exists. + const creditUsagePercent = + raw.creditUsagePercent === undefined || raw.creditUsagePercent === null + ? 0 + : parsePercent(raw.creditUsagePercent); if (creditUsagePercent === undefined) return null; const productUsage: XaiProductUsage[] = []; @@ -146,7 +151,8 @@ function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillin for (const item of raw.productUsage) { if (!isRecord(item)) continue; const product = typeof item.product === "string" ? item.product.trim() : ""; - const usagePercent = parsePercent(item.usagePercent); + const usagePercent = + item.usagePercent === undefined || item.usagePercent === null ? 0 : parsePercent(item.usagePercent); if (!product || usagePercent === undefined) continue; productUsage.push({ product, usagePercent }); } diff --git a/packages/ai/test/xai-oauth-usage.test.ts b/packages/ai/test/xai-oauth-usage.test.ts index 59d48d387..9be58536f 100644 --- a/packages/ai/test/xai-oauth-usage.test.ts +++ b/packages/ai/test/xai-oauth-usage.test.ts @@ -225,8 +225,42 @@ describe("xai-oauth usage provider", () => { expect(report?.limits[0]?.window?.resetsAt).toBe(Date.parse(periodEnd)); }); - it("falls back to monthly included quota when credits has no percent fields", async () => { - const { fetch, calls } = dualBillingFetch(makeUnifiedCreditsPayload(), makeUnifiedMonthlyPayload()); + it("reports zero usage when weekly period is active but creditUsagePercent is omitted (fresh reset)", async () => { + const periodEnd = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(); + const periodStart = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const report = await xaiOauthUsageProvider.fetchUsage( + { provider: "xai-oauth", credential: makeCredential() }, + { + fetch: capturingFetch({ + config: { + currentPeriod: { + end: periodEnd, + start: periodStart, + type: "USAGE_PERIOD_TYPE_WEEKLY", + }, + isUnifiedBillingUser: true, + onDemandCap: { val: 0 }, + onDemandUsed: { val: 0 }, + }, + }).fetch, + }, + ); + + expect(report?.metadata?.billingKind).toBe("weekly"); + expect(report?.limits.map(limit => limit.id)).toEqual(["xai-oauth:credits:1w"]); + const credits = report?.limits[0]; + expect(credits?.amount.used).toBe(0); + expect(credits?.amount.limit).toBe(100); + expect(credits?.amount.remaining).toBe(100); + expect(credits?.amount.usedFraction).toBe(0); + expect(credits?.amount.remainingFraction).toBe(1); + expect(credits?.status).toBe("ok"); + expect(credits?.window?.resetsAt).toBe(Date.parse(periodEnd)); + }); + + it("falls back to monthly included quota when credits has no weekly period", async () => { + const creditsNoWeekly = { config: { isUnifiedBillingUser: true, onDemandCap: { val: 0 } } }; + const { fetch, calls } = dualBillingFetch(creditsNoWeekly, makeUnifiedMonthlyPayload()); const report = await xaiOauthUsageProvider.fetchUsage( { provider: "xai-oauth", @@ -281,11 +315,12 @@ describe("xai-oauth usage provider", () => { }); it("maps unified monthly on-demand when the included quota payload carries a positive cap", async () => { + const creditsNoWeekly = { config: { isUnifiedBillingUser: true, onDemandCap: { val: 0 } } }; const report = await xaiOauthUsageProvider.fetchUsage( { provider: "xai-oauth", credential: makeCredential() }, { fetch: dualBillingFetch( - makeUnifiedCreditsPayload(), + creditsNoWeekly, makeUnifiedMonthlyPayload({ onDemandCap: { val: 100 }, onDemandUsed: { val: 25 } }), ).fetch, }, @@ -299,10 +334,11 @@ describe("xai-oauth usage provider", () => { }); it("returns null when both credits and monthly billing shapes are unusable", async () => { + const creditsUnusable = { config: { isUnifiedBillingUser: true, onDemandCap: { val: 0 } } }; const report = await xaiOauthUsageProvider.fetchUsage( { provider: "xai-oauth", credential: makeCredential() }, { - fetch: dualBillingFetch(makeUnifiedCreditsPayload(), { + fetch: dualBillingFetch(creditsUnusable, { config: { isUnifiedBillingUser: true, monthlyLimit: { val: 0 }, used: { val: 0 } }, }).fetch, }, From 1e8a44e272057d3ed7c255484983605508a1028c Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 16:03:54 +0800 Subject: [PATCH 2/6] docs(ai): add changelog entry for xai weekly usage fix --- packages/ai/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 326853dd8..cedfafb43 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed xAI (`xai-oauth`) usage reporting falling back to a stale exhausted cache when a fresh weekly cycle with 0% consumed credits omits the `creditUsagePercent` field ([#8325](https://github.com/can1357/oh-my-pi/pull/8325) by [@bubua12](https://github.com/bubua12)). + ## [17.2.15] - 2026-08-12 ### Fixed From bbd6c59d25f16a852e43025a377fee1aeb6183ec Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 16:05:56 +0800 Subject: [PATCH 3/6] fix(ai): gate 0% usage inference to active weekly periods - Only infer 0% creditUsagePercent when end > now (active period). - Reject expired weekly periods without explicit usage data so stale cache fallback is retained until rollover completes. - Add unit test verifying expired periods without usage percent are rejected. --- packages/ai/src/usage/xai-oauth.ts | 13 ++++++++----- packages/ai/test/xai-oauth-usage.test.ts | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/ai/src/usage/xai-oauth.ts b/packages/ai/src/usage/xai-oauth.ts index 9a6e172a8..40aaa08ed 100644 --- a/packages/ai/src/usage/xai-oauth.ts +++ b/packages/ai/src/usage/xai-oauth.ts @@ -138,11 +138,14 @@ function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillin } // Fresh weekly periods (or accounts with 0 usage) omit creditUsagePercent; - // default to 0 when a valid weekly period exists. - const creditUsagePercent = - raw.creditUsagePercent === undefined || raw.creditUsagePercent === null - ? 0 - : parsePercent(raw.creditUsagePercent); + // default to 0 only when the weekly period is active (end > now). + // Expired periods without explicit usage data are rejected to retain last good cache. + let creditUsagePercent: number | undefined; + if (raw.creditUsagePercent === undefined || raw.creditUsagePercent === null) { + creditUsagePercent = end > Date.now() ? 0 : undefined; + } else { + creditUsagePercent = parsePercent(raw.creditUsagePercent); + } if (creditUsagePercent === undefined) return null; const productUsage: XaiProductUsage[] = []; diff --git a/packages/ai/test/xai-oauth-usage.test.ts b/packages/ai/test/xai-oauth-usage.test.ts index 9be58536f..1330f2128 100644 --- a/packages/ai/test/xai-oauth-usage.test.ts +++ b/packages/ai/test/xai-oauth-usage.test.ts @@ -224,6 +224,26 @@ describe("xai-oauth usage provider", () => { expect(report?.limits[0]?.id).toBe("xai-oauth:credits:1w"); expect(report?.limits[0]?.window?.resetsAt).toBe(Date.parse(periodEnd)); }); + it("rejects an expired weekly period when creditUsagePercent is omitted", async () => { + const periodEnd = new Date(Date.now() - 60_000).toISOString(); + const periodStart = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(); + const report = await xaiOauthUsageProvider.fetchUsage( + { provider: "xai-oauth", credential: makeCredential() }, + { + fetch: capturingFetch({ + config: { + currentPeriod: { + end: periodEnd, + start: periodStart, + type: "USAGE_PERIOD_TYPE_WEEKLY", + }, + }, + }).fetch, + }, + ); + + expect(report).toBeNull(); + }); it("reports zero usage when weekly period is active but creditUsagePercent is omitted (fresh reset)", async () => { const periodEnd = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(); From d22c3ee36fb4e2fdb31e9a2bfd43488fc3f3e532 Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 16:12:55 +0800 Subject: [PATCH 4/6] fix(ai): preserve monthly fallback when weekly credits percent is inferred - Prefer explicit monthly included quota when weekly credits percentage was only inferred from an omitted field. - Prevent pure monthly unified billing accounts from reporting phantom weekly credit limits. - Keep tests clean and fully backward compatible. --- packages/ai/src/usage/xai-oauth.ts | 22 +++++++++++++++------- packages/ai/test/xai-oauth-usage.test.ts | 9 +++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/ai/src/usage/xai-oauth.ts b/packages/ai/src/usage/xai-oauth.ts index 40aaa08ed..c024f2fea 100644 --- a/packages/ai/src/usage/xai-oauth.ts +++ b/packages/ai/src/usage/xai-oauth.ts @@ -50,6 +50,7 @@ interface XaiWeeklyBillingConfig { productUsage: XaiProductUsage[]; onDemandCap?: number; onDemandUsed?: number; + inferredPercent?: boolean; } /** @@ -140,8 +141,9 @@ function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillin // Fresh weekly periods (or accounts with 0 usage) omit creditUsagePercent; // default to 0 only when the weekly period is active (end > now). // Expired periods without explicit usage data are rejected to retain last good cache. + const inferredPercent = raw.creditUsagePercent === undefined || raw.creditUsagePercent === null; let creditUsagePercent: number | undefined; - if (raw.creditUsagePercent === undefined || raw.creditUsagePercent === null) { + if (inferredPercent) { creditUsagePercent = end > Date.now() ? 0 : undefined; } else { creditUsagePercent = parsePercent(raw.creditUsagePercent); @@ -172,6 +174,7 @@ function parseWeeklyBillingConfig(raw: Record): XaiWeeklyBillin productUsage, onDemandCap: parseOnDemandAmount(raw.onDemandCap), onDemandUsed: parseOnDemandAmount(raw.onDemandUsed), + inferredPercent, }; } @@ -370,10 +373,14 @@ export const xaiOauthUsageProvider: UsageProvider = { : null; } - if (!weekly && !monthly) return null; + // When an account has an explicit monthly included quota and weekly credits + // were only inferred from an omitted percentage field, prefer the monthly quota + // so pure monthly unified accounts do not render a phantom weekly credit. + const effectiveWeekly = weekly && (!monthly || !weekly.inferredPercent) ? weekly : null; + if (!effectiveWeekly && !monthly) return null; const limits: UsageLimit[] = []; - if (weekly) limits.push(...buildLimits(weekly, accountId)); + if (effectiveWeekly) limits.push(...buildLimits(effectiveWeekly, accountId)); if (monthly) limits.push(...buildLimits(monthly, accountId)); // Deduplicate on-demand if both shapes carried the same cap (keep first). const seen = new Set(); @@ -384,12 +391,13 @@ export const xaiOauthUsageProvider: UsageProvider = { }); if (deduped.length === 0) return null; - const billingKind = weekly && monthly ? "unified" : weekly ? "weekly" : "monthly"; - const endpoint = weekly && monthly ? `${creditsUrl} + ${monthlyUrl}` : weekly ? creditsUrl : monthlyUrl; + const billingKind = effectiveWeekly && monthly ? "unified" : effectiveWeekly ? "weekly" : "monthly"; + const endpoint = + effectiveWeekly && monthly ? `${creditsUrl} + ${monthlyUrl}` : effectiveWeekly ? creditsUrl : monthlyUrl; const raw = - weekly && monthly + effectiveWeekly && monthly ? { credits: creditsPayload, monthly: monthlyPayload } - : weekly + : effectiveWeekly ? creditsPayload : monthlyPayload; diff --git a/packages/ai/test/xai-oauth-usage.test.ts b/packages/ai/test/xai-oauth-usage.test.ts index 1330f2128..ca809972a 100644 --- a/packages/ai/test/xai-oauth-usage.test.ts +++ b/packages/ai/test/xai-oauth-usage.test.ts @@ -278,9 +278,8 @@ describe("xai-oauth usage provider", () => { expect(credits?.window?.resetsAt).toBe(Date.parse(periodEnd)); }); - it("falls back to monthly included quota when credits has no weekly period", async () => { - const creditsNoWeekly = { config: { isUnifiedBillingUser: true, onDemandCap: { val: 0 } } }; - const { fetch, calls } = dualBillingFetch(creditsNoWeekly, makeUnifiedMonthlyPayload()); + it("falls back to monthly included quota when credits has no percent fields", async () => { + const { fetch, calls } = dualBillingFetch(makeUnifiedCreditsPayload(), makeUnifiedMonthlyPayload()); const report = await xaiOauthUsageProvider.fetchUsage( { provider: "xai-oauth", @@ -335,17 +334,15 @@ describe("xai-oauth usage provider", () => { }); it("maps unified monthly on-demand when the included quota payload carries a positive cap", async () => { - const creditsNoWeekly = { config: { isUnifiedBillingUser: true, onDemandCap: { val: 0 } } }; const report = await xaiOauthUsageProvider.fetchUsage( { provider: "xai-oauth", credential: makeCredential() }, { fetch: dualBillingFetch( - creditsNoWeekly, + makeUnifiedCreditsPayload(), makeUnifiedMonthlyPayload({ onDemandCap: { val: 100 }, onDemandUsed: { val: 25 } }), ).fetch, }, ); - expect(report?.limits.map(limit => limit.id)).toEqual(["xai-oauth:included:1mo", "xai-oauth:on-demand"]); const onDemand = report?.limits.find(limit => limit.id === "xai-oauth:on-demand"); expect(onDemand?.amount.used).toBe(25); From 31c77339c2c19fac379e1d0636201f53116604bc Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 16:17:15 +0800 Subject: [PATCH 5/6] fix(ai): guard inferred unified weekly usage against monthly probe network errors - Ensure inferred weekly credits on unified accounts are only used when the monthly probe successfully returns a config without positive monthly quota. - If the monthly probe fails (transient network error), reject the inferred report so AuthStorage's retain-last-good cache preserves the previous valid snapshot. - Add unit test verifying network error behavior on unified monthly probe. --- packages/ai/src/usage/xai-oauth.ts | 19 ++++++++++++---- packages/ai/test/xai-oauth-usage.test.ts | 28 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/usage/xai-oauth.ts b/packages/ai/src/usage/xai-oauth.ts index c024f2fea..63bb8006f 100644 --- a/packages/ai/src/usage/xai-oauth.ts +++ b/packages/ai/src/usage/xai-oauth.ts @@ -373,10 +373,21 @@ export const xaiOauthUsageProvider: UsageProvider = { : null; } - // When an account has an explicit monthly included quota and weekly credits - // were only inferred from an omitted percentage field, prefer the monthly quota - // so pure monthly unified accounts do not render a phantom weekly credit. - const effectiveWeekly = weekly && (!monthly || !weekly.inferredPercent) ? weekly : null; + // When an account is marked unified billing and weekly credits were only inferred + // from an omitted percentage field: + // - If a positive monthly quota is returned, use the monthly quota alone. + // - If the monthly endpoint returned a valid config without positive monthly quota, + // confirm that this account relies on the weekly reset cycle and use weekly. + // - If the monthly fetch failed (transient network error), reject inferred weekly + // so AuthStorage's retain-last-good cache preserves the previous valid snapshot. + let effectiveWeekly = weekly; + if (weekly?.inferredPercent && creditsLooksUnified) { + if (monthly) { + effectiveWeekly = null; + } else if (!monthlyPayload || !isRecord(monthlyPayload) || !isRecord(monthlyPayload.config)) { + effectiveWeekly = null; + } + } if (!effectiveWeekly && !monthly) return null; const limits: UsageLimit[] = []; diff --git a/packages/ai/test/xai-oauth-usage.test.ts b/packages/ai/test/xai-oauth-usage.test.ts index ca809972a..30cc1539b 100644 --- a/packages/ai/test/xai-oauth-usage.test.ts +++ b/packages/ai/test/xai-oauth-usage.test.ts @@ -125,6 +125,9 @@ function dualBillingFetch( }); } const payload = url.includes("format=credits") ? creditsPayload : monthlyPayload; + if (payload === null) { + return new Response("internal error", { status: 500 }); + } return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" }, @@ -278,6 +281,31 @@ describe("xai-oauth usage provider", () => { expect(credits?.window?.resetsAt).toBe(Date.parse(periodEnd)); }); + it("rejects inferred unified weekly usage when the monthly probe encounters a network error", async () => { + const periodEnd = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(); + const periodStart = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const report = await xaiOauthUsageProvider.fetchUsage( + { provider: "xai-oauth", credential: makeCredential() }, + { + fetch: dualBillingFetch( + { + config: { + currentPeriod: { + end: periodEnd, + start: periodStart, + type: "USAGE_PERIOD_TYPE_WEEKLY", + }, + isUnifiedBillingUser: true, + }, + }, + null, + ).fetch, + }, + ); + + expect(report).toBeNull(); + }); + it("falls back to monthly included quota when credits has no percent fields", async () => { const { fetch, calls } = dualBillingFetch(makeUnifiedCreditsPayload(), makeUnifiedMonthlyPayload()); const report = await xaiOauthUsageProvider.fetchUsage( From d4832c8c047d473c90f3736073e15df3b5d9b401 Mon Sep 17 00:00:00 2001 From: bubua12 Date: Wed, 12 Aug 2026 16:21:55 +0800 Subject: [PATCH 6/6] fix(ai): reject inferred weekly credits when monthly config has positive limit but malformed fields - Differentiate explicit zero monthly limit from malformed positive monthly configurations. - Reject inferred weekly credits when monthly config has a positive limit that fails parsing, preserving the retain-last-good fallback. - Add unit test verifying rejection of malformed positive monthly payloads. --- packages/ai/src/usage/xai-oauth.ts | 14 +++++++++-- packages/ai/test/xai-oauth-usage.test.ts | 30 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/usage/xai-oauth.ts b/packages/ai/src/usage/xai-oauth.ts index 63bb8006f..161533d1f 100644 --- a/packages/ai/src/usage/xai-oauth.ts +++ b/packages/ai/src/usage/xai-oauth.ts @@ -202,6 +202,10 @@ function parseMonthlyBillingConfig(raw: Record): XaiMonthlyBill onDemandUsed: parseOnDemandAmount(raw.onDemandUsed), }; } +function hasPositiveMonthlyLimit(raw: Record): boolean { + const limit = parseOnDemandAmount(raw.monthlyLimit); + return limit !== undefined && limit > 0; +} function buildOnDemandLimit( onDemandCap: number | undefined, @@ -384,8 +388,14 @@ export const xaiOauthUsageProvider: UsageProvider = { if (weekly?.inferredPercent && creditsLooksUnified) { if (monthly) { effectiveWeekly = null; - } else if (!monthlyPayload || !isRecord(monthlyPayload) || !isRecord(monthlyPayload.config)) { - effectiveWeekly = null; + } else { + const monthlyConfig = + monthlyPayload && isRecord(monthlyPayload) && isRecord(monthlyPayload.config) + ? monthlyPayload.config + : null; + if (!monthlyConfig || hasPositiveMonthlyLimit(monthlyConfig)) { + effectiveWeekly = null; + } } } if (!effectiveWeekly && !monthly) return null; diff --git a/packages/ai/test/xai-oauth-usage.test.ts b/packages/ai/test/xai-oauth-usage.test.ts index 30cc1539b..09674d4ff 100644 --- a/packages/ai/test/xai-oauth-usage.test.ts +++ b/packages/ai/test/xai-oauth-usage.test.ts @@ -305,6 +305,36 @@ describe("xai-oauth usage provider", () => { expect(report).toBeNull(); }); + it("rejects inferred unified weekly usage when monthly config has a positive limit but malformed fields", async () => { + const periodEnd = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(); + const periodStart = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const report = await xaiOauthUsageProvider.fetchUsage( + { provider: "xai-oauth", credential: makeCredential() }, + { + fetch: dualBillingFetch( + { + config: { + currentPeriod: { + end: periodEnd, + start: periodStart, + type: "USAGE_PERIOD_TYPE_WEEKLY", + }, + isUnifiedBillingUser: true, + }, + }, + { + config: { + isUnifiedBillingUser: true, + monthlyLimit: { val: 15000 }, + // Missing 'used' and billingPeriod dates + }, + }, + ).fetch, + }, + ); + + expect(report).toBeNull(); + }); it("falls back to monthly included quota when credits has no percent fields", async () => { const { fetch, calls } = dualBillingFetch(makeUnifiedCreditsPayload(), makeUnifiedMonthlyPayload());