One LLM API in Python and Node - Switch Models in One Line
Call GPT-6, Claude, Gemini and DeepSeek with one key from Python and Node. Switch models in one line, add fallback on errors and compare answers with cost.
One client and one key are enough for GPT, Claude, Gemini and DeepSeek: the official OpenAI SDK pointed at https://jantaris.com/v1 reaches all 22 chat models in the Jantaris catalog, and you switch models by changing one string, for example from gpt-6-sol to claude-sonnet-5 or deepseek-v4.1-flash. Everything bills to one prepaid balance. Below is working Python and Node code for three things: switching the model from config, falling back when a model fails, and comparing several models' answers with latency and cost.
If you only need the basics of calling non-OpenAI models through the OpenAI SDK, start with Call Claude, Gemini, DeepSeek and GPT with the OpenAI SDK. This guide builds on it.
Why one API instead of four SDKs
Every vendor ships its own SDK, message format, error classes and billing account. Supporting four vendors means four code paths, four keys and four invoices.
Jantaris exposes one OpenAI-format endpoint (/v1/chat/completions) for every chat model, plus an Anthropic-format endpoint (/v1/messages) for Claude. To your code, the model becomes a string in config. To your finance team, it is one balance and one set of top-ups.
| Model (examples) | Id | Price per 1M tokens (input / output) |
|---|---|---|
| GPT-6 Sol | gpt-6-sol | $2.16 / $10.80 per 1M tokens |
| Claude Sonnet 5 | claude-sonnet-5 | $3.24 / $16.20 per 1M tokens |
| gemini-3.8-flash | gemini-3.8-flash | |
| DeepSeek V4.1 Flash | deepseek-v4.1-flash | $0.54 / $1.75 per 1M tokens |
| GPT-6 Luna | gpt-6-luna | $0.108 / $0.54 per 1M tokens |
The full list, including GLM, Kimi, Qwen, MiniMax and Grok, is in the model catalog and at GET /v1/models?category=chat.
Step 1: key and install
- Sign up and top up (from $10).
- In the dashboard, create an API key and give it a monthly limit. The
sk-jt-...key is shown once. - Keep the key in an environment variable, not in code.

