04633ba208
* fix(models): update model names to match OpenCode Zen catalog OpenCode Zen recently updated their official model catalog, deprecating several preview and free model variants: DEPRECATED → NEW (Official Zen Names): - gemini-3-pro-preview → gemini-3-pro - gemini-3-flash-preview → gemini-3-flash - grok-code → gpt-5-nano (FREE tier maintained) - glm-4.7-free → big-pickle (FREE tier maintained) - glm-4.6v → glm-4.6 Changes: - Updated 6 source files (model-requirements, delegate-task, think-mode, etc.) - Updated 9 documentation files (installation, configurations, features, etc.) - Updated 14 test files with new model references - Regenerated snapshots to reflect catalog changes - Removed duplicate think-mode entries for preview variants Impact: - FREE tier access preserved via gpt-5-nano and big-pickle - All 55 model-related tests passing - Zero breaking changes - pure string replacement - Aligns codebase with official OpenCode Zen model catalog Verified: - Zero deprecated model names in codebase - All model-related tests pass (55/55) - Snapshots regenerated and validated Affects: 30 files (6 source, 9 docs, 14 tests, 1 snapshot) * fix(multimodal-looker): update fallback chain with glm-4.6v and gpt-5-nano - Change glm-4.6 to glm-4.6v for zai-coding-plan provider - Add opencode/gpt-5-nano as 4th fallback (FREE tier) - Push gpt-5.2 to 5th position Fallback chain now: 1. gemini-3-flash (google, github-copilot, opencode) 2. claude-haiku-4-5 (anthropic, github-copilot, opencode) 3. glm-4.6v (zai-coding-plan) 4. gpt-5-nano (opencode) - FREE 5. gpt-5.2 (openai, github-copilot, opencode) * chore: update bun.lock --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
242 lines
8.1 KiB
TypeScript
242 lines
8.1 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
import { mkdtempSync, writeFileSync, rmSync } from "fs"
|
|
import { tmpdir } from "os"
|
|
import { join } from "path"
|
|
import { fetchAvailableModels, fuzzyMatchModel, __resetModelCache } from "./model-availability"
|
|
|
|
describe("fetchAvailableModels", () => {
|
|
let tempDir: string
|
|
let originalXdgCache: string | undefined
|
|
|
|
beforeEach(() => {
|
|
__resetModelCache()
|
|
tempDir = mkdtempSync(join(tmpdir(), "opencode-test-"))
|
|
originalXdgCache = process.env.XDG_CACHE_HOME
|
|
process.env.XDG_CACHE_HOME = tempDir
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalXdgCache !== undefined) {
|
|
process.env.XDG_CACHE_HOME = originalXdgCache
|
|
} else {
|
|
delete process.env.XDG_CACHE_HOME
|
|
}
|
|
rmSync(tempDir, { recursive: true, force: true })
|
|
})
|
|
|
|
function writeModelsCache(data: Record<string, any>) {
|
|
const cacheDir = join(tempDir, "opencode")
|
|
require("fs").mkdirSync(cacheDir, { recursive: true })
|
|
writeFileSync(join(cacheDir, "models.json"), JSON.stringify(data))
|
|
}
|
|
|
|
it("#given cache file with models #when fetchAvailableModels called #then returns Set of model IDs", async () => {
|
|
writeModelsCache({
|
|
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
|
anthropic: { id: "anthropic", models: { "claude-opus-4-5": { id: "claude-opus-4-5" } } },
|
|
google: { id: "google", models: { "gemini-3-pro": { id: "gemini-3-pro" } } },
|
|
})
|
|
|
|
const result = await fetchAvailableModels()
|
|
|
|
expect(result).toBeInstanceOf(Set)
|
|
expect(result.size).toBe(3)
|
|
expect(result.has("openai/gpt-5.2")).toBe(true)
|
|
expect(result.has("anthropic/claude-opus-4-5")).toBe(true)
|
|
expect(result.has("google/gemini-3-pro")).toBe(true)
|
|
})
|
|
|
|
it("#given cache file not found #when fetchAvailableModels called #then returns empty Set", async () => {
|
|
const result = await fetchAvailableModels()
|
|
|
|
expect(result).toBeInstanceOf(Set)
|
|
expect(result.size).toBe(0)
|
|
})
|
|
|
|
it("#given cache read twice #when second call made #then uses cached result", async () => {
|
|
writeModelsCache({
|
|
openai: { id: "openai", models: { "gpt-5.2": { id: "gpt-5.2" } } },
|
|
anthropic: { id: "anthropic", models: { "claude-opus-4-5": { id: "claude-opus-4-5" } } },
|
|
})
|
|
|
|
const result1 = await fetchAvailableModels()
|
|
const result2 = await fetchAvailableModels()
|
|
|
|
expect(result1).toEqual(result2)
|
|
expect(result1.has("openai/gpt-5.2")).toBe(true)
|
|
})
|
|
|
|
it("#given empty providers in cache #when fetchAvailableModels called #then returns empty Set", async () => {
|
|
writeModelsCache({})
|
|
|
|
const result = await fetchAvailableModels()
|
|
|
|
expect(result).toBeInstanceOf(Set)
|
|
expect(result.size).toBe(0)
|
|
})
|
|
|
|
it("#given cache file with various providers #when fetchAvailableModels called #then extracts all IDs correctly", async () => {
|
|
writeModelsCache({
|
|
openai: { id: "openai", models: { "gpt-5.2-codex": { id: "gpt-5.2-codex" } } },
|
|
anthropic: { id: "anthropic", models: { "claude-sonnet-4-5": { id: "claude-sonnet-4-5" } } },
|
|
google: { id: "google", models: { "gemini-3-flash": { id: "gemini-3-flash" } } },
|
|
opencode: { id: "opencode", models: { "gpt-5-nano": { id: "gpt-5-nano" } } },
|
|
})
|
|
|
|
const result = await fetchAvailableModels()
|
|
|
|
expect(result.size).toBe(4)
|
|
expect(result.has("openai/gpt-5.2-codex")).toBe(true)
|
|
expect(result.has("anthropic/claude-sonnet-4-5")).toBe(true)
|
|
expect(result.has("google/gemini-3-flash")).toBe(true)
|
|
expect(result.has("opencode/gpt-5-nano")).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("fuzzyMatchModel", () => {
|
|
// #given available models from multiple providers
|
|
// #when searching for a substring match
|
|
// #then return the matching model
|
|
it("should match substring in model name", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"openai/gpt-5.2-codex",
|
|
"anthropic/claude-opus-4-5",
|
|
])
|
|
const result = fuzzyMatchModel("gpt-5.2", available)
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given available models with partial matches
|
|
// #when searching for a substring
|
|
// #then return exact match if it exists
|
|
it("should prefer exact match over substring match", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"openai/gpt-5.2-codex",
|
|
"openai/gpt-5.2-ultra",
|
|
])
|
|
const result = fuzzyMatchModel("gpt-5.2", available)
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given available models with multiple substring matches
|
|
// #when searching for a substring
|
|
// #then return the shorter model name (more specific)
|
|
it("should prefer shorter model name when multiple matches exist", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2-ultra",
|
|
"openai/gpt-5.2-ultra-mega",
|
|
])
|
|
const result = fuzzyMatchModel("gpt-5.2", available)
|
|
expect(result).toBe("openai/gpt-5.2-ultra")
|
|
})
|
|
|
|
// #given available models with claude variants
|
|
// #when searching for claude-opus
|
|
// #then return matching claude-opus model
|
|
it("should match claude-opus to claude-opus-4-5", () => {
|
|
const available = new Set([
|
|
"anthropic/claude-opus-4-5",
|
|
"anthropic/claude-sonnet-4-5",
|
|
])
|
|
const result = fuzzyMatchModel("claude-opus", available)
|
|
expect(result).toBe("anthropic/claude-opus-4-5")
|
|
})
|
|
|
|
// #given available models from multiple providers
|
|
// #when providers filter is specified
|
|
// #then only search models from specified providers
|
|
it("should filter by provider when providers array is given", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/claude-opus-4-5",
|
|
"google/gemini-3",
|
|
])
|
|
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given available models from multiple providers
|
|
// #when providers filter excludes matching models
|
|
// #then return null
|
|
it("should return null when provider filter excludes all matches", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/claude-opus-4-5",
|
|
])
|
|
const result = fuzzyMatchModel("claude", available, ["openai"])
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
// #given available models
|
|
// #when no substring match exists
|
|
// #then return null
|
|
it("should return null when no match found", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/claude-opus-4-5",
|
|
])
|
|
const result = fuzzyMatchModel("gemini", available)
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
// #given available models with different cases
|
|
// #when searching with different case
|
|
// #then match case-insensitively
|
|
it("should match case-insensitively", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/claude-opus-4-5",
|
|
])
|
|
const result = fuzzyMatchModel("GPT-5.2", available)
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given available models with exact match and longer variants
|
|
// #when searching for exact match
|
|
// #then return exact match first
|
|
it("should prioritize exact match over longer variants", () => {
|
|
const available = new Set([
|
|
"anthropic/claude-opus-4-5",
|
|
"anthropic/claude-opus-4-5-extended",
|
|
])
|
|
const result = fuzzyMatchModel("claude-opus-4-5", available)
|
|
expect(result).toBe("anthropic/claude-opus-4-5")
|
|
})
|
|
|
|
// #given available models with multiple providers
|
|
// #when multiple providers are specified
|
|
// #then search all specified providers
|
|
it("should search all specified providers", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/claude-opus-4-5",
|
|
"google/gemini-3",
|
|
])
|
|
const result = fuzzyMatchModel("gpt", available, ["openai", "google"])
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given available models with provider prefix
|
|
// #when searching with provider filter
|
|
// #then only match models with correct provider prefix
|
|
it("should only match models with correct provider prefix", () => {
|
|
const available = new Set([
|
|
"openai/gpt-5.2",
|
|
"anthropic/gpt-something",
|
|
])
|
|
const result = fuzzyMatchModel("gpt", available, ["openai"])
|
|
expect(result).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
// #given empty available set
|
|
// #when searching
|
|
// #then return null
|
|
it("should return null for empty available set", () => {
|
|
const available = new Set<string>()
|
|
const result = fuzzyMatchModel("gpt", available)
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|