Skip to main content
The Edgee Rust SDK supports OpenAI-compatible function calling (tools), allowing models to request execution of functions you define. This enables models to interact with external APIs, databases, and your application logic.

Overview

Function calling works in two steps:
  1. Request: Send a request with tool definitions. The model may request to call one or more tools.
  2. Execute & Respond: Execute the requested functions and send the results back to the model.

Tool Definition

A tool is defined using the Tool struct:
use edgee::{Tool, FunctionDefinition, JsonSchema};
use std::collections::HashMap;

let tool = Tool::function(FunctionDefinition {
    name: "function_name".to_string(),
    description: Some("Function description".to_string()),
    parameters: JsonSchema {
        schema_type: "object".to_string(),
        properties: Some(HashMap::new()),
        required: Some(vec![]),
        description: None,
    },
});

FunctionDefinition

PropertyTypeDescription
name StringThe name of the function (must be unique, a-z, A-Z, 0-9, _, -)
descriptionOption<String>Description of what the function does. Highly recommended - helps the model understand when to use it
parametersJsonSchemaJSON Schema object describing the function parameters

Parameters Schema

The parameters field uses JSON Schema format via the JsonSchema struct:
use edgee::JsonSchema;
use std::collections::HashMap;

let parameters = JsonSchema {
    schema_type: "object".to_string(),
    properties: Some({
        let mut props = HashMap::new();
        props.insert("paramName".to_string(), serde_json::json!({
            "type": "string",
            "description": "Parameter description"
        }));
        props
    }),
    required: Some(vec!["paramName".to_string()]),
    description: None,
};
Example - Defining a Tool:
use edgee::{Edgee, Message, InputObject, Tool, FunctionDefinition, JsonSchema};
use std::collections::HashMap;

let client = Edgee::from_env()?;

let function = FunctionDefinition {
    name: "get_weather".to_string(),
    description: Some("Get the current weather for a location".to_string()),
    parameters: JsonSchema {
        schema_type: "object".to_string(),
        properties: Some({
            let mut props = HashMap::new();
            props.insert("location".to_string(), serde_json::json!({
                "type": "string",
                "description": "The city and state, e.g. San Francisco, CA"
            }));
            props.insert("unit".to_string(), serde_json::json!({
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }));
            props
        }),
        required: Some(vec!["location".to_string()]),
        description: None,
    },
};

let input = InputObject::new(vec![
    Message::user("What is the weather in Paris?")
])
.with_tools(vec![Tool::function(function)]);

let response = client.send("gpt-4o", input).await?;

Tool Choice

The tool_choice parameter controls when and which tools the model should call. In Rust, this is set using serde_json::Value:
ValueTypeDescription
"auto"serde_json::ValueLet the model decide whether to call tools (default)
"none"serde_json::ValueDon’t call any tools, even if provided
{"type": "function", "function": {"name": "function_name"}}serde_json::ValueForce the model to call a specific function
Example - Force a Specific Tool:
use serde_json::json;

let input = InputObject::new(vec![
    Message::user("What is the weather?")
])
.with_tools(vec![Tool::function(function)])
.with_tool_choice(json!({
    "type": "function",
    "function": {"name": "get_weather"}
}));

let response = client.send("gpt-4o", input).await?;
// Model will always call get_weather
Example - Disable Tool Calls:
use serde_json::json;

let input = InputObject::new(vec![
    Message::user("What is the weather?")
])
.with_tools(vec![Tool::function(function)])
.with_tool_choice(json!("none"));

let response = client.send("gpt-4o", input).await?;
// Model will not call tools, even though they're available

Tool Call Object Structure

When the model requests a tool call, you receive a ToolCall object in the response:
PropertyTypeDescription
idStringUnique identifier for this tool call
call_typeStringType of tool call (typically "function")
functionFunctionCallFunction call details
function.nameStringName of the function to call
function.argumentsStringJSON string containing the function arguments

Parsing Arguments

use serde_json;

if let Some(tool_calls) = response.tool_calls() {
    let tool_call = &tool_calls[0];
    let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
    // args is now a serde_json::Value
    println!("Location: {}", args["location"]);
}

Complete Example

