Tool Calling (Function Calling)

OpenAI-Compatible Tool Calling

Captain supports OpenAI-compatible function calling, allowing your AI to use tools and execute code client-side. Tools are executed on the client side, giving you full control over security and data access.

Overview

Tool calling (also known as function calling) allows the AI model to request the execution of specific functions when it needs to perform actions like:

  • ๐Ÿงฎ Calculations: Perform complex math operations
  • ๐Ÿ” Data Retrieval: Look up information from databases or APIs
  • ๐Ÿ“Š Data Processing: Transform or analyze data
  • ๐ŸŒ External APIs: Call third-party services
  • ๐Ÿ“ File Operations: Read or write files (client-side)

How It Works

  1. Define tools with their parameters and descriptions
  2. Send request to Captain API with tools included
  3. Model decides if it needs to use a tool
  4. API returns tool call request (not executed yet)
  5. Client executes the tool locally
  6. Continue conversation with tool results (optional)

Client-Side Execution

Tools are never executed on Captainโ€™s servers. The API only returns tool call requests - you execute them in your own environment with full control.

Quick Start

Python Example

1from openai import OpenAI
2
3client = OpenAI(
4 base_url="https://api.runcaptain.com/v1",
5 api_key="your_api_key",
6 default_headers={"X-Organization-ID": "your_org_id"}
7)
8
9# Define a tool
10tools = [{
11 "type": "function",
12 "function": {
13 "name": "get_weather",
14 "description": "Get current weather for a location",
15 "parameters": {
16 "type": "object",
17 "properties": {
18 "location": {
19 "type": "string",
20 "description": "City name, e.g. San Francisco"
21 },
22 "unit": {
23 "type": "string",
24 "enum": ["celsius", "fahrenheit"]
25 }
26 },
27 "required": ["location"]
28 },
29 "strict": True
30 }
31}]
32
33# Make request with tools
34response = client.chat.completions.create(
35 model="captain-voyager-latest",
36 messages=[
37 {"role": "user", "content": "What's the weather in Tokyo?"}
38 ],
39 tools=tools
40)
41
42# Check if model wants to use a tool
43if response.choices[0].finish_reason == "tool_calls":
44 tool_call = response.choices[0].message.tool_calls[0]
45
46 # Execute tool client-side
47 import json
48 args = json.loads(tool_call.function.arguments)
49 result = get_weather(args["location"], args.get("unit", "celsius"))
50
51 print(f"Tool used: {tool_call.function.name}")
52 print(f"Result: {result}")

JavaScript/TypeScript with Vercel AI SDK

1import { createOpenAI } from '@ai-sdk/openai';
2import { generateText } from 'ai';
3import { z } from 'zod';
4
5const captain = createOpenAI({
6 apiKey: process.env.CAPTAIN_API_KEY,
7 baseURL: 'https://api.runcaptain.com/v1',
8 headers: {
9 'X-Organization-ID': process.env.CAPTAIN_ORG_ID
10 }
11});
12
13const result = await generateText({
14 model: captain.chat('captain-voyager-latest'),
15 messages: [
16 { role: 'user', content: "What's the weather in Tokyo?" }
17 ],
18 tools: {
19 getWeather: {
20 description: 'Get current weather for a location',
21 parameters: z.object({
22 location: z.string().describe('City name'),
23 unit: z.enum(['celsius', 'fahrenheit']).optional()
24 }),
25 execute: async ({ location, unit }) => {
26 // Execute tool client-side
27 const weather = await fetchWeather(location, unit);
28 return weather;
29 }
30 }
31 },
32 maxSteps: 5 // Allow multiple tool calls
33});
34
35console.log(result.text);

Complete Examples

Python: Calculator Tool

