ProvenPin Install

OpenAI Node SDK tool calling + Zod, tested with a mocked fetch: working versions (tested 2026-10-15)

Run an OpenAI chat tool-calling loop with Zod-validated tool arguments, tested offline by injecting a fake fetch into the official SDK.

passed2 passed · 0 failed2026-10-15 11:55 UTC
Status
passed
Last passed
2026-10-15 11:55 UTC
Tests
2 / 2
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
openai@7.34.0zod@4.6.7tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("openai-sdk-mocked-tool-calls") (install), or see them on GitHub.

package.json
{
  "name": "openai-sdk-mocked-tool-calls",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "openai": "7.34.0",
    "zod": "4.6.7"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/agent.ts
import OpenAI from "openai";
import { z } from "zod";

const WeatherArgs = z.object({ city: z.string().min(1) });
export const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [{
  type: "function",
  function: { name: "get_weather", description: "Current weather for a city", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } },
}];

export async function runAgent(client: OpenAI, question: string, getWeather: (city: string) => Promise<string>) {
  const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [{ role: "user", content: question }];
  for (let turn = 0; turn < 5; turn++) {
    const res = await client.chat.completions.create({ model: "gpt-4o-mini", messages, tools });
    const msg = res.choices[0].message;
    messages.push(msg);
    if (!msg.tool_calls?.length) return msg.content ?? "";
    for (const call of msg.tool_calls) {
      if (call.type !== "function") continue;
      const parsed = WeatherArgs.safeParse(JSON.parse(call.function.arguments));
      const content = parsed.success ? await getWeather(parsed.data.city) : `invalid arguments: ${parsed.error.message}`;
      messages.push({ role: "tool", tool_call_id: call.id, content });
    }
  }
  throw new Error("too many tool turns");
}
test/agent.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import OpenAI from "openai";
import { runAgent } from "../src/agent.ts";

function completion(message: object) {
  return { id: "c1", object: "chat.completion", created: 0, model: "gpt-4o-mini", choices: [{ index: 0, finish_reason: "stop", logprobs: null, message: { role: "assistant", content: null, refusal: null, ...message } }] };
}

function fakeClient(replies: object[], seen: any[]) {
  const fetch = async (_url: any, init: any) => {
    seen.push(JSON.parse(init.body));
    return new Response(JSON.stringify(replies.shift()), { status: 200, headers: { "content-type": "application/json" } });
  };
  return new OpenAI({ apiKey: "test", baseURL: "http://mock.local/v1", fetch: fetch as any, maxRetries: 0 });
}

test("calls the tool then answers", async () => {
  const seen: any[] = [];
  const client = fakeClient([
    completion({ tool_calls: [{ id: "t1", type: "function", function: { name: "get_weather", arguments: JSON.stringify({ city: "Anchorage" }) } }] }),
    completion({ content: "It is 2C in Anchorage." }),
  ], seen);
  const answer = await runAgent(client, "Weather in Anchorage?", async (c) => `2C in ${c}`);
  assert.equal(answer, "It is 2C in Anchorage.");
  const toolMsg = seen[1].messages.find((m: any) => m.role === "tool");
  assert.deepEqual(toolMsg, { role: "tool", tool_call_id: "t1", content: "2C in Anchorage" });
  assert.equal(seen[0].tools[0].function.name, "get_weather");
});

test("bad tool arguments are reported back, not thrown", async () => {
  const seen: any[] = [];
  const client = fakeClient([
    completion({ tool_calls: [{ id: "t1", type: "function", function: { name: "get_weather", arguments: "{}" } }] }),
    completion({ content: "Which city?" }),
  ], seen);
  assert.equal(await runAgent(client, "Weather?", async () => "x"), "Which city?");
  assert.match(seen[1].messages.at(-1).content, /invalid arguments/);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.