sdkrouter 0.1.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.
- sdkrouter/__init__.py +110 -0
- sdkrouter/_api/__init__.py +28 -0
- sdkrouter/_api/client.py +204 -0
- sdkrouter/_api/generated/__init__.py +21 -0
- sdkrouter/_api/generated/cdn/__init__.py +209 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/__init__.py +7 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/client.py +133 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/models.py +163 -0
- sdkrouter/_api/generated/cdn/cdn__api__cdn/sync_client.py +132 -0
- sdkrouter/_api/generated/cdn/client.py +75 -0
- sdkrouter/_api/generated/cdn/logger.py +256 -0
- sdkrouter/_api/generated/cdn/pyproject.toml +55 -0
- sdkrouter/_api/generated/cdn/retry.py +272 -0
- sdkrouter/_api/generated/cdn/sync_client.py +58 -0
- sdkrouter/_api/generated/cleaner/__init__.py +212 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/__init__.py +7 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/client.py +83 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/models.py +117 -0
- sdkrouter/_api/generated/cleaner/cleaner__api__cleaner/sync_client.py +82 -0
- sdkrouter/_api/generated/cleaner/client.py +75 -0
- sdkrouter/_api/generated/cleaner/enums.py +55 -0
- sdkrouter/_api/generated/cleaner/logger.py +256 -0
- sdkrouter/_api/generated/cleaner/pyproject.toml +55 -0
- sdkrouter/_api/generated/cleaner/retry.py +272 -0
- sdkrouter/_api/generated/cleaner/sync_client.py +58 -0
- sdkrouter/_api/generated/keys/__init__.py +212 -0
- sdkrouter/_api/generated/keys/client.py +75 -0
- sdkrouter/_api/generated/keys/enums.py +64 -0
- sdkrouter/_api/generated/keys/keys__api__keys/__init__.py +7 -0
- sdkrouter/_api/generated/keys/keys__api__keys/client.py +150 -0
- sdkrouter/_api/generated/keys/keys__api__keys/models.py +152 -0
- sdkrouter/_api/generated/keys/keys__api__keys/sync_client.py +149 -0
- sdkrouter/_api/generated/keys/logger.py +256 -0
- sdkrouter/_api/generated/keys/pyproject.toml +55 -0
- sdkrouter/_api/generated/keys/retry.py +272 -0
- sdkrouter/_api/generated/keys/sync_client.py +58 -0
- sdkrouter/_api/generated/models/__init__.py +209 -0
- sdkrouter/_api/generated/models/client.py +75 -0
- sdkrouter/_api/generated/models/logger.py +256 -0
- sdkrouter/_api/generated/models/models__api__llm_models/__init__.py +7 -0
- sdkrouter/_api/generated/models/models__api__llm_models/client.py +99 -0
- sdkrouter/_api/generated/models/models__api__llm_models/models.py +206 -0
- sdkrouter/_api/generated/models/models__api__llm_models/sync_client.py +99 -0
- sdkrouter/_api/generated/models/pyproject.toml +55 -0
- sdkrouter/_api/generated/models/retry.py +272 -0
- sdkrouter/_api/generated/models/sync_client.py +58 -0
- sdkrouter/_api/generated/shortlinks/__init__.py +209 -0
- sdkrouter/_api/generated/shortlinks/client.py +75 -0
- sdkrouter/_api/generated/shortlinks/logger.py +256 -0
- sdkrouter/_api/generated/shortlinks/pyproject.toml +55 -0
- sdkrouter/_api/generated/shortlinks/retry.py +272 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/__init__.py +7 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/client.py +137 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/models.py +153 -0
- sdkrouter/_api/generated/shortlinks/shortlinks__api__shortlinks/sync_client.py +136 -0
- sdkrouter/_api/generated/shortlinks/sync_client.py +58 -0
- sdkrouter/_api/generated/vision/__init__.py +212 -0
- sdkrouter/_api/generated/vision/client.py +75 -0
- sdkrouter/_api/generated/vision/enums.py +40 -0
- sdkrouter/_api/generated/vision/logger.py +256 -0
- sdkrouter/_api/generated/vision/pyproject.toml +55 -0
- sdkrouter/_api/generated/vision/retry.py +272 -0
- sdkrouter/_api/generated/vision/sync_client.py +58 -0
- sdkrouter/_api/generated/vision/vision__api__vision/__init__.py +7 -0
- sdkrouter/_api/generated/vision/vision__api__vision/client.py +65 -0
- sdkrouter/_api/generated/vision/vision__api__vision/models.py +138 -0
- sdkrouter/_api/generated/vision/vision__api__vision/sync_client.py +65 -0
- sdkrouter/_client.py +432 -0
- sdkrouter/_config.py +74 -0
- sdkrouter/_constants.py +21 -0
- sdkrouter/_internal/__init__.py +1 -0
- sdkrouter/_types/__init__.py +30 -0
- sdkrouter/_types/cdn.py +27 -0
- sdkrouter/_types/models.py +26 -0
- sdkrouter/_types/ocr.py +24 -0
- sdkrouter/_types/parsed.py +101 -0
- sdkrouter/_types/shortlinks.py +27 -0
- sdkrouter/_types/vision.py +29 -0
- sdkrouter/_version.py +3 -0
- sdkrouter/helpers/__init__.py +13 -0
- sdkrouter/helpers/formatting.py +15 -0
- sdkrouter/helpers/html.py +100 -0
- sdkrouter/helpers/json_cleaner.py +53 -0
- sdkrouter/tools/__init__.py +129 -0
- sdkrouter/tools/cdn.py +285 -0
- sdkrouter/tools/cleaner.py +186 -0
- sdkrouter/tools/keys.py +215 -0
- sdkrouter/tools/models.py +196 -0
- sdkrouter/tools/shortlinks.py +165 -0
- sdkrouter/tools/vision.py +173 -0
- sdkrouter/utils/__init__.py +27 -0
- sdkrouter/utils/parsing.py +109 -0
- sdkrouter/utils/tokens.py +375 -0
- sdkrouter-0.1.1.dist-info/METADATA +411 -0
- sdkrouter-0.1.1.dist-info/RECORD +96 -0
- sdkrouter-0.1.1.dist-info/WHEEL +4 -0
sdkrouter/_client.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Main SDK client classes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Optional, Type
|
|
6
|
+
|
|
7
|
+
from openai import AsyncOpenAI, OpenAI
|
|
8
|
+
|
|
9
|
+
from ._config import SDKConfig
|
|
10
|
+
from ._constants import (
|
|
11
|
+
DEFAULT_BASE_URL,
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_OPENROUTER_URL,
|
|
14
|
+
DEFAULT_TIMEOUT,
|
|
15
|
+
ENV_API_KEY,
|
|
16
|
+
ENV_OPENROUTER_KEY,
|
|
17
|
+
)
|
|
18
|
+
from ._types.parsed import ParsedChatCompletion, ParsedChoice
|
|
19
|
+
from .tools.vision import AsyncVisionResource, VisionResource
|
|
20
|
+
from .tools.cdn import AsyncCDNResource, CDNResource
|
|
21
|
+
from .tools.shortlinks import AsyncShortlinksResource, ShortlinksResource
|
|
22
|
+
from .tools.keys import AsyncKeysResource, KeysResource
|
|
23
|
+
from .tools.cleaner import AsyncCleanerResource, CleanerResource
|
|
24
|
+
from .tools.models import AsyncModelsResource, ModelsResource
|
|
25
|
+
from .utils.parsing import ResponseFormatT, type_to_response_format
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_completion(
|
|
29
|
+
raw_response: Any,
|
|
30
|
+
response_format: Type[ResponseFormatT],
|
|
31
|
+
) -> ParsedChatCompletion[ResponseFormatT]:
|
|
32
|
+
"""
|
|
33
|
+
Parse raw ChatCompletion into ParsedChatCompletion with typed content.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
raw_response: Raw ChatCompletion from OpenAI API
|
|
37
|
+
response_format: Pydantic model class for parsing
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ParsedChatCompletion with parsed content in each choice
|
|
41
|
+
"""
|
|
42
|
+
parsed_choices: list[ParsedChoice[ResponseFormatT]] = []
|
|
43
|
+
|
|
44
|
+
for choice in raw_response.choices:
|
|
45
|
+
parsed_content = None
|
|
46
|
+
content = getattr(choice.message, "content", None)
|
|
47
|
+
|
|
48
|
+
if content:
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(content)
|
|
51
|
+
parsed_content = response_format.model_validate(data)
|
|
52
|
+
except (json.JSONDecodeError, Exception):
|
|
53
|
+
# If parsing fails, leave parsed as None
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
parsed_choices.append(
|
|
57
|
+
ParsedChoice.from_choice(choice, parsed_content=parsed_content)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return ParsedChatCompletion.from_completion(raw_response, parsed_choices)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SDKRouter(OpenAI):
|
|
64
|
+
"""
|
|
65
|
+
Main SDK client - OpenAI-compatible with additional tools.
|
|
66
|
+
|
|
67
|
+
Extends OpenAI client with vision, OCR, CDN, and shortlinks tools.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
```python
|
|
71
|
+
from sdkrouter import SDKRouter
|
|
72
|
+
|
|
73
|
+
client = SDKRouter(api_key="your-api-key")
|
|
74
|
+
|
|
75
|
+
# OpenAI-compatible chat
|
|
76
|
+
response = client.chat.completions.create(
|
|
77
|
+
model="openai/gpt-4o",
|
|
78
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Vision analysis
|
|
82
|
+
result = client.vision.analyze(image_url="https://example.com/image.jpg")
|
|
83
|
+
|
|
84
|
+
# CDN upload
|
|
85
|
+
file = client.cdn.upload(Path("image.png"))
|
|
86
|
+
```
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
*,
|
|
92
|
+
api_key: Optional[str] = None,
|
|
93
|
+
base_url: Optional[str] = None,
|
|
94
|
+
use_self_hosted: bool = True,
|
|
95
|
+
openrouter_api_key: Optional[str] = None,
|
|
96
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
97
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
98
|
+
**kwargs,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize SDKRouter client.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
api_key: API key (or set SDKROUTER_API_KEY env var)
|
|
105
|
+
base_url: Base URL for API (default: ai.sdkrouter.com)
|
|
106
|
+
use_self_hosted: If True, use self-hosted proxy. If False, direct to OpenRouter
|
|
107
|
+
openrouter_api_key: OpenRouter API key (for direct mode)
|
|
108
|
+
timeout: Request timeout in seconds
|
|
109
|
+
max_retries: Number of retries for failed requests
|
|
110
|
+
**kwargs: Additional arguments passed to OpenAI client
|
|
111
|
+
"""
|
|
112
|
+
# Resolve API key
|
|
113
|
+
resolved_key = (
|
|
114
|
+
api_key
|
|
115
|
+
or os.getenv(ENV_API_KEY)
|
|
116
|
+
or openrouter_api_key
|
|
117
|
+
or os.getenv(ENV_OPENROUTER_KEY)
|
|
118
|
+
)
|
|
119
|
+
if not resolved_key:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"API key required. Set SDKROUTER_API_KEY environment variable "
|
|
122
|
+
"or pass api_key= parameter."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Resolve base URL
|
|
126
|
+
if use_self_hosted:
|
|
127
|
+
resolved_url = base_url or os.getenv("SDKROUTER_BASE_URL") or DEFAULT_BASE_URL
|
|
128
|
+
else:
|
|
129
|
+
resolved_url = DEFAULT_OPENROUTER_URL
|
|
130
|
+
|
|
131
|
+
# Initialize OpenAI client
|
|
132
|
+
super().__init__(
|
|
133
|
+
api_key=resolved_key,
|
|
134
|
+
base_url=resolved_url,
|
|
135
|
+
timeout=timeout,
|
|
136
|
+
max_retries=max_retries,
|
|
137
|
+
**kwargs,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Store configuration
|
|
141
|
+
self._sdk_config = SDKConfig(
|
|
142
|
+
api_key=resolved_key,
|
|
143
|
+
base_url=resolved_url,
|
|
144
|
+
use_self_hosted=use_self_hosted,
|
|
145
|
+
timeout=timeout,
|
|
146
|
+
max_retries=max_retries,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Lazy-loaded tool resources
|
|
150
|
+
self._vision: Optional[VisionResource] = None
|
|
151
|
+
self._cdn: Optional[CDNResource] = None
|
|
152
|
+
self._shortlinks: Optional[ShortlinksResource] = None
|
|
153
|
+
self._keys: Optional[KeysResource] = None
|
|
154
|
+
self._cleaner: Optional[CleanerResource] = None
|
|
155
|
+
self._models: Optional[ModelsResource] = None
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def vision(self) -> VisionResource:
|
|
159
|
+
"""Vision analysis and OCR tool."""
|
|
160
|
+
if self._vision is None:
|
|
161
|
+
self._vision = VisionResource(self._sdk_config)
|
|
162
|
+
return self._vision
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def cdn(self) -> CDNResource:
|
|
166
|
+
"""CDN file storage tool."""
|
|
167
|
+
if self._cdn is None:
|
|
168
|
+
self._cdn = CDNResource(self._sdk_config)
|
|
169
|
+
return self._cdn
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def shortlinks(self) -> ShortlinksResource:
|
|
173
|
+
"""URL shortening tool."""
|
|
174
|
+
if self._shortlinks is None:
|
|
175
|
+
self._shortlinks = ShortlinksResource(self._sdk_config)
|
|
176
|
+
return self._shortlinks
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def keys(self) -> KeysResource:
|
|
180
|
+
"""API keys management tool."""
|
|
181
|
+
if self._keys is None:
|
|
182
|
+
self._keys = KeysResource(self._sdk_config)
|
|
183
|
+
return self._keys
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def cleaner(self) -> CleanerResource:
|
|
187
|
+
"""HTML cleaner tool."""
|
|
188
|
+
if self._cleaner is None:
|
|
189
|
+
self._cleaner = CleanerResource(self._sdk_config)
|
|
190
|
+
return self._cleaner
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def models(self) -> ModelsResource:
|
|
194
|
+
"""LLM models listing tool."""
|
|
195
|
+
if self._models is None:
|
|
196
|
+
self._models = ModelsResource(self._sdk_config)
|
|
197
|
+
return self._models
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def config(self) -> SDKConfig:
|
|
201
|
+
"""SDK configuration."""
|
|
202
|
+
return self._sdk_config
|
|
203
|
+
|
|
204
|
+
def parse(
|
|
205
|
+
self,
|
|
206
|
+
*,
|
|
207
|
+
model: str,
|
|
208
|
+
messages: list[dict[str, Any]],
|
|
209
|
+
response_format: Type[ResponseFormatT],
|
|
210
|
+
**kwargs: Any,
|
|
211
|
+
) -> ParsedChatCompletion[ResponseFormatT]:
|
|
212
|
+
"""
|
|
213
|
+
Create chat completion with structured output.
|
|
214
|
+
|
|
215
|
+
Automatically converts Pydantic model to JSON Schema and parses
|
|
216
|
+
the response back into the model.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
model: Model ID (e.g., "openai/gpt-4o")
|
|
220
|
+
messages: Chat messages
|
|
221
|
+
response_format: Pydantic model class for response parsing
|
|
222
|
+
**kwargs: Additional parameters (temperature, max_tokens, etc.)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
ParsedChatCompletion with .choices[0].message.parsed
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
```python
|
|
229
|
+
class MathResponse(BaseModel):
|
|
230
|
+
steps: list[str]
|
|
231
|
+
answer: float
|
|
232
|
+
|
|
233
|
+
result = client.parse(
|
|
234
|
+
model="openai/gpt-4o",
|
|
235
|
+
messages=[{"role": "user", "content": "Solve 2+2"}],
|
|
236
|
+
response_format=MathResponse,
|
|
237
|
+
)
|
|
238
|
+
print(result.choices[0].message.parsed.answer)
|
|
239
|
+
```
|
|
240
|
+
"""
|
|
241
|
+
# Convert Pydantic model to response_format
|
|
242
|
+
format_param = type_to_response_format(response_format)
|
|
243
|
+
|
|
244
|
+
# Make API call
|
|
245
|
+
raw_response = self.chat.completions.create(
|
|
246
|
+
model=model,
|
|
247
|
+
messages=messages,
|
|
248
|
+
response_format=format_param,
|
|
249
|
+
**kwargs,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Parse response
|
|
253
|
+
return _parse_completion(raw_response, response_format)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class AsyncSDKRouter(AsyncOpenAI):
|
|
257
|
+
"""
|
|
258
|
+
Async SDK client - OpenAI-compatible with additional tools.
|
|
259
|
+
|
|
260
|
+
Async version of SDKRouter for use in async contexts.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
```python
|
|
264
|
+
from sdkrouter import AsyncSDKRouter
|
|
265
|
+
|
|
266
|
+
client = AsyncSDKRouter(api_key="your-api-key")
|
|
267
|
+
|
|
268
|
+
# Async chat
|
|
269
|
+
response = await client.chat.completions.create(
|
|
270
|
+
model="openai/gpt-4o",
|
|
271
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Async vision
|
|
275
|
+
result = await client.vision.analyze(image_url="https://example.com/image.jpg")
|
|
276
|
+
```
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
*,
|
|
282
|
+
api_key: Optional[str] = None,
|
|
283
|
+
base_url: Optional[str] = None,
|
|
284
|
+
use_self_hosted: bool = True,
|
|
285
|
+
openrouter_api_key: Optional[str] = None,
|
|
286
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
287
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
288
|
+
**kwargs,
|
|
289
|
+
):
|
|
290
|
+
"""Initialize async SDKRouter client."""
|
|
291
|
+
# Resolve API key
|
|
292
|
+
resolved_key = (
|
|
293
|
+
api_key
|
|
294
|
+
or os.getenv(ENV_API_KEY)
|
|
295
|
+
or openrouter_api_key
|
|
296
|
+
or os.getenv(ENV_OPENROUTER_KEY)
|
|
297
|
+
)
|
|
298
|
+
if not resolved_key:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
"API key required. Set SDKROUTER_API_KEY environment variable "
|
|
301
|
+
"or pass api_key= parameter."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Resolve base URL
|
|
305
|
+
if use_self_hosted:
|
|
306
|
+
resolved_url = base_url or os.getenv("SDKROUTER_BASE_URL") or DEFAULT_BASE_URL
|
|
307
|
+
else:
|
|
308
|
+
resolved_url = DEFAULT_OPENROUTER_URL
|
|
309
|
+
|
|
310
|
+
# Initialize AsyncOpenAI client
|
|
311
|
+
super().__init__(
|
|
312
|
+
api_key=resolved_key,
|
|
313
|
+
base_url=resolved_url,
|
|
314
|
+
timeout=timeout,
|
|
315
|
+
max_retries=max_retries,
|
|
316
|
+
**kwargs,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Store configuration
|
|
320
|
+
self._sdk_config = SDKConfig(
|
|
321
|
+
api_key=resolved_key,
|
|
322
|
+
base_url=resolved_url,
|
|
323
|
+
use_self_hosted=use_self_hosted,
|
|
324
|
+
timeout=timeout,
|
|
325
|
+
max_retries=max_retries,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Lazy-loaded async tool resources
|
|
329
|
+
self._vision: Optional[AsyncVisionResource] = None
|
|
330
|
+
self._cdn: Optional[AsyncCDNResource] = None
|
|
331
|
+
self._shortlinks: Optional[AsyncShortlinksResource] = None
|
|
332
|
+
self._keys: Optional[AsyncKeysResource] = None
|
|
333
|
+
self._cleaner: Optional[AsyncCleanerResource] = None
|
|
334
|
+
self._models: Optional[AsyncModelsResource] = None
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def vision(self) -> AsyncVisionResource:
|
|
338
|
+
"""Vision analysis and OCR tool (async)."""
|
|
339
|
+
if self._vision is None:
|
|
340
|
+
self._vision = AsyncVisionResource(self._sdk_config)
|
|
341
|
+
return self._vision
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def cdn(self) -> AsyncCDNResource:
|
|
345
|
+
"""CDN file storage tool (async)."""
|
|
346
|
+
if self._cdn is None:
|
|
347
|
+
self._cdn = AsyncCDNResource(self._sdk_config)
|
|
348
|
+
return self._cdn
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def shortlinks(self) -> AsyncShortlinksResource:
|
|
352
|
+
"""URL shortening tool (async)."""
|
|
353
|
+
if self._shortlinks is None:
|
|
354
|
+
self._shortlinks = AsyncShortlinksResource(self._sdk_config)
|
|
355
|
+
return self._shortlinks
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def keys(self) -> AsyncKeysResource:
|
|
359
|
+
"""API keys management tool (async)."""
|
|
360
|
+
if self._keys is None:
|
|
361
|
+
self._keys = AsyncKeysResource(self._sdk_config)
|
|
362
|
+
return self._keys
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def cleaner(self) -> AsyncCleanerResource:
|
|
366
|
+
"""HTML cleaner tool (async)."""
|
|
367
|
+
if self._cleaner is None:
|
|
368
|
+
self._cleaner = AsyncCleanerResource(self._sdk_config)
|
|
369
|
+
return self._cleaner
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def models(self) -> AsyncModelsResource:
|
|
373
|
+
"""LLM models listing tool (async)."""
|
|
374
|
+
if self._models is None:
|
|
375
|
+
self._models = AsyncModelsResource(self._sdk_config)
|
|
376
|
+
return self._models
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def config(self) -> SDKConfig:
|
|
380
|
+
"""SDK configuration."""
|
|
381
|
+
return self._sdk_config
|
|
382
|
+
|
|
383
|
+
async def parse(
|
|
384
|
+
self,
|
|
385
|
+
*,
|
|
386
|
+
model: str,
|
|
387
|
+
messages: list[dict[str, Any]],
|
|
388
|
+
response_format: Type[ResponseFormatT],
|
|
389
|
+
**kwargs: Any,
|
|
390
|
+
) -> ParsedChatCompletion[ResponseFormatT]:
|
|
391
|
+
"""
|
|
392
|
+
Create async chat completion with structured output.
|
|
393
|
+
|
|
394
|
+
Automatically converts Pydantic model to JSON Schema and parses
|
|
395
|
+
the response back into the model.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
model: Model ID (e.g., "openai/gpt-4o")
|
|
399
|
+
messages: Chat messages
|
|
400
|
+
response_format: Pydantic model class for response parsing
|
|
401
|
+
**kwargs: Additional parameters (temperature, max_tokens, etc.)
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
ParsedChatCompletion with .choices[0].message.parsed
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
```python
|
|
408
|
+
class MathResponse(BaseModel):
|
|
409
|
+
steps: list[str]
|
|
410
|
+
answer: float
|
|
411
|
+
|
|
412
|
+
result = await client.parse(
|
|
413
|
+
model="openai/gpt-4o",
|
|
414
|
+
messages=[{"role": "user", "content": "Solve 2+2"}],
|
|
415
|
+
response_format=MathResponse,
|
|
416
|
+
)
|
|
417
|
+
print(result.choices[0].message.parsed.answer)
|
|
418
|
+
```
|
|
419
|
+
"""
|
|
420
|
+
# Convert Pydantic model to response_format
|
|
421
|
+
format_param = type_to_response_format(response_format)
|
|
422
|
+
|
|
423
|
+
# Make API call
|
|
424
|
+
raw_response = await self.chat.completions.create(
|
|
425
|
+
model=model,
|
|
426
|
+
messages=messages,
|
|
427
|
+
response_format=format_param,
|
|
428
|
+
**kwargs,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Parse response
|
|
432
|
+
return _parse_completion(raw_response, response_format)
|
sdkrouter/_config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""SDK configuration."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
from ._constants import (
|
|
8
|
+
DEFAULT_BASE_URL,
|
|
9
|
+
DEFAULT_CDN_URL,
|
|
10
|
+
DEFAULT_CONNECT_TIMEOUT,
|
|
11
|
+
DEFAULT_DJANGO_URL,
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_OPENAI_URL,
|
|
14
|
+
DEFAULT_OPENROUTER_URL,
|
|
15
|
+
DEFAULT_TIMEOUT,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SDKConfig(BaseSettings):
|
|
20
|
+
"""SDK configuration with environment variable support."""
|
|
21
|
+
|
|
22
|
+
model_config = SettingsConfigDict(
|
|
23
|
+
env_prefix="SDKROUTER_",
|
|
24
|
+
env_file=".env",
|
|
25
|
+
extra="ignore",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# API Keys
|
|
29
|
+
api_key: Optional[str] = None
|
|
30
|
+
openrouter_api_key: Optional[str] = None
|
|
31
|
+
openai_api_key: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
# Base URLs (for self-hosted or direct)
|
|
34
|
+
base_url: str = DEFAULT_BASE_URL
|
|
35
|
+
django_url: str = DEFAULT_DJANGO_URL
|
|
36
|
+
cdn_url: str = DEFAULT_CDN_URL
|
|
37
|
+
|
|
38
|
+
# Direct provider URLs (bypass self-hosted)
|
|
39
|
+
openrouter_url: str = DEFAULT_OPENROUTER_URL
|
|
40
|
+
openai_url: str = DEFAULT_OPENAI_URL
|
|
41
|
+
|
|
42
|
+
# Routing mode
|
|
43
|
+
use_self_hosted: bool = True # False = direct to OpenRouter/OpenAI
|
|
44
|
+
|
|
45
|
+
# Timeouts
|
|
46
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
47
|
+
connect_timeout: float = DEFAULT_CONNECT_TIMEOUT
|
|
48
|
+
|
|
49
|
+
# Retry
|
|
50
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_config: Optional[SDKConfig] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_config() -> SDKConfig:
|
|
57
|
+
"""Get or create SDK configuration."""
|
|
58
|
+
global _config
|
|
59
|
+
if _config is None:
|
|
60
|
+
_config = SDKConfig()
|
|
61
|
+
return _config
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def configure(**kwargs) -> SDKConfig:
|
|
65
|
+
"""Configure SDK with custom settings."""
|
|
66
|
+
global _config
|
|
67
|
+
_config = SDKConfig(**kwargs)
|
|
68
|
+
return _config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def reset_config() -> None:
|
|
72
|
+
"""Reset configuration to defaults."""
|
|
73
|
+
global _config
|
|
74
|
+
_config = None
|
sdkrouter/_constants.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""SDK constants and defaults."""
|
|
2
|
+
|
|
3
|
+
# Environment variable names
|
|
4
|
+
ENV_API_KEY = "SDKROUTER_API_KEY"
|
|
5
|
+
ENV_OPENROUTER_KEY = "OPENROUTER_API_KEY"
|
|
6
|
+
ENV_OPENAI_KEY = "OPENAI_API_KEY"
|
|
7
|
+
ENV_BASE_URL = "SDKROUTER_BASE_URL"
|
|
8
|
+
ENV_DJANGO_URL = "SDKROUTER_DJANGO_URL"
|
|
9
|
+
ENV_CDN_URL = "SDKROUTER_CDN_URL"
|
|
10
|
+
|
|
11
|
+
# Default URLs
|
|
12
|
+
DEFAULT_BASE_URL = "https://ai.sdkrouter.com/v1"
|
|
13
|
+
DEFAULT_DJANGO_URL = "https://api.sdkrouter.com"
|
|
14
|
+
DEFAULT_CDN_URL = "https://cdn.sdkrouter.com"
|
|
15
|
+
DEFAULT_OPENROUTER_URL = "https://openrouter.ai/api/v1"
|
|
16
|
+
DEFAULT_OPENAI_URL = "https://api.openai.com/v1"
|
|
17
|
+
|
|
18
|
+
# Default settings
|
|
19
|
+
DEFAULT_TIMEOUT = 60.0
|
|
20
|
+
DEFAULT_CONNECT_TIMEOUT = 5.0
|
|
21
|
+
DEFAULT_MAX_RETRIES = 2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal helpers (not part of public API)."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Type definitions for SDK resources."""
|
|
2
|
+
|
|
3
|
+
from .cdn import CDNFile, CDNUploadRequest
|
|
4
|
+
from .models import Model, ModelPricing
|
|
5
|
+
from .ocr import OCRRequest, OCRResponse
|
|
6
|
+
from .parsed import ParsedChatCompletion, ParsedChoice, ParsedMessage
|
|
7
|
+
from .shortlinks import ShortLink, ShortLinkCreateRequest
|
|
8
|
+
from .vision import VisionAnalyzeRequest, VisionAnalyzeResponse
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# CDN
|
|
12
|
+
"CDNFile",
|
|
13
|
+
"CDNUploadRequest",
|
|
14
|
+
# Models
|
|
15
|
+
"Model",
|
|
16
|
+
"ModelPricing",
|
|
17
|
+
# OCR
|
|
18
|
+
"OCRRequest",
|
|
19
|
+
"OCRResponse",
|
|
20
|
+
# Parsed (structured output)
|
|
21
|
+
"ParsedChatCompletion",
|
|
22
|
+
"ParsedChoice",
|
|
23
|
+
"ParsedMessage",
|
|
24
|
+
# Shortlinks
|
|
25
|
+
"ShortLink",
|
|
26
|
+
"ShortLinkCreateRequest",
|
|
27
|
+
# Vision
|
|
28
|
+
"VisionAnalyzeRequest",
|
|
29
|
+
"VisionAnalyzeResponse",
|
|
30
|
+
]
|
sdkrouter/_types/cdn.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CDN types."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CDNUploadRequest(BaseModel):
|
|
10
|
+
"""Request for CDN file upload."""
|
|
11
|
+
|
|
12
|
+
filename: Optional[str] = Field(default=None, description="Original filename")
|
|
13
|
+
content_type: Optional[str] = Field(default=None, description="MIME type")
|
|
14
|
+
ttl: str = Field(default="30d", description="Time to live (e.g., '7d', '30d', '1y')")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CDNFile(BaseModel):
|
|
18
|
+
"""CDN file information."""
|
|
19
|
+
|
|
20
|
+
uuid: str = Field(description="Unique file identifier")
|
|
21
|
+
short_code: str = Field(description="Short code for URL")
|
|
22
|
+
filename: str = Field(description="Original filename")
|
|
23
|
+
content_type: str = Field(description="MIME type")
|
|
24
|
+
size_bytes: int = Field(description="File size in bytes")
|
|
25
|
+
url: str = Field(description="Full URL to access file")
|
|
26
|
+
expires_at: Optional[datetime] = Field(default=None, description="Expiration time")
|
|
27
|
+
created_at: Optional[datetime] = Field(default=None, description="Upload time")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Model types."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelPricing(BaseModel):
|
|
9
|
+
"""Model pricing information."""
|
|
10
|
+
|
|
11
|
+
prompt: float = Field(description="Cost per 1M input tokens")
|
|
12
|
+
completion: float = Field(description="Cost per 1M output tokens")
|
|
13
|
+
image: Optional[float] = Field(default=None, description="Cost per image")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Model(BaseModel):
|
|
17
|
+
"""LLM model information."""
|
|
18
|
+
|
|
19
|
+
id: str = Field(description="Model identifier")
|
|
20
|
+
name: str = Field(description="Display name")
|
|
21
|
+
provider: str = Field(description="Provider name")
|
|
22
|
+
context_length: int = Field(description="Max context length")
|
|
23
|
+
pricing: ModelPricing = Field(description="Pricing information")
|
|
24
|
+
supports_vision: bool = Field(default=False, description="Supports image input")
|
|
25
|
+
supports_tools: bool = Field(default=False, description="Supports function calling")
|
|
26
|
+
supports_json: bool = Field(default=False, description="Supports JSON mode")
|
sdkrouter/_types/ocr.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OCR types."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OCRRequest(BaseModel):
|
|
9
|
+
"""Request for OCR text extraction."""
|
|
10
|
+
|
|
11
|
+
image: Optional[str] = Field(default=None, description="Base64 encoded image")
|
|
12
|
+
image_url: Optional[str] = Field(default=None, description="URL of image")
|
|
13
|
+
mode: Literal["tiny", "small", "base", "gundam"] = Field(
|
|
14
|
+
default="base", description="OCR model size/quality"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OCRResponse(BaseModel):
|
|
19
|
+
"""Response from OCR extraction."""
|
|
20
|
+
|
|
21
|
+
text: str = Field(description="Extracted text")
|
|
22
|
+
model: str = Field(description="Model used")
|
|
23
|
+
cost_usd: float = Field(description="Cost in USD")
|
|
24
|
+
cached: bool = Field(default=False, description="Whether result was from cache")
|