python-open-router 0.3.3__py3-none-any.whl → 0.4.0__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.
@@ -6,34 +6,48 @@ from python_open_router.exceptions import (
6
6
  OpenRouterError,
7
7
  )
8
8
  from python_open_router.models import (
9
- CreatedKey,
10
- CreateKeyDataWrapper,
9
+ DefaultParameters,
10
+ DeletedKeyResponse,
11
11
  Key,
12
+ KeyCreateResponse,
12
13
  KeyData,
13
14
  KeyDataWrapper,
15
+ KeyResponseWrapper,
14
16
  KeysDataWrapper,
15
17
  Modality,
16
18
  Model,
17
19
  ModelArchitecture,
20
+ ModelLinks,
21
+ ModelResponseWrapper,
18
22
  ModelsDataWrapper,
23
+ PerRequestLimits,
24
+ PublicPricing,
19
25
  SupportedParameter,
26
+ TopProviderInfo,
20
27
  )
21
28
  from python_open_router.open_router import OpenRouterClient
22
29
 
23
30
  __all__ = [
24
- "CreateKeyDataWrapper",
25
- "CreatedKey",
31
+ "DefaultParameters",
32
+ "DeletedKeyResponse",
26
33
  "Key",
34
+ "KeyCreateResponse",
27
35
  "KeyData",
28
36
  "KeyDataWrapper",
37
+ "KeyResponseWrapper",
29
38
  "KeysDataWrapper",
30
39
  "Modality",
31
40
  "Model",
32
41
  "ModelArchitecture",
42
+ "ModelLinks",
43
+ "ModelResponseWrapper",
33
44
  "ModelsDataWrapper",
34
45
  "OpenRouterAuthenticationError",
35
46
  "OpenRouterClient",
36
47
  "OpenRouterConnectionError",
37
48
  "OpenRouterError",
49
+ "PerRequestLimits",
50
+ "PublicPricing",
38
51
  "SupportedParameter",
52
+ "TopProviderInfo",
39
53
  ]
@@ -24,10 +24,23 @@ class KeyData(DataClassORJSONMixin):
24
24
  """The OpenRouter key data."""
25
25
 
26
26
  label: str
27
- usage: int
27
+ usage: float
28
28
  is_provisioning_key: bool
29
- limit_remaining: int | None
29
+ limit_remaining: float | None
30
30
  is_free_tier: bool
31
+ limit: float | None = None
32
+ limit_reset: str | None = None
33
+ usage_daily: float = 0.0
34
+ usage_weekly: float = 0.0
35
+ usage_monthly: float = 0.0
36
+ byok_usage: float = 0.0
37
+ byok_usage_daily: float = 0.0
38
+ byok_usage_weekly: float = 0.0
39
+ byok_usage_monthly: float = 0.0
40
+ include_byok_in_limit: bool = False
41
+ is_management_key: bool = False
42
+ creator_user_id: str | None = None
43
+ expires_at: str | None = None
31
44
 
32
45
 
33
46
  @dataclass
@@ -38,10 +51,11 @@ class KeysDataWrapper(DataClassORJSONMixin):
38
51
 
39
52
 
40
53
  @dataclass
41
- class CreateKeyDataWrapper(DataClassORJSONMixin):
42
- """Wrapper for OpenRouter key data."""
54
+ class KeyCreateResponse(DataClassORJSONMixin):
55
+ """Response from creating a key."""
43
56
 
44
- data: CreatedKey
57
+ data: Key
58
+ key: str
45
59
 
46
60
 
47
61
  @dataclass
@@ -52,15 +66,44 @@ class Key(DataClassORJSONMixin):
52
66
  name: str
53
67
  label: str
54
68
  disabled: bool
55
- limit: float
69
+ limit: float | None = None
56
70
  usage: float = 0.0
71
+ created_at: str = ""
72
+ updated_at: str | None = None
73
+ limit_remaining: float | None = None
74
+ limit_reset: str | None = None
75
+ workspace_id: str = ""
76
+ expires_at: str | None = None
77
+ usage_daily: float = 0.0
78
+ usage_weekly: float = 0.0
79
+ usage_monthly: float = 0.0
80
+ byok_usage: float = 0.0
81
+ byok_usage_daily: float = 0.0
82
+ byok_usage_weekly: float = 0.0
83
+ byok_usage_monthly: float = 0.0
84
+ include_byok_in_limit: bool = False
85
+ creator_user_id: str | None = None
57
86
 
58
87
 
