Dayward AI
Week 1 · D3About 4 hours

The Responses API and Built-in Tools: Function Calling, Web Search / File Search / Computer Use, Structured Output

Look one layer below a finished product like Codex: how the Responses API organizes the model, tools, and multi-turn state, and exactly how it differs from Chat Completions.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Explain the three differences between the Responses API and Chat Completions in input shape, output shape, and state management
  2. Write a minimal script with function calling and web search, and correctly feed function_call_output back in
  3. Get structured output with strict json_schema mode, and handle the refusal branch

For two days you used the finished product. Today we open a layer: what the interface products like Codex use to talk to the model looks like, how tools get wired in, and where multi-turn state lives. Once you have read this and finished the work, scroll back to the top and tick off the three goals.

Plain-Language Walkthrough

From messages to items: more than a field rename

Still the two contractors. You sent a job to each firm, and the two firms use different work-order formats. The old format (Chat Completions) is a chat transcript: messages one after another, each labeled with who said it. The new format (the Responses API) is more like a work log: it holds not only what was said but what was looked up, which tool was called, and what the tool returned, each as a typed entry. OpenAI calls the latter items — the input is an array of items, the output is an array of items, and a message is only one of the types.

Three differences are worth getting exactly right, because they decide how you write code:

  1. Input shape: Chat Completions takes a messages array with the system prompt as its first entry; Responses takes input (either a string or an array of items), with system-level instruction in a separate top-level instructions field.
  2. Output shape: Chat Completions returns choices and you read choices[0].message.content; Responses returns an output array holding, in order, everything the model did this turn (tool calls, searches, messages), plus an output_text helper in the SDK to get the final text directly.
  3. State management: Chat Completions is stateless and you resend the history every turn; Responses defaults store to true, saving this response so the next turn only needs previous_response_id and no resent history. You can also turn storage off and maintain the history yourself — the last section covers that trade-off.

The minimal call looks like this:

hello.ts
import OpenAI from 'openai'
 
const client = new OpenAI() // reads the OPENAI_API_KEY environment variable
 
const response = await client.responses.create({
  model: process.env.OPENAI_MODEL ?? '',
  instructions: 'You are an assistant for a TODO API project. Keep answers short.',
  input: 'Explain what input validation is in one sentence',
})
 
console.log(response.output_text)

Read that alongside the Chat Completions request from D1 of the 30-day course: messages became input plus instructions, and choices[0].message.content became output_text. This course always reads the model id from the OPENAI_MODEL environment variable — OpenAI's model names change quickly, hard-coding one in the prose expires within months, and the current recommendation in the official documentation is the authority.

Why change the format at all? Because the chat-transcript model of Chat Completions cannot hold tools. Once the model is not merely replying but calling tools, searching the web, and reading files, forcing those actions into "messages" gets awkward. Items give every kind of action its own type, which is the precondition for the built-in tools of the next few sections to slot in cleanly.

Function calling: declare, request, feed back

When the contractor needs something from your internal systems, they cannot fetch it themselves — you fetch it and tell them the result. Function calling is that round trip: you declare which functions can be called and what their parameters look like; the model, when it judges it necessary, requests a call with arguments; your code actually executes it and feeds the result back, and the model continues from there.

The three steps correspond to three kinds of item. The declaration is an entry in the request's tools array with type function, carrying name, description, parameters as a JSON Schema, and strict: true to require the arguments match the schema exactly. The model's request is a function_call item in the output, carrying call_id (the id of this call) and arguments (a JSON string). Feeding back means placing a function_call_output item in the next turn's input, with a matching call_id and the result as output.

tools.ts
import OpenAI from 'openai'
 
const client = new OpenAI()
const model = process.env.OPENAI_MODEL ?? ''
 
const todos = [{ id: 1, title: 'Add validation to POST /todos', done: false }]
 
