A comprehensive Python library for working with OpenRouter models, providing utilities for filtering, analyzing, and prompt management. This companion library works with the OpenRouter Python client to offer:
- Model Filtering & Analysis: Find models by capabilities, pricing, and features
- Prompt Management: Template-based prompt system with variable substitution
- Quality Filtering: Automatically exclude problematic model variants
pip install -e .Or install just the dependencies:
pip install -r requirements.txt- Python 3.9+
- OpenRouter API key (set as
OPENROUTER_API_KEYenvironment variable) - The OpenRouter Python client (automatically installed from GitHub)
from openrouter_client import OpenRouterClient
from openrouter_companion import ModelFilter, ModelCapability, SortOrder
# Initialize OpenRouter client with your API key
client = OpenRouterClient(api_key="your-openrouter-api-key-here")
# Initialize filter with the client
filter_obj = ModelFilter(client=client)
# Find models that support structured output
models = filter_obj.filter_models(
capabilities=ModelCapability.STRUCTURED_OUTPUT,
sort_order=SortOrder.PRICE_ASC
)
# Find models with multiple capabilities
multimodal_models = filter_obj.filter_models(
capabilities=ModelCapability.IMAGE_INPUT | ModelCapability.STRUCTURED_OUTPUT,
sort_order=SortOrder.PRICE_ASC
)
# Access model information
for model in models[:5]: # Top 5 cheapest
print(f"{model.id}: ${model.get_pricing_per_1m_tokens():.2f}/1M tokens")from openrouter_companion import Prompt, StringTemplatePrompt, FileTemplatePrompt
# Simple string template with variable substitution
template = StringTemplatePrompt("Hello $name! Help me with $task.")
prompt_text = template.render(name="Alice", task="Python debugging")
# Load prompts from files
file_prompt = FileTemplatePrompt("path/to/template.txt")
content = file_prompt.render()
# Custom prompt class
class CustomPrompt(Prompt):
def _render_content(self, **kwargs):
return f"Custom logic for {kwargs.get('topic', 'general')} assistance"
custom = CustomPrompt()
result = custom.render(topic="API integration")The main filtering interface that queries OpenRouter's API and applies filters:
from openrouter_companion import ModelFilter
# Basic usage
filter_obj = ModelFilter()
# With custom API key or client
filter_obj = ModelFilter(api_key="your-key")
filter_obj = ModelFilter(client=your_openrouter_client)Capability flags that can be combined using the | operator:
ModelCapability.IMAGE_INPUT- Models that support image/multimodal inputModelCapability.STRUCTURED_OUTPUT- Models that support structured output (response_format)ModelCapability.REASONING- Models that support reasoning/thinking modeModelCapability.MULTIMODAL- Alias for IMAGE_INPUTModelCapability.ALL- All capabilities combined
Sort options for filtering results:
SortOrder.PRICE_ASC- Cheapest firstSortOrder.PRICE_DESC- Most expensive firstSortOrder.NAME_ASC/NAME_DESC- Alphabetical sortingSortOrder.CONTEXT_ASC/CONTEXT_DESC- By context lengthSortOrder.NONE- No sorting
Enhanced model wrapper with utility methods:
# Pricing information
model.get_pricing_per_1m_tokens() # Price per million tokens
model.is_free() # True if free model
model.has_pricing() # True if pricing info available
model.get_image_pricing() # Price per image (if applicable)
# Capability checks
model.supports_images() # True if supports image input
model.supports_structured_output() # True if supports structured output
model.supports_reasoning() # True if supports reasoning/thinking modeAbstract base class for all prompts using the template method pattern:
from openrouter_companion import Prompt
class CustomPrompt(Prompt):
def _render_content(self, **kwargs):
# Implement your prompt logic here
return "Your prompt content"
def get_required_params(self):
return ["param1", "param2"]
def validate_params(self, **kwargs):
# Custom validation logic
passTemplate-based prompts with $variable substitution:
from openrouter_companion import StringTemplatePrompt
# Create template with variables
template = StringTemplatePrompt("Help $name with $task using $style approach.")
# Render with variables
result = template.render(name="Alice", task="debugging", style="systematic")
# Safe render (leaves missing variables as $var)
partial = template.safe_render(name="Bob")
# Get template variables
variables = template.get_template_variables() # ['name', 'task', 'style']Load prompts from external files:
from openrouter_companion import FileTemplatePrompt
# Load from file
file_prompt = FileTemplatePrompt("templates/system_prompt.txt")
content = file_prompt.render()
# Dynamic template path
file_prompt.set_template_path("other_template.txt")# Find image-capable models
image_models = filter_obj.filter_models(
capabilities=ModelCapability.IMAGE_INPUT,
include_deprecated=False,
sort_order=SortOrder.PRICE_ASC
)# Find models with both image input AND structured output
advanced_models = filter_obj.filter_models(
capabilities=ModelCapability.IMAGE_INPUT | ModelCapability.STRUCTURED_OUTPUT,
sort_order=SortOrder.PRICE_ASC
)
# Find reasoning models (thinking/chain-of-thought capable)
reasoning_models = filter_obj.filter_models(
capabilities=ModelCapability.REASONING,
sort_order=SortOrder.PRICE_ASC
)
# Find models with reasoning AND structured output
smart_models = filter_obj.filter_models(
capabilities=ModelCapability.REASONING | ModelCapability.STRUCTURED_OUTPUT,
sort_order=SortOrder.PRICE_ASC
)# Include deprecated models and sort by context length
all_models = filter_obj.filter_models(
capabilities=ModelCapability.ALL,
include_deprecated=True,
sort_order=SortOrder.CONTEXT_DESC
)models = filter_obj.filter_models(
capabilities=ModelCapability.STRUCTURED_OUTPUT,
sort_order=SortOrder.PRICE_ASC
)
# Find free models
free_models = [m for m in models if m.is_free()]
print(f"Found {len(free_models)} free models")
# Find cheapest paid model
paid_models = [m for m in models if m.has_pricing() and not m.is_free()]
if paid_models:
cheapest = min(paid_models, key=lambda m: m.get_pricing_per_1m_tokens())
print(f"Cheapest paid: {cheapest.id} at ${cheapest.get_pricing_per_1m_tokens():.2f}/1M tokens")from openrouter_companion import Prompt
class ErrorCorrectionPrompt(Prompt):
"""Prompt for providing error feedback during iterative generation."""
def _render_content(self, **kwargs):
self.validate_params(**kwargs)
last_error = kwargs.get('last_error')
return f"""The previous strategy had issues. Please fix them.
ERRORS FOUND:
{last_error}
Please provide a corrected version."""
def validate_params(self, **kwargs):
last_error = kwargs.get('last_error')
if not last_error or not isinstance(last_error, str):
raise ValueError("last_error is required and must be a string")
def get_required_params(self):
return ['last_error']
# Usage
error_prompt = ErrorCorrectionPrompt()
result = error_prompt.render(last_error="Variable x is undefined on line 42")# Create a template file: templates/code_review.txt
"""
You are a senior software engineer reviewing code.
Task: {task}
Language: {language}
Experience Level: {experience}
Please provide {style} feedback focusing on:
1. Code quality and best practices
2. Potential bugs or issues
3. Performance optimizations
4. Security considerations
Code to review:
{code}
"""
# Use with FileTemplatePrompt (no variable substitution)
file_prompt = FileTemplatePrompt("templates/code_review.txt")
template_content = file_prompt.render()
# Use with StringTemplatePrompt for variable substitution
with open("templates/code_review.txt") as f:
content = f.read().replace("{", "$").replace("}", "") # Convert to $var format
string_prompt = StringTemplatePrompt(content)
review_prompt = string_prompt.render(
task="API security review",
language="Python",
experience="senior",
style="detailed",
code="def authenticate(token): return True"
)def filter_models(
capabilities: ModelCapability = ModelCapability.NONE,
include_deprecated: bool = False,
include_problematic_variants: bool = False,
sort_order: SortOrder = SortOrder.NONE
) -> List[ModelInfo]:Parameters:
capabilities: Capability flags to filter by (can be combined with|)include_deprecated: Whether to include deprecated modelsinclude_problematic_variants: Whether to include models with canonical slug mismatchessort_order: How to sort the results
Returns: List of ModelInfo objects matching the criteria
get_pricing_per_1m_tokens() -> float: Price per million tokens (0 for free, inf for no pricing)is_free() -> bool: True if the model is free to usehas_pricing() -> bool: True if pricing information is availableget_image_pricing() -> Optional[float]: Price per image for multimodal modelssupports_images() -> bool: True if model supports image inputsupports_structured_output() -> bool: True if model supports structured outputsupports_reasoning() -> bool: True if model supports reasoning/thinking modehas_canonical_slug_mismatch() -> bool: True if ID != canonical_slug (indicates problematic variant)
class Prompt(ABC):
def render(**kwargs) -> str: # Main rendering method
def _render_content(**kwargs) -> str: # Abstract method to implement
def get_prompt_suffix() -> str: # Optional suffix content
def validate_params(**kwargs) -> None: # Parameter validation
def get_required_params() -> list[str]: # Required parameter names
def get_optional_params() -> list[str]: # Optional parameter names
def get_schema() -> dict | None: # JSON schema for structured outputclass StringTemplatePrompt(Prompt):
def __init__(template_string: str)
def safe_render(**kwargs) -> str: # Safe rendering with partial substitution
def get_template_variables() -> list[str]: # Get template variablesclass FileTemplatePrompt(Prompt):
def __init__(template_path: str)
def get_template_path() -> str: # Get current template path
def set_template_path(path: str) -> None: # Set new template pathThe library automatically handles OpenRouter API authentication through:
- Environment variable:
OPENROUTER_API_KEY - Custom API key passed to ModelFilter constructor
- Pre-configured OpenRouterClient instance
The library includes robust error handling for:
- Missing API keys
- Network failures
- Invalid model data
- API rate limits
Errors are wrapped in clear RuntimeError exceptions with descriptive messages.
This project is developed by @jmazzahacks. Contributions are welcome!
MIT License - see the project repository for details.