Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
bd06bd3
docs(candil): initial CHANGELOG.md with v1.0.0 release notes (first p…
mavis Jun 15, 2026
ed52954
docs(candil): add Project history section and link 1.0.0 release
Lorenzo-SF Jun 23, 2026
1a946af
feat(candil): add Candil facade module (public API entry point)
Lorenzo-SF Jun 23, 2026
0597470
fix(candil): move Llm moduledoc to docs/, fix broken headers
Lorenzo-SF Jun 24, 2026
20e0ebf
ci(candil): add multi-stage CI with format, credo, sobelow, test+cove…
Lorenzo-SF Jun 24, 2026
bb5ada4
fix(candil): remove broken moduledoc remnants blocking compile, drop …
Lorenzo-SF Jun 24, 2026
72af6cf
fix(candil): retryable?/2 was ignoring retry_on parameter — wire it t…
Lorenzo-SF Jun 24, 2026
ba3d2c5
fix(candil): parse_openai_tool_calls used rescue _ to catch all error…
Lorenzo-SF Jun 24, 2026
45f18f3
feat(candil): add non-blocking retry helpers (schedule_retry/7, handl…
Lorenzo-SF Jun 24, 2026
676b764
chore(candil): relax credo to non-strict (matches apero config)
Lorenzo-SF Jun 24, 2026
202261d
ci(candil): drop --strict on credo (style rules rejected new helpers)
Lorenzo-SF Jun 24, 2026
d8372be
ci(candil): comment out credo step (style issues block strict gate)
Lorenzo-SF Jun 24, 2026
26626ff
ci: retrigger workflow (no source change, manual kick to refresh cach…
Lorenzo-SF Jun 24, 2026
457f9c9
merge(candil): pull request #1 from Lorenzo-SF/cleanup/audit-and-i18n
Lorenzo-SF Jun 24, 2026
498c32d
docs(candil): move Spanish README to docs/, add source_ref for ex_doc
Lorenzo-SF Jun 24, 2026
9875cc5
chore(candil): bump pote to 26bc8fc7 (Pote.Theme + storage_dir override)
mavis Jun 25, 2026
2017ee4
chore(candil): bump pote to 26bc8fc7da9c0d7c5c5dc18a4d8e96597b03a1c82…
mavis Jun 25, 2026
df3810a
fix(candil): correct pote SHA in mix.lock + mix.exs docs fix
mavis Jun 25, 2026
926abcf
chore: bump @version to 0.2.0 (consolidated release: audit fixes, the…
mavis Jun 25, 2026
9af4090
merge(candil): cleanup/audit-and-i18n (English-only docs, CI workflow…
mavis Jun 25, 2026
f13a58a
docs(candil): mark v0.2.0 release in CHANGELOG
mavis Jun 25, 2026
7c13d57
ci(candil): add workflow_dispatch trigger for manual CI re-runs
mavis Jun 25, 2026
50a2a05
style(candil): apply mix format across the codebase
mavis Jun 25, 2026
52cdc5b
docs(candil): bump version refs to 0.2.0, add docs/README.es.md (Span…
mavis Jun 25, 2026
6adad64
Added readme candil
Lorenzo-SF Jun 25, 2026
48a1364
chore(candil): bump version + CHANGELOG for v0.3.0 (Provider tagged)
mavis Jun 26, 2026
2b6326b
chore: update refs
mavis Jun 27, 2026
71c3f6a
fix(candil): production hardening — API key validation, circuit break…
mavis Jun 28, 2026
9b61d69
chore: update refs
mavis Jun 28, 2026
d6ae0e0
chore(candil): track apero and arrea on `main`
Lorenzo-SF Jun 29, 2026
4270ac6
candil: add Health, ConfigManager, Embeddings from apero; fix Registry
Lorenzo-SF Jul 5, 2026
cb4da5e
docs: update CHANGELOG and README for new modules + Registry fix
Lorenzo-SF Jul 5, 2026
4c8a3c1
Merge pull request #2 from Lorenzo-SF/feat/apero-llm-move
Lorenzo-SF Jul 5, 2026
0aa68a2
refactor(candil): remove unused Candil.Retry + align source_ref to v0…
Lorenzo-SF Jul 6, 2026
5f70aeb
docs(candil): add 'A note on versioning' footer + drop dangling v0.2.…
Lorenzo-SF Jul 6, 2026
63ee1a5
fix(candil): source_ref points to existing tag + drop dangling v0.2.0…
Lorenzo-SF Jul 6, 2026
923b7c3
chore(candil): redo CHANGELOG as 2.0.0 (#8)
Lorenzo-SF Jul 6, 2026
7514452
fix(candil): ConfigManager improper_list_constr + bump to 2.0.0 (#9)
Lorenzo-SF Jul 7, 2026
fb68b9d
fix(candil): add Code.ensure_loaded(Candil) in FacadeTest setup (#10)
Lorenzo-SF Jul 7, 2026
06d9f15
Feat/engine launch callback (#12)
Lorenzo-SF Jul 10, 2026
a643bde
fix(candil): add Code.ensure_loaded(Candil) in FacadeTest setup (#10)
Lorenzo-SF Jul 10, 2026
9291b78
chore: update versions
Lorenzo-SF Jul 11, 2026
cca32ee
Domain cleanup (#13)
Lorenzo-SF Jul 11, 2026
9d5fb72
chore: update versions
Lorenzo-SF Jul 11, 2026
cce5de4
fix: usar Apero.OS.type/0 en vez de Trebejo.OS.type/0 (eliminado en d…
Lorenzo-SF Jul 11, 2026
f739bd3
fix: path deps locales para apero/trebejo/arrea + Apero.OS.type/0 en …
Lorenzo-SF Jul 11, 2026
4935142
feat: add LRU EnginePool with tests, fix pool test isolation
Lorenzo-SF Jul 12, 2026
f3a9fd0
credo fixes
Lorenzo-SF Jul 12, 2026
e39c39e
fix: auth_headers merge, config safe_to_atom, streaming circuit break…
Lorenzo-SF Jul 13, 2026
67d221b
refactor(candil): migrate Health to Candil.HTTP, extract HealthPoller
Lorenzo-SF Jul 13, 2026
672965a
fix port bug
Lorenzo-SF Jul 13, 2026
493db5d
candil: CAN-01..14 audit fixes
Lorenzo-SF Jul 20, 2026
af75628
fix(candil): restore HTTP result typing
Lorenzo-SF Jul 21, 2026
4a0dc6d
fix(candil): reject unknown config atoms
Lorenzo-SF Jul 21, 2026
f5b5209
test(candil): isolate detector release requests
Lorenzo-SF Jul 21, 2026
22386da
chore(candil): satisfy strict credo
Lorenzo-SF Jul 21, 2026
8566713
docs(candil): document cost helpers
Lorenzo-SF Jul 21, 2026
5324202
docs(candil): align usage with current API
Lorenzo-SF Jul 21, 2026
75f817a
docs(candil): actualizar EXECUTION_PLAN con tareas realizadas + refac…
Lorenzo-SF Jul 21, 2026
8988434
audit docs
Lorenzo-SF Jul 21, 2026
da78834
fix(candil): CAN-08 remove term() from reason() union for sound Dialyzer
Lorenzo-SF Jul 22, 2026
afcd3a7
docs(candil): AUDIT v2 + agrupación por impacto (LOCAL/MEDIO/CRÍTICO)
Lorenzo-SF Jul 22, 2026
affa195
test(candil): CAN-19 HTTP test exercises get/post_json/post_streaming…
Lorenzo-SF Jul 22, 2026
48dfbf5
test(candil): CAN-20 Inference smoke tests verify module API surface
Lorenzo-SF Jul 22, 2026
0a83987
style(candil): CAN fix credo warnings (length/1 → empty list comparis…
Lorenzo-SF Jul 22, 2026
e10a918
refactor(candil): CAN-17 partial split — extract Conversation.TokenEs…
Lorenzo-SF Jul 22, 2026
1bc6eaa
docs(candil): mark project closed — CAN-17 TokenEstimator extracted (…
Lorenzo-SF Jul 22, 2026
42f1cae
refactor(candil): CAN-15 split Inference (409→136 LoC) — extract Chat…
Lorenzo-SF Jul 22, 2026
d849899
refactor(candil): CAN-16 split HTTP (254→91 LoC) — extract Client, Re…
Lorenzo-SF Jul 22, 2026
b3b6222
refactor(candil): CAN-17 + CAN-18 — split Detector (258→83) + Convers…
Lorenzo-SF Jul 22, 2026
d3e8e22
fix(candil): compiler warnings — alias ordering, unused opts, TokenEs…
Lorenzo-SF Jul 23, 2026
702f457
added "chat_template_kwargs" and "extra_body" params
Lorenzo-SF Jul 24, 2026
9a3b2b6
chore: align histories with origin/main for PR compatibility
Lorenzo-SF Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .codegraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- Preserved successful HTTP response maps and corrected their Dialyzer typing.
- Sent health-check embedding payloads as maps accepted by the shared HTTP client.
- Rejected unknown string config keys without creating atoms at runtime.
- Replaced live GitHub calls in detector tests with the configured mock HTTP adapter.

### Changed
- Updated English and Spanish README dependency, API arity, and architecture examples.

## [2.1.0] - 2026-XX-XX

### Added
- `Candil.Engine.Launcher` behaviour for custom engine launchers (external
processes, systemd units, docker containers).
- `Candil.Engine.Server.External` GenServer for managing engines whose
lifecycle is handled outside Candil.
- `Candil.EnginePool` LRU pool to track and manage engine usage.
- `:launcher` field in `Candil.Engine` struct.

### Changed
Expand Down
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ LLM inference and model management for Elixir. Run local models via llama.cpp or
```elixir
def deps do
[
{:candil, "~> 0.2"}
{:candil, "~> 2.1"}
]
end
```

## Dependencies

Candil requires:
- `:apero` - System utilities (included automatically via path in dev)
- `:arrea` - Parallel execution (included automatically via path in dev)
- `:apero` - HTTP transport, retry, and system utilities
- `:arrea` - Circuit breakers and long-running process supervision
- `:trebejo` - OS and architecture detection
- `:jason` - JSON encoding/decoding
- `:req` - HTTP client

## Configuration

Expand Down Expand Up @@ -92,7 +92,7 @@ Candil.Config.register_model(model)
provider = %Candil.Provider{
alias: :openai,
type: :openai,
base_url: "https://api.openai.com/v1",
base_url: "https://api.openai.com",
api_key: System.get_env("OPENAI_API_KEY")
}

Expand Down Expand Up @@ -168,7 +168,7 @@ IO.puts(response.content)
# Run inference directly
{:ok, response} = Candil.chat(model, provider, [
%{role: "user", content: "Hello!"}
])
], [])

IO.puts(response.content)
```
Expand All @@ -188,7 +188,7 @@ Candil.stream(model, provider, [
%{role: "user", content: "Write a story"}
], fn chunk ->
IO.write(chunk.content)
end)
end, [])
```

### Embeddings
Expand All @@ -198,7 +198,7 @@ end)
{:ok, embeddings} = Candil.embed(:llama3, ["Hello world", "How are you?"])

# Remote embeddings
{:ok, embeddings} = Candil.embed(model, provider, ["Hello world", "How are you?"])
{:ok, embeddings} = Candil.embed(model, provider, ["Hello world", "How are you?"], [])
```

### Conversation Management
Expand All @@ -222,12 +222,16 @@ IO.puts(response.content)
- **Candil.Llm** - Main entry point for all LLM operations
- **Candil.Engine** - Manages local llama-server processes
- **Candil.Engine.Server** - GenServer wrapping the llama-server OS process
- **Candil.EnginePool** - LRU tracking for active engines
- **Candil.Inference** - Handles chat completions and embeddings
- **Candil.HTTP** - Shared HTTP client with retries, circuit breaking, and rate limiting
- **Candil.Stream** - SSE streaming support
- **Candil.Provider** - Remote API provider abstraction (OpenAI, Anthropic, Ollama)
- **Candil.Model** - Model definitions (local or remote)
- **Candil.Config** - ETS-based registry for engines, models, and providers
- **Candil.ConfigManager** - Config validation and normalization for ad-hoc provider connections
- **Candil.Error** - Unified inference and transport errors
- **Candil.Cost** - Token cost estimation for known models
- **Candil.Health** - Health probes (ping, latency, model availability) for LLM providers
- **Candil.Embeddings** - Embedding generation across ollama, local, and OpenAI-compatible APIs
- **Candil.Detector** - OS/GPU detection for binary selection
Expand All @@ -240,10 +244,8 @@ IO.puts(response.content)
## Project history

This library was developed as part of a larger internal toolkit and extracted
to open source in mid-2026. The single commit visible on `main` represents the
OSS cut-over point — all the features shipped in `0.2.0` were built and tested
before being made public. Subsequent releases (`0.2.1`, `0.3.0`, ...) will be
tagged normally, providing a clean public history going forward.
to open source in mid-2026. The canonical releases are `1.0.0` and `2.0.0`;
the codebase is currently in the `2.1.0` development cycle.

## License

Expand Down
214 changes: 214 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Candil — Architectural Reference

> LLM inference and model management for Elixir — v2.1.0

---

## 1. What is Candil

Candil is the **LLM inference engine** of the Lorenzo-SF ecosystem. It
provides a unified API for running local models (via `llama.cpp` / `llama-server`)
and remote models (OpenAI, Anthropic, Ollama, OpenAI-compatible, Azure OpenAI).
It handles engine lifecycle, model downloads (GGUF), chat completion,
streaming (SSE), embeddings, provider configuration, conversation management,
cost estimation, health checks, and circuit-broken HTTP transport.

---

## 2. Architecture Overview

```
┌──────────────────────────────────────────────────────────────┐
│ Candil (Facade) │
│ lib/candil.ex — chat/2-5, embed/2-4, stream/3-5 │
├──────────────────────────────────────────────────────────────┤
│ │ │
│ ┌───────▼───────┐ │
│ │ Candil.Llm │ (internal orchestrator) │
│ │ │ │
│ │ dispatch to │ │
│ │ local/remote │ │
│ └───────┬───────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ ┌─────▼──────┐ ┌─────▼──────┐ │
│ │ Local │ │ Remote │ │
│ │ inference │ │ inference │ │
│ │ │ │ │ │
│ │ llama.cpp │ │ OpenAI │ │
│ │ via Engine │ │ Anthropic │ │
│ │ ── Server │ │ Ollama │ │
│ │ (OS pr) │ │ Azure │ │
│ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │
│ ┌─────▼────────────────────────────▼──────┐ │
│ │ Inference Engine │ │
│ │ Candil.Inference — chat_local/remote │ │
│ │ Candil.RequestBuilder — build bodies │ │
│ │ Candil.Stream — SSE parsing │ │
│ │ Candil.HTTP — circuit + retry + rate │ │
│ └──────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Engine Lifecycle │ │
│ │ │ │
│ │ Engine.Server — GenServer over llama-server OS proc │ │
│ │ Engine.Server.External — externally-managed engines │ │
│ │ Engine.HealthPoller — periodic /health probe │ │
│ │ EnginePool — LRU pool of running engines │ │
│ │ Engine.Launcher — behaviour for custom launchers │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Configuration │ │
│ │ │ │
│ │ Config — ETS-based registry (engines/models/provid) │ │
│ │ ConfigManager — map-based config validation │ │
│ │ Provider — struct with auth, URLs, type │ │
│ │ Model — struct (local GGUF or remote name) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Download & Detection │ │
│ │ │ │
│ │ Installer — download llama.cpp + GGUF (resume) │ │
│ │ Detector — GPU detection (nvidia/amd/intel/apple) │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```

---

## 3. Subsystems

### 3.1 Public API (Candil + Candil.Llm)
- `chat/2-5` — text completion (local or remote)
- `embed/2-4` — embeddings (local or remote)
- `stream/3-5` — streaming chat with SSE callback
- `download_engine/1` — download llama.cpp binary
- `download_model/1` — download GGUF model file
- `start_engine/2` — start llama-server with model
- `stop_engine/1` — stop running engine
- `engine_healthy?/1` — health probe

### 3.2 Engine Lifecycle
- **Engine.Server** (GenServer): Manages `llama-server` OS process via
`Arrea.LongRunning`. Builds CLI args, polls `/health`, registers in
`Candil.Registry`. Auto port cleanup.
- **Engine.Server.External** (GenServer): For externally-managed engines
(Docker, systemd, k8s via `Candil.Engine.Launcher`). Holds `base_url`,
polls health, sends shutdown on terminate.
- **Engine.HealthPoller**: Shared health-polling logic. Probes `<base_url>/health`
every 5s. Used by both Server implementations.
- **EnginePool** (GenServer): LRU pool of running engines. Ordered by recency.
`get/0`, `put/1`, `evict/0`. Used for automatic engine selection.
- **Engine struct**: alias, binary_dir, host, port, context_size, etc.

### 3.3 Inference
- **Inference**: `chat_local/3`, `chat_remote/4`, `embed_local/3`, `embed_remote/4`.
Builds provider-specific request bodies, parses responses (OpenAI, Anthropic,
Ollama format). Validates context window. Emits telemetry.
- **RequestBuilder**: Normalizes messages, handles system prompts, streaming flag,
tool definitions, stop sequences for each provider type.
- **Stream**: SSE parsing for all providers. Parses OpenAI, Anthropic, Ollama chunk
formats. Calls user callback per token: `%{content:, finish_reason:, done:}`.

### 3.4 HTTP Transport (Candil.HTTP)
- Circuit breaker (`Arrea.CircuitBreaker`)
- Retry with exponential backoff (`Apero.Retry`)
- Sliding-window rate limiter
- `post_json/4`, `post_streaming/5`, `get/3`

### 3.5 Configuration
- **Config** (GenServer): ETS-based registry for engines, models, providers.
Loads from application env on init. Resolves `{:system, "ENV_VAR"}` api_key
tuples at lookup time.
- **ConfigManager**: Raw map-based config validation/normalization. Validates
provider configs, provides defaults.
- **Provider struct**: Types: `:openai`, `:anthropic`, `:ollama`,
`:openai_compatible`, `:azure_openai`. Generates `auth_headers/1`,
`chat_url/1`, `embeddings_url/1`.
- **Model struct**: Local (GGUF + engine) or remote (model name + provider).
Fields: alias, type, model_dir, filename, context_size, usage.

### 3.6 Installer & Detector
- **Installer**: Downloads llama.cpp binaries and GGUF models. Streams to disk
(no full memory load), supports resume via HTTP Range, SHA-256 verification.
- **Detector**: System capability detection for llama.cpp binary selection.
Detects OS (Apero.OS), arch (Trebejo.OS), GPU (nvidia-smi, rocminfo,
vulkaninfo, sycl-ls, Metal). Builds asset pattern for GitHub release matching.

### 3.7 Conversation & Cost
- **Conversation**: Maintains message list, auto-trims to fit context window.
Token estimation via `ceil(byte_size/4)`. Supports local and remote models.
- **Cost**: Built-in pricing table for OpenAI and Anthropic models.
`estimate(model, input_tokens, output_tokens)` → `{:ok, float}` or `:unknown`.

### 3.8 Health
- `Candil.Health`: Provider health checks. Probes `<url>/v1/models` endpoint,
returns reachability, latency, model count. `ping/3` sends a minimal embedding
request to verify a model is loaded.

---

## 4. Dependencies

| Dependency | Version | Purpose |
|------------|---------|---------|
| **Apero** | path: ../apero | HTTP transport (`Apero.Http`), retry (`Apero.Retry`), OS detection (`Apero.OS`) |
| **Arrea** | path: ../arrea | Circuit breaker (`Arrea.CircuitBreaker`), long-running OS process (`Arrea.LongRunning`), Registry, Monitor, WorkerSupervisor |
| **Trebejo** | path: ../trebejo | OS architecture detection (`Trebejo.OS.arch/0`) |
| Jason | ~> 1.4 | JSON encoding/decoding for API requests |

Candil depends on **Apero** (HTTP), **Arrea** (resilience, process mgmt),
and **Trebejo** (OS detection).

---

## 5. Consumed by

| Project | What it uses |
|---------|--------------|
| **Delfos** | `Candil.Provider` struct, `Candil.chat` for LLM summarization/explanation, `Candil.embed` for embeddings, `Candil.Health.probe` for health checks, `Candil.HTTP` for API calls |

Candil is a leaf library: it does the LLM work for Delfos.

---

## 6. Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| **local/remote split in Llm** | Single API (`chat/2-5`) handles both. Caller doesn't care where the model runs. |
| **Engine as OS process via Arrea.LongRunning** | Proper supervision, telemetry, crash isolation, and port cleanup. Not a bare `System.cmd`. |
| **Provider struct with URL generation** | Each provider knows its own API format. `chat_url/1`, `embeddings_url/1` encapsulate the variation. |
| **ETS config registry** | Hot-reloadable config without application restart. `{:system, "VAR"}` tuples defer resolution to lookup time. |
| **SSE streaming standardized** | All provider chunk formats normalized to `%{content:, finish_reason:, done:}` callback. Consumer writes one handler. |
| **Download with resume** | HTTP Range headers for interrupted downloads. Critical for multi-GB GGUF files. |
| **GPU detection** | Auto-selects the right llama.cpp binary (CUDA, ROCm, Vulkan, SYCL, Metal, CPU). No manual config. |
| **Circuit breaker on HTTP** | Prevents cascading failures when LLM endpoints are down. |

---

## 7. Supervision Tree

```
Candil.Application
├── Candil.Registry (Elixir.Registry)
├── Candil.Config (GenServer, ETS)
├── Candil.EnginePool (GenServer, LRU)
└── Candil.EngineSupervisor (DynamicSupervisor)
└── Candil.Engine.Server (GenServer, one per running engine)
```

---

## 8. Current State (v2.1.0 — Jul 2026)

- 24 source modules across 8 subsystems
- 19 test files
- Supports: llama.cpp (local), OpenAI, Anthropic, Ollama, OpenAI-compatible, Azure
- GPU detection: NVIDIA (nvidia-smi), AMD (rocminfo), Intel (sycl-ls), Apple (Metal), CPU fallback
- Streaming, embeddings, conversation management, cost estimation all operational
- Used by Delfos for all LLM operations
Loading
Loading