Metadata-Version: 2.5
Name: vidman-ai
Version: 0.1.2
Summary: The official Python library for the Vidman AI API
Project-URL: Homepage, https://vidman.ai
Project-URL: Documentation, https://docs.vidman.ai
Author: Vidman AI
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,llm,sdk,vidman,vidman-ai
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: anyio<5,>=3.5.0
Requires-Dist: httpx<1,>=0.25.0
Requires-Dist: pydantic<3,>=2.0.0
Requires-Dist: typing-extensions<5,>=4.7
Provides-Extra: dev
Requires-Dist: mypy>=1.5; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: respx>=0.20; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: websockets<18,>=13; extra == 'dev'
Provides-Extra: realtime
Requires-Dist: websockets<18,>=13; extra == 'realtime'
Description-Content-Type: text/markdown

# Vidman AI Python library

The official Python library for the Vidman AI API: chat and other model calls, agents, batches, Text to speech, Speech to text, Document Intelligence, knowledge bases and your wallet. It has typed responses, sync and async clients, streaming, automatic retries and idempotency keys.

## Installation

```sh
pip install vidman-ai
```

Requires Python 3.10 or later.

## Quick start

```python
from vidman_ai import VidmanAI

client = VidmanAI()  # reads VIDMAN_API_KEY from the environment

reply = client.inference.chat_completions(
    messages=[{"role": "user", "content": "Write a haiku about the monsoon."}],
)
print(reply.text)
print(reply.usage.credits)  # INR charged for this call
```

Create an API key in the Vidman AI console under **Settings → API keys**. We recommend keeping it in an environment variable rather than in code:

```sh
export VIDMAN_API_KEY="sk-..."
```

## Configuration

