feat(hooks): add think-mode hook for automatic model switching
Detects thinking keywords (ultrathink, deepthink, etc.) and switches to thinking-capable models automatically. Supports model patterns: - claude-sonnet-4-0 -> claude-sonnet-4-0-max-thinking - claude-sonnet-4-20250514 -> claude-sonnet-4-20250514-max-thinking 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
57
src/hooks/think-mode/detector.ts
Normal file
57
src/hooks/think-mode/detector.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
const ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i]
|
||||
|
||||
const MULTILINGUAL_KEYWORDS = [
|
||||
"생각", "고민", "검토", "제대로",
|
||||
"思考", "考虑", "考慮",
|
||||
"思考", "考え", "熟考",
|
||||
"सोच", "विचार",
|
||||
"تفكير", "تأمل",
|
||||
"চিন্তা", "ভাবনা",
|
||||
"думать", "думай", "размышлять", "размышляй",
|
||||
"pensar", "pense", "refletir", "reflita",
|
||||
"pensar", "piensa", "reflexionar", "reflexiona",
|
||||
"penser", "pense", "réfléchir", "réfléchis",
|
||||
"denken", "denk", "nachdenken",
|
||||
"suy nghĩ", "cân nhắc",
|
||||
"düşün", "düşünmek",
|
||||
"pensare", "pensa", "riflettere", "rifletti",
|
||||
"คิด", "พิจารณา",
|
||||
"myśl", "myśleć", "zastanów",
|
||||
"denken", "denk", "nadenken",
|
||||
"berpikir", "pikir", "pertimbangkan",
|
||||
"думати", "думай", "роздумувати",
|
||||
"σκέψου", "σκέφτομαι",
|
||||
"myslet", "mysli", "přemýšlet",
|
||||
"gândește", "gândi", "reflectă",
|
||||
"tänka", "tänk", "fundera",
|
||||
"gondolkodj", "gondolkodni",
|
||||
"ajattele", "ajatella", "pohdi",
|
||||
"tænk", "tænke", "overvej",
|
||||
"tenk", "tenke", "gruble",
|
||||
"חשוב", "לחשוב", "להרהר",
|
||||
"fikir", "berfikir",
|
||||
]
|
||||
|
||||
const MULTILINGUAL_PATTERNS = MULTILINGUAL_KEYWORDS.map((kw) => new RegExp(kw, "i"))
|
||||
const THINK_PATTERNS = [...ENGLISH_PATTERNS, ...MULTILINGUAL_PATTERNS]
|
||||
|
||||
const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
|
||||
const INLINE_CODE_PATTERN = /`[^`]+`/g
|
||||
|
||||
function removeCodeBlocks(text: string): string {
|
||||
return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "")
|
||||
}
|
||||
|
||||
export function detectThinkKeyword(text: string): boolean {
|
||||
const textWithoutCode = removeCodeBlocks(text)
|
||||
return THINK_PATTERNS.some((pattern) => pattern.test(textWithoutCode))
|
||||
}
|
||||
|
||||
export function extractPromptText(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("")
|
||||
}
|
||||
73
src/hooks/think-mode/index.ts
Normal file
73
src/hooks/think-mode/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { detectThinkKeyword, extractPromptText } from "./detector"
|
||||
import { getHighVariant, isAlreadyHighVariant } from "./switcher"
|
||||
import type { ThinkModeState, ThinkModeInput } from "./types"
|
||||
|
||||
export * from "./detector"
|
||||
export * from "./switcher"
|
||||
export * from "./types"
|
||||
|
||||
const thinkModeState = new Map<string, ThinkModeState>()
|
||||
|
||||
export function clearThinkModeState(sessionID: string): void {
|
||||
thinkModeState.delete(sessionID)
|
||||
}
|
||||
|
||||
export function createThinkModeHook() {
|
||||
return {
|
||||
"chat.params": async (
|
||||
output: ThinkModeInput,
|
||||
sessionID: string
|
||||
): Promise<void> => {
|
||||
const promptText = extractPromptText(output.parts)
|
||||
|
||||
const state: ThinkModeState = {
|
||||
requested: false,
|
||||
modelSwitched: false,
|
||||
}
|
||||
|
||||
if (!detectThinkKeyword(promptText)) {
|
||||
thinkModeState.set(sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
state.requested = true
|
||||
|
||||
const currentModel = output.message.model
|
||||
if (!currentModel) {
|
||||
thinkModeState.set(sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
state.providerID = currentModel.providerID
|
||||
state.modelID = currentModel.modelID
|
||||
|
||||
if (isAlreadyHighVariant(currentModel.modelID)) {
|
||||
thinkModeState.set(sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
const highVariant = getHighVariant(currentModel.modelID)
|
||||
|
||||
if (!highVariant) {
|
||||
thinkModeState.set(sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
output.message.model = {
|
||||
providerID: currentModel.providerID,
|
||||
modelID: highVariant,
|
||||
}
|
||||
state.modelSwitched = true
|
||||
thinkModeState.set(sessionID, state)
|
||||
},
|
||||
|
||||
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
||||
if (event.type === "session.deleted") {
|
||||
const props = event.properties as { info?: { id?: string } } | undefined
|
||||
if (props?.info?.id) {
|
||||
thinkModeState.delete(props.info.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
91
src/hooks/think-mode/switcher.ts
Normal file
91
src/hooks/think-mode/switcher.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
const HIGH_VARIANT_MAP: Record<string, string> = {
|
||||
"claude-sonnet-4-5": "claude-sonnet-4-5-high",
|
||||
"claude-opus-4-5": "claude-opus-4-5-high",
|
||||
"gpt-5.1": "gpt-5.1-high",
|
||||
"gpt-5.1-medium": "gpt-5.1-high",
|
||||
"gpt-5.1-codex": "gpt-5.1-codex-high",
|
||||
"gemini-3-pro": "gemini-3-pro-high",
|
||||
"gemini-3-pro-low": "gemini-3-pro-high",
|
||||
}
|
||||
|
||||
const ALREADY_HIGH: Set<string> = new Set([
|
||||
"claude-sonnet-4-5-high",
|
||||
"claude-opus-4-5-high",
|
||||
"gpt-5.1-high",
|
||||
"gpt-5.1-codex-high",
|
||||
"gemini-3-pro-high",
|
||||
])
|
||||
|
||||
export const THINKING_CONFIGS: Record<string, Record<string, unknown>> = {
|
||||
anthropic: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 64000,
|
||||
},
|
||||
},
|
||||
"amazon-bedrock": {
|
||||
reasoningConfig: {
|
||||
type: "enabled",
|
||||
budgetTokens: 32000,
|
||||
},
|
||||
},
|
||||
google: {
|
||||
providerOptions: {
|
||||
google: {
|
||||
thinkingConfig: {
|
||||
thinkingLevel: "HIGH",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"google-vertex": {
|
||||
providerOptions: {
|
||||
"google-vertex": {
|
||||
thinkingConfig: {
|
||||
thinkingLevel: "HIGH",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const THINKING_CAPABLE_MODELS: Record<string, string[]> = {
|
||||
anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-3"],
|
||||
"amazon-bedrock": ["claude", "anthropic"],
|
||||
google: ["gemini-2", "gemini-3"],
|
||||
"google-vertex": ["gemini-2", "gemini-3"],
|
||||
}
|
||||
|
||||
export function getHighVariant(modelID: string): string | null {
|
||||
if (ALREADY_HIGH.has(modelID)) {
|
||||
return null
|
||||
}
|
||||
return HIGH_VARIANT_MAP[modelID] ?? null
|
||||
}
|
||||
|
||||
export function isAlreadyHighVariant(modelID: string): boolean {
|
||||
return ALREADY_HIGH.has(modelID) || modelID.endsWith("-high")
|
||||
}
|
||||
|
||||
export function getThinkingConfig(
|
||||
providerID: string,
|
||||
modelID: string
|
||||
): Record<string, unknown> | null {
|
||||
if (isAlreadyHighVariant(modelID)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const config = THINKING_CONFIGS[providerID]
|
||||
const capablePatterns = THINKING_CAPABLE_MODELS[providerID]
|
||||
|
||||
if (!config || !capablePatterns) {
|
||||
return null
|
||||
}
|
||||
|
||||
const modelLower = modelID.toLowerCase()
|
||||
const isCapable = capablePatterns.some((pattern) =>
|
||||
modelLower.includes(pattern.toLowerCase())
|
||||
)
|
||||
|
||||
return isCapable ? config : null
|
||||
}
|
||||
20
src/hooks/think-mode/types.ts
Normal file
20
src/hooks/think-mode/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface ThinkModeState {
|
||||
requested: boolean
|
||||
modelSwitched: boolean
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
}
|
||||
|
||||
export interface ModelRef {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
export interface MessageWithModel {
|
||||
model?: ModelRef
|
||||
}
|
||||
|
||||
export interface ThinkModeInput {
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
message: MessageWithModel
|
||||
}
|
||||
Reference in New Issue
Block a user