diff --git a/extensions/zai/index.ts b/extensions/zai/index.ts index 109bf5144a1..33929645968 100644 --- a/extensions/zai/index.ts +++ b/extensions/zai/index.ts @@ -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), diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index dc62cece821..982ffbc8be5 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -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": { diff --git a/src/infra/provider-usage.shared.test.ts b/src/infra/provider-usage.shared.test.ts index 048352a183d..4f575f197ff 100644 --- a/src/infra/provider-usage.shared.test.ts +++ b/src/infra/provider-usage.shared.test.ts @@ -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 }); + } + }); }); diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 6fa823db630..b801da4824c 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -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 (work: Promise, 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; + } +} diff --git a/src/plugin-sdk/provider-usage.ts b/src/plugin-sdk/provider-usage.ts index 33757596965..9b63a53ea93 100644 --- a/src/plugin-sdk/provider-usage.ts +++ b/src/plugin-sdk/provider-usage.ts @@ -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, diff --git a/src/plugins/contracts/runtime.contract.test.ts b/src/plugins/contracts/runtime.contract.test.ts index 073ad01c960..87acf1f8a13 100644 --- a/src/plugins/contracts/runtime.contract.test.ts +++ b/src/plugins/contracts/runtime.contract.test.ts @@ -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) => {