@doclo/providers-llm 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/index.js ADDED
@@ -0,0 +1,2743 @@
1
+ // src/schema-translator.ts
2
+ import { zodToJsonSchema } from "@alcyone-labs/zod-to-json-schema";
3
+ var SchemaTranslator = class {
4
+ /**
5
+ * Unified → OpenAI/Grok (standard JSON Schema)
6
+ * OpenAI strict mode doesn't support nullable: true
7
+ * Must convert to anyOf: [{ type: "string" }, { type: "null" }]
8
+ */
9
+ toOpenAISchema(schema) {
10
+ const jsonSchema = this.convertZodIfNeeded(schema);
11
+ return this.convertNullableToAnyOf(jsonSchema);
12
+ }
13
+ /**
14
+ * Detect if schema is a Zod schema and convert to JSON Schema
15
+ * Public method to allow embedding schemas in prompts
16
+ */
17
+ convertZodIfNeeded(schema) {
18
+ if (schema && typeof schema === "object") {
19
+ const flexibleSchema = schema;
20
+ if (flexibleSchema["~standard"]?.vendor === "zod") {
21
+ const jsonSchema = zodToJsonSchema(schema);
22
+ delete jsonSchema.$schema;
23
+ delete jsonSchema.$defs;
24
+ delete jsonSchema.definitions;
25
+ return jsonSchema;
26
+ }
27
+ if (flexibleSchema._def) {
28
+ const jsonSchema = zodToJsonSchema(schema);
29
+ delete jsonSchema.$schema;
30
+ delete jsonSchema.$defs;
31
+ delete jsonSchema.definitions;
32
+ return jsonSchema;
33
+ }
34
+ }
35
+ return schema;
36
+ }
37
+ /**
38
+ * Convert nullable fields to anyOf format for OpenAI strict mode
39
+ * nullable: true is not supported, must use anyOf with null type
40
+ */
41
+ convertNullableToAnyOf(schema) {
42
+ if (typeof schema !== "object" || schema === null) {
43
+ return schema;
44
+ }
45
+ const result = { ...schema };
46
+ if (result.nullable === true) {
47
+ delete result.nullable;
48
+ const baseType = result.type;
49
+ if (baseType) {
50
+ return {
51
+ anyOf: [
52
+ { type: baseType },
53
+ { type: "null" }
54
+ ]
55
+ };
56
+ }
57
+ }
58
+ if (result.properties) {
59
+ result.properties = Object.fromEntries(
60
+ Object.entries(result.properties).map(([key, value]) => {
61
+ if (value && typeof value === "object" && value.nullable === true) {
62
+ const { nullable, type, ...rest } = value;
63
+ return [key, {
64
+ anyOf: [
65
+ { type, ...rest },
66
+ { type: "null" }
67
+ ]
68
+ }];
69
+ }
70
+ return [key, this.convertNullableToAnyOf(value)];
71
+ })
72
+ );
73
+ }
74
+ if (result.items && !Array.isArray(result.items)) {
75
+ result.items = this.convertNullableToAnyOf(result.items);
76
+ }
77
+ const keywords = ["anyOf", "oneOf", "allOf"];
78
+ keywords.forEach((keyword) => {
79
+ const schemaArray = result[keyword];
80
+ if (schemaArray && Array.isArray(schemaArray)) {
81
+ result[keyword] = schemaArray.map((s) => this.convertNullableToAnyOf(s));
82
+ }
83
+ });
84
+ return result;
85
+ }
86
+ /**
87
+ * Unified → Claude (Tool Input Schema format)
88
+ * Claude requires tool calling with input_schema
89
+ * Claude supports nullable: true directly
90
+ */
91
+ toClaudeToolSchema(schema) {
92
+ const jsonSchema = this.convertZodIfNeeded(schema);
93
+ return {
94
+ name: "extract_data",
95
+ description: "Extract structured data according to the schema",
96
+ input_schema: jsonSchema
97
+ };
98
+ }
99
+ /**
100
+ * Unified → Claude for OpenRouter
101
+ * When using Claude via OpenRouter, use anyOf format like OpenAI
102
+ */
103
+ toClaudeOpenRouterSchema(schema) {
104
+ const jsonSchema = this.convertZodIfNeeded(schema);
105
+ return this.convertNullableToAnyOf(jsonSchema);
106
+ }
107
+ /**
108
+ * Unified → Gemini (OpenAPI 3.0 subset with propertyOrdering)
109
+ * Gemini uses a subset of OpenAPI 3.0 schema
110
+ */
111
+ toGeminiSchema(schema) {
112
+ const jsonSchema = this.convertZodIfNeeded(schema);
113
+ const geminiSchema = {
114
+ type: jsonSchema.type
115
+ };
116
+ if (jsonSchema.properties) {
117
+ geminiSchema.properties = {};
118
+ const propertyNames = Object.keys(jsonSchema.properties);
119
+ geminiSchema.propertyOrdering = propertyNames;
120
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
121
+ geminiSchema.properties[key] = this.convertPropertyToGemini(value);
122
+ }
123
+ }
124
+ if (jsonSchema.required && Array.isArray(jsonSchema.required)) {
125
+ geminiSchema.required = jsonSchema.required;
126
+ }
127
+ if (jsonSchema.additionalProperties !== void 0) {
128
+ geminiSchema.additionalProperties = jsonSchema.additionalProperties;
129
+ }
130
+ return geminiSchema;
131
+ }
132
+ /**
133
+ * Convert individual property to Gemini format
134
+ */
135
+ convertPropertyToGemini(property) {
136
+ const geminiProp = {
137
+ type: property.type
138
+ };
139
+ if (property.description) {
140
+ geminiProp.description = property.description;
141
+ }
142
+ if (property.nullable !== void 0) {
143
+ geminiProp.nullable = property.nullable;
144
+ }
145
+ if (property.enum) {
146
+ geminiProp.enum = property.enum;
147
+ }
148
+ if (property.items) {
149
+ if (Array.isArray(property.items)) {
150
+ geminiProp.items = property.items.length > 0 ? this.convertPropertyToGemini(property.items[0]) : void 0;
151
+ } else {
152
+ geminiProp.items = this.convertPropertyToGemini(property.items);
153
+ }
154
+ }
155
+ if (property.properties) {
156
+ geminiProp.properties = {};
157
+ for (const [key, value] of Object.entries(property.properties)) {
158
+ geminiProp.properties[key] = this.convertPropertyToGemini(value);
159
+ }
160
+ }
161
+ if (property.required) {
162
+ geminiProp.required = property.required;
163
+ }
164
+ return geminiProp;
165
+ }
166
+ };
167
+
168
+ // src/schema-prompt-formatter.ts
169
+ function formatSchemaForPrompt(schema, indent = 0) {
170
+ if (!schema || typeof schema !== "object") {
171
+ return "";
172
+ }
173
+ const indentStr = " ".repeat(indent);
174
+ let result = "";
175
+ if (schema.type === "object" && schema.properties) {
176
+ const properties = schema.properties;
177
+ const required = schema.required || [];
178
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
179
+ const isRequired = required.includes(fieldName);
180
+ const requiredMarker = isRequired ? " (REQUIRED)" : " (optional)";
181
+ result += `${indentStr}- \`${fieldName}\`${requiredMarker}`;
182
+ const type = getTypeDescription(fieldSchema);
183
+ if (type) {
184
+ result += `: ${type}`;
185
+ }
186
+ if (fieldSchema.description) {
187
+ result += `
188
+ ${indentStr} ${fieldSchema.description}`;
189
+ }
190
+ if (fieldSchema.enum) {
191
+ result += `
192
+ ${indentStr} Allowed values: ${fieldSchema.enum.map((v) => JSON.stringify(v)).join(", ")}`;
193
+ }
194
+ result += "\n";
195
+ if (fieldSchema.type === "object" && fieldSchema.properties) {
196
+ result += formatSchemaForPrompt(fieldSchema, indent + 1);
197
+ }
198
+ if (fieldSchema.type === "array" && fieldSchema.items) {
199
+ result += `${indentStr} Array items:
200
+ `;
201
+ const itemSchema = Array.isArray(fieldSchema.items) ? fieldSchema.items[0] : fieldSchema.items;
202
+ if (itemSchema && itemSchema.type === "object" && itemSchema.properties) {
203
+ result += formatSchemaForPrompt(itemSchema, indent + 2);
204
+ } else if (itemSchema) {
205
+ const itemType = getTypeDescription(itemSchema);
206
+ result += `${indentStr} ${itemType}
207
+ `;
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+ function getTypeDescription(schema) {
215
+ if (!schema) return "any";
216
+ if (schema.type) {
217
+ const typeStr = Array.isArray(schema.type) ? schema.type.join(" | ") : schema.type;
218
+ if (typeStr === "array" || Array.isArray(schema.type) && schema.type.includes("array")) {
219
+ if (schema.items && !Array.isArray(schema.items) && schema.items.type) {
220
+ const itemType = Array.isArray(schema.items.type) ? schema.items.type.join(" | ") : schema.items.type;
221
+ return `array of ${itemType}`;
222
+ }
223
+ return "array";
224
+ }
225
+ if ((typeStr === "string" || Array.isArray(schema.type) && schema.type.includes("string")) && schema.format) {
226
+ const formatHints = {
227
+ "date": "YYYY-MM-DD",
228
+ "time": "HH:MM or HH:MM:SS",
229
+ "date-time": "YYYY-MM-DDTHH:MM:SS (ISO 8601)"
230
+ };
231
+ const hint = formatHints[schema.format];
232
+ if (hint) {
233
+ return `string (format: ${schema.format}, use ${hint})`;
234
+ }
235
+ return `string (format: ${schema.format})`;
236
+ }
237
+ return typeStr;
238
+ }
239
+ if (schema.anyOf) {
240
+ return schema.anyOf.map((s) => getTypeDescription(s)).join(" OR ");
241
+ }
242
+ if (schema.oneOf) {
243
+ return schema.oneOf.map((s) => getTypeDescription(s)).join(" OR ");
244
+ }
245
+ return "any";
246
+ }
247
+ function buildSchemaPromptSection(schema) {
248
+ const schemaFields = formatSchemaForPrompt(schema);
249
+ return `
250
+ ==================================================
251
+ CRITICAL: OUTPUT STRUCTURE REQUIREMENTS
252
+ ==================================================
253
+
254
+ YOU MUST RETURN JSON MATCHING THIS EXACT STRUCTURE:
255
+
256
+ ${schemaFields}
257
+
258
+ CRITICAL FIELD NAME REQUIREMENTS:
259
+ \u2713 Use EXACTLY the field names shown above (character-for-character match)
260
+ \u2713 Preserve the exact casing (e.g., "fullName", not "full_name" or "FullName")
261
+ \u2713 Do NOT abbreviate field names (e.g., "dob" instead of "dateOfBirth")
262
+ \u2713 Do NOT invent alternative names (e.g., "directorName" instead of "fullName")
263
+ \u2713 Do NOT use snake_case if the schema uses camelCase
264
+ \u2713 Do NOT flatten nested structures or rename nested fields
265
+ \u2713 The schema above is the SINGLE SOURCE OF TRUTH for field naming
266
+
267
+ MISSING DATA:
268
+ - If a required field has no data in the document, use null
269
+ - If an optional field has no data, you may omit it or use null
270
+ - Do NOT invent data that isn't in the document
271
+
272
+ ==================================================
273
+ `.trim();
274
+ }
275
+ function combineSchemaAndUserPrompt(schema, userPrompt) {
276
+ const schemaSection = buildSchemaPromptSection(schema);
277
+ if (!userPrompt || userPrompt.trim() === "") {
278
+ return schemaSection + "\n\nTASK: Extract structured data from the provided document.";
279
+ }
280
+ return schemaSection + "\n\n" + userPrompt;
281
+ }
282
+
283
+ // src/provider-registry.ts
284
+ function normalizeProviderType(type) {
285
+ if (type === "x-ai") return "xai";
286
+ return type;
287
+ }
288
+ var ProviderRegistry = class {
289
+ factories = /* @__PURE__ */ new Map();
290
+ /**
291
+ * Register a provider factory
292
+ * Called by each provider package when it's imported
293
+ */
294
+ register(type, factory) {
295
+ const normalizedType = normalizeProviderType(type);
296
+ this.factories.set(normalizedType, factory);
297
+ }
298
+ /**
299
+ * Check if a provider is registered
300
+ */
301
+ has(type) {
302
+ const normalizedType = normalizeProviderType(type);
303
+ return this.factories.has(normalizedType);
304
+ }
305
+ /**
306
+ * Get a provider factory
307
+ */
308
+ get(type) {
309
+ const normalizedType = normalizeProviderType(type);
310
+ return this.factories.get(normalizedType);
311
+ }
312
+ /**
313
+ * Create a provider instance
314
+ * @throws Error if provider is not registered
315
+ */
316
+ create(config) {
317
+ const normalizedType = normalizeProviderType(config.provider);
318
+ const factory = this.factories.get(normalizedType);
319
+ if (!factory) {
320
+ const registered = Array.from(this.factories.keys()).join(", ") || "none";
321
+ throw new Error(
322
+ `Provider '${config.provider}' is not registered. Registered providers: ${registered}. Make sure to import the provider package (e.g., import '@doclo/providers-${normalizedType}').`
323
+ );
324
+ }
325
+ return factory({ ...config, provider: normalizedType });
326
+ }
327
+ /**
328
+ * Get all registered provider types
329
+ */
330
+ getRegisteredTypes() {
331
+ return Array.from(this.factories.keys());
332
+ }
333
+ /**
334
+ * Clear all registrations (for testing)
335
+ */
336
+ clear() {
337
+ this.factories.clear();
338
+ }
339
+ };
340
+ var providerRegistry = new ProviderRegistry();
341
+ function registerProvider(type, factory) {
342
+ providerRegistry.register(type, factory);
343
+ }
344
+ function createProviderFromRegistry(config) {
345
+ return providerRegistry.create(config);
346
+ }
347
+
348
+ // src/providers/openai.ts
349
+ import { fetchWithTimeout, DEFAULT_LIMITS, safeJsonParse } from "@doclo/core/security";
350
+ function extractProviderFromModel(model, defaultProvider) {
351
+ const slashIndex = model.indexOf("/");
352
+ return slashIndex > 0 ? model.substring(0, slashIndex) : defaultProvider;
353
+ }
354
+ var OpenAIProvider = class {
355
+ name;
356
+ capabilities = {
357
+ supportsStructuredOutput: true,
358
+ supportsStreaming: true,
359
+ supportsImages: true,
360
+ supportsPDFs: true,
361
+ maxPDFPages: 100,
362
+ maxPDFSize: 32,
363
+ maxContextTokens: 128e3
364
+ };
365
+ config;
366
+ translator;
367
+ limits;
368
+ constructor(config) {
369
+ this.config = config;
370
+ const baseProvider = extractProviderFromModel(config.model, "openai");
371
+ this.name = `${baseProvider}:${config.model}`;
372
+ this.translator = new SchemaTranslator();
373
+ this.limits = {
374
+ ...DEFAULT_LIMITS,
375
+ ...config.limits || {}
376
+ };
377
+ }
378
+ async completeJson(params) {
379
+ const startTime = Date.now();
380
+ const mode = params.mode || (params.schema ? "strict" : "relaxed");
381
+ if (mode === "strict" && !params.schema) {
382
+ throw new Error('schema is required when mode is "strict"');
383
+ }
384
+ const shouldEmbedSchema = params.embedSchemaInPrompt !== false && params.schema;
385
+ let enhancedInput = params.input;
386
+ if (shouldEmbedSchema) {
387
+ const jsonSchema = this.translator.convertZodIfNeeded(params.schema);
388
+ const enhancedText = combineSchemaAndUserPrompt(
389
+ jsonSchema,
390
+ params.input.text || ""
391
+ );
392
+ enhancedInput = {
393
+ ...params.input,
394
+ text: enhancedText
395
+ };
396
+ }
397
+ const messages = this.buildMessages(enhancedInput);
398
+ const requestBody = {
399
+ model: this.config.model,
400
+ messages,
401
+ max_tokens: params.max_tokens || 4096,
402
+ // Set reasonable default to avoid massive token requests
403
+ // Enable usage tracking for OpenRouter cost info
404
+ usage: {
405
+ include: true
406
+ }
407
+ };
408
+ if (mode === "relaxed") {
409
+ requestBody.response_format = {
410
+ type: "json_object"
411
+ };
412
+ if (process.env.DEBUG_PROVIDERS) {
413
+ console.log("[OpenAIProvider] Using relaxed JSON mode (json_object)");
414
+ }
415
+ } else {
416
+ const schema = this.translator.toOpenAISchema(params.schema);
417
+ const fixSchemaForAzure = (obj) => {
418
+ if (obj && typeof obj === "object") {
419
+ if (obj.type === "object" && obj.properties) {
420
+ const allProps = Object.keys(obj.properties);
421
+ obj.required = allProps;
422
+ obj.additionalProperties = false;
423
+ for (const key in obj.properties) {
424
+ obj.properties[key] = fixSchemaForAzure(obj.properties[key]);
425
+ }
426
+ } else if (obj.type === "array" && obj.items) {
427
+ obj.items = fixSchemaForAzure(obj.items);
428
+ }
429
+ }
430
+ return obj;
431
+ };
432
+ fixSchemaForAzure(schema);
433
+ if (process.env.DEBUG_PROVIDERS) {
434
+ console.log("[OpenAIProvider] Using strict JSON mode (json_schema)");
435
+ console.log("[OpenAIProvider] Fixed schema:", JSON.stringify(schema, null, 2));
436
+ }
437
+ requestBody.response_format = {
438
+ type: "json_schema",
439
+ json_schema: {
440
+ name: "extraction",
441
+ strict: true,
442
+ schema
443
+ }
444
+ };
445
+ }
446
+ if (params.reasoning) {
447
+ requestBody.reasoning = this.buildReasoningConfig(params.reasoning);
448
+ }
449
+ if (this.config.via === "openrouter" && mode === "strict") {
450
+ requestBody.provider = {
451
+ require_parameters: true
452
+ // Only route to models supporting json_schema
453
+ };
454
+ }
455
+ const endpoint = this.config.via === "openrouter" ? "https://openrouter.ai/api/v1" : this.config.baseUrl || "https://api.openai.com/v1";
456
+ const headers = {
457
+ "Content-Type": "application/json",
458
+ "Authorization": `Bearer ${this.config.apiKey}`
459
+ };
460
+ if (this.config.via === "openrouter") {
461
+ headers["HTTP-Referer"] = "https://github.com/docloai/sdk";
462
+ headers["X-Title"] = "Doclo SDK";
463
+ }
464
+ const response = await fetchWithTimeout(`${endpoint}/chat/completions`, {
465
+ method: "POST",
466
+ headers,
467
+ body: JSON.stringify(requestBody)
468
+ }, this.limits.REQUEST_TIMEOUT);
469
+ if (!response.ok) {
470
+ const error = await response.text();
471
+ throw new Error(`OpenAI API error (${response.status}): ${error}`);
472
+ }
473
+ const data = await response.json();
474
+ const latencyMs = Date.now() - startTime;
475
+ const content = data.choices?.[0]?.message?.content ?? "{}";
476
+ const parsed = safeJsonParse(content);
477
+ const message = data.choices?.[0]?.message;
478
+ const reasoning = message?.reasoning;
479
+ const reasoning_details = message?.reasoning_details;
480
+ let costUSD;
481
+ if (this.config.via === "openrouter") {
482
+ costUSD = data.usage?.total_cost ?? data.usage?.cost;
483
+ } else {
484
+ costUSD = this.calculateCost(data.usage);
485
+ }
486
+ const baseProvider = extractProviderFromModel(this.config.model, "openai");
487
+ return {
488
+ json: parsed,
489
+ rawText: content,
490
+ metrics: {
491
+ costUSD,
492
+ inputTokens: data.usage?.prompt_tokens,
493
+ outputTokens: data.usage?.completion_tokens,
494
+ latencyMs,
495
+ attemptNumber: 1,
496
+ provider: baseProvider,
497
+ // Base provider (e.g., "openai" from "openai/gpt-4...")
498
+ model: this.config.model
499
+ },
500
+ reasoning,
501
+ reasoning_details
502
+ };
503
+ }
504
+ buildReasoningConfig(reasoning) {
505
+ const config = {};
506
+ if (reasoning.effort) {
507
+ config.effort = reasoning.effort;
508
+ } else if (reasoning.enabled) {
509
+ config.effort = "medium";
510
+ }
511
+ if (reasoning.exclude !== void 0) {
512
+ config.exclude = reasoning.exclude;
513
+ }
514
+ return Object.keys(config).length > 0 ? config : void 0;
515
+ }
516
+ buildMessages(input) {
517
+ const content = [];
518
+ if (input.text) {
519
+ content.push({ type: "text", text: input.text });
520
+ }
521
+ if (input.images && input.images.length > 0) {
522
+ for (const image of input.images) {
523
+ if (image.url) {
524
+ content.push({
525
+ type: "image_url",
526
+ image_url: { url: image.url }
527
+ });
528
+ } else if (image.base64) {
529
+ content.push({
530
+ type: "image_url",
531
+ image_url: {
532
+ url: `data:${image.mimeType};base64,${this.extractBase64(image.base64)}`
533
+ }
534
+ });
535
+ }
536
+ }
537
+ }
538
+ if (input.pdfs && input.pdfs.length > 0) {
539
+ for (const pdf of input.pdfs) {
540
+ let fileData;
541
+ if (pdf.url) {
542
+ fileData = pdf.url;
543
+ } else if (pdf.base64) {
544
+ fileData = `data:application/pdf;base64,${this.extractBase64(pdf.base64)}`;
545
+ } else {
546
+ continue;
547
+ }
548
+ content.push({
549
+ type: "file",
550
+ file: {
551
+ filename: "document.pdf",
552
+ file_data: fileData
553
+ }
554
+ });
555
+ }
556
+ }
557
+ return [{ role: "user", content }];
558
+ }
559
+ /**
560
+ * Extract base64 data from a data URL or return as-is if already raw base64
561
+ */
562
+ extractBase64(input) {
563
+ if (input.startsWith("data:")) {
564
+ const base64Part = input.split(",")[1];
565
+ if (!base64Part) {
566
+ throw new Error(`Invalid data URL format: ${input.substring(0, 50)}`);
567
+ }
568
+ return base64Part;
569
+ }
570
+ return input;
571
+ }
572
+ calculateCost(usage) {
573
+ if (!usage) return void 0;
574
+ const inputCostPer1k = 5e-3;
575
+ const outputCostPer1k = 0.015;
576
+ const inputCost = usage.prompt_tokens / 1e3 * inputCostPer1k;
577
+ const outputCost = usage.completion_tokens / 1e3 * outputCostPer1k;
578
+ return inputCost + outputCost;
579
+ }
580
+ };
581
+
582
+ // src/providers/anthropic.ts
583
+ import { fetchWithTimeout as fetchWithTimeout2, DEFAULT_LIMITS as DEFAULT_LIMITS2, validateUrl, safeJsonParse as safeJsonParse2 } from "@doclo/core/security";
584
+ import { detectMimeTypeFromBase64 } from "@doclo/core";
585
+ function extractProviderFromModel2(model, defaultProvider) {
586
+ const slashIndex = model.indexOf("/");
587
+ return slashIndex > 0 ? model.substring(0, slashIndex) : defaultProvider;
588
+ }
589
+ var AnthropicProvider = class {
590
+ name;
591
+ capabilities = {
592
+ supportsStructuredOutput: true,
593
+ // via tool calling
594
+ supportsStreaming: true,
595
+ supportsImages: true,
596
+ supportsPDFs: true,
597
+ maxPDFPages: 100,
598
+ maxPDFSize: void 0,
599
+ // ~400k tokens with overhead
600
+ maxContextTokens: 2e5
601
+ };
602
+ config;
603
+ translator;
604
+ limits;
605
+ constructor(config) {
606
+ this.config = config;
607
+ const baseProvider = extractProviderFromModel2(config.model, "anthropic");
608
+ this.name = `${baseProvider}:${config.model}`;
609
+ this.translator = new SchemaTranslator();
610
+ this.limits = {
611
+ ...DEFAULT_LIMITS2,
612
+ ...config.limits || {}
613
+ };
614
+ }
615
+ async completeJson(params) {
616
+ const startTime = Date.now();
617
+ const mode = params.mode || (params.schema ? "strict" : "relaxed");
618
+ if (mode === "strict" && !params.schema) {
619
+ throw new Error('schema is required when mode is "strict"');
620
+ }
621
+ const shouldEmbedSchema = params.embedSchemaInPrompt !== false && params.schema;
622
+ let enhancedInput = params.input;
623
+ if (shouldEmbedSchema) {
624
+ const jsonSchema = this.translator.convertZodIfNeeded(params.schema);
625
+ const enhancedText = combineSchemaAndUserPrompt(
626
+ jsonSchema,
627
+ params.input.text || ""
628
+ );
629
+ enhancedInput = {
630
+ ...params.input,
631
+ text: enhancedText
632
+ };
633
+ }
634
+ const messages = await this.buildMessages(enhancedInput);
635
+ const useNewStructuredOutputs = this.supportsNewStructuredOutputs();
636
+ const requestBody = {
637
+ model: this.config.model,
638
+ max_tokens: params.max_tokens || 4096,
639
+ messages
640
+ };
641
+ if (mode === "relaxed") {
642
+ requestBody.messages.push({
643
+ role: "assistant",
644
+ content: "{"
645
+ });
646
+ if (process.env.DEBUG_PROVIDERS) {
647
+ console.log("[AnthropicProvider] Using relaxed JSON mode (prompt + prefilling)");
648
+ }
649
+ } else if (useNewStructuredOutputs) {
650
+ const jsonSchema = this.translator.convertZodIfNeeded(params.schema);
651
+ const fixedSchema = this.fixSchemaForStrictMode(jsonSchema);
652
+ if (process.env.DEBUG_PROVIDERS) {
653
+ console.log("[AnthropicProvider] Original schema:", JSON.stringify(jsonSchema, null, 2));
654
+ console.log("[AnthropicProvider] Fixed schema:", JSON.stringify(fixedSchema, null, 2));
655
+ }
656
+ requestBody.output_format = {
657
+ type: "json_schema",
658
+ schema: fixedSchema
659
+ };
660
+ if (process.env.DEBUG_PROVIDERS) {
661
+ console.log("[AnthropicProvider] Using NEW structured outputs API (strict mode)");
662
+ }
663
+ } else {
664
+ const tool = this.translator.toClaudeToolSchema(params.schema);
665
+ requestBody.tools = [tool];
666
+ requestBody.tool_choice = { type: "tool", name: "extract_data" };
667
+ if (process.env.DEBUG_PROVIDERS) {
668
+ console.log("[AnthropicProvider] Using legacy tool calling approach (strict mode)");
669
+ }
670
+ }
671
+ if (this.config.via !== "openrouter" && params.reasoning) {
672
+ const thinkingConfig = this.buildNativeThinkingConfig(params.reasoning, params.max_tokens);
673
+ if (thinkingConfig) {
674
+ requestBody.thinking = thinkingConfig;
675
+ }
676
+ }
677
+ let response;
678
+ let parsed;
679
+ let inputTokens;
680
+ let outputTokens;
681
+ let costUSD;
682
+ if (this.config.via === "openrouter") {
683
+ const useNewStructuredOutputs2 = this.supportsNewStructuredOutputs();
684
+ const openRouterRequest = this.translateToOpenRouterFormat(messages, params.schema, mode, params.max_tokens, params.reasoning);
685
+ if (process.env.DEBUG_PROVIDERS) {
686
+ console.log("[AnthropicProvider] OpenRouter request body (messages):");
687
+ console.log(JSON.stringify(openRouterRequest.messages, null, 2));
688
+ console.log("[AnthropicProvider] Using new structured outputs:", useNewStructuredOutputs2);
689
+ }
690
+ response = await fetchWithTimeout2("https://openrouter.ai/api/v1/chat/completions", {
691
+ method: "POST",
692
+ headers: {
693
+ "Content-Type": "application/json",
694
+ "Authorization": `Bearer ${this.config.apiKey}`,
695
+ "HTTP-Referer": "https://github.com/docloai/sdk",
696
+ "X-Title": "Doclo SDK"
697
+ },
698
+ body: JSON.stringify(openRouterRequest)
699
+ }, this.limits.REQUEST_TIMEOUT);
700
+ if (!response.ok) {
701
+ const error = await response.text();
702
+ throw new Error(`Anthropic API error (${response.status}): ${error}`);
703
+ }
704
+ const data = await response.json();
705
+ const message = data.choices?.[0]?.message;
706
+ let content = message?.content ?? (useNewStructuredOutputs2 ? "{}" : "}");
707
+ if (!useNewStructuredOutputs2) {
708
+ content = "{" + content;
709
+ }
710
+ const reasoning = message?.reasoning;
711
+ const reasoning_details = message?.reasoning_details;
712
+ content = content.replace(/^```json\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
713
+ content = content.replace(/\*\*/g, "").replace(/\*/g, "");
714
+ const firstBrace = content.indexOf("{");
715
+ if (firstBrace !== -1) {
716
+ let braceCount = 0;
717
+ let jsonEnd = -1;
718
+ for (let i = firstBrace; i < content.length; i++) {
719
+ if (content[i] === "{") braceCount++;
720
+ if (content[i] === "}") braceCount--;
721
+ if (braceCount === 0) {
722
+ jsonEnd = i + 1;
723
+ break;
724
+ }
725
+ }
726
+ if (jsonEnd !== -1) {
727
+ content = content.substring(firstBrace, jsonEnd);
728
+ }
729
+ } else if (!content.startsWith("[")) {
730
+ throw new Error(`Claude did not return JSON. Response: ${content.substring(0, 200)}`);
731
+ }
732
+ content = content.trim();
733
+ parsed = safeJsonParse2(content);
734
+ if (mode === "relaxed" && this.looksLikeUnwrappedProperties(parsed)) {
735
+ parsed = this.wrapAsSchema(parsed);
736
+ }
737
+ inputTokens = data.usage?.prompt_tokens;
738
+ outputTokens = data.usage?.completion_tokens;
739
+ costUSD = data.usage?.total_cost ?? data.usage?.cost;
740
+ const cacheCreationInputTokens = data.usage?.cache_creation_input_tokens;
741
+ const cacheReadInputTokens = data.usage?.cache_read_input_tokens;
742
+ if (process.env.DEBUG_PROVIDERS) {
743
+ console.log("[AnthropicProvider] OpenRouter usage response:", JSON.stringify(data.usage, null, 2));
744
+ console.log("[AnthropicProvider] Extracted costUSD:", costUSD);
745
+ console.log("[AnthropicProvider] Cache creation tokens:", cacheCreationInputTokens);
746
+ console.log("[AnthropicProvider] Cache read tokens:", cacheReadInputTokens);
747
+ }
748
+ const latencyMs = Date.now() - startTime;
749
+ const baseProvider = extractProviderFromModel2(this.config.model, "anthropic");
750
+ return {
751
+ json: parsed,
752
+ rawText: JSON.stringify(parsed),
753
+ metrics: {
754
+ costUSD,
755
+ inputTokens,
756
+ outputTokens,
757
+ latencyMs,
758
+ attemptNumber: 1,
759
+ provider: baseProvider,
760
+ // Base provider (e.g., "anthropic" from "anthropic/claude-...")
761
+ model: this.config.model,
762
+ cacheCreationInputTokens,
763
+ cacheReadInputTokens
764
+ },
765
+ reasoning,
766
+ reasoning_details
767
+ };
768
+ } else {
769
+ const endpoint = this.config.baseUrl || "https://api.anthropic.com/v1";
770
+ const headers = {
771
+ "Content-Type": "application/json",
772
+ "x-api-key": this.config.apiKey,
773
+ "anthropic-version": "2023-06-01"
774
+ };
775
+ if (useNewStructuredOutputs) {
776
+ headers["anthropic-beta"] = "structured-outputs-2025-11-13";
777
+ }
778
+ response = await fetchWithTimeout2(`${endpoint}/messages`, {
779
+ method: "POST",
780
+ headers,
781
+ body: JSON.stringify(requestBody)
782
+ }, this.limits.REQUEST_TIMEOUT);
783
+ if (!response.ok) {
784
+ const error = await response.text();
785
+ throw new Error(`Anthropic API error (${response.status}): ${error}`);
786
+ }
787
+ const data = await response.json();
788
+ if (mode === "relaxed") {
789
+ const textBlock = data.content?.find((block) => block.type === "text");
790
+ if (!textBlock || !textBlock.text) {
791
+ throw new Error("Claude did not return structured output (relaxed mode)");
792
+ }
793
+ let content = "{" + textBlock.text;
794
+ const firstBrace = content.indexOf("{");
795
+ if (firstBrace !== -1) {
796
+ let braceCount = 0;
797
+ let jsonEnd = -1;
798
+ for (let i = firstBrace; i < content.length; i++) {
799
+ if (content[i] === "{") braceCount++;
800
+ if (content[i] === "}") braceCount--;
801
+ if (braceCount === 0) {
802
+ jsonEnd = i + 1;
803
+ break;
804
+ }
805
+ }
806
+ if (jsonEnd !== -1) {
807
+ content = content.substring(firstBrace, jsonEnd);
808
+ }
809
+ }
810
+ parsed = safeJsonParse2(content);
811
+ } else if (useNewStructuredOutputs) {
812
+ const textBlock = data.content?.find((block) => block.type === "text");
813
+ if (!textBlock || !textBlock.text) {
814
+ throw new Error("Claude did not return structured output via new API");
815
+ }
816
+ parsed = safeJsonParse2(textBlock.text);
817
+ } else {
818
+ const toolUseBlock = data.content?.find((block) => block.type === "tool_use");
819
+ if (!toolUseBlock || !toolUseBlock.input) {
820
+ throw new Error("Claude did not return structured output via tool calling");
821
+ }
822
+ parsed = toolUseBlock.input;
823
+ }
824
+ inputTokens = data.usage?.input_tokens;
825
+ outputTokens = data.usage?.output_tokens;
826
+ costUSD = this.calculateCost(data.usage);
827
+ const thinkingBlock = data.content?.find((block) => block.type === "thinking");
828
+ const reasoning = thinkingBlock?.thinking;
829
+ const latencyMs = Date.now() - startTime;
830
+ const baseProvider = extractProviderFromModel2(this.config.model, "anthropic");
831
+ return {
832
+ json: parsed,
833
+ rawText: JSON.stringify(parsed),
834
+ metrics: {
835
+ costUSD,
836
+ inputTokens,
837
+ outputTokens,
838
+ latencyMs,
839
+ attemptNumber: 1,
840
+ provider: baseProvider,
841
+ // Base provider (e.g., "anthropic" from "anthropic/claude-...")
842
+ model: this.config.model
843
+ },
844
+ reasoning,
845
+ reasoning_details: reasoning ? [{
846
+ type: "reasoning.text",
847
+ text: reasoning,
848
+ signature: null,
849
+ id: "thinking-1",
850
+ format: "anthropic-claude-v1"
851
+ }] : void 0
852
+ };
853
+ }
854
+ }
855
+ buildNativeThinkingConfig(reasoning, max_tokens) {
856
+ if (!reasoning.effort && !reasoning.enabled) {
857
+ return void 0;
858
+ }
859
+ const effort = reasoning.effort || "medium";
860
+ const requestMaxTokens = max_tokens || 4096;
861
+ const effortRatios = { low: 0.2, medium: 0.5, high: 0.8 };
862
+ const ratio = effortRatios[effort];
863
+ const budget_tokens = Math.max(1024, Math.min(32e3, Math.floor(requestMaxTokens * ratio)));
864
+ return {
865
+ type: "enabled",
866
+ budget_tokens
867
+ };
868
+ }
869
+ translateToOpenRouterFormat(messages, schema, mode, max_tokens, reasoning) {
870
+ const useNewStructuredOutputs = this.supportsNewStructuredOutputs();
871
+ const systemMessage = {
872
+ role: "system",
873
+ content: mode === "strict" ? "You are a data extraction assistant. You must respond ONLY with valid JSON that matches the provided schema. Do not include any markdown formatting, explanations, or additional text." : "You are a data extraction assistant. You must respond ONLY with valid JSON. Do not include any markdown formatting, explanations, or additional text."
874
+ };
875
+ const messageArray = [systemMessage, ...messages];
876
+ const requestBody = {
877
+ model: this.config.model,
878
+ messages: messageArray,
879
+ // Enable usage tracking for OpenRouter cost info
880
+ usage: {
881
+ include: true
882
+ }
883
+ };
884
+ if (mode === "relaxed") {
885
+ requestBody.response_format = {
886
+ type: "json_object"
887
+ };
888
+ } else {
889
+ const openRouterSchema = this.translator.toClaudeOpenRouterSchema(schema);
890
+ const fixedSchema = this.fixSchemaForStrictMode(openRouterSchema);
891
+ if (process.env.DEBUG_PROVIDERS) {
892
+ console.log("[AnthropicProvider] Original schema:", JSON.stringify(openRouterSchema, null, 2));
893
+ console.log("[AnthropicProvider] Fixed schema:", JSON.stringify(fixedSchema, null, 2));
894
+ }
895
+ if (!useNewStructuredOutputs) {
896
+ messageArray.push({
897
+ role: "assistant",
898
+ content: "{"
899
+ });
900
+ }
901
+ requestBody.response_format = {
902
+ type: "json_schema",
903
+ json_schema: {
904
+ name: "extraction",
905
+ strict: true,
906
+ schema: fixedSchema
907
+ }
908
+ };
909
+ }
910
+ if (reasoning) {
911
+ requestBody.reasoning = this.buildReasoningConfig(reasoning, max_tokens);
912
+ }
913
+ return requestBody;
914
+ }
915
+ buildReasoningConfig(reasoning, max_tokens) {
916
+ const config = {};
917
+ if (reasoning.effort || reasoning.enabled) {
918
+ const effort = reasoning.effort || "medium";
919
+ const requestMaxTokens = max_tokens || 4096;
920
+ const effortRatios = { low: 0.2, medium: 0.5, high: 0.8 };
921
+ const ratio = effortRatios[effort];
922
+ const reasoningBudget = Math.max(1024, Math.min(32e3, Math.floor(requestMaxTokens * ratio)));
923
+ config.max_tokens = reasoningBudget;
924
+ }
925
+ if (reasoning.exclude !== void 0) {
926
+ config.exclude = reasoning.exclude;
927
+ }
928
+ return Object.keys(config).length > 0 ? config : void 0;
929
+ }
930
+ supportsNewStructuredOutputs() {
931
+ const model = this.config.model.toLowerCase();
932
+ const sonnetMatch = model.match(/sonnet[_-](\d+)[._-](\d+)/);
933
+ const opusMatch = model.match(/opus[_-](\d+)[._-](\d+)/);
934
+ if (sonnetMatch) {
935
+ const major = parseInt(sonnetMatch[1], 10);
936
+ const minor = parseInt(sonnetMatch[2], 10);
937
+ const version = major + minor / 10;
938
+ return version >= 4.5;
939
+ }
940
+ if (opusMatch) {
941
+ const major = parseInt(opusMatch[1], 10);
942
+ const minor = parseInt(opusMatch[2], 10);
943
+ const version = major + minor / 10;
944
+ return version >= 4.1;
945
+ }
946
+ return false;
947
+ }
948
+ fixSchemaForStrictMode(schema) {
949
+ const clonedSchema = JSON.parse(JSON.stringify(schema));
950
+ if (!clonedSchema.type) {
951
+ clonedSchema.type = "object";
952
+ }
953
+ const fixRecursive = (obj) => {
954
+ if (!obj || typeof obj !== "object") {
955
+ return obj;
956
+ }
957
+ if (obj.type === "object") {
958
+ obj.additionalProperties = false;
959
+ if (obj.properties) {
960
+ const allProps = Object.keys(obj.properties);
961
+ obj.required = allProps;
962
+ for (const key in obj.properties) {
963
+ obj.properties[key] = fixRecursive(obj.properties[key]);
964
+ }
965
+ }
966
+ }
967
+ if (obj.type === "array" && obj.items) {
968
+ obj.items = fixRecursive(obj.items);
969
+ }
970
+ ["anyOf", "oneOf", "allOf"].forEach((keyword) => {
971
+ if (obj[keyword] && Array.isArray(obj[keyword])) {
972
+ obj[keyword] = obj[keyword].map((s) => fixRecursive(s));
973
+ }
974
+ });
975
+ return obj;
976
+ };
977
+ return fixRecursive(clonedSchema);
978
+ }
979
+ async buildMessages(input) {
980
+ const content = [];
981
+ const hasMedia = input.images && input.images.length > 0 || input.pdfs && input.pdfs.length > 0;
982
+ if (process.env.DEBUG_PROVIDERS) {
983
+ console.log("[AnthropicProvider.buildMessages] Input state:");
984
+ console.log(" hasMedia:", hasMedia);
985
+ console.log(" input.images:", input.images?.length || 0);
986
+ console.log(" input.pdfs:", input.pdfs?.length || 0);
987
+ console.log(" input.text:", input.text ? `"${input.text.substring(0, 50)}..."` : "undefined");
988
+ console.log(" via:", this.config.via);
989
+ }
990
+ if (this.config.via === "openrouter") {
991
+ if (input.images && input.images.length > 0) {
992
+ for (const image of input.images) {
993
+ if (image.url) {
994
+ content.push({
995
+ type: "image_url",
996
+ image_url: { url: image.url }
997
+ });
998
+ } else if (image.base64) {
999
+ const actualMimeType = detectMimeTypeFromBase64(image.base64);
1000
+ if (image.mimeType && image.mimeType !== actualMimeType) {
1001
+ console.warn(
1002
+ `[AnthropicProvider] MIME type mismatch detected: declared "${image.mimeType}", actual "${actualMimeType}". Using detected type "${actualMimeType}" to prevent API errors.`
1003
+ );
1004
+ }
1005
+ content.push({
1006
+ type: "image_url",
1007
+ image_url: {
1008
+ url: `data:${actualMimeType};base64,${this.extractBase64(image.base64)}`
1009
+ }
1010
+ });
1011
+ }
1012
+ }
1013
+ }
1014
+ if (input.pdfs && input.pdfs.length > 0) {
1015
+ for (const pdf of input.pdfs) {
1016
+ let fileData;
1017
+ if (pdf.url) {
1018
+ fileData = pdf.url;
1019
+ } else if (pdf.base64) {
1020
+ const actualMimeType = detectMimeTypeFromBase64(pdf.base64);
1021
+ if (actualMimeType !== "application/pdf") {
1022
+ console.warn(
1023
+ `[AnthropicProvider] PDF MIME type mismatch: expected "application/pdf", detected "${actualMimeType}". Using detected type.`
1024
+ );
1025
+ }
1026
+ fileData = `data:${actualMimeType};base64,${this.extractBase64(pdf.base64)}`;
1027
+ } else {
1028
+ continue;
1029
+ }
1030
+ content.push({
1031
+ type: "file",
1032
+ file: {
1033
+ filename: "document.pdf",
1034
+ file_data: fileData
1035
+ }
1036
+ });
1037
+ }
1038
+ }
1039
+ if (hasMedia) {
1040
+ const textContent = input.text || "Extract the requested information from the document.";
1041
+ if (process.env.DEBUG_PROVIDERS) {
1042
+ console.log("[AnthropicProvider.buildMessages] Adding text block with cache_control");
1043
+ console.log(" textContent:", textContent);
1044
+ }
1045
+ content.push({
1046
+ type: "text",
1047
+ text: textContent,
1048
+ cache_control: { type: "ephemeral" }
1049
+ });
1050
+ } else if (input.text) {
1051
+ content.push({
1052
+ type: "text",
1053
+ text: input.text
1054
+ });
1055
+ }
1056
+ } else {
1057
+ if (input.text) {
1058
+ content.push({ type: "text", text: input.text });
1059
+ }
1060
+ if (input.images && input.images.length > 0) {
1061
+ for (const image of input.images) {
1062
+ if (image.url) {
1063
+ const base64 = await this.urlToBase64(image.url);
1064
+ content.push({
1065
+ type: "image",
1066
+ source: {
1067
+ type: "base64",
1068
+ media_type: image.mimeType,
1069
+ data: base64
1070
+ }
1071
+ });
1072
+ } else if (image.base64) {
1073
+ content.push({
1074
+ type: "image",
1075
+ source: {
1076
+ type: "base64",
1077
+ media_type: image.mimeType,
1078
+ data: this.extractBase64(image.base64)
1079
+ }
1080
+ });
1081
+ }
1082
+ }
1083
+ }
1084
+ if (input.pdfs && input.pdfs.length > 0) {
1085
+ for (const pdf of input.pdfs) {
1086
+ if (pdf.fileId) {
1087
+ content.push({
1088
+ type: "document",
1089
+ source: {
1090
+ type: "file",
1091
+ file_id: pdf.fileId
1092
+ }
1093
+ });
1094
+ } else if (pdf.base64) {
1095
+ content.push({
1096
+ type: "document",
1097
+ source: {
1098
+ type: "base64",
1099
+ media_type: "application/pdf",
1100
+ data: this.extractBase64(pdf.base64)
1101
+ }
1102
+ });
1103
+ } else if (pdf.url) {
1104
+ const base64 = await this.urlToBase64(pdf.url);
1105
+ content.push({
1106
+ type: "document",
1107
+ source: {
1108
+ type: "base64",
1109
+ media_type: "application/pdf",
1110
+ data: base64
1111
+ }
1112
+ });
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ if (process.env.DEBUG_PROVIDERS) {
1118
+ console.log("[AnthropicProvider.buildMessages] Final content array length:", content.length);
1119
+ console.log("[AnthropicProvider.buildMessages] Final content array:", JSON.stringify(content, null, 2));
1120
+ }
1121
+ return [{ role: "user", content }];
1122
+ }
1123
+ async urlToBase64(url) {
1124
+ validateUrl(url);
1125
+ const response = await fetchWithTimeout2(url, {}, this.limits.REQUEST_TIMEOUT);
1126
+ if (!response.ok) {
1127
+ throw new Error(`Failed to fetch URL: ${url}`);
1128
+ }
1129
+ const buffer = await response.arrayBuffer();
1130
+ return Buffer.from(buffer).toString("base64");
1131
+ }
1132
+ /**
1133
+ * Extract base64 data from a data URL or return as-is if already raw base64
1134
+ */
1135
+ extractBase64(input) {
1136
+ if (input.startsWith("data:")) {
1137
+ const base64Part = input.split(",")[1];
1138
+ if (!base64Part) {
1139
+ throw new Error(`Invalid data URL format: ${input.substring(0, 50)}`);
1140
+ }
1141
+ return base64Part;
1142
+ }
1143
+ return input;
1144
+ }
1145
+ calculateCost(usage) {
1146
+ if (!usage) return void 0;
1147
+ const inputCostPer1k = 3e-3;
1148
+ const outputCostPer1k = 0.015;
1149
+ const inputCost = usage.input_tokens / 1e3 * inputCostPer1k;
1150
+ const outputCost = usage.output_tokens / 1e3 * outputCostPer1k;
1151
+ return inputCost + outputCost;
1152
+ }
1153
+ /**
1154
+ * Detect if a parsed JSON object looks like unwrapped JSON Schema properties
1155
+ * (e.g., missing the root "type": "object" and "properties": {...} wrapper)
1156
+ */
1157
+ looksLikeUnwrappedProperties(obj) {
1158
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
1159
+ return false;
1160
+ }
1161
+ if (obj.type === "object" && obj.properties) {
1162
+ return false;
1163
+ }
1164
+ const keys = Object.keys(obj);
1165
+ if (keys.length === 0) {
1166
+ return false;
1167
+ }
1168
+ let schemaPropertyCount = 0;
1169
+ for (const key of keys) {
1170
+ const value = obj[key];
1171
+ if (value && typeof value === "object" && "type" in value) {
1172
+ schemaPropertyCount++;
1173
+ }
1174
+ }
1175
+ return schemaPropertyCount / keys.length > 0.5;
1176
+ }
1177
+ /**
1178
+ * Wrap unwrapped properties into a proper JSON Schema structure
1179
+ */
1180
+ wrapAsSchema(unwrapped) {
1181
+ const required = [];
1182
+ for (const [key, value] of Object.entries(unwrapped)) {
1183
+ if (value && typeof value === "object") {
1184
+ const propDef = value;
1185
+ if (!propDef.type || (propDef.type === "string" || propDef.type === "number" || propDef.type === "boolean" || propDef.type === "integer")) {
1186
+ required.push(key);
1187
+ } else if (propDef.type === "object" || propDef.type === "array") {
1188
+ required.push(key);
1189
+ }
1190
+ }
1191
+ }
1192
+ return {
1193
+ type: "object",
1194
+ properties: unwrapped,
1195
+ required
1196
+ };
1197
+ }
1198
+ };
1199
+
1200
+ // src/providers/google.ts
1201
+ import { fetchWithTimeout as fetchWithTimeout3, validateUrl as validateUrl2, DEFAULT_LIMITS as DEFAULT_LIMITS3, safeJsonParse as safeJsonParse3 } from "@doclo/core/security";
1202
+ function extractProviderFromModel3(model, defaultProvider) {
1203
+ const slashIndex = model.indexOf("/");
1204
+ return slashIndex > 0 ? model.substring(0, slashIndex) : defaultProvider;
1205
+ }
1206
+ var GoogleProvider = class {
1207
+ name;
1208
+ capabilities = {
1209
+ supportsStructuredOutput: true,
1210
+ supportsStreaming: true,
1211
+ supportsImages: true,
1212
+ supportsPDFs: true,
1213
+ maxPDFPages: 1e3,
1214
+ // 1 page = 1 image
1215
+ maxPDFSize: 50,
1216
+ maxContextTokens: 1e6
1217
+ // 1M tokens
1218
+ };
1219
+ config;
1220
+ translator;
1221
+ limits;
1222
+ constructor(config) {
1223
+ this.config = config;
1224
+ const baseProvider = extractProviderFromModel3(config.model, "google");
1225
+ this.name = `${baseProvider}:${config.model}`;
1226
+ this.translator = new SchemaTranslator();
1227
+ this.limits = {
1228
+ ...DEFAULT_LIMITS3,
1229
+ ...config.limits || {}
1230
+ };
1231
+ if (process.env.DEBUG_PROVIDERS) {
1232
+ console.log("[GoogleProvider] Config:", JSON.stringify({
1233
+ provider: config.provider,
1234
+ model: config.model,
1235
+ via: config.via,
1236
+ hasApiKey: !!config.apiKey
1237
+ }));
1238
+ }
1239
+ }
1240
+ async completeJson(params) {
1241
+ const startTime = Date.now();
1242
+ const mode = params.mode || (params.schema ? "strict" : "relaxed");
1243
+ if (mode === "strict" && !params.schema) {
1244
+ throw new Error('schema is required when mode is "strict"');
1245
+ }
1246
+ const shouldEmbedSchema = params.embedSchemaInPrompt !== false && params.schema;
1247
+ let enhancedInput = params.input;
1248
+ if (shouldEmbedSchema) {
1249
+ const jsonSchema = this.translator.convertZodIfNeeded(params.schema);
1250
+ const enhancedText = combineSchemaAndUserPrompt(
1251
+ jsonSchema,
1252
+ params.input.text || ""
1253
+ );
1254
+ enhancedInput = {
1255
+ ...params.input,
1256
+ text: enhancedText
1257
+ };
1258
+ }
1259
+ const contents = await this.buildContents(enhancedInput);
1260
+ const requestBody = {
1261
+ contents,
1262
+ generationConfig: {
1263
+ // Google's native responseSchema has strict validation issues with complex schemas.
1264
+ // Use JSON mode without responseSchema - schema is already in the prompt via combineSchemaAndUserPrompt.
1265
+ // See: https://ubaidullahmomer.medium.com/why-google-geminis-response-schema-isn-t-ready-for-complex-json-46f35c3aaaea
1266
+ responseMimeType: "application/json"
1267
+ }
1268
+ };
1269
+ if (process.env.DEBUG_PROVIDERS) {
1270
+ console.log(`[GoogleProvider] Using ${mode} JSON mode (schema in prompt, no responseSchema)`);
1271
+ }
1272
+ if (this.config.via !== "openrouter" && params.reasoning) {
1273
+ const thinkingConfig = this.buildNativeThinkingConfig(params.reasoning, params.max_tokens);
1274
+ if (thinkingConfig) {
1275
+ requestBody.generationConfig.thinking_config = thinkingConfig;
1276
+ }
1277
+ }
1278
+ let response;
1279
+ if (process.env.DEBUG_PROVIDERS) {
1280
+ console.log("[GoogleProvider] Using via:", this.config.via, "Checking:", this.config.via === "openrouter");
1281
+ }
1282
+ if (this.config.via === "openrouter") {
1283
+ const openRouterRequest = this.translateToOpenRouterFormat(contents, mode, params.max_tokens, params.reasoning);
1284
+ response = await fetchWithTimeout3("https://openrouter.ai/api/v1/chat/completions", {
1285
+ method: "POST",
1286
+ headers: {
1287
+ "Content-Type": "application/json",
1288
+ "Authorization": `Bearer ${this.config.apiKey}`,
1289
+ "HTTP-Referer": "https://github.com/docloai/sdk",
1290
+ "X-Title": "Doclo SDK"
1291
+ },
1292
+ body: JSON.stringify(openRouterRequest)
1293
+ }, this.limits.REQUEST_TIMEOUT);
1294
+ } else {
1295
+ const endpoint = this.config.baseUrl || `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model}:generateContent`;
1296
+ validateUrl2(endpoint);
1297
+ response = await fetchWithTimeout3(endpoint, {
1298
+ method: "POST",
1299
+ headers: {
1300
+ "Content-Type": "application/json",
1301
+ "x-goog-api-key": this.config.apiKey
1302
+ // Use header instead of query param
1303
+ },
1304
+ body: JSON.stringify(requestBody)
1305
+ }, this.limits.REQUEST_TIMEOUT);
1306
+ }
1307
+ if (!response.ok) {
1308
+ const error = await response.text();
1309
+ throw new Error(`Google API error (${response.status}): ${error}`);
1310
+ }
1311
+ const data = await response.json();
1312
+ const latencyMs = Date.now() - startTime;
1313
+ let content;
1314
+ let inputTokens;
1315
+ let outputTokens;
1316
+ let costUSD;
1317
+ if (this.config.via === "openrouter") {
1318
+ const message = data.choices?.[0]?.message;
1319
+ content = message?.content?.trim() || "{}";
1320
+ inputTokens = data.usage?.prompt_tokens;
1321
+ outputTokens = data.usage?.completion_tokens;
1322
+ costUSD = data.usage?.total_cost ?? data.usage?.cost;
1323
+ const reasoning = message?.reasoning;
1324
+ const reasoning_details = message?.reasoning_details;
1325
+ content = content.replace(/^```json\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
1326
+ const parsed = safeJsonParse3(content);
1327
+ const baseProvider = extractProviderFromModel3(this.config.model, "google");
1328
+ return {
1329
+ json: parsed,
1330
+ rawText: content,
1331
+ metrics: {
1332
+ costUSD,
1333
+ inputTokens,
1334
+ outputTokens,
1335
+ latencyMs,
1336
+ attemptNumber: 1,
1337
+ provider: baseProvider,
1338
+ // Base provider (e.g., "google" from "google/gemini-...")
1339
+ model: this.config.model
1340
+ },
1341
+ reasoning,
1342
+ reasoning_details
1343
+ };
1344
+ } else {
1345
+ const candidate = data.candidates?.[0];
1346
+ content = candidate?.content?.parts?.[0]?.text?.trim() || "{}";
1347
+ inputTokens = data.usageMetadata?.promptTokenCount;
1348
+ outputTokens = data.usageMetadata?.candidatesTokenCount;
1349
+ costUSD = this.calculateCost(data.usageMetadata);
1350
+ const thinkingPart = candidate?.content?.parts?.find((part) => part.thought === true);
1351
+ const reasoning = thinkingPart?.text;
1352
+ const parsed = safeJsonParse3(content);
1353
+ const baseProvider = extractProviderFromModel3(this.config.model, "google");
1354
+ return {
1355
+ json: parsed,
1356
+ rawText: content,
1357
+ metrics: {
1358
+ costUSD,
1359
+ inputTokens,
1360
+ outputTokens,
1361
+ latencyMs,
1362
+ attemptNumber: 1,
1363
+ provider: baseProvider,
1364
+ // Base provider (e.g., "google" from "google/gemini-...")
1365
+ model: this.config.model
1366
+ },
1367
+ reasoning,
1368
+ reasoning_details: reasoning ? [{
1369
+ type: "reasoning.text",
1370
+ text: reasoning,
1371
+ signature: null,
1372
+ id: "thinking-1",
1373
+ format: "google-gemini-v1"
1374
+ }] : void 0
1375
+ };
1376
+ }
1377
+ }
1378
+ buildNativeThinkingConfig(reasoning, max_tokens) {
1379
+ if (!reasoning.effort && !reasoning.enabled) {
1380
+ return void 0;
1381
+ }
1382
+ const effort = reasoning.effort || "medium";
1383
+ const requestMaxTokens = max_tokens || 8192;
1384
+ const effortRatios = { low: 0.2, medium: 0.5, high: 0.8 };
1385
+ const ratio = effortRatios[effort];
1386
+ const thinking_budget = Math.min(24576, Math.floor(requestMaxTokens * ratio));
1387
+ return {
1388
+ thinking_budget
1389
+ };
1390
+ }
1391
+ translateToOpenRouterFormat(contents, mode, max_tokens, reasoning) {
1392
+ const messages = [];
1393
+ for (const content of contents) {
1394
+ if (content.role === "user") {
1395
+ const messageContent = [];
1396
+ for (const part of content.parts) {
1397
+ if (!part) continue;
1398
+ if (part.text) {
1399
+ messageContent.push({ type: "text", text: part.text });
1400
+ } else if (part.inlineData) {
1401
+ if (part.inlineData.mimeType === "application/pdf") {
1402
+ messageContent.push({
1403
+ type: "file",
1404
+ file: {
1405
+ filename: "document.pdf",
1406
+ file_data: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
1407
+ }
1408
+ });
1409
+ } else {
1410
+ messageContent.push({
1411
+ type: "image_url",
1412
+ image_url: {
1413
+ url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
1414
+ }
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ messages.push({
1420
+ role: "user",
1421
+ content: messageContent.length === 1 && messageContent[0].type === "text" ? messageContent[0].text : messageContent
1422
+ });
1423
+ }
1424
+ }
1425
+ const requestBody = {
1426
+ model: this.config.model,
1427
+ messages,
1428
+ // Enable usage tracking for OpenRouter cost info
1429
+ usage: {
1430
+ include: true
1431
+ },
1432
+ // Both relaxed and strict modes use json_object for Google via OpenRouter
1433
+ // Schema is already in the prompt via combineSchemaAndUserPrompt
1434
+ response_format: {
1435
+ type: "json_object"
1436
+ }
1437
+ };
1438
+ if (reasoning) {
1439
+ requestBody.reasoning = this.buildReasoningConfig(reasoning, max_tokens);
1440
+ }
1441
+ return requestBody;
1442
+ }
1443
+ buildReasoningConfig(reasoning, max_tokens) {
1444
+ const config = {};
1445
+ if (reasoning.effort || reasoning.enabled) {
1446
+ const effort = reasoning.effort || "medium";
1447
+ const requestMaxTokens = max_tokens || 8192;
1448
+ const effortRatios = { low: 0.2, medium: 0.5, high: 0.8 };
1449
+ const ratio = effortRatios[effort];
1450
+ const reasoningBudget = Math.floor(requestMaxTokens * ratio);
1451
+ config.max_tokens = reasoningBudget;
1452
+ }
1453
+ if (reasoning.exclude !== void 0) {
1454
+ config.exclude = reasoning.exclude;
1455
+ }
1456
+ return Object.keys(config).length > 0 ? config : void 0;
1457
+ }
1458
+ async buildContents(input) {
1459
+ const parts = [];
1460
+ if (input.text) {
1461
+ parts.push({ text: input.text });
1462
+ }
1463
+ if (input.images && input.images.length > 0) {
1464
+ for (const image of input.images) {
1465
+ if (image.url) {
1466
+ const base64 = await this.urlToBase64(image.url);
1467
+ parts.push({
1468
+ inlineData: {
1469
+ mimeType: image.mimeType,
1470
+ data: base64
1471
+ }
1472
+ });
1473
+ } else if (image.base64) {
1474
+ parts.push({
1475
+ inlineData: {
1476
+ mimeType: image.mimeType,
1477
+ data: this.extractBase64(image.base64)
1478
+ }
1479
+ });
1480
+ }
1481
+ }
1482
+ }
1483
+ if (input.pdfs && input.pdfs.length > 0) {
1484
+ for (const pdf of input.pdfs) {
1485
+ if (pdf.fileId) {
1486
+ parts.push({
1487
+ fileData: {
1488
+ fileUri: `https://generativelanguage.googleapis.com/v1beta/files/${pdf.fileId}`,
1489
+ mimeType: "application/pdf"
1490
+ }
1491
+ });
1492
+ } else if (pdf.base64) {
1493
+ parts.push({
1494
+ inlineData: {
1495
+ mimeType: "application/pdf",
1496
+ data: this.extractBase64(pdf.base64)
1497
+ }
1498
+ });
1499
+ } else if (pdf.url) {
1500
+ const base64 = await this.urlToBase64(pdf.url);
1501
+ parts.push({
1502
+ inlineData: {
1503
+ mimeType: "application/pdf",
1504
+ data: base64
1505
+ }
1506
+ });
1507
+ }
1508
+ }
1509
+ }
1510
+ return [{ role: "user", parts }];
1511
+ }
1512
+ async urlToBase64(url) {
1513
+ validateUrl2(url);
1514
+ const response = await fetchWithTimeout3(url, {}, this.limits.REQUEST_TIMEOUT);
1515
+ if (!response.ok) {
1516
+ throw new Error(`Failed to fetch URL: ${url}`);
1517
+ }
1518
+ const buffer = await response.arrayBuffer();
1519
+ return Buffer.from(buffer).toString("base64");
1520
+ }
1521
+ /**
1522
+ * Extract base64 data from a data URL or return as-is if already raw base64
1523
+ */
1524
+ extractBase64(input) {
1525
+ if (input.startsWith("data:")) {
1526
+ const base64Part = input.split(",")[1];
1527
+ if (!base64Part) {
1528
+ throw new Error(`Invalid data URL format: ${input.substring(0, 50)}`);
1529
+ }
1530
+ return base64Part;
1531
+ }
1532
+ return input;
1533
+ }
1534
+ calculateCost(usage) {
1535
+ if (!usage) return void 0;
1536
+ const inputCostPer1k = 25e-5;
1537
+ const outputCostPer1k = 1e-3;
1538
+ const inputCost = usage.promptTokenCount / 1e3 * inputCostPer1k;
1539
+ const outputCost = usage.candidatesTokenCount / 1e3 * outputCostPer1k;
1540
+ return inputCost + outputCost;
1541
+ }
1542
+ };
1543
+
1544
+ // src/providers/xai.ts
1545
+ import { fetchWithTimeout as fetchWithTimeout4, DEFAULT_LIMITS as DEFAULT_LIMITS4, safeJsonParse as safeJsonParse4 } from "@doclo/core/security";
1546
+ function extractProviderFromModel4(model, defaultProvider) {
1547
+ const slashIndex = model.indexOf("/");
1548
+ return slashIndex > 0 ? model.substring(0, slashIndex) : defaultProvider;
1549
+ }
1550
+ var XAIProvider = class {
1551
+ name;
1552
+ capabilities = {
1553
+ supportsStructuredOutput: true,
1554
+ supportsStreaming: false,
1555
+ // Not with structured outputs
1556
+ supportsImages: true,
1557
+ supportsPDFs: true,
1558
+ // via page-by-page images
1559
+ maxPDFPages: void 0,
1560
+ maxPDFSize: void 0,
1561
+ maxContextTokens: 131072
1562
+ };
1563
+ config;
1564
+ translator;
1565
+ limits;
1566
+ constructor(config) {
1567
+ this.config = config;
1568
+ const baseProvider = extractProviderFromModel4(config.model, "xai");
1569
+ this.name = `${baseProvider}:${config.model}`;
1570
+ this.translator = new SchemaTranslator();
1571
+ this.limits = {
1572
+ ...DEFAULT_LIMITS4,
1573
+ ...config.limits || {}
1574
+ };
1575
+ }
1576
+ async completeJson(params) {
1577
+ const startTime = Date.now();
1578
+ const mode = params.mode || (params.schema ? "strict" : "relaxed");
1579
+ if (mode === "strict" && !params.schema) {
1580
+ throw new Error('schema is required when mode is "strict"');
1581
+ }
1582
+ const shouldEmbedSchema = params.embedSchemaInPrompt !== false && params.schema;
1583
+ let enhancedInput = params.input;
1584
+ if (shouldEmbedSchema) {
1585
+ const jsonSchema = this.translator.convertZodIfNeeded(params.schema);
1586
+ const enhancedText = combineSchemaAndUserPrompt(
1587
+ jsonSchema,
1588
+ params.input.text || ""
1589
+ );
1590
+ enhancedInput = {
1591
+ ...params.input,
1592
+ text: enhancedText
1593
+ };
1594
+ }
1595
+ const messages = await this.buildMessages(enhancedInput);
1596
+ const requestBody = {
1597
+ model: this.config.model,
1598
+ messages,
1599
+ max_tokens: params.max_tokens || 4096,
1600
+ stream: false,
1601
+ // Structured output doesn't support streaming
1602
+ // Enable usage tracking for OpenRouter cost info
1603
+ usage: {
1604
+ include: true
1605
+ }
1606
+ };
1607
+ if (mode === "relaxed") {
1608
+ requestBody.response_format = {
1609
+ type: "json_object"
1610
+ };
1611
+ if (process.env.DEBUG_PROVIDERS) {
1612
+ console.log("[XAIProvider] Using relaxed JSON mode (json_object)");
1613
+ }
1614
+ } else {
1615
+ const schema = this.translator.toOpenAISchema(params.schema);
1616
+ const fixSchemaRecursive = (obj) => {
1617
+ if (obj && typeof obj === "object") {
1618
+ if (obj.type === "object" && obj.properties) {
1619
+ const allProps = Object.keys(obj.properties);
1620
+ obj.required = allProps;
1621
+ obj.additionalProperties = false;
1622
+ for (const key in obj.properties) {
1623
+ obj.properties[key] = fixSchemaRecursive(obj.properties[key]);
1624
+ }
1625
+ } else if (obj.type === "array" && obj.items) {
1626
+ obj.items = fixSchemaRecursive(obj.items);
1627
+ }
1628
+ }
1629
+ return obj;
1630
+ };
1631
+ fixSchemaRecursive(schema);
1632
+ if (process.env.DEBUG_PROVIDERS) {
1633
+ console.log("[XAIProvider] Using strict JSON mode (json_schema)");
1634
+ }
1635
+ requestBody.response_format = {
1636
+ type: "json_schema",
1637
+ json_schema: {
1638
+ name: "extraction",
1639
+ schema
1640
+ }
1641
+ };
1642
+ }
1643
+ if (params.reasoning) {
1644
+ requestBody.reasoning = this.buildReasoningConfig(params.reasoning);
1645
+ }
1646
+ if (this.config.via === "openrouter" && mode === "strict") {
1647
+ requestBody.provider = {
1648
+ require_parameters: true
1649
+ // Only route to models supporting json_schema
1650
+ };
1651
+ }
1652
+ const endpoint = this.config.via === "openrouter" ? "https://openrouter.ai/api/v1" : this.config.baseUrl || "https://api.x.ai/v1";
1653
+ const headers = {
1654
+ "Content-Type": "application/json",
1655
+ "Authorization": `Bearer ${this.config.apiKey}`
1656
+ };
1657
+ if (this.config.via === "openrouter") {
1658
+ headers["HTTP-Referer"] = "https://github.com/docloai/sdk";
1659
+ headers["X-Title"] = "Doclo SDK";
1660
+ }
1661
+ const response = await fetchWithTimeout4(`${endpoint}/chat/completions`, {
1662
+ method: "POST",
1663
+ headers,
1664
+ body: JSON.stringify(requestBody)
1665
+ }, this.limits.REQUEST_TIMEOUT);
1666
+ if (!response.ok) {
1667
+ const error = await response.text();
1668
+ throw new Error(`xAI API error (${response.status}): ${error}`);
1669
+ }
1670
+ const data = await response.json();
1671
+ const latencyMs = Date.now() - startTime;
1672
+ const message = data.choices?.[0]?.message;
1673
+ const content = message?.content ?? "{}";
1674
+ const parsed = safeJsonParse4(content);
1675
+ const reasoning = message?.reasoning;
1676
+ const reasoning_details = message?.reasoning_details;
1677
+ let costUSD;
1678
+ if (this.config.via === "openrouter") {
1679
+ costUSD = data.usage?.total_cost ?? data.usage?.cost;
1680
+ } else {
1681
+ costUSD = this.calculateCost(data.usage);
1682
+ }
1683
+ const baseProvider = extractProviderFromModel4(this.config.model, "xai");
1684
+ return {
1685
+ json: parsed,
1686
+ rawText: content,
1687
+ metrics: {
1688
+ costUSD,
1689
+ inputTokens: data.usage?.prompt_tokens,
1690
+ outputTokens: data.usage?.completion_tokens,
1691
+ latencyMs,
1692
+ attemptNumber: 1,
1693
+ provider: baseProvider,
1694
+ // Base provider (e.g., "x-ai" from "x-ai/grok-...")
1695
+ model: this.config.model
1696
+ },
1697
+ reasoning,
1698
+ reasoning_details
1699
+ };
1700
+ }
1701
+ buildReasoningConfig(reasoning) {
1702
+ const config = {};
1703
+ if (reasoning.effort) {
1704
+ config.effort = reasoning.effort;
1705
+ } else if (reasoning.enabled) {
1706
+ config.effort = "medium";
1707
+ }
1708
+ if (reasoning.exclude !== void 0) {
1709
+ config.exclude = reasoning.exclude;
1710
+ }
1711
+ return Object.keys(config).length > 0 ? config : void 0;
1712
+ }
1713
+ async buildMessages(input) {
1714
+ const content = [];
1715
+ if (input.text) {
1716
+ content.push({ type: "text", text: input.text });
1717
+ }
1718
+ if (input.images && input.images.length > 0) {
1719
+ for (const image of input.images) {
1720
+ if (image.url) {
1721
+ content.push({
1722
+ type: "image_url",
1723
+ image_url: { url: image.url }
1724
+ });
1725
+ } else if (image.base64) {
1726
+ content.push({
1727
+ type: "image_url",
1728
+ image_url: {
1729
+ url: `data:${image.mimeType};base64,${this.extractBase64(image.base64)}`
1730
+ }
1731
+ });
1732
+ }
1733
+ }
1734
+ }
1735
+ if (input.pdfs && input.pdfs.length > 0) {
1736
+ for (const pdf of input.pdfs) {
1737
+ let fileData;
1738
+ if (pdf.url) {
1739
+ fileData = pdf.url;
1740
+ } else if (pdf.base64) {
1741
+ fileData = `data:application/pdf;base64,${this.extractBase64(pdf.base64)}`;
1742
+ } else {
1743
+ continue;
1744
+ }
1745
+ content.push({
1746
+ type: "file",
1747
+ file: {
1748
+ filename: "document.pdf",
1749
+ file_data: fileData
1750
+ }
1751
+ });
1752
+ }
1753
+ }
1754
+ return [{ role: "user", content }];
1755
+ }
1756
+ /**
1757
+ * Extract base64 data from a data URL or return as-is if already raw base64
1758
+ */
1759
+ extractBase64(input) {
1760
+ if (input.startsWith("data:")) {
1761
+ const base64Part = input.split(",")[1];
1762
+ if (!base64Part) {
1763
+ throw new Error(`Invalid data URL format: ${input.substring(0, 50)}`);
1764
+ }
1765
+ return base64Part;
1766
+ }
1767
+ return input;
1768
+ }
1769
+ calculateCost(usage) {
1770
+ if (!usage) return void 0;
1771
+ const inputCostPer1k = 5e-3;
1772
+ const outputCostPer1k = 0.015;
1773
+ const inputCost = usage.prompt_tokens / 1e3 * inputCostPer1k;
1774
+ const outputCost = usage.completion_tokens / 1e3 * outputCostPer1k;
1775
+ return inputCost + outputCost;
1776
+ }
1777
+ };
1778
+
1779
+ // src/fallback-manager.ts
1780
+ import {
1781
+ executeHook,
1782
+ createLogger
1783
+ } from "@doclo/core/observability";
1784
+ var FallbackManager = class {
1785
+ config;
1786
+ circuitBreakers;
1787
+ constructor(config) {
1788
+ this.config = config;
1789
+ this.circuitBreakers = /* @__PURE__ */ new Map();
1790
+ }
1791
+ async executeWithFallback(input, schema, max_tokens, reasoning, mode, observability) {
1792
+ const errors = [];
1793
+ const logger = createLogger({
1794
+ observability: observability?.config,
1795
+ flowId: observability?.flowId,
1796
+ executionId: observability?.executionId,
1797
+ stepId: observability?.stepId,
1798
+ traceContext: observability?.traceContext,
1799
+ metadata: observability?.metadata
1800
+ });
1801
+ for (const [providerIndex, providerConfig] of this.config.providers.entries()) {
1802
+ const providerKey = `${providerConfig.provider}:${providerConfig.model}`;
1803
+ if (this.isCircuitOpen(providerKey)) {
1804
+ logger.warn(`Circuit breaker open for ${providerKey}, skipping`);
1805
+ if (observability?.config && observability.traceContext && observability.executionId) {
1806
+ const cbState = this.circuitBreakers.get(providerKey);
1807
+ const circuitBreakerContext = {
1808
+ flowId: observability.flowId ?? "flow",
1809
+ executionId: observability.executionId,
1810
+ timestamp: Date.now(),
1811
+ provider: providerConfig.provider,
1812
+ model: providerConfig.model,
1813
+ failureCount: cbState?.consecutiveFailures ?? 0,
1814
+ threshold: this.config.circuitBreakerThreshold ?? 5,
1815
+ cooldownMs: 6e4,
1816
+ // Default cooldown
1817
+ metadata: observability.metadata,
1818
+ traceContext: observability.traceContext
1819
+ };
1820
+ await executeHook(observability.config.onCircuitBreakerTriggered, {
1821
+ hookName: "onCircuitBreakerTriggered",
1822
+ config: observability.config,
1823
+ context: circuitBreakerContext
1824
+ });
1825
+ }
1826
+ continue;
1827
+ }
1828
+ const provider = this.createProvider(providerConfig);
1829
+ const maxRetriesForProvider = providerIndex === 0 ? this.config.primaryMaxRetries ?? this.config.maxRetries : this.config.maxRetries;
1830
+ let lastError = null;
1831
+ for (let attempt = 1; attempt <= maxRetriesForProvider; attempt++) {
1832
+ try {
1833
+ logger.info(`Attempting ${providerKey} (attempt ${attempt}/${maxRetriesForProvider})`);
1834
+ const requestStartTime = Date.now();
1835
+ if (observability?.config && observability.traceContext && observability.executionId) {
1836
+ const providerRequestContext = {
1837
+ flowId: observability.flowId ?? "flow",
1838
+ executionId: observability.executionId,
1839
+ stepId: observability.stepId,
1840
+ timestamp: requestStartTime,
1841
+ provider: providerConfig.provider,
1842
+ model: providerConfig.model,
1843
+ input,
1844
+ schema,
1845
+ attemptNumber: attempt,
1846
+ maxAttempts: maxRetriesForProvider,
1847
+ metadata: observability.metadata,
1848
+ traceContext: observability.traceContext
1849
+ };
1850
+ await executeHook(observability.config.onProviderRequest, {
1851
+ hookName: "onProviderRequest",
1852
+ config: observability.config,
1853
+ context: providerRequestContext
1854
+ });
1855
+ }
1856
+ const response = await provider.completeJson({ input, schema, max_tokens, reasoning, mode });
1857
+ if (this.validateResponse(response)) {
1858
+ this.recordSuccess(providerKey);
1859
+ if (observability?.config && observability.traceContext && observability.executionId) {
1860
+ const providerResponseContext = {
1861
+ flowId: observability.flowId ?? "flow",
1862
+ executionId: observability.executionId,
1863
+ stepId: observability.stepId,
1864
+ timestamp: Date.now(),
1865
+ startTime: requestStartTime,
1866
+ duration: Date.now() - requestStartTime,
1867
+ provider: providerConfig.provider,
1868
+ model: providerConfig.model,
1869
+ modelUsed: response.metrics.model,
1870
+ output: response.json,
1871
+ usage: {
1872
+ inputTokens: response.metrics.inputTokens ?? 0,
1873
+ outputTokens: response.metrics.outputTokens ?? 0,
1874
+ totalTokens: (response.metrics.inputTokens ?? 0) + (response.metrics.outputTokens ?? 0),
1875
+ cacheCreationInputTokens: response.metrics.cacheCreationInputTokens,
1876
+ cacheReadInputTokens: response.metrics.cacheReadInputTokens
1877
+ },
1878
+ cost: response.metrics.costUSD ?? 0,
1879
+ finishReason: response.metrics.finishReason,
1880
+ attemptNumber: attempt,
1881
+ metadata: observability.metadata,
1882
+ traceContext: observability.traceContext
1883
+ };
1884
+ await executeHook(observability.config.onProviderResponse, {
1885
+ hookName: "onProviderResponse",
1886
+ config: observability.config,
1887
+ context: providerResponseContext
1888
+ });
1889
+ }
1890
+ return {
1891
+ ...response,
1892
+ metrics: {
1893
+ ...response.metrics,
1894
+ attemptNumber: attempt
1895
+ }
1896
+ };
1897
+ } else {
1898
+ throw new Error("Response validation failed: incomplete or invalid data");
1899
+ }
1900
+ } catch (error) {
1901
+ lastError = error;
1902
+ logger.error(`${providerKey} attempt ${attempt} failed`, lastError, { providerKey, attempt });
1903
+ if (!this.isRetryable(lastError) || attempt === maxRetriesForProvider) {
1904
+ break;
1905
+ }
1906
+ const retryDelay = this.calculateDelay(attempt);
1907
+ if (observability?.config && observability.traceContext && observability.executionId) {
1908
+ const providerRetryContext = {
1909
+ flowId: observability.flowId ?? "flow",
1910
+ executionId: observability.executionId,
1911
+ stepId: observability.stepId,
1912
+ timestamp: Date.now(),
1913
+ provider: providerConfig.provider,
1914
+ model: providerConfig.model,
1915
+ attemptNumber: attempt,
1916
+ maxAttempts: maxRetriesForProvider,
1917
+ error: lastError,
1918
+ nextRetryDelay: retryDelay,
1919
+ metadata: observability.metadata,
1920
+ traceContext: observability.traceContext
1921
+ };
1922
+ await executeHook(observability.config.onProviderRetry, {
1923
+ hookName: "onProviderRetry",
1924
+ config: observability.config,
1925
+ context: providerRetryContext
1926
+ });
1927
+ }
1928
+ await this.sleep(retryDelay);
1929
+ }
1930
+ }
1931
+ if (lastError) {
1932
+ errors.push({ provider: providerKey, error: lastError });
1933
+ this.recordFailure(providerKey);
1934
+ }
1935
+ }
1936
+ throw new Error(
1937
+ `All providers failed:
1938
+ ${errors.map((e) => ` ${e.provider}: ${e.error.message}`).join("\n")}`
1939
+ );
1940
+ }
1941
+ createProvider(config) {
1942
+ return createProviderFromRegistry(config);
1943
+ }
1944
+ validateResponse(response) {
1945
+ if (!response.json || typeof response.json !== "object") {
1946
+ return false;
1947
+ }
1948
+ return true;
1949
+ }
1950
+ isRetryable(error) {
1951
+ const message = error.message.toLowerCase();
1952
+ const retryablePatterns = [
1953
+ "408",
1954
+ "429",
1955
+ "500",
1956
+ "502",
1957
+ "503",
1958
+ "504",
1959
+ "timeout",
1960
+ "rate limit",
1961
+ "overloaded"
1962
+ ];
1963
+ return retryablePatterns.some(
1964
+ (pattern) => message.includes(pattern)
1965
+ );
1966
+ }
1967
+ calculateDelay(attempt) {
1968
+ if (!this.config.useExponentialBackoff) {
1969
+ return this.config.retryDelay;
1970
+ }
1971
+ const exponentialDelay = this.config.retryDelay * Math.pow(2, attempt - 1);
1972
+ const jitter = Math.random() * 1e3;
1973
+ return Math.min(exponentialDelay + jitter, 3e4);
1974
+ }
1975
+ sleep(ms) {
1976
+ return new Promise((resolve) => setTimeout(resolve, ms));
1977
+ }
1978
+ // Circuit breaker logic
1979
+ isCircuitOpen(providerKey) {
1980
+ const state = this.circuitBreakers.get(providerKey);
1981
+ if (!state || !state.isOpen) return false;
1982
+ if (state.lastFailureTime && Date.now() - state.lastFailureTime > 3e4) {
1983
+ this.circuitBreakers.set(providerKey, {
1984
+ consecutiveFailures: 0,
1985
+ isOpen: false
1986
+ });
1987
+ return false;
1988
+ }
1989
+ return true;
1990
+ }
1991
+ recordSuccess(providerKey) {
1992
+ this.circuitBreakers.set(providerKey, {
1993
+ consecutiveFailures: 0,
1994
+ isOpen: false
1995
+ });
1996
+ }
1997
+ recordFailure(providerKey) {
1998
+ const state = this.circuitBreakers.get(providerKey) || {
1999
+ consecutiveFailures: 0,
2000
+ isOpen: false
2001
+ };
2002
+ state.consecutiveFailures++;
2003
+ state.lastFailureTime = Date.now();
2004
+ const threshold = this.config.circuitBreakerThreshold || 3;
2005
+ if (state.consecutiveFailures >= threshold) {
2006
+ state.isOpen = true;
2007
+ console.warn(`Circuit breaker opened for ${providerKey} after ${state.consecutiveFailures} failures`);
2008
+ }
2009
+ this.circuitBreakers.set(providerKey, state);
2010
+ }
2011
+ };
2012
+
2013
+ // src/adapter.ts
2014
+ function adaptToCoreLLMProvider(provider) {
2015
+ return {
2016
+ name: provider.name,
2017
+ capabilities: {
2018
+ supportsImages: true,
2019
+ supportsPDFs: provider.capabilities.supportsPDFs,
2020
+ maxPDFPages: provider.capabilities.maxPDFPages
2021
+ },
2022
+ async completeJson(input) {
2023
+ let multimodalInput;
2024
+ if (typeof input.prompt === "string") {
2025
+ multimodalInput = { text: input.prompt };
2026
+ } else {
2027
+ multimodalInput = input.prompt;
2028
+ }
2029
+ const response = await provider.completeJson({
2030
+ input: multimodalInput,
2031
+ schema: input.schema,
2032
+ max_tokens: input.max_tokens,
2033
+ reasoning: input.reasoning
2034
+ });
2035
+ return {
2036
+ json: response.json,
2037
+ rawText: response.rawText,
2038
+ costUSD: response.metrics.costUSD,
2039
+ inputTokens: response.metrics.inputTokens,
2040
+ outputTokens: response.metrics.outputTokens,
2041
+ cacheCreationInputTokens: response.metrics.cacheCreationInputTokens,
2042
+ cacheReadInputTokens: response.metrics.cacheReadInputTokens
2043
+ };
2044
+ }
2045
+ };
2046
+ }
2047
+
2048
+ // src/metadata.ts
2049
+ var SUPPORTED_IMAGE_TYPES = {
2050
+ COMMON: ["image/png", "image/jpeg", "image/webp", "image/gif"],
2051
+ EXTENDED: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/bmp", "image/tiff", "image/heif"]
2052
+ };
2053
+ var PROVIDER_METADATA = {
2054
+ openai: {
2055
+ id: "openai",
2056
+ name: "OpenAI",
2057
+ vendor: "openai",
2058
+ models: ["gpt-5.1", "gpt-4.1", "gpt-4.1-mini", "o3", "o3-mini", "o4-mini"],
2059
+ detailedModels: [
2060
+ {
2061
+ id: "gpt-4.1",
2062
+ name: "GPT-4.1",
2063
+ openRouterId: "openai/gpt-4.1",
2064
+ capabilities: { supportsReasoning: false },
2065
+ limits: { maxContextTokens: 128e3, maxOutputTokens: 16384 },
2066
+ pricing: { inputPer1k: 2e-3, outputPer1k: 8e-3 }
2067
+ },
2068
+ {
2069
+ id: "gpt-4.1-mini",
2070
+ name: "GPT-4.1 Mini",
2071
+ openRouterId: "openai/gpt-4.1-mini",
2072
+ capabilities: { supportsReasoning: false },
2073
+ limits: { maxContextTokens: 128e3, maxOutputTokens: 16384 },
2074
+ pricing: { inputPer1k: 4e-4, outputPer1k: 16e-4 }
2075
+ },
2076
+ {
2077
+ id: "o3",
2078
+ name: "o3",
2079
+ openRouterId: "openai/o3",
2080
+ capabilities: { supportsReasoning: true },
2081
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 1e5 },
2082
+ pricing: { inputPer1k: 0.01, outputPer1k: 0.04 }
2083
+ },
2084
+ {
2085
+ id: "o3-mini",
2086
+ name: "o3-mini",
2087
+ openRouterId: "openai/o3-mini",
2088
+ capabilities: { supportsReasoning: true },
2089
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 1e5 },
2090
+ pricing: { inputPer1k: 11e-4, outputPer1k: 44e-4 }
2091
+ },
2092
+ {
2093
+ id: "o4-mini",
2094
+ name: "o4-mini",
2095
+ openRouterId: "openai/o4-mini",
2096
+ capabilities: { supportsReasoning: true },
2097
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 1e5 },
2098
+ pricing: { inputPer1k: 11e-4, outputPer1k: 44e-4 }
2099
+ },
2100
+ {
2101
+ id: "gpt-5.1",
2102
+ name: "GPT-5.1",
2103
+ openRouterId: "openai/gpt-5.1",
2104
+ capabilities: { supportsReasoning: true },
2105
+ limits: { maxContextTokens: 256e3, maxOutputTokens: 32768 },
2106
+ pricing: { inputPer1k: 5e-3, outputPer1k: 0.015 }
2107
+ }
2108
+ ],
2109
+ accessMethods: {
2110
+ native: {
2111
+ available: true,
2112
+ endpoint: "https://api.openai.com/v1",
2113
+ requiresApiKey: true
2114
+ },
2115
+ openrouter: {
2116
+ available: true,
2117
+ modelPrefix: "openai/"
2118
+ }
2119
+ },
2120
+ capabilities: {
2121
+ supportsImages: true,
2122
+ supportsPDFs: true,
2123
+ supportsReasoning: true,
2124
+ supportsStreaming: true,
2125
+ supportsStructuredOutput: true
2126
+ },
2127
+ // LLM with vision - can work with raw documents OR parsed text
2128
+ inputRequirements: {
2129
+ inputType: "any",
2130
+ acceptedMethods: ["url", "base64"]
2131
+ },
2132
+ compatibleNodes: {
2133
+ parse: true,
2134
+ // ✅ VLMProvider - can convert images/PDFs to text
2135
+ extract: true,
2136
+ // ✅ VLMProvider - can extract structured data
2137
+ categorize: true,
2138
+ // ✅ VLMProvider - can classify documents
2139
+ qualify: true,
2140
+ // ✅ VLMProvider - can assess quality
2141
+ split: true
2142
+ // ✅ VLMProvider - can detect document boundaries
2143
+ },
2144
+ inputFormats: {
2145
+ images: {
2146
+ mimeTypes: SUPPORTED_IMAGE_TYPES.COMMON,
2147
+ methods: ["url", "base64"],
2148
+ maxSize: 20,
2149
+ // 20 MB per image
2150
+ maxDimensions: void 0,
2151
+ // Images resized server-side
2152
+ notes: "Inline via image_url with data URL or HTTP URL. Large images auto-resized."
2153
+ },
2154
+ pdfs: {
2155
+ supported: true,
2156
+ methods: ["base64", "fileId"],
2157
+ maxSize: 50,
2158
+ // 50 MB per file, 50 MB total per request
2159
+ maxPages: 100,
2160
+ notes: "Inline via type: file with base64, or via Files API. Extracts text + images of each page. File URLs NOT supported for chat completions."
2161
+ }
2162
+ },
2163
+ outputFormat: {
2164
+ supportsJSON: true,
2165
+ supportsReasoning: true,
2166
+ tokenTracking: true,
2167
+ costTracking: true
2168
+ },
2169
+ pricing: {
2170
+ model: "per-token",
2171
+ inputPer1k: 5e-3,
2172
+ outputPer1k: 0.015,
2173
+ currency: "USD",
2174
+ notes: "Cost calculated from tokens. OpenRouter may include cost in response. GPT-4.1 baseline."
2175
+ },
2176
+ limits: {
2177
+ maxContextTokens: 128e3,
2178
+ maxOutputTokens: 16384,
2179
+ requestsPerMinute: void 0
2180
+ },
2181
+ nativeAPI: {
2182
+ imageFormat: 'type: "image_url", image_url: { url: "data:image/jpeg;base64,..." }',
2183
+ pdfFormat: 'type: "file", file: { file_id: "..." } OR file: { filename: "...", file_data: "data:application/pdf;base64,..." }',
2184
+ reasoningConfig: 'reasoning: { effort: "low"|"medium"|"high", exclude?: boolean }'
2185
+ },
2186
+ openRouterAPI: {
2187
+ imageFormat: "Same as native (OpenAI-compatible)",
2188
+ pdfFormat: "Same as native (OpenAI-compatible)",
2189
+ reasoningConfig: "Same as native (OpenAI-compatible)",
2190
+ differences: [
2191
+ "File URLs not supported (base64 only)",
2192
+ "Cost may be available via usage.total_cost or generation endpoint"
2193
+ ]
2194
+ }
2195
+ },
2196
+ anthropic: {
2197
+ id: "anthropic",
2198
+ name: "Anthropic (Claude)",
2199
+ vendor: "anthropic",
2200
+ models: ["claude-opus-4.5", "claude-sonnet-4.5", "claude-haiku-4.5", "claude-opus-4", "claude-sonnet-4"],
2201
+ detailedModels: [
2202
+ {
2203
+ id: "claude-opus-4.5",
2204
+ name: "Claude Opus 4.5",
2205
+ openRouterId: "anthropic/claude-opus-4.5",
2206
+ capabilities: { supportsReasoning: true },
2207
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 32e3 },
2208
+ pricing: { inputPer1k: 0.015, outputPer1k: 0.075 }
2209
+ },
2210
+ {
2211
+ id: "claude-sonnet-4.5",
2212
+ name: "Claude Sonnet 4.5",
2213
+ openRouterId: "anthropic/claude-sonnet-4.5",
2214
+ // Reasoning available via toggle (extended thinking)
2215
+ capabilities: { supportsReasoning: true },
2216
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 16e3 },
2217
+ pricing: { inputPer1k: 3e-3, outputPer1k: 0.015 }
2218
+ },
2219
+ {
2220
+ id: "claude-haiku-4.5",
2221
+ name: "Claude Haiku 4.5",
2222
+ openRouterId: "anthropic/claude-haiku-4.5",
2223
+ // Reasoning available via toggle (extended thinking)
2224
+ capabilities: { supportsReasoning: true },
2225
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 8192 },
2226
+ pricing: { inputPer1k: 8e-4, outputPer1k: 4e-3 }
2227
+ },
2228
+ {
2229
+ id: "claude-opus-4",
2230
+ name: "Claude Opus 4",
2231
+ openRouterId: "anthropic/claude-opus-4",
2232
+ capabilities: { supportsReasoning: true },
2233
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 32e3 },
2234
+ pricing: { inputPer1k: 0.015, outputPer1k: 0.075 }
2235
+ },
2236
+ {
2237
+ id: "claude-sonnet-4",
2238
+ name: "Claude Sonnet 4",
2239
+ openRouterId: "anthropic/claude-sonnet-4",
2240
+ // Reasoning available via toggle (extended thinking)
2241
+ capabilities: { supportsReasoning: true },
2242
+ limits: { maxContextTokens: 2e5, maxOutputTokens: 16e3 },
2243
+ pricing: { inputPer1k: 3e-3, outputPer1k: 0.015 }
2244
+ }
2245
+ ],
2246
+ accessMethods: {
2247
+ native: {
2248
+ available: true,
2249
+ endpoint: "https://api.anthropic.com/v1",
2250
+ requiresApiKey: true
2251
+ },
2252
+ openrouter: {
2253
+ available: true,
2254
+ modelPrefix: "anthropic/"
2255
+ }
2256
+ },
2257
+ capabilities: {
2258
+ supportsImages: true,
2259
+ supportsPDFs: true,
2260
+ supportsReasoning: true,
2261
+ supportsStreaming: true,
2262
+ supportsStructuredOutput: true
2263
+ },
2264
+ // LLM with vision - can work with raw documents OR parsed text
2265
+ inputRequirements: {
2266
+ inputType: "any",
2267
+ acceptedMethods: ["base64"]
2268
+ // URLs must be downloaded and converted
2269
+ },
2270
+ compatibleNodes: {
2271
+ parse: true,
2272
+ extract: true,
2273
+ categorize: true,
2274
+ qualify: true,
2275
+ split: true
2276
+ },
2277
+ inputFormats: {
2278
+ images: {
2279
+ mimeTypes: SUPPORTED_IMAGE_TYPES.COMMON,
2280
+ methods: ["base64"],
2281
+ // URLs must be downloaded and converted
2282
+ maxSize: 5,
2283
+ // 5 MB per image (API), 10 MB (claude.ai)
2284
+ maxDimensions: { width: 8e3, height: 8e3 },
2285
+ // Standard limit, 2000x2000 for 20+ images
2286
+ notes: "Native API requires base64. Max 100 images/request. Optimal at 1568px max dimension."
2287
+ },
2288
+ pdfs: {
2289
+ supported: true,
2290
+ methods: ["base64", "fileId"],
2291
+ maxSize: 32,
2292
+ // 32 MB per file
2293
+ maxPages: 100,
2294
+ // 100 pages for full visual analysis
2295
+ notes: "Inline via type: document with base64, or via Files API (beta). PDFs over 100 pages: text-only processing."
2296
+ }
2297
+ },
2298
+ outputFormat: {
2299
+ supportsJSON: true,
2300
+ supportsReasoning: true,
2301
+ tokenTracking: true,
2302
+ costTracking: true
2303
+ },
2304
+ pricing: {
2305
+ model: "per-token",
2306
+ inputPer1k: 3e-3,
2307
+ outputPer1k: 0.015,
2308
+ currency: "USD",
2309
+ notes: "Cost calculated from tokens. OpenRouter may include cost in response. Claude 3.5 Sonnet baseline."
2310
+ },
2311
+ limits: {
2312
+ maxContextTokens: 2e5,
2313
+ maxOutputTokens: 8192,
2314
+ requestsPerMinute: void 0
2315
+ },
2316
+ nativeAPI: {
2317
+ imageFormat: 'type: "image", source: { type: "base64", media_type: "image/jpeg", data: "..." }',
2318
+ pdfFormat: 'type: "document", source: { type: "base64"|"file", media_type: "application/pdf", data: "..." | file_id: "..." }',
2319
+ reasoningConfig: 'thinking: { type: "enabled", budget_tokens: 1024-32000 }'
2320
+ },
2321
+ openRouterAPI: {
2322
+ imageFormat: 'type: "image_url", image_url: { url: "data:image/jpeg;base64,..." }',
2323
+ pdfFormat: 'type: "file", file: { filename: "...", file_data: "data:application/pdf;base64,..." }',
2324
+ reasoningConfig: "reasoning: { max_tokens: number, exclude?: boolean }",
2325
+ differences: [
2326
+ "Uses OpenAI-compatible format (image_url, file types)",
2327
+ "Reasoning uses max_tokens instead of budget_tokens",
2328
+ 'Response prefill trick ({ role: "assistant", content: "{" }) for strict JSON',
2329
+ "Tool calling instead of native structured output"
2330
+ ]
2331
+ }
2332
+ },
2333
+ google: {
2334
+ id: "google",
2335
+ name: "Google (Gemini)",
2336
+ vendor: "google",
2337
+ models: ["gemini-3-pro", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"],
2338
+ detailedModels: [
2339
+ {
2340
+ id: "gemini-3-pro",
2341
+ name: "Gemini 3 Pro",
2342
+ openRouterId: "google/gemini-3-pro",
2343
+ capabilities: { supportsReasoning: true },
2344
+ limits: { maxContextTokens: 1e6, maxOutputTokens: 65536 },
2345
+ pricing: { inputPer1k: 125e-5, outputPer1k: 5e-3 }
2346
+ },
2347
+ {
2348
+ id: "gemini-2.5-pro",
2349
+ name: "Gemini 2.5 Pro",
2350
+ openRouterId: "google/gemini-2.5-pro-preview-06-05",
2351
+ capabilities: { supportsReasoning: true },
2352
+ limits: { maxContextTokens: 1e6, maxOutputTokens: 65536 },
2353
+ pricing: { inputPer1k: 125e-5, outputPer1k: 5e-3 }
2354
+ },
2355
+ {
2356
+ id: "gemini-2.5-flash",
2357
+ name: "Gemini 2.5 Flash",
2358
+ openRouterId: "google/gemini-2.5-flash-preview-09-2025",
2359
+ capabilities: { supportsReasoning: false },
2360
+ limits: { maxContextTokens: 1e6, maxOutputTokens: 8192 },
2361
+ pricing: { inputPer1k: 15e-5, outputPer1k: 6e-4 }
2362
+ },
2363
+ {
2364
+ id: "gemini-2.5-flash-lite",
2365
+ name: "Gemini 2.5 Flash Lite",
2366
+ openRouterId: "google/gemini-2.5-flash-lite",
2367
+ capabilities: { supportsReasoning: false },
2368
+ limits: { maxContextTokens: 1e6, maxOutputTokens: 8192 },
2369
+ pricing: { inputPer1k: 75e-6, outputPer1k: 3e-4 }
2370
+ }
2371
+ ],
2372
+ accessMethods: {
2373
+ native: {
2374
+ available: true,
2375
+ endpoint: "https://generativelanguage.googleapis.com/v1beta",
2376
+ requiresApiKey: true
2377
+ },
2378
+ openrouter: {
2379
+ available: true,
2380
+ modelPrefix: "google/"
2381
+ }
2382
+ },
2383
+ capabilities: {
2384
+ supportsImages: true,
2385
+ supportsPDFs: true,
2386
+ supportsReasoning: true,
2387
+ supportsStreaming: true,
2388
+ supportsStructuredOutput: true
2389
+ },
2390
+ // LLM with vision - can work with raw documents OR parsed text
2391
+ inputRequirements: {
2392
+ inputType: "any",
2393
+ acceptedMethods: ["base64"]
2394
+ // URLs are downloaded and converted
2395
+ },
2396
+ compatibleNodes: {
2397
+ parse: true,
2398
+ extract: true,
2399
+ categorize: true,
2400
+ qualify: true,
2401
+ split: true
2402
+ },
2403
+ inputFormats: {
2404
+ images: {
2405
+ mimeTypes: [...SUPPORTED_IMAGE_TYPES.COMMON, "image/bmp", "image/tiff", "image/heif"],
2406
+ methods: ["base64"],
2407
+ // URLs are downloaded and converted
2408
+ maxSize: 20,
2409
+ // 20 MB inline data limit (File API: 2GB for Gemini 2.0)
2410
+ maxDimensions: { width: 3072, height: 3072 },
2411
+ // Images scaled to fit, padded to preserve ratio
2412
+ notes: "Inline via inlineData. Max 3000 images/request. File API supports larger files (stored 48 hours)."
2413
+ },
2414
+ pdfs: {
2415
+ supported: true,
2416
+ methods: ["base64", "fileId"],
2417
+ maxSize: 50,
2418
+ // 50 MB limit (inline & File API)
2419
+ maxPages: 1e3,
2420
+ // 1000 pages max, each page = 258 tokens
2421
+ notes: "Inline via inlineData OR File API. Pages scaled to 3072x3072 max. Native text not charged."
2422
+ }
2423
+ },
2424
+ outputFormat: {
2425
+ supportsJSON: true,
2426
+ supportsReasoning: true,
2427
+ tokenTracking: true,
2428
+ costTracking: true
2429
+ },
2430
+ pricing: {
2431
+ model: "per-token",
2432
+ inputPer1k: 25e-5,
2433
+ outputPer1k: 1e-3,
2434
+ currency: "USD",
2435
+ notes: "Cost calculated from tokens. OpenRouter may include cost in response. Gemini 2.5 Flash baseline."
2436
+ },
2437
+ limits: {
2438
+ maxContextTokens: 1e6,
2439
+ // 1M tokens
2440
+ maxOutputTokens: 8192,
2441
+ requestsPerMinute: void 0
2442
+ },
2443
+ nativeAPI: {
2444
+ imageFormat: 'inlineData: { mimeType: "image/jpeg", data: "..." }',
2445
+ pdfFormat: 'inlineData: { mimeType: "application/pdf", data: "..." } OR fileData: { fileUri: "...", mimeType: "application/pdf" }',
2446
+ reasoningConfig: "generationConfig.thinking_config: { thinking_budget: number } (max 24576)"
2447
+ },
2448
+ openRouterAPI: {
2449
+ imageFormat: 'type: "image_url", image_url: { url: "data:image/jpeg;base64,..." }',
2450
+ pdfFormat: 'type: "file", file: { filename: "...", file_data: "data:application/pdf;base64,..." }',
2451
+ reasoningConfig: "reasoning: { max_tokens: number, exclude?: boolean }",
2452
+ differences: [
2453
+ "Uses OpenAI-compatible format instead of parts/inlineData",
2454
+ "Reasoning uses max_tokens instead of thinking_budget",
2455
+ "Different content structure (messages vs contents.parts)"
2456
+ ]
2457
+ }
2458
+ },
2459
+ xai: {
2460
+ id: "xai",
2461
+ name: "xAI (Grok)",
2462
+ vendor: "xai",
2463
+ models: ["grok-4.1", "grok-4.1-fast", "grok-4", "grok-4-fast"],
2464
+ detailedModels: [
2465
+ {
2466
+ id: "grok-4.1",
2467
+ name: "Grok 4.1",
2468
+ openRouterId: "x-ai/grok-4.1",
2469
+ capabilities: { supportsReasoning: true },
2470
+ limits: { maxContextTokens: 256e3, maxOutputTokens: 32768 },
2471
+ pricing: { inputPer1k: 3e-3, outputPer1k: 0.015 }
2472
+ },
2473
+ {
2474
+ id: "grok-4.1-fast",
2475
+ name: "Grok 4.1 Fast",
2476
+ openRouterId: "x-ai/grok-4.1-fast",
2477
+ capabilities: { supportsReasoning: false },
2478
+ limits: { maxContextTokens: 2e6, maxOutputTokens: 32768 },
2479
+ pricing: { inputPer1k: 5e-3, outputPer1k: 0.025 }
2480
+ },
2481
+ {
2482
+ id: "grok-4",
2483
+ name: "Grok 4",
2484
+ openRouterId: "x-ai/grok-4",
2485
+ capabilities: { supportsReasoning: true },
2486
+ limits: { maxContextTokens: 256e3, maxOutputTokens: 32768 },
2487
+ pricing: { inputPer1k: 3e-3, outputPer1k: 0.015 }
2488
+ },
2489
+ {
2490
+ id: "grok-4-fast",
2491
+ name: "Grok 4 Fast",
2492
+ openRouterId: "x-ai/grok-4-fast",
2493
+ capabilities: { supportsReasoning: false },
2494
+ limits: { maxContextTokens: 2e6, maxOutputTokens: 32768 },
2495
+ pricing: { inputPer1k: 5e-3, outputPer1k: 0.025 }
2496
+ }
2497
+ ],
2498
+ accessMethods: {
2499
+ native: {
2500
+ available: true,
2501
+ endpoint: "https://api.x.ai/v1",
2502
+ requiresApiKey: true
2503
+ },
2504
+ openrouter: {
2505
+ available: true,
2506
+ modelPrefix: "xai/"
2507
+ }
2508
+ },
2509
+ capabilities: {
2510
+ supportsImages: true,
2511
+ supportsPDFs: true,
2512
+ supportsReasoning: true,
2513
+ supportsStreaming: false,
2514
+ // Not with structured outputs
2515
+ supportsStructuredOutput: true
2516
+ },
2517
+ // LLM with vision - can work with raw documents OR parsed text
2518
+ inputRequirements: {
2519
+ inputType: "any",
2520
+ acceptedMethods: ["url", "base64"]
2521
+ },
2522
+ compatibleNodes: {
2523
+ parse: true,
2524
+ extract: true,
2525
+ categorize: true,
2526
+ qualify: true,
2527
+ split: true
2528
+ },
2529
+ inputFormats: {
2530
+ images: {
2531
+ mimeTypes: ["image/jpeg", "image/png"],
2532
+ // Only jpg/jpeg and png supported
2533
+ methods: ["url", "base64"],
2534
+ maxSize: 30,
2535
+ // 30 MB per file (API), 25 MB (chat)
2536
+ maxDimensions: void 0,
2537
+ notes: "OpenAI-compatible format. Max 10 images via API. Only JPEG/PNG supported."
2538
+ },
2539
+ pdfs: {
2540
+ supported: true,
2541
+ methods: ["url", "base64"],
2542
+ maxSize: 30,
2543
+ // 30 MB per file
2544
+ maxPages: void 0,
2545
+ notes: "OpenAI-compatible format. Inline via type: file. Also supports DOCX, TXT, MD, CSV."
2546
+ }
2547
+ },
2548
+ outputFormat: {
2549
+ supportsJSON: true,
2550
+ supportsReasoning: true,
2551
+ tokenTracking: true,
2552
+ costTracking: true
2553
+ },
2554
+ pricing: {
2555
+ model: "per-token",
2556
+ inputPer1k: 5e-3,
2557
+ outputPer1k: 0.015,
2558
+ currency: "USD",
2559
+ notes: "Cost calculated from tokens. OpenRouter may include cost in response. Grok-4 baseline."
2560
+ },
2561
+ limits: {
2562
+ maxContextTokens: 131072,
2563
+ maxOutputTokens: void 0,
2564
+ requestsPerMinute: void 0
2565
+ },
2566
+ nativeAPI: {
2567
+ imageFormat: 'type: "image_url", image_url: { url: "data:image/jpeg;base64,..." }',
2568
+ pdfFormat: 'type: "file", file: { filename: "...", file_data: "data:application/pdf;base64,..." }',
2569
+ reasoningConfig: 'reasoning: { effort: "low"|"medium"|"high", exclude?: boolean }'
2570
+ },
2571
+ openRouterAPI: {
2572
+ imageFormat: "Same as native (OpenAI-compatible)",
2573
+ pdfFormat: "Same as native (OpenAI-compatible)",
2574
+ reasoningConfig: "Same as native (OpenAI-compatible)",
2575
+ differences: [
2576
+ "Minimal differences - xAI uses OpenAI-compatible format",
2577
+ "Cost tracking via usage.total_cost field in OpenRouter"
2578
+ ]
2579
+ }
2580
+ }
2581
+ };
2582
+ function isImageTypeSupported(providerId, mimeType) {
2583
+ const provider = PROVIDER_METADATA[providerId];
2584
+ return provider.inputFormats.images.mimeTypes.includes(mimeType);
2585
+ }
2586
+ function supportsPDFsInline(providerId) {
2587
+ const provider = PROVIDER_METADATA[providerId];
2588
+ return provider.inputFormats.pdfs.supported && provider.inputFormats.pdfs.methods.includes("base64");
2589
+ }
2590
+ function getProvidersForNode(nodeType) {
2591
+ return Object.values(PROVIDER_METADATA).filter(
2592
+ (provider) => provider.compatibleNodes[nodeType]
2593
+ );
2594
+ }
2595
+ function isProviderCompatibleWithNode(providerId, nodeType) {
2596
+ return PROVIDER_METADATA[providerId].compatibleNodes[nodeType];
2597
+ }
2598
+ function estimateCost(providerId, inputTokens, outputTokens) {
2599
+ const provider = PROVIDER_METADATA[providerId];
2600
+ const inputCost = inputTokens / 1e3 * provider.pricing.inputPer1k;
2601
+ const outputCost = outputTokens / 1e3 * provider.pricing.outputPer1k;
2602
+ return inputCost + outputCost;
2603
+ }
2604
+ function getCheapestProvider(inputTokens, outputTokens) {
2605
+ const providers = Object.values(PROVIDER_METADATA);
2606
+ return providers.reduce((cheapest, current) => {
2607
+ const cheapestCost = estimateCost(cheapest.id, inputTokens, outputTokens);
2608
+ const currentCost = estimateCost(current.id, inputTokens, outputTokens);
2609
+ return currentCost < cheapestCost ? current : cheapest;
2610
+ });
2611
+ }
2612
+ function compareNativeVsOpenRouter(providerId) {
2613
+ const provider = PROVIDER_METADATA[providerId];
2614
+ return {
2615
+ provider: provider.name,
2616
+ nativeAvailable: provider.accessMethods.native.available,
2617
+ openRouterAvailable: provider.accessMethods.openrouter.available,
2618
+ differences: provider.openRouterAPI.differences
2619
+ };
2620
+ }
2621
+
2622
+ // src/index.ts
2623
+ if (!providerRegistry.has("openai")) {
2624
+ providerRegistry.register("openai", (config) => new OpenAIProvider(config));
2625
+ }
2626
+ if (!providerRegistry.has("anthropic")) {
2627
+ providerRegistry.register("anthropic", (config) => new AnthropicProvider(config));
2628
+ }
2629
+ if (!providerRegistry.has("google")) {
2630
+ providerRegistry.register("google", (config) => new GoogleProvider(config));
2631
+ }
2632
+ if (!providerRegistry.has("xai")) {
2633
+ providerRegistry.register("xai", (config) => new XAIProvider(config));
2634
+ }
2635
+ function createVLMProvider(config) {
2636
+ let internalProvider;
2637
+ const providerConfig = {
2638
+ provider: config.provider,
2639
+ model: config.model,
2640
+ apiKey: config.apiKey,
2641
+ via: config.via,
2642
+ baseUrl: config.baseUrl
2643
+ };
2644
+ internalProvider = createProviderFromRegistry(providerConfig);
2645
+ return {
2646
+ name: internalProvider.name,
2647
+ capabilities: {
2648
+ supportsImages: true,
2649
+ supportsPDFs: internalProvider.capabilities.supportsPDFs,
2650
+ maxPDFPages: internalProvider.capabilities.maxPDFPages
2651
+ },
2652
+ async completeJson(input) {
2653
+ let multimodalInput;
2654
+ if (typeof input.prompt === "string") {
2655
+ multimodalInput = { text: input.prompt };
2656
+ } else {
2657
+ multimodalInput = input.prompt;
2658
+ }
2659
+ const response = await internalProvider.completeJson({
2660
+ input: multimodalInput,
2661
+ schema: input.schema,
2662
+ max_tokens: input.max_tokens,
2663
+ reasoning: input.reasoning
2664
+ });
2665
+ return {
2666
+ json: response.json,
2667
+ rawText: response.rawText,
2668
+ costUSD: response.metrics.costUSD,
2669
+ inputTokens: response.metrics.inputTokens,
2670
+ outputTokens: response.metrics.outputTokens
2671
+ };
2672
+ }
2673
+ };
2674
+ }
2675
+ function buildLLMProvider(config) {
2676
+ const manager = new FallbackManager(config);
2677
+ let observabilityContext;
2678
+ const coreProvider = {
2679
+ name: config.providers.map((p) => `${p.provider}:${p.model}`).join(" -> "),
2680
+ capabilities: {
2681
+ supportsImages: true,
2682
+ supportsPDFs: true,
2683
+ maxPDFPages: Math.min(...config.providers.map((_) => 100))
2684
+ },
2685
+ async completeJson(input) {
2686
+ let multimodalInput;
2687
+ if (typeof input.prompt === "string") {
2688
+ multimodalInput = { text: input.prompt };
2689
+ } else {
2690
+ multimodalInput = input.prompt;
2691
+ }
2692
+ const response = await manager.executeWithFallback(
2693
+ multimodalInput,
2694
+ input.schema,
2695
+ input.max_tokens,
2696
+ input.reasoning,
2697
+ void 0,
2698
+ // mode - defaults to strict when schema provided
2699
+ observabilityContext
2700
+ );
2701
+ return {
2702
+ json: response.json,
2703
+ rawText: response.rawText,
2704
+ costUSD: response.metrics.costUSD,
2705
+ inputTokens: response.metrics.inputTokens,
2706
+ outputTokens: response.metrics.outputTokens,
2707
+ cacheCreationInputTokens: response.metrics.cacheCreationInputTokens,
2708
+ cacheReadInputTokens: response.metrics.cacheReadInputTokens
2709
+ };
2710
+ }
2711
+ };
2712
+ coreProvider.__setObservabilityContext = (ctx) => {
2713
+ observabilityContext = ctx;
2714
+ };
2715
+ return coreProvider;
2716
+ }
2717
+ export {
2718
+ AnthropicProvider,
2719
+ FallbackManager,
2720
+ GoogleProvider,
2721
+ OpenAIProvider,
2722
+ PROVIDER_METADATA,
2723
+ SUPPORTED_IMAGE_TYPES,
2724
+ SchemaTranslator,
2725
+ XAIProvider,
2726
+ adaptToCoreLLMProvider,
2727
+ buildLLMProvider,
2728
+ buildSchemaPromptSection,
2729
+ combineSchemaAndUserPrompt,
2730
+ compareNativeVsOpenRouter,
2731
+ createProviderFromRegistry,
2732
+ createVLMProvider,
2733
+ estimateCost,
2734
+ formatSchemaForPrompt,
2735
+ getCheapestProvider,
2736
+ getProvidersForNode,
2737
+ isImageTypeSupported,
2738
+ isProviderCompatibleWithNode,
2739
+ providerRegistry,
2740
+ registerProvider,
2741
+ supportsPDFsInline
2742
+ };
2743
+ //# sourceMappingURL=index.js.map