| Argument | Default | Notes |
|---|---|---|
| `api_key` | `VIDMAN_API_KEY` | Your API key. |
| `timeout` | 60 s, or 10 min for model calls, runs and streams | Seconds, or an `httpx.Timeout`. Applies to every request when set. |
| `max_retries` | `2` | See [Retries](#retries). |
| `default_headers` | none | Added to every request. |
| `default_query` | none | Added to every request. |
| `http_client` | a new `httpx.Client` | Bring your own client for proxies or custom TLS settings. |

The API address is built into the library. You don't need to configure it.

## Inference

```python
# Chat
reply = client.inference.chat_completions(
    model="vidman-adaptive",            # default; routes each request to a suitable model
    messages=[
        {"role": "system", "content": "You are concise."},
        {"role": "user", "content": "Capital of Kerala?"},
    ],
    temperature=0.2,
    max_tokens=100,
    reasoning_effort="low",             # none, low, medium, high, max
)
print(reply.text, reply.model)          # .model is the model that served the request

# Streaming
with client.inference.stream_chat_completions(messages=[{"role": "user", "content": "Tell me a story"}]) as stream:
    for chunk in stream:
        print(chunk.text, end="", flush=True)
# The last chunk carries .usage (tokens and credits). Leaving the block early stops generation.

# Embeddings and rerank
vectors = client.inference.embeddings(input=["first text", "second text"], dimensions=512)
ranked = client.inference.rerank(query="refund policy", documents=docs, top_n=3, return_documents=True)

# Model catalogue
for model in client.inference.list_models(type="chat"):
    print(model.id, model.pricing)
model = client.inference.retrieve_model("vidman-adaptive")
```

`client.chat.completions.create(...)` gives you the same chat endpoint in the widely used `chat.completions.create` shape. It returns a stream when you pass `stream=True`. `client.models`, `client.embeddings` and `client.rerank` also mirror `client.inference`.

### Tools and structured output

```python
reply = client.chat.completions.create(
    messages=[{"role": "user", "content": "Weather in Pune?"}],
    tools=[{
        "name": "get_weather",
        "description": "Current weather for a city",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
    }],
    tool_choice="auto",
)
call = reply.choices[0].message.tool_calls[0]
print(call.function.name, call.function.arguments)

reply = client.chat.completions.create(
    messages=[{"role": "user", "content": "List three Indian rivers as JSON"}],
    response_format={"type": "json_object"},
)
```

Tools can be written flat, as above, or in the `{"type": "function", "function": {...}}` form.

## Prompt caching

Every chat method takes `cache_control`, `prompt_cache`, `prompt_cache_options`, `prompt_cache_key` and `session_id`. A top-level `cache_control` caches the repeating part of the prompt: the first call writes it (billed at the write price), a repeat reads it back (billed at the cache-read price).

```python
reply = client.inference.chat_completions(
    model="anthropic/claude-sonnet-5",
    cache_control={"type": "ephemeral"},          # or {"type": "ephemeral", "ttl": "1h"}
    messages=[{"role": "system", "content": LONG_INSTRUCTIONS}, {"role": "user", "content": question}],
)
print(reply.usage.cached_tokens, reply.usage.cache_write_tokens)
```

Put `"cache_control"` on a content part or a tool to place a breakpoint yourself.

## Messages API (Anthropic dialect)

```python
msg = client.messages.create(
    model="anthropic/claude-sonnet-5",
    max_tokens=1024,
    system=[{"type": "text", "text": LONG_INSTRUCTIONS, "cache_control": {"type": "ephemeral"}}],
    messages=[{"role": "user", "content": "Hello"}],
)
print(msg.text, msg.usage.cache_read_input_tokens, msg.usage.credits)

for event in client.messages.stream(model="anthropic/claude-sonnet-5", max_tokens=1024,
                                    messages=[{"role": "user", "content": "Hi"}]):
    print(event.text, end="")
```

The Anthropic SDKs and Claude Code work too: point them at `https://api.vidman.ai` with your Vidman AI key.

## Wallet

```python
balance = client.wallet.get_balance()
print(balance.credits, balance.currency, balance.monthly_budget)

for txn in client.wallet.get_transactions():       # top-ups, refunds, promotional credits
    print(txn.created_at, txn.type, txn.amount, txn.credits)

for use in client.wallet.get_usage(model="vidman-adaptive"):   # every billed call
    print(use.created_at, use.endpoint, use.total_tokens, use.credits)

pricing = client.wallet.get_pricing()
for price in pricing.data:
    print(price.model, price.pricing)
print(pricing.exchange_rate.usd_inr)               # USD prices are charged in INR at this rate
```

Any key can read the balance and pricing. Transactions and usage need a key with **All** or **Read only** access. `client.account.retrieve()` describes the key you are using and the scopes it has.

## Agents

```python
agent = client.agents.create(
    name="Support assistant",
    system_prompt="Answer from the knowledge base. Be brief.",
    knowledge_base_ids=[store.id],
)

# Stream a run
for event in client.runs.stream(agent_id=agent.agent_id, message="How do I reset my password?"):
    if event.type == "token":
        print(event.content, end="")
    elif event.type == "tool_call":
        print(f"\n[{event.tool}]")
    elif event.type == "cost":
        print(f"\n{event.credits} credits")

# Or run in the background and wait
run = client.runs.create_and_poll(agent_id=agent.agent_id, message="Summarise today's tickets")
print(run.output)

# Change the draft, then publish it as a new version
client.agents.update(agent.agent_id, system_prompt="...")
client.agents.versions.publish(agent.agent_id, label="Shorter answers")
```

Agents also have `agents.test_sets` (saved questions you can rerun against each version), `agent_tools` (your own HTTP APIs as tools), `agent_credentials` (connector credentials) and `agents.list_connectors()`. To continue a conversation, pass the same `session_id` to `runs.stream` or `runs.create`.

## Batches

```python
batch = client.batches.create(file="requests.jsonl")   # each line: {"custom_id", "method", "url", "body"}
batch = client.batches.poll(batch.id)
for line in client.batches.download_output(batch.id).iter_lines():
    print(line)
```

## Text to speech and Speech to text

```python
audio = client.speech.create(input="Namaste! Your order has shipped.", voice="vidman-meera")
audio.write_to_file("order.mp3")

with client.speech.stream(input="Lower latency, as it is synthesised.") as stream:
    stream.stream_to_file("live.mp3")

transcript = client.transcriptions.create(file="call.mp3", diarize=True, response_format="verbose_json")
for segment in transcript.segments:
    print(segment.speaker, segment.text)

subtitles = client.transcriptions.create(file="talk.mp3", response_format="srt")   # str
```

### Live transcription

Stream microphone (or any) audio in and receive text as it is spoken. Install the extra first: `pip install "vidman-ai[realtime]"`.

```python
with client.transcriptions.realtime.connect(language="en-IN") as session:
    # send 16-bit mono PCM at session.sample_rate, in small chunks as you capture it
    for chunk in microphone_chunks(session.sample_rate):
        session.send_audio(chunk)
    session.commit()                         # finish the current segment now
    for event in session:
        if event.type == "transcript.delta":
            print(event.text, end="\r")
        elif event.type == "transcript.completed":
            print(event.text)
            break
# leaving the block ends the session and collects the last segment
print(session.text, session.usage.audio_seconds, session.usage.credits)
```

Sending and receiving can happen at the same time: send from one thread and iterate in another, or use `AsyncVidmanAI` (`async with ... as session`, `await session.send_audio(...)`, `async for event in session`).

A session lasts up to 15 minutes and ends after 60 seconds without audio. If it ends abnormally, you get `vidman_ai.RealtimeError`, whose `.code` is the close code (for example 4402 for no credits).

## Document Intelligence

```python
result = client.documents.analyze(file="invoice.pdf", task="digitise")    # markdown with tables
print(result.content)

result = client.documents.analyze(
    file="invoice.pdf",
    task="extract",
    field_schema={"fields": {"invoice_number": {"type": "string"}, "total": {"type": "number"}}},
)
print(result.fields)

draft = client.documents.generate_schema(description="An invoice: number, date, line items and total")
saved = client.documents.schemas.create(name="Invoice", field_schema=draft.field_schema)
client.documents.analyze(file="next.pdf", task="extract", schema_id=saved.id)

parts = client.documents.split(file="bundle.pdf")              # e.g. invoice + packing list
```

## Knowledge bases

```python
store = client.vector_stores.create(name="Help centre")
client.vector_stores.files.upload(store.id, file="faq.pdf")
hits = client.vector_stores.search(store.id, query="refund window", max_results=5)
for hit in hits.data:
    print(hit.score, hit.filename, hit.text[:80])
```

## Datasets

Training data for custom models. Upload your own (.jsonl, up to 200 MB: one JSON object per line, `{"messages": [...]}` for fine-tuning or `{"text": "..."}` for pre-training), import from Hugging Face, or start from a ready-made sample.

```python
check = client.datasets.validate(file="chats.jsonl")        # free; nothing is stored
print(check.valid, check.rows, check.errors)

dataset = client.datasets.create(file="chats.jsonl", name="Support chats")
print(client.datasets.status(dataset.id).progress_percent)  # prepared in the background

for d in client.datasets.list(status="ready"):
    print(d.id, d.name, d.rows, d.training_methods)
found = client.datasets.list(type="sft", q="support", sort="name")  # also oldest, largest; newest by default

rows = client.datasets.preview(dataset.id, page=1, page_size=10)
client.datasets.update_column_mapping(dataset.id, column_mapping={"question": "instruction", "answer": "output"})
client.datasets.delete(dataset.id)

samples = client.datasets.samples()                         # read-only, free to keep
formats = client.datasets.formats()                         # what each kind of training expects
storage = client.datasets.storage()                         # size and monthly cost
limits = client.datasets.settings()                         # largest upload, imports on/off, your limits

found = client.datasets.hugging_face.search(q="alpaca", sort="downloads")
peek = client.datasets.hugging_face.preview(dataset_id="tatsu-lab/alpaca", limit=3)
imported = client.datasets.hugging_face.import_(dataset_id="tatsu-lab/alpaca")   # streamed: nothing copied, free
sampled = client.datasets.hugging_face.import_(
    dataset_id="tatsu-lab/alpaca", max_samples=5000, sample="random",          # a row limit keeps a copy
)
private = client.datasets.hugging_face.import_(
    dataset_id="acme-research/tickets", hf_token="hf_xxxxxxxxxxxxxxxx",         # used once, never stored
)
print(imported.hugging_face.storage)                        # "stream"
print(imported.hugging_face.revision)                       # the Hugging Face commit read
```

`storage="stream"` (the default) copies nothing and costs nothing: rows are read from Hugging Face when a job trains. `storage="copy"` keeps a copy, charged per GB like an upload; it is the default when `max_samples` is set, and `max_samples` with `storage="stream"` raises `UnprocessableEntityError`. `sample` is `first` (the default) or `random`. `hf_token` reads a private dataset for this import only; it is never stored or returned. A preview also gives `confidence` (0 to 1, how sure the shape detection is), `usable_rows` per kind of training and `length` in tokens. When a dataset has several subsets and you pass none, the preview may list only `subsets` (with `type` None and no examples): pass one of them as `subset`.

A Hugging Face dataset can be imported only once at a time on the platform. Importing one that is already imported or being imported raises `ConflictError` with `code == "already_importing"`. When it is your own import, `err.dataset_id` is that dataset: open it, or delete it to import it again with other settings. When it is held by an import no organisation owns, `err.dataset_id` is `None`: try again later or contact support.

```python
try:
    client.datasets.hugging_face.import_(dataset_id="tatsu-lab/alpaca")
except vidman_ai.ConflictError as err:
    if err.code == "already_importing" and err.dataset_id:
        existing = client.datasets.retrieve(err.dataset_id)
```

### Keep a streamed dataset current

A streamed import reads Hugging Face when a job trains, so its source can change. Both calls are free and work only on your own streamed imports (`hugging_face.storage == "stream"`):

```python
check = client.datasets.revalidate(imported.id)     # is it still trainable, and did it change?
print(check.valid, check.changed, check.message)    # e.g. True False "Nothing has changed. It can still be trained on."

moved = client.datasets.update_source(imported.id)  # move to Hugging Face's latest revision
print(moved.updated, moved.dataset.hugging_face.revision)
```

A copied or uploaded dataset does not change: both raise `ConflictError` with `code == "not_streamed"`. A sample raises `PermissionDeniedError` (`dataset_read_only`), and someone else's dataset raises `NotFoundError`.

Your workspace's own limits can refuse an upload or import: `ConflictError` with `code == "dataset_limit_reached"` or `"storage_limit_reached"` (the message says what is used and allowed), and for imports `PermissionDeniedError` with `code == "hugging_face_import_disabled"` or `"copy_storage_disabled"` (import streamed instead). `client.datasets.storage()` shows the space used, the price per GB and an estimate for the month; streamed imports are never charged.

### Private datasets from your Hugging Face account

Connect your own Hugging Face account with a read token (create one at huggingface.co/settings/tokens) to browse, preview and import its private datasets. The token is stored encrypted and never returned; only its last 4 characters are shown. Connecting is free.

```python
account = client.datasets.integrations.create(label="Research team", token="hf_xxxxxxxxxxxxxxxx")
print(account.id, account.account, account.token_last4)

found = client.datasets.integrations.browse(account.id, q="tickets", sort="updated")
peek = client.datasets.integrations.preview(account.id, dataset_id="acme-research/tickets", limit=3)
imported = client.datasets.integrations.import_(account.id, dataset_id="acme-research/tickets")
print(imported.hugging_face.via_account)                    # True

for a in client.datasets.integrations.list():
    print(a.id, a.label, a.status)
client.datasets.integrations.delete(account.id)             # imported datasets are kept
```

A token Hugging Face refuses raises `BadRequestError` with `code == "token_rejected"`. An account that is no longer connected raises `NotFoundError` with `code == "integration_not_found"`; connect it again. `import_` takes the same `storage`, `max_samples` and `sample` and follows the same rules as other imports (workspace limits, `already_importing`, the monthly budget). Its `hugging_face.private` is always `True`.

## Training

Train your own model on your datasets. Pick a base model, see the setups it can be trained with, run a free price check (a quote, valid for 30 minutes), then start the job. The machines are chosen for you. Training is priced per trained token: the quote estimates the tokens from your data's measured lengths, and the job is charged for the exact tokens it trains, never for machine time or time spent waiting. Starting needs the quote's estimated cost in the wallet. Money is in rupees.

```python
import vidman_ai

# 1. Pick a base model and see what can be changed
for m in client.training.models.list(q="llama"):
    print(m.id, m.parameters_b, m.recommended)
options = client.training.options()                          # methods, adapters, settings with defaults

# 2. See what it can be trained with: precisions, optimizers and context lengths (free)
setups = client.training.setups("meta-llama/Llama-3.1-8B-Instruct", method="sft", adapter="lora")
if not setups.available:
    raise SystemExit(setups.reason)
for s in setups.setups:
    print(s.precision, s.optimizer, s.context_lengths)

# 3. Check the price (free)
quote = client.training.quotes.create(
    name="support-bot-v1",
    model="meta-llama/Llama-3.1-8B-Instruct",
    dataset_ids=[dataset.id],
    settings={"epochs": 3, "precision": "bf16", "max_length": setups.default_context_length},
)
if not quote.valid:
    raise SystemExit(quote.errors)
est = quote.estimate
print(est.tokens, est.price_per_million_tokens, est.cost)     # tokens, rupees per million, rupees

# 4. Start it, then wait for it to finish
try:
    job = client.training.jobs.create(quote.id)
except vidman_ai.InsufficientCreditsError:
    raise SystemExit("Top up the wallet: starting needs the quote's estimated cost")
except vidman_ai.ConflictError as err:                       # quote_expired: check the price again
    raise SystemExit(err.code)

job = client.training.jobs.poll(job.id, poll_interval=30, max_wait=6 * 3600)
print(job.status, job.cost_so_far, job.error)
```

`precision`, `optimizer` and `max_length` must match one of `setups` (else `BadRequestError` with `code` `setup_not_offered`, `context_length_not_offered` or `no_tested_setup`). `validation_rows` holds rows out to check the model as it trains (0 turns it off; unset, 5% of the rows, at most 1,000); it cannot be combined with `validation_dataset_ids` (`validation_conflict`). `quote.validation` gives the range accepted for your data.

When no machine is free the job waits with status `queued` (free, first come first served, up to 7 days; `job.queue.position`), and you are emailed when it starts. `job.pricing` keeps the price per million tokens and the estimate it started with; `job.cost_so_far` and `job.charged` follow the tokens trained. A stopped job pays for the tokens trained so far, and a job that fails before saving any version pays nothing. Every job keeps two saved versions: the best (lowest validation loss) and the last.

`jobs.create` sends an idempotency key, reused across retries, so a retry never starts a second paid job. Pass `idempotency_key="..."` (8 to 128 characters) to choose your own.

```python
client.training.jobs.list(status="running", q="qwen")       # q: job name or base model; iterate for every page
client.training.jobs.metrics(job.id).data                    # loss and learning rate by step
client.training.jobs.logs(job.id, tail=100).data
client.training.jobs.stop(job.id)                            # keep_data=True keeps the saved versions; a stopped job does not resume

for cp in client.training.jobs.checkpoints(job.id):          # saved versions
    print(cp.id, cp.label, cp.validation_loss, cp.best)
client.training.jobs.delete_checkpoint(job.id, cp.id)        # ConflictError "checkpoint_in_use" if deployed

files = client.training.jobs.checkpoint_files(job.id, cp.id)  # names, sizes and the base model
client.training.jobs.download_checkpoint(job.id, cp.id, "version.zip")           # the whole version
client.training.jobs.download_checkpoint_file(job.id, cp.id, files.files[0].name, "adapter.bin")
client.training.jobs.download_checkpoint_script(job.id, cp.id, "get-version.sh")  # then: VIDMAN_API_KEY=... sh get-version.sh
```

## Deployments

Serve your model on dedicated hardware: a version saved by one of your training jobs, or a public Hugging Face model. A deployment is called like every other Vidman AI model, with your usual key and its `model_id` as `model` (e.g. `model="support-bot"`; the `dep_...` id works too). It is billed by the second while its hardware works (downloading, loading, serving); waiting for hardware and being stopped cost nothing. Creating or resuming needs two hours of the hardware in the wallet, and when the wallet runs out the deployment is paused until you resume it. Money is in rupees.

```python
import vidman_ai

# 1. Pick a saved version of a training job, and hardware for it
saved = next(cp for cp in client.training.jobs.checkpoints(job.id) if cp.deployable)
gpus = client.deployments.hardware(job_id=job.id, checkpoint_id=saved.id)
choice = next(c for c in gpus.choices if c.can_choose)
print(choice.label, choice.price_per_gpu_hour)               # rupees per GPU per hour

# 2. Deploy it, then wait until it is running
try:
    deployment = client.deployments.create(
        name="Support bot",
        model_id="support-bot",                              # optional: made from name when left out
        source={"type": "checkpoint", "job_id": job.id, "checkpoint_id": saved.id},
        gpu_type=choice.gpu_type,
        gpu_count=choice.recommended_count,
    )
except vidman_ai.InsufficientCreditsError:
    raise SystemExit("Top up the wallet: deploying needs two hours of the chosen hardware")

deployment = client.deployments.poll(deployment.id, poll_interval=15, max_wait=3600)
if deployment.status != "running":                           # failed, crashed, stopped or paused
    raise SystemExit(f"{deployment.status}: {deployment.status_detail}")

# 3. Chat with it like any other model
reply = client.chat.completions.create(
    model=deployment.model_id,                               # "support-bot"
    messages=[{"role": "user", "content": "Where is my order?"}],
)
print(reply.choices[0].message.content)
```

A Hugging Face model works the same way: pick one from `client.deployments.catalog(q="llama")` (every model that can be deployed as it is, most downloaded first, not only the ones offered for training; each item is a `TrainingModel`), check it with `client.deployments.check_model("meta-llama/Llama-3.1-8B-Instruct")`, price it with `client.deployments.hardware(model=...)`, and deploy it with `source={"type": "hugging_face", "model": ...}`. A `model_id` is lowercase letters, digits, `.`, `_` and `-` (up to 64, starting with a letter or digit), unique among your deployments and matched exactly; it never changes, even if you rename the deployment. One that is malformed or starts with `dep_` or `vidman-` raises `BadRequestError` with code `invalid_model_id`; one already in use raises `ConflictError` with code `model_id_taken`. Calling a deployment that is not running raises `ConflictError` with code `deployment_not_running`. `create` and `resume` send an idempotency key, reused across retries, so a retry never starts a second paid deployment; pass `idempotency_key="..."` to choose your own.

```python
client.deployments.list()                                    # iterate to walk every page
client.deployments.reserving()                               # still waiting for a GPU (after booking_pending)
client.deployments.failed_reservations()                     # the platform could not reserve the GPU: reason; nothing charged
client.deployments.dismiss_failed_reservation("res_...")       # clear one once seen
client.deployments.preview_model_id(name="vidman-ai-model")   # → my-vidman-ai-model, with why; or model_id="help-desk" to check one
client.deployments.update(deployment.id, context_length=65536, quantization="fp8")  # applies on restart/resume
client.deployments.restart(deployment.id)
client.deployments.stop(deployment.id)                       # billing stops
client.deployments.resume(deployment.id)                     # loads afresh; same wallet rule as create
metrics = client.deployments.metrics(deployment.id, window="24h")  # bucket says how long each point covers
print(metrics.totals.average_tokens_per_second, metrics.totals.cached_tokens, metrics.data[-1].running_requests)
client.deployments.events(deployment.id).data                # created, downloading, ready, restarts, stops
client.deployments.delete(deployment.id)
```

### Serving settings

`update` changes the context length, the precision (`bf16`, `fp8` or `int4`), whether the capacity queue is on, and the serving settings. Every change is saved at once and applies on the next `restart` (now) or `resume`; `restart_required` is true until then. Check first, for free, whether the current hardware can take a new context length or precision with `serving_preview`.

```python
preview = client.deployments.serving_preview(deployment.id, context_length=131072, quantization="int4")
if not preview.fits:
    print(f"Needs {preview.minimum_count} x {preview.gpu_label}; {preview.available_count} free now")

options = client.deployments.serving_options()                  # parser ids and labels
print([p.id for p in options.tool_call_parsers], [p.id for p in options.reasoning_parsers])

client.deployments.update(
    deployment.id,
    serving={"tool_call_parser": "hermes", "reasoning_parser": None},  # None = let Vidman AI pick
)
client.deployments.update(deployment.id, serving={"chat_template": None})  # clear a custom template
client.deployments.update(deployment.id, capacity_queue=False)
client.deployments.restart(deployment.id)                       # apply now

serving = client.deployments.retrieve(deployment.id).serving
print(serving.detected_tool_call_parser, serving.tool_call_mode)  # native, best_effort or unavailable
```

#### Advanced settings

`serving` also takes `kv_cache` (`auto`, the default, or `fp8_e5m2`, `turboquant_4b`, `turboquant_3b`, `turboquant_2b`, which store the KV cache in fewer bits so more context fits; fewer bits save more memory at more cost in quality), `max_running_requests` (how many requests are served at once, 1 to 4096, 256 by default; the platform caps it to what the GPU can serve safely) and `torch_compile` (off by default; it can lower latency at low concurrency but adds minutes of compilation to every start). They can be set when you create the deployment, too; leave `serving` out and Vidman AI picks automatically.

```python
deployment = client.deployments.create(
    name="Support bot", source={"type": "hugging_face", "model": "meta-llama/Llama-3.1-8B-Instruct"},
    gpu_type="H100_80GB",
    serving={"kv_cache": "fp8_e5m2", "max_running_requests": 64, "tool_call_parser": "llama3"},
)
client.deployments.update(deployment.id, serving={"kv_cache": "auto", "max_running_requests": None})  # back to defaults
print(options.kv_cache, options.max_running_requests)              # choices and the allowed range
```

Parsers take `auto` (the same as None: Vidman AI picks) or `off` (tool calls are rejected; thinking text stays in the reply). Each option from `serving_options()` has an `id`, a `label` and, for some, a `description`; `auto` is not listed.

Only the `serving` keys you give are changed; the rest keep their saved values. An unknown parser raises `BadRequestError` with code `invalid_parser`, a template over 65536 bytes `chat_template_too_large`, and turning the queue on for a deployment created without 3 to 5 GPU choices `capacity_queue_unavailable`. Changing the precision of a model whose precision is fixed (`quantization_locked`) raises `ConflictError` with code `quantization_locked`.

While it starts up, `setup` says how far it has got (`phase`, `attempt`, `elapsed_seconds`), and after a crash or failure `setup.reason` and `last_error` say what went wrong. A queued deployment has `queue` (`position`, `since`, `expires_at`), and `selected_gpu_rank` says which choice of `gpu_priorities` it started on. Each of `events(...).data` has a `level` (`info`, `warning` or `error`) and `data` with extra facts.

### Status, progress and changing the GPU

`actions` says what a deployment accepts right now (`stop`, `resume`, `restart`, `delete`, `change_gpu`). `activity` says what it is in the middle of: `kind` is `restart` or `resume` (one you asked for), `recovery` (set up again on a new machine after a fault) or `retry` (setting up tried again on a different machine), with an `attempt` count. While it downloads or loads, `progress` has the `percent`, bytes or weight files done and an `eta_seconds`. `link` describes the control link to its machine (`overall` is `two_way` when healthy), and `status_email_failed` is true when a status email could not be delivered.

A deployment that is not running (`actions.change_gpu`) can be moved to other hardware. It keeps its id and `model_id`, starts on the new GPU (billed time, like `resume`) and needs two hours of it in the wallet.

```python
deployment = client.deployments.retrieve(deployment.id)
if deployment.progress:
    print(deployment.progress.phase, deployment.progress.percent, deployment.progress.eta_seconds)

if deployment.actions.change_gpu:
    options = client.deployments.hardware_for(deployment.id)          # priced for this deployment
    choice = next(c for c in options.choices if c.can_choose and c.available_now)
    client.deployments.change_gpu(deployment.id, gpu_type=choice.gpu_type, gpu_count=choice.recommended_count)
    client.deployments.poll(deployment.id)
```

A running deployment raises `ConflictError` with code `cannot_change_gpu` (stop it first); a GPU that cannot serve the model or a count it cannot be booked in raises `BadRequestError` with `hardware_not_available` (`too_few_gpus` below its `minimum_count`); a GPU not offered to your workspace `PermissionDeniedError` with `gpu_not_allowed`; a short wallet `InsufficientCreditsError`. `change_gpu` sends an idempotency key like `resume`.

### Capacity queue

When every GPU you would take is busy, a deployment can wait for one instead of failing. Turn on `capacity_queue` and list 3 to 5 different GPUs in `gpu_priorities`, in the order you prefer them; the first must be the `gpu_type` and `gpu_count` you deploy with. The deployment shows `status == "queued"` (`is_queued`) while it waits, for up to seven days, and nothing is charged until it starts. It starts on the first GPU in your order that frees up, and `hardware` then shows that GPU. Requests are served in fair rounds, so one account cannot hold up everyone else. The hourly price never goes above the highest total in your order, and the wallet must hold two hours of that. Each hardware choice's `available_count` says how many of those GPUs are free right now.

```python
deployment = client.deployments.create(
    name="Support bot",
    source={"type": "hugging_face", "model": "meta-llama/Llama-3.1-8B-Instruct"},
    gpu_type="RTX_PRO_6000_96GB",
    gpu_count=1,
    capacity_queue=True,
    gpu_priorities=[
        {"gpu_type": "RTX_PRO_6000_96GB", "gpu_count": 1},       # same as gpu_type / gpu_count
        {"gpu_type": "H100_80GB", "gpu_count": 1},
        {"gpu_type": "A100_80GB", "gpu_count": 1},
    ],
)
print(deployment.status, deployment.hardware.label)             # "queued" until a GPU frees up
```

A malformed order raises `BadRequestError` with code `invalid_gpu_priorities`. When fewer than three of your choices can serve the model within your price, `create` raises `BadRequestError` with code `capacity_queue_unavailable`, and nothing is charged. `poll` keeps waiting through `queued`, so pass `max_wait` if you do not want to wait days; `stop` or `delete` takes a queued deployment out of the queue.

## Files

Anywhere the library takes a `file`, you can pass any of these:
- a path (`"a.pdf"` or `Path("a.pdf")`)
- bytes
- a file opened in binary mode
- a `(filename, content[, content_type])` tuple

## Async

Every method has an async version on `AsyncVidmanAI`:

```python
import asyncio
from vidman_ai import AsyncVidmanAI

async def main():
    async with AsyncVidmanAI() as client:
        reply = await client.inference.chat_completions(messages=[{"role": "user", "content": "Hi"}])

        stream = await client.inference.stream_chat_completions(messages=[{"role": "user", "content": "Hi"}])
        async for chunk in stream:
            print(chunk.text, end="")

        async for txn in client.wallet.get_transactions():   # pages are fetched as you go
            print(txn.id)

asyncio.run(main())
```

## Errors

Every error the library raises is a `vidman_ai.VidmanAIError`. If the API answers with an error status, you get an `APIStatusError` subclass that carries:
- `status_code`
- `code` (e.g. `insufficient_credits`)
- `message`
- `request_id` (quote this to support)

`ConflictError` also has `dataset_id`: for `already_importing`, your dataset that already holds that Hugging Face import (None otherwise).

| Status | Error |
|---|---|
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 402 | `InsufficientCreditsError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 409 | `ConflictError` |
| 413 | `RequestTooLargeError` |
| 422 | `UnprocessableEntityError` |
| 429 | `RateLimitError` |
| ≥500 | `InternalServerError` |
| no response | `APIConnectionError`, `APITimeoutError` |
| error inside a stream | `StreamError` |

A feature that is still coming soon on your account (Datasets, Training, Deployments, Text to Speech, Speech to Text, Knowledge Base, Knowledge Search, Agents, Document Intelligence) raises `PermissionDeniedError` with `code == "feature_not_available"` and a message such as "Agents is coming soon on your account." It is not retried; other features keep working.

```python
import vidman_ai

try:
    client.inference.chat_completions(messages=[{"role": "user", "content": "Hi"}])
except vidman_ai.InsufficientCreditsError:
    print("Top up in the Vidman AI console")
except vidman_ai.RateLimitError as err:
    print("Slow down", err.request_id)
except vidman_ai.APIStatusError as err:
    print(err.status_code, err.code, err.message)
```

## Retries

These failures are retried automatically, up to 2 times by default:
- connection errors and timeouts
- 408, 429 and 5xx responses

The library waits with exponential backoff and jitter (0.5 s, 1 s, … capped at 8 s). It honours the server's `Retry-After`. A monthly budget limit is not retried, because waiting won't help. Nor is a 409 conflict (`ConflictError`): retrying will not resolve it. The one exception is a 409 that says when to try again (`Retry-After`), which the gateway sends while an earlier request with the same idempotency key is still running.

```python
client = VidmanAI(max_retries=5)                                    # every request
client.with_options(max_retries=0).wallet.get_balance()            # one client copy
client.inference.chat_completions(messages=msgs, max_retries=0)    # one call
```

## Idempotency

Each POST gets a unique `Idempotency-Key`, and retries of that call reuse it. Within 24 hours, the API replays a request it has already completed instead of running it again. A retry after a dropped connection is therefore never billed twice.

To make your own repeats safe too, such as a job queue redelivering a message, pass your own key:

```python
client.inference.chat_completions(messages=msgs, idempotency_key=f"ticket-{ticket.id}")
```

If you reuse a key with a different request, you get `UnprocessableEntityError` (`code="idempotency_key_reused"`).

## Timeouts

```python
client = VidmanAI(timeout=20.0)
client.inference.chat_completions(messages=msgs, timeout=120)
```

A request that runs out of time raises `APITimeoutError`, after any retries.

## Pagination

List methods return a page. Iterating it walks every item, fetching later pages only when they're needed. Use `page.data` for the current page only, and `page.has_next_page()` and `page.get_next_page()` to page manually.

## Raw responses and request options

```python
raw = client.with_raw_response.wallet.get_balance()
print(raw.status_code, raw.headers, raw.request_id)
balance = raw.parse()
```

Every method also accepts:
- `extra_headers`
- `extra_query`
- `extra_body`
- `timeout`
- `max_retries`
- `idempotency_key`

## Logging

Set `VIDMAN_LOG=info` or `VIDMAN_LOG=debug` to log requests and retries.

## License

Apache-2.0
