The OpenAI Assistants API maintains persistent conversation threads with AI memory — client.beta.assistants.create({ model, instructions, tools }) creates a reusable assistant. client.beta.threads.create() creates a conversation thread. client.beta.threads.messages.create(threadId, { role: "user", content }) adds a message. client.beta.threads.runs.create(threadId, { assistant_id }) starts a run. Runs go through queued → in_progress → completed states. client.beta.threads.runs.poll(threadId, runId) waits for completion. requires_action status with submit_tool_outputs handles function calls. File Search with vector stores enables document retrieval. Code Interpreter executes Python for data analysis. Streaming via client.beta.threads.runs.stream() provides real-time token streaming. Claude Code generates assistant creation, thread management, tool call handling, vector store setup, and streaming response patterns.
CLAUDE.md for OpenAI Assistants
## OpenAI Assistants Stack
- Version: openai >= 4.49 (Node.js SDK)
- Create: client.beta.assistants.create({ model: "gpt-4-turbo", instructions, tools: [{ type: "code_interpreter" }] })
- Thread: const thread = await client.beta.threads.create()
- Message: await client.beta.threads.messages.create(threadId, { role: "user", content })
- Run: const run = await client.beta.threads.runs.createAndPoll(threadId, { assistant_id })
- Stream: const stream = client.beta.threads.runs.stream(threadId, { assistant_id })
- Tool: if (run.status === "requires_action") → submit tool outputs
- Files: client.beta.vector_stores.create() + file_search tool for RAG
- Poll: createAndPoll() — convenience method that polls until terminal state
Assistant Setup
// lib/openai/assistant.ts — create and cache assistant
import OpenAI from "openai"
export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
// Function tools the assistant can call
const TOOLS: OpenAI.Beta.AssistantTool[] = [
{
type: "function",
function: {
name: "get_order_status",
description: "Get the current status and details of a customer order by order ID",
parameters: {
type: "object",
properties: {
order_id: {
type: "string",
description: "The order ID (e.g. ORD-12345)",
},
},
required: ["order_id"],
},
},
},
{
type: "function",
function: {
name: "search_products",
description: "Search the product catalog for items matching a query",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
category: { type: "string", description: "Optional product category filter" },
max_price: { type: "number", description: "Maximum price in USD" },
},
required: ["query"],
},
},
},
{ type: "code_interpreter" },
{ type: "file_search" },
]
// Cache assistant ID in env to avoid recreating each time
let cachedAssistantId: string | null = process.env.OPENAI_ASSISTANT_ID ?? null
export async function getOrCreateAssistant(): Promise<string> {
if (cachedAssistantId) return cachedAssistantId
const assistant = await openai.beta.assistants.create({
model: "gpt-4-turbo-preview",
name: "Customer Support Assistant",
instructions: `You are a helpful customer support assistant for an e-commerce platform.
You have access to:
- Order status lookup via get_order_status function
- Product catalog search via search_products function
- Code interpreter for analyzing purchase data
- Knowledge base files for product manuals and policies
Guidelines:
- Always verify order IDs before looking them up
- Be concise and helpful
- If you cannot help, offer to connect the customer to a human agent
- Never make up order details — only report what the function returns`,
tools: TOOLS,
})
cachedAssistantId = assistant.id
console.log(`Created assistant: ${assistant.id}`)
return assistant.id
}
Thread and Message Management
// lib/openai/chat.ts — conversation management
import OpenAI from "openai"
import { openai, getOrCreateAssistant } from "./assistant"
import { db } from "@/lib/db"
// Tool implementations
async function executeToolCall(
toolName: string,
args: Record<string, unknown>,
): Promise<string> {
switch (toolName) {
case "get_order_status": {
const order = await db.order.findUnique({
where: { id: args.order_id as string },
include: { items: true, shipping: true },
})
if (!order) return JSON.stringify({ error: "Order not found" })
return JSON.stringify({
id: order.id,
status: order.status,
createdAt: order.createdAt,
items: order.items.map(i => ({ name: i.name, quantity: i.quantity })),
tracking: order.shipping?.trackingNumber ?? null,
estimatedDelivery: order.shipping?.estimatedDelivery ?? null,
})
}
case "search_products": {
const products = await db.product.findMany({
where: {
name: { contains: args.query as string, mode: "insensitive" },
...(args.category && { category: args.category as string }),
...(args.max_price && { priceCents: { lte: Math.round((args.max_price as number) * 100) } }),
active: true,
},
take: 5,
select: { id: true, name: true, priceCents: true, description: true, imageUrl: true },
})
return JSON.stringify(products.map(p => ({ ...p, price: p.priceCents / 100 })))
}
default:
return JSON.stringify({ error: `Unknown tool: ${toolName}` })
}
}
// Send a message and get a response
export async function chat(params: {
threadId: string
message: string
userId: string
}): Promise<{ reply: string; threadId: string }> {
const assistantId = await getOrCreateAssistant()
// Add user message to thread
await openai.beta.threads.messages.create(params.threadId, {
role: "user",
content: params.message,
})
// Run and poll — handles tool calls automatically
let run = await openai.beta.threads.runs.createAndPoll(params.threadId, {
assistant_id: assistantId,
additional_instructions: `Current user ID: ${params.userId}. Current time: ${new Date().toISOString()}.`,
})
// Handle tool calls if required
while (run.status === "requires_action") {
const toolCalls = run.required_action!.submit_tool_outputs.tool_calls
const toolOutputs = await Promise.all(
toolCalls.map(async toolCall => ({
tool_call_id: toolCall.id,
output: await executeToolCall(
toolCall.function.name,
JSON.parse(toolCall.function.arguments),
),
})),
)
run = await openai.beta.threads.runs.submitToolOutputsAndPoll(
params.threadId,
run.id,
{ tool_outputs: toolOutputs },
)
}
if (run.status !== "completed") {
throw new Error(`Run failed with status: ${run.status}`)
}
// Get the assistant's last message
const messages = await openai.beta.threads.messages.list(params.threadId, {
order: "desc",
limit: 1,
})
const reply = messages.data[0]?.content
.filter(c => c.type === "text")
.map(c => (c as OpenAI.Beta.Threads.TextContentBlock).text.value)
.join("\n") ?? "I could not generate a response."
return { reply, threadId: params.threadId }
}
// Create a new thread
export async function createThread(): Promise<string> {
const thread = await openai.beta.threads.create()
return thread.id
}
Streaming API Route
// app/api/chat/stream/route.ts — streaming assistant responses
import { NextRequest } from "next/server"
import { openai, getOrCreateAssistant } from "@/lib/openai/assistant"
import { auth } from "@clerk/nextjs/server"
export async function POST(request: NextRequest) {
const { userId } = await auth()
if (!userId) return new Response("Unauthorized", { status: 401 })
const { threadId, message } = await request.json()
const assistantId = await getOrCreateAssistant()
// Add user message
await openai.beta.threads.messages.create(threadId, {
role: "user",
content: message,
})
// Create a readable stream for SSE
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const runStream = openai.beta.threads.runs.stream(threadId, {
assistant_id: assistantId,
})
try {
for await (const event of runStream) {
if (event.event === "thread.message.delta") {
const delta = event.data.delta
const text = delta.content
?.filter(c => c.type === "text")
.map(c => (c as any).text?.value ?? "")
.join("") ?? ""
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`))
}
}
if (event.event === "thread.run.completed") {
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
}
if (event.event === "thread.run.failed") {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: "Run failed" })}\n\n`))
}
}
} finally {
controller.close()
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
})
}
Vector Store for File Search
// lib/openai/knowledge-base.ts — RAG with File Search
import { openai } from "./assistant"
import fs from "fs"
export async function createKnowledgeBase(name: string): Promise<string> {
const vectorStore = await openai.beta.vector_stores.create({ name })
return vectorStore.id
}
export async function addFileToKnowledgeBase(
vectorStoreId: string,
filePath: string,
fileName: string,
) {
const file = await openai.files.create({
file: fs.createReadStream(filePath),
purpose: "assistants",
})
await openai.beta.vector_stores.files.create(vectorStoreId, {
file_id: file.id,
})
return file.id
}
// Attach vector store to assistant for file search
export async function attachKnowledgeBase(
assistantId: string,
vectorStoreId: string,
) {
await openai.beta.assistants.update(assistantId, {
tool_resources: {
file_search: { vector_store_ids: [vectorStoreId] },
},
})
}
For the Vercel AI SDK alternative when a simpler streaming chat interface with useChat, streamText, and built-in multi-provider support (OpenAI, Anthropic, Google) is preferred over the Assistants API’s thread persistence model — the Vercel AI SDK suits stateless or session-based chat without server-side thread storage, see the Vercel AI SDK guide. For the LangChain alternative when multi-step agent chains, custom tool integration, memory, and complex RAG pipelines with LangGraph orchestration are needed — LangChain provides a higher-level abstraction over the raw OpenAI API for complex agentic workflows, see the LangChain guide. The Claude Skills 360 bundle includes OpenAI Assistants skill sets covering threads, tool calls, and file search. Start with the free tier to try AI assistant generation.