59
- @dataclass(kw_only=True)
60
- class CreatedKey(Key):
61
- """Created key."""
88
+ @dataclass
89
+ class KeyResponseWrapper(DataClassORJSONMixin):
90
+ """Wrapper for single key response."""
91
+
92
+ data: Key
93
+
94
+
95
+ @dataclass
96
+ class DeletedKeyResponse(DataClassORJSONMixin):
97
+ """Response after deleting a key."""
98
+
99
+ deleted: bool
62
100
 
63
- key: str
101
+
102
+ @dataclass
103
+ class ModelResponseWrapper(DataClassORJSONMixin):
104
+ """Wrapper for single OpenRouter model response."""
105
+
106
+ data: Model
64
107
 
65
108
 
66
109
  @dataclass
@@ -75,25 +118,29 @@ class SupportedParameter(StrEnum):
75
118
 
76
119
  TEMPERATURE = "temperature"
77
120
  TOP_P = "top_p"
121
+ TOP_K = "top_k"
78
122
  MAX_TOKENS = "max_tokens"
123
+ MAX_COMPLETION_TOKENS = "max_completion_tokens"
79
124
  REASONING = "reasoning"
125
+ REASONING_EFFORT = "reasoning_effort"
80
126
  INCLUDE_REASONING = "include_reasoning"
81
127
  STOP = "stop"
82
- TOK_K = "top_k"
83
128
  SEED = "seed"
84
129
  RESPONSE_FORMAT = "response_format"
130
+ STRUCTURED_OUTPUTS = "structured_outputs"
85
131
  TOOLS = "tools"
86
132
  TOOL_CHOICE = "tool_choice"
133
+ PARALLEL_TOOL_CALLS = "parallel_tool_calls"
87
134
  FREQUENCY_PENALTY = "frequency_penalty"
88
135
  PRESENCE_PENALTY = "presence_penalty"
89
- MIN_P = "min_p"
90
136
  REPETITION_PENALTY = "repetition_penalty"
137
+ MIN_P = "min_p"
138
+ TOP_A = "top_a"
91
139
  LOGPROBS = "logprobs"
92
140
  LOGIT_BIAS = "logit_bias"
93
141
  TOP_LOGPROBS = "top_logprobs"
94
- STRUCTURED_OUTPUTS = "structured_outputs"
95
142
  WEB_SEARCH_OPTIONS = "web_search_options"
96
- TOP_A = "top_a"
143
+ VERBOSITY = "verbosity"
97
144
 
98
145
 
99
146
  class Modality(StrEnum):
@@ -105,14 +152,77 @@ class Modality(StrEnum):
105
152
  FILE = "file"
106
153
  EMBEDDINGS = "embeddings"
107
154
  VIDEO = "video"
155
+ RERANK = "rerank"
156
+ SPEECH = "speech"
157
+ TRANSCRIPTION = "transcription"
108
158
 
109
159
 
110
160
  @dataclass(kw_only=True)
111
161
  class ModelArchitecture(DataClassORJSONMixin):
112
- """Model data."""
162
+ """Model architecture data."""
113
163
 
114
164
  input_modalities: list[Modality]
115
165
  output_modalities: list[Modality]
166
+ modality: str | None = None
167
+ instruct_type: str | None = None
168
+ tokenizer: str | None = None
169
+
170
+
171
+ @dataclass(kw_only=True)
172
+ class PublicPricing(DataClassORJSONMixin):
173
+ """Model pricing data."""
174
+
175
+ completion: str
176
+ prompt: str
177
+ audio: str | None = None
178
+ audio_output: str | None = None
179
+ image: str | None = None
180
+ image_output: str | None = None
181
+ image_token: str | None = None
182
+ input_audio_cache: str | None = None
183
+ input_cache_read: str | None = None
184
+ input_cache_write: str | None = None
185
+ input_cache_write_1h: str | None = None
186
+ internal_reasoning: str | None = None
187
+ request: str | None = None
188
+ web_search: str | None = None
189
+ discount: float | None = None
190
+
191
+
192
+ @dataclass(kw_only=True)
193
+ class TopProviderInfo(DataClassORJSONMixin):
194
+ """Top provider information."""
195
+
196
+ is_moderated: bool
197
+ context_length: int | None = None
198
+ max_completion_tokens: int | None = None
199
+
200
+
201
+ @dataclass(kw_only=True)
202
+ class DefaultParameters(DataClassORJSONMixin):
203
+ """Default model parameters."""
204
+
205
+ temperature: float | None = None
206
+ top_p: float | None = None
207
+ top_k: int | None = None
208
+ frequency_penalty: float | None = None
209
+ presence_penalty: float | None = None
210
+ repetition_penalty: float | None = None
211
+
212
+
213
+ @dataclass(kw_only=True)
214
+ class PerRequestLimits(DataClassORJSONMixin):
215
+ """Per-request limits."""
216
+
217
+ completion_tokens: float
218
+ prompt_tokens: float
219
+
220
+
221
+ @dataclass(kw_only=True)
222
+ class ModelLinks(DataClassORJSONMixin):
223
+ """Model links."""
224
+
225
+ details: str
116
226
 
