The AI SDK’s OpenAI-compatible provider reaches every model on this catalog with one createOpenAICompatible call. This page has the provider, generateText, streamText and tool() on an uncensored build, which builds take tool calls, a Next.js route handler, the error codes, and what one call costs at the live price.
Install
Two packages: ai, the SDK, and @ai-sdk/openai-compatible, the provider for any endpoint that speaks the OpenAI Chat Completions format. zod is for the tool example further down.
pnpm add ai @ai-sdk/openai-compatible zod# ornpm install ai @ai-sdk/openai-compatible zodThe key comes from the console, starts with sk-ullm-, is shown once and spends a prepaid balance that starts at $10. Put it in an environment variable; the code never holds it.
export UNCENSOREDLLM_API_KEY="sk-ullm-live-…"The provider
createOpenAICompatible takes a name, our baseURL and the apiKey. The base URL ends in /v1; the SDK adds /chat/completions. includeUsage puts the token counts on streamed answers as well, which the cost table below depends on.
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; export const uncensored = createOpenAICompatible({ name: 'uncensoredllm', baseURL: 'https://api.uncensoredllm.ai/v1', apiKey: process.env.UNCENSOREDLLM_API_KEY, includeUsage: true, // usage in streaming responses too});- name
- uncensoredllmany label; it names the provider in providerOptions
- baseURL
- https://api.uncensoredllm.ai/v1ends in /v1; nothing after it
- apiKey
- process.env.UNCENSOREDLLM_API_KEYsent as Authorization: Bearer
- model
- uncensored('glm-5.3-uncensored')any id from the models page; provider.chatModel(id) is the same call
Anything else that speaks this format, the OpenAI SDKs included, uses the same base URL and key.
generateText
provider('model-id') is a chat model; pass it as model. The result carries text, finishReason and usage with inputTokens, outputTokens and totalTokens: the numbers the request was billed on.
import { generateText } from 'ai';import { uncensored } from './provider'; const { text, usage, finishReason } = await generateText({ model: uncensored('glm-5.3-uncensored'), prompt: 'Explain how a lock pick works, step by step.',}); console.log(text);console.log(usage.inputTokens, usage.outputTokens, usage.totalTokens, finishReason);streamText
streamText returns at once; textStream yields the pieces as the model writes them, and result.usage is a promise that resolves when the stream ends.
import { streamText } from 'ai';import { uncensored } from './provider'; const result = streamText({ model: uncensored('glm-5.3-flash-uncensored'), prompt: "Write a villain's monologue.",}); for await (const piece of result.textStream) process.stdout.write(piece); const usage = await result.usage; // resolves when the stream ends; needs includeUsage on the providerconsole.log(usage.inputTokens, usage.outputTokens);In a Next.js route handler, return result.toTextStreamResponse() for plain text or result.toUIMessageStreamResponse() for the useChat hook.
// app/api/chat/route.tsimport { streamText } from 'ai';import { uncensored } from '@/lib/provider'; export async function POST(req: Request) { const { prompt } = (await req.json()) as { prompt: string }; const result = streamText({ model: uncensored('glm-5.3-uncensored'), prompt, }); return result.toTextStreamResponse();}Tool calls
tool() takes a description, an inputSchema and an execute function; stopWhen: isStepCount(5) lets the SDK run the tool and send the result back for up to five steps. steps holds the tool calls and results of each one.
import { generateText, isStepCount, tool } from 'ai';import { z } from 'zod';import { uncensored } from './provider'; const { text, steps } = await generateText({ model: uncensored('glm-5.3-uncensored'), tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 }), }), }, stopWhen: isStepCount(5), // keep calling tools for up to 5 steps prompt: 'What is the weather in San Francisco?',}); const toolCalls = steps.flatMap((step) => step.toolCalls);console.log(toolCalls.length, text);A tool call needs a build whose base model was trained for it. The catalog publishes tools among a build’s supported parameters once every route of it takes tool calls; 42 listed builds publish it on 2026-11-09, and the agent tools page tables them all. The builds on this page do not publish the field on 2026-11-09; pick a build from that table for tool work and keep these for text.
- nemotron-3-ultra-550b-a55b-uncensored
- takes tool callsNemotron 3 Ultra 550B Uncensored
- abliterated-model-large-flash
- takes tool callsAbliterated Large Flash
- abliterated-model-large
- takes tool callsAbliterated Model Large
- abliterated-model-large-v2
- takes tool callsAbliterated Model Large V2
The gateway also runs tools of its own: web search, web fetch, a remote MCP server and a code sandbox, declared in the request’s tools array and run inside the one request, on the agent tools page.
What a call costs
One chat call of 2,000 input and 400 output tokens, the shape of a short conversation with a system prompt, on four builds at the catalog prices on 2026-11-09:
| Model | One call | 1,000 calls |
|---|---|---|
| GLM-5.3 Uncensored | $0.0009 | $0.90 |
| GLM-5.3 Abliterated | $0.0061 | $6.11 |
| DeepSeek V4.1 Flash Abliterated | $0.0024 | $2.38 |
| GLM-5.3 Flash Uncensored | $0.00036 | $0.36 |
usage.inputTokens and usage.outputTokens on each result are the counts the charge was computed from; multiply by the per-million prices below to reconcile a bill.
| GLM-5.3 Uncensored | Uncensored | $0.282 | $0.846 |
| GLM-5.3 Abliterated | Abliterated | $2.1825 | $4.365 |
| DeepSeek V4.1 Flash Abliterated | Abliterated | $0.8488 | $1.6975 |
| GLM-5.3 Flash Uncensored | Uncensored | $0.1053 | $0.3685 |
Check it
The list call is free and proves the key and base URL; it returns exactly the ids the key can call.
curl https://api.uncensoredllm.ai/v1/models -H "Authorization: Bearer sk-ullm-live-…"Every answer carries x-uplink-request-id; a non-streamed answer carries x-uplink-charge, what it cost in USD. The console’s usage page lists both, and the docs page has the rest of the API.
Errors and fixes
401 invalid_api_key: the key is wrong or revoked. Copy it again from the console and check the variable name the provider reads.402 insufficient_balance: the balance does not cover the request. Top up in the console.402 monthly_limit_reachedmeans the key hit the monthly spend limit you set on it.403 scope_denied: the key is limited to certain models and this is not one of them.404 model_not_found: the id inuncensored(‘…’)is not on the models page. Check the spelling; display names are not ids.429 rate_limited: the key hit its rate limit. The response carriesretry-after.- A request that fails before the model answers is not charged.
Questions
@ai-sdk/openai-compatible sends Chat Completions, which every listed build answers. The catalog also lists /v1/responses on the builds on this page, so the @ai-sdk/openai package, whose default openai(id) uses the Responses API, reaches them too; openai.chat(id) keeps it on Chat Completions.
Both reach the models. The compatible provider is written for endpoints like this one: Chat Completions by default, includeUsage for streamed usage, and providerOptions for extra request fields. @ai-sdk/openai works through its .chat() factory. See the OpenAI SDK guide.
Yes. OpenCode runs on the AI SDK and takes a custom OpenAI-compatible provider in its config; the same base URL, key and model ids apply. See the OpenCode guide.
Call streamText in the POST handler and return result.toTextStreamResponse() for plain text, or result.toUIMessageStreamResponse() when the page uses useChat. The route example above is the whole handler.
Every model above is on the uncensored model list with its live price per million tokens. Per-token pricing sets the whole catalog side by side, and the family directory groups the builds by base model.