---
title: OpenAPI Tools
description: Turn any OpenAPI spec into tools your agents can call.
---

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.

```ts
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:

```tsx
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:

```ts
// 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:

```ts
{ 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:

```ts
// 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`:

```ts
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:

```ts
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:

```ts
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.

```ts
// 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.

```ts
// 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:

```ts
{
  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:

| Keyword | Zod equivalent |
|---------|---------------|
| `allOf` with one entry | the single entry schema |
| `allOf` with multiple entries | `z.intersection(schemaA, schemaB)` chained |
| `oneOf` | `z.union([...variants])` |
| `anyOf` | `z.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:

```ts
// 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.
