PL Log inOpen Studio
Preview version. Model catalog, prices, calculator and docs are live. Generation goes live at launch.

Swap GPT, Claude, Gemini and DeepSeek by Changing One String (Python and Node)

Switch LLM models in Python and Node with one key: GPT-6, Claude, Gemini and DeepSeek via the OpenAI SDK, with error fallback and cost comparison.

You do not need four SDKs to use four model families. Point the official OpenAI SDK at https://kadraris.com/v1 and every chat model in the Kadraris catalog answers to the same client. Changing models means changing one string, say from gpt-6-sol to claude-sonnet-5 or deepseek-v4-flash, and all usage lands on one prepaid balance.

This guide has working Python and Node code for three jobs: picking the model from config, falling back to another model on errors, and comparing several models' answers with their speed and cost. For the very basics of calling non-OpenAI models with the OpenAI SDK, read the OpenAI SDK with any model first.

Setup in three steps

  1. Create an account and add funds (from $10).
  2. Make an API key in the dashboard and set a monthly limit for it. The sk-kd-... key is shown only once.
  3. Store it in an environment variable, never in source code.
API keys screen in the Kadraris dashboard with a name and a monthly limit
One key for all models; a monthly cap stops a runaway script
bash
pip install openai requests
export KADRARIS_API_KEY=sk-kd-...
export LLM_MODEL=gpt-6-sol

The model as a config value

The smallest useful version reads the model id from the environment, so switching requires no code change.

