Metadata-Version: 2.5
Name: spt-models
Version: 0.6.0
Summary: Python client for the SPT Models GPU inference platform
Project-URL: Homepage, https://sponge-theory.ai
Project-URL: Repository, https://github.com/sponge-theory/spt-models
Project-URL: Documentation, https://github.com/sponge-theory/spt-models/tree/main/spt-models-python
Author-email: Sponge Theory <contact@sponge-theory.ai>
License-Expression: MIT
License-File: LICENSE
Keywords: ai,api-client,gpu,inference,llm,ml,openai
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-httpx>=0.34; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# spt-models

Python client for the SPT Models GPU inference platform.

## Thinking models and streamed tool calls

Responses keep every field the gateway sends. `message.reasoning_content` and
`usage.reasoning_tokens` carry a thinking model's reasoning; models that
preserve thinking across turns expect `reasoning_content` back in the assistant
message of the history. A stream delivers tool calls as OpenAI fragments on
`delta.tool_calls`, next to `delta.content` and `delta.reasoning_content`:

```python
calls: dict[int, dict] = {}
for chunk in client.chat.completions.create(model="default-vlm", messages=messages,
                                            tools=tools, stream=True):
    for fragment in chunk.choices[0].delta.tool_calls or []:
        call = calls.setdefault(fragment["index"], {"name": "", "arguments": ""})
        call["name"] += fragment.get("function", {}).get("name") or ""
        call["arguments"] += fragment.get("function", {}).get("arguments") or ""
```

A field the gateway adds later stays reachable through `model_extra`.

## Speech

`client.audio.speech.create()` returns the audio bytes. Since 0.4.1 the JSON
envelope the gateway sends for a non-streamed answer is decoded, as
`client.music.generate()` already did; streamed audio and SSE events come back
unchanged.

```python
wav = client.audio.speech.create(model="default-tts", input="Bonjour", response_format="wav")
pathlib.Path("bonjour.wav").write_bytes(wav)
```

## Model cards

`client.models.retrieve(name)` returns the model card: `type`, `description`,
`capabilities` and `prompting_guide`, whose `recommended_params` are the
settings to apply before inferring. `client.models.list(verbose=True)` returns
the same card for every model; `list()` without the flag is unchanged.

```python
card = client.models.retrieve("default-image-model")
params = (card.prompting_guide or {}).get("recommended_params", {})
```

## Music transcription

`client.music.transcribe()` turns a song recording into a score (ABC, keys,
sections, optional MIDI). Only the options you pass are sent; `wait=True` runs
the transcription as a job and polls it until it ends.

```python
score = client.music.transcribe(model="sheetsage2", file="song.wav",
                                task="melody_full", include_midi=True, wait=True)
print(score.abc or score.abc_error)
```

`await client.music.atranscribe(...)` is the async counterpart.

## Audio separation (vocals / accompaniment)

`client.audio.separations.create()` splits a mixed song (up to 90 s) into
stems. Each stem is a WAV with the input's sample rate, channel count and exact
number of samples, so times measured on the vocal stem (word timestamps from
whisperx, for instance) apply to the mix unchanged.

```python
sep = client.audio.separations.create(model="htdemucs", file="song.wav",
                                      stems="vocals", wait=True)
sep.save("vocals", "song.vocals.wav")
words = client.audio.transcriptions.create(model="whisperx-large-v3",
                                           file="song.vocals.wav", language="en")
```

`stems` takes `"vocals"` (default), `"accompaniment"`, `"vocals,accompaniment"`
or a list. `await client.audio.separations.create(...)` on an `AsyncClient`.

## Classifications (typed decisions)

`client.classifications.create()` asks typed questions of one document and
returns a distribution per question. `input` is ONE document (a text, a JSON
object, or a list of conversation turns — a list is one structured content, not a
batch); `items=[{"id", "input"}]` answers several documents separately.

```python
res = client.classifications.create(
    model="laya-multilingual",
    input="Ma facture de septembre comporte un double prélèvement...",
    questions={
        "department": {"type": "choice", "instructions": "Quel service doit traiter ce message ?",
                       "criteria": {"billing": "factures, paiements", "technical": "bugs, pannes",
                                    "sales": "tarifs", "other": "tout le reste"}},
        "urgency": {"type": "score", "instructions": "Niveau d'urgence ?",
                    "criteria": ["pas urgent", "bientôt", "bloquant"]},
        "board_billing": {"type": "noul", "instructions": "Ce message relève-t-il du tableau Facturation ?"},
    },
)
res.answers["department"].choice          # "billing"
res.answers["department"].probabilities   # per label
res.answers["urgency"].score              # expected level, 0..2
res.answers["board_billing"].noul         # P(true)
res.device                                # "cuda", "mps" or "cpu" — what actually ran
```

What the numbers are: `probabilities` is the model's temperature-scaled softmax;
`confidence` on `choice`/`score` is 1 − normalised entropy (how concentrated the
distribution is, **not** a probability of being right); on `noul` it is
max(P, 1−P). Nothing is calibrated on your data — read `noul` / `probabilities`
and choose your own thresholds.

Truncation is explicit: each question sees at most `limits.max_len` tokens
(instructions and options first, then the document) and option descriptions are
cut at 48 tokens. By default such a request is refused (400 with the counts);
`on_truncation="truncate"` answers anyway and fills `truncation`.

`await client.classifications.acreate(...)` is the async counterpart.

## Model aliases

An admin can publish client-facing names (`gpt-4`, `prod-embeddings`, …) that
point at a catalogue model. Every `/v1/*` call accepts an alias wherever it
accepts a slug, and echoes the alias back in the response `model` field.
`client.admin.aliases` manages them (admin token required); `Model.alias_of`
tells an alias entry apart from a real model in `client.models.list()`.

```python
from spt_models import Client

with Client(api_key="sk-...", admin_token="...") as client:
    # Create an alias pointing at model id 1, then use it like a slug.
    alias = client.admin.aliases.create("gpt-4", 1, description="OpenAI drop-in")
    resp = client.chat.completions.create(
        model="gpt-4", messages=[{"role": "user", "content": "Hello"}]
    )
    assert resp.model == "gpt-4"          # the alias is echoed, not the slug

    # Repoint, disable, or drop it — clients keep the same name throughout.
    client.admin.aliases.update(alias.id, model_id=2)
    client.admin.aliases.update(alias.id, enabled=False)
    client.admin.aliases.delete(alias.id)

    for m in client.models.list().data:
        if m.alias_of:
            print(f"{m.id} -> {m.alias_of}")
```

Async variants follow the usual `a` prefix: `alist`, `acreate`, `aupdate`, `adelete`.
