-
Notifications
You must be signed in to change notification settings - Fork 41
Expand file tree
/
Copy pathformatting.ts
More file actions
194 lines (183 loc) · 6.96 KB
/
Copy pathformatting.ts
File metadata and controls
194 lines (183 loc) · 6.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
* Formatting helpers for converting Mistral API payloads to OTEL GenAI convention formats.
*
* These are pure functions with no OTEL dependencies - they transform objects to objects
* matching the GenAI semantic convention schemas for input/output messages and tool definitions.
* The caller is responsible for the final JSON serialization (single JSON.stringify on the whole
* collection) before setting span attributes.
*
* Schemas:
* - Input messages: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-input-messages.json
* - Output messages: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-output-messages.json
* - Tool definitions: https://github.com/Cirilla-zmh/semantic-conventions/blob/cc4d07e7e56b80e9aa5904a3d524c134699da37f/docs/gen-ai/gen-ai-tool-definitions.json
*/
/**
* Convert Mistral message content to OTEL parts array.
*
* Mistral content is either a string or an array of content chunks.
*/
function contentToParts(content: unknown): Array<Record<string, unknown>> {
if (content == null) {
return [];
}
if (typeof content === "string") {
return [{ type: "text", content }];
}
if (!Array.isArray(content)) {
return [];
}
// Content chunks array - map known Mistral types to OTEL part types
const parts: Array<Record<string, unknown>> = [];
for (const chunk of content) {
if (typeof chunk === "string") {
parts.push({ type: "text", content: chunk });
} else if (typeof chunk === "object" && chunk !== null) {
const c = chunk as Record<string, unknown>;
const chunkType = (c["type"] as string) || "";
if (chunkType === "text") {
parts.push({ type: "text", content: c["text"] || "" });
} else if (chunkType === "thinking") {
const thinking = c["thinking"];
let contentStr: string;
if (Array.isArray(thinking)) {
const textParts = thinking
.filter(
(sub): sub is Record<string, unknown> =>
typeof sub === "object" && sub !== null && (sub as Record<string, unknown>)["type"] === "text"
)
.map((sub) => (sub["text"] as string) || "");
contentStr = textParts.join("\n");
} else {
contentStr = String(thinking || "");
}
parts.push({ type: "reasoning", content: contentStr });
} else if (chunkType === "image_url") {
const url = c["image_url"];
const uri =
typeof url === "object" && url !== null
? ((url as Record<string, unknown>)["url"] as string) || ""
: String(url || "");
parts.push({ type: "uri", modality: "image", uri });
} else {
// Catch-all for other content chunk types
parts.push({ type: chunkType });
}
}
}
return parts;
}
/**
* Convert Mistral tool_calls to OTEL ToolCallRequestPart entries.
*/
function toolCallsToParts(toolCalls: unknown): Array<Record<string, unknown>> {
if (!toolCalls || !Array.isArray(toolCalls)) {
return [];
}
const parts: Array<Record<string, unknown>> = [];
for (const tc of toolCalls) {
if (typeof tc !== "object" || tc === null) continue;
const tcObj = tc as Record<string, unknown>;
const func = (tcObj["function"] as Record<string, unknown>) || {};
const part: Record<string, unknown> = {
type: "tool_call",
name: func["name"] || "",
};
const tcId = tcObj["id"];
if (tcId != null) {
part["id"] = tcId;
}
const args = func["arguments"];
if (args != null) {
part["arguments"] = args;
}
parts.push(part);
}
return parts;
}
/**
* Format a single input message per the OTEL GenAI convention.
*
* Schema: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-input-messages.json
* ChatMessage: {role (required), parts (required), name?}
*
* Conversation entry objects (e.g. function.result) don't carry a "role"
* field - they are detected via their "type" and mapped to the closest OTEL role.
*/
export function formatInputMessage(message: Record<string, unknown>): Record<string, unknown> {
const entryType = message["type"];
// Conversation entry: function.result to OTEL tool role
if (entryType === "function.result") {
const part: Record<string, unknown> = { type: "tool_call_response", response: message["result"] };
const toolCallId = message["tool_call_id"];
if (toolCallId != null) {
part["id"] = toolCallId;
}
return { role: "tool", parts: [part] };
}
// TODO: may need to handle other types for conversations (e.g. agent handoff)
const role = (message["role"] as string) || "unknown";
const parts: Array<Record<string, unknown>> = [];
if (role === "tool") {
// Tool messages are responses to tool calls
const toolPart: Record<string, unknown> = {
type: "tool_call_response",
response: message["content"],
};
const toolCallId = message["tool_call_id"];
if (toolCallId != null) {
toolPart["id"] = toolCallId;
}
parts.push(toolPart);
} else {
parts.push(...contentToParts(message["content"]));
parts.push(...toolCallsToParts(message["tool_calls"]));
}
return { role, parts };
}
/**
* Format a single output choice/message per the OTEL GenAI convention.
*
* Schema: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-output-messages.json
* OutputMessage: {role (required), parts (required), finish_reason (required), name?}
*/
export function formatOutputMessage(choice: Record<string, unknown>): Record<string, unknown> {
const message = (choice["message"] as Record<string, unknown>) || {};
const parts: Array<Record<string, unknown>> = [];
parts.push(...contentToParts(message["content"]));
parts.push(...toolCallsToParts(message["tool_calls"]));
return {
role: message["role"] || "assistant",
parts,
finish_reason: choice["finish_reason"] || "",
};
}
/**
* Flatten a Mistral tool definition to the OTEL GenAI convention schema.
*
* Mistral format: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
* OTEL format: {"type": "function", "name": ..., "description": ..., "parameters": ...}
*
* Schema, still under review: https://github.com/open-telemetry/semantic-conventions
*/
export function formatToolDefinition(tool: Record<string, unknown>): Record<string, unknown> | null {
// Early exit conditions: only functions supported for now, and name is required
const type = (tool["type"] as string) || "function";
const func = tool["function"] as Record<string, unknown> | undefined;
if (!func) {
return null;
}
const name = func["name"];
if (!name) {
return null;
}
const formatted: Record<string, unknown> = { type, name };
const description = func["description"];
if (description != null) {
formatted["description"] = description;
}
const parameters = func["parameters"];
if (parameters != null) {
formatted["parameters"] = parameters;
}
return formatted;
}