python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://kadraris.com/v1",
    api_key=os.environ["KADRARIS_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

q = "PUT vs PATCH in REST: what is the difference?"
print(ask(q))
print(ask(q, model="claude-sonnet-5"))

Start it with LLM_MODEL=deepseek-v4-flash python app.py and the same program talks to DeepSeek. Streaming (stream=True) and tool calls (tools) use one format across models; see the chat completions docs.

What sits behind the single endpoint

Each producer normally brings its own SDK, message shape, error types and billing account. Four producers mean four code paths, four keys and four invoices.

Kadraris API gives you one OpenAI-style endpoint, /v1/chat/completions, for all chat models, and an Anthropic-style /v1/messages for Claude. In code, the model is a config string. On the money side, there is one balance.

A few examples with their rates per 1M tokens (input / output):

GLM, Kimi, MiniMax and the others are in the model catalog, on the LLM API page and at GET /v1/models?category=chat.

Falling back to another model

Any producer can have a bad day. Some catalog models already switch between infrastructure providers on their own, but a second safety net in your code helps: move to a different model. The trick is to skip the fallback for errors that a different model cannot fix.

python
import time
from openai import APIStatusError, APIConnectionError

CHAIN = ["claude-sonnet-5", "gpt-6-sol",
         "gemini-3.6-flash"]
# key, balance, key limit, bad input: stop here
NO_RETRY = {401, 402, 403, 422}

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  # bad model id, try the next
                # 429 or 5xx: wait a bit, retry
                time.sleep(2 * (attempt + 1))
            except APIConnectionError as e:
                last = e
                time.sleep(2 * (attempt + 1))
    raise last

msgs = [{"role": "user",
         "content": "GDPR in 5 bullet points."}]
model, answer = ask_with_fallback(msgs)
print(f"[{model}] {answer}")

How the code treats each status:

CodeCauseTry another model?
401key wrong or deletedno
402not enough balanceno, add funds
403monthly key limit reachedno, raise it in the dashboard
404model id not foundyes, and correct the id
422invalid parametersno, fix the request
429too many requestsyes, after a pause
502provider error upstreamyes

A failed request is not billed, so retrying after an error does not charge you twice. All codes: Errors.

Side-by-side test with cost per answer

Before committing a project to a model, I send identical prompts to several models at once and compare quality, response time and cost. Rates come from the public GET /v1/models, token counts from each response's usage.

python
import os, time
from concurrent.futures import ThreadPoolExecutor

import requests
from openai import OpenAI

BASE = "https://kadraris.com/v1"
client = OpenAI(base_url=BASE,
                api_key=os.environ["KADRARIS_API_KEY"])

# public catalog, no key needed
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.6-flash", "deepseek-v4-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
    secs = time.perf_counter() - t0
    text = r.choices[0].message.content
    return (model, secs, u.prompt_tokens,
            u.completion_tokens, cost, text)

with ThreadPoolExecutor(len(MODELS)) as pool:
    results = list(pool.map(run, MODELS))

# cheapest answer first
for m, s, tin, tout, c, text in sorted(
        results, key=lambda x: x[4]):
    print(f"\n=== {m} | {s:.1f}s | "
          f"{tin}+{tout} tok | ${c:.5f}")
    print(text)

Lessons from using this in real projects:

  • A single prompt decides nothing. I run 20 to 50 real examples before choosing.
  • Reasoning models count their thinking as output, so a short reply can still be token-heavy.
  • Models split the same text into different numbers of tokens, so compare the cost of whole requests rather than the rates.

For a ranking based on 1,000 support replies, see the cheapest LLM API in 2026.

Node.js version

bash
npm install openai
javascript
import OpenAI from "openai";

const BASE = "https://kadraris.com/v1";
const client = new OpenAI({
  baseURL: BASE,
  apiKey: process.env.KADRARIS_API_KEY,
});

const res = await fetch(
  `${BASE}/models?category=chat&currency=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-flash"];
const prompt = "3 names for a cafe by 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 u = r.usage;
    const cost = (u.prompt_tokens * Number(p.input_per_mtok)
      + u.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);
}

With Promise.allSettled, one failing model does not kill the whole comparison. Mind the option name: baseURL in Node, base_url in Python.

Keeping Anthropic SDK code as it is

Code written for the Anthropic SDK, with tool_use blocks or prompt caching for example, can stay. /v1/messages serves Claude in its own format:

python
from anthropic import Anthropic

claude = Anthropic(
    base_url="https://kadraris.com",
    api_key=os.environ["KADRARIS_API_KEY"],
)
msg = claude.messages.create(
    model="claude-opus-5.5",
    max_tokens=1024,
    messages=[{"role": "user",
               "content": "Table schema for booking "
                          "meeting rooms?"}],
)
print(msg.content[-1].text)

The Anthropic SDK gets the base URL without /v1. This endpoint is for Claude only. If one app mixes Claude with GPT and Gemini, sticking to the OpenAI format everywhere is easier. Claude costs and caching: Claude Opus 5.5 API pricing. Endpoint details: Anthropic Messages docs.

Differences that bite after a switch

The request format is shared; the models are not. The ones I run into most:

  • Sampling parameters. Claude Sonnet 5 returns a 400 error for non-default temperature or top_p, and OpenAI says GPT-6 Astra does not take them either. If a request has to work on any model, drop both.
  • Output budget on reasoning models. Thinking eats into max_tokens. Set it too low and the answer comes back cut off or empty. Leave it unset at first, or give plenty of room.
  • Limits. On Kadraris, Gemini 3.6 Flash can write at most 8,192 output tokens, and Claude Haiku 4.5 has a 200K context rather than 1M. GET /v1/models lists context_window and max_output_tokens.
  • Tone and length. One system prompt gives different styles on different models. Read a dozen answers before you ship a switch.
Kadraris quickstart page with base URL and a first chat request
Quickstart in the docs: base URL, key, first request

No-code tools work too: n8n, Make and Zapier accept the same key with base URL https://kadraris.com/v1, see the no-code docs. GPT-6 tiers and prices: GPT-6 API pricing.

Questions

Does streaming work the same way for Claude and Gemini through this endpoint?

Yes. stream=True uses the standard Chat Completions chunk format for every model, so one streaming handler covers them all.

Which errors should never trigger a fallback?

401, 402, 403 and 422. They point to the key, the balance, the key's limit or the request itself, and another model would fail the same way.

How do I find the context size of each model?

Call GET /v1/models?category=chat. Each entry has context_window and max_output_tokens, and the endpoint is public.

Do I need separate accounts with OpenAI, Anthropic and Google?

No. One Kadraris key and one balance reach every model in the catalog, including image, video and music models.