src/vs/platform/agentHost/node/copilot/byokOpenAiTranslation.ts

241 LOC · 229 covered · 12 uncovered · 52 ranges · 948 concepts · 14 introducers · 441 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- byokOpenAiTranslation.ts ×9
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 {
7 > IByokLmChatMessage,
8 > IByokLmChatRequest,
9 > IByokLmChatResult,
10 > IByokLmTool,
11 > IByokLmToolCall,
12 > } from '../../common/agentHostByokLm.js';
13 >
14 > /**
15 > * Minimal subset of the OpenAI Chat Completions wire format the Copilot SDK
16 > * runtime emits for a `type: 'openai'`, `wireApi: 'completions'` provider
17 > * (verified against the runtime's `chat_completion_transport.rs`, which POSTs
18 > * to `{baseUrl}/chat/completions`). Only the fields this proxy understands are
19 > * modeled; unknown fields are ignored.
20 > */
21 >
22 > interface IOpenAiTextContentPart {
23 > readonly type: 'text';
24 > readonly text: string;
25 > }
26 >
27 > type IOpenAiContentPart = IOpenAiTextContentPart | { readonly type: string;[k: string]: unknown };
28 >
29 > interface IOpenAiToolCall {
30 > readonly id?: string;
31 > readonly type?: string;
32 > readonly function?: {
33 > readonly name?: string;
34 > readonly arguments?: string;
35 > };
36 > }
37 >
38 > interface IOpenAiRequestMessage {
39 > readonly role?: string;
40 > readonly content?: string | IOpenAiContentPart[] | null;
41 > readonly tool_calls?: IOpenAiToolCall[];
42 > readonly tool_call_id?: string;
43 > }
44 >
45 > interface IOpenAiToolDefinition {
46 > readonly type?: string;
47 > readonly function?: {
48 > readonly name?: string;
49 > readonly description?: string;
50 > readonly parameters?: object;
51 > };
52 > }
53 >
54 > export interface IOpenAiChatRequest {
55 > readonly model?: string;
56 > readonly messages?: IOpenAiRequestMessage[];
57 > readonly tools?: IOpenAiToolDefinition[];
58 > readonly stream?: boolean;
59 > readonly temperature?: number;
60 > readonly top_p?: number;
61 > readonly max_tokens?: number;
62 > readonly [k: string]: unknown;
63 > }
64 >
65 > /** Thrown when the inbound body cannot be mapped to a bridge request. */
66 > export class OpenAiTranslationError extends Error { }
67 >
68 > function flattenContent(content: string | IOpenAiContentPart[] | null | undefined): string { byokOpenAiTranslation.ts ×8
69 > if (typeof content === 'string') {
70 > return content;
71 > }
72 > if (Array.isArray(content)) { byokOpenAiTranslation.ts ×9
73 > let out = '';
74 > for (const part of content) {
75 > if (part && part.type === 'text' && typeof (part as IOpenAiTextContentPart).text === 'string') {
76 > out += (part as IOpenAiTextContentPart).text;
77 > }
78 > }
79 > return out;
80 > }
81 return '';
82 }
84 > function toBridgeRole(role: string | undefined): IByokLmChatMessage['role'] { byokOpenAiTranslation.ts ×8
85 > switch (role) {
86 > case 'system':
87 > case 'developer':
88 > return 'system'; byokOpenAiTranslation.ts ×9
89 > case 'assistant': byokOpenAiTranslation.ts ×8
90 > return 'assistant'; byokOpenAiTranslation.ts ×3
91 > case 'tool': byokOpenAiTranslation.ts ×8
92 > case 'function':
93 > return 'tool'; byokOpenAiTranslation.ts ×9
94 > case 'user': byokOpenAiTranslation.ts ×8
95 > default:
96 > return 'user'; byokOpenAiTranslation.ts ×2
98 > }
100 > function toBridgeToolCalls(toolCalls: IOpenAiToolCall[] | undefined): IByokLmToolCall[] | undefined { byokOpenAiTranslation.ts ×8
101 > if (!toolCalls || toolCalls.length === 0) {
102 > return undefined; byokOpenAiTranslation.ts ×2
103 > }
104 > const mapped: IByokLmToolCall[] = []; byokOpenAiTranslation.ts ×3
105 > for (let i = 0; i < toolCalls.length; i++) {
106 > const call = toolCalls[i];
107 > const name = call.function?.name;
108 > if (!name) {
109 > // A tool call without a function name is malformed: reject at the byokOpenAiTranslation.ts ×1
110 > // boundary (→ 400) rather than forwarding an invalid `tool_use` part
111 > // that would fail later, deeper in the renderer.
112 > throw new OpenAiTranslationError(`tool_calls[${i}].function.name is required`);
113 > }
114 > mapped.push({ byokOpenAiTranslation.ts ×9
115 > id: call.id ?? `call_${i}`,
117 > argumentsJson: call.function?.arguments ?? '{}',
118 > });
119 > }
120 > return mapped; byokOpenAiTranslation.ts ×9
121 > }
123 > function toBridgeTools(tools: IOpenAiToolDefinition[] | undefined): IByokLmTool[] | undefined { byokOpenAiTranslation.ts ×5
124 > if (!tools || tools.length === 0) {
125 > return undefined; byokOpenAiTranslation.ts ×1
126 > }
127 > const mapped: IByokLmTool[] = []; byokOpenAiTranslation.ts ×9
128 > for (const tool of tools) {
129 > const fn = tool.function;
130 > if (!fn?.name) {
131 continue;
132 }
133 > mapped.push({ byokOpenAiTranslation.ts ×9
134 > name: fn.name,
135 > description: fn.description,
136 > parametersSchema: fn.parameters,
137 > });
138 > }
139 > return mapped.length ? mapped : undefined; byokOpenAiTranslation.ts ×5
140 > }
142 > /**
143 > * Convert a parsed OpenAI Chat Completions request into the serializable
144 > * bridge request. `vendor` is the synthesized provider name the runtime used
145 > * (it is not present in the OpenAI body); `model` becomes the provider-local
146 > * wire model id resolved on the renderer.
147 > */
148 > export function openAiRequestToBridge(vendor: string, body: IOpenAiChatRequest): IByokLmChatRequest {
149 > const model = typeof body.model === 'string' ? body.model : ''; byokOpenAiTranslation.ts ×4
150 > if (!model) {
151 > throw new OpenAiTranslationError('Request is missing the "model" field'); byokOpenAiTranslation.ts ×1
152 > }
153 > const sourceMessages = Array.isArray(body.messages) ? body.messages : []; byokOpenAiTranslation.ts ×4
154 > const messages: IByokLmChatMessage[] = sourceMessages.map(message => ({
155 > role: toBridgeRole(message.role), byokOpenAiTranslation.ts ×8
156 > content: flattenContent(message.content),
157 > toolCalls: toBridgeToolCalls(message.tool_calls),
158 > toolCallId: message.tool_call_id,
160 >
161 > const modelOptions: Record<string, unknown> = {};
162 > if (typeof body.temperature === 'number') {
163 > modelOptions.temperature = body.temperature; byokOpenAiTranslation.ts ×9
164 > }
165 > if (typeof body.top_p === 'number') { byokOpenAiTranslation.ts ×5
166 modelOptions.top_p = body.top_p;
167 }
168 > if (typeof body.max_tokens === 'number') { byokOpenAiTranslation.ts ×5
169 > modelOptions.max_tokens = body.max_tokens; byokOpenAiTranslation.ts ×9
170 > }
172 > return {
173 > vendor,
174 > modelId: model,
175 > messages,
176 > tools: toBridgeTools(body.tools),
177 > modelOptions: Object.keys(modelOptions).length ? modelOptions : undefined, byokOpenAiTranslation.ts ×4
178 > };
179 > }
181 > let chunkCounter = 0;
182 >
183 > function nextCompletionId(): string { byokOpenAiTranslation.ts ×6
184 > chunkCounter = (chunkCounter + 1) % Number.MAX_SAFE_INTEGER;
185 > return `chatcmpl-byok-${Date.now().toString(36)}-${chunkCounter.toString(36)}`;
186 > }
188 > /** Serialize a single SSE `data:` frame. */
189 > function sseFrame(payload: unknown): string { byokOpenAiTranslation.ts ×6
190 > return `data: ${JSON.stringify(payload)}\n\n`;
191 > }
193 > /**
194 > * Encode a buffered {@link IByokLmChatResult} as a sequence of OpenAI
195 > * `chat.completion.chunk` SSE frames terminated by `data: [DONE]`.
196 > *
197 > * The whole completion is emitted in one content delta (Stage 1 is
198 > * non-streaming end-to-end); the runtime's SSE parser accepts this shape.
199 > */
200 > export function bridgeResultToSseFrames(result: IByokLmChatResult, model: string): string[] {
201 > const id = nextCompletionId(); byokOpenAiTranslation.ts ×6
202 > const created = Math.floor(Date.now() / 1000);
203 > const base = { id, object: 'chat.completion.chunk', created, model };
204 > const frames: string[] = [];
205 >
206 > // Role delta first, matching the OpenAI streaming contract.
207 > frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }] }));
208 >
209 > if (result.content) {
210 > frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { content: result.content }, finish_reason: null }] })); byokOpenAiTranslation.ts ×1
211 > }
213 > let finishReason: 'stop' | 'tool_calls' = 'stop';
214 > if (result.toolCalls && result.toolCalls.length > 0) {
215 > finishReason = 'tool_calls'; byokOpenAiTranslation.ts ×1
216 > const toolCallsDelta = result.toolCalls.map((call, index) => ({
217 > index,
218 > id: call.id,
219 > type: 'function',
220 > function: { name: call.name, arguments: call.argumentsJson },
221 > }));
222 > frames.push(sseFrame({ ...base, choices: [{ index: 0, delta: { tool_calls: toolCallsDelta }, finish_reason: null }] }));
223 > }
225 > const finalChunk: Record<string, unknown> = { ...base, choices: [{ index: 0, delta: {}, finish_reason: finishReason }] };
226 > if (result.usage) {
227 finalChunk.usage = {
228 prompt_tokens: result.usage.promptTokens ?? 0,
229 completion_tokens: result.usage.completionTokens ?? 0,
230 total_tokens: (result.usage.promptTokens ?? 0) + (result.usage.completionTokens ?? 0),
231 };
232 }
233 > frames.push(sseFrame(finalChunk)); byokOpenAiTranslation.ts ×6
234 > frames.push('data: [DONE]\n\n');
235 > return frames;
236 > }
238 > /** Build an OpenAI-style error envelope body. */
239 > export function openAiErrorBody(message: string, type = 'api_error'): string {
240 > return JSON.stringify({ error: { message, type } }); byokLmProxyService.ts ×2
241 > }