spt-models 0.2.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.
- spt_models/__init__.py +52 -0
- spt_models/_base.py +50 -0
- spt_models/_client.py +238 -0
- spt_models/_exceptions.py +99 -0
- spt_models/_streaming.py +82 -0
- spt_models/resources/__init__.py +1 -0
- spt_models/resources/admin/__init__.py +52 -0
- spt_models/resources/admin/aliases.py +91 -0
- spt_models/resources/admin/conversion.py +81 -0
- spt_models/resources/admin/dashboard.py +39 -0
- spt_models/resources/admin/gpu.py +51 -0
- spt_models/resources/admin/keys.py +65 -0
- spt_models/resources/admin/models.py +310 -0
- spt_models/resources/admin/usage.py +148 -0
- spt_models/resources/audio.py +156 -0
- spt_models/resources/chat.py +78 -0
- spt_models/resources/completions.py +38 -0
- spt_models/resources/embeddings.py +29 -0
- spt_models/resources/images.py +27 -0
- spt_models/resources/models.py +45 -0
- spt_models/resources/videos.py +27 -0
- spt_models/types/__init__.py +1 -0
- spt_models/types/admin.py +248 -0
- spt_models/types/audio.py +10 -0
- spt_models/types/chat.py +55 -0
- spt_models/types/completions.py +36 -0
- spt_models/types/embeddings.py +20 -0
- spt_models/types/images.py +19 -0
- spt_models/types/models.py +47 -0
- spt_models-0.2.0.dist-info/METADATA +65 -0
- spt_models-0.2.0.dist-info/RECORD +33 -0
- spt_models-0.2.0.dist-info/WHEEL +4 -0
- spt_models-0.2.0.dist-info/licenses/LICENSE +21 -0
spt_models/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""SPT Models Python client library."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from ._client import Client, AsyncClient
|
|
6
|
+
from ._exceptions import (
|
|
7
|
+
SPTError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
UpstreamError,
|
|
13
|
+
APIConnectionError,
|
|
14
|
+
APIStatusError,
|
|
15
|
+
)
|
|
16
|
+
from .types.chat import ChatCompletion, ChatCompletionChunk, ChatMessage
|
|
17
|
+
from .types.completions import Completion, CompletionChunk
|
|
18
|
+
from .types.embeddings import EmbeddingResponse
|
|
19
|
+
from .types.images import ImageResponse, VideoResponse
|
|
20
|
+
from .types.audio import Transcription
|
|
21
|
+
from .types.models import Model, ModelList, ModelParameters, ParameterInfo
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Clients
|
|
25
|
+
"Client",
|
|
26
|
+
"AsyncClient",
|
|
27
|
+
# Exceptions
|
|
28
|
+
"SPTError",
|
|
29
|
+
"AuthenticationError",
|
|
30
|
+
"RateLimitError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"UpstreamError",
|
|
34
|
+
"APIConnectionError",
|
|
35
|
+
"APIStatusError",
|
|
36
|
+
# Inference types
|
|
37
|
+
"ChatCompletion",
|
|
38
|
+
"ChatCompletionChunk",
|
|
39
|
+
"ChatMessage",
|
|
40
|
+
"Completion",
|
|
41
|
+
"CompletionChunk",
|
|
42
|
+
"EmbeddingResponse",
|
|
43
|
+
"ImageResponse",
|
|
44
|
+
"VideoResponse",
|
|
45
|
+
"Transcription",
|
|
46
|
+
"Model",
|
|
47
|
+
"ModelList",
|
|
48
|
+
"ModelParameters",
|
|
49
|
+
"ParameterInfo",
|
|
50
|
+
# Version
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|
spt_models/_base.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Shared base client configuration and utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
_DEFAULT_BASE_URL = "https://models.sponge-theory.dev"
|
|
9
|
+
# Server-side inference budget is 3600s end-to-end (orchestrator's
|
|
10
|
+
# inference_timeout_seconds, gateway's proxy/admin-router timeouts — video
|
|
11
|
+
# gen can run 28+ min). Default must clear that budget with margin, or the
|
|
12
|
+
# client gives up on a call the server is about to finish successfully — the
|
|
13
|
+
# worst kind of failure, since it looks like an error when none occurred.
|
|
14
|
+
_DEFAULT_TIMEOUT = 3900 # seconds
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BaseClientConfig:
|
|
19
|
+
api_key: str | None = None
|
|
20
|
+
admin_token: str | None = None
|
|
21
|
+
base_url: str = ""
|
|
22
|
+
timeout: float | None = None
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if not self.api_key:
|
|
26
|
+
self.api_key = os.environ.get("SPT_API_KEY")
|
|
27
|
+
if not self.admin_token:
|
|
28
|
+
self.admin_token = os.environ.get("SPT_ADMIN_TOKEN")
|
|
29
|
+
if not self.base_url:
|
|
30
|
+
self.base_url = os.environ.get("SPT_BASE_URL", _DEFAULT_BASE_URL)
|
|
31
|
+
if self.base_url.endswith("/"):
|
|
32
|
+
self.base_url = self.base_url.rstrip("/")
|
|
33
|
+
if self.timeout is None:
|
|
34
|
+
self.timeout = _DEFAULT_TIMEOUT
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_headers(*, api_key: str | None) -> dict[str, str]:
|
|
38
|
+
"""Build HTTP headers for inference requests (API key only)."""
|
|
39
|
+
headers: dict[str, str] = {"Accept": "application/json"}
|
|
40
|
+
if api_key:
|
|
41
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
42
|
+
return headers
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_admin_headers(*, admin_token: str | None) -> dict[str, str]:
|
|
46
|
+
"""Build HTTP headers for admin requests (admin token only)."""
|
|
47
|
+
headers: dict[str, str] = {"Accept": "application/json"}
|
|
48
|
+
if admin_token:
|
|
49
|
+
headers["Authorization"] = f"Bearer {admin_token}"
|
|
50
|
+
return headers
|
spt_models/_client.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Sync and async client implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._base import BaseClientConfig, build_headers, build_admin_headers
|
|
10
|
+
from ._exceptions import APIConnectionError, raise_for_status
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _ResourceMixin:
|
|
14
|
+
"""Provides lazy resource namespace access shared between sync/async clients."""
|
|
15
|
+
|
|
16
|
+
_config: BaseClientConfig
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def chat(self) -> Any:
|
|
20
|
+
if not hasattr(self, "_chat"):
|
|
21
|
+
from .resources.chat import ChatResource
|
|
22
|
+
self._chat = ChatResource(self)
|
|
23
|
+
return self._chat
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def completions(self) -> Any:
|
|
27
|
+
if not hasattr(self, "_completions"):
|
|
28
|
+
from .resources.completions import CompletionsResource
|
|
29
|
+
self._completions = CompletionsResource(self)
|
|
30
|
+
return self._completions
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def embeddings(self) -> Any:
|
|
34
|
+
if not hasattr(self, "_embeddings"):
|
|
35
|
+
from .resources.embeddings import EmbeddingsResource
|
|
36
|
+
self._embeddings = EmbeddingsResource(self)
|
|
37
|
+
return self._embeddings
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def images(self) -> Any:
|
|
41
|
+
if not hasattr(self, "_images"):
|
|
42
|
+
from .resources.images import ImagesResource
|
|
43
|
+
self._images = ImagesResource(self)
|
|
44
|
+
return self._images
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def videos(self) -> Any:
|
|
48
|
+
if not hasattr(self, "_videos"):
|
|
49
|
+
from .resources.videos import VideosResource
|
|
50
|
+
self._videos = VideosResource(self)
|
|
51
|
+
return self._videos
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def audio(self) -> Any:
|
|
55
|
+
if not hasattr(self, "_audio"):
|
|
56
|
+
from .resources.audio import AudioResource
|
|
57
|
+
self._audio = AudioResource(self)
|
|
58
|
+
return self._audio
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def models(self) -> Any:
|
|
62
|
+
if not hasattr(self, "_models"):
|
|
63
|
+
from .resources.models import ModelsResource
|
|
64
|
+
self._models = ModelsResource(self)
|
|
65
|
+
return self._models
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def admin(self) -> Any:
|
|
69
|
+
if not hasattr(self, "_admin"):
|
|
70
|
+
from .resources.admin import AdminResource
|
|
71
|
+
self._admin = AdminResource(self)
|
|
72
|
+
return self._admin
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Client(_ResourceMixin):
|
|
76
|
+
"""Synchronous SPT Models client."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
api_key: str | None = None,
|
|
82
|
+
admin_token: str | None = None,
|
|
83
|
+
base_url: str = "",
|
|
84
|
+
timeout: float | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
self._config = BaseClientConfig(
|
|
87
|
+
api_key=api_key, admin_token=admin_token,
|
|
88
|
+
base_url=base_url, timeout=timeout,
|
|
89
|
+
)
|
|
90
|
+
self._http = httpx.Client(
|
|
91
|
+
base_url=self._config.base_url,
|
|
92
|
+
timeout=httpx.Timeout(self._config.timeout, connect=10.0),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _request(
|
|
96
|
+
self,
|
|
97
|
+
method: str,
|
|
98
|
+
path: str,
|
|
99
|
+
*,
|
|
100
|
+
json: dict[str, Any] | None = None,
|
|
101
|
+
params: dict[str, Any] | None = None,
|
|
102
|
+
headers: dict[str, str] | None = None,
|
|
103
|
+
timeout: float | None = None,
|
|
104
|
+
admin: bool = False,
|
|
105
|
+
) -> httpx.Response:
|
|
106
|
+
"""Send an HTTP request with auth headers and error handling."""
|
|
107
|
+
if headers is None:
|
|
108
|
+
if admin:
|
|
109
|
+
headers = build_admin_headers(admin_token=self._config.admin_token)
|
|
110
|
+
else:
|
|
111
|
+
headers = build_headers(api_key=self._config.api_key)
|
|
112
|
+
try:
|
|
113
|
+
resp = self._http.request(
|
|
114
|
+
method, path, json=json, params=params,
|
|
115
|
+
headers=headers, timeout=timeout,
|
|
116
|
+
)
|
|
117
|
+
except httpx.ConnectError as e:
|
|
118
|
+
raise APIConnectionError(f"Connection error: {e}") from e
|
|
119
|
+
except httpx.TimeoutException as e:
|
|
120
|
+
raise APIConnectionError(f"Request timed out: {e}") from e
|
|
121
|
+
raise_for_status(resp)
|
|
122
|
+
return resp
|
|
123
|
+
|
|
124
|
+
def _stream_request(
|
|
125
|
+
self,
|
|
126
|
+
method: str,
|
|
127
|
+
path: str,
|
|
128
|
+
*,
|
|
129
|
+
json: dict[str, Any] | None = None,
|
|
130
|
+
headers: dict[str, str] | None = None,
|
|
131
|
+
timeout: float | None = None,
|
|
132
|
+
) -> httpx.Response:
|
|
133
|
+
"""Send a request and return the response for SSE line parsing."""
|
|
134
|
+
if headers is None:
|
|
135
|
+
headers = build_headers(api_key=self._config.api_key)
|
|
136
|
+
try:
|
|
137
|
+
resp = self._http.request(
|
|
138
|
+
method, path, json=json, headers=headers, timeout=timeout,
|
|
139
|
+
)
|
|
140
|
+
except httpx.ConnectError as e:
|
|
141
|
+
raise APIConnectionError(f"Connection error: {e}") from e
|
|
142
|
+
except httpx.TimeoutException as e:
|
|
143
|
+
raise APIConnectionError(f"Request timed out: {e}") from e
|
|
144
|
+
raise_for_status(resp)
|
|
145
|
+
return resp
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
"""Close the underlying HTTP client."""
|
|
149
|
+
self._http.close()
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> Client:
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *args: Any) -> None:
|
|
155
|
+
self.close()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AsyncClient(_ResourceMixin):
|
|
159
|
+
"""Asynchronous SPT Models client."""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
api_key: str | None = None,
|
|
165
|
+
admin_token: str | None = None,
|
|
166
|
+
base_url: str = "",
|
|
167
|
+
timeout: float | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
self._config = BaseClientConfig(
|
|
170
|
+
api_key=api_key, admin_token=admin_token,
|
|
171
|
+
base_url=base_url, timeout=timeout,
|
|
172
|
+
)
|
|
173
|
+
self._http = httpx.AsyncClient(
|
|
174
|
+
base_url=self._config.base_url,
|
|
175
|
+
timeout=httpx.Timeout(self._config.timeout, connect=10.0),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def _request(
|
|
179
|
+
self,
|
|
180
|
+
method: str,
|
|
181
|
+
path: str,
|
|
182
|
+
*,
|
|
183
|
+
json: dict[str, Any] | None = None,
|
|
184
|
+
params: dict[str, Any] | None = None,
|
|
185
|
+
headers: dict[str, str] | None = None,
|
|
186
|
+
timeout: float | None = None,
|
|
187
|
+
admin: bool = False,
|
|
188
|
+
) -> httpx.Response:
|
|
189
|
+
"""Send an async HTTP request with auth headers and error handling."""
|
|
190
|
+
if headers is None:
|
|
191
|
+
if admin:
|
|
192
|
+
headers = build_admin_headers(admin_token=self._config.admin_token)
|
|
193
|
+
else:
|
|
194
|
+
headers = build_headers(api_key=self._config.api_key)
|
|
195
|
+
try:
|
|
196
|
+
resp = await self._http.request(
|
|
197
|
+
method, path, json=json, params=params,
|
|
198
|
+
headers=headers, timeout=timeout,
|
|
199
|
+
)
|
|
200
|
+
except httpx.ConnectError as e:
|
|
201
|
+
raise APIConnectionError(f"Connection error: {e}") from e
|
|
202
|
+
except httpx.TimeoutException as e:
|
|
203
|
+
raise APIConnectionError(f"Request timed out: {e}") from e
|
|
204
|
+
raise_for_status(resp)
|
|
205
|
+
return resp
|
|
206
|
+
|
|
207
|
+
async def _stream_request(
|
|
208
|
+
self,
|
|
209
|
+
method: str,
|
|
210
|
+
path: str,
|
|
211
|
+
*,
|
|
212
|
+
json: dict[str, Any] | None = None,
|
|
213
|
+
headers: dict[str, str] | None = None,
|
|
214
|
+
timeout: float | None = None,
|
|
215
|
+
) -> httpx.Response:
|
|
216
|
+
"""Send an async request and return the response for SSE line parsing."""
|
|
217
|
+
if headers is None:
|
|
218
|
+
headers = build_headers(api_key=self._config.api_key)
|
|
219
|
+
try:
|
|
220
|
+
resp = await self._http.request(
|
|
221
|
+
method, path, json=json, headers=headers, timeout=timeout,
|
|
222
|
+
)
|
|
223
|
+
except httpx.ConnectError as e:
|
|
224
|
+
raise APIConnectionError(f"Connection error: {e}") from e
|
|
225
|
+
except httpx.TimeoutException as e:
|
|
226
|
+
raise APIConnectionError(f"Request timed out: {e}") from e
|
|
227
|
+
raise_for_status(resp)
|
|
228
|
+
return resp
|
|
229
|
+
|
|
230
|
+
async def close(self) -> None:
|
|
231
|
+
"""Close the underlying async HTTP client."""
|
|
232
|
+
await self._http.aclose()
|
|
233
|
+
|
|
234
|
+
async def __aenter__(self) -> AsyncClient:
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
238
|
+
await self.close()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Exception hierarchy for SPT Models client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SPTError(Exception):
|
|
10
|
+
"""Base exception for all SPT Models client errors."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
message: str,
|
|
15
|
+
*,
|
|
16
|
+
status_code: int | None = None,
|
|
17
|
+
response: httpx.Response | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.response = response
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(SPTError):
|
|
25
|
+
"""Raised on 401/403 — invalid or missing API key / admin token."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(SPTError):
|
|
29
|
+
"""Raised on 429 — rate limit exceeded."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str,
|
|
34
|
+
*,
|
|
35
|
+
status_code: int = 429,
|
|
36
|
+
response: httpx.Response | None = None,
|
|
37
|
+
retry_after: int | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(message, status_code=status_code, response=response)
|
|
40
|
+
self.retry_after = retry_after
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotFoundError(SPTError):
|
|
44
|
+
"""Raised on 404 — model, key, or resource not found."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ValidationError(SPTError):
|
|
48
|
+
"""Raised on 400/409/413 — bad request, conflict, or payload too large."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UpstreamError(SPTError):
|
|
52
|
+
"""Raised on 502 — orchestrator or worker unreachable."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class APIConnectionError(SPTError):
|
|
56
|
+
"""Raised when the HTTP connection fails entirely."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class APIStatusError(SPTError):
|
|
60
|
+
"""Raised on unexpected HTTP status codes."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_detail(resp: httpx.Response) -> str:
|
|
64
|
+
"""Extract error message from response body."""
|
|
65
|
+
try:
|
|
66
|
+
body = resp.json()
|
|
67
|
+
if isinstance(body, dict) and "detail" in body:
|
|
68
|
+
detail = body["detail"]
|
|
69
|
+
return detail if isinstance(detail, str) else str(detail)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return resp.text or f"HTTP {resp.status_code}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def raise_for_status(resp: httpx.Response) -> None:
|
|
76
|
+
"""Raise a typed SPTError if the response indicates an error."""
|
|
77
|
+
code = resp.status_code
|
|
78
|
+
if code < 400:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
message = _extract_detail(resp)
|
|
82
|
+
|
|
83
|
+
if code in (401, 403):
|
|
84
|
+
raise AuthenticationError(message, status_code=code, response=resp)
|
|
85
|
+
if code == 404:
|
|
86
|
+
raise NotFoundError(message, status_code=code, response=resp)
|
|
87
|
+
if code == 429:
|
|
88
|
+
retry = resp.headers.get("Retry-After")
|
|
89
|
+
raise RateLimitError(
|
|
90
|
+
message,
|
|
91
|
+
status_code=code,
|
|
92
|
+
response=resp,
|
|
93
|
+
retry_after=int(retry) if retry else None,
|
|
94
|
+
)
|
|
95
|
+
if code in (400, 409, 413):
|
|
96
|
+
raise ValidationError(message, status_code=code, response=resp)
|
|
97
|
+
if code == 502:
|
|
98
|
+
raise UpstreamError(message, status_code=code, response=resp)
|
|
99
|
+
raise APIStatusError(message, status_code=code, response=resp)
|
spt_models/_streaming.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""SSE (Server-Sent Events) stream parsing for sync and async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Iterator, AsyncIterator, TypeVar, Generic, Type
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ._exceptions import SPTError
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T", bound=BaseModel)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_sse_line(line: str) -> dict[str, Any] | None:
|
|
16
|
+
"""Parse a single SSE line. Returns parsed JSON, None for [DONE]/empty, or raises on error."""
|
|
17
|
+
line = line.strip()
|
|
18
|
+
if not line or line.startswith(":"):
|
|
19
|
+
return None
|
|
20
|
+
if line.startswith("data: "):
|
|
21
|
+
data = line[6:]
|
|
22
|
+
if data == "[DONE]":
|
|
23
|
+
return None
|
|
24
|
+
parsed = json.loads(data)
|
|
25
|
+
# Check for error chunks
|
|
26
|
+
choices = parsed.get("choices", [])
|
|
27
|
+
if choices and choices[0].get("finish_reason") == "error":
|
|
28
|
+
raise SPTError("Stream error: server returned error chunk during streaming")
|
|
29
|
+
return parsed
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SyncSSEIterator(Generic[T]):
|
|
34
|
+
"""Synchronous iterator over SSE stream, yielding typed Pydantic models."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, response: Any, model_class: Type[T]) -> None:
|
|
37
|
+
self._response = response
|
|
38
|
+
self._model_class = model_class
|
|
39
|
+
# Parse lines from the response text
|
|
40
|
+
self._lines = iter(response.text.splitlines())
|
|
41
|
+
|
|
42
|
+
def __iter__(self) -> Iterator[T]:
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def __next__(self) -> T:
|
|
46
|
+
while True:
|
|
47
|
+
try:
|
|
48
|
+
line = next(self._lines)
|
|
49
|
+
except StopIteration:
|
|
50
|
+
raise
|
|
51
|
+
parsed = parse_sse_line(line)
|
|
52
|
+
if parsed is not None:
|
|
53
|
+
return self._model_class.model_validate(parsed)
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AsyncSSEIterator(Generic[T]):
|
|
60
|
+
"""Async iterator over SSE stream, yielding typed Pydantic models."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, response: Any, model_class: Type[T]) -> None:
|
|
63
|
+
self._response = response
|
|
64
|
+
self._model_class = model_class
|
|
65
|
+
self._iter = response.aiter_lines()
|
|
66
|
+
|
|
67
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __anext__(self) -> T:
|
|
71
|
+
while True:
|
|
72
|
+
try:
|
|
73
|
+
line = await self._iter.__anext__()
|
|
74
|
+
except StopAsyncIteration:
|
|
75
|
+
await self._response.aclose()
|
|
76
|
+
raise
|
|
77
|
+
parsed = parse_sse_line(line)
|
|
78
|
+
if parsed is not None:
|
|
79
|
+
return self._model_class.model_validate(parsed)
|
|
80
|
+
|
|
81
|
+
async def close(self) -> None:
|
|
82
|
+
await self._response.aclose()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Resource namespace modules for the SPT client."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Admin resource namespace — client.admin.*"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...types.admin import AdminHealth
|
|
8
|
+
from .dashboard import AdminDashboardResource
|
|
9
|
+
from .models import AdminModelsResource
|
|
10
|
+
from .aliases import AdminAliasesResource
|
|
11
|
+
from .keys import AdminKeysResource
|
|
12
|
+
from .usage import AdminUsageResource
|
|
13
|
+
from .gpu import AdminGPUResource
|
|
14
|
+
from .conversion import AdminConversionResource
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AdminResource:
|
|
18
|
+
def __init__(self, client: Any) -> None:
|
|
19
|
+
self._client = client
|
|
20
|
+
self.dashboard = AdminDashboardResource(client)
|
|
21
|
+
self.models = AdminModelsResource(client)
|
|
22
|
+
self.aliases = AdminAliasesResource(client)
|
|
23
|
+
self.keys = AdminKeysResource(client)
|
|
24
|
+
self.usage = AdminUsageResource(client)
|
|
25
|
+
self.gpu = AdminGPUResource(client)
|
|
26
|
+
self.conversion = AdminConversionResource(client)
|
|
27
|
+
|
|
28
|
+
def login(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
29
|
+
resp = self._client._request(
|
|
30
|
+
"POST", "/admin/api/auth/login",
|
|
31
|
+
json={"token": self._client._config.admin_token},
|
|
32
|
+
admin=True, timeout=timeout,
|
|
33
|
+
)
|
|
34
|
+
return resp.json()
|
|
35
|
+
|
|
36
|
+
async def alogin(self, *, timeout: float | None = None) -> dict[str, Any]:
|
|
37
|
+
resp = await self._client._request(
|
|
38
|
+
"POST", "/admin/api/auth/login",
|
|
39
|
+
json={"token": self._client._config.admin_token},
|
|
40
|
+
admin=True, timeout=timeout,
|
|
41
|
+
)
|
|
42
|
+
return resp.json()
|
|
43
|
+
|
|
44
|
+
def health(self, *, timeout: float | None = None) -> AdminHealth:
|
|
45
|
+
resp = self._client._request("GET", "/admin/api/health", admin=True, timeout=timeout)
|
|
46
|
+
return AdminHealth.model_validate(resp.json())
|
|
47
|
+
|
|
48
|
+
async def ahealth(self, *, timeout: float | None = None) -> AdminHealth:
|
|
49
|
+
resp = await self._client._request(
|
|
50
|
+
"GET", "/admin/api/health", admin=True, timeout=timeout
|
|
51
|
+
)
|
|
52
|
+
return AdminHealth.model_validate(resp.json())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Admin model aliases resource — client-facing names that point at a catalogue model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ...types.admin import AdminAlias
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AdminAliasesResource:
|
|
14
|
+
def __init__(self, client: Any) -> None:
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def list(self, *, timeout: float | None = None) -> list[AdminAlias]:
|
|
18
|
+
resp = self._client._request("GET", "/admin/api/aliases", admin=True, timeout=timeout)
|
|
19
|
+
return [AdminAlias.model_validate(a) for a in resp.json()]
|
|
20
|
+
|
|
21
|
+
async def alist(self, *, timeout: float | None = None) -> list[AdminAlias]:
|
|
22
|
+
resp = await self._client._request(
|
|
23
|
+
"GET", "/admin/api/aliases", admin=True, timeout=timeout
|
|
24
|
+
)
|
|
25
|
+
return [AdminAlias.model_validate(a) for a in resp.json()]
|
|
26
|
+
|
|
27
|
+
def create(
|
|
28
|
+
self,
|
|
29
|
+
alias: str,
|
|
30
|
+
model_id: int,
|
|
31
|
+
*,
|
|
32
|
+
description: str = "",
|
|
33
|
+
enabled: bool = True,
|
|
34
|
+
timeout: float | None = None,
|
|
35
|
+
) -> AdminAlias:
|
|
36
|
+
body: dict[str, Any] = {
|
|
37
|
+
"alias": alias,
|
|
38
|
+
"model_id": model_id,
|
|
39
|
+
"description": description,
|
|
40
|
+
"enabled": enabled,
|
|
41
|
+
}
|
|
42
|
+
resp = self._client._request(
|
|
43
|
+
"POST", "/admin/api/aliases", json=body, admin=True, timeout=timeout
|
|
44
|
+
)
|
|
45
|
+
return AdminAlias.model_validate(resp.json())
|
|
46
|
+
|
|
47
|
+
async def acreate(
|
|
48
|
+
self,
|
|
49
|
+
alias: str,
|
|
50
|
+
model_id: int,
|
|
51
|
+
*,
|
|
52
|
+
description: str = "",
|
|
53
|
+
enabled: bool = True,
|
|
54
|
+
timeout: float | None = None,
|
|
55
|
+
) -> AdminAlias:
|
|
56
|
+
body: dict[str, Any] = {
|
|
57
|
+
"alias": alias,
|
|
58
|
+
"model_id": model_id,
|
|
59
|
+
"description": description,
|
|
60
|
+
"enabled": enabled,
|
|
61
|
+
}
|
|
62
|
+
resp = await self._client._request(
|
|
63
|
+
"POST", "/admin/api/aliases", json=body, admin=True, timeout=timeout
|
|
64
|
+
)
|
|
65
|
+
return AdminAlias.model_validate(resp.json())
|
|
66
|
+
|
|
67
|
+
def update(
|
|
68
|
+
self, alias_id: int, *, timeout: float | None = None, **kwargs: Any
|
|
69
|
+
) -> AdminAlias:
|
|
70
|
+
resp = self._client._request(
|
|
71
|
+
"PUT", f"/admin/api/aliases/{alias_id}", json=kwargs, admin=True, timeout=timeout
|
|
72
|
+
)
|
|
73
|
+
return AdminAlias.model_validate(resp.json())
|
|
74
|
+
|
|
75
|
+
async def aupdate(
|
|
76
|
+
self, alias_id: int, *, timeout: float | None = None, **kwargs: Any
|
|
77
|
+
) -> AdminAlias:
|
|
78
|
+
resp = await self._client._request(
|
|
79
|
+
"PUT", f"/admin/api/aliases/{alias_id}", json=kwargs, admin=True, timeout=timeout
|
|
80
|
+
)
|
|
81
|
+
return AdminAlias.model_validate(resp.json())
|
|
82
|
+
|
|
83
|
+
def delete(self, alias_id: int, *, timeout: float | None = None) -> None:
|
|
84
|
+
self._client._request(
|
|
85
|
+
"DELETE", f"/admin/api/aliases/{alias_id}", admin=True, timeout=timeout
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def adelete(self, alias_id: int, *, timeout: float | None = None) -> None:
|
|
89
|
+
await self._client._request(
|
|
90
|
+
"DELETE", f"/admin/api/aliases/{alias_id}", admin=True, timeout=timeout
|
|
91
|
+
)
|