Claude Code for OpenAI Assistants API: Persistent AI Threads — Claude Skills 360 Blog
Blog / Backend / Claude Code for OpenAI Assistants API: Persistent AI Threads
Backend

Claude Code for OpenAI Assistants API: Persistent AI Threads

Published: April 18, 2027
Read time: 8 min read
By: Claude Skills 360

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",
    },
  })
}
// 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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free