swarmauri_llm_openrouter 0.1.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ """Provide OpenRouter image-generation support."""
2
+
3
+ import asyncio
4
+ import base64
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import Field, PrivateAttr, SecretStr
8
+ from swarmauri_base.ComponentBase import ComponentBase
9
+ from swarmauri_base.image_gens.ImageGenBase import ImageGenBase
10
+ from swarmauri_core.transports.i_http_client import IHttpClient
11
+ from swarmauri_core.transports.i_http_retry_policy import IHttpRetryPolicy
12
+ from swarmauri_provider_openrouter import (
13
+ OpenRouterModelCatalog,
14
+ OpenRouterProvider,
15
+ OpenRouterProviderPreferences,
16
+ )
17
+
18
+ from ._client import (
19
+ aclose_client,
20
+ arequest,
21
+ build_openrouter_client,
22
+ close_client,
23
+ request,
24
+ )
25
+ from ._internal.common import compact
26
+
27
+
28
+ @ComponentBase.register_type(ImageGenBase, "OpenRouterImgGenModel")
29
+ class OpenRouterImgGenModel(ImageGenBase):
30
+ """Generate images through OpenRouter's dedicated image API."""
31
+
32
+ api_key: SecretStr | None = Field(default=None, exclude=True)
33
+ name: str = "openai/gpt-image-1"
34
+ type: Literal["OpenRouterImgGenModel"] = "OpenRouterImgGenModel"
35
+ base_url: str = "https://openrouter.ai/api/v1"
36
+ site_url: str | None = None
37
+ app_name: str | None = None
38
+ timeout: float = 600.0
39
+ max_retries: int = Field(default=3, ge=1)
40
+ routing: OpenRouterProviderPreferences | None = None
41
+ allowed_models: list[str] = Field(default_factory=list)
42
+ _provider: OpenRouterProvider = PrivateAttr()
43
+ _http_client: IHttpClient = PrivateAttr()
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ provider: OpenRouterProvider
49
+ | OpenRouterProviderPreferences
50
+ | None = None,
51
+ routing: OpenRouterProviderPreferences | None = None,
52
+ http_client: IHttpClient | None = None,
53
+ retry_policy: IHttpRetryPolicy | None = None,
54
+ **data: Any,
55
+ ) -> None:
56
+ if isinstance(provider, OpenRouterProviderPreferences):
57
+ routing = provider
58
+ provider = None
59
+ if routing is not None:
60
+ data["routing"] = routing
61
+ super().__init__(**data)
62
+ self._provider = provider or OpenRouterProvider(
63
+ api_key=self.api_key,
64
+ base_url=self.base_url,
65
+ site_url=self.site_url,
66
+ app_name=self.app_name,
67
+ timeout=self.timeout,
68
+ max_retries=self.max_retries,
69
+ )
70
+ if self._provider.api_key is None:
71
+ raise ValueError(
72
+ "api_key or an authenticated provider is required"
73
+ )
74
+ self.base_url = self._provider.base_url or self.base_url
75
+ self._http_client = build_openrouter_client(
76
+ provider=self._provider,
77
+ http_client=http_client,
78
+ retry_policy=retry_policy,
79
+ )
80
+
81
+ @property
82
+ def catalog(self) -> OpenRouterModelCatalog:
83
+ return self._provider.catalog
84
+
85
+ def _decode_images(self, response: dict[str, Any]) -> list[bytes]:
86
+ images: list[bytes] = []
87
+ for item in response.get("data", []):
88
+ encoded = item.get("b64_json") or item.get("b64")
89
+ if encoded:
90
+ images.append(base64.b64decode(encoded))
91
+ elif item.get("url"):
92
+ downloaded = self._http_client.request("GET", item["url"])
93
+ downloaded.raise_for_status()
94
+ images.append(downloaded.read())
95
+ return images
96
+
97
+ async def _adecode_images(self, response: dict[str, Any]) -> list[bytes]:
98
+ images: list[bytes] = []
99
+ for item in response.get("data", []):
100
+ encoded = item.get("b64_json") or item.get("b64")
101
+ if encoded:
102
+ images.append(base64.b64decode(encoded))
103
+ elif item.get("url"):
104
+ downloaded = await self._http_client.arequest(
105
+ "GET", item["url"]
106
+ )
107
+ downloaded.raise_for_status()
108
+ images.append(await downloaded.aread())
109
+ return images
110
+
111
+ def _payload(self, prompt: str, **kwargs: Any) -> dict[str, Any]:
112
+ return compact(
113
+ {
114
+ "model": self.name,
115
+ "prompt": prompt,
116
+ "provider": self.routing.model_dump(exclude_none=True)
117
+ if self.routing
118
+ else None,
119
+ **kwargs,
120
+ }
121
+ )
122
+
123
+ def generate_image(
124
+ self, prompt: str, **kwargs: Any
125
+ ) -> bytes | list[bytes]:
126
+ response = request(
127
+ self._provider,
128
+ self._http_client,
129
+ "images.generate",
130
+ model=self.name,
131
+ json_data=self._payload(prompt, **kwargs),
132
+ )
133
+ images = self._decode_images(response.json())
134
+ return images[0] if len(images) == 1 else images
135
+
136
+ async def agenerate_image(
137
+ self, prompt: str, **kwargs: Any
138
+ ) -> bytes | list[bytes]:
139
+ response = await arequest(
140
+ self._provider,
141
+ self._http_client,
142
+ "images.generate",
143
+ model=self.name,
144
+ json_data=self._payload(prompt, **kwargs),
145
+ )
146
+ images = await self._adecode_images(response.json())
147
+ return images[0] if len(images) == 1 else images
148
+
149
+ def batch_generate(
150
+ self, prompts: list[str], **kwargs: Any
151
+ ) -> list[bytes | list[bytes]]:
152
+ return [self.generate_image(prompt, **kwargs) for prompt in prompts]
153
+
154
+ async def abatch_generate(
155
+ self, prompts: list[str], max_concurrent: int = 5, **kwargs: Any
156
+ ) -> list[bytes | list[bytes]]:
157
+ semaphore = asyncio.Semaphore(max_concurrent)
158
+
159
+ async def run(prompt: str) -> bytes | list[bytes]:
160
+ async with semaphore:
161
+ return await self.agenerate_image(prompt, **kwargs)
162
+
163
+ return await asyncio.gather(*(run(prompt) for prompt in prompts))
164
+
165
+ def close(self) -> None:
166
+ close_client(self._http_client)
167
+
168
+ async def aclose(self) -> None:
169
+ await aclose_client(self._http_client)
@@ -0,0 +1,192 @@
1
+ """Provide OpenRouter text and reasoning model support."""
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncIterator, Iterator
5
+ from typing import Any, ClassVar, Literal
6
+
7
+ from pydantic import Field, PrivateAttr, SecretStr
8
+ from swarmauri_base.ComponentBase import ComponentBase
9
+ from swarmauri_base.llms.LLMBase import LLMBase
10
+ from swarmauri_core.transports.i_http_client import IHttpClient
11
+ from swarmauri_core.transports.i_http_retry_policy import IHttpRetryPolicy
12
+ from swarmauri_provider_openrouter import (
13
+ OpenRouterModelCatalog,
14
+ OpenRouterProvider,
15
+ OpenRouterProviderPreferences,
16
+ )
17
+ from swarmauri_standard.messages.AgentMessage import AgentMessage
18
+
19
+ from ._client import (
20
+ aclose_client,
21
+ arequest,
22
+ astream_events,
23
+ build_openrouter_client,
24
+ close_client,
25
+ request,
26
+ stream_events,
27
+ )
28
+ from ._internal.common import (
29
+ add_assistant_response,
30
+ compact,
31
+ delta_text,
32
+ format_messages,
33
+ )
34
+
35
+
36
+ @ComponentBase.register_type(LLMBase, "OpenRouterModel")
37
+ class OpenRouterModel(LLMBase):
38
+ """Route text and reasoning conversations through OpenRouter."""
39
+
40
+ api_key: SecretStr | None = Field(default=None, exclude=True)
41
+ name: str = "openrouter/auto"
42
+ type: Literal["OpenRouterModel"] = "OpenRouterModel"
43
+ base_url: str = "https://openrouter.ai/api/v1"
44
+ site_url: str | None = None
45
+ app_name: str | None = None
46
+ routing: OpenRouterProviderPreferences | None = None
47
+ allowed_models: list[str] = Field(default_factory=list)
48
+ capabilities: ClassVar[frozenset[str]] = frozenset(
49
+ {"predict", "apredict", "stream", "astream", "batch", "abatch"}
50
+ )
51
+ _provider: OpenRouterProvider = PrivateAttr()
52
+ _http_client: IHttpClient = PrivateAttr()
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ provider: OpenRouterProvider
58
+ | OpenRouterProviderPreferences
59
+ | None = None,
60
+ routing: OpenRouterProviderPreferences | None = None,
61
+ http_client: IHttpClient | None = None,
62
+ retry_policy: IHttpRetryPolicy | None = None,
63
+ **data: Any,
64
+ ) -> None:
65
+ if isinstance(provider, OpenRouterProviderPreferences):
66
+ routing = provider
67
+ provider = None
68
+ if routing is not None:
69
+ data["routing"] = routing
70
+ super().__init__(**data)
71
+ self._provider = provider or OpenRouterProvider(
72
+ api_key=self.api_key,
73
+ base_url=self.base_url,
74
+ site_url=self.site_url,
75
+ app_name=self.app_name,
76
+ timeout=self.timeout,
77
+ max_retries=self.max_retries,
78
+ )
79
+ if self._provider.api_key is None:
80
+ raise ValueError(
81
+ "api_key or an authenticated provider is required"
82
+ )
83
+ self.base_url = self._provider.base_url or self.base_url
84
+ self._http_client = build_openrouter_client(
85
+ provider=self._provider,
86
+ http_client=http_client,
87
+ retry_policy=retry_policy,
88
+ )
89
+
90
+ @property
91
+ def catalog(self) -> OpenRouterModelCatalog:
92
+ return self._provider.catalog
93
+
94
+ def refresh_catalog(self) -> list[dict[str, Any]]:
95
+ response = request(self._provider, self._http_client, "models.list")
96
+ return self.catalog.load_response(response.json())
97
+
98
+ async def arefresh_catalog(self) -> list[dict[str, Any]]:
99
+ response = await arequest(
100
+ self._provider, self._http_client, "models.list"
101
+ )
102
+ return self.catalog.load_response(response.json())
103
+
104
+ def _request_headers(self) -> dict[str, str]:
105
+ config = self._provider.connection_config(
106
+ "chat.completions", model=self.name
107
+ )
108
+ return self._provider.authorize(config)
109
+
110
+ def _payload(self, conversation: Any, **kwargs: Any) -> dict[str, Any]:
111
+ return compact(
112
+ {
113
+ "model": self.name,
114
+ "messages": format_messages(conversation.history),
115
+ "provider": self.routing.model_dump(exclude_none=True)
116
+ if self.routing
117
+ else None,
118
+ **kwargs,
119
+ }
120
+ )
121
+
122
+ def predict(self, conversation: Any, **kwargs: Any) -> Any:
123
+ response = request(
124
+ self._provider,
125
+ self._http_client,
126
+ "chat.completions",
127
+ model=self.name,
128
+ json_data=self._payload(conversation, **kwargs),
129
+ )
130
+ return add_assistant_response(conversation, response.json())
131
+
132
+ async def apredict(self, conversation: Any, **kwargs: Any) -> Any:
133
+ response = await arequest(
134
+ self._provider,
135
+ self._http_client,
136
+ "chat.completions",
137
+ model=self.name,
138
+ json_data=self._payload(conversation, **kwargs),
139
+ )
140
+ return add_assistant_response(conversation, response.json())
141
+
142
+ def stream(self, conversation: Any, **kwargs: Any) -> Iterator[str]:
143
+ content: list[str] = []
144
+ payload = self._payload(conversation, stream=True, **kwargs)
145
+ for event in stream_events(
146
+ self._provider,
147
+ self._http_client,
148
+ "chat.completions",
149
+ payload,
150
+ model=self.name,
151
+ ):
152
+ if delta := delta_text(event):
153
+ content.append(delta)
154
+ yield delta
155
+ conversation.add_message(AgentMessage(content="".join(content)))
156
+
157
+ async def astream(
158
+ self, conversation: Any, **kwargs: Any
159
+ ) -> AsyncIterator[str]:
160
+ content: list[str] = []
161
+ payload = self._payload(conversation, stream=True, **kwargs)
162
+ async for event in astream_events(
163
+ self._provider,
164
+ self._http_client,
165
+ "chat.completions",
166
+ payload,
167
+ model=self.name,
168
+ ):
169
+ if delta := delta_text(event):
170
+ content.append(delta)
171
+ yield delta
172
+ conversation.add_message(AgentMessage(content="".join(content)))
173
+
174
+ def batch(self, conversations: list[Any], **kwargs: Any) -> list[Any]:
175
+ return [self.predict(item, **kwargs) for item in conversations]
176
+
177
+ async def abatch(
178
+ self, conversations: list[Any], max_concurrent: int = 5, **kwargs: Any
179
+ ) -> list[Any]:
180
+ semaphore = asyncio.Semaphore(max_concurrent)
181
+
182
+ async def run(item: Any) -> Any:
183
+ async with semaphore:
184
+ return await self.apredict(item, **kwargs)
185
+
186
+ return await asyncio.gather(*(run(item) for item in conversations))
187
+
188
+ def close(self) -> None:
189
+ close_client(self._http_client)
190
+
191
+ async def aclose(self) -> None:
192
+ await aclose_client(self._http_client)
@@ -0,0 +1,34 @@
1
+ """Retry policy for OpenRouter model operations."""
2
+
3
+ from swarmauri_core.transports.i_http_response import IHttpResponse
4
+ from swarmauri_transport_retry import ExponentialBackoffRetryPolicy
5
+
6
+
7
+ class OpenRouterRetryPolicy(ExponentialBackoffRetryPolicy):
8
+ """Retry safe reads and rate-limited model submissions."""
9
+
10
+ def __init__(self, **kwargs) -> None:
11
+ kwargs.setdefault("retryable_methods", {"GET", "POST"})
12
+ super().__init__(**kwargs)
13
+
14
+ def should_retry(
15
+ self,
16
+ *,
17
+ method: str,
18
+ attempt: int,
19
+ response: IHttpResponse | None = None,
20
+ error: Exception | None = None,
21
+ ) -> bool:
22
+ if method.upper() == "POST":
23
+ return (
24
+ attempt < self.max_attempts
25
+ and error is None
26
+ and response is not None
27
+ and response.status_code == 429
28
+ )
29
+ return super().should_retry(
30
+ method=method,
31
+ attempt=attempt,
32
+ response=response,
33
+ error=error,
34
+ )