UplinkSwitch

Remote MCP server for the UplinkSwitch model API: list_models and list_media_models with live per-token prices, estimate a request's cost, read the daily model-identity check (proof), and chat on any of 470+ models with a prepaid key. Streamable HTTP, stateless; list, estimate and proof need no key.

Hosted MCP Server

npx add-mcp 'https://api.uplinkswitch.com/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

›uplinkswitch

api.uplinkswitch.com/v1Console

›uplinkswitch

OpenAI, Anthropic and Gemini SDKs

A base URL and your key. The rest of your code stays.

Checked 2026-10-21

·All guides·Models

At the end of this page your existing OpenAI SDK, Anthropic SDK or Gemini SDK code runs unchanged, with its base URL set to https://api.uplinkswitch.com/v1 and one key in place of three. The OpenAI SDK reads OPENAI_BASE_URL, the Anthropic SDK reads ANTHROPIC_BASE_URL, and the Gemini SDK takes the base URL in a constructor option. Every model on the model list is callable from the same key, and each request's charge lands on your prepaid balance with a signed receipt in the response headers. The same two values are what the Claude Code and Cursor guides set, so one key covers your code and your editor.

Environment variables

Both SDKs read these, so no code changes.

shell

# OpenAI SDK (Python and TypeScript read these)
export OPENAI_BASE_URL="https://api.uplinkswitch.com/v1"
export OPENAI_API_KEY="sk-uplink-live-..."

# Anthropic SDK (Python and TypeScript read these)
export ANTHROPIC_BASE_URL="https://api.uplinkswitch.com/v1"
export ANTHROPIC_API_KEY="sk-uplink-live-..."

The Python and TypeScript clients of each SDK read the same two names, so a service that already runs on OPENAI_API_KEY switches with one line in its environment. Keys begin with sk-uplink-live-; the key page in the console shows the prefix after creation and the full key once. Neither SDK reads a header from the environment, so the HTTP-Referer and X-Title headers in the examples below, which name the app against each request in our log, go in the constructor.

TypeScript

The constructor takes the same two values when you would rather not use environment variables. openai on npm, any version that has chat.completions.create:

openai.ts

import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://api.uplinkswitch.com/v1',
  apiKey: process.env.UPLINKSWITCH_API_KEY,
  defaultHeaders: {
    'HTTP-Referer': 'https://uplinkswitch.ai',
    'X-Title': 'your app name',
  },
});

const completion = await client.chat.completions.create({
  model: 'gpt-6.1-sol',
  messages: [{ role: 'user', content: 'Say ok.' }],
});
console.log(completion.choices[0].message.content);

With OPENAI_BASE_URL and OPENAI_API_KEY exported, new OpenAI() with no arguments reads them and the base URL and key lines go away.

The Anthropic SDK, @anthropic-ai/sdk on npm, sends to /v1/messages and needs max_tokens on every call:

anthropic.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  baseURL: 'https://api.uplinkswitch.com/v1',
  apiKey: process.env.UPLINKSWITCH_API_KEY,
  defaultHeaders: {
    'HTTP-Referer': 'https://uplinkswitch.ai',
    'X-Title': 'your app name',
  },
});

const message = await client.messages.create({
  model: 'claude-sonnet-5-5',
  max_tokens: 256,
  messages: [{ role: 'user', content: 'Say ok.' }],
});
const first = message.content[0];
console.log(first.type === 'text' ? first.text : first.type);

ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY work there too, so new Anthropic() with no arguments is enough once the shell block above is in place. The Messages format answers only Claude ids; the section on formats below says what the other ids return.

Gemini SDK

The base URL goes in http_options.

gemini.py

from google import genai

client = genai.Client(
    api_key="sk-uplink-live-...",
    http_options={
        "base_url": "https://api.uplinkswitch.com/v1",
        "headers": {
            "HTTP-Referer": "https://uplinkswitch.ai",
            "X-Title": "your app name",
        },
    },
)

The client then sends Google's format under /v1beta and the Gemini ids answer it. The same ids also answer the OpenAI format, so a project that already uses the OpenAI SDK can call gemini-3.1-pro-preview without adding a second client.

Vercel AI SDK

The provider package sets the base URL and reads your key from UPLINKSWITCH_API_KEY. Any model id from the Models page works, with generateText and streamText alike. Source: github.com/uplinkswitch.

shell

npm install ai @uplinkswitch/ai-sdk-provider

ai-sdk.ts

