feat(coding-agent): replace model registry with model runtime

Move provider auth and OAuth flows onto pi-ai Models, compose models.json and extension overlays through ModelRuntime, and retain ModelRegistry as an extension compatibility facade.
This commit is contained in:
Mario Zechner
2026-07-14 17:48:45 +02:00
parent 6731a0ba9e
commit 9993c96907
133 changed files with 5103 additions and 4340 deletions
@@ -260,12 +260,12 @@ describe("resolveModelScopeWithDiagnostics", () => {
describe("resolveCliModel", () => {
test("resolves --model provider/id without --provider", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openai/gpt-4o",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -275,13 +275,13 @@ describe("resolveCliModel", () => {
test("resolves fuzzy patterns within an explicit provider", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "openai",
cliModel: "4o",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -291,12 +291,12 @@ describe("resolveCliModel", () => {
test("supports --model <pattern>:<thinking> (without explicit --thinking)", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "sonnet:high",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -306,12 +306,12 @@ describe("resolveCliModel", () => {
test("prefers exact model id match over provider inference (OpenRouter-style ids)", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openai/gpt-4o:extended",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -321,13 +321,13 @@ describe("resolveCliModel", () => {
test("does not strip invalid :suffix as thinking level in --model (treat as raw id)", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "openai",
cliModel: "gpt-4o:extended",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -337,13 +337,13 @@ describe("resolveCliModel", () => {
test("allows custom model ids for explicit providers without double prefixing", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "openrouter",
cliModel: "openrouter/openai/ghost-model",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -353,13 +353,13 @@ describe("resolveCliModel", () => {
test("returns a clear error when there are no models", () => {
const registry = {
getAll: () => [],
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => [],
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "openai",
cliModel: "gpt-4o",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.model).toBeUndefined();
@@ -394,13 +394,13 @@ describe("resolveCliModel", () => {
maxTokens: 8192,
};
const registry = {
getAll: () => [...allModels, zaiModel, gatewayModel],
getModels: () => [...allModels, zaiModel, gatewayModel],
hasConfiguredAuth: () => true,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "zai/glm-5",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -434,13 +434,13 @@ describe("resolveCliModel", () => {
maxTokens: 8192,
};
const registry = {
getAll: () => [...allModels, commandcodeModel, xiaomiModel],
hasConfiguredAuth: (model: Model<"anthropic-messages">) => model.provider === "commandcode",
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => [...allModels, commandcodeModel, xiaomiModel],
hasConfiguredAuth: (provider: string) => provider === "commandcode",
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "xiaomi/mimo-v2.5-pro",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -450,12 +450,12 @@ describe("resolveCliModel", () => {
test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "openrouter/qwen",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -483,12 +483,12 @@ describe("resolveCliModel", () => {
test("strips :thinking suffix from custom model id in fallback path", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:high",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -501,12 +501,12 @@ describe("resolveCliModel", () => {
test("custom model without thinking suffix works normally in fallback path", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "neuralwatt/zai-org/GLM-5.1-FP8",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -517,13 +517,13 @@ describe("resolveCliModel", () => {
test("all valid thinking levels work in fallback path", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"]) {
const result = resolveCliModel({
cliModel: `neuralwatt/zai-org/GLM-5.1-FP8:${level}`,
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -534,12 +534,12 @@ describe("resolveCliModel", () => {
test("invalid thinking suffix on custom model is treated as part of model id", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:banana",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -551,13 +551,13 @@ describe("resolveCliModel", () => {
test("explicit --provider with custom model:thinking strips suffix correctly", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliProvider: "neuralwatt",
cliModel: "zai-org/GLM-5.1-FP8:high",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -568,13 +568,13 @@ describe("resolveCliModel", () => {
test("with explicit --thinking, :suffix is kept as part of model id", () => {
const registry = {
getAll: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRegistry"];
getModels: () => modelsWithNeuralwatt,
} as unknown as Parameters<typeof resolveCliModel>[0]["modelRuntime"];
const result = resolveCliModel({
cliModel: "neuralwatt/zai-org/GLM-5.1-FP8:high",
cliThinking: "medium",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.error).toBeUndefined();
@@ -606,15 +606,15 @@ describe("default model selection", () => {
test("findInitialModel accepts explicit provider custom model ids", async () => {
const registry = {
getAll: () => allModels,
} as unknown as Parameters<typeof findInitialModel>[0]["modelRegistry"];
getModels: () => allModels,
} as unknown as Parameters<typeof findInitialModel>[0]["modelRuntime"];
const result = await findInitialModel({
cliProvider: "openrouter",
cliModel: "openrouter/openai/ghost-model",
scopedModels: [],
isContinuing: false,
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.model?.provider).toBe("openrouter");
@@ -637,12 +637,12 @@ describe("default model selection", () => {
const registry = {
getAvailable: async () => [aiGatewayModel],
} as unknown as Parameters<typeof findInitialModel>[0]["modelRegistry"];
} as unknown as Parameters<typeof findInitialModel>[0]["modelRuntime"];
const result = await findInitialModel({
scopedModels: [],
isContinuing: false,
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.model?.provider).toBe("vercel-ai-gateway");
@@ -668,20 +668,20 @@ describe("default model selection", () => {
baseUrl: "http://spark-two:8000/v1",
};
const registry = {
find: (provider: string, modelId: string) =>
getModel: (provider: string, modelId: string) =>
provider === savedDeepSeekModel.provider && modelId === savedDeepSeekModel.id
? savedDeepSeekModel
: undefined,
hasConfiguredAuth: (model: Model<"anthropic-messages">) => model.provider === "spark-two",
hasConfiguredAuth: (provider: string) => provider === "spark-two",
getAvailable: async () => [localDeepSeekModel],
} as unknown as Parameters<typeof findInitialModel>[0]["modelRegistry"];
} as unknown as Parameters<typeof findInitialModel>[0]["modelRuntime"];
const result = await findInitialModel({
scopedModels: [],
isContinuing: false,
defaultProvider: "deepseek",
defaultModelId: "deepseek-v4-flash",
modelRegistry: registry,
modelRuntime: registry,
});
expect(result.model?.provider).toBe("spark-two");