Repository Public Research preview
1import { RECOMMENDED, resolveModel } from "./knowledge.mjs";23/**4 * Starter code for the main ways to build with the OpenAI API. Every template is executed against5 * the live API by test/run-scaffolds.mjs in both languages. Templates use the Responses API (the6 * recommended surface) and only emit reasoning effort where the model supports that level.7 */8export const APPROACHES = {9 single_call: "One request, one response (Responses API).",10 streaming: "Token streaming with typed events.",11 structured_output: "JSON guaranteed to match a schema (text.format json_schema, strict).",12 function_loop: "You own the function-calling loop (function_call / function_call_output items).",13 conversation: "Multi-turn state held by the API with previous_response_id.",14 web_search: "Hosted web search tool with source citations.",15 agents_sdk: "OpenAI Agents SDK: agent loop, tools and tracing handled for you.",16 embeddings: "Text embeddings and cosine similarity.",17 batch: "Asynchronous bulk processing at half price (Batch API).",18};19export const LANGUAGES = ["typescript", "python"];2021function profile(modelId, effort) {22 const m = resolveModel(modelId) ?? resolveModel(RECOMMENDED.balanced);23 const levels = m.responses.effort_levels;24 const e = levels.length === 0 ? null : levels.includes(effort) ? effort : levels.includes("low") ? "low" : levels[0];25 return { id: m.id, effort: e };26}27const tsEffort = (p, indent = " ") => (p.effort ? `\n${indent}reasoning: { effort: "${p.effort}" },` : "");28const pyEffort = (p, indent = " ") => (p.effort ? `\n${indent}reasoning={"effort": "${p.effort}"},` : "");2930const TS = {31 single_call: (p) => `import OpenAI from "openai";3233const client = new OpenAI(); // reads OPENAI_API_KEY3435const response = await client.responses.create({36 model: "${p.id}",${tsEffort(p)}37 instructions: "You are a concise assistant.",38 input: process.argv[2] ?? "Say hello in one short sentence.",39 max_output_tokens: 4000, // includes reasoning tokens40});4142// Always check status before trusting the output.43if (response.status === "incomplete") console.error(\`incomplete: \${response.incomplete_details?.reason}\`);44console.log(response.output_text);45console.error(\`tokens: \${response.usage?.input_tokens} in / \${response.usage?.output_tokens} out\`);46`,47 streaming: (p) => `import OpenAI from "openai";4849const client = new OpenAI();5051const stream = await client.responses.create({52 model: "${p.id}",${tsEffort(p)}53 input: process.argv[2] ?? "Count from one to five, one word per line.",54 max_output_tokens: 4000,55 stream: true,56});5758for await (const event of stream) {59 if (event.type === "response.output_text.delta") process.stdout.write(event.delta);60 else if (event.type === "response.completed") console.error(\`\\ntokens out: \${event.response.usage?.output_tokens}\`);61 else if (event.type === "response.incomplete") console.error(\`\\nincomplete: \${event.response.incomplete_details?.reason}\`);62 else if (event.type === "error") throw new Error(event.message);63}64process.stdout.write("\\n");65`,66 structured_output: (p) => `import OpenAI from "openai";67import { zodTextFormat } from "openai/helpers/zod";68import { z } from "zod";6970const client = new OpenAI();7172// Strict mode: every field is required; model optional values as .nullable().73const Sentiment = z.object({74 sentiment: z.enum(["positive", "neutral", "negative"]),75 confidence: z.number(),76 reasons: z.array(z.string()),77});7879const response = await client.responses.parse({80 model: "${p.id}",${tsEffort(p)}81 input: \`Classify the sentiment of: \${process.argv[2] ?? "The install was painless and the docs are clear."}\`,82 text: { format: zodTextFormat(Sentiment, "sentiment") },83 max_output_tokens: 4000,84});8586if (response.status !== "completed") throw new Error(\`no complete JSON: \${response.status} \${response.incomplete_details?.reason ?? ""}\`);87const result = response.output_parsed; // typed, already validated against the schema88if (!result) throw new Error("the model refused or returned no parsed output");89console.log(result);90`,91 function_loop: (p) => `import OpenAI from "openai";9293const client = new OpenAI();9495const tools: OpenAI.Responses.Tool[] = [96 {97 type: "function",98 name: "get_weather",99 description: "Get the current weather for a city. Use it whenever the user asks about weather. Returns a short text summary.",100 strict: true,101 parameters: {102 type: "object",103 properties: { city: { type: "string", description: "City name, e.g. Montréal" } },104 required: ["city"],105 additionalProperties: false,106 },107 },108];109110// Your tool implementations. Arguments are model output: validate them before acting.111async function runTool(name: string, args: unknown): Promise<string> {112 if (name === "get_weather") {113 const { city } = args as { city: string };114 return \`Sunny, 21°C in \${city}\`;115 }116 throw new Error(\`unknown tool: \${name}\`);117}118119const input: OpenAI.Responses.ResponseInput = [{ role: "user", content: process.argv[2] ?? "What is the weather in Montréal and in Québec City?" }];120121for (let turn = 0; turn < 10; turn++) { // hard cap: never loop unbounded122 const response = await client.responses.create({ model: "${p.id}",${tsEffort(p, " ").replace("\n", " ").trimEnd()} tools, input, max_output_tokens: 4000 });123 // Append EVERY output item (reasoning items included), not just the text.124 input.push(...response.output);125126 const calls = response.output.filter((item) => item.type === "function_call");127 if (calls.length === 0) {128 if (response.status !== "completed") console.error(\`status: \${response.status}\`);129 console.log(response.output_text);130 break;131 }132 // Run every call (in parallel) and answer each one with its call_id.133 const outputs = await Promise.all(134 calls.map(async (call) => {135 let output: string;136 try {137 output = await runTool(call.name, JSON.parse(call.arguments));138 } catch (err) {139 output = \`error: \${String(err)}\`; // report the failure to the model instead of dropping it140 }141 return { type: "function_call_output" as const, call_id: call.call_id, output };142 }),143 );144 input.push(...outputs);145}146`,147 conversation: (p) => `import OpenAI from "openai";148149const client = new OpenAI();150151// The API keeps the conversation: each turn sends only the new message plus previous_response_id.152// Responses are stored for this to work (store defaults to true). Use store: false + your own153// history when you must not retain data.154const first = await client.responses.create({155 model: "${p.id}",${tsEffort(p)}156 instructions: "You are a concise assistant.",157 input: "My favourite number is 42. Remember it.",158 max_output_tokens: 2000,159});160console.log("1:", first.output_text);161162const second = await client.responses.create({163 model: "${p.id}",${tsEffort(p)}164 previous_response_id: first.id,165 instructions: "You are a concise assistant.", // instructions are NOT carried over: resend them166 input: "What is my favourite number? Reply with the number only.",167 max_output_tokens: 2000,168});169console.log("2:", second.output_text);170`,171 web_search: (p) => `import OpenAI from "openai";172173const client = new OpenAI();174175const response = await client.responses.create({176 model: "${p.id}",${tsEffort(p)}177 tools: [{ type: "web_search" }], // hosted tool: OpenAI runs the search; billed per call178 input: process.argv[2] ?? "What is the URL of the official Model Context Protocol specification site? One line.",179 max_output_tokens: 4000,180});181182console.log(response.output_text);183// Sources are attached to the text as url_citation annotations.184for (const item of response.output) {185 if (item.type !== "message") continue;186 for (const part of item.content) {187 if (part.type !== "output_text") continue;188 for (const a of part.annotations) if (a.type === "url_citation") console.error(\`source: \${a.url}\`);189 }190}191`,192 agents_sdk: (p) => `import { Agent, run, tool } from "@openai/agents";193import { z } from "zod";194195// The Agents SDK runs the loop (model call → tools → model call …) and records a trace.196const getWeather = tool({197 name: "get_weather",198 description: "Get the current weather for a city. Use it whenever the user asks about weather.",199 parameters: z.object({ city: z.string().describe("City name, e.g. Montréal") }),200 execute: async ({ city }) => \`Sunny, 21°C in \${city}\`,201});202203const agent = new Agent({204 name: "Weather assistant",205 instructions: "You answer weather questions briefly, using the tool.",206 model: "${p.id}",207 tools: [getWeather],208});209210const result = await run(agent, process.argv[2] ?? "What is the weather in Montréal and in Québec City?", { maxTurns: 10 });211console.log(result.finalOutput);212`,213 embeddings: () => `import OpenAI from "openai";214215const client = new OpenAI();216const texts = ["How do I reset my password?", "Steps to change a forgotten password", "Best pizza toppings"];217218const { data, usage } = await client.embeddings.create({ model: "text-embedding-3-small", input: texts });219const vectors = data.map((d) => d.embedding);220221const cosine = (a: number[], b: number[]) => {222 let dot = 0, na = 0, nb = 0;223 for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }224 return dot / Math.sqrt(na * nb);225};226console.log(\`dimensions: \${vectors[0].length}, tokens: \${usage.total_tokens}\`);227console.log(\`similar pair: \${cosine(vectors[0], vectors[1]).toFixed(3)}\`);228console.log(\`unrelated pair: \${cosine(vectors[0], vectors[2]).toFixed(3)}\`);229`,230 batch: (p) => `import OpenAI, { toFile } from "openai";231232const client = new OpenAI();233const inputs = ["Summarise in five words: the cat sat on the mat.", "Summarise in five words: rain is expected tomorrow."];234235// One JSON line per request. Half price, asynchronous (within 24 h). Not for interactive use.236const jsonl = inputs237 .map((input, i) => JSON.stringify({ custom_id: \`item-\${i}\`, method: "POST", url: "/v1/responses", body: { model: "${p.id}", input, max_output_tokens: 2000 } }))238 .join("\\n");239const file = await client.files.create({ file: await toFile(Buffer.from(jsonl), "batch.jsonl"), purpose: "batch" });240let batch = await client.batches.create({ input_file_id: file.id, endpoint: "/v1/responses", completion_window: "24h" });241console.error(\`batch \${batch.id}: \${batch.status}\`);242243while (!["completed", "failed", "expired", "cancelled"].includes(batch.status)) {244 await new Promise((r) => setTimeout(r, 15_000));245 batch = await client.batches.retrieve(batch.id);246}247if (batch.status !== "completed" || !batch.output_file_id) throw new Error(\`batch ended as \${batch.status}\`);248249// Results come back in ANY order: match them by custom_id.250const text = await (await client.files.content(batch.output_file_id)).text();251for (const line of text.trim().split("\\n")) {252 const row = JSON.parse(line);253 const message = row.response?.body?.output?.find((o: { type: string }) => o.type === "message");254 console.log(row.custom_id, "→", message?.content?.[0]?.text ?? row.error?.message ?? "(no text)");255}256`,257};258259const PY = {260 single_call: (p) => `import sys261from openai import OpenAI262263client = OpenAI() # reads OPENAI_API_KEY264265response = client.responses.create(266 model="${p.id}",${pyEffort(p)}267 instructions="You are a concise assistant.",268 input=sys.argv[1] if len(sys.argv) > 1 else "Say hello in one short sentence.",269 max_output_tokens=4000, # includes reasoning tokens270)271272# Always check status before trusting the output.273if response.status == "incomplete":274 print(f"incomplete: {response.incomplete_details.reason}", file=sys.stderr)275print(response.output_text)276print(f"tokens: {response.usage.input_tokens} in / {response.usage.output_tokens} out", file=sys.stderr)277`,278 streaming: (p) => `import sys279from openai import OpenAI280281client = OpenAI()282283stream = client.responses.create(284 model="${p.id}",${pyEffort(p)}285 input=sys.argv[1] if len(sys.argv) > 1 else "Count from one to five, one word per line.",286 max_output_tokens=4000,287 stream=True,288)289290for event in stream:291 if event.type == "response.output_text.delta":292 print(event.delta, end="", flush=True)293 elif event.type == "response.completed":294 print(f"\\ntokens out: {event.response.usage.output_tokens}", file=sys.stderr)295 elif event.type == "response.incomplete":296 print(f"\\nincomplete: {event.response.incomplete_details.reason}", file=sys.stderr)297 elif event.type == "error":298 raise RuntimeError(event.message)299print()300`,301 structured_output: (p) => `import sys302from typing import Literal303304from openai import OpenAI305from pydantic import BaseModel306307client = OpenAI()308309310# Strict mode: every field is required; model optional values as \`X | None\`.311class Sentiment(BaseModel):312 sentiment: Literal["positive", "neutral", "negative"]313 confidence: float314 reasons: list[str]315316317text_in = sys.argv[1] if len(sys.argv) > 1 else "The install was painless and the docs are clear."318response = client.responses.parse(319 model="${p.id}",${pyEffort(p)}320 input=f"Classify the sentiment of: {text_in}",321 text_format=Sentiment,322 max_output_tokens=4000,323)324325if response.status != "completed":326 raise SystemExit(f"no complete JSON: {response.status}")327result = response.output_parsed # a validated Sentiment instance328if result is None:329 raise SystemExit("the model refused or returned no parsed output")330print(result.model_dump())331`,332 function_loop: (p) => `import json333import sys334from openai import OpenAI335336client = OpenAI()337338tools = [339 {340 "type": "function",341 "name": "get_weather",342 "description": "Get the current weather for a city. Use it whenever the user asks about weather. Returns a short text summary.",343 "strict": True,344 "parameters": {345 "type": "object",346 "properties": {"city": {"type": "string", "description": "City name, e.g. Montréal"}},347 "required": ["city"],348 "additionalProperties": False,349 },350 }351]352353354def run_tool(name: str, args: dict) -> str:355 """Your tool implementations. Arguments are model output: validate them before acting."""356 if name == "get_weather":357 return f"Sunny, 21°C in {args['city']}"358 raise ValueError(f"unknown tool: {name}")359360361conversation = [{"role": "user", "content": sys.argv[1] if len(sys.argv) > 1 else "What is the weather in Montréal and in Québec City?"}]362363for _ in range(10): # hard cap: never loop unbounded364 response = client.responses.create(365 model="${p.id}",${pyEffort(p, " ")}366 tools=tools,367 input=conversation,368 max_output_tokens=4000,369 )370 # Append EVERY output item (reasoning items included), not just the text.371 conversation += response.output372373 calls = [item for item in response.output if item.type == "function_call"]374 if not calls:375 if response.status != "completed":376 print(f"status: {response.status}", file=sys.stderr)377 print(response.output_text)378 break379380 # Answer each call with its call_id.381 for call in calls:382 try:383 output = run_tool(call.name, json.loads(call.arguments))384 except Exception as err: # report the failure to the model instead of dropping it385 output = f"error: {err}"386 conversation.append({"type": "function_call_output", "call_id": call.call_id, "output": output})387`,388 conversation: (p) => `from openai import OpenAI389390client = OpenAI()391392# The API keeps the conversation: each turn sends only the new message plus previous_response_id.393# Responses are stored for this to work (store defaults to True). Use store=False + your own394# history when you must not retain data.395first = client.responses.create(396 model="${p.id}",${pyEffort(p)}397 instructions="You are a concise assistant.",398 input="My favourite number is 42. Remember it.",399 max_output_tokens=2000,400)401print("1:", first.output_text)402403second = client.responses.create(404 model="${p.id}",${pyEffort(p)}405 previous_response_id=first.id,406 instructions="You are a concise assistant.", # instructions are NOT carried over: resend them407 input="What is my favourite number? Reply with the number only.",408 max_output_tokens=2000,409)410print("2:", second.output_text)411`,412 web_search: (p) => `import sys413from openai import OpenAI414415client = OpenAI()416417response = client.responses.create(418 model="${p.id}",${pyEffort(p)}419 tools=[{"type": "web_search"}], # hosted tool: OpenAI runs the search; billed per call420 input=sys.argv[1] if len(sys.argv) > 1 else "What is the URL of the official Model Context Protocol specification site? One line.",421 max_output_tokens=4000,422)423424print(response.output_text)425# Sources are attached to the text as url_citation annotations.426for item in response.output:427 if item.type != "message":428 continue429 for part in item.content:430 if part.type != "output_text":431 continue432 for a in part.annotations:433 if a.type == "url_citation":434 print(f"source: {a.url}", file=sys.stderr)435`,436 agents_sdk: (p) => `import sys437438from agents import Agent, Runner, function_tool439440441# The Agents SDK runs the loop (model call -> tools -> model call ...) and records a trace.442@function_tool443def get_weather(city: str) -> str:444 """Get the current weather for a city. Use it whenever the user asks about weather.445446 Args:447 city: City name, e.g. Montréal.448 """449 return f"Sunny, 21°C in {city}"450451452agent = Agent(453 name="Weather assistant",454 instructions="You answer weather questions briefly, using the tool.",455 model="${p.id}",456 tools=[get_weather],457)458459prompt = sys.argv[1] if len(sys.argv) > 1 else "What is the weather in Montréal and in Québec City?"460result = Runner.run_sync(agent, prompt, max_turns=10)461print(result.final_output)462`,463 embeddings: () => `import math464from openai import OpenAI465466client = OpenAI()467texts = ["How do I reset my password?", "Steps to change a forgotten password", "Best pizza toppings"]468469result = client.embeddings.create(model="text-embedding-3-small", input=texts)470vectors = [d.embedding for d in result.data]471472473def cosine(a: list[float], b: list[float]) -> float:474 dot = sum(x * y for x, y in zip(a, b))475 return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))476477478print(f"dimensions: {len(vectors[0])}, tokens: {result.usage.total_tokens}")479print(f"similar pair: {cosine(vectors[0], vectors[1]):.3f}")480print(f"unrelated pair: {cosine(vectors[0], vectors[2]):.3f}")481`,482 batch: (p) => `import json483import time484from openai import OpenAI485486client = OpenAI()487inputs = ["Summarise in five words: the cat sat on the mat.", "Summarise in five words: rain is expected tomorrow."]488489# One JSON line per request. Half price, asynchronous (within 24 h). Not for interactive use.490jsonl = "\\n".join(491 json.dumps({"custom_id": f"item-{i}", "method": "POST", "url": "/v1/responses", "body": {"model": "${p.id}", "input": text, "max_output_tokens": 2000}})492 for i, text in enumerate(inputs)493)494file = client.files.create(file=("batch.jsonl", jsonl.encode()), purpose="batch")495batch = client.batches.create(input_file_id=file.id, endpoint="/v1/responses", completion_window="24h")496print(f"batch {batch.id}: {batch.status}")497498while batch.status not in ("completed", "failed", "expired", "cancelled"):499 time.sleep(15)500 batch = client.batches.retrieve(batch.id)501if batch.status != "completed" or not batch.output_file_id:502 raise SystemExit(f"batch ended as {batch.status}")503504# Results come back in ANY order: match them by custom_id.505for line in client.files.content(batch.output_file_id).text.strip().split("\\n"):506 row = json.loads(line)507 message = next((o for o in row["response"]["body"]["output"] if o["type"] == "message"), None)508 print(row["custom_id"], "->", message["content"][0]["text"] if message else "(no text)")509`,510};511512export function scaffold({ approach, language = "typescript", model, effort }) {513 if (!APPROACHES[approach]) return { error: `Unknown approach "${approach}". One of: ${Object.keys(APPROACHES).join(", ")}.` };514 if (!LANGUAGES.includes(language)) return { error: `Unknown language "${language}". One of: ${LANGUAGES.join(", ")}.` };515 if (model && !resolveModel(model)) return { error: `Unknown model "${model}". Call list_models.` };516 const p = profile(model ?? RECOMMENDED.balanced, effort);517 const ts = language === "typescript";518 const file = `${approach.replace(/_/g, "-")}.${ts ? "ts" : "py"}`;519 const extra = approach === "agents_sdk" ? (ts ? " @openai/agents zod" : " openai-agents") : approach === "structured_output" ? (ts ? " zod" : " pydantic") : "";520 return {521 approach, language, model: approach === "embeddings" ? "text-embedding-3-small" : p.id, description: APPROACHES[approach], filename: file,522 install: ts ? `npm install openai${extra}` : `pip install openai${extra}`,523 run: ts ? `OPENAI_API_KEY=… npx tsx ${file}` : `OPENAI_API_KEY=… python ${file}`,524 code: (ts ? TS : PY)[approach](p),525 notes: [526 approach === "agents_sdk" ? "The Agents SDK is a separate package from the API client." : null,527 approach === "batch" ? "Batch costs 50% of standard rates; results arrive in any order, within 24 hours." : null,528 approach === "web_search" ? "Hosted tools are billed per call on top of tokens." : null,529 p.effort && approach !== "embeddings" && approach !== "agents_sdk" && approach !== "batch" ? `Reasoning effort "${p.effort}" is explicit because defaults differ between models.` : null,530 "max_output_tokens includes reasoning tokens: keep it generous on reasoning models.",531 ].filter(Boolean),532 };533}534