117
227
 
118
228
  @dataclass(kw_only=True)
@@ -121,24 +231,34 @@ class Model(DataClassORJSONMixin):
121
231
 
122
232
  id: str
123
233
  canonical_slug: str
124
- hugging_face_id: str | None
125
234
  name: str
126
- description: str
127
- context_length: int
235
+ context_length: int | None
128
236
  architecture: ModelArchitecture
129
237
  supported_parameters: list[SupportedParameter]
238
+ pricing: PublicPricing
239
+ top_provider: TopProviderInfo
240
+ hugging_face_id: str | None = None
241
+ description: str | None = None
242
+ created: int | None = None
243
+ default_parameters: DefaultParameters | None = None
244
+ links: ModelLinks | None = None
245
+ per_request_limits: PerRequestLimits | None = None
246
+ supported_voices: list[str] | None = None
247
+ expiration_date: str | None = None
248
+ knowledge_cutoff: str | None = None
130
249
 
131
250
  @classmethod
132
251
  def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
133
252
  """Pre deserialize hook."""
134
253
  parameters = d.get("supported_parameters", [])
254
+ supported = set(SupportedParameter)
255
+ d["supported_parameters"] = [p for p in parameters if p in supported]
135
256
  for parameter in parameters:
136
- if parameter not in SupportedParameter:
257
+ if parameter not in supported:
137
258
  _LOGGER.warning(
138
259
  "Unsupported parameter: %s. Please report at https://github.com/joostlek/python-open-router/issues.",
139
260
  parameter,
140
261
  )
141
- parameters.remove(parameter)
142
262
  return d
143
263
 
144
264
  @classmethod
@@ -146,4 +266,6 @@ class Model(DataClassORJSONMixin):
146
266
  """Post deserialize hook."""
147
267
  if obj.hugging_face_id == "":
148
268
  obj.hugging_face_id = None
269
+ if obj.description == "":
270
+ obj.description = None
149
271
  return obj
@@ -9,17 +9,24 @@ import socket
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from aiohttp import ClientError, ClientResponseError, ClientSession
12
- from aiohttp.hdrs import METH_GET, METH_POST
12
+ from aiohttp.hdrs import METH_DELETE, METH_GET, METH_PATCH, METH_POST
13
+ import orjson
13
14
  from yarl import URL
14
15
 
15
- from python_open_router.exceptions import OpenRouterConnectionError
16
+ from python_open_router.exceptions import (
17
+ OpenRouterAuthenticationError,
18
+ OpenRouterConnectionError,
19
+ )
16
20
  from python_open_router.models import (
17
- CreateKeyDataWrapper,
21
+ DeletedKeyResponse,
18
22
  Key,
23
+ KeyCreateResponse,
19
24
  KeyData,
20
25
  KeyDataWrapper,
26
+ KeyResponseWrapper,
21
27
  KeysDataWrapper,
22
28
  Model,
29
+ ModelResponseWrapper,
23
30
  ModelsDataWrapper,
24
31
  )
25
32
 
@@ -81,6 +88,10 @@ class OpenRouterClient:
81
88
  msg = "Error occurred while communicating with the service"
82
89
  raise OpenRouterConnectionError(msg) from exception
83
90
 
91
+ if response.status == 401:
92
+ msg = "Authentication failed"
93
+ raise OpenRouterAuthenticationError(msg)
94
+
84
95
  if response.status >= 400:
85
96
  content_type = response.headers.get("Content-Type", "")
86
97
  text = await response.text()
@@ -97,25 +108,162 @@ class OpenRouterClient:
97
108
  response = await self._request(METH_GET, "key")
98
109
  return KeyDataWrapper.from_json(response).data
99
110
 
