Resend all messages after summarization (#298236)

This commit is contained in:
Christof Marti
2026-04-10 15:32:42 +02:00
parent b064f7b2a5
commit cde30eb64e
7 changed files with 227 additions and 7 deletions

View File

@@ -103,7 +103,7 @@ export interface IToolCallingBuiltPromptEvent {
tools: LanguageModelToolInformation[];
}
export type ToolCallingLoopFetchOptions = Required<Pick<IMakeChatRequestOptions, 'messages' | 'finishedCb' | 'requestOptions' | 'userInitiatedRequest' | 'turnId'>> & Pick<IMakeChatRequestOptions, 'modelCapabilities'>;
export type ToolCallingLoopFetchOptions = Required<Pick<IMakeChatRequestOptions, 'messages' | 'finishedCb' | 'requestOptions' | 'userInitiatedRequest' | 'turnId'>> & Pick<IMakeChatRequestOptions, 'modelCapabilities' | 'summarizedAtRoundId'>;
interface StartHookResult {
/**
@@ -1189,6 +1189,28 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
this.turn.setMetadata(conversationSummary);
}
// Find the latest summarized round.
let summarizedAtRoundId: string | undefined;
for (let i = this.toolCallRounds.length - 1; i >= 0; i--) {
if (this.toolCallRounds[i].summary) {
summarizedAtRoundId = this.toolCallRounds[i].id;
break;
}
}
if (!summarizedAtRoundId) {
for (const turn of [...context.history].reverse()) {
for (const round of [...turn.rounds].reverse()) {
if (round.summary) {
summarizedAtRoundId = round.id;
break;
}
}
if (summarizedAtRoundId) {
break;
}
}
}
const endpoint = await this._endpointProvider.getChatEndpoint(this.options.request);
const tokenizer = endpoint.acquireTokenizer();
const promptTokenLength = await tokenizer.countMessagesTokens(effectiveBuildPromptResult.messages);
@@ -1272,6 +1294,7 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
const fetchResult = await this.fetch({
messages: this.applyMessagePostProcessing(effectiveBuildPromptResult.messages, { stripOrphanedToolCalls: isGeminiFamily(endpoint) }),
turnId: this.turn.id,
summarizedAtRoundId,
finishedCb: async (text, index, delta) => {
fetchStreamSource?.update(text, delta);
if (delta.copilotToolCalls) {

View File

@@ -236,6 +236,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
opts.useFetcher,
canRetryOnce,
requestKindOptions,
opts.summarizedAtRoundId,
);
response = fetchResult.result;
actualFetcher = fetchResult.fetcher;
@@ -847,6 +848,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useFetcher?: FetcherId,
canRetryOnce?: boolean,
requestKindOptions?: IBackgroundRequestOptions | ISubagentRequestOptions,
summarizedAtRoundId?: string,
): Promise<{ result: ChatResults | ChatRequestFailed | ChatRequestCanceled; fetcher?: FetcherId; bytesReceived?: number; statusCode?: number; suspendEventSeen?: boolean; resumeEventSeen?: boolean; otelSpan?: ISpanHandle }> {
const isPowerSaveBlockerEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.ChatRequestPowerSaveBlocker, this._experimentationService);
const blockerHandle = isPowerSaveBlockerEnabled && location !== ChatLocation.Other ? this._powerService.acquirePowerSaveBlocker() : undefined;
@@ -885,6 +887,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useFetcher,
canRetryOnce,
requestKindOptions,
summarizedAtRoundId,
);
return { ...fetchResult, suspendEventSeen: suspendEventSeen || undefined, resumeEventSeen: resumeEventSeen || undefined };
} catch (err) {
@@ -922,6 +925,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
useFetcher?: FetcherId,
canRetryOnce?: boolean,
requestKindOptions?: IBackgroundRequestOptions | ISubagentRequestOptions,
summarizedAtRoundId?: string,
): Promise<{ result: ChatResults | ChatRequestFailed | ChatRequestCanceled; fetcher?: FetcherId; bytesReceived?: number; statusCode?: number; otelSpan?: ISpanHandle }> {
if (cancellationToken.isCancellationRequested) {
@@ -994,6 +998,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
userInitiatedRequest,
telemetryProperties,
requestKindOptions,
summarizedAtRoundId,
);
return { ...wsResult, otelSpan };
}
@@ -1051,6 +1056,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
userInitiatedRequest: boolean | undefined,
telemetryProperties: TelemetryProperties | undefined,
requestKindOptions: IBackgroundRequestOptions | ISubagentRequestOptions | undefined,
summarizedAtRoundId: string | undefined,
): Promise<{ result: ChatResults | ChatRequestFailed | ChatRequestCanceled }> {
const intent = locationToIntent(location);
const agentInteractionType = requestKindOptions?.kind === 'subagent' ?
@@ -1108,7 +1114,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
this._telemetryService.sendGHTelemetryEvent('request.sent', telemetryData.properties, telemetryData.measurements);
const requestStart = Date.now();
const handle = connection.sendRequest(request, { userInitiated: !!userInitiatedRequest, turnId, requestId: ourRequestId, countTokens, tokenCountMax: chatEndpointInfo.maxOutputTokens }, cancellationToken);
const handle = connection.sendRequest(request, { userInitiated: !!userInitiatedRequest, turnId, requestId: ourRequestId, countTokens, tokenCountMax: chatEndpointInfo.maxOutputTokens, summarizedAtRoundId }, cancellationToken);
const extendedBaseTelemetryData = baseTelemetryData.extendedBy({ modelCallId });
const processor = this._instantiationService.createInstance(OpenAIResponsesProcessor, extendedBaseTelemetryData, this._telemetryService, modelRequestId.headerRequestId, modelRequestId.gitHubRequestId, getResponsesApiCompactionThresholdFromBody(request));

View File

@@ -48,9 +48,15 @@ export function createResponsesRequestBody(accessor: ServicesAccessor, options:
const compactThreshold = getResponsesApiCompactionThreshold(configService, expService, endpoint);
// compaction supported for all the models but works well for codex models and any future models after 5.3
const webSocketStatefulMarker = resolveWebSocketStatefulMarker(accessor, options);
// When WebSocket is in use, always defer to the WebSocket marker (which may be
// undefined if the connection is new or the summary state changed). Never fall
// back to the HTTP marker lookup in that case.
const ignoreStatefulMarker = !!options.ignoreStatefulMarker || !!options.useWebSocket;
const body: IEndpointBody = {
model,
...rawMessagesToResponseAPI(model, options.messages, !!options.ignoreStatefulMarker, resolveWebSocketStatefulMarker(accessor, options)),
...rawMessagesToResponseAPI(model, options.messages, ignoreStatefulMarker, webSocketStatefulMarker),
stream: true,
tools: options.requestOptions?.tools?.map((tool): OpenAI.Responses.FunctionTool & OpenAiResponsesFunctionTool => ({
...tool.function,
@@ -136,7 +142,15 @@ function resolveWebSocketStatefulMarker(accessor: ServicesAccessor, options: ICr
if (options.ignoreStatefulMarker || !options.useWebSocket || !options.conversationId) {
return undefined;
}
return accessor.get(IChatWebSocketManager).getStatefulMarker(options.conversationId);
const wsManager = accessor.get(IChatWebSocketManager);
// If client-side summarization state changed since the stateful marker
// was stored (new summary, or rollback removing a summary), the server's
// state no longer matches. Skip the marker so the full history is sent.
const connSummarizedAt = wsManager.getSummarizedAtRoundId(options.conversationId);
if (options.summarizedAtRoundId !== connSummarizedAt) {
return undefined;
}
return wsManager.getStatefulMarker(options.conversationId);
}
function rawMessagesToResponseAPI(modelId: string, messages: readonly Raw.ChatMessage[], ignoreStatefulMarker: boolean, webSocketStatefulMarker: string | undefined): { input: OpenAI.Responses.ResponseInputItem[]; previous_response_id?: string } {

View File

@@ -813,3 +813,146 @@ describe('processResponseFromChatEndpoint telemetry', () => {
services.dispose();
});
});
describe('summarizedAtRoundId and stateful marker interaction', () => {
it('skips stateful marker when summarizedAtRoundId differs from connection', () => {
const services = createPlatformServices();
const wsManager: IChatWebSocketManager = {
_serviceBrand: undefined,
getOrCreateConnection: () => { throw new Error('not implemented'); },
hasActiveConnection: () => false,
getStatefulMarker: () => 'resp-prev',
getSummarizedAtRoundId: () => 'round-old',
closeConnection: () => { },
closeAll: () => { },
};
services.set(IChatWebSocketManager, wsManager);
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const messages: Raw.ChatMessage[] = [
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'summarized history' }] },
createStatefulMarkerMessage(testEndpoint.model, 'resp-prev'),
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'after marker' }] },
];
const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(
servicesAccessor,
{ ...createRequestOptions(messages, true), conversationId: 'conv-1', summarizedAtRoundId: 'round-new' },
testEndpoint.model, testEndpoint,
));
expect(body.previous_response_id).toBeUndefined();
expect(body.input).toHaveLength(2);
accessor.dispose();
services.dispose();
});
it('uses stateful marker when summarizedAtRoundId matches connection', () => {
const services = createPlatformServices();
const wsManager = new NullChatWebSocketManager();
wsManager.getStatefulMarker = () => 'resp-prev';
wsManager.getSummarizedAtRoundId = () => 'round-5';
services.set(IChatWebSocketManager, wsManager);
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const messages: Raw.ChatMessage[] = [
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'summarized history' }] },
createStatefulMarkerMessage(testEndpoint.model, 'resp-prev'),
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'after marker' }] },
];
const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(
servicesAccessor,
{ ...createRequestOptions(messages, true), conversationId: 'conv-1', summarizedAtRoundId: 'round-5' },
testEndpoint.model, testEndpoint,
));
expect(body.previous_response_id).toBe('resp-prev');
expect(body.input).toHaveLength(1);
accessor.dispose();
services.dispose();
});
it('uses stateful marker when both sides have no summary', () => {
const services = createPlatformServices();
const wsManager = new NullChatWebSocketManager();
wsManager.getStatefulMarker = () => 'resp-prev';
wsManager.getSummarizedAtRoundId = () => undefined;
services.set(IChatWebSocketManager, wsManager);
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const messages: Raw.ChatMessage[] = [
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'first message' }] },
createStatefulMarkerMessage(testEndpoint.model, 'resp-prev'),
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'second message' }] },
];
const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(
servicesAccessor,
{ ...createRequestOptions(messages, true), conversationId: 'conv-1' },
testEndpoint.model, testEndpoint,
));
expect(body.previous_response_id).toBe('resp-prev');
expect(body.input).toHaveLength(1);
accessor.dispose();
services.dispose();
});
it('skips stateful marker when conversation is rolled back past summary', () => {
const services = createPlatformServices();
const wsManager = new NullChatWebSocketManager();
wsManager.getStatefulMarker = () => 'resp-prev';
wsManager.getSummarizedAtRoundId = () => 'round-5';
services.set(IChatWebSocketManager, wsManager);
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const messages: Raw.ChatMessage[] = [
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'first message' }] },
createStatefulMarkerMessage(testEndpoint.model, 'resp-prev'),
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'second message' }] },
];
const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(
servicesAccessor,
{ ...createRequestOptions(messages, true), conversationId: 'conv-1', summarizedAtRoundId: undefined },
testEndpoint.model, testEndpoint,
));
expect(body.previous_response_id).toBeUndefined();
expect(body.input).toHaveLength(2);
accessor.dispose();
services.dispose();
});
it('skips stateful marker on first request after new summarization', () => {
const services = createPlatformServices();
const wsManager = new NullChatWebSocketManager();
wsManager.getStatefulMarker = () => 'resp-prev';
wsManager.getSummarizedAtRoundId = () => undefined;
services.set(IChatWebSocketManager, wsManager);
const accessor = services.createTestingAccessor();
const instantiationService = accessor.get(IInstantiationService);
const messages: Raw.ChatMessage[] = [
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'summarized history' }] },
createStatefulMarkerMessage(testEndpoint.model, 'resp-prev'),
{ role: Raw.ChatRole.User, content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'after marker' }] },
];
const body = instantiationService.invokeFunction(servicesAccessor => createResponsesRequestBody(
servicesAccessor,
{ ...createRequestOptions(messages, true), conversationId: 'conv-1', summarizedAtRoundId: 'round-new' },
testEndpoint.model, testEndpoint,
));
expect(body.previous_response_id).toBeUndefined();
expect(body.input).toHaveLength(2);
accessor.dispose();
services.dispose();
});
});

View File

@@ -198,6 +198,11 @@ export interface IMakeChatRequestOptions {
useFetcher?: FetcherId;
/** Per-request model capability opt-ins (thinking, tool search, context editing). */
modelCapabilities?: IModelCapabilityOptions;
/**
* The round ID at which the most recent client-side summarization occurred.
* Used to detect when the WebSocket stateful marker predates a summary.
*/
summarizedAtRoundId?: string;
/** Enable retrying once on simple network errors like ECONNRESET. */
canRetryOnceWithoutRollback?: boolean;
/** Custom metadata to be displayed in the log document */

View File

@@ -45,6 +45,12 @@ export interface IChatWebSocketManager {
*/
getStatefulMarker(conversationId: string): string | undefined;
/**
* Returns the round ID at which the last client-side summarization
* occurred for this connection, or undefined if none.
*/
getSummarizedAtRoundId(conversationId: string): string | undefined;
/**
* Closes and removes the connection for a specific conversation.
*/
@@ -66,6 +72,7 @@ export class NullChatWebSocketManager implements IChatWebSocketManager {
}
hasActiveConnection(_conversationId: string): boolean { return false; }
getStatefulMarker(_conversationId: string): string | undefined { return undefined; }
getSummarizedAtRoundId(_conversationId: string): string | undefined { return undefined; }
closeConnection(_conversationId: string): void { }
closeAll(): void { }
}
@@ -76,6 +83,7 @@ export interface IChatWebSocketRequestOptions {
requestId: string;
countTokens: () => Promise<number>;
tokenCountMax: number;
summarizedAtRoundId?: string;
}
export interface IChatWebSocketConnection extends IDisposable {
@@ -216,6 +224,11 @@ export class ChatWebSocketManager extends Disposable implements IChatWebSocketMa
return connection?.isOpen ? connection.statefulMarker : undefined;
}
getSummarizedAtRoundId(conversationId: string): string | undefined {
const connection = this._connections.get(conversationId);
return connection?.isOpen ? connection.summarizedAtRoundId : undefined;
}
closeConnection(conversationId: string): void {
const connection = this._connections.get(conversationId);
if (connection) {
@@ -274,6 +287,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
private _state: ConnectionState = ConnectionState.Closed;
private _activeRequest: ChatWebSocketActiveRequest | undefined;
private _statefulMarker: string | undefined;
private _summarizedAtRoundId: string | undefined;
private readonly _onDidDispose = this._register(new Emitter<void>());
readonly onDidDispose = this._onDidDispose.event;
@@ -320,6 +334,10 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
return this._statefulMarker;
}
get summarizedAtRoundId(): string | undefined {
return this._summarizedAtRoundId;
}
get responseHeaders(): IHeaders {
return this._responseHeaders;
}
@@ -469,6 +487,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
if (!isCAPIWebSocketError(parsed) && parsed.type === 'response.completed') {
this._statefulMarker = parsed.response.id;
this._summarizedAtRoundId = this._activeRequest?.summarizedAtRoundId;
}
this._activeRequest?.handleEvent(parsed);
@@ -536,12 +555,13 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
const statefulMarkerMatched = this._statefulMarker === body.previous_response_id;
const previousResponseIdUnset = body.previous_response_id === undefined;
const hasCompactionData = body.input?.some(item => item?.type === 'compaction') ?? false;
const summarizedAtRoundIdMatched = options.summarizedAtRoundId === this._summarizedAtRoundId;
const statefulMarkerPrefix = this._statefulMarker?.slice(0, 5).concat('...') ?? '<none>';
const previousResponsePrefix = body.previous_response_id?.slice(0, 5).concat('...') ?? '<none>';
if (statefulMarkerMatched) {
this._logService.trace(`[ChatWebSocketManager] WebSocket stateful marker matches previous_response_id (${previousResponsePrefix})`);
this._logService.trace(`[ChatWebSocketManager] WebSocket stateful marker matches previous_response_id (${previousResponsePrefix}), summarizedAtRoundIdMatched: ${summarizedAtRoundIdMatched}`);
} else {
this._logService.info(`[ChatWebSocketManager] WebSocket stateful marker (${statefulMarkerPrefix}) does not match previous_response_id (${previousResponsePrefix})`);
this._logService.debug(`[ChatWebSocketManager] WebSocket stateful marker (${statefulMarkerPrefix}) does not match previous_response_id (${previousResponsePrefix}), summarizedAtRoundIdMatched: ${summarizedAtRoundIdMatched}`);
}
// Supersede any in-flight request before updating turn state
@@ -572,7 +592,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
const promptTokenCountPromise = options.countTokens();
let promptTokenCount = -1;
promptTokenCountPromise.then(count => { promptTokenCount = count; }, () => { promptTokenCount = -2; });
const request = new ChatWebSocketActiveRequest(requestId, body.model, this._configurationService, this._logService);
const request = new ChatWebSocketActiveRequest(requestId, body.model, options.summarizedAtRoundId, this._configurationService, this._logService);
request.onDidSettle(({ outcome, closeCode, closeReason, serverErrorMessage, serverErrorCode }) => {
if (this._activeRequest === request) {
this._activeRequest = undefined;
@@ -596,6 +616,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
statefulMarkerMatched,
previousResponseIdUnset,
hasCompactionData,
summarizedAtRoundIdMatched,
promptTokenCount,
tokenCountMax: options.tokenCountMax,
connectionDurationMs,
@@ -650,6 +671,7 @@ class ChatWebSocketConnection extends Disposable implements IChatWebSocketConnec
statefulMarkerMatched,
previousResponseIdUnset,
hasCompactionData,
summarizedAtRoundIdMatched,
tokenCountMax: options.tokenCountMax,
connectionDurationMs,
totalSentMessageCount: this._totalSentMessageCount,
@@ -702,6 +724,7 @@ class ChatWebSocketActiveRequest implements IChatWebSocketRequestHandle {
constructor(
readonly requestId: string,
readonly modelId: string | undefined,
readonly summarizedAtRoundId: string | undefined,
private readonly _configurationService: IConfigurationService,
private readonly _logService: ILogService,
) {

View File

@@ -64,6 +64,7 @@ export interface IChatWebSocketRequestSentTelemetryProperties extends IChatWebSo
statefulMarkerMatched: boolean;
previousResponseIdUnset: boolean;
hasCompactionData: boolean;
summarizedAtRoundIdMatched: boolean;
tokenCountMax: number;
connectionDurationMs: number;
totalSentMessageCount: number;
@@ -90,6 +91,7 @@ export interface IChatWebSocketRequestOutcomeTelemetryProperties extends IChatWe
statefulMarkerMatched: boolean;
previousResponseIdUnset: boolean;
hasCompactionData: boolean;
summarizedAtRoundIdMatched: boolean;
promptTokenCount: number;
tokenCountMax: number;
connectionDurationMs: number;
@@ -306,6 +308,7 @@ export class ChatWebSocketTelemetrySender {
"statefulMarkerMatched": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the connection stateful marker matched the previous_response_id sent in the request", "isMeasurement": true },
"previousResponseIdUnset": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether previous_response_id was undefined in the request", "isMeasurement": true },
"hasCompactionData": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the request input contains compaction data", "isMeasurement": true },
"summarizedAtRoundIdMatched": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the summarized round ID matches the one stored on the connection", "isMeasurement": true },
"tokenCountMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum generated tokens", "isMeasurement": true },
"totalSentMessageCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of messages sent over this connection", "isMeasurement": true },
"totalReceivedMessageCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of messages received over this connection", "isMeasurement": true },
@@ -328,6 +331,7 @@ export class ChatWebSocketTelemetrySender {
statefulMarkerMatched: properties.statefulMarkerMatched ? 1 : 0,
previousResponseIdUnset: properties.previousResponseIdUnset ? 1 : 0,
hasCompactionData: properties.hasCompactionData ? 1 : 0,
summarizedAtRoundIdMatched: properties.summarizedAtRoundIdMatched ? 1 : 0,
tokenCountMax: properties.tokenCountMax,
totalSentMessageCount: properties.totalSentMessageCount,
totalReceivedMessageCount: properties.totalReceivedMessageCount,
@@ -403,6 +407,7 @@ export class ChatWebSocketTelemetrySender {
"statefulMarkerMatched": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the connection stateful marker matched the previous_response_id sent in the request", "isMeasurement": true },
"previousResponseIdUnset": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether previous_response_id was undefined in the request", "isMeasurement": true },
"hasCompactionData": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the request input contains compaction data", "isMeasurement": true },
"summarizedAtRoundIdMatched": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Whether the summarized round ID matches the one stored on the connection", "isMeasurement": true },
"promptTokenCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of prompt tokens, locally counted", "isMeasurement": true },
"tokenCountMax": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Maximum generated tokens", "isMeasurement": true },
"totalSentMessageCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Number of messages sent over this connection", "isMeasurement": true },
@@ -438,6 +443,7 @@ export class ChatWebSocketTelemetrySender {
statefulMarkerMatched: properties.statefulMarkerMatched ? 1 : 0,
previousResponseIdUnset: properties.previousResponseIdUnset ? 1 : 0,
hasCompactionData: properties.hasCompactionData ? 1 : 0,
summarizedAtRoundIdMatched: properties.summarizedAtRoundIdMatched ? 1 : 0,
promptTokenCount: properties.promptTokenCount,
tokenCountMax: properties.tokenCountMax,
totalSentMessageCount: properties.totalSentMessageCount,