diff --git a/src/shared/model-requirements.test.ts b/src/shared/model-requirements.test.ts new file mode 100644 index 000000000..0103d7ddb --- /dev/null +++ b/src/shared/model-requirements.test.ts @@ -0,0 +1,417 @@ +import { describe, expect, test } from "bun:test" +import { + AGENT_MODEL_REQUIREMENTS, + CATEGORY_MODEL_REQUIREMENTS, + type FallbackEntry, + type ModelRequirement, +} from "./model-requirements" + +describe("AGENT_MODEL_REQUIREMENTS", () => { + test("oracle has valid fallbackChain with gpt-5.2 as primary", () => { + // #given - oracle agent requirement + const oracle = AGENT_MODEL_REQUIREMENTS["oracle"] + + // #when - accessing oracle requirement + // #then - fallbackChain exists with gpt-5.2 as first entry + expect(oracle).toBeDefined() + expect(oracle.fallbackChain).toBeArray() + expect(oracle.fallbackChain.length).toBeGreaterThan(0) + + const primary = oracle.fallbackChain[0] + expect(primary.providers).toContain("openai") + expect(primary.model).toBe("gpt-5.2") + expect(primary.variant).toBe("high") + }) + + test("Sisyphus has valid fallbackChain with claude-opus-4-5 as primary", () => { + // #given - Sisyphus agent requirement + const sisyphus = AGENT_MODEL_REQUIREMENTS["Sisyphus"] + + // #when - accessing Sisyphus requirement + // #then - fallbackChain exists with claude-opus-4-5 as first entry + expect(sisyphus).toBeDefined() + expect(sisyphus.fallbackChain).toBeArray() + expect(sisyphus.fallbackChain.length).toBeGreaterThan(0) + + const primary = sisyphus.fallbackChain[0] + expect(primary.providers[0]).toBe("anthropic") + expect(primary.model).toBe("claude-opus-4-5") + expect(primary.variant).toBe("max") + }) + + test("librarian has valid fallbackChain with glm-4.7 as primary", () => { + // #given - librarian agent requirement + const librarian = AGENT_MODEL_REQUIREMENTS["librarian"] + + // #when - accessing librarian requirement + // #then - fallbackChain exists with glm-4.7 as first entry + expect(librarian).toBeDefined() + expect(librarian.fallbackChain).toBeArray() + expect(librarian.fallbackChain.length).toBeGreaterThan(0) + + const primary = librarian.fallbackChain[0] + expect(primary.providers[0]).toBe("zai-coding-plan") + expect(primary.model).toBe("glm-4.7") + }) + + test("explore has valid fallbackChain with gemini-3-flash-preview as primary", () => { + // #given - explore agent requirement + const explore = AGENT_MODEL_REQUIREMENTS["explore"] + + // #when - accessing explore requirement + // #then - fallbackChain exists with gemini-3-flash-preview as first entry + expect(explore).toBeDefined() + expect(explore.fallbackChain).toBeArray() + expect(explore.fallbackChain.length).toBeGreaterThan(0) + + const primary = explore.fallbackChain[0] + expect(primary.providers).toContain("google") + expect(primary.model).toBe("gemini-3-flash-preview") + }) + + test("multimodal-looker has valid fallbackChain with gemini-3-flash-preview as primary", () => { + // #given - multimodal-looker agent requirement + const multimodalLooker = AGENT_MODEL_REQUIREMENTS["multimodal-looker"] + + // #when - accessing multimodal-looker requirement + // #then - fallbackChain exists with gemini-3-flash-preview as first entry + expect(multimodalLooker).toBeDefined() + expect(multimodalLooker.fallbackChain).toBeArray() + expect(multimodalLooker.fallbackChain.length).toBeGreaterThan(0) + + const primary = multimodalLooker.fallbackChain[0] + expect(primary.providers[0]).toBe("google") + expect(primary.model).toBe("gemini-3-flash-preview") + }) + + test("Prometheus (Planner) has valid fallbackChain with claude-opus-4-5 as primary", () => { + // #given - Prometheus agent requirement + const prometheus = AGENT_MODEL_REQUIREMENTS["Prometheus (Planner)"] + + // #when - accessing Prometheus requirement + // #then - fallbackChain exists with claude-opus-4-5 as first entry + expect(prometheus).toBeDefined() + expect(prometheus.fallbackChain).toBeArray() + expect(prometheus.fallbackChain.length).toBeGreaterThan(0) + + const primary = prometheus.fallbackChain[0] + expect(primary.model).toBe("claude-opus-4-5") + expect(primary.providers[0]).toBe("anthropic") + expect(primary.variant).toBe("max") + }) + + test("Metis (Plan Consultant) has valid fallbackChain with claude-opus-4-5 as primary", () => { + // #given - Metis agent requirement + const metis = AGENT_MODEL_REQUIREMENTS["Metis (Plan Consultant)"] + + // #when - accessing Metis requirement + // #then - fallbackChain exists with claude-opus-4-5 as first entry + expect(metis).toBeDefined() + expect(metis.fallbackChain).toBeArray() + expect(metis.fallbackChain.length).toBeGreaterThan(0) + + const primary = metis.fallbackChain[0] + expect(primary.model).toBe("claude-opus-4-5") + expect(primary.providers[0]).toBe("anthropic") + expect(primary.variant).toBe("max") + }) + + test("Momus (Plan Reviewer) has valid fallbackChain with gpt-5.2 as primary", () => { + // #given - Momus agent requirement + const momus = AGENT_MODEL_REQUIREMENTS["Momus (Plan Reviewer)"] + + // #when - accessing Momus requirement + // #then - fallbackChain exists with gpt-5.2 as first entry, variant medium + expect(momus).toBeDefined() + expect(momus.fallbackChain).toBeArray() + expect(momus.fallbackChain.length).toBeGreaterThan(0) + + const primary = momus.fallbackChain[0] + expect(primary.model).toBe("gpt-5.2") + expect(primary.variant).toBe("medium") + expect(primary.providers[0]).toBe("openai") + }) + + test("Atlas has valid fallbackChain with claude-sonnet-4-5 as primary", () => { + // #given - Atlas agent requirement + const atlas = AGENT_MODEL_REQUIREMENTS["Atlas"] + + // #when - accessing Atlas requirement + // #then - fallbackChain exists with claude-sonnet-4-5 as first entry + expect(atlas).toBeDefined() + expect(atlas.fallbackChain).toBeArray() + expect(atlas.fallbackChain.length).toBeGreaterThan(0) + + const primary = atlas.fallbackChain[0] + expect(primary.model).toBe("claude-sonnet-4-5") + expect(primary.providers[0]).toBe("anthropic") + }) + + test("all 9 builtin agents have valid fallbackChain arrays", () => { + // #given - list of 9 agent names + const expectedAgents = [ + "Sisyphus", + "oracle", + "librarian", + "explore", + "multimodal-looker", + "Prometheus (Planner)", + "Metis (Plan Consultant)", + "Momus (Plan Reviewer)", + "Atlas", + ] + + // #when - checking AGENT_MODEL_REQUIREMENTS + const definedAgents = Object.keys(AGENT_MODEL_REQUIREMENTS) + + // #then - all agents present with valid fallbackChain + expect(definedAgents).toHaveLength(9) + for (const agent of expectedAgents) { + const requirement = AGENT_MODEL_REQUIREMENTS[agent] + expect(requirement).toBeDefined() + expect(requirement.fallbackChain).toBeArray() + expect(requirement.fallbackChain.length).toBeGreaterThan(0) + + for (const entry of requirement.fallbackChain) { + expect(entry.providers).toBeArray() + expect(entry.providers.length).toBeGreaterThan(0) + expect(typeof entry.model).toBe("string") + expect(entry.model.length).toBeGreaterThan(0) + } + } + }) +}) + +describe("CATEGORY_MODEL_REQUIREMENTS", () => { + test("ultrabrain has valid fallbackChain with gpt-5.2-codex as primary", () => { + // #given - ultrabrain category requirement + const ultrabrain = CATEGORY_MODEL_REQUIREMENTS["ultrabrain"] + + // #when - accessing ultrabrain requirement + // #then - fallbackChain exists with gpt-5.2-codex as first entry + expect(ultrabrain).toBeDefined() + expect(ultrabrain.fallbackChain).toBeArray() + expect(ultrabrain.fallbackChain.length).toBeGreaterThan(0) + + const primary = ultrabrain.fallbackChain[0] + expect(primary.variant).toBe("xhigh") + expect(primary.model).toBe("gpt-5.2-codex") + expect(primary.providers[0]).toBe("openai") + }) + + test("visual-engineering has valid fallbackChain with gemini-3-pro-preview as primary", () => { + // #given - visual-engineering category requirement + const visualEngineering = CATEGORY_MODEL_REQUIREMENTS["visual-engineering"] + + // #when - accessing visual-engineering requirement + // #then - fallbackChain exists with gemini-3-pro-preview as first entry + expect(visualEngineering).toBeDefined() + expect(visualEngineering.fallbackChain).toBeArray() + expect(visualEngineering.fallbackChain.length).toBeGreaterThan(0) + + const primary = visualEngineering.fallbackChain[0] + expect(primary.providers[0]).toBe("google") + expect(primary.model).toBe("gemini-3-pro-preview") + }) + + test("quick has valid fallbackChain with claude-haiku-4-5 as primary", () => { + // #given - quick category requirement + const quick = CATEGORY_MODEL_REQUIREMENTS["quick"] + + // #when - accessing quick requirement + // #then - fallbackChain exists with claude-haiku-4-5 as first entry + expect(quick).toBeDefined() + expect(quick.fallbackChain).toBeArray() + expect(quick.fallbackChain.length).toBeGreaterThan(0) + + const primary = quick.fallbackChain[0] + expect(primary.model).toBe("claude-haiku-4-5") + expect(primary.providers[0]).toBe("anthropic") + }) + + test("unspecified-low has valid fallbackChain with claude-sonnet-4-5 as primary", () => { + // #given - unspecified-low category requirement + const unspecifiedLow = CATEGORY_MODEL_REQUIREMENTS["unspecified-low"] + + // #when - accessing unspecified-low requirement + // #then - fallbackChain exists with claude-sonnet-4-5 as first entry + expect(unspecifiedLow).toBeDefined() + expect(unspecifiedLow.fallbackChain).toBeArray() + expect(unspecifiedLow.fallbackChain.length).toBeGreaterThan(0) + + const primary = unspecifiedLow.fallbackChain[0] + expect(primary.model).toBe("claude-sonnet-4-5") + expect(primary.providers[0]).toBe("anthropic") + }) + + test("unspecified-high has valid fallbackChain with claude-opus-4-5 as primary", () => { + // #given - unspecified-high category requirement + const unspecifiedHigh = CATEGORY_MODEL_REQUIREMENTS["unspecified-high"] + + // #when - accessing unspecified-high requirement + // #then - fallbackChain exists with claude-opus-4-5 as first entry + expect(unspecifiedHigh).toBeDefined() + expect(unspecifiedHigh.fallbackChain).toBeArray() + expect(unspecifiedHigh.fallbackChain.length).toBeGreaterThan(0) + + const primary = unspecifiedHigh.fallbackChain[0] + expect(primary.model).toBe("claude-opus-4-5") + expect(primary.variant).toBe("max") + expect(primary.providers[0]).toBe("anthropic") + }) + + test("artistry has valid fallbackChain with gemini-3-pro-preview as primary", () => { + // #given - artistry category requirement + const artistry = CATEGORY_MODEL_REQUIREMENTS["artistry"] + + // #when - accessing artistry requirement + // #then - fallbackChain exists with gemini-3-pro-preview as first entry + expect(artistry).toBeDefined() + expect(artistry.fallbackChain).toBeArray() + expect(artistry.fallbackChain.length).toBeGreaterThan(0) + + const primary = artistry.fallbackChain[0] + expect(primary.model).toBe("gemini-3-pro-preview") + expect(primary.variant).toBe("max") + expect(primary.providers[0]).toBe("google") + }) + + test("writing has valid fallbackChain with gemini-3-flash-preview as primary", () => { + // #given - writing category requirement + const writing = CATEGORY_MODEL_REQUIREMENTS["writing"] + + // #when - accessing writing requirement + // #then - fallbackChain exists with gemini-3-flash-preview as first entry + expect(writing).toBeDefined() + expect(writing.fallbackChain).toBeArray() + expect(writing.fallbackChain.length).toBeGreaterThan(0) + + const primary = writing.fallbackChain[0] + expect(primary.model).toBe("gemini-3-flash-preview") + expect(primary.providers[0]).toBe("google") + }) + + test("all 7 categories have valid fallbackChain arrays", () => { + // #given - list of 7 category names + const expectedCategories = [ + "visual-engineering", + "ultrabrain", + "artistry", + "quick", + "unspecified-low", + "unspecified-high", + "writing", + ] + + // #when - checking CATEGORY_MODEL_REQUIREMENTS + const definedCategories = Object.keys(CATEGORY_MODEL_REQUIREMENTS) + + // #then - all categories present with valid fallbackChain + expect(definedCategories).toHaveLength(7) + for (const category of expectedCategories) { + const requirement = CATEGORY_MODEL_REQUIREMENTS[category] + expect(requirement).toBeDefined() + expect(requirement.fallbackChain).toBeArray() + expect(requirement.fallbackChain.length).toBeGreaterThan(0) + + for (const entry of requirement.fallbackChain) { + expect(entry.providers).toBeArray() + expect(entry.providers.length).toBeGreaterThan(0) + expect(typeof entry.model).toBe("string") + expect(entry.model.length).toBeGreaterThan(0) + } + } + }) +}) + +describe("FallbackEntry type", () => { + test("FallbackEntry structure is correct", () => { + // #given - a valid FallbackEntry object + const entry: FallbackEntry = { + providers: ["anthropic", "github-copilot", "opencode"], + model: "claude-opus-4-5", + variant: "high", + } + + // #when - accessing properties + // #then - all properties are accessible + expect(entry.providers).toEqual(["anthropic", "github-copilot", "opencode"]) + expect(entry.model).toBe("claude-opus-4-5") + expect(entry.variant).toBe("high") + }) + + test("FallbackEntry variant is optional", () => { + // #given - a FallbackEntry without variant + const entry: FallbackEntry = { + providers: ["opencode", "anthropic"], + model: "glm-4.7-free", + } + + // #when - accessing variant + // #then - variant is undefined + expect(entry.variant).toBeUndefined() + }) +}) + +describe("ModelRequirement type", () => { + test("ModelRequirement structure with fallbackChain is correct", () => { + // #given - a valid ModelRequirement object + const requirement: ModelRequirement = { + fallbackChain: [ + { providers: ["anthropic", "github-copilot"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot"], model: "gpt-5.2", variant: "high" }, + ], + } + + // #when - accessing properties + // #then - fallbackChain is accessible with correct structure + expect(requirement.fallbackChain).toBeArray() + expect(requirement.fallbackChain).toHaveLength(2) + expect(requirement.fallbackChain[0].model).toBe("claude-opus-4-5") + expect(requirement.fallbackChain[1].model).toBe("gpt-5.2") + }) + + test("ModelRequirement variant is optional", () => { + // #given - a ModelRequirement without top-level variant + const requirement: ModelRequirement = { + fallbackChain: [{ providers: ["opencode"], model: "glm-4.7-free" }], + } + + // #when - accessing variant + // #then - variant is undefined + expect(requirement.variant).toBeUndefined() + }) + + test("no model in fallbackChain has provider prefix", () => { + // #given - all agent and category requirements + const allRequirements = [ + ...Object.values(AGENT_MODEL_REQUIREMENTS), + ...Object.values(CATEGORY_MODEL_REQUIREMENTS), + ] + + // #when - checking each model in fallbackChain + // #then - none contain "/" (provider prefix) + for (const req of allRequirements) { + for (const entry of req.fallbackChain) { + expect(entry.model).not.toContain("/") + } + } + }) + + test("all fallbackChain entries have non-empty providers array", () => { + // #given - all agent and category requirements + const allRequirements = [ + ...Object.values(AGENT_MODEL_REQUIREMENTS), + ...Object.values(CATEGORY_MODEL_REQUIREMENTS), + ] + + // #when - checking each entry in fallbackChain + // #then - all have non-empty providers array + for (const req of allRequirements) { + for (const entry of req.fallbackChain) { + expect(entry.providers).toBeArray() + expect(entry.providers.length).toBeGreaterThan(0) + } + } + }) +}) diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts new file mode 100644 index 000000000..f2e662094 --- /dev/null +++ b/src/shared/model-requirements.ts @@ -0,0 +1,128 @@ +export type FallbackEntry = { + providers: string[] + model: string + variant?: string // Entry-specific variant (e.g., GPT→high, Opus→max) +} + +export type ModelRequirement = { + fallbackChain: FallbackEntry[] + variant?: string // Default variant (used when entry doesn't specify one) +} + +export const AGENT_MODEL_REQUIREMENTS: Record = { + Sisyphus: { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + oracle: { + fallbackChain: [ + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + librarian: { + fallbackChain: [ + { providers: ["zai-coding-plan"], model: "glm-4.7" }, + { providers: ["opencode"], model: "glm-4.7-free" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }, + ], + }, + explore: { + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" }, + { providers: ["opencode", "github-copilot"], model: "grok-code" }, + ], + }, + "multimodal-looker": { + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, + ], + }, + "Prometheus (Planner)": { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + "Metis (Plan Consultant)": { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + "Momus (Plan Reviewer)": { + fallbackChain: [ + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "medium" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + Atlas: { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, +} + +export const CATEGORY_MODEL_REQUIREMENTS: Record = { + "visual-engineering": { + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + ], + }, + ultrabrain: { + fallbackChain: [ + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "xhigh" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + artistry: { + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview", variant: "max" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, + ], + }, + quick: { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.1-codex-mini" }, + ], + }, + "unspecified-low": { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" }, + ], + }, + "unspecified-high": { + fallbackChain: [ + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5", variant: "max" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" }, + ], + }, + writing: { + fallbackChain: [ + { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash-preview" }, + { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }, + { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, + ], + }, +}