diff --git a/extensions/cli/src/services/AgentFileService.ts b/extensions/cli/src/services/AgentFileService.ts index e13078698..28438d2b6 100644 --- a/extensions/cli/src/services/AgentFileService.ts +++ b/extensions/cli/src/services/AgentFileService.ts @@ -16,6 +16,7 @@ import { loadModelFromHub, loadPackageFromHub, } from "../hubLoader.js"; +import { resolveModelSlug } from "../util/genericModels.js"; import { logger } from "../util/logger.js"; import { BaseService, ServiceWithDependencies } from "./BaseService.js"; @@ -122,7 +123,9 @@ export class AgentFileService "Cannot load agent model, failed to load api client service", ); } - const model = await loadModelFromHub(agentFile.model); + // Resolve generic model IDs (like "claude-haiku") to full hub slugs + const resolvedModelSlug = resolveModelSlug(agentFile.model); + const model = await loadModelFromHub(resolvedModelSlug); this.setState({ agentFileModel: model, }); diff --git a/extensions/cli/src/util/genericModels.test.ts b/extensions/cli/src/util/genericModels.test.ts new file mode 100644 index 000000000..a107225dd --- /dev/null +++ b/extensions/cli/src/util/genericModels.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; + +import { + getAllGenericModels, + isGenericModelId, + resolveModelSlug, +} from "./genericModels.js"; + +describe("genericModels", () => { + describe("resolveModelSlug", () => { + it("should resolve claude-haiku to anthropic/claude-haiku-4-5", () => { + expect(resolveModelSlug("claude-haiku")).toBe( + "anthropic/claude-haiku-4-5", + ); + }); + + it("should resolve claude-sonnet to anthropic/claude-sonnet-4-5", () => { + expect(resolveModelSlug("claude-sonnet")).toBe( + "anthropic/claude-sonnet-4-5", + ); + }); + + it("should resolve claude-opus to anthropic/claude-opus-4-5", () => { + expect(resolveModelSlug("claude-opus")).toBe("anthropic/claude-opus-4-5"); + }); + + it("should pass through slugs that already contain /", () => { + expect(resolveModelSlug("anthropic/claude-haiku-4-5")).toBe( + "anthropic/claude-haiku-4-5", + ); + expect(resolveModelSlug("openai/gpt-4")).toBe("openai/gpt-4"); + }); + + it("should pass through unknown model IDs unchanged", () => { + expect(resolveModelSlug("unknown-model")).toBe("unknown-model"); + expect(resolveModelSlug("gpt-4")).toBe("gpt-4"); + }); + }); + + describe("isGenericModelId", () => { + it("should return true for known generic model IDs", () => { + expect(isGenericModelId("claude-haiku")).toBe(true); + expect(isGenericModelId("claude-sonnet")).toBe(true); + expect(isGenericModelId("claude-opus")).toBe(true); + }); + + it("should return false for unknown model IDs", () => { + expect(isGenericModelId("unknown-model")).toBe(false); + expect(isGenericModelId("gpt-4")).toBe(false); + expect(isGenericModelId("anthropic/claude-haiku-4-5")).toBe(false); + }); + }); + + describe("getAllGenericModels", () => { + it("should return all defined generic models", () => { + const models = getAllGenericModels(); + expect(models.length).toBeGreaterThan(0); + expect(models.some((m) => m.id === "claude-haiku")).toBe(true); + expect(models.some((m) => m.id === "claude-sonnet")).toBe(true); + expect(models.some((m) => m.id === "claude-opus")).toBe(true); + }); + + it("should include all required fields for each model", () => { + const models = getAllGenericModels(); + for (const model of models) { + expect(model.id).toBeDefined(); + expect(model.displayName).toBeDefined(); + expect(model.provider).toBeDefined(); + expect(model.description).toBeDefined(); + expect(model.currentModelPackageSlug).toBeDefined(); + expect(model.currentModelPackageSlug).toContain("/"); + } + }); + }); +}); diff --git a/extensions/cli/src/util/genericModels.ts b/extensions/cli/src/util/genericModels.ts new file mode 100644 index 000000000..de6b0051b --- /dev/null +++ b/extensions/cli/src/util/genericModels.ts @@ -0,0 +1,73 @@ +/** + * Generic model ID to hub slug mapping. + * These allow users to specify simplified model IDs in agent files + * that get resolved to their full hub package slugs. + */ + +interface GenericModelDefinition { + id: string; + displayName: string; + provider: string; + description: string; + currentModelPackageSlug: string; +} + +const GENERIC_MODELS: readonly GenericModelDefinition[] = [ + { + id: "claude-opus", + displayName: "Claude Opus 4.5", + provider: "anthropic", + description: "Most capable, best for complex tasks", + currentModelPackageSlug: "anthropic/claude-opus-4-5", + }, + { + id: "claude-sonnet", + displayName: "Claude Sonnet 4.5", + provider: "anthropic", + description: "Balanced performance and speed", + currentModelPackageSlug: "anthropic/claude-sonnet-4-5", + }, + { + id: "claude-haiku", + displayName: "Claude Haiku 4.5", + provider: "anthropic", + description: "Fast and efficient", + currentModelPackageSlug: "anthropic/claude-haiku-4-5", + }, +]; + +/** + * Resolve a generic model ID to its current package slug. + * If the model is already a valid slug (contains "/"), returns it unchanged. + * If it's a generic ID, resolves to the full package slug. + * Returns the original value if not recognized (to allow hub slugs to pass through). + */ +export function resolveModelSlug(modelId: string): string { + // If it already looks like a hub slug (contains "/"), return as-is + if (modelId.includes("/")) { + return modelId; + } + + // Try to find a matching generic model + const genericModel = GENERIC_MODELS.find((m) => m.id === modelId); + if (genericModel) { + return genericModel.currentModelPackageSlug; + } + + // Return original value - loadModelFromHub will handle validation + return modelId; +} + +/** + * Check if a model ID is a known generic model ID + */ +export function isGenericModelId(modelId: string): boolean { + return GENERIC_MODELS.some((m) => m.id === modelId); +} + +/** + * Get all available generic models + */ +export function getAllGenericModels(): readonly GenericModelDefinition[] { + return GENERIC_MODELS; +}