mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 13:42:50 +00:00
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>
This commit is contained in:
co-authored by
peanut_hzm
parent
32a420db1b
commit
0e60432de9
@@ -23,60 +23,6 @@ function writeMiniMaxCredentials(
|
||||
writeFileSync(join(dir, ".mmx", "credentials.json"), JSON.stringify(payload));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the OAuth login the `mcode` CLI keeps under
|
||||
* `~/.minimax/auth/<buildEnv>/<region>/<clientId>/`.
|
||||
*/
|
||||
function writeMiniMaxOAuthLogin(
|
||||
dir: string,
|
||||
options: {
|
||||
buildEnv?: string;
|
||||
region?: string;
|
||||
clientId?: string;
|
||||
accessToken?: string;
|
||||
expiresAtMs?: number;
|
||||
} = {},
|
||||
): string {
|
||||
const buildEnv = options.buildEnv ?? "prod";
|
||||
const region = options.region ?? "cn";
|
||||
const clientId = options.clientId ?? "mcode-public";
|
||||
const clientDir = join(dir, ".minimax", "auth", buildEnv, region, clientId);
|
||||
mkdirSync(clientDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(clientDir, "auth.json"),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
records: {
|
||||
"com.minimax.mcode.oauth.prod.cn": {
|
||||
schemaVersion: 1,
|
||||
accessToken: options.accessToken ?? "mmoat_fixture_token",
|
||||
refreshToken: "mmort_fixture_token",
|
||||
tokenType: "Bearer",
|
||||
clientId,
|
||||
scopes: ["agent.default"],
|
||||
audience: "agent-backend",
|
||||
...(options.expiresAtMs === undefined ? {} : { expiresAtMs: options.expiresAtMs }),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
writeFileSync(
|
||||
join(clientDir, "auth-state.json"),
|
||||
JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
status: "authenticated",
|
||||
storeKind: "file",
|
||||
clientId,
|
||||
scopes: ["agent.default"],
|
||||
audience: "agent-backend",
|
||||
buildEnv,
|
||||
region,
|
||||
...(options.expiresAtMs === undefined ? {} : { expiresAtMs: options.expiresAtMs }),
|
||||
}),
|
||||
);
|
||||
return join(clientDir, "auth.json");
|
||||
}
|
||||
|
||||
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
|
||||
return vi.fn(async (url: RequestInfo | URL) => {
|
||||
const key = url.toString();
|
||||
@@ -185,7 +131,7 @@ describe("minimax usage source", () => {
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimax.io/v1/api/openplatform/coding_plan/remains");
|
||||
expect(requestedUrl).toBe("https://api.minimax.io/v1/token_plan/remains");
|
||||
expect(authorization).toBe("Bearer minimax_test_token");
|
||||
expect(miniMax).toMatchObject({
|
||||
status: "available",
|
||||
@@ -212,88 +158,6 @@ describe("minimax usage source", () => {
|
||||
expect(await discover()).toEqual([]);
|
||||
});
|
||||
|
||||
it("discovers the OAuth login the mcode CLI stores under ~/.minimax/auth", async () => {
|
||||
const authPath = writeMiniMaxOAuthLogin(homeDir, {
|
||||
accessToken: "mmoat_fixture_token",
|
||||
});
|
||||
|
||||
const accounts = await discover();
|
||||
|
||||
expect(accounts).toHaveLength(1);
|
||||
expect(accounts[0]?.input).toEqual({
|
||||
store: "oauth",
|
||||
locator: authPath,
|
||||
baseUrl: "https://api.minimaxi.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the region host the CLI logged in against", async () => {
|
||||
writeMiniMaxOAuthLogin(homeDir, { region: "en" });
|
||||
|
||||
const accounts = await discover();
|
||||
|
||||
expect(accounts[0]?.input).toMatchObject({ baseUrl: "https://api.minimax.io" });
|
||||
});
|
||||
|
||||
it("fetches MiniMax usage from the CLI OAuth login", async () => {
|
||||
writeMiniMaxOAuthLogin(homeDir, { accessToken: "mmoat_fixture_token" });
|
||||
let requestedUrl: string | null = null;
|
||||
let authorization: string | null = null;
|
||||
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
|
||||
requestedUrl = url.toString();
|
||||
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
|
||||
return jsonResponse({
|
||||
model_remains: [
|
||||
{
|
||||
model_name: "general",
|
||||
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
|
||||
weekly_end_time: Date.parse("2026-06-26T00:00:00.000Z"),
|
||||
current_interval_remaining_percent: 87,
|
||||
current_weekly_remaining_percent: 78,
|
||||
},
|
||||
],
|
||||
});
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains");
|
||||
expect(authorization).toBe("Bearer mmoat_fixture_token");
|
||||
expect(miniMax).toMatchObject({
|
||||
status: "available",
|
||||
windows: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "interval_general", usedPct: 13, remainingPct: 87 }),
|
||||
expect.objectContaining({ id: "weekly_general", usedPct: 22, remainingPct: 78 }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an expired CLI OAuth login as expired", async () => {
|
||||
writeMiniMaxOAuthLogin(homeDir, {
|
||||
accessToken: "mmoat_fixture_token",
|
||||
expiresAtMs: Date.now() - 60_000,
|
||||
});
|
||||
let requested = false;
|
||||
fetchApi = (async () => {
|
||||
requested = true;
|
||||
return jsonResponse({ model_remains: [] });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requested).toBe(false);
|
||||
expect(miniMax).toMatchObject({
|
||||
status: "unavailable",
|
||||
problem: { kind: "expired" },
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores a CLI auth directory that has no access token", async () => {
|
||||
writeMiniMaxOAuthLogin(homeDir, { accessToken: "" });
|
||||
|
||||
expect(await discover()).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads MiniMax OAuth credentials from the CLI credentials file", async () => {
|
||||
writeMiniMaxCredentials(
|
||||
homeDir,
|
||||
@@ -309,9 +173,7 @@ describe("minimax usage source", () => {
|
||||
|
||||
await service().listUsage();
|
||||
|
||||
expect(requestedUrl).toBe(
|
||||
"https://account.example.com/v1/api/openplatform/coding_plan/remains",
|
||||
);
|
||||
expect(requestedUrl).toBe("https://account.example.com/v1/token_plan/remains");
|
||||
});
|
||||
|
||||
it("falls back to MiniMax api_key in the CLI config file", async () => {
|
||||
@@ -327,7 +189,7 @@ describe("minimax usage source", () => {
|
||||
|
||||
const miniMax = findProvider(await service().listUsage(), "minimax");
|
||||
|
||||
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains");
|
||||
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/token_plan/remains");
|
||||
expect(miniMax.status).toBe("unavailable");
|
||||
});
|
||||
|
||||
@@ -336,7 +198,7 @@ describe("minimax usage source", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
|
||||
"https://api.minimaxi.com/v1/token_plan/remains",
|
||||
() =>
|
||||
jsonResponse({
|
||||
model_remains: null,
|
||||
@@ -360,7 +222,7 @@ describe("minimax usage source", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
|
||||
"https://api.minimax.io/v1/token_plan/remains",
|
||||
() => jsonResponse({ model_remains: null }),
|
||||
],
|
||||
]),
|
||||
@@ -380,7 +242,7 @@ describe("minimax usage source", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
|
||||
"https://api.minimax.io/v1/token_plan/remains",
|
||||
() =>
|
||||
jsonResponse({
|
||||
base_resp: { status_code: 0, status_msg: "success" },
|
||||
@@ -413,7 +275,7 @@ describe("minimax usage source", () => {
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
|
||||
"https://api.minimax.io/v1/token_plan/remains",
|
||||
() =>
|
||||
jsonResponse({
|
||||
model_remains: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UsageInput } from "../shared/input.js";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
unavailable,
|
||||
@@ -20,10 +20,6 @@ const ApiOptionalStringSchema = z.preprocess(
|
||||
const MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io";
|
||||
const MINIMAX_CN_BASE_URL = "https://api.minimaxi.com";
|
||||
|
||||
// The coding-plan quota endpoint the CLI itself calls. The older
|
||||
// `/v1/token_plan/remains` path is not the one the agent backend serves.
|
||||
const MINIMAX_CODING_PLAN_PATH = "/v1/api/openplatform/coding_plan/remains";
|
||||
|
||||
const MiniMaxModelRemainSchema = z.object({
|
||||
model_name: ApiOptionalStringSchema,
|
||||
start_time: ApiNumberSchema.optional(),
|
||||
@@ -72,31 +68,6 @@ const MiniMaxConfigSchema = z.object({
|
||||
oauth: MiniMaxCredentialsSchema.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* The `mcode` CLI stores its OAuth login under `~/.minimax/auth/<env>/<region>/<clientId>/`.
|
||||
* Each client directory holds an `auth.json` with the token and an
|
||||
* `auth-state.json` with the region and expiry.
|
||||
*/
|
||||
const MiniMaxOAuthAuthSchema = z.object({
|
||||
schemaVersion: z.number().optional(),
|
||||
records: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
accessToken: z.string().optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
expiresAtMs: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const MiniMaxOAuthStateSchema = z.object({
|
||||
status: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
buildEnv: z.string().optional(),
|
||||
region: z.string().optional(),
|
||||
expiresAtMs: z.number().optional(),
|
||||
});
|
||||
|
||||
type MiniMaxModelRemain = z.infer<typeof MiniMaxModelRemainSchema>;
|
||||
|
||||
interface MiniMaxResolvedAuth {
|
||||
@@ -183,7 +154,7 @@ export async function fetchUsage(
|
||||
if (auth.expiresAt && Date.parse(auth.expiresAt) <= Date.now())
|
||||
return unavailable({ kind: "expired", expiresAt: new Date(auth.expiresAt).toISOString() });
|
||||
|
||||
const res = await fetchApi(`${auth.baseUrl}${MINIMAX_CODING_PLAN_PATH}`, {
|
||||
const res = await fetchApi(`${auth.baseUrl}/v1/token_plan/remains`, {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
@@ -236,9 +207,6 @@ async function readAuth(
|
||||
? { token, baseUrl: resolveBaseUrl({ baseUrl: process.env.MINIMAX_BASE_URL }) }
|
||||
: undefined;
|
||||
}
|
||||
if (input.store === "oauth") {
|
||||
return readOAuthAuth(input);
|
||||
}
|
||||
try {
|
||||
const raw: unknown = JSON.parse(await fs.readFile(input.locator, "utf8"));
|
||||
if (input.store === "credentials") {
|
||||
@@ -271,101 +239,6 @@ async function readAuth(
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the OAuth login the `mcode` CLI writes to
|
||||
* `~/.minimax/auth/<buildEnv>/<region>/<clientId>/auth.json`. The region
|
||||
* decides which host serves the account, and the token lives inside a
|
||||
* `records` map keyed by the OAuth grant.
|
||||
*/
|
||||
async function readOAuthAuth(
|
||||
input: UsageInput,
|
||||
): Promise<(MiniMaxResolvedAuth & { expiresAt?: string }) | undefined> {
|
||||
try {
|
||||
const raw: unknown = JSON.parse(await fs.readFile(input.locator, "utf8"));
|
||||
const auth = MiniMaxOAuthAuthSchema.parse(raw);
|
||||
const record = Object.values(auth.records).find((entry) => entry.accessToken);
|
||||
if (!record?.accessToken) return undefined;
|
||||
|
||||
const state = await readOAuthState(input.locator);
|
||||
const expiresAtMs = record.expiresAtMs ?? state?.expiresAtMs;
|
||||
return {
|
||||
token: record.accessToken,
|
||||
baseUrl: resolveBaseUrl({
|
||||
baseUrl: input.baseUrl,
|
||||
region: state?.region,
|
||||
}),
|
||||
expiresAt: typeof expiresAtMs === "number" ? new Date(expiresAtMs).toISOString() : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function readOAuthState(
|
||||
authPath: string,
|
||||
): Promise<z.infer<typeof MiniMaxOAuthStateSchema> | undefined> {
|
||||
try {
|
||||
const statePath = join(dirname(authPath), "auth-state.json");
|
||||
const raw: unknown = JSON.parse(await fs.readFile(statePath, "utf8"));
|
||||
return MiniMaxOAuthStateSchema.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Locates the `mcode` CLI's OAuth login. The layout is
|
||||
* `~/.minimax/auth/<buildEnv>/<region>/<clientId>/auth.json`, so the client
|
||||
* directory is found by scanning the region directories rather than by
|
||||
* assuming a fixed client id.
|
||||
*/
|
||||
async function discoverOAuthLogins(): Promise<UsageInput[]> {
|
||||
const authRoot = join(homedir(), ".minimax", "auth");
|
||||
const inputs: UsageInput[] = [];
|
||||
let buildEnvDirs: string[];
|
||||
try {
|
||||
buildEnvDirs = (await fs.readdir(authRoot, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
} catch {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
for (const buildEnv of buildEnvDirs) {
|
||||
const envRoot = join(authRoot, buildEnv);
|
||||
let regionDirs: string[];
|
||||
try {
|
||||
regionDirs = (await fs.readdir(envRoot, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const region of regionDirs) {
|
||||
const regionRoot = join(envRoot, region);
|
||||
let clientDirs: string[];
|
||||
try {
|
||||
clientDirs = (await fs.readdir(regionRoot, { withFileTypes: true }))
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const clientId of clientDirs) {
|
||||
const authPath = join(regionRoot, clientId, "auth.json");
|
||||
const auth = await readOAuthAuth({ store: "oauth", locator: authPath });
|
||||
if (!auth) continue;
|
||||
inputs.push({
|
||||
store: "oauth",
|
||||
locator: authPath,
|
||||
baseUrl: auth.baseUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export async function discover(): Promise<UsageAccount[]> {
|
||||
const candidates: UsageInput[] = [
|
||||
{ store: "env", locator: "MINIMAX_API_KEY" },
|
||||
@@ -375,14 +248,5 @@ export async function discover(): Promise<UsageAccount[]> {
|
||||
const accounts: UsageAccount[] = [];
|
||||
for (const input of candidates)
|
||||
if (await readAuth(input)) accounts.push({ key: "default", input });
|
||||
if (accounts.length > 0) return accounts;
|
||||
|
||||
const oauthInputs = await discoverOAuthLogins();
|
||||
for (const input of oauthInputs) {
|
||||
if (await readAuth(input)) {
|
||||
accounts.push({ key: "default", input });
|
||||
break;
|
||||
}
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { z } from "zod";
|
||||
export const inputSchema = z
|
||||
.object({
|
||||
store: z.enum(["env", "credentials", "config", "oauth"]),
|
||||
store: z.enum(["env", "credentials", "config"]),
|
||||
locator: z.string().min(1),
|
||||
/**
|
||||
* Base URL for the coding-plan quota endpoint. The CLI's OAuth login is
|
||||
* region-scoped (cn vs en), and each region serves its own host.
|
||||
*/
|
||||
baseUrl: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
export type UsageInput = z.infer<typeof inputSchema>;
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("zai usage source", () => {
|
||||
process.env["USERPROFILE"] = homeDir;
|
||||
for (const key of [
|
||||
"APPDATA",
|
||||
"XDG_CONFIG_HOME",
|
||||
"COPILOT_TOKEN",
|
||||
"GITHUB_TOKEN",
|
||||
"GITHUB_PAT",
|
||||
|
||||
Reference in New Issue
Block a user