/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ContentBlockParam, DocumentBlockParam, ImageBlockParam, MessageParam, RedactedThinkingBlockParam, RefusalStopDetails, TextBlockParam, ThinkingBlockParam, ToolReferenceBlockParam, ToolResultBlockParam } from '@anthropic-ai/sdk/resources'; import { Raw } from '@vscode/prompt-tsx'; import { Response } from '../../../platform/networking/common/fetcherService'; import { AsyncIterableObject } from '../../../util/vs/base/common/async'; import { SSEParser } from '../../../util/vs/base/common/sseParser'; import { generateUuid } from '../../../util/vs/base/common/uuid'; import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ChatLocation } from '../../chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../configuration/common/configurationService'; import { ILogService } from '../../log/common/logService'; import { AnthropicMessagesTool, ContextManagementResponse, CUSTOM_TOOL_SEARCH_NAME, getContextManagementFromConfig, isAnthropicContextEditingEnabled, isExtendedCacheTtlEnabled, isExtendedCacheTtlMessagesEnabled } from '../../networking/common/anthropic'; import { FinishedCallback, getRequestId, gitHubCopilotRequestTeProperty, IIPCodeCitation, IResponseDelta } from '../../networking/common/fetch'; import { IChatEndpoint, ICreateEndpointBodyOptions, IEndpointBody } from '../../networking/common/networking'; import { ChatCompletion, FinishedCompletionReason, rawMessageToCAPI } from '../../networking/common/openai'; import { IToolDeferralService } from '../../networking/common/toolDeferralService'; import { sendEngineMessagesTelemetry } from '../../networking/node/chatStream'; import { IExperimentationService } from '../../telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; import { TelemetryData } from '../../telemetry/common/telemetryData'; /** * Build the `input_schema` for an Anthropic tool from an arbitrary JSON Schema * object. Ensures `type: 'object'` and `properties` default, preserves extra * keys like `$defs` and `additionalProperties`, and strips `$schema` which the * Anthropic API rejects. */ export function buildToolInputSchema(schema: Record | undefined): Record & { type: 'object' } { if (!schema) { return { type: 'object', properties: {} }; } const { $schema: _, ...rest } = schema; return { type: 'object', properties: {}, ...rest }; } /** * Anthropic only accepts ASCII letters, digits, underscores, and hyphens in tool call IDs. */ function sanitizeToolCallId(id: string): string { return id.replace(/[^a-zA-Z0-9_-]/gu, '_'); } /** * Allocates Anthropic-compatible tool call IDs while preserving call/result pairing. */ function createAnthropicToolCallIdMapper(messages: readonly Raw.ChatMessage[]): (id: string) => string { const validIdPattern = /^[a-zA-Z0-9_-]+$/u; const usedIds = new Set(); for (const message of messages) { if (message.role === Raw.ChatRole.Assistant) { for (const toolCall of message.toolCalls ?? []) { if (validIdPattern.test(toolCall.id)) { usedIds.add(toolCall.id); } } } else if (message.role === Raw.ChatRole.Tool && validIdPattern.test(message.toolCallId)) { usedIds.add(message.toolCallId); } } const mappedIds = new Map(); return id => { const existingId = mappedIds.get(id); if (existingId !== undefined) { return existingId; } if (validIdPattern.test(id)) { mappedIds.set(id, id); usedIds.add(id); return id; } const baseId = sanitizeToolCallId(id) || 'tool_call'; let mappedId = baseId; let suffix = 1; while (usedIds.has(mappedId)) { mappedId = `${baseId}_${suffix++}`; } mappedIds.set(id, mappedId); usedIds.add(mappedId); return mappedId; }; } /** IP Code Citation annotation from Messages API copilot_annotations */ interface AnthropicIPCodeCitation { id: number; start_offset: number; end_offset: number; details: Record; citations: { snippet: string; url: string; ip_type?: string; license: string; }; } interface AnthropicStreamEvent { type: string; message?: { id: string; type: string; role: string; content: ContentBlockParam[]; model: string; stop_reason: string | null; stop_sequence: string | null; usage: { input_tokens: number; output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; cache_creation?: { ephemeral_1h_input_tokens?: number; ephemeral_5m_input_tokens?: number; }; output_tokens_details?: { thinking_tokens?: number; }; }; }; index?: number; content_block?: ContentBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam; delta?: { type: string; text?: string; partial_json?: string; thinking?: string; signature?: string; stop_reason?: string; stop_sequence?: string; stop_details?: RefusalStopDetails | null; }; copilot_annotations?: { IPCodeCitations?: AnthropicIPCodeCitation[]; }; usage?: { output_tokens: number; input_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; cache_creation?: { ephemeral_1h_input_tokens?: number; ephemeral_5m_input_tokens?: number; }; output_tokens_details?: { thinking_tokens?: number; }; }; copilot_usage?: { total_nano_aiu: number; }; context_management?: ContextManagementResponse; } export function createMessagesRequestBody(accessor: ServicesAccessor, options: ICreateEndpointBodyOptions, model: string, endpoint: IChatEndpoint): IEndpointBody { const configurationService = accessor.get(IConfigurationService); const experimentationService = accessor.get(IExperimentationService); const toolDeferralService = accessor.get(IToolDeferralService); const logService = accessor.get(ILogService); const toolSearchEnabled = !!endpoint.supportsToolSearch && !!options.requestOptions?.tools?.some(t => t.function.name === CUSTOM_TOOL_SEARCH_NAME); // Split tools into non-deferred and deferred up front so we can build finalTools // with non-deferred first. This ensures the cache_control breakpoint on the last // non-deferred tool caches the maximum stable prefix. const nonDeferredTools: AnthropicMessagesTool[] = []; const deferredTools: AnthropicMessagesTool[] = []; if (options.requestOptions?.tools) { for (const tool of options.requestOptions.tools) { if (!tool.function.name || tool.function.name.length === 0) { continue; } const isDeferred = options.modelCapabilities?.enableToolSearch && toolSearchEnabled && !toolDeferralService.isNonDeferredTool(tool.function.name); const anthropicTool: AnthropicMessagesTool = { name: tool.function.name, description: tool.function.description || '', input_schema: buildToolInputSchema(tool.function.parameters as Record | undefined), ...(isDeferred ? { defer_loading: true } : {}), }; (isDeferred ? deferredTools : nonDeferredTools).push(anthropicTool); } } // Build final tools array. The client-side search_tools tool is already in the // anthropicTools array (registered as a model-specific VS Code tool) and will handle // tool search client-side. Deferred tools still have defer_loading: true so the model // knows to use the search tool to discover them. const finalTools: AnthropicMessagesTool[] = [...nonDeferredTools, ...deferredTools]; // Thinking is enabled only when options.modelCapabilities?.enableThinking is true, a non-zero thinking budget // is configured for the model, and the model supports thinking. reasoningEffort (if present) // is used only to configure the effort level when thinking is enabled, not to gate it. const reasoningEffort = options.modelCapabilities?.reasoningEffort; let thinkingConfig: { type: 'enabled' | 'adaptive'; budget_tokens?: number; display?: 'summarized' } | undefined; if (options.modelCapabilities?.enableThinking) { const hardcodedBudget = 16000; if (endpoint.supportsAdaptiveThinking) { thinkingConfig = { type: 'adaptive', display: 'summarized' }; } else if (endpoint.maxThinkingBudget && endpoint.minThinkingBudget) { const maxTokens = options.postOptions.max_tokens ?? 1024; const minBudget = endpoint.minThinkingBudget ?? 1024; const normalizedBudget = hardcodedBudget < minBudget ? minBudget : hardcodedBudget; const maxBudget = endpoint.maxThinkingBudget ?? 32000; const thinkingBudget = Math.min(maxBudget, maxTokens - 1, normalizedBudget); if (thinkingBudget) { thinkingConfig = { type: 'enabled', budget_tokens: thinkingBudget }; } } } const thinkingEnabled = !!thinkingConfig; let effort: string | undefined; if (thinkingConfig) { const declaredLevels = endpoint.supportsReasoningEffort?.length ? endpoint.supportsReasoningEffort : undefined; const explicitlyUnsupported = endpoint.supportsReasoningEffort !== undefined && endpoint.supportsReasoningEffort.length === 0; const defaultEffort = declaredLevels ? (declaredLevels.includes('medium') ? 'medium' : declaredLevels[Math.floor((declaredLevels.length - 1) / 2)]) : !explicitlyUnsupported && endpoint.supportsAdaptiveThinking ? 'high' : undefined; const requestedEffort = configurationService.getConfig(ConfigKey.Advanced.ReasoningEffortOverride) ?? reasoningEffort; if (defaultEffort !== undefined) { const candidateEffort = requestedEffort ?? defaultEffort; if (typeof candidateEffort === 'string' && candidateEffort.length > 0) { if (!declaredLevels || declaredLevels.includes(candidateEffort)) { effort = candidateEffort; } else { logService.warn(`[reasoningEffort] Dropping reasoning effort '${candidateEffort}' for model '${endpoint.model}' — not in server-declared levels [${declaredLevels.join(', ')}].`); } } } else if (explicitlyUnsupported && typeof requestedEffort === 'string' && requestedEffort.length > 0) { logService.warn(`[reasoningEffort] Dropping reasoning effort '${requestedEffort}' for model '${endpoint.model}' — server declares no supported effort levels.`); } } // Build context management configuration const contextManagement = options.modelCapabilities?.enableContextEditing && isAnthropicContextEditingEnabled(endpoint, configurationService, experimentationService) ? getContextManagementFromConfig(configurationService, experimentationService, thinkingEnabled) : undefined; const telemetryService = accessor.get(ITelemetryService); // TODO: Ideally the custom tool_search tool should filter results itself, but it doesn't // have access to the enabled tools for the request. For now, filter tool_reference blocks // here against the actual tools sent to Anthropic to avoid 400 errors from unknown tool names. const validToolNames = finalTools.length > 0 ? new Set(finalTools.map(t => t.name)) : undefined; const messagesResult = rawMessagesToMessagesAPI(options.messages, toolSearchEnabled ? validToolNames : undefined); // Subagent requests are out of scope for the extended cache TTL — their // context is short-lived. The three subagent call sites (search loop, // execution loop, Task-tool-spawned agent) all set // `interactionTypeOverride: 'conversation-subagent'`, which is also the // source of truth for the `X-Interaction-Type` wire header. // // The rolling message breakpoints default to the 5m TTL and only upgrade to // 1h when the `extendedTtlMessages` sub-toggle is on (which itself requires // the parent `extendedTtl` to be on — see `isExtendedCacheTtlMessagesEnabled`). const isSubagent = options.interactionTypeOverride === 'conversation-subagent'; const useExtendedCacheTtl = isExtendedCacheTtlEnabled(endpoint, configurationService, experimentationService, options.location, isSubagent); const cacheTtl = useExtendedCacheTtl ? '1h' : undefined; const useExtendedCacheTtlMessages = isExtendedCacheTtlMessagesEnabled(useExtendedCacheTtl, configurationService, experimentationService); const messageCacheTtl = useExtendedCacheTtlMessages ? '1h' : undefined; clearAllCacheControl(messagesResult); addMessagesApiCacheControl(messagesResult, messageCacheTtl); addToolsAndSystemCacheControl(finalTools, messagesResult, cacheTtl); // Guard: The Anthropic Messages API requires the conversation to end with a user message. // A trailing assistant message is treated as a prefill request, which is not supported // and will return a 400 error. This catches upstream edge cases where isContinuation // skips the UserMessage or validateToolMessages drops trailing tool messages. const lastMessage = messagesResult.messages.at(-1); if (lastMessage && lastMessage.role === 'assistant') { logService.warn(`[messagesAPI] Trailing assistant message detected — appending synthetic user message to prevent prefill error. Total messages: ${messagesResult.messages.length}`); /* __GDPR__ "messagesApi.trailingAssistantGuard" : { "owner": "bhavyaus", "comment": "Tracks when a trailing assistant message is detected and a synthetic user message is appended to prevent prefill errors", "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model being used" }, "location": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The chat location (agent, panel, etc)" }, "messageCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of messages in the conversation" } } */ telemetryService.sendMSFTTelemetryEvent('messagesApi.trailingAssistantGuard', { model, location: ChatLocation.toString(options.location) }, { messageCount: messagesResult.messages.length } ); messagesResult.messages.push({ role: 'user', content: [{ type: 'text', text: 'Please continue.' }], }); } return { model, ...messagesResult, stream: options.requestOptions?.stream ?? true, tools: finalTools.length > 0 ? finalTools : undefined, max_tokens: options.postOptions.max_tokens, thinking: thinkingConfig, ...(effort ? { output_config: { effort } } : {}), ...(contextManagement ? { context_management: contextManagement } : {}), }; } export function rawMessagesToMessagesAPI(messages: readonly Raw.ChatMessage[], validToolNames?: Set): { messages: MessageParam[]; system?: TextBlockParam[] } { const unmergedMessages: MessageParam[] = []; const systemBlocks: TextBlockParam[] = []; const toolCallIdToName = new Map(); const mapToolCallId = createAnthropicToolCallIdMapper(messages); for (const message of messages) { switch (message.role) { case Raw.ChatRole.System: { systemBlocks.push(...rawContentToAnthropicContent(message.content).filter((c): c is TextBlockParam => c.type === 'text')); break; } case Raw.ChatRole.User: { const content = rawContentToAnthropicContent(message.content); if (content.length > 0) { unmergedMessages.push({ role: 'user', content, }); } break; } case Raw.ChatRole.Assistant: { const content = rawContentToAnthropicContent(message.content); if (message.toolCalls) { for (const toolCall of message.toolCalls) { let parsedInput: Record = {}; try { parsedInput = JSON.parse(toolCall.function.arguments); } catch { // Keep empty object if parse fails } content.push({ type: 'tool_use', id: mapToolCallId(toolCall.id), name: toolCall.function.name, input: parsedInput, }); toolCallIdToName.set(toolCall.id, toolCall.function.name); } } if (content.length > 0) { unmergedMessages.push({ role: 'assistant', content, }); } break; } case Raw.ChatRole.Tool: { if (message.toolCallId) { const toolContent = rawContentToAnthropicContent(message.content); // Extract cache_control from content blocks - it belongs on the tool_result block, not inner content let hasCacheControl = false; for (const block of toolContent) { if (contentBlockSupportsCacheControl(block) && block.cache_control) { hasCacheControl = true; delete block.cache_control; } } // If this is a custom tool search result and validToolNames is provided, // attempt to convert the text content into tool_reference blocks per the // Anthropic custom tool search spec. When validToolNames is undefined // (i.e. tool_reference conversion is disabled/unsupported for this // request), fall through to the regular text/image filter to avoid // sending unsupported content types. const isCustomToolSearch = validToolNames && toolCallIdToName.get(message.toolCallId) === CUSTOM_TOOL_SEARCH_NAME; const toolReferenceContent = isCustomToolSearch ? tryParseToolReferences(toolContent, validToolNames) : undefined; const validContent = toolReferenceContent ?? toolContent.filter((c): c is TextBlockParam | ImageBlockParam | DocumentBlockParam => (c.type === 'text' || c.type === 'image' || c.type === 'document') && !(c.type === 'text' && c.text.trim() === '') ); const toolResultBlock: ToolResultBlockParam = { type: 'tool_result', tool_use_id: mapToolCallId(message.toolCallId), content: validContent.length > 0 ? validContent : undefined, }; if (hasCacheControl) { toolResultBlock.cache_control = { type: 'ephemeral' }; } unmergedMessages.push({ role: 'user', content: [toolResultBlock], }); } break; } } } const mergedMessages: MessageParam[] = []; for (const message of unmergedMessages) { const lastMessage = mergedMessages[mergedMessages.length - 1]; if (lastMessage && lastMessage.role === message.role) { const prevContent = Array.isArray(lastMessage.content) ? lastMessage.content : [{ type: 'text' as const, text: lastMessage.content }]; const newContent = Array.isArray(message.content) ? message.content : [{ type: 'text' as const, text: message.content }]; lastMessage.content = lastMessage.role === 'assistant' ? mergeAssistantContent(prevContent, newContent) : [...prevContent, ...newContent]; } else { mergedMessages.push(message); } } return { messages: mergedMessages, ...(systemBlocks.length ? { system: systemBlocks } : {}), }; } function isThinkingBlock(block: ContentBlockParam): boolean { return block.type === 'thinking' || block.type === 'redacted_thinking'; } // Keep at most one response's thinking blocks when merging consecutive assistants (#327646). function mergeAssistantContent(prevContent: ContentBlockParam[], newContent: ContentBlockParam[]): ContentBlockParam[] { const hasToolUse = (blocks: ContentBlockParam[]) => blocks.some(block => block.type === 'tool_use'); const thinkingSource = hasToolUse(newContent) ? newContent : hasToolUse(prevContent) ? prevContent : undefined; return [ ...(thinkingSource?.filter(isThinkingBlock) ?? []), ...prevContent.filter(block => !isThinkingBlock(block)), ...newContent.filter(block => !isThinkingBlock(block)), ]; } /** * Parses tool result content from the custom tool search tool into * tool_reference content blocks that the Anthropic API understands. * Expects a single text block containing a JSON array of tool name strings. * @see https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#custom-tool-search-implementation */ function tryParseToolReferences(content: ContentBlockParam[], validToolNames?: Set): ToolReferenceBlockParam[] | undefined { if (content.length !== 1 || content[0].type !== 'text') { return undefined; } let parsed: unknown; try { parsed = JSON.parse(content[0].text); } catch { return undefined; } if (!Array.isArray(parsed)) { return undefined; } return parsed .filter((name): name is string => typeof name === 'string' && (!validToolNames || validToolNames.has(name))) .map((name): ToolReferenceBlockParam => ({ type: 'tool_reference', tool_name: name })); } function rawContentToAnthropicContent(content: readonly Raw.ChatCompletionContentPart[]): ContentBlockParam[] { const convertedContent: ContentBlockParam[] = []; // Track pending cache_control that couldn't be attached to a preceding block let pendingCacheControl = false; for (const part of content) { switch (part.type) { case Raw.ChatCompletionContentPartKind.Text: if (part.text.trim()) { convertedContent.push({ type: 'text', text: part.text }); } break; case Raw.ChatCompletionContentPartKind.Image: { const url = part.imageUrl.url; // Parse data URL: data:image/png;base64, const match = url.match(/^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/); if (match) { convertedContent.push({ type: 'image', source: { type: 'base64', media_type: match[1] as 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp', data: match[2], } }); } else if (url.startsWith('https://')) { // URL image source: https://platform.claude.com/docs/en/api/messages#url_image_source convertedContent.push({ type: 'image', source: { type: 'url', url, } }); } break; } case Raw.ChatCompletionContentPartKind.CacheBreakpoint: { const previousBlock = convertedContent.at(-1); if (previousBlock && contentBlockSupportsCacheControl(previousBlock)) { previousBlock.cache_control = { type: 'ephemeral' }; } else { // No preceding block to attach to — defer until the next // cacheable content block is added, or silently drop it. // Previously this created a whitespace-only text block which // the Anthropic API rejects with "text content block must // contain non-whitespace text". pendingCacheControl = true; } break; } case Raw.ChatCompletionContentPartKind.Document: { if (part.documentData.mediaType === 'application/pdf') { convertedContent.push({ type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: part.documentData.data, } } satisfies DocumentBlockParam); } break; } case Raw.ChatCompletionContentPartKind.Opaque: { if (part.value && typeof part.value === 'object' && 'type' in part.value) { const opaqueValue = part.value as { type: string; thinking?: { id: string; text?: string | string[]; encrypted?: string; redacted?: boolean } }; if (opaqueValue.type === 'thinking' && opaqueValue.thinking) { const thinkingText = Array.isArray(opaqueValue.thinking.text) ? opaqueValue.thinking.text.join('') : opaqueValue.thinking.text; if (opaqueValue.thinking.redacted && opaqueValue.thinking.encrypted) { // Genuine redacted_thinking block: `encrypted` holds the opaque `data` blob. convertedContent.push({ type: 'redacted_thinking', data: opaqueValue.thinking.encrypted, }); } else if (opaqueValue.thinking.encrypted) { // Regular thinking block: `encrypted` holds the signature. The text may be // empty (e.g. `display: "omitted"` or pruned under token budget); the Anthropic // API still accepts a thinking block with an empty `thinking` field as long as // the signature is intact. We must NEVER ship the signature as redacted `data`, // which the API rejects with "Invalid 'data' in 'redacted_thinking' block". convertedContent.push({ type: 'thinking', thinking: thinkingText || '', signature: opaqueValue.thinking.encrypted, }); } } } break; } } // Attach any pending cache_control to the block we just added if (pendingCacheControl && convertedContent.length > 0) { const lastBlock = convertedContent.at(-1)!; if (contentBlockSupportsCacheControl(lastBlock)) { lastBlock.cache_control = { type: 'ephemeral' }; pendingCacheControl = false; } } } return convertedContent; } function contentBlockSupportsCacheControl(block: ContentBlockParam): block is Exclude { return block.type !== 'thinking' && block.type !== 'redacted_thinking'; } /** Removes any cache_control fields from system and message blocks. */ export function clearAllCacheControl( messagesResult: { messages: MessageParam[]; system?: TextBlockParam[] }, ): void { if (messagesResult.system) { for (const block of messagesResult.system) { delete block.cache_control; } } for (const msg of messagesResult.messages) { if (Array.isArray(msg.content)) { for (const block of msg.content) { if (typeof block === 'object' && 'cache_control' in block) { delete (block as { cache_control?: unknown }).cache_control; } } } } } /** * Marks the last non-deferred tool and the last system block for caching. * * When {@link cacheTtl} is `'1h'`, the breakpoints request the extended cache * TTL. Sending this requires the `extended-cache-ttl-2025-04-11` Anthropic * beta header — see {@link IChatEndpoint.getExtraHeaders}. When omitted, the * default 5 minute TTL is used. */ export function addToolsAndSystemCacheControl( tools: AnthropicMessagesTool[], messagesResult: { messages: MessageParam[]; system?: TextBlockParam[] }, cacheTtl?: '1h', ): void { const cacheControl = cacheTtl ? { type: 'ephemeral' as const, ttl: cacheTtl } : { type: 'ephemeral' as const }; for (let i = tools.length - 1; i >= 0; i--) { if (!tools[i].defer_loading) { tools[i].cache_control = cacheControl; break; } } const lastSystemBlock = messagesResult.system?.at(-1); if (lastSystemBlock && !lastSystemBlock.cache_control) { lastSystemBlock.cache_control = cacheControl; } } /** * Marks the last cacheable block of the two most recent cacheable messages. * * Anthropic's prompt cache matches on content prefix, so a single tail anchor * is sufficient under steady-state. The second (older) anchor is intended to * improve the chance of retaining a useful fallback within Anthropic's * lookback window: if the tail anchor misses (TTL expiry on a slow tool call, * or rare content drift), the older anchor can still serve a cache hit * covering everything up to it, so we typically lose at most one exchange * instead of resetting the entire conversation cache. * * Combined with the tools + system breakpoints, this can produce up to 4 * cache_control markers, which matches Anthropic's per-request limit. */ export function addMessagesApiCacheControl( messagesResult: { messages: MessageParam[]; system?: TextBlockParam[] }, cacheTtl?: '1h', ): void { const messages = messagesResult.messages; let marked = 0; for (let i = messages.length - 1; i >= 0 && marked < 2; i--) { const msg = messages[i]; if (Array.isArray(msg.content) && msg.content.some(b => typeof b === 'object' && contentBlockSupportsCacheControl(b))) { markLastCacheableBlock(msg, cacheTtl); marked++; } } } function markLastCacheableBlock(msg: MessageParam, cacheTtl?: '1h'): void { if (!Array.isArray(msg.content)) { return; } for (let j = msg.content.length - 1; j >= 0; j--) { const block = msg.content[j]; if (typeof block === 'object' && contentBlockSupportsCacheControl(block)) { block.cache_control = cacheTtl ? { type: 'ephemeral', ttl: cacheTtl } : { type: 'ephemeral' }; return; } } } export async function processResponseFromMessagesEndpoint( instantiationService: IInstantiationService, telemetryService: ITelemetryService, logService: ILogService, response: Response, finishCallback: FinishedCallback, telemetryData: TelemetryData ): Promise> { // Route based on Content-Type: non-streaming Anthropic responses return // application/json, streaming returns text/event-stream. const contentType = response.headers.get('content-type') ?? ''; if (!contentType.includes('text/event-stream')) { return processNonStreamingResponseFromMessagesEndpoint(telemetryService, logService, response, finishCallback, telemetryData); } return new AsyncIterableObject(async feed => { const requestId = response.headers.get('X-Request-ID') ?? generateUuid(); const ghRequestId = response.headers.get('x-github-request-id') ?? ''; const { serverExperiments, copilotServiceRequestId, gitHubCopilotRequestTe } = getRequestId(response.headers); const processor = instantiationService.createInstance(AnthropicMessagesProcessor, telemetryData, requestId, ghRequestId, copilotServiceRequestId, serverExperiments); processor.gitHubCopilotRequestTe = gitHubCopilotRequestTe; const parser = new SSEParser((ev) => { try { logService.trace(`[messagesAPI]SSE: ${ev.data}`); const trimmed = ev.data?.trim(); if (!trimmed || trimmed === '[DONE]') { return; } const parsed = JSON.parse(trimmed) as Partial; const type = parsed.type ?? ev.type; if (!type) { return; } const completion = processor.push({ ...parsed, type } as AnthropicStreamEvent, finishCallback); if (completion) { logService.info(`[messagesAPI] message ${completion.choiceIndex} returned. finish reason: [${completion.finishReason}]`); const dataToSendToTelemetry = telemetryData.extendedBy({ completionChoiceFinishReason: completion.finishReason, headerRequestId: completion.requestId.headerRequestId, ...gitHubCopilotRequestTeProperty(completion.requestId.gitHubCopilotRequestTe), }); telemetryService.sendGHTelemetryEvent('completion.finishReason', dataToSendToTelemetry.properties, dataToSendToTelemetry.measurements); const telemetryMessages = completion.telemetryMessages ?? [rawMessageToCAPI(completion.message)]; let telemetryDataWithUsage = telemetryData.extendedBy(gitHubCopilotRequestTeProperty(completion.requestId.gitHubCopilotRequestTe)); if (completion.usage) { telemetryDataWithUsage = telemetryDataWithUsage.extendedBy({}, { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens, ...(completion.usage.prompt_tokens_details && { cachedTokens: completion.usage.prompt_tokens_details.cached_tokens }), ...(completion.usage.completion_tokens_details && { reasoningTokens: completion.usage.completion_tokens_details.reasoning_tokens, acceptedPredictionTokens: completion.usage.completion_tokens_details.accepted_prediction_tokens, rejectedPredictionTokens: completion.usage.completion_tokens_details.rejected_prediction_tokens, }), }); } sendEngineMessagesTelemetry(telemetryService, telemetryMessages, telemetryDataWithUsage, true, logService); feed.emitOne(completion); } } catch (e) { feed.reject(e); } }); for await (const chunk of response.body) { parser.feed(chunk); } }, async () => { await response.body.destroy(); }); } /** * Inputs for {@link buildAnthropicCompletion}. Both the streaming * `message_stop` handler and the non-streaming response handler * populate this from their respective data sources. */ interface AnthropicCompletionState { readonly model: string; readonly messageId: string; readonly stopReason: string | null | undefined; readonly textContent: string; readonly toolCalls: readonly { id: string; name: string; arguments: string }[]; readonly inputTokens: number; readonly outputTokens: number; readonly cacheCreationTokens: number; readonly cacheCreation1hTokens: number | undefined; readonly cacheCreation5mTokens: number | undefined; readonly cacheReadTokens: number; /** * Anthropic-reported thinking (reasoning) tokens, a subset of * `output_tokens`. Surfaced as `completion_tokens_details.reasoning_tokens` * to match the OpenAI/CAPI naming used elsewhere in telemetry. Undefined * when the server did not include `output_tokens_details`. */ readonly thinkingTokens: number | undefined; readonly requestId: string; readonly ghRequestId: string; readonly copilotServiceRequestId: string; readonly gitHubCopilotRequestTe?: string; readonly serverExperiments: string; readonly telemetryData: TelemetryData; readonly copilotUsage?: { total_nano_aiu: number }; } /** * Map an Anthropic `stop_reason` string to a {@link FinishedCompletionReason}. */ function mapStopReason(stopReason: string | null | undefined): FinishedCompletionReason { switch (stopReason) { case 'refusal': return FinishedCompletionReason.Refusal; case 'max_tokens': case 'model_context_window_exceeded': return FinishedCompletionReason.Length; default: return FinishedCompletionReason.Stop; } } /** * Build a {@link ChatCompletion} from the accumulated Anthropic response * state. Shared between the streaming (`message_stop`) and non-streaming * response handlers so token accounting, finish-reason mapping, and * message construction are defined in exactly one place. * * @param logService Used for the cache-token consistency warning. */ function buildAnthropicCompletion(state: AnthropicCompletionState, logService: ILogService): ChatCompletion { const computedPromptTokens = state.inputTokens + state.cacheCreationTokens + state.cacheReadTokens; if (computedPromptTokens < state.cacheReadTokens) { logService.warn(`[messagesAPI] Token count inconsistency: computed prompt_tokens (${computedPromptTokens}) < cached_tokens (${state.cacheReadTokens}). Raw values: inputTokens=${state.inputTokens}, cacheCreationTokens=${state.cacheCreationTokens}, cacheReadTokens=${state.cacheReadTokens}`); } return { blockFinished: true, choiceIndex: 0, model: state.model, tokens: [], telemetryData: state.telemetryData, requestId: { headerRequestId: state.requestId, gitHubRequestId: state.ghRequestId, copilotServiceRequestId: state.copilotServiceRequestId, ...gitHubCopilotRequestTeProperty(state.gitHubCopilotRequestTe), completionId: state.messageId, created: Date.now(), deploymentId: '', serverExperiments: state.serverExperiments, }, usage: { prompt_tokens: computedPromptTokens, completion_tokens: state.outputTokens, total_tokens: computedPromptTokens + state.outputTokens, prompt_tokens_details: { cached_tokens: state.cacheReadTokens, cache_creation_input_tokens: state.cacheCreationTokens, ...(state.cacheCreation1hTokens !== undefined || state.cacheCreation5mTokens !== undefined ? { anthropic_cache_creation: { ...(state.cacheCreation1hTokens !== undefined ? { ephemeral_1h_input_tokens: state.cacheCreation1hTokens } : {}), ...(state.cacheCreation5mTokens !== undefined ? { ephemeral_5m_input_tokens: state.cacheCreation5mTokens } : {}), }, } : {}), }, completion_tokens_details: { reasoning_tokens: state.thinkingTokens ?? 0, accepted_prediction_tokens: 0, rejected_prediction_tokens: 0, }, copilot_usage: state.copilotUsage, }, finishReason: mapStopReason(state.stopReason), message: { role: Raw.ChatRole.Assistant, content: state.textContent ? [{ type: Raw.ChatCompletionContentPartKind.Text, text: state.textContent }] : [], ...(state.toolCalls.length > 0 ? { toolCalls: state.toolCalls.map(tc => ({ id: tc.id, type: 'function' as const, function: { name: tc.name, arguments: tc.arguments } })) } : {}) } }; } /** * Shape of an Anthropic Messages API non-streaming response. * Tagged union: successful responses have `type: 'message'`, error * responses have `type: 'error'`. */ type AnthropicNonStreamingResponse = | { type: 'message'; id: string; role: string; content: readonly ( | { type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: unknown } | { type: 'thinking'; thinking: string; signature: string } | { type: 'redacted_thinking'; data: string } )[]; model: string; stop_reason: string | null; stop_details?: RefusalStopDetails | null; usage: { input_tokens: number; output_tokens: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; cache_creation?: { ephemeral_1h_input_tokens?: number; ephemeral_5m_input_tokens?: number; }; output_tokens_details?: { thinking_tokens?: number; }; }; } | { type: 'error'; error: { type?: string; message?: string }; }; /** * Process a non-streaming response from the Anthropic Messages API. * Returns the same `ChatCompletion` shape as the streaming path. * * NOTE: Thinking / redacted_thinking content blocks are retained in restricted telemetry but * intentionally not surfaced to callers. If a future caller needs them, this function must be * extended to include them in the `ChatCompletion.message` and in the * `finishCallback` delta. */ export async function processNonStreamingResponseFromMessagesEndpoint( telemetryService: ITelemetryService, logService: ILogService, response: Response, finishCallback: FinishedCallback, telemetryData: TelemetryData ): Promise> { return new AsyncIterableObject(async feed => { const { headerRequestId, serverExperiments, copilotServiceRequestId, gitHubCopilotRequestTe } = getRequestId(response.headers); const requestId = headerRequestId || generateUuid(); const ghRequestId = response.headers.get('x-github-request-id') ?? ''; const bodyText = await response.text(); let parsed: AnthropicNonStreamingResponse; try { parsed = JSON.parse(bodyText); } catch (e) { feed.reject(new Error(`Failed to parse non-streaming Anthropic response: ${e instanceof Error ? e.message : String(e)}`)); return; } if (parsed.type === 'error') { feed.reject(new Error(`Anthropic API error: ${parsed.error?.message ?? 'Unknown error'}`)); return; } // Extract content blocks — guard against missing content array // (shouldn't happen for type=message, but be defensive). let textContent = ''; const toolCalls: { id: string; name: string; arguments: string }[] = []; for (const block of parsed.content ?? []) { switch (block.type) { case 'text': textContent += block.text; break; case 'tool_use': toolCalls.push({ id: block.id, name: block.name, arguments: JSON.stringify(block.input), }); break; case 'thinking': case 'redacted_thinking': // Intentionally not surfaced — see function JSDoc. break; default: { // Parity with streaming path: log + emit telemetry for unknown block types const unknownType = (block as { type: string }).type; logService.warn(`[messagesAPI] non-streaming: unknown content_block type '${unknownType}' for model ${parsed.model}`); /* __GDPR__ "messagesApi.unknownContentBlock" : { "owner": "bhavyaus", "comment": "Tracks unknown Anthropic content block types", "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request ID for correlation" }, "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that emitted the unknown block" }, "blockType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The unknown content_block.type string" } } */ telemetryService.sendMSFTTelemetryEvent('messagesApi.unknownContentBlock', { requestId, model: parsed.model, blockType: unknownType, }); break; } } } if (parsed.stop_reason === 'refusal') { const category = parsed.stop_details?.category ?? 'unknown'; logService.warn(`[messagesAPI] non-streaming: Refusal received: category='${category}' for model ${parsed.model}`); /* __GDPR__ "messagesApi.refusal" : { "owner": "bhavyaus", "comment": "Tracks Anthropic refusal responses including cyber and other policy categories", "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request ID for correlation" }, "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that produced the refusal" }, "category": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The refusal category (e.g. cyber, content_policy)" } } */ telemetryService.sendMSFTTelemetryEvent('messagesApi.refusal', { requestId, model: parsed.model, category, } ); } // There are no incremental deltas here, so callback-only consumers need the whole response. const delta: IResponseDelta = { text: textContent, ...(toolCalls.length > 0 ? { copilotToolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments, })), } : {}), }; await finishCallback(textContent, 0, delta); const usage = parsed.usage; const completion = buildAnthropicCompletion({ model: parsed.model, messageId: parsed.id, stopReason: parsed.stop_reason, textContent, toolCalls, inputTokens: usage?.input_tokens ?? 0, outputTokens: usage?.output_tokens ?? 0, cacheCreationTokens: usage?.cache_creation_input_tokens ?? 0, cacheCreation1hTokens: usage?.cache_creation?.ephemeral_1h_input_tokens, cacheCreation5mTokens: usage?.cache_creation?.ephemeral_5m_input_tokens, cacheReadTokens: usage?.cache_read_input_tokens ?? 0, thinkingTokens: usage?.output_tokens_details?.thinking_tokens, requestId, ghRequestId, copilotServiceRequestId, gitHubCopilotRequestTe, serverExperiments, telemetryData, }, logService); logService.info(`[messagesAPI] non-streaming message returned. finish reason: [${completion.finishReason}]`); const dataToSendToTelemetry = telemetryData.extendedBy({ completionChoiceFinishReason: completion.finishReason, headerRequestId: completion.requestId.headerRequestId, ...gitHubCopilotRequestTeProperty(completion.requestId.gitHubCopilotRequestTe), }); telemetryService.sendGHTelemetryEvent('completion.finishReason', dataToSendToTelemetry.properties, dataToSendToTelemetry.measurements); const telemetryMessage = rawMessageToCAPI(completion.message); const telemetryMessages = parsed.content?.some(block => block.type === 'thinking' || block.type === 'redacted_thinking') ? [{ ...telemetryMessage, content: parsed.content }] : [telemetryMessage]; let telemetryDataWithUsage = telemetryData.extendedBy(gitHubCopilotRequestTeProperty(completion.requestId.gitHubCopilotRequestTe)); if (completion.usage) { telemetryDataWithUsage = telemetryDataWithUsage.extendedBy({}, { promptTokens: completion.usage.prompt_tokens, completionTokens: completion.usage.completion_tokens, totalTokens: completion.usage.total_tokens, ...(completion.usage.prompt_tokens_details && { cachedTokens: completion.usage.prompt_tokens_details.cached_tokens }), ...(completion.usage.completion_tokens_details && { reasoningTokens: completion.usage.completion_tokens_details.reasoning_tokens, acceptedPredictionTokens: completion.usage.completion_tokens_details.accepted_prediction_tokens, rejectedPredictionTokens: completion.usage.completion_tokens_details.rejected_prediction_tokens, }), }); } sendEngineMessagesTelemetry(telemetryService, telemetryMessages, telemetryDataWithUsage, true, logService); feed.emitOne(completion); }, async () => { await response.body.destroy(); }); } export class AnthropicMessagesProcessor { private textAccumulator: string = ''; private toolCallAccumulator: Map = new Map(); private thinkingAccumulator: Map = new Map(); private readonly telemetryContent = new Map(); private completedToolCalls: Array<{ id: string; name: string; arguments: string }> = []; private messageId: string = ''; private model: string = ''; private inputTokens: number = 0; private outputTokens: number = 0; private cacheCreationTokens: number = 0; private cacheCreation1hTokens: number | undefined; private cacheCreation5mTokens: number | undefined; private cacheReadTokens: number = 0; private thinkingTokens: number | undefined; private copilotUsage?: { total_nano_aiu: number }; private contextManagementResponse?: ContextManagementResponse; private stopReason: string | undefined; private stopDetails?: RefusalStopDetails; /** Raw `X-GitHub-Copilot-Request-Te` value for this model call. */ gitHubCopilotRequestTe: string | undefined; constructor( private readonly telemetryData: TelemetryData, private readonly requestId: string, private readonly ghRequestId: string, private readonly copilotServiceRequestId: string, private readonly serverExperiments: string, @ILogService private readonly logService: ILogService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { } /** * Extract IP code citations from copilot_annotations and convert to IIPCodeCitation format */ private extractIPCodeCitations(annotations?: { IPCodeCitations?: AnthropicIPCodeCitation[] }): IIPCodeCitation[] { if (!annotations?.IPCodeCitations?.length) { return []; } // Deduplicate by URL since the same citation can appear multiple times const seenUrls = new Set(); const citations: IIPCodeCitation[] = []; for (const citation of annotations.IPCodeCitations) { const citationDetails = citation.citations; if (!citationDetails) { continue; } const { url, license, snippet } = citationDetails; if (typeof url !== 'string' || url.trim() === '') { continue; } if (typeof license !== 'string' || license.trim() === '') { continue; } if (typeof snippet !== 'string' || snippet.trim() === '') { continue; } if (!seenUrls.has(url)) { seenUrls.add(url); citations.push({ citations: { url, license, snippet, } }); } } if (citations.length > 0) { this.logService.trace(`[messagesAPI] IP code citations found: ${citations.length} unique citations`); } return citations; } public push(chunk: AnthropicStreamEvent, _onProgress: FinishedCallback): ChatCompletion | undefined { const onProgress = (delta: IResponseDelta): undefined => { this.textAccumulator += delta.text; _onProgress(this.textAccumulator, 0, delta); }; switch (chunk.type) { case 'message_start': if (chunk.message) { this.messageId = chunk.message.id; this.model = chunk.message.model; this.inputTokens = chunk.message.usage.input_tokens ?? 0; this.outputTokens = chunk.message.usage.output_tokens ?? 0; this.cacheCreationTokens = chunk.message.usage.cache_creation_input_tokens ?? 0; this.cacheCreation1hTokens = chunk.message.usage.cache_creation?.ephemeral_1h_input_tokens ?? this.cacheCreation1hTokens; this.cacheCreation5mTokens = chunk.message.usage.cache_creation?.ephemeral_5m_input_tokens ?? this.cacheCreation5mTokens; this.cacheReadTokens = chunk.message.usage.cache_read_input_tokens ?? 0; this.thinkingTokens = chunk.message.usage.output_tokens_details?.thinking_tokens ?? this.thinkingTokens; } return; case 'content_block_start': if (chunk.content_block?.type === 'tool_use' && chunk.index !== undefined) { const toolCallId = chunk.content_block.id || generateUuid(); this.toolCallAccumulator.set(chunk.index, { id: toolCallId, name: chunk.content_block.name || '', arguments: '', }); if (this.textAccumulator.length) { onProgress({ text: ' ' }); } onProgress({ text: '', beginToolCalls: [{ name: chunk.content_block.name || '', id: toolCallId }] }); } else if (chunk.content_block?.type === 'text' && chunk.index !== undefined) { this.telemetryContent.set(chunk.index, { ...chunk.content_block }); } else if (chunk.content_block?.type === 'thinking' && chunk.index !== undefined) { this.thinkingAccumulator.set(chunk.index, { thinking: '', signature: '', }); this.telemetryContent.set(chunk.index, { ...chunk.content_block }); } else if (chunk.content_block?.type === 'redacted_thinking' && chunk.index !== undefined) { this.telemetryContent.set(chunk.index, { ...chunk.content_block }); const data = (chunk.content_block as { type: 'redacted_thinking'; data: string }).data; onProgress({ text: '', thinking: { id: `thinking_${chunk.index}`, encrypted: data, redacted: true, } }); } return; case 'content_block_delta': if (chunk.delta) { if (chunk.delta.type === 'text_delta' && chunk.delta.text) { const block = chunk.index !== undefined ? this.telemetryContent.get(chunk.index) : undefined; if (block?.type === 'text') { block.text += chunk.delta.text; } const ipCitations = this.extractIPCodeCitations(chunk.copilot_annotations); if (ipCitations.length > 0) { return onProgress({ text: chunk.delta.text, ipCitations }); } return onProgress({ text: chunk.delta.text }); } else if (chunk.delta.type === 'thinking_delta' && chunk.delta.thinking && chunk.index !== undefined) { const block = this.telemetryContent.get(chunk.index); if (block?.type === 'thinking') { block.thinking += chunk.delta.thinking; } const thinking = this.thinkingAccumulator.get(chunk.index); if (thinking) { thinking.thinking += chunk.delta.thinking; } return onProgress({ text: '', thinking: { id: `thinking_${chunk.index}`, text: chunk.delta.thinking, } }); } else if (chunk.delta.type === 'signature_delta' && chunk.delta.signature && chunk.index !== undefined) { const block = this.telemetryContent.get(chunk.index); if (block?.type === 'thinking') { block.signature += chunk.delta.signature; } const thinking = this.thinkingAccumulator.get(chunk.index); if (thinking) { thinking.signature += chunk.delta.signature; } // Don't report signature deltas to the user } else if (chunk.delta.type === 'input_json_delta' && chunk.delta.partial_json && chunk.index !== undefined) { const toolCall = this.toolCallAccumulator.get(chunk.index); if (toolCall) { toolCall.arguments += chunk.delta.partial_json; onProgress({ text: '', copilotToolCallStreamUpdates: [{ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments, }], }); } } } return; case 'content_block_stop': if (chunk.index !== undefined) { const toolCall = this.toolCallAccumulator.get(chunk.index); if (toolCall) { this.completedToolCalls.push(toolCall); onProgress({ text: '', copilotToolCalls: [{ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments, }], }); this.toolCallAccumulator.delete(chunk.index); } const thinking = this.thinkingAccumulator.get(chunk.index); if (thinking && thinking.signature) { onProgress({ text: '', thinking: { id: `thinking_${chunk.index}`, encrypted: thinking.signature, } }); this.thinkingAccumulator.delete(chunk.index); } } return; case 'message_delta': if (chunk.usage) { // message_delta provides the most accurate token counts this.outputTokens = chunk.usage.output_tokens; this.inputTokens = chunk.usage.input_tokens ?? this.inputTokens; this.cacheCreationTokens = chunk.usage.cache_creation_input_tokens ?? this.cacheCreationTokens; this.cacheCreation1hTokens = chunk.usage.cache_creation?.ephemeral_1h_input_tokens ?? this.cacheCreation1hTokens; this.cacheCreation5mTokens = chunk.usage.cache_creation?.ephemeral_5m_input_tokens ?? this.cacheCreation5mTokens; this.cacheReadTokens = chunk.usage.cache_read_input_tokens ?? this.cacheReadTokens; this.thinkingTokens = chunk.usage.output_tokens_details?.thinking_tokens ?? this.thinkingTokens; } if (chunk.copilot_usage && typeof chunk.copilot_usage.total_nano_aiu === 'number') { this.copilotUsage = chunk.copilot_usage; } if (chunk.context_management) { this.contextManagementResponse = chunk.context_management; // Report context management via delta so it gets logged to request logger onProgress({ text: '', contextManagement: chunk.context_management }); } // Track stop_reason and stop_details for determining finish reason in message_stop if (chunk.delta?.stop_reason) { this.stopReason = chunk.delta.stop_reason; } if (chunk.delta?.stop_details) { this.stopDetails = chunk.delta.stop_details; } return; case 'message_stop': { if (this.contextManagementResponse) { const totalClearedTokens = this.contextManagementResponse.applied_edits.reduce( (sum, edit) => sum + (edit.cleared_input_tokens || 0), 0 ); const totalClearedToolUses = this.contextManagementResponse.applied_edits.reduce( (sum, edit) => sum + (edit.cleared_tool_uses || 0), 0 ); const totalClearedThinkingTurns = this.contextManagementResponse.applied_edits.reduce( (sum, edit) => sum + (edit.cleared_thinking_turns || 0), 0 ); this.logService.trace(`[messagesAPI] Anthropic context editing applied: cleared ${totalClearedTokens} tokens, ${totalClearedToolUses} tool uses.`); /* __GDPR__ "contextEditingApplied" : { "owner": "bhavyaus", "comment": "Tracks when Anthropic context editing is applied to manage context window", "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request ID for correlation" }, "interactionId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The interaction ID for correlation" }, "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model used" }, "clearedTokens": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total tokens cleared" }, "clearedToolUses": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total tool uses cleared" }, "clearedThinkingTurns": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total thinking turns cleared" } } */ this.telemetryService.sendMSFTTelemetryEvent('contextEditingApplied', { requestId: this.requestId, interactionId: this.requestId, model: this.model, }, { clearedTokens: totalClearedTokens, clearedToolUses: totalClearedToolUses, clearedThinkingTurns: totalClearedThinkingTurns, } ); } if (this.stopReason === 'refusal') { const category = this.stopDetails?.category ?? 'unknown'; this.logService.warn(`[messagesAPI] Refusal received: category='${category}' for model ${this.model}`); /* __GDPR__ "messagesApi.refusal" : { "owner": "bhavyaus", "comment": "Tracks Anthropic refusal responses including cyber and other policy categories", "requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The request ID for correlation" }, "model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that produced the refusal" }, "category": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The refusal category (e.g. cyber, content_policy)" } } */ this.telemetryService.sendMSFTTelemetryEvent('messagesApi.refusal', { requestId: this.requestId, model: this.model, category, } ); } const completion = buildAnthropicCompletion({ model: this.model, messageId: this.messageId, stopReason: this.stopReason, textContent: this.textAccumulator, toolCalls: this.completedToolCalls, inputTokens: this.inputTokens, outputTokens: this.outputTokens, cacheCreationTokens: this.cacheCreationTokens, cacheCreation1hTokens: this.cacheCreation1hTokens, cacheCreation5mTokens: this.cacheCreation5mTokens, cacheReadTokens: this.cacheReadTokens, thinkingTokens: this.thinkingTokens, requestId: this.requestId, ghRequestId: this.ghRequestId, copilotServiceRequestId: this.copilotServiceRequestId, gitHubCopilotRequestTe: this.gitHubCopilotRequestTe, serverExperiments: this.serverExperiments, telemetryData: this.telemetryData, copilotUsage: this.copilotUsage, }, this.logService); const content = [...this.telemetryContent.entries()].sort(([a], [b]) => a - b).map(([, block]) => block); if (content.some(block => block.type === 'thinking' || block.type === 'redacted_thinking')) { completion.telemetryMessages = [{ ...rawMessageToCAPI(completion.message), content }]; } return completion; } case 'error': { const errorMessage = (chunk as unknown as { error?: { message?: string } }).error?.message || 'Unknown error'; return onProgress({ text: '', copilotErrors: [{ agent: 'anthropic', code: 'unknown', message: errorMessage, type: 'error', identifier: undefined }] }); } } } }