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