Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~27 min· topic 1 of 6

Topic 1.1

The Chat Request and Response

In one line

A chat request is a JSON list of messages, each with a role. The response is JSON too: the answer text, the reason the answer stopped, and token counts. Reading all three fields correctly is what separates a finished answer from a cut-off one.

Think of it like this

Posting a letter to a very patient help desk, with the whole conversation so far copied onto the page, because the desk forgets you the moment the letter is sent. The reply comes back in an envelope with three things: the answer, a note saying why the answer ends here ("finished" or "ran out of space"), and a line saying how much the reply cost. A chat request and response are that letter and that envelope, written in JSON.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

messages
The list of everything said so far, in order. The model answers the last one, using the earlier ones as context.
Role
Who said a message: system (the developer's instructions), user (the visitor) or assistant (the model).
System message
A message from you that sets the model's job and rules. The visitor does not type it.
choices
The list of answers in a response. A chat UI reads choices[0].
finish_reason
Why the model stopped writing: stop (finished), length (hit the limit), content_filter (blocked by safety rules).
usage
Token counts for the request: tokens in, tokens out, and the total.
Token
A small piece of text the model counts in. Roughly a short word or part of a longer one. Billing and limits use tokens.
OpenAI-compatible
A request and response shape that many providers copy, so code written for one often works with another.

Step by step

01The request body

This is everything a chat endpoint needs. The first message is yours and tells the model its job. The second is the visitor's question. The model answers the last message, with the earlier ones as background.

request.jsonwhole filejson
{
  "model": "provider/model-name",
  "stream": false,
  "messages": [
    { "role": "system", "content": "You are Mitra, the help assistant of Chai & Co." },
    { "role": "user", "content": "When will my order ship?" }
  ]
}

02The response body

The text to show is nested three levels down. The first live example prints each part so you can see where it sits. Notice that choices is a list, message is an object with a role (always assistant here) and the content.

response.jsonwhole filejson
{
  "id": "chatcmpl-…",
  "object": "chat.completion",
  "model": "provider/model-name",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Orders ship in 1 to 2 working days." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 42, "completion_tokens": 8, "total_tokens": 50 }
}

03Read it in the right order

Good code reads the response in a fixed order. (1) Was the HTTP status ok? (Topic 1.3.) (2) Is there a choices[0]? (3) Why did the model stop (finish_reason)? (4) Is there text? (5) Only then decide what to show. The second live example runs four different replies through one function that does exactly this.

Read it in the right orderdiagram
Rendering diagram…

04Edge cases: other alphabets, empty lists, bad bodies

The third live example sends a question in Hindi and gets back Hindi text with an emoji. JSON carries any text, so nothing special is needed, but notice how many more prompt tokens a long question uses: a question of 300 repeated words is far larger than a short one, and you pay for it.

The same example sends three broken requests. The endpoint answers each with 400 and a body shaped { error: { message, type } }. That error shape is the other half of the contract, and Topic 1.3 shows how to handle it.

05What the screen needs from all this

Four decisions follow from the fields: what to show for a complete answer, for a cut-off one, for a blocked one and for an empty one. Mitra v1 (Topic 1.6) makes these four decisions in one small function, and every later phase builds on it.

Try it yourself

  1. 1

    Change the order of the messages

    In the first example, add an assistant message between the system and user messages, for instance { role: "assistant", content: "Hello! How can I help?" }, and run again.

    The prompt_tokens number grows (from 42 to 56 with the stand-in's estimate), because every message you send is counted and billed on every request. This is why long conversations cost more with each turn (Topic 4.3).

  2. 2

    Make a reply that is cut off

    In the second example, replace the first reply with { text: "Returns are accepted within", finish: "length" }, and look at the first line of output.

    The stand-in says length and the text stops mid-sentence, with HTTP 200 and no error. Check what the screen function does with it. A page that shows content alone would hide the problem.

  3. 3

    Count characters and tokens

    In the third example, change the long question to "चाय ".repeat(300) and compare the prompt tokens with the English version.

    The prompt tokens go from 233 to 308. The stand-in only counts one token per four characters, and "चाय " is four characters where "Hi " is three, so the rise here is modest. A real tokenizer usually splits Devanagari and other non-Latin scripts into many more pieces per character, so the real rise is much bigger. The lesson: length in characters is not a safe guide to cost, so read the provider's usage.

Code & diagrams

The request and response, field by field

Not a real model: the reply is scripted, and the token counts are estimates from the stand-in. In a real app this endpoint is your provider's, and your own route (Topic 1.5) calls it with a secret key. Try changing the system message and the question: the reply stays the same because it is scripted.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

status: 200 true
content-type: application/json
top-level keys: [ 'id', 'object', 'model', 'choices', 'usage' ]
choices: 1
message: { role: 'assistant', content: 'Orders ship in 1 to 2 working days.' }
finish_reason: stop
usage: { prompt_tokens: 42, completion_tokens: 8, total_tokens: 50 }
the text to show: Orders ship in 1 to 2 working days.
Four replies, four decisions: complete, cut off, blocked, empty

All four replies came back with HTTP 200. Only `finish_reason` and the text tell them apart. The second one stops after the word "and": without the check, a page would show that as a finished answer. The step `finish: "content-filter"` is how the stand-in scripts a blocked answer; a real provider reports it as `content_filter`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

"returns?" → stop "Returns are accepted within 7 days."
   screen: { show: 'Returns are accepted within 7 days.', mark: 'complete' }
"tell me about chai" → length "Our masala chai is black tea blended with cardamom, ginger and"
   screen: { show: 'Our masala chai is black tea blended with cardamom, ginger and…', mark: 'cut off' }
"something blocked" → content_filter ""
   screen: { show: 'Mitra cannot answer that. Try asking in a different way.', mark: 'blocked' }
"something empty" → stop ""
   screen: { show: 'Mitra did not have an answer. Please try again.', mark: 'empty' }
Other alphabets, token counts and broken requests

Text in any script and emoji travel through JSON unchanged. A real endpoint rejects bad bodies the same way: a 400 with an `error` object. The token numbers here are the stand-in's estimate (about one token per four characters of JSON); a real tokenizer gives different numbers, and often more per character for non-Latin text.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Hindi and emoji reply: 200 ☕ चाय को दूध और मसाले के साथ उबाला जाता है। 🙂
empty messages list: 400 { message: '\'messages\' is required and must not be empty.', type: 'invalid_request_error' }
no messages field: 400 { message: '\'messages\' is required and must not be empty.', type: 'invalid_request_error' }
body is not JSON: 400 { message: 'The request body must be JSON.', type: 'invalid_request_error' }
prompt tokens, short question: 8
prompt tokens, long question: 233
Odd but legal replies: null text, a new finish reason, no choices, no usage

Reply 1 has `content: null` because the model asked for a tool (Phase 8): `?? ""` turns it into an empty string so `.trim()` cannot throw. Reply 2 has a finish reason this code has never seen: it still shows the text, and the state tells you to look. Replies 3 and 4 have no `choices` at all, and 1 to 4 have no `usage`: nothing crashes, nothing prints "undefined". This function is the answer to practice question 1.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

0 { text: 'Fine.', state: 'complete' } usage: 7
1 { text: '', state: 'empty' } usage: none
2 { text: 'Hello', state: 'unknown-reason:some_new_reason' } usage: none
3 { text: '', state: 'empty' } usage: none
4 { text: '', state: 'empty' } usage: none

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Read the text from the wrong place

In the first example, replace the line that prints choices with console.log("message:", data.message.content);.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of undefined (reading 'content')

Break #2

Show the text and ignore finish_reason

In the second example, change whatToShow to only return { show: choice.message.content, mark: "complete" }; and run.

terminal
$ Console
── what you'll see ──
screen: { show: 'Our masala chai is black tea blended with cardamom, ginger and', mark: 'complete' }
screen: { show: '', mark: 'complete' }
screen: { show: '', mark: 'complete' }

Myth vs fact

Myth

A response with status 200 is a good answer.

Fact

200 only means the request worked. The answer can be cut off (length), blocked (content_filter) or empty. Check finish_reason and the text.

Myth

choices has one item, so I can skip the list.

Fact

It is a list because a request can ask for several alternatives. Reading choices[0] is right for chat, but check that it exists.

Myth

The model remembers the conversation.

Fact

It remembers nothing between requests. The only memory is the messages list you send, which is why history costs tokens on every turn.

Myth

Token count equals word count.

Fact

A token is a piece of text, often part of a word. Counts differ by language and by model, so use the usage numbers the provider reports.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Treat the provider's JSON as untrusted input to your own code. Even a well-behaved provider can change a field, add a new finish_reason value or omit usage (for example on a gateway that does not report it). Read with optional chaining and give every switch on finish_reason a default branch that shows a safe message.

  • ▸

    Keep the system message on the server (Topic 1.5). A system message in browser code is readable by every visitor, who can then copy your instructions and craft inputs that argue with them. The browser sends the conversation, and the server adds the system message and the model name.

  • ▸

    usage is your best free measurement. Log prompt_tokens and completion_tokens per request on the server from day one (Topic 13.5). A conversation that quietly grows from 300 to 6,000 prompt tokens is the usual cause of a surprise bill, and you can only see it if you recorded the numbers.

Remember this

  1. 1

    The problem this solves: Every provider answers in a shape, and screens that read it carelessly break in quiet ways. A page that only shows the text will show a sentence that stopped in the middle as if it were complete. A page that assumes there is always text will crash on an empty answer. This topic goes through each field of the request and the response, so you know what you can rely on and what you must check.

  2. 2

    The request. You send an HTTP POST with a JSON body. The important field is messages: a list, in order, of everything said so far. Each message has a role and a content. There are three roles you use now: system (instructions from you, the developer: who the assistant is and what it may talk about), user (what the visitor typed), and assistant (what the model said earlier). A fourth role, tool, appears in Phase 8. A request usually also carries the name of the model, whether you want streaming (stream), and optional limits such as the longest answer allowed (the field's name differs between providers).

  3. 3

    The model has no memory. It does not remember your last request. If the visitor asks "And what about returns?", the model only understands "And" if the earlier turns are in messages again. So every request resends the conversation. Topic 4.3 covers what that costs and how to trim it. For now, a conversation is one system message followed by one user message.

  4. 4

    The response (this course uses the OpenAI-compatible shape). Many providers and gateways speak this shape, so one parser works across them. Others, such as Anthropic's and Google's native APIs, use their own shapes, which is one reason Phase 5 introduces an SDK. A non-streamed reply has id, model, a list choices, and usage. The answer lives at choices[0].message.content. choices is a list because a request can ask for several alternative answers; in a chat UI you ask for one and read the first.

  5. 5

    **finish_reason says why the model stopped, and it is not an error.** The HTTP status is still 200. stop means the model reached a natural end: the answer is complete. length means it hit the limit on answer length and was cut off, possibly in the middle of a sentence. content_filter means the provider's safety system stopped or blocked the answer. (A fourth value, tool_calls, means the model wants your code to run a tool: Phase 8.) A screen that ignores finish_reason shows cut-off and blocked answers as if they were finished.

  6. 6

    **usage counts tokens, not words.** prompt_tokens is what you sent, completion_tokens is what the model wrote, total_tokens is their sum. You are billed by these numbers, and the model has a maximum it can handle per request. Tokens are not characters: a short English word is often one token, a long word several, and text in some non-Latin scripts and emoji often takes more tokens per character, so the same sentence can cost more in Hindi or Japanese than in English. The numbers in this course's examples come from a stand-in model that only estimates; a real provider counts with its own tokenizer.

  7. 7

    An empty answer is a valid answer. The response can arrive with 200, a normal shape and content empty (""). Some providers send no text at all when the model asked for a tool instead. A safety filter can also leave nothing to show. When the model asks for a tool, content can even be null (not a string), and null.trim() throws. Treat content as possibly empty, null or missing, and decide what the screen says in that case, because "nothing" on screen looks like a bug.

Explain it without notes

01

What does a chat request contain, and why is the whole conversation sent every time?

02

A response arrives with HTTP 200. List three reasons the answer might still be unusable on screen.

03

What does finish_reason: "length" mean, and what should the UI do about it?

04

What does usage report, and why can it differ from a character count?

Practice

01

Write a function readReply(data) that returns { text, state } where state is "complete", "cut-off", "blocked" or "empty", and never throws, even if data has no choices.

02

Add a line under each answer in the first example that shows usage.total_tokens as Used 50 tokens, and hides the line when usage is missing.

Trade-offs

  • ↔

    Reading the whole response at once (this phase) is simple and lets you validate everything before showing it, at the cost of the visitor waiting for the full answer. Streaming (Phase 2) shows text sooner but makes validation harder.

  • ↔

    Following the OpenAI-compatible shape lets one parser work against many providers, but providers differ in details such as the name of the length-limit field and how they report errors, so keep the parsing in one small function you can adjust.

Done when you can

  • I can write a chat request body with a system and a user message.

  • I can find the answer text, the finish reason and the token counts in a response.

  • I can name the three finish reasons and say what each means for the screen.

  • I can show a sensible message for a cut-off, a blocked and an empty answer.

  • I can explain why each request carries the whole conversation.