Here’s a complete end-to-end example with error handling:
use edgee::{Edgee, Message, InputObject, Tool, FunctionDefinition, JsonSchema};
use std::collections::HashMap;
use serde_json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Edgee::from_env()?;

    // Define the weather function
    let function = FunctionDefinition {
        name: "get_weather".to_string(),
        description: Some("Get the current weather for a location".to_string()),
        parameters: JsonSchema {
            schema_type: "object".to_string(),
            properties: Some({
                let mut props = HashMap::new();
                props.insert("location".to_string(), serde_json::json!({
                    "type": "string",
                    "description": "The city name"
                }));
                props.insert("unit".to_string(), serde_json::json!({
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }));
                props
            }),
            required: Some(vec!["location".to_string()]),
            description: None,
        },
    };

    // Step 1: Initial request with tools
    let input = InputObject::new(vec![
        Message::user("What is the weather in Paris and Tokyo?")
    ])
    .with_tools(vec![Tool::function(function)]);

    let response1 = client.send("gpt-4o", input).await?;

    // Step 2: Execute all tool calls
    let mut messages = vec![
        Message::user("What is the weather in Paris and Tokyo?")
    ];

    // Add assistant's message
    if let Some(message) = response1.message() {
        messages.push(message.clone());
    }

    if let Some(tool_calls) = response1.tool_calls() {
        for tool_call in tool_calls {
            let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
            let result = get_weather(
                args["location"].as_str().unwrap(),
                args.get("unit").and_then(|v| v.as_str())
            );
            
            messages.push(Message::tool(
                tool_call.id.clone(),
                serde_json::to_string(&result)?
            ));
        }
    }

    // Step 3: Send results back
    let function2 = FunctionDefinition {
        name: "get_weather".to_string(),
        description: Some("Get the current weather for a location".to_string()),
        parameters: JsonSchema {
            schema_type: "object".to_string(),
            properties: Some({
                let mut props = HashMap::new();
                props.insert("location".to_string(), serde_json::json!({
                    "type": "string",
                    "description": "The city name"
                }));
                props.insert("unit".to_string(), serde_json::json!({
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"]
                }));
                props
            }),
            required: Some(vec!["location".to_string()]),
            description: None,
        },
    };

    let input2 = InputObject::new(messages)
        .with_tools(vec![Tool::function(function2)]);

    let response2 = client.send("gpt-4o", input2).await?;
    println!("{}", response2.text().unwrap_or(""));

    Ok(())
}

fn get_weather(location: &str, unit: Option<&str>) -> serde_json::Value {
    serde_json::json!({
        "location": location,
        "temperature": 15,
        "unit": unit.unwrap_or("celsius"),
        "condition": "sunny"
    })
}
Example - Multiple Tools: You can provide multiple tools and let the model choose which ones to call:
let get_weather_tool = Tool::function(get_weather_function);
let send_email_tool = Tool::function(send_email_function);

let input = InputObject::new(vec![
    Message::user("Get the weather in Paris and send an email about it")
])
.with_tools(vec![get_weather_tool, send_email_tool]);

let response = client.send("gpt-4o", input).await?;

Streaming with Tools

The stream() method also supports tools. For details about streaming, see the Stream Method documentation.
use tokio_stream::StreamExt;

let input = InputObject::new(vec![
    Message::user("What is the weather in Paris?")
])
.with_tools(vec![Tool::function(function)]);

let mut stream = client.stream("gpt-4o", input).await?;

while let Some(result) = stream.next().await {
    match result {
        Ok(chunk) => {
            if let Some(text) = chunk.text() {
                print!("{}", text);
            }
            
            // Check for tool calls in the delta
            if let Some(choice) = chunk.choices.first() {
                if let Some(tool_calls) = &choice.delta.tool_calls {
                    println!("\nTool calls detected: {:?}", tool_calls);
                }
            }
            
            if chunk.finish_reason() == Some("tool_calls") {
                println!("\nModel requested tool calls");
            }
        }
        Err(e) => eprintln!("Stream error: {}", e),
    }
}

Best Practices

1. Always Provide Descriptions

Descriptions help the model understand when to use each function:
// ✅ Good
let function = FunctionDefinition {
    name: "get_weather".to_string(),
    description: Some("Get the current weather conditions for a specific location".to_string()),
    parameters: JsonSchema { /* ... */ },
};

// ❌ Bad
let function = FunctionDefinition {
    name: "get_weather".to_string(),
    description: None,  // Missing description
    parameters: JsonSchema { /* ... */ },
};

2. Use Clear Parameter Names

