doc-detective-common 3.6.0 → 3.7.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/schemas/config_v3.schema.json +3880 -40
  2. package/dist/schemas/report_v3.schema.json +3991 -69
  3. package/dist/schemas/resolvedTests_v3.schema.json +7925 -163
  4. package/dist/schemas/sourceLocation_v3.schema.json +106 -0
  5. package/dist/schemas/spec_v3.schema.json +3991 -69
  6. package/dist/schemas/step_v3.schema.json +1915 -7
  7. package/dist/schemas/test_v3.schema.json +3995 -73
  8. package/package.json +6 -2
  9. package/src/ai.js +532 -0
  10. package/src/index.js +8 -0
  11. package/src/refineStep.js +267 -0
  12. package/src/schemas/build/checkLink_v2.schema.json +1 -1
  13. package/src/schemas/build/checkLink_v3.schema.json +1 -1
  14. package/src/schemas/build/click_v3.schema.json +1 -1
  15. package/src/schemas/build/config_v2.schema.json +14 -14
  16. package/src/schemas/build/config_v3.schema.json +30 -6
  17. package/src/schemas/build/context_v2.schema.json +1 -1
  18. package/src/schemas/build/context_v3.schema.json +1 -1
  19. package/src/schemas/build/dragAndDrop_v3.schema.json +1 -1
  20. package/src/schemas/build/find_v2.schema.json +3 -3
  21. package/src/schemas/build/find_v3.schema.json +4 -4
  22. package/src/schemas/build/goTo_v2.schema.json +1 -1
  23. package/src/schemas/build/goTo_v3.schema.json +1 -1
  24. package/src/schemas/build/httpRequest_v2.schema.json +2 -2
  25. package/src/schemas/build/httpRequest_v3.schema.json +3 -3
  26. package/src/schemas/build/loadCookie_v3.schema.json +1 -1
  27. package/src/schemas/build/loadVariables_v3.schema.json +1 -1
  28. package/src/schemas/build/moveTo_v2.schema.json +1 -1
  29. package/src/schemas/build/openApi_v2.schema.json +1 -1
  30. package/src/schemas/build/openApi_v3.schema.json +1 -1
  31. package/src/schemas/build/record_v3.schema.json +1 -1
  32. package/src/schemas/build/report_v3.schema.json +5 -5
  33. package/src/schemas/build/resolvedTests_v3.schema.json +3 -3
  34. package/src/schemas/build/runCode_v2.schema.json +1 -1
  35. package/src/schemas/build/runCode_v3.schema.json +1 -1
  36. package/src/schemas/build/runShell_v2.schema.json +1 -1
  37. package/src/schemas/build/runShell_v3.schema.json +1 -1
  38. package/src/schemas/build/saveCookie_v3.schema.json +1 -1
  39. package/src/schemas/build/saveScreenshot_v2.schema.json +1 -1
  40. package/src/schemas/build/screenshot_v3.schema.json +1 -1
  41. package/src/schemas/build/setVariables_v2.schema.json +1 -1
  42. package/src/schemas/build/sourceLocation_v3.schema.json +107 -0
  43. package/src/schemas/build/spec_v2.schema.json +4 -4
  44. package/src/schemas/build/spec_v3.schema.json +4 -4
  45. package/src/schemas/build/startRecording_v2.schema.json +1 -1
  46. package/src/schemas/build/step_v3.schema.json +45 -38
  47. package/src/schemas/build/stopRecord_v3.schema.json +1 -1
  48. package/src/schemas/build/stopRecording_v2.schema.json +1 -1
  49. package/src/schemas/build/test_v2.schema.json +15 -15
  50. package/src/schemas/build/test_v3.schema.json +10 -6
  51. package/src/schemas/build/typeKeys_v2.schema.json +1 -1
  52. package/src/schemas/build/type_v3.schema.json +1 -1
  53. package/src/schemas/build/wait_v2.schema.json +1 -1
  54. package/src/schemas/build/wait_v3.schema.json +1 -1
  55. package/src/schemas/dereferenceSchemas.js +1 -0
  56. package/src/schemas/output_schemas/config_v3.schema.json +3880 -40
  57. package/src/schemas/output_schemas/report_v3.schema.json +3991 -69
  58. package/src/schemas/output_schemas/resolvedTests_v3.schema.json +7925 -163
  59. package/src/schemas/output_schemas/sourceLocation_v3.schema.json +106 -0
  60. package/src/schemas/output_schemas/spec_v3.schema.json +3991 -69
  61. package/src/schemas/output_schemas/step_v3.schema.json +1915 -7
  62. package/src/schemas/output_schemas/test_v3.schema.json +3995 -73
  63. package/src/schemas/schemas.json +25978 -596
  64. package/src/schemas/src_schemas/config_v3.schema.json +24 -0
  65. package/src/schemas/src_schemas/sourceLocation_v3.schema.json +106 -0
  66. package/src/schemas/src_schemas/step_v3.schema.json +7 -0
  67. package/src/schemas/src_schemas/test_v3.schema.json +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc-detective-common",
