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:
@@ -1,15 +1,11 @@
|
||||
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { AuthStorage } from "../src/core/auth-storage.ts";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../src/core/provider-display-names.ts";
|
||||
import { OAuthSelectorComponent } from "../src/modes/interactive/components/oauth-selector.ts";
|
||||
import { isApiKeyLoginProvider } from "../src/modes/interactive/interactive-mode.ts";
|
||||
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../src/utils/ansi.ts";
|
||||
|
||||
const originalOpenAiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
describe("OAuthSelectorComponent", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
@@ -19,119 +15,133 @@ describe("OAuthSelectorComponent", () => {
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalOpenAiApiKey === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = originalOpenAiApiKey;
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps built-in API key providers separate from OAuth-only providers", () => {
|
||||
const oauthProviderIds = new Set(["anthropic", "github-copilot", "custom-oauth"]);
|
||||
const builtInProviderIds = new Set(["anthropic", "github-copilot", "amazon-bedrock", "openai"]);
|
||||
|
||||
expect(isApiKeyLoginProvider("anthropic", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||
expect(BUILT_IN_PROVIDER_DISPLAY_NAMES.anthropic).toBe("Anthropic");
|
||||
expect(isApiKeyLoginProvider("openai", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||
expect(isApiKeyLoginProvider("github-copilot", oauthProviderIds, builtInProviderIds)).toBe(false);
|
||||
expect(isApiKeyLoginProvider("amazon-bedrock", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||
expect(isApiKeyLoginProvider("custom-oauth", oauthProviderIds, builtInProviderIds)).toBe(false);
|
||||
expect(isApiKeyLoginProvider("custom-api", oauthProviderIds, builtInProviderIds)).toBe(true);
|
||||
});
|
||||
|
||||
it("shows stored OAuth auth distinctly in the API key selector", () => {
|
||||
const authStorage = AuthStorage.inMemory({
|
||||
anthropic: {
|
||||
type: "oauth",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
it("projects provider-owned auth options without provider-specific filtering", () => {
|
||||
const getLoginProviderOptions = (
|
||||
InteractiveMode as unknown as {
|
||||
prototype: {
|
||||
getLoginProviderOptions(
|
||||
this: object,
|
||||
authType?: "oauth" | "api_key",
|
||||
): Array<{ id: string; name: string; authType: string; method?: { name: string; login?: unknown } }>;
|
||||
};
|
||||
}
|
||||
).prototype.getLoginProviderOptions;
|
||||
const providers = [
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic",
|
||||
auth: {
|
||||
oauth: { name: "Anthropic (Claude Pro/Max)", login: async () => ({}) },
|
||||
apiKey: { name: "Anthropic API key", login: async () => ({}) },
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
id: "google-vertex",
|
||||
name: "Google Vertex AI",
|
||||
auth: { apiKey: { name: "Google Cloud credentials" } },
|
||||
},
|
||||
];
|
||||
const fakeThis = {
|
||||
session: {
|
||||
modelRuntime: {
|
||||
getProviders: () => providers,
|
||||
getProviderAuthStatus: () => ({ configured: false }),
|
||||
isUsingOAuth: () => false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const apiKeyOptions = getLoginProviderOptions.call(fakeThis, "api_key");
|
||||
expect(apiKeyOptions).toMatchObject([
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic",
|
||||
authType: "api_key",
|
||||
method: { name: "Anthropic API key" },
|
||||
},
|
||||
{
|
||||
id: "google-vertex",
|
||||
name: "Google Vertex AI",
|
||||
authType: "api_key",
|
||||
method: { name: "Google Cloud credentials" },
|
||||
},
|
||||
]);
|
||||
expect(getLoginProviderOptions.call(fakeThis, "oauth")).toMatchObject([
|
||||
{ id: "anthropic", name: "Anthropic", authType: "oauth" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders an option without compiled auth status as unconfigured", () => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
authStorage,
|
||||
[{ id: "anthropic", name: "Anthropic", authType: "api_key" }],
|
||||
[{ id: "google", name: "Google", authType: "api_key", status: undefined }],
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
expect(output).toContain("unconfigured");
|
||||
expect(output).not.toContain("✓ configured");
|
||||
});
|
||||
|
||||
expect(output).toContain("Anthropic");
|
||||
it("shows OAuth auth distinctly in the API key selector", () => {
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
[{ id: "anthropic", name: "Anthropic", authType: "api_key", status: { type: "oauth", source: "OAuth" } }],
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
expect(output).toContain("subscription configured");
|
||||
});
|
||||
|
||||
it("shows environment API key auth as configured", () => {
|
||||
process.env.OPENAI_API_KEY = "test-openai-key";
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
authStorage,
|
||||
[{ id: "openai", name: "OpenAI", authType: "api_key" }],
|
||||
[{ id: "openai", name: "OpenAI", authType: "api_key", status: { type: "api_key", source: "OPENAI_API_KEY" } }],
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
|
||||
expect(output).toContain("OpenAI");
|
||||
expect(output).toContain("✓ env: OPENAI_API_KEY");
|
||||
expect(output).not.toContain("unconfigured");
|
||||
});
|
||||
|
||||
it("shows custom provider environment API key auth from status resolver", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
authStorage,
|
||||
[{ id: "ollama", name: "ollama", authType: "api_key" }],
|
||||
() => {},
|
||||
() => {},
|
||||
() => ({ configured: true, source: "environment", label: "OLLAMA_API_KEY" }),
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
|
||||
expect(output).toContain("ollama");
|
||||
expect(output).toContain("✓ env: OLLAMA_API_KEY");
|
||||
expect(output).not.toContain("unconfigured");
|
||||
});
|
||||
|
||||
it("shows models.json API key auth as configured", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
authStorage,
|
||||
[{ id: "local-proxy", name: "local-proxy", authType: "api_key" }],
|
||||
[
|
||||
{
|
||||
id: "local-proxy",
|
||||
name: "local-proxy",
|
||||
authType: "api_key",
|
||||
status: { type: "api_key", source: "key in models.json" },
|
||||
},
|
||||
],
|
||||
() => {},
|
||||
() => {},
|
||||
() => ({ configured: true, source: "models_json_key" }),
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
|
||||
expect(output).toContain("local-proxy");
|
||||
expect(output).toContain("✓ key in models.json");
|
||||
expect(output).not.toContain("unconfigured");
|
||||
expect(stripAnsi(selector.render(120).join("\n"))).toContain("✓ key in models.json");
|
||||
});
|
||||
|
||||
it("shows models.json command auth as configured", () => {
|
||||
const authStorage = AuthStorage.inMemory();
|
||||
const selector = new OAuthSelectorComponent(
|
||||
"login",
|
||||
authStorage,
|
||||
[{ id: "op-proxy", name: "op-proxy", authType: "api_key" }],
|
||||
[
|
||||
{
|
||||
id: "op-proxy",
|
||||
name: "op-proxy",
|
||||
authType: "api_key",
|
||||
status: { type: "api_key", source: "command in models.json" },
|
||||
},
|
||||
],
|
||||
() => {},
|
||||
() => {},
|
||||
() => ({ configured: true, source: "models_json_command" }),
|
||||
);
|
||||
|
||||
const output = stripAnsi(selector.render(120).join("\n"));
|
||||
|
||||
expect(output).toContain("op-proxy");
|
||||
expect(output).toContain("✓ command in models.json");
|
||||
expect(output).not.toContain("unconfigured");
|
||||
expect(stripAnsi(selector.render(120).join("\n"))).toContain("✓ command in models.json");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user