fix: remove hardcoded model defaults from categories and agents

BREAKING CHANGE: Model resolution overhauled

- Created centralized model-resolver.ts with priority chain:
  userModel → inheritedModel → systemDefaultModel
- Removed model field from all 7 DEFAULT_CATEGORIES entries
- Removed DEFAULT_MODEL constants from 10 agents
- Removed singleton agent exports (use factories instead)
- Made CategoryConfigSchema.model optional
- CLI no longer generates model overrides
- Empty strings treated as unset (uses fallback)

Users must now:
1. Use factory functions (createOracleAgent, etc.) instead of singletons
2. Provide model explicitly or use systemDefaultModel
3. Configure category models explicitly if needed

Fixes model fallback bug where hardcoded defaults overrode
user's OpenCode configured model.
This commit is contained in:
Kenny
2026-01-17 12:51:03 -05:00
parent be1af6d980
commit 76508cfc7d
31 changed files with 459 additions and 496 deletions
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, test } from "bun:test";
import { resolveModel, type ModelResolutionInput } from "./model-resolver";
describe("resolveModel", () => {
describe("priority chain", () => {
test("returns userModel when all three are set", () => {
// #given
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-5",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
};
// #when
const result = resolveModel(input);
// #then
expect(result).toBe("anthropic/claude-opus-4-5");
});
test("returns inheritedModel when userModel is undefined", () => {
// #given
const input: ModelResolutionInput = {
userModel: undefined,
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
};
// #when
const result = resolveModel(input);
// #then
expect(result).toBe("openai/gpt-5.2");
});
test("returns systemDefault when both userModel and inheritedModel are undefined", () => {
// #given
const input: ModelResolutionInput = {
userModel: undefined,
inheritedModel: undefined,
systemDefault: "google/gemini-3-pro",
};
// #when
const result = resolveModel(input);
// #then
expect(result).toBe("google/gemini-3-pro");
});
});
describe("empty string handling", () => {
test("treats empty string as unset, uses fallback", () => {
// #given
const input: ModelResolutionInput = {
userModel: "",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
};
// #when
const result = resolveModel(input);
// #then
expect(result).toBe("openai/gpt-5.2");
});
test("treats whitespace-only string as unset, uses fallback", () => {
// #given
const input: ModelResolutionInput = {
userModel: " ",
inheritedModel: "",
systemDefault: "google/gemini-3-pro",
};
// #when
const result = resolveModel(input);
// #then
expect(result).toBe("google/gemini-3-pro");
});
});
describe("purity", () => {
test("same input returns same output (referential transparency)", () => {
// #given
const input: ModelResolutionInput = {
userModel: "anthropic/claude-opus-4-5",
inheritedModel: "openai/gpt-5.2",
systemDefault: "google/gemini-3-pro",
};
// #when
const result1 = resolveModel(input);
const result2 = resolveModel(input);
// #then
expect(result1).toBe(result2);
});
});
});