diff --git a/src/celeste/core.py b/src/celeste/core.py index 79e62e0..2eb8e21 100644 --- a/src/celeste/core.py +++ b/src/celeste/core.py @@ -26,6 +26,7 @@ class Provider(StrEnum): GROQ = "groq" GRADIUM = "gradium" OLLAMA = "ollama" + OPENROUTER = "openrouter" class Protocol(StrEnum): diff --git a/src/celeste/credentials.py b/src/celeste/credentials.py index 9c31541..163d699 100644 --- a/src/celeste/credentials.py +++ b/src/celeste/credentials.py @@ -75,6 +75,7 @@ class Credentials(BaseSettings): bfl_api_key: SecretStr | None = Field(None, alias="BFL_API_KEY") groq_api_key: SecretStr | None = Field(None, alias="GROQ_API_KEY") gradium_api_key: SecretStr | None = Field(None, alias="GRADIUM_API_KEY") + openrouter_api_key: SecretStr | None = Field(None, alias="OPENROUTER_API_KEY") model_config = { "env_file": find_dotenv(), diff --git a/src/celeste/modalities/text/models.py b/src/celeste/modalities/text/models.py index 04256a7..4b5b328 100644 --- a/src/celeste/modalities/text/models.py +++ b/src/celeste/modalities/text/models.py @@ -12,6 +12,7 @@ from .providers.moonshot.models import MODELS as MOONSHOT_MODELS from .providers.ollama.models import MODELS as OLLAMA_MODELS from .providers.openai.models import MODELS as OPENAI_MODELS +from .providers.openrouter.models import MODELS as OPENROUTER_MODELS from .providers.xai.models import MODELS as XAI_MODELS MODELS: list[Model] = [ @@ -25,5 +26,6 @@ *MISTRAL_MODELS, *MOONSHOT_MODELS, *OPENAI_MODELS, + *OPENROUTER_MODELS, *XAI_MODELS, ] diff --git a/src/celeste/modalities/text/providers/__init__.py b/src/celeste/modalities/text/providers/__init__.py index 071e1bc..3d14b21 100644 --- a/src/celeste/modalities/text/providers/__init__.py +++ b/src/celeste/modalities/text/providers/__init__.py @@ -13,6 +13,7 @@ from .moonshot import MoonshotTextClient from .ollama import OllamaTextClient from .openai import OpenAITextClient +from .openrouter import OpenRouterTextClient from .xai import XAITextClient PROVIDERS: dict[Provider, type[TextClient]] = { @@ -26,5 +27,6 @@ Provider.MISTRAL: MistralTextClient, Provider.MOONSHOT: MoonshotTextClient, Provider.OPENAI: OpenAITextClient, + Provider.OPENROUTER: OpenRouterTextClient, Provider.XAI: XAITextClient, } diff --git a/src/celeste/modalities/text/providers/openrouter/__init__.py b/src/celeste/modalities/text/providers/openrouter/__init__.py new file mode 100644 index 0000000..89357dd --- /dev/null +++ b/src/celeste/modalities/text/providers/openrouter/__init__.py @@ -0,0 +1,6 @@ +"""OpenRouter provider for text modality.""" + +from .client import OpenRouterTextClient +from .models import MODELS + +__all__ = ["MODELS", "OpenRouterTextClient"] diff --git a/src/celeste/modalities/text/providers/openrouter/client.py b/src/celeste/modalities/text/providers/openrouter/client.py new file mode 100644 index 0000000..40802cf --- /dev/null +++ b/src/celeste/modalities/text/providers/openrouter/client.py @@ -0,0 +1,20 @@ +"""OpenRouter text client (OpenResponses protocol).""" + +from typing import ClassVar + +from celeste.modalities.text.protocols.openresponses.client import ( + OpenResponsesTextClient, + OpenResponsesTextStream, +) +from celeste.providers.openrouter.config import DEFAULT_BASE_URL + + +class OpenRouterTextClient(OpenResponsesTextClient): + """OpenRouter — OpenResponses with default https://openrouter.ai/api.""" + + _default_base_url: ClassVar[str] = DEFAULT_BASE_URL + + +OpenRouterTextStream = OpenResponsesTextStream + +__all__ = ["OpenRouterTextClient", "OpenRouterTextStream"] diff --git a/src/celeste/modalities/text/providers/openrouter/models.py b/src/celeste/modalities/text/providers/openrouter/models.py new file mode 100644 index 0000000..d46bda4 --- /dev/null +++ b/src/celeste/modalities/text/providers/openrouter/models.py @@ -0,0 +1,5 @@ +"""OpenRouter models for text modality.""" + +from celeste.models import Model + +MODELS: list[Model] = [] diff --git a/src/celeste/providers/__init__.py b/src/celeste/providers/__init__.py index e9cbafc..fb92511 100644 --- a/src/celeste/providers/__init__.py +++ b/src/celeste/providers/__init__.py @@ -15,6 +15,7 @@ moonshot, ollama, openai, + openrouter, xai, ) @@ -33,5 +34,6 @@ "moonshot", "ollama", "openai", + "openrouter", "xai", ] diff --git a/src/celeste/providers/openrouter/__init__.py b/src/celeste/providers/openrouter/__init__.py new file mode 100644 index 0000000..eb10ce4 --- /dev/null +++ b/src/celeste/providers/openrouter/__init__.py @@ -0,0 +1,11 @@ +"""OpenRouter provider package for Celeste AI.""" + +from celeste.core import Provider +from celeste.credentials import register_auth + +register_auth( # nosec B106 - env var name, not hardcoded password + provider=Provider.OPENROUTER, + secret_name="OPENROUTER_API_KEY", + header="Authorization", + prefix="Bearer ", +) diff --git a/src/celeste/providers/openrouter/config.py b/src/celeste/providers/openrouter/config.py new file mode 100644 index 0000000..c149344 --- /dev/null +++ b/src/celeste/providers/openrouter/config.py @@ -0,0 +1,3 @@ +"""OpenRouter configuration.""" + +DEFAULT_BASE_URL = "https://openrouter.ai/api" diff --git a/src/celeste/telemetry.py b/src/celeste/telemetry.py index bd3e929..1230c08 100644 --- a/src/celeste/telemetry.py +++ b/src/celeste/telemetry.py @@ -37,6 +37,7 @@ Provider.XAI: "x_ai", Provider.GROQ: "groq", Provider.DEEPSEEK: "deepseek", + Provider.OPENROUTER: "openrouter", Provider.PERPLEXITY: "perplexity", Provider.COHERE: "cohere", } diff --git a/tests/unit_tests/test_provider_api_templates.py b/tests/unit_tests/test_provider_api_templates.py index 818f630..241e0b0 100644 --- a/tests/unit_tests/test_provider_api_templates.py +++ b/tests/unit_tests/test_provider_api_templates.py @@ -93,7 +93,7 @@ def _provider_api_client_files() -> list[Path]: # Wrapper providers that just re-export another provider's client # These don't need to match the template contract - wrapper_providers = {"ollama"} + wrapper_providers = {"ollama", "openrouter"} out: list[Path] = [] for provider_dir in sorted([p for p in providers_dir.iterdir() if p.is_dir()]):