@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/src/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
wrapLanguageModel,
|
|
3
|
+
streamText as aiStreamText,
|
|
4
|
+
generateText as aiGenerateText,
|
|
5
|
+
type LanguageModel,
|
|
6
|
+
} from "ai";
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Log levels for controlling verbosity of CognitiveLayer logging.
|
|
@@ -50,10 +54,10 @@ function createLogger(logLevel: LogLevel) {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export interface CognitiveLayerConfig {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
apiKey: string;
|
|
58
|
+
appId?: string;
|
|
59
|
+
projectId?: string;
|
|
55
60
|
baseUrl?: string;
|
|
56
|
-
apiKey?: string;
|
|
57
61
|
/**
|
|
58
62
|
* Delay in milliseconds before triggering memory processing after a response.
|
|
59
63
|
* Set to 0 to disable automatic processing.
|
|
@@ -62,21 +66,77 @@ export interface CognitiveLayerConfig {
|
|
|
62
66
|
processDelayMs?: number;
|
|
63
67
|
/**
|
|
64
68
|
* Log level for controlling verbosity of CognitiveLayer logging.
|
|
65
|
-
* - 'none': No logging
|
|
66
|
-
* - 'error': Only errors
|
|
67
|
-
* - 'warn': Errors and warnings
|
|
68
|
-
* - 'info': Errors, warnings, and info messages
|
|
69
|
-
* - 'debug': All messages including detailed snapshot data
|
|
70
69
|
* Default: 'info'
|
|
71
70
|
*/
|
|
72
71
|
logLevel?: LogLevel;
|
|
72
|
+
/**
|
|
73
|
+
* Factory for creating a provider that routes through a gateway URL.
|
|
74
|
+
*/
|
|
75
|
+
providerFactory?: (baseURL: string) => (modelId: string) => LanguageModel;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export type CLModelWrapper = (
|
|
76
79
|
modelId: string,
|
|
77
|
-
settings?: { userId?: string;
|
|
80
|
+
settings?: { userId?: string; projectId?: string; sessionId?: string },
|
|
78
81
|
providerOptions?: Record<string, unknown>
|
|
79
|
-
) =>
|
|
82
|
+
) => LanguageModel;
|
|
83
|
+
|
|
84
|
+
export interface PromptConfig {
|
|
85
|
+
slug: string;
|
|
86
|
+
variables?: Record<string, string>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type CLStreamTextOptions = Omit<Parameters<typeof aiStreamText>[0], 'system' | 'prompt'> & {
|
|
90
|
+
prompt: PromptConfig;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type CLGenerateTextOptions = Omit<Parameters<typeof aiGenerateText>[0], 'system' | 'prompt'> & {
|
|
94
|
+
prompt: PromptConfig;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export interface LogConversationPayload {
|
|
98
|
+
userId: string;
|
|
99
|
+
projectId: string;
|
|
100
|
+
sessionId: string;
|
|
101
|
+
messages: any[];
|
|
102
|
+
memorySystemPrompt?: string;
|
|
103
|
+
modelId?: string;
|
|
104
|
+
usage?: Record<string, unknown>;
|
|
105
|
+
promptSlug?: string;
|
|
106
|
+
promptVersion?: number;
|
|
107
|
+
promptId?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type CognitiveLayer = CLModelWrapper & {
|
|
111
|
+
streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
|
|
112
|
+
generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
|
|
113
|
+
resolvePrompt: (slug: string) => Promise<CachedPrompt>;
|
|
114
|
+
logConversation: (payload: LogConversationPayload) => Promise<void>;
|
|
115
|
+
triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
|
|
116
|
+
clearPromptCache: () => void;
|
|
117
|
+
clearSessionCache: (sessionKey?: string) => void;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ─── Prompt Cache ────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export interface CachedPrompt {
|
|
123
|
+
promptId: string;
|
|
124
|
+
slug: string;
|
|
125
|
+
version: number;
|
|
126
|
+
content: string;
|
|
127
|
+
fetchedAt: number;
|
|
128
|
+
gatewaySlug?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const PROMPT_CACHE_TTL_MS = 60_000; // 1 minute
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Interpolate {{variable}} placeholders in a template string.
|
|
135
|
+
* Unmatched variables are left as-is.
|
|
136
|
+
*/
|
|
137
|
+
function interpolateTemplate(content: string, variables: Record<string, string>): string {
|
|
138
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? `{{${key}}}`);
|
|
139
|
+
}
|
|
80
140
|
|
|
81
141
|
// Session-scoped snapshot cache: sessionKey → formatted memory block
|
|
82
142
|
const sessionSnapshots = new Map<string, string>();
|
|
@@ -84,6 +144,12 @@ const sessionSnapshots = new Map<string, string>();
|
|
|
84
144
|
// Regex to detect if memory has already been injected
|
|
85
145
|
const MEMORY_TAG_REGEX = /<MemoryContext>/i;
|
|
86
146
|
|
|
147
|
+
// Symbol-keyed property to track session settings on model objects
|
|
148
|
+
const SESSION_KEY = Symbol.for("cl:session");
|
|
149
|
+
|
|
150
|
+
// Session key → prompt metadata (populated by cl.streamText/cl.generateText, read by middleware)
|
|
151
|
+
const sessionPromptMetadata = new Map<string, { promptSlug: string; promptVersion: number; promptId: string }>();
|
|
152
|
+
|
|
87
153
|
/**
|
|
88
154
|
* Check if any system message already contains a <MemoryContext> block.
|
|
89
155
|
*/
|
|
@@ -105,7 +171,7 @@ function hasExistingMemoryInjection(messages: any[]): boolean {
|
|
|
105
171
|
export function createCognitiveLayer(config: {
|
|
106
172
|
provider: any;
|
|
107
173
|
clConfig: CognitiveLayerConfig;
|
|
108
|
-
}):
|
|
174
|
+
}): CognitiveLayer {
|
|
109
175
|
const { provider, clConfig } = config;
|
|
110
176
|
const baseUrl = clConfig.baseUrl || "http://localhost:3001";
|
|
111
177
|
// Default to 500ms delay to allow DB writes to settle
|
|
@@ -114,18 +180,48 @@ export function createCognitiveLayer(config: {
|
|
|
114
180
|
const logLevel = clConfig.logLevel || 'info';
|
|
115
181
|
const logger = createLogger(logLevel);
|
|
116
182
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
183
|
+
const authHeaders = {
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
"Authorization": `Bearer ${clConfig.apiKey}`,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Prompt cache: slug → CachedPrompt
|
|
189
|
+
const promptCache = new Map<string, CachedPrompt>();
|
|
190
|
+
|
|
191
|
+
const resolvePrompt = async (slug: string): Promise<CachedPrompt> => {
|
|
192
|
+
const cached = promptCache.get(slug);
|
|
193
|
+
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
194
|
+
logger.debug("Using cached prompt", { slug, version: cached.version });
|
|
195
|
+
return cached;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const res = await fetch(`${baseUrl}/api/cognitive/prompt?slug=${encodeURIComponent(slug)}`, {
|
|
199
|
+
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const body = await res.text();
|
|
203
|
+
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const data = await res.json();
|
|
207
|
+
const entry: CachedPrompt = {
|
|
208
|
+
promptId: data.promptId,
|
|
209
|
+
slug: data.slug,
|
|
210
|
+
version: data.version,
|
|
211
|
+
content: data.content,
|
|
212
|
+
fetchedAt: Date.now(),
|
|
213
|
+
gatewaySlug: data.gatewaySlug,
|
|
214
|
+
};
|
|
215
|
+
promptCache.set(slug, entry);
|
|
216
|
+
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
217
|
+
return entry;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const logConversation = async (payload: LogConversationPayload) => {
|
|
125
221
|
try {
|
|
126
222
|
await fetch(`${baseUrl}/api/cognitive/log`, {
|
|
127
223
|
method: "POST",
|
|
128
|
-
headers:
|
|
224
|
+
headers: authHeaders,
|
|
129
225
|
body: JSON.stringify({
|
|
130
226
|
...payload,
|
|
131
227
|
type: "conversation",
|
|
@@ -137,12 +233,12 @@ export function createCognitiveLayer(config: {
|
|
|
137
233
|
}
|
|
138
234
|
};
|
|
139
235
|
|
|
140
|
-
const triggerProcessing = (userId: string,
|
|
236
|
+
const triggerProcessing = (userId: string, projectId: string, sessionId: string) => {
|
|
141
237
|
const run = () => {
|
|
142
238
|
fetch(`${baseUrl}/api/cognitive/process`, {
|
|
143
239
|
method: "POST",
|
|
144
|
-
headers:
|
|
145
|
-
body: JSON.stringify({ userId,
|
|
240
|
+
headers: authHeaders,
|
|
241
|
+
body: JSON.stringify({ userId, sessionId }),
|
|
146
242
|
}).catch(e => logger.error("Process trigger failed", e));
|
|
147
243
|
};
|
|
148
244
|
|
|
@@ -157,185 +253,358 @@ export function createCognitiveLayer(config: {
|
|
|
157
253
|
params: any,
|
|
158
254
|
incomingMessages: any[],
|
|
159
255
|
memoryPrompt: string
|
|
160
|
-
): { nextParams: any; messages: any[]; mode: "overwrite-
|
|
256
|
+
): { nextParams: any; messages: any[]; mode: "overwrite-first-system" | "prepend-system" } => {
|
|
161
257
|
const nextParams = { ...params };
|
|
162
258
|
|
|
163
|
-
// 1) If
|
|
164
|
-
if (nextParams.system) {
|
|
165
|
-
nextParams.system = memoryPrompt;
|
|
166
|
-
return { nextParams, messages: incomingMessages, mode: "overwrite-param" };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 2) If first message is system, replace its content.
|
|
259
|
+
// 1) If first message is system, append memory to its content (without mutating original).
|
|
170
260
|
if (incomingMessages.length > 0 && incomingMessages[0]?.role === "system") {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
261
|
+
const original = incomingMessages[0];
|
|
262
|
+
const updatedContent =
|
|
263
|
+
typeof original.content === "string"
|
|
264
|
+
? original.content + "\n\n" + memoryPrompt
|
|
265
|
+
: memoryPrompt;
|
|
266
|
+
const updated = [{ ...original, content: updatedContent }, ...incomingMessages.slice(1)];
|
|
175
267
|
return { nextParams, messages: updated, mode: "overwrite-first-system" };
|
|
176
268
|
}
|
|
177
269
|
|
|
178
|
-
//
|
|
270
|
+
// 2) Otherwise prepend a system message.
|
|
179
271
|
const updated = [{ role: "system", content: memoryPrompt }, ...incomingMessages];
|
|
180
272
|
return { nextParams, messages: updated, mode: "prepend-system" };
|
|
181
273
|
};
|
|
182
274
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
) => {
|
|
188
|
-
// Pass provider options through to the underlying provider
|
|
189
|
-
const model = (
|
|
190
|
-
providerOptions
|
|
191
|
-
? provider(modelId, providerOptions)
|
|
192
|
-
: provider(modelId)
|
|
193
|
-
) as LanguageModelV2;
|
|
194
|
-
const userId = settings?.userId;
|
|
195
|
-
const agentId = settings?.agentId || clConfig.defaultAgentId || "default";
|
|
196
|
-
const sessionId = settings?.sessionId;
|
|
197
|
-
const sessionMissing = !!userId && !sessionId;
|
|
198
|
-
|
|
199
|
-
if (sessionMissing) {
|
|
200
|
-
logger.warn("sessionId is required to log and process memories; skipping logging until provided.");
|
|
201
|
-
}
|
|
275
|
+
const buildMiddleware = (userId: string | undefined, projectId: string, sessionId: string | undefined, modelId: string) => ({
|
|
276
|
+
specificationVersion: 'v3' as const,
|
|
277
|
+
async transformParams({ params }: { params: any }) {
|
|
278
|
+
if (!userId) return params;
|
|
202
279
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async transformParams({ params }) {
|
|
207
|
-
if (!userId) return params;
|
|
280
|
+
const incomingMessages = Array.isArray((params as any).prompt)
|
|
281
|
+
? (params as any).prompt
|
|
282
|
+
: [];
|
|
208
283
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
284
|
+
// 1) Check if memory is already injected in messages
|
|
285
|
+
if (hasExistingMemoryInjection(incomingMessages)) {
|
|
286
|
+
logger.debug("Memory already injected, skipping");
|
|
287
|
+
return params;
|
|
288
|
+
}
|
|
212
289
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return params;
|
|
217
|
-
}
|
|
290
|
+
// 2) Check session cache
|
|
291
|
+
const sessionKey = `${userId}:${projectId}:${sessionId || "default"}`;
|
|
292
|
+
let systemPromptToAdd = sessionSnapshots.get(sessionKey);
|
|
218
293
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
systemBlock !== "" || userContextBlock !== ""
|
|
234
|
-
? `
|
|
294
|
+
// 3) Fetch snapshot only if not cached
|
|
295
|
+
if (systemPromptToAdd === undefined) {
|
|
296
|
+
try {
|
|
297
|
+
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
298
|
+
const res = await fetch(url, {
|
|
299
|
+
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
300
|
+
});
|
|
301
|
+
if (res.ok) {
|
|
302
|
+
const data = await res.json();
|
|
303
|
+
const systemBlock = data.systemBlock || "";
|
|
304
|
+
const userContextBlock = data.userContextBlock || "";
|
|
305
|
+
systemPromptToAdd =
|
|
306
|
+
systemBlock !== "" || userContextBlock !== ""
|
|
307
|
+
? `
|
|
235
308
|
<MemoryContext>
|
|
236
309
|
Use the following memory to stay consistent. Prefer UserContext facts for answers; AgentHeuristics guide style, safety, and priorities.
|
|
237
310
|
${systemBlock || "None"}
|
|
238
311
|
${userContextBlock || "None"}
|
|
239
312
|
</MemoryContext>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} else {
|
|
261
|
-
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
262
|
-
systemPromptToAdd = "";
|
|
263
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
264
|
-
}
|
|
265
|
-
} catch (e) {
|
|
266
|
-
logger.warn("Failed to fetch snapshot", e);
|
|
267
|
-
systemPromptToAdd = "";
|
|
268
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
269
|
-
}
|
|
313
|
+
`.trim()
|
|
314
|
+
: "";
|
|
315
|
+
|
|
316
|
+
// Cache the snapshot for this session
|
|
317
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
318
|
+
|
|
319
|
+
logger.info("Snapshot fetched and cached", {
|
|
320
|
+
userId,
|
|
321
|
+
projectId,
|
|
322
|
+
sessionId,
|
|
323
|
+
sessionKey,
|
|
324
|
+
systemLen: systemBlock.length,
|
|
325
|
+
userLen: userContextBlock.length,
|
|
326
|
+
});
|
|
327
|
+
// At debug level, log the full snapshot data
|
|
328
|
+
logger.debug("Full snapshot data", {
|
|
329
|
+
systemBlock,
|
|
330
|
+
userContextBlock,
|
|
331
|
+
rawData: data,
|
|
332
|
+
});
|
|
270
333
|
} else {
|
|
271
|
-
logger.
|
|
334
|
+
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
335
|
+
systemPromptToAdd = "";
|
|
336
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
272
337
|
}
|
|
338
|
+
} catch (e) {
|
|
339
|
+
logger.warn("Failed to fetch snapshot", e);
|
|
340
|
+
systemPromptToAdd = "";
|
|
341
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
logger.debug("Using cached snapshot for session", { sessionKey });
|
|
345
|
+
}
|
|
273
346
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
347
|
+
if (!systemPromptToAdd) {
|
|
348
|
+
return params;
|
|
349
|
+
}
|
|
277
350
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
351
|
+
const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(
|
|
352
|
+
params,
|
|
353
|
+
incomingMessages,
|
|
354
|
+
systemPromptToAdd
|
|
355
|
+
);
|
|
283
356
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
357
|
+
logger.info("Injecting memory system prompt", {
|
|
358
|
+
sessionKey,
|
|
359
|
+
promptLength: systemPromptToAdd.length,
|
|
360
|
+
});
|
|
361
|
+
logger.debug("Injected prompt content", { systemPromptToAdd });
|
|
289
362
|
|
|
290
|
-
|
|
291
|
-
|
|
363
|
+
return { ...nextParams, prompt: messagesWithMemory };
|
|
364
|
+
},
|
|
292
365
|
|
|
293
|
-
|
|
294
|
-
|
|
366
|
+
async wrapGenerate({ doGenerate, params }: { doGenerate: any; params: any }) {
|
|
367
|
+
let result;
|
|
368
|
+
try {
|
|
369
|
+
result = await doGenerate();
|
|
370
|
+
} catch (err) {
|
|
371
|
+
logger.error("doGenerate failed", err);
|
|
372
|
+
logger.error("doGenerate params.prompt", JSON.stringify((params as any).prompt?.map((m: any) => ({ role: m.role, contentType: typeof m.content, contentLength: Array.isArray(m.content) ? m.content.length : undefined })), null, 2));
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
295
375
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
376
|
+
if (userId && sessionId) {
|
|
377
|
+
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
378
|
+
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
379
|
+
|
|
380
|
+
const messagesInput = (params as any).messages || (params as any).prompt || [];
|
|
381
|
+
const resultMessages = (result as any)?.response?.messages;
|
|
382
|
+
const assistantMessage = (result as any)?.text
|
|
383
|
+
? [{ role: "assistant", content: [{ type: "text", text: (result as any).text }] }]
|
|
384
|
+
: [];
|
|
385
|
+
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
386
|
+
? resultMessages
|
|
387
|
+
: [...messagesInput, ...assistantMessage];
|
|
388
|
+
|
|
389
|
+
logConversation({
|
|
390
|
+
userId,
|
|
391
|
+
projectId,
|
|
392
|
+
sessionId,
|
|
393
|
+
messages: finalMessages,
|
|
394
|
+
modelId,
|
|
395
|
+
usage: result.usage,
|
|
396
|
+
...(promptMeta && {
|
|
397
|
+
promptSlug: promptMeta.promptSlug,
|
|
398
|
+
promptVersion: promptMeta.promptVersion,
|
|
399
|
+
promptId: promptMeta.promptId,
|
|
400
|
+
}),
|
|
401
|
+
}).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
402
|
+
}
|
|
305
403
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
404
|
+
return result;
|
|
405
|
+
},
|
|
406
|
+
async wrapStream({ doStream, params }: { doStream: any; params: any }) {
|
|
407
|
+
let result;
|
|
408
|
+
try {
|
|
409
|
+
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
410
|
+
result = await doStream();
|
|
411
|
+
} catch (err) {
|
|
412
|
+
console.log((err as TypeError).cause)
|
|
413
|
+
console.log((err as TypeError).stack)
|
|
414
|
+
logger.error("doStream failed", err);
|
|
415
|
+
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (userId && sessionId) {
|
|
420
|
+
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
421
|
+
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
314
422
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
423
|
+
const messagesInput = (params as any).messages || (params as any).prompt || [];
|
|
424
|
+
const resultMessages = (result as any)?.response?.messages;
|
|
425
|
+
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
426
|
+
? resultMessages
|
|
427
|
+
: messagesInput;
|
|
319
428
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
429
|
+
let streamUsage: Record<string, unknown> | undefined;
|
|
430
|
+
let accumulatedText = '';
|
|
431
|
+
|
|
432
|
+
const originalStream = result.stream;
|
|
433
|
+
const transformStream = new TransformStream({
|
|
434
|
+
transform(chunk, controller) {
|
|
435
|
+
if (chunk.type === 'text-delta') {
|
|
436
|
+
accumulatedText += chunk.delta;
|
|
437
|
+
}
|
|
438
|
+
if (chunk.type === 'finish' && chunk.usage) {
|
|
439
|
+
streamUsage = chunk.usage;
|
|
440
|
+
}
|
|
441
|
+
controller.enqueue(chunk);
|
|
442
|
+
},
|
|
443
|
+
flush() {
|
|
444
|
+
const allMessages = accumulatedText
|
|
445
|
+
? [...finalMessages, { role: "assistant", content: [{ type: "text", text: accumulatedText }] }]
|
|
446
|
+
: finalMessages;
|
|
326
447
|
|
|
327
448
|
logConversation({
|
|
328
449
|
userId,
|
|
329
|
-
|
|
450
|
+
projectId,
|
|
330
451
|
sessionId,
|
|
331
|
-
messages:
|
|
452
|
+
messages: allMessages,
|
|
332
453
|
modelId,
|
|
333
|
-
|
|
454
|
+
usage: streamUsage,
|
|
455
|
+
...(promptMeta && {
|
|
456
|
+
promptSlug: promptMeta.promptSlug,
|
|
457
|
+
promptVersion: promptMeta.promptVersion,
|
|
458
|
+
promptId: promptMeta.promptId,
|
|
459
|
+
}),
|
|
460
|
+
}).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
334
461
|
}
|
|
462
|
+
});
|
|
335
463
|
|
|
336
|
-
|
|
337
|
-
|
|
464
|
+
result.stream = originalStream.pipeThrough(transformStream);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const resolveModel = (originalModel: LanguageModel, gatewaySlug?: string): LanguageModel => {
|
|
472
|
+
if (!gatewaySlug || !clConfig.providerFactory) {
|
|
473
|
+
if (gatewaySlug && !clConfig.providerFactory) {
|
|
474
|
+
logger.warn("Gateway config found but no providerFactory provided");
|
|
338
475
|
}
|
|
476
|
+
return originalModel;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const gatewayURL = `${baseUrl}/api/cognitive/gateway/${gatewaySlug}`;
|
|
481
|
+
const modelId = (originalModel as any).modelId || 'default';
|
|
482
|
+
const rawModel = clConfig.providerFactory(gatewayURL)(modelId);
|
|
483
|
+
|
|
484
|
+
const session = (originalModel as any)[SESSION_KEY];
|
|
485
|
+
if (!session) return rawModel as LanguageModel;
|
|
486
|
+
|
|
487
|
+
const wrapped = wrapLanguageModel({
|
|
488
|
+
model: rawModel as any,
|
|
489
|
+
middleware: buildMiddleware(session.userId, session.projectId, session.sessionId, modelId) as any,
|
|
490
|
+
});
|
|
491
|
+
(wrapped as any)[SESSION_KEY] = session;
|
|
492
|
+
return wrapped as LanguageModel;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
logger.error("Failed to create gateway model, falling back to original", err);
|
|
495
|
+
return originalModel;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const clWrapper: CLModelWrapper = (
|
|
500
|
+
modelId: string,
|
|
501
|
+
settings?: { userId?: string; projectId?: string; sessionId?: string },
|
|
502
|
+
providerOptions?: Record<string, unknown>
|
|
503
|
+
) => {
|
|
504
|
+
// Pass provider options through to the underlying provider
|
|
505
|
+
const model = (
|
|
506
|
+
providerOptions
|
|
507
|
+
? provider(modelId, providerOptions)
|
|
508
|
+
: provider(modelId)
|
|
509
|
+
) as LanguageModel;
|
|
510
|
+
const userId = settings?.userId;
|
|
511
|
+
const projectId = settings?.projectId || clConfig.projectId || "default";
|
|
512
|
+
const sessionId = settings?.sessionId;
|
|
513
|
+
const sessionMissing = !!userId && !sessionId;
|
|
514
|
+
|
|
515
|
+
if (sessionMissing) {
|
|
516
|
+
logger.warn("sessionId is required to log and process memories; skipping logging until provided.");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const wrappedModel = wrapLanguageModel({
|
|
520
|
+
model: model as any,
|
|
521
|
+
middleware: buildMiddleware(userId, projectId, sessionId, modelId) as any,
|
|
522
|
+
}) as LanguageModel;
|
|
523
|
+
|
|
524
|
+
// Track session settings on the model for use in cl.streamText/cl.generateText
|
|
525
|
+
if (userId && sessionId) {
|
|
526
|
+
(wrappedModel as any)[SESSION_KEY] = { userId, projectId, sessionId };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return wrappedModel;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const clStreamText = async (options: CLStreamTextOptions) => {
|
|
533
|
+
const { prompt: promptConfig, ...rest } = options;
|
|
534
|
+
|
|
535
|
+
// Resolve and interpolate prompt
|
|
536
|
+
const resolved = await resolvePrompt(promptConfig.slug);
|
|
537
|
+
const system = promptConfig.variables
|
|
538
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
539
|
+
: resolved.content;
|
|
540
|
+
|
|
541
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
542
|
+
const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId: string } | undefined;
|
|
543
|
+
if (session) {
|
|
544
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
545
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
546
|
+
promptSlug: resolved.slug,
|
|
547
|
+
promptVersion: resolved.version,
|
|
548
|
+
promptId: resolved.promptId,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
logger.info("cl.streamText called", {
|
|
553
|
+
slug: promptConfig.slug,
|
|
554
|
+
version: resolved.version,
|
|
555
|
+
systemLength: system.length,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
559
|
+
return aiStreamText({ ...rest, model, system } as any);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const clGenerateText = async (options: CLGenerateTextOptions) => {
|
|
563
|
+
const { prompt: promptConfig, ...rest } = options;
|
|
564
|
+
|
|
565
|
+
// Resolve and interpolate prompt
|
|
566
|
+
const resolved = await resolvePrompt(promptConfig.slug);
|
|
567
|
+
const system = promptConfig.variables
|
|
568
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
569
|
+
: resolved.content;
|
|
570
|
+
|
|
571
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
572
|
+
const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId: string } | undefined;
|
|
573
|
+
if (session) {
|
|
574
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
575
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
576
|
+
promptSlug: resolved.slug,
|
|
577
|
+
promptVersion: resolved.version,
|
|
578
|
+
promptId: resolved.promptId,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
logger.info("cl.generateText called", {
|
|
583
|
+
slug: promptConfig.slug,
|
|
584
|
+
version: resolved.version,
|
|
585
|
+
systemLength: system.length,
|
|
339
586
|
});
|
|
587
|
+
|
|
588
|
+
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
589
|
+
return aiGenerateText({ ...rest, model, system } as any);
|
|
340
590
|
};
|
|
591
|
+
|
|
592
|
+
// Return the model wrapper function with streamText/generateText attached
|
|
593
|
+
return Object.assign(clWrapper, {
|
|
594
|
+
streamText: clStreamText,
|
|
595
|
+
generateText: clGenerateText,
|
|
596
|
+
resolvePrompt,
|
|
597
|
+
logConversation,
|
|
598
|
+
triggerProcessing,
|
|
599
|
+
clearPromptCache: () => promptCache.clear(),
|
|
600
|
+
clearSessionCache: (sessionKey?: string) => {
|
|
601
|
+
if (sessionKey) {
|
|
602
|
+
sessionSnapshots.delete(sessionKey);
|
|
603
|
+
sessionPromptMetadata.delete(sessionKey);
|
|
604
|
+
} else {
|
|
605
|
+
sessionSnapshots.clear();
|
|
606
|
+
sessionPromptMetadata.clear();
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
});
|
|
341
610
|
}
|