Fix Zai and Minimax auth for usage
This commit is contained in:
@@ -291,6 +291,12 @@ describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
|
||||
expect(serverOpts.authStorage.setApiKey).toBeTypeOf("function");
|
||||
expect(serverOpts.authStorage.clearApiKey).toBeTypeOf("function");
|
||||
expect(serverOpts.authStorage.hasApiKey).toBeTypeOf("function");
|
||||
expect(serverOpts.authStorage.getApiKeyProviders()).toEqual([
|
||||
{ id: "kimi-coding", name: "Kimi" },
|
||||
{ id: "minimax", name: "Minimax" },
|
||||
{ id: "openrouter", name: "OpenRouter" },
|
||||
{ id: "zai", name: "Zai" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates AuthStorage via AuthStorage.create()", async () => {
|
||||
@@ -642,4 +648,3 @@ describe("runDashboard — per-project engine manager (multi-project)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -247,9 +247,18 @@ const mocks = vi.hoisted(() => {
|
||||
|
||||
const authStorage = {
|
||||
getApiKey: vi.fn().mockResolvedValue(undefined),
|
||||
reload: vi.fn(),
|
||||
getOAuthProviders: vi.fn().mockReturnValue([]),
|
||||
hasAuth: vi.fn().mockReturnValue(false),
|
||||
login: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
set: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
get: vi.fn(),
|
||||
};
|
||||
|
||||
const modelRegistry = {
|
||||
getAll: vi.fn().mockReturnValue([]),
|
||||
registerProvider: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -10,12 +10,11 @@ import {
|
||||
} from "./task-lifecycle.js";
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
|
||||
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
|
||||
// Re-export for backward compatibility with tests
|
||||
export { promptForPort };
|
||||
|
||||
type LoginCallbacks = Parameters<AuthStorage["login"]>[1];
|
||||
|
||||
let processDiagnosticsRegistered = false;
|
||||
let diagnosticIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
@@ -184,80 +183,6 @@ function setDiagnosticStoreListenerCheck(check: () => Record<string, number>): v
|
||||
diagnosticStoreListenerCheck = check;
|
||||
}
|
||||
|
||||
interface DashboardAuthStorage {
|
||||
reload(): void;
|
||||
getOAuthProviders(): Array<{ id: string; name: string }>;
|
||||
hasAuth(provider: string): boolean;
|
||||
login(providerId: string, callbacks: LoginCallbacks): Promise<void>;
|
||||
logout(provider: string): void;
|
||||
getApiKeyProviders(): Array<{ id: string; name: string }>;
|
||||
setApiKey(providerId: string, apiKey: string): void;
|
||||
clearApiKey(providerId: string): void;
|
||||
hasApiKey(providerId: string): boolean;
|
||||
}
|
||||
|
||||
function getProviderDisplayName(providerId: string): string {
|
||||
const knownProviderNames: Record<string, string> = {
|
||||
openrouter: "OpenRouter",
|
||||
"kimi-coding": "Kimi",
|
||||
};
|
||||
|
||||
if (knownProviderNames[providerId]) {
|
||||
return knownProviderNames[providerId];
|
||||
}
|
||||
|
||||
return providerId
|
||||
.split(/[-_]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function wrapAuthStorageWithApiKeyProviders(
|
||||
authStorage: AuthStorage,
|
||||
modelRegistry: ModelRegistry,
|
||||
): DashboardAuthStorage {
|
||||
return {
|
||||
reload: () => authStorage.reload(),
|
||||
getOAuthProviders: () =>
|
||||
authStorage
|
||||
.getOAuthProviders()
|
||||
.map((provider) => ({ id: provider.id, name: provider.name })),
|
||||
hasAuth: (provider) => authStorage.hasAuth(provider),
|
||||
login: (providerId, callbacks) =>
|
||||
authStorage.login(providerId as Parameters<AuthStorage["login"]>[0], callbacks),
|
||||
logout: (provider) => authStorage.logout(provider),
|
||||
getApiKeyProviders: () => {
|
||||
const oauthProviderIds = new Set(
|
||||
authStorage.getOAuthProviders().map((provider) => provider.id),
|
||||
);
|
||||
const providers = new Map<string, string>();
|
||||
|
||||
for (const model of modelRegistry.getAll()) {
|
||||
const providerId = model.provider;
|
||||
if (!providerId || oauthProviderIds.has(providerId) || providers.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
providers.set(providerId, getProviderDisplayName(providerId));
|
||||
}
|
||||
|
||||
return Array.from(providers, ([id, name]) => ({ id, name })).sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
},
|
||||
setApiKey: (providerId, apiKey) => {
|
||||
authStorage.set(providerId, { type: "api_key", key: apiKey });
|
||||
},
|
||||
clearApiKey: (providerId) => {
|
||||
authStorage.remove(providerId);
|
||||
},
|
||||
hasApiKey: (providerId) => {
|
||||
const credential = authStorage.get(providerId);
|
||||
return credential?.type === "api_key" || authStorage.hasAuth(providerId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean; open?: boolean } = {}) {
|
||||
ensureProcessDiagnostics();
|
||||
|
||||
|
||||
95
packages/cli/src/commands/provider-auth.ts
Normal file
95
packages/cli/src/commands/provider-auth.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
AuthStorage,
|
||||
ModelRegistry,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export type LoginCallbacks = Parameters<AuthStorage["login"]>[1];
|
||||
|
||||
export interface DashboardAuthStorage {
|
||||
reload(): void;
|
||||
getOAuthProviders(): Array<{ id: string; name: string }>;
|
||||
hasAuth(provider: string): boolean;
|
||||
login(providerId: string, callbacks: LoginCallbacks): Promise<void>;
|
||||
logout(provider: string): void;
|
||||
getApiKeyProviders(): Array<{ id: string; name: string }>;
|
||||
setApiKey(providerId: string, apiKey: string): void;
|
||||
clearApiKey(providerId: string): void;
|
||||
hasApiKey(providerId: string): boolean;
|
||||
getApiKey(providerId: string): Promise<string | undefined>;
|
||||
get(providerId: string): { type?: string; key?: string } | undefined;
|
||||
}
|
||||
|
||||
const BUILT_IN_API_KEY_PROVIDERS: Array<{ id: string; name: string }> = [
|
||||
{ id: "kimi-coding", name: "Kimi" },
|
||||
{ id: "minimax", name: "Minimax" },
|
||||
{ id: "openrouter", name: "OpenRouter" },
|
||||
{ id: "zai", name: "Zai" },
|
||||
];
|
||||
|
||||
function getProviderDisplayName(providerId: string): string {
|
||||
const knownProviderNames = new Map(
|
||||
BUILT_IN_API_KEY_PROVIDERS.map((provider) => [provider.id, provider.name]),
|
||||
);
|
||||
|
||||
const knownName = knownProviderNames.get(providerId);
|
||||
if (knownName) return knownName;
|
||||
|
||||
return providerId
|
||||
.split(/[-_]+/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function wrapAuthStorageWithApiKeyProviders(
|
||||
authStorage: AuthStorage,
|
||||
modelRegistry: ModelRegistry,
|
||||
): DashboardAuthStorage {
|
||||
return {
|
||||
reload: () => authStorage.reload(),
|
||||
getOAuthProviders: () =>
|
||||
authStorage
|
||||
.getOAuthProviders()
|
||||
.map((provider) => ({ id: provider.id, name: provider.name })),
|
||||
hasAuth: (provider) => authStorage.hasAuth(provider),
|
||||
login: (providerId, callbacks) =>
|
||||
authStorage.login(providerId as Parameters<AuthStorage["login"]>[0], callbacks),
|
||||
logout: (provider) => authStorage.logout(provider),
|
||||
getApiKeyProviders: () => {
|
||||
const oauthProviderIds = new Set(
|
||||
authStorage.getOAuthProviders().map((provider) => provider.id),
|
||||
);
|
||||
const providers = new Map<string, string>();
|
||||
|
||||
for (const provider of BUILT_IN_API_KEY_PROVIDERS) {
|
||||
if (!oauthProviderIds.has(provider.id)) {
|
||||
providers.set(provider.id, provider.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const model of modelRegistry.getAll()) {
|
||||
const providerId = model.provider;
|
||||
if (!providerId || oauthProviderIds.has(providerId) || providers.has(providerId)) {
|
||||
continue;
|
||||
}
|
||||
providers.set(providerId, getProviderDisplayName(providerId));
|
||||
}
|
||||
|
||||
return Array.from(providers, ([id, name]) => ({ id, name })).sort((a, b) =>
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
},
|
||||
setApiKey: (providerId, apiKey) => {
|
||||
authStorage.set(providerId, { type: "api_key", key: apiKey });
|
||||
},
|
||||
clearApiKey: (providerId) => {
|
||||
authStorage.remove(providerId);
|
||||
},
|
||||
hasApiKey: (providerId) => {
|
||||
const credential = authStorage.get(providerId);
|
||||
return credential?.type === "api_key" || authStorage.hasAuth(providerId);
|
||||
},
|
||||
getApiKey: (providerId) => authStorage.getApiKey(providerId),
|
||||
get: (providerId) => authStorage.get(providerId),
|
||||
};
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "./task-lifecycle.js";
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
|
||||
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
|
||||
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
let diagnosticIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -493,9 +494,11 @@ export async function runServe(
|
||||
modelRegistry.refresh();
|
||||
}
|
||||
|
||||
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(authStorage, modelRegistry);
|
||||
|
||||
const app = createServer(store, {
|
||||
onMerge: (taskId) => engine.onMerge(taskId),
|
||||
authStorage,
|
||||
authStorage: dashboardAuthStorage,
|
||||
modelRegistry,
|
||||
automationStore,
|
||||
missionAutopilot,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { githubRateLimiter } from "./github-poll.js";
|
||||
import { terminalSessionManager } from "./terminal.js";
|
||||
import { getTerminalService } from "./terminal-service.js";
|
||||
import { listFiles, readFile, writeFile, listWorkspaceFiles, readWorkspaceFile, writeWorkspaceFile, copyWorkspaceFile, moveWorkspaceFile, deleteWorkspaceFile, renameWorkspaceFile, getWorkspaceFileForDownload, getWorkspaceFolderForZip, readProjectFile, writeProjectFile, FileServiceError, type FileListResponse, type FileContentResponse, type SaveFileResponse, type FileOperationResponse } from "./file-service.js";
|
||||
import { fetchAllProviderUsage } from "./usage.js";
|
||||
import { clearUsageCache, fetchAllProviderUsage } from "./usage.js";
|
||||
import {
|
||||
getGitHubAppConfig,
|
||||
verifyWebhookSignature,
|
||||
@@ -97,6 +97,10 @@ export interface AuthStorageLike {
|
||||
clearApiKey?(providerId: string): void;
|
||||
/** Check if a provider has an API key configured. */
|
||||
hasApiKey?(providerId: string): boolean;
|
||||
/** Get the configured API key for usage providers. */
|
||||
getApiKey?(providerId: string): string | null | undefined | Promise<string | null | undefined>;
|
||||
/** Get raw stored credentials for usage providers. */
|
||||
get?(providerId: string): { type?: string; key?: string } | null | undefined;
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
@@ -8074,7 +8078,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
*/
|
||||
router.get("/usage", async (_req, res) => {
|
||||
try {
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const providers = await fetchAllProviderUsage(options?.authStorage);
|
||||
res.json({ providers });
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -14833,6 +14837,7 @@ function registerAuthRoutes(router: Router, authStorage?: AuthStorageLike): void
|
||||
}
|
||||
|
||||
storage.setApiKey(provider, apiKey.trim());
|
||||
clearUsageCache();
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -14861,6 +14866,7 @@ function registerAuthRoutes(router: Router, authStorage?: AuthStorageLike): void
|
||||
}
|
||||
|
||||
storage.clearApiKey(provider);
|
||||
clearUsageCache();
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
if (err instanceof ApiError) {
|
||||
|
||||
@@ -2049,6 +2049,55 @@ describe("usage", () => {
|
||||
expect(minimax.windows[0].label).toBe("MiniMax-M*");
|
||||
});
|
||||
|
||||
it("reads API key from provided AuthStorage before auth files", async () => {
|
||||
const mockResponse = {
|
||||
model_remains: [
|
||||
{
|
||||
model_name: "MiniMax-M*",
|
||||
current_interval_total_count: 100,
|
||||
current_interval_usage_count: 40,
|
||||
remains_time: 60_000,
|
||||
start_time: Date.now() - 60_000,
|
||||
end_time: Date.now() + 60_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
expect(options.headers.authorization).toBe("Bearer auth-storage-minimax-key");
|
||||
const mockRes = {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage({
|
||||
reload: vi.fn(),
|
||||
hasAuth: vi.fn(() => true),
|
||||
getApiKey: vi.fn((provider: string) =>
|
||||
provider === "minimax" ? "auth-storage-minimax-key" : null
|
||||
),
|
||||
});
|
||||
|
||||
const minimax = providers.find((p) => p.name === "Minimax")!;
|
||||
expect(minimax.status).toBe("ok");
|
||||
expect(minimax.windows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles 401 auth error", async () => {
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".pi/agent/auth.json")) {
|
||||
@@ -2280,6 +2329,106 @@ describe("usage", () => {
|
||||
expect(mcpWindow.percentUsed).toBe(2.5);
|
||||
});
|
||||
|
||||
it("reads API key from provided AuthStorage before auth files", async () => {
|
||||
const mockResponse = {
|
||||
code: 200,
|
||||
msg: "Operation successful",
|
||||
data: {
|
||||
limits: [
|
||||
{
|
||||
type: "TOKENS_LIMIT",
|
||||
percentage: 10,
|
||||
nextResetTime: Date.now() + 60_000,
|
||||
},
|
||||
],
|
||||
level: "pro",
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
expect(options.headers.authorization).toBe("auth-storage-zai-key");
|
||||
const mockRes = {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage({
|
||||
reload: vi.fn(),
|
||||
hasAuth: vi.fn(() => true),
|
||||
get: vi.fn((provider: string) =>
|
||||
provider === "zai" ? { type: "api_key", key: "auth-storage-zai-key" } : null
|
||||
),
|
||||
});
|
||||
|
||||
const zai = providers.find((p) => p.name === "Zai")!;
|
||||
expect(zai.status).toBe("ok");
|
||||
expect(zai.plan).toBe("Pro");
|
||||
});
|
||||
|
||||
it("falls back to fusion auth files when pi auth files are absent", async () => {
|
||||
const mockResponse = {
|
||||
code: 200,
|
||||
msg: "Operation successful",
|
||||
data: {
|
||||
limits: [
|
||||
{
|
||||
type: "TOKENS_LIMIT",
|
||||
percentage: 10,
|
||||
nextResetTime: Date.now() + 60_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
success: true,
|
||||
};
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes(".fusion/agent/auth.json")) {
|
||||
return JSON.stringify({
|
||||
zai: { type: "api_key", key: "fusion-zai-key" },
|
||||
});
|
||||
}
|
||||
throw new Error("File not found");
|
||||
});
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("Keychain item not found");
|
||||
});
|
||||
|
||||
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
|
||||
mockRequest.mockImplementation((options: any, callback: any) => {
|
||||
expect(options.headers.authorization).toBe("fusion-zai-key");
|
||||
const mockRes = {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
on: vi.fn((event: string, handler: any) => {
|
||||
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
|
||||
if (event === "end") handler();
|
||||
}),
|
||||
};
|
||||
callback(mockRes);
|
||||
return mockReq;
|
||||
});
|
||||
|
||||
const providers = await fetchAllProviderUsage();
|
||||
const zai = providers.find((p) => p.name === "Zai")!;
|
||||
expect(zai.status).toBe("ok");
|
||||
});
|
||||
|
||||
it("parses only TOKENS_LIMIT when no TIME_LIMIT present", async () => {
|
||||
const mockResponse = {
|
||||
code: 200,
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface ProviderUsage {
|
||||
export interface AuthStorageLike {
|
||||
reload(): void;
|
||||
hasAuth(provider: string): boolean;
|
||||
get?(provider: string): { type?: string; key?: string } | null | undefined;
|
||||
getApiKey?(provider: string): string | null | undefined | Promise<string | null | undefined>;
|
||||
}
|
||||
|
||||
// Cache for usage data with TTL
|
||||
@@ -227,12 +229,21 @@ function decodeJwtPayload(token: string): any {
|
||||
|
||||
// ── Pi auth storage reader ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Read an API key from pi's auth storage (~/.pi/agent/auth.json).
|
||||
* Returns the API key string or null if not found.
|
||||
*/
|
||||
function readPiAuthKey(provider: string): string | null {
|
||||
const authPath = path.join(process.env.HOME || "~", ".pi", "agent", "auth.json");
|
||||
function getAuthFileCandidates(): string[] {
|
||||
const home = process.env.HOME || "~";
|
||||
return [
|
||||
path.join(home, ".pi", "agent", "auth.json"),
|
||||
path.join(home, ".pi", "auth.json"),
|
||||
path.join(home, ".fusion", "agent", "auth.json"),
|
||||
path.join(home, ".fusion", "auth.json"),
|
||||
path.join(process.cwd(), ".fusion", "agent", "auth.json"),
|
||||
path.join(process.cwd(), ".fusion", "auth.json"),
|
||||
path.join(process.cwd(), ".pi", "agent", "auth.json"),
|
||||
path.join(process.cwd(), ".pi", "auth.json"),
|
||||
];
|
||||
}
|
||||
|
||||
function readAuthKeyFromFile(authPath: string, provider: string): string | null {
|
||||
try {
|
||||
const auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
|
||||
const entry = auth?.[provider];
|
||||
@@ -243,6 +254,35 @@ function readPiAuthKey(provider: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an API key from the same AuthStorage object used by the dashboard when
|
||||
* available, then fall back to conventional pi/fusion auth files.
|
||||
*/
|
||||
async function readConfiguredApiKey(provider: string, authStorage?: AuthStorageLike): Promise<string | null> {
|
||||
try {
|
||||
authStorage?.reload();
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const apiKey = await authStorage?.getApiKey?.(provider);
|
||||
if (apiKey) return apiKey;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const entry = authStorage?.get?.(provider);
|
||||
if (entry && (entry.type === "api_key" || entry.type === "key") && entry.key) {
|
||||
return entry.key;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
for (const authPath of getAuthFileCandidates()) {
|
||||
const apiKey = readAuthKeyFromFile(authPath, provider);
|
||||
if (apiKey) return apiKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Claude fetcher ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1222,7 +1262,7 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
|
||||
|
||||
// ── Minimax fetcher ─────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchMinimaxUsage(): Promise<ProviderUsage> {
|
||||
async function fetchMinimaxUsage(authStorage?: AuthStorageLike): Promise<ProviderUsage> {
|
||||
const usage: ProviderUsage = {
|
||||
name: "Minimax",
|
||||
icon: "🟣",
|
||||
@@ -1230,8 +1270,8 @@ async function fetchMinimaxUsage(): Promise<ProviderUsage> {
|
||||
windows: [],
|
||||
};
|
||||
|
||||
// Load Minimax API key from pi's auth storage
|
||||
const apiKey = readPiAuthKey("minimax");
|
||||
// Load Minimax API key from the same auth storage the dashboard uses.
|
||||
const apiKey = await readConfiguredApiKey("minimax", authStorage);
|
||||
if (!apiKey) {
|
||||
usage.error = "No Minimax credentials — add API key to pi";
|
||||
return usage;
|
||||
@@ -1316,7 +1356,7 @@ async function fetchMinimaxUsage(): Promise<ProviderUsage> {
|
||||
|
||||
// ── Zai (Zhipu AI) fetcher ──────────────────────────────────────────────────
|
||||
|
||||
async function fetchZaiUsage(): Promise<ProviderUsage> {
|
||||
async function fetchZaiUsage(authStorage?: AuthStorageLike): Promise<ProviderUsage> {
|
||||
const usage: ProviderUsage = {
|
||||
name: "Zai",
|
||||
icon: "🟡",
|
||||
@@ -1324,8 +1364,8 @@ async function fetchZaiUsage(): Promise<ProviderUsage> {
|
||||
windows: [],
|
||||
};
|
||||
|
||||
// Load Zai API key from pi's auth storage
|
||||
const apiKey = readPiAuthKey("zai");
|
||||
// Load Zai API key from the same auth storage the dashboard uses.
|
||||
const apiKey = await readConfiguredApiKey("zai", authStorage);
|
||||
if (!apiKey) {
|
||||
usage.error = "No Zai credentials — add API key to pi";
|
||||
return usage;
|
||||
@@ -1496,7 +1536,7 @@ export function withTimeout(
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAllProviderUsage(_authStorage?: AuthStorageLike): Promise<ProviderUsage[]> {
|
||||
export async function fetchAllProviderUsage(authStorage?: AuthStorageLike): Promise<ProviderUsage[]> {
|
||||
// Check cache
|
||||
if (usageCache && Date.now() - usageCache.timestamp < CACHE_TTL_MS) {
|
||||
return usageCache.data;
|
||||
@@ -1508,8 +1548,8 @@ export async function fetchAllProviderUsage(_authStorage?: AuthStorageLike): Pro
|
||||
withTimeout(fetchClaudeUsage(), "Claude", CLAUDE_FETCH_TIMEOUT_MS),
|
||||
withTimeout(fetchCodexUsage(), "Codex"),
|
||||
withTimeout(fetchGeminiUsage(), "Gemini"),
|
||||
withTimeout(fetchMinimaxUsage(), "Minimax"),
|
||||
withTimeout(fetchZaiUsage(), "Zai"),
|
||||
withTimeout(fetchMinimaxUsage(authStorage), "Minimax"),
|
||||
withTimeout(fetchZaiUsage(authStorage), "Zai"),
|
||||
]);
|
||||
|
||||
const providers: ProviderUsage[] = [];
|
||||
|
||||
Reference in New Issue
Block a user