@kognitivedev/vercel-ai-provider 0.1.5 → 0.1.6
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 +244 -17
- package/dist/__tests__/wrap-stream-logging.test.d.ts +1 -0
- package/dist/__tests__/wrap-stream-logging.test.js +84 -0
- package/dist/index.d.ts +50 -12
- package/dist/index.js +334 -138
- package/package.json +6 -4
- package/src/__tests__/wrap-stream-logging.test.ts +104 -0
- package/src/index.ts +435 -166
- package/vitest.config.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
14
|
exports.createCognitiveLayer = createCognitiveLayer;
|
|
4
15
|
const ai_1 = require("ai");
|
|
@@ -37,10 +48,22 @@ function createLogger(logLevel) {
|
|
|
37
48
|
},
|
|
38
49
|
};
|
|
39
50
|
}
|
|
51
|
+
const PROMPT_CACHE_TTL_MS = 60000; // 1 minute
|
|
52
|
+
/**
|
|
53
|
+
* Interpolate {{variable}} placeholders in a template string.
|
|
54
|
+
* Unmatched variables are left as-is.
|
|
55
|
+
*/
|
|
56
|
+
function interpolateTemplate(content, variables) {
|
|
57
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => { var _a; return (_a = variables[key]) !== null && _a !== void 0 ? _a : `{{${key}}}`; });
|
|
58
|
+
}
|
|
40
59
|
// Session-scoped snapshot cache: sessionKey → formatted memory block
|
|
41
60
|
const sessionSnapshots = new Map();
|
|
42
61
|
// Regex to detect if memory has already been injected
|
|
43
62
|
const MEMORY_TAG_REGEX = /<MemoryContext>/i;
|
|
63
|
+
// Symbol-keyed property to track session settings on model objects
|
|
64
|
+
const SESSION_KEY = Symbol.for("cl:session");
|
|
65
|
+
// Session key → prompt metadata (populated by cl.streamText/cl.generateText, read by middleware)
|
|
66
|
+
const sessionPromptMetadata = new Map();
|
|
44
67
|
/**
|
|
45
68
|
* Check if any system message already contains a <MemoryContext> block.
|
|
46
69
|
*/
|
|
@@ -65,11 +88,43 @@ function createCognitiveLayer(config) {
|
|
|
65
88
|
// Default to 'info' log level
|
|
66
89
|
const logLevel = clConfig.logLevel || 'info';
|
|
67
90
|
const logger = createLogger(logLevel);
|
|
91
|
+
const authHeaders = {
|
|
92
|
+
"Content-Type": "application/json",
|
|
93
|
+
"Authorization": `Bearer ${clConfig.apiKey}`,
|
|
94
|
+
};
|
|
95
|
+
// Prompt cache: slug → CachedPrompt
|
|
96
|
+
const promptCache = new Map();
|
|
97
|
+
const resolvePrompt = async (slug) => {
|
|
98
|
+
const cached = promptCache.get(slug);
|
|
99
|
+
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
100
|
+
logger.debug("Using cached prompt", { slug, version: cached.version });
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
const res = await fetch(`${baseUrl}/api/cognitive/prompt?slug=${encodeURIComponent(slug)}`, {
|
|
104
|
+
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const body = await res.text();
|
|
108
|
+
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
109
|
+
}
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
const entry = {
|
|
112
|
+
promptId: data.promptId,
|
|
113
|
+
slug: data.slug,
|
|
114
|
+
version: data.version,
|
|
115
|
+
content: data.content,
|
|
116
|
+
fetchedAt: Date.now(),
|
|
117
|
+
gatewaySlug: data.gatewaySlug,
|
|
118
|
+
};
|
|
119
|
+
promptCache.set(slug, entry);
|
|
120
|
+
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
121
|
+
return entry;
|
|
122
|
+
};
|
|
68
123
|
const logConversation = async (payload) => {
|
|
69
124
|
try {
|
|
70
125
|
await fetch(`${baseUrl}/api/cognitive/log`, {
|
|
71
126
|
method: "POST",
|
|
72
|
-
headers:
|
|
127
|
+
headers: authHeaders,
|
|
73
128
|
body: JSON.stringify(Object.assign(Object.assign({}, payload), { type: "conversation", timestamp: new Date().toISOString() })),
|
|
74
129
|
});
|
|
75
130
|
}
|
|
@@ -77,12 +132,12 @@ function createCognitiveLayer(config) {
|
|
|
77
132
|
logger.error("Log failed", e);
|
|
78
133
|
}
|
|
79
134
|
};
|
|
80
|
-
const triggerProcessing = (userId,
|
|
135
|
+
const triggerProcessing = (userId, projectId, sessionId) => {
|
|
81
136
|
const run = () => {
|
|
82
137
|
fetch(`${baseUrl}/api/cognitive/process`, {
|
|
83
138
|
method: "POST",
|
|
84
|
-
headers:
|
|
85
|
-
body: JSON.stringify({ userId,
|
|
139
|
+
headers: authHeaders,
|
|
140
|
+
body: JSON.stringify({ userId, sessionId }),
|
|
86
141
|
}).catch(e => logger.error("Process trigger failed", e));
|
|
87
142
|
};
|
|
88
143
|
if (processDelay > 0) {
|
|
@@ -95,157 +150,298 @@ function createCognitiveLayer(config) {
|
|
|
95
150
|
const withMemorySystemPrompt = (params, incomingMessages, memoryPrompt) => {
|
|
96
151
|
var _a;
|
|
97
152
|
const nextParams = Object.assign({}, params);
|
|
98
|
-
// 1) If
|
|
99
|
-
if (nextParams.system) {
|
|
100
|
-
nextParams.system = memoryPrompt;
|
|
101
|
-
return { nextParams, messages: incomingMessages, mode: "overwrite-param" };
|
|
102
|
-
}
|
|
103
|
-
// 2) If first message is system, replace its content.
|
|
153
|
+
// 1) If first message is system, append memory to its content (without mutating original).
|
|
104
154
|
if (incomingMessages.length > 0 && ((_a = incomingMessages[0]) === null || _a === void 0 ? void 0 : _a.role) === "system") {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
updated
|
|
155
|
+
const original = incomingMessages[0];
|
|
156
|
+
const updatedContent = typeof original.content === "string"
|
|
157
|
+
? original.content + "\n\n" + memoryPrompt
|
|
158
|
+
: memoryPrompt;
|
|
159
|
+
const updated = [Object.assign(Object.assign({}, original), { content: updatedContent }), ...incomingMessages.slice(1)];
|
|
110
160
|
return { nextParams, messages: updated, mode: "overwrite-first-system" };
|
|
111
161
|
}
|
|
112
|
-
//
|
|
162
|
+
// 2) Otherwise prepend a system message.
|
|
113
163
|
const updated = [{ role: "system", content: memoryPrompt }, ...incomingMessages];
|
|
114
164
|
return { nextParams, messages: updated, mode: "prepend-system" };
|
|
115
165
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (systemPromptToAdd === undefined) {
|
|
147
|
-
try {
|
|
148
|
-
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}&agentId=${agentId}&appId=${clConfig.appId}`;
|
|
149
|
-
const res = await fetch(url);
|
|
150
|
-
if (res.ok) {
|
|
151
|
-
const data = await res.json();
|
|
152
|
-
const systemBlock = data.systemBlock || "";
|
|
153
|
-
const userContextBlock = data.userContextBlock || "";
|
|
154
|
-
systemPromptToAdd =
|
|
155
|
-
systemBlock !== "" || userContextBlock !== ""
|
|
156
|
-
? `
|
|
166
|
+
const buildMiddleware = (userId, projectId, sessionId, modelId) => ({
|
|
167
|
+
specificationVersion: 'v3',
|
|
168
|
+
async transformParams({ params }) {
|
|
169
|
+
if (!userId)
|
|
170
|
+
return params;
|
|
171
|
+
const incomingMessages = Array.isArray(params.prompt)
|
|
172
|
+
? params.prompt
|
|
173
|
+
: [];
|
|
174
|
+
// 1) Check if memory is already injected in messages
|
|
175
|
+
if (hasExistingMemoryInjection(incomingMessages)) {
|
|
176
|
+
logger.debug("Memory already injected, skipping");
|
|
177
|
+
return params;
|
|
178
|
+
}
|
|
179
|
+
// 2) Check session cache
|
|
180
|
+
const sessionKey = `${userId}:${projectId}:${sessionId || "default"}`;
|
|
181
|
+
let systemPromptToAdd = sessionSnapshots.get(sessionKey);
|
|
182
|
+
// 3) Fetch snapshot only if not cached
|
|
183
|
+
if (systemPromptToAdd === undefined) {
|
|
184
|
+
try {
|
|
185
|
+
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
186
|
+
const res = await fetch(url, {
|
|
187
|
+
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
188
|
+
});
|
|
189
|
+
if (res.ok) {
|
|
190
|
+
const data = await res.json();
|
|
191
|
+
const systemBlock = data.systemBlock || "";
|
|
192
|
+
const userContextBlock = data.userContextBlock || "";
|
|
193
|
+
systemPromptToAdd =
|
|
194
|
+
systemBlock !== "" || userContextBlock !== ""
|
|
195
|
+
? `
|
|
157
196
|
<MemoryContext>
|
|
158
197
|
Use the following memory to stay consistent. Prefer UserContext facts for answers; AgentHeuristics guide style, safety, and priorities.
|
|
159
198
|
${systemBlock || "None"}
|
|
160
199
|
${userContextBlock || "None"}
|
|
161
200
|
</MemoryContext>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
userId,
|
|
168
|
-
agentId,
|
|
169
|
-
sessionId,
|
|
170
|
-
sessionKey,
|
|
171
|
-
systemLen: systemBlock.length,
|
|
172
|
-
userLen: userContextBlock.length,
|
|
173
|
-
});
|
|
174
|
-
// At debug level, log the full snapshot data
|
|
175
|
-
logger.debug("Full snapshot data", {
|
|
176
|
-
systemBlock,
|
|
177
|
-
userContextBlock,
|
|
178
|
-
rawData: data,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
183
|
-
systemPromptToAdd = "";
|
|
184
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch (e) {
|
|
188
|
-
logger.warn("Failed to fetch snapshot", e);
|
|
189
|
-
systemPromptToAdd = "";
|
|
190
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
logger.debug("Using cached snapshot for session", { sessionKey });
|
|
195
|
-
}
|
|
196
|
-
if (!systemPromptToAdd) {
|
|
197
|
-
return Object.assign(Object.assign({}, params), { messages: incomingMessages });
|
|
198
|
-
}
|
|
199
|
-
const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(params, incomingMessages, systemPromptToAdd);
|
|
200
|
-
logger.info("Injecting memory system prompt", {
|
|
201
|
-
sessionKey,
|
|
202
|
-
promptLength: systemPromptToAdd.length,
|
|
203
|
-
});
|
|
204
|
-
logger.debug("Injected prompt content", { systemPromptToAdd });
|
|
205
|
-
return Object.assign(Object.assign({}, nextParams), { prompt: messagesWithMemory });
|
|
206
|
-
},
|
|
207
|
-
async wrapGenerate({ doGenerate, params }) {
|
|
208
|
-
var _a;
|
|
209
|
-
const result = await doGenerate();
|
|
210
|
-
if (userId && sessionId) {
|
|
211
|
-
const messagesInput = params.messages || params.prompt || [];
|
|
212
|
-
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
213
|
-
const assistantMessage = (result === null || result === void 0 ? void 0 : result.text)
|
|
214
|
-
? [{ role: "assistant", content: [{ type: "text", text: result.text }] }]
|
|
215
|
-
: [];
|
|
216
|
-
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
217
|
-
? resultMessages
|
|
218
|
-
: [...messagesInput, ...assistantMessage];
|
|
219
|
-
logConversation({
|
|
201
|
+
`.trim()
|
|
202
|
+
: "";
|
|
203
|
+
// Cache the snapshot for this session
|
|
204
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
205
|
+
logger.info("Snapshot fetched and cached", {
|
|
220
206
|
userId,
|
|
221
|
-
|
|
207
|
+
projectId,
|
|
222
208
|
sessionId,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
209
|
+
sessionKey,
|
|
210
|
+
systemLen: systemBlock.length,
|
|
211
|
+
userLen: userContextBlock.length,
|
|
212
|
+
});
|
|
213
|
+
// At debug level, log the full snapshot data
|
|
214
|
+
logger.debug("Full snapshot data", {
|
|
215
|
+
systemBlock,
|
|
216
|
+
userContextBlock,
|
|
217
|
+
rawData: data,
|
|
218
|
+
});
|
|
226
219
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const result = await doStream();
|
|
232
|
-
if (userId && sessionId) {
|
|
233
|
-
const messagesInput = params.messages || params.prompt || [];
|
|
234
|
-
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
235
|
-
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
236
|
-
? resultMessages
|
|
237
|
-
: messagesInput;
|
|
238
|
-
logConversation({
|
|
239
|
-
userId,
|
|
240
|
-
agentId,
|
|
241
|
-
sessionId,
|
|
242
|
-
messages: finalMessages,
|
|
243
|
-
modelId,
|
|
244
|
-
}).then(() => triggerProcessing(userId, agentId, sessionId));
|
|
220
|
+
else {
|
|
221
|
+
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
222
|
+
systemPromptToAdd = "";
|
|
223
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
245
224
|
}
|
|
246
|
-
return result;
|
|
247
225
|
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
logger.warn("Failed to fetch snapshot", e);
|
|
228
|
+
systemPromptToAdd = "";
|
|
229
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
logger.debug("Using cached snapshot for session", { sessionKey });
|
|
234
|
+
}
|
|
235
|
+
if (!systemPromptToAdd) {
|
|
236
|
+
return params;
|
|
237
|
+
}
|
|
238
|
+
const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(params, incomingMessages, systemPromptToAdd);
|
|
239
|
+
logger.info("Injecting memory system prompt", {
|
|
240
|
+
sessionKey,
|
|
241
|
+
promptLength: systemPromptToAdd.length,
|
|
242
|
+
});
|
|
243
|
+
logger.debug("Injected prompt content", { systemPromptToAdd });
|
|
244
|
+
return Object.assign(Object.assign({}, nextParams), { prompt: messagesWithMemory });
|
|
245
|
+
},
|
|
246
|
+
async wrapGenerate({ doGenerate, params }) {
|
|
247
|
+
var _a, _b;
|
|
248
|
+
let result;
|
|
249
|
+
try {
|
|
250
|
+
result = await doGenerate();
|
|
248
251
|
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
logger.error("doGenerate failed", err);
|
|
254
|
+
logger.error("doGenerate params.prompt", JSON.stringify((_a = params.prompt) === null || _a === void 0 ? void 0 : _a.map((m) => ({ role: m.role, contentType: typeof m.content, contentLength: Array.isArray(m.content) ? m.content.length : undefined })), null, 2));
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
if (userId && sessionId) {
|
|
258
|
+
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
259
|
+
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
260
|
+
const messagesInput = params.messages || params.prompt || [];
|
|
261
|
+
const resultMessages = (_b = result === null || result === void 0 ? void 0 : result.response) === null || _b === void 0 ? void 0 : _b.messages;
|
|
262
|
+
const assistantMessage = (result === null || result === void 0 ? void 0 : result.text)
|
|
263
|
+
? [{ role: "assistant", content: [{ type: "text", text: result.text }] }]
|
|
264
|
+
: [];
|
|
265
|
+
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
266
|
+
? resultMessages
|
|
267
|
+
: [...messagesInput, ...assistantMessage];
|
|
268
|
+
logConversation(Object.assign({ userId,
|
|
269
|
+
projectId,
|
|
270
|
+
sessionId, messages: finalMessages, modelId, usage: result.usage }, (promptMeta && {
|
|
271
|
+
promptSlug: promptMeta.promptSlug,
|
|
272
|
+
promptVersion: promptMeta.promptVersion,
|
|
273
|
+
promptId: promptMeta.promptId,
|
|
274
|
+
}))).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
},
|
|
278
|
+
async wrapStream({ doStream, params }) {
|
|
279
|
+
var _a;
|
|
280
|
+
let result;
|
|
281
|
+
try {
|
|
282
|
+
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
283
|
+
result = await doStream();
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.log(err.cause);
|
|
287
|
+
console.log(err.stack);
|
|
288
|
+
logger.error("doStream failed", err);
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
if (userId && sessionId) {
|
|
292
|
+
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
293
|
+
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
294
|
+
const messagesInput = params.messages || params.prompt || [];
|
|
295
|
+
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
296
|
+
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
297
|
+
? resultMessages
|
|
298
|
+
: messagesInput;
|
|
299
|
+
let streamUsage;
|
|
300
|
+
let accumulatedText = '';
|
|
301
|
+
const originalStream = result.stream;
|
|
302
|
+
const transformStream = new TransformStream({
|
|
303
|
+
transform(chunk, controller) {
|
|
304
|
+
if (chunk.type === 'text-delta') {
|
|
305
|
+
accumulatedText += chunk.delta;
|
|
306
|
+
}
|
|
307
|
+
if (chunk.type === 'finish' && chunk.usage) {
|
|
308
|
+
streamUsage = chunk.usage;
|
|
309
|
+
}
|
|
310
|
+
controller.enqueue(chunk);
|
|
311
|
+
},
|
|
312
|
+
flush() {
|
|
313
|
+
const allMessages = accumulatedText
|
|
314
|
+
? [...finalMessages, { role: "assistant", content: [{ type: "text", text: accumulatedText }] }]
|
|
315
|
+
: finalMessages;
|
|
316
|
+
logConversation(Object.assign({ userId,
|
|
317
|
+
projectId,
|
|
318
|
+
sessionId, messages: allMessages, modelId, usage: streamUsage }, (promptMeta && {
|
|
319
|
+
promptSlug: promptMeta.promptSlug,
|
|
320
|
+
promptVersion: promptMeta.promptVersion,
|
|
321
|
+
promptId: promptMeta.promptId,
|
|
322
|
+
}))).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
result.stream = originalStream.pipeThrough(transformStream);
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
const resolveModel = (originalModel, gatewaySlug) => {
|
|
331
|
+
if (!gatewaySlug || !clConfig.providerFactory) {
|
|
332
|
+
if (gatewaySlug && !clConfig.providerFactory) {
|
|
333
|
+
logger.warn("Gateway config found but no providerFactory provided");
|
|
334
|
+
}
|
|
335
|
+
return originalModel;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const gatewayURL = `${baseUrl}/api/cognitive/gateway/${gatewaySlug}`;
|
|
339
|
+
const modelId = originalModel.modelId || 'default';
|
|
340
|
+
const rawModel = clConfig.providerFactory(gatewayURL)(modelId);
|
|
341
|
+
const session = originalModel[SESSION_KEY];
|
|
342
|
+
if (!session)
|
|
343
|
+
return rawModel;
|
|
344
|
+
const wrapped = (0, ai_1.wrapLanguageModel)({
|
|
345
|
+
model: rawModel,
|
|
346
|
+
middleware: buildMiddleware(session.userId, session.projectId, session.sessionId, modelId),
|
|
347
|
+
});
|
|
348
|
+
wrapped[SESSION_KEY] = session;
|
|
349
|
+
return wrapped;
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
logger.error("Failed to create gateway model, falling back to original", err);
|
|
353
|
+
return originalModel;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const clWrapper = (modelId, settings, providerOptions) => {
|
|
357
|
+
// Pass provider options through to the underlying provider
|
|
358
|
+
const model = (providerOptions
|
|
359
|
+
? provider(modelId, providerOptions)
|
|
360
|
+
: provider(modelId));
|
|
361
|
+
const userId = settings === null || settings === void 0 ? void 0 : settings.userId;
|
|
362
|
+
const projectId = (settings === null || settings === void 0 ? void 0 : settings.projectId) || clConfig.projectId || "default";
|
|
363
|
+
const sessionId = settings === null || settings === void 0 ? void 0 : settings.sessionId;
|
|
364
|
+
const sessionMissing = !!userId && !sessionId;
|
|
365
|
+
if (sessionMissing) {
|
|
366
|
+
logger.warn("sessionId is required to log and process memories; skipping logging until provided.");
|
|
367
|
+
}
|
|
368
|
+
const wrappedModel = (0, ai_1.wrapLanguageModel)({
|
|
369
|
+
model: model,
|
|
370
|
+
middleware: buildMiddleware(userId, projectId, sessionId, modelId),
|
|
371
|
+
});
|
|
372
|
+
// Track session settings on the model for use in cl.streamText/cl.generateText
|
|
373
|
+
if (userId && sessionId) {
|
|
374
|
+
wrappedModel[SESSION_KEY] = { userId, projectId, sessionId };
|
|
375
|
+
}
|
|
376
|
+
return wrappedModel;
|
|
377
|
+
};
|
|
378
|
+
const clStreamText = async (options) => {
|
|
379
|
+
const { prompt: promptConfig } = options, rest = __rest(options, ["prompt"]);
|
|
380
|
+
// Resolve and interpolate prompt
|
|
381
|
+
const resolved = await resolvePrompt(promptConfig.slug);
|
|
382
|
+
const system = promptConfig.variables
|
|
383
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
384
|
+
: resolved.content;
|
|
385
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
386
|
+
const session = options.model[SESSION_KEY];
|
|
387
|
+
if (session) {
|
|
388
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
389
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
390
|
+
promptSlug: resolved.slug,
|
|
391
|
+
promptVersion: resolved.version,
|
|
392
|
+
promptId: resolved.promptId,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
logger.info("cl.streamText called", {
|
|
396
|
+
slug: promptConfig.slug,
|
|
397
|
+
version: resolved.version,
|
|
398
|
+
systemLength: system.length,
|
|
399
|
+
});
|
|
400
|
+
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
401
|
+
return (0, ai_1.streamText)(Object.assign(Object.assign({}, rest), { model, system }));
|
|
402
|
+
};
|
|
403
|
+
const clGenerateText = async (options) => {
|
|
404
|
+
const { prompt: promptConfig } = options, rest = __rest(options, ["prompt"]);
|
|
405
|
+
// Resolve and interpolate prompt
|
|
406
|
+
const resolved = await resolvePrompt(promptConfig.slug);
|
|
407
|
+
const system = promptConfig.variables
|
|
408
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
409
|
+
: resolved.content;
|
|
410
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
411
|
+
const session = options.model[SESSION_KEY];
|
|
412
|
+
if (session) {
|
|
413
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
414
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
415
|
+
promptSlug: resolved.slug,
|
|
416
|
+
promptVersion: resolved.version,
|
|
417
|
+
promptId: resolved.promptId,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
logger.info("cl.generateText called", {
|
|
421
|
+
slug: promptConfig.slug,
|
|
422
|
+
version: resolved.version,
|
|
423
|
+
systemLength: system.length,
|
|
249
424
|
});
|
|
425
|
+
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
426
|
+
return (0, ai_1.generateText)(Object.assign(Object.assign({}, rest), { model, system }));
|
|
250
427
|
};
|
|
428
|
+
// Return the model wrapper function with streamText/generateText attached
|
|
429
|
+
return Object.assign(clWrapper, {
|
|
430
|
+
streamText: clStreamText,
|
|
431
|
+
generateText: clGenerateText,
|
|
432
|
+
resolvePrompt,
|
|
433
|
+
logConversation,
|
|
434
|
+
triggerProcessing,
|
|
435
|
+
clearPromptCache: () => promptCache.clear(),
|
|
436
|
+
clearSessionCache: (sessionKey) => {
|
|
437
|
+
if (sessionKey) {
|
|
438
|
+
sessionSnapshots.delete(sessionKey);
|
|
439
|
+
sessionPromptMetadata.delete(sessionKey);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
sessionSnapshots.clear();
|
|
443
|
+
sessionPromptMetadata.clear();
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
251
447
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kognitivedev/vercel-ai-provider",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc -w",
|
|
12
|
+
"test": "vitest run",
|
|
12
13
|
"prepublishOnly": "npm run build"
|
|
13
14
|
},
|
|
14
15
|
"peerDependencies": {
|
|
15
|
-
"ai": "^
|
|
16
|
+
"ai": "^5.0.0 || ^6.0.0"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"typescript": "^5.0.0",
|
|
19
|
-
"ai": "
|
|
20
|
-
"@types/node": "^20.0.0"
|
|
20
|
+
"ai": "^6.0.0",
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"vitest": "^3.0.0"
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createCognitiveLayer } from "../index";
|
|
3
|
+
import { streamText } from "ai";
|
|
4
|
+
import { MockLanguageModelV3, convertArrayToReadableStream } from "ai/test";
|
|
5
|
+
|
|
6
|
+
describe("wrapStream logging", () => {
|
|
7
|
+
let fetchCalls: { url: string; body: any }[];
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
fetchCalls = [];
|
|
11
|
+
|
|
12
|
+
vi.stubGlobal(
|
|
13
|
+
"fetch",
|
|
14
|
+
vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
15
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
16
|
+
|
|
17
|
+
if (urlStr.includes("/api/cognitive/snapshot")) {
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({ systemBlock: "", userContextBlock: "" }),
|
|
20
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (urlStr.includes("/api/cognitive/log")) {
|
|
25
|
+
const body = JSON.parse(init?.body as string);
|
|
26
|
+
fetchCalls.push({ url: urlStr, body });
|
|
27
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (urlStr.includes("/api/cognitive/process")) {
|
|
31
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new Response("not found", { status: 404 });
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should include assistant message in logged conversation after streaming", async () => {
|
|
40
|
+
const mockModel = new MockLanguageModelV3({
|
|
41
|
+
doStream: async () => ({
|
|
42
|
+
stream: convertArrayToReadableStream([
|
|
43
|
+
{ type: "text-start" as const, id: "t1" },
|
|
44
|
+
{ type: "text-delta" as const, id: "t1", delta: "Hello" },
|
|
45
|
+
{ type: "text-delta" as const, id: "t1", delta: " world" },
|
|
46
|
+
{ type: "text-end" as const, id: "t1" },
|
|
47
|
+
{
|
|
48
|
+
type: "finish" as const,
|
|
49
|
+
finishReason: {
|
|
50
|
+
unified: "stop" as const,
|
|
51
|
+
raw: undefined,
|
|
52
|
+
},
|
|
53
|
+
usage: {
|
|
54
|
+
inputTokens: { total: 10, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
55
|
+
outputTokens: { total: 5, text: undefined, reasoning: undefined },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
] satisfies import("@ai-sdk/provider").LanguageModelV3StreamPart[]),
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const mockProvider = () => mockModel;
|
|
63
|
+
|
|
64
|
+
const cl = createCognitiveLayer({
|
|
65
|
+
provider: mockProvider,
|
|
66
|
+
clConfig: {
|
|
67
|
+
apiKey: "test-api-key",
|
|
68
|
+
appId: "test-app",
|
|
69
|
+
projectId: "test-project",
|
|
70
|
+
processDelayMs: 0,
|
|
71
|
+
logLevel: "none",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const model = cl("mock-model", {
|
|
76
|
+
userId: "user-1",
|
|
77
|
+
projectId: "project-1",
|
|
78
|
+
sessionId: "session-1",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = streamText({
|
|
82
|
+
model,
|
|
83
|
+
messages: [{ role: "user", content: "Hi" }],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Fully consume the stream
|
|
87
|
+
const text = await result.text;
|
|
88
|
+
expect(text).toBe("Hello world");
|
|
89
|
+
|
|
90
|
+
// Wait for async logConversation to complete
|
|
91
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
92
|
+
|
|
93
|
+
// Find the log call
|
|
94
|
+
const logCall = fetchCalls.find((c) => c.url.includes("/api/cognitive/log"));
|
|
95
|
+
expect(logCall).toBeDefined();
|
|
96
|
+
|
|
97
|
+
const messages = logCall!.body.messages;
|
|
98
|
+
const assistantMsg = messages.find((m: any) => m.role === "assistant");
|
|
99
|
+
expect(assistantMsg).toBeDefined();
|
|
100
|
+
expect(assistantMsg.content).toEqual([
|
|
101
|
+
{ type: "text", text: "Hello world" },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|