chore(vendor): import pi 1.3.0

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
2026-10-15 17:58:48 -04:00
parent 719370c8f7
commit 6081e3e325
1743 changed files with 205460 additions and 131705 deletions

View File

@@ -1,19 +1,13 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type {
AnthropicMessagesCompat,
Api,
Context,
Model,
OpenAICompletionsCompat,
} from "@earendil-works/pi-ai/compat";
import { normalizeContext } from "@earendil-works/pi-ai";
import type { AnthropicMessagesCompat, Api, Model, OpenAICompletionsCompat } from "@earendil-works/pi-ai/compat";
import { getApiProvider, getModels, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.ts";
import type { ModelsJsonProvider } from "../src/core/model-config.ts";
import { clearApiKeyCache, type ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts";
import { createModelRegistry } from "./model-runtime-test-utils.ts";
describe("ModelRegistry", () => {
@@ -93,9 +87,9 @@ describe("ModelRegistry", () => {
maxTokens: 4096,
};
const emptyContext: Context = {
const emptyContext = normalizeContext({
messages: [],
};
});
describe("baseUrl override (no custom models)", () => {
test("overriding baseUrl keeps all built-in models", async () => {
@@ -311,14 +305,14 @@ describe("ModelRegistry", () => {
writeModelsJson({
openrouter: providerConfig(
"https://my-proxy.example.com/v1",
[{ id: "anthropic/claude-sonnet-4" }],
[{ id: "anthropic/claude-sonnet-4.5" }],
"openai-completions",
),
});
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnetModels = models.filter((m) => m.id === "anthropic/claude-sonnet-4");
const sonnetModels = models.filter((m) => m.id === "anthropic/claude-sonnet-4.5");
expect(sonnetModels).toHaveLength(1);
expect(sonnetModels[0].baseUrl).toBe("https://my-proxy.example.com/v1");
@@ -639,7 +633,7 @@ describe("ModelRegistry", () => {
},
],
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "Overridden Built-in Sonnet",
},
},
@@ -651,7 +645,7 @@ describe("ModelRegistry", () => {
expect(models.some((m) => m.id === "custom/openrouter-model")).toBe(true);
expect(
models.some((m) => m.id === "anthropic/claude-sonnet-4" && m.name === "Overridden Built-in Sonnet"),
models.some((m) => m.id === "anthropic/claude-sonnet-4.5" && m.name === "Overridden Built-in Sonnet"),
).toBe(true);
});
@@ -696,7 +690,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "Custom Sonnet Name",
},
},
@@ -706,15 +700,61 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
expect(sonnet?.name).toBe("Custom Sonnet Name");
// Other models should be unchanged
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1");
expect(opus?.name).not.toBe("Custom Sonnet Name");
});
test("custom model and model override carry sampling params", async () => {
test("Anthropic model override replaces allowed fallback metadata", async () => {
const allowedFallbackModels = [
{
provider: "anthropic",
model: "claude-opus-5",
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
},
{
provider: "anthropic",
model: "claude-opus-4-8",
cost: { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 },
},
];
writeRawModelsJson({
anthropic: {
modelOverrides: {
"claude-fable-5": {
compat: { allowedFallbackModels },
},
},
},
});
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const compat = registry.find("anthropic", "claude-fable-5")?.compat as AnthropicMessagesCompat | undefined;
expect(registry.getError()).toBeUndefined();
expect(compat?.allowedFallbackModels).toEqual(allowedFallbackModels);
});
test("empty allowed fallback model override disables server-side fallback", async () => {
writeRawModelsJson({
anthropic: {
modelOverrides: {
"claude-fable-5": { compat: { allowedFallbackModels: [] } },
},
},
});
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const compat = registry.find("anthropic", "claude-fable-5")?.compat as AnthropicMessagesCompat | undefined;
expect(registry.getError()).toBeUndefined();
expect(compat?.allowedFallbackModels).toEqual([]);
});
test("custom models and model overrides carry sampling params", async () => {
writeRawModelsJson({
openrouter: {
baseUrl: "https://my-proxy.example.com/v1",
@@ -723,11 +763,22 @@ describe("ModelRegistry", () => {
{
id: "custom/sampling-model",
samplingParams: { temperature: 1, top_p: 0.95, top_k: 0 },
samplingParamsByThinkingLevel: {
low: { temperature: 0.6, top_p: 0.95 },
high: { temperature: 0.8 },
},
},
],
modelOverrides: {
"anthropic/claude-sonnet-4": {
"custom/sampling-model": {
samplingParamsByThinkingLevel: {
low: { temperature: 0.5, top_k: 20 },
max: { temperature: 1 },
},
},
"anthropic/claude-sonnet-4.5": {
samplingParams: { top_p: 0.9 },
samplingParamsByThinkingLevel: { high: { temperature: 0.8 } },
},
},
},
@@ -738,20 +789,103 @@ describe("ModelRegistry", () => {
const custom = models.find((m) => m.id === "custom/sampling-model");
expect(custom?.samplingParams).toEqual({ temperature: 1, top_p: 0.95, top_k: 0 });
expect(custom?.samplingParamsByThinkingLevel).toEqual({
low: { temperature: 0.5, top_p: 0.95, top_k: 20 },
high: { temperature: 0.8 },
max: { temperature: 1 },
});
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
expect(sonnet?.samplingParams).toEqual({ top_p: 0.9 });
expect(sonnet?.samplingParamsByThinkingLevel).toEqual({ high: { temperature: 0.8 } });
// Models without sampling config keep it unset.
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1");
expect(opus?.samplingParams).toBeUndefined();
expect(opus?.samplingParamsByThinkingLevel).toBeUndefined();
});
test("custom model and model override carry prompt cache lifetimes", async () => {
writeRawModelsJson({
openrouter: {
baseUrl: "https://my-proxy.example.com/v1",
api: "openai-completions",
models: [{ id: "custom/cached-model", promptCache: { short: 120 } }],
modelOverrides: {
"anthropic/claude-sonnet-4.5": { promptCache: { short: 300 } },
},
},
anthropic: {
modelOverrides: {
"claude-sonnet-4-6": { promptCache: { long: 1800 } },
},
},
});
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const openrouter = getModelsForProvider(registry, "openrouter");
expect(registry.getError()).toBeUndefined();
expect(openrouter.find((m) => m.id === "custom/cached-model")?.promptCache).toEqual({ short: 120 });
expect(openrouter.find((m) => m.id === "anthropic/claude-sonnet-4.5")?.promptCache).toEqual({ short: 300 });
expect(openrouter.find((m) => m.id === "anthropic/claude-opus-4.1")?.promptCache).toBeUndefined();
// Overrides merge per tier with the built-in catalog.
expect(registry.find("anthropic", "claude-sonnet-4-6")?.promptCache).toEqual({ short: 300, long: 1800 });
});
// Regression test for https://github.com/earendil-works/pi/issues/9631
test("model override deep-merges image resize limits", async () => {
writeRawModelsJson({
test: {
baseUrl: "https://example.com",
apiKey: "test-key",
api: "openai-completions",
models: [
{
id: "vision-model",
input: ["text", "image"],
inputLimits: {
maxRequestBytes: 32 * 1024 * 1024,
images: {
maxPerRequest: 100,
resize: {
maxWidth: 2000,
maxHeight: 2000,
maxBytes: 4.5 * 1024 * 1024,
jpegQuality: 80,
},
},
},
},
],
modelOverrides: {
"vision-model": {
inputLimits: {
images: { resize: { maxWidth: 1568, maxBytes: 524288, jpegQuality: 75 } },
},
},
},
},
});
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const model = registry.find("test", "vision-model");
expect(registry.getError()).toBeUndefined();
expect(model?.inputLimits).toMatchObject({
maxRequestBytes: 32 * 1024 * 1024,
images: {
maxPerRequest: 100,
resize: { maxWidth: 1568, maxHeight: 2000, maxBytes: 524288, jpegQuality: 75 },
},
});
});
test("model override with compat.openRouterRouting", async () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
compat: {
openRouterRouting: { only: ["amazon-bedrock"] },
},
@@ -763,7 +897,7 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
const compat = sonnet?.compat as OpenAICompletionsCompat | undefined;
expect(compat?.openRouterRouting).toEqual({ only: ["amazon-bedrock"] });
});
@@ -772,7 +906,7 @@ describe("ModelRegistry", () => {
const provider: ModelsJsonProvider = {
compat: { supportsFinishReason: true },
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
compat: { supportsFinishReason: false },
},
},
@@ -781,8 +915,8 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((model) => model.id === "anthropic/claude-sonnet-4");
const opus = models.find((model) => model.id === "anthropic/claude-opus-4");
const sonnet = models.find((model) => model.id === "anthropic/claude-sonnet-4.5");
const opus = models.find((model) => model.id === "anthropic/claude-opus-4.1");
expect((sonnet?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(false);
expect((opus?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(true);
@@ -792,7 +926,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
compat: {
openRouterRouting: { order: ["anthropic", "together"] },
},
@@ -803,7 +937,7 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
// Should have both the new routing AND preserve other compat settings
const compat = sonnet?.compat as OpenAICompletionsCompat | undefined;
@@ -814,10 +948,10 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
compat: { openRouterRouting: { only: ["amazon-bedrock"] } },
},
"anthropic/claude-opus-4": {
"anthropic/claude-opus-4.1": {
compat: { openRouterRouting: { only: ["anthropic"] } },
},
},
@@ -827,8 +961,8 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1");
const sonnetCompat = sonnet?.compat as OpenAICompletionsCompat | undefined;
const opusCompat = opus?.compat as OpenAICompletionsCompat | undefined;
@@ -841,7 +975,7 @@ describe("ModelRegistry", () => {
openrouter: {
baseUrl: "https://my-proxy.example.com/v1",
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "Proxied Sonnet",
},
},
@@ -850,14 +984,14 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
// Both overrides should apply
expect(sonnet?.baseUrl).toBe("https://my-proxy.example.com/v1");
expect(sonnet?.name).toBe("Proxied Sonnet");
// Other models should have the baseUrl but not the name override
const opus = models.find((m) => m.id === "anthropic/claude-opus-4");
const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1");
expect(opus?.baseUrl).toBe("https://my-proxy.example.com/v1");
expect(opus?.name).not.toBe("Proxied Sonnet");
});
@@ -886,7 +1020,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
cost: { input: 99 },
},
},
@@ -895,7 +1029,7 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
// Input cost should be overridden
expect(sonnet?.cost.input).toBe(99);
@@ -907,7 +1041,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
headers: { "X-Custom-Model-Header": "value" },
},
},
@@ -916,7 +1050,7 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const models = getModelsForProvider(registry, "openrouter");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4");
const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4.5");
expect(sonnet).toBeDefined();
const auth = await registry.getApiKeyAndHeaders(sonnet!);
@@ -930,7 +1064,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "First Name",
},
},
@@ -939,14 +1073,14 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
expect(
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4")?.name,
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4.5")?.name,
).toBe("First Name");
// Update and refresh
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "Second Name",
},
},
@@ -955,7 +1089,7 @@ describe("ModelRegistry", () => {
await registry.refresh();
expect(
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4")?.name,
getModelsForProvider(registry, "openrouter").find((m) => m.id === "anthropic/claude-sonnet-4.5")?.name,
).toBe("Second Name");
});
@@ -963,7 +1097,7 @@ describe("ModelRegistry", () => {
writeRawModelsJson({
openrouter: {
modelOverrides: {
"anthropic/claude-sonnet-4": {
"anthropic/claude-sonnet-4.5": {
name: "Custom Name",
},
},
@@ -972,7 +1106,7 @@ describe("ModelRegistry", () => {
const registry = await createModelRegistry(authStorage, modelsJsonPath);
const customName = getModelsForProvider(registry, "openrouter").find(
(m) => m.id === "anthropic/claude-sonnet-4",
(m) => m.id === "anthropic/claude-sonnet-4.5",
)?.name;
expect(customName).toBe("Custom Name");
@@ -981,7 +1115,7 @@ describe("ModelRegistry", () => {
await registry.refresh();
const restoredName = getModelsForProvider(registry, "openrouter").find(
(m) => m.id === "anthropic/claude-sonnet-4",
(m) => m.id === "anthropic/claude-sonnet-4.5",
)?.name;
expect(restoredName).not.toBe("Custom Name");
});