Skip to main content
You have an internal API. It has an OpenAPI spec. Your agent needs to call it. You could hand-write a tool for every endpoint — define the schema, build the URL, set the headers, parse the response. Or you could point Smithers at the spec and let it do that for you.

The Problem

Every REST API endpoint you want an agent to use requires a tool. A tool needs three things: a Zod schema describing the parameters, a description the LLM can read, and an execute function that makes the HTTP request. For a single endpoint that is fine. For an API with forty endpoints, it is tedious and error-prone. OpenAPI specs already contain all the information you need. The parameter types are there. The descriptions are there. The URL patterns and HTTP methods are there. The only question is how to convert that information into tools.

The Solution

createOpenApiTools reads an OpenAPI 3.0+ spec and returns a Record<string, Tool> — one tool per operation. Each tool has a Zod schema derived from the operation’s parameters and request body, a description from the operation’s summary, and an execute function that builds the correct HTTP request and returns the response.
import { createOpenApiTools } from "smithers-orchestrator";

const tools = await createOpenApiTools("https://api.example.com/openapi.json", {
  auth: { type: "bearer", token: process.env.API_TOKEN! },
});
That is the whole thing. tools is now a map of operation IDs to AI SDK tools. Hand them to an agent:
const apiAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools,
});

<Task id="fetch-data" agent={apiAgent}>
  List the first 10 items from the inventory API.
</Task>
The agent sees tool descriptions like “List all pets” and parameters like { limit: z.number().optional() }. It decides which endpoints to call, fills in the parameters, and gets back JSON responses. No glue code required.

How It Works

The conversion follows four steps:
  1. Parse the spec. Smithers loads the OpenAPI document (JSON object, URL, or file path), resolves $ref pointers, and extracts every operation.
  2. Convert schemas. Each operation’s path parameters, query parameters, header parameters, and request body are converted from JSON Schema into Zod schemas. Strings become z.string(), integers become z.number().int(), objects become z.object() with the correct shape. When a schema is too complex for clean conversion, Smithers falls back to z.any() with a description so the LLM still knows what to provide.
  3. Build the tool. Each operation becomes an AI SDK tool() with the converted schema as inputSchema, the operation summary as description, and an execute function that assembles the HTTP request.
  4. Execute at runtime. When an agent calls the tool, the execute function substitutes path parameters into the URL, appends query parameters, sets headers (including authentication), sends the request via fetch, and returns the response body.

Authentication

Three authentication methods are supported:
// Bearer token
{ auth: { type: "bearer", token: "sk-..." } }

// Basic auth
{ auth: { type: "basic", username: "admin", password: "secret" } }

// API key (in header or query)
{ auth: { type: "apiKey", name: "X-API-Key", value: "key123", in: "header" } }
You can also pass arbitrary headers:
{ headers: { "X-Custom-Header": "value" } }

Filtering Operations

Most APIs have endpoints you do not want an agent calling. Use include or exclude to control which operations become tools:
// Only these operations
const tools = await createOpenApiTools(spec, {
  include: ["listPets", "getPet"],
});

// Everything except these
const tools = await createOpenApiTools(spec, {
  exclude: ["deletePet", "deleteAllPets"],
});

Single Operation

If you only need one tool from a spec, use createOpenApiTool:
import { createOpenApiTool } from "smithers-orchestrator";

const listPets = await createOpenApiTool(spec, "listPets", {
  baseUrl: "https://api.petstore.example.com",
});

Observability

Every OpenAPI tool call emits an OpenApiToolCalled event and updates three metrics:
  • smithers.openapi.tool_calls — counter of total calls
  • smithers.openapi.tool_call_errors — counter of failed calls
  • smithers.openapi.tool_duration_ms — histogram of call durations
These integrate with the standard Smithers observability pipeline, so they appear in your logs, Prometheus exports, and OpenTelemetry traces alongside all other tool metrics.

Synchronous Loading

Two variants exist. The async createOpenApiTools and createOpenApiTool work with any input — objects, local files, or remote URLs (fetched via fetch). The sync variants createOpenApiToolsSync and createOpenApiToolSync skip the network fetch step, so they only work with spec objects or local file paths:
import { createOpenApiToolsSync } from "smithers-orchestrator";

