Skip to content

Commit 91046c3

Browse files
mattappersonclaude
andcommitted
refactor: consolidate tool creation functions into unified tool()
- Rename createTool to tool() as the primary API - Add support for manual tools with execute: false - Auto-detect tool type based on configuration: - Generator tool: when eventSchema is provided - Regular tool: when execute is a function - Manual tool: when execute: false - Keep createTool, createGeneratorTool, createManualTool as deprecated aliases - Update tests and examples to use new tool() API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 559fd35 commit 91046c3

File tree

5 files changed

+492
-84
lines changed

5 files changed

+492
-84
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Example: Tool Request Mutations with callModel
3+
*
4+
* This example demonstrates how to use the tool() function with nextTurnParams
5+
* for tools that can mutate the request between turns.
6+
*
7+
* To run this example from the examples directory:
8+
* npm run build && npx tsx callModel-tool-request-mutations.example.ts
9+
*/
10+
11+
import dotenv from "dotenv";
12+
dotenv.config();
13+
14+
import { OpenRouter, tool } from "../src/index.js";
15+
import z from "zod";
16+
import { readFileSync } from "node:fs";
17+
18+
const openRouter = new OpenRouter({
19+
apiKey: process.env["OPENROUTER_API_KEY"] ?? "",
20+
});
21+
22+
const SkillsTool = tool({
23+
name: "Skill",
24+
description: `
25+
Execute a skill within the main conversation. Skills provide specialized capabilities and domain knowledge for specific tasks.
26+
27+
How to invoke:
28+
- Use this tool with the skill name only (no arguments)
29+
- Examples:
30+
- skill: "pdf" - invoke the pdf skill
31+
- skill: "xlsx" - invoke the xlsx skill
32+
- skill: "ms-office-suite:pdf" - invoke using fully qualified name
33+
34+
Key behaviors:
35+
- When a skill is relevant, I must invoke it IMMEDIATELY as my first action
36+
- I should never just mention a skill without actually calling this tool
37+
- Only use skills listed in <available_skills>
38+
- Cannot invoke a skill that is already running
39+
- Not used for built-in CLI commands`,
40+
inputSchema: z.object({
41+
type: z
42+
.string()
43+
.describe('The skill name (no arguments). E.g., "pdf" or "xlsx"'),
44+
}),
45+
outputSchema: z.string(),
46+
// these run on on every turn where this tool was called after all tool calls
47+
// are executed and before the responses are sent to the model. They are exicuted in the order of the tools array
48+
// in the request. nextTurnParams is optional.
49+
nextTurnParams: {
50+
// allowed keys/params are anything in the normal callModel request
51+
input: (params, context) => {
52+
// This is a quick and dirty way to check if the skill is already loaded
53+
// Not recommended for production use
54+
if (
55+
JSON.stringify(context.input).includes(
56+
`Skill ${params.type} is already loaded`
57+
)
58+
) {
59+
return context.input;
60+
}
61+
62+
const skill = readFileSync(
63+
`~/.claude/skills/${params.type}/SKILL.md`,
64+
"utf-8"
65+
);
66+
// ... mutate / build new instructions based on context
67+
return [
68+
...context.input,
69+
{
70+
role: "user",
71+
content: `Base directory for this skill: ~/.claude/skills/${params.type}/
72+
73+
${skill}`,
74+
},
75+
];
76+
},
77+
},
78+
// params is automatically typed as { location: string }
79+
execute: async (params, context) => {
80+
if (JSON.stringify(context.input).includes("")) {
81+
return `Skill ${params.type} is already loaded`;
82+
}
83+
84+
return `Laynching skill ${params.type}`;
85+
},
86+
});
87+
88+
// Create a generator tool with typed progress events
89+
// The eventSchema triggers generator mode
90+
const searchTool = tool({
91+
name: "search_database",
92+
description: "Search database with progress updates",
93+
inputSchema: z.object({
94+
query: z.string().describe("The search query"),
95+
}),
96+
eventSchema: z.object({
97+
progress: z.number(),
98+
message: z.string(),
99+
}),
100+
outputSchema: z.object({
101+
results: z.array(z.string()),
102+
totalFound: z.number(),
103+
}),
104+
// execute is a generator that yields typed progress events
105+
execute: async function* (params) {
106+
console.log(`Searching for: ${params.query}`);
107+
// Each yield is typed as { progress: number; message: string }
108+
yield { progress: 25, message: "Searching..." };
109+
yield { progress: 50, message: "Processing results..." };
110+
yield { progress: 75, message: "Almost done..." };
111+
// Final result is typed as { results: string[]; totalFound: number }
112+
yield { progress: 100, message: "Complete!" };
113+
},
114+
});
115+
116+
async function main() {
117+
console.log("=== Typed Tool Calling Example ===\n");
118+
119+
// Use 'as const' to enable full type inference for tool calls
120+
const result = openRouter.callModel({
121+
instructions: "You are a helpful assistant. Your name is Mark",
122+
model: "openai/gpt-4o-mini",
123+
input: "Hello! What is the weather in San Francisco?",
124+
tools: [SkillsTool] as const,
125+
});
126+
127+
// Get text response (tools are auto-executed)
128+
const text = await result.getText();
129+
console.log("Response:", text);
130+
131+
console.log("\n=== Getting Tool Calls ===\n");
132+
133+
// Create a fresh request for demonstrating getToolCalls
134+
const result2 = openRouter.callModel({
135+
model: "openai/gpt-4o-mini",
136+
input: "What's the weather like in Paris?",
137+
tools: [SkillsTool] as const,
138+
maxToolRounds: 0, // Don't auto-execute, just get the tool calls
139+
});
140+
141+
// Tool calls are now typed based on the tool definitions!
142+
const toolCalls = await result2.getToolCalls();
143+
144+
for (const toolCall of toolCalls) {
145+
console.log(`Tool: ${toolCall.name}`);
146+
// toolCall.arguments is typed as { location: string }
147+
console.log(`Arguments:`, toolCall.arguments);
148+
}
149+
150+
console.log("\n=== Streaming Tool Calls ===\n");
151+
152+
// Create another request for demonstrating streaming
153+
const result3 = openRouter.callModel({
154+
model: "openai/gpt-4o-mini",
155+
input: "What's the weather in Tokyo?",
156+
tools: [SkillsTool] as const,
157+
maxToolRounds: 0,
158+
});
159+
160+
// Stream tool calls with typed arguments
161+
for await (const toolCall of result3.getToolCallsStream()) {
162+
console.log(`Streamed tool: ${toolCall.name}`);
163+
// toolCall.arguments is typed based on tool definitions
164+
console.log(`Streamed arguments:`, toolCall.arguments);
165+
}
166+
167+
console.log("\n=== Generator Tool with Typed Events ===\n");
168+
169+
// Use generator tool with typed progress events
170+
const result4 = openRouter.callModel({
171+
model: "openai/gpt-4o-mini",
172+
input: "Search for documents about TypeScript",
173+
tools: [searchTool] as const,
174+
});
175+
176+
// Stream events from getToolStream - events are fully typed!
177+
for await (const event of result4.getToolStream()) {
178+
if (event.type === "preliminary_result") {
179+
// event.result is typed as { progress: number; message: string }
180+
console.log(
181+
`Progress: ${event.result.progress}% - ${event.result.message}`
182+
);
183+
} else if (event.type === "delta") {
184+
// Tool argument deltas
185+
process.stdout.write(event.content);
186+
}
187+
}
188+
189+
console.log("\n=== Mixed Tools with Typed Events ===\n");
190+
191+
// Use both regular and generator tools together
192+
const result5 = openRouter.callModel({
193+
model: "openai/gpt-4o-mini",
194+
input: "First search for weather data, then get the weather in Seattle",
195+
tools: [SkillsTool, searchTool] as const,
196+
});
197+
198+
// Events are a union of all generator tool event types
199+
for await (const event of result5.getToolStream()) {
200+
if (event.type === "preliminary_result") {
201+
// event.result is typed as { progress: number; message: string }
202+
// (only searchTool has eventSchema, so that's the event type)
203+
console.log(`Event:`, event.result);
204+
}
205+
}
206+
}
207+
208+
main().catch(console.error);

