@narumitw/pi-langfuse 0.18.0 → 0.26.0

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/src/langfuse.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  ExtensionAPI,
3
3
  ExtensionCommandContext,
4
+ ExtensionContext,
4
5
  } from "@earendil-works/pi-coding-agent";
5
6
  import {
6
7
  DEFAULT_BASE_URL,
@@ -10,20 +11,24 @@ import {
10
11
  normalizeLangfuseConfig,
11
12
  writeLangfuseConfig,
12
13
  } from "./config.js";
13
- import { type TraceBackend, TraceRecorder } from "./tracing.js";
14
+ import {
15
+ type ContextSnapshot,
16
+ type GitMetadata,
17
+ type TraceBackend,
18
+ TraceRecorder,
19
+ } from "./tracing.js";
14
20
 
15
21
  interface ExtensionDependencies {
16
22
  loadConfig(path?: string): Promise<LangfuseConfigResult>;
17
23
  writeConfig(config: LangfuseConfig, path?: string): Promise<LangfuseConfig>;
18
24
  createBackend(config: LangfuseConfig): Promise<TraceBackend>;
25
+ resolveGitMetadata(cwd: string): Promise<GitMetadata | undefined>;
19
26
  }
20
27
 
21
- const COMMAND_COMPLETIONS = [
22
- { value: "status", label: "status", description: "Show Langfuse tracing status" },
23
- { value: "flush", label: "flush", description: "Export all completed traces now" },
24
- { value: "help", label: "help", description: "Show Langfuse command help" },
25
- { value: "init", label: "init", description: "Interactively create or update config" },
26
- ];
28
+ const FLUSH_ACTION = "Flush completed traces for this session";
29
+ const SET_UP_ACTION = "Set up Langfuse for this Pi agent directory (restart required)";
30
+ const UPDATE_ACTION = "Update Langfuse for this Pi agent directory (restart required)";
31
+ const HELP_ACTION = "Show setup and privacy help";
27
32
 
28
33
  export function createLangfuseExtension(
29
34
  dependencies: Partial<ExtensionDependencies> = {},
@@ -38,97 +43,97 @@ export function createLangfuseExtension(
38
43
  });
39
44
 
40
45
  return function langfuse(pi: ExtensionAPI) {
46
+ const resolveGit =
47
+ dependencies.resolveGitMetadata ??
48
+ ((cwd: string) =>
49
+ resolveGitMetadata((command, args, options) => pi.exec(command, args, options), cwd));
41
50
  let recorder: TraceRecorder | undefined;
42
51
  let activeConfig: LangfuseConfig | undefined;
43
52
  let configPath: string | undefined;
44
53
  let initializationError: string | undefined;
54
+ let hasStoredConfig = false;
55
+ let configurationNotice: string | undefined;
56
+ let sessionGeneration = 0;
57
+ let nextAttemptReason: string | undefined;
45
58
 
46
59
  pi.registerCommand("langfuse", {
47
- description: "Manage Langfuse tracing and configuration",
48
- getArgumentCompletions: (prefix) => {
49
- const normalized = prefix.trimStart().toLowerCase();
50
- if (/\s/.test(normalized)) return null;
51
- const matches = COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(normalized));
52
- return matches.length > 0 ? matches : null;
53
- },
54
- handler: async (args, ctx) => {
55
- const action = args.trim().toLowerCase() || "status";
56
- if (action === "status") {
57
- if (activeConfig) {
58
- ctx.ui.notify(
59
- [
60
- "Langfuse tracing is enabled.",
61
- `Endpoint: ${activeConfig.baseUrl}`,
62
- `Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
63
- `Configuration: ${configPath ?? "unknown"}`,
64
- ].join("\n"),
65
- "info",
66
- );
67
- return;
68
- }
60
+ description: "Open interactive Langfuse tracing controls",
61
+ handler: async (_args, ctx) => {
62
+ if (!ctx.hasUI) {
69
63
  ctx.ui.notify(
70
- initializationError ??
71
- "Langfuse tracing is disabled because its credentials are not configured.",
64
+ formatNonInteractiveStatus(activeConfig, configPath, initializationError),
72
65
  "warning",
73
66
  );
74
67
  return;
75
68
  }
76
- if (action === "flush") {
77
- if (!recorder) {
78
- ctx.ui.notify("Langfuse tracing is not enabled.", "warning");
69
+
70
+ const menuGeneration = sessionGeneration;
71
+ const menuRecorder = recorder;
72
+ const menuConfig = activeConfig;
73
+ const choice = await ctx.ui.select(
74
+ formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice),
75
+ [
76
+ ...(menuRecorder ? [FLUSH_ACTION] : []),
77
+ hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
78
+ HELP_ACTION,
79
+ ],
80
+ );
81
+ if (!choice || menuGeneration !== sessionGeneration) return;
82
+
83
+ if (choice === FLUSH_ACTION) {
84
+ if (!menuRecorder) {
85
+ ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
79
86
  return;
80
87
  }
81
88
  try {
82
- await recorder.flush();
83
- ctx.ui.notify("Langfuse traces flushed.", "info");
89
+ await menuRecorder.flush();
90
+ if (menuGeneration !== sessionGeneration) return;
91
+ ctx.ui.notify("Langfuse traces flushed for this session.", "info");
84
92
  } catch (error) {
85
- ctx.ui.notify(`Langfuse flush failed: ${formatError(error)}`, "error");
93
+ if (menuGeneration !== sessionGeneration) return;
94
+ ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
86
95
  }
87
96
  return;
88
97
  }
89
- if (action === "init") {
90
- if (!ctx.hasUI) {
91
- ctx.ui.notify(
92
- "/langfuse init requires interactive UI. Edit pi-langfuse.json manually.",
93
- "warning",
94
- );
95
- return;
96
- }
98
+
99
+ if (choice === SET_UP_ACTION || choice === UPDATE_ACTION) {
97
100
  const loaded = await loadConfig(configPath);
101
+ if (menuGeneration !== sessionGeneration) return;
98
102
  configPath = loaded.path;
99
103
  const next = await promptForConfig(ctx, loaded.ok ? loaded.config : undefined);
100
- if (!next) return;
104
+ if (!next || menuGeneration !== sessionGeneration) return;
101
105
  try {
102
106
  await writeConfig(next, loaded.path);
103
- initializationError =
104
- "Langfuse config was saved. Restart Pi to apply the new configuration.";
105
- ctx.ui.notify(`Saved Langfuse config to ${loaded.path}. Restart Pi to apply it.`, "info");
107
+ if (menuGeneration !== sessionGeneration) return;
108
+ hasStoredConfig = true;
109
+ configurationNotice =
110
+ "Saved; restart each Pi process to use it in subsequent sessions.";
111
+ ctx.ui.notify(
112
+ `Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
113
+ "info",
114
+ );
106
115
  } catch (error) {
107
- ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error)}`, "error");
116
+ if (menuGeneration !== sessionGeneration) return;
117
+ ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
108
118
  }
109
119
  return;
110
120
  }
111
- if (action === "help") {
112
- ctx.ui.notify(
113
- [
114
- "Usage: /langfuse [status|flush|help|init]",
115
- "status: show tracing state without credentials",
116
- "flush: wait for completed traces to export",
117
- "init: interactively create or update the private config",
118
- ].join("\n"),
119
- "info",
120
- );
121
- return;
121
+
122
+ if (choice === HELP_ACTION) {
123
+ ctx.ui.notify(formatHelp(configPath), "info");
122
124
  }
123
- ctx.ui.notify("Usage: /langfuse [status|flush|help|init]", "warning");
124
125
  },
125
126
  });
126
127
 
127
128
  pi.on("session_start", async (_event, ctx) => {
129
+ sessionGeneration += 1;
128
130
  recorder = undefined;
129
131
  activeConfig = undefined;
130
132
  configPath = undefined;
131
133
  initializationError = undefined;
134
+ hasStoredConfig = false;
135
+ configurationNotice = undefined;
136
+ nextAttemptReason = undefined;
132
137
 
133
138
  const result = await loadConfig();
134
139
  configPath = result.path;
@@ -139,6 +144,7 @@ export function createLangfuseExtension(
139
144
  return;
140
145
  }
141
146
 
147
+ hasStoredConfig = true;
142
148
  try {
143
149
  const backend = await createBackend(result.config);
144
150
  activeConfig = result.config;
@@ -149,43 +155,59 @@ export function createLangfuseExtension(
149
155
  captureContent: result.config.captureContent,
150
156
  });
151
157
  } catch (error) {
152
- initializationError = `Langfuse tracing could not start: ${formatError(error)}`;
158
+ initializationError = `Langfuse tracing could not start: ${formatError(error, result.config)}`;
153
159
  ctx.ui.notify(initializationError, "warning");
154
160
  }
155
161
  });
156
162
 
157
- pi.on("before_agent_start", (event, ctx) => {
158
- recorder?.beginAgent({
163
+ pi.on("before_agent_start", async (event, ctx) => {
164
+ nextAttemptReason = undefined;
165
+ const activeRecorder = recorder;
166
+ if (!activeRecorder) return;
167
+ const git = await resolveGit(ctx.cwd).catch(() => undefined);
168
+ if (recorder !== activeRecorder) return;
169
+ activeRecorder.beginAgent({
159
170
  prompt: event.prompt,
160
171
  images: event.images,
161
- model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
172
+ model: ctx.model
173
+ ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
174
+ : undefined,
175
+ git,
176
+ snapshot: contextSnapshot(ctx),
162
177
  });
163
178
  });
164
179
 
180
+ pi.on("agent_start", () => {
181
+ if (!recorder) return;
182
+ recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : undefined);
183
+ nextAttemptReason = undefined;
184
+ });
185
+
165
186
  pi.on("turn_start", (event, ctx) => {
166
187
  if (!recorder) return;
167
- if (!recorder.hasActiveTrace()) {
168
- recorder.beginAgent({
169
- prompt: "[automatic continuation]",
170
- model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
171
- });
172
- }
188
+ ensureActiveRun(recorder, ctx);
173
189
  recorder.beginTurn(event.turnIndex);
174
190
  });
175
191
 
176
- pi.on("before_provider_request", (_event, ctx) => {
192
+ pi.on("before_provider_request", (event, ctx) => {
177
193
  if (!recorder) return;
178
- if (!recorder.hasActiveTrace()) {
179
- recorder.beginAgent({
180
- prompt: "[automatic continuation]",
181
- model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
182
- });
183
- }
184
- recorder.beginGeneration();
194
+ ensureActiveRun(recorder, ctx);
195
+ recorder.beginGeneration({
196
+ payload: event.payload,
197
+ payloadStage: "before_provider_request",
198
+ model: ctx.model
199
+ ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
200
+ : undefined,
201
+ thinkingLevel: pi.getThinkingLevel(),
202
+ });
185
203
  });
186
204
 
187
205
  pi.on("after_provider_response", (event) => {
188
- recorder?.recordProviderResponse(event.status);
206
+ recorder?.recordProviderResponse(event.status, event.headers);
207
+ });
208
+
209
+ pi.on("message_update", (event) => {
210
+ if (isRealOutputDelta(event.assistantMessageEvent)) recorder?.markGenerationFirstOutput();
189
211
  });
190
212
 
191
213
  pi.on("message_end", (event) => {
@@ -201,11 +223,15 @@ export function createLangfuseExtension(
201
223
  });
202
224
 
203
225
  pi.on("tool_execution_start", (event) => {
204
- recorder?.beginTool(event.toolCallId, event.toolName);
226
+ recorder?.beginTool(event.toolCallId, event.toolName, event.args);
227
+ });
228
+
229
+ pi.on("tool_execution_update", (event) => {
230
+ recorder?.recordToolProgress(event.toolCallId);
205
231
  });
206
232
 
207
233
  pi.on("tool_result", (event) => {
208
- recorder?.updateToolInput(event.toolCallId, event.input);
234
+ recorder?.recordToolInput(event.toolCallId, event.input);
209
235
  });
210
236
 
211
237
  pi.on("tool_execution_end", (event) => {
@@ -217,33 +243,177 @@ export function createLangfuseExtension(
217
243
  });
218
244
 
219
245
  pi.on("agent_end", (event) => {
220
- for (let index = event.messages.length - 1; index >= 0; index -= 1) {
221
- const message = event.messages[index];
222
- if (message?.role !== "assistant") continue;
223
- recorder?.finishAssistant(message);
224
- break;
225
- }
226
- recorder?.settle();
246
+ const message = findLastAssistant(event.messages);
247
+ recorder?.finishAttempt(message);
248
+ });
249
+
250
+ pi.on("session_before_compact", (event) => {
251
+ recorder?.beginCompaction({
252
+ reason: event.reason,
253
+ willRetry: event.willRetry,
254
+ tokensBefore: event.preparation.tokensBefore,
255
+ messagesToSummarize: event.preparation.messagesToSummarize.length,
256
+ turnPrefixMessages: event.preparation.turnPrefixMessages.length,
257
+ branchEntries: event.branchEntries.length,
258
+ isSplitTurn: event.preparation.isSplitTurn,
259
+ });
260
+ });
261
+
262
+ pi.on("session_compact", (event) => {
263
+ const entry = event.compactionEntry as typeof event.compactionEntry & {
264
+ usage?: Parameters<TraceRecorder["finishCompaction"]>[0]["usage"];
265
+ };
266
+ recorder?.finishCompaction({
267
+ reason: event.reason,
268
+ willRetry: event.willRetry,
269
+ fromExtension: event.fromExtension,
270
+ tokensBefore: entry.tokensBefore,
271
+ details: entry.details,
272
+ usage: entry.usage,
273
+ });
274
+ if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
275
+ });
276
+
277
+ pi.on("agent_settled", (_event, ctx) => {
278
+ recorder?.settle(contextSnapshot(ctx));
279
+ nextAttemptReason = undefined;
227
280
  });
228
281
 
229
282
  pi.on("session_shutdown", async (event, ctx) => {
283
+ sessionGeneration += 1;
230
284
  const activeRecorder = recorder;
285
+ const shutdownConfig = activeConfig;
231
286
  recorder = undefined;
232
287
  activeConfig = undefined;
233
288
  if (!activeRecorder) return;
234
289
  try {
235
- if (event.reason === "quit") await activeRecorder.shutdown();
290
+ const snapshot = contextSnapshot(ctx);
291
+ if (event.reason === "quit") await activeRecorder.shutdown(snapshot);
236
292
  else {
237
- activeRecorder.settle();
293
+ activeRecorder.interrupt(
294
+ `Pi session ended before settlement (${event.reason}).`,
295
+ snapshot,
296
+ );
238
297
  await activeRecorder.flush();
239
298
  }
240
299
  } catch (error) {
241
- ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error)}`, "error");
300
+ ctx.ui.notify(
301
+ `Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`,
302
+ "error",
303
+ );
242
304
  }
243
305
  });
244
306
  };
