From 4f7fc99423d4830a949a0eb0d6bedff1506896ed Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 12 Jun 2026 17:29:12 +0200 Subject: [PATCH 1/2] feat(text): add OpenRouter provider via OpenResponses Register Provider.OPENROUTER with env-based auth and a minimal OpenResponsesTextClient subclass so users can call any OpenRouter model slug without a curated catalog. Co-authored-by: Cursor --- src/celeste/core.py | 1 + src/celeste/credentials.py | 1 + src/celeste/modalities/text/models.py | 2 ++ .../modalities/text/providers/__init__.py | 2 ++ .../text/providers/openrouter/__init__.py | 6 ++++ .../text/providers/openrouter/client.py | 33 +++++++++++++++++++ .../text/providers/openrouter/models.py | 5 +++ src/celeste/providers/__init__.py | 2 ++ src/celeste/providers/openrouter/__init__.py | 11 +++++++ src/celeste/providers/openrouter/config.py | 3 ++ src/celeste/telemetry.py | 1 + .../unit_tests/test_provider_api_templates.py | 2 +- 12 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/celeste/modalities/text/providers/openrouter/__init__.py create mode 100644 src/celeste/modalities/text/providers/openrouter/client.py create mode 100644 src/celeste/modalities/text/providers/openrouter/models.py create mode 100644 src/celeste/providers/openrouter/__init__.py create mode 100644 src/celeste/providers/openrouter/config.py diff --git a/src/celeste/core.py b/src/celeste/core.py index 79e62e0f..2eb8e21b 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 9c315410..163d6990 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 04256a76..4b5b328d 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 071e1bc0..3d14b216 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 00000000..89357ddc --- /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 00000000..e98b0ad4 --- /dev/null +++ b/src/celeste/modalities/text/providers/openrouter/client.py @@ -0,0 +1,33 @@ +"""OpenRouter text client (OpenResponses protocol).""" + +import os +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 + + def _json_headers( + self, extra_headers: dict[str, str] | None = None + ) -> dict[str, str]: + headers = super()._json_headers(extra_headers) + referer = os.environ.get("OPENROUTER_HTTP_REFERER") + if referer: + headers["HTTP-Referer"] = referer + title = os.environ.get("OPENROUTER_APP_TITLE") + if title: + headers["X-OpenRouter-Title"] = title + return headers + + +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 00000000..d46bda46 --- /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 e9cbafc2..fb925114 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 00000000..eb10ce4c --- /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 00000000..c1493442 --- /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 bd3e9297..1230c08d 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 818f630d..241e0b0a 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()]): From 9c9a2052ac4e78a94438ffe22829d4e4cb420a46 Mon Sep 17 00:00:00 2001 From: kamilbenkirane Date: Fri, 12 Jun 2026 17:33:26 +0200 Subject: [PATCH 2/2] refactor(openrouter): drop env-based attribution headers Use extra_headers at call time instead of reading OPENROUTER_HTTP_REFERER and OPENROUTER_APP_TITLE via os.environ, matching other providers. Co-authored-by: Cursor --- .../modalities/text/providers/openrouter/client.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/celeste/modalities/text/providers/openrouter/client.py b/src/celeste/modalities/text/providers/openrouter/client.py index e98b0ad4..40802cf8 100644 --- a/src/celeste/modalities/text/providers/openrouter/client.py +++ b/src/celeste/modalities/text/providers/openrouter/client.py @@ -1,6 +1,5 @@ """OpenRouter text client (OpenResponses protocol).""" -import os from typing import ClassVar from celeste.modalities.text.protocols.openresponses.client import ( @@ -15,18 +14,6 @@ class OpenRouterTextClient(OpenResponsesTextClient): _default_base_url: ClassVar[str] = DEFAULT_BASE_URL - def _json_headers( - self, extra_headers: dict[str, str] | None = None - ) -> dict[str, str]: - headers = super()._json_headers(extra_headers) - referer = os.environ.get("OPENROUTER_HTTP_REFERER") - if referer: - headers["HTTP-Referer"] = referer - title = os.environ.get("OPENROUTER_APP_TITLE") - if title: - headers["X-OpenRouter-Title"] = title - return headers - OpenRouterTextStream = OpenResponsesTextStream