Motivation & Architecture
Why Haskell is uniquely suited for robust, type-safe local AI pipelines.
Why Haskell for Local LLMs?
Building production applications on top of Large Language Models introduces unique challenges:
- Unpredictable Output Formats: Without strict type contracts, JSON responses from LLMs can suffer from missing fields, schema drift, or subtle type mismatches.
- Resource & Connection Exhaustion: Naive HTTP streaming can lead to socket leaks, memory buffering bottlenecks, and unhandled connection dropouts.
- Complex State Management: Multi-turn chat sessions and agentic tool-calling loops require thread-safe, concurrency-friendly state stores.
ollama-haskell addresses each of these challenges by leveraging Haskell’s strong type system, high-performance runtime, and STM concurrency.
Architectural Principles
┌─────────────────────────────────────────────────────────────┐
│ User Application Code │
└──────────────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Top-Level API │
│ (chat, generate, embed, listModels, pullStream, etc.) │
└──────────────┬───────────────────────────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ OllamaClient Core │ │ Conduit Streaming │
│ - Connection pooling (HTTP)│ │ - Constant memory bounds │
│ - Retry policies & backoff │ │ - Zero-copy chunking │
│ - Dynamic auth & headers │ │ - Immediate backpressure │
└──────────────┬──────────────┘ └──────────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Ollama Local HTTP Daemon │
│ (http://127.0.0.1:11434) │
└─────────────────────────────────────────────────────────────┘
1. Type-Safe Smart Constructors & Newtypes
Rather than passing raw string primitives, ollama-haskell provides dedicated newtypes with validation:
ModelName: Validated non-empty model identifier.Think/ThinkingLevel: Explicit ADT for reasoning token budgets (ThinkDisabled,ThinkLevel ThinkHigh).Duration: Strongly-typed nanosecond duration parser with convenience helpers (durationToSeconds,durationToMillis).Digest: SHA-256 model digest validation.
2. Generic JSON Schema Derivation (ToSchema)
With Ollama.Types.Format.SchemaDerive, you never have to manually write error-prone JSON schema specifications:
data UserProfile = UserProfile
{ username :: Text
, age :: Int
, email :: Maybe Text
, tags :: [Text]
} deriving stock (Generic, Show)
deriving anyclass (ToSchema, FromJSON)
-- Directly pass schema to LLM:
let req = (chatRequest "qwen3.5:2b" msgs) { chatFormat = Just (formatFor @UserProfile) }GHC’s compiler automatically derives the exact JSON schema, omits optional Maybe fields from the required array, and ensures the LLM’s structured output maps directly to your Haskell data type.
3. High-Throughput Streaming with Conduit
Response streaming is powered by conduit. Unlike lazy I/O or callback systems, conduit guarantees:
- Constant Memory Footprint: Responses are processed token-by-token as they arrive from the socket.
- Composable Transformations: Easily pipe tokens to stdout, filter chunks, or fold them into accumulators using collectStream and foldStream.
- Deterministic Resource Management: Connection managers are automatically cleaned up even if downstream consumers encounter errors.
4. Concurrency & STM Conversation Storage
Managing conversation history in multi-threaded web servers is simplified with Ollama.Conversation:
-- Thread-safe STM in-memory store
store <- initInMemoryStore
now <- getCurrentTime
let conv = Conversation "session-42" [userMessage "Hello!"] "qwen3.5:9b" now now
saveConversationInMemory store convThe transactional nature of STM ensures that concurrent requests to the same session will never result in corrupted history or race conditions.