Module 2: Structured Outputs and Tools · 45 min

Function Calling and Tool Contracts

Learning goal

Turn this lesson into one checked practice output

By the end, you should be able to explain the core idea behind “Function Calling and Tool Contracts” in your own words, apply it to one small real or sample task, and identify what still needs human review.

  1. 1

    Learn

    Read the 45-minute lesson without copying an output blindly.

  2. 2

    Try

    Use a small, non-sensitive example that you can inspect line by line.

  3. 3

    Review

    Check facts, fit, and risk; save one improvement note for next time.

Function calling lets a model request a capability through a named, structured contract; it does not give the model permission to execute arbitrary code. Your application receives a proposed tool name and arguments, validates them, checks the user's authorization and business rules, executes an allowlisted function, and returns a bounded result. That separation is the difference between a useful tool workflow and an unsafe model with accidental production access.

Use function calling when the answer depends on controlled data or an action outside the model: looking up a delivery zone, checking an approved catalog, creating a draft record, or scheduling a review. Do not expose a general shell, unrestricted URL fetcher, raw SQL executor, or “run any command” function because it is convenient.

A Tool Contract Has More Than a Schema

A production contract should answer these questions:

  1. Name and purpose: what single capability does the tool provide?
  2. Arguments: what types, enums, limits, and required fields are accepted?
  3. Authorization: which user or service may request it, and for which records?
  4. Side effect: is it read-only, a draft, or an external action?
  5. Confirmation: what requires explicit human approval immediately before execution?
  6. Idempotency: how does a retry avoid creating the same effect twice?
  7. Result: what bounded fields or error codes return to the model?
  8. Audit: which safe identifiers, versions, actor, and outcome are recorded?

The model-facing description helps selection, but application code enforces the contract. Prompt instructions such as “never send without asking” are not a substitute for a server-side approval check.

Worked Example: A Read-Only Delivery-Zone Lookup

Define one narrow tool. It accepts an order's zone code, not a full home address, and returns synthetic service information from a local allowlist:

// tool-contract.mjs
const tool = {
  type: "function",
  name: "lookup_delivery_zone",
  description: "Look up the synthetic service level for an approved zone code.",
  parameters: {
    type: "object",
    properties: {
      zone_code: {
        type: "string",
        enum: ["LHE-C", "ISB-C", "KHI-C"]
      }
    },
    required: ["zone_code"],
    additionalProperties: false
  },
  strict: true
};

const zones = new Map([
  ["LHE-C", { service: "standard", eta_business_days: 2 }],
  ["ISB-C", { service: "standard", eta_business_days: 2 }],
  ["KHI-C", { service: "standard", eta_business_days: 3 }]
]);

function executeTool({ actor, name, args }) {
  if (actor.role !== "support-agent") {
    return { ok: false, code: "FORBIDDEN" };
  }
  if (name !== "lookup_delivery_zone") {
    return { ok: false, code: "UNKNOWN_TOOL" };
  }
  if (!args || Object.keys(args).length !== 1 || !zones.has(args.zone_code)) {
    return { ok: false, code: "INVALID_ARGUMENTS" };
  }
  return { ok: true, zone_code: args.zone_code, ...zones.get(args.zone_code) };
}

const tests = [
  { actor: { role: "support-agent" }, name: "lookup_delivery_zone", args: { zone_code: "LHE-C" } },
  { actor: { role: "guest" }, name: "lookup_delivery_zone", args: { zone_code: "LHE-C" } },
  { actor: { role: "support-agent" }, name: "lookup_delivery_zone", args: { zone_code: "PEW-C" } },
  { actor: { role: "support-agent" }, name: "run_shell", args: { command: "anything" } }
];

for (const test of tests) console.log(executeTool(test));

Run node tool-contract.mjs. One case succeeds; the guest, unknown zone, and unknown tool fail with controlled codes. This local router remains necessary even when a provider's strict function schema produces well-formed arguments. The model cannot add a new zone or grant a guest access.

The values above are fictional lab fixtures, not delivery promises. In a real business, an authorized system owns current service data, and the tool returns its version or timestamp.

Connect the Contract to a Model Safely

With the current OpenAI Responses API, the model can receive a function definition and return output items of type function_call. Your program must parse, validate, authorize, and execute each call before appending a function_call_output item using the matching call ID. The conceptual loop is:

const input = [{ role: "user", content: "Use zone LHE-C for this synthetic order." }];

let response = await client.responses.create({
  model: process.env.OPENAI_MODEL,
  tools: [tool],
  input
});

input.push(...response.output);

for (const item of response.output) {
  if (item.type !== "function_call") continue;

  let args;
  try {
    args = JSON.parse(item.arguments);
  } catch {
    args = null;
  }

  const result = executeTool({
    actor: authenticatedUser,
    name: item.name,
    args
  });

  input.push({
    type: "function_call_output",
    call_id: item.call_id,
    output: JSON.stringify(result)
  });
}

