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
5 changes: 5 additions & 0 deletions .changeset/tool-only-null-content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openrouter/ai-sdk-provider': patch
---

Send `content: null` instead of `content: ""` for assistant messages that contain only tool calls. Fixes AWS Bedrock Nova rejecting requests with "The text field in the ContentBlock object is blank."
97 changes: 96 additions & 1 deletion src/chat/convert-to-openrouter-chat-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2831,6 +2831,101 @@ describe('tool messages', () => {
});
});

describe('tool-only assistant messages send null content (issue #443)', () => {
it('should send content: null for assistant messages with only tool calls', () => {
const result = convertToOpenRouterChatMessages([
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'get_weather',
input: { location: 'San Francisco' },
},
],
},
]);

expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call-1',
type: 'function',
function: {
name: 'get_weather',
arguments: expect.any(String),
},
},
],
});
});

it('should send content: null for assistant messages with multiple tool calls and no text', () => {
const result = convertToOpenRouterChatMessages([
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'get_weather',
input: { location: 'SF' },
},
{
type: 'tool-call',
toolCallId: 'call-2',
toolName: 'get_time',
input: {},
},
],
},
]);

expect(result).toHaveLength(1);
expect(result[0]?.content).toBeNull();
const msg = result[0] as { tool_calls?: unknown[] };
expect(msg.tool_calls).toHaveLength(2);
});

it('should preserve text content when assistant message has both text and tool calls', () => {
const result = convertToOpenRouterChatMessages([
{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me check the weather.' },
{
type: 'tool-call',
toolCallId: 'call-1',
toolName: 'get_weather',
input: { location: 'San Francisco' },
},
],
},
]);

expect(result).toHaveLength(1);
expect(result[0]?.content).toBe('Let me check the weather.');
const msg = result[0] as { tool_calls?: unknown[] };
expect(msg.tool_calls).toHaveLength(1);
});

it('should preserve text content for text-only assistant messages', () => {
const result = convertToOpenRouterChatMessages([
{
role: 'assistant',
content: [{ type: 'text', text: 'Hello!' }],
},
]);

expect(result).toHaveLength(1);
expect(result[0]?.content).toBe('Hello!');
});
});

describe('deterministic tool call argument serialization', () => {
it('should produce identical serialized arguments regardless of key insertion order', () => {
// Simulate the same tool call arguments with different key insertion orders,
Expand Down Expand Up @@ -2877,7 +2972,7 @@ describe('deterministic tool call argument serialization', () => {
expect(resultA).toEqual([
{
role: 'assistant',
content: '',
content: null,
tool_calls: [
{
id: 'call-1',
Expand Down
2 changes: 1 addition & 1 deletion src/chat/convert-to-openrouter-chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export function convertToOpenRouterChatMessages(

messages.push({
role: 'assistant',
content: text,
content: text || null,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
reasoning: effectiveReasoning,
reasoning_details: finalReasoningDetails,
Expand Down
Loading