fix: Resolve generic model IDs in agent files
When loading an agent file with a model field like `model: claude-haiku`, the CLI now resolves this to the full hub slug `anthropic/claude-haiku-4-5` before attempting to load the model from the hub. This fixes the "Invalid hub slug format" error that occurred when using generic model IDs in agent files instead of full hub package slugs. Supported generic model IDs: - claude-haiku -> anthropic/claude-haiku-4-5 - claude-sonnet -> anthropic/claude-sonnet-4-5 - claude-opus -> anthropic/claude-opus-4-5 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
75
extensions/cli/src/util/genericModels.test.ts
Normal file
75
extensions/cli/src/util/genericModels.test.ts
Normal file
@@ -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("/");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
73
extensions/cli/src/util/genericModels.ts
Normal file
73
extensions/cli/src/util/genericModels.ts
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user