response = await client.responses.create({
  model: process.env.OPENAI_MODEL,
  tools: [tool],
  input
});

authenticatedUser must come from your application's verified session, never from a model-generated argument. The returned tool result is untrusted input too: a web page, document, or database field could contain instructions aimed at the model. Wrap results in clear data boundaries, limit length and fields, and tell the model to treat them as data rather than authority. The final prose should cite tool-returned facts and preserve error codes instead of inventing an answer after a failed lookup.

Add Side Effects Only With Stronger Gates

Suppose a later tool creates a draft customer message. Keep drafting separate from sending. A safe sequence is:

  1. read authorized facts;
  2. generate a structured draft;
  3. validate claims and destination;
  4. show the exact message and recipient to a human;
  5. record explicit approval;
  6. call a narrow send tool with an idempotency key;
  7. record the provider's message ID and status.

Payment, deletion, publication, account changes, and messages to real people deserve the same or stronger separation. “The model seemed confident” is never approval.

Failure Cases to Test

  • Unknown tool name: reject it; never map fuzzy names to powerful functions.
  • Schema-valid but unauthorized call: check the authenticated actor and record ownership in application code.
  • Argument injection: do not concatenate model strings into SQL, shell commands, filesystem paths, or URLs. Use parameterized, allowlisted operations.
  • Duplicate action after timeout: use an idempotency key and look up prior outcome before repeating.
  • Parallel conflict: two calls may update the same record. Use version checks, locks, or a serialized queue where needed.
  • Tool timeout: return a bounded error, preserve workflow state, and decide whether retry is safe.
  • Oversized result: select and cap fields before returning data to the model.
  • Prompt injection in tool data: treat retrieved text as untrusted content and never let it redefine permissions.
  • Approval too early: confirmation must display the final action, destination, and material parameters, not a vague permission at session start.
  • Secrets in audit logs: log safe IDs and outcomes, not authorization headers or full private records.

🇵🇰 Pakistan Angle

For a Pakistani retailer or service business, start with read-only tools against synthetic or approved catalog data. Zone codes are preferable to full addresses when the task only needs serviceability. Preserve Urdu and Roman-Urdu labels as UTF-8, but keep stable internal codes separate from display text so spelling variation does not trigger the wrong action.

Where internet or electricity interruptions are possible, an idempotency record is essential: a browser may report failure even when a provider accepted a message or order. Use server-generated job IDs and reconcile status before retrying. Require human confirmation for customer messages, invoices, refunds, payment links, and record deletion. Define who owns the tool, who can revoke access, and what happens when a freelancer's engagement ends. Never use a model to bypass a vendor's current terms or a client's access controls.

Hands-On Exercise: Defend a Tool Boundary

Build one local, read-only tool for an approved catalog, zone list, FAQ record, or appointment slot fixture. Submit:

  1. tool.json or an exported JavaScript definition with strict parameters and additionalProperties: false.
  2. router.mjs with an allowlisted tool name, authenticated role check, argument validation, bounded result, and controlled error codes.
  3. At least eight tests: valid, missing argument, extra argument, invalid enum, unknown tool, forbidden actor, duplicate call ID, and oversized result.
  4. TOOL-REVIEW.md documenting side effects, permissions, confirmation, idempotency, timeout, retry, audit fields, and data-retention rules.
  5. A proposed second tool that has a side effect, but keep its executor disabled. Draw the approval sequence and explain why the read-only tool can run automatically while the second cannot.

The verifiable deliverable is a test command that returns non-zero if an unsafe case succeeds. A screenshot of a model choosing the right tool once is not sufficient evidence.

Completion Rubric

  • Pass — narrow contract: the tool performs one named capability with bounded arguments and output.
  • Pass — independent authorization: application code derives actor and record access outside model output.
  • Pass — adversarial tests: unknown, malformed, unauthorized, duplicate, and oversized cases fail safely.
  • Pass — side-effect discipline: any real-world action has final-state confirmation and idempotency design.
  • Pass — auditability: safe call ID, actor ID, tool version, outcome, and timing are recordable without secrets.
  • Needs revision: the model can choose arbitrary code/URLs/SQL, prompt text is the only permission control, or a retry can duplicate an external action.

Sources


Key takeaway: A tool call is an untrusted proposal: validate its strict contract, authorize against the real user and record, execute only an allowlisted capability, require confirmation for side effects, and return a bounded auditable result.

Self-check

Before you mark Lesson 2.2 complete

  • Can I explain “Function Calling and Tool Contracts” without reading the lesson back word for word?
  • Did I complete the lesson’s practice step on a real or clearly labelled sample task?
  • Did I check the result for invented facts, private data, unsafe actions, and mismatch with the brief?