@oxygen-agent/cli 1.987.20 → 1.1010.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/README.md +1 -1
- package/dist/admin-primary-providers-render.d.ts +0 -2
- package/dist/admin-primary-providers-render.js +1 -1
- package/dist/browser-login.js +1 -4
- package/dist/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.d.ts +3 -2
- package/dist/command-manifest.js +25 -2
- package/dist/credentials.d.ts +1 -1
- package/dist/functions-commands.js +13 -9
- package/dist/help.d.ts +8 -0
- package/dist/help.js +46 -0
- package/dist/index.js +2751 -242
- package/dist/knowledge-mirror.d.ts +2 -2
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +1 -1
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/session.d.ts +4 -3
- package/dist/skills.d.ts +8 -7
- package/dist/skills.js +58 -20
- package/dist/transcript.d.ts +2 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +14 -2
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
- package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
- package/node_modules/@oxygen/formula/dist/hash.js +199 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -22
- package/node_modules/@oxygen/shared/dist/billing.js +195 -40
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +311 -28
- package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +62 -0
- package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +548 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
- package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +107 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +809 -0
- package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
- package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
- package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
- package/node_modules/@oxygen/shared/dist/index.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/index.js +15 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +416 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +361 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +6 -1
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +79 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +366 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
- package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +36 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +36 -1
- package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +115 -5
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
- package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
- package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +28 -2
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
- package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
- package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.js +8 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
- package/node_modules/@oxygen/shared/package.json +59 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
- package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
- package/package.json +3 -2
|
@@ -36,6 +36,8 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
36
36
|
import { OXYGEN_VERSION } from "./version.js";
|
|
37
37
|
import { prepareLlmPayload } from "./llm-payload.js";
|
|
38
38
|
import { log } from "./log.js";
|
|
39
|
+
import { llmPromptLabel, registeredLlmPrompt } from "./llm-prompts.js";
|
|
40
|
+
import { activeTelemetryTraceId } from "./telemetry.js";
|
|
39
41
|
const FLUSH_TIMEOUT_MS = 5_000;
|
|
40
42
|
const WARN_THROTTLE_MS = 30_000;
|
|
41
43
|
/**
|
|
@@ -73,6 +75,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
|
|
|
73
75
|
const explicit = env.LANGFUSE_TRACING_ENVIRONMENT?.trim().toLowerCase();
|
|
74
76
|
if (explicit)
|
|
75
77
|
return explicit.replace(/[^a-z0-9_-]/g, "-");
|
|
78
|
+
// Self-hosted first: those processes carry neither VERCEL_ENV nor a Fly app
|
|
79
|
+
// name, so without this every self-hosted tier would report "development".
|
|
80
|
+
// selfhost-shadow stays "development" deliberately — production-shaped data,
|
|
81
|
+
// not production traffic.
|
|
82
|
+
const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
|
|
83
|
+
if (deployEnv === "selfhost-prod")
|
|
84
|
+
return "production";
|
|
85
|
+
if (deployEnv === "selfhost-shadow" || deployEnv === "selfhost-dev")
|
|
86
|
+
return "development";
|
|
76
87
|
if (env.VERCEL_ENV)
|
|
77
88
|
return env.VERCEL_ENV === "production" ? "production" : "development";
|
|
78
89
|
const flyApp = env.FLY_APP_NAME ?? env.FLY_APP;
|
|
@@ -90,6 +101,15 @@ export function resolveLlmTracingEnvironment(env = process.env) {
|
|
|
90
101
|
* the web app nor the worker.
|
|
91
102
|
*/
|
|
92
103
|
export function resolveLlmTracingServiceName(env = process.env) {
|
|
104
|
+
const deployEnv = env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase();
|
|
105
|
+
if (deployEnv === "selfhost-dev" || deployEnv === "selfhost-prod" || deployEnv === "selfhost-shadow") {
|
|
106
|
+
if (env.OXYGEN_PROCESS_ROLE === "worker") {
|
|
107
|
+
return deployEnv === "selfhost-prod" ? "oxygen-worker" : "oxygen-worker-dev";
|
|
108
|
+
}
|
|
109
|
+
if (env.OXYGEN_PROCESS_ROLE === "web" || env.OXYGEN_SERVICE_NAME === "oxygen-web")
|
|
110
|
+
return "oxygen-web";
|
|
111
|
+
return "oxygen-local";
|
|
112
|
+
}
|
|
93
113
|
const flyApp = env.FLY_APP_NAME?.trim() || env.FLY_APP?.trim();
|
|
94
114
|
if (flyApp)
|
|
95
115
|
return flyApp.includes("-dev") ? "oxygen-worker-dev" : "oxygen-worker";
|
|
@@ -119,6 +139,9 @@ export function llmTraceIdForSeed(seed) {
|
|
|
119
139
|
function rootSpanIdForSeed(seed) {
|
|
120
140
|
return createHash("sha256").update(`langfuse-root:${seed}`, "utf8").digest("hex").slice(0, 16);
|
|
121
141
|
}
|
|
142
|
+
function observationSpanId(parts) {
|
|
143
|
+
return createHash("sha256").update(JSON.stringify(parts), "utf8").digest("hex").slice(0, 16);
|
|
144
|
+
}
|
|
122
145
|
// A score's `comment` is free text (a thumbs-down reason is user-authored and
|
|
123
146
|
// unbounded) and the API types it as a STRING, so it cannot take
|
|
124
147
|
// a recoverable payload envelope. It gets the same
|
|
@@ -139,9 +162,57 @@ function compact(body) {
|
|
|
139
162
|
}
|
|
140
163
|
return out;
|
|
141
164
|
}
|
|
165
|
+
// --- Export summary ----------------------------------------------------------
|
|
166
|
+
// ingest_failed is throttled per process, so it is a floor on failures and says
|
|
167
|
+
// nothing about the healthy volume beside it. This rollup gives the ratio: how
|
|
168
|
+
// many spans Langfuse acknowledged, how many batches it refused, and how many
|
|
169
|
+
// observations never reached the exporter at all. Counts only; never content.
|
|
170
|
+
const EXPORT_SUMMARY_INTERVAL_MS = 60_000;
|
|
171
|
+
export function createLlmExportSummary(now = Date.now) {
|
|
172
|
+
let exported = 0;
|
|
173
|
+
let failed = 0;
|
|
174
|
+
let dropped = 0;
|
|
175
|
+
let windowStartedAt = now();
|
|
176
|
+
const report = () => {
|
|
177
|
+
const at = now();
|
|
178
|
+
if (exported + failed + dropped > 0) {
|
|
179
|
+
log("info", "llm_tracing.export_summary", {
|
|
180
|
+
provider: "langfuse",
|
|
181
|
+
exported,
|
|
182
|
+
failed,
|
|
183
|
+
dropped,
|
|
184
|
+
window_seconds: Math.max(0, Math.round((at - windowStartedAt) / 1000)),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
exported = 0;
|
|
188
|
+
failed = 0;
|
|
189
|
+
dropped = 0;
|
|
190
|
+
windowStartedAt = at;
|
|
191
|
+
};
|
|
192
|
+
const add = (count) => (Number.isFinite(count) && count > 0 ? Math.floor(count) : 0);
|
|
193
|
+
return {
|
|
194
|
+
exported: (count) => { exported += add(count); },
|
|
195
|
+
failed: (count) => { failed += add(count); },
|
|
196
|
+
dropped: (count) => { dropped += add(count); },
|
|
197
|
+
maybeReport: () => { if (now() - windowStartedAt >= EXPORT_SUMMARY_INTERVAL_MS)
|
|
198
|
+
report(); },
|
|
199
|
+
reportNow: report,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A thrown value's class name is the only part of it that may leave the
|
|
204
|
+
* private store: messages from payload preparation can quote the payload.
|
|
205
|
+
*/
|
|
206
|
+
function sanitizedErrorReason(error) {
|
|
207
|
+
const name = error instanceof Error ? error.name : typeof error;
|
|
208
|
+
return /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ? name : "unknown";
|
|
209
|
+
}
|
|
142
210
|
function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
|
|
143
211
|
return new Promise((resolve) => {
|
|
144
|
-
const timer = setTimeout(
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
warn(new Error("langfuse_flush_deadline_exceeded"));
|
|
214
|
+
resolve();
|
|
215
|
+
}, timeoutMs);
|
|
145
216
|
timer.unref?.();
|
|
146
217
|
rejectable
|
|
147
218
|
.catch((error) => warn(error))
|
|
@@ -152,17 +223,133 @@ function boundedNever(rejectable, warn, timeoutMs = FLUSH_TIMEOUT_MS) {
|
|
|
152
223
|
});
|
|
153
224
|
}
|
|
154
225
|
/** Lazy, private, always-sampled OTel transport. Never register it globally. */
|
|
155
|
-
function createOtelEmitter(env, warn) {
|
|
226
|
+
function createOtelEmitter(env, warn, summary) {
|
|
156
227
|
let handle = null;
|
|
157
228
|
const init = () => {
|
|
158
229
|
handle ??= (async () => {
|
|
159
230
|
try {
|
|
160
|
-
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core] = await Promise.all([
|
|
231
|
+
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core, { JsonTraceSerializer }] = await Promise.all([
|
|
161
232
|
import("@langfuse/otel"), import("@opentelemetry/sdk-trace-base"),
|
|
162
233
|
import("@opentelemetry/resources"),
|
|
163
234
|
import("@langfuse/tracing"), import("@opentelemetry/api"), import("@langfuse/core"),
|
|
235
|
+
import("@opentelemetry/otlp-transformer"),
|
|
164
236
|
]);
|
|
237
|
+
const exportUrl = `${(env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "")}/api/public/otel/v1/traces`;
|
|
238
|
+
const exportHeaders = {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
Authorization: `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`,
|
|
241
|
+
"x-langfuse-sdk-name": "javascript",
|
|
242
|
+
"x-langfuse-sdk-version": core.LANGFUSE_SDK_VERSION,
|
|
243
|
+
"x-langfuse-public-key": env.LANGFUSE_PUBLIC_KEY,
|
|
244
|
+
};
|
|
245
|
+
const pendingExports = new Set();
|
|
246
|
+
// The upstream HTTP exporter logs raw partial-success/malformed response
|
|
247
|
+
// bodies through GLOBAL diagnostics before its callback runs. Keep the
|
|
248
|
+
// supported OTLP JSON serializer, but own delivery and response handling
|
|
249
|
+
// inside this private provider. Never mutate the global diagnostic logger.
|
|
250
|
+
const send = async (spans) => {
|
|
251
|
+
const encoded = JsonTraceSerializer.serializeRequest(spans);
|
|
252
|
+
if (!encoded)
|
|
253
|
+
throw new Error("serialization_failed");
|
|
254
|
+
const body = new TextDecoder().decode(encoded);
|
|
255
|
+
const signal = AbortSignal.timeout(LANGFUSE_EXPORT_TIMEOUT_SECONDS * 1000);
|
|
256
|
+
let response;
|
|
257
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
response = await fetch(exportUrl, { method: "POST", headers: exportHeaders, body, signal, redirect: "error" });
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
if (signal.aborted)
|
|
263
|
+
throw new Error("deadline_exceeded");
|
|
264
|
+
if (attempt === 2)
|
|
265
|
+
throw new Error("network_failed");
|
|
266
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (![429, 502, 503, 504].includes(response.status) || attempt === 2)
|
|
270
|
+
break;
|
|
271
|
+
// A retryable rejection has no useful private response to retain.
|
|
272
|
+
await response.body?.cancel().catch(() => { });
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
274
|
+
}
|
|
275
|
+
if (!response?.ok) {
|
|
276
|
+
await response?.body?.cancel().catch(() => { });
|
|
277
|
+
throw new Error("http_rejected");
|
|
278
|
+
}
|
|
279
|
+
const chunks = [];
|
|
280
|
+
let bytes = 0;
|
|
281
|
+
const reader = response.body?.getReader();
|
|
282
|
+
if (reader) {
|
|
283
|
+
try {
|
|
284
|
+
for (;;) {
|
|
285
|
+
const next = await reader.read();
|
|
286
|
+
if (next.done)
|
|
287
|
+
break;
|
|
288
|
+
bytes += next.value.byteLength;
|
|
289
|
+
// OTLP acknowledgements are tiny; an echoed payload must never
|
|
290
|
+
// become an unbounded allocation or reach any diagnostic logger.
|
|
291
|
+
if (bytes > 64 * 1024)
|
|
292
|
+
throw new Error("response_too_large");
|
|
293
|
+
chunks.push(next.value);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
await reader.cancel().catch(() => { });
|
|
298
|
+
throw new Error(error instanceof Error && error.message === "response_too_large" ? "response_too_large" : "response_read_failed");
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
reader.releaseLock();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let acknowledgement;
|
|
305
|
+
try {
|
|
306
|
+
acknowledgement = bytes ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : {};
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
throw new Error("response_invalid");
|
|
310
|
+
}
|
|
311
|
+
if (!acknowledgement || typeof acknowledgement !== "object" || Array.isArray(acknowledgement))
|
|
312
|
+
throw new Error("response_invalid");
|
|
313
|
+
const envelope = acknowledgement;
|
|
314
|
+
const partial = envelope.partialSuccess ?? envelope.partial_success;
|
|
315
|
+
if (partial !== undefined && partial !== null) {
|
|
316
|
+
if (typeof partial !== "object" || Array.isArray(partial))
|
|
317
|
+
throw new Error("response_invalid");
|
|
318
|
+
const rawRejected = partial.rejectedSpans ?? partial.rejected_spans ?? 0;
|
|
319
|
+
if ((typeof rawRejected !== "number" && typeof rawRejected !== "string") || !/^\d+$/.test(String(rawRejected)))
|
|
320
|
+
throw new Error("response_invalid");
|
|
321
|
+
if (Number(rawRejected) > 0)
|
|
322
|
+
throw new Error("partial_rejection");
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const privateExporter = {
|
|
326
|
+
export: (spans, done) => {
|
|
327
|
+
const pending = send(spans).then(() => {
|
|
328
|
+
// Complete the batch first: a logging fault must never leave the
|
|
329
|
+
// processor waiting out its own export timeout.
|
|
330
|
+
done({ code: 0 });
|
|
331
|
+
summary.exported(spans.length);
|
|
332
|
+
summary.maybeReport();
|
|
333
|
+
}, (error) => {
|
|
334
|
+
const failures = new Set(["serialization_failed", "deadline_exceeded", "network_failed", "http_rejected", "response_too_large", "response_read_failed", "response_invalid", "partial_rejection"]);
|
|
335
|
+
const failureKind = error instanceof Error && failures.has(error.message) ? error.message : "transport_failed";
|
|
336
|
+
// BSP forwards failures to the global OTel error handler. It only
|
|
337
|
+
// receives our constant classification, never the server body.
|
|
338
|
+
done({ code: 1, error: new Error("langfuse_delivery_failed") });
|
|
339
|
+
summary.failed(spans.length);
|
|
340
|
+
summary.maybeReport();
|
|
341
|
+
warn(error, { stage: "export", observation_count: spans.length, failure_kind: failureKind });
|
|
342
|
+
});
|
|
343
|
+
pendingExports.add(pending);
|
|
344
|
+
// Even an unexpected SDK callback failure must not create a rejected
|
|
345
|
+
// fire-and-forget promise in the product process.
|
|
346
|
+
void pending.then(() => pendingExports.delete(pending), () => pendingExports.delete(pending));
|
|
347
|
+
},
|
|
348
|
+
shutdown: async () => { await Promise.allSettled([...pendingExports]); },
|
|
349
|
+
forceFlush: async () => { await Promise.allSettled([...pendingExports]); },
|
|
350
|
+
};
|
|
165
351
|
const processor = new LangfuseSpanProcessor({
|
|
352
|
+
exporter: privateExporter,
|
|
166
353
|
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
|
167
354
|
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
168
355
|
...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
|
|
@@ -258,7 +445,19 @@ function createOtelEmitter(env, warn) {
|
|
|
258
445
|
return handle;
|
|
259
446
|
};
|
|
260
447
|
return {
|
|
261
|
-
emit: (emission) => {
|
|
448
|
+
emit: (emission) => {
|
|
449
|
+
void init().then((h) => {
|
|
450
|
+
// init already warned once; every later emission is lost the same way.
|
|
451
|
+
// Report from here too: a process whose transport never came up has
|
|
452
|
+
// no exports to carry the window, and that is when dropped matters.
|
|
453
|
+
if (!h) {
|
|
454
|
+
summary.dropped(1);
|
|
455
|
+
summary.maybeReport();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
h.emit(emission);
|
|
459
|
+
}).catch((error) => { summary.dropped(1); summary.maybeReport(); warn(error, { stage: emission.kind }); });
|
|
460
|
+
},
|
|
262
461
|
flush: () => init().then((h) => h?.processor.forceFlush() ?? Promise.resolve()),
|
|
263
462
|
shutdown: () => init().then((h) => h?.processor.shutdown() ?? Promise.resolve()),
|
|
264
463
|
};
|
|
@@ -338,6 +537,126 @@ function createApiScorer(env, warn) {
|
|
|
338
537
|
},
|
|
339
538
|
};
|
|
340
539
|
}
|
|
540
|
+
const NOOP_PROMPT_RESOLVER = { versionFor: () => undefined };
|
|
541
|
+
const PROMPT_FOUND_TTL_MS = 24 * 60 * 60 * 1000;
|
|
542
|
+
const PROMPT_MISSING_TTL_MS = 10 * 60 * 1000;
|
|
543
|
+
const PROMPT_ERROR_TTL_MS = 60 * 1000;
|
|
544
|
+
const PROMPT_CACHE_LIMIT = 512;
|
|
545
|
+
function promptMetadata(prompt) {
|
|
546
|
+
return prompt
|
|
547
|
+
? { oxygen_prompt_name: prompt.name, oxygen_prompt_hash: prompt.hash, oxygen_prompt_registered: prompt.registered }
|
|
548
|
+
: {};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Only a deployed dev process talking to the self-hosted instance may create
|
|
552
|
+
* prompt versions. Production and shadow data never do (the environment label
|
|
553
|
+
* is "production" there, or the host is Langfuse Cloud), and a local process
|
|
554
|
+
* would publish whatever uncommitted template its checkout holds.
|
|
555
|
+
*/
|
|
556
|
+
function mayPublishMissingPrompts(env, baseUrl) {
|
|
557
|
+
if (resolveLlmTracingEnvironment(env) !== "development")
|
|
558
|
+
return false;
|
|
559
|
+
if (resolveLlmTracingServiceName(env) === "oxygen-local")
|
|
560
|
+
return false;
|
|
561
|
+
if (env.OXYGEN_DEPLOY_ENV?.trim().toLowerCase() !== "selfhost-dev")
|
|
562
|
+
return false;
|
|
563
|
+
let host;
|
|
564
|
+
try {
|
|
565
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
return host !== "langfuse.com" && !host.endsWith(".langfuse.com");
|
|
571
|
+
}
|
|
572
|
+
export function createApiPromptResolver(env, warn, fetchImpl = fetch, now = Date.now) {
|
|
573
|
+
const baseUrl = (env.LANGFUSE_BASE_URL?.trim() || "https://cloud.langfuse.com").replace(/\/+$/, "");
|
|
574
|
+
const authorization = `Basic ${Buffer.from(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`).toString("base64")}`;
|
|
575
|
+
const cache = new Map();
|
|
576
|
+
const inflight = new Set();
|
|
577
|
+
const publishAttempted = new Set();
|
|
578
|
+
const publishes = mayPublishMissingPrompts(env, baseUrl);
|
|
579
|
+
// One attempt per template per process; any failure falls back to metadata only.
|
|
580
|
+
const publish = async (key, prompt) => {
|
|
581
|
+
const definition = publishes && !publishAttempted.has(key) ? registeredLlmPrompt(prompt) : undefined;
|
|
582
|
+
if (!definition)
|
|
583
|
+
return undefined;
|
|
584
|
+
publishAttempted.add(key);
|
|
585
|
+
const label = llmPromptLabel(prompt.hash);
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts`, {
|
|
588
|
+
method: "POST",
|
|
589
|
+
headers: { Authorization: authorization, "Content-Type": "application/json" },
|
|
590
|
+
body: JSON.stringify({
|
|
591
|
+
type: "text", name: definition.name, prompt: definition.template, labels: [label],
|
|
592
|
+
tags: ["oxygen", "code-owned"], commitMessage: `oxygen code template ${prompt.hash} (runtime publish)`,
|
|
593
|
+
}),
|
|
594
|
+
signal: AbortSignal.timeout(5_000),
|
|
595
|
+
redirect: "error",
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
await response.body?.cancel().catch(() => { });
|
|
599
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish", status: response.status });
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
const body = await response.json();
|
|
603
|
+
const labels = Array.isArray(body.labels) ? body.labels : [];
|
|
604
|
+
return typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label) ? body.version : undefined;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_publish" });
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const lookup = async (key, prompt) => {
|
|
612
|
+
const label = llmPromptLabel(prompt.hash);
|
|
613
|
+
let entry = { version: undefined, expiresAt: now() + PROMPT_ERROR_TTL_MS };
|
|
614
|
+
try {
|
|
615
|
+
const response = await fetchImpl(`${baseUrl}/api/public/v2/prompts/${encodeURIComponent(prompt.name)}?label=${encodeURIComponent(label)}`, { headers: { Authorization: authorization }, signal: AbortSignal.timeout(5_000), redirect: "error" });
|
|
616
|
+
if (response.status === 404) {
|
|
617
|
+
await response.body?.cancel().catch(() => { });
|
|
618
|
+
const version = await publish(key, prompt);
|
|
619
|
+
entry = version === undefined
|
|
620
|
+
? { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS }
|
|
621
|
+
: { version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
|
|
622
|
+
}
|
|
623
|
+
else if (!response.ok) {
|
|
624
|
+
await response.body?.cancel().catch(() => { });
|
|
625
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link", status: response.status });
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const body = await response.json();
|
|
629
|
+
const labels = Array.isArray(body.labels) ? body.labels : [];
|
|
630
|
+
if (typeof body.version === "number" && Number.isInteger(body.version) && labels.includes(label)) {
|
|
631
|
+
entry = { version: body.version, expiresAt: now() + PROMPT_FOUND_TTL_MS };
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
entry = { version: undefined, expiresAt: now() + PROMPT_MISSING_TTL_MS };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
warn(new Error("langfuse_prompt_link_failed"), { stage: "prompt_link" });
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
if (cache.size >= PROMPT_CACHE_LIMIT)
|
|
643
|
+
cache.delete(cache.keys().next().value);
|
|
644
|
+
cache.set(key, entry);
|
|
645
|
+
inflight.delete(key);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
return {
|
|
649
|
+
versionFor: (prompt) => {
|
|
650
|
+
const key = `${prompt.name}\u0000${prompt.hash}`;
|
|
651
|
+
const cached = cache.get(key);
|
|
652
|
+
if ((!cached || cached.expiresAt <= now()) && !inflight.has(key)) {
|
|
653
|
+
inflight.add(key);
|
|
654
|
+
void lookup(key, prompt);
|
|
655
|
+
}
|
|
656
|
+
return cached?.version;
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
341
660
|
/**
|
|
342
661
|
* Construct a fail-open Langfuse client, or `null` when tracing is disabled or
|
|
343
662
|
* misconfigured. Prefer the process-wide `getLlmTracingClient` in app code;
|
|
@@ -346,24 +665,65 @@ function createApiScorer(env, warn) {
|
|
|
346
665
|
export function createLlmTracingClient(env = process.env, options) {
|
|
347
666
|
if (!options?.emitterImpl && !isLlmTracingEnabled(env))
|
|
348
667
|
return null;
|
|
349
|
-
|
|
668
|
+
const lastWarnAtMs = new Map();
|
|
350
669
|
const warn = (error, context) => {
|
|
351
670
|
const nowMs = Date.now();
|
|
352
|
-
|
|
671
|
+
const errorCode = error instanceof Error && ["langfuse_flush_deadline_exceeded", "langfuse_replay_identity_unavailable", "langfuse_prompt_link_failed", "langfuse_observation_build_failed"].includes(error.message)
|
|
672
|
+
? error.message : "langfuse_delivery_failed";
|
|
673
|
+
// An identity gap must not suppress a later actual delivery failure.
|
|
674
|
+
const warningKey = `${errorCode}:${context?.stage ?? "unknown"}`;
|
|
675
|
+
if (nowMs - (lastWarnAtMs.get(warningKey) ?? -Infinity) < WARN_THROTTLE_MS)
|
|
353
676
|
return;
|
|
354
|
-
lastWarnAtMs
|
|
677
|
+
lastWarnAtMs.set(warningKey, nowMs);
|
|
355
678
|
log("warn", "llm_tracing.ingest_failed", {
|
|
356
679
|
provider: "langfuse",
|
|
357
|
-
|
|
680
|
+
// Exporter/API error messages can contain the rejected request or server
|
|
681
|
+
// response. Only this fixed classification may leave the private store.
|
|
682
|
+
error_code: errorCode,
|
|
358
683
|
...context,
|
|
359
684
|
});
|
|
360
685
|
};
|
|
361
|
-
|
|
686
|
+
// Opt-in per environment; managed projects retain their existing append
|
|
687
|
+
// semantics. A real execution is identified by the caller's immutable start
|
|
688
|
+
// plus logical id, never by the logical id alone (approval/tool ids can recur).
|
|
689
|
+
const stableIdentity = env.LANGFUSE_OBSERVATION_IDENTITY === "stable-v1";
|
|
690
|
+
const physicalId = (body, kind) => {
|
|
691
|
+
if (!stableIdentity)
|
|
692
|
+
return undefined;
|
|
693
|
+
// Verified ledger-backed control hooks supply the exact durable dedupe key.
|
|
694
|
+
// It already defines identity in the authoritative session/run ledger, so
|
|
695
|
+
// identity hashing needs no additional clock. The durable capture adapter
|
|
696
|
+
// separately supplies the original ledger timestamp for v3's date key.
|
|
697
|
+
const durableEventKey = body.metadata?.oxygen_durable_event_key;
|
|
698
|
+
if (kind === "event" && typeof durableEventKey === "string" && durableEventKey.length > 0) {
|
|
699
|
+
return observationSpanId(["langfuse-durable-event-v1", body.traceId, body.name, durableEventKey]);
|
|
700
|
+
}
|
|
701
|
+
// Approval ids name a durable decision/request across invocation slices.
|
|
702
|
+
// Other event keys are not universally unique; do not infer that guarantee.
|
|
703
|
+
if (kind === "event" && /^apr:(requested|decided):.+/.test(body.id)) {
|
|
704
|
+
return observationSpanId(["langfuse-event-v1", body.traceId, body.name, body.id]);
|
|
705
|
+
}
|
|
706
|
+
if (!body.startTime || !Number.isFinite(body.startTime.getTime())) {
|
|
707
|
+
warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: kind });
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
return observationSpanId(["langfuse-observation-v1", body.traceId, kind, body.name, body.id,
|
|
711
|
+
body.startTime.toISOString(), body.metadata?.attempt_id ?? null, body.metadata?.call_id ?? null]);
|
|
712
|
+
};
|
|
713
|
+
const summary = createLlmExportSummary();
|
|
714
|
+
const transport = options?.emitterImpl ?? createOtelEmitter(env, warn, summary);
|
|
362
715
|
const emitter = {
|
|
363
716
|
...transport,
|
|
364
717
|
emit: (emission) => {
|
|
365
718
|
const observationId = emission.observationId ?? randomBytes(8).toString("hex");
|
|
366
719
|
const metadata = { ...emission.attributes.metadata, oxygen_capture_version: 2 };
|
|
720
|
+
// Cross-link to the operational trace (SigNoz/Axiom). Read here, still in
|
|
721
|
+
// the caller's synchronous context; the private provider never sees it.
|
|
722
|
+
if (metadata.otel_trace_id === undefined) {
|
|
723
|
+
const otelTraceId = activeTelemetryTraceId();
|
|
724
|
+
if (otelTraceId)
|
|
725
|
+
metadata.otel_trace_id = otelTraceId;
|
|
726
|
+
}
|
|
367
727
|
const attributes = { ...emission.attributes, version: OXYGEN_VERSION, metadata };
|
|
368
728
|
const chunkEmissions = [];
|
|
369
729
|
const capture = (value, field, key) => {
|
|
@@ -371,12 +731,18 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
371
731
|
for (const chunk of prepared.chunks) {
|
|
372
732
|
chunkEmissions.push({
|
|
373
733
|
kind: "event", traceSeed: emission.traceSeed, name: "llm.payload_chunk", parentObservationId: observationId,
|
|
734
|
+
...(stableIdentity ? { observationId: observationSpanId(["langfuse-payload-v1", observationId, field, chunk.payload_id, chunk.index]) } : {}),
|
|
374
735
|
attributes: { version: OXYGEN_VERSION, input: chunk, metadata: {
|
|
375
736
|
oxygen_capture_version: 2, oxygen_payload_id: chunk.payload_id,
|
|
376
737
|
oxygen_payload_chunk_index: chunk.index, oxygen_payload_chunk_count: prepared.chunks.length,
|
|
377
738
|
oxygen_observation_id: `${metadata.oxygen_observation_id ?? observationId}:payload:${field}:${chunk.index}`,
|
|
378
739
|
} },
|
|
379
|
-
|
|
740
|
+
// V3's replacing key includes the start date. The root's end can
|
|
741
|
+
// move on resume, while unchanged chunks retain their IDs; pin their
|
|
742
|
+
// date to the immutable owner start in stable mode as well.
|
|
743
|
+
correlation: emission.correlation,
|
|
744
|
+
startTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
|
|
745
|
+
endTime: stableIdentity ? emission.startTime : emission.endTime ?? emission.startTime,
|
|
380
746
|
});
|
|
381
747
|
}
|
|
382
748
|
return prepared.value;
|
|
@@ -405,12 +771,18 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
405
771
|
},
|
|
406
772
|
};
|
|
407
773
|
const scorer = options?.scorerImpl ?? createApiScorer(env, warn);
|
|
774
|
+
const promptResolver = options?.promptResolverImpl ?? (options?.emitterImpl ? NOOP_PROMPT_RESOLVER : createApiPromptResolver(env, warn));
|
|
408
775
|
const guarded = (fn, stage) => {
|
|
409
776
|
try {
|
|
410
777
|
fn();
|
|
411
778
|
}
|
|
412
779
|
catch (error) {
|
|
413
|
-
|
|
780
|
+
// Building the observation failed before any transport saw it (payload
|
|
781
|
+
// preparation, identity hashing, a substituted emitter). That is a lost
|
|
782
|
+
// capture, not a delivery failure, and must not read as one.
|
|
783
|
+
summary.dropped(1);
|
|
784
|
+
summary.maybeReport();
|
|
785
|
+
warn(new Error("langfuse_observation_build_failed"), { stage, reason: sanitizedErrorReason(error) });
|
|
414
786
|
}
|
|
415
787
|
};
|
|
416
788
|
// propagateAttributes rejects non-string ids and anything over 200 chars, and
|
|
@@ -433,11 +805,17 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
433
805
|
tags: input.tags,
|
|
434
806
|
});
|
|
435
807
|
};
|
|
808
|
+
const promptLink = (prompt) => {
|
|
809
|
+
if (!prompt?.registered)
|
|
810
|
+
return {};
|
|
811
|
+
const version = promptResolver.versionFor(prompt);
|
|
812
|
+
return version === undefined ? {} : { prompt: { name: prompt.name, version, isFallback: false } };
|
|
813
|
+
};
|
|
436
814
|
const modelObservation = (body, kind) => guarded(() => {
|
|
437
815
|
emitter.emit({
|
|
438
816
|
kind,
|
|
439
817
|
traceSeed: body.traceId,
|
|
440
|
-
|
|
818
|
+
observationId: body.observationId ?? physicalId(body, kind),
|
|
441
819
|
name: body.name,
|
|
442
820
|
attributes: compact({
|
|
443
821
|
input: body.input,
|
|
@@ -449,7 +827,8 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
449
827
|
completionStartTime: body.completionStartTime,
|
|
450
828
|
usageDetails: body.usageDetails,
|
|
451
829
|
costDetails: body.costDetails,
|
|
452
|
-
|
|
830
|
+
...promptLink(body.prompt),
|
|
831
|
+
metadata: { ...(body.metadata ?? {}), ...promptMetadata(body.prompt), oxygen_observation_id: body.id },
|
|
453
832
|
}),
|
|
454
833
|
// The cost-bearing observation: v5 session cost only rolls up when
|
|
455
834
|
// the generation itself carries the session.
|
|
@@ -486,6 +865,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
486
865
|
const sliceStart = typeof body.metadata.slice_started_at === "string" ? new Date(body.metadata.slice_started_at) : startTime;
|
|
487
866
|
emitter.emit({
|
|
488
867
|
kind: "span", traceSeed: body.id, name: `${body.name}.slice`,
|
|
868
|
+
...(stableIdentity ? { observationId: observationSpanId(["langfuse-slice-v1", body.id, body.metadata.slice_id, sliceStart.getTime()]) } : {}),
|
|
489
869
|
attributes: compact({ input: body.input, output: body.output, metadata: { ...body.metadata, oxygen_observation_id: `${body.id}:slice:${body.metadata.slice_id}` } }),
|
|
490
870
|
correlation: correlate({ sessionId: body.sessionId, userId: body.userId }),
|
|
491
871
|
startTime: Number.isFinite(sliceStart.getTime()) ? sliceStart : startTime, endTime: body.endTime ?? startTime,
|
|
@@ -496,6 +876,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
496
876
|
emitter.emit({
|
|
497
877
|
kind: "span",
|
|
498
878
|
traceSeed: body.traceId,
|
|
879
|
+
observationId: physicalId(body, "span"),
|
|
499
880
|
name: body.name,
|
|
500
881
|
attributes: compact({
|
|
501
882
|
input: body.input,
|
|
@@ -512,10 +893,22 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
512
893
|
generation: (body) => modelObservation(body, "generation"),
|
|
513
894
|
embedding: (body) => modelObservation(body, "embedding"),
|
|
514
895
|
event: (body) => guarded(() => {
|
|
896
|
+
const durableKey = body.metadata?.oxygen_durable_event_key;
|
|
897
|
+
const durableEvent = (typeof durableKey === "string" && durableKey.length > 0) || /^apr:(requested|decided):.+/.test(body.id);
|
|
898
|
+
if (stableIdentity && durableEvent && (!body.startTime || !Number.isFinite(body.startTime.getTime()))) {
|
|
899
|
+
// A stable id with "now" as its date creates another V3 observation
|
|
900
|
+
// after midnight. Missing canonical time is missing capture, not a
|
|
901
|
+
// reason to fabricate a historical event or duplicate it silently.
|
|
902
|
+
warn(new Error("langfuse_replay_identity_unavailable"), { stage: "identity", observation_kind: "event" });
|
|
903
|
+
summary.dropped(1);
|
|
904
|
+
summary.maybeReport();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
515
907
|
const startTime = body.startTime ?? new Date();
|
|
516
908
|
emitter.emit({
|
|
517
909
|
kind: "event",
|
|
518
910
|
traceSeed: body.traceId,
|
|
911
|
+
observationId: physicalId(body, "event"),
|
|
519
912
|
name: body.name,
|
|
520
913
|
attributes: compact({
|
|
521
914
|
input: body.input,
|
|
@@ -542,7 +935,14 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
542
935
|
// enforced here (15s for serverless after()) rather than inside one emitter — an emitter that throws
|
|
543
936
|
// synchronously or rejects must still not escape into product code.
|
|
544
937
|
flush: () => boundedNever((async () => emitter.flush())(), (error) => warn(error, { stage: "flush" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
|
|
545
|
-
shutdown: () => boundedNever(
|
|
938
|
+
shutdown: () => boundedNever(
|
|
939
|
+
// The final window is reported whatever its age: nothing follows it.
|
|
940
|
+
(async () => { try {
|
|
941
|
+
await emitter.shutdown();
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
summary.reportNow();
|
|
945
|
+
} })(), (error) => warn(error, { stage: "shutdown" }), env.VERCEL ? 15_000 : FLUSH_TIMEOUT_MS),
|
|
546
946
|
};
|
|
547
947
|
}
|
|
548
948
|
// --- Process-wide singleton (both runtimes construct at most one client) ------
|
|
@@ -554,6 +954,8 @@ function cacheKey(env) {
|
|
|
554
954
|
env.LANGFUSE_SECRET_KEY ?? "",
|
|
555
955
|
env.LANGFUSE_BASE_URL ?? "",
|
|
556
956
|
resolveLlmTracingEnvironment(env),
|
|
957
|
+
resolveLlmTracingServiceName(env),
|
|
958
|
+
env.LANGFUSE_OBSERVATION_IDENTITY ?? "",
|
|
557
959
|
].join("|");
|
|
558
960
|
}
|
|
559
961
|
export function getLlmTracingClient(env = process.env) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The country vocabulary behind the CRM's "Primary Location — Country" column.
|
|
3
|
+
*
|
|
4
|
+
* WHY A CHECKED-IN TABLE AND NOT `Intl.DisplayNames` AT RUNTIME: these strings
|
|
5
|
+
* are persisted. They become `definition.options[].value` on the column and the
|
|
6
|
+
* literal text stored in every cell, so deriving them from ICU would let a Node
|
|
7
|
+
* upgrade silently rename an option and orphan every row that already carried
|
|
8
|
+
* the old spelling. The table was GENERATED from CLDR English region names once
|
|
9
|
+
* (the 249 officially assigned ISO 3166-1 alpha-2 codes, plus XK/Kosovo, which
|
|
10
|
+
* LinkedIn lists and ISO does not), then frozen here with the overrides below.
|
|
11
|
+
*
|
|
12
|
+
* NAMING: LinkedIn spells a handful differently from CLDR's English short name
|
|
13
|
+
* — those are the entries carrying a trailing comment. Everything else is the
|
|
14
|
+
* CLDR name verbatim, which is what LinkedIn uses. This is a faithful
|
|
15
|
+
* reproduction of LinkedIn's vocabulary, not a scrape of its dropdown: the
|
|
16
|
+
* RESOLVER below is what makes that safe, because it accepts the alpha-2 code,
|
|
17
|
+
* any spelling in this table, and the aliases in COUNTRY_ALIASES. A scrape that
|
|
18
|
+
* returns "US", "United States", "USA" or "Congo - Kinshasa" all land on the
|
|
19
|
+
* one canonical option rather than on an empty cell.
|
|
20
|
+
*/
|
|
21
|
+
/** Canonical LinkedIn country name for each ISO 3166-1 alpha-2 code, A→Z by name. */
|
|
22
|
+
export declare const LINKEDIN_COUNTRIES: ReadonlyArray<readonly [code: string, name: string]>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a provider value — an alpha-2 code, a canonical name, or a known
|
|
25
|
+
* alias — to its canonical LinkedIn country name.
|
|
26
|
+
*
|
|
27
|
+
* Returns null for anything unrecognized, which is deliberate: the column is a
|
|
28
|
+
* select, so writing a value that is not one of its options would produce a
|
|
29
|
+
* cell no filter can match and no dropdown can show. An empty cell is the
|
|
30
|
+
* honest outcome, and the raw payload is still on the record.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveLinkedInCountry(value: unknown): string | null;
|
|
33
|
+
export { LINKEDIN_COUNTRY_PRIMARY_TIME_ZONES, linkedInCountryPrimaryTimeZone, } from "./linkedin-country-timezones.js";
|