Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] caf22464a5 test(usage): isolate the legacy login locator from inherited credentials 2026-10-31 12:04:32 +00:00
paseo-bot[bot] 705cf93681 Merge remote-tracking branch 'origin/main' into bot/diagnose-8309-7be72288 2026-10-31 11:57:29 +00:00
paseo-bot[bot]andpeanut_hzm 0e60432de9 fix(usage): narrow credential discovery repair to the verified GLM path
Keep the independently reproduced GLM discovery fix and isolate its file test from inherited XDG_CONFIG_HOME. Leave MiniMax quota routing for separate verification before changing existing API-key requests.

Co-authored-by: peanut_hzm <peanut_hzm@163.com>
2026-10-31 11:56:21 +00:00
peanut_hzm 32a420db1b Merge remote-tracking branch 'origin/main' into fix/usage-source-cli-auth-locations 2026-10-31 15:47:12 +08:00
peanut_hzm f56f277606 Merge remote-tracking branch 'origin/main' into fix/usage-source-cli-auth-locations 2026-10-31 10:38:40 +08:00
peanut_hzm 6b325bdbcd Merge remote-tracking branch 'origin/main' into fix/usage-source-cli-auth-locations 2026-10-31 10:20:41 +08:00
peanut_hzm 7ba608fcb2 fix(usage): follow the CLIs' current auth locations for MiniMax and Z.ai
The usage sources look for credentials where the CLIs used to keep them, so
their plan quota never appears in Settings → Usage.

MiniMax Code (`mcode`) moved its OAuth login to
`~/.minimax/auth/<buildEnv>/<region>/<clientId>/auth.json`, with the token
inside a `records` map and the region in a sibling `auth-state.json`. The
source still looked in `~/.mmx/` for the older `access_token` shape. It also
called `/v1/token_plan/remains`, which the agent backend answers with
`status_code 1004` ("Please carry the API secret key in the 'Authorization'
field"); the coding-plan endpoint the CLI itself uses is
`/v1/api/openplatform/coding_plan/remains`.

Z.ai's `glm-acp-agent` reads `Z_AI_API_KEY` and persists it to
`~/.config/glm-acp-agent/credentials.json` as `z_ai_api_key`. The source
looked for `ZAI_API_KEY` and `GLM_API_KEY` only, and never read the
credentials file.

Both keep their existing lookups working, so a configured environment
variable or the older `~/.mmx/` layout still resolves.
2026-10-31 00:41:24 +08:00
3 changed files with 96 additions and 12 deletions
+58 -9
View File
@@ -1,4 +1,4 @@
import { mkdtempSync, rmSync } from "node:fs";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -32,11 +32,13 @@ describe("zai usage source", () => {
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"XDG_CONFIG_HOME",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"Z_AI_API_KEY",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
@@ -116,11 +118,61 @@ describe("zai usage source", () => {
details: expect.arrayContaining([{ id: "status", label: "Status", value: "VALID" }]),
});
});
});
it("discovery returns a locator when fetch finds zai credentials", async () => {
const previous = process.env["ZAI_API_KEY"];
try {
it("fetches Z.ai usage from Z_AI_API_KEY, the name the glm-acp-agent CLI uses", async () => {
process.env["Z_AI_API_KEY"] = "zai_cli_token";
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_cli_token");
expect(zai).toMatchObject({ status: "available", planLabel: "GLM Coding Max" });
});
it("reads Z.ai usage from the credentials file the glm-acp-agent CLI writes", async () => {
const configDir = join(homeDir, ".config", "glm-acp-agent");
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, "credentials.json"),
JSON.stringify({ z_ai_api_key: "zai_file_token" }),
);
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_file_token");
expect(zai).toMatchObject({ status: "available" });
});
it("honours XDG_CONFIG_HOME for the credentials file", async () => {
const configDir = join(homeDir, "xdg", "glm-acp-agent");
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, "credentials.json"),
JSON.stringify({ z_ai_api_key: "zai_xdg_token" }),
);
process.env["XDG_CONFIG_HOME"] = join(homeDir, "xdg");
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_xdg_token");
expect(zai).toMatchObject({ status: "available" });
});
it("discovery returns a locator when fetch finds zai credentials", async () => {
process.env["ZAI_API_KEY"] = "fixture-token";
let requested = false;
await fetchFirst(async () => {
@@ -131,10 +183,7 @@ it("discovery returns a locator when fetch finds zai credentials", async () => {
expect(await discover()).toEqual([
{ key: "default", input: { store: "env", locator: "ZAI_API_KEY" } },
]);
} finally {
if (previous === undefined) delete process.env["ZAI_API_KEY"];
else process.env["ZAI_API_KEY"] = previous;
}
});
});
describe("account discovery", () => {
+37 -2
View File
@@ -1,4 +1,7 @@
import type { UsageInput } from "../shared/input.js";
import { promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
unavailable,
@@ -25,11 +28,16 @@ const ZaiUsageResponseSchema = z.object({
.optional(),
});
/** The credentials file the `glm-acp-agent` CLI writes. */
const ZaiCredentialsSchema = z.object({
z_ai_api_key: z.string().optional(),
});
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
const token = process.env[input.locator];
const token = await readToken(input);
if (!token) throw new Error("Z.ai login store no longer exists");
const res = await fetchApi("https://api.z.ai/api/biz/subscription/list", {
@@ -64,9 +72,36 @@ export async function fetchUsage(
};
}
/**
* The Z.AI API key comes from the environment or from the credentials file the
* `glm-acp-agent` CLI writes. The CLI names the variable `Z_AI_API_KEY`; the
* older `ZAI_API_KEY` spelling is still accepted.
*/
async function readToken(input: UsageInput): Promise<string | undefined> {
if (input.store === "env") {
return process.env[input.locator];
}
try {
const raw: unknown = JSON.parse(await fs.readFile(input.locator, "utf8"));
return ZaiCredentialsSchema.parse(raw).z_ai_api_key;
} catch {
return undefined;
}
}
export async function discover(): Promise<UsageAccount[]> {
for (const locator of ["ZAI_API_KEY", "GLM_API_KEY"]) {
for (const locator of ["Z_AI_API_KEY", "ZAI_API_KEY", "GLM_API_KEY"]) {
if (process.env[locator]) return [{ key: "default", input: { store: "env", locator } }];
}
const credentialsPath = join(
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
"glm-acp-agent",
"credentials.json",
);
if (await readToken({ store: "file", locator: credentialsPath })) {
return [{ key: "default", input: { store: "file", locator: credentialsPath } }];
}
return [];
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { z } from "zod";
export const inputSchema = z
.object({
store: z.literal("env"),
store: z.enum(["env", "file"]),
locator: z.string().min(1),
})
.strict();