@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
|
@@ -24,6 +24,98 @@ const test_1 = require("ai/test");
|
|
|
24
24
|
return new Response("not found", { status: 404 });
|
|
25
25
|
}));
|
|
26
26
|
});
|
|
27
|
+
(0, vitest_1.it)("should capture tool-call chunks and include them in logged conversation", async () => {
|
|
28
|
+
const mockModel = new test_1.MockLanguageModelV3({
|
|
29
|
+
doStream: async () => ({
|
|
30
|
+
stream: (0, test_1.convertArrayToReadableStream)([
|
|
31
|
+
{ type: "text-start", id: "t1" },
|
|
32
|
+
{ type: "text-delta", id: "t1", delta: "Let me check" },
|
|
33
|
+
{ type: "text-end", id: "t1" },
|
|
34
|
+
{
|
|
35
|
+
type: "tool-call",
|
|
36
|
+
toolCallId: "call-1",
|
|
37
|
+
toolName: "get_weather",
|
|
38
|
+
input: '{"city":"London"}',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "tool-result",
|
|
42
|
+
toolCallId: "call-1",
|
|
43
|
+
toolName: "get_weather",
|
|
44
|
+
result: { temperature: 15, unit: "celsius" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "finish",
|
|
48
|
+
finishReason: {
|
|
49
|
+
unified: "tool-calls",
|
|
50
|
+
raw: undefined,
|
|
51
|
+
},
|
|
52
|
+
usage: {
|
|
53
|
+
inputTokens: { total: 20, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
|
|
54
|
+
outputTokens: { total: 15, text: undefined, reasoning: undefined },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
]),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
const mockProvider = () => mockModel;
|
|
61
|
+
const cl = (0, index_1.createCognitiveLayer)({
|
|
62
|
+
provider: mockProvider,
|
|
63
|
+
clConfig: {
|
|
64
|
+
apiKey: "test-api-key",
|
|
65
|
+
appId: "test-app",
|
|
66
|
+
projectId: "test-project",
|
|
67
|
+
processDelayMs: 0,
|
|
68
|
+
logLevel: "none",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const model = cl("mock-model", {
|
|
72
|
+
userId: "user-1",
|
|
73
|
+
projectId: "project-1",
|
|
74
|
+
sessionId: "session-1",
|
|
75
|
+
});
|
|
76
|
+
const result = (0, ai_1.streamText)({
|
|
77
|
+
model,
|
|
78
|
+
messages: [{ role: "user", content: "What's the weather in London?" }],
|
|
79
|
+
});
|
|
80
|
+
// Fully consume the stream
|
|
81
|
+
await result.text;
|
|
82
|
+
// Wait for async logConversation to complete
|
|
83
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
84
|
+
// Find the log call
|
|
85
|
+
const logCall = fetchCalls.find((c) => c.url.includes("/api/cognitive/log"));
|
|
86
|
+
(0, vitest_1.expect)(logCall).toBeDefined();
|
|
87
|
+
const messages = logCall.body.messages;
|
|
88
|
+
// Assistant message should contain text + tool-call parts
|
|
89
|
+
const assistantMsg = messages.find((m) => m.role === "assistant");
|
|
90
|
+
(0, vitest_1.expect)(assistantMsg).toBeDefined();
|
|
91
|
+
(0, vitest_1.expect)(assistantMsg.content).toEqual([
|
|
92
|
+
{ type: "text", text: "Let me check" },
|
|
93
|
+
{
|
|
94
|
+
type: "tool-call",
|
|
95
|
+
toolCallId: "call-1",
|
|
96
|
+
toolName: "get_weather",
|
|
97
|
+
input: '{"city":"London"}',
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
// Tool results should be in a separate tool message
|
|
101
|
+
const toolMsg = messages.find((m) => m.role === "tool");
|
|
102
|
+
(0, vitest_1.expect)(toolMsg).toBeDefined();
|
|
103
|
+
(0, vitest_1.expect)(toolMsg.content).toEqual([
|
|
104
|
+
{
|
|
105
|
+
type: "tool-result",
|
|
106
|
+
toolCallId: "call-1",
|
|
107
|
+
toolName: "get_weather",
|
|
108
|
+
result: { temperature: 15, unit: "celsius" },
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
// Spans should include the tool call with populated previews
|
|
112
|
+
const spans = logCall.body.spans;
|
|
113
|
+
const toolSpan = spans === null || spans === void 0 ? void 0 : spans.find((s) => s.spanType === "tool");
|
|
114
|
+
(0, vitest_1.expect)(toolSpan).toBeDefined();
|
|
115
|
+
(0, vitest_1.expect)(toolSpan.toolName).toBe("get_weather");
|
|
116
|
+
(0, vitest_1.expect)(toolSpan.inputPreview).toContain("London");
|
|
117
|
+
(0, vitest_1.expect)(toolSpan.outputPreview).toContain("15");
|
|
118
|
+
});
|
|
27
119
|
(0, vitest_1.it)("should include assistant message in logged conversation after streaming", async () => {
|
|
28
120
|
const mockModel = new test_1.MockLanguageModelV3({
|
|
29
121
|
doStream: async () => ({
|
package/dist/index.d.ts
CHANGED
|
@@ -55,11 +55,32 @@ export interface LogConversationPayload {
|
|
|
55
55
|
promptSlug?: string;
|
|
56
56
|
promptVersion?: number;
|
|
57
57
|
promptId?: string;
|
|
58
|
+
traceId?: string;
|
|
59
|
+
parentSpanId?: string;
|
|
60
|
+
requestPreview?: string;
|
|
61
|
+
responsePreview?: string;
|
|
62
|
+
state?: "active" | "completed" | "error";
|
|
63
|
+
startedAt?: string;
|
|
64
|
+
endedAt?: string;
|
|
65
|
+
durationMs?: number;
|
|
66
|
+
metadata?: Record<string, unknown>;
|
|
67
|
+
spans?: Array<{
|
|
68
|
+
spanKey: string;
|
|
69
|
+
parentSpanKey?: string;
|
|
70
|
+
name: string;
|
|
71
|
+
spanType: string;
|
|
72
|
+
status?: "active" | "completed" | "error";
|
|
73
|
+
inputPreview?: string;
|
|
74
|
+
outputPreview?: string;
|
|
75
|
+
toolName?: string;
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
}>;
|
|
58
79
|
}
|
|
59
80
|
export type CognitiveLayer = CLModelWrapper & {
|
|
60
81
|
streamText: (options: CLStreamTextOptions) => Promise<ReturnType<typeof aiStreamText>>;
|
|
61
82
|
generateText: (options: CLGenerateTextOptions) => ReturnType<typeof aiGenerateText>;
|
|
62
|
-
resolvePrompt: (slug: string) => Promise<CachedPrompt>;
|
|
83
|
+
resolvePrompt: (slug: string, userId?: string) => Promise<CachedPrompt>;
|
|
63
84
|
logConversation: (payload: LogConversationPayload) => Promise<void>;
|
|
64
85
|
triggerProcessing: (userId: string, projectId: string, sessionId: string) => void;
|
|
65
86
|
clearPromptCache: () => void;
|
package/dist/index.js
CHANGED
|
@@ -13,12 +13,25 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.createCognitiveLayer = createCognitiveLayer;
|
|
15
15
|
const ai_1 = require("ai");
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
16
17
|
function isValidId(value) {
|
|
17
18
|
if (value == null || typeof value !== "string")
|
|
18
19
|
return false;
|
|
19
20
|
const trimmed = value.trim();
|
|
20
21
|
return trimmed !== "" && trimmed !== "null" && trimmed !== "undefined";
|
|
21
22
|
}
|
|
23
|
+
function maskSecret(secret) {
|
|
24
|
+
if (!secret)
|
|
25
|
+
return "missing";
|
|
26
|
+
if (secret.length <= 8)
|
|
27
|
+
return `${secret.slice(0, 2)}***`;
|
|
28
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
29
|
+
}
|
|
30
|
+
function previewText(value, maxLength = 240) {
|
|
31
|
+
if (value.length <= maxLength)
|
|
32
|
+
return value;
|
|
33
|
+
return `${value.slice(0, maxLength)}...`;
|
|
34
|
+
}
|
|
22
35
|
const LOG_LEVEL_PRIORITY = {
|
|
23
36
|
none: 0,
|
|
24
37
|
error: 1,
|
|
@@ -55,6 +68,92 @@ function createLogger(logLevel) {
|
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
const PROMPT_CACHE_TTL_MS = 60000; // 1 minute
|
|
71
|
+
function getContentText(content) {
|
|
72
|
+
if (typeof content === "string")
|
|
73
|
+
return content;
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return "";
|
|
76
|
+
return content.map((part) => {
|
|
77
|
+
if (!part || typeof part !== "object")
|
|
78
|
+
return "";
|
|
79
|
+
if (typeof part.text === "string")
|
|
80
|
+
return part.text;
|
|
81
|
+
if (part.type === "tool-call" && typeof part.toolName === "string")
|
|
82
|
+
return `Called ${part.toolName}`;
|
|
83
|
+
if (part.type === "tool-result")
|
|
84
|
+
return "Received tool result";
|
|
85
|
+
return "";
|
|
86
|
+
}).filter(Boolean).join(" ");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Unwraps V2/V3 ToolResultOutput discriminated union to a displayable value.
|
|
90
|
+
* Stream ToolResult uses plain `result` (passthrough), while prompt ToolResultPart
|
|
91
|
+
* uses `output` with a discriminated union: text, json, error-text, error-json, content, execution-denied.
|
|
92
|
+
*/
|
|
93
|
+
function extractOutputValue(raw) {
|
|
94
|
+
var _a;
|
|
95
|
+
if (raw == null)
|
|
96
|
+
return raw;
|
|
97
|
+
if (typeof raw !== 'object')
|
|
98
|
+
return raw;
|
|
99
|
+
const obj = raw;
|
|
100
|
+
if (typeof obj.type !== 'string')
|
|
101
|
+
return raw;
|
|
102
|
+
switch (obj.type) {
|
|
103
|
+
case 'text':
|
|
104
|
+
case 'json':
|
|
105
|
+
case 'error-text':
|
|
106
|
+
case 'error-json':
|
|
107
|
+
case 'content':
|
|
108
|
+
return obj.value;
|
|
109
|
+
case 'execution-denied':
|
|
110
|
+
return `Execution denied: ${(_a = obj.reason) !== null && _a !== void 0 ? _a : 'unknown'}`;
|
|
111
|
+
default:
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function buildTracePreviews(messages) {
|
|
116
|
+
const request = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "user");
|
|
117
|
+
const response = [...messages].reverse().find((message) => (message === null || message === void 0 ? void 0 : message.role) === "assistant");
|
|
118
|
+
return {
|
|
119
|
+
requestPreview: request ? getContentText(request.content).slice(0, 220) : "No request captured",
|
|
120
|
+
responsePreview: response ? getContentText(response.content).slice(0, 240) : "No response captured",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function buildTraceSpansFromMessages(messages) {
|
|
124
|
+
var _a, _b;
|
|
125
|
+
const resultMap = new Map();
|
|
126
|
+
for (const message of messages) {
|
|
127
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
128
|
+
continue;
|
|
129
|
+
for (const part of message.content) {
|
|
130
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-result" && typeof part.toolCallId === "string") {
|
|
131
|
+
resultMap.set(part.toolCallId, (_a = part.result) !== null && _a !== void 0 ? _a : part.output);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const spans = [];
|
|
136
|
+
for (const message of messages) {
|
|
137
|
+
if (!Array.isArray(message === null || message === void 0 ? void 0 : message.content))
|
|
138
|
+
continue;
|
|
139
|
+
for (const part of message.content) {
|
|
140
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === "tool-call" && typeof part.toolCallId === "string") {
|
|
141
|
+
const result = resultMap.get(part.toolCallId);
|
|
142
|
+
spans.push({
|
|
143
|
+
spanKey: part.toolCallId,
|
|
144
|
+
parentSpanKey: "root",
|
|
145
|
+
name: typeof part.toolName === "string" ? part.toolName : "tool",
|
|
146
|
+
spanType: "tool",
|
|
147
|
+
status: "completed",
|
|
148
|
+
inputPreview: JSON.stringify((_b = part.input) !== null && _b !== void 0 ? _b : {}).slice(0, 220),
|
|
149
|
+
outputPreview: result != null ? JSON.stringify(extractOutputValue(result)).slice(0, 220) : "No tool result captured",
|
|
150
|
+
toolName: typeof part.toolName === "string" ? part.toolName : undefined,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return spans;
|
|
156
|
+
}
|
|
58
157
|
/**
|
|
59
158
|
* Interpolate {{variable}} placeholders in a template string.
|
|
60
159
|
* Unmatched variables are left as-is.
|
|
@@ -100,17 +199,43 @@ function createCognitiveLayer(config) {
|
|
|
100
199
|
};
|
|
101
200
|
// Prompt cache: slug → CachedPrompt
|
|
102
201
|
const promptCache = new Map();
|
|
103
|
-
const resolvePrompt = async (slug) => {
|
|
104
|
-
|
|
202
|
+
const resolvePrompt = async (slug, userId) => {
|
|
203
|
+
var _a;
|
|
204
|
+
const cacheKey = userId ? `${slug}:${userId}` : slug;
|
|
205
|
+
const cached = promptCache.get(cacheKey);
|
|
105
206
|
if (cached && Date.now() - cached.fetchedAt < PROMPT_CACHE_TTL_MS) {
|
|
106
207
|
logger.debug("Using cached prompt", { slug, version: cached.version });
|
|
107
208
|
return cached;
|
|
108
209
|
}
|
|
109
|
-
const
|
|
210
|
+
const url = new URL(`${baseUrl}/api/cognitive/prompt`);
|
|
211
|
+
url.searchParams.set("slug", slug);
|
|
212
|
+
if (userId)
|
|
213
|
+
url.searchParams.set("userId", userId);
|
|
214
|
+
logger.debug("Resolving prompt from backend", {
|
|
215
|
+
slug,
|
|
216
|
+
userId,
|
|
217
|
+
url: url.toString(),
|
|
218
|
+
baseUrl,
|
|
219
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
220
|
+
});
|
|
221
|
+
const res = await fetch(url.toString(), {
|
|
110
222
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
111
223
|
});
|
|
224
|
+
logger.debug("Prompt resolve response received", {
|
|
225
|
+
slug,
|
|
226
|
+
userId,
|
|
227
|
+
status: res.status,
|
|
228
|
+
ok: res.ok,
|
|
229
|
+
contentType: res.headers.get("content-type"),
|
|
230
|
+
});
|
|
112
231
|
if (!res.ok) {
|
|
113
232
|
const body = await res.text();
|
|
233
|
+
logger.debug("Prompt resolve response body preview", {
|
|
234
|
+
slug,
|
|
235
|
+
userId,
|
|
236
|
+
status: res.status,
|
|
237
|
+
bodyPreview: previewText(body),
|
|
238
|
+
});
|
|
114
239
|
throw new Error(`Failed to resolve prompt "${slug}": ${res.status} ${body}`);
|
|
115
240
|
}
|
|
116
241
|
const data = await res.json();
|
|
@@ -122,7 +247,15 @@ function createCognitiveLayer(config) {
|
|
|
122
247
|
fetchedAt: Date.now(),
|
|
123
248
|
gatewaySlug: data.gatewaySlug,
|
|
124
249
|
};
|
|
125
|
-
promptCache.set(
|
|
250
|
+
promptCache.set(cacheKey, entry);
|
|
251
|
+
logger.debug("Prompt resolved payload", {
|
|
252
|
+
slug,
|
|
253
|
+
resolvedSlug: entry.slug,
|
|
254
|
+
version: entry.version,
|
|
255
|
+
promptId: entry.promptId,
|
|
256
|
+
contentLength: entry.content.length,
|
|
257
|
+
gatewaySlug: (_a = entry.gatewaySlug) !== null && _a !== void 0 ? _a : null,
|
|
258
|
+
});
|
|
126
259
|
logger.info("Prompt resolved", { slug, version: entry.version });
|
|
127
260
|
return entry;
|
|
128
261
|
};
|
|
@@ -189,9 +322,25 @@ function createCognitiveLayer(config) {
|
|
|
189
322
|
if (systemPromptToAdd === undefined) {
|
|
190
323
|
try {
|
|
191
324
|
const url = `${baseUrl}/api/cognitive/snapshot?userId=${userId}`;
|
|
325
|
+
logger.debug("Fetching snapshot from backend", {
|
|
326
|
+
userId,
|
|
327
|
+
projectId,
|
|
328
|
+
sessionId,
|
|
329
|
+
url,
|
|
330
|
+
baseUrl,
|
|
331
|
+
apiKeyHint: maskSecret(clConfig.apiKey),
|
|
332
|
+
});
|
|
192
333
|
const res = await fetch(url, {
|
|
193
334
|
headers: { "Authorization": `Bearer ${clConfig.apiKey}` },
|
|
194
335
|
});
|
|
336
|
+
logger.debug("Snapshot response received", {
|
|
337
|
+
userId,
|
|
338
|
+
projectId,
|
|
339
|
+
sessionId,
|
|
340
|
+
status: res.status,
|
|
341
|
+
ok: res.ok,
|
|
342
|
+
contentType: res.headers.get("content-type"),
|
|
343
|
+
});
|
|
195
344
|
if (res.ok) {
|
|
196
345
|
const data = await res.json();
|
|
197
346
|
const systemBlock = data.systemBlock || "";
|
|
@@ -224,7 +373,15 @@ ${userContextBlock || "None"}
|
|
|
224
373
|
});
|
|
225
374
|
}
|
|
226
375
|
else {
|
|
376
|
+
const body = await res.text();
|
|
227
377
|
logger.warn("Snapshot fetch failed", { status: res.status });
|
|
378
|
+
logger.debug("Snapshot response body preview", {
|
|
379
|
+
userId,
|
|
380
|
+
projectId,
|
|
381
|
+
sessionId,
|
|
382
|
+
status: res.status,
|
|
383
|
+
bodyPreview: previewText(body),
|
|
384
|
+
});
|
|
228
385
|
systemPromptToAdd = "";
|
|
229
386
|
sessionSnapshots.set(sessionKey, systemPromptToAdd);
|
|
230
387
|
}
|
|
@@ -250,7 +407,8 @@ ${userContextBlock || "None"}
|
|
|
250
407
|
return Object.assign(Object.assign({}, nextParams), { prompt: messagesWithMemory });
|
|
251
408
|
},
|
|
252
409
|
async wrapGenerate({ doGenerate, params }) {
|
|
253
|
-
var _a
|
|
410
|
+
var _a;
|
|
411
|
+
const startedAt = new Date();
|
|
254
412
|
let result;
|
|
255
413
|
try {
|
|
256
414
|
result = await doGenerate();
|
|
@@ -261,28 +419,57 @@ ${userContextBlock || "None"}
|
|
|
261
419
|
throw err;
|
|
262
420
|
}
|
|
263
421
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
422
|
+
const endedAt = new Date();
|
|
264
423
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
265
424
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
266
|
-
const messagesInput = params.
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
425
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
426
|
+
// Build assistant message from result.content (V2/V3 GenerateResult)
|
|
427
|
+
const resultContent = Array.isArray(result === null || result === void 0 ? void 0 : result.content) ? result.content : [];
|
|
428
|
+
const assistantParts = [];
|
|
429
|
+
for (const part of resultContent) {
|
|
430
|
+
if ((part === null || part === void 0 ? void 0 : part.type) === 'text') {
|
|
431
|
+
assistantParts.push({ type: 'text', text: part.text });
|
|
432
|
+
}
|
|
433
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-call') {
|
|
434
|
+
assistantParts.push({
|
|
435
|
+
type: 'tool-call',
|
|
436
|
+
toolCallId: part.toolCallId,
|
|
437
|
+
toolName: part.toolName,
|
|
438
|
+
input: part.input,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
else if ((part === null || part === void 0 ? void 0 : part.type) === 'tool-result') {
|
|
442
|
+
assistantParts.push({
|
|
443
|
+
type: 'tool-result',
|
|
444
|
+
toolCallId: part.toolCallId,
|
|
445
|
+
toolName: part.toolName,
|
|
446
|
+
result: part.result,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const assistantMessage = assistantParts.length > 0
|
|
451
|
+
? [{ role: "assistant", content: assistantParts }]
|
|
270
452
|
: [];
|
|
271
|
-
const finalMessages =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
logConversation(Object.assign({ userId,
|
|
453
|
+
const finalMessages = [...messagesInput, ...assistantMessage];
|
|
454
|
+
const { requestPreview, responsePreview } = buildTracePreviews(finalMessages);
|
|
455
|
+
const spans = buildTraceSpansFromMessages(finalMessages);
|
|
456
|
+
logConversation(Object.assign(Object.assign({ userId,
|
|
275
457
|
projectId,
|
|
276
458
|
sessionId, messages: finalMessages, modelId, usage: result.usage }, (promptMeta && {
|
|
277
459
|
promptSlug: promptMeta.promptSlug,
|
|
278
460
|
promptVersion: promptMeta.promptVersion,
|
|
279
461
|
promptId: promptMeta.promptId,
|
|
280
|
-
}))
|
|
462
|
+
})), { traceId: (0, crypto_1.randomUUID)(), requestPreview,
|
|
463
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
464
|
+
appId: clConfig.appId,
|
|
465
|
+
}, spans })).then(() => triggerProcessing(userId, projectId, sessionId));
|
|
281
466
|
}
|
|
282
467
|
return result;
|
|
283
468
|
},
|
|
284
469
|
async wrapStream({ doStream, params }) {
|
|
285
470
|
var _a;
|
|
471
|
+
const startedAt = new Date();
|
|
472
|
+
const traceId = (0, crypto_1.randomUUID)();
|
|
286
473
|
let result;
|
|
287
474
|
try {
|
|
288
475
|
logger.debug("Starting doStream with params", JSON.stringify(params, null, 2));
|
|
@@ -297,13 +484,16 @@ ${userContextBlock || "None"}
|
|
|
297
484
|
if (isValidId(userId) && isValidId(sessionId)) {
|
|
298
485
|
const sessionKey = `${userId}:${projectId}:${sessionId}`;
|
|
299
486
|
const promptMeta = sessionPromptMetadata.get(sessionKey);
|
|
300
|
-
const messagesInput = params.
|
|
487
|
+
const messagesInput = params.prompt || params.messages || [];
|
|
301
488
|
const resultMessages = (_a = result === null || result === void 0 ? void 0 : result.response) === null || _a === void 0 ? void 0 : _a.messages;
|
|
302
489
|
const finalMessages = Array.isArray(resultMessages) && resultMessages.length > 0
|
|
303
490
|
? resultMessages
|
|
304
491
|
: messagesInput;
|
|
305
492
|
let streamUsage;
|
|
306
493
|
let accumulatedText = '';
|
|
494
|
+
const toolCallInputs = new Map();
|
|
495
|
+
const completedToolCalls = [];
|
|
496
|
+
const completedToolResults = [];
|
|
307
497
|
const originalStream = result.stream;
|
|
308
498
|
const transformStream = new TransformStream({
|
|
309
499
|
transform(chunk, controller) {
|
|
@@ -313,19 +503,72 @@ ${userContextBlock || "None"}
|
|
|
313
503
|
if (chunk.type === 'finish' && chunk.usage) {
|
|
314
504
|
streamUsage = chunk.usage;
|
|
315
505
|
}
|
|
506
|
+
// Capture tool-call stream chunks (V2/V3 shared types)
|
|
507
|
+
if (chunk.type === 'tool-input-start') {
|
|
508
|
+
toolCallInputs.set(chunk.id, { toolName: chunk.toolName, chunks: [] });
|
|
509
|
+
}
|
|
510
|
+
if (chunk.type === 'tool-input-delta') {
|
|
511
|
+
const entry = toolCallInputs.get(chunk.id);
|
|
512
|
+
if (entry)
|
|
513
|
+
entry.chunks.push(chunk.delta);
|
|
514
|
+
}
|
|
515
|
+
if (chunk.type === 'tool-call') {
|
|
516
|
+
completedToolCalls.push({
|
|
517
|
+
type: 'tool-call',
|
|
518
|
+
toolCallId: chunk.toolCallId,
|
|
519
|
+
toolName: chunk.toolName,
|
|
520
|
+
input: chunk.input,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (chunk.type === 'tool-result') {
|
|
524
|
+
completedToolResults.push({
|
|
525
|
+
type: 'tool-result',
|
|
526
|
+
toolCallId: chunk.toolCallId,
|
|
527
|
+
toolName: chunk.toolName,
|
|
528
|
+
result: chunk.result,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
316
531
|
controller.enqueue(chunk);
|
|
317
532
|
},
|
|
318
|
-
flush() {
|
|
319
|
-
const
|
|
320
|
-
|
|
533
|
+
async flush() {
|
|
534
|
+
const endedAt = new Date();
|
|
535
|
+
// Finalize any tool calls from incremental input chunks
|
|
536
|
+
for (const [id, entry] of toolCallInputs) {
|
|
537
|
+
// Only add if not already captured via a tool-call chunk
|
|
538
|
+
if (!completedToolCalls.some((tc) => tc.toolCallId === id)) {
|
|
539
|
+
completedToolCalls.push({
|
|
540
|
+
type: 'tool-call',
|
|
541
|
+
toolCallId: id,
|
|
542
|
+
toolName: entry.toolName,
|
|
543
|
+
input: entry.chunks.join(''),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const assistantParts = [];
|
|
548
|
+
if (accumulatedText)
|
|
549
|
+
assistantParts.push({ type: "text", text: accumulatedText });
|
|
550
|
+
for (const tc of completedToolCalls)
|
|
551
|
+
assistantParts.push(tc);
|
|
552
|
+
const allMessages = assistantParts.length > 0
|
|
553
|
+
? [...finalMessages, { role: "assistant", content: assistantParts }]
|
|
321
554
|
: finalMessages;
|
|
322
|
-
|
|
555
|
+
if (completedToolResults.length > 0) {
|
|
556
|
+
allMessages.push({ role: "tool", content: completedToolResults });
|
|
557
|
+
}
|
|
558
|
+
const { requestPreview, responsePreview } = buildTracePreviews(allMessages);
|
|
559
|
+
const spans = buildTraceSpansFromMessages(allMessages);
|
|
560
|
+
await logConversation(Object.assign(Object.assign({ userId,
|
|
323
561
|
projectId,
|
|
324
562
|
sessionId, messages: allMessages, modelId, usage: streamUsage }, (promptMeta && {
|
|
325
563
|
promptSlug: promptMeta.promptSlug,
|
|
326
564
|
promptVersion: promptMeta.promptVersion,
|
|
327
565
|
promptId: promptMeta.promptId,
|
|
328
|
-
}))
|
|
566
|
+
})), { traceId,
|
|
567
|
+
requestPreview,
|
|
568
|
+
responsePreview, state: "completed", startedAt: startedAt.toISOString(), endedAt: endedAt.toISOString(), durationMs: endedAt.getTime() - startedAt.getTime(), metadata: {
|
|
569
|
+
appId: clConfig.appId,
|
|
570
|
+
}, spans }));
|
|
571
|
+
triggerProcessing(userId, projectId, sessionId);
|
|
329
572
|
}
|
|
330
573
|
});
|
|
331
574
|
result.stream = originalStream.pipeThrough(transformStream);
|
|
@@ -376,60 +619,90 @@ ${userContextBlock || "None"}
|
|
|
376
619
|
middleware: buildMiddleware(userId, projectId, sessionId, modelId),
|
|
377
620
|
});
|
|
378
621
|
// Track session settings on the model for use in cl.streamText/cl.generateText
|
|
379
|
-
if
|
|
380
|
-
|
|
622
|
+
// Always store if userId is valid — sessionId may be missing but userId is still
|
|
623
|
+
// needed for prompt resolution (e.g. A/B test assignment)
|
|
624
|
+
if (isValidId(userId)) {
|
|
625
|
+
wrappedModel[SESSION_KEY] = { userId, projectId, sessionId: isValidId(sessionId) ? sessionId : undefined };
|
|
381
626
|
}
|
|
382
627
|
return wrappedModel;
|
|
383
628
|
};
|
|
384
629
|
const clStreamText = async (options) => {
|
|
385
630
|
const { prompt: promptConfig } = options, rest = __rest(options, ["prompt"]);
|
|
386
|
-
// Resolve and interpolate prompt
|
|
387
|
-
const resolved = await resolvePrompt(promptConfig.slug);
|
|
388
|
-
const system = promptConfig.variables
|
|
389
|
-
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
390
|
-
: resolved.content;
|
|
391
|
-
// Store prompt metadata for the session (read by middleware during logging)
|
|
392
631
|
const session = options.model[SESSION_KEY];
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
632
|
+
// Resolve and interpolate prompt (graceful fallback on failure)
|
|
633
|
+
let resolved = null;
|
|
634
|
+
try {
|
|
635
|
+
resolved = await resolvePrompt(promptConfig.slug, session === null || session === void 0 ? void 0 : session.userId);
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
logger.warn(`Failed to resolve prompt "${promptConfig.slug}", streaming without system prompt.`, err);
|
|
639
|
+
}
|
|
640
|
+
let system;
|
|
641
|
+
if (resolved) {
|
|
642
|
+
system = promptConfig.variables
|
|
643
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
644
|
+
: resolved.content;
|
|
645
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
646
|
+
if (session === null || session === void 0 ? void 0 : session.sessionId) {
|
|
647
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
648
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
649
|
+
promptSlug: resolved.slug,
|
|
650
|
+
promptVersion: resolved.version,
|
|
651
|
+
promptId: resolved.promptId,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
logger.info("cl.streamText called", {
|
|
655
|
+
slug: promptConfig.slug,
|
|
656
|
+
version: resolved.version,
|
|
657
|
+
systemLength: system.length,
|
|
399
658
|
});
|
|
400
659
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
407
|
-
return (0, ai_1.streamText)(Object.assign(Object.assign({}, rest), { model, system }));
|
|
660
|
+
else {
|
|
661
|
+
logger.info("cl.streamText called without resolved prompt", {
|
|
662
|
+
slug: promptConfig.slug,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const model = resolveModel(options.model, resolved === null || resolved === void 0 ? void 0 : resolved.gatewaySlug);
|
|
666
|
+
return (0, ai_1.streamText)(Object.assign(Object.assign(Object.assign({}, rest), { model }), (system && { system })));
|
|
408
667
|
};
|
|
409
668
|
const clGenerateText = async (options) => {
|
|
410
669
|
const { prompt: promptConfig } = options, rest = __rest(options, ["prompt"]);
|
|
411
|
-
// Resolve and interpolate prompt
|
|
412
|
-
const resolved = await resolvePrompt(promptConfig.slug);
|
|
413
|
-
const system = promptConfig.variables
|
|
414
|
-
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
415
|
-
: resolved.content;
|
|
416
|
-
// Store prompt metadata for the session (read by middleware during logging)
|
|
417
670
|
const session = options.model[SESSION_KEY];
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
671
|
+
// Resolve and interpolate prompt (graceful fallback on failure)
|
|
672
|
+
let resolved = null;
|
|
673
|
+
try {
|
|
674
|
+
resolved = await resolvePrompt(promptConfig.slug, session === null || session === void 0 ? void 0 : session.userId);
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
logger.warn(`Failed to resolve prompt "${promptConfig.slug}", generating without system prompt.`, err);
|
|
678
|
+
}
|
|
679
|
+
let system;
|
|
680
|
+
if (resolved) {
|
|
681
|
+
system = promptConfig.variables
|
|
682
|
+
? interpolateTemplate(resolved.content, promptConfig.variables)
|
|
683
|
+
: resolved.content;
|
|
684
|
+
// Store prompt metadata for the session (read by middleware during logging)
|
|
685
|
+
if (session === null || session === void 0 ? void 0 : session.sessionId) {
|
|
686
|
+
const sessionKey = `${session.userId}:${session.projectId}:${session.sessionId}`;
|
|
687
|
+
sessionPromptMetadata.set(sessionKey, {
|
|
688
|
+
promptSlug: resolved.slug,
|
|
689
|
+
promptVersion: resolved.version,
|
|
690
|
+
promptId: resolved.promptId,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
logger.info("cl.generateText called", {
|
|
694
|
+
slug: promptConfig.slug,
|
|
695
|
+
version: resolved.version,
|
|
696
|
+
systemLength: system.length,
|
|
424
697
|
});
|
|
425
698
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
const model = resolveModel(options.model, resolved.gatewaySlug);
|
|
432
|
-
return (0, ai_1.generateText)(Object.assign(Object.assign({}, rest), { model, system }));
|
|
699
|
+
else {
|
|
700
|
+
logger.info("cl.generateText called without resolved prompt", {
|
|
701
|
+
slug: promptConfig.slug,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
const model = resolveModel(options.model, resolved === null || resolved === void 0 ? void 0 : resolved.gatewaySlug);
|
|
705
|
+
return (0, ai_1.generateText)(Object.assign(Object.assign(Object.assign({}, rest), { model }), (system && { system })));
|
|
433
706
|
};
|
|
434
707
|
// Return the model wrapper function with streamText/generateText attached
|
|
435
708
|
return Object.assign(clWrapper, {
|