Skip to content

Commit 1c07bc1

Browse files
committed
cleanup
1 parent 3e7f10c commit 1c07bc1

File tree

2 files changed

+7
-312
lines changed

2 files changed

+7
-312
lines changed

examples/callModel.example.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ dotenv.config();
1212
*/
1313

1414
import { OpenRouter } from "../src/index.js";
15-
import { Message, OpenResponsesEasyInputMessage } from "../src/models";
15+
import type { OpenResponsesEasyInputMessage } from "../src/models";
1616

17-
import { MessageParam as AnthropicClaudeMessage } from "@anthropic-ai/sdk/resources/messages";
1817

1918
const openRouter = new OpenRouter({
2019
apiKey: process.env["OPENROUTER_API_KEY"] ?? "",

src/funcs/call-model.ts

Lines changed: 6 additions & 310 deletions
Original file line numberDiff line numberDiff line change
@@ -3,233 +3,16 @@ 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";
76
import { ModelResult } from "../lib/model-result.js";
87
import { 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
*/
31134
export 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

Comments
 (0)