Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
Story 2.4: Provider Ollama (LLM Local)
Status: done
Story
As a system, I want to integrate Ollama as an LLM provider with custom system prompt support, so that Pro users can translate documents with local LLMs for privacy and cost efficiency.
Acceptance Criteria
- AC1: API Integration - Given
OLLAMA_BASE_URLis configured (default: http://localhost:11434), whenOllamaProvider.translate_text()is called with model and prompt, then text is translated using the specified Ollama model - AC2: Graceful Error Handling - Connection/timeout returns error code
OLLAMA_UNAVAILABLE/OLLAMA_TIMEOUTwith clear message (e.g. "Service Ollama indisponible..."), never HTTP 500 - AC3: Custom System Prompt - Custom system prompt can be injected via the request to guide translation context
- AC4: Health Check - Provider
is_available()returnsTruewhen Ollama is reachable and model is pulled,Falseotherwise - AC5: Registry Integration - Provider is registered in
ProviderRegistryand appears in fallback chain - AC6: Unit Tests - Tests verify all error scenarios, connection handling, and mock Ollama API responses
Tasks / Subtasks
-
Task 1: Create Ollama Provider Implementation (AC: 1, 3)
- 1.1 Create
services/providers/ollama_provider.py - 1.2 Implement
OllamaTranslationProviderclass extendingTranslationProvider - 1.3 Implement
translate_text()using Ollama REST API (/api/generateor/api/chat) - 1.4 Support custom system prompt injection via request metadata
- 1.5 Configure default translation system prompt
- 1.1 Create
-
Task 2: Implement Error Handling (AC: 2)
- 2.1 Define error codes:
OLLAMA_UNAVAILABLE,OLLAMA_MODEL_NOT_FOUND,OLLAMA_TIMEOUT,OLLAMA_GENERATION_ERROR,OLLAMA_CONTEXT_TOO_LONG - 2.2 Implement
OllamaProviderErrorexception class (follow existing pattern) - 2.3 Map Ollama API errors to structured error responses
- 2.4 Add retry logic with exponential backoff for transient errors
- 2.5 Add timeout configuration (default 120s for LLM - longer than classic)
- 2.6 Ensure all errors return JSON:
{error, message, details?}format
- 2.1 Define error codes:
-
Task 3: Implement Health Check (AC: 4)
- 3.1 Implement
is_available()to check Ollama service reachability - 3.2 Add
health_check()with caching (TTL 60s) matching existing provider pattern - 3.3 Verify configured model is available (pulled) via
/api/tags - 3.4 Return
ProviderHealthStatuswith availability, latency, and model info
- 3.1 Implement
-
Task 4: Registry Integration (AC: 5)
- 4.1 Add
register_ollama_provider()function - 4.2 Add
get_ollama_provider()singleton function - 4.3 Update
services/providers/__init__.pyto auto-register Ollama when enabled - 4.4 Verify provider appears in fallback chain when configured
- 4.1 Add
-
Task 5: Configuration Updates (AC: 1, 2)
- 5.1 Verify
OLLAMA_BASE_URL,OLLAMA_MODEL,OLLAMA_ENABLEDinconfig.py(already present) - 5.2 Add Ollama-specific configuration options to
config.py:OLLAMA_TIMEOUT=120(LLM needs longer timeout)OLLAMA_MAX_RETRIES=2OLLAMA_RETRY_DELAY=2
- 5.3 Update
.env.examplewith Ollama-specific config
- 5.1 Verify
-
Task 6: Create Unit Tests (AC: 6)
- 6.1 Create
tests/test_providers/test_ollama_provider.py - 6.2 Test successful translation with mocked Ollama API
- 6.3 Test all error scenarios (unavailable, model not found, timeout)
- 6.4 Test custom system prompt injection
- 6.5 Test retry logic
- 6.6 Test health check functionality
- 6.7 Test registry integration
- 6.1 Create
-
Task 7: Update Documentation (AC: 1-6)
- 7.1 Update
services/providers/README.mdwith Ollama section - 7.2 Document Ollama setup requirements (pull models first)
- 7.3 Document supported models and recommendations for translation
- 7.1 Update
Dev Notes
Ollama API Specifics
Ollama REST API Endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
/api/generate |
POST | Generate text (streaming or not) |
/api/chat |
POST | Chat completion with messages |
/api/tags |
GET | List pulled models |
/api/show |
POST | Show model info |
Recommended: Use /api/chat for translation (better prompt handling):
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
payload = {
"model": "llama3",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text_to_translate}
],
"stream": False,
"options": {
"temperature": 0.3 # Lower for more consistent translation
}
}
API Response Format:
{
"model": "llama3",
"created_at": "2024-01-15T10:30:00Z",
"message": {
"role": "assistant",
"content": "Bonjour, comment allez-vous?"
},
"done": true
}
Recommended Models for Translation
| Model | Size | Best For | Notes |
|---|---|---|---|
llama3 |
8B | General translation | Good balance of speed/quality |
llama3:70b |
70B | High-quality translation | Requires significant RAM |
mistral |
7B | Fast translation | Good for real-time |
qwen2 |
7B | Multi-language | Strong non-English support |
deepseek-coder |
6.7B | Technical docs | Good for code comments |
Pre-requisite: Models must be pulled before use:
ollama pull llama3
ollama pull mistral
Default System Prompt for Translation
DEFAULT_TRANSLATION_PROMPT = """You are a professional translator. Translate the following text from {source_lang} to {target_lang}.
Rules:
- Translate ONLY the text, do not add explanations or notes
- Preserve the original formatting, line breaks, and structure
- Maintain the original tone and style
- For technical terms, use the standard translation in the target language
- If the text contains proper nouns or brand names, keep them unchanged unless there's a well-known translation"""
def _build_system_prompt(
source_lang: str,
target_lang: str,
custom_prompt: Optional[str] = None
) -> str:
if custom_prompt:
return custom_prompt
return DEFAULT_TRANSLATION_PROMPT.format(
source_lang=source_lang,
target_lang=target_lang
)
Architecture Compliance
Per _bmad-output/planning-artifacts/architecture.md:
Error Format:
{
"error": "OLLAMA_UNAVAILABLE",
"message": "Service Ollama indisponible. Vérifiez que Ollama est en cours d'exécution.",
"details": {
"provider": "ollama",
"base_url": "http://localhost:11434",
"model": "llama3"
}
}
Never return HTTP 500 - All errors must be 4xx or 502 (upstream error).
Naming Conventions:
- File:
ollama_provider.py(snake_case) - Class:
OllamaTranslationProvider(PascalCase) - Error codes:
OLLAMA_*(UPPER_SNAKE_CASE) - JSON fields: snake_case
Previous Story Intelligence (Story 2.2 & 2.3)
What Worked Well:
deep_translatorlibrary integration for Google/DeepL- Thread-safe translator instances per thread
- Error codes with
to_dict()method - Retry logic with exponential backoff
- Health check with 60s TTL caching
- Structlog-compatible logging with keyword args
Patterns to Reuse:
# Error codes pattern
OLLAMA_UNAVAILABLE = "OLLAMA_UNAVAILABLE"
OLLAMA_MODEL_NOT_FOUND = "OLLAMA_MODEL_NOT_FOUND"
OLLAMA_TIMEOUT = "OLLAMA_TIMEOUT"
OLLAMA_GENERATION_ERROR = "OLLAMA_GENERATION_ERROR"
OLLAMA_CONTEXT_TOO_LONG = "OLLAMA_CONTEXT_TOO_LONG"
_RETRYABLE_ERRORS = {OLLAMA_UNAVAILABLE, OLLAMA_TIMEOUT}
# Exception class pattern (from Google/DeepL providers)
class OllamaProviderError(Exception):
def __init__(self, code: str, message: str, details: Optional[Dict[str, Any]] = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)
def to_dict(self) -> Dict[str, Any]:
result = {"error": self.code, "message": self.message}
if self.details:
result["details"] = self.details
return result
Key Difference for Ollama:
- Uses HTTP requests (not a library like
deep_translator) - Longer timeout required (120s default vs 30s for classic)
- Model must be pre-pulled before use
- Custom system prompt support is essential
File Structure
Files to Create:
services/providers/ollama_provider.py- Main provider implementationtests/test_providers/test_ollama_provider.py- Unit tests
Files to Modify:
services/providers/__init__.py- Add Ollama auto-registrationservices/providers/config.py- Add OLLAMA_TIMEOUT, OLLAMA_MAX_RETRIES, OLLAMA_RETRY_DELAY.env.example- Add Ollama-specific config (may already have basic config)services/providers/README.md- Add Ollama documentation
Error Codes to Implement
| Code | HTTP | Scenario | Message Template |
|---|---|---|---|
OLLAMA_UNAVAILABLE |
502 | Ollama service not reachable | "Service Ollama indisponible. Vérifiez que Ollama est en cours d'exécution." |
OLLAMA_MODEL_NOT_FOUND |
400 | Model not pulled | "Modèle '{model}' non trouvé. Exécutez: ollama pull {model}" |
OLLAMA_TIMEOUT |
502 | Request timeout | "Délai d'attente Ollama dépassé. Réessayez avec un texte plus court." |
OLLAMA_GENERATION_ERROR |
502 | LLM generation failed | "Erreur de génération Ollama: {error}" |
OLLAMA_CONTEXT_TOO_LONG |
413 | Context exceeds model limit | "Texte trop long pour le modèle (max ~{max_tokens} tokens)." |
Configuration
Environment Variables (.env.example):
# Ollama Provider (Local LLM)
OLLAMA_ENABLED=true
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3
OLLAMA_VISION_MODEL=llava
OLLAMA_TIMEOUT=120
OLLAMA_MAX_RETRIES=2
OLLAMA_RETRY_DELAY=2
Provider Config (services/providers/config.py):
Add after existing OLLAMA config:
OLLAMA_TIMEOUT: int = int(os.getenv("OLLAMA_TIMEOUT", "120"))
OLLAMA_MAX_RETRIES: int = int(os.getenv("OLLAMA_MAX_RETRIES", "2"))
OLLAMA_RETRY_DELAY: float = float(os.getenv("OLLAMA_RETRY_DELAY", "2.0"))
Testing Strategy
Unit Tests (Mocked):
- Mock
httpxorrequestsresponses - Test successful translation
- Test all error scenarios (unavailable, model not found, timeout)
- Test custom system prompt injection
- Test health check logic
- Test retry logic
- Test registry integration
Integration Tests (Optional):
- With Ollama running locally: real API calls
- Without Ollama: skip integration tests
- Use pytest markers:
@pytest.mark.integration
Test Commands:
# Unit tests only
pytest tests/test_providers/test_ollama_provider.py -v
# All provider tests
pytest tests/test_providers/ -v
# With coverage
pytest tests/test_providers/ --cov=services/providers -v
Logging Pattern
try:
import structlog
logger = structlog.get_logger(__name__)
except ImportError:
import logging
logger = logging.getLogger(__name__)
# Good - metadata only (NO document content)
logger.info(
"ollama_translation_success",
chars=len(text),
source_lang=source_language,
target_lang=target_language,
model=self._model,
latency_ms=round(latency * 1000, 2),
)
logger.error(
"ollama_translation_failed",
error_code=error.code,
text_length=len(text),
source_lang=source_language,
target_lang=target_language,
model=self._model,
)
Dependencies
Internal:
services/providers/base.py- TranslationProvider abstract classservices/providers/registry.py- ProviderRegistryservices/providers/config.py- Configurationservices/providers/schemas.py- TranslationRequest/Response models
External:
httpx- HTTP client (preferred over requests for async support)structlogor standardlogging- Structured logging
HTTP Client Pattern
Use httpx for Ollama API calls (supports async and sync):
import httpx
class OllamaTranslationProvider(TranslationProvider):
def __init__(self, base_url: str, model: str, timeout: int = 120):
self._base_url = base_url.rstrip("/")
self._model = model
self._timeout = timeout
self._client = httpx.Client(timeout=timeout)
def _make_api_request(self, text: str, system_prompt: str) -> str:
response = self._client.post(
f"{self._base_url}/api/chat",
json={
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"stream": False,
"options": {"temperature": 0.3}
}
)
# ... error handling
return response.json()["message"]["content"]
References
- [Source: _bmad-output/planning-artifacts/architecture.md#Error Handling]
- [Source: _bmad-output/planning-artifacts/architecture.md#API Response Formats]
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.4]
- [Source: _bmad-output/planning-artifacts/prd.md#FR7 LLM providers (Ollama, OpenAI)]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR12 Zero HTTP 500 errors]
- [Source: _bmad-output/planning-artifacts/prd.md#NFR13 Provider fallback]
- [Source: _bmad-output/implementation-artifacts/2-2-provider-google-translate.md]
- [Source: _bmad-output/implementation-artifacts/2-3-provider-deepl.md]
- [Source: services/providers/google_provider.py - Implementation pattern]
- [Source: services/providers/registry.py - Registration pattern]
- [Source: https://github.com/ollama/ollama/blob/main/docs/api.md - Ollama API docs]
Security Considerations
Local Deployment:
- Ollama runs locally by default (no external API calls)
- No API key required for local Ollama
- If using remote Ollama, consider network security
Data Privacy:
- Never log document content (NFR11)
- Only log metadata: text length, languages, model, timestamps
- Ollama keeps data local (privacy advantage over cloud LLMs)
Pro Feature Integration
Per PRD FR26: "Pro users can access LLM translation modes"
This provider will be used when:
- User tier is "pro"
- User selects "LLM" mode
- User selects "Ollama" as LLM provider
The tier check happens in the translation service/router, not in the provider itself.
Dev Agent Record
Agent Model Used
Claude (GLM-5) via opencode
Debug Log References
- Fixed logging compatibility issue: standard logging doesn't support keyword arguments like structlog
- Created helper functions
_log_info,_log_warning,_log_errorto bridge the gap - Updated test file to use
requestsinstead ofhttpx(httpx not in requirements)
Completion Notes List
- ✅ Implemented
OllamaTranslationProviderclass with all required features - ✅ Uses
/api/chatendpoint for translation with system prompt support - ✅ All 5 error codes implemented with French messages
- ✅ Retry logic with exponential backoff for
OLLAMA_UNAVAILABLEandOLLAMA_TIMEOUT - ✅ Health check with 60s TTL caching and model availability verification
- ✅ Registry integration with auto-registration when
OLLAMA_ENABLED=true - ✅ Custom system prompt injection via
request.metadata["custom_prompt"] - ✅ Language name mapping for better LLM understanding
- ✅ 29 unit tests created and all passing
- ✅ Documentation updated in README.md with Ollama section
Code Review Fixes (AI) – 2026-02-21
- AC4 / ProviderHealthStatus – Added optional fields
modelandmodel_availabletoProviderHealthStatusinschemas.py; Ollamahealth_check()now returns model info (availability, latency, model name). - Health check messages – Unified to French in
health_check()(e.g. "Service Ollama indisponible...", "Modèle 'x' non trouvé..."). - Tests – Removed unused
import socket; addedtest_timeout_returns_ollama_timeout_error; strengthenedtest_health_check_cachingwith mock to assert no API call when cache is valid; added assertions formodelandmodel_availablein health check tests. - AC2 – Story AC2 wording updated to reflect implementation (error codes
OLLAMA_UNAVAILABLE/OLLAMA_TIMEOUT).
File List
Files Created:
services/providers/ollama_provider.py- Main Ollama provider implementationtests/test_providers/test_ollama_provider.py- 29 unit tests
Files Modified:
services/providers/__init__.py- Added Ollama auto-registrationservices/providers/config.py- Added OLLAMA_TIMEOUT, OLLAMA_MAX_RETRIES, OLLAMA_RETRY_DELAYservices/providers/schemas.py- Added metadata field to TranslationRequest for custom prompt supportservices/providers/README.md- Added comprehensive Ollama documentation.env.example- Added Ollama-specific configuration options
Change Log
- 2026-02-21: Story 2.4 implementation complete - Ollama provider with local LLM translation, custom prompts, error handling, and 29 passing tests
- 2026-02-21: Code review fixes - ProviderHealthStatus model info (model, model_available), health check messages in French, tests (timeout error, cache assertion, model assertions), AC2 wording aligned