python-open-router 0.3.3__py3-none-any.whl → 0.4.1__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,30 @@ 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"
131
+ PREDICTION = "prediction"
85
132
  TOOLS = "tools"
86
133
  TOOL_CHOICE = "tool_choice"
134
+ PARALLEL_TOOL_CALLS = "parallel_tool_calls"
87
135
  FREQUENCY_PENALTY = "frequency_penalty"
88
136
  PRESENCE_PENALTY = "presence_penalty"
89
- MIN_P = "min_p"
90
137
  REPETITION_PENALTY = "repetition_penalty"
138
+ MIN_P = "min_p"
139
+ TOP_A = "top_a"
91
140
  LOGPROBS = "logprobs"
92
141
  LOGIT_BIAS = "logit_bias"
93
142
  TOP_LOGPROBS = "top_logprobs"
94
- STRUCTURED_OUTPUTS = "structured_outputs"
95
143
  WEB_SEARCH_OPTIONS = "web_search_options"
96
- TOP_A = "top_a"
144
+ VERBOSITY = "verbosity"
97
145
 
98
146
 
99
147
  class Modality(StrEnum):
@@ -105,14 +153,77 @@ class Modality(StrEnum):
105
153
  FILE = "file"
106
154
  EMBEDDINGS = "embeddings"
107
155
  VIDEO = "video"
156
+ RERANK = "rerank"
157
+ SPEECH = "speech"
158
+ TRANSCRIPTION = "transcription"
108
159
 
109
160
 
110
161
  @dataclass(kw_only=True)
111
162
  class ModelArchitecture(DataClassORJSONMixin):
112
- """Model data."""
163
+ """Model architecture data."""
113
164
 
114
165
  input_modalities: list[Modality]
115
166
  output_modalities: list[Modality]
167
+ modality: str | None = None
168
+ instruct_type: str | None = None
169
+ tokenizer: str | None = None
170
+
171
+
172
+ @dataclass(kw_only=True)
173
+ class PublicPricing(DataClassORJSONMixin):
174
+ """Model pricing data."""
175
+
176
+ completion: str
177
+ prompt: str
178
+ audio: str | None = None
179
+ audio_output: str | None = None
180
+ image: str | None = None
181
+ image_output: str | None = None
182
+ image_token: str | None = None
183
+ input_audio_cache: str | None = None
184
+ input_cache_read: str | None = None
185
+ input_cache_write: str | None = None
186
+ input_cache_write_1h: str | None = None
187
+ internal_reasoning: str | None = None
188
+ request: str | None = None
189
+ web_search: str | None = None
190
+ discount: float | None = None
191
+
192
+
193
+ @dataclass(kw_only=True)
194
+ class TopProviderInfo(DataClassORJSONMixin):
195
+ """Top provider information."""
196
+
197
+ is_moderated: bool
198
+ context_length: int | None = None
199
+ max_completion_tokens: int | None = None
200
+
201
+
202
+ @dataclass(kw_only=True)
203
+ class DefaultParameters(DataClassORJSONMixin):
204
+ """Default model parameters."""
205
+
206
+ temperature: float | None = None
207
+ top_p: float | None = None
208
+ top_k: int | None = None
209
+ frequency_penalty: float | None = None
210
+ presence_penalty: float | None = None
211
+ repetition_penalty: float | None = None
212
+
213
+
214
+ @dataclass(kw_only=True)
215
+ class PerRequestLimits(DataClassORJSONMixin):
216
+ """Per-request limits."""
217
+
218
+ completion_tokens: float
219
+ prompt_tokens: float
220
+
221
+
222
+ @dataclass(kw_only=True)
223
+ class ModelLinks(DataClassORJSONMixin):
224
+ """Model links."""
225
+
226
+ details: str
116
227
 
117
228
 
118
229
  @dataclass(kw_only=True)
@@ -121,24 +232,34 @@ class Model(DataClassORJSONMixin):
121
232
 
122
233
  id: str
123
234
  canonical_slug: str
124
- hugging_face_id: str | None
125
235
  name: str
126
- description: str
127
- context_length: int
236
+ context_length: int | None
128
237
  architecture: ModelArchitecture
129
238
  supported_parameters: list[SupportedParameter]
239
+ pricing: PublicPricing
240
+ top_provider: TopProviderInfo
241
+ hugging_face_id: str | None = None
242
+ description: str | None = None
243
+ created: int | None = None
244
+ default_parameters: DefaultParameters | None = None
245
+ links: ModelLinks | None = None
246
+ per_request_limits: PerRequestLimits | None = None
247
+ supported_voices: list[str] | None = None
248
+ expiration_date: str | None = None
249
+ knowledge_cutoff: str | None = None
130
250
 
131
251
  @classmethod
132
252
  def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
133
253
  """Pre deserialize hook."""
134
254
  parameters = d.get("supported_parameters", [])
255
+ supported = set(SupportedParameter)
256
+ d["supported_parameters"] = [p for p in parameters if p in supported]
135
257
  for parameter in parameters:
136
- if parameter not in SupportedParameter:
258
+ if parameter not in supported:
137
259
  _LOGGER.warning(
138
260
  "Unsupported parameter: %s. Please report at https://github.com/joostlek/python-open-router/issues.",
139
261
  parameter,
140
262
  )
141
- parameters.remove(parameter)
142
263
  return d
143
264
 
144
265
  @classmethod
@@ -146,4 +267,6 @@ class Model(DataClassORJSONMixin):
146
267
  """Post deserialize hook."""
147
268
  if obj.hugging_face_id == "":
148
269
  obj.hugging_face_id = None
270
+ if obj.description == "":
271
+ obj.description = None
149
272
  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.1
4
4
  Summary: Asynchronous Python client for Open Router.
5
5
  License: MIT
6
6
  License-File: LICENSE.md
@@ -121,7 +121,7 @@ check [the contributor's page][contributors].
121
121
 
122
122
  MIT License
123
123
 
124
- Copyright (c) 2025 Joost Lekkerkerker
124
+ Copyright (c) 2025-2026 Joost Lekkerkerker
125
125
 
126
126
  Permission is hereby granted, free of charge, to any person obtaining a copy
127
127
  of this software and associated documentation files (the "Software"), to deal
@@ -151,7 +151,7 @@ SOFTWARE.
151
151
  [joostlek]: https://github.com/joostlek
152
152
  [keepchangelog]: http://keepachangelog.com/en/1.0.0/
153
153
  [license-shield]: https://img.shields.io/github/license/joostlek/python-open-router.svg
154
- [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg
154
+ [maintenance-shield]: https://img.shields.io/maintenance/yes/2026.svg
155
155
  [poetry-install]: https://python-poetry.org/docs/#installation
156
156
  [poetry]: https://python-poetry.org
157
157
  [pre-commit]: https://pre-commit.com/
@@ -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=QYU81QGRUrrZL4ocTLBghT7R2xh3qge1F7s5IiG7atA,6973
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.1.dist-info/METADATA,sha256=SLP9hPBfCK5wTOi1_ogXl5qAilEP14-jQnQTajvtVEA,6060
7
+ python_open_router-0.4.1.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
8
+ python_open_router-0.4.1.dist-info/licenses/LICENSE.md,sha256=nVkEdxeRRC-wzKM8tgyAhQTR1hxiY2jftVwlTDKKQMQ,1082
9
+ python_open_router-0.4.1.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,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright (c) 2025 Joost Lekkerkerker
3
+ Copyright (c) 2025-2026 Joost Lekkerkerker
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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,,