Skip to content

Commit b2c52b3

Browse files
committed
WebSocket request/response headers
1 parent 5a910dc commit b2c52b3

16 files changed

Lines changed: 236 additions & 58 deletions

File tree

chat-lib/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chat-lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"dependencies": {
1717
"@microsoft/tiktokenizer": "^1.0.10",
1818
"@sinclair/typebox": "^0.34.41",
19-
"@vscode/copilot-api": "^0.2.12",
19+
"@vscode/copilot-api": "0.2.12-websocket.1",
2020
"@vscode/l10n": "^0.0.18",
2121
"@vscode/prompt-tsx": "^0.4.0-alpha.6",
2222
"@vscode/tree-sitter-wasm": "0.0.5-php.2",

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5939,7 +5939,7 @@
59395939
"@microsoft/tiktokenizer": "^1.0.10",
59405940
"@modelcontextprotocol/sdk": "^1.25.2",
59415941
"@sinclair/typebox": "^0.34.41",
5942-
"@vscode/copilot-api": "^0.2.12",
5942+
"@vscode/copilot-api": "0.2.12-websocket.1",
59435943
"@vscode/extension-telemetry": "^1.5.0",
59445944
"@vscode/l10n": "^0.0.18",
59455945
"@vscode/prompt-tsx": "^0.4.0-alpha.6",

src/extension/mcp/test/vscode-node/util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as fs from 'fs/promises';
77
import path from 'path';
8-
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response } from '../../../../platform/networking/common/fetcherService';
8+
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response, WebSocketConnection } from '../../../../platform/networking/common/fetcherService';
99
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
1010
import { Event } from '../../../../util/vs/base/common/event';
1111
import { ICommandExecutor } from '../../vscode-node/util';
@@ -104,6 +104,7 @@ export class FixtureFetcherService implements IFetcherService {
104104
_serviceBrand: undefined;
105105
readonly onDidFetch = Event.None;
106106
getUserAgentLibrary(): string { throw new Error('Method not implemented.'); }
107+
createWebSocket(_url: string): WebSocketConnection { throw new Error('Method not implemented.'); }
107108
disconnectAll(): Promise<unknown> { throw new Error('Method not implemented.'); }
108109
makeAbortController(): IAbortController { throw new Error('Method not implemented.'); }
109110
isAbortError(e: any): boolean { throw new Error('Method not implemented.'); }

src/extension/prompt/node/chatMLFetcher.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,9 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
764764
turnId,
765765
conversationId,
766766
cancellationToken,
767+
userInitiatedRequest,
767768
telemetryProperties,
769+
requestKindOptions,
768770
);
769771
}
770772

@@ -801,9 +803,33 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
801803
turnId: string,
802804
conversationId: string,
803805
cancellationToken: CancellationToken,
806+
userInitiatedRequest: boolean | undefined,
804807
telemetryProperties: TelemetryProperties | undefined,
808+
requestKindOptions: IBackgroundRequestOptions | ISubagentRequestOptions | undefined,
805809
): Promise<{ result: ChatResults | ChatRequestFailed | ChatRequestCanceled }> {
806-
const connection = await this._webSocketManager.getOrCreateConnection(conversationId, turnId, secretKey);
810+
const intent = locationToIntent(location);
811+
const agentInteractionType = requestKindOptions?.kind === 'subagent' ?
812+
'conversation-subagent' :
813+
requestKindOptions?.kind === 'background' ?
814+
'conversation-background' :
815+
intent === 'conversation-agent' ? intent : undefined;
816+
const additionalHeaders: Record<string, string> = {
817+
'Authorization': `Bearer ${secretKey}`,
818+
'X-Request-Id': ourRequestId,
819+
'OpenAI-Intent': intent,
820+
'X-GitHub-Api-Version': '2025-05-01',
821+
'X-Interaction-Id': this._interactionService.interactionId,
822+
'X-Initiator': userInitiatedRequest ? 'user' : 'agent',
823+
...(chatEndpointInfo.getExtraHeaders ? chatEndpointInfo.getExtraHeaders(location) : {}),
824+
};
825+
if (agentInteractionType) {
826+
additionalHeaders['X-Interaction-Type'] = agentInteractionType;
827+
additionalHeaders['X-Agent-Task-Id'] = ourRequestId;
828+
}
829+
if (request.messages?.some((m: CAPIChatMessage) => Array.isArray(m.content) ? m.content.some(c => 'image_url' in c) : false) && chatEndpointInfo.supportsVision) {
830+
additionalHeaders['Copilot-Vision-Request'] = 'true';
831+
}
832+
const connection = await this._webSocketManager.getOrCreateConnection(conversationId, turnId, additionalHeaders);
807833

808834
// Generate unique ID to link input and output messages
809835
const modelCallId = generateUuid();
@@ -818,6 +844,9 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
818844
maxTokenWindow: chatEndpointInfo.modelMaxPromptTokens
819845
});
820846

