From 279f0d150f234b4102f07c9e75a17acb97d60dcc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 10 May 2026 15:03:01 +0900 Subject: [PATCH 1/2] fix(chat-params): guard non-positive max output tokens --- src/plugin/chat-params.test.ts | 32 +++++++++++++++++++ src/plugin/chat-params.ts | 11 +++++-- .../model-settings-compatibility.test.ts | 12 +++++++ src/shared/model-settings-compatibility.ts | 4 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/plugin/chat-params.test.ts b/src/plugin/chat-params.test.ts index 5886b7204..de7acb5df 100644 --- a/src/plugin/chat-params.test.ts +++ b/src/plugin/chat-params.test.ts @@ -253,4 +253,36 @@ describe("createChatParamsHandler", () => { options: {}, }) }) + + 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) + }) }) diff --git a/src/plugin/chat-params.ts b/src/plugin/chat-params.ts index 41e4a0200..ac665b62b 100644 --- a/src/plugin/chat-params.ts +++ b/src/plugin/chat-params.ts @@ -96,7 +96,10 @@ export function createChatParamsHandler(args: { if (storedPromptParams.topP !== undefined) { output.topP = storedPromptParams.topP } - if (storedPromptParams.maxOutputTokens !== undefined) { + if ( + typeof storedPromptParams.maxOutputTokens === "number" && + storedPromptParams.maxOutputTokens > 0 + ) { (output as Record).maxOutputTokens = storedPromptParams.maxOutputTokens } if (storedPromptParams.options) { @@ -162,10 +165,12 @@ export function createChatParamsHandler(args: { } if ("maxTokens" in compatibility) { - if (compatibility.maxTokens !== undefined) { + if (compatibility.maxTokens !== undefined && compatibility.maxTokens > 0) { output.maxOutputTokens = compatibility.maxTokens } else { - delete output.maxOutputTokens + const capabilitiesLimit = capabilities?.maxOutputTokens + output.maxOutputTokens = + typeof capabilitiesLimit === "number" && capabilitiesLimit > 0 ? capabilitiesLimit : 4096 } } diff --git a/src/shared/model-settings-compatibility.test.ts b/src/shared/model-settings-compatibility.test.ts index 725b2a54c..a653f7259 100644 --- a/src/shared/model-settings-compatibility.test.ts +++ b/src/shared/model-settings-compatibility.test.ts @@ -553,6 +553,18 @@ describe("resolveCompatibleModelSettings", () => { expect(result.changes).toEqual([]) }) + test("#given desired.maxTokens is 0 #then maxTokens is dropped", () => { + const result = resolveCompatibleModelSettings({ + providerID: "openai", + modelID: "gpt-5.4", + desired: { maxTokens: 0 }, + capabilities: { maxOutputTokens: 128_000 }, + }) + + expect(result.maxTokens).toBeUndefined() + expect(result.changes).toEqual([]) + }) + // Passthrough: undefined desired values produce no changes test("no-op when desired settings are empty", () => { const result = resolveCompatibleModelSettings({ diff --git a/src/shared/model-settings-compatibility.ts b/src/shared/model-settings-compatibility.ts index c2354038e..4e9145192 100644 --- a/src/shared/model-settings-compatibility.ts +++ b/src/shared/model-settings-compatibility.ts @@ -162,6 +162,10 @@ export function resolveCompatibleModelSettings( } let maxTokens = input.desired.maxTokens + if (maxTokens !== undefined && maxTokens <= 0) { + maxTokens = undefined + } + if ( maxTokens !== undefined && input.capabilities?.maxOutputTokens !== undefined && From 2f8ce576dccdc1e1b28ef22da936200652daa8e4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 11 May 2026 08:47:49 +0900 Subject: [PATCH 2/2] fix(chat-params): use conservative token fallback for invalid limits Avoid inflating non-positive maxOutputTokens values to model capability maxima by always falling back to a safe fixed budget. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/plugin/chat-params.test.ts | 46 +++++++++++++++++++++++++++++++--- src/plugin/chat-params.ts | 16 +++++++++--- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/plugin/chat-params.test.ts b/src/plugin/chat-params.test.ts index de7acb5df..736e36d21 100644 --- a/src/plugin/chat-params.test.ts +++ b/src/plugin/chat-params.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path" import { createChatParamsHandler, type ChatParamsOutput } from "./chat-params" import * as dataPathModule from "../shared/data-path" -import { writeProviderModelsCache } from "../shared" +import * as sharedModule from "../shared" import { clearSessionPromptParams, getSessionPromptParams, @@ -21,13 +21,13 @@ describe("createChatParamsHandler", () => { getCacheDirSpy = spyOn(dataPathModule, "getOmoOpenCodeCacheDir").mockReturnValue( join(tempCacheRoot, "oh-my-opencode"), ) - writeProviderModelsCache({ connected: [], models: {} }) + sharedModule.writeProviderModelsCache({ connected: [], models: {} }) }) afterEach(() => { clearSessionPromptParams("ses_chat_params") clearSessionPromptParams("ses_chat_params_temperature") - writeProviderModelsCache({ connected: [], models: {} }) + sharedModule.writeProviderModelsCache({ connected: [], models: {} }) getCacheDirSpy?.mockRestore() if (tempCacheRoot) { rmSync(tempCacheRoot, { recursive: true, force: true }) @@ -101,7 +101,7 @@ describe("createChatParamsHandler", () => { test("applies stored prompt params for the session", async () => { //#given - writeProviderModelsCache({ + sharedModule.writeProviderModelsCache({ connected: ["openai"], models: { openai: [ @@ -256,6 +256,7 @@ describe("createChatParamsHandler", () => { test("falls back to default maxOutputTokens when stored and compatibility tokens are non-positive", async () => { //#given + const logSpy = spyOn(sharedModule, "log").mockImplementation(() => undefined) setSessionPromptParams("ses_chat_params", { maxOutputTokens: 0, }) @@ -282,6 +283,43 @@ describe("createChatParamsHandler", () => { //#when await handler(input, output) + //#then + expect(output.maxOutputTokens).toBe(4096) + expect(logSpy).toHaveBeenCalledWith( + "[plugin] maxOutputTokens=0 is non-positive; using safe fallback 4096", + ) + + logSpy.mockRestore() + }) + + test("uses safe fallback instead of model max when stored maxOutputTokens is non-positive", async () => { + //#given + setSessionPromptParams("ses_chat_params", { + maxOutputTokens: -1, + }) + + const handler = createChatParamsHandler({ + anthropicEffort: null, + }) + + const input = { + sessionID: "ses_chat_params", + agent: { name: "oracle" }, + model: { providerID: "openai", modelID: "gpt-5.4" }, + provider: { id: "openai" }, + message: {}, + } + + const output: ChatParamsOutput = { + topP: 1, + topK: 1, + maxOutputTokens: -1, + options: {}, + } + + //#when + await handler(input, output) + //#then expect(output.maxOutputTokens).toBe(4096) }) diff --git a/src/plugin/chat-params.ts b/src/plugin/chat-params.ts index ac665b62b..26f35d03d 100644 --- a/src/plugin/chat-params.ts +++ b/src/plugin/chat-params.ts @@ -1,5 +1,7 @@ import { getSessionPromptParams } from "../shared/session-prompt-params-state" -import { getModelCapabilities, resolveCompatibleModelSettings } from "../shared" +import { getModelCapabilities, log, resolveCompatibleModelSettings } from "../shared" + +const SAFE_MAX_OUTPUT_TOKENS_FALLBACK = 4096 export type ChatParamsInput = { sessionID: string @@ -168,9 +170,15 @@ export function createChatParamsHandler(args: { if (compatibility.maxTokens !== undefined && compatibility.maxTokens > 0) { output.maxOutputTokens = compatibility.maxTokens } else { - const capabilitiesLimit = capabilities?.maxOutputTokens - output.maxOutputTokens = - typeof capabilitiesLimit === "number" && capabilitiesLimit > 0 ? capabilitiesLimit : 4096 + const originalMaxOutputTokens = typeof output.maxOutputTokens === "number" + ? output.maxOutputTokens + : compatibility.maxTokens + output.maxOutputTokens = SAFE_MAX_OUTPUT_TOKENS_FALLBACK + if (typeof originalMaxOutputTokens === "number" && originalMaxOutputTokens <= 0) { + log( + `[plugin] maxOutputTokens=${originalMaxOutputTokens} is non-positive; using safe fallback ${SAFE_MAX_OUTPUT_TOKENS_FALLBACK}`, + ) + } } }