IMPORTANT: DO NOT WRITE COMMENTS INTO THE BODY OF ANY FUNCTIONS.
ReqLLM is a composable Elixir library for AI interactions built on Req, providing a unified interface to AI providers through a plugin-based architecture. The library uses OpenAI Chat Completions as the baseline API standard, with providers implementing translation layers for non-compatible APIs.
mix test- Run all tests using cached fixturesmix test test/req_llm_test.exs- Run specific test filemix test --only describe:"model/1 top-level API"- Run specific describe blockLIVE=true mix test- Run against real APIs and (re)generate fixturesREQ_LLM_DEBUG=1 mix test- Run tests with verbose fixture debugging outputmix compile- Compile the projectmix qualityormix q- Run quality checks (format, compile --warnings-as-errors, dialyzer, credo)
mix mcormix req_llm.model_compat- Show models with passing fixturesmix mc "*:*"- Validate all models (parallel, fixture-based)mix mc --sample- Validate sample model subset (config/config.exs)mix mc anthropic- Validate all Anthropic modelsmix mc "openai:gpt-4o"- Validate specific modelmix mc "xai:*" --record- Re-record fixtures for xAI modelsmix mc --available- List all models from registry (priv/models_dev/)
Coverage System Architecture:
- Model Registry: provided by the
llm_dbdependency (no manual sync needed) - Fixture State:
priv/supported_models.json(auto-generated artifact) - Parallel Execution: Tests run concurrently for speed
- State Tracking: Skips models with passing fixtures unless
--recordor--record-all
ReqLLM uses structured key/value tags for precise test filtering:
Tag Dimensions:
category- Test type (:core,:streaming,:tools,:embedding)provider- LLM provider (:anthropic,:openai,:google,:groq,:openrouter,:xai)
Examples:
mix test --only "category:core"- Run all core testsmix test --only "provider:anthropic"- Run Anthropic tests onlymix test --only "category:core" --only "provider:openrouter"- Run OpenRouter core testsLIVE=true mix test --only "category:core" --only "provider:anthropic"- Regenerate Anthropic core fixtures
mix format- Format Elixir codemix format --check-formatted- Check if code is properly formattedmix dialyzer- Run Dialyzer type analysismix credo --strict- Run Credo linting (includes custom rule to enforce no comments in function bodies)
lib/req_llm.ex- Main API facade with generate_text/3, stream_text/3, generate_object/4lib/req_llm/- Core modules (Model, Provider, Error structures)lib/req_llm/providers/- Provider-specific implementations (Anthropic, OpenAI, etc.)test/- Three-tier testing architecture (seetest/AGENTS.mdfor detailed testing guide)test/req_llm/- Core package tests (NO API calls, unit tests with mocks)test/provider/- Mocked provider-specific tests (NO API calls, tests provider nuances)test/coverage/- Live API coverage tests (fixture-based, high-level API only)test/support/- Shared helpers (live fixtures, HTTP mocks, test macros)
ReqLLM.Context- Conversation history as a collection of messagesReqLLM.Message- Single conversation message with multi-modal content supportReqLLM.Message.ContentPart- Individual content piece (text, image, tool call, etc.)ReqLLM.Tool- Function calling definition with schema and callbackReqLLM.StreamChunk- Unified streaming response format across providersLLMDB.Model- Canonical model metadata struct used by ReqLLMReqLLM.Response- High-level LLM response with context and metadata
- ReqLLM supports full explicit model specs for models that are not in LLMDB yet
- Keep this path backwards compatible: public APIs may still receive a plain map as
model_spec - Internally, plain-map model specs should be normalized into
%LLMDB.Model{}as early as possible - Prefer documenting advanced examples with
ReqLLM.model!/1orLLMDB.Model.new!/1 - Do not require LLMDB catalog membership for full model specs; only require the metadata needed to route the request
- Each provider implements
ReqLLM.Providerbehavior with callbacks:prepare_request/4- Configure operation-specific requests (non-streaming only)attach/3- Set up authentication and Req pipeline steps (non-streaming only)encode_body/1- Transform context to provider JSON (non-streaming only)decode_response/1- Parse API responses (non-streaming only)attach_stream/4- Build complete Finch streaming request (streaming only, optional)decode_sse_event/2- Decode provider SSE events to StreamChunk structs (streaming only, optional)extract_usage/2- Extract usage/cost data (optional)translate_options/3- Provider-specific parameter translation (optional)
- Response Assembly:
ReqLLM.Provider.ResponseBuilderbehaviour centralizes response construction from StreamChunksResponseBuilder.for_model/1routes to provider-specific builders (Anthropic, Google, OpenAI Responses API)Provider.Defaults.ResponseBuilderhandles OpenAI-compatible providers- Custom builders ensure streaming and non-streaming produce identical Response structs
- Providers use
ReqLLM.Provider.DSLmacro for registration and metadata loading - Non-streaming: Core API uses provider's
attach/3to compose Req requests with provider-specific steps - Streaming: Uses Finch with provider's
attach_stream/4to build streaming requests anddecode_sse_event/2to parse SSE events - Options Translation: Providers can implement
translate_options/3to handle model-specific parameter requirements (e.g., OpenAI o1 models requiremax_completion_tokensinstead ofmax_tokens)
- Provider callbacks handle encoding/decoding requests and responses
- Built-in defaults provide OpenAI-style wire format handling
- Providers can override
encode_body/1anddecode_response/1for custom formats
- Follow standard Elixir conventions and use
mix formatfor consistent formatting - Use
@moduledocand@docfor comprehensive documentation - Prefer pattern matching over conditionals where possible
- Use
{:ok, result}/{:error, reason}tuple returns for fallible operations - No inline comments in method bodies - code should be self-explanatory through clear naming and structure
- Minimize imports, prefer explicit module calls (e.g.,
ReqLLM.model!/1) - Group deps in mix.exs: runtime deps first, then dev/test deps with
, only: [:dev, :test]
- Use Zoi schemas for structured data with validation:
@schema [ field1: [type: :string, required: true], field2: [type: :integer, default: 0] ]
- Use Splode for structured error handling with specific error types
- Return
{:ok, result}or{:error, %ReqLLM.Error{}}tuples - Use Splode error types:
ReqLLM.Error.API,ReqLLM.Error.Parse,ReqLLM.Error.Auth - Include helpful error messages and context in error structs
- Only add entries to
.dialyzer_ignore.exsas an absolute last resort - Prefer fixing the underlying type issue or improving type specs
- Tests are grouped by capability, not by individual function-call
- All suites use
ReqLLM.Test.LiveFixture.use_fixture/3to abstract live vs cached responses - Cached JSON fixtures live next to the test in
fixtures/<provider>/<test_name>.jsonand are automatically written when theLIVE=trueenv-var is set - Most suites run
async: true. Suites that write fixtures are forced to synchronous execution via@moduletag :capture_log
defmodule CoreTest do
use ReqLLM.Test.LiveFixture, provider: :openai
use ExUnit.Case, async: true
describe "generate_text/3" do
test "basic happy-path" do
{:ok, text} =
use_fixture(:provider, "core-basic", fn ->
ReqLLM.generate_text!("openai:gpt-4o", "Hello!")
end)
assert text =~ "Hello"
end
end
endIMPORTANT: This project uses bd (beads) for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
- Dependency-aware: Track blockers and relationships between issues
- Git-friendly: Auto-syncs to JSONL for version control
- Agent-optimized: JSON output, ready work detection, discovered-from links
- Prevents duplicate tracking systems and confusion
Check for ready work:
bd ready --jsonCreate new issues:
bd create "Issue title" -t bug|feature|task -p 0-4 --json
bd create "Issue title" -p 1 --deps discovered-from:bd-123 --jsonClaim and update:
bd update bd-42 --status in_progress --json
bd update bd-42 --priority 1 --jsonComplete work:
bd close bd-42 --reason "Completed" --jsonbug- Something brokenfeature- New functionalitytask- Work item (tests, docs, refactoring)epic- Large feature with subtaskschore- Maintenance (dependencies, tooling)
0- Critical (security, data loss, broken builds)1- High (major features, important bugs)2- Medium (default, nice-to-have)3- Low (polish, optimization)4- Backlog (future ideas)
- Check ready work:
bd readyshows unblocked issues - Claim your task:
bd update <id> --status in_progress - Work on it: Implement, test, document
- Discover new work? Create linked issue:
bd create "Found bug" -p 1 --deps discovered-from:<parent-id>
- Complete:
bd close <id> --reason "Done" - Commit together: Always commit the
.beads/issues.jsonlfile together with the code changes so issue state stays in sync with code state
bd automatically syncs with git:
- Exports to
.beads/issues.jsonlafter changes (5s debounce) - Imports from JSONL when newer (e.g., after
git pull) - No manual export/import needed!
- ✅ Use bd for ALL task tracking
- ✅ Always use
--jsonflag for programmatic use - ✅ Link discovered work with
discovered-fromdependencies - ✅ Check
bd readybefore asking "what should I work on?" - ❌ Do NOT create markdown TODO lists
- ❌ Do NOT use external issue trackers
- ❌ Do NOT duplicate tracking systems
Use GitHub PR labels to track review state after triage:
ready_to_merge- No blocking review findings remain, the PR is merge-clean, and GitHub CI is greenneeds_work- Blocking findings, missing coverage, merge conflicts, or failing CI still need follow-up before merge
- After every substantive PR review, apply exactly one of
ready_to_mergeorneeds_work - When setting one review-state label, remove the other one in the same step
- Only mark
ready_to_mergewhen the reviewer believes the PR can merge without further code changes - Use
needs_workwhen there are correctness risks, behavior regressions, missing critical tests, unresolved review comments, merge conflicts, or red GitHub checks - If you push follow-up commits to a PR branch, re-check the label state before finishing; do not leave stale
ready_to_mergelabels on a PR that still needs validation
gh pr edit <number> --add-label ready_to_merge --remove-label needs_work
gh pr edit <number> --add-label needs_work --remove-label ready_to_merge