Skip to content

Commit a2cbf0a

Browse files
mattappersonclaude
andcommitted
feat: add strongly typed tools with createTool helpers
- Add createTool, createGeneratorTool, createManualTool factory functions - Add type inference helpers: InferToolInput, InferToolOutput, InferToolEvent - Add TypedToolCall and TypedToolCallUnion for typed tool call results - Add InferToolEventsUnion for typed generator event streams - Make ResponseWrapper generic with TTools parameter - Make callModel generic to preserve tool types through call chain - Make event types generic (ToolStreamEvent, ChatStreamEvent, etc.) - Export all new types from index.ts - Add example demonstrating typed tool calling When using 'as const' with tools array, all type information flows through: - toolCall.arguments is typed based on inputSchema - event.result in streams is typed based on eventSchema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a42a700 commit a2cbf0a

File tree

7 files changed

+692
-50
lines changed

7 files changed

+692
-50
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Example: Typed Tool Calling with callModel
3+
*
4+
* This example demonstrates how to use createTool and createGeneratorTool for
5+
* fully-typed tool definitions where execute params, return types, and event
6+
* types are automatically inferred from Zod schemas.
7+
*
8+
* To run this example from the examples directory:
9+
* npm run build && npx tsx callModel-typed-tool-calling.example.ts
10+
*/
11+
12+
import dotenv from "dotenv";
13+
dotenv.config();
14+
15+
import { OpenRouter, createTool, createGeneratorTool } from "../src/index.js";
16+
import z from "zod";
17+
18+
const openRouter = new OpenRouter({
19+
apiKey: process.env["OPENROUTER_API_KEY"] ?? "",
20+
});
21+
22+
// Create a typed tool using createTool
23+
// The execute function params are automatically typed as z.infer<typeof inputSchema>
24+
// The return type is enforced based on outputSchema
25+
const weatherTool = createTool({
26+
name: "get_weather",
27+
description: "Get the current weather for a location",
28+
inputSchema: z.object({
29+
location: z.string().describe("The city and country, e.g. San Francisco, CA"),
30+
}),
31+
outputSchema: z.object({
32+
temperature: z.number(),
33+
description: z.string(),
34+
}),
35+
// params is automatically typed as { location: string }
36+
execute: async (params) => {
37+
console.log(`Getting weather for: ${params.location}`);
38+
// Return type is enforced as { temperature: number; description: string }
39+
return {
40+
temperature: 20,
41+
description: "Sunny",
42+
};
43+
},
44+
});
45+
46+
// Create a generator tool with typed progress events
47+
// The eventSchema defines the type of events yielded during execution
48+
const searchTool = createGeneratorTool({
49+
name: "search_database",
50+
description: "Search database with progress updates",
51+
inputSchema: z.object({
52+
query: z.string().describe("The search query"),
53+
}),
54+
eventSchema: z.object({
55+
progress: z.number(),
56+
message: z.string(),
57+
}),
58+
outputSchema: z.object({
59+
results: z.array(z.string()),
60+
totalFound: z.number(),
61+
}),
62+
// execute is a generator that yields typed progress events
63+
execute: async function* (params) {
64+
console.log(`Searching for: ${params.query}`);
65+
// Each yield is typed as { progress: number; message: string }
66+
yield { progress: 25, message: "Searching..." };
67+
yield { progress: 50, message: "Processing results..." };
68+
yield { progress: 75, message: "Almost done..." };
69+
// Final result is typed as { results: string[]; totalFound: number }
70+
yield { progress: 100, message: "Complete!" };
71+
},
72+
});
73+
74+
async function main() {
75+
console.log("=== Typed Tool Calling Example ===\n");
76+
77+
// Use 'as const' to enable full type inference for tool calls
78+
const result = openRouter.callModel({
79+
instructions: "You are a helpful assistant. Your name is Mark",
80+
model: "openai/gpt-4o-mini",
81+
input: "Hello! What is the weather in San Francisco?",
82+
tools: [weatherTool] as const,
83+
});
84+
85+
// Get text response (tools are auto-executed)
86+
const text = await result.getText();
87+
console.log("Response:", text);
88+
89+
console.log("\n=== Getting Tool Calls ===\n");
90+
91+
// Create a fresh request for demonstrating getToolCalls
92+
const result2 = openRouter.callModel({
93+
model: "openai/gpt-4o-mini",
94+
input: "What's the weather like in Paris?",
95+
tools: [weatherTool] as const,
96+
maxToolRounds: 0, // Don't auto-execute, just get the tool calls
97+
});
98+
99+
// Tool calls are now typed based on the tool definitions!
100+
const toolCalls = await result2.getToolCalls();
101+
102+
for (const toolCall of toolCalls) {
103+
console.log(`Tool: ${toolCall.name}`);
104+
// toolCall.arguments is typed as { location: string }
105+
console.log(`Arguments:`, toolCall.arguments);
106+
}
107+
108+
console.log("\n=== Streaming Tool Calls ===\n");
109+
110+
// Create another request for demonstrating streaming
111+
const result3 = openRouter.callModel({
112+
model: "openai/gpt-4o-mini",
113+
input: "What's the weather in Tokyo?",
114+
tools: [weatherTool] as const,
115+
maxToolRounds: 0,
116+
});
117+
118+
// Stream tool calls with typed arguments
119+
for await (const toolCall of result3.getToolCallsStream()) {
120+
console.log(`Streamed tool: ${toolCall.name}`);
121+
// toolCall.arguments is typed based on tool definitions
122+
console.log(`Streamed arguments:`, toolCall.arguments);
123+
}
124+
125+
console.log("\n=== Generator Tool with Typed Events ===\n");
126+
127+
// Use generator tool with typed progress events
128+
const result4 = openRouter.callModel({
129+
model: "openai/gpt-4o-mini",
130+
input: "Search for documents about TypeScript",
131+
tools: [searchTool] as const,
132+
});
133+
134+
// Stream events from getToolStream - events are fully typed!
135+
for await (const event of result4.getToolStream()) {
136+
if (event.type === "preliminary_result") {
137+
// event.result is typed as { progress: number; message: string }
138+
console.log(`Progress: ${event.result.progress}% - ${event.result.message}`);
139+
} else if (event.type === "delta") {
140+
// Tool argument deltas
141+
process.stdout.write(event.content);
142+
}
143+
}
144+
145+
console.log("\n=== Mixed Tools with Typed Events ===\n");
146+
147+
// Use both regular and generator tools together
148+
const result5 = openRouter.callModel({
149+
model: "openai/gpt-4o-mini",
150+
input: "First search for weather data, then get the weather in Seattle",
151+
tools: [weatherTool, searchTool] as const,
152+
});
153+
154+
// Events are a union of all generator tool event types
155+
for await (const event of result5.getToolStream()) {
156+
if (event.type === "preliminary_result") {
157+
// event.result is typed as { progress: number; message: string }
158+
// (only searchTool has eventSchema, so that's the event type)
159+
console.log(`Event:`, event.result);
160+
}
161+
}
162+
}
163+
164+
main().catch(console.error);

