> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sdvm.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI-compatible endpoint

> Call audit, fix and refine through any OpenAI chat-completions client, with your sdvm_ key.

`https://api.sdvm.ai/v1` speaks the OpenAI chat-completions protocol. Any client that can talk to OpenAI — the official SDKs, LangChain, LiteLLM, a `curl` — can talk to SDVM by changing the base URL and the key. Nothing to install.

The model is picked by the **model id**; the samples are the **user message**; the result is the **assistant message**. Your `sdvm_` key authenticates and is billed exactly as on the native routes.

```python wrap theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
import json
import os

from openai import OpenAI

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

sample = {
    "task_type": "multiple_choice",
    "style": "qa",
    "context": "What is the capital of France?",
    "choices": ["Paris", "Berlin", "Madrid", "Rome"],
    "answer_index": 0,
}

response = client.chat.completions.create(
    model="sdvm/audit-1",
    messages=[{"role": "user", "content": json.dumps({"data": [sample]})}],
)

audited = json.loads(response.choices[0].message.content)
print(audited[0]["audit"]["label_correct"])   # True
print(response.usage.total_tokens)             # what the request billed on
```

## Models

| Model id        | What it does                              | Same as                          |
| --------------- | ----------------------------------------- | -------------------------------- |
| `sdvm/audit-1`  | One verdict per check, nothing changed    | [`POST /audit`](/models/audit)   |
| `sdvm/fix-1`    | The fix, behind the never-worse guarantee | [`POST /fix`](/models/fix)       |
| `sdvm/refine-1` | Audit, fix, then reaudit                  | [`POST /refine`](/models/refine) |

The unversioned ids `sdvm/audit`, `sdvm/fix` and `sdvm/refine` still work and are not listed.

`GET /v1/models` lists them with pricing and context limits, in the OpenAI shape, and `GET /v1/models/{id}` (`client.models.retrieve("sdvm/audit-1")`) returns the card for one. An unknown id is a 404 with `code: model_not_found`.

## The message

The **last user message** carries the samples. Three forms are accepted:

<Tabs>
  <Tab title="Envelope (all options)">
    A JSON object with `data` plus any of the options below.

    ```json theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
    {
      "data": [
        {"task_type": "text", "text": "i has went to teh store"}
      ],
      "conventions": "lowercase throughout is intentional",
      "config": {"votes": 3}
    }
    ```
  </Tab>

  <Tab title="Bare array">
    A JSON array of samples, default options.

    ```json theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
    [
      {"task_type": "text", "text": "i has went to teh store"}
    ]
    ```
  </Tab>

  <Tab title="Plain text">
    Anything that is not JSON is one text sample. `sdvm/audit-1` rates it on the four text dimensions, `sdvm/fix-1` rewrites it, `sdvm/refine-1` does both.

    ```text theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
    i has went to teh store
    ```
  </Tab>
</Tabs>

Samples are the same objects the native routes take — see [Sample types](/core-concepts). System messages and earlier turns are ignored.

| Option        | Applies to | Range     | Meaning                                                                                                                                                                                                        |
| ------------- | ---------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conventions` | all        | free text | Dataset conventions the audit must not mistake for defects ([guide](/guides/conventions))                                                                                                                      |
| `config`      | all        | per verb  | The verb's stage config, exactly as on the native route: `{"votes"}` for audit, `{"max_attempts"}` for fix, `{"audit", "fix", "reaudit"}` for refine ([votes](/guides/votes), [attempts](/models/fix#options)) |

Out-of-range or wrongly-typed options are a `400` before any work is done.

## The reply

`choices[0].message.content` is a JSON string: the input array, each sample carrying its `audit` block (audit, refine), its `fix` block where the fix changed it — `{"changes": [...], "flagged": bool, "attempts": int}`; a text sample left untouched carries none — and, on refine, its `reaudit` block (`null` for a sample the fix left alone). Parse it with `json.loads`. `finish_reason` is always `"stop"`.

`usage` reports the tokens the **whole pipeline** consumed upstream — every audit pass, every fix pass — which is what the request billed on, at the rates in `GET /v1/models`. It is deliberately not the size of the strings on the wire: one visible request fans out into `votes` audits and up to `max_attempts` fixes per sample, and that fan-out is the work being paid for.

Sampling parameters (`temperature`, `top_p`, `max_tokens`, `n`, `tools`, `response_format`) are accepted and ignored. Audit and fix are constrained transformations and always run at the lowest temperature the underlying model allows.

## Streaming

`stream: true` returns server-sent events. The pipeline is not incremental, so the content arrives at the end — but the connection is held open with comment keepalives while it runs, which is what stops a proxy or client timeout from killing a multi-minute refine. The final chunk always carries `usage`, whether or not you asked for it via `stream_options`.

```python wrap theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
text = "i has went to teh store yesterday and buyed three apple"

chunks = []
for chunk in client.chat.completions.create(
    model="sdvm/fix-1",
    messages=[{"role": "user", "content": text}],
    stream=True,
):
    if chunk.choices and chunk.choices[0].delta.content:
        chunks.append(chunk.choices[0].delta.content)
    if chunk.usage:
        print("billed on", chunk.usage.total_tokens, "tokens")

fixed = json.loads("".join(chunks))
print(fixed[0]["text"])
# I went to the store yesterday and bought three apples.
```

A failure after the stream has opened arrives as an `{"error": ...}` event in the body (the HTTP status is already 200), followed by `data: [DONE]` — the shape OpenAI-compatible clients already parse.

## curl

```bash theme={"theme":{"light":"material-theme-lighter","dark":"material-theme-darker"}}
curl https://api.sdvm.ai/v1/chat/completions \
  -H "Authorization: Bearer $SDVM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "sdvm/fix-1",
    "messages": [
      {"role": "user", "content": "i has went to teh store"}
    ]
  }'
```

## Errors

Errors use OpenAI's envelope, `{"error": {"message", "type", "code"}}`, so client libraries raise their usual exceptions.

| Status | `code`                    | When                                                                                             |
| ------ | ------------------------- | ------------------------------------------------------------------------------------------------ |
| 400    | `invalid_request`         | Empty message, malformed sample, option out of range, unsupported task type                      |
| 401    | `invalid_api_key`         | Missing, unknown or revoked key                                                                  |
| 402    | `insufficient_quota`      | Balance cannot cover the request — before any work when empty, after it if the real cost overran |
| 404    | `model_not_found`         | Unknown model id                                                                                 |
| 413    | `context_length_exceeded` | A sample larger than the model's context, or an estimated cost above the per-request cap         |
| 429    | `rate_limit_exceeded`     | Over the per-key budget of 600 requests per minute, or the provider is throttling                |
| 502    | `service_error`           | The provider failed the request; retry                                                           |
| 503    | `service_unavailable`     | The service is unavailable; retry later                                                          |

## Limits and billing

* Any number of samples per request. Requests larger than a native call is allowed are split into batches internally and processed in order; only the estimated **cost** is capped, at \$5.00 per request.
* 600 requests per minute per key on this endpoint (the native routes allow 100).
* Billing is identical to the native routes: 1¢ is held when the request starts, the real token cost is settled when it finishes, and the hold is released if the request fails. If the balance cannot cover the settled cost the request returns `402` and no result.
* `/fix` and `/refine` input and output are kept for your history for 30 days, the same as natively. Audit stores nothing.
