Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ContextManagementResponse, CUSTOM_TOOL_SEARCH_NAME, getContextManagemen
import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService';
import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch';
import { APIUsage } from '../../../platform/networking/common/openai';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, stringifyToolDefinitionsForOTel, toSystemInstructions, truncateForOTel } from '../../../platform/otel/common/index';
import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
Expand Down Expand Up @@ -502,9 +502,9 @@ export class AnthropicLMProvider extends AbstractLanguageModelChatProvider {
if (this._otelService.config.captureContent) {
// Tool definitions on the chat span (issue #299934) with `parameters`
// per OTel GenAI semantic conventions (issue #300318).
const toolDefs = toToolDefinitions(options.tools);
if (toolDefs) {
otelSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(JSON.stringify(toolDefs), this._otelService.config.maxAttributeSizeChars));
const toolDefsJson = stringifyToolDefinitionsForOTel(options.tools);
if (toolDefsJson) {
otelSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(toolDefsJson, this._otelService.config.maxAttributeSizeChars));
}
try {
const { systemTexts, inputMsgs } = buildOTelInputFromChatMessages(messages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ILogService } from '../../../platform/log/common/logService';
import { IResponseDelta, OpenAiFunctionTool } from '../../../platform/networking/common/fetch';
import { APIUsage } from '../../../platform/networking/common/openai';
import { CustomDataPartMimeTypes } from '../../../platform/endpoint/common/endpointTypes';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, type OTelModelOptions, StdAttr, stringifyToolDefinitionsForOTel, toSystemInstructions, truncateForOTel } from '../../../platform/otel/common/index';
import { IOTelService, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { retrieveCapturingTokenByCorrelation, runWithCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
Expand Down Expand Up @@ -363,9 +363,9 @@ export class GeminiNativeBYOKLMProvider extends AbstractLanguageModelChatProvide
if (this._otelService.config.captureContent) {
// Tool definitions on the chat span (issue #299934) with `parameters`
// per OTel GenAI semantic conventions (issue #300318).
const toolDefs = toToolDefinitions(options.tools);
if (toolDefs) {
otelSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(JSON.stringify(toolDefs), this._otelService.config.maxAttributeSizeChars));
const toolDefsJson = stringifyToolDefinitionsForOTel(options.tools);
if (toolDefsJson) {
otelSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(toolDefsJson, this._otelService.config.maxAttributeSizeChars));
}
try {
const { systemTexts, inputMsgs } = buildOTelInputFromChatMessages(messages);
Expand Down
30 changes: 12 additions & 18 deletions extensions/copilot/src/extension/intents/node/toolCallingLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ILogService } from '../../../platform/log/common/logService';
import { isOpenAIContextManagementResponse, OpenAiFunctionDef } from '../../../platform/networking/common/fetch';
import { IMakeChatRequestOptions } from '../../../platform/networking/common/networking';
import { OpenAIContextManagementResponse } from '../../../platform/networking/common/openai';
import { CopilotChatAttr, emitAgentTurnEvent, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, GitHubCopilotAttr, resolveWorkspaceOTelMetadata, StdAttr, truncateForOTel, workspaceMetadataToOTelAttributes } from '../../../platform/otel/common/index';
import { CopilotChatAttr, emitAgentTurnEvent, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, GitHubCopilotAttr, resolveWorkspaceOTelMetadata, StdAttr, stringifyToolDefinitionsForOTel, truncateForOTel, workspaceMetadataToOTelAttributes } from '../../../platform/otel/common/index';
import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
Expand Down Expand Up @@ -893,14 +893,10 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
// Includes `parameters` (inputSchema) per OTel GenAI semantic convention so
// trace viewers can render full tool signatures (issue #300318).
if (result.availableTools.length > 0) {
span.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(JSON.stringify(
result.availableTools.map(t => ({
type: 'function',
name: t.name,
description: t.description,
parameters: t.inputSchema,
}))
)));
const toolDefsJson = stringifyToolDefinitionsForOTel(result.availableTools);
if (toolDefsJson) {
span.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(toolDefsJson));
}
}
}
span.setStatus(SpanStatusCode.OK);
Expand Down Expand Up @@ -1207,15 +1203,13 @@ export abstract class ToolCallingLoop<TOptions extends IToolCallingLoopOptions =
// starts in fetch(). This lets the debug logger write tools_*.json early.
if (!this.toolsAvailableEmitted && this.agentSpan && availableTools.length > 0) {
this.toolsAvailableEmitted = true;
this.agentSpan.addEvent('tools_available', {
toolDefinitions: truncateForOTel(JSON.stringify(availableTools.map(t => ({
type: 'function',
name: t.name,
description: t.description,
parameters: t.inputSchema,
})))),
...(this.chatSessionIdForTools ? { [CopilotChatAttr.CHAT_SESSION_ID]: this.chatSessionIdForTools } : {}),
});
const toolDefsJson = stringifyToolDefinitionsForOTel(availableTools);
if (toolDefsJson) {
this.agentSpan.addEvent('tools_available', {
toolDefinitions: truncateForOTel(toolDefsJson),
...(this.chatSessionIdForTools ? { [CopilotChatAttr.CHAT_SESSION_ID]: this.chatSessionIdForTools } : {}),
});
}
}

