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
@@ -52,7 +52,7 @@ describe("AgentSessionRuntime characterization", () => {
faux.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two"), fauxAssistantMessage("three")]);
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await authStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const runtimeOptions = {
agentDir: tempDir,
@@ -343,7 +343,7 @@ describe("AgentSessionRuntime characterization", () => {
faux.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two"), fauxAssistantMessage("three")]);
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await authStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const runtimeOptions = {
agentDir: tempDir,
@@ -454,7 +454,7 @@ describe("AgentSessionRuntime characterization", () => {
mkdirSync(secondDir, { recursive: true });
const { runtime, faux, tempDir } = await createRuntimeForTest(() => {}, { cwd: firstDir });
const otherAuthStorage = AuthStorage.inMemory();
otherAuthStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await otherAuthStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const otherRuntimeOptions = {
agentDir: tempDir,
authStorage: otherAuthStorage,
@@ -527,7 +527,7 @@ describe("AgentSessionRuntime characterization", () => {
const otherDir = join(tempDir, "other");
mkdirSync(otherDir, { recursive: true });
const otherAuthStorage = AuthStorage.inMemory();
otherAuthStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await otherAuthStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const otherRuntimeOptions = {
agentDir: tempDir,
authStorage: otherAuthStorage,
+4 -4
View File
@@ -1,3 +1,4 @@
import { createInMemoryModelRegistry, getModelRuntime } from "../model-runtime-test-utils.ts";
/**
* Local test harness for the new coding-agent test suite.
*/
@@ -18,7 +19,6 @@ import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-sessi
import { AuthStorage } from "../../src/core/auth-storage.ts";
import type { ExtensionRunner } from "../../src/core/extensions/index.ts";
import { convertToLlm } from "../../src/core/messages.ts";
import { ModelRegistry } from "../../src/core/model-registry.ts";
import { SessionManager } from "../../src/core/session-manager.ts";
import type { Settings } from "../../src/core/settings-manager.ts";
import { SettingsManager } from "../../src/core/settings-manager.ts";
@@ -113,9 +113,9 @@ export async function createHarness(options: HarnessOptions = {}): Promise<Harne
const authStorage = AuthStorage.inMemory();
if (withConfiguredAuth) {
authStorage.setRuntimeApiKey(model.provider, "faux-key");
await authStorage.modify(model.provider, async () => ({ type: "api_key", key: "faux-key" }));
}
const modelRegistry = ModelRegistry.inMemory(authStorage);
const modelRegistry = await createInMemoryModelRegistry(authStorage);
if (withConfiguredAuth) {
modelRegistry.registerProvider(model.provider, {
baseUrl: model.baseUrl,
@@ -178,7 +178,7 @@ export async function createHarness(options: HarnessOptions = {}): Promise<Harne
sessionManager,
settingsManager,
cwd: tempDir,
modelRegistry,
modelRuntime: getModelRuntime(modelRegistry),
resourceLoader,
baseToolsOverride: toolMap,
initialActiveToolNames: options.initialActiveToolNames,
@@ -10,6 +10,7 @@ import {
createAgentSessionServices,
} from "../../../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../../../src/core/auth-storage.ts";
import { ModelRuntime } from "../../../src/core/model-runtime.ts";
import { SessionManager } from "../../../src/core/session-manager.ts";
describe("issue #2753 reload stale resource settings", () => {
@@ -32,13 +33,17 @@ describe("issue #2753 reload stale resource settings", () => {
models: [{ id: "faux-1", reasoning: false }],
});
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await authStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const modelRuntime = await ModelRuntime.create({
credentials: authStorage,
modelsPath: join(agentDir, "models.json"),
});
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
const services = await createAgentSessionServices({
cwd,
agentDir,
authStorage,
modelRuntime,
resourceLoaderOptions: {
extensionFactories: [
(pi) => {
@@ -11,6 +11,7 @@ import {
createAgentSessionServices,
} from "../../../src/core/agent-session-runtime.ts";
import { AuthStorage } from "../../../src/core/auth-storage.ts";
import { ModelRuntime } from "../../../src/core/model-runtime.ts";
import { SessionManager } from "../../../src/core/session-manager.ts";
import type { ExtensionAPI, ExtensionCommandContext, ExtensionFactory } from "../../../src/index.ts";
@@ -45,13 +46,17 @@ describe("regression #2860: replaced session callbacks", () => {
faux.setResponses(responses.map((response) => fauxAssistantMessage(response)));
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey(faux.getModel().provider, "faux-key");
await authStorage.modify(faux.getModel().provider, async () => ({ type: "api_key", key: "faux-key" }));
const modelRuntime = await ModelRuntime.create({
credentials: authStorage,
modelsPath: join(tempDir, "models.json"),
});
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
const services = await createAgentSessionServices({
cwd,
agentDir: tempDir,
authStorage,
modelRuntime,
resourceLoaderOptions: {
extensionFactories: [
(pi: ExtensionAPI) => {
@@ -1,5 +1,5 @@
import { setKeybindings, type TUI } from "@earendil-works/pi-tui";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { KeybindingsManager } from "../../../src/core/keybindings.ts";
import { ModelSelectorComponent } from "../../../src/modes/interactive/components/model-selector.ts";
import { ScopedModelsSelectorComponent } from "../../../src/modes/interactive/components/scoped-models-selector.ts";
@@ -13,10 +13,6 @@ function createFakeTui(): TUI {
} as unknown as TUI;
}
async function waitForAsyncRender(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 0));
}
describe("issue #3217 scoped model ordering", () => {
const harnesses: Harness[] = [];
@@ -83,13 +79,15 @@ describe("issue #3217 scoped model ordering", () => {
createFakeTui(),
modelOne,
harness.settingsManager,
harness.session.modelRegistry,
harness.session.modelRuntime,
[{ model: modelTwo }, { model: modelOne }, { model: modelThree }],
() => {},
() => {},
);
await waitForAsyncRender();
await vi.waitFor(() => {
expect(stripAnsi(selector.render(120).join("\n"))).toContain(`[${modelOne.provider}]`);
});
const renderedLines = stripAnsi(selector.render(120).join("\n"))
.split("\n")
@@ -70,6 +70,20 @@ describe("LoginDialogComponent OAuth prompts", () => {
expect(output).toContain("First prompt:");
});
test("preserves neutral information and links when showing a prompt", () => {
const dialog = createDialog();
dialog.showInfo("Configure credentials outside pi.", [
{ label: "Provider documentation", url: "https://example.invalid/docs" },
]);
dialog.showPrompt("Press Enter to continue:");
const output = renderDialog(dialog).join("\n");
expect(output).toContain("Configure credentials outside pi.");
expect(output).toContain("Provider documentation: https://example.invalid/docs");
expect(output).toContain("Press Enter to continue:");
});
test("keeps previous manual input stable when a later prompt is active", async () => {
const dialog = createDialog();
@@ -7,10 +7,10 @@ import { afterEach, describe, expect, it } from "vitest";
import { AgentSession } from "../../../src/core/agent-session.ts";
import { AuthStorage } from "../../../src/core/auth-storage.ts";
import { convertToLlm } from "../../../src/core/messages.ts";
import { ModelRegistry } from "../../../src/core/model-registry.ts";
import { SessionManager } from "../../../src/core/session-manager.ts";
import { SettingsManager } from "../../../src/core/settings-manager.ts";
import { initTheme } from "../../../src/modes/interactive/theme/theme.ts";
import { createInMemoryModelRegistry, getModelRuntime } from "../../model-runtime-test-utils.ts";
import { createTestResourceLoader } from "../../utilities.ts";
describe("regression #5596: missing configured theme export", () => {
@@ -32,8 +32,8 @@ describe("regression #5596: missing configured theme export", () => {
const model = faux.getModel();
const authStorage = AuthStorage.inMemory();
authStorage.setRuntimeApiKey(model.provider, "faux-key");
const modelRegistry = ModelRegistry.inMemory(authStorage);
await authStorage.modify(model.provider, async () => ({ type: "api_key", key: "faux-key" }));
const modelRegistry = await createInMemoryModelRegistry(authStorage);
modelRegistry.registerProvider(model.provider, {
baseUrl: model.baseUrl,
apiKey: "faux-key",
@@ -67,7 +67,7 @@ describe("regression #5596: missing configured theme export", () => {
sessionManager,
settingsManager,
cwd: tempDir,
modelRegistry,
modelRuntime: getModelRuntime(modelRegistry),
resourceLoader: createTestResourceLoader(),
});
cleanups.push(() => {
@@ -3,8 +3,8 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { ENV_AGENT_DIR } from "../../../src/config.ts";
import { AuthStorage } from "../../../src/core/auth-storage.ts";
import { ModelRegistry } from "../../../src/core/model-registry.ts";
import { runMigrations } from "../../../src/migrations.ts";
import { createModelRegistry } from "../../model-runtime-test-utils.ts";
import { createHarness } from "../harness.ts";
describe("regression #5661: uppercase models.json header values", () => {
@@ -79,7 +79,7 @@ describe("regression #5661: uppercase models.json header values", () => {
expect(migrated.providers["my-provider"]?.apiKey).toBe("CUSTOM_API_KEY");
expect(migrated.providers["my-provider"]?.headers?.Authorization).toBe("BEARER");
const registry = ModelRegistry.create(AuthStorage.create(join(harness.tempDir, "auth.json")), modelsPath);
const registry = await createModelRegistry(AuthStorage.create(join(harness.tempDir, "auth.json")), modelsPath);
const model = registry.find("my-provider", "my-model");
expect(model).toBeDefined();
expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({