Add configuration-time validation for model names using the model catalog. Invalid models now show fuzzy suggestions. Add --force flag to skip validation. - Add validateModelInCatalog() and validateImageModel() helpers - Update models set, set-image, fallbacks add, image-fallbacks add - Graceful degradation when catalog unavailable - Add tests for validation functions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
155 lines
5.5 KiB
TypeScript
155 lines
5.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mockLoadModelCatalog = vi.fn();
|
|
const mockFindModelInCatalog = vi.fn();
|
|
const mockModelSupportsVision = vi.fn();
|
|
|
|
vi.mock("../../agents/model-catalog.js", () => ({
|
|
loadModelCatalog: () => mockLoadModelCatalog(),
|
|
findModelInCatalog: (catalog: unknown[], provider: string, modelId: string) =>
|
|
mockFindModelInCatalog(catalog, provider, modelId),
|
|
modelSupportsVision: (entry: unknown) => mockModelSupportsVision(entry),
|
|
}));
|
|
|
|
describe("validateModelInCatalog", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mockLoadModelCatalog.mockReset();
|
|
mockFindModelInCatalog.mockReset();
|
|
mockModelSupportsVision.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns valid: true when model is found in catalog", async () => {
|
|
const mockEntry = { id: "claude-opus-4-5", name: "Claude Opus 4.5", provider: "anthropic" };
|
|
mockLoadModelCatalog.mockResolvedValue([mockEntry]);
|
|
mockFindModelInCatalog.mockReturnValue(mockEntry);
|
|
|
|
const { validateModelInCatalog } = await import("./shared.js");
|
|
const result = await validateModelInCatalog("anthropic", "claude-opus-4-5");
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.entry).toEqual(mockEntry);
|
|
expect(result.suggestions).toBeUndefined();
|
|
});
|
|
|
|
it("returns valid: false with suggestions when model not found", async () => {
|
|
const catalog = [
|
|
{ id: "claude-opus-4-5", name: "Claude Opus 4.5", provider: "anthropic" },
|
|
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
];
|
|
mockLoadModelCatalog.mockResolvedValue(catalog);
|
|
mockFindModelInCatalog.mockReturnValue(undefined);
|
|
|
|
const { validateModelInCatalog } = await import("./shared.js");
|
|
const result = await validateModelInCatalog("anthropic", "claude-sonnet-4");
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.suggestions).toBeDefined();
|
|
expect(result.suggestions?.length).toBeGreaterThan(0);
|
|
// claude-sonnet-4-5 should be suggested as it's similar
|
|
expect(result.suggestions).toContain("anthropic/claude-sonnet-4-5");
|
|
});
|
|
|
|
it("returns valid: true when catalog is empty (graceful degradation)", async () => {
|
|
mockLoadModelCatalog.mockResolvedValue([]);
|
|
|
|
const { validateModelInCatalog } = await import("./shared.js");
|
|
const result = await validateModelInCatalog("anthropic", "claude-opus-4-5");
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.entry).toBeUndefined();
|
|
});
|
|
|
|
it("returns valid: true when catalog fails to load (graceful degradation)", async () => {
|
|
mockLoadModelCatalog.mockRejectedValue(new Error("Failed to load catalog"));
|
|
|
|
const { validateModelInCatalog } = await import("./shared.js");
|
|
const result = await validateModelInCatalog("anthropic", "claude-opus-4-5");
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.entry).toBeUndefined();
|
|
});
|
|
|
|
it("fuzzy matches similar model names within same provider", async () => {
|
|
const catalog = [
|
|
{ id: "gpt-4o", name: "GPT-4o", provider: "openai" },
|
|
{ id: "gpt-4o-mini", name: "GPT-4o Mini", provider: "openai" },
|
|
{ id: "gpt-4-turbo", name: "GPT-4 Turbo", provider: "openai" },
|
|
];
|
|
mockLoadModelCatalog.mockResolvedValue(catalog);
|
|
mockFindModelInCatalog.mockReturnValue(undefined);
|
|
|
|
const { validateModelInCatalog } = await import("./shared.js");
|
|
const result = await validateModelInCatalog("openai", "gpt-4");
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.suggestions).toBeDefined();
|
|
// gpt-4o and gpt-4-turbo should be suggested
|
|
expect(result.suggestions?.some((s) => s.includes("gpt-4"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("validateImageModel", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
mockLoadModelCatalog.mockReset();
|
|
mockFindModelInCatalog.mockReset();
|
|
mockModelSupportsVision.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns supportsVision: true when model supports image input", async () => {
|
|
const mockEntry = {
|
|
id: "gpt-4o",
|
|
name: "GPT-4o",
|
|
provider: "openai",
|
|
input: ["text", "image"],
|
|
};
|
|
mockLoadModelCatalog.mockResolvedValue([mockEntry]);
|
|
mockFindModelInCatalog.mockReturnValue(mockEntry);
|
|
mockModelSupportsVision.mockReturnValue(true);
|
|
|
|
const { validateImageModel } = await import("./shared.js");
|
|
const result = await validateImageModel("openai", "gpt-4o");
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.supportsVision).toBe(true);
|
|
});
|
|
|
|
it("returns supportsVision: false when model does not support image input", async () => {
|
|
const mockEntry = {
|
|
id: "gpt-3.5-turbo",
|
|
name: "GPT-3.5 Turbo",
|
|
provider: "openai",
|
|
input: ["text"],
|
|
};
|
|
mockLoadModelCatalog.mockResolvedValue([mockEntry]);
|
|
mockFindModelInCatalog.mockReturnValue(mockEntry);
|
|
mockModelSupportsVision.mockReturnValue(false);
|
|
|
|
const { validateImageModel } = await import("./shared.js");
|
|
const result = await validateImageModel("openai", "gpt-3.5-turbo");
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.supportsVision).toBe(false);
|
|
});
|
|
|
|
it("returns valid: false when model not found", async () => {
|
|
mockLoadModelCatalog.mockResolvedValue([{ id: "gpt-4o", name: "GPT-4o", provider: "openai" }]);
|
|
mockFindModelInCatalog.mockReturnValue(undefined);
|
|
|
|
const { validateImageModel } = await import("./shared.js");
|
|
const result = await validateImageModel("openai", "nonexistent-model");
|
|
|
|
expect(result.valid).toBe(false);
|
|
expect(result.supportsVision).toBeUndefined();
|
|
});
|
|
});
|