1from openai import OpenAI
2import json
3
4client = OpenAI(
5 base_url="https://api.runcaptain.com/v1",
6 api_key="your_api_key",
7 default_headers={"X-Organization-ID": "your_org_id"}
8)
9
10def calculate(operation, a, b):
11 """Execute calculation client-side"""
12 operations = {
13 "add": lambda x, y: x + y,
14 "subtract": lambda x, y: x - y,
15 "multiply": lambda x, y: x * y,
16 "divide": lambda x, y: x / y if y != 0 else "Error: Division by zero"
17 }
18 return {"result": operations[operation](a, b)}
19
20tools = [{
21 "type": "function",
22 "function": {
23 "name": "calculate",
24 "description": "Perform arithmetic calculations",
25 "parameters": {
26 "type": "object",
27 "properties": {
28 "operation": {
29 "type": "string",
30 "enum": ["add", "subtract", "multiply", "divide"],
31 "description": "The operation to perform"
32 },
33 "a": {"type": "number", "description": "First number"},
34 "b": {"type": "number", "description": "Second number"}
35 },
36 "required": ["operation", "a", "b"]
37 },
38 "strict": True
39 }
40}]
41
42response = client.chat.completions.create(
43 model="captain-voyager-latest",
44 messages=[
45 {"role": "system", "content": "You are a helpful assistant with access to a calculator."},
46 {"role": "user", "content": "What is 156 multiplied by 243?"}
47 ],
48 tools=tools
49)
50
51if response.choices[0].finish_reason == "tool_calls":
52 tool_call = response.choices[0].message.tool_calls[0]
53 args = json.loads(tool_call.function.arguments)
54
55 # Execute tool
56 result = calculate(args["operation"], args["a"], args["b"])
57 print(f"Calculation result: {result['result']}")

TypeScript: Database Query Tool

1import { createOpenAI } from '@ai-sdk/openai';
2import { generateText } from 'ai';
3import { z } from 'zod';
4
5const captain = createOpenAI({
6 apiKey: process.env.CAPTAIN_API_KEY!,
7 baseURL: 'https://api.runcaptain.com/v1',
8 headers: {
9 'X-Organization-ID': process.env.CAPTAIN_ORG_ID!
10 }
11});
12
13// Tool definition with execution
14const tools = {
15 queryDatabase: {
16 description: 'Query the customer database',
17 parameters: z.object({
18 customerId: z.string().describe('Customer ID to look up'),
19 fields: z.array(z.string()).describe('Fields to retrieve')
20 }),
21 execute: async ({ customerId, fields }) => {
22 // Execute query client-side (your database)
23 const customer = await db.customers.findOne({ id: customerId });
24
25 return {
26 customer: {
27 id: customer.id,
28 ...fields.reduce((acc, field) => ({
29 ...acc,
30 [field]: customer[field]
31 }), {})
32 }
33 };
34 }
35 }
36};
37
38const result = await generateText({
39 model: captain.chat('captain-voyager-latest'),
40 messages: [
41 {
42 role: 'user',
43 content: 'Get the email and phone number for customer CUST-12345'
44 }
45 ],
46 tools,
47 maxSteps: 5
48});
49
50console.log(result.text);

Tool Calling with Context

Combine tool calling with Captainโ€™s infinite context processing:

Python: Analyze Document with Tools

1from openai import OpenAI
2
3client = OpenAI(
4 base_url="https://api.runcaptain.com/v1",
5 api_key="your_api_key",
6 default_headers={"X-Organization-ID": "your_org_id"}
7)
8
9# Read large document
10with open('financial_report.txt', 'r') as f:
11 document = f.read()
12
13tools = [{
14 "type": "function",
15 "function": {
16 "name": "calculate_total",
17 "description": "Calculate sum of numbers",
18 "parameters": {
19 "type": "object",
20 "properties": {
21 "numbers": {
22 "type": "array",
23 "items": {"type": "number"},
24 "description": "List of numbers to sum"
25 }
26 },
27 "required": ["numbers"]
28 },
29 "strict": True
30 }
31}]
32
33response = client.chat.completions.create(
34 model="captain-voyager-latest",
35 messages=[
36 {"role": "user", "content": "What is the total revenue from all quarters?"}
37 ],
38 tools=tools,
39 extra_body={
40 "captain": {
41 "context": document # Large document context
42 }
43 }
44)
45
46if response.choices[0].finish_reason == "tool_calls":
47 tool_call = response.choices[0].message.tool_calls[0]
48 args = json.loads(tool_call.function.arguments)
49
50 # Execute calculation
51 total = sum(args["numbers"])
52 print(f"Total revenue: ${total:,.2f}")

