Skip to content
Adapters

OpenAI-Compatible Adapter

openaiCompatible calls any server that speaks /chat/completions and has no @tanstack/ai-* package. Set baseURL and the model list, then call chat().

openaiCompatible is one generic adapter for all of them. Point it at the baseURL, list your models, and you get the same type-safe chat() as with the other adapters.

If a dedicated adapter exists, use it, because it has curated metadata for each model:

Perplexity Sonar chat uses this adapter. @tanstack/ai-perplexity is for search and grounding only. To send the X-Pplx-Integration attribution header of Perplexity, pass defaultHeaders: getPerplexityIntegrationHeaders() from that package.

1. Install

The adapter is in @tanstack/ai-openai, under the /compatible subpath:

shell
npm i @tanstack/ai-openai

2. Set the API key

Put the key of your provider in the environment of your server, with the variable name that you like:

shell
DEEPSEEK_API_KEY=sk-...

The adapter reads no environment variable itself. You pass the key as apiKey in the next step.

3. Stream a chat from the server

Configure the provider one time with openaiCompatible({ baseURL, apiKey, models }). Then pick a model for each call:

ts
import {
  chat,
  chatParamsFromRequest,
  toServerSentEventsResponse,
} from "@tanstack/ai";
import { openaiCompatible } from "@tanstack/ai-openai/compatible";

const deepseek = openaiCompatible({
  name: "deepseek", // optional label for devtools and errors (default: "openai-compatible")
  baseURL: "https://api.deepseek.com/v1",
  apiKey: process.env.DEEPSEEK_API_KEY!,
  models: ["deepseek-chat", "deepseek-reasoner"],
});

export async function POST(request: Request) {
  const { messages, threadId, runId } = await chatParamsFromRequest(request);

  const stream = chat({
    adapter: deepseek("deepseek-chat"),
    messages,
    threadId,
    runId,
  });

  return toServerSentEventsResponse(stream);
}

The model name is a type-safe union of the models that you declared. deepseek("deepseek-reasoner") is valid. deepseek("gpt-4o") is a type error.

4. Render the chat

The client is the same for every provider. Point useChat at your route:

tsx
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
  });

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        sendMessage(input);
        setInput("");
      }}
    >
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts
            .map((part) => (part.type === "text" ? part.content : ""))
            .join("")}
        </p>
      ))}
      <input value={input} onChange={(event) => setInput(event.target.value)} />
    </form>
  );
}

For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.

You now have a streaming chat with any OpenAI-compatible provider. Tools work the same as with every adapter, on models that support function calling.

One model only

For a single model, skip the provider factory and build the adapter inline with openaiCompatibleText:

ts
import { chat } from "@tanstack/ai";
import { openaiCompatibleText } from "@tanstack/ai-openai/compatible";

const stream = chat({
  adapter: openaiCompatibleText("deepseek-chat", {
    baseURL: "https://api.deepseek.com/v1",
    apiKey: process.env.DEEPSEEK_API_KEY!,
  }),
  messages: [{ role: "user", content: "Hello!" }],
  threadId: "thread-1",
});

Local and self-hosted servers

Point the adapter at any local OpenAI-compatible server. The API key is usually a placeholder:

ts
import { openaiCompatible } from "@tanstack/ai-openai/compatible";

// LM Studio
const lmstudio = openaiCompatible({
  name: "lmstudio",
  baseURL: "http://localhost:1234/v1",
  apiKey: "lm-studio",
  models: ["local-model"],
});

// vLLM
const vllm = openaiCompatible({
  name: "vllm",
  baseURL: "http://localhost:8000/v1",
  apiKey: "not-needed",
  models: ["meta-llama/Llama-3.3-70B-Instruct"],
});

// The OpenAI-compatible endpoint of Ollama
const ollama = openaiCompatible({
  name: "ollama",
  baseURL: "http://localhost:11434/v1",
  apiKey: "ollama",
  models: ["llama3.3"],
});

Ollama also has a dedicated adapter, @tanstack/ai-ollama, for its native API. If you want the OpenAI-compatible surface of Ollama, use openaiCompatible.

LiteLLM proxy

