You want Claude, GPT, and Gemini in your app, and you do not want an account and a key for each provider. The Neon AI Gateway gives your Neon branch one credential for every model in its catalog.
Install @tanstack/ai-neon. Then call neonText with a model id such as claude-opus-5-5.
npm i @tanstack/ai-neonpnpm add @tanstack/ai-neonyarn add @tanstack/ai-neonbun add @tanstack/ai-neonThe adapter reads two variables:
neon env pull writes both to .env. A deployed Neon Function gets both automatically.
NEON_AI_GATEWAY_TOKEN=nt_live_...
NEON_AI_GATEWAY_BASE_URL=https://br-winter-pond-aptw82ef-api.ai.c-2.us-east-2.aws.neon.techYou can also pass the token to a create* factory. baseURL is the same bare host:
import { createNeonText } from "@tanstack/ai-neon"
const adapter = createNeonText("claude-opus-5-5", process.env.NEON_AI_GATEWAY_TOKEN!, {
baseURL: process.env.NEON_AI_GATEWAY_BASE_URL,
})Model ids are the catalog ids, with no provider prefix.
Server. An endpoint that streams the reply over SSE:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai"
import { neonText } from "@tanstack/ai-neon"
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
const stream = chat({
adapter: neonText("claude-opus-5-5"),
messages,
threadId,
runId,
})
return toServerSentEventsResponse(stream)
}Client. The same useChat hook as every other provider:
import { useState } from "react"
import { fetchServerSentEvents, useChat } from "@tanstack/ai-react"
export function Chat() {
const [input, setInput] = useState("")
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}</strong>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault()
if (!input.trim() || isLoading) return
sendMessage(input)
setInput("")
}}
>
<input value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
)
}To change the model, change the id. The rest of the code stays the same.
The gateway has one endpoint per API style. neonText sends each model to the endpoint that serves it best:
| Models | Endpoint | What you get |
|---|---|---|
| Claude (claude-*) | /anthropic/v1/messages | The Anthropic adapter, with thinking budgets and prompt caching |
| gpt-5-3-codex, gpt-5-5-pro | /openai/v1/responses | The OpenAI Responses API, the only API that serves these models |
| All other models | /v1/chat/completions | Chat Completions, which serves every model in the catalog |
The types follow the endpoint. For example, a Claude model takes the Anthropic model options.
Chat Completions accepts text and images through this adapter. For documents, use Claude or a GPT model with api: "responses". Audio and video inputs are not supported.
Other GPT models can also use the Responses API. Pass { api: "responses" }:
import { chat } from "@tanstack/ai"
import { neonText } from "@tanstack/ai-neon"
const stream = chat({
adapter: neonText("gpt-6-1-sol", { api: "responses" }),
messages: [{ role: "user", content: "Hello" }],
threadId: "thread-1",
})NEON_RESPONSES_MODELS lists the models that take api: "responses". TypeScript rejects it for any other model.
Need a header on each request, or a log of each model call? The chat adapters on this page support wrapFetch.
Set reasoning on chat(). The types take the levels of each model:
import { chat } from "@tanstack/ai"
import { neonText } from "@tanstack/ai-neon"
const stream = chat({
adapter: neonText("gemini-3-8-flash"),
messages: [{ role: "user", content: "Plan a database migration." }],
threadId: "thread-1",
reasoning: "high",
})Embed text with the same credential. Then keep the vectors in your branch, next to the rows they describe:
import { embed } from "@tanstack/ai"
import { neonEmbedding } from "@tanstack/ai-neon"
const result = await embed({
adapter: neonEmbedding("qwen3-embedding-0-6b"),
input: ["Neon branches a database like code."],
})
console.log(result.embeddings[0]?.vector.length) // 1024All three embedding models (qwen3-embedding-0-6b, gte-large-en, bge-large-en) return 1024 numbers. A pgvector column of the same size stores them:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE documents (id serial PRIMARY KEY, body text, embedding vector(1024));import { summarize } from "@tanstack/ai"
import { neonSummarize } from "@tanstack/ai-neon"
const result = await summarize({
adapter: neonSummarize("gpt-6-1-sol"),
threadId: "thread-1",
text: "The Fender Stratocaster is a versatile electric guitar.",
maxLength: 60,
})Summarize uses the same endpoint and route options as neonText for the model. With api: "responses", modelOptions accepts Responses options such as max_output_tokens.
Pass a model id from the catalog. TypeScript rejects an id that is not on a list.
The lists come from the Neon model catalog. Your account can have a smaller set. To see the models your branch can call, ask the gateway:
curl "$NEON_AI_GATEWAY_BASE_URL/v1/models" \
-H "Authorization: Bearer $NEON_AI_GATEWAY_TOKEN"A model with "enabled": false in that list is in the catalog, but your account cannot call it yet.
The gateway needs a paid Neon plan and a project in a supported region. See Neon AI Gateway for regions, pricing, and limits.