diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 73a0c165a6f9..bdb00407e59a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Kimi Code, Z.AI, and ClinePass usage polls logging a warning every poll for accounts without a metered plan ([#17057](https://github.com/can1357/oh-my-pi/pull/17057) by [@trukhinyuri](https://github.com/trukhinyuri)) - Fixed Xiaomi MiMo API-key login failing with `Unsupported model mimo-v2.5` after the V2.5 retirement ([#16727](https://github.com/can1357/oh-my-pi/pull/16727) by [@LiPu-jpg](https://github.com/LiPu-jpg)) - Fixed non-streaming requests reporting a missing API key when the credential resolver actually failed, hiding auth broker, OAuth and network outages behind "No API key" ([#16655](https://github.com/can1357/oh-my-pi/pull/16655) by [@adi-IL](https://github.com/adi-IL)) - Fixed auth-gateway requests failing when a client requests an unsupported thinking effort; the gateway now uses the nearest supported tier ([#16671](https://github.com/can1357/oh-my-pi/pull/16671) by [@codyw912](https://github.com/codyw912)) diff --git a/packages/ai/src/usage/cline-pass.ts b/packages/ai/src/usage/cline-pass.ts index 53b0be8fbab6..45c72b23bd4f 100644 --- a/packages/ai/src/usage/cline-pass.ts +++ b/packages/ai/src/usage/cline-pass.ts @@ -114,6 +114,12 @@ async function fetchClinePassUsage(params: UsageFetchParams, ctx: UsageFetchCont response.status, ); } + // An account that never subscribed to ClinePass (free models and credit only) + // answers 404 "no plan history found for user": nothing to meter, not a failure. + if (response.status === 404 && /no plan history/i.test(await response.text())) { + ctx.logger?.debug("ClinePass account has no plan to meter"); + return null; + } ctx.logger?.warn("ClinePass usage fetch failed", { status: response.status, statusText: response.statusText, diff --git a/packages/ai/src/usage/kimi.ts b/packages/ai/src/usage/kimi.ts index 7478380562e0..61b368e88c84 100644 --- a/packages/ai/src/usage/kimi.ts +++ b/packages/ai/src/usage/kimi.ts @@ -316,6 +316,11 @@ export const kimiUsageProvider: UsageProvider = { const parsed = parseUsagePayload(payload, nowMs); if (!parsed || parsed.rows.length === 0) { + // `{}` is Kimi's answer for an account without a Kimi Code plan: nothing to meter. + if (isRecord(payload) && Object.keys(payload).length === 0) { + ctx.logger?.debug("Kimi account has no Kimi Code plan to meter", { provider: params.provider }); + return null; + } ctx.logger?.warn("Kimi usage response invalid", { provider: params.provider }); return null; } diff --git a/packages/ai/src/usage/zai.ts b/packages/ai/src/usage/zai.ts index 30c25127e16b..11405aae9386 100644 --- a/packages/ai/src/usage/zai.ts +++ b/packages/ai/src/usage/zai.ts @@ -240,6 +240,12 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext): if (!payload) return null; if (payload.success !== true) { + // An account without a GLM Coding Plan (pay-as-you-go models only) answers + // `{code: 500, msg: "当前用户不存在coding plan"}`: nothing to meter, not a failure. + if (typeof payload.msg === "string" && /不存在\s*coding\s*plan|no\s+coding\s+plan/i.test(payload.msg)) { + ctx.logger?.debug("ZAI account has no Coding Plan to meter", { code: payload.code }); + return null; + } ctx.logger?.warn("ZAI usage response invalid", { code: payload.code, message: payload.msg }); return null; } diff --git a/packages/ai/test/cline-pass-usage.test.ts b/packages/ai/test/cline-pass-usage.test.ts index be7a86e2934a..6bcffac499a5 100644 --- a/packages/ai/test/cline-pass-usage.test.ts +++ b/packages/ai/test/cline-pass-usage.test.ts @@ -143,4 +143,19 @@ describe("ClinePass usage provider", () => { ).toBe(false); expect(clinePassUsageProvider.validatesCredentials).toBe(true); }); + + it("treats an account without a ClinePass plan as nothing to meter, not a failed fetch", async () => { + const warnings: string[] = []; + const logger = { debug() {}, warn: (message: string) => warnings.push(message) }; + const noPlan = { data: null, error: "no plan history found for user", success: false }; + const missing = { error: "Not Found", success: false }; + + expect(await clinePassUsageProvider.fetchUsage(makeParams(), { ...makeContext(noPlan, 404), logger })).toBeNull(); + expect(warnings).toEqual([]); + + expect( + await clinePassUsageProvider.fetchUsage(makeParams(), { ...makeContext(missing, 404), logger }), + ).toBeNull(); + expect(warnings).toEqual(["ClinePass usage fetch failed"]); + }); }); diff --git a/packages/ai/test/kimi-usage.test.ts b/packages/ai/test/kimi-usage.test.ts index 6ed410ab5334..5cc37548e4a1 100644 --- a/packages/ai/test/kimi-usage.test.ts +++ b/packages/ai/test/kimi-usage.test.ts @@ -183,4 +183,20 @@ describe("kimi usage provider", () => { expect(report!.limits).toHaveLength(1); expect(report!.limits[0]!.label).toBe("Weekly limit"); }); + + it("treats an account without a Kimi Code plan as nothing to meter, not an invalid response", async () => { + const warnings: string[] = []; + const logger = { debug() {}, warn: (message: string) => warnings.push(message) }; + const fetchUsage = (payload: unknown) => + kimiUsageProvider.fetchUsage( + { provider: "kimi-code", credential: makeCredential() }, + { ...makeCtx(payload), logger }, + ); + + expect(await fetchUsage({})).toBeNull(); + expect(warnings).toEqual([]); + + expect(await fetchUsage({ usage: "garbled" })).toBeNull(); + expect(warnings).toEqual(["Kimi usage response invalid"]); + }); }); diff --git a/packages/ai/test/zai-usage.test.ts b/packages/ai/test/zai-usage.test.ts index cea05b4a4ccc..a911ead6ea43 100644 --- a/packages/ai/test/zai-usage.test.ts +++ b/packages/ai/test/zai-usage.test.ts @@ -237,4 +237,21 @@ describe("zai usage provider", () => { expect(ranked.primary?.id).toBe("zai:tokens:5h"); expect(ranked.secondary?.id).toBe("zai:tokens:1w"); }); + + it("treats an account without a GLM Coding Plan as nothing to meter, not an invalid response", async () => { + const warnings: string[] = []; + const logger = { debug() {}, warn: (message: string) => warnings.push(message) }; + const fetchUsage = (payload: unknown) => + zaiUsageProvider.fetchUsage!( + { provider: "zai", credential: makeCredential(), signal: undefined }, + { ...makeCtx(payload), logger }, + ); + + expect(await fetchUsage({ code: 500, msg: "当前用户不存在coding plan", success: false })).toBeNull(); + expect(warnings).toEqual([]); + + expect(await fetchUsage({ code: 500, msg: "您的coding plan已过期", success: false })).toBeNull(); + expect(await fetchUsage({ code: 500, msg: "internal error", success: false })).toBeNull(); + expect(warnings).toEqual(["ZAI usage response invalid", "ZAI usage response invalid"]); + }); });