Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/networking/node/chatStream.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Raw } from '@vscode/prompt-tsx';
7
import { hash } from '../../../util/vs/base/common/hash';
8
import { LRUCache } from '../../../util/vs/base/common/map';
9
import { generateUuid } from '../../../util/vs/base/common/uuid';
10
import { toTextParts } from '../../chat/common/globalStringUtils';
11
import { ILogService } from '../../log/common/logService';
12
import { ITelemetryService, multiplexProperties } from '../../telemetry/common/telemetry';
13
import { TelemetryData } from '../../telemetry/common/telemetryData';
14
import { APIJsonData, CAPIChatMessage, ChatCompletion, rawMessageToCAPI } from '../common/openai';
15
import { FinishedCompletion, convertToAPIJsonData } from './stream';
16
17
// TODO @lramos15 - Find a better file for this, since this file is for the chat stream and should not be telemetry related
18
export function sendEngineMessagesLengthTelemetry(telemetryService: ITelemetryService, messages: CAPIChatMessage[], telemetryData: TelemetryData, isOutput: boolean, logService?: ILogService) {
19
const messageType = isOutput ? 'output' : 'input';
20
21
// Get the unique model call ID - it should already be set in the base telemetryData
22
const modelCallId = telemetryData.properties.modelCallId as string;
23
if (!modelCallId) {
24
// This shouldn't happen if the ID was properly generated at request start
25
logService?.warn('[TELEMETRY] modelCallId not found in telemetryData, input/output messages cannot be linked');
26
return;
27
}
28
29
// Create messages with content and tool_calls arguments replaced by length
30
const messagesWithLength = messages.map(msg => {
31
const processedMsg: any = {
32
...msg, // This preserves ALL existing fields including tool_calls, tool_call_id, copilot_references, etc.
33
content: typeof msg.content === 'string'
34
? msg.content.length
35
: Array.isArray(msg.content)
36
? msg.content.reduce((total: number, part: any) => {
37
if (typeof part === 'string') {
38
return total + part.length;
39
}
40
if (part.type === 'text') {
41
return total + (part.text?.length || 0);
42
}
43
return total;
44
}, 0)
45
: 0,
46
};
47
48
// Process tool_calls if present
49
if ('tool_calls' in msg && msg.tool_calls && Array.isArray(msg.tool_calls)) {
50
processedMsg.tool_calls = msg.tool_calls.map((toolCall: any) => ({
51
...toolCall,
52
function: toolCall.function ? {
53
...toolCall.function,
54
arguments: typeof toolCall.function.arguments === 'string'
55
? toolCall.function.arguments.length
56
: toolCall.function.arguments
57
} : toolCall.function
58
}));
59
}
60
61
return processedMsg;
62
});
63
64
// Process properties to replace request.option.tools.* field values with their length
65
const processedProperties: { [key: string]: string } = {};
66
for (const [key, value] of Object.entries(telemetryData.properties)) {
67
if (key.startsWith('request.option.tools')) {
68
// Replace the content with its length
69
if (typeof value === 'string') {
70
// If it's a string, it might be a JSON array, try to parse it
71
try {
72
const parsed = JSON.parse(value);
73
if (Array.isArray(parsed)) {
74
processedProperties[key] = parsed.length.toString();
75
} else {
76
processedProperties[key] = value.length.toString();
77
}
78
} catch {
79
// If parsing fails, just use string length
80
processedProperties[key] = value.length.toString();
81
}
82
} else if (Array.isArray(value)) {
83
processedProperties[key] = (value as any[]).length.toString();
84
} else {
85
processedProperties[key] = '0';
86
}
87
} else {
88
processedProperties[key] = value;
89
}
90
}
91
92
const telemetryDataWithPrompt = TelemetryData.createAndMarkAsIssued({
93
...processedProperties,
94
messagesJson: JSON.stringify(messagesWithLength),
95
message_direction: messageType,
96
modelCallId: modelCallId, // Include at telemetry event level too
97
}, telemetryData.measurements);
98
99
telemetryService.sendEnhancedGHTelemetryEvent('engine.messages.length', multiplexProperties(telemetryDataWithPrompt.properties), telemetryDataWithPrompt.measurements);
100
telemetryService.sendInternalMSFTTelemetryEvent('engine.messages.length', multiplexProperties(telemetryDataWithPrompt.properties), telemetryDataWithPrompt.measurements);
101
}
102
103
// LRU cache from message hash to UUID to ensure same content gets same UUID (limit: 1000 entries)
104
const messageHashToUuid = new LRUCache<string, string>(1000);
105
106
// LRU cache from request options hash to requestOptionsId to ensure same options get same ID (limit: 500 entries)
107
const requestOptionsHashToId = new LRUCache<string, string>(500);
108
109
// LRU cache to track headerRequestId to requestTurn mapping for temporal location tracking along main agent flow (limit: 1000 entries)
110
const headerRequestIdTracker = new LRUCache<string, number>(1000);
111
112
// Track most recent conversation headerRequestId for linking supplementary calls
113
const mainHeaderRequestIdTracker: { headerRequestId: string | null } = {
114
headerRequestId: null
115
};
116
117
// Track conversation turns for model.request.added events (limit: 100 entries)
118
const conversationTracker = new LRUCache<string, number>(100);
119
120
/**
121
* Updates the headerRequestIdTracker with the given headerRequestId.
122
* If the headerRequestId already exists, increments its requestTurn.
123
* If it doesn't exist, adds it with requestTurn = 1.
124
* Returns the current requestTurn for the headerRequestId.
125
*/
126
function updateHeaderRequestIdTracker(headerRequestId: string): number {
127
const currentTurn = headerRequestIdTracker.get(headerRequestId);
128
if (currentTurn !== undefined) {
129
// HeaderRequestId exists, increment turn
130
const newTurn = currentTurn + 1;
131
headerRequestIdTracker.set(headerRequestId, newTurn);
132
return newTurn;
133
} else {
134
// New headerRequestId, set turn to 1
135
headerRequestIdTracker.set(headerRequestId, 1);
136
return 1;
137
}
138
}
139
140
/**
141
* Updates the conversationTracker with the given conversationId.
142
* If the conversationId already exists, increments its turn.
143
* If it doesn't exist, adds it with turn = 1.
144
* Returns the current conversationTurn for the conversationId.
145
*/
146
function updateConversationTracker(conversationId: string): number {
147
const currentTurn = conversationTracker.get(conversationId);
148
if (currentTurn !== undefined) {
149
// ConversationId exists, increment turn
150
const newTurn = currentTurn + 1;
151
conversationTracker.set(conversationId, newTurn);
152
return newTurn;
153
} else {
154
// New conversationId, set turn to 1
155
conversationTracker.set(conversationId, 1);
156
return 1;
157
}
158
}
159
160
// ===== MODEL TELEMETRY FUNCTIONS =====
161
// These functions send 'model...' events and are grouped together for better organization
162
163
function sendModelRequestOptionsTelemetry(telemetryService: ITelemetryService, telemetryData: TelemetryData, logService?: ILogService): string | undefined {
164
// Extract all request.option.* properties
165
const requestOptions: { [key: string]: string } = {};
166
for (const [key, value] of Object.entries(telemetryData.properties)) {
167
if (key.startsWith('request.option.')) {
168
requestOptions[key] = value;
169
}
170
}
171
172
// Only process if there are request options
173
if (Object.keys(requestOptions).length === 0) {
174
return undefined;
175
}
176
177
// Extract context properties
178
const conversationId = telemetryData.properties.conversationId || telemetryData.properties.sessionId || 'unknown';
179
const headerRequestId = telemetryData.properties.headerRequestId || 'unknown';
180
181
// Create a hash of the request options to detect duplicates
182
const requestOptionsHash = hash(requestOptions).toString();
183
184
// Get existing requestOptionsId for this content, or generate a new one
185
let requestOptionsId = requestOptionsHashToId.get(requestOptionsHash);
186
if (!requestOptionsId) {
187
// This is a new set of request options, generate ID and send the event
188
requestOptionsId = generateUuid();
189
requestOptionsHashToId.set(requestOptionsHash, requestOptionsId);
190
} else {
191
// Skip sending model.request.options.added if this exact request options have already been logged
192
return requestOptionsId;
193
}
194
195
// Convert request options to JSON string for chunking
196
const requestOptionsJsonString = JSON.stringify(requestOptions);
197
const maxChunkSize = 8000;
198
199
// Split request options JSON into chunks of 8000 characters or less
200
const chunks: string[] = [];
201
for (let i = 0; i < requestOptionsJsonString.length; i += maxChunkSize) {
202
chunks.push(requestOptionsJsonString.substring(i, i + maxChunkSize));
203
}
204
205
// Send one telemetry event per chunk
206
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
207
const requestOptionsData = TelemetryData.createAndMarkAsIssued({
208
requestOptionsId,
209
conversationId,
210
headerRequestId,
211
requestOptionsJson: chunks[chunkIndex], // Store chunk of request options JSON
212
chunkIndex: chunkIndex.toString(), // 0-based chunk index for ordering
213
totalChunks: chunks.length.toString(), // Total number of chunks for this request
214
}, telemetryData.measurements); // Include measurements from original telemetryData
215
216
telemetryService.sendInternalMSFTTelemetryEvent('model.request.options.added', requestOptionsData.properties, requestOptionsData.measurements);
217
}
218
219
return requestOptionsId;
220
}
221
222
function sendNewRequestAddedTelemetry(telemetryService: ITelemetryService, telemetryData: TelemetryData, logService?: ILogService): void {
223
// This function captures user-level request context (username, session info, user preferences, etc.)
224
// It's called once per unique user request (identified by headerRequestId)
225
// It excludes message content and request options which are captured separately
226
227
// Extract headerRequestId to check for uniqueness
228
const headerRequestId = telemetryData.properties.headerRequestId;
229
if (!headerRequestId) {
230
return;
231
}
232
233
// Check if this is a conversation mode (has conversationId) or supplementary mode
234
// This must be done BEFORE the duplicate check to ensure tracker is always updated
235
const conversationId = telemetryData.properties.conversationId;
236
if (conversationId) {
237
// Conversation mode: update tracker with current headerRequestId
238
mainHeaderRequestIdTracker.headerRequestId = headerRequestId;
239
}
240
241
// Check if we've already processed this headerRequestId
242
if (headerRequestIdTracker.has(headerRequestId)) {
243
return;
244
}
245
246
// Update conversation tracker and get conversation turn only for new headerRequestIds
247
let conversationTurn: number | undefined;
248
if (conversationId) {
249
conversationTurn = updateConversationTracker(conversationId);
250
}
251
252
// Filter out properties that start with "message" or "request.option" and exclude modelCallId
253
const filteredProperties: { [key: string]: string } = {};
254
for (const [key, value] of Object.entries(telemetryData.properties)) {
255
if (!key.startsWith('message') && !key.startsWith('request.option') && key !== 'modelCallId') {
256
filteredProperties[key] = value;
257
}
258
}
259
260
// Add conversationTurn if conversationId is present
261
if (conversationTurn !== undefined) {
262
filteredProperties.conversationTurn = conversationTurn.toString();
263
}
264
265
// For supplementary mode: add conversation linking fields if we have tracked data
266
if (!conversationId && mainHeaderRequestIdTracker.headerRequestId) {
267
const mostRecentTurn = headerRequestIdTracker.get(mainHeaderRequestIdTracker.headerRequestId);
268
filteredProperties.mostRecentConversationHeaderRequestId = mainHeaderRequestIdTracker.headerRequestId;
269
if (mostRecentTurn !== undefined) {
270
filteredProperties.mostRecentConversationHeaderRequestIdTurn = mostRecentTurn.toString();
271
}
272
}
273
274
// Create telemetry data for the request
275
const requestData = TelemetryData.createAndMarkAsIssued(filteredProperties, telemetryData.measurements);
276
277
telemetryService.sendInternalMSFTTelemetryEvent('model.request.added', requestData.properties, requestData.measurements);
278
}
279
280
function sendIndividualMessagesTelemetry(telemetryService: ITelemetryService, messages: CAPIChatMessage[], telemetryData: TelemetryData, messageDirection: 'input' | 'output', logService?: ILogService): Array<{ uuid: string; headerRequestId: string }> {
281
const messageData: Array<{ uuid: string; headerRequestId: string }> = [];
282
283
for (const message of messages) {
284
// Extract context properties with fallbacks
285
const conversationId = telemetryData.properties.conversationId || telemetryData.properties.sessionId || 'unknown';
286
const headerRequestId = telemetryData.properties.headerRequestId || 'unknown';
287
288
// Create a hash of the message content AND headerRequestId to detect duplicates
289
// Including headerRequestId ensures same message content with different headerRequestIds gets separate UUIDs
290
const messageHash = hash({
291
role: message.role,
292
content: message.content,
293
headerRequestId: headerRequestId, // Include headerRequestId in hash for proper deduplication
294
...(('tool_calls' in message && message.tool_calls) && { tool_calls: message.tool_calls }),
295
...(('tool_call_id' in message && message.tool_call_id) && { tool_call_id: message.tool_call_id })
296
}).toString();
297
298
// Get existing UUID for this message content + headerRequestId combination, or generate a new one
299
let messageUuid = messageHashToUuid.get(messageHash);
300
301
if (!messageUuid) {
302
// This is a new message, generate UUID and send the event
303
messageUuid = generateUuid();
304
messageHashToUuid.set(messageHash, messageUuid);
305
} else {
306
// Always collect UUIDs and headerRequestIds for model call tracking
307
messageData.push({ uuid: messageUuid, headerRequestId });
308
309
// Skip sending model.message.added if this exact message has already been logged
310
continue;
311
}
312
313
// Always collect UUIDs and headerRequestIds for model call tracking
314
messageData.push({ uuid: messageUuid, headerRequestId });
315
316
// Convert message to JSON string for chunking
317
const messageJsonString = JSON.stringify(message);
318
319
const maxChunkSize = 8000;
320
321
// Split messageJson into chunks of 8000 characters or less
322
const chunks: string[] = [];
323
for (let i = 0; i < messageJsonString.length; i += maxChunkSize) {
324
chunks.push(messageJsonString.substring(i, i + maxChunkSize));
325
}
326
327
// Send one telemetry event per chunk
328
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
329
const messageData = TelemetryData.createAndMarkAsIssued({
330
messageUuid,
331
messageDirection,
332
conversationId,
333
headerRequestId,
334
messageJson: chunks[chunkIndex], // Store chunk of message JSON
335
chunkIndex: chunkIndex.toString(), // 0-based chunk index for ordering
336
totalChunks: chunks.length.toString(), // Total number of chunks for this message
337
}, telemetryData.measurements); // Include measurements from original telemetryData
338
339
telemetryService.sendInternalMSFTTelemetryEvent('model.message.added', messageData.properties, messageData.measurements);
340
}
341
}
342
343
return messageData; // Return collected message data with UUIDs and headerRequestIds
344
}
345
346
function sendModelCallTelemetry(telemetryService: ITelemetryService, messageData: Array<{ uuid: string; headerRequestId: string }>, telemetryData: TelemetryData, messageDirection: 'input' | 'output', logService?: ILogService) {
347
// Get the unique model call ID
348
const modelCallId = telemetryData.properties.modelCallId as string;
349
if (!modelCallId) {
350
return;
351
}
352
353
// For input calls, process request options and get requestOptionsId
354
let requestOptionsId: string | undefined;
355
if (messageDirection === 'input') {
356
requestOptionsId = sendModelRequestOptionsTelemetry(telemetryService, telemetryData, logService);
357
}
358
359
// Extract trajectory context
360
const conversationId = telemetryData.properties.conversationId || telemetryData.properties.sessionId || 'unknown';
361
362
// Group messages by headerRequestId
363
const messagesByHeaderRequestId = new Map<string, string[]>();
364
365
for (const item of messageData) {
366
if (!messagesByHeaderRequestId.has(item.headerRequestId)) {
367
messagesByHeaderRequestId.set(item.headerRequestId, []);
368
}
369
messagesByHeaderRequestId.get(item.headerRequestId)!.push(item.uuid);
370
}
371
372
// Send separate telemetry events for each headerRequestId
373
for (const [headerRequestId, messageUuids] of messagesByHeaderRequestId) {
374
const eventName = messageDirection === 'input' ? 'model.modelCall.input' : 'model.modelCall.output';
375
376
// Update headerRequestIdTracker and get requestTurn only for input events
377
let requestTurn: number | undefined;
378
if (messageDirection === 'input') {
379
requestTurn = updateHeaderRequestIdTracker(headerRequestId);
380
}
381
382
// Convert messageUuids to JSON string for chunking
383
const messageUuidsJsonString = JSON.stringify(messageUuids);
384
const maxChunkSize = 8000;
385
386
// Split messageUuids JSON into chunks of 8000 characters or less
387
const chunks: string[] = [];
388
for (let i = 0; i < messageUuidsJsonString.length; i += maxChunkSize) {
389
chunks.push(messageUuidsJsonString.substring(i, i + maxChunkSize));
390
}
391
392
// Send one telemetry event per chunk
393
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
394
const parentToolCallId = telemetryData.properties.parentToolCallId;
395
const parentHeaderRequestId = telemetryData.properties.parentHeaderRequestId;
396
const modelCallData = TelemetryData.createAndMarkAsIssued({
397
modelCallId,
398
conversationId, // Trajectory identifier linking main and supplementary calls
399
headerRequestId, // Specific to this set of messages
400
messageDirection,
401
messageUuids: chunks[chunkIndex], // Store chunk of messageUuids JSON
402
chunkIndex: chunkIndex.toString(), // 0-based chunk index for ordering
403
totalChunks: chunks.length.toString(), // Total number of chunks for this headerRequestId
404
messageCount: messageUuids.length.toString(),
405
...(requestTurn !== undefined && { requestTurn: requestTurn.toString() }), // Add requestTurn only for input calls
406
...(requestOptionsId && { requestOptionsId }), // Add requestOptionsId for input calls
407
...(telemetryData.properties.turnIndex && { turnIndex: telemetryData.properties.turnIndex }), // Add turnIndex from original telemetryData
408
...(parentToolCallId && { parentToolCallId }), // Link subagent calls to parent tool invocation
409
...(parentHeaderRequestId && { parentHeaderRequestId }), // Link subagent calls to parent HTTP request
410
}, telemetryData.measurements); // Include measurements from original telemetryData
411
412
telemetryService.sendInternalMSFTTelemetryEvent(eventName, modelCallData.properties, modelCallData.measurements);
413
}
414
}
415
}
416
417
function sendModelTelemetryEvents(telemetryService: ITelemetryService, messages: CAPIChatMessage[], telemetryData: TelemetryData, isOutput: boolean, logService?: ILogService): void {
418
// Skip model telemetry events for XtabProvider and api.* message sources
419
const messageSource = telemetryData.properties.messageSource as string;
420
if (messageSource === 'XtabProvider' || (messageSource && messageSource.startsWith('api.'))) {
421
return;
422
}
423
424
// Send model.request.added event for user input requests (once per headerRequestId)
425
// This captures user-level context (username, session info, etc.) for the user's request
426
// Note: This is different from model-level context which is captured in model.modelCall events
427
if (!isOutput) {
428
sendNewRequestAddedTelemetry(telemetryService, telemetryData, logService);
429
}
430
431
// Skip input message telemetry for retry requests to avoid duplicates
432
// Retry requests are identified by the presence of retryAfterFilterCategory property
433
const isRetryRequest = telemetryData.properties.retryAfterFilterCategory !== undefined;
434
if (!isOutput && isRetryRequest) {
435
return;
436
}
437
438
// Send individual message telemetry for deduplication tracking and collect UUIDs with their headerRequestIds
439
const messageData = sendIndividualMessagesTelemetry(telemetryService, messages, telemetryData, isOutput ? 'output' : 'input', logService);
440
441
// Send model call telemetry grouped by headerRequestId (separate events for different headerRequestIds)
442
// For input calls, this also handles request options deduplication
443
// Always send model call telemetry regardless of whether messages are new or duplicates to ensure every model invocation is tracked
444
sendModelCallTelemetry(telemetryService, messageData, telemetryData, isOutput ? 'output' : 'input', logService);
445
}
446
447
// ===== END MODEL TELEMETRY FUNCTIONS =====
448
449
export function sendEngineMessagesTelemetry(telemetryService: ITelemetryService, messages: CAPIChatMessage[], telemetryData: TelemetryData, isOutput: boolean, logService?: ILogService) {
450
const telemetryDataWithPrompt = telemetryData.extendedBy({
451
messagesJson: JSON.stringify(messages),
452
});
453
454
telemetryService.sendEnhancedGHTelemetryEvent('engine.messages', multiplexProperties(telemetryDataWithPrompt.properties), telemetryDataWithPrompt.measurements);
455
// Commenting this out to test a new deduplicated way to collect the same information using sendModelTelemetryEvents()
456
// TO DO remove this line completely if the new way allows for complete reconstruction of entire message arrays with much lower drop rate
457
//telemetryService.sendInternalMSFTTelemetryEvent('engine.messages', multiplexProperties(telemetryDataWithPrompt.properties), telemetryDataWithPrompt.measurements);
458
459
// Send all model telemetry events (model.request.added, model.message.added, model.modelCall.input/output, model.request.options.added)
460
// Comment out the line below to disable the new deduplicated model telemetry events
461
sendModelTelemetryEvents(telemetryService, messages, telemetryData, isOutput, logService);
462
463
// Also send length-only telemetry
464
sendEngineMessagesLengthTelemetry(telemetryService, messages, telemetryData, isOutput, logService);
465
}
466
467
export function sendResponsesApiCompactionTelemetry(
468
telemetryService: ITelemetryService,
469
properties: {
470
outcome: 'compaction_returned' | 'threshold_met_no_compaction';
471
headerRequestId: string;
472
gitHubRequestId: string;
473
model: string;
474
},
475
measurements: {
476
compactThreshold?: number;
477
promptTokens: number;
478
totalTokens: number;
479
}
480
): void {
481
/* __GDPR__
482
"responsesApi.compactionOutcome" : {
483
"owner": "dileepy",
484
"comment": "Tracks server-side Responses API compaction outcomes.",
485
"outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the server returned a compaction item or exceeded the threshold without returning one." },
486
"headerRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Request ID from the response headers." },
487
"gitHubRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "GitHub request ID from the response headers if present." },
488
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Model identifier reported by the response." },
489
"compactThreshold": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Compaction threshold configured for the request." },
490
"promptTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Prompt token count reported by the response." },
491
"totalTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total token count reported by the response." }
492
}
493
*/
494
telemetryService.sendGHTelemetryEvent('responsesApi.compactionOutcome', {
495
outcome: properties.outcome,
496
headerRequestId: properties.headerRequestId,
497
gitHubRequestId: properties.gitHubRequestId,
498
model: properties.model,
499
}, {
500
compactThreshold: measurements.compactThreshold,
501
promptTokens: measurements.promptTokens,
502
totalTokens: measurements.totalTokens,
503
});
504
}
505
506
export function prepareChatCompletionForReturn(
507
telemetryService: ITelemetryService,
508
logService: ILogService,
509
c: FinishedCompletion,
510
telemetryData: TelemetryData
511
): ChatCompletion {
512
let messageContent = c.solution.text.join('');
513
514
let blockFinished = false;
515
if (c.finishOffset !== undefined) {
516
// Trim solution to finishOffset returned by finishedCb
517
logService.debug(`message ${c.index}: early finish at offset ${c.finishOffset}`);
518
messageContent = messageContent.substring(0, c.finishOffset);
519
blockFinished = true;
520
}
521
522
logService.info(`message ${c.index} returned. finish reason: [${c.reason}]`);
523
logService.debug(
524
`message ${c.index} details: finishOffset: [${c.finishOffset}] completionId: [{${c.requestId.completionId}}] created: [{${c.requestId.created}}]`
525
);
526
const jsonData: APIJsonData = convertToAPIJsonData(c.solution);
527
const message: Raw.ChatMessage = {
528
role: Raw.ChatRole.Assistant,
529
content: toTextParts(messageContent),
530
};
531
532
// Create enhanced message for telemetry with usage information
533
const telemetryMessage = rawMessageToCAPI(message);
534
535
// Add request metadata to telemetry data
536
telemetryData.extendWithRequestId(c.requestId);
537
538
// Add usage information to telemetryData if available
539
let telemetryDataWithUsage = telemetryData;
540
if (c.usage) {
541
telemetryDataWithUsage = telemetryData.extendedBy({}, {
542
promptTokens: c.usage.prompt_tokens,
543
completionTokens: c.usage.completion_tokens,
544
totalTokens: c.usage.total_tokens,
545
...(c.usage.prompt_tokens_details && { cachedTokens: c.usage.prompt_tokens_details.cached_tokens }),
546
...(c.usage.completion_tokens_details && {
547
reasoningTokens: c.usage.completion_tokens_details.reasoning_tokens,
548
acceptedPredictionTokens: c.usage.completion_tokens_details.accepted_prediction_tokens,
549
rejectedPredictionTokens: c.usage.completion_tokens_details.rejected_prediction_tokens,
550
}),
551
});
552
}
553
554
sendEngineMessagesTelemetry(telemetryService, [telemetryMessage], telemetryDataWithUsage, true, logService);
555
return {
556
message: message,
557
choiceIndex: c.index,
558
requestId: c.requestId,
559
blockFinished: blockFinished,
560
finishReason: c.reason,
561
filterReason: c.filterReason,
562
error: c.error,
563
tokens: jsonData.tokens,
564
model: c.solution.model,
565
usage: c.usage,
566
telemetryData: telemetryDataWithUsage,
567
};
568
}
569
570