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