Overview
Interview Simulator uses a provider pattern for AI integrations, allowing multiple LLM providers to work together with automatic fallback. The system supports Google Gemini and OpenRouter out of the box, with extensibility for custom providers.
Provider Architecture
Provider Interface
All AI providers implement the AIProvider protocol defined in client/ai_provider.py:
from typing import Protocol
class AIProvider(Protocol):
def generate_text(self, prompt: str) -> str:
...
Any class implementing this generate_text method can be used as a provider.
Provider Manager
The ProviderManager class in client/ai_provider_manager.py orchestrates multiple providers with circuit breaker pattern:
client/ai_provider_manager.py
class ProviderManager:
def __init__(self, providers: list[AIProvider]):
self.providers = providers
self.fail_count = {p: 0 for p in providers}
self.open_until = {p: 0 for p in providers}
def generate_text(self, prompt):
last_error = None
for provider in self.providers:
if not self._is_available(provider):
continue
try:
return provider.generate_text(prompt)
except Exception as e:
last_error = e
self.fail_count[provider] += 1
if self.fail_count[provider] >= 3:
self.open_until[provider] = time.time() + 120.0
raise AIServiceError(f"All providers failed: {last_error}")
Key features:
- Automatic fallback: Tries providers in order until one succeeds
- Circuit breaker: After 3 failures, a provider is disabled for 120 seconds
- Resilience: System continues working if one provider fails
Initialization
Providers are initialized in app/extensions.py during application startup:
def init_ai_providers(app):
global provider_manager, ai_client
openrouter_key = app.config.get("OPENROUTER_API_KEY", "")
gemini_key = app.config.get("GEMINI_API_KEY", "")
providers = []
if openrouter_key:
providers.append(
OpenRouterProvider(
api_key=openrouter_key, model_name="openai/gpt-oss-20b:free"
)
)
if gemini_key:
providers.append(
GeminiProvider(api_key=gemini_key, model_name="gemini-2.5-flash")
)
if not providers:
app.logger.warning("No AI providers configured! Check your API keys.")
return
provider_manager = ProviderManager(providers)
ai_client = AIClient(provider_manager)
app.logger.info(f"Initialized {len(providers)} AI provider(s)")
Built-in Providers
Google Gemini
The Gemini provider uses Google’s Generative AI SDK:
client/gemini_provider.py
from google import genai
from tenacity import retry, stop_after_attempt, wait_exponential
class GeminiProvider:
def __init__(self, api_key: str, model_name: str = "gemini-2.5-flash"):
if not api_key:
raise ValueError("API key is required")
self.client = genai.Client(api_key=api_key)
self.model_name = model_name
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
)
def generate_text(self, prompt: str) -> str:
response = self.client.models.generate_content(
model=self.model_name,
contents=prompt,
)
if not response.parts:
raise RuntimeError("Gemini response was blocked or empty")
text = response.text
if not text:
raise RuntimeError("Gemini returned empty response")
return text
Configuration:
GEMINI_API_KEY=your-api-key-here
Features:
- Automatic retry with exponential backoff (3 attempts)
- Response validation (checks for blocked or empty responses)
- Configurable model selection
Get API key: https://ai.google.dev/
OpenRouter
The OpenRouter provider provides access to multiple LLM providers through a unified API:
client/openrouter_provider.py
import requests
import json
from tenacity import retry, stop_after_attempt, wait_exponential
class OpenRouterProvider:
def __init__(self, api_key: str, model_name: str = "openai/gpt-oss-20b:free"):
if not api_key:
raise ValueError("API key is required")
self.api_key = api_key
self.model_name = model_name
self.endpoint = "https://openrouter.ai/api/v1/chat/completions"
self._headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(
(requests.RequestException, ConnectionError, TimeoutError)
),
)
def generate_text(self, prompt: str) -> str:
payload = {
"model": self.model_name,
"messages": [{"role": "user", "content": prompt}],
"extra_body": {"reasoning": {"enabled": True}},
}
response = requests.post(
self.endpoint, headers=self._headers, data=json.dumps(payload)
)
if response.status_code != 200:
raise RuntimeError(
f"OpenRouter API error: {response.status_code} - {response.text}"
)
data = response.json()
try:
message = data["choices"][0]["message"]
text = message.get("content")
except (KeyError, IndexError):
raise RuntimeError("Malformed response from OpenRouter API")
if not text:
raise RuntimeError("OpenRouter returned empty response")
return text
Configuration:
OPENROUTER_API_KEY=your-api-key-here
Features:
- Automatic retry with exponential backoff (3 attempts)
- Reasoning mode enabled by default
- Response validation and error handling
- Access to multiple LLM providers through one API
Get API key: https://openrouter.ai/
Available models: See OpenRouter models
Configuration Options
Selecting Providers
Configure which providers to use via environment variables:
# Use both providers (default)
GEMINI_API_KEY=your-gemini-key
OPENROUTER_API_KEY=your-openrouter-key
ACTIVE_PROVIDERS=openrouter,gemini
# Use only Gemini
GEMINI_API_KEY=your-gemini-key
ACTIVE_PROVIDERS=gemini
# Use only OpenRouter
OPENROUTER_API_KEY=your-openrouter-key
ACTIVE_PROVIDERS=openrouter
At least one provider must be configured. The application will log a warning if no API keys are set.
Changing Models
To use different models, modify the initialization in app/extensions.py:
# Use Gemini Pro instead of Flash
if gemini_key:
providers.append(
GeminiProvider(api_key=gemini_key, model_name="gemini-2.5-pro")
)
# Use a different OpenRouter model
if openrouter_key:
providers.append(
OpenRouterProvider(
api_key=openrouter_key,
model_name="anthropic/claude-3.5-sonnet"
)
)
Provider Priority
Providers are tried in the order they’re added to the list. To change priority, reorder the initialization:
providers = []
# Try Gemini first
if gemini_key:
providers.append(GeminiProvider(...))
# Then fall back to OpenRouter
if openrouter_key:
providers.append(OpenRouterProvider(...))
Adding Custom Providers
Create a new provider by implementing the AIProvider protocol:
Create provider class
Create a new file client/custom_provider.py:client/custom_provider.py
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
class CustomProvider:
def __init__(self, api_key: str, model_name: str = "default-model"):
if not api_key:
raise ValueError("API key is required")
self.api_key = api_key
self.model_name = model_name
# Initialize your client here
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
)
def generate_text(self, prompt: str) -> str:
# Implement your API call here
# Must return a string
response = self._call_api(prompt)
return response
def _call_api(self, prompt: str) -> str:
# Your implementation
pass
Add configuration
Add environment variable to app/config.py:class Config:
# ... existing config ...
CUSTOM_API_KEY = os.getenv("CUSTOM_API_KEY", "")
Add to .env.example: Register provider
Add initialization logic to app/extensions.py:from client.custom_provider import CustomProvider
def init_ai_providers(app):
# ... existing code ...
custom_key = app.config.get("CUSTOM_API_KEY", "")
if custom_key:
providers.append(
CustomProvider(
api_key=custom_key,
model_name="your-model-name"
)
)
Test provider
Set your API key and test:CUSTOM_API_KEY=your-key-here
python wsgi.py
The application logs will show:Initialized 3 AI provider(s)
Retry and Error Handling
All providers use the tenacity library for automatic retries:
@retry(
stop=stop_after_attempt(3), # Maximum 3 attempts
wait=wait_exponential(multiplier=1, min=2, max=10), # Exponential backoff
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
)
def generate_text(self, prompt: str) -> str:
# Implementation
Retry behavior:
- 1st attempt: Immediate
- 2nd attempt: Wait 2 seconds
- 3rd attempt: Wait 4 seconds
After 3 failures, the ProviderManager’s circuit breaker opens for 120 seconds.
Circuit Breaker Pattern
The ProviderManager implements a circuit breaker to prevent cascading failures:
client/ai_provider_manager.py
def generate_text(self, prompt):
for provider in self.providers:
if not self._is_available(provider): # Circuit open?
continue
try:
return provider.generate_text(prompt)
except Exception as e:
self.fail_count[provider] += 1
if self.fail_count[provider] >= 3:
# Open circuit for 120 seconds
self.open_until[provider] = time.time() + 120.0
States:
- Closed: Provider accepts requests (normal operation)
- Open: Provider temporarily disabled after 3 consecutive failures
- Half-open: After 120 seconds, provider accepts requests again
AI Client Layer
The AIClient class in client/ai_client.py provides high-level methods for interview features:
class AIClient:
def __init__(self, provider_manager):
self.provider_manager = provider_manager
def generate_first_question(self, cv_text, job_desc, job_title, company_name) -> str:
prompt = PromptTemplates.first_question_generation(...)
return self._generate(prompt)
def generate_followup_question(self, convo_history, cv_text, job_desc, ...) -> str:
prompt = PromptTemplates.followup_question_generation(...)
return self._generate(prompt)
def generate_feedback(self, convo_history, cv_text, job_desc, job_title) -> dict:
prompt = PromptTemplates.feedback_generation(...)
return self._parse_json(self._generate(prompt), expect_list=False)
def _generate(self, prompt: str) -> str:
try:
return self.provider_manager.generate_text(prompt)
except Exception as e:
raise AIServiceError(f"AI generation failed: {e}")
The AIClient abstracts provider details from the rest of the application.
Best Practices
- Use multiple providers: Configure both Gemini and OpenRouter for redundancy
- Monitor failures: Check application logs for provider failures
- Validate responses: All providers validate response format before returning
- Configure retries: Use
tenacity for automatic retry with backoff
- Handle empty responses: Check for empty/blocked responses before using
- Set appropriate models: Choose models based on cost/performance tradeoffs
Troubleshooting
Cause: No API keys set in .env file.
Solution: Set at least one provider API key:
GEMINI_API_KEY=your-key-here
“All providers failed” error
Cause: All configured providers are returning errors or have open circuit breakers.
Solution:
- Check API key validity
- Verify network connectivity
- Check provider service status
- Review application logs for specific error messages
- Wait 120 seconds for circuit breakers to reset
Empty or blocked responses
Cause: Provider filtered the response due to safety/content policies.
Solution:
- Review prompt content
- Try a different provider
- Adjust prompt templates to avoid policy violations
Rate limiting errors
Cause: Exceeded provider API rate limits.
Solution:
- Implement request throttling
- Use multiple providers for load distribution
- Upgrade to higher-tier API plan
- Cache responses when possible