# SDKs and compatibility

Use the OpenAI, Anthropic or OpenRouter SDK you already have; change the base URL and the key.

There is no SDK of our own to install. Each route speaks a shape an existing SDK already reads, so you point that SDK at the API and keep your code.

| SDK | Base URL | Key |
| --- | --- | --- |
| OpenAI (Python, JavaScript and the rest) | `https://api.vultrinference.com/v1` | `api_key` |
| Anthropic (Python, JavaScript), Claude Code | `https://api.vultrinference.com` (the SDK adds `/v1`) | `api_key`, sent as `x-api-key` |
| OpenRouter (`@openrouter/sdk`, `openrouter` for Python) | `https://api.vultrinference.com/v1` | `api_key` |

```python
import os

from openai import OpenAI

# The only change from OpenAI's own API: the base URL and the key
client = OpenAI(base_url="https://api.vultrinference.com/v1", api_key=os.environ["VULTR_INFERENCE_API_KEY"])

chat = client.chat.completions.create(
    model="deepseek-v4.1-flash",
    messages=[{"role": "user", "content": "Name three prime numbers."}],
)
print(chat.choices[0].message.content)

embedding = client.embeddings.create(model="qwen3-embedding-4b", input="Vultr Serverless Inference")
print(len(embedding.data[0].embedding), "dimensions")
```

```typescript
import OpenAI from "openai";

// The only change from OpenAI's own API: the base URL and the key
const client = new OpenAI({ baseURL: "https://api.vultrinference.com/v1", apiKey: process.env.VULTR_INFERENCE_API_KEY });

const chat = await client.chat.completions.create({
  model: "deepseek-v4.1-flash",
  messages: [{ role: "user", content: "Name three prime numbers." }],
});
console.log(chat.choices[0].message.content);

const embedding = await client.embeddings.create({ model: "qwen3-embedding-4b", input: "Vultr Serverless Inference" });
console.log(embedding.data[0].embedding.length, "dimensions");
```

```python
import os

from anthropic import Anthropic

# No /v1 here: Anthropic's SDK adds /v1/messages itself
client = Anthropic(base_url="https://api.vultrinference.com", api_key=os.environ["VULTR_INFERENCE_API_KEY"])

message = client.messages.create(
    model="deepseek-v4.1-flash",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Name three prime numbers."}],
)
print(message.content[-1].text)
```

```typescript
import Anthropic from "@anthropic-ai/sdk";

// No /v1 here: Anthropic's SDK adds /v1/messages itself
const client = new Anthropic({ baseURL: "https://api.vultrinference.com", apiKey: process.env.VULTR_INFERENCE_API_KEY });

const message = await client.messages.create({
  model: "deepseek-v4.1-flash",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Name three prime numbers." }],
});
const last = message.content[message.content.length - 1];
console.log(last.type === "text" ? last.text : last);
```

## What each route speaks

| Route | Shape | SDK calls |
| --- | --- | --- |
| `POST /v1/chat/completions` | OpenRouter's chat completion, which is OpenAI's with more fields | OpenAI `chat.completions.create`, OpenRouter `chat.send` |
| `POST /v1/responses` | OpenAI's Responses API | OpenAI `responses.create`, OpenRouter `responses.send` |
| `POST /v1/messages` | Anthropic's Messages API, errors included | Anthropic `messages.create`, Claude Code |
| `GET /v1/models` | Our model list; OpenRouter's or Anthropic's shape when the client asks for it | OpenAI and OpenRouter `models.list` |
| `POST /v1/images/generations`, `POST /v1/images/edits` | OpenAI's images API (a link by default) | OpenAI `images.generate`, `images.edit` |
| `POST /v1/images` | OpenRouter's images API (base64 by default) | OpenRouter `images.generate` |
| `POST /v1/videos` | OpenRouter's video API, with OpenAI's field names | OpenAI `videos.create_and_poll`, OpenRouter `videoGeneration` |
| `POST /v1/audio/speech` | OpenAI's speech API, with OpenRouter's fields | OpenAI `audio.speech.create`, OpenRouter `tts.createSpeech` |
| `POST /v1/audio/transcriptions` | OpenAI's (a file upload) or OpenRouter's (JSON) | OpenAI `audio.transcriptions.create`, OpenRouter `stt.createTranscription` |
| `POST /v1/embeddings` | OpenAI's embeddings, with OpenRouter's `input_type` | OpenAI `embeddings.create`, OpenRouter `embeddings.generate` |
| `POST /v1/rerank` | OpenRouter's rerank, compatible with Cohere's v2 | OpenRouter `rerank.rerank` |
| `POST /v1/systemone` | TypeSafe's System One API | TypeSafe's SDKs |
| `/v1/files` | OpenAI's Files API, for documents | OpenAI `files.create`, `files.retrieve`, `files.list`, `files.delete` |
| `POST /v1/music`, `/v1/vector_store` | Our own | Plain HTTP |

A field the SDK does not know goes in its extra-body option (`extra_body` in OpenAI's Python SDK), as [Reasoning](/docs/concepts/reasoning) shows.

## Where the API differs

Each route keeps its SDK's shape. These are the places it behaves differently from the original:

| Route | What differs |
| --- | --- |
| Chat completions | One answer shape whichever engine serves the model (below). No system prompt is added, and a final assistant message is continued rather than answered. |
| Responses | Nothing is stored, so send the whole conversation in `input` each time. Function, custom and namespace tools work; hosted tools (web search, file search, code interpreter) do not. |
| Messages | `model` and `max_tokens` are required. There is no prompt caching and no token counting route. The models are open models, not Claude ([Claude Code](/docs/workflows/claude-code)). |
| Files | Documents only, read once into text. A file expires after 30 days and cannot be downloaded again. |
| Videos | OpenAI's SDKs see `queued` for `pending`, and a cancelled or expired job as `failed`. Webhooks always carry the default shape. |
| Every route | OpenRouter's routing fields (`provider`, `models`, `route` and the like) are accepted and ignored: there is nothing to route between. |

### One chat shape

Whichever engine serves the model, a chat completion comes back the same way. A real one, with `created`, `system_fingerprint` and `logprobs` left out:

```json
{
  "id": "chatcmpl-00e333f8-d237-4c25-910f-8171becb0de9",
  "object": "chat.completion",
  "model": "deepseek-v4.1-flash",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "No, 221 is not prime because it equals 13 × 17.",
        "reasoning": "We need answer one sentence. Need determine 221 prime? 221 = 13*17? 13*17=221 yes composite. Answer one sentence."
      },
      "finish_reason": "stop",
      "native_finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 38,
    "completion_tokens": 51,
    "total_tokens": 89,
    "prompt_tokens_details": { "cached_tokens": 0 },
    "completion_tokens_details": { "reasoning_tokens": 33 }
  }
}
```

- The thinking is always `reasoning`, never `reasoning_content`.
- `finish_reason` is one of `stop`, `length`, `tool_calls`, `content_filter` or `error`. The engine's own word for it is in `native_finish_reason`.
- `usage` always has both detail objects, so code can read them without checking.
- A stream ends with a usage chunk whether or not you ask for one ([Streaming](/docs/concepts/streaming)).
