Skip to content

Commit 64bb2b3

Browse files
authored
Fix whitespace-only text block from orphaned CacheBreakpoint (#4839)
When a CacheBreakpoint had no preceding content block to attach cache_control to, rawContentToAnthropicContent created a placeholder { type: 'text', text: ' ' } block. The Anthropic API rejects whitespace-only text blocks with 'text content block must contain non-whitespace text'. This happens in long conversations after prompt-tsx prunes content to fit the token budget, leaving a CacheBreakpoint with no real content before it. Fix: defer the cache_control to the next cacheable content block. If no subsequent block exists, silently drop it (nothing to cache anyway). Fixes microsoft/vscode#305956
1 parent 00df316 commit 64bb2b3

2 files changed

Lines changed: 42 additions & 8 deletions

File tree

src/platform/endpoint/node/messagesApi.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,8 @@ function tryParseToolReferences(content: ContentBlockParam[], validToolNames?: S
381381

382382
function rawContentToAnthropicContent(content: readonly Raw.ChatCompletionContentPart[]): ContentBlockParam[] {
383383
const convertedContent: ContentBlockParam[] = [];
384+
// Track pending cache_control that couldn't be attached to a preceding block
385+
let pendingCacheControl = false;
384386

385387
for (const part of content) {
386388
switch (part.type) {
@@ -419,12 +421,12 @@ function rawContentToAnthropicContent(content: readonly Raw.ChatCompletionConten
419421
if (previousBlock && contentBlockSupportsCacheControl(previousBlock)) {
420422
previousBlock.cache_control = { type: 'ephemeral' };
421423
} else {
422-
// Empty string is invalid
423-
convertedContent.push({
424-
type: 'text',
425-
text: ' ',
426-
cache_control: { type: 'ephemeral' }
427-
});
424+
// No preceding block to attach to — defer until the next
425+
// cacheable content block is added, or silently drop it.
426+
// Previously this created a whitespace-only text block which
427+
// the Anthropic API rejects with "text content block must
428+
// contain non-whitespace text".
429+
pendingCacheControl = true;
428430
}
429431
break;
430432
}
@@ -467,6 +469,15 @@ function rawContentToAnthropicContent(content: readonly Raw.ChatCompletionConten
467469
break;
468470
}
469471
}
472+
473+
// Attach any pending cache_control to the block we just added
474+
if (pendingCacheControl && convertedContent.length > 0) {
475+
const lastBlock = convertedContent.at(-1)!;
476+
if (contentBlockSupportsCacheControl(lastBlock)) {
477+
lastBlock.cache_control = { type: 'ephemeral' };
478+
pendingCacheControl = false;
479+
}
480+
}
470481
}
471482

472483
return convertedContent;

src/platform/endpoint/test/node/messagesApi.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,33 @@ suite('rawMessagesToMessagesAPI', function () {
370370

371371
const toolResult = findToolResult(result.messages);
372372
expect(toolResult).toBeDefined();
373-
expect(toolResult!.cache_control).toEqual({ type: 'ephemeral' });
374-
// The dummy whitespace-only text block should be filtered out
373+
// Orphaned cache breakpoint with no content to attach to is silently dropped
374+
expect(toolResult!.cache_control).toBeUndefined();
375375
expect(toolResult!.content).toBeUndefined();
376376
});
377+
378+
test('cache breakpoint before content defers cache_control to next block', function () {
379+
const messages: Raw.ChatMessage[] = [
380+
{
381+
role: Raw.ChatRole.User,
382+
content: [
383+
{ type: Raw.ChatCompletionContentPartKind.CacheBreakpoint, cacheType: 'ephemeral' },
384+
{ type: Raw.ChatCompletionContentPartKind.Text, text: 'hello world' },
385+
],
386+
},
387+
];
388+
389+
const result = rawMessagesToMessagesAPI(messages);
390+
391+
expect(result.messages).toHaveLength(1);
392+
const content = assertContentArray(result.messages[0].content);
393+
expect(content).toHaveLength(1);
394+
expect(content[0]).toEqual({
395+
type: 'text',
396+
text: 'hello world',
397+
cache_control: { type: 'ephemeral' },
398+
});
399+
});
377400
});
378401

379402
suite('addToolsAndSystemCacheControl', function () {

0 commit comments

Comments
 (0)