From 64b2d690362e0b4a6a2c1fec0514db04c1c98a7b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 19 Feb 2026 15:20:53 +0900 Subject: [PATCH] feat(ultrawork): implement per-message model override with deferred DB retry strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add per-message ultrawork mode detection via keyword matching - Implement deferred DB override strategy using microtask retry loop - Fall back to setTimeout after 10 microtask retries for robustness - Update agent configuration schema with ultrawork model/variant fields - Integrate with chat.message hook to apply overrides on detection - Add comprehensive tests for all override scenarios - Generated schema includes ultrawork configuration 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode) --- assets/oh-my-opencode.schema.json | 317 ++++++++++++++- src/config/schema/agent-overrides.ts | 7 + src/plugin/chat-message.ts | 3 + .../ultrawork-db-model-override.test.ts | 180 +++++++++ src/plugin/ultrawork-db-model-override.ts | 95 +++++ src/plugin/ultrawork-model-override.test.ts | 365 ++++++++++++++++++ src/plugin/ultrawork-model-override.ts | 127 ++++++ src/shared/model-requirements.test.ts | 2 +- src/shared/model-requirements.ts | 38 +- 9 files changed, 1112 insertions(+), 22 deletions(-) create mode 100644 src/plugin/ultrawork-db-model-override.test.ts create mode 100644 src/plugin/ultrawork-db-model-override.ts create mode 100644 src/plugin/ultrawork-model-override.test.ts create mode 100644 src/plugin/ultrawork-model-override.ts diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index 20f98e4f..bb4f4e7c 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -101,7 +101,8 @@ "tasks-todowrite-disabler", "write-existing-file-guard", "anthropic-effort", - "hashline-read-enhancer" + "hashline-read-enhancer", + "hashline-edit-diff-enhancer" ] } }, @@ -165,6 +166,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -210,6 +214,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -297,7 +304,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -338,6 +360,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -383,6 +408,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -470,7 +498,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -511,6 +554,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -556,6 +602,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -643,7 +692,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -684,6 +748,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -729,6 +796,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -816,7 +886,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -857,6 +942,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -902,6 +990,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -989,7 +1080,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1030,6 +1136,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1075,6 +1184,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -1162,7 +1274,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1203,6 +1330,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1248,6 +1378,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -1335,7 +1468,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1376,6 +1524,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1421,6 +1572,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -1508,7 +1662,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1549,6 +1718,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1594,6 +1766,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -1681,7 +1856,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1722,6 +1912,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1767,6 +1960,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -1854,7 +2050,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1895,6 +2106,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -1940,6 +2154,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -2027,7 +2244,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2068,6 +2300,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -2113,6 +2348,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -2200,7 +2438,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2241,6 +2494,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -2286,6 +2542,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -2373,7 +2632,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2414,6 +2688,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -2459,6 +2736,9 @@ }, { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "string", "enum": [ @@ -2546,7 +2826,22 @@ }, "providerOptions": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} + }, + "ultrawork": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -2556,6 +2851,9 @@ }, "categories": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "object", "properties": { @@ -2619,6 +2917,9 @@ }, "tools": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -2659,6 +2960,9 @@ }, "plugins_override": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "boolean" } @@ -2932,6 +3236,9 @@ }, "metadata": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": {} }, "allowed-tools": { @@ -2983,6 +3290,9 @@ }, "providerConcurrency": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "number", "minimum": 0 @@ -2990,6 +3300,9 @@ }, "modelConcurrency": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "number", "minimum": 0 @@ -3162,4 +3475,4 @@ } }, "additionalProperties": false -} +} \ No newline at end of file diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index 876560ec..59bb360e 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -38,6 +38,13 @@ export const AgentOverrideConfigSchema = z.object({ textVerbosity: z.enum(["low", "medium", "high"]).optional(), /** Provider-specific options. Passed directly to OpenCode SDK. */ providerOptions: z.record(z.string(), z.unknown()).optional(), + /** Per-message ultrawork override model/variant when ultrawork keyword is detected. */ + ultrawork: z + .object({ + model: z.string().optional(), + variant: z.string().optional(), + }) + .optional(), }) export const AgentOverridesSchema = z.object({ diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index b5bd70c8..612b7122 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -10,6 +10,7 @@ import { hasConnectedProvidersCache } from "../shared" import { setSessionAgent, } from "../features/claude-code-session-state" +import { applyUltraworkModelOverrideOnMessage } from "./ultrawork-model-override" import type { CreatedHooks } from "../create-hooks" @@ -138,5 +139,7 @@ export function createChatMessageHandler(args: { hooks.ralphLoop.cancelLoop(input.sessionID) } } + + applyUltraworkModelOverrideOnMessage(pluginConfig, input.agent, output, ctx.client.tui) } } diff --git a/src/plugin/ultrawork-db-model-override.test.ts b/src/plugin/ultrawork-db-model-override.test.ts new file mode 100644 index 00000000..b647bb6d --- /dev/null +++ b/src/plugin/ultrawork-db-model-override.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test" +import { Database } from "bun:sqlite" +import { mkdtempSync, mkdirSync, rmSync } from "node:fs" +import { join } from "node:path" +import { tmpdir } from "node:os" +import * as dataPathModule from "../shared/data-path" +import * as sharedModule from "../shared" + +function flushMicrotasks(depth: number): Promise { + return new Promise((resolve) => { + let remaining = depth + function step() { + if (remaining <= 0) { resolve(); return } + remaining-- + queueMicrotask(step) + } + queueMicrotask(step) + }) +} + +function flushWithTimeout(): Promise { + return new Promise((resolve) => setTimeout(resolve, 10)) +} + +describe("scheduleDeferredModelOverride", () => { + let tempDir: string + let dbPath: string + let logSpy: ReturnType + let getDataDirSpy: ReturnType + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "ultrawork-db-test-")) + const opencodePath = join(tempDir, "opencode") + mkdirSync(opencodePath, { recursive: true }) + dbPath = join(opencodePath, "opencode.db") + + const db = new Database(dbPath) + db.run(` + CREATE TABLE IF NOT EXISTS message ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + time_created TEXT NOT NULL DEFAULT (datetime('now')), + time_updated TEXT NOT NULL DEFAULT (datetime('now')), + data TEXT NOT NULL DEFAULT '{}' + ) + `) + db.close() + + getDataDirSpy = spyOn(dataPathModule, "getDataDir").mockReturnValue(tempDir) + logSpy = spyOn(sharedModule, "log").mockImplementation(() => {}) + }) + + afterEach(() => { + getDataDirSpy?.mockRestore() + logSpy?.mockRestore() + rmSync(tempDir, { recursive: true, force: true }) + }) + + function insertMessage(id: string, model: { providerID: string; modelID: string }) { + const db = new Database(dbPath) + db.run( + `INSERT INTO message (id, session_id, data) VALUES (?, ?, ?)`, + id, + "ses_test", + JSON.stringify({ model }), + ) + db.close() + } + + function readMessageModel(id: string): { providerID: string; modelID: string } | null { + const db = new Database(dbPath) + const row = db.query(`SELECT data FROM message WHERE id = ?`).get(id) as + | { data: string } + | null + db.close() + if (!row) return null + const parsed = JSON.parse(row.data) + return parsed.model ?? null + } + + function readMessageField(id: string, field: string): unknown { + const db = new Database(dbPath) + const row = db.query(`SELECT data FROM message WHERE id = ?`).get(id) as + | { data: string } + | null + db.close() + if (!row) return null + return JSON.parse(row.data)[field] ?? null + } + + test("should update model in DB after microtask flushes", async () => { + //#given + insertMessage("msg_001", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) + + //#when + const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") + scheduleDeferredModelOverride( + "msg_001", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + ) + await flushMicrotasks(5) + + //#then + const model = readMessageModel("msg_001") + expect(model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) + }) + + test("should update variant and thinking fields when variant provided", async () => { + //#given + insertMessage("msg_002", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) + + //#when + const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") + scheduleDeferredModelOverride( + "msg_002", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + "max", + ) + await flushMicrotasks(5) + + //#then + expect(readMessageField("msg_002", "variant")).toBe("max") + expect(readMessageField("msg_002", "thinking")).toBe("max") + }) + + test("should fall back to setTimeout when message never appears", async () => { + //#given — no message inserted + + //#when + const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") + scheduleDeferredModelOverride( + "msg_nonexistent", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + ) + await flushWithTimeout() + + //#then + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("setTimeout fallback failed"), + expect.objectContaining({ messageId: "msg_nonexistent" }), + ) + }) + + test("should not update variant fields when variant is undefined", async () => { + //#given + insertMessage("msg_003", { providerID: "anthropic", modelID: "claude-sonnet-4-6" }) + + //#when + const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") + scheduleDeferredModelOverride( + "msg_003", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + ) + await flushMicrotasks(5) + + //#then + const model = readMessageModel("msg_003") + expect(model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) + expect(readMessageField("msg_003", "variant")).toBeNull() + expect(readMessageField("msg_003", "thinking")).toBeNull() + }) + + test("should not crash when DB path does not exist", async () => { + //#given + getDataDirSpy.mockReturnValue("/nonexistent/path/that/does/not/exist") + + //#when + const { scheduleDeferredModelOverride } = await import("./ultrawork-db-model-override") + scheduleDeferredModelOverride( + "msg_004", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + ) + await flushMicrotasks(5) + + //#then + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("DB not found"), + ) + }) +}) diff --git a/src/plugin/ultrawork-db-model-override.ts b/src/plugin/ultrawork-db-model-override.ts new file mode 100644 index 00000000..d8fade94 --- /dev/null +++ b/src/plugin/ultrawork-db-model-override.ts @@ -0,0 +1,95 @@ +import { Database } from "bun:sqlite" +import { join } from "node:path" +import { existsSync } from "node:fs" +import { getDataDir } from "../shared/data-path" +import { log } from "../shared" + +function getDbPath(): string { + return join(getDataDir(), "opencode", "opencode.db") +} + +const MAX_MICROTASK_RETRIES = 10 + +function tryUpdateMessageModel( + db: InstanceType, + messageId: string, + targetModel: { providerID: string; modelID: string }, + variant?: string, +): boolean { + const stmt = db.prepare( + `UPDATE message SET data = json_set(data, '$.model.providerID', ?, '$.model.modelID', ?) WHERE id = ?`, + ) + const result = stmt.run(targetModel.providerID, targetModel.modelID, messageId) + if (result.changes === 0) return false + + if (variant) { + db.prepare( + `UPDATE message SET data = json_set(data, '$.variant', ?, '$.thinking', ?) WHERE id = ?`, + ).run(variant, variant, messageId) + } + return true +} + +function retryViaMicrotask( + db: InstanceType, + messageId: string, + targetModel: { providerID: string; modelID: string }, + variant: string | undefined, + attempt: number, +): void { + if (attempt >= MAX_MICROTASK_RETRIES) { + log("[ultrawork-db-override] Exhausted microtask retries, falling back to setTimeout", { + messageId, + attempt, + }) + setTimeout(() => { + if (tryUpdateMessageModel(db, messageId, targetModel, variant)) { + log(`[ultrawork-db-override] setTimeout fallback succeeded: ${targetModel.providerID}/${targetModel.modelID}`, { messageId }) + } else { + log("[ultrawork-db-override] setTimeout fallback failed - message not found", { messageId }) + } + db.close() + }, 0) + return + } + + queueMicrotask(() => { + if (tryUpdateMessageModel(db, messageId, targetModel, variant)) { + log(`[ultrawork-db-override] Deferred DB update (attempt ${attempt}): ${targetModel.providerID}/${targetModel.modelID}`, { messageId }) + db.close() + return + } + retryViaMicrotask(db, messageId, targetModel, variant, attempt + 1) + }) +} + +/** + * Schedules a deferred SQLite update to change the message model in the DB + * WITHOUT triggering a Bus event. Uses microtask retry loop to wait for + * Session.updateMessage() to save the message first, then overwrites the model. + * + * Falls back to setTimeout(fn, 0) after 10 microtask attempts. + */ +export function scheduleDeferredModelOverride( + messageId: string, + targetModel: { providerID: string; modelID: string }, + variant?: string, +): void { + queueMicrotask(() => { + const dbPath = getDbPath() + if (!existsSync(dbPath)) { + log("[ultrawork-db-override] DB not found, skipping deferred override") + return + } + + const db = new Database(dbPath) + try { + retryViaMicrotask(db, messageId, targetModel, variant, 0) + } catch (error) { + log("[ultrawork-db-override] Failed to apply deferred model override", { + error: String(error), + }) + db.close() + } + }) +} diff --git a/src/plugin/ultrawork-model-override.test.ts b/src/plugin/ultrawork-model-override.test.ts new file mode 100644 index 00000000..0746f33e --- /dev/null +++ b/src/plugin/ultrawork-model-override.test.ts @@ -0,0 +1,365 @@ +import { describe, expect, test, beforeEach, afterEach, spyOn, mock } from "bun:test" +import { + applyUltraworkModelOverrideOnMessage, + resolveUltraworkOverride, + detectUltrawork, +} from "./ultrawork-model-override" +import * as sharedModule from "../shared" +import * as dbOverrideModule from "./ultrawork-db-model-override" + +describe("detectUltrawork", () => { + test("should detect ultrawork keyword", () => { + expect(detectUltrawork("ultrawork do something")).toBe(true) + }) + + test("should detect ulw keyword", () => { + expect(detectUltrawork("ulw fix the bug")).toBe(true) + }) + + test("should be case insensitive", () => { + expect(detectUltrawork("ULTRAWORK do something")).toBe(true) + }) + + test("should not detect in code blocks", () => { + const textWithCodeBlock = [ + "check this:", + "```", + "ultrawork mode", + "```", + ].join("\n") + expect(detectUltrawork(textWithCodeBlock)).toBe(false) + }) + + test("should not detect in inline code", () => { + expect(detectUltrawork("the `ultrawork` mode is cool")).toBe(false) + }) + + test("should not detect when keyword absent", () => { + expect(detectUltrawork("just do something normal")).toBe(false) + }) +}) + +describe("resolveUltraworkOverride", () => { + function createOutput(text: string, agentName?: string) { + return { + message: { + ...(agentName ? { agent: agentName } : {}), + } as Record, + parts: [{ type: "text", text }], + } + } + + function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { + return { + agents: { + [agentName]: { ultrawork }, + }, + } as unknown as Parameters[0] + } + + test("should resolve override when ultrawork keyword detected", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" }) + }) + + test("should return null when no keyword detected", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("just do something normal") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toBeNull() + }) + + test("should return null when agent name is undefined", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, undefined, output) + + //#then + expect(result).toBeNull() + }) + + test("should use message.agent when input agent is undefined", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("ultrawork do something", "sisyphus") + + //#when + const result = resolveUltraworkOverride(config, undefined, output) + + //#then + expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6", variant: undefined }) + }) + + test("should return null when agents config is missing", () => { + //#given + const config = {} as Parameters[0] + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toBeNull() + }) + + test("should return null when agent has no ultrawork config", () => { + //#given + const config = { + agents: { sisyphus: { model: "anthropic/claude-sonnet-4-6" } }, + } as unknown as Parameters[0] + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toBeNull() + }) + + test("should return null when ultrawork.model is not set", () => { + //#given + const config = createConfig("sisyphus", { variant: "max" }) + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toBeNull() + }) + + test("should handle model string with multiple slashes", () => { + //#given + const config = createConfig("sisyphus", { model: "openai/gpt-5.3/codex" }) + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toEqual({ providerID: "openai", modelID: "gpt-5.3/codex", variant: undefined }) + }) + + test("should return null when model string has no slash", () => { + //#given + const config = createConfig("sisyphus", { model: "just-a-model" }) + const output = createOutput("ultrawork do something") + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toBeNull() + }) + + test("should resolve display name to config key", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) + const output = createOutput("ulw do something") + + //#when + const result = resolveUltraworkOverride(config, "Sisyphus (Ultraworker)", output) + + //#then + expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6", variant: "max" }) + }) + + test("should handle multiple text parts by joining them", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = { + message: {} as Record, + parts: [ + { type: "text", text: "hello " }, + { type: "image", text: undefined }, + { type: "text", text: "ultrawork now" }, + ], + } + + //#when + const result = resolveUltraworkOverride(config, "sisyphus", output) + + //#then + expect(result).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6", variant: undefined }) + }) +}) + +describe("applyUltraworkModelOverrideOnMessage", () => { + let logSpy: ReturnType + let dbOverrideSpy: ReturnType + + beforeEach(() => { + logSpy = spyOn(sharedModule, "log").mockImplementation(() => {}) + dbOverrideSpy = spyOn(dbOverrideModule, "scheduleDeferredModelOverride").mockImplementation(() => {}) + }) + + afterEach(() => { + logSpy?.mockRestore() + dbOverrideSpy?.mockRestore() + }) + + function createMockTui() { + return { + showToast: async () => {}, + } + } + + function createOutput( + text: string, + options?: { + existingModel?: { providerID: string; modelID: string } + agentName?: string + messageId?: string + }, + ) { + return { + message: { + ...(options?.existingModel ? { model: options.existingModel } : {}), + ...(options?.agentName ? { agent: options.agentName } : {}), + ...(options?.messageId ? { id: options.messageId } : {}), + } as Record, + parts: [{ type: "text", text }], + } + } + + function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) { + return { + agents: { + [agentName]: { ultrawork }, + }, + } as unknown as Parameters[0] + } + + test("should schedule deferred DB override when message ID present", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) + const output = createOutput("ultrawork do something", { messageId: "msg_123" }) + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(dbOverrideSpy).toHaveBeenCalledWith( + "msg_123", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + "max", + ) + }) + + test("should NOT mutate output.message.model when message ID present", () => { + //#given + const sonnetModel = { providerID: "anthropic", modelID: "claude-sonnet-4-6" } + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("ultrawork do something", { + existingModel: sonnetModel, + messageId: "msg_123", + }) + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(output.message.model).toEqual(sonnetModel) + }) + + test("should fall back to direct mutation when no message ID", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) + const output = createOutput("ultrawork do something") + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(output.message.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" }) + expect(output.message["variant"]).toBe("max") + expect(output.message["thinking"]).toBe("max") + expect(dbOverrideSpy).not.toHaveBeenCalled() + }) + + test("should not apply override when no keyword detected", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("just do something normal", { messageId: "msg_123" }) + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(dbOverrideSpy).not.toHaveBeenCalled() + }) + + test("should log the model transition with deferred DB tag", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const existingModel = { providerID: "anthropic", modelID: "claude-sonnet-4-6" } + const output = createOutput("ultrawork do something", { + existingModel, + messageId: "msg_123", + }) + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining("deferred DB"), + expect.objectContaining({ agent: "sisyphus" }), + ) + }) + + test("should call showToast on override", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6" }) + const output = createOutput("ultrawork do something", { messageId: "msg_123" }) + let toastCalled = false + const tui = { + showToast: async () => { + toastCalled = true + }, + } + + //#when + applyUltraworkModelOverrideOnMessage(config, "sisyphus", output, tui) + + //#then + expect(toastCalled).toBe(true) + }) + + test("should resolve display name to config key with deferred path", () => { + //#given + const config = createConfig("sisyphus", { model: "anthropic/claude-opus-4-6", variant: "max" }) + const output = createOutput("ulw do something", { messageId: "msg_123" }) + const tui = createMockTui() + + //#when + applyUltraworkModelOverrideOnMessage(config, "Sisyphus (Ultraworker)", output, tui) + + //#then + expect(dbOverrideSpy).toHaveBeenCalledWith( + "msg_123", + { providerID: "anthropic", modelID: "claude-opus-4-6" }, + "max", + ) + }) +}) diff --git a/src/plugin/ultrawork-model-override.ts b/src/plugin/ultrawork-model-override.ts new file mode 100644 index 00000000..80facd71 --- /dev/null +++ b/src/plugin/ultrawork-model-override.ts @@ -0,0 +1,127 @@ +import type { OhMyOpenCodeConfig } from "../config" +import type { AgentOverrides } from "../config/schema/agent-overrides" +import { log } from "../shared" +import { getAgentConfigKey } from "../shared/agent-display-names" +import { scheduleDeferredModelOverride } from "./ultrawork-db-model-override" + +const CODE_BLOCK = /```[\s\S]*?```/g +const INLINE_CODE = /`[^`]+`/g +const ULTRAWORK_PATTERN = /\b(ultrawork|ulw)\b/i + +export function detectUltrawork(text: string): boolean { + const clean = text.replace(CODE_BLOCK, "").replace(INLINE_CODE, "") + return ULTRAWORK_PATTERN.test(clean) +} + +function extractPromptText(parts: Array<{ type: string; text?: string }>): string { + return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("") +} + +type ToastFn = { + showToast: (o: { body: Record }) => Promise +} + +function showToast(tui: unknown, title: string, message: string): void { + const toastFn = tui as Partial + if (typeof toastFn.showToast !== "function") return + toastFn.showToast({ + body: { title, message, variant: "warning" as const, duration: 3000 }, + }).catch(() => {}) +} + +export type UltraworkOverrideResult = { + providerID: string + modelID: string + variant?: string +} + +/** + * Resolves the ultrawork model override config for the given agent and prompt text. + * Returns null if no override should be applied. + */ +export function resolveUltraworkOverride( + pluginConfig: OhMyOpenCodeConfig, + inputAgentName: string | undefined, + output: { + message: Record + parts: Array<{ type: string; text?: string; [key: string]: unknown }> + }, +): UltraworkOverrideResult | null { + const promptText = extractPromptText(output.parts) + if (!detectUltrawork(promptText)) return null + + const messageAgentName = + typeof output.message["agent"] === "string" ? (output.message["agent"] as string) : undefined + const rawAgentName = inputAgentName ?? messageAgentName + if (!rawAgentName || !pluginConfig.agents) return null + + const agentConfigKey = getAgentConfigKey(rawAgentName) + const agentConfig = pluginConfig.agents[agentConfigKey as keyof AgentOverrides] + const ultraworkConfig = agentConfig?.ultrawork + if (!ultraworkConfig?.model) return null + + const modelParts = ultraworkConfig.model.split("/") + if (modelParts.length < 2) return null + + return { + providerID: modelParts[0], + modelID: modelParts.slice(1).join("/"), + variant: ultraworkConfig.variant, + } +} + +/** + * Applies ultrawork model override using a deferred DB update strategy. + * + * Instead of directly mutating output.message.model (which would cause the TUI + * bottom bar to show the override model), this schedules a queueMicrotask that + * updates the message model directly in SQLite AFTER Session.updateMessage() + * saves the original model, but BEFORE loop() reads it for the API call. + * + * Result: API call uses opus, TUI bottom bar stays on sonnet. + */ +export function applyUltraworkModelOverrideOnMessage( + pluginConfig: OhMyOpenCodeConfig, + inputAgentName: string | undefined, + output: { + message: Record + parts: Array<{ type: string; text?: string; [key: string]: unknown }> + }, + tui: unknown, +): void { + const override = resolveUltraworkOverride(pluginConfig, inputAgentName, output) + if (!override) return + + const messageId = output.message["id"] as string | undefined + if (!messageId) { + log("[ultrawork-model-override] No message ID found, falling back to direct mutation") + output.message.model = { providerID: override.providerID, modelID: override.modelID } + if (override.variant) { + output.message["variant"] = override.variant + output.message["thinking"] = override.variant + } + return + } + + const fromModel = (output.message.model as { modelID?: string } | undefined)?.modelID ?? "unknown" + const agentConfigKey = getAgentConfigKey( + inputAgentName ?? + (typeof output.message["agent"] === "string" ? (output.message["agent"] as string) : "unknown"), + ) + + scheduleDeferredModelOverride( + messageId, + { providerID: override.providerID, modelID: override.modelID }, + override.variant, + ) + + log(`[ultrawork-model-override] ${fromModel} -> ${override.modelID} (deferred DB)`, { + agent: agentConfigKey, + }) + + showToast( + tui, + "Ultrawork Model Override", + `${fromModel} \u2192 ${override.modelID}. Maximum precision engaged.`, + ) +} diff --git a/src/shared/model-requirements.test.ts b/src/shared/model-requirements.test.ts index 908f9b20..cb96d67f 100644 --- a/src/shared/model-requirements.test.ts +++ b/src/shared/model-requirements.test.ts @@ -64,7 +64,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => { const explore = AGENT_MODEL_REQUIREMENTS["explore"] // when - accessing explore requirement - // then - fallbackChain exists with grok-code-fast-1 as first entry, minimax-m2.5-free as second + // then - fallbackChain: grok → minimax-free → haiku → nano expect(explore).toBeDefined() expect(explore.fallbackChain).toBeArray() expect(explore.fallbackChain).toHaveLength(4) diff --git a/src/shared/model-requirements.ts b/src/shared/model-requirements.ts index d012952b..703749f2 100644 --- a/src/shared/model-requirements.ts +++ b/src/shared/model-requirements.ts @@ -16,10 +16,10 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { sisyphus: { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" }, - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "kimi-k2.5-free" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, + { providers: ["opencode"], model: "kimi-k2.5-free" }, { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "big-pickle" }, + { providers: ["opencode"], model: "big-pickle" }, ], requiresAnyModel: true, }, @@ -38,23 +38,23 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { }, librarian: { fallbackChain: [ - { providers: ["opencode", "opencode-zen-abuse"], model: "minimax-m2.5-free" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "big-pickle" }, + { providers: ["opencode"], model: "minimax-m2.5-free" }, + { providers: ["opencode"], model: "big-pickle" }, ], }, explore: { fallbackChain: [ - { providers: ["github-copilot", "venice"], model: "grok-code-fast-1" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "minimax-m2.5-free" }, + { providers: ["github-copilot"], model: "grok-code-fast-1" }, + { providers: ["opencode"], model: "minimax-m2.5-free" }, { providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "gpt-5-nano" }, + { providers: ["opencode"], model: "gpt-5-nano" }, ], }, "multimodal-looker": { fallbackChain: [ - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "kimi-k2.5-free" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, + { providers: ["opencode"], model: "kimi-k2.5-free" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }, { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, { providers: ["zai-coding-plan"], model: "glm-4.6v" }, @@ -64,16 +64,16 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" }, { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "kimi-k2.5-free" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, + { providers: ["opencode"], model: "kimi-k2.5-free" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }, ], }, metis: { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" }, - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "kimi-k2.5-free" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, + { providers: ["opencode"], model: "kimi-k2.5-free" }, { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" }, ], @@ -87,8 +87,8 @@ export const AGENT_MODEL_REQUIREMENTS: Record = { }, atlas: { fallbackChain: [ - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "kimi-k2.5-free" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, + { providers: ["opencode"], model: "kimi-k2.5-free" }, { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, { providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }, ], @@ -101,7 +101,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro", variant: "high" }, { providers: ["zai-coding-plan", "opencode"], model: "glm-5" }, { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" }, - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, ], }, ultrabrain: { @@ -131,7 +131,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { fallbackChain: [ { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }, - { providers: ["opencode", "opencode-zen-abuse"], model: "gpt-5-nano" }, + { providers: ["opencode"], model: "gpt-5-nano" }, ], }, "unspecified-low": { @@ -150,7 +150,7 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record = { }, writing: { fallbackChain: [ - { providers: ["kimi-for-coding", "friendli"], model: "k2p5" }, + { providers: ["kimi-for-coding"], model: "k2p5" }, { providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }, { providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" }, ],