* feat: add opt-in Langfuse fanout collector * feat: fan out Langfuse feedback scores * docs: prepare Langfuse fanout for OSS setup * fix: clarify Langfuse fanout collector config * test: stabilize librechat suite * test: fix upload dialog import order * fix: omit empty Langfuse tenant fields * fix: gate tenant Langfuse fanout * test: cover central Langfuse env fallback * style: format Langfuse fanout config * feat: route langfuse fanout by destination * docs: clarify langfuse compose destination scope * test: remove unrelated suite stabilization * style: sort agent imports * fix: treat blank tenant fanout toggle as disabled * fix: rename tenant fanout emergency toggle * test: guard langfuse fanout collector config drift * feat: tune langfuse fanout batching * test: render fanout helm tests without dependencies * fix: narrow remote agent run config * refactor: share string normalization helper * fix: align langfuse fanout env parsing * fix(langfuse): align score fanout toggles with traces * fix(langfuse): keep central fanout config collector-only * fix(langfuse): type fanout collector config * fix(langfuse): harden tenant fanout config * feat(langfuse): support media fanout gateway * fix(langfuse): route tenant fanout through destination URL * fix(langfuse): harden fanout routing checks * ci(langfuse): test fanout gateway changes * ci(langfuse): check fanout go formatting * fix(langfuse): satisfy api typecheck |
||
|---|---|---|
| .. | ||
| cmd/langfuse-fanout | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| otelcol.yaml | ||
| README.md | ||
Langfuse Fanout Gateway
LibreChat can send tenant-scoped agent traces to a tenant Langfuse project and also copy those traces to a central Langfuse project. When trace payloads contain Langfuse media references, the gateway also copies the media upload to central and tenant Langfuse storage. This is optional and is disabled unless you explicitly deploy the fanout gateway.
The deployment is a hybrid:
- the Go gateway is the only endpoint LibreChat talks to;
- trace requests are proxied to an internal OpenTelemetry collector;
- the collector owns trace memory limiting, batching, routing, and export;
- the Go gateway owns Langfuse media create/upload/patch fanout.
How It Works
- Agent traces use Langfuse OTLP ingestion.
- LibreChat sends tenant traces to the local fanout gateway when
LANGFUSE_FANOUT_ENABLED=trueandLANGFUSE_FANOUT_COLLECTOR_URLpoints at the fanout gateway. - The gateway forwards trace requests to the internal OpenTelemetry collector
at
LANGFUSE_FANOUT_TRACE_COLLECTOR_URL. - The collector exports every trace to the central Langfuse project using
LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER. This prebuilt header is collector-only; the LibreChat app derives central score auth fromLANGFUSE_PUBLIC_KEYandLANGFUSE_SECRET_KEY. - The collector also exports tenant-enabled traces to the tenant Langfuse
project by routing on
librechat.langfuse.destination, then forwarding the tenantAuthorizationheader that LibreChat attaches to the OTLP request. - For tenant-exportable runs, LibreChat uses a destination-scoped gateway URL
like
http://langfuse-fanout-collector:4318/tenant/us. Langfuse media upload requests do not carry span attributes, so this path gives the gateway the destination needed to copy media into the tenant's Langfuse region. For traces on this path, the gateway restores the internal tenant routing attributes before handing the request to the collector. - Before export, the collector deletes the internal
librechat.langfuse.*routing attributes from central and tenant traces. - Langfuse media upload is fanned out by calling
POST /api/public/mediaon central and tenant Langfuse, returning a one-time gateway upload URL, then uploading the received bytes to each upstream presigned upload URL. The SDK'sPATCH /api/public/media/{mediaId}status call is also fanned out. - Tenant export is conditional. LibreChat uses a destination-scoped gateway URL
only when tenant keys are configured, the tenant base URL matches a configured
startup destination, and
LANGFUSE_FANOUT_TENANT_EXPORT_DISABLEDis not true. Other traces are still exported to central through the gateway without tenant auth. - User feedback scores use Langfuse's direct REST API from the LibreChat API process. Central scores use LibreChat's normal central Langfuse env config; tenant scores use tenant app configuration when tenant fanout is enabled.
Tenant Langfuse keys are expected to come from LibreChat app configuration, for example from an admin panel or another configuration data source. They are not defined in this gateway config.
Limitations
- Langfuse base URLs are startup configuration.
LANGFUSE_FANOUT_CENTRAL_BASE_URLandLANGFUSE_FANOUT_TENANT_DESTINATIONSmust be known when LibreChat and the gateway start. Tenant app configuration may choose any configured tenant destination. - Tenant Langfuse API keys can be added, changed, or disabled in tenant app configuration at runtime without restarting LibreChat or the gateway.
- Tenant app configuration must set a Langfuse base URL matching one of the startup destinations before tenant trace/score export is enabled; keys alone are treated as central-only.
LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED=truecan be set on LibreChat as an emergency switch to stop tenant trace and score export while keeping central gateway export active. When omitted, false, or blank, tenant export remains available if tenant keys and a known destination are configured.- This supports Langfuse Cloud and self-hosted Langfuse as long as each allowed tenant base URL is configured at LibreChat/gateway startup. Runtime tenant config selects from those known destinations; it does not inject arbitrary export URLs into the gateway.
- The provided Compose gateway config is a three-region Langfuse Cloud preset
(
eu,us,jp). Compose's static collector config routes only those keys; the gateway fails startup whenLANGFUSE_FANOUT_TENANT_DESTINATIONScontains a key outsideLANGFUSE_FANOUT_TRACE_DESTINATION_KEYS. For self-hosted or additional destination keys, update the collector config too or use Helm. - Helm binds the internal collector receiver to
127.0.0.1:4319because the collector is a sidecar. Compose binds it to0.0.0.0:4319on the privatelangfuse-fanoutnetwork. Do not publish the internal collector receiver outside the fanout deployment; tenant routing validation happens in the gateway before traces reach the collector. - The gateway stores short-lived one-time media upload plans in Redis. This lets
media create and byte-upload requests land on different gateway replicas.
Compose includes a private Redis container; Helm can derive the URI from the
bundled Redis chart or use an explicit
langfuseFanout.redis.uri. - The gateway requires an explicit public/internal base URL for one-time upload
URLs. Compose sets
LANGFUSE_FANOUT_PUBLIC_URLto its private gateway service URL. Helm derives the fanout Service DNS name unlesspublicUrlis set. - Media fanout is not transactional across central and tenant projects. If one
destination accepts
POST /api/public/mediaand another fails, LibreChat sees a gateway error and will not upload bytes, but the successful destination may retain a short-lived, unused media record. - Trace batching is handled by the collector. By default it flushes after 128
items or 1 second, and tenant batches are separated by the request
Authorizationmetadata. - The gateway exposes Prometheus metrics at
/metricsusing the same bearer token shape as LibreChat. SetLANGFUSE_FANOUT_METRICS_SECRET, or provideMETRICS_SECRETin the gateway environment. When neither is set,/metricsreturns 401.
Docker Compose
Set the central Langfuse destination in .env:
# Used by LibreChat for central feedback scores. Set this to the same non-EU
# region as LANGFUSE_FANOUT_CENTRAL_BASE_URL when applicable.
LANGFUSE_BASE_URL=https://cloud.langfuse.com
# Used by the gateway for central trace and media export.
LANGFUSE_FANOUT_CENTRAL_BASE_URL=https://cloud.langfuse.com
LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER=Basic <base64-public-key-colon-secret-key>
# Compose's included gateway config supports these three destination keys.
LANGFUSE_FANOUT_TENANT_DESTINATIONS=eu=https://cloud.langfuse.com,us=https://us.cloud.langfuse.com,jp=https://jp.cloud.langfuse.com
LANGFUSE_FANOUT_TRACE_DESTINATION_KEYS=eu,us,jp
LANGFUSE_FANOUT_TENANT_EU_BASE_URL=https://cloud.langfuse.com
LANGFUSE_FANOUT_TENANT_US_BASE_URL=https://us.cloud.langfuse.com
LANGFUSE_FANOUT_TENANT_JP_BASE_URL=https://jp.cloud.langfuse.com
LANGFUSE_FANOUT_TENANT_EXPORT_DISABLED=false
LANGFUSE_FANOUT_UPSTREAM_TIMEOUT=30s
LANGFUSE_FANOUT_PUBLIC_URL=http://langfuse-fanout-collector:4318
LANGFUSE_FANOUT_REDIS_URI=redis://langfuse-fanout-redis:6379
LANGFUSE_FANOUT_REDIS_USERNAME=
LANGFUSE_FANOUT_REDIS_PASSWORD=
LANGFUSE_FANOUT_REDIS_KEY_PREFIX=langfuse-fanout
LANGFUSE_FANOUT_OTEL_RECEIVER_ENDPOINT=0.0.0.0:4319
LANGFUSE_FANOUT_METRICS_SECRET=<metrics-bearer-token>
LANGFUSE_FANOUT_MEMORY_LIMIT_MIB=256
LANGFUSE_FANOUT_MEMORY_SPIKE_LIMIT_MIB=64
LANGFUSE_FANOUT_BATCH_TIMEOUT=1s
LANGFUSE_FANOUT_BATCH_SEND_SIZE=128
LANGFUSE_FANOUT_METADATA_CARDINALITY_LIMIT=1000
Langfuse Cloud base URL options:
| Region | Base URL |
|---|---|
| EU | https://cloud.langfuse.com |
| US | https://us.cloud.langfuse.com |
| JP | https://jp.cloud.langfuse.com |
Then start LibreChat with the fanout override:
docker compose -f docker-compose.yml -f docker-compose.langfuse-fanout.yml up -d
For the deployed compose stack:
docker compose -f deploy-compose.yml -f deploy-compose.langfuse-fanout.yml up -d
The override builds the fanout gateway image, sets LANGFUSE_FANOUT_ENABLED=true, and points LibreChat at
http://langfuse-fanout-collector:4318. It also starts an internal
langfuse-fanout-otel service on the private fanout network for trace export.
Helm
Create a secret containing the central Langfuse Basic auth header:
kubectl create secret generic langfuse-central \
--from-literal=LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER='Basic <base64-public-key-colon-secret-key>'
Enable the gateway in values. Use either the bundled Redis chart as shown here
or set langfuseFanout.redis.uri to an external Redis service.
redis:
enabled: true
langfuseFanout:
enabled: true
central:
baseUrl: https://cloud.langfuse.com
authHeaderSecret:
name: langfuse-central
key: LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER
metrics:
secret:
name: librechat-metrics
key: METRICS_SECRET
tenant:
destinations:
eu:
baseUrl: https://cloud.langfuse.com
us:
baseUrl: https://us.cloud.langfuse.com
jp:
baseUrl: https://jp.cloud.langfuse.com
upstreamTimeout: 30s
publicUrl: ""
otelCollector:
receiverEndpoint: 127.0.0.1:4319
redis:
uri: ""
username: ""
passwordSecret:
name: ""
key: REDIS_PASSWORD
keyPrefix: langfuse-fanout
memoryLimitMiB: 256
memorySpikeLimitMiB: 64
batchTimeout: 1s
batchSendSize: 128
metadataCardinalityLimit: 1000
The chart renders one fanout Deployment with two containers: the gateway on
4318 and an internal OpenTelemetry collector on 4319. The Service exposes
only the gateway. The chart also injects LANGFUSE_FANOUT_ENABLED plus
LANGFUSE_FANOUT_COLLECTOR_URL into the LibreChat app ConfigMap when they are
not already supplied in librechat.configEnv.
Set langfuseFanout.redis.uri when using an external Redis service. If Redis
requires auth, set langfuseFanout.redis.username and point
langfuseFanout.redis.passwordSecret.name/.key at an existing Kubernetes
Secret. When using the bundled Redis chart with auth enabled, create a password
Secret for the gateway or provide an explicit authenticated URI.
Prefer passwordSecret over embedding credentials in redis.uri, because the
URI is rendered directly into the Deployment environment.
Scale the gateway manually with langfuseFanout.replicaCount; the chart does
not create a fanout HPA. The gateway container has configurable /healthz
liveness and readiness probes under langfuseFanout.
Useful gateway metrics include:
langfuse_fanout_http_requests_totallangfuse_fanout_upstream_requests_totallangfuse_fanout_trace_exports_totallangfuse_fanout_media_upload_plans_created_totallangfuse_fanout_media_upload_plans_completed_totallangfuse_fanout_media_upload_plan_misses_totallangfuse_fanout_media_upload_plan_store_errors_totallangfuse_fanout_media_upload_byteslangfuse_fanout_media_divergence_total
langfuse_fanout_media_divergence_total{kind="media_id"} is the correctness
signal for trace/media token fanout. kind="upload_url_presence" records that
some destinations returned an upload URL while others treated the media as
already uploaded.
Notes
- The gateway handles Langfuse media uploads and proxies traces to the internal collector. Feedback scores go directly to Langfuse's REST API from the LibreChat API process.
LANGFUSE_FANOUT_CENTRAL_AUTH_HEADERmust be a full Basic auth header and is consumed by the fanout deployment only. The app does not use it for scores.LANGFUSE_FANOUT_CENTRAL_BASE_URLis also consumed by the fanout deployment only. For non-EU central feedback scores, set LibreChat's normalLANGFUSE_BASE_URLto the same central Langfuse region.- Tenant destinations default to the three configured Langfuse Cloud regions. Add or
override
langfuseFanout.tenant.destinationsin Helm for self-hosted or custom destinations. LANGFUSE_FANOUT_UPSTREAM_TIMEOUTtunes the timeout for gateway calls to Langfuse APIs and presigned media upload URLs.LANGFUSE_FANOUT_PUBLIC_URLpins the base URL returned for the SDK's one-time media upload. The gateway fails startup when it is unset or invalid; this avoids trusting requestHostorX-Forwarded-Hostheaders.LANGFUSE_FANOUT_TRACE_DESTINATION_KEYSis a startup guard that must contain every key inLANGFUSE_FANOUT_TENANT_DESTINATIONS; this prevents media fanout from accepting a destination the collector cannot route traces to.LANGFUSE_FANOUT_REDIS_URI, optionalLANGFUSE_FANOUT_REDIS_USERNAME, optionalLANGFUSE_FANOUT_REDIS_PASSWORD, andLANGFUSE_FANOUT_REDIS_KEY_PREFIXconfigure the shared one-time media upload plan store. The gateway fails startup without a Redis URI.LANGFUSE_FANOUT_OTEL_RECEIVER_ENDPOINTcontrols the internal collector receiver bind address.LANGFUSE_FANOUT_METRICS_SECRETprotects the gateway/metricsendpoint. If unset, the gateway falls back toMETRICS_SECRETwhen present.LANGFUSE_FANOUT_MEMORY_LIMIT_MIB,LANGFUSE_FANOUT_MEMORY_SPIKE_LIMIT_MIB,LANGFUSE_FANOUT_BATCH_TIMEOUT,LANGFUSE_FANOUT_BATCH_SEND_SIZE, andLANGFUSE_FANOUT_METADATA_CARDINALITY_LIMITtune the internal collector.LANGFUSE_FANOUT_COLLECTOR_URLis the local gateway URL used by LibreChat. The env name is kept for compatibility with the original collector shape; it is not a Langfuse Cloud base URL.