@jterrazz/intelligence 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +259 -55
- package/dist/index.cjs +594 -783
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +352 -5
- package/dist/index.js +620 -5
- package/dist/index.js.map +1 -1
- package/package.json +26 -20
- package/dist/middleware/__tests__/logging.middleware.test.d.ts +0 -1
- package/dist/middleware/__tests__/logging.middleware.test.js +0 -390
- package/dist/middleware/__tests__/logging.middleware.test.js.map +0 -1
- package/dist/middleware/logging.middleware.d.ts +0 -21
- package/dist/middleware/logging.middleware.js +0 -296
- package/dist/middleware/logging.middleware.js.map +0 -1
- package/dist/parsing/__tests__/create-schema-prompt.test.d.ts +0 -1
- package/dist/parsing/__tests__/create-schema-prompt.test.js +0 -53
- package/dist/parsing/__tests__/create-schema-prompt.test.js.map +0 -1
- package/dist/parsing/__tests__/parse-object.test.d.ts +0 -1
- package/dist/parsing/__tests__/parse-object.test.js +0 -193
- package/dist/parsing/__tests__/parse-object.test.js.map +0 -1
- package/dist/parsing/__tests__/parse-text.test.d.ts +0 -1
- package/dist/parsing/__tests__/parse-text.test.js +0 -167
- package/dist/parsing/__tests__/parse-text.test.js.map +0 -1
- package/dist/parsing/create-schema-prompt.d.ts +0 -28
- package/dist/parsing/create-schema-prompt.js +0 -42
- package/dist/parsing/create-schema-prompt.js.map +0 -1
- package/dist/parsing/parse-object.d.ts +0 -33
- package/dist/parsing/parse-object.js +0 -360
- package/dist/parsing/parse-object.js.map +0 -1
- package/dist/parsing/parse-text.d.ts +0 -14
- package/dist/parsing/parse-text.js +0 -76
- package/dist/parsing/parse-text.js.map +0 -1
- package/dist/providers/openrouter.provider.d.ts +0 -36
- package/dist/providers/openrouter.provider.js +0 -58
- package/dist/providers/openrouter.provider.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,622 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { Langfuse } from "langfuse";
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import { jsonrepair } from "jsonrepair";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
6
6
|
|
|
7
|
+
//#region src/logging/logging.middleware.ts
|
|
8
|
+
/**
|
|
9
|
+
* Creates middleware that logs AI SDK requests and responses.
|
|
10
|
+
*/
|
|
11
|
+
function createLoggingMiddleware(options) {
|
|
12
|
+
const { logger, include = {} } = options;
|
|
13
|
+
const { params: includeParams, content: includeContent, usage: includeUsage = true } = include;
|
|
14
|
+
return {
|
|
15
|
+
specificationVersion: "v3",
|
|
16
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
logger.debug("ai.generate.start", {
|
|
19
|
+
model: model.modelId,
|
|
20
|
+
...includeParams && { params }
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
const result = await doGenerate();
|
|
24
|
+
const textContent = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
25
|
+
logger.debug("ai.generate.complete", {
|
|
26
|
+
model: model.modelId,
|
|
27
|
+
durationMs: Date.now() - startTime,
|
|
28
|
+
finishReason: result.finishReason,
|
|
29
|
+
...includeUsage && { usage: result.usage },
|
|
30
|
+
...includeContent && { content: textContent }
|
|
31
|
+
});
|
|
32
|
+
return result;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error("ai.generate.error", {
|
|
35
|
+
model: model.modelId,
|
|
36
|
+
durationMs: Date.now() - startTime,
|
|
37
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
38
|
+
});
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
wrapStream: async ({ doStream, params, model }) => {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
logger.debug("ai.stream.start", {
|
|
45
|
+
model: model.modelId,
|
|
46
|
+
...includeParams && { params }
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const result = await doStream();
|
|
50
|
+
const chunks = [];
|
|
51
|
+
const transformStream = new TransformStream({
|
|
52
|
+
transform(chunk, controller) {
|
|
53
|
+
if (includeContent && chunk.type === "text-delta") chunks.push(chunk.delta);
|
|
54
|
+
controller.enqueue(chunk);
|
|
55
|
+
},
|
|
56
|
+
flush() {
|
|
57
|
+
logger.debug("ai.stream.complete", {
|
|
58
|
+
model: model.modelId,
|
|
59
|
+
durationMs: Date.now() - startTime,
|
|
60
|
+
...includeContent && { content: chunks.join("") }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
specificationVersion: "v3",
|
|
66
|
+
...result,
|
|
67
|
+
stream: result.stream.pipeThrough(transformStream)
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error("ai.stream.error", {
|
|
71
|
+
model: model.modelId,
|
|
72
|
+
durationMs: Date.now() - startTime,
|
|
73
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
74
|
+
});
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/observability/observability.middleware.ts
|
|
83
|
+
/**
|
|
84
|
+
* Helper to create type-safe observability metadata for providerOptions
|
|
85
|
+
*/
|
|
86
|
+
function withObservability(meta) {
|
|
87
|
+
return { observability: meta };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates middleware that sends generation data to an observability platform.
|
|
91
|
+
*/
|
|
92
|
+
function createObservabilityMiddleware(options) {
|
|
93
|
+
const { observability, providerMetadata } = options;
|
|
94
|
+
return {
|
|
95
|
+
specificationVersion: "v3",
|
|
96
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
97
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
98
|
+
const meta = params.providerOptions?.observability;
|
|
99
|
+
const result = await doGenerate();
|
|
100
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
101
|
+
if (meta?.traceId) {
|
|
102
|
+
const extracted = providerMetadata?.extract(result.providerMetadata);
|
|
103
|
+
const outputText = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
104
|
+
observability.generation({
|
|
105
|
+
traceId: meta.traceId,
|
|
106
|
+
name: meta.name ?? "generation",
|
|
107
|
+
model: model.modelId,
|
|
108
|
+
input: params.prompt,
|
|
109
|
+
output: outputText,
|
|
110
|
+
startTime,
|
|
111
|
+
endTime,
|
|
112
|
+
usage: extracted?.usage,
|
|
113
|
+
cost: extracted?.cost,
|
|
114
|
+
metadata: meta.metadata
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
wrapStream: async ({ doStream, params, model }) => {
|
|
120
|
+
const startTime = /* @__PURE__ */ new Date();
|
|
121
|
+
const meta = params.providerOptions?.observability;
|
|
122
|
+
const result = await doStream();
|
|
123
|
+
if (!meta?.traceId) return result;
|
|
124
|
+
const chunks = [];
|
|
125
|
+
const transformStream = new TransformStream({
|
|
126
|
+
transform(chunk, controller) {
|
|
127
|
+
if (chunk.type === "text-delta") chunks.push(chunk.delta);
|
|
128
|
+
controller.enqueue(chunk);
|
|
129
|
+
},
|
|
130
|
+
flush() {
|
|
131
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
132
|
+
observability.generation({
|
|
133
|
+
traceId: meta.traceId,
|
|
134
|
+
name: meta.name ?? "generation",
|
|
135
|
+
model: model.modelId,
|
|
136
|
+
input: params.prompt,
|
|
137
|
+
output: chunks.join(""),
|
|
138
|
+
startTime,
|
|
139
|
+
endTime,
|
|
140
|
+
metadata: meta.metadata
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
specificationVersion: "v3",
|
|
146
|
+
...result,
|
|
147
|
+
stream: result.stream.pipeThrough(transformStream)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/observability/langfuse.adapter.ts
|
|
155
|
+
/**
|
|
156
|
+
* Langfuse adapter implementing ObservabilityPort
|
|
157
|
+
*/
|
|
158
|
+
var LangfuseAdapter = class {
|
|
159
|
+
client;
|
|
160
|
+
constructor(config) {
|
|
161
|
+
this.client = new Langfuse({
|
|
162
|
+
secretKey: config.secretKey,
|
|
163
|
+
publicKey: config.publicKey,
|
|
164
|
+
baseUrl: config.baseUrl,
|
|
165
|
+
environment: config.environment,
|
|
166
|
+
release: config.release
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async flush() {
|
|
170
|
+
await this.client.flushAsync();
|
|
171
|
+
}
|
|
172
|
+
generation(params) {
|
|
173
|
+
const usageDetails = params.usage ? {
|
|
174
|
+
input: params.usage.input,
|
|
175
|
+
output: params.usage.output,
|
|
176
|
+
total: params.usage.total ?? params.usage.input + params.usage.output,
|
|
177
|
+
...params.usage.reasoning !== void 0 && { reasoning: params.usage.reasoning },
|
|
178
|
+
...params.usage.cacheRead !== void 0 && { cache_read: params.usage.cacheRead },
|
|
179
|
+
...params.usage.cacheWrite !== void 0 && { cache_write: params.usage.cacheWrite }
|
|
180
|
+
} : void 0;
|
|
181
|
+
const costDetails = params.cost ? {
|
|
182
|
+
total: params.cost.total,
|
|
183
|
+
...params.cost.input !== void 0 && { input: params.cost.input },
|
|
184
|
+
...params.cost.output !== void 0 && { output: params.cost.output }
|
|
185
|
+
} : void 0;
|
|
186
|
+
this.client.generation({
|
|
187
|
+
traceId: params.traceId,
|
|
188
|
+
name: params.name,
|
|
189
|
+
model: params.model,
|
|
190
|
+
input: params.input,
|
|
191
|
+
output: params.output,
|
|
192
|
+
startTime: params.startTime,
|
|
193
|
+
endTime: params.endTime,
|
|
194
|
+
usageDetails,
|
|
195
|
+
costDetails,
|
|
196
|
+
metadata: params.metadata
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async shutdown() {
|
|
200
|
+
await this.client.shutdownAsync();
|
|
201
|
+
}
|
|
202
|
+
trace(params) {
|
|
203
|
+
this.client.trace({
|
|
204
|
+
id: params.id,
|
|
205
|
+
name: params.name,
|
|
206
|
+
metadata: params.metadata
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/observability/noop.adapter.ts
|
|
213
|
+
/**
|
|
214
|
+
* No-op adapter that silently discards all observability data.
|
|
215
|
+
* Useful for testing, development, or when observability is disabled.
|
|
216
|
+
*/
|
|
217
|
+
var NoopObservabilityAdapter = class {
|
|
218
|
+
async flush() {}
|
|
219
|
+
generation(_params) {}
|
|
220
|
+
async shutdown() {}
|
|
221
|
+
trace(_params) {}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/result/result.ts
|
|
226
|
+
/**
|
|
227
|
+
* Create a successful result
|
|
228
|
+
*/
|
|
229
|
+
function generationSuccess(data) {
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
data
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Create a failed result
|
|
237
|
+
*/
|
|
238
|
+
function generationFailure(code, message, cause) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: {
|
|
242
|
+
code,
|
|
243
|
+
message,
|
|
244
|
+
cause
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Classify an error into a GenerationErrorCode
|
|
250
|
+
*/
|
|
251
|
+
function classifyError(error) {
|
|
252
|
+
if (error instanceof Error) {
|
|
253
|
+
const message = error.message.toLowerCase();
|
|
254
|
+
if (message.includes("timeout") || message.includes("timed out")) return "TIMEOUT";
|
|
255
|
+
if (message.includes("rate limit") || message.includes("429")) return "RATE_LIMITED";
|
|
256
|
+
if (message.includes("parse") || message.includes("json") || message.includes("unexpected token")) return "PARSING_FAILED";
|
|
257
|
+
if (message.includes("valid") || message.includes("schema") || message.includes("zod")) return "VALIDATION_FAILED";
|
|
258
|
+
}
|
|
259
|
+
return "AI_GENERATION_FAILED";
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Check if a result is successful (type guard)
|
|
263
|
+
*/
|
|
264
|
+
function isSuccess(result) {
|
|
265
|
+
return result.success;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Check if a result is a failure (type guard)
|
|
269
|
+
*/
|
|
270
|
+
function isFailure(result) {
|
|
271
|
+
return !result.success;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Unwrap a result, throwing if it fails
|
|
275
|
+
*/
|
|
276
|
+
function unwrap(result) {
|
|
277
|
+
if (result.success) return result.data;
|
|
278
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Unwrap a result with a default value for failures
|
|
282
|
+
*/
|
|
283
|
+
function unwrapOr(result, defaultValue) {
|
|
284
|
+
return result.success ? result.data : defaultValue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/parsing/parse-object.ts
|
|
289
|
+
const MARKDOWN_CODE_BLOCK_RE = /```(?:json)?\r?\n([^`]*?)\r?\n```/g;
|
|
290
|
+
/**
|
|
291
|
+
* Error thrown when object parsing fails.
|
|
292
|
+
* Contains the original text for debugging purposes.
|
|
293
|
+
*/
|
|
294
|
+
var ParseObjectError = class extends Error {
|
|
295
|
+
name = "ParseObjectError";
|
|
296
|
+
constructor(message, cause, text) {
|
|
297
|
+
super(message);
|
|
298
|
+
this.cause = cause;
|
|
299
|
+
this.text = text;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function convertToPrimitive(value, schema) {
|
|
303
|
+
if (schema instanceof z.ZodBoolean) return Boolean(value);
|
|
304
|
+
if (schema instanceof z.ZodNull) return null;
|
|
305
|
+
if (schema instanceof z.ZodNumber) return Number(value);
|
|
306
|
+
if (schema instanceof z.ZodString) return String(value);
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
function extractArray(text, originalText) {
|
|
310
|
+
const start = text.indexOf("[");
|
|
311
|
+
const end = text.lastIndexOf("]");
|
|
312
|
+
if (start === -1 || end === -1) throw new ParseObjectError("No array found in response", void 0, originalText);
|
|
313
|
+
try {
|
|
314
|
+
const raw = text.slice(start, end + 1);
|
|
315
|
+
return JSON.parse(jsonrepair(raw));
|
|
316
|
+
} catch (error) {
|
|
317
|
+
throw new ParseObjectError("Failed to parse array JSON", error, originalText);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function extractObject(text, originalText) {
|
|
321
|
+
const start = text.indexOf("{");
|
|
322
|
+
const end = text.lastIndexOf("}");
|
|
323
|
+
if (start === -1 || end === -1) throw new ParseObjectError("No object found in response", void 0, originalText);
|
|
324
|
+
try {
|
|
325
|
+
const raw = text.slice(start, end + 1);
|
|
326
|
+
return JSON.parse(jsonrepair(raw));
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new ParseObjectError("Failed to parse object JSON", error, originalText);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function extractPrimitive(text, schema) {
|
|
332
|
+
const trimmed = text.trim();
|
|
333
|
+
try {
|
|
334
|
+
return convertToPrimitive(JSON.parse(trimmed), schema);
|
|
335
|
+
} catch {
|
|
336
|
+
return convertToPrimitive(trimmed, schema);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function extractBySchemaType(text, schema, originalText) {
|
|
340
|
+
if (schema instanceof z.ZodArray) return extractArray(text, originalText);
|
|
341
|
+
if (schema instanceof z.ZodObject) return extractObject(text, originalText);
|
|
342
|
+
if (schema instanceof z.ZodBoolean || schema instanceof z.ZodNull || schema instanceof z.ZodNumber || schema instanceof z.ZodString) return extractPrimitive(text, schema);
|
|
343
|
+
if (schema instanceof z.ZodUnion || schema instanceof z.ZodDiscriminatedUnion) {
|
|
344
|
+
if (text.indexOf("{") !== -1) return extractObject(text, originalText);
|
|
345
|
+
if (text.indexOf("[") !== -1) return extractArray(text, originalText);
|
|
346
|
+
throw new ParseObjectError("No object or array found for union type", void 0, originalText);
|
|
347
|
+
}
|
|
348
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) return extractBySchemaType(text, schema.unwrap(), originalText);
|
|
349
|
+
if (schema instanceof z.ZodDefault) return extractBySchemaType(text, schema.def.innerType, originalText);
|
|
350
|
+
if (schema instanceof z.ZodPipe) return extractBySchemaType(text, schema.def.in, originalText);
|
|
351
|
+
throw new ParseObjectError("Unsupported schema type", void 0, originalText);
|
|
352
|
+
}
|
|
353
|
+
function extractJsonFromCodeBlock(block) {
|
|
354
|
+
const content = block.replace(/```(?:json)?\r?\n([^`]*?)\r?\n```/, "$1").trim();
|
|
355
|
+
try {
|
|
356
|
+
JSON.parse(content);
|
|
357
|
+
return content;
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function findLongestString(strings) {
|
|
363
|
+
return strings.reduce((longest, current) => current.length > longest.length ? current : longest);
|
|
364
|
+
}
|
|
365
|
+
function findJsonStructures(text) {
|
|
366
|
+
const matches = [];
|
|
367
|
+
let depth = 0;
|
|
368
|
+
let start = -1;
|
|
369
|
+
for (let i = 0; i < text.length; i++) {
|
|
370
|
+
const char = text[i];
|
|
371
|
+
if (char === "{" || char === "[") {
|
|
372
|
+
if (depth === 0) start = i;
|
|
373
|
+
depth++;
|
|
374
|
+
} else if (char === "}" || char === "]") {
|
|
375
|
+
depth--;
|
|
376
|
+
if (depth === 0 && start !== -1) {
|
|
377
|
+
const candidate = text.slice(start, i + 1);
|
|
378
|
+
try {
|
|
379
|
+
JSON.parse(candidate);
|
|
380
|
+
matches.push(candidate);
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return matches;
|
|
386
|
+
}
|
|
387
|
+
function extractJsonString(text) {
|
|
388
|
+
const codeBlocks = text.match(MARKDOWN_CODE_BLOCK_RE);
|
|
389
|
+
if (codeBlocks && codeBlocks.length > 0) {
|
|
390
|
+
const validBlocks = codeBlocks.map((block) => extractJsonFromCodeBlock(block)).filter((block) => block !== null);
|
|
391
|
+
if (validBlocks.length > 0) return findLongestString(validBlocks);
|
|
392
|
+
}
|
|
393
|
+
const structures = findJsonStructures(text);
|
|
394
|
+
if (structures.length > 0) return findLongestString(structures);
|
|
395
|
+
return text.replace(/\s+/g, " ").trim();
|
|
396
|
+
}
|
|
397
|
+
function unescapeString(text) {
|
|
398
|
+
return text.replace(/\\"/g, "\"").replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(Number.parseInt(code, 16)));
|
|
399
|
+
}
|
|
400
|
+
function unescapeJsonValues(json) {
|
|
401
|
+
if (typeof json === "string") return unescapeString(json);
|
|
402
|
+
if (Array.isArray(json)) return json.map(unescapeJsonValues);
|
|
403
|
+
if (typeof json === "object" && json !== null) return Object.fromEntries(Object.entries(json).map(([key, value]) => [key, unescapeJsonValues(value)]));
|
|
404
|
+
return json;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Parses AI-generated text into structured data validated against a Zod schema.
|
|
408
|
+
*
|
|
409
|
+
* Handles common AI response formats:
|
|
410
|
+
* - JSON wrapped in markdown code blocks
|
|
411
|
+
* - JSON embedded in prose text
|
|
412
|
+
* - Malformed JSON (auto-repaired)
|
|
413
|
+
* - Escaped unicode and special characters
|
|
414
|
+
*
|
|
415
|
+
* @param text - The raw AI response text
|
|
416
|
+
* @param schema - A Zod schema to validate and type the result
|
|
417
|
+
* @returns The parsed and validated data
|
|
418
|
+
* @throws {ParseObjectError} When parsing or validation fails
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* const schema = z.object({ title: z.string(), tags: z.array(z.string()) });
|
|
423
|
+
* const result = parseObject(aiResponse, schema);
|
|
424
|
+
* // result is typed as { title: string; tags: string[] }
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
function parseObject(text, schema) {
|
|
428
|
+
try {
|
|
429
|
+
const unescaped = unescapeJsonValues(extractBySchemaType(extractJsonString(text), schema, text));
|
|
430
|
+
return schema.parse(unescaped);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof ParseObjectError) throw error;
|
|
433
|
+
if (error instanceof z.ZodError) throw new ParseObjectError("Failed to validate response against schema", error, text);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/generation/generate-structured.ts
|
|
440
|
+
/**
|
|
441
|
+
* Generate structured data from an AI model with automatic parsing and error handling.
|
|
442
|
+
* Observability is handled by middleware - no metadata exposed to caller.
|
|
443
|
+
*/
|
|
444
|
+
async function generateStructured(options) {
|
|
445
|
+
const { model, prompt, system, schema, providerOptions, abortSignal, maxOutputTokens, temperature } = options;
|
|
446
|
+
try {
|
|
447
|
+
const response = await generateText({
|
|
448
|
+
model,
|
|
449
|
+
prompt,
|
|
450
|
+
system,
|
|
451
|
+
providerOptions,
|
|
452
|
+
abortSignal,
|
|
453
|
+
maxOutputTokens,
|
|
454
|
+
temperature
|
|
455
|
+
});
|
|
456
|
+
if (!response.text || response.text.trim() === "") return generationFailure("EMPTY_RESULT", "AI returned empty response");
|
|
457
|
+
try {
|
|
458
|
+
return generationSuccess(parseObject(response.text, schema));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error instanceof ParseObjectError) return generationFailure("PARSING_FAILED", error.message, error);
|
|
461
|
+
return generationFailure("VALIDATION_FAILED", "Schema validation failed", error);
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return generationFailure(classifyError(error), error instanceof Error ? error.message : "Unknown error", error);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/parsing/create-schema-prompt.ts
|
|
470
|
+
/**
|
|
471
|
+
* Creates a system prompt that instructs the model to output structured data
|
|
472
|
+
* matching the provided Zod schema.
|
|
473
|
+
*
|
|
474
|
+
* Use this with `generateText` when the provider doesn't support native
|
|
475
|
+
* structured outputs, then parse the response with `parseObject`.
|
|
476
|
+
*
|
|
477
|
+
* @param schema - A Zod schema defining the expected output structure
|
|
478
|
+
* @returns A system prompt string with JSON schema instructions
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* import { generateText } from 'ai';
|
|
483
|
+
* import { createSchemaPrompt, parseObject } from '@jterrazz/intelligence';
|
|
484
|
+
*
|
|
485
|
+
* const schema = z.object({ title: z.string(), tags: z.array(z.string()) });
|
|
486
|
+
*
|
|
487
|
+
* const { text } = await generateText({
|
|
488
|
+
* model,
|
|
489
|
+
* prompt: 'Generate an article about TypeScript',
|
|
490
|
+
* system: createSchemaPrompt(schema),
|
|
491
|
+
* });
|
|
492
|
+
*
|
|
493
|
+
* const result = parseObject(text, schema);
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
function createSchemaPrompt(schema) {
|
|
497
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
498
|
+
const schemaJson = JSON.stringify(jsonSchema, null, 2);
|
|
499
|
+
if ([
|
|
500
|
+
"boolean",
|
|
501
|
+
"integer",
|
|
502
|
+
"number",
|
|
503
|
+
"string"
|
|
504
|
+
].includes(jsonSchema.type)) return `<OUTPUT_FORMAT>
|
|
505
|
+
You must respond with a ${jsonSchema.type} value that matches this schema:
|
|
506
|
+
|
|
507
|
+
\`\`\`json
|
|
508
|
+
${schemaJson}
|
|
509
|
+
\`\`\`
|
|
510
|
+
|
|
511
|
+
Your response should be only the ${jsonSchema.type} value, without any JSON wrapping or additional text.
|
|
512
|
+
</OUTPUT_FORMAT>`;
|
|
513
|
+
return `<OUTPUT_FORMAT>
|
|
514
|
+
You must respond with valid JSON that matches this JSON schema:
|
|
515
|
+
|
|
516
|
+
\`\`\`json
|
|
517
|
+
${schemaJson}
|
|
518
|
+
\`\`\`
|
|
519
|
+
|
|
520
|
+
Your response must be parseable JSON that validates against this schema. Do not include any text outside the JSON.
|
|
521
|
+
</OUTPUT_FORMAT>`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/parsing/parse-text.ts
|
|
526
|
+
const INVISIBLE_CHARS_RE = /[\u00AD\u180E\u200B-\u200C\u200E-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
527
|
+
const ASCII_CTRL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g;
|
|
528
|
+
const SPACE_LIKE_RE = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
529
|
+
const MULTIPLE_SPACES_RE = / {2,}/g;
|
|
530
|
+
const CR_RE = /\r\n?/g;
|
|
531
|
+
const CITATION_RE = / *\(oaicite:\d+\)\{index=\d+\}/g;
|
|
532
|
+
const EM_DASH_SEPARATOR_RE = /\s*[—–―‒]\s*/g;
|
|
533
|
+
const TYPOGRAPHY_REPLACEMENTS = [
|
|
534
|
+
{
|
|
535
|
+
pattern: /[\u2018\u2019\u201A]/g,
|
|
536
|
+
replacement: "'"
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
pattern: /[\u201C\u201D\u201E]/g,
|
|
540
|
+
replacement: "\""
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
pattern: /\u2026/g,
|
|
544
|
+
replacement: "..."
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
pattern: /[\u2022\u25AA-\u25AB\u25B8-\u25B9\u25CF]/g,
|
|
548
|
+
replacement: "-"
|
|
549
|
+
}
|
|
550
|
+
];
|
|
551
|
+
/**
|
|
552
|
+
* Parses and sanitizes text by removing AI artifacts and normalizing typography.
|
|
553
|
+
*
|
|
554
|
+
* @param text - The text to parse
|
|
555
|
+
* @param options - Parsing options
|
|
556
|
+
* @returns The cleaned text
|
|
557
|
+
*/
|
|
558
|
+
function parseText(text, options = {}) {
|
|
559
|
+
const { normalizeEmDashesToCommas = true, collapseSpaces = true } = options;
|
|
560
|
+
if (!text) return "";
|
|
561
|
+
let result = text;
|
|
562
|
+
result = result.replace(CR_RE, "\n");
|
|
563
|
+
result = result.replace(CITATION_RE, "");
|
|
564
|
+
result = result.normalize("NFKC");
|
|
565
|
+
result = result.replace(INVISIBLE_CHARS_RE, "");
|
|
566
|
+
result = result.replace(ASCII_CTRL_RE, "");
|
|
567
|
+
if (normalizeEmDashesToCommas) result = result.replace(EM_DASH_SEPARATOR_RE, ", ");
|
|
568
|
+
result = result.replace(SPACE_LIKE_RE, " ");
|
|
569
|
+
for (const { pattern, replacement } of TYPOGRAPHY_REPLACEMENTS) result = result.replace(pattern, replacement);
|
|
570
|
+
if (collapseSpaces) result = result.replace(MULTIPLE_SPACES_RE, " ").trim();
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/provider/openrouter.provider.ts
|
|
576
|
+
/**
|
|
577
|
+
* Creates an OpenRouter provider for AI SDK models.
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```ts
|
|
581
|
+
* const provider = createOpenRouterProvider({ apiKey: process.env.OPENROUTER_API_KEY });
|
|
582
|
+
* const model = provider.model('anthropic/claude-sonnet-4-20250514');
|
|
583
|
+
*
|
|
584
|
+
* const { text } = await generateText({ model, prompt: 'Hello!' });
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
function createOpenRouterProvider(config) {
|
|
588
|
+
const openrouter = createOpenRouter({ apiKey: config.apiKey });
|
|
589
|
+
return { model(name, options = {}) {
|
|
590
|
+
return openrouter(name, {
|
|
591
|
+
...options.maxTokens !== void 0 && { maxTokens: options.maxTokens },
|
|
592
|
+
...options.reasoning && { extraBody: { reasoning: options.reasoning } }
|
|
593
|
+
});
|
|
594
|
+
} };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/provider/openrouter-metadata.adapter.ts
|
|
599
|
+
/**
|
|
600
|
+
* OpenRouter adapter for extracting usage and cost from provider metadata
|
|
601
|
+
*/
|
|
602
|
+
var OpenRouterMetadataAdapter = class {
|
|
603
|
+
extract(providerMetadata) {
|
|
604
|
+
const meta = providerMetadata?.openrouter;
|
|
605
|
+
if (!meta?.usage) return {};
|
|
606
|
+
const usage = meta.usage;
|
|
607
|
+
return {
|
|
608
|
+
usage: {
|
|
609
|
+
input: usage.promptTokens ?? 0,
|
|
610
|
+
output: usage.completionTokens ?? 0,
|
|
611
|
+
total: usage.totalTokens,
|
|
612
|
+
reasoning: usage.completionTokensDetails?.reasoningTokens,
|
|
613
|
+
cacheRead: usage.promptTokensDetails?.cachedTokens
|
|
614
|
+
},
|
|
615
|
+
cost: usage.cost !== void 0 ? { total: usage.cost } : void 0
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
//#endregion
|
|
621
|
+
export { LangfuseAdapter, NoopObservabilityAdapter, OpenRouterMetadataAdapter, ParseObjectError, classifyError, createLoggingMiddleware, createObservabilityMiddleware, createOpenRouterProvider, createSchemaPrompt, generateStructured, generationFailure, generationSuccess, isFailure, isSuccess, parseObject, parseText, unwrap, unwrapOr, withObservability };
|
|
7
622
|
//# sourceMappingURL=index.js.map
|