Vercel AI SDK with a custom endpoint

The AI SDK ships a default OpenAI provider that is hard-wired to OpenAI's URL. It also ships the factory that builds the same provider around any OpenAI-compatible base URL (one extra import), and every model your gateway exposes becomes usable from generateText and streamText.

In short: The Vercel AI SDK's default openai() provider is bound to OpenAI's own URL; createOpenAI({ baseURL }) builds the same provider around any OpenAI-compatible endpoint and works unchanged with generateText, streamText and tool calling.

Step 1 — build your own provider

Don't have an account yet? You need an API key from the gateway before the steps below will work — how the discount works and what you take on. Create one free → (no prepay, $1 minimum top-up)
// lib/gateway.ts
import { createOpenAI } from '@ai-sdk/openai';

// createOpenAI(), not openai(). The latter is bound to OpenAI's own URL.
export const gateway = createOpenAI({
  baseURL: process.env.GATEWAY_BASE_URL ?? 'https://aicomp.ai/v1',
  apiKey: process.env.GATEWAY_API_KEY ?? '',
  // 'compatible' relaxes strict-mode checks some OpenAI-compatible
  // routes trip over. Drop it if your endpoint is strictly OpenAI-shaped.
  compatibility: 'compatible',
});

// Model IDs are strings, not enums (anything your endpoint accepts works).
export const cheap = gateway('deepseek-v4-flash');
export const strong = gateway('deepseek-v4-pro');

This is the whole trick. Everything after this point is ordinary AI SDK code.

Step 2 — a plain generation

import { generateText } from 'ai';
import { gateway } from './lib/gateway';

const { text, usage } = await generateText({
  model: gateway('deepseek-v4-pro'),
  prompt: 'Explain what this regex does, in one paragraph.',
  maxOutputTokens: 400,
});

console.log(text);
// usage.promptTokens / usage.completionTokens are your own cost basis —
// log them, then multiply by your rates rather than trusting a library table.
console.log(usage.promptTokens, usage.completionTokens);

Step 3 — streaming from a route handler

// app/api/chat/route.ts
import { streamText } from 'ai';
import { gateway } from '@/lib/gateway';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: gateway('deepseek-v4-flash'),
    messages,
    system: 'Answer concisely. No preamble.',
  });

  return result.toDataStreamResponse();
}

Step 4 — tool calling

Tool calling rides the same wire format, so it works through the custom provider. The thing people forget is maxSteps: without it the model produces a tool call and stops, because acting on the result is a second step.

import { generateText, tool } from 'ai';
import { z } from 'zod';
import { gateway } from './lib/gateway';

const { text, toolResults } = await generateText({
  model: gateway('deepseek-v4-pro'),
  prompt: 'What is the weather in Taipei?',
  tools: {
    weather: tool({
      description: 'Get current weather for a city',
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, celsius: 28 }),
    }),
  },
  maxSteps: 3,   // allow the model to act on the tool result
});

console.log(text, toolResults);
Confirm the key works first. One command, no SDK, costs nothing:
curl https://aicomp.ai/v1/models \
  -H "Authorization: Bearer sk-your-gateway-key"

A JSON list of model IDs means the key is good. Invalid token means it was copied wrong.

You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment

How this fails in practice

SymptomCauseFix
Requests still go to OpenAIUsing the default openai() providerUse createOpenAI({ baseURL })
404 on every requestDoubled /v1 — the SDK appends the resource pathBase URL ends at /v1, no trailing slash beyond it
Strict-mode validation errorsEndpoint is not byte-strict OpenAISet compatibility: 'compatible'
Tool call produced, nothing happenedDefault step budget is oneSet maxSteps explicitly
Undefined model in the client bundleServer-only env vars referenced from client codeKeep the provider module on the server and call it from your route handler

Why changing one URL is enough

An OpenAI-compatible client does not have a provider hardcoded into it. It holds a base URL, and at request time it concatenates that base URL with a resource path — /chat/completions, /models, then sends the same JSON body it would have sent anywhere else, with your key in an Authorization: Bearer header. The base URL is therefore the only thing that decides where the request physically goes.