245
307
  }
246
308
 
309
+ function ensureActiveRun(recorder: TraceRecorder, ctx: ExtensionContext): void {
310
+ if (!recorder.hasActiveTrace()) {
311
+ recorder.beginAgent({
312
+ prompt: "[automatic continuation]",
313
+ model: ctx.model
314
+ ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
315
+ : undefined,
316
+ snapshot: contextSnapshot(ctx),
317
+ });
318
+ }
319
+ if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
320
+ }
321
+
322
+ function contextSnapshot(ctx: ExtensionContext): ContextSnapshot {
323
+ return {
324
+ leafId:
325
+ typeof ctx.sessionManager.getLeafId === "function"
326
+ ? ctx.sessionManager.getLeafId()
327
+ : undefined,
328
+ contextUsage: ctx.getContextUsage(),
329
+ };
330
+ }
331
+
332
+ function findLastAssistant<T extends { role?: string }>(messages: readonly T[]): T | undefined {
333
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
334
+ const message = messages[index];
335
+ if (message?.role === "assistant") return message;
336
+ }
337
+ return undefined;
338
+ }
339
+
340
+ function isRealOutputDelta(event: { type: string; delta?: unknown }): boolean {
341
+ return (
342
+ (event.type === "text_delta" ||
343
+ event.type === "thinking_delta" ||
344
+ event.type === "toolcall_delta") &&
345
+ typeof event.delta === "string" &&
346
+ event.delta.length > 0
347
+ );
348
+ }
349
+
350
+ const GIT_LOOKUP_TIMEOUT_MS = 1_000;
351
+ const MAX_GIT_BRANCH_LENGTH = 256;
352
+
353
+ type GitExecutor = ExtensionAPI["exec"];
354
+
355
+ export async function resolveGitMetadata(
356
+ exec: GitExecutor,
357
+ cwd: string,
358
+ ): Promise<GitMetadata | undefined> {
359
+ const [branchResult, commit] = await Promise.all([
360
+ resolveGitBranch(exec, cwd),
361
+ resolveGitCommit(exec, cwd),
362
+ ]);
363
+ if (!branchResult.resolved) return undefined;
364
+ if (branchResult.branch) {
365
+ return {
366
+ branch: branchResult.branch,
367
+ ...(commit ? { commit } : {}),
368
+ detached: false,
369
+ };
370
+ }
371
+ return commit ? { commit, detached: true } : undefined;
372
+ }
373
+
374
+ async function resolveGitBranch(
375
+ exec: GitExecutor,
376
+ cwd: string,
377
+ ): Promise<{ resolved: boolean; branch?: string }> {
378
+ try {
379
+ const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
380
+ cwd,
381
+ timeout: GIT_LOOKUP_TIMEOUT_MS,
382
+ });
383
+ if (result.killed) return { resolved: false };
384
+ if (result.code === 1) return { resolved: true };
385
+ if (result.code !== 0) return { resolved: false };
386
+ const branch = normalizeGitBranch(result.stdout);
387
+ return branch ? { resolved: true, branch } : { resolved: false };
388
+ } catch {
389
+ return { resolved: false };
390
+ }
391
+ }
392
+
393
+ async function resolveGitCommit(exec: GitExecutor, cwd: string): Promise<string | undefined> {
394
+ try {
395
+ const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
396
+ cwd,
397
+ timeout: GIT_LOOKUP_TIMEOUT_MS,
398
+ });
399
+ if (result.code !== 0 || result.killed) return undefined;
400
+ const commit = result.stdout.trim();
401
+ return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : undefined;
402
+ } catch {
403
+ return undefined;
404
+ }
405
+ }
406
+
407
+ function normalizeGitBranch(value: string): string | undefined {
408
+ const branch = value.trim();
409
+ if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return undefined;
410
+ for (const character of branch) {
411
+ const code = character.codePointAt(0) ?? 0;
412
+ if (code <= 31 || code === 127) return undefined;
413
+ }
414
+ return branch;
415
+ }
416
+
247
417
  async function promptForConfig(
248
418
  ctx: ExtensionCommandContext,
249
419
  current: LangfuseConfig | undefined,
@@ -281,15 +451,88 @@ async function promptForConfig(
281
451
  return normalized.config;
282
452
  }
283
453
 
454
+ function formatMenuTitle(
455
+ activeConfig: LangfuseConfig | undefined,
456
+ configPath: string | undefined,
457
+ initializationError: string | undefined,
458
+ configurationNotice: string | undefined,
459
+ ): string {
460
+ const lines = [
461
+ "Langfuse",
462
+ "",
463
+ "Current session:",
464
+ ` Tracing: ${activeConfig ? "enabled" : "disabled"}`,
465
+ ];
466
+ if (activeConfig) {
467
+ lines.push(
468
+ ` Endpoint: ${activeConfig.baseUrl}`,
469
+ ` Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
470
+ );
471
+ } else if (configurationNotice) {
472
+ lines.push(" State: tracing remains disabled until Pi restarts.");
473
+ } else if (initializationError) {
474
+ lines.push(` Reason: ${initializationError}`);
475
+ }
476
+ lines.push(
477
+ "",
478
+ "Agent-directory configuration:",
479
+ ` Configuration: ${configPath ?? "unknown"}`,
480
+ " Scope: this Pi agent directory; restart each Pi process to apply changes.",
481
+ );
482
+ if (configurationNotice) lines.push(` Pending: ${configurationNotice}`);
483
+ lines.push("", "What do you want to do?");
484
+ return lines.join("\n");
485
+ }
486
+
487
+ function formatNonInteractiveStatus(
488
+ activeConfig: LangfuseConfig | undefined,
489
+ configPath: string | undefined,
490
+ initializationError: string | undefined,
491
+ ): string {
492
+ return [
493
+ "/langfuse requires interactive UI.",
494
+ `Current session tracing: ${activeConfig ? "enabled" : "disabled"}`,
495
+ ...(activeConfig
496
+ ? [
497
+ `Endpoint: ${activeConfig.baseUrl}`,
498
+ `Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
499
+ ]
500
+ : initializationError
501
+ ? [`Reason: ${initializationError}`]
502
+ : []),
503
+ `Configuration: ${configPath ?? "unknown"}`,
504
+ "Edit the private config manually, then restart each Pi process to apply it to subsequent sessions.",
505
+ ].join("\n");
506
+ }
507
+
508
+ function formatHelp(configPath: string | undefined): string {
509
+ return [
510
+ "Langfuse setup and privacy:",
511
+ "Run /langfuse in interactive Pi to manage tracing for the current session.",
512
+ `Configuration: ${configPath ?? "pi-langfuse.json"}`,
513
+ "The file belongs to this Pi agent directory; restart each Pi process after changing it.",
514
+ "Trace content may contain prompts, responses, tool arguments, tool results, and source code.",
515
+ "Git branch, commit, cwd, model, and usage remain in metadata when content capture is disabled.",
516
+ 'Use "captureContent": false in the private config to export metadata only.',
517
+ ].join("\n");
518
+ }
519
+
284
520
  function formatConfigError(result: Extract<LangfuseConfigResult, { ok: false }>): string {
285
521
  const setupHint = result.reason.startsWith("Configuration file not found:")
286
- ? " Run /langfuse init to create it."
522
+ ? " Run /langfuse and choose Set up Langfuse."
287
523
  : "";
288
524
  return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
289
525
  }
290
526
 
291
- function formatError(error: unknown): string {
292
- return error instanceof Error ? error.message : String(error);
527
+ function formatError(error: unknown, config?: LangfuseConfig): string {
528
+ let message = error instanceof Error ? error.message : String(error);
529
+ const secrets = [config?.publicKey, config?.secretKey]
530
+ .filter((secret): secret is string => Boolean(secret))
531
+ .sort((left, right) => right.length - left.length);
532
+ for (const secret of secrets) {
533
+ message = message.split(secret).join("[LANGFUSE_KEY_REDACTED]");
534
+ }
535
+ return message;
293
536
  }
294
537
 
295
538
  export default createLangfuseExtension();