100
- async def get_keys(self) -> list[Key]:
111
+ async def get_keys(
112
+ self,
113
+ *,
114
+ include_disabled: bool | None = None,
115
+ offset: int | None = None,
116
+ workspace_id: str | None = None,
117
+ ) -> list[Key]:
101
118
  """Get all keys."""
102
- response = await self._request(METH_GET, "keys")
119
+ params: dict[str, Any] = {}
120
+ if include_disabled is not None:
121
+ params["include_disabled"] = str(include_disabled).lower()
122
+ if offset is not None:
123
+ params["offset"] = str(offset)
124
+ if workspace_id is not None:
125
+ params["workspace_id"] = workspace_id
126
+ response = await self._request(METH_GET, "keys", params=params or None)
103
127
  return KeysDataWrapper.from_json(response).data
104
128
 
105
- async def create_key(self, name: str, limit: float | None = None) -> Key:
129
+ async def create_key(
130
+ self,
131
+ name: str,
132
+ limit: float | None = None,
133
+ *,
134
+ limit_reset: str | None = None,
135
+ workspace_id: str | None = None,
136
+ expires_at: str | None = None,
137
+ include_byok_in_limit: bool | None = None,
138
+ ) -> Key:
106
139
  """Create a new key."""
107
140
  data: dict[str, Any] = {"name": name}
108
141
  if limit is not None:
109
142
  data["limit"] = limit
143
+ if limit_reset is not None:
144
+ data["limit_reset"] = limit_reset
145
+ if workspace_id is not None:
146
+ data["workspace_id"] = workspace_id
147
+ if expires_at is not None:
148
+ data["expires_at"] = expires_at
149
+ if include_byok_in_limit is not None:
150
+ data["include_byok_in_limit"] = include_byok_in_limit
110
151
 
111
152
  response = await self._request(METH_POST, "keys", data=data)
112
- return CreateKeyDataWrapper.from_json(response).data
153
+ return KeyCreateResponse.from_json(response).data
154
+
155
+ async def get_key(self, key_hash: str) -> Key:
156
+ """Get a single key by hash."""
157
+ response = await self._request(METH_GET, f"keys/{key_hash}")
158
+ return KeyResponseWrapper.from_json(response).data
159
+
160
+ async def update_key(
161
+ self,
162
+ key_hash: str,
163
+ *,
164
+ name: str | None = None,
165
+ disabled: bool | None = None,
166
+ limit: float | None = None,
167
+ limit_reset: str | None = None,
168
+ include_byok_in_limit: bool | None = None,
169
+ ) -> Key:
170
+ """Update an existing key."""
171
+ data: dict[str, Any] = {}
172
+ if name is not None:
173
+ data["name"] = name
174
+ if disabled is not None:
175
+ data["disabled"] = disabled
176
+ if limit is not None:
177
+ data["limit"] = limit
178
+ if limit_reset is not None:
179
+ data["limit_reset"] = limit_reset
180
+ if include_byok_in_limit is not None:
181
+ data["include_byok_in_limit"] = include_byok_in_limit
182
+
183
+ response = await self._request(METH_PATCH, f"keys/{key_hash}", data=data)
184
+ return KeyResponseWrapper.from_json(response).data
185
+
186
+ async def delete_key(self, key_hash: str) -> bool:
187
+ """Delete a key by hash."""
188
+ response = await self._request(METH_DELETE, f"keys/{key_hash}")
189
+ return DeletedKeyResponse.from_json(response).deleted
190
+
191
+ async def get_model(self, author: str, slug: str) -> Model:
192
+ """Get a single model by author and slug."""
193
+ response = await self._request(METH_GET, f"model/{author}/{slug}")
194
+ return ModelResponseWrapper.from_json(response).data
195
+
196
+ @staticmethod
197
+ def _filter_model_params(
198
+ kwargs: dict[str, Any],
199
+ *,
200
+ numeric_keys: frozenset[str] = frozenset(),
201
+ ) -> dict[str, Any]:
202
+ """Filter None values and convert numeric params to strings."""
203
+ params = {k: v for k, v in kwargs.items() if v is not None}
204
+ for key in numeric_keys:
205
+ if key in params:
206
+ params[key] = str(params[key])
207
+ return params
113
208
 
114
- async def get_models(self) -> list[Model]:
209
+ async def get_models( # pylint: disable=too-many-locals
210
+ self,
211
+ *,
212
+ category: str | None = None,
213
+ supported_parameters: str | None = None,
214
+ output_modalities: str | None = None,
215
+ sort: str | None = None,
216
+ q: str | None = None,
217
+ input_modalities: str | None = None,
218
+ context: int | None = None,
219
+ min_price: float | None = None,
220
+ max_price: float | None = None,
221
+ arch: str | None = None,
222
+ model_authors: str | None = None,
223
+ providers: str | None = None,
224
+ distillable: str | None = None,
225
+ zdr: str | None = None,
226
+ region: str | None = None,
227
+ ) -> list[Model]:
115
228
  """Get all available models."""
