Fetch live provider model lists instead of the hardcoded catalog

This commit is contained in:
ZacharyZhang-NY committed 2026-05-30 00:51:42 -04:00
1 parent 5737f76d6b
commit cc876e6e16
11 files changed
+235 -11

No files matched your search

@@ -87,11 +87,20 @@ describe("provider contracts", () => {
}))).toThrow("Azure base URL must use HTTPS.");
});
// Providers with no enumerable models endpoint keep returning the curated
// catalog; the rest fetch live (covered offline by provider-models-fetch.test).
const CURATED_MODEL_PROVIDERS = new Set([
"github-copilot", "google-antigravity", "gpt-account", "azure-openai", "aws-bedrock", "vertex-ai",
]);
test("exposes model listing and rate limit adapter contracts", async () => {
for (const provider of PROVIDER_CATALOG) {
const adapter = providerAdapters[provider.key];
const authProfile = adapter.parseAuthProfile(PROVIDER_SECRET_SAMPLES[provider.key]);
await expect(adapter.listModels({ authProfile, model: provider.defaultModel })).resolves.toEqual(provider.models);
expect(typeof adapter.listModels).toBe("function");
if (CURATED_MODEL_PROVIDERS.has(provider.key)) {
await expect(adapter.listModels({ authProfile, model: provider.defaultModel })).resolves.toEqual(provider.models);
}
await expect(adapter.getRateLimitState({ authProfile, model: provider.defaultModel })).resolves.toBeNull();
expect(typeof adapter.streamResponse).toBe("function");
expect(typeof adapter.cancelResponse).toBe("function");
@@ -0,0 +1,58 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { mapAnthropicModels, mapOpenAiModels } from "../../src/main/services/provider-models-fetch";
import { DatabaseConnection } from "../../src/main/database/connection";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters } from "../../src/main/services/provider-adapters";
const testVault = {
encryptSecret: (secret: string) => Buffer.from(secret, "utf8"),
decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"),
};
describe("provider model-list mapping", () => {
test("maps the OpenAI /models shape, deduping by id", () => {
const models = mapOpenAiModels({ data: [{ id: "gpt-x" }, { id: "gpt-y" }, { id: "gpt-x" }, { id: "" }, { other: 1 }] });
expect(models).toEqual([{ id: "gpt-x", name: "gpt-x" }, { id: "gpt-y", name: "gpt-y" }]);
});
test("maps the Anthropic /models shape, preferring display_name", () => {
const models = mapAnthropicModels({ data: [{ id: "claude-x", display_name: "Claude X" }, { id: "claude-y" }] });
expect(models).toEqual([{ id: "claude-x", name: "Claude X" }, { id: "claude-y", name: "claude-y" }]);
});
test("throws on a malformed response instead of returning a static list", () => {
expect(() => mapOpenAiModels({})).toThrow(/expected shape/);
expect(() => mapOpenAiModels({ data: [] })).toThrow(/no models/);
expect(() => mapAnthropicModels({ data: [{ id: "" }] })).toThrow(/no models/);
});
});
describe("ProviderService.listModels routing", () => {
test("routes a typed credential to the matching adapter and returns its live models", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-models-${randomUUID()}.db`));
let parsedSecret = "";
const fakeAdapter = {
...providerAdapters.openai,
authType: "api_key" as const,
parseAuthProfile: (secret: string) => { parsedSecret = secret; return { type: "api_key" as const, apiKey: secret }; },
listModels: async () => [{ id: "live-1", name: "live-1" }, { id: "live-2", name: "live-2" }],
};
const registry = { ...providerAdapters, openai: fakeAdapter };
const service = new ProviderService(connection.db, registry, testVault);
const models = await service.listModels({ providerKey: "openai", secret: "sk-live-key" });
expect(parsedSecret).toBe("sk-live-key");
expect(models).toEqual([{ id: "live-1", name: "live-1" }, { id: "live-2", name: "live-2" }]);
connection.close();
});
test("requires a credential reference when no secret is provided", async () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-models-${randomUUID()}.db`));
const service = new ProviderService(connection.db, providerAdapters, testVault);
await expect(service.listModels({ providerKey: "openai" })).rejects.toThrow(/credential is required/);
connection.close();
});
});