847+
const modelRequestId = getRequestId(connection.responseHeaders);
848+
telemetryData.extendWithRequestId(modelRequestId);
849+
821850
for (const [key, value] of Object.entries(request)) {
822851
if (key === 'messages' || key === 'input') {
823852
continue;
@@ -832,7 +861,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
832861
const handle = connection.sendRequest(request as Record<string, unknown>, cancellationToken);
833862

834863
const extendedBaseTelemetryData = baseTelemetryData.extendedBy({ modelCallId });
835-
const processor = this._instantiationService.createInstance(OpenAIResponsesProcessor, extendedBaseTelemetryData, ourRequestId, '');
864+
const processor = this._instantiationService.createInstance(OpenAIResponsesProcessor, extendedBaseTelemetryData, modelRequestId.headerRequestId, modelRequestId.gitHubRequestId);
836865

837866
const chatCompletions = new AsyncIterableObject<ChatCompletion>(async emitter => {
838867
try {
@@ -846,6 +875,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
846875
});
847876

848877
handle.onError(error => {
878+
(error as any).gitHubRequestId = modelRequestId.gitHubRequestId;
849879
if (isCancellationError(error)) {
850880
reject(error);
851881
return;

src/lib/node/chatLibMain.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ import { TestLanguageDiagnosticsService } from '../../platform/languages/common/
9696
import { ConsoleLog, ILogService, LogLevel as InternalLogLevel, LogServiceImpl } from '../../platform/log/common/logService';
9797
import { ICompletionsFetchService } from '../../platform/nesFetch/common/completionsFetchService';
9898
import { CompletionsFetchService } from '../../platform/nesFetch/node/completionsFetchServiceImpl';
99-
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions } from '../../platform/networking/common/fetcherService';
99+
import { FetchOptions, HeadersImpl, IAbortController, IFetcherService, PaginationOptions, WebSocketConnection, WebSocketConnectOptions } from '../../platform/networking/common/fetcherService';
100100
import { IFetcher } from '../../platform/networking/common/networking';
101101
import { IChatWebSocketManager, NullChatWebSocketManager } from '../../platform/networking/node/chatWebSocketManager';
102102
import { IProxyModelsService } from '../../platform/proxyModels/common/proxyModelsService';
@@ -479,6 +479,9 @@ class SingleFetcherService implements IFetcherService {
479479
fetch(url: string, options: FetchOptions) {
480480
return this._fetcher.fetch(url, options);
481481
}
482+
createWebSocket(url: string, options?: WebSocketConnectOptions): WebSocketConnection {
483+
return { webSocket: new WebSocket(url, options), responseHeaders: new HeadersImpl({}) };
484+
}
482485
disconnectAll(): Promise<unknown> {
483486
return this._fetcher.disconnectAll();
484487
}

src/platform/authentication/test/node/copilotToken.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IDomainService } from '../../../endpoint/common/domainService';
1313
import { IEnvService } from '../../../env/common/envService';
1414
import { NullBaseOctoKitService } from '../../../github/common/nullOctokitServiceImpl';
1515
import { ILogService } from '../../../log/common/logService';
16-
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response } from '../../../networking/common/fetcherService';
16+
import { FetchOptions, IAbortController, IFetcherService, PaginationOptions, Response, WebSocketConnection } from '../../../networking/common/fetcherService';
1717
import { ITelemetryService } from '../../../telemetry/common/telemetry';
1818
import { createFakeResponse } from '../../../test/node/fetcher';
1919
import { createPlatformServices, ITestingServicesAccessor } from '../../../test/node/services';
@@ -642,6 +642,9 @@ class StaticFetcherService implements IFetcherService {
642642
}
643643
return createFakeResponse(404, '');
644644
}
645+
createWebSocket(_url: string): WebSocketConnection {
646+
throw new Error('Method not implemented.');
647+
}
645648
disconnectAll(): Promise<unknown> {
646649
throw new Error('Method not implemented.');
647650
}

src/platform/endpoint/node/test/routerDecisionFetcher.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
import { beforeEach, describe, expect, it, vi } from 'vitest';
77
import { Event } from '../../../../util/vs/base/common/event';
8+
import { IAuthenticationService } from '../../../authentication/common/authentication';
89
import { ConfigKey, IConfigurationService } from '../../../configuration/common/configurationService';
910
import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService';
1011
import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';
1112
import { ILogService } from '../../../log/common/logService';
12-
import { IAbortController, IFetcherService, PaginationOptions } from '../../../networking/common/fetcherService';
13+
import { IAbortController, IFetcherService, PaginationOptions, WebSocketConnection } from '../../../networking/common/fetcherService';
1314
import { IExperimentationService, NullExperimentationService } from '../../../telemetry/common/nullExperimentationService';
1415
import { NullTelemetryService } from '../../../telemetry/common/nullTelemetryService';
1516
import { ITelemetryService } from '../../../telemetry/common/telemetry';
1617
import { createFakeResponse } from '../../../test/node/fetcher';
17-
import { IAuthenticationService } from '../../../authentication/common/authentication';
1818
import { RouterDecisionFetcher } from '../routerDecisionFetcher';
1919

2020
const createValidRouterResponse = (chosenModel = 'gpt-4o') => ({
@@ -45,6 +45,9 @@ describe('RouterDecisionFetcher', () => {
4545
_serviceBrand: undefined,
4646
onDidFetch: Event.None,
4747
fetch: mockFetch,
48+
createWebSocket(_url: string): WebSocketConnection {
49+
throw new Error('Method not implemented.');
50+
},
4851
fetchWithPagination<T>(_baseUrl: string, _options: PaginationOptions<T>): Promise<T[]> {
4952
throw new Error('Method not implemented.');
5053
},

src/platform/networking/common/fetcherService.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IFetcherService {
1313
readonly onDidFetch: Event<FetchEvent>;
1414
getUserAgentLibrary(): string;
1515
fetch(url: string, options: FetchOptions): Promise<Response>;
16+
createWebSocket(url: string, options?: WebSocketConnectOptions): WebSocketConnection;
1617
disconnectAll(): Promise<unknown>;
1718
makeAbortController(): IAbortController;
1819
isAbortError(e: any): boolean;
@@ -163,6 +164,15 @@ export interface PaginationOptions<T> extends FetchOptions {
163164
buildUrl: (baseUrl: string, pageSize: number, page: number) => string;
164165
}
165166

167+
export interface WebSocketConnectOptions {
168+
headers?: { [name: string]: string };
169+
}
170+
171+
export interface WebSocketConnection {
172+
readonly webSocket: WebSocket;
173+
readonly responseHeaders: IHeaders;
174+
}
175+
166176
export interface IAbortSignal {
167177
readonly aborted: boolean;
168178
addEventListener(type: 'abort', listener: (this: AbortSignal) => void): void;
@@ -178,6 +188,33 @@ export interface IHeaders extends Iterable<[string, string]> {
178188
get(name: string): string | null;
179189
}
180190

191+
export class HeadersImpl implements IHeaders {
192+
constructor(private readonly _record: Readonly<Record<string, string | string[] | undefined>>) { }
193+
194+
static fromMap(map: ReadonlyMap<string, string>): HeadersImpl {
195+
return new HeadersImpl(Object.fromEntries(map));
196+
}
197+
198+
get(name: string): string | null {
199+
const result = this._record[name];
200+
return Array.isArray(result) ? result[0] : result ?? null;
201+
}
202+
203+
[Symbol.iterator](): Iterator<[string, string]> {
204+
const keys = Object.keys(this._record);
205+
let index = 0;
206+
return {
207+
next: (): IteratorResult<[string, string]> => {
208+
if (index >= keys.length) {
209+
return { done: true, value: undefined };
210+
}
211+
const key = keys[index++];
212+
return { done: false, value: [key, this.get(key)!] };
213+
}
214+
};
215+
}
216+
}
217+
181218
/**
182219
* Wraps a ReadableStream to allow cancellation even while a `for await` loop
183220
* holds the stream locked. Use `destroy()` to safely cancel from an external

0 commit comments

Comments
 (0)