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.
- spt_models-0.2.0/.gitignore +53 -0
- spt_models-0.2.0/LICENSE +21 -0
- spt_models-0.2.0/PKG-INFO +65 -0
- spt_models-0.2.0/README.md +34 -0
- spt_models-0.2.0/deploy-pypi.sh +68 -0
- spt_models-0.2.0/pyproject.toml +55 -0
- spt_models-0.2.0/src/spt_models/__init__.py +52 -0
- spt_models-0.2.0/src/spt_models/_base.py +50 -0
- spt_models-0.2.0/src/spt_models/_client.py +238 -0
- spt_models-0.2.0/src/spt_models/_exceptions.py +99 -0
- spt_models-0.2.0/src/spt_models/_streaming.py +82 -0
- spt_models-0.2.0/src/spt_models/resources/__init__.py +1 -0
- spt_models-0.2.0/src/spt_models/resources/admin/__init__.py +52 -0
- spt_models-0.2.0/src/spt_models/resources/admin/aliases.py +91 -0
- spt_models-0.2.0/src/spt_models/resources/admin/conversion.py +81 -0
- spt_models-0.2.0/src/spt_models/resources/admin/dashboard.py +39 -0
- spt_models-0.2.0/src/spt_models/resources/admin/gpu.py +51 -0
- spt_models-0.2.0/src/spt_models/resources/admin/keys.py +65 -0
- spt_models-0.2.0/src/spt_models/resources/admin/models.py +310 -0
- spt_models-0.2.0/src/spt_models/resources/admin/usage.py +148 -0
- spt_models-0.2.0/src/spt_models/resources/audio.py +156 -0
- spt_models-0.2.0/src/spt_models/resources/chat.py +78 -0
- spt_models-0.2.0/src/spt_models/resources/completions.py +38 -0
- spt_models-0.2.0/src/spt_models/resources/embeddings.py +29 -0
- spt_models-0.2.0/src/spt_models/resources/images.py +27 -0
- spt_models-0.2.0/src/spt_models/resources/models.py +45 -0
- spt_models-0.2.0/src/spt_models/resources/videos.py +27 -0
- spt_models-0.2.0/src/spt_models/types/__init__.py +1 -0
- spt_models-0.2.0/src/spt_models/types/admin.py +248 -0
- spt_models-0.2.0/src/spt_models/types/audio.py +10 -0
- spt_models-0.2.0/src/spt_models/types/chat.py +55 -0
- spt_models-0.2.0/src/spt_models/types/completions.py +36 -0
- spt_models-0.2.0/src/spt_models/types/embeddings.py +20 -0
- spt_models-0.2.0/src/spt_models/types/images.py +19 -0
- spt_models-0.2.0/src/spt_models/types/models.py +47 -0
- spt_models-0.2.0/tests/test_admin_aliases.py +144 -0
- spt_models-0.2.0/tests/test_admin_conversion.py +79 -0
- spt_models-0.2.0/tests/test_admin_dashboard.py +64 -0
- spt_models-0.2.0/tests/test_admin_gpu.py +85 -0
- spt_models-0.2.0/tests/test_admin_keys.py +81 -0
- spt_models-0.2.0/tests/test_admin_models.py +256 -0
- spt_models-0.2.0/tests/test_admin_usage.py +143 -0
- spt_models-0.2.0/tests/test_audio.py +79 -0
- spt_models-0.2.0/tests/test_base.py +59 -0
- spt_models-0.2.0/tests/test_chat.py +89 -0
- spt_models-0.2.0/tests/test_client_init.py +69 -0
- spt_models-0.2.0/tests/test_exceptions.py +103 -0
- spt_models-0.2.0/tests/test_inference_resources.py +108 -0
- spt_models-0.2.0/tests/test_models_resource.py +93 -0
- spt_models-0.2.0/tests/test_streaming.py +29 -0
- spt_models-0.2.0/tests/test_types_admin.py +166 -0
- spt_models-0.2.0/tests/test_types_inference.py +169 -0
- 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
|
spt_models-0.2.0/LICENSE
ADDED
|
@@ -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()
|