src/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,32 @@ export * from "./sdk/sdk.js";
1212
export { fromClaudeMessages, toClaudeMessage } from "./lib/anthropic-compat.js";
1313
export { fromChatMessages, toChatMessage } from "./lib/chat-compat.js";
1414
export { extractUnsupportedContent, hasUnsupportedContent, getUnsupportedContentSummary } from "./lib/stream-transformers.js";
15+
16+
// Tool creation helpers
17+
export {
18+
createTool,
19+
createGeneratorTool,
20+
createManualTool,
21+
} from "./lib/create-tool.js";
22+
23+
// Tool type inference helpers
24+
export type {
25+
Tool,
26+
ToolWithExecute,
27+
ToolWithGenerator,
28+
ManualTool,
29+
InferToolInput,
30+
InferToolOutput,
31+
TypedToolCall,
32+
TypedToolCallUnion,
33+
TurnContext,
34+
ParsedToolCall,
35+
// Event type inference helpers
36+
InferToolEvent,
37+
InferToolEventsUnion,
38+
// Stream event types
39+
ToolPreliminaryResultEvent,
40+
ToolStreamEvent,
41+
ChatStreamEvent,
42+
EnhancedResponseStreamEvent,
43+
} from "./lib/tool-types.js";

src/lib/create-tool.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import type { ZodObject, ZodRawShape, ZodType, z } from 'zod/v4';
2+
import {
3+
ToolType,
4+
type TurnContext,
5+
type ToolWithExecute,
6+
type ToolWithGenerator,
7+
type ManualTool,
8+
} from './tool-types.js';
9+
10+
/**
11+
* Creates a tool with full type inference from Zod schemas.
12+
*
13+
* The execute function parameters are automatically typed based on the inputSchema,
14+
* and the return type is enforced based on the outputSchema.
15+
*
16+
* @example
17+
* ```typescript
18+
* const weatherTool = createTool({
19+
* name: "get_weather",
20+
* description: "Get weather for a location",
21+
* inputSchema: z.object({ location: z.string() }),
22+
* outputSchema: z.object({ temperature: z.number() }),
23+
* execute: async (params) => {
24+
* // params is typed as { location: string }
25+
* return { temperature: 72 }; // return type is enforced
26+
* },
27+
* });
28+
* ```
29+
*/
30+
export function createTool<
31+
TInput extends ZodObject<ZodRawShape>,
32+
TOutput extends ZodType = ZodType<unknown>,
33+
>(config: {
34+
name: string;
35+
description?: string;
36+
inputSchema: TInput;
37+
outputSchema?: TOutput;
38+
execute: (
39+
params: z.infer<TInput>,
40+
context?: TurnContext,
41+
) => Promise<z.infer<TOutput>> | z.infer<TOutput>;
42+
}): ToolWithExecute<TInput, TOutput> {
43+
const fn: ToolWithExecute<TInput, TOutput>['function'] = {
44+
name: config.name,
45+
inputSchema: config.inputSchema,
46+
execute: config.execute,
47+
};
48+
49+
if (config.description !== undefined) {
50+
fn.description = config.description;
51+
}
52+
53+
if (config.outputSchema !== undefined) {
54+
fn.outputSchema = config.outputSchema as TOutput;
55+
}
56+
57+
return {
58+
type: ToolType.Function,
59+
function: fn,
60+
};
61+
}
62+
63+
/**
64+
* Creates a generator tool with streaming capabilities.
65+
*
66+
* Generator tools can yield intermediate events (validated by eventSchema) during execution
67+
* and a final output (validated by outputSchema) as the last emission.
68+
*
69+
* @example
70+
* ```typescript
71+
* const progressTool = createGeneratorTool({
72+
* name: "process_data",
73+
* inputSchema: z.object({ data: z.string() }),
74+
* eventSchema: z.object({ progress: z.number() }),
75+
* outputSchema: z.object({ result: z.string() }),
76+
* execute: async function* (params) {
77+
* yield { progress: 50 }; // typed as event
78+
* yield { result: "done" }; // typed as output
79+
* },
80+
* });
81+
* ```
82+
*/
83+
export function createGeneratorTool<
84+
TInput extends ZodObject<ZodRawShape>,
85+
TEvent extends ZodType,
86+
TOutput extends ZodType,
87+
>(config: {
88+
name: string;
89+
description?: string;
90+
inputSchema: TInput;
91+
eventSchema: TEvent;
92+
outputSchema: TOutput;
93+
execute: (
94+
params: z.infer<TInput>,
95+
context?: TurnContext,
96+
) => AsyncGenerator<z.infer<TEvent> | z.infer<TOutput>>;
97+
}): ToolWithGenerator<TInput, TEvent, TOutput> {
98+
const fn: ToolWithGenerator<TInput, TEvent, TOutput>['function'] = {
99+
name: config.name,
100+
inputSchema: config.inputSchema,
101+
eventSchema: config.eventSchema,
102+
outputSchema: config.outputSchema,
103+
execute: config.execute as ToolWithGenerator<TInput, TEvent, TOutput>['function']['execute'],
104+
};
105+
106+
if (config.description !== undefined) {
107+
fn.description = config.description;
108+
}
109+
110+
return {
111+
type: ToolType.Function,
112+
function: fn,
113+
};
114+
}
115+
116+
/**
117+
* Creates a manual tool without an execute function.
118+
*
119+
* Manual tools are useful when you want to handle tool execution yourself,
120+
* for example when the tool requires external processing or user interaction.
121+
*
122+
* @example
123+
* ```typescript
124+
* const manualTool = createManualTool({
125+
* name: "external_api",
126+
* inputSchema: z.object({ query: z.string() }),
127+
* outputSchema: z.object({ response: z.string() }),
128+
* });
129+
* ```
130+
*/
131+
export function createManualTool<
132+
TInput extends ZodObject<ZodRawShape>,
133+
TOutput extends ZodType = ZodType<unknown>,
134+
>(config: {
135+
name: string;
136+
description?: string;
137+
inputSchema: TInput;
138+
outputSchema?: TOutput;
139+
}): ManualTool<TInput, TOutput> {
140+
const fn: ManualTool<TInput, TOutput>['function'] = {
141+
name: config.name,
142+
inputSchema: config.inputSchema,
143+
};
144+
145+
if (config.description !== undefined) {
146+
fn.description = config.description;
147+
}
148+
149+
if (config.outputSchema !== undefined) {
150+
fn.outputSchema = config.outputSchema as TOutput;
151+
}
152+
153+
return {
154+
type: ToolType.Function,
155+
function: fn,
156+
};
157+
}

0 commit comments

Comments
 (0)