diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1f722e5fc..64ca65d56 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed QwenCloud Token Plan quota reporting to call the current console usage RPC and document how to capture its optional Cookie during login. + ## [17.1.1] - 2026-07-24 ### Added diff --git a/packages/ai/src/registry/alibaba-token-plan.ts b/packages/ai/src/registry/alibaba-token-plan.ts index 73b70cf43..9ce8747dd 100644 --- a/packages/ai/src/registry/alibaba-token-plan.ts +++ b/packages/ai/src/registry/alibaba-token-plan.ts @@ -9,7 +9,8 @@ const TOKEN_PLAN_BASE_URL = "https://token-plan.ap-southeast-1.maas.aliyuncs.com const loginApiKey = createApiKeyLogin({ providerLabel: "QwenCloud Token Plan", authUrl: "https://home.qwencloud.com/billing/subscription/token-plan-individual", - instructions: "Subscribe to Token Plan Individual and copy its dedicated API key", + instructions: + "Subscribe to Token Plan Individual and copy its dedicated API key. Keep this page open; the next prompt explains how to enable optional quota reporting.", promptMessage: "Paste your QwenCloud Token Plan API key", placeholder: "sk-sp-...", validation: { @@ -24,15 +25,30 @@ export async function loginAlibabaTokenPlan(options: OAuthController): Promise { + const separator = segment.indexOf("="); + return separator > 0 && Boolean(segment.slice(0, separator).trim() && segment.slice(separator + 1).trim()); + }) + ) { + throw new AIError.ConfigurationError( + "Invalid QwenCloud Cookie header. Copy the complete Cookie request header from the cs-data.qwencloud.com usage request, not a single cookie value.", + ); + } return serializeAlibabaTokenPlanCredential(apiKey, cookie); } diff --git a/packages/ai/src/usage/alibaba-token-plan.ts b/packages/ai/src/usage/alibaba-token-plan.ts index deee58705..02ce4584b 100644 --- a/packages/ai/src/usage/alibaba-token-plan.ts +++ b/packages/ai/src/usage/alibaba-token-plan.ts @@ -14,13 +14,21 @@ const PROVIDER = "alibaba-token-plan"; const CONSOLE_ORIGIN = "https://home.qwencloud.com"; const DASHBOARD_URL = `${CONSOLE_ORIGIN}/billing/subscription/token-plan-individual`; const USER_INFO_URL = `${CONSOLE_ORIGIN}/tool/user/info.json`; -const USAGE_URL = `${CONSOLE_ORIGIN}/data/api.json?product=sfm_bailian&action=IntlBroadScopeAspnGateway`; const GATEWAY_ACTION = "IntlBroadScopeAspnGateway"; const USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage"; +const USAGE_URL = `https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=${GATEWAY_ACTION}&api=${encodeURIComponent(USAGE_API)}`; const BROWSER_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"; const FIVE_HOURS_MS = 5 * 60 * 60 * 1000; const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; +const CONSOLE_CORNERSTONE_PARAM = { + domain: "home.qwencloud.com", + consoleSite: "QWENCLOUD", + console: "ONE_CONSOLE", + xsp_lang: "en-US", + protocol: "V2", + productCode: "p_efm", +} as const; function extractCookieValue(header: string, name: string): string | undefined { for (const segment of header.split(";")) { @@ -32,6 +40,21 @@ function extractCookieValue(header: string, name: string): string | undefined { return undefined; } +function unwrapGatewayData(value: Record): Record { + let current = value; + if (typeof current.Data === "string") { + try { + const parsed: unknown = JSON.parse(current.Data); + if (isRecord(parsed)) current = parsed; + } catch { + return current; + } + } + if (isRecord(current.DataV2) && isRecord(current.DataV2.data)) current = current.DataV2.data; + if (isRecord(current.data)) current = current.data; + return current; +} + function parseResetTime(value: unknown): number | undefined { const parsed = toNumber(value); if (parsed === undefined || parsed <= 0) return undefined; @@ -127,7 +150,11 @@ async function fetchAlibabaTokenPlanUsage( action: GATEWAY_ACTION, region: "ap-southeast-1", sec_token: secToken, - params: JSON.stringify({ Api: USAGE_API, Data: {} }), + params: JSON.stringify({ + Api: USAGE_API, + Data: { cornerstoneParam: CONSOLE_CORNERSTONE_PARAM }, + V: "1.0", + }), }); const usageResponse = await ctx.fetch(USAGE_URL, { method: "POST", @@ -145,22 +172,23 @@ async function fetchAlibabaTokenPlanUsage( ctx.logger?.warn("QwenCloud usage response invalid", { provider: PROVIDER }); return null; } + const responseData = unwrapGatewayData(payload.data); const accountId = accountIdFromUserData(userPayload.data); const limits = [ buildLimit( "5h", "5 Hour Credits", FIVE_HOURS_MS, - parseUsedFraction(payload.data.per5HourPercentage), - parseResetTime(payload.data.per5HourResetTime), + parseUsedFraction(responseData.per5HourPercentage), + parseResetTime(responseData.per5HourResetTime), accountId, ), buildLimit( "7d", "7 Day Credits", SEVEN_DAYS_MS, - parseUsedFraction(payload.data.per1WeekPercentage), - parseResetTime(payload.data.per1WeekResetTime), + parseUsedFraction(responseData.per1WeekPercentage), + parseResetTime(responseData.per1WeekResetTime), accountId, ), ].filter((limit): limit is UsageLimit => limit !== undefined); diff --git a/packages/ai/test/alibaba-token-plan-usage.test.ts b/packages/ai/test/alibaba-token-plan-usage.test.ts index 45739ca4e..46e6ac59c 100644 --- a/packages/ai/test/alibaba-token-plan-usage.test.ts +++ b/packages/ai/test/alibaba-token-plan-usage.test.ts @@ -27,13 +27,17 @@ describe("QwenCloud Token Plan opt-in usage", () => { } return Promise.resolve( Response.json({ - code: "200", - successResponse: true, data: { - per5HourPercentage: 0.25, - per5HourResetTime: 1_800_000_000_000, - per1WeekPercentage: 0.5, - per1WeekResetTime: 1_800_100_000_000, + DataV2: { + data: { + data: { + per5HourPercentage: 0.25, + per5HourResetTime: 1_800_000_000_000, + per1WeekPercentage: 0.5, + per1WeekResetTime: 1_800_100_000_000, + }, + }, + }, }, }), ); @@ -48,7 +52,7 @@ describe("QwenCloud Token Plan opt-in usage", () => { expect(new Headers(requests[0]?.init?.headers).get("Cookie")).toBe(cookie); expect(requests[0]?.init?.redirect).toBe("manual"); expect(requests[1]?.url).toBe( - "https://home.qwencloud.com/data/api.json?product=sfm_bailian&action=IntlBroadScopeAspnGateway", + "https://cs-data.qwencloud.com/data/api.json?product=sfm_bailian&action=IntlBroadScopeAspnGateway&api=zeldaHttp.apikeyMgr.%2Ftokenplan%2Fpersonal%2Fapi%2Fv2%2Fusage", ); const usageHeaders = new Headers(requests[1]?.init?.headers); expect(usageHeaders.get("Cookie")).toBe(cookie); @@ -59,8 +63,22 @@ describe("QwenCloud Token Plan opt-in usage", () => { expect(usageHeaders.get("x-csrf-token")).toBe("csrf-token"); expect(requests[1]?.init?.redirect).toBe("manual"); const body = new URLSearchParams(String(requests[1]?.init?.body)); + expect(body.get("sec_token")).toBe("sec-token"); expect(body.get("params")).toBe( - JSON.stringify({ Api: "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage", Data: {} }), + JSON.stringify({ + Api: "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage", + Data: { + cornerstoneParam: { + domain: "home.qwencloud.com", + consoleSite: "QWENCLOUD", + console: "ONE_CONSOLE", + xsp_lang: "en-US", + protocol: "V2", + productCode: "p_efm", + }, + }, + V: "1.0", + }), ); expect(report).toMatchObject({ provider: "alibaba-token-plan", diff --git a/packages/ai/test/alibaba-token-plan.test.ts b/packages/ai/test/alibaba-token-plan.test.ts index af9b3fd47..e5f17c887 100644 --- a/packages/ai/test/alibaba-token-plan.test.ts +++ b/packages/ai/test/alibaba-token-plan.test.ts @@ -23,7 +23,8 @@ describe("QwenCloud Token Plan login", () => { expect(authRequests).toEqual([ { url: "https://home.qwencloud.com/billing/subscription/token-plan-individual", - instructions: "Subscribe to Token Plan Individual and copy its dedicated API key", + instructions: + "Subscribe to Token Plan Individual and copy its dedicated API key. Keep this page open; the next prompt explains how to enable optional quota reporting.", }, ]); expect(requestedUrl).toBe("https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models"); @@ -31,12 +32,19 @@ describe("QwenCloud Token Plan login", () => { }); test("stores an optional console Cookie while sending only the API key to inference", async () => { - const prompts = ["sk-sp-test", "session_id=test; login_aliyunid_csrf=csrf-token"]; + let cookiePrompt = ""; const credential = await loginAlibabaTokenPlan({ onAuth: () => {}, - onPrompt: async () => prompts.shift() ?? "", + onPrompt: async prompt => { + if (!prompt.allowEmpty) return "sk-sp-test"; + cookiePrompt = prompt.message; + return "Cookie: session_id=test; login_aliyunid_csrf=csrf-token"; + }, fetch: () => Promise.resolve(Response.json({ data: [{ id: "qwen3.7-plus" }] })), }); + expect(cookiePrompt).toContain("DevTools → Network"); + expect(cookiePrompt).toContain("cs-data.qwencloud.com/data/api.json"); + expect(cookiePrompt).toContain("Request Headers → Cookie"); expect(JSON.parse(credential)).toEqual({ token: "sk-sp-test", cookie: "session_id=test; login_aliyunid_csrf=csrf-token", @@ -52,6 +60,17 @@ describe("QwenCloud Token Plan login", () => { expect(JSON.stringify(setup)).not.toContain("session_id=test"); }); + test("rejects a single cookie value with actionable guidance", async () => { + const prompts = ["sk-sp-test", "5123456789012345"]; + await expect( + loginAlibabaTokenPlan({ + onAuth: () => {}, + onPrompt: async () => prompts.shift() ?? "", + fetch: () => Promise.resolve(Response.json({ data: [{ id: "qwen3.7-plus" }] })), + }), + ).rejects.toThrow("cs-data.qwencloud.com usage request"); + }); + test("rejects malformed compound credentials before inference setup", () => { const model = getBundledModel<"openai-completions">("alibaba-token-plan", "qwen3.7-plus"); if (!model) throw new Error("expected bundled QwenCloud Token Plan model"); diff --git a/packages/ai/test/auth-storage-usage-cache.test.ts b/packages/ai/test/auth-storage-usage-cache.test.ts index b4887b3c8..6fbba57a4 100644 --- a/packages/ai/test/auth-storage-usage-cache.test.ts +++ b/packages/ai/test/auth-storage-usage-cache.test.ts @@ -350,13 +350,17 @@ describe("AuthStorage usage cache: provider failure policy", () => { return Promise.resolve( usageCalls === 1 ? Response.json({ - code: "200", - successResponse: true, data: { - per5HourPercentage: 0.25, - per5HourResetTime: 1_800_000_000_000, - per1WeekPercentage: 0.5, - per1WeekResetTime: 1_800_100_000_000, + DataV2: { + data: { + data: { + per5HourPercentage: 0.25, + per5HourResetTime: 1_800_000_000_000, + per1WeekPercentage: 0.5, + per1WeekResetTime: 1_800_100_000_000, + }, + }, + }, }, }) : Response.json({