Path: blob/main/extensions/copilot/src/extension/agents/node/adapters/anthropicAdapter.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import Anthropic from '@anthropic-ai/sdk';6import * as http from 'http';7import type { OpenAiFunctionTool } from '../../../../platform/networking/common/fetch';8import { IMakeChatRequestOptions } from '../../../../platform/networking/common/networking';9import { APIUsage } from '../../../../platform/networking/common/openai';10import { coalesce } from '../../../../util/vs/base/common/arrays';11import { anthropicMessagesToRawMessages } from '../../../byok/common/anthropicMessageConverter';12import { IAgentStreamBlock, IParsedRequest, IProtocolAdapter, IProtocolAdapterFactory, IStreamEventData, IStreamingContext } from './types';1314export class AnthropicAdapterFactory implements IProtocolAdapterFactory {15createAdapter(): IProtocolAdapter {16return new AnthropicAdapter();17}18}1920class AnthropicAdapter implements IProtocolAdapter {21readonly name = 'anthropic';2223// Per-request state24private currentBlockIndex = 0;25private hasTextBlock = false;26private hadToolCalls = false;27parseRequest(body: string): IParsedRequest {28const requestBody: Anthropic.MessageStreamParams = JSON.parse(body);2930// Build a single system text block from "system" if provided31let systemText = '';32if (typeof requestBody.system === 'string') {33systemText = requestBody.system;34} else if (Array.isArray(requestBody.system) && requestBody.system.length > 0) {35systemText = requestBody.system.map(s => s.text).join('\n');36}3738const type = systemText.includes('You are a helpful AI assistant tasked with summarizing conversations') ? 'summary' : undefined;3940// Convert Anthropic messages to Raw (TSX) messages41const rawMessages = anthropicMessagesToRawMessages(requestBody.messages, { type: 'text', text: systemText });4243const options: IMakeChatRequestOptions['requestOptions'] = {44temperature: requestBody.temperature,45};4647if (requestBody.tools && requestBody.tools.length > 0) {48// Map Anthropic tools to VS Code chat tools. Provide a no-op invoke since this server doesn't run tools.49const tools = coalesce(requestBody.tools.map(tool => {50if ('input_schema' in tool) {51const chatTool: OpenAiFunctionTool = {52type: 'function',53function: {54name: tool.name,55description: tool.description || '',56parameters: tool.input_schema || {},57}58};59return chatTool;60}61return undefined;62}));63if (tools.length) {64options.tools = tools;65}66}6768return {69model: requestBody.model,70messages: rawMessages,71options,72type73};74}7576formatStreamResponse(77streamData: IAgentStreamBlock,78context: IStreamingContext79): IStreamEventData[] {80const events: IStreamEventData[] = [];8182if (streamData.type === 'text') {83if (!this.hasTextBlock) {84// Send content_block_start for text85const contentBlockStart: Anthropic.RawContentBlockStartEvent = {86type: 'content_block_start',87index: this.currentBlockIndex,88content_block: {89type: 'text',90text: '',91citations: null92}93};94events.push({95event: contentBlockStart.type,96data: this.formatEventData(contentBlockStart)97});98this.hasTextBlock = true;99}100101// Send content_block_delta for text102const contentDelta: Anthropic.RawContentBlockDeltaEvent = {103type: 'content_block_delta',104index: this.currentBlockIndex,105delta: {106type: 'text_delta',107text: streamData.content108}109};110events.push({111event: contentDelta.type,112data: this.formatEventData(contentDelta)113});114115} else if (streamData.type === 'tool_call') {116// End current text block if it exists117if (this.hasTextBlock) {118const contentBlockStop: Anthropic.RawContentBlockStopEvent = {119type: 'content_block_stop',120index: this.currentBlockIndex121};122events.push({123event: contentBlockStop.type,124data: this.formatEventData(contentBlockStop)125});126this.currentBlockIndex++;127this.hasTextBlock = false;128}129130this.hadToolCalls = true;131132// Send tool use block133const toolBlockStart: Anthropic.RawContentBlockStartEvent = {134type: 'content_block_start',135index: this.currentBlockIndex,136content_block: {137type: 'tool_use',138id: streamData.callId,139name: streamData.name,140input: {},141caller: { type: 'direct' },142}143};144events.push({145event: toolBlockStart.type,146data: this.formatEventData(toolBlockStart)147});148149// Send tool use content150const toolBlockContent: Anthropic.RawContentBlockDeltaEvent = {151type: 'content_block_delta',152index: this.currentBlockIndex,153delta: {154type: 'input_json_delta',155partial_json: JSON.stringify(streamData.input || {})156}157};158events.push({159event: toolBlockContent.type,160data: this.formatEventData(toolBlockContent)161});162163const toolBlockStop: Anthropic.RawContentBlockStopEvent = {164type: 'content_block_stop',165index: this.currentBlockIndex166};167events.push({168event: toolBlockStop.type,169data: this.formatEventData(toolBlockStop)170});171172this.currentBlockIndex++;173}174175return events;176}177178generateFinalEvents(context: IStreamingContext, usage?: APIUsage): IStreamEventData[] {179const events: IStreamEventData[] = [];180181// Send final events182if (this.hasTextBlock) {183const contentBlockStop: Anthropic.RawContentBlockStopEvent = {184type: 'content_block_stop',185index: this.currentBlockIndex186};187events.push({188event: contentBlockStop.type,189data: this.formatEventData(contentBlockStop)190});191}192193// Adjust token usage to make the agent think it has a 200k context window194// when the real one is smaller195const adjustedUsage = this.adjustTokenUsageForContextWindow(context, usage);196197const messageDelta: Anthropic.RawMessageDeltaEvent = {198type: 'message_delta',199delta: {200stop_reason: this.hadToolCalls ? 'tool_use' : 'end_turn',201stop_sequence: null,202stop_details: null,203container: null204},205usage: {206output_tokens: adjustedUsage.completion_tokens,207cache_creation_input_tokens: 0,208cache_read_input_tokens: 0,209input_tokens: adjustedUsage.prompt_tokens,210server_tool_use: null211}212};213events.push({214event: messageDelta.type,215data: this.formatEventData(messageDelta)216});217218const messageStop: Anthropic.RawMessageStopEvent = {219type: 'message_stop'220};221events.push({222event: messageStop.type,223data: this.formatEventData(messageStop)224});225226return events;227}228229private adjustTokenUsageForContextWindow(context: IStreamingContext, usage?: APIUsage): APIUsage {230// If we don't have usage, return defaults231if (!usage) {232return {233prompt_tokens: 0,234completion_tokens: 0,235total_tokens: 0236};237}238239// If we don't have endpoint info, return the unadjusted usage240if (context.endpoint.modelId === 'gpt-4o-mini') {241return usage;242}243244const realContextLimit = context.endpoint.modelMaxPromptTokens;245const agentAssumedContextLimit = 200000; // The agent thinks it has 200k tokens246247// Calculate scaling factor to make the agent think it has a larger context window248// When the real usage approaches the real limit, the adjusted usage should approach the assumed limit249const scalingFactor = agentAssumedContextLimit / realContextLimit;250251const adjustedPromptTokens = Math.floor(usage.prompt_tokens * scalingFactor);252const adjustedCompletionTokens = Math.floor(usage.completion_tokens * scalingFactor);253const adjustedTotalTokens = adjustedPromptTokens + adjustedCompletionTokens;254255return {256...usage,257prompt_tokens: adjustedPromptTokens,258completion_tokens: adjustedCompletionTokens,259total_tokens: adjustedTotalTokens,260};261}262263generateInitialEvents(context: IStreamingContext): IStreamEventData[] {264// Use adjusted token usage for initial events to be consistent265// For initial events, we don't have real usage yet, so we'll use defaults266const adjustedUsage = this.adjustTokenUsageForContextWindow(context, undefined);267268// Send message_start event269const messageStart: Anthropic.RawMessageStartEvent = {270type: 'message_start',271message: {272id: context.requestId,273type: 'message',274role: 'assistant',275model: context.endpoint.modelId,276content: [],277container: null,278stop_reason: null,279stop_sequence: null,280stop_details: null,281usage: {282input_tokens: adjustedUsage.prompt_tokens,283cache_creation_input_tokens: 0,284cache_read_input_tokens: 0,285output_tokens: 1,286service_tier: null,287server_tool_use: null,288cache_creation: null,289} as Anthropic.Usage290}291};292293return [{294event: messageStart.type,295data: this.formatEventData(messageStart)296}];297}298299getContentType(): string {300return 'text/event-stream';301}302303extractAuthKey(headers: http.IncomingHttpHeaders): string | undefined {304return headers['x-api-key'] as string | undefined;305}306307private formatEventData(data: unknown): string {308return JSON.stringify(data).replace(/\n/g, '\\n');309}310}311312313