diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,67 +1,33 @@
-# Revision history for ollama-haskell
-
-## Unreleased
-
-## 0.2.1.0 -- 2025-09-24
-
-* Added blob module for managing binary large objects (GGUF files/safetensors).
-* Breaking: Added `dimensions` option in embeddings for specifying embedding dimensions.
-* Breaking: Added `doneReason` field in chat and generate responses.
-* Improved test suite with more comprehensive test cases.
-* Added `onComplete` callback function in stream handlers for better streaming control.
-* Breaking: Standardized field names across the library for consistency.
-* Applied fourmolu code formatting and added hlint for better code quality.
-
-## 0.2.0.0 -- 2025-06-05
-
-* Added stack matrix to ensure lib is buildable from lts-19.33
-* Made parameters & template fields optional in `ShowModelResponse`.
-* Added extra parameters fields in `ModelInfo`.
-* Added strict annotations for all fields.
-* Fixed ToJSON instance for delete model request body.
-* Removed duplicate code by using unified `withOllamaRequest` function for all API calls.
-* Added unified config type `OllamaConfig` to hold common configuration options.
-* Added validation for generate and chat functions to ensure required fields are present.
-* Added convience functions for generating Message and ToolCall types.
-* Added thinking field for chat and generate function.
-* Added ModelOptions type to encapsulate model options.
-* Added get ollama version function.
-* Added Common Manager, Callback functions and retry option in OllamaConfig.
-* Fixed tool_calls.
-* Added MonadIO versions of api functions.
-* Added more comprehensive error handling for API calls.
-* Added more comprehensive test cases for all functions.
-* Added schema builder for passing json format for structured output.
-
-## 0.1.3.0 -- 2025-03-25
-
-* Added options, tools and tool_calls fields in chat and generate.
-* Exported EmbeddingResponse.
-* Added Format argument in chat and generate function for structured output.
-
-## 0.1.2.0 -- 2024-11-20
-
-* Added hostUrl and responseTimeOut options in generate function.
-* Added hostUrl and responseTimeOut options in chat function.
-
-## 0.1.1.3 -- 2024-11-08
-
-* Increase response timeout to 15 minutes
-* Added encodeImage utility function that converts image filePath to base64 image data.
-* Added generateJson and chatJson. High level function to return response in Haskell type.
-
-## 0.1.0.3 -- 2024-11-05
-
-* Moving to stack instead of cabal.
+# Changelog
 
-## 0.1.0.2 -- 2024-10-18
+All notable changes to `ollama-haskell` will be documented in this file.
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [PVP (Haskell Package Versioning Policy)](https://pvp.haskell.org/).
 
-* Increased response timeout time for chat function. 
+---
 
-## 0.1.0.1 -- 2024-10-18
+## [0.3.0.0] - 2026-08-04
 
-* Renaming Lib.hs to OllamaExamples.hs as it was conflicting `Lib.hs` name
+### Added
+- **`OllamaClient` Core**: Thread-safe client handle with automatic connection manager lifecycle management (`newClient`, `defaultClient`, `clientFromEnv`, `withClient`).
+- **First-Class Streaming Pipeline**: `conduit`-based response streaming (`chatStream`, `generateStream`, `pullStream`, `pushStream`, `createModelStream`).
+- **Stream Combinators**: `collectStream` and `foldStream` in `Ollama.Streaming`.
+- **Structured Output DSL**: Type-safe `SchemaBuilder` DSL in `Ollama.Types.Format.SchemaBuilder` (`|+`, `|++`, `|!`, `|!!`) for constructing JSON Schemas.
+- **Thinking / Reasoning Models Support**: `Think` ADT (`ThinkEnabled`, `ThinkDisabled`, `ThinkLevel`) and `ThinkingLevel` (`ThinkLow`, `ThinkMedium`, `ThinkHigh`, `ThinkMax`) supporting models such as `qwen3.5` and `deepseek-r1`.
+- **Function / Tool Calling**: `Tool`, `FunctionDef`, `FunctionParameters`, `ToolCall`, and `toolResultMessage` helper.
+- **Environment Resolution**: Automatic `OLLAMA_HOST` parsing and normalization in `clientFromEnv` supporting `host:port`, `http://host:port`, and bare `host`.
+- **Authorization & Headers**: Support for `OLLAMA_API_KEY` bearer tokens and custom `configHeaders`.
+- **Configurable Resilience**: `RetryPolicy` ADT (`NoRetry`, `ConstantRetry`, `ExponentialRetry`), lifecycle callbacks (`configOnStart`, `configOnSuccess`, `configOnError`), and structured logger `configLogger`.
+- **Token Throughput Metrics**: Metrics helpers `chatEvalTokensPerSecond`, `chatPromptEvalTokensPerSecond`, `evalTokensPerSecond`, `promptEvalTokensPerSecond`, `tokensPerSecond`.
+- **Testing Infrastructure**: Built-in mock testing module `Ollama.Testing` (`newMockClient`, `withMockClient`, `mockGenerateResponse`, `mockChatResponse`, `mockEmbedResponse`, `mockListModelsResponse`).
+- **Conversation Store**: Transactional STM-backed `InMemoryStore` and `ConversationStore` typeclass.
+- **New API Endpoints**: `Ollama.API.Embed` (`/api/embed`), `Ollama.API.Blobs` (`/api/blobs`), `Ollama.API.Ps` (`/api/ps`), `Ollama.API.Version` (`/api/version`).
+- **Benchmark Suite**: Criterion/tasty-bench suite in `bench/Main.hs` measuring serialization and throughput.
 
-## 0.1.0.0 -- YYYY-mm-dd
+### Changed
+- **MonadIO / MonadUnliftIO Polymorphism**: All API functions use `MonadIO m =>` / `MonadUnliftIO m =>` signatures instead of dual `*M` variants.
+- **Typed Newtypes**: `ModelName`, `Digest`, `Base64Image`, `Duration`, `Version` replace primitive string types.
+- **Unified Error Type**: `OllamaError` sum type with structured constructors and `Exception` instance.
 
-* First version. Released on an unsuspecting world.
+### Deprecated
+- `embeddings` endpoint (`/api/embeddings`) marked deprecated in favor of `/api/embed`.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,102 +1,181 @@
-# 🦙 Ollama Haskell
+# ollama-haskell
 
-<div style="text-align: center;">
-<img src="./examples/ollama_haskell.png" alt="logo image" height="200"/>
-</div>
+[![Hackage](https://img.shields.io/hackage/v/ollama-haskell.svg)](https://hackage.haskell.org/package/ollama-haskell)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
 
-**`ollama-haskell`** is an unofficial Haskell client for [Ollama](https://ollama.com), inspired by [`ollama-python`](https://github.com/ollama/ollama-python). It enables interaction with locally running LLMs through the Ollama HTTP API — directly from Haskell.
+Industry-grade, feature-complete, modern Haskell client library for the [Ollama](https://ollama.com) local LLM engine.
 
+## Features
+
+- **Client-Centric Architecture**: Thread-safe `OllamaClient` handle with connection pooling and resource management (`newClient`, `defaultClient`, `clientFromEnv`, `withClient`).
+- **First-Class Streaming**: `conduit`-based response streaming (`chatStream`, `generateStream`, `pullStream`, `pushStream`, `createModelStream`).
+- **Complete API Surface**: Text generation, chat completions, vector embeddings, model management (list, show, copy, delete, pull, push, create), and system endpoints.
+- **Structured Outputs**: Powerful `SchemaBuilder` DSL (`|+`, `|++`, `|!`, `|!!`) for type-safe JSON Schema structured responses.
+- **Function / Tool Calling**: Full support for tool definitions (`Tool`), tool calls (`ToolCall`), and execution results (`toolResultMessage`).
+- **Thinking Models Support**: Native support for reasoning models (`qwen3.5`, `deepseek-r1`) with `Think` / `ThinkingLevel` types.
+- **Environment & Auth Integration**: Robust URL normalization for `OLLAMA_HOST` and bearer token support for `OLLAMA_API_KEY`.
+- **Configurable Resilience**: Flexible retry policies (`NoRetry`, `ConstantRetry`, `ExponentialRetry`), lifecycle callbacks, and structured logging.
+- **Conversation Store**: Transactional STM-backed `InMemoryStore` and `ConversationStore` typeclass for managing multi-turn chat sessions.
+- **SDK Comparison Matrix**: Detailed feature comparison against Python, JS/TS, and Go SDKs in [doc/COMPARISON.md](doc/COMPARISON.md).
+
 ---
 
-## ✨ Features
+## Installation
 
-* 💬 Chat with models
-* ✍️ Text generation (with streaming)
-* ✅ Chat with structured messages and tools
-* 🧠 Embeddings
-* 🧰 Model management (list, pull, push, show, delete)
-* 🗃️ In-memory conversation history
-* ⚙️ Configurable timeouts, retries, streaming handlers
+Add `ollama-haskell` to your `.cabal` file:
 
+```cabal
+build-depends:
+    base >= 4.17 && < 5
+  , ollama-haskell >= 0.3.0.0
+```
+
+Or using Stack in `package.yaml`:
+
+```yaml
+dependencies:
+  - ollama-haskell >= 0.3.0.0
+```
+
 ---
 
-## ⚡ Quick Example
+## Quick Start (5 Lines)
 
 ```haskell
-{-# LANGUAGE OverloadedStrings #-}
-module Main where
-
-import Data.Ollama.Generate
-import qualified Data.Text.IO as T
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text.IO qualified as TIO
+import Ollama
 
 main :: IO ()
 main = do
-  let ops =
-        defaultGenerateOps
-          { modelName = "gemma3"
-          , prompt = "What is the meaning of life?"
-          }
-  eRes <- generate ops Nothing
-  case eRes of
-    Left err -> putStrLn $ "Something went wrong: " ++ show err
-    Right r -> do
-      putStr "LLM response: "
-      T.putStrLn (genResponse r)
+  client <- defaultClient
+  res <- chat client $ chatRequest "qwen3.5:2b" (userMessage "Why is the sky blue?" :| [])
+  case res of
+    Left err   -> print err
+    Right resp -> mapM_ (TIO.putStrLn . messageContent) (crMessage resp)
 ```
 
 ---
 
-## 📦 Installation
+## Streaming Responses with Conduit
 
-Add to your `.cabal` file:
+Stream LLM responses token-by-token as they generate:
 
-```cabal
-build-depends:
-  base >=4.7 && <5,
-  ollama-haskell
-```
+```haskell
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text.IO qualified as TIO
+import Ollama
 
-Or use with `stack`/`nix-shell`.
+main :: IO ()
+main = do
+  client <- defaultClient
+  let req = chatRequest "qwen3.5:2b" (userMessage "Count from 1 to 5." :| [])
+  
+  -- Stream chunks directly into stdout or collect them
+  chunks <- collectStream (chatStream client req)
+  mapM_ (TIO.putStr . maybe "" messageContent . crMessage) chunks
+  putStrLn ""
+```
 
 ---
 
-## 📚 More Examples
+## Function & Tool Calling
 
-See [`examples/OllamaExamples.hs`](examples/OllamaExamples.hs) for:
+Define function signatures and let the LLM execute structured tool calls:
 
-* Chat with conversation memory
-* Structured JSON output
-* Embeddings
-* Tool/function calling
-* Multimodal input
-* Streaming and non-streaming variants
+```haskell
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Ollama
 
+calculatorTool :: Tool
+calculatorTool = Tool "function" $ FunctionDef
+  { fnName = "add"
+  , fnDescription = Just "Add two numbers"
+  , fnParameters = Just (FunctionParameters "object" Nothing (Just ["a", "b"]) Nothing Nothing Nothing)
+  , fnStrict = Just True
+  }
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let req = (chatRequest "qwen3.5:2b" (userMessage "What is 40 + 2?" :| []))
+        { chatTools = Just [calculatorTool] }
+  res <- chat client req
+  case res of
+    Left err   -> print err
+    Right resp -> print (crMessage resp)
+```
+
 ---
 
-## 🛠 Prerequisite
+## Structured Outputs (JSON Schema DSL)
 
-Make sure you have [Ollama installed and running locally](https://ollama.com/download). Run `ollama pull llama3` to download a model.
+Enforce structured JSON output formats using `SchemaBuilder`:
 
+```haskell
+import Data.Text.IO qualified as TIO
+import Ollama
+import Ollama.Types.Format.SchemaBuilder
+
+personSchema :: Schema
+personSchema = buildSchema $ emptyObject
+  |+ ("name", JString)
+  |+ ("age", JInteger)
+  |! "name"
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let req = (generateRequest "qwen3.5:2b" "Generate a person profile.")
+        { genFormat = Just (SchemaFormat personSchema) }
+  res <- generate client req
+  case res of
+    Left err   -> print err
+    Right resp -> TIO.putStrLn (grResponse resp)
+```
+
 ---
 
-## 🧪 Dev & Nix Support
+## Environment Variables & Configuration
 
-Use Nix:
+Construct a client using environment variables (`OLLAMA_HOST`, `OLLAMA_API_KEY`):
 
-```bash
-nix-shell
+```haskell
+main :: IO ()
+main = do
+  client <- clientFromEnv
+  -- Automatically connects to OLLAMA_HOST with optional Authorization: Bearer header
+  ...
 ```
 
-This will install `stack` and Ollama.
+Or configure custom retry policies and loggers:
 
+```haskell
+customConfig :: OllamaClientConfig
+customConfig = defaultConfig
+  { configBaseUrl = "http://my-ollama-server:11434"
+  , configTimeout = 120
+  , configRetry   = ExponentialRetry 3 1 -- 3 retries with exponential backoff
+  , configLogger  = Just (\level msg -> putStrLn $ "[" <> show level <> "] " <> show msg)
+  }
+
+main :: IO ()
+main = withClient customConfig $ \client -> do
+  ...
+```
+
 ---
 
-## 👨‍💻 Author
+## Documentation & SDK Comparison
 
-Created and maintained by [@tusharad](https://github.com/tusharad). PRs and feedback are welcome!
+- [doc/COMPARISON.md](doc/COMPARISON.md) — SDK Feature Matrix comparing `ollama-haskell` with Python, JS/TS, and Go SDKs.
+- [ARCHITECTURE.md](ARCHITECTURE.md) — Detailed internal module design and extension guide.
+- [CONTRIBUTING.md](CONTRIBUTING.md) — Development setup, testing guidelines, and code style.
+- [CHANGELOG.md](CHANGELOG.md) — Release notes and changelog.
+- [Hackage Documentation](https://hackage.haskell.org/package/ollama-haskell) — Full Haddock reference.
 
 ---
 
-## 🤝 Contributing
+## License
 
-Have ideas or improvements? Feel free to [open an issue](https://github.com/tusharad/ollama-haskell/issues) or submit a PR!
+MIT © 2024–2026 Tushar Adhatrao
diff --git a/bench/Main.hs b/bench/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/Main.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+{- |
+Module      : Main
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Benchmark suite for Ollama client JSON serialization and URL normalization operations.
+
+@since 1.0.0.0
+-}
+module Main (main) where
+
+import Data.Aeson (decode, encode)
+import Data.ByteString.Lazy (ByteString)
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Ollama.API.Chat (ChatRequest, ChatResponse, chatRequest)
+import Ollama.API.Embed (EmbedRequest, EmbedResponse, embedRequest)
+import Ollama.API.Generate (GenerateRequest, GenerateResponse, generateRequest)
+import Ollama.API.Models.Create (CreateRequest, defaultCreateRequest)
+import Ollama.Testing (mockChatResponse, mockEmbedResponse, mockGenerateResponse)
+import Ollama.Types.Common (ModelName (..))
+import Ollama.Types.Message (userMessage)
+import Test.Tasty.Bench (bench, bgroup, defaultMain, whnf)
+
+main :: IO ()
+main =
+  defaultMain
+    [ bgroup
+        "JSON Encoding"
+        [ bench "GenerateRequest" $ whnf encode sampleGenerateRequest
+        , bench "ChatRequest" $ whnf encode sampleChatRequest
+        , bench "EmbedRequest" $ whnf encode sampleEmbedRequest
+        , bench "CreateRequest" $ whnf encode sampleCreateRequest
+        ]
+    , bgroup
+        "JSON Decoding"
+        [ bench "GenerateResponse" $ whnf (decode @GenerateResponse) sampleGenerateBytes
+        , bench "ChatResponse" $ whnf (decode @ChatResponse) sampleChatBytes
+        , bench "EmbedResponse" $ whnf (decode @EmbedResponse) sampleEmbedBytes
+        ]
+    ]
+
+sampleGenerateRequest :: GenerateRequest
+sampleGenerateRequest = generateRequest (ModelName "llama3.2") "Why is the sky blue?"
+
+sampleChatRequest :: ChatRequest
+sampleChatRequest = chatRequest (ModelName "llama3.2") (userMessage "Hello, world!" :| [])
+
+sampleEmbedRequest :: EmbedRequest
+sampleEmbedRequest = embedRequest (ModelName "nomic-embed-text") ["Hello", "World", "Vector"]
+
+sampleCreateRequest :: CreateRequest
+sampleCreateRequest = defaultCreateRequest (ModelName "custom-llama")
+
+sampleGenerateBytes :: ByteString
+sampleGenerateBytes = encode $ mockGenerateResponse (ModelName "llama3.2") "Sky is blue"
+
+sampleChatBytes :: ByteString
+sampleChatBytes = encode $ mockChatResponse (ModelName "llama3.2") "Hello there"
+
+sampleEmbedBytes :: ByteString
+sampleEmbedBytes = encode $ mockEmbedResponse (ModelName "nomic") [[0.1, 0.2]]
diff --git a/doc/COMPARISON.md b/doc/COMPARISON.md
new file mode 100644
--- /dev/null
+++ b/doc/COMPARISON.md
@@ -0,0 +1,66 @@
+# SDK Feature & Architecture Comparison
+
+This document provides a comprehensive technical comparison between **`ollama-haskell` (v0.3.0.0)**, official SDKs in other ecosystems (**Python**, **JavaScript/TypeScript**, **Go**), and existing Haskell LLM libraries.
+
+---
+
+## 1. Feature Support Matrix Across Language SDKs
+
+| Feature / Endpoint | `ollama-haskell` (v0.3.0.0) | `ollama-python` | `ollama-js` | `ollama/ollama/api` (Go) |
+| :--- | :---: | :---: | :---: | :---: |
+| **Language Paradigm** | Pure Functional / Typed | Dynamic / Async | Dynamic / Promises | Imperative / Structs |
+| **Response Streaming** | `conduit` Pipelines | Iterator / AsyncGenerator | AsyncIterable | Channel / Callback |
+| **Chat Completions (`/api/chat`)** | ✅ | ✅ | ✅ | ✅ |
+| **Text Generation (`/api/generate`)** | ✅ | ✅ | ✅ | ✅ |
+| **Thinking / Reasoning Models** | ✅ `Think` ADT (`qwen3.5`, `deepseek-r1`) | ⚠️ Raw JSON parameter | ⚠️ Raw JSON parameter | ⚠️ Raw JSON parameter |
+| **Structured Output Schema** | ✅ Type-safe `SchemaBuilder` DSL | ⚠️ Pydantic / Raw Dict | ⚠️ Zod / Raw Schema | ⚠️ Struct tag / Raw Schema |
+| **Function / Tool Calling** | ✅ Strongly-typed `Tool` ADT | ✅ Dict / Callables | ✅ JSON schema objects | ✅ Go Structs |
+| **Vector Embeddings (`/api/embed`)** | ✅ | ✅ | ✅ | ✅ |
+| **Model Lifecycle (`copy`, `delete`, `show`, `ps`)** | ✅ | ✅ | ✅ | ✅ |
+| **Blob Management (`checkBlob`, `pushBlob`)** | ✅ | ✅ | ✅ | ✅ |
+| **Automatic Retry Policy** | ✅ `NoRetry`, `Constant`, `Exponential` | ❌ (User implementation) | ❌ (User implementation) | ❌ (User implementation) |
+| **Connection Lifecycles** | ✅ Automatic GC & Bracket | ⚠️ Manual session | ⚠️ Fetch client | ⚠️ Manual `http.Client` |
+| **STM Session Management** | ✅ `InMemoryStore` & `ConversationStore` | ❌ | ❌ | ❌ |
+| **Mock Testing Harness** | ✅ `Ollama.Testing` (`withMockClient`) | ❌ | ❌ | ❌ |
+
+---
+
+## 2. Comparison with Other Haskell LLM Libraries
+
+| Feature | `ollama-haskell` | `openai-hs` | `langchain-hs` |
+| :--- | :--- | :--- | :--- |
+| **Target Provider** | Local Ollama Engine | OpenAI Cloud API | Multi-provider Framework |
+| **Streaming Abstraction** | First-class `conduit` streams | Lazy ByteString / SSE | Custom Stream types |
+| **Retry & Resilience** | Integrated `retry` policy backoff | None | Basic |
+| **Mocking & Testing** | Built-in `Ollama.Testing` module | None | Mock handlers |
+| **Memory Persistence** | STM-backed `InMemoryStore` | Manual | Vector store abstractions |
+
+---
+
+## 3. Key Architectural Advantages of `ollama-haskell`
+
+1. **Type Safety & Smart Constructors**:
+   - String parameters are wrapped in domain-specific newtypes (`ModelName`, `Digest`, `Base64Image`, `Duration`, `Version`).
+   - `mkModelName` validates model name invariants at runtime.
+
+2. **First-Class Streaming via `conduit`**:
+   - `chatStream` and `generateStream` stream line-delimited JSON objects over constant memory pipelines without buffering entire responses in memory.
+   - Stream combinators `collectStream` and `foldStream` simplify consumption.
+
+3. **Domain-Specific Schema Builder (`SchemaBuilder`)**:
+   - Construct complex JSON Schemas using readable infix operators:
+     ```haskell
+     userSchema = buildSchema $
+       emptyObject
+         |+ ("name", JString)
+         |+ ("age", JInteger)
+         |! "name"
+     ```
+
+4. **Built-In Resilient Transport**:
+   - Exponential backoff retry strategies (`ExponentialRetry 3 1.0`).
+   - Lifecycle callbacks (`configOnStart`, `configOnSuccess`, `configOnError`).
+   - Customizable logger thresholds (`Debug`, `Info`, `Warn`, `Error`).
+
+5. **Mock Infrastructure (`Ollama.Testing`)**:
+   - Test application code deterministically offline using `withMockClient` without needing a running Ollama server during CI unit testing.
diff --git a/examples/AllFeatures.hs b/examples/AllFeatures.hs
new file mode 100644
--- /dev/null
+++ b/examples/AllFeatures.hs
@@ -0,0 +1,187 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+{- |
+Module      : Main
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Comprehensive example demonstrating all v1.0 features of ollama-haskell:
+  * Client builder with env resolution, custom headers, & retry policies
+  * Conduit-based response streaming
+  * Thinking / reasoning model integration
+  * Function / tool calling
+  * Structured JSON schema output
+  * Vector embeddings & token throughput metrics
+  * Transactional STM conversation storage
+
+@since 1.0.0.0
+-}
+module Main (main) where
+
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text qualified as T
+import Data.Time (getCurrentTime)
+import Ollama
+
+fastOptions :: Maybe ModelOptions
+fastOptions = Just (defaultOptions {optNumPredict = Just 20})
+
+main :: IO ()
+main = do
+  putStrLn "=========================================================="
+  putStrLn "   ollama-haskell v1.0 — All Features Showcase Example"
+  putStrLn "=========================================================="
+
+  -- 1. Client Builder with env vars, custom headers, & Exponential Retry Policy
+  putStrLn "\n[1] Initializing client..."
+  let customConfig =
+        defaultConfig
+          { configRetry = ExponentialRetry 3 1
+          , configHeaders = [("X-Custom-Client", "ollama-haskell-v1.0")]
+          }
+  client <- newClient customConfig
+  putStrLn "Client initialized with ExponentialRetry policy."
+
+  -- 2. System Inspection
+  putStrLn "\n[2] System Engine Version..."
+  verRes <- getVersion client
+  case verRes of
+    Left err -> putStrLn $ "Error fetching version: " <> show err
+    Right ver -> putStrLn $ "Ollama Engine Version: " <> T.unpack (unVersion ver)
+
+  putStrLn "\n[3] Listing Local Models..."
+  modelsRes <- listModels client
+  case modelsRes of
+    Left err -> putStrLn $ "Error listing models: " <> show err
+    Right (ListResponse ms) -> do
+      putStrLn $ "Found " <> show (length ms) <> " installed models:"
+      mapM_ (\m -> putStrLn $ "  - " <> T.unpack (unModelName (miName m))) ms
+
+  let model = "qwen3.5:2b"
+
+  -- 4. Conduit-Based Streaming Chat
+  putStrLn $ "\n[4] Streaming Chat with Conduit (" <> T.unpack (unModelName model) <> ")..."
+  let streamReq =
+        (chatRequest model (userMessage "Count from 1 to 5." :| []))
+          { chatOptions = fastOptions
+          , chatThink = Just ThinkDisabled
+          }
+  putStr "Response stream: "
+  chunks <- collectStream (chatStream client streamReq)
+  mapM_
+    ( \c -> case crMessage c of
+        Just msg -> putStr (T.unpack (messageContent msg))
+        Nothing -> pure ()
+    )
+    chunks
+  putStrLn ""
+
+  -- 5. Non-Streaming Chat with Token Throughput Metrics
+  putStrLn "\n[5] Non-Streaming Chat & Token Metrics..."
+  let chatReq =
+        ( chatRequest
+            model
+            (systemMessage "You are a concise assistant." :| [userMessage "Explain gravity in one sentence."])
+        )
+          { chatOptions = fastOptions
+          , chatThink = Just ThinkDisabled
+          }
+  chatRes <- chat client chatReq
+  case chatRes of
+    Left err -> putStrLn $ "Chat error: " <> show err
+    Right resp -> do
+      case crMessage resp of
+        Just msg -> putStrLn $ "Answer: " <> T.unpack (messageContent msg)
+        Nothing -> putStrLn "No message returned."
+      case chatEvalTokensPerSecond resp of
+        Just tps -> putStrLn $ "Generation Speed: " <> show tps <> " tokens/sec"
+        Nothing -> pure ()
+
+  -- 6. Thinking / Reasoning Model Integration
+  putStrLn "\n[6] Generation with Thinking Mode..."
+  let thinkReq =
+        (generateRequest model "What is 15 * 14?")
+          { genThink = Just (ThinkLevel ThinkMedium)
+          , genOptions = fastOptions
+          }
+  thinkRes <- generate client thinkReq
+  case thinkRes of
+    Left err -> putStrLn $ "Generate error: " <> show err
+    Right resp -> putStrLn $ "Result: " <> T.unpack (grResponse resp)
+
+  -- 7. Tool / Function Calling
+  putStrLn "\n[7] Tool / Function Calling..."
+  let calcTool =
+        Tool
+          { toolType = "function"
+          , toolFunction =
+              FunctionDef
+                { fnName = "calculator"
+                , fnDescription = Just "Perform basic math calculations"
+                , fnParameters =
+                    Just
+                      FunctionParameters
+                        { fpType = "object"
+                        , fpProperties = Nothing
+                        , fpRequired = Just ["expression"]
+                        , fpAdditionalProperties = Nothing
+                        , fpDescription = Nothing
+                        , fpEnum = Nothing
+                        }
+                , fnStrict = Just True
+                }
+          }
+      toolReq =
+        (chatRequest model (userMessage "Calculate 42 * 8" :| []))
+          { chatTools = Just [calcTool]
+          , chatOptions = fastOptions
+          , chatThink = Just ThinkDisabled
+          }
+  toolRes <- chat client toolReq
+  case toolRes of
+    Left err -> putStrLn $ "Tool chat error: " <> show err
+    Right resp -> case crMessage resp of
+      Just msg -> case messageToolCalls msg of
+        Just calls -> putStrLn $ "Model requested tool execution: " <> show calls
+        Nothing -> putStrLn $ "Response: " <> T.unpack (messageContent msg)
+      Nothing -> putStrLn "No message returned."
+
+  -- 8. Vector Embeddings
+  putStrLn "\n[8] Vector Embeddings..."
+  let embReq = embedRequest model ["Haskell AI development", "Ollama LLM client"]
+  embRes <- embed client embReq
+  case embRes of
+    Left err -> putStrLn $ "Embed error: " <> show err
+    Right resp -> putStrLn $ "Generated " <> show (length (erEmbeddings resp)) <> " embedding vectors."
+
+  -- 9. Transactional Conversation Store
+  putStrLn "\n[9] STM Conversation Store..."
+  store <- initInMemoryStore
+  now <- getCurrentTime
+  let convId = "demo-session-42"
+      session =
+        Conversation
+          convId
+          [systemMessage "Context saved.", userMessage "Favorite language is Haskell."]
+          model
+          now
+          now
+  saveConversationInMemory store session
+  retrieved <- loadConversationInMemory store convId
+  case retrieved of
+    Just c ->
+      putStrLn $
+        "Successfully loaded conversation ["
+          <> T.unpack (conversationId c)
+          <> "] with "
+          <> show (length (messages c))
+          <> " messages."
+    Nothing -> putStrLn "Failed to load conversation."
+
+  closeClient client
+  putStrLn "\n=========================================================="
+  putStrLn "   Showcase Completed Successfully!"
+  putStrLn "=========================================================="
diff --git a/examples/BasicChat.hs b/examples/BasicChat.hs
new file mode 100644
--- /dev/null
+++ b/examples/BasicChat.hs
@@ -0,0 +1,21 @@
+module Main (main) where
+
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text.IO qualified as TIO
+import Ollama
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let opts = Just (defaultOptions {optNumPredict = Just 20})
+      req =
+        (chatRequest "qwen3.5:2b" (userMessage "Why is the sky blue?" :| []))
+          { chatOptions = opts
+          , chatThink = Just ThinkDisabled
+          }
+  res <- chat client req
+  case res of
+    Left err -> putStrLn $ "Error: " <> show err
+    Right resp -> case crMessage resp of
+      Just msg -> TIO.putStrLn $ "Response: " <> messageContent msg
+      Nothing -> putStrLn "No content returned"
diff --git a/examples/Embeddings.hs b/examples/Embeddings.hs
new file mode 100644
--- /dev/null
+++ b/examples/Embeddings.hs
@@ -0,0 +1,12 @@
+module Main (main) where
+
+import Ollama
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let req = embedRequest "qwen3.5:2b" ["Hello world", "Haskell LLM integration"]
+  res <- embed client req
+  case res of
+    Left err -> putStrLn $ "Error: " <> show err
+    Right resp -> putStrLn $ "Generated " <> show (length $ erEmbeddings resp) <> " embeddings."
diff --git a/examples/ModelManagement.hs b/examples/ModelManagement.hs
new file mode 100644
--- /dev/null
+++ b/examples/ModelManagement.hs
@@ -0,0 +1,13 @@
+module Main (main) where
+
+import Ollama
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  res <- listModels client
+  case res of
+    Left err -> putStrLn $ "Error listing models: " <> show err
+    Right (ListResponse ms) -> do
+      putStrLn "Installed Models:"
+      mapM_ (print . miName) ms
diff --git a/examples/StreamingChat.hs b/examples/StreamingChat.hs
new file mode 100644
--- /dev/null
+++ b/examples/StreamingChat.hs
@@ -0,0 +1,18 @@
+module Main (main) where
+
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text.IO qualified as TIO
+import Ollama
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let opts = Just (defaultOptions {optNumPredict = Just 20})
+      req =
+        (chatRequest "qwen3.5:2b" (userMessage "Count from 1 to 5." :| []))
+          { chatOptions = opts
+          , chatThink = Just ThinkDisabled
+          }
+  chunks <- collectStream (chatStream client req)
+  mapM_ (TIO.putStr . maybe "" messageContent . crMessage) chunks
+  putStrLn ""
diff --git a/examples/StructuredOutput.hs b/examples/StructuredOutput.hs
new file mode 100644
--- /dev/null
+++ b/examples/StructuredOutput.hs
@@ -0,0 +1,28 @@
+module Main (main) where
+
+import Data.Text.IO qualified as TIO
+import Ollama
+import Ollama.Types.Format.SchemaBuilder
+
+personSchema :: Schema
+personSchema =
+  buildSchema $
+    emptyObject
+      |+ ("name", JString)
+      |+ ("age", JInteger)
+      |! "name"
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let opts = Just (defaultOptions {optNumPredict = Just 20})
+      req =
+        (generateRequest "qwen3.5:2b" "Generate a person profile.")
+          { genFormat = Just (SchemaFormat personSchema)
+          , genOptions = opts
+          , genThink = Just ThinkDisabled
+          }
+  res <- generate client req
+  case res of
+    Left err -> putStrLn $ "Error: " <> show err
+    Right resp -> TIO.putStrLn $ "Structured Response:\n" <> grResponse resp
diff --git a/examples/ToolCalling.hs b/examples/ToolCalling.hs
new file mode 100644
--- /dev/null
+++ b/examples/ToolCalling.hs
@@ -0,0 +1,41 @@
+module Main (main) where
+
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Ollama
+
+calculatorTool :: Tool
+calculatorTool =
+  Tool
+    { toolType = "function"
+    , toolFunction =
+        FunctionDef
+          { fnName = "add"
+          , fnDescription = Just "Add two numbers"
+          , fnParameters =
+              Just
+                FunctionParameters
+                  { fpType = "object"
+                  , fpProperties = Nothing
+                  , fpRequired = Just ["a", "b"]
+                  , fpAdditionalProperties = Nothing
+                  , fpDescription = Nothing
+                  , fpEnum = Nothing
+                  }
+          , fnStrict = Just True
+          }
+    }
+
+main :: IO ()
+main = do
+  client <- defaultClient
+  let opts = Just (defaultOptions {optNumPredict = Just 20})
+      req =
+        (chatRequest "qwen3.5:2b" (userMessage "What is 25 + 17?" :| []))
+          { chatTools = Just [calculatorTool]
+          , chatOptions = opts
+          , chatThink = Just ThinkDisabled
+          }
+  res <- chat client req
+  case res of
+    Left err -> putStrLn $ "Error: " <> show err
+    Right resp -> print (crMessage resp)
diff --git a/ollama-haskell.cabal b/ollama-haskell.cabal
--- a/ollama-haskell.cabal
+++ b/ollama-haskell.cabal
@@ -1,115 +1,241 @@
-cabal-version: 1.12
-
--- This file has been generated from package.yaml by hpack version 0.38.1.
---
--- see: https://github.com/sol/hpack
-
-name:           ollama-haskell
-version:        0.2.1.0
-synopsis:       Haskell client for ollama.
-description:    Ollama client for Haskell
-category:       Web
-homepage:       https://github.com/tusharad/ollama-haskell#readme
-bug-reports:    https://github.com/tusharad/ollama-haskell/issues
-author:         tushar
-maintainer:     tusharadhatrao@gmail.com
-copyright:      2024 tushar
-license:        MIT
-license-file:   LICENSE
-build-type:     Simple
-extra-source-files:
-    README.md
-    CHANGELOG.md
+cabal-version: 3.0
+name:          ollama-haskell
+version:       0.3.0.0
+synopsis:      Industry-grade Haskell client for the Ollama API
+description:
+  A type-safe, well-tested Haskell client for interacting with
+  locally-running LLMs via the Ollama HTTP API. Supports chat,
+  text generation, embeddings, model management, streaming via
+  conduit, structured outputs, tool calling, and more.
+category:      Web, AI, Network
+license:       MIT
+license-file:  LICENSE
+author:        Tushar Adhatrao
+maintainer:    tusharadhatrao@gmail.com
+copyright:     2024-2026 Tushar Adhatrao
+homepage:      https://github.com/tusharad/ollama-haskell
+bug-reports:   https://github.com/tusharad/ollama-haskell/issues
+tested-with:   GHC == 9.4.8, GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3, GHC == 9.12.4
+stability:     stable
+extra-doc-files:
+  README.md
+  CHANGELOG.md
+  doc/COMPARISON.md
 
 source-repository head
   type: git
   location: https://github.com/tusharad/ollama-haskell
 
+flag integration-tests
+  description: Build and run integration tests (requires running Ollama server)
+  default: False
+  manual: True
+
+common warnings
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wmissing-export-lists
+    -Wmissing-home-modules
+    -Wpartial-fields
+    -Wredundant-constraints
+
+common lang
+  default-language: GHC2021
+  default-extensions:
+    StrictData
+    DerivingStrategies
+    DeriveGeneric
+    DeriveAnyClass
+    GeneralizedNewtypeDeriving
+    OverloadedStrings
+    ImportQualifiedPost
+    TypeApplications
+    RecordWildCards
+    NamedFieldPuns
+    LambdaCase
+    MultiWayIf
+    ScopedTypeVariables
+    BangPatterns
+
 library
+  import: warnings, lang
+  hs-source-dirs: src
   exposed-modules:
-      Data.Ollama.Blob
-      Data.Ollama.Chat
-      Data.Ollama.Common.Config
-      Data.Ollama.Common.Error
-      Data.Ollama.Common.SchemaBuilder
-      Data.Ollama.Common.Types
-      Data.Ollama.Common.Utils
-      Data.Ollama.Conversation
-      Data.Ollama.Copy
-      Data.Ollama.Create
-      Data.Ollama.Delete
-      Data.Ollama.Embeddings
-      Data.Ollama.Generate
-      Data.Ollama.List
-      Data.Ollama.Load
-      Data.Ollama.Ps
-      Data.Ollama.Pull
-      Data.Ollama.Push
-      Data.Ollama.Show
-      Ollama
+    Ollama
+    Ollama.Client
+    Ollama.Client.Config
+    Ollama.API.Generate
+    Ollama.API.Chat
+    Ollama.API.Embed
+    Ollama.API.Models
+    Ollama.API.Models.Create
+    Ollama.API.Models.Pull
+    Ollama.API.Models.Push
+    Ollama.API.Blobs
+    Ollama.API.Ps
+    Ollama.API.Version
+    Ollama.Types
+    Ollama.Types.Message
+    Ollama.Types.Tool
+    Ollama.Types.Model
+    Ollama.Types.Options
+    Ollama.Types.Format
+    Ollama.Types.Format.SchemaBuilder
+    Ollama.Types.Common
+    Ollama.Error
+    Ollama.Streaming
+    Ollama.Testing
+    Ollama.Conversation
   other-modules:
-      Paths_ollama_haskell
-  hs-source-dirs:
-      src
-  default-extensions:
-      ImportQualifiedPost
-  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+    Ollama.Client.Internal
   build-depends:
-      aeson ==2.*
-    , base >=4.7 && <5
-    , base64-bytestring ==1.*
-    , bytestring >=0.10 && <0.13
-    , containers >=0.6 && <0.9
-    , directory >=1 && <1.4
-    , filepath >=1 && <1.6
-    , http-client >=0.6 && <0.8
-    , http-client-tls >=0.2 && <0.4
-    , http-types >=0.7 && <0.13
-    , mtl ==2.*
-    , stm ==2.*
-    , text >=1 && <3
-    , time ==1.*
-  default-language: Haskell2010
+      aeson            >= 2.0 && < 3
+    , base             >= 4.17 && < 5
+    , bytestring       >= 0.10 && < 0.13
+    , case-insensitive >= 1.2 && < 1.3
+    , conduit          >= 1.3 && < 1.4
+    , conduit-extra    >= 1.3 && < 1.4
+    , containers       >= 0.6 && < 0.9
+    , hashable         >= 1.4 && < 1.6
+    , http-client      >= 0.6 && < 0.8
+    , http-client-tls  >= 0.2 && < 0.5
+    , http-types       >= 0.7 && < 0.13
+    , mtl              >= 2.2 && < 3
+    , retry            >= 0.9 && < 0.10
+    , resourcet        >= 1.3 && < 1.4
+    , stm              >= 2.5 && < 3
+    , text             >= 2.0 && < 3
+    , time             >= 1.11 && < 2
+    , unliftio-core    >= 0.2 && < 0.3
 
 test-suite ollama-haskell-test
+  import: warnings, lang
   type: exitcode-stdio-1.0
+  hs-source-dirs: test
   main-is: Main.hs
   other-modules:
-      Test.Ollama.Blob
-      Test.Ollama.Chat
-      Test.Ollama.Common
-      Test.Ollama.Copy
-      Test.Ollama.Create
-      Test.Ollama.Delete
-      Test.Ollama.Embedding
-      Test.Ollama.Generate
-      Test.Ollama.List
-      Test.Ollama.Load
-      Test.Ollama.Show
-      Paths_ollama_haskell
-  hs-source-dirs:
-      test
-  default-extensions:
-      ImportQualifiedPost
-  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+    Test.Ollama.Unit.Types
+    Test.Ollama.Unit.Error
+    Test.Ollama.Unit.Config
+    Test.Ollama.Unit.SchemaBuilder
+    Test.Ollama.Unit.Testing
+    Test.Ollama.Property.Arbitrary
+    Test.Ollama.Property.Roundtrip
+    Test.Ollama.Golden.Chat
+    Test.Ollama.Golden.Generate
+    Test.Ollama.Golden.Embed
+    Test.Ollama.Golden.Models
   build-depends:
-      aeson
-    , base >=4.7 && <5
-    , base64-bytestring ==1.*
-    , bytestring >=0.10 && <0.13
-    , containers >=0.6 && <0.9
-    , directory >=1 && <1.4
-    , filepath >=1 && <1.6
-    , http-client >=0.6 && <0.8
-    , http-client-tls >=0.2 && <0.4
-    , http-types >=0.7 && <0.13
-    , mtl ==2.*
+      base
     , ollama-haskell
-    , scientific
-    , silently
-    , stm ==2.*
-    , tasty >=1.5
+    , aeson
+    , bytestring
+    , containers
+    , QuickCheck       >= 2.14
+    , tasty          >= 1.5
+    , tasty-golden   >= 2.3
     , tasty-hunit
+    , tasty-quickcheck >= 0.10
     , text
-    , time ==1.*
-  default-language: Haskell2010
+    , time
+    , conduit
+
+test-suite ollama-haskell-integration
+  import: warnings, lang
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test-integration
+  main-is: Main.hs
+  if !flag(integration-tests)
+    buildable: False
+  build-depends:
+      base
+    , ollama-haskell
+    , aeson
+    , tasty
+    , tasty-hunit
+    , text
+    , time
+
+benchmark ollama-haskell-bench
+  import: warnings, lang
+  type: exitcode-stdio-1.0
+  hs-source-dirs: bench
+  main-is: Main.hs
+  build-depends:
+      base
+    , ollama-haskell
+    , aeson
+    , bytestring
+    , tasty          >= 1.5
+    , tasty-bench    >= 0.3
+    , text
+
+executable ollama-example-basic-chat
+  import: warnings, lang
+  main-is: BasicChat.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-streaming-chat
+  import: warnings, lang
+  main-is: StreamingChat.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-tool-calling
+  import: warnings, lang
+  main-is: ToolCalling.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-structured-output
+  import: warnings, lang
+  main-is: StructuredOutput.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-embeddings
+  import: warnings, lang
+  main-is: Embeddings.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-model-management
+  import: warnings, lang
+  main-is: ModelManagement.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , text
+
+executable ollama-example-all-features
+  import: warnings, lang
+  main-is: AllFeatures.hs
+  hs-source-dirs: examples
+  build-depends:
+      base
+    , ollama-haskell
+    , aeson
+    , text
+    , time
+
diff --git a/src/Data/Ollama/Blob.hs b/src/Data/Ollama/Blob.hs
deleted file mode 100644
--- a/src/Data/Ollama/Blob.hs
+++ /dev/null
@@ -1,176 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Blob
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functions for managing binary large objects (blobs) in the Ollama API.
-
-This module provides functionality for checking the existence of blobs and uploading
-files as blobs to the Ollama server. Blobs are used when creating models from
-GGUF files or safetensors directories.
-
-The blob operations interact with the @\/api\/blobs@ endpoints:
-
-- 'checkBlobExists' sends a HEAD request to check if a blob exists
-- 'createBlob' uploads a file as a blob using streaming POST
-
-Example usage:
-
->>> let digest = "sha256:29fdb92e57cf0827ded04ae6461b5931d01fa595843f55d36f5b275a52087dd2"
->>> exists <- checkBlobExists digest Nothing
->>> case exists of
->>>   Right True -> putStrLn "Blob exists"
->>>   Right False -> putStrLn "Blob not found"
->>>   Left err -> putStrLn $ "Error: " ++ show err
-
->>> result <- createBlob "model.gguf" digest Nothing
->>> case result of
->>>   Right () -> putStrLn "Blob uploaded successfully"
->>>   Left err -> putStrLn $ "Upload failed: " ++ show err
--}
-module Data.Ollama.Blob
-  ( -- * Blob Operations
-    checkBlobExists
-  , createBlob
-  ) where
-
-import Control.Exception (try)
-import Data.ByteString qualified as BS
-import Data.Maybe (fromMaybe)
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Data.Ollama.Common.Error
-import Data.Ollama.Common.Error qualified as Error
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Text.Encoding qualified as TE
-import Network.HTTP.Client
-import Network.HTTP.Client.TLS
-import Network.HTTP.Types (Status (statusCode))
-import System.Directory (doesFileExist)
-
-{- | Check if a blob exists on the Ollama server.
-
-Sends a HEAD request to @\/api\/blobs\/sha256:\<digest\>@ to check if a blob
-with the given SHA256 digest exists on the server.
-
-Returns:
-- 'Right' 'True' if the blob exists (HTTP 200 OK)
-- 'Right' 'False' if the blob does not exist (HTTP 404 Not Found)
-- 'Left' 'OllamaError' for other errors (network issues, invalid digest, etc.)
-
-The digest should be a SHA256 hash string without the "sha256:" prefix.
--}
-checkBlobExists ::
-  -- | SHA256 digest of the blob (without "sha256:" prefix)
-  Text ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError Bool)
-checkBlobExists digest mbOllamaConfig = do
-  let OllamaConfig {..} = fromMaybe defaultOllamaConfig mbOllamaConfig
-      endpoint = "/api/blobs/sha256:" <> digest
-      fullUrl = T.unpack $ hostUrl <> endpoint
-      timeoutMicros = timeout * 1000000
-
-  manager <- case commonManager of
-    Nothing ->
-      newTlsManagerWith
-        tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro timeoutMicros}
-    Just m -> pure m
-
-  eRequest <- try $ parseRequest fullUrl
-  case eRequest of
-    Left ex -> return $ Left $ Error.HttpError ex
-    Right req -> do
-      let request = req {method = "HEAD"}
-      eResponse <- try $ httpNoBody request manager
-      case eResponse of
-        Left ex -> return $ Left $ Error.HttpError ex
-        Right response -> do
-          let status = statusCode $ responseStatus response
-          case status of
-            200 -> return $ Right True
-            404 -> return $ Right False
-            _ ->
-              return $
-                Left $
-                  Error.ApiError $
-                    "Unexpected status code: " <> T.pack (show status)
-
-{- | Upload a file as a blob to the Ollama server.
-
-Streams the contents of the file at the given 'FilePath' to the Ollama server
-via a POST request to @\/api\/blobs\/sha256:\<digest\>@. The server will verify
-that the uploaded file matches the expected SHA256 digest.
-
-Returns:
-- 'Right' @()@ if the blob was uploaded successfully (HTTP 201 Created)
-- 'Left' 'OllamaError' if the upload failed
-
-The digest should be the expected SHA256 hash of the file without the "sha256:" prefix.
-If the file content doesn't match the digest, the server will return an error.
-
-Note: This function streams the file content, so it can handle large files efficiently
-without loading them entirely into memory.
--}
-createBlob ::
-  -- | Path to the file to upload
-  FilePath ->
-  -- | Expected SHA256 digest of the file (without "sha256:" prefix)
-  Text ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError ())
-createBlob filePath digest mbOllamaConfig = do
-  let OllamaConfig {..} = fromMaybe defaultOllamaConfig mbOllamaConfig
-      endpoint = "/api/blobs/sha256:" <> digest
-      fullUrl = T.unpack $ hostUrl <> endpoint
-      timeoutMicros = timeout * 1000000
-
-  manager <- case commonManager of
-    Nothing ->
-      newTlsManagerWith
-        tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro timeoutMicros}
-    Just m -> pure m
-
-  eRequest <- try $ parseRequest fullUrl
-  case eRequest of
-    Left ex -> return $ Left $ Error.HttpError ex
-    Right req -> do
-      -- check if file exists
-      fileExists <- doesFileExist filePath
-      if not fileExists
-        then return $ Left $ Error.ApiError "File does not exist"
-        else do
-          -- Create a request body from the file
-          fileContent <- BS.readFile filePath
-          let request =
-                req
-                  { method = "POST"
-                  , requestBody = RequestBodyBS fileContent
-                  }
-
-          eResponse <- try $ withResponse request manager $ \response -> do
-            let status = statusCode $ responseStatus response
-            case status of
-              201 -> return $ Right ()
-              400 -> do
-                bodyReader <- brRead $ responseBody response
-                return $
-                  Left $
-                    Error.ApiError $
-                      "Bad request - digest mismatch: " <> TE.decodeUtf8 bodyReader
-              _ -> do
-                bodyReader <- brRead $ responseBody response
-                return $
-                  Left $
-                    Error.ApiError $
-                      "Unexpected status code " <> T.pack (show status) <> ": " <> TE.decodeUtf8 bodyReader
-
-          case eResponse of
-            Left ex -> return $ Left $ Error.HttpError ex
-            Right result -> return result
diff --git a/src/Data/Ollama/Chat.hs b/src/Data/Ollama/Chat.hs
deleted file mode 100644
--- a/src/Data/Ollama/Chat.hs
+++ /dev/null
@@ -1,295 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Chat
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Chat functionality for interacting with the Ollama API.
-
-This module provides functions and types for initiating and managing chat interactions with an Ollama model.
-It includes APIs for sending chat requests, constructing messages with different roles, and configuring chat
-operations. The module supports both streaming and non-streaming responses, as well as optional tools and
-structured output formats.
-
-The primary functions are 'chat' and 'chatM' for sending chat requests, and helper functions like
-'systemMessage', 'userMessage', 'assistantMessage', and 'toolMessage' for constructing messages.
-The 'ChatOps' type allows customization of chat parameters, and 'defaultChatOps' provides a convenient
-starting point for configuration.
-
-Example:
-
->>> let ops = defaultChatOps { chatModelName = "customModel", messages = userMessage "Hello!" :| [] }
->>> chat ops Nothing
-Either OllamaError ChatResponse
--}
-module Data.Ollama.Chat
-  ( -- * Chat APIs
-    chat
-  , chatM
-
-    -- * Message Types
-  , Message (..)
-  , Role (..)
-  , systemMessage
-  , userMessage
-  , assistantMessage
-  , toolMessage
-  , genMessage
-
-    -- * Chat Configuration
-  , defaultChatOps
-  , ChatOps (..)
-
-    -- * Response Types
-  , ChatResponse (..)
-  , Format (..)
-
-    -- * Configuration and Error Types
-  , OllamaConfig (..)
-  , defaultOllamaConfig
-  , OllamaError (..)
-  , ModelOptions (..)
-  , defaultModelOptions
-
-    -- * Tool and Function Types
-  , InputTool (..)
-  , FunctionDef (..)
-  , FunctionParameters (..)
-  , OutputFunction (..)
-  , ToolCall (..)
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.List.NonEmpty as NonEmpty
-import Data.Maybe (isNothing)
-import Data.Ollama.Common.Config
-import Data.Ollama.Common.Error (OllamaError (..))
-import Data.Ollama.Common.Types
-  ( ChatResponse (..)
-  , Format (..)
-  , FunctionDef (..)
-  , FunctionParameters (..)
-  , InputTool (..)
-  , Message (..)
-  , ModelOptions (..)
-  , OutputFunction (..)
-  , Role (..)
-  , ToolCall (..)
-  )
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import Data.Text qualified as T
-
-{- | Constructs a 'Message' with the specified role and content.
-
-Creates a 'Message' with the given 'Role' and textual content, setting optional fields
-('images', 'tool_calls', 'thinking') to 'Nothing'.
-
-Example:
-
->>> genMessage User "What's the weather like?"
-Message {role = User, content = "What's the weather like?", images = Nothing, tool_calls = Nothing, thinking = Nothing}
--}
-genMessage :: Role -> Text -> Message
-genMessage r c =
-  Message
-    { role = r
-    , content = c
-    , images = Nothing
-    , tool_calls = Nothing
-    , thinking = Nothing
-    }
-
-{- | Creates a 'Message' with the 'System' role.
-
-Example:
-
->>> systemMessage "You are a helpful assistant."
-Message {role = System, content = "You are a helpful assistant.", images = Nothing, tool_calls = Nothing, thinking = Nothing}
--}
-systemMessage :: Text -> Message
-systemMessage = genMessage System
-
-{- | Creates a 'Message' with the 'User' role.
-
-Example:
-
->>> userMessage "What's 2+2?"
-Message {role = User, content = "What's 2+2?", images = Nothing, tool_calls = Nothing, thinking = Nothing}
--}
-userMessage :: Text -> Message
-userMessage = genMessage User
-
-{- | Creates a 'Message' with the 'Assistant' role.
-
-Example:
-
->>> assistantMessage "2+2 equals 4."
-Message {role = Assistant, content = "2+2 equals 4.", images = Nothing, tool_calls = Nothing, thinking = Nothing}
--}
-assistantMessage :: Text -> Message
-assistantMessage = genMessage Assistant
-
-{- | Creates a 'Message' with the 'Tool' role.
-
-Example:
-
->>> toolMessage "Tool output: success"
-Message {role = Tool, content = "Tool output: success", images = Nothing, tool_calls = Nothing, thinking = Nothing}
--}
-toolMessage :: Text -> Message
-toolMessage = genMessage Tool
-
-{- | Validates 'ChatOps' to ensure required fields are non-empty.
-
-Checks that the 'chatModelName' is not empty and that no 'Message' in 'messages' has empty content.
-Returns 'Right' with the validated 'ChatOps' or 'Left' with an 'OllamaError' if validation fails.
-
-@since 0.2.0.0
--}
-validateChatOps :: ChatOps -> Either OllamaError ChatOps
-validateChatOps ops
-  | T.null (modelName ops) = Left $ InvalidRequest "Chat model name cannot be empty"
-  | any (T.null . content) (messages ops) =
-      Left $ InvalidRequest "Messages cannot have empty content"
-  | otherwise = Right ops
-
-{- | Configuration for initiating a chat with an Ollama model.
-
-Defines the parameters for a chat request, including the model name, messages, and optional settings
-for tools, response format, streaming, timeout, and model options.
--}
-data ChatOps = ChatOps
-  { modelName :: !Text
-  -- ^ The name of the chat model to be used (e.g., "gemma3").
-  , messages :: !(NonEmpty Message)
-  -- ^ A non-empty list of messages forming the conversation context.
-  , tools :: !(Maybe [InputTool])
-  -- ^ Optional tools that may be used in the chat.
-  , format :: !(Maybe Format)
-  -- ^ Optional format for the chat response (e.g., JSON or JSON schema).
-  --
-  -- @since 0.1.3.0
-  , stream :: !(Maybe (ChatResponse -> IO (), IO ()))
-  -- ^ Optional callback function to be called with each incoming response.
-  , keepAlive :: !(Maybe Int)
-  -- ^ Optional override for the response timeout in minutes (default: 15 minutes).
-  , options :: !(Maybe ModelOptions)
-  -- ^ Optional model parameters (e.g., temperature) as specified in the Modelfile.
-  --
-  -- @since 0.1.3.0
-  , think :: !(Maybe Bool)
-  -- ^ Optional flag to enable thinking mode.
-  --
-  -- @since 0.2.0.0
-  }
-
-instance Show ChatOps where
-  show
-    ( ChatOps
-        { modelName = m
-        , messages = ms
-        , tools = t
-        , format = f
-        , keepAlive = ka
-        , think = th
-        }
-      ) =
-      let messagesStr = show (toList ms)
-          toolsStr = show t
-          formatStr = show f
-          keepAliveStr = show ka
-          thinkStr = show th
-       in T.unpack m
-            ++ "\nMessages:\n"
-            ++ messagesStr
-            ++ "\n"
-            ++ toolsStr
-            ++ "\n"
-            ++ formatStr
-            ++ "\n"
-            ++ keepAliveStr
-            ++ "\n"
-            ++ thinkStr
-
-instance Eq ChatOps where
-  (==) a b =
-    modelName a == modelName b
-      && messages a == messages b
-      && tools a == tools b
-      && format a == format b
-      && keepAlive a == keepAlive b
-
-instance ToJSON ChatOps where
-  toJSON (ChatOps model_ messages_ tools_ format_ stream_ keepAlive_ options_ think_) =
-    object
-      [ "model" .= model_
-      , "messages" .= messages_
-      , "tools" .= tools_
-      , "format" .= format_
-      , "stream" .= if isNothing stream_ then Just False else Just True
-      , "keep_alive" .= keepAlive_
-      , "options" .= options_
-      , "think" .= think_
-      ]
-
-{- | Default configuration for initiating a chat.
-
-Provides a default 'ChatOps' with the "gemma3" model and a sample user message ("What is 2+2?").
-Can be customized by modifying fields as needed.
-
-Example:
-
->>> let ops = defaultChatOps { chatModelName = "customModel", messages = userMessage "Hello!" :| [] }
->>> chat ops Nothing
-Either OllamaError ChatResponse
--}
-defaultChatOps :: ChatOps
-defaultChatOps =
-  ChatOps
-    { modelName = "gemma3"
-    , messages = userMessage "What is 2+2?" :| []
-    , tools = Nothing
-    , format = Nothing
-    , stream = Nothing
-    , keepAlive = Nothing
-    , options = Nothing
-    , think = Nothing
-    }
-
-{- | Sends a chat request to the Ollama API.
-
-Validates the 'ChatOps' configuration and sends a POST request to the @\/api\/chat@ endpoint.
-Supports both streaming and non-streaming responses based on the 'stream' field in 'ChatOps'.
-Returns an 'Either' containing an 'OllamaError' on failure or a 'ChatResponse' on success.
-
-Example:
-
->>> let ops = defaultChatOps { chatModelName = "gemma3", messages = userMessage "What's the capital of France?" :| [] }
->>> chat ops Nothing
-Either OllamaError ChatResponse
--}
-chat :: ChatOps -> Maybe OllamaConfig -> IO (Either OllamaError ChatResponse)
-chat ops mbConfig =
-  case validateChatOps ops of
-    Left err -> return $ Left err
-    Right _ -> withOllamaRequest "/api/chat" "POST" (Just ops) mbConfig handler
-  where
-    handler = maybe commonNonStreamingHandler commonStreamHandler (stream ops)
-
-{- | MonadIO version of 'chat' for use in monadic contexts.
-
-Lifts the 'chat' function into a 'MonadIO' context, allowing it to be used in monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> let ops = defaultChatOps { chatModelName = "gemma3", messages = userMessage "Hello!" :| [] }
->>> runReaderT (chatM ops Nothing) someContext
-Either OllamaError ChatResponse
--}
-chatM :: MonadIO m => ChatOps -> Maybe OllamaConfig -> m (Either OllamaError ChatResponse)
-chatM ops mbCfg = liftIO $ chat ops mbCfg
diff --git a/src/Data/Ollama/Common/Config.hs b/src/Data/Ollama/Common/Config.hs
deleted file mode 100644
--- a/src/Data/Ollama/Common/Config.hs
+++ /dev/null
@@ -1,90 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Common.Config
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : A unified configuration type for controlling Ollama client behavior.
-
-== Overview
-
-This module defines the core configuration record used throughout the Ollama Haskell client.
-
-Use 'defaultOllamaConfig' as a starting point and customize it with helper functions
-like 'withOnModelStart', 'withOnModelFinish', or 'withOnModelError'.
-
-Includes settings for base URL, timeout, retry logic, and custom HTTP managers.
--}
-module Data.Ollama.Common.Config
-  ( -- * Configuration Type
-    OllamaConfig (..)
-
-    -- * Default Config
-  , defaultOllamaConfig
-
-    -- * Hook Helpers
-  , withOnModelStart
-  , withOnModelFinish
-  , withOnModelError
-  ) where
-
-import Data.Text (Text)
-import GHC.Generics
-import Network.HTTP.Client
-
-{- | Configuration for the Ollama client.
-Used across all requests to customize behavior such as timeouts, retries,
-custom HTTP manager, and lifecycle hooks.
-
-@since 0.2.0.0
--}
-data OllamaConfig = OllamaConfig
-  { hostUrl :: Text
-  -- ^ Base URL for the Ollama server (default: @http://127.0.0.1:11434@)
-  , timeout :: Int
-  -- ^ Timeout in seconds for API requests (ignored if 'commonManager' is set)
-  , onModelStart :: Maybe (IO ())
-  -- ^ Callback executed when a model starts
-  , onModelError :: Maybe (IO ())
-  -- ^ Callback executed if a model encounters an error
-  , onModelFinish :: Maybe (IO ())
-  -- ^ Callback executed when a model finishes (not called on error)
-  , retryCount :: Maybe Int
-  -- ^ Number of retries on failure (default: @0@ if 'Nothing')
-  , retryDelay :: Maybe Int
-  -- ^ Delay between retries in seconds (if applicable)
-  , commonManager :: Maybe Manager
-  -- ^ Shared HTTP manager; disables timeout and retry settings
-  }
-  deriving (Generic)
-
-{- | A default configuration pointing to @localhost:11434@ with 90s timeout
-and no hooks or retry logic.
--}
-defaultOllamaConfig :: OllamaConfig
-defaultOllamaConfig =
-  OllamaConfig
-    { hostUrl = "http://127.0.0.1:11434"
-    , timeout = 90
-    , onModelStart = Nothing
-    , onModelError = Nothing
-    , onModelFinish = Nothing
-    , retryCount = Nothing
-    , retryDelay = Nothing
-    , commonManager = Nothing
-    }
-
--- | Add a callback to be executed when a model starts.
-withOnModelStart :: IO () -> OllamaConfig -> OllamaConfig
-withOnModelStart f cfg = cfg {onModelStart = Just f}
-
--- | Add a callback to be executed when a model errors.
-withOnModelError :: IO () -> OllamaConfig -> OllamaConfig
-withOnModelError f cfg = cfg {onModelError = Just f}
-
--- | Add a callback to be executed when a model finishes successfully.
-withOnModelFinish :: IO () -> OllamaConfig -> OllamaConfig
-withOnModelFinish f cfg = cfg {onModelFinish = Just f}
diff --git a/src/Data/Ollama/Common/Error.hs b/src/Data/Ollama/Common/Error.hs
deleted file mode 100644
--- a/src/Data/Ollama/Common/Error.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-
-{- |
-Module      : Data.Ollama.Common.Error
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Unified error type for handling failures across the Ollama client.
-
-== Overview
-
-Defines the core 'OllamaError' type that wraps all potential errors
-encountered while interacting with the Ollama API, including HTTP errors,
-JSON decoding failures, API-specific errors, file I/O errors, and timeouts.
--}
-module Data.Ollama.Common.Error
-  ( -- * Error Types
-    OllamaError (..)
-
-    -- * Decoding Utilities
-  , DecodingErrorMessage
-  , DecodingFailedValue
-  ) where
-
-import Control.Exception (Exception, IOException)
-import Data.Text (Text)
-import GHC.Generics
-import Network.HTTP.Client (HttpException)
-
--- | Type alias for a decoding error message string.
-type DecodingErrorMessage = String
-
--- | Type alias for the value that failed to decode.
-type DecodingFailedValue = String
-
-{- | Represents all possible errors that may occur when using the Ollama client.
-
-@since 0.2.0.0
--}
-data OllamaError
-  = -- | Low-level HTTP exception (connection failure, etc.)
-    HttpError HttpException
-  | -- | Failure to decode a JSON response, includes message and raw value
-    DecodeError DecodingErrorMessage DecodingFailedValue
-  | -- | Error returned from Ollama's HTTP API
-    ApiError Text
-  | -- | Error during file operations (e.g., loading an image)
-    FileError IOException
-  | -- | Mismatch in expected JSON schema or structure
-    JsonSchemaError String
-  | -- | Request timed out
-    TimeoutError String
-  | -- | Request is malformed or violates input constraints
-    InvalidRequest String
-  deriving (Show, Generic)
-
-instance Eq OllamaError where
-  (HttpError _) == (HttpError _) = True
-  x == y = eqOllamaError x y
-    where
-      eqOllamaError :: OllamaError -> OllamaError -> Bool
-      eqOllamaError = (==)
-
-instance Exception OllamaError
diff --git a/src/Data/Ollama/Common/SchemaBuilder.hs b/src/Data/Ollama/Common/SchemaBuilder.hs
deleted file mode 100644
--- a/src/Data/Ollama/Common/SchemaBuilder.hs
+++ /dev/null
@@ -1,191 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Common.SchemaBuilder
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : DSL for constructing structured JSON Schemas for Ollama's structured output API.
-
-== Overview
-
-This module defines a simple schema builder DSL for programmatically constructing
-JSON Schemas compatible with the structured output features in the Ollama API.
-
-It supports nested objects, arrays, required fields, and custom types, and
-provides infix operators for a fluent and expressive syntax.
-
-== Example
-
-@
-import Data.Ollama.Common.SchemaBuilder
-
-let schema =
-      emptyObject
-        |+ ("name", JString)
-        |+ ("age", JInteger)
-        |++ ("address", buildSchema $
-              emptyObject
-                |+ ("city", JString)
-                |+ ("zip", JInteger)
-                |! "city"
-            )
-        |!! ["name", "age"]
-        & buildSchema
-
-printSchema schema
-@
--}
-module Data.Ollama.Common.SchemaBuilder
-  ( -- * Core Types
-    JsonType (..)
-  , Property (..)
-  , Schema (..)
-
-    -- * Schema Construction
-  , emptyObject
-  , addProperty
-  , addObjectProperty
-  , requireField
-  , requireFields
-  , buildSchema
-
-    -- * Schema Utilities
-  , objectOf
-  , arrayOf
-  , toOllamaFormat
-  , printSchema
-
-    -- * Infix Schema DSL
-  , (|+)
-  , (|++)
-  , (|!)
-  , (|!!)
-  ) where
-
-import Data.Aeson
-import Data.Map.Strict qualified as HM
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Text.Lazy qualified as TL
-import Data.Text.Lazy.Encoding qualified as T
-import GHC.Generics
-
--- | Supported JSON types for schema generation.
-data JsonType
-  = JString
-  | JNumber
-  | JInteger
-  | JBoolean
-  | JNull
-  | -- | Array of a specific type
-    JArray JsonType
-  | -- | Nested object schema
-    JObject Schema
-  deriving (Show, Eq, Generic)
-
-instance ToJSON JsonType where
-  toJSON JString = "string"
-  toJSON JNumber = "number"
-  toJSON JInteger = "integer"
-  toJSON JBoolean = "boolean"
-  toJSON JNull = "null"
-  toJSON (JArray _) = "array"
-  toJSON (JObject _) = "object"
-
--- | A named property with a given type (supports nested values).
-newtype Property = Property JsonType
-  deriving (Show, Eq, Generic)
-
-instance ToJSON Property where
-  toJSON (Property (JArray itemType)) =
-    object ["type" .= ("array" :: Text), "items" .= Property itemType]
-  toJSON (Property (JObject schema)) = toJSON schema
-  toJSON (Property typ) = object ["type" .= typ]
-
-{- | Complete schema representation.
-
-@since 0.2.0.0
--}
-data Schema = Schema
-  { schemaProperties :: HM.Map Text Property
-  , schemaRequired :: [Text]
-  }
-  deriving (Show, Eq, Generic)
-
-instance ToJSON Schema where
-  toJSON (Schema props req) =
-    object
-      [ "type" .= ("object" :: Text)
-      , "properties" .= props
-      , "required" .= req
-      ]
-
--- | Internal builder for schema DSL.
-newtype SchemaBuilder = SchemaBuilder Schema
-  deriving (Show, Eq)
-
--- | Create an empty schema object.
-emptyObject :: SchemaBuilder
-emptyObject = SchemaBuilder $ Schema HM.empty []
-
--- | Add a simple field with a given name and type.
-addProperty :: Text -> JsonType -> SchemaBuilder -> SchemaBuilder
-addProperty name typ (SchemaBuilder s) =
-  SchemaBuilder $ s {schemaProperties = HM.insert name (Property typ) (schemaProperties s)}
-
--- | Add a nested object field with its own schema.
-addObjectProperty :: Text -> Schema -> SchemaBuilder -> SchemaBuilder
-addObjectProperty name nestedSchema (SchemaBuilder s) =
-  SchemaBuilder $
-    s {schemaProperties = HM.insert name (Property (JObject nestedSchema)) (schemaProperties s)}
-
--- | Mark a field as required.
-requireField :: Text -> SchemaBuilder -> SchemaBuilder
-requireField name (SchemaBuilder s) =
-  SchemaBuilder $ s {schemaRequired = name : schemaRequired s}
-
--- | Mark multiple fields as required.
-requireFields :: [Text] -> SchemaBuilder -> SchemaBuilder
-requireFields names builder = foldr requireField builder names
-
--- | Finalize the schema from a builder.
-buildSchema :: SchemaBuilder -> Schema
-buildSchema (SchemaBuilder s) = s
-
--- | Wrap a 'SchemaBuilder' as a nested object type.
-objectOf :: SchemaBuilder -> JsonType
-objectOf builder = JObject (buildSchema builder)
-
--- | Create an array of a given JSON type.
-arrayOf :: JsonType -> JsonType
-arrayOf = JArray
-
--- | Convert schema into a JSON 'Value' suitable for API submission.
-toOllamaFormat :: Schema -> Value
-toOllamaFormat = toJSON
-
--- | Pretty print a schema as formatted JSON.
-printSchema :: Schema -> IO ()
-printSchema = putStrLn . T.unpack . TL.toStrict . T.decodeUtf8 . encode
-
--- | Infix alias for 'addProperty'.
-(|+) :: SchemaBuilder -> (Text, JsonType) -> SchemaBuilder
-builder |+ (name, typ) = addProperty name typ builder
-
--- | Infix alias for 'addObjectProperty'.
-(|++) :: SchemaBuilder -> (Text, Schema) -> SchemaBuilder
-builder |++ (name, schema) = addObjectProperty name schema builder
-
--- | Infix alias for 'requireField'.
-(|!) :: SchemaBuilder -> Text -> SchemaBuilder
-builder |! name = requireField name builder
-
--- | Infix alias for 'requireFields'.
-(|!!) :: SchemaBuilder -> [Text] -> SchemaBuilder
-builder |!! names = requireFields names builder
-
-infixl 7 |+, |++
-infixl 6 |!, |!!
diff --git a/src/Data/Ollama/Common/Types.hs b/src/Data/Ollama/Common/Types.hs
deleted file mode 100644
--- a/src/Data/Ollama/Common/Types.hs
+++ /dev/null
@@ -1,464 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Common.Types
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Shared data types for request and response structures used throughout the Ollama client.
-
-== 📋 Overview
-
-This module defines common types for working with Ollama's API, including:
-
-- Chat messages and roles
-- Text generation responses
-- Structured function/tool calling
-- Model metadata
-- Streaming handling
-- Custom model parameters
-
-These types are consumed and returned by higher-level modules
-like `Data.Ollama.Chat`, `Data.Ollama.Generate`, and others.
-
-== Includes
-
-- Chat message structure and roles
-- Generate and chat response records
-- ModelOptions and advanced config
-- Structured function/tool call interfaces
-- JSON format hints and schema wrapping
-- Helper class 'HasDone' for streaming termination
-
-Most types implement `ToJSON`/`FromJSON` for direct API interaction.
--}
-module Data.Ollama.Common.Types
-  ( ModelDetails (..)
-  , Format (..)
-  , GenerateResponse (..)
-  , Message (..)
-  , Role (..)
-  , ChatResponse (..)
-  , HasDone (..)
-  , ModelOptions (..)
-  , InputTool (..)
-  , FunctionDef (..)
-  , FunctionParameters (..)
-  , ToolCall (..)
-  , OutputFunction (..)
-  , Version (..)
-  ) where
-
-import Data.Aeson
-import Data.Map qualified as HM
-import Data.Maybe (catMaybes)
-import Data.Ollama.Common.SchemaBuilder
-import Data.Text (Text)
-import Data.Time (UTCTime)
-import GHC.Generics
-import GHC.Int (Int64)
-
--- | Metadata describing a specific model's identity and configuration.
-data ModelDetails = ModelDetails
-  { parentModel :: !(Maybe Text)
-  -- ^ The parent model from which this model was derived, if any.
-  , format :: !Text
-  -- ^ The format used for the model (e.g., "gguf").
-  , family :: !Text
-  -- ^ The family name of the model (e.g., "llama", "mistral").
-  , families :: ![Text]
-  -- ^ Alternative or related family identifiers.
-  , parameterSize :: !Text
-  -- ^ The size of the model's parameters, typically expressed as a string (e.g., "7B").
-  , quantizationLevel :: Text
-  -- ^ The quantization level used (e.g., "Q4", "Q8").
-  }
-  deriving (Eq, Show)
-
-instance FromJSON ModelDetails where
-  parseJSON = withObject "ModelDetails" $ \v ->
-    ModelDetails
-      <$> v .: "parent_model"
-      <*> v .: "format"
-      <*> v .: "family"
-      <*> v .:? "families" .!= []
-      <*> v .: "parameter_size"
-      <*> v .: "quantization_level"
-
-{- | Format specification for the chat output.
-
-@since 0.1.3.0
--}
-data Format = JsonFormat | SchemaFormat Schema
-  deriving (Show, Eq)
-
-instance ToJSON Format where
-  toJSON JsonFormat = String "json"
-  toJSON (SchemaFormat schema) = toJSON schema
-
-{- |
-Result type for 'generate' function containing the model's response and meta-information.
--}
-data GenerateResponse = GenerateResponse
-  { model :: !Text
-  -- ^ The name of the model that generated the response.
-  , createdAt :: !UTCTime
-  -- ^ The timestamp when the response was created.
-  , genResponse :: !Text
-  -- ^ The generated response from the model.
-  , done :: !Bool
-  -- ^ A flag indicating whether the generation process is complete.
-  , totalDuration :: !(Maybe Int64)
-  -- ^ Optional total duration in milliseconds for the generation process.
-  , loadDuration :: !(Maybe Int64)
-  -- ^ Optional load duration in milliseconds for loading the model.
-  , promptEvalCount :: !(Maybe Int64)
-  -- ^ Optional count of prompt evaluations during the generation process.
-  , promptEvalDuration :: !(Maybe Int64)
-  -- ^ Optional duration in milliseconds for evaluating the prompt.
-  , evalCount :: !(Maybe Int64)
-  -- ^ Optional count of evaluations during the generation process.
-  , evalDuration :: !(Maybe Int64)
-  -- ^ Optional duration in milliseconds for evaluations during the generation process.
-  , thinking :: !(Maybe Text)
-  -- ^ Thinking of reasoning models; if think is set to true
-  --
-  -- @since 0.2.0.0
-  , doneReason :: !(Maybe Text)
-  -- ^ Reason why the generation process completed (e.g., "stop", "length", "cancel").
-  -- Available when the Ollama server provides completion reason information.
-  --
-  -- @since 0.2.1.0
-  }
-  deriving (Show, Eq)
-
-instance FromJSON GenerateResponse where
-  parseJSON = withObject "GenerateResponse" $ \v ->
-    GenerateResponse
-      <$> v .: "model"
-      <*> v .: "created_at"
-      <*> v .: "response"
-      <*> v .: "done"
-      <*> v .:? "total_duration"
-      <*> v .:? "load_duration"
-      <*> v .:? "prompt_eval_count"
-      <*> v .:? "prompt_eval_duration"
-      <*> v .:? "eval_count"
-      <*> v .:? "eval_duration"
-      <*> v .:? "thinking"
-      <*> v .:? "done_reason"
-
--- | Enumerated roles that can participate in a chat.
-data Role = System | User | Assistant | Tool
-  deriving (Show, Eq)
-
-instance ToJSON Role where
-  toJSON System = String "system"
-  toJSON User = String "user"
-  toJSON Assistant = String "assistant"
-  toJSON Tool = String "tool"
-
-instance FromJSON Role where
-  parseJSON = withText "Role" $ \t ->
-    case t of
-      "system" -> pure System
-      "user" -> pure User
-      "assistant" -> pure Assistant
-      "tool" -> pure Tool
-      _ -> fail $ "Invalid Role value: " <> show t
-
--- | Represents a message within a chat, including its role and content.
-data Message = Message
-  { role :: !Role
-  -- ^ The role of the entity sending the message (e.g., 'User', 'Assistant').
-  , content :: !Text
-  -- ^ The textual content of the message.
-  , images :: !(Maybe [Text])
-  -- ^ Optional list of base64 encoded images that accompany the message.
-  , tool_calls :: !(Maybe [ToolCall])
-  -- ^ a list of tools in JSON that the model wants to use
-  --
-  -- @since 0.1.3.0
-  , thinking :: !(Maybe Text)
-  --
-  -- @since 0.2.0.0
-  }
-  deriving (Show, Eq, Generic, ToJSON, FromJSON)
-
-data ChatResponse = ChatResponse
-  { model :: !Text
-  -- ^ The name of the model that generated this response.
-  , createdAt :: !UTCTime
-  -- ^ The timestamp when the response was created.
-  , message :: !(Maybe Message)
-  -- ^ The message content of the response, if any.
-  , done :: !Bool
-  -- ^ Indicates whether the chat process has completed.
-  , totalDuration :: !(Maybe Int64)
-  -- ^ Optional total duration in milliseconds for the chat process.
-  , loadDuration :: !(Maybe Int64)
-  -- ^ Optional load duration in milliseconds for loading the model.
-  , promptEvalCount :: !(Maybe Int64)
-  -- ^ Optional count of prompt evaluations during the chat process.
-  , promptEvalDuration :: !(Maybe Int64)
-  -- ^ Optional duration in milliseconds for evaluating the prompt.
-  , evalCount :: !(Maybe Int64)
-  -- ^ Optional count of evaluations during the chat process.
-  , evalDuration :: !(Maybe Int64)
-  -- ^ Optional duration in milliseconds for evaluations during the chat process.
-  , doneReason :: !(Maybe Text)
-  -- ^ Reason why the chat process completed (e.g., "stop", "length", "cancel").
-  -- Available when the Ollama server provides completion reason information.
-  --
-  -- @since 0.2.1.0
-  }
-  deriving (Show, Eq)
-
-instance FromJSON ChatResponse where
-  parseJSON = withObject "ChatResponse" $ \v ->
-    ChatResponse
-      <$> v .: "model"
-      <*> v .: "created_at"
-      <*> v .: "message"
-      <*> v .: "done"
-      <*> v .:? "total_duration"
-      <*> v .:? "load_duration"
-      <*> v .:? "prompt_eval_count"
-      <*> v .:? "prompt_eval_duration"
-      <*> v .:? "eval_count"
-      <*> v .:? "eval_duration"
-      <*> v .:? "done_reason"
-
--- | A workaround to use done field within commonStreamHandler
-class HasDone a where
-  getDone :: a -> Bool
-
-instance HasDone GenerateResponse where
-  getDone GenerateResponse {..} = done
-
-instance HasDone ChatResponse where
-  getDone ChatResponse {..} = done
-
-{- | Optional model tuning parameters that influence generation behavior.
-
-@since 0.2.0.0
--}
-data ModelOptions = ModelOptions
-  { numKeep :: Maybe Int
-  -- ^ Number of tokens to keep from the previous context.
-  , seed :: Maybe Int
-  -- ^ Random seed for reproducibility.
-  , numPredict :: Maybe Int
-  -- ^ Maximum number of tokens to predict.
-  , topK :: Maybe Int
-  -- ^ Top-K sampling parameter.
-  , topP :: Maybe Double
-  -- ^ Top-P (nucleus) sampling parameter.
-  , minP :: Maybe Double
-  -- ^ Minimum probability for nucleus sampling.
-  , typicalP :: Maybe Double
-  -- ^ Typical sampling probability.
-  , repeatLastN :: Maybe Int
-  -- ^ Number of tokens to consider for repetition penalty.
-  , temperature :: Maybe Double
-  -- ^ Sampling temperature. Higher = more randomness.
-  , repeatPenalty :: Maybe Double
-  -- ^ Penalty for repeating the same tokens.
-  , presencePenalty :: Maybe Double
-  -- ^ Penalty for introducing new tokens.
-  , frequencyPenalty :: Maybe Double
-  -- ^ Penalty for frequent tokens.
-  , penalizeNewline :: Maybe Bool
-  -- ^ Whether to penalize newline tokens.
-  , stop :: Maybe [Text]
-  -- ^ List of stop sequences to end generation.
-  , numa :: Maybe Bool
-  -- ^ Whether to enable NUMA-aware optimizations.
-  , numCtx :: Maybe Int
-  -- ^ Number of context tokens.
-  , numBatch :: Maybe Int
-  -- ^ Batch size used during generation.
-  , numGpu :: Maybe Int
-  -- ^ Number of GPUs to use.
-  , mainGpu :: Maybe Int
-  -- ^ Index of the primary GPU to use.
-  , useMmap :: Maybe Bool
-  -- ^ Whether to memory-map the model.
-  , numThread :: Maybe Int
-  -- ^ Number of threads to use for inference.
-  }
-  deriving (Show, Eq)
-
--- | Custom ToJSON instance for Options
-instance ToJSON ModelOptions where
-  toJSON opts =
-    object $
-      catMaybes
-        [ ("num_keep" .=) <$> numKeep opts
-        , ("seed" .=) <$> seed opts
-        , ("num_predict" .=) <$> numPredict opts
-        , ("top_k" .=) <$> topK opts
-        , ("top_p" .=) <$> topP opts
-        , ("min_p" .=) <$> minP opts
-        , ("typical_p" .=) <$> typicalP opts
-        , ("repeat_last_n" .=) <$> repeatLastN opts
-        , ("temperature" .=) <$> temperature opts
-        , ("repeat_penalty" .=) <$> repeatPenalty opts
-        , ("presence_penalty" .=) <$> presencePenalty opts
-        , ("frequency_penalty" .=) <$> frequencyPenalty opts
-        , ("penalize_newline" .=) <$> penalizeNewline opts
-        , ("stop" .=) <$> stop opts
-        , ("numa" .=) <$> numa opts
-        , ("num_ctx" .=) <$> numCtx opts
-        , ("num_batch" .=) <$> numBatch opts
-        , ("num_gpu" .=) <$> numGpu opts
-        , ("main_gpu" .=) <$> mainGpu opts
-        , ("use_mmap" .=) <$> useMmap opts
-        , ("num_thread" .=) <$> numThread opts
-        ]
-
--- | A wrapper for the Ollama engine version string.
-newtype Version = Version Text
-  deriving (Eq, Show)
-
-instance FromJSON Version where
-  parseJSON = withObject "version" $ \v -> do
-    Version <$> v .: "version"
-
-{- | Represents a tool that can be used in the conversation.
-
-@since 0.2.0.0
--}
-data InputTool = InputTool
-  { toolType :: Text
-  -- ^ The type of the tool
-  , function :: FunctionDef
-  -- ^ The function associated with the tool
-  }
-  deriving (Show, Eq, Generic)
-
-instance ToJSON InputTool where
-  toJSON InputTool {..} =
-    object
-      [ "type" .= toolType
-      , "function" .= function
-      ]
-
-instance FromJSON InputTool where
-  parseJSON = withObject "Tool" $ \v ->
-    InputTool
-      <$> v .: "type"
-      <*> v .: "function"
-
-{- | Represents a function that can be called by the model.
-
-@since 0.2.0.0
--}
-data FunctionDef = FunctionDef
-  { functionName :: Text
-  -- ^ The name of the function
-  , functionDescription :: Maybe Text
-  -- ^ Optional description of the function
-  , functionParameters :: Maybe FunctionParameters
-  -- ^ Optional parameters for the function
-  , functionStrict :: Maybe Bool
-  -- ^ Optional strictness flag
-  }
-  deriving (Show, Eq, Generic)
-
-instance ToJSON FunctionDef where
-  toJSON FunctionDef {..} =
-    object $
-      [ "name" .= functionName
-      ]
-        ++ maybe [] (\d -> ["description" .= d]) functionDescription
-        ++ maybe [] (\p -> ["parameters" .= p]) functionParameters
-        ++ maybe [] (\s -> ["strict" .= s]) functionStrict
-
-instance FromJSON FunctionDef where
-  parseJSON = withObject "Function" $ \v ->
-    FunctionDef
-      <$> v .: "name"
-      <*> v .:? "description"
-      <*> v .:? "parameters"
-      <*> v .:? "strict"
-
-{- | Parameters definition for a function call used in structured output or tool calls.
-
-@since 0.2.0.0
--}
-data FunctionParameters = FunctionParameters
-  { parameterType :: Text
-  -- ^ Type of the parameter (usually "object").
-  , parameterProperties :: Maybe (HM.Map Text FunctionParameters)
-  -- ^ Optional nested parameters as a property map.
-  , requiredParams :: Maybe [Text]
-  -- ^ List of required parameter names.
-  , additionalProperties :: Maybe Bool
-  -- ^ Whether additional (unspecified) parameters are allowed.
-  }
-  deriving (Show, Eq)
-
-instance ToJSON FunctionParameters where
-  toJSON FunctionParameters {..} =
-    object
-      [ "type" .= parameterType
-      , "properties" .= parameterProperties
-      , "required" .= requiredParams
-      , "additionalProperties" .= additionalProperties
-      ]
-
-instance FromJSON FunctionParameters where
-  parseJSON = withObject "parameters" $ \v ->
-    FunctionParameters
-      <$> v .: "type"
-      <*> v .: "properties"
-      <*> v .: "required"
-      <*> v .: "additionalProperties"
-
-{- | A single tool call returned from the model, containing the function to be invoked.
-
-@since 0.2.0.0
--}
-newtype ToolCall = ToolCall
-  { outputFunction :: OutputFunction
-  -- ^ The function the model intends to call, with arguments.
-  }
-  deriving (Show, Eq)
-
-{- | Output representation of a function to be called, including its name and arguments.
-
-@since 0.2.0.0
--}
-data OutputFunction = OutputFunction
-  { outputFunctionName :: Text
-  -- ^ The name of the function to invoke.
-  , arguments :: HM.Map Text Value
-  -- ^ A key-value map of argument names to values (JSON values).
-  }
-  deriving (Eq, Show)
-
-instance ToJSON OutputFunction where
-  toJSON OutputFunction {..} =
-    object
-      [ "name" .= outputFunctionName
-      , "arguments" .= arguments
-      ]
-
-instance FromJSON OutputFunction where
-  parseJSON = withObject "function" $ \v ->
-    OutputFunction
-      <$> v .: "name"
-      <*> v .: "arguments"
-
-instance ToJSON ToolCall where
-  toJSON ToolCall {..} = object ["function" .= outputFunction]
-
-instance FromJSON ToolCall where
-  parseJSON = withObject "tool_calls" $ \v ->
-    ToolCall <$> v .: "function"
diff --git a/src/Data/Ollama/Common/Utils.hs b/src/Data/Ollama/Common/Utils.hs
deleted file mode 100644
--- a/src/Data/Ollama/Common/Utils.hs
+++ /dev/null
@@ -1,303 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Common.Utils
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Utility functions for interacting with the Ollama API, including image encoding, HTTP request handling, and retry logic.
-
-This module provides helper functions for common tasks in the Ollama client, such as encoding images to Base64,
-sending HTTP requests to the Ollama API, handling streaming and non-streaming responses, and managing retries for failed requests.
-It also includes a default model options configuration and a function to retrieve the Ollama server version.
-
-The functions in this module are used internally by other modules like 'Data.Ollama.Chat' and 'Data.Ollama.Generate' but can also be used directly for custom API interactions.
--}
-module Data.Ollama.Common.Utils
-  ( -- * Image Encoding
-    encodeImage
-
-    -- * HTTP Request Handling
-  , withOllamaRequest
-  , commonNonStreamingHandler
-  , commonStreamHandler
-  , nonJsonHandler
-
-    -- * Model Options
-  , defaultModelOptions
-
-    -- * Retry Logic
-  , withRetry
-
-    -- * Version Retrieval
-  , getVersion
-  ) where
-
-import Control.Concurrent (threadDelay)
-import Control.Exception (IOException, try)
-import Data.Aeson
-import Data.ByteString qualified as BS
-import Data.ByteString.Base64 qualified as Base64
-import Data.ByteString.Lazy qualified as BSL
-import Data.Char (toLower)
-import Data.Maybe (fromMaybe)
-import Data.Ollama.Common.Config
-import Data.Ollama.Common.Error
-import Data.Ollama.Common.Error qualified as Error
-import Data.Ollama.Common.Types
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Text.Encoding qualified as TE
-import Network.HTTP.Client
-import Network.HTTP.Client.TLS
-import Network.HTTP.Types (Status (statusCode))
-import System.Directory
-import System.FilePath
-
--- | List of supported image file extensions for 'encodeImage'.
-supportedExtensions :: [String]
-supportedExtensions = [".jpg", ".jpeg", ".png"]
-
--- | Safely read a file, returning an 'Either' with an 'IOException' on failure.
-safeReadFile :: FilePath -> IO (Either IOException BS.ByteString)
-safeReadFile = try . BS.readFile
-
--- | Read a file if it exists, returning 'Nothing' if it does not.
-asPath :: FilePath -> IO (Maybe BS.ByteString)
-asPath filePath = do
-  exists <- doesFileExist filePath
-  if exists
-    then either (const Nothing) Just <$> safeReadFile filePath
-    else return Nothing
-
--- | Check if a file has a supported image extension.
-isSupportedExtension :: FilePath -> Bool
-isSupportedExtension p = map toLower (takeExtension p) `elem` supportedExtensions
-
-{- | Encodes an image file to Base64 format.
-
-Takes a file path to an image (jpg, jpeg, or png) and returns its data encoded as a Base64 'Text'.
-Returns 'Nothing' if the file extension is unsupported or the file cannot be read.
-This is useful for including images in API requests that expect Base64-encoded data, such as 'GenerateOps' images field.
--}
-encodeImage :: FilePath -> IO (Maybe Text)
-encodeImage filePath = do
-  if not (isSupportedExtension filePath)
-    then return Nothing
-    else do
-      maybeContent <- asPath filePath
-      return $ fmap (TE.decodeUtf8 . Base64.encode) maybeContent
-
-{- | Executes an action with retry logic for recoverable errors.
-
-Retries the given action up to the specified number of times with a delay (in seconds) between attempts.
-Only retries on recoverable errors such as HTTP errors, timeouts, JSON schema errors, or decoding errors.
--}
-withRetry ::
-  -- | Number of retries
-  Int ->
-  -- | Delay between retries in seconds
-  Int ->
-  -- | Action to execute, returning 'Either' 'OllamaError' or a result
-  IO (Either OllamaError a) ->
-  IO (Either OllamaError a)
-withRetry 0 _ action = action
-withRetry retries delaySeconds action = do
-  result <- action
-  case result of
-    Left err | isRetryableError err -> do
-      threadDelay (delaySeconds * 1000000) -- Convert to microseconds
-      withRetry (retries - 1) delaySeconds action
-    _ -> return result
-  where
-    isRetryableError (HttpError _) = True
-    isRetryableError (TimeoutError _) = True
-    isRetryableError (JsonSchemaError _) = True
-    isRetryableError (DecodeError _ _) = True
-    isRetryableError _ = False
-
-{- | Sends an HTTP request to the Ollama API.
-
-A unified function for making API requests to the Ollama server. Supports both GET and POST methods,
-customizable payloads, and optional configuration. The response is processed by the provided handler.
--}
-withOllamaRequest ::
-  forall payload response.
-  (ToJSON payload) =>
-  -- | API endpoint
-  Text ->
-  -- | HTTP method ("GET" or "POST")
-  BS.ByteString ->
-  -- | Optional request payload (must implement 'ToJSON')
-  Maybe payload ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig')
-  Maybe OllamaConfig ->
-  -- | Response handler to process the HTTP response
-  (Response BodyReader -> IO (Either OllamaError response)) ->
-  IO (Either OllamaError response)
-withOllamaRequest endpoint reqMethod mbPayload mbOllamaConfig handler = do
-  let OllamaConfig {..} = fromMaybe defaultOllamaConfig mbOllamaConfig
-      fullUrl = T.unpack $ hostUrl <> endpoint
-      timeoutMicros = timeout * 1000000
-  manager <- case commonManager of
-    Nothing ->
-      newTlsManagerWith
-        tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro timeoutMicros}
-    Just m -> pure m
-  eRequest <- try $ parseRequest fullUrl
-  case eRequest of
-    Left ex -> return $ Left $ Error.HttpError ex
-    Right req -> do
-      let request =
-            req
-              { method = reqMethod
-              , requestBody =
-                  maybe mempty (RequestBodyLBS . encode) mbPayload
-              }
-          retryCnt = fromMaybe 0 retryCount
-          retryDelay_ = fromMaybe 1 retryDelay
-      withRetry retryCnt retryDelay_ $ do
-        fromMaybe (pure ()) onModelStart
-        eResponse <- try $ withResponse request manager handler
-        case eResponse of
-          Left ex -> do
-            fromMaybe (pure ()) onModelError
-            case ex of
-              (HttpExceptionRequest _ ResponseTimeout) ->
-                return $ Left $ Error.TimeoutError "No response from LLM yet"
-              _ -> return $ Left $ Error.HttpError ex
-          Right result -> do
-            fromMaybe (pure ()) onModelFinish
-            return result
-
-{- | Handles non-streaming API responses.
-
-Processes an HTTP response, accumulating all chunks until EOF and decoding the result as JSON.
-Returns an 'Either' with an 'OllamaError' on failure or the decoded response on success.
-Suitable for APIs that return a single JSON response.
--}
-commonNonStreamingHandler ::
-  FromJSON a =>
-  Response BodyReader ->
-  IO (Either OllamaError a)
-commonNonStreamingHandler resp = do
-  let bodyReader = responseBody resp
-      respStatus = statusCode $ responseStatus resp
-  if respStatus >= 200 && respStatus < 300
-    then do
-      finalBs <- readFullBuff BS.empty bodyReader
-      case eitherDecode (BSL.fromStrict finalBs) of
-        Left err -> pure . Left $ Error.DecodeError err (show finalBs)
-        Right decoded -> pure . Right $ decoded
-    else Left . ApiError . TE.decodeUtf8 <$> brRead bodyReader
-
-{- | Accumulates response chunks into a single ByteString.
-
-Internal helper function to read all chunks from a 'BodyReader' until EOF.
--}
-readFullBuff :: BS.ByteString -> BodyReader -> IO BS.ByteString
-readFullBuff acc reader = do
-  chunk <- brRead reader
-  if BS.null chunk
-    then pure acc
-    else readFullBuff (acc `BS.append` chunk) reader
-
-{- | Handles streaming API responses.
-
-Processes a streaming HTTP response, decoding each chunk as JSON and passing it to the provided
-'sendChunk' function. The 'flush' function is called after each chunk. Stops when the response
-indicates completion (via 'HasDone'). Returns the final decoded response or an error.
--}
-commonStreamHandler ::
-  (HasDone a, FromJSON a) =>
-  -- | Function to handle each decoded chunk
-  (a -> IO (), IO ()) ->
-  Response BodyReader ->
-  IO (Either OllamaError a)
-commonStreamHandler (sendChunk, onComplete) resp = go mempty
-  where
-    go acc = do
-      bs <- brRead $ responseBody resp
-      if BS.null bs
-        then do
-          case eitherDecode (BSL.fromStrict acc) of
-            Left err -> pure $ Left $ Error.DecodeError err (show acc)
-            Right decoded -> pure $ Right decoded
-        else do
-          let chunk = BSL.fromStrict bs
-          case eitherDecode chunk of
-            Left err -> return $ Left $ Error.DecodeError err (show acc)
-            Right res -> do
-              sendChunk res
-              if getDone res then onComplete >> return (Right res) else go (acc <> bs)
-
-{- | Handles non-JSON API responses.
-
-Processes an HTTP response, accumulating all chunks into a 'ByteString'. Returns the accumulated
-data on success (HTTP status 2xx) or an 'ApiError' on failure.
--}
-nonJsonHandler :: Response BodyReader -> IO (Either OllamaError BS.ByteString)
-nonJsonHandler resp = do
-  let bodyReader = responseBody resp
-      respStatus = statusCode $ responseStatus resp
-  if respStatus >= 200 && respStatus < 300
-    then Right <$> readFullBuff BS.empty bodyReader
-    else Left . ApiError . TE.decodeUtf8 <$> brRead bodyReader
-
-{- | Default model options for API requests.
-
-Provides a default 'ModelOptions' configuration with all fields set to 'Nothing',
-suitable as a starting point for customizing model parameters like temperature or token limits.
-
-Example:
-
->>> let opts = defaultModelOptions { temperature = Just 0.7 }
--}
-defaultModelOptions :: ModelOptions
-defaultModelOptions =
-  ModelOptions
-    { numKeep = Nothing
-    , seed = Nothing
-    , numPredict = Nothing
-    , topK = Nothing
-    , topP = Nothing
-    , minP = Nothing
-    , typicalP = Nothing
-    , repeatLastN = Nothing
-    , temperature = Nothing
-    , repeatPenalty = Nothing
-    , presencePenalty = Nothing
-    , frequencyPenalty = Nothing
-    , penalizeNewline = Nothing
-    , stop = Nothing
-    , numa = Nothing
-    , numCtx = Nothing
-    , numBatch = Nothing
-    , numGpu = Nothing
-    , mainGpu = Nothing
-    , useMmap = Nothing
-    , numThread = Nothing
-    }
-
-{- | Retrieves the Ollama server version.
-
-Sends a GET request to the @\/api\/version@ endpoint and returns the server version
-as a 'Version' wrapped in an 'Either' 'OllamaError'.
-
-Example:
-
->>> getVersion
-
-@since 0.2.0.0
--}
-getVersion :: IO (Either OllamaError Version)
-getVersion = do
-  withOllamaRequest
-    "/api/version"
-    "GET"
-    (Nothing :: Maybe Value)
-    Nothing
-    commonNonStreamingHandler
diff --git a/src/Data/Ollama/Conversation.hs b/src/Data/Ollama/Conversation.hs
deleted file mode 100644
--- a/src/Data/Ollama/Conversation.hs
+++ /dev/null
@@ -1,186 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Conversation
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Conversation management for the Ollama client, including storage and retrieval of chat sessions.
-
-This module provides types and functions for managing conversations in the Ollama client. It defines
-a 'Conversation' type to represent a chat session, a 'ConversationStore' typeclass for storage operations,
-and an in-memory implementation using 'InMemoryStore' and 'ConvoM'. The module supports saving, loading,
-listing, and deleting conversations, with thread-safe operations using STM (Software Transactional Memory).
-
-The 'Conversation' type includes metadata such as a unique ID, messages, model name, and timestamps.
-The 'ConversationStore' typeclass defines a generic interface for conversation storage, while 'InMemoryStore'
-provides a concrete in-memory implementation. The 'ConvoM' monad integrates with 'InMemoryStore' for
-monadic operations.
-
-Example:
-
->>> store <- initInMemoryStore
->>> let conv = Conversation "conv1" [userMessage "Hello!"] "gemma3" <$> getCurrentTime <*> getCurrentTime
->>> runInMemoryConvo store $ saveConversation conv
->>> runInMemoryConvo store $ loadConversation "conv1"
-Just (Conversation ...)
--}
-module Data.Ollama.Conversation
-  ( -- * Conversation Types
-    Conversation (..)
-  , ConversationStore (..)
-
-    -- * In-Memory Store
-  , InMemoryStore (..)
-  , ConvoM (..)
-  , initInMemoryStore
-  , runInMemoryConvo
-
-    -- * Validation
-  , validateConversation
-  ) where
-
-import Control.Concurrent.STM
-import Control.Monad.Reader
-import Data.Aeson (FromJSON, ToJSON)
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Ollama.Common.Types
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Time (UTCTime, getCurrentTime)
-import GHC.Generics (Generic)
-
-{- | Represents a chat session with metadata and messages.
-
-Stores a conversation's unique identifier, list of messages, model name, creation time, and last updated time.
--}
-data Conversation = Conversation
-  { conversationId :: !Text
-  -- ^ Unique identifier for the conversation.
-  , messages :: ![Message]
-  -- ^ List of messages in the conversation.
-  , model :: !Text
-  -- ^ Name of the model used in the conversation (e.g., "gemma3").
-  , createdAt :: !UTCTime
-  -- ^ Timestamp when the conversation was created.
-  , lastUpdated :: !UTCTime
-  -- ^ Timestamp when the conversation was last updated.
-  }
-  deriving (Show, Eq, Generic)
-
-instance ToJSON Conversation
-instance FromJSON Conversation
-
-{- | Typeclass defining operations for storing and managing conversations.
-
-Provides methods for saving, loading, listing, and deleting conversations in a monadic context.
-
-@since 0.2.0.0
--}
-class Monad m => ConversationStore m where
-  -- | Saves a conversation to the store.
-  --
-  -- Validates the conversation and updates its 'lastUpdated' timestamp before saving.
-  saveConversation :: Conversation -> m ()
-
-  -- | Loads a conversation by its ID.
-  --
-  -- Returns 'Just' the conversation if found, or 'Nothing' if not.
-  loadConversation :: Text -> m (Maybe Conversation)
-
-  -- | Lists all conversations in the store.
-  listConversations :: m [Conversation]
-
-  -- | Deletes a conversation by its ID.
-  --
-  -- Returns 'True' if the conversation was found and deleted, 'False' otherwise.
-  deleteConversation :: Text -> m Bool
-
-{- | In-memory conversation store using a 'TVar' for thread-safe operations.
-
-Stores conversations in a 'Map' keyed by conversation IDs, wrapped in a 'TVar' for concurrent access.
--}
-newtype InMemoryStore = InMemoryStore (TVar (Map Text Conversation))
-
-{- | Monad for operations with 'InMemoryStore'.
-
-A wrapper around 'ReaderT' that provides access to an 'InMemoryStore' in a monadic context.
--}
-newtype ConvoM a = ConvoM {runConvoM :: ReaderT InMemoryStore IO a}
-  deriving (Functor, Applicative, Monad, MonadIO, MonadReader InMemoryStore)
-
-{- | Runs a 'ConvoM' action with the given 'InMemoryStore'.
-
-Executes a monadic computation in the context of an in-memory store.
-
-Example:
-
->>> store <- initInMemoryStore
->>> runInMemoryConvo store $ saveConversation conv
--}
-runInMemoryConvo :: InMemoryStore -> ConvoM a -> IO a
-runInMemoryConvo store = flip runReaderT store . runConvoM
-
-instance ConversationStore ConvoM where
-  saveConversation conv = do
-    case validateConversation conv of
-      Left err -> liftIO $ putStrLn ("Validation error: " <> T.unpack err)
-      Right validConv -> do
-        now <- liftIO getCurrentTime
-        let updatedConv = validConv {lastUpdated = now}
-        InMemoryStore ref <- ask
-        liftIO . atomically $ modifyTVar' ref (Map.insert (conversationId updatedConv) updatedConv)
-
-  loadConversation cid = do
-    InMemoryStore ref <- ask
-    convs <- liftIO $ readTVarIO ref
-    return $ Map.lookup cid convs
-
-  listConversations = do
-    InMemoryStore ref <- ask
-    convs <- liftIO $ readTVarIO ref
-    return $ Map.elems convs
-
-  deleteConversation cid = do
-    InMemoryStore ref <- ask
-    liftIO . atomically $ do
-      convs <- readTVar ref
-      if Map.member cid convs
-        then do
-          writeTVar ref (Map.delete cid convs)
-          return True
-        else return False
-
-{- | Validates a 'Conversation' to ensure required fields are non-empty.
-
-Checks that the 'conversationId' is not empty and that the 'messages' list contains at least one message.
-Returns 'Right' with the validated conversation or 'Left' with an error message.
-
-Example:
-
->>> let conv = Conversation "" [] "gemma3" time time
->>> validateConversation conv
-Left "Conversation ID cannot be empty"
--}
-validateConversation :: Conversation -> Either Text Conversation
-validateConversation conv
-  | T.null (conversationId conv) = Left "Conversation ID cannot be empty"
-  | null (messages conv) = Left "Conversation must have at least one message"
-  | otherwise = Right conv
-
-{- | Creates a new empty in-memory conversation store.
-
-Initializes an 'InMemoryStore' with an empty 'Map' wrapped in a 'TVar' for thread-safe operations.
-
-Example:
-
->>> store <- initInMemoryStore
->>> runInMemoryConvo store $ listConversations
-[]
--}
-initInMemoryStore :: IO InMemoryStore
-initInMemoryStore = InMemoryStore <$> newTVarIO Map.empty
diff --git a/src/Data/Ollama/Copy.hs b/src/Data/Ollama/Copy.hs
deleted file mode 100644
--- a/src/Data/Ollama/Copy.hs
+++ /dev/null
@@ -1,86 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Copy
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for copying models in the Ollama client.
-
-This module provides functions to copy a model from a source name to a destination name using the Ollama API.
-It includes both an IO-based function ('copyModel') and a monadic version ('copyModelM') for use in
-'MonadIO' contexts. The copy operation is performed via a POST request to the @\/api\/copy@ endpoint.
-
-Example:
-
->>> copyModel "gemma3" "gemma3-copy" Nothing
-Right ()
--}
-module Data.Ollama.Copy
-  ( -- * Copy Model API
-    copyModel
-  , copyModelM
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig (..))
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Utils (nonJsonHandler, withOllamaRequest)
-import Data.Text (Text)
-import GHC.Generics
-
--- | Configuration for copying a model.
-data CopyModelOps = CopyModelOps
-  { source :: !Text
-  -- ^ The name of the source model to copy.
-  , destination :: !Text
-  -- ^ The name of the destination model.
-  }
-  deriving (Show, Eq, Generic, ToJSON)
-
-{- | Copies a model from a source name to a destination name.
-
-Sends a POST request to the @\/api\/copy@ endpoint with the source and destination model names.
-Returns 'Right ()' on success or 'Left' with an 'OllamaError' on failure.
-Example:
-
->>> copyModel "gemma3" "gemma3-copy" Nothing
-Right ()
--}
-copyModel ::
-  -- | Source model name
-  Text ->
-  -- | Destination model name
-  Text ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError ())
-copyModel
-  source_
-  destination_
-  mbConfig = do
-    let reqBody = CopyModelOps {source = source_, destination = destination_}
-    withOllamaRequest
-      "/api/copy"
-      "POST"
-      (Just reqBody)
-      mbConfig
-      (fmap (const () <$>) . nonJsonHandler)
-
-{- | MonadIO version of 'copyModel' for use in monadic contexts.
-
-Lifts the 'copyModel' function into a 'MonadIO' context, allowing it to be used in monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (copyModelM "gemma3" "gemma3-copy" Nothing) someContext
-Right ()
--}
-copyModelM :: MonadIO m => Text -> Text -> Maybe OllamaConfig -> m (Either OllamaError ())
-copyModelM s d mbCfg = liftIO $ copyModel s d mbCfg
diff --git a/src/Data/Ollama/Create.hs b/src/Data/Ollama/Create.hs
deleted file mode 100644
--- a/src/Data/Ollama/Create.hs
+++ /dev/null
@@ -1,214 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Create
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for creating new models in the Ollama client.
-
-This module provides functions to create new models in the Ollama API. Models can be created from:
-
-- Another existing model (using 'from' parameter)
-- A safetensors directory (using 'files' parameter)
-- A GGUF file (using 'files' parameter)
-
-The create operation is performed via a POST request to the @\/api\/create@ endpoint, with streaming
-support for progress updates. The module supports model quantization, custom templates, system prompts,
-parameters, and LORA adapters.
-
-Example creating from existing model:
-
->>> let ops = defaultCreateOps { modelName = "mario", fromModel = Just "llama3.2", systemPrompt = Just "You are Mario from Super Mario Bros." }
->>> createModel ops Nothing
-Creating model...
-Success
-
-Example quantizing a model:
-
->>> let ops = defaultCreateOps { modelName = "llama3.2:quantized", fromModel = Just "llama3.2:3b-instruct-fp16", quantizeType = Just Q4_K_M }
->>> createModel ops Nothing
-Quantizing model...
-Success
--}
-module Data.Ollama.Create
-  ( -- * Create Model API
-    createModel
-  , createModelM
-
-    -- * Configuration Types
-  , CreateOps (..)
-  , defaultCreateOps
-
-    -- * Response Types
-  , CreateResp (..)
-
-    -- * Quantization Types
-  , QuantizationType (..)
-  ) where
-
-import Control.Monad (void)
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Map.Strict (Map)
-import Data.Maybe (catMaybes)
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Types (HasDone (getDone), Message, ModelOptions)
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import GHC.Generics (Generic)
-import GHC.Int (Int64)
-
--- | Quantization types supported by Ollama.
-data QuantizationType
-  = -- | Recommended quantization type
-    Q4_K_M
-  | -- | Alternative quantization type
-    Q4_K_S
-  | -- | Recommended quantization type
-    Q8_0
-  deriving (Show, Eq, Generic)
-
-instance ToJSON QuantizationType where
-  toJSON Q4_K_M = String "q4_K_M"
-  toJSON Q4_K_S = String "q4_K_S"
-  toJSON Q8_0 = String "q8_0"
-
-instance FromJSON QuantizationType where
-  parseJSON = withText "QuantizationType" $ \t ->
-    case t of
-      "q4_K_M" -> pure Q4_K_M
-      "q4_K_S" -> pure Q4_K_S
-      "q8_0" -> pure Q8_0
-      _ -> fail $ "Invalid QuantizationType: " <> show t
-
--- | Configuration for creating a new model.
-data CreateOps = CreateOps
-  { modelName :: !Text
-  -- ^ The name of the model to create.
-  , fromModel :: !(Maybe Text)
-  -- ^ Optional name of an existing model to create the new model from.
-  , files :: !(Maybe (Map Text Text))
-  -- ^ Optional dictionary of file names to SHA256 digests of blobs to create the model from.
-  , adapters :: !(Maybe (Map Text Text))
-  -- ^ Optional dictionary of file names to SHA256 digests of blobs for LORA adapters.
-  , template :: !(Maybe Text)
-  -- ^ Optional prompt template for the model.
-  , license :: !(Maybe [Text])
-  -- ^ Optional list of strings containing the license(s) for the model.
-  , systemPrompt :: !(Maybe Text)
-  -- ^ Optional system prompt for the model.
-  , parameters :: !(Maybe ModelOptions)
-  -- ^ Optional parameters for the model.
-  , messages :: !(Maybe [Message])
-  -- ^ Optional list of message objects used to create a conversation.
-  , stream :: !(Maybe Bool)
-  -- ^ Optional flag to enable streaming progress updates.
-  , quantizeType :: !(Maybe QuantizationType)
-  -- ^ Optional quantization type for quantizing a non-quantized model.
-  }
-  deriving (Show, Eq, Generic)
-
--- | Default configuration for creating a model.
-defaultCreateOps :: Text -> CreateOps
-defaultCreateOps name =
-  CreateOps
-    { modelName = name
-    , fromModel = Nothing
-    , files = Nothing
-    , adapters = Nothing
-    , template = Nothing
-    , license = Nothing
-    , systemPrompt = Nothing
-    , parameters = Nothing
-    , messages = Nothing
-    , stream = Nothing
-    , quantizeType = Nothing
-    }
-
--- | Response type for model creation operations.
-data CreateResp = CreateResp
-  { status :: !Text
-  -- ^ The status of the create operation (e.g., "success", "reading model metadata").
-  , digest :: !(Maybe Text)
-  -- ^ Optional digest (hash) of the model layer being processed.
-  , total :: !(Maybe Int64)
-  -- ^ Optional total size in bytes for quantization operations.
-  , completed :: !(Maybe Int64)
-  -- ^ Optional number of bytes completed for quantization operations.
-  }
-  deriving (Show, Eq, Generic)
-
-instance HasDone CreateResp where
-  getDone CreateResp {..} = status == "success"
-
-instance ToJSON CreateOps where
-  toJSON CreateOps {..} =
-    object $
-      catMaybes
-        [ Just ("model" .= modelName)
-        , ("from" .=) <$> fromModel
-        , ("files" .=) <$> files
-        , ("adapters" .=) <$> adapters
-        , ("template" .=) <$> template
-        , ("license" .=) <$> license
-        , ("system" .=) <$> systemPrompt
-        , ("parameters" .=) <$> parameters
-        , ("messages" .=) <$> messages
-        , ("stream" .=) <$> stream
-        , ("quantize" .=) <$> quantizeType
-        ]
-
-instance FromJSON CreateResp where
-  parseJSON = withObject "CreateResp" $ \v ->
-    CreateResp
-      <$> v .: "status"
-      <*> v .:? "digest"
-      <*> v .:? "total"
-      <*> v .:? "completed"
-
-{- | Creates a new model according to the provided configuration.
-
-Sends a POST request to the @\/api\/create@ endpoint to create a model. The model can be created from:
-
-- Another existing model (specify 'fromModel')
-- A safetensors directory (specify 'files' with file name to SHA256 digest mappings)
-- A GGUF file (specify 'files' with file name to SHA256 digest mapping)
-
-Supports quantization, custom templates, system prompts, parameters, and LORA adapters.
-Prints progress messages to the console during creation.
--}
-createModel ::
-  -- | Model creation configuration
-  CreateOps ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO ()
-createModel createOps mbConfig =
-  void $
-    withOllamaRequest
-      "/api/create"
-      "POST"
-      (Just createOps)
-      mbConfig
-      (commonStreamHandler (onToken, pure ()))
-  where
-    onToken :: CreateResp -> IO ()
-    onToken CreateResp {..} = case (total, completed) of
-      (Just t, Just c) -> putStrLn $ "Progress: " <> show c <> "/" <> show t <> " bytes"
-      _ -> putStrLn $ "Status: " <> show status
-
-{- | MonadIO version of 'createModel' for use in monadic contexts.
-
-Lifts the 'createModel' function into a 'MonadIO' context, allowing it to be used in monadic computations.
--}
-createModelM ::
-  MonadIO m =>
-  CreateOps ->
-  Maybe OllamaConfig ->
-  m ()
-createModelM createOps mbCfg = liftIO $ createModel createOps mbCfg
diff --git a/src/Data/Ollama/Delete.hs b/src/Data/Ollama/Delete.hs
deleted file mode 100644
--- a/src/Data/Ollama/Delete.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Delete
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for deleting models in the Ollama client.
-
-This module provides functions to delete a model from the Ollama server using its name. It includes
-both an IO-based function ('deleteModel') and a monadic version ('deleteModelM') for use in
-'MonadIO' contexts. The delete operation is performed via a DELETE request to the @\/api\/delete@ endpoint.
-
-Example:
-
->>> deleteModel "gemma3" Nothing
-Right ()
--}
-module Data.Ollama.Delete
-  ( -- * Delete Model API
-    deleteModel
-  , deleteModelM
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig (..))
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Utils (nonJsonHandler, withOllamaRequest)
-import Data.Text (Text)
-
--- | Request payload for deleting a model.
-newtype DeleteModelReq
-  = -- | The name of the model to delete.
-    DeleteModelReq {name :: Text}
-  deriving newtype (Show, Eq)
-
-instance ToJSON DeleteModelReq where
-  toJSON (DeleteModelReq name_) = object ["name" .= name_]
-
-{- | Deletes a model from the Ollama server.
-
-Sends a DELETE request to the "/api/delete" endpoint with the specified model name.
-Returns 'Right ()' on success or 'Left' with an 'OllamaError' on failure.
--}
-deleteModel ::
-  -- | Model name to delete
-  Text ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError ())
-deleteModel modelName mbConfig = do
-  let reqBody = DeleteModelReq {name = modelName}
-  withOllamaRequest
-    "/api/delete"
-    "DELETE"
-    (Just reqBody)
-    mbConfig
-    (fmap (const () <$>) . nonJsonHandler)
-
-{- | MonadIO version of 'deleteModel' for use in monadic contexts.
-
-Lifts the 'deleteModel' function into a  context,
-allowing it to be used in monadic computations.
--}
-deleteModelM :: MonadIO m => Text -> Maybe OllamaConfig -> m (Either OllamaError ())
-deleteModelM t mbCfg = liftIO $ deleteModel t mbCfg
diff --git a/src/Data/Ollama/Embeddings.hs b/src/Data/Ollama/Embeddings.hs
deleted file mode 100644
--- a/src/Data/Ollama/Embeddings.hs
+++ /dev/null
@@ -1,189 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Embeddings
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for generating text embeddings using the Ollama API.
-
-This module provides functions to generate text embeddings from an Ollama model. It includes both
-high-level ('embedding', 'embeddingM') and low-level ('embeddingOps', 'embeddingOpsM') APIs for
-generating embeddings, with support for customizing model options, truncation, and keep-alive settings.
-The embeddings are returned as a list of float vectors, suitable for tasks like semantic search or
-text similarity analysis.
-
-The 'EmbeddingOps' type configures the embedding request, and 'EmbeddingResp' represents the response
-containing the model name and the generated embeddings. The 'defaultEmbeddingOps' provides a default
-configuration for convenience.
-
-Example:
-
->>> embedding "llama3.2" ["Hello, world!"]
-Right (EmbeddingResp "llama3.2" [[0.1, 0.2, ...]])
--}
-module Data.Ollama.Embeddings
-  ( -- * Embedding API
-    embedding
-  , embeddingOps
-  , embeddingM
-  , embeddingOpsM
-
-    -- * Configuration and Response Types
-  , defaultEmbeddingOps
-  , EmbeddingOps (..)
-  , EmbeddingResp (..)
-
-    -- * Model Options
-  , ModelOptions (..)
-  , defaultModelOptions
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Types (ModelOptions (..))
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-
-{- | Default configuration for embedding requests.
-
-Provides a default 'EmbeddingOps' with the "llama3.2" model, an empty input list, and no additional options.
-Can be customized by modifying fields as needed.
--}
-defaultEmbeddingOps :: EmbeddingOps
-defaultEmbeddingOps =
-  EmbeddingOps
-    { modelName = "nomic-embed-text"
-    , input = []
-    , truncateInput = Nothing
-    , keepAlive = Nothing
-    , modelOptions = Nothing
-    , dimensions = Nothing
-    }
-
--- | Configuration for an embedding request.
-data EmbeddingOps = EmbeddingOps
-  { modelName :: !Text
-  -- ^ The name of the model to use for generating embeddings (e.g., "llama3.2").
-  , input :: ![Text]
-  -- ^ List of input texts to generate embeddings for.
-  , truncateInput :: !(Maybe Bool)
-  -- ^ Optional flag to truncate input if it exceeds model limits.
-  , keepAlive :: !(Maybe Int)
-  -- ^ Optional override for the keep-alive timeout in minutes.
-  , modelOptions :: !(Maybe ModelOptions)
-  -- ^ Optional model parameters (e.g., temperature) as specified in the Modelfile.
-  --
-  -- @since 0.2.0.0
-  , dimensions :: !(Maybe Int)
-  -- ^ number of dimensions for the embedding
-  --
-  -- @since 0.2.1.0
-  }
-  deriving (Show, Eq)
-
--- | Response type for an embedding request.
-data EmbeddingResp = EmbeddingResp
-  { respondedModel :: !Text
-  -- ^ The name of the model that generated the embeddings.
-  , respondedEmbeddings :: ![[Float]]
-  -- ^ List of embedding vectors, one for each input text.
-  }
-  deriving (Show, Eq)
-
-instance FromJSON EmbeddingResp where
-  parseJSON = withObject "EmbeddingResp" $ \v ->
-    EmbeddingResp
-      <$> v .: "model"
-      <*> v .: "embeddings"
-
-instance ToJSON EmbeddingOps where
-  toJSON (EmbeddingOps model_ input_ truncate' keepAlive_ ops dimensions_) =
-    object
-      [ "model" .= model_
-      , "input" .= input_
-      , "truncate" .= truncate'
-      , "keep_alive" .= keepAlive_
-      , "options" .= ops
-      , "dimensions" .= dimensions_
-      ]
-
-{- | Generates embeddings for a list of input texts with full configuration.
-
-Sends a POST request to the @\/api\/embed@ endpoint to generate embeddings for the provided inputs.
-Allows customization of truncation, keep-alive settings, model options, and Ollama configuration.
-Returns 'Right' with an 'EmbeddingResp' on success or 'Left' with an 'OllamaError' on failure.
--}
-embeddingOps ::
-  -- | Model name
-  Text ->
-  -- | List of input texts
-  [Text] ->
-  -- | Optional truncation flag
-  Maybe Bool ->
-  -- | Optional keep-alive timeout in minutes
-  Maybe Int ->
-  -- | Optional model options
-  Maybe ModelOptions ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe Int ->
-  Maybe OllamaConfig ->
-  IO (Either OllamaError EmbeddingResp)
-embeddingOps modelName_ input_ mTruncate mKeepAlive mbOptions mbDimensions mbConfig = do
-  withOllamaRequest
-    "/api/embed"
-    "POST"
-    ( Just $
-        EmbeddingOps
-          { modelName = modelName_
-          , input = input_
-          , truncateInput = mTruncate
-          , keepAlive = mKeepAlive
-          , modelOptions = mbOptions
-          , dimensions = mbDimensions
-          }
-    )
-    mbConfig
-    commonNonStreamingHandler
-
-{- | Simplified API for generating embeddings.
-
-A higher-level function that generates embeddings using default settings for truncation, keep-alive,
-model options, and Ollama configuration. Suitable for basic use cases.
--}
-embedding ::
-  -- | Model name
-  Text ->
-  -- | List of input texts
-  [Text] ->
-  IO (Either OllamaError EmbeddingResp)
-embedding modelName_ input_ =
-  embeddingOps modelName_ input_ Nothing Nothing Nothing Nothing Nothing
-
-{- | MonadIO version of 'embedding' for use in monadic contexts.
-
-Lifts the 'embedding' function into a 'MonadIO' context, allowing it to be used in monadic computations.
--}
-embeddingM :: MonadIO m => Text -> [Text] -> m (Either OllamaError EmbeddingResp)
-embeddingM m ip = liftIO $ embedding m ip
-
-{- | MonadIO version of 'embeddingOps' for use in monadic contexts.
-
-Lifts the 'embeddingOps' function into a 'MonadIO' context, allowing it to be used in monadic computations
-with full configuration options.
--}
-embeddingOpsM ::
-  MonadIO m =>
-  Text ->
-  [Text] ->
-  Maybe Bool ->
-  Maybe Int ->
-  Maybe ModelOptions ->
-  Maybe Int ->
-  Maybe OllamaConfig ->
-  m (Either OllamaError EmbeddingResp)
-embeddingOpsM m ip mbTruncate mbKeepAlive mbOptions mbDimensions mbCfg =
-  liftIO $ embeddingOps m ip mbTruncate mbKeepAlive mbOptions mbDimensions mbCfg
diff --git a/src/Data/Ollama/Generate.hs b/src/Data/Ollama/Generate.hs
deleted file mode 100644
--- a/src/Data/Ollama/Generate.hs
+++ /dev/null
@@ -1,248 +0,0 @@
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Generate
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Text generation functionality for the Ollama client.
-
-This module provides functions and types for generating text using an Ollama model. It includes APIs
-for sending generation requests, both in IO ('generate') and monadic ('generateM') contexts, with
-support for streaming and non-streaming responses. The 'GenerateOps' type configures the generation
-request, allowing customization of the model, prompt, images, format, and other parameters. The
-'defaultGenerateOps' provides a convenient starting point for configuration.
-
-The module supports advanced features like Base64-encoded images, custom templates, and model-specific
-options (e.g., temperature). It also includes validation to ensure required fields are non-empty.
-
-Example:
-
->>> let ops = defaultGenerateOps { modelName = "gemma3", prompt = "Write a poem." }
->>> generate ops Nothing
-Right (GenerateResponse ...)
--}
-module Data.Ollama.Generate
-  ( -- * Generate Texts
-    generate
-  , generateM
-
-    -- * Configuration
-  , defaultGenerateOps
-  , GenerateOps (..)
-  , validateGenerateOps
-
-    -- * Response and Configuration Types
-  , GenerateResponse (..)
-  , Format (..)
-  , OllamaConfig (..)
-  , defaultOllamaConfig
-  , ModelOptions (..)
-  , defaultModelOptions
-
-    -- * Error Types
-  , OllamaError (..)
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Maybe
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Data.Ollama.Common.Error (OllamaError (..))
-import Data.Ollama.Common.Types (Format (..), GenerateResponse (..), ModelOptions (..))
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import Data.Text qualified as T
-
-{- | Validates 'GenerateOps' to ensure required fields are non-empty.
-
-Checks that the 'modelName' and 'prompt' fields are not empty. Returns 'Right' with the validated
-'GenerateOps' or 'Left' with an 'OllamaError' if validation fails.
-
-Example:
-
->>> validateGenerateOps defaultGenerateOps
-Left (InvalidRequest "Prompt cannot be empty")
-
-@since 0.2.0.0
--}
-validateGenerateOps :: GenerateOps -> Either OllamaError GenerateOps
-validateGenerateOps ops
-  | T.null (modelName ops) = Left $ InvalidRequest "Model name cannot be empty"
-  | T.null (prompt ops) = Left $ InvalidRequest "Prompt cannot be empty"
-  | otherwise = Right ops
-
--- | Configuration for a text generation request.
-data GenerateOps = GenerateOps
-  { modelName :: !Text
-  -- ^ The name of the model to use for generation (e.g., "gemma3").
-  , prompt :: !Text
-  -- ^ The prompt text to provide to the model for generating a response.
-  , suffix :: Maybe Text
-  -- ^ Optional suffix to append to the generated text (not supported by all models).
-  , images :: !(Maybe [Text])
-  -- ^ Optional list of Base64-encoded images to include with the request.
-  , format :: !(Maybe Format)
-  -- ^ Optional format specifier for the response (e.g., JSON).
-  --
-  -- @since 0.1.3.0
-  , system :: !(Maybe Text)
-  -- ^ Optional system text to include in the generation context.
-  , template :: !(Maybe Text)
-  -- ^ Optional template to format the response.
-  , stream :: !(Maybe (GenerateResponse -> IO (), IO ()))
-  -- ^ Optional callback function to be called with each incoming response.
-  , raw :: !(Maybe Bool)
-  -- ^ Optional flag to return the raw response.
-  , keepAlive :: !(Maybe Int)
-  -- ^ Optional override for how long (in minutes) the model stays loaded in memory (default: 5 minutes).
-  , options :: !(Maybe ModelOptions)
-  -- ^ Optional model parameters (e.g., temperature) as specified in the Modelfile.
-  --
-  -- @since 0.1.3.0
-  , think :: !(Maybe Bool)
-  -- ^ Optional flag to enable thinking mode.
-  --
-  -- @since 0.2.0.0
-  }
-
-instance Show GenerateOps where
-  show GenerateOps {..} =
-    "GenerateOps { "
-      <> "model : "
-      <> T.unpack modelName
-      <> ", prompt : "
-      <> T.unpack prompt
-      <> ", suffix : "
-      <> show suffix
-      <> ", images : "
-      <> show images
-      <> ", format : "
-      <> show format
-      <> ", system : "
-      <> show system
-      <> ", template : "
-      <> show template
-      <> ", stream : "
-      <> "Stream functions"
-      <> ", raw : "
-      <> show raw
-      <> ", keepAlive : "
-      <> show keepAlive
-      <> ", options : "
-      <> show options
-      <> ", think: "
-      <> show think
-
-instance Eq GenerateOps where
-  (==) a b =
-    modelName a == modelName b
-      && prompt a == prompt b
-      && suffix a == suffix b
-      && images a == images b
-      && format a == format b
-      && system a == system b
-      && template a == template b
-      && raw a == raw b
-      && keepAlive a == keepAlive b
-      && options a == options b
-      && think a == think b
-
-instance ToJSON GenerateOps where
-  toJSON
-    ( GenerateOps
-        model
-        prompt
-        suffix
-        images
-        format
-        system
-        template
-        stream
-        raw
-        keepAlive
-        options
-        think
-      ) =
-      object
-        [ "model" .= model
-        , "prompt" .= prompt
-        , "suffix" .= suffix
-        , "images" .= images
-        , "format" .= format
-        , "system" .= system
-        , "template" .= template
-        , "stream" .= if isNothing stream then Just False else Just True
-        , "raw" .= raw
-        , "keep_alive" .= keepAlive
-        , "options" .= options
-        , "think" .= think
-        ]
-
-{- | Default configuration for text generation.
-
-Provides a default 'GenerateOps' with the "gemma3" model and an empty prompt. Other fields are set
-to 'Nothing' or default values. Can be customized by modifying fields as needed.
-
-Example:
-
->>> let ops = defaultGenerateOps { modelName = "customModel", prompt = "Hello!" }
->>> generate ops Nothing
--}
-defaultGenerateOps :: GenerateOps
-defaultGenerateOps =
-  GenerateOps
-    { modelName = "gemma3"
-    , prompt = ""
-    , suffix = Nothing
-    , images = Nothing
-    , format = Nothing
-    , system = Nothing
-    , template = Nothing
-    , stream = Nothing
-    , raw = Nothing
-    , keepAlive = Nothing
-    , options = Nothing
-    , think = Nothing
-    }
-
-{- | Generates text using the specified model and configuration.
-
-Validates the 'GenerateOps' configuration and sends a POST request to the @\/api\/generate@ endpoint.
-Supports both streaming and non-streaming responses based on the 'stream' field in 'GenerateOps'.
-Returns 'Right' with a 'GenerateResponse' on success or 'Left' with an 'OllamaError' on failure.
-
-Example:
-
->>> let ops = defaultGenerateOps { modelName = "gemma3", prompt = "Write a short poem." }
->>> generate ops Nothing
-Right (GenerateResponse ...)
--}
-generate :: GenerateOps -> Maybe OllamaConfig -> IO (Either OllamaError GenerateResponse)
-generate ops mbConfig =
-  case validateGenerateOps ops of
-    Left err -> pure $ Left err
-    Right _ -> withOllamaRequest "/api/generate" "POST" (Just ops) mbConfig handler
-  where
-    handler = maybe commonNonStreamingHandler commonStreamHandler (stream ops)
-
-{- | MonadIO version of 'generate' for use in monadic contexts.
-
-Lifts the 'generate' function into a 'MonadIO' context, allowing it to be used in monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> let ops = defaultGenerateOps { modelName = "gemma3", prompt = "Hello!" }
->>> runReaderT (generateM ops Nothing) someContext
-Right (GenerateResponse ...)
--}
-generateM ::
-  MonadIO m =>
-  GenerateOps ->
-  Maybe OllamaConfig ->
-  m (Either OllamaError GenerateResponse)
-generateM ops mbCfg = liftIO $ generate ops mbCfg
diff --git a/src/Data/Ollama/List.hs b/src/Data/Ollama/List.hs
deleted file mode 100644
--- a/src/Data/Ollama/List.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.List
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for listing available models in the Ollama client.
-
-This module provides functions to retrieve a list of models available on the Ollama server.
-It includes both an IO-based function ('list') and a monadic version ('listM') for use in
-'MonadIO' contexts. The list operation is performed via a GET request to the @\/api\/tags@ endpoint,
-returning a 'Models' type containing a list of 'ModelInfo' records with details about each model.
-
-Example:
-
->>> list Nothing
-Right (Models [ModelInfo ...])
--}
-module Data.Ollama.List
-  ( -- * List Models API
-    list
-  , listM
-
-    -- * Model Types
-  , Models (..)
-  , ModelInfo (..)
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Types as CT
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import Data.Time
-import GHC.Int (Int64)
-
--- | A wrapper type containing a list of available models.
-newtype Models
-  = -- | List of 'ModelInfo' records describing available models.
-    Models [ModelInfo]
-  deriving (Eq, Show)
-
--- | Details about a specific model.
-data ModelInfo = ModelInfo
-  { name :: !Text
-  -- ^ The name of the model.
-  , modifiedAt :: !UTCTime
-  -- ^ The timestamp when the model was last modified.
-  , size :: !Int64
-  -- ^ The size of the model in bytes.
-  , digest :: !Text
-  -- ^ The digest (hash) of the model.
-  , details :: !ModelDetails
-  -- ^ Additional details about the model (e.g., format, family, parameters).
-  }
-  deriving (Eq, Show)
-
--- | JSON parsing instance for 'Models'.
-instance FromJSON Models where
-  parseJSON = withObject "Models" $ \v -> Models <$> v .: "models"
-
--- | JSON parsing instance for 'ModelInfo'.
-instance FromJSON ModelInfo where
-  parseJSON = withObject "ModelInfo" $ \v ->
-    ModelInfo
-      <$> v .: "name"
-      <*> v .: "modified_at"
-      <*> v .: "size"
-      <*> v .: "digest"
-      <*> v .: "details"
-
-{- | Retrieves a list of available models from the Ollama server.
-
-Sends a GET request to the @\/api\/tags@ endpoint to fetch the list of models.
-Returns 'Right' with a 'Models' containing the list of 'ModelInfo' on success,
-or 'Left' with an 'OllamaError' on failure.
--}
-list ::
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError Models)
-list mbConfig = do
-  withOllamaRequest
-    "/api/tags"
-    "GET"
-    (Nothing :: Maybe Value)
-    mbConfig
-    commonNonStreamingHandler
-
-{- | MonadIO version of 'list' for use in monadic contexts.
-
-Lifts the 'list' function into a 'MonadIO' context, allowing it to be used in monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (listM Nothing) someContext
-Right (Models [ModelInfo ...])
--}
-listM :: MonadIO m => Maybe OllamaConfig -> m (Either OllamaError Models)
-listM mbCfg = liftIO $ list mbCfg
diff --git a/src/Data/Ollama/Load.hs b/src/Data/Ollama/Load.hs
deleted file mode 100644
--- a/src/Data/Ollama/Load.hs
+++ /dev/null
@@ -1,103 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Load
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : High-level functions for loading and unloading models in the Ollama client.
-
-This module provides functions to load and unload generative models in the Ollama server.
-It includes both IO-based functions ('loadGenModel', 'unloadGenModel') and monadic versions
-('loadGenModelM', 'unloadGenModelM') for use in 'MonadIO' contexts. The operations are
-performed via POST requests to the @\/api\/generate@ endpoint, leveraging the 'GenerateOps'
-configuration from the 'Data.Ollama.Generate' module.
-
-Loading a model keeps it in memory for faster subsequent requests, while unloading frees
-up memory by setting the keep-alive duration to zero.
-
-Example:
-
->>> loadGenModel "gemma3"
-Right ()
->>> unloadGenModel "gemma3"
-Right ()
--}
-module Data.Ollama.Load
-  ( -- * Load and Unload Model APIs
-    loadGenModel
-  , unloadGenModel
-  , loadGenModelM
-  , unloadGenModelM
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Ollama.Common.Error
-import Data.Ollama.Common.Utils (commonNonStreamingHandler, withOllamaRequest)
-import Data.Ollama.Generate qualified as Gen
-import Data.Text (Text)
-
-{- | Loads a generative model into memory.
-
-Sends a POST request to the @\/api\/generate@ endpoint to load the specified model into
-memory, ensuring faster response times for subsequent requests. Returns 'Right ()' on
-success or 'Left' with an 'OllamaError' on failure.
-
-@since 0.2.0.0
--}
-loadGenModel ::
-  -- |  Model name (e.g., "gemma3")
-  Text ->
-  IO (Either OllamaError ())
-loadGenModel m = do
-  let ops = Gen.defaultGenerateOps {Gen.modelName = m}
-  withOllamaRequest "/api/generate" "POST" (Just ops) Nothing commonNonStreamingHandler
-
-{- | Unloads a generative model from memory.
-
-Sends a POST request to the @\/api\/generate@ endpoint with a keep-alive duration of zero
-to unload the specified model from memory, freeing up resources. Returns 'Right ()' on
-success or 'Left' with an 'OllamaError' on failure.
-
-@since 0.2.0.0
--}
-unloadGenModel ::
-  -- | Model name (e.g., "gemma3")
-  Text ->
-  IO (Either OllamaError ())
-unloadGenModel m = do
-  let ops = Gen.defaultGenerateOps {Gen.modelName = m, Gen.keepAlive = Just 0}
-  withOllamaRequest "/api/generate" "POST" (Just ops) Nothing commonNonStreamingHandler
-
-{- | MonadIO version of 'loadGenModel' for use in monadic contexts.
-
-Lifts the 'loadGenModel' function into a 'MonadIO' context, allowing it to be used in
-monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (loadGenModelM "gemma3") someContext
-Right ()
-
-@since 0.2.0.0
--}
-loadGenModelM :: MonadIO m => Text -> m (Either OllamaError ())
-loadGenModelM t = liftIO $ loadGenModel t
-
-{- | MonadIO version of 'unloadGenModel' for use in monadic contexts.
-
-Lifts the 'unloadGenModel' function into a 'MonadIO' context, allowing it to be used in
-monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (unloadGenModelM "gemma3") someContext
-Right ()
-
-@since 0.2.0.0
--}
-unloadGenModelM :: MonadIO m => Text -> m (Either OllamaError ())
-unloadGenModelM t = liftIO $ unloadGenModel t
diff --git a/src/Data/Ollama/Ps.hs b/src/Data/Ollama/Ps.hs
deleted file mode 100644
--- a/src/Data/Ollama/Ps.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Ps
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for listing running models in the Ollama client.
-
-This module provides functions to retrieve a list of models currently running on the Ollama server.
-It includes both an IO-based function ('ps') and a monadic version ('psM') for use in 'MonadIO'
-contexts. The operation is performed via a GET request to the @\/api\/ps@ endpoint, returning a
-'RunningModels' type containing a list of 'RunningModel' records with details about each running model.
-
-Example:
-
->>> ps Nothing
-Right (RunningModels [RunningModel ...])
--}
-module Data.Ollama.Ps
-  ( -- * List Running Models API
-    ps
-  , psM
-
-    -- * Model Types
-  , RunningModels (..)
-  , RunningModel (..)
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Types as CT
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import Data.Time
-import GHC.Int (Int64)
-
--- | A wrapper type containing a list of running models.
-newtype RunningModels
-  = -- | List of 'RunningModel' records describing currently running models.
-    RunningModels [RunningModel]
-  deriving (Eq, Show)
-
--- | Details about a specific running model.
-data RunningModel = RunningModel
-  { name_ :: !Text
-  -- ^ The name of the running model instance.
-  , modelName :: !Text
-  -- ^ The base model name (e.g., "gemma3").
-  , size_ :: !Int64
-  -- ^ The size of the model in bytes.
-  , modelDigest :: !Text
-  -- ^ The digest (hash) of the model.
-  , modelDetails :: !ModelDetails
-  -- ^ Additional details about the model (e.g., format, family, parameters).
-  , expiresAt :: !UTCTime
-  -- ^ The timestamp when the model's memory allocation expires.
-  , sizeVRam :: !Int64
-  -- ^ The size of the model's VRAM usage in bytes.
-  }
-  deriving (Eq, Show)
-
--- | JSON parsing instance for 'RunningModels'.
-instance FromJSON RunningModels where
-  parseJSON = withObject "Models" $ \v -> RunningModels <$> v .: "models"
-
--- | JSON parsing instance for 'RunningModel'.
-instance FromJSON RunningModel where
-  parseJSON = withObject "RunningModel" $ \v ->
-    RunningModel
-      <$> v .: "name"
-      <*> v .: "model"
-      <*> v .: "size"
-      <*> v .: "digest"
-      <*> v .: "details"
-      <*> v .: "expires_at"
-      <*> v .: "size_vram"
-
-{- | Retrieves a list of currently running models from the Ollama server.
-
-Sends a GET request to the @\/api\/ps@ endpoint to fetch the list of running models.
-Returns 'Right' with a 'RunningModels' containing the list of 'RunningModel' on success,
-or 'Left' with an 'OllamaError' on failure.
-Example:
-
->>> ps Nothing
-Right (RunningModels [RunningModel {name_ = "gemma3:instance1", modelName = "gemma3", ...}])
--}
-ps ::
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError RunningModels)
-ps mbConfig = do
-  withOllamaRequest
-    "/api/ps"
-    "GET"
-    (Nothing :: Maybe Value)
-    mbConfig
-    commonNonStreamingHandler
-
-{- | MonadIO version of 'ps' for use in monadic contexts.
-
-Lifts the 'ps' function into a 'MonadIO' context, allowing it to be used in monadic computations.
--}
-psM :: MonadIO m => Maybe OllamaConfig -> m (Either OllamaError RunningModels)
-psM mbCfg = liftIO $ ps mbCfg
diff --git a/src/Data/Ollama/Pull.hs b/src/Data/Ollama/Pull.hs
deleted file mode 100644
--- a/src/Data/Ollama/Pull.hs
+++ /dev/null
@@ -1,153 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Pull
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for pulling models in the Ollama client.
-
-This module provides functions to pull (download) models from the Ollama server. It includes both
-high-level ('pull', 'pullM') and low-level ('pullOps', 'pullOpsM') APIs for pulling models, with
-support for streaming progress updates and insecure connections. The 'PullOps' type configures the
-pull request, and 'PullResp' represents the response containing the status and progress details.
-
-The pull operation is performed via a POST request to the @\/api\/pull@ endpoint. Streaming mode,
-when enabled, provides real-time progress updates by printing the remaining bytes to the console.
-
-Example:
-
->>> pull "gemma3"
-Remaining bytes: 123456789
-...
-Completed
-Right (PullResp {status = "success", ...})
--}
-module Data.Ollama.Pull
-  ( -- * Pull Model API
-    pull
-  , pullOps
-  , pullM
-  , pullOpsM
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Maybe (fromMaybe)
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Types (HasDone (..))
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import GHC.Generics
-import GHC.Int (Int64)
-
--- | Configuration options for pulling a model.
-data PullOps = PullOps
-  { name :: !Text
-  -- ^ The name of the model to pull (e.g., "gemma3").
-  , insecure :: !(Maybe Bool)
-  -- ^ Optional flag to allow insecure connections. If 'Just True', insecure connections are permitted.
-  , stream :: !(Maybe Bool)
-  -- ^ Optional flag to enable streaming of the download. If 'Just True', progress updates are streamed.
-  }
-  deriving (Show, Eq, Generic, ToJSON)
-
--- | Response data from a pull operation.
-data PullResp = PullResp
-  { status :: !Text
-  -- ^ The status of the pull operation (e.g., "success" or "failure").
-  , digest :: !(Maybe Text)
-  -- ^ The digest (hash) of the model, if available.
-  , total :: !(Maybe Int64)
-  -- ^ The total size of the model in bytes, if available.
-  , completed :: !(Maybe Int64)
-  -- ^ The number of bytes downloaded, if available.
-  }
-  deriving (Show, Eq, Generic, FromJSON)
-
-instance HasDone PullResp where
-  getDone PullResp {..} = status /= "success"
-
-{- | Pulls a model with full configuration.
-
-Sends a POST request to the @\/api\/pull@ endpoint to download the specified model. Supports
-streaming progress updates (if 'stream' is 'Just True') and insecure connections (if 'insecure'
-is 'Just True'). Prints remaining bytes during streaming and "Completed" when finished.
-Returns 'Right' with a 'PullResp' on success or 'Left' with an 'OllamaError' on failure.
--}
-pullOps ::
-  -- | Model name
-  Text ->
-  -- | Optional insecure connection flag
-  Maybe Bool ->
-  -- | Optional streaming flag
-  Maybe Bool ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError PullResp)
-pullOps modelName mInsecure mStream mbConfig = do
-  withOllamaRequest
-    "/api/pull"
-    "POST"
-    (Just $ PullOps {name = modelName, insecure = mInsecure, stream = mStream})
-    mbConfig
-    (commonStreamHandler (onToken, pure ()))
-  where
-    onToken :: PullResp -> IO ()
-    onToken res = do
-      let completed' = fromMaybe 0 (completed res)
-      let total' = fromMaybe 0 (total res)
-      putStrLn $ "Remaining bytes: " <> show (total' - completed')
-
-{- | Simplified API for pulling a model.
-
-A higher-level function that pulls a model using default settings for insecure connections,
-streaming, and Ollama configuration. Suitable for basic use cases.
--}
-pull ::
-  -- | Model name
-  Text ->
-  IO (Either OllamaError PullResp)
-pull modelName = pullOps modelName Nothing Nothing Nothing
-
-{- | MonadIO version of 'pull' for use in monadic contexts.
-
-Lifts the 'pull' function into a 'MonadIO' context, allowing it to be used in monadic computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (pullM "gemma3") someContext
-Right (PullResp {status = "success", ...})
--}
-pullM :: MonadIO m => Text -> m (Either OllamaError PullResp)
-pullM t = liftIO $ pull t
-
-{- | MonadIO version of 'pullOps' for use in monadic contexts.
-
-Lifts the 'pullOps' function into a 'MonadIO' context, allowing it to be used in monadic computations
-with full configuration options.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (pullOpsM "gemma3" Nothing (Just True) Nothing) someContext
-Remaining bytes: 123456789
-...
-Completed
-Right (PullResp {status = "success", ...})
--}
-pullOpsM ::
-  MonadIO m =>
-  Text ->
-  Maybe Bool ->
-  Maybe Bool ->
-  Maybe OllamaConfig ->
-  m (Either OllamaError PullResp)
-pullOpsM t mbInsecure mbStream mbCfg = liftIO $ pullOps t mbInsecure mbStream mbCfg
diff --git a/src/Data/Ollama/Push.hs b/src/Data/Ollama/Push.hs
deleted file mode 100644
--- a/src/Data/Ollama/Push.hs
+++ /dev/null
@@ -1,115 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards #-}
-
-{- |
-Module      : Data.Ollama.Push
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for pushing models to the Ollama server.
-
-This module provides functions to push (upload) a model to the Ollama server. It includes
-both an IO-based function ('push') and a monadic version ('pushM') for use in 'MonadIO'
-contexts. The push operation is performed via a POST request to the @\/api\/pull@ endpoint,
-with support for streaming progress updates and insecure connections.
-
-The 'PushOps' type configures the push request, and 'PushResp' represents the response
-containing the status and progress details. Streaming mode, when enabled, provides
-real-time progress updates by printing to the console.
-
-Example:
-
->>> push "gemma3" Nothing (Just True) Nothing
-Pushing...
-Completed
--}
-module Data.Ollama.Push
-  ( -- * Push Model API
-    push
-  , pushM
-  ) where
-
-import Control.Monad (void)
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Types (HasDone (getDone))
-import Data.Ollama.Common.Utils as CU
-import Data.Text (Text)
-import GHC.Generics
-import GHC.Int (Int64)
-
--- | Configuration options for pushing a model.
-data PushOps = PushOps
-  { name :: !Text
-  -- ^ The name of the model to push (e.g., "gemma3").
-  , insecure :: !(Maybe Bool)
-  -- ^ Optional flag to allow insecure connections.
-  -- If 'Just True', insecure connections are permitted.
-  , stream :: !(Maybe Bool)
-  -- ^ Optional flag to enable streaming of the upload.
-  -- If 'Just True', progress updates are streamed.
-  }
-  deriving (Show, Eq, Generic, ToJSON)
-
--- | Response data from a push operation.
-data PushResp = PushResp
-  { status :: !Text
-  -- ^ The status of the push operation (e.g., "success" or "failure").
-  , digest :: !(Maybe Text)
-  -- ^ The digest (hash) of the model, if available.
-  , total :: !(Maybe Int64)
-  -- ^ The total size of the model in bytes, if available.
-  }
-  deriving (Show, Eq, Generic, FromJSON)
-
-instance HasDone PushResp where
-  getDone PushResp {..} = status /= "success"
-
-{- | Pushes a model to the Ollama server with specified options.
-
-Sends a POST request to the @\/api\/pull@ endpoint to upload the specified model. Supports
-streaming progress updates (if 'stream' is 'Just True') and insecure connections (if
-'insecure' is 'Just True'). Prints "Pushing..." during streaming and "Completed" when
-finished. Returns '()' on completion.
--}
-push ::
-  -- | Model name
-  Text ->
-  -- | Optional insecure connection flag
-  Maybe Bool ->
-  -- | Optional streaming flag
-  Maybe Bool ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO ()
-push modelName mInsecure mStream mbConfig = do
-  void $
-    withOllamaRequest
-      "/api/push"
-      "POST"
-      (Just $ PushOps {name = modelName, insecure = mInsecure, stream = mStream})
-      mbConfig
-      (commonStreamHandler (onToken, pure ()))
-  where
-    onToken :: PushResp -> IO ()
-    onToken _ = putStrLn "Pushing... "
-
-{- | MonadIO version of 'push' for use in monadic contexts.
-
-Lifts the 'push' function into a 'MonadIO' context, allowing it to be used in monadic
-computations.
-
-Example:
-
->>> import Control.Monad.IO.Class
->>> runReaderT (pushM "gemma3" Nothing (Just True) Nothing) someContext
-Pushing...
-Completed
--}
-pushM :: MonadIO m => Text -> Maybe Bool -> Maybe Bool -> Maybe OllamaConfig -> m ()
-pushM t insec s mbCfg = liftIO $ push t insec s mbCfg
diff --git a/src/Data/Ollama/Show.hs b/src/Data/Ollama/Show.hs
deleted file mode 100644
--- a/src/Data/Ollama/Show.hs
+++ /dev/null
@@ -1,230 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DuplicateRecordFields #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-{- |
-Module      : Data.Ollama.Show
-Copyright   : (c) 2025 Tushar Adhatrao
-License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
-Description : Functionality for retrieving detailed information about models in the Ollama client.
-
-This module provides functions to fetch detailed information about a specific model on the Ollama server.
-It includes both high-level ('showModel', 'showModelM') and low-level ('showModelOps', 'showModelOpsM') APIs
-for retrieving model details, with support for verbose output. The operation is performed via a POST request
-to the @\/api\/show@ endpoint, returning a 'ShowModelResponse' containing comprehensive model metadata.
-
-The 'ShowModelOps' type configures the request, and 'ShowModelResponse' and 'ShowModelInfo' represent the
-response structure. The module also re-exports 'CT.ModelDetails' for completeness.
-
-Note: Verbose mode parsing is currently not fully supported.
-
-Example:
-
->>> showModel "gemma3"
-Right (ShowModelResponse {modelFile = "...", ...})
-
-@since 1.0.0.0
--}
-module Data.Ollama.Show
-  ( -- * Show Model Info API
-    showModel
-  , showModelM
-  , showModelOps
-  , showModelOpsM
-
-    -- * Response Types
-  , ShowModelResponse (..)
-  , ShowModelInfo (..)
-  , CT.ModelDetails (..)
-  ) where
-
-import Control.Monad.IO.Class (MonadIO (liftIO))
-import Data.Aeson
-import Data.Ollama.Common.Config (OllamaConfig)
-import Data.Ollama.Common.Error (OllamaError)
-import Data.Ollama.Common.Types qualified as CT
-import Data.Ollama.Common.Utils (commonNonStreamingHandler, withOllamaRequest)
-import Data.Text (Text)
-import GHC.Generics
-import GHC.Int (Int64)
-
--- | Configuration options for requesting model information.
-data ShowModelOps = ShowModelOps
-  { name :: !Text
-  -- ^ The name of the model to query (e.g., "gemma3").
-  , verbose :: !(Maybe Bool)
-  -- ^ Optional flag to request verbose output. Note: Verbose mode parsing is currently incomplete.
-  }
-  deriving (Show, Eq, Generic, ToJSON)
-
--- | Response structure for model information.
-data ShowModelResponse = ShowModelResponse
-  { modelFile :: !Text
-  -- ^ The content of the model's Modelfile.
-  , parameters :: !(Maybe Text)
-  -- ^ Optional model parameters (e.g., temperature settings).
-  , template :: !(Maybe Text)
-  -- ^ Optional template used for the model.
-  , details :: !CT.ModelDetails
-  -- ^ General details about the model (e.g., format, family).
-  , modelInfo :: !ShowModelInfo
-  -- ^ Detailed technical information about the model.
-  , license :: !(Maybe Text)
-  -- ^ Optional license information for the model.
-  --
-  -- @since 0.2.0.0
-  , capabilities :: Maybe [Text]
-  -- ^ Optional list of model capabilities.
-  --
-  -- @since 0.2.0.0
-  }
-  deriving (Show, Eq)
-
--- | Detailed technical information about a model.
-data ShowModelInfo = ShowModelInfo
-  { generalArchitecture :: !(Maybe Text)
-  -- ^ The architecture of the model (e.g., "llama").
-  , generalFileType :: !(Maybe Int)
-  -- ^ The file type identifier for the model.
-  , generalParameterCount :: !(Maybe Int64)
-  -- ^ The number of parameters in the model.
-  , generalQuantizationVersion :: !(Maybe Int)
-  -- ^ The quantization version used by the model.
-  , llamaAttentionHeadCount :: !(Maybe Int)
-  -- ^ Number of attention heads in the LLaMA model.
-  , llamaAttentionHeadCountKV :: !(Maybe Int)
-  -- ^ Number of key-value attention heads in the LLaMA model.
-  , llamaAttentionLayerNormRMSEpsilon :: !(Maybe Float)
-  -- ^ RMS epsilon for layer normalization in the LLaMA model.
-  , llamaBlockCount :: !(Maybe Int)
-  -- ^ Number of blocks in the LLaMA model.
-  , llamaContextLength :: !(Maybe Int)
-  -- ^ Context length supported by the LLaMA model.
-  , llamaEmbeddingLength :: !(Maybe Int)
-  -- ^ Embedding length used by the LLaMA model.
-  , llamaFeedForwardLength :: !(Maybe Int)
-  -- ^ Feed-forward layer length in the LLaMA model.
-  , llamaRopeDimensionCount :: !(Maybe Int)
-  -- ^ RoPE dimension count in the LLaMA model.
-  , llamaRopeFreqBase :: !(Maybe Int64)
-  -- ^ Base frequency for RoPE in the LLaMA model.
-  , llamaVocabSize :: !(Maybe Int64)
-  -- ^ Vocabulary size of the LLaMA model.
-  , tokenizerGgmlBosToken_id :: !(Maybe Int)
-  -- ^ BOS (beginning of sequence) token ID for the GGML tokenizer.
-  , tokenizerGgmlEosToken_id :: !(Maybe Int)
-  -- ^ EOS (end of sequence) token ID for the GGML tokenizer.
-  , tokenizerGgmlMerges :: !(Maybe [Text])
-  -- ^ List of merges for the GGML tokenizer.
-  , tokenizerGgmlMode :: !(Maybe Text)
-  -- ^ Mode of the GGML tokenizer.
-  , tokenizerGgmlPre :: !(Maybe Text)
-  -- ^ Pre-tokenization configuration for the GGML tokenizer.
-  , tokenizerGgmlTokenType :: !(Maybe [Text])
-  -- ^ Token type information for the GGML tokenizer.
-  , tokenizerGgmlTokens :: !(Maybe [Text])
-  -- ^ List of tokens for the GGML tokenizer.
-  }
-  deriving (Show, Eq)
-
--- | JSON parsing instance for 'ShowModelResponse'.
-instance FromJSON ShowModelResponse where
-  parseJSON = withObject "ShowModelResponse" $ \v ->
-    ShowModelResponse
-      <$> v .: "modelfile"
-      <*> v .:? "parameters"
-      <*> v .:? "template"
-      <*> v .: "details"
-      <*> v .: "model_info"
-      <*> v .:? "license"
-      <*> v .:? "capabilities"
-
--- | JSON parsing instance for 'ShowModelInfo'.
-instance FromJSON ShowModelInfo where
-  parseJSON = withObject "ModelInfo" $ \v ->
-    ShowModelInfo
-      <$> v .:? "general.architecture"
-      <*> v .:? "general.file_type"
-      <*> v .:? "general.parameter_count"
-      <*> v .:? "general.quantization_version"
-      <*> v .:? "llama.attention.head_count"
-      <*> v .:? "llama.attention.head_count_kv"
-      <*> v .:? "llama.attention.layer_norm_rms_epsilon"
-      <*> v .:? "llama.block_count"
-      <*> v .:? "llama.context_length"
-      <*> v .:? "llama.embedding_length"
-      <*> v .:? "llama.feed_forward_length"
-      <*> v .:? "llama.rope.dimension_count"
-      <*> v .:? "llama.rope.freq_base"
-      <*> v .:? "llama.vocab_size"
-      <*> v .:? "tokenizer.ggml.bos_token_id"
-      <*> v .:? "tokenizer.ggml.eos_token_id"
-      <*> v .:? "tokenizer.ggml.merges"
-      <*> v .:? "tokenizer.ggml.model"
-      <*> v .:? "tokenizer.ggml.pre"
-      <*> v .:? "tokenizer.ggml.token_type"
-      <*> v .:? "tokenizer.ggml.tokens"
-
-{- | Retrieves model information with configuration options.
-
-Sends a POST request to the @\/api\/show@ endpoint to fetch detailed information about
-the specified model. Supports verbose output if 'verbose' is 'Just True' (though verbose
-mode parsing is currently incomplete). Returns 'Right' with a 'ShowModelResponse' on
-success or 'Left' with an 'OllamaError' on failure.
--}
-showModelOps ::
-  -- | Model name
-  Text ->
-  -- | Optional verbose flag
-  Maybe Bool ->
-  -- | Optional 'OllamaConfig' (defaults to 'defaultOllamaConfig' if 'Nothing')
-  Maybe OllamaConfig ->
-  IO (Either OllamaError ShowModelResponse)
-showModelOps modelName verbose_ mbConfig = do
-  withOllamaRequest
-    "/api/show"
-    "POST"
-    ( Just $
-        ShowModelOps
-          { name = modelName
-          , verbose = verbose_
-          }
-    )
-    mbConfig
-    commonNonStreamingHandler
-
-{- | Simplified API for retrieving model information.
-
-A higher-level function that fetches model information using default settings for
-verbose output and Ollama configuration. Suitable for basic use cases.
--}
-showModel ::
-  -- | Model name
-  Text ->
-  IO (Either OllamaError ShowModelResponse)
-showModel modelName =
-  showModelOps modelName Nothing Nothing
-
-{- | MonadIO version of 'showModel' for use in monadic contexts.
-
-Lifts the 'showModel' function into a 'MonadIO' context, allowing it to be used in
-monadic computations.
--}
-showModelM :: MonadIO m => Text -> m (Either OllamaError ShowModelResponse)
-showModelM t = liftIO $ showModel t
-
-{- | MonadIO version of 'showModelOps' for use in monadic contexts.
-
-Lifts the 'showModelOps' function into a 'MonadIO' context, allowing it to be used in
-monadic computations with full configuration options.
--}
-showModelOpsM ::
-  MonadIO m =>
-  Text ->
-  Maybe Bool ->
-  Maybe OllamaConfig ->
-  m (Either OllamaError ShowModelResponse)
-showModelOpsM t v mbCfg = liftIO $ showModelOps t v mbCfg
diff --git a/src/Ollama.hs b/src/Ollama.hs
--- a/src/Ollama.hs
+++ b/src/Ollama.hs
@@ -1,199 +1,189 @@
-{-# LANGUAGE DuplicateRecordFields #-}
-
 {- |
-Module      : Data.Ollama
-Copyright   : (c) 2025 Tushar Adhatrao
+Module      : Ollama
+Copyright   : (c) 2024-2026 Tushar Adhatrao
 License     : MIT
-Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
-Stability   : experimental
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
 Portability : portable
 
-== Ollama Haskell
-
-This module provides a high-level Haskell interface to the [Ollama](https://ollama.com) API
-    for interacting with local LLMs. It includes support for:
-
-- Text generation (sync/streaming)
-- Conversational chat (with tools and images)
-- Embeddings
-- Model management (pull, push, delete, list, show)
-- Structured outputs
-- Custom configuration and model options
-
-Inspired by @ollama-python@, this library is built to offer idiomatic Haskell bindings
-over Ollama’s HTTP API.
-
-== 🔧 Usage
+Top-level umbrella re-export module for the Ollama Haskell client library.
 
-Import this module as a top-level interface:
+== Quick Example
 
 @
 import Ollama
-@
 
-All functions return @Either OllamaError a@ or can be used in a Monad stack using
-their @\*M@ variants.
-
-== 🔑 Main APIs
-
-=== ✍️ Generate Text
-
-- 'generate', 'generateM' – Generate text from a model
-- 'defaultGenerateOps' – Default generation parameters
-- 'GenerateOps', 'GenerateResponse' – Request and response types
-
-=== 💬 Chat with LLMs
-
-- 'chat', 'chatM' – Send chat messages to a model
-- 'ChatOps', 'ChatResponse', 'Role', 'Message' – Chat input/output types
-- Supports tools via 'InputTool', 'FunctionDef', 'OutputFunction', etc.
-
-=== 🧠 Embeddings
-
-- 'embedding', 'embeddingM' – Generate vector embeddings
-- 'EmbeddingOps', 'EmbeddingResp' – Request/response types
-
-=== 📦 Model Management
-
-- 'copyModel', 'createModel', 'deleteModel'
-- 'list' – List all installed models
-- 'ps', 'psM' – Show running models
-- 'showModel', 'showModelM' – Show model info
-- 'pull', 'push' – Pull/push models (with progress support)
-
-=== ⚙️ Configuration
-
-- 'defaultOllamaConfig' – Modify host, retries, streaming, etc.
-- 'withOnModelStart', 'withOnModelFinish', 'withOnModelError' – Hook support
+main :: IO ()
+main = do
+  client <- defaultClient
+  let req = chatRequest "llama3.2" (userMessage "Why is the sky blue?" :| [])
+  result <- chat client req
+  case result of
+    Left err -> print err
+    Right resp -> case crMessage resp of
+      Just msg -> putStrLn (messageContent msg)
+      Nothing  -> putStrLn "No message returned"
+@
 
-=== 🧰 Utilities
+@since 1.0.0.0
+-}
+module Ollama (
+  -- * Client
+  OllamaClient,
+  newClient,
+  defaultClient,
+  clientFromEnv,
+  closeClient,
+  withClient,
 
-- 'defaultModelOptions', 'encodeImage', 'withOllamaRequest'
-- 'loadGenModel', 'unloadGenModel' – Load/unload generation models
-- 'getVersion' – Ollama server version
+  -- * Config & Retry
+  OllamaClientConfig (..),
+  defaultConfig,
+  RetryPolicy (..),
+  LogLevel (..),
 
-== 🧾 Types
+  -- * API Endpoints
 
-All request/response payloads and enums are exposed, including:
+  -- ** Chat
+  chat,
+  chatStream,
+  ChatRequest (..),
+  ChatResponse (..),
+  chatRequest,
+  chatEvalTokensPerSecond,
+  chatPromptEvalTokensPerSecond,
 
-- 'ModelOptions', 'OllamaConfig', 'OllamaError', 'Format'
-- 'Models', 'ModelInfo', 'ModelDetails', 'ShowModelResponse'
-- 'RunningModels', 'RunningModel', 'Version'
--}
-module Ollama
-  ( -- * Main APIs
+  -- ** Generate
+  generate,
+  generateStream,
+  GenerateRequest (..),
+  GenerateResponse (..),
+  generateRequest,
+  evalTokensPerSecond,
+  promptEvalTokensPerSecond,
 
-    -- ** Generate Texts
-    generate
-  , generateM
-  , defaultGenerateOps
-  , GenerateOps (..)
-  , GenerateResponse (..)
+  -- ** Embeddings
+  embed,
+  EmbedRequest (..),
+  EmbedResponse (..),
+  embedRequest,
+  embeddings,
+  EmbeddingsRequest (..),
+  EmbeddingsResponse (..),
 
-    -- ** Chat with LLMs
-  , chat
-  , chatM
-  , Role (..)
-  , defaultChatOps
-  , ChatResponse (..)
-  , ChatOps (..)
-  , InputTool (..)
-  , FunctionDef (..)
-  , FunctionParameters (..)
-  , ToolCall (..)
-  , OutputFunction (..)
+  -- ** Model Management
+  listModels,
+  showModel,
+  copyModel,
+  deleteModel,
+  ListResponse (..),
+  ModelInfo (..),
+  RunningModel (..),
+  ShowResponse (..),
 
-    -- ** Embeddings
-  , embedding
-  , embeddingOps
-  , embeddingM
-  , embeddingOpsM
-  , EmbeddingOps (..)
-  , EmbeddingResp (..)
+  -- ** Create
+  createModel,
+  createModelStream,
+  defaultCreateRequest,
+  CreateRequest (..),
+  CreateResponse (..),
+  QuantizationType (..),
 
-    -- ** Copy Models
-  , copyModel
-  , copyModelM
+  -- ** Pull & Push
+  pull,
+  pullStream,
+  push,
+  pushStream,
+  PullResponse (..),
+  PushResponse (..),
 
-    -- ** Create Models
-  , createModel
-  , createModelM
+  -- ** Blobs
+  checkBlob,
+  pushBlob,
 
-    -- ** Delete Models
-  , deleteModel
-  , deleteModelM
+  -- ** System
+  getVersion,
+  listRunning,
+  RunningModelsResponse (..),
 
-    -- ** List Models
-  , list
+  -- * Types & Primitives
+  ModelName (..),
+  mkModelName,
+  Digest (..),
+  Base64Image (..),
+  Duration (..),
+  durationToSeconds,
+  durationToMillis,
+  tokensPerSecond,
+  Version (..),
+  Think (..),
+  ThinkingLevel (..),
 
-    -- ** List currently running models
-  , ps
-  , psM
+  -- ** Messages
+  Role (..),
+  Message (..),
+  userMessage,
+  systemMessage,
+  assistantMessage,
+  toolMessage,
+  toolResultMessage,
+  imageMessage,
 
-    -- ** Push and Pull
-  , push
-  , pushM
-  , pull
-  , pullM
-  , pullOps
-  , pullOpsM
+  -- ** Tools & Functions
+  Tool (..),
+  FunctionDef (..),
+  FunctionParameters (..),
+  ToolCall (..),
+  ToolCallFunction (..),
 
-    -- ** Show Model Info
-  , showModel
-  , showModelOps
-  , showModelM
-  , showModelOpsM
+  -- ** Options & Format
+  ModelOptions (..),
+  defaultOptions,
+  Format (..),
 
-    -- ** Blob Operations
-  , checkBlobExists
-  , createBlob
+  -- * Error Handling
+  OllamaError (..),
+  isRetryable,
+  throwOllama,
 
-    -- * Ollama config
-  , defaultOllamaConfig
-  , withOnModelStart
-  , withOnModelFinish
-  , withOnModelError
+  -- * Streaming
+  HasDone (..),
+  collectStream,
+  foldStream,
 
-    -- * Utils
-  , defaultModelOptions
-  , ModelOptions (..)
-  , encodeImage
-  , withOllamaRequest
-  , getVersion
-  , loadGenModel
-  , unloadGenModel
-  , loadGenModelM
-  , unloadGenModelM
+  -- * Testing Infrastructure
+  newMockClient,
+  withMockClient,
+  mockGenerateResponse,
+  mockChatResponse,
+  mockEmbedResponse,
+  mockListModelsResponse,
 
-    -- * Types
-  , ShowModelResponse (..)
-  , Models (..)
-  , ModelInfo (..)
-  , ModelDetails (..)
-  , ShowModelInfo (..)
-  , RunningModels (..)
-  , RunningModel (..)
-  , Message (..)
-  , Format (..)
-  , OllamaError (..)
-  , OllamaConfig (..)
-  , Version (..)
-  )
-where
+  -- * Conversation Store
+  Conversation (..),
+  ConversationStore (..),
+  InMemoryStore (..),
+  initInMemoryStore,
+  saveConversationInMemory,
+  loadConversationInMemory,
+  listConversationsInMemory,
+  deleteConversationInMemory,
+) where
 
-import Data.Ollama.Blob
-import Data.Ollama.Chat
-import Data.Ollama.Common.Config
-import Data.Ollama.Common.Types
-import Data.Ollama.Common.Utils
-import Data.Ollama.Copy
-import Data.Ollama.Create
-import Data.Ollama.Delete
-import Data.Ollama.Embeddings hiding (keepAlive, modelName)
-import Data.Ollama.Generate
-import Data.Ollama.List
-import Data.Ollama.Load
-import Data.Ollama.Ps hiding (modelName)
-import Data.Ollama.Pull
-import Data.Ollama.Push
-import Data.Ollama.Show
+import Ollama.API.Blobs
+import Ollama.API.Chat
+import Ollama.API.Embed
+import Ollama.API.Generate
+import Ollama.API.Models
+import Ollama.API.Models.Create
+import Ollama.API.Models.Pull
+import Ollama.API.Models.Push
+import Ollama.API.Ps
+import Ollama.API.Version
+import Ollama.Client
+import Ollama.Client.Config
+import Ollama.Conversation
+import Ollama.Error
+import Ollama.Streaming
+import Ollama.Testing
+import Ollama.Types
diff --git a/src/Ollama/API/Blobs.hs b/src/Ollama/API/Blobs.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Blobs.hs
@@ -0,0 +1,45 @@
+{- |
+Module      : Ollama.API.Blobs
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Blob management endpoints (@/api/blobs/:digest@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Blobs (
+  checkBlob,
+  pushBlob,
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Data.ByteString (ByteString)
+import Data.Functor (void)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (requestRaw)
+import Ollama.Error (OllamaError (..))
+import Ollama.Types.Common (Digest (..))
+
+{- | Check if a blob exists on the server (@HEAD /api/blobs/:digest@).
+
+@since 1.0.0.0
+-}
+checkBlob :: (MonadIO m) => OllamaClient -> Digest -> m (Either OllamaError Bool)
+checkBlob client (Digest d) = do
+  res <- requestRaw client "HEAD" ("/api/blobs/" <> d) Nothing
+  pure $ case res of
+    Right _ -> Right True
+    Left (ApiError 404 _) -> Right False
+    Left err -> Left err
+
+{- | Push / upload a file blob to the server (@POST /api/blobs/:digest@).
+
+@since 1.0.0.0
+-}
+pushBlob :: (MonadIO m) => OllamaClient -> Digest -> ByteString -> m (Either OllamaError ())
+pushBlob client (Digest d) payload = do
+  res <- requestRaw client "POST" ("/api/blobs/" <> d) (Just payload)
+  pure $ void res
diff --git a/src/Ollama/API/Chat.hs b/src/Ollama/API/Chat.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Chat.hs
@@ -0,0 +1,170 @@
+{- |
+Module      : Ollama.API.Chat
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Chat completion API endpoint (@/api/chat@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Chat (
+  ChatRequest (..),
+  ChatResponse (..),
+  chatRequest,
+  chat,
+  chatStream,
+  chatEvalTokensPerSecond,
+  chatPromptEvalTokensPerSecond,
+) where
+
+import Conduit (ConduitT)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.Aeson
+import Data.List.NonEmpty (NonEmpty)
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request, requestStreaming)
+import Ollama.Error (OllamaError)
+import Ollama.Streaming (HasDone (..))
+import Ollama.Types.Common (Duration, ModelName, Think, tokensPerSecond)
+import Ollama.Types.Format (Format)
+import Ollama.Types.Message (Message)
+import Ollama.Types.Options (ModelOptions)
+import Ollama.Types.Tool (Tool)
+
+{- | Chat completion request payload.
+
+@since 1.0.0.0
+-}
+data ChatRequest = ChatRequest
+  { chatModel :: !ModelName
+  , chatMessages :: !(NonEmpty Message)
+  , chatTools :: !(Maybe [Tool])
+  , chatFormat :: !(Maybe Format)
+  , chatOptions :: !(Maybe ModelOptions)
+  , chStream :: !(Maybe Bool)
+  , chatKeepAlive :: !(Maybe Text)
+  , chatThink :: !(Maybe Think)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON ChatRequest where
+  toJSON ChatRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= chatModel
+        , Just $ "messages" .= chatMessages
+        , ("tools" .=) <$> chatTools
+        , ("format" .=) <$> chatFormat
+        , ("options" .=) <$> chatOptions
+        , ("stream" .=) <$> chStream
+        , ("keep_alive" .=) <$> chatKeepAlive
+        , ("think" .=) <$> chatThink
+        ]
+
+{- | Create a default 'ChatRequest' for a model and message history.
+
+@since 1.0.0.0
+-}
+chatRequest :: ModelName -> NonEmpty Message -> ChatRequest
+chatRequest model msgs =
+  ChatRequest
+    { chatModel = model
+    , chatMessages = msgs
+    , chatTools = Nothing
+    , chatFormat = Nothing
+    , chatOptions = Nothing
+    , chStream = Just False
+    , chatKeepAlive = Nothing
+    , chatThink = Nothing
+    }
+
+{- | Chat completion response payload.
+
+@since 1.0.0.0
+-}
+data ChatResponse = ChatResponse
+  { crModel :: !ModelName
+  , crCreatedAt :: !UTCTime
+  , crMessage :: !(Maybe Message)
+  , crDone :: !Bool
+  , crDoneReason :: !(Maybe Text)
+  , crTotalDuration :: !(Maybe Duration)
+  , crLoadDuration :: !(Maybe Duration)
+  , crPromptEvalCount :: !(Maybe Int)
+  , crPromptEvalDuration :: !(Maybe Duration)
+  , crEvalCount :: !(Maybe Int)
+  , crEvalDuration :: !(Maybe Duration)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON ChatResponse where
+  parseJSON = withObject "ChatResponse" $ \v ->
+    ChatResponse
+      <$> v .: "model"
+      <*> v .: "created_at"
+      <*> v .:? "message"
+      <*> v .: "done"
+      <*> v .:? "done_reason"
+      <*> v .:? "total_duration"
+      <*> v .:? "load_duration"
+      <*> v .:? "prompt_eval_count"
+      <*> v .:? "prompt_eval_duration"
+      <*> v .:? "eval_count"
+      <*> v .:? "eval_duration"
+
+instance ToJSON ChatResponse where
+  toJSON ChatResponse {..} =
+    object
+      [ "model" .= crModel
+      , "created_at" .= crCreatedAt
+      , "message" .= crMessage
+      , "done" .= crDone
+      , "done_reason" .= crDoneReason
+      , "total_duration" .= crTotalDuration
+      , "load_duration" .= crLoadDuration
+      , "prompt_eval_count" .= crPromptEvalCount
+      , "prompt_eval_duration" .= crPromptEvalDuration
+      , "eval_count" .= crEvalCount
+      , "eval_duration" .= crEvalDuration
+      ]
+
+instance HasDone ChatResponse where
+  isDone = crDone
+
+{- | Non-streaming chat completion API.
+
+@since 1.0.0.0
+-}
+chat :: (MonadIO m) => OllamaClient -> ChatRequest -> m (Either OllamaError ChatResponse)
+chat client req = request client "POST" "/api/chat" (Just req {chStream = Just False})
+
+{- | Streaming chat completion API yielding 'ChatResponse' chunks.
+
+@since 1.0.0.0
+-}
+chatStream :: (MonadUnliftIO m) => OllamaClient -> ChatRequest -> ConduitT () ChatResponse m ()
+chatStream client req = requestStreaming client "/api/chat" (req {chStream = Just True})
+
+{- | Calculate generation throughput (eval tokens \/ second) from a 'ChatResponse'.
+
+@since 1.0.0.0
+-}
+chatEvalTokensPerSecond :: ChatResponse -> Maybe Double
+chatEvalTokensPerSecond ChatResponse {..} =
+  tokensPerSecond <$> crEvalCount <*> crEvalDuration
+
+{- | Calculate prompt evaluation throughput (prompt tokens \/ second) from a 'ChatResponse'.
+
+@since 1.0.0.0
+-}
+chatPromptEvalTokensPerSecond :: ChatResponse -> Maybe Double
+chatPromptEvalTokensPerSecond ChatResponse {..} =
+  tokensPerSecond <$> crPromptEvalCount <*> crPromptEvalDuration
diff --git a/src/Ollama/API/Embed.hs b/src/Ollama/API/Embed.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Embed.hs
@@ -0,0 +1,175 @@
+{- |
+Module      : Ollama.API.Embed
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Vector embeddings API endpoint (@/api/embed@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Embed (
+  EmbedRequest (..),
+  EmbedResponse (..),
+  embedRequest,
+  embed,
+
+  -- * Deprecated Legacy API
+  EmbeddingsRequest (..),
+  EmbeddingsResponse (..),
+  embeddings,
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Data.Aeson
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request)
+import Ollama.Error (OllamaError)
+import Ollama.Types.Common (Duration, ModelName)
+import Ollama.Types.Options (ModelOptions)
+
+{- | Embedding request payload for single text or batch list of texts.
+
+@since 1.0.0.0
+-}
+data EmbedRequest = EmbedRequest
+  { embModel :: !ModelName
+  , embInput :: !(Either Text [Text])
+  , embTruncate :: !(Maybe Bool)
+  , embOptions :: !(Maybe ModelOptions)
+  , embKeepAlive :: !(Maybe Text)
+  , embDimensions :: !(Maybe Int)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON EmbedRequest where
+  toJSON EmbedRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= embModel
+        , Just $ case embInput of
+            Left single -> "input" .= single
+            Right multiple -> "input" .= multiple
+        , ("truncate" .=) <$> embTruncate
+        , ("options" .=) <$> embOptions
+        , ("keep_alive" .=) <$> embKeepAlive
+        , ("dimensions" .=) <$> embDimensions
+        ]
+
+{- | Create an 'EmbedRequest' for a list of input texts.
+
+@since 1.0.0.0
+-}
+embedRequest :: ModelName -> [Text] -> EmbedRequest
+embedRequest model inputs =
+  EmbedRequest
+    { embModel = model
+    , embInput = Right inputs
+    , embTruncate = Nothing
+    , embOptions = Nothing
+    , embKeepAlive = Nothing
+    , embDimensions = Nothing
+    }
+
+{- | Embedding response payload containing vector embeddings.
+
+@since 1.0.0.0
+-}
+data EmbedResponse = EmbedResponse
+  { erModel :: !ModelName
+  , erEmbeddings :: ![[Double]]
+  , erTotalDuration :: !(Maybe Duration)
+  , erLoadDuration :: !(Maybe Duration)
+  , erPromptEvalCount :: !(Maybe Int)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON EmbedResponse where
+  parseJSON = withObject "EmbedResponse" $ \v ->
+    EmbedResponse
+      <$> v .: "model"
+      <*> v .: "embeddings"
+      <*> v .:? "total_duration"
+      <*> v .:? "load_duration"
+      <*> v .:? "prompt_eval_count"
+
+instance ToJSON EmbedResponse where
+  toJSON EmbedResponse {..} =
+    object
+      [ "model" .= erModel
+      , "embeddings" .= erEmbeddings
+      , "total_duration" .= erTotalDuration
+      , "load_duration" .= erLoadDuration
+      , "prompt_eval_count" .= erPromptEvalCount
+      ]
+
+{- | Generate vector embeddings.
+
+@since 1.0.0.0
+-}
+embed :: (MonadIO m) => OllamaClient -> EmbedRequest -> m (Either OllamaError EmbedResponse)
+embed client req = request client "POST" "/api/embed" (Just req)
+
+{- | Legacy request payload for deprecated @/api/embeddings@ endpoint.
+
+@since 1.0.0.0
+-}
+data EmbeddingsRequest = EmbeddingsRequest
+  { ebrModel :: !ModelName
+  , ebrPrompt :: !Text
+  , ebrOptions :: !(Maybe ModelOptions)
+  , ebrKeepAlive :: !(Maybe Text)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON EmbeddingsRequest where
+  toJSON EmbeddingsRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= ebrModel
+        , Just $ "prompt" .= ebrPrompt
+        , ("options" .=) <$> ebrOptions
+        , ("keep_alive" .=) <$> ebrKeepAlive
+        ]
+
+instance FromJSON EmbeddingsRequest where
+  parseJSON = withObject "EmbeddingsRequest" $ \v ->
+    EmbeddingsRequest
+      <$> v .: "model"
+      <*> v .: "prompt"
+      <*> v .:? "options"
+      <*> v .:? "keep_alive"
+
+{- | Legacy response payload for deprecated @/api/embeddings@ endpoint.
+
+@since 1.0.0.0
+-}
+newtype EmbeddingsResponse = EmbeddingsResponse
+  { ebrEmbedding :: [Double]
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON EmbeddingsResponse where
+  parseJSON = withObject "EmbeddingsResponse" $ \v ->
+    EmbeddingsResponse <$> v .: "embedding"
+
+instance ToJSON EmbeddingsResponse where
+  toJSON EmbeddingsResponse {..} =
+    object ["embedding" .= ebrEmbedding]
+
+{- | Generate vector embeddings using the deprecated @/api/embeddings@ endpoint.
+
+@since 1.0.0.0
+-}
+embeddings ::
+  (MonadIO m) =>
+  OllamaClient ->
+  EmbeddingsRequest ->
+  m (Either OllamaError EmbeddingsResponse)
+embeddings client req = request client "POST" "/api/embeddings" (Just req)
+{-# DEPRECATED embeddings "Use embed instead" #-}
diff --git a/src/Ollama/API/Generate.hs b/src/Ollama/API/Generate.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Generate.hs
@@ -0,0 +1,199 @@
+{- |
+Module      : Ollama.API.Generate
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Text completion API endpoint (@/api/generate@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Generate (
+  GenerateRequest (..),
+  GenerateResponse (..),
+  generateRequest,
+  generate,
+  generateStream,
+  evalTokensPerSecond,
+  promptEvalTokensPerSecond,
+) where
+
+import Conduit (ConduitT)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.Aeson
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request, requestStreaming)
+import Ollama.Error (OllamaError)
+import Ollama.Streaming (HasDone (..))
+import Ollama.Types.Common (Base64Image, Duration, ModelName, Think, tokensPerSecond)
+import Ollama.Types.Format (Format)
+import Ollama.Types.Options (ModelOptions)
+
+{- | Request payload for text generation completion.
+
+@since 1.0.0.0
+-}
+data GenerateRequest = GenerateRequest
+  { genModel :: !ModelName
+  , genPrompt :: !Text
+  , genSuffix :: !(Maybe Text)
+  , genImages :: !(Maybe [Base64Image])
+  , genFormat :: !(Maybe Format)
+  , genOptions :: !(Maybe ModelOptions)
+  , genSystem :: !(Maybe Text)
+  , genTemplate :: !(Maybe Text)
+  , genStream :: !(Maybe Bool)
+  , genRaw :: !(Maybe Bool)
+  , genKeepAlive :: !(Maybe Text)
+  , genThink :: !(Maybe Think)
+  , genWidth :: !(Maybe Int)
+  , genHeight :: !(Maybe Int)
+  , genSteps :: !(Maybe Int)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON GenerateRequest where
+  toJSON GenerateRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= genModel
+        , Just $ "prompt" .= genPrompt
+        , ("suffix" .=) <$> genSuffix
+        , ("images" .=) <$> genImages
+        , ("format" .=) <$> genFormat
+        , ("options" .=) <$> genOptions
+        , ("system" .=) <$> genSystem
+        , ("template" .=) <$> genTemplate
+        , ("stream" .=) <$> genStream
+        , ("raw" .=) <$> genRaw
+        , ("keep_alive" .=) <$> genKeepAlive
+        , ("think" .=) <$> genThink
+        , ("width" .=) <$> genWidth
+        , ("height" .=) <$> genHeight
+        , ("steps" .=) <$> genSteps
+        ]
+
+{- | Create a default 'GenerateRequest' for a model and prompt.
+
+@since 1.0.0.0
+-}
+generateRequest :: ModelName -> Text -> GenerateRequest
+generateRequest model prompt =
+  GenerateRequest
+    { genModel = model
+    , genPrompt = prompt
+    , genSuffix = Nothing
+    , genImages = Nothing
+    , genFormat = Nothing
+    , genOptions = Nothing
+    , genSystem = Nothing
+    , genTemplate = Nothing
+    , genStream = Just False
+    , genRaw = Nothing
+    , genKeepAlive = Nothing
+    , genThink = Nothing
+    , genWidth = Nothing
+    , genHeight = Nothing
+    , genSteps = Nothing
+    }
+
+{- | Response payload returned by text generation.
+
+@since 1.0.0.0
+-}
+data GenerateResponse = GenerateResponse
+  { grModel :: !ModelName
+  , grCreatedAt :: !UTCTime
+  , grResponse :: !Text
+  , grDone :: !Bool
+  , grDoneReason :: !(Maybe Text)
+  , grContext :: !(Maybe [Int])
+  , grTotalDuration :: !(Maybe Duration)
+  , grLoadDuration :: !(Maybe Duration)
+  , grPromptEvalCount :: !(Maybe Int)
+  , grPromptEvalDuration :: !(Maybe Duration)
+  , grEvalCount :: !(Maybe Int)
+  , grEvalDuration :: !(Maybe Duration)
+  , grThinking :: !(Maybe Text)
+  , grImage :: !(Maybe Base64Image)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON GenerateResponse where
+  parseJSON = withObject "GenerateResponse" $ \v ->
+    GenerateResponse
+      <$> v .: "model"
+      <*> v .: "created_at"
+      <*> v .: "response"
+      <*> v .: "done"
+      <*> v .:? "done_reason"
+      <*> v .:? "context"
+      <*> v .:? "total_duration"
+      <*> v .:? "load_duration"
+      <*> v .:? "prompt_eval_count"
+      <*> v .:? "prompt_eval_duration"
+      <*> v .:? "eval_count"
+      <*> v .:? "eval_duration"
+      <*> v .:? "thinking"
+      <*> v .:? "image"
+
+instance ToJSON GenerateResponse where
+  toJSON GenerateResponse {..} =
+    object
+      [ "model" .= grModel
+      , "created_at" .= grCreatedAt
+      , "response" .= grResponse
+      , "done" .= grDone
+      , "done_reason" .= grDoneReason
+      , "context" .= grContext
+      , "total_duration" .= grTotalDuration
+      , "load_duration" .= grLoadDuration
+      , "prompt_eval_count" .= grPromptEvalCount
+      , "prompt_eval_duration" .= grPromptEvalDuration
+      , "eval_count" .= grEvalCount
+      , "eval_duration" .= grEvalDuration
+      , "thinking" .= grThinking
+      , "image" .= grImage
+      ]
+
+instance HasDone GenerateResponse where
+  isDone = grDone
+
+{- | Non-streaming text completion API.
+
+@since 1.0.0.0
+-}
+generate ::
+  (MonadIO m) => OllamaClient -> GenerateRequest -> m (Either OllamaError GenerateResponse)
+generate client req = request client "POST" "/api/generate" (Just req {genStream = Just False})
+
+{- | Streaming text completion API yielding 'GenerateResponse' chunks.
+
+@since 1.0.0.0
+-}
+generateStream ::
+  (MonadUnliftIO m) => OllamaClient -> GenerateRequest -> ConduitT () GenerateResponse m ()
+generateStream client req = requestStreaming client "/api/generate" (req {genStream = Just True})
+
+{- | Calculate generation throughput (eval tokens \/ second) from a 'GenerateResponse'.
+
+@since 1.0.0.0
+-}
+evalTokensPerSecond :: GenerateResponse -> Maybe Double
+evalTokensPerSecond GenerateResponse {..} =
+  tokensPerSecond <$> grEvalCount <*> grEvalDuration
+
+{- | Calculate prompt evaluation throughput (prompt tokens \/ second) from a 'GenerateResponse'.
+
+@since 1.0.0.0
+-}
+promptEvalTokensPerSecond :: GenerateResponse -> Maybe Double
+promptEvalTokensPerSecond GenerateResponse {..} =
+  tokensPerSecond <$> grPromptEvalCount <*> grPromptEvalDuration
diff --git a/src/Ollama/API/Models.hs b/src/Ollama/API/Models.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Models.hs
@@ -0,0 +1,171 @@
+{- |
+Module      : Ollama.API.Models
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model management endpoints (list, show, copy, delete).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Models (
+  -- * Listing
+  listModels,
+  ListResponse (..),
+
+  -- * Show Info
+  showModel,
+  ShowRequest (..),
+  ShowResponse (..),
+  ShowModelInfo (..),
+
+  -- * Copy
+  copyModel,
+  CopyRequest (..),
+
+  -- * Delete
+  deleteModel,
+  DeleteRequest (..),
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Data.Aeson
+import Data.Map.Strict (Map)
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request)
+import Ollama.Error (OllamaError)
+import Ollama.Types.Common (ModelName)
+import Ollama.Types.Model (ListResponse (..), ModelDetails (..))
+
+{- | Show model info request payload.
+
+@since 1.0.0.0
+-}
+data ShowRequest = ShowRequest
+  { srqModel :: !ModelName
+  , srqVerbose :: !(Maybe Bool)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON ShowRequest where
+  toJSON ShowRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= srqModel
+        , ("verbose" .=) <$> srqVerbose
+        ]
+
+{- | Detailed technical parameters from model metadata.
+
+@since 1.0.0.0
+-}
+newtype ShowModelInfo = ShowModelInfo
+  { modelInfoMap :: Map Text Value
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON ShowModelInfo where
+  parseJSON v = ShowModelInfo <$> parseJSON v
+
+instance ToJSON ShowModelInfo where
+  toJSON (ShowModelInfo m) = toJSON m
+
+{- | Model inspection response payload.
+
+@since 1.0.0.0
+-}
+data ShowResponse = ShowResponse
+  { srsModelfile :: !Text
+  , srsParameters :: !(Maybe Text)
+  , srsTemplate :: !(Maybe Text)
+  , srsDetails :: !ModelDetails
+  , srsModelInfo :: !(Maybe ShowModelInfo)
+  , srsLicense :: !(Maybe Text)
+  , srsCapabilities :: !(Maybe [Text])
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON ShowResponse where
+  parseJSON = withObject "ShowResponse" $ \v ->
+    ShowResponse
+      <$> v .:? "modelfile" .!= ""
+      <*> v .:? "parameters"
+      <*> v .:? "template"
+      <*> v .:? "details" .!= ModelDetails Nothing "" "" [] "" ""
+      <*> v .:? "model_info"
+      <*> v .:? "license"
+      <*> v .:? "capabilities"
+
+instance ToJSON ShowResponse where
+  toJSON ShowResponse {..} =
+    object
+      [ "modelfile" .= srsModelfile
+      , "parameters" .= srsParameters
+      , "template" .= srsTemplate
+      , "details" .= srsDetails
+      , "model_info" .= srsModelInfo
+      , "license" .= srsLicense
+      , "capabilities" .= srsCapabilities
+      ]
+
+{- | Copy model request payload.
+
+@since 1.0.0.0
+-}
+data CopyRequest = CopyRequest
+  { cpSource :: !ModelName
+  , cpDestination :: !ModelName
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON CopyRequest where
+  toJSON CopyRequest {..} =
+    object
+      [ "source" .= cpSource
+      , "destination" .= cpDestination
+      ]
+
+{- | Delete model request payload.
+
+@since 1.0.0.0
+-}
+newtype DeleteRequest = DeleteRequest
+  { delModel :: ModelName
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON DeleteRequest where
+  toJSON DeleteRequest {..} = object ["model" .= delModel]
+
+{- | List installed local models (@GET /api/tags@).
+
+@since 1.0.0.0
+-}
+listModels :: (MonadIO m) => OllamaClient -> m (Either OllamaError ListResponse)
+listModels client = request client "GET" "/api/tags" (Nothing :: Maybe Value)
+
+{- | Fetch information and Modelfile content for a model (@POST /api/show@).
+
+@since 1.0.0.0
+-}
+showModel :: (MonadIO m) => OllamaClient -> ModelName -> m (Either OllamaError ShowResponse)
+showModel client model = request client "POST" "/api/show" (Just $ ShowRequest model Nothing)
+
+{- | Duplicate an existing model under a new tag (@POST /api/copy@).
+
+@since 1.0.0.0
+-}
+copyModel :: (MonadIO m) => OllamaClient -> ModelName -> ModelName -> m (Either OllamaError ())
+copyModel client src dst = request client "POST" "/api/copy" (Just $ CopyRequest src dst)
+
+{- | Delete a local model (@DELETE /api/delete@).
+
+@since 1.0.0.0
+-}
+deleteModel :: (MonadIO m) => OllamaClient -> ModelName -> m (Either OllamaError ())
+deleteModel client model = request client "DELETE" "/api/delete" (Just $ DeleteRequest model)
diff --git a/src/Ollama/API/Models/Create.hs b/src/Ollama/API/Models/Create.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Models/Create.hs
@@ -0,0 +1,165 @@
+{- |
+Module      : Ollama.API.Models.Create
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model creation endpoint (@/api/create@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Models.Create (
+  CreateRequest (..),
+  CreateResponse (..),
+  QuantizationType (..),
+  createModel,
+  createModelStream,
+  defaultCreateRequest,
+) where
+
+import Conduit (ConduitT)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.Aeson
+import Data.Int (Int64)
+import Data.Map.Strict (Map)
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request, requestStreaming)
+import Ollama.Error (OllamaError)
+import Ollama.Streaming (HasDone (..))
+import Ollama.Types.Common (Digest, ModelName)
+import Ollama.Types.Message (Message)
+import Ollama.Types.Options (ModelOptions)
+
+{- | Quantization precision options for model creation.
+
+@since 1.0.0.0
+-}
+data QuantizationType = Q4_K_M | Q4_K_S | Q8_0
+  deriving stock (Eq, Show, Bounded, Enum, Generic)
+
+instance ToJSON QuantizationType where
+  toJSON Q4_K_M = String "q4_K_M"
+  toJSON Q4_K_S = String "q4_K_S"
+  toJSON Q8_0 = String "q8_0"
+
+instance FromJSON QuantizationType where
+  parseJSON = withText "QuantizationType" $ \case
+    "q4_K_M" -> pure Q4_K_M
+    "q4_K_S" -> pure Q4_K_S
+    "q8_0" -> pure Q8_0
+    other -> fail $ "Invalid QuantizationType: " <> show other
+
+{- | Model creation request configuration payload.
+
+@since 1.0.0.0
+-}
+data CreateRequest = CreateRequest
+  { crqModel :: !ModelName
+  , crqFrom :: !(Maybe ModelName)
+  , crqFiles :: !(Maybe (Map Text Digest))
+  , crqAdapters :: !(Maybe (Map Text Digest))
+  , crqTemplate :: !(Maybe Text)
+  , crqRenderer :: !(Maybe Text)
+  , crqParser :: !(Maybe Text)
+  , crqLicense :: !(Maybe [Text])
+  , crqSystem :: !(Maybe Text)
+  , crqParameters :: !(Maybe ModelOptions)
+  , crqMessages :: !(Maybe [Message])
+  , crqStream :: !(Maybe Bool)
+  , crqQuantize :: !(Maybe QuantizationType)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON CreateRequest where
+  toJSON CreateRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= crqModel
+        , ("from" .=) <$> crqFrom
+        , ("files" .=) <$> crqFiles
+        , ("adapters" .=) <$> crqAdapters
+        , ("template" .=) <$> crqTemplate
+        , ("renderer" .=) <$> crqRenderer
+        , ("parser" .=) <$> crqParser
+        , ("license" .=) <$> crqLicense
+        , ("system" .=) <$> crqSystem
+        , ("parameters" .=) <$> crqParameters
+        , ("messages" .=) <$> crqMessages
+        , ("stream" .=) <$> crqStream
+        , ("quantize" .=) <$> crqQuantize
+        ]
+
+{- | Smart constructor for a basic 'CreateRequest'.
+
+@since 1.0.0.0
+-}
+defaultCreateRequest :: ModelName -> CreateRequest
+defaultCreateRequest name =
+  CreateRequest
+    { crqModel = name
+    , crqFrom = Nothing
+    , crqFiles = Nothing
+    , crqAdapters = Nothing
+    , crqTemplate = Nothing
+    , crqRenderer = Nothing
+    , crqParser = Nothing
+    , crqLicense = Nothing
+    , crqSystem = Nothing
+    , crqParameters = Nothing
+    , crqMessages = Nothing
+    , crqStream = Just False
+    , crqQuantize = Nothing
+    }
+
+{- | Progress / status response during model creation.
+
+@since 1.0.0.0
+-}
+data CreateResponse = CreateResponse
+  { crsStatus :: !Text
+  , crsDigest :: !(Maybe Digest)
+  , crsTotal :: !(Maybe Int64)
+  , crsCompleted :: !(Maybe Int64)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON CreateResponse where
+  parseJSON = withObject "CreateResponse" $ \v ->
+    CreateResponse
+      <$> v .: "status"
+      <*> v .:? "digest"
+      <*> v .:? "total"
+      <*> v .:? "completed"
+
+instance ToJSON CreateResponse where
+  toJSON CreateResponse {..} =
+    object
+      [ "status" .= crsStatus
+      , "digest" .= crsDigest
+      , "total" .= crsTotal
+      , "completed" .= crsCompleted
+      ]
+
+instance HasDone CreateResponse where
+  isDone CreateResponse {..} = crsStatus == "success"
+
+{- | Create a model non-streaming.
+
+@since 1.0.0.0
+-}
+createModel :: (MonadIO m) => OllamaClient -> CreateRequest -> m (Either OllamaError CreateResponse)
+createModel client req = request client "POST" "/api/create" (Just req {crqStream = Just False})
+
+{- | Create a model streaming progress updates.
+
+@since 1.0.0.0
+-}
+createModelStream ::
+  (MonadUnliftIO m) => OllamaClient -> CreateRequest -> ConduitT () CreateResponse m ()
+createModelStream client req = requestStreaming client "/api/create" (req {crqStream = Just True})
diff --git a/src/Ollama/API/Models/Pull.hs b/src/Ollama/API/Models/Pull.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Models/Pull.hs
@@ -0,0 +1,100 @@
+{- |
+Module      : Ollama.API.Models.Pull
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model pull endpoint (@/api/pull@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Models.Pull (
+  PullRequest (..),
+  PullResponse (..),
+  pull,
+  pullStream,
+) where
+
+import Conduit (ConduitT)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.Aeson
+import Data.Int (Int64)
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request, requestStreaming)
+import Ollama.Error (OllamaError)
+import Ollama.Streaming (HasDone (..))
+import Ollama.Types.Common (Digest, ModelName)
+
+{- | Pull model request configuration payload.
+
+@since 1.0.0.0
+-}
+data PullRequest = PullRequest
+  { prqModel :: !ModelName
+  , prqInsecure :: !(Maybe Bool)
+  , prqStream :: !(Maybe Bool)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON PullRequest where
+  toJSON PullRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= prqModel
+        , ("insecure" .=) <$> prqInsecure
+        , ("stream" .=) <$> prqStream
+        ]
+
+{- | Pull progress/status response payload.
+
+@since 1.0.0.0
+-}
+data PullResponse = PullResponse
+  { prStatus :: !Text
+  , prDigest :: !(Maybe Digest)
+  , prTotal :: !(Maybe Int64)
+  , prCompleted :: !(Maybe Int64)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON PullResponse where
+  parseJSON = withObject "PullResponse" $ \v ->
+    PullResponse
+      <$> v .: "status"
+      <*> v .:? "digest"
+      <*> v .:? "total"
+      <*> v .:? "completed"
+
+instance ToJSON PullResponse where
+  toJSON PullResponse {..} =
+    object
+      [ "status" .= prStatus
+      , "digest" .= prDigest
+      , "total" .= prTotal
+      , "completed" .= prCompleted
+      ]
+
+instance HasDone PullResponse where
+  isDone PullResponse {..} = prStatus == "success"
+
+{- | Download / pull a model (non-streaming, blocks until complete).
+
+@since 1.0.0.0
+-}
+pull :: (MonadIO m) => OllamaClient -> ModelName -> m (Either OllamaError PullResponse)
+pull client model =
+  request client "POST" "/api/pull" (Just $ PullRequest model Nothing (Just False))
+
+{- | Download / pull a model streaming progress updates.
+
+@since 1.0.0.0
+-}
+pullStream :: (MonadUnliftIO m) => OllamaClient -> ModelName -> ConduitT () PullResponse m ()
+pullStream client model =
+  requestStreaming client "/api/pull" (PullRequest model Nothing (Just True))
diff --git a/src/Ollama/API/Models/Push.hs b/src/Ollama/API/Models/Push.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Models/Push.hs
@@ -0,0 +1,100 @@
+{- |
+Module      : Ollama.API.Models.Push
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model push endpoint (@/api/push@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Models.Push (
+  PushRequest (..),
+  PushResponse (..),
+  push,
+  pushStream,
+) where
+
+import Conduit (ConduitT)
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.Aeson
+import Data.Int (Int64)
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request, requestStreaming)
+import Ollama.Error (OllamaError)
+import Ollama.Streaming (HasDone (..))
+import Ollama.Types.Common (Digest, ModelName)
+
+{- | Push model request payload.
+
+@since 1.0.0.0
+-}
+data PushRequest = PushRequest
+  { psqModel :: !ModelName
+  , psqInsecure :: !(Maybe Bool)
+  , psqStream :: !(Maybe Bool)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON PushRequest where
+  toJSON PushRequest {..} =
+    object $
+      catMaybes
+        [ Just $ "model" .= psqModel
+        , ("insecure" .=) <$> psqInsecure
+        , ("stream" .=) <$> psqStream
+        ]
+
+{- | Push progress response payload.
+
+@since 1.0.0.0
+-}
+data PushResponse = PushResponse
+  { psStatus :: !Text
+  , psDigest :: !(Maybe Digest)
+  , psTotal :: !(Maybe Int64)
+  , psCompleted :: !(Maybe Int64)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON PushResponse where
+  parseJSON = withObject "PushResponse" $ \v ->
+    PushResponse
+      <$> v .: "status"
+      <*> v .:? "digest"
+      <*> v .:? "total"
+      <*> v .:? "completed"
+
+instance ToJSON PushResponse where
+  toJSON PushResponse {..} =
+    object
+      [ "status" .= psStatus
+      , "digest" .= psDigest
+      , "total" .= psTotal
+      , "completed" .= psCompleted
+      ]
+
+instance HasDone PushResponse where
+  isDone PushResponse {..} = psStatus == "success"
+
+{- | Push a model to a remote library (non-streaming).
+
+@since 1.0.0.0
+-}
+push :: (MonadIO m) => OllamaClient -> ModelName -> m (Either OllamaError PushResponse)
+push client model =
+  request client "POST" "/api/push" (Just $ PushRequest model Nothing (Just False))
+
+{- | Push a model streaming upload progress updates.
+
+@since 1.0.0.0
+-}
+pushStream :: (MonadUnliftIO m) => OllamaClient -> ModelName -> ConduitT () PushResponse m ()
+pushStream client model =
+  requestStreaming client "/api/push" (PushRequest model Nothing (Just True))
diff --git a/src/Ollama/API/Ps.hs b/src/Ollama/API/Ps.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Ps.hs
@@ -0,0 +1,31 @@
+{- |
+Module      : Ollama.API.Ps
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Running models endpoint (@/api/ps@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Ps (
+  listRunning,
+  RunningModelsResponse (..),
+  RunningModel (..),
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Data.Aeson (Value)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request)
+import Ollama.Error (OllamaError)
+import Ollama.Types.Model (RunningModel (..), RunningModelsResponse (..))
+
+{- | List models currently loaded into memory (@GET /api/ps@).
+
+@since 1.0.0.0
+-}
+listRunning :: (MonadIO m) => OllamaClient -> m (Either OllamaError RunningModelsResponse)
+listRunning client = request client "GET" "/api/ps" (Nothing :: Maybe Value)
diff --git a/src/Ollama/API/Version.hs b/src/Ollama/API/Version.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/API/Version.hs
@@ -0,0 +1,30 @@
+{- |
+Module      : Ollama.API.Version
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Version endpoint (@/api/version@).
+
+@since 1.0.0.0
+-}
+module Ollama.API.Version (
+  getVersion,
+  Version (..),
+) where
+
+import Control.Monad.IO.Class (MonadIO)
+import Data.Aeson (Value)
+import Ollama.Client (OllamaClient)
+import Ollama.Client.Internal (request)
+import Ollama.Error (OllamaError)
+import Ollama.Types.Common (Version (..))
+
+{- | Retrieve Ollama server engine version (@GET /api/version@).
+
+@since 1.0.0.0
+-}
+getVersion :: (MonadIO m) => OllamaClient -> m (Either OllamaError Version)
+getVersion client = request client "GET" "/api/version" (Nothing :: Maybe Value)
diff --git a/src/Ollama/Client.hs b/src/Ollama/Client.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Client.hs
@@ -0,0 +1,110 @@
+{- |
+Module      : Ollama.Client
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Opaque client management and construction functions.
+
+@since 1.0.0.0
+-}
+module Ollama.Client (
+  OllamaClient (..),
+  newClient,
+  defaultClient,
+  clientFromEnv,
+  closeClient,
+  withClient,
+) where
+
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)
+import Data.Text (Text)
+import Data.Text qualified as T
+import Network.HTTP.Client (Manager, newManager)
+import Network.HTTP.Client.TLS (tlsManagerSettings)
+import Ollama.Client.Config (OllamaClientConfig (..), defaultConfig)
+import System.Environment (lookupEnv)
+
+{- | Opaque client handle for dispatching API calls.
+
+@since 1.0.0.0
+-}
+data OllamaClient = OllamaClient
+  { clientManager :: !Manager
+  , clientConfig :: !OllamaClientConfig
+  , clientOwned :: !Bool
+  }
+
+{- | Construct a client with custom configuration settings.
+
+@since 1.0.0.0
+-}
+newClient :: (MonadIO m) => OllamaClientConfig -> m OllamaClient
+newClient cfg = liftIO $ do
+  case configManager cfg of
+    Just mgr -> pure $ OllamaClient mgr cfg False
+    Nothing -> do
+      mgr <- newManager tlsManagerSettings
+      pure $ OllamaClient mgr cfg True
+
+{- | Construct a client with default settings (@http://127.0.0.1:11434@).
+
+@since 1.0.0.0
+-}
+defaultClient :: (MonadIO m) => m OllamaClient
+defaultClient = newClient defaultConfig
+
+{- | Construct a client resolving host and credentials from environment variables
+(@OLLAMA_HOST@, @OLLAMA_API_KEY@).
+
+Parses @OLLAMA_HOST@ robustly, handling these formats:
+
+  * @host:port@        → @http:\/\/host:port@
+  * @http:\/\/host:port@ → used as-is
+  * @host@             → @http:\/\/host:11434@
+
+@since 1.0.0.0
+-}
+clientFromEnv :: (MonadIO m) => m OllamaClient
+clientFromEnv = liftIO $ do
+  mbHost <- lookupEnv "OLLAMA_HOST"
+  mbKey <- lookupEnv "OLLAMA_API_KEY"
+  let hostText = maybe "http://127.0.0.1:11434" (normalizeHost . T.pack) mbHost
+      keyText = T.pack <$> mbKey
+      cfg = defaultConfig {configBaseUrl = hostText, configApiKey = keyText}
+  newClient cfg
+
+{- | Normalize an @OLLAMA_HOST@ value to a full URL with scheme.
+
+Handles: bare @host:port@, @http(s):\/\/host:port@, and bare @host@.
+
+@since 1.0.0.0
+-}
+normalizeHost :: Text -> Text
+normalizeHost raw
+  | "http://" `T.isPrefixOf` raw || "https://" `T.isPrefixOf` raw = T.dropWhileEnd (== '/') raw
+  | ":" `T.isInfixOf` raw = "http://" <> T.dropWhileEnd (== '/') raw
+  | otherwise = "http://" <> T.dropWhileEnd (== '/') raw <> ":11434"
+
+{- | Close the underlying HTTP connection manager if owned by this client.
+
+Note: Connection managers in @http-client@ are automatically reclaimed by garbage collection.
+
+@since 1.0.0.0
+-}
+closeClient :: (MonadIO m) => OllamaClient -> m ()
+closeClient _ = pure ()
+
+{- | Resource bracket helper to initialize, run a computation, and close client resources.
+
+@since 1.0.0.0
+-}
+withClient :: (MonadUnliftIO m) => OllamaClientConfig -> (OllamaClient -> m a) -> m a
+withClient cfg action = withRunInIO $ \run -> do
+  client <- newClient cfg
+  res <- run (action client)
+  closeClient client
+  pure res
diff --git a/src/Ollama/Client/Config.hs b/src/Ollama/Client/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Client/Config.hs
@@ -0,0 +1,103 @@
+{- |
+Module      : Ollama.Client.Config
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Client configuration settings, retry policies, and logging thresholds.
+
+@since 3.0.0.0
+-}
+module Ollama.Client.Config (
+  OllamaClientConfig (..),
+  defaultConfig,
+  RetryPolicy (..),
+  noRetry,
+  constantRetry,
+  exponentialRetry,
+  LogLevel (..),
+) where
+
+import Data.ByteString (ByteString)
+import Data.CaseInsensitive (CI)
+import Data.Text (Text)
+import Network.HTTP.Client (Manager)
+
+{- | Logging levels for structured client events.
+
+@since 3.0.0.0
+-}
+data LogLevel = Debug | Info | Warn | Error
+  deriving stock (Eq, Ord, Show, Bounded, Enum)
+
+{- | Configurable retry strategy for recoverable network errors.
+
+@since 3.0.0.0
+-}
+data RetryPolicy
+  = -- | Disable all retries
+    NoRetry
+  | -- | Retry up to @count@ times with fixed @delayMicros@ interval
+    ConstantRetry !Int !Int
+  | -- | Retry up to @count@ times with exponential backoff starting at @baseDelayMicros@
+    ExponentialRetry !Int !Int
+  deriving stock (Eq, Show)
+
+{- | Helper constructor for 'NoRetry'.
+
+@since 3.0.0.0
+-}
+noRetry :: RetryPolicy
+noRetry = NoRetry
+
+{- | Helper constructor for 'ConstantRetry'.
+
+@since 3.0.0.0
+-}
+constantRetry :: Int -> Int -> RetryPolicy
+constantRetry = ConstantRetry
+
+{- | Helper constructor for 'ExponentialRetry'.
+
+@since 3.0.0.0
+-}
+exponentialRetry :: Int -> Int -> RetryPolicy
+exponentialRetry = ExponentialRetry
+
+{- | Configuration settings for an 'OllamaClient'.
+
+@since 3.0.0.0
+-}
+data OllamaClientConfig = OllamaClientConfig
+  { configBaseUrl :: !Text
+  , configTimeout :: !Int
+  , configRetry :: !RetryPolicy
+  , configManager :: !(Maybe Manager)
+  , configHeaders :: ![(CI ByteString, ByteString)]
+  , configApiKey :: !(Maybe Text)
+  , configLogger :: !(Maybe (LogLevel -> Text -> IO ()))
+  , configOnStart :: !(Maybe (IO ()))
+  , configOnSuccess :: !(Maybe (IO ()))
+  , configOnError :: !(Maybe (IO ()))
+  }
+
+{- | Default configuration connecting to @http://127.0.0.1:11434@ with 90s timeout and 'NoRetry'.
+
+@since 3.0.0.0
+-}
+defaultConfig :: OllamaClientConfig
+defaultConfig =
+  OllamaClientConfig
+    { configBaseUrl = "http://127.0.0.1:11434"
+    , configTimeout = 90
+    , configRetry = NoRetry
+    , configManager = Nothing
+    , configHeaders = []
+    , configApiKey = Nothing
+    , configLogger = Nothing
+    , configOnStart = Nothing
+    , configOnSuccess = Nothing
+    , configOnError = Nothing
+    }
diff --git a/src/Ollama/Client/Internal.hs b/src/Ollama/Client/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Client/Internal.hs
@@ -0,0 +1,276 @@
+{- |
+Module      : Ollama.Client.Internal
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Low-level HTTP transport plumbing and request dispatchers.
+
+Implements retry logic, lifecycle callbacks, structured logging,
+authentication header injection, and conduit-based response streaming.
+
+@since 1.0.0.0
+-}
+module Ollama.Client.Internal (
+  request,
+  requestRaw,
+  requestStreaming,
+) where
+
+import Conduit (
+  ConduitT,
+  awaitForever,
+  bracketP,
+  filterC,
+  takeWhileC,
+  transPipe,
+  yield,
+  (.|),
+ )
+import Control.Exception (SomeException, catch, try)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Control.Monad.Trans.Resource (runResourceT)
+import Control.Retry qualified as Retry
+import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode)
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as BS
+import Data.ByteString.Lazy qualified as BSL
+import Data.CaseInsensitive (CI)
+import Data.Conduit.Binary qualified as CB
+import Data.Conduit.Combinators (repeatM)
+import Data.Text (Text)
+import Data.Text qualified as T
+import Data.Text.Encoding qualified as TE
+import Network.HTTP.Client
+import Network.HTTP.Types (statusCode)
+import Ollama.Client (OllamaClient (..))
+import Ollama.Client.Config (LogLevel (..), OllamaClientConfig (..), RetryPolicy (..))
+import Ollama.Error (OllamaError (..), isRetryable)
+
+-- ---------------------------------------------------------------------------
+-- Public API
+-- ---------------------------------------------------------------------------
+
+{- | Dispatch a non-streaming JSON API request.
+
+Applies authentication headers, retry policy, lifecycle callbacks,
+and structured logging as configured on the client.
+
+@since 1.0.0.0
+-}
+request ::
+  (MonadIO m, ToJSON req, FromJSON resp) =>
+  OllamaClient ->
+  ByteString ->
+  Text ->
+  Maybe req ->
+  m (Either OllamaError resp)
+request client reqMethod endpoint mbPayload =
+  liftIO $
+    withRetry client endpoint $
+      executeJsonRequest client reqMethod endpoint mbPayload
+
+{- | Dispatch a raw (non-JSON) request returning raw bytes.
+
+Used for blob endpoints where the response body is not JSON.
+Applies authentication headers, retry policy, lifecycle callbacks,
+and structured logging.
+
+@since 1.0.0.0
+-}
+requestRaw ::
+  (MonadIO m) =>
+  OllamaClient ->
+  ByteString ->
+  Text ->
+  Maybe ByteString ->
+  m (Either OllamaError ByteString)
+requestRaw client reqMethod endpoint mbPayload =
+  liftIO $
+    withRetry client endpoint $
+      executeRawRequest client reqMethod endpoint mbPayload
+
+{- | Dispatch a conduit-based streaming API request.
+
+Opens an HTTP response stream, reads line-delimited JSON chunks as they arrive,
+decodes each chunk, and yields values into a 'ConduitT'. Stops when the server
+closes the connection.
+
+@since 1.0.0.0
+-}
+requestStreaming ::
+  (MonadUnliftIO m, ToJSON req, FromJSON resp) =>
+  OllamaClient ->
+  Text ->
+  req ->
+  ConduitT () resp m ()
+requestStreaming OllamaClient {..} endpoint payload = do
+  let fullUrl = T.unpack $ configBaseUrl clientConfig <> endpoint
+      cfg = clientConfig
+  initReq <- liftIO $ parseRequest fullUrl
+  let req =
+        initReq
+          { method = "POST"
+          , responseTimeout = responseTimeoutNone
+          , requestHeaders =
+              [ ("Content-Type", "application/json")
+              , ("Accept", "application/x-ndjson, application/json")
+              , ("Connection", "keep-alive")
+              ]
+                ++ authHeader cfg
+                ++ configHeaders cfg
+          , requestBody = RequestBodyLBS (encode payload)
+          }
+  transPipe runResourceT $
+    bracketP
+      (responseOpen req clientManager)
+      responseClose
+      ( \resp -> do
+          let bodyReader = responseBody resp
+              readChunk = liftIO $ brRead bodyReader `catch` \(_ :: HttpException) -> pure BS.empty
+              source = repeatM readChunk .| takeWhileC (not . BS.null)
+          source .| CB.lines .| filterC (not . BS.null) .| parseAndYield
+      )
+  where
+    parseAndYield = awaitForever $ \line -> do
+      case eitherDecode (BSL.fromStrict line) of
+        Left _err -> pure ()
+        Right val -> yield val
+
+-- ---------------------------------------------------------------------------
+-- Core request execution
+-- ---------------------------------------------------------------------------
+
+-- | Execute a JSON request without retry wrapping.
+executeJsonRequest ::
+  (ToJSON req, FromJSON resp) =>
+  OllamaClient ->
+  ByteString ->
+  Text ->
+  Maybe req ->
+  IO (Either OllamaError resp)
+executeJsonRequest OllamaClient {..} reqMethod endpoint mbPayload = do
+  let fullUrl = T.unpack $ configBaseUrl clientConfig <> endpoint
+      cfg = clientConfig
+  initReq <- parseRequest fullUrl
+  let timeoutMicro = configTimeout cfg * 1000000
+      req =
+        initReq
+          { method = reqMethod
+          , responseTimeout = responseTimeoutMicro timeoutMicro
+          , requestHeaders =
+              [("Content-Type", "application/json"), ("Accept", "application/json")]
+                ++ authHeader cfg
+                ++ configHeaders cfg
+          , requestBody = maybe mempty (RequestBodyLBS . encode) mbPayload
+          }
+  result <- try @HttpException $ httpLbs req clientManager
+  case result of
+    Left httpErr -> pure $ Left $ HttpError httpErr
+    Right resp -> do
+      let status = statusCode (responseStatus resp)
+          body = responseBody resp
+      if status >= 200 && status < 300
+        then
+          if BSL.null body
+            then case eitherDecode "null" of
+              Left err -> pure $ Left $ DecodeError (T.pack err) (BSL.toStrict body)
+              Right val -> pure $ Right val
+            else case eitherDecode body of
+              Left err -> pure $ Left $ DecodeError (T.pack err) (BSL.toStrict body)
+              Right val -> pure $ Right val
+        else pure $ Left $ ApiError status (TE.decodeUtf8 . BSL.toStrict $ body)
+
+-- | Execute a raw byte request without retry wrapping.
+executeRawRequest ::
+  OllamaClient ->
+  ByteString ->
+  Text ->
+  Maybe ByteString ->
+  IO (Either OllamaError ByteString)
+executeRawRequest OllamaClient {..} reqMethod endpoint mbPayload = do
+  let fullUrl = T.unpack $ configBaseUrl clientConfig <> endpoint
+      cfg = clientConfig
+  initReq <- parseRequest fullUrl
+  let timeoutMicro = configTimeout cfg * 1000000
+      req =
+        initReq
+          { method = reqMethod
+          , responseTimeout = responseTimeoutMicro timeoutMicro
+          , requestHeaders = authHeader cfg ++ configHeaders cfg
+          , requestBody = maybe mempty RequestBodyBS mbPayload
+          }
+  result <- try @HttpException $ httpLbs req clientManager
+  case result of
+    Left httpErr -> pure $ Left $ HttpError httpErr
+    Right resp -> do
+      let status = statusCode (responseStatus resp)
+      if status >= 200 && status < 300
+        then pure $ Right (BSL.toStrict $ responseBody resp)
+        else pure $ Left $ ApiError status (TE.decodeUtf8 . BSL.toStrict $ body)
+      where
+        body = responseBody resp
+
+-- ---------------------------------------------------------------------------
+-- Retry logic
+-- ---------------------------------------------------------------------------
+
+{- | Wrap an IO action with the client's configured retry policy, lifecycle
+callbacks, and structured logging.
+
+Only retries when 'isRetryable' returns 'True' for the error.
+-}
+withRetry ::
+  OllamaClient ->
+  Text ->
+  IO (Either OllamaError a) ->
+  IO (Either OllamaError a)
+withRetry OllamaClient {clientConfig = cfg} endpoint action = do
+  let policy = toRetryPolicy (configRetry cfg)
+      shouldRetry _status (Left err) = do
+        logMsg cfg Warn $ "Retryable error on " <> endpoint <> ", will retry: " <> T.pack (show err)
+        pure $ isRetryable err
+      shouldRetry _status (Right _) = pure False
+  fireCallback (configOnStart cfg)
+  logMsg cfg Debug $ "Requesting " <> endpoint
+  result <- Retry.retrying policy shouldRetry (const action)
+  case result of
+    Left err -> do
+      fireCallback (configOnError cfg)
+      logMsg cfg Error $ "Request failed: " <> endpoint <> " — " <> T.pack (show err)
+    Right _ -> do
+      fireCallback (configOnSuccess cfg)
+      logMsg cfg Info $ "Request succeeded: " <> endpoint
+  pure result
+
+-- | Map our 'RetryPolicy' ADT to the @retry@ package's 'Retry.RetryPolicyM'.
+toRetryPolicy :: RetryPolicy -> Retry.RetryPolicyM IO
+toRetryPolicy NoRetry = Retry.limitRetries 0
+toRetryPolicy (ConstantRetry count delaySec) =
+  Retry.constantDelay (delaySec * 1000000) <> Retry.limitRetries count
+toRetryPolicy (ExponentialRetry count initialDelayMs) =
+  Retry.exponentialBackoff (initialDelayMs * 1000) <> Retry.limitRetries count
+
+-- ---------------------------------------------------------------------------
+-- Helpers
+-- ---------------------------------------------------------------------------
+
+-- | Build the Authorization header if an API key is configured.
+authHeader :: OllamaClientConfig -> [(CI ByteString, ByteString)]
+authHeader cfg = case configApiKey cfg of
+  Nothing -> []
+  Just key -> [("Authorization", "Bearer " <> TE.encodeUtf8 key)]
+
+-- | Fire an optional callback, silently ignoring exceptions.
+fireCallback :: Maybe (IO ()) -> IO ()
+fireCallback Nothing = pure ()
+fireCallback (Just cb) = cb `catch` \(_ :: SomeException) -> pure ()
+
+-- | Log a message via the configured logger, if present.
+logMsg :: OllamaClientConfig -> LogLevel -> Text -> IO ()
+logMsg cfg level msg = case configLogger cfg of
+  Nothing -> pure ()
+  Just logger -> logger level msg `catch` \(_ :: SomeException) -> pure ()
diff --git a/src/Ollama/Conversation.hs b/src/Ollama/Conversation.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Conversation.hs
@@ -0,0 +1,110 @@
+{- |
+Module      : Ollama.Conversation
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Conversation store typeclass and in-memory transactional implementation.
+
+@since 1.0.0.0
+-}
+module Ollama.Conversation (
+  Conversation (..),
+  ConversationStore (..),
+  InMemoryStore (..),
+  initInMemoryStore,
+  saveConversationInMemory,
+  loadConversationInMemory,
+  listConversationsInMemory,
+  deleteConversationInMemory,
+) where
+
+import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVar, readTVarIO)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Reader (ReaderT, ask)
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+import Ollama.Types.Common (ModelName)
+import Ollama.Types.Message (Message)
+
+{- | Recorded chat conversation session.
+
+@since 1.0.0.0
+-}
+data Conversation = Conversation
+  { conversationId :: !Text
+  , messages :: ![Message]
+  , model :: !ModelName
+  , createdAt :: !UTCTime
+  , lastUpdated :: !UTCTime
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+{- | Abstract interface for persisting and managing chat conversations.
+
+@since 1.0.0.0
+-}
+class (Monad m) => ConversationStore m where
+  saveConversation :: Conversation -> m ()
+  loadConversation :: Text -> m (Maybe Conversation)
+  listConversations :: m [Conversation]
+  deleteConversation :: Text -> m Bool
+
+{- | Thread-safe transactional in-memory conversation store.
+
+@since 1.0.0.0
+-}
+newtype InMemoryStore = InMemoryStore (TVar (Map Text Conversation))
+
+{- | Initialize a new empty 'InMemoryStore'.
+
+@since 1.0.0.0
+-}
+initInMemoryStore :: IO InMemoryStore
+initInMemoryStore = InMemoryStore <$> newTVarIO Map.empty
+
+{- | Save a conversation into an 'InMemoryStore'.
+
+@since 1.0.0.0
+-}
+saveConversationInMemory :: (MonadIO m) => InMemoryStore -> Conversation -> m ()
+saveConversationInMemory (InMemoryStore ref) conv = liftIO $ atomically $ do
+  modifyTVar' ref (Map.insert (conversationId conv) conv)
+
+{- | Load a conversation by ID from an 'InMemoryStore'.
+
+@since 1.0.0.0
+-}
+loadConversationInMemory :: (MonadIO m) => InMemoryStore -> Text -> m (Maybe Conversation)
+loadConversationInMemory (InMemoryStore ref) cid = liftIO $ Map.lookup cid <$> readTVarIO ref
+
+{- | List all stored conversations from an 'InMemoryStore'.
+
+@since 1.0.0.0
+-}
+listConversationsInMemory :: (MonadIO m) => InMemoryStore -> m [Conversation]
+listConversationsInMemory (InMemoryStore ref) = liftIO $ Map.elems <$> readTVarIO ref
+
+{- | Delete a conversation by ID from an 'InMemoryStore'.
+
+@since 1.0.0.0
+-}
+deleteConversationInMemory :: (MonadIO m) => InMemoryStore -> Text -> m Bool
+deleteConversationInMemory (InMemoryStore ref) cid = liftIO $ atomically $ do
+  m <- readTVar ref
+  if Map.member cid m
+    then modifyTVar' ref (Map.delete cid) >> pure True
+    else pure False
+
+instance (MonadIO m) => ConversationStore (ReaderT InMemoryStore m) where
+  saveConversation conv = ask >>= \store -> saveConversationInMemory store conv
+  loadConversation cid = ask >>= \store -> loadConversationInMemory store cid
+  listConversations = ask >>= \store -> listConversationsInMemory store
+  deleteConversation cid = ask >>= \store -> deleteConversationInMemory store cid
diff --git a/src/Ollama/Error.hs b/src/Ollama/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Error.hs
@@ -0,0 +1,65 @@
+{- |
+Module      : Ollama.Error
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Core error type definitions for the Ollama library.
+
+@since 3.0.0.0
+-}
+module Ollama.Error (
+  OllamaError (..),
+  isRetryable,
+  throwOllama,
+) where
+
+import Control.Exception (Exception, throwIO)
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Network.HTTP.Client (HttpException)
+
+{- | Unified error type representing all failure modes in the Ollama client.
+
+@since 3.0.0.0
+-}
+data OllamaError
+  = -- | HTTP transport failure (connection error, DNS failure, etc.)
+    HttpError !HttpException
+  | -- | Ollama API returned an error response (HTTP status code + message)
+    ApiError !Int !Text
+  | -- | Failed to decode JSON response body
+    DecodeError !Text !ByteString
+  | -- | Request timed out waiting for a response
+    TimeoutError
+  | -- | Client-side validation failure before sending the request
+    InvalidRequest !Text
+  deriving stock (Show)
+
+instance Exception OllamaError
+
+instance Eq OllamaError where
+  ApiError s1 t1 == ApiError s2 t2 = s1 == s2 && t1 == t2
+  DecodeError t1 _ == DecodeError t2 _ = t1 == t2
+  TimeoutError == TimeoutError = True
+  InvalidRequest t1 == InvalidRequest t2 = t1 == t2
+  HttpError _ == HttpError _ = False
+  _ == _ = False
+
+{- | Determine whether an error is transient and safe to retry.
+
+@since 3.0.0.0
+-}
+isRetryable :: OllamaError -> Bool
+isRetryable (HttpError _) = True
+isRetryable TimeoutError = True
+isRetryable _ = False
+
+{- | Helper to throw an 'OllamaError' as an exception.
+
+@since 3.0.0.0
+-}
+throwOllama :: OllamaError -> IO a
+throwOllama = throwIO
diff --git a/src/Ollama/Streaming.hs b/src/Ollama/Streaming.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Streaming.hs
@@ -0,0 +1,41 @@
+{- |
+Module      : Ollama.Streaming
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Streaming pipeline utilities, stream combinators, and 'HasDone' class for API responses.
+
+@since 1.0.0.0
+-}
+module Ollama.Streaming (
+  HasDone (..),
+  collectStream,
+  foldStream,
+) where
+
+import Conduit (ConduitT, foldlC, runConduit, sinkList, (.|))
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+
+{- | Typeclass for responses that indicate completion in a stream.
+
+@since 1.0.0.0
+-}
+class HasDone a where
+  isDone :: a -> Bool
+
+{- | Collect all yielded items from a streaming conduit into a list.
+
+@since 1.0.0.0
+-}
+collectStream :: (MonadUnliftIO m) => ConduitT () a m () -> m [a]
+collectStream stream = runConduit $ stream .| sinkList
+
+{- | Fold over all yielded items from a streaming conduit.
+
+@since 1.0.0.0
+-}
+foldStream :: (MonadUnliftIO m) => (b -> a -> b) -> b -> ConduitT () a m () -> m b
+foldStream f acc stream = runConduit $ stream .| foldlC f acc
diff --git a/src/Ollama/Testing.hs b/src/Ollama/Testing.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Testing.hs
@@ -0,0 +1,162 @@
+{- |
+Module      : Ollama.Testing
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Mock client and test helpers for unit testing code without a live Ollama server.
+
+@since 1.0.0.0
+-}
+module Ollama.Testing (
+  newMockClient,
+  withMockClient,
+  mockGenerateResponse,
+  mockChatResponse,
+  mockEmbedResponse,
+  mockListModelsResponse,
+) where
+
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import Control.Monad.IO.Unlift (MonadUnliftIO)
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 qualified as BS8
+import Data.IORef (atomicModifyIORef, newIORef)
+import Data.Text (Text)
+import Data.Time (Day (..), UTCTime (..))
+import Network.HTTP.Client (
+  defaultManagerSettings,
+  managerRawConnection,
+  newManager,
+ )
+import Network.HTTP.Client.Internal (makeConnection)
+import Ollama.API.Chat (ChatResponse (..))
+import Ollama.API.Embed (EmbedResponse (..))
+import Ollama.API.Generate (GenerateResponse (..))
+import Ollama.Client (OllamaClient (..), closeClient)
+import Ollama.Client.Config (defaultConfig)
+import Ollama.Types.Common (Digest (..), Duration (..), ModelName (..))
+import Ollama.Types.Message (assistantMessage)
+import Ollama.Types.Model (ListResponse (..), ModelDetails (..), ModelInfo (..))
+
+{- | Construct an 'OllamaClient' that returns mock HTTP response bytes for any request.
+
+@since 1.0.0.0
+-}
+newMockClient :: (MonadIO m) => ByteString -> m OllamaClient
+newMockClient bodyBytes = liftIO $ do
+  let httpResponse =
+        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: "
+          <> BS8.pack (show (BS8.length bodyBytes))
+          <> "\r\n\r\n"
+          <> bodyBytes
+  ref <- newIORef [httpResponse]
+  let mockConn =
+        makeConnection
+          ( atomicModifyIORef ref $ \case
+              [] -> ([], "")
+              (x : xs) -> (xs, x)
+          )
+          (\_ -> pure ())
+          (pure ())
+      settings = defaultManagerSettings {managerRawConnection = pure (\_ _ _ -> mockConn)}
+  mgr <- newManager settings
+  pure $ OllamaClient mgr defaultConfig True
+
+{- | Resource bracket helper to create a mock client, execute an action, and close resources.
+
+@since 1.0.0.0
+-}
+withMockClient :: (MonadUnliftIO m) => ByteString -> (OllamaClient -> m a) -> m a
+withMockClient body action = do
+  client <- newMockClient body
+  res <- action client
+  closeClient client
+  pure res
+
+{- | Construct a mock 'GenerateResponse'.
+
+@since 1.0.0.0
+-}
+mockGenerateResponse :: ModelName -> Text -> GenerateResponse
+mockGenerateResponse model text =
+  GenerateResponse
+    { grModel = model
+    , grCreatedAt = UTCTime (ModifiedJulianDay 60000) 0
+    , grResponse = text
+    , grDone = True
+    , grDoneReason = Just "stop"
+    , grContext = Just [1, 2, 3]
+    , grTotalDuration = Just (Duration 1000000000)
+    , grLoadDuration = Just (Duration 100000000)
+    , grPromptEvalCount = Just 10
+    , grPromptEvalDuration = Just (Duration 200000000)
+    , grEvalCount = Just 20
+    , grEvalDuration = Just (Duration 700000000)
+    , grThinking = Nothing
+    , grImage = Nothing
+    }
+
+{- | Construct a mock 'ChatResponse'.
+
+@since 1.0.0.0
+-}
+mockChatResponse :: ModelName -> Text -> ChatResponse
+mockChatResponse model text =
+  ChatResponse
+    { crModel = model
+    , crCreatedAt = UTCTime (ModifiedJulianDay 60000) 0
+    , crMessage = Just (assistantMessage text)
+    , crDone = True
+    , crDoneReason = Just "stop"
+    , crTotalDuration = Just (Duration 1000000000)
+    , crLoadDuration = Just (Duration 100000000)
+    , crPromptEvalCount = Just 10
+    , crPromptEvalDuration = Just (Duration 200000000)
+    , crEvalCount = Just 20
+    , crEvalDuration = Just (Duration 700000000)
+    }
+
+{- | Construct a mock 'EmbedResponse'.
+
+@since 1.0.0.0
+-}
+mockEmbedResponse :: ModelName -> [[Double]] -> EmbedResponse
+mockEmbedResponse model vectors =
+  EmbedResponse
+    { erModel = model
+    , erEmbeddings = vectors
+    , erTotalDuration = Just (Duration 500000000)
+    , erLoadDuration = Just (Duration 50000000)
+    , erPromptEvalCount = Just 8
+    }
+
+{- | Construct a mock 'ListResponse'.
+
+@since 1.0.0.0
+-}
+mockListModelsResponse :: [ModelName] -> ListResponse
+mockListModelsResponse names =
+  ListResponse
+    { models = map mkModel names
+    }
+  where
+    mkModel name =
+      ModelInfo
+        { miName = name
+        , miModel = name
+        , miModifiedAt = UTCTime (ModifiedJulianDay 60000) 0
+        , miSize = 4000000000
+        , miDigest = Digest "sha256:1234567890abcdef"
+        , miDetails =
+            ModelDetails
+              { parentModel = Nothing
+              , format = "gguf"
+              , family = "llama"
+              , families = ["llama"]
+              , parameterSize = "7B"
+              , quantizationLevel = "Q4_K_M"
+              }
+        }
diff --git a/src/Ollama/Types.hs b/src/Ollama/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types.hs
@@ -0,0 +1,38 @@
+{- |
+Module      : Ollama.Types
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Re-export all domain types for convenience.
+
+@since 3.0.0.0
+-}
+module Ollama.Types (
+  module Ollama.Types.Common,
+  Role (System, User, Assistant),
+  Message (..),
+  userMessage,
+  systemMessage,
+  assistantMessage,
+  toolMessage,
+  toolResultMessage,
+  imageMessage,
+  Tool (..),
+  FunctionDef (..),
+  FunctionParameters (..),
+  ToolCall (..),
+  ToolCallFunction (..),
+  module Ollama.Types.Options,
+  module Ollama.Types.Format,
+  module Ollama.Types.Model,
+) where
+
+import Ollama.Types.Common
+import Ollama.Types.Format
+import Ollama.Types.Message
+import Ollama.Types.Model
+import Ollama.Types.Options
+import Ollama.Types.Tool
diff --git a/src/Ollama/Types/Common.hs b/src/Ollama/Types/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Common.hs
@@ -0,0 +1,152 @@
+{- |
+Module      : Ollama.Types.Common
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Common newtypes and domain primitives for the Ollama API.
+
+@since 3.0.0.0
+-}
+module Ollama.Types.Common (
+  ModelName (..),
+  mkModelName,
+  Digest (..),
+  Base64Image (..),
+  Duration (..),
+  durationToSeconds,
+  durationToMillis,
+  tokensPerSecond,
+  Version (..),
+  Think (..),
+  ThinkingLevel (..),
+) where
+
+import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withText, (.:), (.=))
+import Data.Aeson.Types (typeMismatch)
+import Data.Hashable (Hashable)
+import Data.Int (Int64)
+import Data.String (IsString)
+import Data.Text (Text)
+import Data.Text qualified as T
+import GHC.Generics (Generic)
+
+{- | Model name following the @model:tag@ format.
+
+@since 3.0.0.0
+-}
+newtype ModelName = ModelName {unModelName :: Text}
+  deriving newtype (Eq, Ord, Show, IsString, ToJSON, FromJSON, Hashable)
+
+{- | Smart constructor that validates that a model name is non-empty.
+
+@since 3.0.0.0
+-}
+mkModelName :: Text -> Either Text ModelName
+mkModelName t
+  | T.null t = Left "Model name cannot be empty"
+  | otherwise = Right (ModelName t)
+
+{- | SHA256 digest of a layer blob.
+
+@since 3.0.0.0
+-}
+newtype Digest = Digest {unDigest :: Text}
+  deriving newtype (Eq, Ord, Show, ToJSON, FromJSON, Hashable)
+
+{- | Base64-encoded image data for multimodal inputs.
+
+@since 3.0.0.0
+-}
+newtype Base64Image = Base64Image {unBase64Image :: Text}
+  deriving newtype (Eq, Show, ToJSON, FromJSON)
+
+{- | Duration in nanoseconds as returned by the Ollama API.
+
+@since 3.0.0.0
+-}
+newtype Duration = Duration {durationNanos :: Int64}
+  deriving newtype (Eq, Ord, Show, ToJSON, FromJSON, Num)
+
+{- | Convert duration nanoseconds to seconds.
+
+@since 3.0.0.0
+-}
+durationToSeconds :: Duration -> Double
+durationToSeconds (Duration ns) = fromIntegral ns / 1e9
+
+{- | Convert duration nanoseconds to milliseconds.
+
+@since 1.0.0.0
+-}
+durationToMillis :: Duration -> Double
+durationToMillis (Duration ns) = fromIntegral ns / 1e6
+
+{- | Calculate tokens per second (tokens\/s) given a token count and a 'Duration'.
+
+@since 1.0.0.0
+-}
+tokensPerSecond :: Int -> Duration -> Double
+tokensPerSecond count dur =
+  let secs = durationToSeconds dur
+   in if secs <= 0 then 0.0 else fromIntegral count / secs
+
+{- | Ollama server engine version string.
+
+@since 3.0.0.0
+-}
+newtype Version = Version {unVersion :: Text}
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON Version where
+  parseJSON = \case
+    String s -> pure $ Version s
+    Object v -> Version <$> v .: "version"
+    v -> typeMismatch "Version" v
+
+instance ToJSON Version where
+  toJSON (Version s) = object ["version" .= s]
+
+{- | Thinking level settings for reasoning models.
+
+@since 3.0.0.0
+-}
+data ThinkingLevel = ThinkLow | ThinkMedium | ThinkHigh | ThinkMax
+  deriving stock (Eq, Show, Bounded, Enum, Generic)
+
+instance ToJSON ThinkingLevel where
+  toJSON ThinkLow = String "low"
+  toJSON ThinkMedium = String "medium"
+  toJSON ThinkHigh = String "high"
+  toJSON ThinkMax = String "max"
+
+instance FromJSON ThinkingLevel where
+  parseJSON = withText "ThinkingLevel" $ \case
+    "low" -> pure ThinkLow
+    "medium" -> pure ThinkMedium
+    "high" -> pure ThinkHigh
+    "max" -> pure ThinkMax
+    other -> fail $ "Unknown thinking level: " <> T.unpack other
+
+{- | Controls whether a thinking/reasoning model outputs its thoughts.
+
+@since 3.0.0.0
+-}
+data Think
+  = ThinkEnabled
+  | ThinkDisabled
+  | ThinkLevel !ThinkingLevel
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON Think where
+  toJSON ThinkEnabled = Bool True
+  toJSON ThinkDisabled = Bool False
+  toJSON (ThinkLevel lvl) = toJSON lvl
+
+instance FromJSON Think where
+  parseJSON (Bool True) = pure ThinkEnabled
+  parseJSON (Bool False) = pure ThinkDisabled
+  parseJSON (String s) = ThinkLevel <$> parseJSON (String s)
+  parseJSON v = typeMismatch "Think" v
diff --git a/src/Ollama/Types/Format.hs b/src/Ollama/Types/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Format.hs
@@ -0,0 +1,38 @@
+{- |
+Module      : Ollama.Types.Format
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Response formatting specifications for structured model output.
+
+@since 1.0.0.0
+-}
+module Ollama.Types.Format (
+  Format (..),
+  module Ollama.Types.Format.SchemaBuilder,
+) where
+
+import Data.Aeson
+import Ollama.Types.Format.SchemaBuilder
+
+{- | Response output format hint.
+
+@since 1.0.0.0
+-}
+data Format
+  = -- | Constrain response output to generic valid JSON
+    JsonFormat
+  | -- | Constrain response output to a specific JSON Schema
+    SchemaFormat !Schema
+  deriving stock (Eq, Show)
+
+instance ToJSON Format where
+  toJSON JsonFormat = String "json"
+  toJSON (SchemaFormat sch) = toJSON sch
+
+instance FromJSON Format where
+  parseJSON (String "json") = pure JsonFormat
+  parseJSON v = SchemaFormat <$> parseJSON v
diff --git a/src/Ollama/Types/Format/SchemaBuilder.hs b/src/Ollama/Types/Format/SchemaBuilder.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Format/SchemaBuilder.hs
@@ -0,0 +1,223 @@
+{- |
+Module      : Ollama.Types.Format.SchemaBuilder
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+DSL for constructing structured JSON Schemas for Ollama's structured output API.
+
+@since 1.0.0.0
+-}
+module Ollama.Types.Format.SchemaBuilder (
+  JsonType (..),
+  Property (..),
+  Schema (..),
+  SchemaBuilder,
+  emptyObject,
+  addProperty,
+  addObjectProperty,
+  requireField,
+  requireFields,
+  buildSchema,
+  objectOf,
+  arrayOf,
+  printSchema,
+  (|+),
+  (|++),
+  (|!),
+  (|!!),
+) where
+
+import Data.Aeson
+import Data.Map.Strict qualified as HM
+import Data.Text (Text)
+import Data.Text qualified as T
+import Data.Text.Lazy qualified as TL
+import Data.Text.Lazy.Encoding qualified as TEncoding
+import GHC.Generics (Generic)
+
+{- | Supported JSON primitive and compound types.
+
+@since 1.0.0.0
+-}
+data JsonType
+  = JString
+  | JNumber
+  | JInteger
+  | JBoolean
+  | JNull
+  | JArray !JsonType
+  | JObject !Schema
+  deriving stock (Show, Eq, Generic)
+
+instance ToJSON JsonType where
+  toJSON JString = "string"
+  toJSON JNumber = "number"
+  toJSON JInteger = "integer"
+  toJSON JBoolean = "boolean"
+  toJSON JNull = "null"
+  toJSON (JArray _) = "array"
+  toJSON (JObject _) = "object"
+
+{- | Property metadata for schema properties.
+
+@since 1.0.0.0
+-}
+newtype Property = Property JsonType
+  deriving stock (Show, Eq, Generic)
+
+instance ToJSON Property where
+  toJSON (Property (JArray itemType)) =
+    object ["type" .= ("array" :: Text), "items" .= Property itemType]
+  toJSON (Property (JObject schema)) = toJSON schema
+  toJSON (Property typ) = object ["type" .= typ]
+
+{- | JSON schema specification.
+
+@since 1.0.0.0
+-}
+data Schema = Schema
+  { schemaProperties :: !(HM.Map Text Property)
+  , schemaRequired :: ![Text]
+  }
+  deriving stock (Show, Eq, Generic)
+
+instance ToJSON Schema where
+  toJSON (Schema props req) =
+    object
+      [ "type" .= ("object" :: Text)
+      , "properties" .= props
+      , "required" .= req
+      ]
+
+instance FromJSON JsonType where
+  parseJSON = withText "JsonType" $ \case
+    "string" -> pure JString
+    "number" -> pure JNumber
+    "integer" -> pure JInteger
+    "boolean" -> pure JBoolean
+    "null" -> pure JNull
+    "array" -> pure (JArray JString)
+    "object" -> pure (JObject (Schema HM.empty []))
+    other -> fail $ "Unknown JsonType: " <> T.unpack other
+
+instance FromJSON Property where
+  parseJSON = withObject "Property" $ \v -> do
+    t <- v .: "type"
+    case (t :: Text) of
+      "array" -> Property . JArray <$> (v .: "items" >>= parseJSON)
+      "object" -> Property . JObject <$> parseJSON (Object v)
+      _ -> Property <$> parseJSON (String t)
+
+instance FromJSON Schema where
+  parseJSON = withObject "Schema" $ \v ->
+    Schema
+      <$> v .:? "properties" .!= HM.empty
+      <*> v .:? "required" .!= []
+
+{- | Opaque builder for fluid schema construction.
+
+@since 1.0.0.0
+-}
+newtype SchemaBuilder = SchemaBuilder Schema
+  deriving stock (Show, Eq)
+
+{- | Create an empty schema builder object.
+
+@since 1.0.0.0
+-}
+emptyObject :: SchemaBuilder
+emptyObject = SchemaBuilder $ Schema HM.empty []
+
+{- | Add a primitive property to the schema builder.
+
+@since 1.0.0.0
+-}
+addProperty :: Text -> JsonType -> SchemaBuilder -> SchemaBuilder
+addProperty name typ (SchemaBuilder s) =
+  SchemaBuilder $ s {schemaProperties = HM.insert name (Property typ) (schemaProperties s)}
+
+{- | Add a nested object property to the schema builder.
+
+@since 1.0.0.0
+-}
+addObjectProperty :: Text -> Schema -> SchemaBuilder -> SchemaBuilder
+addObjectProperty name nestedSchema (SchemaBuilder s) =
+  SchemaBuilder $
+    s {schemaProperties = HM.insert name (Property (JObject nestedSchema)) (schemaProperties s)}
+
+{- | Mark a field as required.
+
+@since 1.0.0.0
+-}
+requireField :: Text -> SchemaBuilder -> SchemaBuilder
+requireField name (SchemaBuilder s) =
+  SchemaBuilder $ s {schemaRequired = name : schemaRequired s}
+
+{- | Mark multiple fields as required.
+
+@since 1.0.0.0
+-}
+requireFields :: [Text] -> SchemaBuilder -> SchemaBuilder
+requireFields names builder = foldr requireField builder names
+
+{- | Finalize a 'Schema' from a 'SchemaBuilder'.
+
+@since 1.0.0.0
+-}
+buildSchema :: SchemaBuilder -> Schema
+buildSchema (SchemaBuilder s) = s
+
+{- | Treat a 'SchemaBuilder' as a nested object type.
+
+@since 1.0.0.0
+-}
+objectOf :: SchemaBuilder -> JsonType
+objectOf builder = JObject (buildSchema builder)
+
+{- | Create an array schema type of an element type.
+
+@since 1.0.0.0
+-}
+arrayOf :: JsonType -> JsonType
+arrayOf = JArray
+
+{- | Pretty-print a schema as formatted JSON.
+
+@since 1.0.0.0
+-}
+printSchema :: Schema -> IO ()
+printSchema = putStrLn . T.unpack . TL.toStrict . TEncoding.decodeUtf8 . encode
+
+{- | Infix alias for 'addProperty'.
+
+@since 1.0.0.0
+-}
+(|+) :: SchemaBuilder -> (Text, JsonType) -> SchemaBuilder
+builder |+ (name, typ) = addProperty name typ builder
+
+{- | Infix alias for 'addObjectProperty'.
+
+@since 1.0.0.0
+-}
+(|++) :: SchemaBuilder -> (Text, Schema) -> SchemaBuilder
+builder |++ (name, schema) = addObjectProperty name schema builder
+
+{- | Infix alias for 'requireField'.
+
+@since 1.0.0.0
+-}
+(|!) :: SchemaBuilder -> Text -> SchemaBuilder
+builder |! name = requireField name builder
+
+{- | Infix alias for 'requireFields'.
+
+@since 1.0.0.0
+-}
+(|!!) :: SchemaBuilder -> [Text] -> SchemaBuilder
+builder |!! names = requireFields names builder
+
+infixl 7 |+, |++
+infixl 6 |!, |!!
diff --git a/src/Ollama/Types/Message.hs b/src/Ollama/Types/Message.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Message.hs
@@ -0,0 +1,129 @@
+{- |
+Module      : Ollama.Types.Message
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Chat message definitions and helper constructors.
+
+@since 3.0.0.0
+-}
+module Ollama.Types.Message (
+  Role (..),
+  Message (..),
+  userMessage,
+  systemMessage,
+  assistantMessage,
+  toolMessage,
+  toolResultMessage,
+  imageMessage,
+) where
+
+import Data.Aeson
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Ollama.Types.Common (Base64Image)
+import Ollama.Types.Tool (ToolCall)
+
+{- | Entity role in a conversation.
+
+@since 3.0.0.0
+-}
+data Role = System | User | Assistant | Tool
+  deriving stock (Eq, Ord, Show, Bounded, Enum, Generic)
+
+instance ToJSON Role where
+  toJSON System = String "system"
+  toJSON User = String "user"
+  toJSON Assistant = String "assistant"
+  toJSON Tool = String "tool"
+
+instance FromJSON Role where
+  parseJSON = withText "Role" $ \case
+    "system" -> pure System
+    "user" -> pure User
+    "assistant" -> pure Assistant
+    "tool" -> pure Tool
+    other -> fail $ "Invalid Role: " <> show other
+
+{- | Chat message within a conversation payload.
+
+@since 3.0.0.0
+-}
+data Message = Message
+  { messageRole :: !Role
+  , messageContent :: !Text
+  , messageImages :: !(Maybe [Base64Image])
+  , messageToolCalls :: !(Maybe [ToolCall])
+  , messageToolName :: !(Maybe Text)
+  , messageThinking :: !(Maybe Text)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON Message where
+  toJSON Message {..} =
+    object $
+      catMaybes
+        [ Just $ "role" .= messageRole
+        , Just $ "content" .= messageContent
+        , ("images" .=) <$> messageImages
+        , ("tool_calls" .=) <$> messageToolCalls
+        , ("tool_name" .=) <$> messageToolName
+        , ("thinking" .=) <$> messageThinking
+        ]
+
+instance FromJSON Message where
+  parseJSON = withObject "Message" $ \v ->
+    Message
+      <$> v .: "role"
+      <*> v .: "content"
+      <*> v .:? "images"
+      <*> v .:? "tool_calls"
+      <*> v .:? "tool_name"
+      <*> v .:? "thinking"
+
+{- | Create a 'User' role message.
+
+@since 3.0.0.0
+-}
+userMessage :: Text -> Message
+userMessage t = Message User t Nothing Nothing Nothing Nothing
+
+{- | Create a 'System' role message.
+
+@since 3.0.0.0
+-}
+systemMessage :: Text -> Message
+systemMessage t = Message System t Nothing Nothing Nothing Nothing
+
+{- | Create an 'Assistant' role message.
+
+@since 3.0.0.0
+-}
+assistantMessage :: Text -> Message
+assistantMessage t = Message Assistant t Nothing Nothing Nothing Nothing
+
+{- | Create a 'Tool' role message.
+
+@since 3.0.0.0
+-}
+toolMessage :: Text -> Message
+toolMessage t = Message Tool t Nothing Nothing Nothing Nothing
+
+{- | Create a 'Tool' role message with specific @tool_name@ informing the model of tool execution.
+
+@since 3.0.0.0
+-}
+toolResultMessage :: Text -> Text -> Message
+toolResultMessage content toolName =
+  Message Tool content Nothing Nothing (Just toolName) Nothing
+
+{- | Create a 'User' message with attached Base64 image data.
+
+@since 3.0.0.0
+-}
+imageMessage :: Text -> [Base64Image] -> Message
+imageMessage t imgs = Message User t (Just imgs) Nothing Nothing Nothing
diff --git a/src/Ollama/Types/Model.hs b/src/Ollama/Types/Model.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Model.hs
@@ -0,0 +1,161 @@
+{- |
+Module      : Ollama.Types.Model
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model info, details, listing, and metadata types.
+
+@since 1.0.0.0
+-}
+module Ollama.Types.Model (
+  ModelDetails (..),
+  ModelInfo (..),
+  ListResponse (..),
+  RunningModel (..),
+  RunningModelsResponse (..),
+) where
+
+import Data.Aeson
+import Data.Int (Int64)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import GHC.Generics (Generic)
+import Ollama.Types.Common (Digest, ModelName)
+
+{- | Detailed specifications of a model's architecture and family.
+
+@since 1.0.0.0
+-}
+data ModelDetails = ModelDetails
+  { parentModel :: !(Maybe Text)
+  , format :: !Text
+  , family :: !Text
+  , families :: ![Text]
+  , parameterSize :: !Text
+  , quantizationLevel :: !Text
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON ModelDetails where
+  parseJSON = withObject "ModelDetails" $ \v ->
+    ModelDetails
+      <$> v .:? "parent_model"
+      <*> v .:? "format" .!= ""
+      <*> v .:? "family" .!= ""
+      <*> v .:? "families" .!= []
+      <*> v .:? "parameter_size" .!= ""
+      <*> v .:? "quantization_level" .!= ""
+
+instance ToJSON ModelDetails where
+  toJSON ModelDetails {..} =
+    object
+      [ "parent_model" .= parentModel
+      , "format" .= format
+      , "family" .= family
+      , "families" .= families
+      , "parameter_size" .= parameterSize
+      , "quantization_level" .= quantizationLevel
+      ]
+
+{- | Summary information for an installed local model.
+
+@since 1.0.0.0
+-}
+data ModelInfo = ModelInfo
+  { miName :: !ModelName
+  , miModel :: !ModelName
+  , miModifiedAt :: !UTCTime
+  , miSize :: !Int64
+  , miDigest :: !Digest
+  , miDetails :: !ModelDetails
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON ModelInfo where
+  parseJSON = withObject "ModelInfo" $ \v ->
+    ModelInfo
+      <$> v .: "name"
+      <*> v .: "model"
+      <*> v .: "modified_at"
+      <*> v .: "size"
+      <*> v .: "digest"
+      <*> v .: "details"
+
+instance ToJSON ModelInfo where
+  toJSON ModelInfo {..} =
+    object
+      [ "name" .= miName
+      , "model" .= miModel
+      , "modified_at" .= miModifiedAt
+      , "size" .= miSize
+      , "digest" .= miDigest
+      , "details" .= miDetails
+      ]
+
+{- | Response listing available local models.
+
+@since 1.0.0.0
+-}
+newtype ListResponse = ListResponse
+  { models :: [ModelInfo]
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+{- | Summary information for a model currently loaded in memory.
+
+@since 1.0.0.0
+-}
+data RunningModel = RunningModel
+  { rmName :: !ModelName
+  , rmModel :: !ModelName
+  , rmSize :: !Int64
+  , rmDigest :: !Digest
+  , rmDetails :: !ModelDetails
+  , rmExpiresAt :: !UTCTime
+  , rmSizeVram :: !Int64
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON RunningModel where
+  parseJSON = withObject "RunningModel" $ \v ->
+    RunningModel
+      <$> v .: "name"
+      <*> v .: "model"
+      <*> v .: "size"
+      <*> v .: "digest"
+      <*> v .: "details"
+      <*> v .: "expires_at"
+      <*> v .: "size_vram"
+
+instance ToJSON RunningModel where
+  toJSON RunningModel {..} =
+    object
+      [ "name" .= rmName
+      , "model" .= rmModel
+      , "size" .= rmSize
+      , "digest" .= rmDigest
+      , "details" .= rmDetails
+      , "expires_at" .= rmExpiresAt
+      , "size_vram" .= rmSizeVram
+      ]
+
+{- | Response listing loaded running models.
+
+@since 1.0.0.0
+-}
+newtype RunningModelsResponse = RunningModelsResponse
+  { runningModels :: [RunningModel]
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance FromJSON RunningModelsResponse where
+  parseJSON = withObject "RunningModelsResponse" $ \v ->
+    RunningModelsResponse <$> v .: "models"
+
+instance ToJSON RunningModelsResponse where
+  toJSON RunningModelsResponse {..} =
+    object ["models" .= runningModels]
diff --git a/src/Ollama/Types/Options.hs b/src/Ollama/Types/Options.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Options.hs
@@ -0,0 +1,135 @@
+{- |
+Module      : Ollama.Types.Options
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Model parameters and runtime execution options.
+
+@since 3.0.0.0
+-}
+module Ollama.Types.Options (
+  ModelOptions (..),
+  defaultOptions,
+) where
+
+import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:?), (.=))
+import Data.Maybe (catMaybes)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+{- | Optional inference and hardware tuning parameters.
+
+@since 3.0.0.0
+-}
+data ModelOptions = ModelOptions
+  { optNumKeep :: !(Maybe Int)
+  , optSeed :: !(Maybe Int)
+  , optNumPredict :: !(Maybe Int)
+  , optDraftNumPredict :: !(Maybe Int)
+  , optTopK :: !(Maybe Int)
+  , optTopP :: !(Maybe Double)
+  , optMinP :: !(Maybe Double)
+  , optTypicalP :: !(Maybe Double)
+  , optRepeatLastN :: !(Maybe Int)
+  , optTemperature :: !(Maybe Double)
+  , optRepeatPenalty :: !(Maybe Double)
+  , optPresencePenalty :: !(Maybe Double)
+  , optFrequencyPenalty :: !(Maybe Double)
+  , optPenalizeNewline :: !(Maybe Bool)
+  , optStop :: !(Maybe [Text])
+  , optNuma :: !(Maybe Bool)
+  , optNumCtx :: !(Maybe Int)
+  , optNumBatch :: !(Maybe Int)
+  , optNumGpu :: !(Maybe Int)
+  , optMainGpu :: !(Maybe Int)
+  , optUseMmap :: !(Maybe Bool)
+  , optNumThread :: !(Maybe Int)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON ModelOptions where
+  toJSON opts =
+    object $
+      catMaybes
+        [ ("num_keep" .=) <$> optNumKeep opts
+        , ("seed" .=) <$> optSeed opts
+        , ("num_predict" .=) <$> optNumPredict opts
+        , ("draft_num_predict" .=) <$> optDraftNumPredict opts
+        , ("top_k" .=) <$> optTopK opts
+        , ("top_p" .=) <$> optTopP opts
+        , ("min_p" .=) <$> optMinP opts
+        , ("typical_p" .=) <$> optTypicalP opts
+        , ("repeat_last_n" .=) <$> optRepeatLastN opts
+        , ("temperature" .=) <$> optTemperature opts
+        , ("repeat_penalty" .=) <$> optRepeatPenalty opts
+        , ("presence_penalty" .=) <$> optPresencePenalty opts
+        , ("frequency_penalty" .=) <$> optFrequencyPenalty opts
+        , ("penalize_newline" .=) <$> optPenalizeNewline opts
+        , ("stop" .=) <$> optStop opts
+        , ("numa" .=) <$> optNuma opts
+        , ("num_ctx" .=) <$> optNumCtx opts
+        , ("num_batch" .=) <$> optNumBatch opts
+        , ("num_gpu" .=) <$> optNumGpu opts
+        , ("main_gpu" .=) <$> optMainGpu opts
+        , ("use_mmap" .=) <$> optUseMmap opts
+        , ("num_thread" .=) <$> optNumThread opts
+        ]
+
+instance FromJSON ModelOptions where
+  parseJSON = withObject "ModelOptions" $ \v ->
+    ModelOptions
+      <$> v .:? "num_keep"
+      <*> v .:? "seed"
+      <*> v .:? "num_predict"
+      <*> v .:? "draft_num_predict"
+      <*> v .:? "top_k"
+      <*> v .:? "top_p"
+      <*> v .:? "min_p"
+      <*> v .:? "typical_p"
+      <*> v .:? "repeat_last_n"
+      <*> v .:? "temperature"
+      <*> v .:? "repeat_penalty"
+      <*> v .:? "presence_penalty"
+      <*> v .:? "frequency_penalty"
+      <*> v .:? "penalize_newline"
+      <*> v .:? "stop"
+      <*> v .:? "numa"
+      <*> v .:? "num_ctx"
+      <*> v .:? "num_batch"
+      <*> v .:? "num_gpu"
+      <*> v .:? "main_gpu"
+      <*> v .:? "use_mmap"
+      <*> v .:? "num_thread"
+
+{- | Default empty options (all settings default to server Modelfile values).
+
+@since 3.0.0.0
+-}
+defaultOptions :: ModelOptions
+defaultOptions =
+  ModelOptions
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
+    Nothing
diff --git a/src/Ollama/Types/Tool.hs b/src/Ollama/Types/Tool.hs
new file mode 100644
--- /dev/null
+++ b/src/Ollama/Types/Tool.hs
@@ -0,0 +1,148 @@
+{- |
+Module      : Ollama.Types.Tool
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Tool calling and structured function interfaces for the Ollama API.
+
+@since 3.0.0.0
+-}
+module Ollama.Types.Tool (
+  Tool (..),
+  FunctionDef (..),
+  FunctionParameters (..),
+  ToolCall (..),
+  ToolCallFunction (..),
+) where
+
+import Data.Aeson
+import Data.Map.Strict (Map)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+
+{- | Tool definition provided to the model.
+
+@since 3.0.0.0
+-}
+data Tool = Tool
+  { toolType :: !Text
+  , toolFunction :: !FunctionDef
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON Tool where
+  toJSON Tool {..} =
+    object
+      [ "type" .= toolType
+      , "function" .= toolFunction
+      ]
+
+instance FromJSON Tool where
+  parseJSON = withObject "Tool" $ \v ->
+    Tool
+      <$> v .: "type"
+      <*> v .: "function"
+
+{- | Definition of a function that can be called by the model.
+
+@since 3.0.0.0
+-}
+data FunctionDef = FunctionDef
+  { fnName :: !Text
+  , fnDescription :: !(Maybe Text)
+  , fnParameters :: !(Maybe FunctionParameters)
+  , fnStrict :: !(Maybe Bool)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON FunctionDef where
+  toJSON FunctionDef {..} =
+    object $
+      ["name" .= fnName]
+        ++ maybe [] (\d -> ["description" .= d]) fnDescription
+        ++ maybe [] (\p -> ["parameters" .= p]) fnParameters
+        ++ maybe [] (\s -> ["strict" .= s]) fnStrict
+
+instance FromJSON FunctionDef where
+  parseJSON = withObject "FunctionDef" $ \v ->
+    FunctionDef
+      <$> v .: "name"
+      <*> v .:? "description"
+      <*> v .:? "parameters"
+      <*> v .:? "strict"
+
+{- | Parameters schema for a function call.
+
+@since 3.0.0.0
+-}
+data FunctionParameters = FunctionParameters
+  { fpType :: !Text
+  , fpProperties :: !(Maybe (Map Text FunctionParameters))
+  , fpRequired :: !(Maybe [Text])
+  , fpAdditionalProperties :: !(Maybe Bool)
+  , fpDescription :: !(Maybe Text)
+  , fpEnum :: !(Maybe [Text])
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON FunctionParameters where
+  toJSON FunctionParameters {..} =
+    object $
+      ["type" .= fpType]
+        ++ maybe [] (\p -> ["properties" .= p]) fpProperties
+        ++ maybe [] (\r -> ["required" .= r]) fpRequired
+        ++ maybe [] (\a -> ["additionalProperties" .= a]) fpAdditionalProperties
+        ++ maybe [] (\d -> ["description" .= d]) fpDescription
+        ++ maybe [] (\e -> ["enum" .= e]) fpEnum
+
+instance FromJSON FunctionParameters where
+  parseJSON = withObject "FunctionParameters" $ \v ->
+    FunctionParameters
+      <$> v .: "type"
+      <*> v .:? "properties"
+      <*> v .:? "required"
+      <*> v .:? "additionalProperties"
+      <*> v .:? "description"
+      <*> v .:? "enum"
+
+{- | Tool call returned in model's assistant response.
+
+@since 3.0.0.0
+-}
+newtype ToolCall = ToolCall
+  { tcFunction :: ToolCallFunction
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON ToolCall where
+  toJSON ToolCall {..} = object ["function" .= tcFunction]
+
+instance FromJSON ToolCall where
+  parseJSON = withObject "ToolCall" $ \v ->
+    ToolCall <$> v .: "function"
+
+{- | Function invocation payload inside a tool call.
+
+@since 3.0.0.0
+-}
+data ToolCallFunction = ToolCallFunction
+  { tcfName :: !Text
+  , tcfArguments :: !(Map Text Value)
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToJSON ToolCallFunction where
+  toJSON ToolCallFunction {..} =
+    object
+      [ "name" .= tcfName
+      , "arguments" .= tcfArguments
+      ]
+
+instance FromJSON ToolCallFunction where
+  parseJSON = withObject "ToolCallFunction" $ \v ->
+    ToolCallFunction
+      <$> v .: "name"
+      <*> v .: "arguments"
diff --git a/test-integration/Main.hs b/test-integration/Main.hs
new file mode 100644
--- /dev/null
+++ b/test-integration/Main.hs
@@ -0,0 +1,176 @@
+module Main (main) where
+
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text qualified as T
+import Data.Time (getCurrentTime)
+import Ollama
+import Test.Tasty
+import Test.Tasty.HUnit
+
+testModel :: ModelName
+testModel = "qwen3.5:2b"
+
+fastOptions :: Maybe ModelOptions
+fastOptions = Just (defaultOptions {optNumPredict = Just 15})
+
+tests :: TestTree
+tests =
+  testGroup
+    "ollama-haskell Live Server End-to-End Test Suite"
+    [ testCase "GET /api/version — getVersion" $ do
+        client <- defaultClient
+        res <- getVersion client
+        case res of
+          Left err -> assertFailure $ "Version request failed: " <> show err
+          Right ver -> assertBool "Version non-empty" (not $ T.null $ unVersion ver)
+    , testCase "GET /api/tags — listModels" $ do
+        client <- defaultClient
+        res <- listModels client
+        case res of
+          Left err -> assertFailure $ "List models failed: " <> show err
+          Right (ListResponse ms) ->
+            assertBool "Has installed models" (not $ null ms)
+    , testCase "POST /api/show — showModel" $ do
+        client <- defaultClient
+        res <- showModel client testModel
+        case res of
+          Left err -> assertFailure $ "Show model failed: " <> show err
+          Right resp ->
+            assertBool "Modelfile or details present" (not $ T.null $ srsModelfile resp)
+    , testCase "GET /api/ps — listRunning" $ do
+        client <- defaultClient
+        res <- listRunning client
+        case res of
+          Left err -> assertFailure $ "List running failed: " <> show err
+          Right _ -> pure ()
+    , testCase "POST /api/chat — non-streaming chat" $ do
+        client <- defaultClient
+        let msgs = systemMessage "You are a helpful assistant." :| [userMessage "Say hello in one word."]
+            req = (chatRequest testModel msgs) {chatOptions = fastOptions, chatThink = Just ThinkDisabled}
+        res <- chat client req
+        case res of
+          Left err -> assertFailure $ "Chat request failed: " <> show err
+          Right resp -> do
+            assertBool "Chat response done" (crDone resp)
+            case crMessage resp of
+              Nothing -> assertFailure "Expected message in response"
+              Just msg ->
+                assertBool
+                  "Message content or thinking present"
+                  (not (T.null (messageContent msg)) || maybe False (not . T.null) (messageThinking msg))
+    , testCase "POST /api/chat — streaming chat with conduit" $ do
+        client <- defaultClient
+        let req = (chatRequest testModel (userMessage "Count from 1 to 5." :| [])) {chatThink = Just ThinkDisabled}
+        chunks <- collectStream (chatStream client req)
+        assertBool "Stream produced response chunks" (not $ null chunks)
+    , testCase "POST /api/generate — non-streaming generate" $ do
+        client <- defaultClient
+        let req =
+              (generateRequest testModel "Write 3 words.")
+                { genOptions = fastOptions
+                , genThink = Just ThinkDisabled
+                }
+        res <- generate client req
+        case res of
+          Left err -> assertFailure $ "Generate request failed: " <> show err
+          Right resp -> do
+            assertBool "Generate response done" (grDone resp)
+            assertBool "Generated response non-empty" (not $ T.null $ grResponse resp)
+    , testCase "POST /api/generate — streaming generate with conduit" $ do
+        client <- defaultClient
+        let req = (generateRequest testModel "Say hi.") {genThink = Just ThinkDisabled}
+        chunks <- collectStream (generateStream client req)
+        assertBool "Stream produced generate chunks" (not $ null chunks)
+    , testCase "POST /api/generate — thinking model support" $ do
+        client <- defaultClient
+        let req =
+              (generateRequest testModel "What is 2 + 2?")
+                { genThink = Just ThinkEnabled
+                , genOptions = fastOptions
+                }
+        res <- generate client req
+        case res of
+          Left err -> assertFailure $ "Thinking generate failed: " <> show err
+          Right resp -> assertBool "Response done" (grDone resp)
+    , testCase "POST /api/chat — tool calling definition and execution" $ do
+        client <- defaultClient
+        let weatherTool =
+              Tool
+                { toolType = "function"
+                , toolFunction =
+                    FunctionDef
+                      { fnName = "get_current_weather"
+                      , fnDescription = Just "Get current weather for a city"
+                      , fnParameters =
+                          Just
+                            FunctionParameters
+                              { fpType = "object"
+                              , fpProperties = Nothing
+                              , fpRequired = Just ["location"]
+                              , fpAdditionalProperties = Nothing
+                              , fpDescription = Nothing
+                              , fpEnum = Nothing
+                              }
+                      , fnStrict = Nothing
+                      }
+                }
+            req =
+              (chatRequest testModel (userMessage "What is the weather in Tokyo?" :| []))
+                { chatTools = Just [weatherTool]
+                , chatOptions = fastOptions
+                , chatThink = Just ThinkDisabled
+                }
+        res <- chat client req
+        case res of
+          Left err -> assertFailure $ "Tool chat request failed: " <> show err
+          Right resp -> assertBool "Chat response completed" (crDone resp)
+    , testCase "POST /api/chat — structured output format" $ do
+        client <- defaultClient
+        let req =
+              (chatRequest testModel (userMessage "Respond with JSON listing 2 colors" :| []))
+                { chatFormat = Just JsonFormat
+                , chatOptions = fastOptions
+                , chatThink = Just ThinkDisabled
+                }
+        res <- chat client req
+        case res of
+          Left err -> assertFailure $ "Structured chat failed: " <> show err
+          Right resp -> assertBool "Response done" (crDone resp)
+    , testCase "POST /api/embed — vector embeddings" $ do
+        client <- defaultClient
+        let req = embedRequest testModel ["Hello world", "Haskell LLM client"]
+        res <- embed client req
+        case res of
+          Left (ApiError 501 _) -> pure ()
+          Left err -> assertFailure $ "Embed request failed: " <> show err
+          Right resp ->
+            assertBool "Embeddings non-empty" (not $ null $ erEmbeddings resp)
+    , testCase "POST /api/copy & DELETE /api/delete — model lifecycle" $ do
+        client <- defaultClient
+        let copyTarget = "qwen3.5:2b-test-copy"
+        copyRes <- copyModel client testModel copyTarget
+        case copyRes of
+          Left err -> assertFailure $ "Copy model failed: " <> show err
+          Right () -> do
+            delRes <- deleteModel client copyTarget
+            case delRes of
+              Left err -> assertFailure $ "Delete model failed: " <> show err
+              Right () -> pure ()
+    , testCase "ConversationStore — InMemoryStore with real conversation" $ do
+        store <- initInMemoryStore
+        now <- getCurrentTime
+        let cid = "test-conv-1"
+            conv =
+              Conversation
+                cid
+                [systemMessage "You are a concise assistant.", userMessage "My favorite color is green."]
+                testModel
+                now
+                now
+        saveConversationInMemory store conv
+        mConv <- loadConversationInMemory store cid
+        assertEqual "Loaded saved conversation" (Just conv) mConv
+    ]
+
+main :: IO ()
+main = defaultMain tests
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,47 +1,32 @@
-{-# LANGUAGE OverloadedStrings #-}
-
 module Main (main) where
 
-import Data.Text (unpack)
-import Ollama (Version (..), getVersion)
-import Test.Ollama.Blob qualified as Blob
-import Test.Ollama.Chat qualified as Chat
-import Test.Ollama.Common qualified as Common
-import Test.Ollama.Copy qualified as Copy
-import Test.Ollama.Create qualified as Create
-import Test.Ollama.Delete qualified as Delete
-import Test.Ollama.Embedding qualified as Embeddings
-import Test.Ollama.Generate qualified as Generate
-import Test.Ollama.List qualified as List
-import Test.Ollama.Load qualified as Load
-import Test.Ollama.Show qualified as Show
+import Test.Ollama.Golden.Chat qualified as GoldenChat
+import Test.Ollama.Golden.Embed qualified as GoldenEmbed
+import Test.Ollama.Golden.Generate qualified as GoldenGenerate
+import Test.Ollama.Golden.Models qualified as GoldenModels
+import Test.Ollama.Property.Roundtrip qualified as PropertyRoundtrip
+import Test.Ollama.Unit.Config qualified as UnitConfig
+import Test.Ollama.Unit.Error qualified as UnitError
+import Test.Ollama.Unit.SchemaBuilder qualified as UnitSchemaBuilder
+import Test.Ollama.Unit.Testing qualified as UnitTesting
+import Test.Ollama.Unit.Types qualified as UnitTypes
 import Test.Tasty
 
 tests :: TestTree
 tests =
   testGroup
-    "Tests"
-    [ -- Core functionality tests
-      Generate.tests
-    , Chat.tests
-    , Embeddings.tests
-    , -- Model management tests
-      Show.tests
-    , List.tests
-    , Copy.tests
-    , Create.tests
-    , Delete.tests
-    , Load.tests
-    , -- Utility and blob tests
-      Blob.tests
-    , Common.tests
+    "ollama-haskell Pure Test Suite"
+    [ UnitTypes.tests
+    , UnitError.tests
+    , UnitConfig.tests
+    , UnitSchemaBuilder.tests
+    , UnitTesting.testingTests
+    , PropertyRoundtrip.tests
+    , GoldenChat.tests
+    , GoldenGenerate.tests
+    , GoldenEmbed.tests
+    , GoldenModels.tests
     ]
 
 main :: IO ()
-main = do
-  eRes <- getVersion
-  case eRes of
-    Left err -> print err
-    Right (Version r) -> do
-      putStrLn $ "Ollama client version: " <> unpack r
-      defaultMain tests
+main = defaultMain tests
diff --git a/test/Test/Ollama/Blob.hs b/test/Test/Ollama/Blob.hs
deleted file mode 100644
--- a/test/Test/Ollama/Blob.hs
+++ /dev/null
@@ -1,50 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Blob (tests) where
-
-import Data.Ollama.Blob
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testCheckBlobExistsInvalid :: TestTree
-testCheckBlobExistsInvalid = testCase "Check blob exists: invalid digest should fail" $ do
-  res <- checkBlobExists "invalid-digest" Nothing
-  case res of
-    Left _ -> assertBool "Should fail with invalid digest" True
-    Right _ -> assertBool "Or return False for invalid digest" True
-
-testCheckBlobExistsNonExistent :: TestTree
-testCheckBlobExistsNonExistent = testCase "Check blob exists: non-existent blob" $ do
-  let fakeDigest = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
-  res <- checkBlobExists fakeDigest Nothing
-  case res of
-    Left _ -> return () -- Network error is acceptable
-    Right exists -> assertBool "Non-existent blob should return False" (not exists)
-
-testCheckBlobExistsWithConfig :: TestTree
-testCheckBlobExistsWithConfig = testCase "Check blob exists: with custom config" $ do
-  let config = Just $ defaultOllamaConfig {timeout = 5}
-      fakeDigest = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
-  res <- checkBlobExists fakeDigest config
-  case res of
-    Left _ -> return () -- Network error is acceptable
-    Right _ -> assertBool "Should handle custom config" True
-
-testCreateBlobInvalidFile :: TestTree
-testCreateBlobInvalidFile = testCase "Create blob: invalid file path should fail" $ do
-  let fakeDigest = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
-  res <- createBlob "/nonexistent/file/path" fakeDigest Nothing
-  case res of
-    Left _ -> assertBool "Should fail with invalid file path" True
-    Right () -> assertFailure "Should not succeed with invalid file path"
-
-tests :: TestTree
-tests =
-  testGroup
-    "Blob tests"
-    [ testCheckBlobExistsInvalid
-    , testCheckBlobExistsNonExistent
-    , testCheckBlobExistsWithConfig
-    , testCreateBlobInvalidFile
-    ]
diff --git a/test/Test/Ollama/Chat.hs b/test/Test/Ollama/Chat.hs
deleted file mode 100644
--- a/test/Test/Ollama/Chat.hs
+++ /dev/null
@@ -1,307 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Chat (tests) where
-
-import Control.Monad (void)
-import Data.Aeson qualified as Aeson
-import Data.ByteString.Lazy.Char8 qualified as BSL
-import Data.IORef (modifyIORef, newIORef, readIORef, writeIORef)
-import Data.List.NonEmpty (fromList)
-import Data.List.NonEmpty qualified as NE
-import Data.Map qualified as HM
-import Data.Maybe (isJust)
-import Data.Ollama.Chat
-import Data.Scientific
-import Data.Text qualified as T
-import Data.Time (diffUTCTime, getCurrentTime)
-import Network.HTTP.Client
-import Network.HTTP.Client.TLS
-import Test.Tasty
-import Test.Tasty.HUnit
-
--- | Basic chat test with default options
-basicChatTest :: TestTree
-basicChatTest = testCase "Basic chat should contain 4 for 2+2" $ do
-  let ops = defaultChatOps
-  eRes <- chat ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> case message r of
-      Nothing -> assertFailure "Expected a message in response"
-      Just msg -> assertBool "Should contain '4'" (T.isInfixOf "4" (content msg))
-
--- | Test timeout configuration
-timeoutTest :: TestTree
-timeoutTest = testCase "Setting timeout" $ do
-  let config = Just $ defaultOllamaConfig {timeout = 1}
-  eRes <- chat defaultChatOps config
-  case eRes of
-    Right _ -> assertFailure "The model responded before timeout"
-    Left (TimeoutError _) -> pure ()
-    Left other -> assertFailure $ "Expected timeout error, got " ++ show other
-
--- | Test model lifecycle hooks on failure
-hooksFailTest :: TestTree
-hooksFailTest = testCase "Model lifecycle hooks should trigger on failure" $ do
-  refStart <- newIORef False
-  refError <- newIORef False
-  refFinish <- newIORef True
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- Guaranteed to fail
-          , onModelStart = Just $ writeIORef refStart True
-          , onModelError = Just $ writeIORef refError True
-          , onModelFinish = Just $ writeIORef refFinish False
-          }
-  void $ chat defaultChatOps (Just config)
-  wasStarted <- readIORef refStart
-  wasErrored <- readIORef refError
-  wasFinished <- readIORef refFinish
-  assertBool "onModelStart should be called" wasStarted
-  assertBool "onModelError should be called" wasErrored
-  assertBool "onModelFinish should be called" wasFinished
-
--- | Test model lifecycle hooks on success
-hooksSuccessTest :: TestTree
-hooksSuccessTest = testCase "Model lifecycle hooks should trigger on success" $ do
-  refStart <- newIORef False
-  refError <- newIORef True
-  refFinish <- newIORef False
-  let config =
-        defaultOllamaConfig
-          { onModelStart = Just $ writeIORef refStart True
-          , onModelError = Just $ writeIORef refError False
-          , onModelFinish = Just $ writeIORef refFinish True
-          }
-  void $ chat defaultChatOps (Just config)
-  wasStarted <- readIORef refStart
-  wasErrored <- readIORef refError
-  wasFinished <- readIORef refFinish
-  assertBool "onModelStart should be called" wasStarted
-  assertBool "onModelError should not be called" wasErrored
-  assertBool "onModelFinish should be called" wasFinished
-
--- | Test retry count
-retryCountTest :: TestTree
-retryCountTest = testCase "Should retry chat call retryCount times" $ do
-  counter <- newIORef (0 :: Int)
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- Fails
-          , retryCount = Just 2
-          , retryDelay = Just 1
-          , onModelStart = Just $ modifyIORef counter (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-  _ <- chat defaultChatOps (Just config)
-  calls <- readIORef counter
-  assertEqual "Expected 3 attempts (1 initial + 2 retries)" 3 calls
-
--- | Test retry delay
-retryDelayTest :: TestTree
-retryDelayTest = testCase "Should delay between retries" $ do
-  counter <- newIORef (0 :: Int)
-  let delaySecs = 2
-  start <- getCurrentTime
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- Fails
-          , retryCount = Just 1
-          , retryDelay = Just delaySecs
-          , onModelStart = Just $ modifyIORef counter (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-  _ <- chat defaultChatOps (Just config)
-  end <- getCurrentTime
-  let elapsed = realToFrac (diffUTCTime end start) :: Double
-      expectedMin = fromIntegral delaySecs
-  assertBool
-    ("Elapsed time should be at least " ++ show expectedMin ++ "s, but was " ++ show elapsed)
-    (elapsed >= expectedMin)
-
--- | Test common manager usage
-commonManagerTest :: TestTree
-commonManagerTest = testCase "Should reuse provided commonManager" $ do
-  refStart <- newIORef (0 :: Int)
-  mgr <-
-    newTlsManagerWith
-      tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro 1000000}
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- Will fail fast
-          , commonManager = Just mgr
-          , timeout = 999 -- Shouldn’t matter, manager timeout takes precedence
-          , onModelStart = Just $ modifyIORef refStart (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-  _ <- chat defaultChatOps (Just config)
-  _ <- chat defaultChatOps (Just config)
-  startCount <- readIORef refStart
-  assertEqual "Both requests should start (reuse manager)" 2 startCount
-
--- | Test JSON format response
-jsonFormatTest :: TestTree
-jsonFormatTest = testCase "Should return response in JSON format" $ do
-  let ops =
-        defaultChatOps
-          { messages =
-              fromList
-                [userMessage "Return a JSON with keys 'name' and 'age' for John, 25 years old."]
-          , format = Just JsonFormat
-          }
-  eRes <- chat ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> case message r of
-      Nothing -> assertFailure "Expected a message in response"
-      Just msg -> do
-        let responseText = content msg
-        let decoded = Aeson.decode (BSL.pack $ T.unpack responseText) :: Maybe Aeson.Value
-        assertBool "Expected valid JSON object in response" (isJust decoded)
-
--- | Test streaming response
-streamingTest :: TestTree
-streamingTest = testCase "Should handle streaming response" $ do
-  chunksRef <- newIORef []
-  let streamHandler chunk = modifyIORef chunksRef (++ [message chunk])
-      ops = defaultChatOps {stream = Just (streamHandler, pure ())}
-  eRes <- chat ops Nothing
-  chunks <- readIORef chunksRef
-  let fullOutput = T.concat (map (maybe "" content) chunks)
-  case eRes of
-    Left err -> assertFailure $ "Expected streaming success, got error: " ++ show err
-    Right _ -> assertBool "Expected some streamed content" (not $ T.null fullOutput)
-
--- | Test custom model options
-modelOptionsTest :: TestTree
-modelOptionsTest = testCase "Should use custom model options" $ do
-  let opts =
-        Just $
-          defaultModelOptions
-            { temperature = Just 0.9
-            , topP = Just 0.8
-            , topK = Nothing
-            , numPredict = Just 20
-            }
-      ops = defaultChatOps {options = opts}
-  eRes <- chat ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> assertBool "Expected a response message" (isJust (message r))
-
-testToolCallAddTwoNumbers :: TestTree
-testToolCallAddTwoNumbers = testCase "Tool call: addTwoNumbers(23, 46)" $ do
-  let messageList = NE.singleton $ userMessage "What is 23 + 46? (Use tool)"
-      paramProps =
-        HM.fromList
-          [ ("a", FunctionParameters "number" Nothing Nothing Nothing)
-          , ("b", FunctionParameters "number" Nothing Nothing Nothing)
-          ]
-      functionParams =
-        FunctionParameters
-          { parameterType = "object"
-          , requiredParams = Just ["a", "b"]
-          , parameterProperties = Just paramProps
-          , additionalProperties = Just False
-          }
-      functionDef =
-        FunctionDef
-          { functionName = "addTwoNumbers"
-          , functionDescription = Just "Add two numbers"
-          , functionParameters = Just functionParams
-          , functionStrict = Nothing
-          }
-      tool =
-        InputTool
-          { toolType = "function"
-          , function = functionDef
-          }
-      ops =
-        defaultChatOps
-          { modelName = "qwen3:0.6b"
-          , messages = messageList
-          , tools = Just [tool]
-          }
-
-  res <- chat ops Nothing
-  case res of
-    Left err -> assertFailure $ "Chat failed: " ++ show err
-    Right ChatResponse {message = Nothing} -> assertFailure "No message in response"
-    Right ChatResponse {message = Just msg} ->
-      case tool_calls msg of
-        Nothing -> assertFailure "No tool calls received"
-        Just [toolCall] -> do
-          result <- captureAddToolCall toolCall
-          assertEqual "Expected result of 23 + 46" 69 result
-        Just other -> assertFailure $ "Unexpected number of tool calls: " ++ show other
-
--- Helper to evaluate the tool call
-captureAddToolCall :: ToolCall -> IO Int
-captureAddToolCall (ToolCall func)
-  | outputFunctionName func == "addTwoNumbers" =
-      case ( HM.lookup "a" (arguments func) >>= convertToNumber
-           , HM.lookup "b" (arguments func) >>= convertToNumber
-           ) of
-        (Just a, Just b) -> return $ addTwoNumbers a b
-        _ -> assertFailure "Missing parameters a or b" >> return 0
-  | otherwise = assertFailure "Unexpected function name" >> return 0
-
-addTwoNumbers :: Int -> Int -> Int
-addTwoNumbers = (+)
-
--- Convert Aeson value to Int
-convertToNumber :: Aeson.Value -> Maybe Int
-convertToNumber (Aeson.Number n) = toBoundedInteger n
-convertToNumber _ = Nothing
-
--- | Test conversation with multiple messages
-multiMessageConversationTest :: TestTree
-multiMessageConversationTest = testCase "Multi-message conversation should work" $ do
-  let msgs =
-        fromList
-          [ systemMessage "You are a helpful assistant."
-          , userMessage "What is 2+2?"
-          , assistantMessage "2+2 equals 4."
-          , userMessage "What about 3+3?"
-          ]
-      ops = defaultChatOps {messages = msgs}
-  eRes <- chat ops Nothing
-  case eRes of
-    Left _ -> assertFailure "Expected success, got error"
-    Right r -> case message r of
-      Nothing -> assertFailure "Expected a message in response"
-      Just msg -> assertBool "Should contain '6'" (T.isInfixOf "6" (content msg))
-
--- | Test invalid model name
-invalidModelChatTest :: TestTree
-invalidModelChatTest = testCase "Invalid model name should fail" $ do
-  let ops = defaultChatOps {modelName = "nonexistent-chat-model"}
-  eRes <- chat ops Nothing
-  case eRes of
-    Left _ -> assertBool "Should fail with invalid model" True
-    Right _ -> assertFailure "Should not succeed with invalid model"
-
--- | Group all tests
-tests :: TestTree
-tests =
-  sequentialTestGroup
-    "Chat tests"
-    AllFinish
-    [ basicChatTest
-    , timeoutTest
-    , hooksFailTest
-    , hooksSuccessTest
-    , retryCountTest
-    , retryDelayTest
-    , commonManagerTest
-    , jsonFormatTest
-    , streamingTest
-    , modelOptionsTest
-    , testToolCallAddTwoNumbers
-    , multiMessageConversationTest
-    , invalidModelChatTest
-    ]
diff --git a/test/Test/Ollama/Common.hs b/test/Test/Ollama/Common.hs
deleted file mode 100644
--- a/test/Test/Ollama/Common.hs
+++ /dev/null
@@ -1,91 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Common (tests) where
-
-import Data.Ollama.Common.SchemaBuilder
-import Data.Ollama.Common.Utils (encodeImage)
-import Data.Text qualified as T
-import Test.Tasty
-import Test.Tasty.HUnit
-
--- Test SchemaBuilder functionality
-testSchemaBuilderBasic :: TestTree
-testSchemaBuilderBasic = testCase "SchemaBuilder: basic object construction" $ do
-  let schema =
-        buildSchema $
-          emptyObject
-            |+ ("name", JString)
-            |+ ("age", JNumber)
-            |! "name"
-            |! "age"
-
-  -- Check that schema is not null/empty
-  let schemaText = T.pack $ show schema
-  assertBool "Schema should not be empty" (not $ T.null schemaText)
-  assertBool "Schema should contain 'name'" ("name" `T.isInfixOf` schemaText)
-  assertBool "Schema should contain 'age'" ("age" `T.isInfixOf` schemaText)
-
-testSchemaBuilderArray :: TestTree
-testSchemaBuilderArray = testCase "SchemaBuilder: array types" $ do
-  let arraySchema =
-        buildSchema $
-          emptyObject
-            |+ ("items", JArray JString)
-            |+ ("numbers", JArray JNumber)
-            |+ ("flags", JArray JBoolean)
-            |! "items"
-
-  let schemaText = T.pack $ show arraySchema
-  assertBool "Array schema should not be empty" (not $ T.null schemaText)
-  assertBool "Should contain items field" ("items" `T.isInfixOf` schemaText)
-
-testSchemaBuilderOptionalFields :: TestTree
-testSchemaBuilderOptionalFields = testCase "SchemaBuilder: optional vs required fields" $ do
-  let schema =
-        buildSchema $
-          emptyObject
-            |+ ("required_field", JString)
-            |+ ("optional_field", JString)
-            |! "required_field"
-
-  let schemaText = T.pack $ show schema
-  assertBool "Schema should contain both fields" $
-    "required_field" `T.isInfixOf` schemaText
-      && "optional_field" `T.isInfixOf` schemaText
-
--- Test image encoding utility
-testEncodeImageValidFile :: TestTree
-testEncodeImageValidFile = testCase "EncodeImage: should handle existing image file" $ do
-  -- Test with the sample image from the examples
-  result <- encodeImage "./examples/sample.png"
-  case result of
-    Nothing -> return () -- File might not exist in test environment
-    Just encoded -> do
-      assertBool "Encoded image should not be empty" (not $ T.null encoded)
-      assertBool "Should be base64-like content" (T.length encoded > 10)
-
-testEncodeImageNonExistentFile :: TestTree
-testEncodeImageNonExistentFile = testCase "EncodeImage: should return Nothing for non-existent file" $ do
-  result <- encodeImage "./nonexistent/file.png"
-  case result of
-    Nothing -> assertBool "Should return Nothing for non-existent file" True
-    Just _ -> assertFailure "Should not encode non-existent file"
-
-testEncodeImageInvalidFormat :: TestTree
-testEncodeImageInvalidFormat = testCase "EncodeImage: should handle invalid file format" $ do
-  result <- encodeImage "./test/Main.hs" -- Text file, not an image
-  case result of
-    Nothing -> assertBool "Should return Nothing for invalid format" True
-    Just _ -> assertFailure "Should not encode invalid format"
-
-tests :: TestTree
-tests =
-  testGroup
-    "Common utilities tests"
-    [ testSchemaBuilderBasic
-    , testSchemaBuilderArray
-    , testSchemaBuilderOptionalFields
-    , testEncodeImageValidFile
-    , testEncodeImageNonExistentFile
-    , testEncodeImageInvalidFormat
-    ]
diff --git a/test/Test/Ollama/Copy.hs b/test/Test/Ollama/Copy.hs
deleted file mode 100644
--- a/test/Test/Ollama/Copy.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Copy (tests) where
-
-import Control.Monad (void)
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Data.Ollama.Copy
-import Data.Ollama.Delete (deleteModel)
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testCopyModelBasic :: TestTree
-testCopyModelBasic = testCase "Copy model: basic functionality" $ do
-  res <- copyModel "gemma3" "gemma3-test-copy" Nothing
-  case res of
-    Left _ -> return () -- Allow failure if source model doesn't exist
-    Right () -> do
-      assertBool "Copy should succeed" True
-      void $ deleteModel "gemma3-test-copy" Nothing
-
-testCopyModelWithConfig :: TestTree
-testCopyModelWithConfig = testCase "Copy model: with custom config" $ do
-  let config = Just $ defaultOllamaConfig {timeout = 30}
-  res <- copyModel "gemma3" "gemma3-test-copy-config" config
-  case res of
-    Left _ -> return () -- Allow failure if source doesn't exist
-    Right () -> do
-      assertBool "Copy with config should work" True
-      void $ deleteModel "gemma3-test-copy-config" Nothing
-
-testCopyModelInvalidSource :: TestTree
-testCopyModelInvalidSource = testCase "Copy model: invalid source should fail" $ do
-  res <- copyModel "nonexistent-model-12345" "some-destination" Nothing
-  case res of
-    Left _ -> assertBool "Should fail with invalid source" True
-    Right () -> assertFailure "Should not succeed with invalid source"
-
-tests :: TestTree
-tests =
-  testGroup
-    "Copy tests"
-    [ testCopyModelBasic
-    , testCopyModelWithConfig
-    , testCopyModelInvalidSource
-    ]
diff --git a/test/Test/Ollama/Create.hs b/test/Test/Ollama/Create.hs
deleted file mode 100644
--- a/test/Test/Ollama/Create.hs
+++ /dev/null
@@ -1,79 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Create (tests) where
-
-import Control.Monad (void)
-import Data.Maybe (isNothing)
-import Data.Ollama.Common.Types (ModelOptions (..))
-import Data.Ollama.Common.Utils (defaultModelOptions)
-import Data.Ollama.Create
-import Data.Ollama.Delete (deleteModel)
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testCreateModelBasic :: TestTree
-testCreateModelBasic = testCase "Create model: basic from existing model" $ do
-  let ops =
-        (defaultCreateOps "test-model-basic")
-          { fromModel = Just "gemma3"
-          , systemPrompt = Just "You are a helpful assistant."
-          }
-  createModel ops Nothing
-  assertBool "Create should complete without error" True
-  void $ deleteModel "test-model-basic" Nothing
-
-testCreateModelWithQuantization :: TestTree
-testCreateModelWithQuantization = testCase "Create model: with quantization" $ do
-  let ops =
-        (defaultCreateOps "test-model-quantized")
-          { fromModel = Just "gemma3"
-          , quantizeType = Just Q4_K_M
-          }
-  createModel ops Nothing
-  assertBool "Quantization request should complete" True
-  void $ deleteModel "test-model-quantized" Nothing
-
-testCreateModelWithParameters :: TestTree
-testCreateModelWithParameters = testCase "Create model: with custom parameters" $ do
-  let modelOpts =
-        defaultModelOptions
-          { temperature = Just 0.7
-          , topP = Just 0.9
-          , topK = Just 40
-          }
-      ops =
-        (defaultCreateOps "test-model-params")
-          { fromModel = Just "gemma3"
-          , parameters = Just modelOpts
-          , template = Just "Custom template: {{.Prompt}}"
-          }
-  createModel ops Nothing
-  assertBool "Custom parameters should be handled" True
-  void $ deleteModel "test-model-params" Nothing
-
-testQuantizationTypeValues :: TestTree
-testQuantizationTypeValues = testCase "Quantization types: should have valid values" $ do
-  let q1 = Q4_K_M
-      q2 = Q4_K_S
-      q3 = Q8_0
-  assertBool "Q4_K_M should be valid" (show q1 == "Q4_K_M")
-  assertBool "Q4_K_S should be valid" (show q2 == "Q4_K_S")
-  assertBool "Q8_0 should be valid" (show q3 == "Q8_0")
-
-testCreateModelFieldAccess :: TestTree
-testCreateModelFieldAccess = testCase "CreateOps: field access should work" $ do
-  let ops = defaultCreateOps "test-model"
-  assertBool "modelName should be accessible" (modelName ops == "test-model")
-  assertBool "fromModel should be Nothing by default" (isNothing (fromModel ops))
-  assertBool "files should be Nothing by default" (isNothing (files ops))
-
-tests :: TestTree
-tests =
-  testGroup
-    "Create tests"
-    [ testCreateModelBasic
-    , testCreateModelWithQuantization
-    , testCreateModelWithParameters
-    , testQuantizationTypeValues
-    , testCreateModelFieldAccess
-    ]
diff --git a/test/Test/Ollama/Delete.hs b/test/Test/Ollama/Delete.hs
deleted file mode 100644
--- a/test/Test/Ollama/Delete.hs
+++ /dev/null
@@ -1,42 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Delete (tests) where
-
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Data.Ollama.Delete
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testDeleteModelNonExistent :: TestTree
-testDeleteModelNonExistent = testCase "Delete non-existent model should fail gracefully" $ do
-  res <- deleteModel "nonexistent-model-12345" Nothing
-  case res of
-    Left _ -> assertBool "Should fail when deleting non-existent model" True
-    Right () -> assertBool "Or succeed if server handles gracefully" True
-
-testDeleteModelWithConfig :: TestTree
-testDeleteModelWithConfig = testCase "Delete model: with custom config" $ do
-  let config = Just $ defaultOllamaConfig {timeout = 30}
-  res <- deleteModel "nonexistent-test-model" config
-  case res of
-    Left _ -> assertBool "Should handle custom config" True
-    Right () -> assertBool "Or succeed if server handles gracefully" True
-
-testDeleteModelEmptyName :: TestTree
-testDeleteModelEmptyName = testCase "Delete model: empty name should fail" $ do
-  res <- deleteModel "" Nothing
-  case res of
-    Left _ -> assertBool "Should fail with empty name" True
-    Right () -> assertFailure "Should not succeed with empty name"
-
--- Note: We avoid testing actual deletion of existing models in unit tests
--- as it would be destructive. Integration tests could cover this.
-
-tests :: TestTree
-tests =
-  testGroup
-    "Delete tests"
-    [ testDeleteModelNonExistent
-    , testDeleteModelWithConfig
-    , testDeleteModelEmptyName
-    ]
diff --git a/test/Test/Ollama/Embedding.hs b/test/Test/Ollama/Embedding.hs
deleted file mode 100644
--- a/test/Test/Ollama/Embedding.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Embedding (tests) where
-
-import Data.Ollama.Embeddings
-import Test.Tasty
-import Test.Tasty.HUnit
-
--- Qwen doesn't support embeddings yet
--- TODO: Add embeddings supporting model.
-
--- testEmbeddingBasic :: TestTree
--- testEmbeddingBasic = testCase "Basic embedding with qwen3" $ do
---   res <- embedding "qwen3:0.6b" ["The sky is blue.", "Cats are independent."]
---   case res of
---     Left err -> assertFailure $ "Expected success, got error: " ++ show err
---     Right EmbeddingResp {..} -> do
---       assertEqual "Should return two embeddings" 2 (length respondedEmbeddings)
---       assertBool "Embeddings should not be empty" (not (any null respondedEmbeddings))
-
--- testEmbeddingWithOptions :: TestTree
--- testEmbeddingWithOptions = testCase "Embedding with truncate and keepAlive" $ do
---   let opts = defaultModelOptions {numKeep = Just 5, seed = Just 42}
---   res <-
---     embeddingOps
---       "qwen3:0.6b"
---       ["Hello world"]
---       (Just True)
---       (Just 30)
---       (Just opts)
---       Nothing
---       Nothing
---   case res of
---     Left err -> assertFailure $ "Unexpected error: " ++ show err
---     Right EmbeddingResp {..} -> do
---       assertEqual "Should return one embedding" 1 (length respondedEmbeddings)
---       assertBool
---         "Embedding vector should not be empty"
---         (not . null $ listToMaybe respondedEmbeddings)
-
-testEmbeddingInvalidModel :: TestTree
-testEmbeddingInvalidModel = testCase "Embedding with invalid model name" $ do
-  res <- embedding "nonexistent-model" ["This should fail."]
-  case res of
-    Left _ -> return () -- Expected failure
-    Right _ -> assertFailure "Expected failure with invalid model name"
-
-tests :: TestTree
-tests =
-  testGroup
-    "Embeddings tests"
-    [ testEmbeddingInvalidModel
-    ]
diff --git a/test/Test/Ollama/Generate.hs b/test/Test/Ollama/Generate.hs
deleted file mode 100644
--- a/test/Test/Ollama/Generate.hs
+++ /dev/null
@@ -1,360 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-{-
-  Tests related to Generate module
--}
-module Test.Ollama.Generate (tests) where
-
-import Control.Monad (void)
-import Data.Aeson qualified as Aeson
-import Data.ByteString.Lazy.Char8 qualified as BSL
-import Data.IORef (modifyIORef, newIORef, readIORef, writeIORef)
-import Data.Maybe (isJust)
-import Data.Ollama.Common.SchemaBuilder
-import Data.Ollama.Common.Utils (encodeImage)
-import Data.Ollama.Generate
-import Data.Text qualified as T
-import Data.Time (diffUTCTime, getCurrentTime)
-import Network.HTTP.Client
-import Network.HTTP.Client.TLS
-import Test.Tasty
-import Test.Tasty.HUnit
-
-generateTests :: TestTree
-generateTests =
-  testGroup
-    "Generation with various options"
-    [ testCase "Should contain 4 in 2+2" $ do
-        eRes <-
-          generate
-            defaultGenerateOps {modelName = "gemma3", prompt = "What is 2+2?"}
-            Nothing
-        case eRes of
-          Left err -> assertFailure $ "Expected success, got error: " ++ show err
-          Right r -> assertBool "Should contain 4" (T.isInfixOf "4" (genResponse r))
-    , testCase "Setting timeout" $ do
-        eRes <-
-          generate
-            defaultGenerateOps
-              { modelName = "gemma3"
-              , prompt = "Write a poem about French revolution"
-              }
-            (Just $ defaultOllamaConfig {timeout = 1})
-        case eRes of
-          Left (TimeoutError _) -> pure ()
-          _ -> assertFailure "Expected timeout error"
-    ]
-
-testOnModelHooksFail :: TestTree
-testOnModelHooksFail = testCase "Model lifecycle hooks should be triggered" $ do
-  refStart <- newIORef False
-  refError <- newIORef False
-  refFinish <- newIORef True
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- guaranteed to fail
-          , onModelStart = Just $ writeIORef refStart True
-          , onModelError = Just $ writeIORef refError True
-          , onModelFinish = Just $ writeIORef refFinish True
-          }
-
-  void $
-    generate
-      defaultGenerateOps {modelName = "gemma3", prompt = "what is 23+41?"}
-      (Just config)
-
-  wasStarted <- readIORef refStart
-  wasErrored <- readIORef refError
-  wasFinished <- readIORef refFinish
-
-  assertBool "onModelStart should be called" wasStarted
-  assertBool "onModelError should be called" wasErrored
-  assertBool "onModelFinish should be called" wasFinished
-
-testOnModelHooksSucc :: TestTree
-testOnModelHooksSucc = testCase "Model lifecycle hooks should be triggered 2" $ do
-  refStart <- newIORef False
-  refError <- newIORef True
-  refFinish <- newIORef False
-  let config =
-        defaultOllamaConfig
-          { onModelStart = Just $ writeIORef refStart True
-          , onModelError = Just $ writeIORef refError False
-          , onModelFinish = Just $ writeIORef refFinish True
-          }
-
-  void $
-    generate
-      defaultGenerateOps {modelName = "gemma3", prompt = "what is 23+41?"}
-      (Just config)
-
-  wasStarted <- readIORef refStart
-  wasErrored <- readIORef refError
-  wasFinished <- readIORef refFinish
-
-  assertBool "onModelStart should be called" wasStarted
-  assertBool "onModelError should be called" wasErrored
-  assertBool "onModelFinish should be called" wasFinished
-
-testRetryCount :: TestTree
-testRetryCount = testCase "Should retry generate call retryCount times" $ do
-  counter <- newIORef (0 :: Int)
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- fails
-          , retryCount = Just 2
-          , retryDelay = Just 1
-          , onModelStart = Just $ modifyIORef counter (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-
-  _ <- generate defaultGenerateOps {prompt = "Retry test"} (Just config)
-  calls <- readIORef counter
-  -- Should be retryCount + 1 (initial + retries)
-  assertEqual "Expected 3 attempts (1 initial + 2 retries)" 3 calls
-
-testRetryDelay :: TestTree
-testRetryDelay = testCase "Should delay between retries" $ do
-  counter <- newIORef (0 :: Int)
-  let delaySecs = 2
-  start <- getCurrentTime
-
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- fails
-          , retryCount = Just 1
-          , retryDelay = Just delaySecs
-          , onModelStart = Just $ modifyIORef counter (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-
-  _ <- generate defaultGenerateOps {prompt = "Retry delay test"} (Just config)
-  end <- getCurrentTime
-  let elapsed = realToFrac (diffUTCTime end start) :: Double
-  let expectedMin = fromIntegral delaySecs
-
-  assertBool
-    ("Elapsed time should be at least " ++ show expectedMin ++ "s, but was " ++ show elapsed)
-    (elapsed >= expectedMin)
-
-testCommonManagerUsage :: TestTree
-testCommonManagerUsage = testCase "Should reuse provided commonManager" $ do
-  refStart <- newIORef (0 :: Int)
-  mgr <-
-    newTlsManagerWith tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro 1000000}
-  let config =
-        defaultOllamaConfig
-          { hostUrl = "http://localhost:12345" -- will fail fast
-          , commonManager = Just mgr
-          , timeout = 999 -- shouldn't matter, manager timeout will be used
-          , onModelStart = Just $ modifyIORef refStart (+ 1)
-          , onModelError = Just $ pure ()
-          , onModelFinish = Just $ pure ()
-          }
-
-  _ <- generate defaultGenerateOps {prompt = "1"} (Just config)
-  _ <- generate defaultGenerateOps {prompt = "2"} (Just config)
-  startCount <- readIORef refStart
-  assertEqual "Both requests should start (reuse manager)" 2 startCount
-
-{-
- Suffix is not supported for few gemma3 and qwen3.
-
-testSuffixOption :: TestTree
-testSuffixOption = testCase "Should respect suffix in generation" $ do
-  let ops = defaultGenerateOps
-              { modelName = "qwen3:0.6b"
-              , prompt = "Complete this sentence: The Eiffel Tower is in"
-              , suffix = Just " [End]"
-              }
-  eRes <- generate ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> assertBool "Expected suffix in response" $
-                  T.isSuffixOf "[End]" (genResponse r)
-                  -}
-
-testThinkOption :: TestTree
-testThinkOption = testCase "Should activate thinking mode when think=True" $ do
-  let ops =
-        defaultGenerateOps
-          { modelName = "qwen3:0.6b"
-          , prompt = "What is 2+2?"
-          , think = Just True
-          }
-  eRes <- generate ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right _ -> pure () -- TODO: Need to find a way to know if model is thinking
-
-testFormatJsonFormat :: TestTree
-testFormatJsonFormat = testCase "Should return response in JsonFormat" $ do
-  let ops =
-        defaultGenerateOps
-          { modelName = "gemma3"
-          , prompt =
-              "John was 23 year old in 2023, this is year 2025."
-                <> "How old is John assuming he celebrated this year's birthday; "
-                <> "Return an object with keys 'name' and 'age'."
-          , format = Just JsonFormat
-          }
-  eRes <- generate ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> do
-      let responseText = genResponse r
-      let decoded =
-            Aeson.decode (BSL.pack $ T.unpack responseText) ::
-              Maybe Aeson.Value
-      assertBool "Expected valid JSON object in response" (isJust decoded)
-
-testFormatSchemaFormat :: TestTree
-testFormatSchemaFormat = testCase "Should include SchemaFormat in the request" $ do
-  let schema =
-        buildSchema $
-          emptyObject
-            |+ ("fruit", JString)
-            |+ ("quantity", JNumber)
-            |! "fruit"
-            |! "quantity"
-
-      ops =
-        defaultGenerateOps
-          { modelName = "gemma3"
-          , prompt = "I had 3 apples, 1 gave one away. How many left?"
-          , format = Just (SchemaFormat schema)
-          }
-
-  eRes <- generate ops Nothing
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right r -> do
-      let response = T.toLower (genResponse r)
-      assertBool "Expected fruit information in response" $
-        "apple" `T.isInfixOf` response || "fruit" `T.isInfixOf` response
-
-testImageInput :: TestTree
-testImageInput = testCase "Should accept and process base64 image input" $ do
-  maybeImg <- encodeImage "./examples/sample.png"
-  case maybeImg of
-    Nothing -> assertFailure "Image encoding failed (unsupported format or missing file)"
-    Just imgData -> do
-      -- Validate the encoded image data
-      assertBool "Encoded image should not be empty" (not $ T.null imgData)
-      assertBool "Encoded image should be reasonable length" (T.length imgData > 100)
-
-      let ops =
-            defaultGenerateOps
-              { modelName = "gemma3"
-              , prompt = "Describe this image."
-              , images = Just [imgData]
-              }
-          cfg = Just defaultOllamaConfig {timeout = 300}
-
-      eRes <- generate ops cfg
-      case eRes of
-        Left err -> assertFailure $ "Expected success, got error: " ++ show err
-        Right r -> do
-          let response = T.toLower (genResponse r)
-          assertBool "Response should not be empty" (not $ T.null response)
-          assertBool "Expected image-related description in response" $
-            T.isInfixOf "i love haskell" response
-
-testStreamingHandler :: TestTree
-testStreamingHandler = testCase "Should handle streaming response" $ do
-  -- IORef to collect streamed chunks
-  chunksRef <- newIORef []
-  -- Define the stream handler: accumulate responses
-  let streamHandler chunk = modifyIORef chunksRef (++ [genResponse chunk])
-      ops =
-        defaultGenerateOps
-          { modelName = "gemma3"
-          , prompt = "Write few words about Haskell."
-          , stream = Just (streamHandler, pure ())
-          }
-  eRes <- generate ops Nothing
-  -- Collect streamed chunks from IORef
-  chunks <- readIORef chunksRef
-  let fullOutput = T.concat chunks
-  case eRes of
-    Left err -> assertFailure $ "Expected streaming success, got error: " ++ show err
-    Right _ -> do
-      assertBool "Expected streamed text to include 'haskell'" $
-        "haskell" `T.isInfixOf` T.toLower fullOutput
-      assertBool "Expected some streamed content" $ not (T.null fullOutput)
-
-testModelOptionsBasic :: TestTree
-testModelOptionsBasic = testCase "ModelOptions: temperature and topP" $ do
-  let opts =
-        Just $
-          defaultModelOptions
-            { temperature = Just 0.9
-            , topP = Just 0.8
-            , topK = Nothing
-            , numPredict = Just 20
-            }
-
-  eRes <-
-    generate
-      defaultGenerateOps
-        { modelName = "gemma3"
-        , prompt = "Generate a random list of 3 animals"
-        , options = opts
-        }
-      Nothing
-
-  case eRes of
-    Left err -> assertFailure $ "Expected success, got: " ++ show err
-    Right r -> assertBool "Response should not be empty" (not . T.null $ genResponse r)
-
-testModelOptionsEdgeCases :: TestTree
-testModelOptionsEdgeCases = testCase "ModelOptions: edge case values" $ do
-  let opts =
-        Just $
-          defaultModelOptions
-            { temperature = Just 0.0 -- Minimum temperature
-            , topP = Just 1.0 -- Maximum topP
-            , topK = Just 1 -- Minimum topK
-            , numPredict = Just 1 -- Minimum prediction
-            }
-  eRes <-
-    generate
-      defaultGenerateOps
-        { modelName = "gemma3"
-        , prompt = "Hi"
-        , options = opts
-        }
-      Nothing
-  case eRes of
-    Left _ -> assertFailure "Expected success, got error"
-    Right _ -> assertBool "Should handle edge case model options" True
-
-testInvalidModelName :: TestTree
-testInvalidModelName = testCase "Invalid model name should fail gracefully" $ do
-  eRes <- generate defaultGenerateOps {modelName = "invalid-model-xyz"} Nothing
-  case eRes of
-    Left _ -> assertBool "Should fail with invalid model name" True
-    Right _ -> assertFailure "Should not succeed with invalid model name"
-
-tests :: TestTree
-tests =
-  sequentialTestGroup
-    "Generate tests"
-    AllFinish
-    [ generateTests
-    , testOnModelHooksFail
-    , testOnModelHooksSucc
-    , testRetryCount
-    , testRetryDelay
-    , testCommonManagerUsage
-    , testThinkOption
-    , testFormatJsonFormat
-    , testFormatSchemaFormat
-    , testImageInput
-    , testStreamingHandler
-    , testModelOptionsBasic
-    , testModelOptionsEdgeCases
-    , testInvalidModelName
-    ]
diff --git a/test/Test/Ollama/Golden/Chat.hs b/test/Test/Ollama/Golden/Chat.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Golden/Chat.hs
@@ -0,0 +1,25 @@
+module Test.Ollama.Golden.Chat (tests) where
+
+import Data.Aeson (encode)
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Ollama
+import Test.Tasty
+import Test.Tasty.Golden
+
+tests :: TestTree
+tests =
+  testGroup
+    "Golden JSON Serialization Tests"
+    [ goldenVsString
+        "ChatRequest JSON wire format matches golden fixture"
+        "test/golden/chat_request.golden"
+        (pure $ encode $ chatRequest "llama3.2" (userMessage "Why is the sky blue?" :| []))
+    , goldenVsString
+        "GenerateRequest JSON wire format matches golden fixture"
+        "test/golden/generate_request.golden"
+        (pure $ encode $ generateRequest "llama3.2" "Why is the sky blue?")
+    , goldenVsString
+        "EmbedRequest JSON wire format matches golden fixture"
+        "test/golden/embed_request.golden"
+        (pure $ encode $ embedRequest "nomic-embed-text" ["hello", "world"])
+    ]
diff --git a/test/Test/Ollama/Golden/Embed.hs b/test/Test/Ollama/Golden/Embed.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Golden/Embed.hs
@@ -0,0 +1,16 @@
+module Test.Ollama.Golden.Embed (tests) where
+
+import Data.Aeson (encode)
+import Ollama
+import Test.Tasty
+import Test.Tasty.Golden
+
+tests :: TestTree
+tests =
+  testGroup
+    "Embed Golden JSON Serialization Tests"
+    [ goldenVsString
+        "EmbedRequest golden serialization"
+        "test/golden/embed_request.golden"
+        (pure $ encode $ embedRequest "nomic-embed-text" ["hello", "world"])
+    ]
diff --git a/test/Test/Ollama/Golden/Generate.hs b/test/Test/Ollama/Golden/Generate.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Golden/Generate.hs
@@ -0,0 +1,16 @@
+module Test.Ollama.Golden.Generate (tests) where
+
+import Data.Aeson (encode)
+import Ollama
+import Test.Tasty
+import Test.Tasty.Golden
+
+tests :: TestTree
+tests =
+  testGroup
+    "Generate Golden JSON Serialization Tests"
+    [ goldenVsString
+        "GenerateRequest golden serialization"
+        "test/golden/generate_request.golden"
+        (pure $ encode $ generateRequest "llama3.2" "Why is the sky blue?")
+    ]
diff --git a/test/Test/Ollama/Golden/Models.hs b/test/Test/Ollama/Golden/Models.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Golden/Models.hs
@@ -0,0 +1,16 @@
+module Test.Ollama.Golden.Models (tests) where
+
+import Data.Aeson (encode)
+import Ollama
+import Test.Tasty
+import Test.Tasty.Golden
+
+tests :: TestTree
+tests =
+  testGroup
+    "Model Management Golden JSON Serialization Tests"
+    [ goldenVsString
+        "CreateRequest golden serialization"
+        "test/golden/create_request.golden"
+        (pure $ encode $ defaultCreateRequest "custom-model")
+    ]
diff --git a/test/Test/Ollama/List.hs b/test/Test/Ollama/List.hs
deleted file mode 100644
--- a/test/Test/Ollama/List.hs
+++ /dev/null
@@ -1,41 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.List (tests) where
-
-import Data.Ollama.Common.Config (OllamaConfig (..), defaultOllamaConfig)
-import Data.Ollama.List
-import Data.Text qualified as T
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testListBasic :: TestTree
-testListBasic = testCase "List models: basic call should return Models" $ do
-  res <- list Nothing
-  case res of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right (Models modelList) -> do
-      assertBool "Should return a list (possibly empty)" True
-      -- Additional checks if models are present
-      case modelList of
-        [] -> return () -- Empty list is acceptable
-        (model : _) -> do
-          assertBool "Model name should not be empty" (not $ T.null $ name model)
-          assertBool "Model digest should not be empty" (not $ T.null $ digest model)
-          assertBool "Model size should be positive" (size model > 0)
-          assertBool "details should be present" True -- ModelDetails is always present
-
-testListWithConfig :: TestTree
-testListWithConfig = testCase "List with custom config should work" $ do
-  let config = Just $ defaultOllamaConfig {timeout = 10}
-  res <- list config
-  case res of
-    Left _ -> assertFailure "Expected success, got error"
-    Right (Models _) -> assertBool "Should return Models type" True
-
-tests :: TestTree
-tests =
-  testGroup
-    "List tests"
-    [ testListBasic
-    , testListWithConfig
-    ]
diff --git a/test/Test/Ollama/Load.hs b/test/Test/Ollama/Load.hs
deleted file mode 100644
--- a/test/Test/Ollama/Load.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Load (tests) where
-
-import Data.Ollama.Load
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testLoadGenModelBasic :: TestTree
-testLoadGenModelBasic = testCase "Load generation model: basic functionality" $ do
-  res <- loadGenModel "gemma3"
-  case res of
-    Left _ -> assertFailure "Expected success, got error"
-    Right () -> assertBool "Load should succeed" True
-
-testUnloadGenModelBasic :: TestTree
-testUnloadGenModelBasic = testCase "Unload generation model: basic functionality" $ do
-  res <- unloadGenModel "gemma3"
-  case res of
-    Left _ -> assertFailure "Expected success, got error"
-    Right () -> assertBool "Unload should succeed" True
-
-testLoadInvalidModel :: TestTree
-testLoadInvalidModel = testCase "Load invalid model should fail" $ do
-  res <- loadGenModel "nonexistent-model-12345"
-  case res of
-    Left _ -> assertBool "Should fail with invalid model" True
-    Right () -> assertFailure "Should not succeed with invalid model"
-
-testUnloadInvalidModel :: TestTree
-testUnloadInvalidModel = testCase "Unload invalid model should handle gracefully" $ do
-  res <- unloadGenModel "nonexistent-model-12345"
-  case res of
-    Left _ -> assertBool "Should fail or handle gracefully" True
-    Right () -> assertBool "Or succeed if server handles gracefully" True
-
-testLoadEmptyModelName :: TestTree
-testLoadEmptyModelName = testCase "Load model: empty name should fail" $ do
-  res <- loadGenModel ""
-  case res of
-    Left _ -> assertBool "Should fail with empty name" True
-    Right () -> assertFailure "Should not succeed with empty name"
-
-tests :: TestTree
-tests =
-  testGroup
-    "Load tests"
-    [ testLoadGenModelBasic
-    , testUnloadGenModelBasic
-    , testLoadInvalidModel
-    , testUnloadInvalidModel
-    , testLoadEmptyModelName
-    ]
diff --git a/test/Test/Ollama/Property/Arbitrary.hs b/test/Test/Ollama/Property/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Property/Arbitrary.hs
@@ -0,0 +1,51 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Test.Ollama.Property.Arbitrary () where
+
+import Data.Text (Text)
+import Data.Text qualified as T
+import Ollama
+import Ollama.Types.Message qualified as M
+import Test.QuickCheck
+
+genNonEmptyText :: Gen Text
+genNonEmptyText = T.pack <$> listOf1 (elements ['a' .. 'z'])
+
+instance Arbitrary Role where
+  arbitrary = elements [System, User, Assistant, M.Tool]
+
+instance Arbitrary ModelName where
+  arbitrary = ModelName <$> genNonEmptyText
+
+instance Arbitrary Digest where
+  arbitrary = Digest . ("sha256:" <>) <$> genNonEmptyText
+
+instance Arbitrary Base64Image where
+  arbitrary = Base64Image <$> genNonEmptyText
+
+instance Arbitrary Duration where
+  arbitrary = Duration <$> choose (1, 1000000000)
+
+instance Arbitrary ThinkingLevel where
+  arbitrary = elements [ThinkLow, ThinkMedium, ThinkHigh, ThinkMax]
+
+instance Arbitrary Think where
+  arbitrary =
+    oneof
+      [ pure ThinkEnabled
+      , pure ThinkDisabled
+      , ThinkLevel <$> arbitrary
+      ]
+
+instance Arbitrary Message where
+  arbitrary =
+    Message
+      <$> arbitrary
+      <*> genNonEmptyText
+      <*> pure Nothing
+      <*> pure Nothing
+      <*> pure Nothing
+      <*> pure Nothing
+
+instance Arbitrary Format where
+  arbitrary = pure JsonFormat
diff --git a/test/Test/Ollama/Property/Roundtrip.hs b/test/Test/Ollama/Property/Roundtrip.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Property/Roundtrip.hs
@@ -0,0 +1,34 @@
+module Test.Ollama.Property.Roundtrip (tests) where
+
+import Data.Aeson (decode, encode)
+import Data.Text qualified as T
+import Ollama
+import Test.Ollama.Property.Arbitrary ()
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+tests :: TestTree
+tests =
+  testGroup
+    "Property & Roundtrip QuickCheck Tests"
+    [ testProperty "Role JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(r :: Role) -> decode (encode r) === Just r
+    , testProperty "Message JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(msg :: Message) -> decode (encode msg) === Just msg
+    , testProperty "ModelName JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(m :: ModelName) -> decode (encode m) === Just m
+    , testProperty "Think JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(t :: Think) -> decode (encode t) === Just t
+    , testProperty "Digest JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(d :: Digest) -> decode (encode d) === Just d
+    , testProperty "Duration JSON roundtrip: ∀ x. decode (encode x) == Just x" $
+        \(dur :: Duration) -> decode (encode dur) === Just dur
+    , testProperty "Idempotency property: ∀ x. encode (decoded x) == encode x" $
+        \(msg :: Message) -> case decode (encode msg) :: Maybe Message of
+          Nothing -> property False
+          Just decoded -> encode decoded === encode msg
+    , testProperty "Smart constructor invariant: mkModelName never returns empty ModelName" $
+        \txt -> case mkModelName (T.pack txt) of
+          Left err -> err === "Model name cannot be empty"
+          Right (ModelName name) -> not (T.null name) === True
+    ]
diff --git a/test/Test/Ollama/Show.hs b/test/Test/Ollama/Show.hs
deleted file mode 100644
--- a/test/Test/Ollama/Show.hs
+++ /dev/null
@@ -1,61 +0,0 @@
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE OverloadedStrings #-}
-
-module Test.Ollama.Show (tests) where
-
-import Data.Maybe (isJust)
-import Data.Ollama.Ps
-import Data.Ollama.Show
-import Data.Text qualified as T
-import Test.Tasty
-import Test.Tasty.HUnit
-
-testShowModelBasic :: TestTree
-testShowModelBasic = testCase "Show model info: basic call" $ do
-  res <- showModel "gemma3"
-  case res of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right ShowModelResponse {modelFile, modelInfo = ShowModelInfo {generalArchitecture}} -> do
-      assertBool "modelFile should not be empty" (not $ T.null modelFile)
-      assertBool "Architecture should be present" (isJust generalArchitecture)
-
-testShowModelVerbose :: TestTree
-testShowModelVerbose = testCase "Show model info: verbose enabled" $ do
-  res <- showModelOps "qwen3:0.6b" (Just True) Nothing
-  case res of
-    Left _ -> pure () -- assertFailure $ "Expected success, got error: " ++ show err
-    Right ShowModelResponse {template, parameters} -> do
-      -- Verbose should yield more details like parameters/template
-      assertBool "Should have a template if verbose" (isJust template)
-      assertBool "Should have parameters if verbose" (isJust parameters)
-
-testPsBasic :: TestTree
-testPsBasic = testCase "List running models: basic success" $ do
-  res <- ps Nothing
-  case res of
-    Left err -> assertFailure $ "Expected success, got error: " ++ show err
-    Right (RunningModels _) -> do
-      assertBool "Should return a list (possibly empty)" True
-
-testPsModelFields :: TestTree
-testPsModelFields = testCase "Running model fields are populated" $ do
-  res <- ps Nothing
-  case res of
-    Left _ -> return () -- Allow failure if no models are running
-    Right (RunningModels (m : _)) -> do
-      assertBool "name_ should not be empty" (not $ T.null $ name_ m)
-      assertBool "modelName should not be empty" (not $ T.null $ modelName m)
-      assertBool "modelDigest should not be empty" (not $ T.null $ modelDigest m)
-      assertBool "size_ should be positive" (size_ m > 0)
-      assertBool "sizeVRam should be non-negative" (sizeVRam m >= 0)
-    Right _ -> return () -- If empty, that's acceptable
-
-tests :: TestTree
-tests =
-  testGroup
-    "show model tests"
-    [ testShowModelBasic
-    , testShowModelVerbose
-    , testPsBasic
-    , testPsModelFields
-    ]
diff --git a/test/Test/Ollama/Unit/Config.hs b/test/Test/Ollama/Unit/Config.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Unit/Config.hs
@@ -0,0 +1,19 @@
+module Test.Ollama.Unit.Config (tests) where
+
+import Ollama.Client.Config
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: TestTree
+tests =
+  testGroup
+    "Unit Config Tests"
+    [ testCase "defaultConfig settings" $ do
+        assertEqual "Default base URL" "http://127.0.0.1:11434" (configBaseUrl defaultConfig)
+        assertEqual "Default timeout" 90 (configTimeout defaultConfig)
+        assertEqual "Default retry policy" NoRetry (configRetry defaultConfig)
+    , testCase "RetryPolicy smart constructors" $ do
+        assertEqual "noRetry" NoRetry noRetry
+        assertEqual "constantRetry" (ConstantRetry 3 1000000) (constantRetry 3 1000000)
+        assertEqual "exponentialRetry" (ExponentialRetry 5 500000) (exponentialRetry 5 500000)
+    ]
diff --git a/test/Test/Ollama/Unit/Error.hs b/test/Test/Ollama/Unit/Error.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Unit/Error.hs
@@ -0,0 +1,22 @@
+module Test.Ollama.Unit.Error (tests) where
+
+import Control.Exception (try)
+import Ollama.Error
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: TestTree
+tests =
+  testGroup
+    "Unit Error Tests"
+    [ testCase "isRetryable correctly classifies transient vs non-transient errors" $ do
+        assertBool "TimeoutError is retryable" (isRetryable TimeoutError)
+        assertBool "ApiError is not retryable" (not $ isRetryable (ApiError 404 "Not Found"))
+        assertBool "InvalidRequest is not retryable" (not $ isRetryable (InvalidRequest "Bad model"))
+    , testCase "OllamaError Eq instance compares ApiError and TimeoutError" $ do
+        assertEqual "ApiError equal" (ApiError 500 "Server Error") (ApiError 500 "Server Error")
+        assertBool "ApiError not equal to TimeoutError" (ApiError 500 "Server Error" /= TimeoutError)
+    , testCase "throwOllama raises OllamaError as an Exception" $ do
+        res <- try (throwOllama TimeoutError) :: IO (Either OllamaError ())
+        assertEqual "Caught thrown OllamaError" (Left TimeoutError) res
+    ]
diff --git a/test/Test/Ollama/Unit/SchemaBuilder.hs b/test/Test/Ollama/Unit/SchemaBuilder.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Unit/SchemaBuilder.hs
@@ -0,0 +1,33 @@
+module Test.Ollama.Unit.SchemaBuilder (tests) where
+
+import Data.Aeson (decode, encode)
+import Ollama.Types.Format
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: TestTree
+tests =
+  testGroup
+    "Unit SchemaBuilder DSL Tests"
+    [ testCase "Build object schema with required and optional fields" $ do
+        let builder =
+              emptyObject
+                |+ ("name", JString)
+                |+ ("age", JInteger)
+                |+ ("is_student", JBoolean)
+                |! "name"
+            sch = buildSchema builder
+            encoded = encode sch
+        assertBool "Non-empty JSON schema" (not $ null $ show encoded)
+    , testCase "Build array schema" $ do
+        let itemType = arrayOf JString
+            builder = emptyObject |+ ("items", itemType)
+            sch = buildSchema builder
+            encoded = encode sch
+        assertBool "Non-empty array schema" (not $ null $ show encoded)
+    , testCase "SchemaFormat wrapping & JSON roundtrip" $ do
+        let builder = emptyObject |+ ("count", JInteger) |! "count"
+            sch = buildSchema builder
+            fmt = SchemaFormat sch
+        assertEqual "SchemaFormat JSON roundtrip check" (Just fmt) (decode $ encode fmt)
+    ]
diff --git a/test/Test/Ollama/Unit/Testing.hs b/test/Test/Ollama/Unit/Testing.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Unit/Testing.hs
@@ -0,0 +1,87 @@
+{-# OPTIONS_GHC -Wno-deprecations #-}
+
+{- |
+Module      : Test.Ollama.Unit.Testing
+Copyright   : (c) 2024-2026 Tushar Adhatrao
+License     : MIT
+Maintainer  : tusharadhatrao@gmail.com
+Stability   : stable
+Portability : portable
+
+Unit tests for Ollama.Testing mock client and helper functions.
+
+@since 1.0.0.0
+-}
+module Test.Ollama.Unit.Testing (testingTests) where
+
+import Data.Aeson (encode)
+import Data.ByteString.Lazy qualified as LBS
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Ollama.API.Chat (ChatResponse (..), chat, chatRequest)
+import Ollama.API.Embed (
+  EmbedResponse (..),
+  EmbeddingsRequest (..),
+  EmbeddingsResponse (..),
+  embed,
+  embedRequest,
+  embeddings,
+ )
+import Ollama.API.Generate (GenerateResponse (..), generate, generateRequest)
+import Ollama.API.Models (ListResponse (..), listModels)
+import Ollama.Testing (
+  mockChatResponse,
+  mockEmbedResponse,
+  mockGenerateResponse,
+  mockListModelsResponse,
+  withMockClient,
+ )
+import Ollama.Types.Common (ModelName (..))
+import Ollama.Types.Message (userMessage)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertEqual, testCase)
+
+testingTests :: TestTree
+testingTests =
+  testGroup
+    "Unit Testing Infrastructure Tests"
+    [ testCase "Mock generate response" $ do
+        let respPayload = mockGenerateResponse (ModelName "llama3.2") "Sky is blue"
+            encoded = encode respPayload
+        withMockClient (LBS.toStrict encoded) $ \client -> do
+          res <- generate client (generateRequest (ModelName "llama3.2") "Why?")
+          case res of
+            Left err -> fail $ "Expected success, got: " <> show err
+            Right resp -> assertEqual "Matching text" "Sky is blue" (grResponse resp)
+    , testCase "Mock chat response" $ do
+        let respPayload = mockChatResponse (ModelName "llama3.2") "Hello there"
+            encoded = encode respPayload
+        withMockClient (LBS.toStrict encoded) $ \client -> do
+          res <- chat client (chatRequest (ModelName "llama3.2") (userMessage "Hi" :| []))
+          case res of
+            Left err -> fail $ "Expected success, got: " <> show err
+            Right resp -> assertEqual "Matching model" (ModelName "llama3.2") (crModel resp)
+    , testCase "Mock embed response" $ do
+        let respPayload = mockEmbedResponse (ModelName "nomic") [[0.1, 0.2]]
+            encoded = encode respPayload
+        withMockClient (LBS.toStrict encoded) $ \client -> do
+          res <- embed client (embedRequest (ModelName "nomic") ["test"])
+          case res of
+            Left err -> fail $ "Expected success, got: " <> show err
+            Right resp -> assertEqual "Matching embeddings" [[0.1, 0.2]] (erEmbeddings resp)
+    , testCase "Mock deprecated embeddings endpoint" $ do
+        let respPayload = EmbeddingsResponse [0.5, 0.6]
+            encoded = encode respPayload
+        withMockClient (LBS.toStrict encoded) $ \client -> do
+          res <- embeddings client (EmbeddingsRequest (ModelName "nomic") "test" Nothing Nothing)
+          case res of
+            Left err -> fail $ "Expected success, got: " <> show err
+            Right resp -> assertEqual "Matching embedding" [0.5, 0.6] (ebrEmbedding resp)
+    , testCase "Mock list models response" $ do
+        let respPayload = mockListModelsResponse [ModelName "llama3.2", ModelName "nomic"]
+            encoded = encode respPayload
+        withMockClient (LBS.toStrict encoded) $ \client -> do
+          res <- listModels client
+          case res of
+            Left err -> fail $ "Expected success, got: " <> show err
+            Right resp -> assertEqual "Matching count" 2 (length (models resp))
+    ]
diff --git a/test/Test/Ollama/Unit/Types.hs b/test/Test/Ollama/Unit/Types.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Ollama/Unit/Types.hs
@@ -0,0 +1,72 @@
+module Test.Ollama.Unit.Types (tests) where
+
+import Conduit (yield)
+import Data.Aeson (decode, encode)
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Time (getCurrentTime)
+import Ollama
+import Ollama.Types.Message qualified as M
+import Test.Tasty
+import Test.Tasty.HUnit
+
+tests :: TestTree
+tests =
+  testGroup
+    "Unit Types & API Tests"
+    [ testCase "ModelName smart constructor validation" $ do
+        assertEqual "Empty name invalid" (Left "Model name cannot be empty") (mkModelName "")
+        assertEqual "Valid name" (Right "llama3.2") (mkModelName "llama3.2")
+    , testCase "GenerateRequest smart constructor" $ do
+        let req = generateRequest "llama3.2" "Hello world"
+        assertEqual "Model match" (ModelName "llama3.2") (genModel req)
+        assertEqual "Prompt match" "Hello world" (genPrompt req)
+        assertEqual "Stream default" (Just False) (genStream req)
+    , testCase "ChatRequest smart constructor" $ do
+        let msg = userMessage "Hi"
+            req = chatRequest "llama3.2" (msg :| [])
+        assertEqual "Model match" (ModelName "llama3.2") (chatModel req)
+        assertEqual "Messages length" 1 (length (chatMessages req))
+    , testCase "EmbedRequest smart constructor" $ do
+        let req = embedRequest "nomic-embed-text" ["hello", "world"]
+        assertEqual "Model match" (ModelName "nomic-embed-text") (embModel req)
+        assertEqual "Input match" (Right ["hello", "world"]) (embInput req)
+    , testCase "CreateRequest smart constructor" $ do
+        let req = defaultCreateRequest "custom-model"
+        assertEqual "Model match" (ModelName "custom-model") (crqModel req)
+    , testCase "Role JSON roundtrip" $ do
+        assertEqual "User role" (Just User) (decode (encode User))
+        assertEqual "Assistant role" (Just Assistant) (decode (encode Assistant))
+        assertEqual "System role" (Just System) (decode (encode System))
+        assertEqual "Tool role" (Just M.Tool) (decode (encode M.Tool))
+    , testCase "InMemoryStore operations" $ do
+        store <- initInMemoryStore
+        now <- getCurrentTime
+        let conv =
+              Conversation
+                { conversationId = "c1"
+                , messages = [userMessage "hi"]
+                , model = "llama3.2"
+                , createdAt = now
+                , lastUpdated = now
+                }
+        saveConversationInMemory store conv
+        mLoaded <- loadConversationInMemory store "c1"
+        assertEqual "Load conversation" (Just conv) mLoaded
+
+        allConvs <- listConversationsInMemory store
+        assertEqual "List conversations" [conv] allConvs
+
+        deleted <- deleteConversationInMemory store "c1"
+        assertBool "Delete succeeded" deleted
+
+        mLoadedAfter <- loadConversationInMemory store "c1"
+        assertEqual "Load after delete" Nothing mLoadedAfter
+    , testCase "Streaming collectStream helper" $ do
+        let mockStream = mapM_ yield [1 .. 5 :: Int]
+        items <- collectStream mockStream
+        assertEqual "Collected list" [1, 2, 3, 4, 5] items
+    , testCase "Streaming foldStream helper" $ do
+        let mockStream = mapM_ yield [1 .. 5 :: Int]
+        sumVal <- foldStream (+) 0 mockStream
+        assertEqual "Folded sum" 15 sumVal
+    ]
