Skip to content

Commit 21e10c8

Browse files
committed
fix: auto-convert Claude-style message format to OpenRouter format
- Add auto-detection of Claude-style messages with content block arrays - Convert Claude content blocks (text, image) to OpenRouter format - Fixes validation errors when passing Claude MessageParam directly to callModel() - Resolves CI test failures for Claude-style messages with content blocks arrays
1 parent 9a0983c commit 21e10c8

File tree

1 file changed

+123
-2
lines changed

1 file changed

+123
-2
lines changed

src/funcs/call-model.ts

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,115 @@ import type * as models from "../models/index.js";
66
import { ModelResult } from "../lib/model-result.js";
77
import { convertToolsToAPIFormat } from "../lib/tool-executor.js";
88

9+
/**
10+
* Checks if a message looks like a Claude-style message
11+
*/
12+
function isClaudeStyleMessage(msg: any): msg is models.ClaudeMessageParam {
13+
if (!msg || typeof msg !== 'object') return false;
14+
15+
// Check if it has a role field that's user or assistant
16+
const role = msg.role;
17+
if (role !== 'user' && role !== 'assistant') return false;
18+
19+
// Check if content is an array with Claude-style content blocks
20+
if (Array.isArray(msg.content)) {
21+
return msg.content.some((block: any) =>
22+
block &&
23+
typeof block === 'object' &&
24+
block.type &&
25+
// Claude content block types (not OpenRouter types)
26+
(block.type === 'text' || block.type === 'image' || block.type === 'tool_use' || block.type === 'tool_result')
27+
);
28+
}
29+
30+
return false;
31+
}
32+
33+
/**
34+
* Converts Claude-style content blocks to OpenRouter format
35+
*/
36+
function convertClaudeContentBlock(
37+
block: models.ClaudeContentBlockParam
38+
): models.ResponseInputText | models.ResponseInputImage | null {
39+
if (!block || typeof block !== 'object' || !('type' in block)) {
40+
return null;
41+
}
42+
43+
switch (block.type) {
44+
case 'text': {
45+
const textBlock = block as models.ClaudeTextBlockParam;
46+
return {
47+
type: 'input_text',
48+
text: textBlock.text,
49+
};
50+
}
51+
case 'image': {
52+
const imageBlock = block as models.ClaudeImageBlockParam;
53+
if (imageBlock.source.type === 'url') {
54+
return {
55+
type: 'input_image',
56+
detail: 'auto',
57+
imageUrl: imageBlock.source.url,
58+
};
59+
} else if (imageBlock.source.type === 'base64') {
60+
const dataUri = `data:${imageBlock.source.media_type};base64,${imageBlock.source.data}`;
61+
return {
62+
type: 'input_image',
63+
detail: 'auto',
64+
imageUrl: dataUri,
65+
};
66+
}
67+
return null;
68+
}
69+
case 'tool_use':
70+
case 'tool_result':
71+
// tool_use and tool_result are not handled here as they map to different input types
72+
return null;
73+
default:
74+
return null;
75+
}
76+
}
77+
78+
/**
79+
* Converts a Claude-style message to OpenRouter EasyInputMessage format
80+
*/
81+
function convertClaudeMessage(msg: models.ClaudeMessageParam): models.OpenResponsesEasyInputMessage {
82+
const { role, content } = msg;
83+
84+
if (typeof content === 'string') {
85+
return {
86+
role: role === 'user' ? 'user' : 'assistant',
87+
content,
88+
};
89+
}
990

91+
// Convert array of content blocks
92+
const convertedBlocks: (models.ResponseInputText | models.ResponseInputImage)[] = [];
93+
for (const block of content) {
94+
const converted = convertClaudeContentBlock(block);
95+
if (converted) {
96+
convertedBlocks.push(converted);
97+
}
98+
}
99+
100+
// If all blocks were text, concatenate them into a string
101+
const allText = convertedBlocks.every(b => b.type === 'input_text');
102+
if (allText) {
103+
const text = convertedBlocks
104+
.map(b => (b as models.ResponseInputText).text)
105+
.join('');
106+
return {
107+
role: role === 'user' ? 'user' : 'assistant',
108+
content: text,
109+
};
110+
}
111+
112+
// Otherwise, return as array
113+
return {
114+
role: role === 'user' ? 'user' : 'assistant',
115+
content: convertedBlocks,
116+
};
117+
}
10118

11119
/**
12120
* Get a response with multiple consumption patterns
@@ -41,14 +149,27 @@ export function callModel(
41149
): ModelResult {
42150
const { tools, maxToolRounds, ...apiRequest } = request;
43151

152+
// Auto-convert Claude-style messages if detected
153+
let processedInput = apiRequest.input;
154+
if (Array.isArray(apiRequest.input)) {
155+
const hasClaudeMessages = apiRequest.input.some(isClaudeStyleMessage);
156+
if (hasClaudeMessages) {
157+
processedInput = apiRequest.input.map((msg: any) => {
158+
if (isClaudeStyleMessage(msg)) {
159+
return convertClaudeMessage(msg);
160+
}
161+
return msg;
162+
});
163+
}
164+
}
44165

45166
// Convert tools to API format and extract enhanced tools if present
46167
const apiTools = tools ? convertToolsToAPIFormat(tools) : undefined;
47168

48-
49-
// Build the request with converted tools
169+
// Build the request with converted tools and input
50170
const finalRequest: models.OpenResponsesRequest = {
51171
...apiRequest,
172+
...(processedInput !== undefined && { input: processedInput }),
52173
...(apiTools !== undefined && { tools: apiTools }),
53174
};
54175

0 commit comments

Comments
 (0)