Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions open-sse/services/usage/qwen-token-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import {
fetchQwenTokenPlanQuota,
resolveConsoleSite,
QWEN_TOKEN_PLAN_WINDOW_5H,
QWEN_TOKEN_PLAN_WINDOW_WEEKLY,
type QwenTokenPlanQuota,
Expand Down Expand Up @@ -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.",
};
Expand Down
11 changes: 9 additions & 2 deletions src/lib/providers/validation/audioMiscProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions tests/unit/bailian-coding-plan-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
8 changes: 6 additions & 2 deletions tests/unit/search-provider-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down
77 changes: 77 additions & 0 deletions tests/unit/token-plan-console-aware-messages.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
Loading