spt-models 0.2.0__tar.gz

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.
Files changed (53) hide show
  1. spt_models-0.2.0/.gitignore +53 -0
  2. spt_models-0.2.0/LICENSE +21 -0
  3. spt_models-0.2.0/PKG-INFO +65 -0
  4. spt_models-0.2.0/README.md +34 -0
  5. spt_models-0.2.0/deploy-pypi.sh +68 -0
  6. spt_models-0.2.0/pyproject.toml +55 -0
  7. spt_models-0.2.0/src/spt_models/__init__.py +52 -0
  8. spt_models-0.2.0/src/spt_models/_base.py +50 -0
  9. spt_models-0.2.0/src/spt_models/_client.py +238 -0
  10. spt_models-0.2.0/src/spt_models/_exceptions.py +99 -0
  11. spt_models-0.2.0/src/spt_models/_streaming.py +82 -0
  12. spt_models-0.2.0/src/spt_models/resources/__init__.py +1 -0
  13. spt_models-0.2.0/src/spt_models/resources/admin/__init__.py +52 -0
  14. spt_models-0.2.0/src/spt_models/resources/admin/aliases.py +91 -0
  15. spt_models-0.2.0/src/spt_models/resources/admin/conversion.py +81 -0
  16. spt_models-0.2.0/src/spt_models/resources/admin/dashboard.py +39 -0
  17. spt_models-0.2.0/src/spt_models/resources/admin/gpu.py +51 -0
  18. spt_models-0.2.0/src/spt_models/resources/admin/keys.py +65 -0
  19. spt_models-0.2.0/src/spt_models/resources/admin/models.py +310 -0
  20. spt_models-0.2.0/src/spt_models/resources/admin/usage.py +148 -0
  21. spt_models-0.2.0/src/spt_models/resources/audio.py +156 -0
  22. spt_models-0.2.0/src/spt_models/resources/chat.py +78 -0
  23. spt_models-0.2.0/src/spt_models/resources/completions.py +38 -0
  24. spt_models-0.2.0/src/spt_models/resources/embeddings.py +29 -0
  25. spt_models-0.2.0/src/spt_models/resources/images.py +27 -0
  26. spt_models-0.2.0/src/spt_models/resources/models.py +45 -0
  27. spt_models-0.2.0/src/spt_models/resources/videos.py +27 -0
  28. spt_models-0.2.0/src/spt_models/types/__init__.py +1 -0
  29. spt_models-0.2.0/src/spt_models/types/admin.py +248 -0
  30. spt_models-0.2.0/src/spt_models/types/audio.py +10 -0
  31. spt_models-0.2.0/src/spt_models/types/chat.py +55 -0
  32. spt_models-0.2.0/src/spt_models/types/completions.py +36 -0
  33. spt_models-0.2.0/src/spt_models/types/embeddings.py +20 -0
  34. spt_models-0.2.0/src/spt_models/types/images.py +19 -0
  35. spt_models-0.2.0/src/spt_models/types/models.py +47 -0
  36. spt_models-0.2.0/tests/test_admin_aliases.py +144 -0
  37. spt_models-0.2.0/tests/test_admin_conversion.py +79 -0
  38. spt_models-0.2.0/tests/test_admin_dashboard.py +64 -0
  39. spt_models-0.2.0/tests/test_admin_gpu.py +85 -0
  40. spt_models-0.2.0/tests/test_admin_keys.py +81 -0
  41. spt_models-0.2.0/tests/test_admin_models.py +256 -0
  42. spt_models-0.2.0/tests/test_admin_usage.py +143 -0
  43. spt_models-0.2.0/tests/test_audio.py +79 -0
  44. spt_models-0.2.0/tests/test_base.py +59 -0
  45. spt_models-0.2.0/tests/test_chat.py +89 -0
  46. spt_models-0.2.0/tests/test_client_init.py +69 -0
  47. spt_models-0.2.0/tests/test_exceptions.py +103 -0
  48. spt_models-0.2.0/tests/test_inference_resources.py +108 -0
  49. spt_models-0.2.0/tests/test_models_resource.py +93 -0
  50. spt_models-0.2.0/tests/test_streaming.py +29 -0
  51. spt_models-0.2.0/tests/test_types_admin.py +166 -0
  52. spt_models-0.2.0/tests/test_types_inference.py +169 -0
  53. spt_models-0.2.0/uv.lock +359 -0