const context = this.createPromptContext(availableTools, outputStream);
Expand Down
20 changes: 14 additions & 6 deletions extensions/copilot/src/extension/prompt/node/chatMLFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { sendEngineMessagesTelemetry } from '../../../platform/networking/node/c
import { CAPIWebSocketErrorEvent, IChatWebSocketManager, isCAPIWebSocketError } from '../../../platform/networking/node/chatWebSocketManager';
import { sendCommunicationErrorTelemetry } from '../../../platform/networking/node/stream';
import { ChatFailKind, ChatRequestCanceled, ChatRequestFailed, ChatResults, FetchResponseKind } from '../../../platform/openai/node/fetch';
import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, normalizeProviderMessages, StdAttr, toSystemInstructions, toToolDefinitions, truncateForOTel } from '../../../platform/otel/common/index';
import { collectSystemTextsFromRequestBody, CopilotChatAttr, emitInferenceDetailsEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, normalizeProviderMessages, StdAttr, stringifyToolDefinitionsForOTel, stringifyToolsRawForTelemetry, toSystemInstructions, truncateForOTel } from '../../../platform/otel/common/index';
import { IOTelService, ISpanHandle, SpanKind, SpanStatusCode } from '../../../platform/otel/common/otelService';
import { IRequestLogger } from '../../../platform/requestLogger/common/requestLogger';
import { getCurrentCapturingToken } from '../../../platform/requestLogger/node/requestLogger';
Expand Down Expand Up @@ -295,9 +295,9 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
// Tool definitions: emit on every chat span so trace viewers can render the
// tool catalog per LLM call (issue #299934). Includes `parameters` per
// OTel GenAI semantic conventions (issue #300318).
const toolDefs = toToolDefinitions(requestBody.tools);
if (toolDefs) {
otelInferenceSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(JSON.stringify(toolDefs), this._otelService.config.maxAttributeSizeChars));
const toolDefsJson = stringifyToolDefinitionsForOTel(requestBody.tools);
if (toolDefsJson) {
otelInferenceSpan.setAttribute(GenAiAttr.TOOL_DEFINITIONS, truncateForOTel(toolDefsJson, this._otelService.config.maxAttributeSizeChars));
}
// Cache-relevant request options. Anything in this blob, when changed
// between two requests, will invalidate the prompt cache even when
Expand Down Expand Up @@ -1171,6 +1171,10 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
if (key === 'messages' || key === 'input') {
continue;
} // Skip messages (PII)
if (key === 'tools') {
telemetryData.properties[`request.option.${key}`] = stringifyToolsRawForTelemetry(value as ReadonlyArray<unknown> | undefined) ?? 'undefined';
continue;
}
telemetryData.properties[`request.option.${key}`] = JSON.stringify(value) ?? 'undefined';
}
this._telemetryService.sendGHTelemetryEvent('request.sent', telemetryData.properties, telemetryData.measurements);
Expand All @@ -1179,7 +1183,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
this._telemetryService.sendEnhancedGHTelemetryEvent('request.options.tools', multiplexProperties({
headerRequestId: ourRequestId,
conversationId,
messagesJson: JSON.stringify(request.tools),
messagesJson: stringifyToolsRawForTelemetry(request.tools)!,
}), telemetryData.measurements);
}

Expand Down Expand Up @@ -1445,6 +1449,10 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
if (key === 'messages' || key === 'input') {
continue;
} // Skip messages (PII)
if (key === 'tools') {
telemetryData.properties[`request.option.${key}`] = stringifyToolsRawForTelemetry(value as ReadonlyArray<unknown> | undefined) ?? 'undefined';
continue;
}
telemetryData.properties[`request.option.${key}`] = JSON.stringify(value) ?? 'undefined';
}

Expand All @@ -1459,7 +1467,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
this._telemetryService.sendEnhancedGHTelemetryEvent('request.options.tools', multiplexProperties({
headerRequestId: ourRequestId,
conversationId: telemetryProperties?.conversationId,
messagesJson: JSON.stringify(request.tools),
messagesJson: stringifyToolsRawForTelemetry(request.tools)!,
}), telemetryData.measurements);
}

