Current section

Files

Jump to
req_llm usage-rules.md
Raw

usage-rules.md

# ReqLLM Usage Rules
This document provides best practices and guidelines for using ReqLLM effectively in Elixir applications.
## Core Purpose and Scope
ReqLLM is a composable library for AI interactions that normalizes LLM provider differences through a plugin-based architecture. It treats each provider as a Req plugin, handling format translation via a Codec protocol while leveraging Req's HTTP infrastructure.
Key concepts:
- **Provider Plugins**: Each AI provider (Anthropic, OpenAI) implements the `ReqLLM.Provider` behavior
- **Codec Protocols**: Translate between canonical structures and provider-specific formats
- **Model Specifications**: Flexible ways to specify models (string, tuple, struct)
- **StreamChunks**: Unified output format across all providers
- **Context/Message/ContentPart**: Provider-agnostic conversation structures
## Essential Usage Guidelines
### Model Specifications
**Best Practice**: Start with string format and graduate to struct format as needed.
```elixir
# Simple - good for basic usage
{:ok, response} = ReqLLM.generate_text("anthropic:claude-3-sonnet", "Hello")
# Advanced - good for complex configurations
model = %ReqLLM.Model{
provider: :anthropic,
model: "claude-3-sonnet",
temperature: 0.7,
max_tokens: 1000
}
{:ok, response} = ReqLLM.generate_text(model, "Hello")
```
**Avoid**: Creating Model structs manually when string format suffices.
### API Function Selection
**Best Practice**: Use bang variants (`!`) for simple use cases, full functions for production.
```elixir
# Development/simple cases
ReqLLM.generate_text!("anthropic:claude-3-sonnet", "Hello")
# Production - need access to metadata, errors, usage data
{:ok, response} = ReqLLM.generate_text("anthropic:claude-3-sonnet", "Hello")
response.text() #=> "Hello! How can I help?"
response.usage() #=> %{input_tokens: 10, output_tokens: 8}
```
**Avoid**: Using bang variants when you need detailed error handling or usage tracking.
### Context and Message Construction
**Best Practice**: Import context helpers for clean message building.
```elixir
import ReqLLM.Context
context = Context.new([
system("You are a helpful assistant"),
user("What's 2+2?")
])
{:ok, response} = ReqLLM.generate_text("anthropic:claude-3-sonnet", context)
```
**Avoid**: Manually constructing Message structs when helpers exist.
### Multimodal Content
**Best Practice**: Import ContentPart helpers for clean multimodal handling.
```elixir
import ReqLLM.Message.ContentPart
message = ReqLLM.Context.user([
text("Analyze this image"),
image_url("https://example.com/chart.png")
])
{:ok, response} = ReqLLM.generate_text("anthropic:claude-3-sonnet", [message])
```
### Streaming
**Best Practice**: Filter StreamChunks by type for clean processing.
```elixir
{:ok, stream} = ReqLLM.stream_text("anthropic:claude-3-sonnet", "Tell a story")
# Collect only text content
text = stream
|> Stream.filter(&(&1.type == :text))
|> Stream.map(&(&1.text))
|> Enum.join()
# Handle different chunk types
stream
|> Enum.each(fn chunk ->
case chunk.type do
:text -> IO.write(chunk.text)
:tool_call -> handle_tool_call(chunk)
:meta -> handle_metadata(chunk)
_ -> :ok
end
end)
```
### Error Handling
**Best Practice**: Pattern match on specific error types for appropriate handling.
```elixir
case ReqLLM.generate_text("anthropic:claude-3-sonnet", "Hello") do
{:ok, response} ->
handle_success(response)
{:error, %ReqLLM.Error.Invalid.Provider{}} ->
Logger.error("Unsupported provider")
{:error, :unsupported_provider}
{:error, %ReqLLM.Error.API.RateLimit{retry_after: seconds}} ->
Logger.warn("Rate limited, retry after #{seconds}s")
:timer.sleep(seconds * 1000)
retry_request()
{:error, %ReqLLM.Error.API.Authentication{}} ->
Logger.error("Authentication failed - check API key")
{:error, :auth_failed}
{:error, error} ->
Logger.error("Unexpected error: #{inspect(error)}")
{:error, :unknown}
end
```
## Configuration and Setup
### Key Management
**Important**: ReqLLM uses JidoKeys with automatic .env loading via Dotenvy.
```elixir
# Preferred - JidoKeys automatically loads from .env files
# No explicit setup needed if keys are in .env as:
# ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# Optional manual key setup
ReqLLM.put_key("anthropic_api_key", System.get_env("ANTHROPIC_API_KEY"))
ReqLLM.put_key("openai_api_key", System.get_env("OPENAI_API_KEY"))
# Providers automatically retrieve keys via JidoKeys
{:ok, response} = ReqLLM.generate_text("anthropic:claude-3-sonnet", "Hello")
```
## Tool Calling
### Tool Definition
**Best Practice**: Define tools with comprehensive parameter schemas using NimbleOptions.
```elixir
weather_tool = ReqLLM.Tool.new!(
name: "get_weather",
description: "Get current weather for a location",
parameter_schema: [
location: [type: :string, required: true, doc: "City name"],
units: [type: :string, default: "celsius", doc: "Temperature units"]
],
callback: {WeatherAPI, :fetch_weather}
)
```
### Tool Execution
**Best Practice**: Handle tool execution errors gracefully with proper return tuples.
```elixir
defmodule WeatherAPI do
def fetch_weather(%{location: location, units: units}) do
case HTTPClient.get("/weather", location: location, units: units) do
{:ok, %{status: 200, body: data}} ->
{:ok, data}
{:ok, %{status: 404}} ->
{:error, "Location not found"}
{:error, reason} ->
{:error, "Weather service unavailable: #{inspect(reason)}"}
end
rescue
error ->
{:error, "Weather fetch failed: #{Exception.message(error)}"}
end
end
```
## Advanced Usage Patterns
### Custom Req Middleware
**Best Practice**: Use provider's `attach/3` with custom Req pipeline steps.
```elixir
def traced_generate_text(model_spec, messages, opts \\ []) do
with {:ok, model} <- ReqLLM.Model.from(model_spec),
{:ok, provider_module} <- ReqLLM.Provider.get(model.provider) do
request = Req.new()
|> provider_module.attach(model, opts)
|> Req.Request.append_request_steps(tracing: &add_request_tracing/1)
Req.request(request)
end
end
defp add_request_tracing(request) do
request_id = UUID.uuid4()
%{request |
headers: [{"x-request-id", request_id} | request.headers],
private: Map.put(request.private, :trace_id, request_id)
}
end
```
### Usage Monitoring
**Best Practice**: Extract usage data from Response structs for monitoring.
```elixir
def monitored_generation(model_spec, messages, opts \\ []) do
start_time = System.monotonic_time()
case ReqLLM.generate_text(model_spec, messages, opts) do
{:ok, response} ->
duration = System.monotonic_time() - start_time
Logger.info("LLM Generation", [
model: inspect(model_spec),
usage: response.usage(),
duration_ms: System.convert_time_unit(duration, :native, :millisecond)
])
{:ok, response.text()}
{:error, error} ->
Logger.error("LLM Generation failed: #{inspect(error)}")
{:error, error}
end
end
```
## Performance Considerations
### Memory Management
**Best Practice**: Process streams incrementally for large responses.
```elixir
{:ok, stream} = ReqLLM.stream_text("anthropic:claude-3-sonnet", "Write a long story")
stream
|> Stream.filter(&(&1.type == :text))
|> Stream.chunk_every(100) # Process in chunks
|> Stream.each(&process_chunk/1)
|> Stream.run()
```
### Connection Reuse
**Important**: ReqLLM leverages Req's connection pooling automatically. Avoid creating new clients per request.
## Common Pitfalls
### Model Specification Errors
**Problem**: Using unsupported model names or invalid provider IDs.
**Solution**: Use `ReqLLM.Model.from/1` to validate specifications early.
```elixir
case ReqLLM.Model.from("invalid:model") do
{:ok, model} -> proceed_with_model(model)
{:error, error} ->
Logger.error("Invalid model spec: #{inspect(error)}")
use_fallback_model()
end
```
### Provider-Specific Parameter Requirements
**Problem**: Some models require different parameter names (e.g., OpenAI o1 models need `max_completion_tokens` instead of `max_tokens`).
**Solution**: ReqLLM automatically handles parameter translation. Use standard parameter names and let providers translate them as needed.
```elixir
# Use standard parameters - ReqLLM handles model-specific translation automatically
{:ok, response} = ReqLLM.generate_text(
"openai:o1-mini",
"Hello",
max_tokens: 150, # Automatically becomes max_completion_tokens for o1 models
temperature: 0.7 # Automatically dropped with warning for o1 models
)
# Control warning behavior with on_unsupported option
{:ok, response} = ReqLLM.generate_text(
"openai:o1-mini",
"Hello",
max_tokens: 150,
temperature: 0.7,
on_unsupported: :ignore # Suppress warnings for unsupported parameters
)
```
### Stream Processing Issues
**Problem**: Assuming all stream chunks contain text data.
**Solution**: Always filter by chunk type before processing.
```elixir
# Safe stream processing
text_chunks = stream
|> Stream.filter(fn chunk -> chunk.type == :text and chunk.text != nil end)
|> Stream.map(&(&1.text))
```