@@ -0,0 +1,53 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.egg
10
+ .venv/
11
+ venv/
12
+
13
+ # Node / Admin
14
+ node_modules/
15
+ admin/dist/
16
+
17
+ # Environment
18
+ .env
19
+ .env.local
20
+ .env.*.local
21
+
22
+ # IDE
23
+ .vscode/
24
+ .idea/
25
+ *.swp
26
+ *.swo
27
+ *~
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Docker
34
+ *.log
35
+
36
+ # Models cache (large files)
37
+ /data/
38
+ *.bin
39
+ *.safetensors
40
+ *.gguf
41
+
42
+ # Testing
43
+ .pytest_cache/
44
+ .coverage
45
+ htmlcov/
46
+
47
+ # Session artefacts — agent caches, local test inputs, ad-hoc outputs
48
+ .playwright-mcp/
49
+ examples/
50
+ results/
51
+
52
+ # Harness runtime lock (do not commit)
53
+ .claude/scheduled_tasks.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sponge Theory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.5
2
+ Name: spt-models
3
+ Version: 0.2.0
4
+ Summary: Python client for the SPT Models GPU inference platform
5
+ Project-URL: Homepage, https://models.sponge-theory.dev
6
+ Project-URL: Repository, https://github.com/sponge-theory/spt-models
7
+ Project-URL: Documentation, https://github.com/sponge-theory/spt-models/tree/main/spt-models-python
8
+ Author-email: Sponge Theory <dev@sponge-theory.dev>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,api-client,gpu,inference,llm,ml,openai
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx<1,>=0.27
23
+ Requires-Dist: pydantic<3,>=2
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
26
+ Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: respx>=0.22; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # spt-models
33
+
34
+ Python client for the SPT Models GPU inference platform.
35
+
36
+ ## Model aliases
37
+
38
+ An admin can publish client-facing names (`gpt-4`, `prod-embeddings`, …) that
39
+ point at a catalogue model. Every `/v1/*` call accepts an alias wherever it
40
+ accepts a slug, and echoes the alias back in the response `model` field.
41
+ `client.admin.aliases` manages them (admin token required); `Model.alias_of`
42
+ tells an alias entry apart from a real model in `client.models.list()`.
43
+
44
+ ```python
45
+ from spt_models import Client
46
+
47
+ with Client(api_key="sk-...", admin_token="...") as client:
48
+ # Create an alias pointing at model id 1, then use it like a slug.
49
+ alias = client.admin.aliases.create("gpt-4", 1, description="OpenAI drop-in")
50
+ resp = client.chat.completions.create(
51
+ model="gpt-4", messages=[{"role": "user", "content": "Hello"}]
52
+ )
53
+ assert resp.model == "gpt-4" # the alias is echoed, not the slug
54
+
55
+ # Repoint, disable, or drop it — clients keep the same name throughout.
56
+ client.admin.aliases.update(alias.id, model_id=2)
57
+ client.admin.aliases.update(alias.id, enabled=False)
58
+ client.admin.aliases.delete(alias.id)
59
+
60
+ for m in client.models.list().data:
61
+ if m.alias_of:
62
+ print(f"{m.id} -> {m.alias_of}")
63
+ ```
64
+
65
+ Async variants follow the usual `a` prefix: `alist`, `acreate`, `aupdate`, `adelete`.
@@ -0,0 +1,34 @@
1
+ # spt-models
2
+
3
+ Python client for the SPT Models GPU inference platform.
4
+
5
+ ## Model aliases
6
+
7
+ An admin can publish client-facing names (`gpt-4`, `prod-embeddings`, …) that
8
+ point at a catalogue model. Every `/v1/*` call accepts an alias wherever it
9
+ accepts a slug, and echoes the alias back in the response `model` field.
10
+ `client.admin.aliases` manages them (admin token required); `Model.alias_of`
11
+ tells an alias entry apart from a real model in `client.models.list()`.
12
+
13
+ ```python
14
+ from spt_models import Client
15
+
16
+ with Client(api_key="sk-...", admin_token="...") as client:
17
+ # Create an alias pointing at model id 1, then use it like a slug.
18
+ alias = client.admin.aliases.create("gpt-4", 1, description="OpenAI drop-in")
19
+ resp = client.chat.completions.create(
20
+ model="gpt-4", messages=[{"role": "user", "content": "Hello"}]
21
+ )
22
+ assert resp.model == "gpt-4" # the alias is echoed, not the slug
23
+
24
+ # Repoint, disable, or drop it — clients keep the same name throughout.
25
+ client.admin.aliases.update(alias.id, model_id=2)
26
+ client.admin.aliases.update(alias.id, enabled=False)
27
+ client.admin.aliases.delete(alias.id)
28
+
29
+ for m in client.models.list().data:
30
+ if m.alias_of:
31
+ print(f"{m.id} -> {m.alias_of}")
32
+ ```
33
+
34
+ Async variants follow the usual `a` prefix: `alist`, `acreate`, `aupdate`, `adelete`.
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Deploy spt-models to PyPI
5
+ # Usage:
6
+ # ./deploy-pypi.sh # deploy to PyPI
7
+ # ./deploy-pypi.sh --test # deploy to TestPyPI first
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ cd "$SCRIPT_DIR"
11
+
12
+ # Colors
13
+ RED='\033[0;31m'
14
+ GREEN='\033[0;32m'
15
+ YELLOW='\033[1;33m'
16
+ NC='\033[0m'
17
+
18
+ info() { echo -e "${GREEN}[INFO]${NC} $*"; }
19
+ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
20
+ error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
21
+
22
+ # Check dependencies
23
+ command -v uv >/dev/null 2>&1 || error "uv is required. Install: curl -LsSf https://astral.sh/uv/install.sh | sh"
24
+
25
+ # Parse version from pyproject.toml
26
+ VERSION=$(python3 -c "
27
+ import tomllib
28
+ with open('pyproject.toml', 'rb') as f:
29
+ print(tomllib.load(f)['project']['version'])
30
+ ")
31
+ info "Package version: $VERSION"
32
+
33
+ # Clean previous builds
34
+ info "Cleaning previous builds..."
35
+ rm -rf dist/ build/ src/*.egg-info
36
+
37
+ # Build
38
+ info "Building sdist and wheel..."
39
+ uv build
40
+
41
+ # Verify build artifacts
42
+ SDIST="dist/spt_models-${VERSION}.tar.gz"
43
+ WHEEL=$(ls dist/spt_models-${VERSION}-*.whl 2>/dev/null | head -1)
44
+
45
+ [ -f "$SDIST" ] || error "sdist not found: $SDIST"
46
+ [ -n "$WHEEL" ] || error "wheel not found in dist/"
47
+
48
+ info "Built: $(basename "$SDIST")"
49
+ info "Built: $(basename "$WHEEL")"
50
+
51
+ # Upload
52
+ if [[ "${1:-}" == "--test" ]]; then
53
+ info "Uploading to TestPyPI..."
54
+ uv publish --publish-url https://test.pypi.org/legacy/
55
+ info "Uploaded to TestPyPI"
56
+ echo ""
57
+ info "Install from TestPyPI with:"
58
+ echo " uv pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ spt-models==$VERSION"
59
+ echo ""
60
+ warn "Run again without --test to publish to production PyPI"
61
+ else
62
+ info "Uploading to PyPI..."
63
+ uv publish
64
+ info "Published spt-models==$VERSION to PyPI"
65
+ echo ""
66
+ info "Install with:"
67
+ echo " uv pip install spt-models==$VERSION"
68
+ fi
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "spt-models"
7
+ version = "0.2.0"
8
+ description = "Python client for the SPT Models GPU inference platform"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Sponge Theory", email = "dev@sponge-theory.dev" },
14
+ ]
15
+ keywords = ["ai", "ml", "inference", "gpu", "llm", "openai", "api-client"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27,<1",
29
+ "pydantic>=2,<3",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://models.sponge-theory.dev"
34
+ Repository = "https://github.com/sponge-theory/spt-models"
35
+ Documentation = "https://github.com/sponge-theory/spt-models/tree/main/spt-models-python"
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=8",
40
+ "pytest-asyncio>=0.24",
41
+ "pytest-httpx>=0.34",
42
+ "respx>=0.22",
43
+ "ruff>=0.8",
44
+ ]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/spt_models"]
48
+
49
+ [tool.pytest.ini_options]
50
+ asyncio_mode = "auto"
51
+ testpaths = ["tests"]
52
+
53
+ [tool.ruff]
54
+ target-version = "py311"
55
+ line-length = 100
@@ -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
+ ]
@@ -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
@@ -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()