@@ -3,233 +3,16 @@ import type { RequestOptions } from "../lib/sdks.js";
33import type { Tool , MaxToolRounds } from "../lib/tool-types.js" ;
44import type * as models from "../models/index.js" ;
55
6- import { fromClaudeMessages } from "../lib/anthropic-compat.js" ;
76import { ModelResult } from "../lib/model-result.js" ;
87import { convertToolsToAPIFormat } from "../lib/tool-executor.js" ;
98
10- /**
11- * Tool type that accepts chat-style, responses-style, or enhanced tools
12- */
13- export type CallModelTools =
14- | Tool [ ]
15- | models . ToolDefinitionJson [ ]
16- | models . OpenResponsesRequest [ "tools" ] ;
17-
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- }
359
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-
109- /**
110- * Discriminated tool type detection result
111- */
112- type ToolTypeResult =
113- | { kind : "enhanced" ; tools : Tool [ ] }
114- | { kind : "chat" ; tools : models . ToolDefinitionJson [ ] }
115- | { kind : "responses" ; tools : models . OpenResponsesRequest [ "tools" ] }
116- | { kind : "empty" } ;
117-
118- /**
119- * Type guard for tool objects with a function property containing an object
120- */
121- function hasFunctionProperty (
122- tool : unknown
123- ) : tool is { function : Record < string , unknown > } {
124- if ( typeof tool !== "object" || tool === null ) {
125- return false ;
126- }
127- if ( ! ( "function" in tool ) ) {
128- return false ;
129- }
130- const fn = ( tool as { function : unknown } ) . function ;
131- return typeof fn === "object" && fn !== null ;
132- }
133-
134- /**
135- * Type guard for responses-style tools (has name at top level, no function property)
136- */
137- function isResponsesStyleTools (
138- tools : CallModelTools
139- ) : tools is NonNullable < models . OpenResponsesRequest [ "tools" ] > {
140- if ( ! Array . isArray ( tools ) || tools . length === 0 ) {
141- return false ;
142- }
143- const firstTool = tools [ 0 ] ;
144- // Responses-style tools have 'name' at top level and no 'function' property
145- return (
146- typeof firstTool === "object" &&
147- firstTool !== null &&
148- "name" in firstTool &&
149- ! ( "function" in firstTool )
150- ) ;
151- }
152-
153- /**
154- * Type guard for enhanced tools (has function.inputSchema)
155- */
156- function isEnhancedTools ( tools : CallModelTools ) : tools is Tool [ ] {
157- if ( ! Array . isArray ( tools ) || tools . length === 0 ) {
158- return false ;
159- }
160- const firstTool = tools [ 0 ] ;
161- return hasFunctionProperty ( firstTool ) && "inputSchema" in firstTool . function ;
162- }
163-
164- /**
165- * Type guard for chat-style tools (has function.name but no inputSchema)
166- */
167- function isChatStyleTools (
168- tools : CallModelTools
169- ) : tools is models . ToolDefinitionJson [ ] {
170- if ( ! Array . isArray ( tools ) || tools . length === 0 ) {
171- return false ;
172- }
173- const firstTool = tools [ 0 ] ;
174- return (
175- hasFunctionProperty ( firstTool ) &&
176- "name" in firstTool . function &&
177- ! ( "inputSchema" in firstTool . function )
178- ) ;
179- }
180-
181- /**
182- * Detect the type of tools provided and return a discriminated result.
183- * This centralizes all tool type detection logic in one place.
184- *
185- * Tool types:
186- * - Enhanced: has function.inputSchema (our SDK tools with Zod schemas)
187- * - Chat: has function.name but no inputSchema (OpenAI chat-style)
188- * - Responses: has name at top level (OpenResponses API native format)
189- */
190- function detectToolType ( tools : CallModelTools | undefined ) : ToolTypeResult {
191- if ( ! tools || ! Array . isArray ( tools ) || tools . length === 0 ) {
192- return { kind : "empty" } ;
193- }
194-
195- if ( isEnhancedTools ( tools ) ) {
196- return { kind : "enhanced" , tools } ;
197- }
198-
199- if ( isChatStyleTools ( tools ) ) {
200- return { kind : "chat" , tools } ;
201- }
202-
203- if ( isResponsesStyleTools ( tools ) ) {
204- return { kind : "responses" , tools } ;
205- }
206-
207- // Fallback - treat as responses-style
208- return { kind : "responses" , tools : tools as models . OpenResponsesRequest [ "tools" ] } ;
209- }
210-
211- /**
212- * Convert chat-style tools to responses-style
213- */
214- function convertChatToResponsesTools (
215- tools : models . ToolDefinitionJson [ ]
216- ) : models . OpenResponsesRequest [ "tools" ] {
217- return tools . map (
218- ( tool ) : models . OpenResponsesRequestToolFunction => ( {
219- type : "function" ,
220- name : tool . function . name ,
221- description : tool . function . description ?? null ,
222- strict : tool . function . strict ?? null ,
223- parameters : tool . function . parameters ?? null ,
224- } )
225- ) ;
226- }
22710
22811/**
22912 * Get a response with multiple consumption patterns
23013 *
23114 * @remarks
232- * Creates a response using the OpenResponses API in streaming mode and returns
15+ * Creates a response using the OpenResponses API and returns
23316 * a wrapper that allows consuming the response in multiple ways:
23417 *
23518 * - `await response.getText()` - Get just the text content (tools auto-executed)
@@ -241,114 +24,27 @@ function convertChatToResponsesTools(
24124 * - `await response.getToolCalls()` - Get all tool calls from completed response
24225 * - `for await (const msg of response.getNewMessagesStream())` - Stream incremental message updates
24326 * - `for await (const event of response.getFullResponsesStream())` - Stream all events (incl. tool preliminary)
244- * - `for await (const event of response.getFullChatStream())` - Stream in chat format (incl. tool preliminary)
24527 *
24628 * All consumption patterns can be used concurrently on the same response.
24729 *
24830 * For message format conversion, use the helper functions:
24931 * - `fromChatMessages()` / `toChatMessage()` for OpenAI chat format
25032 * - `fromClaudeMessages()` / `toClaudeMessage()` for Anthropic Claude format
251- *
252- * @example
253- * ```typescript
254- * import { z } from 'zod';
255- * import { fromChatMessages, toChatMessage } from '@openrouter/sdk';
256- *
257- * // Simple text extraction
258- * const response = openrouter.callModel({
259- * model: "openai/gpt-4",
260- * input: "Hello!"
261- * });
262- * const text = await response.getText();
263- * console.log(text);
264- *
265- * // With chat-style messages (using helper)
266- * const response = openrouter.callModel({
267- * model: "openai/gpt-4",
268- * input: fromChatMessages([
269- * { role: "system", content: "You are helpful." },
270- * { role: "user", content: "Hello!" }
271- * ])
272- * });
273- * const result = await response.getResponse();
274- * const chatMessage = toChatMessage(result);
275- *
276- * // With tools (automatic execution)
277- * const response = openrouter.callModel({
278- * model: "openai/gpt-4",
279- * input: "What's the weather in SF?",
280- * tools: [{
281- * type: "function",
282- * function: {
283- * name: "get_weather",
284- * description: "Get current weather",
285- * inputSchema: z.object({
286- * location: z.string()
287- * }),
288- * outputSchema: z.object({
289- * temperature: z.number(),
290- * description: z.string()
291- * }),
292- * execute: async (params) => {
293- * return { temperature: 72, description: "Sunny" };
294- * }
295- * }
296- * }],
297- * maxToolRounds: 5, // or function: (context: TurnContext) => boolean
298- * });
299- * const text = await response.getText(); // Tools auto-executed!
300- *
301- * // Stream with preliminary results
302- * for await (const event of response.getFullChatStream()) {
303- * if (event.type === "content.delta") {
304- * process.stdout.write(event.delta);
305- * } else if (event.type === "tool.preliminary_result") {
306- * console.log("Tool progress:", event.result);
307- * }
308- * }
309- * ```
31033 */
31134export function callModel (
31235 client : OpenRouterCore ,
313- request : Omit < models . OpenResponsesRequest , "stream" | "tools" | "input" > & {
314- input ?: CallModelInput ;
315- tools ?: CallModelTools ;
36+ request : Omit < models . OpenResponsesRequest , "stream" | "tools" > & {
37+ tools ?: Tool [ ] ;
31638 maxToolRounds ?: MaxToolRounds ;
31739 } ,
31840 options ?: RequestOptions
31941) : ModelResult {
320- const { tools, maxToolRounds, input, ...restRequest } = request ;
321-
322- // Normalize input - convert Claude-style messages if needed
323- const normalizedInput = normalizeInput ( input ) ;
324-
325- const apiRequest = {
326- ...restRequest ,
327- input : normalizedInput ,
328- } ;
42+ const { tools, maxToolRounds, ...apiRequest } = request ;
32943
330- // Detect tool type using discriminated union
331- const toolType = detectToolType ( tools ) ;
33244
33345 // Convert tools to API format and extract enhanced tools if present
334- let apiTools : models . OpenResponsesRequest [ "tools" ] ;
335- let enhancedTools : Tool [ ] | undefined ;
46+ const apiTools = tools ? convertToolsToAPIFormat ( tools ) : undefined ;
33647
337- switch ( toolType . kind ) {
338- case "enhanced" :
339- enhancedTools = toolType . tools ;
340- apiTools = convertToolsToAPIFormat ( toolType . tools ) ;
341- break ;
342- case "chat" :
343- apiTools = convertChatToResponsesTools ( toolType . tools ) ;
344- break ;
345- case "responses" :
346- apiTools = toolType . tools ;
347- break ;
348- case "empty" :
349- apiTools = undefined ;
350- break ;
351- }
35248
35349 // Build the request with converted tools
35450 const finalRequest : models . OpenResponsesRequest = {
@@ -360,7 +56,7 @@ export function callModel(
36056 client,
36157 request : finalRequest ,
36258 options : options ?? { } ,
363- ... ( enhancedTools !== undefined && { tools : enhancedTools } ) ,
59+ tools : tools ?? [ ] ,
36460 ...( maxToolRounds !== undefined && { maxToolRounds } ) ,
36561 } ) ;
36662}
0 commit comments