import { createUplinkSwitch } from '@uplinkswitch/ai-sdk-provider';
import { generateText } from 'ai';

const uplinkswitch = createUplinkSwitch({
  // reads UPLINKSWITCH_API_KEY
  headers: {
    'HTTP-Referer': 'https://uplinkswitch.ai',
    'X-Title': 'Vercel AI SDK',
  },
});

const { text } = await generateText({
  model: uplinkswitch('gpt-6.1-sol'),
  prompt: 'Explain what an API gateway does in one sentence.',
});

Without the package, @ai-sdk/openai-compatible works the same way with the base URL set by hand:

openai-compatible.ts

import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { generateText } from 'ai';

const uplinkswitch = createOpenAICompatible({
  name: 'uplinkswitch',
  apiKey: process.env.UPLINKSWITCH_API_KEY,
  baseURL: 'https://api.uplinkswitch.com/v1',
  headers: {
    'HTTP-Referer': 'https://uplinkswitch.ai',
    'X-Title': 'Vercel AI SDK',
  },
});

const { text } = await generateText({
  model: uplinkswitch('gpt-6.1-sol'),
  prompt: 'Explain what an API gateway does in one sentence.',
});

Either way the Claude ids go through the same provider, since they answer the OpenAI chat completions format; there is no separate Anthropic provider to configure for them.

Check it

First call: the model list. It returns the ids this key can call, which is the whole catalog unless the key carries an allowlist, plus any aliases your org defined, each with an alias_of field naming the real id.

shell

curl https://api.uplinkswitch.com/v1/models -H "Authorization: Bearer $UPLINKSWITCH_API_KEY"

Then one chat completion with -i, so the response headers print above the body:

shell

curl -i https://api.uplinkswitch.com/v1/chat/completions \
  -H "Authorization: Bearer $UPLINKSWITCH_API_KEY" \
  -H "content-type: application/json" \
  -H "HTTP-Referer: https://uplinkswitch.ai" \
  -H "X-Title: your app name" \
  -d '{"model": "claude-haiku-5-5", "messages": [{"role": "user", "content": "Say ok."}]}'

Three headers matter. x-uplink-request-id is the id that looks the request up afterwards. x-uplink-charge is the exact dollar amount taken from your balance, on the order of $0.0002 for a 2,000-in, 500-out turn on Claude Haiku 5.5. x-uplink-receipt is a signed statement of which model answered, the billed token counts, the charge and SHA-256 hashes of the request and the response.

Any request's receipt is also served on its own route, and the signature checks on the verifier page without a key:

shell

curl https://api.uplinkswitch.com/v1/requests/REQUEST_ID/receipt \
  -H "Authorization: Bearer $UPLINKSWITCH_API_KEY"

A streamed response carries the request id but not the charge or the receipt. Its token counts and charge are at GET /v1/requests/{id} about a minute after the stream ends, and the receipt route works for it from then on.

What a request costs

Prices are per million tokens, live from the catalog. The last column is one chat turn of 2,000 input tokens and 500 output tokens, a short system prompt plus a question in and a paragraph out, with nothing counted at the cached rate.

ModelInput / 1MOutput / 1MList inputList outputUnder list (in / out)Turn, 2,000 in + 500 out
Claude Sonnet 5.5$1.20$3.00$2.00$10.0040% / 70%$0.0039
Claude Opus 5.5$2.80$10.00$4.00$20.0030% / 50%$0.0106
Claude Haiku 5.5$0.06$0.15$0.10$0.5040% / 70%$0.0002
GPT-6.1 Sol$0.60$3.00$2.00$10.0070% / 70%$0.0027
GPT-6 Luna$0.0625$0.20$0.10$0.5037% / 60%$0.0002
Gemini 3.1 Pro Preview$1.20$7.20$2.00$12.0040% / 40%$0.006
DeepSeek V4 Pro 0813$0.792$1.188$1.32$3.9640% / 70%$0.0022
Kimi K3$0.90$4.50$3.00$15.0070% / 70%$0.0041

USD per million tokens, live from GET /v1/catalog. List: the price on the developer’s own page. A dash means the catalog has no list price on record for that model.

A longer context changes the input side only: a 20,000-token prompt costs ten times the input figure above and the same output figure. Cached input, when your SDK sends the same prefix twice, bills at each model's cached rate, shown on its model page. The LLM API pricing comparison carries the full list next to each developer's page, and the compare pages put two models side by side.

