← Blog

How to Convert OpenAI Code to Claude API

2026-09-27 · 5 min read · SubToAPI Team

If you already have an app built against OpenAI's chat completions API and want to add or switch to Claude, you don't need a rewrite — you need to remap a handful of concepts. The two APIs are conceptually similar (send messages, get a message back, optionally stream or call tools), but the request shape, response shape, and streaming format are different enough that a straight find-and-replace won't work.

This guide walks through the actual differences you'll hit converting OpenAI code to Claude's API, with before/after examples for the parts that trip people up: system prompts, response parsing, streaming chunks, and function/tool calling.

The core mapping

At a high level:

| OpenAI | Claude | |---|---| | POST /v1/chat/completions | POST /v1/messages | | Authorization: Bearer sk-... | x-api-key: sk-ant-... (or Authorization: Bearer via a gateway) | | messages: [{role: "system", ...}] | top-level system field | | choices[0].message.content (string) | content (array of blocks, e.g. {type: "text", text: "..."}) | | functions / tools with function_call | tools with tool_use content blocks | | finish_reason | stop_reason | | SSE data: {"choices":[{"delta":...}]} | SSE with named events: content_block_delta, message_stop, etc. |

If you're going through SubToAPI instead of Anthropic directly, the shape of requests and responses follows the same Messages API structure — you just point at https://api.subtoapi.app/v1/messages and authenticate with Authorization: Bearer $SUBTOAPI_KEY. That matters for this conversion because it means everything below applies whether you're calling Claude directly or through SubToAPI.

Step 1: move the system message out of the array

OpenAI treats the system prompt as just another message with role: "system". Claude pulls it out into a dedicated system parameter.

OpenAI:

{
  "model": "gpt-4o",
  "messages": [
    {"role": "system", "content": "You are a concise support agent."},
    {"role": "user", "content": "How do I reset my password?"}
  ]
}

Claude:

{
  "model": "claude-sonnet-4-5",
  "system": "You are a concise support agent.",
  "max_tokens": 1024,
  "messages": [
    {"role": "user", "content": "How do I reset my password?"}
  ]
}

Two things to note: Claude requires max_tokens, and it doesn't accept a system role inside the messages array — if your code loops over a stored message history and just appends a system message, that logic needs a branch to redirect it into the system field instead. See /docs/messages for the full request schema.

Step 2: parse content as an array, not a string

This is the most common bug when converting. OpenAI's response gives you a plain string:

const reply = response.choices[0].message.content;

Claude returns a content array because a single response can mix text, tool calls, and (with extended thinking) reasoning blocks:

const reply = response.content
  .filter(block => block.type === "text")
  .map(block => block.text)
  .join("");

If you're just doing plain text chat with no tools, this is usually a one-line change, but it's easy to miss if choices[0].message.content is buried deep in a helper function or a response formatter.

Step 3: rewrite streaming handlers

OpenAI's stream is a flat sequence of chunks, each with a delta.content string. Claude's stream is event-typed SSE — you get message_start, content_block_start, content_block_delta, content_block_stop, message_delta, and message_stop events, and you only care about content_block_delta for plain text output.

OpenAI-style handler:

for await (const chunk of stream) {
  const token = chunk.choices[0]?.delta?.content || "";
  process.stdout.write(token);
}

Claude-style handler:

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

This one is worth testing carefully — it's the part most likely to silently produce empty output if you forget to check event.type first. Full event reference is in /docs/streaming.

Step 4: remap function calling to tools

OpenAI's functions/tools + function_call pattern maps to Claude's tools + tool_use content blocks. The tool definition schema is nearly identical (name, description, JSON Schema parameters/input_schema), but the response shape differs:

OpenAI: the model returns tool_calls on the message object.

Claude: the model returns a content block with type: "tool_use", containing id, name, and input. You send the result back as a user message with a tool_result content block referencing that id:

{
  "role": "user",
  "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01A...", "content": "72°F, sunny"}
  ]
}

If your app has a tool-calling loop, expect to rewrite the "check if the model wants a tool" branch and the "send the result back" branch. Everything else — your actual tool implementations — stays the same. See /docs/tools for worked examples.

Step 5: check usage and error fields

OpenAI's usage object has prompt_tokens/completion_tokens; Claude uses input_tokens/output_tokens. If you log token counts for billing or rate limiting, update the field names. Error responses also differ in shape — Claude wraps errors as {"type": "error", "error": {"type": "...", "message": "..."}}, so any error-parsing middleware needs a matching branch.

A practical migration order

  1. Swap the endpoint and auth header.
  2. Move system prompts out of messages.
  3. Fix response parsing (content array instead of string).
  4. Fix streaming event handling.
  5. Rewrite the tool-calling loop if you use function calling.
  6. Update usage/error field names in logging and billing code.
  7. Re-run your existing test suite against Claude and diff outputs manually for a handful of prompts — behavior differences (verbosity, refusal patterns) are separate from format differences and worth checking early.

If you're setting this up for a team rather than a single script, it's worth putting the Claude access behind a proper API layer from the start — SubToAPI gives you per-application sub_live_... keys, usage metadata per key, and seat-based team access on top of the same Messages API, so the conversion work above only has to happen once. Start with /docs/quickstart, or check /pricing if you're evaluating plans for a team rollout.

FAQ

Does Claude support the OpenAI SDK directly? No — the request and response shapes are different enough (system field, content arrays, streaming events) that you need to use Anthropic's Messages API format or a client library built for it, not the OpenAI SDK unmodified.

What's the biggest source of bugs when converting? Response parsing. Code that assumes message.content is a string breaks silently or throws when it receives Claude's content array instead, especially in streaming handlers.

Do I need to rewrite my tool/function definitions? Mostly no — the JSON Schema for parameters carries over almost unchanged. What changes is how the model signals a tool call (tool_use block instead of function_call) and how you return results (tool_result block).

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →