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.
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);
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.
https://aicomp.ai/v1).
Create one free →
How this fails in practice
| Symptom | Cause | Fix |
|---|---|---|
| Requests still go to OpenAI | Using the default openai() provider | Use createOpenAI({ baseURL }) |
| 404 on every request | Doubled /v1 — the SDK appends the resource path | Base URL ends at /v1, no trailing slash beyond it |
| Strict-mode validation errors | Endpoint is not byte-strict OpenAI | Set compatibility: 'compatible' |
| Tool call produced, nothing happened | Default step budget is one | Set maxSteps explicitly |
| Undefined model in the client bundle | Server-only env vars referenced from client code | Keep 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:
| Model | Vendor | Rate in / out per 1M | Per day | Per month |
|---|---|---|---|---|
| gpt-5.6-luna | OpenAI | $0.1 / $0.6 | $0.06 | $1 |
| MiniMax-M3 | MiniMax | $0.15 / $0.6 | $0.07 | $1 |
| deepseek-v4-flash | DeepSeek | $0.22 / $0.66 | $0.08 | $2 |
| gemini-3.7-flash | $0.375 / $1.875 | $0.19 | $4 | |
| claude-haiku-4-5-20251001 | Anthropic | $0.5 / $2.5 | $0.25 | $5 |
| deepseek-v4-pro | DeepSeek | $0.66 / $1.98 | $0.25 | $5 |
| glm-5.3 | Zhipu | $0.7 / $2.2 | $0.27 | $5 |
| qwen3.8-max | Alibaba | $1 / $3 | $0.38 | $8 |
| claude-sonnet-5 | Anthropic | $1 / $5 | $0.50 | $10 |
| gpt-5.6-terra | OpenAI | $1 / $6 | $0.56 | $11 |
| kimi-k3 | Moonshot | $1.5 / $7.5 | $0.75 | $15 |
| claude-opus-5 | Anthropic | $2.5 / $12.5 | $1.25 | $25 |
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.