fix(plugin): constrain Anthropic prefill guard
Tighten the assistant-tail repair after review so only the Anthropic provider is treated as prefill-rejecting, and so assistant-tail model metadata is evaluated independently from the last user model. Add regression coverage for an allowed user model followed by a rejecting Anthropic assistant tail, plus a non-Anthropic provider carrying a Claude-looking model id. Plan: plans/fix-anthropic-assistant-prefill-tail.md
This commit is contained in:
@@ -286,6 +286,48 @@ describe("createMessagesTransformHandler", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("#given the assistant tail identifies a rejecting Anthropic model after an allowed user model #when messages transform runs #then it appends a synthetic user recovery turn", async () => {
|
||||||
|
//#given
|
||||||
|
const messages: TestMessage[] = [
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
id: "msg_user_allowed_then_rejecting_assistant",
|
||||||
|
role: "user",
|
||||||
|
sessionID: "ses_allowed_then_rejecting_assistant",
|
||||||
|
agent: "sisyphus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5.4" },
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: "continue" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
id: "msg_assistant_rejecting_metadata",
|
||||||
|
role: "assistant",
|
||||||
|
sessionID: "ses_allowed_then_rejecting_assistant",
|
||||||
|
model: { providerID: "anthropic", modelID: "claude-opus-4-6" },
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: "done" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await runHandler(makeHooks({}), messages)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(messages).toHaveLength(3)
|
||||||
|
expect(messages.at(-1)?.info).toMatchObject({
|
||||||
|
role: "user",
|
||||||
|
sessionID: "ses_allowed_then_rejecting_assistant",
|
||||||
|
agent: "sisyphus",
|
||||||
|
model: { providerID: "openai", modelID: "gpt-5.4" },
|
||||||
|
})
|
||||||
|
expect(messages.at(-1)?.parts[0]).toMatchObject({
|
||||||
|
type: "text",
|
||||||
|
text: "[internal] Continue from the previous assistant state.",
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("#given rejecting model metadata uses direct provider and model fields #when messages transform runs #then it appends a synthetic user recovery turn", async () => {
|
it("#given rejecting model metadata uses direct provider and model fields #when messages transform runs #then it appends a synthetic user recovery turn", async () => {
|
||||||
//#given
|
//#given
|
||||||
const messages: TestMessage[] = [
|
const messages: TestMessage[] = [
|
||||||
@@ -353,6 +395,13 @@ describe("createMessagesTransformHandler", () => {
|
|||||||
name: "missing model",
|
name: "missing model",
|
||||||
userInfo: { role: "user" },
|
userInfo: { role: "user" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "non-anthropic provider",
|
||||||
|
userInfo: {
|
||||||
|
role: "user",
|
||||||
|
model: { providerID: "opencode", modelID: "claude-opus-4-7" },
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const scenario of scenarios) {
|
for (const scenario of scenarios) {
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { normalizeModelID } from "../shared/model-normalization"
|
|||||||
import type { CreatedHooks } from "../create-hooks"
|
import type { CreatedHooks } from "../create-hooks"
|
||||||
|
|
||||||
const ASSISTANT_PREFILL_RECOVERY_TEXT = "[internal] Continue from the previous assistant state."
|
const ASSISTANT_PREFILL_RECOVERY_TEXT = "[internal] Continue from the previous assistant state."
|
||||||
const ASSISTANT_PREFILL_UNSUPPORTED_PROVIDERS = new Set([
|
const ASSISTANT_PREFILL_UNSUPPORTED_PROVIDER = "anthropic"
|
||||||
"anthropic",
|
|
||||||
"google-vertex-anthropic",
|
|
||||||
"opencode",
|
|
||||||
])
|
|
||||||
const ASSISTANT_PREFILL_UNSUPPORTED_MODEL_PREFIXES = [
|
const ASSISTANT_PREFILL_UNSUPPORTED_MODEL_PREFIXES = [
|
||||||
"claude-opus-4-7",
|
"claude-opus-4-7",
|
||||||
"claude-opus-4-6",
|
"claude-opus-4-6",
|
||||||
@@ -98,7 +94,7 @@ function shouldRepairAssistantPrefillForModel(model: ModelIdentifier | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
const providerID = model.providerID.toLowerCase()
|
const providerID = model.providerID.toLowerCase()
|
||||||
if (!ASSISTANT_PREFILL_UNSUPPORTED_PROVIDERS.has(providerID)) {
|
if (providerID !== ASSISTANT_PREFILL_UNSUPPORTED_PROVIDER) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +158,8 @@ function ensureUserTurnAfterAssistantTail(output: MessagesTransformOutput): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldRepairAssistantTail = hasInternalContinuationTrigger(output.messages) ||
|
const shouldRepairAssistantTail = hasInternalContinuationTrigger(output.messages) ||
|
||||||
shouldRepairAssistantPrefillForModel(findLastUserModel(output.messages) ?? readModelIdentifier(lastMessage.info))
|
shouldRepairAssistantPrefillForModel(findLastUserModel(output.messages)) ||
|
||||||
|
shouldRepairAssistantPrefillForModel(readModelIdentifier(lastMessage.info))
|
||||||
if (!shouldRepairAssistantTail) {
|
if (!shouldRepairAssistantTail) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user