Skip to content

Chat Completions and Responses ​

Open the lesson playground and you'll find two buttons, one for each of OpenAI's two API shapes: Chat Completions and Responses. You'll meet both names again in other people's code and in the provider's own documentation, usually with no explanation of why there are two.

They do the same job. Both send instructions and a user message to a model and get a reply back. What differs is where you put each piece of the request, and where the reply text turns up afterwards, and that second difference is the one that bites when you're copying a snippet from somewhere.

Those playground buttons call routes on the Express server, the small Node program the course project runs alongside the page, so that model requests happen away from browser code and your API key is never exposed. In Scrimba, output from those server routes appears in the Runner tab instead of the Console tab.

Both examples below use a client built the same way, from the values you saved in Provider setup:

js
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.AI_KEY,
  baseURL: process.env.AI_URL,
});

Chat Completions ​

Chat Completions was the first stable, widely adopted API for language models, and other providers implemented the same shape, which is why it turns up in so many code samples. It places the system prompt and user input together in messages:

js
const response = await client.chat.completions.create({
  model: process.env.AI_MODEL,
  messages: [
    { role: "system", content: "You are a helpful assistant." },
    {
      role: "user",
      content: "Give me a short explanation of why open-source tools matter.",
    },
  ],
});

The reply text is nested under the first choice:

js
response.choices[0].message.content
JunoChat Completions Everything goes into one messages list: the system prompt first, then your question. The reply comes back at response.choices[0].message.content.

That path looks fussy the first few times you type it. Read it slowly once, because you'll see it in almost every code sample online.

JunoChat Completions One array carries the whole request, with role deciding what each entry is for. The reply sits under choices[0] because the API can return several alternative completions in one call, and you almost always want the first.

Recognizing that path is useful in itself: it tells you at a glance that a snippet you found is Chat Completions rather than Responses.

JunoChat Completionschoices is a plural that is almost always one. It exists for the n parameter, which asks for several independent completions of the same request, and the shape keeps it even though most code leaves n at 1.

That explains the ergonomics you're about to trade away. Every read costs an array index that carries no information, and every turn you add to a conversation goes back into the same flat list by hand.

Responses ​

Responses gives the system prompt its own instructions field. For a simple request, input can be a direct string:

js
const response = await client.responses.create({
  model: process.env.AI_MODEL,
  instructions: "You are a helpful assistant.",
  input: "Give me a short explanation of why open-source tools matter.",
});

The same direct string can be moved into a role/content message object when you need the message form:

js
input: [
  {
    role: "user",
    content: "Give me a short explanation of why open-source tools matter.",
  },
],

Responses exposes the reply text directly:

js
response.output_text

It also keeps the complete response structure in response.output. A basic text response usually contains a message item whose content includes the same output text.

JunoResponses The system prompt gets its own instructions field instead of sharing the list, and input can be a plain string when you're only asking one thing.

The reply is at response.output_text, one step away instead of three. If the Responses button prints a reply in the Runner tab, your provider and model handle this API.

JunoResponsesinstructions keeps the system prompt out of the conversation, so you aren't rebuilding an array every turn to keep it at the front, and output_text gives you the text directly.

input still takes the array-of-messages form when you need it, which is what conversation history uses later in the course. The string form is a shortcut for the simple case, not a different API.

JunoResponsesoutput_text is an SDK helper over output, which is the real return value: a list of typed items rather than a single message. A turn that calls tools returns tool-call items in that list, and a single content string would have nowhere to put them.

So read output_text for a final answer and output whenever you need to know what the model actually did.

Why the course uses Responses ​

Chat Completions is still a supported API, and the agent functionality in this course could be built with it.

The course uses Responses because of the difference you can already see in the two code blocks above. Reading a reply is response.output_text instead of response.choices[0].message.content, and the system prompt has a named home instead of being the first item in an array you also append user turns to. Once tool calls and conversation history start accumulating, that shape means less code holding the pieces together.

OpenRouter supports the Responses API too, with one difference from OpenAI: it doesn't store conversation state on its servers. That doesn't affect the course, which keeps its own conversation history in code.

Use the full output when you need the structure

output_text is the convenient way to read a final text reply. Use output when you need to inspect the complete set of response items.

JunoWhy the course uses Responses Both APIs can do everything this course needs. Responses is the one with less to type: the reply is one step away and the system prompt has a field of its own.

You don't need to memorize the differences. Use Responses here, and recognize Chat Completions when you meet it elsewhere.

JunoWhy the course uses Responses The choice is about how much glue code you write around the call. Chat Completions is still supported and plenty of production code uses it, so if you inherit a codebase built on it, nothing here says it needs rewriting.

If you're on OpenRouter, the missing server-side state is why the course stores its own history. Every request carries the conversation it needs.

JunoWhy the course uses Responses The difference compounds rather than showing up in the first request. In a tool-calling loop you append the model's turn, then the tool results, then call again, and Chat Completions has you rebuild that flat array each time while keeping the system message pinned at the front.

Responses models a turn as items, which is the shape the loop actually has. The two toy examples above can't show that, which is why it's worth knowing before you meet it.

Where this goes next ​

Running the code locally explains how the browser and Express server connect after you download a lesson from Scrimba. If you haven't picked a model yet, Recommended models covers what agent work requires.

JunoWhere this goes next If both buttons print a reply in the Runner tab, your setup works and you can carry on with the course. The local guide is only for when you want the same project on your own computer.
JunoWhere this goes next The button-to-route setup here is the same one you'll run locally: the page sends a request to /api, Vite passes it to Express, and the model call happens on the server.
JunoWhere this goes next A model that fails the Responses request here won't get far in the course, since every lesson after this one builds on that API. If it fails, check the model against the recommended list before anything else.