Metadata-Version: 2.5
Name: zarx
Version: 0.3.0
Summary: Python SDK for Zarx, the OpenAI-compatible inference API run by NPAW in Europe.
Project-URL: Homepage, https://zarx.ai
Author-email: NPAW <contact@zarx.ai>
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: europe,inference,llm,openai,zarx
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: openai<4,>=1.99
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: websockets>=12; extra == 'dev'
Provides-Extra: realtime
Requires-Dist: websockets>=12; extra == 'realtime'
Description-Content-Type: text/markdown

# zarx (Python)

Python SDK and command line for [Zarx](https://zarx.ai), the OpenAI-compatible inference API that NPAW runs on its own GPUs in Spain (EU).

`zarx` is a thin layer over the official `openai` package: `zarx.Zarx` *is* an `openai.OpenAI` client, so everything you know from the OpenAI SDK works as is. What the layer adds:

- the right endpoint and key: production by default, beta with `environment="beta"` or `ZARX_ENVIRONMENT=beta`, and `ZARX_API_KEY` only (an `OPENAI_API_KEY` is never sent to Zarx, an `OPENAI_BASE_URL` never redirects it);
- Responses API calls default to `store=False`, so responses are not stored for later retrieval unless you ask for it (what else is retained depends on your key's retention policy, see `docs/data.md`);
- generous read timeouts for the reasoning tiers (300 s read, 10 s connect) and 2 retries;
- model name helpers (tiers and dated ids), a helper to read the reasoning text, and a `zarx` command;
- one call per capability beyond chat: `zarx.audio.transcribe`, `transcribe_realtime`, `speak`, `translate`, `zarx.forecast` / `forecast_csv` for time series, and `zarx.embed` for embeddings.

Requires Python 3.9 or newer (0.1.0 verified on 3.9 to 3.13 on 2026-10-31; 0.2.0 run on 3.13 on 2026-11-01; 0.3.0 on 3.13 on 2026-11-05).

## Install

```bash
pip install zarx
pip install "zarx[realtime]"   # adds websockets, for transcribe_realtime
```

or from a checkout of this repository: `pip install ./python` (`pip install './python[realtime]'` for realtime transcription).

## Quickstart

```bash
export ZARX_API_KEY=...            # your Zarx key
# export ZARX_ENVIRONMENT=beta     # only for beta, with a beta key
```

```python
from zarx import Zarx, Z_FAST

client = Zarx()
completion = client.chat.completions.create(
    model=Z_FAST,
    messages=[{"role": "user", "content": "Say hello in one sentence."}],
    max_tokens=64,
)
print(completion.choices[0].message.content)
print(completion.model)  # the dated id that answered, e.g. z-fast-2026-10
```

Async: `from zarx import AsyncZarx`, same arguments.

### Plain OpenAI SDK

You do not need this package to use Zarx. The official SDK works with two settings:

```python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["ZARX_API_KEY"], base_url="https://api.zarx.ai/v1")
```

If you go this way, pass `store=False` yourself on every Responses API call.

## Models

| Tier | Use it for | Context |
|---|---|---|
| `z-ultra` | hardest reasoning, long documents | 600,000 tokens |
| `z-pro` | strong general model with reasoning, very long documents | 1,048,576 tokens (1M) |
| `z-fast` | low latency, high volume | 16,384 tokens |

`client.models.list()` is the authority on these values; production and beta both list 1,048,576 tokens for `z-pro` (checked 2026-11-01). `z-ultra` and `z-pro` also read images; `z-fast` is text only. See `docs/models.md`.

A tier always points at its current release. A dated id names one release, and the `model` field of every answer is the dated id of the release that answered (once a release is retired, its dated id is answered by the newer one; see `docs/models.md`):

```python
from zarx import pin, tier_of, Z_FAST

pin(Z_FAST, "2026-10")         # "z-fast-2026-10"
tier_of("z-fast-2026-10")      # "z-fast"
```

Reasoning tokens count toward `max_tokens`: give `z-pro` at least 128 (`zarx.models.Z_PRO_MIN_MAX_TOKENS`), or the answer can come back empty with `finish_reason` `length`. The reasoning tiers accept `reasoning_effort` (`none`, `low`, `medium`, `high`) and return their reasoning next to the answer. `z-ultra` still reasons with `none` (stage, 2026-10-31), so leave it room in `max_tokens`:

```python
from zarx import reasoning_text

message = completion.choices[0].message
print(reasoning_text(message))  # None when the tier returned no reasoning
```

## Responses API and data

```python
response = client.responses.create(model="z-pro", input="Two lines on GDPR.", max_output_tokens=256)
print(response.output_text)
```

This client sends `store=False` unless you pass `store=True`. The server default is `store=True`, and how long stored content is kept depends on your key's retention policy. See `docs/data.md` in the repository.

## Errors

Zarx answers in the OpenAI error format, so the OpenAI exception classes apply and are re-exported:

```python
import zarx

try:
    client.chat.completions.create(model="z-unknown", messages=[{"role": "user", "content": "hi"}])
except zarx.NotFoundError as exc:
    print(exc.code)  # model_not_found
```

A missing key raises `zarx.ZarxConfigError` before any request is made.

### Retries and rate limits

Like the OpenAI client, `Zarx` retries twice on connection errors, 408, 409, 429 and 5xx. On a 429 it waits what the `retry-after` header says (up to 59 seconds), so one call can block silently for up to about 2 minutes. Zarx keeps nothing after the response, so a retry runs the request again (there is no server-side deduplication; `Idempotency-Key` is ignored). When latency or a duplicate matters, turn retries off and handle the error yourself:

```python
client = Zarx(max_retries=0)
try:
    client.chat.completions.create(model="z-fast", messages=[{"role": "user", "content": "hi"}])
except zarx.RateLimitError as exc:
    print(exc.response.headers.get("retry-after"))  # seconds until the next minute window
```

Chat Completions responses carry `x-ratelimit-*` headers (requests and tokens: limit, remaining, reset) when your account has rate limits set, and every response carries an `x-request-id`. The per-minute window is the clock minute. See `docs/errors.md`.

## Production or beta

| Environment | Base URL | What it is |
|---|---|---|
| `production` (default) | `https://api.zarx.ai/v1` | the stable API |
| `beta` | `https://api-stage.zarx.ai/v1` | the stage environment: new models and features land here first and may change |

```python
from zarx import Zarx

client = Zarx()                         # production
beta = Zarx(environment="beta")         # beta; "prod" and "stage" also work
```

The first that is set wins: `base_url=`, `environment=`, `ZARX_BASE_URL`, `ZARX_ENVIRONMENT`, then production. What the code says beats what the shell says. Keys are per environment: a production key gets 401 on beta and the other way round, so set the key that matches. `zarx.ENVIRONMENTS`, `zarx.PRODUCTION_BASE_URL` and `zarx.BETA_BASE_URL` are exported; `STAGE_BASE_URL` stays as an alias of `BETA_BASE_URL`. Realtime transcription follows the client's environment.

## Command line

```bash
zarx models                       # models your key can use
zarx chat "Hello"                 # streams the answer from z-fast
zarx chat -m z-pro --show-reasoning "Why is the sky blue?"
zarx speak "Good morning" -o hello.wav      # text to speech (WAV; -f mp3 for MP3, -v for the voice)
zarx transcribe hello.wav                   # speech to text
zarx translate "Good morning" --to es       # text translation (a code or a name: es, Spanish)
zarx forecast 3 4 6 5 7 9 -p 3              # next 3 periods of a series (hourly steps ending now)
zarx forecast --csv visits.csv -p 24 --band # from a CSV, with a low/high band; --json for the raw answer
zarx --beta models                # same against beta (or ZARX_ENVIRONMENT=beta)
```

Exit codes: 0 success, 1 API or network error, 2 configuration or input error (for example, no key, or fewer than 3 values to forecast).

If the console cannot print a character of the answer (for example a Windows console in cp1252 and an answer with Japanese text), `zarx` prints `?` in its place instead of failing. Set `PYTHONIOENCODING=utf-8` to see every character.

## Audio

One call each, with the client built from the environment unless you pass `client=`:

```python
from zarx import audio

text = audio.transcribe("meeting.wav")                       # speech to text
wav = audio.speak("Good morning", voice="marin")             # text to speech, WAV bytes
spanish = audio.translate("Good morning", "Spanish")         # text translation
text = audio.transcribe_realtime("meeting-16k.wav", on_delta=print)  # text as it is recognised
spanish_wav = audio.speech_to_speech("meeting.wav", "es")      # voice to voice, WAV bytes
for piece in audio.speak_stream("Good morning"):             # speech in pieces, PCM 24 kHz
    player.write(piece)
```

Voices: `zarx.voices.voices()` lists them and `zarx.voices.voice_catalog()` adds the design attributes (beta and production since 2026-11-02; a deployment without the route raises `zarx.voices.VoicesNotAvailableError`).

`zarx.audio` (`transcribe`, `speak`, `translate`) and the `zarx speak`, `zarx transcribe` and `zarx translate` commands were run against stage on 2026-10-30, 2026-10-31 and 2026-11-01 with a zero-retention key: all three audio models are cleared for zero retention and answer 200. The audio model names may still change before general availability. Production lists the audio models since 2026-11-02; the measurements below are from stage (beta):

- `transcribe` (`z-transcribe`, `/v1/audio/transcriptions`): WAV (8, 16, 24 or 44.1 kHz, mono or stereo), MP3 or FLAC. 13 languages detected from the audio, plus Catalan, Galician and Basque on files when you name them: `transcribe(path, language="ca")` (`gl`, `eu`; the plain ISO 639-1 code) is served by a second engine of Zarx behind the same model, same name and price (beta since 2026-11-02, production since 2026-11-04). Without `language`, Galician and Basque come out unusable and Catalan comes out as Spanish with no warning, so name the language when you know it. For the other languages `language` is a hint that does not change the result; `prompt` never does. `response_format="text"` answers plain text (`text/plain`) on production and beta; the helper returns the text. `verbose_json`, `srt` and `vtt` get 400 (`verbose_json` is answered, with `segments`, when `language` is `ca`, `gl` or `eu`: beta and production, checked on production on 2026-11-05). `/v1/audio/translations` does not translate. On production `text` starts with a space from the engine; beta trims it since 2026-11-05.
- `transcribe_realtime` (`z-transcribe`, WebSocket at `/v1/audio/transcriptions/realtime`, Zarx's own events, not the OpenAI Realtime API): takes a WAV file or bytes in PCM16 mono 16 kHz (other WAV formats are refused before connecting), raw PCM bytes, or an iterable of PCM chunks (a microphone). `on_delta(text)` gets every piece of recognised text, `on_segment(text)` every finished sentence; the return value is the final transcript. Needs the `realtime` extra (`websockets`). A 3 s sample answered in 1.4 s on stage on 2026-11-01. Catalan, Galician and Basque are served on files only: the live route refuses `source_language` `ca`, `gl` or `eu` with `unsupported_language` (the helper never sends it), and Catalan audio sent live comes out as Spanish.
- `speak` (`z-speech`, `/v1/audio/speech`): `response_format` `wav` (default, 24 kHz mono 16-bit), `mp3`, `opus`, `flac`, `aac` or `pcm`; `ogg` gets 422. `voice` defaults to `"alloy"`, one of 13 presets (`alloy`, `ash`, `ballad`, `cedar`, `coral`, `echo`, `fable`, `marin`, `nova`, `onyx`, `sage`, `shimmer`, `verse`); `auto` also works, and `design:<attributes>` builds a voice from comma-separated attributes, at most one per group (gender, age, pitch, style, accent), for example `design:female,british accent` or `design:male,elderly,low pitch`; the lists are in `zarx.audio.VOICE_DESIGN_ATTRIBUTES`; free text or an unknown name gets 400 `invalid_voice` with a partner key and 422 with an account-wide key, and two attributes of one group (two accents) get 500. The same preset is not guaranteed to sound identical across calls. `speed` works; free-text `instructions` gets 422.
- `speak_stream` (`z-speech` with `stream_format="sse"`): yields the audio in pieces as it is generated (PCM by default, about ten pieces for a sentence) and sets `.usage` (`{"input_characters": N}`) at the end. Streams on beta and production (2026-11-02); a deployment that does not stream sends the whole audio as one piece.
- `translate` (`z-translate`, through chat completions): the target is a language name in English (`"Spanish"`) or a code (`"es"`, `"pt-BR"`); good for English, Spanish, French and German, and `GET /v1/models` lists the 36 languages it serves in `capabilities.languages.supported` (beta and production). Catalan, Galician and Basque are not served by `z-translate`: the request answers 400 `unsupported_language` (beta since 2026-11-02, production since 2026-11-04) (`openai.BadRequestError`; before that it answered Spanish in 8 of 10 sentences). The helper does not switch models by itself: for those three languages pass `model="z-ultra"`, `zarx.audio.translate(text, "ca", model="z-ultra")` (45 of 46 correct in our test).
- `speech_to_speech` (`z-voice`, `/v1/audio/speech-to-speech`): an audio file in, the same speech in `target_language` (an ISO 639-1 code, default `"en"`) out, as WAV bytes whatever format is asked. About 3 s for a short sentence on beta and production (2026-11-02). `target_language` `"ca"`, `"gl"` or `"eu"` works with nothing to do on your side: the translation step runs on `z-ultra` behind the same call (beta since 2026-11-02, a Spanish clip into Catalan in 4.2 s; production since 2026-11-04). A Catalan clip in works on production since 2026-11-05: `speech_to_speech(path, "es", source_language="ca")` (about 4 s for a sentence).
- Use the model name you sent, not the `model` field of audio responses, to identify the model.

See `docs/models.md` for the routes and events.

## Forecasting

```python
import zarx

result = zarx.forecast([3, 4, 6, 5, 7, 9], 3)                 # next 3 periods, hourly steps ending now
result = zarx.forecast(pairs, 12, include_band=True)            # [(timestamp, value), ...] with a low/high band
result = zarx.forecast_csv("visits.csv", 24, data_points=256)   # header row, timestamp first, value last

print(result.values)              # the point forecasts
for point in result:              # .timestamp, .value, .low, .high, .percentiles
    print(point.timestamp, point.value)
print(result.billable_points)     # what the call is billed for (usage.datapoints, or context plus forecast points)
```

`series` is a list of numbers (one timestamp every `step`, default `"1h"`, ending now or starting at `start`), of `(timestamp, value)` pairs, of dicts `{"timestamp", "value", ...covariates}`, or a mapping of timestamp to value; at least 3 points. Pass real timestamps when you have them, the model uses the calendar. `periods` must be at least 1. Anything else you pass as a keyword goes into the request body as is (for example `forecast_extra_params={"quantile_low": 0.2, "quantile_high": 0.8}`). `result.raw` is the response exactly as the API returned it.

Verified on beta on 2026-11-01 and 2026-11-02 with `z-forecast` (64 hourly points, a 512-row CSV, a 12-point daily series); production lists the model since 2026-11-02. The horizon is at most 1,000 periods per call. See `docs/models.md`.

## Embeddings

`z-embed` (Z Embed) turns text into vectors for search and similarity. It is on beta since 2026-11-05 and not on production yet, so use a beta key with `environment="beta"`. Search in a few lines: embed the documents with `input_type="document"`, the query with `input_type="query"`, and rank by the dot product (the vectors are L2-normalized, so the dot product is the cosine similarity):

```python
import zarx

client = zarx.Zarx(environment="beta")
documents = ["The invoice is due within 30 days of delivery.", "To reset your password, open Settings and choose Security."]
docs = zarx.embed(documents, input_type="document", client=client)
query = zarx.embed("How do I change my password?", input_type="query", client=client)[0]

scores = [sum(q * d for q, d in zip(query, vector)) for vector in docs]
print(documents[scores.index(max(scores))])   # the password document
print(docs.model, docs.dimensions, docs.prompt_tokens)  # z-embed-2026-11 4096 <input tokens billed>
```

`input` is a text or a list of texts (up to 2048, each up to 8192 tokens); leave `input_type` out for plain text such as clustering, and anything other than `"query"` or `"document"` raises `ValueError` before any request. With `"query"` the server puts its retrieval instruction in front of the text: those tokens are billed (about 19 per query) and the input must be text, not token ids; `"document"` embeds the text as it is. `dimensions=256` (32 to 4096) gives shorter vectors, re-normalized; keep the same value on both sides. The result has `vectors` (one per input, in order), `model` (the dated id that answered), `prompt_tokens`, `dimensions` and `raw`. Without the helper: `client.embeddings.create(model=zarx.Z_EMBED, input=texts, extra_body={"input_type": "query"})`. See `docs/models.md`.

## Development

```bash
uv venv .venv && uv pip install -e '.[dev]'
.venv/bin/pytest                    # offline tests
ZARX_API_KEY=... ZARX_ENVIRONMENT=beta .venv/bin/pytest -m live   # with a beta key
uv build                            # wheel and sdist in dist/
```

The `dev` extra includes `websockets`, which the `realtime` extra adds for users of `transcribe_realtime`.

## License

Apache License 2.0. See LICENSE and NOTICE.
