Skip to content

Commit 3f6d98f

Browse files
authored
[ai] Align toolsToModelTools with core AI SDK's prepareToolsAndToolChoice (#1544)
1 parent a12c0e8 commit 3f6d98f

3 files changed

Lines changed: 155 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/ai": patch
3+
---
4+
5+
Forward `strict`, `inputExamples`, and `providerOptions` tool properties to language model providers, and handle `type: 'dynamic'` tools

packages/ai/src/agent/tools-to-model-tools.test.ts

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('toolsToModelTools', () => {
88
const tools = {
99
weather: tool({
1010
description: 'Get the weather',
11-
parameters: z.object({ city: z.string() }),
11+
inputSchema: z.object({ city: z.string() }),
1212
execute: async ({ city }) => `Weather in ${city}: sunny`,
1313
}),
1414
};
@@ -61,7 +61,7 @@ describe('toolsToModelTools', () => {
6161
const tools = {
6262
weather: tool({
6363
description: 'Get the weather',
64-
parameters: z.object({ city: z.string() }),
64+
inputSchema: z.object({ city: z.string() }),
6565
execute: async ({ city }) => `Weather in ${city}: sunny`,
6666
}),
6767
webSearch: providerTool,
@@ -103,4 +103,119 @@ describe('toolsToModelTools', () => {
103103
args: {},
104104
});
105105
});
106+
107+
it('forwards strict: true', async () => {
108+
const tools = {
109+
weather: tool({
110+
description: 'Get weather',
111+
inputSchema: z.object({ location: z.string() }),
112+
execute: async () => 'sunny',
113+
strict: true,
114+
}),
115+
};
116+
117+
const result = await toolsToModelTools(tools);
118+
119+
expect(result[0]).toMatchObject({ strict: true });
120+
});
121+
122+
it('forwards strict: false', async () => {
123+
const tools = {
124+
weather: tool({
125+
description: 'Get weather',
126+
inputSchema: z.object({ location: z.string() }),
127+
execute: async () => 'sunny',
128+
strict: false,
129+
}),
130+
};
131+
132+
const result = await toolsToModelTools(tools);
133+
134+
expect(result[0]).toMatchObject({ strict: false });
135+
});
136+
137+
it('omits strict key when not set', async () => {
138+
const tools = {
139+
weather: tool({
140+
description: 'Get weather',
141+
inputSchema: z.object({ location: z.string() }),
142+
execute: async () => 'sunny',
143+
}),
144+
};
145+
146+
const result = await toolsToModelTools(tools);
147+
148+
expect(result[0]).not.toHaveProperty('strict');
149+
});
150+
151+
it('forwards inputExamples', async () => {
152+
const examples = [{ input: { location: 'Tokyo' } }];
153+
const tools = {
154+
weather: tool({
155+
description: 'Get weather',
156+
inputSchema: z.object({ location: z.string() }),
157+
execute: async () => 'sunny',
158+
inputExamples: examples,
159+
}),
160+
};
161+
162+
const result = await toolsToModelTools(tools);
163+
164+
expect(result[0]).toMatchObject({ inputExamples: examples });
165+
});
166+
167+
it('omits inputExamples key when not set', async () => {
168+
const tools = {
169+
weather: tool({
170+
description: 'Get weather',
171+
inputSchema: z.object({ location: z.string() }),
172+
execute: async () => 'sunny',
173+
}),
174+
};
175+
176+
const result = await toolsToModelTools(tools);
177+
178+
expect(result[0]).not.toHaveProperty('inputExamples');
179+
});
180+
181+
it('forwards providerOptions', async () => {
182+
const providerOptions = { openai: { parallel_tool_calls: false } };
183+
const tools = {
184+
weather: tool({
185+
description: 'Get weather',
186+
inputSchema: z.object({ location: z.string() }),
187+
execute: async () => 'sunny',
188+
providerOptions,
189+
}),
190+
};
191+
192+
const result = await toolsToModelTools(tools);
193+
194+
expect(result[0]).toMatchObject({ providerOptions });
195+
});
196+
197+
it('handles tools with type: "dynamic" as function tools', async () => {
198+
const tools = {
199+
dynamic: {
200+
type: 'dynamic' as const,
201+
description: 'A dynamic tool',
202+
inputSchema: z.object({ input: z.string() }),
203+
execute: async () => 'result',
204+
},
205+
};
206+
207+
const result = await toolsToModelTools(tools as any);
208+
209+
expect(result).toHaveLength(1);
210+
expect(result[0]).toMatchObject({
211+
type: 'function',
212+
name: 'dynamic',
213+
description: 'A dynamic tool',
214+
});
215+
});
216+
217+
it('returns empty array for empty tools', async () => {
218+
const result = await toolsToModelTools({});
219+
expect(result).toEqual([]);
220+
});
106221
});

packages/ai/src/agent/tools-to-model-tools.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,45 @@ import type {
44
} from '@ai-sdk/provider';
55
import { asSchema, type ToolSet } from 'ai';
66

7+
// Mirrors the tool→LanguageModelV3FunctionTool/LanguageModelV3ProviderTool
8+
// mapping in the core AI SDK's prepareToolsAndToolChoice
9+
// (ai/src/prompt/prepare-tools-and-tool-choice.ts).
710
export async function toolsToModelTools(
811
tools: ToolSet
912
): Promise<Array<LanguageModelV3FunctionTool | LanguageModelV3ProviderTool>> {
1013
return Promise.all(
1114
Object.entries(tools).map(async ([name, tool]) => {
12-
// Preserve provider tool identity (e.g. anthropic.tools.webSearch)
13-
// instead of converting to a plain function tool
14-
if ((tool as any).type === 'provider') {
15-
return {
16-
type: 'provider' as const,
17-
id: (tool as any).id as `${string}.${string}`,
18-
name,
19-
args: (tool as any).args ?? {},
20-
};
21-
}
15+
const toolType = tool.type;
2216

23-
return {
24-
type: 'function' as const,
25-
name,
26-
description: tool.description,
27-
inputSchema: await asSchema(tool.inputSchema).jsonSchema,
28-
};
17+
switch (toolType) {
18+
case undefined:
19+
case 'dynamic':
20+
case 'function':
21+
return {
22+
type: 'function' as const,
23+
name,
24+
description: tool.description,
25+
inputSchema: await asSchema(tool.inputSchema).jsonSchema,
26+
...(tool.inputExamples != null
27+
? { inputExamples: tool.inputExamples }
28+
: {}),
29+
providerOptions: tool.providerOptions,
30+
...(tool.strict != null ? { strict: tool.strict } : {}),
31+
};
32+
case 'provider':
33+
// Preserve provider tool identity (e.g. anthropic.tools.webSearch)
34+
// instead of converting to a plain function tool
35+
return {
36+
type: 'provider' as const,
37+
name,
38+
id: tool.id,
39+
args: tool.args ?? {},
40+
};
41+
default: {
42+
const exhaustiveCheck: never = toolType as never;
43+
throw new Error(`Unsupported tool type: ${exhaustiveCheck}`);
44+
}
45+
}
2946
})
3047
);
3148
}

0 commit comments

Comments
 (0)