openapi-ai-generator 0.1.3 → 0.1.5

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.
package/dist/cli.js CHANGED
@@ -173,7 +173,12 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
173
173
  }
174
174
  let pathItem;
175
175
  try {
176
- pathItem = await callLLM(route, options.jsdocMode, getModel());
176
+ pathItem = await callLLM(
177
+ route,
178
+ options.jsdocMode,
179
+ getModel(),
180
+ options.limits
181
+ );
177
182
  } catch (err) {
178
183
  const message = err instanceof Error ? err.message : String(err);
179
184
  const isContentFilter = message.includes("content filtering policy") || message.includes("content_filter") || err?.status === 400;
@@ -238,12 +243,14 @@ For each exported function named GET, POST, PUT, PATCH, or DELETE, document:
238
243
 
239
244
  Return a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text \u2014 only the JSON object.`;
240
245
  }
241
- async function callLLM(route, jsdocMode, model) {
246
+ async function callLLM(route, jsdocMode, model, limits) {
242
247
  const prompt = buildPrompt(route, jsdocMode);
243
248
  const { text } = await (0, import_ai.generateText)({
244
249
  model,
245
250
  prompt,
246
- temperature: 0
251
+ temperature: 0,
252
+ maxOutputTokens: limits.maxTokens,
253
+ abortSignal: AbortSignal.timeout(limits.timeoutMs)
247
254
  });
248
255
  return parsePathItem(text, route.urlPath);
249
256
  }
@@ -277,7 +284,11 @@ var defaults = {
277
284
  cacheDir: ".openapi-cache",
278
285
  include: ["src/app/api/**/route.ts"],
279
286
  exclude: [],
280
- envFile: [".env", ".env.local"]
287
+ envFile: [".env", ".env.local"],
288
+ limits: {
289
+ maxTokens: 4e3,
290
+ timeoutMs: 6e4
291
+ }
281
292
  };
282
293
  function resolveConfig(config) {
283
294
  let envFile;
@@ -292,9 +303,14 @@ function resolveConfig(config) {
292
303
  ...defaults,
293
304
  ...config,
294
305
  envFile,
306
+ limits: {
307
+ ...defaults.limits,
308
+ ...config.limits
309
+ },
295
310
  output: {
296
311
  scalarDocs: false,
297
312
  scalarPath: "src/app/api/docs/route.ts",
313
+ scalarConfig: {},
298
314
  ...config.output
299
315
  },
300
316
  openapi: {
@@ -407,6 +423,7 @@ export function GET() {
407
423
  <script
408
424
  id="api-reference"
409
425
  data-url="${specUrl}"
426
+ ${config.output.scalarConfig && Object.keys(config.output.scalarConfig).length > 0 ? `data-config="${JSON.stringify(config.output.scalarConfig)}"` : ""}
410
427
  ></script>
411
428
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
412
429
  </body>
@@ -515,12 +532,15 @@ async function generate(options = {}) {
515
532
  console.log(`[openapi-ai-generator] Scanning routes...`);
516
533
  const routes = await scanRoutes(config.include, config.exclude, cwd);
517
534
  console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);
518
- console.log(`[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`);
535
+ console.log(
536
+ `[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`
537
+ );
519
538
  const analyzed = await analyzeRoutes(routes, {
520
539
  provider: config.provider,
521
540
  jsdocMode: config.jsdocMode,
522
541
  cache: config.cache,
523
- cacheDir: config.cacheDir
542
+ cacheDir: config.cacheDir,
543
+ limits: config.limits
524
544
  });
525
545
  const fromCache = analyzed.filter((r) => r.fromCache).length;
526
546
  const skippedLLM = analyzed.filter((r) => r.skippedLLM).length;
@@ -529,9 +549,13 @@ async function generate(options = {}) {
529
549
  );
530
550
  const spec = assembleSpec(config, analyzed);
531
551
  writeOutputFiles(config, spec, cwd);
532
- console.log(`[openapi-ai-generator] Spec written to ${config.output.specPath}`);
552
+ console.log(
553
+ `[openapi-ai-generator] Spec written to ${config.output.specPath}`
554
+ );
533
555
  if (config.output.scalarDocs) {
534
- console.log(`[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`);
556
+ console.log(
557
+ `[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`
558
+ );
535
559
  }
536
560
  return {
537
561
  routesAnalyzed: analyzed.length,
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/analyzer.ts","../src/cache.ts","../src/providers/index.ts","../src/config.ts","../src/generator.ts","../src/scanner.ts","../src/index.ts"],"sourcesContent":["import { Command } from 'commander';\n\nimport type { Provider } from './config.js';\n\nimport { generate } from './index.js';\n\nconst program = new Command();\n\nprogram\n\t.name('openapi-ai-generator')\n\t.description('Generate OpenAPI 3.1 specs from Next.js API routes using AI')\n\t.version('0.1.0');\n\nprogram\n\t.command('generate')\n\t.description('Scan Next.js API routes and generate an OpenAPI spec')\n\t.option('-c, --config <path>', 'Path to config file (default: openapi-gen.config.ts)')\n\t.option('-p, --provider <provider>', 'Override provider (azure | openai | anthropic)')\n\t.option('--no-cache', 'Disable caching (always re-analyze all routes)')\n\t.action(async (opts: { config?: string; provider?: string; cache: boolean }) => {\n\t\ttry {\n\t\t\tconst result = await generate({\n\t\t\t\tconfig: opts.config,\n\t\t\t\tprovider: opts.provider as Provider | undefined,\n\t\t\t\tcache: opts.cache,\n\t\t\t});\n\n\t\t\tconsole.log(`\\n✓ OpenAPI spec generated successfully`);\n\t\t\tconsole.log(` Routes analyzed: ${result.routesAnalyzed}`);\n\t\t\tconsole.log(` From cache: ${result.routesFromCache}`);\n\t\t\tconsole.log(\n\t\t\t\t` LLM calls made: ${result.routesAnalyzed - result.routesFromCache - (result.routesSkippedLLM - result.routesFromCache)}`,\n\t\t\t);\n\t\t\tconsole.log(` Spec written to: ${result.specPath}`);\n\t\t} catch (err) {\n\t\t\tconsole.error('[openapi-ai-generator] Error:', err instanceof Error ? err.message : err);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram.parse();\n","import type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\n\nimport type { JSDocMode, Provider } from \"./config.js\";\nimport type { RouteInfo } from \"./scanner.js\";\n\nimport { computeHash, RouteCache } from \"./cache.js\";\nimport { createModel, getModelId } from \"./providers/index.js\";\n\nexport interface AnalyzeOptions {\n provider: Provider;\n jsdocMode: JSDocMode;\n cache: boolean;\n cacheDir: string;\n}\n\nexport interface AnalyzedRoute {\n urlPath: string;\n pathItem: Record<string, unknown>;\n fromCache: boolean;\n skippedLLM: boolean;\n}\n\nexport async function analyzeRoutes(\n routes: RouteInfo[],\n options: AnalyzeOptions,\n): Promise<AnalyzedRoute[]> {\n const modelId = getModelId(options.provider);\n const cache = options.cache ? new RouteCache(options.cacheDir) : null;\n\n // Lazy-init the model only when we actually need it\n let model: LanguageModel | null = null;\n const getModel = (): LanguageModel => {\n if (!model) model = createModel(options.provider);\n return model;\n };\n\n const results: AnalyzedRoute[] = [];\n\n for (const route of routes) {\n const result = await analyzeRoute(route, options, modelId, cache, getModel);\n results.push(result);\n }\n\n return results;\n}\n\nasync function analyzeRoute(\n route: RouteInfo,\n options: AnalyzeOptions,\n modelId: string,\n cache: RouteCache | null,\n getModel: () => LanguageModel,\n): Promise<AnalyzedRoute> {\n // If @openapi-exact is present or jsdocMode is 'exact', skip LLM\n if (route.hasExactJsdoc && route.exactPathItem) {\n return {\n urlPath: route.urlPath,\n pathItem: route.exactPathItem,\n fromCache: false,\n skippedLLM: true,\n };\n }\n\n if (options.jsdocMode === \"exact\") {\n // In exact mode, if there's a @openapi tag, use it; otherwise still use LLM\n if (route.hasExactJsdoc && route.exactPathItem) {\n return {\n urlPath: route.urlPath,\n pathItem: route.exactPathItem,\n fromCache: false,\n skippedLLM: true,\n };\n }\n }\n\n // Compute cache hash\n const hash = computeHash(route.sourceCode, options.provider, modelId);\n\n // Check cache\n if (cache) {\n const cached = cache.get(hash);\n if (cached) {\n return {\n urlPath: route.urlPath,\n pathItem: cached,\n fromCache: true,\n skippedLLM: true,\n };\n }\n }\n\n // Call LLM\n let pathItem: Record<string, unknown>;\n try {\n pathItem = await callLLM(route, options.jsdocMode, getModel());\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isContentFilter =\n message.includes(\"content filtering policy\") ||\n message.includes(\"content_filter\") ||\n (err as { status?: number })?.status === 400;\n\n if (isContentFilter) {\n console.warn(\n `Warning: Content filter blocked response for ${route.urlPath}. ` +\n `Skipping route. Use @openapi-exact JSDoc to provide the spec manually.`,\n );\n return {\n urlPath: route.urlPath,\n pathItem: {},\n fromCache: false,\n skippedLLM: false,\n };\n }\n throw err;\n }\n\n // Store in cache\n if (cache) {\n cache.set(hash, pathItem);\n }\n\n return {\n urlPath: route.urlPath,\n pathItem,\n fromCache: false,\n skippedLLM: false,\n };\n}\n\nfunction buildPrompt(route: RouteInfo, jsdocMode: JSDocMode): string {\n const jsDocSection =\n route.jsdocComments.length > 0\n ? `JSDoc COMMENTS (use as ${jsdocMode === \"context\" ? \"additional context\" : \"primary source\"}):\\n${route.jsdocComments.join(\"\\n\\n\")}`\n : \"No JSDoc comments found.\";\n\n return `You are a technical documentation tool that reads existing source code and produces OpenAPI 3.1 documentation data. You do not write or execute code — you only read and describe it.\n\n## Task\n\nRead the Next.js API route source file below and produce a JSON object that documents its HTTP endpoints according to the OpenAPI 3.1 PathItem schema.\n\n## Route metadata\n\n- File: ${route.relativePath}\n- URL path: ${route.urlPath}\n\n## Source file contents\n\n\\`\\`\\`typescript\n${route.sourceCode}\n\\`\\`\\`\n\n${jsDocSection}\n\n## Instructions\n\nFor each exported function named GET, POST, PUT, PATCH, or DELETE, document:\n- operationId: a unique camelCase identifier\n- summary: a short one-line description (max ~4 words, avoid starting with \"Get\", \"Post\", \"Put\", \"Patch\", \"Delete\", these are sometimes inferred from the operationId, and are sometimes in the format of \"Create a ___\", \"Update a ___\", \"Delete a ___\", \"Get a ___\", \"List ___\")\n- description: a fuller explanation of what the endpoint does\n- parameters: path params from URL segments like {id}, and query params from searchParams usage\n- requestBody: schema inferred from request.json() calls and TypeScript types (POST/PUT/PATCH only)\n- responses: per status code, inferred from NextResponse.json() calls and return type annotations\n- tags: inferred from the URL path segments, but typically only one tag should be used. If more than one exists, they show up in multiple categories, and that can be confusing. If there's a question, use the more specific option.\n- security: noted if the code checks for auth tokens, session cookies, or middleware guards\n\n## Output format\n\nReturn a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text — only the JSON object.`;\n}\n\nasync function callLLM(\n route: RouteInfo,\n jsdocMode: JSDocMode,\n model: LanguageModel,\n): Promise<Record<string, unknown>> {\n const prompt = buildPrompt(route, jsdocMode);\n\n const { text } = await generateText({\n model,\n prompt,\n temperature: 0,\n });\n\n return parsePathItem(text, route.urlPath);\n}\n\nfunction parsePathItem(text: string, urlPath: string): Record<string, unknown> {\n // Strip any accidental markdown code fences\n let json = text.trim();\n if (json.startsWith(\"```\")) {\n json = json\n .replace(/^```(?:json)?\\s*/i, \"\")\n .replace(/\\s*```$/, \"\")\n .trim();\n }\n\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed !== \"object\" ||\n Array.isArray(parsed) ||\n parsed === null\n ) {\n throw new Error(\"Response is not a JSON object\");\n }\n return parsed;\n } catch (err) {\n console.warn(\n `Warning: Failed to parse LLM response for ${urlPath}. Using empty PathItem.`,\n err,\n );\n return {};\n }\n}\n","import { createHash } from 'crypto';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\n\nexport interface CacheEntry {\n\thash: string;\n\tpathItem: Record<string, unknown>;\n\tcachedAt: string;\n}\n\nexport function computeHash(content: string, provider: string, modelId: string): string {\n\treturn createHash('sha256').update(content).update(provider).update(modelId).digest('hex');\n}\n\nexport class RouteCache {\n\tprivate readonly cacheDir: string;\n\n\tconstructor(cacheDir: string) {\n\t\tthis.cacheDir = cacheDir;\n\t}\n\n\tprivate ensureDir(): void {\n\t\tif (!existsSync(this.cacheDir)) {\n\t\t\tmkdirSync(this.cacheDir, { recursive: true });\n\t\t}\n\t}\n\n\tget(hash: string): Record<string, unknown> | null {\n\t\tconst filePath = join(this.cacheDir, `${hash}.json`);\n\t\tif (!existsSync(filePath)) return null;\n\t\ttry {\n\t\t\tconst entry: CacheEntry = JSON.parse(readFileSync(filePath, 'utf8'));\n\t\t\treturn entry.pathItem;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tset(hash: string, pathItem: Record<string, unknown>): void {\n\t\tthis.ensureDir();\n\t\tconst entry: CacheEntry = {\n\t\t\thash,\n\t\t\tpathItem,\n\t\t\tcachedAt: new Date().toISOString(),\n\t\t};\n\t\tconst filePath = join(this.cacheDir, `${hash}.json`);\n\t\twriteFileSync(filePath, JSON.stringify(entry, null, 2), 'utf8');\n\t}\n}\n","import type { LanguageModel } from \"ai\";\n\nimport type { Provider } from \"../config.js\";\n\nexport function createModel(provider: Provider): LanguageModel {\n switch (provider) {\n case \"azure\":\n return createAzureModel();\n case \"openai\":\n return createOpenAIModel();\n case \"anthropic\":\n return createAnthropicModel();\n default: {\n const _exhaustive: never = provider;\n throw new Error(`Unknown provider: ${_exhaustive}`);\n }\n }\n}\n\nfunction createAzureModel(): LanguageModel {\n const endpoint = requireEnv(\"AZURE_OPENAI_ENDPOINT\");\n const resourceName = endpoint?.match(/https?:\\/\\/([^.]+)/)?.[1];\n const apiKey = requireEnv(\"AZURE_OPENAI_API_KEY\");\n const deployment = requireEnv(\"AZURE_OPENAI_DEPLOYMENT\");\n\n // Dynamic import to avoid loading unused provider SDKs\n const { createAzure } = require(\"@ai-sdk/azure\");\n const azure = createAzure({ resourceName, apiKey });\n return azure(deployment);\n}\n\nfunction createOpenAIModel(): LanguageModel {\n const apiKey = requireEnv(\"OPENAI_API_KEY\");\n const model = process.env.OPENAI_MODEL ?? \"gpt-4o\";\n\n const { createOpenAI } = require(\"@ai-sdk/openai\");\n const openai = createOpenAI({ apiKey });\n return openai(model);\n}\n\nfunction createAnthropicModel(): LanguageModel {\n const apiKey = requireEnv(\"ANTHROPIC_API_KEY\");\n const model = process.env.ANTHROPIC_MODEL ?? \"claude-sonnet-4-6\";\n\n const { createAnthropic } = require(\"@ai-sdk/anthropic\");\n const anthropic = createAnthropic({ apiKey });\n return anthropic(model);\n}\n\nfunction requireEnv(name: string): string {\n const val = process.env[name];\n if (!val) {\n throw new Error(`Required environment variable ${name} is not set.`);\n }\n return val;\n}\n\nexport function getModelId(provider: Provider): string {\n switch (provider) {\n case \"azure\":\n return process.env.AZURE_OPENAI_DEPLOYMENT ?? \"unknown\";\n case \"openai\":\n return process.env.OPENAI_MODEL ?? \"gpt-4o\";\n case \"anthropic\":\n return process.env.ANTHROPIC_MODEL ?? \"claude-sonnet-4-6\";\n }\n}\n","import { existsSync } from \"fs\";\nimport { resolve } from \"path\";\nimport { pathToFileURL } from \"url\";\n\nexport type Provider = \"azure\" | \"openai\" | \"anthropic\";\nexport type JSDocMode = \"context\" | \"exact\";\n\nexport interface OpenAPIGenConfig {\n provider: Provider;\n output: {\n specPath: string;\n scalarDocs?: boolean;\n scalarPath?: string;\n };\n openapi: {\n title: string;\n version: string;\n description?: string;\n servers?: Array<{ url: string; description?: string }>;\n security: Array<{ [key: string]: string[] }>;\n components?: {\n securitySchemes?: {\n [key: string]: {\n type: string;\n in: string;\n name: string;\n };\n };\n };\n };\n jsdocMode?: JSDocMode;\n cache?: boolean;\n cacheDir?: string;\n include?: string[];\n exclude?: string[];\n /**\n * Path(s) to .env files to load before running. Defaults to ['.env', '.env.local'].\n * Set to false to disable automatic .env loading.\n */\n envFile?: string | string[] | false;\n}\n\nexport interface ResolvedConfig extends Omit<\n Required<OpenAPIGenConfig>,\n \"envFile\"\n> {\n output: Required<OpenAPIGenConfig[\"output\"]>;\n openapi: Required<OpenAPIGenConfig[\"openapi\"]>;\n envFile: string[] | false;\n}\n\nconst defaults: Omit<ResolvedConfig, \"provider\" | \"output\" | \"openapi\"> = {\n jsdocMode: \"context\",\n cache: true,\n cacheDir: \".openapi-cache\",\n include: [\"src/app/api/**/route.ts\"],\n exclude: [],\n envFile: [\".env\", \".env.local\"],\n};\n\nexport function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig {\n let envFile: string[] | false;\n if (config.envFile === false) {\n envFile = false;\n } else if (typeof config.envFile === \"string\") {\n envFile = [config.envFile];\n } else {\n envFile = config.envFile ?? (defaults.envFile as string[]);\n }\n\n return {\n ...defaults,\n ...config,\n envFile,\n output: {\n scalarDocs: false,\n scalarPath: \"src/app/api/docs/route.ts\",\n ...config.output,\n },\n openapi: {\n description: \"\",\n servers: [],\n components: {},\n ...config.openapi,\n },\n };\n}\n\nexport async function loadConfig(configPath?: string): Promise<ResolvedConfig> {\n const searchPaths = configPath\n ? [configPath]\n : [\n \"openapi-gen.config.ts\",\n \"openapi-gen.config.js\",\n \"openapi-gen.config.mjs\",\n \"openapi-gen.config.cjs\",\n ];\n\n for (const p of searchPaths) {\n const abs = resolve(process.cwd(), p);\n if (existsSync(abs)) {\n const mod = await importConfig(abs);\n const config: OpenAPIGenConfig = mod.default ?? mod;\n return resolveConfig(config);\n }\n }\n\n throw new Error(\n \"No openapi-gen.config.ts found. Create one at your project root.\",\n );\n}\n\nasync function importConfig(\n filePath: string,\n): Promise<{ default?: OpenAPIGenConfig } & OpenAPIGenConfig> {\n // For .ts files, try to use tsx/ts-node if available, else fall back to require\n if (filePath.endsWith(\".ts\")) {\n return importTypeScriptConfig(filePath);\n }\n const url = pathToFileURL(filePath).href;\n return import(url);\n}\n\nasync function importTypeScriptConfig(\n filePath: string,\n): Promise<{ default?: OpenAPIGenConfig } & OpenAPIGenConfig> {\n // Use tsx (bundled as a dependency) to register CJS TypeScript hooks\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require(\"tsx/cjs\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n return require(filePath);\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'fs';\nimport { dirname, join, resolve } from 'path';\n\nimport type { AnalyzedRoute } from './analyzer.js';\nimport type { ResolvedConfig } from './config.js';\n\nexport interface OpenAPISpec {\n\topenapi: '3.1.0';\n\tinfo: {\n\t\ttitle: string;\n\t\tversion: string;\n\t\tdescription?: string;\n\t};\n\tservers?: Array<{ url: string; description?: string }>;\n\tsecurity?: Array<{ [key: string]: string[] }>;\n\tcomponents?: {\n\t\tsecuritySchemes?: Record<string, unknown>;\n\t\t[key: string]: unknown;\n\t};\n\tpaths: Record<string, unknown>;\n}\n\nexport function assembleSpec(config: ResolvedConfig, routes: AnalyzedRoute[]): OpenAPISpec {\n\tconst paths: Record<string, unknown> = {};\n\n\tfor (const route of routes) {\n\t\tif (Object.keys(route.pathItem).length > 0) {\n\t\t\tpaths[route.urlPath] = route.pathItem;\n\t\t}\n\t}\n\n\tconst spec: OpenAPISpec = {\n\t\topenapi: '3.1.0',\n\t\tinfo: {\n\t\t\ttitle: config.openapi.title,\n\t\t\tversion: config.openapi.version,\n\t\t\t...(config.openapi.description ? { description: config.openapi.description } : {}),\n\t\t},\n\t\tpaths,\n\t};\n\n\tif (config.openapi.servers && config.openapi.servers.length > 0) {\n\t\tspec.servers = config.openapi.servers;\n\t}\n\n\tif (config.openapi.security && config.openapi.security.length > 0) {\n\t\tspec.security = config.openapi.security;\n\t}\n\n\tif (config.openapi.components && Object.keys(config.openapi.components).length > 0) {\n\t\tspec.components = config.openapi.components;\n\t}\n\n\treturn spec;\n}\n\nexport function writeOutputFiles(\n\tconfig: ResolvedConfig,\n\tspec: OpenAPISpec,\n\tcwd: string = process.cwd(),\n): void {\n\twriteSpecFiles(config, spec, cwd);\n\n\tif (config.output.scalarDocs) {\n\t\twriteScalarRoute(config, cwd);\n\t}\n}\n\nfunction writeSpecFiles(config: ResolvedConfig, spec: OpenAPISpec, cwd: string): void {\n\tconst specRoutePath = resolve(cwd, config.output.specPath);\n\tconst specDir = dirname(specRoutePath);\n\n\tensureDir(specDir);\n\n\t// Write spec.json co-located with the route\n\tconst specJsonPath = join(specDir, 'spec.json');\n\twriteFileSync(specJsonPath, JSON.stringify(spec, null, 2), 'utf8');\n\n\t// Write the Next.js route that serves the spec\n\tconst routeContent = `import spec from './spec.json';\n\nexport const dynamic = 'force-static';\n\nexport function GET() {\n return Response.json(spec);\n}\n`;\n\twriteFileSync(specRoutePath, routeContent, 'utf8');\n}\n\nfunction writeScalarRoute(config: ResolvedConfig, cwd: string): void {\n\tconst scalarRoutePath = resolve(cwd, config.output.scalarPath);\n\tensureDir(dirname(scalarRoutePath));\n\n\t// Derive the spec URL from the specPath\n\tconst specUrl = filePathToApiUrl(config.output.specPath);\n\n\tconst routeContent = `export const dynamic = 'force-static';\n\nexport function GET() {\n return new Response(\n \\`<!doctype html>\n<html>\n <head>\n <title>API Docs</title>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body>\n <script\n id=\"api-reference\"\n data-url=\"${specUrl}\"\n ></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n </body>\n</html>\\`,\n { headers: { 'Content-Type': 'text/html' } }\n );\n}\n`;\n\twriteFileSync(scalarRoutePath, routeContent, 'utf8');\n}\n\n/**\n * Convert a file path like src/app/api/openapi.json/route.ts to /api/openapi.json\n */\nfunction filePathToApiUrl(filePath: string): string {\n\tlet path = filePath.replace(/\\\\/g, '/');\n\tpath = path.replace(/^(src\\/)?app\\//, '');\n\tpath = path.replace(/\\/route\\.(ts|tsx|js|jsx)$/, '');\n\tif (!path.startsWith('/')) path = `/${path}`;\n\treturn path;\n}\n\nfunction ensureDir(dir: string): void {\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n}\n","import { readFileSync } from \"fs\";\nimport { relative } from \"path\";\n\nexport interface RouteInfo {\n filePath: string;\n relativePath: string;\n urlPath: string;\n sourceCode: string;\n jsdocComments: string[];\n hasExactJsdoc: boolean;\n exactPathItem?: Record<string, unknown>;\n}\n\nexport async function scanRoutes(\n include: string[],\n exclude: string[],\n cwd: string = process.cwd(),\n): Promise<RouteInfo[]> {\n // Lazy import so that importing this module does not eagerly load fast-glob.\n // This keeps the module lightweight and allows Vitest to mock it cleanly.\n const { default: fg } = await import(\"fast-glob\");\n const files = await fg(include, {\n cwd,\n ignore: exclude,\n absolute: true,\n });\n\n return files.map((filePath) => parseRoute(filePath, cwd));\n}\n\nfunction parseRoute(filePath: string, cwd: string): RouteInfo {\n const relativePath = relative(cwd, filePath);\n const sourceCode = readFileSync(filePath, \"utf8\");\n const urlPath = filePathToUrlPath(relativePath);\n const { jsdocComments, hasExactJsdoc, exactPathItem } =\n extractJsdoc(sourceCode);\n\n return {\n filePath,\n relativePath,\n urlPath,\n sourceCode,\n jsdocComments,\n hasExactJsdoc,\n exactPathItem,\n };\n}\n\n/**\n * Convert a Next.js route file path to an OpenAPI URL path.\n * e.g. src/app/api/users/[id]/route.ts -> /api/users/{id}\n */\nexport function filePathToUrlPath(filePath: string): string {\n // Normalize separators\n let path = filePath.replace(/\\\\/g, \"/\");\n\n // Remove leading src/ or app/ prefixes\n path = path.replace(/^(src\\/)?app\\//, \"\");\n\n // Remove trailing /route.ts or /route.js\n path = path.replace(/\\/route\\.(ts|tsx|js|jsx)$/, \"\");\n\n // Convert Next.js dynamic segments [param] to OpenAPI {param}\n path = path.replace(/\\[([^\\]]+)\\]/g, (_, param) => {\n // Handle catch-all [...param] and optional [[...param]]\n if (param.startsWith(\"...\")) {\n return `{${param.slice(3)}}`;\n }\n return `{${param}}`;\n });\n\n // Ensure leading slash\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n\n return path;\n}\n\ninterface JsdocResult {\n jsdocComments: string[];\n hasExactJsdoc: boolean;\n exactPathItem?: Record<string, unknown>;\n}\n\nfunction extractJsdoc(sourceCode: string): JsdocResult {\n // Match all JSDoc comment blocks /** ... */\n const jsdocRegex = /\\/\\*\\*([\\s\\S]*?)\\*\\//g;\n const jsdocComments: string[] = [];\n let hasExactJsdoc = false;\n let exactPathItem: Record<string, unknown> | undefined;\n\n let match: RegExpExecArray | null;\n // biome-ignore lint/suspicious/noAssignInExpressions: required for while loop\n while ((match = jsdocRegex.exec(sourceCode)) !== null) {\n const comment = match[0];\n jsdocComments.push(comment);\n\n // Check for @openapi-exact tag\n if (/@openapi-exact/.test(comment)) {\n hasExactJsdoc = true;\n // Try to extract the JSON from @openapi tag\n const openapiMatch = comment.match(\n /@openapi\\s+([\\s\\S]*?)(?=\\s*\\*\\/|\\s*\\*\\s*@)/,\n );\n if (openapiMatch) {\n try {\n // Clean up JSDoc asterisks from the JSON\n const jsonStr = openapiMatch[1]\n .split(\"\\n\")\n .map((line) => line.replace(/^\\s*\\*\\s?/, \"\"))\n .join(\"\\n\")\n .trim();\n exactPathItem = JSON.parse(jsonStr);\n } catch {\n // If JSON parse fails, fall through to LLM\n hasExactJsdoc = false;\n }\n }\n }\n }\n\n return { jsdocComments, hasExactJsdoc, exactPathItem };\n}\n","export type { JSDocMode, OpenAPIGenConfig, Provider, ResolvedConfig } from './config.js';\n\nexport { analyzeRoutes } from './analyzer.js';\nexport { loadConfig, resolveConfig } from './config.js';\nexport { assembleSpec, writeOutputFiles } from './generator.js';\nexport { filePathToUrlPath, scanRoutes } from './scanner.js';\n\nimport { resolve } from 'node:path';\nimport type { OpenAPIGenConfig } from './config.js';\n\nimport { analyzeRoutes } from './analyzer.js';\nimport { loadConfig } from './config.js';\nimport { assembleSpec, writeOutputFiles } from './generator.js';\nimport { scanRoutes } from './scanner.js';\n\nexport interface GenerateOptions {\n\tconfig?: string;\n\tprovider?: OpenAPIGenConfig['provider'];\n\tcache?: boolean;\n\tcwd?: string;\n}\n\nexport interface GenerateResult {\n\troutesAnalyzed: number;\n\troutesFromCache: number;\n\troutesSkippedLLM: number;\n\tspecPath: string;\n}\n\nexport async function generate(options: GenerateOptions = {}): Promise<GenerateResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst config = await loadConfig(options.config);\n\n\t// Allow CLI overrides\n\tif (options.provider) config.provider = options.provider;\n\tif (options.cache === false) config.cache = false;\n\n\t// Load .env files before provider env vars are read\n\tif (config.envFile !== false) {\n\t\tconst { config: dotenvConfig } = await import('dotenv');\n\t\tfor (const file of config.envFile) {\n\t\t\tdotenvConfig({ path: resolve(cwd, file), override: false });\n\t\t}\n\t}\n\n\tconsole.log(`[openapi-ai-generator] Scanning routes...`);\n\tconst routes = await scanRoutes(config.include, config.exclude, cwd);\n\tconsole.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);\n\n\tconsole.log(`[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`);\n\tconst analyzed = await analyzeRoutes(routes, {\n\t\tprovider: config.provider,\n\t\tjsdocMode: config.jsdocMode,\n\t\tcache: config.cache,\n\t\tcacheDir: config.cacheDir,\n\t});\n\n\tconst fromCache = analyzed.filter((r) => r.fromCache).length;\n\tconst skippedLLM = analyzed.filter((r) => r.skippedLLM).length;\n\tconsole.log(\n\t\t`[openapi-ai-generator] ${analyzed.length} routes analyzed (${fromCache} from cache, ${skippedLLM - fromCache} exact JSDoc)`,\n\t);\n\n\tconst spec = assembleSpec(config, analyzed);\n\twriteOutputFiles(config, spec, cwd);\n\n\tconsole.log(`[openapi-ai-generator] Spec written to ${config.output.specPath}`);\n\tif (config.output.scalarDocs) {\n\t\tconsole.log(`[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`);\n\t}\n\n\treturn {\n\t\troutesAnalyzed: analyzed.length,\n\t\troutesFromCache: fromCache,\n\t\troutesSkippedLLM: skippedLLM,\n\t\tspecPath: config.output.specPath,\n\t};\n}\n"],"mappings":"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","names":["import_fs","import_path","import_fs","import_path","import_fs","import_path"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/analyzer.ts","../src/cache.ts","../src/providers/index.ts","../src/config.ts","../src/generator.ts","../src/scanner.ts","../src/index.ts"],"sourcesContent":["import { Command } from 'commander';\n\nimport type { Provider } from './config.js';\n\nimport { generate } from './index.js';\n\nconst program = new Command();\n\nprogram\n\t.name('openapi-ai-generator')\n\t.description('Generate OpenAPI 3.1 specs from Next.js API routes using AI')\n\t.version('0.1.0');\n\nprogram\n\t.command('generate')\n\t.description('Scan Next.js API routes and generate an OpenAPI spec')\n\t.option('-c, --config <path>', 'Path to config file (default: openapi-gen.config.ts)')\n\t.option('-p, --provider <provider>', 'Override provider (azure | openai | anthropic)')\n\t.option('--no-cache', 'Disable caching (always re-analyze all routes)')\n\t.action(async (opts: { config?: string; provider?: string; cache: boolean }) => {\n\t\ttry {\n\t\t\tconst result = await generate({\n\t\t\t\tconfig: opts.config,\n\t\t\t\tprovider: opts.provider as Provider | undefined,\n\t\t\t\tcache: opts.cache,\n\t\t\t});\n\n\t\t\tconsole.log(`\\n✓ OpenAPI spec generated successfully`);\n\t\t\tconsole.log(` Routes analyzed: ${result.routesAnalyzed}`);\n\t\t\tconsole.log(` From cache: ${result.routesFromCache}`);\n\t\t\tconsole.log(\n\t\t\t\t` LLM calls made: ${result.routesAnalyzed - result.routesFromCache - (result.routesSkippedLLM - result.routesFromCache)}`,\n\t\t\t);\n\t\t\tconsole.log(` Spec written to: ${result.specPath}`);\n\t\t} catch (err) {\n\t\t\tconsole.error('[openapi-ai-generator] Error:', err instanceof Error ? err.message : err);\n\t\t\tprocess.exit(1);\n\t\t}\n\t});\n\nprogram.parse();\n","import type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\n\nimport type { JSDocMode, Provider, ResolvedLimits } from \"./config.js\";\nimport type { RouteInfo } from \"./scanner.js\";\n\nimport { computeHash, RouteCache } from \"./cache.js\";\nimport { createModel, getModelId } from \"./providers/index.js\";\n\nexport interface AnalyzeOptions {\n provider: Provider;\n jsdocMode: JSDocMode;\n cache: boolean;\n cacheDir: string;\n limits: ResolvedLimits;\n}\n\nexport interface AnalyzedRoute {\n urlPath: string;\n pathItem: Record<string, unknown>;\n fromCache: boolean;\n skippedLLM: boolean;\n}\n\nexport async function analyzeRoutes(\n routes: RouteInfo[],\n options: AnalyzeOptions,\n): Promise<AnalyzedRoute[]> {\n const modelId = getModelId(options.provider);\n const cache = options.cache ? new RouteCache(options.cacheDir) : null;\n\n // Lazy-init the model only when we actually need it\n let model: LanguageModel | null = null;\n const getModel = (): LanguageModel => {\n if (!model) model = createModel(options.provider);\n return model;\n };\n\n const results: AnalyzedRoute[] = [];\n\n for (const route of routes) {\n const result = await analyzeRoute(route, options, modelId, cache, getModel);\n results.push(result);\n }\n\n return results;\n}\n\nasync function analyzeRoute(\n route: RouteInfo,\n options: AnalyzeOptions,\n modelId: string,\n cache: RouteCache | null,\n getModel: () => LanguageModel,\n): Promise<AnalyzedRoute> {\n // If @openapi-exact is present or jsdocMode is 'exact', skip LLM\n if (route.hasExactJsdoc && route.exactPathItem) {\n return {\n urlPath: route.urlPath,\n pathItem: route.exactPathItem,\n fromCache: false,\n skippedLLM: true,\n };\n }\n\n if (options.jsdocMode === \"exact\") {\n // In exact mode, if there's a @openapi tag, use it; otherwise still use LLM\n if (route.hasExactJsdoc && route.exactPathItem) {\n return {\n urlPath: route.urlPath,\n pathItem: route.exactPathItem,\n fromCache: false,\n skippedLLM: true,\n };\n }\n }\n\n // Compute cache hash\n const hash = computeHash(route.sourceCode, options.provider, modelId);\n\n // Check cache\n if (cache) {\n const cached = cache.get(hash);\n if (cached) {\n return {\n urlPath: route.urlPath,\n pathItem: cached,\n fromCache: true,\n skippedLLM: true,\n };\n }\n }\n\n // Call LLM\n let pathItem: Record<string, unknown>;\n try {\n pathItem = await callLLM(\n route,\n options.jsdocMode,\n getModel(),\n options.limits,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const isContentFilter =\n message.includes(\"content filtering policy\") ||\n message.includes(\"content_filter\") ||\n (err as { status?: number })?.status === 400;\n\n if (isContentFilter) {\n console.warn(\n `Warning: Content filter blocked response for ${route.urlPath}. ` +\n `Skipping route. Use @openapi-exact JSDoc to provide the spec manually.`,\n );\n return {\n urlPath: route.urlPath,\n pathItem: {},\n fromCache: false,\n skippedLLM: false,\n };\n }\n throw err;\n }\n\n // Store in cache\n if (cache) {\n cache.set(hash, pathItem);\n }\n\n return {\n urlPath: route.urlPath,\n pathItem,\n fromCache: false,\n skippedLLM: false,\n };\n}\n\nfunction buildPrompt(route: RouteInfo, jsdocMode: JSDocMode): string {\n const jsDocSection =\n route.jsdocComments.length > 0\n ? `JSDoc COMMENTS (use as ${jsdocMode === \"context\" ? \"additional context\" : \"primary source\"}):\\n${route.jsdocComments.join(\"\\n\\n\")}`\n : \"No JSDoc comments found.\";\n\n return `You are a technical documentation tool that reads existing source code and produces OpenAPI 3.1 documentation data. You do not write or execute code — you only read and describe it.\n\n## Task\n\nRead the Next.js API route source file below and produce a JSON object that documents its HTTP endpoints according to the OpenAPI 3.1 PathItem schema.\n\n## Route metadata\n\n- File: ${route.relativePath}\n- URL path: ${route.urlPath}\n\n## Source file contents\n\n\\`\\`\\`typescript\n${route.sourceCode}\n\\`\\`\\`\n\n${jsDocSection}\n\n## Instructions\n\nFor each exported function named GET, POST, PUT, PATCH, or DELETE, document:\n- operationId: a unique camelCase identifier\n- summary: a short one-line description (max ~4 words, avoid starting with \"Get\", \"Post\", \"Put\", \"Patch\", \"Delete\", these are sometimes inferred from the operationId, and are sometimes in the format of \"Create a ___\", \"Update a ___\", \"Delete a ___\", \"Get a ___\", \"List ___\")\n- description: a fuller explanation of what the endpoint does\n- parameters: path params from URL segments like {id}, and query params from searchParams usage\n- requestBody: schema inferred from request.json() calls and TypeScript types (POST/PUT/PATCH only)\n- responses: per status code, inferred from NextResponse.json() calls and return type annotations\n- tags: inferred from the URL path segments, but typically only one tag should be used. If more than one exists, they show up in multiple categories, and that can be confusing. If there's a question, use the more specific option.\n- security: noted if the code checks for auth tokens, session cookies, or middleware guards\n\n## Output format\n\nReturn a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text — only the JSON object.`;\n}\n\nasync function callLLM(\n route: RouteInfo,\n jsdocMode: JSDocMode,\n model: LanguageModel,\n limits: ResolvedLimits,\n): Promise<Record<string, unknown>> {\n const prompt = buildPrompt(route, jsdocMode);\n\n const { text } = await generateText({\n model,\n prompt,\n temperature: 0,\n maxOutputTokens: limits.maxTokens,\n abortSignal: AbortSignal.timeout(limits.timeoutMs),\n });\n\n return parsePathItem(text, route.urlPath);\n}\n\nfunction parsePathItem(text: string, urlPath: string): Record<string, unknown> {\n // Strip any accidental markdown code fences\n let json = text.trim();\n if (json.startsWith(\"```\")) {\n json = json\n .replace(/^```(?:json)?\\s*/i, \"\")\n .replace(/\\s*```$/, \"\")\n .trim();\n }\n\n try {\n const parsed = JSON.parse(json);\n if (\n typeof parsed !== \"object\" ||\n Array.isArray(parsed) ||\n parsed === null\n ) {\n throw new Error(\"Response is not a JSON object\");\n }\n return parsed;\n } catch (err) {\n console.warn(\n `Warning: Failed to parse LLM response for ${urlPath}. Using empty PathItem.`,\n err,\n );\n return {};\n }\n}\n","import { createHash } from 'crypto';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\n\nexport interface CacheEntry {\n\thash: string;\n\tpathItem: Record<string, unknown>;\n\tcachedAt: string;\n}\n\nexport function computeHash(content: string, provider: string, modelId: string): string {\n\treturn createHash('sha256').update(content).update(provider).update(modelId).digest('hex');\n}\n\nexport class RouteCache {\n\tprivate readonly cacheDir: string;\n\n\tconstructor(cacheDir: string) {\n\t\tthis.cacheDir = cacheDir;\n\t}\n\n\tprivate ensureDir(): void {\n\t\tif (!existsSync(this.cacheDir)) {\n\t\t\tmkdirSync(this.cacheDir, { recursive: true });\n\t\t}\n\t}\n\n\tget(hash: string): Record<string, unknown> | null {\n\t\tconst filePath = join(this.cacheDir, `${hash}.json`);\n\t\tif (!existsSync(filePath)) return null;\n\t\ttry {\n\t\t\tconst entry: CacheEntry = JSON.parse(readFileSync(filePath, 'utf8'));\n\t\t\treturn entry.pathItem;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tset(hash: string, pathItem: Record<string, unknown>): void {\n\t\tthis.ensureDir();\n\t\tconst entry: CacheEntry = {\n\t\t\thash,\n\t\t\tpathItem,\n\t\t\tcachedAt: new Date().toISOString(),\n\t\t};\n\t\tconst filePath = join(this.cacheDir, `${hash}.json`);\n\t\twriteFileSync(filePath, JSON.stringify(entry, null, 2), 'utf8');\n\t}\n}\n","import type { LanguageModel } from \"ai\";\n\nimport type { Provider } from \"../config.js\";\n\nexport function createModel(provider: Provider): LanguageModel {\n switch (provider) {\n case \"azure\":\n return createAzureModel();\n case \"openai\":\n return createOpenAIModel();\n case \"anthropic\":\n return createAnthropicModel();\n default: {\n const _exhaustive: never = provider;\n throw new Error(`Unknown provider: ${_exhaustive}`);\n }\n }\n}\n\nfunction createAzureModel(): LanguageModel {\n const endpoint = requireEnv(\"AZURE_OPENAI_ENDPOINT\");\n const resourceName = endpoint?.match(/https?:\\/\\/([^.]+)/)?.[1];\n const apiKey = requireEnv(\"AZURE_OPENAI_API_KEY\");\n const deployment = requireEnv(\"AZURE_OPENAI_DEPLOYMENT\");\n\n // Dynamic import to avoid loading unused provider SDKs\n const { createAzure } = require(\"@ai-sdk/azure\");\n const azure = createAzure({ resourceName, apiKey });\n return azure(deployment);\n}\n\nfunction createOpenAIModel(): LanguageModel {\n const apiKey = requireEnv(\"OPENAI_API_KEY\");\n const model = process.env.OPENAI_MODEL ?? \"gpt-4o\";\n\n const { createOpenAI } = require(\"@ai-sdk/openai\");\n const openai = createOpenAI({ apiKey });\n return openai(model);\n}\n\nfunction createAnthropicModel(): LanguageModel {\n const apiKey = requireEnv(\"ANTHROPIC_API_KEY\");\n const model = process.env.ANTHROPIC_MODEL ?? \"claude-sonnet-4-6\";\n\n const { createAnthropic } = require(\"@ai-sdk/anthropic\");\n const anthropic = createAnthropic({ apiKey });\n return anthropic(model);\n}\n\nfunction requireEnv(name: string): string {\n const val = process.env[name];\n if (!val) {\n throw new Error(`Required environment variable ${name} is not set.`);\n }\n return val;\n}\n\nexport function getModelId(provider: Provider): string {\n switch (provider) {\n case \"azure\":\n return process.env.AZURE_OPENAI_DEPLOYMENT ?? \"unknown\";\n case \"openai\":\n return process.env.OPENAI_MODEL ?? \"gpt-4o\";\n case \"anthropic\":\n return process.env.ANTHROPIC_MODEL ?? \"claude-sonnet-4-6\";\n }\n}\n","import { existsSync } from \"fs\";\nimport { resolve } from \"path\";\nimport { pathToFileURL } from \"url\";\n\nexport type Provider = \"azure\" | \"openai\" | \"anthropic\";\nexport type JSDocMode = \"context\" | \"exact\";\n\nexport interface OpenAPIGenConfig {\n provider: Provider;\n output: {\n specPath: string;\n scalarDocs?: boolean;\n scalarPath?: string;\n scalarConfig?: Record<string, unknown>;\n };\n openapi: {\n title: string;\n version: string;\n description?: string;\n servers?: Array<{ url: string; description?: string }>;\n security: Array<{ [key: string]: string[] }>;\n components?: {\n securitySchemes?: {\n [key: string]: {\n type: string;\n in: string;\n name: string;\n };\n };\n };\n };\n jsdocMode?: JSDocMode;\n cache?: boolean;\n cacheDir?: string;\n include?: string[];\n exclude?: string[];\n /**\n * Path(s) to .env files to load before running. Defaults to ['.env', '.env.local'].\n * Set to false to disable automatic .env loading.\n */\n envFile?: string | string[] | false;\n /**\n * Safeguards for LLM usage per route analysis call.\n */\n limits?: {\n /**\n * Maximum number of output tokens per LLM call. Defaults to 4000.\n */\n maxTokens?: number;\n /**\n * Timeout in milliseconds per LLM call. Defaults to 60000 (60s).\n */\n timeoutMs?: number;\n };\n}\n\nexport interface ResolvedLimits {\n maxTokens: number;\n timeoutMs: number;\n}\n\nexport interface ResolvedConfig extends Omit<\n Required<OpenAPIGenConfig>,\n \"envFile\" | \"limits\"\n> {\n output: Required<OpenAPIGenConfig[\"output\"]>;\n openapi: Required<OpenAPIGenConfig[\"openapi\"]>;\n envFile: string[] | false;\n limits: ResolvedLimits;\n}\n\nconst defaults: Omit<ResolvedConfig, \"provider\" | \"output\" | \"openapi\"> = {\n jsdocMode: \"context\",\n cache: true,\n cacheDir: \".openapi-cache\",\n include: [\"src/app/api/**/route.ts\"],\n exclude: [],\n envFile: [\".env\", \".env.local\"],\n limits: {\n maxTokens: 4000,\n timeoutMs: 60_000,\n },\n};\n\nexport function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig {\n let envFile: string[] | false;\n if (config.envFile === false) {\n envFile = false;\n } else if (typeof config.envFile === \"string\") {\n envFile = [config.envFile];\n } else {\n envFile = config.envFile ?? (defaults.envFile as string[]);\n }\n\n return {\n ...defaults,\n ...config,\n envFile,\n limits: {\n ...defaults.limits,\n ...config.limits,\n },\n output: {\n scalarDocs: false,\n scalarPath: \"src/app/api/docs/route.ts\",\n scalarConfig: {},\n ...config.output,\n },\n openapi: {\n description: \"\",\n servers: [],\n components: {},\n ...config.openapi,\n },\n };\n}\n\nexport async function loadConfig(configPath?: string): Promise<ResolvedConfig> {\n const searchPaths = configPath\n ? [configPath]\n : [\n \"openapi-gen.config.ts\",\n \"openapi-gen.config.js\",\n \"openapi-gen.config.mjs\",\n \"openapi-gen.config.cjs\",\n ];\n\n for (const p of searchPaths) {\n const abs = resolve(process.cwd(), p);\n if (existsSync(abs)) {\n const mod = await importConfig(abs);\n const config: OpenAPIGenConfig = mod.default ?? mod;\n return resolveConfig(config);\n }\n }\n\n throw new Error(\n \"No openapi-gen.config.ts found. Create one at your project root.\",\n );\n}\n\nasync function importConfig(\n filePath: string,\n): Promise<{ default?: OpenAPIGenConfig } & OpenAPIGenConfig> {\n // For .ts files, try to use tsx/ts-node if available, else fall back to require\n if (filePath.endsWith(\".ts\")) {\n return importTypeScriptConfig(filePath);\n }\n const url = pathToFileURL(filePath).href;\n return import(url);\n}\n\nasync function importTypeScriptConfig(\n filePath: string,\n): Promise<{ default?: OpenAPIGenConfig } & OpenAPIGenConfig> {\n // Use tsx (bundled as a dependency) to register CJS TypeScript hooks\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require(\"tsx/cjs\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n return require(filePath);\n}\n","import { existsSync, mkdirSync, writeFileSync } from \"fs\";\nimport { dirname, join, resolve } from \"path\";\n\nimport type { AnalyzedRoute } from \"./analyzer.js\";\nimport type { ResolvedConfig } from \"./config.js\";\n\nexport interface OpenAPISpec {\n openapi: \"3.1.0\";\n info: {\n title: string;\n version: string;\n description?: string;\n };\n servers?: Array<{ url: string; description?: string }>;\n security?: Array<{ [key: string]: string[] }>;\n components?: {\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n };\n paths: Record<string, unknown>;\n}\n\nexport function assembleSpec(\n config: ResolvedConfig,\n routes: AnalyzedRoute[],\n): OpenAPISpec {\n const paths: Record<string, unknown> = {};\n\n for (const route of routes) {\n if (Object.keys(route.pathItem).length > 0) {\n paths[route.urlPath] = route.pathItem;\n }\n }\n\n const spec: OpenAPISpec = {\n openapi: \"3.1.0\",\n info: {\n title: config.openapi.title,\n version: config.openapi.version,\n ...(config.openapi.description\n ? { description: config.openapi.description }\n : {}),\n },\n paths,\n };\n\n if (config.openapi.servers && config.openapi.servers.length > 0) {\n spec.servers = config.openapi.servers;\n }\n\n if (config.openapi.security && config.openapi.security.length > 0) {\n spec.security = config.openapi.security;\n }\n\n if (\n config.openapi.components &&\n Object.keys(config.openapi.components).length > 0\n ) {\n spec.components = config.openapi.components;\n }\n\n return spec;\n}\n\nexport function writeOutputFiles(\n config: ResolvedConfig,\n spec: OpenAPISpec,\n cwd: string = process.cwd(),\n): void {\n writeSpecFiles(config, spec, cwd);\n\n if (config.output.scalarDocs) {\n writeScalarRoute(config, cwd);\n }\n}\n\nfunction writeSpecFiles(\n config: ResolvedConfig,\n spec: OpenAPISpec,\n cwd: string,\n): void {\n const specRoutePath = resolve(cwd, config.output.specPath);\n const specDir = dirname(specRoutePath);\n\n ensureDir(specDir);\n\n // Write spec.json co-located with the route\n const specJsonPath = join(specDir, \"spec.json\");\n writeFileSync(specJsonPath, JSON.stringify(spec, null, 2), \"utf8\");\n\n // Write the Next.js route that serves the spec\n const routeContent = `import spec from './spec.json';\n\nexport const dynamic = 'force-static';\n\nexport function GET() {\n return Response.json(spec);\n}\n`;\n writeFileSync(specRoutePath, routeContent, \"utf8\");\n}\n\nfunction writeScalarRoute(config: ResolvedConfig, cwd: string): void {\n const scalarRoutePath = resolve(cwd, config.output.scalarPath);\n ensureDir(dirname(scalarRoutePath));\n\n // Derive the spec URL from the specPath\n const specUrl = filePathToApiUrl(config.output.specPath);\n\n const routeContent = `export const dynamic = 'force-static';\n\nexport function GET() {\n return new Response(\n \\`<!doctype html>\n<html>\n <head>\n <title>API Docs</title>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n </head>\n <body>\n <script\n id=\"api-reference\"\n data-url=\"${specUrl}\"\n ${config.output.scalarConfig && Object.keys(config.output.scalarConfig).length > 0 ? `data-config=\"${JSON.stringify(config.output.scalarConfig)}\"` : \"\"}\n ></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n </body>\n</html>\\`,\n { headers: { 'Content-Type': 'text/html' } }\n );\n}\n`;\n writeFileSync(scalarRoutePath, routeContent, \"utf8\");\n}\n\n/**\n * Convert a file path like src/app/api/openapi.json/route.ts to /api/openapi.json\n */\nfunction filePathToApiUrl(filePath: string): string {\n let path = filePath.replace(/\\\\/g, \"/\");\n path = path.replace(/^(src\\/)?app\\//, \"\");\n path = path.replace(/\\/route\\.(ts|tsx|js|jsx)$/, \"\");\n if (!path.startsWith(\"/\")) path = `/${path}`;\n return path;\n}\n\nfunction ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n","import { readFileSync } from \"fs\";\nimport { relative } from \"path\";\n\nexport interface RouteInfo {\n filePath: string;\n relativePath: string;\n urlPath: string;\n sourceCode: string;\n jsdocComments: string[];\n hasExactJsdoc: boolean;\n exactPathItem?: Record<string, unknown>;\n}\n\nexport async function scanRoutes(\n include: string[],\n exclude: string[],\n cwd: string = process.cwd(),\n): Promise<RouteInfo[]> {\n // Lazy import so that importing this module does not eagerly load fast-glob.\n // This keeps the module lightweight and allows Vitest to mock it cleanly.\n const { default: fg } = await import(\"fast-glob\");\n const files = await fg(include, {\n cwd,\n ignore: exclude,\n absolute: true,\n });\n\n return files.map((filePath) => parseRoute(filePath, cwd));\n}\n\nfunction parseRoute(filePath: string, cwd: string): RouteInfo {\n const relativePath = relative(cwd, filePath);\n const sourceCode = readFileSync(filePath, \"utf8\");\n const urlPath = filePathToUrlPath(relativePath);\n const { jsdocComments, hasExactJsdoc, exactPathItem } =\n extractJsdoc(sourceCode);\n\n return {\n filePath,\n relativePath,\n urlPath,\n sourceCode,\n jsdocComments,\n hasExactJsdoc,\n exactPathItem,\n };\n}\n\n/**\n * Convert a Next.js route file path to an OpenAPI URL path.\n * e.g. src/app/api/users/[id]/route.ts -> /api/users/{id}\n */\nexport function filePathToUrlPath(filePath: string): string {\n // Normalize separators\n let path = filePath.replace(/\\\\/g, \"/\");\n\n // Remove leading src/ or app/ prefixes\n path = path.replace(/^(src\\/)?app\\//, \"\");\n\n // Remove trailing /route.ts or /route.js\n path = path.replace(/\\/route\\.(ts|tsx|js|jsx)$/, \"\");\n\n // Convert Next.js dynamic segments [param] to OpenAPI {param}\n path = path.replace(/\\[([^\\]]+)\\]/g, (_, param) => {\n // Handle catch-all [...param] and optional [[...param]]\n if (param.startsWith(\"...\")) {\n return `{${param.slice(3)}}`;\n }\n return `{${param}}`;\n });\n\n // Ensure leading slash\n if (!path.startsWith(\"/\")) {\n path = `/${path}`;\n }\n\n return path;\n}\n\ninterface JsdocResult {\n jsdocComments: string[];\n hasExactJsdoc: boolean;\n exactPathItem?: Record<string, unknown>;\n}\n\nfunction extractJsdoc(sourceCode: string): JsdocResult {\n // Match all JSDoc comment blocks /** ... */\n const jsdocRegex = /\\/\\*\\*([\\s\\S]*?)\\*\\//g;\n const jsdocComments: string[] = [];\n let hasExactJsdoc = false;\n let exactPathItem: Record<string, unknown> | undefined;\n\n let match: RegExpExecArray | null;\n // biome-ignore lint/suspicious/noAssignInExpressions: required for while loop\n while ((match = jsdocRegex.exec(sourceCode)) !== null) {\n const comment = match[0];\n jsdocComments.push(comment);\n\n // Check for @openapi-exact tag\n if (/@openapi-exact/.test(comment)) {\n hasExactJsdoc = true;\n // Try to extract the JSON from @openapi tag\n const openapiMatch = comment.match(\n /@openapi\\s+([\\s\\S]*?)(?=\\s*\\*\\/|\\s*\\*\\s*@)/,\n );\n if (openapiMatch) {\n try {\n // Clean up JSDoc asterisks from the JSON\n const jsonStr = openapiMatch[1]\n .split(\"\\n\")\n .map((line) => line.replace(/^\\s*\\*\\s?/, \"\"))\n .join(\"\\n\")\n .trim();\n exactPathItem = JSON.parse(jsonStr);\n } catch {\n // If JSON parse fails, fall through to LLM\n hasExactJsdoc = false;\n }\n }\n }\n }\n\n return { jsdocComments, hasExactJsdoc, exactPathItem };\n}\n","export type {\n JSDocMode,\n OpenAPIGenConfig,\n Provider,\n ResolvedConfig,\n ResolvedLimits,\n} from \"./config.js\";\n\nexport { analyzeRoutes } from \"./analyzer.js\";\nexport { loadConfig, resolveConfig } from \"./config.js\";\nexport { assembleSpec, writeOutputFiles } from \"./generator.js\";\nexport { filePathToUrlPath, scanRoutes } from \"./scanner.js\";\n\nimport { resolve } from \"node:path\";\n\nimport type { OpenAPIGenConfig } from \"./config.js\";\n\nimport { analyzeRoutes } from \"./analyzer.js\";\nimport { loadConfig } from \"./config.js\";\nimport { assembleSpec, writeOutputFiles } from \"./generator.js\";\nimport { scanRoutes } from \"./scanner.js\";\n\nexport interface GenerateOptions {\n config?: string;\n provider?: OpenAPIGenConfig[\"provider\"];\n cache?: boolean;\n cwd?: string;\n}\n\nexport interface GenerateResult {\n routesAnalyzed: number;\n routesFromCache: number;\n routesSkippedLLM: number;\n specPath: string;\n}\n\nexport async function generate(\n options: GenerateOptions = {},\n): Promise<GenerateResult> {\n const cwd = options.cwd ?? process.cwd();\n const config = await loadConfig(options.config);\n\n // Allow CLI overrides\n if (options.provider) config.provider = options.provider;\n if (options.cache === false) config.cache = false;\n\n // Load .env files before provider env vars are read\n if (config.envFile !== false) {\n const { config: dotenvConfig } = await import(\"dotenv\");\n for (const file of config.envFile) {\n dotenvConfig({ path: resolve(cwd, file), override: false });\n }\n }\n\n console.log(`[openapi-ai-generator] Scanning routes...`);\n const routes = await scanRoutes(config.include, config.exclude, cwd);\n console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);\n\n console.log(\n `[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`,\n );\n const analyzed = await analyzeRoutes(routes, {\n provider: config.provider,\n jsdocMode: config.jsdocMode,\n cache: config.cache,\n cacheDir: config.cacheDir,\n limits: config.limits,\n });\n\n const fromCache = analyzed.filter((r) => r.fromCache).length;\n const skippedLLM = analyzed.filter((r) => r.skippedLLM).length;\n console.log(\n `[openapi-ai-generator] ${analyzed.length} routes analyzed (${fromCache} from cache, ${skippedLLM - fromCache} exact JSDoc)`,\n );\n\n const spec = assembleSpec(config, analyzed);\n writeOutputFiles(config, spec, cwd);\n\n console.log(\n `[openapi-ai-generator] Spec written to ${config.output.specPath}`,\n );\n if (config.output.scalarDocs) {\n console.log(\n `[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`,\n );\n }\n\n return {\n routesAnalyzed: analyzed.length,\n routesFromCache: fromCache,\n routesSkippedLLM: skippedLLM,\n specPath: config.output.specPath,\n };\n}\n"],"mappings":"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","names":["import_fs","import_path","import_fs","import_path","import_fs","import_path"]}
package/dist/index.d.mts CHANGED
@@ -6,6 +6,7 @@ interface OpenAPIGenConfig {
6
6
  specPath: string;
7
7
  scalarDocs?: boolean;
8
8
  scalarPath?: string;
9
+ scalarConfig?: Record<string, unknown>;
9
10
  };
10
11
  openapi: {
11
12
  title: string;
@@ -38,11 +39,29 @@ interface OpenAPIGenConfig {
38
39
  * Set to false to disable automatic .env loading.
39
40
  */
40
41
  envFile?: string | string[] | false;
42
+ /**
43
+ * Safeguards for LLM usage per route analysis call.
44
+ */
45
+ limits?: {
46
+ /**
47
+ * Maximum number of output tokens per LLM call. Defaults to 4000.
48
+ */
49
+ maxTokens?: number;
50
+ /**
51
+ * Timeout in milliseconds per LLM call. Defaults to 60000 (60s).
52
+ */
53
+ timeoutMs?: number;
54
+ };
55
+ }
56
+ interface ResolvedLimits {
57
+ maxTokens: number;
58
+ timeoutMs: number;
41
59
  }
42
- interface ResolvedConfig extends Omit<Required<OpenAPIGenConfig>, "envFile"> {
60
+ interface ResolvedConfig extends Omit<Required<OpenAPIGenConfig>, "envFile" | "limits"> {
43
61
  output: Required<OpenAPIGenConfig["output"]>;
44
62
  openapi: Required<OpenAPIGenConfig["openapi"]>;
45
63
  envFile: string[] | false;
64
+ limits: ResolvedLimits;
46
65
  }
47
66
  declare function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig;
48
67
  declare function loadConfig(configPath?: string): Promise<ResolvedConfig>;
@@ -68,6 +87,7 @@ interface AnalyzeOptions {
68
87
  jsdocMode: JSDocMode;
69
88
  cache: boolean;
70
89
  cacheDir: string;
90
+ limits: ResolvedLimits;
71
91
  }
72
92
  interface AnalyzedRoute {
73
93
  urlPath: string;
@@ -78,7 +98,7 @@ interface AnalyzedRoute {
78
98
  declare function analyzeRoutes(routes: RouteInfo[], options: AnalyzeOptions): Promise<AnalyzedRoute[]>;
79
99
 
80
100
  interface OpenAPISpec {
81
- openapi: '3.1.0';
101
+ openapi: "3.1.0";
82
102
  info: {
83
103
  title: string;
84
104
  version: string;
@@ -102,7 +122,7 @@ declare function writeOutputFiles(config: ResolvedConfig, spec: OpenAPISpec, cwd
102
122
 
103
123
  interface GenerateOptions {
104
124
  config?: string;
105
- provider?: OpenAPIGenConfig['provider'];
125
+ provider?: OpenAPIGenConfig["provider"];
106
126
  cache?: boolean;
107
127
  cwd?: string;
108
128
  }
@@ -114,4 +134,4 @@ interface GenerateResult {
114
134
  }
115
135
  declare function generate(options?: GenerateOptions): Promise<GenerateResult>;
116
136
 
117
- export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
137
+ export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, type ResolvedLimits, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ interface OpenAPIGenConfig {
6
6
  specPath: string;
7
7
  scalarDocs?: boolean;
8
8
  scalarPath?: string;
9
+ scalarConfig?: Record<string, unknown>;
9
10
  };
10
11
  openapi: {
11
12
  title: string;
@@ -38,11 +39,29 @@ interface OpenAPIGenConfig {
38
39
  * Set to false to disable automatic .env loading.
39
40
  */
40
41
  envFile?: string | string[] | false;
42
+ /**
43
+ * Safeguards for LLM usage per route analysis call.
44
+ */
45
+ limits?: {
46
+ /**
47
+ * Maximum number of output tokens per LLM call. Defaults to 4000.
48
+ */
49
+ maxTokens?: number;
50
+ /**
51
+ * Timeout in milliseconds per LLM call. Defaults to 60000 (60s).
52
+ */
53
+ timeoutMs?: number;
54
+ };
55
+ }
56
+ interface ResolvedLimits {
57
+ maxTokens: number;
58
+ timeoutMs: number;
41
59
  }
42
- interface ResolvedConfig extends Omit<Required<OpenAPIGenConfig>, "envFile"> {
60
+ interface ResolvedConfig extends Omit<Required<OpenAPIGenConfig>, "envFile" | "limits"> {
43
61
  output: Required<OpenAPIGenConfig["output"]>;
44
62
  openapi: Required<OpenAPIGenConfig["openapi"]>;
45
63
  envFile: string[] | false;
64
+ limits: ResolvedLimits;
46
65
  }
47
66
  declare function resolveConfig(config: OpenAPIGenConfig): ResolvedConfig;
48
67
  declare function loadConfig(configPath?: string): Promise<ResolvedConfig>;
@@ -68,6 +87,7 @@ interface AnalyzeOptions {
68
87
  jsdocMode: JSDocMode;
69
88
  cache: boolean;
70
89
  cacheDir: string;
90
+ limits: ResolvedLimits;
71
91
  }
72
92
  interface AnalyzedRoute {
73
93
  urlPath: string;
@@ -78,7 +98,7 @@ interface AnalyzedRoute {
78
98
  declare function analyzeRoutes(routes: RouteInfo[], options: AnalyzeOptions): Promise<AnalyzedRoute[]>;
79
99
 
80
100
  interface OpenAPISpec {
81
- openapi: '3.1.0';
101
+ openapi: "3.1.0";
82
102
  info: {
83
103
  title: string;
84
104
  version: string;
@@ -102,7 +122,7 @@ declare function writeOutputFiles(config: ResolvedConfig, spec: OpenAPISpec, cwd
102
122
 
103
123
  interface GenerateOptions {
104
124
  config?: string;
105
- provider?: OpenAPIGenConfig['provider'];
125
+ provider?: OpenAPIGenConfig["provider"];
106
126
  cache?: boolean;
107
127
  cwd?: string;
108
128
  }
@@ -114,4 +134,4 @@ interface GenerateResult {
114
134
  }
115
135
  declare function generate(options?: GenerateOptions): Promise<GenerateResult>;
116
136
 
117
- export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
137
+ export { type GenerateOptions, type GenerateResult, type JSDocMode, type OpenAPIGenConfig, type Provider, type ResolvedConfig, type ResolvedLimits, analyzeRoutes, assembleSpec, filePathToUrlPath, generate, loadConfig, resolveConfig, scanRoutes, writeOutputFiles };
package/dist/index.js CHANGED
@@ -152,7 +152,12 @@ async function analyzeRoute(route, options, modelId, cache, getModel) {
152
152
  }
153
153
  let pathItem;
154
154
  try {
155
- pathItem = await callLLM(route, options.jsdocMode, getModel());
155
+ pathItem = await callLLM(
156
+ route,
157
+ options.jsdocMode,
158
+ getModel(),
159
+ options.limits
160
+ );
156
161
  } catch (err) {
157
162
  const message = err instanceof Error ? err.message : String(err);
158
163
  const isContentFilter = message.includes("content filtering policy") || message.includes("content_filter") || err?.status === 400;
@@ -217,12 +222,14 @@ For each exported function named GET, POST, PUT, PATCH, or DELETE, document:
217
222
 
218
223
  Return a single raw JSON object matching the OpenAPI 3.1 PathItem schema. No explanation, no markdown fences, no extra text \u2014 only the JSON object.`;
219
224
  }
220
- async function callLLM(route, jsdocMode, model) {
225
+ async function callLLM(route, jsdocMode, model, limits) {
221
226
  const prompt = buildPrompt(route, jsdocMode);
222
227
  const { text } = await ai.generateText({
223
228
  model,
224
229
  prompt,
225
- temperature: 0
230
+ temperature: 0,
231
+ maxOutputTokens: limits.maxTokens,
232
+ abortSignal: AbortSignal.timeout(limits.timeoutMs)
226
233
  });
227
234
  return parsePathItem(text, route.urlPath);
228
235
  }
@@ -251,7 +258,11 @@ var defaults = {
251
258
  cacheDir: ".openapi-cache",
252
259
  include: ["src/app/api/**/route.ts"],
253
260
  exclude: [],
254
- envFile: [".env", ".env.local"]
261
+ envFile: [".env", ".env.local"],
262
+ limits: {
263
+ maxTokens: 4e3,
264
+ timeoutMs: 6e4
265
+ }
255
266
  };
256
267
  function resolveConfig(config) {
257
268
  let envFile;
@@ -266,9 +277,14 @@ function resolveConfig(config) {
266
277
  ...defaults,
267
278
  ...config,
268
279
  envFile,
280
+ limits: {
281
+ ...defaults.limits,
282
+ ...config.limits
283
+ },
269
284
  output: {
270
285
  scalarDocs: false,
271
286
  scalarPath: "src/app/api/docs/route.ts",
287
+ scalarConfig: {},
272
288
  ...config.output
273
289
  },
274
290
  openapi: {
@@ -377,6 +393,7 @@ export function GET() {
377
393
  <script
378
394
  id="api-reference"
379
395
  data-url="${specUrl}"
396
+ ${config.output.scalarConfig && Object.keys(config.output.scalarConfig).length > 0 ? `data-config="${JSON.stringify(config.output.scalarConfig)}"` : ""}
380
397
  ></script>
381
398
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
382
399
  </body>
@@ -478,12 +495,15 @@ async function generate(options = {}) {
478
495
  console.log(`[openapi-ai-generator] Scanning routes...`);
479
496
  const routes = await scanRoutes(config.include, config.exclude, cwd);
480
497
  console.log(`[openapi-ai-generator] Found ${routes.length} route(s)`);
481
- console.log(`[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`);
498
+ console.log(
499
+ `[openapi-ai-generator] Analyzing routes with provider: ${config.provider}`
500
+ );
482
501
  const analyzed = await analyzeRoutes(routes, {
483
502
  provider: config.provider,
484
503
  jsdocMode: config.jsdocMode,
485
504
  cache: config.cache,
486
- cacheDir: config.cacheDir
505
+ cacheDir: config.cacheDir,
506
+ limits: config.limits
487
507
  });
488
508
  const fromCache = analyzed.filter((r) => r.fromCache).length;
489
509
  const skippedLLM = analyzed.filter((r) => r.skippedLLM).length;
@@ -492,9 +512,13 @@ async function generate(options = {}) {
492
512
  );
493
513
  const spec = assembleSpec(config, analyzed);
494
514
  writeOutputFiles(config, spec, cwd);
495
- console.log(`[openapi-ai-generator] Spec written to ${config.output.specPath}`);
515
+ console.log(
516
+ `[openapi-ai-generator] Spec written to ${config.output.specPath}`
517
+ );
496
518
  if (config.output.scalarDocs) {
497
- console.log(`[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`);
519
+ console.log(
520
+ `[openapi-ai-generator] Scalar docs written to ${config.output.scalarPath}`
521
+ );
498
522
  }
499
523
  return {
500
524
  routesAnalyzed: analyzed.length,