import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { withAuth } from "@oh-my-pi/pi-ai/auth-retry"; import type { Api, Model } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { removeSyncWithRetries, Snowflake } from "@oh-my-pi/pi-utils"; function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; } function stdoutCommand(value: string): string { if (process.platform !== "win32") return `printf %s ${shellQuote(value)}`; return `${JSON.stringify(process.execPath)} -e ${JSON.stringify(`process.stdout.write(${JSON.stringify(value)})`)}`; } function trackedTokenCommand(tokenFile: string, counterFile: string): string { if (process.platform !== "win32") { return `IFS= read -r token < ${shellQuote(tokenFile)}; printf 1 >> ${shellQuote(counterFile)}; [ "$token" = FAIL ] && exit 1; printf %s "$token"`; } const script = `const fs=require("node:fs");fs.appendFileSync(${JSON.stringify(counterFile)}, "1");const token=fs.readFileSync(${JSON.stringify(tokenFile)}, "utf8").trim();if(token==="FAIL")process.exit(1);process.stdout.write(token);`; return `${JSON.stringify(process.execPath)} -e ${JSON.stringify(script)}`; } function failedTrackingCommand(counterFile: string): string { if (process.platform !== "win32") return `printf 1 >> ${shellQuote(counterFile)}; exit 1`; const script = `const fs=require("node:fs");fs.appendFileSync(${JSON.stringify(counterFile)}, "1");process.exit(1);`; return `${JSON.stringify(process.execPath)} -e ${JSON.stringify(script)}`; } describe("ModelRegistry command-resolved models.yml values", () => { let tempDir = ""; let authStorage: AuthStorage; let modelsPath = ""; beforeEach(async () => { tempDir = path.join(os.tmpdir(), `pi-test-model-command-values-${Snowflake.next()}`); fs.mkdirSync(tempDir, { recursive: true }); modelsPath = path.join(tempDir, "models.json"); authStorage = await AuthStorage.create(":memory:"); }); afterEach(() => { authStorage.close(); if (!tempDir || !fs.existsSync(tempDir)) return; try { removeSyncWithRetries(tempDir); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "EBUSY") throw error; } }); test("provider apiKey and headers resolve from command stdout", async () => { fs.writeFileSync( modelsPath, JSON.stringify({ providers: { anthropic: { baseUrl: "https://anthropic-proxy.example.com/v1", apiKey: `!${stdoutCommand("cmd-api-key")}`, authHeader: true, headers: { "X-Api-Key": `!${stdoutCommand("cmd-header")}` }, }, }, }), ); const registry = new ModelRegistry(authStorage, modelsPath); expect(registry.hasCommandBackedApiKey("anthropic")).toBe(true); expect(registry.hasCommandBackedApiKey("openai")).toBe(false); const models = registry.getAll().filter(model => model.provider === "anthropic"); expect(models.length).toBeGreaterThan(1); for (const model of models) { expect(model.headers?.Authorization).toBe("Bearer cmd-api-key"); expect(model.headers?.["X-Api-Key"]).toBe("cmd-header"); } expect(await registry.getApiKey(models[0])).toBe("cmd-api-key"); }); test("modelOverrides headers resolve from command stdout", async () => { fs.writeFileSync( modelsPath, JSON.stringify({ providers: { "custom-proxy": { baseUrl: "https://custom-proxy.example.com/v1", api: "openai-completions", apiKey: `!${stdoutCommand("cmd-api-key")}`, authHeader: true, models: [{ id: "custom-model", name: "Custom Model" }], modelOverrides: { "custom-model": { headers: { "X-Model-Key": `!${stdoutCommand("cmd-model-header")}` } }, }, }, }, }), ); const registry = new ModelRegistry(authStorage, modelsPath); const model = registry.find("custom-proxy", "custom-model"); expect(model).toBeDefined(); expect(model?.headers?.["X-Model-Key"]).toBe("cmd-model-header"); expect(model?.headers?.Authorization).toBe("Bearer cmd-api-key"); }); test("401 reruns a command-backed API key and updates live auth headers", async () => { const tokenFile = path.join(tempDir, "token.txt"); const counterFile = path.join(tempDir, "counter.txt"); fs.writeFileSync(tokenFile, "stale-key"); fs.writeFileSync(counterFile, ""); const command = trackedTokenCommand(tokenFile, counterFile); fs.writeFileSync( modelsPath, JSON.stringify({ providers: { "custom-proxy": { baseUrl: "https://custom-proxy.example.com/v1", api: "openai-completions", apiKey: `!${command}`, authHeader: true, models: [{ id: "custom-model", name: "Custom Model" }], }, }, }), ); const registry = new ModelRegistry(authStorage, modelsPath); const model = registry.find("custom-proxy", "custom-model"); if (!model) throw new Error("Expected custom model"); fs.writeFileSync(tokenFile, "fresh-key"); const attemptedKeys: string[] = []; const result = await withAuth(registry.resolver(model), async key => { attemptedKeys.push(key); if (key === "stale-key") { throw Object.assign(new Error("401 authentication_error"), { status: 401 }); } if (key === "fresh-key") return "ok"; throw new Error(`Unexpected API key: ${key}`); }); expect(result).toBe("ok"); expect(attemptedKeys).toEqual(["stale-key", "fresh-key"]); expect(fs.readFileSync(counterFile, "utf8")).toBe("11"); expect(model.headers?.Authorization).toBe("Bearer fresh-key"); }); test("failed 401 refresh discards the rejected command-backed key", async () => { const tokenFile = path.join(tempDir, "token.txt"); const counterFile = path.join(tempDir, "counter.txt"); fs.writeFileSync(tokenFile, "stale-key"); fs.writeFileSync(counterFile, ""); const command = trackedTokenCommand(tokenFile, counterFile); fs.writeFileSync( modelsPath, JSON.stringify({ providers: { "custom-proxy": { baseUrl: "https://custom-proxy.example.com/v1", api: "openai-completions", apiKey: `!${command}`, authHeader: true, models: [{ id: "custom-model", name: "Custom Model" }], }, }, }), ); const registry = new ModelRegistry(authStorage, modelsPath); const model = registry.find("custom-proxy", "custom-model"); if (!model) throw new Error("Expected custom model"); fs.writeFileSync(tokenFile, "FAIL"); const refreshed = await registry.resolver(model)({ lastChance: false, error: Object.assign(new Error("401 authentication_error"), { status: 401 }), previousKey: "stale-key", }); expect(refreshed).toBeUndefined(); expect(fs.readFileSync(counterFile, "utf8")).toBe("11"); expect(await registry.getApiKey(model)).toBeUndefined(); expect(model.headers?.Authorization).toBeUndefined(); }); test("resolveCommandConfig caches failed executions so they do not retry", async () => { const counterFile = path.join(tempDir, "counter.txt"); fs.writeFileSync(counterFile, ""); // Command increments a counter and then fails (exit 1). const trackingCommand = failedTrackingCommand(counterFile); fs.writeFileSync( modelsPath, JSON.stringify({ providers: { "custom-proxy": { baseUrl: "https://custom-proxy.example.com/v1", api: "openai-completions", apiKey: `!${trackingCommand}`, }, }, }), ); // Init triggers the first command resolution. const registry = new ModelRegistry(authStorage, modelsPath); const dummyModel: Model = buildModel({ id: "foo", name: "foo", api: "openai-completions", provider: "custom-proxy", baseUrl: "a", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 4096, maxTokens: 1024, }); // Trigger the fallback resolver which also calls resolveConfigValue. await registry.getApiKey(dummyModel); // Another call to ensure it hits cache multiple times. await registry.getApiKey(dummyModel); // The command should have only run once. expect(fs.readFileSync(counterFile, "utf8")).toBe("1"); }); });