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.
- swarmauri_llm_openrouter/OpenRouterImgGenModel.py +169 -0
- swarmauri_llm_openrouter/OpenRouterModel.py +192 -0
- swarmauri_llm_openrouter/OpenRouterRetryPolicy.py +34 -0
- swarmauri_llm_openrouter/OpenRouterToolModel.py +354 -0
- swarmauri_llm_openrouter/OpenRouterVLM.py +144 -0
- swarmauri_llm_openrouter/__init__.py +31 -0
- swarmauri_llm_openrouter/_client.py +126 -0
- swarmauri_llm_openrouter/_internal/__init__.py +1 -0
- swarmauri_llm_openrouter/_internal/common.py +54 -0
- swarmauri_llm_openrouter-0.1.0.dev3.dist-info/METADATA +161 -0
- swarmauri_llm_openrouter-0.1.0.dev3.dist-info/RECORD +14 -0
- swarmauri_llm_openrouter-0.1.0.dev3.dist-info/WHEEL +4 -0
- swarmauri_llm_openrouter-0.1.0.dev3.dist-info/entry_points.txt +12 -0
- swarmauri_llm_openrouter-0.1.0.dev3.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|
+
)
|