const tools: OpenAI.Responses.Tool[] = [
  {
    type: 'function',
    name: 'list_todos',
    description: 'List all current TODOs',
    parameters: { type: 'object', properties: {}, additionalProperties: false },
    strict: true,
  },
]
 
const first = await client.responses.create({
  model,
  tools,
  input: 'What is still unfinished?',
})
 
// Find the function calls the model requested, actually execute them, then feed the results back
const calls = first.output.filter((item) => item.type === 'function_call')
const outputs = calls.map((call) => ({
  type: 'function_call_output' as const,
  call_id: call.call_id,
  output: JSON.stringify(todos.filter((t) => !t.done)),
}))
 
const second = await client.responses.create({
  model,
  tools,
  previous_response_id: first.id, // continue from the last turn; no history to resend
  input: outputs,
})
 
console.log(second.output_text)

Note two engineering details. First, call_id must match verbatim, since that is how the model pairs a result with a request; three functions requested in one turn means three results fed back. Second, strict: true and additionalProperties: false are a pair — the former makes the model's arguments conform to the schema, the latter forbids extra fields; drop either and you have to handle malformed arguments defensively in your code. What those two lines buy you is an argument-parsing layer that needs essentially no try/catch.

Look back at D1: Codex reading files and running commands is this same round trip underneath, except its tools are the shell and the file system, and it turns the loop itself. The twenty-odd-line script you now have in hand is one iteration of a minimal agent loop.

Built-in tools: web search and file search are run for you by the platform

The functions of the last section are executed by you. Some tools are so universal that OpenAI simply executes them server-side, and you only declare them in tools — those are the built-in tools. They are declared in the same array as function tools, differing in type, and they need nothing fed back.

web search: { type: 'web_search' }. The model searches on its own when it judges it necessary, the output gains a web_search_call item recording what it searched for, and the final message item carries annotations in which each url_citation points at a source. search_context_size (low / medium / high) controls how much page content is pulled into the context, and filters.allowed_domains restricts it to certain sites — very practical for question answering over an enterprise's own knowledge.

file search: upload files into a vector store first, then declare { type: 'file_search', vector_store_ids: ['...'], max_num_results: 3 }. The model retrieves from that store when it needs to, the output gains a file_search_call item, and the annotations in the message are file_citation. Adding include: ['file_search_call.results'] to the request returns the retrieved passages as well, which helps when debugging what it actually saw. This is essentially a hosted RAG: the pipeline hand-built in D12 of the 30-day course compressed into one field.

search.ts
import OpenAI from 'openai'
 
const client = new OpenAI()
 
const response = await client.responses.create({
  model: process.env.OPENAI_MODEL ?? '',
  tools: [{ type: 'web_search', search_context_size: 'low' }],
  input: 'Which libraries does the community commonly use for request body validation in Express? Cite sources.',
})
 
console.log(response.output_text)
 
// List the cited sources separately: annotations live in the content of the message item
for (const item of response.output) {
  if (item.type !== 'message') continue
  for (const part of item.content) {
    if (part.type !== 'output_text') continue
    for (const a of part.annotations) {
      if (a.type === 'url_citation') console.log('source:', a.url)
    }
  }
}

How do you choose between a built-in tool and a function of your own? The rule: data that lives outside and is generic (the public web, documents you uploaded) goes to a built-in tool, and data that lives inside your system (databases, internal services, business logic) gets a function. Built-in tools spare you the execution and the feeding back, but you do not control how they search or what they find; function tools require you to write everything, but every step is in your hands. Production systems almost always mix the two.

Computer use: letting the model operate an interface, and why it belongs in isolation

One step further: what if the task is "open this admin console and change the status of these tickets," there is no API to call, and all that exists is a web interface? That is what computer use addresses — letting the model see screenshots, decide where to click, and decide what to type. The documentation currently offers two routes: have the model write code to drive the interface (generating a Playwright script, say, executed in an isolated environment that persists across calls), or use the { type: 'computer' } computer tool, where the model returns structured actions (click, type, scroll, keypress, screenshot, and so on), your code executes them, and you send the new screenshot back, round and round.