The real-model check today, one test prompt per model with the token counts public: Claude Sonnet 5.5, steady since Oct 12; Claude Opus 5.5, steady since Oct 29; Claude Haiku 5.5, steady since Oct 19; GPT-6.1 Sol, steady since Oct 19; GPT-6 Luna, steady since Oct 19; Gemini 3.1 Pro Preview, steady since Nov 1; DeepSeek V4 Pro 0813, changed Nov 3; Kimi K3, steady since Oct 12. Each verdict is the live one from the proof page, not a typed claim.

Tool calling, JSON and streaming

Tools go out in OpenAI's tools schema and calls come back as tool events, with arguments streaming incrementally, on every model in the table, so an agent loop written against the OpenAI SDK runs on a Claude, Gemini or DeepSeek id without a branch.

A request that asks for JSON or a schema, through response_format or the header x-uplink-require: json, pays only for an answer that matches. A miss is retried once and the missed answer is free, so a parse failure never reaches your balance. This holds for non-streamed requests; a stream is billed as sent.

Streams pass through in each format's own SSE events: OpenAI chunks on /v1/chat/completions, Anthropic events on /v1/messages, Google's on /v1beta.

stream.py

from openai import OpenAI

client = OpenAI(  # reads OPENAI_BASE_URL and OPENAI_API_KEY
    default_headers={
        "HTTP-Referer": "https://uplinkswitch.ai",
        "X-Title": "your app name",
    },
)
with client.chat.completions.stream(
    model="gpt-6.1-sol",
    messages=[{"role": "user", "content": "Count to five."}],
) as s:
    for event in s:
        if event.type == "content.delta":
            print(event.delta, end="")

Which format for which model

Every model takes the OpenAI format. The Claude models also take Anthropic's, and the Gemini models Google's. Each model on the Models page lists its own; the table's models, live from the catalog:

ModelFormats
Claude Sonnet 5.5anthropic, openai
Claude Opus 5.5anthropic, openai
Claude Haiku 5.5openai, anthropic
GPT-6.1 Solopenai
GPT-6 Lunaopenai
Gemini 3.1 Pro Previewopenai
DeepSeek V4 Pro 0813openai
Kimi K3openai

wire_formats from GET /v1/catalog. openai: /v1/chat/completions. anthropic: /v1/messages. gemini: Google’s format under /v1beta.

A non-Claude id sent to /v1/messages returns a 400 whose message says the model is served in the openai format; switch that call to the OpenAI SDK and keep the id. Gemini ids take Google's format under /v1beta from the Gemini SDK and the OpenAI format from everything else.

Keys, limits and errors

  • The key goes in Authorization: Bearer or in x-api-key; a key in the URL is refused, and a request carrying two different keys returns 400.
  • Keys carry their environment in the prefix, sk-uplink-live- or sk-uplink-test-, so a glance at a log line says which one a request used.
  • Every key has a requests-per-minute limit and a concurrency limit, and can carry a tokens-per-minute limit; over any of them the answer is 429 rate_limited with a retry-after header that says how long to wait.
  • A per-key monthly spend limit, set in the console, returns 402 monthly_limit_reached once the key's settled charges and open holds reach it; the counter restarts on the first of the UTC month.
  • A key can carry its own budget: a hard cap in dollars, taken from your balance, that does not reset. Once the key's charges and open holds reach it the answer is 402 key_budget_exhausted until the budget is raised. The next section shows how to hand such keys out by API.
  • A key narrowed to a model allowlist returns 403 scope_denied for any other id, and the ids it may call are exactly what GET /v1/models lists for it.
  • An admin key, the one that manages keys, gets 403 admin_key on every model route. It is for /v1/keys only.
  • 401 invalid_api_key: the key is wrong, expired or revoked. Create a new one in the console; the Claude API key post walks the steps.
  • 404 model_not_found: the id is not in the catalog. Copy it from GET /v1/models; ids are the developer's own, with no prefix.
  • 402 insufficient_balance: the prepaid balance is at zero. Top up in the console and the same request goes through. Nothing here is free except the catalog and the estimate route; the free AI API post lists what is.
  • Errors come back in the caller's wire format, so the OpenAI SDK raises its usual APIStatusError with the code in the body and the Anthropic SDK its own, and every error response still carries x-uplink-request-id.

Keys with their own budget: the key API

