open_ai_rust_fn_call_extension
Ergonomic procedural macros for building OpenAI function-calling tools in
Rust. The companion crate to open_ai_rust.
Describe Rust types and async functions as structured OpenAI tool schemas with zero boilerplate. Three macros, one purpose: make tool calling feel native.
Highlights
- Derive JSON schemas from any Rust type —
#[derive(FunctionCall)]generates a staticFunctionCallableimpl withschema_type()andfn_schema()methods. No instance required. - Annotate functions with their tool schema —
#[function_call("...")]emits aconstschema next to your function. - One-line tool registration —
#[tool("...")]adds a JSON-dispatch wrapper that deserialises arguments, calls your function (sync orasync), and serialises the result. With thetool_registryfeature it also auto-registers into a global tool registry — no manual wiring. Option<T>→required: false— optionality flows naturally into the JSON schema.Result<T, E>returns unwrapped automatically — your JSON payload isT, not{"Ok": T}.- Doc-comment descriptions —
///on items becomes the description in the schema if you don't override it. - Async- and generics-aware — the wrapped function's signature is preserved verbatim.
Quick start
[]
= "1"
= "0.3"
= { = "1", = ["derive"] }
= "1"
use FunctionCallable;
use ;
/// A 2D point.
/// Compute the Euclidean distance between two points.
async
That's it. You now have:
Point::fn_schema()— a fullFunctionCalldescribingPointas a JSON schema object with three parameters (x,y,labelwithrequired: false).Point::schema_type()— the same as aFunctionType::Object(...), suitable for nesting inside other schemas.const DISTANCE: FunctionCallRaw<'static>— the function's tool schema as aconst-evaluable value.fn distance_dispatch(args: serde_json::Value) -> Pin<Box<dyn Future<...>>>— a typed dispatch wrapper, ready to plug into an OpenAI tool-call loop.
The three macros
#[derive(FunctionCall)]
Derive FunctionCallable for a struct or unit-variant enum.
use FunctionCall;
/// A widget.
Field attributes:
| Attribute | Effect |
|---|---|
#[function_call(skip)] |
omit field from the schema |
#[function_call(rename = "...")] |
rename in the schema |
#[function_call(description = "...")] |
explicit description |
/// ... |
doc-comment fallback for description |
Container attribute:
| Attribute | Effect |
|---|---|
#[function_call(crate = "::path")] |
override the module path that owns FunctionCallable (defaults to ::open_ai_rust::logoi::input::tool::raw_macro) |
Generated methods:
| Method | Returns | Use case |
|---|---|---|
schema_type() |
FunctionType |
nest the type inside another schema |
fn_schema() |
FunctionCall |
pass directly to tool registration |
Supported shapes: named-field structs, tuple structs (fields named
_0, _1, …), unit structs, unit-variant enums, and generic types.
Enums with data variants and unions produce a clear compile error.
#[function_call("description")]
Emit a const schema next to a free function.
use function_call;
/// Toggle a smart-home light.
Expansion (alongside the original function, untouched):
const CHANGE_LIGHT: = FunctionCallRaw ;
async fn, generics, visibility modifiers, and unsafe all pass through
untouched.
Note on per-parameter descriptions. Stable Rust rejects
#[doc]and///doc-comments on function parameters, so the only way to attach a description to a parameter is the#[function_call_description = "..."]attribute shown above. The macro consumes it and strips it from the emitted function signature.
#[tool("description")]
A superset of #[function_call]: the same schema const, plus a typed
JSON-dispatch wrapper and (optionally) global auto-registration.
use tool;
/// Safely divide two integers.
Expansion adds:
The wrapper:
- Pulls each parameter from
args[<name>], defaulting toValue::Null. - Deserialises each into its declared Rust type via
serde_json::from_value. - Calls the function (awaiting it if
async). - For
Result<T, E>returns: unwraps toTonOk, returns the rendered error onErr. For plain returns: serialises directly.
Global auto-registration (tool_registry feature)
Enable the feature in both this crate and open_ai_rust:
[]
= { = "1", = ["tool_registry"] }
= { = "0.3", = ["tool_registry"] }
Each #[tool]-annotated function is then linked into
open_ai_rust::tool_registry::TOOLS at build time. Dispatch by name:
let result = invoke_tool.await?;
No manual registration code. Tools are discovered across crate boundaries
via linkme.
Description sources
| Item kind | Priority order |
|---|---|
Function (#[function_call] / #[tool]) |
"..." arg → /// doc-comment → omitted |
| Struct / unit enum | /// doc-comment → omitted |
| Derive field | #[function_call(description = "...")] → /// doc-comment → omitted |
| Function parameter | #[function_call_description = "..."] → omitted (no #[doc] allowed on params in stable Rust) |
Option<T> and the required flag
When you derive FunctionCall on a struct, each emitted FunctionParameter
carries a required: bool flag:
field: T→required: truefield: Option<T>→required: false
This matches OpenAI's strict-schema convention: optionality lives in the
top-level required array, not in the parameter's type.
Feature flags
| Feature | Default | Effect |
|---|---|---|
tool_registry |
off | Auto-register #[tool] functions into open_ai_rust::tool_registry::TOOLS via linkme. Requires open_ai_rust/tool_registry. |
Compatibility
- MSRV: Rust 1.65 (uses
let ... else). syn: 2.x.open_ai_rust: ≥ 1.1.