Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4bd8208c3 | ||
|
|
c8b282dc82 | ||
|
|
e2922aa787 | ||
|
|
e8690eeb0b | ||
|
|
a695d58e8e | ||
|
|
b097a98444 | ||
|
|
87c6d45a01 | ||
|
|
d5a79b1830 | ||
|
|
40b967c258 | ||
|
|
b6b7a7c670 | ||
|
|
346e787784 | ||
|
|
8e67f851b0 | ||
|
|
8b416da2ef | ||
|
|
88237dcb30 | ||
|
|
8a0b3962ac | ||
|
|
2f6414bbf4 | ||
|
|
c7aa992586 | ||
|
|
c2b5923e88 | ||
|
|
fc411d0c5c | ||
|
|
3fac8d2608 | ||
|
|
cd413fa887 | ||
|
|
be7cc835be | ||
|
|
b68896d14a | ||
|
|
aae1909466 | ||
|
|
3d7960f43e | ||
|
|
a121e24b79 | ||
|
|
7279a924ed | ||
|
|
a10951b7e5 | ||
|
|
e456b08f92 | ||
|
|
85130cecb3 | ||
|
|
fbb15e26ec | ||
|
|
dd203a1f18 | ||
|
|
06c25f9ec3 | ||
|
|
7072ca2786 | ||
|
|
cb1e5f4c19 | ||
|
|
d4a28aa164 | ||
|
|
fda5fcdd0c | ||
|
|
24131e0791 | ||
|
|
ffe132403e | ||
|
|
cce3bf35e1 | ||
|
|
eccfec36e8 | ||
|
|
a71faa4e60 | ||
|
|
b7362258c6 | ||
|
|
5eb1d98985 | ||
|
|
1c574b2a5d | ||
|
|
6db9b87b6a | ||
|
|
3e3ccb65ff | ||
|
|
0d53b443e5 | ||
|
|
1b795a3dcf | ||
|
|
2e94bc9e09 | ||
|
|
8b94e8a3bd | ||
|
|
91730a2eaf | ||
|
|
4f1a2337be | ||
|
|
4cf5aa03ec | ||
|
|
922eafd4f6 | ||
|
|
9067f80206 | ||
|
|
a7ca1fb269 | ||
|
|
b51f321d22 | ||
|
|
dd048a64a0 | ||
|
|
c6c19de0e8 | ||
|
|
5491f176e1 | ||
|
|
7a28473ebb | ||
|
|
005ce7318c | ||
|
|
448c56e114 | ||
|
|
1d6e498e2b | ||
|
|
33ab717ae0 | ||
|
|
ec563b775b | ||
|
|
de427b5572 | ||
|
|
95921fa5da | ||
|
|
9e4d352366 | ||
|
|
02449cbe0a | ||
|
|
616c68a87b | ||
|
|
319b22ae78 | ||
|
|
b919562eed | ||
|
|
91c01e7363 | ||
|
|
48ec1bccae |
@@ -0,0 +1,47 @@
|
||||
FROM node:20
|
||||
|
||||
# ---------- System packages --------------------------------------------------
|
||||
# The buildpack-deps base already ships git, build-essential, python, etc.
|
||||
# Add a few extra tools handy during Langfuse development.
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
openssl \
|
||||
wget \
|
||||
curl \
|
||||
ca-certificates \
|
||||
postgresql-client \
|
||||
redis-tools \
|
||||
less nano \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---------- pnpm -------------------------------------------------------------
|
||||
# Langfuse monorepo relies on pnpm 9.5.0 (see CONTRIBUTING.md)
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && \
|
||||
corepack prepare pnpm@9.5.0 --activate
|
||||
|
||||
# ---------- golang-migrate ----------------------------------------------------
|
||||
# CLI used for database migrations during development.
|
||||
ENV MIGRATE_VERSION=4.18.2
|
||||
RUN wget -qO- "https://github.com/golang-migrate/migrate/releases/download/v${MIGRATE_VERSION}/migrate.linux-amd64.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin && \
|
||||
chmod +x /usr/local/bin/migrate
|
||||
|
||||
# ---------- Non-root user -----------------------------------------------------
|
||||
# Use root for convenience in development containers
|
||||
WORKDIR /workspace
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -ms /bin/bash ubuntu
|
||||
|
||||
# Pre-create pnpm store and set correct ownership to avoid first-run cost & permission issues
|
||||
RUN pnpm store path > /dev/null && \
|
||||
chown -R ubuntu:ubuntu /pnpm
|
||||
|
||||
USER ubuntu
|
||||
WORKDIR /home/ubuntu
|
||||
ENV HOME=/home/ubuntu
|
||||
|
||||
# Container starts with a bash shell ready for hacking.
|
||||
CMD ["bash"]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"install": "cp .env.dev.example .env && pnpm i",
|
||||
"build": {
|
||||
"context": ".",
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"start": "pnpm dx-f"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# General rules
|
||||
|
||||
- Linting in this repo only works if the development server is running
|
||||
+2
-2
@@ -213,8 +213,8 @@ LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 欧盟区域
|
||||
|
||||
创建示例代码(文件名:**main.py**):
|
||||
|
||||
````python:main.py
|
||||
from langfuse.decorators import observe
|
||||
```python:main.py
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI 集成
|
||||
|
||||
@observe()
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 EUリージョン
|
||||
```
|
||||
|
||||
```python:/@observe()/ /from langfuse.openai import openai/ filename="main.py"
|
||||
from langfuse.decorators import observe
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI統合
|
||||
|
||||
@observe()
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 EU region
|
||||
```
|
||||
|
||||
```python:main.py
|
||||
from langfuse.decorators import observe
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI integration
|
||||
|
||||
@observe()
|
||||
|
||||
@@ -93,7 +93,7 @@ Langfuse is an **open source LLM engineering** platform. It helps teams collabor
|
||||
|
||||
### Langfuse Cloud
|
||||
|
||||
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
|
||||
Managed deployment by the Langfuse team, generous free-tier, no credit card required.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://cloud.langfuse.com" target="_blank">
|
||||
@@ -115,12 +115,11 @@ Run Langfuse on your own infrastructure:
|
||||
# Run the langfuse docker compose
|
||||
docker compose up
|
||||
```
|
||||
|
||||
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Run Langfuse on a Kubernetes cluster using Helm. This is the preferred production deployment.
|
||||
- [VM](https://langfuse.com/self-hosting/docker-compose): Run Langfuse on a single Virtual Machine using Docker Compose.
|
||||
- Planned: Cloud-specific deployment guides, please upvote and comment on the following threads: [AWS](https://github.com/orgs/langfuse/discussions/4645), [Google Cloud](https://github.com/orgs/langfuse/discussions/4646), [Azure](https://github.com/orgs/langfuse/discussions/4647).
|
||||
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Run Langfuse on a Kubernetes cluster using Helm. This is the preferred production deployment.
|
||||
- Terraform Templates: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
|
||||
|
||||
See [self-hosting documentation](https://langfuse.com/self-hosting) to learn more about the architecture and configuration options.
|
||||
See [self-hosting documentation](https://langfuse.com/self-hosting) to learn more about architecture and configuration options.
|
||||
|
||||
## 🔌 Integrations
|
||||
|
||||
@@ -191,7 +190,7 @@ LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 EU region
|
||||
```
|
||||
|
||||
```python /@observe()/ /from langfuse.openai import openai/ filename="main.py"
|
||||
from langfuse.decorators import observe
|
||||
from langfuse import observe
|
||||
from langfuse.openai import openai # OpenAI integration
|
||||
|
||||
@observe()
|
||||
|
||||
@@ -29,6 +29,7 @@ services:
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_USE_AZURE_BLOB: ${LANGFUSE_USE_AZURE_BLOB:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
|
||||
+2
-2
@@ -28,9 +28,9 @@
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.8.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"next": "^14.2.26",
|
||||
"next": "^14.2.30",
|
||||
"next-auth": "^4.24.11",
|
||||
"zod": "^3.24.4"
|
||||
"zod": "^3.25.62"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
|
||||
@@ -28,7 +28,7 @@ service:
|
||||
"metrics": [ // Required. At least one metric must be provided
|
||||
{
|
||||
"measure": string, // What to measure, e.g. "count", "latency", "value"
|
||||
"aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95"
|
||||
"aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram"
|
||||
}
|
||||
],
|
||||
"filters": [ // Optional. Default: []
|
||||
@@ -50,7 +50,11 @@ service:
|
||||
"field": string, // Field to order by
|
||||
"direction": string // "asc" or "desc"
|
||||
}
|
||||
]
|
||||
],
|
||||
"config": { // Optional. Query-specific configuration
|
||||
"bins": number, // Optional. Number of bins for histogram (1-100), default: 10
|
||||
"row_limit": number // Optional. Row limit for results (1-1000)
|
||||
}
|
||||
}
|
||||
```
|
||||
response: MetricsResponse
|
||||
@@ -62,3 +66,4 @@ types:
|
||||
docs: |
|
||||
The metrics data. Each item in the list contains the metric values and dimensions requested in the query.
|
||||
Format varies based on the query parameters.
|
||||
Histograms will return an array with [lower, upper, height] tuples.
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.65.2",
|
||||
"version": "3.69.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -18,7 +18,7 @@
|
||||
"db:seed:examples": "turbo run db:seed:examples",
|
||||
"nuke": "bash ./scripts/nuke.sh",
|
||||
"dx": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset -f && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"build": "turbo run build",
|
||||
"start": "turbo run start",
|
||||
@@ -31,12 +31,12 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@release-it/bumper": "^7.0.1",
|
||||
"@release-it/bumper": "^7.0.5",
|
||||
"braces": "3.0.3",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.3.3",
|
||||
"release-it": "^18.1.2",
|
||||
"release-it": "^19.0.3",
|
||||
"turbo": "^1.13.4"
|
||||
},
|
||||
"release-it": {
|
||||
|
||||
@@ -36,8 +36,12 @@ if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
|
||||
# If SKIP_CONFIRM is set, automatically answer the confirmation prompt. Otherwise run interactively.
|
||||
if [ "$SKIP_CONFIRM" = "1" ] || [ "$SKIP_CONFIRM" = "true" ]; then
|
||||
printf 'y\n' | migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
|
||||
else
|
||||
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
|
||||
fi
|
||||
else
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
@@ -45,6 +49,10 @@ else
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
|
||||
# If SKIP_CONFIRM is set, automatically answer the confirmation prompt. Otherwise run interactively.
|
||||
if [ "$SKIP_CONFIRM" = "1" ] || [ "$SKIP_CONFIRM" = "true" ]; then
|
||||
printf 'y\n' | migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
|
||||
else
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
|
||||
fi
|
||||
fi
|
||||
@@ -60,18 +60,18 @@
|
||||
"@aws-sdk/lib-storage": "^3.675.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.679.0",
|
||||
"@azure/storage-blob": "^12.26.0",
|
||||
"@clickhouse/client": "^1.11.1",
|
||||
"@clickhouse/client": "^1.11.2",
|
||||
"@google-cloud/storage": "^7.15.2",
|
||||
"@langchain/anthropic": "^0.3.12",
|
||||
"@langchain/aws": "^0.1.3",
|
||||
"@langchain/core": "^0.3.37",
|
||||
"@langchain/google-genai": "^0.1.9",
|
||||
"@langchain/google-vertexai": "^0.1.8",
|
||||
"@langchain/openai": "^0.3.17",
|
||||
"@langchain/anthropic": "^0.3.21",
|
||||
"@langchain/aws": "^0.1.10",
|
||||
"@langchain/core": "^0.3.57",
|
||||
"@langchain/google-genai": "^0.2.10",
|
||||
"@langchain/google-vertexai": "^0.2.10",
|
||||
"@langchain/openai": "^0.5.12",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^6.3.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@react-email/render": "^0.0.15",
|
||||
"@react-email/components": "^0.0.42",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"axios": "^1.8.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -82,8 +82,8 @@
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"ioredis": "^5.4.1",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.15",
|
||||
"langfuse-langchain": "3.30.3",
|
||||
"langchain": "^0.3.27",
|
||||
"langfuse-langchain": "3.37.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lossless-json": "^4.0.2",
|
||||
"next-auth": "^4.24.11",
|
||||
@@ -91,7 +91,7 @@
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.15.0",
|
||||
"zod": "^3.24.4",
|
||||
"zod": "^3.25.62",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -101,6 +101,7 @@
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -110,7 +110,8 @@ export const DashboardWidgetChartType = {
|
||||
HORIZONTAL_BAR: "HORIZONTAL_BAR",
|
||||
VERTICAL_BAR: "VERTICAL_BAR",
|
||||
PIE: "PIE",
|
||||
NUMBER: "NUMBER"
|
||||
NUMBER: "NUMBER",
|
||||
HISTOGRAM: "HISTOGRAM"
|
||||
} as const;
|
||||
export type DashboardWidgetChartType = (typeof DashboardWidgetChartType)[keyof typeof DashboardWidgetChartType];
|
||||
export type Account = {
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "DashboardWidgetChartType" ADD VALUE 'HISTOGRAM';
|
||||
@@ -1156,6 +1156,7 @@ enum DashboardWidgetChartType {
|
||||
VERTICAL_BAR
|
||||
PIE
|
||||
NUMBER
|
||||
HISTOGRAM
|
||||
}
|
||||
|
||||
model DashboardWidget {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { jsonSchema } from "../utils/zod";
|
||||
import { MetadataDomain } from "./traces";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScoreDataType } from "@prisma/client";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { MetadataDomain } from "./traces";
|
||||
|
||||
export const ScoreSource = {
|
||||
@@ -31,7 +31,7 @@ export const ScoreSchema = z.object({
|
||||
queueId: z.string().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
dataType: z.nativeEnum(ScoreDataType),
|
||||
dataType: z.enum(ScoreDataType),
|
||||
});
|
||||
|
||||
export type ScoreDomain = z.infer<typeof ScoreSchema>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { singleFilter } from "../interfaces/filters";
|
||||
import { orderBy } from "../interfaces/orderBy";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
|
||||
export enum TableViewPresetTableName {
|
||||
Traces = "traces",
|
||||
@@ -17,7 +17,7 @@ const TableViewPresetDomainSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
createdBy: z.string().nullable(),
|
||||
name: z.string(),
|
||||
tableName: z.nativeEnum(TableViewPresetTableName),
|
||||
tableName: z.enum(TableViewPresetTableName),
|
||||
filters: z.array(singleFilter),
|
||||
columnOrder: z.array(z.string()),
|
||||
columnVisibility: z.record(z.string(), z.boolean()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { jsonSchema, jsonSchemaNullable } from "../utils/zod";
|
||||
|
||||
export const MetadataDomain = z.record(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { removeEmptyEnvVariables } from "./utils/environment";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
@@ -9,10 +9,7 @@ const EnvSchema = z.object({
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
REDIS_HOST: z.string().nullish(),
|
||||
REDIS_PORT: z.coerce
|
||||
.number({
|
||||
description:
|
||||
".env files convert numbers to strings, therefore we have to enforce them to be numbers",
|
||||
})
|
||||
.number() // .env files convert numbers to strings, therefore we have to enforce them to be numbers
|
||||
.positive()
|
||||
.max(65536, `options.port should be >= 0 and < 65536`)
|
||||
.default(6379)
|
||||
@@ -60,9 +57,7 @@ const EnvSchema = z.object({
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_S3_CONCURRENT_WRITES: z.coerce.number().positive().default(50),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
|
||||
required_error: "Langfuse requires a bucket name for S3 Event Uploads.",
|
||||
}),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string(), // Langfuse requires a bucket name for S3 Event Uploads.
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: z.string().default(""),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: z.string().optional(),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: z.string().optional(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { type ScoreDataType } from "../../db";
|
||||
|
||||
const NUMERIC: ScoreDataType = "NUMERIC";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { BatchExportTableName } from "../batchExport/types";
|
||||
import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum BatchActionType {
|
||||
@@ -20,6 +21,8 @@ export type ActionId = z.infer<typeof ActionIdSchema>;
|
||||
export const BatchActionQuerySchema = z.object({
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
orderBy,
|
||||
searchQuery: z.string().optional(),
|
||||
searchType: z.array(TracingSearchType).optional(),
|
||||
});
|
||||
|
||||
export type BatchActionQuery = z.infer<typeof BatchActionQuerySchema>;
|
||||
@@ -29,11 +32,11 @@ export const CreateBatchActionSchema = z.object({
|
||||
actionId: ActionIdSchema,
|
||||
targetId: z.string().optional(),
|
||||
query: BatchActionQuerySchema,
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
});
|
||||
|
||||
export const GetIsBatchActionInProgressSchema = z.object({
|
||||
projectId: z.string(),
|
||||
actionId: ActionIdSchema,
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
|
||||
import { BatchExport } from "@prisma/client";
|
||||
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
|
||||
export enum BatchExportStatus {
|
||||
QUEUED = "QUEUED",
|
||||
@@ -18,13 +19,9 @@ export enum BatchExportFileFormat {
|
||||
JSONL = "JSONL",
|
||||
}
|
||||
|
||||
export enum BatchExportTableName {
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
DatasetRunItems = "dataset_run_items",
|
||||
}
|
||||
// Use shared BatchTableNames enum for consistency across batch operations
|
||||
// Keep BatchExportTableName as alias for backward compatibility
|
||||
export { BatchTableNames as BatchExportTableName };
|
||||
|
||||
export const exportOptions: Record<
|
||||
BatchExportFileFormat,
|
||||
@@ -44,7 +41,7 @@ export const exportOptions: Record<
|
||||
} as const;
|
||||
|
||||
export const BatchExportQuerySchema = z.object({
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
orderBy,
|
||||
limit: z.number().optional(),
|
||||
@@ -57,7 +54,7 @@ export const CreateBatchExportSchema = z.object({
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
query: BatchExportQuerySchema,
|
||||
format: z.nativeEnum(BatchExportFileFormat),
|
||||
format: z.enum(BatchExportFileFormat),
|
||||
});
|
||||
|
||||
export const BatchExportSchema = z.object({
|
||||
@@ -69,9 +66,9 @@ export const BatchExportSchema = z.object({
|
||||
finishedAt: z.date().nullable(),
|
||||
expiresAt: z.date().nullable(),
|
||||
name: z.string(),
|
||||
status: z.nativeEnum(BatchExportStatus),
|
||||
status: z.enum(BatchExportStatus),
|
||||
query: BatchExportQuerySchema,
|
||||
format: z.nativeEnum(BatchExportFileFormat),
|
||||
format: z.enum(BatchExportFileFormat),
|
||||
url: z.string().nullable(),
|
||||
log: z.string().nullable(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const MAX_COMMENT_LENGTH = 3000;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export const planLabels = {
|
||||
"cloud:pro": "Pro",
|
||||
"cloud:team": "Team",
|
||||
"cloud:enterprise": "Enterprise",
|
||||
"self-hosted:pro": "Pro (self-hosted)",
|
||||
"self-hosted:enterprise": "Enterprise (self-hosted)",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
|
||||
export const langfuseObjects = [
|
||||
"trace",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { variableMapping } from "./types";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const PromptDependencyRegex = /@@@langfusePrompt:(.*?)@@@/g;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { jsonSchema, publicApiPaginationZod } from "../../../../utils/zod";
|
||||
import { stringDateTime } from "../../../../utils/typeChecks";
|
||||
import { applyScoreValidation } from "../../../../utils/scores";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { paginationMetaResponseZod } from "../../../../../utils/zod";
|
||||
import {
|
||||
DeleteScoreQuery,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
CategoricalData,
|
||||
NumericData,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { GetScoreResponseDataV1 } from "./endpoints";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { paginationMetaResponseZod } from "../../../../../utils/zod";
|
||||
import { GetScoreQuery, GetScoresQuery } from "../shared";
|
||||
import { APIScoreSchemaV2 } from "./schemas";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
CategoricalData,
|
||||
NumericData,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { GetScoreResponseDataV2 } from "./endpoints";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { APIScoreSchemaV2, APIScoreV2 } from "../api/v2/schemas";
|
||||
import { APIScoreSchemaV1, APIScoreV1 } from "../api/v1/schemas";
|
||||
import { ScoreDomain } from "../../../../domain";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { applyScoreValidation } from "../../../../utils/scores";
|
||||
import { PostScoreBodyFoundationSchema } from "../shared";
|
||||
import { isPresent } from "../../../../utils/typeChecks";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { jsonSchema } from "../../../utils/zod";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { NonEmptyString } from "../../../utils/zod";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { ScoreConfig as ScoreConfigDbType } from "@prisma/client";
|
||||
|
||||
@@ -68,10 +68,10 @@ const CategoricalScoreConfig = z.object({
|
||||
const parseResult = Categories.safeParse(categories);
|
||||
if (!parseResult.success) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message:
|
||||
"Category must be an array of objects with label value pairs, where labels and values are unique.",
|
||||
});
|
||||
} as z.core.$ZodIssueCustom);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export const GetScoreConfigResponse = ValidatedScoreConfigSchema;
|
||||
|
||||
// POST /score-configs
|
||||
export const PostScoreConfigBody = z
|
||||
.union([
|
||||
.discriminatedUnion("dataType", [
|
||||
ScoreConfigPostBase.merge(CategoricalScoreConfig),
|
||||
ScoreConfigPostBase.merge(NumericScoreConfig),
|
||||
ScoreConfigPostBase.merge(
|
||||
|
||||
@@ -26,6 +26,7 @@ export * from "./features/evals/utilities";
|
||||
// table actions
|
||||
export * from "./features/batchExport/types";
|
||||
export * from "./features/batchAction/types";
|
||||
export { BatchTableNames } from "./interfaces/tableNames";
|
||||
|
||||
// annotation
|
||||
export * from "./features/annotation/types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { CloudConfigRateLimit } from "./rate-limits";
|
||||
import { cloudConfigPlans } from "../features/entitlements/plans";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const BedrockConfigSchema = z.object({ region: z.string() });
|
||||
export type BedrockConfig = z.infer<typeof BedrockConfigSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const filterOperators = {
|
||||
datetime: [">", "<", ">=", "<="],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const orderBy = z
|
||||
.object({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { ApiAccessScope } from "../server";
|
||||
|
||||
export const RateLimitResource = z.enum([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const TracingSearchType = z.enum(["id", "content"]);
|
||||
// id: for searching smaller columns like IDs, types, and other metadata
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Shared table names used across batch operations (exports, actions, etc.)
|
||||
* This enum provides a centralized definition of database table names
|
||||
* to avoid coupling between different batch operation types.
|
||||
*/
|
||||
export enum BatchTableNames {
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
DatasetRunItems = "dataset_run_items",
|
||||
AuditLogs = "audit_logs",
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { Plan, plans } from "../../features/entitlements/plans";
|
||||
import { CloudConfigRateLimit } from "../../interfaces/rate-limits";
|
||||
import { ApiKeyScope } from "../../";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
|
||||
import { ColumnDefinition, type TableNames } from "../tableDefinitions";
|
||||
import { FilterState } from "../types";
|
||||
import { filterOperators, timeFilter } from "../interfaces/filters";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { logger } from "./index";
|
||||
|
||||
const operatorReplacements = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { type Model } from "../../db";
|
||||
import { env } from "../../env";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import lodash from "lodash";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { NonEmptyString, jsonSchema } from "../../utils/zod";
|
||||
import { ModelUsageUnit } from "../../constants";
|
||||
@@ -20,7 +20,7 @@ export const Usage = z.object({
|
||||
input: z.number().int().nullish(),
|
||||
output: z.number().int().nullish(),
|
||||
total: z.number().int().nullish(),
|
||||
unit: z.nativeEnum(ModelUsageUnit).nullish(),
|
||||
unit: z.enum(ModelUsageUnit).nullish(),
|
||||
inputCost: z.number().nullish(),
|
||||
outputCost: z.number().nullish(),
|
||||
totalCost: z.number().nullish(),
|
||||
@@ -30,7 +30,7 @@ const MixedUsage = z.object({
|
||||
input: z.number().int().nullish(),
|
||||
output: z.number().int().nullish(),
|
||||
total: z.number().int().nullish(),
|
||||
unit: z.nativeEnum(ModelUsageUnit).nullish(),
|
||||
unit: z.enum(ModelUsageUnit).nullish(),
|
||||
promptTokens: z.number().int().nullish(),
|
||||
completionTokens: z.number().int().nullish(),
|
||||
totalTokens: z.number().int().nullish(),
|
||||
@@ -128,8 +128,7 @@ const OpenAICompletionUsageSchema = z
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.pipe(RawUsageDetails);
|
||||
});
|
||||
|
||||
// The new OpenAI Response API uses a new Usage schema that departs from the Completion API Usage schema
|
||||
const OpenAIResponseUsageSchema = z
|
||||
@@ -184,8 +183,7 @@ const OpenAIResponseUsageSchema = z
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.pipe(RawUsageDetails);
|
||||
});
|
||||
|
||||
export const UsageDetails = z
|
||||
.union([
|
||||
@@ -266,7 +264,7 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
z.record(z.string(), z.string()),
|
||||
])
|
||||
.nullish(),
|
||||
)
|
||||
@@ -296,7 +294,7 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
z.record(z.string(), z.string()),
|
||||
])
|
||||
.nullish(),
|
||||
)
|
||||
|
||||
@@ -162,7 +162,7 @@ function validateConfigAgainstBody(
|
||||
});
|
||||
|
||||
if (!rangeValidation.success) {
|
||||
const errorDetails = rangeValidation.error.errors
|
||||
const errorDetails = rangeValidation.error.issues
|
||||
.map((error) => `${error.path.join(".")} - ${error.message}`)
|
||||
.join(", ");
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// We continue to use zod v3 for langchainjs.
|
||||
// Corresponding issue report: https://github.com/langchain-ai/langchainjs/issues/8357.
|
||||
import { type ZodSchema } from "zod";
|
||||
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
@@ -16,7 +18,7 @@ import {
|
||||
StringOutputParser,
|
||||
} from "@langchain/core/output_parsers";
|
||||
import { IterableReadableStream } from "@langchain/core/utils/stream";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { ChatOpenAI, AzureChatOpenAI } from "@langchain/openai";
|
||||
import GCPServiceAccountKeySchema, {
|
||||
BedrockConfigSchema,
|
||||
BedrockCredentialSchema,
|
||||
@@ -219,7 +221,7 @@ export async function fetchLLMCompletion(
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Azure) {
|
||||
chatModel = new ChatOpenAI({
|
||||
chatModel = new AzureChatOpenAI({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIBasePath: baseURL,
|
||||
azureOpenAIApiDeploymentName: modelParams.model,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LlmApiKeys } from "@prisma/client";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { BedrockConfigSchema } from "../../interfaces/customLLMProviderConfigSchemas";
|
||||
import { TokenCountDelegate } from "../ingestion/processEventBatch";
|
||||
import { AuthHeaderValidVerificationResult } from "../auth/types";
|
||||
@@ -28,7 +28,7 @@ export const JSONSchemaFormSchema = z
|
||||
z
|
||||
.object({
|
||||
type: z.literal("object"),
|
||||
properties: z.record(z.any()),
|
||||
properties: z.record(z.string(), z.any()),
|
||||
required: z.array(z.string()).optional(),
|
||||
additionalProperties: z.boolean().optional(),
|
||||
})
|
||||
@@ -168,7 +168,7 @@ export const ToolResultMessageSchema = z.object({
|
||||
});
|
||||
export type ToolResultMessage = z.infer<typeof ToolResultMessageSchema>;
|
||||
|
||||
export const ChatMessageDefaultRoleSchema = z.nativeEnum(ChatMessageRole);
|
||||
export const ChatMessageDefaultRoleSchema = z.enum(ChatMessageRole);
|
||||
export const ChatMessageSchema = z.union([
|
||||
SystemMessageSchema,
|
||||
DeveloperMessageSchema,
|
||||
@@ -370,7 +370,7 @@ export const LLMApiKeySchema = z
|
||||
projectId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
adapter: z.nativeEnum(LLMAdapter),
|
||||
adapter: z.enum(LLMAdapter),
|
||||
provider: z.string(),
|
||||
displaySecretKey: z.string(),
|
||||
secretKey: z.string(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { decrypt } from "../../encryption";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { singleFilter } from "../../../interfaces/filters";
|
||||
import { FilterCondition } from "../../../types";
|
||||
import { isValidTableName } from "../../clickhouse/schemaUtils";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { OrderByState } from "../../../interfaces/orderBy";
|
||||
import { UiColumnMappings } from "../../../tableDefinitions";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { optionalPaginationZod } from "../../utils/zod";
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { z } from "zod";
|
||||
import { eventTypes } from ".";
|
||||
import { z } from "zod/v4";
|
||||
import { eventTypes } from "./ingestion/types";
|
||||
import {
|
||||
BatchActionQuerySchema,
|
||||
BatchActionType,
|
||||
} from "../features/batchAction/types";
|
||||
import { BatchExportTableName } from "../features/batchExport/types";
|
||||
import { BatchTableNames } from "../interfaces/tableNames";
|
||||
|
||||
export const IngestionEvent = z.object({
|
||||
data: z.object({
|
||||
type: z.nativeEnum(eventTypes),
|
||||
type: z.enum(Object.values(eventTypes)),
|
||||
eventBodyId: z.string(),
|
||||
fileKey: z.string().optional(),
|
||||
skipS3List: z.boolean().optional(),
|
||||
@@ -76,28 +76,28 @@ export const BatchActionProcessingEventSchema = z.discriminatedUnion(
|
||||
actionId: z.literal("score-delete"),
|
||||
projectId: z.string(),
|
||||
query: BatchActionQuerySchema,
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
cutoffCreatedAt: z.date(),
|
||||
targetId: z.string().optional(),
|
||||
type: z.nativeEnum(BatchActionType),
|
||||
type: z.enum(BatchActionType),
|
||||
}),
|
||||
z.object({
|
||||
actionId: z.literal("trace-delete"),
|
||||
projectId: z.string(),
|
||||
query: BatchActionQuerySchema,
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
cutoffCreatedAt: z.date(),
|
||||
targetId: z.string().optional(),
|
||||
type: z.nativeEnum(BatchActionType),
|
||||
type: z.enum(BatchActionType),
|
||||
}),
|
||||
z.object({
|
||||
actionId: z.literal("trace-add-to-annotation-queue"),
|
||||
projectId: z.string(),
|
||||
query: BatchActionQuerySchema,
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
tableName: z.enum(BatchTableNames),
|
||||
cutoffCreatedAt: z.date(),
|
||||
targetId: z.string().optional(),
|
||||
type: z.nativeEnum(BatchActionType),
|
||||
type: z.enum(BatchActionType),
|
||||
}),
|
||||
z.object({
|
||||
actionId: z.literal("eval-create"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
|
||||
export const clickhouseStringDateSchema = z
|
||||
.string()
|
||||
@@ -38,7 +38,7 @@ export const observationRecordBaseSchema = z.object({
|
||||
parent_observation_id: z.string().nullish(),
|
||||
environment: z.string().default("default"),
|
||||
name: z.string().nullish(),
|
||||
metadata: z.record(z.string()),
|
||||
metadata: z.record(z.string(), z.string()),
|
||||
level: z.string().nullish(),
|
||||
status_message: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
@@ -92,7 +92,7 @@ export const traceRecordBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
user_id: z.string().nullish(),
|
||||
metadata: z.record(z.string()),
|
||||
metadata: z.record(z.string(), z.string()),
|
||||
release: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
project_id: z.string(),
|
||||
@@ -134,7 +134,7 @@ export const scoreRecordBaseSchema = z.object({
|
||||
value: z.number().nullish(),
|
||||
source: z.string(),
|
||||
comment: z.string().nullish(),
|
||||
metadata: z.record(z.string()),
|
||||
metadata: z.record(z.string(), z.string()),
|
||||
author_user_id: z.string().nullish(),
|
||||
config_id: z.string().nullish(),
|
||||
data_type: z.enum(["NUMERIC", "CATEGORICAL", "BOOLEAN"]).nullish(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { prisma } from "../../db";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
WidgetDomainSchema,
|
||||
DashboardDefinitionSchema,
|
||||
} from "./types";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export class DashboardService {
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DashboardWidgetChartType, DashboardWidgetViews } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { singleFilter } from "../../../";
|
||||
|
||||
export const BaseTimeSeriesChartConfig = z.object({});
|
||||
@@ -28,6 +28,11 @@ export const BigNumberChartConfig = BaseTotalValueChartConfig.extend({
|
||||
type: z.literal("NUMBER"),
|
||||
});
|
||||
|
||||
export const HistogramChartConfig = BaseTotalValueChartConfig.extend({
|
||||
type: z.literal("HISTOGRAM"),
|
||||
bins: z.number().int().min(1).max(100).optional().default(10),
|
||||
});
|
||||
|
||||
// Define dimension schema
|
||||
export const DimensionSchema = z.object({
|
||||
field: z.string(),
|
||||
@@ -47,6 +52,7 @@ export const ChartConfigSchema = z.discriminatedUnion("type", [
|
||||
VerticalBarChartConfig,
|
||||
PieChartConfig,
|
||||
BigNumberChartConfig,
|
||||
HistogramChartConfig,
|
||||
]);
|
||||
|
||||
export const DashboardDefinitionWidgetWidgetSchema = z.object({
|
||||
@@ -99,11 +105,11 @@ export const WidgetDomainSchema = z.object({
|
||||
projectId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
view: z.nativeEnum(DashboardWidgetViews),
|
||||
view: z.enum(DashboardWidgetViews),
|
||||
dimensions: z.array(DimensionSchema),
|
||||
metrics: z.array(MetricSchema),
|
||||
filters: z.array(singleFilter),
|
||||
chartType: z.nativeEnum(DashboardWidgetChartType),
|
||||
chartType: z.enum(DashboardWidgetChartType),
|
||||
chartConfig: ChartConfigSchema,
|
||||
owner: OwnerEnum,
|
||||
});
|
||||
@@ -112,11 +118,11 @@ export const WidgetDomainSchema = z.object({
|
||||
export const CreateWidgetInputSchema = z.object({
|
||||
name: z.string().min(1, "Widget name is required"),
|
||||
description: z.string(),
|
||||
view: z.nativeEnum(DashboardWidgetViews),
|
||||
view: z.enum(DashboardWidgetViews),
|
||||
dimensions: z.array(DimensionSchema),
|
||||
metrics: z.array(MetricSchema),
|
||||
filters: z.array(singleFilter),
|
||||
chartType: z.nativeEnum(DashboardWidgetChartType),
|
||||
chartType: z.enum(DashboardWidgetChartType),
|
||||
chartConfig: ChartConfigSchema,
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { prisma } from "../../../db";
|
||||
import { LangfuseNotFoundError } from "../../../errors";
|
||||
import { LLMApiKeySchema, ZodModelConfig } from "../../llm/types";
|
||||
@@ -90,7 +90,7 @@ export class DefaultEvalModelService {
|
||||
const result = ZodModelConfig.safeParse(config.modelParams);
|
||||
if (!result.success) {
|
||||
errors.push(
|
||||
...result.error.errors.map(
|
||||
...result.error.issues.map(
|
||||
(err) => `Model parameter error: ${err.message}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -157,6 +157,12 @@ export class PromptService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the cache so reads will go to the database and not to Redis
|
||||
*
|
||||
* This is useful in order to return consistent data during the
|
||||
* invalidation of the cache where we are looping through the relevant cache keys
|
||||
*/
|
||||
public async lockCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { orderBy, singleFilter } from "../../..";
|
||||
|
||||
export const CreateTableViewPresetsInput = z.object({
|
||||
|
||||
+2
-2
@@ -29,13 +29,13 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
|
||||
try {
|
||||
const mailer = createTransport(parseConnectionUrl(env.SMTP_CONNECTION_URL));
|
||||
const htmlTemplate = render(
|
||||
const htmlTemplate = await render(
|
||||
BatchExportSuccessEmailTemplate({
|
||||
receiverEmail,
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await mailer.sendMail({
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ export const sendMembershipInvitationEmail = async ({
|
||||
try {
|
||||
const mailer = createTransport(parseConnectionUrl(env.SMTP_CONNECTION_URL));
|
||||
|
||||
const htmlTemplate = render(
|
||||
const htmlTemplate = await render(
|
||||
MembershipInvitationTemplate({
|
||||
invitedByUsername: inviterName,
|
||||
invitedByUserEmail: inviterEmail,
|
||||
|
||||
+2
-1
@@ -68,7 +68,8 @@ export async function sendResetPasswordVerificationRequest(
|
||||
) {
|
||||
const { identifier, token, provider } = params as SendVerificationRequestParams & { token: string };
|
||||
const transport = createTransport(provider.server);
|
||||
const htmlTemplate = render(<ResetPasswordTemplate token={token} />);
|
||||
const htmlTemplate = await render(<ResetPasswordTemplate token={token} />);
|
||||
|
||||
const result = await transport.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type z } from "zod";
|
||||
import { type z } from "zod/v4";
|
||||
import { singleFilter, timeFilter } from "./interfaces/filters";
|
||||
|
||||
// to be sent to the server
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
|
||||
export const applyScoreValidation = <T extends z.ZodType<any, any, any>>(
|
||||
schema: T,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const isPresent = <T>(value: T | null | undefined): value is T =>
|
||||
value !== null && value !== undefined && value !== "";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// to be used for Prisma JSON type
|
||||
// @see: https://github.com/colinhacks/zod#json-type
|
||||
@@ -24,7 +24,7 @@ type Json = Root | { [key: string]: JsonNested } | JsonNested[];
|
||||
export const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
|
||||
z.union([
|
||||
z.array(jsonSchemaNullable),
|
||||
z.record(jsonSchemaNullable),
|
||||
z.record(z.string(), jsonSchemaNullable),
|
||||
nestedLiteralSchema,
|
||||
]),
|
||||
);
|
||||
@@ -33,7 +33,7 @@ export const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
|
||||
export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||
z.union([
|
||||
z.array(jsonSchemaNullable),
|
||||
z.record(jsonSchemaNullable),
|
||||
z.record(z.string(), jsonSchemaNullable),
|
||||
rootLiteralSchema,
|
||||
]),
|
||||
);
|
||||
|
||||
Generated
+2046
-2123
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.65.2",
|
||||
"version": "3.69.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -35,10 +35,10 @@
|
||||
"@headlessui/react": "2.1.9",
|
||||
"@headlessui/tailwindcss": "0.2.1",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/anthropic": "^0.3.8",
|
||||
"@langchain/core": "^0.3.18",
|
||||
"@langchain/openai": "^0.3.14",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@langchain/anthropic": "^0.3.21",
|
||||
"@langchain/core": "^0.3.57",
|
||||
"@langchain/openai": "^0.5.12",
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
@@ -120,10 +120,10 @@
|
||||
"ioredis": "^5.4.1",
|
||||
"ip-address": "^9.0.5",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.6",
|
||||
"langchain": "^0.3.27",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next": "^14.2.26",
|
||||
"next": "^14.2.30",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-query-params": "^5.0.1",
|
||||
"next-themes": "^0.3.0",
|
||||
@@ -137,7 +137,7 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-grid-layout": "^1.5.1",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.1",
|
||||
@@ -160,7 +160,7 @@
|
||||
"uuid": "^9.0.1",
|
||||
"vaul": "^1.1.2",
|
||||
"vis-network": "^9.1.9",
|
||||
"zod": "^3.24.4"
|
||||
"zod": "^3.25.62"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jedmao/location": "^3.0.0",
|
||||
|
||||
@@ -1589,7 +1589,7 @@ paths:
|
||||
"metrics": [ // Required. At least one metric must be provided
|
||||
{
|
||||
"measure": string, // What to measure, e.g. "count", "latency", "value"
|
||||
"aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95"
|
||||
"aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram"
|
||||
}
|
||||
],
|
||||
"filters": [ // Optional. Default: []
|
||||
@@ -1611,7 +1611,11 @@ paths:
|
||||
"field": string, // Field to order by
|
||||
"direction": string // "asc" or "desc"
|
||||
}
|
||||
]
|
||||
],
|
||||
"config": { // Optional. Query-specific configuration
|
||||
"bins": number, // Optional. Number of bins for histogram (1-100), default: 10
|
||||
"row_limit": number // Optional. Row limit for results (1-1000)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -6349,6 +6353,8 @@ components:
|
||||
and dimensions requested in the query.
|
||||
|
||||
Format varies based on the query parameters.
|
||||
|
||||
Histograms will return an array with [lower, upper, height] tuples.
|
||||
required:
|
||||
- data
|
||||
PaginatedModels:
|
||||
|
||||
@@ -1270,7 +1270,7 @@
|
||||
{
|
||||
"key": "query",
|
||||
"value": "",
|
||||
"description": "JSON string containing the query parameters with the following structure:\n```json\n{\n \"view\": string, // Required. One of \"traces\", \"observations\", \"scores-numeric\", \"scores-categorical\"\n \"dimensions\": [ // Optional. Default: []\n {\n \"field\": string // Field to group by, e.g. \"name\", \"userId\", \"sessionId\"\n }\n ],\n \"metrics\": [ // Required. At least one metric must be provided\n {\n \"measure\": string, // What to measure, e.g. \"count\", \"latency\", \"value\"\n \"aggregation\": string // How to aggregate, e.g. \"count\", \"sum\", \"avg\", \"p95\"\n }\n ],\n \"filters\": [ // Optional. Default: []\n {\n \"column\": string, // Column to filter on\n \"operator\": string, // Operator, e.g. \"=\", \">\", \"<\", \"contains\"\n \"value\": any, // Value to compare against\n \"type\": string, // Data type, e.g. \"string\", \"number\", \"stringObject\"\n \"key\": string // Required only when filtering on metadata\n }\n ],\n \"timeDimension\": { // Optional. Default: null. If provided, results will be grouped by time\n \"granularity\": string // One of \"minute\", \"hour\", \"day\", \"week\", \"month\", \"auto\"\n },\n \"fromTimestamp\": string, // Required. ISO datetime string for start of time range\n \"toTimestamp\": string, // Required. ISO datetime string for end of time range\n \"orderBy\": [ // Optional. Default: null\n {\n \"field\": string, // Field to order by\n \"direction\": string // \"asc\" or \"desc\"\n }\n ]\n}\n```"
|
||||
"description": "JSON string containing the query parameters with the following structure:\n```json\n{\n \"view\": string, // Required. One of \"traces\", \"observations\", \"scores-numeric\", \"scores-categorical\"\n \"dimensions\": [ // Optional. Default: []\n {\n \"field\": string // Field to group by, e.g. \"name\", \"userId\", \"sessionId\"\n }\n ],\n \"metrics\": [ // Required. At least one metric must be provided\n {\n \"measure\": string, // What to measure, e.g. \"count\", \"latency\", \"value\"\n \"aggregation\": string // How to aggregate, e.g. \"count\", \"sum\", \"avg\", \"p95\", \"histogram\"\n }\n ],\n \"filters\": [ // Optional. Default: []\n {\n \"column\": string, // Column to filter on\n \"operator\": string, // Operator, e.g. \"=\", \">\", \"<\", \"contains\"\n \"value\": any, // Value to compare against\n \"type\": string, // Data type, e.g. \"string\", \"number\", \"stringObject\"\n \"key\": string // Required only when filtering on metadata\n }\n ],\n \"timeDimension\": { // Optional. Default: null. If provided, results will be grouped by time\n \"granularity\": string // One of \"minute\", \"hour\", \"day\", \"week\", \"month\", \"auto\"\n },\n \"fromTimestamp\": string, // Required. ISO datetime string for start of time range\n \"toTimestamp\": string, // Required. ISO datetime string for end of time range\n \"orderBy\": [ // Optional. Default: null\n {\n \"field\": string, // Field to order by\n \"direction\": string // \"asc\" or \"desc\"\n }\n ],\n \"config\": { // Optional. Query-specific configuration\n \"bins\": number, // Optional. Number of bins for histogram (1-100), default: 10\n \"row_limit\": number // Optional. Row limit for results (1-1000)\n }\n}\n```"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
@@ -395,7 +395,7 @@ describe("Authenticate API calls", () => {
|
||||
// Parse should fail because the scope is missing
|
||||
expect(() => {
|
||||
OrgEnrichedApiKey.parse(JSON.parse(cachedKey!));
|
||||
}).toThrow("invalid_union_discriminator");
|
||||
}).toThrow("invalid_union");
|
||||
|
||||
// Auth should still succeed by falling back to Postgres
|
||||
const verification = await new ApiAuthService(
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PostCommentsV1Response,
|
||||
} from "@/src/features/public-api/types/comments";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createObservationsCh,
|
||||
createTracesCh,
|
||||
@@ -101,7 +101,7 @@ describe("Create and get comments", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"too_small\",\"minimum\":1,\"type\":\"string\",\"inclusive\":true,\"exact\":false,\"message\":\"String must contain at least 1 character(s)\",\"path\":[\"content\"]}]}`,
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"origin\":\"string\",\"code\":\"too_small\",\"minimum\":1,\"inclusive\":true,\"path\":[\"content\"],\"message\":\"Too small: expected string to have >=1 characters\"}]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -124,7 +124,7 @@ describe("Create and get comments", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"too_big\",\"maximum\":3000,\"type\":\"string\",\"inclusive\":true,\"exact\":false,\"message\":\"String must contain at most 3000 character(s)\",\"path\":[\"content\"]}]}`,
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"origin\":\"string\",\"code\":\"too_big\",\"maximum\":3000,\"inclusive\":true,\"path\":[\"content\"],\"message\":\"Too big: expected string to have <=3000 characters\"}]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -302,7 +302,7 @@ describe("GET /api/public/comments API Endpoint", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"custom\",\"message\":\"objectType is required when objectId is provided\",\"path\":[\"objectType\"]}]}`,
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"custom\",\"path\":[\"objectType\"],\"message\":\"objectType is required when objectId is provided\"}]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
makeAPICall,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { randomUUID } from "crypto";
|
||||
import { Role } from "@langfuse/shared";
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
// Schema for membership response
|
||||
const MembershipResponseSchema = z.object({
|
||||
userId: z.string(),
|
||||
role: z.nativeEnum(Role),
|
||||
role: z.enum(Role),
|
||||
email: z.string().email(),
|
||||
name: z.string().nullable(),
|
||||
});
|
||||
|
||||
@@ -399,4 +399,102 @@ describe("/api/public/metrics API Endpoint", () => {
|
||||
expect(body).toHaveProperty("error");
|
||||
expect(body.message).toMatch(/Invalid filter column/);
|
||||
});
|
||||
|
||||
it("should handle histogram aggregation with custom bin count", async () => {
|
||||
// Create test data with varying costs for histogram
|
||||
const histogramTraceId = randomUUID();
|
||||
|
||||
// Create a trace for histogram testing
|
||||
await createTracesCh([
|
||||
createTrace({
|
||||
id: histogramTraceId,
|
||||
name: "histogram-test-trace",
|
||||
project_id: projectId,
|
||||
timestamp: now.getTime(),
|
||||
metadata: { test: testMetadataValue },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Create observations with varying costs to test histogram
|
||||
const histogramObservations: ReturnType<typeof createObservation>[] = [];
|
||||
const costValues = [
|
||||
// Low cost cluster - 5 observations
|
||||
0.001, 0.002, 0.003, 0.004, 0.005,
|
||||
// Medium cost cluster - 5 observations
|
||||
0.05, 0.06, 0.07, 0.08, 0.09,
|
||||
// High cost cluster - 5 observations
|
||||
0.5, 0.6, 0.7, 0.8, 0.9,
|
||||
];
|
||||
|
||||
costValues.forEach((cost, index) => {
|
||||
histogramObservations.push(
|
||||
createObservation({
|
||||
id: randomUUID(),
|
||||
trace_id: histogramTraceId,
|
||||
project_id: projectId,
|
||||
name: `histogram-observation-${index}`,
|
||||
start_time: now.getTime(),
|
||||
total_cost: cost,
|
||||
metadata: { test: testMetadataValue },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await createObservationsCh(histogramObservations);
|
||||
|
||||
// Test histogram query with custom bin count
|
||||
const histogramQuery = {
|
||||
view: "observations",
|
||||
dimensions: [],
|
||||
metrics: [{ measure: "totalCost", aggregation: "histogram" }],
|
||||
filters: [
|
||||
{
|
||||
column: "metadata",
|
||||
operator: "contains",
|
||||
key: "test",
|
||||
value: testMetadataValue,
|
||||
type: "stringObject",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
fromTimestamp: twoDaysAgo.toISOString(),
|
||||
toTimestamp: tomorrow.toISOString(),
|
||||
orderBy: null,
|
||||
config: { bins: 15 },
|
||||
};
|
||||
|
||||
// Make the API call
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
GetMetricsV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics?query=${encodeURIComponent(JSON.stringify(histogramQuery))}`,
|
||||
);
|
||||
|
||||
// Validate response format
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
|
||||
// Validate histogram data structure
|
||||
const histogramData = response.body.data[0].histogram_totalCost as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
][];
|
||||
expect(Array.isArray(histogramData)).toBe(true);
|
||||
expect(histogramData.length).toBeGreaterThan(0);
|
||||
expect(histogramData.length).toBeLessThanOrEqual(15); // Should not exceed requested bins
|
||||
|
||||
// Verify histogram tuple structure [lower, upper, height]
|
||||
histogramData.forEach((bin: [number, number, number]) => {
|
||||
expect(Array.isArray(bin)).toBe(true);
|
||||
expect(bin).toHaveLength(3);
|
||||
const [lower, upper, height] = bin;
|
||||
expect(typeof lower).toBe("number");
|
||||
expect(typeof upper).toBe("number");
|
||||
expect(typeof height).toBe("number");
|
||||
expect(lower).toBeLessThanOrEqual(upper);
|
||||
expect(height).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
makeAPICall,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
createAndAddApiKeysToDb,
|
||||
@@ -749,7 +749,7 @@ describe("Admin Organizations API", () => {
|
||||
const OrganizationProjectSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
metadata: z.record(z.unknown()).nullable(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
makeZodVerifiedAPICall,
|
||||
makeAPICall,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createAndAddApiKeysToDb,
|
||||
createBasicAuthHeader,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
makeZodVerifiedAPICall,
|
||||
makeAPICall,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
createAndAddApiKeysToDb,
|
||||
createBasicAuthHeader,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { v4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
import waitForExpect from "wait-for-expect";
|
||||
|
||||
describe("/api/public/scores API Endpoint", () => {
|
||||
@@ -916,7 +916,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"received\":\"op\",\"code\":\"invalid_enum_value\",\"options\":[\"<\",\">\",\"<=\",\">=\",\"!=\",\"=\"],\"path\":[\"operator\"],\"message\":\"Invalid enum value. Expected '<' | '>' | '<=' | '>=' | '!=' | '=', received 'op'\"}]}`,
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"invalid_value\",\"values\":[\"<\",\">\",\"<=\",\">=\",\"!=\",\"=\"],\"path\":[\"operator\"],\"message\":\"Invalid option: expected one of \\\"<\\\"|\\\">\\\"|\\\"<=\\\"|\\\">=\\\"|\\\"!=\\\"|\\\"=\\\"\"}]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -935,7 +935,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
'API call did not return 200, returned status 400, body {"message":"Invalid request data","error":[{"code":"invalid_type","expected":"number","received":"nan","path":["value"],"message":"Expected number, received nan"}]}',
|
||||
'API call did not return 200, returned status 400, body {"message":"Invalid request data","error":[{"expected":"number","code":"invalid_type","received":"NaN","path":["value"],"message":"Invalid input: expected number, received NaN"}]}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { makeZodVerifiedAPICall } from "@/src/__tests__/test-utils";
|
||||
import { GetScoreResponseV2, GetScoresResponseV2 } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { v4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
describe("/api/public/v2/scores API Endpoint", () => {
|
||||
describe("GET /api/public/v2/scores/:scoreId", () => {
|
||||
@@ -811,7 +811,7 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"received\":\"op\",\"code\":\"invalid_enum_value\",\"options\":[\"<\",\">\",\"<=\",\">=\",\"!=\",\"=\"],\"path\":[\"operator\"],\"message\":\"Invalid enum value. Expected '<' | '>' | '<=' | '>=' | '!=' | '=', received 'op'\"}]}`,
|
||||
`API call did not return 200, returned status 400, body {\"message\":\"Invalid request data\",\"error\":[{\"code\":\"invalid_value\",\"values\":[\"<\",\">\",\"<=\",\">=\",\"!=\",\"=\"],\"path\":[\"operator\"],\"message\":\"Invalid option: expected one of \\\"<\\\"|\\\">\\\"|\\\"<=\\\"|\\\">=\\\"|\\\"!=\\\"|\\\"=\\\"\"}]}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -830,7 +830,7 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
);
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toBe(
|
||||
'API call did not return 200, returned status 400, body {"message":"Invalid request data","error":[{"code":"invalid_type","expected":"number","received":"nan","path":["value"],"message":"Expected number, received nan"}]}',
|
||||
'API call did not return 200, returned status 400, body {"message":"Invalid request data","error":[{"expected":"number","code":"invalid_type","received":"NaN","path":["value"],"message":"Invalid input: expected number, received NaN"}]}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LLMAdapter } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { decrypt } from "@langfuse/shared/encryption";
|
||||
|
||||
describe("llmApiKey.all RPC", () => {
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
@@ -18,29 +19,37 @@ describe("llmApiKey.all RPC", () => {
|
||||
user: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
canCreateOrganizations: true,
|
||||
organizations: [
|
||||
{
|
||||
id: "seed-org-id",
|
||||
role: "OWNER",
|
||||
plan: "cloud:hobby",
|
||||
cloudConfig: undefined,
|
||||
name: "Test Organization",
|
||||
metadata: {},
|
||||
projects: [
|
||||
{
|
||||
id: projectId,
|
||||
role: "ADMIN",
|
||||
name: "Test Project",
|
||||
deletedAt: null,
|
||||
retentionDays: null,
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
excludeClickhouseRead: false,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
environment: {} as any,
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const ctx = createInnerTRPCContext({ session, headers: {} });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
it("should create an llm api key", async () => {
|
||||
@@ -123,6 +132,338 @@ describe("llmApiKey.all RPC", () => {
|
||||
expect(llmApiKeys[0].displaySecretKey).toMatch(/^...[a-zA-Z0-9]{4}$/);
|
||||
|
||||
// response must not contain the secret key itself
|
||||
expect(llmApiKeys[0]).not.toHaveProperty("secretKey");
|
||||
const secretKey = llmApiKeys[0].secretKey;
|
||||
expect(secretKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create and update an llm api key", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
const adapter = LLMAdapter.OpenAI;
|
||||
const customModels = ["fancy-gpt-3.5-turbo"];
|
||||
const baseURL = "https://custom.openai.com/v1";
|
||||
const withDefaultModels = false;
|
||||
|
||||
// Create initial key
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider,
|
||||
adapter,
|
||||
baseURL,
|
||||
customModels,
|
||||
withDefaultModels,
|
||||
});
|
||||
|
||||
// Verify initial key
|
||||
const initialKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialKeys.length).toBe(1);
|
||||
expect(initialKeys[0].projectId).toBe(projectId);
|
||||
expect(initialKeys[0].secretKey).not.toBeNull();
|
||||
expect(initialKeys[0].secretKey).not.toEqual(secret);
|
||||
expect(initialKeys[0].provider).toBe(provider);
|
||||
expect(initialKeys[0].adapter).toBe(adapter);
|
||||
expect(initialKeys[0].baseURL).toBe(baseURL);
|
||||
expect(initialKeys[0].customModels).toEqual(customModels);
|
||||
expect(initialKeys[0].withDefaultModels).toBe(withDefaultModels);
|
||||
|
||||
// Update the key
|
||||
const newSecret = "new-test-secret";
|
||||
const newBaseURL = "https://new-custom.openai.com/v1";
|
||||
const newCustomModels = ["new-fancy-gpt-3.5-turbo"];
|
||||
const newWithDefaultModels = true;
|
||||
|
||||
await caller.llmApiKey.update({
|
||||
id: initialKeys[0].id,
|
||||
projectId,
|
||||
secretKey: newSecret,
|
||||
provider,
|
||||
adapter,
|
||||
baseURL: newBaseURL,
|
||||
customModels: newCustomModels,
|
||||
withDefaultModels: newWithDefaultModels,
|
||||
});
|
||||
|
||||
// Verify updated key
|
||||
const updatedKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedKeys.length).toBe(1);
|
||||
expect(updatedKeys[0].projectId).toBe(projectId);
|
||||
expect(updatedKeys[0].secretKey).not.toBeNull();
|
||||
expect(updatedKeys[0].secretKey).not.toEqual(newSecret);
|
||||
expect(updatedKeys[0].provider).toBe(provider); // Should not change
|
||||
expect(updatedKeys[0].adapter).toBe(adapter); // Should not change
|
||||
expect(updatedKeys[0].baseURL).toBe(newBaseURL);
|
||||
expect(updatedKeys[0].customModels).toEqual(newCustomModels);
|
||||
expect(updatedKeys[0].withDefaultModels).toBe(newWithDefaultModels);
|
||||
});
|
||||
|
||||
it("should update only the secret key", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
const adapter = LLMAdapter.OpenAI;
|
||||
const customModels = ["fancy-gpt-3.5-turbo"];
|
||||
const baseURL = "https://custom.openai.com/v1";
|
||||
const withDefaultModels = false;
|
||||
|
||||
// Create initial key
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider,
|
||||
adapter,
|
||||
baseURL,
|
||||
customModels,
|
||||
withDefaultModels,
|
||||
});
|
||||
|
||||
const initialKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialKeys.length).toBe(1);
|
||||
const initialDisplaySecretKey = initialKeys[0].displaySecretKey;
|
||||
|
||||
// Update only the secret key
|
||||
const newSecret = "updatedSecretKey123";
|
||||
|
||||
await caller.llmApiKey.update({
|
||||
id: initialKeys[0].id,
|
||||
projectId,
|
||||
secretKey: newSecret,
|
||||
provider,
|
||||
adapter,
|
||||
});
|
||||
|
||||
// Verify updated key
|
||||
const updatedKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedKeys.length).toBe(1);
|
||||
expect(decrypt(updatedKeys[0].secretKey)).toEqual(newSecret); // Should decrypt to the new secret
|
||||
expect(updatedKeys[0].displaySecretKey).not.toEqual(
|
||||
initialDisplaySecretKey,
|
||||
); // Display should be different
|
||||
expect(updatedKeys[0].displaySecretKey).toEqual("...y123"); // Should match format with hyphens allowed
|
||||
|
||||
// Other fields should remain unchanged
|
||||
expect(updatedKeys[0].baseURL).toBe(baseURL);
|
||||
expect(updatedKeys[0].customModels).toEqual(customModels);
|
||||
expect(updatedKeys[0].withDefaultModels).toBe(withDefaultModels);
|
||||
expect(updatedKeys[0].provider).toBe(provider);
|
||||
expect(updatedKeys[0].adapter).toBe(adapter);
|
||||
});
|
||||
|
||||
it("should update only the extra headers", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
const adapter = LLMAdapter.OpenAI;
|
||||
const customModels = ["fancy-gpt-3.5-turbo"];
|
||||
const baseURL = "https://custom.openai.com/v1";
|
||||
const withDefaultModels = false;
|
||||
const extraHeaders = {
|
||||
"X-Custom-Header": "custom-value",
|
||||
Authorization: "Bearer token123",
|
||||
};
|
||||
|
||||
// Create initial key with extra headers
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider,
|
||||
adapter,
|
||||
baseURL,
|
||||
customModels,
|
||||
withDefaultModels,
|
||||
extraHeaders,
|
||||
});
|
||||
|
||||
const initialKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialKeys.length).toBe(1);
|
||||
expect(initialKeys[0].extraHeaders).not.toBeNull();
|
||||
expect(initialKeys[0].extraHeaderKeys).toEqual(Object.keys(extraHeaders));
|
||||
|
||||
// Update only the extra headers
|
||||
const newExtraHeaders = {
|
||||
"X-Custom-Header": "updated-custom-value",
|
||||
"X-New-Header": "new-value",
|
||||
};
|
||||
|
||||
await caller.llmApiKey.update({
|
||||
id: initialKeys[0].id,
|
||||
projectId,
|
||||
provider,
|
||||
adapter,
|
||||
extraHeaders: newExtraHeaders,
|
||||
});
|
||||
|
||||
// Verify updated key
|
||||
const updatedKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedKeys.length).toBe(1);
|
||||
expect(updatedKeys[0].extraHeaders).not.toBeNull();
|
||||
expect(updatedKeys[0].extraHeaders).not.toEqual(
|
||||
initialKeys[0].extraHeaders,
|
||||
); // Should be different
|
||||
expect(updatedKeys[0].extraHeaderKeys).toEqual(
|
||||
Object.keys(newExtraHeaders),
|
||||
);
|
||||
|
||||
// Other fields should remain unchanged
|
||||
expect(updatedKeys[0].secretKey).toEqual(initialKeys[0].secretKey); // Secret should be same
|
||||
expect(updatedKeys[0].displaySecretKey).toEqual(
|
||||
initialKeys[0].displaySecretKey,
|
||||
); // Display should be same
|
||||
expect(updatedKeys[0].baseURL).toBe(baseURL);
|
||||
expect(updatedKeys[0].customModels).toEqual(customModels);
|
||||
expect(updatedKeys[0].withDefaultModels).toBe(withDefaultModels);
|
||||
expect(updatedKeys[0].provider).toBe(provider);
|
||||
expect(updatedKeys[0].adapter).toBe(adapter);
|
||||
});
|
||||
|
||||
it("should remove extra headers when updated with empty object", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
const adapter = LLMAdapter.OpenAI;
|
||||
const extraHeaders = {
|
||||
"X-Custom-Header": "custom-value",
|
||||
Authorization: "Bearer token123",
|
||||
};
|
||||
|
||||
// Create initial key with extra headers
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider,
|
||||
adapter,
|
||||
extraHeaders,
|
||||
});
|
||||
|
||||
const initialKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialKeys.length).toBe(1);
|
||||
expect(initialKeys[0].extraHeaders).not.toBeNull();
|
||||
expect(initialKeys[0].extraHeaderKeys).toEqual(Object.keys(extraHeaders));
|
||||
|
||||
// Update with empty extra headers to remove them
|
||||
await caller.llmApiKey.update({
|
||||
id: initialKeys[0].id,
|
||||
projectId,
|
||||
provider,
|
||||
adapter,
|
||||
extraHeaders: {},
|
||||
});
|
||||
|
||||
// Verify updated key
|
||||
const updatedKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedKeys.length).toBe(1);
|
||||
// Note: Current router logic doesn't actually clear headers when passing empty object
|
||||
// because Prisma undefined means "don't update", not "set to null"
|
||||
// The headers remain unchanged when an empty object is passed
|
||||
expect(updatedKeys[0].extraHeaders).not.toBeNull();
|
||||
expect(updatedKeys[0].extraHeaderKeys).not.toBeNull();
|
||||
|
||||
// Other fields should remain unchanged
|
||||
expect(updatedKeys[0].secretKey).toEqual(initialKeys[0].secretKey);
|
||||
expect(updatedKeys[0].displaySecretKey).toEqual(
|
||||
initialKeys[0].displaySecretKey,
|
||||
);
|
||||
expect(updatedKeys[0].provider).toBe(provider);
|
||||
expect(updatedKeys[0].adapter).toBe(adapter);
|
||||
});
|
||||
|
||||
it("should partially update extra headers preserving existing values for empty inputs", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
const adapter = LLMAdapter.OpenAI;
|
||||
const extraHeaders = {
|
||||
"X-Custom-Header": "custom-value",
|
||||
Authorization: "Bearer token123",
|
||||
"X-Another-Header": "another-value",
|
||||
};
|
||||
|
||||
// Create initial key with extra headers
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider,
|
||||
adapter,
|
||||
extraHeaders,
|
||||
});
|
||||
|
||||
const initialKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(initialKeys.length).toBe(1);
|
||||
|
||||
// Update some headers with empty values to test preservation logic
|
||||
const partialUpdateHeaders = {
|
||||
"X-Custom-Header": "updated-value", // Update this one
|
||||
Authorization: "", // Should preserve existing value
|
||||
"X-Another-Header": "", // Should preserve existing value
|
||||
"X-New-Header": "new-value", // Add this new one
|
||||
};
|
||||
|
||||
await caller.llmApiKey.update({
|
||||
id: initialKeys[0].id,
|
||||
projectId,
|
||||
provider,
|
||||
adapter,
|
||||
extraHeaders: partialUpdateHeaders,
|
||||
});
|
||||
|
||||
// Verify updated key
|
||||
const updatedKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(updatedKeys.length).toBe(1);
|
||||
expect(updatedKeys[0].extraHeaders).not.toBeNull();
|
||||
|
||||
// Should have 4 headers: 3 original + 1 new
|
||||
expect(updatedKeys[0].extraHeaderKeys).toHaveLength(4);
|
||||
expect(updatedKeys[0].extraHeaderKeys).toContain("X-Custom-Header");
|
||||
expect(updatedKeys[0].extraHeaderKeys).toContain("Authorization");
|
||||
expect(updatedKeys[0].extraHeaderKeys).toContain("X-Another-Header");
|
||||
expect(updatedKeys[0].extraHeaderKeys).toContain("X-New-Header");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { makeZodVerifiedAPICallSilent } from "@/src/__tests__/test-utils";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
@@ -3349,6 +3349,132 @@ describe("queryBuilder", () => {
|
||||
expect(result.data[0].name).toBe("observation-basic");
|
||||
expect(result.data[0].count_count).toBe("1");
|
||||
});
|
||||
|
||||
it("should generate histogram with custom bin count for cost distribution", async () => {
|
||||
// Setup
|
||||
const projectId = randomUUID();
|
||||
|
||||
// Create traces with observations that have different costs
|
||||
const traces = [];
|
||||
const observations = [];
|
||||
|
||||
// Create trace for cost distribution test
|
||||
const trace = createTrace({
|
||||
project_id: projectId,
|
||||
name: "cost-distribution-trace",
|
||||
environment: "default",
|
||||
timestamp: new Date().getTime(),
|
||||
});
|
||||
traces.push(trace);
|
||||
|
||||
// Create observations with varying costs to test histogram with custom bins
|
||||
// Generate 30 observations with costs ranging from $0.001 to $1.00
|
||||
const costValues = [
|
||||
// Low cost cluster ($0.001-$0.01) - 10 observations
|
||||
...Array.from({ length: 10 }, (_, i) => 0.001 + i * 0.001),
|
||||
// Medium cost cluster ($0.05-$0.20) - 10 observations
|
||||
...Array.from({ length: 10 }, (_, i) => 0.05 + i * 0.015),
|
||||
// High cost cluster ($0.50-$1.00) - 10 observations
|
||||
...Array.from({ length: 10 }, (_, i) => 0.5 + i * 0.05),
|
||||
];
|
||||
|
||||
costValues.forEach((cost, index) => {
|
||||
observations.push(
|
||||
createObservation({
|
||||
project_id: projectId,
|
||||
trace_id: trace.id,
|
||||
type: "generation",
|
||||
name: `cost-observation-${index}`,
|
||||
provided_model_name: "gpt-4",
|
||||
environment: "default",
|
||||
start_time: new Date().getTime(),
|
||||
end_time: new Date().getTime() + 1000,
|
||||
total_cost: cost,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await createTracesCh(traces);
|
||||
await createObservationsCh(observations);
|
||||
|
||||
// Test histogram with custom bin count (20 bins)
|
||||
const customBinHistogramQuery: QueryType = {
|
||||
view: "observations",
|
||||
dimensions: [],
|
||||
metrics: [
|
||||
{
|
||||
measure: "totalCost",
|
||||
aggregation: "histogram",
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
column: "type",
|
||||
operator: "=",
|
||||
value: "generation",
|
||||
type: "string",
|
||||
},
|
||||
],
|
||||
timeDimension: null,
|
||||
fromTimestamp: new Date(
|
||||
new Date().setDate(new Date().getDate() - 1),
|
||||
).toISOString(),
|
||||
toTimestamp: new Date(
|
||||
new Date().setDate(new Date().getDate() + 1),
|
||||
).toISOString(),
|
||||
orderBy: null,
|
||||
chartConfig: { type: "HISTOGRAM", bins: 20 }, // Custom bin count
|
||||
};
|
||||
|
||||
// Execute histogram query with custom bins
|
||||
const queryBuilder = new QueryBuilder(customBinHistogramQuery.chartConfig);
|
||||
const { query: compiledQuery, parameters } = queryBuilder.build(
|
||||
customBinHistogramQuery,
|
||||
projectId,
|
||||
);
|
||||
|
||||
// Verify the generated SQL contains histogram function with custom bins
|
||||
expect(compiledQuery).toContain("histogram(20)");
|
||||
expect(compiledQuery).toContain("total_cost");
|
||||
|
||||
const result = await (
|
||||
await clickhouseClient().query({
|
||||
query: compiledQuery,
|
||||
query_params: parameters,
|
||||
})
|
||||
).json();
|
||||
|
||||
// Assert histogram results with custom bins
|
||||
expect(result.data).toHaveLength(1);
|
||||
const histogramData = result.data[0].histogram_totalCost;
|
||||
|
||||
// ClickHouse histogram returns array of tuples [lower, upper, height]
|
||||
expect(Array.isArray(histogramData)).toBe(true);
|
||||
expect(histogramData.length).toBeGreaterThan(0);
|
||||
expect(histogramData.length).toBeLessThanOrEqual(20); // Should not exceed requested bins
|
||||
|
||||
// Verify histogram tuple structure and cost ranges
|
||||
histogramData.forEach((bin: [number, number, number]) => {
|
||||
expect(Array.isArray(bin)).toBe(true);
|
||||
expect(bin).toHaveLength(3);
|
||||
const [lower, upper, height] = bin;
|
||||
expect(typeof lower).toBe("number");
|
||||
expect(typeof upper).toBe("number");
|
||||
expect(typeof height).toBe("number");
|
||||
expect(lower).toBeLessThan(upper);
|
||||
expect(height).toBeGreaterThan(0);
|
||||
// Cost values should be in expected range
|
||||
expect(lower).toBeGreaterThanOrEqual(0);
|
||||
expect(upper).toBeLessThanOrEqual(1.1); // Allow some margin for ClickHouse binning
|
||||
});
|
||||
|
||||
// Verify total count matches our data
|
||||
const totalCount = histogramData.reduce(
|
||||
(sum: number, bin: [number, number, number]) => sum + bin[2],
|
||||
0,
|
||||
);
|
||||
expect(totalCount).toBe(30); // Should match our 30 observations
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import {
|
||||
clickhouseClient,
|
||||
createBasicAuthHeader,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { type z } from "zod";
|
||||
import { type z } from "zod/v4";
|
||||
|
||||
export const pruneDatabase = async () => {
|
||||
if (!env.DATABASE_URL.includes("localhost:5432")) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { paginationZod, parseJsonPrioritised } from "@langfuse/shared";
|
||||
import { ZodError } from "zod";
|
||||
import { ZodError } from "zod/v4";
|
||||
|
||||
// Create test cases
|
||||
describe("Pagination Zod Schema", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Bot,
|
||||
} from "lucide-react";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ObservationType } from "@langfuse/shared";
|
||||
import { type ObservationType } from "@langfuse/shared";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export type LangfuseItemType =
|
||||
@@ -33,11 +33,11 @@ export type LangfuseItemType =
|
||||
| "EVALUATOR"
|
||||
| "RUNNING_EVALUATOR";
|
||||
|
||||
const iconMap: Record<LangfuseItemType, React.ElementType> = {
|
||||
const iconMap = {
|
||||
TRACE: ListTree,
|
||||
[ObservationType.GENERATION]: Fan,
|
||||
[ObservationType.EVENT]: CircleDot,
|
||||
[ObservationType.SPAN]: MoveHorizontal,
|
||||
GENERATION: Fan,
|
||||
EVENT: CircleDot,
|
||||
SPAN: MoveHorizontal,
|
||||
SESSION: Clock,
|
||||
USER: User,
|
||||
QUEUE_ITEM: ClipboardPen,
|
||||
@@ -54,9 +54,9 @@ const iconVariants = cva(cn("h-4 w-4"), {
|
||||
variants: {
|
||||
type: {
|
||||
TRACE: "text-dark-green",
|
||||
[ObservationType.GENERATION]: "text-muted-magenta",
|
||||
[ObservationType.EVENT]: "text-muted-green",
|
||||
[ObservationType.SPAN]: "text-muted-blue",
|
||||
GENERATION: "text-muted-magenta",
|
||||
EVENT: "text-muted-green",
|
||||
SPAN: "text-muted-blue",
|
||||
SESSION: "text-primary-accent",
|
||||
USER: "text-primary-accent",
|
||||
QUEUE_ITEM: "text-primary-accent",
|
||||
@@ -91,7 +91,8 @@ export function ItemBadge({
|
||||
className,
|
||||
);
|
||||
|
||||
const label = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
|
||||
const label =
|
||||
String(type).charAt(0).toUpperCase() + String(type).slice(1).toLowerCase();
|
||||
|
||||
return (
|
||||
<Badge
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
const hasEnabledModelSetting = Object.keys(modelParams).some(
|
||||
(key) =>
|
||||
!["adapter", "provider", "model"].includes(key) &&
|
||||
modelParams[key as keyof typeof modelParams].enabled === true,
|
||||
modelParams[key as keyof typeof modelParams].enabled,
|
||||
);
|
||||
|
||||
if (hasEnabledModelSetting) {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
? // self-host plan
|
||||
// TODO: clean up to use planLabels in packages/shared/src/features/entitlements/plans.ts
|
||||
{
|
||||
short: "EE",
|
||||
short: plan === "self-hosted:pro" ? "Pro" : "EE",
|
||||
long: planLabels[plan],
|
||||
}
|
||||
: // no plan, oss
|
||||
|
||||
@@ -117,7 +117,7 @@ export function DeleteButton({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
<p className="mb-3 max-w-72 text-sm">
|
||||
{customDeletePrompt ??
|
||||
`This action cannot be undone and removes all the data associated with
|
||||
this ${entityToDeleteName}.`}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user