10 Commits

Author SHA1 Message Date
2c82a8416f Document OpenRouter as a supported LLM_BASE_URL provider 2026-08-12 23:43:51 +02:00
cb5e7085bf Support OpenRouter's real per-request cost reporting
Send usage: { include: true } on every chat completion request (an
OpenRouter-specific extension - requires Usage Accounting enabled on
the OpenRouter account; other OpenAI-compatible providers generally
ignore unknown top-level request fields). When the response includes
OpenRouter's own usage.cost, forward it to Langfuse as
costDetails.total, so cost is the real per-request figure OpenRouter
billed rather than a guess from Langfuse's model price list (which
has no entry for proxied/aggregated OpenRouter model names anyway).

Verified: full suite (84/84) passing, tsc/eslint clean, docker
compose build succeeds. Per Langfuse's OpenRouter integration guide:
https://langfuse.com/integrations/gateways/openrouter
2026-08-12 23:43:30 +02:00
9a4d254968 Fix input/output getting misrouted by mixed usage-schema keys
The previous cached-tokens commit added cache_read_input_tokens (a
flat-schema key) alongside prompt_tokens/completion_tokens (OpenAI's
native schema) in the same usageDetails object. Langfuse's ingestion
explicitly does not support mixing those two schemas, and doing so
broke generation.update()'s output field - confirmed live: prompt/
response content was landing in the Usage panel instead of Input/
Output.

Fixed by staying entirely within OpenAI's native schema: cached
tokens now nest under prompt_tokens_details.cached_tokens instead of
using the flat cache_read_input_tokens key. Cast past the installed
@langfuse/tracing SDK's usageDetails type (which only exposes a
3-field camelCase shape with no _details nesting, unlike the wire
schema in @langfuse/core) since the nesting is valid on the wire even
though the SDK's public type doesn't model it.

Verified: full suite (82/82) passing, tsc/eslint clean.
2026-08-12 23:24:09 +02:00
df4ca0bf76 Track cached prompt tokens through to Langfuse
OpenAI's chat completions response includes prompt_tokens_details.
cached_tokens when prompt caching kicks in - previously dropped
entirely. Now extracted in LlmClient and forwarded to Langfuse as the
flat cache_read_input_tokens usage key.

Used the flat key rather than OpenAI's nested prompt_tokens_details
shape: the installed @langfuse/tracing SDK's usageDetails type is
Record<string, number> | OpenAiUsage (a 3-field camelCase shape, no
_details nesting) even though the wire schema in @langfuse/core
supports nesting - a gap in the SDK's public types, not something
worth fighting with a cast.

Verified: full suite (82/82) passing, tsc/eslint clean.
2026-08-12 23:15:42 +02:00
6f0300f038 Fix Langfuse usage tracking: map to snake_case keys it recognizes
LangfuseTracer was passing our internal camelCase usage fields
(promptTokens/completionTokens/totalTokens) straight through as
usageDetails. Langfuse only recognizes a flat schema (input/output/...)
or OpenAI's native snake_case schema (prompt_tokens/completion_tokens/
total_tokens) for usage/cost calculation - anything else is silently
ignored, so every generation was showing zero usage regardless of
what LlmClient actually reported.

Note: cost still requires a price-list entry for the model name.
Langfuse's built-in list only covers hosted providers (OpenAI,
Anthropic, Google); self-hosted models (e.g. LLM_MODEL=llama3 via
Ollama) need a custom model price added in Project Settings > Models
for a $ figure to show up, even with token counts now correct.

Verified: full suite (80/80) passing, tsc/eslint clean.
2026-08-12 23:00:04 +02:00
e47c57b50d Group pipeline runs into Langfuse sessions, tagged by trigger source
Each pipeline invocation (runAsync/searchAsync) now gets a fresh
sessionId, wrapped via the new Tracer.runWithSession() (implemented
with @langfuse/tracing's propagateAttributes, since v5 has no direct
sessionId/tags option on startObservation) so it shows up in
Langfuse's Sessions view. The session is also tagged with what
triggered the run: "cron", "api-list", "api-trigger", "api-search".

Verified: full test suite (79/79) passing, tsc/eslint clean, real
docker compose build + container smoke test against /health.
2026-08-12 22:51:17 +02:00
b615f08039 Copy pnpm-workspace.yaml into Docker build so protobufjs build-script
approval (needed by the new OTEL-based Langfuse SDK) actually takes
effect inside the image, not just locally.

Verified with docker compose build --no-cache + a real container run
against /health.
2026-08-12 22:21:15 +02:00
a5120d5153 upgrade langfuse 2026-08-12 22:17:56 +02:00
2ffba19392 add langfuse url to server starting 2026-08-12 22:03:04 +02:00
a537f22384 fix network 2026-08-12 21:52:45 +02:00
21 changed files with 1089 additions and 104 deletions

View File

@@ -4,7 +4,12 @@ JELLYFIN_API_KEY=
JELLYFIN_USER_ID= JELLYFIN_USER_ID=
TMDB_API_KEY= TMDB_API_KEY=
# LLM (disabled by default — works with Ollama, OpenAI, LM Studio, etc.) # LLM (disabled by default — any OpenAI-compatible endpoint: Ollama, OpenAI,
# LM Studio, OpenRouter, etc.)
# For OpenRouter specifically: LLM_BASE_URL=https://openrouter.ai/api/v1,
# LLM_API_KEY=<your OpenRouter key>, LLM_MODEL=<an OpenRouter model slug>.
# Enable "Usage Accounting" on the OpenRouter account to get real per-request
# cost reported back to Langfuse instead of a price-list estimate.
LLM_ENABLED=false LLM_ENABLED=false
LLM_BASE_URL=http://localhost:11434/v1 LLM_BASE_URL=http://localhost:11434/v1
LLM_API_KEY= LLM_API_KEY=

View File

@@ -1,7 +1,7 @@
FROM node:22-alpine AS build FROM node:22-alpine AS build
WORKDIR /app WORKDIR /app
RUN corepack enable RUN corepack enable
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm install --frozen-lockfile --ignore-scripts
COPY tsconfig.json ./ COPY tsconfig.json ./
COPY src ./src COPY src ./src
@@ -11,7 +11,7 @@ FROM node:22-alpine AS runtime
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
RUN corepack enable RUN corepack enable
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile --prod RUN pnpm install --frozen-lockfile --prod
COPY --from=build /app/dist ./dist COPY --from=build /app/dist ./dist
ENTRYPOINT ["node", "dist/app/index.js"] ENTRYPOINT ["node", "dist/app/index.js"]

View File

@@ -34,9 +34,8 @@ services:
LANGFUSE_BASE_URL: ${LANGFUSE_BASE_URL} LANGFUSE_BASE_URL: ${LANGFUSE_BASE_URL}
LANGFUSE_PROMPT_CACHE_ENABLED: ${LANGFUSE_PROMPT_CACHE_ENABLED} LANGFUSE_PROMPT_CACHE_ENABLED: ${LANGFUSE_PROMPT_CACHE_ENABLED}
networks: networks:
- media
- coolify - coolify
networks: networks:
media:
coolify: coolify:
external: true