116
- response = await self._request(METH_GET, "models")
229
+ numeric = frozenset({"context", "min_price", "max_price"})
230
+ params = self._filter_model_params(
231
+ {
232
+ "category": category,
233
+ "supported_parameters": supported_parameters,
234
+ "output_modalities": output_modalities,
235
+ "sort": sort,
236
+ "q": q,
237
+ "input_modalities": input_modalities,
238
+ "context": context,
239
+ "min_price": min_price,
240
+ "max_price": max_price,
241
+ "arch": arch,
242
+ "model_authors": model_authors,
243
+ "providers": providers,
244
+ "distillable": distillable,
245
+ "zdr": zdr,
246
+ "region": region,
247
+ },
248
+ numeric_keys=numeric,
249
+ )
250
+ response = await self._request(METH_GET, "models", params=params or None)
117
251
  return ModelsDataWrapper.from_json(response).data
118
252
 
253
+ async def count_models(
254
+ self,
255
+ *,
256
+ output_modalities: str | None = None,
257
+ ) -> int:
258
+ """Get total count of available models."""
259
+ params: dict[str, Any] = {}
260
+ if output_modalities is not None:
261
+ params["output_modalities"] = output_modalities
262
+ response = await self._request(METH_GET, "models/count", params=params or None)
263
+ result: dict[str, Any] = orjson.loads(response) # pylint: disable=no-member
264
+ count: int = result.get("data", {}).get("count", 0)
265
+ return count
266
+
119
267
  async def close(self) -> None:
120
268
  """Close open client session."""
121
269
  if self.session and self._close_session:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_open_router
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Asynchronous Python client for Open Router.
5
5
  License: MIT
6
6
  License-File: LICENSE.md
@@ -0,0 +1,9 @@
1
+ python_open_router/__init__.py,sha256=9a8JXk66oCsPUc-XxgHBI29sVi6QbjlnUuCUxLDJUfo,1166
2
+ python_open_router/exceptions.py,sha256=68nskqNdXNMIK4iA1XOE1E5P5rbhAf27DDACkk-ZVwU,312
3
+ python_open_router/models.py,sha256=oVEE8q195laYFa7WXSYOKI6FiTqvJ6lPSTzTphj8eHI,6943
4
+ python_open_router/open_router.py,sha256=JB_2fu1akGJ8niuP7FOlExWTV3ytDvmH_sXWKY5iO6I,9520
5
+ python_open_router/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ python_open_router-0.4.0.dist-info/METADATA,sha256=Iv2GN34OuueJ3t0rUj_ELgoEFu-aHPYX7TvCm1XvSqU,6055
7
+ python_open_router-0.4.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
8
+ python_open_router-0.4.0.dist-info/licenses/LICENSE.md,sha256=Xbw7n2Ic30r4j8_MDNND4ss_sdoj9IGeIkEWc2Hsfs8,1077
9
+ python_open_router-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.4.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,9 +0,0 @@
1
- python_open_router/__init__.py,sha256=UQt6SpcnmQkS2wiyjwA_Ggvvzu7Ucx8BOCyZLZ_F2uA,840
2
- python_open_router/exceptions.py,sha256=68nskqNdXNMIK4iA1XOE1E5P5rbhAf27DDACkk-ZVwU,312
3
- python_open_router/models.py,sha256=GHGwXr8DLYhf-jnmqNmqdQ4ISFRyYdWBd4WsQNlkDRY,3379
4
- python_open_router/open_router.py,sha256=zq7aleWZfNFqbBb69Qp3_3jed95cwxtgGMlYFhDzYKw,4148
5
- python_open_router/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- python_open_router-0.3.3.dist-info/METADATA,sha256=jKLq5jP-h8fd4mHXqj0Q8GLXFeDNhL6GMGUo5c0nVHM,6055
7
- python_open_router-0.3.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
8
- python_open_router-0.3.3.dist-info/licenses/LICENSE.md,sha256=Xbw7n2Ic30r4j8_MDNND4ss_sdoj9IGeIkEWc2Hsfs8,1077
9
- python_open_router-0.3.3.dist-info/RECORD,,