pi-langfuse 1.5.7 → 1.5.8
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/src/langfuse.ts +267 -50
- package/src/types.ts +25 -0
package/package.json
CHANGED
package/src/langfuse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LangfuseRuntime, LangfuseScoreClient } from "./types.js";
|
|
1
|
+
import type { LangfuseRuntime, LangfuseScoreClient, PendingScore } from "./types.js";
|
|
2
2
|
import { state } from "./state.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
@@ -60,6 +60,11 @@ interface RestFallbackStore {
|
|
|
60
60
|
const OTEL_VISIBILITY_TIMEOUT_MS = 1_500;
|
|
61
61
|
const OTEL_VISIBILITY_POLL_INTERVAL_MS = 200;
|
|
62
62
|
const DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS = 2_000;
|
|
63
|
+
const DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS = 5;
|
|
64
|
+
const DEFAULT_SCORE_FLUSH_AT = 10;
|
|
65
|
+
const DEFAULT_SCORE_FLUSH_INTERVAL_MS = 1_000;
|
|
66
|
+
const MAX_SCORE_QUEUE_SIZE = 100_000;
|
|
67
|
+
const MAX_SCORE_BATCH_SIZE = 100;
|
|
63
68
|
|
|
64
69
|
let shutdownStepTimeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS;
|
|
65
70
|
|
|
@@ -67,8 +72,27 @@ function nowIso() {
|
|
|
67
72
|
return new Date().toISOString();
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
function
|
|
71
|
-
|
|
75
|
+
function resolvePositiveEnvNumber(name: string, fallback: number, integer = false): number {
|
|
76
|
+
const parsed = Number(process.env[name]);
|
|
77
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
78
|
+
return fallback;
|
|
79
|
+
}
|
|
80
|
+
return integer ? Math.floor(parsed) : parsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function delay(ms: number, signal?: AbortSignal) {
|
|
84
|
+
return new Promise<void>((resolve, reject) => {
|
|
85
|
+
if (signal?.aborted) {
|
|
86
|
+
reject(signal.reason);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const timeout = setTimeout(resolve, ms);
|
|
91
|
+
signal?.addEventListener("abort", () => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
reject(signal.reason);
|
|
94
|
+
}, { once: true });
|
|
95
|
+
});
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
function debugLog(message: string) {
|
|
@@ -107,7 +131,14 @@ export function getLastRuntimeError(): { scope: string; message: string; timesta
|
|
|
107
131
|
return lastRuntimeError;
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
async function
|
|
134
|
+
async function withShutdownDeadline<T>(label: string, startOperation: () => Promise<T> | undefined, deadline: number): Promise<T | undefined> {
|
|
135
|
+
const remainingMs = deadline - Date.now();
|
|
136
|
+
if (remainingMs <= 0) {
|
|
137
|
+
debugLog(`📊 Langfuse: Skipped ${label}; shutdown deadline elapsed`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const operation = startOperation();
|
|
111
142
|
if (!operation) {
|
|
112
143
|
return undefined;
|
|
113
144
|
}
|
|
@@ -118,9 +149,9 @@ async function withTimeout<T>(label: string, operation: Promise<T> | undefined):
|
|
|
118
149
|
operation,
|
|
119
150
|
new Promise<undefined>((resolve) => {
|
|
120
151
|
timeout = setTimeout(() => {
|
|
121
|
-
debugLog(`📊 Langfuse: ${label} timed out
|
|
152
|
+
debugLog(`📊 Langfuse: ${label} timed out; shutdown deadline elapsed`);
|
|
122
153
|
resolve(undefined);
|
|
123
|
-
},
|
|
154
|
+
}, remainingMs);
|
|
124
155
|
}),
|
|
125
156
|
]);
|
|
126
157
|
} finally {
|
|
@@ -130,6 +161,142 @@ async function withTimeout<T>(label: string, operation: Promise<T> | undefined):
|
|
|
130
161
|
}
|
|
131
162
|
}
|
|
132
163
|
|
|
164
|
+
function getRuntimeConfig(rt: LangfuseRuntime) {
|
|
165
|
+
return rt.runtimeConfig ?? state.config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ingestionHeaders(rt: LangfuseRuntime): Record<string, string> {
|
|
169
|
+
const config = getRuntimeConfig(rt);
|
|
170
|
+
if (!config) {
|
|
171
|
+
throw new Error("Langfuse runtime config is unavailable");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const auth = Buffer.from(`${config.publicKey}:${config.secretKey}`).toString("base64");
|
|
175
|
+
return {
|
|
176
|
+
Authorization: `Basic ${auth}`,
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function ingestBatch(rt: LangfuseRuntime, batch: unknown[], signal: AbortSignal): Promise<unknown[]> {
|
|
182
|
+
const config = getRuntimeConfig(rt);
|
|
183
|
+
if (!config) {
|
|
184
|
+
throw new Error("Langfuse runtime config is unavailable");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const response = await fetch(`${config.host.replace(/\/$/, "")}/api/public/ingestion`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: ingestionHeaders(rt),
|
|
190
|
+
body: JSON.stringify({ batch }),
|
|
191
|
+
signal,
|
|
192
|
+
});
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`Langfuse ingestion failed with HTTP ${response.status}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const text = await response.text();
|
|
198
|
+
if (!text) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const responseBody = JSON.parse(text) as { errors?: unknown[] };
|
|
203
|
+
return Array.isArray(responseBody.errors) ? responseBody.errors : [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function flushPendingScores(rt: LangfuseRuntime, signal: AbortSignal): Promise<void> {
|
|
207
|
+
const pendingScores = rt.pendingScores;
|
|
208
|
+
if (!pendingScores || pendingScores.length === 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
while (pendingScores.length > 0) {
|
|
213
|
+
const scores = pendingScores.slice(0, MAX_SCORE_BATCH_SIZE);
|
|
214
|
+
try {
|
|
215
|
+
const errors = await ingestBatch(
|
|
216
|
+
rt,
|
|
217
|
+
scores.map((score) => ({
|
|
218
|
+
type: "score-create",
|
|
219
|
+
id: randomUUID(),
|
|
220
|
+
timestamp: nowIso(),
|
|
221
|
+
body: score,
|
|
222
|
+
})),
|
|
223
|
+
signal,
|
|
224
|
+
);
|
|
225
|
+
pendingScores.splice(0, scores.length);
|
|
226
|
+
if (errors.length > 0) {
|
|
227
|
+
rememberRuntimeError("score ingestion", new Error(JSON.stringify(errors)));
|
|
228
|
+
console.warn("📊 Langfuse: Score ingestion reported errors", errors);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if ((error as { name?: string }).name !== "AbortError") {
|
|
232
|
+
rememberRuntimeError("score ingestion", error);
|
|
233
|
+
console.warn("📊 Langfuse: Failed to flush scores", error);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function clearScoreFlushTimer(rt: LangfuseRuntime) {
|
|
241
|
+
if (rt.scoreFlushTimer) {
|
|
242
|
+
clearTimeout(rt.scoreFlushTimer);
|
|
243
|
+
rt.scoreFlushTimer = undefined;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function scheduleScoreFlush(rt: LangfuseRuntime) {
|
|
248
|
+
if (
|
|
249
|
+
rt.scoreFlushStopped
|
|
250
|
+
|| rt.scoreFlushTimer
|
|
251
|
+
|| rt.scoreFlushPromise
|
|
252
|
+
|| !rt.pendingScores?.length
|
|
253
|
+
) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
rt.scoreFlushTimer = setTimeout(() => {
|
|
258
|
+
rt.scoreFlushTimer = undefined;
|
|
259
|
+
void startScoreFlush(rt);
|
|
260
|
+
}, rt.scoreFlushIntervalMs ?? DEFAULT_SCORE_FLUSH_INTERVAL_MS);
|
|
261
|
+
rt.scoreFlushTimer.unref?.();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function startScoreFlush(rt: LangfuseRuntime): Promise<void> {
|
|
265
|
+
if (rt.scoreFlushPromise) {
|
|
266
|
+
return rt.scoreFlushPromise;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
clearScoreFlushTimer(rt);
|
|
270
|
+
const controller = new AbortController();
|
|
271
|
+
const timeout = setTimeout(
|
|
272
|
+
() => controller.abort(new DOMException("Langfuse score request timed out", "AbortError")),
|
|
273
|
+
rt.scoreRequestTimeoutMs ?? DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS * 1_000,
|
|
274
|
+
);
|
|
275
|
+
timeout.unref?.();
|
|
276
|
+
rt.scoreFlushController = controller;
|
|
277
|
+
|
|
278
|
+
const promise = flushPendingScores(rt, controller.signal).finally(() => {
|
|
279
|
+
clearTimeout(timeout);
|
|
280
|
+
if (rt.scoreFlushPromise === promise) {
|
|
281
|
+
rt.scoreFlushPromise = undefined;
|
|
282
|
+
}
|
|
283
|
+
if (rt.scoreFlushController === controller) {
|
|
284
|
+
rt.scoreFlushController = undefined;
|
|
285
|
+
}
|
|
286
|
+
scheduleScoreFlush(rt);
|
|
287
|
+
});
|
|
288
|
+
rt.scoreFlushPromise = promise;
|
|
289
|
+
return promise;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stopScoreFlush(rt: LangfuseRuntime) {
|
|
293
|
+
rt.scoreFlushStopped = true;
|
|
294
|
+
clearScoreFlushTimer(rt);
|
|
295
|
+
rt.scoreFlushController?.abort(
|
|
296
|
+
new DOMException("Langfuse score flushing stopped", "AbortError"),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
133
300
|
function toIso(value: unknown): string | undefined {
|
|
134
301
|
if (!value) {
|
|
135
302
|
return undefined;
|
|
@@ -258,27 +425,40 @@ function wrapObservation(
|
|
|
258
425
|
};
|
|
259
426
|
}
|
|
260
427
|
|
|
261
|
-
async function traceExists(rt: LangfuseRuntime, traceId: string): Promise<boolean> {
|
|
428
|
+
async function traceExists(rt: LangfuseRuntime, traceId: string, signal: AbortSignal): Promise<boolean> {
|
|
429
|
+
const config = getRuntimeConfig(rt);
|
|
430
|
+
if (!config) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
262
434
|
try {
|
|
263
|
-
const
|
|
264
|
-
|
|
435
|
+
const response = await fetch(
|
|
436
|
+
`${config.host.replace(/\/$/, "")}/api/public/traces/${encodeURIComponent(traceId)}`,
|
|
437
|
+
{
|
|
438
|
+
headers: ingestionHeaders(rt),
|
|
439
|
+
signal,
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
if (response.status === 404) {
|
|
265
443
|
return false;
|
|
266
444
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return false;
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
throw new Error(`Langfuse trace visibility check failed with HTTP ${response.status}`);
|
|
270
447
|
}
|
|
271
448
|
return true;
|
|
272
|
-
} catch {
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (signal.aborted) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
273
453
|
return false;
|
|
274
454
|
}
|
|
275
455
|
}
|
|
276
456
|
|
|
277
|
-
async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string): Promise<boolean> {
|
|
457
|
+
async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string, signal: AbortSignal): Promise<boolean> {
|
|
278
458
|
const deadline = Date.now() + OTEL_VISIBILITY_TIMEOUT_MS;
|
|
279
459
|
|
|
280
460
|
while (true) {
|
|
281
|
-
if (await traceExists(rt, traceId)) {
|
|
461
|
+
if (await traceExists(rt, traceId, signal)) {
|
|
282
462
|
return true;
|
|
283
463
|
}
|
|
284
464
|
|
|
@@ -287,7 +467,7 @@ async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string): Pro
|
|
|
287
467
|
return false;
|
|
288
468
|
}
|
|
289
469
|
|
|
290
|
-
await delay(Math.min(OTEL_VISIBILITY_POLL_INTERVAL_MS, remainingMs));
|
|
470
|
+
await delay(Math.min(OTEL_VISIBILITY_POLL_INTERVAL_MS, remainingMs), signal);
|
|
291
471
|
}
|
|
292
472
|
}
|
|
293
473
|
|
|
@@ -295,14 +475,14 @@ function eventTimestamp(record: { endTime?: string; startTime?: string; timestam
|
|
|
295
475
|
return record.endTime ?? record.startTime ?? record.timestamp ?? nowIso();
|
|
296
476
|
}
|
|
297
477
|
|
|
298
|
-
async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
478
|
+
async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal) {
|
|
299
479
|
const store = rt.restFallback as RestFallbackStore | undefined;
|
|
300
480
|
if (!store?.trace || store.attempted) {
|
|
301
481
|
return;
|
|
302
482
|
}
|
|
303
483
|
store.attempted = true;
|
|
304
484
|
|
|
305
|
-
if (await waitForTraceVisibility(rt, store.trace.id)) {
|
|
485
|
+
if (await waitForTraceVisibility(rt, store.trace.id, signal)) {
|
|
306
486
|
return;
|
|
307
487
|
}
|
|
308
488
|
|
|
@@ -355,31 +535,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
355
535
|
});
|
|
356
536
|
}
|
|
357
537
|
|
|
358
|
-
const
|
|
359
|
-
if (!ingestionApi?.batch) {
|
|
360
|
-
debugLog("📊 Langfuse: REST fallback ingestion is unavailable");
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const response = await withTimeout(
|
|
365
|
-
"REST fallback ingestion",
|
|
366
|
-
ingestionApi.batch({
|
|
367
|
-
batch,
|
|
368
|
-
metadata: {
|
|
369
|
-
source: "pi-langfuse",
|
|
370
|
-
fallback: "rest-ingestion",
|
|
371
|
-
reason: "otel-trace-not-visible-after-flush",
|
|
372
|
-
},
|
|
373
|
-
}),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
if (!response) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
381
|
-
const responseErrors = responseBody?.errors;
|
|
382
|
-
const errors = Array.isArray(responseErrors) ? responseErrors : [];
|
|
538
|
+
const errors = await ingestBatch(rt, batch, signal);
|
|
383
539
|
if (errors.length > 0) {
|
|
384
540
|
rememberRuntimeError("REST fallback ingestion", new Error(JSON.stringify(errors)));
|
|
385
541
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
@@ -426,6 +582,11 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
426
582
|
|
|
427
583
|
try {
|
|
428
584
|
ensureOtelContextManager(context, AsyncHooksContextManager);
|
|
585
|
+
const scoreFlushAt = resolvePositiveEnvNumber("LANGFUSE_FLUSH_AT", DEFAULT_SCORE_FLUSH_AT, true);
|
|
586
|
+
const scoreFlushIntervalMs =
|
|
587
|
+
resolvePositiveEnvNumber("LANGFUSE_FLUSH_INTERVAL", DEFAULT_SCORE_FLUSH_INTERVAL_MS / 1_000) * 1_000;
|
|
588
|
+
const scoreRequestTimeoutMs =
|
|
589
|
+
resolvePositiveEnvNumber("LANGFUSE_TIMEOUT", DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS) * 1_000;
|
|
429
590
|
const spanProcessor = new LangfuseSpanProcessor({
|
|
430
591
|
publicKey: state.config.publicKey,
|
|
431
592
|
secretKey: state.config.secretKey,
|
|
@@ -449,6 +610,16 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
449
610
|
tracerProvider,
|
|
450
611
|
clearTracerProvider: () => tracing.setLangfuseTracerProvider(null),
|
|
451
612
|
restFallback,
|
|
613
|
+
pendingScores: [],
|
|
614
|
+
scoreFlushAt,
|
|
615
|
+
scoreFlushIntervalMs,
|
|
616
|
+
scoreRequestTimeoutMs,
|
|
617
|
+
scoreFlushStopped: false,
|
|
618
|
+
runtimeConfig: {
|
|
619
|
+
publicKey: state.config.publicKey,
|
|
620
|
+
secretKey: state.config.secretKey,
|
|
621
|
+
host: state.config.host,
|
|
622
|
+
},
|
|
452
623
|
};
|
|
453
624
|
lastRuntimeError = null;
|
|
454
625
|
} catch (e) {
|
|
@@ -468,17 +639,36 @@ function doShutdownRuntime(): Promise<void> {
|
|
|
468
639
|
|
|
469
640
|
const rt = runtime;
|
|
470
641
|
runtime = null;
|
|
642
|
+
const deadline = Date.now() + shutdownStepTimeoutMs;
|
|
643
|
+
const controller = new AbortController();
|
|
644
|
+
const abortTimeout = setTimeout(() => controller.abort(), shutdownStepTimeoutMs);
|
|
645
|
+
stopScoreFlush(rt);
|
|
471
646
|
|
|
472
647
|
try {
|
|
473
|
-
await
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
648
|
+
await withShutdownDeadline(
|
|
649
|
+
"Active score flush",
|
|
650
|
+
() => rt.scoreFlushPromise,
|
|
651
|
+
deadline,
|
|
652
|
+
);
|
|
653
|
+
await withShutdownDeadline("OTel force flush", () => rt.tracerProvider?.forceFlush?.(), deadline);
|
|
654
|
+
await withShutdownDeadline(
|
|
655
|
+
"REST fallback ingestion",
|
|
656
|
+
() => fallbackToRestIngestion(rt, controller.signal),
|
|
657
|
+
deadline,
|
|
658
|
+
);
|
|
659
|
+
await flushPendingScores(rt, controller.signal);
|
|
660
|
+
await withShutdownDeadline("Langfuse score flush", () => rt.scoreClient.flush?.(), deadline);
|
|
661
|
+
await withShutdownDeadline("Langfuse client shutdown", () => rt.scoreClient.shutdown?.(), deadline);
|
|
662
|
+
await withShutdownDeadline("OTel tracer shutdown", () => rt.tracerProvider?.shutdown?.(), deadline);
|
|
478
663
|
} catch (e) {
|
|
479
664
|
rememberRuntimeError("runtime shutdown", e);
|
|
480
665
|
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
481
666
|
} finally {
|
|
667
|
+
clearTimeout(abortTimeout);
|
|
668
|
+
clearScoreFlushTimer(rt);
|
|
669
|
+
rt.scoreFlushController?.abort();
|
|
670
|
+
rt.scoreFlushController = undefined;
|
|
671
|
+
rt.scoreFlushPromise = undefined;
|
|
482
672
|
if (!runtime) {
|
|
483
673
|
rt.clearTracerProvider?.();
|
|
484
674
|
}
|
|
@@ -516,7 +706,13 @@ export async function forceShutdownRuntime(): Promise<void> {
|
|
|
516
706
|
}
|
|
517
707
|
|
|
518
708
|
export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS): void {
|
|
709
|
+
if (runtime && runtime !== rt) {
|
|
710
|
+
stopScoreFlush(runtime);
|
|
711
|
+
}
|
|
519
712
|
runtime = rt;
|
|
713
|
+
if (rt) {
|
|
714
|
+
rt.scoreFlushStopped = false;
|
|
715
|
+
}
|
|
520
716
|
shutdownStepTimeoutMs = timeoutMs;
|
|
521
717
|
activeSessions.clear();
|
|
522
718
|
}
|
|
@@ -524,14 +720,35 @@ export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFA
|
|
|
524
720
|
export async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
525
721
|
try {
|
|
526
722
|
const rt = await getRuntime();
|
|
527
|
-
|
|
723
|
+
const score: PendingScore = {
|
|
528
724
|
name,
|
|
529
725
|
value,
|
|
530
726
|
dataType: name === "session_had_errors" || name === "tool_is_error" ? "BOOLEAN" : "NUMERIC",
|
|
531
727
|
traceId: options.traceId,
|
|
532
728
|
observationId: options.observationId,
|
|
533
729
|
sessionId: options.traceId ? undefined : state.currentSessionId || undefined,
|
|
534
|
-
|
|
730
|
+
...(process.env.LANGFUSE_TRACING_ENVIRONMENT
|
|
731
|
+
? { environment: process.env.LANGFUSE_TRACING_ENVIRONMENT }
|
|
732
|
+
: {}),
|
|
733
|
+
};
|
|
734
|
+
if (!rt.pendingScores) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (rt.pendingScores.length >= MAX_SCORE_QUEUE_SIZE) {
|
|
738
|
+
const error = new Error(
|
|
739
|
+
`Langfuse score queue is full (${MAX_SCORE_QUEUE_SIZE}); dropping score`,
|
|
740
|
+
);
|
|
741
|
+
rememberRuntimeError("score queue", error);
|
|
742
|
+
console.warn(`📊 Langfuse: ${error.message}`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
rt.pendingScores.push(score);
|
|
747
|
+
if (rt.pendingScores.length >= (rt.scoreFlushAt ?? DEFAULT_SCORE_FLUSH_AT)) {
|
|
748
|
+
void startScoreFlush(rt);
|
|
749
|
+
} else {
|
|
750
|
+
scheduleScoreFlush(rt);
|
|
751
|
+
}
|
|
535
752
|
} catch (e) {
|
|
536
753
|
rememberRuntimeError(`score ${name}`, e);
|
|
537
754
|
console.warn(`📊 Langfuse: Failed to send score ${name}`, e);
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,22 @@ export interface LangfuseScoreClient {
|
|
|
59
59
|
shutdown?: () => Promise<void>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface PendingScore {
|
|
63
|
+
traceId?: string;
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
observationId?: string;
|
|
66
|
+
name: string;
|
|
67
|
+
value: number;
|
|
68
|
+
dataType?: "NUMERIC" | "BOOLEAN";
|
|
69
|
+
environment?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LangfuseRuntimeConfig {
|
|
73
|
+
publicKey: string;
|
|
74
|
+
secretKey: string;
|
|
75
|
+
host: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
export interface LangfuseRuntime {
|
|
63
79
|
startObservation: (
|
|
64
80
|
name: string,
|
|
@@ -79,6 +95,15 @@ export interface LangfuseRuntime {
|
|
|
79
95
|
tracerProvider?: { forceFlush?: () => Promise<void>; shutdown?: () => Promise<void> };
|
|
80
96
|
clearTracerProvider?: () => void;
|
|
81
97
|
restFallback?: unknown;
|
|
98
|
+
pendingScores?: PendingScore[];
|
|
99
|
+
scoreFlushAt?: number;
|
|
100
|
+
scoreFlushIntervalMs?: number;
|
|
101
|
+
scoreRequestTimeoutMs?: number;
|
|
102
|
+
scoreFlushTimer?: NodeJS.Timeout;
|
|
103
|
+
scoreFlushPromise?: Promise<void>;
|
|
104
|
+
scoreFlushController?: AbortController;
|
|
105
|
+
scoreFlushStopped?: boolean;
|
|
106
|
+
runtimeConfig?: LangfuseRuntimeConfig;
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
export interface GenerationState {
|