View File

@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"main": "dist/app/index.js", "main": "dist/app/index.js",
"packageManager": "pnpm@11.21.0",
"scripts": { "scripts": {
"dev": "tsx watch src/app/index.ts", "dev": "tsx watch src/app/index.ts",
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
@@ -13,9 +14,12 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@langfuse/client": "^5.10.0",
"@langfuse/otel": "^5.10.0",
"@langfuse/tracing": "^5.10.0",
"@opentelemetry/sdk-node": "^0.221.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"fastify": "^5.0.0", "fastify": "^5.0.0",
"langfuse": "^3.30.0",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"openai": "^4.60.0", "openai": "^4.60.0",
"pino": "^9.4.0", "pino": "^9.4.0",

707
pnpm-lock.yaml generated
View File

@@ -8,15 +8,24 @@ importers:
.: .:
dependencies: dependencies:
'@langfuse/client':
specifier: ^5.10.0
version: 5.10.0(@opentelemetry/api@1.9.1)
'@langfuse/otel':
specifier: ^5.10.0
version: 5.10.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))
'@langfuse/tracing':
specifier: ^5.10.0
version: 5.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-node':
specifier: ^0.221.0
version: 0.221.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0)
dotenv: dotenv:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.6.1 version: 16.6.1
fastify: fastify:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.11.3 version: 5.11.3
langfuse:
specifier: ^3.30.0
version: 3.38.20
node-cron: node-cron:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@@ -393,6 +402,15 @@ packages:
'@fastify/proxy-addr@5.1.0': '@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
'@grpc/grpc-js@1.14.4':
resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==}
engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.8.1':
resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==}
engines: {node: '>=6'}
hasBin: true
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0':
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -409,6 +427,34 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
'@langfuse/client@5.10.0':
resolution: {integrity: sha512-WI//PooxMyFEFtDy77XNv6WxOpd52U6HNgI8cqhPX+S2aJ8Mz2UTWz7olwzLlrwToJpsy5aZNtpdhsol2Umubw==}
peerDependencies:
'@opentelemetry/api': ^1.9.0
'@langfuse/core@5.10.0':
resolution: {integrity: sha512-XXz1DBEwGMpNXz8DZHFsixqo+3vBkfYpat3oLLldRMuT8ur6OWQ81Kgh5Obh0CZ31adaR4FrLTHh4N9eBXo64A==}
peerDependencies:
'@opentelemetry/api': ^1.9.0
'@langfuse/otel@5.10.0':
resolution: {integrity: sha512-q1expgCm3QwjWII0PeCJJSyEV5o96FQ4VNzL5hgziWGmCPCd9TrrWxWxHdSY7cpZWddEJCp71zdh1nq0xISOpQ==}
engines: {node: '>=20'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
'@opentelemetry/core': ^2.0.1
'@opentelemetry/exporter-trace-otlp-http': '>=0.202.0 <1.0.0'
'@opentelemetry/sdk-trace-base': ^2.0.1
'@langfuse/tracing@5.10.0':
resolution: {integrity: sha512-//S9dcZszmEk2yx+h7eg10mEMswyU7MYLkGIJlO6gUfrXA+3Y5pvNiZNZU3cfDus94OAr16dHQpg3nNFiziZjg==}
engines: {node: '>=20'}
peerDependencies:
'@opentelemetry/api': ^1.9.0
'@napi-rs/lzma-linux-x64-gnu@1.5.1': '@napi-rs/lzma-linux-x64-gnu@1.5.1':
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
engines: {node: ^22.20 || ^24.12 || >=25} engines: {node: ^22.20 || ^24.12 || >=25}
@@ -428,9 +474,210 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@opentelemetry/api-logs@0.221.0':
resolution: {integrity: sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==}
engines: {node: '>=8.0.0'}
'@opentelemetry/api@1.9.1':
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
'@opentelemetry/configuration@0.221.0':
resolution: {integrity: sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.9.0
'@opentelemetry/context-async-hooks@2.10.0':
resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/core@2.10.0':
resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/exporter-logs-otlp-grpc@0.221.0':
resolution: {integrity: sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-logs-otlp-http@0.221.0':
resolution: {integrity: sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-logs-otlp-proto@0.221.0':
resolution: {integrity: sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-metrics-otlp-grpc@0.221.0':
resolution: {integrity: sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-metrics-otlp-http@0.221.0':
resolution: {integrity: sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-metrics-otlp-proto@0.221.0':
resolution: {integrity: sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-prometheus@0.221.0':
resolution: {integrity: sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-grpc@0.221.0':
resolution: {integrity: sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-http@0.221.0':
resolution: {integrity: sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-proto@0.221.0':
resolution: {integrity: sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-zipkin@2.10.0':
resolution: {integrity: sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/instrumentation@0.221.0':
resolution: {integrity: sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-exporter-base@0.221.0':
resolution: {integrity: sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-grpc-exporter-base@0.221.0':
resolution: {integrity: sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-transformer@0.221.0':
resolution: {integrity: sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/propagator-b3@2.10.0':
resolution: {integrity: sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/propagator-jaeger@2.10.0':
resolution: {integrity: sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/resources@2.10.0':
resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-logs@0.221.0':
resolution: {integrity: sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.4.0 <1.10.0'
'@opentelemetry/sdk-metrics@2.10.0':
resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.9.0 <1.10.0'
'@opentelemetry/sdk-node@0.221.0':
resolution: {integrity: sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-trace-base@2.10.0':
resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-trace-node@2.10.0':
resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/sdk-trace@2.10.0':
resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/semantic-conventions@1.43.0':
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
engines: {node: '>=14'}
'@pinojs/redact@0.4.0': '@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
'@protobufjs/base64@1.1.2':
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
'@protobufjs/codegen@2.0.5':
resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
'@protobufjs/eventemitter@1.1.1':
resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==}
'@protobufjs/fetch@1.1.1':
resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==}
'@protobufjs/float@1.0.2':
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
'@protobufjs/path@1.1.2':
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
'@protobufjs/pool@1.1.0':
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
'@protobufjs/utf8@1.1.2':
resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==}
'@rollup/rollup-android-arm-eabi@4.62.4': '@rollup/rollup-android-arm-eabi@4.62.4':
resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
cpu: [arm] cpu: [arm]
@@ -779,6 +1026,13 @@ packages:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
cjs-module-lexer@2.2.0:
resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1: color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'} engines: {node: '>=7.0.0'}
@@ -843,6 +1097,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
end-of-stream@1.4.5: end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@ -857,6 +1114,9 @@ packages:
es-module-lexer@1.7.0: es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-module-lexer@2.3.1:
resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
es-object-atoms@1.1.2: es-object-atoms@1.1.2:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -875,6 +1135,10 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@4.0.0: escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1018,6 +1282,10 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1082,6 +1350,10 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
import-in-the-middle@3.3.3:
resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==}
engines: {node: '>=18'}
imurmurhash@0.1.4: imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
@@ -1101,6 +1373,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3: is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1138,14 +1414,6 @@ packages:
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
langfuse-core@3.38.20:
resolution: {integrity: sha512-zBKVmQN/1oT5VWZUBYlWzvokIlkC/6mnpgr/2atMyTeAm+jR3ia7w2iJMjlrF5/oG8ukO1s8+LDRCzJpF1QeEA==}
engines: {node: '>=18'}
langfuse@3.38.20:
resolution: {integrity: sha512-MAmBAASSzJtmK1O9HQegA1mFsQhT8Yf+OJRGvE7FXkyv3g/eiBE0glLD0Ohg3pkxhoPdggM5SejK7ue9ctlaMA==}
engines: {node: '>=18'}
levn@0.4.1: levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1157,9 +1425,15 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.merge@4.6.2: lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
loupe@3.2.1: loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -1188,6 +1462,9 @@ packages:
minimist@1.2.8: minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
module-details-from-path@1.0.4:
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -1319,6 +1596,10 @@ packages:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
protobufjs@7.6.5:
resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==}
engines: {node: '>=12.0.0'}
pump@3.0.4: pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
@@ -1340,10 +1621,18 @@ packages:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2: require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
require-in-the-middle@8.0.1:
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1425,6 +1714,10 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string_decoder@1.3.0: string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1598,9 +1891,30 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1803,6 +2117,18 @@ snapshots:
'@fastify/forwarded': 3.0.2 '@fastify/forwarded': 3.0.2
ipaddr.js: 2.5.0 ipaddr.js: 2.5.0
'@grpc/grpc-js@1.14.4':
dependencies:
'@grpc/proto-loader': 0.8.1
'@js-sdsl/ordered-map': 4.4.2
'@grpc/proto-loader@0.8.1':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
protobufjs: 7.6.5
yargs: 17.7.3
'@humanwhocodes/config-array@0.13.0(supports-color@7.2.0)': '@humanwhocodes/config-array@0.13.0(supports-color@7.2.0)':
dependencies: dependencies:
'@humanwhocodes/object-schema': 2.0.3 '@humanwhocodes/object-schema': 2.0.3
@@ -1817,6 +2143,32 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
'@js-sdsl/ordered-map@4.4.2': {}
'@langfuse/client@5.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@langfuse/core': 5.10.0(@opentelemetry/api@1.9.1)
'@langfuse/tracing': 5.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/api': 1.9.1
mustache: 4.2.0
'@langfuse/core@5.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@langfuse/otel@5.10.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))':
dependencies:
'@langfuse/core': 5.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1)
'@langfuse/tracing@5.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@langfuse/core': 5.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/api': 1.9.1
'@napi-rs/lzma-linux-x64-gnu@1.5.1': '@napi-rs/lzma-linux-x64-gnu@1.5.1':
optional: true optional: true
@@ -1832,8 +2184,252 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1 fastq: 1.20.1
'@opentelemetry/api-logs@0.221.0':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api@1.9.1': {}
'@opentelemetry/configuration@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
yaml: 2.9.0
'@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/exporter-logs-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-logs-otlp-http@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-logs-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-http@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-prometheus@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/exporter-trace-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-http@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-zipkin@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api-logs': 0.221.0
import-in-the-middle: 3.3.3
require-in-the-middle: 8.0.1(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
'@opentelemetry/otlp-exporter-base@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-grpc-exporter-base@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@grpc/grpc-js': 1.14.4
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-transformer@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api-logs': 0.221.0
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/propagator-b3@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/propagator-jaeger@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api-logs': 0.221.0
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-node@0.221.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api-logs': 0.221.0
'@opentelemetry/configuration': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-logs-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-logs-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-logs-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-metrics-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-prometheus': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-http': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-trace-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/exporter-zipkin': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0)
'@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/propagator-b3': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/propagator-jaeger': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-node': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
transitivePeerDependencies:
- supports-color
'@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.43.0
'@opentelemetry/semantic-conventions@1.43.0': {}
'@pinojs/redact@0.4.0': {} '@pinojs/redact@0.4.0': {}
'@protobufjs/aspromise@1.1.2': {}
'@protobufjs/base64@1.1.2': {}
'@protobufjs/codegen@2.0.5': {}
'@protobufjs/eventemitter@1.1.1': {}
'@protobufjs/fetch@1.1.1':
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/float@1.0.2': {}
'@protobufjs/path@1.1.2': {}
'@protobufjs/pool@1.1.0': {}
'@protobufjs/utf8@1.1.2': {}
'@rollup/rollup-android-arm-eabi@4.62.4': '@rollup/rollup-android-arm-eabi@4.62.4':
optional: true optional: true
@@ -2156,6 +2752,14 @@ snapshots:
check-error@2.1.3: {} check-error@2.1.3: {}
cjs-module-lexer@2.2.0: {}
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1: color-convert@2.0.1:
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
@@ -2206,6 +2810,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
emoji-regex@8.0.0: {}
end-of-stream@1.4.5: end-of-stream@1.4.5:
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
@@ -2216,6 +2822,8 @@ snapshots:
es-module-lexer@1.7.0: {} es-module-lexer@1.7.0: {}
es-module-lexer@2.3.1: {}
es-object-atoms@1.1.2: es-object-atoms@1.1.2:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -2282,6 +2890,8 @@ snapshots:
'@esbuild/win32-ia32': 0.28.2 '@esbuild/win32-ia32': 0.28.2
'@esbuild/win32-x64': 0.28.2 '@esbuild/win32-x64': 0.28.2
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-scope@7.2.2: eslint-scope@7.2.2:
@@ -2464,6 +3074,8 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -2532,6 +3144,12 @@ snapshots:
parent-module: 1.0.1 parent-module: 1.0.1
resolve-from: 4.0.0 resolve-from: 4.0.0
import-in-the-middle@3.3.3:
dependencies:
cjs-module-lexer: 2.2.0
es-module-lexer: 2.3.1
module-details-from-path: 1.0.4
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
inflight@1.0.6: inflight@1.0.6:
@@ -2545,6 +3163,8 @@ snapshots:
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3: is-glob@4.0.3:
dependencies: dependencies:
is-extglob: 2.1.1 is-extglob: 2.1.1
@@ -2575,14 +3195,6 @@ snapshots:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
langfuse-core@3.38.20:
dependencies:
mustache: 4.2.0
langfuse@3.38.20:
dependencies:
langfuse-core: 3.38.20
levn@0.4.1: levn@0.4.1:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -2598,8 +3210,12 @@ snapshots:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
lodash.camelcase@4.3.0: {}
lodash.merge@4.6.2: {} lodash.merge@4.6.2: {}
long@5.3.2: {}
loupe@3.2.1: {} loupe@3.2.1: {}
magic-string@0.30.21: magic-string@0.30.21:
@@ -2624,6 +3240,8 @@ snapshots:
minimist@1.2.8: {} minimist@1.2.8: {}
module-details-from-path@1.0.4: {}
ms@2.1.3: {} ms@2.1.3: {}
mustache@4.2.0: {} mustache@4.2.0: {}
@@ -2750,6 +3368,20 @@ snapshots:
process@0.11.10: {} process@0.11.10: {}
protobufjs@7.6.5:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.5
'@protobufjs/eventemitter': 1.1.1
'@protobufjs/fetch': 1.1.1
'@protobufjs/float': 1.0.2
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.2
'@types/node': 22.20.1
long: 5.3.2
pump@3.0.4: pump@3.0.4:
dependencies: dependencies:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
@@ -2771,8 +3403,17 @@ snapshots:
real-require@0.2.0: {} real-require@0.2.0: {}
require-directory@2.1.1: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
require-in-the-middle@8.0.1(supports-color@7.2.0):
dependencies:
debug: 4.4.3(supports-color@7.2.0)
module-details-from-path: 1.0.4
transitivePeerDependencies:
- supports-color
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
ret@0.5.0: {} ret@0.5.0: {}
@@ -2857,6 +3498,12 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string_decoder@1.3.0: string_decoder@1.3.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@@ -3006,8 +3653,30 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrappy@1.0.2: {} wrappy@1.0.2: {}
y18n@5.0.8: {}
yaml@2.9.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.3:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@3.25.76: {} zod@3.25.76: {}

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
allowBuilds:
protobufjs: true

View File

@@ -40,7 +40,7 @@ export class RecommendationsController extends Controller {
: this.config.LLM_ENABLED; : this.config.LLM_ENABLED;
try { try {
const recommendations = await this.pipeline.runAsync({ count, useLlm }); const recommendations = await this.pipeline.runAsync({ count, useLlm }, { trigger: 'api-list' });
reply.send(recommendations); reply.send(recommendations);
} catch (err) { } catch (err) {
reply.code(500).send({ error: String(err) }); reply.code(500).send({ error: String(err) });
@@ -49,10 +49,10 @@ export class RecommendationsController extends Controller {
private async handleTrigger(_request: FastifyRequest, reply: FastifyReply) { private async handleTrigger(_request: FastifyRequest, reply: FastifyReply) {
try { try {
const recommendations = await this.pipeline.runAsync({ const recommendations = await this.pipeline.runAsync(
count: this.config.RECOMMEND_COUNT, { count: this.config.RECOMMEND_COUNT, useLlm: this.config.LLM_ENABLED },
useLlm: this.config.LLM_ENABLED, { trigger: 'api-trigger' }
}); );
if (this.config.RADARR_ENABLED) { if (this.config.RADARR_ENABLED) {
await this.pipeline.autoRequest(recommendations); await this.pipeline.autoRequest(recommendations);
} }
@@ -73,7 +73,7 @@ export class RecommendationsController extends Controller {
return; return;
} }
try { try {
const recommendations = await this.pipeline.searchAsync(query, this.config.RECOMMEND_COUNT); const recommendations = await this.pipeline.searchAsync(query, this.config.RECOMMEND_COUNT, 'api-search');
reply.send(recommendations); reply.send(recommendations);
} catch (err) { } catch (err) {
reply.code(500).send({ error: String(err) }); reply.code(500).send({ error: String(err) });

View File

@@ -9,10 +9,10 @@ export function scheduleCron(config: AppConfig, pipeline: RecommendationPipeline
cron.schedule(config.CRON_SCHEDULE, async () => { cron.schedule(config.CRON_SCHEDULE, async () => {
logger.info('Cron: running recommendation pipeline'); logger.info('Cron: running recommendation pipeline');
try { try {
const recommendations = await pipeline.runAsync({ const recommendations = await pipeline.runAsync(
count: config.RECOMMEND_COUNT, { count: config.RECOMMEND_COUNT, useLlm: config.LLM_ENABLED },
useLlm: config.LLM_ENABLED, { trigger: 'cron' }
}); );
if (config.RADARR_ENABLED) { if (config.RADARR_ENABLED) {
await pipeline.autoRequest(recommendations); await pipeline.autoRequest(recommendations);
} }

View File

@@ -32,10 +32,19 @@ async function main(): Promise<void> {
const promptService = new PromptService(promptGateway, config.LANGFUSE_PROMPT_CACHE_ENABLED); const promptService = new PromptService(promptGateway, config.LANGFUSE_PROMPT_CACHE_ENABLED);
const llmClient = new LlmClient({ baseUrl: config.LLM_BASE_URL, apiKey: config.LLM_API_KEY }); const llmClient = new LlmClient({ baseUrl: config.LLM_BASE_URL, apiKey: config.LLM_API_KEY });
const jellyfin = new JellyfinClient(config.JELLYFIN_URL, config.JELLYFIN_API_KEY, config.JELLYFIN_USER_ID); const jellyfin = new JellyfinClient(
config.JELLYFIN_URL,
config.JELLYFIN_API_KEY,
config.JELLYFIN_USER_ID
);
const tmdb = new TmdbClient(config.TMDB_API_KEY); const tmdb = new TmdbClient(config.TMDB_API_KEY);
const radarr = config.RADARR_ENABLED const radarr = config.RADARR_ENABLED
? new RadarrClient(config.RADARR_URL, config.RADARR_API_KEY, config.RADARR_QUALITY_PROFILE_ID, config.RADARR_ROOT_FOLDER) ? new RadarrClient(
config.RADARR_URL,
config.RADARR_API_KEY,
config.RADARR_QUALITY_PROFILE_ID,
config.RADARR_ROOT_FOLDER
)
: undefined; : undefined;
const recommenderTool = new MovieRecommenderTool(promptService, llmClient, config.LLM_MODEL); const recommenderTool = new MovieRecommenderTool(promptService, llmClient, config.LLM_MODEL);
@@ -58,12 +67,19 @@ async function main(): Promise<void> {
radarrEnabled: config.RADARR_ENABLED, radarrEnabled: config.RADARR_ENABLED,
cronEnabled: config.CRON_ENABLED, cronEnabled: config.CRON_ENABLED,
apiEnabled: config.API_ENABLED, apiEnabled: config.API_ENABLED,
langfuse: config.LANGFUSE_BASE_URL,
}, },
'sugarr starting' 'sugarr starting'
); );
scheduleCron(config, pipeline, logger); scheduleCron(config, pipeline, logger);
const shutdown = () => {
tracer.shutdown().catch((err) => logger.error({ err }, 'Error shutting down Langfuse tracer'));
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
if (config.API_ENABLED) { if (config.API_ENABLED) {
const app = buildServer({ const app = buildServer({
secretKey: config.SECRET_KEY, secretKey: config.SECRET_KEY,

View File

@@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto';
import { LlmPipeline } from '../../core/llm/LlmPipeline'; import { LlmPipeline } from '../../core/llm/LlmPipeline';
import type { RunContext } from '../../core/llm/RunContext'; import type { RunContext } from '../../core/llm/RunContext';
import type { JellyfinClient } from '../clients/jellyfin/JellyfinClient'; import type { JellyfinClient } from '../clients/jellyfin/JellyfinClient';
@@ -114,8 +115,16 @@ export class RecommendationPipeline extends LlmPipeline<RecommendationRequest, R
} }
} }
async searchAsync(query: string, count: number): Promise<Recommendation[]> { async searchAsync(query: string, count: number, trigger?: string): Promise<Recommendation[]> {
const trace = this.tracer.startTrace(`${this.name}-search`); const sessionId = randomUUID();
return this.tracer.runWithSession(
{ sessionId, tags: trigger ? [trigger] : undefined },
() => this.runSearch(query, count, trigger)
);
}
private async runSearch(query: string, count: number, trigger?: string): Promise<Recommendation[]> {
const trace = this.tracer.startTrace(`${this.name}-search`, { trigger });
const ctx: RunContext = { trace, logger: this.logger }; const ctx: RunContext = { trace, logger: this.logger };
try { try {
let library: Movie[] = []; let library: Movie[] = [];

View File

@@ -1,11 +1,11 @@
import { Langfuse } from 'langfuse'; import { LangfuseClient } from '@langfuse/client';
import type { PromptFetcher, RawPrompt } from './PromptGateway'; import type { PromptFetcher, RawPrompt } from './PromptGateway';
export class LangfusePromptGateway implements PromptFetcher { export class LangfusePromptGateway implements PromptFetcher {
private readonly client: Langfuse; private readonly client: LangfuseClient;
constructor(opts: { publicKey: string; secretKey: string; baseUrl: string }) { constructor(opts: { publicKey: string; secretKey: string; baseUrl: string }) {
this.client = new Langfuse({ this.client = new LangfuseClient({
publicKey: opts.publicKey, publicKey: opts.publicKey,
secretKey: opts.secretKey, secretKey: opts.secretKey,
baseUrl: opts.baseUrl, baseUrl: opts.baseUrl,
@@ -13,7 +13,7 @@ export class LangfusePromptGateway implements PromptFetcher {
} }
async fetch(promptName: string): Promise<RawPrompt> { async fetch(promptName: string): Promise<RawPrompt> {
const prompt = await this.client.getPrompt(promptName, undefined, { type: 'chat', cacheTtlSeconds: 0 }); const prompt = await this.client.prompt.get(promptName, { type: 'chat' });
return { return {
name: prompt.name, name: prompt.name,
version: prompt.version, version: prompt.version,

View File

@@ -1,33 +1,82 @@
import { Langfuse } from 'langfuse'; import { NodeSDK } from '@opentelemetry/sdk-node';
import { LangfuseSpanProcessor } from '@langfuse/otel';
import { propagateAttributes, startObservation } from '@langfuse/tracing';
import type { LangfuseGeneration, LangfuseSpan } from '@langfuse/tracing';
import { Tracer } from '../llm/Tracer'; import { Tracer } from '../llm/Tracer';
import type { GenerationHandle, GenerationInput, GenerationOutput, TraceHandle } from '../llm/Tracer'; import type {
GenerationHandle,
GenerationInput,
GenerationOutput,
SessionInfo,
TraceHandle,
} from '../llm/Tracer';
interface OpenAiUsageDetails {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
prompt_tokens_details?: { cached_tokens: number };
}
/**
* Langfuse recognizes usage in either a flat generic schema (input/output/
* cache_read_input_tokens/...) or OpenAI's native nested schema
* (prompt_tokens/completion_tokens/prompt_tokens_details.cached_tokens/...)
* - but NOT a mix of the two ("do not mix formats" per Langfuse's docs).
* Mixing them previously caused the whole generation.update() payload
* (including output) to land wrong, so this stays entirely within the
* OpenAI-native schema.
*
* The installed @langfuse/tracing SDK's usageDetails type is
* `Record<string, number> | OpenAiUsage` (a 3-field camelCase shape with
* no _details nesting) even though the wire schema (@langfuse/core's
* OpenAiCompletionUsageSchema) supports this nested form - a gap in the
* SDK's public types, not a real runtime restriction, hence the cast.
*/
function toUsageDetails(usage: GenerationOutput['usage']): OpenAiUsageDetails | undefined {
if (!usage) return undefined;
const details: OpenAiUsageDetails = {};
if (usage.promptTokens !== undefined) details.prompt_tokens = usage.promptTokens;
if (usage.completionTokens !== undefined) details.completion_tokens = usage.completionTokens;
if (usage.totalTokens !== undefined) details.total_tokens = usage.totalTokens;
if (usage.cachedTokens !== undefined) details.prompt_tokens_details = { cached_tokens: usage.cachedTokens };
return Object.keys(details).length > 0 ? details : undefined;
}
export class LangfuseTracer extends Tracer { export class LangfuseTracer extends Tracer {
private readonly client: Langfuse; private readonly processor: LangfuseSpanProcessor;
private readonly sdk: NodeSDK;
constructor(opts: { publicKey: string; secretKey: string; baseUrl: string }) { constructor(opts: { publicKey: string; secretKey: string; baseUrl: string }) {
super(); super();
this.client = new Langfuse({ this.processor = new LangfuseSpanProcessor({
publicKey: opts.publicKey, publicKey: opts.publicKey,
secretKey: opts.secretKey, secretKey: opts.secretKey,
baseUrl: opts.baseUrl, baseUrl: opts.baseUrl,
}); });
this.sdk = new NodeSDK({ spanProcessors: [this.processor] });
this.sdk.start();
} }
startTrace(name: string, metadata?: Record<string, unknown>): TraceHandle { startTrace(name: string, metadata?: Record<string, unknown>): TraceHandle {
const trace = this.client.trace({ name, metadata }); const span: LangfuseSpan = startObservation(name, { metadata });
return { return {
id: trace.id, id: span.traceId,
startGeneration(input: GenerationInput): GenerationHandle { startGeneration(input: GenerationInput): GenerationHandle {
const generation = trace.generation({ const generation: LangfuseGeneration = span.startObservation(
name: input.name, input.name,
model: input.model, { model: input.model, input: input.input, metadata: input.metadata },
input: input.input, { asType: 'generation' }
metadata: input.metadata, );
});
return { return {
end(output: GenerationOutput) { end(output: GenerationOutput) {
generation.end({ output: output.output, usage: output.usage, metadata: output.metadata }); generation.update({
output: output.output,
usageDetails: toUsageDetails(output.usage) as Record<string, number> | undefined,
costDetails: output.usage?.cost !== undefined ? { total: output.usage.cost } : undefined,
metadata: output.metadata,
});
generation.end();
}, },
}; };
}, },
@@ -35,6 +84,14 @@ export class LangfuseTracer extends Tracer {
} }
async flush(): Promise<void> { async flush(): Promise<void> {
await this.client.flushAsync(); await this.processor.forceFlush();
}
runWithSession<T>(session: SessionInfo, fn: () => Promise<T>): Promise<T> {
return propagateAttributes({ sessionId: session.sessionId, tags: session.tags }, fn);
}
async shutdown(): Promise<void> {
await this.sdk.shutdown();
} }
} }

View File

@@ -1,4 +1,6 @@
import OpenAI from 'openai'; import OpenAI from 'openai';
import type { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions';
import type { CompletionUsage } from 'openai/resources/completions';
export interface ChatMessage { export interface ChatMessage {
role: 'system' | 'user' | 'assistant'; role: 'system' | 'user' | 'assistant';
@@ -13,9 +15,28 @@ export interface ChatOptions {
export interface ChatResult { export interface ChatResult {
content: string; content: string;
usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number }; usage?: {
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
cachedTokens?: number;
cost?: number;
};
} }
/**
* OpenRouter-specific request/response extensions, not part of OpenAI's
* official schema. `usage.include` asks OpenRouter to report its own real
* per-request cost (requires Usage Accounting enabled on the OpenRouter
* account); other OpenAI-compatible providers generally ignore unknown
* top-level request fields. See:
* https://langfuse.com/integrations/gateways/openrouter
*/
type OpenRouterChatCompletionCreateParams = ChatCompletionCreateParamsNonStreaming & {
usage?: { include: boolean };
};
type OpenRouterCompletionUsage = CompletionUsage & { cost?: number };
export class LlmClient { export class LlmClient {
private readonly client: OpenAI; private readonly client: OpenAI;
@@ -29,20 +50,25 @@ export class LlmClient {
temperature: options.temperature, temperature: options.temperature,
max_tokens: options.maxTokens, max_tokens: options.maxTokens,
messages, messages,
}); usage: { include: true },
} as OpenRouterChatCompletionCreateParams);
const choice = response.choices[0]; const choice = response.choices[0];
if (!choice) { if (!choice) {
throw new Error('LLM returned no choices'); throw new Error('LLM returned no choices');
} }
const usage = response.usage as OpenRouterCompletionUsage | undefined;
return { return {
content: choice.message?.content ?? '', content: choice.message?.content ?? '',
usage: response.usage usage: usage
? { ? {
promptTokens: response.usage.prompt_tokens, promptTokens: usage.prompt_tokens,
completionTokens: response.usage.completion_tokens, completionTokens: usage.completion_tokens,
totalTokens: response.usage.total_tokens, totalTokens: usage.total_tokens,
cachedTokens: usage.prompt_tokens_details?.cached_tokens,
cost: usage.cost,
} }
: undefined, : undefined,
}; };

View File

@@ -1,8 +1,14 @@
import { randomUUID } from 'node:crypto';
import type { Logger } from '../logging/logger'; import type { Logger } from '../logging/logger';
import type { LlmTool } from './LlmTool'; import type { LlmTool } from './LlmTool';
import type { RunContext } from './RunContext'; import type { RunContext } from './RunContext';
import type { Tracer } from './Tracer'; import type { Tracer } from './Tracer';
export interface PipelineRunOptions {
/** What triggered this run (e.g. "cron", "api-list") — attached as a Langfuse tag. */
trigger?: string;
}
export abstract class LlmPipeline<TInput, TOutput> { export abstract class LlmPipeline<TInput, TOutput> {
constructor( constructor(
protected readonly tracer: Tracer, protected readonly tracer: Tracer,
@@ -12,14 +18,20 @@ export abstract class LlmPipeline<TInput, TOutput> {
protected abstract readonly name: string; protected abstract readonly name: string;
protected abstract runInternal(input: TInput, ctx: RunContext): Promise<TOutput>; protected abstract runInternal(input: TInput, ctx: RunContext): Promise<TOutput>;
async runAsync(input: TInput): Promise<TOutput> { async runAsync(input: TInput, options: PipelineRunOptions = {}): Promise<TOutput> {
const trace = this.tracer.startTrace(this.name); const sessionId = randomUUID();
const ctx: RunContext = { trace, logger: this.logger }; return this.tracer.runWithSession(
try { { sessionId, tags: options.trigger ? [options.trigger] : undefined },
return await this.runInternal(input, ctx); async () => {
} finally { const trace = this.tracer.startTrace(this.name, { trigger: options.trigger });
await this.tracer.flush(); const ctx: RunContext = { trace, logger: this.logger };
} try {
return await this.runInternal(input, ctx);
} finally {
await this.tracer.flush();
}
}
);
} }
protected runTool<TToolInput, TToolOutput>( protected runTool<TToolInput, TToolOutput>(

View File

@@ -7,7 +7,13 @@ export interface GenerationInput {
export interface GenerationOutput { export interface GenerationOutput {
output: unknown; output: unknown;
usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number }; usage?: {
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
cachedTokens?: number;
cost?: number;
};
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
} }
@@ -20,7 +26,18 @@ export interface TraceHandle {
startGeneration(input: GenerationInput): GenerationHandle; startGeneration(input: GenerationInput): GenerationHandle;
} }
export interface SessionInfo {
sessionId: string;
tags?: string[];
}
export abstract class Tracer { export abstract class Tracer {
abstract startTrace(name: string, metadata?: Record<string, unknown>): TraceHandle; abstract startTrace(name: string, metadata?: Record<string, unknown>): TraceHandle;
abstract flush(): Promise<void>; abstract flush(): Promise<void>;
/**
* Runs `fn` with `session` (sessionId/tags) attached to every trace/span
* created inside it, so multiple runs can be grouped in the Langfuse UI.
*/
abstract runWithSession<T>(session: SessionInfo, fn: () => Promise<T>): Promise<T>;
} }

View File

@@ -17,7 +17,7 @@ describe('RecommendationsController', () => {
const res = await app.inject({ method: 'GET', url: '/recommendations' }); const res = await app.inject({ method: 'GET', url: '/recommendations' });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }); expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }, { trigger: 'api-list' });
}); });
it('GET /recommendations?count= and force_llm/force_tmdb override config', async () => { it('GET /recommendations?count= and force_llm/force_tmdb override config', async () => {
@@ -25,7 +25,7 @@ describe('RecommendationsController', () => {
const app = makeApp({ RECOMMEND_COUNT: 10, LLM_ENABLED: false }, { runAsync } as unknown as RecommendationPipeline); const app = makeApp({ RECOMMEND_COUNT: 10, LLM_ENABLED: false }, { runAsync } as unknown as RecommendationPipeline);
await app.inject({ method: 'GET', url: '/recommendations?count=3&force_llm=true' }); await app.inject({ method: 'GET', url: '/recommendations?count=3&force_llm=true' });
expect(runAsync).toHaveBeenCalledWith({ count: 3, useLlm: true }); expect(runAsync).toHaveBeenCalledWith({ count: 3, useLlm: true }, { trigger: 'api-list' });
}); });
it('falls back to the configured count for a malformed or non-positive count query param', async () => { it('falls back to the configured count for a malformed or non-positive count query param', async () => {
@@ -33,13 +33,13 @@ describe('RecommendationsController', () => {
const app = makeApp({ RECOMMEND_COUNT: 10, LLM_ENABLED: true }, { runAsync } as unknown as RecommendationPipeline); const app = makeApp({ RECOMMEND_COUNT: 10, LLM_ENABLED: true }, { runAsync } as unknown as RecommendationPipeline);
await app.inject({ method: 'GET', url: '/recommendations?count=abc' }); await app.inject({ method: 'GET', url: '/recommendations?count=abc' });
expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }); expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }, { trigger: 'api-list' });
await app.inject({ method: 'GET', url: '/recommendations?count=-5' }); await app.inject({ method: 'GET', url: '/recommendations?count=-5' });
expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }); expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }, { trigger: 'api-list' });
await app.inject({ method: 'GET', url: '/recommendations?count=0' }); await app.inject({ method: 'GET', url: '/recommendations?count=0' });
expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }); expect(runAsync).toHaveBeenCalledWith({ count: 10, useLlm: true }, { trigger: 'api-list' });
}); });
it('POST /recommendations/trigger auto-requests via Radarr when enabled', async () => { it('POST /recommendations/trigger auto-requests via Radarr when enabled', async () => {
@@ -75,6 +75,6 @@ describe('RecommendationsController', () => {
payload: { query: 'sci-fi like Blade Runner' }, payload: { query: 'sci-fi like Blade Runner' },
}); });
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(searchAsync).toHaveBeenCalledWith('sci-fi like Blade Runner', 10); expect(searchAsync).toHaveBeenCalledWith('sci-fi like Blade Runner', 10, 'api-search');
}); });
}); });

View File

@@ -25,6 +25,7 @@ function makeTracer() {
const tracer = { const tracer = {
startTrace: vi.fn().mockReturnValue(trace), startTrace: vi.fn().mockReturnValue(trace),
flush: vi.fn().mockResolvedValue(undefined), flush: vi.fn().mockResolvedValue(undefined),
runWithSession: vi.fn().mockImplementation((_session: unknown, fn: () => unknown) => fn()),
} as unknown as Tracer; } as unknown as Tracer;
return tracer; return tracer;
} }

View File

@@ -1,20 +1,22 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
const compile = vi.fn().mockReturnValue([{ role: 'user', content: 'hi' }]); const compile = vi.fn().mockReturnValue([{ role: 'user', content: 'hi' }]);
const getPrompt = vi.fn().mockResolvedValue({ name: 'greet', version: 2, config: { model: 'llama3' }, compile }); const promptGet = vi
.fn()
.mockResolvedValue({ name: 'greet', version: 2, config: { model: 'llama3' }, compile });
vi.mock('langfuse', () => ({ vi.mock('@langfuse/client', () => ({
Langfuse: vi.fn().mockImplementation(() => ({ getPrompt })), LangfuseClient: vi.fn().mockImplementation(() => ({ prompt: { get: promptGet } })),
})); }));
import { LangfusePromptGateway } from '../../../src/core/langfuse/LangfusePromptGateway'; import { LangfusePromptGateway } from '../../../src/core/langfuse/LangfusePromptGateway';
describe('LangfusePromptGateway', () => { describe('LangfusePromptGateway', () => {
it('fetches a chat prompt from Langfuse bypassing the SDK cache, and adapts it to RawPrompt', async () => { it('fetches a chat prompt from Langfuse and adapts it to RawPrompt', async () => {
const gateway = new LangfusePromptGateway({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' }); const gateway = new LangfusePromptGateway({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const raw = await gateway.fetch('greet'); const raw = await gateway.fetch('greet');
expect(getPrompt).toHaveBeenCalledWith('greet', undefined, { type: 'chat', cacheTtlSeconds: 0 }); expect(promptGet).toHaveBeenCalledWith('greet', { type: 'chat' });
expect(raw.name).toBe('greet'); expect(raw.name).toBe('greet');
expect(raw.version).toBe(2); expect(raw.version).toBe(2);
expect(raw.config).toEqual({ model: 'llama3' }); expect(raw.config).toEqual({ model: 'llama3' });

View File

@@ -1,42 +1,148 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
const generationEnd = vi.fn(); const {
const generation = vi.fn().mockReturnValue({ end: generationEnd }); forceFlush,
const trace = vi.fn().mockReturnValue({ id: 'lf-trace-1', generation }); shutdown,
const flushAsync = vi.fn().mockResolvedValue(undefined); sdkStart,
sdkShutdown,
generationUpdate,
generationEnd,
childStartObservation,
startObservation,
propagateAttributes,
} = vi.hoisted(() => {
const generationUpdate = vi.fn();
const generationEnd = vi.fn();
const generation = { update: generationUpdate, end: generationEnd };
const childStartObservation = vi.fn().mockReturnValue(generation);
const rootSpan = { traceId: 'lf-trace-1', startObservation: childStartObservation };
return {
forceFlush: vi.fn().mockResolvedValue(undefined),
shutdown: vi.fn().mockResolvedValue(undefined),
sdkStart: vi.fn(),
sdkShutdown: vi.fn().mockResolvedValue(undefined),
generationUpdate,
generationEnd,
childStartObservation,
startObservation: vi.fn().mockReturnValue(rootSpan),
propagateAttributes: vi.fn().mockImplementation((_attrs: unknown, fn: () => unknown) => fn()),
};
});
vi.mock('langfuse', () => ({ vi.mock('@langfuse/otel', () => ({
Langfuse: vi.fn().mockImplementation(() => ({ trace, flushAsync })), LangfuseSpanProcessor: vi.fn().mockImplementation(() => ({ forceFlush, shutdown })),
})); }));
vi.mock('@opentelemetry/sdk-node', () => ({
NodeSDK: vi.fn().mockImplementation(() => ({ start: sdkStart, shutdown: sdkShutdown })),
}));
vi.mock('@langfuse/tracing', () => ({ startObservation, propagateAttributes }));
import { LangfuseTracer } from '../../../src/core/langfuse/LangfuseTracer'; import { LangfuseTracer } from '../../../src/core/langfuse/LangfuseTracer';
describe('LangfuseTracer', () => { describe('LangfuseTracer', () => {
it('starts a Langfuse trace and exposes its id', () => { it('starts the OTEL SDK with a LangfuseSpanProcessor on construction', () => {
new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
expect(sdkStart).toHaveBeenCalled();
});
it('starts a root observation and exposes its trace id', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' }); const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline', { foo: 'bar' }); const handle = tracer.startTrace('my-pipeline', { foo: 'bar' });
expect(trace).toHaveBeenCalledWith({ name: 'my-pipeline', metadata: { foo: 'bar' } }); expect(startObservation).toHaveBeenCalledWith('my-pipeline', { metadata: { foo: 'bar' } });
expect(handle.id).toBe('lf-trace-1'); expect(handle.id).toBe('lf-trace-1');
}); });
it('startGeneration creates a Langfuse generation and end() forwards output/usage', () => { it('startGeneration creates a child generation observation and end() forwards output/usage', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' }); const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline'); const handle = tracer.startTrace('my-pipeline');
const gen = handle.startGeneration({ name: 'step-1', model: 'llama3', input: ['hi'] }); const gen = handle.startGeneration({ name: 'step-1', model: 'llama3', input: ['hi'] });
gen.end({ output: 'done', usage: { totalTokens: 5 } }); gen.end({
output: 'done',
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
});
expect(generation).toHaveBeenCalledWith( expect(childStartObservation).toHaveBeenCalledWith(
expect.objectContaining({ name: 'step-1', model: 'llama3', input: ['hi'] }) 'step-1',
expect.objectContaining({ model: 'llama3', input: ['hi'] }),
{ asType: 'generation' }
); );
expect(generationEnd).toHaveBeenCalledWith( expect(generationUpdate).toHaveBeenCalledWith(
expect.objectContaining({ output: 'done', usage: { totalTokens: 5 } }) expect.objectContaining({
output: 'done',
usageDetails: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
costDetails: undefined,
})
);
expect(generationEnd).toHaveBeenCalledTimes(1);
});
it('forwards an explicit cost (e.g. from OpenRouter) as costDetails.total instead of relying on price-list lookup', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline');
const gen = handle.startGeneration({ name: 'step-1', model: 'openrouter/auto', input: ['hi'] });
gen.end({
output: 'done',
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15, cost: 0.00042 },
});
expect(generationUpdate).toHaveBeenCalledWith(
expect.objectContaining({ costDetails: { total: 0.00042 } })
); );
}); });
it('flush() calls flushAsync on the underlying client', async () => { it('maps cachedTokens to the OpenAI-native nested prompt_tokens_details.cached_tokens, not a mixed flat key', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline');
const gen = handle.startGeneration({ name: 'step-1', model: 'gpt-4o-mini', input: ['hi'] });
gen.end({
output: 'done',
usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120, cachedTokens: 80 },
});
expect(generationUpdate).toHaveBeenCalledWith(
expect.objectContaining({
usageDetails: {
prompt_tokens: 100,
completion_tokens: 20,
total_tokens: 120,
prompt_tokens_details: { cached_tokens: 80 },
},
})
);
});
it('omits usageDetails entirely when no usage is reported', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline');
const gen = handle.startGeneration({ name: 'step-1', model: 'llama3', input: ['hi'] });
gen.end({ output: 'done' });
expect(generationUpdate).toHaveBeenCalledWith(expect.objectContaining({ usageDetails: undefined }));
});
it('flush() force-flushes the span processor without shutting down the SDK', async () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' }); const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
await tracer.flush(); await tracer.flush();
expect(flushAsync).toHaveBeenCalledTimes(1); expect(forceFlush).toHaveBeenCalledTimes(1);
expect(sdkShutdown).not.toHaveBeenCalled();
});
it('shutdown() shuts down the underlying OTEL SDK', async () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
await tracer.shutdown();
expect(sdkShutdown).toHaveBeenCalledTimes(1);
});
it('runWithSession propagates sessionId/tags to the callback via propagateAttributes and returns its result', async () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const fn = vi.fn().mockResolvedValue('done');
const result = await tracer.runWithSession({ sessionId: 'session-1', tags: ['cron'] }, fn);
expect(propagateAttributes).toHaveBeenCalledWith({ sessionId: 'session-1', tags: ['cron'] }, fn);
expect(result).toBe('done');
}); });
}); });

View File

@@ -29,6 +29,46 @@ describe('LlmClient', () => {
temperature: 0.7, temperature: 0.7,
max_tokens: undefined, max_tokens: undefined,
messages: [{ role: 'user', content: 'hi' }], messages: [{ role: 'user', content: 'hi' }],
usage: { include: true },
});
});
it('extracts OpenRouter cost from the response usage when present', async () => {
createMock.mockResolvedValue({
choices: [{ message: { content: 'hello' } }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15, cost: 0.00042 },
});
const client = new LlmClient({ baseUrl: 'https://openrouter.ai/api/v1', apiKey: 'key' });
const result = await client.chat([{ role: 'user', content: 'hi' }], { model: 'openrouter/auto' });
expect(result.usage).toEqual({
promptTokens: 10,
completionTokens: 5,
totalTokens: 15,
cost: 0.00042,
});
});
it('extracts cachedTokens from prompt_tokens_details when present', async () => {
createMock.mockResolvedValue({
choices: [{ message: { content: 'hello' } }],
usage: {
prompt_tokens: 100,
completion_tokens: 20,
total_tokens: 120,
prompt_tokens_details: { cached_tokens: 80 },
},
});
const client = new LlmClient({ baseUrl: 'http://localhost:11434/v1' });
const result = await client.chat([{ role: 'user', content: 'hi' }], { model: 'gpt-4o-mini' });
expect(result.usage).toEqual({
promptTokens: 100,
completionTokens: 20,
totalTokens: 120,
cachedTokens: 80,
}); });
}); });

View File

@@ -23,24 +23,44 @@ class ThrowingPipeline extends LlmPipeline<void, void> {
function makeTracer(trace: TraceHandle) { function makeTracer(trace: TraceHandle) {
const flush = vi.fn().mockResolvedValue(undefined); const flush = vi.fn().mockResolvedValue(undefined);
const startTrace = vi.fn().mockReturnValue(trace); const startTrace = vi.fn().mockReturnValue(trace);
const tracer = { startTrace, flush } as unknown as Tracer; const runWithSession = vi.fn().mockImplementation((_session: unknown, fn: () => unknown) => fn());
return { tracer, startTrace, flush }; const tracer = { startTrace, flush, runWithSession } as unknown as Tracer;
return { tracer, startTrace, flush, runWithSession };
} }
describe('LlmPipeline', () => { describe('LlmPipeline', () => {
it('opens one trace, runs runInternal with it, and flushes afterward', async () => { it('runs the whole call inside a session scoped to a fresh session id, opens one trace, runs runInternal, and flushes afterward', async () => {
const trace = { id: 'trace-1', startGeneration: vi.fn() } as unknown as TraceHandle; const trace = { id: 'trace-1', startGeneration: vi.fn() } as unknown as TraceHandle;
const { tracer, startTrace, flush } = makeTracer(trace); const { tracer, startTrace, flush, runWithSession } = makeTracer(trace);
const logger = {} as unknown as Logger; const logger = {} as unknown as Logger;
const pipeline = new SumPipeline(tracer, logger); const pipeline = new SumPipeline(tracer, logger);
const result = await pipeline.runAsync([1, 2, 3]); const result = await pipeline.runAsync([1, 2, 3]);
expect(result).toBe(6); expect(result).toBe(6);
expect(startTrace).toHaveBeenCalledWith('sum-pipeline'); expect(runWithSession).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: expect.any(String), tags: undefined }),
expect.any(Function)
);
expect(startTrace).toHaveBeenCalledWith('sum-pipeline', { trigger: undefined });
expect(flush).toHaveBeenCalledTimes(1); expect(flush).toHaveBeenCalledTimes(1);
}); });
it('tags the session with the given trigger', async () => {
const trace = { id: 'trace-1', startGeneration: vi.fn() } as unknown as TraceHandle;
const { tracer, startTrace, runWithSession } = makeTracer(trace);
const logger = {} as unknown as Logger;
const pipeline = new SumPipeline(tracer, logger);
await pipeline.runAsync([1, 2, 3], { trigger: 'cron' });
expect(runWithSession).toHaveBeenCalledWith(
expect.objectContaining({ tags: ['cron'] }),
expect.any(Function)
);
expect(startTrace).toHaveBeenCalledWith('sum-pipeline', { trigger: 'cron' });
});
it('still flushes the trace when runInternal throws', async () => { it('still flushes the trace when runInternal throws', async () => {
const trace = { id: 'trace-2', startGeneration: vi.fn() } as unknown as TraceHandle; const trace = { id: 'trace-2', startGeneration: vi.fn() } as unknown as TraceHandle;
const { tracer, flush } = makeTracer(trace); const { tracer, flush } = makeTracer(trace);