Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b20f4107 | ||
|
|
fb8f26a00e | ||
|
|
3d7ba475e2 | ||
|
|
03028a92b4 | ||
|
|
8a96290f03 | ||
|
|
bca09bdefe | ||
|
|
2d314250e9 | ||
|
|
4a87f402ec | ||
|
|
800b2136b9 | ||
|
|
278df5ab3c | ||
|
|
c7f9e18768 | ||
|
|
2121b40cca | ||
|
|
5ba440f2de | ||
|
|
657a3275de | ||
|
|
56bb9c8dcf | ||
|
|
66e15bcf0d | ||
|
|
5f94ca8e82 | ||
|
|
e8561fb105 | ||
|
|
18e84c6498 | ||
|
|
05152ac8d6 | ||
|
|
6cb9e24ba2 | ||
|
|
484fa424fb | ||
|
|
555aebe8c7 | ||
|
|
af29424b48 | ||
|
|
42f1c63bbf | ||
|
|
663ded0743 | ||
|
|
d7ced99d16 | ||
|
|
c979cae620 | ||
|
|
c483293b10 | ||
|
|
932913cd76 | ||
|
|
5875537132 | ||
|
|
1a53981a7a | ||
|
|
0e6b676b19 | ||
|
|
955e25555f | ||
|
|
2a11f1e8fb | ||
|
|
143a461f0c | ||
|
|
c8544690f7 | ||
|
|
05a533dd7e | ||
|
|
f3d15bb36e | ||
|
|
24c321f632 | ||
|
|
0e9cf548b2 | ||
|
|
7158a41fe9 | ||
|
|
bc384a525a | ||
|
|
3f70828df6 | ||
|
|
0988635427 | ||
|
|
41ed6fed5e | ||
|
|
eb40659e44 | ||
|
|
a22bcb1188 | ||
|
|
392fbe6c8c | ||
|
|
48d5a8f5a1 | ||
|
|
44428870f7 | ||
|
|
724a4f5d9a | ||
|
|
2358264b66 | ||
|
|
65e3b4c5ca | ||
|
|
62c413faf5 | ||
|
|
8ea14ac537 | ||
|
|
c32b84663c | ||
|
|
23ab79ba42 | ||
|
|
6ffec3337f | ||
|
|
34a2a9cddf | ||
|
|
0854429850 | ||
|
|
05fd690486 | ||
|
|
bc3f39272b | ||
|
|
96ace71f9a | ||
|
|
70415dcab5 | ||
|
|
6a18f249aa | ||
|
|
cc60d506a7 | ||
|
|
a31a7f238e | ||
|
|
41ebc77f86 | ||
|
|
cdc41cf573 | ||
|
|
c86e32f756 | ||
|
|
85576c162a | ||
|
|
93ad1e6880 | ||
|
|
97b17fc8bb | ||
|
|
04e2d9249f | ||
|
|
cb94a54f0c | ||
|
|
7c90bbed0c | ||
|
|
f133526ba6 | ||
|
|
dc014de54c | ||
|
|
c6beec1ad5 | ||
|
|
a810f7750f | ||
|
|
2b34c82e98 | ||
|
|
fe283284ef | ||
|
|
307f3d18ce | ||
|
|
46fff7148e | ||
|
|
9cce111be6 | ||
|
|
dbd7f810c1 | ||
|
|
abc3ea2b5a | ||
|
|
3d8a4f9d75 | ||
|
|
7b4bef7c2e | ||
|
|
b10353115b | ||
|
|
f3235ceccb | ||
|
|
92455b4e5a | ||
|
|
217b92d548 | ||
|
|
c6ddd3822f | ||
|
|
67d6c32ec5 | ||
|
|
d89379e693 | ||
|
|
4b8d3a764e | ||
|
|
5757fa6e38 | ||
|
|
34c3e5af08 | ||
|
|
c68ec2fca0 | ||
|
|
03dda153e8 | ||
|
|
63641521d8 | ||
|
|
29d11642c4 | ||
|
|
fb98a404d2 | ||
|
|
032fce56c4 | ||
|
|
518b8df2da | ||
|
|
70761f7ab0 | ||
|
|
a85d4ccf07 | ||
|
|
1b42eae7dd | ||
|
|
898cca56f9 | ||
|
|
83e052e769 | ||
|
|
b5f19771fa | ||
|
|
644390183b | ||
|
|
4d8e9003fe | ||
|
|
651c46a270 | ||
|
|
6b127bced0 | ||
|
|
e3a837b561 | ||
|
|
11c04f02c8 | ||
|
|
536aa138bc | ||
|
|
61b596c46a | ||
|
|
ddf43c0836 | ||
|
|
d5ab03892a | ||
|
|
918b6643d2 | ||
|
|
937b4c0f0f | ||
|
|
9b7f046e71 | ||
|
|
fe63704508 | ||
|
|
c39279f677 | ||
|
|
4206f5025d | ||
|
|
0260a4e861 | ||
|
|
244adf0dc3 | ||
|
|
dc6b98d009 | ||
|
|
b20fc5a20b | ||
|
|
f59ad5fd60 | ||
|
|
06ef41e372 | ||
|
|
e215a2db99 | ||
|
|
5e247e88c9 | ||
|
|
8e1e27c868 | ||
|
|
4648a5be8b |
@@ -51,3 +51,6 @@ REDIS_AUTH="myredissecret"
|
||||
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
|
||||
+37
-2
@@ -28,6 +28,9 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# Use CSP headers to enforce HTTPS, optional
|
||||
# LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
|
||||
# Configure base path for self-hosting, optional
|
||||
# Note: You need to build the docker image with the base path set and cannot use the pre-built docker image if you set this.
|
||||
# NEXT_PUBLIC_BASE_PATH="/app"
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
@@ -39,6 +42,10 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Logging, optional
|
||||
# LANGFUSE_LOG_LEVEL=info
|
||||
# LANGFUSE_LOG_FORMAT=text
|
||||
|
||||
# Enable experimental features, optional
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
|
||||
@@ -93,10 +100,33 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# S3_REGION=
|
||||
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
|
||||
|
||||
# S3 storage for events, optional, used to persist all incoming events
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
|
||||
# Optional prefix to be used within the bucket. Must end with `/` if set
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
# The following four options are optional and fallback to the normal SDK credential provider chain if omitted
|
||||
# See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
# Automated provisioning of default resources
|
||||
# LANGFUSE_INIT_ORG_ID=org-id
|
||||
# LANGFUSE_INIT_ORG_NAME=org-name
|
||||
# LANGFUSE_INIT_PROJECT_ID=project-id
|
||||
# LANGFUSE_INIT_PROJECT_NAME=project-name
|
||||
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-1234567890
|
||||
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-1234567890
|
||||
# LANGFUSE_INIT_USER_EMAIL=user@example.com
|
||||
# LANGFUSE_INIT_USER_NAME=User Name
|
||||
# LANGFUSE_INIT_USER_PASSWORD=password
|
||||
|
||||
|
||||
|
||||
### START Enterprise Edition Configuration
|
||||
@@ -109,6 +139,12 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# LANGFUSE_UI_DOCUMENTATION_HREF=https://docs.example.com
|
||||
# LANGFUSE_UI_SUPPORT_HREF=https://support.example.com
|
||||
# LANGFUSE_UI_FEEDBACK_HREF=https://feedback.example.com
|
||||
# LANGFUSE_UI_LOGO_LIGHT_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-light-mode.png
|
||||
# LANGFUSE_UI_LOGO_DARK_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-dark-mode.png
|
||||
# LANGFUSE_UI_DEFAULT_MODEL_ADAPTER=Anthropic # OpenAI, Anthropic, Azure
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_OPENAI=https://api.openai.com/v1
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_ANTHROPIC=https://api.anthropic.com
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_AZURE_OPENAI=https://{instanceName}.openai.azure.com/openai/deployments
|
||||
|
||||
### END Enterprise Edition Configuration
|
||||
|
||||
@@ -145,7 +181,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# SENTRY_CSP_REPORT_URI=
|
||||
# LANGFUSE_WORKER_BETTERSTACK_TOKEN=
|
||||
|
||||
|
||||
# Cloudflare Turnstile
|
||||
@@ -201,8 +236,8 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
# LANGFUSE_LOG_LEVEL=
|
||||
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
|
||||
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
|
||||
|
||||
## END Langfuse V3 Ingestion
|
||||
@@ -32,6 +32,10 @@ updates:
|
||||
patterns:
|
||||
- "lodash"
|
||||
- "@types/lodash"
|
||||
express:
|
||||
patterns:
|
||||
- "express"
|
||||
- "@types/express"
|
||||
observability:
|
||||
patterns:
|
||||
- "dd-trace"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: WorkflowCall - Deploy to ECS Service
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
type: string
|
||||
description: Deployment environment
|
||||
required: true
|
||||
service:
|
||||
type: string
|
||||
description: Name of the service to be deployed, e.g. web-ingestion, web, or worker.
|
||||
required: true
|
||||
jobs:
|
||||
ecs-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
steps:
|
||||
- name: Get app name
|
||||
uses: winterjung/split@v2
|
||||
id: split
|
||||
with:
|
||||
msg: ${{ inputs.service }}
|
||||
separator: "-"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Authenticate with AWS
|
||||
# GitHub/AWS recommend to use OIDC here: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc
|
||||
# Probably more painful to configure, but would remove all long-lived credentials.
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
- name: Login to AWS ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
- name: Build, tag, and push Docker image
|
||||
env:
|
||||
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
REPOSITORY: ${{ steps.split.outputs._0 }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
docker build \
|
||||
-t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
|
||||
-f ./${{ steps.split.outputs._0 }}/Dockerfile \
|
||||
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }} \
|
||||
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }} \
|
||||
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} \
|
||||
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }} \
|
||||
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} \
|
||||
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} \
|
||||
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
|
||||
--build-arg NEXT_PUBLIC_CRISP_WEBSITE_ID=${{ vars.NEXT_PUBLIC_CRISP_WEBSITE_ID }} \
|
||||
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
|
||||
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
|
||||
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
|
||||
.
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
|
||||
- name: Render AWS ECS Task Definition
|
||||
id: render-task-definition
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
container-name: ${{ inputs.service }}
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/${{ steps.split.outputs._0 }}:${{ github.sha }}
|
||||
task-definition-family: ${{ inputs.environment }}-${{ inputs.service }}
|
||||
- name: Update AWS ECS Service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||
with:
|
||||
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
|
||||
service: ${{ inputs.environment }}-${{ inputs.service }}
|
||||
cluster: ${{ inputs.environment }}-cluster
|
||||
wait-for-service-stability: true
|
||||
@@ -0,0 +1,105 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- production
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
service:
|
||||
description: 'Service to be deployed'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- web
|
||||
- web-ingestion
|
||||
- worker
|
||||
required: true
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- prod-eu-temp
|
||||
- prod-us
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
# Support concurrent `push` and `workflow_dispatch`` actions
|
||||
group: deploy-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
name: Deploy to ECS
|
||||
jobs:
|
||||
affected-services:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
services: ${{ steps.affected-services.outputs.result }}
|
||||
steps:
|
||||
- name: Get affected services
|
||||
id: affected-services
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
if (context.payload.inputs.service === "all") {
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return `["${context.payload.inputs.service}"]`
|
||||
}
|
||||
if (context.eventName === "push") {
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return "[]"
|
||||
result-encoding: string
|
||||
- name: Print services to build
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
services: ${{ steps.affected-services.outputs.result }}
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
console.log('Services', `${process.env.services}` ?? 'n/a');
|
||||
|
||||
affected-environments:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
environments: ${{ steps.affected-environments.outputs.result }}
|
||||
steps:
|
||||
- name: Get affected environments
|
||||
id: affected-environments
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
return `["${context.payload.inputs.environment}"]`
|
||||
}
|
||||
if (context.eventName === "push") {
|
||||
if (context.ref === "refs/heads/main") {
|
||||
return `["staging"]`
|
||||
}
|
||||
if (context.ref === "refs/heads/production") {
|
||||
return `["prod-eu-temp", "prod-us"]`
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
result-encoding: string
|
||||
- name: Print environments to build
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
environments: ${{ steps.affected-environments.outputs.result }}
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
console.log('Environments', `${process.env.environments}` ?? 'n/a');
|
||||
|
||||
ecs-deploy:
|
||||
uses: ./.github/workflows/_deploy_ecs_service.yml
|
||||
needs: [affected-services, affected-environments]
|
||||
secrets: inherit
|
||||
strategy:
|
||||
matrix:
|
||||
service: ${{ fromJson(needs.affected-services.outputs.services) }}
|
||||
environment: ${{ fromJson(needs.affected-environments.outputs.environments) }}
|
||||
with:
|
||||
service: ${{ matrix.service }}
|
||||
environment: ${{ matrix.environment }}
|
||||
@@ -40,6 +40,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and run both images from compose
|
||||
run: |
|
||||
docker compose -f docker-compose.build.yml up -d
|
||||
@@ -94,7 +97,7 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
grep -v -e '^REDIS_HOST=' .env.dev.example > .env
|
||||
grep -v -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
|
||||
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
@@ -107,13 +110,22 @@ jobs:
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Start Langfuse
|
||||
run: (pnpm run start&)
|
||||
env:
|
||||
LANGFUSE_INIT_ORG_ID: "seed-org-id"
|
||||
LANGFUSE_INIT_ORG_NAME: "Seed Org"
|
||||
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
|
||||
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
|
||||
LANGFUSE_INIT_USER_NAME: "Demo User"
|
||||
LANGFUSE_INIT_USER_PASSWORD: "password"
|
||||
|
||||
- name: run tests
|
||||
run: pnpm --filter=web run test
|
||||
@@ -317,6 +329,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to web
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4060"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to web (Staging)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4037"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to web (US temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4054"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to worker (EU temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4060"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to worker (staging)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4037"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to worker (US temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4054"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
|
||||
Vendored
+2
-2
@@ -2,10 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"name": "Web and Worker: server side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev"
|
||||
"command": "pnpm run dev"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||

|
||||

|
||||
|
||||
<div align="center"><h1> Langfuse: Open Source LLM Observability & Engineering Platform</h4></div>
|
||||
<div align="center"><h2> Debug and improve your LLM app</h4></div>
|
||||
<div align="center"><h3> LLM Observability, Prompt Management, LLM Evaluations, Datasets, LLM Metrics and Prompt Playground</h4></div>
|
||||
<div align="center"><h1>Langfuse: Open Source LLM Engineering Platform</h1></div>
|
||||
<div align="center"><h4>LLM Observability, Prompt Management, LLM Evaluations,<br/>Datasets, LLM Metrics, and Prompt Playground</h4></div>
|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
@@ -28,15 +27,15 @@
|
||||
</div>
|
||||
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
|
||||
<br/>
|
||||
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Backend Engineering, Product Engineering, and Developer Relations.</span>
|
||||
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Product Engineering and Developer Relations.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/v/langfuse.svg?style=flat-square&label=pypi+langfuse" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?style=flat-square&logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?style=flat-square&logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
@@ -93,10 +92,10 @@ docker compose up -d
|
||||
|
||||
### Self-host (docker)
|
||||
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container and a postgres database.
|
||||
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
|
||||
|
||||
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
Templated deployments: [Railway, GCP, AWS, Azure, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -108,31 +107,31 @@ You need a Langfuse public and secret key to get started. Sign up [here](https:/
|
||||
|
||||
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python (Decorators)](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
|
||||
|
||||
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
|
||||
|
||||
See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Langfuse.
|
||||
|
||||
### LLM Observability Integrations
|
||||
|
||||
| Integration | Supports | Description |
|
||||
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [API](/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
Packages that integrate with Langfuse:
|
||||
Packages integrated with Langfuse:
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Mirascope](/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [AI SDK by Vercel](/docs/sdk/typescript/example-vercel-ai) | Typescript SDK that makes streaming LLM outputs super easy. |
|
||||
| [Flowise](/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| Name | Description |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | Open source LLM app development platform with no-code builder. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Easily run open source LLMs on your own machine. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
|
||||
## Questions and feedback
|
||||
|
||||
@@ -140,13 +139,13 @@ Packages that integrate with Langfuse:
|
||||
|
||||
- [Roadmap](https://langfuse.com/roadmap)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/idea
|
||||
- [Feature Requests](https://langfuse.com/ideas)
|
||||
|
||||
### Support and feedback
|
||||
|
||||
In order of preference the best way to communicate with us:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions) (preferred): Contribute [ideas](https://langfuse.com/ideas), [support requests](https://langfuse.com/gh-support) and [report bugs](https://langfuse.com/issues)
|
||||
- [Discord](https://langfuse.com/discord): community support
|
||||
- Privately: contact at langfuse dot com
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ service:
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
|
||||
docs: Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
|
||||
docs: Optional filter to only include traces and observations before a certain datetime (ISO 8601)
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
|
||||
@@ -3,32 +3,22 @@ groups:
|
||||
local:
|
||||
generators:
|
||||
- name: fernapi/fern-openapi
|
||||
version: 0.0.28
|
||||
version: 0.0.31
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/public/generated/api
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 1.1.0-rc2
|
||||
version: 2.16.0
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
config:
|
||||
client_class_name: FernLangfuse
|
||||
improved_imports: false
|
||||
inline_request_params: false
|
||||
pydantic_config:
|
||||
require_optional_fields: false
|
||||
use_str_enums: false
|
||||
- name: fernapi/fern-typescript-node-sdk
|
||||
version: 0.7.1
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/typescript-server
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-postman
|
||||
version: 0.0.45
|
||||
output:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.30.3"
|
||||
"version": "0.43.7"
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.75.2",
|
||||
"version": "2.81.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
"import": "./dist/src/server/index.js",
|
||||
"require": "./dist/src/server/index.js"
|
||||
},
|
||||
"./src/server/auth/apiKeys": {
|
||||
"import": "./dist/src/server/auth/apiKeys.js",
|
||||
"require": "./dist/src/server/auth/apiKeys.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
@@ -51,12 +55,12 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.627.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/lib-storage": "^3.645.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@clickhouse/client": "^1.4.0",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@langchain/anthropic": "^0.3.1",
|
||||
"@langchain/core": "^0.3.3",
|
||||
"@langchain/openai": "^0.3.0",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.9.0",
|
||||
"@prisma/client": "^5.17.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
@@ -69,19 +73,21 @@
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.2.6",
|
||||
"langchain": "^0.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.24.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.14.2",
|
||||
"winston-transport": "^4.7.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/lodash": "^4.17.9",
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/pg": "^8.11.6",
|
||||
@@ -103,7 +109,7 @@
|
||||
"vitest": "^1.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.79",
|
||||
"react": "^18.0.0"
|
||||
"@types/react": "~18.2.79",
|
||||
"react": "~18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export type Account = {
|
||||
scope: string | null;
|
||||
id_token: string | null;
|
||||
session_state: string | null;
|
||||
refresh_token_expires_in: number | null;
|
||||
};
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- For OpenAI reasoning models, manually tokenizing output string does not account for reasoning tokens, thus would lead to incorrect cost
|
||||
-- So no manual tokenizer config is provided
|
||||
-- Reference: https://platform.openai.com/docs/guides/reasoning/how-reasoning-works
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- o1-preview
|
||||
('cm10ivcdp0000gix7lelmbw80', NULL, 'o1-preview', '(?i)^(o1-preview)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-preview-2024-09-12
|
||||
('cm10ivo130000n8x7qopcjjcg', NULL, 'o1-preview-2024-09-12', '(?i)^(o1-preview-2024-09-12)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-mini
|
||||
('cm10ivwo40000r1x7gg3syjq0', NULL, 'o1-mini', '(?i)^(o1-mini)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-mini-2024-09-12
|
||||
('cm10iw6p20000wgx7it1hlb22', NULL, 'o1-mini-2024-09-12', '(?i)^(o1-mini-2024-09-12)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL)
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "traces_project_id_idx"; -- covered by traces_project_id_timestamp_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "datasets_project_id_idx"; -- covered by datasets_project_id_name_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "eval_templates_project_id_idx"; -- covered by eval_templates_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "job_configurations_project_id_idx"; -- covered by job_configurations_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "job_executions_project_id_idx"; -- covered by job_executions_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "llm_api_keys_project_id_provider_idx"; -- covered by llm_api_keys_project_id_provider_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_start_date_unit_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_project_id_idx"; -- covered by observations_project_id_start_time_type_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_project_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "organization_memberships_org_id_idx"; -- covered by organization_memberships_org_id_user_id_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "posthog_integrations_project_id_idx"; -- covered by posthog_integrations_pkey (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_idx"; -- covered by prompts_project_id_name_version_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_name_version_idx"; -- covered by prompts_project_id_name_version_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "scores_project_id_idx"; -- covered by scores_project_id_name_idx
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ import { parseArgs } from "node:util";
|
||||
import { chunk } from "lodash";
|
||||
import { v4 } from "uuid";
|
||||
import { ModelUsageUnit } from "../src";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "../src/server";
|
||||
import { getDisplaySecretKey, hashSecretKey, logger } from "../src/server";
|
||||
import { encrypt } from "../src/encryption";
|
||||
import { redis } from "../src/server/redis/redis";
|
||||
|
||||
@@ -265,11 +265,11 @@ async function main() {
|
||||
project1,
|
||||
project2,
|
||||
promptIds,
|
||||
configIdsAndNames
|
||||
configIdsAndNames,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`
|
||||
logger.info(
|
||||
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`,
|
||||
);
|
||||
|
||||
await uploadObjects(
|
||||
@@ -278,7 +278,7 @@ async function main() {
|
||||
scores,
|
||||
sessions,
|
||||
events,
|
||||
comments
|
||||
comments,
|
||||
);
|
||||
|
||||
// If openai key is in environment, add it to the projects LLM API keys
|
||||
@@ -295,8 +295,8 @@ async function main() {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
|
||||
logger.warn(
|
||||
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ async function main() {
|
||||
|
||||
for (const datasetItemId of datasetItemIds) {
|
||||
const relevantObservations = observations.filter(
|
||||
(o) => o.projectId === project2.id
|
||||
(o) => o.projectId === project2.id,
|
||||
);
|
||||
const observation =
|
||||
relevantObservations[
|
||||
@@ -457,13 +457,13 @@ main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
redis?.disconnect();
|
||||
console.log("Disconnected from postgres and redis");
|
||||
logger.info("Disconnected from postgres and redis");
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
logger.error(e);
|
||||
await prisma.$disconnect();
|
||||
redis?.disconnect();
|
||||
console.log("Disconnected from postgres and redis");
|
||||
logger.info("Disconnected from postgres and redis");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -473,7 +473,7 @@ async function uploadObjects(
|
||||
scores: Prisma.ScoreCreateManyInput[],
|
||||
sessions: Prisma.TraceSessionCreateManyInput[],
|
||||
events: Prisma.ObservationCreateManyInput[],
|
||||
comments: Prisma.CommentCreateManyInput[]
|
||||
comments: Prisma.CommentCreateManyInput[],
|
||||
) {
|
||||
let promises: Prisma.PrismaPromise<unknown>[] = [];
|
||||
|
||||
@@ -487,14 +487,14 @@ async function uploadObjects(
|
||||
},
|
||||
create: chunk[0]!,
|
||||
update: {},
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -505,13 +505,13 @@ async function uploadObjects(
|
||||
promises.push(
|
||||
prisma.trace.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -521,14 +521,14 @@ async function uploadObjects(
|
||||
promises.push(
|
||||
prisma.observation.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -538,14 +538,14 @@ async function uploadObjects(
|
||||
promises.push(
|
||||
prisma.observation.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -555,13 +555,13 @@ async function uploadObjects(
|
||||
promises.push(
|
||||
prisma.score.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -571,13 +571,13 @@ async function uploadObjects(
|
||||
promises.push(
|
||||
prisma.comment.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`
|
||||
logger.info(
|
||||
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`,
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
@@ -598,7 +598,7 @@ function createObjects(
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[]
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const traces: Prisma.TraceCreateManyInput[] = [];
|
||||
const observations: Prisma.ObservationCreateManyInput[] = [];
|
||||
@@ -612,7 +612,7 @@ function createObjects(
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000)
|
||||
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
|
||||
@@ -753,11 +753,11 @@ function createObjects(
|
||||
for (let j = 0; j < Math.floor(Math.random() * 10) + 1; j++) {
|
||||
// add between 1 and 30 ms to trace timestamp
|
||||
const spanTsStart = new Date(
|
||||
traceTs.getTime() + Math.floor(Math.random() * 30)
|
||||
traceTs.getTime() + Math.floor(Math.random() * 30),
|
||||
);
|
||||
// random duration of upto 5000ms
|
||||
const spanTsEnd = new Date(
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 5000),
|
||||
);
|
||||
|
||||
const span = {
|
||||
@@ -792,22 +792,22 @@ function createObjects(
|
||||
const generationTsStart = new Date(
|
||||
spanTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
|
||||
)
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
const generationTsEnd = new Date(
|
||||
generationTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() *
|
||||
(spanTsEnd.getTime() - generationTsStart.getTime())
|
||||
)
|
||||
(spanTsEnd.getTime() - generationTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
// somewhere in the middle
|
||||
const generationTsCompletionStart = new Date(
|
||||
generationTsStart.getTime() +
|
||||
Math.floor(
|
||||
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
|
||||
)
|
||||
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3,
|
||||
),
|
||||
);
|
||||
|
||||
const promptTokens = Math.floor(Math.random() * 1000) + 300;
|
||||
@@ -828,7 +828,7 @@ function createObjects(
|
||||
const promptId =
|
||||
promptIds.get(projectId)![
|
||||
Math.floor(
|
||||
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
|
||||
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2),
|
||||
)
|
||||
];
|
||||
|
||||
@@ -910,8 +910,8 @@ function createObjects(
|
||||
const eventTs = new Date(
|
||||
spanTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
|
||||
)
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
|
||||
events.push({
|
||||
@@ -933,7 +933,7 @@ function createObjects(
|
||||
}
|
||||
// find unique sessions by id and projectid
|
||||
const uniqueSessions: Prisma.TraceSessionCreateManyInput[] = Array.from(
|
||||
new Set(sessions.map((session) => JSON.stringify(session)))
|
||||
new Set(sessions.map((session) => JSON.stringify(session))),
|
||||
).map((session) => JSON.parse(session) as Prisma.TraceSessionCreateManyInput);
|
||||
|
||||
return {
|
||||
@@ -954,7 +954,7 @@ async function generatePromptsForProject(projects: Project[]) {
|
||||
projects.map(async (project) => {
|
||||
const promptIdsForProject = await generatePrompts(project);
|
||||
promptIds.set(project.id, promptIdsForProject);
|
||||
})
|
||||
}),
|
||||
);
|
||||
return promptIds;
|
||||
}
|
||||
@@ -1140,7 +1140,7 @@ async function generateConfigsForProject(projects: Project[]) {
|
||||
projects.map(async (project) => {
|
||||
const configNameAndId = await generateConfigs(project);
|
||||
projectIdsToConfigs.set(project.id, configNameAndId);
|
||||
})
|
||||
}),
|
||||
);
|
||||
return projectIdsToConfigs;
|
||||
}
|
||||
|
||||
+67
-36
@@ -1,7 +1,7 @@
|
||||
// This file exports the prisma db connection, the Prisma Object, and the Typescript types.
|
||||
// This is not imported in the index.ts file of this package, as we must not import this into FE code.
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { env } from "process";
|
||||
import kyselyExtension from "prisma-extension-kysely";
|
||||
import {
|
||||
@@ -11,45 +11,76 @@ import {
|
||||
PostgresQueryCompiler,
|
||||
} from "kysely";
|
||||
import { DB } from ".";
|
||||
import { logger } from "./server";
|
||||
|
||||
// Instantiated according to the Prisma documentation
|
||||
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
|
||||
export class PrismaClientSingleton {
|
||||
private static instance: PrismaClient;
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error", "warn"],
|
||||
});
|
||||
};
|
||||
public static getInstance(): PrismaClient {
|
||||
if (PrismaClientSingleton.instance) {
|
||||
return PrismaClientSingleton.instance;
|
||||
}
|
||||
|
||||
const kyselySingleton = (prismaClient: PrismaClient) => {
|
||||
return prismaClient.$extends(
|
||||
kyselyExtension({
|
||||
kysely: (driver) =>
|
||||
new Kysely<DB>({
|
||||
dialect: {
|
||||
// This is where the magic happens!
|
||||
createDriver: () => driver,
|
||||
// Don't forget to customize these to match your database!
|
||||
createAdapter: () => new PostgresAdapter(),
|
||||
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||
var kyselyPrisma: undefined | ReturnType<typeof kyselySingleton>;
|
||||
const client = new PrismaClient<
|
||||
Prisma.PrismaClientOptions,
|
||||
"warn" | "error" | "query"
|
||||
>({
|
||||
log: [
|
||||
{ emit: "event", level: "query" },
|
||||
{ emit: "event", level: "error" },
|
||||
{ emit: "event", level: "warn" },
|
||||
],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV === "development") {
|
||||
client.$on("query", (event) => {
|
||||
logger.info(`prisma:query ${event.query}, ${event.duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
client.$on("warn", (event) => {
|
||||
logger.warn(`prisma:warn ${event.message}`);
|
||||
});
|
||||
|
||||
client.$on("error", (event) => {
|
||||
logger.error(`prisma:error ${event.message}`);
|
||||
});
|
||||
|
||||
PrismaClientSingleton.instance = client;
|
||||
|
||||
return PrismaClientSingleton.instance;
|
||||
}
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
||||
export const kyselyPrisma = globalThis.kyselyPrisma ?? kyselySingleton(prisma);
|
||||
export class KyselySingleton {
|
||||
private static instance: { $kysely: Kysely<DB> };
|
||||
|
||||
public static getInstance() {
|
||||
if (KyselySingleton.instance) {
|
||||
return KyselySingleton.instance;
|
||||
}
|
||||
|
||||
KyselySingleton.instance = PrismaClientSingleton.getInstance().$extends(
|
||||
kyselyExtension({
|
||||
kysely: (driver) =>
|
||||
new Kysely<DB>({
|
||||
dialect: {
|
||||
// This is where the magic happens!
|
||||
createDriver: () => driver,
|
||||
// Don't forget to customize these to match your database!
|
||||
createAdapter: () => new PostgresAdapter(),
|
||||
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
return KyselySingleton.instance;
|
||||
}
|
||||
}
|
||||
|
||||
export const prisma = PrismaClientSingleton.getInstance();
|
||||
export const kyselyPrisma = KyselySingleton.getInstance();
|
||||
|
||||
export * from "@prisma/client";
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
|
||||
|
||||
@@ -21,7 +21,7 @@ const EnvSchema = z.object({
|
||||
.string()
|
||||
.length(
|
||||
64,
|
||||
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
|
||||
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32",
|
||||
)
|
||||
.optional(),
|
||||
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
|
||||
@@ -38,6 +38,11 @@ const EnvSchema = z.object({
|
||||
.number()
|
||||
.positive()
|
||||
.default(60 * 10),
|
||||
SALT: z.string().optional(), // used by components imported by web package
|
||||
LANGFUSE_LOG_LEVEL: z
|
||||
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
||||
.optional(),
|
||||
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(process.env);
|
||||
|
||||
@@ -19,6 +19,8 @@ export enum BatchExportFileFormat {
|
||||
|
||||
export enum BatchExportTableName {
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Generations = "generations",
|
||||
}
|
||||
|
||||
export const exportOptions: Record<
|
||||
|
||||
@@ -83,13 +83,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -97,7 +97,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
message: "Value must be either 0 or 1",
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -161,7 +161,7 @@ export const ScorePropsAgainstConfig = z.union([
|
||||
*/
|
||||
export const filterAndValidateDbScoreList = (
|
||||
scores: Score[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): APIScore[] =>
|
||||
scores.reduce((acc, ts) => {
|
||||
const result = APIScoreSchema.safeParse(ts);
|
||||
@@ -198,14 +198,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -215,14 +215,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -256,7 +256,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
|
||||
trace: z.object({
|
||||
userId: z.string().nullish(),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
export const GetScoresResponse = z.object({
|
||||
data: z.array(LegacyGetScoreResponseDataV1),
|
||||
@@ -265,7 +265,7 @@ export const GetScoresResponse = z.object({
|
||||
|
||||
export const legacyFilterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
|
||||
@@ -278,7 +278,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
|
||||
);
|
||||
|
||||
// GET /scores/{scoreId}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
export * from "./constants";
|
||||
export * from "./queries";
|
||||
export * from "./interfaces/filters";
|
||||
export * from "./interfaces/orderBy";
|
||||
export * from "./interfaces/cloudConfigSchema";
|
||||
export * from "./interfaces/parseDbOrg";
|
||||
export * from "./tableDefinitions";
|
||||
export * from "./types";
|
||||
export * from "./filterToPrisma";
|
||||
export * from "./orderByToPrisma";
|
||||
export * from "./tracesTable";
|
||||
export * from "./server/auth/auth";
|
||||
export * from "./server/auth/apiKeys";
|
||||
export * from "./observationsTable";
|
||||
export * from "./utils/zod";
|
||||
export * from "./utils/json";
|
||||
|
||||
@@ -6,6 +6,7 @@ export const RateLimitResource = z.enum([
|
||||
"public-api",
|
||||
"public-api-metrics",
|
||||
"prompts",
|
||||
"legacy-ingestion",
|
||||
]);
|
||||
|
||||
// result of the rate limit check.
|
||||
|
||||
@@ -52,7 +52,8 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
name: "Time To First Token (s)",
|
||||
id: "timeToFirstToken",
|
||||
type: "number",
|
||||
internal: 'o."completion_start_time" - o."start_time"',
|
||||
internal:
|
||||
'EXTRACT(EPOCH FROM (o."completion_start_time" - o."start_time"))',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { createSessionsAllQuery } from "./createSessionsAllQuery";
|
||||
@@ -0,0 +1,81 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import { env } from "../../env";
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
async function generateKeySet() {
|
||||
return {
|
||||
pk: `pk-lf-${randomUUID()}`,
|
||||
sk: `sk-lf-${randomUUID()}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function createAndAddApiKeysToDb(p: {
|
||||
prisma: PrismaClient;
|
||||
projectId: string;
|
||||
note?: string;
|
||||
predefinedKeys?: {
|
||||
secretKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
}) {
|
||||
const salt = env.SALT;
|
||||
if (!salt) {
|
||||
throw new Error("SALT is not set");
|
||||
}
|
||||
|
||||
const { pk, sk } = p.predefinedKeys
|
||||
? { pk: p.predefinedKeys.publicKey, sk: p.predefinedKeys.secretKey }
|
||||
: await generateKeySet();
|
||||
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
const hashFromProvidedKey = createShaHash(sk, salt);
|
||||
|
||||
const apiKey = await p.prisma.apiKey.create({
|
||||
data: {
|
||||
projectId: p.projectId,
|
||||
publicKey: pk,
|
||||
hashedSecretKey: hashedSk,
|
||||
displaySecretKey: displaySk,
|
||||
fastHashedSecretKey: hashFromProvidedKey,
|
||||
note: p.note,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: apiKey.id,
|
||||
createdAt: apiKey.createdAt,
|
||||
note: apiKey.note,
|
||||
publicKey: apiKey.publicKey,
|
||||
secretKey: sk,
|
||||
displaySecretKey: displaySk,
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
export function generateSecretKey() {
|
||||
return `sk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function generatePublicKey() {
|
||||
return `pk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
export async function generateKeySet() {
|
||||
const pk = generatePublicKey();
|
||||
const sk = generateSecretKey();
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
return {
|
||||
pk,
|
||||
sk,
|
||||
hashedSk,
|
||||
displaySk,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
+5
-4
@@ -1,8 +1,9 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
|
||||
import { FilterState } from "./types";
|
||||
import { filterOperators, timeFilter } from "./interfaces/filters";
|
||||
import { ColumnDefinition, type TableNames } from "../tableDefinitions";
|
||||
import { FilterState } from "../types";
|
||||
import { filterOperators, timeFilter } from "../interfaces/filters";
|
||||
import { z } from "zod";
|
||||
import { logger } from "./index";
|
||||
|
||||
const operatorReplacements = {
|
||||
"any of": "IN",
|
||||
@@ -51,7 +52,7 @@ export function tableColumnsToSqlFilter(
|
||||
c.name === filter.column || c.id === filter.column
|
||||
);
|
||||
if (!col) {
|
||||
console.error("Invalid filter column", filter.column);
|
||||
logger.error("Invalid filter column", filter.column);
|
||||
throw new Error("Invalid filter column: " + filter.column);
|
||||
}
|
||||
const colPrisma = Prisma.raw(col.internal);
|
||||
@@ -3,7 +3,8 @@ export * from "./services/email/organizationInvitation/sendMembershipInvitationE
|
||||
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
|
||||
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
|
||||
export * from "./services/PromptService";
|
||||
export * from "./auth/auth";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
@@ -24,4 +25,8 @@ export * from "./auth/types";
|
||||
export * from "./ingestion/legacy/index";
|
||||
export * from "./queues";
|
||||
export * from "./ingestion/legacy/EventProcessor";
|
||||
export * from "./orderByToPrisma";
|
||||
export * from "./filterToPrisma";
|
||||
export * from "./instrumentation";
|
||||
export * from "./logger";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -18,6 +18,8 @@ import { mergeJson } from "../../../utils/json";
|
||||
import { jsonSchema } from "../../../utils/zod";
|
||||
import { prisma } from "../../../db";
|
||||
import { LegacyIngestionAccessScope } from ".";
|
||||
import { logger } from "../../logger";
|
||||
import { ApiAccessScope } from "../../auth/types";
|
||||
|
||||
export interface EventProcessor {
|
||||
auth(apiScope: LegacyIngestionAccessScope): void;
|
||||
@@ -47,7 +49,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
async convertToObservation(
|
||||
apiScope: LegacyIngestionAccessScope,
|
||||
existingObservation: Observation | null
|
||||
existingObservation: Omit<Observation, "input" | "output"> | null
|
||||
): Promise<{
|
||||
id: string;
|
||||
create: Prisma.ObservationUncheckedCreateInput;
|
||||
@@ -119,7 +121,8 @@ export class ObservationProcessor implements EventProcessor {
|
||||
// Token counts
|
||||
const [newInputCount, newOutputCount] =
|
||||
"usage" in this.event.body
|
||||
? this.calculateTokenCounts(
|
||||
? await this.calculateTokenCounts(
|
||||
apiScope.projectId,
|
||||
this.event.body,
|
||||
this.calculateTokenDelegate,
|
||||
internalModel ?? undefined,
|
||||
@@ -187,8 +190,9 @@ export class ObservationProcessor implements EventProcessor {
|
||||
: undefined;
|
||||
|
||||
// Only null if promptName and promptVersion are set but prompt is not found
|
||||
if (prompt === null)
|
||||
console.warn("Prompt not found for observation", this.event.body);
|
||||
if (prompt === null) {
|
||||
logger.warn("Prompt not found for observation", this.event.body);
|
||||
}
|
||||
|
||||
const observationId = this.event.body.id ?? v4();
|
||||
|
||||
@@ -309,7 +313,8 @@ export class ObservationProcessor implements EventProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
calculateTokenCounts(
|
||||
async calculateTokenCounts(
|
||||
projectId: string,
|
||||
body:
|
||||
| z.infer<typeof legacyObservationCreateEvent>["body"]
|
||||
| z.infer<typeof generationCreateEvent>["body"],
|
||||
@@ -318,29 +323,59 @@ export class ObservationProcessor implements EventProcessor {
|
||||
text: unknown;
|
||||
}) => number | undefined,
|
||||
model?: Model,
|
||||
existingObservation?: Observation
|
||||
existingObservation?: Omit<Observation, "input" | "output">
|
||||
) {
|
||||
const newPromptTokens =
|
||||
body.usage?.input ??
|
||||
((body.input || existingObservation?.input) && model && model.tokenizerId
|
||||
? calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.input ?? existingObservation?.input,
|
||||
})
|
||||
: undefined);
|
||||
let newPromptTokens = body.usage?.input;
|
||||
if (newPromptTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.input) {
|
||||
newPromptTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.input,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`No input provided, trying to calculate for id: ${existingObservation?.id}`
|
||||
);
|
||||
const observationInput = await prisma.observation.findFirst({
|
||||
where: { id: existingObservation?.id, projectId: projectId },
|
||||
select: {
|
||||
input: true,
|
||||
},
|
||||
});
|
||||
|
||||
const newCompletionTokens =
|
||||
body.usage?.output ??
|
||||
((body.output || existingObservation?.output) &&
|
||||
model &&
|
||||
model.tokenizerId
|
||||
? calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.output ?? existingObservation?.output,
|
||||
})
|
||||
: undefined);
|
||||
newPromptTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: observationInput?.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [newPromptTokens, newCompletionTokens];
|
||||
let newCompletionTokens = body.usage?.output;
|
||||
|
||||
if (newCompletionTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.output) {
|
||||
newCompletionTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.output,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`No output provided, trying to calculate for id: ${existingObservation?.id}`
|
||||
);
|
||||
const observationOutput = await prisma.observation.findFirst({
|
||||
where: { id: existingObservation?.id, projectId: projectId },
|
||||
select: {
|
||||
output: true,
|
||||
},
|
||||
});
|
||||
newCompletionTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: observationOutput?.output,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [newPromptTokens ?? undefined, newCompletionTokens ?? undefined];
|
||||
}
|
||||
|
||||
static calculateTokenCosts(
|
||||
@@ -408,7 +443,43 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
const existingObservation = this.event.body.id
|
||||
? await prisma.observation.findFirst({
|
||||
where: { id: this.event.body.id },
|
||||
select: {
|
||||
// do not select I/O to spare our db
|
||||
input: false,
|
||||
output: false,
|
||||
|
||||
id: true,
|
||||
traceId: true,
|
||||
projectId: true,
|
||||
type: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
name: true,
|
||||
metadata: true,
|
||||
parentObservationId: true,
|
||||
level: true,
|
||||
statusMessage: true,
|
||||
version: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
model: true,
|
||||
internalModelId: true,
|
||||
modelParameters: true,
|
||||
promptTokens: true,
|
||||
completionTokens: true,
|
||||
totalTokens: true,
|
||||
unit: true,
|
||||
inputCost: true,
|
||||
outputCost: true,
|
||||
totalCost: true,
|
||||
calculatedInputCost: true,
|
||||
calculatedOutputCost: true,
|
||||
calculatedTotalCost: true,
|
||||
completionStartTime: true,
|
||||
promptId: true,
|
||||
internalModel: true,
|
||||
},
|
||||
where: { id: this.event.body.id, projectId: apiScope.projectId },
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -455,11 +526,8 @@ export class TraceProcessor implements EventProcessor {
|
||||
|
||||
const internalId = body.id ?? v4();
|
||||
|
||||
console.log(
|
||||
"Trying to create trace, project ",
|
||||
apiScope.projectId,
|
||||
", id:",
|
||||
internalId
|
||||
logger.debug(
|
||||
`Trying to create trace, project ${apiScope.projectId}, id: ${internalId}`
|
||||
);
|
||||
|
||||
const existingTrace = await prisma.trace.findFirst({
|
||||
@@ -619,7 +687,7 @@ export class SdkLogProcessor implements EventProcessor {
|
||||
|
||||
process() {
|
||||
try {
|
||||
console.log("SDK Log", this.event);
|
||||
logger.info("SDK Log", this.event);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
|
||||
@@ -8,10 +8,11 @@ import { IngestionUtils } from "../IngestionUtils";
|
||||
import { IngestionEventType } from "../types";
|
||||
import { redis } from "../../redis/redis";
|
||||
import { env } from "../../../env";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export async function enqueueIngestionEvents(
|
||||
projectId: string,
|
||||
events: IngestionEventType[]
|
||||
events: IngestionEventType[],
|
||||
) {
|
||||
const ingestionFlushQueue = getIngestionFlushQueue();
|
||||
|
||||
@@ -31,8 +32,8 @@ export async function enqueueIngestionEvents(
|
||||
event,
|
||||
redis,
|
||||
ingestionFlushQueue,
|
||||
batchTimestamp
|
||||
)
|
||||
batchTimestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +45,11 @@ async function enqueueSingleIngestionEvent(
|
||||
event: IngestionEventType,
|
||||
redis: Redis,
|
||||
ingestionFlushQueue: IngestionFlushQueue,
|
||||
batchTimestamp: string
|
||||
batchTimestamp: string,
|
||||
): Promise<void> {
|
||||
if (!("id" in event.body && event.body.id)) {
|
||||
console.warn(
|
||||
`Received ingestion event without id: ${JSON.stringify(event)}`
|
||||
logger.warn(
|
||||
`Received ingestion event without id: ${JSON.stringify(event)}`,
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { redis } from "../../redis/redis";
|
||||
import { backOff } from "exponential-backoff";
|
||||
import { Model } from "../../..";
|
||||
import { enqueueIngestionEvents } from "./enqueueIngestionEvents";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export type BatchResult = {
|
||||
result: unknown;
|
||||
@@ -49,9 +50,9 @@ type LegacyIngestionAuthHeaderVerificationResult =
|
||||
export const handleBatch = async (
|
||||
events: z.infer<typeof ingestionApiSchema>["batch"],
|
||||
authCheck: LegacyIngestionAuthHeaderVerificationResult,
|
||||
calculateTokenDelegate: (p: TokenCountInput) => number | undefined
|
||||
calculateTokenDelegate: (p: TokenCountInput) => number | undefined,
|
||||
) => {
|
||||
console.log(`handling ingestion ${events.length} events`);
|
||||
logger.debug(`handling ingestion ${events.length} events`);
|
||||
|
||||
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
|
||||
|
||||
@@ -69,7 +70,7 @@ export const handleBatch = async (
|
||||
return await handleSingleEvent(
|
||||
singleEvent,
|
||||
authCheck.scope,
|
||||
calculateTokenDelegate
|
||||
calculateTokenDelegate,
|
||||
);
|
||||
});
|
||||
results.push({
|
||||
@@ -79,7 +80,7 @@ export const handleBatch = async (
|
||||
}); // Push each result into the array
|
||||
} catch (error) {
|
||||
// Handle or log the error if `handleSingleEvent` fails
|
||||
console.error("Error handling event:", error);
|
||||
logger.error("Error handling event:", error);
|
||||
// Decide how to handle the error: rethrow, continue, or push an error object to results
|
||||
// For example, push an error object:
|
||||
errors.push({
|
||||
@@ -93,9 +94,9 @@ export const handleBatch = async (
|
||||
if (env.CLICKHOUSE_URL) {
|
||||
try {
|
||||
await enqueueIngestionEvents(authCheck.scope.projectId, events);
|
||||
console.log(`Added ${events.length} ingestion events to queue`);
|
||||
logger.info(`Added ${events.length} ingestion events to queue`);
|
||||
} catch (err) {
|
||||
console.error("Error adding ingestion events to queue", err);
|
||||
logger.error("Error adding ingestion events to queue", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +108,10 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
|
||||
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
|
||||
retry: (e: Error, attemptNumber: number) => {
|
||||
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
|
||||
console.log("not retrying auth error");
|
||||
logger.info("not retrying auth error");
|
||||
return false;
|
||||
}
|
||||
console.log(`retrying processing events ${attemptNumber}`);
|
||||
logger.info(`retrying processing events ${attemptNumber}`);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -122,7 +123,7 @@ const handleSingleEvent = async (
|
||||
calculateTokenDelegate: (p: {
|
||||
model: Model;
|
||||
text: unknown;
|
||||
}) => number | undefined
|
||||
}) => number | undefined,
|
||||
) => {
|
||||
const { body } = event;
|
||||
let restEvent = body;
|
||||
@@ -137,8 +138,8 @@ const handleSingleEvent = async (
|
||||
restEvent = rest;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`
|
||||
logger.info(
|
||||
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
|
||||
);
|
||||
|
||||
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
|
||||
@@ -159,7 +160,7 @@ const handleSingleEvent = async (
|
||||
case eventTypes.GENERATION_UPDATE:
|
||||
processor = new ObservationProcessor(
|
||||
cleanedEvent,
|
||||
calculateTokenDelegate
|
||||
calculateTokenDelegate,
|
||||
);
|
||||
break;
|
||||
case eventTypes.SCORE_CREATE: {
|
||||
@@ -201,7 +202,7 @@ export function cleanEvent(obj: unknown): unknown {
|
||||
}
|
||||
|
||||
export const isNotNullOrUndefined = <T>(
|
||||
val?: T | null
|
||||
val?: T | null,
|
||||
): val is Exclude<T, null | undefined> => !isUndefinedOrNull(val);
|
||||
|
||||
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
|
||||
@@ -209,7 +210,7 @@ export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
|
||||
|
||||
export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
batchResults: BatchResult[],
|
||||
projectId: string
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
const traceEvents: TraceUpsertEventType[] = batchResults
|
||||
.filter((result) => result.type === eventTypes.TRACE_CREATE) // we only have create, no update.
|
||||
@@ -219,17 +220,17 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
"id" in result.result
|
||||
? // ingestion API only gets traces for one projectId
|
||||
{ traceId: result.result.id as string, projectId }
|
||||
: null
|
||||
: null,
|
||||
)
|
||||
.filter(isNotNullOrUndefined);
|
||||
|
||||
try {
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && redis) {
|
||||
console.log(`Sending ${traceEvents.length} events to worker via Redis`);
|
||||
logger.info(`Sending ${traceEvents.length} events to worker via Redis`);
|
||||
|
||||
const queue = getTraceUpsertQueue();
|
||||
if (!queue) {
|
||||
console.error("TraceUpsertQueue not initialized");
|
||||
logger.error("TraceUpsertQueue not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,7 +240,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
env.LANGFUSE_WORKER_PASSWORD &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
) {
|
||||
console.log(`Sending ${traceEvents.length} events to worker via HTTP`);
|
||||
logger.info(`Sending ${traceEvents.length} events to worker via HTTP`);
|
||||
const body: EventBodyType = {
|
||||
name: EventName.TraceUpsert,
|
||||
payload: traceEvents,
|
||||
@@ -253,7 +254,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(
|
||||
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD
|
||||
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD,
|
||||
).toString("base64"),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
@@ -262,6 +263,6 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending events to worker", error);
|
||||
logger.error("Error sending events to worker", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function findModel(p: {
|
||||
unit?: string;
|
||||
startTime?: Date;
|
||||
};
|
||||
existingDbObservation?: Observation;
|
||||
existingDbObservation?: Omit<Observation, "input" | "output">;
|
||||
}): Promise<Model | null> {
|
||||
const { event, existingDbObservation } = p;
|
||||
// either get the model from the existing observation
|
||||
|
||||
@@ -15,7 +15,7 @@ type ValidateAndInflateScoreParams = {
|
||||
};
|
||||
|
||||
export async function validateAndInflateScore(
|
||||
params: ValidateAndInflateScoreParams
|
||||
params: ValidateAndInflateScoreParams,
|
||||
): Promise<Score> {
|
||||
const { body, projectId } = params;
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function validateAndInflateScore(
|
||||
|
||||
if (!config || !validateDbScoreConfigSafe(config).success)
|
||||
throw new LangfuseNotFoundError(
|
||||
"The configId you provided does not match a valid config in this project"
|
||||
"The configId you provided does not match a valid config in this project",
|
||||
);
|
||||
|
||||
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
|
||||
@@ -47,7 +47,7 @@ export async function validateAndInflateScore(
|
||||
|
||||
if (!validation.success) {
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ function inferDataType(value: string | number): ScoreDataType {
|
||||
|
||||
function mapStringValueToNumericValue(
|
||||
config: ValidatedScoreConfig,
|
||||
label: string
|
||||
label: string,
|
||||
): number | null {
|
||||
return (
|
||||
config.categories?.find((category) => category.label === label)?.value ??
|
||||
@@ -71,7 +71,7 @@ function mapStringValueToNumericValue(
|
||||
}
|
||||
|
||||
function inflateScoreBody(
|
||||
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig }
|
||||
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig },
|
||||
): Score {
|
||||
const { body, projectId, scoreId, config } = params;
|
||||
|
||||
@@ -105,25 +105,25 @@ function inflateScoreBody(
|
||||
|
||||
function validateConfigAgainstBody(
|
||||
body: any,
|
||||
config: ValidatedScoreConfig
|
||||
config: ValidatedScoreConfig,
|
||||
): void {
|
||||
const { maxValue, minValue, categories, dataType: configDataType } = config;
|
||||
|
||||
if (body.dataType && body.dataType !== configDataType) {
|
||||
throw new InvalidRequestError(
|
||||
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`
|
||||
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.isArchived) {
|
||||
throw new InvalidRequestError(
|
||||
"Config is archived and cannot be used to create new scores. Please restore the config first."
|
||||
"Config is archived and cannot be used to create new scores. Please restore the config first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.name !== body.name) {
|
||||
throw new InvalidRequestError(
|
||||
`Name mismatch based on config: expected ${config.name}, got ${body.name}`
|
||||
`Name mismatch based on config: expected ${config.name}, got ${body.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ function validateConfigAgainstBody(
|
||||
|
||||
if (!dataTypeValidation.success) {
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score body not valid against provided config data type.`
|
||||
`Ingested score body not valid against provided config data type.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ function validateConfigAgainstBody(
|
||||
.join(", ");
|
||||
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score body not valid against provided config: ${errorDetails}`
|
||||
`Ingested score body not valid against provided config: ${errorDetails}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,54 +3,42 @@ import * as dd from "dd-trace";
|
||||
|
||||
// type CallbackFn<T> = () => T;
|
||||
|
||||
export type TCarrier = {
|
||||
traceparent?: string;
|
||||
tracestate?: string;
|
||||
};
|
||||
|
||||
export type SpanCtx = {
|
||||
name: string;
|
||||
spanKind?: opentelemetry.SpanKind; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#spankind
|
||||
rootSpan?: boolean; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#traces
|
||||
traceScope?: string;
|
||||
traceContext?: TCarrier;
|
||||
};
|
||||
|
||||
type AsyncCallbackFn<T> = () => Promise<T>;
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
|
||||
|
||||
export async function instrumentAsync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: AsyncCallbackFn<T>
|
||||
callback: AsyncCallbackFn<T>,
|
||||
): Promise<T> {
|
||||
return await getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
const result = await callback();
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
traceException(ex as opentelemetry.Exception, span);
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
const activeContext = ctx.traceContext
|
||||
? opentelemetry.propagation.extract(
|
||||
opentelemetry.context.active(),
|
||||
ctx.traceContext,
|
||||
)
|
||||
: opentelemetry.context.active();
|
||||
|
||||
type SyncCallbackFn<T> = () => T;
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: SyncCallbackFn<T>
|
||||
): T {
|
||||
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: ctx.rootSpan,
|
||||
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
(span) => {
|
||||
activeContext,
|
||||
async (span) => {
|
||||
try {
|
||||
const result = callback();
|
||||
const result = await callback(span);
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
@@ -58,16 +46,62 @@ export function instrumentSync<T>(
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: SyncCallbackFn<T>,
|
||||
): T {
|
||||
const activeContext = ctx.traceContext
|
||||
? opentelemetry.propagation.extract(
|
||||
opentelemetry.context.active(),
|
||||
ctx.traceContext,
|
||||
)
|
||||
: opentelemetry.context.active();
|
||||
|
||||
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
activeContext,
|
||||
(span) => {
|
||||
try {
|
||||
const result = callback(span);
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
traceException(ex as opentelemetry.Exception, span);
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export const getCurrentSpan = () => opentelemetry.trace.getActiveSpan();
|
||||
|
||||
export const addTraceContext = <T extends Record<string, any>>(
|
||||
input: T,
|
||||
): T & { _tracecontext?: TCarrier } => {
|
||||
const context = {};
|
||||
opentelemetry.propagation.inject(opentelemetry.context.active(), context);
|
||||
|
||||
return {
|
||||
...input,
|
||||
_tracecontext: context,
|
||||
};
|
||||
};
|
||||
|
||||
export const traceException = (
|
||||
ex: unknown,
|
||||
span?: opentelemetry.Span,
|
||||
code?: string
|
||||
code?: string,
|
||||
) => {
|
||||
const activeSpan = span ?? getCurrentSpan();
|
||||
|
||||
@@ -115,7 +149,7 @@ export const traceException = (
|
||||
|
||||
export const addUserToSpan = (
|
||||
attributes: { userId?: string; projectId?: string; email?: string },
|
||||
span?: opentelemetry.Span
|
||||
span?: opentelemetry.Span,
|
||||
) => {
|
||||
const activeSpan = span ?? getCurrentSpan();
|
||||
|
||||
@@ -138,7 +172,7 @@ export const recordGauge = (
|
||||
| {
|
||||
[tag: string]: string | number;
|
||||
}
|
||||
| undefined
|
||||
| undefined,
|
||||
) => {
|
||||
dd.dogstatsd.gauge(stat, value, tags);
|
||||
};
|
||||
@@ -146,7 +180,7 @@ export const recordGauge = (
|
||||
export const recordIncrement = (
|
||||
stat: string,
|
||||
value?: number | undefined,
|
||||
tags?: { [tag: string]: string | number } | undefined
|
||||
tags?: { [tag: string]: string | number } | undefined,
|
||||
) => {
|
||||
dd.dogstatsd.increment(stat, value, tags);
|
||||
};
|
||||
@@ -154,7 +188,7 @@ export const recordIncrement = (
|
||||
export const recordHistogram = (
|
||||
stat: string,
|
||||
value?: number | undefined,
|
||||
tags?: { [tag: string]: string | number } | undefined
|
||||
tags?: { [tag: string]: string | number } | undefined,
|
||||
) => {
|
||||
dd.dogstatsd.histogram(stat, value, tags);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type ZodSchema } from "zod";
|
||||
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import {
|
||||
AIMessage,
|
||||
@@ -12,20 +14,12 @@ import {
|
||||
import { IterableReadableStream } from "@langchain/core/utils/stream";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
ChatMessageRole,
|
||||
LLMFunctionCall,
|
||||
ModelParams,
|
||||
LLMAdapter,
|
||||
} from "./types";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { JsonOutputFunctionsParser } from "langchain/output_parsers";
|
||||
import { ChatMessage, ChatMessageRole, ModelParams, LLMAdapter } from "./types";
|
||||
|
||||
type LLMCompletionParams = {
|
||||
messages: ChatMessage[];
|
||||
modelParams: ModelParams;
|
||||
functionCall?: LLMFunctionCall;
|
||||
structuredOutputSchema?: ZodSchema;
|
||||
callbacks?: BaseCallbackHandler[];
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
@@ -51,7 +45,7 @@ export async function fetchLLMCompletion(
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
functionCall: LLMFunctionCall;
|
||||
structuredOutputSchema: ZodSchema;
|
||||
}
|
||||
): Promise<unknown>;
|
||||
|
||||
@@ -121,18 +115,41 @@ export async function fetchLLMCompletion(
|
||||
throw new Error("This model provider is not supported.");
|
||||
}
|
||||
|
||||
if (params.functionCall) {
|
||||
const functionCallingModel = chatModel.bind({
|
||||
functions: [
|
||||
{
|
||||
...params.functionCall,
|
||||
parameters: zodToJsonSchema(params.functionCall.parameters),
|
||||
},
|
||||
],
|
||||
function_call: { name: params.functionCall.name },
|
||||
});
|
||||
const outputParser = new JsonOutputFunctionsParser();
|
||||
return await functionCallingModel.pipe(outputParser).invoke(finalMessages);
|
||||
if (params.structuredOutputSchema) {
|
||||
return await (chatModel as ChatOpenAI) // Typecast necessary due to https://github.com/langchain-ai/langchainjs/issues/6795
|
||||
.withStructuredOutput(params.structuredOutputSchema)
|
||||
.invoke(finalMessages);
|
||||
}
|
||||
|
||||
/*
|
||||
Workaround OpenAI o1 while in beta:
|
||||
|
||||
This is a temporary workaround to avoid sending system messages to OpenAI's O1 models.
|
||||
O1 models do not support in beta:
|
||||
- system messages
|
||||
- top_p
|
||||
- max_tokens at all, one has to use max_completion_tokens instead
|
||||
- temperature different than 1
|
||||
|
||||
Reference: https://platform.openai.com/docs/guides/reasoning/beta-limitations
|
||||
*/
|
||||
if (modelParams.model.startsWith("o1-")) {
|
||||
return await new ChatOpenAI({
|
||||
openAIApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
temperature: 1,
|
||||
maxTokens: undefined,
|
||||
topP: undefined,
|
||||
callbacks,
|
||||
maxRetries,
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
})
|
||||
.pipe(new StringOutputParser())
|
||||
.invoke(
|
||||
finalMessages.filter((message) => message._getType() !== "system")
|
||||
);
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import z from "zod";
|
||||
export type PromptVariable = { name: string; value: string; isUsed: boolean };
|
||||
|
||||
export type ChatMessage = {
|
||||
role: ChatMessageRole;
|
||||
role: ChatMessageRole | string; // Users may ingest any string as role via API/SDK
|
||||
content: string;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ export enum ChatMessageRole {
|
||||
Assistant = "assistant",
|
||||
}
|
||||
|
||||
export const ChatMessageDefaultRoleSchema = z.nativeEnum(ChatMessageRole);
|
||||
|
||||
export type ModelParams = {
|
||||
provider: string;
|
||||
adapter: LLMAdapter;
|
||||
@@ -52,6 +54,10 @@ export const openAIModels = [
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"o1-preview",
|
||||
"o1-preview-2024-09-12",
|
||||
"o1-mini",
|
||||
"o1-mini-2024-09-12",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0613",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { env } from "../env";
|
||||
import winston from "winston";
|
||||
import Transport from "winston-transport";
|
||||
import { getCurrentSpan } from "./instrumentation";
|
||||
|
||||
class TracedTransport extends Transport {
|
||||
constructor(opts: Transport.TransportStreamOptions = {}) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
log(info: Record<string, any>, callback: () => void): void {
|
||||
setImmediate(() => {
|
||||
this.emit("logged", info);
|
||||
});
|
||||
|
||||
const currentSpan = getCurrentSpan();
|
||||
info.trace_id = currentSpan?.spanContext().traceId;
|
||||
info.span_id = currentSpan?.spanContext().spanId;
|
||||
console.log(JSON.stringify(info));
|
||||
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
const getWinstonLogger = (
|
||||
nodeEnv: "development" | "production" | "test",
|
||||
minLevel = "info",
|
||||
) => {
|
||||
const textLoggerFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.timestamp(),
|
||||
winston.format.align(),
|
||||
winston.format.printf((info) => {
|
||||
const logMessage = `${info.timestamp} ${info.level} ${info.message}`;
|
||||
return info.stack ? `${logMessage}\n${info.stack}` : logMessage;
|
||||
}),
|
||||
);
|
||||
|
||||
const jsonLoggerFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.timestamp(),
|
||||
winston.format.json(),
|
||||
);
|
||||
|
||||
const format =
|
||||
env.LANGFUSE_LOG_FORMAT === "text" ? textLoggerFormat : jsonLoggerFormat;
|
||||
const transport =
|
||||
env.LANGFUSE_LOG_FORMAT === "text"
|
||||
? new winston.transports.Console()
|
||||
: new TracedTransport();
|
||||
return winston.createLogger({
|
||||
level: minLevel,
|
||||
format: format,
|
||||
transports: [transport],
|
||||
});
|
||||
};
|
||||
|
||||
export const logger = getWinstonLogger(env.NODE_ENV, env.LANGFUSE_LOG_LEVEL);
|
||||
+8
-7
@@ -2,8 +2,9 @@ import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { ColumnDefinition } from "./tableDefinitions/types";
|
||||
import type { OrderByState } from "./interfaces/orderBy";
|
||||
import type { ColumnDefinition } from "../tableDefinitions/types";
|
||||
import type { OrderByState } from "../interfaces/orderBy";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* Convert orderBy to SQL ORDER BY clause
|
||||
@@ -13,7 +14,7 @@ import type { OrderByState } from "./interfaces/orderBy";
|
||||
*/
|
||||
export function orderByToPrismaSql(
|
||||
orderBy: OrderByState,
|
||||
tableColumns: ColumnDefinition[]
|
||||
tableColumns: ColumnDefinition[],
|
||||
): Prisma.Sql {
|
||||
if (!orderBy) {
|
||||
return Prisma.sql`ORDER BY t.timestamp DESC`;
|
||||
@@ -22,11 +23,11 @@ export function orderByToPrismaSql(
|
||||
const col = tableColumns.find(
|
||||
// TODO: Only use id instead of name.
|
||||
// It's less error-prone & decouples data fetching from the human-readable UI labels
|
||||
(c) => c.name === orderBy.column || c.id === orderBy.column
|
||||
(c) => c.name === orderBy.column || c.id === orderBy.column,
|
||||
);
|
||||
|
||||
if (!col) {
|
||||
console.log("Invalid filter column", orderBy.column);
|
||||
logger.warn("Invalid filter column", orderBy.column);
|
||||
throw new Error("Invalid filter column: " + orderBy.column);
|
||||
}
|
||||
|
||||
@@ -34,12 +35,12 @@ export function orderByToPrismaSql(
|
||||
const orderByOrder = z.enum(["ASC", "DESC"]);
|
||||
const order = orderByOrder.safeParse(orderBy.order);
|
||||
if (!order.success) {
|
||||
console.log("Invalid order", orderBy.order);
|
||||
logger.warn("Invalid order", orderBy.order);
|
||||
throw new Error("Invalid order: " + orderBy.order);
|
||||
}
|
||||
|
||||
// Both column and order are safe, can use raw SQL
|
||||
return Prisma.raw(
|
||||
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`
|
||||
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ObservationView, Prisma } from "@prisma/client";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "../filterToPrisma";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
import { observationsTableCols } from "../../observationsTable";
|
||||
import { TableFilters } from "./types";
|
||||
|
||||
type AdditionalObservationFields = {
|
||||
traceName: string | null;
|
||||
promptName: string | null;
|
||||
promptVersion: string | null;
|
||||
traceTags: Array<string>;
|
||||
};
|
||||
|
||||
type FullObservation = AdditionalObservationFields & ObservationView;
|
||||
|
||||
export type FullObservations = Array<FullObservation>;
|
||||
|
||||
export type FullObservationsWithScores = Array<
|
||||
FullObservation & { scores?: Record<string, string[] | number[]> | null }
|
||||
>;
|
||||
|
||||
export type IOAndMetadataOmittedObservations = Array<
|
||||
Omit<ObservationView, "input" | "output" | "metadata"> &
|
||||
AdditionalObservationFields
|
||||
>;
|
||||
|
||||
export function parseGetAllGenerationsInput(filters: TableFilters) {
|
||||
const searchCondition = filters.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${filters.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
filters.filter ?? [],
|
||||
observationsTableCols,
|
||||
"observations"
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
filters.orderBy,
|
||||
observationsTableCols
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = filters.filter?.find(
|
||||
(f) => f.column === "Start Time" && f.type === "datetime"
|
||||
);
|
||||
|
||||
const datetimeFilter =
|
||||
startTimeFilter && startTimeFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
startTimeFilter.operator,
|
||||
startTimeFilter.value
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
return {
|
||||
searchCondition,
|
||||
filterCondition,
|
||||
orderByCondition,
|
||||
datetimeFilter,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGenerationsQuery({
|
||||
projectId,
|
||||
datetimeFilter = Prisma.empty,
|
||||
page,
|
||||
limit,
|
||||
searchCondition = Prisma.empty,
|
||||
filterCondition = Prisma.empty,
|
||||
orderByCondition = Prisma.empty,
|
||||
selectIOAndMetadata = false,
|
||||
selectScoreValues = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
datetimeFilter?: Prisma.Sql;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchCondition?: Prisma.Sql;
|
||||
filterCondition?: Prisma.Sql;
|
||||
orderByCondition?: Prisma.Sql;
|
||||
selectIOAndMetadata?: boolean;
|
||||
selectScoreValues?: boolean;
|
||||
}) {
|
||||
return Prisma.sql`
|
||||
WITH scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
|
||||
avg(value) avg_value,
|
||||
comment
|
||||
FROM
|
||||
scores
|
||||
WHERE
|
||||
project_id = ${projectId}
|
||||
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
|
||||
GROUP BY
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
comment
|
||||
ORDER BY
|
||||
trace_id
|
||||
) tmp
|
||||
GROUP BY
|
||||
trace_id,
|
||||
observation_id
|
||||
)
|
||||
SELECT
|
||||
${selectScoreValues ? Prisma.sql`s_avg."scores_values" AS "scores",` : Prisma.empty}
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o."modelParameters",
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
${selectIOAndMetadata ? Prisma.sql`o.input, o.output, o.metadata,` : Prisma.empty}
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.time_to_first_token as "timeToFirstToken",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.unit,
|
||||
o.level,
|
||||
o.status_message as "statusMessage",
|
||||
o.version,
|
||||
o.model_id as "modelId",
|
||||
o.input_price as "inputPrice",
|
||||
o.output_price as "outputPrice",
|
||||
o.total_price as "totalPrice",
|
||||
o.calculated_input_cost as "calculatedInputCost",
|
||||
o.calculated_output_cost as "calculatedOutputCost",
|
||||
o.calculated_total_cost as "calculatedTotalCost",
|
||||
o."latency",
|
||||
o.prompt_id as "promptId",
|
||||
p.name as "promptName",
|
||||
p.version as "promptVersion",
|
||||
t.tags as "traceTags"
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id AND t.project_id = ${projectId}
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
LEFT JOIN prompts p ON p.id = o.prompt_id AND p.project_id = ${projectId}
|
||||
WHERE
|
||||
o.project_id = ${projectId}
|
||||
AND o.type = 'GENERATION'
|
||||
${datetimeFilter}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
|
||||
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
|
||||
`;
|
||||
}
|
||||
+5
-5
@@ -2,10 +2,10 @@ import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
|
||||
import { singleFilter } from "../interfaces/filters";
|
||||
import { orderBy } from "../interfaces/orderBy";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
import { sessionsViewCols } from "../tableDefinitions/index";
|
||||
import { sessionsViewCols } from "../../tableDefinitions";
|
||||
|
||||
const GetSessionTableSQLParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
@@ -22,7 +22,7 @@ export const createSessionsAllQuery = (
|
||||
options?: {
|
||||
ignoreOrderBy?: boolean; // used by session.metrics and session.all.totalCount
|
||||
sessionIdList?: string[]; // used by session.metrics
|
||||
}
|
||||
},
|
||||
): Prisma.Sql => {
|
||||
const { projectId, filter, orderBy, page, limit } =
|
||||
GetSessionTableSQLParamsSchema.parse(params);
|
||||
@@ -30,7 +30,7 @@ export const createSessionsAllQuery = (
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
filter ?? [],
|
||||
sessionsViewCols,
|
||||
"sessions"
|
||||
"sessions",
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { TableFilters } from "./types";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "../filterToPrisma";
|
||||
import { tracesTableCols } from "../../tracesTable";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
|
||||
export function parseTraceAllFilters(input: TableFilters) {
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter ?? [],
|
||||
tracesTableCols,
|
||||
"traces"
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(input.orderBy, tracesTableCols);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const timeseriesFilter = input.filter?.find(
|
||||
(f) => f.column === "Timestamp" && f.type === "datetime"
|
||||
);
|
||||
|
||||
const observationTimeseriesFilter =
|
||||
timeseriesFilter && timeseriesFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
timeseriesFilter.operator,
|
||||
timeseriesFilter.value
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
t."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."external_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."user_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
return {
|
||||
filterCondition,
|
||||
orderByCondition,
|
||||
observationTimeseriesFilter,
|
||||
searchCondition,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTracesQuery({
|
||||
select,
|
||||
projectId,
|
||||
observationTimeseriesFilter = Prisma.empty,
|
||||
page,
|
||||
limit,
|
||||
searchCondition = Prisma.empty,
|
||||
filterCondition = Prisma.empty,
|
||||
orderByCondition = Prisma.empty,
|
||||
selectScoreValues = false,
|
||||
}: {
|
||||
select: Prisma.Sql;
|
||||
projectId: string;
|
||||
observationTimeseriesFilter?: Prisma.Sql;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchCondition?: Prisma.Sql;
|
||||
filterCondition?: Prisma.Sql;
|
||||
orderByCondition?: Prisma.Sql;
|
||||
selectScoreValues?: boolean;
|
||||
}) {
|
||||
return Prisma.sql`
|
||||
SELECT
|
||||
${select}
|
||||
FROM
|
||||
"traces" AS t
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
SUM(prompt_tokens) AS "promptTokens",
|
||||
SUM(completion_tokens) AS "completionTokens",
|
||||
SUM(total_tokens) AS "totalTokens",
|
||||
SUM(calculated_total_cost) AS "calculatedTotalCost",
|
||||
SUM(calculated_input_cost) AS "calculatedInputCost",
|
||||
SUM(calculated_output_cost) AS "calculatedOutputCost"
|
||||
FROM
|
||||
"observations_view"
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
) AS generation_metrics ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) AS "observationCount",
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency",
|
||||
COALESCE(
|
||||
MAX(CASE WHEN level = 'ERROR' THEN 'ERROR' END),
|
||||
MAX(CASE WHEN level = 'WARNING' THEN 'WARNING' END),
|
||||
MAX(CASE WHEN level = 'DEFAULT' THEN 'DEFAULT' END),
|
||||
'DEBUG'
|
||||
) AS "level"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
) AS observation_metrics ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
|
||||
FROM (
|
||||
SELECT
|
||||
name,
|
||||
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
|
||||
AVG(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND t."project_id" = ${projectId}
|
||||
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
|
||||
GROUP BY
|
||||
name
|
||||
) tmp
|
||||
) AS s_avg ON true
|
||||
WHERE
|
||||
t."project_id" = ${projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
|
||||
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { createSessionsAllQuery } from "./createSessionsAllQuery";
|
||||
export { createTracesQuery, parseTraceAllFilters } from "./createTracesQuery";
|
||||
export {
|
||||
createGenerationsQuery,
|
||||
parseGetAllGenerationsInput,
|
||||
type FullObservations,
|
||||
type FullObservationsWithScores,
|
||||
type IOAndMetadataOmittedObservations,
|
||||
} from "./createGenerationsQuery";
|
||||
@@ -0,0 +1,14 @@
|
||||
import z from "zod";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { optionalPaginationZod } from "../../utils/zod";
|
||||
|
||||
const TableFilterSchema = z.object({
|
||||
projectId: z.string(),
|
||||
filter: z.array(singleFilter).nullish(),
|
||||
searchQuery: z.string().nullish(),
|
||||
orderBy: orderBy,
|
||||
...optionalPaginationZod,
|
||||
});
|
||||
|
||||
export type TableFilters = z.infer<typeof TableFilterSchema>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { ingestionBatchEvent } from ".";
|
||||
import { ingestionBatchEvent, TCarrier } from ".";
|
||||
|
||||
export enum EventName {
|
||||
TraceUpsert = "TraceUpsert",
|
||||
@@ -80,23 +80,27 @@ export type TQueueJobTypes = {
|
||||
id: string;
|
||||
payload: TraceUpsertEventType;
|
||||
name: QueueJobs.TraceUpsert;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.EvaluationExecution]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: EvalExecutionEventType;
|
||||
name: QueueJobs.EvaluationExecution;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.BatchExport]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: BatchExportJobType;
|
||||
name: QueueJobs.BatchExportJob;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.LegacyIngestionQueue]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: LegacyIngestionEventType;
|
||||
name: QueueJobs.LegacyIngestionJob;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,8 +15,12 @@ export const getBatchExportQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
removeOnFail: 10_000,
|
||||
attempts: 2,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -18,9 +18,13 @@ export const getIngestionFlushQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 1_000,
|
||||
removeOnFail: 100_000,
|
||||
delay: env.LANGFUSE_INGESTION_FLUSH_DELAY_MS,
|
||||
attempts: env.LANGFUSE_INGESTION_FLUSH_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -2,28 +2,36 @@ import { Queue } from "bullmq";
|
||||
import { QueueName, TQueueJobTypes } from "../queues";
|
||||
import { createNewRedisInstance } from "./redis";
|
||||
|
||||
let legacyIngestionQueue: Queue<
|
||||
TQueueJobTypes[QueueName.LegacyIngestionQueue]
|
||||
> | null = null;
|
||||
export class LegacyIngestionQueue {
|
||||
private static instance: Queue<
|
||||
TQueueJobTypes[QueueName.LegacyIngestionQueue]
|
||||
> | null = null;
|
||||
|
||||
export const getLegacyIngestionQueue = () => {
|
||||
if (legacyIngestionQueue) return legacyIngestionQueue;
|
||||
public static getInstance(): Queue<
|
||||
TQueueJobTypes[QueueName.LegacyIngestionQueue]
|
||||
> | null {
|
||||
if (LegacyIngestionQueue.instance) return LegacyIngestionQueue.instance;
|
||||
|
||||
const newRedis = createNewRedisInstance();
|
||||
const newRedis = createNewRedisInstance({ enableOfflineQueue: false });
|
||||
|
||||
legacyIngestionQueue = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
|
||||
QueueName.LegacyIngestionQueue,
|
||||
{
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
attempts: 5,
|
||||
LegacyIngestionQueue.instance = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
|
||||
QueueName.LegacyIngestionQueue,
|
||||
{
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
: null;
|
||||
)
|
||||
: null;
|
||||
|
||||
return legacyIngestionQueue;
|
||||
};
|
||||
return LegacyIngestionQueue.instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import Redis from "ioredis";
|
||||
import Redis, { RedisOptions } from "ioredis";
|
||||
import { env } from "../../env";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export const createNewRedisInstance = () => {
|
||||
export const createNewRedisInstance = (
|
||||
additionalOptions: Partial<RedisOptions> = {},
|
||||
) => {
|
||||
return env.REDIS_CONNECTION_STRING
|
||||
? new Redis(env.REDIS_CONNECTION_STRING, {
|
||||
maxRetriesPerRequest: null,
|
||||
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
|
||||
...additionalOptions,
|
||||
})
|
||||
: env.REDIS_HOST
|
||||
? new Redis({
|
||||
@@ -14,6 +18,7 @@ export const createNewRedisInstance = () => {
|
||||
password: String(env.REDIS_AUTH),
|
||||
maxRetriesPerRequest: null, // Set to `null` to disable retrying
|
||||
enableAutoPipelining: env.REDIS_ENABLE_AUTO_PIPELINING === "true",
|
||||
...additionalOptions,
|
||||
})
|
||||
: null;
|
||||
};
|
||||
@@ -22,7 +27,7 @@ const createRedisClient = () => {
|
||||
try {
|
||||
return createNewRedisInstance();
|
||||
} catch (e) {
|
||||
console.error(e, "Failed to connect to redis");
|
||||
logger.error("Failed to connect to redis", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,12 @@ export const getTraceUpsertQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 1_000,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
@@ -30,7 +35,7 @@ export const getTraceUpsertQueue = () => {
|
||||
};
|
||||
|
||||
export function convertTraceUpsertEventsToRedisEvents(
|
||||
events: TraceUpsertEventType[]
|
||||
events: TraceUpsertEventType[],
|
||||
) {
|
||||
const uniqueTracesPerProject = events.reduce((acc, event) => {
|
||||
if (!acc.get(event.projectId)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prompt, PrismaClient } from "@prisma/client";
|
||||
import { Redis } from "ioredis";
|
||||
import { env } from "../../env";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class PromptService {
|
||||
private cacheEnabled: boolean;
|
||||
@@ -11,7 +12,7 @@ export class PromptService {
|
||||
private redis: Redis | null,
|
||||
private metricIncrementer?: // used for otel metrics
|
||||
(name: string, value?: number) => void,
|
||||
cacheEnabled?: boolean // used for testing
|
||||
cacheEnabled?: boolean, // used for testing
|
||||
) {
|
||||
this.cacheEnabled =
|
||||
Boolean(redis) &&
|
||||
@@ -25,7 +26,7 @@ export class PromptService {
|
||||
const cachedPrompt = await this.getCachedPrompt(params);
|
||||
|
||||
this.incrementMetric(
|
||||
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss
|
||||
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss,
|
||||
);
|
||||
|
||||
if (cachedPrompt) {
|
||||
@@ -117,7 +118,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
public async lockCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -133,7 +134,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
public async unlockCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -149,7 +150,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private async isCacheLocked(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<boolean> {
|
||||
const lockKey = this.getLockKey(params);
|
||||
|
||||
@@ -163,14 +164,14 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private getLockKey(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
// Important to *pre*fix LOCK as otherwise it would be deleted by deleteKeysByPrefix
|
||||
return `LOCK:${this.getCacheKeyPrefix(params)}`;
|
||||
}
|
||||
|
||||
public async invalidateCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -187,7 +188,7 @@ export class PromptService {
|
||||
await this.redis?.del([...(keys ?? []), keyIndexKey]);
|
||||
|
||||
this.logInfo(
|
||||
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`
|
||||
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logError("Error deleting keys for prefix", cacheKeyPrefix, e);
|
||||
@@ -203,23 +204,23 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private getCacheKeyPrefix(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
return `prompt:${params.projectId}:${params.promptName}`;
|
||||
}
|
||||
|
||||
private getKeyIndexKey(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
return `prompt_key_index:${params.projectId}:${params.promptName}`;
|
||||
}
|
||||
|
||||
private logError(message: string, ...args: any[]) {
|
||||
console.error(`[PromptService] ${message}`, ...args);
|
||||
logger.error(`[PromptService] ${message}`, ...args);
|
||||
}
|
||||
|
||||
private logInfo(message: string, ...args: any[]) {
|
||||
console.log(`[PromptService] ${message}`, ...args);
|
||||
logger.info(`[PromptService] ${message}`, ...args);
|
||||
}
|
||||
|
||||
private incrementMetric(name: Metrics, value: number = 1) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { Readable } from "stream";
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { logger } from "../logger";
|
||||
|
||||
type UploadFile = {
|
||||
fileName: string;
|
||||
@@ -15,20 +20,26 @@ export class S3StorageService {
|
||||
private bucketName: string;
|
||||
|
||||
constructor(params: {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
accessKeyId: string | undefined;
|
||||
secretAccessKey: string | undefined;
|
||||
bucketName: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
endpoint: string | undefined;
|
||||
region: string | undefined;
|
||||
}) {
|
||||
const { accessKeyId, secretAccessKey, bucketName, endpoint, region } =
|
||||
params;
|
||||
|
||||
// Use accessKeyId and secretAccessKey if provided or fallback to default credentials
|
||||
const credentials =
|
||||
accessKeyId !== undefined && secretAccessKey !== undefined
|
||||
? {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
credentials,
|
||||
endpoint,
|
||||
region,
|
||||
});
|
||||
@@ -56,15 +67,30 @@ export class S3StorageService {
|
||||
|
||||
return { signedUrl };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
logger.error(`Failed to upload file to ${fileName}`, err);
|
||||
throw new Error("Failed to upload to S3 or generate signed URL");
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadJson(path: string, body: Record<string, unknown>) {
|
||||
const putCommand = new PutObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: path,
|
||||
Body: JSON.stringify(body),
|
||||
ContentType: "application/json",
|
||||
});
|
||||
|
||||
try {
|
||||
await this.client.send(putCommand);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to upload JSON to S3 ${path}`, err);
|
||||
throw Error("Failed to upload JSON to S3");
|
||||
}
|
||||
}
|
||||
|
||||
private async getSignedUrl(
|
||||
fileName: string,
|
||||
ttlSeconds: number
|
||||
ttlSeconds: number,
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await getSignedUrl(
|
||||
@@ -74,9 +100,10 @@ export class S3StorageService {
|
||||
Key: fileName,
|
||||
ResponseContentDisposition: `attachment; filename="${fileName}"`,
|
||||
}),
|
||||
{ expiresIn: ttlSeconds }
|
||||
{ expiresIn: ttlSeconds },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to generate presigned URL for ${fileName}`, err);
|
||||
throw Error("Failed to generate signed URL");
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -59,6 +59,10 @@ export const BatchExportSuccessEmailTemplate = ({
|
||||
download. The download link will expire in{" "}
|
||||
<strong>{expiresInHours} hours</strong>.
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Please note data exports do not reflect custom column ordering or
|
||||
visibility.
|
||||
</Text>
|
||||
<Section className="mb-4 mt-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-white no-underline"
|
||||
|
||||
+4
-4
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import { BatchExportSuccessEmailTemplate } from "./BatchExportSuccessEmailTemplate";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
type SendBatchExportSuccessParams = {
|
||||
env: Partial<
|
||||
@@ -24,8 +25,7 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
expiresInHours,
|
||||
}: SendBatchExportSuccessParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error("Missing environment variables for sending email.");
|
||||
|
||||
logger.error("Missing environment variables for sending email.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await mailer.sendMail({
|
||||
@@ -51,6 +51,6 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
+7
-6
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import MembershipInvitationTemplate from "./MembershipInvitationEmailTemplate";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
const langfuseUrls = {
|
||||
US: "https://us.cloud.langfuse.com",
|
||||
@@ -34,8 +35,8 @@ export const sendMembershipInvitationEmail = async ({
|
||||
orgName,
|
||||
}: SendMembershipInvitationParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error(
|
||||
"Missing environment variables for sending membership invitation email."
|
||||
logger.error(
|
||||
"Missing environment variables for sending membership invitation email.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -49,8 +50,8 @@ export const sendMembershipInvitationEmail = async ({
|
||||
|
||||
const authUrl = getAuthURL();
|
||||
if (!authUrl) {
|
||||
console.error(
|
||||
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable."
|
||||
logger.error(
|
||||
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +68,7 @@ export const sendMembershipInvitationEmail = async ({
|
||||
inviteLink: authUrl,
|
||||
emailFromAddress: env.EMAIL_FROM_ADDRESS,
|
||||
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await mailer.sendMail({
|
||||
@@ -77,6 +78,6 @@ export const sendMembershipInvitationEmail = async ({
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
+4
-3
@@ -56,8 +56,9 @@ const ResetPasswordTemplate = ({ url }: ResetPasswordTemplateProps) => {
|
||||
</Button>
|
||||
</Section>
|
||||
<Text className="text-center text-xs leading-6 text-[#666666]">
|
||||
If you do not want to change your password or didn't request
|
||||
a reset, you can ignore and delete this email.
|
||||
The link is valid for 10 minutes. If you do not want to change
|
||||
your password or didn't request a reset, you can ignore and
|
||||
delete this email.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
@@ -76,7 +77,7 @@ export async function sendResetPasswordVerificationRequest(
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
subject: `Forgot your password?`,
|
||||
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nIf you do not want to change your password or didn't request a reset, you can ignore and delete this email.`,
|
||||
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nThe link is valid for 10 minutes. If you do not want to change your password or didn't request a reset, you can ignore and delete this email.`,
|
||||
html: htmlTemplate,
|
||||
});
|
||||
const failed = result.rejected.concat(result.pending).filter(Boolean);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Transform, type TransformCallback } from "stream";
|
||||
import { stringify } from "./stringify";
|
||||
|
||||
const DELIMITER = ",";
|
||||
|
||||
export function transformStreamToCsv(): Transform {
|
||||
let isFirstChunk = true;
|
||||
let headers: string[] = [];
|
||||
@@ -15,15 +17,19 @@ export function transformStreamToCsv(): Transform {
|
||||
if (isFirstChunk) {
|
||||
// Extract headers from the first object
|
||||
headers = Object.keys(row);
|
||||
this.push(headers.join(",") + "\n");
|
||||
this.push(headers.join(DELIMITER) + "\n");
|
||||
isFirstChunk = false;
|
||||
}
|
||||
|
||||
// Convert the object to a CSV line and push it
|
||||
const csvRow = headers.map((header) => {
|
||||
const field = row[header];
|
||||
const str = stringify(field);
|
||||
let str = stringify(field);
|
||||
|
||||
// escape and format fields that contain commas
|
||||
if (str.includes(",")) {
|
||||
str = `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
if (str.startsWith('"') && str.endsWith('"')) {
|
||||
return str;
|
||||
} else {
|
||||
@@ -31,7 +37,7 @@ export function transformStreamToCsv(): Transform {
|
||||
}
|
||||
});
|
||||
|
||||
this.push(csvRow.join(",") + "\n");
|
||||
this.push(csvRow.join(DELIMITER) + "\n");
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
@@ -78,28 +78,28 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
name: "Input Tokens",
|
||||
id: "inputTokens",
|
||||
type: "number",
|
||||
internal: 'tm."promptTokens"',
|
||||
internal: 'generation_metrics."promptTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Output Tokens",
|
||||
id: "outputTokens",
|
||||
type: "number",
|
||||
internal: 'tm."completionTokens"',
|
||||
internal: 'generation_metrics."completionTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Total Tokens",
|
||||
id: "totalTokens",
|
||||
type: "number",
|
||||
internal: 'tm."totalTokens"',
|
||||
internal: 'generation_metrics."totalTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Usage",
|
||||
id: "usage",
|
||||
type: "number",
|
||||
internal: 'tm."totalTokens"',
|
||||
internal: 'generation_metrics."totalTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
name: "Latency (s)",
|
||||
id: "latency",
|
||||
type: "number",
|
||||
internal: "tl.latency",
|
||||
internal: "observation_metrics.latency",
|
||||
},
|
||||
{
|
||||
name: "Input Cost ($)",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type z } from "zod";
|
||||
import { singleFilter } from "./interfaces/filters";
|
||||
import { singleFilter, timeFilter } from "./interfaces/filters";
|
||||
|
||||
// to be sent to the server
|
||||
export type TimeFilter = z.infer<typeof timeFilter>;
|
||||
export type FilterCondition = z.infer<typeof singleFilter>;
|
||||
export type FilterState = FilterCondition[];
|
||||
|
||||
|
||||
Generated
+905
-1117
File diff suppressed because it is too large
Load Diff
+39
-9
@@ -33,16 +33,43 @@ ENV DOCKER_BUILD 1
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
|
||||
# pass public variables in build step
|
||||
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
ARG NEXT_PUBLIC_DEMO_ORG_ID
|
||||
ARG NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
ARG NEXT_PUBLIC_SIGN_UP_DISABLED
|
||||
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
ARG NEXT_PUBLIC_CRISP_WEBSITE_ID
|
||||
|
||||
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
ENV NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
ARG NEXT_PUBLIC_DEMO_ORG_ID
|
||||
ENV NEXT_PUBLIC_DEMO_ORG_ID=$NEXT_PUBLIC_DEMO_ORG_ID
|
||||
ARG NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
ENV NEXT_PUBLIC_DEMO_PROJECT_ID=$NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
ARG NEXT_PUBLIC_SIGN_UP_DISABLED
|
||||
ENV NEXT_PUBLIC_SIGN_UP_DISABLED=$NEXT_PUBLIC_SIGN_UP_DISABLED
|
||||
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST
|
||||
ARG NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
ENV NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE=$NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ARG NEXT_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
|
||||
ARG NEXT_PUBLIC_BASE_PATH
|
||||
ENV NEXT_PUBLIC_BASE_PATH=$NEXT_PUBLIC_BASE_PATH
|
||||
|
||||
# Sentry already needs to be set on build time to upload sourcemaps
|
||||
# This must not be set for OSS releases as we would share our Sentry secret.
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
|
||||
ARG SENTRY_ORG
|
||||
ENV SENTRY_ORG=$SENTRY_ORG
|
||||
ARG SENTRY_PROJECT
|
||||
ENV SENTRY_PROJECT=$SENTRY_PROJECT
|
||||
|
||||
# Accept build id as NEXT_PUBLIC_BUILD_ID
|
||||
ARG NEXT_PUBLIC_BUILD_ID
|
||||
ENV NEXT_PUBLIC_BUILD_ID=$NEXT_PUBLIC_BUILD_ID
|
||||
|
||||
# Copy source code of isolated subworkspace
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
@@ -55,8 +82,11 @@ RUN rm -f ./web/src/middleware.ts
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
# set the CI flag to true to get CI specific logs
|
||||
ENV CI true
|
||||
|
||||
RUN turbo run build --filter=web
|
||||
|
||||
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
|
||||
|
||||
+51
-45
@@ -11,8 +11,8 @@ import { env } from "./src/env.mjs";
|
||||
* img-src https to allow loading images from SSO providers
|
||||
*/
|
||||
const cspHeader = `
|
||||
default-src 'self' https://*.langfuse.com https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://ph.langfuse.com https://static.cloudflareinsights.com https://*.stripe.com;
|
||||
default-src 'self' https://*.langfuse.com https://*.langfuse.dev https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://static.cloudflareinsights.com https://*.stripe.com;
|
||||
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
|
||||
img-src 'self' https: blob: data: https://client.crisp.chat https://image.crisp.chat https://storage.crisp.chat;
|
||||
font-src 'self' https://client.crisp.chat;
|
||||
@@ -22,7 +22,7 @@ const cspHeader = `
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
connect-src 'self' https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io https://ph.langfuse.com;
|
||||
connect-src 'self' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io;
|
||||
media-src 'self' https://client.crisp.chat;
|
||||
${env.LANGFUSE_CSP_ENFORCE_HTTPS === "true" ? "upgrade-insecure-requests; block-all-mixed-content;" : ""}
|
||||
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
|
||||
@@ -55,6 +55,7 @@ const nextConfig = {
|
||||
],
|
||||
},
|
||||
poweredByHeader: false,
|
||||
basePath: env.NEXT_PUBLIC_BASE_PATH,
|
||||
|
||||
/**
|
||||
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
|
||||
@@ -104,22 +105,22 @@ const nextConfig = {
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
? [
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// all files in /public/generated are public and can be accessed from any origin, e.g. to render an API reference based on our openapi schema
|
||||
{
|
||||
@@ -149,39 +150,44 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const sentryOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
export default withSentryConfig(nextConfig, {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
authToken: env.SENTRY_AUTH_TOKEN,
|
||||
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: !process.env.CI,
|
||||
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// See the sections below for information on the following options:
|
||||
// 'Configure Source Maps':
|
||||
// - disableServerWebpackPlugin
|
||||
// - disableClientWebpackPlugin
|
||||
// - hideSourceMaps
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Automatically annotate React components to show their full name in breadcrumbs and session replay
|
||||
reactComponentAnnotation: {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
// tunnelRoute: "/api/monitoring-tunnel",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
// - widenClientFileUpload
|
||||
// 'Configure Legacy Browser Support':
|
||||
// - transpileClientSDK
|
||||
// 'Configure Serverside Auto-instrumentation':
|
||||
// - autoInstrumentServerFunctions
|
||||
// - excludeServerRoutes
|
||||
// 'Configure Tunneling':
|
||||
// - tunnelRoute
|
||||
|
||||
// An auth token is required for uploading source maps.
|
||||
authToken: env.SENTRY_AUTH_TOKEN,
|
||||
tunnelRoute: "/api/monitoring-tunnel",
|
||||
};
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
export default withSentryConfig(nextConfig, sentryOptions);
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: false,
|
||||
});
|
||||
|
||||
+20
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.75.2",
|
||||
"version": "2.81.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -27,13 +27,17 @@
|
||||
"@baselime/trpc-opentelemetry-middleware": "^0.1.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lint": "^6.8.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "1.7.18",
|
||||
"@headlessui/tailwindcss": "0.2.1",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@langchain/anthropic": "^0.3.1",
|
||||
"@langchain/core": "^0.3.3",
|
||||
"@langchain/openai": "^0.3.0",
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^0.5.4",
|
||||
@@ -44,15 +48,18 @@
|
||||
"@opentelemetry/context-async-hooks": "^1.25.1",
|
||||
"@opentelemetry/core": "^1.14.0",
|
||||
"@opentelemetry/instrumentation": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-aws-sdk": "^0.44.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.52.1",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
||||
"@opentelemetry/instrumentation-undici": "^0.4.0",
|
||||
"@opentelemetry/instrumentation-winston": "^0.36.0",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-metrics": "1.23.0",
|
||||
"@opentelemetry/sdk-node": "^0.50.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.25.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.25.1",
|
||||
"@opentelemetry/winston-transport": "^0.2.0",
|
||||
"@prisma/instrumentation": "^5.13.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -108,14 +115,14 @@
|
||||
"ip-address": "^9.0.5",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.2.6",
|
||||
"langchain": "^0.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.429.0",
|
||||
"lucide-react": "^0.436.0",
|
||||
"next": "^14.2.6",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-query-params": "^5.0.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"posthog-js": "^1.154.6",
|
||||
"posthog-js": "^1.161.3",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prexit": "^2.2.0",
|
||||
"prisma": "^5.13.0",
|
||||
@@ -154,10 +161,10 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/lodash": "^4.17.9",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react": "~18.2.79",
|
||||
"@types/react-dom": "~18.2.25",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
@@ -172,10 +179,10 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.18.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.4.5",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
@@ -183,6 +190,6 @@
|
||||
"initVersion": "7.13.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"crisp-sdk-web": "^1.0.21"
|
||||
"crisp-sdk-web": "^1.0.25"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: &ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -141,7 +141,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/dataset-items/{id}:
|
||||
get:
|
||||
description: Get a dataset item
|
||||
@@ -186,7 +187,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/dataset-run-items:
|
||||
post:
|
||||
description: Create a dataset run item
|
||||
@@ -226,7 +228,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -286,7 +289,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
post:
|
||||
description: Create a dataset
|
||||
operationId: datasets_create
|
||||
@@ -325,7 +329,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -376,7 +381,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/datasets/{datasetName}/runs/{runName}:
|
||||
get:
|
||||
description: Get a dataset run and its items
|
||||
@@ -426,7 +432,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/datasets/{datasetName}/runs:
|
||||
get:
|
||||
description: Get dataset runs
|
||||
@@ -485,7 +492,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/health:
|
||||
get:
|
||||
description: Check health of API and database
|
||||
@@ -580,7 +588,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -649,8 +658,8 @@ paths:
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces on or after a certain
|
||||
datetime (ISO 8601)
|
||||
Optional filter to only include traces and observations on or after
|
||||
a certain datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -659,8 +668,8 @@ paths:
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces before a certain datetime
|
||||
(ISO 8601)
|
||||
Optional filter to only include traces and observations before a
|
||||
certain datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -698,7 +707,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/models:
|
||||
post:
|
||||
description: Create a model
|
||||
@@ -738,7 +748,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -797,7 +808,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/models/{id}:
|
||||
get:
|
||||
description: Get a model
|
||||
@@ -842,7 +854,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
delete:
|
||||
description: >-
|
||||
Delete a model. Cannot delete models managed by Langfuse. You can create
|
||||
@@ -885,7 +898,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/observations/{observationId}:
|
||||
get:
|
||||
description: Get a observation
|
||||
@@ -933,7 +947,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/observations:
|
||||
get:
|
||||
description: Get a list of observations
|
||||
@@ -1046,7 +1061,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/projects:
|
||||
get:
|
||||
description: Get Project associated with API key
|
||||
@@ -1086,7 +1102,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/v2/prompts/{promptName}:
|
||||
get:
|
||||
description: Get a prompt
|
||||
@@ -1148,7 +1165,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/v2/prompts:
|
||||
get:
|
||||
description: Get a list of prompt names with versions and labels
|
||||
@@ -1240,7 +1258,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
post:
|
||||
description: Create a new version for the prompt with the given `name`
|
||||
operationId: prompts_create
|
||||
@@ -1279,7 +1298,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1327,7 +1347,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1388,7 +1409,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/score-configs/{configId}:
|
||||
get:
|
||||
description: Get a score config
|
||||
@@ -1434,7 +1456,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/scores:
|
||||
post:
|
||||
description: Create a score
|
||||
@@ -1474,7 +1497,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -1612,7 +1636,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/scores/{scoreId}:
|
||||
get:
|
||||
description: Get a score
|
||||
@@ -1658,7 +1683,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
delete:
|
||||
description: Delete a score
|
||||
operationId: score_delete
|
||||
@@ -1699,7 +1725,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/sessions:
|
||||
get:
|
||||
description: Get sessions
|
||||
@@ -1775,7 +1802,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/sessions/{sessionId}:
|
||||
get:
|
||||
description: >-
|
||||
@@ -1824,7 +1852,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/traces/{traceId}:
|
||||
get:
|
||||
description: Get a specific trace
|
||||
@@ -1870,7 +1899,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
/api/public/traces:
|
||||
get:
|
||||
description: Get list of traces
|
||||
@@ -1997,7 +2027,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
security:
|
||||
- BasicAuth: []
|
||||
components:
|
||||
schemas:
|
||||
Trace:
|
||||
@@ -2696,6 +2727,7 @@ components:
|
||||
to exact match, use `(?i)^modelname$`
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
nullable: true
|
||||
description: Apply only to generations which are newer than this ISO date.
|
||||
unit:
|
||||
@@ -3508,6 +3540,7 @@ components:
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
countTraces:
|
||||
type: integer
|
||||
countObservations:
|
||||
@@ -3592,6 +3625,7 @@ components:
|
||||
to exact match, use `(?i)^modelname$`
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
nullable: true
|
||||
description: Apply only to generations which are newer than this ISO date.
|
||||
unit:
|
||||
|
||||
@@ -516,12 +516,12 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces on or after a certain datetime (ISO 8601)"
|
||||
"description": "Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces before a certain datetime (ISO 8601)"
|
||||
"description": "Optional filter to only include traces and observations before a certain datetime (ISO 8601)"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
@@ -2,13 +2,15 @@ import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
|
||||
|
||||
// Replay may only be enabled for the client-side
|
||||
integrations: [
|
||||
Sentry.replayIntegration(),
|
||||
// Sentry.debugIntegration(),
|
||||
Sentry.captureConsoleIntegration({
|
||||
levels: ["error"],
|
||||
}),
|
||||
// Sentry.captureConsoleIntegration({
|
||||
// levels: ["error"],
|
||||
// }),
|
||||
],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
@@ -16,24 +18,16 @@ Sentry.init({
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
? Number(process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE)
|
||||
: 0.5,
|
||||
: 0,
|
||||
|
||||
// Capture Replay for 100% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
? Number(process.env.NEXT_PUBLIC_LANGFUSE_TRACING_SAMPLE_RATE)
|
||||
: 0.5,
|
||||
: 0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
debug: false,
|
||||
|
||||
beforeSend(event, _hint) {
|
||||
// Check if it is an exception, and if so, show the report dialog
|
||||
if (event.exception) {
|
||||
Sentry.showReportDialog({ eventId: event.event_id });
|
||||
}
|
||||
return event;
|
||||
},
|
||||
|
||||
// ...
|
||||
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
|
||||
@@ -97,3 +97,44 @@ test("Unauthenticated user should be redirected to target URL after login", asyn
|
||||
|
||||
await expect(page).toHaveURL(promptUrl);
|
||||
});
|
||||
|
||||
test("Unauthenticated user should not be redirected to non-relative URLs after login", async ({
|
||||
page,
|
||||
}) => {
|
||||
const nonRelativeUrl = "https://example.com";
|
||||
await page.goto(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(nonRelativeUrl)}`,
|
||||
);
|
||||
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Expect to be redirected to the home page, not the non-relative URL
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Verify we're logged in
|
||||
await expect(page.getByRole("button", { name: /Demo User/ })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Unauthenticated user should be redirected to relative URL after login", async ({
|
||||
page,
|
||||
}) => {
|
||||
const relativeUrl = "/support";
|
||||
await page.goto(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(relativeUrl)}`,
|
||||
);
|
||||
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Expect to be redirected to the relative URL
|
||||
await expect(page).toHaveURL(relativeUrl);
|
||||
});
|
||||
|
||||
@@ -548,6 +548,167 @@ describe("Authenticate API calls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalidates api keys in redis", () => {
|
||||
const redis = new Redis("redis://:myredissecret@127.0.0.1:6379", {
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// if we do not remove the key, it will remain in the cache and
|
||||
// calling the test twice will not add the key to the cache
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
console.log("before each deleting keys", keys);
|
||||
if (keys.length > 0) {
|
||||
console.log("before each deleting keys. actually deleting", keys);
|
||||
await redis.del(keys);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// if we do not remove the key, it will remain in the cache and
|
||||
// calling the test twice will not add the key to the cache
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
console.log("after each deleting keys", keys);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(keys);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
redis.disconnect();
|
||||
});
|
||||
|
||||
it("should invalidate organization API keys in redis", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
// put keys into cache
|
||||
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
|
||||
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
|
||||
);
|
||||
|
||||
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
|
||||
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
|
||||
);
|
||||
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { publicKey: "pk-lf-1234567890" },
|
||||
});
|
||||
expect(apiKey).not.toBeNull();
|
||||
|
||||
const cachedKey = await redis.get(
|
||||
`api-key:${apiKey?.fastHashedSecretKey}`,
|
||||
);
|
||||
expect(cachedKey).not.toBeNull();
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
|
||||
"seed-org-id",
|
||||
);
|
||||
|
||||
const invalidatedCachedKey = await redis.get(
|
||||
`api-key:${apiKey?.fastHashedSecretKey}`,
|
||||
);
|
||||
expect(invalidatedCachedKey).toBeNull();
|
||||
});
|
||||
|
||||
it("if no keys in redis, invalidating org keys should do nothing", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey: "pk-lf-1234567890" },
|
||||
data: {
|
||||
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
|
||||
},
|
||||
});
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
|
||||
"seed-org-id",
|
||||
);
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
expect(keys.length).toBe(0);
|
||||
});
|
||||
|
||||
it("if no keys in redis, invalidating org keys without fast hash should do nothing", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateOrgApiKeys(
|
||||
"seed-org-id",
|
||||
);
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
expect(keys.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should invalidate project API keys in redis", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
// put keys into cache
|
||||
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
|
||||
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
|
||||
);
|
||||
await new ApiAuthService(prisma, redis).verifyAuthHeaderAndReturnScope(
|
||||
"Basic cGstbGYtMTIzNDU2Nzg5MDpzay1sZi0xMjM0NTY3ODkw",
|
||||
);
|
||||
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { publicKey: "pk-lf-1234567890" },
|
||||
});
|
||||
expect(apiKey).not.toBeNull();
|
||||
|
||||
const cachedKey = await redis.get(
|
||||
`api-key:${apiKey?.fastHashedSecretKey}`,
|
||||
);
|
||||
expect(cachedKey).not.toBeNull();
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
|
||||
const invalidatedCachedKey = await redis.get(
|
||||
`api-key:${apiKey?.fastHashedSecretKey}`,
|
||||
);
|
||||
expect(invalidatedCachedKey).toBeNull();
|
||||
});
|
||||
|
||||
it("if no keys in redis, invalidating project keys should do nothing", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey: "pk-lf-1234567890" },
|
||||
data: {
|
||||
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
|
||||
},
|
||||
});
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
expect(keys.length).toBe(0);
|
||||
});
|
||||
|
||||
it("if no keys in redis, invalidating project keys without fast hash should do nothing", async () => {
|
||||
await createAPIKey();
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey: "pk-lf-1234567890" },
|
||||
data: {
|
||||
fastHashedSecretKey: Math.random().toString(36).substring(2, 15),
|
||||
},
|
||||
});
|
||||
|
||||
await new ApiAuthService(prisma, redis).invalidateProjectApiKeys(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
|
||||
const keys = await redis.keys("api-key*");
|
||||
expect(keys.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const createAPIKey = async () => {
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { orderByToPrismaSql } from "@langfuse/shared";
|
||||
import { orderByToPrismaSql } from "@langfuse/shared/src/server";
|
||||
import { tracesTableCols } from "@langfuse/shared";
|
||||
|
||||
// The test for the orderByToPrisma function
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type z } from "zod";
|
||||
|
||||
@@ -24,10 +21,7 @@ export const pruneDatabase = async () => {
|
||||
await prisma.llmApiKeys.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
username: string,
|
||||
password: string,
|
||||
): string {
|
||||
function createBasicAuthHeader(username: string, password: string): string {
|
||||
const base64Credentials = Buffer.from(`${username}:${password}`).toString(
|
||||
"base64",
|
||||
);
|
||||
@@ -97,38 +91,3 @@ export async function makeZodVerifiedAPICall<T extends z.ZodTypeAny>(
|
||||
}
|
||||
return { body: resBody, status };
|
||||
}
|
||||
|
||||
export const setupUserAndProject = async () => {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
email: "demo@langfuse.com",
|
||||
password: await hash("password", 12),
|
||||
},
|
||||
});
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
name: "llm-app",
|
||||
apiKeys: {
|
||||
create: [
|
||||
{
|
||||
note: "seeded key",
|
||||
hashedSecretKey: await hashSecretKey("sk-lf-1234567890"),
|
||||
displaySecretKey: getDisplaySecretKey("sk-lf-1234567890"),
|
||||
publicKey: "pk-lf-1234567890",
|
||||
},
|
||||
],
|
||||
},
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return { user, project };
|
||||
};
|
||||
|
||||
@@ -30,7 +30,17 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
props,
|
||||
) => {
|
||||
const [isExporting, setIsExporting] = React.useState(false);
|
||||
const createExport = api.batchExport.create.useMutation();
|
||||
const createExport = api.batchExport.create.useMutation({
|
||||
onSettled: () => {
|
||||
setIsExporting(false);
|
||||
},
|
||||
onSuccess: () => {
|
||||
showSuccessToast({
|
||||
title: "Export queued",
|
||||
description: "You will receive an email when the export is ready.",
|
||||
});
|
||||
},
|
||||
});
|
||||
const entitled = useHasOrgEntitlement("batch-export");
|
||||
const hasAccess = useHasProjectAccess({
|
||||
projectId: props.projectId,
|
||||
@@ -49,11 +59,6 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
orderBy: props.orderByState,
|
||||
},
|
||||
});
|
||||
setIsExporting(false);
|
||||
showSuccessToast({
|
||||
title: "Export queued",
|
||||
description: "You will receive an email when the export is ready.",
|
||||
});
|
||||
};
|
||||
|
||||
if (!entitled || !hasAccess) return null;
|
||||
|
||||
@@ -9,27 +9,40 @@ import type { MessagesContext } from "./types";
|
||||
|
||||
type ChatMessageProps = Pick<
|
||||
MessagesContext,
|
||||
"deleteMessage" | "updateMessage"
|
||||
"deleteMessage" | "updateMessage" | "availableRoles"
|
||||
> & { message: ChatMessageWithId };
|
||||
|
||||
export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
message,
|
||||
updateMessage,
|
||||
deleteMessage,
|
||||
availableRoles,
|
||||
}) => {
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const [textAreaRows, setTextAreaRows] = useState(1);
|
||||
const [roleIndex, setRoleIndex] = useState(1);
|
||||
|
||||
const toggleRole = () => {
|
||||
if (message.role === ChatMessageRole.System) return;
|
||||
|
||||
updateMessage(
|
||||
message.id,
|
||||
"role",
|
||||
message.role === ChatMessageRole.User
|
||||
? ChatMessageRole.Assistant
|
||||
: ChatMessageRole.User,
|
||||
);
|
||||
// if user has set custom roles, available roles will be non-empty and we toggle through custom and default roles (assistant, user)
|
||||
if (!!availableRoles && Boolean(availableRoles.length)) {
|
||||
let randomRole = availableRoles[roleIndex % availableRoles.length];
|
||||
if (randomRole === message.role) {
|
||||
randomRole = availableRoles[(roleIndex + 1) % availableRoles.length];
|
||||
}
|
||||
updateMessage(message.id, "role", randomRole);
|
||||
setRoleIndex(roleIndex + 1);
|
||||
} else {
|
||||
// if user has not set custom roles, we toggle through default roles (assistant, user)
|
||||
updateMessage(
|
||||
message.id,
|
||||
"role",
|
||||
message.role === ChatMessageRole.User
|
||||
? ChatMessageRole.Assistant
|
||||
: ChatMessageRole.User,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContentChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
@@ -64,7 +77,7 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
<Textarea
|
||||
ref={textAreaRef}
|
||||
id={message.id}
|
||||
className="height-[auto] min-h-6 w-full font-mono text-xs focus:outline-none"
|
||||
className="height-[auto] min-h-6 w-full font-mono text-xs focus:outline-none"
|
||||
placeholder={placeholder}
|
||||
value={message.content}
|
||||
onChange={handleContentChange}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ChatMessageRole, ChatMessageWithId } from "@langfuse/shared";
|
||||
|
||||
|
||||
export type MessagesContext = {
|
||||
messages: ChatMessageWithId[];
|
||||
addMessage: (role: ChatMessageRole, content?: string) => ChatMessageWithId;
|
||||
@@ -8,6 +7,7 @@ export type MessagesContext = {
|
||||
updateMessage: <Key extends keyof ChatMessageWithId>(
|
||||
id: string,
|
||||
key: Key,
|
||||
value: ChatMessageWithId[Key]
|
||||
value: ChatMessageWithId[Key],
|
||||
) => void;
|
||||
availableRoles?: string[]; // Only defined if user has extended default roles (ChatMessageRole) with custom roles via SDK/API
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@ import { cn } from "@/src/utils/tailwind";
|
||||
import Link from "next/link";
|
||||
import { EnvLabel } from "./EnvLabel";
|
||||
import { VersionLabel } from "./VersionLabel";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { useUiCustomization } from "@/src/ee/features/ui-customization/useUiCustomization";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
size = 32,
|
||||
@@ -12,7 +15,7 @@ export const LangfuseIcon = ({
|
||||
}) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src="/icon.svg"
|
||||
src={`${env.NEXT_PUBLIC_BASE_PATH ?? ""}/icon.svg`}
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Langfuse Icon"
|
||||
@@ -20,6 +23,52 @@ export const LangfuseIcon = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const LangfuseLogotypeOrCustomized = ({ size }: { size: "sm" | "xl" }) => {
|
||||
const uiCustomization = useUiCustomization();
|
||||
|
||||
if (uiCustomization?.logoLightModeHref && uiCustomization?.logoDarkModeHref) {
|
||||
// logo is a url, maximum aspect ratio of 1:3 needs to be supported according to docs
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={uiCustomization.logoLightModeHref}
|
||||
alt="Langfuse Logo"
|
||||
className={cn(
|
||||
"dark:hidden",
|
||||
size === "sm" ? "max-h-4 max-w-14" : "max-h-5 max-w-16",
|
||||
)}
|
||||
/>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={uiCustomization.logoDarkModeHref}
|
||||
alt="Langfuse Logo"
|
||||
className={cn(
|
||||
"hidden dark:block",
|
||||
size === "sm" ? "max-h-4 max-w-14" : "max-h-5 max-w-16",
|
||||
)}
|
||||
/>
|
||||
<PlusIcon size={size === "sm" ? 8 : 12} />
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
<span
|
||||
className={cn(
|
||||
"ml-2 font-mono font-semibold",
|
||||
size === "sm" ? "text-sm" : "text-xl",
|
||||
)}
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LangfuseLogo = ({
|
||||
className,
|
||||
size = "sm",
|
||||
@@ -30,26 +79,23 @@ export const LangfuseLogo = ({
|
||||
className?: string;
|
||||
version?: boolean;
|
||||
showEnvLabel?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex flex-wrap gap-4 lg:flex-col lg:items-start", className)}
|
||||
>
|
||||
{/* Environment Labeling for Langfuse Maintainers */}
|
||||
{showEnvLabel && <EnvLabel />}
|
||||
{/* Langfuse Logo */}
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center">
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
<span
|
||||
className={cn(
|
||||
"ml-2 font-mono font-semibold",
|
||||
size === "sm" ? "text-sm" : "text-xl",
|
||||
)}
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
</Link>
|
||||
{version && <VersionLabel className="ml-2" />}
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-4 lg:flex-col lg:items-start",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Environment Labeling for Langfuse Maintainers */}
|
||||
{showEnvLabel && <EnvLabel />}
|
||||
{/* Langfuse Logo */}
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center">
|
||||
<LangfuseLogotypeOrCustomized size={size} />
|
||||
</Link>
|
||||
{version && <VersionLabel className="ml-2" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
|
||||
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
|
||||
import { FormDescription } from "@/src/components/ui/form";
|
||||
|
||||
export type ModelParamsContext = {
|
||||
modelParams: UIModelParams;
|
||||
@@ -29,6 +30,7 @@ export type ModelParamsContext = {
|
||||
) => void;
|
||||
setModelParamEnabled?: (key: keyof UIModelParams, enabled: boolean) => void;
|
||||
formDisabled?: boolean;
|
||||
modelParamsDescription?: string;
|
||||
};
|
||||
|
||||
export const ModelParameters: React.FC<
|
||||
@@ -41,6 +43,7 @@ export const ModelParameters: React.FC<
|
||||
setModelParamEnabled,
|
||||
evalModelsOnly,
|
||||
formDisabled = false,
|
||||
modelParamsDescription,
|
||||
}) => {
|
||||
const projectId = useProjectIdFromURL();
|
||||
|
||||
@@ -74,7 +77,22 @@ export const ModelParameters: React.FC<
|
||||
value={modelParams.model.value}
|
||||
options={availableModels}
|
||||
updateModelParam={updateModelParamValue}
|
||||
modelParamsDescription={modelParamsDescription}
|
||||
/>
|
||||
{modelParams.model.value?.startsWith("o1-") ? (
|
||||
<p className="text-sm text-dark-yellow">
|
||||
For {modelParams.model.value}, the system message and the
|
||||
temperature, max_tokens and top_p setting are not supported while
|
||||
it is in beta.{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/guides/reasoning/beta-limitations"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
More info ↗
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
<ModelParamsSlider
|
||||
title="Temperature"
|
||||
modelParamsKey="temperature"
|
||||
@@ -128,6 +146,7 @@ type ModelParamsSelectProps = {
|
||||
options: string[];
|
||||
updateModelParam: ModelParamsContext["updateModelParamValue"];
|
||||
disabled?: boolean;
|
||||
modelParamsDescription?: string;
|
||||
};
|
||||
const ModelParamsSelect = ({
|
||||
title,
|
||||
@@ -136,6 +155,7 @@ const ModelParamsSelect = ({
|
||||
options,
|
||||
updateModelParam,
|
||||
disabled,
|
||||
modelParamsDescription,
|
||||
}: ModelParamsSelectProps) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
@@ -168,6 +188,9 @@ const ModelParamsSelect = ({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{modelParamsDescription ? (
|
||||
<FormDescription>{modelParamsDescription}</FormDescription>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BadgeCheck,
|
||||
Github,
|
||||
HardDriveDownload,
|
||||
Info,
|
||||
Map,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
@@ -20,6 +21,7 @@ import { api } from "@/src/utils/api";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
|
||||
@@ -30,6 +32,7 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
|
||||
});
|
||||
const isEeVersion = useIsEeEnabled();
|
||||
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
|
||||
const hasUpdate =
|
||||
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
@@ -46,8 +49,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="xs" className={className}>
|
||||
<Button variant="ghost" size="xs" className={cn("text-xs",className)}>
|
||||
{VERSION}
|
||||
{!isLangfuseCloud && (isEeVersion ? " EE" : " OSS")}
|
||||
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -96,6 +100,17 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
Roadmap
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!isLangfuseCloud && !isEeVersion && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="https://langfuse.com/docs/deployment/feature-overview"
|
||||
target="_blank"
|
||||
>
|
||||
<Info size={16} className="mr-2" />
|
||||
Compare Versions
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user