Expand Down
7 changes: 5 additions & 2 deletions extensions/copilot/src/platform/otel/common/genAiEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { GenAiAttr, GenAiOperationName, StdAttr } from './genAiAttributes';
import { normalizeProviderMessages, toSystemInstructions, truncateForOTel } from './messageFormatters';
import { normalizeProviderMessages, stringifyToolsRawForTelemetry, toSystemInstructions, truncateForOTel } from './messageFormatters';
import type { IOTelService } from './otelService';
import { type WorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from './workspaceOTelMetadata';

Expand Down Expand Up @@ -71,7 +71,10 @@ export function emitInferenceDetailsEvent(
}
}
if (request.tools !== undefined) {
attributes[GenAiAttr.TOOL_DEFINITIONS] = truncateForOTel(JSON.stringify(request.tools), maxLen);
const toolsJson = stringifyToolsRawForTelemetry(request.tools as ReadonlyArray<unknown> | undefined);
if (toolsJson !== undefined) {
attributes[GenAiAttr.TOOL_DEFINITIONS] = truncateForOTel(toolsJson, maxLen);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/copilot/src/platform/otel/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { CopilotChatAttr, CopilotCliSdkAttr, FILE_TOOL_NAMES, GenAiAttr, GenAiOp
export type { AgentType, EditOperationType, HookDecision } from './genAiAttributes';
export { emitAgentTurnEvent, emitCloudSessionInvokeEvent, emitEditFeedbackEvent, emitEditHunkActionEvent, emitEditSurvivalEvent, emitInferenceDetailsEvent, emitInlineDoneEvent, emitSessionStartEvent, emitToolCallEvent, emitUserFeedbackEvent } from './genAiEvents';
export { GenAiMetrics } from './genAiMetrics';
export { collectSystemTextsFromRequestBody, extractTextFromContent, normalizeProviderMessages, toInputMessages, toOutputMessages, toSystemInstructions, toToolDefinitions, truncateForOTel } from './messageFormatters';
export { collectSystemTextsFromRequestBody, extractTextFromContent, normalizeProviderMessages, stringifyToolDefinitionsForOTel, stringifyToolsRawForTelemetry, toInputMessages, toOutputMessages, toSystemInstructions, toToolDefinitions, truncateForOTel } from './messageFormatters';
export { NoopOTelService } from './noopOtelService';
export { resolveOTelConfig, DEFAULT_OTLP_ENDPOINT, type OTelConfig, type OTelConfigInput } from './otelConfig';
export { IOTelService, SpanKind, SpanStatusCode, type ICompletedSpanData, type ISpanEventData, type ISpanEventRecord, type ISpanHandle, type OTelModelOptions, type SpanOptions, type TraceContext } from './otelService';
Expand Down
70 changes: 70 additions & 0 deletions extensions/copilot/src/platform/otel/common/messageFormatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,73 @@ export function toToolDefinitions(tools: ReadonlyArray<{
}
return out.length > 0 ? out : undefined;
}

// Tool-definition JSON is multi-MB and reused across many telemetry/OTel sites
// per LLM round, often byte-identical across consecutive agent-loop rounds.
// Intern by array reference (WeakMap) plus a single-slot last-string cache so
// content-equal serializations from distinct refs collapse to one instance.

const toolDefsJsonByRef = new WeakMap<object, string>();
const toolsRawJsonByRef = new WeakMap<object, string>();
let lastToolDefsJson: string | undefined;
let lastToolsRawJson: string | undefined;

function internToolDefsString(s: string): string {
if (lastToolDefsJson !== undefined && lastToolDefsJson === s) {
return lastToolDefsJson;
}
lastToolDefsJson = s;
return s;
}

function internToolsRawString(s: string): string {
if (lastToolsRawJson !== undefined && lastToolsRawJson === s) {
return lastToolsRawJson;
}
lastToolsRawJson = s;
return s;
}

/**
* Return the OTel-normalized JSON string for a tools array, memoized so all
* telemetry/span sites within (and across consecutive identical rounds of) an
* LLM call share a single string instance. Returns `undefined` if no
* normalized tools would be emitted.
*/
export function stringifyToolDefinitionsForOTel(tools: Parameters<typeof toToolDefinitions>[0]): string | undefined {
if (!tools || tools.length === 0) {
return undefined;
}
const cached = toolDefsJsonByRef.get(tools);
if (cached !== undefined) {
return cached;
}
const defs = toToolDefinitions(tools);
if (!defs) {
return undefined;
}
const s = internToolDefsString(JSON.stringify(defs));
toolDefsJsonByRef.set(tools, s);
return s;
}

/**
* Return `JSON.stringify(tools)` memoized by array reference, with a
* single-slot content intern so consecutive rounds producing identical content
* share one string instance. Used for telemetry sinks that consume the raw
* tools shape rather than the OTel-normalized one. Mirrors `JSON.stringify`
* exactly: returns `'[]'` for an empty array and `undefined` only when
* `tools` itself is `undefined`.
*/
export function stringifyToolsRawForTelemetry(tools: ReadonlyArray<unknown> | undefined): string | undefined {
if (!tools) {
return undefined;
}
const cached = toolsRawJsonByRef.get(tools);
if (cached !== undefined) {
return cached;
}
const s = internToolsRawString(JSON.stringify(tools));
toolsRawJsonByRef.set(tools, s);
return s;
}
Loading
Loading