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.
This commit is contained in:
peanut_hzm
2026-10-31 00:41:24 +08:00
parent a4f696620b
commit 7ba608fcb2
6 changed files with 382 additions and 14 deletions
+55 -1
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";
@@ -37,6 +37,7 @@ describe("zai usage source", () => {
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"Z_AI_API_KEY",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
@@ -116,6 +117,59 @@ describe("zai usage source", () => {
details: expect.arrayContaining([{ id: "status", label: "Status", value: "VALID" }]),
});
});
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 () => {
+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();