pip install openai requests
export JANTARIS_API_KEY=sk-jt-...
export LLM_MODEL=gpt-6-solSwitch models with one line
The simplest version reads the model from an environment variable, so you switch it without touching code.
import os
from openai import OpenAI
client = OpenAI(base_url="https://jantaris.com/v1", api_key=os.environ["JANTARIS_API_KEY"])
MODEL = os.environ.get("LLM_MODEL", "gpt-6-sol")
def ask(prompt: str, model: str = MODEL) -> str:
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "Be concise."},
{"role": "user", "content": prompt},
],
)
return resp.choices[0].message.content
print(ask("What is the difference between PUT and PATCH in REST?"))
print(ask("What is the difference between PUT and PATCH in REST?", model="claude-sonnet-5"))Run LLM_MODEL=deepseek-v4.1-flash python app.py and the same program runs on DeepSeek. Streaming (stream=True) and tool calls (tools) use the same format for every model, see the chat completions docs.
Compare answers from several models, with cost
Before I pick a model for a project, I send the same prompts to several models in parallel and look at three things: answer quality, latency and cost. Prices come from the public GET /v1/models, and usage from each response's usage field.
import os, time
from concurrent.futures import ThreadPoolExecutor
import requests
from openai import OpenAI
BASE = "https://jantaris.com/v1"
client = OpenAI(base_url=BASE, api_key=os.environ["JANTARIS_API_KEY"])
catalog = requests.get(f"{BASE}/models", params={"category": "chat", "currency": "USD"}, timeout=30).json()["data"]
PRICES = {m["id"]: m["pricing"] for m in catalog}
MODELS = ["gpt-6-sol", "claude-sonnet-5", "gemini-3.8-flash", "deepseek-v4.1-flash"]
PROMPT = "A customer writes: 'My parcel has not arrived for 6 days'. Write a polite 3-sentence reply."
def run(model):
t0 = time.perf_counter()
r = client.chat.completions.create(model=model, messages=[{"role": "user", "content": PROMPT}])
u, p = r.usage, PRICES[model]
cost = (u.prompt_tokens * float(p["input_per_mtok"]) + u.completion_tokens * float(p["output_per_mtok"])) / 1_000_000
return model, time.perf_counter() - t0, u.prompt_tokens, u.completion_tokens, cost, r.choices[0].message.content
with ThreadPoolExecutor(max_workers=len(MODELS)) as pool:
results = list(pool.map(run, MODELS))
for model, sec, tin, tout, cost, text in sorted(results, key=lambda x: x[4]):
print(f"\n=== {model} | {sec:.1f}s | {tin} + {tout} tokens | ${cost:.5f}")
print(text)The output is sorted from the cheapest answer. A few notes from practice:
- one prompt proves nothing, so for a decision I use 20 to 50 real examples from the project,
- output tokens on reasoning models include thinking, so a short answer can carry a lot of tokens,
- the same text is a different number of tokens in different models, which is why I compare the cost of whole requests, not rates.
For a ranking of the rates themselves on 1,000 chat replies, see Cheapest LLM API in 2026.
Fallback when a model fails
Every vendor has bad days. Part of the Jantaris catalog already fails over between infrastructure providers, but a second layer in your code helps: switching to a different model. The key is not to switch on errors a different model will not fix.
import time
from openai import APIStatusError, APIConnectionError
CHAIN = ["claude-sonnet-5", "gpt-6-sol", "gemini-3.8-flash"]
NO_RETRY = {401, 402, 403, 422} # key, balance, key limit, bad parameters
def ask_with_fallback(messages, **kw):
last = None
for model in CHAIN:
for attempt in range(2):
try:
r = client.chat.completions.create(model=model, messages=messages, **kw)
return model, r.choices[0].message.content
except APIStatusError as e:
if e.status_code in NO_RETRY:
raise
last = e
if e.status_code == 404:
break # unknown model id - go straight to the next one
time.sleep(2 * (attempt + 1)) # 429 and 5xx: short pause, then retry
except APIConnectionError as e:
last = e
time.sleep(2 * (attempt + 1))
raise last
model, answer = ask_with_fallback([{"role": "user", "content": "Summarize GDPR in 5 bullet points."}])
print(f"[{model}] {answer}")What each status code means here:
| Code | Meaning | Switch models? |
|---|---|---|
| 401 | invalid or deleted key | no |
| 402 | balance too low | no, top up |
| 403 | key reached its monthly limit | no, raise the limit in the dashboard |
| 404 | unknown model | yes, and fix the id |
| 422 | invalid parameters | no, fix the request |
| 429 | rate limited | yes, after a short pause |
| 502 | upstream provider error | yes |
Failed requests are not charged, so a retry after an error does not cost twice. Full list: Errors.
The same in Node.js
npm install openaiimport OpenAI from "openai";
const BASE = "https://jantaris.com/v1";
const client = new OpenAI({ baseURL: BASE, apiKey: process.env.JANTARIS_API_KEY });
const res = await fetch(`${BASE}/models?category=chat¤cy=USD`);
const prices = Object.fromEntries((await res.json()).data.map((m) => [m.id, m.pricing]));
const models = ["gpt-6-luna", "claude-haiku-4.5", "deepseek-v4.1-flash"];
const prompt = "Suggest 3 names for a coffee shop next to a train station.";
const results = await Promise.allSettled(
models.map(async (model) => {
const r = await client.chat.completions.create({ model, messages: [{ role: "user", content: prompt }] });
const p = prices[model];
const cost = (r.usage.prompt_tokens * Number(p.input_per_mtok) + r.usage.completion_tokens * Number(p.output_per_mtok)) / 1e6;
return { model, cost: "$" + cost.toFixed(5), text: r.choices[0].message.content };
}),
);
for (const r of results) console.log(r.status === "fulfilled" ? r.value : r.reason.message);Promise.allSettled keeps one failing model from aborting the whole comparison. Note the option name: baseURL in Node, base_url in Python.
When to stay on the Anthropic SDK
If your code is written for the Anthropic SDK (with tool_use blocks or prompt caching, say), you do not have to rewrite it. The /v1/messages endpoint serves Claude in its native format:
from anthropic import Anthropic
claude = Anthropic(base_url="https://jantaris.com", api_key=os.environ["JANTARIS_API_KEY"])
msg = claude.messages.create(
model="claude-opus-5.5",
max_tokens=1024,
messages=[{"role": "user", "content": "Propose a table schema for a meeting-room booking system."}],
)
print(msg.content[-1].text)Give the Anthropic SDK the base URL without /v1. This endpoint serves Claude models only. When one app mixes Claude with GPT and Gemini, keeping everything in the OpenAI format is simpler. Claude costs and caching are covered in Claude Opus 5.5 API pricing.
Pitfalls when switching models
The format is shared, but the models are not identical. These are the ones I hit most:
temperatureandtop_p. Claude Sonnet 5 rejects non-default values with a 400 error, and OpenAI says GPT-6 Astra does not accept them either. If a request must run on any model, leave these parameters out.max_tokenson reasoning models. Thinking uses part of the output budget, so a limit that is too low ends in a truncated or empty answer. Leave it unset at first, or give it headroom.- Output and context limits. Gemini 3.6 Flash is capped at 8,192 output tokens in Jantaris, and Claude Haiku 4.5 has a 200K context instead of 1M. Check
context_windowandmax_output_tokensinGET /v1/models. - Answer style. The same system prompt produces different lengths and tone across models. After a switch, read a dozen answers before it goes to production.

If you prefer no-code tools, the same key works in n8n, Make and Zapier with the base URL https://jantaris.com/v1, see the no-code docs. For GPT-6 tiers and prices, see GPT-6 API pricing.
FAQ
Can I call Claude and Gemini with the OpenAI SDK?
Yes. Set base_url="https://jantaris.com/v1", use your sk-jt-... key and a model id such as claude-sonnet-5 or gemini-3.8-flash. Requests and responses use the standard Chat Completions format, including streaming and tools.
How do I switch LLMs without changing code?
Keep the model id in an environment variable or config file and pass it to model. Moving from gpt-6-sol to deepseek-v4.1-flash is then a one-value change.
How do I add fallback between LLMs?
Catch 429, 5xx and connection errors, then try the next model in a list. Do not switch on 401, 402, 403 or 422, since those are key, balance, limit or parameter problems. The code is above.
How much does comparing several models cost?
The sum of tokens across all requests at each model's rates. The script in this guide computes each answer's cost from usage and the prices in GET /v1/models.
Do I need separate OpenAI, Anthropic and Google accounts?
No. One Jantaris key and one prepaid balance cover every model in the catalog, including image, video and music models.












