refactor(think-mode): migrate hook from chat.params to chat.message and remove thinking config injection
Drop provider-specific thinking config injection (THINKING_CONFIGS, getThinkingConfig, resolveProvider) and instead rely on the provider to handle thinking based on the variant field. Hook now fires on chat.message using model from input rather than from the message object. 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
+113
-410
@@ -1,452 +1,155 @@
|
||||
import { describe, expect, it, beforeEach } from "bun:test"
|
||||
import type { ThinkModeInput } from "./types"
|
||||
import { beforeEach, describe, expect, it } from "bun:test"
|
||||
|
||||
const { createThinkModeHook, clearThinkModeState } = await import("./index")
|
||||
const { clearThinkModeState, createThinkModeHook } = await import("./index")
|
||||
|
||||
type ThinkModeHookInput = {
|
||||
sessionID: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
}
|
||||
|
||||
type ThinkModeHookOutput = {
|
||||
message: Record<string, unknown>
|
||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
}
|
||||
|
||||
function createHookInput(args: {
|
||||
sessionID?: string
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
}): ThinkModeHookInput {
|
||||
const { sessionID = "test-session-id", providerID, modelID } = args
|
||||
|
||||
if (!providerID || !modelID) {
|
||||
return { sessionID }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a mock ThinkModeInput for testing
|
||||
*/
|
||||
function createMockInput(
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
promptText: string
|
||||
): ThinkModeInput {
|
||||
return {
|
||||
parts: [{ type: "text", text: promptText }],
|
||||
message: {
|
||||
model: {
|
||||
providerID,
|
||||
modelID,
|
||||
},
|
||||
},
|
||||
sessionID,
|
||||
model: { providerID, modelID },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper for accessing dynamically injected properties on message
|
||||
*/
|
||||
type MessageWithInjectedProps = Record<string, unknown>
|
||||
function createHookOutput(promptText: string, variant?: string): ThinkModeHookOutput {
|
||||
return {
|
||||
message: variant ? { variant } : {},
|
||||
parts: [{ type: "text", text: promptText }],
|
||||
}
|
||||
}
|
||||
|
||||
describe("createThinkModeHook integration", () => {
|
||||
describe("createThinkModeHook", () => {
|
||||
const sessionID = "test-session-id"
|
||||
|
||||
beforeEach(() => {
|
||||
clearThinkModeState(sessionID)
|
||||
})
|
||||
|
||||
describe("GitHub Copilot provider integration", () => {
|
||||
describe("Claude models", () => {
|
||||
it("should activate thinking mode for github-copilot Claude with think keyword", async () => {
|
||||
// given a github-copilot Claude model and prompt with "think" keyword
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"claude-opus-4-6",
|
||||
"Please think deeply about this problem"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant and inject thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||
expect(message.thinking).toBeDefined()
|
||||
expect((message.thinking as Record<string, unknown>)?.type).toBe(
|
||||
"enabled"
|
||||
)
|
||||
expect(
|
||||
(message.thinking as Record<string, unknown>)?.budgetTokens
|
||||
).toBe(64000)
|
||||
})
|
||||
|
||||
it("should handle github-copilot Claude with dots in version", async () => {
|
||||
// given a github-copilot Claude model with dot format (claude-opus-4.6)
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"claude-opus-4.6",
|
||||
"ultrathink mode"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant (hyphen format)
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||
expect(message.thinking).toBeDefined()
|
||||
})
|
||||
|
||||
it("should handle github-copilot Claude Sonnet", async () => {
|
||||
// given a github-copilot Claude Sonnet model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"claude-sonnet-4-6",
|
||||
"think about this"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-sonnet-4-6-high")
|
||||
expect(message.thinking).toBeDefined()
|
||||
})
|
||||
it("sets high variant and switches model when think keyword is present", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "github-copilot",
|
||||
modelID: "claude-opus-4-6",
|
||||
})
|
||||
const output = createHookOutput("Please think deeply about this")
|
||||
|
||||
describe("Gemini models", () => {
|
||||
it("should activate thinking mode for github-copilot Gemini Pro", async () => {
|
||||
// given a github-copilot Gemini Pro model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gemini-3-pro",
|
||||
"think about this"
|
||||
)
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant and inject google thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-pro-high")
|
||||
expect(message.providerOptions).toBeDefined()
|
||||
const googleOptions = (
|
||||
message.providerOptions as Record<string, unknown>
|
||||
)?.google as Record<string, unknown>
|
||||
expect(googleOptions?.thinkingConfig).toBeDefined()
|
||||
})
|
||||
|
||||
it("should activate thinking mode for github-copilot Gemini Flash", async () => {
|
||||
// given a github-copilot Gemini Flash model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gemini-3-flash",
|
||||
"ultrathink"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-flash-high")
|
||||
expect(message.providerOptions).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("GPT models", () => {
|
||||
it("should activate thinking mode for github-copilot GPT-5.2", async () => {
|
||||
// given a github-copilot GPT-5.2 model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gpt-5.2",
|
||||
"please think"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant and inject openai thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gpt-5-2-high")
|
||||
expect(message.reasoning_effort).toBe("high")
|
||||
})
|
||||
|
||||
it("should activate thinking mode for github-copilot GPT-5", async () => {
|
||||
// given a github-copilot GPT-5 model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput("github-copilot", "gpt-5", "think deeply")
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should upgrade to high variant
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gpt-5-high")
|
||||
expect(message.reasoning_effort).toBe("high")
|
||||
})
|
||||
})
|
||||
|
||||
describe("No think keyword", () => {
|
||||
it("should NOT activate for github-copilot without think keyword", async () => {
|
||||
// given a prompt without any think keyword
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"claude-opus-4-6",
|
||||
"Just do this task"
|
||||
)
|
||||
const originalModelID = input.message.model?.modelID
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should NOT change model or inject config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe(originalModelID)
|
||||
expect(message.thinking).toBeUndefined()
|
||||
})
|
||||
// then
|
||||
expect(output.message.variant).toBe("high")
|
||||
expect(output.message.model).toEqual({
|
||||
providerID: "github-copilot",
|
||||
modelID: "claude-opus-4-6-high",
|
||||
})
|
||||
})
|
||||
|
||||
describe("Backwards compatibility with direct providers", () => {
|
||||
it("should still work for direct anthropic provider", async () => {
|
||||
// given direct anthropic provider
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"anthropic",
|
||||
"claude-sonnet-4-6",
|
||||
"think about this"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should work as before
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-sonnet-4-6-high")
|
||||
expect(message.thinking).toBeDefined()
|
||||
it("supports dotted model IDs by switching to normalized high variant", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "github-copilot",
|
||||
modelID: "gpt-5.2",
|
||||
})
|
||||
const output = createHookOutput("ultrathink about this")
|
||||
|
||||
it("should work for direct google-vertex-anthropic provider", async () => {
|
||||
//#given direct google-vertex-anthropic provider
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"google-vertex-anthropic",
|
||||
"claude-opus-4-6",
|
||||
"think deeply"
|
||||
)
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
//#when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
//#then should upgrade model and inject Claude thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||
expect(message.thinking).toBeDefined()
|
||||
expect((message.thinking as Record<string, unknown>)?.budgetTokens).toBe(
|
||||
64000
|
||||
)
|
||||
})
|
||||
|
||||
it("should still work for direct google provider", async () => {
|
||||
// given direct google provider
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"google",
|
||||
"gemini-3-pro",
|
||||
"think about this"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should work as before
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gemini-3-pro-high")
|
||||
expect(message.providerOptions).toBeDefined()
|
||||
})
|
||||
|
||||
it("should still work for direct openai provider", async () => {
|
||||
// given direct openai provider
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput("openai", "gpt-5", "think about this")
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should work
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gpt-5-high")
|
||||
expect(message.reasoning_effort).toBe("high")
|
||||
})
|
||||
|
||||
it("should still work for amazon-bedrock provider", async () => {
|
||||
// given amazon-bedrock provider
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"amazon-bedrock",
|
||||
"claude-sonnet-4-6",
|
||||
"think"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should inject bedrock thinking config
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-sonnet-4-6-high")
|
||||
expect(message.reasoningConfig).toBeDefined()
|
||||
// then
|
||||
expect(output.message.variant).toBe("high")
|
||||
expect(output.message.model).toEqual({
|
||||
providerID: "github-copilot",
|
||||
modelID: "gpt-5-2-high",
|
||||
})
|
||||
})
|
||||
|
||||
describe("Already-high variants", () => {
|
||||
it("should NOT re-upgrade already-high variants", async () => {
|
||||
// given an already-high variant model
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"claude-opus-4-6-high",
|
||||
"think deeply"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should NOT modify the model (already high)
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("claude-opus-4-6-high")
|
||||
// No additional thinking config should be injected
|
||||
expect(message.thinking).toBeUndefined()
|
||||
it("skips when message variant is already set", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "github-copilot",
|
||||
modelID: "claude-sonnet-4-6",
|
||||
})
|
||||
const output = createHookOutput("think through this", "max")
|
||||
|
||||
it("should NOT re-upgrade already-high GPT variants", async () => {
|
||||
// given an already-high GPT variant
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"gpt-5.2-high",
|
||||
"ultrathink"
|
||||
)
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should NOT modify the model
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("gpt-5.2-high")
|
||||
expect(message.reasoning_effort).toBeUndefined()
|
||||
})
|
||||
// then
|
||||
expect(output.message.variant).toBe("max")
|
||||
expect(output.message.model).toBeUndefined()
|
||||
})
|
||||
|
||||
describe("Unknown models", () => {
|
||||
it("should not crash for unknown models via github-copilot", async () => {
|
||||
// given an unknown model type
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"github-copilot",
|
||||
"llama-3-70b",
|
||||
"think about this"
|
||||
)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should not crash and model should remain unchanged
|
||||
expect(input.message.model?.modelID).toBe("llama-3-70b")
|
||||
it("does nothing when think keyword is absent", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "google",
|
||||
modelID: "gemini-3-pro",
|
||||
})
|
||||
const output = createHookOutput("Please solve this directly")
|
||||
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
// then
|
||||
expect(output.message.variant).toBeUndefined()
|
||||
expect(output.message.model).toBeUndefined()
|
||||
})
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("should handle missing model gracefully", async () => {
|
||||
// given input without a model
|
||||
const hook = createThinkModeHook()
|
||||
const input: ThinkModeInput = {
|
||||
parts: [{ type: "text", text: "think about this" }],
|
||||
message: {},
|
||||
}
|
||||
|
||||
// when the chat.params hook is called
|
||||
// then should not crash
|
||||
await expect(
|
||||
hook["chat.params"](input, sessionID)
|
||||
).resolves.toBeUndefined()
|
||||
it("does not modify already-high models", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({
|
||||
sessionID,
|
||||
providerID: "openai",
|
||||
modelID: "gpt-5-high",
|
||||
})
|
||||
const output = createHookOutput("think deeply")
|
||||
|
||||
it("should handle empty prompt gracefully", async () => {
|
||||
// given empty prompt
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput("github-copilot", "claude-opus-4-6", "")
|
||||
// when
|
||||
await hook["chat.message"](input, output)
|
||||
|
||||
// when the chat.params hook is called
|
||||
await hook["chat.params"](input, sessionID)
|
||||
|
||||
// then should not upgrade (no think keyword)
|
||||
expect(input.message.model?.modelID).toBe("claude-opus-4-6")
|
||||
})
|
||||
// then
|
||||
expect(output.message.variant).toBeUndefined()
|
||||
expect(output.message.model).toBeUndefined()
|
||||
})
|
||||
|
||||
describe("Agent-level thinking configuration respect", () => {
|
||||
it("should omit Z.ai GLM disabled thinking config", async () => {
|
||||
//#given a Z.ai GLM model with think prompt
|
||||
const hook = createThinkModeHook()
|
||||
const input = createMockInput(
|
||||
"zai-coding-plan",
|
||||
"glm-5",
|
||||
"ultrathink mode"
|
||||
)
|
||||
it("handles missing input model without crashing", async () => {
|
||||
// given
|
||||
const hook = createThinkModeHook()
|
||||
const input = createHookInput({ sessionID })
|
||||
const output = createHookOutput("think about this")
|
||||
|
||||
//#when think mode resolves Z.ai thinking configuration
|
||||
await hook["chat.params"](input, sessionID)
|
||||
// when
|
||||
await expect(hook["chat.message"](input, output)).resolves.toBeUndefined()
|
||||
|
||||
//#then thinking config should be omitted from request
|
||||
const message = input.message as MessageWithInjectedProps
|
||||
expect(input.message.model?.modelID).toBe("glm-5")
|
||||
expect(message.thinking).toBeUndefined()
|
||||
expect(message.providerOptions).toBeUndefined()
|
||||
})
|
||||
|
||||
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<string, unknown>
|
||||
expect((providerOpts.google as Record<string, unknown>).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()
|
||||
})
|
||||
// then
|
||||
expect(output.message.variant).toBeUndefined()
|
||||
expect(output.message.model).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user