refactor(usage): share legacy pi auth token lookup

This commit is contained in:
Peter Steinberger
2026-03-17 06:09:38 +00:00
parent 143530407d
commit 520d753b27
6 changed files with 112 additions and 55 deletions

View File

@@ -1,6 +1,3 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
@@ -10,7 +7,6 @@ import {
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
import { resolveRequiredHomeDir } from "openclaw/plugin-sdk/infra-runtime";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
@@ -23,7 +19,7 @@ import {
} from "openclaw/plugin-sdk/provider-auth";
import { DEFAULT_CONTEXT_TOKENS, normalizeModelCompat } from "openclaw/plugin-sdk/provider-models";
import { createZaiToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream";
import { fetchZaiUsage } from "openclaw/plugin-sdk/provider-usage";
import { fetchZaiUsage, resolveLegacyPiAgentAccessToken } from "openclaw/plugin-sdk/provider-usage";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { applyZaiConfig, applyZaiProviderConfig, ZAI_DEFAULT_MODEL_REF } from "./onboard.js";
@@ -68,27 +64,6 @@ function resolveGlm5ForwardCompatModel(
} as ProviderRuntimeModel);
}
function resolveLegacyZaiUsageToken(env: NodeJS.ProcessEnv): string | undefined {
try {
const authPath = path.join(
resolveRequiredHomeDir(env, os.homedir),
".pi",
"agent",
"auth.json",
);
if (!fs.existsSync(authPath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record<
string,
{ access?: string }
>;
return parsed["z-ai"]?.access || parsed.zai?.access;
} catch {
return undefined;
}
}
function resolveZaiDefaultModel(modelIdOverride?: string): string {
return modelIdOverride ? `zai/${modelIdOverride}` : ZAI_DEFAULT_MODEL_REF;
}
@@ -328,7 +303,7 @@ const zaiPlugin = {
if (apiKey) {
return { token: apiKey };
}
const legacyToken = resolveLegacyZaiUsageToken(ctx.env);
const legacyToken = resolveLegacyPiAgentAccessToken(ctx.env, ["z-ai", "zai"]);
return legacyToken ? { token: legacyToken } : null;
},
fetchUsageSnapshot: async (ctx) => await fetchZaiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),

View File

@@ -1,6 +1,3 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
dedupeProfileIds,
ensureAuthProfileStore,
@@ -12,9 +9,9 @@ import { isNonSecretApiKeyMarker } from "../agents/model-auth-markers.js";
import { resolveUsableCustomProviderApiKey } from "../agents/model-auth.js";
import { normalizeProviderId } from "../agents/model-selection.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
import { resolveLegacyPiAgentAccessToken } from "./provider-usage.shared.js";
import type { UsageProviderId } from "./provider-usage.types.js";
export type ProviderAuth = {
@@ -44,27 +41,6 @@ function parseGoogleUsageToken(apiKey: string): string {
return apiKey;
}
function resolveLegacyZaiUsageToken(env: NodeJS.ProcessEnv): string | undefined {
try {
const authPath = path.join(
resolveRequiredHomeDir(env, os.homedir),
".pi",
"agent",
"auth.json",
);
if (!fs.existsSync(authPath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record<
string,
{ access?: string }
>;
return parsed["z-ai"]?.access || parsed.zai?.access;
} catch {
return undefined;
}
}
function resolveProviderApiKeyFromConfigAndStore(params: {
state: UsageAuthState;
providerIds: string[];
@@ -225,7 +201,7 @@ async function resolveProviderUsageAuthFallback(params: {
if (apiKey) {
return { provider: "zai", token: apiKey };
}
const legacyToken = resolveLegacyZaiUsageToken(params.state.env);
const legacyToken = resolveLegacyPiAgentAccessToken(params.state.env, ["z-ai", "zai"]);
return legacyToken ? { provider: "zai", token: legacyToken } : null;
}
case "minimax": {

View File

@@ -1,5 +1,13 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { clampPercent, resolveUsageProviderId, withTimeout } from "./provider-usage.shared.js";
import {
clampPercent,
resolveLegacyPiAgentAccessToken,
resolveUsageProviderId,
withTimeout,
} from "./provider-usage.shared.js";
describe("provider-usage.shared", () => {
afterEach(() => {
@@ -52,4 +60,34 @@ describe("provider-usage.shared", () => {
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
});
it("reads legacy pi auth tokens for known provider aliases", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-provider-usage-"));
await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true });
await fs.writeFile(
path.join(home, ".pi", "agent", "auth.json"),
`${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`,
"utf8",
);
try {
expect(resolveLegacyPiAgentAccessToken({ HOME: home }, ["z-ai", "zai"])).toBe(
"legacy-zai-key",
);
} finally {
await fs.rm(home, { recursive: true, force: true });
}
});
it("returns undefined for invalid legacy pi auth files", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-provider-usage-"));
await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true });
await fs.writeFile(path.join(home, ".pi", "agent", "auth.json"), "{not-json", "utf8");
try {
expect(resolveLegacyPiAgentAccessToken({ HOME: home }, ["z-ai", "zai"])).toBeUndefined();
} finally {
await fs.rm(home, { recursive: true, force: true });
}
});
});

View File

@@ -1,4 +1,8 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeProviderId } from "../agents/model-selection.js";
import { resolveRequiredHomeDir } from "./home-dir.js";
import type { UsageProviderId } from "./provider-usage.types.js";
export const DEFAULT_TIMEOUT_MS = 5000;
@@ -59,3 +63,33 @@ export const withTimeout = async <T>(work: Promise<T>, ms: number, fallback: T):
}
}
};
export function resolveLegacyPiAgentAccessToken(
env: NodeJS.ProcessEnv,
providerIds: string[],
): string | undefined {
try {
const authPath = path.join(
resolveRequiredHomeDir(env, os.homedir),
".pi",
"agent",
"auth.json",
);
if (!fs.existsSync(authPath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record<
string,
{ access?: string }
>;
for (const providerId of providerIds) {
const token = parsed[providerId]?.access;
if (typeof token === "string" && token.trim()) {
return token;
}
}
return undefined;
} catch {
return undefined;
}
}

View File

@@ -13,7 +13,11 @@ export {
fetchMinimaxUsage,
fetchZaiUsage,
} from "../infra/provider-usage.fetch.js";
export { clampPercent, PROVIDER_LABELS } from "../infra/provider-usage.shared.js";
export {
clampPercent,
PROVIDER_LABELS,
resolveLegacyPiAgentAccessToken,
} from "../infra/provider-usage.shared.js";
export {
buildUsageErrorSnapshot,
buildUsageHttpErrorSnapshot,

View File

@@ -1,3 +1,6 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { createProviderUsageFetch, makeResponse } from "../../test-utils/provider-usage-fetch.js";
import type { ProviderRuntimeModel } from "../types.js";
@@ -514,6 +517,33 @@ describe("provider runtime contract", () => {
});
});
it("falls back to legacy pi auth tokens for usage auth", async () => {
const provider = requireProvider("zai");
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-contract-"));
await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true });
await fs.writeFile(
path.join(home, ".pi", "agent", "auth.json"),
`${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } }, null, 2)}\n`,
"utf8",
);
try {
await expect(
provider.resolveUsageAuth?.({
config: {} as never,
env: { HOME: home } as NodeJS.ProcessEnv,
provider: "zai",
resolveApiKeyFromConfigAndStore: () => undefined,
resolveOAuthToken: async () => null,
}),
).resolves.toEqual({
token: "legacy-zai-token",
});
} finally {
await fs.rm(home, { recursive: true, force: true });
}
});
it("owns usage snapshot fetching", async () => {
const provider = requireProviderContractProvider("zai");
const mockFetch = createProviderUsageFetch(async (url) => {