Metadata-Version: 2.5
Name: spt-models
Version: 0.5.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.

## 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`.
