@smithers-orchestrator/agents 0.24.2 → 0.25.1

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.
Files changed (55) hide show
  1. package/package.json +15 -5
  2. package/src/AgentLike.ts +5 -0
  3. package/src/AmpAgent.js +15 -5
  4. package/src/AmpAgentOptions.ts +6 -0
  5. package/src/BaseCliAgent/BaseCliAgent.js +198 -10
  6. package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +21 -3
  7. package/src/BaseCliAgent/index.d.ts +467 -0
  8. package/src/ClaudeCodeAgent.js +6 -2
  9. package/src/CodexAgent.js +4 -0
  10. package/src/GeminiAgent.js +34 -224
  11. package/src/GeminiAgentOptions.ts +4 -9
  12. package/src/OpenCodeAgent.js +2 -12
  13. package/src/OpenCodeAgentOptions.ts +19 -0
  14. package/src/PiAgent.js +4 -0
  15. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +0 -1
  16. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +3 -2
  17. package/src/cli-capabilities/getCliAgentCapabilityReport.js +0 -6
  18. package/src/cli-surface/cliAgentSurfaceManifest.js +1 -40
  19. package/src/createElevenLabsTextToSpeechTool.js +128 -0
  20. package/src/createElevenLabsTextToSpeechTool.ts +33 -0
  21. package/src/diagnostics/getDiagnosticStrategy.js +163 -35
  22. package/src/document-parsing/DocumentParsingProvider.ts +13 -0
  23. package/src/document-parsing/DocumentParsingResult.ts +13 -0
  24. package/src/document-parsing/DocumentParsingToolset.ts +4 -0
  25. package/src/document-parsing/DocumentParsingToolsetOptions.ts +9 -0
  26. package/src/document-parsing/createDocumentParsingToolset.d.ts +9 -0
  27. package/src/document-parsing/createDocumentParsingToolset.js +416 -0
  28. package/src/http/CreateHttpToolOptions.ts +4 -0
  29. package/src/http/HttpToolAuth.ts +15 -0
  30. package/src/http/HttpToolInput.ts +11 -0
  31. package/src/http/HttpToolOutput.ts +7 -0
  32. package/src/http/createHttpTool.js +136 -0
  33. package/src/image-generation/ImageGenerationProvider.ts +7 -0
  34. package/src/image-generation/ImageGenerationRequest.ts +8 -0
  35. package/src/image-generation/ImageGenerationResult.ts +10 -0
  36. package/src/image-generation/ImageGenerationToolOptions.ts +10 -0
  37. package/src/image-generation/createImageGenerationTool.d.ts +18 -0
  38. package/src/image-generation/createImageGenerationTool.js +92 -0
  39. package/src/index.d.ts +490 -147
  40. package/src/index.js +23 -5
  41. package/src/streamResultToGenerateResult.js +55 -26
  42. package/src/transcription/createTranscriptionTool.js +182 -0
  43. package/src/transcription/createTranscriptionTool.ts +29 -0
  44. package/src/transcription/index.js +1 -0
  45. package/src/transcription/index.ts +6 -0
  46. package/src/web-search/GroundedWebSearchProvider.ts +21 -0
  47. package/src/web-search/GroundedWebSearchToolset.ts +6 -0
  48. package/src/web-search/createBraveSearchProvider.js +53 -0
  49. package/src/web-search/createExaSearchProvider.js +72 -0
  50. package/src/web-search/createGroundedWebSearchToolset.js +110 -0
  51. package/src/web-search/createSerperSearchProvider.js +63 -0
  52. package/src/web-search/createTavilySearchProvider.js +59 -0
  53. package/src/web-search/index.js +5 -0
  54. package/src/zodToOpenAISchema.js +4 -0
  55. package/src/OpenCodeAgent.ts +0 -43