If you resell access, run bots for clients or hand keys to a team, give each key a budget instead of sharing your balance. A budget is a hard cap for that key alone, carved out of your prepaid balance: the key stops at its budget, the balance is still what is debited, and refunds and receipts work as for any other request. Set it in the console when you create a key, or do the whole thing by API. Reselling access needs Uplink Switch LLC's written authorization first: email hello@uplinkswitch.com to ask.

The key API needs an admin key. Create one in the console (Keys, then tick Admin key): it manages keys through GET, POST, PATCH and DELETE /v1/keys and cannot call models, so a leaked admin key spends nothing. It cannot create another admin key either; only an owner in the console can.

Create a key with a $5 budget

# $ADMIN_KEY is an admin key, created once in the console (Keys, "Admin key").
curl https://api.uplinkswitch.com/v1/keys \
  -H "Authorization: Bearer $ADMIN_KEY" \
  -H "content-type: application/json" \
  -d '{"name": "customer-7", "budget_usd": 5, "rpm": 60, "allowed_models": ["claude-haiku-5-5"]}'

# 201: the full key, once, with its record.
# {"key": "sk-uplink-live-...", "data": {"id": "k7m2...", "budget_micro": 5000000,
#   "budget_spent_micro": 0, "budget_remaining_micro": 5000000, "scope": "api", ...}}

Usage, changes, revocation

# What the key has used today, this month and in all, and what is left of its budget.
curl https://api.uplinkswitch.com/v1/keys/KEY_ID/usage -H "Authorization: Bearer $ADMIN_KEY"

# Raise the budget (never under what is spent), narrow the models, pause or resume.
curl -X PATCH https://api.uplinkswitch.com/v1/keys/KEY_ID \
  -H "Authorization: Bearer $ADMIN_KEY" -H "content-type: application/json" \
  -d '{"budget_usd": 10, "status": "active"}'

# Revoke for good.
curl -X DELETE https://api.uplinkswitch.com/v1/keys/KEY_ID -H "Authorization: Bearer $ADMIN_KEY"

POST /v1/keys takes name, and optionally allowed_models, rpm, tpm, monthly_cap_usd, budget_usd and expires_at (Unix milliseconds). PATCH takes the same fields plus status; a budget may go up or down but never under what the key has already spent, and null removes it. GET /v1/keys/:id/usage returns requests and spend for today, this month and lifetime, and the budget with what is left. The full shapes are in the OpenAPI document under the keys tag.

API reference

Every route, its request and response shapes and the authentication headers are in the OpenAPI 3.1 document at api.uplinkswitch.com/openapi.json. Model ids and prices come from GET /v1/catalog, which needs no key.

Get a key in the console: prepaid credit, no subscription, 30-day price lock.

Questions

Create it in the console under Keys. The full key is shown once at creation, so copy it then; afterwards the console shows only the prefix and the spend on it. Send it as Authorization: Bearer sk-uplink-live-... or as an x-api-key header; the SDKs do that for you once the environment variable is set.

No. Every request bills per token from prepaid credit, and the price table on this page is the rate per million tokens; a 2,000-in, 500-out turn on Claude Sonnet 5.5 is $0.0039. What needs no key: GET /v1/catalog for prices and POST /v1/estimate for a cost before you send. The free AI API post lists what is free and what is not.

No. Credit is prepaid, there is no monthly fee, and the per-token price is locked for 30 days from each top-up. The same key carries a monthly spend limit you set, so a loop that runs away stops at 402 monthly_limit_reached rather than at your card limit.

No. A non-Claude id sent to /v1/messages returns a 400 saying the model is served in the openai format. Point the OpenAI SDK at the same base URL for GPT, Gemini, DeepSeek, Kimi and the rest; the Claude ids answer both formats, so one OpenAI client can cover the whole list.

Yes. The @uplinkswitch/ai-sdk-provider package speaks the OpenAI chat completions format, and every Claude id answers it, so uplinkswitch("claude-sonnet-5-5") works with generateText, streamText and tool calls the same as a GPT id. Without the package, @ai-sdk/openai-compatible with the base URL set by hand does the same.

No. Two values change: the base URL becomes https://api.uplinkswitch.com/v1 and the key becomes your UplinkSwitch key. Model ids are the developer ids, claude-sonnet-5-5, gpt-6.1-sol, gemini-3.1-pro-preview, and requests and responses keep each SDK’s own shape, including streaming and tool calls.

The OpenAPI 3.1 document at api.uplinkswitch.com/openapi.json lists every route, its request and response shapes, the error codes and the authentication headers. GET /v1/catalog returns every model with its price, list price, wire formats, context window and maximum output, with no key.

Related