API reference
Thundera Models
One OpenAI-compatible gateway in front of every provider. Base URL https://models.thundera.eu/v1; Thundera apps use http://models.thundera.svc.cluster.local/v1.
Overview
The gateway speaks OpenAI Chat Completions, Embeddings and Audio plus the Anthropic Messages protocol, and routes each call to Anthropic (Claude API, direct), Amazon Bedrock, OpenAI, DeepSeek, Google, Deepgram or ElevenLabs, and exposes TypeSafe's Jev decision model on /v1/systemone. Each app has its own API key with a budget, limits and an allowlist. Every response states what it cost.
| Base URL | Who | Notes |
|---|---|---|
https://models.thundera.eu/v1 | External callers | Through CloudFront; see limits. |
http://models.thundera.svc.cluster.local/v1 | Thundera apps | No CloudFront limits; all headers pass. |
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /v1/chat/completions | OpenAI Chat Completions | stream, tools, response_format, images, max_tokens, reasoning_effort / reasoning / thinking, user |
| POST | /v1/messages | Anthropic Messages | max_tokens required; any provider behind it |
| POST | /v1/systemone | TypeSafe System One: {model:"decision", state, questions} | Decisions (JEV): typed answers with probabilities + confidence |
| GET | /v1/systemone/models | – | Decision models in TypeSafe's shape |
| POST | /v1/embeddings | {model, input, dimensions?} | ≤ 2048 inputs |
| POST | /v1/audio/transcriptions | multipart | ≤ 25 MB |
| POST | /v1/audio/speech | {model, input, voice?} | input ≤ 5000 chars (≤ 4096 non-ElevenLabs); returns audio |
| POST · GET · DELETE | /v1/audio/voices[/{voice_id}] | multipart name, files | clone ≤ 40 MB; per-key voices |
| GET | /v1/usage | – | Budget, limits, recent calls of the calling key |
| GET | /v1/models, /v1/models/{id} | – | Public |
| GET | /.well-known/jwks.json | – | Public |
| GET | /healthz, /readyz | – | Probes |
Quickstart
Export your key, then send a chat call and read cost from the response. Examples use the fast alias.
curl
export MODELS_API_KEY=...
curl -s https://models.thundera.eu/v1/chat/completions \
-H "Authorization: Bearer $MODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"fast","messages":[{"role":"user","content":"Hello"}]}' \
| jq '{answer: .choices[0].message.content, cost: .cost.total_usd, left: .cost.budget.remaining_usd}'
Node.js
Use the official openai package with baseURL.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: process.env.MODELS_BASE_URL ?? "https://models.thundera.eu/v1",
apiKey: process.env.MODELS_API_KEY,
});
const res = await client.chat.completions.create({
model: "fast",
messages: [{ role: "user", content: "Hello" }],
});
console.log(res.choices[0].message.content);
console.log("cost USD", res.cost.total_usd, "remaining", res.cost.budget?.remaining_usd);
Python
import os
from openai import OpenAI
client = OpenAI(
base_url=os.environ.get("MODELS_BASE_URL", "https://models.thundera.eu/v1"),
api_key=os.environ["MODELS_API_KEY"],
)
res = client.chat.completions.create(
model="fast",
messages=[{"role": "user", "content": "Hello"}],
)
print(res.choices[0].message.content)
cost = res.model_extra["cost"] # extra fields land in model_extra
print("cost USD", cost["total_usd"])
Go
Plain net/http; sashabaranov/go-openai works too via config.BaseURL, but its typed response drops cost.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
base := os.Getenv("MODELS_BASE_URL")
if base == "" {
base = "https://models.thundera.eu/v1"
}
body, _ := json.Marshal(map[string]any{
"model": "fast",
"messages": []map[string]string{{"role": "user", "content": "Hello"}},
})
req, _ := http.NewRequest("POST", base+"/chat/completions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("MODELS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out struct {
Choices []struct {
Message struct{ Content string } `json:"message"`
} `json:"choices"`
Cost struct {
TotalUSD float64 `json:"total_usd"`
} `json:"cost"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Choices[0].Message.Content, out.Cost.TotalUSD)
}
Rust
With reqwest (features json) and serde_json.
use serde_json::{json, Value};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let base = std::env::var("MODELS_BASE_URL")
.unwrap_or_else(|_| "https://models.thundera.eu/v1".into());
let key = std::env::var("MODELS_API_KEY").expect("MODELS_API_KEY");
let res: Value = reqwest::Client::new()
.post(format!("{base}/chat/completions"))
.bearer_auth(key)
.json(&json!({
"model": "fast",
"messages": [{ "role": "user", "content": "Hello" }]
}))
.send().await?
.json().await?;
println!("{}", res["choices"][0]["message"]["content"]);
println!("cost USD {}", res["cost"]["total_usd"]);
Ok(())
}
Authentication
Send Authorization: Bearer <api key>. Keys are RS256 JWTs issued per app in /system and verifiable offline with /.well-known/jwks.json. Status is database-authoritative: regenerating or revoking a key takes effect on the very next call.
In-cluster, x-api-key: <key> is also accepted. CloudFront strips that header, so external callers must use Authorization.
Thundera user access tokens (auth.thundera.eu) can be accepted when Settings → accept_user_tokens is on. They get a small default budget and an alias allowlist. Currently use an API key.
Models & aliases
GET /v1/models is public and returns {object:"list", data:[…]} with two kinds of entries. Model ids are <provider>/<provider model id>; a bare provider id such as gpt-4.1-mini also resolves. Only enabled and priced models are listed: an unpriced model is never served.
Aliases
Aliases are admin-mapped chains. Use them instead of vendor ids. Failover happens only before the first byte is sent, only on retryable upstream failures (throttling, 5xx, timeouts, model unavailable or not entitled) and never on a 400.
| Alias | Type | Use | Chain (failover in order) |
|---|---|---|---|
cheap |
chat | The cheapest acceptable chat model, for very high volume. | Amazon Nova Micro (us) → OpenAI gpt-oss-20b → Amazon Nova Lite (us) |
code |
chat | Code generation, review and refactoring. | Claude Sonnet 5.5 → Claude Sonnet 5 → Qwen Qwen3-Coder-30B-A3B-Instruct → deepseek-flash |
decision |
decision | Calibrated typed decisions (noul / choice / score / box with probabilities and confidence) from TypeSafe Jev. Only valid on POST /v1/systemone — not a chat model. | Jev 1.14 → Jev 1.13 |
embed |
embedding | Text embeddings, 1024 dimensions (Amazon Titan Text Embeddings v2). Single target: never fails over to a different vector space. | Amazon Titan Text Embeddings V2 |
fast |
chat | Low-latency, low-cost chat for short tasks, routing and extraction. | Amazon Nova Lite (us) → deepseek-flash → Qwen Qwen3 32B (dense) → gpt-4.1-nano |
reasoning |
chat | Multi-step reasoning, planning and hard problems. Slower; returns reasoning_content when the model exposes it. | Claude Opus 5.5 → Claude Opus 5 → gpt-5 → bedrock/global.anthropic.claude-opus-5 → Anthropic Claude Sonnet 4.5 (global) → deepseek-v4-pro |
smart |
chat | Balanced quality for most product features: writing, analysis, tool use. | Claude Sonnet 5.5 → Claude Sonnet 5 → bedrock/global.anthropic.claude-sonnet-5 → Anthropic Claude Haiku 4.5 (us) → gpt-4.1-mini → deepseek-v4-pro |
transcribe |
transcription | Speech-to-text for recorded audio files (multilingual). | Deepgram Nova-3 → gpt-4o-mini-transcribe → whisper-1 |
tts |
speech | Premium text-to-speech and cloned voices (ElevenLabs). voice = an ElevenLabs voice id; an ElevenLabs voice never fails over to another vendor. | Eleven Multilingual v2 → Eleven Flash v2.5 → tts-1 |
vision |
chat | Image understanding (send image_url parts as base64 data URLs). | Claude Haiku 4.5 → Anthropic Claude Haiku 4.5 (us) → gpt-4.1-mini → Amazon Nova Lite (us) |
All models
Prices are USD per 1M tokens unless noted. Capabilities: streaming, tools, vision, reasoning.
| Model id | Type | Capabilities | Context | Input | Output | Health |
|---|---|---|---|---|---|---|
| Claude Fable 5anthropic/claude-fable-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $10.00 | $50.00 | ok |
| Claude Fable 5.1anthropic/claude-fable-5-1 | chat | tools, vision, reasoning, streaming | 1,000,000 | $10.00 | $50.00 | ok |
| Claude Haiku 4.5anthropic/claude-haiku-4-5-20251001 | chat | tools, vision, reasoning, streaming | 200,000 | $1.00 | $5.00 | ok |
| Claude Haiku 5.5anthropic/claude-haiku-5-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $0.10 | $0.50 | ok |
| Claude Opus 4.5anthropic/claude-opus-4-5-20251101 | chat | tools, vision, reasoning, streaming | 200,000 | $5.00 | $25.00 | ok |
| Claude Opus 4.6anthropic/claude-opus-4-6 | chat | tools, vision, reasoning, streaming | 1,000,000 | $5.00 | $25.00 | ok |
| Claude Opus 4.7anthropic/claude-opus-4-7 | chat | tools, vision, reasoning, streaming | 1,000,000 | $5.00 | $25.00 | ok |
| Claude Opus 4.8anthropic/claude-opus-4-8 | chat | tools, vision, reasoning, streaming | 1,000,000 | $5.00 | $25.00 | ok |
| Claude Opus 5anthropic/claude-opus-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $5.00 | $25.00 | ok |
| Claude Opus 5.5anthropic/claude-opus-5-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $4.00 | $20.00 | ok |
| Claude Sonnet 4.6anthropic/claude-sonnet-4-6 | chat | tools, vision, reasoning, streaming | 1,000,000 | $3.00 | $15.00 | ok |
| Claude Sonnet 5anthropic/claude-sonnet-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $2.00 | $10.00 | ok |
| Claude Sonnet 5.5anthropic/claude-sonnet-5-5 | chat | tools, vision, reasoning, streaming | 1,000,000 | $2.00 | $10.00 | ok |
| Amazon Nova Litebedrock/amazon.nova-lite-v1:0 | chat | tools, vision, streaming | — | $0.06 | $0.24 | ok |
| Amazon Nova Microbedrock/amazon.nova-micro-v1:0 | chat | tools, streaming | — | $0.035 | $0.14 | ok |
| Amazon Nova Probedrock/amazon.nova-pro-v1:0 | chat | tools, vision, streaming | — | $0.80 | $3.20 | ok |
| Amazon Titan Embeddings G1 - Textbedrock/amazon.titan-embed-text-v1 | embedding | — | — | $0.10 | — | ok |
| Amazon Titan Text Embeddings V2bedrock/amazon.titan-embed-text-v2:0 | embedding | — | — | $0.02 | — | ok |
| Amazon Nova 2 Lite (global)bedrock/global.amazon.nova-2-lite-v1:0 | chat | tools, vision, streaming | — | $0.30 | $2.50 | ok |
| Anthropic Claude Haiku 4.5 (global)bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0 | chat | tools, vision, streaming | — | $1.00 | $5.00 | ok |
| Anthropic Claude Sonnet 4.5 (global)bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0 | chat | tools, vision, streaming | — | $3.00 | $15.00 | ok |
| OpenAI gpt-oss-120bbedrock/openai.gpt-oss-120b-1:0 | chat | tools, streaming | — | $0.15 | $0.60 | ok |
| OpenAI gpt-oss-20bbedrock/openai.gpt-oss-20b-1:0 | chat | tools, streaming | — | $0.07 | $0.30 | ok |
| Qwen Qwen3 32B (dense)bedrock/qwen.qwen3-32b-v1:0 | chat | tools, streaming | — | $0.20 | $0.78 | ok |
| Qwen Qwen3-Coder-30B-A3B-Instructbedrock/qwen.qwen3-coder-30b-a3b-v1:0 | chat | tools, streaming | — | $0.15 | $0.60 | ok |
| Qwen Qwen3 Coder Nextbedrock/qwen.qwen3-coder-next | chat | tools, streaming | — | $0.50 | $1.20 | ok |
| Qwen Qwen3 Next 80B A3Bbedrock/qwen.qwen3-next-80b-a3b | chat | tools, streaming | — | $0.15 | $1.20 | ok |
| Qwen Qwen3 VL 235B A22Bbedrock/qwen.qwen3-vl-235b-a22b | chat | tools, vision, streaming | — | $0.53 | $2.66 | ok |
| Amazon Nova 2 Lite (us)bedrock/us.amazon.nova-2-lite-v1:0 | chat | tools, vision, streaming | — | $0.30 | $2.50 | ok |
| Amazon Nova Lite (us)bedrock/us.amazon.nova-lite-v1:0 | chat | tools, vision, streaming | — | $0.06 | $0.24 | ok |
| Amazon Nova Micro (us)bedrock/us.amazon.nova-micro-v1:0 | chat | tools, streaming | — | $0.035 | $0.14 | ok |
| Amazon Nova Pro (us)bedrock/us.amazon.nova-pro-v1:0 | chat | tools, vision, streaming | — | $0.80 | $3.20 | ok |
| Anthropic Claude Haiku 4.5 (us)bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 | chat | tools, vision, streaming | — | $1.10 | $5.50 | ok |
| Anthropic Claude Sonnet 4.5 (us)bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0 | chat | tools, vision, streaming | — | $3.30 | $16.50 | ok |
| DeepSeek DeepSeek-R1 (us)bedrock/us.deepseek.r1-v1:0 | chat | reasoning, streaming | — | $1.35 | $5.40 | ok |
| Meta Llama 3.1 70B Instruct (us)bedrock/us.meta.llama3-1-70b-instruct-v1:0 | chat | tools, streaming | — | $0.72 | $0.72 | ok |
| Meta Llama 3.1 8B Instruct (us)bedrock/us.meta.llama3-1-8b-instruct-v1:0 | chat | tools, streaming | — | $0.22 | $0.22 | ok |
| Meta Llama 3.3 70B Instruct (us)bedrock/us.meta.llama3-3-70b-instruct-v1:0 | chat | tools, streaming | — | $0.72 | $0.72 | ok |
| Meta Llama 4 Maverick 17B Instruct (us)bedrock/us.meta.llama4-maverick-17b-instruct-v1:0 | chat | tools, vision, streaming | — | $0.24 | $0.97 | degraded |
| Meta Llama 4 Scout 17B Instruct (us)bedrock/us.meta.llama4-scout-17b-instruct-v1:0 | chat | tools, vision, streaming | — | $0.17 | $0.66 | degraded |
| Mistral AI Pixtral Large (25.02) (us)bedrock/us.mistral.pixtral-large-2502-v1:0 | chat | tools, vision, streaming | — | $2.00 | $6.00 | ok |
| Deepgram Nova-2deepgram/nova-2 | transcription | — | — | $0.0043 /min | — | unknown |
| Deepgram Nova-3deepgram/nova-3 | transcription | — | — | $0.0052 /min | — | ok |
| deepseek-flashdeepseek/deepseek-flash | chat | tools, streaming | — | $0.22 | $0.66 | ok |
| deepseek-v4-prodeepseek/deepseek-v4-pro | chat | tools, reasoning, streaming | — | $0.66 | $1.98 | ok |
| Eleven Flash v2elevenlabs/eleven_flash_v2 | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Eleven Flash v2.5elevenlabs/eleven_flash_v2_5 | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Eleven Multilingual v2elevenlabs/eleven_multilingual_v2 | speech | languages, voice_cloning, max_characters | — | $80.00 /1M chars | — | ok |
| Eleven Turbo v2elevenlabs/eleven_turbo_v2 | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Eleven Turbo v2.5elevenlabs/eleven_turbo_v2_5 | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Eleven v3elevenlabs/eleven_v3 | speech | languages, max_characters | — | $80.00 /1M chars | — | unknown |
| Eleven v3 Conversationalelevenlabs/eleven_v3_conversational | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Eleven v4elevenlabs/eleven_v4 | speech | languages, max_characters | — | $80.00 /1M chars | — | unknown |
| Eleven v4 Turboelevenlabs/eleven_v4_turbo | speech | languages, max_characters | — | $40.00 /1M chars | — | unknown |
| Jev 1.13jev/jev-1.13.0 | decision | vision, decision, long_context | 64,000 | $0.042 | $0.00 | ok |
| Jev 1.14jev/jev-1.14.0 | decision | vision, decision, long_context | 64,000 | $0.042 | $0.00 | ok |
| jev-latestjev/jev-latest | decision | vision, decision, long_context | 64,000 | $0.042 | $0.00 | ok |
| jev-previewjev/jev-preview | decision | vision, decision, long_context | 64,000 | $0.042 | $0.00 | ok |
| gpt-4.1openai/gpt-4.1 | chat | tools, vision, streaming | — | $2.00 | $8.00 | ok |
| gpt-4.1-2025-04-14openai/gpt-4.1-2025-04-14 | chat | tools, vision, streaming | — | $2.00 | $8.00 | ok |
| gpt-4.1-miniopenai/gpt-4.1-mini | chat | tools, vision, streaming | — | $0.40 | $1.60 | ok |
| gpt-4.1-mini-2025-04-14openai/gpt-4.1-mini-2025-04-14 | chat | tools, vision, streaming | — | $0.40 | $1.60 | ok |
| gpt-4.1-nanoopenai/gpt-4.1-nano | chat | tools, vision, streaming | — | $0.10 | $0.40 | ok |
| gpt-4.1-nano-2025-04-14openai/gpt-4.1-nano-2025-04-14 | chat | tools, vision, streaming | — | $0.10 | $0.40 | ok |
| gpt-4oopenai/gpt-4o | chat | tools, vision, streaming | — | $2.50 | $10.00 | ok |
| gpt-4o-2024-05-13openai/gpt-4o-2024-05-13 | chat | tools, vision, streaming | — | $2.50 | $10.00 | ok |
| gpt-4o-2024-08-06openai/gpt-4o-2024-08-06 | chat | tools, vision, streaming | — | $2.50 | $10.00 | ok |
| gpt-4o-2024-11-20openai/gpt-4o-2024-11-20 | chat | tools, vision, streaming | — | $2.50 | $10.00 | ok |
| gpt-4o-miniopenai/gpt-4o-mini | chat | tools, vision, streaming | — | $0.15 | $0.60 | ok |
| gpt-4o-mini-2024-07-18openai/gpt-4o-mini-2024-07-18 | chat | tools, vision, streaming | — | $0.15 | $0.60 | ok |
| gpt-4o-mini-transcribeopenai/gpt-4o-mini-transcribe | transcription | — | — | $0.003 /min | — | ok |
| gpt-4o-mini-transcribe-2025-03-20openai/gpt-4o-mini-transcribe-2025-03-20 | transcription | — | — | $0.003 /min | — | unknown |
| gpt-4o-mini-transcribe-2025-12-15openai/gpt-4o-mini-transcribe-2025-12-15 | transcription | — | — | $0.003 /min | — | unknown |
| gpt-4o-transcribeopenai/gpt-4o-transcribe | transcription | — | — | $0.006 /min | — | unknown |
| gpt-5openai/gpt-5 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $1.25 | $10.00 | ok |
| gpt-5-2025-08-07openai/gpt-5-2025-08-07 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $1.25 | $10.00 | ok |
| gpt-5.6-lunaopenai/gpt-5.6-luna | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.22 | $1.32 | ok |
| gpt-5.6-solopenai/gpt-5.6-sol | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $4.40 | $22.00 | ok |
| gpt-5.6-terraopenai/gpt-5.6-terra | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $2.20 | $13.20 | ok |
| gpt-5-miniopenai/gpt-5-mini | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.25 | $2.00 | ok |
| gpt-5-mini-2025-08-07openai/gpt-5-mini-2025-08-07 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.25 | $2.00 | ok |
| gpt-5-nanoopenai/gpt-5-nano | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.05 | $0.40 | ok |
| gpt-5-nano-2025-08-07openai/gpt-5-nano-2025-08-07 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.05 | $0.40 | ok |
| gpt-6.1-solopenai/gpt-6.1-sol | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $2.00 | $10.00 | ok |
| gpt-6-astraopenai/gpt-6-astra | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $10.00 | $50.00 | ok |
| gpt-6-lunaopenai/gpt-6-luna | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $0.10 | $0.50 | ok |
| gpt-6-solopenai/gpt-6-sol | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $2.00 | $10.00 | ok |
| o3openai/o3 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $2.00 | $8.00 | ok |
| o3-2025-04-16openai/o3-2025-04-16 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $2.00 | $8.00 | ok |
| o3-miniopenai/o3-mini | chat | tools, reasoning, streaming, fixed_temperature | — | $1.10 | $4.40 | ok |
| o3-mini-2025-01-31openai/o3-mini-2025-01-31 | chat | tools, reasoning, streaming, fixed_temperature | — | $1.10 | $4.40 | ok |
| o4-miniopenai/o4-mini | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $1.10 | $4.40 | ok |
| o4-mini-2025-04-16openai/o4-mini-2025-04-16 | chat | tools, vision, reasoning, streaming, fixed_temperature | — | $1.10 | $4.40 | ok |
| text-embedding-3-largeopenai/text-embedding-3-large | embedding | — | — | $0.13 | — | ok |
| text-embedding-3-smallopenai/text-embedding-3-small | embedding | — | — | $0.02 | — | ok |
| text-embedding-ada-002openai/text-embedding-ada-002 | embedding | — | — | $0.10 | — | ok |
| tts-1openai/tts-1 | speech | — | — | $15.00 /1M chars | — | unknown |
| tts-1-1106openai/tts-1-1106 | speech | — | — | $15.00 /1M chars | — | unknown |
| tts-1-hdopenai/tts-1-hd | speech | — | — | $30.00 /1M chars | — | unknown |
| tts-1-hd-1106openai/tts-1-hd-1106 | speech | — | — | $30.00 /1M chars | — | unknown |
| whisper-1openai/whisper-1 | transcription | — | — | $0.006 /min | — | ok |
Providers
| Provider | Example ids |
|---|---|
| Anthropic (Claude API, direct) Primary for Claude: thinking, signed-block round trip, prompt caching, 1M context on 4.6+. Priced from anthropic.com/pricing. | anthropic/claude-sonnet-5-5 anthropic/claude-opus-5-5 … |
| Amazon Bedrock Claude failover, Nova, open-weight models, Titan embeddings. us./in-region Claude costs 1.1× global. | bedrock/global.anthropic.… bedrock/us.amazon.nova-… |
| OpenAI Chat, reasoning, embeddings, transcription, speech. | openai/gpt-5 openai/o4-mini … |
| DeepSeek Thinks by default; see Reasoning. | deepseek/deepseek-v4-pro deepseek/deepseek-flash |
| Google Gemini models that are priced and open to the account. | google/gemini-… |
| TypeSafe (Jev) System One decision model: typed answers with probabilities, not text. Only on /v1/systemone. $0.042 per 1M input tokens, output free. | decision jev/jev-1.14.0 jev/jev-1.13.0 |
| Deepgram File transcription. | deepgram/nova-3 |
Prompt caching (Claude). Add cache_control: {"type": "ephemeral"} at the top level (automatic: the cache point moves with the conversation) or on a system/text/image/tool part; {"type":"ephemeral","ttl":"1h"} for one hour. Cache writes cost 1.25× input (1-hour: 2×), reads 0.05–0.1× — all reflected in cost. Other providers ignore the marker.
Streaming
Set "stream": true for Server-Sent Events. The final chunk has choices: [] and carries cost, then data: [DONE]. Keepalives are sent every 15 s.
const stream = await client.chat.completions.create({
model: "smart",
stream: true,
messages: [{ role: "user", content: "Write a haiku" }],
});
for await (const chunk of stream) {
if (chunk.choices.length) process.stdout.write(chunk.choices[0].delta.content ?? "");
else console.log("\ncost USD", chunk.cost.total_usd); // final chunk
}
On /v1/messages the Anthropic event protocol is used and cost arrives in the message_delta event. If a stream aborts and the provider reported no usage, cost.estimated is true.
Tools
Standard OpenAI tools and tool_choice, on every model with the tools capability. Use smart for tool-heavy features.
{
"model": "smart",
"messages": [{"role": "user", "content": "Weather in Oslo?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
}
}],
"tool_choice": "auto"
}
response_format (JSON mode and schema) is also supported.
Reasoning
One vocabulary for every provider. Send any one of these; the gateway translates it to the chosen model's own control:
| Field | Values |
|---|---|
reasoning_effort | none · minimal · low · medium · high · xhigh |
reasoning | {"effort": "low"}, {"max_tokens": 4000}, {"enabled": false} |
thinking | {"type": "enabled", "budget_tokens": 4000} or {"type": "disabled"} |
Mapping: OpenAI reasoning models get reasoning_effort; DeepSeek gets thinking (it thinks by default — send thinking: {"type":"disabled"} for fast agent turns); Claude (direct and on Bedrock) gets the control its generation accepts: Claude 4.6 and later get adaptive thinking with output_config.effort (and a readable summary), Claude 4.5 and earlier get extended thinking with a token budget (low 1024 · medium 4096 · high 16384, below max_tokens); Claude 5 thinks by default and thinking: {"type":"disabled"} turns it off where the model allows it; temperature/top_p are omitted wherever Claude rejects them (always on 4.7+); gpt-oss gets reasoning_effort; Nova 2 gets reasoningConfig. A model that cannot honour the control never receives it: the call still succeeds and the response carries x-models-dropped-params. Claude 4.5 cannot think while tool_choice forces a tool, and Opus 5.5 / Sonnet 5.5 / Fable 5.1 never accept a forced tool, so it becomes auto (reported).
Tool loops: send the reasoning back
The answer carries message.reasoning_content (streams: delta.reasoning_content) and, for Claude, message.reasoning_signature (streams: delta.reasoning_signature). Claude answers also carry message.reasoning_blocks — every signed thinking block of the turn, in order (streams: one delta.reasoning_blocks chunk before the finish chunk). Claude 5 hides its thinking text by default, so a block may hold only a signature. In a multi-turn tool loop, put the assistant message back exactly as received, including these fields. The gateway restores each provider's native form (DeepSeek: reasoning_content; Claude: a signed thinking block) and strips them for providers that do not take them.
{
"model": "reasoning",
"reasoning_effort": "medium",
"tools": [...],
"messages": [
{"role": "user", "content": "Weather in Oslo, in Fahrenheit?"},
{"role": "assistant", "content": null,
"reasoning_content": "I need the weather tool first…", "reasoning_signature": "EqQBCkgIAR…",
"tool_calls": [{"id": "t1", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\":\"Oslo\"}"}}]},
{"role": "tool", "tool_call_id": "t1", "content": "{\"temp_c\": 4}"}
]
}
If a client drops the reasoning and DeepSeek refuses the follow-up turn, the gateway retries once with thinking disabled and says so in x-models-reasoning: disabled_for_tool_turn. On /v1/messages the same works with Anthropic thinking blocks (signature included) and signature_delta events.
Vision
Send image_url content parts. Use data URLs for portability; plain https URLs work only with OpenAI and Google models. The vision alias picks an image-capable model.
{
"model": "vision",
"messages": [{"role": "user", "content": [
{"type": "text", "text": "What is in this picture?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo..."}}
]}]
}
Decisions (JEV)
Jev (TypeSafe's System One model) does not write text. You send a state and named, typed questions; it returns calibrated answers your code can branch on: a yes/no probability (noul), one option out of a closed set (choice), a rating on an ordered rubric (score) or a bounding box in an image. choice and score answers carry probabilities and a confidence from 0 to 1. The call is POST /v1/systemone with the same request and response shape as TypeSafe's API, so TypeSafe's SDKs work against the gateway; the gateway adds the key's budget and limits, request logging and a cost object. Model alias: decision (newest Jev, then the previous release). It is valid only on /v1/systemone; decision or a jev/… id on /v1/chat/completions or /v1/messages returns 400 use_systemone.
JEV or an LLM?
Use JEV (decision) when… | Use an LLM (smart, fast…) when… |
|---|---|
| Your code needs a decision: route, gate, classify, rank, moderate, filter, extract a category. | A person reads the output: prose, summaries, code, explanations. |
| You want calibrated probabilities and a confidence you can threshold. | You need free-form generation, tools or multi-step reasoning. |
| Many questions about the same text, in one call (fan-out) at ≈ $0.042 per million input tokens, output free. | The answer is open-ended and has no closed set of outcomes. |
| Latency matters (typically a fraction of a second). | You need streaming, long outputs or non-text media other than PNG/JPEG. |
Rule of thumb: let JEV decide whether and where; let an LLM write what. JEV reads text (a string, JSON or an array); audio and video must be turned into text first. English is where accuracy is best; test other languages and lean on confidence.
One call, several questions
Questions are named by you; answers come back under the same names. Mix types freely. The state is read once and every question is evaluated against it, so put many questions in one request instead of one request each.
curl -s https://models.thundera.eu/v1/systemone \
-H "Authorization: Bearer $MODELS_API_KEY" -H "Content-Type: application/json" \
-H "X-Thundera-User: user-123" \
-d '{
"model": "decision",
"state": "I was charged twice for my subscription this month. Please refund the extra charge.",
"questions": {
"billing": { "type": "noul", "instructions": "Is this message about billing?" },
"tone": { "type": "choice", "instructions": "What is the tone of this message?",
"criteria": { "angry": "An upset or hostile message",
"calm": "A neutral or polite message",
"excited": "An enthusiastic or eager message" } },
"urgency": { "type": "score", "instructions": "How urgent is this message?",
"criteria": ["Can wait", "Needs attention this week", "Needs attention today"] }
}
}'
{
"model": "jev-1.14.0",
"answers": {
"billing": { "type": "noul", "noul": 0.98 },
"tone": { "type": "choice", "choice": "calm", "confidence": 0.7,
"probabilities": { "angry": 0.15, "calm": 0.8, "excited": 0.05 } },
"urgency": { "type": "score", "score": 1.7, "confidence": 0.55,
"legend": { "0": "Can wait", "1": "Needs attention this week", "2": "Needs attention today" },
"probabilities": { "0": 0.1, "1": 0.1, "2": 0.8 } }
},
"usage": { "input_tokens": 407, "output_tokens": 73 },
"cost": { "request_id": "…", "model": "jev/jev-1.14.0", "currency": "USD", "total_usd": 0.00001709, "input_usd": 0.00001709 }
}
noul: probability of yes, 0..1. Near 0.5 means unsure. Noconfidencefield: treat distance from 0.5 as certainty.choice:criteriais an object (≤ 255 options, name → description). Returnschoice,probabilities,confidence.score:criteriais an ordered array (1–10 level descriptions, position = score from 0). Returns a fractionalscore,legend,probabilities,confidence.box: locates something in an image (images).state,instructionsand criteria descriptions may be a string, an object or an array; refer to a field in a question with backticks (`ticket.subject`).
Confidence-gated routing
The answer says what; confidence says whether to act. Gate each action at a threshold that matches its risk, and send the uncertain middle to a human or an LLM.
const r = await fetch("https://models.thundera.eu/v1/systemone", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MODELS_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
model: "decision", state: ticketText,
questions: { route: { type: "choice", instructions: "Where should this ticket go?",
criteria: { billing: "Charges, refunds, invoices", bug: "Something is broken", other: "Anything else" } } },
}),
}).then((x) => x.json());
const a = r.answers.route;
if (a.confidence >= 0.8) queue(a.choice); // act automatically
else if (a.confidence >= 0.5) queue(a.choice, { review: true }); // act, flag for review
else escalateToHuman(); // or ask an LLM ("smart")
Thresholds are yours to tune: they scale with the cost of being wrong. A moderation block deserves a higher bar than a label. Pin jev/jev-1.13.0 or jev/jev-1.14.0 instead of decision if you tuned thresholds against one release; the response model tells you which one answered.
Fan-out
Ask everything you might need, including speculative questions, then decide in code which answers matter. Batching 13 questions in one call is about 12× cheaper and 10× faster than 13 calls (TypeSafe's benchmark). There is no question-count limit; the request must fit the context: 64k tokens per request and 32k per question (262k with long context). Typical uses: a guardrail for every user message (injection? PII? self-harm? off-topic?), a composite score built from several atomic score questions with weights in your code, ranking retrieved passages with one score per passage.
SDKs
TypeSafe's SDKs accept a base URL; use the gateway key as the API key and decision as the model (Python model=, Node defaultModel; an SDK left on its default jev-latest also works, the gateway resolves it to jev/jev-latest). The base URL is the host without /v1 (the SDK adds /v1/systemone). In-cluster: http://models.thundera.svc.cluster.local.
# pip install typesafe-sdk
import os
from typesafe_sdk import TypeSafeClient, Noul, Choice
client = TypeSafeClient(
api_key=os.environ["MODELS_API_KEY"],
base_url="https://models.thundera.eu",
model="decision",
)
res = client.system_one(
"I was charged twice. Please refund me.",
{
"billing": Noul(instructions="Is this about billing?"),
"tone": Choice(instructions="Tone?", criteria={"angry": "Upset", "calm": "Neutral or polite"}),
},
)
print(res.nouls["billing"].noul, res.model)
// npm install @typesafe-ai/sdk
import { choice, noul, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.MODELS_API_KEY,
baseURL: "https://models.thundera.eu",
defaultModel: "decision",
});
const res = await client.systemOne({
state: "I was charged twice. Please refund me.",
questions: {
billing: noul("Is this about billing?"),
tone: choice("Tone?", { angry: "Upset", calm: "Neutral or polite" }),
},
});
console.log(res.answers.billing.noul, res.answers.tone.choice);
Per-user budgets work as everywhere: send X-Thundera-User (Python extra_headers, Node headers option or defaultHeaders). The cost is in the response body (cost) and in x-models-cost-usd.
Images, Box and long context (opt-in)
TypeSafe ships these as experimental, behind TypeSafe-Danger-Zone-Images: true (any request with an assets map or a box question) and TypeSafe-Danger-Zone-Long-Context: true (262k context). The gateway forwards each header only if the key was granted the feature (decision.images, decision.long_context; set in /system → Keys); otherwise 403 feature_not_allowed. Decision models themselves are opt-in per key: a key needs the decision feature (ask an admin; app keys get it when the app adopts JEV). Images are PNG or JPEG in assets as {"type":"image","format":"png","data_base64":"…"} (multipart upload is not supported by the gateway), are billed as ordinary input tokens, and their bytes are never stored in the request log. Every place a placeholder appears counts the image again: put an image in the state once and refer to it by path.
Cost & limits
$0.042 per 1M input tokens, output tokens free (TypeSafe pricing): cost.total_usd = usage.input_tokens × 0.042 / 1,000,000. Every call carries about 270 tokens of fixed overhead, so a small request (a few questions on a short text) bills 300–500 input tokens, about $0.00002; 10 questions on a 2,000-character text is about 900 tokens, $0.00004. The budget pre-check reserves a pessimistic estimate (≤ the context limit, ≤ $0.0027 without long context) and the real charge uses the returned usage. The key's allowlist, RPM/TPM and X-Thundera-User budgets apply. Each call is a request_log row with modality decision; request and response payloads are stored (encrypted) only for keys with payload logging on. Metrics: models_decision_questions_total{app,type} and models_decision_confidence{app,type} (a histogram of confidence values, no content).
Errors follow the gateway shape plus TypeSafe's own: 422 validation_error (with detail[] as in TypeSafe, also at the top level), 400 max_tokens_exceeded (over the context length) or invalid_request_error, 413 body over 32 MiB, 415 unsupported Content-Encoding (gzip and zstd bodies are accepted up to the size limit), 429 with Retry-After, and 503 upstream_overloaded for TypeSafe's 529. Transient failures are retried once and fail over from jev-1.14.0 to jev-1.13.0. GET /v1/systemone/models (and models in GET /v1/models) lists the decision models in TypeSafe's shape.
Embeddings
POST /v1/embeddings with {model, input: string | string[], dimensions?, encoding_format?}, up to 2048 inputs. The embed alias is a single target (1024-d Titan v2) and never fails over, so vectors stay comparable.
curl -s https://models.thundera.eu/v1/embeddings \
-H "Authorization: Bearer $MODELS_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"embed","input":["first text","second text"]}'
Audio
Transcriptions
POST /v1/audio/transcriptions, multipart: file (≤ 25 MB), model (transcribe), optional language, prompt, and response_format of json, text or verbose_json. Billed per audio minute.
curl -s https://models.thundera.eu/v1/audio/transcriptions \
-H "Authorization: Bearer $MODELS_API_KEY" \
-F file=@meeting.mp3 -F model=transcribe -F response_format=verbose_json
Speech (Premium TTS)
POST /v1/audio/speech with {model, input, voice, response_format?, speed?, voice_settings?} streams audio back (audio/mpeg for ElevenLabs by default). Billed per input character. Use the alias tts (ElevenLabs Multilingual v2, then Flash v2.5) or a model id such as elevenlabs/eleven_multilingual_v2. voice is an ElevenLabs voice id (e.g. TyBr4pQkGki6qkndJSGq) — it never fails over to another vendor; OpenAI models (openai/tts-1) take names such as alloy. input accepts up to 5000 characters (4096 for non-ElevenLabs models). Optional voice_settings: {stability, similarity_boost, style, use_speaker_boost, speed}. response_format: mp3 (default), opus, pcm (ElevenLabs). Errors: voice_not_found (404, also for a cloned voice owned by another key), voice_provider_mismatch (400).
curl -s https://models.thundera.eu/v1/audio/speech \
-H "Authorization: Bearer $MODELS_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"tts","input":"Hello from Thundera","voice":"TyBr4pQkGki6qkndJSGq"}' -o hello.mp3
Voice cloning
ElevenLabs Instant Voice Cloning, owned per API key. POST /v1/audio/voices (multipart): name (required, ≤ 100 chars), optional description, one or more files (audio, ≤ 40 MB in total, otherwise 413) → 201 {"id","provider":"elevenlabs","name"}. GET /v1/audio/voices lists your key's voices. DELETE /v1/audio/voices/{voice_id} removes one of your voices (404 voice_not_found if it is not yours or does not exist). Use the returned id as voice in speech calls; another key gets 404 for it. At most 25 voices per key (409 voice_limit_reached). Cloning itself is free of per-character cost; every call is logged without audio.
curl -s https://models.thundera.eu/v1/audio/voices \
-H "Authorization: Bearer $MODELS_API_KEY" \
-F name="My voice" -F files=@sample.mp3
curl -s -X DELETE https://models.thundera.eu/v1/audio/voices/$VOICE_ID -H "Authorization: Bearer $MODELS_API_KEY"
Anthropic Messages endpoint
POST /v1/messages accepts the Anthropic wire format and can sit in front of any provider. max_tokens is required; system, tools, tool_choice, images, thinking, output_config.effort, cache_control (block-level and top-level automatic caching) and stream (Anthropic event protocol) are supported. Routed to a anthropic/… model, the call goes to the Claude API natively; signed thinking blocks round-trip unchanged. Point the Anthropic SDK's base URL at https://models.thundera.eu.
curl -s https://models.thundera.eu/v1/messages \
-H "Authorization: Bearer $MODELS_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"smart","max_tokens":256,"system":"Be brief.",
"messages":[{"role":"user","content":"Hello"}]}'
End users & per-user budgets
Attribute a call to your user with X-Thundera-User: <opaque user id> (in-cluster) or the body field user, which also works through CloudFront. For /v1/messages use metadata.user_id.
Each user has a monthly budget (UTC calendar month), the tighter of:
- the key policy
end_user_limit_usd(set in /system), and - the per-call
X-Thundera-User-Budget-Usd: <usd>header (or bodyuser_budget_usd): pass the user's planlimits.ai_monthly_usdfromGET https://auth.thundera.eu/api/v1/auth/billing/effective-plan/<app>.
When exceeded the call fails with 402 end_user_budget_exceeded.
const res = await client.chat.completions.create(
{ model: "smart", messages },
{ headers: { "X-Thundera-User": user.id, "X-Thundera-User-Budget-Usd": String(plan.limits.ai_monthly_usd) } },
);
Budgets & limits
Guardrails are configured per key in /system.
- USD budget with period
daily,monthlyortotal. It is a hard cap: each call reserves its worst-case cost (input estimate +max_tokens× output rate of the most expensive candidate) under a row lock. A call that might exceed the remaining budget is refused with402 budget_exceeded; lowermax_tokens. - Alert thresholds (default 50 / 80 / 100 %) notify Slack and Prometheus.
- RPM / TPM limits return
429 rate_limit_exceededwithRetry-After. max_tokens_cap: larger requests are clamped; see headerx-models-max-tokens-clamped.- Model allowlist: model ids and/or aliases.
- Payload logging (
log_payloads): metadata is always logged; request and response bodies are stored AES-256-GCM encrypted (≤ 64 KB each, 30-day retention) only when on. Off for eam, photomemory and whisper. - Global monthly ceiling (Settings) blocks every non-exempt key with
402 global_ceiling_reached.
Cost object
Every response carries cost: top-level in chat bodies, in the final chunk when streaming, and in message_delta for the Messages stream.
{
"request_id": "…", "model": "bedrock/us.amazon.nova-lite-v1:0", "provider": "bedrock", "alias": "fast",
"currency": "USD", "total_usd": 0.00000204, "input_usd": 3.6e-7, "output_usd": 1.68e-6,
"pricing_tier": "standard",
"usage": { "input_tokens": 6, "output_tokens": 7, "cache_read_tokens": 0, "cache_write_tokens": 0,
"audio_seconds": 0, "characters": 0 },
"budget": { "period": "monthly", "period_start": "…", "limit_usd": 20,
"spent_usd": 0.0000204, "remaining_usd": 19.99998 }
}
| Header | Value |
|---|---|
x-request-id | Correlates with the request log |
x-models-cost-usd | Total cost of this call |
x-models-budget-remaining-usd | Remaining budget of the key |
x-models-model, x-models-provider | What actually served the call |
x-models-pod | Gateway replica |
estimated: true means the provider did not report usage (for example an aborted stream) and tokens were estimated.
Usage API
GET /v1/usage returns the calling key's budget, limits and recent calls.
curl -s https://models.thundera.eu/v1/usage -H "Authorization: Bearer $MODELS_API_KEY"
Errors
Errors use the OpenAI shape, plus type: "error" at the top level for Anthropic clients.
{"type":"error","error":{"message":"…","type":"…","code":"budget_exceeded","request_id":"…"}}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request_error | Bad body |
| 401 | missing_api_key invalid_api_key key_inactive key_expired | Authentication |
| 402 | budget_exceeded end_user_budget_exceeded global_ceiling_reached | Money guardrails |
| 400 | use_systemone | A decision model (decision, jev/…) was sent to a chat endpoint |
| 403 | model_not_allowed feature_not_allowed | Not in the key's allowlist / key lacks the opt-in (decision images, long context) |
| 422 | validation_error | /v1/systemone body invalid (detail[] lists the fields) |
| 404 | model_not_found | Unknown id |
| 413 | request_too_large | Body > 20 MB (audio 25 MB) |
| 429 | rate_limit_exceeded upstream_rate_limited | Yours, or the provider's: retry later |
| 502 / 503 / 504 | upstream_error model_unavailable upstream_timeout | Provider failures after failover |
CloudFront limits vs in-cluster
External callers pass through CloudFront. It forwards only Authorization, Accept, Origin and Host, and closes an origin response that is silent for 60 s. Streaming sends keepalives every 15 s; long non-streaming calls get a committed 200 with JSON-legal leading whitespace after 20 s.
In-cluster callers have none of these limits and every header (such as X-Thundera-User) passes. Prefer the in-cluster URL from Thundera apps.
App onboarding
- Ask an admin to create a key for your app in /system (budget, limits, allowlist, payload logging).
- The key is stored in SSM
/thundera/<app>/models-api-keyand synced to the Kubernetes Secret<app>-models, keyMODELS_API_KEY. - Set the base URL and read the secret in your Deployment:
env:
- name: MODELS_BASE_URL
value: http://models.thundera.svc.cluster.local/v1
- name: MODELS_API_KEY
valueFrom:
secretKeyRef: { name: <app>-models, key: MODELS_API_KEY }
- Allow egress to the gateway in your NetworkPolicy:
egress:
- to:
- podSelector: { matchLabels: { app: models } }
ports:
- { protocol: TCP, port: 8080 }
Use aliases (fast, smart, …) rather than vendor ids, and pass X-Thundera-User for per-user budgets.