@narumitw/pi-langfuse 0.14.1
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/LICENSE +21 -0
- package/README.md +135 -0
- package/package.json +49 -0
- package/src/config.ts +208 -0
- package/src/langfuse.ts +295 -0
- package/src/runtime.ts +247 -0
- package/src/tracing.ts +472 -0
package/src/langfuse.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BASE_URL,
|
|
7
|
+
type LangfuseConfig,
|
|
8
|
+
type LangfuseConfigResult,
|
|
9
|
+
loadLangfuseConfig,
|
|
10
|
+
normalizeLangfuseConfig,
|
|
11
|
+
writeLangfuseConfig,
|
|
12
|
+
} from "./config.js";
|
|
13
|
+
import { type TraceBackend, TraceRecorder } from "./tracing.js";
|
|
14
|
+
|
|
15
|
+
interface ExtensionDependencies {
|
|
16
|
+
loadConfig(path?: string): Promise<LangfuseConfigResult>;
|
|
17
|
+
writeConfig(config: LangfuseConfig, path?: string): Promise<LangfuseConfig>;
|
|
18
|
+
createBackend(config: LangfuseConfig): Promise<TraceBackend>;
|
|
19
|
+
}
|
|
20
|
+
|
|
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
|
+
];
|
|
27
|
+
|
|
28
|
+
export function createLangfuseExtension(
|
|
29
|
+
dependencies: Partial<ExtensionDependencies> = {},
|
|
30
|
+
): (pi: ExtensionAPI) => void {
|
|
31
|
+
const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
|
|
32
|
+
const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
|
|
33
|
+
const createBackend =
|
|
34
|
+
dependencies.createBackend ??
|
|
35
|
+
(async (config) => {
|
|
36
|
+
const { createProductionBackend } = await import("./runtime.js");
|
|
37
|
+
return createProductionBackend(config);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return function langfuse(pi: ExtensionAPI) {
|
|
41
|
+
let recorder: TraceRecorder | undefined;
|
|
42
|
+
let activeConfig: LangfuseConfig | undefined;
|
|
43
|
+
let configPath: string | undefined;
|
|
44
|
+
let initializationError: string | undefined;
|
|
45
|
+
|
|
46
|
+
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
|
+
}
|
|
69
|
+
ctx.ui.notify(
|
|
70
|
+
initializationError ??
|
|
71
|
+
"Langfuse tracing is disabled because its credentials are not configured.",
|
|
72
|
+
"warning",
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (action === "flush") {
|
|
77
|
+
if (!recorder) {
|
|
78
|
+
ctx.ui.notify("Langfuse tracing is not enabled.", "warning");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await recorder.flush();
|
|
83
|
+
ctx.ui.notify("Langfuse traces flushed.", "info");
|
|
84
|
+
} catch (error) {
|
|
85
|
+
ctx.ui.notify(`Langfuse flush failed: ${formatError(error)}`, "error");
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
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
|
+
}
|
|
97
|
+
const loaded = await loadConfig(configPath);
|
|
98
|
+
configPath = loaded.path;
|
|
99
|
+
const next = await promptForConfig(ctx, loaded.ok ? loaded.config : undefined);
|
|
100
|
+
if (!next) return;
|
|
101
|
+
try {
|
|
102
|
+
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");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error)}`, "error");
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
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;
|
|
122
|
+
}
|
|
123
|
+
ctx.ui.notify("Usage: /langfuse [status|flush|help|init]", "warning");
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
128
|
+
recorder = undefined;
|
|
129
|
+
activeConfig = undefined;
|
|
130
|
+
configPath = undefined;
|
|
131
|
+
initializationError = undefined;
|
|
132
|
+
|
|
133
|
+
const result = await loadConfig();
|
|
134
|
+
configPath = result.path;
|
|
135
|
+
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
136
|
+
if (!result.ok) {
|
|
137
|
+
initializationError = formatConfigError(result);
|
|
138
|
+
ctx.ui.notify(initializationError, "warning");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const backend = await createBackend(result.config);
|
|
144
|
+
activeConfig = result.config;
|
|
145
|
+
recorder = new TraceRecorder(backend, {
|
|
146
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
147
|
+
cwd: ctx.cwd,
|
|
148
|
+
mode: ctx.mode,
|
|
149
|
+
captureContent: result.config.captureContent,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
initializationError = `Langfuse tracing could not start: ${formatError(error)}`;
|
|
153
|
+
ctx.ui.notify(initializationError, "warning");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
158
|
+
recorder?.beginAgent({
|
|
159
|
+
prompt: event.prompt,
|
|
160
|
+
images: event.images,
|
|
161
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.on("turn_start", (event, ctx) => {
|
|
166
|
+
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
|
+
}
|
|
173
|
+
recorder.beginTurn(event.turnIndex);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
177
|
+
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();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.on("after_provider_response", (event) => {
|
|
188
|
+
recorder?.recordProviderResponse(event.status);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
pi.on("message_end", (event) => {
|
|
192
|
+
if (event.message.role === "assistant") recorder?.markGenerationEnd();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
pi.on("turn_end", (event) => {
|
|
196
|
+
if (event.message.role === "assistant") recorder?.finishAssistant(event.message);
|
|
197
|
+
recorder?.finishTurn(event.turnIndex, {
|
|
198
|
+
message: event.message,
|
|
199
|
+
toolResultCount: event.toolResults.length,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
pi.on("tool_execution_start", (event) => {
|
|
204
|
+
recorder?.beginTool(event.toolCallId, event.toolName);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
pi.on("tool_result", (event) => {
|
|
208
|
+
recorder?.updateToolInput(event.toolCallId, event.input);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("tool_execution_end", (event) => {
|
|
212
|
+
recorder?.finishTool(event.toolCallId, {
|
|
213
|
+
content: event.result.content,
|
|
214
|
+
details: event.result.details,
|
|
215
|
+
isError: event.isError,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
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();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
230
|
+
const activeRecorder = recorder;
|
|
231
|
+
recorder = undefined;
|
|
232
|
+
activeConfig = undefined;
|
|
233
|
+
if (!activeRecorder) return;
|
|
234
|
+
try {
|
|
235
|
+
if (event.reason === "quit") await activeRecorder.shutdown();
|
|
236
|
+
else {
|
|
237
|
+
activeRecorder.settle();
|
|
238
|
+
await activeRecorder.flush();
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error)}`, "error");
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function promptForConfig(
|
|
248
|
+
ctx: ExtensionCommandContext,
|
|
249
|
+
current: LangfuseConfig | undefined,
|
|
250
|
+
): Promise<LangfuseConfig | undefined> {
|
|
251
|
+
const secretKey = await ctx.ui.input("Langfuse secret key (leave blank to keep existing):");
|
|
252
|
+
if (secretKey === undefined) {
|
|
253
|
+
ctx.ui.notify("Cancelled", "info");
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const publicKey = await ctx.ui.input("Langfuse public key (leave blank to keep existing):");
|
|
257
|
+
if (publicKey === undefined) {
|
|
258
|
+
ctx.ui.notify("Cancelled", "info");
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const baseUrl = await ctx.ui.input(
|
|
262
|
+
`Langfuse base URL (leave blank for default ${DEFAULT_BASE_URL}):`,
|
|
263
|
+
DEFAULT_BASE_URL,
|
|
264
|
+
);
|
|
265
|
+
if (baseUrl === undefined) {
|
|
266
|
+
ctx.ui.notify("Cancelled", "info");
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const normalized = normalizeLangfuseConfig({
|
|
271
|
+
...current,
|
|
272
|
+
publicKey: publicKey.trim() || current?.publicKey || "",
|
|
273
|
+
secretKey: secretKey.trim() || current?.secretKey || "",
|
|
274
|
+
baseUrl: baseUrl.trim() || DEFAULT_BASE_URL,
|
|
275
|
+
captureContent: current?.captureContent ?? true,
|
|
276
|
+
});
|
|
277
|
+
if (!normalized.ok) {
|
|
278
|
+
ctx.ui.notify(`Invalid Langfuse config: ${normalized.reason}`, "error");
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
return normalized.config;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatConfigError(result: Extract<LangfuseConfigResult, { ok: false }>): string {
|
|
285
|
+
const setupHint = result.reason.startsWith("Configuration file not found:")
|
|
286
|
+
? " Run /langfuse init to create it."
|
|
287
|
+
: "";
|
|
288
|
+
return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatError(error: unknown): string {
|
|
292
|
+
return error instanceof Error ? error.message : String(error);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export default createLangfuseExtension();
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
3
|
+
import {
|
|
4
|
+
type LangfuseObservation,
|
|
5
|
+
type LangfuseObservationAttributes,
|
|
6
|
+
type LangfuseTraceAttributes,
|
|
7
|
+
setLangfuseTracerProvider,
|
|
8
|
+
startObservation,
|
|
9
|
+
} from "@langfuse/tracing";
|
|
10
|
+
import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
11
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
12
|
+
import type { LangfuseConfig } from "./config.js";
|
|
13
|
+
import type {
|
|
14
|
+
Observation,
|
|
15
|
+
ObservationAttributes,
|
|
16
|
+
ObservationType,
|
|
17
|
+
TraceBackend,
|
|
18
|
+
} from "./tracing.js";
|
|
19
|
+
|
|
20
|
+
const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
|
|
21
|
+
|
|
22
|
+
interface SharedRuntime {
|
|
23
|
+
fingerprint: string;
|
|
24
|
+
backend: ProductionTraceBackend;
|
|
25
|
+
shutdown: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type GlobalWithRuntime = typeof globalThis & {
|
|
29
|
+
[RUNTIME_KEY]?: Promise<SharedRuntime>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface RuntimeFactories {
|
|
33
|
+
createProcessor(config: LangfuseConfig): SpanProcessor;
|
|
34
|
+
createProvider(processor: SpanProcessor): NodeTracerProvider;
|
|
35
|
+
selectProvider(provider: NodeTracerProvider): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ProductionObservation implements Observation {
|
|
39
|
+
constructor(readonly native: LangfuseObservation) {}
|
|
40
|
+
|
|
41
|
+
update(attributes: ObservationAttributes): Observation {
|
|
42
|
+
this.native.updateOtelSpanAttributes(attributes as LangfuseObservationAttributes);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
updateTrace(attributes: ObservationAttributes): Observation {
|
|
47
|
+
this.native.updateTrace(attributes as LangfuseTraceAttributes);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
end(endTime?: number): Observation {
|
|
52
|
+
this.native.end(endTime);
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class ProductionTraceBackend implements TraceBackend {
|
|
58
|
+
constructor(
|
|
59
|
+
private readonly provider: NodeTracerProvider,
|
|
60
|
+
private readonly processor: SpanProcessor,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
start(
|
|
64
|
+
name: string,
|
|
65
|
+
attributes: ObservationAttributes,
|
|
66
|
+
options: { asType: ObservationType; parent?: Observation },
|
|
67
|
+
): Observation {
|
|
68
|
+
const parent = options.parent;
|
|
69
|
+
if (parent instanceof ProductionObservation) {
|
|
70
|
+
return new ProductionObservation(
|
|
71
|
+
startChild(parent.native, name, attributes, options.asType),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return new ProductionObservation(startRoot(name, attributes, options.asType));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async forceFlush(): Promise<void> {
|
|
78
|
+
await this.processor.forceFlush();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async shutdown(): Promise<void> {
|
|
82
|
+
await this.provider.shutdown();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createProductionBackend(
|
|
87
|
+
config: LangfuseConfig,
|
|
88
|
+
factoryOverrides: Partial<RuntimeFactories> = {},
|
|
89
|
+
): Promise<TraceBackend> {
|
|
90
|
+
const globalRuntime = globalThis as GlobalWithRuntime;
|
|
91
|
+
const fingerprint = configFingerprint(config);
|
|
92
|
+
const existing = globalRuntime[RUNTIME_KEY];
|
|
93
|
+
if (existing) {
|
|
94
|
+
const runtime = await existing;
|
|
95
|
+
if (runtime.shutdown) {
|
|
96
|
+
throw new Error("Langfuse tracing was already shut down; restart Pi to enable it again.");
|
|
97
|
+
}
|
|
98
|
+
if (runtime.fingerprint !== fingerprint) {
|
|
99
|
+
throw new Error("Langfuse configuration changed; restart Pi to apply the new credentials.");
|
|
100
|
+
}
|
|
101
|
+
return runtime.backend;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const factories = { ...defaultFactories, ...factoryOverrides };
|
|
105
|
+
const initializing = initializeRuntime(config, fingerprint, factories);
|
|
106
|
+
globalRuntime[RUNTIME_KEY] = initializing;
|
|
107
|
+
try {
|
|
108
|
+
return (await initializing).backend;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
delete globalRuntime[RUNTIME_KEY];
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function initializeRuntime(
|
|
116
|
+
config: LangfuseConfig,
|
|
117
|
+
fingerprint: string,
|
|
118
|
+
factories: RuntimeFactories,
|
|
119
|
+
): Promise<SharedRuntime> {
|
|
120
|
+
const processor = factories.createProcessor(config);
|
|
121
|
+
let provider: NodeTracerProvider | undefined;
|
|
122
|
+
try {
|
|
123
|
+
provider = factories.createProvider(processor);
|
|
124
|
+
factories.selectProvider(provider);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
const backend = new ProductionTraceBackend(provider, processor);
|
|
130
|
+
const runtime: SharedRuntime = { fingerprint, backend, shutdown: false };
|
|
131
|
+
const originalShutdown = backend.shutdown.bind(backend);
|
|
132
|
+
backend.shutdown = async () => {
|
|
133
|
+
if (runtime.shutdown) return;
|
|
134
|
+
runtime.shutdown = true;
|
|
135
|
+
await originalShutdown();
|
|
136
|
+
};
|
|
137
|
+
return runtime;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const defaultFactories: RuntimeFactories = {
|
|
141
|
+
createProcessor: (config) => {
|
|
142
|
+
const secrets = [config.secretKey, config.publicKey];
|
|
143
|
+
return new LangfuseSpanProcessor({
|
|
144
|
+
publicKey: config.publicKey,
|
|
145
|
+
secretKey: config.secretKey,
|
|
146
|
+
baseUrl: config.baseUrl,
|
|
147
|
+
environment: config.environment ?? "",
|
|
148
|
+
release: config.release ?? "",
|
|
149
|
+
flushAt: 512,
|
|
150
|
+
flushInterval: 5,
|
|
151
|
+
timeout: 5,
|
|
152
|
+
mask: ({ data }) => maskSecrets(data, secrets),
|
|
153
|
+
shouldExportSpan: ({ otelSpan }) =>
|
|
154
|
+
typeof otelSpan.attributes["langfuse.observation.type"] === "string",
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
createProvider: (processor) => new NodeTracerProvider({ spanProcessors: [processor] }),
|
|
158
|
+
selectProvider: setLangfuseTracerProvider,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function startRoot(
|
|
162
|
+
name: string,
|
|
163
|
+
attributes: ObservationAttributes,
|
|
164
|
+
type: ObservationType,
|
|
165
|
+
): LangfuseObservation {
|
|
166
|
+
if (type === "agent") {
|
|
167
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
|
|
168
|
+
}
|
|
169
|
+
if (type === "generation") {
|
|
170
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
171
|
+
asType: "generation",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (type === "tool") {
|
|
175
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
|
|
176
|
+
}
|
|
177
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function startChild(
|
|
181
|
+
parent: LangfuseObservation,
|
|
182
|
+
name: string,
|
|
183
|
+
attributes: ObservationAttributes,
|
|
184
|
+
type: ObservationType,
|
|
185
|
+
): LangfuseObservation {
|
|
186
|
+
if (type === "agent") {
|
|
187
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
188
|
+
asType: "agent",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (type === "generation") {
|
|
192
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
193
|
+
asType: "generation",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (type === "tool") {
|
|
197
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
198
|
+
asType: "tool",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
202
|
+
asType: "span",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function configFingerprint(config: LangfuseConfig): string {
|
|
207
|
+
return createHash("sha256")
|
|
208
|
+
.update(
|
|
209
|
+
`${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
|
|
210
|
+
)
|
|
211
|
+
.digest("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function maskSecrets(data: unknown, secrets: readonly string[]): unknown {
|
|
215
|
+
return maskSecretValue(data, secrets, new WeakSet<object>());
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function maskSecretValue(
|
|
219
|
+
data: unknown,
|
|
220
|
+
secrets: readonly string[],
|
|
221
|
+
active: WeakSet<object>,
|
|
222
|
+
): unknown {
|
|
223
|
+
if (typeof data === "string") {
|
|
224
|
+
let masked = data.replace(/\b(?:sk|pk)-lf-[A-Za-z0-9_-]+\b/g, "[LANGFUSE_KEY_REDACTED]");
|
|
225
|
+
for (const secret of secrets) {
|
|
226
|
+
if (secret) masked = masked.replaceAll(secret, "[LANGFUSE_KEY_REDACTED]");
|
|
227
|
+
}
|
|
228
|
+
return masked;
|
|
229
|
+
}
|
|
230
|
+
if (!data || typeof data !== "object") return data;
|
|
231
|
+
if (active.has(data)) return "[circular]";
|
|
232
|
+
|
|
233
|
+
active.add(data);
|
|
234
|
+
try {
|
|
235
|
+
if (Array.isArray(data)) {
|
|
236
|
+
return data.map((item) => maskSecretValue(item, secrets, active));
|
|
237
|
+
}
|
|
238
|
+
return Object.fromEntries(
|
|
239
|
+
Object.entries(data).map(([key, value]) => [
|
|
240
|
+
key,
|
|
241
|
+
maskSecretValue(value, secrets, active),
|
|
242
|
+
]),
|
|
243
|
+
);
|
|
244
|
+
} finally {
|
|
245
|
+
active.delete(data);
|
|
246
|
+
}
|
|
247
|
+
}
|