AI School Pakistan
← Applied AI Builder0 of 18 complete
LessonCourse overview

Module 2 · Structured Outputs and Tools · 45 min

Function Calling and Tool Contracts

Lesson 5 of 18

Is lesson ke baad tum

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.

In this lesson

Defend a read-only delivery lookup against an unauthorized or malformed request. The supplied Node program uses fictional zone data and needs no account, SDK, or API key. Your task is to add negative tests and keep the router closed when an input fails.

Function calling means the model proposes a tool name and arguments. Your application decides whether to run it. A strict schema helps shape the proposal; it cannot grant access.

A Tool Contract Has More Than a Schema

For this lookup, the name is lookup_delivery_zone, the only argument is zone_code, and only an authenticated support agent may use it. Its result is a small service record or a controlled error. There is no send, payment, or database write.

The fixture actor below simulates an already verified session. A real server must derive that actor from authentication, then check access to the requested record or tenant. Do not accept a role supplied by the model or a browser form. Prompt instructions such as “never send without asking” cannot enforce a permission boundary.

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:

javascript56 lines
// 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 || actor.role !== "support-agent") {
    return { ok: false, code: "FORBIDDEN" };
  }
  if (name !== "lookup_delivery_zone") {
    return { ok: false, code: "UNKNOWN_TOOL" };
  }
  if (!args || typeof args !== "object" || Array.isArray(args) ||
      Object.keys(args).length !== 1 || !Object.hasOwn(args, "zone_code") ||
      typeof args.zone_code !== "string" || !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" } }
];

const expected = ["OK", "FORBIDDEN", "INVALID_ARGUMENTS", "UNKNOWN_TOOL"];
for (const [index, test] of tests.entries()) {
  const result = executeTool(test);
  console.log(result);
  if ((result.ok ? "OK" : result.code) !== expected[index]) {
    throw new Error(`Unexpected result for case ${index + 1}`);
  }
}
// Extend expected beside tests when adding exercise cases.

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

The local router can later sit inside a Responses API application. A returned function_call contains a name, JSON arguments, and call_id. Parse the arguments, run the same independent checks, and return a function_call_output with the matching call ID and a bounded JSON-string result.

When passing response items into another JavaScript request, the current SDK documents toResponseInputItems from openai/lib/responses/ResponseInputItems. Preserve all needed response items, including reasoning items where required; do not keep only the visible tool call. Follow the linked function-calling guide for the complete integration.

That optional integration needs authorized API access and a compatible model. Use the Lesson 1.3 controls: explicit model, store: false, output cap, request timeout, no hidden SDK retries, and a total call/elapsed budget. Handle zero or multiple tool calls; stop at the budget instead of recursively calling forever. A tool result is data, including any untrusted text inside it. It must not redefine permissions. The core exercise below tests the router without making model requests.

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.

Hands-On Exercise: Defend a Tool Boundary

Run node tool-contract.mjs; the four supplied assertions must pass. Then extend the same tests array with these cases:

New inputExpected codeWhy
Support actor, correct name, args: {}INVALID_ARGUMENTSRequired zone missing
Support actor, correct name, args: {zone_code: "LHE-C", address: "demo"}INVALID_ARGUMENTSExtra field rejected
actor: null, correct name and valid zoneFORBIDDENMissing verified identity

Change the valid zone to KHI-C. It should still succeed, with eta_business_days: 3; update the assertion to check that returned value. The support role has not changed, so this is an allowed data choice, not permission escalation.

Repeated calls to this read-only fixture cause no external effect. If you later add sending, use a separate capability with final-artifact approval and durable idempotency. The local lookup does not implement a production login system, tenant isolation, or an action ledger.

Completion Rubric

  • Seven router cases return the expected success/error without crashing on a missing actor.
  • The Karachi mutation returns three fictional business days.
  • Neither a new tool name nor an extra argument reaches an executor.
  • You can identify where a real server would obtain the actor and check record ownership before returning data.

Sources

Practice quiz

Check yourself

4 questions · practise what you just learned

0/4
  1. 1 / 4 · diagnose

    The model requests run_shell, but the router exposes only lookup_delivery_zone. What should the router return?

Apna kaam check karo.

Practice complete ki? Result ko example aur lesson ke checks se compare karo.

This is your own completion record, not a graded assessment.

Kuch samajh nahi aaya? Ask a question →