feat(ultrawork): implement per-message model override with deferred DB retry strategy
- 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)
This commit is contained in:
parent
50de1a18f2
commit
64b2d69036
@ -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
|
||||
}
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
180
src/plugin/ultrawork-db-model-override.test.ts
Normal file
180
src/plugin/ultrawork-db-model-override.test.ts
Normal file
@ -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<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
let remaining = depth
|
||||
function step() {
|
||||
if (remaining <= 0) { resolve(); return }
|
||||
remaining--
|
||||
queueMicrotask(step)
|
||||
}
|
||||
queueMicrotask(step)
|
||||
})
|
||||
}
|
||||
|
||||
function flushWithTimeout(): Promise<void> {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, 10))
|
||||
}
|
||||
|
||||
describe("scheduleDeferredModelOverride", () => {
|
||||
let tempDir: string
|
||||
let dbPath: string
|
||||
let logSpy: ReturnType<typeof spyOn>
|
||||
let getDataDirSpy: ReturnType<typeof spyOn>
|
||||
|
||||
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"),
|
||||
)
|
||||
})
|
||||
})
|
||||
95
src/plugin/ultrawork-db-model-override.ts
Normal file
95
src/plugin/ultrawork-db-model-override.ts
Normal file
@ -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<typeof Database>,
|
||||
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<typeof Database>,
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
365
src/plugin/ultrawork-model-override.test.ts
Normal file
365
src/plugin/ultrawork-model-override.test.ts
Normal file
@ -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<string, unknown>,
|
||||
parts: [{ type: "text", text }],
|
||||
}
|
||||
}
|
||||
|
||||
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
||||
return {
|
||||
agents: {
|
||||
[agentName]: { ultrawork },
|
||||
},
|
||||
} as unknown as Parameters<typeof resolveUltraworkOverride>[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<typeof resolveUltraworkOverride>[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<typeof resolveUltraworkOverride>[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<string, unknown>,
|
||||
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<typeof spyOn>
|
||||
let dbOverrideSpy: ReturnType<typeof spyOn>
|
||||
|
||||
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<string, unknown>,
|
||||
parts: [{ type: "text", text }],
|
||||
}
|
||||
}
|
||||
|
||||
function createConfig(agentName: string, ultrawork: { model?: string; variant?: string }) {
|
||||
return {
|
||||
agents: {
|
||||
[agentName]: { ultrawork },
|
||||
},
|
||||
} as unknown as Parameters<typeof applyUltraworkModelOverrideOnMessage>[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",
|
||||
)
|
||||
})
|
||||
})
|
||||
127
src/plugin/ultrawork-model-override.ts
Normal file
127
src/plugin/ultrawork-model-override.ts
Normal file
@ -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<string, unknown> }) => Promise<unknown>
|
||||
}
|
||||
|
||||
function showToast(tui: unknown, title: string, message: string): void {
|
||||
const toastFn = tui as Partial<ToastFn>
|
||||
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<string, unknown>
|
||||
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<string, unknown>
|
||||
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.`,
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -16,10 +16,10 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
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<string, ModelRequirement> = {
|
||||
},
|
||||
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<string, ModelRequirement> = {
|
||||
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<string, ModelRequirement> = {
|
||||
},
|
||||
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<string, ModelRequirement> = {
|
||||
{ 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<string, ModelRequirement> = {
|
||||
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<string, ModelRequirement> = {
|
||||
},
|
||||
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" },
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user