No code for this today, because it does not belong in a minimal script. What the documentation stresses repeatedly is this: confine it to an isolated browser or virtual machine, and give it an allowlist of sites and actions. A model that can click any button and type any text cannot run on your everyday desktop — it might click Delete All, and it might be induced by a stretch of text on a page to do something else entirely (which is prompt injection in its interface-operating form). So the right starting point for computer use is not code but the environment: a throwaway container, a restricted account, an allowlist.

It is covered here so you can see the full spectrum of tools: from the safest web search (read-only public web), through file search (read-only over the files you supplied), through function calling (your code, your control of the side effects), to computer use (the model producing side effects directly). The further right, the more capable, and the heavier the isolation required. The Codex sandbox from D1 is the isolation solution for the "let the model run a shell" cell of that spectrum.

Structured output: strict mode, the parse helper, and refusal

Back to the contractor. You ask for a progress report and get a page of prose from which you still have to dig the numbers out — better to hand them a form to fill in. Structured output is that form: you supply a JSON Schema and the model's output is guaranteed to match it.

In the Responses API it goes in the text.format field: { type: 'json_schema', name, schema, strict: true }. strict is the key — with it on, the output is guaranteed to validate against the schema rather than being merely very likely valid JSON. Both SDKs offer helpers so you write less schema: in TypeScript, zodTextFormat converts a zod schema; in Python, you pass a pydantic model directly. Then use responses.parse instead of create and the result arrives already parsed in output_parsed.

structured.ts
import OpenAI from 'openai'
import { z } from 'zod'
import { zodTextFormat } from 'openai/helpers/zod'
 
const client = new OpenAI()
 
const Review = z.object({
  verdict: z.enum(['approve', 'request_changes']),
  issues: z.array(z.object({ file: z.string(), line: z.number(), note: z.string() })),
})
 
const response = await client.responses.parse({
  model: process.env.OPENAI_MODEL ?? '',
  input: 'Review this diff: … (elided)',
  text: { format: zodTextFormat(Review, 'review') },
})
 
// Check for a refusal first: refusal is its own content type, not a parse failure
const refused = response.output
  .filter((item) => item.type === 'message')
  .flatMap((item) => item.content)
  .find((part) => part.type === 'refusal')
 
if (refused) {
  console.log('the model refused:', refused.refusal)
} else {
  console.log(response.output_parsed?.verdict, response.output_parsed?.issues.length)
}

That refusal branch is easy to overlook. When the model declines for safety reasons it does not force invalid JSON on you but returns a content block of type refusal. Ignore it and your code receives a response with an empty output_parsed and explodes downstream; handle it and you can distinguish "the model was unwilling" from "the model got it wrong" — which is mandatory in automated review or classification pipelines.

Strict mode solves whether the format is right; it does not solve whether the content is right. The schema guarantees verdict is one of two enum values but guarantees nothing about the judgment being correct. Interviewers often press on this: structured output removes the defensive code in your parsing layer, and removes not one line of validation from your business layer.

Multi-turn state: previous_response_id or carrying the history yourself

One last question: where does the history of a multi-turn conversation live? Responses offers two routes.

Let the server store it: store: true by default saves every response, so the next turn continues with previous_response_id and the request body carries only what is new. The upside is small requests and simple code, which is how the function calling example above was chained. The cost is that the state lives elsewhere: rewinding to turn three means remembering each turn's id; auditing the full conversation means fetching it separately; and where compliance requires data not to leave a jurisdiction, this route is closed.

Maintain it yourself: with store: false, every turn resends the complete items array (including the earlier function_call and function_call_output) as input. That is the carry-your-own-history approach from D1 of the 30-day course: cost grows with turns, but every byte is in your hands, and you can compress it, truncate it, or replay it against a different model.

