Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 44 additions & 16 deletions src/llm/anthropic/llm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2807,6 +2807,22 @@ describe('Claude assistant prefill compatibility', () => {
'global.anthropic.claude-opus-4-6-v1:0',
'anthropic/claude-sonnet-4.6',
'anthropic/claude-sonnet-4.12',
'claude-sonnet-4-6[1m]',
'claude-opus-5',
'claude-sonnet-5',
'claude-sonnet-5-5',
'claude-opus-5-5',
'claude-haiku-5-5',
'claude-haiku-5.5',
'claude-fable-5',
'claude-fable-5-1',
'claude-mythos-5',
'claude-opus-6',
'claude-opus-5-20260301',
'claude-sonnet-5-5@20260801',
'anthropic.claude-haiku-5-5',
'global.anthropic.claude-opus-5-5-v1:0',
'us.anthropic.claude-sonnet-5-v1:0',
])('detects %s as not supporting assistant prefill', (model) => {
expect(modelDisallowsAssistantPrefill(model)).toBe(true);
});
Expand All @@ -2815,29 +2831,41 @@ describe('Claude assistant prefill compatibility', () => {
'claude-sonnet-4-5-20250929',
'claude-opus-4-20250514',
'anthropic.claude-opus-4-20250514-v1:0',
'claude-sonnet-4@20250514',
'claude-opus-4-1-20250805',
'claude-haiku-4-5',
'claude-haiku-4-5-20251001',
'claude-3-7-sonnet-20250219',
'claude-3-5-haiku-latest',
'anthropic.claude-3-haiku-20240307-v1:0',
'gpt-5.4',
'',
undefined,
])('leaves %s prefill support unchanged', (model) => {
expect(modelDisallowsAssistantPrefill(model)).toBe(false);
});

test('strips trailing assistant messages for Claude 4.6+ requests', () => {
const request = {
model: 'claude-opus-4-6',
max_tokens: 100,
messages: [
{ role: 'user' as const, content: 'What changed?' },
{ role: 'assistant' as const, content: 'Draft prefill' },
{ role: 'assistant' as const, content: 'Another prefill' },
],
};
test.each(['claude-opus-4-6', 'claude-haiku-5-5', 'claude-fable-5-1'])(
'strips trailing assistant messages for %s requests',
(model) => {
const request = {
model,
max_tokens: 100,
messages: [
{ role: 'user' as const, content: 'What changed?' },
{ role: 'assistant' as const, content: 'Draft prefill' },
{ role: 'assistant' as const, content: 'Another prefill' },
],
};

const sanitized = stripUnsupportedAssistantPrefill(request);
const sanitized = stripUnsupportedAssistantPrefill(request);

expect(sanitized).not.toBe(request);
expect(sanitized.messages).toEqual([
{ role: 'user', content: 'What changed?' },
]);
});
expect(sanitized).not.toBe(request);
expect(sanitized.messages).toEqual([
{ role: 'user', content: 'What changed?' },
]);
}
);

test('does not strip assistant messages for older Claude models', () => {
const request = {
Expand Down
32 changes: 16 additions & 16 deletions src/llm/anthropic/utils/message_inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
cloneToolMessageWithContent,
serializeStructuredValueBounded,
} from '@/utils/toolContent';
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
import { isReasoningContentBlock } from '@/messages/reasoningTypes';
import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
import { Constants } from '@/common';

type StandardTextBlock = Data.StandardTextBlock;
Expand All @@ -56,10 +56,14 @@
};

const ANTHROPIC_EMPTY_TEXT_PLACEHOLDER = '_';
const CLAUDE_4_RELEASE_DATE_MODEL_PATTERN =
/claude-(?:opus|sonnet|haiku)-4-\d{8}(?:[-.@]|$)/i;
const CLAUDE_4_MINOR_MODEL_PATTERN =
/claude-(?:opus|sonnet|haiku)-4[-.](\d+)(?:[-.@]|$)/i;
/**
* Name-first Claude ids (`claude-sonnet-4-6`, `claude-haiku-5.5`, Bedrock and
* Vertex forms included). The minor is one or two digits so an 8-digit release
* date (`claude-opus-4-20250514`) reads as major only. Number-first ids
* (`claude-3-5-sonnet`) predate 4.6 and never match.
*/
const CLAUDE_FAMILY_VERSION_PATTERN =
/claude-(?:opus|sonnet|haiku|fable|mythos)-(\d+)(?:[-.](\d{1,2}))?(?!\d)/i;

function _formatImage(imageUrl: string) {
const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
Expand Down Expand Up @@ -164,7 +168,7 @@
}
let cacheControl: unknown;
const stripped = content.map((block) => {
if (typeof block !== 'object' || block == null || isProxy(block)) {

Check warning on line 171 in src/llm/anthropic/utils/message_inputs.ts

View workflow job for this annotation

GitHub Actions / validate / lint

Unnecessary conditional, the types have no overlap
return block;
}
try {
Expand Down Expand Up @@ -207,7 +211,7 @@
}

const block = content[0];
if (typeof block !== 'object' || block == null || isProxy(block)) {

Check warning on line 214 in src/llm/anthropic/utils/message_inputs.ts

View workflow job for this annotation

GitHub Actions / validate / lint

Unnecessary conditional, the types have no overlap
return { content, cacheControl: undefined, isError: undefined };
}

Expand Down Expand Up @@ -1047,10 +1051,7 @@
name: correspondingToolCall.name,
input: functionCallPart.functionCall.args,
};
} else if (
isAIMessage(message) &&
isReasoningContentBlock(contentPart)
) {
} else if (isAIMessage(message) && isReasoningContentBlock(contentPart)) {
// Foreign reasoning on an ASSISTANT turn (Bedrock `reasoning_content`,
// Google `reasoning`, LibreChat `think`) carries provider-specific
// signatures Anthropic cannot validate; drop it so a cross-provider
Expand Down Expand Up @@ -1217,17 +1218,16 @@
} as AnthropicMessageCreateParams;
}

/** Claude 4.6 and every later release (5.x, Fable, Mythos) reject a trailing assistant prefill. */
export function modelDisallowsAssistantPrefill(model?: string): boolean {
const modelId = model ?? '';
if (CLAUDE_4_RELEASE_DATE_MODEL_PATTERN.test(modelId)) {
return false;
}

const match = CLAUDE_4_MINOR_MODEL_PATTERN.exec(modelId);
const match = CLAUDE_FAMILY_VERSION_PATTERN.exec(model ?? '');
if (!match) {
return false;
}
return Number(match[1]) >= 6;
const [, majorText, minorText = '0'] = match;
const major = Number(majorText);
const minor = Number(minorText);
return major > 4 || (major === 4 && minor >= 6);
}

function messagesHaveCacheControl(
Expand Down
Loading