Skip to content

Commit b6783da

Browse files
authored
refactoring: restructure Tool types (#14849)
## Background Different types of tools can have different available options. Currently we expose options on tool types that do not support it, e.g. execute on provider executed tools. ## Summary Refactor the `Tool` type into a union of `FunctionTool`, `DynamicTool`, `ProviderDefinedTool`, and `ProviderExecutedTool`. Expose the new types. Add type tests. The goal is to create a foundation for further refactoring and option restrictions. ## Future Work * restrict options available on specific tool types more
1 parent cf517e1 commit b6783da

16 files changed

Lines changed: 912 additions & 303 deletions

.changeset/stupid-lamps-brush.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": patch
3+
---
4+
5+
refactoring: restructure Tool types

packages/openai/src/tool/local-shell.test-d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
type localShellInputSchema,
66
type localShellOutputSchema,
77
} from './local-shell';
8+
89
describe('local-shell tool type', () => {
910
it('should have Tool type', () => {
1011
const localShellTool = localShell({});
1112

12-
expectTypeOf(localShellTool).toEqualTypeOf<
13+
expectTypeOf(localShellTool).toExtend<
1314
Tool<
1415
InferSchema<typeof localShellInputSchema>,
1516
InferSchema<typeof localShellOutputSchema>,

packages/openai/src/tool/web-search.test-d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { InferSchema, Tool } from '@ai-sdk/provider-utils';
22
import { describe, expectTypeOf, it } from 'vitest';
33
import { webSearch, type webSearchOutputSchema } from './web-search';
4+
45
describe('web-search tool type', () => {
56
it('should have Tool type', () => {
67
const webSearchTool = webSearch();
78

8-
expectTypeOf(webSearchTool).toEqualTypeOf<
9+
expectTypeOf(webSearchTool).toExtend<
910
Tool<{}, InferSchema<typeof webSearchOutputSchema>, {}>
1011
>();
1112
});

packages/provider-utils/src/provider-defined-tool-factory.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { tool, type Tool, type ToolExecuteFunction } from './types/tool';
1+
import { tool, type ProviderDefinedTool, type Tool } from './types/tool';
22
import type { FlexibleSchema } from './schema';
33
import type { Context } from './types/context';
4+
import type { ToolExecuteFunction } from './types/tool-execute-function';
45
/**
56
* A provider-defined tool is a tool for which the provider defines the input
67
* and output schemas, but does not execute the tool.
@@ -18,7 +19,7 @@ export type ProviderDefinedToolFactory<
1819
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
1920
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
2021
},
21-
) => Tool<INPUT, OUTPUT, CONTEXT>;
22+
) => ProviderDefinedTool<INPUT, OUTPUT, CONTEXT>;
2223

2324
export function createProviderDefinedToolFactory<
2425
INPUT,
@@ -48,7 +49,7 @@ export function createProviderDefinedToolFactory<
4849
onInputStart?: Tool<INPUT, OUTPUT, CONTEXT>['onInputStart'];
4950
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
5051
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
51-
}): Tool<INPUT, OUTPUT, CONTEXT> =>
52+
}): ProviderDefinedTool<INPUT, OUTPUT, CONTEXT> =>
5253
tool({
5354
type: 'provider',
5455
isProviderExecuted: false,
@@ -62,7 +63,7 @@ export function createProviderDefinedToolFactory<
6263
onInputStart,
6364
onInputDelta,
6465
onInputAvailable,
65-
});
66+
}) as ProviderDefinedTool<INPUT, OUTPUT, CONTEXT>;
6667
}
6768

6869
export type ProviderDefinedToolFactoryWithOutputSchema<
@@ -79,7 +80,7 @@ export type ProviderDefinedToolFactoryWithOutputSchema<
7980
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
8081
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
8182
},
82-
) => Tool<INPUT, OUTPUT, CONTEXT>;
83+
) => ProviderDefinedTool<INPUT, OUTPUT, CONTEXT>;
8384

8485
export function createProviderDefinedToolFactoryWithOutputSchema<
8586
INPUT,
@@ -110,7 +111,7 @@ export function createProviderDefinedToolFactoryWithOutputSchema<
110111
onInputStart?: Tool<INPUT, OUTPUT, CONTEXT>['onInputStart'];
111112
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
112113
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
113-
}): Tool<INPUT, OUTPUT, CONTEXT> =>
114+
}): ProviderDefinedTool<INPUT, OUTPUT, CONTEXT> =>
114115
tool({
115116
type: 'provider',
116117
isProviderExecuted: false,
@@ -124,5 +125,5 @@ export function createProviderDefinedToolFactoryWithOutputSchema<
124125
onInputStart,
125126
onInputDelta,
126127
onInputAvailable,
127-
});
128+
}) as ProviderDefinedTool<INPUT, OUTPUT, CONTEXT>;
128129
}

packages/provider-utils/src/provider-executed-tool-factory.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FlexibleSchema } from './schema';
22
import type { Context } from './types/context';
3-
import { tool, type Tool } from './types/tool';
3+
import { tool, type ProviderExecutedTool, type Tool } from './types/tool';
44
/**
55
* A provider-executed tool is a tool for which the provider executes the tool.
66
*/
@@ -15,7 +15,7 @@ export type ProviderExecutedToolFactory<
1515
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
1616
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
1717
},
18-
) => Tool<INPUT, OUTPUT, CONTEXT>;
18+
) => ProviderExecutedTool<INPUT, OUTPUT, CONTEXT>;
1919

2020
export function createProviderExecutedToolFactory<
2121
INPUT,
@@ -53,7 +53,7 @@ export function createProviderExecutedToolFactory<
5353
onInputStart?: Tool<INPUT, OUTPUT, CONTEXT>['onInputStart'];
5454
onInputDelta?: Tool<INPUT, OUTPUT, CONTEXT>['onInputDelta'];
5555
onInputAvailable?: Tool<INPUT, OUTPUT, CONTEXT>['onInputAvailable'];
56-
}): Tool<INPUT, OUTPUT, CONTEXT> =>
56+
}): ProviderExecutedTool<INPUT, OUTPUT, CONTEXT> =>
5757
tool({
5858
type: 'provider',
5959
isProviderExecuted: true,
@@ -65,5 +65,5 @@ export function createProviderExecutedToolFactory<
6565
onInputDelta,
6666
onInputAvailable,
6767
supportsDeferredResults,
68-
});
68+
}) as ProviderExecutedTool<INPUT, OUTPUT, CONTEXT>;
6969
}

