@kognitivedev/vercel-ai-provider 0.1.7 → 0.1.9
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/dist/__tests__/wrap-stream-logging.test.js +92 -0
- package/dist/index.d.ts +22 -1
- package/dist/index.js +333 -60
- package/package.json +1 -1
- package/src/__tests__/wrap-stream-logging.test.ts +104 -0
- package/src/index.ts +392 -64
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateText as aiGenerateText,
|
|
5
5
|
type LanguageModel,
|
|
6
6
|
} from "ai";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Log levels for controlling verbosity of CognitiveLayer logging.
|
|
@@ -21,6 +22,17 @@ function isValidId(value: string | undefined | null): value is string {
|
|
|
21
22
|
return trimmed !== "" && trimmed !== "null" && trimmed !== "undefined";
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function maskSecret(secret: string | undefined | null): string {
|
|
26
|
+
if (!secret) return "missing";
|
|
27
|
+
if (secret.length <= 8) return `${secret.slice(0, 2)}***`;
|
|
28
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function previewText(value: string, maxLength = 240): string {
|
|
32
|
+
if (value.length <= maxLength) return value;
|
|
33
|
+
return `${value.slice(0, maxLength)}...`;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
25
37
|
none: 0,
|
|
26
38
|
error: 1,
|
|
@@ -111,12 +123,33 @@ export interface LogConversationPayload {
|
|
|
111
123
|
promptSlug?: string;
|
|
112
124
|
promptVersion?: number;
|
|
113
125
|
promptId?: string;
|
|
126
|
+
traceId?: string;
|
|
127
|
+
parentSpanId?: string;
|
|
128
|
+
requestPreview?: string;
|
|
129
|
+
responsePreview?: string;
|
|
130
|
+
state?: "active" | "completed" | "error";
|
|
131
|
+
startedAt?: string;
|
|
132
|
+
endedAt?: string;
|
|
133
|
+
durationMs?: number;
|
|
134
|
+
metadata?: Record<string, unknown>;
|
|
135
|
+
spans?: Array<{
|
|
136
|
+
spanKey: string;
|
|
137
|
+
parentSpanKey?: string;
|
|
138
|
+
name: string;
|
|
139
|
+
spanType: string;
|
|
140
|
+
status?: "active" | "completed" | "error";
|
|
141
|
+
inputPreview?: string;
|
|
142
|
+
outputPreview?: string;
|
|
143
|
+
toolName?: string;
|
|
144
|
+
errorMessage?: string;
|
|
145
|
+
metadata?: Record<string, unknown>;
|
|
146
|
+
}>;
|
|
114
147
|
}
|
|
115
148
|
|
|
116
149
|
export type CognitiveLayer = CLModelWrapper & {
|
|
117
150
|
streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
|
|
118
151
|
generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
|
|
119
|
-
resolvePrompt: (slug: string) => Promise<CachedPrompt>;
|
|
152
|
+
resolvePrompt: (slug: string, userId?: string) => Promise<CachedPrompt>;
|
|
120
153
|
logConversation: (payload: LogConversationPayload) => Promise<void>;
|
|
121
154
|
triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
|
|
122
155
|
clearPromptCache: () => void;
|
|
@@ -136,6 +169,111 @@ export interface CachedPrompt {
|
|
|
136
169
|
|
|
137
170
|
const PROMPT_CACHE_TTL_MS = 60_000; // 1 minute
|
|
138
171
|
|
|
172
|
+
function getContentText(content: any): string {
|
|
173
|
+
if (typeof content === "string") return content;
|
|
174
|
+
if (!Array.isArray(content)) return "";
|
|
175
|
+
|
|
176
|
+
return content.map((part) => {
|
|
177
|
+
if (!part || typeof part !== "object") return "";
|
|
178
|
+
if (typeof part.text === "string") return part.text;
|
|
179
|
+
if (part.type === "tool-call" && typeof part.toolName === "string") return `Called ${part.toolName}`;
|
|
180
|
+
if (part.type === "tool-result") return "Received tool result";
|
|
181
|
+
return "";
|
|
182
|
+
}).filter(Boolean).join(" ");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Unwraps V2/V3 ToolResultOutput discriminated union to a displayable value.
|
|
187
|
+
* Stream ToolResult uses plain `result` (passthrough), while prompt ToolResultPart
|
|
188
|
+
* uses `output` with a discriminated union: text, json, error-text, error-json, content, execution-denied.
|
|
189
|
+
*/
|
|
190
|
+
function extractOutputValue(raw: unknown): unknown {
|
|
191
|
+
if (raw == null) return raw;
|
|
192
|
+
if (typeof raw !== 'object') return raw;
|
|
193
|
+
const obj = raw as Record<string, unknown>;
|
|
194
|
+
if (typeof obj.type !== 'string') return raw;
|
|
195
|
+
switch (obj.type) {
|
|
196
|
+
case 'text':
|
|
197
|
+
case 'json':
|
|
198
|
+
case 'error-text':
|
|
199
|
+
case 'error-json':
|
|
200
|
+
case 'content':
|
|
201
|
+
return obj.value;
|
|
202
|
+
case 'execution-denied':
|
|
203
|
+
return `Execution denied: ${obj.reason ?? 'unknown'}`;
|
|
204
|
+
default:
|
|
205
|
+
return raw;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildTracePreviews(messages: any[]): { requestPreview: string; responsePreview: string } {
|
|
210
|
+
const request = [...messages].reverse().find((message) => message?.role === "user");
|
|
211
|
+
const response = [...messages].reverse().find((message) => message?.role === "assistant");
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
requestPreview: request ? getContentText(request.content).slice(0, 220) : "No request captured",
|
|
215
|
+
responsePreview: response ? getContentText(response.content).slice(0, 240) : "No response captured",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildTraceSpansFromMessages(messages: any[]): Array<{
|
|
220
|
+
spanKey: string;
|
|
221
|
+
parentSpanKey?: string;
|
|
222
|
+
name: string;
|
|
223
|
+
spanType: string;
|
|
224
|
+
status?: "active" | "completed" | "error";
|
|
225
|
+
inputPreview?: string;
|
|
226
|
+
outputPreview?: string;
|
|
227
|
+
toolName?: string;
|
|
228
|
+
errorMessage?: string;
|
|
229
|
+
metadata?: Record<string, unknown>;
|
|
230
|
+
}> {
|
|
231
|
+
const resultMap = new Map<string, unknown>();
|
|
232
|
+
|
|
233
|
+
for (const message of messages) {
|
|
234
|
+
if (!Array.isArray(message?.content)) continue;
|
|
235
|
+
for (const part of message.content) {
|
|
236
|
+
if (part?.type === "tool-result" && typeof part.toolCallId === "string") {
|
|
237
|
+
resultMap.set(part.toolCallId, part.result ?? part.output);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const spans: Array<{
|
|
243
|
+
spanKey: string;
|
|
244
|
+
parentSpanKey?: string;
|
|
245
|
+
name: string;
|
|
246
|
+
spanType: string;
|
|
247
|
+
status?: "active" | "completed" | "error";
|
|
248
|
+
inputPreview?: string;
|
|
249
|
+
outputPreview?: string;
|
|
250
|
+
toolName?: string;
|
|
251
|
+
errorMessage?: string;
|
|
252
|
+
metadata?: Record<string, unknown>;
|
|
253
|
+
}> = [];
|
|
254
|
+
|
|
255
|
+
for (const message of messages) {
|
|
256
|
+
if (!Array.isArray(message?.content)) continue;
|
|
257
|
+
for (const part of message.content) {
|
|
258
|
+
if (part?.type === "tool-call" && typeof part.toolCallId === "string") {
|
|
259
|
+
const result = resultMap.get(part.toolCallId);
|
|
260
|
+
spans.push({
|
|
261
|
+
spanKey: part.toolCallId,
|
|
262
|
+
parentSpanKey: "root",
|
|
263
|
+
name: typeof part.toolName === "string" ? part.toolName : "tool",
|
|
264
|
+
spanType: "tool",
|
|
265
|
+
status: "completed",
|
|
266
|
+
inputPreview: JSON.stringify(part.input ?? {}).slice(0, 220),
|
|
267
|
+
outputPreview: result != null ? JSON.stringify(extractOutputValue(result)).slice(0, 220) : "No tool result captured",
|
|
268
|
+
toolName: typeof part.toolName === "string" ? part.toolName : undefined,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return spans;
|
|
275
|
+
}
|
|
276
|
+
|
|
139
277
|
/**
|
|
140
278
|
* Interpolate {{variable}} placeholders in a template string.
|
|
141
279
|
* Unmatched variables are left as-is.
|
|
@@ -194,18 +332,44 @@ export function createCognitiveLayer(config: {
|
|
|
194
332
|
// Prompt cache: slug → CachedPrompt
|
|
195
333
|
const promptCache = new Map<string, CachedPrompt>();
|
|
196
334
|
|
|
197
|
-
const resolvePrompt = async (slug: string): Promise<CachedPrompt> => {
|
|
198
|
-
const
|
|
335
|
+
const resolvePrompt = async (slug: string, userId?: string): Promise<CachedPrompt> => {
|
|
336
|
+
const cacheKey = userId ? `${slug}:${userId}` : slug;
|
|
337
|
+
const cached = promptCache.get(cacheKey);
|
|
199
338
|
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
200
339
|
logger.debug("Using cached prompt", { slug, version: cached.version });
|
|
201
340
|
return cached;
|
|
202
341
|
}
|
|
203
342
|
|
|
204
|
-
const
|
|
343
|
+
const url = new URL(`${baseUrl}/api/cognitive/prompt`);
|
|
344
|
+
url.searchParams.set("slug", slug);
|
|
345
|
+
if (userId) url.searchParams.set("userId", userId);
|
|
346
|
+
|
|
347
|
+
logger.debug("Resolving prompt from backend", {
|
|
348
|
+
slug,
|
|
349
|
+
userId,
|
|
350
|
+
url: url.toString(),
|
|
351
|
+
baseUrl,
|
|
352
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const res = await fetch(url.toString(), {
|
|
205
356
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
206
357
|
});
|
|
358
|
+
logger.debug("Prompt resolve response received", {
|
|
359
|
+
slug,
|
|
360
|
+
userId,
|
|
361
|
+
status: res.status,
|
|
362
|
+
ok: res.ok,
|
|
363
|
+
contentType: res.headers.get("content-type"),
|
|
364
|
+
});
|
|
207
365
|
if (!res.ok) {
|
|
208
366
|
const body = await res.text();
|
|
367
|
+
logger.debug("Prompt resolve response body preview", {
|
|
368
|
+
slug,
|
|
369
|
+
userId,
|
|
370
|
+
status: res.status,
|
|
371
|
+
bodyPreview: previewText(body),
|
|
372
|
+
});
|
|
209
373
|
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
210
374
|
}
|
|
211
375
|
|
|
@@ -218,7 +382,15 @@ export function createCognitiveLayer(config: {
|
|
|
218
382
|
fetchedAt: Date.now(),
|
|
219
383
|
gatewaySlug: data.gatewaySlug,
|
|
220
384
|
};
|
|
221
|
-
promptCache.set(
|
|
385
|
+
promptCache.set(cacheKey, entry);
|
|
386
|
+
logger.debug("Prompt resolved payload", {
|
|
387
|
+
slug,
|
|
388
|
+
resolvedSlug: entry.slug,
|
|
389
|
+
version: entry.version,
|
|
390
|
+
promptId: entry.promptId,
|
|
391
|
+
contentLength: entry.content.length,
|
|
392
|
+
gatewaySlug: entry.gatewaySlug ?? null,
|
|
393
|
+
});
|
|
222
394
|
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
223
395
|
return entry;
|
|
224
396
|
};
|
|
@@ -301,9 +473,25 @@ export function createCognitiveLayer(config: {
|
|
|
301
473
|
if (systemPromptToAdd === undefined) {
|
|
302
474
|
try {
|
|
303
475
|
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
476
|
+
logger.debug("Fetching snapshot from backend", {
|
|
477
|
+
userId,
|
|
478
|
+
projectId,
|
|
479
|
+
sessionId,
|
|
480
|
+
url,
|
|
481
|
+
baseUrl,
|
|
482
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
483
|
+
});
|
|
304
484
|
const res = await fetch(url, {
|
|
305
485
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
306
486
|
});
|
|
487
|
+
logger.debug("Snapshot response received", {
|
|
488
|
+
userId,
|
|
489
|
+
projectId,
|
|
490
|
+
sessionId,
|
|
491
|
+
status: res.status,
|
|
492
|
+
ok: res.ok,
|
|
493
|
+
contentType: res.headers.get("content-type"),
|
|
494
|
+
});
|
|
307
495
|
if (res.ok) {
|
|
308
496
|
const data = await res.json();
|
|
309
497
|
const systemBlock = data.systemBlock || "";
|
|
@@ -337,7 +525,15 @@ ${userContextBlock || "None"}
|
|
|
337
525
|
rawData: data,
|
|
338
526
|
});
|
|
339
527
|
} else {
|
|
528
|
+
const body = await res.text();
|
|
340
529
|
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
530
|
+
logger.debug("Snapshot response body preview", {
|
|
531
|
+
userId,
|
|
532
|
+
projectId,
|
|
533
|
+
sessionId,
|
|
534
|
+
status: res.status,
|
|
535
|
+
bodyPreview: previewText(body),
|
|
536
|
+
});
|
|
341
537
|
systemPromptToAdd = "";
|
|
342
538
|
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
343
539
|
}
|
|
@@ -370,6 +566,7 @@ ${userContextBlock || "None"}
|
|
|
370
566
|
},
|
|
371
567
|
|
|
372
568
|
async wrapGenerate({ doGenerate, params }: { doGenerate: any; params: any }) {
|
|
569
|
+
const startedAt = new Date();
|
|
373
570
|
let result;
|
|
374
571
|
try {
|
|
375
572
|
result = await doGenerate();
|
|
@@ -380,17 +577,40 @@ ${userContextBlock || "None"}
|
|
|
380
577
|
}
|
|
381
578
|
|
|
382
579
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
580
|
+
const endedAt = new Date();
|
|
383
581
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
384
582
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
385
583
|
|
|
386
|
-
const messagesInput = (params as any).
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
584
|
+
const messagesInput = (params as any).prompt || (params as any).messages || [];
|
|
585
|
+
|
|
586
|
+
// Build assistant message from result.content (V2/V3 GenerateResult)
|
|
587
|
+
const resultContent = Array.isArray(result?.content) ? result.content : [];
|
|
588
|
+
const assistantParts: any[] = [];
|
|
589
|
+
for (const part of resultContent) {
|
|
590
|
+
if (part?.type === 'text') {
|
|
591
|
+
assistantParts.push({ type: 'text', text: part.text });
|
|
592
|
+
} else if (part?.type === 'tool-call') {
|
|
593
|
+
assistantParts.push({
|
|
594
|
+
type: 'tool-call',
|
|
595
|
+
toolCallId: part.toolCallId,
|
|
596
|
+
toolName: part.toolName,
|
|
597
|
+
input: part.input,
|
|
598
|
+
});
|
|
599
|
+
} else if (part?.type === 'tool-result') {
|
|
600
|
+
assistantParts.push({
|
|
601
|
+
type: 'tool-result',
|
|
602
|
+
toolCallId: part.toolCallId,
|
|
603
|
+
toolName: part.toolName,
|
|
604
|
+
result: part.result,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const assistantMessage = assistantParts.length > 0
|
|
609
|
+
? [{ role: "assistant", content: assistantParts }]
|
|
390
610
|
: [];
|
|
391
|
-
const finalMessages =
|
|
392
|
-
|
|
393
|
-
|
|
611
|
+
const finalMessages = [...messagesInput, ...assistantMessage];
|
|
612
|
+
const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
|
|
613
|
+
const spans = buildTraceSpansFromMessages(finalMessages);
|
|
394
614
|
|
|
395
615
|
logConversation({
|
|
396
616
|
userId,
|
|
@@ -404,12 +624,25 @@ ${userContextBlock || "None"}
|
|
|
404
624
|
promptVersion: promptMeta.promptVersion,
|
|
405
625
|
promptId: promptMeta.promptId,
|
|
406
626
|
}),
|
|
627
|
+
traceId: randomUUID(),
|
|
628
|
+
requestPreview,
|
|
629
|
+
responsePreview,
|
|
630
|
+
state: "completed",
|
|
631
|
+
startedAt: startedAt.toISOString(),
|
|
632
|
+
endedAt: endedAt.toISOString(),
|
|
633
|
+
durationMs: endedAt.getTime() - startedAt.getTime(),
|
|
634
|
+
metadata: {
|
|
635
|
+
appId: clConfig.appId,
|
|
636
|
+
},
|
|
637
|
+
spans,
|
|
407
638
|
}).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
408
639
|
}
|
|
409
640
|
|
|
410
641
|
return result;
|
|
411
642
|
},
|
|
412
643
|
async wrapStream({ doStream, params }: { doStream: any; params: any }) {
|
|
644
|
+
const startedAt = new Date();
|
|
645
|
+
const traceId = randomUUID();
|
|
413
646
|
let result;
|
|
414
647
|
try {
|
|
415
648
|
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
@@ -426,7 +659,7 @@ ${userContextBlock || "None"}
|
|
|
426
659
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
427
660
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
428
661
|
|
|
429
|
-
const messagesInput = (params as any).
|
|
662
|
+
const messagesInput = (params as any).prompt || (params as any).messages || [];
|
|
430
663
|
const resultMessages = (result as any)?.response?.messages;
|
|
431
664
|
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
432
665
|
? resultMessages
|
|
@@ -434,6 +667,9 @@ ${userContextBlock || "None"}
|
|
|
434
667
|
|
|
435
668
|
let streamUsage: Record<string, unknown> | undefined;
|
|
436
669
|
let accumulatedText = '';
|
|
670
|
+
const toolCallInputs = new Map<string, { toolName: string; chunks: string[] }>();
|
|
671
|
+
const completedToolCalls: any[] = [];
|
|
672
|
+
const completedToolResults: any[] = [];
|
|
437
673
|
|
|
438
674
|
const originalStream = result.stream;
|
|
439
675
|
const transformStream = new TransformStream({
|
|
@@ -444,14 +680,64 @@ ${userContextBlock || "None"}
|
|
|
444
680
|
if (chunk.type === 'finish' && chunk.usage) {
|
|
445
681
|
streamUsage = chunk.usage;
|
|
446
682
|
}
|
|
683
|
+
// Capture tool-call stream chunks (V2/V3 shared types)
|
|
684
|
+
if (chunk.type === 'tool-input-start') {
|
|
685
|
+
toolCallInputs.set(chunk.id, { toolName: chunk.toolName, chunks: [] });
|
|
686
|
+
}
|
|
687
|
+
if (chunk.type === 'tool-input-delta') {
|
|
688
|
+
const entry = toolCallInputs.get(chunk.id);
|
|
689
|
+
if (entry) entry.chunks.push(chunk.delta);
|
|
690
|
+
}
|
|
691
|
+
if (chunk.type === 'tool-call') {
|
|
692
|
+
completedToolCalls.push({
|
|
693
|
+
type: 'tool-call',
|
|
694
|
+
toolCallId: chunk.toolCallId,
|
|
695
|
+
toolName: chunk.toolName,
|
|
696
|
+
input: chunk.input,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
if (chunk.type === 'tool-result') {
|
|
700
|
+
completedToolResults.push({
|
|
701
|
+
type: 'tool-result',
|
|
702
|
+
toolCallId: chunk.toolCallId,
|
|
703
|
+
toolName: chunk.toolName,
|
|
704
|
+
result: chunk.result,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
447
707
|
controller.enqueue(chunk);
|
|
448
708
|
},
|
|
449
|
-
flush() {
|
|
450
|
-
const
|
|
451
|
-
|
|
709
|
+
async flush() {
|
|
710
|
+
const endedAt = new Date();
|
|
711
|
+
|
|
712
|
+
// Finalize any tool calls from incremental input chunks
|
|
713
|
+
for (const [id, entry] of toolCallInputs) {
|
|
714
|
+
// Only add if not already captured via a tool-call chunk
|
|
715
|
+
if (!completedToolCalls.some((tc: any) => tc.toolCallId === id)) {
|
|
716
|
+
completedToolCalls.push({
|
|
717
|
+
type: 'tool-call',
|
|
718
|
+
toolCallId: id,
|
|
719
|
+
toolName: entry.toolName,
|
|
720
|
+
input: entry.chunks.join(''),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const assistantParts: any[] = [];
|
|
726
|
+
if (accumulatedText) assistantParts.push({ type: "text", text: accumulatedText });
|
|
727
|
+
for (const tc of completedToolCalls) assistantParts.push(tc);
|
|
728
|
+
|
|
729
|
+
const allMessages = assistantParts.length > 0
|
|
730
|
+
? [...finalMessages, { role: "assistant", content: assistantParts }]
|
|
452
731
|
: finalMessages;
|
|
453
732
|
|
|
454
|
-
|
|
733
|
+
if (completedToolResults.length > 0) {
|
|
734
|
+
allMessages.push({ role: "tool", content: completedToolResults });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const { requestPreview, responsePreview } = buildTracePreviews(allMessages);
|
|
738
|
+
const spans = buildTraceSpansFromMessages(allMessages);
|
|
739
|
+
|
|
740
|
+
await logConversation({
|
|
455
741
|
userId,
|
|
456
742
|
projectId,
|
|
457
743
|
sessionId,
|
|
@@ -463,7 +749,19 @@ ${userContextBlock || "None"}
|
|
|
463
749
|
promptVersion: promptMeta.promptVersion,
|
|
464
750
|
promptId: promptMeta.promptId,
|
|
465
751
|
}),
|
|
466
|
-
|
|
752
|
+
traceId,
|
|
753
|
+
requestPreview,
|
|
754
|
+
responsePreview,
|
|
755
|
+
state: "completed",
|
|
756
|
+
startedAt: startedAt.toISOString(),
|
|
757
|
+
endedAt: endedAt.toISOString(),
|
|
758
|
+
durationMs: endedAt.getTime() - startedAt.getTime(),
|
|
759
|
+
metadata: {
|
|
760
|
+
appId: clConfig.appId,
|
|
761
|
+
},
|
|
762
|
+
spans,
|
|
763
|
+
});
|
|
764
|
+
triggerProcessing(userId, projectId, sessionId);
|
|
467
765
|
}
|
|
468
766
|
});
|
|
469
767
|
|
|
@@ -528,8 +826,10 @@ ${userContextBlock || "None"}
|
|
|
528
826
|
}) as LanguageModel;
|
|
529
827
|
|
|
530
828
|
// Track session settings on the model for use in cl.streamText/cl.generateText
|
|
531
|
-
if
|
|
532
|
-
|
|
829
|
+
// Always store if userId is valid — sessionId may be missing but userId is still
|
|
830
|
+
// needed for prompt resolution (e.g. A/B test assignment)
|
|
831
|
+
if (isValidId(userId)) {
|
|
832
|
+
(wrappedModel as any)[SESSION_KEY] = { userId, projectId, sessionId: isValidId(sessionId) ? sessionId : undefined };
|
|
533
833
|
}
|
|
534
834
|
|
|
535
835
|
return wrappedModel;
|
|
@@ -538,61 +838,89 @@ ${userContextBlock || "None"}
|
|
|
538
838
|
const clStreamText = async (options: CLStreamTextOptions) => {
|
|
539
839
|
const { prompt: promptConfig, ...rest } = options;
|
|
540
840
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
});
|
|
841
|
+
const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId?: string } | undefined;
|
|
842
|
+
|
|
843
|
+
// Resolve and interpolate prompt (graceful fallback on failure)
|
|
844
|
+
let resolved: CachedPrompt | null = null;
|
|
845
|
+
try {
|
|
846
|
+
resolved = await resolvePrompt(promptConfig.slug, session?.userId);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
logger.warn(`Failed to resolve prompt "${promptConfig.slug}", streaming without system prompt.`, err);
|
|
556
849
|
}
|
|
557
850
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
851
|
+
let system: string | undefined;
|
|
852
|
+
if (resolved) {
|
|
853
|
+
system = promptConfig.variables
|
|
854
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
855
|
+
: resolved.content;
|
|
856
|
+
|
|
857
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
858
|
+
if (session?.sessionId) {
|
|
859
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
860
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
861
|
+
promptSlug: resolved.slug,
|
|
862
|
+
promptVersion: resolved.version,
|
|
863
|
+
promptId: resolved.promptId,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
logger.info("cl.streamText called", {
|
|
868
|
+
slug: promptConfig.slug,
|
|
869
|
+
version: resolved.version,
|
|
870
|
+
systemLength: system.length,
|
|
871
|
+
});
|
|
872
|
+
} else {
|
|
873
|
+
logger.info("cl.streamText called without resolved prompt", {
|
|
874
|
+
slug: promptConfig.slug,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
563
877
|
|
|
564
|
-
const model = resolveModel(options.model, resolved
|
|
565
|
-
return aiStreamText({ ...rest, model, system } as any);
|
|
878
|
+
const model = resolveModel(options.model, resolved?.gatewaySlug);
|
|
879
|
+
return aiStreamText({ ...rest, model, ...(system && { system }) } as any);
|
|
566
880
|
};
|
|
567
881
|
|
|
568
882
|
const clGenerateText = async (options: CLGenerateTextOptions) => {
|
|
569
883
|
const { prompt: promptConfig, ...rest } = options;
|
|
570
884
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
});
|
|
885
|
+
const session = (options.model as any)[SESSION_KEY] as { userId: string; projectId: string; sessionId?: string } | undefined;
|
|
886
|
+
|
|
887
|
+
// Resolve and interpolate prompt (graceful fallback on failure)
|
|
888
|
+
let resolved: CachedPrompt | null = null;
|
|
889
|
+
try {
|
|
890
|
+
resolved = await resolvePrompt(promptConfig.slug, session?.userId);
|
|
891
|
+
} catch (err) {
|
|
892
|
+
logger.warn(`Failed to resolve prompt "${promptConfig.slug}", generating without system prompt.`, err);
|
|
586
893
|
}
|
|
587
894
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
895
|
+
let system: string | undefined;
|
|
896
|
+
if (resolved) {
|
|
897
|
+
system = promptConfig.variables
|
|
898
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
899
|
+
: resolved.content;
|
|
900
|
+
|
|
901
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
902
|
+
if (session?.sessionId) {
|
|
903
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
904
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
905
|
+
promptSlug: resolved.slug,
|
|
906
|
+
promptVersion: resolved.version,
|
|
907
|
+
promptId: resolved.promptId,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
logger.info("cl.generateText called", {
|
|
912
|
+
slug: promptConfig.slug,
|
|
913
|
+
version: resolved.version,
|
|
914
|
+
systemLength: system.length,
|
|
915
|
+
});
|
|
916
|
+
} else {
|
|
917
|
+
logger.info("cl.generateText called without resolved prompt", {
|
|
918
|
+
slug: promptConfig.slug,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
593
921
|
|
|
594
|
-
const model = resolveModel(options.model, resolved
|
|
595
|
-
return aiGenerateText({ ...rest, model, system } as any);
|
|
922
|
+
const model = resolveModel(options.model, resolved?.gatewaySlug);
|
|
923
|
+
return aiGenerateText({ ...rest, model, ...(system && { system }) } as any);
|
|
596
924
|
};
|
|
597
925
|
|
|
598
926
|
// Return the model wrapper function with streamText/generateText attached
|