@@ -0,0 +1,416 @@
1
+ import { dynamicTool, jsonSchema } from "ai";
2
+
3
+ /** @typedef {import("./DocumentParsingProvider.ts").DocumentParsingProvider} DocumentParsingProvider */
4
+ /** @typedef {import("./DocumentParsingToolset.ts").DocumentParsingToolset} DocumentParsingToolset */
5
+ /** @typedef {import("./DocumentParsingToolsetOptions.ts").DocumentParsingToolsetOptions} DocumentParsingToolsetOptions */
6
+
7
+ const inputSchema = {
8
+ type: "object",
9
+ additionalProperties: false,
10
+ required: ["source"],
11
+ properties: {
12
+ source: {
13
+ oneOf: [
14
+ {
15
+ type: "object",
16
+ additionalProperties: false,
17
+ required: ["type", "url"],
18
+ properties: { type: { const: "url" }, url: { type: "string" } },
19
+ },
20
+ {
21
+ type: "object",
22
+ additionalProperties: false,
23
+ required: ["type", "data"],
24
+ properties: {
25
+ type: { const: "base64" },
26
+ data: { type: "string" },
27
+ mimeType: { type: "string" },
28
+ filename: { type: "string" },
29
+ },
30
+ },
31
+ {
32
+ type: "object",
33
+ additionalProperties: false,
34
+ required: ["type", "text"],
35
+ properties: { type: { const: "text" }, text: { type: "string" }, filename: { type: "string" } },
36
+ },
37
+ ],
38
+ },
39
+ outputFormat: { enum: ["text", "markdown", "json"] },
40
+ instructions: { type: "string" },
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Expose a document parsing / OCR primitive as an AI SDK toolset.
46
+ *
47
+ * The default provider is Firecrawl for document parsing. Mistral OCR and LlamaParse
48
+ * can be selected explicitly, or callers can inject any provider matching the
49
+ * same contract for per-tenant credentials and custom parsing stacks.
50
+ *
51
+ * @param {DocumentParsingToolsetOptions} [options]
52
+ * @returns {DocumentParsingToolset}
53
+ */
54
+ export function createDocumentParsingToolset(options = {}) {
55
+ const provider = resolveProvider(options);
56
+ const toolName = options.toolName ?? "parse_document";
57
+ return {
58
+ tools: {
59
+ [toolName]: dynamicTool({
60
+ description:
61
+ "Parse documents and images into readable text/markdown using Firecrawl, Mistral OCR, LlamaParse, or a custom provider.",
62
+ inputSchema: jsonSchema(inputSchema),
63
+ execute: async (input) => provider.parseDocument(normalizeInput(input)),
64
+ }),
65
+ },
66
+ toolNames: [toolName],
67
+ };
68
+ }
69
+
70
+ /**
71
+ * @param {DocumentParsingToolsetOptions} options
72
+ * @returns {DocumentParsingProvider}
73
+ */
74
+ function resolveProvider(options) {
75
+ if (typeof options.provider === "object" && options.provider) return options.provider;
76
+ const provider = options.provider ?? "firecrawl";
77
+ if (provider === "firecrawl") return createFirecrawlProvider(options);
78
+ if (provider === "mistral-ocr") return createMistralOcrProvider(options);
79
+ if (provider === "llamaparse") return createLlamaParseProvider(options);
80
+ throw new Error(`Unsupported document parsing provider: ${provider}`);
81
+ }
82
+
83
+ /**
84
+ * @param {unknown} input
85
+ * @returns {Parameters<DocumentParsingProvider["parseDocument"]>[0]}
86
+ */
87
+ function normalizeInput(input) {
88
+ const value = /** @type {Parameters<DocumentParsingProvider["parseDocument"]>[0]} */ (input ?? {});
89
+ if (!value.source || typeof value.source !== "object") {
90
+ throw new Error("parse_document requires a source");
91
+ }
92
+ return value;
93
+ }
94
+
95
+ /**
96
+ * @param {DocumentParsingToolsetOptions} options
97
+ * @returns {DocumentParsingProvider}
98
+ */
99
+ function createFirecrawlProvider(options) {
100
+ const request = options.fetch ?? fetch;
101
+ const baseUrl = options.baseUrl ?? "https://api.firecrawl.dev/v2";
102
+ const apiKey = options.apiKey ?? process.env.FIRECRAWL_API_KEY;
103
+ return {
104
+ name: "firecrawl",
105
+ async parseDocument(input) {
106
+ if (input.source.type !== "url") return parseFirecrawlFile(request, baseUrl, apiKey, input);
107
+ const json = await postJson(request, `${baseUrl}/scrape`, apiKey, {
108
+ url: input.source.url,
109
+ formats: [input.outputFormat === "text" ? "markdown" : (input.outputFormat ?? "markdown")],
110
+ onlyMainContent: false,
111
+ });
112
+ const data = pickObject(json, "data") ?? pickObject(json, "result") ?? pickObject(json, undefined);
113
+ const markdown = pickString(data, "markdown");
114
+ const text = pickString(data, "text") ?? pickString(data, "content") ?? markdown ?? "";
115
+ return {
116
+ provider: "firecrawl",
117
+ text,
118
+ ...(markdown ? { markdown } : {}),
119
+ ...(pickObject(data, "metadata") ? { metadata: pickObject(data, "metadata") } : {}),
120
+ raw: json,
121
+ };
122
+ },
123
+ };
124
+ }
125
+
126
+ /**
127
+ * @param {typeof fetch} request
128
+ * @param {string} baseUrl
129
+ * @param {string | undefined} apiKey
130
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]} input
131
+ */
132
+ async function parseFirecrawlFile(request, baseUrl, apiKey, input) {
133
+ const json = await postMultipart(request, `${baseUrl}/parse`, apiKey, createDocumentFormData(input, createFirecrawlOptions(input)));
134
+ const data = pickObject(json, "data") ?? pickObject(json, "result") ?? pickObject(json, undefined);
135
+ const markdown = pickString(data, "markdown");
136
+ const text = pickString(data, "text") ?? pickString(data, "content") ?? markdown ?? "";
137
+ return {
138
+ provider: "firecrawl",
139
+ text,
140
+ ...(markdown ? { markdown } : {}),
141
+ ...(pickObject(data, "metadata") ? { metadata: pickObject(data, "metadata") } : {}),
142
+ raw: json,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * @param {DocumentParsingToolsetOptions} options
148
+ * @returns {DocumentParsingProvider}
149
+ */
150
+ function createMistralOcrProvider(options) {
151
+ const request = options.fetch ?? fetch;
152
+ const baseUrl = options.baseUrl ?? "https://api.mistral.ai/v1";
153
+ const apiKey = options.apiKey ?? process.env.MISTRAL_API_KEY;
154
+ return {
155
+ name: "mistral-ocr",
156
+ async parseDocument(input) {
157
+ if (input.source.type === "text") {
158
+ return { provider: "mistral-ocr", text: input.source.text, raw: { source: "text" } };
159
+ }
160
+ const document = input.source.type === "url"
161
+ ? { type: "document_url", document_url: input.source.url }
162
+ : input.source.type === "base64"
163
+ ? createMistralBase64Document(input.source)
164
+ : undefined;
165
+ const json = await postJson(request, `${baseUrl}/ocr`, apiKey, {
166
+ model: "mistral-ocr-latest",
167
+ document,
168
+ ...(input.instructions ? { prompt: input.instructions } : {}),
169
+ });
170
+ const pages = Array.isArray(json?.pages) ? json.pages.map(normalizePage).filter(Boolean) : undefined;
171
+ const markdown = pages?.map((page) => page.markdown || page.text || "").filter(Boolean).join("\n\n");
172
+ return {
173
+ provider: "mistral-ocr",
174
+ text: markdown || pickString(json, "text") || "",
175
+ ...(markdown ? { markdown } : {}),
176
+ ...(pages?.length ? { pages } : {}),
177
+ raw: json,
178
+ };
179
+ },
180
+ };
181
+ }
182
+
183
+ /**
184
+ * @param {DocumentParsingToolsetOptions} options
185
+ * @returns {DocumentParsingProvider}
186
+ */
187
+ function createLlamaParseProvider(options) {
188
+ const request = options.fetch ?? fetch;
189
+ const baseUrl = options.baseUrl ?? "https://api.cloud.llamaindex.ai";
190
+ const apiKey = options.apiKey ?? process.env.LLAMA_CLOUD_API_KEY;
191
+ return {
192
+ name: "llamaparse",
193
+ async parseDocument(input) {
194
+ const fileId = input.source.type === "url" ? undefined : await uploadLlamaParseFile(request, baseUrl, apiKey, input);
195
+ const created = await postJson(request, `${baseUrl}/api/v2/parse`, apiKey, {
196
+ ...(fileId ? { file_id: fileId } : { source_url: input.source.url }),
197
+ tier: "agentic",
198
+ version: "latest",
199
+ expand: input.outputFormat === "text" ? ["text_full", "metadata"] : ["markdown_full", "text_full", "metadata"],
200
+ ...(input.instructions ? { parsing_instruction: input.instructions } : {}),
201
+ });
202
+ const jobId = pickString(pickObject(created, "job") ?? created, "id");
203
+ if (!jobId) throw new Error("LlamaParse did not return a parse job id");
204
+ const json = await pollLlamaParseJob(request, baseUrl, apiKey, jobId, input.outputFormat);
205
+ const markdown = pickString(json, "markdown_full") ?? pickString(pickObject(json, "markdown"), "text");
206
+ const text = pickString(json, "text_full") ?? pickString(pickObject(json, "text"), "text") ?? markdown ?? "";
207
+ return {
208
+ provider: "llamaparse",
209
+ text,
210
+ ...(markdown ? { markdown } : {}),
211
+ ...(pickObject(json, "metadata") ? { metadata: pickObject(json, "metadata") } : {}),
212
+ raw: json,
213
+ };
214
+ },
215
+ };
216
+ }
217
+
218
+ /**
219
+ * @param {typeof fetch} request
220
+ * @param {string} baseUrl
221
+ * @param {string | undefined} apiKey
222
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]} input
223
+ */
224
+ async function uploadLlamaParseFile(request, baseUrl, apiKey, input) {
225
+ const json = await postMultipart(request, `${baseUrl}/api/v1/files/`, apiKey, createDocumentFormData(input));
226
+ const fileId = pickString(json, "id");
227
+ if (!fileId) throw new Error("LlamaParse did not return an uploaded file id");
228
+ return fileId;
229
+ }
230
+
231
+ /**
232
+ * @param {typeof fetch} request
233
+ * @param {string} url
234
+ * @param {string | undefined} apiKey
235
+ * @param {unknown} body
236
+ * @returns {Promise<any>}
237
+ */
238
+ async function postJson(request, url, apiKey, body) {
239
+ if (!apiKey) throw new Error(`Missing API key for ${url}`);
240
+ const response = await request(url, {
241
+ method: "POST",
242
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
243
+ body: JSON.stringify(body),
244
+ });
245
+ if (!response.ok) {
246
+ const message = await response.text().catch(() => "");
247
+ throw new Error(`Document parsing provider failed (${response.status}): ${message || response.statusText}`);
248
+ }
249
+ return response.json();
250
+ }
251
+
252
+ /**
253
+ * @param {typeof fetch} request
254
+ * @param {string} url
255
+ * @param {string | undefined} apiKey
256
+ * @param {FormData} body
257
+ * @returns {Promise<any>}
258
+ */
259
+ async function postMultipart(request, url, apiKey, body) {
260
+ if (!apiKey) throw new Error(`Missing API key for ${url}`);
261
+ const response = await request(url, {
262
+ method: "POST",
263
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
264
+ body,
265
+ });
266
+ if (!response.ok) {
267
+ const message = await response.text().catch(() => "");
268
+ throw new Error(`Document parsing provider failed (${response.status}): ${message || response.statusText}`);
269
+ }
270
+ return response.json();
271
+ }
272
+
273
+ /**
274
+ * @param {typeof fetch} request
275
+ * @param {string} baseUrl
276
+ * @param {string | undefined} apiKey
277
+ * @param {string} jobId
278
+ * @param {"text" | "markdown" | "json" | undefined} outputFormat
279
+ * @returns {Promise<any>}
280
+ */
281
+ async function pollLlamaParseJob(request, baseUrl, apiKey, jobId, outputFormat) {
282
+ const expand = outputFormat === "text" ? "text_full,metadata" : "markdown_full,text_full,metadata";
283
+ for (let attempt = 0; attempt < 20; attempt += 1) {
284
+ const json = await getJson(request, `${baseUrl}/api/v2/parse/${encodeURIComponent(jobId)}?expand=${expand}`, apiKey);
285
+ const job = pickObject(json, "job") ?? json;
286
+ const status = pickString(job, "status");
287
+ if (status === "COMPLETED" || status === "completed") return json;
288
+ if (status === "FAILED" || status === "failed" || status === "CANCELLED" || status === "cancelled") {
289
+ throw new Error(`LlamaParse job ${jobId} ${status}: ${pickString(job, "error_message") ?? "no error details"}`);
290
+ }
291
+ await new Promise((resolve) => setTimeout(resolve, 1000));
292
+ }
293
+ throw new Error(`LlamaParse job ${jobId} did not complete before timeout`);
294
+ }
295
+
296
+ /**
297
+ * @param {typeof fetch} request
298
+ * @param {string} url
299
+ * @param {string | undefined} apiKey
300
+ * @returns {Promise<any>}
301
+ */
302
+ async function getJson(request, url, apiKey) {
303
+ if (!apiKey) throw new Error(`Missing API key for ${url}`);
304
+ const response = await request(url, {
305
+ method: "GET",
306
+ headers: { Authorization: `Bearer ${apiKey}` },
307
+ });
308
+ if (!response.ok) {
309
+ const message = await response.text().catch(() => "");
310
+ throw new Error(`Document parsing provider failed (${response.status}): ${message || response.statusText}`);
311
+ }
312
+ return response.json();
313
+ }
314
+
315
+ /**
316
+ * @param {unknown} value
317
+ * @returns {{ index: number; text?: string; markdown?: string; images?: unknown[] } | null}
318
+ */
319
+ function normalizePage(value) {
320
+ if (!value || typeof value !== "object") return null;
321
+ const page = /** @type {Record<string, unknown>} */ (value);
322
+ return {
323
+ index: typeof page.index === "number" ? page.index : typeof page.page === "number" ? page.page : 0,
324
+ ...(typeof page.text === "string" ? { text: page.text } : {}),
325
+ ...(typeof page.markdown === "string" ? { markdown: page.markdown } : {}),
326
+ ...(Array.isArray(page.images) ? { images: page.images } : {}),
327
+ };
328
+ }
329
+
330
+ /**
331
+ * @param {unknown} value
332
+ * @param {string | undefined} key
333
+ * @returns {Record<string, unknown> | undefined}
334
+ */
335
+ function pickObject(value, key) {
336
+ const target = key && value && typeof value === "object" ? /** @type {Record<string, unknown>} */ (value)[key] : value;
337
+ return target && typeof target === "object" && !Array.isArray(target) ? /** @type {Record<string, unknown>} */ (target) : undefined;
338
+ }
339
+
340
+ /**
341
+ * @param {unknown} value
342
+ * @param {string} key
343
+ * @returns {string | undefined}
344
+ */
345
+ function pickString(value, key) {
346
+ return value && typeof value === "object" && typeof /** @type {Record<string, unknown>} */ (value)[key] === "string"
347
+ ? /** @type {string} */ (/** @type {Record<string, unknown>} */ (value)[key])
348
+ : undefined;
349
+ }
350
+
351
+ /**
352
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]} input
353
+ * @param {unknown} [options]
354
+ * @returns {FormData}
355
+ */
356
+ function createDocumentFormData(input, options) {
357
+ const form = new FormData();
358
+ form.append("file", sourceToBlob(input.source), sourceFilename(input.source));
359
+ if (options) form.append("options", JSON.stringify(options));
360
+ return form;
361
+ }
362
+
363
+ /**
364
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]} input
365
+ */
366
+ function createFirecrawlOptions(input) {
367
+ return {
368
+ formats: [input.outputFormat === "json" ? "json" : (input.outputFormat ?? "markdown")],
369
+ ...(input.source.type === "base64" && (input.source.mimeType ?? "").includes("pdf")
370
+ ? { parsers: [{ type: "pdf", mode: "auto" }] }
371
+ : {}),
372
+ ...(input.instructions ? { prompt: input.instructions } : {}),
373
+ };
374
+ }
375
+
376
+ /**
377
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]["source"]} source
378
+ * @returns {Blob}
379
+ */
380
+ function sourceToBlob(source) {
381
+ if (source.type === "base64") {
382
+ return new Blob([Buffer.from(source.data, "base64")], { type: source.mimeType ?? "application/octet-stream" });
383
+ }
384
+ if (source.type === "text") return new Blob([source.text], { type: "text/html" });
385
+ throw new Error("File upload requires base64 or text source");
386
+ }
387
+
388
+ /**
389
+ * @param {Parameters<DocumentParsingProvider["parseDocument"]>[0]["source"]} source
390
+ * @returns {string}
391
+ */
392
+ function sourceFilename(source) {
393
+ if (source.type === "base64") return source.filename ?? defaultFilename(source.mimeType);
394
+ if (source.type === "text") return source.filename ?? "document.html";
395
+ return "document";
396
+ }
397
+
398
+ /**
399
+ * @param {string | undefined} mimeType
400
+ */
401
+ function defaultFilename(mimeType) {
402
+ if (mimeType === "application/pdf") return "document.pdf";
403
+ if (mimeType === "text/html") return "document.html";
404
+ if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") return "document.docx";
405
+ if (mimeType === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") return "document.xlsx";
406
+ return "document.bin";
407
+ }
408
+
409
+ /**
410
+ * @param {{ type: "base64"; data: string; mimeType?: string; filename?: string }} source
411
+ */
412
+ function createMistralBase64Document(source) {
413
+ const dataUrl = `data:${source.mimeType ?? "application/pdf"};base64,${source.data}`;
414
+ if ((source.mimeType ?? "").startsWith("image/")) return { type: "image_url", image_url: dataUrl };
415
+ return { type: "document_url", document_url: dataUrl };
416
+ }
@@ -0,0 +1,4 @@
1
+ export type CreateHttpToolOptions = {
2
+ description?: string;
3
+ defaultHeaders?: Record<string, string>;
4
+ };
@@ -0,0 +1,15 @@
1
+ export type HttpToolAuth =
2
+ | {
3
+ type: "bearer";
4
+ token: string;
5
+ }
6
+ | {
7
+ type: "basic";
8
+ username: string;
9
+ password: string;
10
+ }
11
+ | {
12
+ type: "header";
13
+ name: string;
14
+ value: string;
15
+ };
@@ -0,0 +1,11 @@
1
+ import type { HttpToolAuth } from "./HttpToolAuth.ts";
2
+
3
+ export type HttpToolInput = {
4
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
5
+ url: string;
6
+ headers?: Record<string, string>;
7
+ query?: Record<string, string | number | boolean | null | undefined>;
8
+ body?: unknown;
9
+ auth?: HttpToolAuth;
10
+ timeoutMs?: number;
11
+ };
@@ -0,0 +1,7 @@
1
+ export type HttpToolOutput = {
2
+ ok: boolean;
3
+ status: number;
4
+ statusText: string;
5
+ headers: Record<string, string>;
6
+ body: unknown;
7
+ };
@@ -0,0 +1,136 @@
1
+ import { dynamicTool } from "ai";
2
+ import { z } from "zod";
3
+
4
+ /** @typedef {import("ai").Tool} Tool */
5
+ /** @typedef {import("./CreateHttpToolOptions.ts").CreateHttpToolOptions} CreateHttpToolOptions */
6
+ /** @typedef {import("./HttpToolInput.ts").HttpToolInput} HttpToolInput */
7
+ /** @typedef {import("./HttpToolOutput.ts").HttpToolOutput} HttpToolOutput */
8
+
9
+ const httpToolInputSchema = z.object({
10
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().default("GET"),
11
+ url: z.string().url(),
12
+ headers: z.record(z.string(), z.string()).optional(),
13
+ query: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
14
+ body: z.unknown().optional(),
15
+ auth: z
16
+ .discriminatedUnion("type", [
17
+ z.object({ type: z.literal("bearer"), token: z.string() }),
18
+ z.object({ type: z.literal("basic"), username: z.string(), password: z.string() }),
19
+ z.object({ type: z.literal("header"), name: z.string(), value: z.string() }),
20
+ ])
21
+ .optional(),
22
+ timeoutMs: z.number().int().positive().optional(),
23
+ });
24
+
25
+ /**
26
+ * Create an AI SDK tool that can call any REST API without an OpenAPI spec.
27
+ *
28
+ * @param {CreateHttpToolOptions} [options]
29
+ * @returns {Tool}
30
+ */
31
+ export function createHttpTool(options = {}) {
32
+ return dynamicTool({
33
+ description:
34
+ options.description ??
35
+ "Call any REST API by providing method, url, headers, query params, body, and optional auth.",
36
+ inputSchema: httpToolInputSchema,
37
+ execute: async (input) => executeHttpRequest(/** @type {HttpToolInput} */ (input), options),
38
+ });
39
+ }
40
+
41
+ /**
42
+ * @param {HttpToolInput} input
43
+ * @param {CreateHttpToolOptions} options
44
+ * @returns {Promise<HttpToolOutput>}
45
+ */
46
+ async function executeHttpRequest(input, options) {
47
+ const url = new URL(input.url);
48
+ for (const [key, value] of Object.entries(input.query ?? {})) {
49
+ if (value !== null && value !== undefined) {
50
+ url.searchParams.set(key, String(value));
51
+ }
52
+ }
53
+
54
+ const headers = new Headers(options.defaultHeaders ?? {});
55
+ for (const [key, value] of Object.entries(input.headers ?? {})) {
56
+ headers.set(key, value);
57
+ }
58
+ applyAuth(headers, input.auth);
59
+
60
+ const init = /** @type {RequestInit} */ ({
61
+ method: input.method ?? "GET",
62
+ headers,
63
+ });
64
+ if (input.body !== undefined && init.method !== "GET" && init.method !== "HEAD") {
65
+ init.body = serializeBody(input.body, headers);
66
+ }
67
+
68
+ const controller = input.timeoutMs ? new AbortController() : null;
69
+ const timeout = controller ? setTimeout(() => controller.abort(), input.timeoutMs) : null;
70
+ if (controller) {
71
+ init.signal = controller.signal;
72
+ }
73
+ try {
74
+ const response = await fetch(url, init);
75
+ return {
76
+ ok: response.ok,
77
+ status: response.status,
78
+ statusText: response.statusText,
79
+ headers: Object.fromEntries(response.headers.entries()),
80
+ body: await parseResponseBody(response),
81
+ };
82
+ } finally {
83
+ if (timeout) {
84
+ clearTimeout(timeout);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {Headers} headers
91
+ * @param {HttpToolInput["auth"]} auth
92
+ */
93
+ function applyAuth(headers, auth) {
94
+ if (!auth) return;
95
+ if (auth.type === "bearer") {
96
+ headers.set("authorization", `Bearer ${auth.token}`);
97
+ } else if (auth.type === "basic") {
98
+ headers.set("authorization", `Basic ${btoa(`${auth.username}:${auth.password}`)}`);
99
+ } else {
100
+ headers.set(auth.name, auth.value);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * @param {unknown} body
106
+ * @param {Headers} headers
107
+ * @returns {BodyInit}
108
+ */
109
+ function serializeBody(body, headers) {
110
+ if (typeof body === "string" || body instanceof Blob || body instanceof FormData || body instanceof URLSearchParams) {
111
+ return body;
112
+ }
113
+ if (!headers.has("content-type")) {
114
+ headers.set("content-type", "application/json");
115
+ }
116
+ return JSON.stringify(body);
117
+ }
118
+
119
+ /**
120
+ * @param {Response} response
121
+ * @returns {Promise<unknown>}
122
+ */
123
+ async function parseResponseBody(response) {
124
+ if (response.status === 204 || response.status === 205) {
125
+ return null;
126
+ }
127
+ const text = await response.text();
128
+ if (!text) {
129
+ return null;
130
+ }
131
+ const contentType = response.headers.get("content-type") ?? "";
132
+ if (contentType.includes("application/json")) {
133
+ return JSON.parse(text);
134
+ }
135
+ return text;
136
+ }
@@ -0,0 +1,7 @@
1
+ import type { ImageGenerationRequest } from "./ImageGenerationRequest.js";
2
+ import type { ImageGenerationResult } from "./ImageGenerationResult.js";
3
+
4
+ export type ImageGenerationProvider = {
5
+ name?: string;
6
+ generateImage(request: ImageGenerationRequest): Promise<ImageGenerationResult> | ImageGenerationResult;
7
+ };
@@ -0,0 +1,8 @@
1
+ export type ImageGenerationRequest = {
2
+ prompt: string;
3
+ model?: string;
4
+ size?: string;
5
+ count?: number;
6
+ seed?: number;
7
+ style?: string;
8
+ };
@@ -0,0 +1,10 @@
1
+ export type ImageGenerationResult = {
2
+ provider?: string;
3
+ model?: string;
4
+ images: Array<{
5
+ url?: string;
6
+ base64?: string;
7
+ mimeType?: string;
8
+ revisedPrompt?: string;
9
+ }>;
10
+ };
@@ -0,0 +1,10 @@
1
+ export type ImageGenerationToolOptions = {
2
+ /** Tool name used when returning a toolset. */
3
+ name?: string;
4
+ /** Description shown to the model. */
5
+ description?: string;
6
+ /** Provider model to use when the agent does not specify one. */
7
+ model?: string;
8
+ /** Return `{ [name]: tool }` for direct mounting on an agent. */
9
+ asToolset?: boolean;
10
+ };
@@ -0,0 +1,18 @@
1
+ import type { Tool } from "ai";
2
+ import type { ImageGenerationProvider } from "./ImageGenerationProvider.js";
3
+ import type { ImageGenerationToolOptions } from "./ImageGenerationToolOptions.js";
4
+
5
+ export type { ImageGenerationProvider } from "./ImageGenerationProvider.js";
6
+ export type { ImageGenerationRequest } from "./ImageGenerationRequest.js";
7
+ export type { ImageGenerationResult } from "./ImageGenerationResult.js";
8
+ export type { ImageGenerationToolOptions } from "./ImageGenerationToolOptions.js";
9
+
10
+ export declare function createImageGenerationTool(
11
+ provider: ImageGenerationProvider,
12
+ options: ImageGenerationToolOptions & { asToolset: true },
13
+ ): Record<string, Tool>;
14
+
15
+ export declare function createImageGenerationTool(
16
+ provider: ImageGenerationProvider,
17
+ options?: ImageGenerationToolOptions,
18
+ ): Tool;