Which one? Prototypes and internal tools take the first for convenience; user-facing production systems mostly take the second, or a hybrid — chaining short runs with previous_response_id while persisting a copy for audit and recovery. Tomorrow's Agents SDK packages this choice as sessions, and you will see the same trade-off under a new shell.

Source Reading

Hands-On Lab

🧪 D3 lab: a minimal Responses API script with built-in tools

Code location: labs/codex-mastery/day-03-responses-tools

Acceptance criteria:

  1. MOCK=1 pnpm start "what is still unfinished" shows one complete function-calling round trip: the model requests list_todos, the script feeds the result back, and the TODO title appears in the final answer.
  2. With --search, the answer is followed by at least one url_citation source (a fixed fake source under MOCK).
  3. In --structured mode the output is a parsed object, and deliberately feeding content that gets refused prints "the model refused" rather than a stack trace.
  4. Dropping MOCK and running once with a real key shows that the second turn chained by previous_response_id needs no resent history.

The code is in labs/codex-mastery/day-03-responses-tools, with four exercise points cut out of starter/ and complete answers in solution/. This is a one-shot script rather than a REPL, so pass arguments directly and do not pipe input into it.

  1. Run MOCK=1 pnpm start "hello" and watch the simplest responses.create round trip print output_text.
  2. Do exercises 1 and 2: declare the list_todos function tool, filter function_call out of the output, execute it and feed the result back as function_call_output, then run MOCK=1 pnpm start "what is still unfinished" and see the TODO title in the final answer.
  3. Do exercise 3: with --search, add web_search to tools and print the url_citation entries separately out of the message annotations.
  4. Do exercise 4: with --structured, switch to responses.parse and zodTextFormat, checking refusal before reading output_parsed.
  5. Configure OPENAI_API_KEY and OPENAI_MODEL, run once without MOCK, and compare whether the shape of the mock output matches the real one.

Interview Questions

Today's 3 questions are in the question bank below, weighted toward the trade-off between the Responses API and Chat Completions, the boundary between built-in tools and custom functions, and the reliability of structured output. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets.

Checklist and Tomorrow

  • Explain the three differences between the Responses API and Chat Completions in input shape, output shape, and state management
  • Write a minimal script with function calling and web search, and correctly feed function_call_output back in
  • Get structured output with strict json_schema mode, and handle the refusal branch
  • Order web search, file search, function calling, and computer use by "more capability, heavier isolation" and justify it
  • All 4 acceptance criteria of the lab pass
  • Answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D4) we fold today's hand-written loop into a few lines of API with the official Agents SDK: an Agent object holds the instructions and the tools, and a run function turns the loop for you. Then we take up three things today did not touch — how several agents hand off to each other (handoffs), how to put guardrails at the entry and the exit (guardrails), and how to hand multi-turn memory to the SDK to store (sessions). Hand-writing before using the SDK is a deliberate order: you already know what the function_call and function_call_output round trip is, so when the SDK hides them tomorrow you will know what got hidden and which layer to look in when something breaks.

