LLM Tool Calling#
Overview#
Sure integrates with Anthropic and OpenAI for chat, streaming, and function/tool calling — the mechanism by which the AI assistant can invoke application functions (e.g., create_category, get_transactions) and feed results back into the conversation. The architecture is provider-agnostic: shared data structures in Provider::LlmConcept allow both Anthropic and OpenAI backends to flow through the same downstream execution and persistence logic.
Provider Selection#
Provider::Registry resolves which LLM backend to use. preferred_llm_provider checks Setting.llm_provider and tries providers in order — defaulting to Anthropic-first if configured, OpenAI-first otherwise — falling back to whichever has valid credentials. Returns nil if neither is configured; callers must guard on that.
Credentials are read from env vars (ANTHROPIC_ACCESS_TOKEN / ANTHROPIC_API_KEY, OPENAI_ACCESS_TOKEN) or Setting DB values .
Core Data Structures (Provider::LlmConcept)#
Provider::LlmConcept defines the provider-agnostic value objects used throughout the tool-calling pipeline :
| Struct | Fields | Purpose |
|---|---|---|
ChatMessage | id, output_text | Text content from the LLM |
ChatStreamChunk | type, data, usage | Streaming event (types: output_text, response) |
ChatResponse | id, model, messages, function_requests | Final or streamed response |
ChatFunctionRequest | id, call_id, function_name, function_args | One tool call requested by the LLM |
The chat_response interface is what every provider implements. Key parameters: functions (tool definitions), function_results (prior round results), streamer (callback proc for streaming), previous_response_id (for stateful OpenAI Responses API turns).
Tool/Function Definition (Assistant::Function)#
Assistant::Function is the abstract base class every callable tool subclasses. Subclasses must implement:
self.name/self.description— used in the function definition sent to the LLMcall(params)— executed when the LLM requests the functionparams_schema(viabuild_schema) — JSON Schema object describing parameters;strict_mode?defaults totrue, requiring all properties inrequired
to_definition serializes the function into { name:, description:, params_schema:, strict: } , which is forwarded to providers by FunctionToolCaller#function_definitions.
Concrete implementations live in app/models/assistant/function/ (e.g., Assistant::Function::CreateCategory).
Request Building: Anthropic (ChatConfig)#
Provider::Anthropic::ChatConfig assembles the Anthropic API request:
- System prompt is wrapped with
cache_control: { type: "ephemeral" }for prompt caching — ~10× cheaper on cache hits . - Tool definitions are built from
functionsdefinitions; the last tool block also getscache_controlto cache tool definitions . - The
strictkey (OpenAI-only) is stripped from schemas before forwarding to Anthropic .
Response Parsing: Anthropic (ChatParser)#
Provider::Anthropic::ChatParser maps raw Anthropic SDK message objects to ChatResponse:
- Text blocks (
type == :text) →ChatMessagewith joined output text - Tool-use blocks (
type == :tool_use) →ChatFunctionRequest;inputis serialized to JSON if not already a String - Handles both object-style (
.respond_to?(:key)) and hash-style blocks for SDK version flexibility
Tool Execution (FunctionToolCaller)#
Assistant::FunctionToolCaller receives the list of ChatFunctionRequest objects from the parsed response and executes them:
- Looks up the matching
Assistant::Functioninstance by name JSON.parsesfunction_argsand callsfn.call(fn_args)- Wraps the result in a
ToolCall::Functionrecord viafrom_function_request - Any exception is re-raised as
FunctionExecutionErrorwith function name + args in the message
function_definitions serializes all registered functions via to_definition for inclusion in the next API request.
Persistence (ToolCall::Function)#
ToolCall::Function is the ActiveRecord model persisting each tool invocation:
from_function_request(function_request, result)— creates the record from the LLM's request + Ruby execution resultto_result— formats the result for injection into the next LLM turnto_tool_call— formats the call in the OpenAItool_callsmessage format for conversation history replay
Orchestration: Multi-Turn Tool Calling (Assistant::Responder)#
Assistant::Responder drives the full round-trip:
- Sends the initial request to
llm.chat_response(...)with astreamerproc - The streamer emits
:output_textevents (text chunks) and:responseevents (final message) to registered listeners - If the response contains
function_requests, callshandle_follow_up_response:- Executes all requested functions via
FunctionToolCaller#fulfill_requests - Emits
:responsewithfunction_tool_callsfor persistence - Issues a follow-up
chat_responsewithfunction_resultsinjected
- Executes all requested functions via
- Recursion is intentionally blocked: follow-up responses ignore further
function_requeststo prevent runaway LLM spend
For conversation history, Anthropic uses raw Message records (chat_message_records); OpenAI uses a pre-built role/tool_call_id payload (openai_messages_payload) .
Key File Index#
| File | Role |
|---|---|
provider/llm_concept.rb | Provider-agnostic data structures & chat_response interface |
provider/registry.rb | Provider selection / credential resolution |
provider/anthropic/chat_config.rb | Anthropic request builder (tools, prompt caching) |
provider/anthropic/chat_parser.rb | Anthropic response → ChatResponse / ChatFunctionRequest |
assistant/function.rb | Abstract base for all tool definitions |
assistant/function_tool_caller.rb | Tool dispatch, arg parsing, error wrapping |
tool_call/function.rb | Persistence + serialization of tool results |
assistant/responder.rb | Multi-turn orchestration, streaming events, follow-up loop |