LiteLLM is a self-hosted gateway. It gives one OpenAI Chat Completions endpoint for 100+ providers (OpenAI, Anthropic, Google, Azure, AWS Bedrock, Mistral, Groq, and more). Point openaiCompatible at your proxy (default http://localhost:4000/v1). Pick a provider with the provider/model names of LiteLLM:

ts
import { openaiCompatible } from "@tanstack/ai-openai/compatible";

const litellm = openaiCompatible({
  name: "litellm",
  baseURL: "http://localhost:4000/v1", // your LiteLLM proxy
  apiKey: process.env.LITELLM_API_KEY!, // a virtual key issued by the proxy
  models: [
    "anthropic/claude-sonnet-5",
    "openai/gpt-5.5",
    "gemini/gemini-3.5-flash",
  ],
});
  • litellm("anthropic/claude-sonnet-5") goes to Anthropic, and litellm("openai/gpt-5.5") goes to OpenAI, through the one proxy.
  • Declare only the routes that you configured on the proxy. For exact capabilities of each route, use createModel. See Declaring models.
  • The proxy holds the real credentials of each provider. The apiKey here is the virtual or master key of the proxy.

Models and options

Declaring models

The models array takes two forms. You can mix them:

  • A string gets optimistic defaults: text and image input, with streaming, function_calling, and structured_outputs. Good for mainstream chat models.
  • A createModel(name, capabilities) definition declares the exact capabilities of a model, so the types match the model. An example is a reasoning model with no image input.
ts
import { openaiCompatible } from "@tanstack/ai-openai/compatible";
import { createModel } from "@tanstack/ai";

const provider = openaiCompatible({
  baseURL: "https://api.deepseek.com/v1",
  apiKey: process.env.DEEPSEEK_API_KEY!,
  models: [
    "deepseek-chat", // string: optimistic defaults
    createModel("deepseek-reasoner", {
      input: ["text"], // text only
      features: ["reasoning", "structured_outputs"],
    }),
  ],
});

The types enforce the input modalities, the modelOptions, and the provider tools you declare. They do not read features. Function tools and structured output are always allowed on this path. If a provider rejects one of them at run time (for example, tools on a model without tool support), do not pass it for that model.

Providers

Any provider with the OpenAI Chat Completions API works. Common ones are below. Check the baseURL and the model ids in the current docs of each provider, because they change.

ProviderbaseURLExample model
DeepSeekhttps://api.deepseek.com/v1deepseek-chat, deepseek-reasoner
Moonshot / Kimihttps://api.moonshot.ai/v1kimi-k2-0711-preview
Alibaba Qwen (DashScope, intl)https://dashscope-intl.aliyuncs.com/compatible-mode/v1qwen-max, qwen-plus
Alibaba Qwen (DashScope, China)https://dashscope.aliyuncs.com/compatible-mode/v1qwen-max
Together AIhttps://api.together.xyz/v1meta-llama/Llama-3.3-70B-Instruct-Turbo
Fireworks AIhttps://api.fireworks.ai/inference/v1accounts/fireworks/models/llama-v3p3-70b-instruct
Cerebrashttps://api.cerebras.ai/v1llama-3.3-70b
DeepInfrahttps://api.deepinfra.com/v1/openaimeta-llama/Llama-3.3-70B-Instruct
Perplexityhttps://api.perplexity.aisonar, sonar-pro
Mistralhttps://api.mistral.ai/v1mistral-large-latest
Nebiushttps://api.studio.nebius.ai/v1meta-llama/Llama-3.3-70B-Instruct
Z.AI (GLM)https://api.z.ai/api/paas/v4glm-4.6
Basetenhttps://inference.baseten.co/v1depends on the model
Hugging Face (router)https://router.huggingface.co/v1meta-llama/Llama-3.3-70B-Instruct
NVIDIA NIMhttps://integrate.api.nvidia.com/v1meta/llama-3.3-70b-instruct
Kilo Gatewayhttps://api.kilo.ai/api/gatewayanthropic/claude-opus-5.5
ZenMuxhttps://zenmux.ai/api/v1openai/gpt-5.5
Poehttps://api.poe.com/v1openai/gpt-5.5
DigitalOceanhttps://inference.do-ai.run/v1openai-gpt-5.5
Modalhttps://inference.us-west.modal.direct/v1moonshotai/Kimi-K3
Snowflake Cortexhttps://<account>.snowflakecomputing.com/api/v2/cortex/v1claude-opus-5
  • Some providers sign the user in with OAuth, not with an API key. For Poe, DigitalOcean, and Snowflake Cortex, pass the access token from your sign-in code as apiKey.
  • On Chat Completions, RUN_FINISHED.usage carries the prompt cache counts that the provider reports. Cache reads go to promptTokensDetails.cachedTokens, and cache writes go to promptTokensDetails.cacheWriteTokens. Moonshot / Kimi reports both.

Advanced

These topics are on OpenAI-Compatible: Advanced:

  • Configuration: headers, query parameters, and wrapFetch.
  • The Responses API (api: "responses").
  • Azure OpenAI.
  • GitHub Copilot: let users pay with their Copilot plan.
  • ChatGPT plan: let users pay with their ChatGPT plan.

To add custom models to any adapter, see Extending Adapters.