Skip to content

Commit 71f938d

Browse files
authored
feat (ai): add output schema for tools (#6819)
## Background Server side tools and tools without an execute method need a way to specify the output schema to drive type inference and validation. ## Summary Add optional `outputSchema` property to tools.
1 parent 45c1ea2 commit 71f938d

File tree

5 files changed

+58
-52
lines changed

5 files changed

+58
-52
lines changed

‎.changeset/tough-islands-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/provider-utils': major
3+
---
4+
5+
feat (ai): add output schema for tools

‎examples/ai-core/src/generate-text/openai-tool-call.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ async function main() {
3636
// typed tool results for tools with execute method:
3737
for (const toolResult of result.toolResults) {
3838
switch (toolResult.toolName) {
39-
// NOT AVAILABLE (NO EXECUTE METHOD)
40-
// case 'cityAttractions': {
41-
// toolResult.input.city; // string
42-
// toolResult.result;
43-
// break;
44-
// }
39+
case 'cityAttractions': {
40+
toolResult.input.city; // string
41+
toolResult.output; // any since no outputSchema is provided
42+
break;
43+
}
4544

4645
case 'weather': {
4746
toolResult.input.location; // string

‎examples/next-openai/app/api/use-chat-tools/route.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { openai } from '@ai-sdk/openai';
22
import {
33
convertToModelMessages,
4-
InferToolInput,
54
InferUITool,
65
stepCountIs,
76
streamText,
@@ -50,27 +49,23 @@ const askForConfirmationTool = tool({
5049
inputSchema: z.object({
5150
message: z.string().describe('The message to ask for confirmation.'),
5251
}),
52+
outputSchema: z.string(),
5353
});
5454

5555
const getLocationTool = tool({
5656
description:
5757
'Get the user location. Always ask for confirmation before using this tool.',
5858
inputSchema: z.object({}),
59+
outputSchema: z.string(),
5960
});
6061

6162
export type UseChatToolsMessage = UIMessage<
6263
never,
6364
UIDataTypes,
6465
{
6566
getWeatherInformation: InferUITool<typeof getWeatherInformationTool>;
66-
askForConfirmation: {
67-
input: InferToolInput<typeof askForConfirmationTool>;
68-
output: string;
69-
};
70-
getLocation: {
71-
input: InferToolInput<typeof getLocationTool>;
72-
output: string;
73-
};
67+
askForConfirmation: InferUITool<typeof askForConfirmationTool>;
68+
getLocation: InferUITool<typeof getLocationTool>;
7469
}
7570
>;
7671

‎packages/ai/core/tool/tool.test-d.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { z } from 'zod';
22
import {
33
Tool,
4-
ToolCallOptions,
54
ToolExecuteFunction,
65
FlexibleSchema,
76
} from '@ai-sdk/provider-utils';
@@ -37,7 +36,7 @@ describe('tool helper', () => {
3736

3837
expectTypeOf(toolType).toEqualTypeOf<Tool<never, 'test'>>();
3938
expectTypeOf(toolType.execute).toMatchTypeOf<
40-
ToolExecuteFunction<never, 'test'>
39+
ToolExecuteFunction<never, 'test'> | undefined
4140
>();
4241
expectTypeOf(toolType.execute).not.toEqualTypeOf<undefined>();
4342
expectTypeOf(toolType.inputSchema).toEqualTypeOf<undefined>();
@@ -54,10 +53,7 @@ describe('tool helper', () => {
5453

5554
expectTypeOf(toolType).toEqualTypeOf<Tool<{ number: number }, 'test'>>();
5655
expectTypeOf(toolType.execute).toMatchTypeOf<
57-
(
58-
input: { number: number },
59-
options: ToolCallOptions,
60-
) => PromiseLike<'test'> | 'test'
56+
ToolExecuteFunction<{ number: number }, 'test'> | undefined
6157
>();
6258
expectTypeOf(toolType.execute).not.toEqualTypeOf<undefined>();
6359
expectTypeOf(toolType.inputSchema).toEqualTypeOf<

‎packages/provider-utils/src/types/tool.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export type ToolExecuteFunction<INPUT, OUTPUT> = (
2525
options: ToolCallOptions,
2626
) => PromiseLike<OUTPUT> | OUTPUT;
2727

28+
// 0 extends 1 & N checks for any
29+
// [N] extends [never] checks for never
2830
type NeverOptional<N, T> = 0 extends 1 & N
2931
? Partial<T>
3032
: [N] extends [never]
@@ -56,20 +58,35 @@ It is also used to validate the output of the language model.
5658
Use descriptions to make the input understandable for the language model.
5759
*/
5860
inputSchema: FlexibleSchema<INPUT>;
61+
62+
/**
63+
* Optional function that is called when the argument streaming starts.
64+
* Only called when the tool is used in a streaming context.
65+
*/
66+
onInputStart?: (options: ToolCallOptions) => void | PromiseLike<void>;
67+
68+
/**
69+
* Optional function that is called when an argument streaming delta is available.
70+
* Only called when the tool is used in a streaming context.
71+
*/
72+
onInputDelta?: (
73+
options: { inputTextDelta: string } & ToolCallOptions,
74+
) => void | PromiseLike<void>;
75+
76+
/**
77+
* Optional function that is called when a tool call can be started,
78+
* even if the execute function is not provided.
79+
*/
80+
onInputAvailable?: (
81+
options: {
82+
input: [INPUT] extends [never] ? undefined : INPUT;
83+
} & ToolCallOptions,
84+
) => void | PromiseLike<void>;
5985
}
6086
> &
6187
NeverOptional<
6288
OUTPUT,
6389
{
64-
/**
65-
An async function that is called with the arguments from the tool call and produces a result.
66-
If not provided, the tool will not be executed automatically.
67-
68-
@args is the input of the tool call.
69-
@options.abortSignal is a signal that can be used to abort the tool call.
70-
*/
71-
execute: ToolExecuteFunction<INPUT, OUTPUT>;
72-
7390
/**
7491
Optional conversion function that maps the tool result to an output that can be used by the language model.
7592
@@ -78,31 +95,25 @@ If not provided, the tool result will be sent as a JSON object.
7895
toModelOutput?: (
7996
output: OUTPUT,
8097
) => LanguageModelV2ToolResultPart['output'];
98+
} & (
99+
| {
100+
/**
101+
An async function that is called with the arguments from the tool call and produces a result.
102+
If not provided, the tool will not be executed automatically.
81103
82-
/**
83-
* Optional function that is called when the argument streaming starts.
84-
* Only called when the tool is used in a streaming context.
85-
*/
86-
onInputStart?: (options: ToolCallOptions) => void | PromiseLike<void>;
104+
@args is the input of the tool call.
105+
@options.abortSignal is a signal that can be used to abort the tool call.
106+
*/
107+
execute: ToolExecuteFunction<INPUT, OUTPUT>;
87108

88-
/**
89-
* Optional function that is called when an argument streaming delta is available.
90-
* Only called when the tool is used in a streaming context.
91-
*/
92-
onInputDelta?: (
93-
options: { inputTextDelta: string } & ToolCallOptions,
94-
) => void | PromiseLike<void>;
109+
outputSchema?: FlexibleSchema<OUTPUT>;
110+
}
111+
| {
112+
outputSchema: FlexibleSchema<OUTPUT>;
95113

96-
/**
97-
* Optional function that is called when a tool call can be started,
98-
* even if the execute function is not provided.
99-
*/
100-
onInputAvailable?: (
101-
options: {
102-
input: [INPUT] extends [never] ? undefined : INPUT;
103-
} & ToolCallOptions,
104-
) => void | PromiseLike<void>;
105-
}
114+
execute?: never;
115+
}
116+
)
106117
> &
107118
(
108119
| {

0 commit comments

Comments
 (0)