Packages
req_llm
1.18.0
1.28.0
1.27.0
1.26.0
1.25.0
1.24.0
1.23.0
1.22.0
1.21.1
1.21.0
1.20.0
1.19.0
1.18.0
1.17.1
1.17.0
1.16.0
1.15.0
1.14.0
1.13.0
1.12.0
1.11.0
1.10.0
1.9.0
1.8.0
1.7.1
1.7.0
1.6.0
1.5.1
1.5.0
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
Composable Elixir library for LLM interactions built on Req & Finch
Current section
Files
Jump to
Current section
Files
guides/data-structures.md
# Data Structures
## Overview
ReqLLM's canonical data model is the foundation for provider-agnostic AI interactions. It normalizes provider differences by enforcing a small, consistent set of structs that represent models, conversations, tools, responses, and streaming.
## Hierarchy
```
LLMDB.Model # Canonical model metadata struct used by ReqLLM
↓
ReqLLM.Context # Conversation history
↓
ReqLLM.Message # A turn in the conversation
↓
ReqLLM.Message.ContentPart # Typed content (text, images, files, tool calls, results)
↓
ReqLLM.Tool # Tool definitions (name, description, schema)
↓
ReqLLM.StreamChunk # Streaming events (content, tool_call, thinking, meta)
↓
ReqLLM.Response # Canonical final response with usage and helpers
↓
ReqLLM.StreamResponse # Streaming handle with helpers
```
## Design goals
- **Provider-agnostic**: One set of types works across Anthropic, OpenAI, Google, etc.
- **Typed and explicit**: Discriminated unions for content; consistent fields, no surprises.
- **Composable and immutable**: Build contexts and messages with simple, predictable APIs.
- **Extensible**: Metadata fields and new content types can be added without breaking shape.
## 1) LLMDB.Model
Represents a model choice for a specific provider plus optional routing and capability metadata.
**Typical fields**:
- `provider`: atom, e.g., `:anthropic`
- `id`: string, e.g., `"claude-haiku-4-5"`
- `provider_model_id`: optional provider-facing wire ID
- `base_url`: optional per-model endpoint metadata
- `capabilities`, `limits`, `modalities`, `cost`, `pricing`, `extra`: optional metadata (often sourced from LLMDB)
**Constructors**:
```elixir
{:ok, model} = ReqLLM.model("anthropic:claude-haiku-4-5")
model =
ReqLLM.model!(%{
provider: :openai,
id: "gpt-6-mini",
base_url: "http://localhost:8000/v1"
})
# Direct struct creation if you need full control
model = LLMDB.Model.new!(%{
provider: :anthropic,
id: "claude-3-5-sonnet-20241022",
capabilities: %{tool_call: true},
modalities: %{input: [:text, :image], output: [:text]},
cost: %{input: 3.0, output: 15.0}
})
```
**How this supports normalization**:
- One way to specify models across providers.
- Common options are normalized; provider-specific options are translated by the provider adapter.
- See the [Model Specs](model-specs.md) guide for the full model-spec resolution rules and explicit model-specification path.
## 2) ReqLLM.Context
A conversation wrapper around a list of `Message` structs. Implements `Enumerable` and `Collectable` for ergonomic manipulation.
**Constructors and helpers**:
```elixir
import ReqLLM.Context
alias ReqLLM.Message.ContentPart
context = Context.new([
system("You are a helpful assistant."),
user("Summarize this document."),
user([
ContentPart.file(pdf_data, "report.pdf", "application/pdf")
])
])
```
**How this supports normalization**:
- One conversation format for all providers (no provider-specific role/content layouts).
- Multimodal content is embedded uniformly via `ContentPart`.
## 3) ReqLLM.Message
Represents one conversational turn with a role and a list of `ContentPart` items.
**Typical fields**:
- `role`: `:system` | `:user` | `:assistant` | `:tool` (when appropriate)
- `content`: list of `ContentPart`
**Examples**:
```elixir
alias ReqLLM.Message.ContentPart
msg = %ReqLLM.Message{
role: :user,
content: [ContentPart.text("Hello!")]
}
```
**How this supports normalization**:
- Every message has a uniform shape; multimodality is handled by `ContentPart` rather than provider-specific message types.
## 4) ReqLLM.Message.ContentPart
Typed content elements that compose a `Message`. Common variants:
- `text/1`: `ContentPart.text("...")`
- `text/2`: `ContentPart.text("...", metadata)` with metadata map
- `image_url/1`: `ContentPart.image_url("https://...")`
- `image_url/2`: `ContentPart.image_url("https://...", metadata)` with metadata
- `image/2`: `ContentPart.image(binary, "image/png")`
- `image/3`: `ContentPart.image(binary, "image/png", metadata)` with metadata
- `file/3`: `ContentPart.file(binary, "name.ext", "mime/type")`
- `file_id/1`: `ContentPart.file_id("file_123")` for a legacy unowned provider reference
- `owned_file_id/3`: `ContentPart.owned_file_id("file_123", :openai, purpose: :assistants)`
for an explicitly provider-owned reference
- `thinking/1`: `ContentPart.thinking("...")` for models that expose reasoning tokens
- `tool_call/2`: `ContentPart.tool_call("name", %{arg: "value"})` for assistant-issued calls
- `tool_result/2`: `ContentPart.tool_result("tool_call_id", %{...})` for tool outputs
**Example**:
```elixir
parts = [
ContentPart.text("Analyze:"),
ContentPart.image_url("https://example.com/chart.png")
]
```
**Metadata field**:
The `metadata` field allows passing provider-specific attributes through to the wire format. Currently supported metadata keys:
- `cache_control`: Anthropic prompt caching control (e.g., `%{type: "ephemeral"}`)
```elixir
# Enable prompt caching for text content
cached_text = ContentPart.text(
"Long system prompt to cache...",
%{cache_control: %{type: "ephemeral"}}
)
# Enable prompt caching for images
cached_image = ContentPart.image_url(
"https://example.com/large-diagram.png",
%{cache_control: %{type: "ephemeral"}}
)
# Or with binary image data
cached_binary_image = ContentPart.image(
image_data,
"image/png",
%{cache_control: %{type: "ephemeral"}}
)
```
### Provider-owned file references
`owned_file_id/3` adds lifecycle information without adding fields to
`ContentPart` or changing `file_id/1`. Ownership lives under the reserved
`"req_llm" -> "provider_file"` metadata namespace:
```elixir
owned_file =
ContentPart.owned_file_id("file_123", :openai,
media_type: "application/pdf",
purpose: :assistants,
status: :processed,
expires_at: ~U[2030-01-01 00:00:00Z],
size: 12_345,
sha256: "...",
provider_metadata: %{tenant: "documentation"}
)
```
ReqLLM validates only explicitly owned references. Using an owned file with a
different provider, or using it after a known `expires_at`, returns
`ReqLLM.Error.Invalid.ProviderFileReference` before a model request starts.
Plain `file_id/1` references remain unowned and retain their existing routing,
encoding, errors, struct value, JSON, and inspection behavior.
OpenAI and Anthropic encode a matching owned reference through their existing
file-ID wire format. Google encodes the reference ID as a Gemini `fileData`
URI. The constructor does not upload data, read local paths, or infer ownership
from an ID prefix. OpenAI callers that need an upload lifecycle can use the
provider-scoped `ReqLLM.Providers.OpenAI.Files` module; upload support is not
part of the common provider behaviour.
`ContentPart.provider_file_reference/1` returns the complete persisted record.
Treat it as sensitive because it contains the provider reference ID. Use
`ContentPart.inspect_provider_file/1` for a redacted diagnostic value; regular
`Inspect` and telemetry also redact IDs, URLs, and credential-like metadata for
owned references.
**How this supports normalization**:
- Discriminated union eliminates polymorphism across providers.
- New content types can be added without changing the `Message` shape.
- Metadata enables provider-specific features without breaking the canonical model.
## 5) ReqLLM.Tool
Defines callable functions (aka "tools" or "function calling") with validation.
**Typical fields**:
- `name`: string
- `description`: string
- `parameter_schema`: `NimbleOptions`-based schema for argument validation
- `callback`: function or MFA tuple to execute the tool
**Example**:
```elixir
{:ok, tool} = ReqLLM.Tool.new(
name: "get_weather",
description: "Gets weather by city",
parameter_schema: [city: [type: :string, required: true]],
callback: fn %{city: city} -> {:ok, "Weather in #{city}: sunny"} end
)
# Execute locally (e.g., after a model issues a tool_call)
{:ok, result} = ReqLLM.Tool.execute(tool, %{"city" => "NYC"})
```
**How this supports normalization**:
- One tool definition is used across providers that support function/tool calling.
- Tool calls/results appear in `ContentPart` and `StreamChunk` the same way for all providers.
- Structured tool results should be represented in the content body as JSON when
they carry model-visible semantics like `%{ok: true, result: ...}` or
`%{ok: false, error: ...}`. Message metadata can preserve the original native
output for adapters and local consumers, but it should not be the only source
of meaning for follow-up model turns.
### Tool calls and provider-executed builtins
`ReqLLM.ToolCall` represents assistant tool-call requests. Most tool calls are local work your app should execute, but some providers also return server-side builtin calls (for example OpenAI Responses API `web_search_call`). ReqLLM preserves those with `ReqLLM.ToolCall.new_builtin/3`; detect them with `ReqLLM.ToolCall.builtin?/1` and do not replay them as local tools.
### Continuing with matched tool results
`ReqLLM.Context.append_tool_exchange/3` appends a canonical assistant message
and its tool-result messages without executing tools or making another model
call. It validates IDs and names atomically, ignores provider-executed
builtins, and orders results to match the assistant calls. Provider-native
calls remain explicit and require matching results:
```elixir
results = [
ReqLLM.Context.tool_result("call_2", "get_time", "10:00 CEST"),
ReqLLM.Context.tool_result("call_1", "get_weather", "72°F and sunny")
]
{:ok, continued_context} =
ReqLLM.Context.append_tool_exchange(input_context, response, results)
{:ok, next_response} =
ReqLLM.generate_text(model, continued_context, tools: tools)
```
The second model call remains explicit and application-owned. The helper also
accepts `response.message`; when passed `response.context`, it recognizes the
assistant message already present and appends only the matched results.
## 6) ReqLLM.StreamChunk
Unified streaming event payloads emitted during `stream_text`.
**Common chunk types**:
- `:content` — text tokens or content fragments
- `:thinking` — reasoning tokens (if provider exposes them)
- `:tool_call` — a call intent with name and arguments
- `:meta` — metadata such as `finish_reason`, usage deltas, etc.
**Example**:
```elixir
%ReqLLM.StreamChunk{type: :content, text: "Hello"}
%ReqLLM.StreamChunk{type: :tool_call, name: "get_weather", arguments: %{city: "NYC"}}
%ReqLLM.StreamChunk{type: :meta, metadata: %{finish_reason: "stop"}}
```
**How this supports normalization**:
- All providers' streaming formats are mapped into this single, consistent event model.
## 7) ReqLLM.Response
Canonical final response returned by non-streaming calls (and available after streaming completes, when applicable).
**Typical fields and helpers**:
- `content`/`messages`: unified assistant output as Messages/ContentParts
- `usage`: normalized token/cost data when available
- helpers: `ReqLLM.Response.text/1`, `ReqLLM.Response.object/1`, `ReqLLM.Response.usage/1`
**Example**:
```elixir
{:ok, response} = ReqLLM.generate_text("anthropic:claude-haiku-4-5",
[ReqLLM.Context.user("Hello")]
)
text = ReqLLM.Response.text(response)
usage = ReqLLM.Response.usage(response)
```
### Usage Structure
The `usage` field contains normalized usage data with token counts, costs, and tool/image usage:
```elixir
%{
# Token counts
input_tokens: 150,
output_tokens: 200,
total_tokens: 350,
reasoning_tokens: 0, # For reasoning models (o1, o3, gpt-5)
cached_tokens: 100, # Cached input tokens
cache_creation_tokens: 0, # Tokens used to create cache
# Cost breakdown (USD)
input_cost: 0.00045,
output_cost: 0.0006,
total_cost: 0.00105,
# Detailed cost by category
cost: %{
tokens: 0.00105,
tools: 0.02, # Web search, function calls
images: 0.0, # Image generation
total: 0.02105,
line_items: [...] # Per-component cost details
},
# Tool usage (web search, etc.)
tool_usage: %{
web_search: %{count: 2, unit: "call"}
},
# Image usage (for image generation)
image_usage: %{
generated: %{count: 1, size_class: "1024x1024"}
}
}
```
See the [Usage & Billing Guide](usage-and-billing.md) for comprehensive documentation.
**How this supports normalization**:
- One response object to extract text, structured objects, and usage across providers.
## 8) ReqLLM.StreamResponse
Handle for streaming operations with helpers to consume chunks or tokens.
**Example**:
```elixir
{:ok, sr} = ReqLLM.stream_text("anthropic:claude-haiku-4-5",
[ReqLLM.Context.user("Tell me a story")]
)
# Stream raw chunks
ReqLLM.StreamResponse.stream(sr)
|> Stream.each(fn chunk ->
case chunk.type do
:content -> IO.write(chunk.text)
:tool_call -> IO.inspect(chunk, label: "Tool call")
:meta -> :ok
_ -> :ok
end
end)
|> Stream.run()
# Or tokens helper (if available)
ReqLLM.StreamResponse.tokens(sr)
|> Stream.each(&IO.write/1)
|> Stream.run()
```
**How this supports normalization**:
- Same streaming consumption API for every provider; adapters convert SSE/WS specifics into `StreamChunk`.
## 9) Validation and type safety
ReqLLM provides validation utilities so you can fail early and clearly:
- `ReqLLM.Context.validate/1`
- `ReqLLM.StreamChunk.validate/1`
- Tool argument validation via `NimbleOptions` schemas
**Example**:
```elixir
case ReqLLM.Context.validate(context) do
{:ok, ctx} -> ReqLLM.generate_text(model, ctx)
{:error, reason} -> raise ArgumentError, "Invalid context: #{reason}"
end
```
## 10) End-to-end example (provider-agnostic)
```elixir
alias ReqLLM.Message.ContentPart
{:ok, model} = ReqLLM.model("anthropic:claude-haiku-4-5")
{:ok, tool} = ReqLLM.Tool.new(
name: "get_weather",
description: "Gets weather by city",
parameter_schema: [city: [type: :string, required: true]],
callback: fn %{city: city} -> {:ok, "Weather in #{city}: sunny"} end
)
context = ReqLLM.Context.new([
ReqLLM.Context.system("You are a helpful assistant."),
ReqLLM.Context.user([
ContentPart.text("What is the weather in NYC today?")
])
])
{:ok, response} = ReqLLM.generate_text(model, context, tools: [tool])
IO.puts("Answer: " <> ReqLLM.Response.text(response))
IO.inspect(ReqLLM.Response.usage(response), label: "Usage")
```
**How this supports normalization**:
- At no point does your application code need to branch on provider.
- Providers translate request/response specifics into these canonical types.
## Key takeaways
- The canonical data structures are the heart of ReqLLM's "normalize everything" approach.
- Build contexts, messages, and tools once; reuse them across providers.
- Consume streaming and final results through a single, consistent API.