TypeScript: Search Documents with API Calls

1import { createOpenAI } from '@ai-sdk/openai';
2import { generateText } from 'ai';
3import { z } from 'zod';
4
5const captain = createOpenAI({
6 apiKey: process.env.CAPTAIN_API_KEY!,
7 baseURL: 'https://api.runcaptain.com/v1',
8 headers: {
9 'X-Organization-ID': process.env.CAPTAIN_ORG_ID!
10 }
11});
12
13const largeDataset = await fs.readFile('company_data.json', 'utf-8');
14
15const result = await generateText({
16 model: captain.chat('captain-voyager-latest'),
17 messages: [
18 {
19 role: 'user',
20 content: 'Find all employees in the Engineering department and get their current projects from the API'
21 }
22 ],
23 tools: {
24 getProjectDetails: {
25 description: 'Get project details from external API',
26 parameters: z.object({
27 projectId: z.string()
28 }),
29 execute: async ({ projectId }) => {
30 const response = await fetch(
31 `https://api.company.com/projects/${projectId}`
32 );
33 return await response.json();
34 }
35 }
36 },
37 extra_body: {
38 captain: {
39 context: largeDataset
40 }
41 },
42 maxSteps: 10
43});
44
45console.log(result.text);

Advanced Patterns

Multi-Step Tool Calling

Use Vercel AI SDKโ€™s maxSteps for automatic multi-turn conversations:

1const result = await generateText({
2 model: captain.chat('captain-voyager-latest'),
3 messages: [
4 {
5 role: 'user',
6 content: 'Calculate quarterly revenue growth rate'
7 }
8 ],
9 tools: {
10 getRevenue: {
11 description: 'Get revenue for a specific quarter',
12 parameters: z.object({
13 quarter: z.string(),
14 year: z.number()
15 }),
16 execute: async ({ quarter, year }) => {
17 return { revenue: await fetchRevenue(quarter, year) };
18 }
19 },
20 calculateGrowthRate: {
21 description: 'Calculate percentage growth between two values',
22 parameters: z.object({
23 previous: z.number(),
24 current: z.number()
25 }),
26 execute: async ({ previous, current }) => {
27 const growth = ((current - previous) / previous) * 100;
28 return { growthRate: growth };
29 }
30 }
31 },
32 maxSteps: 10 // Allow multiple tool calls in sequence
33});
34
35console.log(result.text);
36console.log(`Tools used: ${result.toolCalls?.length || 0}`);

Error Handling

1from openai import OpenAI
2import json
3
4client = OpenAI(
5 base_url="https://api.runcaptain.com/v1",
6 api_key="your_api_key",
7 default_headers={"X-Organization-ID": "your_org_id"}
8)
9
10def safe_execute_tool(tool_name, args):
11 """Execute tool with error handling"""
12 try:
13 if tool_name == "calculate":
14 return calculate(args["operation"], args["a"], args["b"])
15 elif tool_name == "query_db":
16 return query_database(args["query"])
17 else:
18 return {"error": f"Unknown tool: {tool_name}"}
19 except Exception as e:
20 return {"error": str(e)}
21
22response = client.chat.completions.create(
23 model="captain-voyager-latest",
24 messages=[{"role": "user", "content": "What is 100 divided by 0?"}],
25 tools=tools
26)
27
28if response.choices[0].finish_reason == "tool_calls":
29 for tool_call in response.choices[0].message.tool_calls:
30 args = json.loads(tool_call.function.arguments)
31 result = safe_execute_tool(tool_call.function.name, args)
32
33 if "error" in result:
34 print(f"Tool execution failed: {result['error']}")
35 else:
36 print(f"Tool result: {result}")

API Reference

Tool Definition Format

1{
2 "type": "function",
3 "function": {
4 "name": "function_name", # Required: Tool name
5 "description": "What it does", # Required: Clear description
6 "parameters": { # Required: JSON Schema
7 "type": "object",
8 "properties": {
9 "param1": {
10 "type": "string",
11 "description": "Param description"
12 }
13 },
14 "required": ["param1"]
15 },
16 "strict": True # Required: Must be True
17 }
18}

