fix(anthropic-effort): clamp variant against mutable request message

This commit is contained in:
Ravi Tharuma
2026-03-17 11:57:56 +01:00
parent 9346bc8379
commit 71b1f7e807
4 changed files with 61 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
import { log, normalizeModelID } from "../../shared"
const OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i
const OPUS_PATTERN = /claude-opus/i
function isClaudeProvider(providerID: string, modelID: string): boolean {
if (["anthropic", "google-vertex-anthropic", "opencode"].includes(providerID)) return true
@@ -8,9 +8,9 @@ function isClaudeProvider(providerID: string, modelID: string): boolean {
return false
}
function isOpus46(modelID: string): boolean {
function isOpusModel(modelID: string): boolean {
const normalized = normalizeModelID(modelID)
return OPUS_4_6_PATTERN.test(normalized)
return OPUS_PATTERN.test(normalized)
}
interface ChatParamsInput {
@@ -54,7 +54,7 @@ export function createAnthropicEffortHook() {
if (!isClaudeProvider(model.providerID, model.modelID)) return
if (output.options.effort !== undefined) return
const opus = isOpus46(model.modelID)
const opus = isOpusModel(model.modelID)
const clamped = clampVariant(message.variant, opus)
output.options.effort = clamped

View File

@@ -116,6 +116,21 @@ describe("createAnthropicEffortHook", () => {
//#then should normalize and inject effort
expect(output.options.effort).toBe("max")
})
it("should preserve max for other opus model IDs such as opus-4-5", async () => {
//#given another opus model id that is not 4.6
const hook = createAnthropicEffortHook()
const { input, output } = createMockParams({
modelID: "claude-opus-4-5",
})
//#when chat.params hook is called
await hook["chat.params"](input, output)
//#then max should still be treated as valid for opus family
expect(output.options.effort).toBe("max")
expect(input.message.variant).toBe("max")
})
})
describe("conditions NOT met - should skip", () => {

View File

@@ -35,4 +35,37 @@ describe("createChatParamsHandler", () => {
//#then
expect(called).toBe(true)
})
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,
}
const output = {
temperature: 0.1,
topP: 1,
topK: 1,
options: {},
}
//#when
await handler(input, output)
//#then
expect(message.variant).toBe("high")
})
})

View File

@@ -6,6 +6,10 @@ export type ChatParamsInput = {
message: { variant?: string }
}
type ChatParamsHookInput = ChatParamsInput & {
rawMessage?: Record<string, unknown>
}
export type ChatParamsOutput = {
temperature?: number
topP?: number
@@ -17,7 +21,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
function buildChatParamsInput(raw: unknown): ChatParamsInput | null {
function buildChatParamsInput(raw: unknown): ChatParamsHookInput | null {
if (!isRecord(raw)) return null
const sessionID = raw.sessionID
@@ -56,7 +60,9 @@ function buildChatParamsInput(raw: unknown): ChatParamsInput | null {
agent: { name: agentName },
model: { providerID, modelID },
provider: { id: providerId },
message: typeof variant === "string" ? { variant } : {},
message,
rawMessage: message,
...(typeof variant === "string" ? {} : {}),
}
}
@@ -69,7 +75,7 @@ function isChatParamsOutput(raw: unknown): raw is ChatParamsOutput {
}
export function createChatParamsHandler(args: {
anthropicEffort: { "chat.params"?: (input: ChatParamsInput, output: ChatParamsOutput) => Promise<void> } | null
anthropicEffort: { "chat.params"?: (input: ChatParamsHookInput, output: ChatParamsOutput) => Promise<void> } | null
}): (input: unknown, output: unknown) => Promise<void> {
return async (input, output): Promise<void> => {
const normalizedInput = buildChatParamsInput(input)