@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/src/index.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { wrapLanguageModel } from "ai";
2
- import { LanguageModelV2 } from "@ai-sdk/provider";
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
- appId: string;
54
- defaultAgentId?: string;
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; agentId?: string; sessionId?: string },
86
+ settings?: { userId?: string; projectId?: string; sessionId?: string },
78
87
  providerOptions?: Record<string, unknown>
79
- ) => LanguageModelV2;
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
- }): CLModelWrapper {
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 logConversation = async (payload: {
118
- userId: string;
119
- agentId: string;
120
- sessionId: string;
121
- messages: any[];
122
- memorySystemPrompt?: string;
123
- modelId?: string;
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: { "Content-Type": "application/json" },
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, agentId: string, sessionId: 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: { "Content-Type": "application/json" },
145
- body: JSON.stringify({ userId, agentId, sessionId }),
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-param" | "overwrite-first-system" | "prepend-system" } => {
262
+ ): { nextParams: any; messages: any[]; mode: "overwrite-first-system" | "prepend-system" } => {
161
263
  const nextParams = { ...params };
162
264
 
163
- // 1) If caller provided a top-level system prompt, overwrite it.
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 updated = [...incomingMessages];
172
- let systemMessage = updated[0];
173
- if(typeof systemMessage.content === "string") systemMessage.content = systemMessage + "\n\n" + memoryPrompt;
174
- updated[0] = { ...updated[0], ...systemMessage };
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
- // 3) Otherwise prepend a system message.
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
- return (
184
- modelId: string,
185
- settings?: { userId?: string; agentId?: string; sessionId?: string },
186
- providerOptions?: Record<string, unknown>
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
- return wrapLanguageModel({
204
- model,
205
- middleware: {
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
- const incomingMessages = Array.isArray((params as any).prompt)
210
- ? (params as any).prompt
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
- // 1) Check if memory is already injected in messages
214
- if (hasExistingMemoryInjection(incomingMessages)) {
215
- logger.debug("Memory already injected, skipping");
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
- // 2) Check session cache
220
- const sessionKey = `${userId}:${agentId}:${sessionId || "default"}`;
221
- let systemPromptToAdd = sessionSnapshots.get(sessionKey);
222
-
223
- // 3) Fetch snapshot only if not cached
224
- if (systemPromptToAdd === undefined) {
225
- try {
226
- const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}&agentId=${agentId}&appId=${clConfig.appId}`;
227
- const res = await fetch(url);
228
- if (res.ok) {
229
- const data = await res.json();
230
- const systemBlock = data.systemBlock || "";
231
- const userContextBlock = data.userContextBlock || "";
232
- systemPromptToAdd =
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
- `.trim()
241
- : "";
242
-
243
- // Cache the snapshot for this session
244
- sessionSnapshots.set(sessionKey, systemPromptToAdd);
245
-
246
- logger.info("Snapshot fetched and cached", {
247
- userId,
248
- agentId,
249
- sessionId,
250
- sessionKey,
251
- systemLen: systemBlock.length,
252
- userLen: userContextBlock.length,
253
- });
254
- // At debug level, log the full snapshot data
255
- logger.debug("Full snapshot data", {
256
- systemBlock,
257
- userContextBlock,
258
- rawData: data,
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.debug("Using cached snapshot for session", { sessionKey });
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
- if (!systemPromptToAdd) {
275
- return { ...params, messages: incomingMessages };
276
- }
353
+ if (!systemPromptToAdd) {
354
+ return params;
355
+ }
277
356
 
278
- const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(
279
- params,
280
- incomingMessages,
281
- systemPromptToAdd
282
- );
357
+ const { nextParams, messages: messagesWithMemory } = withMemorySystemPrompt(
358
+ params,
359
+ incomingMessages,
360
+ systemPromptToAdd
361
+ );
283
362
 
284
- logger.info("Injecting memory system prompt", {
285
- sessionKey,
286
- promptLength: systemPromptToAdd.length,
287
- });
288
- logger.debug("Injected prompt content", { systemPromptToAdd });
363
+ logger.info("Injecting memory system prompt", {
364
+ sessionKey,
365
+ promptLength: systemPromptToAdd.length,
366
+ });
367
+ logger.debug("Injected prompt content", { systemPromptToAdd });
289
368
 
290
- return { ...nextParams, prompt: messagesWithMemory };
291
- },
369
+ return { ...nextParams, prompt: messagesWithMemory };
370
+ },
292
371
 
293
- async wrapGenerate({ doGenerate, params }) {
294
- const result = await doGenerate();
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
- if (userId && sessionId) {
297
- const messagesInput = (params as any).messages || (params as any).prompt || [];
298
- const resultMessages = (result as any)?.response?.messages;
299
- const assistantMessage = (result as any)?.text
300
- ? [{ role: "assistant", content: [{ type: "text", text: (result as any).text }] }]
301
- : [];
302
- const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
303
- ? resultMessages
304
- : [...messagesInput, ...assistantMessage];
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
- logConversation({
307
- userId,
308
- agentId,
309
- sessionId,
310
- messages: finalMessages,
311
- modelId,
312
- }).then(() => triggerProcessing(userId, agentId, sessionId));
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
- return result;
316
- },
317
- async wrapStream({ doStream, params }) {
318
- const result = await doStream();
435
+ let streamUsage: Record<string, unknown> | undefined;
436
+ let accumulatedText = '';
319
437
 
320
- if (userId && sessionId) {
321
- const messagesInput = (params as any).messages || (params as any).prompt || [];
322
- const resultMessages = (result as any)?.response?.messages;
323
- const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
324
- ? resultMessages
325
- : messagesInput;
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
- agentId,
456
+ projectId,
330
457
  sessionId,
331
- messages: finalMessages,
458
+ messages: allMessages,
332
459
  modelId,
333
- }).then(() => triggerProcessing(userId, agentId, sessionId));
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
- return result;
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
  }