3
- "version": "3.6.0",
3
+ "version": "3.7.0-preview.0",
4
4
  "description": "Shared components for Doc Detective projects.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -25,12 +25,16 @@
25
25
  "sinon": "^21.0.0"
26
26
  },
27
27
  "dependencies": {
28
+ "@ai-sdk/anthropic": "^2.0.54",
29
+ "@ai-sdk/openai": "^2.0.80",
28
30
  "@apidevtools/json-schema-ref-parser": "^15.1.3",
31
+ "ai": "^5.0.109",
29
32
  "ajv": "^8.17.1",
30
33
  "ajv-errors": "^3.0.0",
31
34
  "ajv-formats": "^3.0.1",
32
35
  "ajv-keywords": "^5.1.0",
33
36
  "axios": "^1.13.2",
34
- "yaml": "^2.8.2"
37
+ "yaml": "^2.8.2",
38
+ "zod": "^4.1.13"
35
39
  }
36
40
  }
package/src/ai.js ADDED
@@ -0,0 +1,532 @@
1
+ const { generateText, generateObject, jsonSchema } = require("ai");
2
+ const { createOpenAI } = require("@ai-sdk/openai");
3
+ const { createAnthropic } = require("@ai-sdk/anthropic");
4
+ const { z } = require("zod");
5
+ const Ajv = require("ajv");
6
+ const addFormats = require("ajv-formats");
7
+
8
+ const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
9
+ const MAX_SCHEMA_VALIDATION_RETRIES = 3;
10
+
11
+ /**
12
+ * Maps our supported model enums to the model identifiers that platforms expect.
13
+ */
14
+ const modelMap = {
15
+ // Anthropic models
16
+ "anthropic/claude-haiku-4.5": "claude-haiku-4-5",
17
+ "anthropic/claude-sonnet-4.5": "claude-sonnet-4-5",
18
+ "anthropic/claude-opus-4.5": "claude-opus-4-5",
19
+ // OpenAI models
20
+ "openai/gpt-5.1": "gpt-5.1",
21
+ "openai/gpt-5-mini": "gpt-5-mini",
22
+ "openai/gpt-5-nano": "gpt-5-nano",
23
+ };
24
+
25
+ const getDefaultProvider = (config) => {
26
+ // Try to detect from environment variables if no model is provided
27
+ if (process.env.ANTHROPIC_API_KEY || config.integrations?.anthropic) {
28
+ return {
29
+ provider: "anthropic",
30
+ model: "claude-haiku-4-5",
31
+ apiKey: process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey,
32
+ };
33
+ } else if (process.env.OPENAI_API_KEY || config.integrations?.openAi) {
34
+ return {
35
+ provider: "openai",
36
+ model: "gpt-5-mini",
37
+ apiKey: process.env.OPENAI_API_KEY || config.integrations.openAi.apiKey,
38
+ };
39
+ } else {
40
+ return { provider: null, model: null, apiKey: null };
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Detects the provider, model, and API from a model string and environment variables.
46
+ * @param {string} model - The model identifier.
47
+ * @returns {{ provider: "openai" | "anthropic" | null, model: string | null, apiKey: string | null }} The detected provider, model, and API key.
48
+ */
49
+ const detectProvider = (config, model) => {
50
+ const detectedModel = modelMap[model] || null;
51
+ if (!detectedModel) return getDefaultProvider(config);
52
+
53
+ if (model.startsWith("anthropic/") && (process.env.ANTHROPIC_API_KEY || config.integrations?.anthropic)) {
54
+ const apiKey = process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey;
55
+ return { provider: "anthropic", model: detectedModel, apiKey };
56
+ }
57
+
58
+ if (model.startsWith("openai/") && (process.env.OPENAI_API_KEY || config.integrations?.openAi)) {
59
+ const apiKey = process.env.OPENAI_API_KEY || config.integrations.openAi.apiKey;
60
+ return { provider: "openai", model: detectedModel, apiKey };
61
+ }
62
+
63
+ return { provider: null, model: null };
64
+ };
65
+
66
+ /**
67
+ * Creates a provider instance based on the provider name.
68
+ * @param {Object} options
69
+ * @param {"openai" | "anthropic"} options.provider - The provider name.
70
+ * @param {string} [options.apiKey] - Optional API key override.
71
+ * @param {string} [options.baseURL] - Optional base URL override.
72
+ * @returns {Function} The provider factory function.
73
+ */
74
+ const createProvider = ({ provider, apiKey, baseURL }) => {
75
+ if (provider === "openai") {
76
+ const options = {};
77
+ if (apiKey) options.apiKey = apiKey;
78
+ if (baseURL) options.baseURL = baseURL;
79
+ return createOpenAI(options);
80
+ }
81
+
82
+ if (provider === "anthropic") {
83
+ const options = {};
84
+ if (apiKey) options.apiKey = apiKey;
85
+ if (baseURL) options.baseURL = baseURL;
86
+ return createAnthropic(options);
87
+ }
88
+
89
+ throw new Error(`Unsupported provider: ${provider}`);
90
+ };
91
+
92
+ /**
93
+ * Converts a file object to AI SDK image part format.
94
+ * @param {Object} file - The file object.
95
+ * @param {string} file.type - The file type (e.g., "image").
96
+ * @param {string} file.data - Base64 data or URL.
97
+ * @param {string} [file.mimeType] - The MIME type (e.g., "image/png").
98
+ * @returns {Object} The AI SDK image part.
99
+ */
100
+ const fileToImagePart = (file) => {
101
+ if (file.type !== "image") {
102
+ throw new Error(
103
+ `Unsupported file type: ${file.type}. Only "image" is supported.`
104
+ );
105
+ }
106
+
107
+ // Check if data is a URL
108
+ const isUrl =
109
+ file.data.startsWith("http://") || file.data.startsWith("https://");
110
+
111
+ if (isUrl) {
112
+ return {
113
+ type: "image",
114
+ image: new URL(file.data),
115
+ };
116
+ }
117
+
118
+ // Base64 data
119
+ return {
120
+ type: "image",
121
+ image: file.data,
122
+ mimeType: file.mimeType,
123
+ };
124
+ };
125
+
126
+ /**
127
+ * Builds message content from prompt and files.
128
+ * @param {Object} options
129
+ * @param {string} options.prompt - The text prompt.
130
+ * @param {Array} [options.files] - Optional array of file objects.
131
+ * @returns {string | Array} The message content.
132
+ */
133
+ const buildMessageContent = ({ prompt, files }) => {
134
+ if (!files || files.length === 0) {
135
+ return prompt;
136
+ }
137
+
138
+ const parts = [];
139
+
140
+ // Add text part
141
+ parts.push({ type: "text", text: prompt });
142
+
143
+ // Add file parts
144
+ for (const file of files) {
145
+ parts.push(fileToImagePart(file));
146
+ }
147
+
148
+ return parts;
149
+ };
150
+
151
+ /**
152
+ * Checks if a schema is a Zod schema.
153
+ * @param {Object} schema - The schema to check.
154
+ * @returns {boolean} True if the schema is a Zod schema.
155
+ */
156
+ const isZodSchema = (schema) => {
157
+ return schema && typeof schema.safeParse === "function";
158
+ };
159
+
160
+ /**
161
+ * Validates an object against a Zod schema.
162
+ * @param {Object} object - The object to validate.
163
+ * @param {z.ZodSchema} schema - The Zod schema.
164
+ * @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
165
+ */
166
+ const validateAgainstZodSchema = (object, schema) => {
167
+ const result = schema.safeParse(object);
168
+
169
+ if (result.success) {
170
+ return { valid: true, errors: null, object: result.data };
171
+ }
172
+
173
+ const errors = result.error.issues
174
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
175
+ .join(", ");
176
+
177
+ return { valid: false, errors, object };
178
+ };
179
+
180
+ /**
181
+ * Validates an object against a JSON schema.
182
+ * @param {Object} object - The object to validate.
183
+ * @param {Object} schema - The JSON schema.
184
+ * @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
185
+ */
186
+ const validateAgainstJsonSchema = (object, schema) => {
187
+ const ajv = new Ajv({
188
+ allErrors: true,
189
+ useDefaults: true,
190
+ coerceTypes: true,
191
+ strict: false,
192
+ });
193
+ addFormats(ajv);
194
+
195
+ const validate = ajv.compile(schema);
196
+ const valid = validate(object);
197
+
198
+ if (valid) {
199
+ return { valid: true, errors: null, object };
200
+ }
201
+
202
+ const errors = validate.errors
203
+ .map((error) => `${error.instancePath || "/"} ${error.message}`)
204
+ .join(", ");
205
+
206
+ return { valid: false, errors, object };
207
+ };
208
+
209
+ /**
210
+ * Validates an object against a schema (Zod or JSON schema).
211
+ * @param {Object} object - The object to validate.
212
+ * @param {z.ZodSchema | Object} schema - The Zod or JSON schema.
213
+ * @returns {{ valid: boolean, errors: string | null, object: Object }} Validation result.
214
+ */
215
+ const validateAgainstSchema = (object, schema) => {
216
+ if (isZodSchema(schema)) {
217
+ return validateAgainstZodSchema(object, schema);
218
+ }
219
+ return validateAgainstJsonSchema(object, schema);
220
+ };
221
+
222
+ /**
223
+ * Converts a schema to the format expected by the AI SDK.
224
+ * Zod schemas are passed directly; JSON schemas are wrapped with jsonSchema().
225
+ * @param {z.ZodSchema | Object} schema - The Zod or JSON schema.
226
+ * @returns {Object} The schema in AI SDK format.
227
+ */
228
+ const toAiSdkSchema = (schema) => {
229
+ if (isZodSchema(schema)) {
230
+ return schema;
231
+ }
232
+ return jsonSchema(schema);
233
+ };
234
+
235
+ /**
236
+ * Extracts the API key for a provider from a Doc Detective config object.
237
+ * @param {Object} config - The Doc Detective configuration object.
238
+ * @param {"openai" | "anthropic"} provider - The provider name.
239
+ * @returns {string | undefined} The API key if found.
240
+ */
241
+ const getApiKey = (config, provider) => {
242
+ if (!config || !config.integrations) return undefined;
243
+
244
+ if (
245
+ provider === "anthropic" &&
246
+ (process.env.ANTHROPIC_API_KEY || config.integrations.anthropic)
247
+ ) {
248
+ return (
249
+ process.env.ANTHROPIC_API_KEY || config.integrations.anthropic.apiKey
250
+ );
251
+ }
252
+
253
+ if (
254
+ provider === "openai" &&
255
+ (process.env.OPENAI_API_KEY || config.integrations.openai)
256
+ ) {
257
+ return process.env.OPENAI_API_KEY || config.integrations.openai.apiKey;
258
+ }
259
+
260
+ return undefined;
261
+ };
262
+
263
+ /**
264
+ * Generates text or structured output using an AI model.
265
+ *
266
+ * @param {Object} options - Generation options.
267
+ * @param {string} [options.prompt] - The text prompt (required if messages not provided).
268
+ * @param {Array} [options.messages] - Array of messages for multi-turn conversation.
269
+ * @param {Array} [options.files] - Array of file objects to include (e.g., images).
270
+ * @param {string} [options.files[].type] - File type ("image").
271
+ * @param {string} [options.files[].data] - Base64 data or URL.
272
+ * @param {string} [options.files[].mimeType] - MIME type (e.g., "image/png").
273
+ * @param {string} [options.model] - Model identifier (default: "anthropic/claude-haiku-4.5").
274
+ * @param {string} [options.system] - System message.
275
+ * @param {z.ZodSchema | Object} [options.schema] - Zod schema or JSON schema for structured output.
276
+ * @param {string} [options.schemaName] - Name for the schema (used in API calls).
277
+ * @param {string} [options.schemaDescription] - Description for the schema.
278
+ * @param {"openai" | "anthropic"} [options.provider] - Explicit provider override.
279
+ * @param {Object} [options.config] - Doc Detective config object with integrations.anthropic/openai API keys.
280
+ * @param {string} [options.apiKey] - API key override (takes precedence over config and env vars).
281
+ * @param {string} [options.baseURL] - Base URL override for the provider.
282
+ * @param {number} [options.temperature] - Temperature for generation.
283
+ * @param {number} [options.maxTokens] - Maximum tokens to generate.
284
+ * @returns {Promise<Object>} Generation result.
285
+ * @returns {string} [result.text] - Generated text (when no schema provided).
286
+ * @returns {Object} [result.object] - Generated object (when schema provided).
287
+ * @returns {Object} result.usage - Token usage information.
288
+ * @returns {string} result.finishReason - Why generation stopped.
289
+ *
290
+ * @throws {Error} If prompt/messages is missing or provider cannot be determined.
291
+ */
292
+ const generate = async ({
293
+ prompt,
294
+ messages,
295
+ files,
296
+ model,
297
+ system,
298
+ schema,
299
+ schemaName,
300
+ schemaDescription,
301
+ provider,
302
+ config = {},
303
+ apiKey,
304
+ baseURL,
305
+ temperature,
306
+ maxTokens,
307
+ }) => {
308
+ // Validate required input
309
+ if (!prompt && (!messages || messages.length === 0)) {
310
+ throw new Error("Either 'prompt' or 'messages' is required.");
311
+ }
312
+
313
+ // Determine provider, model, and API key
314
+ const detected = detectProvider(config, model);
315
+
316
+ if (!detected.provider) {
317
+ throw new Error(
318
+ `Cannot determine provider for model "${model}". Please specify a 'provider' option ("openai" or "anthropic").`
319
+ );
320
+ }
321
+
322
+ // Create provider instance
323
+ const providerFactory = createProvider({
324
+ provider: detected.provider,
325
+ apiKey: detected.apiKey,
326
+ baseURL,
327
+ });
328
+
329
+ // Get model instance
330
+ const modelInstance = providerFactory(detected.model);
331
+
332
+ // Build generation options
333
+ const generationOptions = {
334
+ model: modelInstance,
335
+ };
336
+
337
+ // Add system message if provided
338
+ if (system) {
339
+ generationOptions.system = system;
340
+ }
341
+
342
+ // Add temperature if provided
343
+ if (temperature !== undefined) {
344
+ generationOptions.temperature = temperature;
345
+ }
346
+
347
+ // Add maxTokens if provided
348
+ if (maxTokens !== undefined) {
349
+ generationOptions.maxTokens = maxTokens;
350
+ }
351
+
352
+ // Build messages or prompt
353
+ if (messages && messages.length > 0) {
354
+ // Find the index of the last user message
355
+ const lastUserIndex = messages.findLastIndex((msg) => msg.role === "user");
356
+
357
+ // Use messages array, attaching files only to the last user message
358
+ generationOptions.messages = messages.map((msg, index) => {
359
+ if (index === lastUserIndex && files && files.length > 0) {
360
+ return {
361
+ ...msg,
362
+ content: buildMessageContent({ prompt: msg.content, files }),
363
+ };
364
+ }
365
+ return msg;
366
+ });
367
+ } else if (files && files.length > 0) {
368
+ // When files are provided, we must use messages format for multimodal content
369
+ generationOptions.messages = [
370
+ {
371
+ role: "user",
372
+ content: buildMessageContent({ prompt, files }),
373
+ },
374
+ ];
375
+ } else {
376
+ // Use simple prompt for text-only requests
377
+ generationOptions.prompt = prompt;
378
+ }
379
+
380
+ // Handle structured output with schema
381
+ if (schema) {
382
+ return generateWithSchemaValidation({
383
+ generationOptions,
384
+ schema,
385
+ schemaName,
386
+ schemaDescription,
387
+ prompt,
388
+ messages,
389
+ });
390
+ }
391
+
392
+ // Generate text
393
+ const result = await generateText(generationOptions);
394
+
395
+ return {
396
+ text: result.text,
397
+ usage: result.usage,
398
+ finishReason: result.finishReason,
399
+ };
400
+ };
401
+
402
+ /**
403
+ * Generates structured output with schema validation and retry logic.
404
+ * @param {Object} options
405
+ * @param {Object} options.generationOptions - AI SDK generation options.
406
+ * @param {z.ZodSchema | Object} options.schema - Zod schema or JSON schema for validation.
407
+ * @param {string} [options.schemaName] - Name for the schema.
408
+ * @param {string} [options.schemaDescription] - Description for the schema.
409
+ * @param {string} [options.prompt] - Original prompt for retry context.
410
+ * @param {Array} [options.messages] - Original messages for retry context.
411
+ * @returns {Promise<Object>} Generation result with validated object.
412
+ */
413
+ const generateWithSchemaValidation = async ({
414
+ generationOptions,
415
+ schema,
416
+ schemaName,
417
+ schemaDescription,
418
+ prompt,
419
+ messages,
420
+ }) => {
421
+ let lastError = null;
422
+ let lastObject = null;
423
+ let wrappedSchema = false;
424
+
425
+ // If JSON schema with allOf/anyOf/oneOf at the top level, wrap it in an object
426
+ if (!isZodSchema(schema) && (schema.allOf || schema.anyOf || schema.oneOf)) {
427
+ schema = {
428
+ type: "object",
429
+ properties: {
430
+ object: schema,
431
+ },
432
+ required: ["object"],
433
+ additionalProperties: false,
434
+ };
435
+ wrappedSchema = true;
436
+ }
437
+
438
+ // Convert schema to AI SDK format (wraps JSON schemas with jsonSchema())
439
+ const aiSdkSchema = toAiSdkSchema(schema);
440
+
441
+ for (let attempt = 1; attempt <= MAX_SCHEMA_VALIDATION_RETRIES; attempt++) {
442
+ const objectOptions = {
443
+ ...generationOptions,
444
+ schema: aiSdkSchema,
445
+ };
446
+
447
+ if (schemaName) {
448
+ objectOptions.schemaName = schemaName;
449
+ }
450
+
451
+ if (schemaDescription) {
452
+ objectOptions.schemaDescription = schemaDescription;
453
+ }
454
+
455
+ // Add retry context if this is a retry attempt
456
+ if (attempt > 1 && lastError) {
457
+ const retryMessage = `Previous attempt failed schema validation with errors: ${lastError}. Please fix these issues and try again.`;
458
+
459
+ if (objectOptions.messages) {
460
+ // Add retry context to messages
461
+ objectOptions.messages = [
462
+ ...objectOptions.messages,
463
+ { role: "assistant", content: JSON.stringify(lastObject) },
464
+ { role: "user", content: retryMessage },
465
+ ];
466
+ } else if (typeof objectOptions.prompt === "string") {
467
+ // Add retry context to prompt
468
+ objectOptions.prompt = `${objectOptions.prompt}\n\n${retryMessage}`;
469
+ }
470
+ }
471
+
472
+ try {
473
+ const result = await generateObject(objectOptions);
474
+
475
+ const validationObject = wrappedSchema
476
+ ? result.object.object
477
+ : result.object;
478
+ const validationSchema = wrappedSchema
479
+ ? schema.properties.object
480
+ : schema;
481
+
482
+ // Validate the generated object against the schema ourselves
483
+ const validation = validateAgainstSchema(
484
+ validationObject,
485
+ validationSchema
486
+ );
487
+
488
+ if (validation.valid) {
489
+ return {
490
+ object: validationObject,
491
+ usage: result.usage,
492
+ finishReason: result.finishReason,
493
+ };
494
+ }
495
+
496
+ // Schema validation failed, store error for retry
497
+ lastError = validation.errors;
498
+ lastObject = result.object;
499
+
500
+ if (attempt === MAX_SCHEMA_VALIDATION_RETRIES) {
501
+ throw new Error(
502
+ `Schema validation failed after ${MAX_SCHEMA_VALIDATION_RETRIES} attempts. Last errors: ${validation.errors}`
503
+ );
504
+ }
505
+ } catch (error) {
506
+ // If it's our validation error and we have retries left, continue
507
+ if (
508
+ error.message.includes("Schema validation failed after") ||
509
+ attempt === MAX_SCHEMA_VALIDATION_RETRIES
510
+ ) {
511
+ throw error;
512
+ }
513
+
514
+ // Store the error and retry
515
+ lastError = error.message;
516
+ lastObject = null;
517
+ }
518
+ }
519
+
520
+ throw new Error(
521
+ `Schema validation failed after ${MAX_SCHEMA_VALIDATION_RETRIES} attempts. Last errors: ${lastError}`
522
+ );
523
+ };
524
+
525
+ module.exports = {
526
+ generate,
527
+ detectProvider,
528
+ getApiKey,
529
+ modelMap,
530
+ DEFAULT_MODEL,
531
+ MAX_SCHEMA_VALIDATION_RETRIES,
532
+ };
package/src/index.js CHANGED
@@ -2,6 +2,8 @@ const { schemas } = require("./schemas");
2
2
  const { validate, transformToSchemaKey } = require("./validate");
3
3
  const { resolvePaths } = require("./resolvePaths");
4
4
  const { readFile } = require("./files");
5
+ const { generate, detectProvider, getApiKey, DEFAULT_MODEL, MAX_SCHEMA_VALIDATION_RETRIES } = require("./ai");
6
+ const { refineStep } = require("./refineStep");
5
7
 
6
8
  module.exports = {
7
9
  schemas,
@@ -9,4 +11,10 @@ module.exports = {
9
11
  resolvePaths,
10
12
  readFile,
11
13
  transformToSchemaKey,
14
+ generate,
15
+ detectProvider,
16
+ getApiKey,
17
+ DEFAULT_MODEL,
18
+ MAX_SCHEMA_VALIDATION_RETRIES,
19
+ refineStep,
12
20
  };