Merge main into model-registry

This commit is contained in:
Mario Zechner
2026-06-22 14:00:18 +02:00
220 changed files with 10488 additions and 4354 deletions
+127 -13
View File
@@ -13,7 +13,6 @@ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts";
import { clearDeprecationWarningsForTests } from "../src/utils/deprecation.ts";
describe("ModelRegistry", () => {
let tempDir: string;
@@ -25,7 +24,6 @@ describe("ModelRegistry", () => {
mkdirSync(tempDir, { recursive: true });
modelsJsonPath = join(tempDir, "models.json");
authStorage = AuthStorage.create(join(tempDir, "auth.json"));
clearDeprecationWarningsForTests();
});
afterEach(() => {
@@ -33,7 +31,6 @@ describe("ModelRegistry", () => {
rmSync(tempDir, { recursive: true });
}
clearApiKeyCache();
clearDeprecationWarningsForTests();
vi.restoreAllMocks();
});
@@ -443,6 +440,43 @@ describe("ModelRegistry", () => {
expect(compat?.cacheControlFormat).toBe("anthropic");
});
test("compat schema accepts chat template thinking configuration", () => {
writeRawModelsJson({
demo: {
baseUrl: "https://example.com/v1",
apiKey: "DEMO_KEY",
api: "openai-completions",
models: [
{
id: "demo-model",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000,
maxTokens: 100,
compat: {
thinkingFormat: "chat-template",
chatTemplateKwargs: {
preserve_thinking: true,
thinking: { $var: "thinking.enabled" },
},
},
},
],
},
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined;
expect(registry.getError()).toBeUndefined();
expect(compat?.thinkingFormat).toBe("chat-template");
expect(compat?.chatTemplateKwargs).toEqual({
preserve_thinking: true,
thinking: { $var: "thinking.enabled" },
});
});
test("compat schema accepts Anthropic eager tool input streaming flag", () => {
writeRawModelsJson({
demo: {
@@ -902,26 +936,87 @@ describe("ModelRegistry", () => {
expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider");
});
test("registerProvider warns and temporarily treats uppercase apiKey as an env reference", async () => {
const originalEnv = process.env.CUSTOM_NAME;
process.env.CUSTOM_NAME = "legacy-env-key";
test("stored API key env propagates to request auth and resolves headers", async () => {
authStorage.set("cloudflare-ai-gateway", {
type: "api_key",
key: "$CLOUDFLARE_API_KEY",
env: {
CLOUDFLARE_API_KEY: "stored-cf-token",
CLOUDFLARE_ACCOUNT_ID: "stored-account",
},
});
writeRawModelsJson({
"cloudflare-ai-gateway": {
headers: { "x-account": "$CLOUDFLARE_ACCOUNT_ID" },
},
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
const model = registry.getAll().find((m) => m.provider === "cloudflare-ai-gateway");
expect(model).toBeDefined();
const auth = await registry.getApiKeyAndHeaders(model!);
expect(auth).toEqual({
ok: true,
apiKey: "stored-cf-token",
headers: { "x-account": "stored-account" },
env: {
CLOUDFLARE_API_KEY: "stored-cf-token",
CLOUDFLARE_ACCOUNT_ID: "stored-account",
},
});
});
test("registerProvider treats uppercase apiKey and headers as literals", async () => {
const envKeys = ["CUSTOM_NAME", "BEARER", "MODEL_TOKEN"];
const savedEnv: Record<string, string | undefined> = {};
for (const key of envKeys) {
savedEnv[key] = process.env[key];
process.env[key] = `env-${key}`;
}
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
registry.registerProvider("legacy-provider", {
registry.registerProvider("literal-provider", {
...providerConfig("https://provider.test/v1", [{ id: "demo-model" }], "openai-completions"),
apiKey: "CUSTOM_NAME",
headers: { Authorization: "BEARER" },
models: [
{
id: "demo-model",
name: "demo-model",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 100000,
maxTokens: 8000,
headers: { "x-model-token": "MODEL_TOKEN" },
},
],
});
expect(await registry.getApiKeyForProvider("legacy-provider")).toBe("legacy-env-key");
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Pass "$CUSTOM_NAME" instead'));
expect(await registry.getApiKeyForProvider("literal-provider")).toBe("CUSTOM_NAME");
const model = registry.find("literal-provider", "demo-model");
expect(model).toBeDefined();
expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({
ok: true,
apiKey: "CUSTOM_NAME",
headers: {
Authorization: "BEARER",
"x-model-token": "MODEL_TOKEN",
},
});
expect(warnSpy).not.toHaveBeenCalled();
} finally {
if (originalEnv === undefined) {
delete process.env.CUSTOM_NAME;
} else {
process.env.CUSTOM_NAME = originalEnv;
for (const key of envKeys) {
if (savedEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = savedEnv[key];
}
}
}
});
@@ -1617,6 +1712,25 @@ describe("ModelRegistry", () => {
expect(count).toBe(0);
});
test("getAvailable filters GitHub Copilot OAuth models to account picker availability", () => {
authStorage.set("github-copilot", {
type: "oauth",
refresh: "github-access-token",
access: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;",
expires: Date.now() + 60_000,
availableModelIds: ["gpt-4.1"],
});
const registry = ModelRegistry.create(authStorage, modelsJsonPath);
expect(
registry
.getAvailable()
.filter((m) => m.provider === "github-copilot")
.map((m) => m.id),
).toEqual(["gpt-4.1"]);
});
test("getApiKeyAndHeaders resolves authHeader on every request", async () => {
const tokenFile = join(tempDir, "token");
writeFileSync(tokenFile, "token-1");