fix: model-aware assistant message stripping for Claude via Copilot

The Copilot proxy for Claude enforces Anthropic's rule that conversations
must end with a user message. For Claude models, strip any trailing
assistant message without tool_calls (including non-empty ones). For
other models, only strip truly empty assistant messages.

This fixes the 'assistant message prefill not supported' error seen
in Telegram and other channel adapters when using Claude via Copilot,
without causing infinite agent loops for other models.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dmitry Butko
2026-04-09 21:07:14 +10:00
co-authored by Copilot
parent dc6119d2cc
commit 64b1ec5a7e
+20 -12
View File
@@ -262,19 +262,27 @@ struct OaiUsage {
completion_tokens: u64,
}
/// Strip trailing empty assistant messages without tool calls.
/// Some API proxies (e.g. Copilot proxying Claude) reject conversations
/// ending with an empty assistant message as "assistant message prefill".
/// Only strips messages with no content and no tool_calls — non-empty
/// assistant messages are kept to avoid breaking the agent loop.
fn strip_trailing_empty_assistant(messages: &mut Vec<OaiMessage>) {
/// Strip trailing assistant messages that would trigger "prefill not supported"
/// errors on the Copilot proxy for Claude models.
/// Only strips assistant messages that have no tool_calls (tool call messages
/// are part of the protocol and must stay). Checks the model name to only
/// apply for Claude models which enforce this restriction.
fn strip_trailing_empty_assistant(messages: &mut Vec<OaiMessage>, model: &str) {
let is_claude = model.contains("claude");
while messages.last().map_or(false, |m| {
m.role == "assistant"
&& m.tool_calls.is_none()
&& match &m.content {
None => true,
Some(OaiMessageContent::Text(t)) => t.trim().is_empty(),
_ => false,
&& if is_claude {
// Claude via Copilot: strip any trailing assistant without tool_calls
true
} else {
// Other models: only strip truly empty messages
match &m.content {
None => true,
Some(OaiMessageContent::Text(t)) => t.trim().is_empty(),
_ => false,
}
}
}) {
messages.pop();
@@ -436,7 +444,7 @@ impl LlmDriver for OpenAIDriver {
}
}
strip_trailing_empty_assistant(&mut oai_messages);
strip_trailing_empty_assistant(&mut oai_messages, &request.model);
let oai_tools: Vec<OaiTool> = request
.tools
@@ -895,7 +903,7 @@ impl LlmDriver for OpenAIDriver {
}
}
strip_trailing_empty_assistant(&mut oai_messages);
strip_trailing_empty_assistant(&mut oai_messages, &request.model);
let oai_tools: Vec<OaiTool> = request
.tools