How LLM APIs work
Every feature in this course starts with one HTTP call to a language model. This lesson opens that call up: what goes in, what comes back, why the bill grows with every message, and how to keep the call fast and reliable when thousands of customers ask at once. By the end you will have a small, provider-neutral wrapper that the rest of the course builds on.
By the end you can
- Read a chat-style request and response: system instructions, role-tagged messages, parameters, stop reason and token usage
- Plan a context budget that shares room between instructions, history, product data and the answer, and trim history when it runs out
- Choose sampling settings for extraction and for writing, and explain why temperature 0 is still not a promise of identical output
- Stream answers with server-sent events, and retry rate limits and server errors with backoff and jitter
- Work out the cost per question, per 1,000 questions and per month for a real feature
The everyday picture
Imagine you hire a brilliant phone interpreter. She speaks every language, but she has two odd rules. First, she forgets everything the moment you hang up. If you call back with "and what about the second question?", she has no idea what the first one was, so you have to read the whole conversation to her again before asking. Second, she bills by the word: every word you read to her, and every word she says back, and her own words cost several times more than yours.
She also has habits you learn to work with. She starts talking after a short pause, and you hear her answer as she speaks it, so you can start listening before she finishes. When her line is busy, the operator says "try again later", and if a hundred callers all redial at the same second, the line stays busy. A language model API behaves the same way: it remembers nothing between calls, it counts and bills tokens both ways, it can stream its answer, and it pushes back when too many calls arrive at once.
What one call looks like
Most model providers offer a chat-style API: you send one HTTPS request and get one response. The field names differ by provider, but the parts are the same everywhere. The request carries system instructions (the rules: who the assistant serves, what it may say, what format to use), a list of messages, each tagged with a role (user for the customer, assistant for the model's earlier replies), and parameters such as the model name, the most tokens the answer may use, and sampling settings. Some APIs take the system instructions as a separate field; others take them as the first message with a system role.
Here is the first raw call Nadia's team made for the product-question assistant in the Mangrove app. A customer is looking at a granola and asks about nuts:
{
"model": "your-model-id",
"system": "You answer Mangrove Market customers' questions about one product.\nUse ONLY the product facts below. If they do not answer the question, say so.\n\nProduct facts:\nOat Crunch Granola 500 g. Ingredients: oats, honey, almonds (8%), sunflower oil.\nAllergens: contains nuts (almonds). Made in a factory that also handles peanuts.",
"messages": [
{ "role": "user", "content": "Does this granola have nuts? My son is allergic." }
],
"max_tokens": 300,
"temperature": 0.2
}The response carries three things your code must read, not only the text:
{
"content": [{ "type": "text", "text": "Yes. Oat Crunch Granola contains almonds, and it is made in a factory that also handles peanuts. It is not safe for a nut allergy." }],
"stop_reason": "end_turn",
"usage": { "input_tokens": 1540, "output_tokens": 38 }
}The text is the answer. The stop reason says why the model stopped: it finished, it ran out of output tokens, or (in Tools and the Model Context Protocol) it wants to call a tool. The usage block counts input and output tokens, which is exactly what you are billed for. This example follows one provider's documented shape; another returns a list of choices, each with a finish_reason. Your wrapper maps whichever shape you get into one of your own, so the rest of the app never cares.
The most important fact about the API is that it is stateless. The model keeps nothing between calls. When the customer follows up with "what about the honey one?", the app must send the system instructions, the first question, the first answer and the new question again. A conversation is a list your app keeps and resends, longer each turn. So the bill for one turn grows with the length of the conversation so far.
Quick check
A customer asks about the granola, then asks "is it also gluten-free?". The model answers about a different product. What is the most likely bug?
The API keeps no memory at all, short or long, so the follow-up only makes sense if the app resends the earlier turns and the product facts. Temperature changes word choice; it cannot make the model know which product you mean if you never told it.
Tokens and the context budget
Models read and write tokens, pieces of words chosen by the model's tokenizer. In English, one token is roughly 4 characters, or about three quarters of a word, so 1,000 words is roughly 1,300 tokens. That rule of thumb breaks for other scripts. Text in Bengali, Hindi, Thai or Amharic often needs several times more tokens for the same meaning, because tokenizers were built mostly from English-heavy text. The ratio depends on the tokenizer, so never plan with the English rule. Take a sample of real customer questions in each language you serve, send them, and read the usage numbers (or use your provider's token-counting endpoint, if it has one).
The context window is the most the model can handle in one call. Current models range from tens of thousands of tokens to more than a million, depending on the model. Think of it as a budget shared by four users: your instructions, the conversation history, any data you attach (product facts now, retrieved help pages in Retrieval-augmented generation), and the answer itself. The window is the hard limit, but your own budget should usually be far smaller. Every input token is paid on every call, a longer input makes the first word slower to arrive, and models tend to use a key fact less reliably when it is buried in a very long input.
The answer has its own, separate limit: max output tokens. You set it on every request, and models also have a ceiling of their own, usually much smaller than the window. If input plus the output you ask for does not fit in the window, depending on the provider the request is rejected or the answer is cut short. If the model reaches your max output tokens, it stops mid-sentence, and the stop reason says so (length in one API, max_tokens in another). A truncated sentence looks bad. Truncated JSON breaks the app. Check the stop reason on every call before you use the text.
When history outgrows its share, you have three choices. You can drop the oldest turns (cheap and simple, and what you will write in the Practice section), summarise old turns into a short note with an extra model call, or start a fresh conversation. For a product-question assistant, dropping old turns is usually fine: questions about the granola rarely need what was said twenty messages ago. Always keep the system instructions, and never cut a turn in half, because an answer without its question confuses the model.
Quick check
The assistant must return a small JSON object, and about 1 call in 50 fails to parse. The logs show those calls all have the stop reason for hitting the output limit. What should you do first?
The model ran out of room mid-object, so the JSON is incomplete by construction. Temperature does not change the output limit, and retrying the same request with the same limit will often be cut off again, at full cost each time.
Sampling: temperature and top-p
A model writes one token at a time. At each step it computes a probability for every token it knows, then samples one. After "Oat Crunch Granola contains", the token "almonds" might get 70%, "oats" 20%, and thousands of others share the rest. Two settings shape that choice.
Temperature rescales the probabilities before sampling. Low temperature sharpens them, so the top token wins almost every time. High temperature flattens them, so less likely tokens get picked more often, which reads as more varied and sometimes more creative, and also more often wrong. Top-p cuts the tail instead: with top-p 0.9, the model keeps only the most likely tokens that together make up 90% of the probability and samples among them. Ranges differ by provider (temperature is 0 to 1 in some APIs and 0 to 2 in others), providers usually advise changing one of the two settings and leaving the other alone, and some newer models fix these settings or reject them, so check the model's documentation.
Pick settings by task. For extraction and classification, such as pulling allergens out of a supplier's spec sheet or labelling a question as "delivery" or "product", use temperature 0 or close to it: you want the most likely reading every time. For writing, such as a friendly reply or three recipe ideas for leftover rice, a moderate temperature gives natural, varied wording. For answers that must be correct, low settings help but are never enough on their own; the checks in Prompts and structured output and the evals in Evals: knowing if it got better do that job.
Temperature 0 is not a promise of identical output. At temperature 0 the model picks the top token, but the probabilities it computes can differ slightly from run to run. Providers process many requests together in batches, and the batch size changes the order of floating-point arithmetic, which can flip a near-tie between two tokens. One flipped token changes everything written after it. Providers also update models and serving software behind the same name. So never write a test that expects the exact same sentence twice, and never cache an answer on the assumption that a re-run would match it.
Quick check
Tomás wants the model to read 25,000 supplier spec sheets and fill an allergens list for each product. Which setting fits?
Extraction wants the most likely reading, so a low temperature fits. It reduces variation but promises neither identical runs nor correct answers, so a sampled check is still needed. A high temperature adds randomness, which is the opposite of what a safety-critical list needs.
Streaming, timeouts and retries
Two numbers describe how fast an answer feels. Time to first token (TTFT) covers the network, any queue at the provider and the model reading your input; longer inputs make it longer. After that, output arrives at some tens of tokens per second, depending on the model and how busy the provider is. In Mangrove's first test the TTFT was about 0.8 seconds and output came at about 40 tokens per second, so a 200-token answer took about 0.8 + 200 ÷ 40 = 5.8 seconds in total.
Without streaming, the customer stares at a spinner for all 5.8 seconds. With streaming, the provider sends the answer in small pieces as it writes them, using server-sent events (SSE): the response stays open and carries lines starting with data:, each event ended by a blank line. The first words appear after 0.8 seconds. The total time is the same; the wait feels far shorter, because reading starts almost at once.
Streaming has costs. Structured output is not usable until it is complete: half a JSON object cannot be parsed, so the app either waits for the end or uses a parser that handles partial input. Checks on the whole answer, such as "does this allergen answer match the product facts?", cannot run before the customer has already read the first sentences. For answers where a wrong word is dangerous, buffer the reply, check it, then show it. And usage arrives at the end of the stream, if at all; some providers include it only when you ask, so make sure you still log it.
Calls also fail. Providers enforce rate limits, usually requests per minute and tokens per minute, and answer 429 Too Many Requests when you go over, often with a retry-after header saying how long to wait (Rate limiting explains the algorithms behind them). Servers also return 5xx errors when overloaded, and a call can hang. The rules:
- Set a timeout on every call, so a hung connection does not hold a customer's request forever.
- Retry only what can succeed next time: 429, 5xx, timeouts and dropped connections. A 400 (bad request, input too long) fails the same way every time.
- Back off exponentially, with jitter. Wait about 0.5, 1, 2, then 4 seconds, picking a random time up to that limit, and stop after a few attempts. Without jitter, every app server that hit the limit at 20:00:00 retries at exactly 20:00:01, and the limit is hit again. Respect
retry-afterwhen it is present. - Make retries safe. A model call changes nothing on the provider's side except your bill and your limits, so retrying it is idempotent for data, but not free. A call that timed out may have finished and been billed anyway. The danger is in your own code: if the handler saves the answer or counts the customer's daily questions before the call succeeds, a retry does it twice. Key that work by a request ID for the question. And once streamed text is on screen, do not retry silently; a second attempt writes different words.
Quick check
Which failures should callModel retry automatically?
Those are the failures that can succeed a moment later. A 400 fails identically every time, so the fix belongs in code (trim the history deliberately and log it), and retrying it only adds cost. A 429 asks you to slow down for a moment, and it often says how long to wait.
What a call really costs
Providers price input and output tokens separately, per million tokens, and output usually costs several times more than input. Prices change often and differ by model, so this lesson uses illustrative prices only: say $3 per million input tokens and $15 per million output tokens for a large hosted model, and $0.25 and $1.25 for a small fast model. Check your provider's current price list before you plan with real numbers.
cost of one call = input tokens x input price + output tokens x output price
Turn 1 (1,540 in, 200 out): 1,540 x $3/M + 200 x $15/M = $0.00462 + $0.00300 = $0.00762
Three-question conversation: input 1,540 + 1,780 + 2,020 = 5,340 tokens, output 600
5,340 x $3/M + 600 x $15/M = $0.01602 + $0.00900 = $0.02502
Per question (average): $0.02502 / 3 = $0.00834
Per 1,000 questions: about $8.34Three things stand out. Most of the input is tokens the customer never sees: rules and product facts, paid on every turn. Output is a small share of the tokens but a large share of the money, because each output token costs five times an input token here. And history makes later turns dearer, so a long chat costs much more per question than a short one.
Add what the simple formula leaves out: retries (a timed-out call may be billed), tool rounds in Tools and the Model Context Protocol (each round is a full new call), eval runs before every release, and festival peaks. Discounts for cached prompt prefixes and cheaper models for easy questions come in Cost, latency and safety. For the product manager's view of the same numbers, see How AI features work, for PMs; for the big picture of an AI feature with retrieval and evals, see Designing AI features.
Build it
A provider-neutral callModel wrapper with a timeout, retries with backoff and jitter, and a usage and cost log, plus a streaming version that reads server-sent events. Everything provider-specific sits in one marked block.
-
Define your own types and prices
The rest of the app only ever sees these shapes. Prices are integer cents per million tokens, so tokens × price is an exact integer in millionths of a cent; round only when you add up a bill.
TypeScript// src/assistant/llm.ts: the only file that knows which provider you use export interface Message { role: 'user' | 'assistant'; content: string } export interface CallInput { system: string; messages: Message[]; maxOutputTokens: number; temperature?: number } export interface Usage { inputTokens: number; outputTokens: number } export interface CallResult { text: string; stopReason: 'end' | 'length' | 'other'; usage: Usage; costMicrocents: number; // millionths of a cent: 1,000,000 = 1 cent ms: number; } // ILLUSTRATIVE prices in cents per million tokens ($3 in, $15 out). Use your provider's price list. const PRICE = { inputCentsPerM: 300, outputCentsPerM: 1500 }; export const costMicrocents = (u: Usage) => u.inputTokens * PRICE.inputCentsPerM + u.outputTokens * PRICE.outputCentsPerM; const LENGTH_STOPS = new Set(['length', 'max_tokens']); // the name differs by provider const toStop = (raw?: string): CallResult['stopReason'] => !raw ? 'other' : LENGTH_STOPS.has(raw) ? 'length' : ['stop', 'end_turn'].includes(raw) ? 'end' : 'other'; -
Isolate the provider-specific shape
The endpoint, auth header, body fields and response paths differ by provider. These functions follow one provider's documented shape as an example; map yours from its API reference and change nothing else.
TypeScript// ---------- SHAPE DIFFERS BY PROVIDER: adapt this block only ---------- const ENDPOINT = process.env.LLM_URL!; // your provider's chat endpoint const headers = () => ({ 'content-type': 'application/json', authorization: `Bearer ${process.env.LLM_KEY}`, // some providers use a different header }); const toProviderBody = (i: CallInput, stream: boolean) => ({ model: process.env.LLM_MODEL, system: i.system, // some APIs want this as the first message instead messages: i.messages, max_tokens: i.maxOutputTokens, // field name differs by provider temperature: i.temperature ?? 0.2, stream, }); const fromProviderResponse = (j: any) => ({ text: (j.content ?? []).map((p: any) => p.text ?? '').join(''), rawStop: j.stop_reason as string | undefined, usage: { inputTokens: j.usage?.input_tokens ?? 0, outputTokens: j.usage?.output_tokens ?? 0 }, }); const fromProviderEvent = (e: any) => ({ // one streamed event text: e.delta?.text as string | undefined, rawStop: e.delta?.stop_reason as string | undefined, inputTokens: e.message?.usage?.input_tokens as number | undefined, outputTokens: e.usage?.output_tokens as number | undefined, }); // ---------- end of provider-specific block ---------- -
Call with a timeout, retries, backoff with jitter, and a log line
TypeScriptconst RETRYABLE = new Set([408, 429, 500, 502, 503, 504]); // add your provider's "overloaded" code const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function callModel(input: CallInput, timeoutMs = 20_000, maxAttempts = 4): Promise<CallResult> { for (let attempt = 1; ; attempt++) { const started = Date.now(); const res = await fetch(ENDPOINT, { method: 'POST', headers: headers(), body: JSON.stringify(toProviderBody(input, false)), signal: AbortSignal.timeout(timeoutMs), // never wait forever }).catch((e: Error) => e); // timeout or dropped connection const status = res instanceof Error ? 0 : res.status; if (status === 200) { const r = fromProviderResponse(await (res as Response).json()); const result: CallResult = { text: r.text, stopReason: toStop(r.rawStop), usage: r.usage, costMicrocents: costMicrocents(r.usage), ms: Date.now() - started }; console.log(JSON.stringify({ event: 'llm_call', attempt, ...r.usage, costMicrocents: result.costMicrocents, ms: result.ms, stop: result.stopReason })); return result; // the caller must check stopReason } if ((status !== 0 && !RETRYABLE.has(status)) || attempt >= maxAttempts) { throw new Error(`model call failed: ${status || (res as Error).name} after ${attempt} attempt(s)`); } const retryAfterS = res instanceof Error ? 0 : Number(res.headers.get('retry-after')); const cap = Math.min(8_000, 500 * 2 ** (attempt - 1)); // 0.5 s, 1 s, 2 s, 4 s ... const waitMs = retryAfterS > 0 ? retryAfterS * 1000 : Math.random() * cap; // full jitter console.log(JSON.stringify({ event: 'llm_retry', attempt, status, waitMs: Math.round(waitMs) })); await sleep(waitMs); } }Output{"event":"llm_retry","attempt":1,"status":429,"waitMs":2000} {"event":"llm_call","attempt":2,"inputTokens":1540,"outputTokens":38,"costMicrocents":519000,"ms":1710,"stop":"end"}519,000 millionths of a cent is about half a cent. Ship these log lines to your log store (Observability), and you can chart cost per day, tokens per question and retries per hour from them.
-
Stream the answer from server-sent events
Raw SSE from a provider looks like this:
data:lines with one JSON event each, separated by blank lines. Event names and fields differ by provider, and one provider ends the stream withdata: [DONE].Outputdata: {"type":"message_start","message":{"usage":{"input_tokens":1540,"output_tokens":1}}} data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"Yes. Oat Crunch"}} data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":38}}TypeScriptexport async function streamModel(input: CallInput, onText: (piece: string) => void, timeoutMs = 60_000) { const started = Date.now(); const res = await fetch(ENDPOINT, { method: 'POST', headers: headers(), body: JSON.stringify(toProviderBody(input, true)), signal: AbortSignal.timeout(timeoutMs) }); if (!res.ok || !res.body) throw new Error(`stream failed: ${res.status}`); // safe to retry: nothing shown yet const reader = res.body.pipeThrough(new TextDecoderStream()).getReader(); const usage: Usage = { inputTokens: 0, outputTokens: 0 }; let buf = '', text = '', rawStop: string | undefined, ttftMs = -1; for (;;) { const { value, done } = await reader.read(); if (done) break; buf += value.replace(/\r/g, ''); let cut: number; while ((cut = buf.indexOf('\n\n')) >= 0) { // a blank line ends one event const block = buf.slice(0, cut); buf = buf.slice(cut + 2); const data = block.split('\n').filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).join('\n'); if (!data || data === '[DONE]') continue; const e = fromProviderEvent(JSON.parse(data)); if (e.text) { if (ttftMs < 0) ttftMs = Date.now() - started; // time to first token text += e.text; onText(e.text); // push to the app as it arrives } if (e.rawStop) rawStop = e.rawStop; if (e.inputTokens) usage.inputTokens = e.inputTokens; if (e.outputTokens) usage.outputTokens = e.outputTokens; } } const ms = Date.now() - started; console.log(JSON.stringify({ event: 'llm_stream', ...usage, costMicrocents: costMicrocents(usage), ttftMs, ms, tokensPerSecond: ttftMs < ms ? Math.round(usage.outputTokens / ((ms - ttftMs) / 1000)) : null, stop: toStop(rawStop) })); return { text, stopReason: toStop(rawStop), usage }; }Your API passes each piece on to the Mangrove app over its own SSE response. If the stream breaks after text was shown, send the app an error event and let the customer ask again; a silent retry would write different words under the ones already on screen.
-
Use it, and check the stop reason
TypeScriptconst reply = await callModel({ system: rulesAndProductFacts, messages: history, maxOutputTokens: 600, temperature: 0.2 }); if (reply.stopReason === 'length') { // Never show or parse a cut-off answer. Log it, and fall back to a safe message. return { text: 'Sorry, I could not answer that fully. Please check the product label or ask our support team.' }; } return { text: reply.text };
Bring it to your team
Ask these before the first AI call ships, and again before a festival season.
- Where is our one wrapper around the model API, and does every call go through it?
- What is our token budget per call, and what happens to history when it runs out?
- Do we check the stop reason on every call before showing or parsing the answer?
- Which errors do we retry, with what backoff, and how many attempts before we give up?
- What is our cost per 1,000 questions, from measured usage, and what volume assumption sits behind the monthly number?
- What is our tokens-per-minute limit, and will it hold at 4× traffic on a festival day?
Red flags
- Token counts estimated with the English rule for customers who write in other scripts.
- Retries with a fixed delay and no jitter, or retries on 400 errors.
- No usage logging, so nobody knows the cost until the invoice arrives.
- Tests that expect the exact same answer twice "because temperature is 0".
- Provider SDK calls scattered across the codebase instead of one wrapper.
Practice
Trim the history to fit the budget
Write trimHistory(messages, budgetTokens, estimateTokens). messages[0] is the system message, and the last message is the customer's new question. estimateTokens(text) returns a token count for a string. Return a new array that always keeps the system message and the new question, then adds earlier messages from newest to oldest while the total still fits in budgetTokens, stopping at the first one that does not fit. Keep the original order. The kept history must not start with an assistant message (an answer cut off from its question), so drop any assistant messages at the start of what you kept. Do not change the input array.
// messages: [{ role: 'system' | 'user' | 'assistant', content: '...' }]
// messages[0] is the system message; the last one is the new question.
function trimHistory(messages, budgetTokens, estimateTokens) {
return messages;
}
const est = (text) => Math.ceil(text.length / 4);
const msg = (role, tokens) => ({ role, content: 'x'.repeat(tokens * 4) });
const chat = [msg('system', 100), msg('user', 50), msg('assistant', 100), msg('user', 50), msg('assistant', 100), msg('user', 50)];
test('keeps everything when it fits', () => {
expect(trimHistory(chat, 1000, est)).toEqual(chat);
});
test('keeps the system message and the newest turns, dropping the oldest', () => {
expect(trimHistory(chat, 300, est)).toEqual([chat[0], chat[3], chat[4], chat[5]]);
});
test('never starts the history with an answer cut off from its question', () => {
expect(trimHistory(chat, 260, est)).toEqual([chat[0], chat[5]]);
});
test('always keeps the system message and the new question, even over budget', () => {
expect(trimHistory(chat, 120, est)).toEqual([chat[0], chat[5]]);
});
test('does not change the input array', () => {
const copy = JSON.parse(JSON.stringify(chat));
trimHistory(chat, 260, est);
expect(chat).toEqual(copy);
});
Show a solution
function trimHistory(messages, budgetTokens, estimateTokens) {
const system = messages[0];
const question = messages[messages.length - 1];
const cost = (m) => estimateTokens(m.content);
let used = cost(system) + cost(question); // these two are always sent
const kept = [question];
for (let i = messages.length - 2; i >= 1; i--) { // newest earlier message first
const c = cost(messages[i]);
if (used + c > budgetTokens) break; // stop at the first that does not fit
used += c;
kept.unshift(messages[i]);
}
while (kept.length > 1 && kept[0].role === 'assistant') kept.shift(); // no orphan answers
return [system, ...kept];
}Budget the festival month
Nadia needs the assistant's cost for a festival month. Use the illustrative prices ($3 per million input tokens, $15 per million output). A conversation averages 3 questions, 5,340 input tokens and 600 output tokens in total. On 26 normal days there are about 6,700 conversations a day; on 4 festival days traffic runs at 4×. Work out the cost per conversation, the month's total, and name two things that could make the real bill higher than your number.
- Cost per conversation is about 2.5 cents (5,340 × $3/M + 600 × $15/M)
- The month has 26 × 6,700 + 4 × 26,800 = 281,400 conversations, costing about $7,000
- You name at least two real reasons the bill could be higher, such as longer chats, retries that were billed, more customers than assumed, or other scripts using more tokens
- You state the volume as an assumption, not a fact
Show a model answer
Per conversation: 5,340 x $3/M + 600 x $15/M = $0.01602 + $0.00900 = $0.02502
Normal days: 26 x 6,700 = 174,200 conversations
Festival days: 4 x 6,700 x 4 = 107,200 conversations
Month: 281,400 x $0.02502 = about $7,040About $7,000, against about $5,000 in a normal month. The volume is an assumption, since the assistant has no history yet. The bill could be higher if festival chats run longer (each extra turn resends the whole history), if timeouts during the rush cause retries that were billed anyway, if more customers than 5% try it when it is promoted, or if more questions arrive in scripts that need more tokens. A daily spending alert catches all four early.
Check yourself
With the illustrative prices ($3 per million input, $15 per million output), Nadia proposes cutting the system rules from 1,000 to 500 tokens, and Leila proposes cutting the average answer from 200 to 100 tokens. Which saves more on a first question?
500 × $3/M = $0.0015 and 100 × $15/M = $0.0015. Counting tokens alone favours Nadia, and assuming output always dominates favours Leila; the price ratio decides. In longer chats both savings grow, because both the rules and old answers are resent every turn.
In a test, the assistant's time to first token is 0.8 seconds and it writes about 40 tokens a second. Answers average 200 tokens. Leila asks whether streaming will make answers arrive sooner. What is the accurate reply?
Streaming changes when the customer sees the first words. The model writes at the same speed: 0.8 + 200 ÷ 40 = 5.8 seconds either way. The model still writes token by token, and temperature has nothing to do with delivery.
On a festival evening, Mangrove's 12 API servers all hit the provider's rate limit at the same moment. Each retries after exactly 1, 2 and 4 seconds, and errors keep spiking in waves. What fixes the waves?
Every server waits the same time, so they all retry together and hit the limit together again. Jitter spreads the retries out, retry-after tells clients when capacity returns, and a cap stops endless load. A fixed delay keeps the servers in step, and a longer timeout does not touch the rate limit.
Flashcards
Key takeaways
- A call is system instructions, role-tagged messages and parameters in; text, a stop reason and token usage out. Read all three.
- The API is stateless: the app resends the conversation every turn, so cost per question grows with history.
- Tokens are the unit of price, limits and speed. The window is a budget shared with the answer; measure tokens with real text in every language.
- Low temperature for extraction, moderate for writing, and no promise of identical output even at 0.
- Stream for perceived speed, time out every call, retry 429 and 5xx with backoff and jitter, and log usage and cost on every call.
Sources
- What are tokens and how to count them, OpenAI Help Center, read in 2026
- Using server-sent events, MDN Web Docs, read in 2026
- API errors, Claude API documentation (429, 5xx and retry advice), read in 2026
- Exponential Backoff And Jitter, Marc Brooker, AWS Architecture Blog, 2015
- The Curious Case of Neural Text Degeneration (top-p, nucleus sampling), Holtzman et al., 2019
- Defeating Nondeterminism in LLM Inference, Horace He, Thinking Machines Lab, 2025