Merge pull request #4385 from code-yeongyu/refactor/prompts-core-foundation

refactor(prompts-core): add packages/prompts-core foundation with model-family detector relocation
This commit is contained in:
YeonGyu-Kim
2026-05-24 17:55:32 +09:00
committed by GitHub
18 changed files with 600 additions and 53 deletions
+10
View File
@@ -108,6 +108,14 @@
"@oh-my-opencode/utils": "workspace:*",
},
},
"packages/prompts-core": {
"name": "@oh-my-opencode/prompts-core",
"version": "0.1.0",
"peerDependencies": {
"@oh-my-opencode/model-core": "workspace:*",
"@oh-my-opencode/utils": "workspace:*",
},
},
"packages/rules-engine": {
"name": "@oh-my-opencode/rules-engine",
"version": "0.1.0",
@@ -209,6 +217,8 @@
"@oh-my-opencode/model-core": ["@oh-my-opencode/model-core@workspace:packages/model-core"],
"@oh-my-opencode/prompts-core": ["@oh-my-opencode/prompts-core@workspace:packages/prompts-core"],
"@oh-my-opencode/rules-engine": ["@oh-my-opencode/rules-engine@workspace:packages/rules-engine"],
"@oh-my-opencode/utils": ["@oh-my-opencode/utils@workspace:packages/utils"],
+2 -1
View File
@@ -11,6 +11,7 @@
"packages/ast-grep-mcp",
"packages/utils",
"packages/model-core",
"packages/prompts-core",
"packages/comment-checker-core",
"packages/hashline-core",
"packages/boulder-state",
@@ -49,7 +50,7 @@
"prepublishOnly": "bun run clean && bun run build:lsp-tools-mcp && bun run build",
"test:model-capabilities": "bun test src/shared/model-capability-aliases.test.ts src/shared/model-capability-guardrails.test.ts src/shared/model-capabilities.test.ts src/cli/doctor/checks/model-resolution.test.ts --bail",
"typecheck": "tsgo --noEmit && bun run typecheck:packages",
"typecheck:packages": "tsgo --noEmit -p packages/rules-engine/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/hashline-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json",
"typecheck:packages": "tsgo --noEmit -p packages/rules-engine/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/prompts-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/hashline-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json",
"typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
"test": "bun test",
"build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build"
+1
View File
@@ -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"
@@ -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)
})
})
@@ -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-")
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@oh-my-opencode/prompts-core",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "Harness-agnostic markdown prompt loading and model-variant routing for oh-my-opencode.",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./src/index.ts"
}
},
"types": "./index.d.ts",
"scripts": {
"typecheck": "tsgo --noEmit -p tsconfig.json",
"test": "bun test src/*.test.ts test/*.test.ts"
},
"peerDependencies": {
"@oh-my-opencode/model-core": "workspace:*",
"@oh-my-opencode/utils": "workspace:*"
}
}
@@ -0,0 +1,6 @@
---
title: Test Prompt
enabled: true
---
Default prompt body with {X}.
Second line remains verbatim.
@@ -0,0 +1,3 @@
---
---
GPT prompt body with {A} and {B}.
+11
View File
@@ -0,0 +1,11 @@
export type {
LoadedPrompt,
LoadPromptInput,
ModelVariant,
PromptSource,
RuntimeInjection,
VariantTable,
} from "./types"
export { resolveVariant } from "./variant-resolver"
export type { ResolveVariantInput } from "./variant-resolver"
export { loadPrompt, PromptFileNotFoundError, PromptPathTraversalError } from "./loader"
+128
View File
@@ -0,0 +1,128 @@
import { describe, expect, test } from "bun:test"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { loadPrompt, PromptFileNotFoundError, PromptPathTraversalError } from "./loader"
import type { PromptSource } from "./types"
const fixtureSource: PromptSource = {
baseDir: join(dirname(fileURLToPath(import.meta.url)), "__test_fixtures__"),
}
class ResolverFailureError extends Error {
readonly name = "ResolverFailureError"
}
class ExpectedErrorMissingError extends Error {
readonly name = "ExpectedErrorMissingError"
}
describe("loadPrompt", () => {
test("#given markdown fixture #then returns markdown body verbatim", async () => {
const prompt = await loadPrompt({ source: fixtureSource, name: "test-prompt", variant: "default" })
expect(prompt.body).toBe("Default prompt body with {X}.\nSecond line remains verbatim.\n")
})
test("#given frontmatter fixture #then returns parsed frontmatter", async () => {
const prompt = await loadPrompt<{ readonly title: string; readonly enabled: boolean }>({
source: fixtureSource,
name: "test-prompt",
variant: "default",
})
expect(prompt.frontmatter.title).toBe("Test Prompt")
expect(prompt.frontmatter.enabled).toBe(true)
})
test("#given empty frontmatter #then parses without crashing", async () => {
const prompt = await loadPrompt({ source: fixtureSource, name: "test-prompt", variant: "gpt" })
expect(prompt.frontmatter).toEqual({})
expect(prompt.body).toBe("GPT prompt body with {A} and {B}.\n")
})
test("#given missing file #then error mentions prompt name and variant", async () => {
const error = await captureError(() =>
loadPrompt({ source: fixtureSource, name: "test-prompt", variant: "missing" })
)
expect(error).toBeInstanceOf(PromptFileNotFoundError)
expect(expectError(error).message).toContain("test-prompt/missing")
})
test("#given prompt name escapes source directory #then rejects path traversal", async () => {
const error = await captureError(() =>
loadPrompt({ source: fixtureSource, name: "../test-prompt", variant: "default" })
)
expect(error).toBeInstanceOf(PromptPathTraversalError)
})
test("#given variant escapes source directory #then rejects path traversal", async () => {
const error = await captureError(() =>
loadPrompt({ source: fixtureSource, name: "test-prompt", variant: "../../outside" })
)
expect(error).toBeInstanceOf(PromptPathTraversalError)
})
test("#given runtime injection #then replaces placeholder in body", async () => {
const prompt = await loadPrompt({
source: fixtureSource,
name: "test-prompt",
variant: "default",
inject: [{ placeholder: "{X}", resolver: () => "Y" }],
})
expect(prompt.body).toBe("Default prompt body with Y.\nSecond line remains verbatim.\n")
})
test("#given multiple runtime injections #then applies all and ignores absent placeholders", async () => {
const prompt = await loadPrompt({
source: fixtureSource,
name: "test-prompt",
variant: "gpt",
inject: [
{ placeholder: "{A}", resolver: () => "Alpha" },
{ placeholder: "{B}", resolver: () => "Beta" },
{ placeholder: "{ABSENT}", resolver: () => "No-op" },
],
})
expect(prompt.body).toBe("GPT prompt body with Alpha and Beta.\n")
})
test("#given injection resolver throws #then propagates the error", async () => {
const error = await captureError(() =>
loadPrompt({
source: fixtureSource,
name: "test-prompt",
variant: "default",
inject: [
{
placeholder: "{X}",
resolver: () => {
throw new ResolverFailureError("resolver failed")
},
},
],
})
)
expect(error).toBeInstanceOf(ResolverFailureError)
})
})
async function captureError(operation: () => Promise<unknown>): Promise<unknown> {
try {
await operation()
return undefined
} catch (error) {
return error
}
}
function expectError(error: unknown): Error {
if (error instanceof Error) return error
throw new ExpectedErrorMissingError("Expected operation to throw an Error instance")
}
+82
View File
@@ -0,0 +1,82 @@
import { parseFrontmatter } from "@oh-my-opencode/utils"
import { readFile } from "node:fs/promises"
import { isAbsolute, relative, resolve } from "node:path"
import type { LoadedPrompt, LoadPromptInput, RuntimeInjection } from "./types"
export class PromptFileNotFoundError extends Error {
readonly name = "PromptFileNotFoundError"
constructor(
readonly promptName: string,
readonly variant: string,
readonly filePath: string,
options?: ErrorOptions
) {
super(`Prompt file not found for ${promptName}/${variant}: ${filePath}`, options)
}
}
export class PromptPathTraversalError extends Error {
readonly name = "PromptPathTraversalError"
constructor(
readonly promptName: string,
readonly variant: string
) {
super(`Prompt path escapes source directory for ${promptName}/${variant}`)
}
}
export async function loadPrompt<TFrontmatter = Record<string, unknown>>(
input: LoadPromptInput
): Promise<LoadedPrompt<TFrontmatter>> {
const filePath = resolvePromptFilePath(input.source.baseDir, input.name, input.variant)
const content = await readPromptFile(input.name, input.variant, filePath)
const parsed = parseFrontmatter<TFrontmatter>(content)
const body = await applyRuntimeInjections(parsed.body, input.inject ?? [])
return {
frontmatter: parsed.data,
body,
hadFrontmatter: parsed.hadFrontmatter,
parseError: parsed.parseError,
filePath,
}
}
function resolvePromptFilePath(baseDir: string, promptName: string, variant: string): string {
const resolvedBaseDir = resolve(baseDir)
const filePath = resolve(resolvedBaseDir, promptName, `${variant}.md`)
const relativePath = relative(resolvedBaseDir, filePath)
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
throw new PromptPathTraversalError(promptName, variant)
}
return filePath
}
async function readPromptFile(promptName: string, variant: string, filePath: string): Promise<string> {
try {
return await readFile(filePath, "utf8")
} catch (error) {
if (error instanceof Error && getErrorCode(error) === "ENOENT") {
throw new PromptFileNotFoundError(promptName, variant, filePath, { cause: error })
}
throw error
}
}
async function applyRuntimeInjections(
body: string,
injections: readonly RuntimeInjection[]
): Promise<string> {
let renderedBody = body
for (const injection of injections) {
renderedBody = renderedBody.replaceAll(injection.placeholder, await injection.resolver())
}
return renderedBody
}
function getErrorCode(error: Error): string | undefined {
if (!("code" in error)) return undefined
return typeof error.code === "string" ? error.code : undefined
}
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, test } from "bun:test"
import type { LoadPromptInput, LoadedPrompt, PromptSource, RuntimeInjection } from "./types"
describe("prompt core types", () => {
test("#given loader input shape #then accepts source name variant and runtime injections", () => {
const source: PromptSource = { baseDir: "/tmp/prompts" }
const injection: RuntimeInjection = { placeholder: "{X}", resolver: () => "Y" }
const input = {
source,
name: "test",
variant: "default",
inject: [injection],
} satisfies LoadPromptInput
expect(input.source.baseDir).toBe("/tmp/prompts")
expect(input.inject[0]?.placeholder).toBe("{X}")
})
test("#given loaded prompt shape #then carries frontmatter and rendered body", () => {
const loaded = {
frontmatter: { title: "Fixture" },
body: "Prompt body",
hadFrontmatter: true,
parseError: false,
filePath: "/tmp/prompts/test/default.md",
} satisfies LoadedPrompt<{ readonly title: string }>
expect(loaded.frontmatter.title).toBe("Fixture")
expect(loaded.body).toBe("Prompt body")
})
})
+35
View File
@@ -0,0 +1,35 @@
export type ModelVariant =
| "default"
| "gpt"
| "gemini"
| "kimi"
| "glm"
| "planner"
| "opus-4-7"
| "minimax"
export type PromptSource = {
readonly baseDir: string
}
export type RuntimeInjection = {
readonly placeholder: string
readonly resolver: () => string | Promise<string>
}
export type LoadPromptInput = {
readonly source: PromptSource
readonly name: string
readonly variant: string
readonly inject?: readonly RuntimeInjection[]
}
export type LoadedPrompt<TFrontmatter = Record<string, unknown>> = {
readonly frontmatter: TFrontmatter
readonly body: string
readonly hadFrontmatter: boolean
readonly parseError: boolean
readonly filePath: string
}
export type VariantTable = Readonly<Record<string, PromptSource>>
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test"
import type { PromptSource, VariantTable } from "./types"
import { resolveVariant } from "./variant-resolver"
const promptSource = (baseDir: string): PromptSource => ({ baseDir })
const variants = {
planner: promptSource("/prompts/planner"),
gpt: promptSource("/prompts/gpt"),
gemini: promptSource("/prompts/gemini"),
kimi: promptSource("/prompts/kimi"),
glm: promptSource("/prompts/glm"),
default: promptSource("/prompts/default"),
} satisfies VariantTable
describe("resolveVariant", () => {
test("#given Claude Opus 4.7 model #then resolves default variant", () => {
expect(resolveVariant({ modelID: "claude-opus-4-7", variants })).toBe("default")
})
test("#given GPT model #then resolves gpt variant", () => {
expect(resolveVariant({ modelID: "gpt-5-5", variants })).toBe("gpt")
})
test("#given Gemini model #then resolves gemini variant", () => {
expect(resolveVariant({ modelID: "gemini-3-1-pro", variants })).toBe("gemini")
})
test("#given Kimi K2 model #then resolves kimi variant", () => {
expect(resolveVariant({ modelID: "kimi-k2-6", variants })).toBe("kimi")
})
test("#given GLM model #then resolves glm variant", () => {
expect(resolveVariant({ modelID: "glm-5-1", variants })).toBe("glm")
})
test("#given Prometheus agent #then planner overrides model variant", () => {
expect(resolveVariant({ agentName: "prometheus", modelID: "gpt-5-5", variants })).toBe(
"planner"
)
})
test("#given unknown model #then falls back to default variant", () => {
expect(resolveVariant({ modelID: "claude-haiku-4-5", variants })).toBe("default")
})
test("#given empty variants table #then throws TypeError", () => {
expect(() => resolveVariant({ modelID: "gpt-5-5", variants: {} })).toThrow(TypeError)
})
})
@@ -0,0 +1,58 @@
import {
isClaudeOpus47Model,
isGeminiModel,
isGlmModel,
isGptModel,
isKimiK2Model,
isMiniMaxModel,
} from "@oh-my-opencode/model-core"
import type { VariantTable } from "./types"
type ModelMatcher = (modelID: string) => boolean
export type ResolveVariantInput = {
readonly modelID?: string
readonly agentName?: string
readonly variants: VariantTable
}
const PLANNER_AGENT_NAMES: ReadonlySet<string> = new Set(["prometheus"] as const)
const MODEL_MATCHERS: Readonly<Record<string, ModelMatcher>> = {
gpt: isGptModel,
gemini: isGeminiModel,
kimi: isKimiK2Model,
glm: isGlmModel,
"opus-4-7": isClaudeOpus47Model,
minimax: isMiniMaxModel,
}
export function resolveVariant(input: ResolveVariantInput): string {
const variantNames = Object.keys(input.variants)
if (variantNames.length === 0) {
throw new TypeError("resolveVariant requires at least one prompt variant")
}
if (isPlannerAgent(input.agentName) && variantNames.includes("planner")) {
return "planner"
}
if (input.modelID !== undefined) {
for (const variantName of variantNames) {
if (matchesModelVariant(variantName, input.modelID)) return variantName
}
}
if (variantNames.includes("default")) return "default"
return variantNames[0]
}
function isPlannerAgent(agentName: string | undefined): boolean {
return agentName !== undefined && PLANNER_AGENT_NAMES.has(agentName.toLowerCase())
}
function matchesModelVariant(variantName: string, modelID: string): boolean {
const matcher = MODEL_MATCHERS[variantName]
return matcher?.(modelID) ?? false
}
@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test"
import { readdir, readFile } from "node:fs/promises"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
const SOURCE_DIR = join(dirname(fileURLToPath(import.meta.url)), "../src")
describe("opencode coupling audit", () => {
test("#given prompts-core source #then no file imports @opencode-ai packages", async () => {
const offenders = await findOpenCodeImports(SOURCE_DIR)
expect(offenders).toEqual([])
})
})
async function findOpenCodeImports(sourceDir: string): Promise<readonly string[]> {
const files = await collectTypeScriptFiles(sourceDir)
const offenders: string[] = []
for (const filePath of files) {
const source = await readFile(filePath, "utf8")
if (source.includes("@opencode-ai")) offenders.push(filePath)
}
return offenders
}
async function collectTypeScriptFiles(directory: string): Promise<readonly string[]> {
const entries = await readdir(directory, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
const entryPath = join(directory, entry.name)
if (entry.isDirectory()) {
files.push(...(await collectTypeScriptFiles(entryPath)))
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
files.push(entryPath)
}
}
return files
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext"],
"types": ["bun-types"]
},
"include": ["src/**/*", "test/**/*"]
}
+9 -52
View File
@@ -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"