oh-langfuse 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/metrics-utils.mjs +90 -31
- package/scripts/opencode-langfuse-setup.mjs +158 -30
package/package.json
CHANGED
|
@@ -51,16 +51,93 @@ function ratio(numerator, denominator) {
|
|
|
51
51
|
return numerator / denominator;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function normalizeSkillNames(names) {
|
|
55
|
+
if (!Array.isArray(names)) return [];
|
|
56
|
+
const out = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const raw of names) {
|
|
59
|
+
const name = String(raw || "").trim();
|
|
60
|
+
if (!name || seen.has(name)) continue;
|
|
61
|
+
seen.add(name);
|
|
62
|
+
out.push(name);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapeRegExp(value) {
|
|
68
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectStrings(value, out = []) {
|
|
72
|
+
if (value == null) return out;
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
out.push(value);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
if (typeof value === "number" || typeof value === "boolean") return out;
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
for (const item of value) collectStrings(item, out);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "object") {
|
|
83
|
+
for (const [key, item] of Object.entries(value)) {
|
|
84
|
+
out.push(key);
|
|
85
|
+
collectStrings(item, out);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function detectOpencodeSkillNames(source, knownSkills = []) {
|
|
92
|
+
const skills = normalizeSkillNames(knownSkills);
|
|
93
|
+
if (skills.length === 0) return [];
|
|
94
|
+
const haystack = collectStrings(source).join("\n");
|
|
95
|
+
const found = [];
|
|
96
|
+
for (const skillName of skills) {
|
|
97
|
+
const pattern = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(skillName)}([^A-Za-z0-9_-]|$)`, "i");
|
|
98
|
+
if (pattern.test(haystack)) found.push(skillName);
|
|
99
|
+
}
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pickString(...values) {
|
|
104
|
+
for (const value of values) {
|
|
105
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
106
|
+
}
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function countOpencodeToolActivity(source) {
|
|
111
|
+
const payload = source?.properties ?? source?.body ?? source ?? {};
|
|
112
|
+
const part = payload?.part ?? payload?.properties?.part ?? payload;
|
|
113
|
+
const eventType = pickString(source?.type, payload?.type);
|
|
114
|
+
const partType = pickString(part?.type);
|
|
115
|
+
const toolName = pickString(part?.tool, part?.toolName, part?.name, payload?.tool, payload?.toolName, payload?.name);
|
|
116
|
+
const callId = pickString(part?.callID, part?.callId, payload?.callID, payload?.callId, part?.id);
|
|
117
|
+
const status = pickString(part?.state?.status, part?.status, payload?.state?.status, payload?.status).toLowerCase();
|
|
118
|
+
const hasToolInput = part?.state?.input !== undefined || part?.input !== undefined || payload?.input !== undefined;
|
|
119
|
+
const hasToolOutput = part?.state?.output !== undefined || part?.output !== undefined || payload?.output !== undefined;
|
|
120
|
+
const isToolCall = eventType === "tool_use" || eventType === "tool_result" || partType === "tool" || partType === "tool_use" || Boolean(toolName) || hasToolInput;
|
|
121
|
+
const isToolResult = eventType === "tool_result" || status === "completed" || status === "success" || hasToolOutput;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
toolCallCount: isToolCall ? 1 : 0,
|
|
125
|
+
toolResultCount: isToolCall && isToolResult ? 1 : 0,
|
|
126
|
+
toolCallId: callId || "",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
54
130
|
export function buildInteractionMetadata(options = {}) {
|
|
55
131
|
const source = String(options.source || "unknown");
|
|
56
132
|
const sessionId = String(options.sessionId || options.session_id || "unknown");
|
|
57
133
|
const turnNumber = Number(options.turnNumber ?? options.turn_number ?? 0) || 0;
|
|
58
134
|
const tokenMetrics = normalizeTokenMetrics(options.tokenMetrics);
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
135
|
+
const skillNames = normalizeSkillNames(options.skillNames ?? options.skill_names);
|
|
136
|
+
const skillUseCount = skillNames.length || Number(options.skillUseCount ?? options.skill_use_count ?? 0) || 0;
|
|
137
|
+
const toolCallCount = Math.max(Number(options.toolCallCount ?? options.tool_call_count ?? 0) || 0, skillUseCount);
|
|
138
|
+
const toolResultCount = Math.max(Number(options.toolResultCount ?? options.tool_result_count ?? 0) || 0, skillUseCount);
|
|
62
139
|
|
|
63
|
-
|
|
140
|
+
const metadata = {
|
|
64
141
|
source,
|
|
65
142
|
user_id: String(options.userId || options.user_id || ""),
|
|
66
143
|
session_id: sessionId,
|
|
@@ -83,44 +160,26 @@ export function buildInteractionMetadata(options = {}) {
|
|
|
83
160
|
tokens_per_tool_call: ratio(tokenMetrics.total_tokens, toolCallCount),
|
|
84
161
|
},
|
|
85
162
|
};
|
|
163
|
+
|
|
164
|
+
if (skillNames.length) {
|
|
165
|
+
metadata.skill_names = skillNames;
|
|
166
|
+
metadata.skill_names_json = JSON.stringify(skillNames);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return metadata;
|
|
86
170
|
}
|
|
87
171
|
|
|
88
172
|
export function buildOpencodeMetricAttributes(options = {}) {
|
|
89
173
|
const attrs = options.attributes || {};
|
|
90
174
|
const userId = String(options.userId || attrs["oh.langfuse.user_id"] || attrs["langfuse.user.id"] || "");
|
|
91
|
-
const sessionId = String(attrs["ai.request.headers.x-opencode-session"] || options.sessionId || options.session_id || "unknown");
|
|
92
|
-
const requestId = String(attrs["ai.request.headers.x-opencode-request"] || options.requestId || options.request_id || "unknown");
|
|
93
|
-
const spanId = String(options.spanId || attrs["span.id"] || "unknown");
|
|
94
175
|
const provider = attrs["ai.model.provider"] || attrs["gen_ai.system"] || "";
|
|
95
176
|
const modelId = attrs["ai.model.id"] || attrs["gen_ai.request.model"] || attrs["ai.response.model"] || "";
|
|
96
177
|
const model = provider && modelId ? `${provider}/${modelId}` : provider || modelId || null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
output: attrs["ai.usage.outputTokens"] ?? attrs["ai.usage.completionTokens"] ?? attrs["gen_ai.usage.output_tokens"],
|
|
100
|
-
total: attrs["ai.usage.totalTokens"],
|
|
101
|
-
cacheRead: attrs["ai.usage.cachedInputTokens"] ?? attrs["ai.usage.inputTokenDetails.cacheReadTokens"],
|
|
102
|
-
reasoning: attrs["ai.usage.reasoningTokens"] ?? attrs["ai.usage.outputTokenDetails.reasoningTokens"],
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const out = {
|
|
178
|
+
|
|
179
|
+
return {
|
|
106
180
|
"langfuse.observation.metadata.source": "opencode",
|
|
107
181
|
"langfuse.observation.metadata.user_id": userId,
|
|
108
|
-
"langfuse.observation.metadata.session_id": sessionId,
|
|
109
|
-
"langfuse.observation.metadata.interaction_id": `opencode:${userId || "unknown"}:${sessionId}:${requestId}:${spanId}`,
|
|
110
182
|
"langfuse.observation.metadata.metrics_schema_version": METRICS_SCHEMA_VERSION,
|
|
111
|
-
"langfuse.observation.metadata.interaction_count": 1,
|
|
112
|
-
"langfuse.observation.metadata.user_message_count": 1,
|
|
113
|
-
"langfuse.observation.metadata.assistant_message_count": 1,
|
|
114
|
-
"langfuse.observation.metadata.tool_call_count": 0,
|
|
115
|
-
"langfuse.observation.metadata.tool_result_count": 0,
|
|
116
|
-
"langfuse.observation.metadata.skill_use_count": 0,
|
|
117
|
-
"langfuse.observation.metadata.token_metrics_available": tokenMetrics.token_metrics_available,
|
|
118
183
|
"langfuse.observation.metadata.model": model,
|
|
119
184
|
};
|
|
120
|
-
|
|
121
|
-
for (const key of ["input_tokens", "output_tokens", "total_tokens", "cache_read_tokens", "reasoning_tokens"]) {
|
|
122
|
-
if (tokenMetrics[key] != null) out[`langfuse.observation.metadata.${key}`] = tokenMetrics[key];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return out;
|
|
126
185
|
}
|
|
@@ -218,34 +218,10 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
218
218
|
' const hasModel = attrs["ai.model.id"] || attrs["ai.model.provider"] || attrs["gen_ai.request.model"];',
|
|
219
219
|
' const isTool = attrs["ai.toolCall.name"] || attrs["ai.toolCall.id"];',
|
|
220
220
|
" if (!hasModel || isTool) return false;",
|
|
221
|
-
' if (typeof span.updateName === "function") span.updateName("AI Interaction");',
|
|
222
|
-
' const sessionId = String(attrs["ai.request.headers.x-opencode-session"] || attrs["session.id"] || "unknown");',
|
|
223
|
-
' const requestId = String(attrs["ai.request.headers.x-opencode-request"] || attrs["ai.response.id"] || attrs["ai.operationId"] || "unknown");',
|
|
224
|
-
' const spanId = span.spanContext?.().spanId || attrs["span.id"] || "unknown";',
|
|
225
221
|
' const provider = attrs["ai.model.provider"] || attrs["gen_ai.system"] || "";',
|
|
226
222
|
' const modelId = attrs["ai.model.id"] || attrs["gen_ai.request.model"] || attrs["ai.response.model"] || "";',
|
|
227
223
|
' const model = provider && modelId ? `${provider}/${modelId}` : provider || modelId || undefined;',
|
|
228
|
-
' const inputTokens = metricNumber(attrs["ai.usage.inputTokens"] ?? attrs["ai.usage.promptTokens"] ?? attrs["gen_ai.usage.input_tokens"]);',
|
|
229
|
-
' const outputTokens = metricNumber(attrs["ai.usage.outputTokens"] ?? attrs["ai.usage.completionTokens"] ?? attrs["gen_ai.usage.output_tokens"]);',
|
|
230
|
-
' const totalTokens = metricNumber(attrs["ai.usage.totalTokens"]) ?? (inputTokens !== undefined && outputTokens !== undefined ? inputTokens + outputTokens : undefined);',
|
|
231
|
-
' const cacheReadTokens = metricNumber(attrs["ai.usage.cachedInputTokens"] ?? attrs["ai.usage.inputTokenDetails.cacheReadTokens"]);',
|
|
232
|
-
' const reasoningTokens = metricNumber(attrs["ai.usage.reasoningTokens"] ?? attrs["ai.usage.outputTokenDetails.reasoningTokens"]);',
|
|
233
|
-
" const tokenAvailable = [inputTokens, outputTokens, totalTokens, cacheReadTokens, reasoningTokens].some((value) => value !== undefined);",
|
|
234
|
-
' writeMetric(span, "session_id", sessionId);',
|
|
235
|
-
' writeMetric(span, "interaction_id", `opencode:${userId || "unknown"}:${sessionId}:${requestId}:${spanId}`);',
|
|
236
|
-
' writeMetric(span, "interaction_count", 1);',
|
|
237
|
-
' writeMetric(span, "user_message_count", 1);',
|
|
238
|
-
' writeMetric(span, "assistant_message_count", 1);',
|
|
239
|
-
' writeMetric(span, "token_metrics_available", tokenAvailable);',
|
|
240
|
-
' writeMetric(span, "tool_call_count", 0);',
|
|
241
|
-
' writeMetric(span, "tool_result_count", 0);',
|
|
242
|
-
' writeMetric(span, "skill_use_count", 0);',
|
|
243
224
|
' writeMetric(span, "model", model);',
|
|
244
|
-
' writeMetric(span, "input_tokens", inputTokens);',
|
|
245
|
-
' writeMetric(span, "output_tokens", outputTokens);',
|
|
246
|
-
' writeMetric(span, "total_tokens", totalTokens);',
|
|
247
|
-
' writeMetric(span, "cache_read_tokens", cacheReadTokens);',
|
|
248
|
-
' writeMetric(span, "reasoning_tokens", reasoningTokens);',
|
|
249
225
|
" return true;",
|
|
250
226
|
"};",
|
|
251
227
|
"",
|
|
@@ -271,6 +247,24 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
271
247
|
" }",
|
|
272
248
|
' return "";',
|
|
273
249
|
"};",
|
|
250
|
+
"const countOpencodeToolActivity = (event) => {",
|
|
251
|
+
" const payload = eventPayload(event);",
|
|
252
|
+
" const part = eventPart(event);",
|
|
253
|
+
" const eventType = pickEventString(event?.type, payload?.type);",
|
|
254
|
+
" const partType = pickEventString(part?.type);",
|
|
255
|
+
" const toolName = pickEventString(part?.tool, part?.toolName, part?.name, payload?.tool, payload?.toolName, payload?.name);",
|
|
256
|
+
" const callId = pickEventString(part?.callID, part?.callId, payload?.callID, payload?.callId, part?.id);",
|
|
257
|
+
" const status = pickEventString(part?.state?.status, part?.status, payload?.state?.status, payload?.status).toLowerCase();",
|
|
258
|
+
" const hasToolInput = part?.state?.input !== undefined || part?.input !== undefined || payload?.input !== undefined;",
|
|
259
|
+
" const hasToolOutput = part?.state?.output !== undefined || part?.output !== undefined || payload?.output !== undefined;",
|
|
260
|
+
" const isToolCall = eventType === 'tool_use' || eventType === 'tool_result' || partType === 'tool' || partType === 'tool_use' || Boolean(toolName) || hasToolInput;",
|
|
261
|
+
" const isToolResult = eventType === 'tool_result' || status === 'completed' || status === 'success' || hasToolOutput;",
|
|
262
|
+
" return {",
|
|
263
|
+
" toolCallCount: isToolCall ? 1 : 0,",
|
|
264
|
+
" toolResultCount: isToolCall && isToolResult ? 1 : 0,",
|
|
265
|
+
" toolCallId: callId || '',",
|
|
266
|
+
" };",
|
|
267
|
+
"};",
|
|
274
268
|
"const tokenMetricsFromPart = (part) => {",
|
|
275
269
|
" const tokens = part?.tokens ?? part?.usage ?? {};",
|
|
276
270
|
" return {",
|
|
@@ -281,8 +275,84 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
281
275
|
" reasoning: metricNumber(tokens.reasoning ?? tokens.reasoning_tokens ?? tokens.reasoningTokens),",
|
|
282
276
|
" };",
|
|
283
277
|
"};",
|
|
284
|
-
"",
|
|
285
|
-
"
|
|
278
|
+
"",
|
|
279
|
+
"const normalizeSkillNames = (names) => {",
|
|
280
|
+
" if (!Array.isArray(names)) return [];",
|
|
281
|
+
" const out = [];",
|
|
282
|
+
" const seen = new Set();",
|
|
283
|
+
" for (const raw of names) {",
|
|
284
|
+
" const name = String(raw || '').trim();",
|
|
285
|
+
" if (!name || seen.has(name)) continue;",
|
|
286
|
+
" seen.add(name);",
|
|
287
|
+
" out.push(name);",
|
|
288
|
+
" }",
|
|
289
|
+
" return out;",
|
|
290
|
+
"};",
|
|
291
|
+
"",
|
|
292
|
+
"const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
293
|
+
"",
|
|
294
|
+
"const collectStrings = (value, out = []) => {",
|
|
295
|
+
" if (value == null) return out;",
|
|
296
|
+
" if (typeof value === 'string') {",
|
|
297
|
+
" out.push(value);",
|
|
298
|
+
" return out;",
|
|
299
|
+
" }",
|
|
300
|
+
" if (typeof value === 'number' || typeof value === 'boolean') return out;",
|
|
301
|
+
" if (Array.isArray(value)) {",
|
|
302
|
+
" for (const item of value) collectStrings(item, out);",
|
|
303
|
+
" return out;",
|
|
304
|
+
" }",
|
|
305
|
+
" if (typeof value === 'object') {",
|
|
306
|
+
" for (const [key, item] of Object.entries(value)) {",
|
|
307
|
+
" out.push(key);",
|
|
308
|
+
" collectStrings(item, out);",
|
|
309
|
+
" }",
|
|
310
|
+
" }",
|
|
311
|
+
" return out;",
|
|
312
|
+
"};",
|
|
313
|
+
"",
|
|
314
|
+
"const detectOpencodeSkillNames = (source, knownSkills = []) => {",
|
|
315
|
+
" const skills = normalizeSkillNames(knownSkills);",
|
|
316
|
+
" if (skills.length === 0) return [];",
|
|
317
|
+
" const haystack = collectStrings(source).join('\\n');",
|
|
318
|
+
" const found = [];",
|
|
319
|
+
" for (const skillName of skills) {",
|
|
320
|
+
" const pattern = new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(skillName)}([^A-Za-z0-9_-]|$)`, 'i');",
|
|
321
|
+
" if (pattern.test(haystack)) found.push(skillName);",
|
|
322
|
+
" }",
|
|
323
|
+
" return found;",
|
|
324
|
+
"};",
|
|
325
|
+
"",
|
|
326
|
+
"const collectKnownSkillNames = async () => {",
|
|
327
|
+
" const dirs = [",
|
|
328
|
+
" path.join(process.cwd(), '.opencode', 'skill'),",
|
|
329
|
+
" path.join(process.cwd(), '.opencode', 'skills'),",
|
|
330
|
+
" path.join(os.homedir(), '.config', 'opencode', 'skill'),",
|
|
331
|
+
" path.join(os.homedir(), '.config', 'opencode', 'skills'),",
|
|
332
|
+
" path.join(os.homedir(), '.opencode', 'skill'),",
|
|
333
|
+
" path.join(os.homedir(), '.opencode', 'skills'),",
|
|
334
|
+
" ];",
|
|
335
|
+
" const names = new Set();",
|
|
336
|
+
" for (const dir of dirs) {",
|
|
337
|
+
" try {",
|
|
338
|
+
" const entries = await fs.readdir(dir, { withFileTypes: true });",
|
|
339
|
+
" for (const entry of entries) {",
|
|
340
|
+
" if (!entry.isDirectory() || entry.name.startsWith('.')) continue;",
|
|
341
|
+
" try {",
|
|
342
|
+
" await fs.access(path.join(dir, entry.name, 'SKILL.md'));",
|
|
343
|
+
" names.add(entry.name);",
|
|
344
|
+
" } catch {",
|
|
345
|
+
" // Ignore helper folders that are not OpenCode skills.",
|
|
346
|
+
" }",
|
|
347
|
+
" }",
|
|
348
|
+
" } catch {",
|
|
349
|
+
" // Optional skill directories are allowed to be absent.",
|
|
350
|
+
" }",
|
|
351
|
+
" }",
|
|
352
|
+
" return [...names];",
|
|
353
|
+
"};",
|
|
354
|
+
"",
|
|
355
|
+
"export const LangfusePlugin = async ({ client }) => {",
|
|
286
356
|
" const publicKey = process.env.LANGFUSE_PUBLIC_KEY;",
|
|
287
357
|
" const secretKey = process.env.LANGFUSE_SECRET_KEY;",
|
|
288
358
|
' const baseUrl = process.env.LANGFUSE_BASEURL ?? "https://cloud.langfuse.com";',
|
|
@@ -308,11 +378,19 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
308
378
|
" const sdk = new NodeSDK({ spanProcessors });",
|
|
309
379
|
" sdk.start();",
|
|
310
380
|
" const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
381
|
+
" const knownSkillNames = await collectKnownSkillNames();",
|
|
311
382
|
" const messageTextById = new Map();",
|
|
383
|
+
" const skillNamesByMessageId = new Map();",
|
|
384
|
+
" const skillNamesBySessionId = new Map();",
|
|
385
|
+
" const toolCallIdsByMessageId = new Map();",
|
|
386
|
+
" const toolCallIdsBySessionId = new Map();",
|
|
387
|
+
" const toolResultIdsByMessageId = new Map();",
|
|
388
|
+
" const toolResultIdsBySessionId = new Map();",
|
|
312
389
|
" const emittedMessageIds = new Set();",
|
|
313
390
|
"",
|
|
314
391
|
' log("info", `OTEL tracing initialized -> ${baseUrl}`);',
|
|
315
392
|
' if (userId) log("info", `LANGFUSE userId configured -> ${userId}`);',
|
|
393
|
+
' if (knownSkillNames.length) log("info", `OpenCode skills discovered -> ${knownSkillNames.length}`);',
|
|
316
394
|
"",
|
|
317
395
|
" let shutdownStarted = false;",
|
|
318
396
|
" const flush = async (reason) => {",
|
|
@@ -342,14 +420,51 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
342
420
|
" void shutdown('process.exit').finally(() => originalExit(code));",
|
|
343
421
|
" });",
|
|
344
422
|
"",
|
|
423
|
+
" const rememberSkillNames = (messageId, names) => {",
|
|
424
|
+
" if (!messageId || !names.length) return;",
|
|
425
|
+
" let set = skillNamesByMessageId.get(messageId);",
|
|
426
|
+
" if (!set) {",
|
|
427
|
+
" set = new Set();",
|
|
428
|
+
" skillNamesByMessageId.set(messageId, set);",
|
|
429
|
+
" }",
|
|
430
|
+
" for (const name of names) set.add(name);",
|
|
431
|
+
" };",
|
|
432
|
+
" const rememberSessionSkillNames = (sessionId, names) => {",
|
|
433
|
+
" if (!sessionId || !names.length) return;",
|
|
434
|
+
" let set = skillNamesBySessionId.get(sessionId);",
|
|
435
|
+
" if (!set) {",
|
|
436
|
+
" set = new Set();",
|
|
437
|
+
" skillNamesBySessionId.set(sessionId, set);",
|
|
438
|
+
" }",
|
|
439
|
+
" for (const name of names) set.add(name);",
|
|
440
|
+
" };",
|
|
441
|
+
" const rememberToolActivity = (map, key, activity, kind) => {",
|
|
442
|
+
" if (!key || !activity?.[kind]) return;",
|
|
443
|
+
" let set = map.get(key);",
|
|
444
|
+
" if (!set) {",
|
|
445
|
+
" set = new Set();",
|
|
446
|
+
" map.set(key, set);",
|
|
447
|
+
" }",
|
|
448
|
+
" set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
|
|
449
|
+
" };",
|
|
450
|
+
"",
|
|
345
451
|
" const recordInteractionMetric = (event) => {",
|
|
346
452
|
" const payload = eventPayload(event);",
|
|
347
453
|
" const part = eventPart(event);",
|
|
348
454
|
" const partType = part?.type ?? '';",
|
|
349
455
|
" const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id);",
|
|
350
456
|
" const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id);",
|
|
457
|
+
" const eventSkillNames = detectOpencodeSkillNames([event, payload, part, messageTextById.get(messageId)], knownSkillNames);",
|
|
458
|
+
" rememberSkillNames(messageId, eventSkillNames);",
|
|
459
|
+
" rememberSessionSkillNames(sessionId, eventSkillNames);",
|
|
460
|
+
" const toolActivity = countOpencodeToolActivity(event);",
|
|
461
|
+
" rememberToolActivity(toolCallIdsByMessageId, messageId, toolActivity, 'toolCallCount');",
|
|
462
|
+
" rememberToolActivity(toolCallIdsBySessionId, sessionId, toolActivity, 'toolCallCount');",
|
|
463
|
+
" rememberToolActivity(toolResultIdsByMessageId, messageId, toolActivity, 'toolResultCount');",
|
|
464
|
+
" rememberToolActivity(toolResultIdsBySessionId, sessionId, toolActivity, 'toolResultCount');",
|
|
351
465
|
" if (partType === 'text' && messageId && typeof part.text === 'string') {",
|
|
352
466
|
" messageTextById.set(messageId, part.text);",
|
|
467
|
+
" rememberSkillNames(messageId, detectOpencodeSkillNames(part.text, knownSkillNames));",
|
|
353
468
|
" return;",
|
|
354
469
|
" }",
|
|
355
470
|
" if (partType !== 'step-finish' || !messageId || emittedMessageIds.has(messageId)) return;",
|
|
@@ -359,6 +474,9 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
359
474
|
" const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
|
|
360
475
|
" const span = metricsTracer.startSpan('AI Interaction');",
|
|
361
476
|
" const text = messageTextById.get(messageId) || '';",
|
|
477
|
+
" const skillNames = [...new Set([...(skillNamesByMessageId.get(messageId) ?? []), ...(skillNamesBySessionId.get(sessionId) ?? [])])];",
|
|
478
|
+
" const toolCallCount = Math.max(new Set([...(toolCallIdsByMessageId.get(messageId) ?? []), ...(toolCallIdsBySessionId.get(sessionId) ?? [])]).size, skillNames.length);",
|
|
479
|
+
" const toolResultCount = Math.max(new Set([...(toolResultIdsByMessageId.get(messageId) ?? []), ...(toolResultIdsBySessionId.get(sessionId) ?? [])]).size, skillNames.length);",
|
|
362
480
|
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
363
481
|
' span.setAttribute("oh.langfuse.user_id", userId || "");',
|
|
364
482
|
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.0");',
|
|
@@ -371,9 +489,12 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
371
489
|
' span.setAttribute("langfuse.observation.metadata.user_message_count", 1);',
|
|
372
490
|
' span.setAttribute("langfuse.observation.metadata.assistant_message_count", 1);',
|
|
373
491
|
' span.setAttribute("langfuse.observation.metadata.token_metrics_available", tokenAvailable);',
|
|
374
|
-
' span.setAttribute("langfuse.observation.metadata.tool_call_count",
|
|
375
|
-
' span.setAttribute("langfuse.observation.metadata.tool_result_count",
|
|
376
|
-
' span.setAttribute("langfuse.observation.metadata.skill_use_count",
|
|
492
|
+
' span.setAttribute("langfuse.observation.metadata.tool_call_count", toolCallCount);',
|
|
493
|
+
' span.setAttribute("langfuse.observation.metadata.tool_result_count", toolResultCount);',
|
|
494
|
+
' span.setAttribute("langfuse.observation.metadata.skill_use_count", skillNames.length);',
|
|
495
|
+
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names", skillNames);',
|
|
496
|
+
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names_json", JSON.stringify(skillNames));',
|
|
497
|
+
' if (skillNames.length) span.setAttribute("langfuse.observation.metadata.skill_names_csv", skillNames.join(","));',
|
|
377
498
|
' if (tokenMetrics.input !== undefined) span.setAttribute("langfuse.observation.metadata.input_tokens", tokenMetrics.input);',
|
|
378
499
|
' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
|
|
379
500
|
' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
|
|
@@ -381,6 +502,13 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
381
502
|
' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
|
|
382
503
|
' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
|
|
383
504
|
" span.end();",
|
|
505
|
+
" messageTextById.delete(messageId);",
|
|
506
|
+
" skillNamesByMessageId.delete(messageId);",
|
|
507
|
+
" skillNamesBySessionId.delete(sessionId);",
|
|
508
|
+
" toolCallIdsByMessageId.delete(messageId);",
|
|
509
|
+
" toolCallIdsBySessionId.delete(sessionId);",
|
|
510
|
+
" toolResultIdsByMessageId.delete(messageId);",
|
|
511
|
+
" toolResultIdsBySessionId.delete(sessionId);",
|
|
384
512
|
" };",
|
|
385
513
|
"",
|
|
386
514
|
" return {",
|
|
@@ -391,7 +519,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
391
519
|
" },",
|
|
392
520
|
" event: async ({ event }) => {",
|
|
393
521
|
" const eventType = event?.type ?? '';",
|
|
394
|
-
|
|
522
|
+
" recordInteractionMetric(event);",
|
|
395
523
|
' if (eventType === "session.idle" || eventType === "message.updated" || eventType === "message.part.updated" || eventType === "session.updated") {',
|
|
396
524
|
" await flush(eventType);",
|
|
397
525
|
" }",
|