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 the same client, built once 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 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 buried at response.choices[0].message.content.

That path looks fussy the first few times you type it. It is worth reading slowly once, because you will 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.

You will basically always want the first, so choices[0].message.content becomes muscle memory. Recognising that path is useful even here, because it tells you at a glance that a snippet you found is Chat Completions rather than Responses.

JunoChat Completionschoices is a plural that almost never is one. It dates from the completion API's n parameter, which asks for several independent samples in one request, and it survives in the response shape long after most people stopped using it.

Worth knowing because it explains the ergonomics you are about to trade away. Every read costs you an array index that carries no information, and every tool-call turn you append has to be rebuilt 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.

Best part: the reply is at response.output_text. One step instead of three.

JunoResponses Two practical differences. instructions separates the system prompt from the conversation, so you are not 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 and tool results use later in the course. The string form is a shortcut for the simple case, not a different API.

JunoResponsesoutput_text is a convenience over output, which is the real return value: a list of typed items rather than a single message. That structure is the point, because a turn that calls tools returns tool-call items alongside any text, and a flat content string has nowhere to put them.

So read output_text for a final answer and output whenever you need to know what the model actually did. The moment you add your first tool, the second one is where you will be looking.

Why the course uses Responses

Chat Completions is still a valid 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.

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 instead of three, and the system prompt has a field of its own.

You don't need to memorise the differences. Use Responses here, and recognise 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. With Responses the system prompt has a named home, so it stops being an array element you have to keep at position zero, and the reply is one property deep.

Chat Completions is not deprecated and plenty of production code uses it. If you inherit a codebase built on it, nothing here needs rewriting.

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 makes you reconstruct that whole flat array yourself 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. That is the real argument, and it is invisible in the two toy examples above, which is why it is worth saying out loud 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.