Response Format

When the model wants to use a tool:

1{
2 "id": "chatcmpl-...",
3 "choices": [{
4 "finish_reason": "tool_calls",
5 "message": {
6 "role": "assistant",
7 "content": null,
8 "tool_calls": [{
9 "id": "call_abc123",
10 "type": "function",
11 "function": {
12 "name": "tool_name",
13 "arguments": "{\"param\": \"value\"}"
14 }
15 }]
16 }
17 }]
18}

Parameters

ParameterTypeDescription
toolsarrayList of tool definitions (OpenAI format)
tool_choicestring"auto" (default), "none", or specific tool
extra_body.captain.contextstringOptional large context for tool to analyze

Best Practices

1. Clear Tool Descriptions

1# Good
2"description": "Calculate the sum of two numbers. Use this when you need to perform addition."
3
4# Bad
5"description": "Does math"

2. Strict Parameter Schemas

1# Good - Explicit types and descriptions
2"parameters": {
3 "type": "object",
4 "properties": {
5 "amount": {
6 "type": "number",
7 "description": "Dollar amount to process"
8 },
9 "currency": {
10 "type": "string",
11 "enum": ["USD", "EUR", "GBP"],
12 "description": "Three-letter currency code"
13 }
14 },
15 "required": ["amount", "currency"]
16}

3. Security Considerations

1# Always validate tool inputs
2def execute_tool(tool_name, args):
3 # Validate tool exists
4 if tool_name not in ALLOWED_TOOLS:
5 raise ValueError(f"Tool {tool_name} not allowed")
6
7 # Validate arguments
8 if not validate_args(tool_name, args):
9 raise ValueError("Invalid arguments")
10
11 # Execute with proper permissions
12 return ALLOWED_TOOLS[tool_name](**args)

4. Handle Empty Arguments

1# Model might return incomplete arguments
2if response.choices[0].finish_reason == "tool_calls":
3 tool_call = response.choices[0].message.tool_calls[0]
4
5 try:
6 args = json.loads(tool_call.function.arguments)
7
8 # Check for required parameters
9 if not all(k in args for k in required_params):
10 print("Warning: Missing required parameters")
11 # Provide defaults or skip
12
13 except json.JSONDecodeError:
14 print("Warning: Invalid tool arguments")

Limitations

Current Limitations

  1. Multi-Turn Continuation: Sending tool results back for final answer requires frameworks like Vercel AI SDK with maxSteps 2. Model Parameter Extraction: Model may occasionally return empty/incomplete arguments (provide clear descriptions) 3. Server-Side Execution: Tools are client-side only - cannot execute on Captainโ€™s servers

Framework Support

FrameworkSupport LevelMulti-TurnRecommended
OpenAI Python SDKโœ… FullManualโญ For Python
OpenAI Node.js SDKโœ… FullManualโญ For Node.js
Vercel AI SDKโœ… FullAutomatic (maxSteps)โญ Best DX
LangChainโœ… CompatibleVia agentsโœ“
Custom RESTโœ… FullManualโœ“

Troubleshooting

Tool Not Being Called

1# Make description more explicit
2"description": "ALWAYS use this tool for calculations. Never calculate manually."
3
4# Add stronger system prompt
5messages = [
6 {
7 "role": "system",
8 "content": "You MUST use the provided tools. Never perform calculations yourself."
9 },
10 {"role": "user", "content": "What is 50 + 75?"}
11]

Empty Tool Arguments

1# Check for empty args and provide defaults
2args = json.loads(tool_call.function.arguments)
3
4if not args or not args.get("required_param"):
5 # Provide sensible defaults
6 args = {
7 "required_param": "default_value",
8 **args
9 }

Multi-Turn Issues

1// Use Vercel AI SDK with maxSteps for automatic handling
2const result = await generateText({
3 model: captain.chat('captain-voyager-latest'),
4 messages: [...],
5 tools: {...},
6 maxSteps: 5 // Handles multi-turn automatically
7});

Next Steps

Support

Need help with tool calling?