Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/common/messageFormatters.ts
13401 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
/**
7
* Converts internal message types to OTel GenAI JSON schema format.
8
* @see https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-input-messages.json
9
* @see https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-output-messages.json
10
*/
11
12
/**
13
* Maximum size (in characters) for a single OTel span/log attribute value.
14
* Aligned with common backend limits (Jaeger 64KB, Tempo 100KB).
15
* Matches gemini-cli's approach of capping content to prevent OTLP batch failures.
16
*/
17
const MAX_OTEL_ATTRIBUTE_LENGTH = 64_000;
18
19
/**
20
* Truncate a string to fit within OTel attribute size limits.
21
* Returns the original string if within bounds, otherwise truncates with a suffix.
22
*/
23
export function truncateForOTel(value: string, maxLength: number = MAX_OTEL_ATTRIBUTE_LENGTH): string {
24
if (value.length <= maxLength) {
25
return value;
26
}
27
const suffix = `...[truncated, original ${value.length} chars]`;
28
return value.substring(0, maxLength - suffix.length) + suffix;
29
}
30
31
export interface OTelChatMessage {
32
role: string | undefined;
33
parts: OTelMessagePart[];
34
}
35
36
export interface OTelOutputMessage extends OTelChatMessage {
37
finish_reason?: string;
38
}
39
40
export type OTelMessagePart =
41
| { type: 'text'; content: string }
42
| { type: 'tool_call'; id: string; name: string; arguments: unknown }
43
| { type: 'tool_call_response'; id: string; response: unknown }
44
| { type: 'reasoning'; content: string };
45
46
export type OTelSystemInstruction = Array<{ type: 'text'; content: string }>;
47
48
export interface OTelToolDefinition {
49
type: 'function';
50
name: string;
51
description?: string;
52
parameters?: unknown;
53
}
54
55
/**
56
* Convert an array of internal messages to OTel input message format.
57
* Handles OpenAI format (tool_calls, tool_call_id) natively.
58
*/
59
export function toInputMessages(messages: ReadonlyArray<{ role?: string; content?: string; tool_calls?: ReadonlyArray<{ id: string; function: { name: string; arguments: string } }>; tool_call_id?: string }>): OTelChatMessage[] {
60
return messages.map(msg => {
61
const parts: OTelMessagePart[] = [];
62
63
// OpenAI tool-result message (role=tool): map to tool_call_response
64
if (msg.role === 'tool' && msg.tool_call_id) {
65
parts.push({ type: 'tool_call_response', id: msg.tool_call_id, response: msg.content ?? '' });
66
return { role: msg.role, parts };
67
}
68
69
if (msg.content) {
70
parts.push({ type: 'text', content: msg.content });
71
}
72
73
if (msg.tool_calls) {
74
for (const tc of msg.tool_calls) {
75
let args: unknown;
76
try { args = JSON.parse(tc.function.arguments); } catch { args = tc.function.arguments; }
77
parts.push({
78
type: 'tool_call',
79
id: tc.id,
80
name: tc.function.name,
81
arguments: args,
82
});
83
}
84
}
85
86
return { role: msg.role, parts };
87
});
88
}
89
90
/**
91
* Convert model response choices to OTel output message format.
92
*/
93
export function toOutputMessages(choices: ReadonlyArray<{
94
message?: { role?: string; content?: string; tool_calls?: ReadonlyArray<{ id: string; function: { name: string; arguments: string } }> };
95
finish_reason?: string;
96
}>): OTelOutputMessage[] {
97
return choices.map(choice => {
98
const parts: OTelMessagePart[] = [];
99
const msg = choice.message;
100
101
if (msg?.content) {
102
parts.push({ type: 'text', content: msg.content });
103
}
104
105
if (msg?.tool_calls) {
106
for (const tc of msg.tool_calls) {
107
let args: unknown;
108
try { args = JSON.parse(tc.function.arguments); } catch { args = tc.function.arguments; }
109
parts.push({
110
type: 'tool_call',
111
id: tc.id,
112
name: tc.function.name,
113
arguments: args,
114
});
115
}
116
}
117
118
return {
119
role: msg?.role ?? 'assistant',
120
parts,
121
finish_reason: choice.finish_reason,
122
};
123
});
124
}
125
126
/**
127
* Convert system message to OTel system instruction format.
128
*/
129
export function toSystemInstructions(systemMessage: string | undefined): OTelSystemInstruction | undefined {
130
if (!systemMessage) {
131
return undefined;
132
}
133
return [{ type: 'text', content: systemMessage }];
134
}
135
136
/**
137
* Normalize provider-specific messages (Anthropic content blocks, OpenAI tool messages)
138
* to OTel GenAI semantic convention format.
139
*
140
* Handles:
141
* - Anthropic content block arrays: tool_use → tool_call, tool_result → tool_call_response
142
* - OpenAI format: tool_calls, role=tool with tool_call_id
143
* - Plain string content
144
*/
145
export function normalizeProviderMessages(messages: ReadonlyArray<Record<string, unknown>>): OTelChatMessage[] {
146
return messages.map(msg => {
147
const role = msg.role as string | undefined;
148
const parts: OTelMessagePart[] = [];
149
const content = msg.content;
150
151
// OpenAI tool-result message
152
if (role === 'tool' && typeof msg.tool_call_id === 'string') {
153
parts.push({ type: 'tool_call_response', id: msg.tool_call_id, response: content ?? '' });
154
return { role, parts };
155
}
156
157
if (typeof content === 'string' && content.length > 0) {
158
parts.push({ type: 'text', content });
159
} else if (Array.isArray(content)) {
160
// Anthropic content block array
161
for (const block of content) {
162
if (!block || typeof block !== 'object') { continue; }
163
const b = block as Record<string, unknown>;
164
switch (b.type) {
165
case 'text':
166
if (typeof b.text === 'string') {
167
parts.push({ type: 'text', content: b.text });
168
}
169
break;
170
case 'tool_use':
171
parts.push({
172
type: 'tool_call',
173
id: String(b.id ?? ''),
174
name: String(b.name ?? ''),
175
arguments: b.input,
176
});
177
break;
178
case 'tool_result':
179
parts.push({
180
type: 'tool_call_response',
181
id: String(b.tool_use_id ?? ''),
182
response: b.content ?? '',
183
});
184
break;
185
case 'thinking':
186
if (typeof b.thinking === 'string') {
187
parts.push({ type: 'reasoning', content: b.thinking });
188
}
189
break;
190
default:
191
// Unknown block type — include as text fallback
192
parts.push({ type: 'text', content: JSON.stringify(b) });
193
break;
194
}
195
}
196
}
197
198
// OpenAI tool_calls
199
const toolCalls = msg.tool_calls;
200
if (Array.isArray(toolCalls)) {
201
for (const tc of toolCalls) {
202
if (!tc || typeof tc !== 'object') { continue; }
203
const call = tc as Record<string, unknown>;
204
const fn = call.function as Record<string, unknown> | undefined;
205
if (fn) {
206
let args: unknown;
207
try { args = typeof fn.arguments === 'string' ? JSON.parse(fn.arguments) : fn.arguments; } catch { args = fn.arguments; }
208
parts.push({
209
type: 'tool_call',
210
id: String(call.id ?? ''),
211
name: String(fn.name ?? ''),
212
arguments: args,
213
});
214
}
215
}
216
}
217
218
return { role, parts };
219
});
220
}
221
222
/**
223
* Convert tool definitions to OTel `gen_ai.tool.definitions` format.
224
*
225
* Accepts the variants emitted by the different request bodies/providers:
226
* - OpenAI Chat Completions: `{ type: 'function', function: { name, description, parameters } }`
227
* - OpenAI Responses API: `{ type: 'function', name, description, parameters }`
228
* - Anthropic Messages API: `{ name, description, input_schema }`
229
* - VS Code tool info: `{ name, description, inputSchema }`
230
*
231
* Tools without a name (e.g. OpenAI client-side `tool_search`) are skipped
232
* because OTel `gen_ai.tool.definitions` requires a name per entry.
233
*
234
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-tool-definitions
235
*/
236
export function toToolDefinitions(tools: ReadonlyArray<{
237
type?: string;
238
name?: string;
239
description?: string;
240
parameters?: unknown;
241
input_schema?: unknown;
242
inputSchema?: unknown;
243
function?: { name?: string; description?: string; parameters?: unknown };
244
}> | undefined): OTelToolDefinition[] | undefined {
245
if (!tools || tools.length === 0) {
246
return undefined;
247
}
248
const out: OTelToolDefinition[] = [];
249
for (const t of tools) {
250
const name = t.function?.name ?? t.name;
251
if (!name) {
252
continue;
253
}
254
const description = t.function?.description ?? t.description;
255
const parameters = t.function?.parameters ?? t.parameters ?? t.input_schema ?? t.inputSchema;
256
out.push({
257
type: 'function',
258
name,
259
description,
260
parameters,
261
});
262
}
263
return out.length > 0 ? out : undefined;
264
}
265
266