Merge PR #6521: fix(ai): repair Alibaba Token Plan usage reporting (@eggpeat)
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
- Fixed every Claude (`anthropic-messages`) model on the `opencode-zen` provider failing with `401 Missing API key`: the gateway requires `x-api-key`, so `opencode-zen` now uses X-Api-Key auth like `opencode-go`/`umans` instead of bearer-only, and no longer sends the `context_management` field its Anthropic proxy rejects on thinking requests ([#6510](https://github.com/can1357/oh-my-pi/issues/6510)).
|
||||
- Fixed Anthropic native server-tool blocks being dropped from persisted assistant turns, preserving signed web-search continuations in their original response order ([#6495](https://github.com/can1357/oh-my-pi/issues/6495))
|
||||
- Fixed statusless provider capacity errors such as `no_capacity` and high-demand responses being treated as terminal instead of retryable. ([#6503](https://github.com/can1357/oh-my-pi/issues/6503))
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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<s
|
||||
throw new AIError.OnPromptRequiredError("QwenCloud Token Plan");
|
||||
}
|
||||
const apiKey = await loginApiKey(options);
|
||||
const cookie = await options.onPrompt({
|
||||
const rawCookie = await options.onPrompt({
|
||||
message:
|
||||
"Paste the Cookie request header from home.qwencloud.com for optional quota reporting, or press Enter to skip",
|
||||
placeholder: "login_aliyunid_csrf=...; ...",
|
||||
"Optional quota reporting: open browser DevTools → Network, reload the Token Plan page, filter for api.json, and select the cs-data.qwencloud.com/data/api.json request whose api query ends in /tokenplan/personal/api/v2/usage. Copy Request Headers → Cookie, then paste the complete name=value; ... value here, or press Enter to skip.",
|
||||
placeholder: "name=value; name=value; ...",
|
||||
allowEmpty: true,
|
||||
});
|
||||
const cookie = rawCookie
|
||||
.trim()
|
||||
.replace(/^Cookie:\s*/i, "")
|
||||
.trim();
|
||||
if (options.signal?.aborted) {
|
||||
throw new AIError.LoginCancelledError();
|
||||
}
|
||||
if (
|
||||
cookie &&
|
||||
!cookie.split(";").some(segment => {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, unknown>): Record<string, unknown> {
|
||||
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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user