2026-03-26 19:44:55 +09:00
|
|
|
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs"
|
|
|
|
|
import { tmpdir } from "node:os"
|
|
|
|
|
import { join } from "node:path"
|
2026-02-19 04:41:00 +02:00
|
|
|
|
2026-03-26 18:04:31 +09:00
|
|
|
import { createChatParamsHandler, type ChatParamsOutput } from "./chat-params"
|
2026-03-26 19:44:55 +09:00
|
|
|
import * as dataPathModule from "../shared/data-path"
|
|
|
|
|
import { writeProviderModelsCache } from "../shared"
|
2026-03-18 14:21:27 +01:00
|
|
|
import {
|
|
|
|
|
clearSessionPromptParams,
|
|
|
|
|
getSessionPromptParams,
|
|
|
|
|
setSessionPromptParams,
|
|
|
|
|
} from "../shared/session-prompt-params-state"
|
2026-02-19 04:41:00 +02:00
|
|
|
|
|
|
|
|
describe("createChatParamsHandler", () => {
|
2026-03-26 19:44:55 +09:00
|
|
|
let tempCacheRoot = ""
|
|
|
|
|
let getCacheDirSpy: ReturnType<typeof spyOn>
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
tempCacheRoot = mkdtempSync(join(tmpdir(), "chat-params-cache-"))
|
|
|
|
|
getCacheDirSpy = spyOn(dataPathModule, "getOmoOpenCodeCacheDir").mockReturnValue(
|
|
|
|
|
join(tempCacheRoot, "oh-my-opencode"),
|
|
|
|
|
)
|
|
|
|
|
writeProviderModelsCache({ connected: [], models: {} })
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-18 14:21:27 +01:00
|
|
|
afterEach(() => {
|
|
|
|
|
clearSessionPromptParams("ses_chat_params")
|
2026-03-26 19:44:55 +09:00
|
|
|
clearSessionPromptParams("ses_chat_params_temperature")
|
|
|
|
|
writeProviderModelsCache({ connected: [], models: {} })
|
|
|
|
|
getCacheDirSpy?.mockRestore()
|
|
|
|
|
if (tempCacheRoot) {
|
|
|
|
|
rmSync(tempCacheRoot, { recursive: true, force: true })
|
|
|
|
|
}
|
2026-03-18 14:21:27 +01:00
|
|
|
})
|
|
|
|
|
|
2026-02-19 04:41:00 +02:00
|
|
|
test("normalizes object-style agent payload and runs chat.params hooks", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
let called = false
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: {
|
|
|
|
|
"chat.params": async (input) => {
|
|
|
|
|
called = input.agent.name === "sisyphus"
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const input = {
|
|
|
|
|
sessionID: "ses_chat_params",
|
|
|
|
|
agent: { name: "sisyphus" },
|
2026-04-17 14:51:52 +09:00
|
|
|
model: { providerID: "opencode", modelID: "claude-opus-4-7" },
|
2026-02-19 04:41:00 +02:00
|
|
|
provider: { id: "opencode" },
|
|
|
|
|
message: {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
const output: ChatParamsOutput = {
|
2026-02-19 04:41:00 +02:00
|
|
|
temperature: 0.1,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(called).toBe(true)
|
|
|
|
|
})
|
2026-03-17 11:57:56 +01:00
|
|
|
test("passes the original mutable message object to chat.params hooks", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: {
|
|
|
|
|
"chat.params": async (input) => {
|
|
|
|
|
input.message.variant = "high"
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const message = { variant: "max" }
|
|
|
|
|
const input = {
|
|
|
|
|
sessionID: "ses_chat_params",
|
|
|
|
|
agent: { name: "sisyphus" },
|
|
|
|
|
model: { providerID: "opencode", modelID: "claude-sonnet-4-6" },
|
|
|
|
|
provider: { id: "opencode" },
|
|
|
|
|
message,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
const output: ChatParamsOutput = {
|
2026-03-17 11:57:56 +01:00
|
|
|
temperature: 0.1,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(message.variant).toBe("high")
|
|
|
|
|
})
|
2026-03-18 14:21:27 +01:00
|
|
|
|
|
|
|
|
test("applies stored prompt params for the session", async () => {
|
|
|
|
|
//#given
|
2026-03-26 19:44:55 +09:00
|
|
|
writeProviderModelsCache({
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
models: {
|
|
|
|
|
openai: [
|
|
|
|
|
{
|
|
|
|
|
id: "gpt-5.4",
|
|
|
|
|
name: "GPT-5.4",
|
|
|
|
|
temperature: true,
|
|
|
|
|
reasoning: true,
|
|
|
|
|
variants: {
|
|
|
|
|
low: {},
|
|
|
|
|
high: {},
|
|
|
|
|
},
|
|
|
|
|
limit: { output: 128_000 },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-26 18:04:31 +09:00
|
|
|
setSessionPromptParams("ses_chat_params_temperature", {
|
2026-03-18 14:21:27 +01:00
|
|
|
temperature: 0.4,
|
|
|
|
|
topP: 0.7,
|
2026-04-08 13:02:51 +09:00
|
|
|
maxOutputTokens: 4096,
|
2026-03-18 14:21:27 +01:00
|
|
|
options: {
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const input = {
|
2026-03-26 18:04:31 +09:00
|
|
|
sessionID: "ses_chat_params_temperature",
|
2026-03-18 14:21:27 +01:00
|
|
|
agent: { name: "oracle" },
|
|
|
|
|
model: { providerID: "openai", modelID: "gpt-5.4" },
|
|
|
|
|
provider: { id: "openai" },
|
|
|
|
|
message: {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 18:04:31 +09:00
|
|
|
const output: ChatParamsOutput = {
|
2026-03-18 14:21:27 +01:00
|
|
|
temperature: 0.1,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: { existing: true },
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(output).toEqual({
|
2026-03-26 18:04:31 +09:00
|
|
|
temperature: 0.4,
|
2026-03-18 14:21:27 +01:00
|
|
|
topP: 0.7,
|
|
|
|
|
topK: 1,
|
2026-04-08 13:02:51 +09:00
|
|
|
maxOutputTokens: 4096,
|
2026-03-18 14:21:27 +01:00
|
|
|
options: {
|
|
|
|
|
existing: true,
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-03-26 18:04:31 +09:00
|
|
|
expect(getSessionPromptParams("ses_chat_params_temperature")).toEqual({
|
2026-03-18 14:21:27 +01:00
|
|
|
temperature: 0.4,
|
|
|
|
|
topP: 0.7,
|
2026-04-08 13:02:51 +09:00
|
|
|
maxOutputTokens: 4096,
|
2026-03-18 14:21:27 +01:00
|
|
|
options: {
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-25 14:47:46 +01:00
|
|
|
|
2026-04-08 13:02:51 +09:00
|
|
|
test("drops gpt-5.4 temperature and clamps maxOutputTokens from bundled model capabilities", async () => {
|
2026-03-25 14:47:46 +01:00
|
|
|
//#given
|
2026-03-26 18:04:31 +09:00
|
|
|
setSessionPromptParams("ses_chat_params_temperature", {
|
2026-03-25 14:47:46 +01:00
|
|
|
temperature: 0.7,
|
2026-04-08 13:02:51 +09:00
|
|
|
maxOutputTokens: 200_000,
|
2026-03-25 14:47:46 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const input = {
|
2026-03-26 18:04:31 +09:00
|
|
|
sessionID: "ses_chat_params_temperature",
|
2026-03-25 14:47:46 +01:00
|
|
|
agent: { name: "oracle" },
|
|
|
|
|
model: { providerID: "openai", modelID: "gpt-5.4" },
|
|
|
|
|
provider: { id: "openai" },
|
|
|
|
|
message: {},
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
const output: ChatParamsOutput = {
|
2026-03-25 14:47:46 +01:00
|
|
|
temperature: 0.1,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(output).toEqual({
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
2026-04-08 13:02:51 +09:00
|
|
|
maxOutputTokens: 128_000,
|
|
|
|
|
options: {},
|
2026-03-25 14:47:46 +01:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("drops unsupported reasoning settings from bundled model capabilities", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
setSessionPromptParams("ses_chat_params", {
|
|
|
|
|
temperature: 0.4,
|
|
|
|
|
options: {
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
thinking: { type: "enabled", budgetTokens: 4096 },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const input = {
|
|
|
|
|
sessionID: "ses_chat_params",
|
|
|
|
|
agent: { name: "oracle" },
|
|
|
|
|
model: { providerID: "openai", modelID: "gpt-4.1" },
|
|
|
|
|
provider: { id: "openai" },
|
|
|
|
|
message: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const output = {
|
|
|
|
|
temperature: 0.1,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(output).toEqual({
|
|
|
|
|
temperature: 0.4,
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
options: {},
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-05-10 15:03:01 +09:00
|
|
|
|
|
|
|
|
test("falls back to default maxOutputTokens when stored and compatibility tokens are non-positive", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
setSessionPromptParams("ses_chat_params", {
|
|
|
|
|
maxOutputTokens: 0,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handler = createChatParamsHandler({
|
|
|
|
|
anthropicEffort: null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const input = {
|
|
|
|
|
sessionID: "ses_chat_params",
|
|
|
|
|
agent: { name: "oracle" },
|
|
|
|
|
model: { providerID: "custom-provider", modelID: "custom-model" },
|
|
|
|
|
provider: { id: "custom-provider" },
|
|
|
|
|
message: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const output: ChatParamsOutput = {
|
|
|
|
|
topP: 1,
|
|
|
|
|
topK: 1,
|
|
|
|
|
maxOutputTokens: 0,
|
|
|
|
|
options: {},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await handler(input, output)
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(output.maxOutputTokens).toBe(4096)
|
|
|
|
|
})
|
2026-02-19 04:41:00 +02:00
|
|
|
})
|