SPB Git

Repository Public Research preview

scaffold.mjs 21.2 KB 534 lines javascript

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
Files main
Menu

Theme