packages/provider-utils/src/types/execute-tool.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
22
import { z } from 'zod/v4';
33
import { executeTool } from './execute-tool';
44
import type { ExecutableTool } from './executable-tool';
5-
import { tool, type ToolExecutionOptions } from './tool';
5+
import { tool } from './tool';
6+
import type { ToolExecutionOptions } from './tool-execute-function';
67
describe('executeTool', () => {
78
it('yields a single final output for non-streaming tools', async () => {
89
const weatherTool = tool({

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { ExecutableTool } from './executable-tool';
33
import type { InferToolContext } from './infer-tool-context';
44
import type { InferToolInput } from './infer-tool-input';
55
import type { InferToolOutput } from './infer-tool-output';
6-
import type { Tool, ToolExecutionOptions } from './tool';
6+
import type { Tool } from './tool';
7+
import type { ToolExecutionOptions } from './tool-execute-function';
78

89
/**
910
* Executes a tool function and normalizes its results into a stream of outputs.

packages/provider-utils/src/types/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,17 @@ export type { SystemModelMessage } from './system-model-message';
3636
export {
3737
dynamicTool,
3838
tool,
39+
type DynamicTool,
40+
type FunctionTool,
41+
type ProviderDefinedTool,
42+
type ProviderExecutedTool,
3943
type Tool,
40-
type ToolExecuteFunction,
41-
type ToolExecutionOptions,
42-
type ToolNeedsApprovalFunction,
4344
} from './tool';
45+
export type {
46+
ToolExecuteFunction,
47+
ToolExecutionOptions,
48+
} from './tool-execute-function';
49+
export type { ToolNeedsApprovalFunction } from './tool-needs-approval-function';
4450
export type { ToolSet } from './tool-set';
4551
export type { ToolApprovalRequest } from './tool-approval-request';
4652
export type { ToolApprovalResponse } from './tool-approval-response';
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { describe, expectTypeOf, it } from 'vitest';
2+
import type { NeverOptional } from './never-optional';
3+
4+
describe('NeverOptional', () => {
5+
type Properties = {
6+
required: string;
7+
optional?: number;
8+
readonly readonlyProperty: boolean;
9+
};
10+
11+
it('preserves the original properties for known types', () => {
12+
expectTypeOf<
13+
NeverOptional<string, Properties>
14+
>().toEqualTypeOf<Properties>();
15+
expectTypeOf<
16+
NeverOptional<unknown, Properties>
17+
>().toEqualTypeOf<Properties>();
18+
expectTypeOf<
19+
NeverOptional<string | undefined, Properties>
20+
>().toEqualTypeOf<Properties>();
21+
});
22+
23+
it('makes the properties optional when the condition type is any', () => {
24+
expectTypeOf<NeverOptional<any, Properties>>().toEqualTypeOf<
25+
Partial<Properties>
26+
>();
27+
28+
expectTypeOf<NeverOptional<any, Properties>>().toMatchTypeOf<{
29+
required?: string;
30+
optional?: number;
31+
readonly readonlyProperty?: boolean;
32+
}>();
33+
});
34+
35+
it('allows only undefined optional properties when the condition type is never', () => {
36+
expectTypeOf<NeverOptional<never, Properties>>().toEqualTypeOf<{
37+
required?: undefined;
38+
optional?: undefined;
39+
readonlyProperty?: undefined;
40+
}>();
41+
42+
expectTypeOf<{}>().toMatchTypeOf<NeverOptional<never, Properties>>();
43+
expectTypeOf<{ required: undefined }>().toMatchTypeOf<
44+
NeverOptional<never, Properties>
45+
>();
46+
expectTypeOf<{ optional: undefined }>().toMatchTypeOf<
47+
NeverOptional<never, Properties>
48+
>();
49+
50+
expectTypeOf<{ required: string }>().not.toMatchTypeOf<
51+
NeverOptional<never, Properties>
52+
>();
53+
expectTypeOf<{ optional: number }>().not.toMatchTypeOf<
54+
NeverOptional<never, Properties>
55+
>();
56+
expectTypeOf<{ readonlyProperty: boolean }>().not.toMatchTypeOf<
57+
NeverOptional<never, Properties>
58+
>();
59+
});
60+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// 0 extends 1 & N checks for any
2+
// [N] extends [never] checks for never
3+
export type NeverOptional<N, T> = 0 extends 1 & N
4+
? Partial<T>
5+
: [N] extends [never]
6+
? Partial<Record<keyof T, undefined>>
7+
: T;

0 commit comments

Comments
 (0)