// ✅ Good
properties.insert("location".to_string(), serde_json::json!({
    "type": "string",
    "description": "The city name"
}));

// ❌ Bad
properties.insert("loc".to_string(), serde_json::json!({
    "type": "string"
    // Unclear name, no description
}));

3. Mark Required Parameters

let parameters = JsonSchema {
    schema_type: "object".to_string(),
    properties: Some({
        let mut props = HashMap::new();
        props.insert("location".to_string(), serde_json::json!({
            "type": "string",
            "description": "City name"
        }));
        props.insert("unit".to_string(), serde_json::json!({
            "type": "string",
            "description": "Temperature unit"
        }));
        props
    }),
    required: Some(vec!["location".to_string()]),  // location is required, unit is optional
    description: None,
};

4. Handle Multiple Tool Calls

Models can request multiple tool calls in a single response. Use parallel execution when possible:
use futures::future;

if let Some(tool_calls) = response.tool_calls() {
    // Execute all tool calls in parallel
    let results: Vec<_> = future::join_all(
        tool_calls.iter().map(|tool_call| {
            let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
            let result = execute_function(&tool_call.function.name, &args)?;
            Ok((tool_call.id.clone(), result))
        })
    ).await;

    // Add all tool results to messages
    for (tool_call_id, result) in results {
        messages.push(Message::tool(
            tool_call_id,
            serde_json::to_string(&result)?
        ));
    }
}
Example - Handling Multiple Tool Calls:
// Step 2: Execute all tool calls
let mut messages = vec![
    Message::user("What is the weather in Paris and Tokyo?"),
];

if let Some(message) = response1.message() {
    messages.push(message.clone());
}

if let Some(tool_calls) = response1.tool_calls() {
    for tool_call in tool_calls {
        let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
        let result = get_weather(
            args["location"].as_str().unwrap(),
            args.get("unit").and_then(|v| v.as_str())
        );
        
        messages.push(Message::tool(
            tool_call.id.clone(),
            serde_json::to_string(&result)?
        ));
    }
}

5. Error Handling in Tool Execution

if let Some(tool_calls) = response.tool_calls() {
    for tool_call in tool_calls {
        match serde_json::from_str::<serde_json::Value>(&tool_call.function.arguments) {
            Ok(args) => {
                match execute_function(&tool_call.function.name, &args) {
                    Ok(result) => {
                        messages.push(Message::tool(
                            tool_call.id.clone(),
                            serde_json::to_string(&result)?
                        ));
                    }
                    Err(e) => {
                        // Send error back to model
                        messages.push(Message::tool(
                            tool_call.id.clone(),
                            serde_json::to_string(&serde_json::json!({
                                "error": e.to_string()
                            }))?
                        ));
                    }
                }
            }
            Err(e) => {
                eprintln!("Failed to parse arguments: {}", e);
            }
        }
    }
}

6. Keep Tools Available

Include tools in follow-up requests so the model can call them again if needed:
let input2 = InputObject::new(messages_with_tool_results)
    .with_tools(vec![
        // Keep the same tools available
        Tool::function(function)
    ]);

let response2 = client.send("gpt-4o", input2).await?;
Example - Checking for Tool Calls:
if let Some(tool_calls) = response.tool_calls() {
    // Model wants to call a function
    for tool_call in tool_calls {
        println!("Function: {}", tool_call.function.name);
        println!("Arguments: {}", tool_call.function.arguments);
    }
}
Example - Executing Functions and Sending Results:
// Execute the function
if let Some(tool_calls) = response.tool_calls() {
    let tool_call = &tool_calls[0];
    let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
    let weather_result = get_weather(
        args["location"].as_str().unwrap(),
        args.get("unit").and_then(|v| v.as_str())
    );

    // Send the result back
    let mut messages = vec![
        Message::user("What is the weather in Paris?"),
    ];
    
    // Include assistant's message with tool_calls
    if let Some(message) = response.message() {
        messages.push(message.clone());
    }
    
    messages.push(Message::tool(
        tool_call.id.clone(),
        serde_json::to_string(&weather_result)?
    ));

    let input2 = InputObject::new(messages)
        .with_tools(vec![Tool::function(function)]);

    let response2 = client.send("gpt-4o", input2).await?;
    println!("{}", response2.text().unwrap_or(""));
    // "The weather in Paris is 15°C and sunny."
}