From accdfa9f33a6403b22cb6ccddf91550b4b9935e2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Tue, 1 Sep 2026 11:40:40 -0300 Subject: [PATCH] fix(usage): console-aware Token Plan guidance + subscription hint on bailian 401 (#12288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(usage): console-aware Token Plan guidance and subscription hint on bailian 401 The personal Token Plan is sold through two consoles with different portals, gateway hosts and login tickets. Two operator-facing messages ignored the split: - The quota guidance always said 'get the cookie at home.qwencloud.com', even for connections served by the Alibaba Model Studio console — following it verbatim produces a cookie the gateway rejects (console mismatch → BailianGateway.Login.NotLogined). The guidance now derives the console from the provider via resolveConsoleSite, matching what the fetcher will do with the pasted cookie. - Key validation mapped upstream 401 to a bare 'Invalid API key'. An expired Token Plan subscription produces the exact same upstream 401 (observed live 2026-09-01: subscription ended 08-23, the working key started failing), so the message now names the subscription as a cause worth checking. * test(providers): align the remaining bailian 401/403 message pins to prefix match search-provider-validation.test.ts pinned the exact 'Invalid API key' string for the bailian validator; the message now also names an expired Token Plan subscription. Same property asserted (401/403 => invalid), prefix match. --- open-sse/services/usage/qwen-token-plan.ts | 24 ++++-- .../validation/audioMiscProviders.ts | 11 ++- .../unit/bailian-coding-plan-provider.test.ts | 12 ++- tests/unit/search-provider-validation.test.ts | 8 +- .../token-plan-console-aware-messages.test.ts | 77 +++++++++++++++++++ 5 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 tests/unit/token-plan-console-aware-messages.test.ts diff --git a/open-sse/services/usage/qwen-token-plan.ts b/open-sse/services/usage/qwen-token-plan.ts index d26c54886f..8ec52d6401 100644 --- a/open-sse/services/usage/qwen-token-plan.ts +++ b/open-sse/services/usage/qwen-token-plan.ts @@ -11,6 +11,7 @@ import { fetchQwenTokenPlanQuota, + resolveConsoleSite, QWEN_TOKEN_PLAN_WINDOW_5H, QWEN_TOKEN_PLAN_WINDOW_WEEKLY, type QwenTokenPlanQuota, @@ -54,13 +55,26 @@ export async function getQwenTokenPlanUsage( }); if (!quota) { + // The same plan is sold through two consoles with different portals, gateway + // hosts and login tickets — instructions for the wrong console produce a cookie + // the gateway rejects (console mismatch → NotLogined). With no cookie stored the + // console is inferred from the provider id, same rule the fetcher applies. + const site = resolveConsoleSite("", provider); + const guide = + site.consoleSite === "ALIYUN" + ? "Get it at modelstudio.console.alibabacloud.com (logged in): F12 › Network, " + + "reload, filter by api.json, click a request to " + + "bailian-singapore-cs.alibabacloud.com and copy the whole Cookie value from " + + "Request Headers (it contains login_aliyunid_ticket)." + : "Get it at home.qwencloud.com › Billing › Subscription (logged in): F12 › " + + "Network, reload, filter by api.json, click a request to " + + "cs-data.qwencloud.com and copy the whole Cookie value from Request Headers " + + "(it contains login_qwencloud_ticket)."; + const brand = site.consoleSite === "ALIYUN" ? "Alibaba" : "Qwen"; return { message: - "Qwen Token Plan connected. Quota needs a console session cookie — the inference " + - "API key cannot read it. Get it at home.qwencloud.com › Billing › Subscription " + - "(logged in): F12 › Network, reload, filter by api.json, click a request to " + - "cs-data.qwencloud.com and copy the whole Cookie value from Request Headers " + - "(it contains login_qwencloud_ticket). Paste it into the connection's " + + `${brand} Token Plan connected. Quota needs a console session cookie — the ` + + `inference API key cannot read it. ${guide} Paste it into the connection's ` + "'Qwen / Model Studio console cookie' field, or set QWEN_CLOUD_COOKIE. " + "The cookie expires with the browser session — re-paste it when this message returns.", }; diff --git a/src/lib/providers/validation/audioMiscProviders.ts b/src/lib/providers/validation/audioMiscProviders.ts index eeb0f3c5eb..b86fe19527 100644 --- a/src/lib/providers/validation/audioMiscProviders.ts +++ b/src/lib/providers/validation/audioMiscProviders.ts @@ -302,9 +302,16 @@ export async function validateBailianCodingPlanProvider({ }), }); - // 401/403 => invalid key + // 401/403 => invalid key. An expired/lapsed Token Plan subscription yields the + // exact same upstream 401 invalid_api_key (observed live 2026-09-01: subscription + // ended 08-23, the previously working key started failing), so name it as a cause. if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; + return { + valid: false, + error: + "Invalid API key — or the Token Plan subscription is expired/inactive; " + + "check it in the Model Studio console", + }; } // Non-auth 4xx (e.g., 400 bad request) means auth passed but request was malformed diff --git a/tests/unit/bailian-coding-plan-provider.test.ts b/tests/unit/bailian-coding-plan-provider.test.ts index 757578f214..5fbe3b3430 100644 --- a/tests/unit/bailian-coding-plan-provider.test.ts +++ b/tests/unit/bailian-coding-plan-provider.test.ts @@ -432,7 +432,11 @@ test("validateProviderApiKey returns invalid for 401 response (bailian-coding-pl }); assert.equal(result.valid, false, "Should return invalid for 401"); - assert.equal(result.error, "Invalid API key", "Error should be 'Invalid API key'"); + assert.match( + String(result.error), + /^Invalid API key/, + "Error should start with 'Invalid API key'" + ); } finally { globalThis.fetch = originalFetch; } @@ -457,7 +461,11 @@ test("validateProviderApiKey returns invalid for 403 response (bailian-coding-pl }); assert.equal(result.valid, false, "Should return invalid for 403"); - assert.equal(result.error, "Invalid API key", "Error should be 'Invalid API key'"); + assert.match( + String(result.error), + /^Invalid API key/, + "Error should start with 'Invalid API key'" + ); } finally { globalThis.fetch = originalFetch; } diff --git a/tests/unit/search-provider-validation.test.ts b/tests/unit/search-provider-validation.test.ts index b44652928d..2d9ec8ec6a 100644 --- a/tests/unit/search-provider-validation.test.ts +++ b/tests/unit/search-provider-validation.test.ts @@ -133,7 +133,9 @@ test("bailian-coding-plan validation rejects 401 as invalid key", async () => { }); assert.equal(result.valid, false); - assert.equal(result.error, "Invalid API key"); + // Prefix match: the message now also names an expired Token Plan subscription, + // which yields the identical upstream 401/403. + assert.match(String(result.error), /^Invalid API key/); } finally { globalThis.fetch = originalFetch; } @@ -155,7 +157,9 @@ test("bailian-coding-plan validation rejects 403 as invalid key", async () => { }); assert.equal(result.valid, false); - assert.equal(result.error, "Invalid API key"); + // Prefix match: the message now also names an expired Token Plan subscription, + // which yields the identical upstream 401/403. + assert.match(String(result.error), /^Invalid API key/); } finally { globalThis.fetch = originalFetch; } diff --git a/tests/unit/token-plan-console-aware-messages.test.ts b/tests/unit/token-plan-console-aware-messages.test.ts new file mode 100644 index 0000000000..0f293d2eb6 --- /dev/null +++ b/tests/unit/token-plan-console-aware-messages.test.ts @@ -0,0 +1,77 @@ +/** + * The personal Token Plan is sold through two consoles (QwenCloud and Alibaba Model + * Studio) with different hosts, gateways and login tickets. Two operator-facing + * messages ignored that split (both bit the operator in the 2026-08/09 audits): + * + * 1. The quota guidance always said "get the cookie at home.qwencloud.com", even for + * connections served by the Alibaba console — whose cookie comes from + * modelstudio.console.alibabacloud.com and carries login_aliyunid_ticket. Following + * the instructions verbatim produced a cookie the gateway rejects (console + * mismatch → BailianGateway.Login.NotLogined). + * 2. Key validation mapped upstream 401 to a bare "Invalid API key". For Token Plan + * keys, an expired/lapsed subscription produces the exact same upstream 401 + * (observed live 2026-09-01: subscription ended 08-23, key started failing), so + * the message must point at the subscription as a cause worth checking. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getQwenTokenPlanUsage } from "../../open-sse/services/usage/qwen-token-plan.ts"; +import { validateProviderApiKey } from "../../src/lib/providers/validation.ts"; + +test("cookie guidance points Alibaba-console connections at the Model Studio console", async () => { + const result = await getQwenTokenPlanUsage( + "conn-guidance-aliyun", + "sk-sp-any", + {}, + "bailian-coding-plan" + ); + + assert.ok("message" in result, "no cookie stored → guidance message expected"); + const message = (result as { message: string }).message; + assert.match(message, /modelstudio\.console\.alibabacloud\.com/); + assert.match(message, /bailian-singapore-cs\.alibabacloud\.com/); + assert.match(message, /login_aliyunid_ticket/); + assert.doesNotMatch( + message, + /login_qwencloud_ticket/, + "Alibaba guidance must not tell the operator to hunt for the QwenCloud ticket" + ); +}); + +test("cookie guidance keeps the QwenCloud instructions for the QwenCloud console", async () => { + const result = await getQwenTokenPlanUsage( + "conn-guidance-qwen", + "sk-sp-any", + {}, + "qwen-cloud-token-plan" + ); + + assert.ok("message" in result, "no cookie stored → guidance message expected"); + const message = (result as { message: string }).message; + assert.match(message, /home\.qwencloud\.com/); + assert.match(message, /cs-data\.qwencloud\.com/); + assert.match(message, /login_qwencloud_ticket/); +}); + +test("bailian 401 mentions the subscription as a possible cause", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response(JSON.stringify({ error: { code: "invalid_api_key" } }), { status: 401 }); + try { + const result = await validateProviderApiKey({ + provider: "bailian-coding-plan", + apiKey: "sk-sp-expired-subscription", + }); + assert.equal(result.valid, false); + assert.match(String(result.error), /Invalid API key/); + assert.match( + String(result.error), + /subscription/i, + "an expired Token Plan subscription yields the same upstream 401 — say so" + ); + } finally { + globalThis.fetch = originalFetch; + } +});