That is why this change is one line and not a rewrite. Your message array, your tool definitions, your streaming handling and your response parsing are all unchanged, because the wire format is unchanged. What changes is who answers, and what they charge per token. The two values that travel with the new destination are the base URL itself and the API key; the model ID is the third, and it has to be one the new endpoint actually serves, which you can list with a single call to /v1/models rather than guess at.

The practical consequence is that this is reversible. Keep the base URL in configuration rather than inline, and moving between providers is a config change you can undo in a minute — which is also the right way to test it: point a single non-critical call at the new endpoint first, confirm it in the usage log, then move the rest.

What it costs at production volume

A single-user demo costs nothing. The number that matters is a day of real agent traffic — here priced at 200k input and 60k output tokens:

Cost of one heavy coding day (200k input / 60k output tokens) and a 20-day month. Rates checked 2026-11-09.
ModelVendorRate
in / out per 1M
Per dayPer month
gpt-5.6-lunaOpenAI$0.1 / $0.6$0.06$1
deepseek-v4-flashDeepSeek$0.15 / $0.6$0.07$1
MiniMax-M3MiniMax$0.15 / $0.6$0.07$1
gemini-3.7-flashGoogle$0.375 / $1.875$0.19$4
claude-haiku-4-5-20251001Anthropic$0.5 / $2.5$0.25$5
deepseek-v4-proDeepSeek$0.66 / $1.98$0.25$5
glm-5.3Zhipu$0.7 / $2.2$0.27$5
qwen3.8-maxAlibaba$1 / $3$0.38$8
claude-sonnet-5Anthropic$1 / $5$0.50$10
gpt-5.6-terraOpenAI$1 / $6$0.56$11
kimi-k3Moonshot$1.5 / $7.5$0.75$15
claude-opus-5Anthropic$2.5 / $12.5$1.25$25
Three workload shapes, one month, gateway rates — the coding-day table above prices only the first of them
Workloadgpt-5.6-lunaclaude-sonnet-5deepseek-v4-flash
Chat assistance
50 requests a day, 2k in / 500 out
$0$4$1
Bulk processing
5,000 short jobs a day, 800 in / 300 out
$26$230$30
Agent loop
20 turns a day, 60k in / 800 out
$3$26$4

One month at 20 working days, gateway rates checked 2026-11-09. These are the token volumes, not a quote Your own shape may be larger or smaller.

The table above prices one shape, a heavy coding day. This one prices the range, and the point of putting them side by side is that the ordering is not stable across them. A model that wins on output-heavy generation can lose on a workload that is mostly input, because the two directions are billed separately. Pick the model on the row that matches what you actually ship, not on the row that happens to be first.

Cost note. Split endpoints by task rather than by habit. Exposing two model objects (one inexpensive for drafts and summaries), one stronger for anything touching production code, lets you change economics later without touching call sites. That is the whole argument for putting the provider in one module.

FAQ

Why not just use openai()?

openai() is the default provider, already pointed at OpenAI's own URL with no way to override it. createOpenAI() builds a provider around whatever baseURL you pass (same model objects, different destination).

Is it createOpenAI or createOpenAICompatible?

For OpenAI-compatible endpoints you want createOpenAI from @ai-sdk/openai with its options set, or the compatibility-oriented factory if your installed version ships one. Package names and exports move between major versions. Check the installed version's exports rather than a blog post, including this one.

My model ID is rejected — why?

Some hosts validate against a static list. If yours does, the model worked if it accepts an arbitrary string; if not, you need the host's own model registry. Nothing on the gateway side is validated against a vendor list (the ID you send is the ID that gets billed).

Does streaming work the same way?

Yes. Streaming is part of the OpenAI wire format, so any OpenAI-compatible endpoint streams. In practice streamText with the custom provider behaves identically to the default one.

How do I estimate cost before shipping?

Log usage.promptTokens and usage.completionTokens per request and multiply by your own rates. Do not rely on a library's price table for a gateway route, it does not know your rates, and it definitely does not know your token mix.

Related

Get API access

Rate cards: GPT, DeepSeek and Claude