Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/agents/node/langModelServer.ts
13399 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 { Raw } from '@vscode/prompt-tsx';
7
import * as http from 'http';
8
import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';
9
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
10
import { ILogService } from '../../../platform/log/common/logService';
11
import { IChatEndpoint } from '../../../platform/networking/common/networking';
12
import { APIUsage } from '../../../platform/networking/common/openai';
13
import { createServiceIdentifier } from '../../../util/common/services';
14
import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation';
15
import { generateUuid } from '../../../util/vs/base/common/uuid';
16
import { LanguageModelError } from '../../../vscodeTypes';
17
import { AnthropicAdapterFactory } from './adapters/anthropicAdapter';
18
import { IAgentStreamBlock, IProtocolAdapter, IProtocolAdapterFactory, IStreamingContext } from './adapters/types';
19
20
export interface ILanguageModelServerConfig {
21
readonly port: number;
22
readonly nonce: string;
23
}
24
25
export const ILanguageModelServer = createServiceIdentifier<ILanguageModelServer>('ILanguageModelServer');
26
export interface ILanguageModelServer {
27
readonly _serviceBrand: undefined;
28
start(): Promise<void>;
29
stop(): void;
30
getConfig(): ILanguageModelServerConfig;
31
}
32
33
export class LanguageModelServer implements ILanguageModelServer {
34
declare _serviceBrand: undefined;
35
36
private server: http.Server;
37
protected config: ILanguageModelServerConfig;
38
protected adapterFactories: Map<string, IProtocolAdapterFactory>;
39
protected readonly requestHandlers = new Map<string, { method: string; handler: (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> }>();
40
constructor(
41
@ILogService private readonly logService: ILogService,
42
@IEndpointProvider protected readonly endpointProvider: IEndpointProvider
43
) {
44
this.config = {
45
port: 0, // Will be set to random available port
46
nonce: 'vscode-lm-' + generateUuid()
47
};
48
this.adapterFactories = new Map();
49
this.adapterFactories.set('/v1/messages', new AnthropicAdapterFactory());
50
51
this.server = this.createServer();
52
}
53
54
private createServer(): http.Server {
55
return http.createServer(async (req, res) => {
56
this.logService.trace(`Received request: ${req.method} ${req.url}`);
57
58
if (req.method === 'OPTIONS') {
59
res.writeHead(200);
60
res.end();
61
return;
62
}
63
64
const handler = this.requestHandlers.get(req.url || '');
65
if (handler && handler.method === req.method) {
66
await handler.handler(req, res);
67
return;
68
}
69
70
if (req.method === 'POST') {
71
const adapterFactory = this.getAdapterFactoryForPath(req.url || '');
72
if (adapterFactory) {
73
try {
74
// Create new adapter instance for this request
75
const adapter = adapterFactory.createAdapter();
76
const body = await this.readRequestBody(req);
77
78
// Verify nonce for authentication
79
const authKey = adapter.extractAuthKey(req.headers);
80
if (authKey !== this.config.nonce) {
81
this.logService.trace(`[LanguageModelServer] Invalid auth key`);
82
res.writeHead(401, { 'Content-Type': 'application/json' });
83
res.end(JSON.stringify({ error: 'Invalid authentication' }));
84
return;
85
}
86
87
await this.handleChatRequest(adapter, body, res);
88
} catch (error) {
89
res.writeHead(500, { 'Content-Type': 'application/json' });
90
res.end(JSON.stringify({
91
error: 'Internal server error',
92
details: error instanceof Error ? error.message : String(error)
93
}));
94
}
95
return;
96
}
97
}
98
99
if (req.method === 'GET' && req.url === '/') {
100
res.writeHead(200);
101
res.end('Hello from LanguageModelServer');
102
return;
103
}
104
105
if (req.method === 'GET' && req.url === '/models') {
106
res.writeHead(200, { 'Content-Type': 'application/json' });
107
res.end(JSON.stringify({ data: [] }));
108
return;
109
}
110
111
res.writeHead(404, { 'Content-Type': 'application/json' });
112
res.end(JSON.stringify({ error: 'Not found' }));
113
});
114
}
115
116
private parseUrlPathname(url: string): string {
117
try {
118
const parsedUrl = new URL(url, 'http://localhost');
119
return parsedUrl.pathname;
120
} catch {
121
return url.split('?')[0];
122
}
123
}
124
125
private getAdapterFactoryForPath(url: string): IProtocolAdapterFactory | undefined {
126
const pathname = this.parseUrlPathname(url);
127
return this.adapterFactories.get(pathname);
128
}
129
130
private async readRequestBody(req: http.IncomingMessage): Promise<string> {
131
return new Promise((resolve, reject) => {
132
let body = '';
133
req.on('data', chunk => {
134
body += chunk.toString();
135
});
136
req.on('end', () => {
137
resolve(body);
138
});
139
req.on('error', reject);
140
});
141
}
142
143
private async handleChatRequest(adapter: IProtocolAdapter, body: string, res: http.ServerResponse): Promise<void> {
144
try {
145
const parsedRequest = adapter.parseRequest(body);
146
147
const endpoints = await this.endpointProvider.getAllChatEndpoints();
148
149
if (endpoints.length === 0) {
150
res.writeHead(404, { 'Content-Type': 'application/json' });
151
res.end(JSON.stringify({ error: 'No language models available' }));
152
return;
153
}
154
155
const selectedEndpoint = this.selectEndpoint(endpoints, parsedRequest.model);
156
if (!selectedEndpoint) {
157
res.writeHead(404, { 'Content-Type': 'application/json' });
158
res.end(JSON.stringify({
159
error: 'No model found matching criteria'
160
}));
161
return;
162
}
163
164
// Set up streaming response
165
res.writeHead(200, {
166
'Content-Type': adapter.getContentType(),
167
'Cache-Control': 'no-cache',
168
'Connection': 'keep-alive',
169
});
170
171
// Create cancellation token for the request
172
const tokenSource = new CancellationTokenSource();
173
174
// Handle client disconnect
175
let requestComplete = false;
176
res.on('close', () => {
177
if (!requestComplete) {
178
this.logService.info(`[LanguageModelServer] Client disconnected before request complete`);
179
}
180
181
tokenSource.cancel();
182
});
183
184
try {
185
// Create streaming context with only essential shared data
186
const context: IStreamingContext = {
187
requestId: `req_${Math.random().toString(36).substr(2, 20)}`,
188
endpoint: {
189
modelId: selectedEndpoint.model,
190
modelMaxPromptTokens: selectedEndpoint.modelMaxPromptTokens
191
}
192
};
193
194
// Send initial events if adapter supports them
195
if (adapter.generateInitialEvents) {
196
const initialEvents = adapter.generateInitialEvents(context);
197
for (const event of initialEvents) {
198
res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);
199
}
200
}
201
202
const userInitiatedRequest = parsedRequest.messages.at(-1)?.role === Raw.ChatRole.User;
203
const fetchResult = await selectedEndpoint.makeChatRequest2({
204
debugName: 'agentLMServer' + (parsedRequest.type ? `-${parsedRequest.type}` : ''),
205
messages: parsedRequest.messages as Raw.ChatMessage[],
206
finishedCb: async (_fullText, _index, delta) => {
207
if (tokenSource.token.isCancellationRequested) {
208
return 0; // stop
209
}
210
// Emit text deltas
211
if (delta.text) {
212
const textData: IAgentStreamBlock = {
213
type: 'text',
214
content: delta.text
215
};
216
for (const event of adapter.formatStreamResponse(textData, context)) {
217
res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);
218
}
219
}
220
// Emit tool calls if present
221
if (delta.copilotToolCalls && delta.copilotToolCalls.length > 0) {
222
for (const call of delta.copilotToolCalls) {
223
let input: object = {};
224
try { input = call.arguments ? JSON.parse(call.arguments) : {}; } catch { input = {}; }
225
const toolData: IAgentStreamBlock = {
226
type: 'tool_call',
227
callId: call.id,
228
name: call.name,
229
input
230
};
231
for (const event of adapter.formatStreamResponse(toolData, context)) {
232
res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);
233
}
234
}
235
}
236
return undefined;
237
},
238
location: ChatLocation.Agent,
239
requestOptions: { ...parsedRequest.options, stream: false },
240
userInitiatedRequest,
241
telemetryProperties: {
242
messageSource: `lmServer-${adapter.name}`
243
}
244
}, tokenSource.token);
245
246
// Capture usage information if available
247
let usage: APIUsage | undefined;
248
if (fetchResult.type === ChatFetchResponseType.Success && fetchResult.usage) {
249
usage = fetchResult.usage;
250
}
251
252
requestComplete = true;
253
254
// Send final events
255
const finalEvents = adapter.generateFinalEvents(context, usage);
256
for (const event of finalEvents) {
257
res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);
258
}
259
260
res.end();
261
} catch (error) {
262
requestComplete = true;
263
if (error instanceof LanguageModelError) {
264
res.write(JSON.stringify({
265
error: 'Language model error',
266
code: error.code,
267
message: error.message,
268
cause: error.cause
269
}));
270
} else {
271
res.write(JSON.stringify({
272
error: 'Request failed',
273
message: error instanceof Error ? error.message : String(error)
274
}));
275
}
276
res.end();
277
} finally {
278
tokenSource.dispose();
279
}
280
281
} catch (error) {
282
res.writeHead(500, { 'Content-Type': 'application/json' });
283
res.end(JSON.stringify({
284
error: 'Failed to process chat request',
285
details: error instanceof Error ? error.message : String(error)
286
}));
287
}
288
}
289
290
private selectEndpoint(endpoints: readonly IChatEndpoint[], requestedModel?: string): IChatEndpoint | undefined {
291
if (requestedModel) {
292
// Handle model mapping
293
let mappedModel = requestedModel;
294
if (requestedModel.startsWith('claude-haiku')) {
295
mappedModel = 'claude-haiku-4.5';
296
}
297
if (requestedModel.startsWith('claude-sonnet-4')) {
298
mappedModel = 'claude-sonnet-4.5';
299
}
300
if (requestedModel.startsWith('claude-opus-4')) {
301
mappedModel = 'claude-opus-4.5';
302
}
303
304
// Try to find exact match first
305
let selectedEndpoint = endpoints.find(e => e.family === mappedModel || e.model === mappedModel);
306
307
// If not found, try to find by partial match for Anthropic models
308
if (!selectedEndpoint && requestedModel.startsWith('claude-haiku-4')) {
309
selectedEndpoint = endpoints.find(e => e.model.includes('claude-haiku-4-5')) ?? endpoints.find(e => e.model.includes('claude'));
310
} else if (!selectedEndpoint && requestedModel.startsWith('claude-sonnet-4')) {
311
selectedEndpoint = endpoints.find(e => e.model.includes('claude-sonnet-4-5')) ?? endpoints.find(e => e.model.includes('claude'));
312
} else if (!selectedEndpoint && requestedModel.startsWith('claude-opus-4')) {
313
selectedEndpoint = endpoints.find(e => e.model.includes('claude-opus-4-5')) ?? endpoints.find(e => e.model.includes('claude'));
314
}
315
316
return selectedEndpoint;
317
}
318
319
// Use first available model if no criteria specified
320
return endpoints[0];
321
}
322
323
public async start(): Promise<void> {
324
return new Promise((resolve) => {
325
this.server.listen(0, '127.0.0.1', () => {
326
const address = this.server.address();
327
if (address && typeof address === 'object') {
328
this.config = {
329
...this.config,
330
port: address.port
331
};
332
this.logService.trace(`Language Model Server started on http://localhost:${this.config.port}`);
333
resolve();
334
}
335
});
336
});
337
}
338
339
public stop(): void {
340
this.server.close();
341
}
342
343
public getConfig(): ILanguageModelServerConfig {
344
return { ...this.config };
345
}
346
}
347
348