diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index d0f7da9a0..0d20b75b5 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -220,6 +220,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -346,6 +391,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -472,6 +562,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -598,6 +733,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -724,6 +904,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -850,6 +1075,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -976,6 +1246,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1102,6 +1417,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1228,6 +1588,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1354,6 +1759,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1480,6 +1930,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1606,6 +2101,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } }, @@ -1732,6 +2272,51 @@ ] } } + }, + "maxTokens": { + "type": "number" + }, + "thinking": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + }, + "budgetTokens": { + "type": "number" + } + }, + "required": [ + "type" + ] + }, + "reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high", + "xhigh" + ] + }, + "textVerbosity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "providerOptions": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} } } } diff --git a/src/config/schema.ts b/src/config/schema.ts index 06a3217d5..08e5a1f2e 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -116,6 +116,19 @@ export const AgentOverrideConfigSchema = z.object({ .regex(/^#[0-9A-Fa-f]{6}$/) .optional(), permission: AgentPermissionSchema.optional(), + /** Maximum tokens for response. Passed directly to OpenCode SDK. */ + maxTokens: z.number().optional(), + /** Extended thinking configuration (Anthropic). Overrides category and default settings. */ + thinking: z.object({ + type: z.enum(["enabled", "disabled"]), + budgetTokens: z.number().optional(), + }).optional(), + /** Reasoning effort level (OpenAI). Overrides category and default settings. */ + reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(), + /** Text verbosity level. */ + textVerbosity: z.enum(["low", "medium", "high"]).optional(), + /** Provider-specific options. Passed directly to OpenCode SDK. */ + providerOptions: z.record(z.string(), z.unknown()).optional(), }) export const AgentOverridesSchema = z.object({ diff --git a/src/hooks/think-mode/index.test.ts b/src/hooks/think-mode/index.test.ts index 571ecae96..50ee37a0c 100644 --- a/src/hooks/think-mode/index.test.ts +++ b/src/hooks/think-mode/index.test.ts @@ -350,4 +350,63 @@ describe("createThinkModeHook integration", () => { expect(input.message.model?.modelID).toBe("claude-opus-4-5") }) }) + + describe("Agent-level thinking configuration respect", () => { + it("should NOT inject thinking config when agent has thinking disabled", async () => { + // #given agent with thinking explicitly disabled + const hook = createThinkModeHook() + const input: ThinkModeInput = { + parts: [{ type: "text", text: "ultrathink deeply" }], + message: { + model: { providerID: "google", modelID: "gemini-3-pro" }, + thinking: { type: "disabled" }, + } as ThinkModeInput["message"], + } + + // #when the chat.params hook is called + await hook["chat.params"](input, sessionID) + + // #then should NOT override agent's thinking disabled setting + const message = input.message as MessageWithInjectedProps + expect((message.thinking as { type: string }).type).toBe("disabled") + expect(message.providerOptions).toBeUndefined() + }) + + it("should NOT inject thinking config when agent has custom providerOptions", async () => { + // #given agent with custom providerOptions + const hook = createThinkModeHook() + const input: ThinkModeInput = { + parts: [{ type: "text", text: "ultrathink" }], + message: { + model: { providerID: "google", modelID: "gemini-3-flash" }, + providerOptions: { + google: { thinkingConfig: { thinkingBudget: 0 } }, + }, + } as ThinkModeInput["message"], + } + + // #when the chat.params hook is called + await hook["chat.params"](input, sessionID) + + // #then should NOT override agent's providerOptions + const message = input.message as MessageWithInjectedProps + const providerOpts = message.providerOptions as Record + expect((providerOpts.google as Record).thinkingConfig).toEqual({ + thinkingBudget: 0, + }) + }) + + it("should still inject thinking config when agent has no thinking override", async () => { + // #given agent without thinking override + const hook = createThinkModeHook() + const input = createMockInput("google", "gemini-3-pro", "ultrathink") + + // #when the chat.params hook is called + await hook["chat.params"](input, sessionID) + + // #then should inject thinking config as normal + const message = input.message as MessageWithInjectedProps + expect(message.providerOptions).toBeDefined() + }) + }) }) diff --git a/src/hooks/think-mode/index.ts b/src/hooks/think-mode/index.ts index b8d796665..d8aafc253 100644 --- a/src/hooks/think-mode/index.ts +++ b/src/hooks/think-mode/index.ts @@ -65,13 +65,32 @@ export function createThinkModeHook() { } if (thinkingConfig) { - Object.assign(output.message, thinkingConfig) - state.thinkingConfigInjected = true - log("Think mode: thinking config injected", { - sessionID, - provider: currentModel.providerID, - config: thinkingConfig, - }) + const messageData = output.message as Record + const agentThinking = messageData.thinking as { type?: string } | undefined + const agentProviderOptions = messageData.providerOptions + + const agentDisabledThinking = agentThinking?.type === "disabled" + const agentHasCustomProviderOptions = Boolean(agentProviderOptions) + + if (agentDisabledThinking) { + log("Think mode: skipping - agent has thinking disabled", { + sessionID, + provider: currentModel.providerID, + }) + } else if (agentHasCustomProviderOptions) { + log("Think mode: skipping - agent has custom providerOptions", { + sessionID, + provider: currentModel.providerID, + }) + } else { + Object.assign(output.message, thinkingConfig) + state.thinkingConfigInjected = true + log("Think mode: thinking config injected", { + sessionID, + provider: currentModel.providerID, + config: thinkingConfig, + }) + } } thinkModeState.set(sessionID, state)