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

// 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

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-09-20.
ModelVendorRate
in / out per 1M
Per dayPer month
gpt-5.6-lunaOpenAI$0.1 / $0.6$0.06$1
MiniMax-M3MiniMax$0.15 / $0.6$0.07$1
deepseek-v4-flashDeepSeek$0.22 / $0.66$0.08$2
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
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