mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
feat(plugins): add langfuse plugin (#6577)
feat(plugins): add langfuse example plugin Integrated into release/v3.8.47. (thanks @chirag127)
This commit is contained in:
parent
18d0659426
commit
5e5fb61fcc
3 changed files with 288 additions and 0 deletions
63
examples/plugins/langfuse/README.md
Normal file
63
examples/plugins/langfuse/README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Langfuse Plugin
|
||||
|
||||
Emits [Langfuse](https://langfuse.com/) generation traces for every LLM completion routed through OmniRoute.
|
||||
|
||||
Records **prompt, response, model, provider, token usage, latency, and error details** to Langfuse cloud (`cloud.langfuse.com` or `us.cloud.langfuse.com`) or a self-hosted Langfuse instance.
|
||||
|
||||
## Install
|
||||
|
||||
Copy the `examples/plugins/langfuse/` directory to your OmniRoute plugins path, or install directly from the marketplace UI.
|
||||
|
||||
## Configuration
|
||||
|
||||
Fill these fields in the plugin config panel:
|
||||
|
||||
| Key | Required | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `publicKey` | Yes | `""` | Langfuse public key (`pk-lf-...`) |
|
||||
| `secretKey` | Yes | `""` | Langfuse secret key (`sk-lf-...`) |
|
||||
| `host` | No | `https://cloud.langfuse.com` | Also `https://us.cloud.langfuse.com` or self-hosted URL |
|
||||
| `enabled` | No | `true` | Set to `false` to make the plugin a no-op without uninstalling |
|
||||
| `sampleRate` | No | `1.0` | `0.1` = trace 10% of requests |
|
||||
| `flushAt` | No | `15` | Events to buffer before flushing |
|
||||
| `flushInterval` | No | `10000` | Max ms between flushes |
|
||||
| `redactBody` | No | `false` | Set `true` to strip prompt + completion from traces (metadata still recorded) |
|
||||
|
||||
Get keys at [cloud.langfuse.com](https://cloud.langfuse.com) → Settings → API keys.
|
||||
|
||||
## What gets traced
|
||||
|
||||
Each LLM completion emits one Langfuse `generation` observation inside a per-request trace:
|
||||
|
||||
- **Trace:** `omniroute:<model>` with `userId`, `provider`, `requestId` metadata
|
||||
- **Generation:** `chat.completion` with:
|
||||
- `model` — full model ID
|
||||
- `modelParameters` — `temperature`, `max_tokens`, `top_p`
|
||||
- `input` — messages array (redacted if `redactBody: true`)
|
||||
- `output` — assistant message (redacted if `redactBody: true`)
|
||||
- `usage.promptTokens`, `usage.completionTokens`, `usage.totalTokens`
|
||||
- `startTime`, `endTime` — for latency
|
||||
|
||||
Errors emit a generation with `level: "ERROR"` and `statusMessage`.
|
||||
|
||||
## Runtime dependency
|
||||
|
||||
The plugin lazy-loads the [`langfuse`](https://www.npmjs.com/package/langfuse) npm SDK on first request. Install it in the plugin's own directory so a broken SDK cannot crash the gateway:
|
||||
|
||||
```bash
|
||||
cd examples/plugins/langfuse
|
||||
npm install langfuse
|
||||
```
|
||||
|
||||
## Privacy
|
||||
|
||||
- Prompts and completions are sent to Langfuse cloud (or your self-host) in cleartext unless `redactBody: true`
|
||||
- API keys are stored in the plugin config, encrypted at rest by OmniRoute
|
||||
- Set `sampleRate < 1.0` to reduce data volume
|
||||
- Set `enabled: false` to disable without removing configuration
|
||||
|
||||
## Related
|
||||
|
||||
- [Langfuse docs](https://langfuse.com/docs)
|
||||
- [Langfuse OpenTelemetry endpoint](https://langfuse.com/docs/opentelemetry/get-started) (alternative path — this plugin uses the native SDK)
|
||||
- OmniRoute plugin SDK: `docs/frameworks/PLUGIN_SDK.md`
|
||||
158
examples/plugins/langfuse/index.mjs
Normal file
158
examples/plugins/langfuse/index.mjs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/**
|
||||
* Langfuse Plugin — emits generation traces for every LLM completion.
|
||||
*
|
||||
* Records model, provider, prompt, completion, token usage, latency,
|
||||
* and error details to Langfuse cloud or self-hosted.
|
||||
*
|
||||
* @module langfuse
|
||||
*/
|
||||
|
||||
let langfuseClient = null;
|
||||
|
||||
/**
|
||||
* Lazy-init the Langfuse SDK. First trace request creates the client; the SDK
|
||||
* batches events and flushes per config.flushAt / config.flushInterval.
|
||||
*/
|
||||
async function getClient(config) {
|
||||
if (langfuseClient) return langfuseClient;
|
||||
if (!config.publicKey || !config.secretKey) return null;
|
||||
try {
|
||||
const { Langfuse } = await import("langfuse");
|
||||
langfuseClient = new Langfuse({
|
||||
publicKey: config.publicKey,
|
||||
secretKey: config.secretKey,
|
||||
baseUrl: config.host || "https://cloud.langfuse.com",
|
||||
flushAt: config.flushAt ?? 15,
|
||||
flushInterval: config.flushInterval ?? 10000,
|
||||
});
|
||||
return langfuseClient;
|
||||
} catch (err) {
|
||||
console.error("[langfuse] SDK import failed:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSample(rate) {
|
||||
if (rate >= 1) return true;
|
||||
if (rate <= 0) return false;
|
||||
return Math.random() < rate;
|
||||
}
|
||||
|
||||
function truncateBody(body, redact) {
|
||||
if (redact) return "[REDACTED]";
|
||||
if (body === undefined || body === null) return undefined;
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* onRequest — mark trace start, capture request metadata.
|
||||
*/
|
||||
export async function onRequest(ctx) {
|
||||
const config = ctx?.config || {};
|
||||
if (config.enabled === false) return;
|
||||
if (!shouldSample(config.sampleRate ?? 1)) return;
|
||||
if (ctx?.metadata) {
|
||||
ctx.metadata.__langfuseStart = Date.now();
|
||||
ctx.metadata.__langfuseSampled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onResponse — emit a Langfuse generation event with prompt, completion, usage.
|
||||
*/
|
||||
export async function onResponse(ctx) {
|
||||
const config = ctx?.config || {};
|
||||
if (config.enabled === false) return;
|
||||
if (!ctx?.metadata?.__langfuseSampled) return;
|
||||
|
||||
const client = await getClient(config);
|
||||
if (!client) return;
|
||||
|
||||
const start = ctx.metadata.__langfuseStart || Date.now();
|
||||
const end = Date.now();
|
||||
const body = ctx?.body || {};
|
||||
const response = ctx?.response || {};
|
||||
const usage = response.usage || {};
|
||||
|
||||
try {
|
||||
const trace = client.trace({
|
||||
name: `omniroute:${body.model || "unknown"}`,
|
||||
userId: ctx?.userId,
|
||||
metadata: {
|
||||
provider: ctx?.provider,
|
||||
requestId: ctx?.requestId,
|
||||
omnirouteVersion: ctx?.omnirouteVersion,
|
||||
},
|
||||
});
|
||||
trace.generation({
|
||||
name: "chat.completion",
|
||||
model: body.model || "unknown",
|
||||
modelParameters: {
|
||||
temperature: body.temperature,
|
||||
max_tokens: body.max_tokens,
|
||||
top_p: body.top_p,
|
||||
},
|
||||
input: truncateBody(body.messages, config.redactBody),
|
||||
output: truncateBody(response.choices?.[0]?.message, config.redactBody),
|
||||
usage: {
|
||||
promptTokens: usage.prompt_tokens,
|
||||
completionTokens: usage.completion_tokens,
|
||||
totalTokens: usage.total_tokens,
|
||||
},
|
||||
startTime: new Date(start),
|
||||
endTime: new Date(end),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[langfuse] trace emit failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onError — emit a failed generation with error details.
|
||||
*/
|
||||
export async function onError(ctx) {
|
||||
const config = ctx?.config || {};
|
||||
if (config.enabled === false) return;
|
||||
if (!ctx?.metadata?.__langfuseSampled) return;
|
||||
|
||||
const client = await getClient(config);
|
||||
if (!client) return;
|
||||
|
||||
const start = ctx.metadata.__langfuseStart || Date.now();
|
||||
const body = ctx?.body || {};
|
||||
const error = ctx?.error || {};
|
||||
|
||||
try {
|
||||
const trace = client.trace({
|
||||
name: `omniroute:${body.model || "unknown"}`,
|
||||
userId: ctx?.userId,
|
||||
metadata: {
|
||||
provider: ctx?.provider,
|
||||
requestId: ctx?.requestId,
|
||||
},
|
||||
});
|
||||
trace.generation({
|
||||
name: "chat.completion",
|
||||
model: body.model || "unknown",
|
||||
input: truncateBody(body.messages, config.redactBody),
|
||||
level: "ERROR",
|
||||
statusMessage: error.message || String(error),
|
||||
startTime: new Date(start),
|
||||
endTime: new Date(),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[langfuse] error trace emit failed:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onShutdown — flush pending events before OmniRoute exits.
|
||||
*/
|
||||
export async function onShutdown() {
|
||||
if (!langfuseClient) return;
|
||||
try {
|
||||
await langfuseClient.shutdownAsync();
|
||||
} catch (err) {
|
||||
console.error("[langfuse] shutdown failed:", err.message);
|
||||
}
|
||||
}
|
||||
67
examples/plugins/langfuse/plugin.json
Normal file
67
examples/plugins/langfuse/plugin.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "langfuse",
|
||||
"version": "1.0.0",
|
||||
"description": "Emits Langfuse generation traces for every LLM completion. Records prompt, response, model, usage, latency, and error details to Langfuse cloud or self-hosted.",
|
||||
"author": "chirag127",
|
||||
"license": "MIT",
|
||||
"main": "index.mjs",
|
||||
"source": "local",
|
||||
"tags": ["observability", "tracing", "langfuse", "monitoring"],
|
||||
"hooks": {
|
||||
"onRequest": true,
|
||||
"onResponse": true,
|
||||
"onError": true
|
||||
},
|
||||
"requires": {
|
||||
"permissions": []
|
||||
},
|
||||
"enabledByDefault": false,
|
||||
"configSchema": {
|
||||
"publicKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Langfuse public key (pk-lf-...)"
|
||||
},
|
||||
"secretKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Langfuse secret key (sk-lf-...)"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"default": "https://cloud.langfuse.com",
|
||||
"description": "Langfuse host (cloud.langfuse.com, us.cloud.langfuse.com, or self-hosted URL)"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Emit traces when true; act as a no-op when false"
|
||||
},
|
||||
"sampleRate": {
|
||||
"type": "number",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"description": "Fraction of requests to trace (1.0 = all, 0.1 = 10%)"
|
||||
},
|
||||
"flushAt": {
|
||||
"type": "number",
|
||||
"default": 15,
|
||||
"min": 1,
|
||||
"max": 100,
|
||||
"description": "Number of events to buffer before flushing to Langfuse"
|
||||
},
|
||||
"flushInterval": {
|
||||
"type": "number",
|
||||
"default": 10000,
|
||||
"min": 1000,
|
||||
"max": 60000,
|
||||
"description": "Max ms between flushes (in-flight events flushed on timer)"
|
||||
},
|
||||
"redactBody": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Redact prompt + completion bodies from traces (metadata still recorded)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue