Skip to content

Commit 3e7f10c

Browse files
mattappersonclaude
andcommitted
feat: auto-detect and convert Claude-style messages in callModel input
The callModel function now accepts ClaudeMessageParam[] directly and automatically converts them to OpenResponses format using fromClaudeMessages(). This enables passing Claude-style messages without manual conversion: - ClaudeMessageParam[] with string content - ClaudeMessageParam[] with content blocks (text, tool_use, tool_result, image) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 759ce10 commit 3e7f10c

File tree

2 files changed

+99
-5
lines changed

2 files changed

+99
-5
lines changed

src/funcs/call-model.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { RequestOptions } from "../lib/sdks.js";
33
import type { Tool, MaxToolRounds } from "../lib/tool-types.js";
44
import type * as models from "../models/index.js";
55

6+
import { fromClaudeMessages } from "../lib/anthropic-compat.js";
67
import { ModelResult } from "../lib/model-result.js";
78
import { convertToolsToAPIFormat } from "../lib/tool-executor.js";
89

@@ -14,6 +15,97 @@ export type CallModelTools =
1415
| models.ToolDefinitionJson[]
1516
| models.OpenResponsesRequest["tools"];
1617

18+
/**
19+
* Input type that accepts OpenResponses input or Claude-style messages
20+
*/
21+
export type CallModelInput =
22+
| models.OpenResponsesInput
23+
| models.ClaudeMessageParam[];
24+
25+
/**
26+
* Type guard for Claude-style messages (ClaudeMessageParam[])
27+
* Claude messages have role: "user" | "assistant" and content as string or content blocks
28+
*/
29+
function isClaudeStyleInput(
30+
input: CallModelInput | undefined
31+
): input is models.ClaudeMessageParam[] {
32+
if (!input || !Array.isArray(input) || input.length === 0) {
33+
return false;
34+
}
35+
36+
const firstItem = input[0];
37+
38+
// Claude messages have role: "user" | "assistant"
39+
// and content as string or array of content blocks with type: "text" | "tool_use" | etc.
40+
if (
41+
typeof firstItem !== "object" ||
42+
firstItem === null ||
43+
!("role" in firstItem) ||
44+
!("content" in firstItem)
45+
) {
46+
return false;
47+
}
48+
49+
const role = firstItem.role;
50+
const content = firstItem.content;
51+
52+
// Check if it's a Claude-style role (only "user" or "assistant")
53+
if (role !== "user" && role !== "assistant") {
54+
return false;
55+
}
56+
57+
// If content is an array, check if it has Claude-style content blocks
58+
if (Array.isArray(content)) {
59+
const firstBlock = content[0];
60+
if (
61+
firstBlock &&
62+
typeof firstBlock === "object" &&
63+
"type" in firstBlock &&
64+
(firstBlock.type === "text" ||
65+
firstBlock.type === "tool_use" ||
66+
firstBlock.type === "tool_result" ||
67+
firstBlock.type === "image")
68+
) {
69+
return true;
70+
}
71+
}
72+
73+
// If content is a string, we need to distinguish from OpenResponsesEasyInputMessage
74+
// OpenResponsesEasyInputMessage also has role and content as string
75+
// But Claude uses "user" | "assistant" while OpenResponses uses role enums
76+
// The key difference is that OpenResponsesEasyInputMessage role is an enum value like "user"
77+
// but that's the same...
78+
//
79+
// We need another heuristic: if the input doesn't have other OpenResponses fields
80+
// like "type", "id", etc., it's likely Claude-style
81+
if (typeof content === "string") {
82+
// If item has no "type" field and role is strictly "user" or "assistant"
83+
// it's likely a Claude-style message
84+
// OpenResponses items typically have a "type" field (except for OpenResponsesEasyInputMessage)
85+
// This is ambiguous, so we'll be conservative and check if it matches OpenResponses format first
86+
return !("type" in firstItem);
87+
}
88+
89+
return false;
90+
}
91+
92+
/**
93+
* Convert input to OpenResponsesInput format if needed
94+
*/
95+
function normalizeInput(
96+
input: CallModelInput | undefined
97+
): models.OpenResponsesInput | undefined {
98+
if (input === undefined) {
99+
return undefined;
100+
}
101+
102+
if (isClaudeStyleInput(input)) {
103+
return fromClaudeMessages(input);
104+
}
105+
106+
return input;
107+
}
108+
17109
/**
18110
* Discriminated tool type detection result
19111
*/
@@ -219,17 +311,20 @@ function convertChatToResponsesTools(
219311
export function callModel(
220312
client: OpenRouterCore,
221313
request: Omit<models.OpenResponsesRequest, "stream" | "tools" | "input"> & {
222-
input?: models.OpenResponsesInput;
314+
input?: CallModelInput;
223315
tools?: CallModelTools;
224316
maxToolRounds?: MaxToolRounds;
225317
},
226318
options?: RequestOptions
227319
): ModelResult {
228320
const { tools, maxToolRounds, input, ...restRequest } = request;
229321

322+
// Normalize input - convert Claude-style messages if needed
323+
const normalizedInput = normalizeInput(input);
324+
230325
const apiRequest = {
231326
...restRequest,
232-
input,
327+
input: normalizedInput,
233328
};
234329

235330
// Detect tool type using discriminated union

src/sdk/sdk.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ import { OAuth } from "./oauth.js";
1717
import { ParametersT } from "./parameters.js";
1818
import { Providers } from "./providers.js";
1919
// #region imports
20-
import { callModel as callModelFunc } from "../funcs/call-model.js";
20+
import { callModel as callModelFunc, type CallModelInput } from "../funcs/call-model.js";
2121
import type { ModelResult } from "../lib/model-result.js";
2222
import type { RequestOptions } from "../lib/sdks.js";
2323
import { type MaxToolRounds, Tool, ToolType } from "../lib/tool-types.js";
2424
import type { OpenResponsesRequest } from "../models/openresponsesrequest.js";
25-
import type { OpenResponsesInput } from "../models/openresponsesinput.js";
2625

2726
export { ToolType };
2827
export type { MaxToolRounds };
@@ -97,7 +96,7 @@ export class OpenRouter extends ClientSDK {
9796
// #region sdk-class-body
9897
callModel(
9998
request: Omit<OpenResponsesRequest, "stream" | "tools" | "input"> & {
100-
input?: OpenResponsesInput;
99+
input?: CallModelInput;
101100
tools?: Tool[];
102101
maxToolRounds?: MaxToolRounds;
103102
},

0 commit comments

Comments
 (0)