Interview questions

  • How does the Responses API differ from Chat Completions, and what are the common pitfalls when migrating?Responses API 和 Chat Completions 的区别是什么?从 Chat Completions 迁移过去最容易踩什么坑?
    Common in ChinaCommon overseasBasic#responses-api#openai#migration

    How to reason about it · think before answering

    1. This tests whether you have actually migrated code, not whether you can recite field names.
    2. Split into three axes: input shape (messages array becomes input plus top-level instructions), output shape (choices becomes typed output items with an output_text helper), and state (stateless becomes store by default plus previous_response_id).
    3. Explain the motivation: a chat-transcript model cannot hold tool actions; items give search, function calls and their outputs distinct types, which is what makes built-in tools possible.
    4. Name three pitfalls: store defaults to true so compliance-sensitive apps must disable it; output is an array, so read output_text or walk message items; tool results move from role tool messages to function_call_output items keyed by call_id.
    5. Expect the follow-up: previous_response_id versus self-managed history? Prototypes take the former; production usually keeps its own history for audit and recovery, or mixes both.

    分析过程 · 先想清楚再作答

    1. 这题考的是你有没有真迁移过,而不是能不能背出字段名。只答「新接口更强」会被判为看过文档没写过代码。
    2. 拆成三个维度:输入形态(messages 数组变成 input 加顶层 instructions)、输出形态(choices 变成按类型排列的 output items,SDK 给 output_text 助手)、状态管理(无状态变成默认 store 加 previous_response_id)。
    3. 再说为什么要改:聊天记录模型装不下工具动作;items 让搜索、函数调用、回填各有自己的类型,这是内置工具能接进来的前提。
    4. 迁移坑给三条:默认 store 为 true 意味着数据会被存下来,合规场景要显式关掉;output 是数组不是单个消息,取文本要用 output_text 或遍历 message item;函数调用的回填从 role 为 tool 的消息变成 function_call_output item,call_id 要对上。
    5. 可预期的追问:previous_response_id 和自己维护历史怎么选?原型用前者省事,生产多半自己落一份历史做审计与恢复,或两者混用。

    Key points

    • Input: messages become input plus top-level instructions; output: choices become typed output items plus output_text
    • State: store defaults to true and previous_response_id chains turns without resending history
    • The motivation is distinct item types for tool actions, enabling built-in tools
    • Pitfalls: store on by default, output is an array, tool results go back as function_call_output keyed by call_id

    答题要点

    • 输入:messages 变 input 加顶层 instructions;输出:choices 变按类型排列的 output items 与 output_text
    • 状态:默认 store 为 true,用 previous_response_id 接上一轮,不再每轮重发历史
    • 改的动机是给工具动作独立的 item 类型,内置工具由此接入
    • 迁移坑:store 默认开、output 是数组、回填要用 function_call_output 且 call_id 对上
  • When do you use platform built-in tools (web search, file search, computer use) versus your own function tools, and why does computer use deserve special treatment?平台内置的工具(web search、file search、computer use)和自己写的函数工具,各适合什么场景?为什么 computer use 要单独对待?
    Common in ChinaCommon overseasIntermediate#tools#responses-api#security

    How to reason about it · think before answering

    1. Two cruxes: who executes the tool, and how large its side effects are; comparing features alone signals no production experience.
    2. Executor test: built-in tools run server-side, you declare but never fill results and cannot steer the search; function tools run in your code, more work but full control.
    3. Map to scenarios: external, generic data (the web, your uploaded documents) fits built-ins; data inside your systems (databases, internal services, business logic) needs functions; production mixes both.
    4. Order by side effects: web search reads the public web, file search reads your files, function calls have whatever side effects your code allows, computer use lets the model act directly; more capability demands heavier isolation.
    5. Computer use is special because it can click anything, type anything and be steered by on-screen content, so the starting point is an isolated environment, a restricted account and an allow-list, not code.
    6. Expect the follow-up: built-in file search versus your own RAG? The built-in is a managed pipeline that skips chunking, embedding and retrieval work at the cost of control and observability; build your own when you need custom chunking or reranking.

    分析过程 · 先想清楚再作答

    1. 题眼有两个:一是「谁来执行」,二是「副作用有多大」。只答功能对比不谈执行方与风险,就是没做过工程。
    2. 先给执行方的判据:内置工具由平台在服务端执行,你只声明、不回填、也控制不了它怎么搜;函数工具由你执行,样样自己写,但每一步都在你手里。
    3. 落到场景:数据在外面且通用(公网、你上传的文档)用内置工具;数据在你系统里(数据库、内部服务、业务逻辑)写函数;生产系统几乎总是混用。
    4. 再按副作用排一条光谱:web search 只读公网,file search 只读你给的文件,函数调用的副作用由你的代码决定,computer use 由模型直接产生副作用——越往右能力越强,需要的隔离越重。
    5. computer use 单独对待的原因:它能点任何按钮、输任何文字,还可能被页面内容诱导,所以正确起点是隔离环境、受限账号和站点与动作白名单,不是代码。
    6. 可预期的追问:内置的 file search 和自己搭 RAG 怎么选?前者是托管版,省掉切分、向量化、检索三步,代价是可控性与可观测性弱,需要自定义切分或重排时才自己搭。

    Key points

    • Built-ins run on the platform with no result filling and no steering; functions run in your code with full control
    • External generic data suits built-ins, in-system data and business logic need functions, production mixes both
    • Rank by side effects: web search, file search, function calls, computer use; more power needs more isolation
    • Computer use starts with an isolated environment and an allow-list, not with code

    答题要点

    • 内置工具由平台执行、不用回填、不可干预;函数工具由你执行、全部可控
    • 外部通用数据用内置工具,系统内数据与业务逻辑写函数,生产混用
    • 按副作用排序:web search、file search、函数调用、computer use,能力越强隔离越重
    • computer use 的起点是隔离环境与白名单,不是代码
  • What does strict mode in structured outputs solve, what does it not solve, and what must your code still do after receiving the output?结构化输出的 strict 模式解决了什么问题,没解决什么问题?拿到输出之后代码里还要做什么?
    Common in ChinaCommon overseasIntermediate#structured-output#responses-api#validation

    How to reason about it · think before answering

    1. This tests the distinction between well-formed and correct; claiming strict mode removes the need for validation is the classic mistake.
    2. What it solves: strict plus json_schema guarantees the output validates against the schema, so enums are always allowed values, required fields exist and types are right; parsing-layer try/catch and retries can largely go.
    3. What it does not solve: semantics. The verdict is one of two enums but may be wrong; a number is a number but may be invented. Business validation stays.
    4. Then refusals: a safety refusal comes back as a refusal content block, not malformed JSON; unhandled, downstream code crashes on an empty parse, handled, you can separate unwilling from incorrect.
    5. Give the order in code: check refusal, read output_parsed, run business checks (ranges, referenced entities exist, consistency with context), then persist or act.
    6. Expect the follow-up: schema restrictions under strict? Every object needs additionalProperties false and all fields in required, optional fields become nullable; these constraints are exactly what makes the guarantee possible.

    分析过程 · 先想清楚再作答

    1. 这题考的是对「格式正确」与「内容正确」的区分,答成「有了 strict 就不用校验了」是典型的错误。
    2. 先说解决了什么:strict 加 json_schema 保证输出一定能通过 schema 校验——枚举只会是给定值、必填字段一定在、类型不会错,解析层的 try/catch 与重试基本可以删掉。
    3. 再说没解决什么:schema 管不了语义。verdict 一定是两个枚举之一,但判断可能是错的;数字一定是数字,但可能是编的。业务层校验一行不能省。
    4. 然后是拒答分支:模型因安全原因拒绝时返回 refusal 类型的内容块,而不是硬塞一个不合法的 JSON;不处理它,下游会拿到空的解析结果直接崩,处理了才能区分「不愿意」与「没做对」。
    5. 给出代码里的顺序:先查 refusal,再读 output_parsed,再做业务校验(范围、引用是否存在、与上下文是否一致),最后才落库或执行。
    6. 可预期的追问:strict 对 schema 有什么限制?每个对象都要 additionalProperties 为 false、字段都要在 required 里,可选字段用可空类型表达;这些限制正是它能给出保证的原因。

    Key points

    • Solves: output is guaranteed to match the schema, so parsing defenses can go
    • Does not solve: semantic correctness, so business validation stays
    • Check refusal first, then output_parsed, then business checks, then persist
    • Strict requires additionalProperties false and all fields required, optional fields become nullable

    答题要点

    • 解决:输出保证符合 schema,解析层防御代码可以删
    • 没解决:语义正确性,业务校验一行不能省
    • 先查 refusal 再读 output_parsed,再做业务校验,最后落库
    • strict 要求 additionalProperties 为 false、字段全在 required 里,可选用可空类型表达

Comments