Path: blob/main/extensions/copilot/src/extension/agents/node/langModelServer.ts
13399 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 { Raw } from '@vscode/prompt-tsx';6import * as http from 'http';7import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';8import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';9import { ILogService } from '../../../platform/log/common/logService';10import { IChatEndpoint } from '../../../platform/networking/common/networking';11import { APIUsage } from '../../../platform/networking/common/openai';12import { createServiceIdentifier } from '../../../util/common/services';13import { CancellationTokenSource } from '../../../util/vs/base/common/cancellation';14import { generateUuid } from '../../../util/vs/base/common/uuid';15import { LanguageModelError } from '../../../vscodeTypes';16import { AnthropicAdapterFactory } from './adapters/anthropicAdapter';17import { IAgentStreamBlock, IProtocolAdapter, IProtocolAdapterFactory, IStreamingContext } from './adapters/types';1819export interface ILanguageModelServerConfig {20readonly port: number;21readonly nonce: string;22}2324export const ILanguageModelServer = createServiceIdentifier<ILanguageModelServer>('ILanguageModelServer');25export interface ILanguageModelServer {26readonly _serviceBrand: undefined;27start(): Promise<void>;28stop(): void;29getConfig(): ILanguageModelServerConfig;30}3132export class LanguageModelServer implements ILanguageModelServer {33declare _serviceBrand: undefined;3435private server: http.Server;36protected config: ILanguageModelServerConfig;37protected adapterFactories: Map<string, IProtocolAdapterFactory>;38protected readonly requestHandlers = new Map<string, { method: string; handler: (req: http.IncomingMessage, res: http.ServerResponse) => Promise<void> }>();39constructor(40@ILogService private readonly logService: ILogService,41@IEndpointProvider protected readonly endpointProvider: IEndpointProvider42) {43this.config = {44port: 0, // Will be set to random available port45nonce: 'vscode-lm-' + generateUuid()46};47this.adapterFactories = new Map();48this.adapterFactories.set('/v1/messages', new AnthropicAdapterFactory());4950this.server = this.createServer();51}5253private createServer(): http.Server {54return http.createServer(async (req, res) => {55this.logService.trace(`Received request: ${req.method} ${req.url}`);5657if (req.method === 'OPTIONS') {58res.writeHead(200);59res.end();60return;61}6263const handler = this.requestHandlers.get(req.url || '');64if (handler && handler.method === req.method) {65await handler.handler(req, res);66return;67}6869if (req.method === 'POST') {70const adapterFactory = this.getAdapterFactoryForPath(req.url || '');71if (adapterFactory) {72try {73// Create new adapter instance for this request74const adapter = adapterFactory.createAdapter();75const body = await this.readRequestBody(req);7677// Verify nonce for authentication78const authKey = adapter.extractAuthKey(req.headers);79if (authKey !== this.config.nonce) {80this.logService.trace(`[LanguageModelServer] Invalid auth key`);81res.writeHead(401, { 'Content-Type': 'application/json' });82res.end(JSON.stringify({ error: 'Invalid authentication' }));83return;84}8586await this.handleChatRequest(adapter, body, res);87} catch (error) {88res.writeHead(500, { 'Content-Type': 'application/json' });89res.end(JSON.stringify({90error: 'Internal server error',91details: error instanceof Error ? error.message : String(error)92}));93}94return;95}96}9798if (req.method === 'GET' && req.url === '/') {99res.writeHead(200);100res.end('Hello from LanguageModelServer');101return;102}103104if (req.method === 'GET' && req.url === '/models') {105res.writeHead(200, { 'Content-Type': 'application/json' });106res.end(JSON.stringify({ data: [] }));107return;108}109110res.writeHead(404, { 'Content-Type': 'application/json' });111res.end(JSON.stringify({ error: 'Not found' }));112});113}114115private parseUrlPathname(url: string): string {116try {117const parsedUrl = new URL(url, 'http://localhost');118return parsedUrl.pathname;119} catch {120return url.split('?')[0];121}122}123124private getAdapterFactoryForPath(url: string): IProtocolAdapterFactory | undefined {125const pathname = this.parseUrlPathname(url);126return this.adapterFactories.get(pathname);127}128129private async readRequestBody(req: http.IncomingMessage): Promise<string> {130return new Promise((resolve, reject) => {131let body = '';132req.on('data', chunk => {133body += chunk.toString();134});135req.on('end', () => {136resolve(body);137});138req.on('error', reject);139});140}141142private async handleChatRequest(adapter: IProtocolAdapter, body: string, res: http.ServerResponse): Promise<void> {143try {144const parsedRequest = adapter.parseRequest(body);145146const endpoints = await this.endpointProvider.getAllChatEndpoints();147148if (endpoints.length === 0) {149res.writeHead(404, { 'Content-Type': 'application/json' });150res.end(JSON.stringify({ error: 'No language models available' }));151return;152}153154const selectedEndpoint = this.selectEndpoint(endpoints, parsedRequest.model);155if (!selectedEndpoint) {156res.writeHead(404, { 'Content-Type': 'application/json' });157res.end(JSON.stringify({158error: 'No model found matching criteria'159}));160return;161}162163// Set up streaming response164res.writeHead(200, {165'Content-Type': adapter.getContentType(),166'Cache-Control': 'no-cache',167'Connection': 'keep-alive',168});169170// Create cancellation token for the request171const tokenSource = new CancellationTokenSource();172173// Handle client disconnect174let requestComplete = false;175res.on('close', () => {176if (!requestComplete) {177this.logService.info(`[LanguageModelServer] Client disconnected before request complete`);178}179180tokenSource.cancel();181});182183try {184// Create streaming context with only essential shared data185const context: IStreamingContext = {186requestId: `req_${Math.random().toString(36).substr(2, 20)}`,187endpoint: {188modelId: selectedEndpoint.model,189modelMaxPromptTokens: selectedEndpoint.modelMaxPromptTokens190}191};192193// Send initial events if adapter supports them194if (adapter.generateInitialEvents) {195const initialEvents = adapter.generateInitialEvents(context);196for (const event of initialEvents) {197res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);198}199}200201const userInitiatedRequest = parsedRequest.messages.at(-1)?.role === Raw.ChatRole.User;202const fetchResult = await selectedEndpoint.makeChatRequest2({203debugName: 'agentLMServer' + (parsedRequest.type ? `-${parsedRequest.type}` : ''),204messages: parsedRequest.messages as Raw.ChatMessage[],205finishedCb: async (_fullText, _index, delta) => {206if (tokenSource.token.isCancellationRequested) {207return 0; // stop208}209// Emit text deltas210if (delta.text) {211const textData: IAgentStreamBlock = {212type: 'text',213content: delta.text214};215for (const event of adapter.formatStreamResponse(textData, context)) {216res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);217}218}219// Emit tool calls if present220if (delta.copilotToolCalls && delta.copilotToolCalls.length > 0) {221for (const call of delta.copilotToolCalls) {222let input: object = {};223try { input = call.arguments ? JSON.parse(call.arguments) : {}; } catch { input = {}; }224const toolData: IAgentStreamBlock = {225type: 'tool_call',226callId: call.id,227name: call.name,228input229};230for (const event of adapter.formatStreamResponse(toolData, context)) {231res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);232}233}234}235return undefined;236},237location: ChatLocation.Agent,238requestOptions: { ...parsedRequest.options, stream: false },239userInitiatedRequest,240telemetryProperties: {241messageSource: `lmServer-${adapter.name}`242}243}, tokenSource.token);244245// Capture usage information if available246let usage: APIUsage | undefined;247if (fetchResult.type === ChatFetchResponseType.Success && fetchResult.usage) {248usage = fetchResult.usage;249}250251requestComplete = true;252253// Send final events254const finalEvents = adapter.generateFinalEvents(context, usage);255for (const event of finalEvents) {256res.write(`event: ${event.event}\ndata: ${event.data}\n\n`);257}258259res.end();260} catch (error) {261requestComplete = true;262if (error instanceof LanguageModelError) {263res.write(JSON.stringify({264error: 'Language model error',265code: error.code,266message: error.message,267cause: error.cause268}));269} else {270res.write(JSON.stringify({271error: 'Request failed',272message: error instanceof Error ? error.message : String(error)273}));274}275res.end();276} finally {277tokenSource.dispose();278}279280} catch (error) {281res.writeHead(500, { 'Content-Type': 'application/json' });282res.end(JSON.stringify({283error: 'Failed to process chat request',284details: error instanceof Error ? error.message : String(error)285}));286}287}288289private selectEndpoint(endpoints: readonly IChatEndpoint[], requestedModel?: string): IChatEndpoint | undefined {290if (requestedModel) {291// Handle model mapping292let mappedModel = requestedModel;293if (requestedModel.startsWith('claude-haiku')) {294mappedModel = 'claude-haiku-4.5';295}296if (requestedModel.startsWith('claude-sonnet-4')) {297mappedModel = 'claude-sonnet-4.5';298}299if (requestedModel.startsWith('claude-opus-4')) {300mappedModel = 'claude-opus-4.5';301}302303// Try to find exact match first304let selectedEndpoint = endpoints.find(e => e.family === mappedModel || e.model === mappedModel);305306// If not found, try to find by partial match for Anthropic models307if (!selectedEndpoint && requestedModel.startsWith('claude-haiku-4')) {308selectedEndpoint = endpoints.find(e => e.model.includes('claude-haiku-4-5')) ?? endpoints.find(e => e.model.includes('claude'));309} else if (!selectedEndpoint && requestedModel.startsWith('claude-sonnet-4')) {310selectedEndpoint = endpoints.find(e => e.model.includes('claude-sonnet-4-5')) ?? endpoints.find(e => e.model.includes('claude'));311} else if (!selectedEndpoint && requestedModel.startsWith('claude-opus-4')) {312selectedEndpoint = endpoints.find(e => e.model.includes('claude-opus-4-5')) ?? endpoints.find(e => e.model.includes('claude'));313}314315return selectedEndpoint;316}317318// Use first available model if no criteria specified319return endpoints[0];320}321322public async start(): Promise<void> {323return new Promise((resolve) => {324this.server.listen(0, '127.0.0.1', () => {325const address = this.server.address();326if (address && typeof address === 'object') {327this.config = {328...this.config,329port: address.port330};331this.logService.trace(`Language Model Server started on http://localhost:${this.config.port}`);332resolve();333}334});335});336}337338public stop(): void {339this.server.close();340}341342public getConfig(): ILanguageModelServerConfig {343return { ...this.config };344}345}346347348