Skip to content

Commit 9df0089

Browse files
committed
review comments
1 parent a113bad commit 9df0089

3 files changed

Lines changed: 238 additions & 17 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 add support for `type: 'provider'` tools
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { z } from 'zod';
3+
import { tool } from 'ai';
4+
import { toolsToModelTools } from './tools-to-model-tools.js';
5+
6+
describe('toolsToModelTools', () => {
7+
it('converts a basic function tool', async () => {
8+
const tools = {
9+
weather: tool({
10+
description: 'Get weather',
11+
inputSchema: z.object({ location: z.string() }),
12+
execute: async () => 'sunny',
13+
}),
14+
};
15+
16+
const result = await toolsToModelTools(tools);
17+
18+
expect(result).toHaveLength(1);
19+
expect(result[0]).toMatchObject({
20+
type: 'function',
21+
name: 'weather',
22+
description: 'Get weather',
23+
});
24+
expect(result[0]).toHaveProperty('inputSchema');
25+
expect(result[0]).not.toHaveProperty('strict');
26+
expect(result[0]).not.toHaveProperty('inputExamples');
27+
});
28+
29+
it('forwards strict: true', async () => {
30+
const tools = {
31+
weather: tool({
32+
description: 'Get weather',
33+
inputSchema: z.object({ location: z.string() }),
34+
execute: async () => 'sunny',
35+
strict: true,
36+
}),
37+
};
38+
39+
const result = await toolsToModelTools(tools);
40+
41+
expect(result[0]).toMatchObject({ strict: true });
42+
});
43+
44+
it('forwards strict: false', async () => {
45+
const tools = {
46+
weather: tool({
47+
description: 'Get weather',
48+
inputSchema: z.object({ location: z.string() }),
49+
execute: async () => 'sunny',
50+
strict: false,
51+
}),
52+
};
53+
54+
const result = await toolsToModelTools(tools);
55+
56+
expect(result[0]).toMatchObject({ strict: false });
57+
});
58+
59+
it('omits strict key when not set', async () => {
60+
const tools = {
61+
weather: tool({
62+
description: 'Get weather',
63+
inputSchema: z.object({ location: z.string() }),
64+
execute: async () => 'sunny',
65+
}),
66+
};
67+
68+
const result = await toolsToModelTools(tools);
69+
70+
expect(result[0]).not.toHaveProperty('strict');
71+
});
72+
73+
it('forwards inputExamples', async () => {
74+
const examples = [{ input: { location: 'Tokyo' } }];
75+
const tools = {
76+
weather: tool({
77+
description: 'Get weather',
78+
inputSchema: z.object({ location: z.string() }),
79+
execute: async () => 'sunny',
80+
inputExamples: examples,
81+
}),
82+
};
83+
84+
const result = await toolsToModelTools(tools);
85+
86+
expect(result[0]).toMatchObject({ inputExamples: examples });
87+
});
88+
89+
it('omits inputExamples key when not set', async () => {
90+
const tools = {
91+
weather: tool({
92+
description: 'Get weather',
93+
inputSchema: z.object({ location: z.string() }),
94+
execute: async () => 'sunny',
95+
}),
96+
};
97+
98+
const result = await toolsToModelTools(tools);
99+
100+
expect(result[0]).not.toHaveProperty('inputExamples');
101+
});
102+
103+
it('forwards providerOptions', async () => {
104+
const providerOptions = { openai: { parallel_tool_calls: false } };
105+
const tools = {
106+
weather: tool({
107+
description: 'Get weather',
108+
inputSchema: z.object({ location: z.string() }),
109+
execute: async () => 'sunny',
110+
providerOptions,
111+
}),
112+
};
113+
114+
const result = await toolsToModelTools(tools);
115+
116+
expect(result[0]).toMatchObject({ providerOptions });
117+
});
118+
119+
it('handles provider-type tools', async () => {
120+
const tools = {
121+
webSearch: {
122+
type: 'provider' as const,
123+
id: 'openai.web_search' as const,
124+
args: { search_context_size: 'medium' },
125+
},
126+
};
127+
128+
const result = await toolsToModelTools(
129+
tools as any // provider tools don't have inputSchema/execute
130+
);
131+
132+
expect(result).toHaveLength(1);
133+
expect(result[0]).toEqual({
134+
type: 'provider',
135+
name: 'webSearch',
136+
id: 'openai.web_search',
137+
args: { search_context_size: 'medium' },
138+
});
139+
});
140+
141+
it('handles a mix of function and provider tools', async () => {
142+
const tools = {
143+
weather: tool({
144+
description: 'Get weather',
145+
inputSchema: z.object({ location: z.string() }),
146+
execute: async () => 'sunny',
147+
}),
148+
webSearch: {
149+
type: 'provider' as const,
150+
id: 'openai.web_search' as const,
151+
args: {},
152+
},
153+
};
154+
155+
const result = await toolsToModelTools(tools as any);
156+
157+
expect(result).toHaveLength(2);
158+
expect(result.find((t) => t.name === 'weather')?.type).toBe('function');
159+
expect(result.find((t) => t.name === 'webSearch')?.type).toBe('provider');
160+
});
161+
162+
it('handles tools with type: "dynamic" as function tools', async () => {
163+
const tools = {
164+
dynamic: {
165+
type: 'dynamic' as const,
166+
description: 'A dynamic tool',
167+
inputSchema: z.object({ input: z.string() }),
168+
execute: async () => 'result',
169+
},
170+
};
171+
172+
const result = await toolsToModelTools(tools as any);
173+
174+
expect(result).toHaveLength(1);
175+
expect(result[0]).toMatchObject({
176+
type: 'function',
177+
name: 'dynamic',
178+
description: 'A dynamic tool',
179+
});
180+
});
181+
182+
it('returns empty array for empty tools', async () => {
183+
const result = await toolsToModelTools({});
184+
expect(result).toEqual([]);
185+
});
186+
});
Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,52 @@
1-
import type { LanguageModelV3FunctionTool } from '@ai-sdk/provider';
1+
import type {
2+
LanguageModelV3FunctionTool,
3+
LanguageModelV3ProviderTool,
4+
} from '@ai-sdk/provider';
25
import { asSchema, type ToolSet } from 'ai';
36