// Works: spec object already in memory
const tools = createOpenApiToolsSync(specObject, options);

// Works: local file read synchronously
const tools = createOpenApiToolsSync("/path/to/openapi.json", options);

// Does not work: sync cannot fetch URLs
// const tools = createOpenApiToolsSync("https://api.example.com/openapi.json");
Use the sync variant when you are initializing tools at module load time and cannot await.

Operation ID Fallback

If an OpenAPI operation does not have an operationId, Smithers generates one from the HTTP method and path. For example, GET /pets/{petId} becomes get_pets_petId. The generated ID strips braces and non-alphanumeric characters, joining segments with underscores. You should still set explicit operationId values in your spec whenever possible — they make tool names readable and stable. The fallback exists so that specs without IDs still produce usable tools.

Loading a Spec via the Effect Layer

For Effect-native code, loadSpecEffect returns an Effect.Effect<OpenApiSpec> so you can compose spec loading with your existing Effect pipeline:
import { loadSpecEffect } from "smithers-orchestrator/openapi";
import { Effect } from "effect";

const program = Effect.gen(function* () {
  const spec = yield* loadSpecEffect("https://api.example.com/openapi.json");
  // spec is a fully parsed OpenApiSpec object
});
loadSpecEffect resolves URLs via fetch, reads local files synchronously, and parses both JSON and YAML. Pass a spec object and it returns immediately.

Request Body Handling

When an operation has a requestBody with application/json content, Smithers adds a body parameter to the generated Zod schema. The agent fills body as a plain object; the execute function serializes it with JSON.stringify and sends it with Content-Type: application/json. Required request bodies become required body parameters; optional request bodies become optional.
// Agent input for a POST /pets operation
{
  body: { name: "Fido", species: "dog" }
}
// → POST /pets with Content-Type: application/json and body {"name":"Fido","species":"dog"}
Parameters with in: cookie are silently skipped — cookies are not exposed to agents.

Non-JSON Response Handling

If the API returns a response with a non-JSON content type (anything that does not include application/json), the execute function returns the raw response text as a string. The agent receives that string as the tool result and can parse or summarize it as needed.
// JSON response → parsed JavaScript object returned to agent
// text/plain, text/html, etc. → raw string returned to agent

Error Result Wrapping

When an HTTP call fails (network error, timeout, unexpected exception), the tool does not throw. Instead it returns a structured error object:
{
  error: true,
  message: "fetch failed: connection refused",
  status: "failed",
}
The agent sees this object as the tool result and can decide whether to retry, report the error, or continue with other tools. HTTP 4xx and 5xx responses are not automatically treated as errors — the agent receives the parsed response body and can inspect the status itself.

Schema Composition: allOf, anyOf, oneOf

Smithers converts OpenAPI composition keywords to Zod:
KeywordZod equivalent
allOf with one entrythe single entry schema
allOf with multiple entriesz.intersection(schemaA, schemaB) chained
oneOfz.union([...variants])
anyOfz.union([...variants])
Circular $ref references are detected and replaced with z.any() annotated with the circular reference path.

Nullable and Default Values

Two OpenAPI schema properties affect the generated Zod schema:
  • nullable: true — wraps the schema with .nullable() so the agent can provide null
  • default: <value> — adds .default(<value>) so missing inputs fall back to the spec default
These are applied after the base type, before the description:
// OpenAPI schema: { type: "string", nullable: true, default: "unknown" }
// Generated Zod:  z.string().default("unknown").nullable()

When to Use OpenAPI Tools

Use them when you have an existing REST API with an OpenAPI spec and you want agents to interact with it. They are particularly good for:
  • Internal APIs with dozens of endpoints
  • Third-party APIs that publish OpenAPI specs
  • Rapid prototyping where hand-writing tools is too slow
Do not use them when you need fine-grained control over how an API is called — custom retry logic, request transformation, response filtering. In those cases, write a custom tool and call the API yourself.