- OpenAIUpdated
- Anthropic
- Google Gemini
- Google Vertex AI
- Ollama
- Grok (xAI)
- GroqUpdated
- Mistral
- Cohere
- TypeSafe
- Ollaya
- ElevenLabs
- fal.ai
- Reactor
- World Labs
- OpenRouter AdapterUpdated
- Perplexity Search
- Vercel AI Gateway
- Lovable AI Gateway
- OpenAI-CompatibleUpdated
- LLM Gateway
- CloudflareUpdated
- Claude Code
- Codex
- OpenCode
- Grok Build
- ACP-Compatible
- Amazon BedrockUpdated
- BytePlus
OpenAI
The OpenAI adapter provides access to OpenAI's models, including GPT-4o, GPT-5, image generation (DALL-E), text-to-speech (TTS), and audio transcription (Whisper).
Using a third-party provider that speaks the OpenAI API (DeepSeek, Moonshot/Kimi, Together, Fireworks, a local LM Studio/vLLM server, …)? See the OpenAI-Compatible Adapter for a generic openaiCompatible({ baseURL, apiKey, models }) factory.
Installation#
npm i @tanstack/ai-openaipnpm add @tanstack/ai-openaiyarn add @tanstack/ai-openaibun add @tanstack/ai-openaiBasic Usage#
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
});Chat Completions API#
@tanstack/ai-openai ships two text adapters that hit different OpenAI endpoints. openaiText (default) calls the Responses API (/v1/responses). openaiChatCompletions calls the older Chat Completions API (/v1/chat/completions).
Pick whichever fits your wire format and feature needs:
| openaiText (Responses) | openaiChatCompletions (Chat Completions) | |
|---|---|---|
| Endpoint | /v1/responses | /v1/chat/completions |
| Reasoning summaries | Yes. With reasoning set, the summary text streams as REASONING_* events | No. reasoning goes out as reasoning_effort, and no text streams back |
| Wire-format compatibility | OpenAI-only | Matches the older de-facto industry shape (Grok, Groq, OpenRouter, many local model servers) |
| Structured output streaming | text.format: { type: 'json_schema', strict: true } + stream: true | response_format: { type: 'json_schema', strict: true } + stream: true |
Use openaiText when you want reasoning-summary streaming or OpenAI-specific Responses features. Use openaiChatCompletions when you're migrating off a Chat-Completions-style provider, share request-building code with other Chat-Completions adapters in your stack, or want the more battle-tested wire format.
import { chat } from "@tanstack/ai";
import { openaiChatCompletions } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiChatCompletions("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
});With an explicit API key:
import { chat } from "@tanstack/ai";
import { createOpenaiChatCompletions } from "@tanstack/ai-openai";
const adapter = createOpenaiChatCompletions("gpt-5.2", process.env.OPENAI_API_KEY!, {
// organization, baseURL, headers — all optional
});
const stream = chat({
adapter,
messages: [{ role: "user", content: "Hello!" }],
});Both adapters work the same with Structured Outputs, including stream: true, and accept the same modelOptions (temperature, top_p, max_tokens, stop, and more). Both take reasoning. openaiChatCompletions cannot stream the thinking text.
Azure OpenAI#
Your OpenAI model runs as a deployment in Azure? Use createAzureOpenaiText. It calls the Azure Responses API and sends your key in the api-key header.
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { createAzureOpenaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: createAzureOpenaiText("gpt-5.6", process.env.AZURE_OPENAI_API_KEY!, {
resourceName: "my-resource",
deploymentName: "production-chat",
}),
messages,
});
return toServerSentEventsResponse(stream);
}Azure gets production-chat as the model. Your code still uses gpt-5.6. The client does not change: it reads this route like any other chat route.
reasoning takes the levels of the OpenAI model name, here gpt-5.6. A model name that is not an OpenAI model, such as a bare deployment name, takes no reasoning. See Reasoning.
createAzureOpenaiText reads nothing from the environment. You pass the key, and you pass the endpoint as resourceName or baseURL.
Rather keep it all in the environment? Use azureOpenaiText. Like openaiText, it reads the key from the environment:
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_RESOURCE_NAME=my-resource
AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-5.6=production-chatimport { azureOpenaiText } from "@tanstack/ai-openai";
const adapter = azureOpenaiText("gpt-5.6");azureOpenaiText always reads the key from AZURE_OPENAI_API_KEY, and it throws when that is not set. For the other values, the config wins over the environment:
- Endpoint: baseURL, then resourceName, then AZURE_OPENAI_BASE_URL, then AZURE_OPENAI_RESOURCE_NAME. You must set one of them.
- API version: apiVersion, then AZURE_OPENAI_API_VERSION, then v1.
- Deployment: deploymentName, then deploymentNameMap, then AZURE_OPENAI_DEPLOYMENT_NAME_MAP, then the model name. A deploymentNameMap in the config replaces the environment map. The two maps do not merge.
An Azure resource URL always goes to the /openai/v1 path. A proxy URL keeps its own path.
Basic Usage - Custom API Key#
import { chat } from "@tanstack/ai";
import { createOpenaiChat } from "@tanstack/ai-openai";
const adapter = createOpenaiChat("gpt-5.2", process.env.OPENAI_API_KEY!, {
// ... your config options
});
const stream = chat({
adapter,
messages: [{ role: "user", content: "Hello!" }],
});Sign in with ChatGPT (BYOK)#
Many of your users have a ChatGPT plan but no API key. Let them sign in with ChatGPT. The token bills their ChatGPT plan, not your account.
The helpers in @tanstack/ai-openai/siwc put the access token into the openai BYOK slot. Your relay reads it like a pasted key.
Before you start, check these limits:
- OpenAI allows this sign-in only for open-source and locally hosted apps. For a hosted app, fill in the interest form.
- OpenAI redirects only to 127.0.0.1. Something must answer on that address. Vite listens on ::1 by default. The ts-react-chat example runs a small script on 127.0.0.1:3000 that redirects to localhost:3000.
- Keep the app on localhost, because passkey storage does not work on an IP address. The callback lands on 127.0.0.1, then completeChatGptSignIn sends the browser back to localhost.
- The callback page must be at /auth/callback.
1. Add the sign-in button#
Call startChatGptSignIn from a click. agentName is your app name. OpenAI shows it on the consent screen.
import { startChatGptSignIn } from "@tanstack/ai-openai/siwc";
export function ChatGptButton() {
return (
<button
type="button"
onClick={() => {
void startChatGptSignIn({ agentName: "My App" });
}}
>
Continue with ChatGPT
</button>
);
}2. Finish on /auth/callback#
Render this component on the /auth/callback route. It exchanges the code when the page loads. Then the user clicks to save.
import { useEffect, useRef, useState } from "react";
import {
completeChatGptSignIn,
saveChatGptSignIn,
} from "@tanstack/ai-openai/siwc";
import type { ChatGptSignIn } from "@tanstack/ai-openai/siwc";
import { byok } from "./byok";
export function ChatGptCallback() {
const started = useRef(false);
const [signIn, setSignIn] = useState<ChatGptSignIn | null>(null);
const [error, setError] = useState("");
useEffect(() => {
// The code works one time. Strict Mode runs effects two times.
if (started.current) return;
started.current = true;
completeChatGptSignIn().then(setSignIn, (caught: unknown) => {
setError(caught instanceof Error ? caught.message : "Sign-in failed");
});
}, []);
if (error) return <p>{error}</p>;
if (!signIn) return <p>Finishing sign-in...</p>;
return (
<button
type="button"
onClick={() => {
void saveChatGptSignIn(byok, signIn);
}}
>
Save ChatGPT sign-in
</button>
);
}The save needs a click because passkey storage shows a browser prompt. The refresh token goes into a second keyring slot. No send attaches that slot, so the refresh token stays in the browser.
3. Refresh the token#
The access token lasts one hour. Call refreshChatGptSignIn on a timer:
import { useEffect } from "react";
import { refreshChatGptSignIn } from "@tanstack/ai-openai/siwc";
import { byok } from "./byok";
export function useChatGptRefresh() {
useEffect(() => {
const refresh = () => {
refreshChatGptSignIn(byok).catch(console.error);
};
refresh();
const timer = setInterval(refresh, 60_000);
return () => clearInterval(timer);
}, []);
}The call does nothing until the token has less than five minutes left. If OpenAI rejects the refresh token, the call clears the sign-in. Then the user must sign in again.
4. Read the token on the relay#
The relay code is the same as for a pasted key. Add store: false, because the ChatGPT route requires it.
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { createOpenaiChat } from "@tanstack/ai-openai";
import { openaiByok } from "@tanstack/ai-openai/byok";
import { byokMissing, getByokKey } from "@tanstack/ai/byok/server";
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request);
const apiKey = getByokKey(request, openaiByok);
if (!apiKey) return byokMissing(openaiByok);
const stream = chat({
adapter: createOpenaiChat("gpt-6-astra", apiKey),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
modelOptions: { store: false },
});
return toServerSentEventsResponse(stream);
}store: false also works with an API key. A ChatGPT token does not start with sk-, so use that test if you must send different options.
The ChatGPT route has these limits:
- Use a model that the user's ChatGPT plan includes.
- Do not send temperature, top_p, max_output_tokens, metadata, or previous_response_id.
- Hosted tools do not work. This includes image generation, file search, code interpreter, computer use, and hosted MCP.
- Audio input and transcription do not work.
Click Continue with ChatGPT, approve, save, then send a message. The relay calls OpenAI on the user's ChatGPT plan.
The ts-react-chat example has this flow in its key dialog.
Configuration#
import { createOpenaiChat, type OpenAITextConfig } from "@tanstack/ai-openai";
const config: Omit<OpenAITextConfig, "apiKey"> = {
organization: "org-...", // Optional
baseURL: "https://api.openai.com/v1", // Optional, for custom endpoints
};
const adapter = createOpenaiChat("gpt-5.2", process.env.OPENAI_API_KEY!, config);Need a header on each request, or a log of each model call? The chat adapters on this page support wrapFetch.
Tools that cannot use strict mode#
OpenAI holds tool arguments to the schema only when the tool is sent with strict: true. Some schemas cannot be strict, for example a schema that uses $ref, z.any(), or z.record(). The adapter sends these tools with strict: false, so the tool still works. The model can then return arguments that do not match the schema.
In development, the adapter logs a warning once for each of these tools:
tool "lookup_user" sent with strict: false: schema uses $ref, which strict mode does not supportTo get strict mode back, change the schema so that the reason goes away. If you accept a non-strict tool, turn the warning off in the adapter config:
import { createOpenaiChat } from "@tanstack/ai-openai";
const adapter = createOpenaiChat("gpt-6-astra", process.env.OPENAI_API_KEY!, {
strictFallbackWarning: false,
});The warning never runs when NODE_ENV is production. The same option works on every adapter built on the OpenAI API, for example Groq, Bedrock, and Vercel AI Gateway.
Example: Chat Completion#
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
});
return toServerSentEventsResponse(stream);
}Example: With Tools#
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
const getWeatherDef = toolDefinition({
name: "get_weather",
description: "Get the current weather",
inputSchema: z.object({
location: z.string(),
}),
});
const getWeather = getWeatherDef.server(async ({ location }) => {
// Fetch weather data
return { temperature: 72, conditions: "sunny" };
});
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [getWeather],
});
return toServerSentEventsResponse(stream);
}Model Options#
OpenAI supports various provider-specific options. Sampling parameters live here too — temperature, top_p, and max_output_tokens (the Responses API token-limit key) — rather than as root-level props on chat():
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Hello!" }],
modelOptions: {
temperature: 0.7,
max_output_tokens: 1000,
top_p: 0.9,
},
});The openaiChatCompletions adapter targets /v1/chat/completions, where the token-limit key is max_tokens (not max_output_tokens). If you previously passed temperature / topP / maxTokens at the root of chat(), see Moving Sampling Options into modelOptions.
Reasoning#
Want GPT to think harder, or skip it? Set reasoning on chat():
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: [{ role: "user", content: "Plan a database migration." }],
reasoning: "xhigh",
});What goes on the wire:
- openaiText and azureOpenaiText: reasoning: { effort, summary: "auto" }. The summary text streams as thinking parts. summary: false drops the summary.
- openaiChatCompletions: reasoning_effort. No thinking text streams back.
- off sends effort: "none" on the models that can stop thinking.
The levels of each model:
| Model | Levels |
|---|---|
| gpt-6.1-sol, gpt-6.1-sol-pro, gpt-6-astra, gpt-6-astra-pro | low to max |
| gpt-6-sol, gpt-6-luna, the gpt-5.6 models, and their -pro versions | off, low to max |
| gpt-5.5, gpt-5.4-mini, gpt-5.4-nano, gpt-5.4-image-2, gpt-5.2 | off, low to xhigh |
| gpt-5.5-pro, gpt-5.2-pro | medium, high, xhigh |
| gpt-5.1 | off, low, medium, high |
| gpt-5, gpt-5-mini, gpt-5-nano | minimal to high |
| gpt-chat-latest, the o models, and the codex models | low, medium, high |
| gpt-5.2-chat-latest | medium |
| gpt-5-pro | high |
The other models, such as gpt-4.1 and gpt-4o, take no reasoning.
Native compaction#
OpenAI can compact a long conversation on its side, with POST /responses/compact. The Responses adapters (openaiText and createOpenaiChat) have a compact method for it. Turn it on with the native option of withCompaction:
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { withCompaction } from "@tanstack/ai-compaction";
const adapter = openaiText("gpt-6.1-sol");
const stream = chat({
adapter,
messages: [{ role: "user", content: "Hello!" }],
middleware: [withCompaction({ maxTokens: 100_000, native: adapter })],
});- The result holds your user messages and one encrypted compaction item.
- The adapter sends that item back as it is on each later request to the same model.
- openaiChatCompletions has no compact method, so withCompaction uses its strategy.
Answers on the next request#
OpenAI gives each answer item an id and a phase: commentary for text before a tool call, and final_answer for the answer. The Responses adapters (openaiText and createOpenaiChat) keep both in the assistant message, in metadata.tanstack.responseItems. When the same model gets that message again, for example on the next turn, the adapter sends each item back with its id and phase. Another model gets the plain text.
Prompt caching#
chat() sends prompt_cache_key by default, set to the threadId that you pass. OpenAI uses the key to send requests with the same start to the same cache. With promptCache: 'long', it also asks for the long retention. See Prompt Caching.
Want your own key? Set it in modelOptions. Your value wins:
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
const stream = chat({
adapter: openaiText("gpt-6.1-sol"),
messages: [{ role: "user", content: "Hello!" }],
modelOptions: {
prompt_cache_key: "acme-support",
},
});On gpt-5.6 and later, modelOptions.prompt_cache_options wins over the automatic options. On earlier models, modelOptions.prompt_cache_retention wins over the automatic retention. openaiChatCompletions sends the key only to api.openai.com and its regional hosts (such as eu.api.openai.com), unless you pass promptCache: 'long'.
Tools and prompts added during a conversation#
On gpt-5.4-mini, gpt-5.5, gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-6-astra, gpt-6-luna, and gpt-6-sol, a tool or a system prompt that you add between model calls goes into the conversation. The cached start stays the same.
What openaiText sends on these models:
- tools and instructions keep the tools and the system prompts of the first call.
- An added tool goes into input as { type: "additional_tools", role: "developer", tools: [...] }.
- An added system prompt goes into input as a developer message, at its place in the conversation.
- With a provider tool such as webSearchTool(), tools is the full list for that request.
The channels are on by default with OpenAI's own API. A custom baseURL, a custom fetch, or OPENAI_BASE_URL turns them off. Set midConversationChannels to choose:
import { openaiText } from "@tanstack/ai-openai";
export const fullLists = openaiText("gpt-6-astra", { midConversationChannels: false });
export const throughProxy = openaiText("gpt-6-astra", {
baseURL: "https://llm-proxy.example.com/v1",
midConversationChannels: true,
});If that endpoint sends the request on to OpenAI as it is, set true. See Mid-Conversation Changes.
Summarization#
Summarize long text content:
import { summarize } from "@tanstack/ai";
import { openaiSummarize } from "@tanstack/ai-openai";
const result = await summarize({
adapter: openaiSummarize("gpt-5-mini"),
text: "Your long text to summarize...",
maxLength: 100,
style: "concise", // "concise" | "bullet-points" | "paragraph"
});
console.log(result.summary);Embeddings#
Generate embedding vectors with the text-embedding-3 models:
import { embed } from "@tanstack/ai";
import { openaiEmbedding } from "@tanstack/ai-openai";
const result = await embed({
adapter: openaiEmbedding("text-embedding-3-small"),
input: ["a red guitar", "a blue drum kit"],
});
console.log(result.embeddings[0]?.vector);
console.log(result.usage?.promptTokens);Both models support Matryoshka dimension reduction via the top-level dimensions option:
import { embed } from "@tanstack/ai";
import { openaiEmbedding } from "@tanstack/ai-openai";
const result = await embed({
adapter: openaiEmbedding("text-embedding-3-large"),
input: "a red guitar",
dimensions: 1024,
});See the Embeddings guide for the full API.
Image Generation#
Generate images:
import { generateImage } from "@tanstack/ai";
import { openaiImage } from "@tanstack/ai-openai";
const result = await generateImage({
adapter: openaiImage("gpt-image-2"),
prompt: "A futuristic cityscape at sunset",
numberOfImages: 1,
size: "1024x1024",
});
console.log(result.images);Image Model Options#
import { generateImage } from "@tanstack/ai";
import { openaiImage } from "@tanstack/ai-openai";
const result = await generateImage({
adapter: openaiImage("gpt-image-2"),
prompt: "...",
modelOptions: {
quality: "high", // "high" | "medium" | "low" | "auto"
},
});Text-to-Speech#
Generate speech from text:
import { generateSpeech } from "@tanstack/ai";
import { openaiSpeech } from "@tanstack/ai-openai";
const result = await generateSpeech({
adapter: openaiSpeech("tts-1"),
text: "Hello, welcome to TanStack AI!",
voice: "alloy",
format: "mp3",
});
// result.audio contains base64-encoded audio
console.log(result.format); // "mp3"TTS Voices#
Available voices: alloy, echo, fable, onyx, nova, shimmer, ash, ballad, coral, sage, verse
TTS Model Options#
import { generateSpeech } from "@tanstack/ai";
import { openaiSpeech } from "@tanstack/ai-openai";
const result = await generateSpeech({
adapter: openaiSpeech("tts-1-hd"),
text: "High quality speech",
modelOptions: {
instructions: "Speak slowly and clearly.", // voice instructions (not supported by tts-1/tts-1-hd)
},
});Transcription#
Transcribe audio to text:
import { generateTranscription } from "@tanstack/ai";
import { openaiTranscription } from "@tanstack/ai-openai";
import { audioFile } from "./audio";
const result = await generateTranscription({
adapter: openaiTranscription("whisper-1"),
audio: audioFile, // File object or base64 string
language: "en",
});
console.log(result.text); // Transcribed textTranscription Model Options#
import { generateTranscription } from "@tanstack/ai";
import { openaiTranscription } from "@tanstack/ai-openai";
import { audioFile } from "./audio";
const result = await generateTranscription({
adapter: openaiTranscription("whisper-1"),
audio: audioFile,
responseFormat: "verbose_json",
prompt: "Technical terms: API, SDK",
modelOptions: {
temperature: 0,
timestamp_granularities: ["word", "segment"],
},
});
// Access the transcribed text
console.log(result.text);Speaker Diarization#
Use gpt-4o-transcribe-diarize for speaker-labeled transcripts:
import { generateTranscription } from "@tanstack/ai";
import { openaiTranscription } from "@tanstack/ai-openai";
import { meetingAudioFile } from "./audio";
const result = await generateTranscription({
adapter: openaiTranscription("gpt-4o-transcribe-diarize"),
audio: meetingAudioFile,
modelOptions: {
known_speaker_names: ["agent", "customer"],
known_speaker_references: [
"data:audio/wav;base64,...",
"data:audio/wav;base64,...",
],
},
});
for (const segment of result.segments ?? []) {
console.log(segment.speaker, segment.start, segment.end, segment.text);
}When no response format is specified, gpt-4o-transcribe-diarize requests default to response_format: "diarized_json" and chunking_strategy: "auto"; passing a top-level responseFormat of "json" or "text" opts out of speaker segments. known_speaker_names and known_speaker_references must be provided together (up to 4, matching lengths). OpenAI does not support prompt, include, or timestamp_granularities with diarized transcription.
Evaluate#
Sometimes you need an answer that your code can branch on, not chat text. Examples are a queue name, an urgency level, or a yes or no. Use openaiDecider with decide() to ask typed questions about one shared state. The adapter calls the OpenAI Decisions API (/v1/decisions):
import { decide, choice, score, boolean } from "@tanstack/ai";
import { openaiDecider } from "@tanstack/ai-openai";
const ticket = {
subject: "Charged twice for the same invoice",
body: "Please refund the extra payment.",
};
const result = await decide({
adapter: openaiDecider("gpt-6-luna"),
state: ticket,
questions: {
queue: choice({
instructions: "Which team should handle this ticket?",
options: {
billing: "Payments, invoices, refunds",
tech: "Bugs, outages, integrations",
sales: "Pricing, upgrades, new accounts",
},
}),
urgency: score({
instructions: "How urgent is this ticket?",
levels: ["low", "medium", "high"],
}),
refund: boolean({
instructions: "Is the customer asking for a refund?",
}),
},
});
console.log(result.queue.value); // "billing"
console.log(result.urgency.value); // "medium"
console.log(result.refund.value); // true
console.log(result.meta.usage);openaiDecider reads OPENAI_API_KEY from the environment. To pass a key yourself, use createOpenaiDecider("gpt-6-luna", "sk-...").
Good to know:
- gpt-6-luna is the only Decisions model.
- An object state goes to OpenAI as JSON text.
- If OpenAI refuses to answer a question, decide() throws an error with the name of that question.
- boolean() criteria work. The adapter adds the true and false meanings to the instructions.
See the Evaluate guide for question helpers, the result shape, abort, and middleware.
Environment Variables#
Set your API key in environment variables:
OPENAI_API_KEY=sk-...API Reference#
Every factory pair follows the same shape: the short factory (openaiText, openaiImage, …) reads OPENAI_API_KEY from the environment, while the create* variant takes an explicit API key. Both take model as the first argument.
openaiText(model, config?)#
Creates an OpenAI text adapter against the Responses API (/v1/responses) using OPENAI_API_KEY from the environment.
adapter.inputModalities lists the input kinds of the selected model. It is undefined for a model that this package does not know. See Check what a model accepts.
Parameters:
- model - OpenAI chat model id (e.g. "gpt-5.2", "gpt-4o-mini")
- config?.organization - Organization ID (optional)
- config?.baseURL - Custom base URL (optional)
createOpenaiChat(model, apiKey, config?)#
Creates an OpenAI text adapter (Responses API) with an explicit API key.
openaiChatCompletions(model, config?)#
Creates an OpenAI text adapter that targets /v1/chat/completions instead of the Responses API. See Chat Completions API for when to use this over openaiText.
createOpenaiChatCompletions(model, apiKey, config?)#
Creates an OpenAI chat-completions adapter with an explicit API key.
openaiSummarize(model, config?) / createOpenaiSummarize(model, apiKey, config?)#
Creates an OpenAI summarization adapter.
openaiImage(model, config?) / createOpenaiImage(model, apiKey, config?)#
Creates an OpenAI image generation adapter (DALL-E, gpt-image).
openaiSpeech(model, config?) / createOpenaiSpeech(model, apiKey, config?)#
Creates an OpenAI text-to-speech adapter.
openaiTranscription(model, config?) / createOpenaiTranscription(model, apiKey, config?)#
Creates an OpenAI transcription adapter for Whisper, GPT-4o transcription, and GPT-4o diarized transcription models.
openaiVideo(model, config?) / createOpenaiVideo(model, apiKey, config?)#
Creates an OpenAI video generation adapter (Sora). Experimental.
openaiDecider(model, config?) / createOpenaiDecider(model, apiKey, config?)#
Creates an OpenAI evaluate adapter for decide(). See Evaluate for usage.
openaiRealtime(...) / openaiRealtimeToken(...)#
Realtime voice adapters. See Realtime Voice Chat for usage.
Next Steps#
- Getting Started - Learn the basics
- Tools Guide - Learn about tools
- Other Adapters - Explore other providers
Provider Tools#
OpenAI exposes several native tools beyond user-defined function calls. Import them from @tanstack/ai-openai/tools and pass them into chat({ tools: [...] }).
For the full concept, a comparison matrix, and type-gating details, see Provider Tools.
webSearchTool#
Enables the model to run a web search and return grounded results with citations. Pass a WebSearchToolConfig object (typed from the OpenAI SDK) to configure the tool.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { webSearchTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "What's new in AI this week?" }],
tools: [webSearchTool({ type: "web_search" })],
});Supported models: GPT-4o, GPT-5, and Responses API-capable models. See Provider Tools.
webSearchPreviewTool#
The preview variant of web search with additional options for controlling search context size and user location. Use this when you want fine-grained control over the search context sent to the model.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { webSearchPreviewTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Latest news about TypeScript" }],
tools: [
webSearchPreviewTool({
type: "web_search_preview_2025_03_11",
search_context_size: "high",
}),
],
});Supported models: GPT-4o and above. See Provider Tools.
When the model uses hosted web search, the assistant tool-call part includes metadata.providerExecuted: true and the normalized metadata.sources array. OpenAI's raw search call and URL citations stay under metadata.openai. The agent loop does not execute this call in your application, and the adapter replays the raw response items on the next turn.
fileSearchTool#
Searches OpenAI vector stores that you have pre-populated, letting the model retrieve relevant document chunks. Provide the vector_store_ids to search and optionally limit results with max_num_results (1–50).
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { fileSearchTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "What does the handbook say about PTO?" }],
tools: [
fileSearchTool({
type: "file_search",
vector_store_ids: ["vs_abc123"],
max_num_results: 5,
}),
],
});Supported models: GPT-4o and above. See Provider Tools.
imageGenerationTool#
Allows the model to generate images inline during a conversation using DALL-E/GPT-Image. Pass quality, size, and style options via the config object.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { imageGenerationTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Draw a logo for my app" }],
tools: [
imageGenerationTool({
quality: "high",
size: "1024x1024",
}),
],
});Supported models: GPT-5 and GPT-Image-capable models. See Provider Tools.
codeInterpreterTool#
Gives the model a sandboxed Python execution environment. The container field configures the execution environment; pass the full CodeInterpreterToolConfig object.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { codeInterpreterTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Analyse this CSV and plot a chart" }],
tools: [
codeInterpreterTool({ type: "code_interpreter", container: { type: "auto" } }),
],
});Supported models: GPT-4o and above. See Provider Tools.
mcpTool#
Connects the model to a remote MCP (Model Context Protocol) server, exposing all its capabilities as callable tools. Provide either server_url or connector_id — not both.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { mcpTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "List my GitHub issues" }],
tools: [
mcpTool({
server_url: "https://mcp.example.com",
server_label: "github",
}),
],
});Supported models: GPT-4o and above. See Provider Tools.
computerUseTool#
Lets the model observe a virtual desktop via screenshots and interact with it using keyboard and mouse events. Provide the display dimensions and the execution environment type.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { computerUseTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("computer-use-preview"),
messages: [{ role: "user", content: "Open Chrome and navigate to example.com" }],
tools: [
computerUseTool({
type: "computer_use_preview",
display_width: 1024,
display_height: 768,
environment: "browser",
}),
],
});Supported models: computer-use-preview. See Provider Tools.
localShellTool#
The model returns a local_shell tool call. Your app runs the command. Then your app sends the output back on the next request. localShellTool() takes no arguments. Add it to tools.
Match toolName on TOOL_CALL_START. Read input on the TOOL_CALL_END with the same toolCallId. input.command is the command. input.env is the environment variables.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { localShellTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.6"),
messages: [{ role: "user", content: "Run the test suite and summarise failures" }],
tools: [localShellTool()],
});
let localShellCallId: string | undefined;
for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_START" && chunk.toolName === "local_shell") {
localShellCallId = chunk.toolCallId;
}
if (chunk.type === "TOOL_CALL_END" && chunk.toolCallId === localShellCallId) {
console.log(chunk.input);
}
}Send the command output as the tool result. Use { output: string }, or a string. The next request sends local_shell_call_output.
Supported models: GPT-5.x and other agent-capable models. See Provider Tools.
shellTool#
shellTool() gives the model a shell. A container environment runs on OpenAI. A local environment runs in your app.
When your app must run the commands, pass environment: { type: "local" }. Match toolName on TOOL_CALL_START. Read input on the TOOL_CALL_END with the same toolCallId. input.commands is the command list. input.timeout_ms is the time limit. input.max_output_length is the output limit.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { shellTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.6"),
messages: [{ role: "user", content: "Count lines in all JS files" }],
tools: [shellTool({ environment: { type: "local" } })],
});
let shellCallId: string | undefined;
for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_START" && chunk.toolName === "shell") {
shellCallId = chunk.toolCallId;
}
if (chunk.type === "TOOL_CALL_END" && chunk.toolCallId === shellCallId) {
console.log(chunk.input);
}
}Return one result per command:
const shellResult = {
output: [
{
stdout: "12\n",
stderr: "",
outcome: { type: "exit" as const, exit_code: 0 },
},
],
};outcome.type is "exit" or "timeout". The next request sends shell_call_output. If the same response has no shell_call_output, a shell call with no environment comes back to your app.
A container environment does not come back as a tool call. Pass environment to attach that container and hosted skills.
Supported models: GPT-5.x and other agent-capable models. Responses API only — Chat Completions does not support the shell tool. See Provider Tools.
Attaching hosted skills#
Pass environment.skills to load provider-managed skill bundles into the shell's container (Responses API only).
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { shellTool } from "@tanstack/ai-openai/tools";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [
shellTool({
environment: {
type: "container_auto",
skills: [
{ type: "skill_reference", skill_id: "skill_abc", version: "2" },
],
},
}),
],
});
return toServerSentEventsResponse(stream);
}For the full reference — skill shape, version string format, and the Anthropic equivalent — see Provider Skills.
applyPatchTool#
The model returns an apply_patch tool call. Your app applies the diff. Then your app sends the result back. applyPatchTool() takes no arguments. Add it to tools.
Match toolName on TOOL_CALL_START. Read input on the TOOL_CALL_END with the same toolCallId. input.operation is one file change. operation.type is create_file, update_file, or delete_file. operation.path is the file path. create_file and update_file also include operation.diff.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { applyPatchTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.6"),
messages: [{ role: "user", content: "Fix the import paths in src/index.ts" }],
tools: [applyPatchTool()],
});
let patchCallId: string | undefined;
for await (const chunk of stream) {
if (chunk.type === "TOOL_CALL_START" && chunk.toolName === "apply_patch") {
patchCallId = chunk.toolCallId;
}
if (chunk.type === "TOOL_CALL_END" && chunk.toolCallId === patchCallId) {
console.log(chunk.input);
}
}Send this object as the tool result. The next request sends apply_patch_call_output.
const patchResult = {
status: "completed" as const,
output: "Updated src/index.ts",
};If the patch fails, set status to "failed". Put the error text in output. Keep applyPatchTool() in tools on the next request. A patch-only turn has no text. If you use chat({ stream: false }), text is empty. Read the apply_patch tool call from chunks with the same checks as the loop above.
Supported models: GPT-5.x and other agent-capable models. See Provider Tools.
customTool#
Defines a custom Responses API tool with an explicit name, description, and format. Use this when none of the structured tool types fits your use case. Unlike branded provider tools, customTool returns a plain Tool and is accepted by any chat model.
import { chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { customTool } from "@tanstack/ai-openai/tools";
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages: [{ role: "user", content: "Look up order #1234" }],
tools: [
customTool({
type: "custom",
name: "lookup_order",
description: "Look up the status of a customer order by order ID",
}),
],
});Supported models: all Responses API models. See Provider Tools.