@smithers-orchestrator/openapi 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared private helpers for OpenAPI tool factory
3
+ // ---------------------------------------------------------------------------
4
+ import { tool, zodSchema } from "ai";
5
+ import { Effect, Metric } from "effect";
6
+ import { nowMs } from "@smithers-orchestrator/scheduler/nowMs";
7
+ import { openApiToolCallsTotal, openApiToolCallErrorsTotal, openApiToolDuration, } from "../metrics.js";
8
+ import { buildOperationSchema } from "../schema-converter.js";
9
+ import { extractOperations } from "../spec-parser.js";
10
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
11
+ /** @typedef {import("../OpenApiTool.ts").OpenApiTool} OpenApiTool */
12
+ /** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
13
+ /** @typedef {import("../ParsedOperation.ts").ParsedOperation} ParsedOperation */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // HTTP execution
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * @param {OpenApiToolsOptions} options
20
+ * @returns {Record<string, string>}
21
+ */
22
+ export function buildAuthHeaders(options) {
23
+ const headers = {};
24
+ if (options.auth) {
25
+ switch (options.auth.type) {
26
+ case "bearer":
27
+ headers["Authorization"] = `Bearer ${options.auth.token}`;
28
+ break;
29
+ case "basic": {
30
+ const encoded = btoa(`${options.auth.username}:${options.auth.password}`);
31
+ headers["Authorization"] = `Basic ${encoded}`;
32
+ break;
33
+ }
34
+ case "apiKey":
35
+ if (options.auth.in === "header") {
36
+ headers[options.auth.name] = options.auth.value;
37
+ }
38
+ break;
39
+ }
40
+ }
41
+ if (options.headers) {
42
+ Object.assign(headers, options.headers);
43
+ }
44
+ return headers;
45
+ }
46
+ /**
47
+ * @param {string} baseUrl
48
+ * @param {string} path
49
+ * @param {Record<string, string>} pathParams
50
+ * @param {Record<string, string>} queryParams
51
+ * @param {OpenApiToolsOptions} options
52
+ * @returns {string}
53
+ */
54
+ export function buildUrl(baseUrl, path, pathParams, queryParams, options) {
55
+ // Substitute path parameters
56
+ let url = path;
57
+ for (const [key, value] of Object.entries(pathParams)) {
58
+ url = url.replace(`{${key}}`, encodeURIComponent(value));
59
+ }
60
+ const fullUrl = new URL(url, baseUrl);
61
+ // Add query parameters
62
+ for (const [key, value] of Object.entries(queryParams)) {
63
+ fullUrl.searchParams.set(key, value);
64
+ }
65
+ // Add API key to query if configured
66
+ if (options.auth?.type === "apiKey" && options.auth.in === "query") {
67
+ fullUrl.searchParams.set(options.auth.name, options.auth.value);
68
+ }
69
+ return fullUrl.toString();
70
+ }
71
+ /**
72
+ * @param {ParsedOperation} operation
73
+ * @param {Record<string, unknown>} args
74
+ * @param {string} baseUrl
75
+ * @param {OpenApiToolsOptions} options
76
+ * @returns {Promise<unknown>}
77
+ */
78
+ export async function executeRequest(operation, args, baseUrl, options) {
79
+ /** @type {Record<string, string>} */
80
+ const pathParams = {};
81
+ /** @type {Record<string, string>} */
82
+ const queryParams = {};
83
+ /** @type {Record<string, string>} */
84
+ const headerParams = {};
85
+ // Sort parameters into buckets
86
+ for (const param of operation.parameters) {
87
+ const value = args[param.name];
88
+ if (value === undefined)
89
+ continue;
90
+ const strValue = String(value);
91
+ switch (param.in) {
92
+ case "path":
93
+ pathParams[param.name] = strValue;
94
+ break;
95
+ case "query":
96
+ queryParams[param.name] = strValue;
97
+ break;
98
+ case "header":
99
+ headerParams[param.name] = strValue;
100
+ break;
101
+ }
102
+ }
103
+ const url = buildUrl(baseUrl, operation.path, pathParams, queryParams, options);
104
+ /** @type {Record<string, string>} */
105
+ const headers = {
106
+ ...buildAuthHeaders(options),
107
+ ...headerParams,
108
+ };
109
+ /** @type {RequestInit} */
110
+ const fetchInit = {
111
+ method: operation.method.toUpperCase(),
112
+ headers,
113
+ };
114
+ // Request body
115
+ if (args.body !== undefined) {
116
+ headers["Content-Type"] = "application/json";
117
+ fetchInit.headers = headers;
118
+ fetchInit.body = JSON.stringify(args.body);
119
+ }
120
+ const response = await fetch(url, fetchInit);
121
+ const contentType = response.headers.get("content-type") ?? "";
122
+ if (contentType.includes("application/json")) {
123
+ return response.json();
124
+ }
125
+ return response.text();
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Effect-wrapped execution with metrics
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * @param {ParsedOperation} operation
132
+ * @param {Record<string, unknown>} args
133
+ * @param {string} baseUrl
134
+ * @param {OpenApiToolsOptions} options
135
+ * @returns {Effect.Effect<unknown, unknown, never>}
136
+ */
137
+ export function executeToolEffect(operation, args, baseUrl, options) {
138
+ const started = nowMs();
139
+ return Effect.gen(function* () {
140
+ yield* Metric.increment(openApiToolCallsTotal);
141
+ const result = yield* Effect.tryPromise({
142
+ try: () => executeRequest(operation, args, baseUrl, options),
143
+ catch: (err) => err,
144
+ });
145
+ const durationMs = nowMs() - started;
146
+ yield* Metric.update(openApiToolDuration, durationMs);
147
+ return result;
148
+ }).pipe(Effect.tapError(() => Metric.increment(openApiToolCallErrorsTotal)), Effect.annotateLogs({
149
+ toolName: `openapi:${operation.operationId}`,
150
+ method: operation.method,
151
+ path: operation.path,
152
+ }), Effect.withLogSpan(`openapi:${operation.operationId}`));
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Tool creation
156
+ // ---------------------------------------------------------------------------
157
+ /**
158
+ * @param {ParsedOperation} operation
159
+ * @param {OpenApiSpec} spec
160
+ * @param {string} baseUrl
161
+ * @param {OpenApiToolsOptions} options
162
+ * @returns {{ name: string; tool: OpenApiTool }}
163
+ */
164
+ export function createToolFromOperation(operation, spec, baseUrl, options) {
165
+ const inputSchema = buildOperationSchema(operation.parameters, operation.requestBody, spec);
166
+ const description = operation.summary || operation.description || operation.operationId;
167
+ const prefix = options.namePrefix ?? "";
168
+ return {
169
+ name: `${prefix}${operation.operationId}`,
170
+ tool: tool({
171
+ description,
172
+ inputSchema: zodSchema(inputSchema),
173
+ execute: async (args) => {
174
+ try {
175
+ return await Effect.runPromise(executeToolEffect(operation, /** @type {Record<string, unknown>} */ (args), baseUrl, options));
176
+ }
177
+ catch (error) {
178
+ // Return error info as tool result instead of throwing
179
+ const e = /** @type {{ message?: string }} */ (error);
180
+ return {
181
+ error: true,
182
+ message: e?.message ?? String(error),
183
+ status: "failed",
184
+ };
185
+ }
186
+ },
187
+ }),
188
+ };
189
+ }
190
+ /**
191
+ * @param {OpenApiSpec} spec
192
+ * @param {OpenApiToolsOptions} options
193
+ * @returns {string}
194
+ */
195
+ export function resolveBaseUrl(spec, options) {
196
+ if (options.baseUrl)
197
+ return options.baseUrl;
198
+ if (spec.servers && spec.servers.length > 0)
199
+ return spec.servers[0].url;
200
+ return "http://localhost";
201
+ }
202
+ /**
203
+ * @param {OpenApiSpec} spec
204
+ * @param {OpenApiToolsOptions} options
205
+ * @returns {Record<string, OpenApiTool>}
206
+ */
207
+ export function createOpenApiToolsFromSpec(spec, options) {
208
+ const operations = extractOperations(spec);
209
+ const baseUrl = resolveBaseUrl(spec, options);
210
+ /** @type {Record<string, OpenApiTool>} */
211
+ const tools = {};
212
+ for (const op of operations) {
213
+ // Apply include/exclude filters
214
+ if (options.include && !options.include.includes(op.operationId))
215
+ continue;
216
+ if (options.exclude && options.exclude.includes(op.operationId))
217
+ continue;
218
+ const { name, tool: t } = createToolFromOperation(op, spec, baseUrl, options);
219
+ tools[name] = t;
220
+ }
221
+ return tools;
222
+ }
223
+ /**
224
+ * @param {OpenApiSpec} spec
225
+ * @param {string} operationId
226
+ * @param {OpenApiToolsOptions} options
227
+ * @returns {OpenApiTool}
228
+ */
229
+ export function createOpenApiToolFromSpec(spec, operationId, options) {
230
+ const operations = extractOperations(spec);
231
+ const op = operations.find((o) => o.operationId === operationId);
232
+ if (!op) {
233
+ throw new Error(`Operation "${operationId}" not found in spec. Available: ${operations.map((o) => o.operationId).join(", ")}`);
234
+ }
235
+ const baseUrl = resolveBaseUrl(spec, options);
236
+ return createToolFromOperation(op, spec, baseUrl, options).tool;
237
+ }
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // createOpenApiTool — async single tool creation from OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { Effect } from "effect";
5
+ import { loadSpecEffect } from "../spec-parser.js";
6
+ import { createOpenApiToolFromSpec } from "./_helpers.js";
7
+
8
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
9
+ /** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
10
+
11
+ /**
12
+ * Create a single AI SDK tool from an OpenAPI spec by operationId.
13
+ *
14
+ * @param {string | OpenApiSpec} input - OpenAPI spec as JSON object, file path, URL, or raw text
15
+ * @param {string} operationId - The operationId of the operation to create a tool for
16
+ * @param {OpenApiToolsOptions} [options] - Configuration for auth, base URL, etc.
17
+ * @returns {Promise<any>} A single AI SDK tool
18
+ */
19
+ export async function createOpenApiTool(input, operationId, options = {}) {
20
+ const spec = await Effect.runPromise(loadSpecEffect(input));
21
+ return createOpenApiToolFromSpec(spec, operationId, options);
22
+ }
@@ -0,0 +1,22 @@
1
+ // ---------------------------------------------------------------------------
2
+ // createOpenApiToolSync — synchronous single tool creation from OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { loadSpecSync } from "../spec-parser.js";
5
+ import { createOpenApiToolFromSpec } from "./_helpers.js";
6
+
7
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
8
+ /** @typedef {import("../OpenApiTool.ts").OpenApiTool} OpenApiTool */
9
+ /** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
10
+
11
+ /**
12
+ * Synchronous version — only works with specs that are objects or local files.
13
+ *
14
+ * @param {string | OpenApiSpec} input
15
+ * @param {string} operationId
16
+ * @param {OpenApiToolsOptions} [options]
17
+ * @returns {OpenApiTool}
18
+ */
19
+ export function createOpenApiToolSync(input, operationId, options = {}) {
20
+ const spec = loadSpecSync(input);
21
+ return createOpenApiToolFromSpec(spec, operationId, options);
22
+ }
@@ -0,0 +1,21 @@
1
+ // ---------------------------------------------------------------------------
2
+ // createOpenApiTools — async tool creation from OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { Effect } from "effect";
5
+ import { loadSpecEffect } from "../spec-parser.js";
6
+ import { createOpenApiToolsFromSpec } from "./_helpers.js";
7
+
8
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
9
+ /** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
10
+
11
+ /**
12
+ * Create AI SDK tools from all operations in an OpenAPI spec.
13
+ *
14
+ * @param {string | OpenApiSpec} input - OpenAPI spec as JSON object, file path, URL, or raw text
15
+ * @param {OpenApiToolsOptions} [options] - Configuration for auth, filtering, base URL, etc.
16
+ * @returns {Promise<Record<string, any>>} Record of operationId → AI SDK tool
17
+ */
18
+ export async function createOpenApiTools(input, options = {}) {
19
+ const spec = await Effect.runPromise(loadSpecEffect(input));
20
+ return createOpenApiToolsFromSpec(spec, options);
21
+ }
@@ -0,0 +1,20 @@
1
+ // ---------------------------------------------------------------------------
2
+ // createOpenApiToolsSync — synchronous tool creation from OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { loadSpecSync } from "../spec-parser.js";
5
+ import { createOpenApiToolsFromSpec } from "./_helpers.js";
6
+
7
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
8
+ /** @typedef {import("../OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
9
+
10
+ /**
11
+ * Synchronous version — only works with specs that are objects or local files.
12
+ *
13
+ * @param {string | OpenApiSpec} input
14
+ * @param {OpenApiToolsOptions} [options]
15
+ * @returns {Record<string, any>}
16
+ */
17
+ export function createOpenApiToolsSync(input, options = {}) {
18
+ const spec = loadSpecSync(input);
19
+ return createOpenApiToolsFromSpec(spec, options);
20
+ }
@@ -0,0 +1,5 @@
1
+ export { createOpenApiTools } from "./createOpenApiTools.js";
2
+ export { createOpenApiToolsSync } from "./createOpenApiToolsSync.js";
3
+ export { createOpenApiTool } from "./createOpenApiTool.js";
4
+ export { createOpenApiToolSync } from "./createOpenApiToolSync.js";
5
+ export { listOperations } from "./listOperations.js";
@@ -0,0 +1,23 @@
1
+ // ---------------------------------------------------------------------------
2
+ // listOperations — list all operations from an OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { loadSpecSync } from "../spec-parser.js";
5
+ import { extractOperations } from "../spec-parser.js";
6
+
7
+ /** @typedef {import("../OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
8
+
9
+ /**
10
+ * List all operations from a spec (for CLI preview).
11
+ *
12
+ * @param {string | OpenApiSpec} input
13
+ * @returns {Array<{ operationId: string; method: string; path: string; summary: string }>}
14
+ */
15
+ export function listOperations(input) {
16
+ const spec = loadSpecSync(input);
17
+ return extractOperations(spec).map((op) => ({
18
+ operationId: op.operationId,
19
+ method: op.method.toUpperCase(),
20
+ path: op.path,
21
+ summary: op.summary,
22
+ }));
23
+ }
@@ -0,0 +1 @@
1
+ export { createOpenApiTools, createOpenApiToolsSync, createOpenApiTool, createOpenApiToolSync, listOperations, } from "./tool-factory/index.js";
package/src/types.js ADDED
@@ -0,0 +1,20 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./HttpMethod.ts").HttpMethod} HttpMethod */
3
+ /** @typedef {import("./MediaTypeObject.ts").MediaTypeObject} MediaTypeObject */
4
+ /** @typedef {import("./OpenApiAuth.ts").OpenApiAuth} OpenApiAuth */
5
+ /** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
6
+ /** @typedef {import("./OpenApiToolsOptions.ts").OpenApiToolsOptions} OpenApiToolsOptions */
7
+ /** @typedef {import("./OperationObject.ts").OperationObject} OperationObject */
8
+ /** @typedef {import("./ParameterObject.ts").ParameterObject} ParameterObject */
9
+ /** @typedef {import("./ParsedOperation.ts").ParsedOperation} ParsedOperation */
10
+ /** @typedef {import("./PathItem.ts").PathItem} PathItem */
11
+ /** @typedef {import("./RefObject.ts").RefObject} RefObject */
12
+ /** @typedef {import("./RequestBodyObject.ts").RequestBodyObject} RequestBodyObject */
13
+ /** @typedef {import("./SchemaObject.ts").SchemaObject} SchemaObject */
14
+ // @smithers-type-exports-end
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // OpenAPI types — minimal subset of OpenAPI 3.0+ needed for tool generation
18
+ // ---------------------------------------------------------------------------
19
+ /** @type {HttpMethod[]} */
20
+ export const HTTP_METHODS = ["get", "post", "put", "delete", "patch"];