From e863fe20132cbec928b7728af33394acb4dbdddf Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 18 Feb 2026 17:33:44 +0900 Subject: [PATCH] feat(hooks): add ultrawork-model-override hook for per-agent model swap --- assets/oh-my-opencode.schema.json | 211 ++++++++++++++++++++ docs/configurations.md | 2 + src/config/schema/agent-overrides.ts | 5 + src/config/schema/hooks.ts | 1 + src/hooks/index.ts | 2 + src/hooks/ultrawork-model-override/hook.ts | 83 ++++++++ src/hooks/ultrawork-model-override/index.ts | 1 + src/plugin-interface.ts | 6 +- src/plugin/hooks/create-session-hooks.ts | 7 + 9 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/hooks/ultrawork-model-override/hook.ts create mode 100644 src/hooks/ultrawork-model-override/index.ts diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 4e61ed1a..68281e70 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -80,6 +80,7 @@ "non-interactive-env", "interactive-bash-session", "thinking-block-validator", + "ultrawork-model-override", "ralph-loop", "category-skill-reminder", "compaction-context-injector", @@ -284,6 +285,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -466,6 +482,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -648,6 +679,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -830,6 +876,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1012,6 +1073,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1194,6 +1270,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1376,6 +1467,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1558,6 +1664,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1740,6 +1861,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -1922,6 +2058,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -2104,6 +2255,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -2286,6 +2452,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -2468,6 +2649,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ @@ -2650,6 +2846,21 @@ ], "additionalProperties": false }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": [ + "model" + ], + "additionalProperties": false + }, "reasoningEffort": { "type": "string", "enum": [ diff --git a/docs/configurations.md b/docs/configurations.md index e4f8c429..622b60a8 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -977,6 +977,8 @@ Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `sessio **Note on `directory-agents-injector`**: This hook is **automatically disabled** when running on OpenCode 1.1.37+ because OpenCode now has native support for dynamically resolving AGENTS.md files from subdirectories (PR #10678). This prevents duplicate AGENTS.md injection. For older OpenCode versions, the hook remains active to provide the same functionality. +**Note on `no-sisyphus-gpt`**: Disabling this hook is **STRONGLY discouraged**. Sisyphus is NOT optimized for GPT models — running Sisyphus with GPT performs worse than vanilla Codex and wastes your money. This hook automatically switches to Hephaestus when a GPT model is detected, which is the correct agent for GPT. Only disable this if you fully understand the consequences. + **Note on `auto-update-checker` and `startup-toast`**: The `startup-toast` hook is a sub-feature of `auto-update-checker`. To disable only the startup toast notification while keeping update checking enabled, add `"startup-toast"` to `disabled_hooks`. To disable all update checking features (including the toast), add `"auto-update-checker"` to `disabled_hooks`. ## Disabled Commands diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index 876560ec..0abcb5c9 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -32,6 +32,11 @@ export const AgentOverrideConfigSchema = z.object({ budgetTokens: z.number().optional(), }) .optional(), + /** Ultrawork model override configuration. */ + ultrawork: z.object({ + model: z.string(), + variant: z.string().optional(), + }).optional(), /** Reasoning effort level (OpenAI). Overrides category and default settings. */ reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(), /** Text verbosity level. */ diff --git a/src/config/schema/hooks.ts b/src/config/schema/hooks.ts index cf3d5009..868b8ffc 100644 --- a/src/config/schema/hooks.ts +++ b/src/config/schema/hooks.ts @@ -25,6 +25,7 @@ export const HookNameSchema = z.enum([ "interactive-bash-session", "thinking-block-validator", + "ultrawork-model-override", "ralph-loop", "category-skill-reminder", diff --git a/src/hooks/index.ts b/src/hooks/index.ts index db13d873..f34e9270 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -46,3 +46,5 @@ export { createPreemptiveCompactionHook } from "./preemptive-compaction"; export { createTasksTodowriteDisablerHook } from "./tasks-todowrite-disabler"; export { createWriteExistingFileGuardHook } from "./write-existing-file-guard"; export { createHashlineReadEnhancerHook } from "./hashline-read-enhancer"; + +export { createUltraworkModelOverrideHook } from "./ultrawork-model-override"; diff --git a/src/hooks/ultrawork-model-override/hook.ts b/src/hooks/ultrawork-model-override/hook.ts new file mode 100644 index 00000000..cc14f9eb --- /dev/null +++ b/src/hooks/ultrawork-model-override/hook.ts @@ -0,0 +1,83 @@ +import type { AgentOverrides } from "../../config" +import { log } from "../../shared" +import { getAgentConfigKey } from "../../shared/agent-display-names" + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function getUltraworkConfig(agents: AgentOverrides | undefined, configKey: string) { + if (!agents) return undefined + + for (const [agentKey, override] of Object.entries(agents)) { + if (getAgentConfigKey(agentKey) === configKey) { + return override?.ultrawork + } + } + + return undefined +} + +export function createUltraworkModelOverrideHook(args: { agents?: AgentOverrides }) { + let didLogSpikeInput = false + + return { + "chat.params": async (input: unknown, output: unknown): Promise => { + if (!didLogSpikeInput) { + didLogSpikeInput = true + + const inputRecord = isRecord(input) ? input : null + const messageRecord = isRecord(inputRecord?.message) ? inputRecord.message : null + + log("ultrawork-model-override spike: raw chat.params input", { + inputType: typeof input, + outputType: typeof output, + hasMessage: messageRecord !== null, + messageKeys: messageRecord ? Object.keys(messageRecord) : [], + hasMessageModel: messageRecord ? "model" in messageRecord : false, + messageModelType: messageRecord ? typeof messageRecord.model : "undefined", + }) + } + + if (!isRecord(input)) return + + const message = input.message + if (!isRecord(message)) return + if (message.variant !== "max") return + + const agentName = input.agent + if (typeof agentName !== "string") return + + const configKey = getAgentConfigKey(agentName) + const ultrawork = getUltraworkConfig(args.agents, configKey) + if (!ultrawork?.model) return + + const separatorIndex = ultrawork.model.indexOf("/") + const providerID = separatorIndex === -1 ? ultrawork.model : ultrawork.model.slice(0, separatorIndex) + const modelID = separatorIndex === -1 ? "" : ultrawork.model.slice(separatorIndex + 1) + + const previousModel = isRecord(message.model) + ? { + providerID: + typeof message.model.providerID === "string" ? message.model.providerID : undefined, + modelID: typeof message.model.modelID === "string" ? message.model.modelID : undefined, + } + : undefined + + message.model = { providerID, modelID } + + if (ultrawork.variant !== undefined) { + message.variant = ultrawork.variant + } + + log("ultrawork-model-override: swapped model", { + sessionID: typeof input.sessionID === "string" ? input.sessionID : undefined, + agent: agentName, + configKey, + from: previousModel, + to: message.model, + variant: message.variant, + }) + }, + } +} diff --git a/src/hooks/ultrawork-model-override/index.ts b/src/hooks/ultrawork-model-override/index.ts new file mode 100644 index 00000000..32794e94 --- /dev/null +++ b/src/hooks/ultrawork-model-override/index.ts @@ -0,0 +1 @@ +export { createUltraworkModelOverrideHook } from "./hook" diff --git a/src/plugin-interface.ts b/src/plugin-interface.ts index f9b883a3..e0ea2e8d 100644 --- a/src/plugin-interface.ts +++ b/src/plugin-interface.ts @@ -30,7 +30,11 @@ export function createPluginInterface(args: { return { tool: tools, - "chat.params": createChatParamsHandler({ anthropicEffort: hooks.anthropicEffort }), + "chat.params": async (input, output) => { + await hooks.ultraworkModelOverride?.["chat.params"]?.(input, output) + const handler = createChatParamsHandler({ anthropicEffort: hooks.anthropicEffort }) + await handler(input, output) + }, "chat.message": createChatMessageHandler({ ctx, diff --git a/src/plugin/hooks/create-session-hooks.ts b/src/plugin/hooks/create-session-hooks.ts index f7047a90..4d3a66ed 100644 --- a/src/plugin/hooks/create-session-hooks.ts +++ b/src/plugin/hooks/create-session-hooks.ts @@ -25,6 +25,7 @@ import { createPreemptiveCompactionHook, } from "../../hooks" import { createAnthropicEffortHook } from "../../hooks/anthropic-effort" +import { createUltraworkModelOverrideHook } from "../../hooks/ultrawork-model-override" import { detectExternalNotificationPlugin, getNotificationConflictWarning, @@ -55,6 +56,7 @@ export type SessionHooks = { questionLabelTruncator: ReturnType taskResumeInfo: ReturnType anthropicEffort: ReturnType | null + ultraworkModelOverride: ReturnType | null } export function createSessionHooks(args: { @@ -169,6 +171,10 @@ export function createSessionHooks(args: { ? safeHook("anthropic-effort", () => createAnthropicEffortHook()) : null + const ultraworkModelOverride = isHookEnabled("ultrawork-model-override") + ? safeHook("ultrawork-model-override", () => createUltraworkModelOverrideHook({ agents: pluginConfig.agents })) + : null + return { contextWindowMonitor, preemptiveCompaction, @@ -191,5 +197,6 @@ export function createSessionHooks(args: { questionLabelTruncator, taskResumeInfo, anthropicEffort, + ultraworkModelOverride, } }