examples/callModel-typed-tool-calling.example.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
/*
22
* Example: Typed Tool Calling with callModel
33
*
4-
* This example demonstrates how to use createTool and createGeneratorTool for
4+
* This example demonstrates how to use the tool() function for
55
* fully-typed tool definitions where execute params, return types, and event
66
* types are automatically inferred from Zod schemas.
77
*
8+
* Tool types are auto-detected based on configuration:
9+
* - Generator tool: When `eventSchema` is provided
10+
* - Regular tool: When `execute` is a function (no `eventSchema`)
11+
* - Manual tool: When `execute: false` is set
12+
*
813
* To run this example from the examples directory:
914
* npm run build && npx tsx callModel-typed-tool-calling.example.ts
1015
*/
1116

1217
import dotenv from "dotenv";
1318
dotenv.config();
1419

15-
import { OpenRouter, createTool, createGeneratorTool } from "../src/index.js";
20+
import { OpenRouter, tool } from "../src/index.js";
1621
import z from "zod";
1722

1823
const openRouter = new OpenRouter({
1924
apiKey: process.env["OPENROUTER_API_KEY"] ?? "",
2025
});
2126

22-
// Create a typed tool using createTool
27+
// Create a typed regular tool using tool()
2328
// The execute function params are automatically typed as z.infer<typeof inputSchema>
2429
// The return type is enforced based on outputSchema
25-
const weatherTool = createTool({
30+
const weatherTool = tool({
2631
name: "get_weather",
2732
description: "Get the current weather for a location",
2833
inputSchema: z.object({
@@ -43,9 +48,9 @@ const weatherTool = createTool({
4348
},
4449
});
4550

46-
// Create a generator tool with typed progress events
47-
// The eventSchema defines the type of events yielded during execution
48-
const searchTool = createGeneratorTool({
51+
// Create a generator tool with typed progress events by providing eventSchema
52+
// The eventSchema triggers generator mode - execute becomes an async generator
53+
const searchTool = tool({
4954
name: "search_database",
5055
description: "Search database with progress updates",
5156
inputSchema: z.object({

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export * from "./sdk/sdk.js";
1010

1111
// Tool creation helpers
1212
export {
13+
tool,
14+
// Deprecated - kept for backwards compatibility
1315
createTool,
1416
createGeneratorTool,
1517
createManualTool,

0 commit comments

Comments
 (0)