@kognitivedev/vercel-ai-provider 0.1.4 → 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 -135
- package/package.json +6 -4
- package/src/__tests__/wrap-stream-logging.test.ts +104 -0
- package/src/index.ts +435 -164
- 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,183 +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
|
-
|
|
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)];
|
|
173
267
|
return { nextParams, messages: updated, mode: "overwrite-first-system" };
|
|
174
268
|
}
|
|
175
269
|
|
|
176
|
-
//
|
|
270
|
+
// 2) Otherwise prepend a system message.
|
|
177
271
|
const updated = [{ role: "system", content: memoryPrompt }, ...incomingMessages];
|
|
178
272
|
return { nextParams, messages: updated, mode: "prepend-system" };
|
|
179
273
|
};
|
|
180
274
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
) => {
|
|
186
|
-
// Pass provider options through to the underlying provider
|
|
187
|
-
const model = (
|
|
188
|
-
providerOptions
|
|
189
|
-
? provider(modelId, providerOptions)
|
|
190
|
-
: provider(modelId)
|
|
191
|
-
) as LanguageModelV2;
|
|
192
|
-
const userId = settings?.userId;
|
|
193
|
-
const agentId = settings?.agentId || clConfig.defaultAgentId || "default";
|
|
194
|
-
const sessionId = settings?.sessionId;
|
|
195
|
-
const sessionMissing = !!userId && !sessionId;
|
|
196
|
-
|
|
197
|
-
if (sessionMissing) {
|
|
198
|
-
logger.warn("sessionId is required to log and process memories; skipping logging until provided.");
|
|
199
|
-
}
|
|
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;
|
|
200
279
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
async transformParams({ params }) {
|
|
205
|
-
if (!userId) return params;
|
|
280
|
+
const incomingMessages = Array.isArray((params as any).prompt)
|
|
281
|
+
? (params as any).prompt
|
|
282
|
+
: [];
|
|
206
283
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
289
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return params;
|
|
215
|
-
}
|
|
290
|
+
// 2) Check session cache
|
|
291
|
+
const sessionKey = `${userId}:${projectId}:${sessionId || "default"}`;
|
|
292
|
+
let systemPromptToAdd = sessionSnapshots.get(sessionKey);
|
|
216
293
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
systemBlock !== "" || userContextBlock !== ""
|
|
232
|
-
? `
|
|
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
|
+
? `
|
|
233
308
|
<MemoryContext>
|
|
234
309
|
Use the following memory to stay consistent. Prefer UserContext facts for answers; AgentHeuristics guide style, safety, and priorities.
|
|
235
310
|
${systemBlock || "None"}
|
|
236
311
|
${userContextBlock || "None"}
|
|
237
312
|
</MemoryContext>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} else {
|
|
259
|
-
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
260
|
-
systemPromptToAdd = "";
|
|
261
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
262
|
-
}
|
|
263
|
-
} catch (e) {
|
|
264
|
-
logger.warn("Failed to fetch snapshot", e);
|
|
265
|
-
systemPromptToAdd = "";
|
|
266
|
-
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
267
|
-
}
|
|
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
|
+
});
|
|
268
333
|
} else {
|
|
269
|
-
logger.
|
|
334
|
+
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
335
|
+
systemPromptToAdd = "";
|
|
336
|
+
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
270
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
|
+
}
|
|
271
346
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
347
|
+
if (!systemPromptToAdd) {
|
|
348
|
+
return params;
|
|
349
|
+
}
|
|
275
350
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
351
|
+
const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(
|
|
352
|
+
params,
|
|
353
|
+
incomingMessages,
|
|
354
|
+
systemPromptToAdd
|
|
355
|
+
);
|
|
281
356
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
357
|
+
logger.info("Injecting memory system prompt", {
|
|
358
|
+
sessionKey,
|
|
359
|
+
promptLength: systemPromptToAdd.length,
|
|
360
|
+
});
|
|
361
|
+
logger.debug("Injected prompt content", { systemPromptToAdd });
|
|
287
362
|
|
|
288
|
-
|
|
289
|
-
|
|
363
|
+
return { ...nextParams, prompt: messagesWithMemory };
|
|
364
|
+
},
|
|
290
365
|
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
375
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
403
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
422
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
317
428
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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;
|
|
324
447
|
|
|
325
448
|
logConversation({
|
|
326
449
|
userId,
|
|
327
|
-
|
|
450
|
+
projectId,
|
|
328
451
|
sessionId,
|
|
329
|
-
messages:
|
|
452
|
+
messages: allMessages,
|
|
330
453
|
modelId,
|
|
331
|
-
|
|
454
|
+
usage: streamUsage,
|
|
455
|
+
...(promptMeta && {
|
|
456
|
+
promptSlug: promptMeta.promptSlug,
|
|
457
|
+
promptVersion: promptMeta.promptVersion,
|
|
458
|
+
promptId: promptMeta.promptId,
|
|
459
|
+
}),
|
|
460
|
+
}).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
332
461
|
}
|
|
462
|
+
});
|
|
333
463
|
|
|
334
|
-
|
|
335
|
-
|
|
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");
|
|
336
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,
|
|
337
586
|
});
|
|
587
|
+
|
|
588
|
+
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
589
|
+
return aiGenerateText({ ...rest, model, system } as any);
|
|
338
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
|
+
});
|
|
339
610
|
}
|