@kognitivedev/vercel-ai-provider 0.1.8 → 0.2.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 +112 -8
- package/dist/__tests__/template.test.d.ts +1 -0
- package/dist/__tests__/template.test.js +112 -0
- package/dist/__tests__/wrap-stream-logging.test.js +92 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +257 -22
- package/dist/template.d.ts +2 -0
- package/dist/template.js +13 -0
- package/package.json +4 -1
- package/src/__tests__/template.test.ts +161 -0
- package/src/__tests__/wrap-stream-logging.test.ts +104 -0
- package/src/handlebars.d.ts +4 -0
- package/src/index.ts +307 -20
- package/src/template.ts +10 -0
package/dist/index.js
CHANGED
|
@@ -11,14 +11,31 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.renderTemplate = void 0;
|
|
14
15
|
exports.createCognitiveLayer = createCognitiveLayer;
|
|
15
16
|
const ai_1 = require("ai");
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
var template_1 = require("./template");
|
|
19
|
+
Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
|
|
20
|
+
const template_2 = require("./template");
|
|
16
21
|
function isValidId(value) {
|
|
17
22
|
if (value == null || typeof value !== "string")
|
|
18
23
|
return false;
|
|
19
24
|
const trimmed = value.trim();
|
|
20
25
|
return trimmed !== "" && trimmed !== "null" && trimmed !== "undefined";
|
|
21
26
|
}
|
|
27
|
+
function maskSecret(secret) {
|
|
28
|
+
if (!secret)
|
|
29
|
+
return "missing";
|
|
30
|
+
if (secret.length <= 8)
|
|
31
|
+
return `${secret.slice(0, 2)}***`;
|
|
32
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
33
|
+
}
|
|
34
|
+
function previewText(value, maxLength = 240) {
|
|
35
|
+
if (value.length <= maxLength)
|
|
36
|
+
return value;
|
|
37
|
+
return `${value.slice(0, maxLength)}...`;
|
|
38
|
+
}
|
|
22
39
|
const LOG_LEVEL_PRIORITY = {
|
|
23
40
|
none: 0,
|
|
24
41
|
error: 1,
|
|
@@ -55,12 +72,91 @@ function createLogger(logLevel) {
|
|
|
55
72
|
};
|
|
56
73
|
}
|
|
57
74
|
const PROMPT_CACHE_TTL_MS = 60000; // 1 minute
|
|
75
|
+
function getContentText(content) {
|
|
76
|
+
if (typeof content === "string")
|
|
77
|
+
return content;
|
|
78
|
+
if (!Array.isArray(content))
|
|
79
|
+
return "";
|
|
80
|
+
return content.map((part) => {
|
|
81
|
+
if (!part || typeof part !== "object")
|
|
82
|
+
return "";
|
|
83
|
+
if (typeof part.text === "string")
|
|
84
|
+
return part.text;
|
|
85
|
+
if (part.type === "tool-call" && typeof part.toolName === "string")
|
|
86
|
+
return `Called ${part.toolName}`;
|
|
87
|
+
if (part.type === "tool-result")
|
|
88
|
+
return "Received tool result";
|
|
89
|
+
return "";
|
|
90
|
+
}).filter(Boolean).join(" ");
|
|
91
|
+
}
|
|
58
92
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* Unwraps V2/V3 ToolResultOutput discriminated union to a displayable value.
|
|
94
|
+
* Stream ToolResult uses plain `result` (passthrough), while prompt ToolResultPart
|
|
95
|
+
* uses `output` with a discriminated union: text, json, error-text, error-json, content, execution-denied.
|
|
61
96
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
97
|
+
function extractOutputValue(raw) {
|
|
98
|
+
var _a;
|
|
99
|
+
if (raw == null)
|
|
100
|
+
return raw;
|
|
101
|
+
if (typeof raw !== 'object')
|
|
102
|
+
return raw;
|
|
103
|
+
const obj = raw;
|
|
104
|
+
if (typeof obj.type !== 'string')
|
|
105
|
+
return raw;
|
|
106
|
+
switch (obj.type) {
|
|
107
|
+
case 'text':
|
|
108
|
+
case 'json':
|
|
109
|
+
case 'error-text':
|
|
110
|
+
case 'error-json':
|
|
111
|
+
case 'content':
|
|
112
|
+
return obj.value;
|
|
113
|
+
case 'execution-denied':
|
|
114
|
+
return `Execution denied: ${(_a = obj.reason) !== null && _a !== void 0 ? _a : 'unknown'}`;
|
|
115
|
+
default:
|
|
116
|
+
return raw;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function buildTracePreviews(messages) {
|
|
120
|
+
const request = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "user");
|
|
121
|
+
const response = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "assistant");
|
|
122
|
+
return {
|
|
123
|
+
requestPreview: request ? getContentText(request.content).slice(0, 220) : "No request captured",
|
|
124
|
+
responsePreview: response ? getContentText(response.content).slice(0, 240) : "No response captured",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function buildTraceSpansFromMessages(messages) {
|
|
128
|
+
var _a, _b;
|
|
129
|
+
const resultMap = new Map();
|
|
130
|
+
for (const message of messages) {
|
|
131
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
132
|
+
continue;
|
|
133
|
+
for (const part of message.content) {
|
|
134
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-result" && typeof part.toolCallId === "string") {
|
|
135
|
+
resultMap.set(part.toolCallId, (_a = part.result) !== null && _a !== void 0 ? _a : part.output);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const spans = [];
|
|
140
|
+
for (const message of messages) {
|
|
141
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
142
|
+
continue;
|
|
143
|
+
for (const part of message.content) {
|
|
144
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-call" && typeof part.toolCallId === "string") {
|
|
145
|
+
const result = resultMap.get(part.toolCallId);
|
|
146
|
+
spans.push({
|
|
147
|
+
spanKey: part.toolCallId,
|
|
148
|
+
parentSpanKey: "root",
|
|
149
|
+
name: typeof part.toolName === "string" ? part.toolName : "tool",
|
|
150
|
+
spanType: "tool",
|
|
151
|
+
status: "completed",
|
|
152
|
+
inputPreview: JSON.stringify((_b = part.input) !== null && _b !== void 0 ? _b : {}).slice(0, 220),
|
|
153
|
+
outputPreview: result != null ? JSON.stringify(extractOutputValue(result)).slice(0, 220) : "No tool result captured",
|
|
154
|
+
toolName: typeof part.toolName === "string" ? part.toolName : undefined,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return spans;
|
|
64
160
|
}
|
|
65
161
|
// Session-scoped snapshot cache: sessionKey → formatted memory block
|
|
66
162
|
const sessionSnapshots = new Map();
|
|
@@ -101,6 +197,7 @@ function createCognitiveLayer(config) {
|
|
|
101
197
|
// Prompt cache: slug → CachedPrompt
|
|
102
198
|
const promptCache = new Map();
|
|
103
199
|
const resolvePrompt = async (slug, userId) => {
|
|
200
|
+
var _a;
|
|
104
201
|
const cacheKey = userId ? `${slug}:${userId}` : slug;
|
|
105
202
|
const cached = promptCache.get(cacheKey);
|
|
106
203
|
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
@@ -111,11 +208,31 @@ function createCognitiveLayer(config) {
|
|
|
111
208
|
url.searchParams.set("slug", slug);
|
|
112
209
|
if (userId)
|
|
113
210
|
url.searchParams.set("userId", userId);
|
|
211
|
+
logger.debug("Resolving prompt from backend", {
|
|
212
|
+
slug,
|
|
213
|
+
userId,
|
|
214
|
+
url: url.toString(),
|
|
215
|
+
baseUrl,
|
|
216
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
217
|
+
});
|
|
114
218
|
const res = await fetch(url.toString(), {
|
|
115
219
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
116
220
|
});
|
|
221
|
+
logger.debug("Prompt resolve response received", {
|
|
222
|
+
slug,
|
|
223
|
+
userId,
|
|
224
|
+
status: res.status,
|
|
225
|
+
ok: res.ok,
|
|
226
|
+
contentType: res.headers.get("content-type"),
|
|
227
|
+
});
|
|
117
228
|
if (!res.ok) {
|
|
118
229
|
const body = await res.text();
|
|
230
|
+
logger.debug("Prompt resolve response body preview", {
|
|
231
|
+
slug,
|
|
232
|
+
userId,
|
|
233
|
+
status: res.status,
|
|
234
|
+
bodyPreview: previewText(body),
|
|
235
|
+
});
|
|
119
236
|
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
120
237
|
}
|
|
121
238
|
const data = await res.json();
|
|
@@ -128,6 +245,14 @@ function createCognitiveLayer(config) {
|
|
|
128
245
|
gatewaySlug: data.gatewaySlug,
|
|
129
246
|
};
|
|
130
247
|
promptCache.set(cacheKey, entry);
|
|
248
|
+
logger.debug("Prompt resolved payload", {
|
|
249
|
+
slug,
|
|
250
|
+
resolvedSlug: entry.slug,
|
|
251
|
+
version: entry.version,
|
|
252
|
+
promptId: entry.promptId,
|
|
253
|
+
contentLength: entry.content.length,
|
|
254
|
+
gatewaySlug: (_a = entry.gatewaySlug) !== null && _a !== void 0 ? _a : null,
|
|
255
|
+
});
|
|
131
256
|
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
132
257
|
return entry;
|
|
133
258
|
};
|
|
@@ -194,9 +319,25 @@ function createCognitiveLayer(config) {
|
|
|
194
319
|
if (systemPromptToAdd === undefined) {
|
|
195
320
|
try {
|
|
196
321
|
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
322
|
+
logger.debug("Fetching snapshot from backend", {
|
|
323
|
+
userId,
|
|
324
|
+
projectId,
|
|
325
|
+
sessionId,
|
|
326
|
+
url,
|
|
327
|
+
baseUrl,
|
|
328
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
329
|
+
});
|
|
197
330
|
const res = await fetch(url, {
|
|
198
331
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
199
332
|
});
|
|
333
|
+
logger.debug("Snapshot response received", {
|
|
334
|
+
userId,
|
|
335
|
+
projectId,
|
|
336
|
+
sessionId,
|
|
337
|
+
status: res.status,
|
|
338
|
+
ok: res.ok,
|
|
339
|
+
contentType: res.headers.get("content-type"),
|
|
340
|
+
});
|
|
200
341
|
if (res.ok) {
|
|
201
342
|
const data = await res.json();
|
|
202
343
|
const systemBlock = data.systemBlock || "";
|
|
@@ -229,7 +370,15 @@ ${userContextBlock || "None"}
|
|
|
229
370
|
});
|
|
230
371
|
}
|
|
231
372
|
else {
|
|
373
|
+
const body = await res.text();
|
|
232
374
|
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
375
|
+
logger.debug("Snapshot response body preview", {
|
|
376
|
+
userId,
|
|
377
|
+
projectId,
|
|
378
|
+
sessionId,
|
|
379
|
+
status: res.status,
|
|
380
|
+
bodyPreview: previewText(body),
|
|
381
|
+
});
|
|
233
382
|
systemPromptToAdd = "";
|
|
234
383
|
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
235
384
|
}
|
|
@@ -255,7 +404,8 @@ ${userContextBlock || "None"}
|
|
|
255
404
|
return Object.assign(Object.assign({}, nextParams), { prompt: messagesWithMemory });
|
|
256
405
|
},
|
|
257
406
|
async wrapGenerate({ doGenerate, params }) {
|
|
258
|
-
var _a
|
|
407
|
+
var _a;
|
|
408
|
+
const startedAt = new Date();
|
|
259
409
|
let result;
|
|
260
410
|
try {
|
|
261
411
|
result = await doGenerate();
|
|
@@ -266,28 +416,57 @@ ${userContextBlock || "None"}
|
|
|
266
416
|
throw err;
|
|
267
417
|
}
|
|
268
418
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
419
|
+
const endedAt = new Date();
|
|
269
420
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
270
421
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
271
|
-
const messagesInput = params.
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
422
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
423
|
+
// Build assistant message from result.content (V2/V3 GenerateResult)
|
|
424
|
+
const resultContent = Array.isArray(result === null || result === void 0 ? void 0 : result.content) ? result.content : [];
|
|
425
|
+
const assistantParts = [];
|
|
426
|
+
for (const part of resultContent) {
|
|
427
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === 'text') {
|
|
428
|
+
assistantParts.push({ type: 'text', text: part.text });
|
|
429
|
+
}
|
|
430
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-call') {
|
|
431
|
+
assistantParts.push({
|
|
432
|
+
type: 'tool-call',
|
|
433
|
+
toolCallId: part.toolCallId,
|
|
434
|
+
toolName: part.toolName,
|
|
435
|
+
input: part.input,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-result') {
|
|
439
|
+
assistantParts.push({
|
|
440
|
+
type: 'tool-result',
|
|
441
|
+
toolCallId: part.toolCallId,
|
|
442
|
+
toolName: part.toolName,
|
|
443
|
+
result: part.result,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const assistantMessage = assistantParts.length > 0
|
|
448
|
+
? [{ role: "assistant", content: assistantParts }]
|
|
275
449
|
: [];
|
|
276
|
-
const finalMessages =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logConversation(Object.assign({ userId,
|
|
450
|
+
const finalMessages = [...messagesInput, ...assistantMessage];
|
|
451
|
+
const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
|
|
452
|
+
const spans = buildTraceSpansFromMessages(finalMessages);
|
|
453
|
+
logConversation(Object.assign(Object.assign({ userId,
|
|
280
454
|
projectId,
|
|
281
455
|
sessionId, messages: finalMessages, modelId, usage: result.usage }, (promptMeta && {
|
|
282
456
|
promptSlug: promptMeta.promptSlug,
|
|
283
457
|
promptVersion: promptMeta.promptVersion,
|
|
284
458
|
promptId: promptMeta.promptId,
|
|
285
|
-
}))
|
|
459
|
+
})), { traceId: (0, crypto_1.randomUUID)(), requestPreview,
|
|
460
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
461
|
+
appId: clConfig.appId,
|
|
462
|
+
}, spans })).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
286
463
|
}
|
|
287
464
|
return result;
|
|
288
465
|
},
|
|
289
466
|
async wrapStream({ doStream, params }) {
|
|
290
467
|
var _a;
|
|
468
|
+
const startedAt = new Date();
|
|
469
|
+
const traceId = (0, crypto_1.randomUUID)();
|
|
291
470
|
let result;
|
|
292
471
|
try {
|
|
293
472
|
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
@@ -302,13 +481,16 @@ ${userContextBlock || "None"}
|
|
|
302
481
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
303
482
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
304
483
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
305
|
-
const messagesInput = params.
|
|
484
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
306
485
|
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
307
486
|
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
308
487
|
? resultMessages
|
|
309
488
|
: messagesInput;
|
|
310
489
|
let streamUsage;
|
|
311
490
|
let accumulatedText = '';
|
|
491
|
+
const toolCallInputs = new Map();
|
|
492
|
+
const completedToolCalls = [];
|
|
493
|
+
const completedToolResults = [];
|
|
312
494
|
const originalStream = result.stream;
|
|
313
495
|
const transformStream = new TransformStream({
|
|
314
496
|
transform(chunk, controller) {
|
|
@@ -318,19 +500,72 @@ ${userContextBlock || "None"}
|
|
|
318
500
|
if (chunk.type === 'finish' && chunk.usage) {
|
|
319
501
|
streamUsage = chunk.usage;
|
|
320
502
|
}
|
|
503
|
+
// Capture tool-call stream chunks (V2/V3 shared types)
|
|
504
|
+
if (chunk.type === 'tool-input-start') {
|
|
505
|
+
toolCallInputs.set(chunk.id, { toolName: chunk.toolName, chunks: [] });
|
|
506
|
+
}
|
|
507
|
+
if (chunk.type === 'tool-input-delta') {
|
|
508
|
+
const entry = toolCallInputs.get(chunk.id);
|
|
509
|
+
if (entry)
|
|
510
|
+
entry.chunks.push(chunk.delta);
|
|
511
|
+
}
|
|
512
|
+
if (chunk.type === 'tool-call') {
|
|
513
|
+
completedToolCalls.push({
|
|
514
|
+
type: 'tool-call',
|
|
515
|
+
toolCallId: chunk.toolCallId,
|
|
516
|
+
toolName: chunk.toolName,
|
|
517
|
+
input: chunk.input,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (chunk.type === 'tool-result') {
|
|
521
|
+
completedToolResults.push({
|
|
522
|
+
type: 'tool-result',
|
|
523
|
+
toolCallId: chunk.toolCallId,
|
|
524
|
+
toolName: chunk.toolName,
|
|
525
|
+
result: chunk.result,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
321
528
|
controller.enqueue(chunk);
|
|
322
529
|
},
|
|
323
|
-
flush() {
|
|
324
|
-
const
|
|
325
|
-
|
|
530
|
+
async flush() {
|
|
531
|
+
const endedAt = new Date();
|
|
532
|
+
// Finalize any tool calls from incremental input chunks
|
|
533
|
+
for (const [id, entry] of toolCallInputs) {
|
|
534
|
+
// Only add if not already captured via a tool-call chunk
|
|
535
|
+
if (!completedToolCalls.some((tc) => tc.toolCallId === id)) {
|
|
536
|
+
completedToolCalls.push({
|
|
537
|
+
type: 'tool-call',
|
|
538
|
+
toolCallId: id,
|
|
539
|
+
toolName: entry.toolName,
|
|
540
|
+
input: entry.chunks.join(''),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const assistantParts = [];
|
|
545
|
+
if (accumulatedText)
|
|
546
|
+
assistantParts.push({ type: "text", text: accumulatedText });
|
|
547
|
+
for (const tc of completedToolCalls)
|
|
548
|
+
assistantParts.push(tc);
|
|
549
|
+
const allMessages = assistantParts.length > 0
|
|
550
|
+
? [...finalMessages, { role: "assistant", content: assistantParts }]
|
|
326
551
|
: finalMessages;
|
|
327
|
-
|
|
552
|
+
if (completedToolResults.length > 0) {
|
|
553
|
+
allMessages.push({ role: "tool", content: completedToolResults });
|
|
554
|
+
}
|
|
555
|
+
const { requestPreview, responsePreview } = buildTracePreviews(allMessages);
|
|
556
|
+
const spans = buildTraceSpansFromMessages(allMessages);
|
|
557
|
+
await logConversation(Object.assign(Object.assign({ userId,
|
|
328
558
|
projectId,
|
|
329
559
|
sessionId, messages: allMessages, modelId, usage: streamUsage }, (promptMeta && {
|
|
330
560
|
promptSlug: promptMeta.promptSlug,
|
|
331
561
|
promptVersion: promptMeta.promptVersion,
|
|
332
562
|
promptId: promptMeta.promptId,
|
|
333
|
-
}))
|
|
563
|
+
})), { traceId,
|
|
564
|
+
requestPreview,
|
|
565
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
566
|
+
appId: clConfig.appId,
|
|
567
|
+
}, spans }));
|
|
568
|
+
triggerProcessing(userId, projectId, sessionId);
|
|
334
569
|
}
|
|
335
570
|
});
|
|
336
571
|
result.stream = originalStream.pipeThrough(transformStream);
|
|
@@ -402,7 +637,7 @@ ${userContextBlock || "None"}
|
|
|
402
637
|
let system;
|
|
403
638
|
if (resolved) {
|
|
404
639
|
system = promptConfig.variables
|
|
405
|
-
?
|
|
640
|
+
? (0, template_2.renderTemplate)(resolved.content, promptConfig.variables)
|
|
406
641
|
: resolved.content;
|
|
407
642
|
// Store prompt metadata for the session (read by middleware during logging)
|
|
408
643
|
if (session === null || session === void 0 ? void 0 : session.sessionId) {
|
|
@@ -441,7 +676,7 @@ ${userContextBlock || "None"}
|
|
|
441
676
|
let system;
|
|
442
677
|
if (resolved) {
|
|
443
678
|
system = promptConfig.variables
|
|
444
|
-
?
|
|
679
|
+
? (0, template_2.renderTemplate)(resolved.content, promptConfig.variables)
|
|
445
680
|
: resolved.content;
|
|
446
681
|
// Store prompt metadata for the session (read by middleware during logging)
|
|
447
682
|
if (session === null || session === void 0 ? void 0 : session.sessionId) {
|
package/dist/template.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderTemplate = renderTemplate;
|
|
7
|
+
// Use the pre-built dist to avoid `require.extensions` warning in webpack/Next.js
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const handlebars_1 = __importDefault(require("handlebars/dist/cjs/handlebars"));
|
|
10
|
+
function renderTemplate(template, variables) {
|
|
11
|
+
const compiled = handlebars_1.default.compile(template, { noEscape: true });
|
|
12
|
+
return compiled(variables);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kognitivedev/vercel-ai-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"publishConfig": {
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"prepublishOnly": "npm run build"
|
|
14
14
|
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"handlebars": "^4.7.8"
|
|
17
|
+
},
|
|
15
18
|
"peerDependencies": {
|
|
16
19
|
"ai": "^5.0.0 || ^6.0.0"
|
|
17
20
|
},
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderTemplate } from "../template";
|
|
3
|
+
|
|
4
|
+
describe("renderTemplate", () => {
|
|
5
|
+
describe("variable interpolation", () => {
|
|
6
|
+
it("replaces a single variable", () => {
|
|
7
|
+
expect(renderTemplate("Hello {{name}}", { name: "Alice" })).toBe("Hello Alice");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("replaces multiple variables", () => {
|
|
11
|
+
expect(
|
|
12
|
+
renderTemplate("{{greeting}}, {{name}}!", { greeting: "Hi", name: "Bob" })
|
|
13
|
+
).toBe("Hi, Bob!");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders undefined variables as empty string", () => {
|
|
17
|
+
expect(renderTemplate("Hello {{name}}", {})).toBe("Hello ");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not HTML-escape content", () => {
|
|
21
|
+
expect(renderTemplate("{{content}}", { content: "<b>bold</b> & \"quoted\"" })).toBe(
|
|
22
|
+
"<b>bold</b> & \"quoted\""
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("{{#if}} / {{/if}}", () => {
|
|
28
|
+
it("includes block when variable is truthy string", () => {
|
|
29
|
+
expect(
|
|
30
|
+
renderTemplate("start{{#if show}} visible{{/if}} end", { show: "yes" })
|
|
31
|
+
).toBe("start visible end");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("includes block when variable is true", () => {
|
|
35
|
+
expect(
|
|
36
|
+
renderTemplate("start{{#if show}} visible{{/if}} end", { show: true })
|
|
37
|
+
).toBe("start visible end");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("excludes block when variable is false", () => {
|
|
41
|
+
expect(
|
|
42
|
+
renderTemplate("start{{#if show}} visible{{/if}} end", { show: false })
|
|
43
|
+
).toBe("start end");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("excludes block when variable is undefined", () => {
|
|
47
|
+
expect(
|
|
48
|
+
renderTemplate("start{{#if show}} visible{{/if}} end", {})
|
|
49
|
+
).toBe("start end");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("excludes block when variable is empty string", () => {
|
|
53
|
+
expect(
|
|
54
|
+
renderTemplate("start{{#if show}} visible{{/if}} end", { show: "" })
|
|
55
|
+
).toBe("start end");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("{{#unless}} / {{/unless}}", () => {
|
|
60
|
+
it("includes block when variable is falsy", () => {
|
|
61
|
+
expect(
|
|
62
|
+
renderTemplate("{{#unless premium}}Free tier{{/unless}}", { premium: false })
|
|
63
|
+
).toBe("Free tier");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("excludes block when variable is truthy", () => {
|
|
67
|
+
expect(
|
|
68
|
+
renderTemplate("{{#unless premium}}Free tier{{/unless}}", { premium: true })
|
|
69
|
+
).toBe("");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("{{else}} branches", () => {
|
|
74
|
+
it("renders else branch when if condition is false", () => {
|
|
75
|
+
expect(
|
|
76
|
+
renderTemplate(
|
|
77
|
+
"{{#if vip}}VIP access{{else}}Standard access{{/if}}",
|
|
78
|
+
{ vip: false }
|
|
79
|
+
)
|
|
80
|
+
).toBe("Standard access");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders if branch when condition is true", () => {
|
|
84
|
+
expect(
|
|
85
|
+
renderTemplate(
|
|
86
|
+
"{{#if vip}}VIP access{{else}}Standard access{{/if}}",
|
|
87
|
+
{ vip: true }
|
|
88
|
+
)
|
|
89
|
+
).toBe("VIP access");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("nested conditionals", () => {
|
|
94
|
+
it("handles nested if blocks", () => {
|
|
95
|
+
const template = "{{#if a}}A{{#if b}}-B{{/if}}{{/if}}";
|
|
96
|
+
expect(renderTemplate(template, { a: true, b: true })).toBe("A-B");
|
|
97
|
+
expect(renderTemplate(template, { a: true, b: false })).toBe("A");
|
|
98
|
+
expect(renderTemplate(template, { a: false, b: true })).toBe("");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("variables inside conditionals", () => {
|
|
103
|
+
it("interpolates variables within conditional blocks", () => {
|
|
104
|
+
expect(
|
|
105
|
+
renderTemplate(
|
|
106
|
+
"{{#if hasName}}Name: {{name}}{{/if}}",
|
|
107
|
+
{ hasName: true, name: "Alice" }
|
|
108
|
+
)
|
|
109
|
+
).toBe("Name: Alice");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("real-world fitness app template", () => {
|
|
114
|
+
const template = `You are a fitness coach AI assistant.
|
|
115
|
+
|
|
116
|
+
User: {{userName}}
|
|
117
|
+
Goal: {{fitnessGoal}}
|
|
118
|
+
|
|
119
|
+
{{#if hasImages}}
|
|
120
|
+
The user has attached images for form analysis. Please review them carefully.
|
|
121
|
+
{{/if}}
|
|
122
|
+
{{#if hasAttachments}}
|
|
123
|
+
Additional documents have been provided for context.
|
|
124
|
+
{{/if}}
|
|
125
|
+
{{#unless hasHistory}}
|
|
126
|
+
This is a new user with no prior conversation history. Introduce yourself.
|
|
127
|
+
{{/unless}}
|
|
128
|
+
|
|
129
|
+
Please provide personalized advice.`;
|
|
130
|
+
|
|
131
|
+
it("renders with all flags true", () => {
|
|
132
|
+
const result = renderTemplate(template, {
|
|
133
|
+
userName: "Sarah",
|
|
134
|
+
fitnessGoal: "Build muscle",
|
|
135
|
+
hasImages: true,
|
|
136
|
+
hasAttachments: true,
|
|
137
|
+
hasHistory: true,
|
|
138
|
+
});
|
|
139
|
+
expect(result).toContain("User: Sarah");
|
|
140
|
+
expect(result).toContain("Goal: Build muscle");
|
|
141
|
+
expect(result).toContain("attached images for form analysis");
|
|
142
|
+
expect(result).toContain("Additional documents");
|
|
143
|
+
expect(result).not.toContain("new user with no prior");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("renders with all flags false", () => {
|
|
147
|
+
const result = renderTemplate(template, {
|
|
148
|
+
userName: "Tom",
|
|
149
|
+
fitnessGoal: "Lose weight",
|
|
150
|
+
hasImages: false,
|
|
151
|
+
hasAttachments: false,
|
|
152
|
+
hasHistory: false,
|
|
153
|
+
});
|
|
154
|
+
expect(result).toContain("User: Tom");
|
|
155
|
+
expect(result).toContain("Goal: Lose weight");
|
|
156
|
+
expect(result).not.toContain("attached images");
|
|
157
|
+
expect(result).not.toContain("Additional documents");
|
|
158
|
+
expect(result).toContain("new user with no prior");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|