Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/agents/node/adapters/anthropicAdapter.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import Anthropic from '@anthropic-ai/sdk';
7
import * as http from 'http';
8
import type { OpenAiFunctionTool } from '../../../../platform/networking/common/fetch';
9
import { IMakeChatRequestOptions } from '../../../../platform/networking/common/networking';
10
import { APIUsage } from '../../../../platform/networking/common/openai';
11
import { coalesce } from '../../../../util/vs/base/common/arrays';
12
import { anthropicMessagesToRawMessages } from '../../../byok/common/anthropicMessageConverter';
13
import { IAgentStreamBlock, IParsedRequest, IProtocolAdapter, IProtocolAdapterFactory, IStreamEventData, IStreamingContext } from './types';
14
15
export class AnthropicAdapterFactory implements IProtocolAdapterFactory {
16
createAdapter(): IProtocolAdapter {
17
return new AnthropicAdapter();
18
}
19
}
20
21
class AnthropicAdapter implements IProtocolAdapter {
22
readonly name = 'anthropic';
23
24
// Per-request state
25
private currentBlockIndex = 0;
26
private hasTextBlock = false;
27
private hadToolCalls = false;
28
parseRequest(body: string): IParsedRequest {
29
const requestBody: Anthropic.MessageStreamParams = JSON.parse(body);
30
31
// Build a single system text block from "system" if provided
32
let systemText = '';
33
if (typeof requestBody.system === 'string') {
34
systemText = requestBody.system;
35
} else if (Array.isArray(requestBody.system) && requestBody.system.length > 0) {
36
systemText = requestBody.system.map(s => s.text).join('\n');
37
}
38
39
const type = systemText.includes('You are a helpful AI assistant tasked with summarizing conversations') ? 'summary' : undefined;
40
41
// Convert Anthropic messages to Raw (TSX) messages
42
const rawMessages = anthropicMessagesToRawMessages(requestBody.messages, { type: 'text', text: systemText });
43
44
const options: IMakeChatRequestOptions['requestOptions'] = {
45
temperature: requestBody.temperature,
46
};
47
48
if (requestBody.tools && requestBody.tools.length > 0) {
49
// Map Anthropic tools to VS Code chat tools. Provide a no-op invoke since this server doesn't run tools.
50
const tools = coalesce(requestBody.tools.map(tool => {
51
if ('input_schema' in tool) {
52
const chatTool: OpenAiFunctionTool = {
53
type: 'function',
54
function: {
55
name: tool.name,
56
description: tool.description || '',
57
parameters: tool.input_schema || {},
58
}
59
};
60
return chatTool;
61
}
62
return undefined;
63
}));
64
if (tools.length) {
65
options.tools = tools;
66
}
67
}
68
69
return {
70
model: requestBody.model,
71
messages: rawMessages,
72
options,
73
type
74
};
75
}
76
77
formatStreamResponse(
78
streamData: IAgentStreamBlock,
79
context: IStreamingContext
80
): IStreamEventData[] {
81
const events: IStreamEventData[] = [];
82
83
if (streamData.type === 'text') {
84
if (!this.hasTextBlock) {
85
// Send content_block_start for text
86
const contentBlockStart: Anthropic.RawContentBlockStartEvent = {
87
type: 'content_block_start',
88
index: this.currentBlockIndex,
89
content_block: {
90
type: 'text',
91
text: '',
92
citations: null
93
}
94
};
95
events.push({
96
event: contentBlockStart.type,
97
data: this.formatEventData(contentBlockStart)
98
});
99
this.hasTextBlock = true;
100
}
101
102
// Send content_block_delta for text
103
const contentDelta: Anthropic.RawContentBlockDeltaEvent = {
104
type: 'content_block_delta',
105
index: this.currentBlockIndex,
106
delta: {
107
type: 'text_delta',
108
text: streamData.content
109
}
110
};
111
events.push({
112
event: contentDelta.type,
113
data: this.formatEventData(contentDelta)
114
});
115
116
} else if (streamData.type === 'tool_call') {
117
// End current text block if it exists
118
if (this.hasTextBlock) {
119
const contentBlockStop: Anthropic.RawContentBlockStopEvent = {
120
type: 'content_block_stop',
121
index: this.currentBlockIndex
122
};
123
events.push({
124
event: contentBlockStop.type,
125
data: this.formatEventData(contentBlockStop)
126
});
127
this.currentBlockIndex++;
128
this.hasTextBlock = false;
129
}
130
131
this.hadToolCalls = true;
132
133
// Send tool use block
134
const toolBlockStart: Anthropic.RawContentBlockStartEvent = {
135
type: 'content_block_start',
136
index: this.currentBlockIndex,
137
content_block: {
138
type: 'tool_use',
139
id: streamData.callId,
140
name: streamData.name,
141
input: {},
142
caller: { type: 'direct' },
143
}
144
};
145
events.push({
146
event: toolBlockStart.type,
147
data: this.formatEventData(toolBlockStart)
148
});
149
150
// Send tool use content
151
const toolBlockContent: Anthropic.RawContentBlockDeltaEvent = {
152
type: 'content_block_delta',
153
index: this.currentBlockIndex,
154
delta: {
155
type: 'input_json_delta',
156
partial_json: JSON.stringify(streamData.input || {})
157
}
158
};
159
events.push({
160
event: toolBlockContent.type,
161
data: this.formatEventData(toolBlockContent)
162
});
163
164
const toolBlockStop: Anthropic.RawContentBlockStopEvent = {
165
type: 'content_block_stop',
166
index: this.currentBlockIndex
167
};
168
events.push({
169
event: toolBlockStop.type,
170
data: this.formatEventData(toolBlockStop)
171
});
172
173
this.currentBlockIndex++;
174
}
175
176
return events;
177
}
178
179
generateFinalEvents(context: IStreamingContext, usage?: APIUsage): IStreamEventData[] {
180
const events: IStreamEventData[] = [];
181
182
// Send final events
183
if (this.hasTextBlock) {
184
const contentBlockStop: Anthropic.RawContentBlockStopEvent = {
185
type: 'content_block_stop',
186
index: this.currentBlockIndex
187
};
188
events.push({
189
event: contentBlockStop.type,
190
data: this.formatEventData(contentBlockStop)
191
});
192
}
193
194
// Adjust token usage to make the agent think it has a 200k context window
195
// when the real one is smaller
196
const adjustedUsage = this.adjustTokenUsageForContextWindow(context, usage);
197
198
const messageDelta: Anthropic.RawMessageDeltaEvent = {
199
type: 'message_delta',
200
delta: {
201
stop_reason: this.hadToolCalls ? 'tool_use' : 'end_turn',
202
stop_sequence: null,
203
stop_details: null,
204
container: null
205
},
206
usage: {
207
output_tokens: adjustedUsage.completion_tokens,
208
cache_creation_input_tokens: 0,
209
cache_read_input_tokens: 0,
210
input_tokens: adjustedUsage.prompt_tokens,
211
server_tool_use: null
212
}
213
};
214
events.push({
215
event: messageDelta.type,
216
data: this.formatEventData(messageDelta)
217
});
218
219
const messageStop: Anthropic.RawMessageStopEvent = {
220
type: 'message_stop'
221
};
222
events.push({
223
event: messageStop.type,
224
data: this.formatEventData(messageStop)
225
});
226
227
return events;
228
}
229
230
private adjustTokenUsageForContextWindow(context: IStreamingContext, usage?: APIUsage): APIUsage {
231
// If we don't have usage, return defaults
232
if (!usage) {
233
return {
234
prompt_tokens: 0,
235
completion_tokens: 0,
236
total_tokens: 0
237
};
238
}
239
240
// If we don't have endpoint info, return the unadjusted usage
241
if (context.endpoint.modelId === 'gpt-4o-mini') {
242
return usage;
243
}
244
245
const realContextLimit = context.endpoint.modelMaxPromptTokens;
246
const agentAssumedContextLimit = 200000; // The agent thinks it has 200k tokens
247
248
// Calculate scaling factor to make the agent think it has a larger context window
249
// When the real usage approaches the real limit, the adjusted usage should approach the assumed limit
250
const scalingFactor = agentAssumedContextLimit / realContextLimit;
251
252
const adjustedPromptTokens = Math.floor(usage.prompt_tokens * scalingFactor);
253
const adjustedCompletionTokens = Math.floor(usage.completion_tokens * scalingFactor);
254
const adjustedTotalTokens = adjustedPromptTokens + adjustedCompletionTokens;
255
256
return {
257
...usage,
258
prompt_tokens: adjustedPromptTokens,
259
completion_tokens: adjustedCompletionTokens,
260
total_tokens: adjustedTotalTokens,
261
};
262
}
263
264
generateInitialEvents(context: IStreamingContext): IStreamEventData[] {
265
// Use adjusted token usage for initial events to be consistent
266
// For initial events, we don't have real usage yet, so we'll use defaults
267
const adjustedUsage = this.adjustTokenUsageForContextWindow(context, undefined);
268
269
// Send message_start event
270
const messageStart: Anthropic.RawMessageStartEvent = {
271
type: 'message_start',
272
message: {
273
id: context.requestId,
274
type: 'message',
275
role: 'assistant',
276
model: context.endpoint.modelId,
277
content: [],
278
container: null,
279
stop_reason: null,
280
stop_sequence: null,
281
stop_details: null,
282
usage: {
283
input_tokens: adjustedUsage.prompt_tokens,
284
cache_creation_input_tokens: 0,
285
cache_read_input_tokens: 0,
286
output_tokens: 1,
287
service_tier: null,
288
server_tool_use: null,
289
cache_creation: null,
290
} as Anthropic.Usage
291
}
292
};
293
294
return [{
295
event: messageStart.type,
296
data: this.formatEventData(messageStart)
297
}];
298
}
299
300
getContentType(): string {
301
return 'text/event-stream';
302
}
303
304
extractAuthKey(headers: http.IncomingHttpHeaders): string | undefined {
305
return headers['x-api-key'] as string | undefined;
306
}
307
308
private formatEventData(data: unknown): string {
309
return JSON.stringify(data).replace(/\n/g, '\\n');
310
}
311
}
312
313