Skip to content

Commit f3f0e30

Browse files
committed
feat: add support for chat-style tools in callModel
- Add CallModelTools type to accept ToolDefinitionJson[] (chat format) - Automatically convert chat-style tools to responses format - Add tests for chat-style tools and combined usage
1 parent 7b37dc0 commit f3f0e30

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

src/funcs/callModel.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export type CallModelInput =
1212
| models.OpenResponsesInput
1313
| models.Message[];
1414

15+
/**
16+
* Tool type that accepts chat-style, responses-style, or enhanced tools
17+
*/
18+
export type CallModelTools =
19+
| EnhancedTool[]
20+
| models.ToolDefinitionJson[]
21+
| models.OpenResponsesRequest["tools"];
22+
1523
/**
1624
* Check if input is chat-style messages (Message[])
1725
*/
@@ -25,6 +33,33 @@ function isChatStyleMessages(input: CallModelInput): input is models.Message[] {
2533
return first && 'role' in first && !('type' in first);
2634
}
2735

36+
/**
37+
* Check if tools are chat-style (ToolDefinitionJson[])
38+
*/
39+
function isChatStyleTools(tools: CallModelTools): tools is models.ToolDefinitionJson[] {
40+
if (!Array.isArray(tools)) return false;
41+
if (tools.length === 0) return false;
42+
43+
const first = tools[0] as any;
44+
// Chat-style tools have nested 'function' property with 'name' inside
45+
// Enhanced tools have 'function' with 'inputSchema'
46+
// Responses-style tools have 'name' at top level
47+
return first && 'function' in first && first.function && 'name' in first.function && !('inputSchema' in first.function);
48+
}
49+
50+
/**
51+
* Convert chat-style tools to responses-style
52+
*/
53+
function convertChatToResponsesTools(tools: models.ToolDefinitionJson[]): models.OpenResponsesRequest["tools"] {
54+
return tools.map((tool): models.OpenResponsesRequestToolFunction => ({
55+
type: "function",
56+
name: tool.function.name,
57+
description: tool.function.description ?? null,
58+
strict: tool.function.strict ?? null,
59+
parameters: tool.function.parameters ?? null,
60+
}));
61+
}
62+
2863
/**
2964
* Convert chat-style messages to responses-style input
3065
*/
@@ -140,7 +175,7 @@ export function callModel(
140175
client: OpenRouterCore,
141176
request: Omit<models.OpenResponsesRequest, "stream" | "tools" | "input"> & {
142177
input?: CallModelInput;
143-
tools?: EnhancedTool[] | models.OpenResponsesRequest["tools"];
178+
tools?: CallModelTools;
144179
maxToolRounds?: MaxToolRounds;
145180
},
146181
options?: RequestOptions,
@@ -157,16 +192,27 @@ export function callModel(
157192
input: convertedInput,
158193
};
159194

160-
// Separate enhanced tools from API tools
195+
// Determine tool type and convert as needed
161196
let isEnhancedTools = false;
162-
if (tools && tools.length > 0) {
197+
let isChatTools = false;
198+
199+
if (tools && Array.isArray(tools) && tools.length > 0) {
163200
const firstTool = tools[0] as any;
164201
isEnhancedTools = "function" in firstTool && firstTool.function && "inputSchema" in firstTool.function;
202+
isChatTools = !isEnhancedTools && isChatStyleTools(tools);
165203
}
204+
166205
const enhancedTools = isEnhancedTools ? (tools as EnhancedTool[]) : undefined;
167206

168-
// Convert enhanced tools to API format if provided, otherwise use tools as-is
169-
const apiTools = enhancedTools ? convertEnhancedToolsToAPIFormat(enhancedTools) : (tools as models.OpenResponsesRequest["tools"]);
207+
// Convert tools to API format based on their type
208+
let apiTools: models.OpenResponsesRequest["tools"];
209+
if (enhancedTools) {
210+
apiTools = convertEnhancedToolsToAPIFormat(enhancedTools);
211+
} else if (isChatTools) {
212+
apiTools = convertChatToResponsesTools(tools as models.ToolDefinitionJson[]);
213+
} else {
214+
apiTools = tools as models.OpenResponsesRequest["tools"];
215+
}
170216

171217
// Build the request with converted tools
172218
const finalRequest: models.OpenResponsesRequest = {

src/sdk/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class OpenRouter extends ClientSDK {
9696
callModel(
9797
request: Omit<models.OpenResponsesRequest, "stream" | "tools" | "input"> & {
9898
input?: import("../funcs/callModel.js").CallModelInput;
99-
tools?: EnhancedTool[] | models.OpenResponsesRequest["tools"];
99+
tools?: import("../funcs/callModel.js").CallModelTools;
100100
maxToolRounds?: MaxToolRounds;
101101
},
102102
options?: RequestOptions,

tests/e2e/callModel.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,81 @@ describe("callModel E2E Tests", () => {
8989
expect(text).toBeDefined();
9090
expect(typeof text).toBe("string");
9191
});
92+
93+
it("should accept chat-style tools (ToolDefinitionJson)", async () => {
94+
const response = client.callModel({
95+
model: "meta-llama/llama-3.1-8b-instruct",
96+
input: [
97+
{
98+
role: "user",
99+
content: "What's the weather in Paris? Use the get_weather tool.",
100+
},
101+
],
102+
tools: [
103+
{
104+
type: "function" as const,
105+
function: {
106+
name: "get_weather",
107+
description: "Get weather for a location",
108+
parameters: {
109+
type: "object",
110+
properties: {
111+
location: {
112+
type: "string",
113+
description: "City name",
114+
},
115+
},
116+
required: ["location"],
117+
},
118+
},
119+
},
120+
],
121+
});
122+
123+
const toolCalls = await response.getToolCalls();
124+
125+
// Model should call the tool
126+
expect(toolCalls.length).toBeGreaterThan(0);
127+
expect(toolCalls[0].name).toBe("get_weather");
128+
expect(toolCalls[0].arguments).toBeDefined();
129+
}, 30000);
130+
131+
it("should work with chat-style messages and chat-style tools together", async () => {
132+
const response = client.callModel({
133+
model: "meta-llama/llama-3.1-8b-instruct",
134+
input: [
135+
{
136+
role: "system",
137+
content: "You are a helpful assistant. Use tools when needed.",
138+
},
139+
{
140+
role: "user",
141+
content: "Get the weather in Tokyo using the weather tool.",
142+
},
143+
] as Message[],
144+
tools: [
145+
{
146+
type: "function" as const,
147+
function: {
148+
name: "get_weather",
149+
description: "Get current weather",
150+
parameters: {
151+
type: "object",
152+
properties: {
153+
city: { type: "string" },
154+
},
155+
required: ["city"],
156+
},
157+
},
158+
},
159+
],
160+
});
161+
162+
const toolCalls = await response.getToolCalls();
163+
164+
expect(toolCalls.length).toBeGreaterThan(0);
165+
expect(toolCalls[0].name).toBe("get_weather");
166+
}, 30000);
92167
});
93168

94169
describe("response.text - Text extraction", () => {

0 commit comments

Comments
 (0)