4-
// Mirrors the tool→LanguageModelV3FunctionTool mapping in the core AI SDK's
5-
// prepareToolsAndToolChoice (ai/src/prompt/prepare-tools-and-tool-choice.ts).
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).
610
export async function toolsToModelTools(
711
tools: ToolSet
8-
): Promise<LanguageModelV3FunctionTool[]> {
9-
return Promise.all(
10-
Object.entries(tools).map(async ([name, tool]) => ({
11-
type: 'function' as const,
12-
name,
13-
description: tool.description,
14-
inputSchema: await asSchema(tool.inputSchema).jsonSchema,
15-
...(tool.inputExamples != null
16-
? { inputExamples: tool.inputExamples }
17-
: {}),
18-
providerOptions: tool.providerOptions,
19-
...(tool.strict != null ? { strict: tool.strict } : {}),
20-
}))
21-
);
12+
): Promise<Array<LanguageModelV3FunctionTool | LanguageModelV3ProviderTool>> {
13+
const result: Array<
14+
LanguageModelV3FunctionTool | LanguageModelV3ProviderTool
15+
> = [];
16+
17+
for (const [name, tool] of Object.entries(tools)) {
18+
const toolType = tool.type;
19+
20+
switch (toolType) {
21+
case undefined:
22+
case 'dynamic':
23+
case 'function':
24+
result.push({
25+
type: 'function' as const,
26+
name,
27+
description: tool.description,
28+
inputSchema: await asSchema(tool.inputSchema).jsonSchema,
29+
...(tool.inputExamples != null
30+
? { inputExamples: tool.inputExamples }
31+
: {}),
32+
providerOptions: tool.providerOptions,
33+
...(tool.strict != null ? { strict: tool.strict } : {}),
34+
});
35+
break;
36+
case 'provider':
37+
result.push({
38+
type: 'provider' as const,
39+
name,
40+
id: tool.id,
41+
args: tool.args,
42+
});
43+
break;
44+
default: {
45+
const exhaustiveCheck: never = toolType as never;
46+
throw new Error(`Unsupported tool type: ${exhaustiveCheck}`);
47+
}
48+
}
49+
}
50+
51+
return result;
2252
}

0 commit comments

Comments
 (0)