From 87de056308676dab27130f6e9330accf1593a277 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 24 May 2026 17:16:40 +0900 Subject: [PATCH] refactor(model-core): move model family detectors Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/model-core/src/index.ts | 1 + .../src/model-family-detectors.test.ts | 50 +++++++++++++++ .../model-core/src/model-family-detectors.ts | 45 ++++++++++++++ src/agents/types.ts | 61 +++---------------- 4 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 packages/model-core/src/model-family-detectors.test.ts create mode 100644 packages/model-core/src/model-family-detectors.ts diff --git a/packages/model-core/src/index.ts b/packages/model-core/src/index.ts index 79cb65929..a7d3c9536 100644 --- a/packages/model-core/src/index.ts +++ b/packages/model-core/src/index.ts @@ -1,4 +1,5 @@ export * from "./model-requirements" +export * from "./model-family-detectors" export * from "./model-capability-aliases" export * from "./model-capability-heuristics" export * from "./model-capability-guardrails" diff --git a/packages/model-core/src/model-family-detectors.test.ts b/packages/model-core/src/model-family-detectors.test.ts new file mode 100644 index 000000000..504e646e6 --- /dev/null +++ b/packages/model-core/src/model-family-detectors.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test" +import { + isClaudeOpus47Model, + isGeminiModel, + isGlmModel, + isGptModel, + isKimiK2Model, + isMiniMaxModel, +} from "./model-family-detectors" + +describe("model family detectors", () => { + test("#given GPT model ids #then detects GPT family only", () => { + expect(isGptModel("openai/gpt-5.5")).toBe(true) + expect(isGptModel("github-copilot/gpt-4o")).toBe(true) + expect(isGptModel("openai/o3-mini")).toBe(false) + expect(isGptModel("anthropic/claude-opus-4-7")).toBe(false) + }) + + test("#given Gemini model ids #then detects Gemini family only", () => { + expect(isGeminiModel("google/gemini-3.1-pro")).toBe(true) + expect(isGeminiModel("google-vertex/gemini-3-flash")).toBe(true) + expect(isGeminiModel("github-copilot/gemini-3.1-pro")).toBe(true) + expect(isGeminiModel("openai/gpt-5.5")).toBe(false) + }) + + test("#given Kimi K2 model ids #then detects Kimi K2 family only", () => { + expect(isKimiK2Model("moonshotai/kimi-k2.6")).toBe(true) + expect(isKimiK2Model("opencode/k2p5")).toBe(true) + expect(isKimiK2Model("opencode/k2-p6")).toBe(true) + expect(isKimiK2Model("anthropic/claude-opus-4-7")).toBe(false) + }) + + test("#given GLM model ids #then detects GLM family only", () => { + expect(isGlmModel("z-ai/glm-5.1")).toBe(true) + expect(isGlmModel("opencode/glm-4.6v")).toBe(true) + expect(isGlmModel("google/gemini-3.1-pro")).toBe(false) + }) + + test("#given Claude Opus 4.7 model ids #then detects Opus 4.7 only", () => { + expect(isClaudeOpus47Model("anthropic/claude-opus-4-7")).toBe(true) + expect(isClaudeOpus47Model("anthropic/claude-opus-4.7")).toBe(true) + expect(isClaudeOpus47Model("anthropic/claude-sonnet-4-6")).toBe(false) + }) + + test("#given MiniMax model ids #then detects MiniMax family only", () => { + expect(isMiniMaxModel("opencode/minimax-m2.7")).toBe(true) + expect(isMiniMaxModel("minimax-m2.7-highspeed")).toBe(true) + expect(isMiniMaxModel("moonshotai/kimi-k2.6")).toBe(false) + }) +}) diff --git a/packages/model-core/src/model-family-detectors.ts b/packages/model-core/src/model-family-detectors.ts new file mode 100644 index 000000000..c87203d58 --- /dev/null +++ b/packages/model-core/src/model-family-detectors.ts @@ -0,0 +1,45 @@ +function extractModelName(model: string): string { + return model.includes("/") ? (model.split("/").pop() ?? model) : model +} + +export function isGptModel(model: string): boolean { + const modelName = extractModelName(model).toLowerCase() + return modelName.includes("gpt") +} + +export function isClaudeOpus47Model(model: string): boolean { + const modelName = extractModelName(model).toLowerCase().replaceAll(".", "-") + return modelName.includes("claude-opus-4-7") +} + +export function isKimiK2Model(model: string): boolean { + const modelName = extractModelName(model).toLowerCase() + if (modelName.includes("kimi")) return true + if (/k2[-.]?p[56]/.test(modelName)) return true + return false +} + +export function isMiniMaxModel(model: string): boolean { + const modelName = extractModelName(model).toLowerCase() + return modelName.includes("minimax") +} + +export function isGlmModel(model: string): boolean { + const modelName = extractModelName(model).toLowerCase() + return modelName.includes("glm") +} + +const GEMINI_PROVIDERS = ["google/", "google-vertex/"] as const + +export function isGeminiModel(model: string): boolean { + if (GEMINI_PROVIDERS.some((prefix) => model.startsWith(prefix))) return true + + if ( + model.startsWith("github-copilot/") && + extractModelName(model).toLowerCase().startsWith("gemini") + ) + return true + + const modelName = extractModelName(model).toLowerCase() + return modelName.startsWith("gemini-") +} diff --git a/src/agents/types.ts b/src/agents/types.ts index 111cdefc0..9b582527f 100644 --- a/src/agents/types.ts +++ b/src/agents/types.ts @@ -1,5 +1,14 @@ import type { AgentConfig } from "@opencode-ai/sdk"; +export { + isClaudeOpus47Model, + isGeminiModel, + isGlmModel, + isGptModel, + isKimiK2Model, + isMiniMaxModel, +} from "@oh-my-opencode/model-core"; + /** * Agent mode determines UI model selection behavior: * - "primary": Respects user's UI-selected model (sisyphus, atlas) @@ -74,11 +83,6 @@ function extractModelName(model: string): string { return model.includes("/") ? (model.split("/").pop() ?? model) : model; } -export function isGptModel(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("gpt"); -} - const GPT_NATIVE_SISYPHUS_RE = /gpt-5[.-](?:[4-9]|\d{2,})/i; export function isGptNativeSisyphusModel(model: string): boolean { @@ -101,53 +105,6 @@ export function isGpt5_2Model(model: string): boolean { return modelName.includes("gpt-5.2") || modelName.includes("gpt-5-2"); } -export function isClaudeOpus47Model(model: string): boolean { - const modelName = extractModelName(model).toLowerCase().replaceAll(".", "-"); - return modelName.includes("claude-opus-4-7"); -} - -/** - * Kimi K2.x model detection (K2.5 / K2.6 family). - * - * Matches model IDs containing any of: - * - "kimi" (provider/family signal — kimi-k2.6, moonshotai/Kimi-K2.6, etc.) - * - "k2p5" / "k2-p5" / "k2.p5" - * - "k2p6" / "k2-p6" / "k2.p6" - * - * Match is case-insensitive on the model name (last path segment). - */ -export function isKimiK2Model(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - if (modelName.includes("kimi")) return true; - if (/k2[-.]?p[56]/.test(modelName)) return true; - return false; -} - -const GEMINI_PROVIDERS = ["google/", "google-vertex/"]; - -export function isMiniMaxModel(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("minimax"); -} - -export function isGlmModel(model: string): boolean { - const modelName = extractModelName(model).toLowerCase(); - return modelName.includes("glm"); -} - -export function isGeminiModel(model: string): boolean { - if (GEMINI_PROVIDERS.some((prefix) => model.startsWith(prefix))) return true; - - if ( - model.startsWith("github-copilot/") && - extractModelName(model).toLowerCase().startsWith("gemini") - ) - return true; - - const modelName = extractModelName(model).toLowerCase(); - return modelName.startsWith("gemini-"); -} - export type BuiltinAgentName = | "sisyphus" | "hephaestus"